tor-dl 1.0.2 → 1.0.4
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 +85 -192
- package/dist/cli/display.d.ts.map +1 -1
- package/dist/cli/display.js +13 -7
- package/dist/cli/display.js.map +1 -1
- package/dist/cli/parser.d.ts.map +1 -1
- package/dist/cli/parser.js +59 -37
- package/dist/cli/parser.js.map +1 -1
- package/dist/commands/download.d.ts +1 -1
- package/dist/commands/download.d.ts.map +1 -1
- package/dist/commands/download.js +55 -49
- package/dist/commands/download.js.map +1 -1
- package/dist/commands/search.d.ts.map +1 -1
- package/dist/commands/search.js +29 -1
- package/dist/commands/search.js.map +1 -1
- package/dist/download/engine.d.ts +1 -3
- package/dist/download/engine.d.ts.map +1 -1
- package/dist/download/engine.js +0 -129
- package/dist/download/engine.js.map +1 -1
- package/dist/filters/seeds.d.ts +1 -1
- package/dist/filters/seeds.d.ts.map +1 -1
- package/dist/filters/seeds.js +4 -1
- package/dist/filters/seeds.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/sources/nyaa.d.ts.map +1 -1
- package/dist/sources/nyaa.js +3 -0
- package/dist/sources/nyaa.js.map +1 -1
- package/dist/sources/thepiratebay.d.ts.map +1 -1
- package/dist/sources/thepiratebay.js +1 -0
- package/dist/sources/thepiratebay.js.map +1 -1
- package/dist/sources/yts.d.ts.map +1 -1
- package/dist/sources/yts.js +1 -0
- package/dist/sources/yts.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/help.txt +30 -0
- package/package.json +3 -5
- package/src/cli/display.ts +15 -7
- package/src/cli/parser.ts +62 -40
- package/src/commands/download.ts +48 -15
- package/src/commands/search.ts +84 -52
- package/src/download/engine.ts +1 -144
- package/src/filters/seeds.ts +4 -1
- package/src/index.ts +0 -1
- package/src/sources/nyaa.ts +3 -0
- package/src/sources/thepiratebay.ts +1 -0
- package/src/sources/yts.ts +1 -0
- package/src/types.ts +2 -0
- package/src/cli/progress.ts +0 -78
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tor-dl",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "CLI torrent search
|
|
3
|
+
"version": "1.0.4",
|
|
4
|
+
"description": "CLI torrent search tool - search, open in browser, copy magnet links",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"tor-dl": "dist/bin/tor-dl.js"
|
|
@@ -14,10 +14,8 @@
|
|
|
14
14
|
"axios": "^1.6.0",
|
|
15
15
|
"chalk": "^4.1.2",
|
|
16
16
|
"cheerio": "^1.0.0-rc.12",
|
|
17
|
-
"cli-progress": "^3.12.0",
|
|
18
17
|
"commander": "^11.1.0",
|
|
19
|
-
"ora": "^6.3.1"
|
|
20
|
-
"webtorrent": "^1.9.7"
|
|
18
|
+
"ora": "^6.3.1"
|
|
21
19
|
},
|
|
22
20
|
"devDependencies": {
|
|
23
21
|
"@types/node": "^20.10.0",
|
package/src/cli/display.ts
CHANGED
|
@@ -3,6 +3,10 @@ import { TorrentResult } from '../types';
|
|
|
3
3
|
|
|
4
4
|
(chalk as any).level = 1;
|
|
5
5
|
|
|
6
|
+
function makeClickable(url: string, text: string): string {
|
|
7
|
+
return `\u001b]8;;${url}\u0007${text}\u001b]8;;\u0007`;
|
|
8
|
+
}
|
|
9
|
+
|
|
6
10
|
export function displayResults(results: TorrentResult[]): void {
|
|
7
11
|
if (results.length === 0) {
|
|
8
12
|
console.log(chalk.yellow('No results found.'));
|
|
@@ -12,9 +16,9 @@ export function displayResults(results: TorrentResult[]): void {
|
|
|
12
16
|
const out = (s: string) => process.stdout.write(s + '\n');
|
|
13
17
|
|
|
14
18
|
out('\n' + [
|
|
15
|
-
'
|
|
16
|
-
'│ Num │ Name │ Size │ Seeds │ Leech │ Source │',
|
|
17
|
-
'
|
|
19
|
+
'┌─────┬───┬────────────────────────────────────────┬────────┬───────┬───────┬────────┐',
|
|
20
|
+
'│ Num │ L │ Name │ Size │ Seeds │ Leech │ Source │',
|
|
21
|
+
'├─────┼───┼────────────────────────────────────────┼────────┼───────┼───────┼────────┤'
|
|
18
22
|
].join('\n'));
|
|
19
23
|
|
|
20
24
|
for (const r of results) {
|
|
@@ -24,14 +28,18 @@ export function displayResults(results: TorrentResult[]): void {
|
|
|
24
28
|
const peers = r.peers > 50 ? chalk.cyan(r.peers.toString().padStart(5)) : r.peers.toString().padStart(5);
|
|
25
29
|
const source = (r.source || 'unknown').slice(0, 6).padEnd(6);
|
|
26
30
|
const num = chalk.cyan(r.num.toString().padStart(3));
|
|
31
|
+
const link = r.torrentUrl || r.magnet ? chalk.green('✓') : ' ';
|
|
32
|
+
|
|
33
|
+
const url = r.torrentUrl || r.magnet || '';
|
|
34
|
+
const clickableName = url ? makeClickable(url, name.padEnd(38)) : name.padEnd(38);
|
|
27
35
|
|
|
28
|
-
out(`│ ${num} │ ${
|
|
36
|
+
out(`│ ${num} │ ${link} │ ${clickableName} │ ${size} │ ${seeds} │ ${peers} │ ${source} │`);
|
|
29
37
|
}
|
|
30
38
|
|
|
31
|
-
out('
|
|
32
|
-
out('
|
|
39
|
+
out('├─────┼───┼────────────────────────────────────────┼────────┼───────┼───────┼────────┤');
|
|
40
|
+
out('└─────┴───┴────────────────────────────────────────┴────────┴───────┴───────┴────────┘');
|
|
33
41
|
out('');
|
|
34
|
-
out('
|
|
42
|
+
out(chalk.gray('Use: tor-dl o <number> to copy link to clipboard'));
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
export function displayResultDetails(result: TorrentResult): void {
|
package/src/cli/parser.ts
CHANGED
|
@@ -3,6 +3,15 @@ import { readFileSync, existsSync } from 'fs';
|
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import { FilterConfig, SearchOptions } from '../types';
|
|
5
5
|
|
|
6
|
+
function getVersion(): string {
|
|
7
|
+
try {
|
|
8
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '../../package.json'), 'utf-8'));
|
|
9
|
+
return pkg.version || '1.0.0';
|
|
10
|
+
} catch {
|
|
11
|
+
return '1.0.0';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
6
15
|
export function loadFilters(): FilterConfig {
|
|
7
16
|
const filtersPath = join(process.cwd(), 'filters.json');
|
|
8
17
|
if (existsSync(filtersPath)) {
|
|
@@ -21,35 +30,59 @@ export function loadFilters(): FilterConfig {
|
|
|
21
30
|
|
|
22
31
|
export function createParser(): Command {
|
|
23
32
|
const program = new Command();
|
|
33
|
+
const filters = loadFilters();
|
|
34
|
+
|
|
35
|
+
const mainHelp = `
|
|
36
|
+
Categories: all, movie, tv, anime, music, games, apps
|
|
37
|
+
Sources:
|
|
38
|
+
yts - YTS (Movies) | eztv - EZTV (TV) | thepiratebay - The Pirate Bay | nyaa - Nyaa.si (Anime)`;
|
|
24
39
|
|
|
25
40
|
program
|
|
26
41
|
.name('tor-dl')
|
|
27
|
-
.description('CLI torrent search
|
|
28
|
-
.version('
|
|
29
|
-
.
|
|
30
|
-
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
});
|
|
42
|
+
.description('CLI torrent search tool - search, open in browser, copy magnet links' + mainHelp)
|
|
43
|
+
.version(getVersion(), '-v, --version')
|
|
44
|
+
.showHelpAfterError()
|
|
45
|
+
.showSuggestionAfterError();
|
|
34
46
|
|
|
35
47
|
program
|
|
36
48
|
.command('search <query>')
|
|
37
|
-
.description('Search for torrents')
|
|
38
|
-
.option('-c, --cat <category>', 'Category
|
|
39
|
-
.option('-s, --min-seeds <number>', 'Minimum
|
|
40
|
-
.option('--
|
|
41
|
-
.option('--
|
|
42
|
-
.option('-
|
|
43
|
-
.option('--
|
|
44
|
-
.option('
|
|
45
|
-
.option('--
|
|
49
|
+
.description('Search for torrents. Use -h after search term for examples: tor-dl search "movie" -h')
|
|
50
|
+
.option('-c, --cat <category>', 'Category (all|movie|tv|anime|music|games|apps)')
|
|
51
|
+
.option('-s, --min-seeds <number>', 'Minimum seeders', parseInt)
|
|
52
|
+
.option('--max-seeds <number>', 'Maximum seeders', parseInt)
|
|
53
|
+
.option('--min-size <size>', 'Min size (e.g. 500MB, 1GB)')
|
|
54
|
+
.option('--max-size <size>', 'Max size (e.g. 5GB)')
|
|
55
|
+
.option('-o, --sort <sortBy>', 'Sort by (seeds|size|date)')
|
|
56
|
+
.option('--order <order>', 'Order (asc|desc)')
|
|
57
|
+
.option('-l, --limit <number>', 'Max results (default: 50)', parseInt)
|
|
58
|
+
.option('--sources <sources>', 'Sources (yts,eztv,thepiratebay,nyaa)')
|
|
59
|
+
.option('-h, --help', 'Show help with examples')
|
|
60
|
+
.allowUnknownOption()
|
|
61
|
+
.hook('preAction', (thisCommand) => {
|
|
62
|
+
const opts = thisCommand.opts();
|
|
63
|
+
if (opts.help || thisCommand.args.includes('-h')) {
|
|
64
|
+
console.log('');
|
|
65
|
+
console.log('Categories: all, movie, tv, anime, music, games, apps');
|
|
66
|
+
console.log('Sources:');
|
|
67
|
+
console.log(' yts - YTS (Movies)');
|
|
68
|
+
console.log(' eztv - EZTV (TV)');
|
|
69
|
+
console.log(' thepiratebay - The Pirate Bay');
|
|
70
|
+
console.log(' nyaa - Nyaa.si (Anime)');
|
|
71
|
+
console.log('');
|
|
72
|
+
console.log('Examples:');
|
|
73
|
+
console.log(' tor-dl search "movie" -c movie -s 100');
|
|
74
|
+
console.log(' tor-dl search "anime" --sources nyaa --max-size 2GB');
|
|
75
|
+
console.log(' tor-dl search "linux" --min-size 500MB -l 10');
|
|
76
|
+
process.exit(0);
|
|
77
|
+
}
|
|
78
|
+
})
|
|
46
79
|
.action(async (query: string, options) => {
|
|
47
|
-
const filters = loadFilters();
|
|
48
80
|
|
|
49
81
|
const searchOptions: SearchOptions = {
|
|
50
82
|
query,
|
|
51
83
|
category: options.cat || filters.category,
|
|
52
84
|
minSeeds: options.minSeeds ?? filters.minSeeds,
|
|
85
|
+
maxSeeds: options.maxSeeds,
|
|
53
86
|
minSize: options.minSize || filters.minSize,
|
|
54
87
|
maxSize: options.maxSize || filters.maxSize,
|
|
55
88
|
sortBy: (options.sort as 'seeds' | 'size' | 'date') || filters.sortBy,
|
|
@@ -62,30 +95,19 @@ export function createParser(): Command {
|
|
|
62
95
|
await searchCommand(searchOptions);
|
|
63
96
|
});
|
|
64
97
|
|
|
65
|
-
program
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
});
|
|
98
|
+
const openCmd = program.command('open <number>', { hidden: true });
|
|
99
|
+
openCmd.description('Open .torrent in browser or copy magnet to clipboard');
|
|
100
|
+
openCmd.action(async (number: string) => {
|
|
101
|
+
const { openInBrowser } = await import('../commands/download');
|
|
102
|
+
await openInBrowser(parseInt(number));
|
|
103
|
+
});
|
|
82
104
|
|
|
83
|
-
program
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
105
|
+
const oCmd = program.command('o <number>');
|
|
106
|
+
oCmd.description('Open .torrent in browser or copy magnet to clipboard');
|
|
107
|
+
oCmd.action(async (number: string) => {
|
|
108
|
+
const { openInBrowser } = await import('../commands/download');
|
|
109
|
+
await openInBrowser(parseInt(number));
|
|
110
|
+
});
|
|
89
111
|
|
|
90
112
|
program.on('command:*', () => {
|
|
91
113
|
console.error('Invalid command: %s\nSee --help for a list of available commands.', program.args[0]);
|
package/src/commands/download.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { displayError, displaySuccess, displayInfo } from '../cli/display';
|
|
2
2
|
import { getCachedResults } from '../download/engine';
|
|
3
|
-
import
|
|
3
|
+
import { exec, execSync } from 'child_process';
|
|
4
|
+
import chalk from 'chalk';
|
|
4
5
|
|
|
5
|
-
export async function
|
|
6
|
+
export async function openInBrowser(number: number): Promise<void> {
|
|
6
7
|
const results = getCachedResults();
|
|
7
8
|
|
|
8
9
|
if (results.length === 0) {
|
|
@@ -17,20 +18,52 @@ export async function downloadCommand(number: number, savePath?: string): Promis
|
|
|
17
18
|
|
|
18
19
|
const result = results[number - 1];
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
if (!result.torrentUrl && !result.magnet) {
|
|
22
|
+
displayError('No .torrent URL or magnet available for this result.');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
24
25
|
|
|
25
|
-
const
|
|
26
|
+
const url = result.torrentUrl || result.magnet || '';
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
28
|
+
if (url.startsWith('magnet:')) {
|
|
29
|
+
try {
|
|
30
|
+
if (process.platform === 'win32') {
|
|
31
|
+
execSync(`echo ${url} | clip`, { stdio: 'ignore' });
|
|
32
|
+
} else if (process.platform === 'darwin') {
|
|
33
|
+
execSync(`echo "${url}" | pbcopy`, { stdio: 'ignore' });
|
|
34
|
+
} else {
|
|
35
|
+
execSync(`echo "${url}" | xclip -selection clipboard`, { stdio: 'ignore' });
|
|
36
|
+
}
|
|
37
|
+
displaySuccess('Magnet link copied to clipboard!');
|
|
38
|
+
console.log(chalk.gray(url));
|
|
39
|
+
} catch {
|
|
40
|
+
displayInfo('Magnet link:');
|
|
41
|
+
console.log(chalk.cyan(url));
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
displayInfo('Opening: ' + url);
|
|
45
|
+
const cmd = process.platform === 'win32' ? `start "" "${url}"` :
|
|
46
|
+
process.platform === 'darwin' ? `open "${url}"` :
|
|
47
|
+
`xdg-open "${url}"`;
|
|
48
|
+
|
|
49
|
+
exec(cmd, (err) => {
|
|
50
|
+
if (err) {
|
|
51
|
+
try {
|
|
52
|
+
if (process.platform === 'win32') {
|
|
53
|
+
execSync(`echo ${url} | clip`, { stdio: 'ignore' });
|
|
54
|
+
} else if (process.platform === 'darwin') {
|
|
55
|
+
execSync(`echo "${url}" | pbcopy`, { stdio: 'ignore' });
|
|
56
|
+
} else {
|
|
57
|
+
execSync(`echo "${url}" | xclip -selection clipboard`, { stdio: 'ignore' });
|
|
58
|
+
}
|
|
59
|
+
displaySuccess('URL copied to clipboard!');
|
|
60
|
+
} catch {
|
|
61
|
+
displayError('Failed to open. Copy this URL manually:');
|
|
62
|
+
console.log(chalk.cyan(url));
|
|
63
|
+
}
|
|
64
|
+
} else {
|
|
65
|
+
displaySuccess('Opened in browser');
|
|
66
|
+
}
|
|
67
|
+
});
|
|
35
68
|
}
|
|
36
69
|
}
|
package/src/commands/search.ts
CHANGED
|
@@ -1,53 +1,85 @@
|
|
|
1
|
-
import ora from 'ora';
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (options.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
1
|
+
import ora from 'ora';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { SearchOptions, TorrentResult } from '../types';
|
|
4
|
+
import { getEnabledSources } from '../sources/registry';
|
|
5
|
+
import { filterByCategory, filterBySize, filterBySeeds, sortResults } from '../filters';
|
|
6
|
+
import { displayResults } from '../cli/display';
|
|
7
|
+
import { cacheResults } from '../download/engine';
|
|
8
|
+
|
|
9
|
+
function displaySearchInfo(options: SearchOptions, sources: any[]): void {
|
|
10
|
+
console.log(chalk.gray('\n--- Search Parameters ---'));
|
|
11
|
+
|
|
12
|
+
const sourceNames = sources.map(s => s.name).join(', ');
|
|
13
|
+
console.log(chalk.white('Sources: ') + chalk.cyan(sourceNames));
|
|
14
|
+
console.log(chalk.white('Query: ') + chalk.cyan(options.query));
|
|
15
|
+
|
|
16
|
+
if (options.category) {
|
|
17
|
+
console.log(chalk.white('Category: ') + chalk.cyan(options.category));
|
|
18
|
+
}
|
|
19
|
+
if (options.minSeeds) {
|
|
20
|
+
const seeds = options.maxSeeds ? `${options.minSeeds} - ${options.maxSeeds}` : `${options.minSeeds}+`;
|
|
21
|
+
console.log(chalk.white('Seeds: ') + chalk.cyan(seeds));
|
|
22
|
+
}
|
|
23
|
+
if (options.minSize) {
|
|
24
|
+
console.log(chalk.white('Min Size: ') + chalk.cyan(options.minSize));
|
|
25
|
+
}
|
|
26
|
+
if (options.maxSize) {
|
|
27
|
+
console.log(chalk.white('Max Size: ') + chalk.cyan(options.maxSize));
|
|
28
|
+
}
|
|
29
|
+
if (options.sortBy) {
|
|
30
|
+
console.log(chalk.white('Sort: ') + chalk.cyan(`${options.sortBy} (${options.order})`));
|
|
31
|
+
}
|
|
32
|
+
if (options.limit) {
|
|
33
|
+
console.log(chalk.white('Limit: ') + chalk.cyan(options.limit.toString()));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log(chalk.gray('------------------------\n'));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function searchCommand(options: SearchOptions): Promise<void> {
|
|
40
|
+
const spinner = ora('Searching torrent sources...').start();
|
|
41
|
+
|
|
42
|
+
let sources = getEnabledSources();
|
|
43
|
+
|
|
44
|
+
if (options.sources && options.sources.length > 0) {
|
|
45
|
+
const requested = options.sources.map(s => s.toLowerCase());
|
|
46
|
+
sources = sources.filter(s => requested.includes(s.name.toLowerCase()));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const allResults: TorrentResult[] = [];
|
|
50
|
+
|
|
51
|
+
for (const source of sources) {
|
|
52
|
+
try {
|
|
53
|
+
spinner.text = `Searching ${source.name}...`;
|
|
54
|
+
const results = await source.search(options.query, options.category);
|
|
55
|
+
allResults.push(...results);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
spinner.warn(`Failed to search ${source.name}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
spinner.succeed(`Search complete. Found ${allResults.length} results.`);
|
|
62
|
+
|
|
63
|
+
let filtered = filterByCategory(allResults, options.category || 'all');
|
|
64
|
+
|
|
65
|
+
if (options.minSeeds && options.minSeeds > 0) {
|
|
66
|
+
filtered = filterBySeeds(filtered, options.minSeeds, options.maxSeeds);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (options.minSize || options.maxSize) {
|
|
70
|
+
filtered = filterBySize(filtered, options.minSize, options.maxSize);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
filtered = sortResults(filtered, options.sortBy || 'seeds', options.order || 'desc');
|
|
74
|
+
|
|
75
|
+
if (options.limit && options.limit > 0) {
|
|
76
|
+
filtered = filtered.slice(0, options.limit);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
filtered = filtered.map((r, i) => ({ ...r, num: i + 1 }));
|
|
80
|
+
|
|
81
|
+
cacheResults(filtered);
|
|
82
|
+
|
|
83
|
+
displaySearchInfo(options, sources);
|
|
84
|
+
displayResults(filtered);
|
|
53
85
|
}
|
package/src/download/engine.ts
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { DownloadProgress } from '../cli/progress';
|
|
3
|
-
import { TorrentResult, DownloadOptions } from '../types';
|
|
4
|
-
import axios from 'axios';
|
|
1
|
+
import { TorrentResult } from '../types';
|
|
5
2
|
import { writeFileSync, readFileSync, existsSync } from 'fs';
|
|
6
3
|
import { join } from 'path';
|
|
7
4
|
|
|
@@ -34,144 +31,4 @@ export function getCachedResults(): TorrentResult[] {
|
|
|
34
31
|
cachedResults = loadCache();
|
|
35
32
|
}
|
|
36
33
|
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 "tor-dl 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
34
|
}
|
package/src/filters/seeds.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { TorrentResult } from '../types';
|
|
2
2
|
|
|
3
|
-
export function filterBySeeds(results: TorrentResult[], minSeeds: number = 0): TorrentResult[] {
|
|
3
|
+
export function filterBySeeds(results: TorrentResult[], minSeeds: number = 0, maxSeeds?: number): TorrentResult[] {
|
|
4
|
+
if (maxSeeds) {
|
|
5
|
+
return results.filter(r => r.seeds >= minSeeds && r.seeds <= maxSeeds);
|
|
6
|
+
}
|
|
4
7
|
return results.filter(r => r.seeds >= minSeeds);
|
|
5
8
|
}
|
package/src/index.ts
CHANGED
package/src/sources/nyaa.ts
CHANGED
|
@@ -36,6 +36,8 @@ export class NyaaScraper implements SourceScraper {
|
|
|
36
36
|
const peers = parseInt($(el).find('td:nth-child(7)').text().trim()) || 0;
|
|
37
37
|
|
|
38
38
|
if (title && title.length > 3) {
|
|
39
|
+
const idMatch = link.match(/\/view\/(\d+)/);
|
|
40
|
+
const torrentUrl = idMatch ? `https://nyaa.si/download/${idMatch[1]}.torrent` : '';
|
|
39
41
|
results.push({
|
|
40
42
|
num: results.length + 1,
|
|
41
43
|
name: title,
|
|
@@ -45,6 +47,7 @@ export class NyaaScraper implements SourceScraper {
|
|
|
45
47
|
peers,
|
|
46
48
|
source: 'Nyaa',
|
|
47
49
|
url: link,
|
|
50
|
+
torrentUrl,
|
|
48
51
|
magnet: ''
|
|
49
52
|
});
|
|
50
53
|
}
|
|
@@ -30,6 +30,7 @@ export class ThePirateBayScraper implements SourceScraper {
|
|
|
30
30
|
peers: parseInt(item.leechers) || 0,
|
|
31
31
|
source: 'ThePirateBay',
|
|
32
32
|
url: `https://thepiratebay.org/torrent/${item.id}`,
|
|
33
|
+
torrentUrl: `magnet:?xt=urn:btih:${item.info_hash}`,
|
|
33
34
|
magnet: `magnet:?xt=urn:btih:${item.info_hash}`,
|
|
34
35
|
hash: item.info_hash
|
|
35
36
|
}));
|
package/src/sources/yts.ts
CHANGED
|
@@ -34,6 +34,7 @@ export class YtsScraper implements SourceScraper {
|
|
|
34
34
|
peers: movie.torrents?.[0]?.peers || 0,
|
|
35
35
|
source: 'YTS',
|
|
36
36
|
url: movie.torrents?.[0]?.url || '',
|
|
37
|
+
torrentUrl: movie.torrents?.[0]?.url || '',
|
|
37
38
|
magnet: movie.torrents?.[0]?.hash ? `magnet:?xt=urn:btih:${movie.torrents[0].hash}` : '',
|
|
38
39
|
hash: movie.torrents?.[0]?.hash
|
|
39
40
|
});
|
package/src/types.ts
CHANGED
|
@@ -11,6 +11,7 @@ export interface TorrentResult {
|
|
|
11
11
|
hash?: string;
|
|
12
12
|
category?: string;
|
|
13
13
|
date?: string;
|
|
14
|
+
torrentUrl?: string;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export interface SourceConfig {
|
|
@@ -43,6 +44,7 @@ export interface SearchOptions {
|
|
|
43
44
|
query: string;
|
|
44
45
|
category?: string;
|
|
45
46
|
minSeeds?: number;
|
|
47
|
+
maxSeeds?: number;
|
|
46
48
|
minSize?: string;
|
|
47
49
|
maxSize?: string;
|
|
48
50
|
sortBy?: 'seeds' | 'size' | 'date';
|