import axios from 'axios'; import FormData from 'form-data'; import type {QBittorrentConnectionSettings} from '@shared/schema/ClientConnectionSettings'; import type {QBittorrentAppPreferences} from './types/QBittorrentAppMethods'; import type { QBittorrentMainData, QBittorrentSyncMainData, QBittorrentSyncTorrentPeers, QBittorrentTorrentPeers, } from './types/QBittorrentSyncMethods'; import type {QBittorrentTransferInfo} from './types/QBittorrentTransferMethods'; import type { QBittorrentTorrentContentPriority, QBittorrentTorrentContents, QBittorrentTorrentInfos, QBittorrentTorrentProperties, QBittorrentTorrentsAddOptions, QBittorrentTorrentTrackers, } from './types/QBittorrentTorrentsMethods'; const EMPTY_SERVER_STATE = { dl_info_speed: 0, dl_info_data: 0, up_info_speed: 0, up_info_data: 0, dl_rate_limit: 0, up_rate_limit: 0, dht_nodes: 0, connection_status: 'disconnected', } as const; class ClientRequestManager { private connectionSettings: QBittorrentConnectionSettings; private apiBase: string; private authCookie: Promise = Promise.resolve(undefined); private isMainDataPending = false; private syncRids: { mainData: Promise; } = { mainData: Promise.resolve(0), }; private syncStates: { mainData: QBittorrentMainData; } = { mainData: { categories: {}, server_state: EMPTY_SERVER_STATE, tags: [], torrents: {}, trackers: {}, }, }; async authenticate(connectionSettings = this.connectionSettings): Promise { const {url, username, password} = connectionSettings; return axios .get(`${url}/api/v2/auth/login`, { params: { username, password, }, }) .then((res) => { const cookies = res.headers['set-cookie']; if (Array.isArray(cookies)) { return cookies.filter((cookie) => cookie.includes('SID='))[0]; } return undefined; }); } async updateAuthCookie(connectionSettings?: QBittorrentConnectionSettings): Promise { let authFailed = false; this.authCookie = this.authenticate(connectionSettings).catch(() => { authFailed = true; return undefined; }); await this.authCookie; if (authFailed) { throw new Error(); } } async getRequestHeaders(): Promise> { const Cookie = await this.authCookie; return { ...(Cookie == null ? {} : {Cookie}), }; } async getAppPreferences(): Promise { return axios .get(`${this.apiBase}/app/preferences`, { headers: await this.getRequestHeaders(), }) .then((res) => res.data); } async setAppPreferences(preferences: Partial): Promise { return axios .post(`${this.apiBase}/app/setPreferences`, `json=${JSON.stringify(preferences)}`, { headers: await this.getRequestHeaders(), }) .then(() => { // returns nothing }); } async getTorrentInfos(): Promise { return axios .get(`${this.apiBase}/torrents/info`, { headers: await this.getRequestHeaders(), }) .then((res) => res.data); } async getTorrentContents(hash: string): Promise { return axios .get(`${this.apiBase}/torrents/files`, { params: { hash: hash.toLowerCase(), }, headers: await this.getRequestHeaders(), }) .then((res) => res.data); } async getTorrentProperties(hash: string): Promise { return axios .get(`${this.apiBase}/torrents/properties`, { params: { hash: hash.toLowerCase(), }, headers: await this.getRequestHeaders(), }) .then((res) => res.data); } async getTorrentTrackers(hash: string): Promise { return axios .get(`${this.apiBase}/torrents/trackers`, { params: { hash: hash.toLowerCase(), }, headers: await this.getRequestHeaders(), }) .then((res) => res.data); } async getTransferInfo(): Promise { return axios .get(`${this.apiBase}/transfer/info`, { headers: await this.getRequestHeaders(), }) .then((res) => res.data); } async syncMainData(): Promise { const headers = await this.getRequestHeaders(); if (this.isMainDataPending == false) { this.isMainDataPending = true; this.syncRids.mainData = this.syncRids.mainData.then((rid) => axios .get(`${this.apiBase}/sync/maindata`, { params: { rid, }, headers, }) .then(({data}) => { const { rid: newRid = 0, full_update = false, categories = {}, categories_removed = [], server_state = EMPTY_SERVER_STATE, tags = [], tags_removed = [], torrents = {}, torrents_removed = [], trackers = {}, trackers_removed = [], } = data; if (full_update) { this.syncStates.mainData = { categories, server_state, tags, torrents, trackers, }; } else { // categories Object.keys(categories).forEach((category) => { this.syncStates.mainData.categories[category] = { ...this.syncStates.mainData.categories[category], ...categories[category], }; }); categories_removed.forEach((category) => { delete this.syncStates.mainData.categories[category]; }); // tags this.syncStates.mainData.tags.push(...tags); this.syncStates.mainData.tags = this.syncStates.mainData.tags.filter( (tag) => !tags_removed.includes(tag), ); // torrents Object.keys(torrents).forEach((torrent) => { this.syncStates.mainData.torrents[torrent] = { ...this.syncStates.mainData.torrents[torrent], ...torrents[torrent], }; }); torrents_removed.forEach((torrent) => { delete this.syncStates.mainData.torrents[torrent]; }); // trackers Object.keys(trackers).forEach((tracker) => { this.syncStates.mainData.trackers[tracker] = { ...this.syncStates.mainData.trackers[tracker], ...trackers[tracker], }; }); trackers_removed.forEach((tracker) => { delete this.syncStates.mainData.trackers[tracker]; }); } return newRid; }) .finally(() => { this.isMainDataPending = false; }), ); } try { await this.syncRids.mainData; } catch (e) { this.syncRids.mainData = Promise.resolve(0); throw e; } return this.syncStates.mainData; } async syncTorrentPeers(hash: string): Promise { return axios .get(`${this.apiBase}/sync/torrentPeers`, { params: { hash: hash.toLowerCase(), rid: 0, }, headers: await this.getRequestHeaders(), }) .then(({data}) => data.peers); } async torrentsPause(hashes: Array): Promise { return axios .get(`${this.apiBase}/torrents/pause`, { params: { hashes: hashes.join('|').toLowerCase(), }, headers: await this.getRequestHeaders(), }) .then(() => { // returns nothing }); } async torrentsResume(hashes: Array): Promise { return axios .get(`${this.apiBase}/torrents/resume`, { params: { hashes: hashes.join('|').toLowerCase(), }, headers: await this.getRequestHeaders(), }) .then(() => { // returns nothing }); } async torrentsDelete(hashes: Array, deleteFiles: boolean): Promise { return axios .get(`${this.apiBase}/torrents/delete`, { params: { hashes: hashes.join('|').toLowerCase(), deleteFiles: deleteFiles ? 'true' : 'false', }, headers: await this.getRequestHeaders(), }) .then(() => { // returns nothing }); } async torrentsRecheck(hashes: Array): Promise { return axios .get(`${this.apiBase}/torrents/recheck`, { params: { hashes: hashes.join('|').toLowerCase(), }, headers: await this.getRequestHeaders(), }) .then(() => { // returns nothing }); } async torrentsSetLocation(hashes: Array, location: string): Promise { return axios .get(`${this.apiBase}/torrents/setLocation`, { params: { hashes: hashes.join('|').toLowerCase(), location, }, headers: await this.getRequestHeaders(), }) .then(() => { // returns nothing }); } async torrentsSetTopPrio(hashes: Array): Promise { return axios .get(`${this.apiBase}/torrents/topPrio`, { params: { hashes: hashes.join('|').toLowerCase(), }, headers: await this.getRequestHeaders(), }) .then(() => { // returns nothing }); } async torrentsSetBottomPrio(hashes: Array): Promise { return axios .get(`${this.apiBase}/torrents/bottomPrio`, { params: { hashes: hashes.join('|').toLowerCase(), }, headers: await this.getRequestHeaders(), }) .then(() => { // returns nothing }); } async torrentsAddFiles(files: Array, options: QBittorrentTorrentsAddOptions): Promise { const form = new FormData(); files.forEach((file, index) => { form.append('torrents', file, { filename: `${index}.torrent`, contentType: 'application/x-bittorrent', }); }); Object.keys(options).forEach((key) => { const property = key as keyof typeof options; form.append(property, `${options[property]}`); }); const headers = form.getHeaders({ Cookie: await this.authCookie, 'Content-Length': form.getLengthSync(), }); return axios .post(`${this.apiBase}/torrents/add`, form, { headers, }) .then(() => { // returns nothing }); } async torrentsAddURLs(urls: Array, options: QBittorrentTorrentsAddOptions): Promise { const form = new FormData(); form.append('urls', urls.join('\n')); Object.keys(options).forEach((key) => { const property = key as keyof typeof options; form.append(property, `${options[property]}`); }); const headers = form.getHeaders({ Cookie: await this.authCookie, 'Content-Length': form.getLengthSync(), }); return axios .post(`${this.apiBase}/torrents/add`, form, { headers, }) .then(() => { // returns nothing }); } async torrentsAddTags(hashes: Array, tags: Array): Promise { return axios .get(`${this.apiBase}/torrents/addTags`, { params: { hashes: hashes.join('|').toLowerCase(), tags: tags.join(','), }, headers: await this.getRequestHeaders(), }) .then(() => { // returns nothing }); } async torrentsRemoveTags(hashes: Array, tags?: Array): Promise { return axios .get(`${this.apiBase}/torrents/removeTags`, { params: { hashes: hashes.join('|').toLowerCase(), tags: tags?.join(','), }, headers: await this.getRequestHeaders(), }) .then(() => { // returns nothing }); } async torrentsAddTrackers(hash: string, urls: Array): Promise { if (urls.length > 0) { return axios .get(`${this.apiBase}/torrents/addTrackers`, { params: { hash: hash.toLowerCase(), urls: urls.join('\n'), }, headers: await this.getRequestHeaders(), }) .then(() => { // returns nothing }); } } async torrentsReannounce(hashes: Array): Promise { if (hashes.length > 0) { return axios .get(`${this.apiBase}/torrents/reannounce`, { params: { hashes: hashes.join('|').toLowerCase(), }, headers: await this.getRequestHeaders(), }) .then(() => { // returns nothing }); } } async torrentsRemoveTrackers(hash: string, urls: Array): Promise { if (urls.length > 0) { return axios .get(`${this.apiBase}/torrents/removeTrackers`, { params: { hash: hash.toLowerCase(), urls: urls.join('|'), }, headers: await this.getRequestHeaders(), }) .then(() => { // returns nothing }); } } async torrentsSetSuperSeeding(hashes: Array, value: boolean): Promise { if (hashes.length > 0) { return axios .get(`${this.apiBase}/torrents/setSuperSeeding`, { params: { hashes: hashes.join('|').toLowerCase(), value: value ? 'true' : 'false', }, headers: await this.getRequestHeaders(), }) .then(() => { // returns nothing }); } } async torrentsToggleSequentialDownload(hashes: Array): Promise { if (hashes.length > 0) { return axios .get(`${this.apiBase}/torrents/toggleSequentialDownload`, { params: { hashes: hashes.join('|').toLowerCase(), }, headers: await this.getRequestHeaders(), }) .then(() => { // returns nothing }); } } async torrentsFilePrio(hash: string, ids: Array, priority: QBittorrentTorrentContentPriority) { return axios .get(`${this.apiBase}/torrents/filePrio`, { params: { hash: hash.toLowerCase(), id: ids.join('|'), priority, }, headers: await this.getRequestHeaders(), }) .then(() => { // returns nothing }); } constructor(connectionSettings: QBittorrentConnectionSettings) { this.connectionSettings = connectionSettings; this.apiBase = `${connectionSettings.url}/api/v2`; this.updateAuthCookie().catch(() => undefined); } } export default ClientRequestManager;