first commit

This commit is contained in:
2026-01-02 15:49:01 +03:00
commit 4348f76a7c
80 changed files with 10133 additions and 0 deletions

View File

@@ -0,0 +1,177 @@
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<void> {
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<T>(fn: () => Promise<T>): Promise<T> {
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<string> {
const response = await this.request(() =>
this.client.get<string>("/api/v2/app/version")
);
return response.data;
}
async getTorrentsInfo(): Promise<QbitTorrentInfo[]> {
const response = await this.request(() =>
this.client.get<QbitTorrentInfo[]>("/api/v2/torrents/info", {
params: { filter: "all" },
})
);
return response.data;
}
async getTransferInfo(): Promise<QbitTransferInfo> {
const response = await this.request(() =>
this.client.get<QbitTransferInfo>("/api/v2/transfer/info")
);
return response.data;
}
async getTorrentProperties(hash: string): Promise<QbitTorrentProperties> {
const response = await this.request(() =>
this.client.get<QbitTorrentProperties>("/api/v2/torrents/properties", {
params: { hash },
})
);
return response.data;
}
async getTorrentPeers(hash: string): Promise<QbitPeerList> {
const response = await this.request(() =>
this.client.get<QbitPeerList>("/api/v2/sync/torrentPeers", {
params: { hash },
})
);
return response.data;
}
async exportTorrent(hash: string): Promise<Buffer> {
const response = await this.request(() =>
this.client.get<ArrayBuffer>("/api/v2/torrents/export", {
params: { hashes: hash },
responseType: "arraybuffer",
})
);
return Buffer.from(response.data);
}
async addTorrentByMagnet(magnet: string, options: Record<string, string> = {}) {
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<string, string> = {}) {
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" },
})
);
}
}