@serifpersia/dango 3.3.9 → 3.4.0

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 (180) hide show
  1. package/README.md +1 -1
  2. package/client/dist/assets/{AnimeCard-URCiiy5W.js → AnimeCard-CJy1SXH3.js} +1 -1
  3. package/client/dist/assets/{AnimeInfo-e3z3er9t.js → AnimeInfo-BoXlVvZ-.js} +1 -1
  4. package/client/dist/assets/{AnimeInfoPage-B7Djlmy_.js → AnimeInfoPage-BxbA_7AV.js} +2 -2
  5. package/client/dist/assets/Asmr--Zc8G9zv.js +2 -0
  6. package/client/dist/assets/AsmrResetProgressModal-CKYm-8cp.js +1 -0
  7. package/client/dist/assets/AvSyncCalibrator-3yjZS7s6.js +10 -0
  8. package/client/dist/assets/Home-rrhOGQcg.js +1 -0
  9. package/client/dist/assets/Insights-Bay90Yeg.js +1 -0
  10. package/client/dist/assets/{LibraryListPage-B0qWqhb_.js → LibraryListPage-HzuDavbb.js} +1 -1
  11. package/client/dist/assets/{ListeningList-CSqD8j1C.js → ListeningList-DYcQnnsG.js} +1 -1
  12. package/client/dist/assets/{Manga-Cg2YuObF.js → Manga-D8CT9aO0.js} +1 -1
  13. package/client/dist/assets/{MangaInfoPage-DcArhEsy.js → MangaInfoPage-DGK6CYp1.js} +1 -1
  14. package/client/dist/assets/MangaReadPage-B9XoDKF9.js +1 -0
  15. package/client/dist/assets/{MangaResetProgressModal-D_yT3d1v.js → MangaResetProgressModal-QjZyIAjl.js} +1 -1
  16. package/client/dist/assets/Mature-CHt_-Hz5.js +1 -0
  17. package/client/dist/assets/MediaCard-B5584Cq9.js +1 -0
  18. package/client/dist/assets/Music-HeLrEZ2d.js +1 -0
  19. package/client/dist/assets/OptionListMenu-CJ90CdwI.js +1 -0
  20. package/client/dist/assets/Player-BYtEax_z.js +56 -0
  21. package/client/dist/assets/PlayerSettings-4-cUAY4B.js +1 -0
  22. package/client/dist/assets/QueueOptionsButton-Ba0dg_82.js +1 -0
  23. package/client/dist/assets/{QueueRail-B7f1-e6i.js → QueueRail-O7f4RqjB.js} +1 -1
  24. package/client/dist/assets/Radio-DPZxQnyD.js +1 -0
  25. package/client/dist/assets/{ReadingList-BaIem8H1.js → ReadingList-n6rr8ygy.js} +1 -1
  26. package/client/dist/assets/{ResetProgressModal-DES5Y-Fl.js → ResetProgressModal-CcStlbhP.js} +1 -1
  27. package/client/dist/assets/Search-C2wrWOdy.js +1 -0
  28. package/client/dist/assets/Settings-CluUYs8U.js +21 -0
  29. package/client/dist/assets/{SkeletonGrid-BxfgjP4i.js → SkeletonGrid-CLEPY9nP.js} +1 -1
  30. package/client/dist/assets/Trackers-CTGPxs0V.js +2 -0
  31. package/client/dist/assets/Tv-BwHlDdi8.js +4 -0
  32. package/client/dist/assets/Tv-ZEA45gXK.css +1 -0
  33. package/client/dist/assets/TvInfo-CXnrLekr.js +1 -0
  34. package/client/dist/assets/TvPopup-BekfiNcX.js +1 -0
  35. package/client/dist/assets/{TvSearch-BWDwWnHe.js → TvSearch-DuqR-z3v.js} +1 -1
  36. package/client/dist/assets/{TvWatchlist-D3n5_4MI.js → TvWatchlist-CBvgFrA0.js} +1 -1
  37. package/client/dist/assets/Watchlist-mj8mTOam.js +1 -0
  38. package/client/dist/assets/{hls-BO_L1WoN.js → hls-Boje3IOE.js} +1 -1
  39. package/client/dist/assets/index-BXwhFWB_.js +178 -0
  40. package/client/dist/assets/useAnimeInfoData-DJ7A4B4T.js +1 -0
  41. package/client/dist/assets/useAsmr-DIwIsJXF.js +1 -0
  42. package/client/dist/assets/{useAsmrPopup-D_o9-QlR.js → useAsmrPopup-e4zNN9ig.js} +1 -1
  43. package/client/dist/assets/useMangaLibrary-C9Lci5N4.js +1 -0
  44. package/client/dist/assets/{useMangaPopup-TFzkC-tG.js → useMangaPopup-B8nnpvyi.js} +1 -1
  45. package/client/dist/assets/useProviders-B5Qr4I1y.js +1 -0
  46. package/client/dist/assets/useTv-Dm8MCYlL.js +1 -0
  47. package/client/dist/assets/useTvLibrary-B7jEk2Q7.js +1 -0
  48. package/client/dist/assets/{vendor-query-BXRECSI6.js → vendor-query-BUTye89G.js} +1 -1
  49. package/client/dist/index.html +2 -2
  50. package/orchestrator.js +1 -2
  51. package/package.json +2 -6
  52. package/server/dist/app-auth.js +9 -29
  53. package/server/dist/app-hono.js +621 -0
  54. package/server/dist/config.js +0 -1
  55. package/server/dist/db/drizzle.js +32 -0
  56. package/server/dist/db/schema-anime.js +64 -0
  57. package/server/dist/db/schema-asmr.js +25 -0
  58. package/server/dist/db/schema-manga.js +31 -0
  59. package/server/dist/db/schema-tv.js +33 -0
  60. package/server/dist/db.js +75 -4
  61. package/server/dist/discord-gateway.js +1 -1
  62. package/server/dist/google.js +0 -7
  63. package/server/dist/hono/asmr.js +1 -0
  64. package/server/dist/hono/auth.js +56 -0
  65. package/server/dist/hono/music.js +419 -0
  66. package/server/dist/{controllers/proxy.controller.js → hono/proxy.js} +390 -394
  67. package/server/dist/hono/tv.js +197 -0
  68. package/server/dist/hono/watchlist.js +733 -0
  69. package/server/dist/lib/anilist.js +3 -3
  70. package/server/dist/lib/insights.js +185 -0
  71. package/server/dist/lib/ipc.js +1 -2
  72. package/server/dist/lib/kitsu.js +1 -28
  73. package/server/dist/lib/mal-import.js +291 -0
  74. package/server/dist/lib/migration.js +20 -34
  75. package/server/dist/lib/offline-db.js +36 -36
  76. package/server/dist/lib/tracker/manga-sync.service.js +21 -21
  77. package/server/dist/lib/tracker/sync.service.js +20 -20
  78. package/server/dist/lib/translate.js +79 -0
  79. package/server/dist/lib/ytmusic.js +0 -2
  80. package/server/dist/providers/title-matching.js +0 -1
  81. package/server/dist/repositories/asmr.repository.js +90 -87
  82. package/server/dist/repositories/insights.repository.js +24 -13
  83. package/server/dist/repositories/library.repository.js +33 -93
  84. package/server/dist/repositories/manga.repository.js +108 -116
  85. package/server/dist/repositories/notifications.repository.js +50 -31
  86. package/server/dist/repositories/queue.repository.js +36 -25
  87. package/server/dist/repositories/settings.repository.js +18 -8
  88. package/server/dist/repositories/shows-meta.repository.js +54 -41
  89. package/server/dist/repositories/temp-show-ids.repository.js +43 -31
  90. package/server/dist/repositories/tv.repository.js +98 -115
  91. package/server/dist/repositories/watched-episodes.repository.js +52 -20
  92. package/server/dist/repositories/watchlist.repository.js +36 -48
  93. package/server/dist/server.js +59 -151
  94. package/server/dist/sync.js +41 -4
  95. package/server/dist/trpc/context.js +7 -0
  96. package/server/dist/trpc/errors.js +21 -0
  97. package/server/dist/trpc/index.js +19 -0
  98. package/server/dist/trpc/router.js +49 -0
  99. package/server/dist/trpc/routers/asmr-library.js +122 -0
  100. package/server/dist/trpc/routers/asmr-progress.js +133 -0
  101. package/server/dist/trpc/routers/asmr.js +87 -0
  102. package/server/dist/trpc/routers/auth.js +247 -0
  103. package/server/dist/trpc/routers/continue-watching.js +83 -0
  104. package/server/dist/trpc/routers/data.js +1039 -0
  105. package/server/dist/trpc/routers/discord.js +288 -0
  106. package/server/dist/trpc/routers/discovery.js +15 -0
  107. package/server/dist/trpc/routers/insights.js +45 -0
  108. package/server/dist/trpc/routers/manga-library.js +280 -0
  109. package/server/dist/trpc/routers/manga-progress.js +181 -0
  110. package/server/dist/trpc/routers/manga.js +201 -0
  111. package/server/dist/{controllers/music.controller.js → trpc/routers/music.js} +155 -288
  112. package/server/dist/trpc/routers/notifications.js +80 -0
  113. package/server/dist/trpc/routers/progress.js +15 -0
  114. package/server/dist/trpc/routers/queue.js +95 -0
  115. package/server/dist/{routes/radio.routes.js → trpc/routers/radio.js} +30 -23
  116. package/server/dist/trpc/routers/settings.js +105 -0
  117. package/server/dist/trpc/routers/tracker.js +219 -0
  118. package/server/dist/trpc/routers/tv-library.js +145 -0
  119. package/server/dist/trpc/routers/tv-progress.js +171 -0
  120. package/server/dist/trpc/routers/tv.js +565 -0
  121. package/server/dist/trpc/routers/watchlist.js +564 -0
  122. package/server/dist/trpc/validation.js +159 -0
  123. package/server/dist/utils/security.utils.js +0 -18
  124. package/server/package.json +5 -10
  125. package/client/dist/assets/Asmr-C28TfSvX.js +0 -2
  126. package/client/dist/assets/AsmrResetProgressModal-6Xh71AgR.js +0 -1
  127. package/client/dist/assets/AvSyncCalibrator-BwCtRW0P.js +0 -10
  128. package/client/dist/assets/Home-BJdbHYja.js +0 -1
  129. package/client/dist/assets/Insights-CjRy42YE.js +0 -1
  130. package/client/dist/assets/MangaReadPage-YCETZRte.js +0 -1
  131. package/client/dist/assets/Mature-BGxRWkV5.js +0 -1
  132. package/client/dist/assets/MediaCard-CAzoxsRS.js +0 -1
  133. package/client/dist/assets/Music-Cgo-yQ7E.js +0 -1
  134. package/client/dist/assets/OptionListMenu--Owe9Jle.js +0 -1
  135. package/client/dist/assets/Player-yRXG2BrP.js +0 -56
  136. package/client/dist/assets/PlayerSettings-DrZLa0CC.js +0 -1
  137. package/client/dist/assets/QueueOptionsButton-BVZyPyKf.js +0 -1
  138. package/client/dist/assets/Radio-BFjxlzuF.js +0 -1
  139. package/client/dist/assets/Search-BOBo0Ogh.js +0 -1
  140. package/client/dist/assets/Settings-DOvNQ0gX.js +0 -21
  141. package/client/dist/assets/Trackers-DZ9KqOWv.js +0 -2
  142. package/client/dist/assets/Tv-5mATEU4p.js +0 -4
  143. package/client/dist/assets/Tv-C2SWcVzN.css +0 -1
  144. package/client/dist/assets/TvInfo-CF-5C5Fs.js +0 -1
  145. package/client/dist/assets/TvPopup-Dfqifj3s.js +0 -1
  146. package/client/dist/assets/Watchlist-B8MHNsAU.js +0 -1
  147. package/client/dist/assets/index-D2RkUJIN.js +0 -178
  148. package/client/dist/assets/useAnimeInfoData-BWShDHqE.js +0 -1
  149. package/client/dist/assets/useAsmr-FyfD9G7b.js +0 -1
  150. package/client/dist/assets/useMangaLibrary-BJU5x8Vz.js +0 -1
  151. package/client/dist/assets/useProviders-DDRxa8vl.js +0 -1
  152. package/client/dist/assets/useTv-Bh6WmNnZ.js +0 -1
  153. package/client/dist/assets/useTvLibrary-_Pqm8re1.js +0 -1
  154. package/server/dist/controllers/asmr-library.controller.js +0 -288
  155. package/server/dist/controllers/auth.controller.js +0 -225
  156. package/server/dist/controllers/data.controller.js +0 -767
  157. package/server/dist/controllers/insights.controller.js +0 -203
  158. package/server/dist/controllers/manga-library.controller.js +0 -350
  159. package/server/dist/controllers/settings.controller.js +0 -609
  160. package/server/dist/controllers/tv-library.controller.js +0 -314
  161. package/server/dist/controllers/watchlist.controller.js +0 -1069
  162. package/server/dist/request-logger.js +0 -16
  163. package/server/dist/routes/asmr-library.routes.js +0 -23
  164. package/server/dist/routes/asmr.routes.js +0 -81
  165. package/server/dist/routes/auth.routes.js +0 -25
  166. package/server/dist/routes/data.routes.js +0 -47
  167. package/server/dist/routes/discord-gateway.routes.js +0 -51
  168. package/server/dist/routes/insights.routes.js +0 -35
  169. package/server/dist/routes/lan-auth.routes.js +0 -46
  170. package/server/dist/routes/manga-library.routes.js +0 -25
  171. package/server/dist/routes/manga.routes.js +0 -141
  172. package/server/dist/routes/music.routes.js +0 -20
  173. package/server/dist/routes/providers.routes.js +0 -17
  174. package/server/dist/routes/proxy.routes.js +0 -12
  175. package/server/dist/routes/settings.routes.js +0 -24
  176. package/server/dist/routes/tracker.routes.js +0 -287
  177. package/server/dist/routes/translate.routes.js +0 -88
  178. package/server/dist/routes/tv-library.routes.js +0 -23
  179. package/server/dist/routes/tv.routes.js +0 -629
  180. package/server/dist/routes/watchlist.routes.js +0 -341
@@ -1,7 +1,5 @@
1
1
  import { gotScraping } from 'got-scraping';
2
2
  import path from 'path';
3
- import { Readable } from 'stream';
4
- import { pipeline } from 'stream/promises';
5
3
  import { AppCache } from '../utils/cache.utils.js';
6
4
  import { CONFIG } from '../config.js';
7
5
  import fs from 'fs';
@@ -84,172 +82,257 @@ function assToVtt(body) {
84
82
  return null;
85
83
  return `WEBVTT\n\n${cues.join('\n\n')}\n`;
86
84
  }
87
- export class ProxyController {
88
- static KWIK_DOMAINS = new Set(['kwik.cx', 'kwik.si', 'kwik.pro']);
89
- static ANIMEPAHE_URL = 'https://animepahe.pw/';
90
- static VAULT_CDN_HOSTS = new Set(['uwucdn.top', 'owocdn.top']);
91
- static GOT_SCRAPING_HOSTS = new Set([
92
- 'kwik.cx',
93
- 'kwik.si',
94
- 'kwik.pro',
95
- 'animepahe.pw',
96
- 'animepahe.ru',
97
- ]);
98
- static GOT_SCRAPING_SUFFIXES = [
99
- '.uwucdn.top',
100
- '.owocdn.top',
101
- '.streamzone1.site',
102
- '.nekostream.site',
103
- '.nukitashith.top',
104
- '.aniwatchtv.site',
105
- ];
106
- static HANIME_HOSTS = new Set(['r2.1hanime.com', '1.1hanime.com']);
107
- static OPPAI_HOSTS = new Set(['myspacecat.pictures']);
108
- static KAA_HOSTS = new Set([
109
- 'hls.krussdomi.com',
110
- 'subst.krussdomi.com',
111
- 'krussdomi.com',
112
- 'st1.advancedairesearchlab.xyz',
113
- 'st1.habibikun.xyz',
114
- 'st1.babybayw.xyz',
115
- 'st1.narutokun.xyz',
116
- ]);
117
- static KAA_REFERER = 'https://krussdomi.com/';
118
- static KAA_ORIGIN = 'https://krussdomi.com';
119
- static ANILIGHT_REFERER = 'https://anilight.live/';
120
- static ANILIGHT_ORIGIN = 'https://anilight.live';
121
- static ANILIGHT_HOSTS = [
122
- 'anilight.live',
123
- 'api.anilight.live',
124
- 'lostproject.club',
125
- 'streamzone1.site',
126
- 'nukitashi.top',
127
- 'vivibebe.site',
128
- 'vibeplayer.site',
129
- 'aniwatchtv.site',
130
- ];
131
- static NEXABLOOM_FAMILY_SUFFIXES = [
132
- '.quavex.top',
133
- '.nexabloom.top',
134
- '.hiddenvertex.top',
135
- '.ironhorizon.top',
136
- '.solarhaven.top',
137
- ];
138
- static NEXABLOOM_FALLBACK_HOSTS = ['fetch.nexabloom.top'];
139
- static nexabloomFallbackUrls(urlStr) {
140
- let host = '';
85
+ const KWIK_DOMAINS = new Set(['kwik.cx', 'kwik.si', 'kwik.pro']);
86
+ const ANIMEPAHE_URL = 'https://animepahe.pw/';
87
+ const VAULT_CDN_HOSTS = new Set(['uwucdn.top', 'owocdn.top']);
88
+ const GOT_SCRAPING_HOSTS = new Set([
89
+ 'kwik.cx',
90
+ 'kwik.si',
91
+ 'kwik.pro',
92
+ 'animepahe.pw',
93
+ 'animepahe.ru',
94
+ ]);
95
+ const GOT_SCRAPING_SUFFIXES = [
96
+ '.uwucdn.top',
97
+ '.owocdn.top',
98
+ '.streamzone1.site',
99
+ '.nekostream.site',
100
+ '.nukitashith.top',
101
+ '.aniwatchtv.site',
102
+ ];
103
+ const HANIME_HOSTS = new Set(['r2.1hanime.com', '1.1hanime.com']);
104
+ const OPPAI_HOSTS = new Set(['myspacecat.pictures']);
105
+ const KAA_HOSTS = new Set([
106
+ 'hls.krussdomi.com',
107
+ 'subst.krussdomi.com',
108
+ 'krussdomi.com',
109
+ 'st1.advancedairesearchlab.xyz',
110
+ 'st1.habibikun.xyz',
111
+ 'st1.babybayw.xyz',
112
+ 'st1.narutokun.xyz',
113
+ ]);
114
+ const KAA_REFERER = 'https://krussdomi.com/';
115
+ const KAA_ORIGIN = 'https://krussdomi.com';
116
+ const ANILIGHT_REFERER = 'https://anilight.live/';
117
+ const ANILIGHT_ORIGIN = 'https://anilight.live';
118
+ const ANILIGHT_HOSTS = [
119
+ 'anilight.live',
120
+ 'api.anilight.live',
121
+ 'lostproject.club',
122
+ 'streamzone1.site',
123
+ 'nukitashi.top',
124
+ 'vivibebe.site',
125
+ 'vibeplayer.site',
126
+ 'aniwatchtv.site',
127
+ ];
128
+ const NEXABLOOM_FAMILY_SUFFIXES = [
129
+ '.quavex.top',
130
+ '.nexabloom.top',
131
+ '.hiddenvertex.top',
132
+ '.ironhorizon.top',
133
+ '.solarhaven.top',
134
+ ];
135
+ const NEXABLOOM_FALLBACK_HOSTS = ['fetch.nexabloom.top'];
136
+ function nexabloomFallbackUrls(urlStr) {
137
+ let host = '';
138
+ try {
139
+ host = new URL(urlStr).hostname.toLowerCase();
140
+ }
141
+ catch {
142
+ return [urlStr];
143
+ }
144
+ if (!NEXABLOOM_FAMILY_SUFFIXES.some((s) => host.endsWith(s))) {
145
+ return [urlStr];
146
+ }
147
+ const urls = [urlStr];
148
+ for (const fallback of NEXABLOOM_FALLBACK_HOSTS) {
149
+ if (fallback === host)
150
+ continue;
141
151
  try {
142
- host = new URL(urlStr).hostname.toLowerCase();
152
+ const candidate = new URL(urlStr);
153
+ candidate.hostname = fallback;
154
+ urls.push(candidate.href);
143
155
  }
144
156
  catch {
145
- return [urlStr];
146
- }
147
- if (!ProxyController.NEXABLOOM_FAMILY_SUFFIXES.some((s) => host.endsWith(s))) {
148
- return [urlStr];
157
+ // ignore
149
158
  }
150
- const urls = [urlStr];
151
- for (const fallback of ProxyController.NEXABLOOM_FALLBACK_HOSTS) {
152
- if (fallback === host)
153
- continue;
154
- try {
155
- const candidate = new URL(urlStr);
156
- candidate.hostname = fallback;
157
- urls.push(candidate.href);
158
- }
159
- catch {
160
- // ignore
161
- }
162
- }
163
- return urls;
164
159
  }
165
- static isGotScrapingHost(urlStr) {
166
- try {
167
- const host = new URL(urlStr).hostname.toLowerCase();
168
- if (ProxyController.GOT_SCRAPING_HOSTS.has(host))
169
- return true;
170
- if (ProxyController.HANIME_HOSTS.has(host))
171
- return true;
172
- return ProxyController.GOT_SCRAPING_SUFFIXES.some((s) => host.endsWith(s));
160
+ return urls;
161
+ }
162
+ function isGotScrapingHost(urlStr) {
163
+ try {
164
+ const host = new URL(urlStr).hostname.toLowerCase();
165
+ if (GOT_SCRAPING_HOSTS.has(host))
166
+ return true;
167
+ if (HANIME_HOSTS.has(host))
168
+ return true;
169
+ return GOT_SCRAPING_SUFFIXES.some((s) => host.endsWith(s));
170
+ }
171
+ catch {
172
+ return false;
173
+ }
174
+ }
175
+ function validateKwikUrl(value) {
176
+ if (typeof value !== 'string')
177
+ return null;
178
+ try {
179
+ const url = new URL(value);
180
+ const isSecure = url.protocol === 'https:';
181
+ const isKwik = KWIK_DOMAINS.has(url.hostname.toLowerCase());
182
+ const isEmbedPath = /^\/e\/[A-Za-z0-9_-]+$/.test(url.pathname);
183
+ const noAuthOrQuery = !url.username && !url.password && !url.search && !url.hash;
184
+ return isSecure && isKwik && isEmbedPath && noAuthOrQuery ? url : null;
185
+ }
186
+ catch {
187
+ return null;
188
+ }
189
+ }
190
+ function pinVariant(body, idx) {
191
+ const lines = body.split('\n');
192
+ let streamIdx = -1;
193
+ let skipUri = false;
194
+ const out = [];
195
+ for (const line of lines) {
196
+ const trimmed = line.trim();
197
+ if (trimmed.startsWith('#EXT-X-STREAM-INF')) {
198
+ streamIdx++;
199
+ skipUri = streamIdx !== idx;
200
+ if (!skipUri)
201
+ out.push(line);
202
+ continue;
173
203
  }
174
- catch {
175
- return false;
204
+ if (skipUri && trimmed && !trimmed.startsWith('#')) {
205
+ skipUri = false;
206
+ continue;
176
207
  }
208
+ out.push(line);
177
209
  }
178
- abortWhenClientLeaves(res, abortController) {
179
- res.on('close', () => {
180
- if (!res.writableEnded) {
181
- abortController.abort();
210
+ if (streamIdx < 0 || idx > streamIdx)
211
+ return null;
212
+ return out.join('\n');
213
+ }
214
+ function applyKwikPatches(html, kwikUrl, cookieStr = '') {
215
+ const safeReferer = JSON.stringify(kwikUrl.href).replace(/</g, '\\u003c');
216
+ const safeCookie = JSON.stringify(cookieStr).replace(/</g, '\\u003c');
217
+ const patched = html.replace(/\b(src|href|url)\s*[=:]\s*(["']?)(\/\/[^"'>)]+|\/(?!\/)[^"'>)]*)\2/gi, (match, attr, quote, path) => {
218
+ const full = path.startsWith('//') ? `https:${path}` : `${kwikUrl.origin}${path}`;
219
+ const prefix = match.startsWith('url') ? `url(` : `${attr}=`;
220
+ const suffix = match.startsWith('url') ? `)` : '';
221
+ return `${prefix}${quote}${full}${quote}${suffix}`;
222
+ });
223
+ const iconProxy = `/api/proxy?url=${encodeURIComponent(`${kwikUrl.origin}/app/js/vendor/plyr.svg`)}&referer=${encodeURIComponent(kwikUrl.href)}`;
224
+ const plyrPatch = `<script>if(window.Plyr) Plyr.defaults.iconUrl=${JSON.stringify(iconProxy).replace(/</g, '\\u003c')};</script>`;
225
+ const hlsPatch = `<script>
226
+ (function() {
227
+ var hook = function() {
228
+ if (!window.Hls) return;
229
+ var original = Hls.prototype.loadSource;
230
+ Hls.prototype.loadSource = function(src) {
231
+ if (typeof src === 'string' && src.includes('.m3u8')) {
232
+ src = window.location.origin + '/api/proxy?url=' + encodeURIComponent(src) + '&referer=' + encodeURIComponent(${safeReferer}) + (${safeCookie} ? '&cookie=' + encodeURIComponent(${safeCookie}) : '');
182
233
  }
234
+ return original.call(this, src);
235
+ };
236
+ };
237
+ if (window.Hls) hook();
238
+ else {
239
+ var observer = new MutationObserver(function() {
240
+ if (window.Hls) { hook(); observer.disconnect(); }
241
+ });
242
+ observer.observe(document.documentElement, { childList: true, subtree: true });
243
+ }
244
+ })();
245
+ </script>`;
246
+ const endBridgePatch = `<script>
247
+ (function() {
248
+ var notified = false;
249
+ var notifyEnded = function() {
250
+ if (notified) return;
251
+ notified = true;
252
+ window.parent.postMessage({ type: 'ANI_WEB_MEDIA_ENDED' }, window.location.origin);
253
+ };
254
+
255
+ var attachToVideos = function(root) {
256
+ var scope = root || document;
257
+ var videos = scope.querySelectorAll ? scope.querySelectorAll('video') : [];
258
+ Array.prototype.forEach.call(videos, function(video) {
259
+ if (video.dataset && video.dataset.aniWebEndedBridge === 'true') return;
260
+ if (video.dataset) video.dataset.aniWebEndedBridge = 'true';
261
+ video.addEventListener('ended', notifyEnded, { once: true });
262
+ });
263
+ };
264
+
265
+ attachToVideos(document);
266
+
267
+ var observer = new MutationObserver(function() {
268
+ attachToVideos(document);
183
269
  });
184
- }
185
- validateKwikUrl(value) {
186
- if (typeof value !== 'string')
187
- return null;
270
+
271
+ observer.observe(document.documentElement, { childList: true, subtree: true });
272
+ })();
273
+ </script>`;
274
+ const beforePlyr = patched.replace(/(<script[^>]+\/plyr\.min\.js[^>]*><\/script>)/i, `$1${plyrPatch}`);
275
+ const final = beforePlyr
276
+ .replace(/(<script[^>]+hls(?:\.min)?\.js[^>]*><\/script>)/i, `$1${hlsPatch}`)
277
+ .replace(/<\/body>/i, `${endBridgePatch}</body>`);
278
+ return final === html ? null : final;
279
+ }
280
+ function placeholderSvg() {
281
+ const possiblePaths = [
282
+ path.join(CONFIG.PACKAGE_ROOT, 'client/public/placeholder.svg'),
283
+ path.join(CONFIG.PACKAGE_ROOT, 'client/dist/placeholder.svg'),
284
+ path.join(CONFIG.SERVER_ROOT, '..', 'client/public/placeholder.svg'),
285
+ ];
286
+ for (const p of possiblePaths) {
188
287
  try {
189
- const url = new URL(value);
190
- const isSecure = url.protocol === 'https:';
191
- const isKwik = ProxyController.KWIK_DOMAINS.has(url.hostname.toLowerCase());
192
- const isEmbedPath = /^\/e\/[A-Za-z0-9_-]+$/.test(url.pathname);
193
- const noAuthOrQuery = !url.username && !url.password && !url.search && !url.hash;
194
- return isSecure && isKwik && isEmbedPath && noAuthOrQuery ? url : null;
288
+ if (fs.existsSync(p)) {
289
+ const headers = new Headers();
290
+ headers.set('Cache-Control', 'no-store');
291
+ headers.set('Content-Type', 'image/svg+xml');
292
+ return new Response(fs.readFileSync(p), { headers });
293
+ }
195
294
  }
196
295
  catch {
197
- return null;
198
- }
199
- }
200
- pinVariant(body, idx) {
201
- const lines = body.split('\n');
202
- let streamIdx = -1;
203
- let skipUri = false;
204
- const out = [];
205
- for (const line of lines) {
206
- const trimmed = line.trim();
207
- if (trimmed.startsWith('#EXT-X-STREAM-INF')) {
208
- streamIdx++;
209
- skipUri = streamIdx !== idx;
210
- if (!skipUri)
211
- out.push(line);
212
- continue;
213
- }
214
- if (skipUri && trimmed && !trimmed.startsWith('#')) {
215
- skipUri = false;
216
- continue;
217
- }
218
- out.push(line);
296
+ // ignore
219
297
  }
220
- if (streamIdx < 0 || idx > streamIdx)
221
- return null;
222
- return out.join('\n');
223
298
  }
224
- handleProxy = async (req, res) => {
225
- const { url, referer, cookie } = req.query;
299
+ return null;
300
+ }
301
+ const PROXY_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0';
302
+ function linkAbort(signal, abort) {
303
+ signal.addEventListener('abort', () => abort.abort(), { once: true });
304
+ }
305
+ export function registerProxy(app) {
306
+ app.get('/api/proxy', async (c) => {
307
+ const url = c.req.query('url');
226
308
  if (!url)
227
- return res.status(400).send('URL required');
309
+ return c.text('URL required', 400);
228
310
  const safeCheck = isSafeExternalUrl(url);
229
311
  if (!safeCheck.safe) {
230
- return res.status(400).send(safeCheck.error || 'Invalid URL');
312
+ return c.text(safeCheck.error || 'Invalid URL', 400);
231
313
  }
232
314
  const urlStr = url;
233
- const refererStr = referer || '';
234
- const cookieStr = cookie || '';
235
- const variantIdx = req.query.variant !== undefined ? parseInt(req.query.variant, 10) : NaN;
236
- const pinVariant = Number.isInteger(variantIdx) && variantIdx >= 0;
237
- const cacheKey = `m3u8-${urlStr}-${refererStr}${pinVariant ? `-v${variantIdx}` : ''}`;
238
- const abortController = new AbortController();
239
- this.abortWhenClientLeaves(res, abortController);
315
+ const refererStr = c.req.query('referer') || '';
316
+ const cookieStr = c.req.query('cookie') || '';
317
+ const variantRaw = c.req.query('variant');
318
+ const variantIdx = variantRaw !== undefined ? parseInt(variantRaw, 10) : NaN;
319
+ const pinVar = Number.isInteger(variantIdx) && variantIdx >= 0;
320
+ const cacheKey = `m3u8-${urlStr}-${refererStr}${pinVar ? `-v${variantIdx}` : ''}`;
321
+ const abort = new AbortController();
322
+ linkAbort(c.req.raw.signal, abort);
240
323
  try {
241
- const isVaultCdn = Array.from(ProxyController.VAULT_CDN_HOSTS).some((host) => urlStr.includes(host));
242
- const isHanime = Array.from(ProxyController.HANIME_HOSTS).some((host) => urlStr.includes(host));
243
- const isOppai = Array.from(ProxyController.OPPAI_HOSTS).some((host) => urlStr.includes(host));
244
- const isKaa = refererStr.startsWith(ProxyController.KAA_ORIGIN) ||
245
- Array.from(ProxyController.KAA_HOSTS).some((host) => urlStr.includes(host));
324
+ const isVaultCdn = Array.from(VAULT_CDN_HOSTS).some((host) => urlStr.includes(host));
325
+ const isHanime = Array.from(HANIME_HOSTS).some((host) => urlStr.includes(host));
326
+ const isOppai = Array.from(OPPAI_HOSTS).some((host) => urlStr.includes(host));
327
+ const isKaa = refererStr.startsWith(KAA_ORIGIN) ||
328
+ Array.from(KAA_HOSTS).some((host) => urlStr.includes(host));
246
329
  const headers = {
247
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0',
330
+ 'User-Agent': PROXY_UA,
248
331
  };
249
- if (referer)
332
+ if (refererStr)
250
333
  headers['Referer'] = refererStr;
251
334
  if (isVaultCdn) {
252
- headers['Origin'] = refererStr || ProxyController.ANIMEPAHE_URL;
335
+ headers['Origin'] = refererStr || ANIMEPAHE_URL;
253
336
  const cookieHeader = buildCfClearanceCookie(cookieStr);
254
337
  if (cookieHeader)
255
338
  headers['Cookie'] = cookieHeader;
@@ -266,8 +349,8 @@ export class ProxyController {
266
349
  }
267
350
  if (isKaa) {
268
351
  if (!headers['Referer'])
269
- headers['Referer'] = refererStr || ProxyController.KAA_REFERER;
270
- headers['Origin'] = ProxyController.KAA_ORIGIN;
352
+ headers['Referer'] = refererStr || KAA_REFERER;
353
+ headers['Origin'] = KAA_ORIGIN;
271
354
  }
272
355
  const isMegaplay = refererStr.startsWith(MEGAPLAY_ORIGIN) || urlStr.includes('nexabloom.top');
273
356
  if (isMegaplay && !headers['Origin']) {
@@ -279,23 +362,24 @@ export class ProxyController {
279
362
  const jasmrCookieHeader = buildCfClearanceCookie(cookieStr);
280
363
  if (jasmrCookieHeader)
281
364
  headers['Cookie'] = jasmrCookieHeader;
282
- const jasmrUa = req.query.ua || '';
365
+ const jasmrUa = c.req.query('ua') || '';
283
366
  if (jasmrUa)
284
367
  headers['User-Agent'] = jasmrUa;
285
368
  }
286
- if (req.headers.range)
287
- headers['Range'] = req.headers.range;
369
+ const range = c.req.header('range');
370
+ if (range)
371
+ headers['Range'] = range;
288
372
  if (urlStr.includes('.m3u8')) {
289
373
  const cached = proxyCache.get(cacheKey);
290
374
  if (cached) {
291
- return res
292
- .set('Content-Type', 'application/vnd.apple.mpegurl')
293
- .set('Access-Control-Allow-Origin', '*')
294
- .send(cached);
375
+ const headers = new Headers();
376
+ headers.set('Content-Type', 'application/vnd.apple.mpegurl');
377
+ headers.set('Access-Control-Allow-Origin', '*');
378
+ return new Response(cached, { headers });
295
379
  }
296
380
  const resp = await (async () => {
297
381
  let last = null;
298
- for (const candidate of ProxyController.nexabloomFallbackUrls(urlStr)) {
382
+ for (const candidate of nexabloomFallbackUrls(urlStr)) {
299
383
  const attempt = await gotScraping({
300
384
  url: isNexabloomMasterUrl(candidate) ? signNexabloomMasterUrl(candidate) : candidate,
301
385
  method: 'GET',
@@ -312,18 +396,18 @@ export class ProxyController {
312
396
  return last;
313
397
  })();
314
398
  if (!resp || (resp.statusCode !== 200 && resp.statusCode !== 206)) {
315
- return res.status(resp?.statusCode ?? 502).send('Upstream error');
399
+ return c.text('Upstream error', (resp?.statusCode || 502));
316
400
  }
317
401
  const finalUrl = resp.url || urlStr;
318
402
  const baseUrl = new URL(finalUrl);
319
403
  const proxiedMediaUrl = (targetUrl) => `/api/proxy?url=${encodeURIComponent(targetUrl)}&referer=${encodeURIComponent(refererStr)}` +
320
404
  (cookieStr ? `&cookie=${encodeURIComponent(sanitizeCfClearance(cookieStr))}` : '') +
321
- (req.query.ua ? `&ua=${encodeURIComponent(req.query.ua)}` : '');
405
+ (c.req.query('ua') ? `&ua=${encodeURIComponent(c.req.query('ua'))}` : '');
322
406
  const needsProxy = Boolean(refererStr);
323
407
  const isProxied = (value) => value.includes('/api/proxy');
324
408
  let playlistBody = resp.body;
325
- if (pinVariant) {
326
- const pinned = this.pinVariant(playlistBody, variantIdx);
409
+ if (pinVar) {
410
+ const pinned = pinVariant(playlistBody, variantIdx);
327
411
  if (pinned)
328
412
  playlistBody = pinned;
329
413
  }
@@ -348,17 +432,17 @@ export class ProxyController {
348
432
  })
349
433
  .join('\n');
350
434
  proxyCache.set(cacheKey, rewritten);
351
- res
352
- .set('Content-Type', 'application/vnd.apple.mpegurl')
353
- .set('Access-Control-Allow-Origin', '*')
354
- .send(rewritten);
435
+ const outHeaders = new Headers();
436
+ outHeaders.set('Content-Type', 'application/vnd.apple.mpegurl');
437
+ outHeaders.set('Access-Control-Allow-Origin', '*');
438
+ return new Response(rewritten, { headers: outHeaders });
355
439
  }
356
440
  else {
357
- if (ProxyController.isGotScrapingHost(urlStr)) {
358
- if (req.headers.range)
359
- headers['Range'] = req.headers.range;
441
+ if (isGotScrapingHost(urlStr)) {
442
+ if (range)
443
+ headers['Range'] = range;
360
444
  let resp = null;
361
- for (const candidate of ProxyController.nexabloomFallbackUrls(urlStr)) {
445
+ for (const candidate of nexabloomFallbackUrls(urlStr)) {
362
446
  const attempt = await gotScraping({
363
447
  url: candidate,
364
448
  method: 'GET',
@@ -373,30 +457,34 @@ export class ProxyController {
373
457
  break;
374
458
  }
375
459
  if (!resp || (resp.statusCode !== 200 && resp.statusCode !== 206)) {
376
- return res.status(resp?.statusCode ?? 502).send('Upstream error');
460
+ return c.text('Upstream error', (resp?.statusCode || 502));
377
461
  }
378
462
  const ct = resp.headers['content-type'];
379
463
  const isVtt = urlStr.endsWith('.vtt') || urlStr.includes('.vtt?');
380
- res.set('Content-Type', isVtt ? 'text/vtt; charset=utf-8' : ct || 'application/octet-stream');
464
+ const outHeaders = new Headers();
465
+ outHeaders.set('Content-Type', isVtt ? 'text/vtt; charset=utf-8' : ct || 'application/octet-stream');
381
466
  const cl = resp.headers['content-length'];
382
467
  if (cl)
383
- res.set('Content-Length', cl);
468
+ outHeaders.set('Content-Length', cl);
384
469
  const cr = resp.headers['content-range'];
385
470
  if (cr)
386
- res.set('Content-Range', cr);
471
+ outHeaders.set('Content-Range', cr);
387
472
  const ar = resp.headers['accept-ranges'];
388
473
  if (ar)
389
- res.set('Accept-Ranges', ar);
390
- res.set('Access-Control-Allow-Origin', '*');
391
- res.send(resp.body);
474
+ outHeaders.set('Accept-Ranges', ar);
475
+ outHeaders.set('Access-Control-Allow-Origin', '*');
476
+ return new Response(resp.body, {
477
+ status: resp.statusCode,
478
+ headers: outHeaders,
479
+ });
392
480
  }
393
481
  else {
394
482
  let upstream = null;
395
- for (const candidate of ProxyController.nexabloomFallbackUrls(urlStr)) {
396
- if (abortController.signal.aborted)
483
+ for (const candidate of nexabloomFallbackUrls(urlStr)) {
484
+ if (abort.signal.aborted)
397
485
  break;
398
486
  try {
399
- const attempt = await fetchWithRetry(candidate, { method: 'GET', headers }, { retries: 3, timeoutMs: 30000, signal: abortController.signal });
487
+ const attempt = await fetchWithRetry(candidate, { method: 'GET', headers }, { retries: 3, timeoutMs: 30000, signal: abort.signal });
400
488
  upstream = attempt;
401
489
  if (attempt.status === 200 || attempt.status === 206)
402
490
  break;
@@ -412,7 +500,7 @@ export class ProxyController {
412
500
  }
413
501
  }
414
502
  if (!upstream) {
415
- return res.status(502).send('Upstream error');
503
+ return c.text('Upstream error', 502);
416
504
  }
417
505
  const status = upstream.status;
418
506
  if (status !== 200 && status !== 206) {
@@ -422,60 +510,47 @@ export class ProxyController {
422
510
  catch {
423
511
  // ignore
424
512
  }
425
- return res.status(status).send('Upstream error');
513
+ return c.text('Upstream error', status);
426
514
  }
427
- res.status(status);
428
515
  const isVtt = urlStr.endsWith('.vtt') || urlStr.includes('.vtt?');
429
516
  const ct = upstream.headers.get('content-type');
430
- res.set('Content-Type', isVtt ? 'text/vtt; charset=utf-8' : ct || 'application/octet-stream');
517
+ const outHeaders = new Headers();
518
+ outHeaders.set('Content-Type', isVtt ? 'text/vtt; charset=utf-8' : ct || 'application/octet-stream');
431
519
  const cl = upstream.headers.get('content-length');
432
520
  if (cl)
433
- res.set('Content-Length', cl);
521
+ outHeaders.set('Content-Length', cl);
434
522
  const cr = upstream.headers.get('content-range');
435
523
  if (cr)
436
- res.set('Content-Range', cr);
524
+ outHeaders.set('Content-Range', cr);
437
525
  const ar = upstream.headers.get('accept-ranges');
438
526
  if (ar)
439
- res.set('Accept-Ranges', ar);
440
- res.set('Access-Control-Allow-Origin', '*');
527
+ outHeaders.set('Accept-Ranges', ar);
528
+ outHeaders.set('Access-Control-Allow-Origin', '*');
441
529
  if (!upstream.body) {
442
- return res.status(502).send('Upstream error');
443
- }
444
- try {
445
- await pipeline(Readable.from(upstream.body), res);
446
- }
447
- catch (e) {
448
- if (!res.headersSent) {
449
- logger.error({ url: urlStr, error: e?.message }, '[proxy] upstream stream failed');
450
- res.status(500).send('Proxy error');
451
- }
452
- else {
453
- res.destroy();
454
- }
530
+ return c.text('Upstream error', 502);
455
531
  }
532
+ return new Response(upstream.body, { status: status, headers: outHeaders });
456
533
  }
457
534
  }
458
535
  }
459
536
  catch (e) {
460
- if (abortController.signal.aborted)
461
- return;
462
- if (!res.headersSent) {
463
- logger.error({ url: urlStr, error: e?.message }, '[proxy] upstream request failed');
464
- res.status(500).send('Proxy error');
465
- }
537
+ if (abort.signal.aborted)
538
+ return new Response(null, { status: 499 });
539
+ logger.error({ url: urlStr, error: e?.message }, '[proxy] upstream request failed');
540
+ return c.text('Proxy error', 500);
466
541
  }
467
- };
468
- handleEmbedProxy = async (req, res) => {
469
- const kwikUrl = this.validateKwikUrl(req.query.url);
542
+ });
543
+ app.get('/api/embed-proxy', async (c) => {
544
+ const kwikUrl = validateKwikUrl(c.req.query('url'));
470
545
  if (!kwikUrl)
471
- return res.status(400).send('Invalid or unsupported gateway URL');
472
- const cookieStr = req.query.cookie || '';
473
- const abortController = new AbortController();
474
- this.abortWhenClientLeaves(res, abortController);
546
+ return c.text('Invalid or unsupported gateway URL', 400);
547
+ const cookieStr = c.req.query('cookie') || '';
548
+ const abort = new AbortController();
549
+ linkAbort(c.req.raw.signal, abort);
475
550
  try {
476
551
  const headers = {
477
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0',
478
- Referer: ProxyController.ANIMEPAHE_URL,
552
+ 'User-Agent': PROXY_UA,
553
+ Referer: ANIMEPAHE_URL,
479
554
  Origin: 'https://animepahe.pw',
480
555
  };
481
556
  if (cookieStr) {
@@ -493,102 +568,33 @@ export class ProxyController {
493
568
  throwHttpErrors: false,
494
569
  });
495
570
  const originalHtml = response.body;
496
- const patched = this.applyKwikPatches(originalHtml, kwikUrl, cookieStr);
571
+ const patched = applyKwikPatches(originalHtml, kwikUrl, cookieStr);
497
572
  if (!patched)
498
- return res.status(502).send('Failed to patch video gateway');
499
- return res
500
- .status(200)
501
- .set('Content-Type', 'text/html; charset=utf-8')
502
- .set('Cache-Control', 'private, max-age=120')
503
- .send(patched);
573
+ return c.text('Failed to patch video gateway', 502);
574
+ const outHeaders = new Headers();
575
+ outHeaders.set('Content-Type', 'text/html; charset=utf-8');
576
+ outHeaders.set('Cache-Control', 'private, max-age=120');
577
+ return new Response(patched, { status: 200, headers: outHeaders });
504
578
  }
505
- catch (e) {
506
- if (abortController.signal.aborted)
507
- return;
508
- if (!res.headersSent)
509
- res.status(502).send('Gateway proxy error');
510
- }
511
- };
512
- applyKwikPatches(html, kwikUrl, cookieStr = '') {
513
- const safeReferer = JSON.stringify(kwikUrl.href).replace(/</g, '\\u003c');
514
- const safeCookie = JSON.stringify(cookieStr).replace(/</g, '\\u003c');
515
- const patched = html.replace(/\b(src|href|url)\s*[=:]\s*(["']?)(\/\/[^"'>)]+|\/(?!\/)[^"'>)]*)\2/gi, (match, attr, quote, path) => {
516
- const full = path.startsWith('//') ? `https:${path}` : `${kwikUrl.origin}${path}`;
517
- const prefix = match.startsWith('url') ? `url(` : `${attr}=`;
518
- const suffix = match.startsWith('url') ? `)` : '';
519
- return `${prefix}${quote}${full}${quote}${suffix}`;
520
- });
521
- const iconProxy = `/api/proxy?url=${encodeURIComponent(`${kwikUrl.origin}/app/js/vendor/plyr.svg`)}&referer=${encodeURIComponent(kwikUrl.href)}`;
522
- const plyrPatch = `<script>if(window.Plyr) Plyr.defaults.iconUrl=${JSON.stringify(iconProxy).replace(/</g, '\\u003c')};</script>`;
523
- const hlsPatch = `<script>
524
- (function() {
525
- var hook = function() {
526
- if (!window.Hls) return;
527
- var original = Hls.prototype.loadSource;
528
- Hls.prototype.loadSource = function(src) {
529
- if (typeof src === 'string' && src.includes('.m3u8')) {
530
- src = window.location.origin + '/api/proxy?url=' + encodeURIComponent(src) + '&referer=' + encodeURIComponent(${safeReferer}) + (${safeCookie} ? '&cookie=' + encodeURIComponent(${safeCookie}) : '');
531
- }
532
- return original.call(this, src);
533
- };
534
- };
535
- if (window.Hls) hook();
536
- else {
537
- var observer = new MutationObserver(function() {
538
- if (window.Hls) { hook(); observer.disconnect(); }
539
- });
540
- observer.observe(document.documentElement, { childList: true, subtree: true });
579
+ catch {
580
+ if (abort.signal.aborted)
581
+ return new Response(null, { status: 499 });
582
+ return c.text('Gateway proxy error', 502);
541
583
  }
542
- })();
543
- </script>`;
544
- const endBridgePatch = `<script>
545
- (function() {
546
- var notified = false;
547
- var notifyEnded = function() {
548
- if (notified) return;
549
- notified = true;
550
- window.parent.postMessage({ type: 'ANI_WEB_MEDIA_ENDED' }, window.location.origin);
551
- };
552
-
553
- var attachToVideos = function(root) {
554
- var scope = root || document;
555
- var videos = scope.querySelectorAll ? scope.querySelectorAll('video') : [];
556
- Array.prototype.forEach.call(videos, function(video) {
557
- if (video.dataset && video.dataset.aniWebEndedBridge === 'true') return;
558
- if (video.dataset) video.dataset.aniWebEndedBridge = 'true';
559
- video.addEventListener('ended', notifyEnded, { once: true });
560
- });
561
- };
562
-
563
- attachToVideos(document);
564
-
565
- var observer = new MutationObserver(function() {
566
- attachToVideos(document);
567
- });
568
-
569
- observer.observe(document.documentElement, { childList: true, subtree: true });
570
- })();
571
- </script>`;
572
- const beforePlyr = patched.replace(/(<script[^>]+\/plyr\.min\.js[^>]*><\/script>)/i, `$1${plyrPatch}`);
573
- const final = beforePlyr
574
- .replace(/(<script[^>]+hls(?:\.min)?\.js[^>]*><\/script>)/i, `$1${hlsPatch}`)
575
- .replace(/<\/body>/i, `${endBridgePatch}</body>`);
576
- return final === html ? null : final;
577
- }
578
- handleSubtitleProxy = async (req, res) => {
579
- const { url, referer } = req.query;
584
+ });
585
+ app.get('/api/subtitle-proxy', async (c) => {
586
+ const url = c.req.query('url');
580
587
  if (!url)
581
- return res.status(400).send('URL required');
588
+ return c.text('URL required', 400);
582
589
  const safeCheck = isSafeExternalUrl(url);
583
590
  if (!safeCheck.safe) {
584
- return res.status(400).send(safeCheck.error || 'Invalid URL');
591
+ return c.text(safeCheck.error || 'Invalid URL', 400);
585
592
  }
586
- const abortController = new AbortController();
587
- this.abortWhenClientLeaves(res, abortController);
593
+ const abort = new AbortController();
594
+ linkAbort(c.req.raw.signal, abort);
588
595
  const subUrl = url;
589
- const refererStr = referer || '';
590
- const isAnilight = refererStr.includes('anilight.live') ||
591
- ProxyController.ANILIGHT_HOSTS.some((host) => subUrl.includes(host));
596
+ const refererStr = c.req.query('referer') || '';
597
+ const isAnilight = refererStr.includes('anilight.live') || ANILIGHT_HOSTS.some((host) => subUrl.includes(host));
592
598
  const innerRawUrl = (() => {
593
599
  try {
594
600
  const parsed = new URL(subUrl);
@@ -614,25 +620,25 @@ export class ProxyController {
614
620
  headers['Referer'] = refererStr;
615
621
  }
616
622
  else if (isAnilight) {
617
- headers['Referer'] = ProxyController.ANILIGHT_REFERER;
623
+ headers['Referer'] = ANILIGHT_REFERER;
618
624
  }
619
- if (refererStr.startsWith(ProxyController.KAA_ORIGIN) ||
620
- Array.from(ProxyController.KAA_HOSTS).some((host) => subUrl.includes(host))) {
621
- headers['Origin'] = ProxyController.KAA_ORIGIN;
625
+ if (refererStr.startsWith(KAA_ORIGIN) ||
626
+ Array.from(KAA_HOSTS).some((host) => subUrl.includes(host))) {
627
+ headers['Origin'] = KAA_ORIGIN;
622
628
  }
623
629
  else if (isAnilight) {
624
- headers['Origin'] = ProxyController.ANILIGHT_ORIGIN;
630
+ headers['Origin'] = ANILIGHT_ORIGIN;
625
631
  }
626
632
  return headers;
627
633
  };
628
634
  const fetchText = async (target, retries = 1) => {
629
635
  const headers = buildHeaders();
630
636
  if (innerRawUrl && target === innerRawUrl) {
631
- headers['Referer'] = ProxyController.ANILIGHT_REFERER;
632
- headers['Origin'] = ProxyController.ANILIGHT_ORIGIN;
637
+ headers['Referer'] = ANILIGHT_REFERER;
638
+ headers['Origin'] = ANILIGHT_ORIGIN;
633
639
  }
634
640
  try {
635
- const upstream = await fetchWithRetry(target, { method: 'GET', headers }, { retries, timeoutMs: 15000, signal: abortController.signal });
641
+ const upstream = await fetchWithRetry(target, { method: 'GET', headers }, { retries, timeoutMs: 15000, signal: abort.signal });
636
642
  if (!upstream.ok) {
637
643
  logger.warn({ target, status: upstream.status }, '[subtitle-proxy] upstream returned error status');
638
644
  try {
@@ -651,26 +657,34 @@ export class ProxyController {
651
657
  return { status: upstream.status, body };
652
658
  }
653
659
  catch (e) {
654
- if (abortController.signal.aborted)
660
+ if (abort.signal.aborted)
655
661
  return null;
656
662
  logger.warn({ target, error: e?.message }, '[subtitle-proxy] upstream fetch failed');
657
663
  return { status: 502, body: '' };
658
664
  }
659
665
  };
666
+ const vttHeaders = (cacheControl) => {
667
+ const headers = new Headers();
668
+ headers.set('Content-Type', 'text/vtt; charset=utf-8');
669
+ headers.set('Access-Control-Allow-Origin', '*');
670
+ headers.set('Cache-Control', cacheControl);
671
+ return headers;
672
+ };
660
673
  try {
661
674
  const headers = buildHeaders();
662
675
  let result = await fetchText(subUrl, 1);
663
- if (abortController.signal.aborted)
664
- return;
676
+ if (abort.signal.aborted)
677
+ return new Response(null, { status: 499 });
665
678
  if ((!result || !result.body) && innerRawUrl) {
666
679
  result = await fetchText(innerRawUrl, 0);
667
- if (abortController.signal.aborted)
668
- return;
680
+ if (abort.signal.aborted)
681
+ return new Response(null, { status: 499 });
669
682
  }
670
683
  if (!result || !result.body) {
671
- res.set('Access-Control-Allow-Origin', '*');
672
- res.set('Cache-Control', 'no-store');
673
- return res.status(502).send('Subtitle upstream error');
684
+ const headers = new Headers();
685
+ headers.set('Access-Control-Allow-Origin', '*');
686
+ headers.set('Cache-Control', 'no-store');
687
+ return new Response('Subtitle upstream error', { status: 502, headers });
674
688
  }
675
689
  const body = result.body;
676
690
  const baseForSegments = innerRawUrl || subUrl;
@@ -692,7 +706,7 @@ export class ProxyController {
692
706
  const parts = [];
693
707
  for (const seg of segUrls) {
694
708
  try {
695
- const segRes = await fetchWithRetry(seg, { method: 'GET', headers }, { retries: 1, timeoutMs: 15000, signal: abortController.signal });
709
+ const segRes = await fetchWithRetry(seg, { method: 'GET', headers }, { retries: 1, timeoutMs: 15000, signal: abort.signal });
696
710
  if (!segRes.ok)
697
711
  continue;
698
712
  let segBody = (await segRes.text()).replace(/^\uFEFF/, '');
@@ -710,53 +724,53 @@ export class ProxyController {
710
724
  if (parts.join('\n').length > 2_000_000)
711
725
  break;
712
726
  }
713
- catch (e) {
714
- if (abortController.signal.aborted)
715
- return;
727
+ catch {
728
+ if (abort.signal.aborted)
729
+ return new Response(null, { status: 499 });
716
730
  }
717
731
  }
718
732
  if (parts.length === 0) {
719
- res.set('Access-Control-Allow-Origin', '*');
720
- res.set('Cache-Control', 'no-store');
721
- return res.status(502).send('Subtitle playlist empty');
733
+ const headers = new Headers();
734
+ headers.set('Access-Control-Allow-Origin', '*');
735
+ headers.set('Cache-Control', 'no-store');
736
+ return new Response('Subtitle playlist empty', { status: 502, headers });
722
737
  }
723
- res.set('Content-Type', 'text/vtt; charset=utf-8');
724
- res.set('Access-Control-Allow-Origin', '*');
725
- res.set('Cache-Control', 'public, max-age=86400');
726
- return res.send(`WEBVTT\n\n${parts.join('\n\n')}\n`);
727
- }
728
- res.set('Content-Type', 'text/vtt; charset=utf-8');
729
- res.set('Access-Control-Allow-Origin', '*');
730
- res.set('Cache-Control', 'public, max-age=86400');
731
- if (/^\s*WEBVTT/i.test(body))
732
- return res.send(body);
738
+ return new Response(`WEBVTT\n\n${parts.join('\n\n')}\n`, {
739
+ headers: vttHeaders('public, max-age=86400'),
740
+ });
741
+ }
742
+ if (/^\s*WEBVTT/i.test(body)) {
743
+ return new Response(body, { headers: vttHeaders('public, max-age=86400') });
744
+ }
733
745
  const vttFromAss = assToVtt(body);
734
- if (vttFromAss)
735
- return res.send(vttFromAss);
736
- return res.send(`WEBVTT\n\n${body.replace(/\r\n/g, '\n').replace(/(\d{2}:\d{2}:\d{2}),(\d{3})/g, '$1.$2')}`);
737
- }
738
- catch (e) {
739
- if (abortController.signal.aborted)
740
- return;
741
- logger.warn({ url: subUrl, error: e?.message }, '[subtitle-proxy] failed');
742
- if (!res.headersSent) {
743
- res.set('Access-Control-Allow-Origin', '*');
744
- res.set('Cache-Control', 'no-store');
745
- res.status(502).send('Subtitle upstream error');
746
+ if (vttFromAss) {
747
+ return new Response(vttFromAss, { headers: vttHeaders('public, max-age=86400') });
746
748
  }
749
+ return new Response(`WEBVTT\n\n${body.replace(/\r\n/g, '\n').replace(/(\d{2}:\d{2}:\d{2}),(\d{3})/g, '$1.$2')}`, { headers: vttHeaders('public, max-age=86400') });
747
750
  }
748
- };
749
- handleImageProxy = async (req, res) => {
750
- const { url, cookie, ua } = req.query;
751
+ catch {
752
+ if (abort.signal.aborted)
753
+ return new Response(null, { status: 499 });
754
+ logger.warn({ url: subUrl }, '[subtitle-proxy] failed');
755
+ const headers = new Headers();
756
+ headers.set('Access-Control-Allow-Origin', '*');
757
+ headers.set('Cache-Control', 'no-store');
758
+ return new Response('Subtitle upstream error', { status: 502, headers });
759
+ }
760
+ });
761
+ app.get('/api/image-proxy', async (c) => {
762
+ const url = c.req.query('url');
751
763
  if (!url)
752
- return res.status(400).send('URL required');
764
+ return c.text('URL required', 400);
753
765
  const safeCheck = isSafeExternalUrl(url);
754
766
  if (!safeCheck.safe) {
755
- return res.status(400).send(safeCheck.error || 'Invalid URL');
767
+ return c.text(safeCheck.error || 'Invalid URL', 400);
756
768
  }
757
769
  const targetUrl = url;
758
- const abortController = new AbortController();
759
- this.abortWhenClientLeaves(res, abortController);
770
+ const cookie = c.req.query('cookie') || '';
771
+ const ua = c.req.query('ua') || '';
772
+ const abort = new AbortController();
773
+ linkAbort(c.req.raw.signal, abort);
760
774
  let refererValue = 'https://anilist.co/';
761
775
  const headers = {
762
776
  'User-Agent': ua ||
@@ -798,9 +812,6 @@ export class ProxyController {
798
812
  headers['User-Agent'] = uaParam;
799
813
  }
800
814
  headers['Referer'] = refererValue;
801
- // Use got-scraping for a browser-like TLS fingerprint (needed for hosts
802
- // like i.animepahe.pw that reject stock clients with 403). Retry a
803
- // couple times since Cloudflare can intermittently challenge.
804
815
  let lastStatus = 0;
805
816
  let body = null;
806
817
  let contentType = 'image/webp';
@@ -825,30 +836,34 @@ export class ProxyController {
825
836
  }
826
837
  }
827
838
  if (body) {
828
- res.set('Cache-Control', 'public, max-age=604800, immutable');
829
- res.set('Content-Type', contentType);
830
- res.set('Access-Control-Allow-Origin', '*');
831
- return res.send(body);
832
- }
833
- this.sendPlaceholder(res);
839
+ const outHeaders = new Headers();
840
+ outHeaders.set('Cache-Control', 'public, max-age=604800, immutable');
841
+ outHeaders.set('Content-Type', contentType);
842
+ outHeaders.set('Access-Control-Allow-Origin', '*');
843
+ return new Response(body, { headers: outHeaders });
844
+ }
845
+ const placeholder = placeholderSvg();
846
+ if (placeholder)
847
+ return placeholder;
848
+ return c.text('Not Found', 404);
834
849
  }
835
- catch (e) {
836
- if (abortController.signal.aborted)
837
- return;
838
- const err = e;
839
- if (!res.headersSent) {
840
- this.sendPlaceholder(res);
841
- }
850
+ catch {
851
+ if (abort.signal.aborted)
852
+ return new Response(null, { status: 499 });
853
+ const placeholder = placeholderSvg();
854
+ if (placeholder)
855
+ return placeholder;
856
+ return c.text('Not Found', 404);
842
857
  }
843
- };
844
- handleResolve = async (req, res) => {
845
- const targetUrl = req.query.url;
858
+ });
859
+ app.get('/api/resolve', async (c) => {
860
+ const targetUrl = c.req.query('url');
846
861
  if (!targetUrl || typeof targetUrl !== 'string') {
847
- return res.status(400).json({ error: 'URL required' });
862
+ return c.json({ error: 'URL required' }, 400);
848
863
  }
849
864
  const safeCheck = isSafeExternalUrl(targetUrl);
850
865
  if (!safeCheck.safe) {
851
- return res.status(400).json({ error: safeCheck.error || 'Invalid URL' });
866
+ return c.json({ error: safeCheck.error || 'Invalid URL' }, 400);
852
867
  }
853
868
  try {
854
869
  const response = await fetch(targetUrl, {
@@ -859,33 +874,14 @@ export class ProxyController {
859
874
  },
860
875
  redirect: 'follow',
861
876
  });
862
- res.json({
877
+ return c.json({
863
878
  finalUrl: response.url,
864
879
  status: response.status,
865
880
  });
866
881
  }
867
882
  catch (e) {
868
883
  const err = e;
869
- res.status(500).json({ error: err.message || 'Resolve failed' });
870
- }
871
- };
872
- sendPlaceholder(res) {
873
- if (!res.headersSent)
874
- res.set('Cache-Control', 'no-store');
875
- const possiblePaths = [
876
- path.join(CONFIG.PACKAGE_ROOT, 'client/public/placeholder.svg'),
877
- path.join(CONFIG.PACKAGE_ROOT, 'client/dist/placeholder.svg'),
878
- path.join(CONFIG.SERVER_ROOT, '..', 'client/public/placeholder.svg'),
879
- ];
880
- for (const p of possiblePaths) {
881
- if (fs.existsSync(p)) {
882
- return res.sendFile(p, (err) => {
883
- if (err && !res.headersSent) {
884
- res.status(404).send('Not Found');
885
- }
886
- });
887
- }
884
+ return c.json({ error: err.message || 'Resolve failed' }, 500);
888
885
  }
889
- res.status(404).send('Not Found');
890
- }
886
+ });
891
887
  }