@serifpersia/dango 3.6.7 → 3.6.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/client/dist/assets/{AnimeCard-C3P9zWi9.js → AnimeCard-DraA_TF9.js} +1 -1
- package/client/dist/assets/{AnimeInfo-Et6687Tl.js → AnimeInfo-BJ7pzs-P.js} +1 -1
- package/client/dist/assets/{AnimeInfoPage-DX4eNnlH.js → AnimeInfoPage-B_esUr5C.js} +2 -2
- package/client/dist/assets/{Asmr-CCdEWrFi.js → Asmr-Ckuh2xfW.js} +1 -1
- package/client/dist/assets/{AsmrResetProgressModal-DgEWBJOw.js → AsmrResetProgressModal-BS6UFJN4.js} +1 -1
- package/client/dist/assets/{AvSyncCalibrator-CQ2fjf2E.js → AvSyncCalibrator-DwHCd9yB.js} +1 -1
- package/client/dist/assets/{Home-DUkUTTTy.js → Home-DuM1EuvN.js} +1 -1
- package/client/dist/assets/{Insights-QCh4xW12.js → Insights-D3Pd9c9V.js} +1 -1
- package/client/dist/assets/{LibraryListPage-BoTxRqHz.js → LibraryListPage-Dm2yVn0-.js} +1 -1
- package/client/dist/assets/{ListeningList-DguKjI_t.js → ListeningList-D-zt6EXZ.js} +1 -1
- package/client/dist/assets/{Manga-HydR-L7o.js → Manga-JwprMKBk.js} +1 -1
- package/client/dist/assets/{MangaInfoPage-CBakEI9a.js → MangaInfoPage-DaeyJl2p.js} +1 -1
- package/client/dist/assets/{MangaReadPage-DwAR3iPy.js → MangaReadPage-COg0fHI9.js} +1 -1
- package/client/dist/assets/{MangaResetProgressModal-DGzl8vsO.js → MangaResetProgressModal-kguXv0_M.js} +1 -1
- package/client/dist/assets/{Mature-Pi0TxKuF.js → Mature-IHDaasrs.js} +1 -1
- package/client/dist/assets/{MediaCard--cuFxOCS.js → MediaCard-CpwsrXo5.js} +1 -1
- package/client/dist/assets/{Music-CQpyXTLH.js → Music-Dks4tPeK.js} +1 -1
- package/client/dist/assets/{OptionListMenu-B3D39dGS.js → OptionListMenu-11CcAsy8.js} +1 -1
- package/client/dist/assets/{Player-CsH4x_I_.js → Player-DQoEQkHL.js} +2 -2
- package/client/dist/assets/{PlayerSettings-DGxBsi3F.js → PlayerSettings-D4Ud4kv4.js} +1 -1
- package/client/dist/assets/{QueueOptionsButton-Djq4Cpex.js → QueueOptionsButton-B8PlPKjC.js} +1 -1
- package/client/dist/assets/{QueueRail-DOrYgNVX.js → QueueRail-B4Dok7U3.js} +1 -1
- package/client/dist/assets/{Radio-DF6g6odd.js → Radio-BHK6LSk4.js} +1 -1
- package/client/dist/assets/{ReadingList-DwYoBrNk.js → ReadingList-BCsxIz57.js} +1 -1
- package/client/dist/assets/{ResetProgressModal-CMF86mX6.js → ResetProgressModal-BXHxSfy-.js} +1 -1
- package/client/dist/assets/{Search-DFkhuk-x.js → Search-PK1ds07O.js} +1 -1
- package/client/dist/assets/{Settings-DBB2r0VQ.js → Settings-Bxk_x55l.js} +1 -1
- package/client/dist/assets/{SkeletonGrid-DJelJgAv.js → SkeletonGrid-CpJCZSRN.js} +1 -1
- package/client/dist/assets/{Trackers-DGYXFhYZ.js → Trackers-RieCh_84.js} +1 -1
- package/client/dist/assets/{Tv-C5MDGD2Z.js → Tv-DLytPJUj.js} +1 -1
- package/client/dist/assets/{TvInfo-DrKuK13u.js → TvInfo-C8djKebR.js} +1 -1
- package/client/dist/assets/{TvPopup-DtHilRDT.js → TvPopup-40JGGJDk.js} +1 -1
- package/client/dist/assets/{TvSearch-CzNxdu_w.js → TvSearch-A3hbospf.js} +1 -1
- package/client/dist/assets/{TvWatchlist-D7p4br6S.js → TvWatchlist-DuXjqmZM.js} +1 -1
- package/client/dist/assets/{Watchlist-DrOzswl2.js → Watchlist-B9wGBh0u.js} +1 -1
- package/client/dist/assets/{hls-BGCX7mJy.js → hls-CZ5X3Ya-.js} +1 -1
- package/client/dist/assets/{index-CnfZwsds.js → index-dRhc9-ol.js} +2 -2
- package/client/dist/assets/{useAnimeInfoData-DYXWBPF-.js → useAnimeInfoData-ZzDtLfvD.js} +1 -1
- package/client/dist/assets/{useAsmr-BbhQ59bk.js → useAsmr-CKOdHgol.js} +1 -1
- package/client/dist/assets/{useAsmrPopup-SymV1J-b.js → useAsmrPopup-SFHPhpcl.js} +1 -1
- package/client/dist/assets/{useMangaLibrary-DuesWQaE.js → useMangaLibrary-CKATe31P.js} +1 -1
- package/client/dist/assets/{useMangaPopup-D57OVGUr.js → useMangaPopup-C75QuYgY.js} +1 -1
- package/client/dist/assets/{useProviders-mpQb_dix.js → useProviders-CMtvZbEi.js} +1 -1
- package/client/dist/assets/{useTv-DGflCsSI.js → useTv-DUX4zYMr.js} +1 -1
- package/client/dist/assets/{useTvLibrary-BqeSmyc4.js → useTvLibrary-By4Gt-R_.js} +1 -1
- package/client/dist/index.html +1 -1
- package/package.json +2 -2
- package/server/dist/app-auth.js +1 -157
- package/server/dist/app-hono.js +7 -557
- package/server/dist/config.js +1 -174
- package/server/dist/db/drizzle.js +1 -16
- package/server/dist/db/schema-anime.js +1 -61
- package/server/dist/db/schema-asmr.js +1 -13
- package/server/dist/db/schema-manga.js +1 -31
- package/server/dist/db/schema-tv.js +1 -18
- package/server/dist/db.js +1 -217
- package/server/dist/discord-gateway.js +1 -385
- package/server/dist/discord-rpc.js +1 -413
- package/server/dist/github-sync.js +1 -508
- package/server/dist/google.js +1 -741
- package/server/dist/hono/asmr.js +1 -10
- package/server/dist/hono/auth.js +3 -26
- package/server/dist/hono/music.js +1 -209
- package/server/dist/hono/proxy.js +36 -783
- package/server/dist/hono/tv.js +5 -200
- package/server/dist/hono/watchlist.js +2 -338
- package/server/dist/lib/anilist.js +26 -1458
- package/server/dist/lib/anischedule.js +1 -191
- package/server/dist/lib/insights.js +1 -185
- package/server/dist/lib/ipc.js +1 -23
- package/server/dist/lib/kitsu.js +1 -517
- package/server/dist/lib/mal-import.js +1 -291
- package/server/dist/lib/mal.js +1 -957
- package/server/dist/lib/migration.js +1 -310
- package/server/dist/lib/offline-db.js +4 -384
- package/server/dist/lib/potoken.js +1 -135
- package/server/dist/lib/recommendations.service.js +1 -216
- package/server/dist/lib/temp-ids.js +1 -3
- package/server/dist/lib/tmdb.js +1 -72
- package/server/dist/lib/tracker/anilist-tracker.js +8 -245
- package/server/dist/lib/tracker/manga-sync.service.js +1 -773
- package/server/dist/lib/tracker/sync-state.js +1 -21
- package/server/dist/lib/tracker/sync.service.js +1 -476
- package/server/dist/lib/translate.js +3 -79
- package/server/dist/lib/ump.js +1 -106
- package/server/dist/lib/ytmusic.js +1 -162
- package/server/dist/logger.js +1 -16
- package/server/dist/providers/manga/manga.types.js +0 -1
- package/server/dist/providers/provider.interface.js +0 -1
- package/server/dist/providers/remote-loader.js +2 -614
- package/server/dist/providers/remote-types.js +1 -159
- package/server/dist/providers/title-matching.js +1 -226
- package/server/dist/providers/tv.types.js +0 -1
- package/server/dist/rclone.js +2 -120
- package/server/dist/repositories/asmr.repository.js +11 -46
- package/server/dist/repositories/insights.repository.js +11 -25
- package/server/dist/repositories/library.repository.js +3 -94
- package/server/dist/repositories/mal-cache.repository.js +1 -52
- package/server/dist/repositories/manga.repository.js +14 -58
- package/server/dist/repositories/notifications.repository.js +6 -52
- package/server/dist/repositories/queue.repository.js +5 -58
- package/server/dist/repositories/settings.repository.js +2 -29
- package/server/dist/repositories/shows-meta.repository.js +3 -56
- package/server/dist/repositories/temp-show-ids.repository.js +3 -63
- package/server/dist/repositories/tv.repository.js +11 -54
- package/server/dist/repositories/watched-episodes.repository.js +6 -55
- package/server/dist/repositories/watchlist.repository.js +3 -54
- package/server/dist/scripts/backfill-episode-duration.js +1 -42
- package/server/dist/server.js +1 -310
- package/server/dist/shipped-defaults.js +1 -6
- package/server/dist/sync-payload.js +1 -91
- package/server/dist/sync.js +3 -647
- package/server/dist/trpc/context.js +1 -7
- package/server/dist/trpc/errors.js +1 -15
- package/server/dist/trpc/index.js +1 -18
- package/server/dist/trpc/router.js +1 -49
- package/server/dist/trpc/routers/asmr-library.js +1 -112
- package/server/dist/trpc/routers/asmr-progress.js +1 -121
- package/server/dist/trpc/routers/asmr.js +1 -87
- package/server/dist/trpc/routers/auth.js +1 -194
- package/server/dist/trpc/routers/continue-watching.js +2 -58
- package/server/dist/trpc/routers/data.js +1 -1026
- package/server/dist/trpc/routers/discord.js +1 -285
- package/server/dist/trpc/routers/discovery.js +1 -11
- package/server/dist/trpc/routers/insights.js +1 -14
- package/server/dist/trpc/routers/manga-library.js +1 -263
- package/server/dist/trpc/routers/manga-progress.js +1 -169
- package/server/dist/trpc/routers/manga.js +1 -201
- package/server/dist/trpc/routers/music.js +1 -655
- package/server/dist/trpc/routers/notifications.js +1 -80
- package/server/dist/trpc/routers/progress.js +1 -109
- package/server/dist/trpc/routers/queue.js +1 -93
- package/server/dist/trpc/routers/radio.js +1 -145
- package/server/dist/trpc/routers/settings.js +1 -96
- package/server/dist/trpc/routers/tracker.js +1 -213
- package/server/dist/trpc/routers/tv-library.js +1 -135
- package/server/dist/trpc/routers/tv-progress.js +1 -159
- package/server/dist/trpc/routers/tv.js +1 -551
- package/server/dist/trpc/routers/watchlist.js +2 -549
- package/server/dist/trpc/validation.js +1 -258
- package/server/dist/utils/cache.utils.js +1 -25
- package/server/dist/utils/cookie.utils.js +1 -13
- package/server/dist/utils/db-utils.js +5 -17
- package/server/dist/utils/env.utils.js +5 -77
- package/server/dist/utils/http.utils.js +1 -82
- package/server/dist/utils/request-context.js +1 -2
- package/server/dist/utils/security.utils.js +1 -76
- package/server/package.json +3 -2
|
@@ -1,224 +1,30 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
function
|
|
21
|
-
if (!/^\s*\[Script Info\]/im.test(body))
|
|
22
|
-
return null;
|
|
23
|
-
const lines = body.replace(/\r\n/g, '\n').split('\n');
|
|
24
|
-
let inEvents = false;
|
|
25
|
-
let format = null;
|
|
26
|
-
const cues = [];
|
|
27
|
-
for (const raw of lines) {
|
|
28
|
-
const line = raw.trim();
|
|
29
|
-
if (/^\[Events\]/i.test(line)) {
|
|
30
|
-
inEvents = true;
|
|
31
|
-
continue;
|
|
32
|
-
}
|
|
33
|
-
if (/^\[/.test(line)) {
|
|
34
|
-
inEvents = false;
|
|
35
|
-
continue;
|
|
36
|
-
}
|
|
37
|
-
if (!inEvents)
|
|
38
|
-
continue;
|
|
39
|
-
if (/^Format\s*:/i.test(line)) {
|
|
40
|
-
format = line
|
|
41
|
-
.replace(/^Format\s*:/i, '')
|
|
42
|
-
.split(',')
|
|
43
|
-
.map((s) => s.trim().toLowerCase());
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
if (!/^Dialogue\s*:/i.test(line) || !format)
|
|
47
|
-
continue;
|
|
48
|
-
const payload = line.replace(/^Dialogue\s*:/i, '');
|
|
49
|
-
const parts = [];
|
|
50
|
-
let rest = payload;
|
|
51
|
-
for (let i = 0; i < format.length - 1; i++) {
|
|
52
|
-
const idx = rest.indexOf(',');
|
|
53
|
-
if (idx < 0)
|
|
54
|
-
break;
|
|
55
|
-
parts.push(rest.slice(0, idx));
|
|
56
|
-
rest = rest.slice(idx + 1);
|
|
57
|
-
}
|
|
58
|
-
parts.push(rest);
|
|
59
|
-
const start = assTimeToVtt(parts[format.indexOf('start')] ?? '');
|
|
60
|
-
const end = assTimeToVtt(parts[format.indexOf('end')] ?? '');
|
|
61
|
-
if (!start || !end || format.indexOf('text') < 0)
|
|
62
|
-
continue;
|
|
63
|
-
const text = (parts[format.indexOf('text')] ?? '')
|
|
64
|
-
.replace(/\\\\/g, '\\u0000')
|
|
65
|
-
.replace(/\{[^}]*\}/g, '')
|
|
66
|
-
.replace(/\\N/gi, '\n')
|
|
67
|
-
.replace(/\\n/g, '\n')
|
|
68
|
-
.replace(/\\h/gi, ' ')
|
|
69
|
-
.replace(/\\[a-zA-Z]+\d*/g, '')
|
|
70
|
-
.replace(/\\u0000/g, '\\')
|
|
71
|
-
.split('\n')
|
|
72
|
-
.map((s) => s.trim())
|
|
73
|
-
.filter(Boolean)
|
|
74
|
-
.join('\n');
|
|
75
|
-
if (!text)
|
|
76
|
-
continue;
|
|
77
|
-
cues.push(`${start} --> ${end}\n${text}`);
|
|
78
|
-
if (cues.length > 5000)
|
|
79
|
-
break;
|
|
80
|
-
}
|
|
81
|
-
if (cues.length === 0)
|
|
82
|
-
return null;
|
|
83
|
-
return `WEBVTT\n\n${cues.join('\n\n')}\n`;
|
|
84
|
-
}
|
|
85
|
-
const SUB_WRAP_AT = 50;
|
|
86
|
-
function splitBalancedCueLine(line) {
|
|
87
|
-
const words = line.split(/\s+/).filter((w) => w.length > 0);
|
|
88
|
-
if (words.length < 2)
|
|
89
|
-
return [line];
|
|
90
|
-
const total = words.join(' ').length;
|
|
91
|
-
if (total <= SUB_WRAP_AT)
|
|
92
|
-
return [line];
|
|
93
|
-
const target = total / 2;
|
|
94
|
-
let acc = words[0]?.length ?? 0;
|
|
95
|
-
let best = 1;
|
|
96
|
-
let bestDiff = Math.abs(acc - target);
|
|
97
|
-
for (let i = 1; i < words.length; i++) {
|
|
98
|
-
acc += 1 + (words[i]?.length ?? 0);
|
|
99
|
-
const diff = Math.abs(acc - target);
|
|
100
|
-
if (diff < bestDiff) {
|
|
101
|
-
bestDiff = diff;
|
|
102
|
-
best = i + 1;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
if (best <= 0 || best >= words.length)
|
|
106
|
-
return [line];
|
|
107
|
-
return [words.slice(0, best).join(' '), words.slice(best).join(' ')];
|
|
108
|
-
}
|
|
109
|
-
function rewrapVttCues(vtt) {
|
|
110
|
-
const blocks = vtt.replace(/\r\n/g, '\n').split(/\n\n+/);
|
|
111
|
-
return blocks
|
|
112
|
-
.map((block, idx) => {
|
|
113
|
-
if (idx === 0 && /^\s*WEBVTT/i.test(block))
|
|
114
|
-
return block;
|
|
115
|
-
const lines = block.split('\n');
|
|
116
|
-
const tIdx = lines.findIndex((l) => l.includes('-->'));
|
|
117
|
-
if (tIdx < 0)
|
|
118
|
-
return block;
|
|
119
|
-
const payload = lines.slice(tIdx + 1).filter((l) => l.trim() !== '');
|
|
120
|
-
if (payload.length !== 1)
|
|
121
|
-
return block;
|
|
122
|
-
const split = splitBalancedCueLine((payload[0] ?? '').trim());
|
|
123
|
-
if (split.length < 2)
|
|
124
|
-
return block;
|
|
125
|
-
return [...lines.slice(0, tIdx + 1), ...split].join('\n');
|
|
126
|
-
})
|
|
127
|
-
.join('\n\n');
|
|
128
|
-
}
|
|
129
|
-
function matchProxyRule(urlStr, rules) {
|
|
130
|
-
let host;
|
|
131
|
-
try {
|
|
132
|
-
host = new URL(urlStr).hostname.toLowerCase();
|
|
133
|
-
}
|
|
134
|
-
catch {
|
|
135
|
-
return undefined;
|
|
136
|
-
}
|
|
137
|
-
return rules.find((r) => r.hosts?.some((h) => h === host) || r.suffixes?.some((s) => host.endsWith(s)));
|
|
138
|
-
}
|
|
139
|
-
function resolveOrigin(rule, referer, queryOrigin) {
|
|
140
|
-
return queryOrigin || (rule?.originFromReferer ? referer : '') || rule?.origin || '';
|
|
141
|
-
}
|
|
142
|
-
function mirrorUrls(urlStr, mirrors) {
|
|
143
|
-
if (!mirrors?.length)
|
|
144
|
-
return [urlStr];
|
|
145
|
-
let current;
|
|
146
|
-
try {
|
|
147
|
-
current = new URL(urlStr);
|
|
148
|
-
}
|
|
149
|
-
catch {
|
|
150
|
-
return [urlStr];
|
|
151
|
-
}
|
|
152
|
-
const seen = new Set([current.hostname.toLowerCase()]);
|
|
153
|
-
const urls = [urlStr];
|
|
154
|
-
for (const mirror of mirrors) {
|
|
155
|
-
const host = mirror.toLowerCase();
|
|
156
|
-
if (seen.has(host))
|
|
157
|
-
continue;
|
|
158
|
-
seen.add(host);
|
|
159
|
-
const candidate = new URL(current.href);
|
|
160
|
-
candidate.hostname = host;
|
|
161
|
-
urls.push(candidate.href);
|
|
162
|
-
}
|
|
163
|
-
return urls;
|
|
164
|
-
}
|
|
165
|
-
async function fetchWithMirrors(urlStr, mirrors, signal, attempt, isOk) {
|
|
166
|
-
let last = null;
|
|
167
|
-
for (const candidate of mirrorUrls(urlStr, mirrors)) {
|
|
168
|
-
if (signal.aborted)
|
|
169
|
-
break;
|
|
170
|
-
const r = await attempt(candidate).catch(() => null);
|
|
171
|
-
if (!r)
|
|
172
|
-
continue;
|
|
173
|
-
if (isOk(r))
|
|
174
|
-
return r;
|
|
175
|
-
last = r;
|
|
176
|
-
}
|
|
177
|
-
return last;
|
|
178
|
-
}
|
|
179
|
-
function pinVariant(body, idx) {
|
|
180
|
-
const lines = body.split('\n');
|
|
181
|
-
let streamIdx = -1;
|
|
182
|
-
let skipUri = false;
|
|
183
|
-
const out = [];
|
|
184
|
-
for (const line of lines) {
|
|
185
|
-
const trimmed = line.trim();
|
|
186
|
-
if (trimmed.startsWith('#EXT-X-STREAM-INF')) {
|
|
187
|
-
streamIdx++;
|
|
188
|
-
skipUri = streamIdx !== idx;
|
|
189
|
-
if (!skipUri)
|
|
190
|
-
out.push(line);
|
|
191
|
-
continue;
|
|
192
|
-
}
|
|
193
|
-
if (skipUri && trimmed && !trimmed.startsWith('#')) {
|
|
194
|
-
skipUri = false;
|
|
195
|
-
continue;
|
|
196
|
-
}
|
|
197
|
-
out.push(line);
|
|
198
|
-
}
|
|
199
|
-
if (streamIdx < 0 || idx > streamIdx)
|
|
200
|
-
return null;
|
|
201
|
-
return out.join('\n');
|
|
202
|
-
}
|
|
203
|
-
function applyEmbedPatches(html, embedUrl, cookieStr = '') {
|
|
204
|
-
const safeReferer = JSON.stringify(embedUrl.href).replace(/</g, '\\u003c');
|
|
205
|
-
const safeCookie = JSON.stringify(cookieStr).replace(/</g, '\\u003c');
|
|
206
|
-
const patched = html.replace(/\b(src|href|url)\s*[=:]\s*(["']?)(\/\/[^"'>)]+|\/(?!\/)[^"'>)]*)\2/gi, (match, attr, quote, path) => {
|
|
207
|
-
const full = path.startsWith('//') ? `https:${path}` : `${embedUrl.origin}${path}`;
|
|
208
|
-
const prefix = match.startsWith('url') ? `url(` : `${attr}=`;
|
|
209
|
-
const suffix = match.startsWith('url') ? `)` : '';
|
|
210
|
-
return `${prefix}${quote}${full}${quote}${suffix}`;
|
|
211
|
-
});
|
|
212
|
-
const iconProxy = `/api/proxy?url=${encodeURIComponent(`${embedUrl.origin}/app/js/vendor/plyr.svg`)}&referer=${encodeURIComponent(embedUrl.href)}`;
|
|
213
|
-
const plyrPatch = `<script>if(window.Plyr) Plyr.defaults.iconUrl=${JSON.stringify(iconProxy).replace(/</g, '\\u003c')};</script>`;
|
|
214
|
-
const hlsPatch = `<script>
|
|
1
|
+
import{gotScraping as H}from"got-scraping";import j from"path";import{AppCache as _}from"../utils/cache.utils.js";import{CONFIG as N}from"../config.js";import D from"fs";import A from"../logger.js";import{buildCfClearanceCookie as V,sanitizeCfClearance as ee}from"../utils/cookie.utils.js";import{isSafeExternalUrl as W}from"../utils/security.utils.js";import{fetchWithRetry as G,linkAbort as L,setProxyHeaders as X}from"../utils/http.utils.js";const O=new _({ttlSeconds:30,maxKeys:500}),z=new _({ttlSeconds:86400,maxKeys:3e3});function J(c){const o=c.trim().match(/^(-?\d+):(\d{2}):(\d{2})[.:](\d{2,3})$/);if(!o)return null;const e=String(Math.max(0,parseInt(o[1],10))).padStart(2,"0"),r=o[4].length===2?`${o[4]}0`:o[4];return`${e}:${o[2]}:${o[3]}.${r}`}function te(c){if(!/^\s*\[Script Info\]/im.test(c))return null;const o=c.replace(/\r\n/g,`
|
|
2
|
+
`).split(`
|
|
3
|
+
`);let e=!1,r=null;const i=[];for(const t of o){const n=t.trim();if(/^\[Events\]/i.test(n)){e=!0;continue}if(/^\[/.test(n)){e=!1;continue}if(!e)continue;if(/^Format\s*:/i.test(n)){r=n.replace(/^Format\s*:/i,"").split(",").map(h=>h.trim().toLowerCase());continue}if(!/^Dialogue\s*:/i.test(n)||!r)continue;const s=n.replace(/^Dialogue\s*:/i,""),d=[];let m=s;for(let h=0;h<r.length-1;h++){const b=m.indexOf(",");if(b<0)break;d.push(m.slice(0,b)),m=m.slice(b+1)}d.push(m);const R=J(d[r.indexOf("start")]??""),w=J(d[r.indexOf("end")]??"");if(!R||!w||r.indexOf("text")<0)continue;const T=(d[r.indexOf("text")]??"").replace(/\\\\/g,"\\u0000").replace(/\{[^}]*\}/g,"").replace(/\\N/gi,`
|
|
4
|
+
`).replace(/\\n/g,`
|
|
5
|
+
`).replace(/\\h/gi," ").replace(/\\[a-zA-Z]+\d*/g,"").replace(/\\u0000/g,"\\").split(`
|
|
6
|
+
`).map(h=>h.trim()).filter(Boolean).join(`
|
|
7
|
+
`);if(T&&(i.push(`${R} --> ${w}
|
|
8
|
+
${T}`),i.length>5e3))break}return i.length===0?null:`WEBVTT
|
|
9
|
+
|
|
10
|
+
${i.join(`
|
|
11
|
+
|
|
12
|
+
`)}
|
|
13
|
+
`}const re=50;function ne(c){const o=c.split(/\s+/).filter(s=>s.length>0);if(o.length<2)return[c];const e=o.join(" ").length;if(e<=re)return[c];const r=e/2;let i=o[0]?.length??0,t=1,n=Math.abs(i-r);for(let s=1;s<o.length;s++){i+=1+(o[s]?.length??0);const d=Math.abs(i-r);d<n&&(n=d,t=s+1)}return t<=0||t>=o.length?[c]:[o.slice(0,t).join(" "),o.slice(t).join(" ")]}function P(c){return c.replace(/\r\n/g,`
|
|
14
|
+
`).split(/\n\n+/).map((e,r)=>{if(r===0&&/^\s*WEBVTT/i.test(e))return e;const i=e.split(`
|
|
15
|
+
`),t=i.findIndex(d=>d.includes("-->"));if(t<0)return e;const n=i.slice(t+1).filter(d=>d.trim()!=="");if(n.length!==1)return e;const s=ne((n[0]??"").trim());return s.length<2?e:[...i.slice(0,t+1),...s].join(`
|
|
16
|
+
`)}).join(`
|
|
17
|
+
|
|
18
|
+
`)}function I(c,o){let e;try{e=new URL(c).hostname.toLowerCase()}catch{return}return o.find(r=>r.hosts?.some(i=>i===e)||r.suffixes?.some(i=>e.endsWith(i)))}function B(c,o,e){return e||(c?.originFromReferer?o:"")||c?.origin||""}function se(c,o){if(!o?.length)return[c];let e;try{e=new URL(c)}catch{return[c]}const r=new Set([e.hostname.toLowerCase()]),i=[c];for(const t of o){const n=t.toLowerCase();if(r.has(n))continue;r.add(n);const s=new URL(e.href);s.hostname=n,i.push(s.href)}return i}async function K(c,o,e,r,i){let t=null;for(const n of se(c,o)){if(e.aborted)break;const s=await r(n).catch(()=>null);if(s){if(i(s))return s;t=s}}return t}function oe(c,o){const e=c.split(`
|
|
19
|
+
`);let r=-1,i=!1;const t=[];for(const n of e){const s=n.trim();if(s.startsWith("#EXT-X-STREAM-INF")){r++,i=r!==o,i||t.push(n);continue}if(i&&s&&!s.startsWith("#")){i=!1;continue}t.push(n)}return r<0||o>r?null:t.join(`
|
|
20
|
+
`)}function ie(c,o,e=""){const r=JSON.stringify(o.href).replace(/</g,"\\u003c"),i=JSON.stringify(e).replace(/</g,"\\u003c"),t=c.replace(/\b(src|href|url)\s*[=:]\s*(["']?)(\/\/[^"'>)]+|\/(?!\/)[^"'>)]*)\2/gi,(T,h,b,l)=>{const u=l.startsWith("//")?`https:${l}`:`${o.origin}${l}`,y=T.startsWith("url")?"url(":`${h}=`,f=T.startsWith("url")?")":"";return`${y}${b}${u}${b}${f}`}),n=`/api/proxy?url=${encodeURIComponent(`${o.origin}/app/js/vendor/plyr.svg`)}&referer=${encodeURIComponent(o.href)}`,s=`<script>if(window.Plyr) Plyr.defaults.iconUrl=${JSON.stringify(n).replace(/</g,"\\u003c")};</script>`,d=`<script>
|
|
215
21
|
(function() {
|
|
216
22
|
var hook = function() {
|
|
217
23
|
if (!window.Hls) return;
|
|
218
24
|
var original = Hls.prototype.loadSource;
|
|
219
25
|
Hls.prototype.loadSource = function(src) {
|
|
220
26
|
if (typeof src === 'string' && src.includes('.m3u8')) {
|
|
221
|
-
src = window.location.origin + '/api/proxy?url=' + encodeURIComponent(src) + '&referer=' + encodeURIComponent(${
|
|
27
|
+
src = window.location.origin + '/api/proxy?url=' + encodeURIComponent(src) + '&referer=' + encodeURIComponent(${r}) + (${i} ? '&cookie=' + encodeURIComponent(${i}) : '');
|
|
222
28
|
}
|
|
223
29
|
return original.call(this, src);
|
|
224
30
|
};
|
|
@@ -231,8 +37,7 @@ function applyEmbedPatches(html, embedUrl, cookieStr = '') {
|
|
|
231
37
|
observer.observe(document.documentElement, { childList: true, subtree: true });
|
|
232
38
|
}
|
|
233
39
|
})();
|
|
234
|
-
</script
|
|
235
|
-
const endBridgePatch = `<script>
|
|
40
|
+
</script>`,w=t.replace(/(<script[^>]+\/plyr\.min\.js[^>]*><\/script>)/i,`$1${s}`).replace(/(<script[^>]+hls(?:\.min)?\.js[^>]*><\/script>)/i,`$1${d}`).replace(/<\/body>/i,`<script>
|
|
236
41
|
(function() {
|
|
237
42
|
var notified = false;
|
|
238
43
|
var notifyEnded = function() {
|
|
@@ -259,569 +64,17 @@ function applyEmbedPatches(html, embedUrl, cookieStr = '') {
|
|
|
259
64
|
|
|
260
65
|
observer.observe(document.documentElement, { childList: true, subtree: true });
|
|
261
66
|
})();
|
|
262
|
-
</script
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
try {
|
|
277
|
-
if (fs.existsSync(p)) {
|
|
278
|
-
const headers = new Headers();
|
|
279
|
-
headers.set('Cache-Control', 'no-store');
|
|
280
|
-
headers.set('Content-Type', 'image/svg+xml');
|
|
281
|
-
return new Response(fs.readFileSync(p), { headers });
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
catch {
|
|
285
|
-
// ignore
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
return null;
|
|
289
|
-
}
|
|
290
|
-
const PROXY_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0';
|
|
291
|
-
export function registerProxy(app, getProxyRules) {
|
|
292
|
-
app.get('/api/proxy', async (c) => {
|
|
293
|
-
const url = c.req.query('url');
|
|
294
|
-
if (!url)
|
|
295
|
-
return c.text('URL required', 400);
|
|
296
|
-
const safeCheck = isSafeExternalUrl(url);
|
|
297
|
-
if (!safeCheck.safe) {
|
|
298
|
-
return c.text(safeCheck.error || 'Invalid URL', 400);
|
|
299
|
-
}
|
|
300
|
-
const urlStr = url;
|
|
301
|
-
const rule = matchProxyRule(urlStr, getProxyRules());
|
|
302
|
-
const refererStr = c.req.query('referer') || rule?.referer || '';
|
|
303
|
-
const originStr = resolveOrigin(rule, refererStr, c.req.query('origin'));
|
|
304
|
-
const uaStr = c.req.query('ua') || '';
|
|
305
|
-
const cookieStr = c.req.query('cookie') || '';
|
|
306
|
-
const useScrape = c.req.query('scrape') === '1' || rule?.scrape === true;
|
|
307
|
-
const useCf = c.req.query('cf') === '1' || rule?.cfCookie === true;
|
|
308
|
-
const variantRaw = c.req.query('variant');
|
|
309
|
-
const variantIdx = variantRaw !== undefined ? parseInt(variantRaw, 10) : NaN;
|
|
310
|
-
const pinVar = Number.isInteger(variantIdx) && variantIdx >= 0;
|
|
311
|
-
const cacheKey = `m3u8-${urlStr}-${refererStr}-${originStr}-${uaStr}${pinVar ? `-v${variantIdx}` : ''}`;
|
|
312
|
-
const abort = new AbortController();
|
|
313
|
-
linkAbort(c.req.raw.signal, abort);
|
|
314
|
-
try {
|
|
315
|
-
const headers = {
|
|
316
|
-
'User-Agent': uaStr || PROXY_UA,
|
|
317
|
-
};
|
|
318
|
-
if (refererStr)
|
|
319
|
-
headers['Referer'] = refererStr;
|
|
320
|
-
if (originStr)
|
|
321
|
-
headers['Origin'] = originStr;
|
|
322
|
-
if (cookieStr) {
|
|
323
|
-
const cookieHeader = useCf ? buildCfClearanceCookie(cookieStr) : cookieStr;
|
|
324
|
-
if (cookieHeader)
|
|
325
|
-
headers['Cookie'] = cookieHeader;
|
|
326
|
-
}
|
|
327
|
-
const range = c.req.header('range');
|
|
328
|
-
if (range)
|
|
329
|
-
headers['Range'] = range;
|
|
330
|
-
const playlistResponse = (body) => {
|
|
331
|
-
const outHeaders = new Headers();
|
|
332
|
-
outHeaders.set('Content-Type', 'application/vnd.apple.mpegurl');
|
|
333
|
-
outHeaders.set('Access-Control-Allow-Origin', '*');
|
|
334
|
-
return new Response(body, { headers: outHeaders });
|
|
335
|
-
};
|
|
336
|
-
const rewritePlaylist = (playlistBody, finalUrl) => {
|
|
337
|
-
const baseUrl = new URL(finalUrl);
|
|
338
|
-
const proxiedMediaUrl = (targetUrl) => {
|
|
339
|
-
const params = new URLSearchParams({ url: targetUrl });
|
|
340
|
-
if (refererStr)
|
|
341
|
-
params.set('referer', refererStr);
|
|
342
|
-
if (originStr)
|
|
343
|
-
params.set('origin', originStr);
|
|
344
|
-
if (uaStr)
|
|
345
|
-
params.set('ua', uaStr);
|
|
346
|
-
if (cookieStr)
|
|
347
|
-
params.set('cookie', sanitizeCfClearance(cookieStr));
|
|
348
|
-
if (useScrape)
|
|
349
|
-
params.set('scrape', '1');
|
|
350
|
-
if (useCf)
|
|
351
|
-
params.set('cf', '1');
|
|
352
|
-
return `/api/proxy?${params}`;
|
|
353
|
-
};
|
|
354
|
-
const needsProxy = Boolean(refererStr || originStr || uaStr || useScrape);
|
|
355
|
-
const isProxied = (value) => value.includes('/api/proxy');
|
|
356
|
-
const pinned = pinVar ? pinVariant(playlistBody, variantIdx) : null;
|
|
357
|
-
return (pinned || playlistBody)
|
|
358
|
-
.split('\n')
|
|
359
|
-
.map((line) => {
|
|
360
|
-
const trimmed = line.trim();
|
|
361
|
-
if (!trimmed)
|
|
362
|
-
return line;
|
|
363
|
-
if (trimmed.startsWith('#')) {
|
|
364
|
-
return trimmed.replace(/URI="([^"]+)"/g, (_, uri) => {
|
|
365
|
-
if (isProxied(uri))
|
|
366
|
-
return `URI="${uri}"`;
|
|
367
|
-
const absolute = new URL(uri, baseUrl).href;
|
|
368
|
-
return `URI="${needsProxy || absolute.includes('.m3u8') ? proxiedMediaUrl(absolute) : absolute}"`;
|
|
369
|
-
});
|
|
370
|
-
}
|
|
371
|
-
if (isProxied(trimmed))
|
|
372
|
-
return line;
|
|
373
|
-
const absolute = new URL(trimmed, baseUrl).href;
|
|
374
|
-
return proxiedMediaUrl(absolute);
|
|
375
|
-
})
|
|
376
|
-
.join('\n');
|
|
377
|
-
};
|
|
378
|
-
if (urlStr.includes('.m3u8')) {
|
|
379
|
-
const cached = proxyCache.get(cacheKey);
|
|
380
|
-
if (cached) {
|
|
381
|
-
const headers = new Headers();
|
|
382
|
-
headers.set('Content-Type', 'application/vnd.apple.mpegurl');
|
|
383
|
-
headers.set('Access-Control-Allow-Origin', '*');
|
|
384
|
-
return new Response(cached, { headers });
|
|
385
|
-
}
|
|
386
|
-
const resp = await fetchWithMirrors(urlStr, rule?.mirrors, abort.signal, (candidate) => gotScraping({
|
|
387
|
-
url: candidate,
|
|
388
|
-
method: 'GET',
|
|
389
|
-
headers,
|
|
390
|
-
responseType: 'text',
|
|
391
|
-
timeout: { request: 30000 },
|
|
392
|
-
followRedirect: true,
|
|
393
|
-
throwHttpErrors: false,
|
|
394
|
-
}), (attempt) => attempt.statusCode === 200 || attempt.statusCode === 206);
|
|
395
|
-
if (!resp || (resp.statusCode !== 200 && resp.statusCode !== 206)) {
|
|
396
|
-
return c.text('Upstream error', (resp?.statusCode || 502));
|
|
397
|
-
}
|
|
398
|
-
const rewritten = rewritePlaylist(resp.body, resp.url || urlStr);
|
|
399
|
-
proxyCache.set(cacheKey, rewritten);
|
|
400
|
-
return playlistResponse(rewritten);
|
|
401
|
-
}
|
|
402
|
-
else {
|
|
403
|
-
if (useScrape) {
|
|
404
|
-
if (range)
|
|
405
|
-
headers['Range'] = range;
|
|
406
|
-
const resp = await fetchWithMirrors(urlStr, rule?.mirrors, abort.signal, (candidate) => gotScraping({
|
|
407
|
-
url: candidate,
|
|
408
|
-
method: 'GET',
|
|
409
|
-
headers,
|
|
410
|
-
responseType: 'buffer',
|
|
411
|
-
timeout: { request: 30000 },
|
|
412
|
-
followRedirect: true,
|
|
413
|
-
throwHttpErrors: false,
|
|
414
|
-
}), (attempt) => attempt.statusCode === 200 || attempt.statusCode === 206);
|
|
415
|
-
if (!resp || (resp.statusCode !== 200 && resp.statusCode !== 206)) {
|
|
416
|
-
return c.text('Upstream error', (resp?.statusCode || 502));
|
|
417
|
-
}
|
|
418
|
-
const ct = resp.headers['content-type'];
|
|
419
|
-
const isVtt = urlStr.endsWith('.vtt') || urlStr.includes('.vtt?');
|
|
420
|
-
const bodyText = String(resp.body ?? '');
|
|
421
|
-
if (!isVtt &&
|
|
422
|
-
((typeof ct === 'string' && ct.includes('mpegurl')) || bodyText.startsWith('#EXTM3U'))) {
|
|
423
|
-
const cached = proxyCache.get(cacheKey);
|
|
424
|
-
const rewritten = cached ?? rewritePlaylist(bodyText, resp.url || urlStr);
|
|
425
|
-
if (!cached)
|
|
426
|
-
proxyCache.set(cacheKey, rewritten);
|
|
427
|
-
return playlistResponse(rewritten);
|
|
428
|
-
}
|
|
429
|
-
const outHeaders = new Headers();
|
|
430
|
-
outHeaders.set('Content-Type', isVtt ? 'text/vtt; charset=utf-8' : ct || 'application/octet-stream');
|
|
431
|
-
setProxyHeaders(outHeaders, (name) => resp.headers[name]);
|
|
432
|
-
return new Response(resp.body, {
|
|
433
|
-
status: resp.statusCode,
|
|
434
|
-
headers: outHeaders,
|
|
435
|
-
});
|
|
436
|
-
}
|
|
437
|
-
else {
|
|
438
|
-
const upstream = await fetchWithMirrors(urlStr, rule?.mirrors, abort.signal, async (candidate) => {
|
|
439
|
-
const attempt = await fetchWithRetry(candidate, { method: 'GET', headers }, { retries: 3, timeoutMs: 30000, signal: abort.signal });
|
|
440
|
-
if (attempt.status !== 200 && attempt.status !== 206) {
|
|
441
|
-
try {
|
|
442
|
-
await attempt.body?.cancel();
|
|
443
|
-
}
|
|
444
|
-
catch {
|
|
445
|
-
// ignore
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
return attempt;
|
|
449
|
-
}, (attempt) => attempt.status === 200 || attempt.status === 206);
|
|
450
|
-
if (!upstream) {
|
|
451
|
-
return c.text('Upstream error', 502);
|
|
452
|
-
}
|
|
453
|
-
const status = upstream.status;
|
|
454
|
-
if (status !== 200 && status !== 206) {
|
|
455
|
-
try {
|
|
456
|
-
await upstream.body?.cancel();
|
|
457
|
-
}
|
|
458
|
-
catch {
|
|
459
|
-
// ignore
|
|
460
|
-
}
|
|
461
|
-
return c.text('Upstream error', status);
|
|
462
|
-
}
|
|
463
|
-
const isVtt = urlStr.endsWith('.vtt') || urlStr.includes('.vtt?');
|
|
464
|
-
const ct = upstream.headers.get('content-type');
|
|
465
|
-
const outHeaders = new Headers();
|
|
466
|
-
outHeaders.set('Content-Type', isVtt ? 'text/vtt; charset=utf-8' : ct || 'application/octet-stream');
|
|
467
|
-
setProxyHeaders(outHeaders, (name) => upstream.headers.get(name));
|
|
468
|
-
if (!upstream.body) {
|
|
469
|
-
return c.text('Upstream error', 502);
|
|
470
|
-
}
|
|
471
|
-
return new Response(upstream.body, { status: status, headers: outHeaders });
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
catch (e) {
|
|
476
|
-
if (abort.signal.aborted)
|
|
477
|
-
return new Response(null, { status: 499 });
|
|
478
|
-
logger.error({ url: urlStr, error: e?.message }, '[proxy] upstream request failed');
|
|
479
|
-
return c.text('Proxy error', 500);
|
|
480
|
-
}
|
|
481
|
-
});
|
|
482
|
-
app.get('/api/embed-proxy', async (c) => {
|
|
483
|
-
const rawUrl = c.req.query('url');
|
|
484
|
-
if (!rawUrl)
|
|
485
|
-
return c.text('URL required', 400);
|
|
486
|
-
const safeCheck = isSafeExternalUrl(rawUrl);
|
|
487
|
-
if (!safeCheck.safe)
|
|
488
|
-
return c.text(safeCheck.error || 'Invalid URL', 400);
|
|
489
|
-
const rule = matchProxyRule(rawUrl, getProxyRules());
|
|
490
|
-
if (!rule?.embedPatch)
|
|
491
|
-
return c.text('Invalid or unsupported gateway URL', 400);
|
|
492
|
-
const embedUrl = new URL(rawUrl);
|
|
493
|
-
const cookieStr = c.req.query('cookie') || '';
|
|
494
|
-
const refererStr = c.req.query('referer') || rule.referer || '';
|
|
495
|
-
const originStr = resolveOrigin(rule, refererStr, c.req.query('origin'));
|
|
496
|
-
const abort = new AbortController();
|
|
497
|
-
linkAbort(c.req.raw.signal, abort);
|
|
498
|
-
try {
|
|
499
|
-
const headers = {
|
|
500
|
-
'User-Agent': c.req.query('ua') || PROXY_UA,
|
|
501
|
-
};
|
|
502
|
-
if (refererStr)
|
|
503
|
-
headers['Referer'] = refererStr;
|
|
504
|
-
if (originStr)
|
|
505
|
-
headers['Origin'] = originStr;
|
|
506
|
-
if (cookieStr) {
|
|
507
|
-
const cookieHeader = c.req.query('cf') === '1' || rule.cfCookie ? buildCfClearanceCookie(cookieStr) : cookieStr;
|
|
508
|
-
if (cookieHeader)
|
|
509
|
-
headers['Cookie'] = cookieHeader;
|
|
510
|
-
}
|
|
511
|
-
const response = await gotScraping({
|
|
512
|
-
url: embedUrl.href,
|
|
513
|
-
method: 'GET',
|
|
514
|
-
headers,
|
|
515
|
-
responseType: 'text',
|
|
516
|
-
timeout: { request: 30000 },
|
|
517
|
-
followRedirect: true,
|
|
518
|
-
throwHttpErrors: false,
|
|
519
|
-
});
|
|
520
|
-
const originalHtml = response.body;
|
|
521
|
-
const patched = applyEmbedPatches(originalHtml, embedUrl, cookieStr);
|
|
522
|
-
if (!patched)
|
|
523
|
-
return c.text('Failed to patch video gateway', 502);
|
|
524
|
-
const outHeaders = new Headers();
|
|
525
|
-
outHeaders.set('Content-Type', 'text/html; charset=utf-8');
|
|
526
|
-
outHeaders.set('Cache-Control', 'private, max-age=120');
|
|
527
|
-
return new Response(patched, { status: 200, headers: outHeaders });
|
|
528
|
-
}
|
|
529
|
-
catch {
|
|
530
|
-
if (abort.signal.aborted)
|
|
531
|
-
return new Response(null, { status: 499 });
|
|
532
|
-
return c.text('Gateway proxy error', 502);
|
|
533
|
-
}
|
|
534
|
-
});
|
|
535
|
-
app.get('/api/subtitle-proxy', async (c) => {
|
|
536
|
-
const url = c.req.query('url');
|
|
537
|
-
if (!url)
|
|
538
|
-
return c.text('URL required', 400);
|
|
539
|
-
const safeCheck = isSafeExternalUrl(url);
|
|
540
|
-
if (!safeCheck.safe) {
|
|
541
|
-
return c.text(safeCheck.error || 'Invalid URL', 400);
|
|
542
|
-
}
|
|
543
|
-
const abort = new AbortController();
|
|
544
|
-
linkAbort(c.req.raw.signal, abort);
|
|
545
|
-
const subUrl = url;
|
|
546
|
-
const rule = matchProxyRule(subUrl, getProxyRules());
|
|
547
|
-
const refererStr = c.req.query('referer') || rule?.referer || '';
|
|
548
|
-
const originStr = resolveOrigin(rule, refererStr, c.req.query('origin'));
|
|
549
|
-
const useScrape = c.req.query('scrape') === '1' || rule?.scrape === true;
|
|
550
|
-
const buildHeaders = () => {
|
|
551
|
-
const headers = {
|
|
552
|
-
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
553
|
-
Accept: 'text/vtt,text/plain,*/*;q=0.8',
|
|
554
|
-
};
|
|
555
|
-
if (refererStr)
|
|
556
|
-
headers['Referer'] = refererStr;
|
|
557
|
-
if (originStr)
|
|
558
|
-
headers['Origin'] = originStr;
|
|
559
|
-
return headers;
|
|
560
|
-
};
|
|
561
|
-
const fetchText = async (target, retries = 1) => {
|
|
562
|
-
const headers = buildHeaders();
|
|
563
|
-
if (useScrape) {
|
|
564
|
-
try {
|
|
565
|
-
const upstream = await gotScraping({
|
|
566
|
-
url: target,
|
|
567
|
-
method: 'GET',
|
|
568
|
-
headers,
|
|
569
|
-
responseType: 'text',
|
|
570
|
-
timeout: { request: 15000 },
|
|
571
|
-
followRedirect: true,
|
|
572
|
-
throwHttpErrors: false,
|
|
573
|
-
});
|
|
574
|
-
if (upstream.statusCode !== 200) {
|
|
575
|
-
logger.warn({ target, status: upstream.statusCode }, '[subtitle-proxy] upstream returned error status');
|
|
576
|
-
return { status: upstream.statusCode, body: '' };
|
|
577
|
-
}
|
|
578
|
-
const body = String(upstream.body ?? '').replace(/^\uFEFF/, '');
|
|
579
|
-
if (/^\s*\{[\s\S]*"error"[\s\S]*\}\s*$/.test(body.slice(0, 500))) {
|
|
580
|
-
logger.warn({ target, body: body.slice(0, 200) }, '[subtitle-proxy] upstream JSON error');
|
|
581
|
-
return { status: 502, body: '' };
|
|
582
|
-
}
|
|
583
|
-
return { status: 200, body };
|
|
584
|
-
}
|
|
585
|
-
catch (e) {
|
|
586
|
-
if (abort.signal.aborted)
|
|
587
|
-
return null;
|
|
588
|
-
logger.warn({ target, error: e?.message }, '[subtitle-proxy] upstream fetch failed');
|
|
589
|
-
return { status: 502, body: '' };
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
try {
|
|
593
|
-
const upstream = await fetchWithRetry(target, { method: 'GET', headers }, { retries, timeoutMs: 15000, signal: abort.signal });
|
|
594
|
-
if (!upstream.ok) {
|
|
595
|
-
logger.warn({ target, status: upstream.status }, '[subtitle-proxy] upstream returned error status');
|
|
596
|
-
try {
|
|
597
|
-
await upstream.body?.cancel();
|
|
598
|
-
}
|
|
599
|
-
catch {
|
|
600
|
-
// ignore
|
|
601
|
-
}
|
|
602
|
-
return { status: upstream.status, body: '' };
|
|
603
|
-
}
|
|
604
|
-
const body = (await upstream.text()).replace(/^\uFEFF/, '');
|
|
605
|
-
if (/^\s*\{[\s\S]*"error"[\s\S]*\}\s*$/.test(body.slice(0, 500))) {
|
|
606
|
-
logger.warn({ target, body: body.slice(0, 200) }, '[subtitle-proxy] upstream JSON error');
|
|
607
|
-
return { status: 502, body: '' };
|
|
608
|
-
}
|
|
609
|
-
return { status: upstream.status, body };
|
|
610
|
-
}
|
|
611
|
-
catch (e) {
|
|
612
|
-
if (abort.signal.aborted)
|
|
613
|
-
return null;
|
|
614
|
-
logger.warn({ target, error: e?.message }, '[subtitle-proxy] upstream fetch failed');
|
|
615
|
-
return { status: 502, body: '' };
|
|
616
|
-
}
|
|
617
|
-
};
|
|
618
|
-
const vttHeaders = (cacheControl) => {
|
|
619
|
-
const headers = new Headers();
|
|
620
|
-
headers.set('Content-Type', 'text/vtt; charset=utf-8');
|
|
621
|
-
headers.set('Access-Control-Allow-Origin', '*');
|
|
622
|
-
headers.set('Cache-Control', cacheControl);
|
|
623
|
-
return headers;
|
|
624
|
-
};
|
|
625
|
-
const subtitleError = (text) => {
|
|
626
|
-
const headers = new Headers();
|
|
627
|
-
headers.set('Access-Control-Allow-Origin', '*');
|
|
628
|
-
headers.set('Cache-Control', 'no-store');
|
|
629
|
-
return new Response(text, { status: 502, headers });
|
|
630
|
-
};
|
|
631
|
-
try {
|
|
632
|
-
const headers = buildHeaders();
|
|
633
|
-
const result = await fetchText(subUrl, 1);
|
|
634
|
-
if (abort.signal.aborted)
|
|
635
|
-
return new Response(null, { status: 499 });
|
|
636
|
-
if (!result || !result.body) {
|
|
637
|
-
return subtitleError('Subtitle upstream error');
|
|
638
|
-
}
|
|
639
|
-
const body = result.body;
|
|
640
|
-
const baseForSegments = subUrl;
|
|
641
|
-
if (/#EXTM3U/i.test(body.slice(0, 1000))) {
|
|
642
|
-
const segUrls = body
|
|
643
|
-
.split('\n')
|
|
644
|
-
.map((l) => l.trim())
|
|
645
|
-
.filter((l) => l && !l.startsWith('#'))
|
|
646
|
-
.slice(0, 200)
|
|
647
|
-
.map((u) => {
|
|
648
|
-
try {
|
|
649
|
-
return new URL(u, baseForSegments).href;
|
|
650
|
-
}
|
|
651
|
-
catch {
|
|
652
|
-
return null;
|
|
653
|
-
}
|
|
654
|
-
})
|
|
655
|
-
.filter((u) => Boolean(u));
|
|
656
|
-
const parts = [];
|
|
657
|
-
for (const seg of segUrls) {
|
|
658
|
-
try {
|
|
659
|
-
const segRes = await fetchWithRetry(seg, { method: 'GET', headers }, { retries: 1, timeoutMs: 15000, signal: abort.signal });
|
|
660
|
-
if (!segRes.ok)
|
|
661
|
-
continue;
|
|
662
|
-
let segBody = (await segRes.text()).replace(/^\uFEFF/, '');
|
|
663
|
-
if (/#EXTM3U/i.test(segBody.slice(0, 200)))
|
|
664
|
-
continue;
|
|
665
|
-
segBody = segBody
|
|
666
|
-
.replace(/\r\n/g, '\n')
|
|
667
|
-
.replace(/(\d{2}:\d{2}:\d{2}),(\d{3})/g, '$1.$2');
|
|
668
|
-
if (/^\s*WEBVTT/i.test(segBody)) {
|
|
669
|
-
segBody = segBody.replace(/^\s*WEBVTT[^\n]*\n(\n|.*\n)?/, '');
|
|
670
|
-
}
|
|
671
|
-
segBody = segBody.trim();
|
|
672
|
-
if (segBody)
|
|
673
|
-
parts.push(segBody);
|
|
674
|
-
if (parts.join('\n').length > 2_000_000)
|
|
675
|
-
break;
|
|
676
|
-
}
|
|
677
|
-
catch {
|
|
678
|
-
if (abort.signal.aborted)
|
|
679
|
-
return new Response(null, { status: 499 });
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
if (parts.length === 0) {
|
|
683
|
-
return subtitleError('Subtitle playlist empty');
|
|
684
|
-
}
|
|
685
|
-
return new Response(rewrapVttCues(`WEBVTT\n\n${parts.join('\n\n')}\n`), {
|
|
686
|
-
headers: vttHeaders('public, max-age=86400'),
|
|
687
|
-
});
|
|
688
|
-
}
|
|
689
|
-
if (/^\s*WEBVTT/i.test(body)) {
|
|
690
|
-
return new Response(rewrapVttCues(body), {
|
|
691
|
-
headers: vttHeaders('public, max-age=86400'),
|
|
692
|
-
});
|
|
693
|
-
}
|
|
694
|
-
const vttFromAss = assToVtt(body);
|
|
695
|
-
if (vttFromAss) {
|
|
696
|
-
return new Response(rewrapVttCues(vttFromAss), {
|
|
697
|
-
headers: vttHeaders('public, max-age=86400'),
|
|
698
|
-
});
|
|
699
|
-
}
|
|
700
|
-
return new Response(rewrapVttCues(`WEBVTT\n\n${body.replace(/\r\n/g, '\n').replace(/(\d{2}:\d{2}:\d{2}),(\d{3})/g, '$1.$2')}`), { headers: vttHeaders('public, max-age=86400') });
|
|
701
|
-
}
|
|
702
|
-
catch {
|
|
703
|
-
if (abort.signal.aborted)
|
|
704
|
-
return new Response(null, { status: 499 });
|
|
705
|
-
logger.warn({ url: subUrl }, '[subtitle-proxy] failed');
|
|
706
|
-
return subtitleError('Subtitle upstream error');
|
|
707
|
-
}
|
|
708
|
-
});
|
|
709
|
-
app.get('/api/image-proxy', async (c) => {
|
|
710
|
-
const url = c.req.query('url');
|
|
711
|
-
if (!url)
|
|
712
|
-
return c.text('URL required', 400);
|
|
713
|
-
const safeCheck = isSafeExternalUrl(url);
|
|
714
|
-
if (!safeCheck.safe) {
|
|
715
|
-
return c.text(safeCheck.error || 'Invalid URL', 400);
|
|
716
|
-
}
|
|
717
|
-
const targetUrl = url;
|
|
718
|
-
const cookie = c.req.query('cookie') || '';
|
|
719
|
-
const ua = c.req.query('ua') || '';
|
|
720
|
-
const strict = c.req.query('strict') === '1';
|
|
721
|
-
const rule = matchProxyRule(targetUrl, getProxyRules());
|
|
722
|
-
const refererValue = c.req.query('referer') || rule?.referer || '';
|
|
723
|
-
const originValue = resolveOrigin(rule, refererValue, c.req.query('origin'));
|
|
724
|
-
const abort = new AbortController();
|
|
725
|
-
linkAbort(c.req.raw.signal, abort);
|
|
726
|
-
const headers = {
|
|
727
|
-
'User-Agent': ua ||
|
|
728
|
-
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
729
|
-
};
|
|
730
|
-
const cacheKey = `${targetUrl}|${cookie}|${ua}|${refererValue}|${originValue}`;
|
|
731
|
-
const cached = imageCache.get(cacheKey);
|
|
732
|
-
if (cached) {
|
|
733
|
-
const hit = new Headers();
|
|
734
|
-
hit.set('Cache-Control', 'public, max-age=604800, immutable');
|
|
735
|
-
hit.set('Content-Type', cached.contentType);
|
|
736
|
-
hit.set('Access-Control-Allow-Origin', '*');
|
|
737
|
-
hit.set('X-Dango-Cache', 'hit');
|
|
738
|
-
return new Response(cached.body, { headers: hit });
|
|
739
|
-
}
|
|
740
|
-
try {
|
|
741
|
-
if (refererValue)
|
|
742
|
-
headers['Referer'] = refererValue;
|
|
743
|
-
if (originValue)
|
|
744
|
-
headers['Origin'] = originValue;
|
|
745
|
-
if (c.req.query('cf') === '1' || rule?.cfCookie) {
|
|
746
|
-
const cookieHeader = buildCfClearanceCookie(cookie);
|
|
747
|
-
if (cookieHeader)
|
|
748
|
-
headers['Cookie'] = cookieHeader;
|
|
749
|
-
}
|
|
750
|
-
let body = null;
|
|
751
|
-
let contentType = 'image/webp';
|
|
752
|
-
for (let attempt = 0; attempt < 3 && !body; attempt++) {
|
|
753
|
-
const resp = await gotScraping({
|
|
754
|
-
url: targetUrl,
|
|
755
|
-
method: 'GET',
|
|
756
|
-
headers,
|
|
757
|
-
responseType: 'buffer',
|
|
758
|
-
followRedirect: true,
|
|
759
|
-
throwHttpErrors: false,
|
|
760
|
-
timeout: { request: 30000 },
|
|
761
|
-
});
|
|
762
|
-
if (resp.statusCode === 200 && resp.rawBody?.length) {
|
|
763
|
-
body = Buffer.from(resp.rawBody);
|
|
764
|
-
contentType = String(resp.headers['content-type'] || 'image/webp');
|
|
765
|
-
}
|
|
766
|
-
else {
|
|
767
|
-
if (attempt < 2)
|
|
768
|
-
await new Promise((r) => setTimeout(r, 400 * (attempt + 1)));
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
if (body) {
|
|
772
|
-
imageCache.set(cacheKey, { body, contentType });
|
|
773
|
-
const outHeaders = new Headers();
|
|
774
|
-
outHeaders.set('Cache-Control', 'public, max-age=604800, immutable');
|
|
775
|
-
outHeaders.set('Content-Type', contentType);
|
|
776
|
-
outHeaders.set('Access-Control-Allow-Origin', '*');
|
|
777
|
-
return new Response(body, { headers: outHeaders });
|
|
778
|
-
}
|
|
779
|
-
// strict=1 callers retry another candidate instead of settling for the
|
|
780
|
-
// placeholder, which would otherwise read as a successful load.
|
|
781
|
-
if (strict)
|
|
782
|
-
return c.text('Not Found', 404);
|
|
783
|
-
const placeholder = placeholderSvg();
|
|
784
|
-
if (placeholder)
|
|
785
|
-
return placeholder;
|
|
786
|
-
return c.text('Not Found', 404);
|
|
787
|
-
}
|
|
788
|
-
catch {
|
|
789
|
-
if (abort.signal.aborted)
|
|
790
|
-
return new Response(null, { status: 499 });
|
|
791
|
-
if (strict)
|
|
792
|
-
return c.text('Not Found', 502);
|
|
793
|
-
const placeholder = placeholderSvg();
|
|
794
|
-
if (placeholder)
|
|
795
|
-
return placeholder;
|
|
796
|
-
return c.text('Not Found', 404);
|
|
797
|
-
}
|
|
798
|
-
});
|
|
799
|
-
app.get('/api/resolve', async (c) => {
|
|
800
|
-
const targetUrl = c.req.query('url');
|
|
801
|
-
if (!targetUrl || typeof targetUrl !== 'string') {
|
|
802
|
-
return c.json({ error: 'URL required' }, 400);
|
|
803
|
-
}
|
|
804
|
-
const safeCheck = isSafeExternalUrl(targetUrl);
|
|
805
|
-
if (!safeCheck.safe) {
|
|
806
|
-
return c.json({ error: safeCheck.error || 'Invalid URL' }, 400);
|
|
807
|
-
}
|
|
808
|
-
try {
|
|
809
|
-
const response = await fetch(targetUrl, {
|
|
810
|
-
method: 'GET',
|
|
811
|
-
headers: {
|
|
812
|
-
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
813
|
-
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
814
|
-
},
|
|
815
|
-
redirect: 'follow',
|
|
816
|
-
});
|
|
817
|
-
return c.json({
|
|
818
|
-
finalUrl: response.url,
|
|
819
|
-
status: response.status,
|
|
820
|
-
});
|
|
821
|
-
}
|
|
822
|
-
catch (e) {
|
|
823
|
-
const err = e;
|
|
824
|
-
return c.json({ error: err.message || 'Resolve failed' }, 500);
|
|
825
|
-
}
|
|
826
|
-
});
|
|
827
|
-
}
|
|
67
|
+
</script></body>`);return w===c?null:w}function Y(){const c=[j.join(N.PACKAGE_ROOT,"client/public/placeholder.svg"),j.join(N.PACKAGE_ROOT,"client/dist/placeholder.svg"),j.join(N.SERVER_ROOT,"..","client/public/placeholder.svg")];for(const o of c)try{if(D.existsSync(o)){const e=new Headers;return e.set("Cache-Control","no-store"),e.set("Content-Type","image/svg+xml"),new Response(D.readFileSync(o),{headers:e})}}catch{}return null}const Z="Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0";function ye(c,o){c.get("/api/proxy",async e=>{const r=e.req.query("url");if(!r)return e.text("URL required",400);const i=W(r);if(!i.safe)return e.text(i.error||"Invalid URL",400);const t=r,n=I(t,o()),s=e.req.query("referer")||n?.referer||"",d=B(n,s,e.req.query("origin")),m=e.req.query("ua")||"",R=e.req.query("cookie")||"",w=e.req.query("scrape")==="1"||n?.scrape===!0,T=e.req.query("cf")==="1"||n?.cfCookie===!0,h=e.req.query("variant"),b=h!==void 0?parseInt(h,10):NaN,l=Number.isInteger(b)&&b>=0,u=`m3u8-${t}-${s}-${d}-${m}${l?`-v${b}`:""}`,y=new AbortController;L(e.req.raw.signal,y);try{const f={"User-Agent":m||Z};if(s&&(f.Referer=s),d&&(f.Origin=d),R){const a=T?V(R):R;a&&(f.Cookie=a)}const p=e.req.header("range");p&&(f.Range=p);const U=a=>{const g=new Headers;return g.set("Content-Type","application/vnd.apple.mpegurl"),g.set("Access-Control-Allow-Origin","*"),new Response(a,{headers:g})},E=(a,g)=>{const x=new URL(g),q=k=>{const v=new URLSearchParams({url:k});return s&&v.set("referer",s),d&&v.set("origin",d),m&&v.set("ua",m),R&&v.set("cookie",ee(R)),w&&v.set("scrape","1"),T&&v.set("cf","1"),`/api/proxy?${v}`},$=!!(s||d||m||w),C=k=>k.includes("/api/proxy");return((l?oe(a,b):null)||a).split(`
|
|
68
|
+
`).map(k=>{const v=k.trim();if(!v)return k;if(v.startsWith("#"))return v.replace(/URI="([^"]+)"/g,(ae,F)=>{if(C(F))return`URI="${F}"`;const M=new URL(F,x).href;return`URI="${$||M.includes(".m3u8")?q(M):M}"`});if(C(v))return k;const Q=new URL(v,x).href;return q(Q)}).join(`
|
|
69
|
+
`)};if(t.includes(".m3u8")){const a=O.get(u);if(a){const q=new Headers;return q.set("Content-Type","application/vnd.apple.mpegurl"),q.set("Access-Control-Allow-Origin","*"),new Response(a,{headers:q})}const g=await K(t,n?.mirrors,y.signal,q=>H({url:q,method:"GET",headers:f,responseType:"text",timeout:{request:3e4},followRedirect:!0,throwHttpErrors:!1}),q=>q.statusCode===200||q.statusCode===206);if(!g||g.statusCode!==200&&g.statusCode!==206)return e.text("Upstream error",g?.statusCode||502);const x=E(g.body,g.url||t);return O.set(u,x),U(x)}else if(w){p&&(f.Range=p);const a=await K(t,n?.mirrors,y.signal,C=>H({url:C,method:"GET",headers:f,responseType:"buffer",timeout:{request:3e4},followRedirect:!0,throwHttpErrors:!1}),C=>C.statusCode===200||C.statusCode===206);if(!a||a.statusCode!==200&&a.statusCode!==206)return e.text("Upstream error",a?.statusCode||502);const g=a.headers["content-type"],x=t.endsWith(".vtt")||t.includes(".vtt?"),q=String(a.body??"");if(!x&&(typeof g=="string"&&g.includes("mpegurl")||q.startsWith("#EXTM3U"))){const C=O.get(u),S=C??E(q,a.url||t);return C||O.set(u,S),U(S)}const $=new Headers;return $.set("Content-Type",x?"text/vtt; charset=utf-8":g||"application/octet-stream"),X($,C=>a.headers[C]),new Response(a.body,{status:a.statusCode,headers:$})}else{const a=await K(t,n?.mirrors,y.signal,async C=>{const S=await G(C,{method:"GET",headers:f},{retries:3,timeoutMs:3e4,signal:y.signal});if(S.status!==200&&S.status!==206)try{await S.body?.cancel()}catch{}return S},C=>C.status===200||C.status===206);if(!a)return e.text("Upstream error",502);const g=a.status;if(g!==200&&g!==206){try{await a.body?.cancel()}catch{}return e.text("Upstream error",g)}const x=t.endsWith(".vtt")||t.includes(".vtt?"),q=a.headers.get("content-type"),$=new Headers;return $.set("Content-Type",x?"text/vtt; charset=utf-8":q||"application/octet-stream"),X($,C=>a.headers.get(C)),a.body?new Response(a.body,{status:g,headers:$}):e.text("Upstream error",502)}}catch(f){return y.signal.aborted?new Response(null,{status:499}):(A.error({url:t,error:f?.message},"[proxy] upstream request failed"),e.text("Proxy error",500))}}),c.get("/api/embed-proxy",async e=>{const r=e.req.query("url");if(!r)return e.text("URL required",400);const i=W(r);if(!i.safe)return e.text(i.error||"Invalid URL",400);const t=I(r,o());if(!t?.embedPatch)return e.text("Invalid or unsupported gateway URL",400);const n=new URL(r),s=e.req.query("cookie")||"",d=e.req.query("referer")||t.referer||"",m=B(t,d,e.req.query("origin")),R=new AbortController;L(e.req.raw.signal,R);try{const w={"User-Agent":e.req.query("ua")||Z};if(d&&(w.Referer=d),m&&(w.Origin=m),s){const u=e.req.query("cf")==="1"||t.cfCookie?V(s):s;u&&(w.Cookie=u)}const h=(await H({url:n.href,method:"GET",headers:w,responseType:"text",timeout:{request:3e4},followRedirect:!0,throwHttpErrors:!1})).body,b=ie(h,n,s);if(!b)return e.text("Failed to patch video gateway",502);const l=new Headers;return l.set("Content-Type","text/html; charset=utf-8"),l.set("Cache-Control","private, max-age=120"),new Response(b,{status:200,headers:l})}catch{return R.signal.aborted?new Response(null,{status:499}):e.text("Gateway proxy error",502)}}),c.get("/api/subtitle-proxy",async e=>{const r=e.req.query("url");if(!r)return e.text("URL required",400);const i=W(r);if(!i.safe)return e.text(i.error||"Invalid URL",400);const t=new AbortController;L(e.req.raw.signal,t);const n=r,s=I(n,o()),d=e.req.query("referer")||s?.referer||"",m=B(s,d,e.req.query("origin")),R=e.req.query("scrape")==="1"||s?.scrape===!0,w=()=>{const l={"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",Accept:"text/vtt,text/plain,*/*;q=0.8"};return d&&(l.Referer=d),m&&(l.Origin=m),l},T=async(l,u=1)=>{const y=w();if(R)try{const f=await H({url:l,method:"GET",headers:y,responseType:"text",timeout:{request:15e3},followRedirect:!0,throwHttpErrors:!1});if(f.statusCode!==200)return A.warn({target:l,status:f.statusCode},"[subtitle-proxy] upstream returned error status"),{status:f.statusCode,body:""};const p=String(f.body??"").replace(/^\uFEFF/,"");return/^\s*\{[\s\S]*"error"[\s\S]*\}\s*$/.test(p.slice(0,500))?(A.warn({target:l,body:p.slice(0,200)},"[subtitle-proxy] upstream JSON error"),{status:502,body:""}):{status:200,body:p}}catch(f){return t.signal.aborted?null:(A.warn({target:l,error:f?.message},"[subtitle-proxy] upstream fetch failed"),{status:502,body:""})}try{const f=await G(l,{method:"GET",headers:y},{retries:u,timeoutMs:15e3,signal:t.signal});if(!f.ok){A.warn({target:l,status:f.status},"[subtitle-proxy] upstream returned error status");try{await f.body?.cancel()}catch{}return{status:f.status,body:""}}const p=(await f.text()).replace(/^\uFEFF/,"");return/^\s*\{[\s\S]*"error"[\s\S]*\}\s*$/.test(p.slice(0,500))?(A.warn({target:l,body:p.slice(0,200)},"[subtitle-proxy] upstream JSON error"),{status:502,body:""}):{status:f.status,body:p}}catch(f){return t.signal.aborted?null:(A.warn({target:l,error:f?.message},"[subtitle-proxy] upstream fetch failed"),{status:502,body:""})}},h=l=>{const u=new Headers;return u.set("Content-Type","text/vtt; charset=utf-8"),u.set("Access-Control-Allow-Origin","*"),u.set("Cache-Control",l),u},b=l=>{const u=new Headers;return u.set("Access-Control-Allow-Origin","*"),u.set("Cache-Control","no-store"),new Response(l,{status:502,headers:u})};try{const l=w(),u=await T(n,1);if(t.signal.aborted)return new Response(null,{status:499});if(!u||!u.body)return b("Subtitle upstream error");const y=u.body,f=n;if(/#EXTM3U/i.test(y.slice(0,1e3))){const U=y.split(`
|
|
70
|
+
`).map(a=>a.trim()).filter(a=>a&&!a.startsWith("#")).slice(0,200).map(a=>{try{return new URL(a,f).href}catch{return null}}).filter(a=>!!a),E=[];for(const a of U)try{const g=await G(a,{method:"GET",headers:l},{retries:1,timeoutMs:15e3,signal:t.signal});if(!g.ok)continue;let x=(await g.text()).replace(/^\uFEFF/,"");if(/#EXTM3U/i.test(x.slice(0,200)))continue;if(x=x.replace(/\r\n/g,`
|
|
71
|
+
`).replace(/(\d{2}:\d{2}:\d{2}),(\d{3})/g,"$1.$2"),/^\s*WEBVTT/i.test(x)&&(x=x.replace(/^\s*WEBVTT[^\n]*\n(\n|.*\n)?/,"")),x=x.trim(),x&&E.push(x),E.join(`
|
|
72
|
+
`).length>2e6)break}catch{if(t.signal.aborted)return new Response(null,{status:499})}return E.length===0?b("Subtitle playlist empty"):new Response(P(`WEBVTT
|
|
73
|
+
|
|
74
|
+
${E.join(`
|
|
75
|
+
|
|
76
|
+
`)}
|
|
77
|
+
`),{headers:h("public, max-age=86400")})}if(/^\s*WEBVTT/i.test(y))return new Response(P(y),{headers:h("public, max-age=86400")});const p=te(y);return p?new Response(P(p),{headers:h("public, max-age=86400")}):new Response(P(`WEBVTT
|
|
78
|
+
|
|
79
|
+
${y.replace(/\r\n/g,`
|
|
80
|
+
`).replace(/(\d{2}:\d{2}:\d{2}),(\d{3})/g,"$1.$2")}`),{headers:h("public, max-age=86400")})}catch{return t.signal.aborted?new Response(null,{status:499}):(A.warn({url:n},"[subtitle-proxy] failed"),b("Subtitle upstream error"))}}),c.get("/api/image-proxy",async e=>{const r=e.req.query("url");if(!r)return e.text("URL required",400);const i=W(r);if(!i.safe)return e.text(i.error||"Invalid URL",400);const t=r,n=e.req.query("cookie")||"",s=e.req.query("ua")||"",d=e.req.query("strict")==="1",m=I(t,o()),R=e.req.query("referer")||m?.referer||"",w=B(m,R,e.req.query("origin")),T=new AbortController;L(e.req.raw.signal,T);const h={"User-Agent":s||"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"},b=`${t}|${n}|${s}|${R}|${w}`,l=z.get(b);if(l){const u=new Headers;return u.set("Cache-Control","public, max-age=604800, immutable"),u.set("Content-Type",l.contentType),u.set("Access-Control-Allow-Origin","*"),u.set("X-Dango-Cache","hit"),new Response(l.body,{headers:u})}try{if(R&&(h.Referer=R),w&&(h.Origin=w),e.req.query("cf")==="1"||m?.cfCookie){const p=V(n);p&&(h.Cookie=p)}let u=null,y="image/webp";for(let p=0;p<3&&!u;p++){const U=await H({url:t,method:"GET",headers:h,responseType:"buffer",followRedirect:!0,throwHttpErrors:!1,timeout:{request:3e4}});U.statusCode===200&&U.rawBody?.length?(u=Buffer.from(U.rawBody),y=String(U.headers["content-type"]||"image/webp")):p<2&&await new Promise(E=>setTimeout(E,400*(p+1)))}if(u){z.set(b,{body:u,contentType:y});const p=new Headers;return p.set("Cache-Control","public, max-age=604800, immutable"),p.set("Content-Type",y),p.set("Access-Control-Allow-Origin","*"),new Response(u,{headers:p})}if(d)return e.text("Not Found",404);const f=Y();return f||e.text("Not Found",404)}catch{if(T.signal.aborted)return new Response(null,{status:499});if(d)return e.text("Not Found",502);const u=Y();return u||e.text("Not Found",404)}}),c.get("/api/resolve",async e=>{const r=e.req.query("url");if(!r||typeof r!="string")return e.json({error:"URL required"},400);const i=W(r);if(!i.safe)return e.json({error:i.error||"Invalid URL"},400);try{const t=await fetch(r,{method:"GET",headers:{"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",Accept:"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"},redirect:"follow"});return e.json({finalUrl:t.url,status:t.status})}catch(t){const n=t;return e.json({error:n.message||"Resolve failed"},500)}})}export{ye as registerProxy};
|