debrid-ts 0.1.0 → 0.2.0
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/README.md +24 -2
- package/dist/index.d.mts +333 -1
- package/dist/index.mjs +678 -27
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# debrid-ts
|
|
2
2
|
|
|
3
|
-
Typed TypeScript clients for debrid services — **Real-Debrid**, **AllDebrid
|
|
4
|
-
|
|
3
|
+
Typed TypeScript clients for debrid services — **Real-Debrid**, **AllDebrid**,
|
|
4
|
+
**Premiumize**, **TorBox**, **Debrid-Link**, **Offcloud**, **put.io** and
|
|
5
|
+
**EasyDebrid** — plus one
|
|
5
6
|
provider-agnostic interface so your code can swap providers by changing a
|
|
6
7
|
constructor.
|
|
7
8
|
|
|
@@ -142,6 +143,27 @@ Covers every endpoint in the [API docs](https://www.premiumize.me/api):
|
|
|
142
143
|
`getAuthorizeUrl()` + `exchangeCode()`, device flow via `getDeviceCode()` +
|
|
143
144
|
`waitForDeviceToken()`, and the legacy password grant).
|
|
144
145
|
|
|
146
|
+
### Other providers
|
|
147
|
+
|
|
148
|
+
These clients cover the torrent workflow behind `DebridProvider`, not every endpoint of each API.
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
new TorBox(apiKey) // https://torbox.app/settings
|
|
152
|
+
new DebridLink(token) // https://debrid-link.com/webapp/apikey, or an OAuth access token
|
|
153
|
+
new Offcloud(apiKey) // from your Offcloud account
|
|
154
|
+
new Putio(token) // https://app.put.io/oauth
|
|
155
|
+
new EasyDebrid(apiKey) // from your EasyDebrid dashboard
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
| | TorBox | Debrid-Link | Offcloud | put.io | EasyDebrid |
|
|
159
|
+
| --- | --- | --- | --- | --- | --- |
|
|
160
|
+
| `isCached()` | Real lookup. | **Probe:** adds the hash (only accepted when cached), then removes it. A torrent you already have is never touched. | Real lookup. | Always `false`: there is no shared cache. | Real lookup. |
|
|
161
|
+
| `addMagnet()` | Starts automatically. Throws `ACTIVE_LIMIT` if TorBox queues it instead. | Starts automatically. | Starts automatically. Throws `not_available` on a plan limit. | Starts automatically. | Adds nothing. Throws `not_cached` unless cached; the id is the info hash. |
|
|
162
|
+
| `.torrent` upload | `addTorrentFile()` | `addTorrentFile()` | Not available | `addTorrentFile()` | Not available |
|
|
163
|
+
| `getDownloadLinks()` | One request per file. | Reads the finished files. | Reads the finished files. | One request per file. | Generates links for the hash. |
|
|
164
|
+
| `listTorrents()` paging | Server-side. | Server-side. | Client-side slice. | Client-side slice. | Always empty: nothing is stored per account. |
|
|
165
|
+
| Delete | `deleteTorrent()` | `deleteTorrent()` | `deleteTorrent()` | `deleteTorrent()`, which also deletes the resulting files. | Nothing to delete. |
|
|
166
|
+
|
|
145
167
|
## Options
|
|
146
168
|
|
|
147
169
|
Every client (and every static OAuth helper) accepts:
|
package/dist/index.d.mts
CHANGED
|
@@ -1370,4 +1370,336 @@ declare class Premiumize extends BaseFetcher implements DebridProvider {
|
|
|
1370
1370
|
} & ClientOptions): Promise<PmTokenResponse>;
|
|
1371
1371
|
}
|
|
1372
1372
|
//#endregion
|
|
1373
|
-
|
|
1373
|
+
//#region src/services/torbox/types.d.ts
|
|
1374
|
+
/** Standard TorBox response wrapper (`success: false` also arrives with HTTP 200). */
|
|
1375
|
+
interface TbEnvelope<T> {
|
|
1376
|
+
success: boolean;
|
|
1377
|
+
error: string | null;
|
|
1378
|
+
detail: string;
|
|
1379
|
+
data: T;
|
|
1380
|
+
}
|
|
1381
|
+
interface TbUser {
|
|
1382
|
+
id: number;
|
|
1383
|
+
email: string;
|
|
1384
|
+
/** 0 = free */
|
|
1385
|
+
plan: number;
|
|
1386
|
+
is_subscribed: boolean;
|
|
1387
|
+
premium_expires_at: string | null;
|
|
1388
|
+
}
|
|
1389
|
+
interface TbFile {
|
|
1390
|
+
id: number;
|
|
1391
|
+
name: string;
|
|
1392
|
+
size: number;
|
|
1393
|
+
short_name?: string;
|
|
1394
|
+
mimetype?: string;
|
|
1395
|
+
}
|
|
1396
|
+
interface TbTorrent {
|
|
1397
|
+
id: number;
|
|
1398
|
+
hash: string;
|
|
1399
|
+
name: string;
|
|
1400
|
+
size: number;
|
|
1401
|
+
created_at: string;
|
|
1402
|
+
download_state: string;
|
|
1403
|
+
/** 0–1 */
|
|
1404
|
+
progress: number;
|
|
1405
|
+
download_speed: number;
|
|
1406
|
+
seeds: number;
|
|
1407
|
+
ratio: number;
|
|
1408
|
+
download_present: boolean;
|
|
1409
|
+
download_finished: boolean;
|
|
1410
|
+
files: TbFile[] | null;
|
|
1411
|
+
}
|
|
1412
|
+
interface TbCreateResult {
|
|
1413
|
+
/** Absent when TorBox queued the torrent instead of starting it (`queued_id` is set then). */
|
|
1414
|
+
torrent_id?: number | null;
|
|
1415
|
+
queued_id?: number | null;
|
|
1416
|
+
name?: string;
|
|
1417
|
+
hash: string;
|
|
1418
|
+
}
|
|
1419
|
+
interface TbCacheResult {
|
|
1420
|
+
hash: string;
|
|
1421
|
+
name: string;
|
|
1422
|
+
size: number;
|
|
1423
|
+
}
|
|
1424
|
+
//#endregion
|
|
1425
|
+
//#region src/services/torbox/torbox.d.ts
|
|
1426
|
+
type TorBoxOptions = ClientOptions;
|
|
1427
|
+
declare class TorBox extends BaseFetcher implements DebridProvider {
|
|
1428
|
+
#private;
|
|
1429
|
+
/** @param apiKey API key from https://torbox.app/settings */
|
|
1430
|
+
constructor(apiKey: string, options?: TorBoxOptions);
|
|
1431
|
+
/** Unwraps the `{ success, error, detail, data }` envelope. */
|
|
1432
|
+
protected request<T>(method: string, path: string, options?: RequestOptions): Promise<T>;
|
|
1433
|
+
readonly name = "torbox";
|
|
1434
|
+
getUser(): Promise<UserInfo>;
|
|
1435
|
+
/** Real lookup via `/torrents/checkcached`, no side effects. */
|
|
1436
|
+
isCached(hashOrMagnet: string): Promise<boolean>;
|
|
1437
|
+
addMagnet(magnet: string): Promise<Torrent>;
|
|
1438
|
+
/** Adds a `.torrent` file and returns its torrent id. */
|
|
1439
|
+
addTorrentFile(file: FileInput, filename?: string): Promise<string>;
|
|
1440
|
+
listTorrents(offset?: number, limit?: number): Promise<Torrent[]>;
|
|
1441
|
+
getTorrent(torrentId: string): Promise<Torrent>;
|
|
1442
|
+
/** Requests one link per file. */
|
|
1443
|
+
getDownloadLinks(torrentId: string): Promise<DownloadLink[]>;
|
|
1444
|
+
/** GET /torrents/checkcached — the cached subset of `hashes`. */
|
|
1445
|
+
checkCached(hashes: string[]): Promise<TbCacheResult[]>;
|
|
1446
|
+
/** GET /torrents/requestdl */
|
|
1447
|
+
requestDownloadLink(torrentId: string, fileId: number): Promise<string>;
|
|
1448
|
+
/** POST /torrents/controltorrent with `operation: "delete"` */
|
|
1449
|
+
deleteTorrent(torrentId: string): Promise<void>;
|
|
1450
|
+
/** This instance as the provider-agnostic {@link DebridProvider}. */
|
|
1451
|
+
toProvider(): DebridProvider;
|
|
1452
|
+
}
|
|
1453
|
+
//#endregion
|
|
1454
|
+
//#region src/services/debrid-link/types.d.ts
|
|
1455
|
+
/** `{ success: false, error }` responses are turned into DebridError by the client. */
|
|
1456
|
+
interface DlEnvelope<T> {
|
|
1457
|
+
success: boolean;
|
|
1458
|
+
value: T;
|
|
1459
|
+
error?: string;
|
|
1460
|
+
}
|
|
1461
|
+
interface DlAccount {
|
|
1462
|
+
username: string;
|
|
1463
|
+
email?: string;
|
|
1464
|
+
/** 0 = free, 1 = premium, 2 = lifetime premium */
|
|
1465
|
+
accountType: number;
|
|
1466
|
+
/** Seconds of premium left, 0 for free users */
|
|
1467
|
+
premiumLeft: number;
|
|
1468
|
+
avatarUrl?: string;
|
|
1469
|
+
}
|
|
1470
|
+
interface DlSeedboxFile {
|
|
1471
|
+
id: string;
|
|
1472
|
+
name: string;
|
|
1473
|
+
size: number;
|
|
1474
|
+
downloadUrl: string;
|
|
1475
|
+
downloadPercent: number;
|
|
1476
|
+
}
|
|
1477
|
+
interface DlTorrent {
|
|
1478
|
+
id: string;
|
|
1479
|
+
name: string;
|
|
1480
|
+
/** Unix seconds */
|
|
1481
|
+
created: number;
|
|
1482
|
+
hashString: string;
|
|
1483
|
+
uploadRatio: number;
|
|
1484
|
+
peersConnected: number;
|
|
1485
|
+
/** 0 paused, 1 queued, 2 verifying, 4 downloading, 8 seeding, 100 finished */
|
|
1486
|
+
status: number;
|
|
1487
|
+
totalSize: number;
|
|
1488
|
+
downloadPercent: number;
|
|
1489
|
+
downloadSpeed: number;
|
|
1490
|
+
/** `files` holds a single ZIP in lists when true; `?ids=` returns the real files */
|
|
1491
|
+
isZip: boolean;
|
|
1492
|
+
files: DlSeedboxFile[];
|
|
1493
|
+
}
|
|
1494
|
+
interface DlLink {
|
|
1495
|
+
id: string;
|
|
1496
|
+
name: string;
|
|
1497
|
+
url: string;
|
|
1498
|
+
downloadUrl: string;
|
|
1499
|
+
host: string;
|
|
1500
|
+
size: number;
|
|
1501
|
+
created: number;
|
|
1502
|
+
expired: boolean;
|
|
1503
|
+
}
|
|
1504
|
+
//#endregion
|
|
1505
|
+
//#region src/services/debrid-link/debrid-link.d.ts
|
|
1506
|
+
type DebridLinkOptions = ClientOptions;
|
|
1507
|
+
declare class DebridLink extends BaseFetcher implements DebridProvider {
|
|
1508
|
+
#private;
|
|
1509
|
+
/** @param token Private API key from https://debrid-link.com/webapp/apikey, or an OAuth access token */
|
|
1510
|
+
constructor(token: string, options?: DebridLinkOptions);
|
|
1511
|
+
/** Unwraps `{ success, value }`. */
|
|
1512
|
+
protected request<T>(method: string, path: string, options?: RequestOptions): Promise<T>;
|
|
1513
|
+
readonly name = "debrid-link";
|
|
1514
|
+
getUser(): Promise<UserInfo>;
|
|
1515
|
+
/**
|
|
1516
|
+
* **Probe:** the API has no cache lookup, but only accepts a bare hash when
|
|
1517
|
+
* it's cached. The probe torrent is removed again; a torrent the account
|
|
1518
|
+
* already had is reported from the list and never touched.
|
|
1519
|
+
*/
|
|
1520
|
+
isCached(hashOrMagnet: string): Promise<boolean>;
|
|
1521
|
+
addMagnet(magnet: string): Promise<Torrent>;
|
|
1522
|
+
/** Adds a `.torrent` file (multipart). */
|
|
1523
|
+
addTorrentFile(file: FileInput, filename?: string): Promise<Torrent>;
|
|
1524
|
+
listTorrents(offset?: number, limit?: number): Promise<Torrent[]>;
|
|
1525
|
+
/** GET /seedbox/list */
|
|
1526
|
+
listSeedbox(page?: number): Promise<DlTorrent[]>;
|
|
1527
|
+
getTorrent(torrentId: string): Promise<Torrent>;
|
|
1528
|
+
/** Direct links of the files that finished downloading. */
|
|
1529
|
+
getDownloadLinks(torrentId: string): Promise<DownloadLink[]>;
|
|
1530
|
+
/** POST /downloader/add — a folder link yields several files. */
|
|
1531
|
+
unrestrictLink(url: string): Promise<DlLink[]>;
|
|
1532
|
+
/** DELETE /seedbox/:id/remove */
|
|
1533
|
+
deleteTorrent(torrentId: string): Promise<void>;
|
|
1534
|
+
/** This instance as the provider-agnostic {@link DebridProvider}. */
|
|
1535
|
+
toProvider(): DebridProvider;
|
|
1536
|
+
}
|
|
1537
|
+
//#endregion
|
|
1538
|
+
//#region src/services/offcloud/offcloud.d.ts
|
|
1539
|
+
type OffcloudOptions = ClientOptions;
|
|
1540
|
+
declare class Offcloud extends BaseFetcher implements DebridProvider {
|
|
1541
|
+
#private;
|
|
1542
|
+
/** @param apiKey API key from your Offcloud account */
|
|
1543
|
+
constructor(apiKey: string, options?: OffcloudOptions);
|
|
1544
|
+
/** Errors can arrive as `{ error }` with HTTP 200. */
|
|
1545
|
+
protected request<T>(method: string, path: string, options?: RequestOptions): Promise<T>;
|
|
1546
|
+
readonly name = "offcloud";
|
|
1547
|
+
getUser(): Promise<UserInfo>;
|
|
1548
|
+
/** Real lookup via `/cache/info`, no side effects. */
|
|
1549
|
+
isCached(hashOrMagnet: string): Promise<boolean>;
|
|
1550
|
+
addMagnet(magnet: string): Promise<Torrent>;
|
|
1551
|
+
/** `/cloud/history` has no server-side pagination, so this slices locally. */
|
|
1552
|
+
listTorrents(offset?: number, limit?: number): Promise<Torrent[]>;
|
|
1553
|
+
/** POST /cloud/status */
|
|
1554
|
+
getTorrent(torrentId: string): Promise<Torrent>;
|
|
1555
|
+
/** Direct links from GET /cloud/explore/:id */
|
|
1556
|
+
getDownloadLinks(torrentId: string): Promise<DownloadLink[]>;
|
|
1557
|
+
/** POST /cloud/remove */
|
|
1558
|
+
deleteTorrent(torrentId: string): Promise<void>;
|
|
1559
|
+
/** This instance as the provider-agnostic {@link DebridProvider}. */
|
|
1560
|
+
toProvider(): DebridProvider;
|
|
1561
|
+
}
|
|
1562
|
+
//#endregion
|
|
1563
|
+
//#region src/services/offcloud/types.d.ts
|
|
1564
|
+
interface OcAccount {
|
|
1565
|
+
user_id: string;
|
|
1566
|
+
is_premium: boolean;
|
|
1567
|
+
/** e.g. `2026-12-31` */
|
|
1568
|
+
expiration_date?: string;
|
|
1569
|
+
can_download?: boolean;
|
|
1570
|
+
}
|
|
1571
|
+
type OcStatus = 'created' | 'queued' | 'downloading' | 'downloaded' | 'error' | 'canceled';
|
|
1572
|
+
interface OcCloudItem {
|
|
1573
|
+
requestId: string;
|
|
1574
|
+
fileName?: string;
|
|
1575
|
+
status: OcStatus;
|
|
1576
|
+
originalLink?: string;
|
|
1577
|
+
createdOn?: string;
|
|
1578
|
+
/** Set when the account can't start this download, e.g. `cloud` */
|
|
1579
|
+
not_available?: string;
|
|
1580
|
+
}
|
|
1581
|
+
interface OcCacheItem {
|
|
1582
|
+
cached: boolean;
|
|
1583
|
+
}
|
|
1584
|
+
interface OcFile {
|
|
1585
|
+
id?: string;
|
|
1586
|
+
name?: string;
|
|
1587
|
+
size: number;
|
|
1588
|
+
/** Path inside the download */
|
|
1589
|
+
path: string;
|
|
1590
|
+
url: string;
|
|
1591
|
+
}
|
|
1592
|
+
//#endregion
|
|
1593
|
+
//#region src/services/putio/types.d.ts
|
|
1594
|
+
interface PutioAccount {
|
|
1595
|
+
user_id: number;
|
|
1596
|
+
username: string;
|
|
1597
|
+
mail?: string;
|
|
1598
|
+
avatar_url?: string;
|
|
1599
|
+
account_active: boolean;
|
|
1600
|
+
plan_expiration_date?: string;
|
|
1601
|
+
}
|
|
1602
|
+
interface PutioTransfer {
|
|
1603
|
+
id: number;
|
|
1604
|
+
name: string;
|
|
1605
|
+
/** e.g. `IN_QUEUE`, `DOWNLOADING`, `SEEDING`, `COMPLETED`, `ERROR` */
|
|
1606
|
+
status: string;
|
|
1607
|
+
percent_done: number;
|
|
1608
|
+
size: number;
|
|
1609
|
+
down_speed: number;
|
|
1610
|
+
peers_connected?: number;
|
|
1611
|
+
/** Peer count as named in the API spec */
|
|
1612
|
+
peers?: number;
|
|
1613
|
+
created_at?: string;
|
|
1614
|
+
error_message?: string | null;
|
|
1615
|
+
/** The resulting file or folder, set once finished */
|
|
1616
|
+
file_id?: number | null;
|
|
1617
|
+
magneturi?: string | null;
|
|
1618
|
+
/** What was added: a magnet or HTTP(S) URL */
|
|
1619
|
+
source?: string | null;
|
|
1620
|
+
}
|
|
1621
|
+
interface PutioFile {
|
|
1622
|
+
id: number;
|
|
1623
|
+
name: string;
|
|
1624
|
+
size: number;
|
|
1625
|
+
content_type: string;
|
|
1626
|
+
}
|
|
1627
|
+
/** A file inside a transfer's result, with its path from the result's root. */
|
|
1628
|
+
interface PutioFileEntry {
|
|
1629
|
+
id: number;
|
|
1630
|
+
path: string;
|
|
1631
|
+
size: number;
|
|
1632
|
+
}
|
|
1633
|
+
//#endregion
|
|
1634
|
+
//#region src/services/putio/putio.d.ts
|
|
1635
|
+
type PutioOptions = ClientOptions;
|
|
1636
|
+
declare class Putio extends BaseFetcher implements DebridProvider {
|
|
1637
|
+
#private;
|
|
1638
|
+
/** @param token OAuth token from https://app.put.io/oauth */
|
|
1639
|
+
constructor(token: string, options?: PutioOptions);
|
|
1640
|
+
readonly name = "putio";
|
|
1641
|
+
getUser(): Promise<UserInfo>;
|
|
1642
|
+
/** put.io has no shared cache to look up, so this is always false. */
|
|
1643
|
+
isCached(_hashOrMagnet: string): Promise<boolean>;
|
|
1644
|
+
addMagnet(magnet: string): Promise<Torrent>;
|
|
1645
|
+
/** Uploads a `.torrent` file, which starts a transfer, and returns its id. */
|
|
1646
|
+
addTorrentFile(file: FileInput, filename?: string): Promise<string>;
|
|
1647
|
+
/** `/transfers/list` has no server-side pagination, so this slices locally. */
|
|
1648
|
+
listTorrents(offset?: number, limit?: number): Promise<Torrent[]>;
|
|
1649
|
+
getTorrent(torrentId: string): Promise<Torrent>;
|
|
1650
|
+
/** GET /transfers/:id */
|
|
1651
|
+
getTransfer(transferId: string): Promise<PutioTransfer>;
|
|
1652
|
+
/** Requests one link per file. */
|
|
1653
|
+
getDownloadLinks(torrentId: string): Promise<DownloadLink[]>;
|
|
1654
|
+
/** Every file a finished transfer produced, walking its folder tree. */
|
|
1655
|
+
listTransferFiles(transferId: string): Promise<PutioFileEntry[]>;
|
|
1656
|
+
/** GET /files/list, following the cursor. */
|
|
1657
|
+
listFolder(folderId: number): Promise<PutioFile[]>;
|
|
1658
|
+
/** GET /files/:id/url */
|
|
1659
|
+
getFileUrl(fileId: number): Promise<string>;
|
|
1660
|
+
/**
|
|
1661
|
+
* Removes the transfer and, once finished, the files it produced, so this
|
|
1662
|
+
* matches what "delete" means on the other providers.
|
|
1663
|
+
*/
|
|
1664
|
+
deleteTorrent(torrentId: string): Promise<void>;
|
|
1665
|
+
/** This instance as the provider-agnostic {@link DebridProvider}. */
|
|
1666
|
+
toProvider(): DebridProvider;
|
|
1667
|
+
}
|
|
1668
|
+
//#endregion
|
|
1669
|
+
//#region src/services/easydebrid/easydebrid.d.ts
|
|
1670
|
+
type EasyDebridOptions = ClientOptions;
|
|
1671
|
+
declare class EasyDebrid extends BaseFetcher implements DebridProvider {
|
|
1672
|
+
/** @param apiKey API key from your EasyDebrid dashboard */
|
|
1673
|
+
constructor(apiKey: string, options?: EasyDebridOptions);
|
|
1674
|
+
/** Errors can arrive as `{ error }` with HTTP 200. */
|
|
1675
|
+
protected request<T>(method: string, path: string, options?: RequestOptions): Promise<T>;
|
|
1676
|
+
readonly name = "easydebrid";
|
|
1677
|
+
getUser(): Promise<UserInfo>;
|
|
1678
|
+
/** Real lookup via `/link/lookup`, no side effects. */
|
|
1679
|
+
isCached(hashOrMagnet: string): Promise<boolean>;
|
|
1680
|
+
/** Nothing is added remotely: this checks the cache and returns the hash as the id. */
|
|
1681
|
+
addMagnet(magnet: string): Promise<Torrent>;
|
|
1682
|
+
/** EasyDebrid keeps no list of torrents per account. */
|
|
1683
|
+
listTorrents(_offset?: number, _limit?: number): Promise<Torrent[]>;
|
|
1684
|
+
getTorrent(torrentId: string): Promise<Torrent>;
|
|
1685
|
+
/** Direct links from POST /link/generate */
|
|
1686
|
+
getDownloadLinks(torrentId: string): Promise<DownloadLink[]>;
|
|
1687
|
+
/** This instance as the provider-agnostic {@link DebridProvider}. */
|
|
1688
|
+
toProvider(): DebridProvider;
|
|
1689
|
+
}
|
|
1690
|
+
//#endregion
|
|
1691
|
+
//#region src/services/easydebrid/types.d.ts
|
|
1692
|
+
interface EdUser {
|
|
1693
|
+
id: string;
|
|
1694
|
+
/** e.g. `2026-10-01` */
|
|
1695
|
+
paid_until?: string | null;
|
|
1696
|
+
}
|
|
1697
|
+
interface EdFile {
|
|
1698
|
+
filename: string;
|
|
1699
|
+
/** Folders from the torrent root down to the file */
|
|
1700
|
+
directory?: string[];
|
|
1701
|
+
size: number;
|
|
1702
|
+
url: string;
|
|
1703
|
+
}
|
|
1704
|
+
//#endregion
|
|
1705
|
+
export { type AdDelayedLink, type AdEnvelope, type AdHostDomainsData, type AdHostInfo, type AdHostPrioritiesData, type AdHostsData, type AdItemError, type AdLinkInfo, type AdLinkInfosData, type AdMagnetFileEntry, type AdMagnetFileUploadData, type AdMagnetFilesData, type AdMagnetFilter, type AdMagnetNode, type AdMagnetRestartData, type AdMagnetStatusData, type AdMagnetStatusItem, type AdMagnetStatusParams, type AdMagnetUploadData, type AdPinCheck, type AdPinGet, type AdPing, type AdRedirectorData, type AdStreamQuality, type AdStreamingLink, type AdUnlockedLink, type AdUploadedFile, type AdUploadedMagnet, type AdUserData, type AdUserHost, type AdUserHostsData, type AdUserInfo, type AdUserLink, type AdUserLinksData, type AdVerif, type AdVerifResend, type AdVoucherBalanceData, type AdVoucherDuration, type AdVoucherGenerateData, type AdVoucherGetData, AllDebrid, type AllDebridOptions, BaseFetcher, type BaseFetcherOptions, type BodyLike, type ClientOptions, DebridError, DebridLink, type DebridLinkOptions, type DebridProvider, type DlAccount, type DlEnvelope, type DlLink, type DlSeedboxFile, type DlTorrent, type DownloadLink, EasyDebrid, type EasyDebridOptions, type EdFile, type EdUser, type FileInput, type FormValue, type OcAccount, type OcCacheItem, type OcCloudItem, type OcFile, type OcStatus, Offcloud, type OffcloudOptions, type PmAccountInfo, type PmBreadcrumb, type PmCacheCheck, type PmContentEntry, type PmDeviceCode, type PmDirectDl, type PmDirectDlContent, type PmErrorBody, type PmFileEntry, type PmFolderCreate, type PmFolderEntry, type PmFolderList, type PmFolderListParams, type PmItemDetails, type PmItemListAll, type PmListedFile, type PmOAuthError, type PmResponse, type PmSearchResults, type PmServicesList, type PmSuccess, type PmTokenResponse, type PmTransfer, type PmTransferContainer, type PmTransferCreateResult, type PmTransferCreated, type PmTransferList, type PmTransferStatus, type PmUploadInfo, type PmZipGenerate, Premiumize, type PremiumizeOptions, Putio, type PutioAccount, type PutioFile, type PutioFileEntry, type PutioOptions, type PutioTransfer, type QueryValue, REAL_DEBRID_OPENSOURCE_CLIENT_ID, type RdActiveCount, type RdAlternativeLink, type RdAvailableHost, type RdDeviceAuthorization, type RdDeviceCode, type RdDeviceCredentials, type RdDownloadItem, type RdHost, type RdHostState, type RdHostStatus, type RdHostStatuses, type RdHosts, type RdInstantAvailability, type RdLinkCheck, type RdMagnetAdded, type RdMediaAudioStream, type RdMediaInfo, type RdMediaSubtitleStream, type RdMediaVideoStream, type RdPagination, type RdSettingName, type RdSettings, type RdTokenResponse, type RdTorrent, type RdTorrentFile, type RdTorrentInfo, type RdTorrentStatus, type RdTraffic, type RdTrafficDay, type RdTrafficDetails, type RdTrafficEntry, type RdTranscodeQualities, type RdTranscodeStreams, type RdUnrestrictedLink, type RdUser, RealDebrid, type RealDebridOptions, type RequestOptions, type TbCacheResult, type TbCreateResult, type TbEnvelope, type TbFile, type TbTorrent, type TbUser, TorBox, type TorBoxOptions, type Torrent, type TorrentFile, type TorrentStatus, type UserInfo };
|
package/dist/index.mjs
CHANGED
|
@@ -123,16 +123,21 @@ var BaseFetcher = class {
|
|
|
123
123
|
/**
|
|
124
124
|
* Pulls a message/code out of the error bodies the supported providers send:
|
|
125
125
|
* `{ error: "bad_token", error_code: 8 }` (Real-Debrid / OAuth),
|
|
126
|
-
* `{ error: { code, message } }` (AllDebrid), `{ message, code }` (Premiumize)
|
|
126
|
+
* `{ error: { code, message } }` (AllDebrid), `{ message, code }` (Premiumize),
|
|
127
|
+
* `{ error: "BAD_TOKEN", detail }` (TorBox), `{ error_type, error_message }` (put.io).
|
|
127
128
|
*/
|
|
128
129
|
function describeError(details) {
|
|
129
130
|
if (!details || typeof details !== "object") return {};
|
|
130
131
|
const body = details;
|
|
131
132
|
const error = body.error;
|
|
132
133
|
if (typeof error === "string") return {
|
|
133
|
-
message:
|
|
134
|
+
message: asString(body.error_description) ?? asString(body.detail) ?? error,
|
|
134
135
|
code: error
|
|
135
136
|
};
|
|
137
|
+
if (typeof body.error_type === "string") return {
|
|
138
|
+
message: asString(body.error_message),
|
|
139
|
+
code: body.error_type
|
|
140
|
+
};
|
|
136
141
|
if (error && typeof error === "object") {
|
|
137
142
|
const nested = error;
|
|
138
143
|
return {
|
|
@@ -169,6 +174,11 @@ function toBlob(file, type) {
|
|
|
169
174
|
function toMagnet(hashOrMagnet) {
|
|
170
175
|
return hashOrMagnet.startsWith("magnet:") ? hashOrMagnet : `magnet:?xt=urn:btih:${hashOrMagnet}`;
|
|
171
176
|
}
|
|
177
|
+
/** Accepts a magnet URI or a bare info hash and returns the lowercase hash, or '' if there is none. */
|
|
178
|
+
function toInfoHash(hashOrMagnet) {
|
|
179
|
+
if (!hashOrMagnet.startsWith("magnet:")) return hashOrMagnet.toLowerCase();
|
|
180
|
+
return /[?&]xt=urn:btih:([^&]+)/i.exec(hashOrMagnet)?.[1]?.toLowerCase() ?? "";
|
|
181
|
+
}
|
|
172
182
|
//#endregion
|
|
173
183
|
//#region src/services/real-debrid/real-debrid.ts
|
|
174
184
|
/**
|
|
@@ -177,7 +187,7 @@ function toMagnet(hashOrMagnet) {
|
|
|
177
187
|
* Implements the unified {@link DebridProvider} interface on top of the raw
|
|
178
188
|
* per-endpoint methods (grouped by namespace exactly like the API docs).
|
|
179
189
|
*/
|
|
180
|
-
const API_BASE$
|
|
190
|
+
const API_BASE$7 = "https://api.real-debrid.com/rest/1.0";
|
|
181
191
|
const OAUTH_BASE$1 = "https://api.real-debrid.com/oauth/v2";
|
|
182
192
|
/** grant_type for the device flow token exchange and for refreshing tokens */
|
|
183
193
|
const DEVICE_GRANT = "http://oauth.net/grant_type/device/1.0";
|
|
@@ -190,7 +200,7 @@ var RealDebrid = class RealDebrid extends BaseFetcher {
|
|
|
190
200
|
constructor(apiToken, options = {}) {
|
|
191
201
|
super({
|
|
192
202
|
...options,
|
|
193
|
-
baseUrl: API_BASE$
|
|
203
|
+
baseUrl: API_BASE$7,
|
|
194
204
|
headers: { Authorization: `Bearer ${apiToken}` }
|
|
195
205
|
});
|
|
196
206
|
}
|
|
@@ -242,7 +252,7 @@ var RealDebrid = class RealDebrid extends BaseFetcher {
|
|
|
242
252
|
return (await this.listTorrentsRaw({
|
|
243
253
|
offset,
|
|
244
254
|
limit
|
|
245
|
-
})).map(toTorrent$
|
|
255
|
+
})).map(toTorrent$7);
|
|
246
256
|
}
|
|
247
257
|
/** Unrestricts every host link of a downloaded torrent into direct links. */
|
|
248
258
|
async getDownloadLinks(torrentId) {
|
|
@@ -318,16 +328,16 @@ var RealDebrid = class RealDebrid extends BaseFetcher {
|
|
|
318
328
|
return this.get(`/streaming/mediaInfos/${encodeURIComponent(linkId)}`);
|
|
319
329
|
}
|
|
320
330
|
/** GET /downloads — unrestricted downloads history. */
|
|
321
|
-
listDownloads(pagination = {}) {
|
|
322
|
-
return this.get("/downloads", { ...pagination });
|
|
331
|
+
async listDownloads(pagination = {}) {
|
|
332
|
+
return await this.get("/downloads", { ...pagination }) ?? [];
|
|
323
333
|
}
|
|
324
334
|
/** DELETE /downloads/delete/{id} */
|
|
325
335
|
deleteDownload(downloadId) {
|
|
326
336
|
return this.delete(`/downloads/delete/${encodeURIComponent(downloadId)}`);
|
|
327
337
|
}
|
|
328
338
|
/** GET /torrents — raw wire list. `filter: 'active'` lists active torrents only. */
|
|
329
|
-
listTorrentsRaw(params = {}) {
|
|
330
|
-
return this.get("/torrents", { ...params });
|
|
339
|
+
async listTorrentsRaw(params = {}) {
|
|
340
|
+
return await this.get("/torrents", { ...params }) ?? [];
|
|
331
341
|
}
|
|
332
342
|
/** GET /torrents/info/{id} — raw wire response. */
|
|
333
343
|
getTorrentInfo(torrentId) {
|
|
@@ -335,7 +345,7 @@ var RealDebrid = class RealDebrid extends BaseFetcher {
|
|
|
335
345
|
}
|
|
336
346
|
/** GET /torrents/info/{id} — mapped to the unified Torrent shape. */
|
|
337
347
|
async getTorrent(torrentId) {
|
|
338
|
-
return toTorrent$
|
|
348
|
+
return toTorrent$7(await this.getTorrentInfo(torrentId));
|
|
339
349
|
}
|
|
340
350
|
/**
|
|
341
351
|
* GET /torrents/instantAvailability/{hash}/{hash}… — note that Real-Debrid
|
|
@@ -534,7 +544,7 @@ var RealDebrid = class RealDebrid extends BaseFetcher {
|
|
|
534
544
|
});
|
|
535
545
|
}
|
|
536
546
|
};
|
|
537
|
-
function toTorrent$
|
|
547
|
+
function toTorrent$7(info) {
|
|
538
548
|
const files = "files" in info ? info.files : void 0;
|
|
539
549
|
const selected = files?.filter((file) => file.selected === 1) ?? [];
|
|
540
550
|
const linkFor = (fileId) => selected.length === info.links.length ? info.links[selected.findIndex((file) => file.id === fileId)] : void 0;
|
|
@@ -576,7 +586,7 @@ function toDownloadLink$1(link) {
|
|
|
576
586
|
* Implements the unified {@link DebridProvider} interface on top of the raw
|
|
577
587
|
* per-endpoint methods (grouped by namespace exactly like the API docs).
|
|
578
588
|
*/
|
|
579
|
-
const API_BASE$
|
|
589
|
+
const API_BASE$6 = "https://api.alldebrid.com";
|
|
580
590
|
var AllDebrid = class AllDebrid extends BaseFetcher {
|
|
581
591
|
/**
|
|
582
592
|
* @param apiKey Apikey from https://alldebrid.com/apikeys/
|
|
@@ -584,7 +594,7 @@ var AllDebrid = class AllDebrid extends BaseFetcher {
|
|
|
584
594
|
constructor(apiKey, options = {}) {
|
|
585
595
|
super({
|
|
586
596
|
...options,
|
|
587
|
-
baseUrl: API_BASE$
|
|
597
|
+
baseUrl: API_BASE$6,
|
|
588
598
|
headers: { Authorization: `Bearer ${apiKey}` }
|
|
589
599
|
});
|
|
590
600
|
}
|
|
@@ -629,7 +639,7 @@ var AllDebrid = class AllDebrid extends BaseFetcher {
|
|
|
629
639
|
/** AD has no server-side pagination for /magnet/status — slice locally. */
|
|
630
640
|
async listTorrents(offset = 0, limit = 100) {
|
|
631
641
|
const { magnets } = await this.getMagnetsStatus();
|
|
632
|
-
return magnets.slice(offset, offset + limit).map(toTorrent$
|
|
642
|
+
return magnets.slice(offset, offset + limit).map(toTorrent$6);
|
|
633
643
|
}
|
|
634
644
|
/**
|
|
635
645
|
* Direct download links for every file of a ready magnet. The file links
|
|
@@ -788,7 +798,7 @@ var AllDebrid = class AllDebrid extends BaseFetcher {
|
|
|
788
798
|
const { magnets } = await this.getMagnetsStatus({ id: Number(torrentId) });
|
|
789
799
|
const magnet = magnets.find((item) => String(item.id) === String(torrentId)) ?? magnets[0];
|
|
790
800
|
if (!magnet) throw new DebridError(`Magnet ${torrentId} not found`, 404, "MAGNET_INVALID_ID");
|
|
791
|
-
return toTorrent$
|
|
801
|
+
return toTorrent$6(magnet);
|
|
792
802
|
}
|
|
793
803
|
/** POST /v4/magnet/files — file tree (with `/f/` links to unlock) for the given magnet ids. */
|
|
794
804
|
getMagnetFiles(ids) {
|
|
@@ -823,7 +833,7 @@ var AllDebrid = class AllDebrid extends BaseFetcher {
|
|
|
823
833
|
static #public(options = {}) {
|
|
824
834
|
return new BaseFetcher({
|
|
825
835
|
...options,
|
|
826
|
-
baseUrl: API_BASE$
|
|
836
|
+
baseUrl: API_BASE$6
|
|
827
837
|
});
|
|
828
838
|
}
|
|
829
839
|
/** GET /v4.1/pin/get — start the PIN flow: show `user_url` (or `pin`), then poll. */
|
|
@@ -855,7 +865,7 @@ function unwrap(envelope) {
|
|
|
855
865
|
return envelope.data;
|
|
856
866
|
}
|
|
857
867
|
/** statusCode → unified TorrentStatus (5–15 are all errors). */
|
|
858
|
-
function toTorrentStatus$
|
|
868
|
+
function toTorrentStatus$4(statusCode) {
|
|
859
869
|
switch (statusCode) {
|
|
860
870
|
case 0: return "queued";
|
|
861
871
|
case 1: return "downloading";
|
|
@@ -876,7 +886,7 @@ function flattenTorrentFiles(nodes, out = []) {
|
|
|
876
886
|
});
|
|
877
887
|
return out;
|
|
878
888
|
}
|
|
879
|
-
function toTorrent$
|
|
889
|
+
function toTorrent$6(magnet) {
|
|
880
890
|
const files = magnet.files ? flattenTorrentFiles(magnet.files) : void 0;
|
|
881
891
|
return {
|
|
882
892
|
id: String(magnet.id),
|
|
@@ -884,7 +894,7 @@ function toTorrent$1(magnet) {
|
|
|
884
894
|
hash: magnet.hash ?? "",
|
|
885
895
|
bytes: magnet.size,
|
|
886
896
|
progress: magnet.statusCode === 4 ? 100 : magnet.size > 0 && magnet.downloaded ? Math.min(100, Math.round(magnet.downloaded / magnet.size * 100)) : 0,
|
|
887
|
-
status: toTorrentStatus$
|
|
897
|
+
status: toTorrentStatus$4(magnet.statusCode),
|
|
888
898
|
files,
|
|
889
899
|
links: (files ?? []).map((f) => f.downloadUrl).filter((link) => !!link),
|
|
890
900
|
addedAt: magnet.uploadDate > 0 ? /* @__PURE__ */ new Date(magnet.uploadDate * 1e3) : void 0,
|
|
@@ -903,7 +913,7 @@ function collectFiles(nodes, out = []) {
|
|
|
903
913
|
}
|
|
904
914
|
//#endregion
|
|
905
915
|
//#region src/services/premiumize/premiumize.ts
|
|
906
|
-
const API_BASE = "https://www.premiumize.me/api";
|
|
916
|
+
const API_BASE$5 = "https://www.premiumize.me/api";
|
|
907
917
|
const OAUTH_BASE = "https://www.premiumize.me";
|
|
908
918
|
var Premiumize = class Premiumize extends BaseFetcher {
|
|
909
919
|
/** Kept for the two-step upload (its url is outside the API base). */
|
|
@@ -914,7 +924,7 @@ var Premiumize = class Premiumize extends BaseFetcher {
|
|
|
914
924
|
constructor(apiKey, options = {}) {
|
|
915
925
|
super({
|
|
916
926
|
...options,
|
|
917
|
-
baseUrl: API_BASE,
|
|
927
|
+
baseUrl: API_BASE$5,
|
|
918
928
|
headers: { Authorization: `Bearer ${apiKey}` }
|
|
919
929
|
});
|
|
920
930
|
this.#fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
@@ -953,7 +963,7 @@ var Premiumize = class Premiumize extends BaseFetcher {
|
|
|
953
963
|
/** /transfer/list has no server-side pagination — slice locally. */
|
|
954
964
|
async listTorrents(offset = 0, limit = 100) {
|
|
955
965
|
const { transfers } = await this.listTransfers();
|
|
956
|
-
return transfers.slice(offset, offset + limit).map(toTorrent);
|
|
966
|
+
return transfers.slice(offset, offset + limit).map(toTorrent$5);
|
|
957
967
|
}
|
|
958
968
|
/**
|
|
959
969
|
* Direct links for a finished transfer: a single-file source resolves to its
|
|
@@ -1075,7 +1085,7 @@ var Premiumize = class Premiumize extends BaseFetcher {
|
|
|
1075
1085
|
}
|
|
1076
1086
|
/** GET /transfer/list mapped to the unified Torrent shape. */
|
|
1077
1087
|
async getTorrent(torrentId) {
|
|
1078
|
-
return toTorrent(await this.getTransfer(torrentId));
|
|
1088
|
+
return toTorrent$5(await this.getTransfer(torrentId));
|
|
1079
1089
|
}
|
|
1080
1090
|
/** GET /transfer/list + id lookup — the API has no single-transfer endpoint. */
|
|
1081
1091
|
async getTransfer(transferId) {
|
|
@@ -1194,7 +1204,7 @@ var Premiumize = class Premiumize extends BaseFetcher {
|
|
|
1194
1204
|
return out;
|
|
1195
1205
|
}
|
|
1196
1206
|
};
|
|
1197
|
-
function toTorrentStatus(status) {
|
|
1207
|
+
function toTorrentStatus$3(status) {
|
|
1198
1208
|
switch (status) {
|
|
1199
1209
|
case "queued": return "queued";
|
|
1200
1210
|
case "running": return "downloading";
|
|
@@ -1207,14 +1217,14 @@ function toTorrentStatus(status) {
|
|
|
1207
1217
|
* `/transfer/list` carries no hash, size or file data, so those stay empty —
|
|
1208
1218
|
* call {@link Premiumize.getDownloadLinks} for the real files.
|
|
1209
1219
|
*/
|
|
1210
|
-
function toTorrent(transfer) {
|
|
1220
|
+
function toTorrent$5(transfer) {
|
|
1211
1221
|
return {
|
|
1212
1222
|
id: transfer.id,
|
|
1213
1223
|
name: transfer.name,
|
|
1214
1224
|
hash: "",
|
|
1215
1225
|
bytes: 0,
|
|
1216
1226
|
progress: Math.round(transfer.progress * 100),
|
|
1217
|
-
status: toTorrentStatus(transfer.status),
|
|
1227
|
+
status: toTorrentStatus$3(transfer.status),
|
|
1218
1228
|
links: [],
|
|
1219
1229
|
addedAt: transfer.created_at ? /* @__PURE__ */ new Date(transfer.created_at * 1e3) : void 0
|
|
1220
1230
|
};
|
|
@@ -1231,4 +1241,645 @@ function toDownloadLink(item) {
|
|
|
1231
1241
|
};
|
|
1232
1242
|
}
|
|
1233
1243
|
//#endregion
|
|
1234
|
-
|
|
1244
|
+
//#region src/services/torbox/torbox.ts
|
|
1245
|
+
const API_BASE$4 = "https://api.torbox.app/v1/api";
|
|
1246
|
+
var TorBox = class extends BaseFetcher {
|
|
1247
|
+
/** `/torrents/requestdl` takes the token as a query param, not the header. */
|
|
1248
|
+
#token;
|
|
1249
|
+
/** @param apiKey API key from https://torbox.app/settings */
|
|
1250
|
+
constructor(apiKey, options = {}) {
|
|
1251
|
+
super({
|
|
1252
|
+
...options,
|
|
1253
|
+
baseUrl: API_BASE$4,
|
|
1254
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
1255
|
+
});
|
|
1256
|
+
this.#token = apiKey;
|
|
1257
|
+
}
|
|
1258
|
+
/** Unwraps the `{ success, error, detail, data }` envelope. */
|
|
1259
|
+
async request(method, path, options = {}) {
|
|
1260
|
+
const body = await super.request(method, path, options);
|
|
1261
|
+
if (!body?.success) throw new DebridError(body?.detail ?? "TorBox error", 200, body?.error ?? void 0, body);
|
|
1262
|
+
return body.data;
|
|
1263
|
+
}
|
|
1264
|
+
name = "torbox";
|
|
1265
|
+
async getUser() {
|
|
1266
|
+
const user = await this.get("/user/me");
|
|
1267
|
+
const expiresAt = user.premium_expires_at ? new Date(user.premium_expires_at) : null;
|
|
1268
|
+
return {
|
|
1269
|
+
id: String(user.id),
|
|
1270
|
+
username: user.email,
|
|
1271
|
+
email: user.email,
|
|
1272
|
+
premium: user.plan > 0 && (expiresAt === null || expiresAt.getTime() > Date.now()),
|
|
1273
|
+
expiresAt
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
/** Real lookup via `/torrents/checkcached`, no side effects. */
|
|
1277
|
+
async isCached(hashOrMagnet) {
|
|
1278
|
+
return (await this.checkCached([toInfoHash(hashOrMagnet)])).length > 0;
|
|
1279
|
+
}
|
|
1280
|
+
async addMagnet(magnet) {
|
|
1281
|
+
const form = new FormData();
|
|
1282
|
+
form.append("magnet", magnet);
|
|
1283
|
+
return this.getTorrent(requireStarted(await this.#create(form)));
|
|
1284
|
+
}
|
|
1285
|
+
/** Adds a `.torrent` file and returns its torrent id. */
|
|
1286
|
+
async addTorrentFile(file, filename = "upload.torrent") {
|
|
1287
|
+
const form = new FormData();
|
|
1288
|
+
form.append("file", toBlob(file), filename);
|
|
1289
|
+
return requireStarted(await this.#create(form));
|
|
1290
|
+
}
|
|
1291
|
+
#create(form) {
|
|
1292
|
+
return this.postBody("/torrents/createtorrent", form);
|
|
1293
|
+
}
|
|
1294
|
+
async listTorrents(offset = 0, limit = 100) {
|
|
1295
|
+
try {
|
|
1296
|
+
return (await this.get("/torrents/mylist", {
|
|
1297
|
+
offset,
|
|
1298
|
+
limit,
|
|
1299
|
+
bypass_cache: true
|
|
1300
|
+
}) ?? []).map(toTorrent$4);
|
|
1301
|
+
} catch (err) {
|
|
1302
|
+
if (err instanceof DebridError && (err.status === 404 || err.code === "ITEM_NOT_FOUND")) return [];
|
|
1303
|
+
throw err;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
async getTorrent(torrentId) {
|
|
1307
|
+
return toTorrent$4(await this.get("/torrents/mylist", {
|
|
1308
|
+
id: torrentId,
|
|
1309
|
+
bypass_cache: true
|
|
1310
|
+
}));
|
|
1311
|
+
}
|
|
1312
|
+
/** Requests one link per file. */
|
|
1313
|
+
async getDownloadLinks(torrentId) {
|
|
1314
|
+
const torrent = await this.getTorrent(torrentId);
|
|
1315
|
+
const links = [];
|
|
1316
|
+
for (const file of torrent.files ?? []) {
|
|
1317
|
+
const url = await this.requestDownloadLink(torrentId, file.id);
|
|
1318
|
+
links.push({
|
|
1319
|
+
id: String(file.id),
|
|
1320
|
+
filename: file.path.split("/").pop() || file.path,
|
|
1321
|
+
bytes: file.bytes,
|
|
1322
|
+
url,
|
|
1323
|
+
originalUrl: url,
|
|
1324
|
+
streamable: false
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
return links;
|
|
1328
|
+
}
|
|
1329
|
+
/** GET /torrents/checkcached — the cached subset of `hashes`. */
|
|
1330
|
+
async checkCached(hashes) {
|
|
1331
|
+
return await this.get("/torrents/checkcached", {
|
|
1332
|
+
hash: hashes.join(","),
|
|
1333
|
+
format: "list"
|
|
1334
|
+
}) ?? [];
|
|
1335
|
+
}
|
|
1336
|
+
/** GET /torrents/requestdl */
|
|
1337
|
+
requestDownloadLink(torrentId, fileId) {
|
|
1338
|
+
return this.get("/torrents/requestdl", {
|
|
1339
|
+
token: this.#token,
|
|
1340
|
+
torrent_id: torrentId,
|
|
1341
|
+
file_id: fileId
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
/** POST /torrents/controltorrent with `operation: "delete"` */
|
|
1345
|
+
async deleteTorrent(torrentId) {
|
|
1346
|
+
await this.postBody("/torrents/controltorrent", JSON.stringify({
|
|
1347
|
+
torrent_id: Number(torrentId),
|
|
1348
|
+
operation: "delete"
|
|
1349
|
+
}), { "Content-Type": "application/json" });
|
|
1350
|
+
}
|
|
1351
|
+
/** This instance as the provider-agnostic {@link DebridProvider}. */
|
|
1352
|
+
toProvider() {
|
|
1353
|
+
return this;
|
|
1354
|
+
}
|
|
1355
|
+
};
|
|
1356
|
+
/** shortcut: a torrent TorBox queued (active-download limit) has no id to poll, so it errors; handle `queued_id` via /torrents/getqueued if users hit this. */
|
|
1357
|
+
function requireStarted(created) {
|
|
1358
|
+
if (created.torrent_id == null) throw new DebridError("TorBox queued this torrent because the active download limit was reached. Try again later.", 200, "ACTIVE_LIMIT", created);
|
|
1359
|
+
return String(created.torrent_id);
|
|
1360
|
+
}
|
|
1361
|
+
function toTorrentStatus$2(torrent) {
|
|
1362
|
+
if (torrent.download_finished && torrent.download_present) return "downloaded";
|
|
1363
|
+
const state = torrent.download_state;
|
|
1364
|
+
if (/error|failed|missing/i.test(state)) return "error";
|
|
1365
|
+
if (state === "metaDL") return "magnet_conversion";
|
|
1366
|
+
if (state === "uploading") return "uploading";
|
|
1367
|
+
if (state === "paused" || state === "checkingResumeData" || state === "queued") return "queued";
|
|
1368
|
+
return "downloading";
|
|
1369
|
+
}
|
|
1370
|
+
function toTorrent$4(torrent) {
|
|
1371
|
+
return {
|
|
1372
|
+
id: String(torrent.id),
|
|
1373
|
+
name: torrent.name,
|
|
1374
|
+
hash: torrent.hash,
|
|
1375
|
+
bytes: torrent.size,
|
|
1376
|
+
progress: Math.round(torrent.progress * 100),
|
|
1377
|
+
status: toTorrentStatus$2(torrent),
|
|
1378
|
+
files: torrent.files?.map((file) => ({
|
|
1379
|
+
id: file.id,
|
|
1380
|
+
path: file.name,
|
|
1381
|
+
bytes: file.size,
|
|
1382
|
+
selected: true
|
|
1383
|
+
})),
|
|
1384
|
+
links: [],
|
|
1385
|
+
addedAt: torrent.created_at ? new Date(torrent.created_at) : void 0,
|
|
1386
|
+
speed: torrent.download_speed,
|
|
1387
|
+
seeders: torrent.seeds,
|
|
1388
|
+
ratio: torrent.ratio
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
//#endregion
|
|
1392
|
+
//#region src/services/debrid-link/debrid-link.ts
|
|
1393
|
+
const API_BASE$3 = "https://debrid-link.com/api/v2";
|
|
1394
|
+
/** The API's maximum page size. */
|
|
1395
|
+
const PER_PAGE = 100;
|
|
1396
|
+
const JSON_HEADERS$2 = { "Content-Type": "application/json" };
|
|
1397
|
+
var DebridLink = class extends BaseFetcher {
|
|
1398
|
+
/** @param token Private API key from https://debrid-link.com/webapp/apikey, or an OAuth access token */
|
|
1399
|
+
constructor(token, options = {}) {
|
|
1400
|
+
super({
|
|
1401
|
+
...options,
|
|
1402
|
+
baseUrl: API_BASE$3,
|
|
1403
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
/** Unwraps `{ success, value }`. */
|
|
1407
|
+
async request(method, path, options = {}) {
|
|
1408
|
+
const body = await super.request(method, path, options);
|
|
1409
|
+
if (!body?.success) throw new DebridError(body?.error ?? "Debrid-Link error", 200, body?.error, body);
|
|
1410
|
+
return body.value;
|
|
1411
|
+
}
|
|
1412
|
+
name = "debrid-link";
|
|
1413
|
+
async getUser() {
|
|
1414
|
+
const account = await this.get("/account/infos");
|
|
1415
|
+
return {
|
|
1416
|
+
id: account.username,
|
|
1417
|
+
username: account.username,
|
|
1418
|
+
email: account.email,
|
|
1419
|
+
avatar: account.avatarUrl,
|
|
1420
|
+
premium: account.accountType > 0,
|
|
1421
|
+
expiresAt: account.accountType === 1 ? new Date(Date.now() + account.premiumLeft * 1e3) : null
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1424
|
+
/**
|
|
1425
|
+
* **Probe:** the API has no cache lookup, but only accepts a bare hash when
|
|
1426
|
+
* it's cached. The probe torrent is removed again; a torrent the account
|
|
1427
|
+
* already had is reported from the list and never touched.
|
|
1428
|
+
*/
|
|
1429
|
+
async isCached(hashOrMagnet) {
|
|
1430
|
+
const hash = toInfoHash(hashOrMagnet);
|
|
1431
|
+
const existing = (await this.#listAll()).find((t) => t.hashString.toLowerCase() === hash);
|
|
1432
|
+
if (existing) return isReady(existing);
|
|
1433
|
+
try {
|
|
1434
|
+
const probe = await this.#add(hash);
|
|
1435
|
+
await this.deleteTorrent(probe.id);
|
|
1436
|
+
return true;
|
|
1437
|
+
} catch (err) {
|
|
1438
|
+
if (err instanceof DebridError && err.code === "notAddTorrent") return false;
|
|
1439
|
+
throw err;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
async addMagnet(magnet) {
|
|
1443
|
+
return toTorrent$3(await this.#add(magnet));
|
|
1444
|
+
}
|
|
1445
|
+
/** Adds a `.torrent` file (multipart). */
|
|
1446
|
+
async addTorrentFile(file, filename = "upload.torrent") {
|
|
1447
|
+
const form = new FormData();
|
|
1448
|
+
form.append("file", toBlob(file), filename);
|
|
1449
|
+
return toTorrent$3(await this.postBody("/seedbox/add", form));
|
|
1450
|
+
}
|
|
1451
|
+
#add(url) {
|
|
1452
|
+
return this.postBody("/seedbox/add", JSON.stringify({ url }), JSON_HEADERS$2);
|
|
1453
|
+
}
|
|
1454
|
+
async listTorrents(offset = 0, limit = 100) {
|
|
1455
|
+
const skip = offset % PER_PAGE;
|
|
1456
|
+
const out = [];
|
|
1457
|
+
for (let page = Math.floor(offset / PER_PAGE);; page++) {
|
|
1458
|
+
const batch = await this.listSeedbox(page);
|
|
1459
|
+
out.push(...batch);
|
|
1460
|
+
if (batch.length < PER_PAGE || out.length >= skip + limit) break;
|
|
1461
|
+
}
|
|
1462
|
+
return out.slice(skip, skip + limit).map(toTorrent$3);
|
|
1463
|
+
}
|
|
1464
|
+
async #listAll() {
|
|
1465
|
+
const out = [];
|
|
1466
|
+
for (let page = 0;; page++) {
|
|
1467
|
+
const batch = await this.listSeedbox(page);
|
|
1468
|
+
out.push(...batch);
|
|
1469
|
+
if (batch.length < PER_PAGE) return out;
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
/** GET /seedbox/list */
|
|
1473
|
+
listSeedbox(page = 0) {
|
|
1474
|
+
return this.get("/seedbox/list", {
|
|
1475
|
+
page,
|
|
1476
|
+
perPage: PER_PAGE
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
async getTorrent(torrentId) {
|
|
1480
|
+
return toTorrent$3(await this.#getRaw(torrentId));
|
|
1481
|
+
}
|
|
1482
|
+
/** `?ids=` returns every file, even for torrents listed as a single ZIP. */
|
|
1483
|
+
async #getRaw(torrentId) {
|
|
1484
|
+
const [torrent] = await this.get("/seedbox/list", { ids: torrentId });
|
|
1485
|
+
if (!torrent) throw new DebridError(`Torrent ${torrentId} not found`, 200, "badId");
|
|
1486
|
+
return torrent;
|
|
1487
|
+
}
|
|
1488
|
+
/** Direct links of the files that finished downloading. */
|
|
1489
|
+
async getDownloadLinks(torrentId) {
|
|
1490
|
+
const { files } = await this.#getRaw(torrentId);
|
|
1491
|
+
return files.filter((file) => file.downloadPercent === 100).map((file) => ({
|
|
1492
|
+
id: file.id,
|
|
1493
|
+
filename: file.name,
|
|
1494
|
+
bytes: file.size,
|
|
1495
|
+
url: file.downloadUrl,
|
|
1496
|
+
originalUrl: file.downloadUrl,
|
|
1497
|
+
streamable: false
|
|
1498
|
+
}));
|
|
1499
|
+
}
|
|
1500
|
+
/** POST /downloader/add — a folder link yields several files. */
|
|
1501
|
+
async unrestrictLink(url) {
|
|
1502
|
+
const value = await this.postBody("/downloader/add", JSON.stringify({ url }), JSON_HEADERS$2);
|
|
1503
|
+
return Array.isArray(value) ? value : [value];
|
|
1504
|
+
}
|
|
1505
|
+
/** DELETE /seedbox/:id/remove */
|
|
1506
|
+
async deleteTorrent(torrentId) {
|
|
1507
|
+
await this.delete(`/seedbox/${encodeURIComponent(torrentId)}/remove`);
|
|
1508
|
+
}
|
|
1509
|
+
/** This instance as the provider-agnostic {@link DebridProvider}. */
|
|
1510
|
+
toProvider() {
|
|
1511
|
+
return this;
|
|
1512
|
+
}
|
|
1513
|
+
};
|
|
1514
|
+
function isReady(torrent) {
|
|
1515
|
+
return torrent.status >= 8 || torrent.downloadPercent === 100;
|
|
1516
|
+
}
|
|
1517
|
+
function toTorrent$3(torrent) {
|
|
1518
|
+
return {
|
|
1519
|
+
id: torrent.id,
|
|
1520
|
+
name: torrent.name,
|
|
1521
|
+
hash: torrent.hashString,
|
|
1522
|
+
bytes: torrent.totalSize,
|
|
1523
|
+
progress: torrent.downloadPercent,
|
|
1524
|
+
status: isReady(torrent) ? "downloaded" : torrent.status === 4 ? "downloading" : "queued",
|
|
1525
|
+
files: (torrent.isZip ? [] : torrent.files).map((file, i) => ({
|
|
1526
|
+
id: i,
|
|
1527
|
+
path: file.name,
|
|
1528
|
+
bytes: file.size,
|
|
1529
|
+
selected: true,
|
|
1530
|
+
downloadUrl: file.downloadUrl || void 0
|
|
1531
|
+
})),
|
|
1532
|
+
links: [],
|
|
1533
|
+
addedAt: torrent.created ? /* @__PURE__ */ new Date(torrent.created * 1e3) : void 0,
|
|
1534
|
+
speed: torrent.downloadSpeed,
|
|
1535
|
+
seeders: torrent.peersConnected,
|
|
1536
|
+
ratio: torrent.uploadRatio
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
//#endregion
|
|
1540
|
+
//#region src/services/offcloud/offcloud.ts
|
|
1541
|
+
const API_BASE$2 = "https://offcloud.com/api";
|
|
1542
|
+
const JSON_HEADERS$1 = { "Content-Type": "application/json" };
|
|
1543
|
+
var Offcloud = class extends BaseFetcher {
|
|
1544
|
+
/** @param apiKey API key from your Offcloud account */
|
|
1545
|
+
constructor(apiKey, options = {}) {
|
|
1546
|
+
super({
|
|
1547
|
+
...options,
|
|
1548
|
+
baseUrl: API_BASE$2,
|
|
1549
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
/** Errors can arrive as `{ error }` with HTTP 200. */
|
|
1553
|
+
async request(method, path, options = {}) {
|
|
1554
|
+
const body = await super.request(method, path, options);
|
|
1555
|
+
if (body?.error) throw new DebridError(body.error, 200, body.error, body);
|
|
1556
|
+
return body;
|
|
1557
|
+
}
|
|
1558
|
+
#postJson(path, json) {
|
|
1559
|
+
return this.postBody(path, JSON.stringify(json), JSON_HEADERS$1);
|
|
1560
|
+
}
|
|
1561
|
+
name = "offcloud";
|
|
1562
|
+
async getUser() {
|
|
1563
|
+
const account = await this.get("/account/info");
|
|
1564
|
+
return {
|
|
1565
|
+
id: account.user_id,
|
|
1566
|
+
username: account.user_id,
|
|
1567
|
+
premium: account.is_premium,
|
|
1568
|
+
expiresAt: account.expiration_date ? new Date(account.expiration_date) : null
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1571
|
+
/** Real lookup via `/cache/info`, no side effects. */
|
|
1572
|
+
async isCached(hashOrMagnet) {
|
|
1573
|
+
const [item] = await this.#postJson("/cache/info", { urls: [toMagnet(hashOrMagnet)] });
|
|
1574
|
+
return item?.cached === true;
|
|
1575
|
+
}
|
|
1576
|
+
async addMagnet(magnet) {
|
|
1577
|
+
const item = await this.#postJson("/cloud", { url: magnet });
|
|
1578
|
+
if (item.not_available) throw new DebridError(`Offcloud can't start this download on your plan (${item.not_available})`, 200, "not_available", item);
|
|
1579
|
+
return toTorrent$2(item);
|
|
1580
|
+
}
|
|
1581
|
+
/** `/cloud/history` has no server-side pagination, so this slices locally. */
|
|
1582
|
+
async listTorrents(offset = 0, limit = 100) {
|
|
1583
|
+
return (await this.get("/cloud/history")).slice(offset, offset + limit).map(toTorrent$2);
|
|
1584
|
+
}
|
|
1585
|
+
/** POST /cloud/status */
|
|
1586
|
+
async getTorrent(torrentId) {
|
|
1587
|
+
const body = await this.#postJson("/cloud/status", { requestId: torrentId });
|
|
1588
|
+
return toTorrent$2({
|
|
1589
|
+
...typeof body.status === "string" ? { status: body.status } : body.status,
|
|
1590
|
+
requestId: torrentId
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
/** Direct links from GET /cloud/explore/:id */
|
|
1594
|
+
async getDownloadLinks(torrentId) {
|
|
1595
|
+
const { files } = await this.get(`/cloud/explore/${encodeURIComponent(torrentId)}`, { format: "detailed" });
|
|
1596
|
+
return files.map((file) => ({
|
|
1597
|
+
id: file.id ?? file.path,
|
|
1598
|
+
filename: file.path,
|
|
1599
|
+
bytes: file.size,
|
|
1600
|
+
url: file.url,
|
|
1601
|
+
originalUrl: file.url,
|
|
1602
|
+
streamable: false
|
|
1603
|
+
}));
|
|
1604
|
+
}
|
|
1605
|
+
/** POST /cloud/remove */
|
|
1606
|
+
async deleteTorrent(torrentId) {
|
|
1607
|
+
await this.#postJson("/cloud/remove", { requestIds: [torrentId] });
|
|
1608
|
+
}
|
|
1609
|
+
/** This instance as the provider-agnostic {@link DebridProvider}. */
|
|
1610
|
+
toProvider() {
|
|
1611
|
+
return this;
|
|
1612
|
+
}
|
|
1613
|
+
};
|
|
1614
|
+
function toTorrentStatus$1(status) {
|
|
1615
|
+
switch (status) {
|
|
1616
|
+
case "created":
|
|
1617
|
+
case "queued": return "queued";
|
|
1618
|
+
case "downloading": return "downloading";
|
|
1619
|
+
case "downloaded": return "downloaded";
|
|
1620
|
+
default: return "error";
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
/** The API reports only a state, not progress or size. */
|
|
1624
|
+
function toTorrent$2(item) {
|
|
1625
|
+
const status = toTorrentStatus$1(item.status);
|
|
1626
|
+
return {
|
|
1627
|
+
id: item.requestId,
|
|
1628
|
+
name: item.fileName ?? "",
|
|
1629
|
+
hash: item.originalLink?.startsWith("magnet:") ? toInfoHash(item.originalLink) : "",
|
|
1630
|
+
bytes: 0,
|
|
1631
|
+
progress: status === "downloaded" ? 100 : 0,
|
|
1632
|
+
status,
|
|
1633
|
+
links: [],
|
|
1634
|
+
addedAt: item.createdOn ? new Date(item.createdOn) : void 0
|
|
1635
|
+
};
|
|
1636
|
+
}
|
|
1637
|
+
//#endregion
|
|
1638
|
+
//#region src/services/putio/putio.ts
|
|
1639
|
+
const API_BASE$1 = "https://api.put.io/v2";
|
|
1640
|
+
const UPLOAD_BASE = "https://upload.put.io/v2";
|
|
1641
|
+
const DIRECTORY = "application/x-directory";
|
|
1642
|
+
var Putio = class extends BaseFetcher {
|
|
1643
|
+
#upload;
|
|
1644
|
+
/** @param token OAuth token from https://app.put.io/oauth */
|
|
1645
|
+
constructor(token, options = {}) {
|
|
1646
|
+
const headers = { Authorization: `Bearer ${token}` };
|
|
1647
|
+
super({
|
|
1648
|
+
...options,
|
|
1649
|
+
baseUrl: API_BASE$1,
|
|
1650
|
+
headers
|
|
1651
|
+
});
|
|
1652
|
+
this.#upload = new BaseFetcher({
|
|
1653
|
+
...options,
|
|
1654
|
+
baseUrl: UPLOAD_BASE,
|
|
1655
|
+
headers
|
|
1656
|
+
});
|
|
1657
|
+
}
|
|
1658
|
+
name = "putio";
|
|
1659
|
+
async getUser() {
|
|
1660
|
+
const { info } = await this.get("/account/info");
|
|
1661
|
+
const expiresAt = info.plan_expiration_date ? new Date(info.plan_expiration_date) : null;
|
|
1662
|
+
return {
|
|
1663
|
+
id: String(info.user_id),
|
|
1664
|
+
username: info.username,
|
|
1665
|
+
email: info.mail,
|
|
1666
|
+
avatar: info.avatar_url,
|
|
1667
|
+
premium: info.account_active && expiresAt !== null && expiresAt.getTime() > Date.now(),
|
|
1668
|
+
expiresAt
|
|
1669
|
+
};
|
|
1670
|
+
}
|
|
1671
|
+
/** put.io has no shared cache to look up, so this is always false. */
|
|
1672
|
+
async isCached(_hashOrMagnet) {
|
|
1673
|
+
return false;
|
|
1674
|
+
}
|
|
1675
|
+
async addMagnet(magnet) {
|
|
1676
|
+
const { transfer } = await this.post("/transfers/add", { url: magnet });
|
|
1677
|
+
return toTorrent$1(transfer);
|
|
1678
|
+
}
|
|
1679
|
+
/** Uploads a `.torrent` file, which starts a transfer, and returns its id. */
|
|
1680
|
+
async addTorrentFile(file, filename = "upload.torrent") {
|
|
1681
|
+
const form = new FormData();
|
|
1682
|
+
form.append("file", toBlob(file), filename);
|
|
1683
|
+
const { transfer } = await this.#upload.postBody("/files/upload", form);
|
|
1684
|
+
if (!transfer) throw new DebridError("put.io did not start a transfer for this file", 200);
|
|
1685
|
+
return String(transfer.id);
|
|
1686
|
+
}
|
|
1687
|
+
/** `/transfers/list` has no server-side pagination, so this slices locally. */
|
|
1688
|
+
async listTorrents(offset = 0, limit = 100) {
|
|
1689
|
+
const { transfers } = await this.get("/transfers/list");
|
|
1690
|
+
return transfers.slice(offset, offset + limit).map(toTorrent$1);
|
|
1691
|
+
}
|
|
1692
|
+
async getTorrent(torrentId) {
|
|
1693
|
+
return toTorrent$1(await this.getTransfer(torrentId));
|
|
1694
|
+
}
|
|
1695
|
+
/** GET /transfers/:id */
|
|
1696
|
+
async getTransfer(transferId) {
|
|
1697
|
+
const { transfer } = await this.get(`/transfers/${encodeURIComponent(transferId)}`);
|
|
1698
|
+
return transfer;
|
|
1699
|
+
}
|
|
1700
|
+
/** Requests one link per file. */
|
|
1701
|
+
async getDownloadLinks(torrentId) {
|
|
1702
|
+
const links = [];
|
|
1703
|
+
for (const file of await this.listTransferFiles(torrentId)) {
|
|
1704
|
+
const url = await this.getFileUrl(file.id);
|
|
1705
|
+
links.push({
|
|
1706
|
+
id: String(file.id),
|
|
1707
|
+
filename: file.path,
|
|
1708
|
+
bytes: file.size,
|
|
1709
|
+
url,
|
|
1710
|
+
originalUrl: url,
|
|
1711
|
+
streamable: false
|
|
1712
|
+
});
|
|
1713
|
+
}
|
|
1714
|
+
return links;
|
|
1715
|
+
}
|
|
1716
|
+
/** Every file a finished transfer produced, walking its folder tree. */
|
|
1717
|
+
async listTransferFiles(transferId) {
|
|
1718
|
+
const transfer = await this.getTransfer(transferId);
|
|
1719
|
+
if (!transfer.file_id) throw new DebridError(`Transfer ${transferId} is ${transfer.status} and has produced no files yet`, 200, "not_found");
|
|
1720
|
+
const { file } = await this.get(`/files/${transfer.file_id}`);
|
|
1721
|
+
return this.#walk(file, "");
|
|
1722
|
+
}
|
|
1723
|
+
async #walk(file, prefix) {
|
|
1724
|
+
const path = prefix ? `${prefix}/${file.name}` : file.name;
|
|
1725
|
+
if (file.content_type !== DIRECTORY) return [{
|
|
1726
|
+
id: file.id,
|
|
1727
|
+
path,
|
|
1728
|
+
size: file.size
|
|
1729
|
+
}];
|
|
1730
|
+
const out = [];
|
|
1731
|
+
for (const child of await this.listFolder(file.id)) out.push(...await this.#walk(child, path));
|
|
1732
|
+
return out;
|
|
1733
|
+
}
|
|
1734
|
+
/** GET /files/list, following the cursor. */
|
|
1735
|
+
async listFolder(folderId) {
|
|
1736
|
+
let page = await this.get("/files/list", {
|
|
1737
|
+
parent_id: folderId,
|
|
1738
|
+
per_page: 1e3
|
|
1739
|
+
});
|
|
1740
|
+
const files = [...page.files];
|
|
1741
|
+
while (page.cursor) {
|
|
1742
|
+
page = await this.post("/files/list/continue", {
|
|
1743
|
+
cursor: page.cursor,
|
|
1744
|
+
per_page: 1e3
|
|
1745
|
+
});
|
|
1746
|
+
files.push(...page.files);
|
|
1747
|
+
}
|
|
1748
|
+
return files;
|
|
1749
|
+
}
|
|
1750
|
+
/** GET /files/:id/url */
|
|
1751
|
+
async getFileUrl(fileId) {
|
|
1752
|
+
const { url } = await this.get(`/files/${fileId}/url`);
|
|
1753
|
+
return url;
|
|
1754
|
+
}
|
|
1755
|
+
/**
|
|
1756
|
+
* Removes the transfer and, once finished, the files it produced, so this
|
|
1757
|
+
* matches what "delete" means on the other providers.
|
|
1758
|
+
*/
|
|
1759
|
+
async deleteTorrent(torrentId) {
|
|
1760
|
+
const transfer = await this.getTransfer(torrentId);
|
|
1761
|
+
await this.post("/transfers/cancel", { transfer_ids: torrentId });
|
|
1762
|
+
if (transfer.status === "SEEDING") await this.post("/transfers/remove", { transfer_ids: torrentId });
|
|
1763
|
+
if (transfer.file_id) await this.post("/files/delete", { file_ids: transfer.file_id });
|
|
1764
|
+
}
|
|
1765
|
+
/** This instance as the provider-agnostic {@link DebridProvider}. */
|
|
1766
|
+
toProvider() {
|
|
1767
|
+
return this;
|
|
1768
|
+
}
|
|
1769
|
+
};
|
|
1770
|
+
/** Unknown states count as in progress, so a new status can't fail a healthy transfer. */
|
|
1771
|
+
function toTorrentStatus(status) {
|
|
1772
|
+
switch (status) {
|
|
1773
|
+
case "COMPLETED":
|
|
1774
|
+
case "SEEDING": return "downloaded";
|
|
1775
|
+
case "ERROR": return "error";
|
|
1776
|
+
case "IN_QUEUE":
|
|
1777
|
+
case "WAITING":
|
|
1778
|
+
case "PREPARING_DOWNLOAD": return "queued";
|
|
1779
|
+
default: return "downloading";
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
/** The spec documents `source`; older responses also carry `magneturi`. */
|
|
1783
|
+
function magnetOf(transfer) {
|
|
1784
|
+
if (transfer.magneturi) return transfer.magneturi;
|
|
1785
|
+
return transfer.source?.startsWith("magnet:") ? transfer.source : void 0;
|
|
1786
|
+
}
|
|
1787
|
+
function toTorrent$1(transfer) {
|
|
1788
|
+
return {
|
|
1789
|
+
id: String(transfer.id),
|
|
1790
|
+
name: transfer.name,
|
|
1791
|
+
hash: toInfoHash(magnetOf(transfer) ?? ""),
|
|
1792
|
+
bytes: transfer.size,
|
|
1793
|
+
progress: transfer.percent_done,
|
|
1794
|
+
status: toTorrentStatus(transfer.status),
|
|
1795
|
+
links: [],
|
|
1796
|
+
addedAt: transfer.created_at ? new Date(transfer.created_at) : void 0,
|
|
1797
|
+
speed: transfer.down_speed,
|
|
1798
|
+
seeders: transfer.peers_connected ?? transfer.peers
|
|
1799
|
+
};
|
|
1800
|
+
}
|
|
1801
|
+
//#endregion
|
|
1802
|
+
//#region src/services/easydebrid/easydebrid.ts
|
|
1803
|
+
const API_BASE = "https://easydebrid.com/api/v1";
|
|
1804
|
+
const JSON_HEADERS = { "Content-Type": "application/json" };
|
|
1805
|
+
var EasyDebrid = class extends BaseFetcher {
|
|
1806
|
+
/** @param apiKey API key from your EasyDebrid dashboard */
|
|
1807
|
+
constructor(apiKey, options = {}) {
|
|
1808
|
+
super({
|
|
1809
|
+
...options,
|
|
1810
|
+
baseUrl: API_BASE,
|
|
1811
|
+
headers: {
|
|
1812
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1813
|
+
Accept: "application/json"
|
|
1814
|
+
}
|
|
1815
|
+
});
|
|
1816
|
+
}
|
|
1817
|
+
/** Errors can arrive as `{ error }` with HTTP 200. */
|
|
1818
|
+
async request(method, path, options = {}) {
|
|
1819
|
+
const body = await super.request(method, path, options);
|
|
1820
|
+
if (body?.error) throw new DebridError(body.error, 200, body.error, body);
|
|
1821
|
+
return body;
|
|
1822
|
+
}
|
|
1823
|
+
name = "easydebrid";
|
|
1824
|
+
async getUser() {
|
|
1825
|
+
const user = await this.get("/user/details");
|
|
1826
|
+
const expiresAt = user.paid_until ? new Date(user.paid_until) : null;
|
|
1827
|
+
return {
|
|
1828
|
+
id: user.id,
|
|
1829
|
+
username: user.id,
|
|
1830
|
+
premium: expiresAt !== null && expiresAt.getTime() > Date.now(),
|
|
1831
|
+
expiresAt
|
|
1832
|
+
};
|
|
1833
|
+
}
|
|
1834
|
+
/** Real lookup via `/link/lookup`, no side effects. */
|
|
1835
|
+
async isCached(hashOrMagnet) {
|
|
1836
|
+
const { cached } = await this.postBody("/link/lookup", JSON.stringify({ urls: [toMagnet(hashOrMagnet)] }), JSON_HEADERS);
|
|
1837
|
+
return cached[0] === true;
|
|
1838
|
+
}
|
|
1839
|
+
/** Nothing is added remotely: this checks the cache and returns the hash as the id. */
|
|
1840
|
+
async addMagnet(magnet) {
|
|
1841
|
+
const hash = toInfoHash(magnet);
|
|
1842
|
+
if (!hash) throw new DebridError("Not a valid magnet link", 400, "invalid_magnet");
|
|
1843
|
+
if (!await this.isCached(hash)) throw new DebridError("This torrent isn't cached on EasyDebrid, which only serves cached content.", 200, "not_cached");
|
|
1844
|
+
return toTorrent(hash, new URLSearchParams(magnet.split("?")[1] ?? "").get("dn") ?? "", true);
|
|
1845
|
+
}
|
|
1846
|
+
/** EasyDebrid keeps no list of torrents per account. */
|
|
1847
|
+
async listTorrents(_offset = 0, _limit = 100) {
|
|
1848
|
+
return [];
|
|
1849
|
+
}
|
|
1850
|
+
async getTorrent(torrentId) {
|
|
1851
|
+
return toTorrent(torrentId, "", await this.isCached(torrentId));
|
|
1852
|
+
}
|
|
1853
|
+
/** Direct links from POST /link/generate */
|
|
1854
|
+
async getDownloadLinks(torrentId) {
|
|
1855
|
+
const { files } = await this.postBody("/link/generate", JSON.stringify({ url: toMagnet(torrentId) }), JSON_HEADERS);
|
|
1856
|
+
return files.map((file) => {
|
|
1857
|
+
const path = [...file.directory ?? [], file.filename].join("/");
|
|
1858
|
+
return {
|
|
1859
|
+
id: path,
|
|
1860
|
+
filename: path,
|
|
1861
|
+
bytes: file.size,
|
|
1862
|
+
url: file.url,
|
|
1863
|
+
originalUrl: file.url,
|
|
1864
|
+
streamable: false
|
|
1865
|
+
};
|
|
1866
|
+
});
|
|
1867
|
+
}
|
|
1868
|
+
/** This instance as the provider-agnostic {@link DebridProvider}. */
|
|
1869
|
+
toProvider() {
|
|
1870
|
+
return this;
|
|
1871
|
+
}
|
|
1872
|
+
};
|
|
1873
|
+
function toTorrent(hash, name, cached) {
|
|
1874
|
+
return {
|
|
1875
|
+
id: hash,
|
|
1876
|
+
name,
|
|
1877
|
+
hash,
|
|
1878
|
+
bytes: 0,
|
|
1879
|
+
progress: cached ? 100 : 0,
|
|
1880
|
+
status: cached ? "downloaded" : "dead",
|
|
1881
|
+
links: []
|
|
1882
|
+
};
|
|
1883
|
+
}
|
|
1884
|
+
//#endregion
|
|
1885
|
+
export { AllDebrid, BaseFetcher, DebridError, DebridLink, EasyDebrid, Offcloud, Premiumize, Putio, REAL_DEBRID_OPENSOURCE_CLIENT_ID, RealDebrid, TorBox };
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "debrid-ts",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
5
|
-
"description": "Typed API clients for debrid services (Real-Debrid, AllDebrid, Premiumize) behind one provider-agnostic interface.",
|
|
4
|
+
"version": "0.2.0",
|
|
5
|
+
"description": "Typed API clients for debrid services (Real-Debrid, AllDebrid, Premiumize, TorBox, Debrid-Link, Offcloud, put.io, EasyDebrid) behind one provider-agnostic interface.",
|
|
6
6
|
"author": "Joshue Abance <iamcoderx@gmail.com>",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"homepage": "https://git.tbdh.app/Libraries/debrid-ts#readme",
|
|
@@ -19,6 +19,11 @@
|
|
|
19
19
|
"realdebrid",
|
|
20
20
|
"alldebrid",
|
|
21
21
|
"premiumize",
|
|
22
|
+
"torbox",
|
|
23
|
+
"debrid-link",
|
|
24
|
+
"offcloud",
|
|
25
|
+
"putio",
|
|
26
|
+
"easydebrid",
|
|
22
27
|
"torrent",
|
|
23
28
|
"magnet",
|
|
24
29
|
"api",
|