Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf9aae3322 | ||
|
|
6ac2315936 | ||
|
|
50c8e34080 | ||
|
|
2d3da91d51 | ||
|
|
3ac71985ff | ||
|
|
13bdf0376b | ||
|
|
b2ffdaec0c | ||
|
|
c85786679f | ||
|
|
c7a7886672 |
22
README.md
22
README.md
@@ -40,11 +40,13 @@ while chat.is_alive():
|
|||||||
from pytchat import LiveChat
|
from pytchat import LiveChat
|
||||||
import time
|
import time
|
||||||
|
|
||||||
chat = LiveChat("G1w62uEMZ74", callback = func)
|
def main()
|
||||||
while chat.is_alive():
|
chat = LiveChat("G1w62uEMZ74", callback = func)
|
||||||
#other background operation here.
|
while chat.is_alive():
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
|
#other background operation.
|
||||||
|
|
||||||
|
#callback function is automatically called periodically.
|
||||||
def func(data):
|
def func(data):
|
||||||
for c in data.items:
|
for c in data.items:
|
||||||
print(f"{c.datetime} [{c.author.name}]-{c.message} {c.amountString}")
|
print(f"{c.datetime} [{c.author.name}]-{c.message} {c.amountString}")
|
||||||
@@ -59,9 +61,10 @@ import asyncio
|
|||||||
async def main():
|
async def main():
|
||||||
chat = LiveChatAsync("G1w62uEMZ74", callback = func)
|
chat = LiveChatAsync("G1w62uEMZ74", callback = func)
|
||||||
while chat.is_alive():
|
while chat.is_alive():
|
||||||
#other background operation here.
|
|
||||||
await asyncio.sleep(3)
|
await asyncio.sleep(3)
|
||||||
|
#other background operation.
|
||||||
|
|
||||||
|
#callback function is automatically called periodically.
|
||||||
async def func(data):
|
async def func(data):
|
||||||
for c in data.items:
|
for c in data.items:
|
||||||
print(f"{c.datetime} [{c.author.name}]-{c.message} {c.amountString}")
|
print(f"{c.datetime} [{c.author.name}]-{c.message} {c.amountString}")
|
||||||
@@ -96,10 +99,9 @@ import asyncio
|
|||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
chat = ReplayChatAsync("G1w62uEMZ74", seektime = 1000, callback = func)
|
chat = ReplayChatAsync("G1w62uEMZ74", seektime = 1000, callback = func)
|
||||||
while chat.is_alive():
|
|
||||||
#other background operation here.
|
#other background operation here.
|
||||||
await asyncio.sleep(3)
|
|
||||||
|
|
||||||
|
#callback function is automatically called periodically.
|
||||||
async def func(data):
|
async def func(data):
|
||||||
for count in range(0,len(data.items)):
|
for count in range(0,len(data.items)):
|
||||||
c= data.items[count]
|
c= data.items[count]
|
||||||
@@ -107,15 +109,15 @@ async def func(data):
|
|||||||
tick=data.items[count+1].timestamp -data.items[count].timestamp
|
tick=data.items[count+1].timestamp -data.items[count].timestamp
|
||||||
else:
|
else:
|
||||||
tick=0
|
tick=0
|
||||||
print(f"<{c.timestampText}> [{c.author.name}]-{c.message} {c.amountString}")
|
print(f"<{c.elapsedTime}> [{c.author.name}]-{c.message} {c.amountString}")
|
||||||
await asyncio.sleep(tick/1000)
|
await asyncio.sleep(tick/1000)
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
loop.run_until_complete(main())
|
loop.run_until_complete(main())
|
||||||
```
|
```
|
||||||
|
|
||||||
## Chatdata Structure of Default Processor
|
## Structure of Default Processor
|
||||||
Structure of each item which got from items() function.
|
Each item can be got with items() function.
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<th>name</th>
|
<th>name</th>
|
||||||
@@ -152,7 +154,7 @@ Structure of each item which got from items() function.
|
|||||||
<td>str</td>
|
<td>str</td>
|
||||||
<td>ex. "2019-10-10 12:34:56"</td>
|
<td>ex. "2019-10-10 12:34:56"</td>
|
||||||
</tr>
|
</tr>
|
||||||
<td>timestampText</td>
|
<td>elapsedTime</td>
|
||||||
<td>str</td>
|
<td>str</td>
|
||||||
<td>elapsed time. (ex. "1:02:27") *Replay Only.</td>
|
<td>elapsed time. (ex. "1:02:27") *Replay Only.</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
pytchat is a python library for fetching youtube live chat without using yt api, Selenium, or BeautifulSoup.
|
pytchat is a python library for fetching youtube live chat without using yt api, Selenium, or BeautifulSoup.
|
||||||
"""
|
"""
|
||||||
__copyright__ = 'Copyright (C) 2019 taizan-hokuto'
|
__copyright__ = 'Copyright (C) 2019 taizan-hokuto'
|
||||||
__version__ = '0.0.3.3'
|
__version__ = '0.0.3.5'
|
||||||
__license__ = 'MIT'
|
__license__ = 'MIT'
|
||||||
__author__ = 'taizan-hokuto'
|
__author__ = 'taizan-hokuto'
|
||||||
__author_email__ = '55448286+taizan-hokuto@users.noreply.github.com'
|
__author_email__ = '55448286+taizan-hokuto@users.noreply.github.com'
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
"""
|
||||||
|
pytchat.parser.live
|
||||||
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
This module is parser of live chat JSON.
|
||||||
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from .. import config
|
from .. import config
|
||||||
from .. import mylogger
|
from .. import mylogger
|
||||||
@@ -12,6 +18,27 @@ logger = mylogger.get_logger(__name__,mode=config.LOGGER_MODE)
|
|||||||
|
|
||||||
class Parser:
|
class Parser:
|
||||||
def parse(self, jsn):
|
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:
|
if jsn is None:
|
||||||
return {'timeoutMs':0,'continuation':None},[]
|
return {'timeoutMs':0,'continuation':None},[]
|
||||||
if jsn['response']['responseContext'].get('errors'):
|
if jsn['response']['responseContext'].get('errors'):
|
||||||
|
|||||||
@@ -12,6 +12,31 @@ logger = mylogger.get_logger(__name__,mode=config.LOGGER_MODE)
|
|||||||
|
|
||||||
class Parser:
|
class Parser:
|
||||||
def parse(self, jsn):
|
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:
|
if jsn is None:
|
||||||
return {'timeoutMs':0,'continuation':None},[]
|
return {'timeoutMs':0,'continuation':None},[]
|
||||||
if jsn['response']['responseContext'].get('errors'):
|
if jsn['response']['responseContext'].get('errors'):
|
||||||
@@ -36,6 +61,8 @@ class Parser:
|
|||||||
raise NoContentsException('チャットデータを取得できませんでした。')
|
raise NoContentsException('チャットデータを取得できませんでした。')
|
||||||
interval = self.get_interval(actions)
|
interval = self.get_interval(actions)
|
||||||
metadata.setdefault("timeoutMs",interval)
|
metadata.setdefault("timeoutMs",interval)
|
||||||
|
"""アーカイブ済みチャットはライブチャットと構造が異なっているため、以下の行により
|
||||||
|
ライブチャットと同じ形式にそろえる"""
|
||||||
chatdata = [action["replayChatItemAction"]["actions"][0] for action in actions]
|
chatdata = [action["replayChatItemAction"]["actions"][0] for action in actions]
|
||||||
return metadata, chatdata
|
return metadata, chatdata
|
||||||
|
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ class BaseRenderer:
|
|||||||
self.timestamp = int(timestampUsec/1000)
|
self.timestamp = int(timestampUsec/1000)
|
||||||
tst = self.renderer.get("timestampText")
|
tst = self.renderer.get("timestampText")
|
||||||
if tst:
|
if tst:
|
||||||
self.timestampText = tst.get("simpleText")
|
self.elapsedTime = tst.get("simpleText")
|
||||||
else:
|
else:
|
||||||
self.timestampText = ""
|
self.elapsedTime = ""
|
||||||
self.datetime = self.get_datetime(timestampUsec)
|
self.datetime = self.get_datetime(timestampUsec)
|
||||||
self.message = self.get_message(self.renderer)
|
self.message = self.get_message(self.renderer)
|
||||||
self.messageEx = self.get_message_ex(self.renderer)
|
self.messageEx = self.get_message_ex(self.renderer)
|
||||||
|
|||||||
@@ -1,12 +1,42 @@
|
|||||||
import re
|
import re
|
||||||
from . import currency
|
from . import currency
|
||||||
from .paidmessage import LiveChatPaidMessageRenderer
|
from .base import BaseRenderer
|
||||||
|
superchat_regex = re.compile(r"^(\D*)(\d{1,3}(,\d{3})*(\.\d*)*\b)$")
|
||||||
|
|
||||||
class LiveChatPaidStickerRenderer(LiveChatPaidMessageRenderer):
|
class LiveChatPaidStickerRenderer(BaseRenderer):
|
||||||
def __init__(self, item):
|
def __init__(self, item):
|
||||||
super().__init__(item, "superSticker")
|
super().__init__(item, "superSticker")
|
||||||
|
|
||||||
|
|
||||||
|
def get_snippet(self):
|
||||||
|
super().get_snippet()
|
||||||
|
|
||||||
|
self.author.name = self.renderer["authorName"]["simpleText"]
|
||||||
|
|
||||||
|
amountDisplayString, symbol, amount =(
|
||||||
|
self.get_amountdata(self.renderer)
|
||||||
|
)
|
||||||
|
self.message = ""
|
||||||
|
self.amountValue = amount
|
||||||
|
self.amountString = amountDisplayString
|
||||||
|
self.currency = currency.symbols[symbol]["fxtext"] if currency.symbols.get(symbol) else symbol
|
||||||
|
self.bgColor = self.renderer.get("moneyChipBackgroundColor", 0)
|
||||||
|
self.sticker = "https:"+self.renderer["sticker"]["thumbnails"][0]["url"]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def get_amountdata(self,renderer):
|
||||||
|
amountDisplayString = renderer["purchaseAmountText"]["simpleText"]
|
||||||
|
m = superchat_regex.search(amountDisplayString)
|
||||||
|
if m:
|
||||||
|
symbol = m.group(1)
|
||||||
|
amount = float(m.group(2).replace(',',''))
|
||||||
|
else:
|
||||||
|
symbol = ""
|
||||||
|
amount = 0.0
|
||||||
|
return amountDisplayString, symbol, amount
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
192
pytchat/processors/speed_calculator.py
Normal file
192
pytchat/processors/speed_calculator.py
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
"""
|
||||||
|
speedmeter.py
|
||||||
|
チャットの勢いを算出するChatProcessor
|
||||||
|
Calculate speed of chat.
|
||||||
|
"""
|
||||||
|
import calendar, datetime, pytz
|
||||||
|
|
||||||
|
class RingQueue:
|
||||||
|
"""
|
||||||
|
リング型キュー
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
items : list
|
||||||
|
格納されているアイテムのリスト。
|
||||||
|
first_pos : int
|
||||||
|
キュー内の一番古いアイテムを示すリストのインデックス。
|
||||||
|
last_pos : int
|
||||||
|
キュー内の一番新しいアイテムを示すリストのインデックス。
|
||||||
|
mergin : boolean
|
||||||
|
キュー内に余裕があるか。キュー内のアイテム個数が、キューの最大個数未満であればTrue。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, capacity = 10):
|
||||||
|
"""
|
||||||
|
コンストラクタ
|
||||||
|
|
||||||
|
Parameter
|
||||||
|
----------
|
||||||
|
capacity:このキューに格納するアイテムの最大個数。
|
||||||
|
格納時に最大個数を超える場合は一番古いアイテムから
|
||||||
|
上書きする。
|
||||||
|
"""
|
||||||
|
if capacity <= 0:
|
||||||
|
raise ValueError
|
||||||
|
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
|
||||||
|
self.speed = 0
|
||||||
|
|
||||||
|
def process(self, chat_components: list):
|
||||||
|
if chat_components:
|
||||||
|
for component in chat_components:
|
||||||
|
|
||||||
|
chatdata = component.get('chatdata')
|
||||||
|
|
||||||
|
if chatdata is None:
|
||||||
|
return self.speed
|
||||||
|
self.speed = self.calc(chatdata)
|
||||||
|
return self.speed
|
||||||
|
|
||||||
|
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):
|
||||||
|
|
||||||
|
def empty_data():
|
||||||
|
'''
|
||||||
|
データがない場合にゼロのデータをリングキューに入れる
|
||||||
|
'''
|
||||||
|
timestamp_now = calendar.timegm(datetime.datetime.
|
||||||
|
now(pytz.utc).utctimetuple())
|
||||||
|
self.put({
|
||||||
|
'chat_count':0,
|
||||||
|
'starttime':int(timestamp_now),
|
||||||
|
'endtime':int(timestamp_now)
|
||||||
|
})
|
||||||
|
return self._value()
|
||||||
|
|
||||||
|
if actions is None or len(actions)==0:
|
||||||
|
return empty_data
|
||||||
|
|
||||||
|
#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 empty_data
|
||||||
|
|
||||||
|
self.put({
|
||||||
|
'chat_count':counter,
|
||||||
|
'starttime':int(starttime/1000000),
|
||||||
|
'endtime':int(endtime/1000000)
|
||||||
|
})
|
||||||
|
|
||||||
|
return self._value()
|
||||||
Reference in New Issue
Block a user