@serifpersia/dango 3.3.8 → 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.
- package/README.md +1 -1
- package/client/dist/assets/{AnimeCard-D4qo4289.js → AnimeCard-CJy1SXH3.js} +1 -1
- package/client/dist/assets/{AnimeInfo-BdRRm4So.js → AnimeInfo-BoXlVvZ-.js} +1 -1
- package/client/dist/assets/{AnimeInfoPage-DeJNAKMR.js → AnimeInfoPage-BxbA_7AV.js} +2 -2
- package/client/dist/assets/Asmr--Zc8G9zv.js +2 -0
- package/client/dist/assets/AsmrResetProgressModal-CKYm-8cp.js +1 -0
- package/client/dist/assets/AvSyncCalibrator-3yjZS7s6.js +10 -0
- package/client/dist/assets/Home-rrhOGQcg.js +1 -0
- package/client/dist/assets/Insights-Bay90Yeg.js +1 -0
- package/client/dist/assets/{LibraryListPage-D3hTqTRU.js → LibraryListPage-HzuDavbb.js} +1 -1
- package/client/dist/assets/{ListeningList-BgWsfu40.js → ListeningList-DYcQnnsG.js} +1 -1
- package/client/dist/assets/{Manga-Dn-CYins.js → Manga-D8CT9aO0.js} +1 -1
- package/client/dist/assets/{MangaInfoPage-EmQoK7Bv.js → MangaInfoPage-DGK6CYp1.js} +1 -1
- package/client/dist/assets/MangaReadPage-B9XoDKF9.js +1 -0
- package/client/dist/assets/{MangaResetProgressModal-Dian37PQ.js → MangaResetProgressModal-QjZyIAjl.js} +1 -1
- package/client/dist/assets/Mature-CHt_-Hz5.js +1 -0
- package/client/dist/assets/MediaCard-B5584Cq9.js +1 -0
- package/client/dist/assets/Music-HeLrEZ2d.js +1 -0
- package/client/dist/assets/OptionListMenu-CJ90CdwI.js +1 -0
- package/client/dist/assets/Player-BYtEax_z.js +56 -0
- package/client/dist/assets/PlayerSettings-4-cUAY4B.js +1 -0
- package/client/dist/assets/QueueOptionsButton-Ba0dg_82.js +1 -0
- package/client/dist/assets/{QueueRail-B7DeSFoA.js → QueueRail-O7f4RqjB.js} +1 -1
- package/client/dist/assets/Radio-DPZxQnyD.js +1 -0
- package/client/dist/assets/{ReadingList-BT8kUjCB.js → ReadingList-n6rr8ygy.js} +1 -1
- package/client/dist/assets/{ResetProgressModal-DWeiO-hs.js → ResetProgressModal-CcStlbhP.js} +1 -1
- package/client/dist/assets/Search-C2wrWOdy.js +1 -0
- package/client/dist/assets/Settings-CluUYs8U.js +21 -0
- package/client/dist/assets/{SkeletonGrid-B5loz7ii.js → SkeletonGrid-CLEPY9nP.js} +1 -1
- package/client/dist/assets/Trackers-CTGPxs0V.js +2 -0
- package/client/dist/assets/Tv-BwHlDdi8.js +4 -0
- package/client/dist/assets/Tv-ZEA45gXK.css +1 -0
- package/client/dist/assets/TvInfo-CXnrLekr.js +1 -0
- package/client/dist/assets/TvPopup-BekfiNcX.js +1 -0
- package/client/dist/assets/{TvSearch-BYwCf5kO.js → TvSearch-DuqR-z3v.js} +1 -1
- package/client/dist/assets/{TvWatchlist-BiJU6pKT.js → TvWatchlist-CBvgFrA0.js} +1 -1
- package/client/dist/assets/Watchlist-mj8mTOam.js +1 -0
- package/client/dist/assets/{hls-C11FOWHj.js → hls-Boje3IOE.js} +1 -1
- package/client/dist/assets/index-BXwhFWB_.js +178 -0
- package/client/dist/assets/useAnimeInfoData-DJ7A4B4T.js +1 -0
- package/client/dist/assets/useAsmr-DIwIsJXF.js +1 -0
- package/client/dist/assets/{useAsmrPopup-cT_Uu41e.js → useAsmrPopup-e4zNN9ig.js} +1 -1
- package/client/dist/assets/useMangaLibrary-C9Lci5N4.js +1 -0
- package/client/dist/assets/{useMangaPopup-PnmSctHu.js → useMangaPopup-B8nnpvyi.js} +1 -1
- package/client/dist/assets/useProviders-B5Qr4I1y.js +1 -0
- package/client/dist/assets/useTv-Dm8MCYlL.js +1 -0
- package/client/dist/assets/useTvLibrary-B7jEk2Q7.js +1 -0
- package/client/dist/assets/{vendor-query-BXRECSI6.js → vendor-query-BUTye89G.js} +1 -1
- package/client/dist/index.html +2 -2
- package/orchestrator.js +1 -2
- package/package.json +2 -6
- package/server/dist/app-auth.js +9 -29
- package/server/dist/app-hono.js +621 -0
- package/server/dist/config.js +0 -1
- package/server/dist/db/drizzle.js +32 -0
- package/server/dist/db/schema-anime.js +64 -0
- package/server/dist/db/schema-asmr.js +25 -0
- package/server/dist/db/schema-manga.js +31 -0
- package/server/dist/db/schema-tv.js +33 -0
- package/server/dist/db.js +75 -4
- package/server/dist/discord-gateway.js +1 -1
- package/server/dist/google.js +0 -7
- package/server/dist/hono/asmr.js +1 -0
- package/server/dist/hono/auth.js +56 -0
- package/server/dist/hono/music.js +419 -0
- package/server/dist/{controllers/proxy.controller.js → hono/proxy.js} +390 -394
- package/server/dist/hono/tv.js +197 -0
- package/server/dist/hono/watchlist.js +733 -0
- package/server/dist/lib/anilist.js +3 -3
- package/server/dist/lib/insights.js +185 -0
- package/server/dist/lib/ipc.js +1 -2
- package/server/dist/lib/kitsu.js +1 -28
- package/server/dist/lib/mal-import.js +291 -0
- package/server/dist/lib/migration.js +20 -34
- package/server/dist/lib/offline-db.js +36 -36
- package/server/dist/lib/tracker/manga-sync.service.js +21 -21
- package/server/dist/lib/tracker/sync.service.js +20 -20
- package/server/dist/lib/translate.js +79 -0
- package/server/dist/lib/ytmusic.js +15 -4
- package/server/dist/providers/title-matching.js +0 -1
- package/server/dist/repositories/asmr.repository.js +90 -87
- package/server/dist/repositories/insights.repository.js +24 -13
- package/server/dist/repositories/library.repository.js +33 -93
- package/server/dist/repositories/manga.repository.js +108 -116
- package/server/dist/repositories/notifications.repository.js +50 -31
- package/server/dist/repositories/queue.repository.js +36 -25
- package/server/dist/repositories/settings.repository.js +18 -8
- package/server/dist/repositories/shows-meta.repository.js +54 -41
- package/server/dist/repositories/temp-show-ids.repository.js +43 -31
- package/server/dist/repositories/tv.repository.js +98 -115
- package/server/dist/repositories/watched-episodes.repository.js +52 -20
- package/server/dist/repositories/watchlist.repository.js +36 -48
- package/server/dist/server.js +59 -151
- package/server/dist/sync.js +41 -4
- package/server/dist/trpc/context.js +7 -0
- package/server/dist/trpc/errors.js +21 -0
- package/server/dist/trpc/index.js +19 -0
- package/server/dist/trpc/router.js +49 -0
- package/server/dist/trpc/routers/asmr-library.js +122 -0
- package/server/dist/trpc/routers/asmr-progress.js +133 -0
- package/server/dist/trpc/routers/asmr.js +87 -0
- package/server/dist/trpc/routers/auth.js +247 -0
- package/server/dist/trpc/routers/continue-watching.js +83 -0
- package/server/dist/trpc/routers/data.js +1039 -0
- package/server/dist/trpc/routers/discord.js +288 -0
- package/server/dist/trpc/routers/discovery.js +15 -0
- package/server/dist/trpc/routers/insights.js +45 -0
- package/server/dist/trpc/routers/manga-library.js +280 -0
- package/server/dist/trpc/routers/manga-progress.js +181 -0
- package/server/dist/trpc/routers/manga.js +201 -0
- package/server/dist/{controllers/music.controller.js → trpc/routers/music.js} +235 -295
- package/server/dist/trpc/routers/notifications.js +80 -0
- package/server/dist/trpc/routers/progress.js +15 -0
- package/server/dist/trpc/routers/queue.js +95 -0
- package/server/dist/{routes/radio.routes.js → trpc/routers/radio.js} +30 -23
- package/server/dist/trpc/routers/settings.js +105 -0
- package/server/dist/trpc/routers/tracker.js +219 -0
- package/server/dist/trpc/routers/tv-library.js +145 -0
- package/server/dist/trpc/routers/tv-progress.js +171 -0
- package/server/dist/trpc/routers/tv.js +565 -0
- package/server/dist/trpc/routers/watchlist.js +564 -0
- package/server/dist/trpc/validation.js +159 -0
- package/server/dist/utils/security.utils.js +0 -18
- package/server/package.json +5 -10
- package/client/dist/assets/Asmr-D0EGRYsv.js +0 -2
- package/client/dist/assets/AsmrResetProgressModal-Gs0GmVaT.js +0 -1
- package/client/dist/assets/AvSyncCalibrator-D8hqarVB.js +0 -10
- package/client/dist/assets/Home-BnBzjFkW.js +0 -1
- package/client/dist/assets/Insights-i9QgY3nE.js +0 -1
- package/client/dist/assets/MangaReadPage-xJB8K2YU.js +0 -1
- package/client/dist/assets/Mature-DQtXKOXc.js +0 -1
- package/client/dist/assets/MediaCard-DpKoY-1j.js +0 -1
- package/client/dist/assets/Music-BP43Zqmq.js +0 -1
- package/client/dist/assets/OptionListMenu-Bj9ebsW3.js +0 -1
- package/client/dist/assets/Player-O2Cy2GF1.js +0 -56
- package/client/dist/assets/PlayerSettings-DYZ272Yp.js +0 -1
- package/client/dist/assets/QueueOptionsButton-lv26-6jp.js +0 -1
- package/client/dist/assets/Radio-qQBYnVfY.js +0 -1
- package/client/dist/assets/Search-BkvPYN33.js +0 -1
- package/client/dist/assets/Settings-CyZEBF6S.js +0 -21
- package/client/dist/assets/Trackers-JK-aXBYC.js +0 -2
- package/client/dist/assets/Tv-BwXWJxMy.js +0 -4
- package/client/dist/assets/Tv-C2SWcVzN.css +0 -1
- package/client/dist/assets/TvInfo-Dzyq6ucH.js +0 -1
- package/client/dist/assets/TvPopup-PlCU30FO.js +0 -1
- package/client/dist/assets/Watchlist-BOEeGXH9.js +0 -1
- package/client/dist/assets/index-CtoKRNLc.js +0 -178
- package/client/dist/assets/useAnimeInfoData-kr3WOIWH.js +0 -1
- package/client/dist/assets/useAsmr-CB9THf0m.js +0 -1
- package/client/dist/assets/useMangaLibrary-rM6z3_0E.js +0 -1
- package/client/dist/assets/useProviders-B3VB7A9M.js +0 -1
- package/client/dist/assets/useTv-BTUKMH7Q.js +0 -1
- package/client/dist/assets/useTvLibrary-C0sX-ilZ.js +0 -1
- package/server/dist/controllers/asmr-library.controller.js +0 -288
- package/server/dist/controllers/auth.controller.js +0 -225
- package/server/dist/controllers/data.controller.js +0 -767
- package/server/dist/controllers/insights.controller.js +0 -203
- package/server/dist/controllers/manga-library.controller.js +0 -350
- package/server/dist/controllers/settings.controller.js +0 -609
- package/server/dist/controllers/tv-library.controller.js +0 -314
- package/server/dist/controllers/watchlist.controller.js +0 -1069
- package/server/dist/request-logger.js +0 -16
- package/server/dist/routes/asmr-library.routes.js +0 -23
- package/server/dist/routes/asmr.routes.js +0 -81
- package/server/dist/routes/auth.routes.js +0 -25
- package/server/dist/routes/data.routes.js +0 -47
- package/server/dist/routes/discord-gateway.routes.js +0 -51
- package/server/dist/routes/insights.routes.js +0 -35
- package/server/dist/routes/lan-auth.routes.js +0 -46
- package/server/dist/routes/manga-library.routes.js +0 -25
- package/server/dist/routes/manga.routes.js +0 -141
- package/server/dist/routes/music.routes.js +0 -20
- package/server/dist/routes/providers.routes.js +0 -17
- package/server/dist/routes/proxy.routes.js +0 -12
- package/server/dist/routes/settings.routes.js +0 -24
- package/server/dist/routes/tracker.routes.js +0 -287
- package/server/dist/routes/translate.routes.js +0 -88
- package/server/dist/routes/tv-library.routes.js +0 -23
- package/server/dist/routes/tv.routes.js +0 -629
- 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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
152
|
+
const candidate = new URL(urlStr);
|
|
153
|
+
candidate.hostname = fallback;
|
|
154
|
+
urls.push(candidate.href);
|
|
143
155
|
}
|
|
144
156
|
catch {
|
|
145
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
175
|
-
|
|
204
|
+
if (skipUri && trimmed && !trimmed.startsWith('#')) {
|
|
205
|
+
skipUri = false;
|
|
206
|
+
continue;
|
|
176
207
|
}
|
|
208
|
+
out.push(line);
|
|
177
209
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
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
|
-
|
|
225
|
-
|
|
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
|
|
309
|
+
return c.text('URL required', 400);
|
|
228
310
|
const safeCheck = isSafeExternalUrl(url);
|
|
229
311
|
if (!safeCheck.safe) {
|
|
230
|
-
return
|
|
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
|
|
236
|
-
const
|
|
237
|
-
const
|
|
238
|
-
const
|
|
239
|
-
|
|
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(
|
|
242
|
-
const isHanime = Array.from(
|
|
243
|
-
const isOppai = Array.from(
|
|
244
|
-
const isKaa = refererStr.startsWith(
|
|
245
|
-
Array.from(
|
|
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':
|
|
330
|
+
'User-Agent': PROXY_UA,
|
|
248
331
|
};
|
|
249
|
-
if (
|
|
332
|
+
if (refererStr)
|
|
250
333
|
headers['Referer'] = refererStr;
|
|
251
334
|
if (isVaultCdn) {
|
|
252
|
-
headers['Origin'] = refererStr ||
|
|
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 ||
|
|
270
|
-
headers['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
|
|
365
|
+
const jasmrUa = c.req.query('ua') || '';
|
|
283
366
|
if (jasmrUa)
|
|
284
367
|
headers['User-Agent'] = jasmrUa;
|
|
285
368
|
}
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
326
|
-
const pinned =
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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 (
|
|
358
|
-
if (
|
|
359
|
-
headers['Range'] =
|
|
441
|
+
if (isGotScrapingHost(urlStr)) {
|
|
442
|
+
if (range)
|
|
443
|
+
headers['Range'] = range;
|
|
360
444
|
let resp = null;
|
|
361
|
-
for (const candidate of
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
468
|
+
outHeaders.set('Content-Length', cl);
|
|
384
469
|
const cr = resp.headers['content-range'];
|
|
385
470
|
if (cr)
|
|
386
|
-
|
|
471
|
+
outHeaders.set('Content-Range', cr);
|
|
387
472
|
const ar = resp.headers['accept-ranges'];
|
|
388
473
|
if (ar)
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
|
396
|
-
if (
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
521
|
+
outHeaders.set('Content-Length', cl);
|
|
434
522
|
const cr = upstream.headers.get('content-range');
|
|
435
523
|
if (cr)
|
|
436
|
-
|
|
524
|
+
outHeaders.set('Content-Range', cr);
|
|
437
525
|
const ar = upstream.headers.get('accept-ranges');
|
|
438
526
|
if (ar)
|
|
439
|
-
|
|
440
|
-
|
|
527
|
+
outHeaders.set('Accept-Ranges', ar);
|
|
528
|
+
outHeaders.set('Access-Control-Allow-Origin', '*');
|
|
441
529
|
if (!upstream.body) {
|
|
442
|
-
return
|
|
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 (
|
|
461
|
-
return;
|
|
462
|
-
|
|
463
|
-
|
|
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
|
-
|
|
469
|
-
const kwikUrl =
|
|
542
|
+
});
|
|
543
|
+
app.get('/api/embed-proxy', async (c) => {
|
|
544
|
+
const kwikUrl = validateKwikUrl(c.req.query('url'));
|
|
470
545
|
if (!kwikUrl)
|
|
471
|
-
return
|
|
472
|
-
const cookieStr = req.query
|
|
473
|
-
const
|
|
474
|
-
|
|
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':
|
|
478
|
-
Referer:
|
|
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 =
|
|
571
|
+
const patched = applyKwikPatches(originalHtml, kwikUrl, cookieStr);
|
|
497
572
|
if (!patched)
|
|
498
|
-
return
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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
|
|
506
|
-
if (
|
|
507
|
-
return;
|
|
508
|
-
|
|
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
|
-
|
|
544
|
-
const
|
|
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
|
|
588
|
+
return c.text('URL required', 400);
|
|
582
589
|
const safeCheck = isSafeExternalUrl(url);
|
|
583
590
|
if (!safeCheck.safe) {
|
|
584
|
-
return
|
|
591
|
+
return c.text(safeCheck.error || 'Invalid URL', 400);
|
|
585
592
|
}
|
|
586
|
-
const
|
|
587
|
-
|
|
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'] =
|
|
623
|
+
headers['Referer'] = ANILIGHT_REFERER;
|
|
618
624
|
}
|
|
619
|
-
if (refererStr.startsWith(
|
|
620
|
-
Array.from(
|
|
621
|
-
headers['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'] =
|
|
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'] =
|
|
632
|
-
headers['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:
|
|
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 (
|
|
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 (
|
|
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 (
|
|
668
|
-
return;
|
|
680
|
+
if (abort.signal.aborted)
|
|
681
|
+
return new Response(null, { status: 499 });
|
|
669
682
|
}
|
|
670
683
|
if (!result || !result.body) {
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
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:
|
|
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
|
|
714
|
-
if (
|
|
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
|
-
|
|
720
|
-
|
|
721
|
-
|
|
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
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
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
|
|
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
|
-
|
|
750
|
-
|
|
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
|
|
764
|
+
return c.text('URL required', 400);
|
|
753
765
|
const safeCheck = isSafeExternalUrl(url);
|
|
754
766
|
if (!safeCheck.safe) {
|
|
755
|
-
return
|
|
767
|
+
return c.text(safeCheck.error || 'Invalid URL', 400);
|
|
756
768
|
}
|
|
757
769
|
const targetUrl = url;
|
|
758
|
-
const
|
|
759
|
-
|
|
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
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
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
|
|
836
|
-
if (
|
|
837
|
-
return;
|
|
838
|
-
const
|
|
839
|
-
if (
|
|
840
|
-
|
|
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
|
-
|
|
845
|
-
const targetUrl = req.query
|
|
858
|
+
});
|
|
859
|
+
app.get('/api/resolve', async (c) => {
|
|
860
|
+
const targetUrl = c.req.query('url');
|
|
846
861
|
if (!targetUrl || typeof targetUrl !== 'string') {
|
|
847
|
-
return
|
|
862
|
+
return c.json({ error: 'URL required' }, 400);
|
|
848
863
|
}
|
|
849
864
|
const safeCheck = isSafeExternalUrl(targetUrl);
|
|
850
865
|
if (!safeCheck.safe) {
|
|
851
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
890
|
-
}
|
|
886
|
+
});
|
|
891
887
|
}
|