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:
1 parent
d326e08834
commit
a7250a5f80
2 files changed
+104
-7
No files matched your search
@@ -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) {
|
||||
|
||||
@@ -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})` : ''}`)
|
||||
},
|
||||
|
||||
|
||||
Reference in new issue
Block a user