@serifpersia/dango 3.6.5 → 3.6.8

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 (151) hide show
  1. package/README.md +1 -1
  2. package/client/dist/assets/{AnimeCard-JkXG40Es.js → AnimeCard-DraA_TF9.js} +1 -1
  3. package/client/dist/assets/{AnimeInfo-2L0UJkbB.js → AnimeInfo-BJ7pzs-P.js} +1 -1
  4. package/client/dist/assets/{AnimeInfoPage-BDs03vKk.js → AnimeInfoPage-B_esUr5C.js} +2 -2
  5. package/client/dist/assets/{Asmr-D2mVKEl9.js → Asmr-Ckuh2xfW.js} +1 -1
  6. package/client/dist/assets/{AsmrResetProgressModal-MQA_bNQM.js → AsmrResetProgressModal-BS6UFJN4.js} +1 -1
  7. package/client/dist/assets/AvSyncCalibrator-DwHCd9yB.js +10 -0
  8. package/client/dist/assets/{Home-DxMBHFmc.js → Home-DuM1EuvN.js} +1 -1
  9. package/client/dist/assets/{Insights-hmLWsSXY.js → Insights-D3Pd9c9V.js} +1 -1
  10. package/client/dist/assets/{LibraryListPage-CWI7hgUh.js → LibraryListPage-Dm2yVn0-.js} +1 -1
  11. package/client/dist/assets/{ListeningList-NOc28KAV.js → ListeningList-D-zt6EXZ.js} +1 -1
  12. package/client/dist/assets/{Manga-B5qNhI7v.js → Manga-JwprMKBk.js} +1 -1
  13. package/client/dist/assets/{MangaInfoPage-D4wIyb-V.js → MangaInfoPage-DaeyJl2p.js} +1 -1
  14. package/client/dist/assets/{MangaReadPage-Ce1VX7-0.js → MangaReadPage-COg0fHI9.js} +1 -1
  15. package/client/dist/assets/{MangaResetProgressModal-P3oY4_14.js → MangaResetProgressModal-kguXv0_M.js} +1 -1
  16. package/client/dist/assets/{Mature-D8EwGYQq.js → Mature-IHDaasrs.js} +1 -1
  17. package/client/dist/assets/{MediaCard-Dp8DjyXB.js → MediaCard-CpwsrXo5.js} +1 -1
  18. package/client/dist/assets/{Music-DULtVsAM.js → Music-Dks4tPeK.js} +1 -1
  19. package/client/dist/assets/{OptionListMenu-fIHdE-nq.js → OptionListMenu-11CcAsy8.js} +1 -1
  20. package/client/dist/assets/{Player-B8XeOmso.js → Player-DQoEQkHL.js} +6 -6
  21. package/client/dist/assets/{PlayerSettings-BycNCiCs.js → PlayerSettings-D4Ud4kv4.js} +1 -1
  22. package/client/dist/assets/{QueueOptionsButton-T1J37IZT.js → QueueOptionsButton-B8PlPKjC.js} +1 -1
  23. package/client/dist/assets/{QueueRail-BAwI-apz.js → QueueRail-B4Dok7U3.js} +1 -1
  24. package/client/dist/assets/{Radio-Bv9DyKtZ.js → Radio-BHK6LSk4.js} +1 -1
  25. package/client/dist/assets/{ReadingList-Cl0CcVp3.js → ReadingList-BCsxIz57.js} +1 -1
  26. package/client/dist/assets/{ResetProgressModal--N60WWCB.js → ResetProgressModal-BXHxSfy-.js} +1 -1
  27. package/client/dist/assets/{Search-DvgVDlUR.js → Search-PK1ds07O.js} +1 -1
  28. package/client/dist/assets/{Settings-dCz_-bJn.js → Settings-Bxk_x55l.js} +1 -1
  29. package/client/dist/assets/{SkeletonGrid-BZHKk1I6.js → SkeletonGrid-CpJCZSRN.js} +1 -1
  30. package/client/dist/assets/{Trackers-C1T5VcPu.js → Trackers-RieCh_84.js} +1 -1
  31. package/client/dist/assets/{Tv-Dhv_cg4y.js → Tv-DLytPJUj.js} +4 -4
  32. package/client/dist/assets/{TvInfo-DgaLK2H6.js → TvInfo-C8djKebR.js} +1 -1
  33. package/client/dist/assets/{TvPopup-DyX7f8kC.js → TvPopup-40JGGJDk.js} +1 -1
  34. package/client/dist/assets/{TvSearch-Bp0H6kue.js → TvSearch-A3hbospf.js} +1 -1
  35. package/client/dist/assets/{TvWatchlist-ohMtMeKa.js → TvWatchlist-DuXjqmZM.js} +1 -1
  36. package/client/dist/assets/{Watchlist-BbQtzsER.js → Watchlist-B9wGBh0u.js} +1 -1
  37. package/client/dist/assets/{hls-Bi1cjpLZ.js → hls-CZ5X3Ya-.js} +1 -1
  38. package/client/dist/assets/{index-BTnhzofk.js → index-dRhc9-ol.js} +2 -2
  39. package/client/dist/assets/{useAnimeInfoData-Cvmso3IK.js → useAnimeInfoData-ZzDtLfvD.js} +1 -1
  40. package/client/dist/assets/{useAsmr-CbzGCzmv.js → useAsmr-CKOdHgol.js} +1 -1
  41. package/client/dist/assets/{useAsmrPopup-DaREZ9Ie.js → useAsmrPopup-SFHPhpcl.js} +1 -1
  42. package/client/dist/assets/{useMangaLibrary-BVQRhsFh.js → useMangaLibrary-CKATe31P.js} +1 -1
  43. package/client/dist/assets/{useMangaPopup-CzsCbndG.js → useMangaPopup-C75QuYgY.js} +1 -1
  44. package/client/dist/assets/{useProviders-UuUPzft-.js → useProviders-CMtvZbEi.js} +1 -1
  45. package/client/dist/assets/{useTv-CSsbWKH9.js → useTv-DUX4zYMr.js} +1 -1
  46. package/client/dist/assets/{useTvLibrary-a_Y0c2FS.js → useTvLibrary-By4Gt-R_.js} +1 -1
  47. package/client/dist/index.html +1 -1
  48. package/package.json +2 -2
  49. package/server/dist/app-auth.js +1 -157
  50. package/server/dist/app-hono.js +7 -557
  51. package/server/dist/config.js +1 -174
  52. package/server/dist/db/drizzle.js +1 -16
  53. package/server/dist/db/schema-anime.js +1 -61
  54. package/server/dist/db/schema-asmr.js +1 -13
  55. package/server/dist/db/schema-manga.js +1 -31
  56. package/server/dist/db/schema-tv.js +1 -18
  57. package/server/dist/db.js +1 -217
  58. package/server/dist/discord-gateway.js +1 -385
  59. package/server/dist/discord-rpc.js +1 -413
  60. package/server/dist/github-sync.js +1 -508
  61. package/server/dist/google.js +1 -741
  62. package/server/dist/hono/asmr.js +1 -10
  63. package/server/dist/hono/auth.js +3 -26
  64. package/server/dist/hono/music.js +1 -209
  65. package/server/dist/hono/proxy.js +36 -905
  66. package/server/dist/hono/tv.js +5 -169
  67. package/server/dist/hono/watchlist.js +2 -338
  68. package/server/dist/lib/anilist.js +26 -1458
  69. package/server/dist/lib/anischedule.js +1 -191
  70. package/server/dist/lib/insights.js +1 -185
  71. package/server/dist/lib/ipc.js +1 -23
  72. package/server/dist/lib/kitsu.js +1 -517
  73. package/server/dist/lib/mal-import.js +1 -291
  74. package/server/dist/lib/mal.js +1 -957
  75. package/server/dist/lib/migration.js +1 -310
  76. package/server/dist/lib/offline-db.js +4 -384
  77. package/server/dist/lib/potoken.js +1 -135
  78. package/server/dist/lib/recommendations.service.js +1 -216
  79. package/server/dist/lib/temp-ids.js +1 -3
  80. package/server/dist/lib/tmdb.js +1 -72
  81. package/server/dist/lib/tracker/anilist-tracker.js +8 -245
  82. package/server/dist/lib/tracker/manga-sync.service.js +1 -773
  83. package/server/dist/lib/tracker/sync-state.js +1 -21
  84. package/server/dist/lib/tracker/sync.service.js +1 -476
  85. package/server/dist/lib/translate.js +3 -79
  86. package/server/dist/lib/ump.js +1 -106
  87. package/server/dist/lib/ytmusic.js +1 -162
  88. package/server/dist/logger.js +1 -16
  89. package/server/dist/providers/manga/manga.types.js +0 -1
  90. package/server/dist/providers/provider.interface.js +0 -1
  91. package/server/dist/providers/remote-loader.js +2 -605
  92. package/server/dist/providers/remote-types.js +1 -124
  93. package/server/dist/providers/title-matching.js +1 -226
  94. package/server/dist/providers/tv.types.js +0 -1
  95. package/server/dist/rclone.js +2 -120
  96. package/server/dist/repositories/asmr.repository.js +11 -46
  97. package/server/dist/repositories/insights.repository.js +11 -25
  98. package/server/dist/repositories/library.repository.js +3 -94
  99. package/server/dist/repositories/mal-cache.repository.js +1 -52
  100. package/server/dist/repositories/manga.repository.js +14 -58
  101. package/server/dist/repositories/notifications.repository.js +6 -52
  102. package/server/dist/repositories/queue.repository.js +5 -58
  103. package/server/dist/repositories/settings.repository.js +2 -29
  104. package/server/dist/repositories/shows-meta.repository.js +3 -56
  105. package/server/dist/repositories/temp-show-ids.repository.js +3 -63
  106. package/server/dist/repositories/tv.repository.js +11 -54
  107. package/server/dist/repositories/watched-episodes.repository.js +6 -55
  108. package/server/dist/repositories/watchlist.repository.js +3 -54
  109. package/server/dist/scripts/backfill-episode-duration.js +1 -42
  110. package/server/dist/server.js +1 -307
  111. package/server/dist/shipped-defaults.js +1 -6
  112. package/server/dist/sync-payload.js +1 -91
  113. package/server/dist/sync.js +3 -647
  114. package/server/dist/trpc/context.js +1 -7
  115. package/server/dist/trpc/errors.js +1 -15
  116. package/server/dist/trpc/index.js +1 -18
  117. package/server/dist/trpc/router.js +1 -49
  118. package/server/dist/trpc/routers/asmr-library.js +1 -112
  119. package/server/dist/trpc/routers/asmr-progress.js +1 -121
  120. package/server/dist/trpc/routers/asmr.js +1 -87
  121. package/server/dist/trpc/routers/auth.js +1 -194
  122. package/server/dist/trpc/routers/continue-watching.js +2 -58
  123. package/server/dist/trpc/routers/data.js +1 -1026
  124. package/server/dist/trpc/routers/discord.js +1 -285
  125. package/server/dist/trpc/routers/discovery.js +1 -11
  126. package/server/dist/trpc/routers/insights.js +1 -14
  127. package/server/dist/trpc/routers/manga-library.js +1 -263
  128. package/server/dist/trpc/routers/manga-progress.js +1 -169
  129. package/server/dist/trpc/routers/manga.js +1 -201
  130. package/server/dist/trpc/routers/music.js +1 -655
  131. package/server/dist/trpc/routers/notifications.js +1 -80
  132. package/server/dist/trpc/routers/progress.js +1 -109
  133. package/server/dist/trpc/routers/queue.js +1 -93
  134. package/server/dist/trpc/routers/radio.js +1 -145
  135. package/server/dist/trpc/routers/settings.js +1 -96
  136. package/server/dist/trpc/routers/tracker.js +1 -213
  137. package/server/dist/trpc/routers/tv-library.js +1 -135
  138. package/server/dist/trpc/routers/tv-progress.js +1 -159
  139. package/server/dist/trpc/routers/tv.js +1 -551
  140. package/server/dist/trpc/routers/watchlist.js +2 -549
  141. package/server/dist/trpc/validation.js +1 -258
  142. package/server/dist/utils/cache.utils.js +1 -25
  143. package/server/dist/utils/cookie.utils.js +1 -13
  144. package/server/dist/utils/db-utils.js +5 -17
  145. package/server/dist/utils/env.utils.js +5 -77
  146. package/server/dist/utils/http.utils.js +1 -82
  147. package/server/dist/utils/request-context.js +1 -2
  148. package/server/dist/utils/security.utils.js +1 -76
  149. package/server/package.json +3 -2
  150. package/client/dist/assets/AvSyncCalibrator-DmoH5eRA.js +0 -10
  151. package/server/dist/utils/megaplay.utils.js +0 -40
@@ -1,295 +1,30 @@
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
- import { MEGAPLAY_ORIGIN, isNexabloomMasterUrl, signNexabloomMasterUrl, } from '../utils/megaplay.utils.js';
11
- const proxyCache = new AppCache({ ttlSeconds: 30, maxKeys: 500 });
12
- const imageCache = new AppCache({ ttlSeconds: 86400, maxKeys: 3000 });
13
- function assTimeToVtt(value) {
14
- const m = value.trim().match(/^(-?\d+):(\d{2}):(\d{2})[.:](\d{2,3})$/);
15
- if (!m)
16
- return null;
17
- const h = String(Math.max(0, parseInt(m[1], 10))).padStart(2, '0');
18
- const ms = m[4].length === 2 ? `${m[4]}0` : m[4];
19
- return `${h}:${m[2]}:${m[3]}.${ms}`;
20
- }
21
- function assToVtt(body) {
22
- if (!/^\s*\[Script Info\]/im.test(body))
23
- return null;
24
- const lines = body.replace(/\r\n/g, '\n').split('\n');
25
- let inEvents = false;
26
- let format = null;
27
- const cues = [];
28
- for (const raw of lines) {
29
- const line = raw.trim();
30
- if (/^\[Events\]/i.test(line)) {
31
- inEvents = true;
32
- continue;
33
- }
34
- if (/^\[/.test(line)) {
35
- inEvents = false;
36
- continue;
37
- }
38
- if (!inEvents)
39
- continue;
40
- if (/^Format\s*:/i.test(line)) {
41
- format = line
42
- .replace(/^Format\s*:/i, '')
43
- .split(',')
44
- .map((s) => s.trim().toLowerCase());
45
- continue;
46
- }
47
- if (!/^Dialogue\s*:/i.test(line) || !format)
48
- continue;
49
- const payload = line.replace(/^Dialogue\s*:/i, '');
50
- const parts = [];
51
- let rest = payload;
52
- for (let i = 0; i < format.length - 1; i++) {
53
- const idx = rest.indexOf(',');
54
- if (idx < 0)
55
- break;
56
- parts.push(rest.slice(0, idx));
57
- rest = rest.slice(idx + 1);
58
- }
59
- parts.push(rest);
60
- const start = assTimeToVtt(parts[format.indexOf('start')] ?? '');
61
- const end = assTimeToVtt(parts[format.indexOf('end')] ?? '');
62
- if (!start || !end || format.indexOf('text') < 0)
63
- continue;
64
- const text = (parts[format.indexOf('text')] ?? '')
65
- .replace(/\\\\/g, '\\u0000')
66
- .replace(/\{[^}]*\}/g, '')
67
- .replace(/\\N/gi, '\n')
68
- .replace(/\\n/g, '\n')
69
- .replace(/\\h/gi, ' ')
70
- .replace(/\\[a-zA-Z]+\d*/g, '')
71
- .replace(/\\u0000/g, '\\')
72
- .split('\n')
73
- .map((s) => s.trim())
74
- .filter(Boolean)
75
- .join('\n');
76
- if (!text)
77
- continue;
78
- cues.push(`${start} --> ${end}\n${text}`);
79
- if (cues.length > 5000)
80
- break;
81
- }
82
- if (cues.length === 0)
83
- return null;
84
- return `WEBVTT\n\n${cues.join('\n\n')}\n`;
85
- }
86
- const SUB_WRAP_AT = 50;
87
- function splitBalancedCueLine(line) {
88
- const words = line.split(/\s+/).filter((w) => w.length > 0);
89
- if (words.length < 2)
90
- return [line];
91
- const total = words.join(' ').length;
92
- if (total <= SUB_WRAP_AT)
93
- return [line];
94
- const target = total / 2;
95
- let acc = words[0]?.length ?? 0;
96
- let best = 1;
97
- let bestDiff = Math.abs(acc - target);
98
- for (let i = 1; i < words.length; i++) {
99
- acc += 1 + (words[i]?.length ?? 0);
100
- const diff = Math.abs(acc - target);
101
- if (diff < bestDiff) {
102
- bestDiff = diff;
103
- best = i + 1;
104
- }
105
- }
106
- if (best <= 0 || best >= words.length)
107
- return [line];
108
- return [words.slice(0, best).join(' '), words.slice(best).join(' ')];
109
- }
110
- function rewrapVttCues(vtt) {
111
- const blocks = vtt.replace(/\r\n/g, '\n').split(/\n\n+/);
112
- return blocks
113
- .map((block, idx) => {
114
- if (idx === 0 && /^\s*WEBVTT/i.test(block))
115
- return block;
116
- const lines = block.split('\n');
117
- const tIdx = lines.findIndex((l) => l.includes('-->'));
118
- if (tIdx < 0)
119
- return block;
120
- const payload = lines.slice(tIdx + 1).filter((l) => l.trim() !== '');
121
- if (payload.length !== 1)
122
- return block;
123
- const split = splitBalancedCueLine((payload[0] ?? '').trim());
124
- if (split.length < 2)
125
- return block;
126
- return [...lines.slice(0, tIdx + 1), ...split].join('\n');
127
- })
128
- .join('\n\n');
129
- }
130
- const KWIK_DOMAINS = new Set(['kwik.cx', 'kwik.si', 'kwik.pro']);
131
- const ANIMEPAHE_URL = 'https://animepahe.pw/';
132
- const VAULT_CDN_HOSTS = new Set(['uwucdn.top', 'owocdn.top']);
133
- const GOT_SCRAPING_HOSTS = new Set([
134
- 'kwik.cx',
135
- 'kwik.si',
136
- 'kwik.pro',
137
- 'animepahe.pw',
138
- 'animepahe.ru',
139
- ]);
140
- const GOT_SCRAPING_SUFFIXES = [
141
- '.uwucdn.top',
142
- '.owocdn.top',
143
- '.streamzone1.site',
144
- '.nekostream.site',
145
- '.nukitashith.top',
146
- '.aniwatchtv.site',
147
- '.settlar.io',
148
- ];
149
- const HANIME_HOSTS = new Set(['r2.1hanime.com', '1.1hanime.com']);
150
- const OPPAI_HOSTS = new Set(['myspacecat.pictures']);
151
- const KAA_HOSTS = new Set([
152
- 'hls.krussdomi.com',
153
- 'subst.krussdomi.com',
154
- 'krussdomi.com',
155
- 'st1.advancedairesearchlab.xyz',
156
- 'st1.habibikun.xyz',
157
- 'st1.babybayw.xyz',
158
- 'st1.narutokun.xyz',
159
- ]);
160
- const KAA_REFERER = 'https://krussdomi.com/';
161
- const KAA_ORIGIN = 'https://krussdomi.com';
162
- const ANILIGHT_REFERER = 'https://anilight.live/';
163
- const ANILIGHT_ORIGIN = 'https://anilight.live';
164
- const ANILIGHT_HOSTS = [
165
- 'anilight.live',
166
- 'api.anilight.live',
167
- 'lostproject.club',
168
- 'streamzone1.site',
169
- 'nukitashi.top',
170
- 'vivibebe.site',
171
- 'vibeplayer.site',
172
- 'aniwatchtv.site',
173
- ];
174
- const NEXABLOOM_FAMILY_SUFFIXES = [
175
- '.quavex.top',
176
- '.nexabloom.top',
177
- '.hiddenvertex.top',
178
- '.ironhorizon.top',
179
- '.solarhaven.top',
180
- ];
181
- const NEXABLOOM_FALLBACK_HOSTS = ['fetch.nexabloom.top'];
182
- function nexabloomFallbackUrls(urlStr) {
183
- let host = '';
184
- try {
185
- host = new URL(urlStr).hostname.toLowerCase();
186
- }
187
- catch {
188
- return [urlStr];
189
- }
190
- if (!NEXABLOOM_FAMILY_SUFFIXES.some((s) => host.endsWith(s))) {
191
- return [urlStr];
192
- }
193
- const urls = [urlStr];
194
- for (const fallback of NEXABLOOM_FALLBACK_HOSTS) {
195
- if (fallback === host)
196
- continue;
197
- try {
198
- const candidate = new URL(urlStr);
199
- candidate.hostname = fallback;
200
- urls.push(candidate.href);
201
- }
202
- catch {
203
- // ignore
204
- }
205
- }
206
- return urls;
207
- }
208
- async function fetchNexabloomFallback(urlStr, signal, attempt, isOk) {
209
- let last = null;
210
- for (const candidate of nexabloomFallbackUrls(urlStr)) {
211
- if (signal.aborted)
212
- break;
213
- const r = await attempt(candidate).catch(() => null);
214
- if (!r)
215
- continue;
216
- if (isOk(r))
217
- return r;
218
- last = r;
219
- }
220
- return last;
221
- }
222
- function isGotScrapingHost(urlStr) {
223
- try {
224
- const host = new URL(urlStr).hostname.toLowerCase();
225
- if (GOT_SCRAPING_HOSTS.has(host))
226
- return true;
227
- if (HANIME_HOSTS.has(host))
228
- return true;
229
- return GOT_SCRAPING_SUFFIXES.some((s) => host.endsWith(s));
230
- }
231
- catch {
232
- return false;
233
- }
234
- }
235
- function validateKwikUrl(value) {
236
- if (typeof value !== 'string')
237
- return null;
238
- try {
239
- const url = new URL(value);
240
- const isSecure = url.protocol === 'https:';
241
- const isKwik = KWIK_DOMAINS.has(url.hostname.toLowerCase());
242
- const isEmbedPath = /^\/e\/[A-Za-z0-9_-]+$/.test(url.pathname);
243
- const noAuthOrQuery = !url.username && !url.password && !url.search && !url.hash;
244
- return isSecure && isKwik && isEmbedPath && noAuthOrQuery ? url : null;
245
- }
246
- catch {
247
- return null;
248
- }
249
- }
250
- function pinVariant(body, idx) {
251
- const lines = body.split('\n');
252
- let streamIdx = -1;
253
- let skipUri = false;
254
- const out = [];
255
- for (const line of lines) {
256
- const trimmed = line.trim();
257
- if (trimmed.startsWith('#EXT-X-STREAM-INF')) {
258
- streamIdx++;
259
- skipUri = streamIdx !== idx;
260
- if (!skipUri)
261
- out.push(line);
262
- continue;
263
- }
264
- if (skipUri && trimmed && !trimmed.startsWith('#')) {
265
- skipUri = false;
266
- continue;
267
- }
268
- out.push(line);
269
- }
270
- if (streamIdx < 0 || idx > streamIdx)
271
- return null;
272
- return out.join('\n');
273
- }
274
- function applyKwikPatches(html, kwikUrl, cookieStr = '') {
275
- const safeReferer = JSON.stringify(kwikUrl.href).replace(/</g, '\\u003c');
276
- const safeCookie = JSON.stringify(cookieStr).replace(/</g, '\\u003c');
277
- const patched = html.replace(/\b(src|href|url)\s*[=:]\s*(["']?)(\/\/[^"'>)]+|\/(?!\/)[^"'>)]*)\2/gi, (match, attr, quote, path) => {
278
- const full = path.startsWith('//') ? `https:${path}` : `${kwikUrl.origin}${path}`;
279
- const prefix = match.startsWith('url') ? `url(` : `${attr}=`;
280
- const suffix = match.startsWith('url') ? `)` : '';
281
- return `${prefix}${quote}${full}${quote}${suffix}`;
282
- });
283
- const iconProxy = `/api/proxy?url=${encodeURIComponent(`${kwikUrl.origin}/app/js/vendor/plyr.svg`)}&referer=${encodeURIComponent(kwikUrl.href)}`;
284
- const plyrPatch = `<script>if(window.Plyr) Plyr.defaults.iconUrl=${JSON.stringify(iconProxy).replace(/</g, '\\u003c')};</script>`;
285
- const hlsPatch = `<script>
1
+ import{gotScraping as H}from"got-scraping";import j from"path";import{AppCache as _}from"../utils/cache.utils.js";import{CONFIG as N}from"../config.js";import D from"fs";import A from"../logger.js";import{buildCfClearanceCookie as V,sanitizeCfClearance as ee}from"../utils/cookie.utils.js";import{isSafeExternalUrl as W}from"../utils/security.utils.js";import{fetchWithRetry as G,linkAbort as L,setProxyHeaders as X}from"../utils/http.utils.js";const O=new _({ttlSeconds:30,maxKeys:500}),z=new _({ttlSeconds:86400,maxKeys:3e3});function J(c){const o=c.trim().match(/^(-?\d+):(\d{2}):(\d{2})[.:](\d{2,3})$/);if(!o)return null;const e=String(Math.max(0,parseInt(o[1],10))).padStart(2,"0"),r=o[4].length===2?`${o[4]}0`:o[4];return`${e}:${o[2]}:${o[3]}.${r}`}function te(c){if(!/^\s*\[Script Info\]/im.test(c))return null;const o=c.replace(/\r\n/g,`
2
+ `).split(`
3
+ `);let e=!1,r=null;const i=[];for(const t of o){const n=t.trim();if(/^\[Events\]/i.test(n)){e=!0;continue}if(/^\[/.test(n)){e=!1;continue}if(!e)continue;if(/^Format\s*:/i.test(n)){r=n.replace(/^Format\s*:/i,"").split(",").map(h=>h.trim().toLowerCase());continue}if(!/^Dialogue\s*:/i.test(n)||!r)continue;const s=n.replace(/^Dialogue\s*:/i,""),d=[];let m=s;for(let h=0;h<r.length-1;h++){const b=m.indexOf(",");if(b<0)break;d.push(m.slice(0,b)),m=m.slice(b+1)}d.push(m);const R=J(d[r.indexOf("start")]??""),w=J(d[r.indexOf("end")]??"");if(!R||!w||r.indexOf("text")<0)continue;const T=(d[r.indexOf("text")]??"").replace(/\\\\/g,"\\u0000").replace(/\{[^}]*\}/g,"").replace(/\\N/gi,`
4
+ `).replace(/\\n/g,`
5
+ `).replace(/\\h/gi," ").replace(/\\[a-zA-Z]+\d*/g,"").replace(/\\u0000/g,"\\").split(`
6
+ `).map(h=>h.trim()).filter(Boolean).join(`
7
+ `);if(T&&(i.push(`${R} --> ${w}
8
+ ${T}`),i.length>5e3))break}return i.length===0?null:`WEBVTT
9
+
10
+ ${i.join(`
11
+
12
+ `)}
13
+ `}const re=50;function ne(c){const o=c.split(/\s+/).filter(s=>s.length>0);if(o.length<2)return[c];const e=o.join(" ").length;if(e<=re)return[c];const r=e/2;let i=o[0]?.length??0,t=1,n=Math.abs(i-r);for(let s=1;s<o.length;s++){i+=1+(o[s]?.length??0);const d=Math.abs(i-r);d<n&&(n=d,t=s+1)}return t<=0||t>=o.length?[c]:[o.slice(0,t).join(" "),o.slice(t).join(" ")]}function P(c){return c.replace(/\r\n/g,`
14
+ `).split(/\n\n+/).map((e,r)=>{if(r===0&&/^\s*WEBVTT/i.test(e))return e;const i=e.split(`
15
+ `),t=i.findIndex(d=>d.includes("-->"));if(t<0)return e;const n=i.slice(t+1).filter(d=>d.trim()!=="");if(n.length!==1)return e;const s=ne((n[0]??"").trim());return s.length<2?e:[...i.slice(0,t+1),...s].join(`
16
+ `)}).join(`
17
+
18
+ `)}function I(c,o){let e;try{e=new URL(c).hostname.toLowerCase()}catch{return}return o.find(r=>r.hosts?.some(i=>i===e)||r.suffixes?.some(i=>e.endsWith(i)))}function B(c,o,e){return e||(c?.originFromReferer?o:"")||c?.origin||""}function se(c,o){if(!o?.length)return[c];let e;try{e=new URL(c)}catch{return[c]}const r=new Set([e.hostname.toLowerCase()]),i=[c];for(const t of o){const n=t.toLowerCase();if(r.has(n))continue;r.add(n);const s=new URL(e.href);s.hostname=n,i.push(s.href)}return i}async function K(c,o,e,r,i){let t=null;for(const n of se(c,o)){if(e.aborted)break;const s=await r(n).catch(()=>null);if(s){if(i(s))return s;t=s}}return t}function oe(c,o){const e=c.split(`
19
+ `);let r=-1,i=!1;const t=[];for(const n of e){const s=n.trim();if(s.startsWith("#EXT-X-STREAM-INF")){r++,i=r!==o,i||t.push(n);continue}if(i&&s&&!s.startsWith("#")){i=!1;continue}t.push(n)}return r<0||o>r?null:t.join(`
20
+ `)}function ie(c,o,e=""){const r=JSON.stringify(o.href).replace(/</g,"\\u003c"),i=JSON.stringify(e).replace(/</g,"\\u003c"),t=c.replace(/\b(src|href|url)\s*[=:]\s*(["']?)(\/\/[^"'>)]+|\/(?!\/)[^"'>)]*)\2/gi,(T,h,b,l)=>{const u=l.startsWith("//")?`https:${l}`:`${o.origin}${l}`,y=T.startsWith("url")?"url(":`${h}=`,f=T.startsWith("url")?")":"";return`${y}${b}${u}${b}${f}`}),n=`/api/proxy?url=${encodeURIComponent(`${o.origin}/app/js/vendor/plyr.svg`)}&referer=${encodeURIComponent(o.href)}`,s=`<script>if(window.Plyr) Plyr.defaults.iconUrl=${JSON.stringify(n).replace(/</g,"\\u003c")};</script>`,d=`<script>
286
21
  (function() {
287
22
  var hook = function() {
288
23
  if (!window.Hls) return;
289
24
  var original = Hls.prototype.loadSource;
290
25
  Hls.prototype.loadSource = function(src) {
291
26
  if (typeof src === 'string' && src.includes('.m3u8')) {
292
- src = window.location.origin + '/api/proxy?url=' + encodeURIComponent(src) + '&referer=' + encodeURIComponent(${safeReferer}) + (${safeCookie} ? '&cookie=' + encodeURIComponent(${safeCookie}) : '');
27
+ src = window.location.origin + '/api/proxy?url=' + encodeURIComponent(src) + '&referer=' + encodeURIComponent(${r}) + (${i} ? '&cookie=' + encodeURIComponent(${i}) : '');
293
28
  }
294
29
  return original.call(this, src);
295
30
  };
@@ -302,8 +37,7 @@ function applyKwikPatches(html, kwikUrl, cookieStr = '') {
302
37
  observer.observe(document.documentElement, { childList: true, subtree: true });
303
38
  }
304
39
  })();
305
- </script>`;
306
- const endBridgePatch = `<script>
40
+ </script>`,w=t.replace(/(<script[^>]+\/plyr\.min\.js[^>]*><\/script>)/i,`$1${s}`).replace(/(<script[^>]+hls(?:\.min)?\.js[^>]*><\/script>)/i,`$1${d}`).replace(/<\/body>/i,`<script>
307
41
  (function() {
308
42
  var notified = false;
309
43
  var notifyEnded = function() {
@@ -330,620 +64,17 @@ function applyKwikPatches(html, kwikUrl, cookieStr = '') {
330
64
 
331
65
  observer.observe(document.documentElement, { childList: true, subtree: true });
332
66
  })();
333
- </script>`;
334
- const beforePlyr = patched.replace(/(<script[^>]+\/plyr\.min\.js[^>]*><\/script>)/i, `$1${plyrPatch}`);
335
- const final = beforePlyr
336
- .replace(/(<script[^>]+hls(?:\.min)?\.js[^>]*><\/script>)/i, `$1${hlsPatch}`)
337
- .replace(/<\/body>/i, `${endBridgePatch}</body>`);
338
- return final === html ? null : final;
339
- }
340
- function placeholderSvg() {
341
- const possiblePaths = [
342
- path.join(CONFIG.PACKAGE_ROOT, 'client/public/placeholder.svg'),
343
- path.join(CONFIG.PACKAGE_ROOT, 'client/dist/placeholder.svg'),
344
- path.join(CONFIG.SERVER_ROOT, '..', 'client/public/placeholder.svg'),
345
- ];
346
- for (const p of possiblePaths) {
347
- try {
348
- if (fs.existsSync(p)) {
349
- const headers = new Headers();
350
- headers.set('Cache-Control', 'no-store');
351
- headers.set('Content-Type', 'image/svg+xml');
352
- return new Response(fs.readFileSync(p), { headers });
353
- }
354
- }
355
- catch {
356
- // ignore
357
- }
358
- }
359
- return null;
360
- }
361
- const PROXY_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0';
362
- export function registerProxy(app) {
363
- app.get('/api/proxy', async (c) => {
364
- const url = c.req.query('url');
365
- if (!url)
366
- return c.text('URL required', 400);
367
- const safeCheck = isSafeExternalUrl(url);
368
- if (!safeCheck.safe) {
369
- return c.text(safeCheck.error || 'Invalid URL', 400);
370
- }
371
- const urlStr = url;
372
- const refererStr = c.req.query('referer') || '';
373
- const cookieStr = c.req.query('cookie') || '';
374
- const variantRaw = c.req.query('variant');
375
- const variantIdx = variantRaw !== undefined ? parseInt(variantRaw, 10) : NaN;
376
- const pinVar = Number.isInteger(variantIdx) && variantIdx >= 0;
377
- const cacheKey = `m3u8-${urlStr}-${refererStr}${pinVar ? `-v${variantIdx}` : ''}`;
378
- const abort = new AbortController();
379
- linkAbort(c.req.raw.signal, abort);
380
- try {
381
- const isVaultCdn = Array.from(VAULT_CDN_HOSTS).some((host) => urlStr.includes(host));
382
- const isHanime = Array.from(HANIME_HOSTS).some((host) => urlStr.includes(host));
383
- const isOppai = Array.from(OPPAI_HOSTS).some((host) => urlStr.includes(host));
384
- const isKaa = refererStr.startsWith(KAA_ORIGIN) ||
385
- Array.from(KAA_HOSTS).some((host) => urlStr.includes(host));
386
- const headers = {
387
- 'User-Agent': PROXY_UA,
388
- };
389
- if (refererStr)
390
- headers['Referer'] = refererStr;
391
- if (isVaultCdn) {
392
- headers['Origin'] = refererStr || ANIMEPAHE_URL;
393
- const cookieHeader = buildCfClearanceCookie(cookieStr);
394
- if (cookieHeader)
395
- headers['Cookie'] = cookieHeader;
396
- }
397
- if (isHanime) {
398
- if (!headers['Referer'])
399
- headers['Referer'] = refererStr || 'https://nhplayer.com/';
400
- headers['Origin'] = 'https://nhplayer.com';
401
- }
402
- if (isOppai) {
403
- if (!headers['Referer'])
404
- headers['Referer'] = refererStr || 'https://oppai.stream/';
405
- headers['Origin'] = 'https://oppai.stream';
406
- }
407
- if (isKaa) {
408
- if (!headers['Referer'])
409
- headers['Referer'] = refererStr || KAA_REFERER;
410
- headers['Origin'] = KAA_ORIGIN;
411
- }
412
- const isMegaplay = refererStr.startsWith(MEGAPLAY_ORIGIN) || urlStr.includes('nexabloom.top');
413
- if (isMegaplay && !headers['Origin']) {
414
- headers['Origin'] = MEGAPLAY_ORIGIN;
415
- }
416
- if (urlStr.includes('weeabo0.xyz') || urlStr.includes('weeab0o.xyz')) {
417
- if (!headers['Referer'])
418
- headers['Referer'] = refererStr || 'https://japaneseasmr.com/';
419
- const jasmrCookieHeader = buildCfClearanceCookie(cookieStr);
420
- if (jasmrCookieHeader)
421
- headers['Cookie'] = jasmrCookieHeader;
422
- const jasmrUa = c.req.query('ua') || '';
423
- if (jasmrUa)
424
- headers['User-Agent'] = jasmrUa;
425
- }
426
- const range = c.req.header('range');
427
- if (range)
428
- headers['Range'] = range;
429
- const playlistResponse = (body) => {
430
- const outHeaders = new Headers();
431
- outHeaders.set('Content-Type', 'application/vnd.apple.mpegurl');
432
- outHeaders.set('Access-Control-Allow-Origin', '*');
433
- return new Response(body, { headers: outHeaders });
434
- };
435
- const rewritePlaylist = (playlistBody, finalUrl) => {
436
- const baseUrl = new URL(finalUrl);
437
- const proxiedMediaUrl = (targetUrl) => `/api/proxy?url=${encodeURIComponent(targetUrl)}&referer=${encodeURIComponent(refererStr)}` +
438
- (cookieStr ? `&cookie=${encodeURIComponent(sanitizeCfClearance(cookieStr))}` : '') +
439
- (c.req.query('ua') ? `&ua=${encodeURIComponent(c.req.query('ua'))}` : '');
440
- const needsProxy = Boolean(refererStr);
441
- const isProxied = (value) => value.includes('/api/proxy');
442
- const pinned = pinVar ? pinVariant(playlistBody, variantIdx) : null;
443
- return (pinned || playlistBody)
444
- .split('\n')
445
- .map((line) => {
446
- const trimmed = line.trim();
447
- if (!trimmed)
448
- return line;
449
- if (trimmed.startsWith('#')) {
450
- return trimmed.replace(/URI="([^"]+)"/g, (_, uri) => {
451
- if (isProxied(uri))
452
- return `URI="${uri}"`;
453
- const absolute = new URL(uri, baseUrl).href;
454
- return `URI="${needsProxy || absolute.includes('.m3u8') ? proxiedMediaUrl(absolute) : absolute}"`;
455
- });
456
- }
457
- if (isProxied(trimmed))
458
- return line;
459
- const absolute = new URL(trimmed, baseUrl).href;
460
- return proxiedMediaUrl(absolute);
461
- })
462
- .join('\n');
463
- };
464
- if (urlStr.includes('.m3u8')) {
465
- const cached = proxyCache.get(cacheKey);
466
- if (cached) {
467
- const headers = new Headers();
468
- headers.set('Content-Type', 'application/vnd.apple.mpegurl');
469
- headers.set('Access-Control-Allow-Origin', '*');
470
- return new Response(cached, { headers });
471
- }
472
- const resp = await fetchNexabloomFallback(urlStr, abort.signal, (candidate) => gotScraping({
473
- url: isNexabloomMasterUrl(candidate) ? signNexabloomMasterUrl(candidate) : candidate,
474
- method: 'GET',
475
- headers,
476
- responseType: 'text',
477
- timeout: { request: 30000 },
478
- followRedirect: true,
479
- throwHttpErrors: false,
480
- }), (attempt) => attempt.statusCode === 200 || attempt.statusCode === 206);
481
- if (!resp || (resp.statusCode !== 200 && resp.statusCode !== 206)) {
482
- return c.text('Upstream error', (resp?.statusCode || 502));
483
- }
484
- const rewritten = rewritePlaylist(resp.body, resp.url || urlStr);
485
- proxyCache.set(cacheKey, rewritten);
486
- return playlistResponse(rewritten);
487
- }
488
- else {
489
- if (isGotScrapingHost(urlStr)) {
490
- if (range)
491
- headers['Range'] = range;
492
- const resp = await fetchNexabloomFallback(urlStr, abort.signal, (candidate) => gotScraping({
493
- url: candidate,
494
- method: 'GET',
495
- headers,
496
- responseType: 'buffer',
497
- timeout: { request: 30000 },
498
- followRedirect: true,
499
- throwHttpErrors: false,
500
- }), (attempt) => attempt.statusCode === 200 || attempt.statusCode === 206);
501
- if (!resp || (resp.statusCode !== 200 && resp.statusCode !== 206)) {
502
- return c.text('Upstream error', (resp?.statusCode || 502));
503
- }
504
- const ct = resp.headers['content-type'];
505
- const isVtt = urlStr.endsWith('.vtt') || urlStr.includes('.vtt?');
506
- const bodyText = String(resp.body ?? '');
507
- if (!isVtt &&
508
- ((typeof ct === 'string' && ct.includes('mpegurl')) || bodyText.startsWith('#EXTM3U'))) {
509
- const cached = proxyCache.get(cacheKey);
510
- const rewritten = cached ?? rewritePlaylist(bodyText, resp.url || urlStr);
511
- if (!cached)
512
- proxyCache.set(cacheKey, rewritten);
513
- return playlistResponse(rewritten);
514
- }
515
- const outHeaders = new Headers();
516
- outHeaders.set('Content-Type', isVtt ? 'text/vtt; charset=utf-8' : ct || 'application/octet-stream');
517
- setProxyHeaders(outHeaders, (name) => resp.headers[name]);
518
- return new Response(resp.body, {
519
- status: resp.statusCode,
520
- headers: outHeaders,
521
- });
522
- }
523
- else {
524
- const upstream = await fetchNexabloomFallback(urlStr, abort.signal, async (candidate) => {
525
- const attempt = await fetchWithRetry(candidate, { method: 'GET', headers }, { retries: 3, timeoutMs: 30000, signal: abort.signal });
526
- if (attempt.status !== 200 && attempt.status !== 206) {
527
- try {
528
- await attempt.body?.cancel();
529
- }
530
- catch {
531
- // ignore
532
- }
533
- }
534
- return attempt;
535
- }, (attempt) => attempt.status === 200 || attempt.status === 206);
536
- if (!upstream) {
537
- return c.text('Upstream error', 502);
538
- }
539
- const status = upstream.status;
540
- if (status !== 200 && status !== 206) {
541
- try {
542
- await upstream.body?.cancel();
543
- }
544
- catch {
545
- // ignore
546
- }
547
- return c.text('Upstream error', status);
548
- }
549
- const isVtt = urlStr.endsWith('.vtt') || urlStr.includes('.vtt?');
550
- const ct = upstream.headers.get('content-type');
551
- const outHeaders = new Headers();
552
- outHeaders.set('Content-Type', isVtt ? 'text/vtt; charset=utf-8' : ct || 'application/octet-stream');
553
- setProxyHeaders(outHeaders, (name) => upstream.headers.get(name));
554
- if (!upstream.body) {
555
- return c.text('Upstream error', 502);
556
- }
557
- return new Response(upstream.body, { status: status, headers: outHeaders });
558
- }
559
- }
560
- }
561
- catch (e) {
562
- if (abort.signal.aborted)
563
- return new Response(null, { status: 499 });
564
- logger.error({ url: urlStr, error: e?.message }, '[proxy] upstream request failed');
565
- return c.text('Proxy error', 500);
566
- }
567
- });
568
- app.get('/api/embed-proxy', async (c) => {
569
- const kwikUrl = validateKwikUrl(c.req.query('url'));
570
- if (!kwikUrl)
571
- return c.text('Invalid or unsupported gateway URL', 400);
572
- const cookieStr = c.req.query('cookie') || '';
573
- const abort = new AbortController();
574
- linkAbort(c.req.raw.signal, abort);
575
- try {
576
- const headers = {
577
- 'User-Agent': PROXY_UA,
578
- Referer: ANIMEPAHE_URL,
579
- Origin: 'https://animepahe.pw',
580
- };
581
- if (cookieStr) {
582
- const cookieHeader = buildCfClearanceCookie(cookieStr);
583
- if (cookieHeader)
584
- headers['Cookie'] = cookieHeader;
585
- }
586
- const response = await gotScraping({
587
- url: kwikUrl.href,
588
- method: 'GET',
589
- headers,
590
- responseType: 'text',
591
- timeout: { request: 30000 },
592
- followRedirect: true,
593
- throwHttpErrors: false,
594
- });
595
- const originalHtml = response.body;
596
- const patched = applyKwikPatches(originalHtml, kwikUrl, cookieStr);
597
- if (!patched)
598
- return c.text('Failed to patch video gateway', 502);
599
- const outHeaders = new Headers();
600
- outHeaders.set('Content-Type', 'text/html; charset=utf-8');
601
- outHeaders.set('Cache-Control', 'private, max-age=120');
602
- return new Response(patched, { status: 200, headers: outHeaders });
603
- }
604
- catch {
605
- if (abort.signal.aborted)
606
- return new Response(null, { status: 499 });
607
- return c.text('Gateway proxy error', 502);
608
- }
609
- });
610
- app.get('/api/subtitle-proxy', async (c) => {
611
- const url = c.req.query('url');
612
- if (!url)
613
- return c.text('URL required', 400);
614
- const safeCheck = isSafeExternalUrl(url);
615
- if (!safeCheck.safe) {
616
- return c.text(safeCheck.error || 'Invalid URL', 400);
617
- }
618
- const abort = new AbortController();
619
- linkAbort(c.req.raw.signal, abort);
620
- const subUrl = url;
621
- const refererStr = c.req.query('referer') || '';
622
- const isAnilight = refererStr.includes('anilight.live') || ANILIGHT_HOSTS.some((host) => subUrl.includes(host));
623
- const innerRawUrl = (() => {
624
- try {
625
- const parsed = new URL(subUrl);
626
- if (parsed.hostname.toLowerCase().endsWith('anilight.live') &&
627
- parsed.pathname.includes('/proxy/captions')) {
628
- const inner = parsed.searchParams.get('url');
629
- if (inner && (inner.startsWith('http://') || inner.startsWith('https://'))) {
630
- return inner;
631
- }
632
- }
633
- }
634
- catch {
635
- // ignore
636
- }
637
- return null;
638
- })();
639
- const buildHeaders = () => {
640
- const headers = {
641
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
642
- Accept: 'text/vtt,text/plain,*/*;q=0.8',
643
- };
644
- if (refererStr) {
645
- headers['Referer'] = refererStr;
646
- }
647
- else if (isAnilight) {
648
- headers['Referer'] = ANILIGHT_REFERER;
649
- }
650
- if (refererStr.startsWith(KAA_ORIGIN) ||
651
- Array.from(KAA_HOSTS).some((host) => subUrl.includes(host))) {
652
- headers['Origin'] = KAA_ORIGIN;
653
- }
654
- else if (isAnilight) {
655
- headers['Origin'] = ANILIGHT_ORIGIN;
656
- }
657
- return headers;
658
- };
659
- const fetchText = async (target, retries = 1) => {
660
- const headers = buildHeaders();
661
- if (innerRawUrl && target === innerRawUrl) {
662
- headers['Referer'] = ANILIGHT_REFERER;
663
- headers['Origin'] = ANILIGHT_ORIGIN;
664
- }
665
- if (isGotScrapingHost(target)) {
666
- try {
667
- const upstream = await gotScraping({
668
- url: target,
669
- method: 'GET',
670
- headers,
671
- responseType: 'text',
672
- timeout: { request: 15000 },
673
- followRedirect: true,
674
- throwHttpErrors: false,
675
- });
676
- if (upstream.statusCode !== 200) {
677
- logger.warn({ target, status: upstream.statusCode }, '[subtitle-proxy] upstream returned error status');
678
- return { status: upstream.statusCode, body: '' };
679
- }
680
- const body = String(upstream.body ?? '').replace(/^\uFEFF/, '');
681
- if (/^\s*\{[\s\S]*"error"[\s\S]*\}\s*$/.test(body.slice(0, 500))) {
682
- logger.warn({ target, body: body.slice(0, 200) }, '[subtitle-proxy] upstream JSON error');
683
- return { status: 502, body: '' };
684
- }
685
- return { status: 200, body };
686
- }
687
- catch (e) {
688
- if (abort.signal.aborted)
689
- return null;
690
- logger.warn({ target, error: e?.message }, '[subtitle-proxy] upstream fetch failed');
691
- return { status: 502, body: '' };
692
- }
693
- }
694
- try {
695
- const upstream = await fetchWithRetry(target, { method: 'GET', headers }, { retries, timeoutMs: 15000, signal: abort.signal });
696
- if (!upstream.ok) {
697
- logger.warn({ target, status: upstream.status }, '[subtitle-proxy] upstream returned error status');
698
- try {
699
- await upstream.body?.cancel();
700
- }
701
- catch {
702
- // ignore
703
- }
704
- return { status: upstream.status, body: '' };
705
- }
706
- const body = (await upstream.text()).replace(/^\uFEFF/, '');
707
- if (/^\s*\{[\s\S]*"error"[\s\S]*\}\s*$/.test(body.slice(0, 500))) {
708
- logger.warn({ target, body: body.slice(0, 200) }, '[subtitle-proxy] upstream JSON error');
709
- return { status: 502, body: '' };
710
- }
711
- return { status: upstream.status, body };
712
- }
713
- catch (e) {
714
- if (abort.signal.aborted)
715
- return null;
716
- logger.warn({ target, error: e?.message }, '[subtitle-proxy] upstream fetch failed');
717
- return { status: 502, body: '' };
718
- }
719
- };
720
- const vttHeaders = (cacheControl) => {
721
- const headers = new Headers();
722
- headers.set('Content-Type', 'text/vtt; charset=utf-8');
723
- headers.set('Access-Control-Allow-Origin', '*');
724
- headers.set('Cache-Control', cacheControl);
725
- return headers;
726
- };
727
- const subtitleError = (text) => {
728
- const headers = new Headers();
729
- headers.set('Access-Control-Allow-Origin', '*');
730
- headers.set('Cache-Control', 'no-store');
731
- return new Response(text, { status: 502, headers });
732
- };
733
- try {
734
- const headers = buildHeaders();
735
- let result = await fetchText(subUrl, 1);
736
- if (abort.signal.aborted)
737
- return new Response(null, { status: 499 });
738
- if ((!result || !result.body) && innerRawUrl) {
739
- result = await fetchText(innerRawUrl, 0);
740
- if (abort.signal.aborted)
741
- return new Response(null, { status: 499 });
742
- }
743
- if (!result || !result.body) {
744
- return subtitleError('Subtitle upstream error');
745
- }
746
- const body = result.body;
747
- const baseForSegments = innerRawUrl || subUrl;
748
- if (/#EXTM3U/i.test(body.slice(0, 1000))) {
749
- const segUrls = body
750
- .split('\n')
751
- .map((l) => l.trim())
752
- .filter((l) => l && !l.startsWith('#'))
753
- .slice(0, 200)
754
- .map((u) => {
755
- try {
756
- return new URL(u, baseForSegments).href;
757
- }
758
- catch {
759
- return null;
760
- }
761
- })
762
- .filter((u) => Boolean(u));
763
- const parts = [];
764
- for (const seg of segUrls) {
765
- try {
766
- const segRes = await fetchWithRetry(seg, { method: 'GET', headers }, { retries: 1, timeoutMs: 15000, signal: abort.signal });
767
- if (!segRes.ok)
768
- continue;
769
- let segBody = (await segRes.text()).replace(/^\uFEFF/, '');
770
- if (/#EXTM3U/i.test(segBody.slice(0, 200)))
771
- continue;
772
- segBody = segBody
773
- .replace(/\r\n/g, '\n')
774
- .replace(/(\d{2}:\d{2}:\d{2}),(\d{3})/g, '$1.$2');
775
- if (/^\s*WEBVTT/i.test(segBody)) {
776
- segBody = segBody.replace(/^\s*WEBVTT[^\n]*\n(\n|.*\n)?/, '');
777
- }
778
- segBody = segBody.trim();
779
- if (segBody)
780
- parts.push(segBody);
781
- if (parts.join('\n').length > 2_000_000)
782
- break;
783
- }
784
- catch {
785
- if (abort.signal.aborted)
786
- return new Response(null, { status: 499 });
787
- }
788
- }
789
- if (parts.length === 0) {
790
- return subtitleError('Subtitle playlist empty');
791
- }
792
- return new Response(rewrapVttCues(`WEBVTT\n\n${parts.join('\n\n')}\n`), {
793
- headers: vttHeaders('public, max-age=86400'),
794
- });
795
- }
796
- if (/^\s*WEBVTT/i.test(body)) {
797
- return new Response(rewrapVttCues(body), {
798
- headers: vttHeaders('public, max-age=86400'),
799
- });
800
- }
801
- const vttFromAss = assToVtt(body);
802
- if (vttFromAss) {
803
- return new Response(rewrapVttCues(vttFromAss), {
804
- headers: vttHeaders('public, max-age=86400'),
805
- });
806
- }
807
- 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') });
808
- }
809
- catch {
810
- if (abort.signal.aborted)
811
- return new Response(null, { status: 499 });
812
- logger.warn({ url: subUrl }, '[subtitle-proxy] failed');
813
- return subtitleError('Subtitle upstream error');
814
- }
815
- });
816
- app.get('/api/image-proxy', async (c) => {
817
- const url = c.req.query('url');
818
- if (!url)
819
- return c.text('URL required', 400);
820
- const safeCheck = isSafeExternalUrl(url);
821
- if (!safeCheck.safe) {
822
- return c.text(safeCheck.error || 'Invalid URL', 400);
823
- }
824
- const targetUrl = url;
825
- const cookie = c.req.query('cookie') || '';
826
- const ua = c.req.query('ua') || '';
827
- const strict = c.req.query('strict') === '1';
828
- const abort = new AbortController();
829
- linkAbort(c.req.raw.signal, abort);
830
- let refererValue = 'https://anilist.co/';
831
- const headers = {
832
- 'User-Agent': ua ||
833
- 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
834
- };
835
- const cacheKey = `${targetUrl}|${cookie}|${ua}`;
836
- const cached = imageCache.get(cacheKey);
837
- if (cached) {
838
- const hit = new Headers();
839
- hit.set('Cache-Control', 'public, max-age=604800, immutable');
840
- hit.set('Content-Type', cached.contentType);
841
- hit.set('Access-Control-Allow-Origin', '*');
842
- hit.set('X-Dango-Cache', 'hit');
843
- return new Response(cached.body, { headers: hit });
844
- }
845
- try {
846
- if (targetUrl.includes('animepahe')) {
847
- refererValue = 'https://animepahe.pw/';
848
- headers['Cookie'] = `cf_clearance=${sanitizeCfClearance(cookie)}`;
849
- }
850
- else if (targetUrl.includes('anilist.co')) {
851
- refererValue = 'https://anilist.co/';
852
- }
853
- else if (targetUrl.includes('gogocdn.net')) {
854
- refererValue = 'https://gogoanime.lu/';
855
- }
856
- else if (targetUrl.includes('animeya.cc')) {
857
- refererValue = 'https://animeya.cc/';
858
- }
859
- else if (targetUrl.includes('myspacecat.pictures')) {
860
- refererValue = 'https://oppai.stream/';
861
- }
862
- else if (targetUrl.includes('weeabo0.xyz')) {
863
- refererValue = 'https://japaneseasmr.com/';
864
- const jasmrCookieHeader = buildCfClearanceCookie(cookie);
865
- if (jasmrCookieHeader)
866
- headers['Cookie'] = jasmrCookieHeader;
867
- const uaParam = ua || '';
868
- if (uaParam)
869
- headers['User-Agent'] = uaParam;
870
- }
871
- headers['Referer'] = refererValue;
872
- let body = null;
873
- let contentType = 'image/webp';
874
- for (let attempt = 0; attempt < 3 && !body; attempt++) {
875
- const resp = await gotScraping({
876
- url: targetUrl,
877
- method: 'GET',
878
- headers,
879
- responseType: 'buffer',
880
- followRedirect: true,
881
- throwHttpErrors: false,
882
- timeout: { request: 30000 },
883
- });
884
- if (resp.statusCode === 200 && resp.rawBody?.length) {
885
- body = Buffer.from(resp.rawBody);
886
- contentType = String(resp.headers['content-type'] || 'image/webp');
887
- }
888
- else {
889
- if (attempt < 2)
890
- await new Promise((r) => setTimeout(r, 400 * (attempt + 1)));
891
- }
892
- }
893
- if (body) {
894
- imageCache.set(cacheKey, { body, contentType });
895
- const outHeaders = new Headers();
896
- outHeaders.set('Cache-Control', 'public, max-age=604800, immutable');
897
- outHeaders.set('Content-Type', contentType);
898
- outHeaders.set('Access-Control-Allow-Origin', '*');
899
- return new Response(body, { headers: outHeaders });
900
- }
901
- // strict=1 callers retry another candidate instead of settling for the
902
- // placeholder, which would otherwise read as a successful load.
903
- if (strict)
904
- return c.text('Not Found', 404);
905
- const placeholder = placeholderSvg();
906
- if (placeholder)
907
- return placeholder;
908
- return c.text('Not Found', 404);
909
- }
910
- catch {
911
- if (abort.signal.aborted)
912
- return new Response(null, { status: 499 });
913
- if (strict)
914
- return c.text('Not Found', 502);
915
- const placeholder = placeholderSvg();
916
- if (placeholder)
917
- return placeholder;
918
- return c.text('Not Found', 404);
919
- }
920
- });
921
- app.get('/api/resolve', async (c) => {
922
- const targetUrl = c.req.query('url');
923
- if (!targetUrl || typeof targetUrl !== 'string') {
924
- return c.json({ error: 'URL required' }, 400);
925
- }
926
- const safeCheck = isSafeExternalUrl(targetUrl);
927
- if (!safeCheck.safe) {
928
- return c.json({ error: safeCheck.error || 'Invalid URL' }, 400);
929
- }
930
- try {
931
- const response = await fetch(targetUrl, {
932
- method: 'GET',
933
- headers: {
934
- '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',
935
- Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
936
- },
937
- redirect: 'follow',
938
- });
939
- return c.json({
940
- finalUrl: response.url,
941
- status: response.status,
942
- });
943
- }
944
- catch (e) {
945
- const err = e;
946
- return c.json({ error: err.message || 'Resolve failed' }, 500);
947
- }
948
- });
949
- }
67
+ </script></body>`);return w===c?null:w}function Y(){const c=[j.join(N.PACKAGE_ROOT,"client/public/placeholder.svg"),j.join(N.PACKAGE_ROOT,"client/dist/placeholder.svg"),j.join(N.SERVER_ROOT,"..","client/public/placeholder.svg")];for(const o of c)try{if(D.existsSync(o)){const e=new Headers;return e.set("Cache-Control","no-store"),e.set("Content-Type","image/svg+xml"),new Response(D.readFileSync(o),{headers:e})}}catch{}return null}const Z="Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0";function ye(c,o){c.get("/api/proxy",async e=>{const r=e.req.query("url");if(!r)return e.text("URL required",400);const i=W(r);if(!i.safe)return e.text(i.error||"Invalid URL",400);const t=r,n=I(t,o()),s=e.req.query("referer")||n?.referer||"",d=B(n,s,e.req.query("origin")),m=e.req.query("ua")||"",R=e.req.query("cookie")||"",w=e.req.query("scrape")==="1"||n?.scrape===!0,T=e.req.query("cf")==="1"||n?.cfCookie===!0,h=e.req.query("variant"),b=h!==void 0?parseInt(h,10):NaN,l=Number.isInteger(b)&&b>=0,u=`m3u8-${t}-${s}-${d}-${m}${l?`-v${b}`:""}`,y=new AbortController;L(e.req.raw.signal,y);try{const f={"User-Agent":m||Z};if(s&&(f.Referer=s),d&&(f.Origin=d),R){const a=T?V(R):R;a&&(f.Cookie=a)}const p=e.req.header("range");p&&(f.Range=p);const U=a=>{const g=new Headers;return g.set("Content-Type","application/vnd.apple.mpegurl"),g.set("Access-Control-Allow-Origin","*"),new Response(a,{headers:g})},E=(a,g)=>{const x=new URL(g),q=k=>{const v=new URLSearchParams({url:k});return s&&v.set("referer",s),d&&v.set("origin",d),m&&v.set("ua",m),R&&v.set("cookie",ee(R)),w&&v.set("scrape","1"),T&&v.set("cf","1"),`/api/proxy?${v}`},$=!!(s||d||m||w),C=k=>k.includes("/api/proxy");return((l?oe(a,b):null)||a).split(`
68
+ `).map(k=>{const v=k.trim();if(!v)return k;if(v.startsWith("#"))return v.replace(/URI="([^"]+)"/g,(ae,F)=>{if(C(F))return`URI="${F}"`;const M=new URL(F,x).href;return`URI="${$||M.includes(".m3u8")?q(M):M}"`});if(C(v))return k;const Q=new URL(v,x).href;return q(Q)}).join(`
69
+ `)};if(t.includes(".m3u8")){const a=O.get(u);if(a){const q=new Headers;return q.set("Content-Type","application/vnd.apple.mpegurl"),q.set("Access-Control-Allow-Origin","*"),new Response(a,{headers:q})}const g=await K(t,n?.mirrors,y.signal,q=>H({url:q,method:"GET",headers:f,responseType:"text",timeout:{request:3e4},followRedirect:!0,throwHttpErrors:!1}),q=>q.statusCode===200||q.statusCode===206);if(!g||g.statusCode!==200&&g.statusCode!==206)return e.text("Upstream error",g?.statusCode||502);const x=E(g.body,g.url||t);return O.set(u,x),U(x)}else if(w){p&&(f.Range=p);const a=await K(t,n?.mirrors,y.signal,C=>H({url:C,method:"GET",headers:f,responseType:"buffer",timeout:{request:3e4},followRedirect:!0,throwHttpErrors:!1}),C=>C.statusCode===200||C.statusCode===206);if(!a||a.statusCode!==200&&a.statusCode!==206)return e.text("Upstream error",a?.statusCode||502);const g=a.headers["content-type"],x=t.endsWith(".vtt")||t.includes(".vtt?"),q=String(a.body??"");if(!x&&(typeof g=="string"&&g.includes("mpegurl")||q.startsWith("#EXTM3U"))){const C=O.get(u),S=C??E(q,a.url||t);return C||O.set(u,S),U(S)}const $=new Headers;return $.set("Content-Type",x?"text/vtt; charset=utf-8":g||"application/octet-stream"),X($,C=>a.headers[C]),new Response(a.body,{status:a.statusCode,headers:$})}else{const a=await K(t,n?.mirrors,y.signal,async C=>{const S=await G(C,{method:"GET",headers:f},{retries:3,timeoutMs:3e4,signal:y.signal});if(S.status!==200&&S.status!==206)try{await S.body?.cancel()}catch{}return S},C=>C.status===200||C.status===206);if(!a)return e.text("Upstream error",502);const g=a.status;if(g!==200&&g!==206){try{await a.body?.cancel()}catch{}return e.text("Upstream error",g)}const x=t.endsWith(".vtt")||t.includes(".vtt?"),q=a.headers.get("content-type"),$=new Headers;return $.set("Content-Type",x?"text/vtt; charset=utf-8":q||"application/octet-stream"),X($,C=>a.headers.get(C)),a.body?new Response(a.body,{status:g,headers:$}):e.text("Upstream error",502)}}catch(f){return y.signal.aborted?new Response(null,{status:499}):(A.error({url:t,error:f?.message},"[proxy] upstream request failed"),e.text("Proxy error",500))}}),c.get("/api/embed-proxy",async e=>{const r=e.req.query("url");if(!r)return e.text("URL required",400);const i=W(r);if(!i.safe)return e.text(i.error||"Invalid URL",400);const t=I(r,o());if(!t?.embedPatch)return e.text("Invalid or unsupported gateway URL",400);const n=new URL(r),s=e.req.query("cookie")||"",d=e.req.query("referer")||t.referer||"",m=B(t,d,e.req.query("origin")),R=new AbortController;L(e.req.raw.signal,R);try{const w={"User-Agent":e.req.query("ua")||Z};if(d&&(w.Referer=d),m&&(w.Origin=m),s){const u=e.req.query("cf")==="1"||t.cfCookie?V(s):s;u&&(w.Cookie=u)}const h=(await H({url:n.href,method:"GET",headers:w,responseType:"text",timeout:{request:3e4},followRedirect:!0,throwHttpErrors:!1})).body,b=ie(h,n,s);if(!b)return e.text("Failed to patch video gateway",502);const l=new Headers;return l.set("Content-Type","text/html; charset=utf-8"),l.set("Cache-Control","private, max-age=120"),new Response(b,{status:200,headers:l})}catch{return R.signal.aborted?new Response(null,{status:499}):e.text("Gateway proxy error",502)}}),c.get("/api/subtitle-proxy",async e=>{const r=e.req.query("url");if(!r)return e.text("URL required",400);const i=W(r);if(!i.safe)return e.text(i.error||"Invalid URL",400);const t=new AbortController;L(e.req.raw.signal,t);const n=r,s=I(n,o()),d=e.req.query("referer")||s?.referer||"",m=B(s,d,e.req.query("origin")),R=e.req.query("scrape")==="1"||s?.scrape===!0,w=()=>{const l={"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",Accept:"text/vtt,text/plain,*/*;q=0.8"};return d&&(l.Referer=d),m&&(l.Origin=m),l},T=async(l,u=1)=>{const y=w();if(R)try{const f=await H({url:l,method:"GET",headers:y,responseType:"text",timeout:{request:15e3},followRedirect:!0,throwHttpErrors:!1});if(f.statusCode!==200)return A.warn({target:l,status:f.statusCode},"[subtitle-proxy] upstream returned error status"),{status:f.statusCode,body:""};const p=String(f.body??"").replace(/^\uFEFF/,"");return/^\s*\{[\s\S]*"error"[\s\S]*\}\s*$/.test(p.slice(0,500))?(A.warn({target:l,body:p.slice(0,200)},"[subtitle-proxy] upstream JSON error"),{status:502,body:""}):{status:200,body:p}}catch(f){return t.signal.aborted?null:(A.warn({target:l,error:f?.message},"[subtitle-proxy] upstream fetch failed"),{status:502,body:""})}try{const f=await G(l,{method:"GET",headers:y},{retries:u,timeoutMs:15e3,signal:t.signal});if(!f.ok){A.warn({target:l,status:f.status},"[subtitle-proxy] upstream returned error status");try{await f.body?.cancel()}catch{}return{status:f.status,body:""}}const p=(await f.text()).replace(/^\uFEFF/,"");return/^\s*\{[\s\S]*"error"[\s\S]*\}\s*$/.test(p.slice(0,500))?(A.warn({target:l,body:p.slice(0,200)},"[subtitle-proxy] upstream JSON error"),{status:502,body:""}):{status:f.status,body:p}}catch(f){return t.signal.aborted?null:(A.warn({target:l,error:f?.message},"[subtitle-proxy] upstream fetch failed"),{status:502,body:""})}},h=l=>{const u=new Headers;return u.set("Content-Type","text/vtt; charset=utf-8"),u.set("Access-Control-Allow-Origin","*"),u.set("Cache-Control",l),u},b=l=>{const u=new Headers;return u.set("Access-Control-Allow-Origin","*"),u.set("Cache-Control","no-store"),new Response(l,{status:502,headers:u})};try{const l=w(),u=await T(n,1);if(t.signal.aborted)return new Response(null,{status:499});if(!u||!u.body)return b("Subtitle upstream error");const y=u.body,f=n;if(/#EXTM3U/i.test(y.slice(0,1e3))){const U=y.split(`
70
+ `).map(a=>a.trim()).filter(a=>a&&!a.startsWith("#")).slice(0,200).map(a=>{try{return new URL(a,f).href}catch{return null}}).filter(a=>!!a),E=[];for(const a of U)try{const g=await G(a,{method:"GET",headers:l},{retries:1,timeoutMs:15e3,signal:t.signal});if(!g.ok)continue;let x=(await g.text()).replace(/^\uFEFF/,"");if(/#EXTM3U/i.test(x.slice(0,200)))continue;if(x=x.replace(/\r\n/g,`
71
+ `).replace(/(\d{2}:\d{2}:\d{2}),(\d{3})/g,"$1.$2"),/^\s*WEBVTT/i.test(x)&&(x=x.replace(/^\s*WEBVTT[^\n]*\n(\n|.*\n)?/,"")),x=x.trim(),x&&E.push(x),E.join(`
72
+ `).length>2e6)break}catch{if(t.signal.aborted)return new Response(null,{status:499})}return E.length===0?b("Subtitle playlist empty"):new Response(P(`WEBVTT
73
+
74
+ ${E.join(`
75
+
76
+ `)}
77
+ `),{headers:h("public, max-age=86400")})}if(/^\s*WEBVTT/i.test(y))return new Response(P(y),{headers:h("public, max-age=86400")});const p=te(y);return p?new Response(P(p),{headers:h("public, max-age=86400")}):new Response(P(`WEBVTT
78
+
79
+ ${y.replace(/\r\n/g,`
80
+ `).replace(/(\d{2}:\d{2}:\d{2}),(\d{3})/g,"$1.$2")}`),{headers:h("public, max-age=86400")})}catch{return t.signal.aborted?new Response(null,{status:499}):(A.warn({url:n},"[subtitle-proxy] failed"),b("Subtitle upstream error"))}}),c.get("/api/image-proxy",async e=>{const r=e.req.query("url");if(!r)return e.text("URL required",400);const i=W(r);if(!i.safe)return e.text(i.error||"Invalid URL",400);const t=r,n=e.req.query("cookie")||"",s=e.req.query("ua")||"",d=e.req.query("strict")==="1",m=I(t,o()),R=e.req.query("referer")||m?.referer||"",w=B(m,R,e.req.query("origin")),T=new AbortController;L(e.req.raw.signal,T);const h={"User-Agent":s||"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"},b=`${t}|${n}|${s}|${R}|${w}`,l=z.get(b);if(l){const u=new Headers;return u.set("Cache-Control","public, max-age=604800, immutable"),u.set("Content-Type",l.contentType),u.set("Access-Control-Allow-Origin","*"),u.set("X-Dango-Cache","hit"),new Response(l.body,{headers:u})}try{if(R&&(h.Referer=R),w&&(h.Origin=w),e.req.query("cf")==="1"||m?.cfCookie){const p=V(n);p&&(h.Cookie=p)}let u=null,y="image/webp";for(let p=0;p<3&&!u;p++){const U=await H({url:t,method:"GET",headers:h,responseType:"buffer",followRedirect:!0,throwHttpErrors:!1,timeout:{request:3e4}});U.statusCode===200&&U.rawBody?.length?(u=Buffer.from(U.rawBody),y=String(U.headers["content-type"]||"image/webp")):p<2&&await new Promise(E=>setTimeout(E,400*(p+1)))}if(u){z.set(b,{body:u,contentType:y});const p=new Headers;return p.set("Cache-Control","public, max-age=604800, immutable"),p.set("Content-Type",y),p.set("Access-Control-Allow-Origin","*"),new Response(u,{headers:p})}if(d)return e.text("Not Found",404);const f=Y();return f||e.text("Not Found",404)}catch{if(T.signal.aborted)return new Response(null,{status:499});if(d)return e.text("Not Found",502);const u=Y();return u||e.text("Not Found",404)}}),c.get("/api/resolve",async e=>{const r=e.req.query("url");if(!r||typeof r!="string")return e.json({error:"URL required"},400);const i=W(r);if(!i.safe)return e.json({error:i.error||"Invalid URL"},400);try{const t=await fetch(r,{method:"GET",headers:{"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",Accept:"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"},redirect:"follow"});return e.json({finalUrl:t.url,status:t.status})}catch(t){const n=t;return e.json({error:n.message||"Resolve failed"},500)}})}export{ye as registerProxy};