no1b4me commited on
Commit
ea5d04f
·
verified ·
1 Parent(s): a9a32f5

Upload 15 files

Browse files
index.js ADDED
@@ -0,0 +1,452 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express from 'express';
2
+ import cors from 'cors';
3
+ import fs from 'fs/promises';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import AsyncLock from 'async-lock';
7
+ import { getDebridServices } from './src/debrids.js';
8
+ import { isVideo, base64Encode, base64Decode, extractInfoHash } from './src/util.js';
9
+ import { ERROR } from './src/const.js';
10
+ import { fetchRSSFeeds as fetchIPTFeeds } from './src/iptorrents.js';
11
+ import { fetchRSSFeeds as fetchTDayFeeds } from './src/tday.js';
12
+ import { fetchRSSFeeds as fetchTorrentingFeeds } from './src/torrenting.js';
13
+
14
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
15
+ const app = express();
16
+ const lock = new AsyncLock();
17
+
18
+ app.use(cors({
19
+ origin: '*',
20
+ methods: ['GET', 'POST', 'OPTIONS'],
21
+ allowedHeaders: ['Content-Type', 'Authorization'],
22
+ credentials: true
23
+ }));
24
+
25
+ app.use(express.static(path.join(__dirname, 'public')));
26
+ app.options('*', cors());
27
+
28
+ async function getCinemetaMetadata(imdbId) {
29
+ try {
30
+ console.log(`\n🎬 Fetching Cinemeta data for ${imdbId}`);
31
+ const response = await fetch(`https://v3-cinemeta.strem.io/meta/movie/${imdbId}.json`);
32
+ if (!response.ok) throw new Error('Failed to fetch from Cinemeta');
33
+ const data = await response.json();
34
+ console.log('✅ Found:', data.meta.name);
35
+ return data;
36
+ } catch (error) {
37
+ console.error('❌ Cinemeta error:', error);
38
+ return null;
39
+ }
40
+ }
41
+
42
+ async function readMovieData(imdbId, year) {
43
+ const lockKey = `year-${year}`;
44
+ const yearFile = path.join(__dirname, 'movies', `${year}.json`);
45
+
46
+ try {
47
+ return await lock.acquire(lockKey, async () => {
48
+ console.log(`\n📂 Reading data for year ${year}`);
49
+ const content = await fs.readFile(yearFile, 'utf8');
50
+ const movies = JSON.parse(content);
51
+ const movie = movies.find(m => m.imdbId === imdbId);
52
+ if (movie) {
53
+ console.log(`✅ Found movie: ${movie.originalTitle}`);
54
+ console.log(`Found ${movie.streams.length} streams`);
55
+ }
56
+ return movie;
57
+ });
58
+ } catch (error) {
59
+ if (error.name === 'AsyncLockTimeout') {
60
+ console.error(`❌ Lock timeout reading year ${year}`);
61
+ return null;
62
+ }
63
+ if (error.code !== 'ENOENT') {
64
+ console.error(`❌ Error reading movie data:`, error);
65
+ }
66
+ return null;
67
+ }
68
+ }
69
+
70
+ async function getAllStreams(imdbId) {
71
+ try {
72
+ console.log('\n🔄 Fetching all available streams');
73
+ console.log('Fetching from RSS feeds for IMDB ID:', imdbId);
74
+
75
+ const startTime = Date.now();
76
+ const [iptStreams, tdayStreams, torrentingStreams] = await Promise.all([
77
+ fetchIPTFeeds(imdbId).catch(err => {
78
+ console.error('IPTorrents fetch failed:', err);
79
+ return [];
80
+ }),
81
+ fetchTDayFeeds(imdbId).catch(err => {
82
+ console.error('TorrentDay fetch failed:', err);
83
+ return [];
84
+ }),
85
+ fetchTorrentingFeeds(imdbId).catch(err => {
86
+ console.error('Torrenting fetch failed:', err);
87
+ return [];
88
+ })
89
+ ]);
90
+
91
+ console.log(`\nStream fetch results (${Date.now() - startTime}ms):`);
92
+ console.log('IPTorrents:', iptStreams.length, 'streams');
93
+ console.log('TorrentDay:', tdayStreams.length, 'streams');
94
+ console.log('Torrenting:', torrentingStreams.length, 'streams');
95
+
96
+ const allStreams = [
97
+ ...iptStreams,
98
+ ...tdayStreams,
99
+ ...torrentingStreams
100
+ ];
101
+
102
+ console.log('\nPre-deduplication total:', allStreams.length, 'streams');
103
+
104
+ // Remove duplicates based on infoHash
105
+ const uniqueStreams = Array.from(
106
+ new Map(
107
+ allStreams
108
+ .filter(Boolean)
109
+ .map(stream => {
110
+ const hash = extractInfoHash(stream.magnetLink);
111
+ return [hash, stream];
112
+ })
113
+ ).values()
114
+ );
115
+
116
+ console.log('Post-deduplication total:', uniqueStreams.length, 'streams');
117
+
118
+ // Log some sample streams for debugging
119
+ if (uniqueStreams.length > 0) {
120
+ console.log('\nSample stream data:', {
121
+ magnetLink: uniqueStreams[0].magnetLink.substring(0, 100) + '...',
122
+ filename: uniqueStreams[0].filename,
123
+ quality: uniqueStreams[0].quality,
124
+ size: uniqueStreams[0].size,
125
+ source: uniqueStreams[0].source
126
+ });
127
+ }
128
+
129
+ return uniqueStreams;
130
+ } catch (error) {
131
+ console.error('❌ Error fetching streams:', error);
132
+ return [];
133
+ }
134
+ }
135
+
136
+ async function checkCacheStatuses(service, hashes) {
137
+ if (!hashes?.length) {
138
+ console.log('No hashes to check');
139
+ return {};
140
+ }
141
+
142
+ try {
143
+ console.log(`\n🔍 Checking cache status for ${hashes.length} hashes with ${service.constructor.name}`);
144
+ console.log('Sample hashes:', hashes.slice(0, 3));
145
+
146
+ const startTime = Date.now();
147
+ const results = await service.checkCacheStatuses(hashes);
148
+ console.log(`Cache check completed in ${Date.now() - startTime}ms`);
149
+
150
+ const cachedCount = Object.values(results).filter(r => r.cached).length;
151
+ console.log(`Cache check results: ${cachedCount} cached out of ${hashes.length} total`);
152
+
153
+ // Log some sample results
154
+ const sampleHash = hashes[0];
155
+ if (sampleHash && results[sampleHash]) {
156
+ console.log('Sample cache result:', {
157
+ hash: sampleHash,
158
+ result: results[sampleHash]
159
+ });
160
+ }
161
+
162
+ return results;
163
+ } catch (error) {
164
+ console.error('❌ Cache check error:', error);
165
+ return {};
166
+ }
167
+ }
168
+
169
+ async function mergeAndSaveStreams(existingStreams = [], newStreams = [], imdbId, year, movieTitle = '') {
170
+ const lockKey = `year-${year}`;
171
+
172
+ try {
173
+ return await lock.acquire(lockKey, async () => {
174
+ if (!newStreams.length) {
175
+ console.log('No new streams to merge');
176
+ return existingStreams;
177
+ }
178
+
179
+ console.log(`\n🔄 Merging streams for ${movieTitle}`);
180
+ console.log('Existing streams:', existingStreams.length);
181
+ console.log('New streams:', newStreams.length);
182
+
183
+ const existingHashes = new Set(
184
+ existingStreams.map(stream =>
185
+ extractInfoHash(stream.magnetLink)
186
+ ).filter(Boolean)
187
+ );
188
+
189
+ const uniqueNewStreams = newStreams.filter(stream => {
190
+ const hash = extractInfoHash(stream.magnetLink);
191
+ return hash && !existingHashes.has(hash);
192
+ });
193
+
194
+ if (!uniqueNewStreams.length) {
195
+ console.log('No unique new streams found');
196
+ return existingStreams;
197
+ }
198
+
199
+ console.log(`Found ${uniqueNewStreams.length} new unique streams`);
200
+
201
+ const mergedStreams = [...existingStreams, ...uniqueNewStreams];
202
+ const yearFile = path.join(__dirname, 'movies', `${year}.json`);
203
+
204
+ let movies = [];
205
+ try {
206
+ const content = await fs.readFile(yearFile, 'utf8');
207
+ movies = JSON.parse(content);
208
+ console.log(`Read existing ${year}.json with ${movies.length} movies`);
209
+ } catch (error) {
210
+ console.log(`Creating new ${year}.json file`);
211
+ }
212
+
213
+ const movieIndex = movies.findIndex(m => m.imdbId === imdbId);
214
+ if (movieIndex >= 0) {
215
+ console.log('Updating existing movie entry');
216
+ movies[movieIndex].streams = mergedStreams;
217
+ movies[movieIndex].lastUpdated = new Date().toISOString();
218
+ } else {
219
+ console.log('Adding new movie entry');
220
+ movies.push({
221
+ imdbId,
222
+ streams: mergedStreams,
223
+ originalTitle: movieTitle,
224
+ addedAt: new Date().toISOString(),
225
+ lastUpdated: new Date().toISOString()
226
+ });
227
+ }
228
+
229
+ await fs.mkdir(path.join(__dirname, 'movies'), { recursive: true });
230
+
231
+ const tempFile = `${yearFile}.tmp`;
232
+ await fs.writeFile(tempFile, JSON.stringify(movies, null, 2));
233
+ await fs.rename(tempFile, yearFile);
234
+
235
+ console.log(`✅ Added ${uniqueNewStreams.length} new streams to ${year}.json`);
236
+ return mergedStreams;
237
+ });
238
+ } catch (error) {
239
+ if (error.name === 'AsyncLockTimeout') {
240
+ console.error(`❌ Lock timeout for year ${year}, skipping save`);
241
+ return existingStreams;
242
+ }
243
+ console.error('❌ Error merging and saving streams:', error);
244
+ return existingStreams;
245
+ }
246
+ }
247
+
248
+ app.get('/:apiKeys/manifest.json', (req, res) => {
249
+ const manifest = {
250
+ id: 'org.multirss',
251
+ version: '1.0.0',
252
+ name: 'Multi RSS',
253
+ description: 'Stream movies via Debrid services',
254
+ resources: ['stream'],
255
+ types: ['movie'],
256
+ catalogs: []
257
+ };
258
+ res.json(manifest);
259
+ });
260
+
261
+ app.get('/:apiKeys/stream/:type/:id.json', async (req, res) => {
262
+ const { apiKeys, type, id } = req.params;
263
+
264
+ try {
265
+ console.log('\n📡 Stream request received:', { type, id });
266
+ console.log('API Keys:', apiKeys);
267
+
268
+ const debridServices = getDebridServices(apiKeys);
269
+ if (!debridServices.length) {
270
+ throw new Error('No valid debrid service configured');
271
+ }
272
+
273
+ const metadata = await getCinemetaMetadata(id);
274
+ if (!metadata?.meta) return res.json({ streams: [] });
275
+
276
+ const year = new Date(metadata.meta.released).getFullYear();
277
+ console.log('Movie year:', year);
278
+
279
+ const movieData = await readMovieData(id, year);
280
+
281
+ const localStreams = movieData?.streams || [];
282
+ console.log(`Found ${localStreams.length} streams in cache`);
283
+
284
+ let processedStreams = [];
285
+
286
+ if (localStreams.length > 0) {
287
+ console.log('\n🔍 Processing cached streams');
288
+ const hashes = localStreams.map(stream => extractInfoHash(stream.magnetLink)).filter(Boolean);
289
+ console.log(`Checking ${hashes.length} hashes for cached streams`);
290
+
291
+ const cacheResults = {};
292
+ for (const service of debridServices) {
293
+ console.log(`\nChecking cache with ${service.constructor.name}`);
294
+ const results = await checkCacheStatuses(service, hashes);
295
+ Object.entries(results).forEach(([hash, info]) => {
296
+ if (info.cached) cacheResults[hash] = info;
297
+ });
298
+ }
299
+
300
+ console.log(`Found ${Object.keys(cacheResults).length} cached streams`);
301
+
302
+ processedStreams = localStreams
303
+ .map(stream => {
304
+ const hash = extractInfoHash(stream.magnetLink);
305
+ const cacheInfo = cacheResults[hash];
306
+ if (!cacheInfo?.cached) return null;
307
+
308
+ const quality = stream.quality || stream.websiteTitle.match(/\d{3,4}p|4k|HDTS|CAM/i)?.[0] || '';
309
+ const size = stream.size || stream.websiteTitle.match(/\d+(\.\d+)?\s*(GB|MB)/i)?.[0] || '';
310
+
311
+ return {
312
+ name: ['🧲', quality, size, `⚡️ ${cacheInfo.service}`, `[${stream.source}]`]
313
+ .filter(Boolean)
314
+ .join(' | '),
315
+ title: stream.filename,
316
+ url: `${req.protocol}://${req.get('host')}/${apiKeys}/${base64Encode(stream.magnetLink)}`,
317
+ service: cacheInfo.service
318
+ };
319
+ })
320
+ .filter(Boolean);
321
+ }
322
+
323
+ if (processedStreams.length === 0) {
324
+ console.log('\n🔄 No cached streams available, fetching new streams...');
325
+ const newStreams = await getAllStreams(id);
326
+
327
+ if (newStreams.length > 0) {
328
+ await mergeAndSaveStreams(
329
+ [],
330
+ newStreams,
331
+ id,
332
+ year,
333
+ metadata.meta.name
334
+ );
335
+
336
+ const hashes = newStreams.map(stream => extractInfoHash(stream.magnetLink)).filter(Boolean);
337
+ console.log(`Checking ${hashes.length} hashes for new streams`);
338
+
339
+ const cacheResults = {};
340
+ for (const service of debridServices) {
341
+ console.log(`\nChecking cache with ${service.constructor.name}`);
342
+ const results = await checkCacheStatuses(service, hashes);
343
+ Object.entries(results).forEach(([hash, info]) => {
344
+ if (info.cached) cacheResults[hash] = info;
345
+ });
346
+ }
347
+
348
+ processedStreams = newStreams
349
+ .map(stream => {
350
+ const hash = extractInfoHash(stream.magnetLink);
351
+ const cacheInfo = cacheResults[hash];
352
+ if (!cacheInfo?.cached) return null;
353
+
354
+ return {
355
+ name: ['🧲', stream.quality, stream.size, `⚡️ ${cacheInfo.service}`, `[${stream.source}]`]
356
+ .filter(Boolean)
357
+ .join(' | '),
358
+ title: stream.filename,
359
+ url: `${req.protocol}://${req.get('host')}/${apiKeys}/${base64Encode(stream.magnetLink)}`,
360
+ service: cacheInfo.service
361
+ };
362
+ })
363
+ .filter(Boolean);
364
+ }
365
+ } else {
366
+ console.log('\n🔄 Starting background stream update');
367
+ getAllStreams(id).then(async newStreams => {
368
+ if (newStreams.length > 0) {
369
+ console.log(`Found ${newStreams.length} new streams`);
370
+ await mergeAndSaveStreams(
371
+ localStreams,
372
+ newStreams,
373
+ id,
374
+ year,
375
+ metadata.meta.name
376
+ );
377
+ }
378
+ }).catch(error => {
379
+ console.error('Background update error:', error);
380
+ });
381
+ }
382
+
383
+ processedStreams.sort((a, b) => {
384
+ const getQuality = name => {
385
+ const quality = name.match(/4k|\d{3,4}/i)?.[0]?.toLowerCase();
386
+ if (quality === '4k') return 2160;
387
+ return parseInt(quality) || 0;
388
+ };
389
+
390
+ const qualityA = getQuality(a.name);
391
+ const qualityB = getQuality(b.name);
392
+
393
+ return qualityB - qualityA;
394
+ });
395
+
396
+ console.log(`\n✅ Sending ${processedStreams.length} streams`);
397
+ if (processedStreams.length > 0) {
398
+ console.log('Sample processed stream:', {
399
+ name: processedStreams[0].name,
400
+ title: processedStreams[0].title,
401
+ service: processedStreams[0].service
402
+ });
403
+ }
404
+
405
+ res.json({ streams: processedStreams });
406
+
407
+ } catch (error) {
408
+ console.error('❌ Error processing streams:', error);
409
+ res.json({ streams: [] });
410
+ }
411
+ });
412
+
413
+ app.get('/:apiKeys/:magnetLink', async (req, res) => {
414
+ const { apiKeys, magnetLink } = req.params;
415
+
416
+ try {
417
+ const debridServices = getDebridServices(apiKeys);
418
+ if (!debridServices.length) {
419
+ throw new Error('No valid debrid service configured');
420
+ }
421
+
422
+ console.log('\n🧲 Processing magnet request');
423
+ const decodedMagnet = base64Decode(magnetLink);
424
+ console.log('Decoded magnet link:', decodedMagnet.substring(0, 100) + '...');
425
+
426
+ for (const service of debridServices) {
427
+ try {
428
+ console.log(`\nTrying ${service.constructor.name}`);
429
+ const streamUrl = await service.getStreamUrl(decodedMagnet);
430
+ console.log('Stream URL generated:', streamUrl.substring(0, 100) + '...');
431
+ return res.redirect(streamUrl);
432
+ } catch (error) {
433
+ console.error(`Service ${service.constructor.name} failed:`, error);
434
+ continue;
435
+ }
436
+ }
437
+
438
+ throw new Error('All debrid services failed');
439
+
440
+ } catch (error) {
441
+ console.error('❌ Error processing magnet:', error);
442
+ res.status(500).json({ error: 'Failed to process magnet', details: error.message });
443
+ }
444
+ });
445
+
446
+ app.use((err, req, res, next) => {
447
+ console.error('\n❌ Unhandled error:', err);
448
+ res.status(500).json({ error: 'Internal server error', details: err.message });
449
+ });
450
+
451
+ const port = process.env.PORT || 9518;
452
+ app.listen(port, () => console.log(`\n🚀 Addon running at http://localhost:${port}`));
package.json ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "debrid-streams",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "start": "node index.js"
8
+ },
9
+ "dependencies": {
10
+ "async-lock": "^1.4.0",
11
+ "cors": "^2.8.5",
12
+ "express": "^4.18.2",
13
+ "parse-torrent": "^11.0.17",
14
+ "read-torrent": "^1.3.1",
15
+ "xml2js": "^0.6.2"
16
+ }
17
+ }
public/backdrop.jpg ADDED
public/configure.html ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Multi RSS Streams</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700&display=swap" rel="stylesheet">
8
+ <style>
9
+ * {
10
+ box-sizing: border-box;
11
+ margin: 0;
12
+ padding: 0;
13
+ }
14
+ body, html {
15
+ height: 100%;
16
+ font-family: 'Roboto', sans-serif;
17
+ color: #ffffff;
18
+ }
19
+ body {
20
+ background: linear-gradient(rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.7)),
21
+ url('/backdrop.jpg') no-repeat center center fixed;
22
+ background-size: cover;
23
+ }
24
+ .container {
25
+ display: flex;
26
+ flex-direction: column;
27
+ align-items: center;
28
+ min-height: 100%;
29
+ padding: 20px;
30
+ }
31
+ .logo {
32
+ font-size: 3rem;
33
+ font-weight: 700;
34
+ color: #e50914;
35
+ margin-bottom: 2rem;
36
+ text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
37
+ }
38
+ .content-wrapper {
39
+ background-color: rgba(0, 0, 0, 0.6);
40
+ backdrop-filter: blur(10px);
41
+ border-radius: 4px;
42
+ padding: 40px;
43
+ width: 100%;
44
+ max-width: 450px;
45
+ height: fit-content;
46
+ border: 1px solid rgba(255, 255, 255, 0.1);
47
+ }
48
+ h1 {
49
+ font-size: 2rem;
50
+ font-weight: 700;
51
+ margin-bottom: 28px;
52
+ text-align: center;
53
+ }
54
+ .input-container {
55
+ position: relative;
56
+ margin-bottom: 16px;
57
+ }
58
+ .input {
59
+ width: 100%;
60
+ height: 50px;
61
+ background-color: rgba(51, 51, 51, 0.8);
62
+ border: 1px solid rgba(255, 255, 255, 0.1);
63
+ border-radius: 4px;
64
+ color: white;
65
+ padding: 16px 20px 0;
66
+ font-size: 1rem;
67
+ outline: none;
68
+ }
69
+ .input:focus {
70
+ background-color: rgba(69, 69, 69, 0.8);
71
+ border-color: rgba(255, 255, 255, 0.3);
72
+ }
73
+ .input-label {
74
+ position: absolute;
75
+ top: 50%;
76
+ left: 20px;
77
+ transform: translateY(-50%);
78
+ transition: all 0.1s ease;
79
+ color: #8c8c8c;
80
+ pointer-events: none;
81
+ }
82
+ .input:focus + .input-label,
83
+ .input:not(:placeholder-shown) + .input-label {
84
+ top: 7px;
85
+ font-size: 0.7rem;
86
+ }
87
+ .btn {
88
+ width: 100%;
89
+ height: 50px;
90
+ background-color: #e50914;
91
+ color: white;
92
+ border: none;
93
+ border-radius: 4px;
94
+ font-size: 1rem;
95
+ font-weight: 700;
96
+ margin: 24px 0 12px;
97
+ cursor: pointer;
98
+ transition: background-color 0.2s;
99
+ }
100
+ .btn:hover {
101
+ background-color: #f40612;
102
+ }
103
+ .info {
104
+ color: #737373;
105
+ font-size: 0.9rem;
106
+ margin-top: 16px;
107
+ background-color: rgba(0, 0, 0, 0.6);
108
+ padding: 20px;
109
+ border-radius: 4px;
110
+ border: 1px solid rgba(255, 255, 255, 0.1);
111
+ }
112
+ .addon-url {
113
+ width: 100%;
114
+ background-color: rgba(51, 51, 51, 0.8);
115
+ color: white;
116
+ border: 1px solid rgba(255, 255, 255, 0.1);
117
+ padding: 10px;
118
+ margin-top: 10px;
119
+ border-radius: 4px;
120
+ word-break: break-all;
121
+ display: none;
122
+ }
123
+ .buttons-container {
124
+ display: flex;
125
+ gap: 10px;
126
+ margin-top: 10px;
127
+ }
128
+ .action-btn {
129
+ background-color: #e50914;
130
+ color: white;
131
+ border: none;
132
+ padding: 10px 20px;
133
+ border-radius: 4px;
134
+ cursor: pointer;
135
+ transition: background-color 0.2s;
136
+ flex: 1;
137
+ font-weight: 700;
138
+ }
139
+ .action-btn:hover {
140
+ background-color: #f40612;
141
+ }
142
+ </style>
143
+ </head>
144
+ <body>
145
+ <div class="container">
146
+ <div class="logo">Multi RSS</div>
147
+ <div class="content-wrapper">
148
+ <h1>Stremio Addon Installation</h1>
149
+ <div class="input-container">
150
+ <input type="text" id="dl-key" class="input" placeholder=" " required>
151
+ <label for="dl-key" class="input-label">DebridLink API Key</label>
152
+ </div>
153
+ <div class="input-container">
154
+ <input type="text" id="pr-key" class="input" placeholder=" ">
155
+ <label for="pr-key" class="input-label">Premiumize API Key</label>
156
+ </div>
157
+ <button class="btn" onclick="generateAddonUrl()">GENERATE</button>
158
+ <div id="addon-url" class="addon-url"></div>
159
+ <div class="info">
160
+ <h3>How to get API Keys:</h3>
161
+ <p><strong>DebridLink:</strong> Visit <a href="https://debrid-link.com/webapp/apikey" target="_blank">https://debrid-link.com/webapp/apikey</a></p>
162
+ <p><strong>Premiumize:</strong> Visit <a href="https://www.premiumize.me/account" target="_blank">https://www.premiumize.me/account</a></p>
163
+ </div>
164
+ </div>
165
+ </div>
166
+
167
+ <script>
168
+ function generateAddonUrl() {
169
+ const dlKey = document.getElementById('dl-key').value.trim();
170
+ const prKey = document.getElementById('pr-key').value.trim();
171
+
172
+ if (!dlKey && !prKey) {
173
+ alert('Please enter at least one API key');
174
+ return;
175
+ }
176
+
177
+ // Get the base URL components
178
+ const protocol = window.location.protocol;
179
+ const hostname = window.location.host;
180
+ const baseUrl = `${protocol}//${hostname}`;
181
+
182
+ // Generate the keys part of the URL
183
+ let keys = [];
184
+ if (dlKey) keys.push(`dl=${dlKey}`);
185
+ if (prKey) keys.push(`pr=${prKey}`);
186
+
187
+ // Create the addon URL
188
+ const addonUrl = `${baseUrl}/${keys.join(',')}/manifest.json`;
189
+
190
+ // Always use stremio:// protocol for the Stremio URL, regardless of original protocol
191
+ const stremioUrl = `stremio://${hostname}/${keys.join(',')}/manifest.json`;
192
+
193
+ const urlDiv = document.getElementById('addon-url');
194
+ urlDiv.style.display = 'block';
195
+ urlDiv.innerHTML = `
196
+ <p>Your addon URL:</p>
197
+ <p>${addonUrl}</p>
198
+ <div class="buttons-container">
199
+ <button onclick="window.location.href='${stremioUrl}'" class="action-btn">Install in Stremio</button>
200
+ <button onclick="copyToClipboard('${addonUrl}')" class="action-btn">Copy URL</button>
201
+ </div>
202
+ `;
203
+ }
204
+
205
+ function copyToClipboard(text) {
206
+ navigator.clipboard.writeText(text).then(() => {
207
+ alert('URL copied to clipboard!');
208
+ }).catch(err => {
209
+ console.error('Failed to copy text: ', err);
210
+ alert('Failed to copy URL. Please copy it manually.');
211
+ });
212
+ }
213
+ </script>
214
+ </body>
215
+ </html>
public/index.html ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Multi RSS Streams</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700&display=swap" rel="stylesheet">
8
+ <style>
9
+ * {
10
+ box-sizing: border-box;
11
+ margin: 0;
12
+ padding: 0;
13
+ }
14
+ body, html {
15
+ height: 100%;
16
+ font-family: 'Roboto', sans-serif;
17
+ color: #ffffff;
18
+ }
19
+ body {
20
+ background: linear-gradient(rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.7)),
21
+ url('/backdrop.jpg') no-repeat center center fixed;
22
+ background-size: cover;
23
+ }
24
+ .container {
25
+ display: flex;
26
+ flex-direction: column;
27
+ align-items: center;
28
+ min-height: 100%;
29
+ padding: 20px;
30
+ }
31
+ .logo {
32
+ font-size: 3rem;
33
+ font-weight: 700;
34
+ color: #e50914;
35
+ margin-bottom: 2rem;
36
+ text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
37
+ }
38
+ .content-wrapper {
39
+ background-color: rgba(0, 0, 0, 0.6);
40
+ backdrop-filter: blur(10px);
41
+ border-radius: 4px;
42
+ padding: 40px;
43
+ width: 100%;
44
+ max-width: 450px;
45
+ height: fit-content;
46
+ border: 1px solid rgba(255, 255, 255, 0.1);
47
+ }
48
+ h1 {
49
+ font-size: 2rem;
50
+ font-weight: 700;
51
+ margin-bottom: 28px;
52
+ text-align: center;
53
+ }
54
+ .input-container {
55
+ position: relative;
56
+ margin-bottom: 16px;
57
+ }
58
+ .input {
59
+ width: 100%;
60
+ height: 50px;
61
+ background-color: rgba(51, 51, 51, 0.8);
62
+ border: 1px solid rgba(255, 255, 255, 0.1);
63
+ border-radius: 4px;
64
+ color: white;
65
+ padding: 16px 20px 0;
66
+ font-size: 1rem;
67
+ outline: none;
68
+ }
69
+ .input:focus {
70
+ background-color: rgba(69, 69, 69, 0.8);
71
+ border-color: rgba(255, 255, 255, 0.3);
72
+ }
73
+ .input-label {
74
+ position: absolute;
75
+ top: 50%;
76
+ left: 20px;
77
+ transform: translateY(-50%);
78
+ transition: all 0.1s ease;
79
+ color: #8c8c8c;
80
+ pointer-events: none;
81
+ }
82
+ .input:focus + .input-label,
83
+ .input:not(:placeholder-shown) + .input-label {
84
+ top: 7px;
85
+ font-size: 0.7rem;
86
+ }
87
+ .btn {
88
+ width: 100%;
89
+ height: 50px;
90
+ background-color: #e50914;
91
+ color: white;
92
+ border: none;
93
+ border-radius: 4px;
94
+ font-size: 1rem;
95
+ font-weight: 700;
96
+ margin: 24px 0 12px;
97
+ cursor: pointer;
98
+ transition: background-color 0.2s;
99
+ }
100
+ .btn:hover {
101
+ background-color: #f40612;
102
+ }
103
+ .info {
104
+ color: #737373;
105
+ font-size: 0.9rem;
106
+ margin-top: 16px;
107
+ background-color: rgba(0, 0, 0, 0.6);
108
+ padding: 20px;
109
+ border-radius: 4px;
110
+ border: 1px solid rgba(255, 255, 255, 0.1);
111
+ }
112
+ .addon-url {
113
+ width: 100%;
114
+ background-color: rgba(51, 51, 51, 0.8);
115
+ color: white;
116
+ border: 1px solid rgba(255, 255, 255, 0.1);
117
+ padding: 10px;
118
+ margin-top: 10px;
119
+ border-radius: 4px;
120
+ word-break: break-all;
121
+ display: none;
122
+ }
123
+ .buttons-container {
124
+ display: flex;
125
+ gap: 10px;
126
+ margin-top: 10px;
127
+ }
128
+ .action-btn {
129
+ background-color: #e50914;
130
+ color: white;
131
+ border: none;
132
+ padding: 10px 20px;
133
+ border-radius: 4px;
134
+ cursor: pointer;
135
+ transition: background-color 0.2s;
136
+ flex: 1;
137
+ font-weight: 700;
138
+ }
139
+ .action-btn:hover {
140
+ background-color: #f40612;
141
+ }
142
+ </style>
143
+ </head>
144
+ <body>
145
+ <div class="container">
146
+ <div class="logo">Multi RSS</div>
147
+ <div class="content-wrapper">
148
+ <h1>Stremio Addon Installation</h1>
149
+ <div class="input-container">
150
+ <input type="text" id="dl-key" class="input" placeholder=" " required>
151
+ <label for="dl-key" class="input-label">DebridLink API Key</label>
152
+ </div>
153
+ <div class="input-container">
154
+ <input type="text" id="pr-key" class="input" placeholder=" ">
155
+ <label for="pr-key" class="input-label">Premiumize API Key</label>
156
+ </div>
157
+ <button class="btn" onclick="generateAddonUrl()">GENERATE</button>
158
+ <div id="addon-url" class="addon-url"></div>
159
+ <div class="info">
160
+ <h3>How to get API Keys:</h3>
161
+ <p><strong>DebridLink:</strong> Visit <a href="https://debrid-link.com/webapp/apikey" target="_blank">https://debrid-link.com/webapp/apikey</a></p>
162
+ <p><strong>Premiumize:</strong> Visit <a href="https://www.premiumize.me/account" target="_blank">https://www.premiumize.me/account</a></p>
163
+ </div>
164
+ </div>
165
+ </div>
166
+
167
+ <script>
168
+ function generateAddonUrl() {
169
+ const dlKey = document.getElementById('dl-key').value.trim();
170
+ const prKey = document.getElementById('pr-key').value.trim();
171
+
172
+ if (!dlKey && !prKey) {
173
+ alert('Please enter at least one API key');
174
+ return;
175
+ }
176
+
177
+ // Get the base URL components
178
+ const protocol = window.location.protocol;
179
+ const hostname = window.location.host;
180
+ const baseUrl = `${protocol}//${hostname}`;
181
+
182
+ // Generate the keys part of the URL
183
+ let keys = [];
184
+ if (dlKey) keys.push(`dl=${dlKey}`);
185
+ if (prKey) keys.push(`pr=${prKey}`);
186
+
187
+ // Create the addon URL
188
+ const addonUrl = `${baseUrl}/${keys.join(',')}/manifest.json`;
189
+
190
+ // Always use stremio:// protocol for the Stremio URL, regardless of original protocol
191
+ const stremioUrl = `stremio://${hostname}/${keys.join(',')}/manifest.json`;
192
+
193
+ const urlDiv = document.getElementById('addon-url');
194
+ urlDiv.style.display = 'block';
195
+ urlDiv.innerHTML = `
196
+ <p>Your addon URL:</p>
197
+ <p>${addonUrl}</p>
198
+ <div class="buttons-container">
199
+ <button onclick="window.location.href='${stremioUrl}'" class="action-btn">Install in Stremio</button>
200
+ <button onclick="copyToClipboard('${addonUrl}')" class="action-btn">Copy URL</button>
201
+ </div>
202
+ `;
203
+ }
204
+
205
+ function copyToClipboard(text) {
206
+ navigator.clipboard.writeText(text).then(() => {
207
+ alert('URL copied to clipboard!');
208
+ }).catch(err => {
209
+ console.error('Failed to copy text: ', err);
210
+ alert('Failed to copy URL. Please copy it manually.');
211
+ });
212
+ }
213
+ </script>
214
+ </body>
215
+ </html>
src/aither.js ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import https from 'https';
2
+
3
+ const API_CONFIG = {
4
+ baseUrl: 'https://aither.cc',
5
+ apiKey: 'dRkYjNbT0S4ObFArTH1FXUTLPGsFRV4SqpPwwwEIo0IAtzsKhfF3zkPpTpCOcyBzab5q068680zVeHPk644q9sshtqpMv5mglfou'
6
+ };
7
+
8
+ const agent = new https.Agent({
9
+ rejectUnauthorized: true,
10
+ timeout: 30000,
11
+ keepAlive: true
12
+ });
13
+
14
+ async function fetchWithTimeout(url, options = {}, timeout = 30000) {
15
+ const controller = new AbortController();
16
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
17
+
18
+ try {
19
+ const response = await fetch(url, {
20
+ ...options,
21
+ signal: controller.signal,
22
+ agent
23
+ });
24
+ clearTimeout(timeoutId);
25
+ return response;
26
+ } catch (error) {
27
+ clearTimeout(timeoutId);
28
+ throw error;
29
+ }
30
+ }
31
+
32
+ async function fetchTorrents(imdbId) {
33
+ console.log('\n🔄 Fetching from Aither API for:', imdbId);
34
+ try {
35
+ const params = new URLSearchParams({
36
+ imdbId: imdbId.replace('tt', ''),
37
+ sortField: 'created_at',
38
+ sortDirection: 'desc',
39
+ perPage: '100'
40
+ });
41
+
42
+ const url = `${API_CONFIG.baseUrl}/api/torrents/filter?${params}`;
43
+ console.log('Request URL:', url);
44
+
45
+ const response = await fetchWithTimeout(url, {
46
+ headers: {
47
+ 'Host': 'aither.cc',
48
+ 'Authorization': `Bearer ${API_CONFIG.apiKey}`,
49
+ 'Accept': '*/*',
50
+ 'Accept-Language': '*',
51
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36',
52
+ 'Accept-Encoding': 'gzip, deflate'
53
+ },
54
+ method: 'GET',
55
+ compress: true
56
+ });
57
+
58
+ if (!response.ok) {
59
+ throw new Error(`API request failed: ${response.status}`);
60
+ }
61
+
62
+ const data = await response.json();
63
+
64
+ if (!data.data || !Array.isArray(data.data)) {
65
+ console.log('No torrents found');
66
+ return [];
67
+ }
68
+
69
+ console.log(`Found ${data.data.length} items`);
70
+
71
+ const streams = await Promise.all(data.data.map(async (item, index) => {
72
+ try {
73
+ console.log(`\nProcessing item ${index + 1}/${data.data.length}:`, item.attributes.name);
74
+
75
+ // Check if any of the files are video files
76
+ const hasVideoFiles = item.attributes.files.some(file =>
77
+ /\.(mp4|mkv|avi|mov|wmv)$/i.test(file.name)
78
+ );
79
+
80
+ if (!hasVideoFiles) {
81
+ console.log('No video files found in:', item.attributes.name);
82
+ return null;
83
+ }
84
+
85
+ // Find the main video file (largest video file)
86
+ const videoFiles = item.attributes.files
87
+ .filter(file => /\.(mp4|mkv|avi|mov|wmv)$/i.test(file.name))
88
+ .sort((a, b) => b.size - a.size);
89
+
90
+ const mainFile = videoFiles[0];
91
+
92
+ return {
93
+ magnetLink: `magnet:?xt=urn:btih:${item.attributes.info_hash}`,
94
+ filename: mainFile.name,
95
+ websiteTitle: item.attributes.name,
96
+ quality: extractQuality(item.attributes.name),
97
+ size: formatSize(item.attributes.size),
98
+ source: 'Aither',
99
+ infoHash: item.attributes.info_hash,
100
+ mainFileSize: mainFile.size
101
+ };
102
+ } catch (error) {
103
+ console.error(`Error processing item ${index + 1}:`, error);
104
+ return null;
105
+ }
106
+ }));
107
+
108
+ const validStreams = streams.filter(Boolean);
109
+ console.log(`✅ Processed ${validStreams.length} valid streams`);
110
+
111
+ validStreams.sort((a, b) => {
112
+ const qualityOrder = { '2160p': 4, '4k': 4, '1080p': 3, '720p': 2 };
113
+ return (qualityOrder[b.quality] || 0) - (qualityOrder[a.quality] || 0);
114
+ });
115
+
116
+ return validStreams;
117
+
118
+ } catch (error) {
119
+ console.error('❌ Error fetching from Aither:', error);
120
+ console.error('Error details:', {
121
+ message: error.message,
122
+ cause: error.cause,
123
+ code: error.code,
124
+ syscall: error.syscall
125
+ });
126
+ return [];
127
+ }
128
+ }
129
+
130
+ function extractQuality(title) {
131
+ const qualityMatch = title.match(/\b(2160p|1080p|720p|4k|uhd)\b/i);
132
+ return qualityMatch ? qualityMatch[1].toLowerCase() : '';
133
+ }
134
+
135
+ function formatSize(bytes) {
136
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
137
+ if (bytes === 0) return '0 Bytes';
138
+ const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
139
+ return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
140
+ }
141
+
142
+ export { fetchTorrents };
src/const.js ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const ERROR = {
2
+ EXPIRED_API_KEY: 'Expired API key',
3
+ ACCESS_DENIED: 'Access denied',
4
+ TWO_FACTOR_AUTH: 'Two factor authentication required',
5
+ NOT_PREMIUM: 'Premium account required',
6
+ NOT_READY: 'Torrent not ready',
7
+ INVALID_API_KEY: 'Invalid API key'
8
+ };
9
+
10
+ export const VIDEO_EXTENSIONS = [
11
+ '.mp4', '.mkv', '.avi',
12
+ '.mov', '.wmv', '.flv',
13
+ '.webm', '.m4v', '.ts'
14
+ ];
src/debrids.js ADDED
@@ -0,0 +1,294 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ERROR } from './const.js';
2
+ import { createHash } from 'crypto';
3
+
4
+ class BaseDebrid {
5
+ #apiKey;
6
+
7
+ constructor(apiKey, prefix) {
8
+ this.#apiKey = apiKey.replace(`${prefix}=`, '');
9
+ }
10
+
11
+ getKey() {
12
+ return this.#apiKey;
13
+ }
14
+ }
15
+
16
+ class DebridLink extends BaseDebrid {
17
+ constructor(apiKey) {
18
+ super(apiKey, 'dl');
19
+ }
20
+
21
+ static canHandle(apiKey) {
22
+ return apiKey.startsWith('dl=');
23
+ }
24
+
25
+ async #request(method, path, opts = {}) {
26
+ try {
27
+ const query = opts.query || {};
28
+ const queryString = new URLSearchParams(query).toString();
29
+ const url = `https://debrid-link.com/api/v2${path}${queryString ? '?' + queryString : ''}`;
30
+
31
+ opts = {
32
+ method,
33
+ headers: {
34
+ 'User-Agent': 'Stremio',
35
+ 'Accept': 'application/json',
36
+ 'Authorization': `Bearer ${this.getKey()}`,
37
+ ...(method === 'POST' && {
38
+ 'Content-Type': 'application/json'
39
+ }),
40
+ ...(opts.headers || {})
41
+ },
42
+ ...opts
43
+ };
44
+
45
+ console.log('\n🔷 DebridLink Request:', method, path);
46
+ if (opts.body) console.log('Request Body:', opts.body);
47
+ console.log('Request URL:', url);
48
+ console.log('Request Headers:', opts.headers);
49
+
50
+ const startTime = Date.now();
51
+ const res = await fetch(url, opts);
52
+ console.log(`Response Time: ${Date.now() - startTime}ms`);
53
+ console.log('Response Status:', res.status);
54
+
55
+ const data = await res.json();
56
+ console.log('Response Data:', data);
57
+
58
+ if (!data.success) {
59
+ switch (data.error) {
60
+ case 'badToken':
61
+ throw new Error(ERROR.INVALID_API_KEY);
62
+ case 'maxLink':
63
+ case 'maxLinkHost':
64
+ case 'maxData':
65
+ case 'maxDataHost':
66
+ case 'maxTorrent':
67
+ case 'torrentTooBig':
68
+ case 'freeServerOverload':
69
+ throw new Error(ERROR.NOT_PREMIUM);
70
+ default:
71
+ throw new Error(`API Error: ${JSON.stringify(data)}`);
72
+ }
73
+ }
74
+
75
+ return data.value;
76
+
77
+ } catch (error) {
78
+ console.error('❌ Request failed:', error);
79
+ throw error;
80
+ }
81
+ }
82
+
83
+ async checkCacheStatuses(hashes) {
84
+ try {
85
+ console.log(`\n📡 DebridLink: Batch checking ${hashes.length} hashes`);
86
+ console.log('Sample hashes being checked:', hashes.slice(0, 3));
87
+
88
+ const response = await this.#request('GET', '/seedbox/cached', {
89
+ query: { url: hashes.join(',') }
90
+ });
91
+
92
+ console.log('Raw cache check response:', response);
93
+
94
+ const results = {};
95
+ for (const hash of hashes) {
96
+ const cacheInfo = response[hash];
97
+ results[hash] = {
98
+ cached: !!cacheInfo,
99
+ files: cacheInfo?.files || [],
100
+ fileCount: cacheInfo?.files?.length || 0,
101
+ service: 'DebridLink'
102
+ };
103
+ }
104
+
105
+ const cachedCount = Object.values(results).filter(r => r.cached).length;
106
+ console.log(`DebridLink found ${cachedCount} cached torrents out of ${hashes.length}`);
107
+
108
+ return results;
109
+ } catch (error) {
110
+ if (error.message === ERROR.INVALID_API_KEY) {
111
+ console.error('❌ Invalid DebridLink API key');
112
+ return {};
113
+ }
114
+ console.error('Cache check failed:', error);
115
+ return {};
116
+ }
117
+ }
118
+
119
+ async getStreamUrl(magnetLink) {
120
+ try {
121
+ console.log('\n📥 Using DebridLink to process magnet:', magnetLink.substring(0, 100) + '...');
122
+
123
+ const data = await this.#request('POST', '/seedbox/add', {
124
+ body: JSON.stringify({
125
+ url: magnetLink,
126
+ async: true
127
+ })
128
+ });
129
+
130
+ console.log('Seedbox add response:', data);
131
+
132
+ const videoFiles = data.files
133
+ .filter(file => /\.(mp4|mkv|avi|mov|webm)$/i.test(file.name))
134
+ .sort((a, b) => b.size - a.size);
135
+
136
+ if (!videoFiles.length) {
137
+ console.error('No video files found in torrent');
138
+ throw new Error('No video files found');
139
+ }
140
+
141
+ console.log('Selected video file:', videoFiles[0].name);
142
+ return videoFiles[0].downloadUrl;
143
+ } catch (error) {
144
+ console.error('❌ Failed to get stream URL:', error);
145
+ throw error;
146
+ }
147
+ }
148
+ }
149
+
150
+ class Premiumize extends BaseDebrid {
151
+ #apiUrl = 'https://www.premiumize.me/api';
152
+
153
+ constructor(apiKey) {
154
+ super(apiKey, 'pr');
155
+ }
156
+
157
+ static canHandle(apiKey) {
158
+ return apiKey.startsWith('pr=');
159
+ }
160
+
161
+ async #request(method, url, opts = {}) {
162
+ const retries = 3;
163
+ let lastError;
164
+
165
+ for (let i = 0; i < retries; i++) {
166
+ try {
167
+ console.log(`\n🔷 Premiumize Request (Attempt ${i + 1}/${retries}):`, method, url);
168
+ if (opts.body) console.log('Request Body:', opts.body);
169
+
170
+ const controller = new AbortController();
171
+ const timeout = setTimeout(() => controller.abort(), 30000);
172
+
173
+ const startTime = Date.now();
174
+ const response = await fetch(url, {
175
+ ...opts,
176
+ method,
177
+ signal: controller.signal
178
+ });
179
+
180
+ clearTimeout(timeout);
181
+ console.log(`Response Time: ${Date.now() - startTime}ms`);
182
+ console.log('Response Status:', response.status);
183
+
184
+ const data = await response.json();
185
+ console.log('Response Data:', data);
186
+ return data;
187
+
188
+ } catch (error) {
189
+ console.log(`Attempt ${i + 1} failed:`, error.message);
190
+ lastError = error;
191
+ if (i < retries - 1) {
192
+ console.log('Retrying after 2 seconds...');
193
+ await new Promise(r => setTimeout(r, 2000));
194
+ }
195
+ }
196
+ }
197
+
198
+ throw lastError;
199
+ }
200
+
201
+ async checkCacheStatuses(hashes) {
202
+ try {
203
+ console.log(`\n📡 Premiumize: Batch checking ${hashes.length} hashes`);
204
+ console.log('Sample hashes being checked:', hashes.slice(0, 3));
205
+
206
+ const params = new URLSearchParams({ apikey: this.getKey() });
207
+ hashes.forEach(hash => params.append('items[]', hash));
208
+
209
+ const data = await this.#request('GET', `${this.#apiUrl}/cache/check?${params}`);
210
+
211
+ if (data.status !== 'success') {
212
+ if (data.message === 'Invalid API key.') {
213
+ console.error('❌ Invalid Premiumize API key');
214
+ return {};
215
+ }
216
+ throw new Error('API Error: ' + JSON.stringify(data));
217
+ }
218
+
219
+ const results = {};
220
+ hashes.forEach((hash, index) => {
221
+ results[hash] = {
222
+ cached: data.response[index],
223
+ files: [],
224
+ fileCount: 0,
225
+ service: 'Premiumize'
226
+ };
227
+ });
228
+
229
+ const cachedCount = Object.values(results).filter(r => r.cached).length;
230
+ console.log(`Premiumize found ${cachedCount} cached torrents out of ${hashes.length}`);
231
+
232
+ return results;
233
+ } catch (error) {
234
+ console.error('Cache check failed:', error);
235
+ return {};
236
+ }
237
+ }
238
+
239
+ async getStreamUrl(magnetLink) {
240
+ try {
241
+ console.log('\n📥 Using Premiumize to process magnet:', magnetLink.substring(0, 100) + '...');
242
+
243
+ const body = new FormData();
244
+ body.append('apikey', this.getKey());
245
+ body.append('src', magnetLink);
246
+
247
+ const data = await this.#request('POST', `${this.#apiUrl}/transfer/directdl`, {
248
+ body
249
+ });
250
+
251
+ if (data.status !== 'success') {
252
+ console.error('API Error:', data);
253
+ throw new Error('Failed to add magnet');
254
+ }
255
+
256
+ const videoFiles = data.content
257
+ .filter(file => /\.(mp4|mkv|avi|mov|webm)$/i.test(file.path))
258
+ .sort((a, b) => b.size - a.size);
259
+
260
+ if (!videoFiles.length) {
261
+ console.error('No video files found in torrent');
262
+ throw new Error('No video files found');
263
+ }
264
+
265
+ console.log('Selected video file:', videoFiles[0].path);
266
+ return videoFiles[0].link;
267
+ } catch (error) {
268
+ console.error('❌ Failed to get stream URL:', error);
269
+ throw error;
270
+ }
271
+ }
272
+ }
273
+
274
+ export function getDebridServices(apiKeys) {
275
+ console.log('\n🔐 Initializing debrid services with keys:', apiKeys);
276
+ const services = [];
277
+
278
+ for (const key of apiKeys.split(',')) {
279
+ if (DebridLink.canHandle(key)) {
280
+ console.log('Adding DebridLink service');
281
+ services.push(new DebridLink(key));
282
+ } else if (Premiumize.canHandle(key)) {
283
+ console.log('Adding Premiumize service');
284
+ services.push(new Premiumize(key));
285
+ } else {
286
+ console.log('Unknown service key format:', key);
287
+ }
288
+ }
289
+
290
+ console.log(`Initialized ${services.length} debrid services`);
291
+ return services;
292
+ }
293
+
294
+ export { DebridLink, Premiumize };
src/iptorrents.js ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import xml2js from 'xml2js';
2
+ import readTorrent from 'read-torrent';
3
+ import { promisify } from 'util';
4
+
5
+ const readTorrentPromise = promisify(readTorrent);
6
+
7
+ const RSS_FEEDS = [
8
+ {
9
+ name: 'IPT',
10
+ url: 'https://ipt.beelyrics.net/t.rss?u=change;tp=change;48;20;7;100;101;62;54;77;22;99;4;78;5;66;65;23;26;55;79;25;24'
11
+ }
12
+ ];
13
+
14
+ const parser = new xml2js.Parser({
15
+ explicitArray: false,
16
+ ignoreAttrs: true
17
+ });
18
+
19
+ async function downloadAndParseTorrent(url) {
20
+ try {
21
+ console.log('Downloading torrent from:', url);
22
+
23
+ const options = {
24
+ headers: {
25
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
26
+ 'Accept': '*/*',
27
+ 'Cookie': 'uid=2105434; pass=82acec029bda7c69257905656252be36; login=1',
28
+ 'Referer': 'https://ipt.beelyrics.net/',
29
+ 'Origin': 'https://ipt.beelyrics.net'
30
+ }
31
+ };
32
+
33
+ const torrentInfo = await readTorrentPromise(url, options);
34
+ console.log('Parsed torrent info:', {
35
+ name: torrentInfo.name,
36
+ length: torrentInfo.length,
37
+ files: torrentInfo.files?.length || 0,
38
+ announce: torrentInfo.announce
39
+ });
40
+
41
+ if (!torrentInfo?.infoHash) {
42
+ console.error('No info hash found');
43
+ return null;
44
+ }
45
+
46
+ const videoFiles = torrentInfo.files?.filter(file => {
47
+ const filePath = Array.isArray(file.path) ? file.path.join('/') : file.path;
48
+ return /\.(mp4|mkv|avi|mov|wmv)$/i.test(filePath);
49
+ }) || [];
50
+
51
+ if (videoFiles.length === 0) {
52
+ console.log('No video files found');
53
+ return null;
54
+ }
55
+
56
+ videoFiles.sort((a, b) => b.length - a.length);
57
+ const mainFile = videoFiles[0];
58
+ const mainFilePath = Array.isArray(mainFile.path) ? mainFile.path.join('/') : mainFile.path;
59
+
60
+ const magnetUri = `magnet:?xt=urn:btih:${torrentInfo.infoHash}` +
61
+ `&dn=${encodeURIComponent(torrentInfo.name)}` +
62
+ (torrentInfo.announce ? torrentInfo.announce.map(tr => `&tr=${encodeURIComponent(tr)}`).join('') : '');
63
+
64
+ return {
65
+ magnetLink: magnetUri,
66
+ files: videoFiles,
67
+ infoHash: torrentInfo.infoHash,
68
+ mainFile: {
69
+ path: mainFilePath,
70
+ length: mainFile.length
71
+ }
72
+ };
73
+
74
+ } catch (error) {
75
+ console.error('Error:', error);
76
+ return null;
77
+ }
78
+ }
79
+
80
+ function formatFileSize(bytes) {
81
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
82
+ if (bytes === 0) return '0 Bytes';
83
+ const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
84
+ return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
85
+ }
86
+
87
+ function parseTorrentInfo(item) {
88
+ let size = 'Unknown';
89
+ let category = 'Unknown';
90
+ let sizeInMB = 0;
91
+
92
+ const parts = (item.description || '').split(/[;|]/);
93
+ if (parts.length >= 1) {
94
+ const sizeInfo = formatSize(parts[0].trim());
95
+ size = sizeInfo.size;
96
+ sizeInMB = sizeInfo.sizeInMB;
97
+ }
98
+ if (parts.length >= 2) {
99
+ category = parts[1].trim();
100
+ }
101
+
102
+ return { size, category, sizeInMB };
103
+ }
104
+
105
+ function formatSize(sizeStr) {
106
+ if (!sizeStr) return { size: 'Unknown', sizeInMB: 0 };
107
+
108
+ const match = sizeStr.match(/([\d.]+)\s*(GB|MB|KB|B)/i);
109
+ if (!match) return { size: sizeStr, sizeInMB: 0 };
110
+
111
+ const [, value, unit] = match;
112
+ const numValue = parseFloat(value);
113
+ let sizeInMB = numValue;
114
+
115
+ switch (unit.toUpperCase()) {
116
+ case 'GB':
117
+ sizeInMB *= 1024;
118
+ break;
119
+ case 'KB':
120
+ sizeInMB /= 1024;
121
+ break;
122
+ case 'B':
123
+ sizeInMB /= (1024 * 1024);
124
+ break;
125
+ }
126
+
127
+ return {
128
+ size: sizeStr.trim(),
129
+ sizeInMB: Math.round(sizeInMB * 100) / 100
130
+ };
131
+ }
132
+
133
+ function extractQuality(title) {
134
+ const qualityMatch = title.match(/\b(2160p|1080p|720p|4k|uhd)\b/i);
135
+ return qualityMatch ? qualityMatch[1].toLowerCase() : '';
136
+ }
137
+
138
+ async function fetchRSSFeeds(imdbId) {
139
+ console.log('\n🔄 Fetching RSS feeds for:', imdbId);
140
+ let allStreams = [];
141
+
142
+ for (const feed of RSS_FEEDS) {
143
+ try {
144
+ console.log(`\nFetching from ${feed.name}...`);
145
+
146
+ const rssUrl = `${feed.url};download;q=${imdbId}`;
147
+
148
+ const response = await fetch(rssUrl, {
149
+ headers: {
150
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
151
+ 'Accept': 'application/rss+xml,application/xml,text/xml',
152
+ 'Cookie': 'uid=2105434; pass=82acec029bda7c69257905656252be36; login=1',
153
+ 'Referer': 'https://ipt.beelyrics.net/',
154
+ 'Origin': 'https://ipt.beelyrics.net'
155
+ }
156
+ });
157
+
158
+ if (!response.ok) {
159
+ console.error(`❌ Failed to fetch from ${feed.name}:`, response.status);
160
+ continue;
161
+ }
162
+
163
+ const rssData = await response.text();
164
+ const result = await parser.parseStringPromise(rssData);
165
+
166
+ if (!result?.rss?.channel?.item) {
167
+ console.log(`No items found in ${feed.name}`);
168
+ continue;
169
+ }
170
+
171
+ const items = Array.isArray(result.rss.channel.item) ?
172
+ result.rss.channel.item : [result.rss.channel.item];
173
+
174
+ console.log(`Found ${items.length} items in ${feed.name}`);
175
+
176
+ const streams = await Promise.all(items.map(async (item, index) => {
177
+ try {
178
+ console.log(`\nProcessing item ${index + 1}/${items.length}:`, item.title);
179
+
180
+ const torrentInfo = await downloadAndParseTorrent(item.link);
181
+ if (!torrentInfo) return null;
182
+
183
+ const { size, category } = parseTorrentInfo(item);
184
+ const quality = extractQuality(item.title);
185
+
186
+ return {
187
+ magnetLink: torrentInfo.magnetLink,
188
+ filename: torrentInfo.mainFile.path,
189
+ websiteTitle: item.title,
190
+ quality,
191
+ size,
192
+ source: feed.name,
193
+ infoHash: torrentInfo.infoHash,
194
+ mainFileSize: torrentInfo.mainFile.length
195
+ };
196
+ } catch (error) {
197
+ console.error(`Error processing item ${index + 1}:`, error);
198
+ return null;
199
+ }
200
+ }));
201
+
202
+ const validStreams = streams.filter(Boolean);
203
+ console.log(`✅ Processed ${validStreams.length} valid streams from ${feed.name}`);
204
+ allStreams = [...allStreams, ...validStreams];
205
+
206
+ } catch (error) {
207
+ console.error(`❌ Error fetching ${feed.name}:`, error);
208
+ }
209
+ }
210
+
211
+ allStreams.sort((a, b) => {
212
+ const qualityOrder = { '2160p': 4, '4k': 4, '1080p': 3, '720p': 2 };
213
+ return (qualityOrder[b.quality] || 0) - (qualityOrder[a.quality] || 0);
214
+ });
215
+
216
+ return allStreams;
217
+ }
218
+
219
+ export { fetchRSSFeeds };
src/newrss.rar ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:100a6734684e1bfae4c2d0cba5e7c003e472bb2a2c88b1479b985ab922ce80ce
3
+ size 343421
src/tday.js ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import xml2js from 'xml2js';
2
+ import readTorrent from 'read-torrent';
3
+ import { promisify } from 'util';
4
+
5
+ const readTorrentPromise = promisify(readTorrent);
6
+
7
+ const RSS_FEEDS = [
8
+ {
9
+ name: 'TD',
10
+ url: 'https://www.torrentday.com/t.rss?7;26;14;46;33;2;31;96;34;5;44;25;11;82;24;21;22;48;13;1;3;32;download;u=change;tp=change;change;private;do-not-share'
11
+ }
12
+ ];
13
+
14
+ const parser = new xml2js.Parser({
15
+ explicitArray: false,
16
+ ignoreAttrs: true
17
+ });
18
+
19
+ function parseTorrentInfo(item) {
20
+ const desc = item.description || '';
21
+ const match = desc.match(/Category:\s*([^]+?)Size:\s*([^]+?)$/);
22
+
23
+ if (!match) {
24
+ return { size: 'Unknown', category: 'Unknown', sizeInMB: 0 };
25
+ }
26
+
27
+ const category = match[1].trim();
28
+ const size = match[2].trim();
29
+
30
+ const sizeMatch = size.match(/([\d.]+)\s*(GB|MB|TB)/i);
31
+ let sizeInMB = 0;
32
+
33
+ if (sizeMatch) {
34
+ const [, value, unit] = sizeMatch;
35
+ sizeInMB = parseFloat(value);
36
+ switch (unit.toUpperCase()) {
37
+ case 'TB':
38
+ sizeInMB *= 1024 * 1024;
39
+ break;
40
+ case 'GB':
41
+ sizeInMB *= 1024;
42
+ break;
43
+ }
44
+ }
45
+
46
+ return { size, category, sizeInMB };
47
+ }
48
+
49
+ async function downloadAndParseTorrent(url) {
50
+ try {
51
+ console.log('Downloading torrent from:', url);
52
+
53
+ const options = {
54
+ headers: {
55
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
56
+ 'Accept': '*/*',
57
+ 'Referer': 'https://www.torrentday.com/',
58
+ 'Origin': 'https://www.torrentday.com'
59
+ }
60
+ };
61
+
62
+ const torrentInfo = await readTorrentPromise(url, options);
63
+ console.log('Parsed torrent info:', {
64
+ name: torrentInfo.name,
65
+ length: torrentInfo.length,
66
+ files: torrentInfo.files?.length || 0
67
+ });
68
+
69
+ if (!torrentInfo?.infoHash) {
70
+ console.error('No info hash found');
71
+ return null;
72
+ }
73
+
74
+ const videoFiles = torrentInfo.files?.filter(file => {
75
+ const filePath = Array.isArray(file.path) ? file.path.join('/') : file.path;
76
+ return /\.(mp4|mkv|avi|mov|wmv)$/i.test(filePath);
77
+ }) || [];
78
+
79
+ if (videoFiles.length === 0) {
80
+ console.log('No video files found');
81
+ return null;
82
+ }
83
+
84
+ videoFiles.sort((a, b) => b.length - a.length);
85
+ const mainFile = videoFiles[0];
86
+ const mainFilePath = Array.isArray(mainFile.path) ? mainFile.path.join('/') : mainFile.path;
87
+
88
+ const magnetUri = `magnet:?xt=urn:btih:${torrentInfo.infoHash}` +
89
+ `&dn=${encodeURIComponent(torrentInfo.name)}` +
90
+ (torrentInfo.announce ? torrentInfo.announce.map(tr => `&tr=${encodeURIComponent(tr)}`).join('') : '');
91
+
92
+ return {
93
+ magnetLink: magnetUri,
94
+ files: videoFiles,
95
+ infoHash: torrentInfo.infoHash,
96
+ mainFile: {
97
+ path: mainFilePath,
98
+ length: mainFile.length
99
+ }
100
+ };
101
+
102
+ } catch (error) {
103
+ console.error('Error:', error);
104
+ return null;
105
+ }
106
+ }
107
+
108
+ async function fetchRSSFeeds(imdbId) {
109
+ console.log('\n🔄 Fetching RSS feeds for:', imdbId);
110
+ let allStreams = [];
111
+
112
+ for (const feed of RSS_FEEDS) {
113
+ try {
114
+ console.log(`\nFetching from ${feed.name}...`);
115
+
116
+ const rssUrl = `${feed.url};q=${imdbId}`;
117
+
118
+ const response = await fetch(rssUrl, {
119
+ headers: {
120
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
121
+ 'Accept': 'application/rss+xml,application/xml,text/xml',
122
+ 'Referer': 'https://www.torrentday.com/',
123
+ 'Origin': 'https://www.torrentday.com'
124
+ }
125
+ });
126
+
127
+ if (!response.ok) {
128
+ console.error(`❌ Failed to fetch from ${feed.name}:`, response.status);
129
+ continue;
130
+ }
131
+
132
+ const rssData = await response.text();
133
+ const result = await parser.parseStringPromise(rssData);
134
+
135
+ if (!result?.rss?.channel?.item) {
136
+ console.log(`No items found in ${feed.name}`);
137
+ continue;
138
+ }
139
+
140
+ const items = Array.isArray(result.rss.channel.item) ?
141
+ result.rss.channel.item : [result.rss.channel.item];
142
+
143
+ console.log(`Found ${items.length} items in ${feed.name}`);
144
+
145
+ const streams = await Promise.all(items.map(async (item, index) => {
146
+ try {
147
+ console.log(`\nProcessing item ${index + 1}/${items.length}:`, item.title);
148
+
149
+ const torrentInfo = await downloadAndParseTorrent(item.link);
150
+ if (!torrentInfo) return null;
151
+
152
+ const { size, category } = parseTorrentInfo(item);
153
+ const quality = extractQuality(item.title);
154
+
155
+ return {
156
+ magnetLink: torrentInfo.magnetLink,
157
+ filename: torrentInfo.mainFile.path,
158
+ websiteTitle: item.title,
159
+ quality,
160
+ size,
161
+ category,
162
+ source: feed.name,
163
+ infoHash: torrentInfo.infoHash,
164
+ mainFileSize: torrentInfo.mainFile.length,
165
+ pubDate: item.pubDate
166
+ };
167
+ } catch (error) {
168
+ console.error(`Error processing item ${index + 1}:`, error);
169
+ return null;
170
+ }
171
+ }));
172
+
173
+ const validStreams = streams.filter(Boolean);
174
+ console.log(`✅ Processed ${validStreams.length} valid streams from ${feed.name}`);
175
+ allStreams = [...allStreams, ...validStreams];
176
+
177
+ } catch (error) {
178
+ console.error(`❌ Error fetching ${feed.name}:`, error);
179
+ }
180
+ }
181
+
182
+ allStreams.sort((a, b) => {
183
+ const qualityOrder = { '2160p': 4, '4k': 4, 'uhd': 4, '1080p': 3, '720p': 2 };
184
+ return (qualityOrder[b.quality] || 0) - (qualityOrder[a.quality] || 0);
185
+ });
186
+
187
+ return allStreams;
188
+ }
189
+
190
+ function extractQuality(title) {
191
+ const qualityMatch = title.match(/\b(2160p|1080p|720p|4k|uhd)\b/i);
192
+ return qualityMatch ? qualityMatch[1].toLowerCase() : '';
193
+ }
194
+
195
+ export { fetchRSSFeeds };
src/test.js ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { fetchRSSFeeds } from '../src/rss.js';
2
+
3
+ async function testRSSFeeds() {
4
+ try {
5
+ // Test with a recent movie - Deadpool & Wolverine
6
+ const imdbId = "tt6263850";
7
+ console.log('\n🧪 Testing RSS Feeds with IMDB ID:', imdbId);
8
+
9
+ console.log('Fetching RSS feeds...');
10
+ const streams = await fetchRSSFeeds(imdbId);
11
+
12
+ console.log('\n📊 Results:');
13
+ console.log(`Total streams found: ${streams.length}`);
14
+
15
+ // Group streams by source
16
+ const sourceGroups = streams.reduce((acc, stream) => {
17
+ acc[stream.source] = acc[stream.source] || [];
18
+ acc[stream.source].push(stream);
19
+ return acc;
20
+ }, {});
21
+
22
+ // Print results by source
23
+ Object.entries(sourceGroups).forEach(([source, sourceStreams]) => {
24
+ console.log(`\n${source}:`);
25
+ console.log(`Found ${sourceStreams.length} streams`);
26
+
27
+ // Print first 3 streams as examples
28
+ sourceStreams.slice(0, 3).forEach((stream, index) => {
29
+ console.log(`\n${index + 1}. Stream Details:`);
30
+ console.log(`Title: ${stream.filename}`);
31
+ console.log(`Quality: ${stream.quality}`);
32
+ console.log(`Size: ${stream.size}`);
33
+ console.log(`Magnet: ${stream.magnetLink.substring(0, 60)}...`);
34
+ });
35
+ });
36
+
37
+ // Test quality distribution
38
+ const qualityDistribution = streams.reduce((acc, stream) => {
39
+ acc[stream.quality] = (acc[stream.quality] || 0) + 1;
40
+ return acc;
41
+ }, {});
42
+
43
+ console.log('\n📈 Quality Distribution:');
44
+ Object.entries(qualityDistribution).forEach(([quality, count]) => {
45
+ console.log(`${quality}: ${count} streams`);
46
+ });
47
+
48
+ } catch (error) {
49
+ console.error('❌ Test failed:', error);
50
+ }
51
+ }
52
+
53
+ // Run the test
54
+ console.log('🚀 Starting RSS Feed Test\n');
55
+ testRSSFeeds().then(() => {
56
+ console.log('\n✅ Test completed');
57
+ }).catch(error => {
58
+ console.error('\n❌ Test failed:', error);
59
+ });
src/torrenting.js ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import xml2js from 'xml2js';
2
+ import readTorrent from 'read-torrent';
3
+ import { promisify } from 'util';
4
+
5
+ const readTorrentPromise = promisify(readTorrent);
6
+
7
+ const RSS_FEEDS = [
8
+ {
9
+ name: 'TT',
10
+ url: 'https://torrenting.com/t.rss?5;18;4;82;49;99;47;38;11;55;3;40;1;download;u=change;tp=change;change;private;do-not-share',
11
+ domain: 'torrenting.com'
12
+ }
13
+ ];
14
+
15
+ const parser = new xml2js.Parser({
16
+ explicitArray: false,
17
+ ignoreAttrs: true
18
+ });
19
+
20
+ function parseTorrentInfo(item) {
21
+ const desc = item.description || '';
22
+ const match = desc.match(/Category:\s*([^]+?)Size:\s*([^]+?)$/);
23
+
24
+ if (!match) {
25
+ return { size: 'Unknown', category: 'Unknown', sizeInMB: 0 };
26
+ }
27
+
28
+ const category = match[1].trim();
29
+ const size = match[2].trim();
30
+
31
+ const sizeMatch = size.match(/([\d.]+)\s*(GB|MB|TB)/i);
32
+ let sizeInMB = 0;
33
+
34
+ if (sizeMatch) {
35
+ const [, value, unit] = sizeMatch;
36
+ sizeInMB = parseFloat(value);
37
+ switch (unit.toUpperCase()) {
38
+ case 'TB':
39
+ sizeInMB *= 1024 * 1024;
40
+ break;
41
+ case 'GB':
42
+ sizeInMB *= 1024;
43
+ break;
44
+ }
45
+ }
46
+
47
+ return { size, category, sizeInMB };
48
+ }
49
+
50
+ async function downloadAndParseTorrent(url) {
51
+ try {
52
+ console.log('Downloading torrent from:', url);
53
+
54
+ const options = {
55
+ headers: {
56
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
57
+ 'Accept': '*/*',
58
+ 'Referer': 'https://torrenting.com/',
59
+ 'Origin': 'https://torrenting.com'
60
+ }
61
+ };
62
+
63
+ const torrentInfo = await readTorrentPromise(url, options);
64
+ console.log('Parsed torrent info:', {
65
+ name: torrentInfo.name,
66
+ length: torrentInfo.length,
67
+ files: torrentInfo.files?.length || 0
68
+ });
69
+
70
+ if (!torrentInfo?.infoHash) {
71
+ console.error('No info hash found');
72
+ return null;
73
+ }
74
+
75
+ const videoFiles = torrentInfo.files?.filter(file => {
76
+ const filePath = Array.isArray(file.path) ? file.path.join('/') : file.path;
77
+ return /\.(mp4|mkv|avi|mov|wmv)$/i.test(filePath);
78
+ }) || [];
79
+
80
+ if (videoFiles.length === 0) {
81
+ console.log('No video files found');
82
+ return null;
83
+ }
84
+
85
+ videoFiles.sort((a, b) => b.length - a.length);
86
+ const mainFile = videoFiles[0];
87
+ const mainFilePath = Array.isArray(mainFile.path) ? mainFile.path.join('/') : mainFile.path;
88
+
89
+ const magnetUri = `magnet:?xt=urn:btih:${torrentInfo.infoHash}` +
90
+ `&dn=${encodeURIComponent(torrentInfo.name)}` +
91
+ (torrentInfo.announce ? torrentInfo.announce.map(tr => `&tr=${encodeURIComponent(tr)}`).join('') : '');
92
+
93
+ return {
94
+ magnetLink: magnetUri,
95
+ files: videoFiles,
96
+ infoHash: torrentInfo.infoHash,
97
+ mainFile: {
98
+ path: mainFilePath,
99
+ length: mainFile.length
100
+ }
101
+ };
102
+
103
+ } catch (error) {
104
+ console.error('Error:', error);
105
+ return null;
106
+ }
107
+ }
108
+
109
+ async function fetchRSSFeeds(imdbId) {
110
+ console.log('\n🔄 Fetching RSS feeds for:', imdbId);
111
+ let allStreams = [];
112
+
113
+ for (const feed of RSS_FEEDS) {
114
+ try {
115
+ console.log(`\nFetching from ${feed.name}...`);
116
+
117
+ const rssUrl = `${feed.url}&q=${imdbId}`;
118
+
119
+ const response = await fetch(rssUrl, {
120
+ headers: {
121
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
122
+ 'Accept': 'application/rss+xml,application/xml,text/xml',
123
+ 'Referer': 'https://torrenting.com/',
124
+ 'Origin': 'https://torrenting.com'
125
+ }
126
+ });
127
+
128
+ if (!response.ok) {
129
+ console.error(`❌ Failed to fetch from ${feed.name}:`, response.status);
130
+ continue;
131
+ }
132
+
133
+ const rssData = await response.text();
134
+ const result = await parser.parseStringPromise(rssData);
135
+
136
+ if (!result?.rss?.channel?.item) {
137
+ console.log(`No items found in ${feed.name}`);
138
+ continue;
139
+ }
140
+
141
+ const items = Array.isArray(result.rss.channel.item) ?
142
+ result.rss.channel.item : [result.rss.channel.item];
143
+
144
+ console.log(`Found ${items.length} items in ${feed.name}`);
145
+
146
+ const streams = await Promise.all(items.map(async (item, index) => {
147
+ try {
148
+ console.log(`\nProcessing item ${index + 1}/${items.length}:`, item.title);
149
+
150
+ const torrentInfo = await downloadAndParseTorrent(item.link);
151
+ if (!torrentInfo) return null;
152
+
153
+ const { size, category } = parseTorrentInfo(item);
154
+ const quality = extractQuality(item.title);
155
+
156
+ return {
157
+ magnetLink: torrentInfo.magnetLink,
158
+ filename: torrentInfo.mainFile.path,
159
+ websiteTitle: item.title,
160
+ quality,
161
+ size,
162
+ category,
163
+ source: feed.name,
164
+ infoHash: torrentInfo.infoHash,
165
+ mainFileSize: torrentInfo.mainFile.length,
166
+ pubDate: item.pubDate
167
+ };
168
+ } catch (error) {
169
+ console.error(`Error processing item ${index + 1}:`, error);
170
+ return null;
171
+ }
172
+ }));
173
+
174
+ const validStreams = streams.filter(Boolean);
175
+ console.log(`✅ Processed ${validStreams.length} valid streams from ${feed.name}`);
176
+ allStreams = [...allStreams, ...validStreams];
177
+
178
+ } catch (error) {
179
+ console.error(`❌ Error fetching ${feed.name}:`, error);
180
+ }
181
+ }
182
+
183
+ allStreams.sort((a, b) => {
184
+ const qualityOrder = { '2160p': 4, '4k': 4, 'uhd': 4, '1080p': 3, '720p': 2 };
185
+ return (qualityOrder[b.quality] || 0) - (qualityOrder[a.quality] || 0);
186
+ });
187
+
188
+ return allStreams;
189
+ }
190
+
191
+ function extractQuality(title) {
192
+ const qualityMatch = title.match(/\b(2160p|1080p|720p|4k|uhd)\b/i);
193
+ return qualityMatch ? qualityMatch[1].toLowerCase() : '';
194
+ }
195
+
196
+ export { fetchRSSFeeds };
src/util.js ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { VIDEO_EXTENSIONS } from './const.js';
2
+
3
+ export function isVideo(filename) {
4
+ return VIDEO_EXTENSIONS.some(ext =>
5
+ filename.toLowerCase().endsWith(ext)
6
+ );
7
+ }
8
+
9
+ export function extractInfoHash(magnetLink) {
10
+ const match = magnetLink.match(/xt=urn:btih:([^&]+)/i);
11
+ return match ? match[1].toLowerCase() : null;
12
+ }
13
+
14
+ export async function wait(ms) {
15
+ return new Promise(resolve => setTimeout(resolve, ms));
16
+ }
17
+
18
+ export function base64Encode(str) {
19
+ return Buffer.from(str).toString('base64')
20
+ .replace(/\+/g, '-')
21
+ .replace(/\//g, '_')
22
+ .replace(/=/g, '');
23
+ }
24
+
25
+ export function base64Decode(str) {
26
+ str = str.replace(/-/g, '+').replace(/_/g, '/');
27
+ while (str.length % 4) str += '=';
28
+ return Buffer.from(str, 'base64').toString('ascii');
29
+ }
30
+
31
+ export function findBestFile(files, metadata) {
32
+ if (!files?.length) return null;
33
+ if (files.length === 1) return files[0];
34
+
35
+ // If no metadata provided, return largest file
36
+ if (!metadata?.meta?.name) {
37
+ const largestFile = files.sort((a, b) => (b.size || 0) - (a.size || 0))[0];
38
+ console.log('Selected largest file (no metadata available):', largestFile.name || largestFile.path);
39
+ return largestFile;
40
+ }
41
+
42
+ const title = metadata.meta.name.toLowerCase();
43
+ const year = new Date(metadata.meta.released).getFullYear();
44
+ const yearStr = year.toString();
45
+
46
+ // Get key parts of the title (for Harry Potter and the Prisoner of Azkaban -> ["harry", "potter", "prisoner", "azkaban"])
47
+ const titleParts = title
48
+ .replace(/[^a-z0-9\s]/g, '')
49
+ .split(/\s+/)
50
+ .filter(part => !['and', 'the', 'of'].includes(part));
51
+
52
+ console.log(`Looking for match with title parts:`, titleParts, `(${year})`);
53
+
54
+ // Score each video file
55
+ const scoredFiles = files.map(file => {
56
+ let score = 0;
57
+ const filename = file.name?.toLowerCase() || file.path?.toLowerCase() || '';
58
+
59
+ // Count matching title parts
60
+ const matchingParts = titleParts.filter(part => filename.includes(part));
61
+ score += (matchingParts.length / titleParts.length) * 100;
62
+
63
+ if (matchingParts.length > 0) {
64
+ console.log(`Found ${matchingParts.length}/${titleParts.length} title parts in "${filename}"`);
65
+ }
66
+
67
+ // Year matching
68
+ if (filename.includes(yearStr)) {
69
+ score += 50;
70
+ console.log(`Year match found in "${filename}"`);
71
+ }
72
+
73
+ // Penalize likely pack/collection files
74
+ if (filename.match(/\b(pack|collection|complete|season|episode|[es]\d{2,3})\b/i)) {
75
+ score -= 30;
76
+ console.log(`Pack/collection penalty applied to "${filename}"`);
77
+ }
78
+
79
+ // Penalize extras/bonus content
80
+ if (filename.match(/\b(extra|bonus|behind|feature|trailer|sample)\b/i)) {
81
+ score -= 40;
82
+ console.log(`Extras/bonus penalty applied to "${filename}"`);
83
+ }
84
+
85
+ return {
86
+ file,
87
+ score,
88
+ size: file.size || 0,
89
+ filename,
90
+ matchingParts
91
+ };
92
+ });
93
+
94
+ // Log all scores for debugging
95
+ console.log('\nFile scores:');
96
+ scoredFiles.forEach(({filename, score, size, matchingParts}) => {
97
+ console.log(`"${filename}": score=${score}, size=${(size / (1024 * 1024)).toFixed(2)}MB, matched=${matchingParts.join(', ')}`);
98
+ });
99
+
100
+ // Sort first by score, then by size
101
+ scoredFiles.sort((a, b) => {
102
+ // If scores are significantly different, use score
103
+ if (Math.abs(b.score - a.score) > 20) {
104
+ return b.score - a.score;
105
+ }
106
+ // If scores are close, use file size
107
+ return b.size - a.size;
108
+ });
109
+
110
+ const bestMatch = scoredFiles[0];
111
+
112
+ // If no good matches found (low score), fall back to largest file
113
+ if (bestMatch.score < 30) {
114
+ console.log('No confident match found, falling back to largest file');
115
+ const largestFile = files.sort((a, b) => (b.size || 0) - (a.size || 0))[0];
116
+ console.log(`Selected largest file: ${largestFile.name || largestFile.path}`);
117
+ return largestFile;
118
+ }
119
+
120
+ console.log(`\nSelected "${bestMatch.file.name || bestMatch.file.path}"`);
121
+ console.log(`Score: ${bestMatch.score}`);
122
+ console.log(`Size: ${(bestMatch.size / (1024 * 1024)).toFixed(2)}MB`);
123
+ console.log(`Matched parts: ${bestMatch.matchingParts.join(', ')}`);
124
+ return bestMatch.file;
125
+ }
src/yourbittorrent.js ADDED
@@ -0,0 +1,234 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import readTorrent from 'read-torrent';
2
+ import { promisify } from 'util';
3
+
4
+ const readTorrentPromise = promisify(readTorrent);
5
+
6
+ const SITE_CONFIG = {
7
+ baseUrl: 'https://yourbittorrent.com',
8
+ fallbackUrls: [
9
+ 'https://yourbittorrent2.com'
10
+ ]
11
+ };
12
+
13
+ async function getCinemetaMetadata(imdbId) {
14
+ try {
15
+ console.log(`\n🎬 Fetching Cinemeta data for ${imdbId}`);
16
+ const response = await fetch(`https://v3-cinemeta.strem.io/meta/movie/${imdbId}.json`);
17
+ if (!response.ok) throw new Error('Failed to fetch from Cinemeta');
18
+ const data = await response.json();
19
+ console.log('✅ Found:', data.meta.name);
20
+ return data;
21
+ } catch (error) {
22
+ console.error('❌ Cinemeta error:', error);
23
+ return null;
24
+ }
25
+ }
26
+
27
+ async function fetchWithTimeout(url, options = {}, timeout = 30000) {
28
+ const controller = new AbortController();
29
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
30
+
31
+ try {
32
+ const response = await fetch(url, {
33
+ ...options,
34
+ signal: controller.signal
35
+ });
36
+ clearTimeout(timeoutId);
37
+ return response;
38
+ } catch (error) {
39
+ clearTimeout(timeoutId);
40
+ throw error;
41
+ }
42
+ }
43
+
44
+ async function downloadAndParseTorrent(url) {
45
+ try {
46
+ console.log('Downloading torrent from:', url);
47
+
48
+ const options = {
49
+ headers: {
50
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
51
+ 'Accept': '*/*',
52
+ }
53
+ };
54
+
55
+ const torrentInfo = await readTorrentPromise(url, options);
56
+
57
+ // Check if any of the files are video files
58
+ const hasVideoFiles = torrentInfo.files?.some(file => {
59
+ const filePath = Array.isArray(file.path) ? file.path.join('/') : file.path;
60
+ return /\.(mp4|mkv|avi|mov|wmv)$/i.test(filePath);
61
+ });
62
+
63
+ if (!hasVideoFiles) {
64
+ console.log('No video files found');
65
+ return null;
66
+ }
67
+
68
+ // Find main video file
69
+ const videoFiles = torrentInfo.files.filter(file => {
70
+ const filePath = Array.isArray(file.path) ? file.path.join('/') : file.path;
71
+ return /\.(mp4|mkv|avi|mov|wmv)$/i.test(filePath);
72
+ }).sort((a, b) => b.length - a.length);
73
+
74
+ const mainFile = videoFiles[0];
75
+ const mainFilePath = Array.isArray(mainFile.path) ? mainFile.path.join('/') : mainFile.path;
76
+
77
+ const magnetUri = `magnet:?xt=urn:btih:${torrentInfo.infoHash}` +
78
+ `&dn=${encodeURIComponent(torrentInfo.name)}` +
79
+ (torrentInfo.announce ? torrentInfo.announce.map(tr => `&tr=${encodeURIComponent(tr)}`).join('') : '');
80
+
81
+ return {
82
+ magnetLink: magnetUri,
83
+ filename: mainFilePath,
84
+ torrentName: torrentInfo.name,
85
+ infoHash: torrentInfo.infoHash,
86
+ mainFileSize: mainFile.length
87
+ };
88
+
89
+ } catch (error) {
90
+ console.error('Error downloading/parsing torrent:', error);
91
+ return null;
92
+ }
93
+ }
94
+
95
+ async function searchTorrents(imdbId) {
96
+ console.log('\n🔄 Searching YourBittorrent for IMDB:', imdbId);
97
+
98
+ try {
99
+ const metadata = await getCinemetaMetadata(imdbId);
100
+ if (!metadata?.meta) {
101
+ throw new Error('Failed to get movie metadata');
102
+ }
103
+
104
+ const movieName = metadata.meta.name.replace(/[&]/g, '').trim();
105
+ const searchQuery = `${movieName} ${new Date(metadata.meta.released).getFullYear()}`;
106
+ console.log('Searching for:', searchQuery);
107
+
108
+ const formattedQuery = searchQuery
109
+ .replace(/[\\s]+/g, '-')
110
+ .toLowerCase();
111
+
112
+ const url = `${SITE_CONFIG.baseUrl}/?v=&c=movies&q=${encodeURIComponent(formattedQuery)}`;
113
+ console.log('Request URL:', url);
114
+
115
+ const response = await fetchWithTimeout(url, {
116
+ headers: {
117
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
118
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9',
119
+ 'Accept-Language': 'en-US,en;q=0.9'
120
+ }
121
+ });
122
+
123
+ if (!response.ok) {
124
+ throw new Error(`Search request failed: ${response.status}`);
125
+ }
126
+
127
+ const html = await response.text();
128
+ const results = parseSearchResults(html);
129
+ console.log(`Found ${results.length} raw results`);
130
+
131
+ // Process each result through read-torrent
132
+ const streams = await Promise.all(results.map(async result => {
133
+ try {
134
+ if (!result.magnetLink) return null;
135
+
136
+ const torrentInfo = await downloadAndParseTorrent(result.magnetLink);
137
+ if (!torrentInfo) return null;
138
+
139
+ return {
140
+ magnetLink: torrentInfo.magnetLink,
141
+ filename: torrentInfo.filename,
142
+ websiteTitle: result.title,
143
+ quality: extractQuality(result.title),
144
+ size: result.size,
145
+ source: 'YourBittorrent',
146
+ seeders: parseInt(result.seeders) || 0,
147
+ leechers: parseInt(result.leechers) || 0,
148
+ mainFileSize: torrentInfo.mainFileSize
149
+ };
150
+ } catch (error) {
151
+ console.error('Error processing result:', error);
152
+ return null;
153
+ }
154
+ }));
155
+
156
+ const validStreams = streams.filter(Boolean);
157
+ const movieYear = new Date(metadata.meta.released).getFullYear().toString();
158
+ const searchTerms = movieName.toLowerCase().split(' ');
159
+
160
+ const filteredStreams = validStreams.filter(stream => {
161
+ const streamTitle = stream.websiteTitle.toLowerCase();
162
+ const hasYear = streamTitle.includes(movieYear);
163
+ const hasAllTerms = searchTerms.every(term =>
164
+ streamTitle.includes(term.toLowerCase())
165
+ );
166
+ return hasYear && hasAllTerms;
167
+ });
168
+
169
+ console.log(`Found ${filteredStreams.length} relevant streams after filtering`);
170
+
171
+ filteredStreams.sort((a, b) => {
172
+ const qualityOrder = { '2160p': 4, '4k': 4, '1080p': 3, '720p': 2 };
173
+ const qualityDiff = (qualityOrder[b.quality] || 0) - (qualityOrder[a.quality] || 0);
174
+ if (qualityDiff === 0) {
175
+ return b.seeders - a.seeders;
176
+ }
177
+ return qualityDiff;
178
+ });
179
+
180
+ return filteredStreams;
181
+
182
+ } catch (error) {
183
+ console.error('❌ Error searching YourBittorrent:', error);
184
+
185
+ for (const fallbackUrl of SITE_CONFIG.fallbackUrls) {
186
+ try {
187
+ SITE_CONFIG.baseUrl = fallbackUrl;
188
+ return await searchTorrents(imdbId);
189
+ } catch (fallbackError) {
190
+ console.error(`Fallback ${fallbackUrl} also failed:`, fallbackError);
191
+ }
192
+ }
193
+
194
+ return [];
195
+ }
196
+ }
197
+
198
+ function parseSearchResults(html) {
199
+ const results = [];
200
+ const rows = html.match(/<tr class="table-default">[\s\S]*?<\/tr>/g) || [];
201
+
202
+ for (const row of rows) {
203
+ try {
204
+ const titleMatch = row.match(/href="\/torrent\/.*?">(.*?)<\/a>/);
205
+ const sizeMatch = row.match(/<td.*?>\s*([\d.]+\s*[KMGT]B)\s*<\/td>/);
206
+ const seedersMatch = row.match(/<td.*?>\s*(\d+)\s*<\/td>/);
207
+ const leechersMatch = row.match(/<td.*?>\s*(\d+)\s*<\/td>/);
208
+
209
+ if (titleMatch) {
210
+ const downloadId = row.match(/\/torrent\/(\d+)\//)?.[1];
211
+ results.push({
212
+ title: titleMatch[1],
213
+ size: sizeMatch?.[1] || 'Unknown',
214
+ seeders: seedersMatch?.[1] || '0',
215
+ leechers: leechersMatch?.[1] || '0',
216
+ magnetLink: downloadId ?
217
+ `${SITE_CONFIG.baseUrl}/down/${downloadId}.torrent` :
218
+ null
219
+ });
220
+ }
221
+ } catch (error) {
222
+ console.error('Error parsing result row:', error);
223
+ }
224
+ }
225
+
226
+ return results;
227
+ }
228
+
229
+ function extractQuality(title) {
230
+ const qualityMatch = title.match(/\b(2160p|1080p|720p|4k|uhd)\b/i);
231
+ return qualityMatch ? qualityMatch[1].toLowerCase() : '';
232
+ }
233
+
234
+ export { searchTorrents };