UNPKG

@cao-mei-you-ren/qbittorrent

Version:

TypeScript api wrapper for qbittorrent using got

1,159 lines (1,147 loc) 35.9 kB
"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 });