diff --git a/buttons.py b/buttons.py new file mode 100644 index 0000000..41ab638 --- /dev/null +++ b/buttons.py @@ -0,0 +1,154 @@ +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): + 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): + return 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 diff --git a/commands/__init__.py b/commands/__init__.py new file mode 100644 index 0000000..6e009d0 --- /dev/null +++ b/commands/__init__.py @@ -0,0 +1,140 @@ +import util +import settings +import traceback +import sys +import os +from importlib import util as import_util +from telegram.error import BadRequest +from util import xlate, xlate_aliases +sys.path.append(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) + + +class Command: + _dict = {} + _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._dict[cls._name] = instance + + def _inject_dependency(self, searcharr_instance): + self.searcharr = searcharr_instance + + 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(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(xlate("radarr_disabled")) + return None + + def _validate_sonarr_enabled(self, update): + if settings.sonarr_enabled: + return True + else: + 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(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): + 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 = util.strip_entities(update.message) + + if not len(title): + x_title = xlate("title").title() + response = 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(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: + util.log.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 = import_util.spec_from_file_location(name, path) + module = import_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() diff --git a/commands/book.py b/commands/book.py new file mode 100644 index 0000000..9c35739 --- /dev/null +++ b/commands/book.py @@ -0,0 +1,18 @@ +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, + context=context, + kind="book", + plural="book", + search_function=self.searcharr.readarr.lookup_book, + command_aliases=settings.readarr_book_command_aliases + ) diff --git a/commands/help.py b/commands/help.py new file mode 100644 index 0000000..9bfe545 --- /dev/null +++ b/commands/help.py @@ -0,0 +1,28 @@ +from commands import Command +import settings +from util import xlate, xlate_aliases + + +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 = xlate_aliases("help_sonarr", settings.sonarr_series_command_aliases, "title") + response += f" {sonarr_help}" + if self.searcharr.radarr: + radarr_help = xlate_aliases("help_radarr", settings.radarr_movie_command_aliases, "title") + response += f" {radarr_help}" + if self.searcharr.readarr: + readarr_help = xlate_aliases("help_readarr", settings.readarr_book_command_aliases, "title") + response += f" {readarr_help}" + if response == "": + response = xlate("no_features") + + if self.auth_level == 2: + 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/movie.py b/commands/movie.py new file mode 100644 index 0000000..65660fb --- /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 + ) diff --git a/commands/series.py b/commands/series.py new file mode 100644 index 0000000..6a51a09 --- /dev/null +++ b/commands/series.py @@ -0,0 +1,18 @@ +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, + context=context, + kind="series", + plural="series", + search_function=self.searcharr.sonarr.lookup_series, + command_aliases=settings.sonarr_series_command_aliases + ) diff --git a/commands/start.py b/commands/start.py new file mode 100644 index 0000000..d651ee3 --- /dev/null +++ b/commands/start.py @@ -0,0 +1,39 @@ +from commands import Command +import settings +import util +from util import xlate, xlate_aliases + + +class Start(Command): + _name = "start" + _command_aliases = settings.searcharr_start_command_aliases + _validation_checks = [] + + def _action(self, update, context): + password = util.strip_entities(update.message) + util.log.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( + xlate_aliases("admin_auth_success", settings.searcharr_help_command_aliases) + ) + elif self.searcharr._authenticated(update): + update.message.reply_text( + 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( + xlate_aliases("auth_successful", settings.searcharr_help_command_aliases) + ) + else: + update.message.reply_text(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..9fdbf8a --- /dev/null +++ b/commands/users.py @@ -0,0 +1,40 @@ +from commands import Command +import settings +from util import xlate, xlate_aliases + + +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( + 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(xlate("no_users_found")) + else: + reply_message, reply_markup = self.searcharr._prepare_response_users( + cid, + results, + 0, + 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/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 fa08ec9..097a552 100644 --- a/searcharr.py +++ b/searcharr.py @@ -7,14 +7,13 @@ import argparse import json import os -import yaml import sqlite3 from threading import Lock from urllib.parse import parse_qsl 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,6 +22,10 @@ import sonarr import readarr import settings +import util +from util import xlate, xlate_aliases +from commands import Command +from buttons import KeyboardButtons __version__ = "3.2.2" @@ -63,10 +66,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 @@ -79,23 +80,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 @@ -103,66 +104,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 @@ -175,23 +176,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 @@ -199,66 +200,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( @@ -270,7 +271,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: @@ -280,23 +281,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 @@ -306,23 +307,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 @@ -330,415 +331,97 @@ 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 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( + 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( - "auth_required", - commands=" OR ".join( - [ - f"`/{c} <{self._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() @@ -751,7 +434,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 @@ -760,22 +443,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"]: @@ -793,7 +475,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( @@ -816,8 +498,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( @@ -832,7 +513,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"]) ) @@ -843,7 +524,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( @@ -861,14 +542,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( @@ -880,7 +560,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" @@ -908,7 +588,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( @@ -928,16 +608,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" @@ -966,7 +646,7 @@ def callback(self, update, context): ), None, ) - logger.debug( + util.log.debug( f"Path id [{additional_data['p']}] lookup result: [{path}]" ) if path: @@ -998,7 +678,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( @@ -1018,16 +698,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" @@ -1059,7 +739,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( @@ -1079,16 +759,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" @@ -1107,9 +787,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( @@ -1128,7 +808,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( @@ -1171,7 +851,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"): @@ -1191,7 +871,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( @@ -1217,7 +897,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 @@ -1226,7 +906,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 @@ -1241,19 +921,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( @@ -1280,25 +960,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 ] ), @@ -1325,28 +1005,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 ] ), @@ -1371,28 +1050,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 ] ), @@ -1417,19 +1095,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() @@ -1448,43 +1125,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) ) keyboard.append(keyboardNavRow) @@ -1492,95 +1159,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(kind, cid, i) ) else: keyboardActRow.append( - InlineKeyboardButton( - self._xlate("already_added_button"), - callback_data=f"{cid}^^^{i}^^^noop", - ), + buttons.act.already_added(cid, i) ) keyboardActRow.append( - InlineKeyboardButton( - self._xlate("cancel_search_button"), - callback_data=f"{cid}^^^{i}^^^cancel", - ), + buttons.act.cancel(cid, 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) @@ -1603,175 +1225,64 @@ 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: 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) + self.commands = Command._dict + for command in self.commands.values(): + command._inject_dependency(searcharr_instance=self) + for c in command._command_aliases: + util.log.debug(f"Registering [/{c}] as a {command._name} command") + updater.dispatcher.add_handler(CommandHandler(c, command._execute)) - 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)) 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." ) @@ -1782,7 +1293,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) @@ -1790,7 +1301,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 @@ -1804,7 +1315,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}" ) @@ -1814,37 +1325,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: @@ -1853,7 +1364,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 @@ -1861,20 +1372,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 {} @@ -1883,7 +1394,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) @@ -1891,13 +1402,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: @@ -1906,7 +1417,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 @@ -1915,7 +1426,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) @@ -1923,14 +1434,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) @@ -1938,19 +1449,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}" ) @@ -1959,7 +1470,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 [] @@ -1968,7 +1479,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) @@ -1976,7 +1487,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): @@ -1984,25 +1495,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): @@ -2018,12 +1529,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: @@ -2031,11 +1542,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) @@ -2063,48 +1574,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", @@ -2114,6 +1594,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() 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