@serifpersia/dango 3.6.4 → 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 (60) hide show
  1. package/README.md +1 -1
  2. package/client/dist/assets/{AnimeCard-DVk4BNex.js → AnimeCard-C3P9zWi9.js} +1 -1
  3. package/client/dist/assets/{AnimeInfo-DevqUuNX.js → AnimeInfo-Et6687Tl.js} +1 -1
  4. package/client/dist/assets/{AnimeInfoPage-ZoJMem3T.js → AnimeInfoPage-DX4eNnlH.js} +2 -2
  5. package/client/dist/assets/{Asmr-B7Hc4hT4.js → Asmr-CCdEWrFi.js} +1 -1
  6. package/client/dist/assets/{AsmrResetProgressModal-AdrZB33a.js → AsmrResetProgressModal-DgEWBJOw.js} +1 -1
  7. package/client/dist/assets/AvSyncCalibrator-CQ2fjf2E.js +10 -0
  8. package/client/dist/assets/{Home-Dy6jlbe-.js → Home-DUkUTTTy.js} +1 -1
  9. package/client/dist/assets/{Insights-L72FYEhY.js → Insights-QCh4xW12.js} +1 -1
  10. package/client/dist/assets/{LibraryListPage-CJiChvn7.js → LibraryListPage-BoTxRqHz.js} +1 -1
  11. package/client/dist/assets/{ListeningList-C6DmhQYE.js → ListeningList-DguKjI_t.js} +1 -1
  12. package/client/dist/assets/{Manga-pBfz3N91.js → Manga-HydR-L7o.js} +1 -1
  13. package/client/dist/assets/{MangaInfoPage-DanNLum0.js → MangaInfoPage-CBakEI9a.js} +1 -1
  14. package/client/dist/assets/{MangaReadPage-CmSrkaL3.js → MangaReadPage-DwAR3iPy.js} +1 -1
  15. package/client/dist/assets/{MangaResetProgressModal-DYBD4XfF.js → MangaResetProgressModal-DGzl8vsO.js} +1 -1
  16. package/client/dist/assets/{Mature-APIFXLUd.js → Mature-Pi0TxKuF.js} +1 -1
  17. package/client/dist/assets/{MediaCard-lk6ZO2T2.js → MediaCard--cuFxOCS.js} +1 -1
  18. package/client/dist/assets/{Music-SWFqsret.js → Music-CQpyXTLH.js} +1 -1
  19. package/client/dist/assets/{OptionListMenu-DCG-BF9D.js → OptionListMenu-B3D39dGS.js} +1 -1
  20. package/client/dist/assets/Player-CsH4x_I_.js +56 -0
  21. package/client/dist/assets/{PlayerSettings-aHrCaLly.js → PlayerSettings-DGxBsi3F.js} +1 -1
  22. package/client/dist/assets/{QueueOptionsButton-9ptjLfgY.js → QueueOptionsButton-Djq4Cpex.js} +1 -1
  23. package/client/dist/assets/{QueueRail-DSA94IWc.js → QueueRail-DOrYgNVX.js} +1 -1
  24. package/client/dist/assets/{Radio-fo7eMtmX.js → Radio-DF6g6odd.js} +1 -1
  25. package/client/dist/assets/{ReadingList-DOZYEIaT.js → ReadingList-DwYoBrNk.js} +1 -1
  26. package/client/dist/assets/{ResetProgressModal-PmJXKPXV.js → ResetProgressModal-CMF86mX6.js} +1 -1
  27. package/client/dist/assets/{Search-q_45kuOY.js → Search-DFkhuk-x.js} +1 -1
  28. package/client/dist/assets/{Settings-DCG_27q2.js → Settings-DBB2r0VQ.js} +1 -1
  29. package/client/dist/assets/{SkeletonGrid-D6e8JTc2.js → SkeletonGrid-DJelJgAv.js} +1 -1
  30. package/client/dist/assets/{Trackers-Br8u_-n7.js → Trackers-DGYXFhYZ.js} +1 -1
  31. package/client/dist/assets/{Tv-9jGD-POg.js → Tv-C5MDGD2Z.js} +4 -4
  32. package/client/dist/assets/{TvInfo-DaoG5qd8.js → TvInfo-DrKuK13u.js} +1 -1
  33. package/client/dist/assets/{TvPopup-B11MGIN0.js → TvPopup-DtHilRDT.js} +1 -1
  34. package/client/dist/assets/{TvSearch-BJ_lOIHZ.js → TvSearch-CzNxdu_w.js} +1 -1
  35. package/client/dist/assets/{TvWatchlist-DGvG3C9M.js → TvWatchlist-D7p4br6S.js} +1 -1
  36. package/client/dist/assets/{Watchlist-BHxo147b.js → Watchlist-DrOzswl2.js} +1 -1
  37. package/client/dist/assets/{hls-CnVLfyq6.js → hls-BGCX7mJy.js} +1 -1
  38. package/client/dist/assets/{index-CQlPx8UA.js → index-CnfZwsds.js} +3 -3
  39. package/client/dist/assets/{useAnimeInfoData-BRP9E_M5.js → useAnimeInfoData-DYXWBPF-.js} +1 -1
  40. package/client/dist/assets/{useAsmr-BnqAqfIG.js → useAsmr-BbhQ59bk.js} +1 -1
  41. package/client/dist/assets/{useAsmrPopup-BJ79NC61.js → useAsmrPopup-SymV1J-b.js} +1 -1
  42. package/client/dist/assets/{useMangaLibrary-nC3VQT2C.js → useMangaLibrary-DuesWQaE.js} +1 -1
  43. package/client/dist/assets/{useMangaPopup-CTSFWlC4.js → useMangaPopup-D57OVGUr.js} +1 -1
  44. package/client/dist/assets/{useProviders-DQLAbAta.js → useProviders-mpQb_dix.js} +1 -1
  45. package/client/dist/assets/{useTv-BeQG1pC1.js → useTv-DGflCsSI.js} +1 -1
  46. package/client/dist/assets/{useTvLibrary-F0MkBioA.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/google.js +1 -1
  51. package/server/dist/hono/proxy.js +171 -254
  52. package/server/dist/hono/tv.js +43 -8
  53. package/server/dist/providers/remote-loader.js +15 -4
  54. package/server/dist/providers/remote-types.js +35 -0
  55. package/server/dist/server.js +3 -0
  56. package/server/dist/trpc/routers/data.js +15 -8
  57. package/server/package.json +1 -1
  58. package/client/dist/assets/AvSyncCalibrator-IXHL6uH0.js +0 -10
  59. package/client/dist/assets/Player-Bww3XdSp.js +0 -56
  60. 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,86 +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
- ];
148
- const HANIME_HOSTS = new Set(['r2.1hanime.com', '1.1hanime.com']);
149
- const OPPAI_HOSTS = new Set(['myspacecat.pictures']);
150
- const KAA_HOSTS = new Set([
151
- 'hls.krussdomi.com',
152
- 'subst.krussdomi.com',
153
- 'krussdomi.com',
154
- 'st1.advancedairesearchlab.xyz',
155
- 'st1.habibikun.xyz',
156
- 'st1.babybayw.xyz',
157
- 'st1.narutokun.xyz',
158
- ]);
159
- const KAA_REFERER = 'https://krussdomi.com/';
160
- const KAA_ORIGIN = 'https://krussdomi.com';
161
- const ANILIGHT_REFERER = 'https://anilight.live/';
162
- const ANILIGHT_ORIGIN = 'https://anilight.live';
163
- const ANILIGHT_HOSTS = [
164
- 'anilight.live',
165
- 'api.anilight.live',
166
- 'lostproject.club',
167
- 'streamzone1.site',
168
- 'nukitashi.top',
169
- 'vivibebe.site',
170
- 'vibeplayer.site',
171
- 'aniwatchtv.site',
172
- ];
173
- const NEXABLOOM_FAMILY_SUFFIXES = [
174
- '.quavex.top',
175
- '.nexabloom.top',
176
- '.hiddenvertex.top',
177
- '.ironhorizon.top',
178
- '.solarhaven.top',
179
- ];
180
- const NEXABLOOM_FALLBACK_HOSTS = ['fetch.nexabloom.top'];
181
- function nexabloomFallbackUrls(urlStr) {
182
- let host = '';
129
+ function matchProxyRule(urlStr, rules) {
130
+ let host;
183
131
  try {
184
132
  host = new URL(urlStr).hostname.toLowerCase();
185
133
  }
186
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)
187
144
  return [urlStr];
145
+ let current;
146
+ try {
147
+ current = new URL(urlStr);
188
148
  }
189
- if (!NEXABLOOM_FAMILY_SUFFIXES.some((s) => host.endsWith(s))) {
149
+ catch {
190
150
  return [urlStr];
191
151
  }
152
+ const seen = new Set([current.hostname.toLowerCase()]);
192
153
  const urls = [urlStr];
193
- for (const fallback of NEXABLOOM_FALLBACK_HOSTS) {
194
- if (fallback === host)
154
+ for (const mirror of mirrors) {
155
+ const host = mirror.toLowerCase();
156
+ if (seen.has(host))
195
157
  continue;
196
- try {
197
- const candidate = new URL(urlStr);
198
- candidate.hostname = fallback;
199
- urls.push(candidate.href);
200
- }
201
- catch {
202
- // ignore
203
- }
158
+ seen.add(host);
159
+ const candidate = new URL(current.href);
160
+ candidate.hostname = host;
161
+ urls.push(candidate.href);
204
162
  }
205
163
  return urls;
206
164
  }
207
- async function fetchNexabloomFallback(urlStr, signal, attempt, isOk) {
165
+ async function fetchWithMirrors(urlStr, mirrors, signal, attempt, isOk) {
208
166
  let last = null;
209
- for (const candidate of nexabloomFallbackUrls(urlStr)) {
167
+ for (const candidate of mirrorUrls(urlStr, mirrors)) {
210
168
  if (signal.aborted)
211
169
  break;
212
170
  const r = await attempt(candidate).catch(() => null);
@@ -218,34 +176,6 @@ async function fetchNexabloomFallback(urlStr, signal, attempt, isOk) {
218
176
  }
219
177
  return last;
220
178
  }
221
- function isGotScrapingHost(urlStr) {
222
- try {
223
- const host = new URL(urlStr).hostname.toLowerCase();
224
- if (GOT_SCRAPING_HOSTS.has(host))
225
- return true;
226
- if (HANIME_HOSTS.has(host))
227
- return true;
228
- return GOT_SCRAPING_SUFFIXES.some((s) => host.endsWith(s));
229
- }
230
- catch {
231
- return false;
232
- }
233
- }
234
- function validateKwikUrl(value) {
235
- if (typeof value !== 'string')
236
- return null;
237
- try {
238
- const url = new URL(value);
239
- const isSecure = url.protocol === 'https:';
240
- const isKwik = KWIK_DOMAINS.has(url.hostname.toLowerCase());
241
- const isEmbedPath = /^\/e\/[A-Za-z0-9_-]+$/.test(url.pathname);
242
- const noAuthOrQuery = !url.username && !url.password && !url.search && !url.hash;
243
- return isSecure && isKwik && isEmbedPath && noAuthOrQuery ? url : null;
244
- }
245
- catch {
246
- return null;
247
- }
248
- }
249
179
  function pinVariant(body, idx) {
250
180
  const lines = body.split('\n');
251
181
  let streamIdx = -1;
@@ -270,16 +200,16 @@ function pinVariant(body, idx) {
270
200
  return null;
271
201
  return out.join('\n');
272
202
  }
273
- function applyKwikPatches(html, kwikUrl, cookieStr = '') {
274
- 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');
275
205
  const safeCookie = JSON.stringify(cookieStr).replace(/</g, '\\u003c');
276
206
  const patched = html.replace(/\b(src|href|url)\s*[=:]\s*(["']?)(\/\/[^"'>)]+|\/(?!\/)[^"'>)]*)\2/gi, (match, attr, quote, path) => {
277
- const full = path.startsWith('//') ? `https:${path}` : `${kwikUrl.origin}${path}`;
207
+ const full = path.startsWith('//') ? `https:${path}` : `${embedUrl.origin}${path}`;
278
208
  const prefix = match.startsWith('url') ? `url(` : `${attr}=`;
279
209
  const suffix = match.startsWith('url') ? `)` : '';
280
210
  return `${prefix}${quote}${full}${quote}${suffix}`;
281
211
  });
282
- 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)}`;
283
213
  const plyrPatch = `<script>if(window.Plyr) Plyr.defaults.iconUrl=${JSON.stringify(iconProxy).replace(/</g, '\\u003c')};</script>`;
284
214
  const hlsPatch = `<script>
285
215
  (function() {
@@ -358,7 +288,7 @@ function placeholderSvg() {
358
288
  return null;
359
289
  }
360
290
  const PROXY_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0';
361
- export function registerProxy(app) {
291
+ export function registerProxy(app, getProxyRules) {
362
292
  app.get('/api/proxy', async (c) => {
363
293
  const url = c.req.query('url');
364
294
  if (!url)
@@ -368,97 +298,63 @@ export function registerProxy(app) {
368
298
  return c.text(safeCheck.error || 'Invalid URL', 400);
369
299
  }
370
300
  const urlStr = url;
371
- 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') || '';
372
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;
373
308
  const variantRaw = c.req.query('variant');
374
309
  const variantIdx = variantRaw !== undefined ? parseInt(variantRaw, 10) : NaN;
375
310
  const pinVar = Number.isInteger(variantIdx) && variantIdx >= 0;
376
- const cacheKey = `m3u8-${urlStr}-${refererStr}${pinVar ? `-v${variantIdx}` : ''}`;
311
+ const cacheKey = `m3u8-${urlStr}-${refererStr}-${originStr}-${uaStr}${pinVar ? `-v${variantIdx}` : ''}`;
377
312
  const abort = new AbortController();
378
313
  linkAbort(c.req.raw.signal, abort);
379
314
  try {
380
- const isVaultCdn = Array.from(VAULT_CDN_HOSTS).some((host) => urlStr.includes(host));
381
- const isHanime = Array.from(HANIME_HOSTS).some((host) => urlStr.includes(host));
382
- const isOppai = Array.from(OPPAI_HOSTS).some((host) => urlStr.includes(host));
383
- const isKaa = refererStr.startsWith(KAA_ORIGIN) ||
384
- Array.from(KAA_HOSTS).some((host) => urlStr.includes(host));
385
315
  const headers = {
386
- 'User-Agent': PROXY_UA,
316
+ 'User-Agent': uaStr || PROXY_UA,
387
317
  };
388
318
  if (refererStr)
389
319
  headers['Referer'] = refererStr;
390
- if (isVaultCdn) {
391
- headers['Origin'] = refererStr || ANIMEPAHE_URL;
392
- const cookieHeader = buildCfClearanceCookie(cookieStr);
320
+ if (originStr)
321
+ headers['Origin'] = originStr;
322
+ if (cookieStr) {
323
+ const cookieHeader = useCf ? buildCfClearanceCookie(cookieStr) : cookieStr;
393
324
  if (cookieHeader)
394
325
  headers['Cookie'] = cookieHeader;
395
326
  }
396
- if (isHanime) {
397
- if (!headers['Referer'])
398
- headers['Referer'] = refererStr || 'https://nhplayer.com/';
399
- headers['Origin'] = 'https://nhplayer.com';
400
- }
401
- if (isOppai) {
402
- if (!headers['Referer'])
403
- headers['Referer'] = refererStr || 'https://oppai.stream/';
404
- headers['Origin'] = 'https://oppai.stream';
405
- }
406
- if (isKaa) {
407
- if (!headers['Referer'])
408
- headers['Referer'] = refererStr || KAA_REFERER;
409
- headers['Origin'] = KAA_ORIGIN;
410
- }
411
- const isMegaplay = refererStr.startsWith(MEGAPLAY_ORIGIN) || urlStr.includes('nexabloom.top');
412
- if (isMegaplay && !headers['Origin']) {
413
- headers['Origin'] = MEGAPLAY_ORIGIN;
414
- }
415
- if (urlStr.includes('weeabo0.xyz') || urlStr.includes('weeab0o.xyz')) {
416
- if (!headers['Referer'])
417
- headers['Referer'] = refererStr || 'https://japaneseasmr.com/';
418
- const jasmrCookieHeader = buildCfClearanceCookie(cookieStr);
419
- if (jasmrCookieHeader)
420
- headers['Cookie'] = jasmrCookieHeader;
421
- const jasmrUa = c.req.query('ua') || '';
422
- if (jasmrUa)
423
- headers['User-Agent'] = jasmrUa;
424
- }
425
327
  const range = c.req.header('range');
426
328
  if (range)
427
329
  headers['Range'] = range;
428
- if (urlStr.includes('.m3u8')) {
429
- const cached = proxyCache.get(cacheKey);
430
- if (cached) {
431
- const headers = new Headers();
432
- headers.set('Content-Type', 'application/vnd.apple.mpegurl');
433
- headers.set('Access-Control-Allow-Origin', '*');
434
- return new Response(cached, { headers });
435
- }
436
- const resp = await fetchNexabloomFallback(urlStr, abort.signal, (candidate) => gotScraping({
437
- url: isNexabloomMasterUrl(candidate) ? signNexabloomMasterUrl(candidate) : candidate,
438
- method: 'GET',
439
- headers,
440
- responseType: 'text',
441
- timeout: { request: 30000 },
442
- followRedirect: true,
443
- throwHttpErrors: false,
444
- }), (attempt) => attempt.statusCode === 200 || attempt.statusCode === 206);
445
- if (!resp || (resp.statusCode !== 200 && resp.statusCode !== 206)) {
446
- return c.text('Upstream error', (resp?.statusCode || 502));
447
- }
448
- const finalUrl = resp.url || urlStr;
330
+ const playlistResponse = (body) => {
331
+ const outHeaders = new Headers();
332
+ outHeaders.set('Content-Type', 'application/vnd.apple.mpegurl');
333
+ outHeaders.set('Access-Control-Allow-Origin', '*');
334
+ return new Response(body, { headers: outHeaders });
335
+ };
336
+ const rewritePlaylist = (playlistBody, finalUrl) => {
449
337
  const baseUrl = new URL(finalUrl);
450
- const proxiedMediaUrl = (targetUrl) => `/api/proxy?url=${encodeURIComponent(targetUrl)}&referer=${encodeURIComponent(refererStr)}` +
451
- (cookieStr ? `&cookie=${encodeURIComponent(sanitizeCfClearance(cookieStr))}` : '') +
452
- (c.req.query('ua') ? `&ua=${encodeURIComponent(c.req.query('ua'))}` : '');
453
- 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);
454
355
  const isProxied = (value) => value.includes('/api/proxy');
455
- let playlistBody = resp.body;
456
- if (pinVar) {
457
- const pinned = pinVariant(playlistBody, variantIdx);
458
- if (pinned)
459
- playlistBody = pinned;
460
- }
461
- const rewritten = playlistBody
356
+ const pinned = pinVar ? pinVariant(playlistBody, variantIdx) : null;
357
+ return (pinned || playlistBody)
462
358
  .split('\n')
463
359
  .map((line) => {
464
360
  const trimmed = line.trim();
@@ -478,17 +374,36 @@ export function registerProxy(app) {
478
374
  return proxiedMediaUrl(absolute);
479
375
  })
480
376
  .join('\n');
377
+ };
378
+ if (urlStr.includes('.m3u8')) {
379
+ const cached = proxyCache.get(cacheKey);
380
+ if (cached) {
381
+ const headers = new Headers();
382
+ headers.set('Content-Type', 'application/vnd.apple.mpegurl');
383
+ headers.set('Access-Control-Allow-Origin', '*');
384
+ return new Response(cached, { headers });
385
+ }
386
+ const resp = await fetchWithMirrors(urlStr, rule?.mirrors, abort.signal, (candidate) => gotScraping({
387
+ url: candidate,
388
+ method: 'GET',
389
+ headers,
390
+ responseType: 'text',
391
+ timeout: { request: 30000 },
392
+ followRedirect: true,
393
+ throwHttpErrors: false,
394
+ }), (attempt) => attempt.statusCode === 200 || attempt.statusCode === 206);
395
+ if (!resp || (resp.statusCode !== 200 && resp.statusCode !== 206)) {
396
+ return c.text('Upstream error', (resp?.statusCode || 502));
397
+ }
398
+ const rewritten = rewritePlaylist(resp.body, resp.url || urlStr);
481
399
  proxyCache.set(cacheKey, rewritten);
482
- const outHeaders = new Headers();
483
- outHeaders.set('Content-Type', 'application/vnd.apple.mpegurl');
484
- outHeaders.set('Access-Control-Allow-Origin', '*');
485
- return new Response(rewritten, { headers: outHeaders });
400
+ return playlistResponse(rewritten);
486
401
  }
487
402
  else {
488
- if (isGotScrapingHost(urlStr)) {
403
+ if (useScrape) {
489
404
  if (range)
490
405
  headers['Range'] = range;
491
- const resp = await fetchNexabloomFallback(urlStr, abort.signal, (candidate) => gotScraping({
406
+ const resp = await fetchWithMirrors(urlStr, rule?.mirrors, abort.signal, (candidate) => gotScraping({
492
407
  url: candidate,
493
408
  method: 'GET',
494
409
  headers,
@@ -502,6 +417,15 @@ export function registerProxy(app) {
502
417
  }
503
418
  const ct = resp.headers['content-type'];
504
419
  const isVtt = urlStr.endsWith('.vtt') || urlStr.includes('.vtt?');
420
+ const bodyText = String(resp.body ?? '');
421
+ if (!isVtt &&
422
+ ((typeof ct === 'string' && ct.includes('mpegurl')) || bodyText.startsWith('#EXTM3U'))) {
423
+ const cached = proxyCache.get(cacheKey);
424
+ const rewritten = cached ?? rewritePlaylist(bodyText, resp.url || urlStr);
425
+ if (!cached)
426
+ proxyCache.set(cacheKey, rewritten);
427
+ return playlistResponse(rewritten);
428
+ }
505
429
  const outHeaders = new Headers();
506
430
  outHeaders.set('Content-Type', isVtt ? 'text/vtt; charset=utf-8' : ct || 'application/octet-stream');
507
431
  setProxyHeaders(outHeaders, (name) => resp.headers[name]);
@@ -511,7 +435,7 @@ export function registerProxy(app) {
511
435
  });
512
436
  }
513
437
  else {
514
- const upstream = await fetchNexabloomFallback(urlStr, abort.signal, async (candidate) => {
438
+ const upstream = await fetchWithMirrors(urlStr, rule?.mirrors, abort.signal, async (candidate) => {
515
439
  const attempt = await fetchWithRetry(candidate, { method: 'GET', headers }, { retries: 3, timeoutMs: 30000, signal: abort.signal });
516
440
  if (attempt.status !== 200 && attempt.status !== 206) {
517
441
  try {
@@ -556,25 +480,36 @@ export function registerProxy(app) {
556
480
  }
557
481
  });
558
482
  app.get('/api/embed-proxy', async (c) => {
559
- const kwikUrl = validateKwikUrl(c.req.query('url'));
560
- 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)
561
491
  return c.text('Invalid or unsupported gateway URL', 400);
492
+ const embedUrl = new URL(rawUrl);
562
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'));
563
496
  const abort = new AbortController();
564
497
  linkAbort(c.req.raw.signal, abort);
565
498
  try {
566
499
  const headers = {
567
- 'User-Agent': PROXY_UA,
568
- Referer: ANIMEPAHE_URL,
569
- Origin: 'https://animepahe.pw',
500
+ 'User-Agent': c.req.query('ua') || PROXY_UA,
570
501
  };
502
+ if (refererStr)
503
+ headers['Referer'] = refererStr;
504
+ if (originStr)
505
+ headers['Origin'] = originStr;
571
506
  if (cookieStr) {
572
- const cookieHeader = buildCfClearanceCookie(cookieStr);
507
+ const cookieHeader = c.req.query('cf') === '1' || rule.cfCookie ? buildCfClearanceCookie(cookieStr) : cookieStr;
573
508
  if (cookieHeader)
574
509
  headers['Cookie'] = cookieHeader;
575
510
  }
576
511
  const response = await gotScraping({
577
- url: kwikUrl.href,
512
+ url: embedUrl.href,
578
513
  method: 'GET',
579
514
  headers,
580
515
  responseType: 'text',
@@ -583,7 +518,7 @@ export function registerProxy(app) {
583
518
  throwHttpErrors: false,
584
519
  });
585
520
  const originalHtml = response.body;
586
- const patched = applyKwikPatches(originalHtml, kwikUrl, cookieStr);
521
+ const patched = applyEmbedPatches(originalHtml, embedUrl, cookieStr);
587
522
  if (!patched)
588
523
  return c.text('Failed to patch video gateway', 502);
589
524
  const outHeaders = new Headers();
@@ -608,49 +543,51 @@ export function registerProxy(app) {
608
543
  const abort = new AbortController();
609
544
  linkAbort(c.req.raw.signal, abort);
610
545
  const subUrl = url;
611
- const refererStr = c.req.query('referer') || '';
612
- const isAnilight = refererStr.includes('anilight.live') || ANILIGHT_HOSTS.some((host) => subUrl.includes(host));
613
- const innerRawUrl = (() => {
614
- try {
615
- const parsed = new URL(subUrl);
616
- if (parsed.hostname.toLowerCase().endsWith('anilight.live') &&
617
- parsed.pathname.includes('/proxy/captions')) {
618
- const inner = parsed.searchParams.get('url');
619
- if (inner && (inner.startsWith('http://') || inner.startsWith('https://'))) {
620
- return inner;
621
- }
622
- }
623
- }
624
- catch {
625
- // ignore
626
- }
627
- return null;
628
- })();
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;
629
550
  const buildHeaders = () => {
630
551
  const headers = {
631
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',
632
553
  Accept: 'text/vtt,text/plain,*/*;q=0.8',
633
554
  };
634
- if (refererStr) {
555
+ if (refererStr)
635
556
  headers['Referer'] = refererStr;
636
- }
637
- else if (isAnilight) {
638
- headers['Referer'] = ANILIGHT_REFERER;
639
- }
640
- if (refererStr.startsWith(KAA_ORIGIN) ||
641
- Array.from(KAA_HOSTS).some((host) => subUrl.includes(host))) {
642
- headers['Origin'] = KAA_ORIGIN;
643
- }
644
- else if (isAnilight) {
645
- headers['Origin'] = ANILIGHT_ORIGIN;
646
- }
557
+ if (originStr)
558
+ headers['Origin'] = originStr;
647
559
  return headers;
648
560
  };
649
561
  const fetchText = async (target, retries = 1) => {
650
562
  const headers = buildHeaders();
651
- if (innerRawUrl && target === innerRawUrl) {
652
- headers['Referer'] = ANILIGHT_REFERER;
653
- headers['Origin'] = ANILIGHT_ORIGIN;
563
+ if (useScrape) {
564
+ try {
565
+ const upstream = await gotScraping({
566
+ url: target,
567
+ method: 'GET',
568
+ headers,
569
+ responseType: 'text',
570
+ timeout: { request: 15000 },
571
+ followRedirect: true,
572
+ throwHttpErrors: false,
573
+ });
574
+ if (upstream.statusCode !== 200) {
575
+ logger.warn({ target, status: upstream.statusCode }, '[subtitle-proxy] upstream returned error status');
576
+ return { status: upstream.statusCode, body: '' };
577
+ }
578
+ const body = String(upstream.body ?? '').replace(/^\uFEFF/, '');
579
+ if (/^\s*\{[\s\S]*"error"[\s\S]*\}\s*$/.test(body.slice(0, 500))) {
580
+ logger.warn({ target, body: body.slice(0, 200) }, '[subtitle-proxy] upstream JSON error');
581
+ return { status: 502, body: '' };
582
+ }
583
+ return { status: 200, body };
584
+ }
585
+ catch (e) {
586
+ if (abort.signal.aborted)
587
+ return null;
588
+ logger.warn({ target, error: e?.message }, '[subtitle-proxy] upstream fetch failed');
589
+ return { status: 502, body: '' };
590
+ }
654
591
  }
655
592
  try {
656
593
  const upstream = await fetchWithRetry(target, { method: 'GET', headers }, { retries, timeoutMs: 15000, signal: abort.signal });
@@ -693,19 +630,14 @@ export function registerProxy(app) {
693
630
  };
694
631
  try {
695
632
  const headers = buildHeaders();
696
- let result = await fetchText(subUrl, 1);
633
+ const result = await fetchText(subUrl, 1);
697
634
  if (abort.signal.aborted)
698
635
  return new Response(null, { status: 499 });
699
- if ((!result || !result.body) && innerRawUrl) {
700
- result = await fetchText(innerRawUrl, 0);
701
- if (abort.signal.aborted)
702
- return new Response(null, { status: 499 });
703
- }
704
636
  if (!result || !result.body) {
705
637
  return subtitleError('Subtitle upstream error');
706
638
  }
707
639
  const body = result.body;
708
- const baseForSegments = innerRawUrl || subUrl;
640
+ const baseForSegments = subUrl;
709
641
  if (/#EXTM3U/i.test(body.slice(0, 1000))) {
710
642
  const segUrls = body
711
643
  .split('\n')
@@ -786,14 +718,16 @@ export function registerProxy(app) {
786
718
  const cookie = c.req.query('cookie') || '';
787
719
  const ua = c.req.query('ua') || '';
788
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'));
789
724
  const abort = new AbortController();
790
725
  linkAbort(c.req.raw.signal, abort);
791
- let refererValue = 'https://anilist.co/';
792
726
  const headers = {
793
727
  'User-Agent': ua ||
794
728
  'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
795
729
  };
796
- const cacheKey = `${targetUrl}|${cookie}|${ua}`;
730
+ const cacheKey = `${targetUrl}|${cookie}|${ua}|${refererValue}|${originValue}`;
797
731
  const cached = imageCache.get(cacheKey);
798
732
  if (cached) {
799
733
  const hit = new Headers();
@@ -804,32 +738,15 @@ export function registerProxy(app) {
804
738
  return new Response(cached.body, { headers: hit });
805
739
  }
806
740
  try {
807
- if (targetUrl.includes('animepahe')) {
808
- refererValue = 'https://animepahe.pw/';
809
- headers['Cookie'] = `cf_clearance=${sanitizeCfClearance(cookie)}`;
810
- }
811
- else if (targetUrl.includes('anilist.co')) {
812
- refererValue = 'https://anilist.co/';
813
- }
814
- else if (targetUrl.includes('gogocdn.net')) {
815
- refererValue = 'https://gogoanime.lu/';
816
- }
817
- else if (targetUrl.includes('animeya.cc')) {
818
- refererValue = 'https://animeya.cc/';
819
- }
820
- else if (targetUrl.includes('myspacecat.pictures')) {
821
- refererValue = 'https://oppai.stream/';
822
- }
823
- else if (targetUrl.includes('weeabo0.xyz')) {
824
- refererValue = 'https://japaneseasmr.com/';
825
- const jasmrCookieHeader = buildCfClearanceCookie(cookie);
826
- if (jasmrCookieHeader)
827
- headers['Cookie'] = jasmrCookieHeader;
828
- const uaParam = ua || '';
829
- if (uaParam)
830
- 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;
831
749
  }
832
- headers['Referer'] = refererValue;
833
750
  let body = null;
834
751
  let contentType = 'image/webp';
835
752
  for (let attempt = 0; attempt < 3 && !body; attempt++) {