vanseleem commited on
Commit
e99000d
Β·
verified Β·
1 Parent(s): 11e1e8d

Update index.js

Browse files
Files changed (1) hide show
  1. index.js +167 -269
index.js CHANGED
@@ -1,304 +1,202 @@
1
  const express = require('express');
2
  const fetch = require('node-fetch');
 
3
  const app = express();
4
  const PORT = process.env.PORT || 7860;
5
 
6
- // ─── CORS ────────────────────────────────────────────────────────────────────
7
  app.use((req, res, next) => {
8
- res.setHeader('Access-Control-Allow-Origin', '*');
9
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
10
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
11
  if (req.method === 'OPTIONS') return res.sendStatus(200);
12
  next();
13
  });
14
 
15
- // ═══════════════════════════════════════════════════════════════════════════════
16
- // KEYS β€” Replace OS_API_KEY with your free key from opensubtitles.com
17
- // ═══════════════════════════════════════════════════════════════════════════════
18
- const SUBDL_KEY = 'subdl_2o2IC1-F6J1ru2YgeX6cP-I08DdL4VO0TJJqRLLcfAk';
19
- const OS_API_KEY = 'FJqHyO18FxNEVdGQEPJ1hEM4tc6RqIZF';
20
- const OS_APP_NAME = 'VanSubs'; // your app name for the User-Agent header
21
- const OS_BASE = 'https://api.opensubtitles.com/api/v1';
22
-
23
- // ─── MANIFEST ────────────────────────────────────────────────────────────────
24
- app.get('/', (req, res) => res.send('Van Subs Engine v7 βœ… Online'));
25
-
26
- app.get('/manifest.json', (req, res) => res.json({
27
- id: 'org.van.humansubtitles',
28
- name: 'Van Subs 🎬',
29
- description: 'Human-Verified Arabic & English. Anti-AI. Hash-Precision Sync.',
30
- version: '7.0.0',
31
- resources: ['subtitles'],
32
- types: ['movie', 'series'],
33
- idPrefixes: ['tt'],
34
- catalogs: []
35
- }));
36
-
37
- // ─── ROUTES ──────────────────────────────────────────────────────────────────
38
- app.get('/subtitles/:type/:id.json', (req, res) => handleRequest(req, res));
39
- app.get('/subtitles/:type/:id/:extra.json', (req, res) => handleRequest(req, res));
40
-
41
- // ═══════════════════════════════════════════════════════════════════════════════
42
- // PILLAR 1 β€” PURITY
43
- // ═══════════════════════════════════════════════════════════════════════════════
44
- const AI_FLAGS = [
45
- 'ai-translated','machine translation','chatgpt','translated from',
46
- 'auto-translated','google translate','deepl','whisper','ai generated',
47
- 'machine-translated','openai','gpt-4','gpt4','auto translate'
48
  ];
49
 
50
- // Hard language contamination β€” kills Spanish/French bleeding into English slot
51
- const LANG_POISON = [
52
- 'spanish','espaΓ±ol','espanol','latino','castellano',
53
- 'french','franΓ§ais','francais','portuguese','portuguΓͺs',
54
- 'german','deutsch','italian','italiano','turkish',
55
- '.es.','_es_','.spa.','_spa_','[spa]','[es]',
56
- '.fr.','_fr_','.por.','_por_'
 
 
 
 
 
 
 
 
 
 
 
 
57
  ];
58
 
59
- function isHuman(filename) {
60
- const n = (filename || '').toLowerCase();
61
- return !AI_FLAGS.some(f => n.includes(f));
62
- }
63
-
64
- function isCleanLang(filename, lang) {
65
- if (lang === 'Arabic') return true;
66
- const n = (filename || '').toLowerCase();
67
- return !LANG_POISON.some(p => n.includes(p));
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  }
69
 
70
- // ═══════════════════════════════════════════════════════════════════════════════
71
- // PILLAR 3 β€” ALIGNMENT: Compound Heuristic Scoring
72
- // Scene group + resolution compound bonus ensures perfect encode match at Index 0
73
- // ═══════════════════════════════════════════════════════════════════════════════
74
- const TIER1 = ['qxr','psa','galaxyrg','ntb','deflate','epsilon','tigole','framestor'];
75
- const TIER2 = ['yts','yify','rarbg','fgt','amzn','flux','cm','evolve','tepes'];
76
- const TIER3 = ['bluray','blu-ray','webdl','web-dl','webrip','web-rip','dvdrip','hdrip'];
77
- const AUDIO = ['truehd.atmos','truehd','atmos','ddp5.1','ddp5','ddp','dts-hd','dts','ac3','aac'];
78
-
79
- function score(filename) {
80
- if (!filename) return 0;
81
- const n = filename.toLowerCase();
82
- let groupScore = 0, groupHit = false;
83
-
84
- TIER1.forEach((g, i) => { if (n.includes(g)) { groupScore = (TIER1.length - i) * 20; groupHit = true; }});
85
- if (!groupHit) TIER2.forEach((g, i) => { if (n.includes(g)) { groupScore = (TIER2.length - i) * 12; groupHit = true; }});
86
- if (!groupHit) TIER3.forEach((g, i) => { if (n.includes(g)) { groupScore = (TIER3.length - i) * 6; groupHit = true; }});
87
-
88
- let resScore = 0;
89
- if (n.includes('2160p') || n.includes('4k')) resScore = 25;
90
- else if (n.includes('1080p') || n.includes('1080')) resScore = 18;
91
- else if (n.includes('720p')) resScore = 10;
92
-
93
- // Compound bonus β€” both group AND resolution present = multiplied weight
94
- const compound = (groupHit && resScore > 0) ? Math.round((groupScore + resScore) * 0.6) : 0;
95
-
96
- let audioScore = 0;
97
- AUDIO.forEach((t, i) => { if (n.includes(t)) audioScore += (AUDIO.length - i) * 4; });
98
-
99
- return groupScore + resScore + compound + audioScore;
100
  }
101
 
102
- // ═══════════════════════════════════════════════════════════════════════════════
103
- // EXTRA PARAM PARSER β€” Stremio passes videoHash & videoSize here
104
- // This is the key to hash-precision sync with OpenSubtitles
105
- // ═══════════════════════════════════════════════════════════════════════════════
106
- function parseExtra(raw) {
107
- if (!raw) return {};
108
- try {
109
- const clean = decodeURIComponent((raw || '').replace('.json', ''));
110
- return Object.fromEntries(clean.split('&').map(p => p.split('=')).filter(p => p.length === 2));
111
- } catch { return {}; }
 
 
112
  }
113
 
114
- // ─── FETCH HELPERS ───────────────────────────────────────────────────────────
115
- async function timedFetch(url, opts = {}, ms = 9000) {
116
- const ctrl = new AbortController();
117
- const t = setTimeout(() => ctrl.abort(), ms);
118
- try { return await fetch(url, { ...opts, signal: ctrl.signal }); }
119
- finally { clearTimeout(t); }
120
  }
121
 
122
- async function safeJson(r, tag) {
123
- const txt = await r.text();
124
- try { return JSON.parse(txt); }
125
- catch { console.error(`[${tag}] bad JSON:`, txt.slice(0, 200)); return null; }
126
  }
127
 
128
- // ─── CORE HANDLER ────────────────────────────────────────────────────────────
129
- async function handleRequest(req, res) {
130
- const { type } = req.params;
131
- const rawId = req.params.id.replace('.json', '');
132
- const parts = rawId.split(':');
133
- const imdbId = parts[0];
134
- const season = parts[1] ? parseInt(parts[1]) : null;
135
- const episode = parts[2] ? parseInt(parts[2]) : null;
136
- const extra = parseExtra(req.params.extra);
137
- const hash = extra.videoHash || null;
138
- const size = extra.videoSize || null;
139
-
140
- console.log(`\n[VanSubs] β–Ά ${type} ${imdbId} S${season ?? '-'}E${episode ?? '-'} hash=${hash ?? 'none'}`);
141
-
142
- // Fire both providers simultaneously β€” neither waits for the other
143
- const [r1, r2] = await Promise.allSettled([
144
- fetchSubDL(imdbId, type, season, episode),
145
- fetchOpenSubtitles(imdbId, type, season, episode, hash, size)
146
- ]);
147
-
148
- let pool = [];
149
- if (r1.status === 'fulfilled') { console.log(`[SubDL] ${r1.value.length} raw`); pool.push(...r1.value); }
150
- else console.error('[SubDL] FAILED:', r1.reason?.message);
151
- if (r2.status === 'fulfilled') { console.log(`[OpenSubs] ${r2.value.length} raw`); pool.push(...r2.value); }
152
- else console.error('[OpenSubs] FAILED:', r2.reason?.message);
153
-
154
- // Sanitation pass β€” AI garbage + language contamination + invalid URLs
155
- const clean = pool.filter(s =>
156
- s.url && s.url.startsWith('http') &&
157
- isHuman(s.filename) &&
158
- isCleanLang(s.filename, s.lang)
159
- );
160
-
161
- // Hash-matched tracks get a massive score bonus β€” they are frame-perfect
162
- // This guarantees hash matches always float to Index Zero
163
- clean.forEach(s => {
164
- if (s.hashMatch) s._score = 9999;
165
- else s._score = score(s.filename);
166
- });
167
-
168
- clean.sort((a, b) => b._score - a._score);
169
-
170
- // Sweet spot: 3 per language
171
- // Index 0 = perfect match (auto-selected by Stremio)
172
- // Index 1-2 = fallbacks if encode differs
173
- const TOP = 3;
174
- const arabic = clean.filter(s => s.lang === 'Arabic').slice(0, TOP);
175
- const english = clean.filter(s => s.lang === 'English').slice(0, TOP);
176
- const final = [...arabic, ...english];
177
-
178
- const subtitles = final.map((s, i) => ({
179
- id: `van_${imdbId}_${i}`,
180
- url: s.url,
181
- lang: s.lang
182
- }));
183
-
184
- console.log(`[VanSubs] βœ… AR:${arabic.length} EN:${english.length}`);
185
- console.log(`[VanSubs] #0 β†’ "${final[0]?.filename ?? 'none'}" (score:${final[0]?._score ?? 0})`);
186
- res.json({ subtitles });
187
  }
188
 
189
- // ═══════════════════════════════════════════════════════════════════════════════
190
- // PROVIDER 1 β€” SubDL
191
- // Best for: Arabic subtitles, scene release matching, new content
192
- // ═══════════════════════════════════════════════════════════════════════════════
193
- async function fetchSubDL(imdbId, type, season, episode) {
194
- let url = `https://api.subdl.com/api/v1/subtitles`
195
- + `?api_key=${SUBDL_KEY}`
196
- + `&imdb_id=${imdbId}`
197
- + `&languages=AR,EN`
198
- + `&subs_per_page=30`
199
- + `&releases=1`;
200
-
201
- if (type === 'series' && season !== null && episode !== null)
202
- url += `&season_number=${season}&episode_number=${episode}`;
203
-
204
- const r = await timedFetch(url);
205
- const data = await safeJson(r, 'SubDL');
206
- if (!data?.status) { console.error('[SubDL] err:', data?.error); return []; }
207
-
208
- return (data.subtitles || []).map(sub => {
209
- const lang = (sub.language || '').toLowerCase().startsWith('ar') ? 'Arabic' : 'English';
210
- let dlUrl = sub.url || '';
211
- if (dlUrl && !dlUrl.startsWith('http')) dlUrl = 'https://dl.subdl.com' + dlUrl;
212
- return { filename: sub.release_name || sub.name || 'SubDL', url: dlUrl, lang };
213
- }).filter(s => s.url);
214
  }
215
 
216
- // ═══════════════════════════════════════════════════════════════════════════════
217
- // PROVIDER 2 β€” OpenSubtitles REST v3
218
- // Best for: Hash-precision English sync, massive archive, bulletproof stability
219
- //
220
- // Two-step flow:
221
- // Step 1: GET /subtitles β€” search by IMDB ID + optional hash β†’ get file_id list
222
- // Step 2: POST /download β€” exchange file_id for direct download URL
223
- //
224
- // Hash mode: when videoHash is available, OS matches the EXACT file
225
- // the user is playing β€” this is frame-perfect sync, eliminates all drift
226
- // ═══════════════════════════════════════════════════════════════════════════════
227
- async function fetchOpenSubtitles(imdbId, type, season, episode, videoHash, videoSize) {
228
- const headers = {
229
- 'Api-Key': OS_API_KEY,
230
- 'Content-Type': 'application/json',
231
- 'User-Agent': `${OS_APP_NAME} v7.0`
232
- };
233
-
234
- // ── Step 1: Search ───────────────────────────────────────────────────────
235
- const imdbNumeric = imdbId.replace('tt', '');
236
- let searchUrl = `${OS_BASE}/subtitles`
237
- + `?imdb_id=${imdbNumeric}`
238
- + `&languages=en,ar`
239
- + `&order_by=download_count`
240
- + `&order_direction=desc`
241
- + `&per_page=20`;
242
-
243
- if (type === 'series' && season !== null && episode !== null) {
244
- searchUrl += `&season_number=${season}&episode_number=${episode}`;
245
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
246
 
247
- // Hash mode β€” when available, overrides everything for perfect match
248
- if (videoHash) {
249
- searchUrl += `&moviehash=${videoHash}`;
250
- if (videoSize) searchUrl += `&moviebytesize=${videoSize}`;
251
- }
 
 
 
 
 
 
 
252
 
253
- const sr = await timedFetch(searchUrl, { headers });
254
- const sData = await safeJson(sr, 'OS/search');
255
- if (!sData?.data?.length) {
256
- console.log('[OpenSubs] no results for', imdbId);
257
- return [];
 
 
 
258
  }
 
259
 
260
- // ── Step 2: Get download URLs ────────────────────────────────────────────
261
- // OS returns file_id per subtitle file β€” we POST to /download to get the URL
262
- // We only resolve top 6 by download count (already ordered by API)
263
- const candidates = sData.data.slice(0, 6);
264
- const results = [];
265
-
266
- for (const item of candidates) {
267
- try {
268
- const attr = item.attributes || {};
269
- const files = attr.files || [];
270
- if (!files.length) continue;
271
-
272
- const fileId = files[0].file_id;
273
- const langRaw = (attr.language || '').toLowerCase();
274
- const lang = langRaw === 'ar' || langRaw === 'arabic' ? 'Arabic' : 'English';
275
- const fname = files[0].file_name || attr.release || 'OpenSubtitles';
276
-
277
- // Check if this is a hash-matched result β€” gives it perfect score
278
- const isHashMatch = attr.moviehash_match === true;
279
-
280
- const dlR = await timedFetch(`${OS_BASE}/download`, {
281
- method: 'POST',
282
- headers,
283
- body: JSON.stringify({ file_id: fileId })
284
- });
285
- const dlData = await safeJson(dlR, 'OS/download');
286
- if (!dlData?.link) continue;
287
-
288
- results.push({
289
- filename: fname,
290
- url: dlData.link,
291
- lang,
292
- hashMatch: isHashMatch
293
- });
294
- } catch (e) {
295
- console.error('[OpenSubs] download err:', e.message);
296
- }
297
  }
 
298
 
299
- console.log(`[OpenSubs] resolved ${results.length} download links`);
300
- return results;
301
- }
302
-
303
- // ─── START ───────────────────────────────────────────────────────────────────
304
- app.listen(PORT, '0.0.0.0', () => console.log(`Van Subs v7 running on port ${PORT}`));
 
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}`));