tor-dl 1.0.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.
Files changed (145) hide show
  1. package/AGENTS.md +53 -0
  2. package/README.md +255 -0
  3. package/bin/tordl.js +5 -0
  4. package/dist/bin/tordl.js +5 -0
  5. package/dist/bin/torrent-cli.js +5 -0
  6. package/dist/cli/display.d.ts +7 -0
  7. package/dist/cli/display.d.ts.map +1 -0
  8. package/dist/cli/display.js +58 -0
  9. package/dist/cli/display.js.map +1 -0
  10. package/dist/cli/parser.d.ts +5 -0
  11. package/dist/cli/parser.d.ts.map +1 -0
  12. package/dist/cli/parser.js +122 -0
  13. package/dist/cli/parser.js.map +1 -0
  14. package/dist/cli/progress.d.ts +15 -0
  15. package/dist/cli/progress.d.ts.map +1 -0
  16. package/dist/cli/progress.js +76 -0
  17. package/dist/cli/progress.js.map +1 -0
  18. package/dist/commands/download.d.ts +2 -0
  19. package/dist/commands/download.d.ts.map +1 -0
  20. package/dist/commands/download.js +70 -0
  21. package/dist/commands/download.js.map +1 -0
  22. package/dist/commands/search.d.ts +3 -0
  23. package/dist/commands/search.d.ts.map +1 -0
  24. package/dist/commands/search.js +46 -0
  25. package/dist/commands/search.js.map +1 -0
  26. package/dist/commands/update.d.ts +2 -0
  27. package/dist/commands/update.d.ts.map +1 -0
  28. package/dist/commands/update.js +32 -0
  29. package/dist/commands/update.js.map +1 -0
  30. package/dist/download/engine.d.ts +6 -0
  31. package/dist/download/engine.d.ts.map +1 -0
  32. package/dist/download/engine.js +163 -0
  33. package/dist/download/engine.js.map +1 -0
  34. package/dist/filters/category.d.ts +3 -0
  35. package/dist/filters/category.d.ts.map +1 -0
  36. package/dist/filters/category.js +24 -0
  37. package/dist/filters/category.js.map +1 -0
  38. package/dist/filters/index.d.ts +5 -0
  39. package/dist/filters/index.d.ts.map +1 -0
  40. package/dist/filters/index.js +13 -0
  41. package/dist/filters/index.js.map +1 -0
  42. package/dist/filters/seeds.d.ts +3 -0
  43. package/dist/filters/seeds.d.ts.map +1 -0
  44. package/dist/filters/seeds.js +7 -0
  45. package/dist/filters/seeds.js.map +1 -0
  46. package/dist/filters/size.d.ts +4 -0
  47. package/dist/filters/size.d.ts.map +1 -0
  48. package/dist/filters/size.js +36 -0
  49. package/dist/filters/size.js.map +1 -0
  50. package/dist/filters/sort.d.ts +5 -0
  51. package/dist/filters/sort.d.ts.map +1 -0
  52. package/dist/filters/sort.js +25 -0
  53. package/dist/filters/sort.js.map +1 -0
  54. package/dist/index.d.ts +11 -0
  55. package/dist/index.d.ts.map +1 -0
  56. package/dist/index.js +30 -0
  57. package/dist/index.js.map +1 -0
  58. package/dist/sources/1337x.d.ts +11 -0
  59. package/dist/sources/1337x.d.ts.map +1 -0
  60. package/dist/sources/1337x.js +121 -0
  61. package/dist/sources/1337x.js.map +1 -0
  62. package/dist/sources/eztv.d.ts +11 -0
  63. package/dist/sources/eztv.d.ts.map +1 -0
  64. package/dist/sources/eztv.js +104 -0
  65. package/dist/sources/eztv.js.map +1 -0
  66. package/dist/sources/httpClient.d.ts +24 -0
  67. package/dist/sources/httpClient.d.ts.map +1 -0
  68. package/dist/sources/httpClient.js +42 -0
  69. package/dist/sources/httpClient.js.map +1 -0
  70. package/dist/sources/limetorrent.d.ts +11 -0
  71. package/dist/sources/limetorrent.d.ts.map +1 -0
  72. package/dist/sources/limetorrent.js +113 -0
  73. package/dist/sources/limetorrent.js.map +1 -0
  74. package/dist/sources/nyaa.d.ts +11 -0
  75. package/dist/sources/nyaa.d.ts.map +1 -0
  76. package/dist/sources/nyaa.js +119 -0
  77. package/dist/sources/nyaa.js.map +1 -0
  78. package/dist/sources/rarbg.d.ts +11 -0
  79. package/dist/sources/rarbg.d.ts.map +1 -0
  80. package/dist/sources/rarbg.js +122 -0
  81. package/dist/sources/rarbg.js.map +1 -0
  82. package/dist/sources/registry.d.ts +9 -0
  83. package/dist/sources/registry.d.ts.map +1 -0
  84. package/dist/sources/registry.js +70 -0
  85. package/dist/sources/registry.js.map +1 -0
  86. package/dist/sources/solidtorrents.d.ts +11 -0
  87. package/dist/sources/solidtorrents.d.ts.map +1 -0
  88. package/dist/sources/solidtorrents.js +105 -0
  89. package/dist/sources/solidtorrents.js.map +1 -0
  90. package/dist/sources/thepiratebay.d.ts +11 -0
  91. package/dist/sources/thepiratebay.d.ts.map +1 -0
  92. package/dist/sources/thepiratebay.js +67 -0
  93. package/dist/sources/thepiratebay.js.map +1 -0
  94. package/dist/sources/torlock.d.ts +11 -0
  95. package/dist/sources/torlock.d.ts.map +1 -0
  96. package/dist/sources/torlock.js +116 -0
  97. package/dist/sources/torlock.js.map +1 -0
  98. package/dist/sources/torrentproject.d.ts +11 -0
  99. package/dist/sources/torrentproject.d.ts.map +1 -0
  100. package/dist/sources/torrentproject.js +105 -0
  101. package/dist/sources/torrentproject.js.map +1 -0
  102. package/dist/sources/torrentscsv.d.ts +11 -0
  103. package/dist/sources/torrentscsv.d.ts.map +1 -0
  104. package/dist/sources/torrentscsv.js +62 -0
  105. package/dist/sources/torrentscsv.js.map +1 -0
  106. package/dist/sources/yts.d.ts +12 -0
  107. package/dist/sources/yts.d.ts.map +1 -0
  108. package/dist/sources/yts.js +88 -0
  109. package/dist/sources/yts.js.map +1 -0
  110. package/dist/types.d.ts +60 -0
  111. package/dist/types.d.ts.map +1 -0
  112. package/dist/types.js +3 -0
  113. package/dist/types.js.map +1 -0
  114. package/filters.json +9 -0
  115. package/package.json +37 -0
  116. package/sources.json +105 -0
  117. package/src/cli/display.ts +58 -0
  118. package/src/cli/parser.ts +96 -0
  119. package/src/cli/progress.ts +78 -0
  120. package/src/commands/download.ts +36 -0
  121. package/src/commands/search.ts +53 -0
  122. package/src/commands/update.ts +30 -0
  123. package/src/download/engine.ts +177 -0
  124. package/src/filters/category.ts +26 -0
  125. package/src/filters/index.ts +4 -0
  126. package/src/filters/seeds.ts +5 -0
  127. package/src/filters/size.ts +42 -0
  128. package/src/filters/sort.ts +34 -0
  129. package/src/index.ts +10 -0
  130. package/src/sources/1337x.ts +85 -0
  131. package/src/sources/eztv.ts +70 -0
  132. package/src/sources/httpClient.ts +39 -0
  133. package/src/sources/limetorrent.ts +78 -0
  134. package/src/sources/nyaa.ts +85 -0
  135. package/src/sources/rarbg.ts +86 -0
  136. package/src/sources/registry.ts +70 -0
  137. package/src/sources/solidtorrents.ts +70 -0
  138. package/src/sources/thepiratebay.ts +65 -0
  139. package/src/sources/torlock.ts +82 -0
  140. package/src/sources/torrentproject.ts +70 -0
  141. package/src/sources/torrentscsv.ts +58 -0
  142. package/src/sources/yts.ts +85 -0
  143. package/src/types.d.ts +86 -0
  144. package/src/types.ts +65 -0
  145. package/tsconfig.json +20 -0
@@ -0,0 +1,96 @@
1
+ import { Command } from 'commander';
2
+ import { readFileSync, existsSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { FilterConfig, SearchOptions } from '../types';
5
+
6
+ export function loadFilters(): FilterConfig {
7
+ const filtersPath = join(process.cwd(), 'filters.json');
8
+ if (existsSync(filtersPath)) {
9
+ return JSON.parse(readFileSync(filtersPath, 'utf-8'));
10
+ }
11
+ return {
12
+ category: 'all',
13
+ minSeeds: 0,
14
+ minSize: '0',
15
+ maxSize: '50GB',
16
+ sortBy: 'seeds',
17
+ order: 'desc',
18
+ limit: 50
19
+ };
20
+ }
21
+
22
+ export function createParser(): Command {
23
+ const program = new Command();
24
+
25
+ program
26
+ .name('tordl')
27
+ .description('CLI torrent search and download tool')
28
+ .version('1.0.0')
29
+ .hook('preAction', (thisCommand) => {
30
+ if (thisCommand.name() === 'update') {
31
+ return;
32
+ }
33
+ });
34
+
35
+ program
36
+ .command('search <query>')
37
+ .description('Search for torrents')
38
+ .option('-c, --cat <category>', 'Category: all, movie, tv')
39
+ .option('-s, --min-seeds <number>', 'Minimum seeds', parseInt)
40
+ .option('--min-size <size>', 'Minimum size (e.g., 500MB, 1GB)')
41
+ .option('--max-size <size>', 'Maximum size (e.g., 5GB)')
42
+ .option('-o, --sort <sortBy>', 'Sort by: seeds, size, date')
43
+ .option('--order <order>', 'Order: asc, desc')
44
+ .option('-l, --limit <limit>', 'Result limit', parseInt)
45
+ .option('--sources <sources>', 'Comma-separated source names')
46
+ .action(async (query: string, options) => {
47
+ const filters = loadFilters();
48
+
49
+ const searchOptions: SearchOptions = {
50
+ query,
51
+ category: options.cat || filters.category,
52
+ minSeeds: options.minSeeds ?? filters.minSeeds,
53
+ minSize: options.minSize || filters.minSize,
54
+ maxSize: options.maxSize || filters.maxSize,
55
+ sortBy: (options.sort as 'seeds' | 'size' | 'date') || filters.sortBy,
56
+ order: (options.order as 'asc' | 'desc') || filters.order,
57
+ limit: options.limit || filters.limit,
58
+ sources: options.sources ? options.sources.split(',') : undefined
59
+ };
60
+
61
+ const { searchCommand } = await import('../commands/search');
62
+ await searchCommand(searchOptions);
63
+ });
64
+
65
+ program
66
+ .command('download <number>')
67
+ .description('Download a torrent by number from previous search')
68
+ .option('-p, --path <path>', 'Download path')
69
+ .action(async (number: string, options) => {
70
+ const { downloadCommand } = await import('../commands/download');
71
+ await downloadCommand(parseInt(number), options.path);
72
+ });
73
+
74
+ program
75
+ .command('update')
76
+ .description('Update sources from remote config')
77
+ .alias('u')
78
+ .action(async () => {
79
+ const { updateCommand } = await import('../commands/update');
80
+ await updateCommand();
81
+ });
82
+
83
+ program
84
+ .argument('<number>', 'Torrent number to download')
85
+ .action(async (number: string) => {
86
+ const { downloadCommand } = await import('../commands/download');
87
+ await downloadCommand(parseInt(number));
88
+ });
89
+
90
+ program.on('command:*', () => {
91
+ console.error('Invalid command: %s\nSee --help for a list of available commands.', program.args[0]);
92
+ process.exit(1);
93
+ });
94
+
95
+ return program;
96
+ }
@@ -0,0 +1,78 @@
1
+ import cliProgress from 'cli-progress';
2
+ import chalk from 'chalk';
3
+
4
+ export class DownloadProgress {
5
+ private bar: cliProgress.SingleBar;
6
+ private startTime: number;
7
+ private totalBytes: number = 0;
8
+ private downloadedBytes: number = 0;
9
+
10
+ constructor() {
11
+ this.startTime = Date.now();
12
+ this.bar = new cliProgress.SingleBar({
13
+ format: '[{bar}] {percentage}% | {speed} | ETA: {eta} | {downloaded}',
14
+ barCompleteChar: '\u2588',
15
+ barIncompleteChar: '\u2591',
16
+ hideCursor: true,
17
+ fps: 10,
18
+ etaAsynchronous: true
19
+ });
20
+ }
21
+
22
+ start(total: number): void {
23
+ this.totalBytes = total;
24
+ this.bar.start(total, 0, {
25
+ speed: '0 B/s',
26
+ downloaded: '0 MB'
27
+ });
28
+ }
29
+
30
+ update(downloaded: number, total: number): void {
31
+ this.downloadedBytes = downloaded;
32
+ this.totalBytes = total || this.totalBytes;
33
+
34
+ const speed = this.calculateSpeed();
35
+ const eta = this.calculateETA();
36
+ const downloadedMB = (this.downloadedBytes / (1024 * 1024)).toFixed(2);
37
+
38
+ this.bar.update(this.downloadedBytes, {
39
+ speed,
40
+ downloaded: `${downloadedMB} MB`,
41
+ eta
42
+ });
43
+ }
44
+
45
+ private calculateSpeed(): string {
46
+ const elapsed = (Date.now() - this.startTime) / 1000;
47
+ const bytesPerSecond = elapsed > 0 ? this.downloadedBytes / elapsed : 0;
48
+ return this.formatBytes(bytesPerSecond) + '/s';
49
+ }
50
+
51
+ private calculateETA(): string {
52
+ const elapsed = (Date.now() - this.startTime) / 1000;
53
+ if (this.downloadedBytes === 0 || this.totalBytes === 0) return 'N/A';
54
+
55
+ const speed = this.downloadedBytes / elapsed;
56
+ const remaining = this.totalBytes - this.downloadedBytes;
57
+ const seconds = remaining / speed;
58
+
59
+ if (seconds < 60) return `${Math.round(seconds)}s`;
60
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${Math.round(seconds % 60)}s`;
61
+ return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`;
62
+ }
63
+
64
+ private formatBytes(bytes: number): string {
65
+ if (bytes < 1024) return bytes + ' B';
66
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB';
67
+ if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
68
+ return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
69
+ }
70
+
71
+ stop(): void {
72
+ this.bar.stop();
73
+ }
74
+
75
+ getTotalDownloaded(): number {
76
+ return this.downloadedBytes;
77
+ }
78
+ }
@@ -0,0 +1,36 @@
1
+ import { displayError, displaySuccess, displayInfo } from '../cli/display';
2
+ import { getCachedResults } from '../download/engine';
3
+ import ora from 'ora';
4
+
5
+ export async function downloadCommand(number: number, savePath?: string): Promise<void> {
6
+ const results = getCachedResults();
7
+
8
+ if (results.length === 0) {
9
+ displayError('No search results found. Run "tordl search <query>" first.');
10
+ process.exit(1);
11
+ }
12
+
13
+ if (number < 1 || number > results.length) {
14
+ displayError(`Invalid selection. Choose a number between 1 and ${results.length}.`);
15
+ process.exit(1);
16
+ }
17
+
18
+ const result = results[number - 1];
19
+
20
+ displayInfo(`Downloading: ${result.name}`);
21
+ displayInfo(`Source: ${result.source}`);
22
+ displayInfo(`Size: ${result.size}`);
23
+ displayInfo(`Seeds: ${result.seeds}\n`);
24
+
25
+ const spinner = ora('Starting download...').start();
26
+
27
+ try {
28
+ const { downloadTorrent } = await import('../download/engine');
29
+ await downloadTorrent(result, { savePath });
30
+ spinner.succeed('Download complete!');
31
+ } catch (error: any) {
32
+ spinner.fail(`Download failed: ${error.message}`);
33
+ displayError('Make sure you have a stable internet connection and sufficient disk space.');
34
+ process.exit(1);
35
+ }
36
+ }
@@ -0,0 +1,53 @@
1
+ import ora from 'ora';
2
+ import { SearchOptions, TorrentResult } from '../types';
3
+ import { getEnabledSources } from '../sources/registry';
4
+ import { filterByCategory, filterBySize, filterBySeeds, sortResults } from '../filters';
5
+ import { displayResults, displayError } from '../cli/display';
6
+ import { cacheResults } from '../download/engine';
7
+
8
+ export async function searchCommand(options: SearchOptions): Promise<void> {
9
+ const spinner = ora('Searching torrent sources...').start();
10
+
11
+ let sources = getEnabledSources();
12
+
13
+ if (options.sources && options.sources.length > 0) {
14
+ const requested = options.sources.map(s => s.toLowerCase());
15
+ sources = sources.filter(s => requested.includes(s.name.toLowerCase()));
16
+ }
17
+
18
+ const allResults: TorrentResult[] = [];
19
+
20
+ for (const source of sources) {
21
+ try {
22
+ spinner.text = `Searching ${source.name}...`;
23
+ const results = await source.search(options.query, options.category);
24
+ allResults.push(...results);
25
+ } catch (error) {
26
+ spinner.warn(`Failed to search ${source.name}`);
27
+ }
28
+ }
29
+
30
+ spinner.succeed(`Search complete. Found ${allResults.length} results.`);
31
+
32
+ let filtered = filterByCategory(allResults, options.category || 'all');
33
+
34
+ if (options.minSeeds && options.minSeeds > 0) {
35
+ filtered = filterBySeeds(filtered, options.minSeeds);
36
+ }
37
+
38
+ if (options.minSize || options.maxSize) {
39
+ filtered = filterBySize(filtered, options.minSize, options.maxSize);
40
+ }
41
+
42
+ filtered = sortResults(filtered, options.sortBy || 'seeds', options.order || 'desc');
43
+
44
+ if (options.limit && options.limit > 0) {
45
+ filtered = filtered.slice(0, options.limit);
46
+ }
47
+
48
+ filtered = filtered.map((r, i) => ({ ...r, num: i + 1 }));
49
+
50
+ cacheResults(filtered);
51
+
52
+ displayResults(filtered);
53
+ }
@@ -0,0 +1,30 @@
1
+ import axios from 'axios';
2
+ import { writeFileSync, existsSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { displaySuccess, displayError, displayInfo } from '../cli/display';
5
+ import { getUpdateUrl } from '../sources/registry';
6
+
7
+ export async function updateCommand(): Promise<void> {
8
+ const updateUrl = getUpdateUrl();
9
+
10
+ if (!updateUrl) {
11
+ displayError('No update URL configured. Update sources.json with an updateUrl first.');
12
+ return;
13
+ }
14
+
15
+ displayInfo(`Fetching latest sources from: ${updateUrl}`);
16
+
17
+ try {
18
+ const { data } = await axios.get(updateUrl, { timeout: 15000 });
19
+
20
+ const localPath = join(process.cwd(), 'sources.json');
21
+ writeFileSync(localPath, JSON.stringify(data, null, 2));
22
+
23
+ displaySuccess('Sources updated successfully!');
24
+ displayInfo(`Updated to version: ${data.version || 'unknown'}`);
25
+ displayInfo(`Sources: ${Object.keys(data.sources || {}).join(', ')}`);
26
+ } catch (error: any) {
27
+ displayError(`Failed to update sources: ${error.message}`);
28
+ displayInfo('Check your internet connection and the update URL.');
29
+ }
30
+ }
@@ -0,0 +1,177 @@
1
+ import WebTorrent from 'webtorrent';
2
+ import { DownloadProgress } from '../cli/progress';
3
+ import { TorrentResult, DownloadOptions } from '../types';
4
+ import axios from 'axios';
5
+ import { writeFileSync, readFileSync, existsSync } from 'fs';
6
+ import { join } from 'path';
7
+
8
+ const CACHE_FILE = join(process.cwd(), '.torrent-cache.json');
9
+
10
+ function saveCache(results: TorrentResult[]): void {
11
+ try {
12
+ writeFileSync(CACHE_FILE, JSON.stringify(results, null, 2));
13
+ } catch (e) { /* ignore */ }
14
+ }
15
+
16
+ function loadCache(): TorrentResult[] {
17
+ try {
18
+ if (existsSync(CACHE_FILE)) {
19
+ return JSON.parse(readFileSync(CACHE_FILE, 'utf-8'));
20
+ }
21
+ } catch (e) { /* ignore */ }
22
+ return [];
23
+ }
24
+
25
+ let cachedResults: TorrentResult[] = [];
26
+
27
+ export function cacheResults(results: TorrentResult[]): void {
28
+ cachedResults = results;
29
+ saveCache(results);
30
+ }
31
+
32
+ export function getCachedResults(): TorrentResult[] {
33
+ if (cachedResults.length === 0) {
34
+ cachedResults = loadCache();
35
+ }
36
+ return cachedResults;
37
+ }
38
+
39
+ export async function downloadTorrent(
40
+ result: TorrentResult,
41
+ options: DownloadOptions = {}
42
+ ): Promise<void> {
43
+ const client = new WebTorrent();
44
+ const progress = new DownloadProgress();
45
+ const savePath = options.savePath || process.cwd();
46
+
47
+ let torrentId: string;
48
+
49
+ if (result.magnet && result.magnet.startsWith('magnet:')) {
50
+ torrentId = result.magnet;
51
+ } else if (result.url && result.url.includes('magnet:')) {
52
+ torrentId = result.url;
53
+ } else if (result.url && result.url.includes('nyaa.si')) {
54
+ console.log('Fetching magnet link from Nyaa...');
55
+ const magnet = await getMagnetFromResult(result);
56
+ if (magnet) {
57
+ torrentId = magnet;
58
+ } else {
59
+ throw new Error('Could not get magnet link from Nyaa page');
60
+ }
61
+ } else {
62
+ console.log('Fetching torrent file...');
63
+ const torrentUrl = await getTorrentUrl(result);
64
+ if (torrentUrl) {
65
+ const response = await axios.get(torrentUrl, { responseType: 'arraybuffer' });
66
+ const tempFile = join(savePath, 'temp.torrent');
67
+ writeFileSync(tempFile, response.data);
68
+ torrentId = tempFile;
69
+ } else {
70
+ throw new Error('Could not get torrent URL');
71
+ }
72
+ }
73
+
74
+ console.log(`\nDownloading: ${result.name}`);
75
+ console.log(`Saving to: ${savePath}\n`);
76
+
77
+ return new Promise((resolve, reject) => {
78
+ console.log('Adding torrent to client...');
79
+ console.log('Torrent ID:', torrentId.substring(0, 60) + '...');
80
+
81
+ const torrent = client.add(torrentId, {
82
+ path: savePath
83
+ });
84
+
85
+ torrent.on('warning', (warn) => {
86
+ console.log('Torrent warning:', warn.message);
87
+ });
88
+
89
+ torrent.on('peer', (peer) => {
90
+ console.log('New peer connected:', peer);
91
+ });
92
+
93
+ console.log('Torrent added, waiting for ready...');
94
+
95
+ torrent.on('ready', () => {
96
+ const total = torrent.length;
97
+ console.log(`✓ Torrent ready! Total size: ${(total / (1024*1024)).toFixed(2)} MB`);
98
+ console.log(` Files: ${torrent.files.map(f => f.name).join(', ')}`);
99
+ console.log(` Info hash: ${torrent.infoHash}`);
100
+ progress.start(total);
101
+
102
+ torrent.on('download', (bytes: number) => {
103
+ const percent = ((torrent.downloaded / total) * 100).toFixed(1);
104
+ process.stdout.write(`\rDownloading: ${percent}% (${(torrent.downloaded/1024/1024).toFixed(1)} MB / ${(total/1024/1024).toFixed(1)} MB) - ${torrent.peers.length} peers `);
105
+ progress.update(torrent.downloaded, total);
106
+ });
107
+
108
+ torrent.on('done', () => {
109
+ progress.stop();
110
+ console.log('\n\n✓ Download complete!');
111
+ console.log(`Downloaded to: ${savePath}`);
112
+ client.destroy();
113
+ resolve();
114
+ });
115
+ });
116
+
117
+ torrent.on('error', (err: Error) => {
118
+ console.error('\nDownload error:', err.message);
119
+ client.destroy();
120
+ reject(err);
121
+ });
122
+
123
+ setTimeout(() => {
124
+ console.log('\n\nDebug:');
125
+ console.log(' Downloaded:', torrent.downloaded);
126
+ console.log(' Length:', torrent.length);
127
+ console.log(' Peers:', torrent.peers?.length || 0);
128
+ console.log(' Done:', torrent.done);
129
+
130
+ if (torrent.downloaded > 0) {
131
+ console.log('\nDownload in progress but taking long...');
132
+ console.log(`Progress: ${(torrent.downloaded/torrent.length*100).toFixed(1)}%`);
133
+ } else {
134
+ console.log('\nNo progress after 60s - may be stuck or no peers');
135
+ }
136
+ }, 60000);
137
+ });
138
+ }
139
+
140
+ async function getTorrentUrl(result: TorrentResult): Promise<string> {
141
+ if (result.magnet && result.magnet.startsWith('magnet:')) {
142
+ return result.magnet;
143
+ }
144
+ return result.url;
145
+ }
146
+
147
+ async function getMagnetFromResult(result: TorrentResult): Promise<string> {
148
+ if (result.magnet) return result.magnet;
149
+ if (!result.url) return '';
150
+
151
+ try {
152
+ const { data } = await axios.get(result.url, { timeout: 15000 });
153
+ const cheerio = require('cheerio');
154
+ const $ = cheerio.load(data);
155
+ return $('a[href^="magnet:"]').attr('href') || '';
156
+ } catch {
157
+ return '';
158
+ }
159
+ }
160
+
161
+ export async function downloadByNumber(
162
+ number: number,
163
+ savePath?: string
164
+ ): Promise<void> {
165
+ const results = getCachedResults();
166
+
167
+ if (results.length === 0) {
168
+ throw new Error('No search results. Run "tordl search" first.');
169
+ }
170
+
171
+ if (number < 1 || number > results.length) {
172
+ throw new Error(`Invalid number. Choose between 1 and ${results.length}`);
173
+ }
174
+
175
+ const result = results[number - 1];
176
+ await downloadTorrent(result, { savePath });
177
+ }
@@ -0,0 +1,26 @@
1
+ import { TorrentResult } from '../types';
2
+
3
+ export function filterByCategory(results: TorrentResult[], category: string): TorrentResult[] {
4
+ if (!category || category === 'all') {
5
+ return results;
6
+ }
7
+
8
+ return results.filter(r => {
9
+ const name = r.name.toLowerCase();
10
+ const cat = category.toLowerCase();
11
+
12
+ if (cat === 'movie') {
13
+ return name.includes('movie') || name.includes('720p') || name.includes('1080p') ||
14
+ name.includes('2160p') || name.includes('bluray') || name.includes('brrip') ||
15
+ name.includes('webrip') || name.includes('web-dl') || name.includes('dvdrip');
16
+ }
17
+
18
+ if (cat === 'tv') {
19
+ return name.includes('s01') || name.includes('s02') || name.includes('episode') ||
20
+ name.includes('season') || name.includes('tv') || name.includes('hdtv') ||
21
+ name.includes('webrip') || name.includes('web-dl');
22
+ }
23
+
24
+ return true;
25
+ });
26
+ }
@@ -0,0 +1,4 @@
1
+ export { filterByCategory } from './category';
2
+ export { filterBySize, parseSize } from './size';
3
+ export { filterBySeeds } from './seeds';
4
+ export { sortResults, SortField, SortOrder } from './sort';
@@ -0,0 +1,5 @@
1
+ import { TorrentResult } from '../types';
2
+
3
+ export function filterBySeeds(results: TorrentResult[], minSeeds: number = 0): TorrentResult[] {
4
+ return results.filter(r => r.seeds >= minSeeds);
5
+ }
@@ -0,0 +1,42 @@
1
+ import { TorrentResult } from '../types';
2
+
3
+ export function parseSize(size: string): number {
4
+ if (!size || size === 'Unknown' || size === 'N/A') return 0;
5
+
6
+ const match = size.toString().match(/([\d.]+)\s*(GB|MB|TB|KB|B)/i);
7
+ if (!match) return 0;
8
+
9
+ const value = parseFloat(match[1]);
10
+ const unit = match[2].toUpperCase();
11
+
12
+ const multipliers: Record<string, number> = {
13
+ 'B': 1,
14
+ 'KB': 1024,
15
+ 'MB': 1024 ** 2,
16
+ 'GB': 1024 ** 3,
17
+ 'TB': 1024 ** 4
18
+ };
19
+
20
+ return value * (multipliers[unit] || 1);
21
+ }
22
+
23
+ export function filterBySize(
24
+ results: TorrentResult[],
25
+ minSize?: string,
26
+ maxSize?: string
27
+ ): TorrentResult[] {
28
+ if (!minSize && !maxSize) {
29
+ return results;
30
+ }
31
+
32
+ const minBytes = minSize ? parseSize(minSize) : 0;
33
+ const maxBytes = maxSize && maxSize !== '0' ? parseSize(maxSize) : Number.MAX_SAFE_INTEGER;
34
+
35
+ return results.filter(r => {
36
+ const sizeBytes = r.sizeBytes;
37
+ if (typeof sizeBytes !== 'number' || isNaN(sizeBytes)) {
38
+ return true;
39
+ }
40
+ return sizeBytes >= minBytes && sizeBytes <= maxBytes;
41
+ });
42
+ }
@@ -0,0 +1,34 @@
1
+ import { TorrentResult } from '../types';
2
+
3
+ export type SortField = 'seeds' | 'size' | 'date';
4
+ export type SortOrder = 'asc' | 'desc';
5
+
6
+ export function sortResults(
7
+ results: TorrentResult[],
8
+ sortBy: SortField = 'seeds',
9
+ order: SortOrder = 'desc'
10
+ ): TorrentResult[] {
11
+ const sorted = [...results];
12
+
13
+ sorted.sort((a, b) => {
14
+ let comparison = 0;
15
+
16
+ switch (sortBy) {
17
+ case 'seeds':
18
+ comparison = a.seeds - b.seeds;
19
+ break;
20
+ case 'size':
21
+ comparison = (a.sizeBytes || 0) - (b.sizeBytes || 0);
22
+ break;
23
+ case 'date':
24
+ comparison = (a.date || '').localeCompare(b.date || '');
25
+ break;
26
+ default:
27
+ comparison = a.seeds - b.seeds;
28
+ }
29
+
30
+ return order === 'desc' ? -comparison : comparison;
31
+ });
32
+
33
+ return sorted;
34
+ }
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ export * from './types';
2
+ export { createParser, loadFilters } from './cli/parser';
3
+ export * from './cli/display';
4
+ export * from './cli/progress';
5
+ export * from './sources/registry';
6
+ export * from './filters';
7
+ export * from './download/engine';
8
+ export * from './commands/search';
9
+ export * from './commands/download';
10
+ export * from './commands/update';
@@ -0,0 +1,85 @@
1
+ import axios from 'axios';
2
+ import * as cheerio from 'cheerio';
3
+ import { TorrentResult, SourceScraper } from '../types';
4
+ import { DEFAULT_HEADERS, TIMEOUT } from './httpClient';
5
+
6
+ export class _1337xScraper implements SourceScraper {
7
+ name = '1337x';
8
+
9
+ async search(query: string, category?: string): Promise<TorrentResult[]> {
10
+ try {
11
+ const catMap: Record<string, string> = { movie: 'Movies', tv: 'TV', music: 'Music', games: 'Games', apps: 'Applications' };
12
+ const catParam = category && catMap[category] ? `/category-${catMap[category]}` : '';
13
+ const url = `https://1337x.to${catParam}/search/${encodeURIComponent(query)}/1/`;
14
+
15
+ const { data } = await axios.get(url, {
16
+ headers: DEFAULT_HEADERS,
17
+ timeout: TIMEOUT
18
+ });
19
+
20
+ const $ = cheerio.load(data);
21
+ const results: TorrentResult[] = [];
22
+
23
+ $('table.table-list tbody tr').each((i, el) => {
24
+ const title = $(el).find('td.coll-1 a').last().text().trim() || $(el).find('td.coll-1').text().trim();
25
+ const size = $(el).find('td.coll-4').text().trim();
26
+ const seeds = parseInt($(el).find('td.coll-2').text().trim()) || 0;
27
+ const peers = parseInt($(el).find('td.coll-3').text().trim()) || 0;
28
+ const link = 'https://1337x.to' + ($(el).find('td.coll-1 a').last().attr('href') || '');
29
+
30
+ if (title) {
31
+ results.push({
32
+ num: results.length + 1,
33
+ name: title,
34
+ size: size || 'Unknown',
35
+ sizeBytes: this.parseSize(size),
36
+ seeds,
37
+ peers,
38
+ source: '1337x',
39
+ url: link,
40
+ magnet: ''
41
+ });
42
+ }
43
+ });
44
+
45
+ return results;
46
+ } catch (error: unknown) {
47
+ const message = error instanceof Error ? error.message : 'Unknown error';
48
+ console.error(`1337x search error: ${message}`);
49
+ return [];
50
+ }
51
+ }
52
+
53
+ async getTorrentUrl(result: TorrentResult): Promise<string> {
54
+ try {
55
+ const { data } = await axios.get(result.url, { headers: DEFAULT_HEADERS, timeout: TIMEOUT });
56
+ const $ = cheerio.load(data);
57
+ const torrentUrl = $('a[href$=".torrent"]').attr('href') || '';
58
+ return torrentUrl || result.url;
59
+ } catch {
60
+ return result.url;
61
+ }
62
+ }
63
+
64
+ async getMagnet(result: TorrentResult): Promise<string> {
65
+ try {
66
+ const { data } = await axios.get(result.url, { headers: DEFAULT_HEADERS, timeout: TIMEOUT });
67
+ const $ = cheerio.load(data);
68
+ return $('a[href^="magnet:"]').attr('href') || '';
69
+ } catch {
70
+ return '';
71
+ }
72
+ }
73
+
74
+ private parseSize(size: string): number {
75
+ if (!size) return 0;
76
+ const match = size.toString().match(/([\d.]+)\s*(GB|MB|TB|KB)/i);
77
+ if (!match) return 0;
78
+ const value = parseFloat(match[1]);
79
+ const unit = match[2].toUpperCase();
80
+ const multipliers: Record<string, number> = { 'KB': 1024, 'MB': 1024**2, 'GB': 1024**3, 'TB': 1024**4 };
81
+ return value * (multipliers[unit] || 1);
82
+ }
83
+ }
84
+
85
+ export default new _1337xScraper();