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 CHANGED
@@ -1,7 +1,8 @@
1
1
  # debrid-ts
2
2
 
3
- Typed TypeScript clients for debrid services — **Real-Debrid**, **AllDebrid**
4
- and **Premiumize** — with every documented endpoint as a method, plus one
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
- 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, type DebridProvider, type DownloadLink, type FileInput, type FormValue, 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, 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 Torrent, type TorrentFile, type TorrentStatus, type UserInfo };
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: typeof body.error_description === "string" ? body.error_description : error,
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$2 = "https://api.real-debrid.com/rest/1.0";
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$2,
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$2);
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$2(await this.getTorrentInfo(torrentId));
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$2(info) {
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$1 = "https://api.alldebrid.com";
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$1,
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$1);
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$1(magnet);
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$1
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$1(statusCode) {
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$1(magnet) {
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$1(magnet.statusCode),
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
- export { AllDebrid, BaseFetcher, DebridError, Premiumize, REAL_DEBRID_OPENSOURCE_CLIENT_ID, RealDebrid };
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.1.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",