Compare commits

...

24 Commits

Author SHA1 Message Date
55448286+taizan-hokuto@users.noreply.github.com
365964d88c Merge branch 'develop' 2019-11-04 23:50:58 +09:00
55448286+taizan-hokuto@users.noreply.github.com
517f41f5fe Increment version 2019-11-04 23:50:41 +09:00
55448286+taizan-hokuto@users.noreply.github.com
432825b5ed Use logger when errors occur 2019-11-04 23:45:10 +09:00
55448286+taizan-hokuto@users.noreply.github.com
64ec413bca Move parser functions to processor 2019-11-04 23:43:00 +09:00
55448286+taizan-hokuto@users.noreply.github.com
7c6e12cbe5 Change format of multithread parser to class 2019-11-04 23:29:00 +09:00
55448286+taizan-hokuto@users.noreply.github.com
3912758a52 Update README 2019-11-03 22:43:41 +09:00
55448286+taizan-hokuto@users.noreply.github.com
8bc209fde8 Fix renderer type check 2019-11-03 22:26:47 +09:00
55448286+taizan-hokuto@users.noreply.github.com
3b580690c7 Delete listen_manager 2019-11-03 21:57:30 +09:00
55448286+taizan-hokuto@users.noreply.github.com
44dc5ff1c3 Merge branch 'develop' 2019-11-03 19:53:10 +09:00
55448286+taizan-hokuto@users.noreply.github.com
0676ee5c8c Increment version 2019-11-03 19:50:22 +09:00
55448286+taizan-hokuto@users.noreply.github.com
89ddc0551f Export ChatProcessor 2019-11-03 19:41:15 +09:00
55448286+taizan-hokuto@users.noreply.github.com
0a8cd83d41 Update README 2019-11-03 19:38:09 +09:00
55448286+taizan-hokuto@users.noreply.github.com
cb505074f7 Modify comment 2019-11-03 19:04:02 +09:00
55448286+taizan-hokuto@users.noreply.github.com
e9e16b2bcc Export ChatProcessor 2019-11-03 19:02:40 +09:00
55448286+taizan-hokuto@users.noreply.github.com
c596911901 Add DEMO graphic 2019-11-03 18:30:44 +09:00
55448286+taizan-hokuto@users.noreply.github.com
275e1a7aa8 Fix comment typo 2019-11-03 18:01:00 +09:00
55448286+taizan-hokuto@users.noreply.github.com
737095e7fb Merge branch 'develop' 2019-11-03 16:09:41 +09:00
55448286+taizan-hokuto@users.noreply.github.com
10d9f76f67 Increment version 2019-11-03 16:09:13 +09:00
55448286+taizan-hokuto@users.noreply.github.com
34a74f28aa Add remarks 2019-11-03 16:08:50 +09:00
55448286+taizan-hokuto@users.noreply.github.com
c3c4827798 Fix currency name typo 2019-11-03 16:07:50 +09:00
55448286+taizan-hokuto@users.noreply.github.com
e930c75e2d Update README 2019-11-03 16:07:20 +09:00
55448286+taizan-hokuto@users.noreply.github.com
d5efede758 Merge branch 'develop' 2019-11-03 15:31:06 +09:00
55448286+taizan-hokuto@users.noreply.github.com
dc9b067d1d Increment version 2019-11-03 15:30:35 +09:00
55448286+taizan-hokuto@users.noreply.github.com
940e2a7431 Update manifest.in 2019-11-03 15:26:24 +09:00
16 changed files with 110 additions and 300 deletions

View File

@@ -1,4 +1,3 @@
include requirements.txt
include requirements_test.txt
exclude *.egg-info

View File

@@ -13,10 +13,14 @@ Other features:
+ Quick fetching of initial chat data by generating continuation params
instead of web scraping.
より詳細な説明は [wiki](https://github.com/taizan-hokuto/pytchat/wiki) をご参照ください。
## Install
```python
pip install pytchat
```
## Demo
![demo](https://taizan-hokuto.github.io/statics/demo.gif "demo")
## Examples
### on-demand mode
@@ -133,7 +137,12 @@ Structure of each item which got from items() function.
<tr>
<td>currency</td>
<td>str</td>
<td>ex. "USD"</td>
<td>ISO 4217 currency codes (ex. "USD")</td>
</tr>
<tr>
<td>bgColor</td>
<td>int</td>
<td>RGB Int</td>
</tr>
<tr>
<td>author</td>

View File

@@ -2,7 +2,7 @@
pytchat is a python library for fetching youtube live chat.
"""
__copyright__ = 'Copyright (C) 2019 taizan-hokuto'
__version__ = '0.0.1.9'
__version__ = '0.0.2.3'
__license__ = 'MIT'
__author__ = 'taizan-hokuto'
__author_email__ = '55448286+taizan-hokuto@users.noreply.github.com'
@@ -13,6 +13,7 @@ __all__ = ["core_async","core_multithread","processors"]
from .api import (
LiveChat,
LiveChatAsync,
ChatProcessor,
CompatibleProcessor,
SimpleDisplayProcessor,
JsonfileArchiveProcessor

View File

@@ -1,5 +1,6 @@
from .core_async.livechat import LiveChatAsync
from .core_multithread.livechat import LiveChat
from .processors.chat_processor import ChatProcessor
from .processors.default.processor import DefaultProcessor
from .processors.compatible.processor import CompatibleProcessor
from .processors.simple_display_processor import SimpleDisplayProcessor

View File

@@ -2,7 +2,7 @@
import asyncio
class Buffer(asyncio.Queue):
'''
チャットデータを格納するバッファの役割を持つLIFOキュー
チャットデータを格納するバッファの役割を持つFIFOキュー
Parameter
---------

View File

@@ -1,184 +0,0 @@
import asyncio
from .listener import AsyncListener
from .. import config
from .. import mylogger
import datetime
import os
import aiohttp
import signal
import threading
from .buffer import Buffer
from concurrent.futures import CancelledError
logger = mylogger.get_logger(__name__,mode=config.LOGGER_MODE)
class ListenManager:
'''
動画IDまたは動画IDのリストを受け取り、
動画IDに対応したListenerを生成・保持する。
#Attributes
----------
_listeners: dict
ListenManegerがつかんでいるListener達のリスト.
key:動画ID value:動画IDに対応するListener
_queue: Queue
動画IDを外部から受け渡しするためのキュー
_queueが空である間は、ンブロッキングで他のタスクを実行
_queueに動画IDが投入されると、_dequeueメソッドで
直ちにListenerを生成し返す。
_event: threading.Event
キーボードのCTRL+Cを検知するためのEventオブジェクト
'''
def __init__(self,interruptable = True):
#チャット監視中の動画リスト
self._listeners={}
self._tasks = []
#外部からvideoを受け取るためのキュー
self._queue = asyncio.Queue()
self._event = threading.Event()
self._ready_queue()
self._is_alive = True
#キーボードのCtrl+cを押したとき、_hundler関数を呼び出すように設定
signal.signal(signal.SIGINT, (lambda a, b: self._handler(self._event, a, b)))
def is_alive(self)->bool:
'''
ListenManagerが稼働中であるか。
True->稼働中
False->Ctrl+Cが押されて終了
'''
logger.debug(f'check is_alive() :{self._is_alive}')
return self._is_alive
def _handler(self, event, sig, handler):
'''
Ctrl+Cが押下されたとき、終了フラグをセットする。
'''
logger.debug('Ctrl+c pushed')
self._is_alive = False
logger.debug('terminating listeners.')
for listener in self._listeners.values():
listener.terminate()
logger.debug('end.')
def _ready_queue(self):
#loop = asyncio.get_event_loop()
self._tasks.append(
asyncio.create_task(self._dequeue())
)
async def set_video_ids(self,video_ids:list):
for video_id in video_ids:
if video_id:
await self._queue.put(video_id)
async def get_listener(self,video_id) -> AsyncListener:
return await self._create_listener(video_id)
# async def getlivechat(self,video_id):
# '''
# 指定された動画IDのチャットデータを返す
# Parameter
# ----------
# video_id: str
# 動画ID
# Return
# ----------
# 引数で受け取った動画IDに対応する
# Listenerオブジェクトへの参照
# '''
# logger.debug('manager get/create listener')
# listener = await self._create_listener(video_id)
# '''
# 上が完了しないうちに、下が呼び出される
# '''
# if not listener._initialized:
# await asyncio.sleep(2)
# return []
# if listener:
# #listener._isfirstrun=False
# return await listener.getlivechat()
async def _dequeue(self):
'''
キューに入った動画IDを
Listener登録に回す。
'''
while True:
video_id = await self._queue.get()
#listenerを登録、タスクとして実行する
logger.debug(f'deque got [{video_id}]')
await self._create_listener(video_id)
async def _create_listener(self, video_id) -> AsyncListener:
'''
Listenerを作成しチャット取得中リストに加え、
Listenerを返す
'''
if video_id is None or not isinstance(video_id, str):
raise TypeError('video_idは文字列でなければなりません')
if video_id in self._listeners:
return self._listeners[video_id]
else:
#listenerを登録する
listener = AsyncListener(video_id,interruptable = False,buffer = Buffer())
self._listeners.setdefault(video_id,listener)
#task = asyncio.ensure_future(listener.initialize())
#await asyncio.gather(listener.initialize())
#task.add_done_callback(self.finish)
#await listener.initialize()
#self._tasks.append(task)
return listener
def finish(self,sender):
try:
if sender.result():
video_id = sender.result()[0]
message = sender.result()[1]
#listener終了時のコールバック
#sender.result()[]でデータを取得できる
logger.info(f'終了しました VIDEO_ID:[{video_id}] message:{message}')
#logger.info(f'終了しました')
if video_id in self._listeners:
self._listeners.pop(video_id)
except CancelledError:
logger.debug('cancelled.')
def get_listeners(self):
return self._listeners
def shutdown(self):
'''
ListenManegerを終了する
'''
logger.debug("start shutdown")
self._is_alive =False
try:
#Listenerを停止する。
for listener in self._listeners.values():
listener.terminate()
#taskをキャンセルする。
for task in self._tasks:
if not task.done():
#print(task)
task.cancel()
except Exception as er:
logger.info(str(er),type(er))
logger.debug("finished.")
def get_tasks(self):
return self._tasks

View File

@@ -1,4 +1,4 @@
import aiohttp, asyncio, async_timeout
import aiohttp, asyncio
import datetime
import json
import random

View File

@@ -3,7 +3,7 @@ import queue
class Buffer(queue.Queue):
'''
チャットデータを格納するバッファの役割を持つLIFOキュー
チャットデータを格納するバッファの役割を持つFIFOキュー
Parameter
---------

View File

@@ -5,11 +5,12 @@ class ChatProcessor:
'''
def process(self, chat_components: list):
'''
チャットデータの加工を表すインターフェース
Listenerから呼び出される。
チャットデータの加工を表すインターフェース
LiveChatオブジェクトから呼び出される。
Parameter
----------
chat_components: list<component>
chat_components: [LIST:component]
component : dict {
"video_id" : str
動画ID

View File

@@ -1,43 +0,0 @@
from .renderer.textmessage import LiveChatTextMessageRenderer
from .renderer.paidmessage import LiveChatPaidMessageRenderer
from .renderer.paidsticker import LiveChatPaidStickerRenderer
from .renderer.legacypaid import LiveChatLegacyPaidMessageRenderer
def parse(sitem):
action = sitem.get("addChatItemAction")
if action:
item = action.get("item")
if item is None: return None
rd={}
try:
renderer = get_renderer(item)
if renderer == None:
return None
rd["kind"] = "youtube#liveChatMessage"
rd["etag"] = ""
rd["id"] = 'LCC.' + renderer.get_id()
rd["snippet"] = renderer.get_snippet()
rd["authorDetails"] = renderer.get_authordetails()
except (KeyError,TypeError,AttributeError) as e:
print(f"------{str(type(e))}-{str(e)}----------")
print(sitem)
return None
return rd
def get_renderer(item):
if item.get("liveChatTextMessageRenderer"):
renderer = LiveChatTextMessageRenderer(item)
elif item.get("liveChatPaidMessageRenderer"):
renderer = LiveChatPaidMessageRenderer(item)
elif item.get( "liveChatPaidStickerRenderer"):
renderer = LiveChatPaidStickerRenderer(item)
elif item.get("liveChatLegacyPaidMessageRenderer"):
renderer = LiveChatLegacyPaidMessageRenderer(item)
else:
renderer = None
return renderer

View File

@@ -1,11 +1,15 @@
from . import parser
import json
import os
import traceback
import datetime
import time
class CompatibleProcessor():
from .renderer.textmessage import LiveChatTextMessageRenderer
from .renderer.paidmessage import LiveChatPaidMessageRenderer
from .renderer.paidsticker import LiveChatPaidStickerRenderer
from .renderer.legacypaid import LiveChatLegacyPaidMessageRenderer
from ... import mylogger
from ... import config
logger = mylogger.get_logger(__name__,mode=config.LOGGER_MODE)
class CompatibleProcessor:
def process(self, chat_components: list):
chatlist = []
@@ -26,7 +30,7 @@ class CompatibleProcessor():
if action.get('addChatItemAction') is None: continue
if action['addChatItemAction'].get('item') is None: continue
chat = parser.parse(action)
chat = self.parse(action)
if chat:
chatlist.append(chat)
ret["pollingIntervalMillis"] = int(timeout*1000)
@@ -36,4 +40,42 @@ class CompatibleProcessor():
}
ret["items"] = chatlist
return ret
return ret
def parse(self, sitem):
action = sitem.get("addChatItemAction")
if action:
item = action.get("item")
if item is None: return None
rd={}
try:
renderer = self.get_renderer(item)
if renderer == None:
return None
rd["kind"] = "youtube#liveChatMessage"
rd["etag"] = ""
rd["id"] = 'LCC.' + renderer.get_id()
rd["snippet"] = renderer.get_snippet()
rd["authorDetails"] = renderer.get_authordetails()
except (KeyError,TypeError,AttributeError) as e:
logger.error(f"Error: {str(type(e))}-{str(e)}")
logger.error(f"item: {sitem}")
return None
return rd
def get_renderer(self, item):
if item.get("liveChatTextMessageRenderer"):
renderer = LiveChatTextMessageRenderer(item)
elif item.get("liveChatPaidMessageRenderer"):
renderer = LiveChatPaidMessageRenderer(item)
elif item.get( "liveChatPaidStickerRenderer"):
renderer = LiveChatPaidStickerRenderer(item)
elif item.get("liveChatLegacyPaidMessageRenderer"):
renderer = LiveChatLegacyPaidMessageRenderer(item)
else:
renderer = None
return renderer

View File

@@ -1,39 +0,0 @@
from .renderer.textmessage import LiveChatTextMessageRenderer
from .renderer.paidmessage import LiveChatPaidMessageRenderer
from .renderer.paidsticker import LiveChatPaidStickerRenderer
from .renderer.legacypaid import LiveChatLegacyPaidMessageRenderer
def parse(sitem):
action = sitem.get("addChatItemAction")
if action:
item = action.get("item")
if item is None: return None
try:
renderer = get_renderer(item)
if renderer == None:
return None
renderer.get_snippet()
renderer.get_authordetails()
except (KeyError,TypeError,AttributeError) as e:
print(f"------{str(type(e))}-{str(e)}----------")
print(sitem)
return None
return renderer
def get_renderer(item):
if item.get("liveChatTextMessageRenderer"):
renderer = LiveChatTextMessageRenderer(item)
elif item.get("liveChatPaidMessageRenderer"):
renderer = LiveChatPaidMessageRenderer(item)
elif item.get( "liveChatPaidStickerRenderer"):
renderer = LiveChatPaidStickerRenderer(item)
elif item.get("liveChatLegacyPaidMessageRenderer"):
renderer = LiveChatLegacyPaidMessageRenderer(item)
else:
renderer = None
return renderer

View File

@@ -1,6 +1,9 @@
from . import parser
import asyncio
import time
from .renderer.textmessage import LiveChatTextMessageRenderer
from .renderer.paidmessage import LiveChatPaidMessageRenderer
from .renderer.paidsticker import LiveChatPaidStickerRenderer
from .renderer.legacypaid import LiveChatLegacyPaidMessageRenderer
class Chatdata:
@@ -37,8 +40,40 @@ class DefaultProcessor:
if action.get('addChatItemAction') is None: continue
if action['addChatItemAction'].get('item') is None: continue
chat = parser.parse(action)
chat = self.parse(action)
if chat:
chatlist.append(chat)
return Chatdata(chatlist, float(timeout))
def parse(self, sitem):
action = sitem.get("addChatItemAction")
if action:
item = action.get("item")
if item is None: return None
try:
renderer = self.get_renderer(item)
if renderer == None:
return None
renderer.get_snippet()
renderer.get_authordetails()
except (KeyError,TypeError,AttributeError) as e:
print(f"------{str(type(e))}-{str(e)}----------")
print(sitem)
return None
return renderer
def get_renderer(self, item):
if item.get("liveChatTextMessageRenderer"):
renderer = LiveChatTextMessageRenderer(item)
elif item.get("liveChatPaidMessageRenderer"):
renderer = LiveChatPaidMessageRenderer(item)
elif item.get( "liveChatPaidStickerRenderer"):
renderer = LiveChatPaidStickerRenderer(item)
elif item.get("liveChatLegacyPaidMessageRenderer"):
renderer = LiveChatLegacyPaidMessageRenderer(item)
else:
renderer = None
return renderer

View File

@@ -23,7 +23,7 @@ symbols = {
"PLN\xa0": {"fxtext": "PLN", "jptext": "ポーランド・ズロチ"},
"R$": {"fxtext": "BRL", "jptext": "ブラジル・レアル"},
"RUB\xa0": {"fxtext": "RUB", "jptext": "ロシア・ルーブル"},
"SEK\xa0": {"fxtext": "SEK", "jptext": "スウェーデン・クロー"},
"SEK\xa0": {"fxtext": "SEK", "jptext": "スウェーデン・クロー"},
"£": {"fxtext": "GBP", "jptext": "英・ポンド"},
"": {"fxtext": "KRW", "jptext": "韓国・ウォン"},
"": {"fxtext": "EUR", "jptext": "欧・ユーロ"},

View File

@@ -31,7 +31,7 @@ class SimpleDisplayProcessor(ChatProcessor):
purchase_amount_text = ''
else:
root = ( action['addChatItemAction']['item'].get('liveChatPaidMessageRenderer') or
action['addChatItemAction']['item'].get('liveChatPaidMessageRenderer') )
action['addChatItemAction']['item'].get('liveChatPaidStickerRenderer') )
if root:
author_name = root['authorName']['simpleText']
message = self._parse_message(root.get('message'))

View File

@@ -31,16 +31,6 @@ assert url
with open('README.md', encoding='utf-8') as f:
long_description = f.read()
class CleanCommand(Command):
"""Custom clean command to tidy up the project root."""
user_options = []
def initialize_options(self):
pass
def finalize_options(self):
pass
def run(self):
#system('rm -vrf ./build ./dist ./*.pyc ./*.tgz ./*.egg-info')
system('rmdir /Q /S pytchat.egg-info, dist')
setup(
@@ -68,7 +58,5 @@ setup(
'License :: OSI Approved :: MIT License',
],
keywords='youtube livechat asyncio',
cmdclass={
'clean': CleanCommand,
}
)