feat: ajout de la prise en charges des codex
This commit is contained in:
1 parent
7f6bbd6323
commit
83437def32
5 files changed
+101
-21
No files matched your search
@@ -43,6 +43,34 @@ export function extractTorrentsFromSerieData(sd: any): any[] {
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Dédoublonne une liste d'épisodes par episode_number.
|
||||
* Quand plusieurs variantes existent (ex. x264 + x265), on garde la première
|
||||
* comme épisode principal et on fusionne les torrents des autres dans sa liste.
|
||||
*/
|
||||
export function deduplicateEpisodes(episodes: any[]): any[] {
|
||||
const map = new Map<number, any>()
|
||||
for (const ep of episodes) {
|
||||
const key = ep.episode_number
|
||||
if (!map.has(key)) {
|
||||
map.set(key, { ...ep, torrents: [...(ep.torrents ?? [])] })
|
||||
} else {
|
||||
const primary = map.get(key)!
|
||||
const seenHashes = new Set(primary.torrents.map((t: any) => t.infohash).filter(Boolean))
|
||||
for (const t of ep.torrents ?? []) {
|
||||
if (!t.infohash || !seenHashes.has(t.infohash)) {
|
||||
primary.torrents.push(t)
|
||||
if (t.infohash) seenHashes.add(t.infohash)
|
||||
}
|
||||
}
|
||||
primary.torrent = primary.torrents[0] ?? null
|
||||
primary.available = primary.available || ep.available
|
||||
primary.organized = primary.organized || ep.organized
|
||||
}
|
||||
}
|
||||
return Array.from(map.values())
|
||||
}
|
||||
|
||||
export function buildResolvedEpisodes(sd: any, hash: string, seasonFilter?: number): any[] {
|
||||
const resolved: any[] = []
|
||||
const h = hash.toLowerCase()
|
||||
@@ -69,14 +97,19 @@ export function computeSerieDownloadState(serieData: any | null, organized: Reco
|
||||
|
||||
if (allTorrents.some(t => t.infohash && activeTorrents.has(t.infohash.toLowerCase()))) return 'downloading'
|
||||
|
||||
const allEpisodeIds = new Set<number>()
|
||||
// Dédoublonnage : épisodes uniques par (season_number, episode_number)
|
||||
// pour éviter de compter x264 + x265 comme deux épisodes distincts
|
||||
const uniqueEpKeys = new Set<string>()
|
||||
const epIdToKey = new Map<number, string>()
|
||||
for (const season of serieData.seasons ?? []) {
|
||||
for (const ep of season.episodes ?? []) {
|
||||
allEpisodeIds.add(ep.id)
|
||||
const key = `${season.season_number}:${ep.episode_number}`
|
||||
uniqueEpKeys.add(key)
|
||||
epIdToKey.set(ep.id, key)
|
||||
}
|
||||
}
|
||||
|
||||
const organizedEpisodeIds = new Set<number>()
|
||||
const organizedEpKeys = new Set<string>()
|
||||
|
||||
for (const t of allTorrents) {
|
||||
const hash = t.infohash?.toLowerCase()
|
||||
@@ -85,18 +118,24 @@ export function computeSerieDownloadState(serieData: any | null, organized: Reco
|
||||
if (Object.keys(orgFiles).length === 0) continue
|
||||
for (const ep of buildResolvedEpisodes(serieData, hash, t.season_number)) {
|
||||
const isOrganized = orgFiles[String(ep.episode_id)] !== undefined || orgFiles[ep.filename] !== undefined
|
||||
if (isOrganized) organizedEpisodeIds.add(ep.episode_id)
|
||||
if (isOrganized) {
|
||||
const key = epIdToKey.get(ep.episode_id)
|
||||
if (key) organizedEpKeys.add(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const manualOrg = organized['manual'] ?? {}
|
||||
for (const season of serieData.seasons ?? []) {
|
||||
for (const ep of season.episodes ?? []) {
|
||||
if (manualOrg[String(ep.id)]) organizedEpisodeIds.add(ep.id)
|
||||
if (manualOrg[String(ep.id)]) {
|
||||
const key = epIdToKey.get(ep.id)
|
||||
if (key) organizedEpKeys.add(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (allEpisodeIds.size === 0 || organizedEpisodeIds.size === 0) return 'none'
|
||||
if (organizedEpisodeIds.size >= allEpisodeIds.size) return 'complete'
|
||||
if (uniqueEpKeys.size === 0 || organizedEpKeys.size === 0) return 'none'
|
||||
if (organizedEpKeys.size >= uniqueEpKeys.size) return 'complete'
|
||||
return 'partial'
|
||||
}
|
||||
@@ -113,13 +113,15 @@ function buildFileMap(
|
||||
if (seasonFilter !== undefined && season.season_number !== seasonFilter) continue
|
||||
for (const ep of season.episodes ?? []) {
|
||||
const paths: any[] = ep.paths ?? []
|
||||
let matchedPath: string | null = null
|
||||
let matchedPath : string | null = null
|
||||
let matchedPathObj: any = null
|
||||
|
||||
for (const p of paths) {
|
||||
if (typeof p === 'string') {
|
||||
if (!matchedPath) matchedPath = p.replace(/\\/g, '/')
|
||||
} else if (p?.infohash?.toLowerCase() === h) {
|
||||
matchedPath = p.path.replace(/\\/g, '/')
|
||||
matchedPath = p.path.replace(/\\/g, '/')
|
||||
matchedPathObj = p
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -127,8 +129,12 @@ function buildFileMap(
|
||||
if (!matchedPath) continue
|
||||
const filename = matchedPath.split('/').pop() ?? matchedPath
|
||||
|
||||
const fmtName = ep.formatted_name?.trim()
|
||||
? ep.formatted_name.replace(/[<>:"/\\|?*]/g, '') + '.mkv'
|
||||
// Préférer le formatted_name du path (spécifique à ce torrent) sur celui de l'épisode
|
||||
const rawFmtName = matchedPathObj?.formatted_name?.trim()
|
||||
? matchedPathObj.formatted_name
|
||||
: ep.formatted_name
|
||||
const fmtName = rawFmtName?.trim()
|
||||
? rawFmtName.replace(/[<>:"/\\|?*]/g, '') + '.mkv'
|
||||
: null
|
||||
|
||||
if (!nfoSupport && !fmtName) {
|
||||
@@ -289,14 +295,23 @@ async function organizeTorrent(hash: string, name: string, savePath: string, ser
|
||||
const ep = torrent._episode
|
||||
const season = torrent._season
|
||||
const paths: any[] = ep.paths ?? []
|
||||
let filePath: string | null = null
|
||||
let filePath : string | null = null
|
||||
let matchedPathObj: any = null
|
||||
for (const p of paths) {
|
||||
if (typeof p === 'string') { if (!filePath) filePath = p.replace(/\\/g, '/') }
|
||||
else if (p?.infohash?.toLowerCase() === hash.toLowerCase()) { filePath = p.path.replace(/\\/g, '/'); break }
|
||||
else if (p?.infohash?.toLowerCase() === hash.toLowerCase()) {
|
||||
filePath = p.path.replace(/\\/g, '/')
|
||||
matchedPathObj = p
|
||||
break
|
||||
}
|
||||
}
|
||||
const filename = filePath?.split('/').pop() ?? name
|
||||
const fmtName = ep.formatted_name?.trim()
|
||||
? ep.formatted_name.replace(/[<>:"/\\|?*]/g, '') + '.mkv'
|
||||
// Préférer le formatted_name du path (spécifique à ce torrent) sur celui de l'épisode
|
||||
const rawFmtName = matchedPathObj?.formatted_name?.trim()
|
||||
? matchedPathObj.formatted_name
|
||||
: ep.formatted_name
|
||||
const fmtName = rawFmtName?.trim()
|
||||
? rawFmtName.replace(/[<>:"/\\|?*]/g, '') + '.mkv'
|
||||
: null
|
||||
|
||||
const srcExt = path.extname(filename)
|
||||
|
||||
+14
-2
@@ -101,9 +101,14 @@ export async function scanMediaPath(
|
||||
if (!srcFilename) continue
|
||||
idx(srcFilename, hash)
|
||||
if (ep.nfo_filename) idx(ep.nfo_filename, hash, ep.nfo_filename)
|
||||
// formatted_name spécifique à ce torrent (priorité sur celui de l'épisode)
|
||||
if (p.formatted_name?.trim()) {
|
||||
const fmtBase = p.formatted_name.replace(/[<>:"/\\|?*]/g, '').trim()
|
||||
idx(fmtBase, hash, fmtBase)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. original_filename, nfo_filename et formatted_name — indexés même si paths est vide
|
||||
// 2. original_filename, nfo_filename et formatted_name épisode — fallback
|
||||
const fallbackHash = ep.paths?.[0]?.infohash?.toLowerCase() ?? 'manual'
|
||||
if (ep.original_filename) idx(ep.original_filename, fallbackHash, ep.original_filename)
|
||||
if (ep.nfo_filename) idx(ep.nfo_filename, fallbackHash, ep.nfo_filename)
|
||||
@@ -362,9 +367,16 @@ export async function syncFilenameChanges(
|
||||
? path.basename(orgEntry.dest_path)
|
||||
: orgEntry.dest_filename
|
||||
const srcExt = path.extname(currentName)
|
||||
// Préférer le formatted_name du path correspondant au hash organisé
|
||||
const matchedPathEntry = (ep.paths ?? []).find((p: any) =>
|
||||
typeof p === 'object' && p.infohash?.toLowerCase() === orgHash
|
||||
)
|
||||
const fmtName = matchedPathEntry?.formatted_name?.trim()
|
||||
? matchedPathEntry.formatted_name
|
||||
: ep.formatted_name
|
||||
const expectedName: string = nfoSupport
|
||||
? (ep.nfo_filename ? ep.nfo_filename.replace(/\.[^.]+$/, '') + srcExt : currentName)
|
||||
: (ep.formatted_name?.trim() ? ep.formatted_name.replace(/[<>:"/\\|?*]/g, '').trim() + srcExt : currentName)
|
||||
: (fmtName?.trim() ? fmtName.replace(/[<>:"/\\|?*]/g, '').trim() + srcExt : currentName)
|
||||
|
||||
if (expectedName === currentName) continue
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { readAvailable, readInfohashMap, readSerieData } from '../lib/github-cac
|
||||
import {
|
||||
fankaiGet, normalizeSerie, normalizeSeason, normalizeEpisode,
|
||||
extractTorrentsFromSerieData, buildResolvedEpisodes, computeSerieDownloadState,
|
||||
deduplicateEpisodes,
|
||||
} from '../lib/serie-helpers.js'
|
||||
|
||||
const router = Router()
|
||||
@@ -99,7 +100,7 @@ router.get('/series/:id', requireAuth, async (req, res) => {
|
||||
for (const t of (ep.torrents ?? [])) {
|
||||
if (!episodeTorrentMap[ep.id]) episodeTorrentMap[ep.id] = []
|
||||
const pathEntry = (ep.paths ?? []).find((p: any) => typeof p === 'object' && p.infohash?.toLowerCase() === t.infohash?.toLowerCase())
|
||||
episodeTorrentMap[ep.id].push({ torrent_url: t.torrent_url, magnet: t.magnet, infohash: t.infohash?.toLowerCase() ?? null, type: 'episode', raw: t.title, torrent_name: t.torrent_name ?? null, manual: t.manual ?? false, fankai: t.fankai ?? true, file_index: pathEntry?.file_index ?? null, file_path: pathEntry?.path ?? null })
|
||||
episodeTorrentMap[ep.id].push({ torrent_url: t.torrent_url, magnet: t.magnet, infohash: t.infohash?.toLowerCase() ?? null, type: 'episode', raw: t.title, torrent_name: t.torrent_name ?? null, manual: t.manual ?? false, fankai: t.fankai ?? true, file_index: pathEntry?.file_index ?? null, file_path: pathEntry?.path ?? null, formatted_name: pathEntry?.formatted_name ?? ep.formatted_name ?? null })
|
||||
availableEpisodeIds.add(ep.id)
|
||||
const orgFiles = organized[t.infohash?.toLowerCase()] ?? {}
|
||||
const isOrg = orgFiles[String(ep.id)] !== undefined
|
||||
@@ -124,7 +125,7 @@ router.get('/series/:id', requireAuth, async (req, res) => {
|
||||
if (!pack) continue
|
||||
seenPackHashes.add(hashKey)
|
||||
if (!episodeTorrentMap[ep.id]) episodeTorrentMap[ep.id] = []
|
||||
episodeTorrentMap[ep.id].push({ torrent_url: pack.torrent_url, magnet: pack.magnet, infohash: hashKey, type: pack.type, raw: pack.raw, torrent_name: pack.torrent_name ?? null, manual: false, fankai: pack.fankai, file_index: pathEntry.file_index ?? null, file_path: pathEntry.path ?? null })
|
||||
episodeTorrentMap[ep.id].push({ torrent_url: pack.torrent_url, magnet: pack.magnet, infohash: hashKey, type: pack.type, raw: pack.raw, torrent_name: pack.torrent_name ?? null, manual: false, fankai: pack.fankai, file_index: pathEntry.file_index ?? null, file_path: pathEntry.path ?? null, formatted_name: pathEntry.formatted_name ?? ep.formatted_name ?? null })
|
||||
availableEpisodeIds.add(ep.id)
|
||||
}
|
||||
}
|
||||
@@ -133,7 +134,7 @@ router.get('/series/:id', requireAuth, async (req, res) => {
|
||||
}
|
||||
|
||||
const enrichedSeasons = seasonsWithEpisodes.map((season: any) => {
|
||||
const eps = season.episodes.map((ep: any) => {
|
||||
const rawEps = season.episodes.map((ep: any) => {
|
||||
const epTorrents = episodeTorrentMap[ep.id] ?? []
|
||||
const epTorrent = epTorrents[0] ?? null
|
||||
let fankai: boolean | null = epTorrent ? (epTorrent.fankai ?? true) : null
|
||||
@@ -160,6 +161,9 @@ router.get('/series/:id', requireAuth, async (req, res) => {
|
||||
organized: organizedEpisodeIds.has(ep.id),
|
||||
}
|
||||
})
|
||||
// Dédoublonnage : plusieurs variantes (x264/x265…) du même episode_number
|
||||
// sont fusionnées en un seul épisode avec la liste de torrents combinée
|
||||
const eps = deduplicateEpisodes(rawEps)
|
||||
const total = eps.filter((e: any) => e.available).length
|
||||
const orgCount = eps.filter((e: any) => e.organized).length
|
||||
const epTotal = eps.length
|
||||
|
||||
@@ -491,7 +491,17 @@ function epExpectedName(ep: any): string {
|
||||
if (props.nfoSupport) {
|
||||
return ep.nfo_filename ? ep.nfo_filename.replace(/\.[^.]+$/, '') + srcExt : entry.dest_filename
|
||||
} else {
|
||||
return ep.formatted_name?.trim() ? ep.formatted_name.replace(/[<>:"/\\|?*]/g, '').trim() + srcExt : entry.dest_filename
|
||||
// Préférer le formatted_name du torrent qui correspond au fichier importé.
|
||||
// On essaie de matcher par hash (si l'entrée organized l'expose), sinon on
|
||||
// cherche le premier torrent qui a un formatted_name.
|
||||
const usedHash = entry.hash?.toLowerCase() ?? null
|
||||
const matchedName = usedHash
|
||||
? (ep.torrents ?? []).find((t: any) => t.infohash === usedHash)?.formatted_name
|
||||
: null
|
||||
const name = matchedName
|
||||
?? (ep.torrents ?? []).find((t: any) => t.formatted_name)?.formatted_name
|
||||
?? ep.formatted_name
|
||||
return name?.trim() ? name.replace(/[<>:"/\\|?*]/g, '').trim() + srcExt : entry.dest_filename
|
||||
}
|
||||
}
|
||||
function epNeedsRename(ep: any): boolean {
|
||||
|
||||
Reference in new issue
Block a user