From 90564bd170d0b1bc54334c726d0d8c05dd4c7d4d Mon Sep 17 00:00:00 2001 From: Jordan Ella <45442073+jordanella@users.noreply.github.com> Date: Tue, 16 Jan 2024 23:06:38 -0500 Subject: [PATCH 1/4] Command Abstraction Abstracted commands outside of the Searcharr class and implemented dynamic loading and registration of command modules to allow for future extensibility. --- commands/__init__.py | 166 +++++++++++++++++ commands/book.py | 17 ++ commands/help.py | 27 +++ commands/movie.py | 18 ++ commands/series.py | 17 ++ commands/start.py | 37 ++++ commands/users.py | 40 +++++ searcharr.py | 418 +------------------------------------------ 8 files changed, 330 insertions(+), 410 deletions(-) create mode 100644 commands/__init__.py create mode 100644 commands/book.py create mode 100644 commands/help.py create mode 100644 commands/movie.py create mode 100644 commands/series.py create mode 100644 commands/start.py create mode 100644 commands/users.py diff --git a/commands/__init__.py b/commands/__init__.py new file mode 100644 index 0000000..99c852c --- /dev/null +++ b/commands/__init__.py @@ -0,0 +1,166 @@ +import os +import sys +from importlib import util + +sys.path.append(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) + +import settings + +from telegram.error import BadRequest +import traceback + + +class Command: + _list = [] + _name = "" + _command_aliases = None + _validation_checks = [] + auth_level = None + + def __init__(self): + pass + + def __init_subclass__(cls, **kwargs): + super().__init_subclass__(**kwargs) + instance = cls() + cls._list.append(instance) + + def _register(self, searcharr_instance, logger_instance): + self.searcharr = searcharr_instance + self.logger = logger_instance + + def _strip_entities(self, message): + text = message.text + entities = message.parse_entities() + self.logger.debug(f"{entities=}") + for v in entities.values(): + text = text.replace(v, "") + text = text.replace(" ", "").strip() + self.logger.debug(f"Stripped entities from message [{message.text}]: [{text}]") + return text + + def _xlate(self, key, **kwargs): + return self.searcharr._xlate(key, **kwargs) + + def _xlate_aliases(self, message, aliases, arg = None): + joined_message = self._xlate( + message, + commands = " OR ".join( + [ + f"`/{c}{("" if arg == None else " <" + self._xlate(arg) + ">")}`" + for c in aliases + ] + ), + ) + return joined_message + + def _validate_authenticated(self, update): + self.auth_level = self.searcharr._authenticated(update.message.from_user.id) + if self.auth_level: + return True + else: + update.message.reply_text(self._xlate_aliases("auth_required", settings.searcharr_start_command_aliases, "password")) + return None + + def _validate_radarr_enabled(self, update): + if settings.radarr_enabled: + return True + else: + update.message.reply_text(self._xlate("radarr_disabled")) + return None + + def _validate_sonarr_enabled(self, update): + if settings.sonarr_enabled: + return True + else: + update.message.reply_text(self._xlate("sonarr_disabled")) + return None + + def _validate_readarr_enabled(self, update): + if settings.readarr_enabled: + return True + else: + update.message.reply_text(self._xlate("readarr_disabled")) + return None + + def _validated(self, update): + for check in self._validation_checks: + method = getattr(self, check) + if not method(update): + return None + return True + + def _execute(self, update, context): + self.logger.debug(f"Received {self._name} cmd from [{update.message.from_user.username}]") + if not self._validated(update): + return + self._action(update, context) + + def _action(self, update, context): + pass + + def _search_collection(self, update, context, kind, plural, search_function, command_aliases): + title = self._strip_entities(update.message) + + if not len(title): + x_title = self._xlate("title").title() + response = self._xlate_aliases("include_" + kind + "_title_in_cmd", command_aliases, x_title) + update.message.reply_text(response) + return + + results = search_function(title) + cid = self.searcharr._generate_cid() + self.searcharr._create_conversation( + id = cid, + username = str(update.message.from_user.username), + kind = kind, + results = results, + ) + + if not len(results): + update.message.reply_text(self._xlate("no_matching_" + plural)) + return + + r = results[0] + reply_message, reply_markup = self.searcharr._prepare_response( + kind, r, cid, 0, len(results) + ) + try: + context.bot.sendPhoto( + chat_id=update.message.chat.id, + photo=r["remotePoster"], + caption=reply_message, + reply_markup=reply_markup, + ) + except BadRequest as e: + if str(e) in self._bad_request_poster_error_messages: + self.logger.error( + f"Error sending photo [{r['remotePoster']}]: BadRequest: {e}. Attempting to send with default poster..." + ) + context.bot.sendPhoto( + chat_id=update.message.chat.id, + photo="https://artworks.thetvdb.com/banners/images/missing/movie.jpg", + caption=reply_message, + reply_markup=reply_markup, + ) + else: + raise + + +def load_module(path): + name = os.path.split(path)[-1] + spec = util.spec_from_file_location(name, path) + module = util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + +path = os.path.abspath(__file__) +dirpath = os.path.dirname(path) + +for fname in os.listdir(dirpath): + if not fname.startswith('.') and \ + not fname.startswith('__') and fname.endswith('.py'): + try: + load_module(os.path.join(dirpath, fname)) + except Exception: + traceback.print_exc() \ No newline at end of file diff --git a/commands/book.py b/commands/book.py new file mode 100644 index 0000000..ec5476c --- /dev/null +++ b/commands/book.py @@ -0,0 +1,17 @@ +from commands import Command +import settings + + +class Book(Command): + _name = "book" + _command_aliases = settings.readarr_book_command_aliases + _validation_checks = ["_validate_authenticated", "_validate_readarr_enabled"] + + def _action(self, update, context): + self._search_collection( + update=update, + kind="book", + plural="book", + search_function=self.searcharr.readarr.lookup_book, + command_aliases=settings.readarr_book_command_aliases + ) \ No newline at end of file diff --git a/commands/help.py b/commands/help.py new file mode 100644 index 0000000..4016db8 --- /dev/null +++ b/commands/help.py @@ -0,0 +1,27 @@ +from commands import Command +import settings + + +class Help(Command): + _name = "help" + _command_aliases = settings.searcharr_help_command_aliases + _validation_checks = ["_validate_authenticated"] + + def _action(self, update, context): + response = "" + if self.searcharr.sonarr: + sonarr_help = self._xlate_aliases("help_sonarr", settings.sonarr_series_command_aliases, "title") + response += f" {sonarr_help}" + if self.searcharr.radarr: + radarr_help = self._xlate_aliases("help_radarr", settings.radarr_movie_command_aliases, "title") + response += f" {radarr_help}" + if self.searcharr.readarr: + readarr_help = self._xlate_aliases("help_readarr", settings.readarr_book_command_aliases, "title") + response += f" {readarr_help}" + if response == "": + response = self._xlate("no_features") + + if self.auth_level == 2: + response += " " + self._xlate_aliases("admin_help", settings.searcharr_users_command_aliases) + + update.message.reply_text(response) \ No newline at end of file diff --git a/commands/movie.py b/commands/movie.py new file mode 100644 index 0000000..16fea2b --- /dev/null +++ b/commands/movie.py @@ -0,0 +1,18 @@ +from commands import Command +import settings + + +class Movie(Command): + _name = "movie" + _command_aliases = settings.radarr_movie_command_aliases + _validation_checks = ["_validate_authenticated", "_validate_radarr_enabled"] + + def _action(self, update, context): + self._search_collection( + update=update, + context=context, + kind="movie", + plural="movies", + search_function=self.searcharr.radarr.lookup_movie, + command_aliases=settings.radarr_movie_command_aliases + ) \ No newline at end of file diff --git a/commands/series.py b/commands/series.py new file mode 100644 index 0000000..7aec481 --- /dev/null +++ b/commands/series.py @@ -0,0 +1,17 @@ +from commands import Command +import settings + + +class Series(Command): + _name = "series" + _command_aliases = settings.sonarr_series_command_aliases + _validation_checks = ['_validate_authenticated', '_validate_sonarr_enabled'] + + def _action(self, update, context): + self._search_collection( + update=update, + kind="series", + plural="series", + search_function = self.searcharr.sonarr.lookup_series, + command_aliases = settings.sonarr_series_command_aliases + ) \ No newline at end of file diff --git a/commands/start.py b/commands/start.py new file mode 100644 index 0000000..e06d279 --- /dev/null +++ b/commands/start.py @@ -0,0 +1,37 @@ +from commands import Command +import settings + + +class Start(Command): + _name = "start" + _command_aliases = settings.searcharr_start_command_aliases + _validation_checks = [] + + def _action(self, update, context): + password = self._strip_entities(update.message) + self.logger.debug(f"{update}") + + if password and password == settings.searcharr_admin_password: + self.searcharr._add_user( + id=update.message.from_user.id, + username=update.message.from_user.username, + admin=1, + ) + update.message.reply_text( + self._xlate_aliases("admin_auth_success", settings.searcharr_help_command_aliases) + ) + elif self.searcharr._authenticated(update): + update.message.reply_text( + self._xlate_aliases("already_authenticated", settings.searcharr_help_command_aliases) + ) + + elif password == settings.searcharr_password: + self.searcharr._add_user( + id=update.message.from_user.id, + username=update.message.from_user.username, + ) + update.message.reply_text( + self._xlate_aliases("auth_successful", settings.searcharr_help_command_aliases) + ) + else: + update.message.reply_text(self._xlate("incorrect_pw")) \ No newline at end of file diff --git a/commands/users.py b/commands/users.py new file mode 100644 index 0000000..6b47a31 --- /dev/null +++ b/commands/users.py @@ -0,0 +1,40 @@ +from commands import Command +import settings + + +class Users(Command): + _name = "users" + _command_aliases = settings.searcharr_users_command_aliases + _validation_checks = ["_validate_authenticated"] + + def _action(self, update, context): + if self.auth_level != 2: + update.message.reply_text( + self._xlate_aliases("admin_auth_required", settings.searcharr_start_command_aliases, "admin_password") + ) + return + + results = self.searcharr._get_users() + cid = self.searcharr._generate_cid() + self.searcharr._create_conversation( + id=cid, + username=str(update.message.from_user.username), + kind="users", + results=results, + ) + + if not len(results): + update.message.reply_text(self._xlate("no_users_found")) + else: + reply_message, reply_markup = self.searcharr._prepare_response_users( + cid, + results, + 0, + 5, + len(results), + ) + context.bot.sendMessage( + chat_id=update.message.chat.id, + text=reply_message, + reply_markup=reply_markup, + ) \ No newline at end of file diff --git a/searcharr.py b/searcharr.py index fa08ec9..12b07b8 100644 --- a/searcharr.py +++ b/searcharr.py @@ -23,6 +23,7 @@ import sonarr import readarr import settings +from commands import Command __version__ = "3.2.2" @@ -412,316 +413,6 @@ def __init__(self, token): 'No searcharr_users_command_aliases setting found. Please add searcharr_users_command_aliases to settings.py (e.g. searcharr_users_command_aliases=["users"]. Defaulting to ["users"].' ) - def cmd_start(self, update, context): - logger.debug(f"Received start cmd from [{update.message.from_user.username}]") - password = self._strip_entities(update.message) - if password and password == settings.searcharr_admin_password: - self._add_user( - id=update.message.from_user.id, - username=str(update.message.from_user.username), - admin=1, - ) - update.message.reply_text( - self._xlate( - "admin_auth_success", - commands=" OR ".join( - [f"`/{c}`" for c in settings.searcharr_help_command_aliases] - ), - ) - ) - elif self._authenticated(update.message.from_user.id): - update.message.reply_text( - self._xlate( - "already_authenticated", - commands=" OR ".join( - [f"`/{c}`" for c in settings.searcharr_help_command_aliases] - ), - ) - ) - elif password == settings.searcharr_password: - self._add_user( - id=update.message.from_user.id, - username=str(update.message.from_user.username), - ) - update.message.reply_text( - self._xlate( - "auth_successful", - commands=" OR ".join( - [f"`/{c}`" for c in settings.searcharr_help_command_aliases] - ), - ) - ) - else: - update.message.reply_text(self._xlate("incorrect_pw")) - - def cmd_book(self, update, context): - logger.debug(f"Received book cmd from [{update.message.from_user.username}]") - if not self._authenticated(update.message.from_user.id): - update.message.reply_text( - self._xlate( - "auth_required", - commands=" OR ".join( - [ - f"`/{c} <{self._xlate('password')}>`" - for c in settings.searcharr_start_command_aliases - ] - ), - ) - ) - return - if not settings.readarr_enabled: - update.message.reply_text(self._xlate("readarr_disabled")) - return - title = self._strip_entities(update.message) - if not len(title): - x_title = self._xlate("title").title() - update.message.reply_text( - self._xlate( - "include_book_title_in_cmd", - commands=" OR ".join( - [ - f"`/{c} {x_title}`" - for c in settings.readarr_book_command_aliases - ] - ), - ) - ) - return - results = self.readarr.lookup_book(title) - cid = self._generate_cid() - # self.conversations.update({cid: {"cid": cid, "type": "book", "results": results}}) - self._create_conversation( - id=cid, - username=str(update.message.from_user.username), - kind="book", - results=results, - ) - - if not len(results): - update.message.reply_text(self._xlate("no_matching_books")) - else: - r = results[0] - reply_message, reply_markup = self._prepare_response( - "book", r, cid, 0, len(results) - ) - try: - context.bot.sendPhoto( - chat_id=update.message.chat.id, - photo=r["remotePoster"], - caption=reply_message, - reply_markup=reply_markup, - ) - except BadRequest as e: - if str(e) in self._bad_request_poster_error_messages: - logger.error( - f"Error sending photo [{r['remotePoster']}]: BadRequest: {e}. Attempting to send with default poster..." - ) - context.bot.sendPhoto( - chat_id=update.message.chat.id, - photo="https://artworks.thetvdb.com/banners/images/missing/movie.jpg", - caption=reply_message, - reply_markup=reply_markup, - ) - else: - raise - - def cmd_movie(self, update, context): - logger.debug(f"Received movie cmd from [{update.message.from_user.username}]") - if not self._authenticated(update.message.from_user.id): - update.message.reply_text( - self._xlate( - "auth_required", - commands=" OR ".join( - [ - f"`/{c} <{self._xlate('password')}>`" - for c in settings.searcharr_start_command_aliases - ] - ), - ) - ) - return - if not settings.radarr_enabled: - update.message.reply_text(self._xlate("radarr_disabled")) - return - title = self._strip_entities(update.message) - if not len(title): - x_title = self._xlate("title").title() - update.message.reply_text( - self._xlate( - "include_movie_title_in_cmd", - commands=" OR ".join( - [ - f"`/{c} {x_title}`" - for c in settings.radarr_movie_command_aliases - ] - ), - ) - ) - return - results = self.radarr.lookup_movie(title) - cid = self._generate_cid() - # self.conversations.update({cid: {"cid": cid, "type": "movie", "results": results}}) - self._create_conversation( - id=cid, - username=str(update.message.from_user.username), - kind="movie", - results=results, - ) - - if not len(results): - update.message.reply_text(self._xlate("no_matching_movies")) - else: - r = results[0] - reply_message, reply_markup = self._prepare_response( - "movie", r, cid, 0, len(results) - ) - try: - context.bot.sendPhoto( - chat_id=update.message.chat.id, - photo=r["remotePoster"], - caption=reply_message, - reply_markup=reply_markup, - ) - except BadRequest as e: - if str(e) in self._bad_request_poster_error_messages: - logger.error( - f"Error sending photo [{r['remotePoster']}]: BadRequest: {e}. Attempting to send with default poster..." - ) - context.bot.sendPhoto( - chat_id=update.message.chat.id, - photo="https://artworks.thetvdb.com/banners/images/missing/movie.jpg", - caption=reply_message, - reply_markup=reply_markup, - ) - else: - raise - - def cmd_series(self, update, context): - logger.debug(f"Received series cmd from [{update.message.from_user.username}]") - if not self._authenticated(update.message.from_user.id): - update.message.reply_text( - self._xlate( - "auth_required", - commands=" OR ".join( - [ - f"`/{c} <{self._xlate('password')}>`" - for c in settings.searcharr_start_command_aliases - ] - ), - ) - ) - return - if not settings.sonarr_enabled: - update.message.reply_text(self._xlate("sonarr_disabled")) - return - title = self._strip_entities(update.message) - if not len(title): - x_title = self._xlate("title").title() - update.message.reply_text( - self._xlate( - "include_series_title_in_cmd", - commands=" OR ".join( - [ - f"`/{c} {x_title}`" - for c in settings.sonarr_series_command_aliases - ] - ), - ) - ) - return - results = self.sonarr.lookup_series(title) - cid = self._generate_cid() - # self.conversations.update({cid: {"cid": cid, "type": "series", "results": results}}) - self._create_conversation( - id=cid, - username=str(update.message.from_user.username), - kind="series", - results=results, - ) - - if not len(results): - update.message.reply_text(self._xlate("no_matching_series")) - else: - r = results[0] - reply_message, reply_markup = self._prepare_response( - "series", r, cid, 0, len(results) - ) - try: - context.bot.sendPhoto( - chat_id=update.message.chat.id, - photo=r["remotePoster"], - caption=reply_message, - reply_markup=reply_markup, - ) - except BadRequest as e: - if str(e) in self._bad_request_poster_error_messages: - logger.error( - f"Error sending photo [{r['remotePoster']}]: BadRequest: {e}. Attempting to send with default poster..." - ) - context.bot.sendPhoto( - chat_id=update.message.chat.id, - photo="https://artworks.thetvdb.com/banners/images/missing/movie.jpg", - caption=reply_message, - reply_markup=reply_markup, - ) - else: - raise - - def cmd_users(self, update, context): - logger.debug(f"Received users cmd from [{update.message.from_user.username}]") - auth_level = self._authenticated(update.message.from_user.id) - if not auth_level: - update.message.reply_text( - self._xlate( - "auth_required", - commands=" OR ".join( - [ - f"`/{c} <{self._xlate('password')}>`" - for c in settings.searcharr_start_command_aliases - ] - ), - ) - ) - return - elif auth_level != 2: - update.message.reply_text( - self._xlate( - "admin_auth_required", - commands=" OR ".join( - [ - f"`/{c} <{self._xlate('admin_password')}>`" - for c in settings.searcharr_start_command_aliases - ] - ), - ) - ) - return - - results = self._get_users() - cid = self._generate_cid() - # self.conversations.update({cid: {"cid": cid, "type": "users", "results": results}}) - self._create_conversation( - id=cid, - username=str(update.message.from_user.username), - kind="users", - results=results, - ) - if not len(results): - update.message.reply_text(self._xlate("no_users_found")) - else: - reply_message, reply_markup = self._prepare_response_users( - cid, - results, - 0, - 5, - len(results), - ) - context.bot.sendMessage( - chat_id=update.message.chat.id, - text=reply_message, - reply_markup=reply_markup, - ) - def callback(self, update, context): query = update.callback_query logger.debug( @@ -1664,109 +1355,16 @@ def handle_error(self, update, context): except Exception: pass - def cmd_help(self, update, context): - logger.debug(f"Received help cmd from [{update.message.from_user.username}]") - auth_level = self._authenticated(update.message.from_user.id) - if not auth_level: - update.message.reply_text( - self._xlate( - "auth_required", - commands=" OR ".join( - [ - f"`/{c} <{self._xlate('password')}>`" - for c in settings.searcharr_start_command_aliases - ] - ), - ) - ) - return - sonarr_help = self._xlate( - "help_sonarr", - series_commands=" OR ".join( - [ - f"`/{c} {self._xlate('title').title()}`" - for c in settings.sonarr_series_command_aliases - ] - ), - ) - radarr_help = self._xlate( - "help_radarr", - movie_commands=" OR ".join( - [ - f"`/{c} {self._xlate('title').title()}`" - for c in settings.radarr_movie_command_aliases - ] - ), - ) - if self.readarr: - readarr_help = self._xlate( - "help_readarr", - book_commands=" OR ".join( - [ - f"`/{c} {self._xlate('title').title()}`" - for c in settings.readarr_book_command_aliases - ] - ), - ) - - if ( - settings.sonarr_enabled - or settings.radarr_enabled - or settings.readarr_enabled - ): - resp = "" - if settings.sonarr_enabled: - resp += f" {sonarr_help}" - if settings.radarr_enabled: - resp += f" {radarr_help}" - if settings.readarr_enabled: - resp += f" {readarr_help}" - else: - resp = self._xlate("no_features") - - if auth_level == 2: - resp += " " + self._xlate( - "admin_help", - commands=" OR ".join( - [f"/{c}" for c in settings.searcharr_users_command_aliases] - ), - ) - - update.message.reply_text(resp) - - def _strip_entities(self, message): - text = message.text - entities = message.parse_entities() - logger.debug(f"{entities=}") - for v in entities.values(): - text = text.replace(v, "") - text = text.replace(" ", "").strip() - logger.debug(f"Stripped entities from message [{message.text}]: [{text}]") - return text - def run(self): self._init_db() updater = Updater(self.token, use_context=True) - for c in settings.searcharr_help_command_aliases: - logger.debug(f"Registering [/{c}] as a help command") - updater.dispatcher.add_handler(CommandHandler(c, self.cmd_help)) - for c in settings.searcharr_start_command_aliases: - logger.debug(f"Registering [/{c}] as a start command") - updater.dispatcher.add_handler(CommandHandler(c, self.cmd_start)) - if self.readarr: - for c in settings.readarr_book_command_aliases: - logger.debug(f"Registering [/{c}] as a book command") - updater.dispatcher.add_handler(CommandHandler(c, self.cmd_book)) - for c in settings.radarr_movie_command_aliases: - logger.debug(f"Registering [/{c}] as a movie command") - updater.dispatcher.add_handler(CommandHandler(c, self.cmd_movie)) - for c in settings.sonarr_series_command_aliases: - logger.debug(f"Registering [/{c}] as a series command") - updater.dispatcher.add_handler(CommandHandler(c, self.cmd_series)) - for c in settings.searcharr_users_command_aliases: - logger.debug(f"Registering [/{c}] as a users command") - updater.dispatcher.add_handler(CommandHandler(c, self.cmd_users)) + for command in Command._list: + command._register(searcharr_instance = self, logger_instance = logger) + for c in command._command_aliases: + logger.debug(f"Registering [/{c}] as a {command._name} command") + updater.dispatcher.add_handler(CommandHandler(c, command._execute)) + updater.dispatcher.add_handler(CallbackQueryHandler(self.callback)) if not self.DEV_MODE: updater.dispatcher.add_error_handler(self.handle_error) @@ -2116,4 +1714,4 @@ def _xlate(self, key, **kwargs): args = parse_args() logger = set_up_logger("searcharr", args.verbose, args.console_logging) tgr = Searcharr(settings.tgram_token) - tgr.run() + tgr.run() \ No newline at end of file From 1928f2aa4dfc642b6ce1f9caf7b888bb8e8dfd5f Mon Sep 17 00:00:00 2001 From: Jordan Ella <45442073+jordanella@users.noreply.github.com> Date: Wed, 17 Jan 2024 21:53:56 -0500 Subject: [PATCH 2/4] Utility and KeyboardButton Modules Moved _load_language, _xlate, and _strip_entities functions to a utility module. Language is loaded into the utility module to curb dependency on the Searcharr class for translation function in commands or other modules. The KeyboardInlineButton definitions were moved to a new buttons module to make the _prepare_reponse and _prepare_user_response functions more readable. Revised the calculations for _prepare_response_users pagination to eliminate offset and num so that it can use the same navigation button code as _prepare_response. --- buttons.py | 149 ++++++++++++ commands/__init__.py | 68 ++---- commands/help.py | 11 +- commands/start.py | 14 +- commands/users.py | 6 +- searcharr.py | 536 +++++++++++++++++-------------------------- util.py | 64 ++++++ 7 files changed, 465 insertions(+), 383 deletions(-) create mode 100644 buttons.py create mode 100644 util.py diff --git a/buttons.py b/buttons.py new file mode 100644 index 0000000..e372b7e --- /dev/null +++ b/buttons.py @@ -0,0 +1,149 @@ +from util import xlate +from telegram import InlineKeyboardButton + + +class NavButtons(object): + def prev(self, cid, i): + return InlineKeyboardButton( + xlate("prev_button"), callback_data=f"{cid}^^^{i}^^^prev" + ) + + def next(self, cid, i, total_results): + return InlineKeyboardButton( + xlate("next_button"), callback_data=f"{cid}^^^{i}^^^next" + ) + + def done(self, cid, i): + return InlineKeyboardButton( + xlate("done"), callback_data=f"{cid}^^^{i}^^^done" + ) + +class ExternalButtons(object): + def imdb(self, r): + return InlineKeyboardButton( + "IMDb", url=f"https://imdb.com/title/{r['imdbId']}" + ) + + def tvdb(self, r): + return InlineKeyboardButton( + "tvdb", url=f"https://thetvdb.com/series/{r['titleSlug']}" + ) + + def tmdb(self, r): + return InlineKeyboardButton( + "TMDB", url=f"https://www.themoviedb.org/movie/{r['tmdbId']}" + ) + + def link(self, link): + return InlineKeyboardButton( + link["name"], url=link["url"] + ) + +class ActionButtons(object): + def add(self, kind, cid, i): + InlineKeyboardButton( + xlate("add_button", kind=xlate(kind).title()), + callback_data=f"{cid}^^^{i}^^^add", + ), + + def already_added(self, cid, i): + return InlineKeyboardButton( + xlate("already_added_button"), + callback_data=f"{cid}^^^{i}^^^noop", + ) + + def cancel(self, cid, i): + return InlineKeyboardButton( + xlate("cancel_search_button"), + callback_data=f"{cid}^^^{i}^^^cancel", + ) + + def series_anime(self, cid, i): + return InlineKeyboardButton( + xlate("add_series_anime_button"), + callback_data=f"{cid}^^^{i}^^^add^^st=a", + ) + +class AddButtons(object): + def tag(self, tag, cid, i): + return InlineKeyboardButton( + xlate("add_tag_button", tag=tag["label"]), + callback_data=f"{cid}^^^{i}^^^add^^tt={tag['id']}", + ) + + def finished_tagging(self, cid, i): + return InlineKeyboardButton( + xlate("finished_tagging_button"), + callback_data=f"{cid}^^^{i}^^^add^^td=1", + ) + + def monitor(self, o, k, cid, i): + return InlineKeyboardButton( + xlate("monitor_button", option=o), + callback_data=f"{cid}^^^{i}^^^add^^m={k}", + ) + + def quality(self, q, cid, i): + return InlineKeyboardButton( + xlate("add_quality_button", quality=q["name"]), + callback_data=f"{cid}^^^{i}^^^add^^q={q['id']}", + ) + + def metadata(self, m, cid, i): + return InlineKeyboardButton( + xlate("add_metadata_button", metadata=m["name"]), + callback_data=f"{cid}^^^{i}^^^add^^m={m['id']}", + ) + + def path(self, p, cid, i): + return InlineKeyboardButton( + xlate("add_path_button", path=p["path"]), + callback_data=f"{cid}^^^{i}^^^add^^p={p['id']}", + ) + +class UserButtons(object): + def remove(self, u, cid): + return InlineKeyboardButton( + xlate("remove_user_button"), + callback_data=f"{cid}^^^{u['id']}^^^remove_user", + ) + + def username(self, u, cid): + return InlineKeyboardButton( + f"{u['username'] if u['username'] != 'None' else u['id']}", + callback_data=f"{cid}^^^{u['id']}^^^noop", + ) + + def admin(self, u, cid): + return InlineKeyboardButton( + xlate("remove_admin_button") if u["admin"] else xlate("make_admin_button"), + callback_data=f"{cid}^^^{u['id']}^^^{'remove_admin' if u['admin'] else 'make_admin'}", + ) + +class KeyboardButtons(object): + def __init__(self): + self.nav_buttons = NavButtons() + self.ext_buttons = ExternalButtons() + self.act_buttons = ActionButtons() + self.add_buttons = AddButtons() + self.user_buttons = UserButtons() + + @property + def nav(self): + return self.nav_buttons + + @property + def ext(self): + return self.ext_buttons + + @property + def act(self): + return self.act_buttons + + @property + def add(self): + return self.add_buttons + + @property + def user(self): + return self.user_buttons \ No newline at end of file diff --git a/commands/__init__.py b/commands/__init__.py index 99c852c..7925515 100644 --- a/commands/__init__.py +++ b/commands/__init__.py @@ -1,17 +1,17 @@ import os import sys -from importlib import util - +from importlib import util as import_util sys.path.append(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) - -import settings - from telegram.error import BadRequest import traceback +import settings +import util +from util import xlate, xlate_aliases + class Command: - _list = [] + _dict = {} _name = "" _command_aliases = None _validation_checks = [] @@ -23,64 +23,38 @@ def __init__(self): def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) instance = cls() - cls._list.append(instance) + cls._dict[cls._name] = instance - def _register(self, searcharr_instance, logger_instance): + def _inject_dependency(self, searcharr_instance): self.searcharr = searcharr_instance - self.logger = logger_instance - - def _strip_entities(self, message): - text = message.text - entities = message.parse_entities() - self.logger.debug(f"{entities=}") - for v in entities.values(): - text = text.replace(v, "") - text = text.replace(" ", "").strip() - self.logger.debug(f"Stripped entities from message [{message.text}]: [{text}]") - return text - - def _xlate(self, key, **kwargs): - return self.searcharr._xlate(key, **kwargs) - - def _xlate_aliases(self, message, aliases, arg = None): - joined_message = self._xlate( - message, - commands = " OR ".join( - [ - f"`/{c}{("" if arg == None else " <" + self._xlate(arg) + ">")}`" - for c in aliases - ] - ), - ) - return joined_message def _validate_authenticated(self, update): self.auth_level = self.searcharr._authenticated(update.message.from_user.id) if self.auth_level: return True else: - update.message.reply_text(self._xlate_aliases("auth_required", settings.searcharr_start_command_aliases, "password")) + update.message.reply_text(xlate_aliases("auth_required", settings.searcharr_start_command_aliases, "password")) return None def _validate_radarr_enabled(self, update): if settings.radarr_enabled: return True else: - update.message.reply_text(self._xlate("radarr_disabled")) + update.message.reply_text(xlate("radarr_disabled")) return None def _validate_sonarr_enabled(self, update): if settings.sonarr_enabled: return True else: - update.message.reply_text(self._xlate("sonarr_disabled")) + update.message.reply_text(xlate("sonarr_disabled")) return None def _validate_readarr_enabled(self, update): if settings.readarr_enabled: return True else: - update.message.reply_text(self._xlate("readarr_disabled")) + update.message.reply_text(xlate("readarr_disabled")) return None def _validated(self, update): @@ -91,20 +65,20 @@ def _validated(self, update): return True def _execute(self, update, context): - self.logger.debug(f"Received {self._name} cmd from [{update.message.from_user.username}]") + util.log.debug(f"Received {self._name} cmd from [{update.message.from_user.username}]") if not self._validated(update): return self._action(update, context) def _action(self, update, context): pass - + def _search_collection(self, update, context, kind, plural, search_function, command_aliases): - title = self._strip_entities(update.message) + title = util.strip_entities(update.message) if not len(title): - x_title = self._xlate("title").title() - response = self._xlate_aliases("include_" + kind + "_title_in_cmd", command_aliases, x_title) + x_title = xlate("title").title() + response = xlate_aliases("include_" + kind + "_title_in_cmd", command_aliases, x_title) update.message.reply_text(response) return @@ -118,7 +92,7 @@ def _search_collection(self, update, context, kind, plural, search_function, com ) if not len(results): - update.message.reply_text(self._xlate("no_matching_" + plural)) + update.message.reply_text(xlate("no_matching_" + plural)) return r = results[0] @@ -134,7 +108,7 @@ def _search_collection(self, update, context, kind, plural, search_function, com ) except BadRequest as e: if str(e) in self._bad_request_poster_error_messages: - self.logger.error( + util.log.error( f"Error sending photo [{r['remotePoster']}]: BadRequest: {e}. Attempting to send with default poster..." ) context.bot.sendPhoto( @@ -149,8 +123,8 @@ def _search_collection(self, update, context, kind, plural, search_function, com def load_module(path): name = os.path.split(path)[-1] - spec = util.spec_from_file_location(name, path) - module = util.module_from_spec(spec) + spec = import_util.spec_from_file_location(name, path) + module = import_util.module_from_spec(spec) spec.loader.exec_module(module) return module diff --git a/commands/help.py b/commands/help.py index 4016db8..9bfe545 100644 --- a/commands/help.py +++ b/commands/help.py @@ -1,5 +1,6 @@ from commands import Command import settings +from util import xlate, xlate_aliases class Help(Command): @@ -10,18 +11,18 @@ class Help(Command): def _action(self, update, context): response = "" if self.searcharr.sonarr: - sonarr_help = self._xlate_aliases("help_sonarr", settings.sonarr_series_command_aliases, "title") + sonarr_help = xlate_aliases("help_sonarr", settings.sonarr_series_command_aliases, "title") response += f" {sonarr_help}" if self.searcharr.radarr: - radarr_help = self._xlate_aliases("help_radarr", settings.radarr_movie_command_aliases, "title") + radarr_help = xlate_aliases("help_radarr", settings.radarr_movie_command_aliases, "title") response += f" {radarr_help}" if self.searcharr.readarr: - readarr_help = self._xlate_aliases("help_readarr", settings.readarr_book_command_aliases, "title") + readarr_help = xlate_aliases("help_readarr", settings.readarr_book_command_aliases, "title") response += f" {readarr_help}" if response == "": - response = self._xlate("no_features") + response = xlate("no_features") if self.auth_level == 2: - response += " " + self._xlate_aliases("admin_help", settings.searcharr_users_command_aliases) + response += " " + xlate_aliases("admin_help", settings.searcharr_users_command_aliases) update.message.reply_text(response) \ No newline at end of file diff --git a/commands/start.py b/commands/start.py index e06d279..d651ee3 100644 --- a/commands/start.py +++ b/commands/start.py @@ -1,5 +1,7 @@ from commands import Command import settings +import util +from util import xlate, xlate_aliases class Start(Command): @@ -8,8 +10,8 @@ class Start(Command): _validation_checks = [] def _action(self, update, context): - password = self._strip_entities(update.message) - self.logger.debug(f"{update}") + password = util.strip_entities(update.message) + util.log.debug(f"{update}") if password and password == settings.searcharr_admin_password: self.searcharr._add_user( @@ -18,11 +20,11 @@ def _action(self, update, context): admin=1, ) update.message.reply_text( - self._xlate_aliases("admin_auth_success", settings.searcharr_help_command_aliases) + xlate_aliases("admin_auth_success", settings.searcharr_help_command_aliases) ) elif self.searcharr._authenticated(update): update.message.reply_text( - self._xlate_aliases("already_authenticated", settings.searcharr_help_command_aliases) + xlate_aliases("already_authenticated", settings.searcharr_help_command_aliases) ) elif password == settings.searcharr_password: @@ -31,7 +33,7 @@ def _action(self, update, context): username=update.message.from_user.username, ) update.message.reply_text( - self._xlate_aliases("auth_successful", settings.searcharr_help_command_aliases) + xlate_aliases("auth_successful", settings.searcharr_help_command_aliases) ) else: - update.message.reply_text(self._xlate("incorrect_pw")) \ No newline at end of file + update.message.reply_text(xlate("incorrect_pw")) \ No newline at end of file diff --git a/commands/users.py b/commands/users.py index 6b47a31..9fdbf8a 100644 --- a/commands/users.py +++ b/commands/users.py @@ -1,5 +1,6 @@ from commands import Command import settings +from util import xlate, xlate_aliases class Users(Command): @@ -10,7 +11,7 @@ class Users(Command): def _action(self, update, context): if self.auth_level != 2: update.message.reply_text( - self._xlate_aliases("admin_auth_required", settings.searcharr_start_command_aliases, "admin_password") + xlate_aliases("admin_auth_required", settings.searcharr_start_command_aliases, "admin_password") ) return @@ -24,13 +25,12 @@ def _action(self, update, context): ) if not len(results): - update.message.reply_text(self._xlate("no_users_found")) + update.message.reply_text(xlate("no_users_found")) else: reply_message, reply_markup = self.searcharr._prepare_response_users( cid, results, 0, - 5, len(results), ) context.bot.sendMessage( diff --git a/searcharr.py b/searcharr.py index 12b07b8..23b83c1 100644 --- a/searcharr.py +++ b/searcharr.py @@ -14,7 +14,7 @@ import uuid from datetime import datetime -from telegram import InlineKeyboardButton, InlineKeyboardMarkup, InputMediaPhoto +from telegram import InlineKeyboardMarkup, InputMediaPhoto from telegram.error import BadRequest from telegram.ext import Updater, CommandHandler, CallbackQueryHandler @@ -23,7 +23,10 @@ import sonarr import readarr import settings +import util +from util import xlate from commands import Command +from buttons import KeyboardButtons __version__ = "3.2.2" @@ -64,10 +67,8 @@ class Searcharr(object): def __init__(self, token): self.DEV_MODE = True if args.dev_mode else False self.token = token - logger.info(f"Searcharr v{__version__} - Logging started!") - self._lang = self._load_language() - if self._lang.get("language_ietf") != "en-us": - self._lang_default = self._load_language("en-us") + util.log.info(f"Searcharr v{__version__} - Logging started!") + self._lang = util.load_language() self.sonarr = ( sonarr.Sonarr(settings.sonarr_url, settings.sonarr_api_key, args.verbose) if settings.sonarr_enabled @@ -80,23 +81,23 @@ def __init__(self, token): settings.sonarr_quality_profile_id ] for i in settings.sonarr_quality_profile_id: - logger.debug( + util.log.debug( f"Looking up/validating Sonarr quality profile id for [{i}]..." ) foundProfile = self.sonarr.lookup_quality_profile(i) if not foundProfile: - logger.error(f"Sonarr quality profile id/name [{i}] is invalid!") + util.log.error(f"Sonarr quality profile id/name [{i}] is invalid!") else: - logger.debug( + util.log.debug( f"Found Sonarr quality profile for [{i}]: [{foundProfile}]" ) quality_profiles.append(foundProfile) if not len(quality_profiles): - logger.warning( + util.log.warning( f"No valid Sonarr quality profile(s) provided! Using all of the quality profiles I found in Sonarr: {self.sonarr._quality_profiles}" ) else: - logger.debug( + util.log.debug( f"Using the following Sonarr quality profile(s): {[(x['id'], x['name']) for x in quality_profiles]}" ) self.sonarr._quality_profiles = quality_profiles @@ -104,66 +105,66 @@ def __init__(self, token): root_folders = [] if not hasattr(settings, "sonarr_series_paths"): settings.sonarr_series_paths = [] - logger.warning( + util.log.warning( 'No sonarr_series_paths setting detected. Please set one in settings.py (sonarr_series_paths=["/path/1", "/path/2"]). Proceeding with all root folders configured in Sonarr.' ) if not isinstance(settings.sonarr_series_paths, list): settings.sonarr_series_paths = [settings.sonarr_series_paths] for i in settings.sonarr_series_paths: - logger.debug(f"Looking up/validating Sonarr root folder for [{i}]...") + util.log.debug(f"Looking up/validating Sonarr root folder for [{i}]...") foundPath = self.sonarr.lookup_root_folder(i) if not foundPath: - logger.error(f"Sonarr root folder path/id [{i}] is invalid!") + util.log.error(f"Sonarr root folder path/id [{i}] is invalid!") else: - logger.debug(f"Found Sonarr root folder for [{i}]: [{foundPath}]") + util.log.debug(f"Found Sonarr root folder for [{i}]: [{foundPath}]") root_folders.append(foundPath) if not len(root_folders): - logger.warning( + util.log.warning( f"No valid Sonarr root folder(s) provided! Using all of the root folders I found in Sonarr: {self.sonarr._root_folders}" ) else: - logger.debug( + util.log.debug( f"Using the following Sonarr root folder(s): {[(x['id'], x['path']) for x in root_folders]}" ) self.sonarr._root_folders = root_folders if not hasattr(settings, "sonarr_tag_with_username"): settings.sonarr_tag_with_username = True - logger.warning( + util.log.warning( "No sonarr_tag_with_username setting found. Please add sonarr_tag_with_username to settings.py (sonarr_tag_with_username=True or sonarr_tag_with_username=False). Defaulting to True." ) if not hasattr(settings, "sonarr_series_command_aliases"): settings.sonarr_series_command_aliases = ["series"] - logger.warning( + util.log.warning( 'No sonarr_series_command_aliases setting found. Please add sonarr_series_command_aliases to settings.py (e.g. sonarr_series_command_aliases=["series", "tv"]. Defaulting to ["series"].' ) if not hasattr(settings, "sonarr_season_monitor_prompt"): settings.sonarr_season_monitor_prompt = False - logger.warning( + util.log.warning( "No sonarr_season_monitor_prompt setting found. Please add sonarr_season_monitor_prompt to settings.py (e.g. sonarr_season_monitor_prompt=True if you want users to choose whether to monitor all/first/latest season(s). Defaulting to False." ) if not hasattr(settings, "sonarr_forced_tags"): settings.sonarr_forced_tags = [] - logger.warning( + util.log.warning( 'No sonarr_forced_tags setting found. Please add sonarr_forced_tags to settings.py (e.g. sonarr_forced_tags=["tag-1", "tag-2"]) if you want specific tags added to each series. Defaulting to empty list ([]).' ) if not hasattr(settings, "sonarr_allow_user_to_select_tags"): settings.sonarr_allow_user_to_select_tags = False - logger.warning( + util.log.warning( "No sonarr_allow_user_to_select_tags setting found. Please add sonarr_allow_user_to_select_tags to settings.py (e.g. sonarr_allow_user_to_select_tags=True) if you want users to be able to select tags when adding a series. Defaulting to False." ) if not hasattr(settings, "sonarr_user_selectable_tags"): settings.sonarr_user_selectable_tags = [] - logger.warning( + util.log.warning( 'No sonarr_user_selectable_tags setting found. Please add sonarr_user_selectable_tags to settings.py (e.g. sonarr_user_selectable_tags=["tag-1", "tag-2"]) if you want to limit the tags a user can select. Defaulting to empty list ([]), which will present the user with all tags.' ) for t in settings.sonarr_user_selectable_tags: if t_id := self.sonarr.get_tag_id(t): - logger.debug( + util.log.debug( f"Tag id [{t_id}] for user-selectable Sonarr tag [{t}]" ) for t in settings.sonarr_forced_tags: if t_id := self.sonarr.get_tag_id(t): - logger.debug(f"Tag id [{t_id}] for forced Sonarr tag [{t}]") + util.log.debug(f"Tag id [{t_id}] for forced Sonarr tag [{t}]") self.radarr = ( radarr.Radarr(settings.radarr_url, settings.radarr_api_key, args.verbose) if settings.radarr_enabled @@ -176,23 +177,23 @@ def __init__(self, token): settings.radarr_quality_profile_id ] for i in settings.radarr_quality_profile_id: - logger.debug( + util.log.debug( f"Looking up/validating Radarr quality profile id for [{i}]..." ) foundProfile = self.radarr.lookup_quality_profile(i) if not foundProfile: - logger.error(f"Radarr quality profile id/name [{i}] is invalid!") + util.log.error(f"Radarr quality profile id/name [{i}] is invalid!") else: - logger.debug( + util.log.debug( f"Found Radarr quality profile for [{i}]: [{foundProfile}]" ) quality_profiles.append(foundProfile) if not len(quality_profiles): - logger.warning( + util.log.warning( f"No valid Radarr quality profile(s) provided! Using all of the quality profiles I found in Radarr: {self.radarr._quality_profiles}" ) else: - logger.debug( + util.log.debug( f"Using the following Radarr quality profile(s): {[(x['id'], x['name']) for x in quality_profiles]}" ) self.radarr._quality_profiles = quality_profiles @@ -200,66 +201,66 @@ def __init__(self, token): root_folders = [] if not hasattr(settings, "radarr_movie_paths"): settings.radarr_movie_paths = [] - logger.warning( + util.log.warning( 'No radarr_movie_paths setting detected. Please set one in settings.py (radarr_movie_paths=["/path/1", "/path/2"]). Proceeding with all root folders configured in Radarr.' ) if not isinstance(settings.radarr_movie_paths, list): settings.radarr_movie_paths = [settings.radarr_movie_paths] for i in settings.radarr_movie_paths: - logger.debug(f"Looking up/validating Radarr root folder for [{i}]...") + util.log.debug(f"Looking up/validating Radarr root folder for [{i}]...") foundPath = self.radarr.lookup_root_folder(i) if not foundPath: - logger.error(f"Radarr root folder path/id [{i}] is invalid!") + util.log.error(f"Radarr root folder path/id [{i}] is invalid!") else: - logger.debug(f"Found Radarr root folder for [{i}]: [{foundPath}]") + util.log.debug(f"Found Radarr root folder for [{i}]: [{foundPath}]") root_folders.append(foundPath) if not len(root_folders): - logger.warning( + util.log.warning( f"No valid Radarr root folder(s) provided! Using all of the root folders I found in Radarr: {self.radarr._root_folders}" ) else: - logger.debug( + util.log.debug( f"Using the following Radarr root folder(s): {[(x['id'], x['path']) for x in root_folders]}" ) self.radarr._root_folders = root_folders if not hasattr(settings, "radarr_tag_with_username"): settings.radarr_tag_with_username = True - logger.warning( + util.log.warning( "No radarr_tag_with_username setting found. Please add radarr_tag_with_username to settings.py (radarr_tag_with_username=True or radarr_tag_with_username=False). Defaulting to True." ) if not hasattr(settings, "radarr_min_availability"): settings.radarr_min_availability = "released" - logger.warning( + util.log.warning( 'No radarr_min_availability setting found. Please add radarr_min_availability to settings.py (options: "released", "announced", "inCinema"). Defaulting to "released".' ) if not hasattr(settings, "radarr_movie_command_aliases"): settings.radarr_movie_command_aliases = ["movie"] - logger.warning( + util.log.warning( 'No radarr_movie_command_aliases setting found. Please add radarr_movie_command_aliases to settings.py (e.g. radarr_movie_command_aliases=["movie", "mv"]. Defaulting to ["movie"].' ) if not hasattr(settings, "radarr_forced_tags"): settings.radarr_forced_tags = [] - logger.warning( + util.log.warning( 'No radarr_forced_tags setting found. Please add radarr_forced_tags to settings.py (e.g. radarr_forced_tags=["tag-1", "tag-2"]) if you want specific tags added to each movie. Defaulting to empty list ([]).' ) if not hasattr(settings, "radarr_allow_user_to_select_tags"): settings.radarr_allow_user_to_select_tags = True - logger.warning( + util.log.warning( "No radarr_allow_user_to_select_tags setting found. Please add radarr_allow_user_to_select_tags to settings.py (e.g. radarr_allow_user_to_select_tags=False) if you do not want users to be able to select tags when adding a movie. Defaulting to True." ) if not hasattr(settings, "radarr_user_selectable_tags"): settings.radarr_user_selectable_tags = [] - logger.warning( + util.log.warning( 'No radarr_user_selectable_tags setting found. Please add radarr_user_selectable_tags to settings.py (e.g. radarr_user_selectable_tags=["tag-1", "tag-2"]) if you want to limit the tags a user can select. Defaulting to empty list ([]), which will present the user with all tags.' ) for t in settings.radarr_user_selectable_tags: if t_id := self.radarr.get_tag_id(t): - logger.debug( + util.log.debug( f"Tag id [{t_id}] for user-selectable Radarr tag [{t}]" ) for t in settings.radarr_forced_tags: if t_id := self.radarr.get_tag_id(t): - logger.debug(f"Tag id [{t_id}] for forced Radarr tag [{t}]") + util.log.debug(f"Tag id [{t_id}] for forced Radarr tag [{t}]") if hasattr(settings, "readarr_enabled"): self.readarr = ( readarr.Readarr( @@ -271,7 +272,7 @@ def __init__(self, token): else: settings.readarr_enabled = False self.readarr = None - logger.warning( + util.log.warning( "No readarr_enabled setting found. If you want Searcharr to support Readarr, please refer to the sample settings on github and add settings for Readarr to settings.py." ) if self.readarr: @@ -281,23 +282,23 @@ def __init__(self, token): settings.readarr_quality_profile_id ] for i in settings.readarr_quality_profile_id: - logger.debug( + util.log.debug( f"Looking up/validating Readarr quality profile id for [{i}]..." ) foundProfile = self.readarr.lookup_quality_profile(i) if not foundProfile: - logger.error(f"Readarr quality profile id/name [{i}] is invalid!") + util.log.error(f"Readarr quality profile id/name [{i}] is invalid!") else: - logger.debug( + util.log.debug( f"Found Readarr quality profile for [{i}]: [{foundProfile}]" ) quality_profiles.append(foundProfile) if not len(quality_profiles): - logger.warning( + util.log.warning( f"No valid Readarr quality profile(s) provided! Using all of the quality profiles I found in Readarr: {self.readarr._quality_profiles}" ) else: - logger.debug( + util.log.debug( f"Using the following Readarr quality profile(s): {[(x['id'], x['name']) for x in quality_profiles]}" ) self.readarr._quality_profiles = quality_profiles @@ -307,23 +308,23 @@ def __init__(self, token): settings.readarr_metadata_profile_id ] for i in settings.readarr_metadata_profile_id: - logger.debug( + util.log.debug( f"Looking up/validating Readarr metadata profile id for [{i}]..." ) foundProfile = self.readarr.lookup_metadata_profile(i) if not foundProfile: - logger.error(f"Readarr metadata profile id/name [{i}] is invalid!") + util.log.error(f"Readarr metadata profile id/name [{i}] is invalid!") else: - logger.debug( + util.log.debug( f"Found Readarr metadata profile for [{i}]: [{foundProfile}]" ) metadata_profiles.append(foundProfile) if not len(metadata_profiles): - logger.warning( + util.log.warning( f"No valid Readarr metadata profile(s) provided! Using all of the metadata profiles I found in Readarr: {self.readarr._metadata_profiles}" ) else: - logger.debug( + util.log.debug( f"Using the following Readarr metadata profile(s): {[(x['id'], x['name']) for x in metadata_profiles]}" ) self.readarr._metadata_profiles = metadata_profiles @@ -331,101 +332,101 @@ def __init__(self, token): root_folders = [] if not hasattr(settings, "readarr_book_paths"): settings.readarr_book_paths = [] - logger.warning( + util.log.warning( 'No readarr_book_paths setting detected. Please set one in settings.py (readarr_book_paths=["/path/1", "/path/2"]). Proceeding with all root folders configured in Readarr.' ) if not isinstance(settings.readarr_book_paths, list): settings.readarr_book_paths = [settings.readarr_book_paths] for i in settings.readarr_book_paths: - logger.debug(f"Looking up/validating Readarr root folder for [{i}]...") + util.log.debug(f"Looking up/validating Readarr root folder for [{i}]...") foundPath = self.readarr.lookup_root_folder(i) if not foundPath: - logger.error(f"Readarr root folder path/id [{i}] is invalid!") + util.log.error(f"Readarr root folder path/id [{i}] is invalid!") else: - logger.debug(f"Found Readarr root folder for [{i}]: [{foundPath}]") + util.log.debug(f"Found Readarr root folder for [{i}]: [{foundPath}]") root_folders.append(foundPath) if not len(root_folders): - logger.warning( + util.log.warning( f"No valid Readarr root folder(s) provided! Using all of the root folders I found in Readarr: {self.readarr._root_folders}" ) else: - logger.debug( + util.log.debug( f"Using the following Readarr root folder(s): {[(x['id'], x['path']) for x in root_folders]}" ) self.readarr._root_folders = root_folders if not hasattr(settings, "readarr_tag_with_username"): settings.readarr_tag_with_username = True - logger.warning( + util.log.warning( "No readarr_tag_with_username setting found. Please add readarr_tag_with_username to settings.py (readarr_tag_with_username=True or readarr_tag_with_username=False). Defaulting to True." ) if not hasattr(settings, "readarr_book_command_aliases"): settings.readarr_book_command_aliases = ["book"] - logger.warning( + util.log.warning( 'No readarr_book_command_aliases setting found. Please add readarr_book_command_aliases to settings.py (e.g. readarr_book_command_aliases=["book", "bk"]. Defaulting to ["book"].' ) if not hasattr(settings, "readarr_forced_tags"): settings.readarr_forced_tags = [] - logger.warning( + util.log.warning( 'No readarr_forced_tags setting found. Please add readarr_forced_tags to settings.py (e.g. readarr_forced_tags=["tag-1", "tag-2"]) if you want specific tags added to each book. Defaulting to empty list ([]).' ) if not hasattr(settings, "readarr_allow_user_to_select_tags"): settings.readarr_allow_user_to_select_tags = True - logger.warning( + util.log.warning( "No readarr_allow_user_to_select_tags setting found. Please add readarr_allow_user_to_select_tags to settings.py (e.g. readarr_allow_user_to_select_tags=False) if you do not want users to be able to select tags when adding a book. Defaulting to True." ) if not hasattr(settings, "readarr_user_selectable_tags"): settings.readarr_user_selectable_tags = [] - logger.warning( + util.log.warning( 'No readarr_user_selectable_tags setting found. Please add readarr_user_selectable_tags to settings.py (e.g. readarr_user_selectable_tags=["tag-1", "tag-2"]) if you want to limit the tags a user can select. Defaulting to empty list ([]), which will present the user with all tags.' ) for t in settings.readarr_user_selectable_tags: if t_id := self.readarr.get_tag_id(t): - logger.debug( + util.log.debug( f"Tag id [{t_id}] for user-selectable Readarr tag [{t}]" ) for t in settings.readarr_forced_tags: if t_id := self.readarr.get_tag_id(t): - logger.debug(f"Tag id [{t_id}] for forced Readarr tag [{t}]") + util.log.debug(f"Tag id [{t_id}] for forced Readarr tag [{t}]") self.conversations = {} if not hasattr(settings, "searcharr_admin_password"): settings.searcharr_admin_password = uuid.uuid4().hex - logger.warning( + util.log.warning( f'No admin password detected. Please set one in settings.py (searcharr_admin_password="your admin password"). Using {settings.searcharr_admin_password} as the admin password for this session.' ) if settings.searcharr_password == "": - logger.warning( + util.log.warning( 'Password is blank. This will allow anyone to add series/movies/books using your bot. If this is unexpected, set a password in settings.py (searcharr_password="your password").' ) if not hasattr(settings, "searcharr_start_command_aliases"): settings.searcharr_start_command_aliases = ["start"] - logger.warning( + util.log.warning( 'No searcharr_start_command_aliases setting found. Please add searcharr_start_command_aliases to settings.py (e.g. searcharr_start_command_aliases=["start"]. Defaulting to ["start"].' ) if not hasattr(settings, "searcharr_help_command_aliases"): settings.searcharr_help_command_aliases = ["help"] - logger.warning( + util.log.warning( 'No searcharr_help_command_aliases setting found. Please add searcharr_help_command_aliases to settings.py (e.g. searcharr_help_command_aliases=["help"]. Defaulting to ["help"].' ) if not hasattr(settings, "searcharr_users_command_aliases"): settings.searcharr_users_command_aliases = ["users"] - logger.warning( + util.log.warning( 'No searcharr_users_command_aliases setting found. Please add searcharr_users_command_aliases to settings.py (e.g. searcharr_users_command_aliases=["users"]. Defaulting to ["users"].' ) def callback(self, update, context): query = update.callback_query - logger.debug( + util.log.debug( f"Received callback from [{query.from_user.username}]: [{query.data}]" ) auth_level = self._authenticated(query.from_user.id) if not auth_level: query.message.reply_text( - self._xlate( + xlate( "auth_required", commands=" OR ".join( [ - f"`/{c} <{self._xlate('password')}>`" + f"`/{c} <{xlate('password')}>`" for c in settings.searcharr_start_command_aliases ] ), @@ -442,7 +443,7 @@ def callback(self, update, context): convo = self._get_conversation(query.data.split("^^^")[0]) # convo = self.conversations.get(query.data.split("^^^")[0]) if not convo: - query.message.reply_text(self._xlate("convo_not_found")) + query.message.reply_text(xlate("convo_not_found")) query.message.delete() query.answer() return @@ -451,22 +452,21 @@ def callback(self, update, context): if "^^" in op: op, op_flags = op.split("^^") op_flags = dict(parse_qsl(op_flags)) - for k, v in op_flags.items(): - logger.debug( - f"Adding/Updating additional data for cid=[{cid}], key=[{k}], value=[{v}]..." - ) - self._update_add_data(cid, k, v) + if op == "add": + for k, v in op_flags.items(): + util.log.debug( + f"Adding/Updating additional data for cid=[{cid}], key=[{k}], value=[{v}]..." + ) + self._update_add_data(cid, k, v) i = int(i) if op == "noop": pass elif op == "cancel": self._delete_conversation(cid) - # self.conversations.pop(cid) - query.message.reply_text(self._xlate("search_canceled")) + query.message.reply_text(xlate("search_canceled")) query.message.delete() elif op == "done": self._delete_conversation(cid) - # self.conversations.pop(cid) query.message.delete() elif op == "prev": if convo["type"] in ["series", "movie", "book"]: @@ -484,7 +484,7 @@ def callback(self, update, context): ) except BadRequest as e: if str(e) in self._bad_request_poster_error_messages: - logger.error( + util.log.error( f"Error sending photo [{r['remotePoster']}]: BadRequest: {e}. Attempting to send with default poster..." ) query.message.edit_media( @@ -507,8 +507,7 @@ def callback(self, update, context): reply_message, reply_markup = self._prepare_response_users( cid, convo["results"], - i, - 5, + i-1, len(convo["results"]), ) context.bot.edit_message_text( @@ -523,7 +522,7 @@ def callback(self, update, context): query.answer() return r = convo["results"][i + 1] - logger.debug(f"{r=}") + util.log.debug(f"{r=}") reply_message, reply_markup = self._prepare_response( convo["type"], r, cid, i + 1, len(convo["results"]) ) @@ -534,7 +533,7 @@ def callback(self, update, context): ) except BadRequest as e: if str(e) in self._bad_request_poster_error_messages: - logger.error( + util.log.error( f"Error sending photo [{r['remotePoster']}]: BadRequest: {e}. Attempting to send with default poster..." ) query.message.edit_media( @@ -552,14 +551,13 @@ def callback(self, update, context): reply_markup=reply_markup, ) elif convo["type"] == "users": - if i > len(convo["results"]): - query.answer() - return + #if i >= len(convo["results"])/5: + # query.answer() + # return reply_message, reply_markup = self._prepare_response_users( cid, convo["results"], - i, - 5, + i+1, len(convo["results"]), ) context.bot.edit_message_text( @@ -571,7 +569,7 @@ def callback(self, update, context): elif op == "add": r = convo["results"][i] additional_data = self._get_add_data(cid) - logger.debug(f"{additional_data=}") + util.log.debug(f"{additional_data=}") paths = ( self.sonarr._root_folders if convo["type"] == "series" @@ -599,7 +597,7 @@ def callback(self, update, context): ) except BadRequest as e: if str(e) in self._bad_request_poster_error_messages: - logger.error( + util.log.error( f"Error sending photo [{r['remotePoster']}]: BadRequest: {e}. Attempting to send with default poster..." ) query.message.edit_media( @@ -619,16 +617,16 @@ def callback(self, update, context): query.answer() return elif len(paths) == 1: - logger.debug( + util.log.debug( f"Only one root folder enabled. Adding/Updating additional data for cid=[{cid}], key=[p], value=[{paths[0]['id']}]..." ) self._update_add_data(cid, "p", paths[0]["path"]) else: self._delete_conversation(cid) query.message.reply_text( - self._xlate( + xlate( "no_root_folders", - kind=self._xlate(convo["type"]), + kind=xlate(convo["type"]), app="Sonarr" if convo["type"] == "series" else "Radarr" @@ -657,7 +655,7 @@ def callback(self, update, context): ), None, ) - logger.debug( + util.log.debug( f"Path id [{additional_data['p']}] lookup result: [{path}]" ) if path: @@ -689,7 +687,7 @@ def callback(self, update, context): ) except BadRequest as e: if str(e) in self._bad_request_poster_error_messages: - logger.error( + util.log.error( f"Error sending photo [{r['remotePoster']}]: BadRequest: {e}. Attempting to send with default poster..." ) query.message.edit_media( @@ -709,16 +707,16 @@ def callback(self, update, context): query.answer() return elif len(quality_profiles) == 1: - logger.debug( + util.log.debug( f"Only one quality profile enabled. Adding/Updating additional data for cid=[{cid}], key=[q], value=[{quality_profiles[0]['id']}]..." ) self._update_add_data(cid, "q", quality_profiles[0]["id"]) else: self._delete_conversation(cid) query.message.reply_text( - self._xlate( + xlate( "no_quality_profiles", - kind=self._xlate(convo["type"]), + kind=xlate(convo["type"]), app="Sonarr" if convo["type"] == "series" else "Radarr" @@ -750,7 +748,7 @@ def callback(self, update, context): ) except BadRequest as e: if str(e) in self._bad_request_poster_error_messages: - logger.error( + util.log.error( f"Error sending photo [{r['remotePoster']}]: BadRequest: {e}. Attempting to send with default poster..." ) query.message.edit_media( @@ -770,16 +768,16 @@ def callback(self, update, context): query.answer() return elif len(metadata_profiles) == 1: - logger.debug( + util.log.debug( f"Only one metadata profile enabled. Adding/Updating additional data for cid=[{cid}], key=[m], value=[{metadata_profiles[0]['id']}]..." ) self._update_add_data(cid, "m", metadata_profiles[0]["id"]) else: self._delete_conversation(cid) query.message.reply_text( - self._xlate( + xlate( "no_metadata_profiles", - kind=self._xlate(convo["type"]), + kind=xlate(convo["type"]), app="Sonarr" if convo["type"] == "series" else "Radarr" @@ -798,9 +796,9 @@ def callback(self, update, context): ): # m = monitor season(s) monitor_options = [ - self._xlate("all_seasons"), - self._xlate("first_season"), - self._xlate("latest_season"), + xlate("all_seasons"), + xlate("first_season"), + xlate("latest_season"), ] # prepare response to prompt user to select quality profile, and return reply_message, reply_markup = self._prepare_response( @@ -819,7 +817,7 @@ def callback(self, update, context): ) except BadRequest as e: if str(e) in self._bad_request_poster_error_messages: - logger.error( + util.log.error( f"Error sending photo [{r['remotePoster']}]: BadRequest: {e}. Attempting to send with default poster..." ) query.message.edit_media( @@ -862,7 +860,7 @@ def callback(self, update, context): forced_tags = settings.readarr_forced_tags if allow_user_to_select_tags and not additional_data.get("td"): if not len(all_tags): - logger.warning( + util.log.warning( f"User tagging is enabled, but no tags found. Make sure there are tags{' in Sonarr' if convo['type'] == 'series' else ' in Radarr' if convo['type'] == 'movie' else ' in Readarr' if convo['type'] == 'book' else ''} matching your Searcharr configuration." ) elif not additional_data.get("tt"): @@ -882,7 +880,7 @@ def callback(self, update, context): ) except BadRequest as e: if str(e) in self._bad_request_poster_error_messages: - logger.error( + util.log.error( f"Error sending photo [{r['remotePoster']}]: BadRequest: {e}. Attempting to send with default poster..." ) query.message.edit_media( @@ -908,7 +906,7 @@ def callback(self, update, context): else [] ) tag_ids.append(additional_data["tt"]) - logger.debug(f"Adding tag [{additional_data['tt']}]") + util.log.debug(f"Adding tag [{additional_data['tt']}]") self._update_add_data(cid, "t", ",".join(tag_ids)) return @@ -917,7 +915,7 @@ def callback(self, update, context): if len(additional_data.get("t", "")) else [] ) - logger.debug(f"{tags=}") + util.log.debug(f"{tags=}") if convo["type"] == "series": get_tag_id = self.sonarr.get_tag_id tag_with_username = settings.sonarr_tag_with_username @@ -932,19 +930,19 @@ def callback(self, update, context): if tag_id := get_tag_id(tag): tags.append(str(tag_id)) else: - self.logger.warning( + self.util.log.warning( f"Tag lookup/creation failed for [{tag}]. This tag will not be added to the {convo['type']}." ) for tag in forced_tags: if tag_id := get_tag_id(tag): tags.append(str(tag_id)) else: - self.logger.warning( + self.util.log.warning( f"Tag lookup/creation failed for forced tag [{tag}]. This tag will not be added to the {convo['type']}." ) self._update_add_data(cid, "t", ",".join(list(set(tags)))) - logger.debug("All data is accounted for, proceeding to add...") + util.log.debug("All data is accounted for, proceeding to add...") try: if convo["type"] == "series": added = self.sonarr.add_series( @@ -971,25 +969,25 @@ def callback(self, update, context): else: added = False except Exception as e: - logger.error(f"Error adding {convo['type']}: {e}") + util.log.error(f"Error adding {convo['type']}: {e}") added = False - logger.debug(f"Result of attempt to add {convo['type']}: {added}") + util.log.debug(f"Result of attempt to add {convo['type']}: {added}") if added: self._delete_conversation(cid) - query.message.reply_text(self._xlate("added", title=r["title"])) + query.message.reply_text(xlate("added", title=r["title"])) query.message.delete() else: query.message.reply_text( - self._xlate("unknown_error_adding", kind=convo["type"]) + xlate("unknown_error_adding", kind=convo["type"]) ) elif op == "remove_user": if auth_level != 2: query.message.reply_text( - self._xlate( + xlate( "admin_auth_required", commands=" OR ".join( [ - f"`/{c} <{self._xlate('admin_password')}>`" + f"`/{c} <{xlate('admin_password')}>`" for c in settings.searcharr_start_command_aliases ] ), @@ -1016,28 +1014,27 @@ def callback(self, update, context): cid, convo["results"], 0, - 5, len(convo["results"]), ) context.bot.edit_message_text( chat_id=query.message.chat.id, message_id=query.message.message_id, - text=f"{self._xlate('removed_user', user=i)} {reply_message}", + text=f"{xlate('removed_user', user=i)} {reply_message}", reply_markup=reply_markup, ) except Exception as e: - logger.error(f"Error removing all access for user id [{i}]: {e}") + util.log.error(f"Error removing all access for user id [{i}]: {e}") query.message.reply_text( - self._xlate("unknown_error_removing_user", user=i) + xlate("unknown_error_removing_user", user=i) ) elif op == "make_admin": if auth_level != 2: query.message.reply_text( - self._xlate( + xlate( "admin_auth_required", commands=" OR ".join( [ - f"`/{c} <{self._xlate('admin_password')}>`" + f"`/{c} <{xlate('admin_password')}>`" for c in settings.searcharr_start_command_aliases ] ), @@ -1062,28 +1059,27 @@ def callback(self, update, context): cid, convo["results"], 0, - 5, len(convo["results"]), ) context.bot.edit_message_text( chat_id=query.message.chat.id, message_id=query.message.message_id, - text=f"{self._xlate('added_admin_access', user=i)} {reply_message}", + text=f"{xlate('added_admin_access', user=i)} {reply_message}", reply_markup=reply_markup, ) except Exception as e: - logger.error(f"Error adding admin access for user id [{i}]: {e}") + util.log.error(f"Error adding admin access for user id [{i}]: {e}") query.message.reply_text( - self._xlate("unknown_error_adding_admin", user=i) + xlate("unknown_error_adding_admin", user=i) ) elif op == "remove_admin": if auth_level != 2: query.message.reply_text( - self._xlate( + xlate( "admin_auth_required", commands=" OR ".join( [ - f"`/{c} <{self._xlate('admin_password')}>`" + f"`/{c} <{xlate('admin_password')}>`" for c in settings.searcharr_start_command_aliases ] ), @@ -1108,19 +1104,18 @@ def callback(self, update, context): cid, convo["results"], 0, - 5, len(convo["results"]), ) context.bot.edit_message_text( chat_id=query.message.chat.id, message_id=query.message.message_id, - text=f"{self._xlate('removed_admin_access', user=i)} {reply_message}", + text=f"{xlate('removed_admin_access', user=i)} {reply_message}", reply_markup=reply_markup, ) except Exception as e: - logger.error(f"Error removing admin access for user id [{i}]: {e}") + util.log.error(f"Error removing admin access for user id [{i}]: {e}") query.message.reply_text( - self._xlate("unknown_error_removing_admin", user=i) + xlate("unknown_error_removing_admin", user=i) ) query.answer() @@ -1139,43 +1134,33 @@ def _prepare_response( monitor_options=None, tags=None, ): + buttons = KeyboardButtons() keyboard = [] keyboardNavRow = [] if i > 0: keyboardNavRow.append( - InlineKeyboardButton( - self._xlate("prev_button"), callback_data=f"{cid}^^^{i}^^^prev" - ) + buttons.nav.prev(cid, i) ) if kind == "series" and r["tvdbId"]: keyboardNavRow.append( - InlineKeyboardButton( - "tvdb", url=f"https://thetvdb.com/series/{r['titleSlug']}" - ) + buttons.ext.tvdb(r) ) elif kind == "movie" and r["tmdbId"]: keyboardNavRow.append( - InlineKeyboardButton( - "TMDB", url=f"https://www.themoviedb.org/movie/{r['tmdbId']}" - ) + buttons.ext.tmdb(r) ) elif kind == "book" and r["links"]: for link in r["links"]: keyboardNavRow.append( - InlineKeyboardButton(link["name"], url=link["url"]) + buttons.ext.link(link) ) if kind == "series" or kind == "movie": - if r["imdbId"]: - keyboardNavRow.append( - InlineKeyboardButton( - "IMDb", url=f"https://imdb.com/title/{r['imdbId']}" - ) - ) + keyboardNavRow.append( + buttons.ext.imdb(r) + ) if total_results > 1 and i < total_results - 1: keyboardNavRow.append( - InlineKeyboardButton( - self._xlate("next_button"), callback_data=f"{cid}^^^{i}^^^next" - ) + buttons.nav.next(cid, i, total_results) ) keyboard.append(keyboardNavRow) @@ -1183,95 +1168,50 @@ def _prepare_response( if tags: for tag in tags[:12]: keyboard.append( - [ - InlineKeyboardButton( - self._xlate("add_tag_button", tag=tag["label"]), - callback_data=f"{cid}^^^{i}^^^add^^tt={tag['id']}", - ) - ], + [buttons.add.tag(tag, cid, i)] ) keyboard.append( - [ - InlineKeyboardButton( - self._xlate("finished_tagging_button"), - callback_data=f"{cid}^^^{i}^^^add^^td=1", - ) - ], + [buttons.add.finished_tagging(cid, i)] ) elif monitor_options: for k, o in enumerate(monitor_options): keyboard.append( - [ - InlineKeyboardButton( - self._xlate("monitor_button", option=o), - callback_data=f"{cid}^^^{i}^^^add^^m={k}", - ) - ], + [buttons.add.monitor(o, k, cid, i)] ) elif quality_profiles: for q in quality_profiles: keyboard.append( - [ - InlineKeyboardButton( - self._xlate("add_quality_button", quality=q["name"]), - callback_data=f"{cid}^^^{i}^^^add^^q={q['id']}", - ) - ], + [buttons.add.quality(q, cid, i)] ) elif metadata_profiles: for m in metadata_profiles: keyboard.append( - [ - InlineKeyboardButton( - self._xlate("add_metadata_button", metadata=m["name"]), - callback_data=f"{cid}^^^{i}^^^add^^m={m['id']}", - ) - ], + [buttons.add.metadata(m, cid, i)] ) elif paths: for p in paths: keyboard.append( - [ - InlineKeyboardButton( - self._xlate("add_path_button", path=p["path"]), - callback_data=f"{cid}^^^{i}^^^add^^p={p['id']}", - ) - ], + [buttons.add.path(p, cid, i)] ) keyboardActRow = [] if not add: if not r["id"]: keyboardActRow.append( - InlineKeyboardButton( - self._xlate("add_button", kind=self._xlate(kind).title()), - callback_data=f"{cid}^^^{i}^^^add", - ), + buttons.act.add(cid, i) ) else: keyboardActRow.append( - InlineKeyboardButton( - self._xlate("already_added_button"), - callback_data=f"{cid}^^^{i}^^^noop", - ), + buttons.act.already_added(i) ) keyboardActRow.append( - InlineKeyboardButton( - self._xlate("cancel_search_button"), - callback_data=f"{cid}^^^{i}^^^cancel", - ), + buttons.act.cancel(i) ) if len(keyboardActRow): keyboard.append(keyboardActRow) + if not add and kind == "series" and "Anime" in r["genres"]: - keyboard.append( - [ - InlineKeyboardButton( - self._xlate("add_series_anime_button"), - callback_data=f"{cid}^^^{i}^^^add^^st=a", - ) - ] - ) + keyboard.append([buttons.act.series_anime(cid, i)]) reply_markup = InlineKeyboardMarkup(keyboard) @@ -1294,62 +1234,44 @@ def _prepare_response( 0:1024 ] else: - reply_message = self._xlate("unexpected_error") + reply_message = xlate("unexpected_error") return (reply_message, reply_markup) - def _prepare_response_users(self, cid, users, offset, num, total_results): + def _prepare_response_users(self, cid, users, i, total_results): + buttons = KeyboardButtons() keyboard = [] - for u in users[offset : offset + num]: + for u in users[i*5 : (i*5)+5]: keyboard.append( [ - InlineKeyboardButton( - self._xlate("remove_user_button"), - callback_data=f"{cid}^^^{u['id']}^^^remove_user", - ), - InlineKeyboardButton( - f"{u['username'] if u['username'] != 'None' else u['id']}", - callback_data=f"{cid}^^^{u['id']}^^^noop", - ), - InlineKeyboardButton( - self._xlate("remove_admin_button") - if u["admin"] - else self._xlate("make_admin_button"), - callback_data=f"{cid}^^^{u['id']}^^^{'remove_admin' if u['admin'] else 'make_admin'}", - ), + buttons.user.remove(u, cid), + buttons.user.username(u, cid), + buttons.user.admin(u, cid), ] ) keyboardNavRow = [] - if offset > 0: + if i > 0: keyboardNavRow.append( - InlineKeyboardButton( - self._xlate("prev_button"), - callback_data=f"{cid}^^^{offset - num}^^^prev", - ), + buttons.nav.prev(cid, i) ) keyboardNavRow.append( - InlineKeyboardButton( - self._xlate("done"), callback_data=f"{cid}^^^{offset}^^^done" - ), + buttons.nav.done(cid, i) ) - if total_results > 1 and offset + num < total_results: + if total_results/5 > 1 and (i+1)*5 < total_results: keyboardNavRow.append( - InlineKeyboardButton( - self._xlate("next_button"), - callback_data=f"{cid}^^^{offset + num}^^^next", - ), + buttons.nav.next(cid, i) ) keyboard.append(keyboardNavRow) reply_markup = InlineKeyboardMarkup(keyboard) - reply_message = self._xlate( + reply_message = xlate( "listing_users_pagination", - page_info=f"{offset + 1}-{min(offset + num, total_results)} of {total_results}", + page_info=f"{i*5+1}-{min((i+1)*5, total_results)} of {total_results}", ) return (reply_message, reply_markup) def handle_error(self, update, context): - logger.error(f"Caught error: {context.error}") + util.log.error(f"Caught error: {context.error}") try: update.callback_query.answer() except Exception: @@ -1358,18 +1280,18 @@ def handle_error(self, update, context): def run(self): self._init_db() updater = Updater(self.token, use_context=True) - - for command in Command._list: - command._register(searcharr_instance = self, logger_instance = logger) + self.commands = Command._dict + for command in self.commands.values(): + command._inject_dependency(searcharr_instance=self) for c in command._command_aliases: - logger.debug(f"Registering [/{c}] as a {command._name} command") + util.log.debug(f"Registering [/{c}] as a {command._name} command") updater.dispatcher.add_handler(CommandHandler(c, command._execute)) updater.dispatcher.add_handler(CallbackQueryHandler(self.callback)) if not self.DEV_MODE: updater.dispatcher.add_error_handler(self.handle_error) else: - logger.info( + util.log.info( "Developer mode is enabled; skipping registration of error handler--exceptions will be raised." ) @@ -1380,7 +1302,7 @@ def _create_conversation(self, id, username, kind, results): con, cur = self._get_con_cur() q = "INSERT OR REPLACE INTO conversations (id, username, type, results) VALUES (?, ?, ?, ?)" qa = (id, username, kind, json.dumps(results)) - logger.debug(f"Executing query: [{q}] with args: [{qa}]") + util.log.debug(f"Executing query: [{q}] with args: [{qa}]") try: with DBLOCK: cur.execute(q, qa) @@ -1388,7 +1310,7 @@ def _create_conversation(self, id, username, kind, results): con.close() return True except sqlite3.Error as e: - logger.error( + util.log.error( f"Error executing database query to create conversation [{q}]: {e}" ) raise @@ -1402,7 +1324,7 @@ def _generate_cid(self): r = cur.execute(q, (u,)) except sqlite3.Error as e: r = None - logger.error( + util.log.error( f"Error executing database query to check conversation id uniqueness [{q}]: {e}" ) @@ -1412,37 +1334,37 @@ def _generate_cid(self): con.close() return u else: - logger.warning("Detected conversation id collision. Interesting.") + util.log.warning("Detected conversation id collision. Interesting.") def _get_conversation(self, id): q = "SELECT * FROM conversations WHERE id=?;" qa = (id,) - logger.debug(f"Executing query: [{q}] with args: [{qa}]...") + util.log.debug(f"Executing query: [{q}] with args: [{qa}]...") try: con, cur = self._get_con_cur() r = cur.execute(q, qa) except sqlite3.Error as e: r = None - logger.error( + util.log.error( f"Error executing database query to look up conversation from the database [{q}]: {e}" ) if r: record = r.fetchone() if record: - logger.debug(f"Found conversation {record['id']} in the database") + util.log.debug(f"Found conversation {record['id']} in the database") record.update({"results": json.loads(record["results"])}) con.close() return record - logger.debug(f"Found no conversation for id [{id}]") + util.log.debug(f"Found no conversation for id [{id}]") return None def _delete_conversation(self, id): self._clear_add_data(id) q = "DELETE FROM conversations WHERE id=?;" qa = (id,) - logger.debug(f"Executing query: [{q}] with args: [{qa}]") + util.log.debug(f"Executing query: [{q}] with args: [{qa}]") try: con, cur = self._get_con_cur() with DBLOCK: @@ -1451,7 +1373,7 @@ def _delete_conversation(self, id): con.close() return True except sqlite3.Error as e: - logger.error( + util.log.error( f"Error executing database query to delete conversation from the database [{q}]: {e}" ) return False @@ -1459,20 +1381,20 @@ def _delete_conversation(self, id): def _get_add_data(self, cid): q = "SELECT * FROM add_data WHERE cid=?;" qa = (cid,) - logger.debug(f"Executing query: [{q}] with args: [{qa}]...") + util.log.debug(f"Executing query: [{q}] with args: [{qa}]...") try: con, cur = self._get_con_cur() r = cur.execute(q, qa) except sqlite3.Error as e: r = None - logger.error( + util.log.error( f"Error executing database query to look up conversation add data from the database [{q}]: {e}" ) if r: records = r.fetchall() con.close() - logger.debug(f"Query response: {records}") + util.log.debug(f"Query response: {records}") return {x["key"]: x["value"] for x in records} else: return {} @@ -1481,7 +1403,7 @@ def _update_add_data(self, cid, key, value): con, cur = self._get_con_cur() q = "INSERT OR REPLACE INTO add_data (cid, key, value) VALUES (?, ?, ?)" qa = (cid, key, value) - logger.debug(f"Executing query: [{q}] with args: [{qa}]") + util.log.debug(f"Executing query: [{q}] with args: [{qa}]") try: with DBLOCK: cur.execute(q, qa) @@ -1489,13 +1411,13 @@ def _update_add_data(self, cid, key, value): con.close() return True except sqlite3.Error as e: - logger.error(f"Error executing database query [{q}]: {e}") + util.log.error(f"Error executing database query [{q}]: {e}") raise def _clear_add_data(self, cid): q = "DELETE FROM add_data WHERE cid=?;" qa = (cid,) - logger.debug(f"Executing query: [{q}] with args: [{qa}]") + util.log.debug(f"Executing query: [{q}] with args: [{qa}]") try: con, cur = self._get_con_cur() with DBLOCK: @@ -1504,7 +1426,7 @@ def _clear_add_data(self, cid): con.close() return True except sqlite3.Error as e: - logger.error( + util.log.error( f"Error executing database query to delete conversation add data from the database [{q}]: {e}" ) return False @@ -1513,7 +1435,7 @@ def _add_user(self, id, username, admin=""): con, cur = self._get_con_cur() q = "INSERT OR REPLACE INTO users (id, username, admin) VALUES (?, ?, ?);" qa = (id, username, admin) - logger.debug(f"Executing query: [{q}] with args: [{qa}]") + util.log.debug(f"Executing query: [{q}] with args: [{qa}]") try: with DBLOCK: cur.execute(q, qa) @@ -1521,14 +1443,14 @@ def _add_user(self, id, username, admin=""): con.close() return True except sqlite3.Error as e: - logger.error(f"Error executing database query [{q}]: {e}") + util.log.error(f"Error executing database query [{q}]: {e}") raise def _remove_user(self, id): con, cur = self._get_con_cur() q = "DELETE FROM users where id=?;" qa = (id,) - logger.debug(f"Executing query: [{q}] with args: [{qa}]") + util.log.debug(f"Executing query: [{q}] with args: [{qa}]") try: with DBLOCK: cur.execute(q, qa) @@ -1536,19 +1458,19 @@ def _remove_user(self, id): con.close() return True except sqlite3.Error as e: - logger.error(f"Error executing database query [{q}]: {e}") + util.log.error(f"Error executing database query [{q}]: {e}") raise def _get_users(self, admin=False): adminQ = " where IFNULL(admin, '') != ''" if admin else "" q = f"SELECT * FROM users{adminQ};" - logger.debug(f"Executing query: [{q}] with no args...") + util.log.debug(f"Executing query: [{q}] with no args...") try: con, cur = self._get_con_cur() r = cur.execute(q) except sqlite3.Error as e: r = None - logger.error( + util.log.error( f"Error executing database query to look up users from the database [{q}]: {e}" ) @@ -1557,7 +1479,7 @@ def _get_users(self, admin=False): con.close() return records - logger.debug( + util.log.debug( f"Found no {'admin ' if admin else ''}users in the database (this seems wrong)." ) return [] @@ -1566,7 +1488,7 @@ def _update_admin_access(self, user_id, admin=""): con, cur = self._get_con_cur() q = "UPDATE users set admin=? where id=?;" qa = (str(admin), user_id) - logger.debug(f"Executing query: [{q}] with args: [{qa}]") + util.log.debug(f"Executing query: [{q}] with args: [{qa}]") try: with DBLOCK: cur.execute(q, qa) @@ -1574,7 +1496,7 @@ def _update_admin_access(self, user_id, admin=""): con.close() return True except sqlite3.Error as e: - logger.error(f"Error executing database query [{q}]: {e}") + util.log.error(f"Error executing database query [{q}]: {e}") raise def _authenticated(self, user_id): @@ -1582,25 +1504,25 @@ def _authenticated(self, user_id): # Else return False q = "SELECT * FROM users WHERE id=?;" qa = (user_id,) - logger.debug(f"Executing query: [{q}] with args: [{qa}]...") + util.log.debug(f"Executing query: [{q}] with args: [{qa}]...") try: con, cur = self._get_con_cur() r = cur.execute(q, qa) except sqlite3.Error as e: r = None - logger.error( + util.log.error( f"Error executing database query to look up user from the database [{q}]: {e}" ) return False if r: record = r.fetchone() - logger.debug(f"Query result for user lookup: {record}") + util.log.debug(f"Query result for user lookup: {record}") con.close() if record and record["id"] == user_id: return 2 if record["admin"] else 1 - logger.debug(f"Did not find user [{user_id}] in the database.") + util.log.debug(f"Did not find user [{user_id}] in the database.") return False def _dict_factory(self, cursor, row): @@ -1616,12 +1538,12 @@ def _get_con_cur(self): # Connect to local DB and return tuple containing connection and cursor if not os.path.isdir(DBPATH): try: - logger.debug( + util.log.debug( "The data directory does not exist. Attempting to create it..." ) os.mkdir(DBPATH) except Exception as e: - logger.error(f"Error creating data directory: {e}.") + util.log.error(f"Error creating data directory: {e}.") raise try: @@ -1629,11 +1551,11 @@ def _get_con_cur(self): con.execute("PRAGMA journal_mode = off;") con.row_factory = self._dict_factory cur = con.cursor() - logger.debug( + util.log.debug( f"Database connection established [{os.path.join(DBPATH, DBFILE)}]." ) except sqlite3.Error as e: - logger.error(f"Error connecting to database: {e}") + util.log.error(f"Error connecting to database: {e}") raise return (con, cur) @@ -1661,48 +1583,17 @@ def _init_db(self): );""", ] for q in queries: - logger.debug(f"Executing query: [{q}] with no args...") + util.log.debug(f"Executing query: [{q}] with no args...") try: with DBLOCK: cur.execute(q) except sqlite3.Error as e: - logger.error(f"Error executing database query [{q}]: {e}") + util.log.error(f"Error executing database query [{q}]: {e}") raise con.commit() con.close() - def _load_language(self, lang_ietf=None): - if not lang_ietf: - if not hasattr(settings, "searcharr_language"): - logger.warning( - "No language defined! Defaulting to en-us. Please add searcharr_language to settings.py if you want another language, where the value is a filename (without .yml) in the lang folder." - ) - settings.searcharr_language = "en-us" - lang_ietf = settings.searcharr_language - logger.debug(f"Attempting to load language file: lang/{lang_ietf}.yml...") - try: - with open(f"lang/{lang_ietf}.yml", mode="r", encoding="utf-8") as y: - lang = yaml.load(y, Loader=yaml.SafeLoader) - except FileNotFoundError: - logger.error( - f"Error loading lang/{lang_ietf}.yml. Confirm searcharr_language in settings.py has a corresponding yml file in the lang subdirectory. Using default (English) language file." - ) - with open("lang/en-us.yml", "r") as y: - lang = yaml.load(y, Loader=yaml.SafeLoader) - return lang - - def _xlate(self, key, **kwargs): - if t := self._lang.get(key): - return t.format(**kwargs) - else: - logger.error(f"No translation found for key [{key}]!") - if self._lang.get("language_ietf") != "en-us": - if t := self._lang_default.get(key): - logger.info(f"Using default language for key [{key}]...") - return t.format(**kwargs) - return "(translation not found)" - _bad_request_poster_error_messages = [ "Wrong type of the web page content", "Wrong file identifier/http url specified", @@ -1712,6 +1603,7 @@ def _xlate(self, key, **kwargs): if __name__ == "__main__": args = parse_args() - logger = set_up_logger("searcharr", args.verbose, args.console_logging) + util.log = set_up_logger("searcharr", args.verbose, args.console_logging) + util.load_language() tgr = Searcharr(settings.tgram_token) tgr.run() \ No newline at end of file diff --git a/util.py b/util.py new file mode 100644 index 0000000..26348d1 --- /dev/null +++ b/util.py @@ -0,0 +1,64 @@ +import yaml +import settings + +log = None +lang = None +lang_default = None + + +def load_language(lang_ietf=None): + global lang + global lang_default + if not lang_ietf: + if not hasattr(settings, "searcharr_language"): + log.warning( + "No language defined! Defaulting to en-us. Please add searcharr_language to settings.py if you want another language, where the value is a filename (without .yml) in the lang folder." + ) + settings.searcharr_language = "en-us" + lang_ietf = settings.searcharr_language + log.debug(f"Attempting to load language file: lang/{lang_ietf}.yml...") + try: + with open(f"lang/{lang_ietf}.yml", mode="r", encoding="utf-8") as y: + lang = yaml.load(y, Loader=yaml.SafeLoader) + except FileNotFoundError: + log.error( + f"Error loading lang/{lang_ietf}.yml. Confirm searcharr_language in settings.py has a corresponding yml file in the lang subdirectory. Using default (English) language file." + ) + with open("lang/en-us.yml", "r") as y: + lang = yaml.load(y, Loader=yaml.SafeLoader) + + if lang.get("language_ietf") != "en-us": + lang_default = load_language("en-us") + +def xlate(key, **kwargs): + if t := lang.get(key): + return t.format(**kwargs) + else: + log.error(f"No translation found for key [{key}]!") + if lang.get("language_ietf") != "en-us": + if t := lang_default.get(key): + log.info(f"Using default language for key [{key}]...") + return t.format(**kwargs) + return "(translation not found)" + +def xlate_aliases(message, aliases, arg = None): + t = xlate( + message, + commands = " OR ".join( + [ + f"`/{c}{("" if arg == None else " <" + xlate(arg) + ">")}`" + for c in aliases + ] + ), + ) + return t + +def strip_entities(message): + text = message.text + entities = message.parse_entities() + log.debug(f"{entities=}") + for v in entities.values(): + text = text.replace(v, "") + text = text.replace(" ", "").strip() + log.debug(f"Stripped entities from message [{message.text}]: [{text}]") + return text \ No newline at end of file From 9edbff86f634cb46427a94e2e6b9f3250b50f6fb Mon Sep 17 00:00:00 2001 From: Jordan Ella <45442073+jordanella@users.noreply.github.com> Date: Wed, 17 Jan 2024 23:50:28 -0500 Subject: [PATCH 3/4] Parameter modification Cleaned up any left over or missing arguments from the button migration. Modified references to series_commands, movie_commands, and book_commands to be consistent with all the other command alias functions so that it would work with a unified method to reduce code reuse. --- buttons.py | 6 +++--- commands/book.py | 1 + commands/series.py | 5 +++-- lang/ca-es.yml | 6 +++--- lang/de-de.yml | 6 +++--- lang/en-us.yml | 6 +++--- lang/es-es.yml | 6 +++--- lang/fr-fr.yml | 4 ++-- lang/it-it.yml | 6 +++--- lang/lt-lt.yml | 6 +++--- lang/pt-br.yml | 6 +++--- lang/ro.yml | 6 +++--- lang/ru-ru.yml | 6 +++--- lang/zh-cn.yml | 6 +++--- searcharr.py | 21 ++++++--------------- 15 files changed, 45 insertions(+), 52 deletions(-) diff --git a/buttons.py b/buttons.py index e372b7e..4d02784 100644 --- a/buttons.py +++ b/buttons.py @@ -8,7 +8,7 @@ def prev(self, cid, i): xlate("prev_button"), callback_data=f"{cid}^^^{i}^^^prev" ) - def next(self, cid, i, total_results): + def next(self, cid, i): return InlineKeyboardButton( xlate("next_button"), callback_data=f"{cid}^^^{i}^^^next" ) @@ -41,10 +41,10 @@ def link(self, link): class ActionButtons(object): def add(self, kind, cid, i): - InlineKeyboardButton( + return InlineKeyboardButton( xlate("add_button", kind=xlate(kind).title()), callback_data=f"{cid}^^^{i}^^^add", - ), + ) def already_added(self, cid, i): return InlineKeyboardButton( diff --git a/commands/book.py b/commands/book.py index ec5476c..7922817 100644 --- a/commands/book.py +++ b/commands/book.py @@ -10,6 +10,7 @@ class Book(Command): def _action(self, update, context): self._search_collection( update=update, + context=context, kind="book", plural="book", search_function=self.searcharr.readarr.lookup_book, diff --git a/commands/series.py b/commands/series.py index 7aec481..7dc25e2 100644 --- a/commands/series.py +++ b/commands/series.py @@ -10,8 +10,9 @@ class Series(Command): def _action(self, update, context): self._search_collection( update=update, + context=context, kind="series", plural="series", - search_function = self.searcharr.sonarr.lookup_series, - command_aliases = settings.sonarr_series_command_aliases + search_function=self.searcharr.sonarr.lookup_series, + command_aliases=settings.sonarr_series_command_aliases ) \ No newline at end of file diff --git a/lang/ca-es.yml b/lang/ca-es.yml index 30cf227..3cc52d4 100644 --- a/lang/ca-es.yml +++ b/lang/ca-es.yml @@ -56,13 +56,13 @@ make_admin_button: Fer Administrador remove_admin_button: Eliminar Administrador done: Fet listing_users_pagination: Enumerant usuaris de Searcharr {page_info}. -help_sonarr: Utilitza {series_commands} per afegir una sèrie a Sonarr. -help_radarr: Utilitza {movie_commands} per afegir una pel·lícula a Radarr. +help_sonarr: Utilitza {commands} per afegir una sèrie a Sonarr. +help_radarr: Utilitza {commands} per afegir una pel·lícula a Radarr. no_features: Ho sento, però totes les meves funcions estàn desactivades temporalment. admin_help: Com que ets administrador, també pots utilitzar {commands} per gestionar els usuaris. readarr_disabled: Ho sento, però el suport per llibres està desactivat. include_book_title_in_cmd: Sisplau, inclou el títol del llibre en la ordre, per exemple {commands} no_matching_books: Ho sento, però no he trobat cap llibre que compleixi el criteri de cerca. -help_readarr: Utilitza {book_commands} per afegir una llibre a Readarr. +help_readarr: Utilitza {commands} per afegir una llibre a Readarr. no_metadata_profiles: "Error afegint {kind}: no hi han perfils de metadata activats per {app}! Sisplau, comprova la teva configuració de Searcharr i torna a intentar-ho." add_metadata_button: "Afegir Metadata: {metadata}" \ No newline at end of file diff --git a/lang/de-de.yml b/lang/de-de.yml index 30239df..c31e2c0 100644 --- a/lang/de-de.yml +++ b/lang/de-de.yml @@ -56,13 +56,13 @@ make_admin_button: Administrator Machen remove_admin_button: Administrator entfernen done: Erledigt listing_users_pagination: Searcharr-Benutzer auflisten {page_info}. -help_sonarr: Verwenden Sie {series_commands} um Sonarr eine Serie hinzuzufügen. -help_radarr: Verwenden Sie {movie_commands} um einen Film zu Radarr hinzuzufügen. +help_sonarr: Verwenden Sie {commands} um Sonarr eine Serie hinzuzufügen. +help_radarr: Verwenden Sie {commands} um einen Film zu Radarr hinzuzufügen. no_features: Tut mir leid, aber alle meine Funktionen sind derzeit deaktiviert. admin_help: Da Sie ein Administrator sind, können Sie auch {commands} verwenden, um Benutzer zu verwalten. readarr_disabled: Tut mir leid, aber die Buchunterstützung ist deaktiviert. include_book_title_in_cmd: Bitte geben Sie den Buchtitel in den Befehl ein, z.b. {commands} no_matching_books: Tut mir leid, aber ich habe keine passenden Buchen gefunden. -help_readarr: Verwenden Sie {book_commands} um einen Buchen zu Readarr hinzuzufügen. +help_readarr: Verwenden Sie {commands} um einen Buchen zu Readarr hinzuzufügen. no_metadata_profiles: "Fehler beim Hinzufügen {kind}: keine Metadatenprofil aktiviert für {app}! Bitte überprüfen Sie Ihre Searcharr-Konfiguration und versuchen Sie es erneut." add_metadata_button: "Metadaten hinzufügen: {metadata}" \ No newline at end of file diff --git a/lang/en-us.yml b/lang/en-us.yml index d914d88..59b3325 100644 --- a/lang/en-us.yml +++ b/lang/en-us.yml @@ -56,13 +56,13 @@ make_admin_button: Make Admin remove_admin_button: Remove Admin done: Done listing_users_pagination: Listing Searcharr users {page_info}. -help_sonarr: Use {series_commands} to add a series to Sonarr. -help_radarr: Use {movie_commands} to add a movie to Radarr. +help_sonarr: Use {commands} to add a series to Sonarr. +help_radarr: Use {commands} to add a movie to Radarr. no_features: Sorry, but all of my features are currently disabled. admin_help: Since you are an admin, you can also use {commands} to manage users. readarr_disabled: Sorry, but book support is disabled. include_book_title_in_cmd: Please include the book title in the command, e.g. {commands} no_matching_books: Sorry, but I didn't find any matching books. -help_readarr: Use {book_commands} to add a book to Readarr. +help_readarr: Use {commands} to add a book to Readarr. no_metadata_profiles: "Error adding {kind}: no metadata profiles enabled for {app}! Please check your Searcharr configuration and try again." add_metadata_button: "Add Metadata: {metadata}" \ No newline at end of file diff --git a/lang/es-es.yml b/lang/es-es.yml index ba1eb0c..8792045 100644 --- a/lang/es-es.yml +++ b/lang/es-es.yml @@ -56,13 +56,13 @@ make_admin_button: Hacer Administrador remove_admin_button: Eliminar Administrador done: Hecho listing_users_pagination: Listando usuarios de Searcharr {page_info}. -help_sonarr: Usa {series_commands} para añadir serie a Sonarr. -help_radarr: Usa {movie_commands} para añadir película a Radarr. +help_sonarr: Usa {commands} para añadir serie a Sonarr. +help_radarr: Usa {commands} para añadir película a Radarr. no_features: Lo siento, pero todas mis funciones están actualmente desactivadas. admin_help: Ya que eres administrador, también puedes usar {commands} para gestionar los usuarios. readarr_disabled: Lo siento, pero el soporte para libros está desativado. include_book_title_in_cmd: Por favor, incluye el título del libro en el comando, por ejemplo {commands} no_matching_books: Lo siento, no he encontrado ninguna libro que cumpla el criterio de búsqueda. -help_readarr: Usa {book_commands} para añadir libro a Readarr. +help_readarr: Usa {commands} para añadir libro a Readarr. no_metadata_profiles: "¡Error añadiendo {kind}: no se han encontrado perfiles de metadata activados para {app}! Por favor, consulta tu configuración de Searcharr e inténtalo de nuevo." add_metadata_button: "Añadir Metadata: {metadata}" \ No newline at end of file diff --git a/lang/fr-fr.yml b/lang/fr-fr.yml index 9a3a71d..eecf12b 100644 --- a/lang/fr-fr.yml +++ b/lang/fr-fr.yml @@ -56,8 +56,8 @@ make_admin_button: rendre admin remove_admin_button: retirer l'admin done: Terminé listing_users_pagination: Liste des utilisateurs de Searcharr {page_info}. -help_sonarr: Utilisez {series_commands} pour ajouter une série à Sonarr. -help_radarr: Utilisez {movie_commands} pour ajouter un film à Radarr. +help_sonarr: Utilisez {commands} pour ajouter une série à Sonarr. +help_radarr: Utilisez {commands} pour ajouter un film à Radarr. no_features: Désolé, mais toutes mes fonctions sont actuellement désactivées. admin_help: Puisque vous êtes un administrateur, vous pouvez également utiliser les {commands} pour gérer les utilisateurs. readarr_disabled: Désolé, mais le support des livres est désactivé. diff --git a/lang/it-it.yml b/lang/it-it.yml index f6f7834..31f5319 100644 --- a/lang/it-it.yml +++ b/lang/it-it.yml @@ -56,13 +56,13 @@ make_admin_button: Crea Amministratore remove_admin_button: Rimuovi Amministratore done: Fatto listing_users_pagination: Elenco utenti Searcharr {page_info}. -help_sonarr: Usa {series_commands} per aggiungere una serie a Sonarr. -help_radarr: Usa {movie_commands} per aggiungere un film a Radarr. +help_sonarr: Usa {commands} per aggiungere una serie a Sonarr. +help_radarr: Usa {commands} per aggiungere un film a Radarr. no_features: Tutte le funzionalità sono attualmente disabilitate. admin_help: Essendo un amministratore, puoi anche usare i comandi {commands} per gestire gli utenti. readarr_disabled: Mi dispiace, ma il supporto ai libri è disabilitato. include_book_title_in_cmd: Per favore includere il titolo del libro nel comando (es. {commands}) no_matching_books: Nessun libro corrispondente trovato. -help_readarr: Usa {book_commands} per aggiungere un libro a Readarr. +help_readarr: Usa {commands} per aggiungere un libro a Readarr. no_metadata_profiles: "Errore durante aggiunta {kind}: nessun profilo metadata abilitato per {app}! Verificare la configurazione di Searcharr e provare di nuovo." add_metadata_button: "Aggiungi Metadata: {metadata}" diff --git a/lang/lt-lt.yml b/lang/lt-lt.yml index 984bf4c..e18c91f 100644 --- a/lang/lt-lt.yml +++ b/lang/lt-lt.yml @@ -56,13 +56,13 @@ make_admin_button: Sukurti administratorių remove_admin_button: Pašalinti administratorių done: Atlikta listing_users_pagination: Searcharr vartotojų sąrašas {page_info}. -help_sonarr: Naudokite {series_commands} norėdami įtraukti serialą į Sonarr. -help_radarr: Naudokite {movie_commands} norėdami įtraukti filmą į Radarr. +help_sonarr: Naudokite {commands} norėdami įtraukti serialą į Sonarr. +help_radarr: Naudokite {commands} norėdami įtraukti filmą į Radarr. no_features: Atsiprašome, bet šiuo metu visos funkcijos išjungtos. admin_help: Kadangi esate administratorius, galite administruoti vartotojus komandų {commands} pagalba. readarr_disabled: Atsiprašau, bet negalima ieškoti knygų. include_book_title_in_cmd: Prašome kartu su komanda parašyti knyga pavadinimą, pvz. {commands} no_matching_books: Atsiprašau, bet toks knygos nerastas. -help_readarr: Naudokite {book_commands} norėdami įtraukti knyga į Readarr. +help_readarr: Naudokite {commands} norėdami įtraukti knyga į Readarr. no_metadata_profiles: "Klaida {kind}: neaprašyti {app} metaduomenų profiliai! Patikrinkite Searcharr konfigūraciją ir bandykite dar kartą." add_metadata_button: "Metadata: {metadata}" \ No newline at end of file diff --git a/lang/pt-br.yml b/lang/pt-br.yml index 211962b..be07440 100644 --- a/lang/pt-br.yml +++ b/lang/pt-br.yml @@ -56,13 +56,13 @@ make_admin_button: Tornar Admin remove_admin_button: Remover Admin done: feito listing_users_pagination: Listando usuários do Searcharr {page_info}. -help_sonarr: Use {series_commands} para adicionar uma série ao Sonarr. -help_radarr: Use {movie_commands} para adicionar um filme ao Radarr. +help_sonarr: Use {commands} para adicionar uma série ao Sonarr. +help_radarr: Use {commands} para adicionar um filme ao Radarr. no_features: Desculpe, mas todos os meus recursos estão desativados no momento. admin_help: Como você é um administrador, você também pode usar {commands} para gerenciar usuários. readarr_disabled: Desculpe, mas o suporte a filmes está desativado. include_book_title_in_cmd: Por favor, inclua o título do livro no comando, por exemplo. {commands} no_matching_books: Desculpe, mas não encontrei nenhum livro correspondente. -help_readarr: Use {book_commands} para adicionar um livro ao Readarr. +help_readarr: Use {commands} para adicionar um livro ao Readarr. no_metadata_profiles: "Erro ao adicionar {kind}: nenhum perfil de metadados habilitado para {app}! Verifique a configuração do Searcharr e tente novamente." add_metadata_button: "Add Metadados: {metadata}" \ No newline at end of file diff --git a/lang/ro.yml b/lang/ro.yml index 00520a4..af7d470 100644 --- a/lang/ro.yml +++ b/lang/ro.yml @@ -56,13 +56,13 @@ make_admin_button: Adaugati administrator remove_admin_button: Eliminați Admin done: Terminat listing_users_pagination: Listarea utilizatorilor Searcharr {page_info}. -help_sonarr: Foloseste {series_commands} pentru a adăuga un serial la Sonarr. -help_radarr: Foloseste {movie_commands} pentru a adăuga un film la Radarr. +help_sonarr: Foloseste {commands} pentru a adăuga un serial la Sonarr. +help_radarr: Foloseste {commands} pentru a adăuga un film la Radarr. no_features: Ne pare rău, dar toate funcțiile mele sunt dezactivate. admin_help: Deoarece sunteți administrator, puteți utiliza și {commands} pentru a gestiona utilizatorii. readarr_disabled: Scuze, dar suportul pentru carti este dezactivat. include_book_title_in_cmd: Vă rugăm să includeți titlul cartii în comandă, de exemplu {commands} no_matching_books: Îmi pare rău, dar nu am găsit nici o carte cu titlu acesta. -help_readarr: Foloseste {book_commands} pentru a adăuga un carte la Readarr. +help_readarr: Foloseste {commands} pentru a adăuga un carte la Readarr. no_metadata_profiles: "Eroare la adăugare {kind}: nu sunt activate profiluri de metadate pentru {app}! Verificați configurația Searcharr și încercați din nou." add_metadata_button: "Adăugați metadate: {metadata}" \ No newline at end of file diff --git a/lang/ru-ru.yml b/lang/ru-ru.yml index 3bc61c5..3f3b1f6 100644 --- a/lang/ru-ru.yml +++ b/lang/ru-ru.yml @@ -56,13 +56,13 @@ make_admin_button: Сделать администратором remove_admin_button: Удалить права администратора done: Сделано listing_users_pagination: Список пользователей Searcharr {page_info}. -help_sonarr: Используйте {series_commands} для добавления сериалов в Sonarr. -help_radarr: Используйте {movie_commands} для добавления фильмов в Radarr. +help_sonarr: Используйте {commands} для добавления сериалов в Sonarr. +help_radarr: Используйте {commands} для добавления фильмов в Radarr. no_features: Извините, но все мои функции, на данный момент, отключены. admin_help: У вас есть права администратора, поэтому вы можете использовать {commands} для управления пользователям. readarr_disabled: Извините, но поддержка книг отключена. include_book_title_in_cmd: Добавьте в команду название книги, например {commands} no_matching_books: Извините, но я не смог найти подходящие книги. -help_readarr: Используйте {book_commands}, для добавления сериалов в Readarr. +help_readarr: Используйте {commands}, для добавления сериалов в Readarr. no_metadata_profiles: "Ошибка при добавлении {kind}: профили метаданных не включены для {app}! Пожалуйста, проверьте настройки Searcharr и повторите попытку." add_metadata_button: "Добавить метаданные: {metadata}" \ No newline at end of file diff --git a/lang/zh-cn.yml b/lang/zh-cn.yml index 36163d4..4904db2 100644 --- a/lang/zh-cn.yml +++ b/lang/zh-cn.yml @@ -56,13 +56,13 @@ make_admin_button: 设为管理员 remove_admin_button: 删除管理员 done: 完成 listing_users_pagination: 列出 Searcharr 用户 {page_info}。 -help_sonarr: 使用 {series_commands} 将电视剧添加到 Sonarr。 -help_radarr: 使用 {movie_commands} 将电影添加到 Radarr。 +help_sonarr: 使用 {commands} 将电视剧添加到 Sonarr。 +help_radarr: 使用 {commands} 将电影添加到 Radarr。 no_features: 所有功能已被禁用。 admin_help: 管理员:使用 {commands} 来管理用户。 readarr_disabled: 书籍支持已禁用 include_book_title_in_cmd: 请在命令中包含书名,例如 {commands}。 no_matching_books: 未找到匹配的书籍。 -help_readarr: 使用 {book_commands} 将书籍添加到 Readarr。 +help_readarr: 使用 {commands} 将书籍添加到 Readarr。 no_metadata_profiles: "添加 {kind} 时出错:没有为 {app} 启用元数据配置文件!请检查您的 Searcharr 配置并重试。" add_metadata_button: "添加元数据: {metadata}" diff --git a/searcharr.py b/searcharr.py index 23b83c1..7978bac 100644 --- a/searcharr.py +++ b/searcharr.py @@ -7,7 +7,6 @@ import argparse import json import os -import yaml import sqlite3 from threading import Lock from urllib.parse import parse_qsl @@ -24,7 +23,7 @@ import readarr import settings import util -from util import xlate +from util import xlate, xlate_aliases from commands import Command from buttons import KeyboardButtons @@ -422,15 +421,7 @@ def callback(self, update, context): auth_level = self._authenticated(query.from_user.id) if not auth_level: query.message.reply_text( - xlate( - "auth_required", - commands=" OR ".join( - [ - f"`/{c} <{xlate('password')}>`" - for c in settings.searcharr_start_command_aliases - ] - ), - ) + xlate_aliases("auth_required", settings.searcharr_start_command_aliases, "password") ) query.message.delete() query.answer() @@ -1160,7 +1151,7 @@ def _prepare_response( ) if total_results > 1 and i < total_results - 1: keyboardNavRow.append( - buttons.nav.next(cid, i, total_results) + buttons.nav.next(cid, i) ) keyboard.append(keyboardNavRow) @@ -1198,14 +1189,14 @@ def _prepare_response( if not add: if not r["id"]: keyboardActRow.append( - buttons.act.add(cid, i) + buttons.act.add(kind, cid, i) ) else: keyboardActRow.append( - buttons.act.already_added(i) + buttons.act.already_added(cid, i) ) keyboardActRow.append( - buttons.act.cancel(i) + buttons.act.cancel(cid, i) ) if len(keyboardActRow): keyboard.append(keyboardActRow) From 0c770ea291565fb9e0ba0af0f375f19ab672084d Mon Sep 17 00:00:00 2001 From: Jordan Ella <45442073+jordanella@users.noreply.github.com> Date: Thu, 18 Jan 2024 00:05:36 -0500 Subject: [PATCH 4/4] Flake8 compliance No new line at end of file, order of imports, and various whitespace concerns. --- buttons.py | 39 ++++++++++++++++++++++----------------- commands/__init__.py | 24 ++++++++++++------------ commands/book.py | 10 +++++----- commands/movie.py | 10 +++++----- commands/series.py | 12 ++++++------ searcharr.py | 14 +++++++------- 6 files changed, 57 insertions(+), 52 deletions(-) diff --git a/buttons.py b/buttons.py index 4d02784..41ab638 100644 --- a/buttons.py +++ b/buttons.py @@ -12,12 +12,13 @@ def next(self, cid, i): return InlineKeyboardButton( xlate("next_button"), callback_data=f"{cid}^^^{i}^^^next" ) - + def done(self, cid, i): return InlineKeyboardButton( xlate("done"), callback_data=f"{cid}^^^{i}^^^done" ) + class ExternalButtons(object): def imdb(self, r): return InlineKeyboardButton( @@ -33,12 +34,13 @@ def tmdb(self, r): return InlineKeyboardButton( "TMDB", url=f"https://www.themoviedb.org/movie/{r['tmdbId']}" ) - + def link(self, link): return InlineKeyboardButton( link["name"], url=link["url"] ) + class ActionButtons(object): def add(self, kind, cid, i): return InlineKeyboardButton( @@ -51,75 +53,78 @@ def already_added(self, cid, i): xlate("already_added_button"), callback_data=f"{cid}^^^{i}^^^noop", ) - + def cancel(self, cid, i): return InlineKeyboardButton( xlate("cancel_search_button"), callback_data=f"{cid}^^^{i}^^^cancel", ) - + def series_anime(self, cid, i): return InlineKeyboardButton( xlate("add_series_anime_button"), callback_data=f"{cid}^^^{i}^^^add^^st=a", ) - + + class AddButtons(object): def tag(self, tag, cid, i): return InlineKeyboardButton( xlate("add_tag_button", tag=tag["label"]), callback_data=f"{cid}^^^{i}^^^add^^tt={tag['id']}", ) - + def finished_tagging(self, cid, i): return InlineKeyboardButton( xlate("finished_tagging_button"), callback_data=f"{cid}^^^{i}^^^add^^td=1", ) - + def monitor(self, o, k, cid, i): return InlineKeyboardButton( xlate("monitor_button", option=o), callback_data=f"{cid}^^^{i}^^^add^^m={k}", ) - + def quality(self, q, cid, i): return InlineKeyboardButton( xlate("add_quality_button", quality=q["name"]), callback_data=f"{cid}^^^{i}^^^add^^q={q['id']}", ) - + def metadata(self, m, cid, i): return InlineKeyboardButton( xlate("add_metadata_button", metadata=m["name"]), callback_data=f"{cid}^^^{i}^^^add^^m={m['id']}", ) - + def path(self, p, cid, i): return InlineKeyboardButton( xlate("add_path_button", path=p["path"]), callback_data=f"{cid}^^^{i}^^^add^^p={p['id']}", ) + class UserButtons(object): def remove(self, u, cid): return InlineKeyboardButton( xlate("remove_user_button"), callback_data=f"{cid}^^^{u['id']}^^^remove_user", ) - + def username(self, u, cid): return InlineKeyboardButton( f"{u['username'] if u['username'] != 'None' else u['id']}", callback_data=f"{cid}^^^{u['id']}^^^noop", ) - + def admin(self, u, cid): return InlineKeyboardButton( xlate("remove_admin_button") if u["admin"] else xlate("make_admin_button"), callback_data=f"{cid}^^^{u['id']}^^^{'remove_admin' if u['admin'] else 'make_admin'}", ) + class KeyboardButtons(object): def __init__(self): self.nav_buttons = NavButtons() @@ -131,19 +136,19 @@ def __init__(self): @property def nav(self): return self.nav_buttons - + @property def ext(self): return self.ext_buttons - + @property def act(self): return self.act_buttons - + @property def add(self): return self.add_buttons - + @property def user(self): - return self.user_buttons \ No newline at end of file + return self.user_buttons diff --git a/commands/__init__.py b/commands/__init__.py index 7925515..6e009d0 100644 --- a/commands/__init__.py +++ b/commands/__init__.py @@ -1,13 +1,12 @@ -import os +import util +import settings +import traceback import sys +import os from importlib import util as import_util -sys.path.append(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) from telegram.error import BadRequest -import traceback - -import settings -import util from util import xlate, xlate_aliases +sys.path.append(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) class Command: @@ -72,7 +71,7 @@ def _execute(self, update, context): def _action(self, update, context): pass - + def _search_collection(self, update, context, kind, plural, search_function, command_aliases): title = util.strip_entities(update.message) @@ -85,10 +84,10 @@ def _search_collection(self, update, context, kind, plural, search_function, com results = search_function(title) cid = self.searcharr._generate_cid() self.searcharr._create_conversation( - id = cid, - username = str(update.message.from_user.username), - kind = kind, - results = results, + id=cid, + username=str(update.message.from_user.username), + kind=kind, + results=results, ) if not len(results): @@ -128,6 +127,7 @@ def load_module(path): spec.loader.exec_module(module) return module + path = os.path.abspath(__file__) dirpath = os.path.dirname(path) @@ -137,4 +137,4 @@ def load_module(path): try: load_module(os.path.join(dirpath, fname)) except Exception: - traceback.print_exc() \ No newline at end of file + traceback.print_exc() diff --git a/commands/book.py b/commands/book.py index 7922817..9c35739 100644 --- a/commands/book.py +++ b/commands/book.py @@ -9,10 +9,10 @@ class Book(Command): def _action(self, update, context): self._search_collection( - update=update, + update=update, context=context, - kind="book", - plural="book", - search_function=self.searcharr.readarr.lookup_book, + kind="book", + plural="book", + search_function=self.searcharr.readarr.lookup_book, command_aliases=settings.readarr_book_command_aliases - ) \ No newline at end of file + ) diff --git a/commands/movie.py b/commands/movie.py index 16fea2b..65660fb 100644 --- a/commands/movie.py +++ b/commands/movie.py @@ -10,9 +10,9 @@ class Movie(Command): def _action(self, update, context): self._search_collection( update=update, - context=context, - kind="movie", - plural="movies", - search_function=self.searcharr.radarr.lookup_movie, + context=context, + kind="movie", + plural="movies", + search_function=self.searcharr.radarr.lookup_movie, command_aliases=settings.radarr_movie_command_aliases - ) \ No newline at end of file + ) diff --git a/commands/series.py b/commands/series.py index 7dc25e2..6a51a09 100644 --- a/commands/series.py +++ b/commands/series.py @@ -9,10 +9,10 @@ class Series(Command): def _action(self, update, context): self._search_collection( - update=update, - context=context, - kind="series", - plural="series", - search_function=self.searcharr.sonarr.lookup_series, + update=update, + context=context, + kind="series", + plural="series", + search_function=self.searcharr.sonarr.lookup_series, command_aliases=settings.sonarr_series_command_aliases - ) \ No newline at end of file + ) diff --git a/searcharr.py b/searcharr.py index 7978bac..097a552 100644 --- a/searcharr.py +++ b/searcharr.py @@ -498,7 +498,7 @@ def callback(self, update, context): reply_message, reply_markup = self._prepare_response_users( cid, convo["results"], - i-1, + i - 1, len(convo["results"]), ) context.bot.edit_message_text( @@ -542,13 +542,13 @@ def callback(self, update, context): reply_markup=reply_markup, ) elif convo["type"] == "users": - #if i >= len(convo["results"])/5: + # if i >= len(convo["results"])/5: # query.answer() # return reply_message, reply_markup = self._prepare_response_users( cid, convo["results"], - i+1, + i + 1, len(convo["results"]), ) context.bot.edit_message_text( @@ -1232,7 +1232,7 @@ def _prepare_response( def _prepare_response_users(self, cid, users, i, total_results): buttons = KeyboardButtons() keyboard = [] - for u in users[i*5 : (i*5)+5]: + for u in users[i * 5 : (i * 5) + 5]: keyboard.append( [ buttons.user.remove(u, cid), @@ -1248,7 +1248,7 @@ def _prepare_response_users(self, cid, users, i, total_results): keyboardNavRow.append( buttons.nav.done(cid, i) ) - if total_results/5 > 1 and (i+1)*5 < total_results: + if total_results / 5 > 1 and (i + 1) * 5 < total_results: keyboardNavRow.append( buttons.nav.next(cid, i) ) @@ -1257,7 +1257,7 @@ def _prepare_response_users(self, cid, users, i, total_results): reply_message = xlate( "listing_users_pagination", - page_info=f"{i*5+1}-{min((i+1)*5, total_results)} of {total_results}", + page_info=f"{i * 5 + 1}-{min((i + 1) * 5, total_results)} of {total_results}", ) return (reply_message, reply_markup) @@ -1597,4 +1597,4 @@ def _init_db(self): util.log = set_up_logger("searcharr", args.verbose, args.console_logging) util.load_language() tgr = Searcharr(settings.tgram_token) - tgr.run() \ No newline at end of file + tgr.run()