fix(series): résolution boutons de téléchargement saisons Yu-Gi-Oh et séries similaires

Les boutons "Saison entière" n'apparaissaient pas pour les séries dont les
season_id du scraper ne correspondent pas aux season_id de l'API Fankai.

- Ajouter seasonTorrentMapBySn indexé par season_number en fallback
- Utiliser season_number comme clé de secours dans enrichedSeasons

fix(catalogue): état "partiel" incorrect quand l'intégrale est organisée

Le badge partial s'affichait même quand tous les épisodes étaient organisés
via un pack_integrale, car les torrents d'épisodes séparés faisaient baisser
le ratio. La logique est maintenant basée sur les episode_id uniques.

feat(series-view): badge "Hors Fankai" sur les épisodes de torrents manuels

Les épisodes issus de MANUAL_TORRENTS (ex: MHA Film 4) sont maintenant
marqués d'un badge violet "Hors Fankai" avec tooltip explicatif.

- Propager manual: true du torrent dans le TorrentRef de l'épisode
- Afficher le badge dans SerieView.vue si ep.torrent?.manual

feat(series-view): tooltip avec le nom du torrent sur les boutons intégrale

Quand plusieurs intégrales existent pour une série, survoler le bouton
affiche le raw name du torrent via :title="t.raw".

feat(scraper): extraction du nom bencode pour les torrents locaux

Le champ raw des LOCAL_TORRENTS est maintenant renseigné depuis info.name
extrait directement du fichier .torrent via bencode, au lieu d'utiliser
la clé du dictionnaire LOCAL_TORRENTS.

- fetch_torrent_files_from_local retourne maintenant (files, infohash, name)
- torrent["raw"] est mis à jour avec le vrai nom bencode
This commit is contained in:
MasutaYunikon committed 2026-03-21 07:58:45 +01:00
1 parent b260208701
commit 13f9a27af8
2 files changed
+61 -39

No files matched your search

+30 -14
View File
@@ -142,22 +142,28 @@ function computeSerieDownloadState(
const hasActive = serieTorrents.some(t => activeTorrents.has(t.infohash?.toLowerCase()))
if (hasActive) return 'downloading'
let totalFiles = 0
let doneFiles = 0
// Collecter tous les épisodes uniques de la série (par episode_id)
const allEpisodeIds = new Set<number>()
const organizedEpisodeIds = new Set<number>()
for (const t of serieTorrents) {
const hash = t.infohash?.toLowerCase()
const orgFiles = organized[hash] ?? {}
const files: any[] = t.torrent_files ?? []
if (files.length === 0) {
totalFiles++
if (organized[t.infohash?.toLowerCase()]?.[t.raw]) doneFiles++
} else {
totalFiles += files.length
doneFiles += files.filter((f: any) => organized[t.infohash?.toLowerCase()]?.[f.filename]).length
for (const ep of t.resolved_episodes ?? []) {
allEpisodeIds.add(ep.episode_id)
// Cherche le fichier correspondant à cet épisode dans torrent_files
const epFile = files.find((f: any) => f.filename === ep.filename)
if (epFile ? orgFiles[epFile.filename] : Object.keys(orgFiles).length > 0) {
organizedEpisodeIds.add(ep.episode_id)
}
}
}
if (totalFiles === 0) return 'none'
if (doneFiles === 0) return 'none'
if (doneFiles >= totalFiles) return 'complete'
if (allEpisodeIds.size === 0) return 'none'
if (organizedEpisodeIds.size === 0) return 'none'
if (organizedEpisodeIds.size >= allEpisodeIds.size) return 'complete'
return 'partial'
}
@@ -242,12 +248,13 @@ app.get('/api/series/:id', requireAuth, async (req, res) => {
const availableBySnEn = new Set<string>() // "sn:en"
const episodeTorrentMap: Record<number, any> = {}
const episodeTorrentMapBySnEn: Record<string, any> = {}
const seasonTorrentMap : Record<number, any> = {}
const seasonTorrentMap : Record<number, any> = {} // keyed by season_id (scraper)
const seasonTorrentMapBySn: Record<number, any> = {} // keyed by season_number (fallback)
const integraleTorrents: any[] = []
const packEpisodesTorrents: any[] = []
for (const t of torrents) {
const ref = { torrent_url: t.torrent_url, magnet: t.magnet, type: t.type, raw: t.raw }
const ref = { torrent_url: t.torrent_url, magnet: t.magnet, type: t.type, raw: t.raw, manual: t.manual ?? false }
for (const ep of t.resolved_episodes ?? []) {
availableEpisodeIds.add(ep.episode_id)
const snEnKey = `${ep.season_number}:${ep.episode_number}`
@@ -264,6 +271,8 @@ app.get('/api/series/:id', requireAuth, async (req, res) => {
} else {
const sids = rs.length > 0 ? rs.map((r: any) => r.season_id) : [t.season_id]
for (const sid of sids) { if (sid) seasonTorrentMap[sid] = ref }
const sns = rs.length > 0 ? rs.map((r: any) => r.season_number) : [t.season_number]
for (const sn of sns) { if (sn != null) seasonTorrentMapBySn[sn] = ref }
}
} else if (t.type === 'pack_integrale') {
integraleTorrents.push(t)
@@ -272,6 +281,13 @@ app.get('/api/series/:id', requireAuth, async (req, res) => {
}
}
// ── DEBUG ──────────────────────────────────────────────
console.log(`[debug /api/series/${id}] serie="${serie.title}" allSeasonIds=`, [...allSeasonIds])
console.log(`[debug /api/series/${id}] seasonTorrentMap=`, seasonTorrentMap)
console.log(`[debug /api/series/${id}] seasons from API:`, seasonsWithEpisodes.map((s: any) => ({ id: s.id, season_number: s.season_number, title: s.title })))
console.log(`[debug /api/series/${id}] torrents matched:`, torrents.map(t => ({ raw: t.raw, type: t.type, season_id: t.season_id, resolved_seasons: t.resolved_seasons })))
// ── END DEBUG ──────────────────────────────────────────
let promotedIntegrale: any = null
if (integraleTorrents.length === 0 && allSeasonIds.size > 0) {
for (const t of torrents.filter(t => t.type === 'pack_saison')) {
@@ -349,7 +365,7 @@ app.get('/api/series/:id', requireAuth, async (req, res) => {
orgCount >= total && total > 0 ? 'complete' : 'partial'
return {
...season,
torrent : seasonTorrentMap[season.id] ?? null,
torrent : seasonTorrentMap[season.id] ?? seasonTorrentMapBySn[season.season_number] ?? null,
organized_state : seasonOrganizedState,
organized_count : orgCount,
episodes : eps,