From a7250a5f804189a7ddecef44193436cf6b324c31 Mon Sep 17 00:00:00 2001 From: MasutaYunikon Date: Fri, 29 May 2026 02:56:32 +0200 Subject: [PATCH] =?UTF-8?q?feat(torrent):=20syst=C3=A8me=20g=C3=A9n=C3=A9r?= =?UTF-8?q?ique=20de=20suivi=20des=20t=C3=A2ches=20g=C3=A9r=C3=A9es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - index: ajout de managed_tasks.json (clientUuid → [infohash]) · trackManagedHash() appelé dans dispatchDownload après add() réussi · untrackManagedHash() appelé dans dispatchRemove après remove() réussi · clearManagedHashes() appelé dans removeClient · dispatchList filtre par les hashes suivis pour les drivers avec filterByManagedHashes: true (clients sans catégories natives) - TorrentClientDefinition: nouveau champ filterByManagedHashes (optionnel) - synology-ds: filterByManagedHashes: true — la liste ne retourne que les torrents ajoutés via FanKarr - synology-ds add(): snapshot avant/après création pour capturer le dbid_X et peupler _hashToSynoId immédiatement (sans attendre le prochain list) --- server/torrent-clients/index.ts | 82 +++++++++++++++++++++++++-- server/torrent-clients/synology-ds.ts | 29 +++++++++- 2 files changed, 104 insertions(+), 7 deletions(-) diff --git a/server/torrent-clients/index.ts b/server/torrent-clients/index.ts index 6984310..c03ccc6 100644 --- a/server/torrent-clients/index.ts +++ b/server/torrent-clients/index.ts @@ -22,9 +22,12 @@ export interface FieldDef { } export interface TorrentClientDefinition { - id : string - label : string - fields : FieldDef[] + id : string + label : string + fields : FieldDef[] + /** Si true, dispatchList filtre les résultats pour n'inclure que les + * torrents ajoutés via FanKarr (clients sans catégories natives). */ + filterByManagedHashes?: boolean } export interface SavedClient { @@ -94,7 +97,64 @@ export function getAvailableClients(): TorrentClientDefinition[] { // ─── Persistence ────────────────────────────────────────────────────────────── -const CLIENTS_PATH = path.join(DATA_DIR, 'torrent_clients.json') +const CLIENTS_PATH = path.join(DATA_DIR, 'torrent_clients.json') + +// ─── Managed tasks ───────────────────────────────────────────────────────────── +// Stocke les infohashes des torrents ajoutés via FanKarr, par UUID client. +// Utilisé par dispatchList pour filtrer les résultats des clients sans +// catégories natives (ex: Synology Download Station). +// Structure : { clientUuid: [infohash, ...] } + +const MANAGED_PATH = path.join(DATA_DIR, 'managed_tasks.json') + +function loadManagedMap(): Record { + try { + if (!fs.existsSync(MANAGED_PATH)) return {} + return JSON.parse(fs.readFileSync(MANAGED_PATH, 'utf-8')) + } catch { return {} } +} + +function saveManagedMap(data: Record) { + try { + fs.mkdirSync(path.dirname(MANAGED_PATH), { recursive: true }) + fs.writeFileSync(MANAGED_PATH, JSON.stringify(data, null, 2), 'utf-8') + } catch {} +} + +/** Retourne le Set des infohashes gérés pour un client, ou null si vide. */ +function getManagedSet(clientUuid: string): Set | null { + const list = loadManagedMap()[clientUuid] + if (!list || list.length === 0) return null + return new Set(list) +} + +/** Enregistre un infohash comme géré pour un client (idempotent). */ +export function trackManagedHash(clientUuid: string, infohash: string) { + const data = loadManagedMap() + if (!data[clientUuid]) data[clientUuid] = [] + const h = infohash.toLowerCase() + if (!data[clientUuid].includes(h)) { + data[clientUuid].push(h) + saveManagedMap(data) + } +} + +/** Retire un infohash de la liste gérée pour un client. */ +export function untrackManagedHash(clientUuid: string, infohash: string) { + const data = loadManagedMap() + if (!data[clientUuid]) return + data[clientUuid] = data[clientUuid].filter(h => h !== infohash.toLowerCase()) + if (data[clientUuid].length === 0) delete data[clientUuid] + saveManagedMap(data) +} + +/** Supprime toutes les tâches gérées d'un client (appelé lors de la suppression du client). */ +function clearManagedHashes(clientUuid: string) { + const data = loadManagedMap() + if (!data[clientUuid]) return + delete data[clientUuid] + saveManagedMap(data) +} function loadClients(): SavedClient[] { try { @@ -129,6 +189,7 @@ export function removeClient(uuid: string): boolean { const filtered = clients.filter(c => c.uuid !== uuid) if (filtered.length === clients.length) return false saveClients(filtered) + clearManagedHashes(uuid) logger.info('torrent-clients', `Client supprimé : "${target?.name}" (${target?.type})`) return true } @@ -243,6 +304,10 @@ export async function dispatchDownload(url: string, options?: DownloadOptions): } try { await driver.add(client.config, url, options) + if (options?.infohash) { + trackManagedHash(client.uuid, options.infohash) + logger.debug('torrent-clients', `Tâche ${options.infohash.slice(0, 8)}… suivie pour "${client.name}"`) + } logger.info('torrent-clients', `Téléchargement envoyé à "${client.name}"`) results.push({ uuid: client.uuid, name: client.name, ok: true }) } catch (err) { @@ -266,6 +331,7 @@ export async function dispatchRemove(hash: string, deleteFiles = false): Promise try { await driver.remove(client.config, hash, deleteFiles) logger.info('torrent-clients', `Torrent ${hash.slice(0, 8)}… supprimé de "${client.name}"${deleteFiles ? ' (avec fichiers)' : ''}`) + untrackManagedHash(client.uuid, hash) found = true } catch (err) { const msg = err instanceof Error ? err.message : 'Erreur inconnue' @@ -328,6 +394,14 @@ export async function dispatchList( } } + // Filtre par hashes gérés pour les clients sans catégories natives. + // Si le Set de tâches suivies est non-vide et que ce hash n'y figure pas, + // on ignore ce torrent (il n'a pas été ajouté via FanKarr). + if (driver.definition.filterByManagedHashes) { + const managed = getManagedSet(client.uuid) + if (managed && !managed.has(hash)) continue + } + results.push({ ...t, hash, save_path, client_uuid: client.uuid, client_name: client.name }) } } catch (err) { diff --git a/server/torrent-clients/synology-ds.ts b/server/torrent-clients/synology-ds.ts index d393f40..9aec56d 100644 --- a/server/torrent-clients/synology-ds.ts +++ b/server/torrent-clients/synology-ds.ts @@ -120,9 +120,10 @@ async function dsRequest( const DS: TorrentClientDriver = { definition: { - id : 'synology-ds', - label : 'Synology Download Station', - fields: [ + id : 'synology-ds', + label : 'Synology Download Station', + filterByManagedHashes: true, + fields : [ { key: 'url', label: 'URL DSM', type: 'url', placeholder: 'http://192.168.1.x:5000', required: true }, { key: 'username', label: 'Identifiant', type: 'text', placeholder: 'admin', required: true }, { key: 'password', label: 'Mot de passe', type: 'password', placeholder: '••••••••', required: true }, @@ -245,6 +246,13 @@ const DS: TorrentClientDriver = { const sid = await dsLogin(config) + // Snapshot des IDs existants avant l'ajout — pour identifier la nouvelle tâche Synology + const beforeIds = new Set() + try { + const before = await dsRequest(config, 'SYNO.DownloadStation.Task', 'list', '1', {}, sid) + for (const t of before?.tasks ?? []) beforeIds.add(String(t.id)) + } catch {} + const doAdd = async (withDestination: boolean) => { const params = new URLSearchParams({ api : 'SYNO.DownloadStation.Task', @@ -268,6 +276,21 @@ const DS: TorrentClientDriver = { } if (!data.success) throw new Error(`Ajout échoué : ${JSON.stringify(data.error)}`) + + // Snapshot après ajout — identifier l'ID natif Synology (dbid_X) de la tâche créée + // et le stocker dans _hashToSynoId pour remove/getFiles sans attendre le prochain list() + if (options?.infohash) { + try { + const after = await dsRequest(config, 'SYNO.DownloadStation.Task', 'list', '1', {}, sid) + const newTask = (after?.tasks ?? []).find((t: any) => !beforeIds.has(String(t.id))) + if (newTask) { + const synoId = String(newTask.id).toLowerCase() + _hashToSynoId.set(options.infohash.toLowerCase(), synoId) + logger.debug('synology-ds', `ID Synology capturé : ${synoId} → ${options.infohash.slice(0, 8)}…`) + } + } catch {} + } + logger.info('synology-ds', `Torrent ajouté avec succès${config.savePath ? ` (dossier: ${config.savePath})` : ''}`) },