From 8b4b198697ba3296444759c522bb0139be69e364 Mon Sep 17 00:00:00 2001 From: MasutaYunikon Date: Wed, 27 May 2026 16:57:04 +0200 Subject: [PATCH] fix(client): suppresion de alldebrid MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Supprime le driver AllDebrid et son enregistrement - Synology : extrait le vrai hash BT depuis additional.detail.hash - Synology : ajoute les statuts finishing/extracting → seeding - Synology : fix remove() pour matcher par hash BT ou ID Synology - Synology : add() passe en POST form-encoded --- server/index.ts | 2 - server/torrent-clients/alldebrid.ts | 197 -------------------------- server/torrent-clients/synology-ds.ts | 60 +++++--- 3 files changed, 39 insertions(+), 220 deletions(-) delete mode 100644 server/torrent-clients/alldebrid.ts diff --git a/server/index.ts b/server/index.ts index e7702df..87b11ad 100644 --- a/server/index.ts +++ b/server/index.ts @@ -13,7 +13,6 @@ import synologyDsDriver from './torrent-clients/synology-ds.js' import utorrentDriver from './torrent-clients/utorrent.js' import rtorrentDriver from './torrent-clients/rtorrent.js' import realDebridDriver from './torrent-clients/real-debrid.js' -import allDebridDriver from './torrent-clients/alldebrid.js' import delugeDriver from './torrent-clients/deluge.js' import { autoOrganizeAll, scanMediaPath, syncFilenameChanges, migrateOrganizedEpisodeIds } from './organize.js' import { logger } from './logger.js' @@ -48,7 +47,6 @@ registerDriver(synologyDsDriver) registerDriver(utorrentDriver) registerDriver(rtorrentDriver) registerDriver(realDebridDriver) -registerDriver(allDebridDriver) registerDriver(delugeDriver) const app = express() diff --git a/server/torrent-clients/alldebrid.ts b/server/torrent-clients/alldebrid.ts deleted file mode 100644 index 0d971de..0000000 --- a/server/torrent-clients/alldebrid.ts +++ /dev/null @@ -1,197 +0,0 @@ -import type { TorrentClientDriver, TorrentInfo, TorrentFileProgress, DownloadOptions } from './index.js' -import { logger } from '../logger.js' - -const AD_BASE = 'https://api.alldebrid.com/v4.1' -const AD_BASE_V4 = 'https://api.alldebrid.com/v4' -const AD_AGENT = 'FanKarr' - -function adUrl(config: Record, path: string, base = AD_BASE): string { - const sep = path.includes('?') ? '&' : '?' - return `${base}${path}${sep}agent=${AD_AGENT}&apikey=${config.apiKey}` -} - -async function adFetch( - config: Record, - method: 'GET' | 'POST' | 'DELETE', - path : string, - body ?: URLSearchParams | FormData, - base = AD_BASE, -): Promise { - const res = await fetch(adUrl(config, path, base), { method, body }) - if (!res.ok) { - const text = await res.text().catch(() => '') - throw new Error(`AllDebrid HTTP ${res.status} : ${text}`) - } - const json = await res.json() - if (json.status === 'error') { - throw new Error(`AllDebrid : ${json.error?.message ?? json.error?.code ?? 'Erreur inconnue'}`) - } - return json.data -} - -// statusCode : 0=Processing, 1=Downloading, 2=Compressing, 3=Uploading, 4=Ready, -// 5=Upload Fail, 6=Internal Error, 7=Not Downloaded, 8=File Not Available, -// 9=Banned BitTorrent, 10=Awaiting Slot, 11=Free Trial Limit Reached -// hasLocalMount : true si un chemin local (FUSE/WebDAV) est configuré. -// Sans montage local, statusCode 4 ("prêt sur les serveurs AD") ne doit pas -// déclencher l'import FanKarr — les fichiers ne sont pas encore sur le disque. -function mapStatus(statusCode: number, hasLocalMount: boolean): TorrentInfo['state'] { - switch (statusCode) { - case 4: return hasLocalMount ? 'seeding' : 'downloading' - case 1: - case 0: - case 2: - case 3: - case 10: return 'downloading' - case 5: - case 6: - case 7: - case 8: - case 9: - case 11: return 'error' - default: return 'unknown' - } -} - -function flattenFiles(nodes: any[], prefix = '', startIdx = 0): TorrentFileProgress[] { - const result: TorrentFileProgress[] = [] - let idx = startIdx - for (const node of nodes) { - if (node.e) { - const children = flattenFiles(node.e, prefix ? `${prefix}/${node.n}` : node.n, idx) - result.push(...children) - idx += children.length - } else if (node.n) { - result.push({ index: idx++, name: prefix ? `${prefix}/${node.n}` : node.n, progress: 0, priority: 1 }) - } - } - return result -} - -const adDriver: TorrentClientDriver = { - definition: { - id : 'alldebrid', - label : 'AllDebrid', - fields: [ - { key: 'apiKey', label: 'Clé API', type: 'password', required: true }, - { key: 'localPath', label: 'Dossier local (FUSE/WebDAV/…)', type: 'text', placeholder: '/mnt/alldebrid', required: false }, - ], - }, - - async test(config) { - try { - const data = await adFetch(config, 'GET', '/user') - const username = data.user?.username ?? data.user?.login ?? '?' - logger.info('alldebrid', `Connexion réussie — utilisateur : ${username}`) - return { ok: true, message: `Connecté en tant que ${username}` } - } catch (err) { - const msg = err instanceof Error ? err.message : 'Erreur inconnue' - logger.warn('alldebrid', `Test de connexion échoué : ${msg}`) - return { ok: false, message: msg } - } - }, - - async healthcheck(config) { - try { - const data = await adFetch(config, 'GET', '/user') - const username = data.user?.username ?? data.user?.login ?? '?' - return { online: true, version: `AD — ${username}` } - } catch { - return { online: false } - } - }, - - async add(config, url, options?: DownloadOptions) { - if (options?.file_index != null) - logger.warn('alldebrid', `Sélection de fichier (index ${options.file_index}) non supportée par AllDebrid — pack complet téléchargé`) - - const isMagnet = url.startsWith('magnet:') - - if (isMagnet) { - const body = new URLSearchParams() - body.append('magnets[]', url) - const data = await adFetch(config, 'POST', '/magnet/upload', body) - const id = data.magnets?.[0]?.id - logger.info('alldebrid', `Magnet ajouté → ID ${id}`) - } else { - const torrentRes = await fetch(url) - if (!torrentRes.ok) throw new Error(`Impossible de télécharger le fichier .torrent : ${torrentRes.status}`) - const torrentBuf = await torrentRes.arrayBuffer() - const formData = new FormData() - formData.append('files[]', new Blob([torrentBuf], { type: 'application/x-bittorrent' }), 'torrent.torrent') - const data = await adFetch(config, 'POST', '/magnet/upload/file', formData) - const id = data.magnets?.[0]?.id - logger.info('alldebrid', `.torrent uploadé → ID ${id}`) - } - }, - - async list(config, _category?) { - const data = await adFetch(config, 'GET', '/magnet/status') - const magnets: any[] = data.magnets ?? [] - const localBase = String(config.localPath ?? '').replace(/\/+$/, '') - - return magnets.map((m: any): TorrentInfo => { - const statusCode = typeof m.statusCode === 'number' ? m.statusCode : 0 - const size = typeof m.size === 'number' ? m.size : 0 - const downloaded = typeof m.downloaded === 'number' ? m.downloaded : 0 - const progress = size > 0 - ? Math.round((downloaded / size) * 100) - : (statusCode === 4 ? 100 : 0) - // save_path = dossier PARENT (comme qBittorrent), sans le nom du torrent. - // Le worker construit lui-même le chemin complet via getFiles(). - const save_path = localBase - - const eta = (typeof m.completionDate === 'number' && m.completionDate > 0) - ? Math.max(0, m.completionDate - Math.floor(Date.now() / 1000)) - : -1 - - return { - hash : String(m.hash ?? m.id ?? '').toLowerCase(), - name : String(m.filename ?? m.id ?? ''), - state : mapStatus(statusCode, Boolean(localBase)), - progress, - size, - downloaded, - uploaded : typeof m.uploaded === 'number' ? m.uploaded : 0, - ratio : 0, - speed : typeof m.downloadSpeed === 'number' ? m.downloadSpeed : 0, - upspeed : typeof m.uploadSpeed === 'number' ? m.uploadSpeed : 0, - eta, - save_path, - category : '', - } - }) - }, - - async remove(config, hash, _deleteFiles?) { - const data = await adFetch(config, 'GET', '/magnet/status') - const magnets: any[] = data.magnets ?? [] - const target = magnets.find((m: any) => String(m.hash ?? m.id ?? '').toLowerCase() === hash.toLowerCase()) - if (!target) throw new Error(`Torrent ${hash.slice(0, 8)}… introuvable sur AllDebrid`) - await adFetch(config, 'DELETE', `/magnet/delete?id=${target.id}`) - logger.info('alldebrid', `Torrent ${hash.slice(0, 8)}… supprimé de AllDebrid`) - }, - - async getFiles(config, hash) { - const data = await adFetch(config, 'GET', '/magnet/status') - const magnets: any[] = data.magnets ?? [] - const target = magnets.find((m: any) => String(m.hash ?? m.id ?? '').toLowerCase() === hash.toLowerCase()) - if (!target) return [] - - const statusCode = typeof target.statusCode === 'number' ? target.statusCode : 0 - const size = typeof target.size === 'number' ? target.size : 0 - const downloaded = typeof target.downloaded === 'number' ? target.downloaded : 0 - const overallProgress = size > 0 ? downloaded / size : (statusCode === 4 ? 1 : 0) - - const body = new URLSearchParams() - body.append('id[]', String(target.id)) - const filesData = await adFetch(config, 'POST', '/magnet/files', body, AD_BASE_V4) - const magnetEntry = (filesData.magnets ?? [])[0] - - if (!Array.isArray(magnetEntry?.files)) return [] - - return flattenFiles(magnetEntry.files).map(f => ({ ...f, progress: overallProgress })) - }, -} - -export default adDriver diff --git a/server/torrent-clients/synology-ds.ts b/server/torrent-clients/synology-ds.ts index 46065d5..b5c61c9 100644 --- a/server/torrent-clients/synology-ds.ts +++ b/server/torrent-clients/synology-ds.ts @@ -7,12 +7,13 @@ import { logger } from '../logger.js' // Synology Download Station task status codes function mapState(status: string): TorrentInfo['state'] { - if (status === 'downloading') return 'downloading' - if (status === 'seeding' || status === 'finished') return 'seeding' - if (status === 'paused') return 'paused' - if (status === 'waiting' || status === 'filehosting_waiting') return 'downloading' - if (status === 'hash_checking') return 'checking' - if (status === 'error') return 'error' + if (status === 'downloading') return 'downloading' + if (status === 'seeding' || status === 'finished') return 'seeding' + if (status === 'finishing' || status === 'extracting') return 'seeding' + if (status === 'paused') return 'paused' + if (status === 'waiting' || status === 'filehosting_waiting') return 'downloading' + if (status === 'hash_checking') return 'checking' + if (status === 'error') return 'error' return 'unknown' } @@ -110,10 +111,19 @@ const DS: TorrentClientDriver = { return tasks .map(t => { const transfer = t.additional?.transfer ?? {} + const detail = t.additional?.detail ?? {} const size = t.size ?? 0 const dl = transfer.size_downloaded ?? 0 + + // Synology expose le hash BT dans additional.detail.hash + // Fallback sur dbid_ pour les tâches non-BT (DDL, etc.) + const rawHash = detail.hash + const hash = (typeof rawHash === 'string' && rawHash.length > 0) + ? rawHash.toLowerCase() + : `dbid_${t.id}` + return { - hash : t.id, + hash, name : t.title, state : mapState(t.status), progress : size > 0 ? Math.min(100, Math.round((dl / size) * 100)) : 0, @@ -124,7 +134,7 @@ const DS: TorrentClientDriver = { speed : transfer.speed_download ?? 0, upspeed : transfer.speed_upload ?? 0, eta : -1, - save_path : t.additional?.detail?.destination ?? '', + save_path : detail.destination ?? '', category : category ?? '', } satisfies TorrentInfo }) @@ -134,18 +144,20 @@ const DS: TorrentClientDriver = { if (options?.file_index != null) logger.warn('synology-ds', 'Sélection de fichier non supportée — téléchargement complet') const sid = await dsLogin(config) - const extra: Record = { uri: url } - if (config.savePath) extra.destination = String(config.savePath) - - const params = new URLSearchParams({ + const body = new URLSearchParams({ api : 'SYNO.DownloadStation.Task', version: '1', method : 'create', _sid : sid, - ...extra, + uri : url, }) + if (config.savePath) body.append('destination', String(config.savePath)) - const res = await fetch(`${config.url}/webapi/DownloadStation/task.cgi?${params}`) + const res = await fetch(`${config.url}/webapi/DownloadStation/task.cgi`, { + method : 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body : body.toString(), + }) if (!res.ok) throw new Error(`HTTP ${res.status}`) const data = await res.json() if (!data.success) throw new Error(`Ajout échoué : ${JSON.stringify(data.error)}`) @@ -153,20 +165,26 @@ const DS: TorrentClientDriver = { logger.info('synology-ds', `Torrent ajouté avec succès${config.savePath ? ` (dossier: ${config.savePath})` : ''}`) }, - async remove(config, hash, deleteFiles = false) { - const sid = await dsLogin(config) - // Synology utilise son ID interne — on cherche via la liste + async remove(config, hash, _deleteFiles?) { + const sid = await dsLogin(config) const data = await dsRequest(config, 'SYNO.DownloadStation.Task', 'list', '1', { additional: 'detail' }, sid) const tasks: any[] = data?.tasks ?? [] - // Matcher par hash si résolu, sinon par id - const found = tasks.find(t => t.id?.toLowerCase() === hash.toLowerCase()) + + const h = hash.toLowerCase() + const found = tasks.find((t: any) => { + const detailHash = String(t.additional?.detail?.hash ?? '').toLowerCase() + const synoId = String(t.id ?? '').toLowerCase() + return detailHash === h || synoId === h || `dbid_${synoId}` === h + }) + if (!found) throw new Error(`Torrent ${hash.slice(0, 8)}… introuvable`) + const params = new URLSearchParams({ api : 'SYNO.DownloadStation.Task', version: '1', method : 'delete', id : found.id, - force_complete: deleteFiles ? 'true' : 'false', + force_complete: 'false', _sid : sid, }) const res = await fetch(`${config.url}/webapi/DownloadStation/task.cgi?${params}`) @@ -175,4 +193,4 @@ const DS: TorrentClientDriver = { }, } -export default DS \ No newline at end of file +export default DS