import axios, { AxiosInstance } from "axios"; import { CookieJar } from "tough-cookie"; import { wrapper } from "axios-cookiejar-support"; import FormData from "form-data"; import fs from "node:fs"; import { config } from "../config" import { logger } from "../utils/logger" import { QbitPeerList, QbitTorrentInfo, QbitTorrentProperties, QbitTransferInfo, } from "./qbit.types" export class QbitClient { private client: AxiosInstance; private jar: CookieJar; private loggedIn = false; constructor() { this.jar = new CookieJar(); this.client = wrapper( axios.create({ baseURL: config.qbitBaseUrl, jar: this.jar, withCredentials: true, }) ); } async login(): Promise { if (!config.qbitBaseUrl) { throw new Error("QBIT_BASE_URL missing"); } const form = new URLSearchParams(); form.append("username", config.qbitUsername); form.append("password", config.qbitPassword); await this.client.post("/api/v2/auth/login", form, { headers: { "Content-Type": "application/x-www-form-urlencoded" }, }); this.loggedIn = true; } private async request(fn: () => Promise): Promise { try { if (!this.loggedIn) { await this.login(); } return await fn(); } catch (error) { if ( axios.isAxiosError(error) && (error.response?.status === 401 || error.response?.status === 403) ) { logger.warn("qBittorrent session expired, re-login"); this.loggedIn = false; await this.login(); return await fn(); } throw error; } } async getVersion(): Promise { const response = await this.request(() => this.client.get("/api/v2/app/version") ); return response.data; } async getTorrentsInfo(): Promise { const response = await this.request(() => this.client.get("/api/v2/torrents/info", { params: { filter: "all" }, }) ); return response.data; } async getTransferInfo(): Promise { const response = await this.request(() => this.client.get("/api/v2/transfer/info") ); return response.data; } async getTorrentProperties(hash: string): Promise { const response = await this.request(() => this.client.get("/api/v2/torrents/properties", { params: { hash }, }) ); return response.data; } async getTorrentPeers(hash: string): Promise { const response = await this.request(() => this.client.get("/api/v2/sync/torrentPeers", { params: { hash }, }) ); return response.data; } async exportTorrent(hash: string): Promise { const response = await this.request(() => this.client.get("/api/v2/torrents/export", { params: { hashes: hash }, responseType: "arraybuffer", }) ); return Buffer.from(response.data); } async addTorrentByMagnet(magnet: string, options: Record = {}) { const form = new URLSearchParams(); form.append("urls", magnet); Object.entries(options).forEach(([key, value]) => form.append(key, value)); await this.request(() => this.client.post("/api/v2/torrents/add", form, { headers: { "Content-Type": "application/x-www-form-urlencoded" }, }) ); } async addTorrentByFile(filePath: string, options: Record = {}) { const form = new FormData(); form.append("torrents", fs.createReadStream(filePath)); Object.entries(options).forEach(([key, value]) => form.append(key, value)); await this.request(() => this.client.post("/api/v2/torrents/add", form, { headers: form.getHeaders(), }) ); } async deleteTorrent(hash: string, deleteFiles = true) { const form = new URLSearchParams(); form.append("hashes", hash); form.append("deleteFiles", deleteFiles ? "true" : "false"); await this.request(() => this.client.post("/api/v2/torrents/delete", form, { headers: { "Content-Type": "application/x-www-form-urlencoded" }, }) ); } async pauseTorrent(hash: string) { const form = new URLSearchParams(); form.append("hashes", hash); await this.request(() => this.client.post("/api/v2/torrents/pause", form, { headers: { "Content-Type": "application/x-www-form-urlencoded" }, }) ); } async resumeTorrent(hash: string) { const form = new URLSearchParams(); form.append("hashes", hash); await this.request(() => this.client.post("/api/v2/torrents/resume", form, { headers: { "Content-Type": "application/x-www-form-urlencoded" }, }) ); } async banPeers(peers: string[]) { const form = new URLSearchParams(); form.append("peers", peers.join("|")); await this.request(() => this.client.post("/api/v2/transfer/banPeers", form, { headers: { "Content-Type": "application/x-www-form-urlencoded" }, }) ); } }