import {homedir} from 'node:os'; import path from 'node:path'; import type { AddTorrentByFileOptions, AddTorrentByURLOptions, ReannounceTorrentsOptions, SetTorrentsTagsOptions, } from '@shared/schema/api/torrents'; import type {DelugeConnectionSettings} from '@shared/schema/ClientConnectionSettings'; import type {SetClientSettingsOptions} from '@shared/types/api/client'; import type { CheckTorrentsOptions, DeleteTorrentsOptions, MoveTorrentsOptions, SetTorrentContentsPropertiesOptions, SetTorrentsInitialSeedingOptions, SetTorrentsPriorityOptions, SetTorrentsSequentialOptions, SetTorrentsTrackersOptions, StartTorrentsOptions, StopTorrentsOptions, } from '@shared/types/api/torrents'; import type {ClientSettings} from '@shared/types/ClientSettings'; import type {TorrentList, TorrentListSummary, TorrentProperties} from '@shared/types/Torrent'; import type {TorrentContent} from '@shared/types/TorrentContent'; import {TorrentContentPriority} from '@shared/types/TorrentContent'; import type {TorrentPeer} from '@shared/types/TorrentPeer'; import type {TorrentTracker} from '@shared/types/TorrentTracker'; import {TorrentTrackerType} from '@shared/types/TorrentTracker'; import type {TransferSummary} from '@shared/types/TransferData'; import {fetchUrls} from '../../util/fetchUtil'; import ClientGatewayService from '../clientGatewayService'; import ClientRequestManager from './clientRequestManager'; import {DelugeCoreTorrentFilePriority} from './types/DelugeCoreMethods'; import {getTorrentStatusFromStatuses} from './util/torrentPropertiesUtil'; class DelugeClientGatewayService extends ClientGatewayService { private clientRequestManager = new ClientRequestManager(this.user.client as DelugeConnectionSettings); async addTorrentsByFile({ files, destination, isCompleted, isInitialSeeding, isSequential, start, }: Required): Promise { const result = await Promise.all( files.map(async (file, index) => this.clientRequestManager .coreAddTorrentFile(`${Date.now()}-${index}.torrent`, file, { download_location: destination, add_paused: !start, sequential_download: isSequential, super_seeding: isInitialSeeding, }) .then(this.processClientRequestSuccess, this.processClientRequestError), ), ); if (isCompleted) { // Deluge does not provide function to add completed torrents for await (const hash of result) { await this.checkTorrents({hashes: [hash]}); } } return result.map((hash) => hash.toUpperCase()); } async addTorrentsByURL({ urls: inputUrls, cookies, destination, tags, isBasePath, isCompleted, isInitialSeeding, isSequential, start, }: Required): Promise { const {files, urls} = await fetchUrls(inputUrls, cookies); if (!files[0] && !urls[0]) { throw new Error(); } const result: string[] = []; if (urls[0]) { result.push( ...(await Promise.all( urls.map((url) => this.clientRequestManager .coreAddTorrentMagnet(url, { download_location: destination, add_paused: !start, sequential_download: isSequential, super_seeding: isInitialSeeding, }) .then(this.processClientRequestSuccess, this.processClientRequestError), ), )), ); } if (files[0]) { result.push( ...(await this.addTorrentsByFile({ files: files.map((file) => file.toString('base64')) as [string, ...string[]], destination, tags, isBasePath, isCompleted, isInitialSeeding, isSequential, start, })), ); } return result; } async checkTorrents({hashes}: CheckTorrentsOptions): Promise { return this.clientRequestManager .coreForceRecheck(hashes) .then(this.processClientRequestSuccess, this.processClientRequestError); } async getTorrentContents(hash: TorrentProperties['hash']): Promise> { return this.clientRequestManager .coreGetTorrentStatus(hash, ['files', 'file_progress', 'file_priorities']) .then(this.processClientRequestSuccess, this.processClientRequestError) .then(({files, file_progress, file_priorities}) => files.map((file) => { let priority = TorrentContentPriority.NORMAL; switch (file_priorities[file.index]) { case DelugeCoreTorrentFilePriority.Skip: priority = TorrentContentPriority.DO_NOT_DOWNLOAD; break; case DelugeCoreTorrentFilePriority.High: priority = TorrentContentPriority.HIGH; break; default: break; } return { index: file.index, path: file.path, filename: file.path.split('/').pop() || '', percentComplete: file_progress[file.index] * 100, priority, sizeBytes: file.size, }; }), ); } async getTorrentPeers(hash: TorrentProperties['hash']): Promise> { return this.clientRequestManager .coreGetTorrentStatus(hash, ['peers']) .then(this.processClientRequestSuccess, this.processClientRequestError) .then(({peers}) => peers.map((peer) => ({ address: peer.ip.split(':')[0], country: peer.country, clientVersion: peer.client, completedPercent: peer.progress, downloadRate: peer.down_speed, uploadRate: peer.up_speed, isEncrypted: false, isIncoming: false, })), ); } async getTorrentTrackers(hash: TorrentProperties['hash']): Promise> { return this.clientRequestManager .coreGetTorrentStatus(hash, ['trackers']) .then(this.processClientRequestSuccess, this.processClientRequestError) .then(({trackers}) => trackers.map(({url}) => ({ url, type: url.startsWith('http') ? TorrentTrackerType.HTTP : TorrentTrackerType.UDP, })), ); } async moveTorrents({hashes, destination}: MoveTorrentsOptions): Promise { return this.clientRequestManager .coreMoveStorage(hashes, destination) .then(this.processClientRequestSuccess, this.processClientRequestError); } async reannounceTorrents({hashes}: ReannounceTorrentsOptions): Promise { return this.clientRequestManager .coreForceReannounce(hashes) .then(this.processClientRequestSuccess, this.processClientRequestError); } async removeTorrents({hashes, deleteData}: DeleteTorrentsOptions): Promise { return this.clientRequestManager .coreRemoveTorrents(hashes, deleteData ?? false) .then(this.processClientRequestSuccess, this.processClientRequestError); } async setTorrentsInitialSeeding({hashes, isInitialSeeding}: SetTorrentsInitialSeedingOptions): Promise { return this.clientRequestManager .coreSetTorrentOptions(hashes, {super_seeding: isInitialSeeding}) .then(this.processClientRequestSuccess, this.processClientRequestError); } async setTorrentsPriority({}: SetTorrentsPriorityOptions): Promise { return; } async setTorrentsSequential({hashes, isSequential}: SetTorrentsSequentialOptions): Promise { return this.clientRequestManager .coreSetTorrentOptions(hashes, {sequential_download: isSequential}) .then(this.processClientRequestSuccess, this.processClientRequestError); } async setTorrentsTags({}: SetTorrentsTagsOptions): Promise { return; } async setTorrentsTrackers({hashes, trackers}: SetTorrentsTrackersOptions): Promise { return this.clientRequestManager .coreSetTorrentTrackers( hashes, trackers.map((url) => ({url, tier: 0})), ) .then(this.processClientRequestSuccess, this.processClientRequestError); } async setTorrentContentsPriority( hash: string, {indices, priority}: SetTorrentContentsPropertiesOptions, ): Promise { let delugePriority: DelugeCoreTorrentFilePriority = DelugeCoreTorrentFilePriority.Normal; switch (priority) { case TorrentContentPriority.DO_NOT_DOWNLOAD: delugePriority = DelugeCoreTorrentFilePriority.Skip; break; case TorrentContentPriority.HIGH: delugePriority = DelugeCoreTorrentFilePriority.High; break; default: break; } const {file_priorities} = await this.clientRequestManager .coreGetTorrentStatus(hash, ['file_priorities']) .then(this.processClientRequestSuccess, this.processClientRequestError); indices.forEach((index) => { file_priorities[index] = delugePriority; }); return this.clientRequestManager .coreSetTorrentOptions([hash], {file_priorities}) .then(this.processClientRequestSuccess, this.processClientRequestError); } async startTorrents({hashes}: StartTorrentsOptions): Promise { return this.clientRequestManager .coreResumeTorrents(hashes) .then(this.processClientRequestSuccess, this.processClientRequestError); } async stopTorrents({hashes}: StopTorrentsOptions): Promise { return this.clientRequestManager .corePauseTorrents(hashes) .then(this.processClientRequestSuccess, this.processClientRequestError); } async fetchTorrentList(): Promise { return this.clientRequestManager .coreGetTorrentsStatus([ 'active_time', 'comment', 'download_location', 'download_payload_rate', 'eta', 'finished_time', 'message', 'name', 'num_peers', 'num_seeds', 'private', 'progress', 'ratio', 'sequential_download', 'state', 'super_seeding', 'time_added', 'total_done', 'total_payload_download', 'total_payload_upload', 'total_peers', 'total_size', 'total_seeds', 'tracker_host', 'upload_payload_rate', ]) .then(this.processClientRequestSuccess, this.processClientRequestError) .then(async (torrentsStatus) => { this.emit('PROCESS_TORRENT_LIST_START'); const dateNowSeconds = Math.ceil(Date.now() / 1000); const torrentList: TorrentList = Object.assign( {}, ...(await Promise.all( Object.keys(torrentsStatus).map(async (hash) => { const status = torrentsStatus[hash]; const torrentProperties: TorrentProperties = { bytesDone: status.total_done, comment: status.comment, dateActive: status.download_payload_rate > 0 || status.upload_payload_rate > 0 ? -1 : status.active_time, dateAdded: status.time_added, dateCreated: 0, dateFinished: status.finished_time > 0 ? Math.ceil((dateNowSeconds - status.finished_time) / 10) * 10 : 0, directory: status.download_location, downRate: status.download_payload_rate, downTotal: status.total_payload_download, eta: status.eta === 0 ? -1 : status.eta, hash: hash.toUpperCase(), isPrivate: status.private, isInitialSeeding: status.super_seeding, isSequential: status.sequential_download, message: status.message, name: status.name, peersConnected: status.num_peers, peersTotal: status.total_peers < 0 ? 0 : status.total_peers, percentComplete: status.progress, priority: 1, ratio: status.ratio, seedsConnected: status.num_seeds, seedsTotal: status.total_seeds < 0 ? 0 : status.total_seeds, sizeBytes: status.total_size, status: getTorrentStatusFromStatuses(status), tags: [], trackerURIs: [status.tracker_host], upRate: status.upload_payload_rate, upTotal: status.total_payload_upload, }; this.emit('PROCESS_TORRENT', torrentProperties); return { [torrentProperties.hash]: torrentProperties, }; }), )), ); const torrentListSummary = { id: Date.now(), torrents: torrentList, }; this.emit('PROCESS_TORRENT_LIST_END', torrentListSummary); return torrentListSummary; }); } async fetchTransferSummary(): Promise { return this.clientRequestManager .coreGetSessionStatus([ 'net.recv_payload_bytes', 'net.sent_payload_bytes', 'payload_download_rate', 'payload_upload_rate', ]) .then(this.processClientRequestSuccess, this.processClientRequestError) .then((response) => ({ downRate: response['payload_download_rate'], downTotal: response['net.recv_payload_bytes'], upRate: response['payload_upload_rate'], upTotal: response['net.sent_payload_bytes'], })); } async getClientSessionDirectory(): Promise<{path: string; case: 'lower' | 'upper'}> { // Deluge API does not provide session directory. // We can only guess with the common locations here. switch (process.platform) { case 'win32': if (process.env.APPDATA) { return {path: path.join(process.env.APPDATA, '\\deluge\\state'), case: 'lower'}; } return {path: path.join(homedir(), '\\AppData\\deluge\\state'), case: 'lower'}; default: return {path: path.join(homedir(), '/.config/deluge/state'), case: 'lower'}; } } async getClientSettings(): Promise { return this.clientRequestManager .coreGetConfigValues([ 'dht', 'download_location', 'listen_interface', 'listen_ports', 'max_download_speed', 'max_upload_speed', 'max_upload_slots_per_torrent', 'max_upload_slots_global', 'random_port', 'utpex', ]) .then(this.processClientRequestSuccess, this.processClientRequestError) .then((response) => ({ dht: response.dht, dhtPort: response.listen_ports[0], directoryDefault: response.download_location, networkHttpMaxOpen: -1, networkLocalAddress: [response.listen_interface], networkMaxOpenFiles: -1, networkPortOpen: true, networkPortRandom: response.random_port, networkPortRange: response.listen_ports.join('-'), piecesHashOnCompletion: false, piecesMemoryMax: -1, protocolPex: response.utpex, throttleGlobalDownSpeed: response.max_download_speed, throttleGlobalUpSpeed: response.max_upload_speed, throttleMaxPeersNormal: -1, throttleMaxPeersSeed: -1, throttleMaxDownloads: -1, throttleMaxDownloadsGlobal: -1, throttleMaxUploads: response.max_upload_slots_per_torrent, throttleMaxUploadsGlobal: response.max_upload_slots_global, throttleMinPeersNormal: -1, throttleMinPeersSeed: -1, trackersNumWant: -1, })); } async setClientSettings(settings: SetClientSettingsOptions): Promise { return this.clientRequestManager .coreSetConfig({ dht: settings.dht, download_location: settings.directoryDefault, listen_interface: settings.networkLocalAddress?.[0], listen_ports: settings.networkPortRange?.split('-').map((port) => Number(port)), max_download_speed: settings.throttleGlobalDownSpeed, max_upload_speed: settings.throttleGlobalUpSpeed, max_upload_slots_per_torrent: settings.throttleMaxUploads, max_upload_slots_global: settings.throttleMaxUploadsGlobal, random_port: settings.networkPortRandom, utpex: settings.protocolPex, }) .then(this.processClientRequestSuccess, this.processClientRequestError); } async testGateway(): Promise { await this.clientRequestManager .daemonGetMethodList() .then(this.processClientRequestSuccess, () => this.clientRequestManager.reconnect().then(this.processClientRequestSuccess, this.processClientRequestError), ); } } export default DelugeClientGatewayService;