@ctrl/qbittorrent
Advanced tools
Comparing version 9.1.0 to 9.2.0
@@ -1,14 +0,32 @@ | ||
import type { AddTorrentOptions as NormalizedAddTorrentOptions, AllClientData, NormalizedTorrent, TorrentClient, TorrentSettings } from '@ctrl/shared-torrent'; | ||
import type { Jsonify } from 'type-fest'; | ||
import type { AddTorrentOptions as NormalizedAddTorrentOptions, AllClientData, NormalizedTorrent, TorrentClient, TorrentClientConfig, TorrentClientState } from '@ctrl/shared-torrent'; | ||
import type { AddMagnetOptions, AddTorrentOptions, BuildInfo, Preferences, Torrent, TorrentCategories, TorrentFile, TorrentFilePriority, TorrentFilters, TorrentPeersResponse, TorrentPieceState, TorrentProperties, TorrentTrackers, WebSeed } from './types.js'; | ||
interface QBittorrentState extends TorrentClientState { | ||
auth?: { | ||
/** | ||
* auth cookie | ||
*/ | ||
sid: string; | ||
/** | ||
* cookie expiration | ||
*/ | ||
expires: Date; | ||
}; | ||
version?: { | ||
version: string; | ||
isVersion5OrHigher: boolean; | ||
}; | ||
} | ||
export declare class QBittorrent implements TorrentClient { | ||
config: TorrentSettings; | ||
/** | ||
* auth cookie | ||
* Create a new QBittorrent client from a state | ||
*/ | ||
private _sid?; | ||
static createFromState(config: Readonly<TorrentClientConfig>, state: Readonly<Jsonify<QBittorrentState>>): QBittorrent; | ||
config: TorrentClientConfig; | ||
state: QBittorrentState; | ||
constructor(options?: Partial<TorrentClientConfig>); | ||
/** | ||
* cookie expiration | ||
* Export the state of the client as JSON | ||
*/ | ||
private _exp?; | ||
constructor(options?: Partial<TorrentSettings>); | ||
exportState(): Jsonify<QBittorrentState>; | ||
/** | ||
@@ -215,2 +233,4 @@ * @deprecated | ||
request<T>(path: string, method: 'GET' | 'POST', params?: Record<string, string | number>, body?: URLSearchParams | FormData, headers?: Record<string, string>, isJson?: boolean): Promise<T>; | ||
private checkVersion; | ||
} | ||
export {}; |
@@ -17,11 +17,15 @@ import { parse as cookieParse } from 'cookie'; | ||
export class QBittorrent { | ||
config; | ||
/** | ||
* auth cookie | ||
* Create a new QBittorrent client from a state | ||
*/ | ||
_sid; | ||
/** | ||
* cookie expiration | ||
*/ | ||
_exp; | ||
static createFromState(config, state) { | ||
const client = new QBittorrent(config); | ||
client.state = { | ||
...state, | ||
auth: state.auth ? { ...state.auth, expires: new Date(state.auth.expires) } : undefined, | ||
}; | ||
return client; | ||
} | ||
config; | ||
state = {}; | ||
constructor(options = {}) { | ||
@@ -31,2 +35,8 @@ this.config = { ...defaults, ...options }; | ||
/** | ||
* Export the state of the client as JSON | ||
*/ | ||
exportState() { | ||
return JSON.parse(JSON.stringify(this.state)); | ||
} | ||
/** | ||
* @deprecated | ||
@@ -312,4 +322,5 @@ */ | ||
async pauseTorrent(hashes) { | ||
const endpoint = this.state.version?.isVersion5OrHigher ? '/torrents/stop' : '/torrents/pause'; | ||
const data = { hashes: normalizeHashes(hashes) }; | ||
await this.request('/torrents/pause', 'POST', undefined, objToUrlSearchParams(data)); | ||
await this.request(endpoint, 'POST', undefined, objToUrlSearchParams(data)); | ||
return true; | ||
@@ -321,4 +332,7 @@ } | ||
async resumeTorrent(hashes) { | ||
const endpoint = this.state.version?.isVersion5OrHigher | ||
? '/torrents/start' | ||
: '/torrents/resume'; | ||
const data = { hashes: normalizeHashes(hashes) }; | ||
await this.request('/torrents/resume', 'POST', undefined, objToUrlSearchParams(data)); | ||
await this.request(endpoint, 'POST', undefined, objToUrlSearchParams(data)); | ||
return true; | ||
@@ -369,2 +383,7 @@ } | ||
if (options) { | ||
// Handle version-specific paused/stopped parameter | ||
if (this.state.version?.isVersion5OrHigher && 'paused' in options) { | ||
form.append('stopped', options.paused); | ||
delete options.paused; | ||
} | ||
// disable savepath when autoTMM is defined | ||
@@ -444,2 +463,7 @@ if (options.useAutoTMM === 'true') { | ||
if (options) { | ||
// Handle version-specific paused/stopped parameter | ||
if (this.state.version?.isVersion5OrHigher && 'paused' in options) { | ||
form.append('stopped', options.paused); | ||
delete options.paused; | ||
} | ||
// disable savepath when autoTMM is defined | ||
@@ -535,3 +559,3 @@ if (options.useAutoTMM === 'true') { | ||
async login() { | ||
const url = joinURL(this.config.baseUrl, this.config.path, '/auth/login'); | ||
const url = joinURL(this.config.baseUrl, this.config.path ?? '', '/auth/login'); | ||
const res = await ofetch.raw(url, { | ||
@@ -554,22 +578,22 @@ method: 'POST', | ||
} | ||
const cookie = cookieParse(res.headers.get('set-cookie')); | ||
const cookie = cookieParse(res.headers.get('set-cookie') ?? ''); | ||
if (!cookie.SID) { | ||
throw new Error('Invalid cookie'); | ||
} | ||
this._sid = cookie.SID; | ||
// Not sure if it might be lowercase | ||
const expires = cookie.Expires ?? cookie.expires; | ||
// Assumed to be in seconds | ||
const maxAge = cookie['Max-Age'] ?? cookie['max-age']; | ||
this._exp = expires | ||
? new Date(expires) | ||
: maxAge | ||
? new Date(Number(maxAge) * 1000) | ||
: // Default expiration 1 hour | ||
new Date(Date.now() + 3600000); | ||
this.state.auth = { | ||
sid: cookie.SID, | ||
expires: expires | ||
? new Date(expires) | ||
: maxAge | ||
? new Date(Number(maxAge) * 1000) | ||
: new Date(Date.now() + 3600000), | ||
}; | ||
// Check version after successful login | ||
await this.checkVersion(); | ||
return true; | ||
} | ||
logout() { | ||
this._sid = undefined; | ||
this._exp = undefined; | ||
delete this.state.auth; | ||
return true; | ||
@@ -579,3 +603,5 @@ } | ||
async request(path, method, params, body, headers = {}, isJson = true) { | ||
if (!this._sid || !this._exp || this._exp.getTime() < new Date().getTime()) { | ||
if (!this.state.auth?.sid || | ||
!this.state.auth.expires || | ||
this.state.auth.expires.getTime() < new Date().getTime()) { | ||
const authed = await this.login(); | ||
@@ -586,7 +612,7 @@ if (!authed) { | ||
} | ||
const url = joinURL(this.config.baseUrl, this.config.path, path); | ||
const url = joinURL(this.config.baseUrl, this.config.path ?? '', path); | ||
const res = await ofetch(url, { | ||
method, | ||
headers: { | ||
Cookie: `SID=${this._sid ?? ''}`, | ||
Cookie: `SID=${this.state.auth.sid ?? ''}`, | ||
...headers, | ||
@@ -605,2 +631,13 @@ }, | ||
} | ||
async checkVersion() { | ||
if (!this.state.version?.version) { | ||
const newVersion = await this.getAppVersion(); | ||
// Remove potential 'v' prefix and any extra info after version number | ||
const cleanVersion = newVersion.replace(/^v/, '').split('-')[0]; | ||
this.state.version = { | ||
version: newVersion, | ||
isVersion5OrHigher: cleanVersion === '5.0.0' || isGreater(cleanVersion, '5.0.0'), | ||
}; | ||
} | ||
} | ||
} | ||
@@ -624,1 +661,4 @@ /** | ||
} | ||
function isGreater(a, b) { | ||
return a.localeCompare(b, undefined, { numeric: true }) === 1; | ||
} |
@@ -552,3 +552,4 @@ export interface BuildInfo { | ||
*/ | ||
paused: TrueFalseStr; | ||
paused?: TrueFalseStr; | ||
stopped?: TrueFalseStr; | ||
/** | ||
@@ -610,3 +611,4 @@ * Control filesystem structure for content (added in Web API v2.7) | ||
*/ | ||
paused: TrueFalseStr; | ||
paused?: TrueFalseStr; | ||
stopped?: TrueFalseStr; | ||
/** | ||
@@ -613,0 +615,0 @@ * Create the root folder. Possible values are true, false, unset (default) |
{ | ||
"name": "@ctrl/qbittorrent", | ||
"version": "9.1.0", | ||
"version": "9.2.0", | ||
"description": "TypeScript api wrapper for qbittorrent using got", | ||
@@ -40,7 +40,8 @@ "author": "Scott Cooper <scttcper@gmail.com>", | ||
"@ctrl/magnet-link": "^4.0.2", | ||
"@ctrl/shared-torrent": "^6.1.0", | ||
"@ctrl/shared-torrent": "^6.2.1", | ||
"@ctrl/torrent-file": "^4.1.0", | ||
"cookie": "^1.0.1", | ||
"cookie": "^1.0.2", | ||
"node-fetch-native": "^1.6.4", | ||
"ofetch": "^1.4.1", | ||
"type-fest": "^4.30.2", | ||
"ufo": "^1.5.4", | ||
@@ -51,12 +52,12 @@ "uint8array-extras": "^1.4.0" | ||
"@biomejs/biome": "1.9.4", | ||
"@ctrl/eslint-config-biome": "4.2.11", | ||
"@eslint/compat": "^1.2.2", | ||
"@sindresorhus/tsconfig": "6.0.0", | ||
"@ctrl/eslint-config-biome": "4.3.1", | ||
"@eslint/compat": "^1.2.4", | ||
"@sindresorhus/tsconfig": "7.0.0", | ||
"@types/cookie": "1.0.0", | ||
"@types/node": "22.9.0", | ||
"@vitest/coverage-v8": "2.1.4", | ||
"@types/node": "22.10.2", | ||
"@vitest/coverage-v8": "2.1.8", | ||
"p-wait-for": "5.0.2", | ||
"typedoc": "0.26.11", | ||
"typescript": "5.6.3", | ||
"vitest": "2.1.4" | ||
"typedoc": "0.27.5", | ||
"typescript": "5.7.2", | ||
"vitest": "2.1.8" | ||
}, | ||
@@ -63,0 +64,0 @@ "release": { |
@@ -101,2 +101,11 @@ # qBittorrent [![npm](https://badgen.net/npm/v/@ctrl/qbittorrent)](https://www.npmjs.com/package/@ctrl/qbittorrent) [![coverage](https://badgen.net/codecov/c/github/scttcper/qbittorrent)](https://codecov.io/gh/scttcper/qbittorrent) | ||
##### export and create from state | ||
If you're shutting down the server often (serverless?) you can export the state | ||
```ts | ||
const state = client.exportState() | ||
const client = QBittorrent.createFromState(config, state); | ||
``` | ||
### See Also | ||
@@ -103,0 +112,0 @@ |
82006
2398
133
9
+ Addedtype-fest@^4.30.2
+ Addedtype-fest@4.33.0(transitive)
Updated@ctrl/shared-torrent@^6.2.1
Updatedcookie@^1.0.2