@serifpersia/dango 3.6.5 → 3.6.7
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/README.md +1 -1
- package/client/dist/assets/{AnimeCard-JkXG40Es.js → AnimeCard-C3P9zWi9.js} +1 -1
- package/client/dist/assets/{AnimeInfo-2L0UJkbB.js → AnimeInfo-Et6687Tl.js} +1 -1
- package/client/dist/assets/{AnimeInfoPage-BDs03vKk.js → AnimeInfoPage-DX4eNnlH.js} +2 -2
- package/client/dist/assets/{Asmr-D2mVKEl9.js → Asmr-CCdEWrFi.js} +1 -1
- package/client/dist/assets/{AsmrResetProgressModal-MQA_bNQM.js → AsmrResetProgressModal-DgEWBJOw.js} +1 -1
- package/client/dist/assets/AvSyncCalibrator-CQ2fjf2E.js +10 -0
- package/client/dist/assets/{Home-DxMBHFmc.js → Home-DUkUTTTy.js} +1 -1
- package/client/dist/assets/{Insights-hmLWsSXY.js → Insights-QCh4xW12.js} +1 -1
- package/client/dist/assets/{LibraryListPage-CWI7hgUh.js → LibraryListPage-BoTxRqHz.js} +1 -1
- package/client/dist/assets/{ListeningList-NOc28KAV.js → ListeningList-DguKjI_t.js} +1 -1
- package/client/dist/assets/{Manga-B5qNhI7v.js → Manga-HydR-L7o.js} +1 -1
- package/client/dist/assets/{MangaInfoPage-D4wIyb-V.js → MangaInfoPage-CBakEI9a.js} +1 -1
- package/client/dist/assets/{MangaReadPage-Ce1VX7-0.js → MangaReadPage-DwAR3iPy.js} +1 -1
- package/client/dist/assets/{MangaResetProgressModal-P3oY4_14.js → MangaResetProgressModal-DGzl8vsO.js} +1 -1
- package/client/dist/assets/{Mature-D8EwGYQq.js → Mature-Pi0TxKuF.js} +1 -1
- package/client/dist/assets/{MediaCard-Dp8DjyXB.js → MediaCard--cuFxOCS.js} +1 -1
- package/client/dist/assets/{Music-DULtVsAM.js → Music-CQpyXTLH.js} +1 -1
- package/client/dist/assets/{OptionListMenu-fIHdE-nq.js → OptionListMenu-B3D39dGS.js} +1 -1
- package/client/dist/assets/{Player-B8XeOmso.js → Player-CsH4x_I_.js} +6 -6
- package/client/dist/assets/{PlayerSettings-BycNCiCs.js → PlayerSettings-DGxBsi3F.js} +1 -1
- package/client/dist/assets/{QueueOptionsButton-T1J37IZT.js → QueueOptionsButton-Djq4Cpex.js} +1 -1
- package/client/dist/assets/{QueueRail-BAwI-apz.js → QueueRail-DOrYgNVX.js} +1 -1
- package/client/dist/assets/{Radio-Bv9DyKtZ.js → Radio-DF6g6odd.js} +1 -1
- package/client/dist/assets/{ReadingList-Cl0CcVp3.js → ReadingList-DwYoBrNk.js} +1 -1
- package/client/dist/assets/{ResetProgressModal--N60WWCB.js → ResetProgressModal-CMF86mX6.js} +1 -1
- package/client/dist/assets/{Search-DvgVDlUR.js → Search-DFkhuk-x.js} +1 -1
- package/client/dist/assets/{Settings-dCz_-bJn.js → Settings-DBB2r0VQ.js} +1 -1
- package/client/dist/assets/{SkeletonGrid-BZHKk1I6.js → SkeletonGrid-DJelJgAv.js} +1 -1
- package/client/dist/assets/{Trackers-C1T5VcPu.js → Trackers-DGYXFhYZ.js} +1 -1
- package/client/dist/assets/{Tv-Dhv_cg4y.js → Tv-C5MDGD2Z.js} +4 -4
- package/client/dist/assets/{TvInfo-DgaLK2H6.js → TvInfo-DrKuK13u.js} +1 -1
- package/client/dist/assets/{TvPopup-DyX7f8kC.js → TvPopup-DtHilRDT.js} +1 -1
- package/client/dist/assets/{TvSearch-Bp0H6kue.js → TvSearch-CzNxdu_w.js} +1 -1
- package/client/dist/assets/{TvWatchlist-ohMtMeKa.js → TvWatchlist-D7p4br6S.js} +1 -1
- package/client/dist/assets/{Watchlist-BbQtzsER.js → Watchlist-DrOzswl2.js} +1 -1
- package/client/dist/assets/{hls-Bi1cjpLZ.js → hls-BGCX7mJy.js} +1 -1
- package/client/dist/assets/{index-BTnhzofk.js → index-CnfZwsds.js} +2 -2
- package/client/dist/assets/{useAnimeInfoData-Cvmso3IK.js → useAnimeInfoData-DYXWBPF-.js} +1 -1
- package/client/dist/assets/{useAsmr-CbzGCzmv.js → useAsmr-BbhQ59bk.js} +1 -1
- package/client/dist/assets/{useAsmrPopup-DaREZ9Ie.js → useAsmrPopup-SymV1J-b.js} +1 -1
- package/client/dist/assets/{useMangaLibrary-BVQRhsFh.js → useMangaLibrary-DuesWQaE.js} +1 -1
- package/client/dist/assets/{useMangaPopup-CzsCbndG.js → useMangaPopup-D57OVGUr.js} +1 -1
- package/client/dist/assets/{useProviders-UuUPzft-.js → useProviders-mpQb_dix.js} +1 -1
- package/client/dist/assets/{useTv-CSsbWKH9.js → useTv-DGflCsSI.js} +1 -1
- package/client/dist/assets/{useTvLibrary-a_Y0c2FS.js → useTvLibrary-BqeSmyc4.js} +1 -1
- package/client/dist/index.html +1 -1
- package/package.json +2 -2
- package/server/dist/app-hono.js +1 -1
- package/server/dist/hono/proxy.js +105 -227
- package/server/dist/hono/tv.js +33 -2
- package/server/dist/providers/remote-loader.js +12 -3
- package/server/dist/providers/remote-types.js +35 -0
- package/server/dist/server.js +3 -0
- package/server/package.json +1 -1
- package/client/dist/assets/AvSyncCalibrator-DmoH5eRA.js +0 -10
- package/server/dist/utils/megaplay.utils.js +0 -40
|
@@ -7,7 +7,6 @@ import logger from '../logger.js';
|
|
|
7
7
|
import { buildCfClearanceCookie, sanitizeCfClearance } from '../utils/cookie.utils.js';
|
|
8
8
|
import { isSafeExternalUrl } from '../utils/security.utils.js';
|
|
9
9
|
import { fetchWithRetry, linkAbort, setProxyHeaders } from '../utils/http.utils.js';
|
|
10
|
-
import { MEGAPLAY_ORIGIN, isNexabloomMasterUrl, signNexabloomMasterUrl, } from '../utils/megaplay.utils.js';
|
|
11
10
|
const proxyCache = new AppCache({ ttlSeconds: 30, maxKeys: 500 });
|
|
12
11
|
const imageCache = new AppCache({ ttlSeconds: 86400, maxKeys: 3000 });
|
|
13
12
|
function assTimeToVtt(value) {
|
|
@@ -127,87 +126,45 @@ function rewrapVttCues(vtt) {
|
|
|
127
126
|
})
|
|
128
127
|
.join('\n\n');
|
|
129
128
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const VAULT_CDN_HOSTS = new Set(['uwucdn.top', 'owocdn.top']);
|
|
133
|
-
const GOT_SCRAPING_HOSTS = new Set([
|
|
134
|
-
'kwik.cx',
|
|
135
|
-
'kwik.si',
|
|
136
|
-
'kwik.pro',
|
|
137
|
-
'animepahe.pw',
|
|
138
|
-
'animepahe.ru',
|
|
139
|
-
]);
|
|
140
|
-
const GOT_SCRAPING_SUFFIXES = [
|
|
141
|
-
'.uwucdn.top',
|
|
142
|
-
'.owocdn.top',
|
|
143
|
-
'.streamzone1.site',
|
|
144
|
-
'.nekostream.site',
|
|
145
|
-
'.nukitashith.top',
|
|
146
|
-
'.aniwatchtv.site',
|
|
147
|
-
'.settlar.io',
|
|
148
|
-
];
|
|
149
|
-
const HANIME_HOSTS = new Set(['r2.1hanime.com', '1.1hanime.com']);
|
|
150
|
-
const OPPAI_HOSTS = new Set(['myspacecat.pictures']);
|
|
151
|
-
const KAA_HOSTS = new Set([
|
|
152
|
-
'hls.krussdomi.com',
|
|
153
|
-
'subst.krussdomi.com',
|
|
154
|
-
'krussdomi.com',
|
|
155
|
-
'st1.advancedairesearchlab.xyz',
|
|
156
|
-
'st1.habibikun.xyz',
|
|
157
|
-
'st1.babybayw.xyz',
|
|
158
|
-
'st1.narutokun.xyz',
|
|
159
|
-
]);
|
|
160
|
-
const KAA_REFERER = 'https://krussdomi.com/';
|
|
161
|
-
const KAA_ORIGIN = 'https://krussdomi.com';
|
|
162
|
-
const ANILIGHT_REFERER = 'https://anilight.live/';
|
|
163
|
-
const ANILIGHT_ORIGIN = 'https://anilight.live';
|
|
164
|
-
const ANILIGHT_HOSTS = [
|
|
165
|
-
'anilight.live',
|
|
166
|
-
'api.anilight.live',
|
|
167
|
-
'lostproject.club',
|
|
168
|
-
'streamzone1.site',
|
|
169
|
-
'nukitashi.top',
|
|
170
|
-
'vivibebe.site',
|
|
171
|
-
'vibeplayer.site',
|
|
172
|
-
'aniwatchtv.site',
|
|
173
|
-
];
|
|
174
|
-
const NEXABLOOM_FAMILY_SUFFIXES = [
|
|
175
|
-
'.quavex.top',
|
|
176
|
-
'.nexabloom.top',
|
|
177
|
-
'.hiddenvertex.top',
|
|
178
|
-
'.ironhorizon.top',
|
|
179
|
-
'.solarhaven.top',
|
|
180
|
-
];
|
|
181
|
-
const NEXABLOOM_FALLBACK_HOSTS = ['fetch.nexabloom.top'];
|
|
182
|
-
function nexabloomFallbackUrls(urlStr) {
|
|
183
|
-
let host = '';
|
|
129
|
+
function matchProxyRule(urlStr, rules) {
|
|
130
|
+
let host;
|
|
184
131
|
try {
|
|
185
132
|
host = new URL(urlStr).hostname.toLowerCase();
|
|
186
133
|
}
|
|
187
134
|
catch {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
return rules.find((r) => r.hosts?.some((h) => h === host) || r.suffixes?.some((s) => host.endsWith(s)));
|
|
138
|
+
}
|
|
139
|
+
function resolveOrigin(rule, referer, queryOrigin) {
|
|
140
|
+
return queryOrigin || (rule?.originFromReferer ? referer : '') || rule?.origin || '';
|
|
141
|
+
}
|
|
142
|
+
function mirrorUrls(urlStr, mirrors) {
|
|
143
|
+
if (!mirrors?.length)
|
|
188
144
|
return [urlStr];
|
|
145
|
+
let current;
|
|
146
|
+
try {
|
|
147
|
+
current = new URL(urlStr);
|
|
189
148
|
}
|
|
190
|
-
|
|
149
|
+
catch {
|
|
191
150
|
return [urlStr];
|
|
192
151
|
}
|
|
152
|
+
const seen = new Set([current.hostname.toLowerCase()]);
|
|
193
153
|
const urls = [urlStr];
|
|
194
|
-
for (const
|
|
195
|
-
|
|
154
|
+
for (const mirror of mirrors) {
|
|
155
|
+
const host = mirror.toLowerCase();
|
|
156
|
+
if (seen.has(host))
|
|
196
157
|
continue;
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
202
|
-
catch {
|
|
203
|
-
// ignore
|
|
204
|
-
}
|
|
158
|
+
seen.add(host);
|
|
159
|
+
const candidate = new URL(current.href);
|
|
160
|
+
candidate.hostname = host;
|
|
161
|
+
urls.push(candidate.href);
|
|
205
162
|
}
|
|
206
163
|
return urls;
|
|
207
164
|
}
|
|
208
|
-
async function
|
|
165
|
+
async function fetchWithMirrors(urlStr, mirrors, signal, attempt, isOk) {
|
|
209
166
|
let last = null;
|
|
210
|
-
for (const candidate of
|
|
167
|
+
for (const candidate of mirrorUrls(urlStr, mirrors)) {
|
|
211
168
|
if (signal.aborted)
|
|
212
169
|
break;
|
|
213
170
|
const r = await attempt(candidate).catch(() => null);
|
|
@@ -219,34 +176,6 @@ async function fetchNexabloomFallback(urlStr, signal, attempt, isOk) {
|
|
|
219
176
|
}
|
|
220
177
|
return last;
|
|
221
178
|
}
|
|
222
|
-
function isGotScrapingHost(urlStr) {
|
|
223
|
-
try {
|
|
224
|
-
const host = new URL(urlStr).hostname.toLowerCase();
|
|
225
|
-
if (GOT_SCRAPING_HOSTS.has(host))
|
|
226
|
-
return true;
|
|
227
|
-
if (HANIME_HOSTS.has(host))
|
|
228
|
-
return true;
|
|
229
|
-
return GOT_SCRAPING_SUFFIXES.some((s) => host.endsWith(s));
|
|
230
|
-
}
|
|
231
|
-
catch {
|
|
232
|
-
return false;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
function validateKwikUrl(value) {
|
|
236
|
-
if (typeof value !== 'string')
|
|
237
|
-
return null;
|
|
238
|
-
try {
|
|
239
|
-
const url = new URL(value);
|
|
240
|
-
const isSecure = url.protocol === 'https:';
|
|
241
|
-
const isKwik = KWIK_DOMAINS.has(url.hostname.toLowerCase());
|
|
242
|
-
const isEmbedPath = /^\/e\/[A-Za-z0-9_-]+$/.test(url.pathname);
|
|
243
|
-
const noAuthOrQuery = !url.username && !url.password && !url.search && !url.hash;
|
|
244
|
-
return isSecure && isKwik && isEmbedPath && noAuthOrQuery ? url : null;
|
|
245
|
-
}
|
|
246
|
-
catch {
|
|
247
|
-
return null;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
179
|
function pinVariant(body, idx) {
|
|
251
180
|
const lines = body.split('\n');
|
|
252
181
|
let streamIdx = -1;
|
|
@@ -271,16 +200,16 @@ function pinVariant(body, idx) {
|
|
|
271
200
|
return null;
|
|
272
201
|
return out.join('\n');
|
|
273
202
|
}
|
|
274
|
-
function
|
|
275
|
-
const safeReferer = JSON.stringify(
|
|
203
|
+
function applyEmbedPatches(html, embedUrl, cookieStr = '') {
|
|
204
|
+
const safeReferer = JSON.stringify(embedUrl.href).replace(/</g, '\\u003c');
|
|
276
205
|
const safeCookie = JSON.stringify(cookieStr).replace(/</g, '\\u003c');
|
|
277
206
|
const patched = html.replace(/\b(src|href|url)\s*[=:]\s*(["']?)(\/\/[^"'>)]+|\/(?!\/)[^"'>)]*)\2/gi, (match, attr, quote, path) => {
|
|
278
|
-
const full = path.startsWith('//') ? `https:${path}` : `${
|
|
207
|
+
const full = path.startsWith('//') ? `https:${path}` : `${embedUrl.origin}${path}`;
|
|
279
208
|
const prefix = match.startsWith('url') ? `url(` : `${attr}=`;
|
|
280
209
|
const suffix = match.startsWith('url') ? `)` : '';
|
|
281
210
|
return `${prefix}${quote}${full}${quote}${suffix}`;
|
|
282
211
|
});
|
|
283
|
-
const iconProxy = `/api/proxy?url=${encodeURIComponent(`${
|
|
212
|
+
const iconProxy = `/api/proxy?url=${encodeURIComponent(`${embedUrl.origin}/app/js/vendor/plyr.svg`)}&referer=${encodeURIComponent(embedUrl.href)}`;
|
|
284
213
|
const plyrPatch = `<script>if(window.Plyr) Plyr.defaults.iconUrl=${JSON.stringify(iconProxy).replace(/</g, '\\u003c')};</script>`;
|
|
285
214
|
const hlsPatch = `<script>
|
|
286
215
|
(function() {
|
|
@@ -359,7 +288,7 @@ function placeholderSvg() {
|
|
|
359
288
|
return null;
|
|
360
289
|
}
|
|
361
290
|
const PROXY_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0';
|
|
362
|
-
export function registerProxy(app) {
|
|
291
|
+
export function registerProxy(app, getProxyRules) {
|
|
363
292
|
app.get('/api/proxy', async (c) => {
|
|
364
293
|
const url = c.req.query('url');
|
|
365
294
|
if (!url)
|
|
@@ -369,60 +298,32 @@ export function registerProxy(app) {
|
|
|
369
298
|
return c.text(safeCheck.error || 'Invalid URL', 400);
|
|
370
299
|
}
|
|
371
300
|
const urlStr = url;
|
|
372
|
-
const
|
|
301
|
+
const rule = matchProxyRule(urlStr, getProxyRules());
|
|
302
|
+
const refererStr = c.req.query('referer') || rule?.referer || '';
|
|
303
|
+
const originStr = resolveOrigin(rule, refererStr, c.req.query('origin'));
|
|
304
|
+
const uaStr = c.req.query('ua') || '';
|
|
373
305
|
const cookieStr = c.req.query('cookie') || '';
|
|
306
|
+
const useScrape = c.req.query('scrape') === '1' || rule?.scrape === true;
|
|
307
|
+
const useCf = c.req.query('cf') === '1' || rule?.cfCookie === true;
|
|
374
308
|
const variantRaw = c.req.query('variant');
|
|
375
309
|
const variantIdx = variantRaw !== undefined ? parseInt(variantRaw, 10) : NaN;
|
|
376
310
|
const pinVar = Number.isInteger(variantIdx) && variantIdx >= 0;
|
|
377
|
-
const cacheKey = `m3u8-${urlStr}-${refererStr}${pinVar ? `-v${variantIdx}` : ''}`;
|
|
311
|
+
const cacheKey = `m3u8-${urlStr}-${refererStr}-${originStr}-${uaStr}${pinVar ? `-v${variantIdx}` : ''}`;
|
|
378
312
|
const abort = new AbortController();
|
|
379
313
|
linkAbort(c.req.raw.signal, abort);
|
|
380
314
|
try {
|
|
381
|
-
const isVaultCdn = Array.from(VAULT_CDN_HOSTS).some((host) => urlStr.includes(host));
|
|
382
|
-
const isHanime = Array.from(HANIME_HOSTS).some((host) => urlStr.includes(host));
|
|
383
|
-
const isOppai = Array.from(OPPAI_HOSTS).some((host) => urlStr.includes(host));
|
|
384
|
-
const isKaa = refererStr.startsWith(KAA_ORIGIN) ||
|
|
385
|
-
Array.from(KAA_HOSTS).some((host) => urlStr.includes(host));
|
|
386
315
|
const headers = {
|
|
387
|
-
'User-Agent': PROXY_UA,
|
|
316
|
+
'User-Agent': uaStr || PROXY_UA,
|
|
388
317
|
};
|
|
389
318
|
if (refererStr)
|
|
390
319
|
headers['Referer'] = refererStr;
|
|
391
|
-
if (
|
|
392
|
-
headers['Origin'] =
|
|
393
|
-
|
|
320
|
+
if (originStr)
|
|
321
|
+
headers['Origin'] = originStr;
|
|
322
|
+
if (cookieStr) {
|
|
323
|
+
const cookieHeader = useCf ? buildCfClearanceCookie(cookieStr) : cookieStr;
|
|
394
324
|
if (cookieHeader)
|
|
395
325
|
headers['Cookie'] = cookieHeader;
|
|
396
326
|
}
|
|
397
|
-
if (isHanime) {
|
|
398
|
-
if (!headers['Referer'])
|
|
399
|
-
headers['Referer'] = refererStr || 'https://nhplayer.com/';
|
|
400
|
-
headers['Origin'] = 'https://nhplayer.com';
|
|
401
|
-
}
|
|
402
|
-
if (isOppai) {
|
|
403
|
-
if (!headers['Referer'])
|
|
404
|
-
headers['Referer'] = refererStr || 'https://oppai.stream/';
|
|
405
|
-
headers['Origin'] = 'https://oppai.stream';
|
|
406
|
-
}
|
|
407
|
-
if (isKaa) {
|
|
408
|
-
if (!headers['Referer'])
|
|
409
|
-
headers['Referer'] = refererStr || KAA_REFERER;
|
|
410
|
-
headers['Origin'] = KAA_ORIGIN;
|
|
411
|
-
}
|
|
412
|
-
const isMegaplay = refererStr.startsWith(MEGAPLAY_ORIGIN) || urlStr.includes('nexabloom.top');
|
|
413
|
-
if (isMegaplay && !headers['Origin']) {
|
|
414
|
-
headers['Origin'] = MEGAPLAY_ORIGIN;
|
|
415
|
-
}
|
|
416
|
-
if (urlStr.includes('weeabo0.xyz') || urlStr.includes('weeab0o.xyz')) {
|
|
417
|
-
if (!headers['Referer'])
|
|
418
|
-
headers['Referer'] = refererStr || 'https://japaneseasmr.com/';
|
|
419
|
-
const jasmrCookieHeader = buildCfClearanceCookie(cookieStr);
|
|
420
|
-
if (jasmrCookieHeader)
|
|
421
|
-
headers['Cookie'] = jasmrCookieHeader;
|
|
422
|
-
const jasmrUa = c.req.query('ua') || '';
|
|
423
|
-
if (jasmrUa)
|
|
424
|
-
headers['User-Agent'] = jasmrUa;
|
|
425
|
-
}
|
|
426
327
|
const range = c.req.header('range');
|
|
427
328
|
if (range)
|
|
428
329
|
headers['Range'] = range;
|
|
@@ -434,10 +335,23 @@ export function registerProxy(app) {
|
|
|
434
335
|
};
|
|
435
336
|
const rewritePlaylist = (playlistBody, finalUrl) => {
|
|
436
337
|
const baseUrl = new URL(finalUrl);
|
|
437
|
-
const proxiedMediaUrl = (targetUrl) =>
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
338
|
+
const proxiedMediaUrl = (targetUrl) => {
|
|
339
|
+
const params = new URLSearchParams({ url: targetUrl });
|
|
340
|
+
if (refererStr)
|
|
341
|
+
params.set('referer', refererStr);
|
|
342
|
+
if (originStr)
|
|
343
|
+
params.set('origin', originStr);
|
|
344
|
+
if (uaStr)
|
|
345
|
+
params.set('ua', uaStr);
|
|
346
|
+
if (cookieStr)
|
|
347
|
+
params.set('cookie', sanitizeCfClearance(cookieStr));
|
|
348
|
+
if (useScrape)
|
|
349
|
+
params.set('scrape', '1');
|
|
350
|
+
if (useCf)
|
|
351
|
+
params.set('cf', '1');
|
|
352
|
+
return `/api/proxy?${params}`;
|
|
353
|
+
};
|
|
354
|
+
const needsProxy = Boolean(refererStr || originStr || uaStr || useScrape);
|
|
441
355
|
const isProxied = (value) => value.includes('/api/proxy');
|
|
442
356
|
const pinned = pinVar ? pinVariant(playlistBody, variantIdx) : null;
|
|
443
357
|
return (pinned || playlistBody)
|
|
@@ -469,8 +383,8 @@ export function registerProxy(app) {
|
|
|
469
383
|
headers.set('Access-Control-Allow-Origin', '*');
|
|
470
384
|
return new Response(cached, { headers });
|
|
471
385
|
}
|
|
472
|
-
const resp = await
|
|
473
|
-
url:
|
|
386
|
+
const resp = await fetchWithMirrors(urlStr, rule?.mirrors, abort.signal, (candidate) => gotScraping({
|
|
387
|
+
url: candidate,
|
|
474
388
|
method: 'GET',
|
|
475
389
|
headers,
|
|
476
390
|
responseType: 'text',
|
|
@@ -486,10 +400,10 @@ export function registerProxy(app) {
|
|
|
486
400
|
return playlistResponse(rewritten);
|
|
487
401
|
}
|
|
488
402
|
else {
|
|
489
|
-
if (
|
|
403
|
+
if (useScrape) {
|
|
490
404
|
if (range)
|
|
491
405
|
headers['Range'] = range;
|
|
492
|
-
const resp = await
|
|
406
|
+
const resp = await fetchWithMirrors(urlStr, rule?.mirrors, abort.signal, (candidate) => gotScraping({
|
|
493
407
|
url: candidate,
|
|
494
408
|
method: 'GET',
|
|
495
409
|
headers,
|
|
@@ -521,7 +435,7 @@ export function registerProxy(app) {
|
|
|
521
435
|
});
|
|
522
436
|
}
|
|
523
437
|
else {
|
|
524
|
-
const upstream = await
|
|
438
|
+
const upstream = await fetchWithMirrors(urlStr, rule?.mirrors, abort.signal, async (candidate) => {
|
|
525
439
|
const attempt = await fetchWithRetry(candidate, { method: 'GET', headers }, { retries: 3, timeoutMs: 30000, signal: abort.signal });
|
|
526
440
|
if (attempt.status !== 200 && attempt.status !== 206) {
|
|
527
441
|
try {
|
|
@@ -566,25 +480,36 @@ export function registerProxy(app) {
|
|
|
566
480
|
}
|
|
567
481
|
});
|
|
568
482
|
app.get('/api/embed-proxy', async (c) => {
|
|
569
|
-
const
|
|
570
|
-
if (!
|
|
483
|
+
const rawUrl = c.req.query('url');
|
|
484
|
+
if (!rawUrl)
|
|
485
|
+
return c.text('URL required', 400);
|
|
486
|
+
const safeCheck = isSafeExternalUrl(rawUrl);
|
|
487
|
+
if (!safeCheck.safe)
|
|
488
|
+
return c.text(safeCheck.error || 'Invalid URL', 400);
|
|
489
|
+
const rule = matchProxyRule(rawUrl, getProxyRules());
|
|
490
|
+
if (!rule?.embedPatch)
|
|
571
491
|
return c.text('Invalid or unsupported gateway URL', 400);
|
|
492
|
+
const embedUrl = new URL(rawUrl);
|
|
572
493
|
const cookieStr = c.req.query('cookie') || '';
|
|
494
|
+
const refererStr = c.req.query('referer') || rule.referer || '';
|
|
495
|
+
const originStr = resolveOrigin(rule, refererStr, c.req.query('origin'));
|
|
573
496
|
const abort = new AbortController();
|
|
574
497
|
linkAbort(c.req.raw.signal, abort);
|
|
575
498
|
try {
|
|
576
499
|
const headers = {
|
|
577
|
-
'User-Agent': PROXY_UA,
|
|
578
|
-
Referer: ANIMEPAHE_URL,
|
|
579
|
-
Origin: 'https://animepahe.pw',
|
|
500
|
+
'User-Agent': c.req.query('ua') || PROXY_UA,
|
|
580
501
|
};
|
|
502
|
+
if (refererStr)
|
|
503
|
+
headers['Referer'] = refererStr;
|
|
504
|
+
if (originStr)
|
|
505
|
+
headers['Origin'] = originStr;
|
|
581
506
|
if (cookieStr) {
|
|
582
|
-
const cookieHeader = buildCfClearanceCookie(cookieStr);
|
|
507
|
+
const cookieHeader = c.req.query('cf') === '1' || rule.cfCookie ? buildCfClearanceCookie(cookieStr) : cookieStr;
|
|
583
508
|
if (cookieHeader)
|
|
584
509
|
headers['Cookie'] = cookieHeader;
|
|
585
510
|
}
|
|
586
511
|
const response = await gotScraping({
|
|
587
|
-
url:
|
|
512
|
+
url: embedUrl.href,
|
|
588
513
|
method: 'GET',
|
|
589
514
|
headers,
|
|
590
515
|
responseType: 'text',
|
|
@@ -593,7 +518,7 @@ export function registerProxy(app) {
|
|
|
593
518
|
throwHttpErrors: false,
|
|
594
519
|
});
|
|
595
520
|
const originalHtml = response.body;
|
|
596
|
-
const patched =
|
|
521
|
+
const patched = applyEmbedPatches(originalHtml, embedUrl, cookieStr);
|
|
597
522
|
if (!patched)
|
|
598
523
|
return c.text('Failed to patch video gateway', 502);
|
|
599
524
|
const outHeaders = new Headers();
|
|
@@ -618,51 +543,24 @@ export function registerProxy(app) {
|
|
|
618
543
|
const abort = new AbortController();
|
|
619
544
|
linkAbort(c.req.raw.signal, abort);
|
|
620
545
|
const subUrl = url;
|
|
621
|
-
const
|
|
622
|
-
const
|
|
623
|
-
const
|
|
624
|
-
|
|
625
|
-
const parsed = new URL(subUrl);
|
|
626
|
-
if (parsed.hostname.toLowerCase().endsWith('anilight.live') &&
|
|
627
|
-
parsed.pathname.includes('/proxy/captions')) {
|
|
628
|
-
const inner = parsed.searchParams.get('url');
|
|
629
|
-
if (inner && (inner.startsWith('http://') || inner.startsWith('https://'))) {
|
|
630
|
-
return inner;
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
catch {
|
|
635
|
-
// ignore
|
|
636
|
-
}
|
|
637
|
-
return null;
|
|
638
|
-
})();
|
|
546
|
+
const rule = matchProxyRule(subUrl, getProxyRules());
|
|
547
|
+
const refererStr = c.req.query('referer') || rule?.referer || '';
|
|
548
|
+
const originStr = resolveOrigin(rule, refererStr, c.req.query('origin'));
|
|
549
|
+
const useScrape = c.req.query('scrape') === '1' || rule?.scrape === true;
|
|
639
550
|
const buildHeaders = () => {
|
|
640
551
|
const headers = {
|
|
641
552
|
'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',
|
|
642
553
|
Accept: 'text/vtt,text/plain,*/*;q=0.8',
|
|
643
554
|
};
|
|
644
|
-
if (refererStr)
|
|
555
|
+
if (refererStr)
|
|
645
556
|
headers['Referer'] = refererStr;
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
headers['Referer'] = ANILIGHT_REFERER;
|
|
649
|
-
}
|
|
650
|
-
if (refererStr.startsWith(KAA_ORIGIN) ||
|
|
651
|
-
Array.from(KAA_HOSTS).some((host) => subUrl.includes(host))) {
|
|
652
|
-
headers['Origin'] = KAA_ORIGIN;
|
|
653
|
-
}
|
|
654
|
-
else if (isAnilight) {
|
|
655
|
-
headers['Origin'] = ANILIGHT_ORIGIN;
|
|
656
|
-
}
|
|
557
|
+
if (originStr)
|
|
558
|
+
headers['Origin'] = originStr;
|
|
657
559
|
return headers;
|
|
658
560
|
};
|
|
659
561
|
const fetchText = async (target, retries = 1) => {
|
|
660
562
|
const headers = buildHeaders();
|
|
661
|
-
if (
|
|
662
|
-
headers['Referer'] = ANILIGHT_REFERER;
|
|
663
|
-
headers['Origin'] = ANILIGHT_ORIGIN;
|
|
664
|
-
}
|
|
665
|
-
if (isGotScrapingHost(target)) {
|
|
563
|
+
if (useScrape) {
|
|
666
564
|
try {
|
|
667
565
|
const upstream = await gotScraping({
|
|
668
566
|
url: target,
|
|
@@ -732,19 +630,14 @@ export function registerProxy(app) {
|
|
|
732
630
|
};
|
|
733
631
|
try {
|
|
734
632
|
const headers = buildHeaders();
|
|
735
|
-
|
|
633
|
+
const result = await fetchText(subUrl, 1);
|
|
736
634
|
if (abort.signal.aborted)
|
|
737
635
|
return new Response(null, { status: 499 });
|
|
738
|
-
if ((!result || !result.body) && innerRawUrl) {
|
|
739
|
-
result = await fetchText(innerRawUrl, 0);
|
|
740
|
-
if (abort.signal.aborted)
|
|
741
|
-
return new Response(null, { status: 499 });
|
|
742
|
-
}
|
|
743
636
|
if (!result || !result.body) {
|
|
744
637
|
return subtitleError('Subtitle upstream error');
|
|
745
638
|
}
|
|
746
639
|
const body = result.body;
|
|
747
|
-
const baseForSegments =
|
|
640
|
+
const baseForSegments = subUrl;
|
|
748
641
|
if (/#EXTM3U/i.test(body.slice(0, 1000))) {
|
|
749
642
|
const segUrls = body
|
|
750
643
|
.split('\n')
|
|
@@ -825,14 +718,16 @@ export function registerProxy(app) {
|
|
|
825
718
|
const cookie = c.req.query('cookie') || '';
|
|
826
719
|
const ua = c.req.query('ua') || '';
|
|
827
720
|
const strict = c.req.query('strict') === '1';
|
|
721
|
+
const rule = matchProxyRule(targetUrl, getProxyRules());
|
|
722
|
+
const refererValue = c.req.query('referer') || rule?.referer || '';
|
|
723
|
+
const originValue = resolveOrigin(rule, refererValue, c.req.query('origin'));
|
|
828
724
|
const abort = new AbortController();
|
|
829
725
|
linkAbort(c.req.raw.signal, abort);
|
|
830
|
-
let refererValue = 'https://anilist.co/';
|
|
831
726
|
const headers = {
|
|
832
727
|
'User-Agent': ua ||
|
|
833
728
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
834
729
|
};
|
|
835
|
-
const cacheKey = `${targetUrl}|${cookie}|${ua}`;
|
|
730
|
+
const cacheKey = `${targetUrl}|${cookie}|${ua}|${refererValue}|${originValue}`;
|
|
836
731
|
const cached = imageCache.get(cacheKey);
|
|
837
732
|
if (cached) {
|
|
838
733
|
const hit = new Headers();
|
|
@@ -843,32 +738,15 @@ export function registerProxy(app) {
|
|
|
843
738
|
return new Response(cached.body, { headers: hit });
|
|
844
739
|
}
|
|
845
740
|
try {
|
|
846
|
-
if (
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
refererValue = 'https://gogoanime.lu/';
|
|
855
|
-
}
|
|
856
|
-
else if (targetUrl.includes('animeya.cc')) {
|
|
857
|
-
refererValue = 'https://animeya.cc/';
|
|
858
|
-
}
|
|
859
|
-
else if (targetUrl.includes('myspacecat.pictures')) {
|
|
860
|
-
refererValue = 'https://oppai.stream/';
|
|
861
|
-
}
|
|
862
|
-
else if (targetUrl.includes('weeabo0.xyz')) {
|
|
863
|
-
refererValue = 'https://japaneseasmr.com/';
|
|
864
|
-
const jasmrCookieHeader = buildCfClearanceCookie(cookie);
|
|
865
|
-
if (jasmrCookieHeader)
|
|
866
|
-
headers['Cookie'] = jasmrCookieHeader;
|
|
867
|
-
const uaParam = ua || '';
|
|
868
|
-
if (uaParam)
|
|
869
|
-
headers['User-Agent'] = uaParam;
|
|
741
|
+
if (refererValue)
|
|
742
|
+
headers['Referer'] = refererValue;
|
|
743
|
+
if (originValue)
|
|
744
|
+
headers['Origin'] = originValue;
|
|
745
|
+
if (c.req.query('cf') === '1' || rule?.cfCookie) {
|
|
746
|
+
const cookieHeader = buildCfClearanceCookie(cookie);
|
|
747
|
+
if (cookieHeader)
|
|
748
|
+
headers['Cookie'] = cookieHeader;
|
|
870
749
|
}
|
|
871
|
-
headers['Referer'] = refererValue;
|
|
872
750
|
let body = null;
|
|
873
751
|
let contentType = 'image/webp';
|
|
874
752
|
for (let attempt = 0; attempt < 3 && !body; attempt++) {
|
package/server/dist/hono/tv.js
CHANGED
|
@@ -24,6 +24,28 @@ function rewriteTvPlaylist(body, baseUrl, refererStr, providerStr) {
|
|
|
24
24
|
})
|
|
25
25
|
.join('\n');
|
|
26
26
|
}
|
|
27
|
+
function pinTvVariant(body, idx) {
|
|
28
|
+
const lines = body.split('\n');
|
|
29
|
+
let streamIdx = -1;
|
|
30
|
+
let skipUri = false;
|
|
31
|
+
const out = [];
|
|
32
|
+
for (const line of lines) {
|
|
33
|
+
const trimmed = line.trim();
|
|
34
|
+
if (trimmed.startsWith('#EXT-X-STREAM-INF')) {
|
|
35
|
+
streamIdx++;
|
|
36
|
+
skipUri = streamIdx !== idx;
|
|
37
|
+
if (!skipUri)
|
|
38
|
+
out.push(line);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (skipUri && trimmed && !trimmed.startsWith('#')) {
|
|
42
|
+
skipUri = false;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
out.push(line);
|
|
46
|
+
}
|
|
47
|
+
return streamIdx >= 0 && idx <= streamIdx ? out.join('\n') : body;
|
|
48
|
+
}
|
|
27
49
|
function isPlaylistBody(body) {
|
|
28
50
|
return (body.length >= 7 &&
|
|
29
51
|
body
|
|
@@ -67,13 +89,22 @@ export function registerTv(app, getTvProvider) {
|
|
|
67
89
|
}
|
|
68
90
|
});
|
|
69
91
|
app.get('/api/tv/stream-proxy', async (c) => {
|
|
70
|
-
const
|
|
92
|
+
const requestedUrl = c.req.query('url') ?? '';
|
|
93
|
+
const variantMatch = requestedUrl.match(/#v=(\d+)$/);
|
|
94
|
+
const variantIdx = variantMatch ? parseInt(variantMatch[1], 10) : NaN;
|
|
95
|
+
const urlStr = requestedUrl.replace(/#.*$/, '');
|
|
71
96
|
const refererStr = c.req.query('referer') || '';
|
|
72
97
|
const providerStr = (c.req.query('provider') || '').toLowerCase();
|
|
73
98
|
if (!urlStr)
|
|
74
99
|
return c.text('URL required', 400);
|
|
75
100
|
const providerMod = providerStr ? getTvProvider(providerStr) : undefined;
|
|
76
|
-
const rewrite = (body, baseUrl) =>
|
|
101
|
+
const rewrite = (body, baseUrl) => {
|
|
102
|
+
const providerBody = providerMod?.rewritePlaylist?.(body, baseUrl.href) ?? body;
|
|
103
|
+
const pinned = Number.isInteger(variantIdx)
|
|
104
|
+
? pinTvVariant(providerBody, variantIdx)
|
|
105
|
+
: providerBody;
|
|
106
|
+
return rewriteTvPlaylist(pinned, baseUrl, refererStr, providerStr);
|
|
107
|
+
};
|
|
77
108
|
const safeCheck = isSafeExternalUrl(urlStr);
|
|
78
109
|
if (!safeCheck.safe) {
|
|
79
110
|
return c.text(safeCheck.error || 'Invalid URL', 400);
|
|
@@ -9,7 +9,6 @@ import logger from '../logger.js';
|
|
|
9
9
|
import { CONFIG } from '../config.js';
|
|
10
10
|
import { requestContext } from '../utils/request-context.js';
|
|
11
11
|
import { sanitizeCfClearance, buildCfClearanceCookie } from '../utils/cookie.utils.js';
|
|
12
|
-
import { base64UrlEncode } from '../utils/megaplay.utils.js';
|
|
13
12
|
import { decodeMaybeGzip, parseJsonBody } from '../utils/http.utils.js';
|
|
14
13
|
import { buildQueryVariants, pickBestMatch } from './title-matching.js';
|
|
15
14
|
import { anilistRequest, parseMalId, searchAnilistByTitle } from '../lib/anilist.js';
|
|
@@ -226,7 +225,7 @@ function createCtx(cache) {
|
|
|
226
225
|
},
|
|
227
226
|
crypto: {
|
|
228
227
|
aes256CbcDecryptJson,
|
|
229
|
-
hmacSha256Base64Url: (key, message) =>
|
|
228
|
+
hmacSha256Base64Url: (key, message) => crypto.createHmac('sha256', key).update(message).digest('base64url'),
|
|
230
229
|
},
|
|
231
230
|
scraping: {
|
|
232
231
|
fetch: async (urlOrOptions, maybeOptions) => {
|
|
@@ -441,7 +440,13 @@ async function loadCachedFallback(item, prev, cacheDir, ctx) {
|
|
|
441
440
|
}
|
|
442
441
|
export async function loadRemoteProviders(opts) {
|
|
443
442
|
const registryUrl = (opts.registryUrl ?? CONFIG.PROVIDER_REPO_URL ?? '').trim();
|
|
444
|
-
const out = {
|
|
443
|
+
const out = {
|
|
444
|
+
providers: {},
|
|
445
|
+
mangaProviders: {},
|
|
446
|
+
tvProviders: {},
|
|
447
|
+
catalog: [],
|
|
448
|
+
proxyRules: [],
|
|
449
|
+
};
|
|
445
450
|
if (!registryUrl)
|
|
446
451
|
return out;
|
|
447
452
|
const cacheDir = path.join(CONFIG.ROOT, 'remote-providers');
|
|
@@ -510,6 +515,8 @@ export async function loadRemoteProviders(opts) {
|
|
|
510
515
|
continue;
|
|
511
516
|
}
|
|
512
517
|
out.catalog.push(withFacets(facets, withServers(item, provider, toCatalog(item, true))));
|
|
518
|
+
if (item.proxy)
|
|
519
|
+
out.proxyRules.push(item.proxy);
|
|
513
520
|
logger.info({ provider: item.id, version: item.version }, 'remote provider loaded');
|
|
514
521
|
}
|
|
515
522
|
else {
|
|
@@ -537,6 +544,8 @@ export async function loadRemoteProviders(opts) {
|
|
|
537
544
|
...withFacets(fb.facets, withServers(item, fb.provider, toCatalog({ ...item, version: prev.version }, true))),
|
|
538
545
|
error: `serving cached ${prev.version}: ${error}`,
|
|
539
546
|
});
|
|
547
|
+
if (item.proxy)
|
|
548
|
+
out.proxyRules.push(item.proxy);
|
|
540
549
|
logger.warn({ provider: item.id, error, fallback: prev.version }, 'remote provider update failed, serving cached version');
|
|
541
550
|
continue;
|
|
542
551
|
}
|
|
@@ -94,10 +94,45 @@ function parseEntry(raw, index) {
|
|
|
94
94
|
tier: p.tier,
|
|
95
95
|
modes,
|
|
96
96
|
browse: parseBrowseCaps(p.browse, index),
|
|
97
|
+
proxy: parseProxyRule(p.proxy, index),
|
|
97
98
|
auth: p.auth === 'cookie' ? 'cookie' : undefined,
|
|
98
99
|
enabledByDefault: p.enabledByDefault === false ? false : true,
|
|
99
100
|
};
|
|
100
101
|
}
|
|
102
|
+
function parseProxyRule(raw, index) {
|
|
103
|
+
if (raw === undefined)
|
|
104
|
+
return undefined;
|
|
105
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
106
|
+
throw new Error(`registry.providers[${index}].proxy must be an object`);
|
|
107
|
+
}
|
|
108
|
+
const p = raw;
|
|
109
|
+
const rule = {};
|
|
110
|
+
for (const key of ['hosts', 'suffixes', 'mirrors']) {
|
|
111
|
+
if (p[key] === undefined)
|
|
112
|
+
continue;
|
|
113
|
+
if (!Array.isArray(p[key]) || !p[key].every((v) => typeof v === 'string' && v)) {
|
|
114
|
+
throw new Error(`registry.providers[${index}].proxy.${key} must be a non-empty string array`);
|
|
115
|
+
}
|
|
116
|
+
rule[key] = p[key].map((v) => v.trim().toLowerCase());
|
|
117
|
+
}
|
|
118
|
+
for (const key of ['referer', 'origin']) {
|
|
119
|
+
if (p[key] === undefined)
|
|
120
|
+
continue;
|
|
121
|
+
if (typeof p[key] !== 'string' || !/^https:\/\//i.test(p[key].trim())) {
|
|
122
|
+
throw new Error(`registry.providers[${index}].proxy.${key} must be an https URL`);
|
|
123
|
+
}
|
|
124
|
+
rule[key] = p[key].trim();
|
|
125
|
+
}
|
|
126
|
+
for (const key of ['originFromReferer', 'scrape', 'cfCookie', 'embedPatch']) {
|
|
127
|
+
if (p[key] === undefined)
|
|
128
|
+
continue;
|
|
129
|
+
if (typeof p[key] !== 'boolean') {
|
|
130
|
+
throw new Error(`registry.providers[${index}].proxy.${key} must be boolean`);
|
|
131
|
+
}
|
|
132
|
+
rule[key] = p[key];
|
|
133
|
+
}
|
|
134
|
+
return rule;
|
|
135
|
+
}
|
|
101
136
|
function parseBrowseCaps(raw, index) {
|
|
102
137
|
if (raw === undefined)
|
|
103
138
|
return undefined;
|