vanseleem commited on
Commit
cbeefc9
Β·
verified Β·
1 Parent(s): 2cd7a3e

Update index.js

Browse files
Files changed (1) hide show
  1. index.js +519 -101
index.js CHANGED
@@ -3,160 +3,578 @@
3
  const express = require('express');
4
  const axios = require('axios');
5
 
6
- const app = express();
7
  const PORT = process.env.PORT || 7860;
8
 
9
- const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36';
10
-
11
  app.use((req, res, next) => {
12
- res.header('Access-Control-Allow-Origin', '*');
13
  res.header('Access-Control-Allow-Headers', '*');
14
  res.header('Access-Control-Allow-Methods', 'GET, OPTIONS');
15
  if (req.method === 'OPTIONS') return res.sendStatus(200);
16
  next();
17
  });
18
 
19
- // SubsPlease
20
- async function scrapeSubsPlease(title) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  try {
 
22
  const { data } = await axios.get(
23
- `https://subsplease.org/api/?f=search&tz=&s=${encodeURIComponent(title)}`,
24
  { timeout: 8000 }
25
  );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
  const streams = [];
28
- for (const [key, show] of Object.entries(data || {})) {
29
- if (show.downloads) {
30
- for (const dl of show.downloads) {
31
- if (dl.magnet && dl.res) {
32
- streams.push({
33
- title: `${show.show_name} ${dl.res}p`,
34
- magnet: dl.magnet,
35
- res: dl.res,
36
- source: 'subsplease'
37
- });
38
- }
39
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  }
41
  }
42
- return { ok: true, count: streams.length, data: streams.slice(0, 3) };
43
- } catch (e) {
44
- return { ok: false, error: e.message, count: 0, data: [] };
 
 
45
  }
46
  }
47
 
48
- // APIBay
49
- async function scrapeAPIBay(title) {
 
 
 
50
  try {
 
 
 
 
 
 
 
 
51
  const { data } = await axios.get(
52
- `https://apibay.org/q.php?q=${encodeURIComponent(title)}&cat=208`,
53
- { timeout: 10000 }
54
  );
55
 
56
- const streams = (data || []).slice(0, 10).map(item => ({
57
- title: item.name,
58
- seeders: item.seeders || 0,
59
- hash: item.info_hash,
60
- source: 'apibay'
61
- }));
62
 
63
- return { ok: true, count: streams.length, data: streams };
64
- } catch (e) {
65
- return { ok: false, error: e.message, count: 0, data: [] };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  }
67
  }
68
 
69
- // Torrents.csv
70
- async function scrapeTorrentsCSV(title) {
 
 
 
71
  try {
 
 
 
 
 
 
 
 
72
  const { data } = await axios.get(
73
- `https://torrents-csv.com/service/search?q=${encodeURIComponent(title)}`,
74
- { timeout: 10000 }
75
  );
76
 
77
- const streams = (data || []).slice(0, 10).map(item => ({
78
- title: item.name,
79
- seeders: item.seeders || 0,
80
- hash: item.infohash,
81
- source: 'torrentcsv'
82
- }));
83
 
84
- return { ok: true, count: streams.length, data: streams };
85
- } catch (e) {
86
- return { ok: false, error: e.message, count: 0, data: [] };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  }
88
  }
89
 
90
- // Test endpoint
91
- app.get('/test/:query', async (req, res) => {
92
- const query = decodeURIComponent(req.params.query);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
 
94
- const [sp, ab, csv] = await Promise.allSettled([
95
- scrapeSubsPlease(query),
96
- scrapeAPIBay(query),
97
- scrapeTorrentsCSV(query)
98
  ]);
99
 
100
  res.json({
101
- query,
102
- subsplease: sp.status === 'fulfilled' ? sp.value : { ok: false, error: 'Failed', count: 0 },
103
- apibay: ab.status === 'fulfilled' ? ab.value : { ok: false, error: 'Failed', count: 0 },
104
- torrentcsv: csv.status === 'fulfilled' ? csv.value : { ok: false, error: 'Failed', count: 0 }
 
105
  });
106
  });
107
 
108
- // Main page
109
  app.get('/', (req, res) => {
110
- res.type('html').send(`
111
- <!DOCTYPE html>
112
- <html>
 
 
113
  <head>
114
- <title>Test APIs</title>
 
 
115
  <style>
116
- body { font-family: Arial; background: #0d1117; color: #c9d1d9; padding: 20px; }
117
- .container { max-width: 800px; margin: 0 auto; }
118
- h1 { color: #58a6ff; }
119
- .test-btn { background: #1f6feb; color: white; padding: 10px 20px; border: none; border-radius: 6px; cursor: pointer; margin: 5px; }
120
- .test-btn:hover { background: #388bfd; }
121
- pre { background: #161b22; padding: 15px; border-radius: 6px; overflow-x: auto; }
122
- .status { padding: 10px; margin: 10px 0; border-radius: 6px; }
123
- .ok { background: #0d3922; color: #3fb950; }
124
- .error { background: #3d1f1a; color: #ff7b72; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  </style>
126
  </head>
127
  <body>
128
- <div class="container">
129
- <h1>πŸ§ͺ Torrent APIs Test</h1>
130
- <button class="test-btn" onclick="test('Breaking Bad')">Breaking Bad</button>
131
- <button class="test-btn" onclick="test('Solo Leveling')">Solo Leveling</button>
132
- <button class="test-btn" onclick="test('The Office')">The Office</button>
133
- <input type="text" id="query" placeholder="Custom query" style="padding: 8px; width: 200px; background: #0d1117; color: #c9d1d9; border: 1px solid #30363d; border-radius: 6px;">
134
- <button class="test-btn" onclick="customTest()">Search</button>
135
- <pre id="result">Click a button to test...</pre>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  </div>
137
-
138
- <script>
139
- async function test(q) {
140
- document.getElementById('result').textContent = 'Loading...';
141
- try {
142
- const res = await fetch('/test/' + encodeURIComponent(q));
143
- const data = await res.json();
144
- document.getElementById('result').textContent = JSON.stringify(data, null, 2);
145
- } catch (e) {
146
- document.getElementById('result').textContent = 'Error: ' + e.message;
147
- }
148
- }
149
-
150
- function customTest() {
151
- const q = document.getElementById('query').value;
152
- if (q) test(q);
153
- }
154
- </script>
155
  </body>
156
- </html>
157
- `);
158
  });
159
 
160
  app.listen(PORT, '0.0.0.0', () => {
161
- console.log(`βœ… Running on port ${PORT}`);
 
 
 
 
162
  });
 
3
  const express = require('express');
4
  const axios = require('axios');
5
 
6
+ const app = express();
7
  const PORT = process.env.PORT || 7860;
8
 
 
 
9
  app.use((req, res, next) => {
10
+ res.header('Access-Control-Allow-Origin', '*');
11
  res.header('Access-Control-Allow-Headers', '*');
12
  res.header('Access-Control-Allow-Methods', 'GET, OPTIONS');
13
  if (req.method === 'OPTIONS') return res.sendStatus(200);
14
  next();
15
  });
16
 
17
+ const TMDB_KEY = '83d364331c40bfbe29858aeed82f45cc';
18
+ const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36';
19
+
20
+ const MAX_SIZE_GB_MOVIE = 4;
21
+ const MAX_SIZE_GB_SERIES = 1.5;
22
+ const LINKS_PER_QUALITY = 5;
23
+ const ALLOWED_QUALITIES = ['4k', '1080p', '720p', '576p', '480p', 'webrip'];
24
+
25
+ const QUALITY_RANK = {
26
+ '1080p': 1, '720p': 2, '576p': 3, '480p': 4,
27
+ '4k': 5, '2160p': 5, 'webrip': 6, 'webdl': 6,
28
+ };
29
+
30
+ const PRIORITY_PROVIDERS = ['subsplease', 'apibay', 'torrentcsv'];
31
+
32
+ const ALLOWED_PROVIDERS = [
33
+ 'subsplease',
34
+ 'apibay',
35
+ 'torrentcsv',
36
+ 'thepiratebay',
37
+ ];
38
+
39
+ const TR = [
40
+ 'udp://tracker.opentrackr.org:1337/announce',
41
+ 'udp://open.stealth.si:80/announce',
42
+ 'udp://open.demonii.com:1337/announce',
43
+ 'udp://tracker.torrent.eu.org:451/announce',
44
+ 'udp://p4p.arenabg.com:1337/announce',
45
+ 'udp://exodus.desync.com:6969/announce',
46
+ 'udp://tracker.openbittorrent.com:6969/announce',
47
+ 'udp://tracker.dler.org:6969/announce',
48
+ 'https://tracker.moeblog.cn:443/announce',
49
+ 'https://tracker.zhuqiy.com:443/announce',
50
+ ].map(t => '&tr=' + encodeURIComponent(t)).join('');
51
+
52
+ function getQuality(str = '') {
53
+ const s = str.toLowerCase();
54
+ if (s.includes('4k') || s.includes('2160p') || s.includes('uhd')) return '4k';
55
+ if (s.includes('1080p')) return '1080p';
56
+ if (s.includes('720p')) return '720p';
57
+ if (s.includes('576p')) return '576p';
58
+ if (s.includes('480p')) return '480p';
59
+ if (s.includes('webrip')) return 'webrip';
60
+ if (s.includes('webdl') || s.includes('web-dl')) return 'webdl';
61
+ return null;
62
+ }
63
+
64
+ function getSizeGB(stream) {
65
+ const raw = stream.title || stream.name || '';
66
+ const m = raw.match(/πŸ’Ύ\s*([\d.]+)\s*(GB|MB)/i) || raw.match(/([\d.]+)\s*(GB|MB)/i);
67
+ if (!m) return null;
68
+ const val = parseFloat(m[1]);
69
+ return m[2].toUpperCase() === 'GB' ? val : val / 1024;
70
+ }
71
+
72
+ function getSeeders(stream) {
73
+ if (stream._seeders != null) return stream._seeders;
74
+ if (stream.behaviorHints?.seeders) return stream.behaviorHints.seeders;
75
+ const m = (stream.title || '').match(/πŸ‘€\s*(\d+)/);
76
+ return m ? parseInt(m[1], 10) : 0;
77
+ }
78
+
79
+ function getLangLine(str = '') {
80
+ const lower = str.toLowerCase();
81
+ const found = [];
82
+ const checks = [
83
+ [/\benglish\b/, 'English'],
84
+ [/\bjapanese\b/, 'Japanese'],
85
+ [/\bhindi\b/, 'Hindi'],
86
+ [/\bfrench\b/, 'French'],
87
+ [/\bgerman\b/, 'German'],
88
+ [/\bspanish\b/, 'Spanish'],
89
+ [/\bitalian\b/, 'Italian'],
90
+ [/\brussian\b/, 'Russian'],
91
+ [/\bkorean\b/, 'Korean'],
92
+ [/\bchinese\b/, 'Chinese'],
93
+ [/\barabic\b/, 'Arabic'],
94
+ [/\bportuguese\b/, 'Portuguese'],
95
+ [/\bturkish\b/, 'Turkish'],
96
+ [/\bpolish\b/, 'Polish'],
97
+ [/\bdutch\b/, 'Dutch'],
98
+ [/\bczech\b/, 'Czech'],
99
+ [/\bswedish\b/, 'Swedish'],
100
+ [/\bnorwegian\b/, 'Norwegian'],
101
+ [/\bdanish\b/, 'Danish'],
102
+ [/\bfinnish\b/, 'Finnish'],
103
+ [/\bromanian\b/, 'Romanian'],
104
+ [/\bgreek\b/, 'Greek'],
105
+ [/\bhebrew\b/, 'Hebrew'],
106
+ [/\bthai\b/, 'Thai'],
107
+ [/\bindonesian\b/, 'Indonesian'],
108
+ [/\bvietnamese\b/, 'Vietnamese'],
109
+ [/\btamil\b/, 'Tamil'],
110
+ [/\btelugu\b/, 'Telugu'],
111
+ [/\burdu\b/, 'Urdu'],
112
+ [/\bdubbed\b/, 'Dubbed'],
113
+ [/\bdual[\s\-]audio\b/, 'Dual Audio'],
114
+ [/\bmulti[\s\-]audio\b/, 'Multi Audio'],
115
+ [/\bmulti\b/, 'Multi'],
116
+ ];
117
+ for (const [re, label] of checks) {
118
+ if (re.test(lower)) {
119
+ if (label === 'Multi' && found.includes('Multi Audio')) continue;
120
+ found.push(label);
121
+ }
122
+ }
123
+ return [...new Set(found)].join(' / ');
124
+ }
125
+
126
+ function processStreams(streams, type) {
127
+ console.log(`[processStreams] Starting with ${streams.length} streams, type: ${type}`);
128
+
129
+ const maxSize = type === 'series' ? MAX_SIZE_GB_SERIES : MAX_SIZE_GB_MOVIE;
130
+
131
+ const filtered = streams.filter(s => {
132
+ if (!s.infoHash && !s.url) {
133
+ console.log(`[processStreams] Filtering out stream with no infoHash/url`);
134
+ return false;
135
+ }
136
+ if (s._provider && !ALLOWED_PROVIDERS.includes(s._provider.toLowerCase())) {
137
+ console.log(`[processStreams] Filtering out provider: ${s._provider}`);
138
+ return false;
139
+ }
140
+ const q = s._quality || getQuality(s.title || s.name || '');
141
+ if (!q || !ALLOWED_QUALITIES.includes(q)) {
142
+ console.log(`[processStreams] Filtering out quality: ${q}`);
143
+ return false;
144
+ }
145
+ const sizeGB = s._sizeGB ?? getSizeGB(s);
146
+ if (sizeGB !== null && sizeGB > maxSize) {
147
+ console.log(`[processStreams] Filtering out oversized: ${sizeGB}GB > ${maxSize}GB`);
148
+ return false;
149
+ }
150
+ return true;
151
+ });
152
+
153
+ console.log(`[processStreams] After filtering: ${filtered.length} streams`);
154
+
155
+ const seen = new Set();
156
+ const unique = filtered.filter(s => {
157
+ const key = (s.infoHash || '').toLowerCase();
158
+ if (!key) return true;
159
+ if (seen.has(key)) {
160
+ console.log(`[processStreams] Deduping: ${key}`);
161
+ return false;
162
+ }
163
+ seen.add(key);
164
+ return true;
165
+ });
166
+
167
+ console.log(`[processStreams] After dedup: ${unique.length} streams`);
168
+
169
+ const byQuality = {};
170
+ unique.forEach(s => {
171
+ const q = s._quality || getQuality(s.title || s.name || '') || 'unknown';
172
+ if (!byQuality[q]) byQuality[q] = [];
173
+ byQuality[q].push(s);
174
+ });
175
+
176
+ for (const q in byQuality) {
177
+ byQuality[q].sort((a, b) => {
178
+ const rankA = PRIORITY_PROVIDERS.indexOf((a._source || '').toLowerCase());
179
+ const rankB = PRIORITY_PROVIDERS.indexOf((b._source || '').toLowerCase());
180
+ const pA = rankA === -1 ? 999 : rankA;
181
+ const pB = rankB === -1 ? 999 : rankB;
182
+ if (pA !== pB) return pA - pB;
183
+ return getSeeders(b) - getSeeders(a);
184
+ });
185
+ }
186
+
187
+ const sortedQualities = Object.keys(byQuality).sort(
188
+ (a, b) => (QUALITY_RANK[a] || 99) - (QUALITY_RANK[b] || 99)
189
+ );
190
+
191
+ const result = [];
192
+ sortedQualities.forEach(q => {
193
+ console.log(`[processStreams] Quality ${q}: ${byQuality[q].length} β†’ taking ${Math.min(LINKS_PER_QUALITY, byQuality[q].length)}`);
194
+ result.push(...byQuality[q].slice(0, LINKS_PER_QUALITY));
195
+ });
196
+
197
+ console.log(`[processStreams] Final result: ${result.length} streams`);
198
+ return result;
199
+ }
200
+
201
+ async function tmdbLookup(imdbId) {
202
  try {
203
+ console.log(`[TMDB] Looking up: ${imdbId}`);
204
  const { data } = await axios.get(
205
+ `https://api.themoviedb.org/3/find/${imdbId}?api_key=${TMDB_KEY}&external_source=imdb_id`,
206
  { timeout: 8000 }
207
  );
208
+ const movie = data.movie_results?.[0];
209
+ const tv = data.tv_results?.[0];
210
+ if (movie) {
211
+ console.log(`[TMDB] βœ… Found movie: ${movie.title} (${movie.release_date})`);
212
+ return { title: movie.title || movie.original_title, year: (movie.release_date || '').slice(0, 4) };
213
+ }
214
+ if (tv) {
215
+ console.log(`[TMDB] βœ… Found TV: ${tv.name} (${tv.first_air_date})`);
216
+ return { title: tv.name || tv.original_name, year: (tv.first_air_date || '').slice(0, 4) };
217
+ }
218
+ console.log(`[TMDB] ❌ No results`);
219
+ return null;
220
+ } catch (e) {
221
+ console.error('[TMDB] ❌ Error:', e.message);
222
+ return null;
223
+ }
224
+ }
225
+
226
+ // ═══════════════════════════════════════════════════════════════════════════
227
+ // SUBSPLEASE SCRAPER
228
+ // ═══════════════════════════════════════════════════════════════════════════
229
+ async function scrapeSubsPlease(title, season, episode) {
230
+ if (!title) return [];
231
+ try {
232
+ console.log(`[SubsPlease] πŸ” Searching: ${title}`);
233
+ const { data } = await axios.get(
234
+ `https://subsplease.org/api/?f=search&tz=&s=${encodeURIComponent(title)}`,
235
+ { timeout: 8000, headers: { 'User-Agent': UA } }
236
+ );
237
+
238
+ console.log(`[SubsPlease] πŸ“‘ Response keys:`, Object.keys(data).slice(0, 3));
239
+
240
+ if (!data || Object.keys(data).length === 0) {
241
+ console.log(`[SubsPlease] ❌ No results`);
242
+ return [];
243
+ }
244
 
245
  const streams = [];
246
+ for (const [showKey, showData] of Object.entries(data)) {
247
+ if (!showData.downloads || !Array.isArray(showData.downloads)) continue;
248
+
249
+ console.log(`[SubsPlease] Found: ${showData.show_name} (${showData.downloads.length} downloads)`);
250
+
251
+ for (const dl of showData.downloads) {
252
+ if (!dl.magnet || !dl.res) continue;
253
+
254
+ if (season && episode) {
255
+ const epStr = `S${String(season).padStart(2, '0')}E${String(episode).padStart(2, '0')}`;
256
+ if (!dl.magnet.toLowerCase().includes(epStr.toLowerCase())) continue;
257
  }
258
+
259
+ const hashMatch = dl.magnet.match(/btih:([a-fA-F0-9]{32,40})/i);
260
+ const hash = hashMatch ? hashMatch[1] : '';
261
+ if (!hash) continue;
262
+
263
+ const langLine = getLangLine(dl.magnet);
264
+ streams.push({
265
+ infoHash: hash.toLowerCase(),
266
+ name: `${showData.show_name} ${dl.res}p`,
267
+ title: `β˜€οΈ ${showData.show_name}\n🌱 N/A\nπŸ’Ύ N/A\nπŸ… SubsPlease${langLine ? '\nπŸ”Š ' + langLine : ''}`,
268
+ sources: [],
269
+ _quality: dl.res === '1080' ? '1080p' : dl.res === '720' ? '720p' : '480p',
270
+ _seeders: 0,
271
+ _sizeGB: null,
272
+ _source: 'subsplease',
273
+ _provider: 'subsplease',
274
+ });
275
  }
276
  }
277
+ console.log(`[SubsPlease] βœ… ${streams.length} results`);
278
+ return streams;
279
+ } catch (e) {
280
+ console.error('[SubsPlease] ❌', e.message);
281
+ return [];
282
  }
283
  }
284
 
285
+ // ═══════════════════════════════════════════════════════════════════════════
286
+ // APIBAY SCRAPER (THE PIRATE BAY)
287
+ // ═══════════════════════════════════════════════════════════════════════════
288
+ async function scrapeAPIBay(title, year, season, episode) {
289
+ if (!title) return [];
290
  try {
291
+ let query = title;
292
+ if (season && episode) {
293
+ query += ` S${String(season).padStart(2, '0')}E${String(episode).padStart(2, '0')}`;
294
+ } else if (year) {
295
+ query += ' ' + year;
296
+ }
297
+
298
+ console.log(`[APIBay] πŸ” Searching: ${query}`);
299
  const { data } = await axios.get(
300
+ `https://apibay.org/q.php?q=${encodeURIComponent(query)}&cat=208`,
301
+ { timeout: 12000, headers: { 'User-Agent': UA } }
302
  );
303
 
304
+ console.log(`[APIBay] πŸ“‘ Response type: ${typeof data}, is array: ${Array.isArray(data)}`);
305
+
306
+ if (!Array.isArray(data) || data.length === 0) {
307
+ console.log(`[APIBay] ❌ No results or invalid format`);
308
+ return [];
309
+ }
310
 
311
+ const streams = [];
312
+ for (const item of data) {
313
+ if (!item.info_hash || !item.name) continue;
314
+
315
+ const magnet = `magnet:?xt=urn:btih:${item.info_hash.toLowerCase()}&dn=${encodeURIComponent(item.name)}${TR}`;
316
+ const sizeGB = item.size ? parseInt(item.size, 10) / 1073741824 : null;
317
+ const sizeStr = sizeGB ? sizeGB.toFixed(2) + ' GB' : 'N/A';
318
+ const langLine = getLangLine(item.name || '');
319
+ const seeders = item.seeders ? parseInt(item.seeders, 10) : 0;
320
+
321
+ streams.push({
322
+ infoHash: item.info_hash.toLowerCase(),
323
+ name: item.name,
324
+ title: `β˜€οΈ ${title}${year ? ' (' + year + ')' : ''}\n🌱 ${seeders}\nπŸ’Ύ ${sizeStr}\nπŸ… APIBay${langLine ? '\nπŸ”Š ' + langLine : ''}`,
325
+ sources: [],
326
+ _quality: getQuality(item.name || ''),
327
+ _seeders: seeders,
328
+ _sizeGB: sizeGB,
329
+ _source: 'apibay',
330
+ _provider: 'thepiratebay',
331
+ });
332
+ }
333
+ console.log(`[APIBay] βœ… ${streams.length} results`);
334
+ return streams;
335
+ } catch (e) {
336
+ console.error('[APIBay] ❌', e.message);
337
+ return [];
338
  }
339
  }
340
 
341
+ // ═══════════════════════════════════════════════════════════════════════════
342
+ // TORRENTS.CSV SCRAPER
343
+ // ═══════════════════════════════════════════════════════════════════════════
344
+ async function scrapeTorrentsCSV(title, year, season, episode) {
345
+ if (!title) return [];
346
  try {
347
+ let query = title;
348
+ if (season && episode) {
349
+ query += ` S${String(season).padStart(2, '0')}E${String(episode).padStart(2, '0')}`;
350
+ } else if (year) {
351
+ query += ' ' + year;
352
+ }
353
+
354
+ console.log(`[Torrents.csv] πŸ” Searching: ${query}`);
355
  const { data } = await axios.get(
356
+ `https://torrents-csv.com/service/search?q=${encodeURIComponent(query)}`,
357
+ { timeout: 12000, headers: { 'User-Agent': UA } }
358
  );
359
 
360
+ console.log(`[Torrents.csv] πŸ“‘ Response type: ${typeof data}, is array: ${Array.isArray(data)}`);
361
+
362
+ if (!Array.isArray(data) || data.length === 0) {
363
+ console.log(`[Torrents.csv] ❌ No results or invalid format`);
364
+ return [];
365
+ }
366
 
367
+ const streams = [];
368
+ for (const item of data) {
369
+ if (!item.infohash || !item.name) continue;
370
+
371
+ const magnet = `magnet:?xt=urn:btih:${item.infohash.toLowerCase()}&dn=${encodeURIComponent(item.name)}${TR}`;
372
+ const sizeGB = item.size ? item.size / 1073741824 : null;
373
+ const sizeStr = sizeGB ? sizeGB.toFixed(2) + ' GB' : 'N/A';
374
+ const langLine = getLangLine(item.name || '');
375
+ const seeders = item.seeders || 0;
376
+
377
+ streams.push({
378
+ infoHash: item.infohash.toLowerCase(),
379
+ name: item.name,
380
+ title: `β˜€οΈ ${title}${year ? ' (' + year + ')' : ''}\n🌱 ${seeders}\nπŸ’Ύ ${sizeStr}\nπŸ… Torrents.csv${langLine ? '\nπŸ”Š ' + langLine : ''}`,
381
+ sources: [],
382
+ _quality: getQuality(item.name || ''),
383
+ _seeders: seeders,
384
+ _sizeGB: sizeGB,
385
+ _source: 'torrentcsv',
386
+ _provider: 'torrentcsv',
387
+ });
388
+ }
389
+ console.log(`[Torrents.csv] βœ… ${streams.length} results`);
390
+ return streams;
391
+ } catch (e) {
392
+ console.error('[Torrents.csv] ❌', e.message);
393
+ return [];
394
  }
395
  }
396
 
397
+ // ─── Manifest ──────────────────────────────────────────────────────────
398
+ app.get('/manifest.json', (_, res) => {
399
+ res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
400
+ res.json({
401
+ id: 'org.ghostream.plus.v2',
402
+ name: 'VanStreams+',
403
+ description: 'Stream Everything You Want\nMade With 🀍 By Van',
404
+ version: '15.0.7',
405
+ resources: ['stream'],
406
+ types: ['movie', 'series'],
407
+ idPrefixes: ['tt'],
408
+ catalogs: [],
409
+ });
410
+ });
411
+
412
+ // ─── Stream handler ────────────────────────────────────────────────
413
+ app.get('/stream/:type/:id.json', async (req, res) => {
414
+ const { type, id } = req.params;
415
+ const isSeries = type === 'series';
416
+ const [imdbId, season = '', episode = ''] = id.split(':');
417
+
418
+ console.log(`\n${'='.repeat(70)}`);
419
+ console.log(`[stream] 🎬 Request: ${type}/${id} | Series: ${isSeries}`);
420
+ console.log(`${'='.repeat(70)}`);
421
+
422
+ const meta = await tmdbLookup(imdbId);
423
+ const title = meta?.title || '';
424
+ const year = meta?.year || '';
425
+
426
+ console.log(`[stream] πŸ“Œ Title: ${title} | Year: ${year}`);
427
+
428
+ const [subspleaseR, apibayR, torrentscsvR] = await Promise.allSettled([
429
+ scrapeSubsPlease(title, season, episode),
430
+ scrapeAPIBay(title, year, season, episode),
431
+ scrapeTorrentsCSV(title, year, season, episode),
432
+ ]);
433
+
434
+ const subsplease = subspleaseR.status === 'fulfilled' ? (subspleaseR.value || []) : [];
435
+ const apibay = apibayR.status === 'fulfilled' ? (apibayR.value || []) : [];
436
+ const torrentscsv = torrentscsvR.status === 'fulfilled' ? (torrentscsvR.value || []) : [];
437
+
438
+ console.log(`[stream] πŸ“Š Raw results: subsplease:${subsplease.length} apibay:${apibay.length} torrentscsv:${torrentscsv.length}`);
439
+
440
+ const streams = processStreams([...subsplease, ...apibay, ...torrentscsv], type);
441
+
442
+ console.log(`[stream] βœ… FINAL: ${streams.length} streams`);
443
+ console.log(`${'='.repeat(70)}\n`);
444
+
445
+ res.json({ streams });
446
+ });
447
+
448
+ // ─── Diagnostic endpoints ─────────────────────────────────────────────
449
+ app.get('/diag/:type/:id', async (req, res) => {
450
+ const { type, id } = req.params;
451
+ const isSeries = type === 'series';
452
+ const [imdbId, season = '', episode = ''] = id.split(':');
453
+ const meta = await tmdbLookup(imdbId);
454
 
455
+ const [subspleaseR, apibayR, torrentscsvR] = await Promise.allSettled([
456
+ scrapeSubsPlease(meta?.title || '', season, episode),
457
+ scrapeAPIBay(meta?.title || '', meta?.year || '', season, episode),
458
+ scrapeTorrentsCSV(meta?.title || '', meta?.year || '', season, episode),
459
  ]);
460
 
461
  res.json({
462
+ imdbId, title: meta?.title, year: meta?.year, isSeries,
463
+ tmdb: meta ? `βœ… ${meta.title} (${meta.year})` : '❌ FAILED',
464
+ subsplease: { count: (subspleaseR.value || []).length, err: subspleaseR.reason?.message || null },
465
+ apibay: { count: (apibayR.value || []).length, err: apibayR.reason?.message || null },
466
+ torrentcsv: { count: (torrentscsvR.value || []).length, err: torrentscsvR.reason?.message || null },
467
  });
468
  });
469
 
470
+ // ══════════════════════════ LANDING PAGE ═══════════════════════════
471
  app.get('/', (req, res) => {
472
+ const manifestUrl = 'https://vanseleem-vsplus.hf.space/manifest.json';
473
+ const installUrl = 'stremio://vanseleem-vsplus.hf.space/manifest.json';
474
+
475
+ res.type('html').send(`<!DOCTYPE html>
476
+ <html lang="en">
477
  <head>
478
+ <meta charset="UTF-8">
479
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
480
+ <title>VanStreams+</title>
481
  <style>
482
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
483
+ body {
484
+ font-family: -apple-system, 'Segoe UI', system-ui, sans-serif;
485
+ background: #050810;
486
+ color: #dde6f0;
487
+ min-height: 100vh;
488
+ display: flex;
489
+ align-items: center;
490
+ justify-content: center;
491
+ padding: 28px 20px;
492
+ background-image:
493
+ radial-gradient(ellipse 70% 40% at 50% 0%, rgba(255,0,0,0.05) 0%, transparent 70%),
494
+ radial-gradient(ellipse 40% 30% at 80% 80%, rgba(255,0,0,0.03) 0%, transparent 60%);
495
+ }
496
+ .wrap { width: 100%; max-width: 560px; display: flex; flex-direction: column; gap: 20px; }
497
+ .header { display: flex; flex-direction: column; gap: 12px; }
498
+ .logo { font-size: 2.6rem; font-weight: 800; letter-spacing: -1px; line-height: 1; }
499
+ .logo .van { color: #FF0000; }
500
+ .logo .streams { color: #ffffff; }
501
+ .badge-row { display: flex; gap: 8px; flex-wrap: wrap; }
502
+ .badge {
503
+ display: inline-flex; align-items: center; gap: 5px;
504
+ background: rgba(255,0,0,0.08); border: 1px solid rgba(255,0,0,0.18);
505
+ color: #ffffff; padding: 4px 11px; border-radius: 100px;
506
+ font-size: 0.72rem; font-weight: 700; letter-spacing: 0.4px; text-transform: uppercase;
507
+ }
508
+ .badge.series { color: #ffffff; }
509
+ .desc { color: #ffffff; font-size: 0.9rem; line-height: 1.6; margin-top: 2px; }
510
+ .card {
511
+ background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.07);
512
+ border-radius: 16px; padding: 20px 22px;
513
+ }
514
+ .card-label { font-size: 0.68rem; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: #ffffff; margin-bottom: 10px; }
515
+ .url-box {
516
+ font-family: 'SF Mono','Fira Code','Consolas',monospace; font-size: 0.8rem;
517
+ color: #ffffff; background: rgba(0,0,0,0.25); border: 1px solid rgba(255,0,0,0.12);
518
+ border-radius: 10px; padding: 11px 14px; word-break: break-all; line-height: 1.5;
519
+ }
520
+ .actions { display: flex; gap: 10px; margin-top: 14px; flex-wrap: wrap; }
521
+ .btn { padding: 10px 22px; border-radius: 10px; font-size: 0.84rem; font-weight: 600; text-decoration: none; border: none; cursor: pointer; transition: all 0.15s; font-family: inherit; }
522
+ .btn-install {
523
+ background: #FF0000;
524
+ color: #ffffff;
525
+ }
526
+ .btn-install:hover { background: #cc0000; }
527
+ .btn-copy {
528
+ background: #ffffff;
529
+ color: #050810;
530
+ border: 1px solid #ffffff;
531
+ }
532
+ .btn-copy:hover {
533
+ background: #e6e6e6;
534
+ border-color: #e6e6e6;
535
+ }
536
+ .stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
537
+ .stat { background: rgba(255,255,255,0.02); border: 1px solid rgba(255,255,255,0.06); border-radius: 12px; padding: 16px 12px; text-align: center; }
538
+ .stat-val { font-size: 1.5rem; font-weight: 800; color: #fff; line-height: 1; margin-bottom: 5px; }
539
+ .stat-lbl { font-size: 0.68rem; color: #aaa; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; white-space: pre-line; }
540
+ .made-by { text-align: center; color: #ffffff; font-size: 0.85rem; margin-top: 6px; opacity: 0.8; }
541
  </style>
542
  </head>
543
  <body>
544
+ <div class="wrap">
545
+ <div class="header">
546
+ <div class="logo">
547
+ <span class="van">Van</span> <span class="streams">Streams+</span>
548
+ </div>
549
+ <div class="badge-row">
550
+ <span class="badge">🎬 Movies</span>
551
+ <span class="badge series">πŸ“Ί Series</span>
552
+ </div>
553
+ <p class="desc">Get Your 🍿 Ready</p>
554
+ </div>
555
+ <div class="card">
556
+ <div class="card-label">Stremio Install URL</div>
557
+ <div class="url-box">${manifestUrl}</div>
558
+ <div class="actions">
559
+ <a href="${installUrl}" class="btn btn-install">Install in Stremio</a>
560
+ <button class="btn btn-copy" onclick="navigator.clipboard.writeText('${manifestUrl}').then(()=>{this.textContent='βœ” Copied';setTimeout(()=>this.textContent='Copy URL',2000)})">Copy URL</button>
561
+ </div>
562
+ </div>
563
+ <div class="stats">
564
+ <div class="stat"><div class="stat-val">100K+</div><div class="stat-lbl">Movies</div></div>
565
+ <div class="stat"><div class="stat-val">40K+</div><div class="stat-lbl">Series</div></div>
566
+ <div class="stat"><div class="stat-val">20K+</div><div class="stat-lbl">Anime</div></div>
567
+ </div>
568
+ <div class="made-by">Made With 🀍 By Van</div>
569
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
570
  </body>
571
+ </html>`);
 
572
  });
573
 
574
  app.listen(PORT, '0.0.0.0', () => {
575
+ console.log(`\n${'='.repeat(70)}`);
576
+ console.log(`βœ… VanStreams+ Running on port ${PORT}`);
577
+ console.log(`πŸ”— http://localhost:${PORT}/`);
578
+ console.log(`πŸ“Š Diagnostics: http://localhost:${PORT}/diag/series/tt1234567:1:1`);
579
+ console.log(`${'='.repeat(70)}\n`);
580
  });