import axios, {AxiosError} from 'axios'; import type {TransmissionConnectionSettings} from '@shared/schema/ClientConnectionSettings'; import type { TransmissionSessionGetArguments, TransmissionSessionProperties, TransmissionSessionSetArguments, TransmissionSessionStats, } from './types/TransmissionSessionMethods'; import { TransmissionTorrentIDs, TransmissionTorrentProperties, TransmissionTorrentAddArguments, TransmissionTorrentsGetArguments, TransmissionTorrentsRemoveArguments, TransmissionTorrentsSetArguments, TransmissionTorrentsSetLocationArguments, } from './types/TransmissionTorrentsMethods'; class ClientRequestManager { private rpcURL: string; private authHeader: string; private sessionID?: Promise; async fetchSessionID(url = this.rpcURL, authHeader = this.authHeader): Promise { return axios .get(url, { headers: { Authorization: authHeader, }, }) .then( () => { return undefined; }, (err: AxiosError) => { if (err.response?.status === 409) { return err.response?.headers['x-transmission-session-id']; } throw err; }, ); } async updateSessionID(url = this.rpcURL, authHeader = this.authHeader): Promise { let authFailed = false; this.sessionID = new Promise((resolve) => { this.fetchSessionID(url, authHeader).then( (sessionID) => { resolve(sessionID); }, () => { authFailed = true; resolve(undefined); }, ); }); await this.sessionID; return authFailed ? Promise.reject() : Promise.resolve(); } async getRequestHeaders(): Promise> { const sessionID = await this.sessionID; return { Authorization: this.authHeader, ...(sessionID == null ? {} : {'X-Transmission-Session-Id': sessionID}), }; } async getSessionStats(): Promise { return axios .post( this.rpcURL, {method: 'session-stats'}, { headers: await this.getRequestHeaders(), }, ) .then((res) => { if (res.data.result !== 'success') { throw new Error(); } return res.data.arguments; }); } async getSessionProperties( fields: T, ): Promise> { const sessionGetArguments: TransmissionSessionGetArguments = {fields}; return axios .post( this.rpcURL, {method: 'session-get', arguments: sessionGetArguments}, { headers: await this.getRequestHeaders(), }, ) .then((res) => { if (res.data.result !== 'success') { throw new Error(); } return res.data.arguments; }); } async setSessionProperties(properties: TransmissionSessionSetArguments): Promise { return axios .post( this.rpcURL, {method: 'session-set', arguments: properties}, { headers: await this.getRequestHeaders(), }, ) .then((res) => { if (res.data.result !== 'success') { throw new Error(); } }); } async getTorrents( ids: TransmissionTorrentIDs | null, fields: T, ): Promise>> { const torrentsGetArguments: TransmissionTorrentsGetArguments = { ids: ids || undefined, fields, format: 'objects', }; return axios .post( this.rpcURL, {method: 'torrent-get', arguments: torrentsGetArguments}, { headers: await this.getRequestHeaders(), }, ) .then((res) => { if (res.data.result !== 'success' || res.data.arguments.torrents == null) { throw new Error(); } return res.data.arguments.torrents; }); } async addTorrent( args: TransmissionTorrentAddArguments, ): Promise> { return axios .post( this.rpcURL, {method: 'torrent-add', arguments: args}, { headers: await this.getRequestHeaders(), }, ) .then((res) => { if (res.data.result !== 'success') { throw new Error(); } return res.data.arguments; }); } async setTorrentsProperties(args: TransmissionTorrentsSetArguments): Promise { return axios .post( this.rpcURL, {method: 'torrent-set', arguments: args}, { headers: await this.getRequestHeaders(), }, ) .then((res) => { if (res.data.result !== 'success') { throw new Error(); } }); } async startTorrents(ids: TransmissionTorrentIDs): Promise { return axios .post( this.rpcURL, {method: 'torrent-start-now', arguments: {ids}}, { headers: await this.getRequestHeaders(), }, ) .then((res) => { if (res.data.result !== 'success') { throw new Error(); } }); } async stopTorrents(ids: TransmissionTorrentIDs): Promise { return axios .post( this.rpcURL, {method: 'torrent-stop', arguments: {ids}}, { headers: await this.getRequestHeaders(), }, ) .then((res) => { if (res.data.result !== 'success') { throw new Error(); } }); } async verifyTorrents(ids: TransmissionTorrentIDs): Promise { return axios .post( this.rpcURL, {method: 'torrent-verify', arguments: {ids}}, { headers: await this.getRequestHeaders(), }, ) .then((res) => { if (res.data.result !== 'success') { throw new Error(); } }); } async removeTorrents(ids: TransmissionTorrentIDs, deleteData?: boolean): Promise { const removeTorrentsArguments: TransmissionTorrentsRemoveArguments = { ids, 'delete-local-data': deleteData, }; return axios .post( this.rpcURL, {method: 'torrent-remove', arguments: removeTorrentsArguments}, { headers: await this.getRequestHeaders(), }, ) .then((res) => { if (res.data.result !== 'success') { throw new Error(); } }); } async setTorrentsLocation(ids: TransmissionTorrentIDs, location: string, move?: boolean): Promise { const torrentsSetLocationArguments: TransmissionTorrentsSetLocationArguments = { ids, location, move, }; return axios .post( this.rpcURL, { method: 'torrent-set-location', arguments: torrentsSetLocationArguments, }, { headers: await this.getRequestHeaders(), }, ) .then((res) => { if (res.data.result !== 'success') { throw new Error(); } }); } constructor(connectionSettings: TransmissionConnectionSettings) { const {url, username, password} = connectionSettings; this.rpcURL = url; this.authHeader = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`; this.updateSessionID().catch(() => undefined); setInterval(() => { this.updateSessionID().catch(() => undefined); }, 1000 * 60 * 60 * 8); } } export default ClientRequestManager;