Compare commits

...

28 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
55448286+taizan-hokuto@users.noreply.github.com
8fcb3ab50f Merge branch 'develop' 2019-11-03 15:15:47 +09:00
55448286+taizan-hokuto@users.noreply.github.com
8ef6474c90 Increment version 2019-11-03 15:14:52 +09:00
55448286+taizan-hokuto@users.noreply.github.com
5da28e4d89 Update setup.py 2019-11-03 15:03:09 +09:00
55448286+taizan-hokuto@users.noreply.github.com
8902955fed Update setup.py 2019-11-03 15:02:22 +09:00
16 changed files with 115 additions and 289 deletions

View File

@@ -1 +1,3 @@
include requirements.txt include requirements.txt
include requirements_test.txt

View File

@@ -13,10 +13,14 @@ 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) をご参照ください。
## Install ## Install
```python ```python
pip install pytchat pip install pytchat
``` ```
## Demo
![demo](https://taizan-hokuto.github.io/statics/demo.gif "demo")
## Examples ## Examples
### on-demand mode ### on-demand mode
@@ -133,7 +137,12 @@ Structure of each item which got from items() function.
<tr> <tr>
<td>currency</td> <td>currency</td>
<td>str</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>
<tr> <tr>
<td>author</td> <td>author</td>

View File

@@ -2,7 +2,7 @@
pytchat is a python library for fetching youtube live chat. pytchat is a python library for fetching youtube live chat.
""" """
__copyright__ = 'Copyright (C) 2019 taizan-hokuto' __copyright__ = 'Copyright (C) 2019 taizan-hokuto'
__version__ = '0.0.1.8' __version__ = '0.0.2.3'
__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'
@@ -13,6 +13,7 @@ __all__ = ["core_async","core_multithread","processors"]
from .api import ( from .api import (
LiveChat, LiveChat,
LiveChatAsync, LiveChatAsync,
ChatProcessor,
CompatibleProcessor, CompatibleProcessor,
SimpleDisplayProcessor, SimpleDisplayProcessor,
JsonfileArchiveProcessor JsonfileArchiveProcessor

View File

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

View File

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

View File

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

View File

@@ -5,11 +5,12 @@ class ChatProcessor:
''' '''
def process(self, chat_components: list): def process(self, chat_components: list):
''' '''
チャットデータの加工を表すインターフェース チャットデータの加工を表すインターフェース
Listenerから呼び出される。 LiveChatオブジェクトから呼び出される。
Parameter Parameter
---------- ----------
chat_components: list<component> chat_components: [LIST:component]
component : dict { component : dict {
"video_id" : str "video_id" : str
動画ID 動画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 datetime
import time 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): def process(self, chat_components: list):
chatlist = [] chatlist = []
@@ -26,7 +30,7 @@ class CompatibleProcessor():
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 = parser.parse(action) chat = self.parse(action)
if chat: if chat:
chatlist.append(chat) chatlist.append(chat)
ret["pollingIntervalMillis"] = int(timeout*1000) ret["pollingIntervalMillis"] = int(timeout*1000)
@@ -36,4 +40,42 @@ class CompatibleProcessor():
} }
ret["items"] = chatlist 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 asyncio
import time import time
from .renderer.textmessage import LiveChatTextMessageRenderer
from .renderer.paidmessage import LiveChatPaidMessageRenderer
from .renderer.paidsticker import LiveChatPaidStickerRenderer
from .renderer.legacypaid import LiveChatLegacyPaidMessageRenderer
class Chatdata: class Chatdata:
@@ -37,8 +40,40 @@ 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 = parser.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):
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": "ポーランド・ズロチ"}, "PLN\xa0": {"fxtext": "PLN", "jptext": "ポーランド・ズロチ"},
"R$": {"fxtext": "BRL", "jptext": "ブラジル・レアル"}, "R$": {"fxtext": "BRL", "jptext": "ブラジル・レアル"},
"RUB\xa0": {"fxtext": "RUB", "jptext": "ロシア・ルーブル"}, "RUB\xa0": {"fxtext": "RUB", "jptext": "ロシア・ルーブル"},
"SEK\xa0": {"fxtext": "SEK", "jptext": "スウェーデン・クロー"}, "SEK\xa0": {"fxtext": "SEK", "jptext": "スウェーデン・クロー"},
"£": {"fxtext": "GBP", "jptext": "英・ポンド"}, "£": {"fxtext": "GBP", "jptext": "英・ポンド"},
"": {"fxtext": "KRW", "jptext": "韓国・ウォン"}, "": {"fxtext": "KRW", "jptext": "韓国・ウォン"},
"": {"fxtext": "EUR", "jptext": "欧・ユーロ"}, "": {"fxtext": "EUR", "jptext": "欧・ユーロ"},

View File

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

View File

@@ -1,6 +1,6 @@
from setuptools import setup, find_packages from setuptools import setup, find_packages, Command
from codecs import open from codecs import open
from os import path from os import path, system
import re import re
package_name = "pytchat" package_name = "pytchat"
@@ -14,7 +14,6 @@ def _test_requirements():
return [name.rstrip() for name in open(path.join(root_dir, 'requirements_test.txt')).readlines()] return [name.rstrip() for name in open(path.join(root_dir, 'requirements_test.txt')).readlines()]
with open(path.join(root_dir, package_name, '__init__.py')) as f: with open(path.join(root_dir, package_name, '__init__.py')) as f:
init_text = f.read() init_text = f.read()
version = re.search(r'__version__\s*=\s*[\'\"](.+?)[\'\"]', init_text).group(1) version = re.search(r'__version__\s*=\s*[\'\"](.+?)[\'\"]', init_text).group(1)
@@ -32,6 +31,8 @@ assert url
with open('README.md', encoding='utf-8') as f: with open('README.md', encoding='utf-8') as f:
long_description = f.read() long_description = f.read()
setup( setup(
name=package_name, name=package_name,
packages=find_packages(), packages=find_packages(),