@serifpersia/dango 3.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +247 -0
- package/client/dist/assets/AnimeInfo-CMxs0nRH.css +1 -0
- package/client/dist/assets/AnimeInfo-DVdaocCl.js +1 -0
- package/client/dist/assets/AnimeInfoPage-Bx5EvGt-.js +2 -0
- package/client/dist/assets/Asmr-C2i9GKD4.css +1 -0
- package/client/dist/assets/Asmr-CIjmFtRH.js +2 -0
- package/client/dist/assets/Asmr.module-DPG2JhDJ.js +1 -0
- package/client/dist/assets/AvSyncCalibrator-CIYcyon5.css +1 -0
- package/client/dist/assets/AvSyncCalibrator-vqErH7Cm.js +10 -0
- package/client/dist/assets/ErrorMessage-BwSjzblw.js +1 -0
- package/client/dist/assets/ErrorMessage-CXoRbrLN.css +1 -0
- package/client/dist/assets/Home-D7YfGBUk.js +1 -0
- package/client/dist/assets/Home-DKCfEiwl.css +1 -0
- package/client/dist/assets/Insights-BljI64Ua.css +1 -0
- package/client/dist/assets/Insights-CLQGrlpX.js +1 -0
- package/client/dist/assets/Manga-DQ9HYnpk.js +1 -0
- package/client/dist/assets/Manga-H8Wb04w3.css +1 -0
- package/client/dist/assets/Map-CXFqXmsZ.css +1 -0
- package/client/dist/assets/Map-L_ryVBTq.js +1 -0
- package/client/dist/assets/Mature-BeWzAJ7B.js +1 -0
- package/client/dist/assets/Mature-DJmUUtlF.css +1 -0
- package/client/dist/assets/Player-CYpd__FP.js +30 -0
- package/client/dist/assets/Player-DreAhybc.css +1 -0
- package/client/dist/assets/PlayerRedirect-fxUAp9JV.js +1 -0
- package/client/dist/assets/PlayerSettings-BcsotxP9.js +1 -0
- package/client/dist/assets/PlayerSettings-Cqua1CDD.css +1 -0
- package/client/dist/assets/QueueRail-B9Pgbqc3.js +1 -0
- package/client/dist/assets/QueueRail-BnaltOP8.css +1 -0
- package/client/dist/assets/Radio-BvdY5twD.js +1 -0
- package/client/dist/assets/Radio-CUuYy3DY.css +1 -0
- package/client/dist/assets/Search-BzO-aRP7.css +1 -0
- package/client/dist/assets/Search-R9YLAXS1.js +1 -0
- package/client/dist/assets/Search.module-D8ZbQAiK.js +1 -0
- package/client/dist/assets/Settings-9kYcXdTR.js +21 -0
- package/client/dist/assets/Settings-DF8JThUL.css +1 -0
- package/client/dist/assets/SkeletonGrid-D5vRvX5q.js +1 -0
- package/client/dist/assets/SkeletonGrid-DEbYtO4H.css +1 -0
- package/client/dist/assets/SynopsisText-BlFw0R-0.js +1 -0
- package/client/dist/assets/SynopsisText-CUgOjOxt.css +1 -0
- package/client/dist/assets/Trackers-DfZlnnVq.js +2 -0
- package/client/dist/assets/Trackers-Dt9lR_uP.css +1 -0
- package/client/dist/assets/Tv-CVKC2YBs.css +1 -0
- package/client/dist/assets/Tv-Db1Wx_b7.js +15 -0
- package/client/dist/assets/Watchlist-B2nkNZqM.css +1 -0
- package/client/dist/assets/Watchlist-a77CYtzu.js +1 -0
- package/client/dist/assets/hls-6Owgw2Aj.js +1 -0
- package/client/dist/assets/hls-BOLXTnjL.js +42 -0
- package/client/dist/assets/index-B6qdXFtl.css +1 -0
- package/client/dist/assets/index-B8TPVaIS.js +30354 -0
- package/client/dist/assets/index-CpjgU8ou.js +178 -0
- package/client/dist/assets/useAnimeInfoData-BOfyi9UJ.js +1 -0
- package/client/dist/assets/useIsMobile-CM15uQn8.css +1 -0
- package/client/dist/assets/useIsMobile-Cb8HcKRj.js +1 -0
- package/client/dist/assets/useMatureConsent-CZMYBVXd.js +1 -0
- package/client/dist/assets/vendor-rLNfZm7Y.js +1 -0
- package/client/dist/av-sync-test.mp4 +0 -0
- package/client/dist/favicon.ico +0 -0
- package/client/dist/index.html +31 -0
- package/client/dist/logo-mochi.png +0 -0
- package/client/dist/logo-sakura.png +0 -0
- package/client/dist/logo.png +0 -0
- package/client/dist/placeholder.svg +4 -0
- package/client/dist/robots.txt +3 -0
- package/orchestrator.js +416 -0
- package/package.json +89 -0
- package/server/dist/app-auth.js +231 -0
- package/server/dist/config.js +133 -0
- package/server/dist/controllers/auth.controller.js +247 -0
- package/server/dist/controllers/data.controller.js +785 -0
- package/server/dist/controllers/insights.controller.js +197 -0
- package/server/dist/controllers/proxy.controller.js +665 -0
- package/server/dist/controllers/settings.controller.js +267 -0
- package/server/dist/controllers/watchlist.controller.js +967 -0
- package/server/dist/db.js +152 -0
- package/server/dist/discord-gateway.js +381 -0
- package/server/dist/discord-rpc.js +424 -0
- package/server/dist/github-sync.js +403 -0
- package/server/dist/google.js +549 -0
- package/server/dist/lib/anilist.js +1519 -0
- package/server/dist/lib/anischedule.js +202 -0
- package/server/dist/lib/ipc.js +32 -0
- package/server/dist/lib/kitsu.js +552 -0
- package/server/dist/lib/mal.js +886 -0
- package/server/dist/lib/migration.js +335 -0
- package/server/dist/lib/temp-ids.js +11 -0
- package/server/dist/lib/tmdb.js +71 -0
- package/server/dist/lib/tracker/anilist-tracker.js +250 -0
- package/server/dist/lib/tracker/sync.service.js +312 -0
- package/server/dist/logger.js +19 -0
- package/server/dist/providers/123anime.provider.js +240 -0
- package/server/dist/providers/anilight.provider.js +366 -0
- package/server/dist/providers/animepahe.provider.js +531 -0
- package/server/dist/providers/animeya.provider.js +746 -0
- package/server/dist/providers/base-provider.js +45 -0
- package/server/dist/providers/hn.provider.js +473 -0
- package/server/dist/providers/ht.provider.js +439 -0
- package/server/dist/providers/jasmr.provider.js +504 -0
- package/server/dist/providers/kaa.provider.js +388 -0
- package/server/dist/providers/manga/manga.types.js +10 -0
- package/server/dist/providers/manga/mangadex.provider.js +227 -0
- package/server/dist/providers/manga/mangapill.provider.js +232 -0
- package/server/dist/providers/megaplay.provider.js +583 -0
- package/server/dist/providers/op.provider.js +440 -0
- package/server/dist/providers/provider.interface.js +2 -0
- package/server/dist/providers/title-matching.js +234 -0
- package/server/dist/providers/wh.provider.js +600 -0
- package/server/dist/rclone.js +126 -0
- package/server/dist/repositories/insights.repository.js +43 -0
- package/server/dist/repositories/mal-cache.repository.js +66 -0
- package/server/dist/repositories/notifications.repository.js +40 -0
- package/server/dist/repositories/queue.repository.js +64 -0
- package/server/dist/repositories/settings.repository.js +15 -0
- package/server/dist/repositories/shows-meta.repository.js +43 -0
- package/server/dist/repositories/temp-show-ids.repository.js +54 -0
- package/server/dist/repositories/watched-episodes.repository.js +54 -0
- package/server/dist/repositories/watchlist.repository.js +74 -0
- package/server/dist/routes/asmr.routes.js +72 -0
- package/server/dist/routes/auth.routes.js +28 -0
- package/server/dist/routes/data.routes.js +50 -0
- package/server/dist/routes/discord-gateway.routes.js +57 -0
- package/server/dist/routes/insights.routes.js +12 -0
- package/server/dist/routes/lan-auth.routes.js +49 -0
- package/server/dist/routes/manga.routes.js +115 -0
- package/server/dist/routes/proxy.routes.js +15 -0
- package/server/dist/routes/radio.routes.js +143 -0
- package/server/dist/routes/settings.routes.js +27 -0
- package/server/dist/routes/tracker.routes.js +143 -0
- package/server/dist/routes/translate.routes.js +90 -0
- package/server/dist/routes/tv.routes.js +843 -0
- package/server/dist/routes/watchlist.routes.js +312 -0
- package/server/dist/server.js +310 -0
- package/server/dist/shipped-defaults.js +8 -0
- package/server/dist/sync-config.js +32 -0
- package/server/dist/sync.js +506 -0
- package/server/dist/utils/cookie.utils.js +17 -0
- package/server/dist/utils/db-utils.js +15 -0
- package/server/dist/utils/env.utils.js +83 -0
- package/server/dist/utils/machine-id.js +46 -0
- package/server/dist/utils/request-context.js +5 -0
- package/server/dist/utils/security.utils.js +99 -0
|
@@ -0,0 +1,843 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createTvRouter = createTvRouter;
|
|
4
|
+
const express_1 = require("express");
|
|
5
|
+
const tmdb_1 = require("../lib/tmdb");
|
|
6
|
+
const url_1 = require("url");
|
|
7
|
+
const security_utils_1 = require("../utils/security.utils");
|
|
8
|
+
const MOVY_API = 'https://api.wecollege.net';
|
|
9
|
+
const MOVY_SERVERS = [
|
|
10
|
+
'miami',
|
|
11
|
+
'phoenix',
|
|
12
|
+
'dallas',
|
|
13
|
+
'seattle',
|
|
14
|
+
'denver',
|
|
15
|
+
'cancun',
|
|
16
|
+
'atlanta',
|
|
17
|
+
'houston',
|
|
18
|
+
'portland',
|
|
19
|
+
'austin',
|
|
20
|
+
'munich',
|
|
21
|
+
'berlin',
|
|
22
|
+
'paris',
|
|
23
|
+
'delhi',
|
|
24
|
+
];
|
|
25
|
+
const MOVY_K = [
|
|
26
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
|
27
|
+
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
28
|
+
];
|
|
29
|
+
const MOVY_MAGIC = [109, 118, 109, 49];
|
|
30
|
+
const movyIsEven = (e) => ((e * (e + 1)) & 1) === 0;
|
|
31
|
+
function movyMix(e) {
|
|
32
|
+
e >>>= 0;
|
|
33
|
+
e ^= e >>> 16;
|
|
34
|
+
e = Math.imul(e, 0x85ebca6b) >>> 0;
|
|
35
|
+
e ^= e >>> 13;
|
|
36
|
+
e = Math.imul(e, 0xc2b2ae35) >>> 0;
|
|
37
|
+
e ^= e >>> 16;
|
|
38
|
+
return e >>> 0;
|
|
39
|
+
}
|
|
40
|
+
function movyShift(e, t) {
|
|
41
|
+
return ((e >>>= 0), 0 === (t &= 31) ? e >>> 0 : ((e << t) | (e >>> (32 - t))) >>> 0);
|
|
42
|
+
}
|
|
43
|
+
function decodeMovyPayload(e, t, a) {
|
|
44
|
+
const r = (function (e) {
|
|
45
|
+
const t = e
|
|
46
|
+
.replace(/-/g, '+')
|
|
47
|
+
.replace(/_/g, '/')
|
|
48
|
+
.padEnd(4 * Math.ceil(e.length / 4), '=');
|
|
49
|
+
return new Uint8Array(Buffer.from(t, 'base64'));
|
|
50
|
+
})(e);
|
|
51
|
+
const n = (function (e, t, a) {
|
|
52
|
+
const s = (function (e, t) {
|
|
53
|
+
const s = Array(61);
|
|
54
|
+
let r = movyMix((function (e) {
|
|
55
|
+
let t = 0x811c9dc5;
|
|
56
|
+
for (let a = 0; a < e.length; a++)
|
|
57
|
+
t = Math.imul(t ^ e.charCodeAt(a), 0x1000193) >>> 0;
|
|
58
|
+
return movyMix(t);
|
|
59
|
+
})(e) ^ movyMix((t >>> 0) ^ 0x9e3779b9)) >>> 0;
|
|
60
|
+
for (let e = 0; e < 8; e++) {
|
|
61
|
+
if (movyIsEven(e)) {
|
|
62
|
+
const t = r % 61;
|
|
63
|
+
r = movyShift((r + 0x9e3779b9) >>> 0, 7 + (7 & e));
|
|
64
|
+
s[t] = (r ^ movyMix(r)) >>> 0;
|
|
65
|
+
r = movyMix((r + t) >>> 0);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
s[e] = MOVY_K[15 & e];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { S: s, acc: movyMix(0xa5a5a5a5 ^ r) >>> 0 };
|
|
72
|
+
})(e, t);
|
|
73
|
+
const r = new Uint8Array(a);
|
|
74
|
+
let n = 0;
|
|
75
|
+
for (let e = 0; e < a;) {
|
|
76
|
+
const t = (function (e, t) {
|
|
77
|
+
const r = e.S;
|
|
78
|
+
let n = e.acc;
|
|
79
|
+
const i = n % 61;
|
|
80
|
+
const o = 0 - Number(i in r);
|
|
81
|
+
const l = r[i] >>> 0;
|
|
82
|
+
const c = Math.imul(0x9e3779b9, t + 1) >>> 0;
|
|
83
|
+
const h = ((((n ^ ((l ^ c) >>> 0)) >>> 0) | (n & ((l ^ c) >>> 0) & o)) >>> 0) >>> 0;
|
|
84
|
+
n = movyMix(((movyShift((h + n) >>> 0, 31 & i) ^ movyShift(n, 31 & Math.imul(i, 7))) + 0x9e3779b9) >>>
|
|
85
|
+
0);
|
|
86
|
+
r[i] = n >>> 0;
|
|
87
|
+
e.acc = n;
|
|
88
|
+
return n >>> 0;
|
|
89
|
+
})(s, n++);
|
|
90
|
+
r[e++] = 255 & t;
|
|
91
|
+
if (e < a)
|
|
92
|
+
r[e++] = (t >>> 8) & 255;
|
|
93
|
+
if (e < a)
|
|
94
|
+
r[e++] = (t >>> 16) & 255;
|
|
95
|
+
if (e < a)
|
|
96
|
+
r[e++] = (t >>> 24) & 255;
|
|
97
|
+
}
|
|
98
|
+
return r;
|
|
99
|
+
})(String(t), a, r.length);
|
|
100
|
+
for (let e = 0; e < r.length; e++)
|
|
101
|
+
r[e] ^= n[e];
|
|
102
|
+
for (let e = 0; e < MOVY_MAGIC.length; e++) {
|
|
103
|
+
if (r[e] !== MOVY_MAGIC[e])
|
|
104
|
+
throw new Error('decrypt failed: bad seed or payload');
|
|
105
|
+
}
|
|
106
|
+
return Buffer.from(r.subarray(MOVY_MAGIC.length)).toString('utf8');
|
|
107
|
+
}
|
|
108
|
+
const movySeedCache = new Map();
|
|
109
|
+
const inflightSeedRequests = new Map();
|
|
110
|
+
async function movyGetSeed(mediaId, forceRefresh = false) {
|
|
111
|
+
const key = String(mediaId);
|
|
112
|
+
const now = Date.now();
|
|
113
|
+
if (!forceRefresh) {
|
|
114
|
+
const cached = movySeedCache.get(key);
|
|
115
|
+
if (cached && cached.expiresAt - 4000 > now)
|
|
116
|
+
return cached.seed;
|
|
117
|
+
}
|
|
118
|
+
if (inflightSeedRequests.has(key))
|
|
119
|
+
return await inflightSeedRequests.get(key);
|
|
120
|
+
const promise = (async () => {
|
|
121
|
+
try {
|
|
122
|
+
const r = await fetch(`${MOVY_API}/seed?mediaId=${mediaId}`, {
|
|
123
|
+
headers: {
|
|
124
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
125
|
+
Accept: 'application/json, text/plain, */*',
|
|
126
|
+
Referer: 'https://www.movy.bz/',
|
|
127
|
+
Origin: 'https://www.movy.bz',
|
|
128
|
+
},
|
|
129
|
+
signal: AbortSignal.timeout(5000),
|
|
130
|
+
});
|
|
131
|
+
if (r.ok) {
|
|
132
|
+
const data = await r.json();
|
|
133
|
+
const ttl = data.ttlMs || 30000;
|
|
134
|
+
movySeedCache.set(key, { seed: data.seed, expiresAt: Date.now() + ttl });
|
|
135
|
+
return data.seed;
|
|
136
|
+
}
|
|
137
|
+
if (r.status === 429) {
|
|
138
|
+
const cached = movySeedCache.get(key);
|
|
139
|
+
if (cached)
|
|
140
|
+
return cached.seed;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
const cached = movySeedCache.get(key);
|
|
145
|
+
if (cached)
|
|
146
|
+
return cached.seed;
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
inflightSeedRequests.delete(key);
|
|
150
|
+
}
|
|
151
|
+
return movySeedCache.get(key)?.seed || null;
|
|
152
|
+
})();
|
|
153
|
+
inflightSeedRequests.set(key, promise);
|
|
154
|
+
return await promise;
|
|
155
|
+
}
|
|
156
|
+
async function tryMovyCity(city, baseParams, seed, numericTmdbId, headers) {
|
|
157
|
+
try {
|
|
158
|
+
const params = new URLSearchParams({ ...baseParams, seed });
|
|
159
|
+
const r = await fetch(`${MOVY_API}/${city}/sources?${params.toString()}`, {
|
|
160
|
+
headers,
|
|
161
|
+
signal: AbortSignal.timeout(5000),
|
|
162
|
+
});
|
|
163
|
+
if (!r.ok)
|
|
164
|
+
return null;
|
|
165
|
+
const encrypted = await r.text();
|
|
166
|
+
let decrypted;
|
|
167
|
+
try {
|
|
168
|
+
decrypted = decodeMovyPayload(encrypted, seed, numericTmdbId);
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
const retrySeed = await movyGetSeed(numericTmdbId, true);
|
|
172
|
+
if (!retrySeed)
|
|
173
|
+
return null;
|
|
174
|
+
const retryParams = new URLSearchParams({ ...baseParams, seed: retrySeed });
|
|
175
|
+
const retryResp = await fetch(`${MOVY_API}/${city}/sources?${retryParams.toString()}`, {
|
|
176
|
+
headers,
|
|
177
|
+
signal: AbortSignal.timeout(5000),
|
|
178
|
+
});
|
|
179
|
+
if (!retryResp.ok)
|
|
180
|
+
return null;
|
|
181
|
+
decrypted = decodeMovyPayload(await retryResp.text(), retrySeed, numericTmdbId);
|
|
182
|
+
}
|
|
183
|
+
const data = JSON.parse(decrypted);
|
|
184
|
+
if (!Array.isArray(data.sources) || data.sources.length === 0)
|
|
185
|
+
return null;
|
|
186
|
+
const validSources = data.sources.filter((s) => !(s.url || '').includes('.mpd'));
|
|
187
|
+
if (validSources.length === 0)
|
|
188
|
+
return null;
|
|
189
|
+
const sources = [];
|
|
190
|
+
const collectedAudioTracks = [];
|
|
191
|
+
for (const s of validSources) {
|
|
192
|
+
const isHls = s.url.includes('.m3u8');
|
|
193
|
+
const isMp4 = s.url.includes('.mp4');
|
|
194
|
+
if (isHls) {
|
|
195
|
+
try {
|
|
196
|
+
const plRes = await fetch(s.url, {
|
|
197
|
+
headers: {
|
|
198
|
+
'User-Agent': headers['User-Agent'],
|
|
199
|
+
Referer: 'https://www.movy.bz/',
|
|
200
|
+
Origin: 'https://www.movy.bz',
|
|
201
|
+
},
|
|
202
|
+
signal: AbortSignal.timeout(6000),
|
|
203
|
+
});
|
|
204
|
+
if (!plRes.ok)
|
|
205
|
+
continue;
|
|
206
|
+
const playlist = await plRes.text();
|
|
207
|
+
if (!playlist.includes('#EXTM3U'))
|
|
208
|
+
continue;
|
|
209
|
+
for (const line of playlist.split('\n')) {
|
|
210
|
+
if (line.startsWith('#EXT-X-MEDIA:TYPE=AUDIO')) {
|
|
211
|
+
const language = line.match(/LANGUAGE="([^"]+)"/)?.[1] ?? 'unknown';
|
|
212
|
+
const label = line.match(/NAME="([^"]+)"/)?.[1] ?? 'Audio';
|
|
213
|
+
if (!collectedAudioTracks.find((t) => t.language === language && t.label === label)) {
|
|
214
|
+
collectedAudioTracks.push({ language, label });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const variantRegex = /#EXT-X-STREAM-INF:[^\n]*BANDWIDTH=(\d+)[^\n]*RESOLUTION=(\d+x\d+)[^\n]*(?:FRAME-RATE=([\d.]+))?[^\n]*\n([^\n]+)/g;
|
|
219
|
+
let match;
|
|
220
|
+
const variants = [];
|
|
221
|
+
while ((match = variantRegex.exec(playlist)) !== null) {
|
|
222
|
+
const resParts = match[2].split('x');
|
|
223
|
+
variants.push({
|
|
224
|
+
bandwidth: parseInt(match[1], 10),
|
|
225
|
+
width: parseInt(resParts[0], 10),
|
|
226
|
+
height: parseInt(resParts[1], 10),
|
|
227
|
+
frameRate: match[3] ? parseFloat(match[3]) : null,
|
|
228
|
+
uri: match[4],
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
if (variants.length > 0) {
|
|
232
|
+
let hasValidVariant = false;
|
|
233
|
+
for (const v of variants) {
|
|
234
|
+
const fullUrl = v.uri.startsWith('http') ? v.uri : new url_1.URL(v.uri, s.url).href;
|
|
235
|
+
try {
|
|
236
|
+
const headRes = await fetch(fullUrl, {
|
|
237
|
+
method: 'HEAD',
|
|
238
|
+
headers: {
|
|
239
|
+
'User-Agent': headers['User-Agent'],
|
|
240
|
+
Referer: 'https://www.movy.bz/',
|
|
241
|
+
Origin: 'https://www.movy.bz',
|
|
242
|
+
},
|
|
243
|
+
signal: AbortSignal.timeout(5000),
|
|
244
|
+
});
|
|
245
|
+
if (headRes.ok) {
|
|
246
|
+
hasValidVariant = true;
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
// ignore
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (hasValidVariant || variants.length > 0) {
|
|
255
|
+
sources.push({ url: s.url, quality: s.quality || 'Auto', type: 'hls' });
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
else if (playlist.includes('#EXTINF')) {
|
|
262
|
+
sources.push({ url: s.url, quality: s.quality || 'Auto', type: 'hls' });
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
try {
|
|
274
|
+
const headRes = await fetch(s.url, {
|
|
275
|
+
method: 'HEAD',
|
|
276
|
+
headers: {
|
|
277
|
+
'User-Agent': headers['User-Agent'],
|
|
278
|
+
Referer: 'https://www.movy.bz/',
|
|
279
|
+
Origin: 'https://www.movy.bz',
|
|
280
|
+
},
|
|
281
|
+
signal: AbortSignal.timeout(5000),
|
|
282
|
+
});
|
|
283
|
+
if (!headRes.ok)
|
|
284
|
+
continue;
|
|
285
|
+
sources.push({ url: s.url, quality: s.quality || 'Auto', type: isMp4 ? 'mp4' : 'hls' });
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (sources.length === 0)
|
|
293
|
+
return null;
|
|
294
|
+
return { sources, audioTracks: collectedAudioTracks };
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
function createTvRouter(apiCache) {
|
|
301
|
+
const router = (0, express_1.Router)();
|
|
302
|
+
router.get('/tv/search', async (req, res) => {
|
|
303
|
+
const query = req.query.q || '';
|
|
304
|
+
if (!query)
|
|
305
|
+
return res.json([]);
|
|
306
|
+
const page = parseInt(req.query.page) || 1;
|
|
307
|
+
const cacheKey = `tv-search-${query.toLowerCase()}-${page}`;
|
|
308
|
+
const cached = apiCache.get(cacheKey);
|
|
309
|
+
if (cached)
|
|
310
|
+
return res.json(cached);
|
|
311
|
+
const key = await (0, tmdb_1.getTmdbKey)();
|
|
312
|
+
if (!key)
|
|
313
|
+
return res.status(500).json({ error: 'No TMDB API key available' });
|
|
314
|
+
try {
|
|
315
|
+
const r = await fetch(`${tmdb_1.TMDB_BASE}/search/multi?api_key=${key}&query=${encodeURIComponent(query)}&page=${page}&include_adult=true`, { headers: { 'User-Agent': 'Mozilla/5.0' } });
|
|
316
|
+
if (!r.ok)
|
|
317
|
+
return res.status(500).json({ error: 'TMDB search failed' });
|
|
318
|
+
const d = await r.json();
|
|
319
|
+
const results = (d.results || [])
|
|
320
|
+
.filter((item) => item.media_type !== 'person')
|
|
321
|
+
.map((item) => ({
|
|
322
|
+
id: item.id,
|
|
323
|
+
title: item.title || item.name,
|
|
324
|
+
year: (item.release_date || item.first_air_date || '').split('-')[0],
|
|
325
|
+
type: item.media_type,
|
|
326
|
+
image: item.poster_path ? `${tmdb_1.TMDB_IMAGE}/w500${item.poster_path}` : '',
|
|
327
|
+
vote_average: item.vote_average,
|
|
328
|
+
adult: item.adult === true,
|
|
329
|
+
}));
|
|
330
|
+
apiCache.set(cacheKey, results, 3600);
|
|
331
|
+
res.json(results);
|
|
332
|
+
}
|
|
333
|
+
catch (e) {
|
|
334
|
+
res.status(500).json({ error: e.message });
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
router.get('/tv/details/:type/:id', async (req, res) => {
|
|
338
|
+
const { type, id } = req.params;
|
|
339
|
+
const key = await (0, tmdb_1.getTmdbKey)();
|
|
340
|
+
if (!key)
|
|
341
|
+
return res.status(500).json({ error: 'No TMDB API key available' });
|
|
342
|
+
try {
|
|
343
|
+
const r = await fetch(`${tmdb_1.TMDB_BASE}/${type}/${id}?api_key=${key}&append_to_response=external_ids`, { headers: { 'User-Agent': 'Mozilla/5.0' } });
|
|
344
|
+
if (!r.ok)
|
|
345
|
+
return res.status(500).json({ error: 'TMDB details failed' });
|
|
346
|
+
const d = await r.json();
|
|
347
|
+
const result = {
|
|
348
|
+
id: d.id,
|
|
349
|
+
imdb_id: d.external_ids?.imdb_id || d.imdb_id || null,
|
|
350
|
+
title: d.title || d.name,
|
|
351
|
+
overview: d.overview,
|
|
352
|
+
vote_average: d.vote_average,
|
|
353
|
+
year: (d.release_date || d.first_air_date || '').split('-')[0],
|
|
354
|
+
poster: d.poster_path ? `${tmdb_1.TMDB_IMAGE}/w500${d.poster_path}` : '',
|
|
355
|
+
backdrop: d.backdrop_path ? `${tmdb_1.TMDB_IMAGE}/original${d.backdrop_path}` : '',
|
|
356
|
+
adult: d.adult === true,
|
|
357
|
+
};
|
|
358
|
+
if (type === 'tv') {
|
|
359
|
+
result.seasons = (d.seasons || [])
|
|
360
|
+
.filter((s) => s.season_number > 0)
|
|
361
|
+
.map((s) => ({
|
|
362
|
+
season_number: s.season_number,
|
|
363
|
+
episode_count: s.episode_count,
|
|
364
|
+
}));
|
|
365
|
+
result.number_of_seasons = d.number_of_seasons;
|
|
366
|
+
}
|
|
367
|
+
res.json(result);
|
|
368
|
+
}
|
|
369
|
+
catch (e) {
|
|
370
|
+
res.status(500).json({ error: e.message });
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
router.get('/tv/episodes/:id/:season', async (req, res) => {
|
|
374
|
+
const { id, season } = req.params;
|
|
375
|
+
const key = await (0, tmdb_1.getTmdbKey)();
|
|
376
|
+
if (!key)
|
|
377
|
+
return res.status(500).json({ error: 'No TMDB API key available' });
|
|
378
|
+
try {
|
|
379
|
+
const r = await fetch(`${tmdb_1.TMDB_BASE}/tv/${id}/season/${season}?api_key=${key}`, {
|
|
380
|
+
headers: { 'User-Agent': 'Mozilla/5.0' },
|
|
381
|
+
});
|
|
382
|
+
if (!r.ok)
|
|
383
|
+
return res.status(500).json({ error: 'TMDB episodes failed' });
|
|
384
|
+
const d = await r.json();
|
|
385
|
+
const episodes = (d.episodes || []).map((ep) => ({
|
|
386
|
+
episode_number: ep.episode_number,
|
|
387
|
+
name: ep.name,
|
|
388
|
+
vote_average: ep.vote_average,
|
|
389
|
+
overview: ep.overview,
|
|
390
|
+
still_path: ep.still_path,
|
|
391
|
+
}));
|
|
392
|
+
res.json({ episodes });
|
|
393
|
+
}
|
|
394
|
+
catch (e) {
|
|
395
|
+
res.status(500).json({ error: e.message });
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
router.get('/tv/lookup/imdb-to-tmdb/:imdbId', async (req, res) => {
|
|
399
|
+
const key = await (0, tmdb_1.getTmdbKey)();
|
|
400
|
+
if (!key)
|
|
401
|
+
return res.json({ tmdbId: null, error: 'No TMDB API key available' });
|
|
402
|
+
try {
|
|
403
|
+
const r = await fetch(`${tmdb_1.TMDB_BASE}/find/${req.params.imdbId}?api_key=${key}&external_source=imdb_id`, { headers: { 'User-Agent': 'Mozilla/5.0' } });
|
|
404
|
+
if (!r.ok)
|
|
405
|
+
return res.json({ tmdbId: null, error: `TMDB error ${r.status}` });
|
|
406
|
+
const d = await r.json();
|
|
407
|
+
const movie = d.movie_results?.[0];
|
|
408
|
+
const tv = d.tv_results?.[0];
|
|
409
|
+
const result = movie || tv;
|
|
410
|
+
res.json({
|
|
411
|
+
tmdbId: result?.id || null,
|
|
412
|
+
type: movie ? 'movie' : tv ? 'tv' : null,
|
|
413
|
+
title: result?.title || result?.name || null,
|
|
414
|
+
year: (result?.release_date || result?.first_air_date || '').substring(0, 4) || null,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
catch (e) {
|
|
418
|
+
res.json({ tmdbId: null, error: e.message });
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
router.get('/tv/search/imdb', async (req, res) => {
|
|
422
|
+
const query = req.query.q || '';
|
|
423
|
+
if (!query)
|
|
424
|
+
return res.json([]);
|
|
425
|
+
const cacheKey = `tv-imdb-search-${query.toLowerCase()}`;
|
|
426
|
+
const cached = apiCache.get(cacheKey);
|
|
427
|
+
if (cached)
|
|
428
|
+
return res.json(cached);
|
|
429
|
+
try {
|
|
430
|
+
const response = await fetch(`https://v3.sg.media-imdb.com/suggestion/x/${encodeURIComponent(query)}.json`, { headers: { 'User-Agent': 'Mozilla/5.0' } });
|
|
431
|
+
if (!response.ok)
|
|
432
|
+
return res.status(500).json({ error: 'IMDB search failed' });
|
|
433
|
+
const data = await response.json();
|
|
434
|
+
const TV_TYPES = new Set(['tvSeries', 'tvMiniSeries', 'movie']);
|
|
435
|
+
const matches = (data.d || [])
|
|
436
|
+
.filter((entry) => Boolean(entry.id && entry.l) && TV_TYPES.has(entry.qid || ''))
|
|
437
|
+
.slice(0, 10)
|
|
438
|
+
.map((entry) => ({
|
|
439
|
+
id: entry.id,
|
|
440
|
+
title: entry.l,
|
|
441
|
+
year: entry.y,
|
|
442
|
+
type: entry.qid,
|
|
443
|
+
image: entry.i?.imageUrl || '',
|
|
444
|
+
}));
|
|
445
|
+
apiCache.set(cacheKey, matches, 3600);
|
|
446
|
+
res.json(matches);
|
|
447
|
+
}
|
|
448
|
+
catch (e) {
|
|
449
|
+
res.status(500).json({ error: e.message });
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
router.get('/tv/subtitles/:type/:tmdbId', async (req, res) => {
|
|
453
|
+
const { type, tmdbId } = req.params;
|
|
454
|
+
const numericTmdbId = parseInt(tmdbId, 10);
|
|
455
|
+
if (!numericTmdbId)
|
|
456
|
+
return res.json({ subtitles: [] });
|
|
457
|
+
const mediaType = type === 'movie' ? 'movie' : 'tv';
|
|
458
|
+
const season = String(req.query.season || '1');
|
|
459
|
+
const episode = String(req.query.episode || '1');
|
|
460
|
+
const cacheKey = mediaType === 'tv'
|
|
461
|
+
? `tv-subs-${numericTmdbId}-${season}-${episode}`
|
|
462
|
+
: `tv-subs-${numericTmdbId}-movie`;
|
|
463
|
+
const cached = apiCache.get(cacheKey);
|
|
464
|
+
if (cached)
|
|
465
|
+
return res.json(cached);
|
|
466
|
+
try {
|
|
467
|
+
const searchUrl = mediaType === 'tv'
|
|
468
|
+
? `https://subtitles.vidy.st/search?id=${numericTmdbId}&season=${encodeURIComponent(season)}&episode=${encodeURIComponent(episode)}`
|
|
469
|
+
: `https://subtitles.vidy.st/search?id=${numericTmdbId}`;
|
|
470
|
+
const r = await fetch(searchUrl, {
|
|
471
|
+
headers: { 'User-Agent': 'Mozilla/5.0' },
|
|
472
|
+
signal: AbortSignal.timeout(10000),
|
|
473
|
+
});
|
|
474
|
+
if (!r.ok)
|
|
475
|
+
return res.json({ subtitles: [] });
|
|
476
|
+
const items = (await r.json());
|
|
477
|
+
const subtitles = (Array.isArray(items) ? items : [])
|
|
478
|
+
.filter((s) => typeof s.url === 'string' && s.url.startsWith('https://'))
|
|
479
|
+
.map((s) => ({
|
|
480
|
+
language: String(s.language || 'en'),
|
|
481
|
+
label: String(s.display || s.language || 'Unknown'),
|
|
482
|
+
url: String(s.url),
|
|
483
|
+
trusted: s.isTrusted === true,
|
|
484
|
+
hi: s.isHearingImpaired === true,
|
|
485
|
+
downloads: Number(s.downloadCount) || 0,
|
|
486
|
+
media: String(s.media || s.release || ''),
|
|
487
|
+
}))
|
|
488
|
+
.sort((a, b) => {
|
|
489
|
+
const aEn = a.language.toLowerCase().startsWith('en') ? 0 : 1;
|
|
490
|
+
const bEn = b.language.toLowerCase().startsWith('en') ? 0 : 1;
|
|
491
|
+
if (aEn !== bEn)
|
|
492
|
+
return aEn - bEn;
|
|
493
|
+
if (a.trusted !== b.trusted)
|
|
494
|
+
return a.trusted ? -1 : 1;
|
|
495
|
+
return b.downloads - a.downloads;
|
|
496
|
+
})
|
|
497
|
+
.slice(0, 50);
|
|
498
|
+
const payload = { subtitles };
|
|
499
|
+
apiCache.set(cacheKey, payload, 3600);
|
|
500
|
+
res.json(payload);
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
res.json({ subtitles: [] });
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
router.get('/tv/movybz/:type/:tmdbId', async (req, res) => {
|
|
507
|
+
const { type, tmdbId } = req.params;
|
|
508
|
+
const mediaType = type === 'movie' ? 'movie' : 'tv';
|
|
509
|
+
const season = String(req.query.season || '1');
|
|
510
|
+
const episode = String(req.query.episode || '1');
|
|
511
|
+
const numericTmdbId = parseInt(tmdbId, 10);
|
|
512
|
+
let title = String(req.query.title || '');
|
|
513
|
+
let year = String(req.query.year || '');
|
|
514
|
+
let imdbId = String(req.query.imdbId || '');
|
|
515
|
+
let totalSeasons = String(req.query.totalSeasons || '1');
|
|
516
|
+
if (!title || !imdbId || !year) {
|
|
517
|
+
try {
|
|
518
|
+
const tmdbKey = await (0, tmdb_1.getTmdbKey)();
|
|
519
|
+
if (tmdbKey) {
|
|
520
|
+
const tmdbRes = await fetch(`${tmdb_1.TMDB_BASE}/${mediaType}/${numericTmdbId}?api_key=${tmdbKey}&append_to_response=external_ids`, { headers: { 'User-Agent': 'Mozilla/5.0' }, signal: AbortSignal.timeout(4000) });
|
|
521
|
+
if (tmdbRes.ok) {
|
|
522
|
+
const d = await tmdbRes.json();
|
|
523
|
+
if (!title)
|
|
524
|
+
title = d.title || d.name || '';
|
|
525
|
+
if (!year)
|
|
526
|
+
year = (d.release_date || d.first_air_date || '').split('-')[0] || '';
|
|
527
|
+
if (!imdbId)
|
|
528
|
+
imdbId = d.external_ids?.imdb_id || d.imdb_id || '';
|
|
529
|
+
if (mediaType === 'tv' && d.number_of_seasons)
|
|
530
|
+
totalSeasons = String(d.number_of_seasons);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
catch {
|
|
535
|
+
// ignore
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
const headers = {
|
|
539
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
540
|
+
Accept: 'application/json, text/plain, */*',
|
|
541
|
+
Referer: 'https://www.movy.bz/',
|
|
542
|
+
Origin: 'https://www.movy.bz',
|
|
543
|
+
};
|
|
544
|
+
const seed = await movyGetSeed(numericTmdbId);
|
|
545
|
+
if (!seed) {
|
|
546
|
+
return res.status(500).json({ error: 'Seed unavailable', tmdbId: numericTmdbId });
|
|
547
|
+
}
|
|
548
|
+
const validSeed = seed;
|
|
549
|
+
const baseParams = {
|
|
550
|
+
title,
|
|
551
|
+
mediaType,
|
|
552
|
+
year,
|
|
553
|
+
tmdbId: String(numericTmdbId),
|
|
554
|
+
imdbId,
|
|
555
|
+
enc: '2',
|
|
556
|
+
seed: validSeed,
|
|
557
|
+
};
|
|
558
|
+
if (mediaType === 'tv') {
|
|
559
|
+
baseParams.totalSeasons = totalSeasons;
|
|
560
|
+
baseParams.seasonId = season;
|
|
561
|
+
baseParams.episodeId = episode;
|
|
562
|
+
}
|
|
563
|
+
for (const city of MOVY_SERVERS) {
|
|
564
|
+
const result = await tryMovyCity(city, baseParams, validSeed, numericTmdbId, headers);
|
|
565
|
+
if (result) {
|
|
566
|
+
return res.json({ server: city, sources: result.sources, audioTracks: result.audioTracks });
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
res.json({ sources: [], audioTracks: [], error: 'No sources found from Movy servers' });
|
|
570
|
+
});
|
|
571
|
+
router.get('/tv/movybz/:type/:tmdbId/probe/:city', async (req, res) => {
|
|
572
|
+
const { type, tmdbId, city } = req.params;
|
|
573
|
+
if (!MOVY_SERVERS.includes(city)) {
|
|
574
|
+
return res.status(400).json({ error: 'Invalid city', valid: false });
|
|
575
|
+
}
|
|
576
|
+
const mediaType = type === 'movie' ? 'movie' : 'tv';
|
|
577
|
+
const season = String(req.query.season || '1');
|
|
578
|
+
const episode = String(req.query.episode || '1');
|
|
579
|
+
const numericTmdbId = parseInt(tmdbId, 10);
|
|
580
|
+
let title = String(req.query.title || '');
|
|
581
|
+
let year = String(req.query.year || '');
|
|
582
|
+
let imdbId = String(req.query.imdbId || '');
|
|
583
|
+
let totalSeasons = String(req.query.totalSeasons || '1');
|
|
584
|
+
if (!title || !imdbId || !year) {
|
|
585
|
+
try {
|
|
586
|
+
const tmdbKey = await (0, tmdb_1.getTmdbKey)();
|
|
587
|
+
if (tmdbKey) {
|
|
588
|
+
const tmdbRes = await fetch(`${tmdb_1.TMDB_BASE}/${mediaType}/${numericTmdbId}?api_key=${tmdbKey}&append_to_response=external_ids`, { headers: { 'User-Agent': 'Mozilla/5.0' }, signal: AbortSignal.timeout(4000) });
|
|
589
|
+
if (tmdbRes.ok) {
|
|
590
|
+
const d = await tmdbRes.json();
|
|
591
|
+
if (!title)
|
|
592
|
+
title = d.title || d.name || '';
|
|
593
|
+
if (!year)
|
|
594
|
+
year = (d.release_date || d.first_air_date || '').split('-')[0] || '';
|
|
595
|
+
if (!imdbId)
|
|
596
|
+
imdbId = d.external_ids?.imdb_id || d.imdb_id || '';
|
|
597
|
+
if (mediaType === 'tv' && d.number_of_seasons)
|
|
598
|
+
totalSeasons = String(d.number_of_seasons);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
catch {
|
|
603
|
+
// ignore
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
const headers = {
|
|
607
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
608
|
+
Accept: 'application/json, text/plain, */*',
|
|
609
|
+
Referer: 'https://www.movy.bz/',
|
|
610
|
+
Origin: 'https://www.movy.bz',
|
|
611
|
+
};
|
|
612
|
+
const seed = await movyGetSeed(numericTmdbId);
|
|
613
|
+
if (!seed)
|
|
614
|
+
return res.json({
|
|
615
|
+
server: city,
|
|
616
|
+
sources: [],
|
|
617
|
+
audioTracks: [],
|
|
618
|
+
valid: false,
|
|
619
|
+
error: 'Seed unavailable',
|
|
620
|
+
});
|
|
621
|
+
const baseParams = {
|
|
622
|
+
title,
|
|
623
|
+
mediaType,
|
|
624
|
+
year,
|
|
625
|
+
tmdbId: String(numericTmdbId),
|
|
626
|
+
imdbId,
|
|
627
|
+
enc: '2',
|
|
628
|
+
seed,
|
|
629
|
+
};
|
|
630
|
+
if (mediaType === 'tv') {
|
|
631
|
+
baseParams.totalSeasons = totalSeasons;
|
|
632
|
+
baseParams.seasonId = season;
|
|
633
|
+
baseParams.episodeId = episode;
|
|
634
|
+
}
|
|
635
|
+
const result = await tryMovyCity(city, baseParams, seed, numericTmdbId, headers);
|
|
636
|
+
if (result)
|
|
637
|
+
return res.json({
|
|
638
|
+
server: city,
|
|
639
|
+
sources: result.sources,
|
|
640
|
+
audioTracks: result.audioTracks,
|
|
641
|
+
valid: true,
|
|
642
|
+
});
|
|
643
|
+
return res.json({
|
|
644
|
+
server: city,
|
|
645
|
+
sources: [],
|
|
646
|
+
audioTracks: [],
|
|
647
|
+
valid: false,
|
|
648
|
+
error: 'No valid sources for this city',
|
|
649
|
+
});
|
|
650
|
+
});
|
|
651
|
+
router.get('/tv/vixsrc/:type/:tmdbId', async (req, res) => {
|
|
652
|
+
const { type, tmdbId } = req.params;
|
|
653
|
+
const season = req.query.season;
|
|
654
|
+
const episode = req.query.episode;
|
|
655
|
+
const mediaType = type === 'movie' ? 'movie' : 'tv';
|
|
656
|
+
const BASE_URL = 'https://vixsrc.to';
|
|
657
|
+
const HEADERS = {
|
|
658
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150 Safari/537.36',
|
|
659
|
+
Accept: 'application/json, text/javascript, */*; q=0.01',
|
|
660
|
+
Referer: BASE_URL,
|
|
661
|
+
Origin: BASE_URL,
|
|
662
|
+
};
|
|
663
|
+
try {
|
|
664
|
+
const pageUrl = mediaType === 'movie'
|
|
665
|
+
? `${BASE_URL}/api/movie/${tmdbId}`
|
|
666
|
+
: `${BASE_URL}/api/tv/${tmdbId}/${season}/${episode}`;
|
|
667
|
+
const apiRes = await fetch(pageUrl, { headers: HEADERS });
|
|
668
|
+
if (!apiRes.ok) {
|
|
669
|
+
return res.status(500).json({ error: 'VixSrc API failed', status: apiRes.status });
|
|
670
|
+
}
|
|
671
|
+
const apiData = await apiRes.json();
|
|
672
|
+
if (!apiData?.src) {
|
|
673
|
+
return res.json({ sources: [] });
|
|
674
|
+
}
|
|
675
|
+
const htmlUrl = BASE_URL + apiData.src;
|
|
676
|
+
const htmlRes = await fetch(htmlUrl, {
|
|
677
|
+
headers: { ...HEADERS, Accept: 'text/html,application/xhtml+xml,*/*' },
|
|
678
|
+
});
|
|
679
|
+
if (!htmlRes.ok) {
|
|
680
|
+
return res.status(500).json({ error: 'VixSrc embed failed' });
|
|
681
|
+
}
|
|
682
|
+
const html = await htmlRes.text();
|
|
683
|
+
const token = html.match(/token["']\s*:\s*["']([^"']+)/)?.[1];
|
|
684
|
+
const expires = html.match(/expires["']\s*:\s*["']([^"']+)/)?.[1];
|
|
685
|
+
const playlist = html.match(/url\s*:\s*["']([^"']+)/)?.[1];
|
|
686
|
+
if (!token || !expires || !playlist) {
|
|
687
|
+
return res.json({ sources: [] });
|
|
688
|
+
}
|
|
689
|
+
const sep = playlist.includes('?') ? '&' : '?';
|
|
690
|
+
const masterUrl = `${playlist}${sep}token=${token}&expires=${expires}&h=1`;
|
|
691
|
+
const plRes = await fetch(masterUrl, { headers: { ...HEADERS, Referer: pageUrl } });
|
|
692
|
+
if (!plRes.ok) {
|
|
693
|
+
return res.status(500).json({ error: 'VixSrc playlist failed' });
|
|
694
|
+
}
|
|
695
|
+
const playlistContent = await plRes.text();
|
|
696
|
+
const regex = /#EXT-X-STREAM-INF:[^\n]*RESOLUTION=\d+x(\d+)[^\n]*\n([^\n]+)/g;
|
|
697
|
+
let match;
|
|
698
|
+
let bestResolution = 0;
|
|
699
|
+
while ((match = regex.exec(playlistContent)) !== null) {
|
|
700
|
+
const resVal = parseInt(match[1], 10);
|
|
701
|
+
if (resVal > bestResolution)
|
|
702
|
+
bestResolution = resVal;
|
|
703
|
+
}
|
|
704
|
+
const sources = bestResolution > 0 ? [{ url: masterUrl, quality: `${bestResolution}p`, type: 'hls' }] : [];
|
|
705
|
+
const audioTracks = [];
|
|
706
|
+
const subtitles = [];
|
|
707
|
+
for (const line of playlistContent.split('\n')) {
|
|
708
|
+
if (line.startsWith('#EXT-X-MEDIA:TYPE=AUDIO')) {
|
|
709
|
+
const language = line.match(/LANGUAGE="([^"]+)"/)?.[1] ?? 'unknown';
|
|
710
|
+
const label = line.match(/NAME="([^"]+)"/)?.[1] ?? 'Audio';
|
|
711
|
+
audioTracks.push({ language, label });
|
|
712
|
+
}
|
|
713
|
+
else if (line.startsWith('#EXT-X-MEDIA:TYPE=SUBTITLES')) {
|
|
714
|
+
const language = line.match(/LANGUAGE="([^"]+)"/)?.[1] ?? 'unknown';
|
|
715
|
+
const label = line.match(/NAME="([^"]+)"/)?.[1] ?? 'Subs';
|
|
716
|
+
const uri = line.match(/URI="([^"]+)"/)?.[1];
|
|
717
|
+
if (uri) {
|
|
718
|
+
subtitles.push({ language, label, url: new url_1.URL(uri, masterUrl).href });
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
res.json({ sources, audioTracks, subtitles, masterUrl, referer: pageUrl });
|
|
723
|
+
}
|
|
724
|
+
catch (e) {
|
|
725
|
+
res.status(500).json({ error: 'VixSrc fetch failed', message: e.message });
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
router.get('/tv/stream-proxy', async (req, res) => {
|
|
729
|
+
const { url, referer } = req.query;
|
|
730
|
+
const urlStr = url;
|
|
731
|
+
const refererStr = referer || '';
|
|
732
|
+
if (!urlStr)
|
|
733
|
+
return res.status(400).send('URL required');
|
|
734
|
+
const safeCheck = (0, security_utils_1.isSafeExternalUrl)(urlStr);
|
|
735
|
+
if (!safeCheck.safe) {
|
|
736
|
+
return res.status(400).send(safeCheck.error || 'Invalid URL');
|
|
737
|
+
}
|
|
738
|
+
const abortController = new AbortController();
|
|
739
|
+
const timeout = setTimeout(() => abortController.abort(), 30000);
|
|
740
|
+
res.on('close', () => {
|
|
741
|
+
clearTimeout(timeout);
|
|
742
|
+
abortController.abort();
|
|
743
|
+
});
|
|
744
|
+
try {
|
|
745
|
+
const headers = {
|
|
746
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
747
|
+
};
|
|
748
|
+
if (refererStr)
|
|
749
|
+
headers['Referer'] = refererStr;
|
|
750
|
+
const fetchResp = await fetch(urlStr, {
|
|
751
|
+
headers,
|
|
752
|
+
signal: abortController.signal,
|
|
753
|
+
redirect: 'follow',
|
|
754
|
+
});
|
|
755
|
+
const status = fetchResp.status;
|
|
756
|
+
if (status !== 200 && status !== 206) {
|
|
757
|
+
return res.status(status ?? 502).send('Upstream error');
|
|
758
|
+
}
|
|
759
|
+
const contentType = fetchResp.headers.get('content-type') || 'application/octet-stream';
|
|
760
|
+
const contentLength = fetchResp.headers.get('content-length');
|
|
761
|
+
const contentRange = fetchResp.headers.get('content-range');
|
|
762
|
+
const acceptRanges = fetchResp.headers.get('accept-ranges');
|
|
763
|
+
res.status(status);
|
|
764
|
+
res.set('Content-Type', contentType);
|
|
765
|
+
if (contentLength)
|
|
766
|
+
res.set('Content-Length', contentLength);
|
|
767
|
+
if (contentRange)
|
|
768
|
+
res.set('Content-Range', contentRange);
|
|
769
|
+
if (acceptRanges)
|
|
770
|
+
res.set('Accept-Ranges', acceptRanges);
|
|
771
|
+
res.set('Access-Control-Allow-Origin', '*');
|
|
772
|
+
res.set('Connection', 'keep-alive');
|
|
773
|
+
if (urlStr.includes('.m3u8')) {
|
|
774
|
+
const body = await fetchResp.text();
|
|
775
|
+
const baseUrl = new url_1.URL(fetchResp.url || urlStr);
|
|
776
|
+
const proxiedMediaUrl = (targetUrl) => `/api/tv/stream-proxy?url=${encodeURIComponent(targetUrl)}&referer=${encodeURIComponent(refererStr)}`;
|
|
777
|
+
const rewritten = body
|
|
778
|
+
.split('\n')
|
|
779
|
+
.map((line) => {
|
|
780
|
+
const trimmed = line.trim();
|
|
781
|
+
if (!trimmed)
|
|
782
|
+
return line;
|
|
783
|
+
if (trimmed.startsWith('#')) {
|
|
784
|
+
return trimmed.replace(/URI="([^"]+)"/g, (_, uri) => {
|
|
785
|
+
const absolute = new url_1.URL(uri, baseUrl).href;
|
|
786
|
+
return `URI="${proxiedMediaUrl(absolute)}"`;
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
const absolute = new url_1.URL(trimmed, baseUrl).href;
|
|
790
|
+
return proxiedMediaUrl(absolute);
|
|
791
|
+
})
|
|
792
|
+
.join('\n');
|
|
793
|
+
const bodyBuffer = Buffer.from(rewritten, 'utf8');
|
|
794
|
+
res.set('Content-Type', 'application/vnd.apple.mpegurl');
|
|
795
|
+
res.set('Content-Length', String(bodyBuffer.length));
|
|
796
|
+
if (!res.headersSent) {
|
|
797
|
+
res.send(bodyBuffer);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
else {
|
|
801
|
+
const chunks = [];
|
|
802
|
+
const reader = fetchResp.body?.getReader();
|
|
803
|
+
if (!reader) {
|
|
804
|
+
return res.status(500).send('No response body');
|
|
805
|
+
}
|
|
806
|
+
try {
|
|
807
|
+
while (true) {
|
|
808
|
+
const { done, value } = await reader.read();
|
|
809
|
+
if (done)
|
|
810
|
+
break;
|
|
811
|
+
chunks.push(Buffer.from(value));
|
|
812
|
+
}
|
|
813
|
+
const body = Buffer.concat(chunks);
|
|
814
|
+
res.set('Content-Type', contentType);
|
|
815
|
+
if (contentLength)
|
|
816
|
+
res.set('Content-Length', contentLength);
|
|
817
|
+
if (contentRange)
|
|
818
|
+
res.set('Content-Range', contentRange);
|
|
819
|
+
if (acceptRanges)
|
|
820
|
+
res.set('Accept-Ranges', acceptRanges);
|
|
821
|
+
res.set('Access-Control-Allow-Origin', '*');
|
|
822
|
+
res.set('Connection', 'keep-alive');
|
|
823
|
+
if (!res.headersSent) {
|
|
824
|
+
res.send(body);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
catch (e) {
|
|
828
|
+
if (!res.headersSent) {
|
|
829
|
+
res.status(500).send('Proxy error');
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
catch (e) {
|
|
835
|
+
if (abortController.signal.aborted)
|
|
836
|
+
return;
|
|
837
|
+
if (!res.headersSent) {
|
|
838
|
+
res.status(500).send('Proxy error');
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
});
|
|
842
|
+
return router;
|
|
843
|
+
}
|