@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.
Files changed (57) hide show
  1. package/README.md +1 -1
  2. package/client/dist/assets/{AnimeCard-JkXG40Es.js → AnimeCard-C3P9zWi9.js} +1 -1
  3. package/client/dist/assets/{AnimeInfo-2L0UJkbB.js → AnimeInfo-Et6687Tl.js} +1 -1
  4. package/client/dist/assets/{AnimeInfoPage-BDs03vKk.js → AnimeInfoPage-DX4eNnlH.js} +2 -2
  5. package/client/dist/assets/{Asmr-D2mVKEl9.js → Asmr-CCdEWrFi.js} +1 -1
  6. package/client/dist/assets/{AsmrResetProgressModal-MQA_bNQM.js → AsmrResetProgressModal-DgEWBJOw.js} +1 -1
  7. package/client/dist/assets/AvSyncCalibrator-CQ2fjf2E.js +10 -0
  8. package/client/dist/assets/{Home-DxMBHFmc.js → Home-DUkUTTTy.js} +1 -1
  9. package/client/dist/assets/{Insights-hmLWsSXY.js → Insights-QCh4xW12.js} +1 -1
  10. package/client/dist/assets/{LibraryListPage-CWI7hgUh.js → LibraryListPage-BoTxRqHz.js} +1 -1
  11. package/client/dist/assets/{ListeningList-NOc28KAV.js → ListeningList-DguKjI_t.js} +1 -1
  12. package/client/dist/assets/{Manga-B5qNhI7v.js → Manga-HydR-L7o.js} +1 -1
  13. package/client/dist/assets/{MangaInfoPage-D4wIyb-V.js → MangaInfoPage-CBakEI9a.js} +1 -1
  14. package/client/dist/assets/{MangaReadPage-Ce1VX7-0.js → MangaReadPage-DwAR3iPy.js} +1 -1
  15. package/client/dist/assets/{MangaResetProgressModal-P3oY4_14.js → MangaResetProgressModal-DGzl8vsO.js} +1 -1
  16. package/client/dist/assets/{Mature-D8EwGYQq.js → Mature-Pi0TxKuF.js} +1 -1
  17. package/client/dist/assets/{MediaCard-Dp8DjyXB.js → MediaCard--cuFxOCS.js} +1 -1
  18. package/client/dist/assets/{Music-DULtVsAM.js → Music-CQpyXTLH.js} +1 -1
  19. package/client/dist/assets/{OptionListMenu-fIHdE-nq.js → OptionListMenu-B3D39dGS.js} +1 -1
  20. package/client/dist/assets/{Player-B8XeOmso.js → Player-CsH4x_I_.js} +6 -6
  21. package/client/dist/assets/{PlayerSettings-BycNCiCs.js → PlayerSettings-DGxBsi3F.js} +1 -1
  22. package/client/dist/assets/{QueueOptionsButton-T1J37IZT.js → QueueOptionsButton-Djq4Cpex.js} +1 -1
  23. package/client/dist/assets/{QueueRail-BAwI-apz.js → QueueRail-DOrYgNVX.js} +1 -1
  24. package/client/dist/assets/{Radio-Bv9DyKtZ.js → Radio-DF6g6odd.js} +1 -1
  25. package/client/dist/assets/{ReadingList-Cl0CcVp3.js → ReadingList-DwYoBrNk.js} +1 -1
  26. package/client/dist/assets/{ResetProgressModal--N60WWCB.js → ResetProgressModal-CMF86mX6.js} +1 -1
  27. package/client/dist/assets/{Search-DvgVDlUR.js → Search-DFkhuk-x.js} +1 -1
  28. package/client/dist/assets/{Settings-dCz_-bJn.js → Settings-DBB2r0VQ.js} +1 -1
  29. package/client/dist/assets/{SkeletonGrid-BZHKk1I6.js → SkeletonGrid-DJelJgAv.js} +1 -1
  30. package/client/dist/assets/{Trackers-C1T5VcPu.js → Trackers-DGYXFhYZ.js} +1 -1
  31. package/client/dist/assets/{Tv-Dhv_cg4y.js → Tv-C5MDGD2Z.js} +4 -4
  32. package/client/dist/assets/{TvInfo-DgaLK2H6.js → TvInfo-DrKuK13u.js} +1 -1
  33. package/client/dist/assets/{TvPopup-DyX7f8kC.js → TvPopup-DtHilRDT.js} +1 -1
  34. package/client/dist/assets/{TvSearch-Bp0H6kue.js → TvSearch-CzNxdu_w.js} +1 -1
  35. package/client/dist/assets/{TvWatchlist-ohMtMeKa.js → TvWatchlist-D7p4br6S.js} +1 -1
  36. package/client/dist/assets/{Watchlist-BbQtzsER.js → Watchlist-DrOzswl2.js} +1 -1
  37. package/client/dist/assets/{hls-Bi1cjpLZ.js → hls-BGCX7mJy.js} +1 -1
  38. package/client/dist/assets/{index-BTnhzofk.js → index-CnfZwsds.js} +2 -2
  39. package/client/dist/assets/{useAnimeInfoData-Cvmso3IK.js → useAnimeInfoData-DYXWBPF-.js} +1 -1
  40. package/client/dist/assets/{useAsmr-CbzGCzmv.js → useAsmr-BbhQ59bk.js} +1 -1
  41. package/client/dist/assets/{useAsmrPopup-DaREZ9Ie.js → useAsmrPopup-SymV1J-b.js} +1 -1
  42. package/client/dist/assets/{useMangaLibrary-BVQRhsFh.js → useMangaLibrary-DuesWQaE.js} +1 -1
  43. package/client/dist/assets/{useMangaPopup-CzsCbndG.js → useMangaPopup-D57OVGUr.js} +1 -1
  44. package/client/dist/assets/{useProviders-UuUPzft-.js → useProviders-mpQb_dix.js} +1 -1
  45. package/client/dist/assets/{useTv-CSsbWKH9.js → useTv-DGflCsSI.js} +1 -1
  46. package/client/dist/assets/{useTvLibrary-a_Y0c2FS.js → useTvLibrary-BqeSmyc4.js} +1 -1
  47. package/client/dist/index.html +1 -1
  48. package/package.json +2 -2
  49. package/server/dist/app-hono.js +1 -1
  50. package/server/dist/hono/proxy.js +105 -227
  51. package/server/dist/hono/tv.js +33 -2
  52. package/server/dist/providers/remote-loader.js +12 -3
  53. package/server/dist/providers/remote-types.js +35 -0
  54. package/server/dist/server.js +3 -0
  55. package/server/package.json +1 -1
  56. package/client/dist/assets/AvSyncCalibrator-DmoH5eRA.js +0 -10
  57. 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
- const KWIK_DOMAINS = new Set(['kwik.cx', 'kwik.si', 'kwik.pro']);
131
- const ANIMEPAHE_URL = 'https://animepahe.pw/';
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
- if (!NEXABLOOM_FAMILY_SUFFIXES.some((s) => host.endsWith(s))) {
149
+ catch {
191
150
  return [urlStr];
192
151
  }
152
+ const seen = new Set([current.hostname.toLowerCase()]);
193
153
  const urls = [urlStr];
194
- for (const fallback of NEXABLOOM_FALLBACK_HOSTS) {
195
- if (fallback === host)
154
+ for (const mirror of mirrors) {
155
+ const host = mirror.toLowerCase();
156
+ if (seen.has(host))
196
157
  continue;
197
- try {
198
- const candidate = new URL(urlStr);
199
- candidate.hostname = fallback;
200
- urls.push(candidate.href);
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 fetchNexabloomFallback(urlStr, signal, attempt, isOk) {
165
+ async function fetchWithMirrors(urlStr, mirrors, signal, attempt, isOk) {
209
166
  let last = null;
210
- for (const candidate of nexabloomFallbackUrls(urlStr)) {
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 applyKwikPatches(html, kwikUrl, cookieStr = '') {
275
- const safeReferer = JSON.stringify(kwikUrl.href).replace(/</g, '\\u003c');
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}` : `${kwikUrl.origin}${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(`${kwikUrl.origin}/app/js/vendor/plyr.svg`)}&referer=${encodeURIComponent(kwikUrl.href)}`;
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 refererStr = c.req.query('referer') || '';
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 (isVaultCdn) {
392
- headers['Origin'] = refererStr || ANIMEPAHE_URL;
393
- const cookieHeader = buildCfClearanceCookie(cookieStr);
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) => `/api/proxy?url=${encodeURIComponent(targetUrl)}&referer=${encodeURIComponent(refererStr)}` +
438
- (cookieStr ? `&cookie=${encodeURIComponent(sanitizeCfClearance(cookieStr))}` : '') +
439
- (c.req.query('ua') ? `&ua=${encodeURIComponent(c.req.query('ua'))}` : '');
440
- const needsProxy = Boolean(refererStr);
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 fetchNexabloomFallback(urlStr, abort.signal, (candidate) => gotScraping({
473
- url: isNexabloomMasterUrl(candidate) ? signNexabloomMasterUrl(candidate) : candidate,
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 (isGotScrapingHost(urlStr)) {
403
+ if (useScrape) {
490
404
  if (range)
491
405
  headers['Range'] = range;
492
- const resp = await fetchNexabloomFallback(urlStr, abort.signal, (candidate) => gotScraping({
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 fetchNexabloomFallback(urlStr, abort.signal, async (candidate) => {
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 kwikUrl = validateKwikUrl(c.req.query('url'));
570
- if (!kwikUrl)
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: kwikUrl.href,
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 = applyKwikPatches(originalHtml, kwikUrl, cookieStr);
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 refererStr = c.req.query('referer') || '';
622
- const isAnilight = refererStr.includes('anilight.live') || ANILIGHT_HOSTS.some((host) => subUrl.includes(host));
623
- const innerRawUrl = (() => {
624
- try {
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
- else if (isAnilight) {
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 (innerRawUrl && target === innerRawUrl) {
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
- let result = await fetchText(subUrl, 1);
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 = innerRawUrl || subUrl;
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 (targetUrl.includes('animepahe')) {
847
- refererValue = 'https://animepahe.pw/';
848
- headers['Cookie'] = `cf_clearance=${sanitizeCfClearance(cookie)}`;
849
- }
850
- else if (targetUrl.includes('anilist.co')) {
851
- refererValue = 'https://anilist.co/';
852
- }
853
- else if (targetUrl.includes('gogocdn.net')) {
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++) {
@@ -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 urlStr = c.req.query('url') ?? '';
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) => rewriteTvPlaylist(providerMod?.rewritePlaylist?.(body, baseUrl.href) ?? body, baseUrl, refererStr, providerStr);
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) => base64UrlEncode(crypto.createHmac('sha256', key).update(message).digest()),
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 = { providers: {}, mangaProviders: {}, tvProviders: {}, catalog: [] };
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;