diff --git a/server/organize-worker.ts b/server/organize-worker.ts index 1cfa664..caed5ba 100644 --- a/server/organize-worker.ts +++ b/server/organize-worker.ts @@ -67,12 +67,20 @@ function findTorrentByHash(hash: string, seriesData: any[]): { torrent: any; ser if (t.infohash?.toLowerCase() === hash.toLowerCase()) return { torrent: { ...t, _season: season }, serieData: sd } } - // Niveau épisode - for (const ep of season.episodes ?? []) { - for (const t of ep.torrents ?? []) { - if (t.infohash?.toLowerCase() === hash.toLowerCase()) - return { torrent: { ...t, _episode: ep, _season: season }, serieData: sd } - } + // Niveau épisode — compter combien d'épisodes partagent ce torrent + const matchingEpisodes = season.episodes?.filter((ep: any) => + ep.torrents?.some((t: any) => t.infohash?.toLowerCase() === hash.toLowerCase()) + ) ?? [] + + if (matchingEpisodes.length === 1) { + // Torrent propre à un seul épisode → cas épisode unique + const ep = matchingEpisodes[0] + const t = ep.torrents.find((t: any) => t.infohash?.toLowerCase() === hash.toLowerCase()) + return { torrent: { ...t, _episode: ep, _season: season }, serieData: sd } + } else if (matchingEpisodes.length > 1) { + // Même torrent partagé entre plusieurs épisodes → traiter comme pack_saison + const t = matchingEpisodes[0].torrents.find((t: any) => t.infohash?.toLowerCase() === hash.toLowerCase()) + return { torrent: { ...t, _season: season }, serieData: sd } } } } @@ -135,6 +143,23 @@ function isInExcludedFolder(filePath: string[]): boolean { return false } +// ─── Map correction titres API → titres GitLab ─────────────── +const SERIE_TITLE_GITLAB_MAP: Record = { + 'Enfer Et Paradis Henshū' : 'Enfer et Paradis Henshū', + 'Hajime No Ippo Henshū' : 'Hajime no Ippo Henshū', + 'Hikaru No Go Henshū' : 'Hikaru no Go Henshū', + 'Hokuto No Ken Kaï' : 'Hokuto no Ken Kaï', + 'Kaguya-sama : Love is War Henshū' : 'Kaguya-sama - Love is War Henshū', + 'Kenshin le Vagabond Henshū' : 'Kenshin le vagabond Henshū', + 'Kuroko No Basket Henshū' : 'Kuroko no Basket Henshū', + 'Shingeki No Kyojin Henshū' : 'Shingeki no Kyojin Henshū', + 'Tower Of God Henshū' : 'Tower of God Henshū', +} + +function getGitlabSerieTitle(serieTitle: string): string { + return SERIE_TITLE_GITLAB_MAP[serieTitle] ?? serieTitle +} + // ─── GitLab NFO downloader ──────────────────────────────────── const GITLAB_API = 'https://gitlab.com/api/v4/projects/ElPouki%2Ffankai_pack/repository' const GITLAB_RAW_BASE = 'https://gitlab.com/ElPouki/fankai_pack/-/raw/main/pack' @@ -154,8 +179,9 @@ async function fetchBinary(url: string): Promise { } async function downloadGitlabFolder(serieTitle: string, destRoot: string): Promise { - const folderPath = `pack/${serieTitle}` - log(`[nfo] Récupération dossier GitLab: ${serieTitle}`) + const gitlabTitle = getGitlabSerieTitle(serieTitle) + const folderPath = `pack/${gitlabTitle}` + log(`[nfo] Récupération dossier GitLab: ${gitlabTitle}`) let files: any[] = [] try { @@ -180,12 +206,12 @@ async function downloadGitlabFolder(serieTitle: string, destRoot: string): Promi log(`[nfo] ${fileEntries.length} fichiers à télécharger`) for (const entry of fileEntries) { - const relativePath = entry.path.replace(`pack/${serieTitle}/`, '') + const relativePath = entry.path.replace(`pack/${gitlabTitle}/`, '') const destPath = path.join(destRoot, relativePath) if (fs.existsSync(destPath)) continue try { fs.mkdirSync(path.dirname(destPath), { recursive: true }) - const rawUrl = `${GITLAB_RAW_BASE}/${encodeURIComponent(serieTitle)}/${relativePath.split('/').map(encodeURIComponent).join('/')}` + const rawUrl = `${GITLAB_RAW_BASE}/${encodeURIComponent(gitlabTitle)}/${relativePath.split('/').map(encodeURIComponent).join('/')}` const data = await fetchBinary(rawUrl) fs.writeFileSync(destPath, data) log(`[nfo] ✓ ${relativePath}`) @@ -193,7 +219,7 @@ async function downloadGitlabFolder(serieTitle: string, destRoot: string): Promi warn(`[nfo] ✗ ${relativePath}: ${err instanceof Error ? err.message : err}`) } } - log(`[nfo] GitLab synchronisé pour "${serieTitle}"`) + log(`[nfo] GitLab synchronisé pour "${gitlabTitle}"`) } // ─── Filesystem ops ─────────────────────────────────────────── diff --git a/server/organize.ts b/server/organize.ts index 71adbfc..f2036cf 100644 --- a/server/organize.ts +++ b/server/organize.ts @@ -154,7 +154,19 @@ export async function autoOrganizeAll( for (const t of torrents) _prevStates.set(t.hash, t.state) - if (!isFirstRun && newlySeeding.length === 0) return + // Charger organized.json pour vérifier s'il reste des torrents non organisés + let organized: Record> = {} + try { + const orgPath = path.join(DATA_DIR, 'organized.json') + if (fs.existsSync(orgPath)) + organized = JSON.parse(fs.readFileSync(orgPath, 'utf-8')) + } catch {} + + const hasUnorganized = torrents.some(t => + t.state === 'seeding' && !organized[t.hash?.toLowerCase()] + ) + + if (!isFirstRun && newlySeeding.length === 0 && !hasUnorganized) return if (seedingCount === 0) return if (newlySeeding.length > 0 && !isFirstRun) { diff --git a/src/views/DownloadsView.vue b/src/views/DownloadsView.vue index 4c96358..53a7973 100644 --- a/src/views/DownloadsView.vue +++ b/src/views/DownloadsView.vue @@ -10,6 +10,12 @@

Téléchargements

+
{{ polling ? 'EN DIRECT' : 'PAUSE' }} @@ -190,6 +196,7 @@ const loading = ref(true) const polling = ref(true) const noClients = ref(false) const organizing = ref>({}) +const organizingAll = ref(false) const activeTab = ref<'active' | 'done'>('active') const hideOrganized = ref(false) @@ -251,7 +258,7 @@ function togglePolling() { onMounted(startPolling) onUnmounted(stopPolling) -// ── Organiser ────────────────────────────────────────────────── +// ── Organiser un torrent ─────────────────────────────────────── async function organize(torrent: any) { organizing.value[torrent.hash] = true try { @@ -263,6 +270,7 @@ async function organize(torrent: any) { }) if (res.ok) { toast(`${torrent.name} organisé ✓`, 'success') + await fetchTorrents() } else { const { error } = await res.json() toast(error ?? 'Erreur lors de l\'organisation', 'error') @@ -274,6 +282,40 @@ async function organize(torrent: any) { } } +// ── Organiser tout ───────────────────────────────────────────── +async function organizeAll() { + const toOrganize = doneTorrents.value.filter(t => t.organizeState !== 'done') + if (toOrganize.length === 0) { + toast('Tous les torrents sont déjà organisés', 'success') + return + } + + organizingAll.value = true + let done = 0 + let errors = 0 + + for (const t of toOrganize) { + try { + const res = await fetch('/api/organize', { + method : 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ hash: t.hash, save_path: t.save_path, name: t.name }) + }) + if (res.ok) done++ + else errors++ + } catch { + errors++ + } + } + + organizingAll.value = false + await fetchTorrents() + + if (errors === 0) toast(`${done} torrent(s) organisé(s) ✓`, 'success') + else toast(`${done} OK, ${errors} erreur(s)`, 'error') +} + // ── Helpers affichage ────────────────────────────────────────── function stateBadge(state: string) { const map: Record = {