From 0272319fa64f9d4e79e64340da8f1b929c7f5506 Mon Sep 17 00:00:00 2001 From: taizan-hokuto <55448286+taizan-hokuto@users.noreply.github.com> Date: Mon, 13 Jan 2020 13:09:51 +0900 Subject: [PATCH 1/9] Update README --- README.md | 88 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 52 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index d8c3c62..dfd9d1d 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,16 @@ pytchat is a python library for fetching youtube live chat. pytchat is a python library for fetching youtube live chat without using youtube api, Selenium or BeautifulSoup. +pytchatはAPIを使わずにYouTubeチャットを取得するための軽量pythonライブラリです。 + Other features: + Customizable chat data processors including youtube api compatible one. + Available on asyncio context. + Quick fetching of initial chat data by generating continuation params instead of web scraping. -For more detailed information, see [wiki](https://github.com/taizan-hokuto/pytchat/wiki). +For more detailed information, see [wiki](https://github.com/taizan-hokuto/pytchat/wiki).
+より詳細な解説は[wiki](https://github.com/taizan-hokuto/pytchat/wiki/Home-:)を参照してください。 ## Install ```python @@ -26,13 +29,17 @@ pip install pytchat ### on-demand mode ```python from pytchat import LiveChat +livechat = LiveChat(video_id = "Zvp1pJpie4I") -chat = LiveChat("DSGyEsJ17cI") -while chat.is_alive(): - data = chat.get() - for c in data.items: - print(f"{c.datetime} [{c.author.name}]-{c.message} {c.amountString}") - data.tick() +while livechat.is_alive(): + try: + chatdata = livechat.get() + for c in chatdata.items: + print(f"{c.datetime} [{c.author.name}]- {c.message}") + chatdata.tick() + except KeyboardInterrupt: + livechat.terminate() + break ``` ### callback mode @@ -40,17 +47,21 @@ while chat.is_alive(): from pytchat import LiveChat import time -#callback function is automatically called. -def display(data): - for c in data.items: - print(f"{c.datetime} [{c.author.name}]-{c.message} {c.amountString}") - data.tick() +def main(): + livechat = LiveChat(video_id = "Zvp1pJpie4I", callback = disp) + while livechat.is_alive(): + #other background operation. + time.sleep(1) + livechat.terminate() + +#callback function (automatically called) +def disp(chatdata): + for c in chatdata.items: + print(f"{c.datetime} [{c.author.name}]- {c.message}") + chatdata.tick() if __name__ == '__main__': - chat = LiveChat("DSGyEsJ17cI", callback = display) - while chat.is_alive(): - #other background operation. - time.sleep(3) + main() ``` @@ -61,16 +72,16 @@ from concurrent.futures import CancelledError import asyncio async def main(): - chat = LiveChatAsync("DSGyEsJ17cI", callback = func) - while chat.is_alive(): + livechat = LiveChatAsync("Zvp1pJpie4I", callback = func) + while livechat.is_alive(): #other background operation. await asyncio.sleep(3) #callback function is automatically called. -async def func(data): - for c in data.items: +async def func(chatdata): + for c in chatdata.items: print(f"{c.datetime} [{c.author.name}]-{c.message} {c.amountString}") - await data.tick_async() + await chatdata.tick_async() if __name__ == '__main__': try: @@ -86,18 +97,20 @@ if __name__ == '__main__': from pytchat import LiveChat, CompatibleProcessor import time -chat = LiveChat("DSGyEsJ17cI", +chat = LiveChat("Zvp1pJpie4I", processor = CompatibleProcessor() ) while chat.is_alive(): - data = chat.get() - polling = data['pollingIntervalMillis']/1000 - for c in data['items']: - if c.get('snippet'): - print(f"[{c['authorDetails']['displayName']}]" - f"-{c['snippet']['displayMessage']}") - time.sleep(polling/len(data['items'])) - + try: + data = chat.get() + polling = data['pollingIntervalMillis']/1000 + for c in data['items']: + if c.get('snippet'): + print(f"[{c['authorDetails']['displayName']}]" + f"-{c['snippet']['displayMessage']}") + time.sleep(polling/len(data['items'])) + except KeyboardInterrupt: + chat.terminate() ``` ### replay: If specified video is not live, @@ -110,18 +123,21 @@ def main(): #seektime (seconds): start position of chat. chat = LiveChat("ojes5ULOqhc", seektime = 60*30) print('Replay from 30:00') - while chat.is_alive(): - data = chat.get() - for c in data.items: - print(f"{c.elapsedTime} [{c.author.name}]-{c.message} {c.amountString}") - data.tick() + try: + while chat.is_alive(): + data = chat.get() + for c in data.items: + print(f"{c.elapsedTime} [{c.author.name}]-{c.message} {c.amountString}") + data.tick() + except KeyboardInterrupt: + chat.terminate() if __name__ == '__main__': main() ``` ## Structure of Default Processor -Each item can be got with items() function. +Each item can be got with `items` function. From 1643dd1ad1a7218a5a2aa0ce65a6b90b76915189 Mon Sep 17 00:00:00 2001 From: taizan-hokuto <55448286+taizan-hokuto@users.noreply.github.com> Date: Mon, 13 Jan 2020 18:32:36 +0900 Subject: [PATCH 2/9] Switch author type by icon type --- pytchat/processors/default/renderer/base.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/pytchat/processors/default/renderer/base.py b/pytchat/processors/default/renderer/base.py index 4ea2a83..49f23d2 100644 --- a/pytchat/processors/default/renderer/base.py +++ b/pytchat/processors/default/renderer/base.py @@ -1,5 +1,4 @@ from datetime import datetime - class Author: pass class BaseRenderer: @@ -60,6 +59,7 @@ class BaseRenderer: def get_badges(self,renderer): + #print(json.dumps(renderer,ensure_ascii=False,indent=2)) isVerified = False isChatOwner = False isChatSponsor = False @@ -67,16 +67,17 @@ class BaseRenderer: badges=renderer.get("authorBadges") if badges: for badge in badges: - author_type = badge["liveChatAuthorBadgeRenderer"]["accessibility"]["accessibilityData"]["label"] - if author_type == '確認済み': - isVerified = True - if author_type == '所有者': - isChatOwner = True - if 'メンバー' in author_type: + if badge["liveChatAuthorBadgeRenderer"].get("icon"): + author_type = badge["liveChatAuthorBadgeRenderer"]["icon"]["iconType"] + if author_type == 'VERIFIED': + isVerified = True + if author_type == 'OWNER': + isChatOwner = True + if author_type == 'MODERATOR': + isChatModerator = True + if badge["liveChatAuthorBadgeRenderer"].get("customThumbnail"): isChatSponsor = True self.get_badgeurl(badge) - if author_type == 'モデレーター': - isChatModerator = True return isVerified, isChatOwner, isChatSponsor, isChatModerator From 970d111e1b41c5d4484948503a120706a571a129 Mon Sep 17 00:00:00 2001 From: taizan-hokuto <55448286+taizan-hokuto@users.noreply.github.com> Date: Fri, 17 Jan 2020 23:40:49 +0900 Subject: [PATCH 3/9] Alert default processor attribute error : delete default exception handler Alert default processor attribute error : delete default exception handler Delete unnecessary lines Delete unnecessary lines --- pytchat/core_async/livechat.py | 16 +--------------- pytchat/processors/default/processor.py | 2 +- pytchat/processors/default/renderer/base.py | 1 - 3 files changed, 2 insertions(+), 17 deletions(-) diff --git a/pytchat/core_async/livechat.py b/pytchat/core_async/livechat.py index 49bbb60..b6f4f8e 100644 --- a/pytchat/core_async/livechat.py +++ b/pytchat/core_async/livechat.py @@ -109,9 +109,7 @@ class LiveChatAsync: self._topchat_only = topchat_only if not LiveChatAsync._setup_finished: LiveChatAsync._setup_finished = True - if exception_handler == None: - self._set_exception_handler(self._handle_exception) - else: + if exception_handler: self._set_exception_handler(exception_handler) if interruptable: signal.signal(signal.SIGINT, @@ -321,18 +319,6 @@ class LiveChatAsync: self._buffer.put_nowait({'chatdata':'','timeout':0}) logger.info(f'[{self.video_id}]finished.') - @classmethod - def _set_exception_handler(cls, handler): - loop = asyncio.get_event_loop() - loop.set_exception_handler(handler) - - @classmethod - def _handle_exception(cls, loop, context): - if not isinstance(context["exception"],CancelledError): - logger.error(f"Caught exception: {context}") - loop= asyncio.get_event_loop() - loop.create_task(cls.shutdown(None,None,None)) - @classmethod async def shutdown(cls, event, sig = None, handler=None): logger.debug("shutdown...") diff --git a/pytchat/processors/default/processor.py b/pytchat/processors/default/processor.py index 8c33c8e..85e6f3d 100644 --- a/pytchat/processors/default/processor.py +++ b/pytchat/processors/default/processor.py @@ -61,7 +61,7 @@ class DefaultProcessor(ChatProcessor): renderer.get_snippet() renderer.get_authordetails() - except (KeyError,TypeError,AttributeError) as e: + except (KeyError,TypeError) as e: logger.error(f"{str(type(e))}-{str(e)} sitem:{str(sitem)}") return None return renderer diff --git a/pytchat/processors/default/renderer/base.py b/pytchat/processors/default/renderer/base.py index 49f23d2..8f5970e 100644 --- a/pytchat/processors/default/renderer/base.py +++ b/pytchat/processors/default/renderer/base.py @@ -59,7 +59,6 @@ class BaseRenderer: def get_badges(self,renderer): - #print(json.dumps(renderer,ensure_ascii=False,indent=2)) isVerified = False isChatOwner = False isChatSponsor = False From 1c5852421b73dded345ca1d14337832c17532c57 Mon Sep 17 00:00:00 2001 From: taizan-hokuto <55448286+taizan-hokuto@users.noreply.github.com> Date: Sat, 18 Jan 2020 09:18:20 +0900 Subject: [PATCH 4/9] Undo _set_exception_handler --- pytchat/core_async/livechat.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pytchat/core_async/livechat.py b/pytchat/core_async/livechat.py index b6f4f8e..2f61aae 100644 --- a/pytchat/core_async/livechat.py +++ b/pytchat/core_async/livechat.py @@ -109,7 +109,9 @@ class LiveChatAsync: self._topchat_only = topchat_only if not LiveChatAsync._setup_finished: LiveChatAsync._setup_finished = True - if exception_handler: + if exception_handler == None: + self._set_exception_handler(self._handle_exception) + else: self._set_exception_handler(exception_handler) if interruptable: signal.signal(signal.SIGINT, @@ -319,6 +321,11 @@ class LiveChatAsync: self._buffer.put_nowait({'chatdata':'','timeout':0}) logger.info(f'[{self.video_id}]finished.') + @classmethod + def _set_exception_handler(cls, handler): + loop = asyncio.get_event_loop() + loop.set_exception_handler(handler) + @classmethod async def shutdown(cls, event, sig = None, handler=None): logger.debug("shutdown...") From 2573cc18de2d31dabb619b314a41d30c9cb53c4c Mon Sep 17 00:00:00 2001 From: taizan-hokuto <55448286+taizan-hokuto@users.noreply.github.com> Date: Sat, 18 Jan 2020 09:22:16 +0900 Subject: [PATCH 5/9] Fix setting exception_handler --- pytchat/core_async/livechat.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pytchat/core_async/livechat.py b/pytchat/core_async/livechat.py index 2f61aae..17140f8 100644 --- a/pytchat/core_async/livechat.py +++ b/pytchat/core_async/livechat.py @@ -109,9 +109,7 @@ class LiveChatAsync: self._topchat_only = topchat_only if not LiveChatAsync._setup_finished: LiveChatAsync._setup_finished = True - if exception_handler == None: - self._set_exception_handler(self._handle_exception) - else: + if exception_handler: self._set_exception_handler(exception_handler) if interruptable: signal.signal(signal.SIGINT, From efdf07e3de8de092674333022e6c24b424da660f Mon Sep 17 00:00:00 2001 From: taizan-hokuto <55448286+taizan-hokuto@users.noreply.github.com> Date: Sat, 18 Jan 2020 12:38:36 +0900 Subject: [PATCH 6/9] Make it possible to set custom logger --- pytchat/config/__init__.py | 6 ++---- pytchat/config/mylogger.py | 10 +++++----- pytchat/core_async/livechat.py | 24 ++++++++++++++---------- pytchat/core_multithread/livechat.py | 25 ++++++++++++++----------- pytchat/parser/live.py | 7 +------ 5 files changed, 36 insertions(+), 36 deletions(-) diff --git a/pytchat/config/__init__.py b/pytchat/config/__init__.py index 1a84b59..a36d2b0 100644 --- a/pytchat/config/__init__.py +++ b/pytchat/config/__init__.py @@ -1,13 +1,11 @@ import logging from . import mylogger -LOGGER_MODE = None - 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'} -def logger(module_name: str): - module_logger = mylogger.get_logger(module_name, mode = LOGGER_MODE) +def logger(module_name: str, loglevel = None): + module_logger = mylogger.get_logger(module_name, loglevel = loglevel) return module_logger diff --git a/pytchat/config/mylogger.py b/pytchat/config/mylogger.py index 83e325f..852d244 100644 --- a/pytchat/config/mylogger.py +++ b/pytchat/config/mylogger.py @@ -3,21 +3,21 @@ import logging import datetime -def get_logger(modname,mode=logging.DEBUG): +def get_logger(modname,loglevel=logging.DEBUG): logger = getLogger(modname) - if mode == None: + if loglevel == None: logger.addHandler(NullHandler()) return logger - logger.setLevel(mode) + logger.setLevel(loglevel) #create handler1 for showing info handler1 = StreamHandler() my_formatter = MyFormatter() handler1.setFormatter(my_formatter) - handler1.setLevel(mode) + handler1.setLevel(loglevel) logger.addHandler(handler1) #create handler2 for recording log file - if mode <= logging.DEBUG: + if loglevel <= logging.DEBUG: handler2 = FileHandler(filename="log.txt", encoding='utf-8') handler2.setLevel(logging.ERROR) handler2.setFormatter(my_formatter) diff --git a/pytchat/core_async/livechat.py b/pytchat/core_async/livechat.py index 17140f8..d22145f 100644 --- a/pytchat/core_async/livechat.py +++ b/pytchat/core_async/livechat.py @@ -17,7 +17,6 @@ from ..paramgen import liveparam, arcparam from ..processors.default.processor import DefaultProcessor from ..processors.combinator import Combinator -logger = config.logger(__name__) headers = config.headers MAX_RETRY = 10 @@ -74,6 +73,7 @@ class LiveChatAsync: ''' _setup_finished = False + _logger = config.logger(__name__) def __init__(self, video_id, seektime = 0, @@ -85,7 +85,8 @@ class LiveChatAsync: exception_handler = None, direct_mode = False, force_replay = False, - topchat_only = False + topchat_only = False, + logger = config.logger(__name__), ): self.video_id = video_id self.seektime = seektime @@ -107,6 +108,9 @@ class LiveChatAsync: self._first_fetch = True self._fetch_url = "live_chat/get_live_chat?continuation=" self._topchat_only = topchat_only + self._logger = logger + LiveChatAsync._logger = logger + if not LiveChatAsync._setup_finished: LiveChatAsync._setup_finished = True if exception_handler: @@ -185,14 +189,14 @@ class LiveChatAsync: continuation = metadata.get('continuation') except ChatParseException as e: #self.terminate() - logger.debug(f"[{self.video_id}]{str(e)}") + self._logger.debug(f"[{self.video_id}]{str(e)}") return except (TypeError , json.JSONDecodeError) : #self.terminate() - logger.error(f"{traceback.format_exc(limit = -1)}") + self._logger.error(f"{traceback.format_exc(limit = -1)}") return - logger.debug(f"[{self.video_id}]finished fetching chat.") + self._logger.debug(f"[{self.video_id}]finished fetching chat.") async def _check_pause(self, continuation): if self._pauser.empty(): @@ -252,7 +256,7 @@ class LiveChatAsync: await asyncio.sleep(1) continue else: - logger.error(f"[{self.video_id}]" + self._logger.error(f"[{self.video_id}]" f"Exceeded retry count. status_code={status_code}") return None return livechat_json @@ -307,7 +311,7 @@ class LiveChatAsync: try: self.terminate() except CancelledError: - logger.debug(f'[{self.video_id}]cancelled:{sender}') + self._logger.debug(f'[{self.video_id}]cancelled:{sender}') def terminate(self): ''' @@ -317,7 +321,7 @@ class LiveChatAsync: if self._direct_mode == False: #bufferにダミーオブジェクトを入れてis_alive()を判定させる self._buffer.put_nowait({'chatdata':'','timeout':0}) - logger.info(f'[{self.video_id}]finished.') + self._logger.info(f'[{self.video_id}]finished.') @classmethod def _set_exception_handler(cls, handler): @@ -326,12 +330,12 @@ class LiveChatAsync: @classmethod async def shutdown(cls, event, sig = None, handler=None): - logger.debug("shutdown...") + cls._logger.debug("shutdown...") tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()] [task.cancel() for task in tasks] - logger.debug(f"complete remaining tasks...") + cls._logger.debug(f"complete remaining tasks...") await asyncio.gather(*tasks,return_exceptions=True) loop = asyncio.get_event_loop() loop.stop() \ No newline at end of file diff --git a/pytchat/core_multithread/livechat.py b/pytchat/core_multithread/livechat.py index 71662e7..e2b0136 100644 --- a/pytchat/core_multithread/livechat.py +++ b/pytchat/core_multithread/livechat.py @@ -16,7 +16,6 @@ from ..paramgen import liveparam, arcparam from ..processors.default.processor import DefaultProcessor from ..processors.combinator import Combinator -logger = config.logger(__name__) headers = config.headers MAX_RETRY = 10 @@ -74,7 +73,9 @@ class LiveChat: _setup_finished = False #チャット監視中のListenerのリスト - _listeners= [] + _listeners = [] + _logger = config.logger(__name__) + def __init__(self, video_id, seektime = 0, processor = DefaultProcessor(), @@ -84,7 +85,8 @@ class LiveChat: done_callback = None, direct_mode = False, force_replay = False, - topchat_only = False + topchat_only = False, + logger = config.logger(__name__) ): self.video_id = video_id self.seektime = seektime @@ -106,6 +108,8 @@ class LiveChat: self._first_fetch = True self._fetch_url = "live_chat/get_live_chat?continuation=" self._topchat_only = topchat_only + self._logger = logger + LiveChat._logger = logger if not LiveChat._setup_finished: LiveChat._setup_finished = True if interruptable: @@ -115,7 +119,6 @@ class LiveChat: LiveChat._listeners.append(self) def _setup(self): - #logger.debug("setup") #direct modeがTrueでcallback未設定の場合例外発生。 if self._direct_mode: if self._callback is None: @@ -181,13 +184,13 @@ class LiveChat: time.sleep(diff_time if diff_time > 0 else 0) continuation = metadata.get('continuation') except ChatParseException as e: - logger.debug(f"[{self.video_id}]{str(e)}") + self._logger.debug(f"[{self.video_id}]{str(e)}") return except (TypeError , json.JSONDecodeError) : - logger.error(f"{traceback.format_exc(limit = -1)}") + self._logger.error(f"{traceback.format_exc(limit = -1)}") return - logger.debug(f"[{self.video_id}]finished fetching chat.") + self._logger.debug(f"[{self.video_id}]finished fetching chat.") def _check_pause(self, continuation): if self._pauser.empty(): @@ -244,7 +247,7 @@ class LiveChat: time.sleep(1) continue else: - logger.error(f"[{self.video_id}]" + self._logger.error(f"[{self.video_id}]" f"Exceeded retry count. status_code={status_code}") return None return livechat_json @@ -299,7 +302,7 @@ class LiveChat: try: self.terminate() except CancelledError: - logger.debug(f'[{self.video_id}]cancelled:{sender}') + self._logger.debug(f'[{self.video_id}]cancelled:{sender}') def terminate(self): ''' @@ -309,10 +312,10 @@ class LiveChat: if self._direct_mode == False: #bufferにダミーオブジェクトを入れてis_alive()を判定させる self._buffer.put({'chatdata':'','timeout':0}) - logger.info(f'[{self.video_id}]finished.') + self._logger.info(f'[{self.video_id}]finished.') @classmethod def shutdown(cls, event, sig = None, handler=None): - logger.debug("shutdown...") + cls._logger.debug("shutdown...") for t in LiveChat._listeners: t._is_alive = False \ No newline at end of file diff --git a/pytchat/parser/live.py b/pytchat/parser/live.py index b7b0614..7429e50 100644 --- a/pytchat/parser/live.py +++ b/pytchat/parser/live.py @@ -5,16 +5,12 @@ Parser of live chat JSON. """ import json -from .. import config from .. exceptions import ( ResponseContextError, NoContentsException, NoContinuationsException, ChatParseException ) - -logger = config.logger(__name__) - class Parser: __slots__ = ['is_replay'] @@ -65,8 +61,7 @@ class Parser: raise ChatParseException('Finished chat data') unknown = list(cont.keys())[0] if unknown: - logger.debug(f"Received unknown continuation type:{unknown}") - metadata = cont.get(unknown) + raise ChatParseException(f"Received unknown continuation type:{unknown}") else: raise ChatParseException('Cannot extract continuation data') return self._create_data(metadata, contents) From f8fa0e394ece95d0dc08e750f3a5e9b925a8a92f Mon Sep 17 00:00:00 2001 From: taizan-hokuto <55448286+taizan-hokuto@users.noreply.github.com> Date: Sat, 18 Jan 2020 13:02:43 +0900 Subject: [PATCH 7/9] Delete json_display_processor --- pytchat/processors/json_display_processor.py | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 pytchat/processors/json_display_processor.py diff --git a/pytchat/processors/json_display_processor.py b/pytchat/processors/json_display_processor.py deleted file mode 100644 index ba8e442..0000000 --- a/pytchat/processors/json_display_processor.py +++ /dev/null @@ -1,13 +0,0 @@ -import json -from .chat_processor import ChatProcessor - -class JsonDisplayProcessor(ChatProcessor): - - def process(self,chat_components: list): - if chat_components: - for component in chat_components: - chatdata = component.get('chatdata') - if chatdata: - for chat in chatdata: - print(json.dumps(chat,ensure_ascii=False)[:200]) - From bbd01d6523b69a4d754894918bf08209002ba56d Mon Sep 17 00:00:00 2001 From: taizan-hokuto <55448286+taizan-hokuto@users.noreply.github.com> Date: Sat, 18 Jan 2020 13:12:52 +0900 Subject: [PATCH 8/9] Increment version --- pytchat/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytchat/__init__.py b/pytchat/__init__.py index 0782706..f6aed8e 100644 --- a/pytchat/__init__.py +++ b/pytchat/__init__.py @@ -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.5.0' +__version__ = '0.0.5.1' __license__ = 'MIT' __author__ = 'taizan-hokuto' __author_email__ = '55448286+taizan-hokuto@users.noreply.github.com' From 76b126faf20907a7ebf36b29caff86294d78689b Mon Sep 17 00:00:00 2001 From: taizan-hokuto <55448286+taizan-hokuto@users.noreply.github.com> Date: Sat, 18 Jan 2020 14:43:06 +0900 Subject: [PATCH 9/9] Increment version --- pytchat/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytchat/__init__.py b/pytchat/__init__.py index f6aed8e..0029913 100644 --- a/pytchat/__init__.py +++ b/pytchat/__init__.py @@ -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.5.1' +__version__ = '0.0.5.1.3' __license__ = 'MIT' __author__ = 'taizan-hokuto' __author_email__ = '55448286+taizan-hokuto@users.noreply.github.com'
name