@serifpersia/dango 3.6.7 → 3.6.9

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 (156) hide show
  1. package/README.md +1 -1
  2. package/client/dist/assets/{AnimeCard-C3P9zWi9.js → AnimeCard-DKDJK8Mp.js} +1 -1
  3. package/client/dist/assets/{AnimeInfo-Et6687Tl.js → AnimeInfo-CTcvfjva.js} +1 -1
  4. package/client/dist/assets/{AnimeInfoPage-DX4eNnlH.js → AnimeInfoPage-B_UiacIN.js} +2 -2
  5. package/client/dist/assets/{Asmr-CCdEWrFi.js → Asmr-DMSN2vxl.js} +2 -2
  6. package/client/dist/assets/{AsmrResetProgressModal-DgEWBJOw.js → AsmrResetProgressModal-CRwfOatX.js} +1 -1
  7. package/client/dist/assets/{AvSyncCalibrator-CQ2fjf2E.js → AvSyncCalibrator-CRJsi-vz.js} +1 -1
  8. package/client/dist/assets/{Home-DUkUTTTy.js → Home-pwmK9_Eh.js} +1 -1
  9. package/client/dist/assets/{Insights-QCh4xW12.js → Insights-m7kFzg2A.js} +1 -1
  10. package/client/dist/assets/{LibraryListPage-BoTxRqHz.js → LibraryListPage-C1wpPvu6.js} +1 -1
  11. package/client/dist/assets/ListeningList-OzfEI1Ga.js +1 -0
  12. package/client/dist/assets/{Manga-HydR-L7o.js → Manga-ChRCExhK.js} +1 -1
  13. package/client/dist/assets/{MangaInfoPage-CBakEI9a.js → MangaInfoPage-DSHJPo2A.js} +1 -1
  14. package/client/dist/assets/{MangaReadPage-DwAR3iPy.js → MangaReadPage-CzOVY1wA.js} +1 -1
  15. package/client/dist/assets/{MangaResetProgressModal-DGzl8vsO.js → MangaResetProgressModal-CJfzdUBb.js} +1 -1
  16. package/client/dist/assets/{Mature-Pi0TxKuF.js → Mature-CifQMfhs.js} +1 -1
  17. package/client/dist/assets/{MediaCard--cuFxOCS.js → MediaCard-CM10yB9V.js} +1 -1
  18. package/client/dist/assets/{OptionListMenu-B3D39dGS.js → OptionListMenu-6Kovbsqw.js} +1 -1
  19. package/client/dist/assets/{Player-CsH4x_I_.js → Player-CK0hT-u4.js} +2 -2
  20. package/client/dist/assets/{PlayerSettings-DGxBsi3F.js → PlayerSettings-Bb66uqT9.js} +1 -1
  21. package/client/dist/assets/{QueueOptionsButton-Djq4Cpex.js → QueueOptionsButton-C_nTj8_q.js} +1 -1
  22. package/client/dist/assets/{QueueRail-DOrYgNVX.js → QueueRail-DUK7URAE.js} +1 -1
  23. package/client/dist/assets/{ReadingList-DwYoBrNk.js → ReadingList-Ck9LJThU.js} +1 -1
  24. package/client/dist/assets/{ResetProgressModal-CMF86mX6.js → ResetProgressModal-BUphIv0k.js} +1 -1
  25. package/client/dist/assets/{Search-DFkhuk-x.js → Search-zJyP15h6.js} +1 -1
  26. package/client/dist/assets/{Settings-DBB2r0VQ.js → Settings-BYdoSEUh.js} +1 -1
  27. package/client/dist/assets/{SkeletonGrid-DJelJgAv.js → SkeletonGrid-CqgbOhjX.js} +1 -1
  28. package/client/dist/assets/{Trackers-DGYXFhYZ.js → Trackers-BVjyv6CM.js} +1 -1
  29. package/client/dist/assets/{Tv-C5MDGD2Z.js → Tv-L13ylswR.js} +1 -1
  30. package/client/dist/assets/{TvInfo-DrKuK13u.js → TvInfo-Cvcm5U2D.js} +1 -1
  31. package/client/dist/assets/{TvPopup-DtHilRDT.js → TvPopup-DhxfKvYm.js} +1 -1
  32. package/client/dist/assets/{TvSearch-CzNxdu_w.js → TvSearch-P8bFE6_X.js} +1 -1
  33. package/client/dist/assets/{TvWatchlist-D7p4br6S.js → TvWatchlist-D5gVPjtb.js} +1 -1
  34. package/client/dist/assets/{Watchlist-DrOzswl2.js → Watchlist-Cjq8l3rU.js} +1 -1
  35. package/client/dist/assets/{hls-BGCX7mJy.js → hls-BkzuANAg.js} +1 -1
  36. package/client/dist/assets/{index-CnfZwsds.js → index-CLVgzKLk.js} +19 -19
  37. package/client/dist/assets/{useAnimeInfoData-DYXWBPF-.js → useAnimeInfoData-DZJZu-so.js} +1 -1
  38. package/client/dist/assets/{useAsmr-BbhQ59bk.js → useAsmr-CvrMGHuo.js} +1 -1
  39. package/client/dist/assets/useAsmrPopup-Bimy8Ouw.js +1 -0
  40. package/client/dist/assets/{useMangaLibrary-DuesWQaE.js → useMangaLibrary--MiN11uR.js} +1 -1
  41. package/client/dist/assets/{useMangaPopup-D57OVGUr.js → useMangaPopup-CR5XhKKS.js} +1 -1
  42. package/client/dist/assets/{useProviders-mpQb_dix.js → useProviders-DqdT4fA5.js} +1 -1
  43. package/client/dist/assets/{useTv-DGflCsSI.js → useTv-BL0HMDyP.js} +1 -1
  44. package/client/dist/assets/{useTvLibrary-BqeSmyc4.js → useTvLibrary-D4U4iqN8.js} +1 -1
  45. package/client/dist/index.html +1 -1
  46. package/package.json +3 -5
  47. package/server/dist/config.js +1 -174
  48. package/server/dist/server.js +1068 -289
  49. package/server/dist/shipped-defaults.js +1 -6
  50. package/server/package.json +4 -5
  51. package/client/dist/assets/Asmr.module-CdWlioTY.js +0 -1
  52. package/client/dist/assets/ListeningList-DguKjI_t.js +0 -1
  53. package/client/dist/assets/Music-BGH_0voi.css +0 -1
  54. package/client/dist/assets/Music-CQpyXTLH.js +0 -1
  55. package/client/dist/assets/Radio-CCHNFN7R.css +0 -1
  56. package/client/dist/assets/Radio-DF6g6odd.js +0 -1
  57. package/client/dist/assets/Radio.module-DGSZLpKM.js +0 -1
  58. package/client/dist/assets/useAsmrPopup-SymV1J-b.js +0 -1
  59. package/server/dist/app-auth.js +0 -157
  60. package/server/dist/app-hono.js +0 -598
  61. package/server/dist/db/drizzle.js +0 -16
  62. package/server/dist/db/schema-anime.js +0 -61
  63. package/server/dist/db/schema-asmr.js +0 -13
  64. package/server/dist/db/schema-manga.js +0 -31
  65. package/server/dist/db/schema-tv.js +0 -18
  66. package/server/dist/db.js +0 -217
  67. package/server/dist/discord-gateway.js +0 -385
  68. package/server/dist/discord-rpc.js +0 -413
  69. package/server/dist/github-sync.js +0 -508
  70. package/server/dist/google.js +0 -741
  71. package/server/dist/hono/asmr.js +0 -10
  72. package/server/dist/hono/auth.js +0 -49
  73. package/server/dist/hono/music.js +0 -209
  74. package/server/dist/hono/proxy.js +0 -827
  75. package/server/dist/hono/tv.js +0 -200
  76. package/server/dist/hono/watchlist.js +0 -341
  77. package/server/dist/lib/anilist.js +0 -1583
  78. package/server/dist/lib/anischedule.js +0 -191
  79. package/server/dist/lib/insights.js +0 -185
  80. package/server/dist/lib/ipc.js +0 -23
  81. package/server/dist/lib/kitsu.js +0 -517
  82. package/server/dist/lib/mal-import.js +0 -291
  83. package/server/dist/lib/mal.js +0 -957
  84. package/server/dist/lib/migration.js +0 -310
  85. package/server/dist/lib/offline-db.js +0 -395
  86. package/server/dist/lib/potoken.js +0 -135
  87. package/server/dist/lib/recommendations.service.js +0 -216
  88. package/server/dist/lib/temp-ids.js +0 -3
  89. package/server/dist/lib/tmdb.js +0 -72
  90. package/server/dist/lib/tracker/anilist-tracker.js +0 -308
  91. package/server/dist/lib/tracker/manga-sync.service.js +0 -773
  92. package/server/dist/lib/tracker/sync-state.js +0 -21
  93. package/server/dist/lib/tracker/sync.service.js +0 -476
  94. package/server/dist/lib/translate.js +0 -79
  95. package/server/dist/lib/ump.js +0 -106
  96. package/server/dist/lib/ytmusic.js +0 -162
  97. package/server/dist/logger.js +0 -16
  98. package/server/dist/providers/manga/manga.types.js +0 -1
  99. package/server/dist/providers/provider.interface.js +0 -1
  100. package/server/dist/providers/remote-loader.js +0 -614
  101. package/server/dist/providers/remote-types.js +0 -159
  102. package/server/dist/providers/title-matching.js +0 -226
  103. package/server/dist/providers/tv.types.js +0 -1
  104. package/server/dist/rclone.js +0 -120
  105. package/server/dist/repositories/asmr.repository.js +0 -77
  106. package/server/dist/repositories/insights.repository.js +0 -106
  107. package/server/dist/repositories/library.repository.js +0 -94
  108. package/server/dist/repositories/mal-cache.repository.js +0 -52
  109. package/server/dist/repositories/manga.repository.js +0 -95
  110. package/server/dist/repositories/notifications.repository.js +0 -60
  111. package/server/dist/repositories/queue.repository.js +0 -72
  112. package/server/dist/repositories/settings.repository.js +0 -30
  113. package/server/dist/repositories/shows-meta.repository.js +0 -58
  114. package/server/dist/repositories/temp-show-ids.repository.js +0 -63
  115. package/server/dist/repositories/tv.repository.js +0 -93
  116. package/server/dist/repositories/watched-episodes.repository.js +0 -82
  117. package/server/dist/repositories/watchlist.repository.js +0 -60
  118. package/server/dist/scripts/backfill-episode-duration.js +0 -42
  119. package/server/dist/sync-payload.js +0 -91
  120. package/server/dist/sync.js +0 -657
  121. package/server/dist/trpc/context.js +0 -7
  122. package/server/dist/trpc/errors.js +0 -15
  123. package/server/dist/trpc/index.js +0 -18
  124. package/server/dist/trpc/router.js +0 -49
  125. package/server/dist/trpc/routers/asmr-library.js +0 -112
  126. package/server/dist/trpc/routers/asmr-progress.js +0 -121
  127. package/server/dist/trpc/routers/asmr.js +0 -87
  128. package/server/dist/trpc/routers/auth.js +0 -194
  129. package/server/dist/trpc/routers/continue-watching.js +0 -81
  130. package/server/dist/trpc/routers/data.js +0 -1026
  131. package/server/dist/trpc/routers/discord.js +0 -285
  132. package/server/dist/trpc/routers/discovery.js +0 -11
  133. package/server/dist/trpc/routers/insights.js +0 -14
  134. package/server/dist/trpc/routers/manga-library.js +0 -263
  135. package/server/dist/trpc/routers/manga-progress.js +0 -169
  136. package/server/dist/trpc/routers/manga.js +0 -201
  137. package/server/dist/trpc/routers/music.js +0 -655
  138. package/server/dist/trpc/routers/notifications.js +0 -80
  139. package/server/dist/trpc/routers/progress.js +0 -109
  140. package/server/dist/trpc/routers/queue.js +0 -93
  141. package/server/dist/trpc/routers/radio.js +0 -145
  142. package/server/dist/trpc/routers/settings.js +0 -96
  143. package/server/dist/trpc/routers/tracker.js +0 -213
  144. package/server/dist/trpc/routers/tv-library.js +0 -135
  145. package/server/dist/trpc/routers/tv-progress.js +0 -159
  146. package/server/dist/trpc/routers/tv.js +0 -551
  147. package/server/dist/trpc/routers/watchlist.js +0 -550
  148. package/server/dist/trpc/validation.js +0 -258
  149. package/server/dist/utils/cache.utils.js +0 -25
  150. package/server/dist/utils/cookie.utils.js +0 -13
  151. package/server/dist/utils/db-utils.js +0 -17
  152. package/server/dist/utils/env.utils.js +0 -77
  153. package/server/dist/utils/http.utils.js +0 -82
  154. package/server/dist/utils/request-context.js +0 -2
  155. package/server/dist/utils/security.utils.js +0 -76
  156. /package/client/dist/assets/{Asmr-D1KuQwQy.css → useAsmrPopup-D1KuQwQy.css} +0 -0
@@ -1,827 +0,0 @@
1
- import { gotScraping } from 'got-scraping';
2
- import path from 'path';
3
- import { AppCache } from '../utils/cache.utils.js';
4
- import { CONFIG } from '../config.js';
5
- import fs from 'fs';
6
- import logger from '../logger.js';
7
- import { buildCfClearanceCookie, sanitizeCfClearance } from '../utils/cookie.utils.js';
8
- import { isSafeExternalUrl } from '../utils/security.utils.js';
9
- import { fetchWithRetry, linkAbort, setProxyHeaders } from '../utils/http.utils.js';
10
- const proxyCache = new AppCache({ ttlSeconds: 30, maxKeys: 500 });
11
- const imageCache = new AppCache({ ttlSeconds: 86400, maxKeys: 3000 });
12
- function assTimeToVtt(value) {
13
- const m = value.trim().match(/^(-?\d+):(\d{2}):(\d{2})[.:](\d{2,3})$/);
14
- if (!m)
15
- return null;
16
- const h = String(Math.max(0, parseInt(m[1], 10))).padStart(2, '0');
17
- const ms = m[4].length === 2 ? `${m[4]}0` : m[4];
18
- return `${h}:${m[2]}:${m[3]}.${ms}`;
19
- }
20
- function assToVtt(body) {
21
- if (!/^\s*\[Script Info\]/im.test(body))
22
- return null;
23
- const lines = body.replace(/\r\n/g, '\n').split('\n');
24
- let inEvents = false;
25
- let format = null;
26
- const cues = [];
27
- for (const raw of lines) {
28
- const line = raw.trim();
29
- if (/^\[Events\]/i.test(line)) {
30
- inEvents = true;
31
- continue;
32
- }
33
- if (/^\[/.test(line)) {
34
- inEvents = false;
35
- continue;
36
- }
37
- if (!inEvents)
38
- continue;
39
- if (/^Format\s*:/i.test(line)) {
40
- format = line
41
- .replace(/^Format\s*:/i, '')
42
- .split(',')
43
- .map((s) => s.trim().toLowerCase());
44
- continue;
45
- }
46
- if (!/^Dialogue\s*:/i.test(line) || !format)
47
- continue;
48
- const payload = line.replace(/^Dialogue\s*:/i, '');
49
- const parts = [];
50
- let rest = payload;
51
- for (let i = 0; i < format.length - 1; i++) {
52
- const idx = rest.indexOf(',');
53
- if (idx < 0)
54
- break;
55
- parts.push(rest.slice(0, idx));
56
- rest = rest.slice(idx + 1);
57
- }
58
- parts.push(rest);
59
- const start = assTimeToVtt(parts[format.indexOf('start')] ?? '');
60
- const end = assTimeToVtt(parts[format.indexOf('end')] ?? '');
61
- if (!start || !end || format.indexOf('text') < 0)
62
- continue;
63
- const text = (parts[format.indexOf('text')] ?? '')
64
- .replace(/\\\\/g, '\\u0000')
65
- .replace(/\{[^}]*\}/g, '')
66
- .replace(/\\N/gi, '\n')
67
- .replace(/\\n/g, '\n')
68
- .replace(/\\h/gi, ' ')
69
- .replace(/\\[a-zA-Z]+\d*/g, '')
70
- .replace(/\\u0000/g, '\\')
71
- .split('\n')
72
- .map((s) => s.trim())
73
- .filter(Boolean)
74
- .join('\n');
75
- if (!text)
76
- continue;
77
- cues.push(`${start} --> ${end}\n${text}`);
78
- if (cues.length > 5000)
79
- break;
80
- }
81
- if (cues.length === 0)
82
- return null;
83
- return `WEBVTT\n\n${cues.join('\n\n')}\n`;
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
- }
129
- function matchProxyRule(urlStr, rules) {
130
- let host;
131
- try {
132
- host = new URL(urlStr).hostname.toLowerCase();
133
- }
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)
144
- return [urlStr];
145
- let current;
146
- try {
147
- current = new URL(urlStr);
148
- }
149
- catch {
150
- return [urlStr];
151
- }
152
- const seen = new Set([current.hostname.toLowerCase()]);
153
- const urls = [urlStr];
154
- for (const mirror of mirrors) {
155
- const host = mirror.toLowerCase();
156
- if (seen.has(host))
157
- continue;
158
- seen.add(host);
159
- const candidate = new URL(current.href);
160
- candidate.hostname = host;
161
- urls.push(candidate.href);
162
- }
163
- return urls;
164
- }
165
- async function fetchWithMirrors(urlStr, mirrors, signal, attempt, isOk) {
166
- let last = null;
167
- for (const candidate of mirrorUrls(urlStr, mirrors)) {
168
- if (signal.aborted)
169
- break;
170
- const r = await attempt(candidate).catch(() => null);
171
- if (!r)
172
- continue;
173
- if (isOk(r))
174
- return r;
175
- last = r;
176
- }
177
- return last;
178
- }
179
- function pinVariant(body, idx) {
180
- const lines = body.split('\n');
181
- let streamIdx = -1;
182
- let skipUri = false;
183
- const out = [];
184
- for (const line of lines) {
185
- const trimmed = line.trim();
186
- if (trimmed.startsWith('#EXT-X-STREAM-INF')) {
187
- streamIdx++;
188
- skipUri = streamIdx !== idx;
189
- if (!skipUri)
190
- out.push(line);
191
- continue;
192
- }
193
- if (skipUri && trimmed && !trimmed.startsWith('#')) {
194
- skipUri = false;
195
- continue;
196
- }
197
- out.push(line);
198
- }
199
- if (streamIdx < 0 || idx > streamIdx)
200
- return null;
201
- return out.join('\n');
202
- }
203
- function applyEmbedPatches(html, embedUrl, cookieStr = '') {
204
- const safeReferer = JSON.stringify(embedUrl.href).replace(/</g, '\\u003c');
205
- const safeCookie = JSON.stringify(cookieStr).replace(/</g, '\\u003c');
206
- const patched = html.replace(/\b(src|href|url)\s*[=:]\s*(["']?)(\/\/[^"'>)]+|\/(?!\/)[^"'>)]*)\2/gi, (match, attr, quote, path) => {
207
- const full = path.startsWith('//') ? `https:${path}` : `${embedUrl.origin}${path}`;
208
- const prefix = match.startsWith('url') ? `url(` : `${attr}=`;
209
- const suffix = match.startsWith('url') ? `)` : '';
210
- return `${prefix}${quote}${full}${quote}${suffix}`;
211
- });
212
- const iconProxy = `/api/proxy?url=${encodeURIComponent(`${embedUrl.origin}/app/js/vendor/plyr.svg`)}&referer=${encodeURIComponent(embedUrl.href)}`;
213
- const plyrPatch = `<script>if(window.Plyr) Plyr.defaults.iconUrl=${JSON.stringify(iconProxy).replace(/</g, '\\u003c')};</script>`;
214
- const hlsPatch = `<script>
215
- (function() {
216
- var hook = function() {
217
- if (!window.Hls) return;
218
- var original = Hls.prototype.loadSource;
219
- Hls.prototype.loadSource = function(src) {
220
- if (typeof src === 'string' && src.includes('.m3u8')) {
221
- src = window.location.origin + '/api/proxy?url=' + encodeURIComponent(src) + '&referer=' + encodeURIComponent(${safeReferer}) + (${safeCookie} ? '&cookie=' + encodeURIComponent(${safeCookie}) : '');
222
- }
223
- return original.call(this, src);
224
- };
225
- };
226
- if (window.Hls) hook();
227
- else {
228
- var observer = new MutationObserver(function() {
229
- if (window.Hls) { hook(); observer.disconnect(); }
230
- });
231
- observer.observe(document.documentElement, { childList: true, subtree: true });
232
- }
233
- })();
234
- </script>`;
235
- const endBridgePatch = `<script>
236
- (function() {
237
- var notified = false;
238
- var notifyEnded = function() {
239
- if (notified) return;
240
- notified = true;
241
- window.parent.postMessage({ type: 'ANI_WEB_MEDIA_ENDED' }, window.location.origin);
242
- };
243
-
244
- var attachToVideos = function(root) {
245
- var scope = root || document;
246
- var videos = scope.querySelectorAll ? scope.querySelectorAll('video') : [];
247
- Array.prototype.forEach.call(videos, function(video) {
248
- if (video.dataset && video.dataset.aniWebEndedBridge === 'true') return;
249
- if (video.dataset) video.dataset.aniWebEndedBridge = 'true';
250
- video.addEventListener('ended', notifyEnded, { once: true });
251
- });
252
- };
253
-
254
- attachToVideos(document);
255
-
256
- var observer = new MutationObserver(function() {
257
- attachToVideos(document);
258
- });
259
-
260
- observer.observe(document.documentElement, { childList: true, subtree: true });
261
- })();
262
- </script>`;
263
- const beforePlyr = patched.replace(/(<script[^>]+\/plyr\.min\.js[^>]*><\/script>)/i, `$1${plyrPatch}`);
264
- const final = beforePlyr
265
- .replace(/(<script[^>]+hls(?:\.min)?\.js[^>]*><\/script>)/i, `$1${hlsPatch}`)
266
- .replace(/<\/body>/i, `${endBridgePatch}</body>`);
267
- return final === html ? null : final;
268
- }
269
- function placeholderSvg() {
270
- const possiblePaths = [
271
- path.join(CONFIG.PACKAGE_ROOT, 'client/public/placeholder.svg'),
272
- path.join(CONFIG.PACKAGE_ROOT, 'client/dist/placeholder.svg'),
273
- path.join(CONFIG.SERVER_ROOT, '..', 'client/public/placeholder.svg'),
274
- ];
275
- for (const p of possiblePaths) {
276
- try {
277
- if (fs.existsSync(p)) {
278
- const headers = new Headers();
279
- headers.set('Cache-Control', 'no-store');
280
- headers.set('Content-Type', 'image/svg+xml');
281
- return new Response(fs.readFileSync(p), { headers });
282
- }
283
- }
284
- catch {
285
- // ignore
286
- }
287
- }
288
- return null;
289
- }
290
- const PROXY_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0';
291
- export function registerProxy(app, getProxyRules) {
292
- app.get('/api/proxy', async (c) => {
293
- const url = c.req.query('url');
294
- if (!url)
295
- return c.text('URL required', 400);
296
- const safeCheck = isSafeExternalUrl(url);
297
- if (!safeCheck.safe) {
298
- return c.text(safeCheck.error || 'Invalid URL', 400);
299
- }
300
- const urlStr = url;
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') || '';
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;
308
- const variantRaw = c.req.query('variant');
309
- const variantIdx = variantRaw !== undefined ? parseInt(variantRaw, 10) : NaN;
310
- const pinVar = Number.isInteger(variantIdx) && variantIdx >= 0;
311
- const cacheKey = `m3u8-${urlStr}-${refererStr}-${originStr}-${uaStr}${pinVar ? `-v${variantIdx}` : ''}`;
312
- const abort = new AbortController();
313
- linkAbort(c.req.raw.signal, abort);
314
- try {
315
- const headers = {
316
- 'User-Agent': uaStr || PROXY_UA,
317
- };
318
- if (refererStr)
319
- headers['Referer'] = refererStr;
320
- if (originStr)
321
- headers['Origin'] = originStr;
322
- if (cookieStr) {
323
- const cookieHeader = useCf ? buildCfClearanceCookie(cookieStr) : cookieStr;
324
- if (cookieHeader)
325
- headers['Cookie'] = cookieHeader;
326
- }
327
- const range = c.req.header('range');
328
- if (range)
329
- headers['Range'] = range;
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) => {
337
- const baseUrl = new URL(finalUrl);
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);
355
- const isProxied = (value) => value.includes('/api/proxy');
356
- const pinned = pinVar ? pinVariant(playlistBody, variantIdx) : null;
357
- return (pinned || playlistBody)
358
- .split('\n')
359
- .map((line) => {
360
- const trimmed = line.trim();
361
- if (!trimmed)
362
- return line;
363
- if (trimmed.startsWith('#')) {
364
- return trimmed.replace(/URI="([^"]+)"/g, (_, uri) => {
365
- if (isProxied(uri))
366
- return `URI="${uri}"`;
367
- const absolute = new URL(uri, baseUrl).href;
368
- return `URI="${needsProxy || absolute.includes('.m3u8') ? proxiedMediaUrl(absolute) : absolute}"`;
369
- });
370
- }
371
- if (isProxied(trimmed))
372
- return line;
373
- const absolute = new URL(trimmed, baseUrl).href;
374
- return proxiedMediaUrl(absolute);
375
- })
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);
399
- proxyCache.set(cacheKey, rewritten);
400
- return playlistResponse(rewritten);
401
- }
402
- else {
403
- if (useScrape) {
404
- if (range)
405
- headers['Range'] = range;
406
- const resp = await fetchWithMirrors(urlStr, rule?.mirrors, abort.signal, (candidate) => gotScraping({
407
- url: candidate,
408
- method: 'GET',
409
- headers,
410
- responseType: 'buffer',
411
- timeout: { request: 30000 },
412
- followRedirect: true,
413
- throwHttpErrors: false,
414
- }), (attempt) => attempt.statusCode === 200 || attempt.statusCode === 206);
415
- if (!resp || (resp.statusCode !== 200 && resp.statusCode !== 206)) {
416
- return c.text('Upstream error', (resp?.statusCode || 502));
417
- }
418
- const ct = resp.headers['content-type'];
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
- }
429
- const outHeaders = new Headers();
430
- outHeaders.set('Content-Type', isVtt ? 'text/vtt; charset=utf-8' : ct || 'application/octet-stream');
431
- setProxyHeaders(outHeaders, (name) => resp.headers[name]);
432
- return new Response(resp.body, {
433
- status: resp.statusCode,
434
- headers: outHeaders,
435
- });
436
- }
437
- else {
438
- const upstream = await fetchWithMirrors(urlStr, rule?.mirrors, abort.signal, async (candidate) => {
439
- const attempt = await fetchWithRetry(candidate, { method: 'GET', headers }, { retries: 3, timeoutMs: 30000, signal: abort.signal });
440
- if (attempt.status !== 200 && attempt.status !== 206) {
441
- try {
442
- await attempt.body?.cancel();
443
- }
444
- catch {
445
- // ignore
446
- }
447
- }
448
- return attempt;
449
- }, (attempt) => attempt.status === 200 || attempt.status === 206);
450
- if (!upstream) {
451
- return c.text('Upstream error', 502);
452
- }
453
- const status = upstream.status;
454
- if (status !== 200 && status !== 206) {
455
- try {
456
- await upstream.body?.cancel();
457
- }
458
- catch {
459
- // ignore
460
- }
461
- return c.text('Upstream error', status);
462
- }
463
- const isVtt = urlStr.endsWith('.vtt') || urlStr.includes('.vtt?');
464
- const ct = upstream.headers.get('content-type');
465
- const outHeaders = new Headers();
466
- outHeaders.set('Content-Type', isVtt ? 'text/vtt; charset=utf-8' : ct || 'application/octet-stream');
467
- setProxyHeaders(outHeaders, (name) => upstream.headers.get(name));
468
- if (!upstream.body) {
469
- return c.text('Upstream error', 502);
470
- }
471
- return new Response(upstream.body, { status: status, headers: outHeaders });
472
- }
473
- }
474
- }
475
- catch (e) {
476
- if (abort.signal.aborted)
477
- return new Response(null, { status: 499 });
478
- logger.error({ url: urlStr, error: e?.message }, '[proxy] upstream request failed');
479
- return c.text('Proxy error', 500);
480
- }
481
- });
482
- app.get('/api/embed-proxy', async (c) => {
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)
491
- return c.text('Invalid or unsupported gateway URL', 400);
492
- const embedUrl = new URL(rawUrl);
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'));
496
- const abort = new AbortController();
497
- linkAbort(c.req.raw.signal, abort);
498
- try {
499
- const headers = {
500
- 'User-Agent': c.req.query('ua') || PROXY_UA,
501
- };
502
- if (refererStr)
503
- headers['Referer'] = refererStr;
504
- if (originStr)
505
- headers['Origin'] = originStr;
506
- if (cookieStr) {
507
- const cookieHeader = c.req.query('cf') === '1' || rule.cfCookie ? buildCfClearanceCookie(cookieStr) : cookieStr;
508
- if (cookieHeader)
509
- headers['Cookie'] = cookieHeader;
510
- }
511
- const response = await gotScraping({
512
- url: embedUrl.href,
513
- method: 'GET',
514
- headers,
515
- responseType: 'text',
516
- timeout: { request: 30000 },
517
- followRedirect: true,
518
- throwHttpErrors: false,
519
- });
520
- const originalHtml = response.body;
521
- const patched = applyEmbedPatches(originalHtml, embedUrl, cookieStr);
522
- if (!patched)
523
- return c.text('Failed to patch video gateway', 502);
524
- const outHeaders = new Headers();
525
- outHeaders.set('Content-Type', 'text/html; charset=utf-8');
526
- outHeaders.set('Cache-Control', 'private, max-age=120');
527
- return new Response(patched, { status: 200, headers: outHeaders });
528
- }
529
- catch {
530
- if (abort.signal.aborted)
531
- return new Response(null, { status: 499 });
532
- return c.text('Gateway proxy error', 502);
533
- }
534
- });
535
- app.get('/api/subtitle-proxy', async (c) => {
536
- const url = c.req.query('url');
537
- if (!url)
538
- return c.text('URL required', 400);
539
- const safeCheck = isSafeExternalUrl(url);
540
- if (!safeCheck.safe) {
541
- return c.text(safeCheck.error || 'Invalid URL', 400);
542
- }
543
- const abort = new AbortController();
544
- linkAbort(c.req.raw.signal, abort);
545
- const subUrl = url;
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;
550
- const buildHeaders = () => {
551
- const headers = {
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',
553
- Accept: 'text/vtt,text/plain,*/*;q=0.8',
554
- };
555
- if (refererStr)
556
- headers['Referer'] = refererStr;
557
- if (originStr)
558
- headers['Origin'] = originStr;
559
- return headers;
560
- };
561
- const fetchText = async (target, retries = 1) => {
562
- const headers = buildHeaders();
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
- }
591
- }
592
- try {
593
- const upstream = await fetchWithRetry(target, { method: 'GET', headers }, { retries, timeoutMs: 15000, signal: abort.signal });
594
- if (!upstream.ok) {
595
- logger.warn({ target, status: upstream.status }, '[subtitle-proxy] upstream returned error status');
596
- try {
597
- await upstream.body?.cancel();
598
- }
599
- catch {
600
- // ignore
601
- }
602
- return { status: upstream.status, body: '' };
603
- }
604
- const body = (await upstream.text()).replace(/^\uFEFF/, '');
605
- if (/^\s*\{[\s\S]*"error"[\s\S]*\}\s*$/.test(body.slice(0, 500))) {
606
- logger.warn({ target, body: body.slice(0, 200) }, '[subtitle-proxy] upstream JSON error');
607
- return { status: 502, body: '' };
608
- }
609
- return { status: upstream.status, body };
610
- }
611
- catch (e) {
612
- if (abort.signal.aborted)
613
- return null;
614
- logger.warn({ target, error: e?.message }, '[subtitle-proxy] upstream fetch failed');
615
- return { status: 502, body: '' };
616
- }
617
- };
618
- const vttHeaders = (cacheControl) => {
619
- const headers = new Headers();
620
- headers.set('Content-Type', 'text/vtt; charset=utf-8');
621
- headers.set('Access-Control-Allow-Origin', '*');
622
- headers.set('Cache-Control', cacheControl);
623
- return headers;
624
- };
625
- const subtitleError = (text) => {
626
- const headers = new Headers();
627
- headers.set('Access-Control-Allow-Origin', '*');
628
- headers.set('Cache-Control', 'no-store');
629
- return new Response(text, { status: 502, headers });
630
- };
631
- try {
632
- const headers = buildHeaders();
633
- const result = await fetchText(subUrl, 1);
634
- if (abort.signal.aborted)
635
- return new Response(null, { status: 499 });
636
- if (!result || !result.body) {
637
- return subtitleError('Subtitle upstream error');
638
- }
639
- const body = result.body;
640
- const baseForSegments = subUrl;
641
- if (/#EXTM3U/i.test(body.slice(0, 1000))) {
642
- const segUrls = body
643
- .split('\n')
644
- .map((l) => l.trim())
645
- .filter((l) => l && !l.startsWith('#'))
646
- .slice(0, 200)
647
- .map((u) => {
648
- try {
649
- return new URL(u, baseForSegments).href;
650
- }
651
- catch {
652
- return null;
653
- }
654
- })
655
- .filter((u) => Boolean(u));
656
- const parts = [];
657
- for (const seg of segUrls) {
658
- try {
659
- const segRes = await fetchWithRetry(seg, { method: 'GET', headers }, { retries: 1, timeoutMs: 15000, signal: abort.signal });
660
- if (!segRes.ok)
661
- continue;
662
- let segBody = (await segRes.text()).replace(/^\uFEFF/, '');
663
- if (/#EXTM3U/i.test(segBody.slice(0, 200)))
664
- continue;
665
- segBody = segBody
666
- .replace(/\r\n/g, '\n')
667
- .replace(/(\d{2}:\d{2}:\d{2}),(\d{3})/g, '$1.$2');
668
- if (/^\s*WEBVTT/i.test(segBody)) {
669
- segBody = segBody.replace(/^\s*WEBVTT[^\n]*\n(\n|.*\n)?/, '');
670
- }
671
- segBody = segBody.trim();
672
- if (segBody)
673
- parts.push(segBody);
674
- if (parts.join('\n').length > 2_000_000)
675
- break;
676
- }
677
- catch {
678
- if (abort.signal.aborted)
679
- return new Response(null, { status: 499 });
680
- }
681
- }
682
- if (parts.length === 0) {
683
- return subtitleError('Subtitle playlist empty');
684
- }
685
- return new Response(rewrapVttCues(`WEBVTT\n\n${parts.join('\n\n')}\n`), {
686
- headers: vttHeaders('public, max-age=86400'),
687
- });
688
- }
689
- if (/^\s*WEBVTT/i.test(body)) {
690
- return new Response(rewrapVttCues(body), {
691
- headers: vttHeaders('public, max-age=86400'),
692
- });
693
- }
694
- const vttFromAss = assToVtt(body);
695
- if (vttFromAss) {
696
- return new Response(rewrapVttCues(vttFromAss), {
697
- headers: vttHeaders('public, max-age=86400'),
698
- });
699
- }
700
- 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') });
701
- }
702
- catch {
703
- if (abort.signal.aborted)
704
- return new Response(null, { status: 499 });
705
- logger.warn({ url: subUrl }, '[subtitle-proxy] failed');
706
- return subtitleError('Subtitle upstream error');
707
- }
708
- });
709
- app.get('/api/image-proxy', async (c) => {
710
- const url = c.req.query('url');
711
- if (!url)
712
- return c.text('URL required', 400);
713
- const safeCheck = isSafeExternalUrl(url);
714
- if (!safeCheck.safe) {
715
- return c.text(safeCheck.error || 'Invalid URL', 400);
716
- }
717
- const targetUrl = url;
718
- const cookie = c.req.query('cookie') || '';
719
- const ua = c.req.query('ua') || '';
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'));
724
- const abort = new AbortController();
725
- linkAbort(c.req.raw.signal, abort);
726
- const headers = {
727
- 'User-Agent': ua ||
728
- 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
729
- };
730
- const cacheKey = `${targetUrl}|${cookie}|${ua}|${refererValue}|${originValue}`;
731
- const cached = imageCache.get(cacheKey);
732
- if (cached) {
733
- const hit = new Headers();
734
- hit.set('Cache-Control', 'public, max-age=604800, immutable');
735
- hit.set('Content-Type', cached.contentType);
736
- hit.set('Access-Control-Allow-Origin', '*');
737
- hit.set('X-Dango-Cache', 'hit');
738
- return new Response(cached.body, { headers: hit });
739
- }
740
- try {
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;
749
- }
750
- let body = null;
751
- let contentType = 'image/webp';
752
- for (let attempt = 0; attempt < 3 && !body; attempt++) {
753
- const resp = await gotScraping({
754
- url: targetUrl,
755
- method: 'GET',
756
- headers,
757
- responseType: 'buffer',
758
- followRedirect: true,
759
- throwHttpErrors: false,
760
- timeout: { request: 30000 },
761
- });
762
- if (resp.statusCode === 200 && resp.rawBody?.length) {
763
- body = Buffer.from(resp.rawBody);
764
- contentType = String(resp.headers['content-type'] || 'image/webp');
765
- }
766
- else {
767
- if (attempt < 2)
768
- await new Promise((r) => setTimeout(r, 400 * (attempt + 1)));
769
- }
770
- }
771
- if (body) {
772
- imageCache.set(cacheKey, { body, contentType });
773
- const outHeaders = new Headers();
774
- outHeaders.set('Cache-Control', 'public, max-age=604800, immutable');
775
- outHeaders.set('Content-Type', contentType);
776
- outHeaders.set('Access-Control-Allow-Origin', '*');
777
- return new Response(body, { headers: outHeaders });
778
- }
779
- // strict=1 callers retry another candidate instead of settling for the
780
- // placeholder, which would otherwise read as a successful load.
781
- if (strict)
782
- return c.text('Not Found', 404);
783
- const placeholder = placeholderSvg();
784
- if (placeholder)
785
- return placeholder;
786
- return c.text('Not Found', 404);
787
- }
788
- catch {
789
- if (abort.signal.aborted)
790
- return new Response(null, { status: 499 });
791
- if (strict)
792
- return c.text('Not Found', 502);
793
- const placeholder = placeholderSvg();
794
- if (placeholder)
795
- return placeholder;
796
- return c.text('Not Found', 404);
797
- }
798
- });
799
- app.get('/api/resolve', async (c) => {
800
- const targetUrl = c.req.query('url');
801
- if (!targetUrl || typeof targetUrl !== 'string') {
802
- return c.json({ error: 'URL required' }, 400);
803
- }
804
- const safeCheck = isSafeExternalUrl(targetUrl);
805
- if (!safeCheck.safe) {
806
- return c.json({ error: safeCheck.error || 'Invalid URL' }, 400);
807
- }
808
- try {
809
- const response = await fetch(targetUrl, {
810
- method: 'GET',
811
- headers: {
812
- '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',
813
- Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
814
- },
815
- redirect: 'follow',
816
- });
817
- return c.json({
818
- finalUrl: response.url,
819
- status: response.status,
820
- });
821
- }
822
- catch (e) {
823
- const err = e;
824
- return c.json({ error: err.message || 'Resolve failed' }, 500);
825
- }
826
- });
827
- }