@ctrl/sabnzbd 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +207 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.js +4 -0
- package/dist/src/normalizeUsenetData.d.ts +7 -0
- package/dist/src/normalizeUsenetData.js +231 -0
- package/dist/src/sabnzbd.d.ts +328 -0
- package/dist/src/sabnzbd.js +774 -0
- package/dist/src/types.d.ts +444 -0
- package/dist/src/types.js +1 -0
- package/package.json +61 -0
|
@@ -0,0 +1,774 @@
|
|
|
1
|
+
import { UsenetNotFoundError, UsenetPostProcess, UsenetPriority, } from '@ctrl/shared-usenet';
|
|
2
|
+
import { FormData } from 'node-fetch-native';
|
|
3
|
+
import { ofetch } from 'ofetch';
|
|
4
|
+
import { joinURL } from 'ufo';
|
|
5
|
+
import { normalizeSabHistoryItem, normalizeSabJob, normalizeSabStatus, normalizedPriorityToSab, } from './normalizeUsenetData.js';
|
|
6
|
+
const defaults = {
|
|
7
|
+
baseUrl: 'http://localhost:8080/',
|
|
8
|
+
path: '/api',
|
|
9
|
+
username: '',
|
|
10
|
+
password: '',
|
|
11
|
+
timeout: 5000,
|
|
12
|
+
};
|
|
13
|
+
const addQueuePollAttempts = 40;
|
|
14
|
+
const addQueuePollIntervalMs = 250;
|
|
15
|
+
function toQueryStringValue(value) {
|
|
16
|
+
if (value === undefined) {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
if (typeof value === 'boolean') {
|
|
20
|
+
return value ? '1' : '0';
|
|
21
|
+
}
|
|
22
|
+
return `${value}`;
|
|
23
|
+
}
|
|
24
|
+
function toCommaList(value) {
|
|
25
|
+
if (value === undefined) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
return Array.isArray(value) ? value.join(',') : `${value}`;
|
|
29
|
+
}
|
|
30
|
+
function normalizePostProcess(value) {
|
|
31
|
+
switch (value) {
|
|
32
|
+
case undefined:
|
|
33
|
+
case UsenetPostProcess.default: {
|
|
34
|
+
return -1;
|
|
35
|
+
}
|
|
36
|
+
case UsenetPostProcess.none: {
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
case UsenetPostProcess.repair: {
|
|
40
|
+
return 1;
|
|
41
|
+
}
|
|
42
|
+
case UsenetPostProcess.repairUnpack: {
|
|
43
|
+
return 2;
|
|
44
|
+
}
|
|
45
|
+
case UsenetPostProcess.repairUnpackDelete: {
|
|
46
|
+
return 3;
|
|
47
|
+
}
|
|
48
|
+
default: {
|
|
49
|
+
if (!Number.isInteger(value)) {
|
|
50
|
+
throw new TypeError(`SAB post-process value must be an integer, received: ${value}`);
|
|
51
|
+
}
|
|
52
|
+
if (value < -1 || value > 3) {
|
|
53
|
+
throw new RangeError(`Unsupported SAB post-process value: ${value}`);
|
|
54
|
+
}
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function encodeNzbFile(file) {
|
|
60
|
+
if (typeof file === 'string') {
|
|
61
|
+
return new TextEncoder().encode(file);
|
|
62
|
+
}
|
|
63
|
+
return file;
|
|
64
|
+
}
|
|
65
|
+
async function sleep(milliseconds) {
|
|
66
|
+
await new Promise(resolve => {
|
|
67
|
+
setTimeout(resolve, milliseconds);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
function getAddedJobId(response) {
|
|
71
|
+
const [id] = response.nzo_ids;
|
|
72
|
+
if (!response.status || !id) {
|
|
73
|
+
throw new Error(response.error ?? 'SABnzbd did not return a queue id');
|
|
74
|
+
}
|
|
75
|
+
return id;
|
|
76
|
+
}
|
|
77
|
+
function coercePriority(priority) {
|
|
78
|
+
if (priority === undefined) {
|
|
79
|
+
return UsenetPriority.default;
|
|
80
|
+
}
|
|
81
|
+
if (typeof priority !== 'number') {
|
|
82
|
+
return priority;
|
|
83
|
+
}
|
|
84
|
+
if (!Number.isInteger(priority)) {
|
|
85
|
+
throw new TypeError(`SAB priority must be an integer, received: ${priority}`);
|
|
86
|
+
}
|
|
87
|
+
switch (priority) {
|
|
88
|
+
case -100: {
|
|
89
|
+
return UsenetPriority.default;
|
|
90
|
+
}
|
|
91
|
+
case -4: {
|
|
92
|
+
return UsenetPriority.stopped;
|
|
93
|
+
}
|
|
94
|
+
case -3: {
|
|
95
|
+
return UsenetPriority.duplicate;
|
|
96
|
+
}
|
|
97
|
+
case -2: {
|
|
98
|
+
return UsenetPriority.paused;
|
|
99
|
+
}
|
|
100
|
+
case -1: {
|
|
101
|
+
return UsenetPriority.low;
|
|
102
|
+
}
|
|
103
|
+
case 0: {
|
|
104
|
+
return UsenetPriority.normal;
|
|
105
|
+
}
|
|
106
|
+
case 1: {
|
|
107
|
+
return UsenetPriority.high;
|
|
108
|
+
}
|
|
109
|
+
case 2: {
|
|
110
|
+
return UsenetPriority.force;
|
|
111
|
+
}
|
|
112
|
+
default: {
|
|
113
|
+
throw new RangeError(`Unsupported SAB priority value: ${priority}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function isUsenetNotFoundError(error) {
|
|
118
|
+
return error instanceof UsenetNotFoundError;
|
|
119
|
+
}
|
|
120
|
+
export class Sabnzbd {
|
|
121
|
+
static createFromState(config, state) {
|
|
122
|
+
const client = new Sabnzbd(config);
|
|
123
|
+
client.state = { ...state };
|
|
124
|
+
return client;
|
|
125
|
+
}
|
|
126
|
+
config;
|
|
127
|
+
state = {};
|
|
128
|
+
constructor(options = {}) {
|
|
129
|
+
this.config = { ...defaults, ...options };
|
|
130
|
+
}
|
|
131
|
+
exportState() {
|
|
132
|
+
return JSON.parse(JSON.stringify(this.state));
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Verifies configured credentials against the SABnzbd API.
|
|
136
|
+
*
|
|
137
|
+
* Calls SABnzbd `mode=auth`.
|
|
138
|
+
*
|
|
139
|
+
* @returns The raw SABnzbd authentication response.
|
|
140
|
+
*/
|
|
141
|
+
async auth() {
|
|
142
|
+
const response = await this.request({ mode: 'auth' });
|
|
143
|
+
this.state.auth = {
|
|
144
|
+
apiKey: this.config.apiKey,
|
|
145
|
+
nzbKey: this.config.nzbKey,
|
|
146
|
+
};
|
|
147
|
+
return response;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Reads the SABnzbd version string.
|
|
151
|
+
*
|
|
152
|
+
* Calls SABnzbd `mode=version`.
|
|
153
|
+
*
|
|
154
|
+
* @returns The SABnzbd version.
|
|
155
|
+
*/
|
|
156
|
+
async getVersion() {
|
|
157
|
+
const response = await this.request({ mode: 'version' });
|
|
158
|
+
this.state.version = { version: response.version };
|
|
159
|
+
return response.version;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Loads full server and queue status details from SABnzbd.
|
|
163
|
+
*
|
|
164
|
+
* Calls SABnzbd `mode=fullstatus` with `skip_dashboard=1`.
|
|
165
|
+
*
|
|
166
|
+
* @returns The full status payload.
|
|
167
|
+
*/
|
|
168
|
+
async getFullStatus() {
|
|
169
|
+
return this.request({ mode: 'fullstatus', skip_dashboard: '1' });
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Retrieves the current SABnzbd warning list.
|
|
173
|
+
*
|
|
174
|
+
* Calls SABnzbd `mode=warnings`.
|
|
175
|
+
*
|
|
176
|
+
* @returns All active warnings.
|
|
177
|
+
*/
|
|
178
|
+
async getWarnings() {
|
|
179
|
+
const response = await this.request({ mode: 'warnings' });
|
|
180
|
+
return response.warnings;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Retrieves per-server traffic totals from SABnzbd.
|
|
184
|
+
*
|
|
185
|
+
* Calls SABnzbd `mode=server_stats`.
|
|
186
|
+
*
|
|
187
|
+
* @returns Aggregate and per-server transfer statistics.
|
|
188
|
+
*/
|
|
189
|
+
async getServerStats() {
|
|
190
|
+
return this.request({ mode: 'server_stats' });
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Lists queue entries, optionally filtered and paged.
|
|
194
|
+
*
|
|
195
|
+
* Calls SABnzbd `mode=queue`.
|
|
196
|
+
*
|
|
197
|
+
* @param query Optional queue filters and pagination controls.
|
|
198
|
+
* @returns The raw queue payload including `slots`.
|
|
199
|
+
*/
|
|
200
|
+
async listQueue(query = {}) {
|
|
201
|
+
const response = await this.request({
|
|
202
|
+
mode: 'queue',
|
|
203
|
+
start: toQueryStringValue(query.start),
|
|
204
|
+
limit: toQueryStringValue(query.limit),
|
|
205
|
+
search: query.search,
|
|
206
|
+
category: toCommaList(query.category),
|
|
207
|
+
priority: toCommaList(query.priority),
|
|
208
|
+
status: toCommaList(query.status),
|
|
209
|
+
nzo_ids: toCommaList(query.nzoIds),
|
|
210
|
+
});
|
|
211
|
+
return response.queue;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Lists history entries, optionally filtered and paged.
|
|
215
|
+
*
|
|
216
|
+
* Calls SABnzbd `mode=history`.
|
|
217
|
+
*
|
|
218
|
+
* @param query Optional history filters, pagination, and archive controls.
|
|
219
|
+
* @returns The raw history payload including `slots`.
|
|
220
|
+
*/
|
|
221
|
+
async listHistory(query = {}) {
|
|
222
|
+
const response = await this.request({
|
|
223
|
+
mode: 'history',
|
|
224
|
+
start: toQueryStringValue(query.start),
|
|
225
|
+
limit: toQueryStringValue(query.limit),
|
|
226
|
+
search: query.search,
|
|
227
|
+
category: toCommaList(query.category),
|
|
228
|
+
status: toCommaList(query.status),
|
|
229
|
+
nzo_ids: toCommaList(query.nzoIds),
|
|
230
|
+
failed_only: toQueryStringValue(query.failedOnly),
|
|
231
|
+
archive: toQueryStringValue(query.archived),
|
|
232
|
+
last_history_update: toQueryStringValue(query.lastHistoryUpdate),
|
|
233
|
+
});
|
|
234
|
+
return response.history;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Retrieves configured SABnzbd categories.
|
|
238
|
+
*
|
|
239
|
+
* Calls SABnzbd `mode=get_cats`.
|
|
240
|
+
*
|
|
241
|
+
* @returns Categories normalized to shared `Category` objects.
|
|
242
|
+
*/
|
|
243
|
+
async getCategories() {
|
|
244
|
+
const response = await this.request({ mode: 'get_cats' });
|
|
245
|
+
return response.categories.map(category => ({
|
|
246
|
+
id: category,
|
|
247
|
+
name: category,
|
|
248
|
+
}));
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Retrieves configured SABnzbd post-processing scripts.
|
|
252
|
+
*
|
|
253
|
+
* Calls SABnzbd `mode=get_scripts`.
|
|
254
|
+
*
|
|
255
|
+
* @returns Scripts normalized to shared `Script` objects.
|
|
256
|
+
*/
|
|
257
|
+
async getScripts() {
|
|
258
|
+
const response = await this.request({ mode: 'get_scripts' });
|
|
259
|
+
return response.scripts.map(script => ({
|
|
260
|
+
id: script,
|
|
261
|
+
name: script,
|
|
262
|
+
}));
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Pauses the global download queue.
|
|
266
|
+
*
|
|
267
|
+
* Calls SABnzbd `mode=pause`.
|
|
268
|
+
*
|
|
269
|
+
* @returns `true` when SABnzbd accepts the pause command.
|
|
270
|
+
*/
|
|
271
|
+
async pauseQueue() {
|
|
272
|
+
await this.request({ mode: 'pause' });
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Resumes the global download queue.
|
|
277
|
+
*
|
|
278
|
+
* Calls SABnzbd `mode=resume`.
|
|
279
|
+
*
|
|
280
|
+
* @returns `true` when SABnzbd accepts the resume command.
|
|
281
|
+
*/
|
|
282
|
+
async resumeQueue() {
|
|
283
|
+
await this.request({ mode: 'resume' });
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Requests SABnzbd shutdown.
|
|
288
|
+
*
|
|
289
|
+
* Calls SABnzbd `mode=shutdown`.
|
|
290
|
+
*
|
|
291
|
+
* @returns `true` when SABnzbd accepts the shutdown command.
|
|
292
|
+
*/
|
|
293
|
+
async shutdown() {
|
|
294
|
+
await this.request({ mode: 'shutdown' });
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Requests a standard SABnzbd restart.
|
|
299
|
+
*
|
|
300
|
+
* Calls SABnzbd `mode=restart`.
|
|
301
|
+
*
|
|
302
|
+
* @returns `true` when SABnzbd accepts the restart command.
|
|
303
|
+
*/
|
|
304
|
+
async restart() {
|
|
305
|
+
await this.request({ mode: 'restart' });
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Requests SABnzbd restart with queue repair.
|
|
310
|
+
*
|
|
311
|
+
* Calls SABnzbd `mode=restart_repair`.
|
|
312
|
+
*
|
|
313
|
+
* @returns `true` when SABnzbd accepts the repair restart command.
|
|
314
|
+
*/
|
|
315
|
+
async restartRepair() {
|
|
316
|
+
await this.request({ mode: 'restart_repair' });
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Pauses post-processing tasks.
|
|
321
|
+
*
|
|
322
|
+
* Calls SABnzbd `mode=pause_pp`.
|
|
323
|
+
*
|
|
324
|
+
* @returns `true` when SABnzbd accepts the post-processing pause command.
|
|
325
|
+
*/
|
|
326
|
+
async pausePostProcessing() {
|
|
327
|
+
await this.request({ mode: 'pause_pp' });
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Resumes post-processing tasks.
|
|
332
|
+
*
|
|
333
|
+
* Calls SABnzbd `mode=resume_pp`.
|
|
334
|
+
*
|
|
335
|
+
* @returns `true` when SABnzbd accepts the post-processing resume command.
|
|
336
|
+
*/
|
|
337
|
+
async resumePostProcessing() {
|
|
338
|
+
await this.request({ mode: 'resume_pp' });
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Triggers immediate RSS processing.
|
|
343
|
+
*
|
|
344
|
+
* Calls SABnzbd `mode=rss_now`.
|
|
345
|
+
*
|
|
346
|
+
* @returns `true` when SABnzbd accepts the RSS trigger command.
|
|
347
|
+
*/
|
|
348
|
+
async fetchRss() {
|
|
349
|
+
await this.request({ mode: 'rss_now' });
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Triggers an immediate scan of the watched folder.
|
|
354
|
+
*
|
|
355
|
+
* Calls SABnzbd `mode=watched_now`.
|
|
356
|
+
*
|
|
357
|
+
* @returns `true` when SABnzbd accepts the watched-folder scan command.
|
|
358
|
+
*/
|
|
359
|
+
async scanWatchedFolder() {
|
|
360
|
+
await this.request({ mode: 'watched_now' });
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Resets SABnzbd quota counters.
|
|
365
|
+
*
|
|
366
|
+
* Calls SABnzbd `mode=reset_quota`.
|
|
367
|
+
*
|
|
368
|
+
* @returns `true` when SABnzbd accepts the quota reset command.
|
|
369
|
+
*/
|
|
370
|
+
async resetQuota() {
|
|
371
|
+
await this.request({ mode: 'reset_quota' });
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Clears currently active warnings.
|
|
376
|
+
*
|
|
377
|
+
* Calls SABnzbd `mode=warnings` with `name=clear`.
|
|
378
|
+
*
|
|
379
|
+
* @returns `true` when SABnzbd accepts the warning clear command.
|
|
380
|
+
*/
|
|
381
|
+
async clearWarnings() {
|
|
382
|
+
await this.request({ mode: 'warnings', name: 'clear' });
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Pauses a queue job by its SAB `nzo_id`.
|
|
387
|
+
*
|
|
388
|
+
* Calls SABnzbd `mode=queue` with `name=pause`.
|
|
389
|
+
*
|
|
390
|
+
* @param id SAB queue job identifier (`nzo_id`).
|
|
391
|
+
* @returns `true` when SABnzbd accepts the job pause command.
|
|
392
|
+
*/
|
|
393
|
+
async pauseJob(id) {
|
|
394
|
+
await this.request({ mode: 'queue', name: 'pause', value: id });
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Resumes a queue job by its SAB `nzo_id`.
|
|
399
|
+
*
|
|
400
|
+
* Calls SABnzbd `mode=queue` with `name=resume`.
|
|
401
|
+
*
|
|
402
|
+
* @param id SAB queue job identifier (`nzo_id`).
|
|
403
|
+
* @returns `true` when SABnzbd accepts the job resume command.
|
|
404
|
+
*/
|
|
405
|
+
async resumeJob(id) {
|
|
406
|
+
await this.request({ mode: 'queue', name: 'resume', value: id });
|
|
407
|
+
return true;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Deletes a queue job by its SAB `nzo_id`.
|
|
411
|
+
*
|
|
412
|
+
* Calls SABnzbd `mode=queue` with `name=delete`.
|
|
413
|
+
*
|
|
414
|
+
* @param id SAB queue job identifier (`nzo_id`).
|
|
415
|
+
* @param deleteFiles When `true`, also remove downloaded data files; defaults to `false`.
|
|
416
|
+
* @returns `true` when SABnzbd accepts the delete command.
|
|
417
|
+
*/
|
|
418
|
+
async deleteJob(id, deleteFiles = false) {
|
|
419
|
+
await this.request({
|
|
420
|
+
mode: 'queue',
|
|
421
|
+
name: 'delete',
|
|
422
|
+
value: id,
|
|
423
|
+
del_files: deleteFiles ? '1' : '0',
|
|
424
|
+
});
|
|
425
|
+
return true;
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Moves a queue job to a target position.
|
|
429
|
+
*
|
|
430
|
+
* Calls SABnzbd `mode=switch`.
|
|
431
|
+
*
|
|
432
|
+
* @param id SAB queue job identifier (`nzo_id`).
|
|
433
|
+
* @param position Target zero-based queue position.
|
|
434
|
+
* @returns `true` when SABnzbd accepts the move command.
|
|
435
|
+
*/
|
|
436
|
+
async moveJob(id, position) {
|
|
437
|
+
await this.request({
|
|
438
|
+
mode: 'switch',
|
|
439
|
+
value: id,
|
|
440
|
+
value2: `${position}`,
|
|
441
|
+
});
|
|
442
|
+
return true;
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Changes a queue job category.
|
|
446
|
+
*
|
|
447
|
+
* Calls SABnzbd `mode=change_cat`.
|
|
448
|
+
*
|
|
449
|
+
* @param id SAB queue job identifier (`nzo_id`).
|
|
450
|
+
* @param category SAB category name.
|
|
451
|
+
* @returns `true` when SABnzbd accepts the category change.
|
|
452
|
+
*/
|
|
453
|
+
async changeCategory(id, category) {
|
|
454
|
+
await this.request({
|
|
455
|
+
mode: 'change_cat',
|
|
456
|
+
value: id,
|
|
457
|
+
value2: category,
|
|
458
|
+
});
|
|
459
|
+
return true;
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Changes a queue job post-processing script.
|
|
463
|
+
*
|
|
464
|
+
* Calls SABnzbd `mode=change_script`.
|
|
465
|
+
*
|
|
466
|
+
* @param id SAB queue job identifier (`nzo_id`).
|
|
467
|
+
* @param script SAB configured script name.
|
|
468
|
+
* @returns `true` when SABnzbd accepts the script change.
|
|
469
|
+
*/
|
|
470
|
+
async changeScript(id, script) {
|
|
471
|
+
await this.request({
|
|
472
|
+
mode: 'change_script',
|
|
473
|
+
value: id,
|
|
474
|
+
value2: script,
|
|
475
|
+
});
|
|
476
|
+
return true;
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Changes queue job priority.
|
|
480
|
+
*
|
|
481
|
+
* Calls SABnzbd `mode=queue` with `name=priority`.
|
|
482
|
+
*
|
|
483
|
+
* @param id SAB queue job identifier (`nzo_id`).
|
|
484
|
+
* @param priority Shared normalized priority value.
|
|
485
|
+
* @returns The new queue position when reported by SABnzbd, otherwise `undefined`.
|
|
486
|
+
*/
|
|
487
|
+
async changePriority(id, priority) {
|
|
488
|
+
const response = await this.request({
|
|
489
|
+
mode: 'queue',
|
|
490
|
+
name: 'priority',
|
|
491
|
+
value: id,
|
|
492
|
+
value2: `${normalizedPriorityToSab(priority)}`,
|
|
493
|
+
});
|
|
494
|
+
return response.position;
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Changes queue job post-processing options.
|
|
498
|
+
*
|
|
499
|
+
* Calls SABnzbd `mode=change_opts`.
|
|
500
|
+
*
|
|
501
|
+
* @param id SAB queue job identifier (`nzo_id`).
|
|
502
|
+
* @param postProcess Normalized post-processing mode to apply.
|
|
503
|
+
* @returns `true` when SABnzbd accepts the option change.
|
|
504
|
+
*/
|
|
505
|
+
async changePostProcess(id, postProcess) {
|
|
506
|
+
await this.request({
|
|
507
|
+
mode: 'change_opts',
|
|
508
|
+
value: id,
|
|
509
|
+
value2: `${normalizePostProcess(postProcess)}`,
|
|
510
|
+
});
|
|
511
|
+
return true;
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Renames a queue job and optionally sets an archive password.
|
|
515
|
+
*
|
|
516
|
+
* Calls SABnzbd `mode=rename`.
|
|
517
|
+
*
|
|
518
|
+
* @param id SAB queue job identifier (`nzo_id`).
|
|
519
|
+
* @param name New queue job name.
|
|
520
|
+
* @param password Optional archive password; defaults to an empty string.
|
|
521
|
+
* @returns `true` when SABnzbd accepts the rename command.
|
|
522
|
+
*/
|
|
523
|
+
async renameJob(id, name, password = '') {
|
|
524
|
+
await this.request({
|
|
525
|
+
mode: 'rename',
|
|
526
|
+
value: id,
|
|
527
|
+
value2: name,
|
|
528
|
+
password,
|
|
529
|
+
});
|
|
530
|
+
return true;
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Lists files for a queue job.
|
|
534
|
+
*
|
|
535
|
+
* Calls SABnzbd `mode=get_files`.
|
|
536
|
+
*
|
|
537
|
+
* @param id SAB queue job identifier (`nzo_id`).
|
|
538
|
+
* @returns The raw file listing payload.
|
|
539
|
+
*/
|
|
540
|
+
async getFiles(id) {
|
|
541
|
+
return this.request({ mode: 'get_files', value: id });
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Sets the global download speed limit.
|
|
545
|
+
*
|
|
546
|
+
* Calls SABnzbd `mode=config` with `name=speedlimit`.
|
|
547
|
+
*
|
|
548
|
+
* @param limit Speed limit value passed directly to SABnzbd.
|
|
549
|
+
* @returns `true` when SABnzbd accepts the speed limit update.
|
|
550
|
+
*/
|
|
551
|
+
async setSpeedLimit(limit) {
|
|
552
|
+
await this.request({
|
|
553
|
+
mode: 'config',
|
|
554
|
+
name: 'speedlimit',
|
|
555
|
+
value: `${limit}`,
|
|
556
|
+
});
|
|
557
|
+
return true;
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Adds an NZB to the queue from a URL.
|
|
561
|
+
*
|
|
562
|
+
* Calls SABnzbd `mode=addurl`.
|
|
563
|
+
*
|
|
564
|
+
* @param url Remote NZB URL.
|
|
565
|
+
* @param options Optional SAB add fields; defaults include `category="*"`, `script="Default"`,
|
|
566
|
+
* `priority=-100`, and `postProcess=-1`.
|
|
567
|
+
* @returns The raw SAB add response containing status and optional `nzo_ids`.
|
|
568
|
+
*/
|
|
569
|
+
async addUrl(url, options = {}) {
|
|
570
|
+
const response = await this.request({
|
|
571
|
+
mode: 'addurl',
|
|
572
|
+
name: url,
|
|
573
|
+
nzbname: options.name ?? '',
|
|
574
|
+
password: options.password ?? '',
|
|
575
|
+
cat: options.category ?? '*',
|
|
576
|
+
script: options.script ?? 'Default',
|
|
577
|
+
priority: `${options.priority ?? -100}`,
|
|
578
|
+
pp: `${options.postProcess ?? -1}`,
|
|
579
|
+
});
|
|
580
|
+
return response;
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Adds an NZB to the queue by file upload.
|
|
584
|
+
*
|
|
585
|
+
* Calls SABnzbd `mode=addfile`.
|
|
586
|
+
*
|
|
587
|
+
* @param nzb NZB XML content as text or bytes.
|
|
588
|
+
* @param options Optional SAB add fields; defaults include `category="*"`, `script="Default"`,
|
|
589
|
+
* `priority=-100`, and `postProcess=-1`.
|
|
590
|
+
* @returns The raw SAB add response containing status and optional `nzo_ids`.
|
|
591
|
+
*/
|
|
592
|
+
async addFile(nzb, options = {}) {
|
|
593
|
+
const form = new FormData();
|
|
594
|
+
form.append('mode', 'addfile');
|
|
595
|
+
form.append('output', 'json');
|
|
596
|
+
form.append('nzbname', options.name ?? '');
|
|
597
|
+
form.append('password', options.password ?? '');
|
|
598
|
+
form.append('cat', options.category ?? '*');
|
|
599
|
+
form.append('script', options.script ?? 'Default');
|
|
600
|
+
form.append('priority', `${options.priority ?? -100}`);
|
|
601
|
+
form.append('pp', `${options.postProcess ?? -1}`);
|
|
602
|
+
if (this.config.apiKey) {
|
|
603
|
+
form.append('apikey', this.config.apiKey);
|
|
604
|
+
}
|
|
605
|
+
else if (this.config.nzbKey) {
|
|
606
|
+
form.append('nzbkey', this.config.nzbKey);
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
form.append('ma_username', this.config.username ?? '');
|
|
610
|
+
form.append('ma_password', this.config.password ?? '');
|
|
611
|
+
}
|
|
612
|
+
const filename = options.name?.endsWith('.nzb')
|
|
613
|
+
? options.name
|
|
614
|
+
: `${options.name ?? 'upload'}.nzb`;
|
|
615
|
+
form.append('name', new Blob([Buffer.from(encodeNzbFile(nzb))], { type: 'application/x-nzb+xml' }), filename);
|
|
616
|
+
return this.request({}, { method: 'POST', body: form });
|
|
617
|
+
}
|
|
618
|
+
async getQueue() {
|
|
619
|
+
const queue = await this.listQueue();
|
|
620
|
+
return queue.slots.map(normalizeSabJob);
|
|
621
|
+
}
|
|
622
|
+
async getHistory() {
|
|
623
|
+
const history = await this.listHistory();
|
|
624
|
+
return history.slots.map(normalizeSabHistoryItem);
|
|
625
|
+
}
|
|
626
|
+
async getQueueJob(id) {
|
|
627
|
+
const queue = await this.listQueue({ nzoIds: id });
|
|
628
|
+
const job = queue.slots.find(slot => slot.nzo_id === id);
|
|
629
|
+
if (!job) {
|
|
630
|
+
throw new UsenetNotFoundError('sabnzbd', 'queueJob', id);
|
|
631
|
+
}
|
|
632
|
+
return normalizeSabJob(job);
|
|
633
|
+
}
|
|
634
|
+
async getHistoryJob(id) {
|
|
635
|
+
const history = await this.listHistory({ nzoIds: id });
|
|
636
|
+
const historyItem = history.slots.find(item => item.nzo_id === id);
|
|
637
|
+
if (!historyItem) {
|
|
638
|
+
throw new UsenetNotFoundError('sabnzbd', 'historyJob', id);
|
|
639
|
+
}
|
|
640
|
+
return normalizeSabHistoryItem(historyItem);
|
|
641
|
+
}
|
|
642
|
+
async findJob(id) {
|
|
643
|
+
const queue = await this.listQueue({ nzoIds: id });
|
|
644
|
+
const queueJob = queue.slots.find(slot => slot.nzo_id === id);
|
|
645
|
+
if (queueJob) {
|
|
646
|
+
return {
|
|
647
|
+
source: 'queue',
|
|
648
|
+
job: normalizeSabJob(queueJob),
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
const history = await this.listHistory({ nzoIds: id });
|
|
652
|
+
const historyJob = history.slots.find(item => item.nzo_id === id);
|
|
653
|
+
if (historyJob) {
|
|
654
|
+
return {
|
|
655
|
+
source: 'history',
|
|
656
|
+
job: normalizeSabHistoryItem(historyJob),
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
return null;
|
|
660
|
+
}
|
|
661
|
+
async getAllData() {
|
|
662
|
+
const [queue, history, fullStatus, categories, scripts] = await Promise.all([
|
|
663
|
+
this.listQueue(),
|
|
664
|
+
this.listHistory(),
|
|
665
|
+
this.getFullStatus(),
|
|
666
|
+
this.getCategories(),
|
|
667
|
+
this.getScripts(),
|
|
668
|
+
]);
|
|
669
|
+
return {
|
|
670
|
+
categories,
|
|
671
|
+
scripts,
|
|
672
|
+
queue: queue.slots.map(normalizeSabJob),
|
|
673
|
+
history: history.slots.map(normalizeSabHistoryItem),
|
|
674
|
+
status: normalizeSabStatus(queue, fullStatus),
|
|
675
|
+
raw: {
|
|
676
|
+
queue,
|
|
677
|
+
history,
|
|
678
|
+
fullStatus,
|
|
679
|
+
},
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
async removeJob(id, removeData = false) {
|
|
683
|
+
return this.deleteJob(id, removeData);
|
|
684
|
+
}
|
|
685
|
+
async setCategory(id, category) {
|
|
686
|
+
return this.changeCategory(id, category);
|
|
687
|
+
}
|
|
688
|
+
async setPriority(id, priority) {
|
|
689
|
+
await this.changePriority(id, priority);
|
|
690
|
+
return true;
|
|
691
|
+
}
|
|
692
|
+
async addNzbFile(nzb, options = {}) {
|
|
693
|
+
const response = await this.addFile(nzb, this.normalizeAddOptions(options));
|
|
694
|
+
return getAddedJobId(response);
|
|
695
|
+
}
|
|
696
|
+
async addNzbUrl(url, options = {}) {
|
|
697
|
+
const response = await this.addUrl(url, this.normalizeAddOptions(options));
|
|
698
|
+
return getAddedJobId(response);
|
|
699
|
+
}
|
|
700
|
+
async normalizedAddNzb(input, options = {}) {
|
|
701
|
+
const id = 'url' in input
|
|
702
|
+
? await this.addNzbUrl(input.url, options)
|
|
703
|
+
: await this.addNzbFile(input.file, options);
|
|
704
|
+
return this.waitForQueueJob(id);
|
|
705
|
+
}
|
|
706
|
+
async waitForQueueJob(id) {
|
|
707
|
+
for (let attempt = 0; attempt < addQueuePollAttempts; attempt++) {
|
|
708
|
+
try {
|
|
709
|
+
return await this.getQueueJob(id);
|
|
710
|
+
}
|
|
711
|
+
catch (error) {
|
|
712
|
+
if (!isUsenetNotFoundError(error)) {
|
|
713
|
+
throw error;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
if (attempt < addQueuePollAttempts - 1) {
|
|
717
|
+
await sleep(addQueuePollIntervalMs);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
throw new UsenetNotFoundError('sabnzbd', 'queueJob', id);
|
|
721
|
+
}
|
|
722
|
+
normalizeAddOptions(options) {
|
|
723
|
+
return {
|
|
724
|
+
category: options.category ?? '*',
|
|
725
|
+
script: options.postProcessScript ?? 'Default',
|
|
726
|
+
priority: normalizedPriorityToSab(options.startPaused ? UsenetPriority.paused : coercePriority(options.priority)),
|
|
727
|
+
postProcess: normalizePostProcess(options.postProcess),
|
|
728
|
+
name: options.name,
|
|
729
|
+
password: options.password,
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
async request(params, options = {}) {
|
|
733
|
+
const url = joinURL(this.config.baseUrl, this.config.path ?? '/api');
|
|
734
|
+
const query = options.method === 'POST'
|
|
735
|
+
? undefined
|
|
736
|
+
: {
|
|
737
|
+
output: 'json',
|
|
738
|
+
...this.getAuthQuery(),
|
|
739
|
+
...params,
|
|
740
|
+
};
|
|
741
|
+
const response = await ofetch(url, {
|
|
742
|
+
method: options.method ?? 'GET',
|
|
743
|
+
body: options.body,
|
|
744
|
+
query,
|
|
745
|
+
dispatcher: this.config.dispatcher,
|
|
746
|
+
timeout: this.config.timeout,
|
|
747
|
+
});
|
|
748
|
+
this.assertSabResponse(response);
|
|
749
|
+
return response;
|
|
750
|
+
}
|
|
751
|
+
getAuthQuery() {
|
|
752
|
+
if (this.config.apiKey) {
|
|
753
|
+
return { apikey: this.config.apiKey };
|
|
754
|
+
}
|
|
755
|
+
if (this.config.nzbKey) {
|
|
756
|
+
return { nzbkey: this.config.nzbKey };
|
|
757
|
+
}
|
|
758
|
+
return {
|
|
759
|
+
ma_username: this.config.username ?? '',
|
|
760
|
+
ma_password: this.config.password ?? '',
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
assertSabResponse(response) {
|
|
764
|
+
if (!response || typeof response !== 'object') {
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
if ('status' in response && response.status === false) {
|
|
768
|
+
const error = 'error' in response && typeof response.error === 'string'
|
|
769
|
+
? response.error
|
|
770
|
+
: 'SABnzbd returned status=false';
|
|
771
|
+
throw new Error(error);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|