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,70 @@
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 EZTvScraper implements SourceScraper {
7
+ name = 'EZTV';
8
+
9
+ async search(query: string, category?: string): Promise<TorrentResult[]> {
10
+ try {
11
+ const url = `https://eztv.io/search/${encodeURIComponent(query)}`;
12
+ const { data } = await axios.get(url, {
13
+ headers: DEFAULT_HEADERS,
14
+ timeout: TIMEOUT
15
+ });
16
+
17
+ const $ = cheerio.load(data);
18
+ const results: TorrentResult[] = [];
19
+
20
+ $('tr.forum_topic_border, tr.forum_topic').each((i, el) => {
21
+ const title = $(el).find('td.forum_topic_header a').text().trim() ||
22
+ $(el).find('td:nth-child(2) a').text().trim();
23
+ const size = $(el).find('td:nth-child(4)').text().trim();
24
+ const seeds = parseInt($(el).find('td:nth-child(5)').text().trim()) || 0;
25
+ const peers = parseInt($(el).find('td:nth-child(6)').text().trim()) || 0;
26
+ const url = 'https://eztv.io' + ($(el).find('td:nth-child(2) a').attr('href') || '');
27
+ const magnet = $(el).find('a[href^="magnet:"]').attr('href');
28
+
29
+ if (title) {
30
+ results.push({
31
+ num: results.length + 1,
32
+ name: title,
33
+ size: size || 'Unknown',
34
+ sizeBytes: this.parseSize(size),
35
+ seeds,
36
+ peers,
37
+ source: 'EZTV',
38
+ url,
39
+ magnet
40
+ });
41
+ }
42
+ });
43
+
44
+ return results;
45
+ } catch (error: unknown) {
46
+ const message = error instanceof Error ? error.message : 'Unknown error';
47
+ console.error(`EZTV search error: ${message}`);
48
+ return [];
49
+ }
50
+ }
51
+
52
+ async getTorrentUrl(result: TorrentResult): Promise<string> {
53
+ return result.url;
54
+ }
55
+
56
+ async getMagnet(result: TorrentResult): Promise<string> {
57
+ return result.magnet || '';
58
+ }
59
+
60
+ private parseSize(size: string): number {
61
+ const match = size.match(/([\d.]+)\s*(GB|MB|TB|KB)/i);
62
+ if (!match) return 0;
63
+ const value = parseFloat(match[1]);
64
+ const unit = match[2].toUpperCase();
65
+ const multipliers: Record<string, number> = { 'KB': 1024, 'MB': 1024**2, 'GB': 1024**3, 'TB': 1024**4 };
66
+ return value * (multipliers[unit] || 1);
67
+ }
68
+ }
69
+
70
+ export default new EZTvScraper();
@@ -0,0 +1,39 @@
1
+ import axios from 'axios';
2
+
3
+ const USER_AGENTS = [
4
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
5
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0',
6
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
7
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36',
8
+ 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
9
+ ];
10
+
11
+ function getRandomUserAgent(): string {
12
+ return USER_AGENTS[Math.floor(Math.random() * USER_AGENTS.length)];
13
+ }
14
+
15
+ export function getHeaders() {
16
+ return {
17
+ 'User-Agent': getRandomUserAgent(),
18
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
19
+ 'Accept-Language': 'en-US,en;q=0.5',
20
+ 'Accept-Encoding': 'gzip, deflate',
21
+ 'Connection': 'keep-alive',
22
+ 'Upgrade-Insecure-Requests': '1',
23
+ 'Cache-Control': 'max-age=0',
24
+ 'Referer': 'https://www.google.com/'
25
+ };
26
+ }
27
+
28
+ export const DEFAULT_HEADERS = getHeaders();
29
+ export const TIMEOUT = 20000;
30
+
31
+ export function createAxiosInstance() {
32
+ return axios.create({
33
+ headers: getHeaders(),
34
+ timeout: TIMEOUT,
35
+ maxRedirects: 5
36
+ });
37
+ }
38
+
39
+ export const http = createAxiosInstance();
@@ -0,0 +1,78 @@
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 LimetorrentsScraper implements SourceScraper {
7
+ name = 'Limetorrents';
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] ? `/${catMap[category]}` : '';
13
+ const url = `https://www.limetorrent.eu/search${catParam}/${encodeURIComponent(query)}`;
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.table2 tr').each((i, el) => {
24
+ const title = $(el).find('td:nth-child(1) a').text().trim();
25
+ const size = $(el).find('td:nth-child(2)').text().trim();
26
+ const seeds = parseInt($(el).find('td:nth-child(3)').text().trim()) || 0;
27
+ const peers = parseInt($(el).find('td:nth-child(4)').text().trim()) || 0;
28
+ const link = $(el).find('td:nth-child(1) a').attr('href') || '';
29
+
30
+ if (title && !title.includes(' Torrent')) {
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: 'Limetorrents',
39
+ url: link.startsWith('http') ? link : `https://www.limetorrent.eu${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(`Limetorrents search error: ${message}`);
49
+ return [];
50
+ }
51
+ }
52
+
53
+ async getTorrentUrl(result: TorrentResult): Promise<string> {
54
+ return result.url;
55
+ }
56
+
57
+ async getMagnet(result: TorrentResult): Promise<string> {
58
+ try {
59
+ const { data } = await axios.get(result.url, { headers: DEFAULT_HEADERS, timeout: TIMEOUT });
60
+ const $ = cheerio.load(data);
61
+ return $('a[href^="magnet:"]').attr('href') || '';
62
+ } catch {
63
+ return '';
64
+ }
65
+ }
66
+
67
+ private parseSize(size: string): number {
68
+ if (!size) return 0;
69
+ const match = size.toString().match(/([\d.]+)\s*(GB|MB|TB|KB)/i);
70
+ if (!match) return 0;
71
+ const value = parseFloat(match[1]);
72
+ const unit = match[2].toUpperCase();
73
+ const multipliers: Record<string, number> = { 'KB': 1024, 'MB': 1024**2, 'GB': 1024**3, 'TB': 1024**4 };
74
+ return value * (multipliers[unit] || 1);
75
+ }
76
+ }
77
+
78
+ export default new LimetorrentsScraper();
@@ -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 NyaaScraper implements SourceScraper {
7
+ name = 'Nyaa';
8
+
9
+ async search(query: string, category?: string): Promise<TorrentResult[]> {
10
+ try {
11
+ const catMap: Record<string, string> = {
12
+ movie: '0_0',
13
+ tv: '0_0',
14
+ anime: '1_2',
15
+ music: '2_0',
16
+ games: '3_0',
17
+ apps: '4_0'
18
+ };
19
+ const cat = category && catMap[category] ? catMap[category] : '0_0';
20
+ const url = `https://nyaa.si/?q=${encodeURIComponent(query)}&c=${cat}`;
21
+
22
+ const { data } = await axios.get(url, {
23
+ headers: DEFAULT_HEADERS,
24
+ timeout: TIMEOUT
25
+ });
26
+
27
+ const $ = cheerio.load(data);
28
+ const results: TorrentResult[] = [];
29
+
30
+ $('table tbody tr').each((i, el) => {
31
+ const titleEl = $(el).find('td:nth-child(2) a');
32
+ const title = titleEl.text().trim();
33
+ const link = 'https://nyaa.si' + (titleEl.attr('href') || '');
34
+ const size = $(el).find('td:nth-child(4)').text().trim();
35
+ const seeds = parseInt($(el).find('td:nth-child(6)').text().trim()) || 0;
36
+ const peers = parseInt($(el).find('td:nth-child(7)').text().trim()) || 0;
37
+
38
+ if (title && title.length > 3) {
39
+ results.push({
40
+ num: results.length + 1,
41
+ name: title,
42
+ size: size || 'Unknown',
43
+ sizeBytes: this.parseSize(size),
44
+ seeds,
45
+ peers,
46
+ source: 'Nyaa',
47
+ url: link,
48
+ magnet: ''
49
+ });
50
+ }
51
+ });
52
+
53
+ return results.slice(0, 50);
54
+ } catch (error: unknown) {
55
+ const message = error instanceof Error ? error.message : 'Unknown error';
56
+ console.error(`Nyaa search error: ${message}`);
57
+ return [];
58
+ }
59
+ }
60
+
61
+ async getTorrentUrl(result: TorrentResult): Promise<string> {
62
+ return result.url;
63
+ }
64
+
65
+ async getMagnet(result: TorrentResult): Promise<string> {
66
+ try {
67
+ const { data } = await axios.get(result.url, { headers: DEFAULT_HEADERS, timeout: TIMEOUT });
68
+ const $ = cheerio.load(data);
69
+ return $('a[href^="magnet:"]').attr('href') || '';
70
+ } catch {
71
+ return '';
72
+ }
73
+ }
74
+
75
+ private parseSize(size: string): number {
76
+ const match = size.toString().match(/([\d.]+)\s*(GB|MB|TB|KB|GiB|MiB|TiB|KiB)/i);
77
+ if (!match) return 0;
78
+ const value = parseFloat(match[1]);
79
+ const unit = match[2].toUpperCase().replace('I', '');
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 NyaaScraper();
@@ -0,0 +1,86 @@
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 RarBGScraper implements SourceScraper {
7
+ name = 'RarBG';
8
+
9
+ async search(query: string, category?: string): Promise<TorrentResult[]> {
10
+ try {
11
+ const catMap: Record<string, string> = { movie: '4', tv: '14' };
12
+ const catParam = category && catMap[category] ? `&category=${catMap[category]}` : '';
13
+ const url = `https://rarbg.to/torrents.php?search=${encodeURIComponent(query)}${catParam}`;
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.lista2 tr').each((i, el) => {
24
+ const titleEl = $(el).find('td:nth-child(2) a');
25
+ const title = titleEl.text().trim();
26
+ const size = $(el).find('td:nth-child(4)').text().trim();
27
+ const seeds = parseInt($(el).find('td:nth-child(5)').text().trim()) || 0;
28
+ const peers = parseInt($(el).find('td:nth-child(6)').text().trim()) || 0;
29
+ const link = 'https://rarbg.to' + (titleEl.attr('href') || '');
30
+
31
+ if (title && !title.includes('imdb') && !title.includes('tvcache')) {
32
+ results.push({
33
+ num: results.length + 1,
34
+ name: title,
35
+ size: size || 'Unknown',
36
+ sizeBytes: this.parseSize(size),
37
+ seeds,
38
+ peers,
39
+ source: 'RarBG',
40
+ url: link,
41
+ magnet: ''
42
+ });
43
+ }
44
+ });
45
+
46
+ return results;
47
+ } catch (error: unknown) {
48
+ const message = error instanceof Error ? error.message : 'Unknown error';
49
+ console.error(`RarBG search error: ${message}`);
50
+ return [];
51
+ }
52
+ }
53
+
54
+ async getTorrentUrl(result: TorrentResult): Promise<string> {
55
+ try {
56
+ const { data } = await axios.get(result.url, { headers: DEFAULT_HEADERS, timeout: TIMEOUT });
57
+ const $ = cheerio.load(data);
58
+ const torrentUrl = $('a[href$=".torrent"]').attr('href');
59
+ return torrentUrl || '';
60
+ } catch {
61
+ return '';
62
+ }
63
+ }
64
+
65
+ async getMagnet(result: TorrentResult): Promise<string> {
66
+ try {
67
+ const { data } = await axios.get(result.url, { headers: DEFAULT_HEADERS, timeout: TIMEOUT });
68
+ const $ = cheerio.load(data);
69
+ return $('a[href^="magnet:"]').attr('href') || '';
70
+ } catch {
71
+ return '';
72
+ }
73
+ }
74
+
75
+ private parseSize(size: string): number {
76
+ if (!size) return 0;
77
+ const match = size.toString().match(/([\d.]+)\s*(GB|MB|TB|KB)/i);
78
+ if (!match) return 0;
79
+ const value = parseFloat(match[1]);
80
+ const unit = match[2].toUpperCase();
81
+ const multipliers: Record<string, number> = { 'KB': 1024, 'MB': 1024**2, 'GB': 1024**3, 'TB': 1024**4 };
82
+ return value * (multipliers[unit] || 1);
83
+ }
84
+ }
85
+
86
+ export default new RarBGScraper();
@@ -0,0 +1,70 @@
1
+ import { readFileSync, existsSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { SourcesJSON, SourceConfig, SourceScraper } from '../types';
4
+
5
+ import eztv from './eztv';
6
+ import solidtorrents from './solidtorrents';
7
+ import thepiratebay from './thepiratebay';
8
+ import torlock from './torlock';
9
+ import torrentproject from './torrentproject';
10
+ import torrentscsv from './torrentscsv';
11
+ import limetorrent from './limetorrent';
12
+ import _1337x from './1337x';
13
+ import rarbg from './rarbg';
14
+ import yts from './yts';
15
+ import nyaa from './nyaa';
16
+
17
+ const scrapers: Record<string, SourceScraper> = {
18
+ eztv,
19
+ solidtorrents,
20
+ thepiratebay,
21
+ torlock,
22
+ torrentproject,
23
+ torrentscsv,
24
+ limetorrent,
25
+ '1337x': _1337x,
26
+ rarbg,
27
+ yts,
28
+ nyaa
29
+ };
30
+
31
+ export function loadSourcesConfig(): SourcesJSON {
32
+ const sourcesPath = join(process.cwd(), 'sources.json');
33
+ if (existsSync(sourcesPath)) {
34
+ return JSON.parse(readFileSync(sourcesPath, 'utf-8'));
35
+ }
36
+ return { updateUrl: '', version: '1.0.0', sources: {} };
37
+ }
38
+
39
+ export function getEnabledSources(): SourceScraper[] {
40
+ const config = loadSourcesConfig();
41
+ const enabled: SourceScraper[] = [];
42
+
43
+ for (const [key, sourceConfig] of Object.entries(config.sources)) {
44
+ if (sourceConfig.enabled && scrapers[key]) {
45
+ enabled.push(scrapers[key]);
46
+ }
47
+ }
48
+
49
+ if (enabled.length === 0) {
50
+ return Object.values(scrapers);
51
+ }
52
+
53
+ return enabled;
54
+ }
55
+
56
+ export function getSourceConfig(name: string): SourceConfig | undefined {
57
+ const config = loadSourcesConfig();
58
+ return config.sources[name];
59
+ }
60
+
61
+ export function getUpdateUrl(): string {
62
+ const config = loadSourcesConfig();
63
+ return config.updateUrl;
64
+ }
65
+
66
+ export function getAllScrapers(): Record<string, SourceScraper> {
67
+ return scrapers;
68
+ }
69
+
70
+ export { scrapers };
@@ -0,0 +1,70 @@
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 SolidTorrentsScraper implements SourceScraper {
7
+ name = 'SolidTorrents';
8
+
9
+ async search(query: string, category?: string): Promise<TorrentResult[]> {
10
+ try {
11
+ const url = `https://solidtorrents.to/search?q=${encodeURIComponent(query)}`;
12
+ const { data } = await axios.get(url, {
13
+ headers: DEFAULT_HEADERS,
14
+ timeout: TIMEOUT
15
+ });
16
+
17
+ const $ = cheerio.load(data);
18
+ const results: TorrentResult[] = [];
19
+
20
+ $('div[data-href^="/torrent/"]').each((i, el) => {
21
+ const title = $(el).find('a.torrent-title').text().trim();
22
+ const size = $(el).find('span[data-size]').attr('data-size') || 'Unknown';
23
+ const seeds = parseInt($(el).find('span.seed-count').text().trim()) || 0;
24
+ const peers = parseInt($(el).find('span.leech-count').text().trim()) || 0;
25
+ const link = 'https://solidtorrents.to' + $(el).find('a.torrent-title').attr('href');
26
+ const magnet = $(el).find('a[href^="magnet:"]').attr('href');
27
+
28
+ if (title) {
29
+ results.push({
30
+ num: results.length + 1,
31
+ name: title,
32
+ size,
33
+ sizeBytes: this.parseSize(size),
34
+ seeds,
35
+ peers,
36
+ source: 'SolidTorrents',
37
+ url: link,
38
+ magnet
39
+ });
40
+ }
41
+ });
42
+
43
+ return results;
44
+ } catch (error: unknown) {
45
+ const message = error instanceof Error ? error.message : 'Unknown error';
46
+ console.error(`SolidTorrents search error: ${message}`);
47
+ return [];
48
+ }
49
+ }
50
+
51
+ async getTorrentUrl(result: TorrentResult): Promise<string> {
52
+ return result.url;
53
+ }
54
+
55
+ async getMagnet(result: TorrentResult): Promise<string> {
56
+ return result.magnet || '';
57
+ }
58
+
59
+ private parseSize(size: string): number {
60
+ if (!size || size === 'Unknown') return 0;
61
+ const match = size.toString().match(/([\d.]+)\s*(GB|MB|TB|KB)/i);
62
+ if (!match) return 0;
63
+ const value = parseFloat(match[1]);
64
+ const unit = match[2].toUpperCase();
65
+ const multipliers: Record<string, number> = { 'KB': 1024, 'MB': 1024**2, 'GB': 1024**3, 'TB': 1024**4 };
66
+ return value * (multipliers[unit] || 1);
67
+ }
68
+ }
69
+
70
+ export default new SolidTorrentsScraper();
@@ -0,0 +1,65 @@
1
+ import axios from 'axios';
2
+ import { TorrentResult, SourceScraper } from '../types';
3
+ import { DEFAULT_HEADERS, TIMEOUT } from './httpClient';
4
+
5
+ const API_URL = 'https://apibay.org/q.php';
6
+
7
+ const CAT_MAP: Record<string, string> = { movie: '201', tv: '205', music: '102', games: '400', apps: '300' };
8
+
9
+ export class ThePirateBayScraper implements SourceScraper {
10
+ name = 'ThePirateBay';
11
+
12
+ async search(query: string, category?: string): Promise<TorrentResult[]> {
13
+ try {
14
+ const cat = category && CAT_MAP[category] ? CAT_MAP[category] : '0';
15
+ const url = `${API_URL}?q=${encodeURIComponent(query)}&cat=${cat}&limit=100`;
16
+
17
+ const { data } = await axios.get(url, {
18
+ headers: DEFAULT_HEADERS,
19
+ timeout: TIMEOUT
20
+ });
21
+
22
+ if (!Array.isArray(data)) return [];
23
+
24
+ const results: TorrentResult[] = data.slice(0, 50).map((item: any) => ({
25
+ num: 0,
26
+ name: item.name,
27
+ size: this.formatSize(parseInt(item.size)),
28
+ sizeBytes: parseInt(item.size),
29
+ seeds: parseInt(item.seeders) || 0,
30
+ peers: parseInt(item.leechers) || 0,
31
+ source: 'ThePirateBay',
32
+ url: `https://thepiratebay.org/torrent/${item.id}`,
33
+ magnet: `magnet:?xt=urn:btih:${item.info_hash}`,
34
+ hash: item.info_hash
35
+ }));
36
+
37
+ results.forEach((r, i) => r.num = i + 1);
38
+ return results;
39
+ } catch (error: unknown) {
40
+ const message = error instanceof Error ? error.message : 'Unknown error';
41
+ console.error(`ThePirateBay search error: ${message}`);
42
+ return [];
43
+ }
44
+ }
45
+
46
+ async getTorrentUrl(result: TorrentResult): Promise<string> {
47
+ return result.url;
48
+ }
49
+
50
+ async getMagnet(result: TorrentResult): Promise<string> {
51
+ return result.magnet || '';
52
+ }
53
+
54
+ private formatSize(bytes: number): string {
55
+ const gb = bytes / (1024**3);
56
+ if (gb >= 1) return `${gb.toFixed(2)} GB`;
57
+ const mb = bytes / (1024**2);
58
+ if (mb >= 1) return `${mb.toFixed(2)} MB`;
59
+ const kb = bytes / 1024;
60
+ if (kb >= 1) return `${kb.toFixed(2)} KB`;
61
+ return `${bytes} B`;
62
+ }
63
+ }
64
+
65
+ export default new ThePirateBayScraper();
@@ -0,0 +1,82 @@
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
+ const AD_KEYWORDS = ['vpn', 'service', 'banner', 'advertisement', 'sponsored', 'aclib', 'zoneid'];
7
+
8
+ function isAdContent(title: string): boolean {
9
+ const lower = title.toLowerCase();
10
+ return AD_KEYWORDS.some(keyword => lower.includes(keyword));
11
+ }
12
+
13
+ export class TorLockScraper implements SourceScraper {
14
+ name = 'TorLock';
15
+
16
+ async search(query: string, category?: string): Promise<TorrentResult[]> {
17
+ try {
18
+ const url = `https://www.torlock.com/all/torrents/${encodeURIComponent(query)}.html`;
19
+ const { data } = await axios.get(url, {
20
+ headers: DEFAULT_HEADERS,
21
+ timeout: TIMEOUT
22
+ });
23
+
24
+ const $ = cheerio.load(data);
25
+ const results: TorrentResult[] = [];
26
+
27
+ $('table tbody tr').each((i, el) => {
28
+ const title = $(el).find('td:nth-child(2) a').text().trim();
29
+ const size = $(el).find('td:nth-child(3)').text().trim();
30
+ const seeds = parseInt($(el).find('td:nth-child(4)').text().trim()) || 0;
31
+ const peers = parseInt($(el).find('td:nth-child(5)').text().trim()) || 0;
32
+ const link = 'https://www.torlock.com' + ($(el).find('td:nth-child(2) a').attr('href') || '');
33
+
34
+ if (title && !isAdContent(title)) {
35
+ results.push({
36
+ num: results.length + 1,
37
+ name: title,
38
+ size: size || 'Unknown',
39
+ sizeBytes: this.parseSize(size),
40
+ seeds,
41
+ peers,
42
+ source: 'TorLock',
43
+ url: link,
44
+ magnet: ''
45
+ });
46
+ }
47
+ });
48
+
49
+ return results;
50
+ } catch (error: unknown) {
51
+ const message = error instanceof Error ? error.message : 'Unknown error';
52
+ console.error(`TorLock search error: ${message}`);
53
+ return [];
54
+ }
55
+ }
56
+
57
+ async getTorrentUrl(result: TorrentResult): Promise<string> {
58
+ return result.url;
59
+ }
60
+
61
+ async getMagnet(result: TorrentResult): Promise<string> {
62
+ try {
63
+ const { data } = await axios.get(result.url, { headers: DEFAULT_HEADERS, timeout: TIMEOUT });
64
+ const $ = cheerio.load(data);
65
+ return $('a[href^="magnet:"]').attr('href') || '';
66
+ } catch {
67
+ return '';
68
+ }
69
+ }
70
+
71
+ private parseSize(size: string): number {
72
+ if (!size) return 0;
73
+ const match = size.toString().match(/([\d.]+)\s*(GB|MB|TB|KB)/i);
74
+ if (!match) return 0;
75
+ const value = parseFloat(match[1]);
76
+ const unit = match[2].toUpperCase();
77
+ const multipliers: Record<string, number> = { 'KB': 1024, 'MB': 1024**2, 'GB': 1024**3, 'TB': 1024**4 };
78
+ return value * (multipliers[unit] || 1);
79
+ }
80
+ }
81
+
82
+ export default new TorLockScraper();