fix(surveillance): détection des nouveaux épisodes par la date (#152)

This commit is contained in:
ElPouki authored and GitHub committed 2026-09-21 14:23:38 +02:00
1 parent e8dc76e94a
commit ddc6d665b6
1 file changed
+76 -57
+76 -57
View File
@@ -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<string, Record<string
return ids
}
// ── Torrents publiés ──────────────────────────────────────────
// pub_date nyaa : « AAAA-MM-JJ HH:MM » en UTC
function pubTime(t: any): number {
return typeof t.pub_date === 'string' ? Date.parse(`${t.pub_date.replace(' ', 'T')}Z`) : NaN
}
interface ReleaseEpisode { key: string; name: string; pathEntry: any }
interface Release { torrent: any; episodes: ReleaseEpisode[] }
// Torrents Fankai de la série et épisodes qu'ils contiennent
function listReleases(sd: any): Release[] {
const byHash = new Map<string, Release>()
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<string>([...(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<number>()
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<string>()
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<string, number>()
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<number>()
const toDownload: { url: string; magnet: string | null; file_index: number | null; file_path: string | null; infohash: string; label: string }[] = []
const covered = new Set<string>()
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}`)