feat: add download detection job with Prowlarr integration

For each series with missing volumes and an approved metadata link,
calls Prowlarr to find available matching releases and stores them in
a report (no auto-download). Includes per-series detail page, Telegram
notifications with per-event toggles, and stats display in the jobs table.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-25 13:47:29 +01:00
parent e5e4993e7b
commit d2c9f28227
15 changed files with 1033 additions and 13 deletions

View File

@@ -266,6 +266,9 @@ const en: Record<TranslationKey, string> = {
"jobs.matchReadingStatusShort": "Auto-link unmatched series to the reading status provider",
"jobs.pushReadingStatus": "Push reading statuses",
"jobs.pushReadingStatusShort": "Push changed reading statuses to AniList (differential push)",
"jobs.groupProwlarr": "Download",
"jobs.downloadDetection": "Download detection",
"jobs.downloadDetectionShort": "Search Prowlarr for available releases matching missing volumes",
// Jobs list
"jobsList.id": "ID",
@@ -290,6 +293,7 @@ const en: Record<TranslationKey, string> = {
"jobRow.seriesTotal": "{{count}} series total",
"jobRow.seriesLinked": "{{count}} series linked",
"jobRow.seriesPushed": "{{count}} series pushed",
"jobRow.downloadFound": "{{count}} releases found",
"jobRow.errors": "{{count}} errors",
"jobRow.view": "View",
"jobRow.replay": "Replay",
@@ -381,6 +385,16 @@ const en: Record<TranslationKey, string> = {
"jobDetail.pushed": "Pushed",
"jobDetail.skipped": "Skipped",
"jobDetail.noBooks": "No books",
"jobDetail.downloadDetection": "Download detection",
"jobDetail.downloadDetectionDesc": "Scanning series with missing volumes via Prowlarr",
"jobDetail.downloadDetectionReport": "Detection report",
"jobDetail.downloadFound": "Available",
"jobDetail.downloadNotFound": "Not found",
"jobDetail.downloadNoMissing": "Complete",
"jobDetail.downloadNoMetadata": "No metadata",
"jobDetail.downloadAvailableReleases": "Available releases",
"jobDetail.downloadAvailableReleasesDesc": "{{count}} series with at least one release found",
"jobDetail.downloadMissingCount": "{{count}} missing",
// Job types
"jobType.rebuild": "Indexing",
@@ -413,6 +427,9 @@ const en: Record<TranslationKey, string> = {
"jobType.reading_status_push": "Reading status push",
"jobType.reading_status_pushLabel": "Reading status push",
"jobType.reading_status_pushDesc": "Differentially pushes changed reading statuses (or new series) to AniList.",
"jobType.download_detection": "Download detection",
"jobType.download_detectionLabel": "Available downloads detection",
"jobType.download_detectionDesc": "Scans series with missing volumes and queries Prowlarr to find available releases. Downloads nothing — produces a report of opportunities only.",
// Status badges
"statusBadge.extracting_pages": "Extracting pages",
@@ -647,6 +664,12 @@ const en: Record<TranslationKey, string> = {
"settings.eventBatchFailed": "Batch failed",
"settings.eventRefreshCompleted": "Refresh completed",
"settings.eventRefreshFailed": "Refresh failed",
"settings.eventCategoryReadingStatus": "Reading status",
"settings.eventMatchCompleted": "Sync completed",
"settings.eventMatchFailed": "Sync failed",
"settings.eventPushCompleted": "Push completed",
"settings.eventPushFailed": "Push failed",
"settings.eventCategoryDownloadDetection": "Download detection",
"settings.telegramHelp": "How to get the required information?",
"settings.telegramHelpBot": "Open Telegram, search for <b>@BotFather</b>, send <code>/newbot</code> and follow the instructions. Copy the token it gives you.",
"settings.telegramHelpChat": "Send a message to your bot, then open <code>https://api.telegram.org/bot&lt;TOKEN&gt;/getUpdates</code> in your browser. The <b>chat id</b> is in <code>message.chat.id</code>.",

View File

@@ -264,6 +264,9 @@ const fr = {
"jobs.matchReadingStatusShort": "Lier automatiquement les séries non associées au provider",
"jobs.pushReadingStatus": "Push des états de lecture",
"jobs.pushReadingStatusShort": "Envoyer les états de lecture modifiés vers AniList (push différentiel)",
"jobs.groupProwlarr": "Téléchargement",
"jobs.downloadDetection": "Détection de téléchargements",
"jobs.downloadDetectionShort": "Cherche sur Prowlarr les releases disponibles pour les volumes manquants",
// Jobs list
"jobsList.id": "ID",
@@ -288,6 +291,7 @@ const fr = {
"jobRow.seriesTotal": "{{count}} séries au total",
"jobRow.seriesLinked": "{{count}} séries liées",
"jobRow.seriesPushed": "{{count}} séries synchronisées",
"jobRow.downloadFound": "{{count}} releases trouvées",
"jobRow.errors": "{{count}} erreurs",
"jobRow.view": "Voir",
"jobRow.replay": "Rejouer",
@@ -379,6 +383,16 @@ const fr = {
"jobDetail.pushed": "Envoyés",
"jobDetail.skipped": "Ignorés",
"jobDetail.noBooks": "Sans livres",
"jobDetail.downloadDetection": "Détection de téléchargements",
"jobDetail.downloadDetectionDesc": "Analyse des séries avec volumes manquants via Prowlarr",
"jobDetail.downloadDetectionReport": "Rapport de détection",
"jobDetail.downloadFound": "Disponibles",
"jobDetail.downloadNotFound": "Non trouvés",
"jobDetail.downloadNoMissing": "Complets",
"jobDetail.downloadNoMetadata": "Sans métadonnées",
"jobDetail.downloadAvailableReleases": "Releases disponibles",
"jobDetail.downloadAvailableReleasesDesc": "{{count}} série(s) avec au moins une release trouvée",
"jobDetail.downloadMissingCount": "{{count}} manquant(s)",
// Job types
"jobType.rebuild": "Indexation",
@@ -411,6 +425,9 @@ const fr = {
"jobType.reading_status_push": "Push statut lecture",
"jobType.reading_status_pushLabel": "Push des états de lecture",
"jobType.reading_status_pushDesc": "Envoie les états de lecture modifiés (ou nouvelles séries) vers AniList de façon différentielle.",
"jobType.download_detection": "Détection téléchargements",
"jobType.download_detectionLabel": "Détection de téléchargements disponibles",
"jobType.download_detectionDesc": "Analyse les séries avec des volumes manquants et interroge Prowlarr pour trouver les releases disponibles. Ne télécharge rien — produit uniquement un rapport des opportunités.",
// Status badges
"statusBadge.extracting_pages": "Extraction des pages",
@@ -645,6 +662,12 @@ const fr = {
"settings.eventBatchFailed": "Batch échoué",
"settings.eventRefreshCompleted": "Rafraîchissement terminé",
"settings.eventRefreshFailed": "Rafraîchissement échoué",
"settings.eventCategoryReadingStatus": "État de lecture",
"settings.eventMatchCompleted": "Synchro. terminée",
"settings.eventMatchFailed": "Synchro. échouée",
"settings.eventPushCompleted": "Push terminé",
"settings.eventPushFailed": "Push échoué",
"settings.eventCategoryDownloadDetection": "Détection téléchargements",
"settings.telegramHelp": "Comment obtenir les informations ?",
"settings.telegramHelpBot": "Ouvrez Telegram, recherchez <b>@BotFather</b>, envoyez <code>/newbot</code> et suivez les instructions. Copiez le token fourni.",
"settings.telegramHelpChat": "Envoyez un message à votre bot, puis ouvrez <code>https://api.telegram.org/bot&lt;TOKEN&gt;/getUpdates</code> dans votre navigateur. Le <b>chat id</b> apparaît dans <code>message.chat.id</code>.",