@serifpersia/dango 3.4.4 → 3.4.6

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 (96) hide show
  1. package/README.md +1 -1
  2. package/client/dist/assets/{AnimeCard-4CEz8Buu.js → AnimeCard-BVdH0EeW.js} +1 -1
  3. package/client/dist/assets/{AnimeInfo-DyX9qTwg.js → AnimeInfo-Dw2Nop6Q.js} +1 -1
  4. package/client/dist/assets/{AnimeInfoPage-Dvp_c8vK.js → AnimeInfoPage-Cw-Tq3YX.js} +2 -2
  5. package/client/dist/assets/{Asmr-qg3lMAJn.js → Asmr-BrjzzyOi.js} +1 -1
  6. package/client/dist/assets/{AsmrResetProgressModal-BpPlg_Vv.js → AsmrResetProgressModal-CYjpcYpb.js} +1 -1
  7. package/client/dist/assets/{AvSyncCalibrator-FJllOgOH.js → AvSyncCalibrator-BuHurVgo.js} +3 -3
  8. package/client/dist/assets/AvSyncCalibrator-D0FYSwiz.css +1 -0
  9. package/client/dist/assets/{Home-A7gCeEST.js → Home-KdnP7sNM.js} +1 -1
  10. package/client/dist/assets/{Insights-G9IXepHJ.js → Insights-DDWry-t1.js} +1 -1
  11. package/client/dist/assets/{LibraryListPage-Bh2qCJPh.js → LibraryListPage-CV8Z9JlO.js} +1 -1
  12. package/client/dist/assets/{ListeningList-Czv4cTRl.js → ListeningList-DSMbMZT7.js} +1 -1
  13. package/client/dist/assets/{Manga-qdAAS1z7.js → Manga-J6XoqEKq.js} +1 -1
  14. package/client/dist/assets/{MangaInfoPage-1rTgZzf2.js → MangaInfoPage-N7TxsN5u.js} +1 -1
  15. package/client/dist/assets/{MangaReadPage-CeyxPG6w.js → MangaReadPage-D_VzSNWq.js} +1 -1
  16. package/client/dist/assets/{MangaResetProgressModal-C_ppotmh.js → MangaResetProgressModal-DyhQYBDy.js} +1 -1
  17. package/client/dist/assets/{Mature-Bkhxnr3K.js → Mature-Db53R-LR.js} +1 -1
  18. package/client/dist/assets/{MediaCard-BQZsutEo.js → MediaCard-BcLjlpfR.js} +1 -1
  19. package/client/dist/assets/{Music-C0rGzSbi.js → Music-DjxQjJOl.js} +1 -1
  20. package/client/dist/assets/{OptionListMenu-CyfpCBEy.js → OptionListMenu-DngTgHxH.js} +1 -1
  21. package/client/dist/assets/Player-Be_7Ne5p.js +56 -0
  22. package/client/dist/assets/{PlayerSettings-icy4SfQa.js → PlayerSettings-D47ItGDR.js} +1 -1
  23. package/client/dist/assets/{QueueOptionsButton-5eloKF5J.js → QueueOptionsButton-ClVTpH8J.js} +1 -1
  24. package/client/dist/assets/{QueueRail-DaMjlKL4.js → QueueRail-DeJKVRs5.js} +1 -1
  25. package/client/dist/assets/{Radio-FxGqCxR4.js → Radio-DU0ma3GA.js} +1 -1
  26. package/client/dist/assets/{ReadingList-CCm3IAbt.js → ReadingList-qXW7Dk5z.js} +1 -1
  27. package/client/dist/assets/{ResetProgressModal-CeI4V3Y5.js → ResetProgressModal-C6ukct_c.js} +1 -1
  28. package/client/dist/assets/{Search-qpILaAiQ.js → Search-DjjKA_Wu.js} +1 -1
  29. package/client/dist/assets/{Settings-Bw9M3NGz.js → Settings-DQt_S5Qw.js} +1 -1
  30. package/client/dist/assets/{SkeletonGrid-0dY_xMq3.js → SkeletonGrid-nx2HEi4P.js} +1 -1
  31. package/client/dist/assets/{Trackers-Zy5GOs0P.js → Trackers-BcGzeqj2.js} +1 -1
  32. package/client/dist/assets/Tv-COvyHdeJ.js +4 -0
  33. package/client/dist/assets/{TvInfo-CHIsVzZ0.js → TvInfo-CUd1agFy.js} +1 -1
  34. package/client/dist/assets/{TvPopup-D_tVTyZ2.js → TvPopup-CJeu3tgc.js} +1 -1
  35. package/client/dist/assets/{TvSearch-BVxedBW-.js → TvSearch-BIypjbU3.js} +1 -1
  36. package/client/dist/assets/{TvWatchlist-BEIwq9R9.js → TvWatchlist-DZrBdiqZ.js} +1 -1
  37. package/client/dist/assets/{Watchlist-CSngjqlz.js → Watchlist-Dw8cPe0n.js} +1 -1
  38. package/client/dist/assets/{hls-D48GVTkk.js → hls-B3_1VuDP.js} +1 -1
  39. package/client/dist/assets/{index-CVB5dYhT.js → index-gMxawd-E.js} +2 -2
  40. package/client/dist/assets/{useAnimeInfoData-DxdOg_B7.js → useAnimeInfoData-_iOLEAa8.js} +1 -1
  41. package/client/dist/assets/{useAsmr-ClckxnPe.js → useAsmr-Blk4v41t.js} +1 -1
  42. package/client/dist/assets/{useAsmrPopup-Bbgxefy_.js → useAsmrPopup-D9SQIgZa.js} +1 -1
  43. package/client/dist/assets/{useMangaLibrary-B3_BUKVk.js → useMangaLibrary-B60at6ni.js} +1 -1
  44. package/client/dist/assets/{useMangaPopup-Celk4lzf.js → useMangaPopup-DEfGjWSu.js} +1 -1
  45. package/client/dist/assets/{useProviders-pwxPZPHh.js → useProviders-Ca0bZWt7.js} +1 -1
  46. package/client/dist/assets/{useTv-D9sQrDop.js → useTv-WJCDL5_5.js} +1 -1
  47. package/client/dist/assets/{useTvLibrary-UsQpgBTB.js → useTvLibrary-Fgn5i9rJ.js} +1 -1
  48. package/client/dist/index.html +1 -1
  49. package/package.json +4 -2
  50. package/server/dist/app-hono.js +2 -0
  51. package/server/dist/hono/asmr.js +10 -1
  52. package/server/dist/hono/music.js +122 -332
  53. package/server/dist/hono/proxy.js +105 -99
  54. package/server/dist/hono/tv.js +5 -58
  55. package/server/dist/hono/watchlist.js +2 -291
  56. package/server/dist/lib/anilist.js +0 -3
  57. package/server/dist/lib/mal.js +4 -41
  58. package/server/dist/lib/offline-db.js +0 -6
  59. package/server/dist/lib/potoken.js +135 -0
  60. package/server/dist/lib/tracker/manga-sync.service.js +3 -22
  61. package/server/dist/lib/tracker/sync-state.js +21 -0
  62. package/server/dist/lib/tracker/sync.service.js +3 -22
  63. package/server/dist/lib/ump.js +106 -0
  64. package/server/dist/providers/remote-loader.js +17 -27
  65. package/server/dist/providers/title-matching.js +5 -5
  66. package/server/dist/repositories/asmr.repository.js +4 -53
  67. package/server/dist/repositories/library.repository.js +46 -0
  68. package/server/dist/repositories/manga.repository.js +3 -53
  69. package/server/dist/repositories/settings.repository.js +8 -0
  70. package/server/dist/repositories/tv.repository.js +3 -46
  71. package/server/dist/repositories/watched-episodes.repository.js +0 -2
  72. package/server/dist/server.js +5 -1
  73. package/server/dist/sync.js +3 -5
  74. package/server/dist/trpc/routers/asmr-library.js +2 -12
  75. package/server/dist/trpc/routers/asmr-progress.js +10 -22
  76. package/server/dist/trpc/routers/data.js +17 -36
  77. package/server/dist/trpc/routers/discord.js +1 -8
  78. package/server/dist/trpc/routers/insights.js +15 -16
  79. package/server/dist/trpc/routers/manga-library.js +5 -22
  80. package/server/dist/trpc/routers/manga-progress.js +8 -20
  81. package/server/dist/trpc/routers/music.js +6 -11
  82. package/server/dist/trpc/routers/queue.js +1 -1
  83. package/server/dist/trpc/routers/tracker.js +1 -7
  84. package/server/dist/trpc/routers/tv-library.js +2 -12
  85. package/server/dist/trpc/routers/tv-progress.js +16 -28
  86. package/server/dist/trpc/routers/tv.js +5 -21
  87. package/server/dist/trpc/routers/watchlist.js +1 -8
  88. package/server/dist/trpc/validation.js +35 -0
  89. package/server/dist/utils/db-utils.js +8 -0
  90. package/server/dist/utils/http.utils.js +12 -0
  91. package/server/dist/utils/machine-id.js +1 -2
  92. package/server/dist/utils/megaplay.utils.js +1 -2
  93. package/server/package.json +4 -1
  94. package/client/dist/assets/AvSyncCalibrator-C2wu1ydI.css +0 -1
  95. package/client/dist/assets/Player-yhiTlpYU.js +0 -56
  96. package/client/dist/assets/Tv-DREwc7BX.js +0 -4
@@ -6,7 +6,7 @@ import fs from 'fs';
6
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
- import { fetchWithRetry } from '../utils/http.utils.js';
9
+ import { fetchWithRetry, linkAbort, setProxyHeaders } from '../utils/http.utils.js';
10
10
  import { MEGAPLAY_ORIGIN, isNexabloomMasterUrl, signNexabloomMasterUrl, } from '../utils/megaplay.utils.js';
11
11
  const proxyCache = new AppCache({ ttlSeconds: 30, maxKeys: 500 });
12
12
  function assTimeToVtt(value) {
@@ -82,6 +82,50 @@ function assToVtt(body) {
82
82
  return null;
83
83
  return `WEBVTT\n\n${cues.join('\n\n')}\n`;
84
84
  }
85
+ const SUB_WRAP_AT = 50;
86
+ function splitBalancedCueLine(line) {
87
+ const words = line.split(/\s+/).filter((w) => w.length > 0);
88
+ if (words.length < 2)
89
+ return [line];
90
+ const total = words.join(' ').length;
91
+ if (total <= SUB_WRAP_AT)
92
+ return [line];
93
+ const target = total / 2;
94
+ let acc = words[0]?.length ?? 0;
95
+ let best = 1;
96
+ let bestDiff = Math.abs(acc - target);
97
+ for (let i = 1; i < words.length; i++) {
98
+ acc += 1 + (words[i]?.length ?? 0);
99
+ const diff = Math.abs(acc - target);
100
+ if (diff < bestDiff) {
101
+ bestDiff = diff;
102
+ best = i + 1;
103
+ }
104
+ }
105
+ if (best <= 0 || best >= words.length)
106
+ return [line];
107
+ return [words.slice(0, best).join(' '), words.slice(best).join(' ')];
108
+ }
109
+ function rewrapVttCues(vtt) {
110
+ const blocks = vtt.replace(/\r\n/g, '\n').split(/\n\n+/);
111
+ return blocks
112
+ .map((block, idx) => {
113
+ if (idx === 0 && /^\s*WEBVTT/i.test(block))
114
+ return block;
115
+ const lines = block.split('\n');
116
+ const tIdx = lines.findIndex((l) => l.includes('-->'));
117
+ if (tIdx < 0)
118
+ return block;
119
+ const payload = lines.slice(tIdx + 1).filter((l) => l.trim() !== '');
120
+ if (payload.length !== 1)
121
+ return block;
122
+ const split = splitBalancedCueLine((payload[0] ?? '').trim());
123
+ if (split.length < 2)
124
+ return block;
125
+ return [...lines.slice(0, tIdx + 1), ...split].join('\n');
126
+ })
127
+ .join('\n\n');
128
+ }
85
129
  const KWIK_DOMAINS = new Set(['kwik.cx', 'kwik.si', 'kwik.pro']);
86
130
  const ANIMEPAHE_URL = 'https://animepahe.pw/';
87
131
  const VAULT_CDN_HOSTS = new Set(['uwucdn.top', 'owocdn.top']);
@@ -159,6 +203,20 @@ function nexabloomFallbackUrls(urlStr) {
159
203
  }
160
204
  return urls;
161
205
  }
206
+ async function fetchNexabloomFallback(urlStr, signal, attempt, isOk) {
207
+ let last = null;
208
+ for (const candidate of nexabloomFallbackUrls(urlStr)) {
209
+ if (signal.aborted)
210
+ break;
211
+ const r = await attempt(candidate).catch(() => null);
212
+ if (!r)
213
+ continue;
214
+ if (isOk(r))
215
+ return r;
216
+ last = r;
217
+ }
218
+ return last;
219
+ }
162
220
  function isGotScrapingHost(urlStr) {
163
221
  try {
164
222
  const host = new URL(urlStr).hostname.toLowerCase();
@@ -299,9 +357,6 @@ function placeholderSvg() {
299
357
  return null;
300
358
  }
301
359
  const PROXY_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0';
302
- function linkAbort(signal, abort) {
303
- signal.addEventListener('abort', () => abort.abort(), { once: true });
304
- }
305
360
  export function registerProxy(app) {
306
361
  app.get('/api/proxy', async (c) => {
307
362
  const url = c.req.query('url');
@@ -377,24 +432,15 @@ export function registerProxy(app) {
377
432
  headers.set('Access-Control-Allow-Origin', '*');
378
433
  return new Response(cached, { headers });
379
434
  }
380
- const resp = await (async () => {
381
- let last = null;
382
- for (const candidate of nexabloomFallbackUrls(urlStr)) {
383
- const attempt = await gotScraping({
384
- url: isNexabloomMasterUrl(candidate) ? signNexabloomMasterUrl(candidate) : candidate,
385
- method: 'GET',
386
- headers,
387
- responseType: 'text',
388
- timeout: { request: 30000 },
389
- followRedirect: true,
390
- throwHttpErrors: false,
391
- });
392
- last = attempt;
393
- if (attempt.statusCode === 200 || attempt.statusCode === 206)
394
- return attempt;
395
- }
396
- return last;
397
- })();
435
+ const resp = await fetchNexabloomFallback(urlStr, abort.signal, (candidate) => gotScraping({
436
+ url: isNexabloomMasterUrl(candidate) ? signNexabloomMasterUrl(candidate) : candidate,
437
+ method: 'GET',
438
+ headers,
439
+ responseType: 'text',
440
+ timeout: { request: 30000 },
441
+ followRedirect: true,
442
+ throwHttpErrors: false,
443
+ }), (attempt) => attempt.statusCode === 200 || attempt.statusCode === 206);
398
444
  if (!resp || (resp.statusCode !== 200 && resp.statusCode !== 206)) {
399
445
  return c.text('Upstream error', (resp?.statusCode || 502));
400
446
  }
@@ -441,21 +487,15 @@ export function registerProxy(app) {
441
487
  if (isGotScrapingHost(urlStr)) {
442
488
  if (range)
443
489
  headers['Range'] = range;
444
- let resp = null;
445
- for (const candidate of nexabloomFallbackUrls(urlStr)) {
446
- const attempt = await gotScraping({
447
- url: candidate,
448
- method: 'GET',
449
- headers,
450
- responseType: 'buffer',
451
- timeout: { request: 30000 },
452
- followRedirect: true,
453
- throwHttpErrors: false,
454
- });
455
- resp = attempt;
456
- if (attempt.statusCode === 200 || attempt.statusCode === 206)
457
- break;
458
- }
490
+ const resp = await fetchNexabloomFallback(urlStr, abort.signal, (candidate) => gotScraping({
491
+ url: candidate,
492
+ method: 'GET',
493
+ headers,
494
+ responseType: 'buffer',
495
+ timeout: { request: 30000 },
496
+ followRedirect: true,
497
+ throwHttpErrors: false,
498
+ }), (attempt) => attempt.statusCode === 200 || attempt.statusCode === 206);
459
499
  if (!resp || (resp.statusCode !== 200 && resp.statusCode !== 206)) {
460
500
  return c.text('Upstream error', (resp?.statusCode || 502));
461
501
  }
@@ -463,31 +503,16 @@ export function registerProxy(app) {
463
503
  const isVtt = urlStr.endsWith('.vtt') || urlStr.includes('.vtt?');
464
504
  const outHeaders = new Headers();
465
505
  outHeaders.set('Content-Type', isVtt ? 'text/vtt; charset=utf-8' : ct || 'application/octet-stream');
466
- const cl = resp.headers['content-length'];
467
- if (cl)
468
- outHeaders.set('Content-Length', cl);
469
- const cr = resp.headers['content-range'];
470
- if (cr)
471
- outHeaders.set('Content-Range', cr);
472
- const ar = resp.headers['accept-ranges'];
473
- if (ar)
474
- outHeaders.set('Accept-Ranges', ar);
475
- outHeaders.set('Access-Control-Allow-Origin', '*');
506
+ setProxyHeaders(outHeaders, (name) => resp.headers[name]);
476
507
  return new Response(resp.body, {
477
508
  status: resp.statusCode,
478
509
  headers: outHeaders,
479
510
  });
480
511
  }
481
512
  else {
482
- let upstream = null;
483
- for (const candidate of nexabloomFallbackUrls(urlStr)) {
484
- if (abort.signal.aborted)
485
- break;
486
- try {
487
- const attempt = await fetchWithRetry(candidate, { method: 'GET', headers }, { retries: 3, timeoutMs: 30000, signal: abort.signal });
488
- upstream = attempt;
489
- if (attempt.status === 200 || attempt.status === 206)
490
- break;
513
+ const upstream = await fetchNexabloomFallback(urlStr, abort.signal, async (candidate) => {
514
+ const attempt = await fetchWithRetry(candidate, { method: 'GET', headers }, { retries: 3, timeoutMs: 30000, signal: abort.signal });
515
+ if (attempt.status !== 200 && attempt.status !== 206) {
491
516
  try {
492
517
  await attempt.body?.cancel();
493
518
  }
@@ -495,10 +520,8 @@ export function registerProxy(app) {
495
520
  // ignore
496
521
  }
497
522
  }
498
- catch {
499
- // ignore
500
- }
501
- }
523
+ return attempt;
524
+ }, (attempt) => attempt.status === 200 || attempt.status === 206);
502
525
  if (!upstream) {
503
526
  return c.text('Upstream error', 502);
504
527
  }
@@ -516,16 +539,7 @@ export function registerProxy(app) {
516
539
  const ct = upstream.headers.get('content-type');
517
540
  const outHeaders = new Headers();
518
541
  outHeaders.set('Content-Type', isVtt ? 'text/vtt; charset=utf-8' : ct || 'application/octet-stream');
519
- const cl = upstream.headers.get('content-length');
520
- if (cl)
521
- outHeaders.set('Content-Length', cl);
522
- const cr = upstream.headers.get('content-range');
523
- if (cr)
524
- outHeaders.set('Content-Range', cr);
525
- const ar = upstream.headers.get('accept-ranges');
526
- if (ar)
527
- outHeaders.set('Accept-Ranges', ar);
528
- outHeaders.set('Access-Control-Allow-Origin', '*');
542
+ setProxyHeaders(outHeaders, (name) => upstream.headers.get(name));
529
543
  if (!upstream.body) {
530
544
  return c.text('Upstream error', 502);
531
545
  }
@@ -670,6 +684,12 @@ export function registerProxy(app) {
670
684
  headers.set('Cache-Control', cacheControl);
671
685
  return headers;
672
686
  };
687
+ const subtitleError = (text) => {
688
+ const headers = new Headers();
689
+ headers.set('Access-Control-Allow-Origin', '*');
690
+ headers.set('Cache-Control', 'no-store');
691
+ return new Response(text, { status: 502, headers });
692
+ };
673
693
  try {
674
694
  const headers = buildHeaders();
675
695
  let result = await fetchText(subUrl, 1);
@@ -681,10 +701,7 @@ export function registerProxy(app) {
681
701
  return new Response(null, { status: 499 });
682
702
  }
683
703
  if (!result || !result.body) {
684
- const headers = new Headers();
685
- headers.set('Access-Control-Allow-Origin', '*');
686
- headers.set('Cache-Control', 'no-store');
687
- return new Response('Subtitle upstream error', { status: 502, headers });
704
+ return subtitleError('Subtitle upstream error');
688
705
  }
689
706
  const body = result.body;
690
707
  const baseForSegments = innerRawUrl || subUrl;
@@ -730,32 +747,30 @@ export function registerProxy(app) {
730
747
  }
731
748
  }
732
749
  if (parts.length === 0) {
733
- const headers = new Headers();
734
- headers.set('Access-Control-Allow-Origin', '*');
735
- headers.set('Cache-Control', 'no-store');
736
- return new Response('Subtitle playlist empty', { status: 502, headers });
750
+ return subtitleError('Subtitle playlist empty');
737
751
  }
738
- return new Response(`WEBVTT\n\n${parts.join('\n\n')}\n`, {
752
+ return new Response(rewrapVttCues(`WEBVTT\n\n${parts.join('\n\n')}\n`), {
739
753
  headers: vttHeaders('public, max-age=86400'),
740
754
  });
741
755
  }
742
756
  if (/^\s*WEBVTT/i.test(body)) {
743
- return new Response(body, { headers: vttHeaders('public, max-age=86400') });
757
+ return new Response(rewrapVttCues(body), {
758
+ headers: vttHeaders('public, max-age=86400'),
759
+ });
744
760
  }
745
761
  const vttFromAss = assToVtt(body);
746
762
  if (vttFromAss) {
747
- return new Response(vttFromAss, { headers: vttHeaders('public, max-age=86400') });
763
+ return new Response(rewrapVttCues(vttFromAss), {
764
+ headers: vttHeaders('public, max-age=86400'),
765
+ });
748
766
  }
749
- return new Response(`WEBVTT\n\n${body.replace(/\r\n/g, '\n').replace(/(\d{2}:\d{2}:\d{2}),(\d{3})/g, '$1.$2')}`, { headers: vttHeaders('public, max-age=86400') });
767
+ return new Response(rewrapVttCues(`WEBVTT\n\n${body.replace(/\r\n/g, '\n').replace(/(\d{2}:\d{2}:\d{2}),(\d{3})/g, '$1.$2')}`), { headers: vttHeaders('public, max-age=86400') });
750
768
  }
751
769
  catch {
752
770
  if (abort.signal.aborted)
753
771
  return new Response(null, { status: 499 });
754
772
  logger.warn({ url: subUrl }, '[subtitle-proxy] failed');
755
- const headers = new Headers();
756
- headers.set('Access-Control-Allow-Origin', '*');
757
- headers.set('Cache-Control', 'no-store');
758
- return new Response('Subtitle upstream error', { status: 502, headers });
773
+ return subtitleError('Subtitle upstream error');
759
774
  }
760
775
  });
761
776
  app.get('/api/image-proxy', async (c) => {
@@ -779,12 +794,7 @@ export function registerProxy(app) {
779
794
  try {
780
795
  if (targetUrl.includes('animepahe')) {
781
796
  refererValue = 'https://animepahe.pw/';
782
- const rawCookie = cookie || '';
783
- let sanitized = rawCookie.trim();
784
- sanitized = sanitized.replace(/^cf_clearance/i, '');
785
- sanitized = sanitized.replace(/^[:=]\s*/, '');
786
- sanitized = sanitized.replace(/["']/g, '').trim();
787
- headers['Cookie'] = `cf_clearance=${sanitized}`;
797
+ headers['Cookie'] = `cf_clearance=${sanitizeCfClearance(cookie)}`;
788
798
  }
789
799
  else if (targetUrl.includes('anilist.co')) {
790
800
  refererValue = 'https://anilist.co/';
@@ -800,13 +810,9 @@ export function registerProxy(app) {
800
810
  }
801
811
  else if (targetUrl.includes('weeabo0.xyz')) {
802
812
  refererValue = 'https://japaneseasmr.com/';
803
- const rawCookie = cookie || '';
804
- let sanitized = rawCookie.trim();
805
- sanitized = sanitized.replace(/^cf_clearance/i, '');
806
- sanitized = sanitized.replace(/^[:=]\s*/, '');
807
- sanitized = sanitized.replace(/["']/g, '').trim();
808
- if (sanitized)
809
- headers['Cookie'] = `cf_clearance=${sanitized}`;
813
+ const jasmrCookieHeader = buildCfClearanceCookie(cookie);
814
+ if (jasmrCookieHeader)
815
+ headers['Cookie'] = jasmrCookieHeader;
810
816
  const uaParam = ua || '';
811
817
  if (uaParam)
812
818
  headers['User-Agent'] = uaParam;
@@ -1,38 +1,11 @@
1
1
  import logger from '../logger.js';
2
- import { parseJsonBody } from '../utils/http.utils.js';
3
- import { getTmdbKey, TMDB_BASE, TMDB_IMAGE } from '../lib/tmdb.js';
2
+ import { linkAbort, setProxyHeaders } from '../utils/http.utils.js';
4
3
  import { URL } from 'url';
5
4
  import { isSafeExternalUrl } from '../utils/security.utils.js';
6
- async function resolveTvMeta(mediaType, numericTmdbId, partial) {
7
- const meta = { ...partial };
8
- if (meta.title && meta.imdbId && meta.year)
9
- return meta;
10
- try {
11
- const tmdbKey = await getTmdbKey();
12
- if (tmdbKey) {
13
- const tmdbRes = await fetch(`${TMDB_BASE}/${mediaType}/${numericTmdbId}?api_key=${tmdbKey}&append_to_response=external_ids`, { headers: { 'User-Agent': 'Mozilla/5.0' }, signal: AbortSignal.timeout(4000) });
14
- if (tmdbRes.ok) {
15
- const d = await parseJsonBody(tmdbRes);
16
- if (!meta.title)
17
- meta.title = d.title || d.name || '';
18
- if (!meta.year)
19
- meta.year = (d.release_date || d.first_air_date || '').split('-')[0] || '';
20
- if (!meta.imdbId)
21
- meta.imdbId = d.external_ids?.imdb_id || d.imdb_id || '';
22
- if (mediaType === 'tv' && d.number_of_seasons)
23
- meta.totalSeasons = String(d.number_of_seasons);
24
- }
25
- }
26
- }
27
- catch {
28
- // ignore
29
- }
30
- return meta;
31
- }
32
5
  function proxiedMediaUrl(targetUrl, refererStr) {
33
6
  return `/api/tv/stream-proxy?url=${encodeURIComponent(targetUrl)}&referer=${encodeURIComponent(refererStr)}`;
34
7
  }
35
- export function rewriteTvPlaylist(body, baseUrl, refererStr) {
8
+ function rewriteTvPlaylist(body, baseUrl, refererStr) {
36
9
  return body
37
10
  .split('\n')
38
11
  .map((line) => {
@@ -50,7 +23,7 @@ export function rewriteTvPlaylist(body, baseUrl, refererStr) {
50
23
  })
51
24
  .join('\n');
52
25
  }
53
- export function isPlaylistBody(body) {
26
+ function isPlaylistBody(body) {
54
27
  return (body.length >= 7 &&
55
28
  body
56
29
  .subarray(0, 32)
@@ -59,20 +32,6 @@ export function isPlaylistBody(body) {
59
32
  .trimStart()
60
33
  .startsWith('#EXTM3U'));
61
34
  }
62
- function toSearchResult(item, type) {
63
- return {
64
- id: item.id,
65
- title: item.title || item.name || '',
66
- year: (item.release_date || item.first_air_date || '').split('-')[0],
67
- type,
68
- image: item.poster_path ? `${TMDB_IMAGE}/w500${item.poster_path}` : '',
69
- backdrop: item.backdrop_path ? `${TMDB_IMAGE}/w780${item.backdrop_path}` : '',
70
- overview: item.overview || '',
71
- vote_average: item.vote_average || 0,
72
- adult: item.adult === true,
73
- genre_ids: item.genre_ids || [],
74
- };
75
- }
76
35
  export function registerTv(app, getApiCache, getTvProvider) {
77
36
  app.get('/api/tv/embed/:provider/:type/:tmdbId', async (c) => {
78
37
  const provider = c.req.param('provider');
@@ -117,10 +76,7 @@ export function registerTv(app, getApiCache, getTvProvider) {
117
76
  }
118
77
  const abort = new AbortController();
119
78
  const timeout = setTimeout(() => abort.abort(), 30000);
120
- c.req.raw.signal.addEventListener('abort', () => {
121
- clearTimeout(timeout);
122
- abort.abort();
123
- }, { once: true });
79
+ linkAbort(c.req.raw.signal, abort);
124
80
  try {
125
81
  const headers = {
126
82
  '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',
@@ -137,18 +93,9 @@ export function registerTv(app, getApiCache, getTvProvider) {
137
93
  return c.text('Upstream error', (status || 502));
138
94
  }
139
95
  const contentType = fetchResp.headers.get('content-type') || 'application/octet-stream';
140
- const contentLength = fetchResp.headers.get('content-length');
141
- const contentRange = fetchResp.headers.get('content-range');
142
- const acceptRanges = fetchResp.headers.get('accept-ranges');
143
96
  const outHeaders = new Headers();
144
97
  outHeaders.set('Content-Type', contentType);
145
- if (contentLength)
146
- outHeaders.set('Content-Length', contentLength);
147
- if (contentRange)
148
- outHeaders.set('Content-Range', contentRange);
149
- if (acceptRanges)
150
- outHeaders.set('Accept-Ranges', acceptRanges);
151
- outHeaders.set('Access-Control-Allow-Origin', '*');
98
+ setProxyHeaders(outHeaders, (name) => fetchResp.headers.get(name));
152
99
  outHeaders.set('Connection', 'keep-alive');
153
100
  if (urlStr.includes('.m3u8') || /mpegurl|m3u8/i.test(contentType)) {
154
101
  const body = await fetchResp.text();