fix: les fichiers non selectionner sur un torrent ne produise plus d'erreurs d'import

This commit is contained in:
MasutaYunikon committed 2026-04-21 23:49:38 +02:00
1 parent 245e9e38aa
commit f1c40865da
4 files changed
+86 -17

No files matched your search

+4 -2
View File
@@ -380,8 +380,10 @@ async function organizeTorrent(hash: string, name: string, savePath: string, ser
for (const c of candidates) { if (fs.existsSync(c)) { src = c; break } }
if (!src) {
error(`Fichier source introuvable : ${filename}`)
result.errors.push({ file: filename, error: 'Source introuvable' })
// Fichier absent du disque : pas encore téléchargé ou hors sélection (priorité 0).
// On skip silencieusement — pas une erreur.
debug(`Fichier non disponible (skip) : ${filename}`)
result.skipped++
continue
}
+3 -3
View File
@@ -86,7 +86,7 @@ router.get('/series/:id', requireAuth, async (req, res) => {
for (const season of serieData.seasons ?? []) {
for (const t of (season.torrents ?? [])) {
if (!seasonTorrentMapBySn[season.season_number]) seasonTorrentMapBySn[season.season_number] = []
seasonTorrentMapBySn[season.season_number].push({ torrent_url: t.torrent_url, magnet: t.magnet, type: 'pack_saison', raw: t.title, manual: t.manual ?? false })
seasonTorrentMapBySn[season.season_number].push({ torrent_url: t.torrent_url, magnet: t.magnet, infohash: t.infohash?.toLowerCase() ?? null, type: 'pack_saison', raw: t.title, manual: t.manual ?? false })
const resolved = buildResolvedEpisodes(serieData, t.infohash, season.season_number)
for (const ep of resolved) availableEpisodeIds.add(ep.episode_id)
const orgFiles = organized[t.infohash?.toLowerCase()] ?? {}
@@ -99,7 +99,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, type: 'episode', raw: t.title, 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, manual: t.manual ?? false, fankai: t.fankai ?? true, file_index: pathEntry?.file_index ?? null, file_path: pathEntry?.path ?? null })
availableEpisodeIds.add(ep.id)
const orgFiles = organized[t.infohash?.toLowerCase()] ?? {}
const isOrg = orgFiles[String(ep.id)] !== undefined
@@ -124,7 +124,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, type: pack.type, raw: pack.raw, 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, manual: false, fankai: pack.fankai, file_index: pathEntry.file_index ?? null, file_path: pathEntry.path ?? null })
availableEpisodeIds.add(ep.id)
}
}
+16 -10
View File
@@ -129,7 +129,7 @@
<button
class="w-8 h-8 rounded-lg border flex items-center justify-center transition-colors"
:class="epBtnClass(ep)"
:disabled="ep.organized || (!ep.torrents?.length && !ep.torrent)"
:disabled="epState(ep) !== 'idle'"
@click="handleEpBtnClick(ep)"
>
<component :is="epStateIcon(ep)" />
@@ -230,6 +230,8 @@ const checkIcon = h(Check, { size: 12 })
// ── Helpers hash / progress ────────────────────────────────────
function extractHash(torrent: any): string | null {
// Préférer l'infohash direct (présent même si le magnet est absent)
if (torrent?.infohash) return torrent.infohash.toLowerCase()
if (!torrent?.magnet) return null
const m = torrent.magnet.match(/xt=urn:btih:([a-fA-F0-9]{40})/i)
return m ? m[1].toLowerCase() : null
@@ -317,15 +319,17 @@ function epProgress(ep: any): ActiveTorrent | null {
if (t.file_index != null) {
const sessionDl = isDownloaded(key0) || isDownloaded(keyI)
// Si le client remonte la progression par fichier, on s'en sert en priorité :
// affiche dès que le fichier a commencé (progress > 0) ou téléchargé cette session.
if (active.files && active.files.length > 0) {
const file = active.files.find(f => f.index === t.file_index)
if (file != null && (sessionDl || file.progress > 0)) return { ...active, progress: file.progress }
// Afficher si : téléchargé cette session, fichier en cours (progress > 0), ou torrent terminé
if (file != null && (sessionDl || file.progress > 0 || active.state === 'seeding')) {
return { ...active, progress: file.progress }
}
if (!sessionDl) continue
} else if (!sessionDl) {
// Pas de données fichier → afficher uniquement si demandé cette session
continue
} else {
// Pas de données par fichier : session ou torrent terminé
if (!sessionDl && active.state !== 'seeding') continue
return { ...active, progress: active.state === 'seeding' ? 100 : active.progress }
}
}
return active
@@ -348,11 +352,13 @@ function handleEpBtnClick(ep: any) {
}
function epState(ep: any): 'idle' | 'loading' | 'done' | 'unavailable' {
if (ep.organized || epAnyDownloaded(ep) || isAlreadyQueued(ep.torrent)) return 'done'
// Vert uniquement quand importé dans la bibliothèque
if (ep.organized) return 'done'
// Spinner si : envoyé au client, déjà en cours dans le client, ou fichier en cours/terminé
if (epAnyDownloaded(ep) || epAnyDownloading(ep) || isAlreadyQueued(ep.torrent)) return 'loading'
const prog = epProgress(ep)
if (prog && prog.progress < 100) return 'loading'
if (prog) return 'loading'
if (!ep.torrent || !ep.available) return 'unavailable'
if (epAnyDownloading(ep)) return 'loading'
return 'idle'
}
function epStateIcon(ep: any) {
+63 -2
View File
@@ -163,10 +163,15 @@ const unimportMenuOpen = ref(false)
const downloadingAll = ref(false)
const downloadingSeason = ref<Record<number, boolean>>({})
export interface ActiveTorrent { hash: string; progress: number; state: string; files?: { index: number; progress: number }[] }
export interface ActiveTorrent { hash: string; progress: number; state: string; files?: { index: number; progress: number }[]; save_path?: string; name?: string }
const activeTorrents = ref<ActiveTorrent[]>([])
let pollTimer: ReturnType<typeof setInterval> | null = null
// Suivi des états précédents pour détecter les transitions (download → seeding)
const _prevTorrentStates = new Map<string, string>() // hash → state
const _prevFileProgresses = new Map<string, number>() // hash:index → progress
const _recentlyOrganized = new Set<string>() // hash:fileIndex ou hash seul
const data = computed(() => store.currentSerie)
// ── Helpers intégrale ─────────────────────────────────────────
@@ -187,6 +192,7 @@ function addDownloading(key: string) { if (!downloading.value.includes(key)) dow
function removeDownloading(key: string) { downloading.value = downloading.value.filter(k => k !== key) }
function addDownloaded(key: string) { if (!downloaded.value.includes(key)) downloaded.value.push(key) }
function extractHash(torrent: any): string | null {
if (torrent?.infohash) return torrent.infohash.toLowerCase()
if (!torrent?.magnet) return null
const m = torrent.magnet.match(/xt=urn:btih:([a-fA-F0-9]{40})/i)
return m ? m[1].toLowerCase() : null
@@ -222,7 +228,62 @@ async function fetchActiveDownloads() {
const res = await fetch('/api/downloads', { credentials: 'include' })
if (!res.ok) return
const list: any[] = await res.json()
activeTorrents.value = list.map(t => ({ hash: t.hash, progress: t.progress ?? 0, state: t.state, files: t.files }))
const isFirstPoll = _prevTorrentStates.size === 0
for (const t of list) {
const prevState = _prevTorrentStates.get(t.hash)
// Détecter fichier individuel terminé (progression par fichier)
if (t.files) {
for (const f of t.files) {
const key = `${t.hash}:${f.index}`
const prevProg = _prevFileProgresses.get(key)
_prevFileProgresses.set(key, f.progress)
if (!isFirstPoll && prevProg !== undefined && prevProg < 100 && f.progress === 100 && !_recentlyOrganized.has(key)) {
_recentlyOrganized.add(key)
triggerOrganize(t)
}
}
}
// Détecter torrent entier passé en seeding (épisodes standalone sans per-file data)
if (!isFirstPoll && t.state === 'seeding' && prevState !== undefined && prevState !== 'seeding' && !_recentlyOrganized.has(t.hash)) {
_recentlyOrganized.add(t.hash)
triggerOrganize(t)
}
_prevTorrentStates.set(t.hash, t.state)
}
activeTorrents.value = list.map(t => ({
hash : t.hash,
progress : t.progress ?? 0,
state : t.state,
files : t.files,
save_path: t.save_path,
name : t.name,
}))
} catch {}
}
async function triggerOrganize(torrent: any) {
if (!torrent?.hash || !torrent?.name || !torrent?.save_path) return
try {
const res = await fetch('/api/organize', {
method : 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ hash: torrent.hash, name: torrent.name, save_path: torrent.save_path }),
})
if (res.ok) {
const result = await res.json()
if (result.done > 0) {
toast(`${result.done} épisode(s) importé(s) automatiquement ✓`, 'success')
await fetchOrganized()
load()
}
}
} catch {}
}