@cao-mei-you-ren/qbittorrent
Version:
TypeScript api wrapper for qbittorrent using got
1,159 lines (1,147 loc) • 35.9 kB
JavaScript
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var src_exports = {};
__export(src_exports, {
QBittorrent: () => QBittorrent,
TorrentFilePriority: () => TorrentFilePriority,
TorrentPieceState: () => TorrentPieceState,
TorrentState: () => TorrentState,
TorrentTrackerStatus: () => TorrentTrackerStatus
});
module.exports = __toCommonJS(src_exports);
// src/types.ts
var TorrentState = /* @__PURE__ */ ((TorrentState3) => {
TorrentState3["Error"] = "error";
TorrentState3["PausedUP"] = "pausedUP";
TorrentState3["PausedDL"] = "pausedDL";
TorrentState3["QueuedUP"] = "queuedUP";
TorrentState3["QueuedDL"] = "queuedDL";
TorrentState3["Uploading"] = "uploading";
TorrentState3["StalledUP"] = "stalledUP";
TorrentState3["CheckingUP"] = "checkingUP";
TorrentState3["CheckingDL"] = "checkingDL";
TorrentState3["Downloading"] = "downloading";
TorrentState3["StalledDL"] = "stalledDL";
TorrentState3["ForcedDL"] = "forcedDL";
TorrentState3["ForcedMetaDL"] = "ForcedMetaDL";
TorrentState3["ForcedUP"] = "forcedUP";
TorrentState3["MetaDL"] = "metaDL";
TorrentState3["Allocating"] = "allocating";
TorrentState3["QueuedForChecking"] = "queuedForChecking";
TorrentState3["CheckingResumeData"] = "checkingResumeData";
TorrentState3["Moving"] = "moving";
TorrentState3["Unknown"] = "unknown";
TorrentState3["MissingFiles"] = "missingFiles";
return TorrentState3;
})(TorrentState || {});
var TorrentTrackerStatus = /* @__PURE__ */ ((TorrentTrackerStatus2) => {
TorrentTrackerStatus2[TorrentTrackerStatus2["Disabled"] = 0] = "Disabled";
TorrentTrackerStatus2[TorrentTrackerStatus2["Waiting"] = 1] = "Waiting";
TorrentTrackerStatus2[TorrentTrackerStatus2["Working"] = 2] = "Working";
TorrentTrackerStatus2[TorrentTrackerStatus2["Updating"] = 3] = "Updating";
TorrentTrackerStatus2[TorrentTrackerStatus2["Errored"] = 4] = "Errored";
return TorrentTrackerStatus2;
})(TorrentTrackerStatus || {});
var TorrentFilePriority = /* @__PURE__ */ ((TorrentFilePriority2) => {
TorrentFilePriority2[TorrentFilePriority2["Skip"] = 0] = "Skip";
TorrentFilePriority2[TorrentFilePriority2["NormalPriority"] = 1] = "NormalPriority";
TorrentFilePriority2[TorrentFilePriority2["HighPriority"] = 6] = "HighPriority";
TorrentFilePriority2[TorrentFilePriority2["MaxPriority"] = 7] = "MaxPriority";
return TorrentFilePriority2;
})(TorrentFilePriority || {});
var TorrentPieceState = /* @__PURE__ */ ((TorrentPieceState2) => {
TorrentPieceState2[TorrentPieceState2["NotDownloaded"] = 0] = "NotDownloaded";
TorrentPieceState2[TorrentPieceState2["Requested"] = 1] = "Requested";
TorrentPieceState2[TorrentPieceState2["Downloaded"] = 2] = "Downloaded";
return TorrentPieceState2;
})(TorrentPieceState || {});
// src/qbittorrent.ts
var import_cookie = require("cookie");
var import_node_fetch_native = require("node-fetch-native");
var import_ofetch = require("ofetch");
var import_ufo = require("ufo");
// node_modules/.pnpm/uint8array-extras@1.1.0/node_modules/uint8array-extras/index.js
var objectToString = Object.prototype.toString;
var uint8ArrayStringified = "[object Uint8Array]";
function isUint8Array(value) {
if (!value) {
return false;
}
if (value.constructor === Uint8Array) {
return true;
}
return objectToString.call(value) === uint8ArrayStringified;
}
function assertUint8Array(value) {
if (!isUint8Array(value)) {
throw new TypeError(`Expected \`Uint8Array\`, got \`${typeof value}\``);
}
}
var cachedDecoder = new globalThis.TextDecoder();
function assertString(value) {
if (typeof value !== "string") {
throw new TypeError(`Expected \`string\`, got \`${typeof value}\``);
}
}
var cachedEncoder = new globalThis.TextEncoder();
function stringToUint8Array(string) {
assertString(string);
return cachedEncoder.encode(string);
}
function base64UrlToBase64(base64url) {
return base64url.replaceAll("-", "+").replaceAll("_", "/");
}
function base64ToUint8Array(base64String) {
assertString(base64String);
return Uint8Array.from(globalThis.atob(base64UrlToBase64(base64String)), (x) => x.codePointAt(0));
}
var byteToHexLookupTable = Array.from({ length: 256 }, (_, index) => index.toString(16).padStart(2, "0"));
function uint8ArrayToHex(array) {
assertUint8Array(array);
let hexString = "";
for (let index = 0; index < array.length; index++) {
hexString += byteToHexLookupTable[array[index]];
}
return hexString;
}
var hexToDecimalLookupTable = {
0: 0,
1: 1,
2: 2,
3: 3,
4: 4,
5: 5,
6: 6,
7: 7,
8: 8,
9: 9,
a: 10,
b: 11,
c: 12,
d: 13,
e: 14,
f: 15,
A: 10,
B: 11,
C: 12,
D: 13,
E: 14,
F: 15
};
function hexToUint8Array(hexString) {
assertString(hexString);
if (hexString.length % 2 !== 0) {
throw new Error("Invalid Hex string length.");
}
const resultLength = hexString.length / 2;
const bytes = new Uint8Array(resultLength);
for (let index = 0; index < resultLength; index++) {
const highNibble = hexToDecimalLookupTable[hexString[index * 2]];
const lowNibble = hexToDecimalLookupTable[hexString[index * 2 + 1]];
if (highNibble === void 0 || lowNibble === void 0) {
throw new Error(`Invalid Hex character encountered at position ${index * 2}`);
}
bytes[index] = highNibble << 4 | lowNibble;
}
return bytes;
}
// node_modules/.pnpm/rfc4648@1.5.3/node_modules/rfc4648/lib/rfc4648.js
function parse(string, encoding, opts) {
var _opts$out;
if (opts === void 0) {
opts = {};
}
if (!encoding.codes) {
encoding.codes = {};
for (var i = 0; i < encoding.chars.length; ++i) {
encoding.codes[encoding.chars[i]] = i;
}
}
if (!opts.loose && string.length * encoding.bits & 7) {
throw new SyntaxError("Invalid padding");
}
var end = string.length;
while (string[end - 1] === "=") {
--end;
if (!opts.loose && !((string.length - end) * encoding.bits & 7)) {
throw new SyntaxError("Invalid padding");
}
}
var out = new ((_opts$out = opts.out) != null ? _opts$out : Uint8Array)(end * encoding.bits / 8 | 0);
var bits = 0;
var buffer = 0;
var written = 0;
for (var _i = 0; _i < end; ++_i) {
var value = encoding.codes[string[_i]];
if (value === void 0) {
throw new SyntaxError("Invalid character " + string[_i]);
}
buffer = buffer << encoding.bits | value;
bits += encoding.bits;
if (bits >= 8) {
bits -= 8;
out[written++] = 255 & buffer >> bits;
}
}
if (bits >= encoding.bits || 255 & buffer << 8 - bits) {
throw new SyntaxError("Unexpected end of data");
}
return out;
}
function stringify(data, encoding, opts) {
if (opts === void 0) {
opts = {};
}
var _opts = opts, _opts$pad = _opts.pad, pad = _opts$pad === void 0 ? true : _opts$pad;
var mask = (1 << encoding.bits) - 1;
var out = "";
var bits = 0;
var buffer = 0;
for (var i = 0; i < data.length; ++i) {
buffer = buffer << 8 | 255 & data[i];
bits += 8;
while (bits > encoding.bits) {
bits -= encoding.bits;
out += encoding.chars[mask & buffer >> bits];
}
}
if (bits) {
out += encoding.chars[mask & buffer << encoding.bits - bits];
}
if (pad) {
while (out.length * encoding.bits & 7) {
out += "=";
}
}
return out;
}
var base32Encoding = {
chars: "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567",
bits: 5
};
var base32 = {
parse: function parse$1(string, opts) {
if (opts === void 0) {
opts = {};
}
return parse(opts.loose ? string.toUpperCase().replace(/0/g, "O").replace(/1/g, "L").replace(/8/g, "B") : string, base32Encoding, opts);
},
stringify: function stringify$1(data, opts) {
return stringify(data, base32Encoding, opts);
}
};
// node_modules/.pnpm/@ctrl+magnet-link@4.0.2/node_modules/@ctrl/magnet-link/dist/src/bep53.js
function parseRange(range) {
const generateRange = (start2, end = start2) => Array.from({ length: end - start2 + 1 }, (_, idx) => idx + start2);
return range.reduce((acc, cur) => {
const r = cur.split("-").map((cur2) => parseInt(cur2, 10));
return acc.concat(generateRange(r[0], r[1]));
}, []);
}
// node_modules/.pnpm/@ctrl+magnet-link@4.0.2/node_modules/@ctrl/magnet-link/dist/src/index.js
var start = "magnet:?";
function magnetDecode(uri) {
const data = uri.substr(uri.indexOf(start) + start.length);
const params = data && data.length >= 0 ? data.split("&") : [];
const result = {};
params.forEach((param) => {
const keyval = param.split("=");
if (keyval.length !== 2) {
return;
}
const key = keyval[0];
const val = parseQueryParamValue(key, keyval[1]);
if (val === void 0) {
return;
}
const r = result[key];
if (!r) {
result[key] = val;
return result;
}
if (r && Array.isArray(r)) {
r.push(val);
return;
}
result[key] = [r, val];
return;
});
if (result.xt) {
let m;
const xts = Array.isArray(result.xt) ? result.xt : [result.xt];
xts.forEach((xt) => {
if (m = xt.match(/^urn:btih:(.{40})/)) {
result.infoHash = m[1].toLowerCase();
} else if (m = xt.match(/^urn:btih:(.{32})/)) {
const decodedStr = base32.parse(m[1]);
result.infoHash = uint8ArrayToHex(decodedStr);
} else if (m = xt.match(/^urn:btmh:1220(.{64})/)) {
result.infoHashV2 = m[1].toLowerCase();
}
});
}
if (result.xs) {
let m;
const xss = Array.isArray(result.xs) ? result.xs : [result.xs];
xss.forEach((xs) => {
if (m = /^urn:btpk:(.{64})/.exec(xs)) {
result.publicKey = m[1]?.toLowerCase();
}
});
}
if (result.infoHash) {
result.infoHashIntArray = hexToUint8Array(result.infoHash);
}
if (result.infoHashV2) {
result.infoHashV2IntArray = hexToUint8Array(result.infoHashV2);
}
if (result.publicKey) {
result.publicKeyIntArray = hexToUint8Array(result.publicKey);
}
if (result.dn) {
result.name = result.dn;
}
if (result.kt) {
result.keywords = result.kt;
}
if (typeof result.tr === "string") {
result.announce = [result.tr];
} else if (Array.isArray(result.tr)) {
result.announce = result.tr;
} else {
result.announce = [];
}
result.urlList = [];
if (typeof result.as === "string" || Array.isArray(result.as)) {
result.urlList = result.urlList.concat(result.as);
}
if (typeof result.ws === "string" || Array.isArray(result.ws)) {
result.urlList = result.urlList.concat(result.ws);
}
result.peerAddresses = [];
if (typeof result["x.pe"] === "string" || Array.isArray(result["x.pe"])) {
result.peerAddresses = result.peerAddresses.concat(result["x.pe"]);
}
result.announce = [...new Set(result.announce)].sort((a, b) => a.localeCompare(b));
result.urlList = [...new Set(result.urlList)].sort((a, b) => a.localeCompare(b));
result.peerAddresses = [...new Set(result.peerAddresses)];
return result;
}
function parseQueryParamValue(key, val) {
if (key === "dn") {
return decodeURIComponent(val).replace(/\+/g, " ");
}
if (key === "tr" || key === "xs" || key === "as" || key === "ws") {
return decodeURIComponent(val);
}
if (key === "kt") {
return decodeURIComponent(val).split("+");
}
if (key === "so") {
return parseRange(decodeURIComponent(val).split(","));
}
if (key === "ix") {
return Number(val);
}
return val;
}
// src/qbittorrent.ts
var import_torrent_file = require("@cao-mei-you-ren/torrent-file");
// node_modules/.pnpm/@ctrl+shared-torrent@6.0.0/node_modules/@ctrl/shared-torrent/dist/index.js
var TorrentState2;
(function(TorrentState3) {
TorrentState3["downloading"] = "downloading";
TorrentState3["seeding"] = "seeding";
TorrentState3["paused"] = "paused";
TorrentState3["queued"] = "queued";
TorrentState3["checking"] = "checking";
TorrentState3["warning"] = "warning";
TorrentState3["error"] = "error";
TorrentState3["unknown"] = "unknown";
})(TorrentState2 || (TorrentState2 = {}));
// src/normalizeTorrentData.ts
function normalizeTorrentData(torrent) {
let state = TorrentState2.unknown;
let stateMessage = "";
let { eta } = torrent;
switch (torrent.state) {
case "error" /* Error */:
state = TorrentState2.warning;
stateMessage = "qBittorrent is reporting an error";
break;
case "pausedDL" /* PausedDL */:
state = TorrentState2.paused;
break;
case "queuedDL" /* QueuedDL */:
case "checkingDL" /* CheckingDL */:
case "checkingUP" /* CheckingUP */:
state = TorrentState2.queued;
break;
case "metaDL" /* MetaDL */:
case "forcedDL" /* ForcedDL */:
case "ForcedMetaDL" /* ForcedMetaDL */:
case "downloading" /* Downloading */:
state = TorrentState2.downloading;
break;
case "allocating" /* Allocating */:
state = TorrentState2.queued;
break;
case "stalledDL" /* StalledDL */:
state = TorrentState2.warning;
stateMessage = "The download is stalled with no connection";
break;
case "pausedUP" /* PausedUP */:
case "uploading" /* Uploading */:
case "stalledUP" /* StalledUP */:
case "queuedUP" /* QueuedUP */:
case "forcedUP" /* ForcedUP */:
state = TorrentState2.seeding;
eta = 0;
break;
case "moving" /* Moving */:
case "queuedForChecking" /* QueuedForChecking */:
case "checkingResumeData" /* CheckingResumeData */:
state = TorrentState2.checking;
break;
case "unknown" /* Unknown */:
state = TorrentState2.error;
break;
case "missingFiles" /* MissingFiles */:
state = TorrentState2.error;
stateMessage = "The download is missing files";
break;
default:
break;
}
const isCompleted = torrent.progress === 1;
const result = {
id: torrent.hash,
name: torrent.name,
stateMessage,
state,
eta,
dateAdded: new Date(torrent.added_on * 1e3).toISOString(),
isCompleted,
progress: torrent.progress,
label: torrent.category,
tags: torrent.tags.split(", "),
dateCompleted: new Date(torrent.completion_on * 1e3).toISOString(),
savePath: torrent.save_path,
uploadSpeed: torrent.upspeed,
downloadSpeed: torrent.dlspeed,
queuePosition: torrent.priority,
connectedPeers: torrent.num_leechs,
connectedSeeds: torrent.num_seeds,
totalPeers: torrent.num_incomplete,
totalSeeds: torrent.num_complete,
totalSelected: torrent.size,
totalSize: torrent.total_size,
totalUploaded: torrent.uploaded,
totalDownloaded: torrent.downloaded,
ratio: torrent.ratio,
raw: torrent
};
return result;
}
// src/qbittorrent.ts
var defaults = {
baseUrl: "http://localhost:9091/",
path: "/api/v2",
username: "",
password: "",
timeout: 5e3
};
var QBittorrent = class {
config;
/**
* auth cookie
*/
_sid;
/**
* cookie expiration
*/
_exp;
constructor(options = {}) {
this.config = { ...defaults, ...options };
}
/**
* @deprecated
*/
async version() {
return this.getAppVersion();
}
/**
* Get application version
* {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-application-version}
*/
async getAppVersion() {
const res = await this.request(
"/app/version",
"GET",
void 0,
void 0,
void 0,
false
);
return res;
}
async getApiVersion() {
const res = await this.request(
"/app/webapiVersion",
"GET",
void 0,
void 0,
void 0,
false
);
return res;
}
/**
* {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-build-info}
*/
async getBuildInfo() {
const res = await this.request("/app/buildInfo", "GET");
return res;
}
async getTorrent(hash2) {
const torrentsResponse = await this.listTorrents({ hashes: hash2 });
const torrentData = torrentsResponse[0];
if (!torrentData) {
throw new Error("Torrent not found");
}
return normalizeTorrentData(torrentData);
}
/**
* {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-application-preferences}
*/
async getPreferences() {
const res = await this.request("/app/preferences", "GET");
return res;
}
/**
* {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#set-application-preferences}
*/
async setPreferences(preferences) {
await this.request(
"/app/setPreferences",
"POST",
void 0,
objToUrlSearchParams({
json: JSON.stringify(preferences)
})
);
return true;
}
/**
* {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-main-data}
*/
async getMainData(rid) {
const res = await this.request("/sync/maindata", "GET", { rid });
return res;
}
/**
* Torrents list
* @param hashes Filter by torrent hashes
* @param [filter] Filter torrent list
* @param category Get torrents with the given category (empty string means "without category"; no "category" parameter means "any category")
* @returns list of torrents
*/
async listTorrents({
hashes,
filter,
category,
sort,
offset,
reverse,
tag
} = {}) {
const params = {};
if (hashes) {
params.hashes = normalizeHashes(hashes);
}
if (filter) {
params.filter = filter;
}
if (category !== void 0) {
params.category = category;
}
if (tag !== void 0) {
params.tag = tag;
}
if (offset !== void 0) {
params.offset = `${offset}`;
}
if (sort) {
params.sort = sort;
}
if (reverse) {
params.reverse = JSON.stringify(reverse);
}
const res = await this.request("/torrents/info", "GET", params);
return res;
}
async getAllData() {
const listTorrents = await this.listTorrents();
const results = {
torrents: [],
labels: [],
raw: listTorrents
};
const labels = {};
for (const torrent of listTorrents) {
const torrentData = normalizeTorrentData(torrent);
results.torrents.push(torrentData);
if (torrentData.label) {
if (labels[torrentData.label] === void 0) {
labels[torrentData.label] = {
id: torrentData.label,
name: torrentData.label,
count: 1
};
} else {
labels[torrentData.label].count += 1;
}
}
}
return results;
}
/**
* {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-torrent-generic-properties}
*/
async torrentProperties(hash2) {
const res = await this.request("/torrents/properties", "GET", { hash: hash2 });
return res;
}
/**
* {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-torrent-trackers}
*/
async torrentTrackers(hash2) {
const res = await this.request("/torrents/trackers", "GET", { hash: hash2 });
return res;
}
/**
* {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-torrent-web-seeds}
*/
async torrentWebSeeds(hash2) {
const res = await this.request("/torrents/webseeds", "GET", { hash: hash2 });
return res;
}
async torrentFiles(hash2) {
const res = await this.request("/torrents/files", "GET", { hash: hash2 });
return res;
}
async setFilePriority(hash2, fileIds, priority) {
const res = await this.request("/torrents/filePrio", "GET", {
hash: hash2,
id: normalizeHashes(fileIds),
priority
});
return res;
}
/**
* {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-torrent-pieces-states}
*/
async torrentPieceStates(hash2) {
const res = await this.request("/torrents/pieceStates", "GET", { hash: hash2 });
return res;
}
/**
* Torrents piece hashes
* @returns an array of hashes (strings) of all pieces (in order) of a specific torrent
* {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-torrent-pieces-hashes}
*/
async torrentPieceHashes(hash2) {
const res = await this.request("/torrents/pieceHashes", "GET", { hash: hash2 });
return res;
}
/**
* {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#set-torrent-location}
*/
async setTorrentLocation(hashes, location) {
const data = {
location,
hashes: normalizeHashes(hashes)
};
await this.request("/torrents/setLocation", "POST", void 0, objToUrlSearchParams(data));
return true;
}
/**
* {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#set-torrent-name}
*/
async setTorrentName(hash2, name) {
const data = { hash: hash2, name };
await this.request("/torrents/rename", "POST", void 0, objToUrlSearchParams(data));
return true;
}
/**
* {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-all-tags}
*/
async getTags() {
const res = await this.request("/torrents/tags", "GET");
return res;
}
/**
* @param tags comma separated list
* {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#create-tags}
*/
async createTags(tags) {
const data = { tags };
await this.request(
"/torrents/createTags",
"POST",
void 0,
objToUrlSearchParams(data),
void 0,
false
);
return true;
}
/**
* @param tags comma separated list
* {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#delete-tags}
*/
async deleteTags(tags) {
const data = { tags };
await this.request(
"/torrents/deleteTags",
"POST",
void 0,
objToUrlSearchParams(data),
void 0,
false
);
return true;
}
/**
* {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-all-categories}
*/
async getCategories() {
const res = await this.request("/torrents/categories", "GET");
return res;
}
/**
* {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#add-new-category}
*/
async createCategory(category, savePath = "") {
const data = { category, savePath };
await this.request(
"/torrents/createCategory",
"POST",
void 0,
objToUrlSearchParams(data),
void 0,
false
);
return true;
}
/**
* {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#edit-category}
*/
async editCategory(category, savePath = "") {
const data = { category, savePath };
await this.request(
"/torrents/editCategory",
"POST",
void 0,
objToUrlSearchParams(data),
void 0,
false
);
return true;
}
/**
* {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#remove-categories}
*/
async removeCategory(categories) {
const data = { categories };
await this.request(
"/torrents/removeCategories",
"POST",
void 0,
objToUrlSearchParams(data),
void 0,
false
);
return true;
}
/**
* {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#add-torrent-tags}
*/
async addTorrentTags(hashes, tags) {
const data = { hashes: normalizeHashes(hashes), tags };
await this.request(
"/torrents/addTags",
"POST",
void 0,
objToUrlSearchParams(data),
void 0,
false
);
return true;
}
/**
* if tags are not passed, removes all tags
* {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#remove-torrent-tags}
*/
async removeTorrentTags(hashes, tags) {
const data = { hashes: normalizeHashes(hashes) };
if (tags) {
data.tags = tags;
}
await this.request(
"/torrents/removeTags",
"POST",
void 0,
objToUrlSearchParams(data),
void 0,
false
);
return true;
}
/**
* helper function to remove torrent category
*/
async resetTorrentCategory(hashes) {
return this.setTorrentCategory(hashes);
}
/**
* {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#set-torrent-category}
*/
async setTorrentCategory(hashes, category = "") {
const data = {
hashes: normalizeHashes(hashes),
category
};
await this.request("/torrents/setCategory", "POST", void 0, objToUrlSearchParams(data));
return true;
}
/**
* {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#pause-torrents}
*/
async pauseTorrent(hashes) {
const data = { hashes: normalizeHashes(hashes) };
await this.request("/torrents/pause", "POST", void 0, objToUrlSearchParams(data));
return true;
}
/**
* {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#resume-torrents}
*/
async resumeTorrent(hashes) {
const data = { hashes: normalizeHashes(hashes) };
await this.request("/torrents/resume", "POST", void 0, objToUrlSearchParams(data));
return true;
}
/**
* {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#delete-torrents}
*/
async removeTorrent(hashes, deleteFiles = true) {
const data = {
hashes: normalizeHashes(hashes),
deleteFiles
};
await this.request("/torrents/delete", "POST", void 0, objToUrlSearchParams(data));
return true;
}
/**
* {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#recheck-torrents}
*/
async recheckTorrent(hashes) {
const data = { hashes: normalizeHashes(hashes) };
await this.request("/torrents/recheck", "POST", void 0, objToUrlSearchParams(data));
return true;
}
/**
* {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#reannounce-torrents}
*/
async reannounceTorrent(hashes) {
const data = { hashes: normalizeHashes(hashes) };
await this.request("/torrents/reannounce", "POST", void 0, objToUrlSearchParams(data));
return true;
}
async addTorrent(torrent, options = {}) {
const form = new import_node_fetch_native.FormData();
if (options.filename) {
delete options.filename;
}
const type = { type: "application/x-bittorrent" };
if (typeof torrent === "string") {
form.set("file", new File([base64ToUint8Array(torrent)], "file.torrent", type));
} else {
const file = new File([torrent], options.filename ?? "torrent", type);
form.set("file", file);
}
if (options) {
if (options.useAutoTMM === "true") {
options.savepath = "";
} else {
options.useAutoTMM = "false";
}
for (const [key, value] of Object.entries(options)) {
form.append(key, `${value}`);
}
}
const res = await this.request(
"/torrents/add",
"POST",
void 0,
form,
void 0,
false
);
if (res === "Fails.") {
throw new Error("Failed to add torrent");
}
return true;
}
async normalizedAddTorrent(torrent, options = {}) {
const torrentOptions = {};
if (options.startPaused) {
torrentOptions.paused = "true";
}
if (options.label) {
torrentOptions.category = options.label;
}
let torrentHash;
if (typeof torrent === "string" && torrent.startsWith("magnet:")) {
torrentHash = magnetDecode(torrent).infoHash;
if (!torrentHash) {
throw new Error("Magnet did not contain hash");
}
await this.addMagnet(torrent, torrentOptions);
} else {
if (!isUint8Array(torrent)) {
torrent = stringToUint8Array(torrent);
}
torrentHash = await (0, import_torrent_file.hash)(torrent);
await this.addTorrent(torrent, torrentOptions);
}
return this.getTorrent(torrentHash);
}
/**
* @param hash Hash for desired torrent
* @param id id of the file to be renamed
* @param name new name to be assigned to the file
*/
async renameFile(hash2, id, name) {
const form = new import_node_fetch_native.FormData();
form.append("hash", hash2);
form.append("id", id.toString());
form.append("name", name);
await this.request("/torrents/renameFile", "POST", void 0, void 0, form, false);
return true;
}
/**
* {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#rename-folder}
*/
async renameFolder(hash2, oldPath, newPath) {
const form = new import_node_fetch_native.FormData();
form.append("hash", hash2);
form.append("oldPath", oldPath);
form.append("newPath", newPath);
await this.request("/torrents/renameFolder", "POST", void 0, void 0, form, false);
return true;
}
/**
* @param urls URLs separated with newlines
* @param options
*/
async addMagnet(urls, options = {}) {
const form = new import_node_fetch_native.FormData();
form.append("urls", urls);
if (options) {
if (options.useAutoTMM === "true") {
options.savepath = "";
} else {
options.useAutoTMM = "false";
}
for (const [key, value] of Object.entries(options)) {
form.append(key, `${value}`);
}
}
const res = await this.request(
"/torrents/add",
"POST",
void 0,
form,
void 0,
false
);
if (res === "Fails.") {
throw new Error("Failed to add torrent");
}
return true;
}
/**
* {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#add-trackers-to-torrent}
*/
async addTrackers(hash2, urls) {
const data = { hash: hash2, urls };
await this.request("/torrents/addTrackers", "POST", void 0, objToUrlSearchParams(data));
return true;
}
/**
* {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#edit-trackers}
*/
async editTrackers(hash2, origUrl, newUrl) {
const data = { hash: hash2, origUrl, newUrl };
await this.request("/torrents/editTrackers", "POST", void 0, objToUrlSearchParams(data));
return true;
}
/**
* {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#remove-trackers}
*/
async removeTrackers(hash2, urls) {
const data = { hash: hash2, urls };
await this.request("/torrents/removeTrackers", "POST", void 0, objToUrlSearchParams(data));
return true;
}
/**
* {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#increase-torrent-priority}
*/
async queueUp(hashes) {
const data = { hashes: normalizeHashes(hashes) };
await this.request("/torrents/increasePrio", "POST", void 0, objToUrlSearchParams(data));
return true;
}
/**
* {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#decrease-torrent-priority}
*/
async queueDown(hashes) {
const data = { hashes: normalizeHashes(hashes) };
await this.request("/torrents/decreasePrio", "POST", void 0, objToUrlSearchParams(data));
return true;
}
/**
* {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#maximal-torrent-priority}
*/
async topPriority(hashes) {
const data = { hashes: normalizeHashes(hashes) };
await this.request("/torrents/topPrio", "POST", void 0, objToUrlSearchParams(data));
return true;
}
/**
* {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#minimal-torrent-priority}
*/
async bottomPriority(hashes) {
const data = { hashes: normalizeHashes(hashes) };
await this.request("/torrents/bottomPrio", "POST", void 0, objToUrlSearchParams(data));
return true;
}
/**
* {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-torrent-peers-data}
* @param rid - Response ID. If not provided, rid=0 will be assumed. If the given rid is
* different from the one of last server reply, full_update will be true (see the server reply details for more info)
*/
async torrentPeers(hash2, rid) {
const params = { hash: hash2 };
if (rid) {
params.rid = rid;
}
const res = await this.request("/sync/torrentPeers", "GET", params);
return res;
}
/**
* {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#login}
*/
async login() {
const url = (0, import_ufo.joinURL)(this.config.baseUrl, this.config.path, "/auth/login");
const res = await import_ofetch.ofetch.raw(url, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: new URLSearchParams({
username: this.config.username ?? "",
password: this.config.password ?? ""
}),
redirect: "manual",
retry: false,
timeout: this.config.timeout,
...this.config.agent ? { agent: this.config.agent } : {}
});
if (!res.headers.get("set-cookie")?.length) {
throw new Error("Cookie not found. Auth Failed.");
}
const cookie = (0, import_cookie.parse)(res.headers.get("set-cookie"));
if (!cookie.SID) {
throw new Error("Invalid cookie");
}
this._sid = cookie.SID;
const expires = cookie.Expires ?? cookie.expires;
const maxAge = cookie["Max-Age"] ?? cookie["max-age"];
this._exp = expires ? new Date(expires) : maxAge ? new Date(Number(maxAge) * 1e3) : (
// Default expiration 1 hour
new Date(Date.now() + 36e5)
);
return true;
}
logout() {
this._sid = void 0;
this._exp = void 0;
return true;
}
// eslint-disable-next-line max-params
async request(path, method, params, body, headers = {}, json = true) {
if (!this._sid || !this._exp || this._exp.getTime() < (/* @__PURE__ */ new Date()).getTime()) {
const authed = await this.login();
if (!authed) {
throw new Error("Auth Failed");
}
}
const url = (0, import_ufo.joinURL)(this.config.baseUrl, this.config.path, path);
const res = await (0, import_ofetch.ofetch)(url, {
method,
headers: {
Cookie: `SID=${this._sid ?? ""}`,
...headers
},
body,
params,
// allow proxy agent
retry: 0,
timeout: this.config.timeout,
responseType: json ? "json" : "text",
// @ts-expect-error for some reason agent is not in the type
agent: this.config.agent
});
return res;
}
};
function normalizeHashes(hashes) {
if (Array.isArray(hashes)) {
return hashes.join("|");
}
return hashes;
}
function objToUrlSearchParams(obj) {
const params = new URLSearchParams();
for (const [key, value] of Object.entries(obj)) {
params.append(key, value.toString());
}
return params;
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
QBittorrent,
TorrentFilePriority,
TorrentPieceState,
TorrentState,
TorrentTrackerStatus
});