diff --git a/pytchat/parser/live.py b/pytchat/parser/live.py index c4dada8..a8cf12c 100644 --- a/pytchat/parser/live.py +++ b/pytchat/parser/live.py @@ -1,3 +1,9 @@ +""" +pytchat.parser.live +~~~~~~~~~~~~~~~~~~~ +This module is parser of live chat JSON. +""" + import json from .. import config from .. import mylogger @@ -12,6 +18,27 @@ logger = mylogger.get_logger(__name__,mode=config.LOGGER_MODE) class Parser: def parse(self, jsn): + """ + このparse関数はLiveChat._listen() 関数から定期的に呼び出される。 + 引数jsnはYoutubeから取得したチャットデータの生JSONであり、 + このparse関数によって与えられたJSONを以下に分割して返す。 + + timeout (次のチャットデータ取得までのインターバル) + + chat data(チャットデータ本体) + + continuation (次のチャットデータ取得に必要となるパラメータ). + + Parameter + ---------- + + jsn : dict + + Youtubeから取得したチャットデータのJSONオブジェクト。 + (pythonの辞書形式に変換済みの状態で渡される) + + Returns + ------- + + metadata : dict + + チャットデータに付随するメタデータ。timeout、 動画ID、continuationパラメータで構成される。 + + chatdata : list[dict] + + チャットデータ本体のリスト。 + """ if jsn is None: return {'timeoutMs':0,'continuation':None},[] if jsn['response']['responseContext'].get('errors'): diff --git a/pytchat/parser/replay.py b/pytchat/parser/replay.py index c120fe3..fc27a8d 100644 --- a/pytchat/parser/replay.py +++ b/pytchat/parser/replay.py @@ -12,6 +12,31 @@ logger = mylogger.get_logger(__name__,mode=config.LOGGER_MODE) class Parser: def parse(self, jsn): + """ + このparse関数はReplayChat._listen() 関数から定期的に呼び出される。 + 引数jsnはYoutubeから取得したアーカイブ済みチャットデータの生JSONであり、 + このparse関数によって与えられたJSONを以下に分割して返す。 + + timeout (次のチャットデータ取得までのインターバル) + + chat data(チャットデータ本体) + + continuation (次のチャットデータ取得に必要となるパラメータ). + + ライブ配信のチャットとアーカイブ済み動画のチャットは構造が若干異なっているが、 + ライブチャットと同じデータ形式に変換することにより、 + 同じprocessorでライブとリプレイどちらでも利用できるようにしている。 + + Parameter + ---------- + + jsn : dict + + Youtubeから取得したチャットデータのJSONオブジェクト。 + (pythonの辞書形式に変換済みの状態で渡される) + + Returns + ------- + + metadata : dict + + チャットデータに付随するメタデータ。timeout、 動画ID、continuationパラメータで構成される。 + + chatdata : list[dict] + + チャットデータ本体のリスト。 + """ if jsn is None: return {'timeoutMs':0,'continuation':None},[] if jsn['response']['responseContext'].get('errors'): @@ -36,6 +61,8 @@ class Parser: raise NoContentsException('チャットデータを取得できませんでした。') interval = self.get_interval(actions) metadata.setdefault("timeoutMs",interval) + """アーカイブ済みチャットはライブチャットと構造が異なっているため、以下の行により + ライブチャットと同じ形式にそろえる""" chatdata = [action["replayChatItemAction"]["actions"][0] for action in actions] return metadata, chatdata diff --git a/pytchat/processors/compatible/speed_calculator.py b/pytchat/processors/compatible/speed_calculator.py new file mode 100644 index 0000000..324f343 --- /dev/null +++ b/pytchat/processors/compatible/speed_calculator.py @@ -0,0 +1,172 @@ +""" +speedmeter.py +チャットの勢いを算出するChatProcessor +Calculate speed of chat. +""" + +class RingQueue: + """ + リング型キュー + + Attributes + ---------- + items : list + 格納されているアイテムのリスト。 + first_pos : int + キュー内の一番古いアイテムを示すリストのインデックス。 + last_pos : int + キュー内の一番新しいアイテムを示すリストのインデックス。 + mergin : boolean + キュー内に余裕があるか。キュー内のアイテム個数が、キューの最大個数未満であればTrue。 + """ + + def __init__(self,capacity): + """ + コンストラクタ + + Parameter + ---------- + capacity:このキューに格納するアイテムの最大個数。 + 格納時に最大個数を超える場合は一番古いアイテムから + 上書きする。 + """ + self.items = list() + self.capacity = capacity + self.first_pos = 0 + self.last_pos = 0 + self.mergin = True + + def put(self,item): + """ + 引数itemに指定されたアイテムをこのキューに格納する。 + キューの最大個数を超える場合は、一番古いアイテムの位置に上書きする。 + + Parameter + ---------- + item:格納するアイテム + """ + if self.mergin: + self.items.append(item) + self.last_pos = len(self.items)-1 + if self.last_pos == self.capacity-1: + self.mergin = False + return + self.last_pos += 1 + if self.last_pos > self.capacity-1: + self.last_pos = 0 + self.items[self.last_pos] = item + + self.first_pos += 1 + if self.first_pos > self.capacity-1: + self.first_pos = 0 + + def get(self): + """ + キュー内の一番古いアイテムへの参照を返す + (アイテムは削除しない) + + Return + ---------- + キュー内の一番古いアイテムへの参照 + """ + return self.items[self.first_pos] + + def item_count(self): + return len(self.items) + +class SpeedCalculator(RingQueue): + """ + チャットの勢いを計算するクラス + Parameter + ---------- + 格納するチャットブロックの数 + """ + def __init__(self, capacity,video_id): + super().__init__(capacity) + self.video_id=video_id + + def process(self, chat_components: list): + if chat_components: + for component in chat_components: + + chatdata = component.get('chatdata') + + if chatdata is None: + return + self.calc(chatdata) + + def _value(self): + + """ + ActionsQueue内のチャットデータリストから、 + チャット速度を計算して返す + + Return + --------------------------- + チャット速度(1分間で換算したチャット数) + """ + try: + #キュー内のactionsの総チャット数 + total = sum(item['chat_count'] for item in self.items) + #キュー内の最初と最後のチャットの時間差 + duration = (self.items[self.last_pos]['endtime'] + - self.items[self.first_pos]['starttime']) + if duration != 0: + return int(total*60/duration) + return 0 + except IndexError: + return 0 + + def _get_timestamp(self, action :dict): + """ + チャットデータのtimestampUsecを読み取る + liveChatTickerSponsorItemRenderer等のtickerデータは時刻格納位置が + 異なるため、時刻データなしとして扱う + """ + try: + item = action['addChatItemAction']['item'] + timestamp = int(item[list(item.keys())[0]]['timestampUsec']) + except (KeyError,TypeError): + return None + return timestamp + + def calc(self,actions): + if len(actions)==0: + return None + #actions内の時刻データを持つチャットデータの数(tickerは除く) + counter=0 + #actions内の最初のチャットデータの時刻 + starttime= None + #actions内の最後のチャットデータの時刻 + endtime=None + + for action in actions: + #チャットデータからtimestampUsecを読み取る + gettime = self._get_timestamp(action) + + #時刻のないデータだった場合は次の行のデータで読み取り試行 + if gettime is None: + continue + + #最初に有効な時刻を持つデータのtimestampをstarttimeに設定 + if starttime is None: + starttime = gettime + + #最後のtimestampを設定(途中で時刻のないデータの場合もあるので上書きしていく) + endtime = gettime + + #チャットの数をインクリメント + counter+=1 + + #チャット速度用のデータをリングキューに送る + if starttime is None or endtime is None: + return None + self.put({ + 'chat_count':counter, + #'timestamp':int(time.time()), + 'starttime':int(starttime/1000000), + 'endtime':int(endtime/1000000) + }) + + return self._value() + #print({'chat_count':counter,'timestamp':time.time()}) \ No newline at end of file