diff --git a/.github/workflows/beta.yml b/.github/workflows/beta.yml index e1b5b39..a4a1eb0 100644 --- a/.github/workflows/beta.yml +++ b/.github/workflows/beta.yml @@ -11,19 +11,19 @@ jobs: steps: - name: Set up QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v2 - name: Login to DockerHub - uses: docker/login-action@v1 + uses: docker/login-action@v2 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v3 with: platforms: linux/amd64,linux/arm64,linux/arm/v7 push: true diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index df4355f..7adc81e 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -10,23 +10,23 @@ jobs: steps: - name: Set up QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v2 - name: Get Version id: get_version uses: battila7/get-version-action@v2 - name: Login to DockerHub - uses: docker/login-action@v1 + uses: docker/login-action@v2 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v3 with: platforms: linux/amd64,linux/arm64,linux/arm/v7 push: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index df963e2..02a4716 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,23 +10,23 @@ jobs: steps: - name: Set up QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v2 - name: Get Version id: get_version uses: battila7/get-version-action@v2 - name: Login to DockerHub - uses: docker/login-action@v1 + uses: docker/login-action@v2 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v3 with: platforms: linux/amd64,linux/arm64,linux/arm/v7 push: true diff --git a/README.md b/README.md index 5d3e159..872f294 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ### By Todd Roberts https://github.com/toddrob99/searcharr -This bot allows users to add movies to Radarr and series to Sonarr via Telegram messaging app. +This bot allows users to add movies to Radarr, series to Sonarr, and books to Readarr via Telegram messaging app. ## Setup & Run @@ -20,6 +20,7 @@ You are required to update the following settings, at minimum: * Telegram Bot > Token (see [Telegram Bot Setup Instructions](https://core.telegram.org/bots#6-botfather)) * Sonarr > URL, API Key, Quality Profile ID * Radarr > URL, API Key, Quality Profile ID +* Readarr > URL, API Key, Quality Profile ID, Metadata Profile ID ### Docker & Docker-Compose @@ -45,9 +46,9 @@ Send a private message to your bot saying `/start ` where `` **Double Caution**: Do not authenticate as an admin in a group chat. Always use a private message with your bot. -### Search & Add a Series to Sonarr or a Movie to Radarr +### Search & Add a Series to Sonarr, a Movie to Radarr, or a Book to Readarr -Send the bot a (private or group) message saying `/series ` or `/movie <title>` (replace with custom command aliases, as configured in `settings.py`). The bot will reply with information about the first result, along with buttons to move forward and back within the search results, pop out to tvdb, TMDB, or IMDb, add the current series/movie to Sonarr/Radarr, or cancel the search. When you click the button to add the series/movie to Sonarr/Radarr, the bot will ask what root folder to put the series/movie in, then what quality profile to use--unless you have only one root folder or quality profile enabled in Searcharr settings, in which case it will skip those steps and add the series/movie straight away. +Send the bot a (private or group) message saying `/series <title>`, `/movie <title>`, or `/book <title>` (replace with custom command aliases, as configured in `settings.py`). The bot will reply with information about the first result, along with buttons to move forward and back within the search results, pop out to tvdb, TMDB, or IMDb, or Goodreads for books, add the current series/movie/book to Sonarr/Radarr/Readarr, or cancel the search. When you click the button to add the series/movie/book to Sonarr/Radarr/Readarr, the bot will ask what root folder to put the series/movie/book in, then what quality profile to use--unless you have only one root folder or quality profile enabled in Searcharr settings, in which case it will skip those steps and add the series/movie straight away. ### Manage Users diff --git a/lang/ca-es.yml b/lang/ca-es.yml index d6f6442..30cf227 100644 --- a/lang/ca-es.yml +++ b/lang/ca-es.yml @@ -4,6 +4,7 @@ language_label: Català movie: pel·lícula series: sèrie season: temporada +book: llibre title: títol title_here: títol aquí password: contrasenya @@ -25,6 +26,7 @@ convo_not_found: He rebut la teva ordre, però no reconec la conversa. Sisplau, search_canceled: Cerca cancel·lada! no_root_folders: "Error afegint {kind}: no hi han carpetes arrel per {app}! Sisplau, comprova la teva configuració de Searcharr i torna a intentar-ho." no_quality_profiles: "Error afegint {kind}: no hi han perfils de qualitat activats per {app}! Sisplau, comprova la teva configuració de Searcharr i torna a intentar-ho." +no_language_profiles: "Error afegint {kind}: no hi han perfils de llenguatge activats per {app}! Sisplau, comprova la teva configuració de Searcharr i torna a intentar-ho." all_seasons: Totes les Temporades first_season: Només la Primera Temporada latest_season: Només la Última Temporada @@ -42,6 +44,7 @@ add_tag_button: "Afegir Etiqueta: {tag}" finished_tagging_button: Etiquetat Completat monitor_button: Monitoratge {option} add_quality_button: "Afegir Qualitat: {quality}" +add_language_button: "Afegir llenguatge: {language}" add_path_button: Afegir a {path} add_button: "{kind} afegit!" already_added_button: "Ja Afegit!" @@ -57,3 +60,9 @@ 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. 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. +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 7b1dff4..30239df 100644 --- a/lang/de-de.yml +++ b/lang/de-de.yml @@ -4,6 +4,7 @@ language_label: German movie: Film series: Serie season: Staffel +book: Buchen title: Title title_here: Titel hier password: Kennwort @@ -25,6 +26,7 @@ convo_not_found: Ich habe Ihren Befehl erhalten, aber ich erkenne das Gespräch search_canceled: Suche abgebrochen! no_root_folders: "Fehler beim Hinzufügen {kind}: keine Root-Ordner aktiviert für {app}! Bitte überprüfen Sie Ihre Searcharr-Konfiguration und versuchen Sie es erneut." no_quality_profiles: "Fehler beim Hinzufügen {kind}: keine Qualitätsprofile aktiviert für {app}! Bitte überprüfen Sie Ihre Searcharr-Konfiguration und versuchen Sie es erneut." +no_language_profiles: "Fehler beim Hinzufügen {kind}: keine Sprachprofil aktiviert für {app}! Bitte überprüfen Sie Ihre Searcharr-Konfiguration und versuchen Sie es erneut." all_seasons: Alle Staffel first_season: Nur die erste Staffel latest_season: Nur letzte Staffel @@ -42,6 +44,7 @@ add_tag_button: "Etikette hinzufügen: {tag}" finished_tagging_button: Fertiges Etikette monitor_button: überwachen {option} add_quality_button: "Qualität hinzufügen: {quality}" +add_language_button: "Sprache hinzufügen: {language}" add_path_button: Hinzufügen zu {path} add_button: Hinzufügen {kind}! already_added_button: Schon hinzugefügt! @@ -57,3 +60,9 @@ 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. 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. +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 02b269c..d914d88 100644 --- a/lang/en-us.yml +++ b/lang/en-us.yml @@ -4,6 +4,7 @@ language_label: English movie: movie series: series season: season +book: book title: title title_here: title here password: password @@ -25,6 +26,7 @@ convo_not_found: I received your command, but I don't recognize the conversation search_canceled: Search canceled! no_root_folders: "Error adding {kind}: no root folders enabled for {app}! Please check your Searcharr configuration and try again." no_quality_profiles: "Error adding {kind}: no quality profiles enabled for {app}! Please check your Searcharr configuration and try again." +no_language_profiles: "Error adding {kind}: no language profiles enabled for {app}! Please check your Searcharr configuration and try again." all_seasons: All Seasons first_season: First Season Only latest_season: Latest Season Only @@ -42,6 +44,7 @@ add_tag_button: "Add Tag: {tag}" finished_tagging_button: Finished Tagging monitor_button: Monitor {option} add_quality_button: "Add Quality: {quality}" +add_language_button: "Add Language: {language}" add_path_button: Add to {path} add_button: Add {kind}! already_added_button: Already Added! @@ -57,3 +60,9 @@ help_sonarr: Use {series_commands} to add a series to Sonarr. help_radarr: Use {movie_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. +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 a979830..ba1eb0c 100644 --- a/lang/es-es.yml +++ b/lang/es-es.yml @@ -4,6 +4,7 @@ language_label: Español movie: película series: serie season: temporada +book: libro title: título title_here: título aquí password: contraseña @@ -25,6 +26,7 @@ convo_not_found: He recibido tu comando, pero no he reconocido la conversación. search_canceled: ¡Búsqueda cancelada! no_root_folders: "¡Error añadiendo {kind}: no se han encontrado carpetas raíz para {app}! Por favor, consulta tu configuración de Searcharr e inténtalo de nuevo." no_quality_profiles: "¡Error añadiendo {kind}: no se han encontrado perfiles de calidad activados para {app}! Por favor, consulta tu configuración de Searcharr e inténtalo de nuevo." +no_language_profiles: "¡Error añadiendo {kind}: no se han encontrado perfiles de idioma activados para {app}! Por favor, consulta tu configuración de Searcharr e inténtalo de nuevo." all_seasons: Todas las Temporadas first_season: Sólo la Primera Temporada latest_season: Sólo la Última Temporada @@ -42,6 +44,7 @@ add_tag_button: "Añadir Etiqueta: {tag}" finished_tagging_button: Etiquetado Finalizado monitor_button: Monitoraje {option} add_quality_button: "Añadir Calidad: {quality}" +add_language_button: "Añadir Idioma: {language}" add_path_button: Añadir a {path} add_button: ¡Añadir {kind}! already_added_button: ¡Ya Añadido! @@ -57,3 +60,9 @@ help_sonarr: Usa {series_commands} para añadir serie a Sonarr. help_radarr: Usa {movie_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. +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 fe65308..833a2c1 100644 --- a/lang/fr-fr.yml +++ b/lang/fr-fr.yml @@ -4,6 +4,7 @@ language_label: Français movie: film series: séries season: saison +book: livre title: titre title_here: titre ici password: mot de passe @@ -23,8 +24,9 @@ no_matching_series: Désolé, mais je n'ai pas trouvé de série correspondante. no_users_found: Désolé, mais je n'ai pas trouvé d'utilisateurs. Cela semble être une erreur... convo_not_found: J'ai reçu votre commande, mais je ne reconnais pas la conversation. Veuillez recommencer. search_canceled: Recherche annulée ! -no_root_folders: "Error adding {kind} : no root folders enabled for {app} ! Veuillez vérifier votre configuration Searcharr et réessayer." -no_quality_profiles: "Error adding {kind} : no quality profiles enabled for {app} ! Veuillez vérifier votre configuration Searcharr et réessayer." +no_root_folders: "Erreur lors de l'ajout {kind} : aucun dossier racine activé pour {app} ! Veuillez vérifier votre configuration Searcharr et réessayer." +no_quality_profiles: "Erreur lors de l'ajout {kind} : aucun profil de qualité activé pour {app} ! Veuillez vérifier votre configuration Searcharr et réessayer." +no_language_profiles: "Erreur lors de l'ajout {kind} : aucun profil de langue activé pour {app} ! Veuillez vérifier votre configuration Searcharr et réessayer." all_seasons: Toutes saisons first_season: Première saison latest_season: Dernière saison @@ -42,6 +44,7 @@ add_tag_button: "Ajout Tag: {tag}" finished_tagging_button: Tag terminé monitor_button: Monitor {option} add_quality_button: "Qualité : {quality}" +add_language_button: "Langue: {language}" add_path_button: Chemin {path} add_button: ajouter {kind}! already_added_button: Déjà téléchargé! @@ -57,3 +60,9 @@ help_sonarr: Utilisez {series_commands} pour ajouter une série à Sonarr. help_radarr: Utilisez {movie_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é. +include_book_title_in_cmd: "Veuillez inclure le titre du livre dans la commande, par exemple : {commands}" +no_matching_books: Désolé, mais je n'ai pas trouvé de livres correspondants. +help_readarr: Utilisez {book_commands} pour ajouter un livre à Readarr. +no_metadata_profiles: "Erreur lors de l'ajout {kind} : aucun profil de metadata activé pour {app} ! Veuillez vérifier votre configuration Searcharr et réessayer." +add_metadata_button: "Metadata: {metadata}" \ No newline at end of file diff --git a/lang/lt-lt.yml b/lang/lt-lt.yml new file mode 100644 index 0000000..984bf4c --- /dev/null +++ b/lang/lt-lt.yml @@ -0,0 +1,68 @@ +language_ietf: lt-lt +language_label: Lietuvių + +movie: filmai +series: serialai +season: sezonas +book: knyga +title: pavadinimas +title_here: pavadinimas čia +password: slaptažodis +admin_password: administratoriaus slaptažodis +admin_auth_success: Administratoriaus autorizacija sėkminga. Naudokite {commands} komandų sąrašui. +already_authenticated: Jūs jau autorizavotės. Pabandykit {commands} kad sužinotumėte komandas. +auth_successful: Autorizacija sėkminga. Naudokite {commands} komandų sąrašui. +incorrect_pw: Neteisingas slaptažodis. +auth_required: Jūs man nepažįstamas... Prašome autorizuotis su {commands} komanda. +admin_auth_required: Jūs neturite administratoriaus teisių... Prašome autorizuotis su {commands} komanda. +radarr_disabled: Atsiprašau, bet negalima ieškoti filmų. +include_movie_title_in_cmd: Prašome kartu su komanda parašyti filmo pavadinimą, pvz. {commands} +no_matching_movies: Atsiprašau, bet toks filmas nerastas. +sonarr_disabled: Atsiprašau, bet negalima ieškoti serialų. +include_series_title_in_cmd: Prašome kartu su komanda parašyti serialo pavadinimą, pvz. {commands} +no_matching_series: Atsiprašau, bet toks serialas nerastas. +no_users_found: Atsiprašau, bet vartotojas nerastas. Kreipkitės į administratorių... +convo_not_found: Aš gavau komandą, bet negaliu jos įvykdyti. Prašau pabandykit iš naujo. +search_canceled: Paieška atšaukta! +no_root_folders: "Klaida {kind}: neaprašyta {app} šakninis aplankas! Patikrinkite Searcharr konfigūraciją ir bandykite dar kartą." +no_quality_profiles: "Klaida {kind}: neaprašyti {app} kokybės profiliai! Patikrinkite Searcharr konfigūraciją ir bandykite dar kartą." +no_language_profiles: "Klaida {kind}: neaprašyti {app} kalbos profiliai! Patikrinkite Searcharr konfigūraciją ir bandykite dar kartą." +all_seasons: Visus sezonus +first_season: Tik pirmą sezoną +latest_season: Tik paskutinį sezoną +added: "{title} sėkmingai įtrauktas!" +unknown_error_adding: Bandant įtraukti įvyko nežinoma klaida {kind}! +removed_user: Vartotojas [{user}] sėkmingai pašalintas! +unknown_error_removing_user: Bandant pašalinti [{user}] įvyko nežinoma klaida! +added_admin_access: Vartotojui [{user}] suteiktos administratoriaus teisės! +unknown_error_adding_admin: Įvyko nežinoma klaida bandant vartotojui [{user}] suteikti administratoriaus teises! +removed_admin_access: Vartotojo [{user}] administratoriaus teisės sėkmingai pašalintos! +unknown_error_removing_admin: bandant pašalinti vartotojo [{user}] administratoriaus teises įvyko nežinoma klaida! +prev_button: < Ank. +next_button: Sek. > +add_tag_button: "Pridėti žymą: {tag}" +finished_tagging_button: Žymėjimas baigtas +monitor_button: Stebėti {option} +add_quality_button: "Pridėti kokybę: {quality}" +add_language_button: "Pridėti kalbą: {quality}" +add_path_button: Įtraukti į {path} +add_button: Pridėti {kind}! +already_added_button: Pridėta! +cancel_search_button: Atšaukti paiešką +add_series_anime_button: Pridėti serialą kaip anime! +unexpected_error: Kažkas nutiko! +remove_user_button: Pašalinti +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. +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. +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 a9911ae..211962b 100644 --- a/lang/pt-br.yml +++ b/lang/pt-br.yml @@ -4,6 +4,7 @@ language_label: Brazilian Portuguese movie: filme series: séries season: temporada +book: livro title: título title_here: título aqui password: senha @@ -25,6 +26,7 @@ convo_not_found: Recebi seu comando, mas não reconheço a conversa. Por favor, search_canceled: Pesquisa cancelada! no_root_folders: "Erro ao adicionar {kind}: nenhuma pasta raiz habilitada para {app}! Verifique a configuração do Searcharr e tente novamente." no_quality_profiles: "Erro ao adicionar {kind}: nenhum perfil de qualidade habilitado para {app}! Verifique a configuração do Searcharr e tente novamente." +no_language_profiles: "Erro ao adicionar {kind}: nenhum perfil de idioma habilitado para {app}! Verifique a configuração do Searcharr e tente novamente." all_seasons: Todas temporadas first_season: Apenas a primeira temporada latest_season: Apenas a última temporada @@ -42,6 +44,7 @@ add_tag_button: "Add Tag: {tag}" finished_tagging_button: Marcação concluída monitor_button: Monitorar {option} add_quality_button: "Add Qualidade: {quality}" +add_language_button: "Add Idioma: {language}" add_path_button: Add para {path} add_button: Add {kind}! already_added_button: Já adicionado! @@ -56,4 +59,10 @@ 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. 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. \ No newline at end of file +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. +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 573a7cd..00520a4 100644 --- a/lang/ro.yml +++ b/lang/ro.yml @@ -4,6 +4,7 @@ language_label: Romanian movie: film series: serial season: sezon +book: carte title: titlu title_here: titlu aici password: parola @@ -25,6 +26,7 @@ convo_not_found: Am primit comanda ta, dar nu recunosc conversația. Vă rugăm search_canceled: Căutare anulată! no_root_folders: "Eroare la adăugare {kind}: nu sunt activate foldere rădăcină {app}! Verificați configurația Searcharr și încercați din nou." no_quality_profiles: "Eroare la adăugare {kind}: nu sunt activate profiluri de calitate pentru {app}! Verificați configurația Searcharr și încercați din nou." +no_language_profiles: "Eroare la adăugare {kind}: nu sunt activate profiluri de limba pentru {app}! Verificați configurația Searcharr și încercați din nou." all_seasons: Toate sezoanele first_season: Doar primul sezon latest_season: Numai ultimul sezon @@ -42,6 +44,7 @@ add_tag_button: "Adaugă etichetă: {tag}" finished_tagging_button: Etichetarea terminată monitor_button: Monitorizare {option} add_quality_button: "Adăugați calitate: {quality}" +add_language_button: "Adăugați limba: {language}" add_path_button: Adaugă la {path} add_button: Adauga {kind}! already_added_button: Deja Adaugat! @@ -57,3 +60,9 @@ 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. 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. +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 1bbb91d..3bc61c5 100644 --- a/lang/ru-ru.yml +++ b/lang/ru-ru.yml @@ -4,6 +4,7 @@ language_label: Русский movie: фильм series: сериал season: сезон +book: книга title: название title_here: название сюда password: пароль @@ -25,6 +26,7 @@ convo_not_found: Я получил вашу команду, но я не узн search_canceled: Поиск отменён! no_root_folders: "Ошибка при добавлении {kind}: нет включенных корневых папок для {app}! Пожалуйста, проверьте вашу конфигурацию Searcharr и попробуйте еще раз." no_quality_profiles: "Ошибка при добавлении {kind}: нет включенных профилей качества для {app}! Пожалуйста, проверьте вашу конфигурацию Searcharr и попробуйте еще раз." +no_language_profiles: "Ошибка при добавлении {kind}: для {app} не включены языковые профили! Проверьте настройки Searcharr и повторите попытку." all_seasons: Все сезоны first_season: Только первый сезон latest_season: Только последний сезон @@ -42,6 +44,7 @@ add_tag_button: "Добавить тэг: {tag}" finished_tagging_button: Закончить тэггирование monitor_button: Отслеживать {option} add_quality_button: "Добавить качество: {quality}" +add_language_button: "Добавить язык: {language}" add_path_button: Добавить в {path} add_button: Добавить {kind}! already_added_button: Уже добавлен! @@ -57,3 +60,9 @@ help_sonarr: Используйте {series_commands} для добавлени help_radarr: Используйте {movie_commands} для добавления фильмов в Radarr. no_features: Извините, но все мои функции, на данный момент, отключены. admin_help: У вас есть права администратора, поэтому вы можете использовать {commands} для управления пользователям. +readarr_disabled: Извините, но поддержка книг отключена. +include_book_title_in_cmd: Добавьте в команду название книги, например {commands} +no_matching_books: Извините, но я не смог найти подходящие книги. +help_readarr: Используйте {book_commands}, для добавления сериалов в Readarr. +no_metadata_profiles: "Ошибка при добавлении {kind}: профили метаданных не включены для {app}! Пожалуйста, проверьте настройки Searcharr и повторите попытку." +add_metadata_button: "Добавить метаданные: {metadata}" \ No newline at end of file diff --git a/log.py b/log.py index bdabcc9..0f14ae1 100644 --- a/log.py +++ b/log.py @@ -1,6 +1,6 @@ """ Searcharr -Sonarr & Radarr Telegram Bot +Sonarr, Radarr & Readarr Telegram Bot Log Helper By Todd Roberts https://github.com/toddrob99/searcharr diff --git a/radarr.py b/radarr.py index c1c0431..223d4b4 100644 --- a/radarr.py +++ b/radarr.py @@ -1,6 +1,6 @@ """ Searcharr -Sonarr & Radarr Telegram Bot +Sonarr, Radarr & Readarr Telegram Bot Radarr API Wrapper By Todd Roberts https://github.com/toddrob99/searcharr @@ -156,18 +156,24 @@ def get_all_tags(self): self.logger.debug(f"Result of API call to get all tags: {r}") return [] if not r else r - def get_filtered_tags(self, allowed_tags): + def get_filtered_tags(self, allowed_tags, excluded_tags): r = self.get_all_tags() if not r: return [] elif allowed_tags == []: - return [x for x in r if not x["label"].startswith("searcharr-")] + return [ + x + for x in r + if not x["label"].startswith("searcharr-") + and not x["label"] in excluded_tags + ] else: return [ x for x in r if not x["label"].startswith("searcharr-") and (x["label"] in allowed_tags or x["id"] in allowed_tags) + and x["label"] not in excluded_tags ] def add_tag(self, tag): diff --git a/readarr.py b/readarr.py new file mode 100644 index 0000000..d0dc48c --- /dev/null +++ b/readarr.py @@ -0,0 +1,267 @@ +""" +Searcharr +Sonarr, Radarr & Readarr Telegram Bot +Readarr API Wrapper +By Ayman Bagabas +https://github.com/toddrob99/searcharr +""" +import requests +from urllib.parse import quote + +from log import set_up_logger + + +class Readarr(object): + def __init__(self, api_url, api_key, verbose=False): + self.logger = set_up_logger("searcharr.readarr", verbose, False) + self.logger.debug("Logging started!") + if api_url[-1] == "/": + api_url = api_url[:-1] + if api_url[:4] != "http": + self.logger.error( + "Invalid Readarr URL detected. Please update your settings to include http:// or https:// on the beginning of the URL." + ) + self.readarr_version = self.discover_version(api_url, api_key) + if not self.readarr_version.startswith("0."): + self.api_url = api_url + "/api/v1/{endpoint}?apikey=" + api_key + self._quality_profiles = self.get_all_quality_profiles() + self._metadata_profiles = self.get_all_metadata_profiles() + self._root_folders = self.get_root_folders() + + def discover_version(self, api_url, api_key): + try: + self.api_url = api_url + "/api/v1/{endpoint}?apikey=" + api_key + readarrInfo = self._api_get("system/status") + self.logger.debug( + f"Discovered Readarr version {readarrInfo.get('version')}. Using v1 api." + ) + return readarrInfo.get("version") + except requests.exceptions.HTTPError as e: + self.logger.debug(f"Readarr v1 API threw exception: {e}") + + try: + self.api_url = api_url + "/api/{endpoint}?apikey=" + api_key + readarrInfo = self._api_get("system/status") + self.logger.warning( + f"Discovered Readarr version {readarrInfo.get('version')}. Using legacy API. Consider upgrading to the latest version of Readarr for the best experience." + ) + return readarrInfo.get("version") + except requests.exceptions.HTTPError as e: + self.logger.debug(f"Readarr legacy API threw exception: {e}") + + self.logger.debug("Failed to discover Readarr version") + return None + + def lookup_book(self, title): + r = self._api_get("search", {"term": quote(title)}) + if not r: + return [] + + return [ + { + "title": x.get("book").get("title"), + "authorId": x.get("book").get("authorId"), + "authorTitle": x.get("book").get("authorTitle"), + "seriesTitle": x.get("book").get("seriesTitle"), + "disambiguation": x.get("book").get("disambiguation"), + "overview": x.get("book").get("overview", "No overview available."), + "remotePoster": x.get("book").get( + "remoteCover", + "https://artworks.thetvdb.com/banners/images/missing/movie.jpg", + ), + "releaseDate": x.get("book").get("releaseDate"), + "foreignBookId": x.get("book").get("foreignBookId"), + "id": x.get("book").get("id"), + "pageCount": x.get("book").get("pageCount"), + "titleSlug": x.get("book").get("titleSlug"), + "images": x.get("book").get("images"), + "links": x.get("book").get("links"), + "author": x.get("book").get("author"), + "editions": x.get("book").get("editions"), + } + for x in r + if x.get("book") + ] + + def add_book( + self, + book_info=None, + search=True, + monitored=True, + additional_data={}, + ): + if not book_info: + return False + + if not book_info: + book_info = self.lookup_book(book_info["title"]) + if len(book_info): + book_info = book_info[0] + else: + return False + + self.logger.debug(f"Additional data: {additional_data}") + + path = additional_data["p"] + quality = int(additional_data["q"]) + metadata = int(additional_data["m"]) + tags = additional_data.get("t", "") + if len(tags): + tag_ids = [int(x) for x in tags.split(",")] + else: + tag_ids = [] + + params = { + "title": book_info["title"], + "releaseDate": book_info["releaseDate"], + "foreignBookId": book_info["foreignBookId"], + "titleSlug": book_info["titleSlug"], + "monitored": monitored, + "anyEditionOk": True, + "addOptions": { + "searchForNewBook": False # manually searching below instead + }, + "editions": book_info["editions"], + "author": { + "qualityProfileId": quality, + "metadataProfileId": metadata, + "foreignAuthorId": book_info["author"]["foreignAuthorId"], + "rootFolderPath": path, + "tags": tag_ids, + }, + } + + rsp = self._api_post("book", params) + if rsp is not None and search: + # Force book search + srsp = self._api_post( + "command", {"name": "BookSearch", "bookIds": [rsp.get("id")]} + ) + self.logger.debug(f"Result of attempt to search book: {srsp}") + return rsp + + def get_root_folders(self): + r = self._api_get("rootfolder", {}) + if not r: + return [] + + return [ + { + "path": x.get("path"), + "freeSpace": x.get("freeSpace"), + "totalSpace": x.get("totalSpace"), + "id": x.get("id"), + } + for x in r + ] + + def _api_get(self, endpoint, params={}): + url = self.api_url.format(endpoint=endpoint) + for k, v in params.items(): + url += f"&{k}={v}" + self.logger.debug(f"Submitting GET request: [{url}]") + r = requests.get(url) + if r.status_code not in [200, 201, 202, 204]: + r.raise_for_status() + return None + else: + return r.json() + + def get_all_tags(self): + r = self._api_get("tag", {}) + self.logger.debug(f"Result of API call to get all tags: {r}") + return [] if not r else r + + def get_filtered_tags(self, allowed_tags, excluded_tags): + r = self.get_all_tags() + if not r: + return [] + elif allowed_tags == []: + return [ + x + for x in r + if not x["label"].startswith("searcharr-") + and not x["label"] in excluded_tags + ] + else: + return [ + x + for x in r + if not x["label"].startswith("searcharr-") + and (x["label"] in allowed_tags or x["id"] in allowed_tags) + and x["label"] not in excluded_tags + ] + + def add_tag(self, tag): + params = { + "label": tag, + } + t = self._api_post("tag", params) + self.logger.debug(f"Result of API call to add tag: {t}") + return t + + def get_tag_id(self, tag): + if i := next( + iter( + [ + x.get("id") + for x in self.get_all_tags() + if x.get("label").lower() == tag.lower() + ] + ), + None, + ): + self.logger.debug(f"Found tag id [{i}] for tag [{tag}]") + return i + else: + self.logger.debug(f"No tag id found for [{tag}]; adding...") + t = self.add_tag(tag) + if not isinstance(t, dict): + self.logger.error( + f"Wrong data type returned from Readarr API when attempting to add tag [{tag}]. Expected dict, got {type(t)}." + ) + return None + else: + self.logger.debug( + f"Created tag id for tag [{tag}]: {t['id']}" + if t.get("id") + else f"Could not add tag [{tag}]" + ) + return t.get("id", None) + + def lookup_quality_profile(self, v): + # Look up quality profile from a profile name or id + return next( + (x for x in self._quality_profiles if str(v) in [x["name"], str(x["id"])]), + None, + ) + + def get_all_quality_profiles(self): + return (self._api_get("qualityProfile", {})) or None + + def lookup_metadata_profile(self, v): + # Look up metadata profile from a profile name or id + return next( + (x for x in self._metadata_profiles if str(v) in [x["name"], str(x["id"])]), + None, + ) + + def get_all_metadata_profiles(self): + return (self._api_get("metadataprofile", {})) or None + + def lookup_root_folder(self, v): + # Look up root folder from a path or id + return next( + (x for x in self._root_folders if str(v) in [x["path"], str(x["id"])]), + None, + ) + + def _api_post(self, endpoint, params={}): + url = self.api_url.format(endpoint=endpoint) + self.logger.debug(f"Submitting POST request: [{url}]; params: [{params}]") + r = requests.post(url, json=params) + if r.status_code not in [200, 201, 202, 204]: + r.raise_for_status() + return None + else: + return r.json() diff --git a/requirements.txt b/requirements.txt index a4c09bc..eb64d0d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ argparse requests python-telegram-bot pyyaml +arrow diff --git a/searcharr.py b/searcharr.py index 76ce0c5..ff48302 100644 --- a/searcharr.py +++ b/searcharr.py @@ -1,6 +1,6 @@ """ Searcharr -Sonarr & Radarr Telegram Bot +Sonarr, Radarr & Readarr Telegram Bot By Todd Roberts https://github.com/toddrob99/searcharr """ @@ -12,6 +12,7 @@ from threading import Lock from urllib.parse import parse_qsl import uuid +from datetime import datetime from telegram import InlineKeyboardButton, InlineKeyboardMarkup, InputMediaPhoto from telegram.error import BadRequest @@ -20,9 +21,10 @@ from log import set_up_logger import radarr import sonarr +import readarr import settings -__version__ = "2.2" +__version__ = "3.0" DBPATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "data") DBFILE = "searcharr.db" @@ -257,6 +259,132 @@ def __init__(self, token): 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}]") + if hasattr(settings, "readarr_enabled"): + self.readarr = ( + readarr.Readarr( + settings.readarr_url, settings.readarr_api_key, args.verbose + ) + if settings.readarr_enabled + else None + ) + else: + settings.readarr_enabled = False + self.readarr = None + logger.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: + quality_profiles = [] + if not isinstance(settings.readarr_quality_profile_id, list): + settings.readarr_quality_profile_id = [ + settings.readarr_quality_profile_id + ] + for i in settings.readarr_quality_profile_id: + logger.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!") + else: + logger.debug( + f"Found Readarr quality profile for [{i}]: [{foundProfile}]" + ) + quality_profiles.append(foundProfile) + if not len(quality_profiles): + logger.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( + f"Using the following Readarr quality profile(s): {[(x['id'], x['name']) for x in quality_profiles]}" + ) + self.readarr._quality_profiles = quality_profiles + metadata_profiles = [] + if not isinstance(settings.readarr_metadata_profile_id, list): + settings.readarr_metadata_profile_id = [ + settings.readarr_metadata_profile_id + ] + for i in settings.readarr_metadata_profile_id: + logger.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!") + else: + logger.debug( + f"Found Readarr metadata profile for [{i}]: [{foundProfile}]" + ) + metadata_profiles.append(foundProfile) + if not len(metadata_profiles): + logger.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( + f"Using the following Readarr metadata profile(s): {[(x['id'], x['name']) for x in metadata_profiles]}" + ) + self.readarr._metadata_profiles = metadata_profiles + + root_folders = [] + if not hasattr(settings, "readarr_book_paths"): + settings.readarr_book_paths = [] + logger.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}]...") + foundPath = self.readarr.lookup_root_folder(i) + if not foundPath: + logger.error(f"Readarr root folder path/id [{i}] is invalid!") + else: + logger.debug(f"Found Readarr root folder for [{i}]: [{foundPath}]") + root_folders.append(foundPath) + if not len(root_folders): + logger.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( + 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( + "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_movie_command_aliases"): + settings.readarr_book_command_aliases = ["book"] + logger.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( + '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( + "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( + '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( + 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}]") self.conversations = {} if not hasattr(settings, "searcharr_admin_password"): @@ -266,7 +394,7 @@ def __init__(self, token): ) if settings.searcharr_password == "": logger.warning( - 'Password is blank. This will allow anyone to add series/movies using your bot. If this is unexpected, set a password in settings.py (searcharr_password="your password").' + '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"] @@ -326,6 +454,77 @@ def cmd_start(self, update, context): 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.radarr_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): @@ -579,7 +778,7 @@ def callback(self, update, context): # self.conversations.pop(cid) query.message.delete() elif op == "prev": - if convo["type"] in ["series", "movie"]: + if convo["type"] in ["series", "movie", "book"]: if i <= 0: query.answer() return @@ -628,7 +827,7 @@ def callback(self, update, context): reply_markup=reply_markup, ) elif op == "next": - if convo["type"] in ["series", "movie"]: + if convo["type"] in ["series", "movie", "book"]: if i >= len(convo["results"]): query.answer() return @@ -687,6 +886,8 @@ def callback(self, update, context): if convo["type"] == "series" else self.radarr._root_folders if convo["type"] == "movie" + else self.readarr._root_folders + if convo["type"] == "book" else [] ) if not additional_data.get("p"): @@ -737,7 +938,13 @@ def callback(self, update, context): self._xlate( "no_root_folders", kind=self._xlate(convo["type"]), - app="Sonarr" if convo["type"] == "series" else "Radarr", + app="Sonarr" + if convo["type"] == "series" + else "Radarr" + if convo["type"] == "movie" + else "Readarr" + if convo["type"] == "book" + else "???", ) ) query.message.delete() @@ -770,6 +977,8 @@ def callback(self, update, context): self.sonarr._quality_profiles if convo["type"] == "series" else self.radarr._quality_profiles + if convo["type"] == "movie" + else self.readarr._quality_profiles ) if len(quality_profiles) > 1: # prepare response to prompt user to select quality profile, and return @@ -819,7 +1028,72 @@ def callback(self, update, context): self._xlate( "no_quality_profiles", kind=self._xlate(convo["type"]), - app="Sonarr" if convo["type"] == "series" else "Radarr", + app="Sonarr" + if convo["type"] == "series" + else "Radarr" + if convo["type"] == "movie" + else "Readarr", + ) + ) + query.message.delete() + query.answer() + return + + if convo["type"] == "book" and not additional_data.get("m"): + metadata_profiles = self.readarr._metadata_profiles + if len(metadata_profiles) > 1: + # prepare response to prompt user to select quality profile, and return + reply_message, reply_markup = self._prepare_response( + convo["type"], + r, + cid, + i, + len(convo["results"]), + add=True, + metadata_profiles=metadata_profiles, + ) + try: + query.message.edit_media( + media=InputMediaPhoto(r["remotePoster"]), + 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..." + ) + query.message.edit_media( + media=InputMediaPhoto( + "https://artworks.thetvdb.com/banners/images/missing/movie.jpg" + ), + reply_markup=reply_markup, + ) + else: + raise + query.bot.edit_message_caption( + chat_id=query.message.chat_id, + message_id=query.message.message_id, + caption=reply_message, + reply_markup=reply_markup, + ) + query.answer() + return + elif len(metadata_profiles) == 1: + logger.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( + "no_metadata_profiles", + kind=self._xlate(convo["type"]), + app="Sonarr" + if convo["type"] == "series" + else "Radarr" + if convo["type"] == "movie" + else "Readarr", ) ) query.message.delete() @@ -876,20 +1150,29 @@ def callback(self, update, context): if convo["type"] == "series": all_tags = self.sonarr.get_filtered_tags( - settings.sonarr_user_selectable_tags + settings.sonarr_user_selectable_tags, + settings.sonarr_forced_tags, ) allow_user_to_select_tags = settings.sonarr_allow_user_to_select_tags forced_tags = settings.sonarr_forced_tags elif convo["type"] == "movie": all_tags = self.radarr.get_filtered_tags( - settings.radarr_user_selectable_tags + settings.radarr_user_selectable_tags, + settings.radarr_forced_tags, ) allow_user_to_select_tags = settings.radarr_allow_user_to_select_tags forced_tags = settings.radarr_forced_tags + elif convo["type"] == "book": + all_tags = self.readarr.get_filtered_tags( + settings.readarr_user_selectable_tags, + settings.readarr_forced_tags, + ) + allow_user_to_select_tags = settings.readarr_allow_user_to_select_tags + 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( - f"User tagging is enabled, but no tags found. Make sure there are tags in {'Sonarr' if convo['type'] == 'series' else 'Radarr'} matching your Searcharr configuration." + 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"): reply_message, reply_markup = self._prepare_response( @@ -950,6 +1233,9 @@ def callback(self, update, context): elif convo["type"] == "movie": get_tag_id = self.radarr.get_tag_id tag_with_username = settings.radarr_tag_with_username + elif convo["type"] == "book": + get_tag_id = self.readarr.get_tag_id + tag_with_username = settings.readarr_tag_with_username if tag_with_username: tag = f"searcharr-{query.from_user.username if query.from_user.username else query.from_user.id}" if tag_id := get_tag_id(tag): @@ -984,6 +1270,13 @@ def callback(self, update, context): min_avail=settings.radarr_min_availability, additional_data=self._get_add_data(cid), ) + elif convo["type"] == "book": + added = self.readarr.add_book( + book_info=r, + monitored=settings.readarr_add_monitored, + search=settings.readarr_search_on_add, + additional_data=self._get_add_data(cid), + ) else: added = False except Exception as e: @@ -1151,6 +1444,7 @@ def _prepare_response( add=False, paths=None, quality_profiles=None, + metadata_profiles=None, monitor_options=None, tags=None, ): @@ -1174,12 +1468,18 @@ def _prepare_response( "TMDB", url=f"https://www.themoviedb.org/movie/{r['tmdbId']}" ) ) - if r["imdbId"]: - keyboardNavRow.append( - InlineKeyboardButton( - "IMDb", url=f"https://imdb.com/title/{r['imdbId']}" + elif kind == "book" and r["links"]: + for link in r["links"]: + keyboardNavRow.append( + InlineKeyboardButton(link["name"], url=link["url"]) + ) + if kind == "series" or kind == "movie": + if r["imdbId"]: + keyboardNavRow.append( + InlineKeyboardButton( + "IMDb", url=f"https://imdb.com/title/{r['imdbId']}" + ) ) - ) if total_results > 1 and i < total_results - 1: keyboardNavRow.append( InlineKeyboardButton( @@ -1227,6 +1527,16 @@ def _prepare_response( ) ], ) + 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']}", + ) + ], + ) elif paths: for p in paths: keyboard.append( @@ -1282,6 +1592,16 @@ def _prepare_response( reply_message = f"{r['title']}{' (' + str(r['year']) + ')' if r['year'] and str(r['year']) not in r['title'] else ''}{' - ' + str(r['runtime']) + ' min' if r['runtime'] else ''} - {r['status'].title()}\n\n{r['overview']}"[ 0:1024 ] + elif kind == "book": + try: + release = datetime.strptime( + r["releaseDate"], "%Y-%m-%dT%H:%M:%SZ" + ).strftime("%b %d, %Y") + except (ValueError, TypeError): + release = "???" + reply_message = f"{r['author']['authorName']} - {r['title']}{' - ' + r['disambiguation'] if r['disambiguation'] else ''}{' - ' + r['seriesTitle'] if r['seriesTitle'] else ''} ({release})\n\n{r['overview']}"[ + 0:1024 + ] else: reply_message = self._xlate("unexpected_error") @@ -1378,12 +1698,29 @@ def cmd_help(self, update, context): ] ), ) - if settings.sonarr_enabled and settings.radarr_enabled: - resp = f"{sonarr_help} {radarr_help}" - elif settings.sonarr_enabled: - resp = sonarr_help - elif settings.radarr_enabled: - resp = radarr_help + 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") @@ -1417,6 +1754,10 @@ def run(self): 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)) diff --git a/settings-sample.py b/settings-sample.py index 5efa933..39c573a 100644 --- a/settings-sample.py +++ b/settings-sample.py @@ -1,6 +1,6 @@ """ Searcharr -Sonarr & Radarr Telegram Bot +Sonarr, Radarr & Readarr Telegram Bot By Todd Roberts https://github.com/toddrob99/searcharr """ @@ -45,3 +45,18 @@ radarr_min_availability = "released" # options: "announced", "inCinemas", "released" radarr_movie_command_aliases = ["movie"] # e.g. ["movie", "mv", "m"] radarr_movie_paths = [] # e.g. ["/movies", "/other-movies"] - can be full path or id value - leave empty to enable all + +# Readarr +readarr_enabled = True +readarr_url = "" # http://192.168.0.100:8787 +readarr_api_key = "" +readarr_quality_profile_id = ["eBook", "Spoken"] # can be name or id value - include multiple to allow the user to choose +readarr_metadata_profile_id = ["Standard"] # can be name or id value - include multiple to allow the user to choose +readarr_add_monitored = True +readarr_search_on_add = True +readarr_tag_with_username = True +readarr_forced_tags = [] # e.g. ["searcharr", "friends-and-family"] - leave empty for none +readarr_allow_user_to_select_tags = True +readarr_user_selectable_tags = [] # e.g. ["custom-tag-1", "custom-tag-2"] - leave empty to let user choose from all tags in Readarr +readarr_book_command_aliases = ["book"] # e.g. ["book", "bk", "b"] +readarr_book_paths = [] # e.g. ["/books", "/other-books"] - can be full path or id value - leave empty to enable all diff --git a/sonarr.py b/sonarr.py index 909d245..8540f63 100644 --- a/sonarr.py +++ b/sonarr.py @@ -1,6 +1,6 @@ """ Searcharr -Sonarr & Radarr Telegram Bot +Sonarr, Radarr & Readarr Telegram Bot Sonarr API Wrapper By Todd Roberts https://github.com/toddrob99/searcharr @@ -22,12 +22,38 @@ def __init__(self, api_url, api_key, verbose=False): self.logger.error( "Invalid Sonarr URL detected. Please update your settings to include http:// or https:// on the beginning of the URL." ) - self.api_url = api_url + "/api/{endpoint}?apikey=" + api_key + self.sonarr_version = self.discover_version(api_url, api_key) + if not self.sonarr_version.startswith("4."): + self.api_url = api_url + "/api/{endpoint}?apikey=" + api_key self._quality_profiles = self.get_all_quality_profiles() self._root_folders = self.get_root_folders() self._all_series = {} self.get_all_series() + def discover_version(self, api_url, api_key): + try: + self.api_url = api_url + "/api/v3/{endpoint}?apikey=" + api_key + sonarrInfo = self._api_get("system/status") + self.logger.debug( + f"Discovered Sonarr version {sonarrInfo.get('version')} using v3 api." + ) + return sonarrInfo.get("version") + except requests.exceptions.HTTPError as e: + self.logger.debug(f"Sonarr v3 API threw exception: {e}") + + try: + self.api_url = api_url + "/api/{endpoint}?apikey=" + api_key + sonarrInfo = self._api_get("system/status") + self.logger.warning( + f"Discovered Sonarr version {sonarrInfo.get('version')}. Using legacy API. Consider upgrading to the latest version of Radarr for the best experience." + ) + return sonarrInfo.get("version") + except requests.exceptions.HTTPError as e: + self.logger.debug(f"Sonarr legacy API threw exception: {e}") + + self.logger.debug("Failed to discover Sonarr version") + return None + def lookup_series(self, title=None, tvdb_id=None): r = self._api_get( "series/lookup", {"term": f"tvdb:{tvdb_id}" if tvdb_id else quote(title)} @@ -38,7 +64,7 @@ def lookup_series(self, title=None, tvdb_id=None): return [ { "title": x.get("title"), - "seasonCount": x.get("seasonCount", 0), + "seasonCount": len(x.get("seasons")), "status": x.get("status", "Unknown Status"), "overview": x.get("overview", "Overview not available."), "network": x.get("network"), @@ -141,7 +167,7 @@ def add_series( "tags": tag_ids, "addOptions": { "ignoreEpisodesWithFiles": unmonitor_existing, - "ignoreEpisodesWithoutFiles": "false", + "ignoreEpisodesWithoutFiles": False, "searchForMissingEpisodes": search, }, } @@ -168,18 +194,24 @@ def get_all_tags(self): self.logger.debug(f"Result of API call to get all tags: {r}") return [] if not r else r - def get_filtered_tags(self, allowed_tags): + def get_filtered_tags(self, allowed_tags, excluded_tags): r = self.get_all_tags() if not r: return [] elif allowed_tags == []: - return [x for x in r if not x["label"].startswith("searcharr-")] + return [ + x + for x in r + if not x["label"].startswith("searcharr-") + and not x["label"] in excluded_tags + ] else: return [ x for x in r if not x["label"].startswith("searcharr-") and (x["label"] in allowed_tags or x["id"] in allowed_tags) + and x["label"] not in excluded_tags ] def add_tag(self, tag): @@ -227,7 +259,7 @@ def lookup_quality_profile(self, v): ) def get_all_quality_profiles(self): - return self._api_get("profile", {}) or None + return self._api_get("qualityprofile", {}) or None def lookup_root_folder(self, v): # Look up root folder from a path or id