Upload 12 files
Browse files- index.js +380 -173
- public/index.html +7 -18
- src/1337x.js +260 -0
- src/aither.js +97 -24
- src/debrids.js +61 -34
- src/eztv.js +258 -0
- src/tday.js +119 -35
- src/util.js +51 -13
- 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
29 |
try {
|
30 |
-
|
31 |
-
|
|
|
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 |
-
|
43 |
-
const
|
44 |
-
const yearFile = path.join(__dirname,
|
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
|
51 |
-
const
|
52 |
-
if (
|
53 |
-
console.log(`✅ Found
|
54 |
-
|
|
|
|
|
|
|
|
|
|
|
55 |
}
|
56 |
-
return
|
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
|
65 |
}
|
66 |
return null;
|
67 |
}
|
68 |
}
|
69 |
|
70 |
-
async function
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
71 |
try {
|
72 |
console.log('\n🔄 Fetching all available streams');
|
73 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
74 |
|
75 |
const startTime = Date.now();
|
76 |
-
const [iptStreams, tdayStreams, torrentingStreams] = await Promise.all([
|
77 |
-
fetchIPTFeeds(
|
78 |
console.error('IPTorrents fetch failed:', err);
|
79 |
return [];
|
80 |
}),
|
81 |
-
fetchTDayFeeds(
|
82 |
console.error('TorrentDay fetch failed:', err);
|
83 |
return [];
|
84 |
}),
|
85 |
-
fetchTorrentingFeeds(
|
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(
|
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 |
-
|
137 |
-
if (
|
138 |
-
console.log(
|
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 |
-
|
170 |
-
const
|
|
|
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 ${
|
180 |
console.log('Existing streams:', existingStreams.length);
|
181 |
console.log('New streams:', newStreams.length);
|
182 |
|
183 |
const existingHashes = new Set(
|
184 |
-
existingStreams
|
185 |
-
|
186 |
-
|
|
|
187 |
);
|
188 |
|
189 |
-
const uniqueNewStreams = newStreams
|
190 |
-
|
191 |
-
|
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,
|
203 |
|
204 |
-
let
|
205 |
try {
|
206 |
const content = await fs.readFile(yearFile, 'utf8');
|
207 |
-
|
208 |
-
console.log(`Read existing ${year}.json with ${
|
209 |
} catch (error) {
|
210 |
console.log(`Creating new ${year}.json file`);
|
211 |
}
|
212 |
|
213 |
-
const
|
214 |
-
if (
|
215 |
-
console.log('Updating existing
|
216 |
-
|
217 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
218 |
} else {
|
219 |
-
console.log('Adding new
|
220 |
-
|
221 |
-
|
222 |
-
|
223 |
-
originalTitle: movieTitle,
|
224 |
addedAt: new Date().toISOString(),
|
225 |
lastUpdated: new Date().toISOString()
|
226 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
227 |
}
|
228 |
|
229 |
-
await fs.mkdir(path.join(__dirname,
|
230 |
|
|
|
231 |
const tempFile = `${yearFile}.tmp`;
|
232 |
-
await fs.writeFile(tempFile, JSON.stringify(
|
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/
|
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 |
-
|
274 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
275 |
|
276 |
-
|
277 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
278 |
|
279 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
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
|
309 |
-
const size = stream.size || stream.websiteTitle
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
334 |
);
|
335 |
|
336 |
-
const hashes = newStreams
|
|
|
|
|
|
|
|
|
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 |
-
|
384 |
-
|
385 |
-
|
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 |
-
|
397 |
-
|
398 |
-
|
399 |
-
|
400 |
-
|
401 |
-
service: processedStreams[0].service
|
402 |
-
});
|
403 |
-
}
|
404 |
|
405 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
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 ||
|
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>
|
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">
|
147 |
<div class="content-wrapper">
|
148 |
<h1>Stremio Addon Installation</h1>
|
149 |
<div class="input-container">
|
150 |
-
<input type="text" id="
|
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 (!
|
173 |
-
alert('Please enter
|
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}
|
189 |
|
190 |
// Always use stremio:// protocol for the Stremio URL, regardless of original protocol
|
191 |
-
const stremioUrl = `stremio://${hostname}
|
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 |
-
|
33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
34 |
try {
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
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
|
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}
|
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 |
-
//
|
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:
|
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 |
-
|
|
|
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
|
162 |
const retries = 3;
|
163 |
let lastError;
|
164 |
|
165 |
for (let i = 0; i < retries; i++) {
|
166 |
try {
|
167 |
-
|
|
|
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(
|
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('
|
205 |
-
|
206 |
-
const params = new URLSearchParams({ apikey: this.getKey() });
|
207 |
-
hashes.forEach(hash => params.append('items[]', hash));
|
208 |
|
209 |
-
const
|
210 |
-
|
211 |
-
|
212 |
-
|
213 |
-
|
214 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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(
|
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
|
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: '
|
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=
|
11 |
}
|
12 |
];
|
13 |
|
@@ -16,34 +16,56 @@ const parser = new xml2js.Parser({
|
|
16 |
ignoreAttrs: true
|
17 |
});
|
18 |
|
19 |
-
function
|
20 |
-
|
21 |
-
const match = desc.match(/Category:\s*([^]+?)Size:\s*([^]+?)$/);
|
22 |
|
23 |
-
|
24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
25 |
}
|
26 |
|
27 |
-
|
28 |
-
const
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
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
|
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 |
-
|
90 |
-
|
91 |
|
92 |
return {
|
93 |
magnetLink: magnetUri,
|
@@ -105,15 +137,61 @@ async function downloadAndParseTorrent(url) {
|
|
105 |
}
|
106 |
}
|
107 |
|
108 |
-
|
109 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
110 |
let allStreams = [];
|
111 |
|
112 |
for (const feed of RSS_FEEDS) {
|
113 |
try {
|
114 |
console.log(`\nFetching from ${feed.name}...`);
|
115 |
|
116 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
|
|
|
|
|
|
|
|
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 |
-
|
11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
20 |
-
|
21 |
-
.
|
22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
23 |
}
|
24 |
|
25 |
export function base64Decode(str) {
|
26 |
-
|
27 |
-
|
28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
96 |
-
|
97 |
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
102 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
103 |
|
104 |
-
|
105 |
-
|
106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
107 |
|
108 |
-
const formattedQuery =
|
109 |
.replace(/[\\s]+/g, '-')
|
110 |
.toLowerCase();
|
111 |
|
112 |
-
const url = `${SITE_CONFIG.baseUrl}/?v=&c
|
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 |
-
|
161 |
-
|
162 |
-
const
|
163 |
-
|
164 |
-
streamTitle.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
165 |
);
|
166 |
-
|
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(
|
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() : '';
|