diff --git a/breadtube_bot/bot.py b/breadtube_bot/bot.py index e44664f..d381294 100644 --- a/breadtube_bot/bot.py +++ b/breadtube_bot/bot.py @@ -11,6 +11,7 @@ import time import tomllib from typing import Any, TYPE_CHECKING import traceback +import urllib.parse from .config import Config from .discord_manager import ApiEncoder, DiscordManager @@ -177,6 +178,8 @@ class Bot: if new_config is None and content.startswith(b'config'): try: self.config = Config.from_str(content.decode()) + if self.config.to_str() != content.decode(): + new_config = self.config except RuntimeError as error: self.logger.error('Cannot load config from init message: %s', error) has_error = True @@ -345,9 +348,15 @@ class Bot: subscription.video_list, key=lambda x: x.snippet.publishTime, reverse=True)[:internal_size] subscription.last_update = time.time() - @staticmethod - def _video_message_content(video: SearchResultItem) -> str: - return f'https://www.youtube.com/video/{video.id.videoId}' + def _video_message_content(self, video: SearchResultItem) -> str: + return (self.config.youtube_channel_video_message + .replace('{{video_id}}', str(video.id.videoId)) + .replace('{{video_title}}', str(urllib.parse.unquote(video.snippet.title))) + .replace('{{video_description}}', str(video.snippet.description)) + .replace('{{video_publish_time}}', video.snippet.publishTime.isoformat()) + .replace('{{channel_id}}', str(video.snippet.channelId)) + .replace('{{channel_title}}', str(video.snippet.channelTitle)) + ) def _refresh_sub(self, subscription: SubscriptionInfo, channel_dict: dict[str, TextChannel], category_ranges: list[tuple[int, int, ChannelCategory]]): diff --git a/breadtube_bot/config.py b/breadtube_bot/config.py index 692061f..b6bb23a 100644 --- a/breadtube_bot/config.py +++ b/breadtube_bot/config.py @@ -14,6 +14,7 @@ class Config: unmanaged_categories: str = '' youtube_channel_refresh_interval: float = 3600 youtube_channel_video_count: int = 10 + youtube_channel_video_message: str = '[{{video_title}}](https://www.youtube.com/video/{{video_id}})' def to_str(self) -> str: return '\n'.join(['config', *[f'{k}={v}' for k, v in asdict(self).items()]]) diff --git a/breadtube_bot/discord_manager.py b/breadtube_bot/discord_manager.py index f597e99..66b51fd 100644 --- a/breadtube_bot/discord_manager.py +++ b/breadtube_bot/discord_manager.py @@ -30,6 +30,7 @@ class ApiEncoder(json.JSONEncoder): class DiscordManager: MIN_API_VERSION = 9 + TOO_MANY_REQUEST_STATUS = 429 @dataclass class RateLimit: @@ -88,15 +89,31 @@ class DiscordManager: else: request.add_header('Content-Type', 'application/json') request.add_header('Authorization', f'Bot {self._bot_token}') - try: + + def _request() -> tuple[int, dict, bytes | None]: + nonlocal request, request_timeout with urllib.request.urlopen(request, timeout=request_timeout) as response: - if response.status != expected_code: - raise RuntimeError( - f'Unexpected code {response.status} (expected: {expected_code}) -> {response.read().decode()}') - body = response.read() headers = dict(response.getheaders()) + return response.status, headers, response.read() + + try: + body = b'' + try: + status, headers, body = _request() + except urllib.error.HTTPError as error: + if error.status != self.TOO_MANY_REQUEST_STATUS: + raise error + status = error.status + headers = dict(error.headers) + + self._update_rate_limit(headers) + if status == self.TOO_MANY_REQUEST_STATUS: + self._logger.warning('Warning: too many request -> retrying') + status, headers, body = _request() self._update_rate_limit(headers) - return headers, json.loads(body.decode()) if body else None + if status != expected_code: + raise RuntimeError(f'Unexpected code {status} (expected: {expected_code}) -> {body}') + return headers, json.loads(body.decode()) if body else None except urllib.error.HTTPError as error: raise RuntimeError( f'HTTP error calling API ({url}): {error}:\nHeaders:\n{error.headers}Body:\n{error.read()}') from error