diff --git a/server/lib/rss-sync.ts b/server/lib/rss-sync.ts index 95b49b2..bc167ec 100644 --- a/server/lib/rss-sync.ts +++ b/server/lib/rss-sync.ts @@ -1,4 +1,4 @@ -/** Surveillance : envoie au client les épisodes ajoutés au catalogue après activation (toutes les 6 h). */ +/** Surveillance : envoie au client les épisodes dont le torrent est publié après activation (toutes les 6 h). */ import path from 'path' import fs from 'fs' @@ -136,6 +136,44 @@ function getOrganizedEpisodeIds(sd: any, organized: Record() + const add = (t: any) => { + const hash = t.infohash?.toLowerCase() + if (t.fankai && hash && (t.torrent_url || t.magnet) && !byHash.has(hash)) byHash.set(hash, { torrent: t, episodes: [] }) + } + for (const t of sd.torrents ?? []) add(t) + for (const season of sd.seasons ?? []) { + for (const t of season.torrents ?? []) add(t) + for (const ep of season.episodes ?? []) for (const t of ep.torrents ?? []) add(t) + } + + for (const season of sd.seasons ?? []) { + for (const ep of season.episodes ?? []) { + const key = `${season.season_number}:${ep.episode_number}` + const name = `S${String(season.season_number).padStart(2, '0')}E${String(ep.episode_number).padStart(2, '0')}` + const hashes = new Set([...(ep.paths ?? []), ...(ep.torrents ?? [])].map((x: any) => x?.infohash?.toLowerCase()).filter(Boolean)) + for (const hash of hashes) { + const release = byHash.get(hash) + if (!release || release.episodes.some(e => e.key === key)) continue + release.episodes.push({ key, name, pathEntry: (ep.paths ?? []).find((p: any) => p?.infohash?.toLowerCase() === hash) ?? null }) + } + } + } + return [...byHash.values()] +} + // ── Vérification des séries surveillées ─────────────────────── export async function runRssSync(): Promise<{ sent: number; skipped: number; errors: number }> { @@ -172,76 +210,56 @@ export async function runRssSync(): Promise<{ sent: number; skipped: number; err 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) + const releases = listReleases(sd) logger.info('rss-sync', `Vérification de « ${title} » (surveillée depuis le ${activatedAt.toLocaleDateString('fr-FR')})`) - const candidateIds = new Set() - for (const season of sd.seasons ?? []) { - for (const ep of season.episodes ?? []) { - if (!ep.date_added) continue - const epDate = new Date(ep.date_added) - if (epDate <= activatedAt) continue - if (organizedIds.has(ep.id)) continue - const hasTorrent = (ep.torrents ?? []).some((t: any) => t.fankai && (t.torrent_url || t.magnet)) - if (!hasTorrent) continue - candidateIds.add(ep.id) - } - } + // Clé saison:épisode : les variantes x264/x265 d'un même épisode comptent pour un + const organizedKeys = new Set() + for (const season of sd.seasons ?? []) + for (const ep of season.episodes ?? []) + if (organizedIds.has(ep.id)) organizedKeys.add(`${season.season_number}:${ep.episode_number}`) - if (candidateIds.size === 0) { + // Nouvel épisode + const firstPub = new Map() + for (const { torrent, episodes } of releases) { + const time = pubTime(torrent) + for (const { key } of episodes) + firstPub.set(key, Math.min(firstPub.get(key) ?? Infinity, Number.isNaN(time) ? -Infinity : time)) + } + const candidates = new Set([...firstPub].filter(([key, time]) => time > activatedAt.getTime() && !organizedKeys.has(key)).map(([key]) => key)) + + if (candidates.size === 0) { logger.info('rss-sync', `« ${title} » : aucun nouvel épisode depuis l'activation`) continue } - logger.info('rss-sync', `« ${title} » : ${candidateIds.size} épisode(s) à télécharger`) + logger.info('rss-sync', `« ${title} » : ${candidates.size} épisode(s) à télécharger`) - const toDownload: { url: string; file_index?: number | null; file_path?: string | null; infohash?: string | null; label: string }[] = [] - const coveredIds = new Set() + const toDownload: { url: string; magnet: string | null; file_index: number | null; file_path: string | null; infohash: string; label: string }[] = [] + const covered = new Set() + const missing = (r: Release) => r.episodes.filter(e => candidates.has(e.key) && !covered.has(e.key)) - for (const season of sd.seasons ?? []) { - for (const t of season.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 } + for (const release of [...releases].sort((a, b) => missing(b).length - missing(a).length)) { + const eps = missing(release) + if (eps.length === 0) continue + for (const e of eps) covered.add(e.key) - const coversSomeCandidate = (season.episodes ?? []).some((ep: any) => - candidateIds.has(ep.id) && !coveredIds.has(ep.id) - ) - if (!coversSomeCandidate) { skipped++; continue } + const hash = release.torrent.infohash.toLowerCase() + if (activeHashes.has(hash)) { skipped++; continue } - toDownload.push({ url, infohash: hash ?? null, label: `S${season.season_number} ${title}` }) - for (const ep of season.episodes ?? []) coveredIds.add(ep.id) - } - } - - for (const season of sd.seasons ?? []) { - for (const ep of season.episodes ?? []) { - if (!candidateIds.has(ep.id) || coveredIds.has(ep.id)) continue - - for (const t of ep.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 pathEntry = (ep.paths ?? []).find((p: any) => typeof p === 'object' && p.infohash?.toLowerCase() === hash) - toDownload.push({ - url, - infohash : hash ?? null, - file_index: pathEntry?.file_index ?? null, - file_path : pathEntry?.path ?? null, - label : `S${season.season_number}E${ep.episode_number} ${title}`, - }) - coveredIds.add(ep.id) - break - } - } + const single = eps.length === 1 ? eps[0] : null + toDownload.push({ + url : release.torrent.torrent_url ?? release.torrent.magnet, + magnet : release.torrent.magnet ?? null, + infohash : hash, + file_index: single?.pathEntry?.file_index ?? null, + file_path : single?.pathEntry?.path ?? null, + label : `« ${title} » ${single ? single.name : `(${eps.length} épisodes)`}`, + }) } for (const dl of toDownload) { @@ -250,6 +268,7 @@ export async function runRssSync(): Promise<{ sent: number; skipped: number; err file_index: dl.file_index, file_path : dl.file_path, infohash : dl.infohash, + magnet : dl.magnet, }) if (results.some((r: any) => r.ok)) { logger.info('rss-sync', `Envoyé au client : ${dl.label}`)