no1b4me commited on
Commit
8740315
·
verified ·
1 Parent(s): 984b721

Upload 12 files

Browse files
Files changed (9) hide show
  1. index.js +380 -173
  2. public/index.html +7 -18
  3. src/1337x.js +260 -0
  4. src/aither.js +97 -24
  5. src/debrids.js +61 -34
  6. src/eztv.js +258 -0
  7. src/tday.js +119 -35
  8. src/util.js +51 -13
  9. src/yourbittorrent.js +135 -52
index.js CHANGED
@@ -10,6 +10,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();
@@ -25,10 +28,77 @@ app.use(cors({
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);
@@ -38,22 +108,27 @@ async function getCinemetaMetadata(imdbId) {
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') {
@@ -61,30 +136,110 @@ async function readMovieData(imdbId, 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
 
@@ -92,82 +247,52 @@ async function getAllStreams(imdbId) {
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 () => {
@@ -176,60 +301,82 @@ async function mergeAndSaveStreams(existingStreams = [], newStreams = [], imdbId
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`);
@@ -245,20 +392,7 @@ async function mergeAndSaveStreams(existingStreams = [], newStreams = [], imdbId
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 {
@@ -270,28 +404,55 @@ app.get('/:apiKeys/stream/:type/:id.json', async (req, res) => {
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
  });
@@ -299,55 +460,110 @@ app.get('/:apiKeys/stream/:type/:id.json', async (req, res) => {
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
 
@@ -355,55 +571,37 @@ app.get('/:apiKeys/stream/:type/:id.json', async (req, res) => {
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: [] });
@@ -421,12 +619,19 @@ app.get('/:apiKeys/:magnetLink', async (req, res) => {
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) {
@@ -448,5 +653,7 @@ app.use((err, req, res, next) => {
448
  res.status(500).json({ error: 'Internal server error', details: err.message });
449
  });
450
 
451
- const port = process.env.PORT || 3000;
452
- app.listen(port, () => console.log(`\n🚀 Addon running at http://localhost:${port}`));
 
 
 
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
+ import { searchTorrents as searchYBTTorrents } from './src/yourbittorrent.js';
14
+ import { searchTorrents as searchEZTVTorrents } from './src/eztv.js';
15
+ import { searchTorrents as search1337xTorrents } from './src/1337x.js';
16
 
17
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
  const app = express();
 
28
  app.use(express.static(path.join(__dirname, 'public')));
29
  app.options('*', cors());
30
 
31
+ // Configure endpoint that redirects to index.html
32
+ app.get('/configure', (req, res) => {
33
+ res.redirect('/');
34
+ });
35
+
36
+ // Basic manifest endpoint
37
+ app.get('/manifest.json', (req, res) => {
38
+ res.json({
39
+ id: 'org.community.premiumize',
40
+ version: '1.5.0',
41
+ name: 'premiumize',
42
+ logo:'https://dl.strem.io/addon-logo.png',
43
+ description: 'Stream movies and series via premiumize',
44
+ resources: ['stream'],
45
+ types: ['movie', 'series'],
46
+ catalogs: [],
47
+ behaviorHints: {
48
+ configurable: true,
49
+ configurationRequired: true
50
+ },
51
+ idPrefixes: ['tt']
52
+ });
53
+ });
54
+
55
+ // API-based manifest endpoint
56
+ app.get('/:apiKeys/manifest.json', (req, res) => {
57
+ res.json({
58
+ id: 'org.community.community.premiumize',
59
+ version: '1.5.0',
60
+ name: 'premiumize',
61
+ logo:'https://dl.strem.io/addon-logo.png',
62
+ description: 'Stream movies and series via premiumize',
63
+ resources: ['stream'],
64
+ types: ['movie', 'series'],
65
+ catalogs: [],
66
+ idPrefixes: ['tt']
67
+ });
68
+ });
69
+
70
+ function parseSize(sizeStr) {
71
+ if (!sizeStr) return 0;
72
+ const match = sizeStr.match(/(\d+(\.\d+)?)\s*(GB|MB|TB)/i);
73
+ if (!match) return 0;
74
+
75
+ const [, value, , unit] = match;
76
+ const size = parseFloat(value);
77
+ switch (unit.toUpperCase()) {
78
+ case 'TB': return size * 1024 * 1024;
79
+ case 'GB': return size * 1024;
80
+ case 'MB': return size;
81
+ default: return 0;
82
+ }
83
+ }
84
+
85
+ function getQualityValue(name) {
86
+ const quality = name.match(/\b(4k|2160p|1080p|720p|480p)\b/i)?.[1]?.toLowerCase();
87
+ switch (quality) {
88
+ case '4k':
89
+ case '2160p': return 4;
90
+ case '1080p': return 3;
91
+ case '720p': return 2;
92
+ case '480p': return 1;
93
+ default: return 0;
94
+ }
95
+ }
96
+
97
+ async function getCinemetaMetadata(type, id) {
98
  try {
99
+ const cleanId = id.split(':')[0];
100
+ console.log(`\n🎬 Fetching Cinemeta data for ${type} ${cleanId}`);
101
+ const response = await fetch(`https://v3-cinemeta.strem.io/meta/${type}/${cleanId}.json`);
102
  if (!response.ok) throw new Error('Failed to fetch from Cinemeta');
103
  const data = await response.json();
104
  console.log('✅ Found:', data.meta.name);
 
108
  return null;
109
  }
110
  }
111
+ async function readData(type, id, year) {
112
+ const lockKey = `${type}-${year}`;
113
+ const folder = type === 'movie' ? 'movies' : 'series';
114
+ const yearFile = path.join(__dirname, folder, `${year}.json`);
115
 
116
  try {
117
  return await lock.acquire(lockKey, async () => {
118
+ console.log(`\n📂 Reading data for ${type} year ${year}`);
119
  const content = await fs.readFile(yearFile, 'utf8');
120
+ const items = JSON.parse(content);
121
+ const item = items.find(m => m.id === id);
122
+ if (item) {
123
+ console.log(`✅ Found ${type}: ${item.originalTitle}`);
124
+ if (type === 'movie') {
125
+ console.log(`Found ${item.streams.length} streams`);
126
+ } else {
127
+ const episodeCount = Object.keys(item.episodes || {}).length;
128
+ console.log(`Found ${episodeCount} episodes with streams`);
129
+ }
130
  }
131
+ return item;
132
  });
133
  } catch (error) {
134
  if (error.name === 'AsyncLockTimeout') {
 
136
  return null;
137
  }
138
  if (error.code !== 'ENOENT') {
139
+ console.error(`❌ Error reading data:`, error);
140
  }
141
  return null;
142
  }
143
  }
144
 
145
+ async function checkCacheStatuses(service, hashes, streams) {
146
+ if (!hashes?.length) {
147
+ console.log('No hashes to check');
148
+ return {};
149
+ }
150
+
151
+ try {
152
+ console.log(`\n🔍 Checking cache status for ${hashes.length} hashes with ${service.constructor.name}`);
153
+ console.log('Sample hashes:', hashes.slice(0, 3));
154
+
155
+ const startTime = Date.now();
156
+ const results = await service.checkCacheStatuses(hashes);
157
+ console.log(`Cache check completed in ${Date.now() - startTime}ms`);
158
+
159
+ const cachedCount = Object.values(results).filter(r => r.cached).length;
160
+ console.log(`Cache check results: ${cachedCount} cached out of ${hashes.length} total`);
161
+
162
+ if (streams && streams.length > 0) {
163
+ const uncachedStream = streams.find(stream => {
164
+ const hash = extractInfoHash(stream.magnetLink);
165
+ return hash && !results[hash]?.cached;
166
+ });
167
+
168
+ if (uncachedStream) {
169
+ console.log(`\n🔄 Adding uncached magnet to ${service.constructor.name} for future availability`);
170
+ try {
171
+ if (service.constructor.name === 'Premiumize') {
172
+ // Use transfer/create for Premiumize
173
+ const body = new FormData();
174
+ body.append('src', uncachedStream.magnetLink);
175
+ await service.makeRequest('POST', '/transfer/create', {
176
+ body
177
+ });
178
+ console.log('Transfer created in Premiumize');
179
+ } else {
180
+ // For other services, use existing getStreamUrl
181
+ await service.getStreamUrl(uncachedStream.magnetLink).catch(err => {
182
+ console.log('Background caching initiated');
183
+ });
184
+ }
185
+ } catch (error) {
186
+ console.log('Background caching attempt made');
187
+ }
188
+ }
189
+ }
190
+
191
+ return results;
192
+ } catch (error) {
193
+ console.error('❌ Cache check error:', error);
194
+ return {};
195
+ }
196
+ }
197
+
198
+ async function getAllStreams(type, id, season, episode) {
199
  try {
200
  console.log('\n🔄 Fetching all available streams');
201
+ const cleanId = id.split(':')[0];
202
+
203
+ const metadata = await getCinemetaMetadata(type, cleanId).catch(err => {
204
+ console.log('Continuing without Cinemeta metadata');
205
+ return null;
206
+ });
207
+
208
+ let searchQuery;
209
+ if (type === 'series') {
210
+ const showTitle = metadata?.meta?.name || cleanId;
211
+ searchQuery = `${showTitle} S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`;
212
+ } else {
213
+ searchQuery = metadata?.meta?.name || cleanId;
214
+ }
215
+
216
+ console.log('Search query:', searchQuery);
217
 
218
  const startTime = Date.now();
219
+ const [iptStreams, tdayStreams, torrentingStreams, ybtStreams, eztvStreams, l337xStreams] = await Promise.all([
220
+ fetchIPTFeeds(searchQuery, type).catch(err => {
221
  console.error('IPTorrents fetch failed:', err);
222
  return [];
223
  }),
224
+ fetchTDayFeeds(searchQuery, type).catch(err => {
225
  console.error('TorrentDay fetch failed:', err);
226
  return [];
227
  }),
228
+ fetchTorrentingFeeds(searchQuery, type).catch(err => {
229
  console.error('Torrenting fetch failed:', err);
230
  return [];
231
+ }),
232
+ searchYBTTorrents(searchQuery, type).catch(err => {
233
+ console.error('YourBittorrent fetch failed:', err);
234
+ return [];
235
+ }),
236
+ type === 'series' ? searchEZTVTorrents(searchQuery, type).catch(err => {
237
+ console.error('EZTV fetch failed:', err);
238
+ return [];
239
+ }) : Promise.resolve([]),
240
+ search1337xTorrents(searchQuery, type).catch(err => {
241
+ console.error('1337x fetch failed:', err);
242
+ return [];
243
  })
244
  ]);
245
 
 
247
  console.log('IPTorrents:', iptStreams.length, 'streams');
248
  console.log('TorrentDay:', tdayStreams.length, 'streams');
249
  console.log('Torrenting:', torrentingStreams.length, 'streams');
250
+ console.log('YourBittorrent:', ybtStreams.length, 'streams');
251
+ console.log('EZTV:', eztvStreams.length, 'streams');
252
+ console.log('1337x:', l337xStreams.length, 'streams');
253
 
254
  const allStreams = [
255
  ...iptStreams,
256
  ...tdayStreams,
257
+ ...torrentingStreams,
258
+ ...ybtStreams,
259
+ ...eztvStreams,
260
+ ...l337xStreams
261
  ];
262
 
263
  console.log('\nPre-deduplication total:', allStreams.length, 'streams');
264
 
 
265
  const uniqueStreams = Array.from(
266
  new Map(
267
  allStreams
268
+ .filter(stream => stream && stream.magnetLink)
269
  .map(stream => {
270
  const hash = extractInfoHash(stream.magnetLink);
271
+ if (!hash) return null;
272
  return [hash, stream];
273
  })
274
+ .filter(Boolean)
275
  ).values()
276
  );
277
 
278
  console.log('Post-deduplication total:', uniqueStreams.length, 'streams');
279
 
 
 
 
 
 
 
 
 
 
 
 
280
  return uniqueStreams;
281
  } catch (error) {
282
  console.error('❌ Error fetching streams:', error);
283
  return [];
284
  }
285
  }
286
+ async function mergeAndSaveStreams(type, existingStreams = [], newStreams = [], id, year, title = '', season = null, episode = null) {
287
+ // First check if we already have 100+ streams
288
+ if (existingStreams.length >= 100) {
289
+ console.log(`\n📝 Skipping merge - already have ${existingStreams.length} streams in database`);
290
+ return existingStreams;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
291
  }
 
292
 
293
+ const lockKey = `${type}-${year}`;
294
+ const folder = type === 'movie' ? 'movies' : 'series';
295
+ const cleanId = id.split(':')[0];
296
 
297
  try {
298
  return await lock.acquire(lockKey, async () => {
 
301
  return existingStreams;
302
  }
303
 
304
+ console.log(`\n🔄 Merging streams for ${title}`);
305
  console.log('Existing streams:', existingStreams.length);
306
  console.log('New streams:', newStreams.length);
307
 
308
  const existingHashes = new Set(
309
+ existingStreams
310
+ .filter(stream => stream && stream.magnetLink)
311
+ .map(stream => extractInfoHash(stream.magnetLink))
312
+ .filter(Boolean)
313
  );
314
 
315
+ const uniqueNewStreams = newStreams
316
+ .filter(stream => stream && stream.magnetLink)
317
+ .filter(stream => {
318
+ const hash = extractInfoHash(stream.magnetLink);
319
+ return hash && !existingHashes.has(hash);
320
+ });
321
 
322
  if (!uniqueNewStreams.length) {
323
  console.log('No unique new streams found');
324
  return existingStreams;
325
  }
326
 
 
 
327
  const mergedStreams = [...existingStreams, ...uniqueNewStreams];
328
+ const yearFile = path.join(__dirname, folder, `${year}.json`);
329
 
330
+ let items = [];
331
  try {
332
  const content = await fs.readFile(yearFile, 'utf8');
333
+ items = JSON.parse(content);
334
+ console.log(`Read existing ${year}.json with ${items.length} items`);
335
  } catch (error) {
336
  console.log(`Creating new ${year}.json file`);
337
  }
338
 
339
+ const itemIndex = items.findIndex(m => m.id === cleanId);
340
+ if (itemIndex >= 0) {
341
+ console.log('Updating existing entry');
342
+ if (type === 'series') {
343
+ items[itemIndex].episodes = items[itemIndex].episodes || {};
344
+ items[itemIndex].episodes[`${season}x${episode}`] = {
345
+ streams: mergedStreams,
346
+ lastUpdated: new Date().toISOString()
347
+ };
348
+ } else {
349
+ items[itemIndex].streams = mergedStreams;
350
+ items[itemIndex].lastUpdated = new Date().toISOString();
351
+ }
352
  } else {
353
+ console.log('Adding new entry');
354
+ const newItem = {
355
+ id: cleanId,
356
+ originalTitle: title,
 
357
  addedAt: new Date().toISOString(),
358
  lastUpdated: new Date().toISOString()
359
+ };
360
+
361
+ if (type === 'series') {
362
+ newItem.episodes = {
363
+ [`${season}x${episode}`]: {
364
+ streams: mergedStreams,
365
+ lastUpdated: new Date().toISOString()
366
+ }
367
+ };
368
+ } else {
369
+ newItem.streams = mergedStreams;
370
+ }
371
+
372
+ items.push(newItem);
373
  }
374
 
375
+ await fs.mkdir(path.join(__dirname, folder), { recursive: true });
376
 
377
+ // Use a temporary file for atomic write
378
  const tempFile = `${yearFile}.tmp`;
379
+ await fs.writeFile(tempFile, JSON.stringify(items, null, 2));
380
  await fs.rename(tempFile, yearFile);
381
 
382
  console.log(`✅ Added ${uniqueNewStreams.length} new streams to ${year}.json`);
 
392
  }
393
  }
394
 
395
+ app.get('/:apiKeys/stream/:type/:id/:extra?.json', async (req, res) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
396
  const { apiKeys, type, id } = req.params;
397
 
398
  try {
 
404
  throw new Error('No valid debrid service configured');
405
  }
406
 
407
+ let realId = id;
408
+ let season, episode;
409
+ if (type === 'series') {
410
+ const parts = id.split(':');
411
+ if (parts.length === 3) {
412
+ [realId, season, episode] = parts;
413
+ season = parseInt(season);
414
+ episode = parseInt(episode);
415
+ console.log('Series request:', { realId, season, episode });
416
+ } else {
417
+ throw new Error('Invalid series ID format');
418
+ }
419
+ }
420
 
421
+ let year;
422
+ let metadata;
423
+ try {
424
+ metadata = await getCinemetaMetadata(type, realId);
425
+ year = metadata?.meta ? new Date(metadata.meta.released).getFullYear() : new Date().getFullYear();
426
+ } catch (error) {
427
+ console.log('Metadata fetch failed, using current year as fallback');
428
+ year = new Date().getFullYear();
429
+ }
430
+ console.log('Year:', year);
431
 
432
+ const itemData = await readData(type, realId, year);
433
+ let localStreams = [];
434
+
435
+ if (type === 'series') {
436
+ localStreams = itemData?.episodes?.[`${season}x${episode}`]?.streams || [];
437
+ } else {
438
+ localStreams = itemData?.streams || [];
439
+ }
440
 
 
441
  console.log(`Found ${localStreams.length} streams in cache`);
442
 
 
 
443
  if (localStreams.length > 0) {
444
  console.log('\n🔍 Processing cached streams');
445
+ const hashes = localStreams
446
+ .filter(stream => stream && stream.magnetLink)
447
+ .map(stream => extractInfoHash(stream.magnetLink))
448
+ .filter(Boolean);
449
+
450
  console.log(`Checking ${hashes.length} hashes for cached streams`);
451
 
452
  const cacheResults = {};
453
  for (const service of debridServices) {
454
  console.log(`\nChecking cache with ${service.constructor.name}`);
455
+ const results = await checkCacheStatuses(service, hashes, localStreams);
456
  Object.entries(results).forEach(([hash, info]) => {
457
  if (info.cached) cacheResults[hash] = info;
458
  });
 
460
 
461
  console.log(`Found ${Object.keys(cacheResults).length} cached streams`);
462
 
463
+ const processedStreams = localStreams
464
+ .filter(stream => stream && stream.magnetLink)
465
  .map(stream => {
466
  const hash = extractInfoHash(stream.magnetLink);
467
+ if (!hash) return null;
468
+
469
  const cacheInfo = cacheResults[hash];
470
  if (!cacheInfo?.cached) return null;
471
 
472
+ const quality = stream.quality || stream.websiteTitle?.match(/\d{3,4}p|4k|HDTS|CAM/i)?.[0] || '';
473
+ const size = stream.size || stream.websiteTitle?.match(/\d+(\.\d+)?\s*(GB|MB)/i)?.[0] || '';
474
 
475
  return {
476
  name: ['🧲', quality, size, `⚡️ ${cacheInfo.service}`, `[${stream.source}]`]
477
  .filter(Boolean)
478
  .join(' | '),
479
+ title: stream.filename || stream.websiteTitle,
480
  url: `${req.protocol}://${req.get('host')}/${apiKeys}/${base64Encode(stream.magnetLink)}`,
481
  service: cacheInfo.service
482
  };
483
  })
484
  .filter(Boolean);
 
485
 
486
+ processedStreams.sort((a, b) => {
487
+ const qualityDiff = getQualityValue(b.name) - getQualityValue(a.name);
488
+ if (qualityDiff !== 0) return qualityDiff;
489
+
490
+ const sizeA = parseSize(a.name.match(/\|\s*([\d.]+\s*[KMGT]B)/i)?.[1]);
491
+ const sizeB = parseSize(b.name.match(/\|\s*([\d.]+\s*[KMGT]B)/i)?.[1]);
492
+
493
+ return sizeB - sizeA;
494
+ });
495
+
496
+ console.log(`\n✅ Sending ${processedStreams.length} cached streams`);
497
+ if (processedStreams.length > 0) {
498
+ console.log('Top 3 streams:');
499
+ processedStreams.slice(0, 3).forEach((stream, index) => {
500
+ console.log(`${index + 1}. ${stream.name}`);
501
+ });
502
+ }
503
+
504
+ res.json({ streams: processedStreams });
505
+
506
+ if (hashes.length < 100) {
507
+ console.log('\n🔄 Starting background stream update (less than 100 hashes in database)');
508
+ getAllStreams(type, id, season, episode).then(async newStreams => {
509
+ if (newStreams.length > 0) {
510
+ console.log(`Found ${newStreams.length} new streams in background update`);
511
+ await mergeAndSaveStreams(
512
+ type,
513
+ localStreams,
514
+ newStreams,
515
+ id,
516
+ year,
517
+ metadata?.meta?.name || id,
518
+ season,
519
+ episode
520
+ );
521
+ }
522
+ }).catch(error => {
523
+ console.error('Background update error:', error);
524
+ });
525
+ } else {
526
+ console.log('\n📝 Skipping background update - already have 100+ hashes');
527
+ }
528
+
529
+ } else {
530
  console.log('\n🔄 No cached streams available, fetching new streams...');
531
+ const newStreams = await getAllStreams(type, id, season, episode);
532
 
533
  if (newStreams.length > 0) {
534
  await mergeAndSaveStreams(
535
+ type,
536
  [],
537
  newStreams,
538
  id,
539
  year,
540
+ metadata?.meta?.name || id,
541
+ season,
542
+ episode
543
  );
544
 
545
+ const hashes = newStreams
546
+ .filter(stream => stream && stream.magnetLink)
547
+ .map(stream => extractInfoHash(stream.magnetLink))
548
+ .filter(Boolean);
549
+
550
  console.log(`Checking ${hashes.length} hashes for new streams`);
551
 
552
  const cacheResults = {};
553
  for (const service of debridServices) {
554
  console.log(`\nChecking cache with ${service.constructor.name}`);
555
+ const results = await checkCacheStatuses(service, hashes, newStreams);
556
  Object.entries(results).forEach(([hash, info]) => {
557
  if (info.cached) cacheResults[hash] = info;
558
  });
559
  }
560
 
561
+ const processedStreams = newStreams
562
+ .filter(stream => stream && stream.magnetLink)
563
  .map(stream => {
564
  const hash = extractInfoHash(stream.magnetLink);
565
+ if (!hash) return null;
566
+
567
  const cacheInfo = cacheResults[hash];
568
  if (!cacheInfo?.cached) return null;
569
 
 
571
  name: ['🧲', stream.quality, stream.size, `⚡️ ${cacheInfo.service}`, `[${stream.source}]`]
572
  .filter(Boolean)
573
  .join(' | '),
574
+ title: stream.filename || stream.websiteTitle,
575
  url: `${req.protocol}://${req.get('host')}/${apiKeys}/${base64Encode(stream.magnetLink)}`,
576
  service: cacheInfo.service
577
  };
578
  })
579
  .filter(Boolean);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
580
 
581
+ processedStreams.sort((a, b) => {
582
+ const qualityDiff = getQualityValue(b.name) - getQualityValue(a.name);
583
+ if (qualityDiff !== 0) return qualityDiff;
 
 
 
 
 
 
 
 
 
584
 
585
+ const sizeA = parseSize(a.name.match(/\|\s*([\d.]+\s*[KMGT]B)/i)?.[1]);
586
+ const sizeB = parseSize(b.name.match(/\|\s*([\d.]+\s*[KMGT]B)/i)?.[1]);
587
+
588
+ return sizeB - sizeA;
589
+ });
 
 
 
590
 
591
+ console.log(`\n✅ Sending ${processedStreams.length} fresh streams`);
592
+ if (processedStreams.length > 0) {
593
+ console.log('Top 3 streams:');
594
+ processedStreams.slice(0, 3).forEach((stream, index) => {
595
+ console.log(`${index + 1}. ${stream.name}`);
596
+ });
597
+ }
598
 
599
+ res.json({ streams: processedStreams });
600
+ } else {
601
+ console.log('No streams found');
602
+ res.json({ streams: [] });
603
+ }
604
+ }
605
  } catch (error) {
606
  console.error('❌ Error processing streams:', error);
607
  res.json({ streams: [] });
 
619
 
620
  console.log('\n🧲 Processing magnet request');
621
  const decodedMagnet = base64Decode(magnetLink);
622
+ if (!decodedMagnet) {
623
+ throw new Error('Invalid magnet link');
624
+ }
625
  console.log('Decoded magnet link:', decodedMagnet.substring(0, 100) + '...');
626
 
627
  for (const service of debridServices) {
628
  try {
629
  console.log(`\nTrying ${service.constructor.name}`);
630
  const streamUrl = await service.getStreamUrl(decodedMagnet);
631
+ if (!streamUrl) {
632
+ console.error(`No stream URL returned from ${service.constructor.name}`);
633
+ continue;
634
+ }
635
  console.log('Stream URL generated:', streamUrl.substring(0, 100) + '...');
636
  return res.redirect(streamUrl);
637
  } catch (error) {
 
653
  res.status(500).json({ error: 'Internal server error', details: err.message });
654
  });
655
 
656
+ const port = process.env.PORT || 9518;
657
+ app.listen(port, () => console.log(`\n🚀 Addon running at http://localhost:${port}`));
658
+
659
+ export default app;
public/index.html CHANGED
@@ -3,7 +3,7 @@
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
  * {
@@ -143,22 +143,17 @@
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>
@@ -166,11 +161,10 @@
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
 
@@ -179,16 +173,11 @@
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';
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Premiumize</title>
7
  <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700&display=swap" rel="stylesheet">
8
  <style>
9
  * {
 
143
  </head>
144
  <body>
145
  <div class="container">
146
+ <div class="logo">Premiumize</div>
147
  <div class="content-wrapper">
148
  <h1>Stremio Addon Installation</h1>
149
  <div class="input-container">
150
+ <input type="text" id="pr-key" class="input" placeholder=" " required>
 
 
 
 
151
  <label for="pr-key" class="input-label">Premiumize API Key</label>
152
  </div>
153
  <button class="btn" onclick="generateAddonUrl()">GENERATE</button>
154
  <div id="addon-url" class="addon-url"></div>
155
  <div class="info">
156
  <h3>How to get API Keys:</h3>
 
157
  <p><strong>Premiumize:</strong> Visit <a href="https://www.premiumize.me/account" target="_blank">https://www.premiumize.me/account</a></p>
158
  </div>
159
  </div>
 
161
 
162
  <script>
163
  function generateAddonUrl() {
 
164
  const prKey = document.getElementById('pr-key').value.trim();
165
 
166
+ if (!prKey) {
167
+ alert('Please enter the API key');
168
  return;
169
  }
170
 
 
173
  const hostname = window.location.host;
174
  const baseUrl = `${protocol}//${hostname}`;
175
 
 
 
 
 
 
176
  // Create the addon URL
177
+ const addonUrl = `${baseUrl}/pr=${prKey}/manifest.json`;
178
 
179
  // Always use stremio:// protocol for the Stremio URL, regardless of original protocol
180
+ const stremioUrl = `stremio://${hostname}/pr=${prKey}/manifest.json`;
181
 
182
  const urlDiv = document.getElementById('addon-url');
183
  urlDiv.style.display = 'block';
src/1337x.js ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import parseTorrent from 'parse-torrent';
2
+
3
+ const SITE_CONFIG = {
4
+ baseUrl: 'https://1337x.to',
5
+ fallbackUrls: [
6
+ 'https://1337x.st',
7
+ 'https://x1337x.ws',
8
+ 'https://x1337x.eu',
9
+ 'https://x1337x.se',
10
+ 'https://x1337x.cc'
11
+ ]
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 host = new URL(url).host;
20
+ const response = await fetch(url, {
21
+ ...options,
22
+ headers: {
23
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
24
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9',
25
+ 'Accept-Language': 'en-US,en;q=0.9',
26
+ 'Host': host,
27
+ ...options.headers
28
+ },
29
+ signal: controller.signal
30
+ });
31
+ clearTimeout(timeoutId);
32
+ return response;
33
+ } catch (error) {
34
+ clearTimeout(timeoutId);
35
+ throw error;
36
+ }
37
+ }
38
+
39
+ function extractQuality(title) {
40
+ const qualityMatch = title.match(/\b(2160p|1080p|720p|4k|uhd)\b/i);
41
+ return qualityMatch ? qualityMatch[1].toLowerCase() : '';
42
+ }
43
+
44
+ function formatSize(sizeStr) {
45
+ if (!sizeStr) return 'Unknown';
46
+ return sizeStr.replace(/\s+/g, '').toUpperCase();
47
+ }
48
+
49
+ function parseSearchResults(html) {
50
+ const results = [];
51
+ const rows = html.match(/<tr>\s*<td class="coll-1 name">[\s\S]*?<\/tr>/g) || [];
52
+
53
+ for (const row of rows) {
54
+ try {
55
+ // Skip rows without torrent links
56
+ if (!row.includes('href="/torrent/')) continue;
57
+
58
+ // Extract title
59
+ const titleMatch = row.match(/href="\/torrent\/\d+\/([^"]+)"/);
60
+ if (!titleMatch) continue;
61
+ const title = decodeURIComponent(titleMatch[1].replace(/\-/g, ' '));
62
+
63
+ // Extract torrent path
64
+ const pathMatch = row.match(/href="(\/torrent\/\d+\/[^"]+)"/);
65
+ const detailsPath = pathMatch ? pathMatch[1] : null;
66
+
67
+ // Extract size
68
+ const sizeMatch = row.match(/<td class="coll-4[^"]*">([^<]+)<span/);
69
+ const size = sizeMatch ? sizeMatch[1].trim() : 'Unknown';
70
+
71
+ // Extract seeders and leechers
72
+ const seedersMatch = row.match(/<td class="coll-2 seeds">(\d+)<\/td>/);
73
+ const leechersMatch = row.match(/<td class="coll-3 leeches">(\d+)<\/td>/);
74
+ const seeders = seedersMatch ? parseInt(seedersMatch[1]) : 0;
75
+ const leechers = leechersMatch ? parseInt(leechersMatch[1]) : 0;
76
+
77
+ // Extract date
78
+ const dateMatch = row.match(/<td class="coll-date">([^<]+)<\/td>/);
79
+ const uploadDate = dateMatch ? dateMatch[1].trim() : '';
80
+
81
+ if (title && detailsPath) {
82
+ results.push({
83
+ title,
84
+ detailsPath,
85
+ size,
86
+ seeders,
87
+ leechers,
88
+ uploadDate
89
+ });
90
+ }
91
+ } catch (error) {
92
+ console.error('Error parsing row:', error);
93
+ }
94
+ }
95
+
96
+ return results;
97
+ }
98
+
99
+ async function getMagnetLink(detailsPath) {
100
+ try {
101
+ const url = `${SITE_CONFIG.baseUrl}${detailsPath}`;
102
+ console.log('Fetching details:', url);
103
+
104
+ const response = await fetchWithTimeout(url);
105
+ if (!response.ok) throw new Error(`Failed to fetch details: ${response.status}`);
106
+
107
+ const html = await response.text();
108
+
109
+ // Get magnet link
110
+ const magnetMatch = html.match(/href="(magnet:\?xt=urn:btih:[^"]+)"/);
111
+ if (!magnetMatch) {
112
+ console.log('No magnet link found in details page');
113
+ return null;
114
+ }
115
+
116
+ const magnetLink = magnetMatch[1];
117
+
118
+ // Parse the magnet link
119
+ const parsed = await parseTorrent(magnetLink);
120
+ if (!parsed.files) {
121
+ console.log('No file list in magnet link');
122
+ return magnetLink; // Return magnet anyway as some valid magnets might not have file list
123
+ }
124
+
125
+ // Check files
126
+ const hasRarFiles = parsed.files.some(file =>
127
+ /\.rar$|\.r\d+$|part\d+\.rar$/i.test(file.name)
128
+ );
129
+
130
+ if (hasRarFiles) {
131
+ console.log('Skipping magnet containing RAR files');
132
+ return null;
133
+ }
134
+
135
+ const videoFiles = parsed.files.filter(file =>
136
+ /\.(mkv|mp4|avi|mov|wmv|m4v|ts)$/i.test(file.name)
137
+ );
138
+
139
+ if (videoFiles.length === 0) {
140
+ console.log('No video files found in magnet');
141
+ return null;
142
+ }
143
+
144
+ console.log('Found video files:', videoFiles.map(f => f.name));
145
+ return magnetLink;
146
+
147
+ } catch (error) {
148
+ console.error('Error getting magnet link:', error);
149
+ return null;
150
+ }
151
+ }
152
+
153
+ function isCorrectEpisode(title, searchQuery) {
154
+ if (!title || !searchQuery) return false;
155
+
156
+ const queryMatch = searchQuery.match(/S(\d{2})E(\d{2})/i);
157
+ if (!queryMatch) return true; // If no episode info in search, accept all results
158
+
159
+ const season = queryMatch[1];
160
+ const episode = queryMatch[2];
161
+
162
+ // Common episode patterns
163
+ const patterns = [
164
+ new RegExp(`S${season}E${episode}`, 'i'),
165
+ new RegExp(`${season}x${episode}`, 'i'),
166
+ new RegExp(`Season.?${season}.?Episode.?${episode}`, 'i')
167
+ ];
168
+
169
+ return patterns.some(pattern => pattern.test(title));
170
+ }
171
+
172
+ async function searchTorrents(searchQuery, type = 'movie') {
173
+ console.log('\n🔄 Searching 1337x for:', searchQuery);
174
+
175
+ try {
176
+ let searchPath;
177
+ if (type === 'series') {
178
+ searchPath = `/category-search/${encodeURIComponent(searchQuery)}/TV/1/`;
179
+ } else {
180
+ searchPath = `/category-search/${encodeURIComponent(searchQuery)}/Movies/1/`;
181
+ }
182
+
183
+ const url = `${SITE_CONFIG.baseUrl}${searchPath}`;
184
+ console.log('Request URL:', url);
185
+
186
+ const response = await fetchWithTimeout(url);
187
+ if (!response.ok) throw new Error(`Search request failed: ${response.status}`);
188
+
189
+ const html = await response.text();
190
+ const results = parseSearchResults(html);
191
+ console.log(`Found ${results.length} initial results`);
192
+
193
+ // Filter episode matches for TV series
194
+ const filteredResults = type === 'series'
195
+ ? results.filter(r => isCorrectEpisode(r.title, searchQuery))
196
+ : results;
197
+
198
+ console.log(`Filtered to ${filteredResults.length} matching results`);
199
+
200
+ // Process each result to get magnet links
201
+ const streams = await Promise.all(filteredResults.map(async result => {
202
+ try {
203
+ const magnetLink = await getMagnetLink(result.detailsPath);
204
+ if (!magnetLink) return null;
205
+
206
+ return {
207
+ magnetLink,
208
+ filename: result.title,
209
+ websiteTitle: result.title,
210
+ quality: extractQuality(result.title),
211
+ size: formatSize(result.size),
212
+ source: '1337x',
213
+ seeders: result.seeders,
214
+ leechers: result.leechers
215
+ };
216
+ } catch (error) {
217
+ console.error('Error processing result:', error);
218
+ return null;
219
+ }
220
+ }));
221
+
222
+ const validStreams = streams.filter(Boolean);
223
+
224
+ if (validStreams.length > 0) {
225
+ console.log('\nSample stream:', {
226
+ title: validStreams[0].websiteTitle,
227
+ quality: validStreams[0].quality,
228
+ size: validStreams[0].size,
229
+ seeders: validStreams[0].seeders
230
+ });
231
+ }
232
+
233
+ // Sort by seeders and quality
234
+ validStreams.sort((a, b) => {
235
+ const qualityOrder = { '2160p': 4, '4k': 4, 'uhd': 4, '1080p': 3, '720p': 2 };
236
+ const qualityDiff = (qualityOrder[b.quality] || 0) - (qualityOrder[a.quality] || 0);
237
+ if (qualityDiff !== 0) return qualityDiff;
238
+ return b.seeders - a.seeders;
239
+ });
240
+
241
+ return validStreams;
242
+
243
+ } catch (error) {
244
+ console.error('❌ Error searching 1337x:', error);
245
+
246
+ for (const fallbackUrl of SITE_CONFIG.fallbackUrls) {
247
+ try {
248
+ console.log(`Trying fallback URL: ${fallbackUrl}`);
249
+ SITE_CONFIG.baseUrl = fallbackUrl;
250
+ return await searchTorrents(searchQuery, type);
251
+ } catch (fallbackError) {
252
+ console.error(`Fallback ${fallbackUrl} also failed:`, fallbackError);
253
+ }
254
+ }
255
+
256
+ return [];
257
+ }
258
+ }
259
+
260
+ export { searchTorrents };
src/aither.js CHANGED
@@ -29,15 +29,70 @@ async function fetchWithTimeout(url, options = {}, timeout = 30000) {
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);
@@ -48,7 +103,7 @@ async function fetchTorrents(imdbId) {
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',
@@ -60,19 +115,18 @@ async function fetchTorrents(imdbId) {
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
  );
@@ -82,6 +136,14 @@ async function fetchTorrents(imdbId) {
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))
@@ -89,15 +151,21 @@ async function fetchTorrents(imdbId) {
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);
@@ -107,12 +175,22 @@ async function fetchTorrents(imdbId) {
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) {
@@ -127,11 +205,6 @@ async function fetchTorrents(imdbId) {
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';
 
29
  }
30
  }
31
 
32
+ function isCorrectEpisode(title, season, episode) {
33
+ if (!title) return false;
34
+
35
+ const seasonStr = season.toString().padStart(2, '0');
36
+ const episodeStr = episode.toString().padStart(2, '0');
37
+
38
+ // Common episode patterns
39
+ const patterns = [
40
+ new RegExp(`S${seasonStr}E${episodeStr}`, 'i'), // S01E01
41
+ new RegExp(`${season}x${episodeStr}`, 'i'), // 1x01
42
+ new RegExp(`[. ]${season}${episodeStr}[. ]`), // .101. or ' 101 '
43
+ new RegExp(`Season ${season} Episode ${episode}`, 'i'), // Season 1 Episode 1
44
+ new RegExp(`Season.?${season}.?Episode.?${episode}`, 'i') // Season1Episode1 or Season.1.Episode.1
45
+ ];
46
+
47
+ return patterns.some(pattern => pattern.test(title));
48
+ }
49
+
50
+ function extractSeasonEpisode(query) {
51
+ const match = query.match(/S(\d{1,2})E(\d{1,2})/i);
52
+ if (match) {
53
+ return {
54
+ season: parseInt(match[1]),
55
+ episode: parseInt(match[2])
56
+ };
57
+ }
58
+ return null;
59
+ }
60
+
61
+ async function fetchTorrents(searchQuery, type = 'movie') {
62
+ console.log('\n🔄 Fetching from Aither API for:', searchQuery);
63
+
64
  try {
65
+ let params;
66
+ let episodeInfo = null;
67
+
68
+ if (type === 'series') {
69
+ // Extract season and episode info
70
+ episodeInfo = extractSeasonEpisode(searchQuery);
71
+ if (!episodeInfo) {
72
+ console.log('No valid season/episode info found in query');
73
+ return [];
74
+ }
75
+
76
+ // Extract show name for search
77
+ const showName = searchQuery.split(/S\d{2}E\d{2}/i)[0].trim();
78
+ console.log('Series search:', { showName, season: episodeInfo.season, episode: episodeInfo.episode });
79
+
80
+ params = new URLSearchParams({
81
+ name: showName,
82
+ sortField: 'created_at',
83
+ sortDirection: 'desc',
84
+ perPage: '100'
85
+ });
86
+ } else {
87
+ // For movies, use IMDB ID if available, otherwise use title
88
+ const isImdbId = searchQuery.startsWith('tt');
89
+ params = new URLSearchParams({
90
+ [isImdbId ? 'imdbId' : 'name']: isImdbId ? searchQuery.replace('tt', '') : searchQuery,
91
+ sortField: 'created_at',
92
+ sortDirection: 'desc',
93
+ perPage: '100'
94
+ });
95
+ }
96
 
97
  const url = `${API_CONFIG.baseUrl}/api/torrents/filter?${params}`;
98
  console.log('Request URL:', url);
 
103
  'Authorization': `Bearer ${API_CONFIG.apiKey}`,
104
  'Accept': '*/*',
105
  'Accept-Language': '*',
106
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
107
  'Accept-Encoding': 'gzip, deflate'
108
  },
109
  method: 'GET',
 
115
  }
116
 
117
  const data = await response.json();
 
118
  if (!data.data || !Array.isArray(data.data)) {
119
  console.log('No torrents found');
120
  return [];
121
  }
122
 
123
+ console.log(`Found ${data.data.length} initial results`);
124
+
125
  const streams = await Promise.all(data.data.map(async (item, index) => {
126
  try {
127
  console.log(`\nProcessing item ${index + 1}/${data.data.length}:`, item.attributes.name);
128
+
129
+ // Skip if no video files
130
  const hasVideoFiles = item.attributes.files.some(file =>
131
  /\.(mp4|mkv|avi|mov|wmv)$/i.test(file.name)
132
  );
 
136
  return null;
137
  }
138
 
139
+ // For series, check if it matches the requested episode
140
+ if (type === 'series' && episodeInfo) {
141
+ if (!isCorrectEpisode(item.attributes.name, episodeInfo.season, episodeInfo.episode)) {
142
+ console.log('Episode mismatch:', item.attributes.name);
143
+ return null;
144
+ }
145
+ }
146
+
147
  // Find the main video file (largest video file)
148
  const videoFiles = item.attributes.files
149
  .filter(file => /\.(mp4|mkv|avi|mov|wmv)$/i.test(file.name))
 
151
 
152
  const mainFile = videoFiles[0];
153
 
154
+ // Extract quality from name
155
+ const qualityMatch = item.attributes.name.match(/\b(2160p|1080p|720p|4k|uhd)\b/i);
156
+ const quality = qualityMatch ? qualityMatch[1].toLowerCase() : '';
157
+
158
  return {
159
  magnetLink: `magnet:?xt=urn:btih:${item.attributes.info_hash}`,
160
  filename: mainFile.name,
161
  websiteTitle: item.attributes.name,
162
+ quality: quality,
163
  size: formatSize(item.attributes.size),
164
  source: 'Aither',
165
  infoHash: item.attributes.info_hash,
166
+ mainFileSize: mainFile.size,
167
+ seeders: item.attributes.seeders || 0,
168
+ leechers: item.attributes.leechers || 0
169
  };
170
  } catch (error) {
171
  console.error(`Error processing item ${index + 1}:`, error);
 
175
 
176
  const validStreams = streams.filter(Boolean);
177
  console.log(`✅ Processed ${validStreams.length} valid streams`);
178
+
179
+ // Sort by quality and seeders
180
  validStreams.sort((a, b) => {
181
+ const qualityOrder = { '2160p': 4, '4k': 4, 'uhd': 4, '1080p': 3, '720p': 2 };
182
+ const qualityDiff = (qualityOrder[b.quality] || 0) - (qualityOrder[a.quality] || 0);
183
+ return qualityDiff || b.seeders - a.seeders;
184
  });
185
 
186
+ if (validStreams.length > 0) {
187
+ console.log('Sample stream:', {
188
+ name: validStreams[0].websiteTitle,
189
+ quality: validStreams[0].quality,
190
+ size: validStreams[0].size
191
+ });
192
+ }
193
+
194
  return validStreams;
195
 
196
  } catch (error) {
 
205
  }
206
  }
207
 
 
 
 
 
 
208
  function formatSize(bytes) {
209
  const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
210
  if (bytes === 0) return '0 Bytes';
src/debrids.js CHANGED
@@ -1,5 +1,4 @@
1
  import { ERROR } from './const.js';
2
- import { createHash } from 'crypto';
3
 
4
  class BaseDebrid {
5
  #apiKey;
@@ -149,6 +148,7 @@ class DebridLink extends BaseDebrid {
149
 
150
  class Premiumize extends BaseDebrid {
151
  #apiUrl = 'https://www.premiumize.me/api';
 
152
 
153
  constructor(apiKey) {
154
  super(apiKey, 'pr');
@@ -158,20 +158,31 @@ class Premiumize extends BaseDebrid {
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
@@ -183,6 +194,14 @@ class Premiumize extends BaseDebrid {
183
 
184
  const data = await response.json();
185
  console.log('Response Data:', data);
 
 
 
 
 
 
 
 
186
  return data;
187
 
188
  } catch (error) {
@@ -201,35 +220,49 @@ class Premiumize extends BaseDebrid {
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 {};
@@ -241,18 +274,12 @@ class Premiumize extends BaseDebrid {
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);
 
1
  import { ERROR } from './const.js';
 
2
 
3
  class BaseDebrid {
4
  #apiKey;
 
148
 
149
  class Premiumize extends BaseDebrid {
150
  #apiUrl = 'https://www.premiumize.me/api';
151
+ #batchSize = 99;
152
 
153
  constructor(apiKey) {
154
  super(apiKey, 'pr');
 
158
  return apiKey.startsWith('pr=');
159
  }
160
 
161
+ async makeRequest(method, path, opts = {}) {
162
  const retries = 3;
163
  let lastError;
164
 
165
  for (let i = 0; i < retries; i++) {
166
  try {
167
+ const url = `${this.#apiUrl}${path}`;
168
+ console.log(`\n🔷 Premiumize Request (Attempt ${i + 1}/${retries}):`, method, path);
169
  if (opts.body) console.log('Request Body:', opts.body);
170
 
171
  const controller = new AbortController();
172
  const timeout = setTimeout(() => controller.abort(), 30000);
173
 
174
+ // Add API key to FormData if it's a POST request
175
+ if (method === 'POST' && opts.body instanceof FormData) {
176
+ opts.body.append('apikey', this.getKey());
177
+ }
178
+
179
+ // For GET requests, add API key to URL
180
+ const finalUrl = method === 'GET'
181
+ ? `${url}${url.includes('?') ? '&' : '?'}apikey=${this.getKey()}`
182
+ : url;
183
+
184
  const startTime = Date.now();
185
+ const response = await fetch(finalUrl, {
186
  ...opts,
187
  method,
188
  signal: controller.signal
 
194
 
195
  const data = await response.json();
196
  console.log('Response Data:', data);
197
+
198
+ if (data.status === 'error') {
199
+ if (data.message === 'Invalid API key.') {
200
+ throw new Error(ERROR.INVALID_API_KEY);
201
+ }
202
+ throw new Error(`API Error: ${data.message}`);
203
+ }
204
+
205
  return data;
206
 
207
  } catch (error) {
 
220
  async checkCacheStatuses(hashes) {
221
  try {
222
  console.log(`\n📡 Premiumize: Batch checking ${hashes.length} hashes`);
223
+ console.log('Processing in batches of', this.#batchSize);
 
 
 
224
 
225
+ const results = {};
226
+ const batches = [];
227
+
228
+ // Split hashes into batches
229
+ for (let i = 0; i < hashes.length; i += this.#batchSize) {
230
+ batches.push(hashes.slice(i, i + this.#batchSize));
231
+ }
232
+
233
+ console.log(`Split into ${batches.length} batches`);
234
+
235
+ // Process each batch
236
+ for (let i = 0; i < batches.length; i++) {
237
+ const batch = batches[i];
238
+ console.log(`\nProcessing batch ${i + 1}/${batches.length} (${batch.length} hashes)`);
239
+
240
+ const params = new URLSearchParams();
241
+ batch.forEach(hash => params.append('items[]', hash));
242
+
243
+ const data = await this.makeRequest('GET', `/cache/check?${params}`);
244
+
245
+ // Map the responses to the corresponding hashes
246
+ batch.forEach((hash, index) => {
247
+ results[hash] = {
248
+ cached: data.response[index],
249
+ files: [],
250
+ fileCount: 0,
251
+ service: 'Premiumize'
252
+ };
253
+ });
254
+
255
+ // Add a small delay between batches to avoid rate limiting
256
+ if (i < batches.length - 1) {
257
+ await new Promise(resolve => setTimeout(resolve, 500));
258
  }
 
259
  }
260
 
 
 
 
 
 
 
 
 
 
 
261
  const cachedCount = Object.values(results).filter(r => r.cached).length;
262
+ console.log(`\nPremiumize found ${cachedCount} cached torrents out of ${hashes.length}`);
263
+
264
  return results;
265
+
266
  } catch (error) {
267
  console.error('Cache check failed:', error);
268
  return {};
 
274
  console.log('\n📥 Using Premiumize to process magnet:', magnetLink.substring(0, 100) + '...');
275
 
276
  const body = new FormData();
 
277
  body.append('src', magnetLink);
278
 
279
+ const data = await this.makeRequest('POST', '/transfer/directdl', {
280
  body
281
  });
282
 
 
 
 
 
 
283
  const videoFiles = data.content
284
  .filter(file => /\.(mp4|mkv|avi|mov|webm)$/i.test(file.path))
285
  .sort((a, b) => b.size - a.size);
src/eztv.js ADDED
@@ -0,0 +1,258 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { promisify } from 'util';
2
+
3
+ const SITE_CONFIG = {
4
+ baseUrl: 'https://eztvx.to',
5
+ fallbackUrls: [
6
+ 'https://eztv.wf',
7
+ 'https://eztv.tf',
8
+ 'https://eztv.yt',
9
+ 'https://eztv1.xyz'
10
+ ],
11
+ headers: {
12
+ 'Host': 'eztvx.to',
13
+ 'Cookie': 'sort_no=100; q_filter=all; q_filter_web=on; q_filter_reality=on; q_filter_x265=on; layout=def_wlinks',
14
+ 'Accept': '*/*',
15
+ 'Accept-Language': '*',
16
+ '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',
17
+ 'Accept-Encoding': 'gzip, deflate'
18
+ }
19
+ };
20
+
21
+ async function fetchWithTimeout(url, options = {}, timeout = 30000) {
22
+ const controller = new AbortController();
23
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
24
+
25
+ try {
26
+ const host = new URL(url).host;
27
+ const headers = {
28
+ ...SITE_CONFIG.headers,
29
+ 'Host': host,
30
+ ...options.headers
31
+ };
32
+
33
+ const response = await fetch(url, {
34
+ ...options,
35
+ headers,
36
+ signal: controller.signal
37
+ });
38
+ clearTimeout(timeoutId);
39
+ return response;
40
+ } catch (error) {
41
+ clearTimeout(timeoutId);
42
+ throw error;
43
+ }
44
+ }
45
+
46
+ function isCorrectEpisode(title, searchQuery) {
47
+ if (!title || !searchQuery) return false;
48
+
49
+ const queryMatch = searchQuery.match(/S(\d{2})E(\d{2})/i);
50
+ if (!queryMatch) return false;
51
+
52
+ const querySeasonNum = parseInt(queryMatch[1]);
53
+ const queryEpisodeNum = parseInt(queryMatch[2]);
54
+
55
+ const patterns = [
56
+ /S(\d{2})E(\d{2})/i, // S01E01
57
+ /(\d{1,2})x(\d{2})/i, // 1x01
58
+ /[. ](\d{1,2})(\d{2})[. ]/, // .101. or ' 101 '
59
+ /Season (\d{1,2}) Episode (\d{1,2})/i, // Season 1 Episode 1
60
+ /[. ]E(\d{2})[. ]/i, // .E01.
61
+ /(\d{1,2})[. ](\d{2})/ // 1.01 or 1 01
62
+ ];
63
+
64
+ for (const pattern of patterns) {
65
+ const match = title.match(pattern);
66
+ if (match) {
67
+ let seasonNum, episodeNum;
68
+ if (pattern.toString().includes('[. ]E')) {
69
+ seasonNum = querySeasonNum;
70
+ episodeNum = parseInt(match[1]);
71
+ } else {
72
+ seasonNum = parseInt(match[1]);
73
+ episodeNum = parseInt(match[2]);
74
+ }
75
+
76
+ if (seasonNum === querySeasonNum && episodeNum === queryEpisodeNum) {
77
+ return true;
78
+ }
79
+ }
80
+ }
81
+
82
+ // Handle combined format (101, 102, etc)
83
+ const combinedMatch = title.match(/[^0-9](\d)(\d{2})[^0-9]/);
84
+ if (combinedMatch) {
85
+ const seasonNum = parseInt(combinedMatch[1]);
86
+ const episodeNum = parseInt(combinedMatch[2]);
87
+ if (seasonNum === querySeasonNum && episodeNum === queryEpisodeNum) {
88
+ return true;
89
+ }
90
+ }
91
+
92
+ return false;
93
+ }
94
+
95
+ function parseSize(sizeStr) {
96
+ if (!sizeStr) return 0;
97
+ const match = sizeStr.match(/([\d.]+)\s*(KB|MB|GB|TB)/i);
98
+ if (!match) return 0;
99
+
100
+ const [, value, unit] = match;
101
+ const size = parseFloat(value);
102
+
103
+ switch (unit.toUpperCase()) {
104
+ case 'TB': return size * 1024 * 1024 * 1024;
105
+ case 'GB': return size * 1024 * 1024;
106
+ case 'MB': return size * 1024;
107
+ case 'KB': return size;
108
+ default: return 0;
109
+ }
110
+ }
111
+
112
+ function extractQuality(title) {
113
+ const qualityMatch = title.match(/\b(2160p|1080p|720p|4k|uhd)\b/i);
114
+ return qualityMatch ? qualityMatch[1].toLowerCase() : '';
115
+ }
116
+
117
+ function cleanTitle(title) {
118
+ if (!title) return '';
119
+ return title
120
+ .replace(/\[eztv\]/i, '')
121
+ .replace(/\[eztvx?\.to\]/i, '')
122
+ .replace(/\[eztv\.(re|io)\]/i, '')
123
+ .replace(/<img[^>]+>/g, '')
124
+ .replace(/\[.*?\]/g, '')
125
+ .replace(/\(.*?\)/g, '')
126
+ .replace(/\.+/g, ' ')
127
+ .replace(/\s+/g, ' ')
128
+ .trim();
129
+ }
130
+
131
+ async function searchTorrents(searchQuery, type = 'series') {
132
+ if (type !== 'series') {
133
+ console.log('EZTV only supports TV series searches');
134
+ return [];
135
+ }
136
+
137
+ console.log('\n🔄 Searching EZTV for:', searchQuery);
138
+
139
+ try {
140
+ const showMatch = searchQuery.match(/(.+?)S\d{2}E\d{2}/i);
141
+ const searchTerm = showMatch ? showMatch[1].trim() : searchQuery;
142
+
143
+ const formattedQuery = searchTerm
144
+ .replace(/-/g, '')
145
+ .replace(/\s+/g, '-')
146
+ .replace(/&/g, '')
147
+ .toLowerCase();
148
+
149
+ const url = `${SITE_CONFIG.baseUrl}/search/${encodeURIComponent(formattedQuery)}`;
150
+ console.log('Request URL:', url);
151
+
152
+ const response = await fetchWithTimeout(url);
153
+
154
+ if (!response.ok) {
155
+ throw new Error(`Search request failed: ${response.status}`);
156
+ }
157
+
158
+ const html = await response.text();
159
+ console.log('Response received, length:', html.length);
160
+
161
+ const rows = html.match(/<tr[^>]*name="hover"[^>]*>[\s\S]*?<\/tr>/g) || [];
162
+ console.log(`Found ${rows.length} raw results`);
163
+
164
+ const streams = rows.map(row => {
165
+ try {
166
+ const titleLinkMatch = row.match(/<a href="\/ep\/\d+\/[^"]+\/"[^>]*class="epinfo">(.*?)<\/a>/);
167
+ if (!titleLinkMatch) return null;
168
+ let title = titleLinkMatch[1].replace(/<[^>]+>/g, '').trim();
169
+
170
+ const magnetMatch = row.match(/href="(magnet:\?xt=urn:btih:[^"]+)"/);
171
+ if (!magnetMatch) return null;
172
+ const magnetLink = decodeURIComponent(magnetMatch[1]);
173
+
174
+ const sizeMatch = row.match(/<td[^>]*class="forum_thread_post"[^>]*>([^<]+(?:KB|MB|GB|TB))<\/td>/);
175
+ const size = sizeMatch ? sizeMatch[1].trim() : '512 MB';
176
+
177
+ const seedersMatch = row.match(/<font color="green">(\d+)<\/font>/);
178
+ const seeders = seedersMatch ? parseInt(seedersMatch[1]) : 0;
179
+
180
+ title = cleanTitle(title);
181
+
182
+ if (title.includes('COMPLETE') && searchQuery.match(/S\d{2}E\d{2}/i)) {
183
+ console.log('Skipping complete season pack:', title);
184
+ return null;
185
+ }
186
+
187
+ const mainFileSize = parseSize(size);
188
+ if (mainFileSize < (100 * 1024)) {
189
+ console.log('Skipping small file:', title, size);
190
+ return null;
191
+ }
192
+
193
+ return {
194
+ magnetLink,
195
+ filename: title,
196
+ websiteTitle: title,
197
+ quality: extractQuality(title),
198
+ size,
199
+ source: 'EZTV',
200
+ seeders,
201
+ leechers: 0,
202
+ mainFileSize
203
+ };
204
+ } catch (error) {
205
+ console.error('Error parsing row:', error);
206
+ return null;
207
+ }
208
+ }).filter(Boolean);
209
+
210
+ const filteredStreams = streams.filter(stream =>
211
+ isCorrectEpisode(stream.websiteTitle, searchQuery)
212
+ );
213
+
214
+ console.log(`Found ${filteredStreams.length} matching streams after filtering`);
215
+
216
+ filteredStreams.sort((a, b) => {
217
+ const qualityOrder = { '2160p': 4, '4k': 4, 'uhd': 4, '1080p': 3, '720p': 2 };
218
+ const qualityDiff = (qualityOrder[b.quality] || 0) - (qualityOrder[a.quality] || 0);
219
+ if (qualityDiff === 0) {
220
+ return b.mainFileSize - a.mainFileSize;
221
+ }
222
+ return qualityDiff;
223
+ });
224
+
225
+ if (filteredStreams.length > 0) {
226
+ console.log('Sample stream:', {
227
+ title: filteredStreams[0].websiteTitle,
228
+ quality: filteredStreams[0].quality,
229
+ size: filteredStreams[0].size,
230
+ seeders: filteredStreams[0].seeders
231
+ });
232
+ }
233
+
234
+ return filteredStreams;
235
+
236
+ } catch (error) {
237
+ console.error('❌ Error searching EZTV:', error);
238
+
239
+ for (const fallbackUrl of SITE_CONFIG.fallbackUrls) {
240
+ try {
241
+ console.log(`Trying fallback URL: ${fallbackUrl}`);
242
+ const originalBaseUrl = SITE_CONFIG.baseUrl;
243
+ SITE_CONFIG.baseUrl = fallbackUrl;
244
+ const results = await searchTorrents(searchQuery, type);
245
+ if (results.length > 0) {
246
+ return results;
247
+ }
248
+ SITE_CONFIG.baseUrl = originalBaseUrl;
249
+ } catch (fallbackError) {
250
+ console.error(`Fallback ${fallbackUrl} also failed:`, fallbackError);
251
+ }
252
+ }
253
+
254
+ return [];
255
+ }
256
+ }
257
+
258
+ export { searchTorrents };
src/tday.js CHANGED
@@ -6,8 +6,8 @@ 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
 
@@ -16,34 +16,56 @@ const parser = new xml2js.Parser({
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) {
@@ -71,6 +93,7 @@ async function downloadAndParseTorrent(url) {
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);
@@ -81,13 +104,22 @@ async function downloadAndParseTorrent(url) {
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,
@@ -105,15 +137,61 @@ async function downloadAndParseTorrent(url) {
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: {
@@ -146,6 +224,12 @@ async function fetchRSSFeeds(imdbId) {
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
 
@@ -179,17 +263,17 @@ async function fetchRSSFeeds(imdbId) {
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 };
 
6
 
7
  const RSS_FEEDS = [
8
  {
9
+ name: 'TorrentDay',
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=addyourown;tp=addyourown;addyourown;private;do-not-share'
11
  }
12
  ];
13
 
 
16
  ignoreAttrs: true
17
  });
18
 
19
+ function isCorrectEpisode(title, searchQuery) {
20
+ if (!title || !searchQuery) return false;
 
21
 
22
+ // Extract season and episode numbers from search query
23
+ const queryMatch = searchQuery.match(/S(\d{2})E(\d{2})/i);
24
+ if (!queryMatch) return false;
25
+ const querySeasonNum = parseInt(queryMatch[1]);
26
+ const queryEpisodeNum = parseInt(queryMatch[2]);
27
+
28
+ // Common episode patterns
29
+ const patterns = [
30
+ /S(\d{2})E(\d{2})/i, // S01E01
31
+ /(\d{1,2})x(\d{2})/i, // 1x01
32
+ /[. ](\d{1,2})(\d{2})[. ]/, // .101. or ' 101 '
33
+ /Season (\d{1,2}) Episode (\d{1,2})/i, // Season 1 Episode 1
34
+ /Season.?(\d{1,2}).?Episode.?(\d{1,2})/i, // Season1Episode1, Season.1.Episode.1
35
+ /E(\d{2}) of S(\d{2})/i, // E01 of S01
36
+ /(\d{1,2})[. ](\d{2})/, // 1.01 or 1 01
37
+ /\bS(\d{2}) ?- ?E(\d{2})\b/i // S01-E01 or S01 - E01
38
+ ];
39
+
40
+ for (const pattern of patterns) {
41
+ const match = title.match(pattern);
42
+ if (match) {
43
+ let seasonNum, episodeNum;
44
+ if (pattern.toString().includes('of S')) {
45
+ seasonNum = parseInt(match[2]);
46
+ episodeNum = parseInt(match[1]);
47
+ } else {
48
+ seasonNum = parseInt(match[1]);
49
+ episodeNum = parseInt(match[2]);
50
+ }
51
+
52
+ if (seasonNum === querySeasonNum && episodeNum === queryEpisodeNum) {
53
+ return true;
54
+ }
55
+ }
56
  }
57
 
58
+ // Handle combined season+episode format (101, 102, etc)
59
+ const combinedMatch = title.match(/[^0-9](\d)(\d{2})[^0-9]/);
60
+ if (combinedMatch) {
61
+ const seasonNum = parseInt(combinedMatch[1]);
62
+ const episodeNum = parseInt(combinedMatch[2]);
63
+ if (seasonNum === querySeasonNum && episodeNum === queryEpisodeNum) {
64
+ return true;
 
 
 
 
 
 
 
 
 
65
  }
66
  }
67
 
68
+ return false;
69
  }
70
 
71
  async function downloadAndParseTorrent(url) {
 
93
  return null;
94
  }
95
 
96
+ // Filter and sort video files
97
  const videoFiles = torrentInfo.files?.filter(file => {
98
  const filePath = Array.isArray(file.path) ? file.path.join('/') : file.path;
99
  return /\.(mp4|mkv|avi|mov|wmv)$/i.test(filePath);
 
104
  return null;
105
  }
106
 
107
+ // Sort by size and filter small files
108
  videoFiles.sort((a, b) => b.length - a.length);
109
  const mainFile = videoFiles[0];
110
+
111
+ // Skip if main file is too small (likely sample)
112
+ const mainFileSizeMB = mainFile.length / (1024 * 1024);
113
+ if (mainFileSizeMB < 100) {
114
+ console.log('Main file too small, likely a sample:', mainFileSizeMB.toFixed(2) + 'MB');
115
+ return null;
116
+ }
117
+
118
  const mainFilePath = Array.isArray(mainFile.path) ? mainFile.path.join('/') : mainFile.path;
119
 
120
  const magnetUri = `magnet:?xt=urn:btih:${torrentInfo.infoHash}` +
121
+ `&dn=${encodeURIComponent(torrentInfo.name)}` +
122
+ (torrentInfo.announce ? torrentInfo.announce.map(tr => `&tr=${encodeURIComponent(tr)}`).join('') : '');
123
 
124
  return {
125
  magnetLink: magnetUri,
 
137
  }
138
  }
139
 
140
+ function parseTorrentInfo(item) {
141
+ const desc = item.description || '';
142
+ const match = desc.match(/Category:\s*([^]+?)Size:\s*([^]+?)$/);
143
+
144
+ if (!match) {
145
+ return { size: 'Unknown', category: 'Unknown', sizeInMB: 0 };
146
+ }
147
+
148
+ const category = match[1].trim();
149
+ const size = match[2].trim();
150
+
151
+ const sizeMatch = size.match(/([\d.]+)\s*(GB|MB|TB)/i);
152
+ let sizeInMB = 0;
153
+
154
+ if (sizeMatch) {
155
+ const [, value, unit] = sizeMatch;
156
+ sizeInMB = parseFloat(value);
157
+ switch (unit.toUpperCase()) {
158
+ case 'TB':
159
+ sizeInMB *= 1024 * 1024;
160
+ break;
161
+ case 'GB':
162
+ sizeInMB *= 1024;
163
+ break;
164
+ }
165
+ }
166
+
167
+ return { size, category, sizeInMB };
168
+ }
169
+
170
+ function extractQuality(title) {
171
+ const qualityMatch = title.match(/\b(2160p|1080p|720p|4k|uhd)\b/i);
172
+ return qualityMatch ? qualityMatch[1].toLowerCase() : '';
173
+ }
174
+
175
+ async function fetchRSSFeeds(searchQuery, type = 'movie') {
176
+ console.log('\n🔄 Fetching RSS feeds for:', searchQuery);
177
  let allStreams = [];
178
 
179
  for (const feed of RSS_FEEDS) {
180
  try {
181
  console.log(`\nFetching from ${feed.name}...`);
182
 
183
+ // Modify search based on type
184
+ let searchTerm;
185
+ if (type === 'series') {
186
+ // Extract show name without episode info for broader search
187
+ const showMatch = searchQuery.match(/(.+?)S\d{2}E\d{2}/i);
188
+ searchTerm = showMatch ? showMatch[1].trim() : searchQuery;
189
+ } else {
190
+ searchTerm = searchQuery;
191
+ }
192
+
193
+ const rssUrl = `${feed.url};q=${encodeURIComponent(searchTerm)}`;
194
+ console.log('Request URL:', rssUrl);
195
 
196
  const response = await fetch(rssUrl, {
197
  headers: {
 
224
  try {
225
  console.log(`\nProcessing item ${index + 1}/${items.length}:`, item.title);
226
 
227
+ // For series, check if item matches requested episode
228
+ if (type === 'series' && !isCorrectEpisode(item.title, searchQuery)) {
229
+ console.log('Episode mismatch:', item.title);
230
+ return null;
231
+ }
232
+
233
  const torrentInfo = await downloadAndParseTorrent(item.link);
234
  if (!torrentInfo) return null;
235
 
 
263
  }
264
  }
265
 
266
+ // Sort by quality and size
267
  allStreams.sort((a, b) => {
268
  const qualityOrder = { '2160p': 4, '4k': 4, 'uhd': 4, '1080p': 3, '720p': 2 };
269
+ const qualityDiff = (qualityOrder[b.quality] || 0) - (qualityOrder[a.quality] || 0);
270
+ if (qualityDiff === 0) {
271
+ return b.mainFileSize - a.mainFileSize;
272
+ }
273
+ return qualityDiff;
274
  });
275
 
276
  return allStreams;
277
  }
278
 
 
 
 
 
 
279
  export { fetchRSSFeeds };
src/util.js CHANGED
@@ -1,14 +1,21 @@
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) {
@@ -16,20 +23,32 @@ export async function wait(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
@@ -54,8 +73,13 @@ export function findBestFile(files, metadata) {
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;
@@ -70,6 +94,20 @@ export function findBestFile(files, metadata) {
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;
@@ -85,7 +123,7 @@ export function findBestFile(files, metadata) {
85
  return {
86
  file,
87
  score,
88
- size: file.size || 0,
89
  filename,
90
  matchingParts
91
  };
@@ -112,7 +150,7 @@ export function findBestFile(files, metadata) {
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
  }
 
1
  import { VIDEO_EXTENSIONS } from './const.js';
2
 
3
  export function isVideo(filename) {
4
+ if (!filename) return false;
5
  return VIDEO_EXTENSIONS.some(ext =>
6
  filename.toLowerCase().endsWith(ext)
7
  );
8
  }
9
 
10
  export function extractInfoHash(magnetLink) {
11
+ if (!magnetLink) return null;
12
+ try {
13
+ const match = magnetLink.match(/xt=urn:btih:([^&]+)/i);
14
+ return match ? match[1].toLowerCase() : null;
15
+ } catch (error) {
16
+ console.error('Error extracting info hash:', error);
17
+ return null;
18
+ }
19
  }
20
 
21
  export async function wait(ms) {
 
23
  }
24
 
25
  export function base64Encode(str) {
26
+ if (!str) return '';
27
+ try {
28
+ return Buffer.from(str).toString('base64')
29
+ .replace(/\+/g, '-')
30
+ .replace(/\//g, '_')
31
+ .replace(/=/g, '');
32
+ } catch (error) {
33
+ console.error('Error encoding to base64:', error);
34
+ return '';
35
+ }
36
  }
37
 
38
  export function base64Decode(str) {
39
+ if (!str) return '';
40
+ try {
41
+ str = str.replace(/-/g, '+').replace(/_/g, '/');
42
+ while (str.length % 4) str += '=';
43
+ return Buffer.from(str, 'base64').toString('ascii');
44
+ } catch (error) {
45
+ console.error('Error decoding from base64:', error);
46
+ return '';
47
+ }
48
  }
49
 
50
  export function findBestFile(files, metadata) {
51
+ if (!Array.isArray(files) || !files?.length) return null;
52
  if (files.length === 1) return files[0];
53
 
54
  // If no metadata provided, return largest file
 
73
  // Score each video file
74
  const scoredFiles = files.map(file => {
75
  let score = 0;
76
+ const filename = (file.name?.toLowerCase() || file.path?.toLowerCase() || '');
77
 
78
+ // Skip if not a video file
79
+ if (!filename.match(/\.(mp4|mkv|avi|mov|wmv|m4v|ts)$/i)) {
80
+ return { file, score: -100, filename };
81
+ }
82
+
83
  // Count matching title parts
84
  const matchingParts = titleParts.filter(part => filename.includes(part));
85
  score += (matchingParts.length / titleParts.length) * 100;
 
94
  console.log(`Year match found in "${filename}"`);
95
  }
96
 
97
+ // Quality bonus
98
+ if (filename.match(/2160p|4k|uhd/i)) score += 40;
99
+ else if (filename.match(/1080p/i)) score += 30;
100
+ else if (filename.match(/720p/i)) score += 20;
101
+
102
+ // Source quality bonus
103
+ if (filename.match(/blu-?ray|remux/i)) score += 25;
104
+ else if (filename.match(/web-?dl|webrip/i)) score += 15;
105
+ else if (filename.match(/hdtv/i)) score += 10;
106
+
107
+ // Encoding bonus
108
+ if (filename.match(/x265|hevc|h\.?265/i)) score += 15;
109
+ else if (filename.match(/x264|h\.?264/i)) score += 10;
110
+
111
  // Penalize likely pack/collection files
112
  if (filename.match(/\b(pack|collection|complete|season|episode|[es]\d{2,3})\b/i)) {
113
  score -= 30;
 
123
  return {
124
  file,
125
  score,
126
+ size: file.size || file.length || 0,
127
  filename,
128
  matchingParts
129
  };
 
150
  // If no good matches found (low score), fall back to largest file
151
  if (bestMatch.score < 30) {
152
  console.log('No confident match found, falling back to largest file');
153
+ const largestFile = files.sort((a, b) => (b.size || b.length || 0) - (a.size || a.length || 0))[0];
154
  console.log(`Selected largest file: ${largestFile.name || largestFile.path}`);
155
  return largestFile;
156
  }
src/yourbittorrent.js CHANGED
@@ -12,6 +12,10 @@ const SITE_CONFIG = {
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');
@@ -92,24 +96,117 @@ async function downloadAndParseTorrent(url) {
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, {
@@ -154,20 +251,37 @@ async function searchTorrents(imdbId) {
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);
@@ -185,7 +299,7 @@ async function searchTorrents(imdbId) {
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
  }
@@ -195,37 +309,6 @@ async function searchTorrents(imdbId) {
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() : '';
 
12
 
13
  async function getCinemetaMetadata(imdbId) {
14
  try {
15
+ if (!imdbId.startsWith('tt')) {
16
+ return null;
17
+ }
18
+
19
  console.log(`\n🎬 Fetching Cinemeta data for ${imdbId}`);
20
  const response = await fetch(`https://v3-cinemeta.strem.io/meta/movie/${imdbId}.json`);
21
  if (!response.ok) throw new Error('Failed to fetch from Cinemeta');
 
96
  }
97
  }
98
 
99
+ function isCorrectEpisode(title, searchQuery) {
100
+ if (!title || !searchQuery) return false;
101
 
102
+ // Extract season and episode numbers from search query
103
+ const queryMatch = searchQuery.match(/S(\d{2})E(\d{2})/i);
104
+ if (!queryMatch) return false;
105
+ const querySeasonNum = parseInt(queryMatch[1]);
106
+ const queryEpisodeNum = parseInt(queryMatch[2]);
107
+
108
+ // Common episode patterns
109
+ const patterns = [
110
+ /S(\d{2})E(\d{2})/i, // S01E01
111
+ /(\d{1,2})x(\d{2})/i, // 1x01
112
+ /[. ](\d{1,2})(\d{2})[. ]/, // .101. or ' 101 '
113
+ /Season (\d{1,2}) Episode (\d{1,2})/i, // Season 1 Episode 1
114
+ /Season.?(\d{1,2}).?Episode.?(\d{1,2})/i // Season1Episode1, Season.1.Episode.1
115
+ ];
116
+
117
+ for (const pattern of patterns) {
118
+ const match = title.match(pattern);
119
+ if (match) {
120
+ const seasonNum = parseInt(match[1]);
121
+ const episodeNum = parseInt(match[2]);
122
+
123
+ // Check if it matches the requested season and episode
124
+ if (seasonNum === querySeasonNum && episodeNum === queryEpisodeNum) {
125
+ return true;
126
+ }
127
  }
128
+ }
129
+
130
+ // Handle combined season+episode format (101, 102, etc)
131
+ const combinedMatch = title.match(/[^0-9](\d)(\d{2})[^0-9]/);
132
+ if (combinedMatch) {
133
+ const seasonNum = parseInt(combinedMatch[1]);
134
+ const episodeNum = parseInt(combinedMatch[2]);
135
+ if (seasonNum === querySeasonNum && episodeNum === queryEpisodeNum) {
136
+ return true;
137
+ }
138
+ }
139
+
140
+ return false;
141
+ }
142
+
143
+ function parseSearchResults(html) {
144
+ const results = [];
145
+ const rows = html.match(/<tr class="table-default">[\s\S]*?<\/tr>/g) || [];
146
+
147
+ for (const row of rows) {
148
+ try {
149
+ const titleMatch = row.match(/href="\/torrent\/.*?">(.*?)<\/a>/);
150
+ const sizeMatch = row.match(/<td.*?>\s*([\d.]+\s*[KMGT]B)\s*<\/td>/);
151
+ const seedersMatch = row.match(/<td.*?>\s*(\d+)\s*<\/td>/);
152
+ const leechersMatch = row.match(/<td.*?>\s*(\d+)\s*<\/td>/);
153
+ const downloadIdMatch = row.match(/\/torrent\/(\d+)\//);
154
+
155
+ if (titleMatch && downloadIdMatch) {
156
+ const title = titleMatch[1].trim();
157
+
158
+ // Basic video file check
159
+ if (!title.match(/\.(mp4|mkv|avi|mov|wmv)$/i) &&
160
+ !title.match(/(1080p|720p|2160p|4k|uhd|web-?dl|bluray)/i)) {
161
+ continue;
162
+ }
163
+
164
+ results.push({
165
+ title: title,
166
+ size: sizeMatch?.[1] || 'Unknown',
167
+ seeders: seedersMatch?.[1] || '0',
168
+ leechers: leechersMatch?.[1] || '0',
169
+ downloadId: downloadIdMatch[1],
170
+ magnetLink: downloadIdMatch[1] ?
171
+ `${SITE_CONFIG.baseUrl}/down/${downloadIdMatch[1]}.torrent` :
172
+ null
173
+ });
174
+ }
175
+ } catch (error) {
176
+ console.error('Error parsing result row:', error);
177
+ }
178
+ }
179
 
180
+ return results;
181
+ }
182
+
183
+ async function searchTorrents(searchQuery, type = 'movie') {
184
+ console.log('\n🔄 Searching YourBittorrent for:', searchQuery);
185
+
186
+ try {
187
+ let title = searchQuery;
188
+ let year = '';
189
+ let searchTitle;
190
+
191
+ if (type === 'movie' && searchQuery.startsWith('tt')) {
192
+ const metadata = await getCinemetaMetadata(searchQuery);
193
+ if (!metadata?.meta) {
194
+ throw new Error('Failed to get movie metadata');
195
+ }
196
+ title = metadata.meta.name;
197
+ year = new Date(metadata.meta.released).getFullYear().toString();
198
+ searchTitle = title;
199
+ } else if (type === 'series') {
200
+ // For series, extract show name without episode info for initial search
201
+ const showMatch = searchQuery.match(/(.+?)S\d{2}E\d{2}/i);
202
+ searchTitle = showMatch ? showMatch[1].trim() : searchQuery;
203
+ }
204
 
205
+ const formattedQuery = searchTitle
206
  .replace(/[\\s]+/g, '-')
207
  .toLowerCase();
208
 
209
+ const url = `${SITE_CONFIG.baseUrl}/?v=&c=${type === 'movie' ? 'movies' : 'tv'}&q=${encodeURIComponent(formattedQuery)}`;
210
  console.log('Request URL:', url);
211
 
212
  const response = await fetchWithTimeout(url, {
 
251
  }));
252
 
253
  const validStreams = streams.filter(Boolean);
 
 
254
 
255
+ let filteredStreams;
256
+ if (type === 'movie' && year) {
257
+ const searchTerms = title.toLowerCase().split(' ');
258
+ filteredStreams = validStreams.filter(stream => {
259
+ const streamTitle = stream.websiteTitle.toLowerCase();
260
+ const hasYear = streamTitle.includes(year);
261
+ const hasAllTerms = searchTerms.every(term =>
262
+ streamTitle.includes(term.toLowerCase())
263
+ );
264
+ return hasYear && hasAllTerms;
265
+ });
266
+ } else if (type === 'series') {
267
+ // Use strict episode matching
268
+ filteredStreams = validStreams.filter(stream =>
269
+ isCorrectEpisode(stream.websiteTitle, searchQuery)
270
  );
271
+ } else {
272
+ filteredStreams = validStreams;
273
+ }
274
 
275
  console.log(`Found ${filteredStreams.length} relevant streams after filtering`);
276
 
277
+ if (filteredStreams.length > 0) {
278
+ console.log('Sample matched stream:', {
279
+ title: filteredStreams[0].websiteTitle,
280
+ quality: filteredStreams[0].quality,
281
+ size: filteredStreams[0].size
282
+ });
283
+ }
284
+
285
  filteredStreams.sort((a, b) => {
286
  const qualityOrder = { '2160p': 4, '4k': 4, '1080p': 3, '720p': 2 };
287
  const qualityDiff = (qualityOrder[b.quality] || 0) - (qualityOrder[a.quality] || 0);
 
299
  for (const fallbackUrl of SITE_CONFIG.fallbackUrls) {
300
  try {
301
  SITE_CONFIG.baseUrl = fallbackUrl;
302
+ return await searchTorrents(searchQuery, type);
303
  } catch (fallbackError) {
304
  console.error(`Fallback ${fallbackUrl} also failed:`, fallbackError);
305
  }
 
309
  }
310
  }
311
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
312
  function extractQuality(title) {
313
  const qualityMatch = title.match(/\b(2160p|1080p|720p|4k|uhd)\b/i);
314
  return qualityMatch ? qualityMatch[1].toLowerCase() : '';