Compare commits

...

14 Commits

Author SHA1 Message Date
taizan-hokuto
2ac4c99ab4 Increment version 2019-11-22 00:04:18 +09:00
taizan-hokuto
51bf8ad738 Update README 2019-11-21 23:37:35 +09:00
taizan-hokuto
2e70e74bcd Update README 2019-11-21 23:04:12 +09:00
taizan-hokuto
a39d6cb420 Use list comprehension 2019-11-21 22:46:15 +09:00
taizan-hokuto
5dd0cb45b7 Implement messageEx 2019-11-21 22:35:27 +09:00
taizan-hokuto
24873651a6 Fix comments 2019-11-21 20:47:42 +09:00
taizan-hokuto
0e060bf998 Use logger when errors occur 2019-11-20 23:59:16 +09:00
taizan-hokuto
817fed9d1d Make functions private. 2019-11-19 20:53:37 +09:00
taizan-hokuto
823f7fefa4 Fix comments 2019-11-19 20:36:54 +09:00
taizan-hokuto
aa894fc52b Fix comments 2019-11-15 00:58:36 +09:00
taizan-hokuto
6d775e5cd0 Merge branch 'hotfix' 2019-11-14 22:39:22 +09:00
taizan-hokuto
53b70ed86b Increment version 2019-11-14 22:38:25 +09:00
taizan-hokuto
68c707b7d6 Update README 2019-11-14 22:37:16 +09:00
taizan-hokuto
30aaa54a2f Change debug mode 2019-11-14 22:34:29 +09:00
11 changed files with 66 additions and 57 deletions

View File

@@ -13,7 +13,7 @@ Other features:
+ Quick fetching of initial chat data by generating continuation params + Quick fetching of initial chat data by generating continuation params
instead of web scraping. instead of web scraping.
より詳細な説明は [wiki](https://github.com/taizan-hokuto/pytchat/wiki) をご参照ください。 For more detailed information, see [wiki](https://github.com/taizan-hokuto/pytchat/wiki).
## Install ## Install
```python ```python
@@ -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>

View File

@@ -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.0' __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'

View File

@@ -1,4 +1,4 @@
import logging import logging
LOGGER_MODE = logging.DEBUG LOGGER_MODE = logging.ERROR
headers = { headers = {
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36'} 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36'}

View File

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

View File

@@ -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)}")

View File

@@ -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)}")

View File

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

View File

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

View File

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

View File

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

View File

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