vanseleem commited on
Commit
00a1e2e
Β·
verified Β·
1 Parent(s): e8947aa

Update index.js

Browse files
Files changed (1) hide show
  1. index.js +223 -0
index.js CHANGED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use strict';
2
+
3
+ const express = require('express');
4
+ const fetch = require('node-fetch');
5
+ const app = express();
6
+ const PORT = process.env.PORT || 7860;
7
+
8
+ // ── Source links ─────────────────────────────────────────────────
9
+ const SOURCE_1 = 'https://iamtjake-opensubs.hf.space';
10
+ const SOURCE_2 = 'https://iamtjake-subdl.hf.space';
11
+ const SOURCE_3 = 'https://vanseleem-subf2m.hf.space';
12
+ const SOURCE_4 = 'https://iamtjake-subsource.hf.space';
13
+
14
+ const FETCH_TIMEOUT_MS = 8_000;
15
+ const MAX_PER_LANG = 1; // max subtitles per language per source (1 = 6 total, 2 = 12 total, etc.)
16
+
17
+ // ── CORS ───────────────────────────────────────────────────────
18
+ app.use((req, res, next) => {
19
+ res.setHeader('Access-Control-Allow-Origin', '*');
20
+ res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
21
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
22
+ if (req.method === 'OPTIONS') return res.sendStatus(200);
23
+ next();
24
+ });
25
+
26
+ // ── Manifest ────────────────────────────────────────────────────
27
+ app.get('/manifest.json', (req, res) => {
28
+ res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
29
+ res.json({
30
+ id: 'org.van.vansubs.pro',
31
+ name: 'VanSubs+',
32
+ description: 'Human-Verified Arabic & English Subtitles. Anti-AI.',
33
+ version: '1.0.0',
34
+ resources: ['subtitles'],
35
+ types: ['movie', 'series'],
36
+ idPrefixes: ['tt'],
37
+ catalogs: []
38
+ });
39
+ });
40
+
41
+ // ── AbortController-based timeout ─────────────────────────────
42
+ async function fetchWithTimeout(url, ms) {
43
+ const controller = new AbortController();
44
+ const timer = setTimeout(() => controller.abort(), ms);
45
+ try {
46
+ const resp = await fetch(url, { signal: controller.signal });
47
+ return resp;
48
+ } finally {
49
+ clearTimeout(timer);
50
+ }
51
+ }
52
+
53
+ // ── Language normalizer ────────────────────────────────────────
54
+ function normalizeLang(lang) {
55
+ if (!lang) return null;
56
+ const l = lang.toLowerCase().trim();
57
+ if (['en','eng','english','en-us','en-gb'].includes(l)) return 'english';
58
+ if (['ar','ara','arabic','ar-ae','ar-sa'].includes(l)) return 'arabic';
59
+ return null;
60
+ }
61
+
62
+ // ── Pick top MAX_PER_LANG Arabic and English from a source ────
63
+ function topTwoFromSource(subs) {
64
+ if (!Array.isArray(subs) || subs.length === 0) return { arabic: [], english: [] };
65
+ const arabic = [], english = [];
66
+ for (const sub of subs) {
67
+ if (!sub.url) continue;
68
+ const lang = normalizeLang(sub.lang);
69
+ if (lang === 'arabic' && arabic.length < MAX_PER_LANG) arabic.push(sub);
70
+ if (lang === 'english' && english.length < MAX_PER_LANG) english.push(sub);
71
+ if (arabic.length >= MAX_PER_LANG && english.length >= MAX_PER_LANG) break;
72
+ }
73
+ return { arabic, english };
74
+ }
75
+
76
+ // ── Subtitle endpoint (parallel fetch + merge) ────────────────
77
+ app.get('/subtitles/:type/:id/:extra?.json', async (req, res) => {
78
+ const { type, id } = req.params;
79
+ const extra = req.params.extra ? `/${req.params.extra}` : '';
80
+
81
+ const urls = [
82
+ `${SOURCE_1}/subtitles/${type}/${id}${extra}.json`,
83
+ `${SOURCE_2}/subtitles/${type}/${id}${extra}.json`,
84
+ `${SOURCE_3}/subtitles/${type}/${id}${extra}.json`,
85
+ `${SOURCE_4}/subtitles/${type}/${id}${extra}.json` // ← fourth source added
86
+ ];
87
+
88
+ const fetchJson = async (url) => {
89
+ try {
90
+ const resp = await fetchWithTimeout(url, FETCH_TIMEOUT_MS);
91
+ if (!resp.ok) {
92
+ console.log(`[van] ${url} β†’ HTTP ${resp.status}`);
93
+ return null;
94
+ }
95
+ const data = await resp.json();
96
+ const subs = Array.isArray(data) ? data : (data?.subtitles || data?.data || []);
97
+ console.log(`[van] ${url.split('?')[0]} β†’ ${subs.length} subtitles`);
98
+ return subs;
99
+ } catch (err) {
100
+ const reason = err.name === 'AbortError' ? 'timed out' : err.message;
101
+ console.log(`[van] ${url.split('?')[0]} failed: ${reason}`);
102
+ return null;
103
+ }
104
+ };
105
+
106
+ const allResults = await Promise.all(urls.map(u => fetchJson(u)));
107
+
108
+ const seenAr = new Set();
109
+ const seenEn = new Set();
110
+ const mergedAr = [];
111
+ const mergedEn = [];
112
+
113
+ for (let i = 0; i < allResults.length; i++) {
114
+ const subs = allResults[i];
115
+ if (!subs) continue;
116
+
117
+ const { arabic, english } = topTwoFromSource(subs);
118
+ for (const sub of arabic) { if (!seenAr.has(sub.url)) { seenAr.add(sub.url); mergedAr.push(sub); console.log(`[van] source ${i + 1} β†’ Arabic : ${sub.url}`); } }
119
+ for (const sub of english) { if (!seenEn.has(sub.url)) { seenEn.add(sub.url); mergedEn.push(sub); console.log(`[van] source ${i + 1} β†’ English : ${sub.url}`); } }
120
+ }
121
+
122
+ const merged = [...mergedAr, ...mergedEn];
123
+ console.log(`[van] Final: ${merged.length} subtitles (AR:${mergedAr.length} EN:${mergedEn.length})`);
124
+ res.json({ subtitles: merged });
125
+ });
126
+
127
+ // ── Landing page ──────────────────────────────────────────────
128
+ app.get('/', (req, res) => {
129
+ const manifestUrl = `${req.protocol}://${req.get('host')}/manifest.json`;
130
+ const installUrl = 'stremio://vanseleem-vansubs.hf.space/manifest.json';
131
+
132
+ res.type('html').send(`<!DOCTYPE html>
133
+ <html lang="en">
134
+ <head>
135
+ <meta charset="UTF-8">
136
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
137
+ <title>VanSubs+</title>
138
+ <style>
139
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
140
+ body {
141
+ font-family: -apple-system, 'Segoe UI', system-ui, sans-serif;
142
+ background: #050810;
143
+ color: #dde6f0;
144
+ min-height: 100vh;
145
+ display: flex;
146
+ align-items: center;
147
+ justify-content: center;
148
+ padding: 28px 20px;
149
+ background-image:
150
+ radial-gradient(ellipse 70% 40% at 50% 0%, rgba(0,180,255,0.07) 0%, transparent 70%),
151
+ radial-gradient(ellipse 40% 30% at 80% 80%, rgba(0,100,200,0.05) 0%, transparent 60%);
152
+ }
153
+ .wrap { width: 100%; max-width: 560px; display: flex; flex-direction: column; gap: 20px; }
154
+ .header { display: flex; flex-direction: column; gap: 12px; }
155
+ .logo { font-size: 2.6rem; font-weight: 800; letter-spacing: -1px; line-height: 1; }
156
+ .logo .van { color: #00c6ff; }
157
+ .logo .subs { color: #ffffff; }
158
+ .badge-row { display: flex; gap: 8px; flex-wrap: wrap; }
159
+ .badge {
160
+ display: inline-flex; align-items: center; gap: 5px;
161
+ background: rgba(0,198,255,0.08); border: 1px solid rgba(0,198,255,0.18);
162
+ color: #ffffff; padding: 4px 11px; border-radius: 100px;
163
+ font-size: 0.72rem; font-weight: 700; letter-spacing: 0.4px; text-transform: uppercase;
164
+ }
165
+ .badge.green { background: rgba(0,220,130,0.08); border-color: rgba(0,220,130,0.18); color: #00dc82; }
166
+ .badge.anti-ai { color: #ffffff; }
167
+ .desc { color: #ffffff; font-size: 0.9rem; line-height: 1.6; margin-top: 2px; }
168
+ .card {
169
+ background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.07);
170
+ border-radius: 16px; padding: 20px 22px;
171
+ }
172
+ .card-label { font-size: 0.68rem; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: #ffffff; margin-bottom: 10px; }
173
+ .url-box {
174
+ font-family: 'SF Mono','Fira Code','Consolas',monospace; font-size: 0.8rem;
175
+ color: #ffffff; background: rgba(0,0,0,0.25); border: 1px solid rgba(0,198,255,0.12);
176
+ border-radius: 10px; padding: 11px 14px; word-break: break-all; line-height: 1.5;
177
+ }
178
+ .actions { display: flex; gap: 10px; margin-top: 14px; flex-wrap: wrap; }
179
+ .btn { padding: 10px 22px; border-radius: 10px; font-size: 0.84rem; font-weight: 600; text-decoration: none; border: none; cursor: pointer; transition: all 0.15s; font-family: inherit; }
180
+ .btn-primary { background: #00c6ff; color: #050810; }
181
+ .btn-primary:hover { background: #00b0e8; }
182
+ .btn-copy { background: #ffffff; color: #050810; border: 1px solid #ffffff; }
183
+ .btn-copy:hover { background: #e6e6e6; border-color: #e6e6e6; }
184
+ .stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
185
+ .stat { background: rgba(255,255,255,0.02); border: 1px solid rgba(255,255,255,0.06); border-radius: 12px; padding: 16px 12px; text-align: center; }
186
+ .stat-val { font-size: 1.5rem; font-weight: 800; color: #fff; line-height: 1; margin-bottom: 5px; }
187
+ .stat-lbl { font-size: 0.68rem; color: #3a5060; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; }
188
+ .made-by { text-align: center; color: #ffffff; font-size: 0.85rem; margin-top: 6px; opacity: 0.8; }
189
+ </style>
190
+ </head>
191
+ <body>
192
+ <div class="wrap">
193
+ <div class="header">
194
+ <div class="logo"><span class="van">Van</span> <span class="subs">Subs+</span></div>
195
+ <div class="badge-row">
196
+ <span class="badge anti-ai">πŸ›‘ Anti-AI</span>
197
+ <span class="badge green">βœ” Human-Verified</span>
198
+ </div>
199
+ <p class="desc">Human-Verified Arabic &amp; English Subtitles. Anti-AI.</p>
200
+ </div>
201
+ <div class="card">
202
+ <div class="card-label">Stremio Install URL</div>
203
+ <div class="url-box">${manifestUrl}</div>
204
+ <div class="actions">
205
+ <a href="${installUrl}" class="btn btn-primary">Install in Stremio</a>
206
+ <button class="btn btn-copy" onclick="navigator.clipboard.writeText('${manifestUrl}').then(()=>{this.textContent='βœ” Copied';setTimeout(()=>this.textContent='Copy URL',2000)})">Copy URL</button>
207
+ </div>
208
+ </div>
209
+ <div class="stats">
210
+ <div class="stat"><div class="stat-val">2</div><div class="stat-lbl">Languages</div></div>
211
+ <div class="stat"><div class="stat-val">4</div><div class="stat-lbl">Sources</div></div>
212
+ <div class="stat"><div class="stat-val">0</div><div class="stat-lbl">AI Subs</div></div>
213
+ </div>
214
+ <div class="made-by">Made With 🀍 By Van</div>
215
+ </div>
216
+ </body>
217
+ </html>`);
218
+ });
219
+
220
+ app.listen(PORT, '0.0.0.0', () => {
221
+ console.log(`Sources:\n 1. ${SOURCE_1}\n 2. ${SOURCE_2}\n 3. ${SOURCE_3}\n 4. ${SOURCE_4}`);
222
+ console.log(`\nVanSubs+ (v1.0.9) running β†’ http://0.0.0.0:${PORT}`);
223
+ });