/** * Synology Download Station Driver */ import type { TorrentClientDriver, TorrentInfo, DownloadOptions } from './index.js' 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' return 'unknown' } // Authentification DSM — retourne un SID async function dsLogin(config: Record): Promise { const params = new URLSearchParams({ api : 'SYNO.API.Auth', version: '3', method : 'login', account: String(config.username ?? ''), passwd : String(config.password ?? ''), session: 'DownloadStation', format : 'sid', }) const res = await fetch(`${config.url}/webapi/auth.cgi?${params}`) if (!res.ok) throw new Error(`HTTP ${res.status}`) const data = await res.json() if (!data.success) throw new Error(`Auth échouée : ${JSON.stringify(data.error)}`) return data.data.sid } async function dsRequest( config : Record, api : string, method : string, version: string, extra : Record = {}, sid : string, ): Promise { const params = new URLSearchParams({ api, version, method, _sid: sid, ...extra }) const res = await fetch(`${config.url}/webapi/DownloadStation/task.cgi?${params}`) if (!res.ok) throw new Error(`HTTP ${res.status}`) const data = await res.json() if (!data.success) throw new Error(`DS erreur : ${JSON.stringify(data.error)}`) return data.data } const DS: TorrentClientDriver = { definition: { id : 'synology-ds', label : 'Synology Download Station', 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 }, { key: 'category', label: 'Catégorie', type: 'text', placeholder: 'fankai', required: false, default: 'fankai' }, { key: 'savePath', label: 'Dossier cible', type: 'text', placeholder: '/volume1/downloads/fankai', required: false }, { key: 'remotePath', label: 'Chemin distant (client)', type: 'text', placeholder: '/volume1/downloads', required: false }, { key: 'localPath', label: 'Chemin local (FanKarr)', type: 'text', placeholder: '/mnt/nas/downloads', required: false }, ], }, async test(config) { try { await dsLogin(config) logger.info('synology-ds', `Test de connexion réussi sur ${config.url}`) return { ok: true, message: 'Connexion réussie' } } catch (err) { const msg = err instanceof Error ? err.message : 'Erreur inconnue' logger.warn('synology-ds', `Test de connexion échoué sur ${config.url} : ${msg}`) return { ok: false, message: msg } } }, async healthcheck(config) { try { const sid = await dsLogin(config) const params = new URLSearchParams({ api : 'SYNO.DownloadStation.Info', version: '2', method : 'getinfo', _sid : sid, }) const res = await fetch(`${config.url}/webapi/DownloadStation/info.cgi?${params}`) if (!res.ok) return { online: false } const data = await res.json() if (!data.success) return { online: false } const version = data.data?.version_string ?? 'inconnue' logger.debug('synology-ds', `Healthcheck OK — version ${version}`) return { online: true, version } } catch (err) { logger.debug('synology-ds', `Healthcheck échoué : ${err instanceof Error ? err.message : err}`) return { online: false } } }, async list(config, category) { const sid = await dsLogin(config) const data = await dsRequest(config, 'SYNO.DownloadStation.Task', 'list', '1', { additional: 'transfer,detail' }, sid) const tasks: any[] = data?.tasks ?? [] return tasks .map(t => { const transfer = t.additional?.transfer ?? {} const size = t.size ?? 0 const dl = transfer.size_downloaded ?? 0 return { hash : t.id, name : t.title, state : mapState(t.status), progress : size > 0 ? Math.min(100, Math.round((dl / size) * 100)) : 0, size, downloaded: dl, uploaded : transfer.size_uploaded ?? 0, ratio : size > 0 ? Math.round(((transfer.size_uploaded ?? 0) / size) * 100) / 100 : 0, speed : transfer.speed_download ?? 0, upspeed : transfer.speed_upload ?? 0, eta : -1, save_path : t.additional?.detail?.destination ?? '', category : category ?? '', } satisfies TorrentInfo }) }, async add(config, url, options?: DownloadOptions) { 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({ api : 'SYNO.DownloadStation.Task', version: '1', method : 'create', _sid : sid, ...extra, }) const res = await fetch(`${config.url}/webapi/DownloadStation/task.cgi?${params}`) 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)}`) 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 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()) 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 ? 'false' : 'false', _sid : sid, }) const res = await fetch(`${config.url}/webapi/DownloadStation/task.cgi?${params}`) if (!res.ok) throw new Error(`HTTP ${res.status}`) logger.info('synology-ds', `Torrent ${found.id} supprimé`) }, } export default DS