Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ac4c99ab4 | ||
|
|
51bf8ad738 | ||
|
|
2e70e74bcd | ||
|
|
a39d6cb420 | ||
|
|
5dd0cb45b7 | ||
|
|
24873651a6 | ||
|
|
0e060bf998 | ||
|
|
817fed9d1d | ||
|
|
823f7fefa4 | ||
|
|
aa894fc52b |
@@ -137,6 +137,11 @@ Structure of each item which got from items() function.
|
|||||||
<td>str</td>
|
<td>str</td>
|
||||||
<td>emojis are represented by ":(shortcut text):"</td>
|
<td>emojis are represented by ":(shortcut text):"</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>messageEx</td>
|
||||||
|
<td>str</td>
|
||||||
|
<td>list of message texts and emoji URLs.</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>timestamp</td>
|
<td>timestamp</td>
|
||||||
<td>int</td>
|
<td>int</td>
|
||||||
@@ -149,7 +154,7 @@ Structure of each item which got from items() function.
|
|||||||
</tr>
|
</tr>
|
||||||
<td>timestampText</td>
|
<td>timestampText</td>
|
||||||
<td>str</td>
|
<td>str</td>
|
||||||
<td>elapsed time. (ex. "1:02:27")</td>
|
<td>elapsed time. (ex. "1:02:27") *Replay Only.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>amountValue</td>
|
<td>amountValue</td>
|
||||||
@@ -193,7 +198,7 @@ Structure of author object.
|
|||||||
<tr>
|
<tr>
|
||||||
<td>channelId</td>
|
<td>channelId</td>
|
||||||
<td>str</td>
|
<td>str</td>
|
||||||
<td></td>
|
<td>*chatter's channel ID. NOT broadcasting video's channel ID.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>channelUrl</td>
|
<td>channelUrl</td>
|
||||||
|
|||||||
@@ -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.1'
|
__version__ = '0.0.3.2'
|
||||||
__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'
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import datetime
|
|||||||
import json
|
import json
|
||||||
import random
|
import random
|
||||||
import signal
|
import signal
|
||||||
import threading
|
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
@@ -123,7 +122,7 @@ class LiveChatAsync:
|
|||||||
|
|
||||||
async def _startlisten(self):
|
async def _startlisten(self):
|
||||||
"""最初のcontinuationパラメータを取得し、
|
"""最初のcontinuationパラメータを取得し、
|
||||||
_listenループを開始する
|
_listenループのタスクを作成し開始する
|
||||||
"""
|
"""
|
||||||
initial_continuation = await self._get_initial_continuation()
|
initial_continuation = await self._get_initial_continuation()
|
||||||
if initial_continuation is None:
|
if initial_continuation is None:
|
||||||
@@ -286,13 +285,4 @@ class LiveChatAsync:
|
|||||||
logger.debug(f"残っているタスクを終了しています")
|
logger.debug(f"残っているタスクを終了しています")
|
||||||
await asyncio.gather(*tasks,return_exceptions=True)
|
await asyncio.gather(*tasks,return_exceptions=True)
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
loop.stop()
|
loop.stop()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -21,14 +21,19 @@ logger = mylogger.get_logger(__name__,mode=config.LOGGER_MODE)
|
|||||||
MAX_RETRY = 10
|
MAX_RETRY = 10
|
||||||
headers = config.headers
|
headers = config.headers
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ReplayChatAsync:
|
class ReplayChatAsync:
|
||||||
''' aiohttpを利用してYouTubeのライブ配信のチャットデータを取得する
|
'''asyncio(aiohttp)を利用してYouTubeのチャットデータを取得する。
|
||||||
|
|
||||||
Parameter
|
Parameter
|
||||||
---------
|
---------
|
||||||
video_id : str
|
video_id : str
|
||||||
動画ID
|
動画ID
|
||||||
|
|
||||||
|
seektime : int
|
||||||
|
リプレイするチャットデータの開始時間(秒)
|
||||||
|
|
||||||
processor : ChatProcessor
|
processor : ChatProcessor
|
||||||
チャットデータを加工するオブジェクト
|
チャットデータを加工するオブジェクト
|
||||||
|
|
||||||
@@ -46,6 +51,9 @@ class ReplayChatAsync:
|
|||||||
done_callback : func
|
done_callback : func
|
||||||
listener終了時に呼び出すコールバック。
|
listener終了時に呼び出すコールバック。
|
||||||
|
|
||||||
|
exception_handler : func
|
||||||
|
例外を処理する関数
|
||||||
|
|
||||||
direct_mode : bool
|
direct_mode : bool
|
||||||
Trueの場合、bufferを使わずにcallbackを呼ぶ。
|
Trueの場合、bufferを使わずにcallbackを呼ぶ。
|
||||||
Trueの場合、callbackの設定が必須
|
Trueの場合、callbackの設定が必須
|
||||||
@@ -53,26 +61,23 @@ class ReplayChatAsync:
|
|||||||
|
|
||||||
Attributes
|
Attributes
|
||||||
---------
|
---------
|
||||||
_executor : ThreadPoolExecutor
|
|
||||||
チャットデータ取得ループ(_listen)用のスレッド
|
|
||||||
|
|
||||||
_is_alive : bool
|
_is_alive : bool
|
||||||
チャット取得を終了したか
|
チャット取得を停止するためのフラグ
|
||||||
'''
|
'''
|
||||||
|
|
||||||
_setup_finished = False
|
_setup_finished = False
|
||||||
|
|
||||||
def __init__(self, video_id,
|
def __init__(self, video_id,
|
||||||
seektime =0,
|
seektime = 0,
|
||||||
processor = DefaultProcessor(),
|
processor = DefaultProcessor(),
|
||||||
buffer = Buffer(maxsize = 20),
|
buffer = None,
|
||||||
interruptable = True,
|
interruptable = True,
|
||||||
callback = None,
|
callback = None,
|
||||||
done_callback = None,
|
done_callback = None,
|
||||||
exception_handler = None,
|
exception_handler = None,
|
||||||
direct_mode = False):
|
direct_mode = False):
|
||||||
self.video_id = video_id
|
self.video_id = video_id
|
||||||
self.seektime= seektime
|
self.seektime = seektime
|
||||||
self.processor = processor
|
self.processor = processor
|
||||||
self._buffer = buffer
|
self._buffer = buffer
|
||||||
self._callback = callback
|
self._callback = callback
|
||||||
@@ -151,8 +156,8 @@ class ReplayChatAsync:
|
|||||||
|
|
||||||
async def _listen(self, continuation):
|
async def _listen(self, continuation):
|
||||||
''' continuationに紐付いたチャットデータを取得し
|
''' continuationに紐付いたチャットデータを取得し
|
||||||
にチャットデータを格納、
|
Bufferにチャットデータを格納、
|
||||||
次のcontinuaitonを取得してループする
|
次のcontinuaitonを取得してループする。
|
||||||
|
|
||||||
Parameter
|
Parameter
|
||||||
---------
|
---------
|
||||||
@@ -163,10 +168,10 @@ class ReplayChatAsync:
|
|||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
while(continuation and self._is_alive):
|
while(continuation and self._is_alive):
|
||||||
if self._pauser.empty():
|
if self._pauser.empty():
|
||||||
#pauseが呼ばれて_pauserが空状態のときは一時停止する
|
#pause
|
||||||
await self._pauser.get()
|
await self._pauser.get()
|
||||||
#resumeが呼ばれて_pauserにitemが入ったら再開する
|
#resume
|
||||||
#直後に_pauserにitem(None)を入れてブロックを防ぐ
|
#prohibit from blocking by putting None into _pauser.
|
||||||
self._pauser.put_nowait(None)
|
self._pauser.put_nowait(None)
|
||||||
livechat_json = (await
|
livechat_json = (await
|
||||||
self._get_livechat_json(continuation, session, headers)
|
self._get_livechat_json(continuation, session, headers)
|
||||||
@@ -186,11 +191,10 @@ class ReplayChatAsync:
|
|||||||
else:
|
else:
|
||||||
await self._buffer.put(chat_component)
|
await self._buffer.put(chat_component)
|
||||||
diff_time = timeout - (time.time()-time_mark)
|
diff_time = timeout - (time.time()-time_mark)
|
||||||
if diff_time < 0 : diff_time=0
|
|
||||||
await asyncio.sleep(diff_time)
|
await asyncio.sleep(diff_time)
|
||||||
continuation = metadata.get('continuation')
|
continuation = metadata.get('continuation')
|
||||||
except ChatParseException as e:
|
except ChatParseException as e:
|
||||||
logger.error(f"{str(e)}(動画ID:\"{self.video_id}\")")
|
logger.info(f"{str(e)}(video_id:\"{self.video_id}\")")
|
||||||
return
|
return
|
||||||
except (TypeError , json.JSONDecodeError) :
|
except (TypeError , json.JSONDecodeError) :
|
||||||
logger.error(f"{traceback.format_exc(limit = -1)}")
|
logger.error(f"{traceback.format_exc(limit = -1)}")
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ class LiveChat:
|
|||||||
チャットデータ取得ループ(_listen)用のスレッド
|
チャットデータ取得ループ(_listen)用のスレッド
|
||||||
|
|
||||||
_is_alive : bool
|
_is_alive : bool
|
||||||
チャット取得を終了したか
|
チャット取得を停止するためのフラグ
|
||||||
'''
|
'''
|
||||||
|
|
||||||
_setup_finished = False
|
_setup_finished = False
|
||||||
@@ -142,7 +142,7 @@ class LiveChat:
|
|||||||
|
|
||||||
def _listen(self, continuation):
|
def _listen(self, continuation):
|
||||||
''' continuationに紐付いたチャットデータを取得し
|
''' continuationに紐付いたチャットデータを取得し
|
||||||
にチャットデータを格納、
|
BUfferにチャットデータを格納、
|
||||||
次のcontinuaitonを取得してループする
|
次のcontinuaitonを取得してループする
|
||||||
|
|
||||||
Parameter
|
Parameter
|
||||||
@@ -157,7 +157,6 @@ class LiveChat:
|
|||||||
self._get_livechat_json(continuation, session, headers)
|
self._get_livechat_json(continuation, session, headers)
|
||||||
)
|
)
|
||||||
metadata, chatdata = self._parser.parse( livechat_json )
|
metadata, chatdata = self._parser.parse( livechat_json )
|
||||||
#チャットデータを含むコンポーネントを組み立ててbufferに投入する
|
|
||||||
timeout = metadata['timeoutMs']/1000
|
timeout = metadata['timeoutMs']/1000
|
||||||
chat_component = {
|
chat_component = {
|
||||||
"video_id" : self.video_id,
|
"video_id" : self.video_id,
|
||||||
@@ -171,16 +170,12 @@ class LiveChat:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self._buffer.put(chat_component)
|
self._buffer.put(chat_component)
|
||||||
#次のchatを取得するまでsleepする
|
|
||||||
diff_time = timeout - (time.time()-time_mark)
|
diff_time = timeout - (time.time()-time_mark)
|
||||||
if diff_time < 0 : diff_time=0
|
if diff_time < 0 : diff_time=0
|
||||||
time.sleep(diff_time)
|
time.sleep(diff_time)
|
||||||
#次のチャットデータのcontinuationパラメータを取り出す。
|
|
||||||
continuation = metadata.get('continuation')
|
continuation = metadata.get('continuation')
|
||||||
|
|
||||||
#whileループ先頭に戻る
|
|
||||||
except ChatParseException as e:
|
except ChatParseException as e:
|
||||||
logger.error(f"{str(e)}(動画ID:\"{self.video_id}\")")
|
logger.info(f"{str(e)}(video_id:\"{self.video_id}\")")
|
||||||
return
|
return
|
||||||
except (TypeError , json.JSONDecodeError) :
|
except (TypeError , json.JSONDecodeError) :
|
||||||
logger.error(f"{traceback.format_exc(limit = -1)}")
|
logger.error(f"{traceback.format_exc(limit = -1)}")
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ class ReplayChat:
|
|||||||
video_id : str
|
video_id : str
|
||||||
動画ID
|
動画ID
|
||||||
|
|
||||||
|
seektime : int
|
||||||
|
リプレイするチャットデータの開始時間(秒)
|
||||||
|
|
||||||
processor : ChatProcessor
|
processor : ChatProcessor
|
||||||
チャットデータを加工するオブジェクト
|
チャットデータを加工するオブジェクト
|
||||||
|
|
||||||
@@ -65,7 +68,7 @@ class ReplayChat:
|
|||||||
#チャット監視中のListenerのリスト
|
#チャット監視中のListenerのリスト
|
||||||
_listeners= []
|
_listeners= []
|
||||||
def __init__(self, video_id,
|
def __init__(self, video_id,
|
||||||
seektime =0,
|
seektime = 0,
|
||||||
processor = DefaultProcessor(),
|
processor = DefaultProcessor(),
|
||||||
buffer = Buffer(maxsize = 20),
|
buffer = Buffer(maxsize = 20),
|
||||||
interruptable = True,
|
interruptable = True,
|
||||||
@@ -74,7 +77,7 @@ class ReplayChat:
|
|||||||
direct_mode = False
|
direct_mode = False
|
||||||
):
|
):
|
||||||
self.video_id = video_id
|
self.video_id = video_id
|
||||||
self.seektime= seektime
|
self.seektime = seektime
|
||||||
self.processor = processor
|
self.processor = processor
|
||||||
self._buffer = buffer
|
self._buffer = buffer
|
||||||
self._callback = callback
|
self._callback = callback
|
||||||
@@ -159,16 +162,15 @@ class ReplayChat:
|
|||||||
with requests.Session() as session:
|
with requests.Session() as session:
|
||||||
while(continuation and self._is_alive):
|
while(continuation and self._is_alive):
|
||||||
if self._pauser.empty():
|
if self._pauser.empty():
|
||||||
#pauseが呼ばれて_pauserが空状態のときは一時停止する
|
#pause
|
||||||
self._pauser.get()
|
self._pauser.get()
|
||||||
#resumeが呼ばれて_pauserにitemが入ったら再開する
|
#resume
|
||||||
#直後に_pauserにitem(None)を入れてブロックを防ぐ
|
#prohibit from blocking by putting None into _pauser.
|
||||||
self._pauser.put_nowait(None)
|
self._pauser.put_nowait(None)
|
||||||
livechat_json = (
|
livechat_json = (
|
||||||
self._get_livechat_json(continuation, session, headers)
|
self._get_livechat_json(continuation, session, headers)
|
||||||
)
|
)
|
||||||
metadata, chatdata = self._parser.parse( livechat_json )
|
metadata, chatdata = self._parser.parse( livechat_json )
|
||||||
#チャットデータを含むコンポーネントを組み立ててbufferに投入する
|
|
||||||
timeout = metadata['timeoutMs']/1000
|
timeout = metadata['timeoutMs']/1000
|
||||||
chat_component = {
|
chat_component = {
|
||||||
"video_id" : self.video_id,
|
"video_id" : self.video_id,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ def get_logger(modname,mode=logging.DEBUG):
|
|||||||
logger.addHandler(handler1)
|
logger.addHandler(handler1)
|
||||||
#create handler2 for recording log file
|
#create handler2 for recording log file
|
||||||
if mode <= logging.DEBUG:
|
if mode <= logging.DEBUG:
|
||||||
handler2 = logging.FileHandler(filename="log.txt")
|
handler2 = logging.FileHandler(filename="log.txt", encoding='utf-8')
|
||||||
handler2.setLevel(logging.ERROR)
|
handler2.setLevel(logging.ERROR)
|
||||||
handler2.setFormatter(my_formatter)
|
handler2.setFormatter(my_formatter)
|
||||||
|
|
||||||
|
|||||||
@@ -36,9 +36,7 @@ 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 = []
|
chatdata = [action["replayChatItemAction"]["actions"][0] for action in actions]
|
||||||
for action in actions:
|
|
||||||
chatdata.append(action["replayChatItemAction"]["actions"][0])
|
|
||||||
return metadata, chatdata
|
return metadata, chatdata
|
||||||
|
|
||||||
def get_interval(self, actions: list):
|
def get_interval(self, actions: list):
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ from .renderer.textmessage import LiveChatTextMessageRenderer
|
|||||||
from .renderer.paidmessage import LiveChatPaidMessageRenderer
|
from .renderer.paidmessage import LiveChatPaidMessageRenderer
|
||||||
from .renderer.paidsticker import LiveChatPaidStickerRenderer
|
from .renderer.paidsticker import LiveChatPaidStickerRenderer
|
||||||
from .renderer.legacypaid import LiveChatLegacyPaidMessageRenderer
|
from .renderer.legacypaid import LiveChatLegacyPaidMessageRenderer
|
||||||
|
from ... import config
|
||||||
|
from ... import mylogger
|
||||||
|
logger = mylogger.get_logger(__name__,mode=config.LOGGER_MODE)
|
||||||
|
|
||||||
class Chatdata:
|
class Chatdata:
|
||||||
def __init__(self,chatlist:list, timeout:float):
|
def __init__(self,chatlist:list, timeout:float):
|
||||||
@@ -40,32 +42,31 @@ class DefaultProcessor:
|
|||||||
if action.get('addChatItemAction') is None: continue
|
if action.get('addChatItemAction') is None: continue
|
||||||
if action['addChatItemAction'].get('item') is None: continue
|
if action['addChatItemAction'].get('item') is None: continue
|
||||||
|
|
||||||
chat = self.parse(action)
|
chat = self._parse(action)
|
||||||
if chat:
|
if chat:
|
||||||
chatlist.append(chat)
|
chatlist.append(chat)
|
||||||
return Chatdata(chatlist, float(timeout))
|
return Chatdata(chatlist, float(timeout))
|
||||||
|
|
||||||
|
|
||||||
def parse(self, sitem):
|
def _parse(self, sitem):
|
||||||
|
|
||||||
action = sitem.get("addChatItemAction")
|
action = sitem.get("addChatItemAction")
|
||||||
if action:
|
if action:
|
||||||
item = action.get("item")
|
item = action.get("item")
|
||||||
if item is None: return None
|
if item is None: return None
|
||||||
try:
|
try:
|
||||||
renderer = self.get_renderer(item)
|
renderer = self._get_renderer(item)
|
||||||
if renderer == None:
|
if renderer == None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
renderer.get_snippet()
|
renderer.get_snippet()
|
||||||
renderer.get_authordetails()
|
renderer.get_authordetails()
|
||||||
except (KeyError,TypeError,AttributeError) as e:
|
except (KeyError,TypeError,AttributeError) as e:
|
||||||
print(f"------{str(type(e))}-{str(e)}----------")
|
logger.error(f"{str(type(e))}-{str(e)} sitem:{str(sitem)}")
|
||||||
print(sitem)
|
|
||||||
return None
|
return None
|
||||||
return renderer
|
return renderer
|
||||||
|
|
||||||
def get_renderer(self, item):
|
def _get_renderer(self, item):
|
||||||
if item.get("liveChatTextMessageRenderer"):
|
if item.get("liveChatTextMessageRenderer"):
|
||||||
renderer = LiveChatTextMessageRenderer(item)
|
renderer = LiveChatTextMessageRenderer(item)
|
||||||
elif item.get("liveChatPaidMessageRenderer"):
|
elif item.get("liveChatPaidMessageRenderer"):
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class BaseRenderer:
|
|||||||
self.timestampText = ""
|
self.timestampText = ""
|
||||||
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.id = self.renderer.get('id')
|
self.id = self.renderer.get('id')
|
||||||
self.amountValue= 0.0
|
self.amountValue= 0.0
|
||||||
self.amountString = ""
|
self.amountString = ""
|
||||||
@@ -54,6 +55,19 @@ class BaseRenderer:
|
|||||||
message += r.get('text','')
|
message += r.get('text','')
|
||||||
return message
|
return message
|
||||||
|
|
||||||
|
def get_message_ex(self,renderer):
|
||||||
|
message = []
|
||||||
|
if renderer.get("message"):
|
||||||
|
runs=renderer["message"].get("runs")
|
||||||
|
if runs:
|
||||||
|
for r in runs:
|
||||||
|
if r:
|
||||||
|
if r.get('emoji'):
|
||||||
|
message.append(r['emoji']['image']['thumbnails'][1].get('url'))
|
||||||
|
else:
|
||||||
|
message.append(r.get('text',''))
|
||||||
|
return message
|
||||||
|
|
||||||
def get_badges(self,renderer):
|
def get_badges(self,renderer):
|
||||||
isVerified = False
|
isVerified = False
|
||||||
isChatOwner = False
|
isChatOwner = False
|
||||||
|
|||||||
Reference in New Issue
Block a user