Files
flood/server/services/qBittorrent/clientRequestManager.ts
2024-08-13 22:31:39 +00:00

615 lines
16 KiB
TypeScript

import {URLSearchParams} from 'node:url';
import type {QBittorrentConnectionSettings} from '@shared/schema/ClientConnectionSettings';
import axios from 'axios';
import FormData from 'form-data';
import type {QBittorrentAppPreferences} from './types/QBittorrentAppMethods';
import type {
QBittorrentMainData,
QBittorrentSyncMainData,
QBittorrentSyncTorrentPeers,
QBittorrentTorrentPeers,
} from './types/QBittorrentSyncMethods';
import type {
QBittorrentTorrentContentPriority,
QBittorrentTorrentContents,
QBittorrentTorrentInfos,
QBittorrentTorrentProperties,
QBittorrentTorrentsAddOptions,
QBittorrentTorrentTrackers,
} from './types/QBittorrentTorrentsMethods';
import type {QBittorrentTransferInfo} from './types/QBittorrentTransferMethods';
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<string | undefined> = Promise.resolve(undefined);
private isMainDataPending = false;
private syncRids: {
mainData: Promise<number>;
} = {
mainData: Promise.resolve(0),
};
private syncStates: {
mainData: QBittorrentMainData;
} = {
mainData: {
categories: {},
server_state: EMPTY_SERVER_STATE,
tags: [],
torrents: {},
trackers: {},
},
};
async authenticate(connectionSettings = this.connectionSettings): Promise<string | undefined> {
const {url, username, password} = connectionSettings;
return axios
.post(
`${url}/api/v2/auth/login`,
new URLSearchParams({
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<void> {
let authFailed = false;
this.authCookie = this.authenticate(connectionSettings).catch(() => {
authFailed = true;
return undefined;
});
await this.authCookie;
if (authFailed) {
throw new Error();
}
}
async getRequestHeaders(): Promise<Record<string, string>> {
const Cookie = await this.authCookie;
return {
...(Cookie == null ? {} : {Cookie}),
};
}
async getAppPreferences(): Promise<QBittorrentAppPreferences> {
return axios
.post<QBittorrentAppPreferences>(`${this.apiBase}/app/preferences`, null, {
headers: await this.getRequestHeaders(),
})
.then((res) => res.data);
}
async setAppPreferences(preferences: Partial<QBittorrentAppPreferences>): Promise<void> {
return axios
.post(`${this.apiBase}/app/setPreferences`, `json=${JSON.stringify(preferences)}`, {
headers: await this.getRequestHeaders(),
})
.then(() => {
// returns nothing
});
}
async getTorrentInfos(): Promise<QBittorrentTorrentInfos> {
return axios
.post<QBittorrentTorrentInfos>(`${this.apiBase}/torrents/info`, null, {
headers: await this.getRequestHeaders(),
})
.then((res) => res.data);
}
async getTorrentContents(hash: string): Promise<QBittorrentTorrentContents> {
return axios
.post<QBittorrentTorrentContents>(
`${this.apiBase}/torrents/files`,
new URLSearchParams({
hash: hash.toLowerCase(),
}),
{
headers: await this.getRequestHeaders(),
},
)
.then((res) => res.data);
}
async getTorrentProperties(hash: string): Promise<QBittorrentTorrentProperties> {
return axios
.post<QBittorrentTorrentProperties>(
`${this.apiBase}/torrents/properties`,
new URLSearchParams({
hash: hash.toLowerCase(),
}),
{
headers: await this.getRequestHeaders(),
},
)
.then((res) => res.data);
}
async getTorrentTrackers(hash: string): Promise<QBittorrentTorrentTrackers> {
return axios
.post<QBittorrentTorrentTrackers>(
`${this.apiBase}/torrents/trackers`,
new URLSearchParams({
hash: hash.toLowerCase(),
}),
{
headers: await this.getRequestHeaders(),
},
)
.then((res) => res.data);
}
async getTransferInfo(): Promise<QBittorrentTransferInfo> {
return axios
.post<QBittorrentTransferInfo>(`${this.apiBase}/transfer/info`, null, {
headers: await this.getRequestHeaders(),
})
.then((res) => res.data);
}
async syncMainData(): Promise<QBittorrentMainData> {
const headers = await this.getRequestHeaders();
if (this.isMainDataPending == false) {
this.isMainDataPending = true;
this.syncRids.mainData = this.syncRids.mainData.then((rid) =>
axios
.post<QBittorrentSyncMainData>(
`${this.apiBase}/sync/maindata`,
new URLSearchParams({
rid: `${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<QBittorrentTorrentPeers> {
return axios
.post<QBittorrentSyncTorrentPeers>(
`${this.apiBase}/sync/torrentPeers`,
new URLSearchParams({
hash: hash.toLowerCase(),
rid: '0',
}),
{
headers: await this.getRequestHeaders(),
},
)
.then(({data}) => data.peers);
}
async torrentsPause(hashes: Array<string>): Promise<void> {
return axios
.post(
`${this.apiBase}/torrents/pause`,
new URLSearchParams({
hashes: hashes.join('|').toLowerCase(),
}),
{
headers: await this.getRequestHeaders(),
},
)
.then(() => {
// returns nothing
});
}
async torrentsResume(hashes: Array<string>): Promise<void> {
return axios
.post(
`${this.apiBase}/torrents/resume`,
new URLSearchParams({
hashes: hashes.join('|').toLowerCase(),
}),
{
headers: await this.getRequestHeaders(),
},
)
.then(() => {
// returns nothing
});
}
async torrentsDelete(hashes: Array<string>, deleteFiles: boolean): Promise<void> {
return axios
.post(
`${this.apiBase}/torrents/delete`,
new URLSearchParams({
hashes: hashes.join('|').toLowerCase(),
deleteFiles: deleteFiles ? 'true' : 'false',
}),
{
headers: await this.getRequestHeaders(),
},
)
.then(() => {
// returns nothing
});
}
async torrentsRecheck(hashes: Array<string>): Promise<void> {
return axios
.post(
`${this.apiBase}/torrents/recheck`,
new URLSearchParams({
hashes: hashes.join('|').toLowerCase(),
}),
{
headers: await this.getRequestHeaders(),
},
)
.then(() => {
// returns nothing
});
}
async torrentsSetLocation(hashes: Array<string>, location: string): Promise<void> {
return axios
.post(
`${this.apiBase}/torrents/setLocation`,
new URLSearchParams({
hashes: hashes.join('|').toLowerCase(),
location,
}),
{
headers: await this.getRequestHeaders(),
},
)
.then(() => {
// returns nothing
});
}
async torrentsSetTopPrio(hashes: Array<string>): Promise<void> {
return axios
.post(
`${this.apiBase}/torrents/topPrio`,
new URLSearchParams({
hashes: hashes.join('|').toLowerCase(),
}),
{
headers: await this.getRequestHeaders(),
},
)
.then(() => {
// returns nothing
});
}
async torrentsSetBottomPrio(hashes: Array<string>): Promise<void> {
return axios
.post(
`${this.apiBase}/torrents/bottomPrio`,
new URLSearchParams({
hashes: hashes.join('|').toLowerCase(),
}),
{
headers: await this.getRequestHeaders(),
},
)
.then(() => {
// returns nothing
});
}
async torrentsAddFiles(files: Array<Buffer>, options: QBittorrentTorrentsAddOptions): Promise<void> {
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({
...(await this.getRequestHeaders()),
'Content-Length': form.getLengthSync(),
});
return axios
.post(`${this.apiBase}/torrents/add`, form, {
headers,
})
.then(() => {
// returns nothing
});
}
async torrentsAddURLs(urls: Array<string>, options: QBittorrentTorrentsAddOptions): Promise<void> {
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<string>, tags: Array<string>): Promise<void> {
return axios
.post(
`${this.apiBase}/torrents/addTags`,
new URLSearchParams({
hashes: hashes.join('|').toLowerCase(),
tags: tags.join(','),
}),
{
headers: await this.getRequestHeaders(),
},
)
.then(() => {
// returns nothing
});
}
async torrentsRemoveTags(hashes: Array<string>, tags?: Array<string>): Promise<void> {
return axios
.post(
`${this.apiBase}/torrents/removeTags`,
new URLSearchParams({
hashes: hashes.join('|').toLowerCase(),
tags: tags?.join(',') ?? '',
}),
{
headers: await this.getRequestHeaders(),
},
)
.then(() => {
// returns nothing
});
}
async torrentsAddTrackers(hash: string, urls: Array<string>): Promise<void> {
if (urls.length > 0) {
return axios
.post(
`${this.apiBase}/torrents/addTrackers`,
new URLSearchParams({
hash: hash.toLowerCase(),
urls: urls.join('\n'),
}),
{
headers: await this.getRequestHeaders(),
},
)
.then(() => {
// returns nothing
});
}
}
async torrentsReannounce(hashes: Array<string>): Promise<void> {
if (hashes.length > 0) {
return axios
.post(
`${this.apiBase}/torrents/reannounce`,
new URLSearchParams({
hashes: hashes.join('|').toLowerCase(),
}),
{
headers: await this.getRequestHeaders(),
},
)
.then(() => {
// returns nothing
});
}
}
async torrentsRemoveTrackers(hash: string, urls: Array<string>): Promise<void> {
if (urls.length > 0) {
return axios
.post(
`${this.apiBase}/torrents/removeTrackers`,
new URLSearchParams({
hash: hash.toLowerCase(),
urls: urls.join('|'),
}),
{
headers: await this.getRequestHeaders(),
},
)
.then(() => {
// returns nothing
});
}
}
async torrentsSetSuperSeeding(hashes: Array<string>, value: boolean): Promise<void> {
if (hashes.length > 0) {
return axios
.post(
`${this.apiBase}/torrents/setSuperSeeding`,
new URLSearchParams({
hashes: hashes.join('|').toLowerCase(),
value: value ? 'true' : 'false',
}),
{
headers: await this.getRequestHeaders(),
},
)
.then(() => {
// returns nothing
});
}
}
async torrentsToggleSequentialDownload(hashes: Array<string>): Promise<void> {
if (hashes.length > 0) {
return axios
.post(
`${this.apiBase}/torrents/toggleSequentialDownload`,
new URLSearchParams({
hashes: hashes.join('|').toLowerCase(),
}),
{
headers: await this.getRequestHeaders(),
},
)
.then(() => {
// returns nothing
});
}
}
async torrentsFilePrio(hash: string, ids: Array<number>, priority: QBittorrentTorrentContentPriority) {
return axios
.post(
`${this.apiBase}/torrents/filePrio`,
new URLSearchParams({
hash: hash.toLowerCase(),
id: ids.join('|'),
priority: `${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;