From a5e2aa0da1b3af445a102d1acd77a3a08017689a Mon Sep 17 00:00:00 2001 From: MasutaYunikon Date: Mon, 11 May 2026 18:26:31 +0200 Subject: [PATCH] fix(UI/UX): retour sur le modal d'import et refresh fix(rss): le rss est maintenant sur les ajout apres l'activation de la surveillance --- server/lib/rss-sync.ts | 115 +++++++-------------------- src/components/ManualImportModal.vue | 37 +++++++-- src/views/SerieView.vue | 2 +- 3 files changed, 60 insertions(+), 94 deletions(-) diff --git a/server/lib/rss-sync.ts b/server/lib/rss-sync.ts index 58942fa..d2c35c0 100644 --- a/server/lib/rss-sync.ts +++ b/server/lib/rss-sync.ts @@ -1,13 +1,10 @@ /** - * RSS Sync — auto-téléchargement des nouveaux épisodes pour les séries surveillées. + * RSS Sync — auto-téléchargement des épisodes sortis après l'activation de la surveillance. * - * Toutes les 6 heures, FanKarr vérifie si de nouveaux épisodes sont apparus - * APRÈS le dernier épisode organisé de chaque saison. - * - * Règle de frontière : si le dernier épisode organisé d'une saison est le 9, - * seuls les épisodes 10+ sont candidats au téléchargement automatique. - * Si aucun épisode n'est encore organisé dans une saison, rien n'est déclenché - * pour cette saison (l'utilisateur doit lancer le premier téléchargement manuellement). + * Toutes les 6 heures, FanKarr cherche les épisodes dont le champ `date_added` + * (date d'ajout dans la base metadata) est postérieur à la date d'activation + * du sync pour cette série. Si ces épisodes ont un torrent fankai et ne sont + * pas encore organisés, ils sont envoyés automatiquement au client torrent. */ import path from 'path' @@ -23,7 +20,7 @@ import { readSettings } from '../settings.js' export interface SyncedSerie { serieId : number serieName: string - addedAt : string // ISO + addedAt : string // ISO — date d'activation de la surveillance } type SyncedMap = Record @@ -60,7 +57,7 @@ export function isSynced(serieId: number): boolean { return !!loadSynced()[serieId] } -// ── Helpers organisés ───────────────────────────────────────── +// ── Helpers ─────────────────────────────────────────────────── function readOrganized(): Record> { try { @@ -70,20 +67,14 @@ function readOrganized(): Record> { return {} } -/** - * Retourne l'ensemble des IDs d'épisodes organisés pour une série donnée, - * en croisant organized.json avec les données de la série. - */ function getOrganizedEpisodeIds(sd: any, organized: Record>): Set { const ids = new Set() - // Import manuel const manual = organized['manual'] ?? {} for (const season of sd.seasons ?? []) for (const ep of season.episodes ?? []) if (manual[String(ep.id)]) ids.add(ep.id) - // Intégrales for (const t of sd.torrents ?? []) { const orgFiles = organized[t.infohash?.toLowerCase()] ?? {} for (const season of sd.seasons ?? []) @@ -91,7 +82,6 @@ function getOrganizedEpisodeIds(sd: any, organized: Record typeof p === 'object' && p.infohash?.toLowerCase() === t.infohash?.toLowerCase()) if (pathEntry) { const fname = pathEntry.path?.split('/').pop() @@ -115,31 +104,8 @@ function getOrganizedEpisodeIds(sd: any, organized: Record): number { - let max = 0 - for (const ep of season.episodes ?? []) { - if (organizedIds.has(ep.id) && ep.episode_number > max) - max = ep.episode_number - } - return max -} - // ── Logique de sync ─────────────────────────────────────────── -/** - * Lance un cycle de RSS sync. - * - * Par série surveillée : - * - Pour chaque saison, calcule la frontière = dernier épisode organisé. - * - Ne propose au téléchargement que les épisodes APRÈS cette frontière. - * - Si aucun épisode n'est organisé dans une saison → saison ignorée - * (l'utilisateur doit lancer le premier téléchargement manuellement). - * - Ne renvoie pas un torrent déjà actif dans le client. - */ export async function runRssSync(): Promise<{ sent: number; skipped: number; errors: number }> { const synced = loadSynced() const ids = Object.keys(synced).map(Number) @@ -150,7 +116,6 @@ export async function runRssSync(): Promise<{ sent: number; skipped: number; err const organized = readOrganized() const seriesData = await loadEnrichedSeriesData() - // Torrents actifs dans le client const activeHashes = new Set() try { const { category } = readSettings() @@ -166,71 +131,46 @@ export async function runRssSync(): Promise<{ sent: number; skipped: number; err let errors = 0 for (const serieId of ids) { - const sd = seriesData.find((s: any) => s.id === serieId) + const syncedEntry = synced[serieId] + const sd = seriesData.find((s: any) => s.id === serieId) if (!sd) { logger.warn('rss-sync', `Série ${serieId} introuvable dans le cache`) continue } - const title = sd.title ?? sd.show_title ?? String(serieId) + const title = sd.title ?? sd.show_title ?? String(serieId) + const activatedAt = new Date(syncedEntry.addedAt) const organizedIds = getOrganizedEpisodeIds(sd, organized) - if (organizedIds.size === 0) { - logger.info('rss-sync', `${title} — aucun épisode organisé, sync ignoré (lance le 1er téléchargement manuellement)`) - skipped++ - continue - } + logger.info('rss-sync', `Vérification : ${title} (sync activé le ${activatedAt.toLocaleDateString('fr-FR')})`) - logger.info('rss-sync', `Vérification : ${title} (${organizedIds.size} épisodes organisés)`) - - // Épisodes candidats = après la frontière, par saison - // Un épisode est candidat si : episode_number > max organisé dans cette saison + // Épisodes candidats : date_added > date d'activation ET pas encore organisés const candidateIds = new Set() for (const season of sd.seasons ?? []) { - const frontier = maxOrganizedEpNumber(season, organizedIds) - if (frontier === 0) continue // aucun épisode organisé dans cette saison → skip - for (const ep of season.episodes ?? []) { - if (ep.episode_number > frontier && !organizedIds.has(ep.id)) - candidateIds.add(ep.id) + if (!ep.date_added) continue + const epDate = new Date(ep.date_added) + if (epDate <= activatedAt) continue + if (organizedIds.has(ep.id)) continue + // Doit avoir au moins un torrent fankai + const hasTorrent = (ep.torrents ?? []).some((t: any) => t.fankai && (t.torrent_url || t.magnet)) + if (!hasTorrent) continue + candidateIds.add(ep.id) } - - if (candidateIds.size > 0) - logger.info('rss-sync', `${title} S${season.season_number} — frontière ep${frontier}, ${ - [...candidateIds].length} candidat(s)`) } if (candidateIds.size === 0) { - logger.info('rss-sync', `${title} — aucun nouvel épisode`) + logger.info('rss-sync', `${title} — aucun nouvel épisode depuis l'activation`) continue } - // Construire la liste des téléchargements (intégrale → pack saison → épisode) + logger.info('rss-sync', `${title} — ${candidateIds.size} nouvel(s) épisode(s) à télécharger`) + + // Construire les téléchargements : pack saison si couvre des candidats, sinon épisode par épisode const toDownload: { url: string; file_index?: number | null; file_path?: string | null; infohash?: string | null; label: string }[] = [] const coveredIds = new Set() - // 1. Intégrale(s) — uniquement si elle couvre au moins un candidat - for (const t of sd.torrents ?? []) { - if (!t.fankai) continue - const url = t.torrent_url ?? t.magnet - if (!url) continue - const hash = t.infohash?.toLowerCase() - if (hash && activeHashes.has(hash)) { skipped++; continue } - - const integraleEps: any[] = [] - for (const season of sd.seasons ?? []) - for (const ep of season.episodes ?? []) - if (ep.torrents?.some((et: any) => et.infohash?.toLowerCase() === hash)) - integraleEps.push(ep) - - const coversSomeCandidate = integraleEps.some(ep => candidateIds.has(ep.id)) - if (!coversSomeCandidate) { skipped++; continue } - - toDownload.push({ url, infohash: hash ?? null, label: `intégrale ${title}` }) - for (const ep of integraleEps) coveredIds.add(ep.id) - } - - // 2. Packs saison — uniquement si le pack couvre au moins un candidat non couvert + // 1. Packs saison couvrant au moins un candidat for (const season of sd.seasons ?? []) { for (const t of season.torrents ?? []) { if (!t.fankai) continue @@ -249,7 +189,7 @@ export async function runRssSync(): Promise<{ sent: number; skipped: number; err } } - // 3. Épisodes individuels candidats non encore couverts + // 2. Épisodes individuels candidats non couverts par un pack for (const season of sd.seasons ?? []) { for (const ep of season.episodes ?? []) { if (!candidateIds.has(ep.id) || coveredIds.has(ep.id)) continue @@ -275,7 +215,6 @@ export async function runRssSync(): Promise<{ sent: number; skipped: number; err } } - // Envoyer for (const dl of toDownload) { try { const results = await dispatchDownload(dl.url, { diff --git a/src/components/ManualImportModal.vue b/src/components/ManualImportModal.vue index 3d55a4f..1b95cf0 100644 --- a/src/components/ManualImportModal.vue +++ b/src/components/ManualImportModal.vue @@ -32,7 +32,7 @@ -