vanseleem commited on
Commit
2e43b6f
·
verified ·
1 Parent(s): d49c06a

Update index.js

Browse files
Files changed (1) hide show
  1. index.js +314 -198
index.js CHANGED
@@ -1,58 +1,103 @@
1
  const express = require('express');
2
- const fetch = require('node-fetch');
3
-
4
  const app = express();
5
  const PORT = process.env.PORT || 7860;
6
 
 
7
  app.use((req, res, next) => {
8
- res.header('Access-Control-Allow-Origin', '*');
9
- res.header('Access-Control-Allow-Headers', '*');
10
- res.header('Access-Control-Allow-Methods', 'GET, OPTIONS');
11
- if (req.method === 'OPTIONS') return res.sendStatus(200);
12
- next();
13
  });
14
 
15
- const UPSTREAM_URLS = [
16
- 'https://torrentio.strem.fun',
17
- 'https://torrentio.stremio.my.id'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  ];
19
-
20
- const MIN_SEEDERS = 1;
21
- const ALLOWED_QUALITIES = ['480p', '576p', '720p', '1080p', 'webrip', 'webdl', 'bluray'];
22
- const MAX_SIZE_GB_MOVIE = 4;
23
- const MAX_SIZE_GB_SERIES = 1.5;
24
-
25
- const SOURCE_CONFIG = [
26
- { name: 'yts', display: 'YTS', order: 1 },
27
- { name: 'yify', display: 'YTS', order: 1 },
28
- { name: 'eztv', display: 'EZTV', order: 2 },
29
- { name: 'nyaasi', display: 'Nyaa', order: 3 },
30
- { name: 'thepiratebay', display: 'TPB', order: 4 },
31
- { name: 'piratebay', display: 'TPB', order: 4 },
32
- { name: 'the piratebay', display: 'TPB', order: 4 },
33
- { name: 'tpb', display: 'TPB', order: 4 },
34
- { name: '1337x', display: '1337x', order: 5 },
35
- { name: 'torrentgalaxy', display: 'TGx', order: 6 },
36
- { name: 'rarbg', display: 'RARBG', order: 7 },
37
- { name: 'rutracker', display: 'Rutracker', order: 8 }
38
  ];
39
 
40
- const QUALITY_RANK = {
41
- '1080p': 1,
42
- '720p': 2,
43
- 'webdl': 3,
44
- 'webrip': 4,
45
- '576p': 5,
46
- '480p': 6,
47
- 'bluray': 7
48
- };
49
 
50
- const LINKS_PER_QUALITY = 5;
 
 
 
51
 
52
- // ---------- Helper functions for unified title (from V2 layout) ----------
53
  function detectProvider(t) {
54
  const m = t.match(/\[([^\]]+)\]/);
55
- return m ? m[1].toLowerCase() : 'other';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  }
57
 
58
  function parseSizeBytes(t, obj) {
@@ -86,6 +131,21 @@ function parseSizeBytes(t, obj) {
86
  return null;
87
  }
88
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  function extractMovieName(t) {
90
  const firstLine = t.split('\n')[0].trim();
91
  const splitTokens = /[\s.]+(?:1080p|720p|480p|2160p|1440p|4K|WEBRip|WebDL|BluRay|BRRip|HDRip|HDTV|DVDRip|AMZN|WEB-DL|x265|x264|HEVC|AAC|AC3|EAC3|DDP|DDP5|5\.1|2\.0|H264|10bit|S\d{2}|COMPLETE|LiNE|Rapta|YTS|YIFY|RARBG|ETRG|EVO|GalaxyRG|Tigole|UTR|RMTeam|ION10|MeGusta|Vyndros|FLUX|TiPEX|eztv|EZTV)\b/i;
@@ -101,14 +161,6 @@ function extractYear(t) {
101
  return yearMatch ? yearMatch[1] : '';
102
  }
103
 
104
- function formatBytes(b) {
105
- if (!b) return '';
106
- const g = b / (1024 * 1024 * 1024);
107
- if (g >= 1) return g.toFixed(1) + ' GB';
108
- const m = b / (1024 * 1024);
109
- return m.toFixed(0) + ' MB';
110
- }
111
-
112
  function parseStreamInfo(s) {
113
  const t = s.title || s.name || '';
114
 
@@ -140,14 +192,7 @@ function parseStreamInfo(s) {
140
  if (sm) seeders = parseInt(sm[1]);
141
 
142
  let indexer = detectProvider(t);
143
- if (indexer === 'other') {
144
- const im = t.match(/⚙️\s*(\S+)/);
145
- if (im) {
146
- indexer = im[1];
147
- } else if (s._source) {
148
- indexer = s._source;
149
- }
150
- }
151
 
152
  const sizeBytes = parseSizeBytes(t, s);
153
 
@@ -207,172 +252,243 @@ function buildUnifiedTitle(s) {
207
  return lines.join('\n');
208
  }
209
 
210
- // ---------- Original filtering functions (unchanged) ----------
211
- function getQuality(title = '') {
212
- const lower = title.toLowerCase();
213
- if (lower.includes('1080p')) return '1080p';
214
- if (lower.includes('720p')) return '720p';
215
- if (lower.includes('480p')) return '480p';
216
- if (lower.includes('576p')) return '576p';
217
- if (lower.includes('webrip')) return 'webrip';
218
- if (lower.includes('webdl') || lower.includes('web-dl')) return 'webdl';
219
- if (lower.includes('bluray')) return 'bluray';
220
- return null;
221
- }
222
-
223
- function getSeeders(stream) {
224
- if (stream.behaviorHints?.seeders) return stream.behaviorHints.seeders;
225
- const match = stream.title?.match(/👤\s*(\d+)/);
226
- return match ? parseInt(match[1], 10) : 0;
227
- }
228
-
229
- function getSizeInGB(stream) {
230
- const title = stream.title || '';
231
- const sizeMatch = title.match(/💾\s*([\d.]+)\s*(GB|MB)/i) ||
232
- title.match(/([\d.]+)\s*(GB|MB)/i);
233
- if (!sizeMatch) return null;
234
-
235
- const value = parseFloat(sizeMatch[1]);
236
- const unit = sizeMatch[2].toUpperCase();
237
-
238
- if (unit === 'GB') return value;
239
- if (unit === 'MB') return value / 1024;
240
- return null;
241
- }
242
 
243
- function detectSource(title = '') {
244
- const lower = title.toLowerCase();
245
- for (const src of SOURCE_CONFIG) {
246
- if (lower.includes(src.name)) return src.name;
247
  }
248
- return null;
249
- }
250
-
251
- function getSourceOrder(sourceName) {
252
- const config = SOURCE_CONFIG.find(c => c.name === sourceName);
253
- return config ? config.order : 999;
254
- }
255
-
256
- function filterStream(stream, type = 'movie') {
257
- const title = stream.title || '';
258
- if (!stream.url && !stream.infoHash) return false;
259
-
260
- const quality = getQuality(title);
261
- if (!ALLOWED_QUALITIES.includes(quality)) return false;
262
-
263
- if (getSeeders(stream) < MIN_SEEDERS) return false;
264
-
265
- if (!detectSource(title)) return false;
266
-
267
- const maxSize = type === 'series' ? MAX_SIZE_GB_SERIES : MAX_SIZE_GB_MOVIE;
268
- const sizeGB = getSizeInGB(stream);
269
- if (sizeGB !== null && sizeGB > maxSize) return false;
270
-
271
- return true;
272
- }
273
 
274
- async function fetchUpstream(type, id) {
275
- for (const baseUrl of UPSTREAM_URLS) {
276
- try {
277
- const controller = new AbortController();
278
- const timeout = setTimeout(() => controller.abort(), 8000);
279
- const res = await fetch(`${baseUrl}/stream/${type}/${id}.json`, { signal: controller.signal });
280
- clearTimeout(timeout);
281
- if (res.ok) {
282
- const data = await res.json();
283
- if (data.streams?.length) return data.streams;
284
- }
285
- } catch (e) {}
 
 
 
 
 
 
 
 
286
  }
287
- return [];
288
- }
289
 
290
- function sortAndGroup(streams, type = 'movie') {
291
- const filtered = streams.filter(s => filterStream(s, type));
292
-
293
- const byQuality = {};
294
- filtered.forEach(stream => {
295
- const quality = getQuality(stream.title);
296
- if (!byQuality[quality]) byQuality[quality] = [];
297
- byQuality[quality].push(stream);
298
- });
299
-
300
- for (const quality in byQuality) {
301
- byQuality[quality].sort((a, b) => {
302
- const sourceA = detectSource(a.title);
303
- const sourceB = detectSource(b.title);
304
- const orderA = getSourceOrder(sourceA);
305
- const orderB = getSourceOrder(sourceB);
306
- if (orderA !== orderB) return orderA - orderB;
307
- return getSeeders(b) - getSeeders(a);
308
- });
309
- }
310
-
311
- const result = [];
312
- const sortedQualities = Object.keys(byQuality).sort((a, b) => {
313
- const rankA = QUALITY_RANK[a] || 999;
314
- const rankB = QUALITY_RANK[b] || 999;
315
- return rankA - rankB;
316
  });
317
-
318
- sortedQualities.forEach(quality => {
319
- const topLinks = byQuality[quality].slice(0, LINKS_PER_QUALITY);
320
- result.push(...topLinks);
321
- });
322
-
323
  return result;
324
  }
325
 
326
  app.get('/manifest.json', (req, res) => {
327
  res.json({
328
- id: 'org.ghostream.platinum',
329
- name: 'Ghostream 🚀',
330
- description: 'Stream Everything You Want\nMade With 🤍 By Van',
331
- version: '7.3.0',
332
- resources: ['stream'],
333
  types: ['movie', 'series'],
334
  idPrefixes: ['tt'],
335
  catalogs: []
336
  });
337
  });
338
 
339
- app.get('/stream/:type/:id.json', async (req, res) => {
 
 
 
 
 
 
 
 
 
 
340
  const { type, id } = req.params;
 
 
 
 
 
341
  try {
342
- const streams = await fetchUpstream(type, id);
343
- const sorted = sortAndGroup(streams, type);
344
- // Apply the beautiful title template and clean short name
345
- const formatted = sorted.map(s => {
346
- const info = parseStreamInfo(s);
347
- let cleanName = '';
348
- if (info.resolution) {
349
- cleanName = info.resolution;
350
- if (info.ripType) cleanName += ' ' + info.ripType;
351
- } else if (info.ripType) {
352
- cleanName = info.ripType;
353
- } else {
354
- cleanName = (s.title || s.name || '').split('\n')[0].trim();
355
- }
356
- return {
357
- ...s,
358
- name: '✨ ' + cleanName,
359
- title: buildUnifiedTitle(s)
360
- };
361
  });
362
- res.json({ streams: formatted });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
363
  } catch (e) {
364
- res.json({ streams: [] });
 
 
 
 
 
 
 
 
 
 
 
365
  }
366
- });
367
 
368
- app.get('/debug/:type/:id.json', async (req, res) => {
369
- const { type, id } = req.params;
370
  try {
371
- const streams = await fetchUpstream(type, id);
372
- res.json({ streams });
 
 
 
 
 
 
 
 
 
 
 
 
373
  } catch (e) {
374
- res.status(500).json({ error: e.message });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
375
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
376
  });
377
 
378
- app.listen(PORT, () => console.log(`Ghostream 🚀 on ${PORT}`));
 
 
 
1
  const express = require('express');
2
+ const fetch = require('node-fetch'); // Native dependency swap
 
3
  const app = express();
4
  const PORT = process.env.PORT || 7860;
5
 
6
+ // --- CORS & STREMIO APP COMPATIBILITY HEADERS ---
7
  app.use((req, res, next) => {
8
+ res.setHeader('Access-Control-Allow-Origin', '*');
9
+ res.setHeader('Access-Control-Allow-Headers', '*');
10
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
11
+ next();
 
12
  });
13
 
14
+ // --- EXTRAS FOR SUBTITLE PIPELINE ---
15
+ const STREAM_HISTORY_CACHE = new Map();
16
+
17
+ // Private API Tokens wired natively
18
+ const SUBDL_API_KEY = "subdl_2o2IC1-F6J1ru2YgeX6cP-I08DdL4VO0TJJqRLLcfAk";
19
+ const SUBSOURCE_API_KEY = "sk_9a28bb0b487bce3c9d908cf36c90b61d1604fb780ae412c2a8b71ad4636a2d2a";
20
+ // ------------------------------------
21
+
22
+ const MAX_MOVIE_SIZE_GB = 4;
23
+ const MAX_SERIES_SIZE_GB = 1.5;
24
+ const PER_SOURCE_LIMIT = 999;
25
+ const LINKS_PER_QUALITY = 2;
26
+ const MIN_SEEDERS = 2;
27
+ const BLOCK_SIZELESS = true;
28
+
29
+ const QUALITY_ORDER = [
30
+ '1080p', '720p', '576p', '480p', '360p', '240p',
31
+ '1080', '720', '480', '360',
32
+ 'webdl', 'webrip', 'bluray'
33
  ];
34
+ const EXCLUDE_KEYWORDS = ['4k', '2160p', 'screenr', 'screener', 'cam', '3d'];
35
+ const PRIORITY_PROVIDERS = ['yts', 'eztv', 'nyasi'];
36
+ const BLOCKED_PROVIDERS = [
37
+ 'rarbg','1337x', 'kickasstorrents', 'torrentgalaxy', 'magnetdl', 'horriblesubs',
38
+ 'tokyotosho', 'anidex', 'rutor', 'rutracker',
39
+ 'comando', 'bludv', 'micoleaodublado',
40
+ 'torrent9', 'ilcorsaronero',
41
+ 'mejortorrent', 'wolfmax4k', 'cinecalidad', 'besttorrents',
 
 
 
 
 
 
 
 
 
 
 
42
  ];
43
 
44
+ function extractQuality(t) {
45
+ const m = t.match(/\b(\d{3,4}p)\b/i);
46
+ if (m) return m[1].toLowerCase();
47
+ const l = t.toLowerCase();
48
+ return QUALITY_ORDER.find(q => l.includes(q)) || 'other';
49
+ }
 
 
 
50
 
51
+ function qualitySortKey(q) {
52
+ const i = QUALITY_ORDER.indexOf(q);
53
+ return i >= 0 ? i : QUALITY_ORDER.length;
54
+ }
55
 
 
56
  function detectProvider(t) {
57
  const m = t.match(/\[([^\]]+)\]/);
58
+ if (m) {
59
+ const baseMatch = m[1].toLowerCase().match(/^([a-z]+)/);
60
+ if (baseMatch) return baseMatch[1];
61
+ }
62
+ const ep = t.match(/⚙️\s*(\S+)/);
63
+ if (ep) return ep[1].toLowerCase();
64
+ return 'other';
65
+ }
66
+
67
+ function providerSortKey(p) {
68
+ const i = PRIORITY_PROVIDERS.indexOf(p);
69
+ return i >= 0 ? i : PRIORITY_PROVIDERS.length;
70
+ }
71
+
72
+ function parseSizeGB(t, obj) {
73
+ if (t) {
74
+ const labeled = t.match(/(?:💾|📦|Size:|size:)\s*([\d.]+)\s*(GB|MB|TB)/i);
75
+ if (labeled) {
76
+ const v = parseFloat(labeled[1]), u = labeled[2].toUpperCase();
77
+ if (u === 'TB') return v * 1024;
78
+ if (u === 'GB') return v;
79
+ if (u === 'MB') return v / 1024;
80
+ }
81
+ }
82
+ if (obj && obj.size) {
83
+ const m = String(obj.size).match(/^([\d.]+)\s*(GB|MB|TB)$/i);
84
+ if (m) {
85
+ const v = parseFloat(m[1]), u = m[2].toUpperCase();
86
+ if (u === 'TB') return v * 1024;
87
+ if (u === 'GB') return v;
88
+ if (u === 'MB') return v / 1024;
89
+ }
90
+ }
91
+ if (obj && obj.description) {
92
+ const descMatch = String(obj.description).match(/\b([\d.]+)\s*(GB|MB|TB)\b/i);
93
+ if (descMatch) {
94
+ const v = parseFloat(descMatch[1]), u = descMatch[2].toUpperCase();
95
+ if (u === 'TB') return v * 1024;
96
+ if (u === 'GB') return v;
97
+ if (u === 'MB') return v / 1024;
98
+ }
99
+ }
100
+ return null;
101
  }
102
 
103
  function parseSizeBytes(t, obj) {
 
131
  return null;
132
  }
133
 
134
+ function isProviderBlocked(t) {
135
+ const p = detectProvider(t);
136
+ if (BLOCKED_PROVIDERS.includes(p)) return true;
137
+ const l = t.toLowerCase();
138
+ return BLOCKED_PROVIDERS.some(b => l.includes(b));
139
+ }
140
+
141
+ function formatBytes(b) {
142
+ if (!b) return '';
143
+ const g = b / (1024 * 1024 * 1024);
144
+ if (g >= 1) return g.toFixed(1) + ' GB';
145
+ const m = b / (1024 * 1024);
146
+ return m.toFixed(0) + ' MB';
147
+ }
148
+
149
  function extractMovieName(t) {
150
  const firstLine = t.split('\n')[0].trim();
151
  const splitTokens = /[\s.]+(?:1080p|720p|480p|2160p|1440p|4K|WEBRip|WebDL|BluRay|BRRip|HDRip|HDTV|DVDRip|AMZN|WEB-DL|x265|x264|HEVC|AAC|AC3|EAC3|DDP|DDP5|5\.1|2\.0|H264|10bit|S\d{2}|COMPLETE|LiNE|Rapta|YTS|YIFY|RARBG|ETRG|EVO|GalaxyRG|Tigole|UTR|RMTeam|ION10|MeGusta|Vyndros|FLUX|TiPEX|eztv|EZTV)\b/i;
 
161
  return yearMatch ? yearMatch[1] : '';
162
  }
163
 
 
 
 
 
 
 
 
 
164
  function parseStreamInfo(s) {
165
  const t = s.title || s.name || '';
166
 
 
192
  if (sm) seeders = parseInt(sm[1]);
193
 
194
  let indexer = detectProvider(t);
195
+ if (indexer === 'other' && s._source) indexer = s._source;
 
 
 
 
 
 
 
196
 
197
  const sizeBytes = parseSizeBytes(t, s);
198
 
 
252
  return lines.join('\n');
253
  }
254
 
255
+ function filterAndArrange(streams, isSeries) {
256
+ const maxSize = isSeries ? MAX_SERIES_SIZE_GB : MAX_MOVIE_SIZE_GB;
257
+
258
+ const filtered = streams.filter(s => {
259
+ const t = s.title || s.name || '';
260
+ if (EXCLUDE_KEYWORDS.some(k => t.toLowerCase().includes(k))) return false;
261
+ if (isProviderBlocked(t)) return false;
262
+ const gb = parseSizeGB(t, s);
263
+ if (gb === null && BLOCK_SIZELESS) return false;
264
+ if (gb !== null && gb > maxSize) return false;
265
+ return true;
266
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
 
268
+ const groups = {};
269
+ for (const s of filtered) {
270
+ const q = extractQuality(s.title || s.name || '');
271
+ (groups[q] ||= []).push(s);
272
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
 
274
+ const result = [];
275
+ for (const [q, items] of Object.entries(groups)) {
276
+ const buckets = {};
277
+ for (const s of items) {
278
+ const src = s._source;
279
+ (buckets[src] ||= []).push(s);
280
+ }
281
+ const picks = [];
282
+ for (const [src, bucket] of Object.entries(buckets)) {
283
+ bucket.sort((a, b) => {
284
+ const pa = detectProvider(a.title || a.name || '');
285
+ const pb = detectProvider(b.title || b.name || '');
286
+ const skA = providerSortKey(pa);
287
+ const skB = providerSortKey(pb);
288
+ if (skA !== skB) return skA - skB;
289
+ return pa.localeCompare(pb);
290
+ });
291
+ picks.push(...bucket.slice(0, LINKS_PER_QUALITY));
292
+ }
293
+ result.push(...picks);
294
  }
 
 
295
 
296
+ result.sort((a, b) => {
297
+ return qualitySortKey(extractQuality(a.title || a.name || '')) -
298
+ qualitySortKey(extractQuality(b.title || b.name || ''));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
299
  });
300
+
 
 
 
 
 
301
  return result;
302
  }
303
 
304
  app.get('/manifest.json', (req, res) => {
305
  res.json({
306
+ id: 'org.ghostream.plus',
307
+ name: 'Ghostream+ 🚀',
308
+ description: 'Stream Everything You Want with Human Synced Subtitles\nMade With 🤍 By Van',
309
+ version: '8.0.0',
310
+ resources: ['stream', 'subtitles'],
311
  types: ['movie', 'series'],
312
  idPrefixes: ['tt'],
313
  catalogs: []
314
  });
315
  });
316
 
317
+ // --- CORE UPGRADED CONTROLLERS ---
318
+
319
+ app.get('/subtitles/:type/:id.json', async (req, res) => {
320
+ await handleSubtitleRequest(req, res);
321
+ });
322
+
323
+ app.get('/subtitles/:type/:id/:extra.json', async (req, res) => {
324
+ await handleSubtitleRequest(req, res);
325
+ });
326
+
327
+ async function handleSubtitleRequest(req, res) {
328
  const { type, id } = req.params;
329
+ let imdbId = id.split(':')[0];
330
+
331
+ const metadata = STREAM_HISTORY_CACHE.get(imdbId) || { originalTitle: "" };
332
+ const targetReleaseGroup = extractReleaseGroupTag(metadata.originalTitle);
333
+
334
  try {
335
+ const [subDlRes, subsourceRes] = await Promise.allSettled([
336
+ fetchSubDL(id, type),
337
+ fetchSubsource(id, type)
338
+ ]);
339
+
340
+ let rawList = [];
341
+ if (subDlRes.status === 'fulfilled') rawList.push(...subDlRes.value);
342
+ if (subsourceRes.status === 'fulfilled') rawList.push(...subsourceRes.value);
343
+
344
+ const humanVerifiedSubs = rawList.filter(item => {
345
+ const name = (item.filename || "").toLowerCase();
346
+ return !name.includes("ai-translated") && !name.includes("machine translation") && !name.includes("chatgpt");
 
 
 
 
 
 
 
347
  });
348
+
349
+ humanVerifiedSubs.sort((a, b) => {
350
+ const aMatch = targetReleaseGroup && a.filename.toLowerCase().includes(targetReleaseGroup.toLowerCase()) ? 1 : 0;
351
+ const bMatch = targetReleaseGroup && b.filename.toLowerCase().includes(targetReleaseGroup.toLowerCase()) ? 1 : 0;
352
+ return bMatch - aMatch;
353
+ });
354
+
355
+ const output = humanVerifiedSubs.map((sub, index) => ({
356
+ id: `gh_sub_${index}`,
357
+ url: sub.downloadUrl,
358
+ lang: sub.langCode === 'ara' ? 'Arabic' : 'English'
359
+ }));
360
+
361
+ res.json({ subtitles: output });
362
+
363
+ } catch (err) {
364
+ res.json({ subtitles: [] });
365
+ }
366
+ }
367
+
368
+ function extractReleaseGroupTag(title) {
369
+ if (!title) return "";
370
+ const match = title.match(/\b(PSA|QxR|GalaxyRG|YTS|YIFY|RARBG|FGT|Erai-raws|MeGusta|Tigole)\b/i);
371
+ return match ? match[1] : "";
372
+ }
373
+
374
+ // Adapted to native node-fetch syntax
375
+ async function fetchSubDL(stremioId, type) {
376
+ const parts = stremioId.split(':');
377
+ const imdbId = parts[0];
378
+
379
+ let url = `https://api.subdl.com/v1/subtitles?api_key=${SUBDL_API_KEY}&imdb_id=${imdbId}&languages=eng,ara`;
380
+
381
+ if (type === 'series' && parts[1] && parts[2]) {
382
+ url += `&season=${parts[1]}&episode=${parts[2]}`;
383
+ }
384
+
385
+ try {
386
+ const res = await fetch(url, { timeout: 7000 });
387
+ const data = await res.json();
388
+ if (!data || !data.results) return [];
389
+
390
+ let items = [];
391
+ data.results.forEach(sub => {
392
+ const code = sub.lang === 'arabic' ? 'ara' : 'eng';
393
+ items.push({
394
+ filename: sub.filename || sub.release_name || "Subtitle Track",
395
+ downloadUrl: sub.url || sub.download_url,
396
+ langCode: code
397
+ });
398
+ });
399
+ return items;
400
  } catch (e) {
401
+ return [];
402
+ }
403
+ }
404
+
405
+ // Adapted to native node-fetch syntax
406
+ async function fetchSubsource(stremioId, type) {
407
+ const parts = stremioId.split(':');
408
+ const imdbId = parts[0];
409
+
410
+ let url = `https://api.subsource.net/api/v1/subtitles/imdb/${imdbId}`;
411
+ if (type === 'series' && parts[1] && parts[2]) {
412
+ url += `?season=${parts[1]}&episode=${parts[2]}`;
413
  }
 
414
 
 
 
415
  try {
416
+ const res = await fetch(url, {
417
+ headers: { 'Authorization': `Bearer ${SUBSOURCE_API_KEY}` },
418
+ timeout: 7000
419
+ });
420
+ const data = await res.json();
421
+ if (!data || !data.subtitles) return [];
422
+
423
+ return data.subtitles
424
+ .filter(s => s.lang === 'english' || s.lang === 'arabic')
425
+ .map(s => ({
426
+ filename: s.release || s.filename || "Subsource Track",
427
+ downloadUrl: s.downloadUrl || s.url,
428
+ langCode: s.lang === 'arabic' ? 'ara' : 'eng'
429
+ }));
430
  } catch (e) {
431
+ return [];
432
+ }
433
+ }
434
+
435
+ // --- STREAM INTERCEPTOR ---
436
+ app.get('/stream/:type/:id.json', async (req, res) => {
437
+ const { type, id } = req.params;
438
+ const qs = req.url.split('?')[1] || '';
439
+ const isSeries = (type === 'series');
440
+ const imdbId = id.split(':')[0];
441
+
442
+ const sources = [
443
+ { name: 'torrentio', url: `https://torrentio.strem.fun/stream/${type}/${id}.json?${qs}&maxSize=4GB,1.5GB` }
444
+ ];
445
+
446
+ const results = await Promise.allSettled(
447
+ sources.map(src =>
448
+ fetch(src.url, { timeout: 12000 })
449
+ .then(async r => {
450
+ const data = await r.json();
451
+ const streams = data.streams || [];
452
+ return streams.map(s => ({ ...s, _source: src.name }));
453
+ })
454
+ .catch(() => [])
455
+ )
456
+ );
457
+
458
+ let all = [];
459
+ results.forEach(r => {
460
+ if (r.status === 'fulfilled') all = all.concat(r.value);
461
+ });
462
+
463
+ const filtered = filterAndArrange(all, isSeries);
464
+
465
+ if (filtered.length > 0) {
466
+ STREAM_HISTORY_CACHE.set(imdbId, {
467
+ originalTitle: filtered[0].title || filtered[0].name || ""
468
+ });
469
  }
470
+
471
+ const unified = filtered.map(s => {
472
+ const info = parseStreamInfo(s);
473
+ let cleanName = '';
474
+ if (info.resolution) {
475
+ cleanName = info.resolution;
476
+ if (info.ripType) cleanName += ' ' + info.ripType;
477
+ } else if (info.ripType) {
478
+ cleanName = info.ripType;
479
+ } else {
480
+ cleanName = (s.title || s.name || '').split('\n')[0].trim();
481
+ }
482
+ return {
483
+ ...s,
484
+ name: cleanName,
485
+ title: buildUnifiedTitle(s)
486
+ };
487
+ });
488
+
489
+ res.json({ streams: unified });
490
  });
491
 
492
+ app.listen(PORT, '0.0.0.0', () => {
493
+ console.log(`Ghostream+ Engine deployed cleanly on port ${PORT}`);
494
+ });