vanseleem commited on
Commit
e4db899
Β·
verified Β·
1 Parent(s): 309fbf2

Update index.js

Browse files
Files changed (1) hide show
  1. index.js +150 -319
index.js CHANGED
@@ -1,371 +1,202 @@
1
  const express = require('express');
2
- const axios = require('axios');
 
3
  const app = express();
4
  const PORT = process.env.PORT || 7860;
5
 
6
- const MAX_MOVIE_SIZE_GB = 4;
7
- const MAX_SERIES_SIZE_GB = 1.5;
8
- const PER_SOURCE_LIMIT = 3;
9
- const MIN_SEEDERS = 2;
10
- const BLOCK_SIZELESS = true;
 
 
11
 
12
- const QUALITY_ORDER = [
13
- '1080p', '720p', '576p', '480p', '360p', '240p',
14
- '1080', '720', '480', '360',
15
- 'webdl', 'webrip', 'bluray'
16
  ];
17
- const EXCLUDE_KEYWORDS = ['4k', '2160p', 'screenr', 'screener', 'cam', '3d'];
18
- const PRIORITY_PROVIDERS = ['yts', 'eztv', 'nyasi'];
19
- const BLOCKED_PROVIDERS = [
20
- '1337x', 'kickasstorrents', 'torrentgalaxy', 'magnetdl', 'horriblesubs',
21
- 'tokyotosho', 'anidex', 'rutor', 'rutracker',
22
- 'comando', 'bludv', 'micoleaodublado',
23
- 'torrent9', 'ilcorsaronero',
24
- 'mejortorrent', 'wolfmax4k', 'cinecalidad', 'besttorrents',
25
- ];
26
-
27
- const SUBTITLES_URL = 'https://subsource.strem.top/c2tfOWEyOGJiMGI0ODdiY2UzYzlkOTA4Y2YzNmM5MGI2MWQxNjA0ZmI3ODBhZTQxMmMyYThiNzFhZDQ2MzZhMmQyYS9hcmFiaWMsZW5nbGlzaC9oaUluY2x1ZGUvdHlwZToxLDIsNC8';
28
 
29
- function extractQuality(t) {
30
- const m = t.match(/\b(\d{3,4}p)\b/i);
31
- if (m) return m[1].toLowerCase();
32
- const l = t.toLowerCase();
33
- return QUALITY_ORDER.find(q => l.includes(q)) || 'other';
34
- }
35
-
36
- function qualitySortKey(q) {
37
- const i = QUALITY_ORDER.indexOf(q);
38
- return i >= 0 ? i : QUALITY_ORDER.length;
39
- }
 
 
 
 
 
 
 
 
 
40
 
41
- function detectProvider(t) {
42
- const m = t.match(/\[([^\]]+)\]/);
43
- return m ? m[1].toLowerCase() : 'other';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  }
45
 
46
- function providerSortKey(p) {
47
- const i = PRIORITY_PROVIDERS.indexOf(p);
48
- return i >= 0 ? i : PRIORITY_PROVIDERS.length;
 
49
  }
50
 
51
- // Ultra‑strict size parser – only returns a value if it finds a true file size
52
- function parseSizeGB(t, obj) {
53
- // First, look for explicitly labelled sizes (with emoji or "Size:")
54
- if (t) {
55
- const labeled = t.match(/(?:πŸ’Ύ|πŸ“¦|Size:|size:)\s*([\d.]+)\s*(GB|MB|TB)/i);
56
- if (labeled) {
57
- const v = parseFloat(labeled[1]), u = labeled[2].toUpperCase();
58
- if (u === 'TB') return v * 1024;
59
- if (u === 'GB') return v;
60
- if (u === 'MB') return v / 1024;
61
- }
62
- }
63
- // If the object has a separate 'size' field, use it
64
- if (obj && obj.size) {
65
- const m = String(obj.size).match(/^([\d.]+)\s*(GB|MB|TB)$/i);
66
- if (m) {
67
- const v = parseFloat(m[1]), u = m[2].toUpperCase();
68
- if (u === 'TB') return v * 1024;
69
- if (u === 'GB') return v;
70
- if (u === 'MB') return v / 1024;
71
- }
72
- }
73
- // If the object has a description with a size, use it
74
- if (obj && obj.description) {
75
- const descMatch = String(obj.description).match(/\b([\d.]+)\s*(GB|MB|TB)\b/i);
76
- if (descMatch) {
77
- const v = parseFloat(descMatch[1]), u = descMatch[2].toUpperCase();
78
- if (u === 'TB') return v * 1024;
79
- if (u === 'GB') return v;
80
- if (u === 'MB') return v / 1024;
81
- }
82
- }
83
- // No size found anywhere – return null
84
  return null;
85
  }
86
 
87
- // Same ultra‑strict logic for bytes
88
- function parseSizeBytes(t, obj) {
89
- if (t) {
90
- const labeled = t.match(/(?:πŸ’Ύ|πŸ“¦|Size:|size:)\s*([\d.]+)\s*(GB|MB|TB)/i);
91
- if (labeled) {
92
- const v = parseFloat(labeled[1]), u = labeled[2].toUpperCase();
93
- if (u === 'TB') return v * 1024 * 1024 * 1024 * 1024;
94
- if (u === 'GB') return v * 1024 * 1024 * 1024;
95
- if (u === 'MB') return v * 1024 * 1024;
96
- }
97
- }
98
- if (obj && obj.size) {
99
- const m = String(obj.size).match(/^([\d.]+)\s*(GB|MB|TB)$/i);
100
- if (m) {
101
- const v = parseFloat(m[1]), u = m[2].toUpperCase();
102
- if (u === 'TB') return v * 1024 * 1024 * 1024 * 1024;
103
- if (u === 'GB') return v * 1024 * 1024 * 1024;
104
- if (u === 'MB') return v * 1024 * 1024;
105
- }
106
- }
107
- if (obj && obj.description) {
108
- const descMatch = String(obj.description).match(/\b([\d.]+)\s*(GB|MB|TB)\b/i);
109
- if (descMatch) {
110
- const v = parseFloat(descMatch[1]), u = descMatch[2].toUpperCase();
111
- if (u === 'TB') return v * 1024 * 1024 * 1024 * 1024;
112
- if (u === 'GB') return v * 1024 * 1024 * 1024;
113
- if (u === 'MB') return v * 1024 * 1024;
114
- }
115
  }
116
  return null;
117
  }
118
 
119
- function isProviderBlocked(t) {
120
- const p = detectProvider(t);
121
- if (BLOCKED_PROVIDERS.includes(p)) return true;
122
- const l = t.toLowerCase();
123
- return BLOCKED_PROVIDERS.some(b => l.includes(b));
124
- }
125
-
126
- function formatBytes(b) {
127
- if (!b) return '';
128
- const g = b / (1024 * 1024 * 1024);
129
- if (g >= 1) return g.toFixed(1) + ' GB';
130
- const m = b / (1024 * 1024);
131
- return m.toFixed(0) + ' MB';
132
  }
133
 
134
- function extractMovieName(t) {
135
- const firstLine = t.split('\n')[0].trim();
136
- 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;
137
- const parts = firstLine.split(splitTokens);
138
- let name = parts[0].replace(/\./g, ' ').replace(/\s+/g, ' ').trim();
139
- name = name.replace(/\s+\b(19|20)\d{2}\b$/, '').trim();
140
- return name || 'Unknown';
 
 
 
 
 
 
 
 
 
141
  }
142
 
143
- function extractYear(t) {
144
- const firstLine = t.split('\n')[0].trim();
145
- const yearMatch = firstLine.match(/\b(19\d{2}|20\d{2})\b/);
146
- return yearMatch ? yearMatch[1] : '';
147
- }
148
-
149
- function parseStreamInfo(s) {
150
- const t = s.title || s.name || '';
151
-
152
- let resolution = null;
153
- const rm = t.match(/(2160p|1440p|1080p|720p|480p|360p)/i);
154
- if (rm) resolution = rm[1];
155
-
156
- let ripType = '';
157
- const rp = ['webdl','web-dl','webrip','web-rip','bluray','blu-ray','brrip','bdrip','hdrip','hdtv','dvdrip','dvdscr','hdcam','ts','tc','cam'];
158
- const lt = t.toLowerCase();
159
- for (const p of rp) {
160
- if (lt.includes(p)) {
161
- switch (p) {
162
- case 'webdl': case 'web-dl': ripType = 'WebDL'; break;
163
- case 'webrip': case 'web-rip': ripType = 'WEBRip'; break;
164
- case 'bluray': case 'blu-ray': ripType = 'BluRay'; break;
165
- case 'brrip': case 'bdrip': ripType = 'BRRip'; break;
166
- case 'hdrip': ripType = 'HDRip'; break;
167
- case 'hdtv': ripType = 'HDTV'; break;
168
- case 'dvdrip': ripType = 'DVDRip'; break;
169
- default: ripType = p.toUpperCase();
170
  }
171
- break;
172
- }
173
  }
174
-
175
- let seeders = 0;
176
- const sm = t.match(/πŸ‘€\s*(\d+)/);
177
- if (sm) seeders = parseInt(sm[1]);
178
-
179
- let indexer = detectProvider(t);
180
- if (indexer === 'other') {
181
- const im = t.match(/βš™οΈ\s*(\S+)/);
182
- if (im) {
183
- indexer = im[1];
184
- } else if (s._source) {
185
- indexer = s._source;
186
- }
187
- }
188
-
189
- const sizeBytes = parseSizeBytes(t, s);
190
-
191
- let languages = [];
192
- if (s.languages && s.languages.length) {
193
- languages = s.languages;
194
- } else {
195
- const lm = t.match(/πŸ‡¬πŸ‡§\s*([\s\S]+?)(?:\n|$)/);
196
- if (lm) languages = lm[1].split('β€’').map(l => l.trim());
197
- }
198
-
199
- let audioInfo = '';
200
- if (s.audioTags && s.audioTags.length) {
201
- audioInfo = s.audioTags.join(' β€’ ');
202
- } else {
203
- const am = t.match(/(?:DDP|EAC3|AAC|AC3|MP3|FLAC|DTS|TrueHD)\s*\d?\.?\d?/gi);
204
- if (am) audioInfo = am.join(' β€’ ');
205
- }
206
-
207
- return { resolution, ripType, seeders, indexer, sizeBytes, languages, audioInfo };
208
  }
209
 
210
- function buildUnifiedTitle(s) {
211
- const i = parseStreamInfo(s);
212
- const originalTitle = s.title || s.name || '';
213
- const movieName = extractMovieName(originalTitle);
214
- const year = extractYear(originalTitle);
215
- const displayName = year ? `${movieName} (${year})` : movieName;
216
-
217
- const l1 = 'πŸ”† ' + displayName;
218
- const l2 = 'πŸ™‹πŸ»β€β™‚οΈ ' + (i.seeders || '0');
219
-
220
- let l3 = '';
221
- if (i.sizeBytes) {
222
- l3 = 'πŸ’Ύ ' + formatBytes(i.sizeBytes);
223
- } else {
224
- l3 = '⚠️ Size unknown';
225
- }
226
-
227
- const displayIndexer = i.indexer && i.indexer !== 'other'
228
- ? i.indexer.charAt(0).toUpperCase() + i.indexer.slice(1)
229
- : 'Unknown';
230
- const l4 = 'πŸ… ' + displayIndexer;
231
-
232
- let l5 = '';
233
- if (i.languages.length) l5 += 'πŸ‡¬πŸ‡§ ' + i.languages.join(' β€’ ');
234
- if (i.audioInfo) {
235
- if (l5) l5 += ' ';
236
- l5 += 'πŸ”Š ' + i.audioInfo;
237
- }
238
-
239
- const lines = [l1, l2];
240
- if (l3) lines.push(l3);
241
- lines.push(l4);
242
- if (l5) lines.push(l5);
243
-
244
- return lines.join('\n');
245
- }
246
-
247
- function filterAndArrange(streams, isSeries) {
248
- const maxSize = isSeries ? MAX_SERIES_SIZE_GB : MAX_MOVIE_SIZE_GB;
249
-
250
- const filtered = streams.filter(s => {
251
- const t = s.title || s.name || '';
252
- if (EXCLUDE_KEYWORDS.some(k => t.toLowerCase().includes(k))) return false;
253
- if (isProviderBlocked(t)) return false;
254
- const gb = parseSizeGB(t, s);
255
- if (gb === null && BLOCK_SIZELESS) return false;
256
- if (gb !== null && gb > maxSize) return false;
257
- return true;
258
  });
259
-
260
- const groups = {};
261
- for (const s of filtered) {
262
- const q = extractQuality(s.title || s.name || '');
263
- (groups[q] ||= []).push(s);
264
- }
265
-
 
 
 
 
 
266
  const result = [];
267
- for (const [q, items] of Object.entries(groups)) {
268
- const buckets = {};
269
- for (const s of items) {
270
- const src = s._source;
271
- (buckets[src] ||= []).push(s);
272
- }
273
- const picks = [];
274
- for (const [src, bucket] of Object.entries(buckets)) {
275
- bucket.sort((a, b) => {
276
- const pa = detectProvider(a.title || a.name || '');
277
- const pb = detectProvider(b.title || a.name || '');
278
- return providerSortKey(pa) - providerSortKey(pb);
279
- });
280
- picks.push(...bucket.slice(0, PER_SOURCE_LIMIT));
281
- }
282
- result.push(...picks);
283
- }
284
-
285
- result.sort((a, b) => {
286
- return qualitySortKey(extractQuality(a.title || a.name || '')) -
287
- qualitySortKey(extractQuality(b.title || a.name || ''));
288
  });
289
-
 
 
 
 
 
290
  return result;
291
  }
292
 
293
  app.get('/manifest.json', (req, res) => {
294
  res.json({
295
- id: 'org.ghostream.plus',
296
- name: 'Ghostream+ πŸš€',
297
  description: 'Stream Everything You Want\nMade With 🀍 By Van',
298
- version: '5.0.6',
299
- resources: ['stream', 'subtitles'],
300
  types: ['movie', 'series'],
301
  idPrefixes: ['tt'],
302
  catalogs: []
303
  });
304
  });
305
 
306
- app.get('/subtitles/:type/:id.json', async (req, res) => {
 
307
  try {
308
- const response = await axios.get(`${SUBTITLES_URL}/subtitles/${req.params.type}/${req.params.id}.json`, { timeout: 15000 });
309
- res.json(response.data);
 
310
  } catch (e) {
311
- res.json({ subtitles: [] });
312
  }
313
  });
314
 
315
- app.get('/subtitles/:type/:id/:extra.json', async (req, res) => {
 
316
  try {
317
- const response = await axios.get(`${SUBTITLES_URL}/subtitles/${req.params.type}/${req.params.id}/${req.params.extra}.json`, { timeout: 15000 });
318
- res.json(response.data);
319
  } catch (e) {
320
- res.json({ subtitles: [] });
321
  }
322
  });
323
 
324
- app.get('/stream/:type/:id.json', async (req, res) => {
325
- const { type, id } = req.params;
326
- const qs = req.url.split('?')[1] || '';
327
- const isSeries = (type === 'series');
328
-
329
- const sources = [
330
- { name: 'torrentio', url: `https://torrentio.strem.fun/stream/${type}/${id}.json?${qs}&maxSize=4GB,1.5GB` }
331
- ];
332
-
333
- const results = await Promise.allSettled(
334
- sources.map(src =>
335
- axios.get(src.url, { timeout: 12000 })
336
- .then(r => {
337
- const streams = r.data.streams || [];
338
- return streams.map(s => ({ ...s, _source: src.name }));
339
- })
340
- .catch(() => [])
341
- )
342
- );
343
-
344
- let all = [];
345
- results.forEach(r => {
346
- if (r.status === 'fulfilled') all = all.concat(r.value);
347
- });
348
-
349
- const filtered = filterAndArrange(all, isSeries);
350
- const unified = filtered.map(s => {
351
- const info = parseStreamInfo(s);
352
- let cleanName = '';
353
- if (info.resolution) {
354
- cleanName = info.resolution;
355
- if (info.ripType) cleanName += ' ' + info.ripType;
356
- } else if (info.ripType) {
357
- cleanName = info.ripType;
358
- } else {
359
- cleanName = (s.title || s.name || '').split('\n')[0].trim();
360
- }
361
- return {
362
- ...s,
363
- name: cleanName,
364
- title: buildUnifiedTitle(s)
365
- };
366
- });
367
-
368
- res.json({ streams: unified });
369
- });
370
-
371
- app.listen(PORT, '0.0.0.0', () => {});
 
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: 'rutor', display: 'Rutor', order: 8 },
38
+ { name: 'rutracker', display: 'Rutracker', order: 9 }
39
+ ];
40
 
41
+ const QUALITY_RANK = {
42
+ '1080p': 1,
43
+ '720p': 2,
44
+ 'webdl': 3,
45
+ 'webrip': 4,
46
+ '576p': 5,
47
+ '480p': 6,
48
+ 'bluray': 7
49
+ };
50
+
51
+ const LINKS_PER_QUALITY = 5;
52
+
53
+ function getQuality(title = '') {
54
+ const lower = title.toLowerCase();
55
+ if (lower.includes('1080p')) return '1080p';
56
+ if (lower.includes('720p')) return '720p';
57
+ if (lower.includes('480p')) return '480p';
58
+ if (lower.includes('576p')) return '576p';
59
+ if (lower.includes('webrip')) return 'webrip';
60
+ if (lower.includes('webdl') || lower.includes('web-dl')) return 'webdl';
61
+ if (lower.includes('bluray')) return 'bluray';
62
+ return null;
63
  }
64
 
65
+ function getSeeders(stream) {
66
+ if (stream.behaviorHints?.seeders) return stream.behaviorHints.seeders;
67
+ const match = stream.title?.match(/πŸ‘€\s*(\d+)/);
68
+ return match ? parseInt(match[1], 10) : 0;
69
  }
70
 
71
+ function getSizeInGB(stream) {
72
+ const title = stream.title || '';
73
+ const sizeMatch = title.match(/πŸ’Ύ\s*([\d.]+)\s*(GB|MB)/i) ||
74
+ title.match(/([\d.]+)\s*(GB|MB)/i);
75
+ if (!sizeMatch) return null;
76
+
77
+ const value = parseFloat(sizeMatch[1]);
78
+ const unit = sizeMatch[2].toUpperCase();
79
+
80
+ if (unit === 'GB') return value;
81
+ if (unit === 'MB') return value / 1024;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  return null;
83
  }
84
 
85
+ function detectSource(title = '') {
86
+ const lower = title.toLowerCase();
87
+ for (const src of SOURCE_CONFIG) {
88
+ if (lower.includes(src.name)) return src.name;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  }
90
  return null;
91
  }
92
 
93
+ function getSourceOrder(sourceName) {
94
+ const config = SOURCE_CONFIG.find(c => c.name === sourceName);
95
+ return config ? config.order : 999;
 
 
 
 
 
 
 
 
 
 
96
  }
97
 
98
+ function filterStream(stream, type = 'movie') {
99
+ const title = stream.title || '';
100
+ if (!stream.url && !stream.infoHash) return false;
101
+
102
+ const quality = getQuality(title);
103
+ if (!ALLOWED_QUALITIES.includes(quality)) return false;
104
+
105
+ if (getSeeders(stream) < MIN_SEEDERS) return false;
106
+
107
+ if (!detectSource(title)) return false;
108
+
109
+ const maxSize = type === 'series' ? MAX_SIZE_GB_SERIES : MAX_SIZE_GB_MOVIE;
110
+ const sizeGB = getSizeInGB(stream);
111
+ if (sizeGB !== null && sizeGB > maxSize) return false;
112
+
113
+ return true;
114
  }
115
 
116
+ async function fetchUpstream(type, id) {
117
+ for (const baseUrl of UPSTREAM_URLS) {
118
+ try {
119
+ const controller = new AbortController();
120
+ const timeout = setTimeout(() => controller.abort(), 8000);
121
+ const res = await fetch(`${baseUrl}/stream/${type}/${id}.json`, { signal: controller.signal });
122
+ clearTimeout(timeout);
123
+ if (res.ok) {
124
+ const data = await res.json();
125
+ if (data.streams?.length) return data.streams;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  }
127
+ } catch (e) {}
 
128
  }
129
+ return [];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  }
131
 
132
+ function sortAndGroup(streams, type = 'movie') {
133
+ const filtered = streams.filter(s => filterStream(s, type));
134
+
135
+ const byQuality = {};
136
+ filtered.forEach(stream => {
137
+ const quality = getQuality(stream.title);
138
+ if (!byQuality[quality]) byQuality[quality] = [];
139
+ byQuality[quality].push(stream);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  });
141
+
142
+ for (const quality in byQuality) {
143
+ byQuality[quality].sort((a, b) => {
144
+ const sourceA = detectSource(a.title);
145
+ const sourceB = detectSource(b.title);
146
+ const orderA = getSourceOrder(sourceA);
147
+ const orderB = getSourceOrder(sourceB);
148
+ if (orderA !== orderB) return orderA - orderB;
149
+ return getSeeders(b) - getSeeders(a);
150
+ });
151
+ }
152
+
153
  const result = [];
154
+ const sortedQualities = Object.keys(byQuality).sort((a, b) => {
155
+ const rankA = QUALITY_RANK[a] || 999;
156
+ const rankB = QUALITY_RANK[b] || 999;
157
+ return rankA - rankB;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
  });
159
+
160
+ sortedQualities.forEach(quality => {
161
+ const topLinks = byQuality[quality].slice(0, LINKS_PER_QUALITY);
162
+ result.push(...topLinks);
163
+ });
164
+
165
  return result;
166
  }
167
 
168
  app.get('/manifest.json', (req, res) => {
169
  res.json({
170
+ id: 'org.ghostream.platinum',
171
+ name: 'Ghostream πŸš€',
172
  description: 'Stream Everything You Want\nMade With 🀍 By Van',
173
+ version: '7.3.0',
174
+ resources: ['stream'],
175
  types: ['movie', 'series'],
176
  idPrefixes: ['tt'],
177
  catalogs: []
178
  });
179
  });
180
 
181
+ app.get('/stream/:type/:id.json', async (req, res) => {
182
+ const { type, id } = req.params;
183
  try {
184
+ const streams = await fetchUpstream(type, id);
185
+ const sorted = sortAndGroup(streams, type);
186
+ res.json({ streams: sorted });
187
  } catch (e) {
188
+ res.json({ streams: [] });
189
  }
190
  });
191
 
192
+ app.get('/debug/:type/:id.json', async (req, res) => {
193
+ const { type, id } = req.params;
194
  try {
195
+ const streams = await fetchUpstream(type, id);
196
+ res.json({ streams });
197
  } catch (e) {
198
+ res.status(500).json({ error: e.message });
199
  }
200
  });
201
 
202
+ app.listen(PORT, () => console.log(`Ghostream πŸš€ on ${PORT}`));