From cb28baf25d7f24148ed7e13a0a4ea46f085b2ba4 Mon Sep 17 00:00:00 2001 From: MasutaYunikon Date: Sun, 10 May 2026 19:20:01 +0200 Subject: [PATCH] =?UTF-8?q?feat(UI/UX):=20affichage=20de=20la=20serie=20et?= =?UTF-8?q?=20des=20episodes=20sur=20la=20page=20download=20du=20torrent?= =?UTF-8?q?=20fix:=20les=20torrents=20n'utilise=20plus=20la=20pause=20feat?= =?UTF-8?q?(log):=20+=20de=20logs=20sur=20les=20erreurs=20d'import=20(chem?= =?UTF-8?q?in=20tent=C3=A9=20et=20fichier=20du=20torrent=20chercher)=20fix?= =?UTF-8?q?(docker):=20ajout=20du=20fichier=20de=20versions=20pour=20la=20?= =?UTF-8?q?navbar=20dans=20docker=20lors=20du=20build=20fix(import):=20les?= =?UTF-8?q?=20imports=20se=20font=20maintenant=20sur=20les=20series=20sans?= =?UTF-8?q?=20torrents=20qui=20n'avais=20pas=20de=20fichier=20de=20catalog?= =?UTF-8?q?ue=20depuis=20le=20scraper=20fix(UI/UX):=20retour=20visuel=20lo?= =?UTF-8?q?rs=20de=20l'import=20d'une=20serie/episode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/containerize.yml | 4 +- Dockerfile | 2 + server/organize-worker.ts | 13 +++--- server/routes/downloads.ts | 12 +++++- server/routes/import.ts | 8 +++- server/torrent-clients/qbittorrent.ts | 40 ++++--------------- src/components/downloads/TorrentCard.vue | 50 ++++++++++++++++++++++-- 7 files changed, 82 insertions(+), 47 deletions(-) diff --git a/.github/workflows/containerize.yml b/.github/workflows/containerize.yml index b5b858b..2d3a13f 100644 --- a/.github/workflows/containerize.yml +++ b/.github/workflows/containerize.yml @@ -37,4 +37,6 @@ jobs: push: true platforms: linux/amd64,linux/arm64,linux/arm/v7 tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} \ No newline at end of file + labels: ${{ steps.meta.outputs.labels }} + build-args: | + BUILD_VERSION=${{ steps.meta.outputs.version }} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 3830c71..66d95c1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,6 +16,7 @@ RUN pnpm run build # ── Production ───────────────────────────────────────────────── FROM node:22-slim +ARG BUILD_VERSION=dev RUN apt-get update && apt-get install -y gosu && rm -rf /var/lib/apt/lists/* WORKDIR /app @@ -25,6 +26,7 @@ COPY --from=build /app/dist/client /app/public COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh RUN mkdir -p /config +RUN echo "$BUILD_VERSION" > /app/version.txt EXPOSE 3001 ENV NODE_ENV=production diff --git a/server/organize-worker.ts b/server/organize-worker.ts index 9769cd4..dead780 100644 --- a/server/organize-worker.ts +++ b/server/organize-worker.ts @@ -353,7 +353,7 @@ async function organizeTorrent( // Vérification de complétion pour épisode unique if (completedFileNames !== null && !completedFileNames.has(filename.toLowerCase())) { - debug(`Fichier incomplet (skip) : ${filename}`) + warn(`Skip (incomplet / priorité 0) : ${filename}`) return { ...result, total: 1, skipped: 1 } } @@ -371,8 +371,8 @@ async function organizeTorrent( for (const c of candidates) { if (fs.existsSync(c)) { src = c; break } } if (!src) { - error(`Fichier source introuvable : ${filename}`) - return { ...result, total: 1, errors: [{ file: filename, error: 'Source introuvable' }] } + error(`Fichier source introuvable : ${filename}\n Chemins essayés :\n${candidates.map(c => ` - ${c}`).join('\n')}`) + return { ...result, total: 1, errors: [{ file: filename, error: `Source introuvable (chemins essayés : ${candidates.join(', ')})` }] } } if (!fs.existsSync(dest)) { @@ -418,9 +418,9 @@ async function organizeTorrent( if (isOrganized(hash, episode_id)) { result.skipped++; continue } // Vérification de complétion : si on a les infos de progression par fichier, - // on skip les fichiers pas encore à 100% (ex: 1% téléchargé avant priorité appliquée). + // on skip les fichiers pas encore à 100% (ex: priorité 0 ou en cours de téléchargement). if (completedFileNames !== null && !completedFileNames.has(filename.toLowerCase())) { - debug(`Fichier incomplet (skip) : ${filename}`) + warn(`Skip (incomplet / priorité 0) : ${filename}`) result.skipped++ continue } @@ -436,8 +436,7 @@ async function organizeTorrent( if (!src) { // 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}`) + warn(`Skip (fichier absent) : ${filename}\n Chemins essayés :\n${candidates.map(c => ` - ${c}`).join('\n')}`) result.skipped++ continue } diff --git a/server/routes/downloads.ts b/server/routes/downloads.ts index 701fde3..a6ea862 100644 --- a/server/routes/downloads.ts +++ b/server/routes/downloads.ts @@ -49,19 +49,27 @@ router.get('/downloads', requireAuth, async (_req, res) => { const enriched = await Promise.all(torrents.map(async (t: any) => { const orgFiles = organized[t.hash] ?? {} let totalFiles = 1 + let serieName: string | undefined + let episodes: any[] = [] try { const availableIds = await readAvailable() for (const id of availableIds) { const sd = await readSerieData(id) if (!sd) continue const match = extractTorrentsFromSerieData(sd).find((st: any) => st.infohash?.toLowerCase() === t.hash?.toLowerCase()) - if (match) { totalFiles = buildResolvedEpisodes(sd, match.infohash, match.season_number).length || 1; break } + if (match) { + const resolved = buildResolvedEpisodes(sd, match.infohash, match.season_number) + totalFiles = resolved.length || 1 + serieName = sd.title ?? sd.show_title + episodes = resolved + break + } } } catch {} const doneFiles = Object.keys(orgFiles).length const organizeState: 'none' | 'partial' | 'done' = doneFiles >= totalFiles ? 'done' : doneFiles > 0 ? 'partial' : 'none' const notif = recentOrganized.find(n => n.hash === t.hash) - return { ...t, organizeState, organizeProgress: { done: doneFiles, total: totalFiles }, errorFiles: notif?.errorFiles ?? [] } + return { ...t, organizeState, organizeProgress: { done: doneFiles, total: totalFiles }, errorFiles: notif?.errorFiles ?? [], serieName, episodes } })) res.json(enriched) } catch (err) { diff --git a/server/routes/import.ts b/server/routes/import.ts index 9ee7fab..e138619 100644 --- a/server/routes/import.ts +++ b/server/routes/import.ts @@ -32,7 +32,13 @@ router.post('/manual-import', requireAuth, async (req, res) => { } const { mediaPath, organizeMode, nfoSupport } = readSettings() const organized = loadOrganizedJson() - const sd = await readSerieData(Number(serie_id)) + let sd = await readSerieData(Number(serie_id)) + if (!sd) { + // readSerieData ne couvre que le cache local — fallback sur le catalogue complet + // (utile pour les séries sans torrent officiel dont les données sont peut-être absentes du cache individuel) + const all = await loadEnrichedSeriesData() + sd = all.find((s: any) => s.id === Number(serie_id)) ?? null + } if (!sd) { res.status(404).json({ error: 'Série introuvable dans le catalogue' }); return } const rawTitle = sd.title ?? sd.show_title ?? '' const serieTitle = rawTitle.replace(/:/g, ' -').replace(/[<>"/\\|?*]/g, '').replace(/\s+/g, ' ').trim() diff --git a/server/torrent-clients/qbittorrent.ts b/server/torrent-clients/qbittorrent.ts index 636dae9..d16842a 100644 --- a/server/torrent-clients/qbittorrent.ts +++ b/server/torrent-clients/qbittorrent.ts @@ -58,13 +58,12 @@ async function qbTorrentExists(config: Record, authHead /** * Attend que les fichiers soient disponibles, résout l'index par chemin si possible, - * applique les priorités, puis reprend le torrent. + * puis applique les priorités (désactive tout sauf la cible). */ async function qbApplyFilePriority( config : Record, hash : string, fileIndex: number, - resume : boolean, ): Promise { // Première tentative rapide (100 ms), puis 500 ms — 60 tentatives max ≈ 30 s for (let attempt = 0; attempt < 60; attempt++) { @@ -118,36 +117,13 @@ async function qbApplyFilePriority( } } - // Reprendre (torrent mis en pause via paused/stopped à l'ajout) - // On appelle les deux endpoints : resume (qBit ≤ 4.x) et start (qBit ≥ 5.0) - if (resume) { - const resumeBody = new URLSearchParams({ hashes: hash }) - await Promise.allSettled([ - fetch(`${config.url}/api/v2/torrents/resume`, { method: 'POST', body: resumeBody, headers }), - fetch(`${config.url}/api/v2/torrents/start`, { method: 'POST', body: resumeBody, headers }), - ]) - } - - logger.info('qbittorrent', `Fichier ${fileIndex} sélectionné${resume ? ' + reprise' : ''} pour ${hash.slice(0, 8)}…`) + logger.info('qbittorrent', `Fichier ${fileIndex} sélectionné pour ${hash.slice(0, 8)}…`) return } catch (err) { if (err instanceof Error && err.message.includes('hors limites')) throw err } } - // Timeout : reprendre quand même pour ne pas laisser le torrent bloqué - if (resume) { - try { - const authH = await qbAuth(config) - const headers = { ...authH, 'Content-Type': 'application/x-www-form-urlencoded' } - const body = new URLSearchParams({ hashes: hash }) - await Promise.allSettled([ - fetch(`${config.url}/api/v2/torrents/resume`, { method: 'POST', body, headers }), - fetch(`${config.url}/api/v2/torrents/start`, { method: 'POST', body, headers }), - ]) - } catch {} - } - throw new Error(`Timeout : métadonnées non disponibles pour ${hash.slice(0, 8)}…`) } @@ -275,7 +251,7 @@ const QB: TorrentClientDriver = { const exists = await qbTorrentExists(config, authH, hash) if (exists) { logger.info('qbittorrent', `Torrent ${hash.slice(0, 8)}… déjà présent, mise à jour priorité fichier ${options.file_index}`) - qbApplyFilePriority(config, hash, options.file_index, false).catch(err => + qbApplyFilePriority(config, hash, options.file_index).catch(err => logger.warn('qbittorrent', `Priorité fichier non appliquée : ${err instanceof Error ? err.message : err}`) ) return @@ -305,14 +281,12 @@ const QB: TorrentClientDriver = { form.append('urls', url) } else { // FanKarr télécharge le .torrent et l'envoie directement à qBit + // (qBit n'a pas forcément accès à internet / nyaa.si depuis son réseau Docker) logger.debug('qbittorrent', `Téléchargement .torrent via FanKarr : ${url.slice(0, 120)}`) const torrentRes = await fetch(url, { headers: { 'User-Agent': 'FanKarr/1.0' } }) if (!torrentRes.ok) throw new Error(`Impossible de télécharger le .torrent (${torrentRes.status}) : ${url}`) const torrentBytes = await torrentRes.arrayBuffer() form.append('torrents', new Blob([torrentBytes], { type: 'application/x-bittorrent' }), 'torrent.torrent') - // Pause : métadonnées déjà dans le fichier → liste de fichiers disponible immédiatement - form.append('paused', 'true') // qBit < 5.0 - form.append('stopped', 'true') // qBit ≥ 5.0 } const res = await fetch(`${config.url}/api/v2/torrents/add`, { @@ -322,8 +296,8 @@ const QB: TorrentClientDriver = { if (text !== 'Ok.') throw new Error(`Ajout échoué : ${text}`) if (hash) { - logger.info('qbittorrent', `Torrent ajouté (${isMagnet ? 'magnet, actif' : 'en pause'}) — sélection fichier ${options.file_index} en attente des métadonnées`) - qbApplyFilePriority(config, hash, options.file_index, true).catch(err => + logger.info('qbittorrent', `Torrent ajouté — sélection fichier ${options.file_index} en attente des métadonnées`) + qbApplyFilePriority(config, hash, options.file_index).catch(err => logger.warn('qbittorrent', `Priorité fichier non appliquée : ${err instanceof Error ? err.message : err}`) ) } else { @@ -341,7 +315,7 @@ const QB: TorrentClientDriver = { if (!newT) continue const resolvedHash = String(newT.hash).toLowerCase() logger.info('qbittorrent', `Hash résolu : ${resolvedHash.slice(0, 8)}… — application priorité fichier ${fileIndex}`) - await qbApplyFilePriority(config, resolvedHash, fileIndex, true) + await qbApplyFilePriority(config, resolvedHash, fileIndex) return } catch {} } diff --git a/src/components/downloads/TorrentCard.vue b/src/components/downloads/TorrentCard.vue index 5696225..b14e3db 100644 --- a/src/components/downloads/TorrentCard.vue +++ b/src/components/downloads/TorrentCard.vue @@ -4,7 +4,30 @@
-

{{ torrent.name }}

+ +
+

{{ torrent.name }}

+ + +
+ +

{{ torrent.serieName }}

{{ stateBadge(torrent.state).label }} {{ torrent.client_name }} @@ -65,8 +88,12 @@ : 'Non importé' }} - @@ -130,6 +157,23 @@ const withFiles = ref(false) const clampedProgress = computed(() => Math.min(100, Math.max(0, props.torrent.progress ?? 0))) +// Épisodes enrichis avec leur progression depuis les données de fichiers du client +const episodesWithProgress = computed(() => { + const eps: any[] = props.torrent.episodes ?? [] + if (!eps.length) return [] + return eps.map((ep: any) => { + const file = (props.torrent.files ?? []).find((f: any) => { + const basename = String(f.name ?? '').replace(/\\/g, '/').split('/').pop() ?? '' + return basename.toLowerCase() === ep.filename?.toLowerCase() + }) + return { + ...ep, + progress: file != null ? Math.round((file.progress ?? 0) * 100) : null, + priority: file?.priority ?? 1, + } + }) +}) + function stateBadge(state: string) { return ({ downloading: { label: 'Téléchargement', class: 'border-accent/40 text-accent' },