vanseleem commited on
Commit
7b9648c
Β·
verified Β·
1 Parent(s): e99000d

Update index.js

Browse files
Files changed (1) hide show
  1. index.js +267 -117
index.js CHANGED
@@ -1,202 +1,352 @@
 
 
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}`));
 
 
 
1
+ 'use strict';
2
+
3
  const express = require('express');
4
+ const fetch = require('node-fetch');
5
 
6
+ const app = express();
7
  const PORT = process.env.PORT || 7860;
8
 
9
+ // ─── CORS ──────────────────────────────────────────────────────────────────────
10
  app.use((req, res, next) => {
11
+ res.header('Access-Control-Allow-Origin', '*');
12
  res.header('Access-Control-Allow-Headers', '*');
13
  res.header('Access-Control-Allow-Methods', 'GET, OPTIONS');
14
  if (req.method === 'OPTIONS') return res.sendStatus(200);
15
  next();
16
  });
17
 
18
+ // ─── CONFIG ────────────────────────────────────────────────────────────────────
19
+
20
  const UPSTREAM_URLS = [
21
+ 'https://torrentio.strem.fun/providers=eztv,thepiratebay,nyaasi|qualityfilter=scr,cam,threed,unknown',
22
+ 'https://vanseleem-gsplus.hf.space/',
23
  ];
24
 
25
+ const SUBTITLES_URL = 'https://vanseleem-vansubs.hf.space';
26
+
27
+ const MIN_SEEDERS = 2;
28
+ const BLOCK_SIZELESS = true;
29
+ const MAX_SIZE_GB_MOVIE = 4;
30
  const MAX_SIZE_GB_SERIES = 1.5;
31
+ const LINKS_PER_QUALITY = 5;
32
+ const FETCH_TIMEOUT_MS = 12_000;
33
+
34
+ const ALLOWED_QUALITIES = ['4k', '2160p', '1080p', '720p', '576p', '480p'];
35
 
36
  const SOURCE_CONFIG = [
37
+ { name: 'yts', display: 'YTS', order: 1 },
38
+ { name: 'yify', display: 'YTS', order: 1 },
39
+ { name: 'knaben', display: 'Knaben', order: 2 },
40
+ { name: 'eztv', display: 'EZTV', order: 3 },
41
+ { name: 'nyaasi', display: 'Nyaa', order: 4 },
42
+ { name: 'thepiratebay', display: 'TPB', order: 5 },
43
+ { name: 'piratebay', display: 'TPB', order: 5 },
44
+ { name: 'the piratebay',display: 'TPB', order: 5 },
45
+ { name: 'tpb', display: 'TPB', order: 5 },
46
+ ];
47
+
48
+ const BLOCKED_PROVIDERS = [
49
+ 'rarbg', '1337x', 'kickasstorrents', 'torrentgalaxy', 'magnetdl',
50
+ 'horriblesubs', 'tokyotosho', 'anidex', 'rutor', 'rutracker',
51
+ 'comando', 'bludv', 'micoleaodublado',
52
+ 'torrent9', 'ilcorsaronero',
53
+ 'mejortorrent', 'wolfmax4k', 'cinecalidad', 'besttorrents',
54
  ];
55
 
56
  const QUALITY_RANK = {
57
+ '4k': 1, '2160p': 1,
58
+ '1080p': 2,
59
+ '720p': 3,
60
+ 'webdl': 4, 'webrip': 4,
61
+ '576p': 5,
62
+ '480p': 6,
63
+ 'bluray': 7,
64
  };
65
 
66
+ // ─── STREAM HELPERS ────────────────────────────────────────────────────────────
67
 
68
  function getQuality(title = '') {
69
+ const t = title.toLowerCase();
70
+ if (t.includes('4k') || t.includes('2160p')) return '4k';
71
+ if (t.includes('1080p')) return '1080p';
72
+ if (t.includes('720p')) return '720p';
73
+ if (t.includes('576p')) return '576p';
74
+ if (t.includes('480p')) return '480p';
75
+ if (t.includes('webrip')) return 'webrip';
76
+ if (t.includes('webdl') || t.includes('web-dl')) return 'webdl';
77
+ if (t.includes('bluray')) return 'bluray';
78
  return null;
79
  }
80
 
81
  function getSeeders(stream) {
82
  if (stream.behaviorHints?.seeders) return stream.behaviorHints.seeders;
83
+ const m = (stream.title || '').match(/πŸ‘€\s*(\d+)/);
84
+ return m ? parseInt(m[1], 10) : 0;
85
  }
86
 
87
+ function getSizeGB(stream) {
88
  const title = stream.title || '';
89
+ const m = title.match(/πŸ’Ύ\s*([\d.]+)\s*(GB|MB)/i) ||
90
+ title.match(/([\d.]+)\s*(GB|MB)/i);
91
+ if (!m) return null;
92
+ const val = parseFloat(m[1]);
93
+ const unit = m[2].toUpperCase();
94
+ return unit === 'GB' ? val : val / 1024;
 
 
 
 
95
  }
96
 
97
  function detectSource(title = '') {
98
  const lower = title.toLowerCase();
99
  for (const src of SOURCE_CONFIG) {
100
+ if (lower.includes(src.name)) return src;
101
  }
102
  return null;
103
  }
104
 
105
+ function isBlocked(stream) {
106
+ const haystack = (
107
+ (stream.behaviorHints?.bingeGroup || '') + ' ' +
108
+ (stream.title || '') + ' ' +
109
+ (stream.name || '')
110
+ ).toLowerCase();
111
+ return BLOCKED_PROVIDERS.some(p => haystack.includes(p));
112
  }
113
 
114
+ // ─── FILTER & SORT ─────────────────────────────────────────────────────────────
115
+
116
+ function filterStream(stream, type) {
117
  if (!stream.url && !stream.infoHash) return false;
118
+ if (isBlocked(stream)) return false;
119
+
120
+ const quality = getQuality(stream.title);
121
+ if (!quality || !ALLOWED_QUALITIES.includes(quality)) return false;
122
+
123
  if (getSeeders(stream) < MIN_SEEDERS) return false;
124
+
125
+ const sizeGB = getSizeGB(stream);
126
+ if (BLOCK_SIZELESS && sizeGB === null) return false;
127
  const maxSize = type === 'series' ? MAX_SIZE_GB_SERIES : MAX_SIZE_GB_MOVIE;
 
128
  if (sizeGB !== null && sizeGB > maxSize) return false;
129
+
130
  return true;
131
  }
132
 
133
+ function sortAndGroup(streams, type) {
134
+ const filtered = streams.filter(s => filterStream(s, type));
135
+
136
+ // Deduplicate by infoHash then by title+size fingerprint
137
+ const seen = new Set();
138
+ const unique = filtered.filter(s => {
139
+ const key = s.infoHash
140
+ ? s.infoHash.toLowerCase()
141
+ : `${(s.title || '').trim().toLowerCase()}::${getSizeGB(s)}`;
142
+ if (seen.has(key)) return false;
143
+ seen.add(key);
144
+ return true;
145
+ });
146
+
147
+ // Group by quality bucket
148
+ const byQuality = {};
149
+ unique.forEach(s => {
150
+ const q = getQuality(s.title);
151
+ if (!byQuality[q]) byQuality[q] = [];
152
+ byQuality[q].push(s);
153
+ });
154
+
155
+ // Within each bucket: priority source order, then seeders desc
156
+ for (const q in byQuality) {
157
+ byQuality[q].sort((a, b) => {
158
+ const srcA = detectSource(a.title);
159
+ const srcB = detectSource(b.title);
160
+ const ordA = srcA ? srcA.order : 999;
161
+ const ordB = srcB ? srcB.order : 999;
162
+ if (ordA !== ordB) return ordA - ordB;
163
+ return getSeeders(b) - getSeeders(a);
164
+ });
165
+ }
166
+
167
+ // Flatten: best quality first, capped at LINKS_PER_QUALITY per bucket
168
+ const sortedQualities = Object.keys(byQuality).sort(
169
+ (a, b) => (QUALITY_RANK[a] || 999) - (QUALITY_RANK[b] || 999)
170
+ );
171
+
172
+ const result = [];
173
+ sortedQualities.forEach(q => {
174
+ result.push(...byQuality[q].slice(0, LINKS_PER_QUALITY));
175
+ });
176
+
177
+ return result;
178
+ }
179
+
180
+ // ─── TITLE BUILDER ─────────────────────────────────────────────────────────────
181
+
182
+ function formatBytes(gb) {
183
+ if (gb === null) return null;
184
+ return gb >= 1 ? `${gb.toFixed(2)} GB` : `${(gb * 1024).toFixed(0)} MB`;
185
+ }
186
+
187
+ function extractMovieName(title = '') {
188
+ return title
189
+ .replace(/\b(4k|2160p|1080p?|720p?|576p|480p?|360p?|240p?)\b.*/i, '')
190
+ .replace(/\b(bluray|webrip|webdl|web-dl|hdtv|dvdrip)\b.*/i, '')
191
+ .replace(/[\.\-_]+/g, ' ')
192
+ .replace(/\s*\(\d{4}\)\s*$/, '')
193
+ .trim();
194
+ }
195
+
196
+ function extractYear(title = '') {
197
+ const m = title.match(/\b(19\d{2}|20\d{2})\b/);
198
+ return m ? m[1] : '';
199
+ }
200
+
201
+ function getAudio(title = '') {
202
+ if (/DDP?\s*Atmos/i.test(title)) return 'Atmos';
203
+ if (/DDP|EAC3/i.test(title)) return 'DD+';
204
+ if (/DTS[-\s]?HD/i.test(title)) return 'DTS-HD';
205
+ if (/DTS/i.test(title)) return 'DTS';
206
+ if (/TrueHD/i.test(title)) return 'TrueHD';
207
+ if (/AAC/i.test(title)) return 'AAC';
208
+ if (/AC3|Dolby/i.test(title)) return 'DD';
209
+ return '';
210
+ }
211
+
212
+ function getLanguages(title = '') {
213
+ const map = {
214
+ english: 'EN', french: 'FR', spanish: 'ES', german: 'DE',
215
+ italian: 'IT', portuguese: 'PT', russian: 'RU', japanese: 'JA',
216
+ korean: 'KO', chinese: 'ZH', hindi: 'HI', arabic: 'AR',
217
+ };
218
+ const lower = title.toLowerCase();
219
+ return Object.entries(map).filter(([k]) => lower.includes(k)).map(([, v]) => v);
220
+ }
221
+
222
+ function buildTitle(stream) {
223
+ const raw = stream._rawTitle || stream.title || '';
224
+ const name = extractMovieName(raw);
225
+ const year = extractYear(raw);
226
+ const disp = year ? `${name} (${year})` : name;
227
+
228
+ const seeders = getSeeders(stream);
229
+ const sizeGB = getSizeGB(stream);
230
+ const src = detectSource(raw);
231
+ const srcLabel = src ? src.display : 'Unknown';
232
+ const audio = getAudio(raw);
233
+ const langs = getLanguages(raw);
234
+
235
+ const lines = [
236
+ `πŸ”† ${disp}`,
237
+ `🌱 ${seeders}`,
238
+ sizeGB !== null ? `πŸ’Ύ ${formatBytes(sizeGB)}` : '⚠️ Size unknown',
239
+ `πŸ… ${srcLabel}`,
240
+ ];
241
+
242
+ let l5 = '';
243
+ if (langs.length) l5 += `πŸ‡¬πŸ‡§ ${langs.join(' β€’ ')}`;
244
+ if (audio) l5 += (l5 ? ' ' : '') + `πŸ”Š ${audio}`;
245
+ if (l5) lines.push(l5);
246
+
247
+ return lines.join('\n');
248
+ }
249
+
250
+ // ─── UPSTREAM FETCH ────────────────────────────────────────────────────────────
251
+
252
  async function fetchUpstream(type, id) {
253
+ for (const base of UPSTREAM_URLS) {
254
  try {
255
  const controller = new AbortController();
256
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
257
+ const url = `${base}/stream/${type}/${id}.json`;
258
+ const res = await fetch(url, { signal: controller.signal });
259
+ clearTimeout(timer);
260
  if (res.ok) {
261
  const data = await res.json();
262
+ if (data.streams?.length) {
263
+ console.log(`[upstream] ${data.streams.length} streams from ${url}`);
264
+ return data.streams;
265
+ }
266
  }
267
+ } catch (e) {
268
+ console.warn(`[upstream] failed: ${e.message}`);
269
+ }
270
  }
271
  return [];
272
  }
273
 
274
+ async function fetchSubtitles(type, id) {
275
+ try {
276
+ const controller = new AbortController();
277
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
278
+ const url = `${SUBTITLES_URL}/subtitles/${type}/${id}.json`;
279
+ const res = await fetch(url, { signal: controller.signal });
280
+ clearTimeout(timer);
281
+ if (res.ok) {
282
+ const data = await res.json();
283
+ return data.subtitles || [];
284
+ }
285
+ } catch (e) {
286
+ console.warn(`[subtitles] failed: ${e.message}`);
 
 
 
 
 
 
287
  }
288
+ return [];
 
 
 
 
 
 
 
 
 
 
 
 
 
289
  }
290
 
291
+ // ─── ROUTES ────────────────────────────────────────────────────────────────────
292
+
293
  app.get('/manifest.json', (req, res) => {
294
  res.json({
295
+ id: 'community.ghostreamplus',
296
+ name: 'Ghostream+ 🎬',
297
+ description: 'Stream Everything you want\nYTS · Knaben\nMade with 🀍 by Van',
298
+ version: '7.0.5',
299
+ logo: 'https://i.imgur.com/wEYbVqL.png',
300
+ resources: ['stream', 'subtitles'],
301
+ types: ['movie', 'series'],
302
+ idPrefixes: ['tt'],
303
+ catalogs: [],
304
  });
305
  });
306
 
307
  app.get('/stream/:type/:id.json', async (req, res) => {
308
  const { type, id } = req.params;
309
  try {
310
+ const raw = await fetchUpstream(type, id);
311
+ const sorted = sortAndGroup(raw, type);
312
+
313
+ // Apply unified display title
314
+ const titled = sorted.map(s => {
315
+ const clone = Object.assign({}, s);
316
+ clone._rawTitle = s.title || s.name || '';
317
+ clone.title = buildTitle(clone);
318
+ return clone;
319
+ });
320
+
321
+ console.log(`[handler] ${type}/${id} β†’ ${titled.length} final streams`);
322
+ res.json({ streams: titled });
323
  } catch (e) {
324
+ console.error('[handler] error:', e);
325
  res.json({ streams: [] });
326
  }
327
  });
328
 
329
+ app.get('/subtitles/:type/:id.json', async (req, res) => {
330
+ const { type, id } = req.params;
331
+ try {
332
+ const subs = await fetchSubtitles(type, id);
333
+ res.json({ subtitles: subs });
334
+ } catch (e) {
335
+ res.json({ subtitles: [] });
336
+ }
337
+ });
338
+
339
+ // Debug: raw upstream response before any filtering
340
  app.get('/debug/:type/:id.json', async (req, res) => {
341
  const { type, id } = req.params;
342
  try {
343
  const streams = await fetchUpstream(type, id);
344
+ res.json({ count: streams.length, streams });
345
  } catch (e) {
346
  res.status(500).json({ error: e.message });
347
  }
348
  });
349
 
350
+ // ─── START ─────────────────────────────────────────────────────────────────────
351
+
352
+ app.listen(PORT, () => console.log(`Ghostream+ 🎬 running on http://0.0.0.0:${PORT}`));