diff --git a/chzzkpy/__init__.py b/chzzkpy/__init__.py index ce0e76a..da98eaf 100644 --- a/chzzkpy/__init__.py +++ b/chzzkpy/__init__.py @@ -39,7 +39,7 @@ __author__ = "gunyu1019" __license__ = "MIT" __copyright__ = "Copyright 2024-present gunyu1019" -__version__ = "1.0.3" # version_info.to_string() +__version__ = "1.0.4" # version_info.to_string() class VersionInfo(NamedTuple): @@ -57,5 +57,5 @@ def to_string(self) -> str: version_info: VersionInfo = VersionInfo( - major=1, minor=0, micro=3, release_level=None, serial=0 + major=1, minor=0, micro=4, release_level=None, serial=0 ) diff --git a/chzzkpy/chat/__init__.py b/chzzkpy/chat/__init__.py index 9ab433e..43dbcd9 100644 --- a/chzzkpy/chat/__init__.py +++ b/chzzkpy/chat/__init__.py @@ -46,9 +46,9 @@ VideoDonationExtra, MissionDonationExtra, SubscriptionExtra, - SystemExtraParameter, NoticeExtra, SystemExtra, + SystemExtraParameter, ) from .profile import Profile, ActivityBadge, StreamingProperty, Badge from .recent_chat import RecentChat diff --git a/chzzkpy/chat/chat_client.py b/chzzkpy/chat/chat_client.py index 067bac8..7b2f33b 100644 --- a/chzzkpy/chat/chat_client.py +++ b/chzzkpy/chat/chat_client.py @@ -23,11 +23,11 @@ from __future__ import annotations +import aiohttp import asyncio +import datetime import logging -from typing import Any, Optional, Callable, Coroutine, TYPE_CHECKING - -import aiohttp +from typing import Any, Optional, Callable, Coroutine, Literal, TYPE_CHECKING from .enums import ChatCmd from .error import ChatConnectFailed @@ -36,6 +36,7 @@ from .state import ConnectionState from ..client import Client from ..error import LoginRequired +from ..live import LiveDetail, LiveStatus from ..http import ChzzkAPISession if TYPE_CHECKING: @@ -84,6 +85,7 @@ def __init__( dispatch=self.dispatch, handler=handler, client=self ) self._gateway: Optional[ChzzkWebSocket] = None + self._status: Literal["OPEN", "CLOSE"] = None def _session_initial_set(self): self._api_session = ChzzkAPISession(loop=self.loop) @@ -115,6 +117,7 @@ async def connect(self) -> None: if status is None: raise ChatConnectFailed(self.channel_id) self.chat_channel_id = status.chat_channel_id + self._status = status.status if self._game_session.has_login: user = await self.user() @@ -130,10 +133,31 @@ async def close(self): self._ready.clear() if self._gateway is not None: - await self._gateway.socket.close() + await self._gateway.close() await self.ws_session.close() await super().close() + async def _confirm_live_status(self): + live_status = await self.live_status(channel_id=self.channel_id) + if live_status is None: + return + + if self._status != live_status.status: + self._status = live_status.status + if self._status == "OPEN": + self.dispatch("broadcast_open") + elif self._status == "CLOSE": + self.dispatch("broadcast_close") + + if live_status.chat_channel_id == self.chat_channel_id: + return + + _log.debug("A chat_channel_id has been updated. Reconnect websocket.") + await self._gateway.close() + + self.chat_channel_id = live_status.chat_channel_id + raise ReconnectWebsocket() + async def polling(self) -> None: session_id: Optional[str] = None while not self.is_closed: @@ -152,10 +176,22 @@ async def polling(self) -> None: ) session_id = self._gateway.session_id + last_check_time = datetime.datetime.now() + while True: await self._gateway.poll_event() + + # Confirm chat-channel-id with live_status() method. + # When a streamer starts a new broadcast, a chat-channel-id will regenrated. + # + # https://github.com/gunyu1019/chzzkpy/issues/31 + relative_time = datetime.datetime.now() - last_check_time + if relative_time.total_seconds() >= 59: + last_check_time = datetime.datetime.now() + await self._confirm_live_status() except ReconnectWebsocket: self.dispatch("disconnect") + session_id = None continue # Event Handler @@ -411,3 +447,17 @@ async def blind_message(self, message: ChatMessage) -> None: streaming_channel_id=message.extras.streaming_channel_id, ) return + + async def live_status( + self, channel_id: Optional[str] = None + ) -> Optional[LiveStatus]: + if channel_id is None: + channel_id = self.channel_id + return await super().live_status(channel_id) + + async def live_detail( + self, channel_id: Optional[str] = None + ) -> Optional[LiveDetail]: + if channel_id is None: + channel_id = self.channel_id + return await super().live_detail(channel_id) diff --git a/chzzkpy/chat/gateway.py b/chzzkpy/chat/gateway.py index 27b020e..e78ea8f 100644 --- a/chzzkpy/chat/gateway.py +++ b/chzzkpy/chat/gateway.py @@ -65,6 +65,10 @@ def set_hook(self, cmd: ChatCmd, coro_func: Callable[..., Any]): def remove_hook(self, cmd: ChatCmd): self._event_hook[cmd] = None + async def close(self): + self.session_id = None + await self.socket.close() + @classmethod async def new_session( cls, diff --git a/chzzkpy/error.py b/chzzkpy/error.py index 3bd7475..7c21d43 100644 --- a/chzzkpy/error.py +++ b/chzzkpy/error.py @@ -31,6 +31,7 @@ class ChzzkpyException(Exception): class LoginRequired(ChzzkpyException): """Exception that’s raised when a method need login.""" + def __init__(self): super(LoginRequired, self).__init__( "This method(feature) needs to login. Please use `login()` method." @@ -39,6 +40,7 @@ def __init__(self): class NotFound(ChzzkpyException): """Exception that’s raised for when status code 404 occurs.""" + def __init__(self, message: Optional[str] = None): if message is None: message = "Not Found" @@ -47,6 +49,7 @@ def __init__(self, message: Optional[str] = None): class HTTPException(ChzzkpyException): """Exception that’s raised when an HTTP request operation fails.""" + def __init__(self, code: int, message: Optional[str] = None): if message is None: message = f"Reponsed error code ({code})"