diff --git a/breadtube_bot/bot.py b/breadtube_bot/bot.py index fed6a67..8a3b41e 100644 --- a/breadtube_bot/bot.py +++ b/breadtube_bot/bot.py @@ -28,6 +28,7 @@ class Bot: INIT_MESSAGE: str = ('Bot initialized.\nThis is the current configuration used.\n' 'You can upload a new one to update the configuration.') MAX_DOWNLOAD_SIZE: int = 50_000 + YT_CHANNEL_NAME_URL = 'https://www.youtube.com/@' class Task(Enum): DELETE_MESSAGES = 1 @@ -41,7 +42,7 @@ class Bot: raise RuntimeError('Cannot current bot version') return tomllib.loads(pyproject_path.read_text(encoding='utf-8'))['project']['version'] - def __init__(self, bot_token: str, guild_id: int, config: Config | None = None, + def __init__(self, bot_token: str, guild_id: int, yt_api_key: str, config: Config | None = None, log_level: int = logging.INFO): self.config: Config = config or Config() self.guild_id = guild_id @@ -86,7 +87,7 @@ class Bot: raise RuntimeError("Couldn't initialize bot channel/role/permission") self.bot_channel: TextChannel = bot_channel - self.yt_manager = YoutubeManager(logger=self.logger) + self.yt_manager = YoutubeManager(api_key=yt_api_key, logger=self.logger) self._yt_subscriptions: Subscriptions = {} self._scan_bot_channel() self.tasks.append(( @@ -144,10 +145,31 @@ class Bot: new_subscriptions: Subscriptions | None = None delayed_delete: dict[int, Message] = {} immediate_delete: dict[int, Message] = {} + for message in messages: if init_message_found: - self.logger.debug('Marking message for immediate deletion (init found): %s', message) - immediate_delete[message.id] = message + if message.author.id != self.bot_user.id and message.content.startswith(self.YT_CHANNEL_NAME_URL): + self.logger.debug('Parsing message for youtube channel name conversion: %s', message) + answers: list[str] = [] + for line in message.content.splitlines(): + if line.startswith(self.YT_CHANNEL_NAME_URL): + channel_name = line.rstrip()[len(self.YT_CHANNEL_NAME_URL):] + channel_id = self.yt_manager.request_channel_id( + channel_name, request_timeout=self.config.request_timeout) + answers.append(f'{channel_name} -> {channel_id}') + bot_message = self.discord_manager.create_message(self.bot_channel, { + 'content': '\n'.join(answers), + 'message_reference': MessageReference( + type=MessageReferenceType.DEFAULT, + message_id=message.id, + channel_id=self.bot_channel.id, + guild_id=None, + fail_if_not_exists=None)}, request_timeout=self.config.request_timeout) + delayed_delete[bot_message.id] = bot_message + delayed_delete[message.id] = message + else: + self.logger.debug('Marking message for immediate deletion (init found): %s', message) + immediate_delete[message.id] = message continue if len(message.attachments) <= 0: diff --git a/breadtube_bot/youtube_manager.py b/breadtube_bot/youtube_manager.py index afe861d..6fd9259 100644 --- a/breadtube_bot/youtube_manager.py +++ b/breadtube_bot/youtube_manager.py @@ -2,6 +2,7 @@ from __future__ import annotations from dataclasses import dataclass from datetime import datetime +import json import time from typing import TYPE_CHECKING import urllib.error @@ -26,11 +27,12 @@ class YoutubeManager: remaining: int next_reset: float - def __init__(self, logger: logging.Logger): + def __init__(self, api_key: str, logger: logging.Logger): + self._api_key = api_key self._logger = logger self.rate_limit = self.RateLimit(remaining=self.DEFAULT_DAILY_POINTS, next_reset=time.time() + 24 * 3600) - def _request(self, url: str, request_timeout: float, expected_status: int = 200) -> tuple[HTTPHeaders, str]: + def _request(self, url: str, request_timeout: float, expected_status: int = 200) -> tuple[HTTPHeaders, dict]: if time.time() >= self.rate_limit.next_reset: self.rate_limit.next_reset = time.time() + 24 * 3600 self.rate_limit.remaining = self.DEFAULT_DAILY_POINTS @@ -41,14 +43,14 @@ class YoutubeManager: self.rate_limit.remaining -= 1 request = urllib.request.Request(url) - # request.add_header('Accept', 'application/json') + request.add_header('Accept', 'application/json') try: with urllib.request.urlopen(request, timeout=request_timeout) as response: if response.status != expected_status: raise RuntimeError( f'Unexpected YT status {response.status} (expected: {expected_status})' f' -> {response.read().decode()}') - return dict(response.getheaders()), response.read().decode() + return dict(response.getheaders()), json.loads(response.read().decode()) except urllib.error.HTTPError as error: raise RuntimeError( f'HTTP error calling API ({url}): {error}:\n' @@ -69,6 +71,13 @@ class YoutubeManager: except Exception as error: raise RuntimeError(f'Exception calling YouTube shorts ({video_id}): {error}') from error + def request_channel_id(self, channel_name: str, request_timeout: float) -> str: + url = ('https://www.googleapis.com/youtube/v3/channels?part=snippet' + f'&forHandle%40={channel_name}&key={self._api_key}') + self._logger.debug('YoutubeManager: request channel id for channel %s', channel_name) + _, info = self._request(url=url, request_timeout=request_timeout) + return info['items'][0]['id'] + @staticmethod def _parse_rss_data(data) -> tuple[ChannelInfo, list[VideoInfo]]: videos: list[VideoInfo] = [] diff --git a/pyproject.toml b/pyproject.toml index 006b07c..a47a898 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ inline-quotes = "single" [tool.ruff.lint.pylint] max-args=16 -max-branches=32 +max-branches=34 max-locals=16 max-nested-blocks=8 max-public-methods=16 @@ -55,7 +55,7 @@ max-returns=8 max-statements=96 [tool.ruff.lint.mccabe] -max-complexity = 32 +max-complexity = 34 [tool.coverage.run] command_line = "-m pytest" diff --git a/start.py b/start.py index 5ac4cb2..a0434af 100644 --- a/start.py +++ b/start.py @@ -16,7 +16,9 @@ def main(): del arguments bot_token = Path('data/discord_bot_token.txt').read_text(encoding='utf-8').strip() - manager = Bot(bot_token=bot_token, guild_id=guild_id, log_level=logging.DEBUG if debug_mode else logging.INFO) + yt_api_key = Path('data/google_api_key.txt').read_text(encoding='utf-8').strip() + manager = Bot(bot_token=bot_token, guild_id=guild_id, yt_api_key=yt_api_key, + log_level=logging.DEBUG if debug_mode else logging.INFO) try: manager.run() except KeyboardInterrupt: diff --git a/tests/test_youtube_manager.py b/tests/test_youtube_manager.py index 5bfc1dc..7b51b84 100644 --- a/tests/test_youtube_manager.py +++ b/tests/test_youtube_manager.py @@ -7,7 +7,7 @@ from breadtube_bot.youtube_subscription import ChannelInfo def test_rss_parsing(): logger = logging.getLogger('breadtube-bot-test') - manager = YoutubeManager(logger=logger) + manager = YoutubeManager('', logger=logger) channel_info, videos = manager._parse_rss_data(Path('tests/data/rss_feed_sample.xml').read_text(encoding='utf-8')) assert channel_info == ChannelInfo( channel_id='UCFemKOoYVrTGUhuVzuNPt4A', title='Actu Réfractaire',