Compare commits

..

9 Commits

Author SHA1 Message Date
taizan-hokuto
cf9aae3322 Merge branch 'develop' 2019-12-19 01:57:08 +09:00
taizan-hokuto
6ac2315936 Increment version 2019-12-19 01:53:55 +09:00
taizan-hokuto
50c8e34080 Fix README 2019-12-19 01:51:31 +09:00
taizan-hokuto
2d3da91d51 Fix calculation algorithm 2019-12-19 01:21:49 +09:00
taizan-hokuto
3ac71985ff Implement SpeedCalculator 2019-12-17 21:29:20 +09:00
taizan-hokuto
13bdf0376b Merge branch 'develop' 2019-12-01 22:58:53 +09:00
taizan-hokuto
b2ffdaec0c Increment version 2019-12-01 22:54:43 +09:00
taizan-hokuto
c85786679f Merge branch 'feature/1' into develop 2019-12-01 22:50:06 +09:00
taizan-hokuto
c7a7886672 Fix superSticker rendering 2019-12-01 22:47:01 +09:00
7 changed files with 296 additions and 18 deletions

View File

@@ -40,11 +40,13 @@ while chat.is_alive():
from pytchat import LiveChat
import time
chat = LiveChat("G1w62uEMZ74", callback = func)
while chat.is_alive():
#other background operation here.
time.sleep(3)
def main()
chat = LiveChat("G1w62uEMZ74", callback = func)
while chat.is_alive():
time.sleep(3)
#other background operation.
#callback function is automatically called periodically.
def func(data):
for c in data.items:
print(f"{c.datetime} [{c.author.name}]-{c.message} {c.amountString}")
@@ -59,9 +61,10 @@ import asyncio
async def main():
chat = LiveChatAsync("G1w62uEMZ74", callback = func)
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):
for c in data.items:
print(f"{c.datetime} [{c.author.name}]-{c.message} {c.amountString}")
@@ -96,10 +99,9 @@ import asyncio
async def main():
chat = ReplayChatAsync("G1w62uEMZ74", seektime = 1000, callback = func)
while chat.is_alive():
#other background operation here.
await asyncio.sleep(3)
#callback function is automatically called periodically.
async def func(data):
for count in range(0,len(data.items)):
c= data.items[count]
@@ -107,15 +109,15 @@ async def func(data):
tick=data.items[count+1].timestamp -data.items[count].timestamp
else:
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)
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
```
## Chatdata Structure of Default Processor
Structure of each item which got from items() function.
## Structure of Default Processor
Each item can be got with items() function.
<table>
<tr>
<th>name</th>
@@ -152,7 +154,7 @@ Structure of each item which got from items() function.
<td>str</td>
<td>ex. "2019-10-10 12:34:56"</td>
</tr>
<td>timestampText</td>
<td>elapsedTime</td>
<td>str</td>
<td>elapsed time. (ex. "1:02:27") *Replay Only.</td>
</tr>

View File

@@ -2,7 +2,7 @@
pytchat is a python library for fetching youtube live chat without using yt api, Selenium, or BeautifulSoup.
"""
__copyright__ = 'Copyright (C) 2019 taizan-hokuto'
__version__ = '0.0.3.3'
__version__ = '0.0.3.5'
__license__ = 'MIT'
__author__ = 'taizan-hokuto'
__author_email__ = '55448286+taizan-hokuto@users.noreply.github.com'

View File

@@ -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'):

View File

@@ -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

View File

@@ -15,9 +15,9 @@ class BaseRenderer:
self.timestamp = int(timestampUsec/1000)
tst = self.renderer.get("timestampText")
if tst:
self.timestampText = tst.get("simpleText")
self.elapsedTime = tst.get("simpleText")
else:
self.timestampText = ""
self.elapsedTime = ""
self.datetime = self.get_datetime(timestampUsec)
self.message = self.get_message(self.renderer)
self.messageEx = self.get_message_ex(self.renderer)

View File

@@ -1,12 +1,42 @@
import re
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):
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

View 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()