feat(torrent): système générique de suivi des tâches gérées

- 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)
This commit is contained in:
MasutaYunikon committed 2026-05-29 03:03:06 +02:00
1 parent d326e08834
commit a7250a5f80
2 files changed
+104 -7

No files matched your search

+78 -4
View File
@@ -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<string, string[]> {
try {
if (!fs.existsSync(MANAGED_PATH)) return {}
return JSON.parse(fs.readFileSync(MANAGED_PATH, 'utf-8'))
} catch { return {} }
}
function saveManagedMap(data: Record<string, string[]>) {
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<string> | 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) {
+26 -3
View File
@@ -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<string>()
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})` : ''}`)
},