@serifpersia/dango 3.0.3
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/LICENSE +21 -0
- package/README.md +247 -0
- package/client/dist/assets/AnimeInfo-CMxs0nRH.css +1 -0
- package/client/dist/assets/AnimeInfo-DVdaocCl.js +1 -0
- package/client/dist/assets/AnimeInfoPage-Bx5EvGt-.js +2 -0
- package/client/dist/assets/Asmr-C2i9GKD4.css +1 -0
- package/client/dist/assets/Asmr-CIjmFtRH.js +2 -0
- package/client/dist/assets/Asmr.module-DPG2JhDJ.js +1 -0
- package/client/dist/assets/AvSyncCalibrator-CIYcyon5.css +1 -0
- package/client/dist/assets/AvSyncCalibrator-vqErH7Cm.js +10 -0
- package/client/dist/assets/ErrorMessage-BwSjzblw.js +1 -0
- package/client/dist/assets/ErrorMessage-CXoRbrLN.css +1 -0
- package/client/dist/assets/Home-D7YfGBUk.js +1 -0
- package/client/dist/assets/Home-DKCfEiwl.css +1 -0
- package/client/dist/assets/Insights-BljI64Ua.css +1 -0
- package/client/dist/assets/Insights-CLQGrlpX.js +1 -0
- package/client/dist/assets/Manga-DQ9HYnpk.js +1 -0
- package/client/dist/assets/Manga-H8Wb04w3.css +1 -0
- package/client/dist/assets/Map-CXFqXmsZ.css +1 -0
- package/client/dist/assets/Map-L_ryVBTq.js +1 -0
- package/client/dist/assets/Mature-BeWzAJ7B.js +1 -0
- package/client/dist/assets/Mature-DJmUUtlF.css +1 -0
- package/client/dist/assets/Player-CYpd__FP.js +30 -0
- package/client/dist/assets/Player-DreAhybc.css +1 -0
- package/client/dist/assets/PlayerRedirect-fxUAp9JV.js +1 -0
- package/client/dist/assets/PlayerSettings-BcsotxP9.js +1 -0
- package/client/dist/assets/PlayerSettings-Cqua1CDD.css +1 -0
- package/client/dist/assets/QueueRail-B9Pgbqc3.js +1 -0
- package/client/dist/assets/QueueRail-BnaltOP8.css +1 -0
- package/client/dist/assets/Radio-BvdY5twD.js +1 -0
- package/client/dist/assets/Radio-CUuYy3DY.css +1 -0
- package/client/dist/assets/Search-BzO-aRP7.css +1 -0
- package/client/dist/assets/Search-R9YLAXS1.js +1 -0
- package/client/dist/assets/Search.module-D8ZbQAiK.js +1 -0
- package/client/dist/assets/Settings-9kYcXdTR.js +21 -0
- package/client/dist/assets/Settings-DF8JThUL.css +1 -0
- package/client/dist/assets/SkeletonGrid-D5vRvX5q.js +1 -0
- package/client/dist/assets/SkeletonGrid-DEbYtO4H.css +1 -0
- package/client/dist/assets/SynopsisText-BlFw0R-0.js +1 -0
- package/client/dist/assets/SynopsisText-CUgOjOxt.css +1 -0
- package/client/dist/assets/Trackers-DfZlnnVq.js +2 -0
- package/client/dist/assets/Trackers-Dt9lR_uP.css +1 -0
- package/client/dist/assets/Tv-CVKC2YBs.css +1 -0
- package/client/dist/assets/Tv-Db1Wx_b7.js +15 -0
- package/client/dist/assets/Watchlist-B2nkNZqM.css +1 -0
- package/client/dist/assets/Watchlist-a77CYtzu.js +1 -0
- package/client/dist/assets/hls-6Owgw2Aj.js +1 -0
- package/client/dist/assets/hls-BOLXTnjL.js +42 -0
- package/client/dist/assets/index-B6qdXFtl.css +1 -0
- package/client/dist/assets/index-B8TPVaIS.js +30354 -0
- package/client/dist/assets/index-CpjgU8ou.js +178 -0
- package/client/dist/assets/useAnimeInfoData-BOfyi9UJ.js +1 -0
- package/client/dist/assets/useIsMobile-CM15uQn8.css +1 -0
- package/client/dist/assets/useIsMobile-Cb8HcKRj.js +1 -0
- package/client/dist/assets/useMatureConsent-CZMYBVXd.js +1 -0
- package/client/dist/assets/vendor-rLNfZm7Y.js +1 -0
- package/client/dist/av-sync-test.mp4 +0 -0
- package/client/dist/favicon.ico +0 -0
- package/client/dist/index.html +31 -0
- package/client/dist/logo-mochi.png +0 -0
- package/client/dist/logo-sakura.png +0 -0
- package/client/dist/logo.png +0 -0
- package/client/dist/placeholder.svg +4 -0
- package/client/dist/robots.txt +3 -0
- package/orchestrator.js +416 -0
- package/package.json +89 -0
- package/server/dist/app-auth.js +231 -0
- package/server/dist/config.js +133 -0
- package/server/dist/controllers/auth.controller.js +247 -0
- package/server/dist/controllers/data.controller.js +785 -0
- package/server/dist/controllers/insights.controller.js +197 -0
- package/server/dist/controllers/proxy.controller.js +665 -0
- package/server/dist/controllers/settings.controller.js +267 -0
- package/server/dist/controllers/watchlist.controller.js +967 -0
- package/server/dist/db.js +152 -0
- package/server/dist/discord-gateway.js +381 -0
- package/server/dist/discord-rpc.js +424 -0
- package/server/dist/github-sync.js +403 -0
- package/server/dist/google.js +549 -0
- package/server/dist/lib/anilist.js +1519 -0
- package/server/dist/lib/anischedule.js +202 -0
- package/server/dist/lib/ipc.js +32 -0
- package/server/dist/lib/kitsu.js +552 -0
- package/server/dist/lib/mal.js +886 -0
- package/server/dist/lib/migration.js +335 -0
- package/server/dist/lib/temp-ids.js +11 -0
- package/server/dist/lib/tmdb.js +71 -0
- package/server/dist/lib/tracker/anilist-tracker.js +250 -0
- package/server/dist/lib/tracker/sync.service.js +312 -0
- package/server/dist/logger.js +19 -0
- package/server/dist/providers/123anime.provider.js +240 -0
- package/server/dist/providers/anilight.provider.js +366 -0
- package/server/dist/providers/animepahe.provider.js +531 -0
- package/server/dist/providers/animeya.provider.js +746 -0
- package/server/dist/providers/base-provider.js +45 -0
- package/server/dist/providers/hn.provider.js +473 -0
- package/server/dist/providers/ht.provider.js +439 -0
- package/server/dist/providers/jasmr.provider.js +504 -0
- package/server/dist/providers/kaa.provider.js +388 -0
- package/server/dist/providers/manga/manga.types.js +10 -0
- package/server/dist/providers/manga/mangadex.provider.js +227 -0
- package/server/dist/providers/manga/mangapill.provider.js +232 -0
- package/server/dist/providers/megaplay.provider.js +583 -0
- package/server/dist/providers/op.provider.js +440 -0
- package/server/dist/providers/provider.interface.js +2 -0
- package/server/dist/providers/title-matching.js +234 -0
- package/server/dist/providers/wh.provider.js +600 -0
- package/server/dist/rclone.js +126 -0
- package/server/dist/repositories/insights.repository.js +43 -0
- package/server/dist/repositories/mal-cache.repository.js +66 -0
- package/server/dist/repositories/notifications.repository.js +40 -0
- package/server/dist/repositories/queue.repository.js +64 -0
- package/server/dist/repositories/settings.repository.js +15 -0
- package/server/dist/repositories/shows-meta.repository.js +43 -0
- package/server/dist/repositories/temp-show-ids.repository.js +54 -0
- package/server/dist/repositories/watched-episodes.repository.js +54 -0
- package/server/dist/repositories/watchlist.repository.js +74 -0
- package/server/dist/routes/asmr.routes.js +72 -0
- package/server/dist/routes/auth.routes.js +28 -0
- package/server/dist/routes/data.routes.js +50 -0
- package/server/dist/routes/discord-gateway.routes.js +57 -0
- package/server/dist/routes/insights.routes.js +12 -0
- package/server/dist/routes/lan-auth.routes.js +49 -0
- package/server/dist/routes/manga.routes.js +115 -0
- package/server/dist/routes/proxy.routes.js +15 -0
- package/server/dist/routes/radio.routes.js +143 -0
- package/server/dist/routes/settings.routes.js +27 -0
- package/server/dist/routes/tracker.routes.js +143 -0
- package/server/dist/routes/translate.routes.js +90 -0
- package/server/dist/routes/tv.routes.js +843 -0
- package/server/dist/routes/watchlist.routes.js +312 -0
- package/server/dist/server.js +310 -0
- package/server/dist/shipped-defaults.js +8 -0
- package/server/dist/sync-config.js +32 -0
- package/server/dist/sync.js +506 -0
- package/server/dist/utils/cookie.utils.js +17 -0
- package/server/dist/utils/db-utils.js +15 -0
- package/server/dist/utils/env.utils.js +83 -0
- package/server/dist/utils/machine-id.js +46 -0
- package/server/dist/utils/request-context.js +5 -0
- package/server/dist/utils/security.utils.js +99 -0
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ProxyController = exports.axiosInstance = void 0;
|
|
7
|
+
const axios_1 = __importDefault(require("axios"));
|
|
8
|
+
const axios_retry_1 = __importDefault(require("axios-retry"));
|
|
9
|
+
const got_scraping_1 = require("got-scraping");
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const http_1 = __importDefault(require("http"));
|
|
12
|
+
const https_1 = __importDefault(require("https"));
|
|
13
|
+
const node_cache_1 = __importDefault(require("node-cache"));
|
|
14
|
+
const config_1 = require("../config");
|
|
15
|
+
const fs_1 = __importDefault(require("fs"));
|
|
16
|
+
const logger_1 = __importDefault(require("../logger"));
|
|
17
|
+
const cookie_utils_1 = require("../utils/cookie.utils");
|
|
18
|
+
const security_utils_1 = require("../utils/security.utils");
|
|
19
|
+
function firstString(value) {
|
|
20
|
+
if (typeof value === 'string')
|
|
21
|
+
return value;
|
|
22
|
+
if (Array.isArray(value)) {
|
|
23
|
+
const first = value[0];
|
|
24
|
+
if (typeof first === 'string')
|
|
25
|
+
return first;
|
|
26
|
+
}
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
const proxyCache = new node_cache_1.default({ stdTTL: 30, checkperiod: 60 });
|
|
30
|
+
const httpAgent = new http_1.default.Agent({ keepAlive: true, maxSockets: 100 });
|
|
31
|
+
const httpsAgent = new https_1.default.Agent({ keepAlive: true, maxSockets: 100 });
|
|
32
|
+
httpAgent.setMaxListeners(100);
|
|
33
|
+
httpsAgent.setMaxListeners(100);
|
|
34
|
+
exports.axiosInstance = axios_1.default.create({
|
|
35
|
+
httpAgent,
|
|
36
|
+
httpsAgent,
|
|
37
|
+
timeout: 30000,
|
|
38
|
+
});
|
|
39
|
+
(0, axios_retry_1.default)(exports.axiosInstance, { retries: 3, retryDelay: axios_retry_1.default.exponentialDelay });
|
|
40
|
+
class ProxyController {
|
|
41
|
+
static KWIK_DOMAINS = new Set(['kwik.cx', 'kwik.si', 'kwik.pro']);
|
|
42
|
+
static ANIMEPAHE_URL = 'https://animepahe.pw/';
|
|
43
|
+
static VAULT_CDN_HOSTS = new Set(['uwucdn.top', 'owocdn.top']);
|
|
44
|
+
static GOT_SCRAPING_HOSTS = new Set([
|
|
45
|
+
'kwik.cx',
|
|
46
|
+
'kwik.si',
|
|
47
|
+
'kwik.pro',
|
|
48
|
+
'animepahe.pw',
|
|
49
|
+
'animepahe.ru',
|
|
50
|
+
]);
|
|
51
|
+
static GOT_SCRAPING_SUFFIXES = [
|
|
52
|
+
'.uwucdn.top',
|
|
53
|
+
'.owocdn.top',
|
|
54
|
+
'.streamzone1.site',
|
|
55
|
+
'.nekostream.site',
|
|
56
|
+
'.nukitashith.top',
|
|
57
|
+
'.aniwatchtv.site',
|
|
58
|
+
];
|
|
59
|
+
static HANIME_HOSTS = new Set(['r2.1hanime.com', '1.1hanime.com']);
|
|
60
|
+
static OPPAI_HOSTS = new Set(['myspacecat.pictures']);
|
|
61
|
+
static KAA_HOSTS = new Set([
|
|
62
|
+
'hls.krussdomi.com',
|
|
63
|
+
'subst.krussdomi.com',
|
|
64
|
+
'krussdomi.com',
|
|
65
|
+
'st1.advancedairesearchlab.xyz',
|
|
66
|
+
'st1.habibikun.xyz',
|
|
67
|
+
'st1.babybayw.xyz',
|
|
68
|
+
'st1.narutokun.xyz',
|
|
69
|
+
]);
|
|
70
|
+
static KAA_REFERER = 'https://krussdomi.com/';
|
|
71
|
+
static KAA_ORIGIN = 'https://krussdomi.com';
|
|
72
|
+
static isGotScrapingHost(urlStr) {
|
|
73
|
+
try {
|
|
74
|
+
const host = new URL(urlStr).hostname.toLowerCase();
|
|
75
|
+
if (ProxyController.GOT_SCRAPING_HOSTS.has(host))
|
|
76
|
+
return true;
|
|
77
|
+
if (ProxyController.HANIME_HOSTS.has(host))
|
|
78
|
+
return true;
|
|
79
|
+
return ProxyController.GOT_SCRAPING_SUFFIXES.some((s) => host.endsWith(s));
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
abortWhenClientLeaves(res, abortController) {
|
|
86
|
+
res.on('close', () => {
|
|
87
|
+
if (!res.writableEnded) {
|
|
88
|
+
abortController.abort();
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
validateKwikUrl(value) {
|
|
93
|
+
if (typeof value !== 'string')
|
|
94
|
+
return null;
|
|
95
|
+
try {
|
|
96
|
+
const url = new URL(value);
|
|
97
|
+
const isSecure = url.protocol === 'https:';
|
|
98
|
+
const isKwik = ProxyController.KWIK_DOMAINS.has(url.hostname.toLowerCase());
|
|
99
|
+
const isEmbedPath = /^\/e\/[A-Za-z0-9_-]+$/.test(url.pathname);
|
|
100
|
+
const noAuthOrQuery = !url.username && !url.password && !url.search && !url.hash;
|
|
101
|
+
return isSecure && isKwik && isEmbedPath && noAuthOrQuery ? url : null;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
pinVariant(body, idx) {
|
|
108
|
+
const lines = body.split('\n');
|
|
109
|
+
let streamIdx = -1;
|
|
110
|
+
let skipUri = false;
|
|
111
|
+
const out = [];
|
|
112
|
+
for (const line of lines) {
|
|
113
|
+
const trimmed = line.trim();
|
|
114
|
+
if (trimmed.startsWith('#EXT-X-STREAM-INF')) {
|
|
115
|
+
streamIdx++;
|
|
116
|
+
skipUri = streamIdx !== idx;
|
|
117
|
+
if (!skipUri)
|
|
118
|
+
out.push(line);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (skipUri && trimmed && !trimmed.startsWith('#')) {
|
|
122
|
+
skipUri = false;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
out.push(line);
|
|
126
|
+
}
|
|
127
|
+
if (streamIdx < 0 || idx > streamIdx)
|
|
128
|
+
return null;
|
|
129
|
+
return out.join('\n');
|
|
130
|
+
}
|
|
131
|
+
handleProxy = async (req, res) => {
|
|
132
|
+
const { url, referer, cookie } = req.query;
|
|
133
|
+
if (!url)
|
|
134
|
+
return res.status(400).send('URL required');
|
|
135
|
+
const safeCheck = (0, security_utils_1.isSafeExternalUrl)(url);
|
|
136
|
+
if (!safeCheck.safe) {
|
|
137
|
+
return res.status(400).send(safeCheck.error || 'Invalid URL');
|
|
138
|
+
}
|
|
139
|
+
const urlStr = url;
|
|
140
|
+
const refererStr = referer || '';
|
|
141
|
+
const cookieStr = cookie || '';
|
|
142
|
+
const variantIdx = req.query.variant !== undefined ? parseInt(req.query.variant, 10) : NaN;
|
|
143
|
+
const pinVariant = Number.isInteger(variantIdx) && variantIdx >= 0;
|
|
144
|
+
const cacheKey = `m3u8-${urlStr}-${refererStr}${pinVariant ? `-v${variantIdx}` : ''}`;
|
|
145
|
+
const abortController = new AbortController();
|
|
146
|
+
this.abortWhenClientLeaves(res, abortController);
|
|
147
|
+
try {
|
|
148
|
+
const isVaultCdn = Array.from(ProxyController.VAULT_CDN_HOSTS).some((host) => urlStr.includes(host));
|
|
149
|
+
const isHanime = Array.from(ProxyController.HANIME_HOSTS).some((host) => urlStr.includes(host));
|
|
150
|
+
const isOppai = Array.from(ProxyController.OPPAI_HOSTS).some((host) => urlStr.includes(host));
|
|
151
|
+
const isKaa = refererStr.startsWith(ProxyController.KAA_ORIGIN) ||
|
|
152
|
+
Array.from(ProxyController.KAA_HOSTS).some((host) => urlStr.includes(host));
|
|
153
|
+
const headers = {
|
|
154
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0',
|
|
155
|
+
};
|
|
156
|
+
if (referer)
|
|
157
|
+
headers['Referer'] = refererStr;
|
|
158
|
+
if (isVaultCdn) {
|
|
159
|
+
headers['Origin'] = refererStr || ProxyController.ANIMEPAHE_URL;
|
|
160
|
+
const cookieHeader = (0, cookie_utils_1.buildCfClearanceCookie)(cookieStr);
|
|
161
|
+
if (cookieHeader)
|
|
162
|
+
headers['Cookie'] = cookieHeader;
|
|
163
|
+
}
|
|
164
|
+
if (isHanime) {
|
|
165
|
+
if (!headers['Referer'])
|
|
166
|
+
headers['Referer'] = refererStr || 'https://nhplayer.com/';
|
|
167
|
+
headers['Origin'] = 'https://nhplayer.com';
|
|
168
|
+
}
|
|
169
|
+
if (isOppai) {
|
|
170
|
+
if (!headers['Referer'])
|
|
171
|
+
headers['Referer'] = refererStr || 'https://oppai.stream/';
|
|
172
|
+
headers['Origin'] = 'https://oppai.stream';
|
|
173
|
+
}
|
|
174
|
+
if (isKaa) {
|
|
175
|
+
if (!headers['Referer'])
|
|
176
|
+
headers['Referer'] = refererStr || ProxyController.KAA_REFERER;
|
|
177
|
+
headers['Origin'] = ProxyController.KAA_ORIGIN;
|
|
178
|
+
}
|
|
179
|
+
if (urlStr.includes('weeabo0.xyz') || urlStr.includes('weeab0o.xyz')) {
|
|
180
|
+
if (!headers['Referer'])
|
|
181
|
+
headers['Referer'] = refererStr || 'https://japaneseasmr.com/';
|
|
182
|
+
const jasmrCookieHeader = (0, cookie_utils_1.buildCfClearanceCookie)(cookieStr);
|
|
183
|
+
if (jasmrCookieHeader)
|
|
184
|
+
headers['Cookie'] = jasmrCookieHeader;
|
|
185
|
+
const jasmrUa = req.query.ua || '';
|
|
186
|
+
if (jasmrUa)
|
|
187
|
+
headers['User-Agent'] = jasmrUa;
|
|
188
|
+
}
|
|
189
|
+
if (req.headers.range)
|
|
190
|
+
headers['Range'] = req.headers.range;
|
|
191
|
+
if (urlStr.includes('.m3u8')) {
|
|
192
|
+
const cached = proxyCache.get(cacheKey);
|
|
193
|
+
if (cached) {
|
|
194
|
+
return res
|
|
195
|
+
.set('Content-Type', 'application/vnd.apple.mpegurl')
|
|
196
|
+
.set('Access-Control-Allow-Origin', '*')
|
|
197
|
+
.send(cached);
|
|
198
|
+
}
|
|
199
|
+
const resp = await (0, got_scraping_1.gotScraping)({
|
|
200
|
+
url: urlStr,
|
|
201
|
+
method: 'GET',
|
|
202
|
+
headers,
|
|
203
|
+
responseType: 'text',
|
|
204
|
+
timeout: { request: 30000 },
|
|
205
|
+
followRedirect: true,
|
|
206
|
+
throwHttpErrors: false,
|
|
207
|
+
});
|
|
208
|
+
if (resp.statusCode !== 200 && resp.statusCode !== 206) {
|
|
209
|
+
return res.status(resp.statusCode ?? 502).send('Upstream error');
|
|
210
|
+
}
|
|
211
|
+
const finalUrl = resp.url || urlStr;
|
|
212
|
+
const baseUrl = new URL(finalUrl);
|
|
213
|
+
const proxiedMediaUrl = (targetUrl) => `/api/proxy?url=${encodeURIComponent(targetUrl)}&referer=${encodeURIComponent(refererStr)}` +
|
|
214
|
+
(cookieStr ? `&cookie=${encodeURIComponent((0, cookie_utils_1.sanitizeCfClearance)(cookieStr))}` : '') +
|
|
215
|
+
(req.query.ua ? `&ua=${encodeURIComponent(req.query.ua)}` : '');
|
|
216
|
+
const needsProxy = Boolean(refererStr);
|
|
217
|
+
const isProxied = (value) => value.includes('/api/proxy');
|
|
218
|
+
let playlistBody = resp.body;
|
|
219
|
+
if (pinVariant) {
|
|
220
|
+
const pinned = this.pinVariant(playlistBody, variantIdx);
|
|
221
|
+
if (pinned)
|
|
222
|
+
playlistBody = pinned;
|
|
223
|
+
}
|
|
224
|
+
const rewritten = playlistBody
|
|
225
|
+
.split('\n')
|
|
226
|
+
.map((line) => {
|
|
227
|
+
const trimmed = line.trim();
|
|
228
|
+
if (!trimmed)
|
|
229
|
+
return line;
|
|
230
|
+
if (trimmed.startsWith('#')) {
|
|
231
|
+
return trimmed.replace(/URI="([^"]+)"/g, (_, uri) => {
|
|
232
|
+
if (isProxied(uri))
|
|
233
|
+
return `URI="${uri}"`;
|
|
234
|
+
const absolute = new URL(uri, baseUrl).href;
|
|
235
|
+
return `URI="${needsProxy || absolute.includes('.m3u8') ? proxiedMediaUrl(absolute) : absolute}"`;
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
if (isProxied(trimmed))
|
|
239
|
+
return line;
|
|
240
|
+
const absolute = new URL(trimmed, baseUrl).href;
|
|
241
|
+
return proxiedMediaUrl(absolute);
|
|
242
|
+
})
|
|
243
|
+
.join('\n');
|
|
244
|
+
proxyCache.set(cacheKey, rewritten);
|
|
245
|
+
res
|
|
246
|
+
.set('Content-Type', 'application/vnd.apple.mpegurl')
|
|
247
|
+
.set('Access-Control-Allow-Origin', '*')
|
|
248
|
+
.send(rewritten);
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
if (ProxyController.isGotScrapingHost(urlStr)) {
|
|
252
|
+
if (req.headers.range)
|
|
253
|
+
headers['Range'] = req.headers.range;
|
|
254
|
+
const resp = await (0, got_scraping_1.gotScraping)({
|
|
255
|
+
url: urlStr,
|
|
256
|
+
method: 'GET',
|
|
257
|
+
headers,
|
|
258
|
+
responseType: 'buffer',
|
|
259
|
+
timeout: { request: 30000 },
|
|
260
|
+
followRedirect: true,
|
|
261
|
+
throwHttpErrors: false,
|
|
262
|
+
});
|
|
263
|
+
if (resp.statusCode !== 200 && resp.statusCode !== 206) {
|
|
264
|
+
return res.status(resp.statusCode ?? 502).send('Upstream error');
|
|
265
|
+
}
|
|
266
|
+
const ct = resp.headers['content-type'];
|
|
267
|
+
const isVtt = urlStr.endsWith('.vtt') || urlStr.includes('.vtt?');
|
|
268
|
+
res.set('Content-Type', isVtt ? 'text/vtt; charset=utf-8' : ct || 'application/octet-stream');
|
|
269
|
+
const cl = resp.headers['content-length'];
|
|
270
|
+
if (cl)
|
|
271
|
+
res.set('Content-Length', cl);
|
|
272
|
+
const cr = resp.headers['content-range'];
|
|
273
|
+
if (cr)
|
|
274
|
+
res.set('Content-Range', cr);
|
|
275
|
+
const ar = resp.headers['accept-ranges'];
|
|
276
|
+
if (ar)
|
|
277
|
+
res.set('Accept-Ranges', ar);
|
|
278
|
+
res.set('Access-Control-Allow-Origin', '*');
|
|
279
|
+
res.send(resp.body);
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
// Plain media streams (yt-mp4, etc.)
|
|
283
|
+
// Proxy using axios streaming (got-scraping does not support stream/buffer for these hosts)
|
|
284
|
+
const axiosResp = await (0, exports.axiosInstance)({
|
|
285
|
+
url: urlStr,
|
|
286
|
+
method: 'get',
|
|
287
|
+
headers,
|
|
288
|
+
responseType: 'stream',
|
|
289
|
+
timeout: 30000,
|
|
290
|
+
signal: abortController.signal,
|
|
291
|
+
});
|
|
292
|
+
const status = axiosResp.status;
|
|
293
|
+
if (status !== 200 && status !== 206) {
|
|
294
|
+
return res.status(status ?? 502).send('Upstream error');
|
|
295
|
+
}
|
|
296
|
+
res.status(status);
|
|
297
|
+
const isVtt = urlStr.endsWith('.vtt') || urlStr.includes('.vtt?');
|
|
298
|
+
const ct = firstString(axiosResp.headers['content-type']);
|
|
299
|
+
res.set('Content-Type', isVtt ? 'text/vtt; charset=utf-8' : ct || 'application/octet-stream');
|
|
300
|
+
const cl = firstString(axiosResp.headers['content-length']);
|
|
301
|
+
if (cl)
|
|
302
|
+
res.set('Content-Length', cl);
|
|
303
|
+
const cr = axiosResp.headers['content-range'];
|
|
304
|
+
if (cr)
|
|
305
|
+
res.set('Content-Range', cr);
|
|
306
|
+
const ar = axiosResp.headers['accept-ranges'];
|
|
307
|
+
if (ar)
|
|
308
|
+
res.set('Accept-Ranges', ar);
|
|
309
|
+
res.set('Access-Control-Allow-Origin', '*');
|
|
310
|
+
axiosResp.data.pipe(res);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
catch (e) {
|
|
315
|
+
if (axios_1.default.isCancel(e))
|
|
316
|
+
return;
|
|
317
|
+
if (!res.headersSent) {
|
|
318
|
+
logger_1.default.error({ url: urlStr, error: e?.message }, '[proxy] upstream request failed');
|
|
319
|
+
res.status(500).send('Proxy error');
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
handleEmbedProxy = async (req, res) => {
|
|
324
|
+
const kwikUrl = this.validateKwikUrl(req.query.url);
|
|
325
|
+
if (!kwikUrl)
|
|
326
|
+
return res.status(400).send('Invalid or unsupported gateway URL');
|
|
327
|
+
const cookieStr = req.query.cookie || '';
|
|
328
|
+
const abortController = new AbortController();
|
|
329
|
+
this.abortWhenClientLeaves(res, abortController);
|
|
330
|
+
try {
|
|
331
|
+
const headers = {
|
|
332
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0',
|
|
333
|
+
Referer: ProxyController.ANIMEPAHE_URL,
|
|
334
|
+
Origin: 'https://animepahe.pw',
|
|
335
|
+
};
|
|
336
|
+
if (cookieStr) {
|
|
337
|
+
const cookieHeader = (0, cookie_utils_1.buildCfClearanceCookie)(cookieStr);
|
|
338
|
+
if (cookieHeader)
|
|
339
|
+
headers['Cookie'] = cookieHeader;
|
|
340
|
+
}
|
|
341
|
+
const response = await (0, got_scraping_1.gotScraping)({
|
|
342
|
+
url: kwikUrl.href,
|
|
343
|
+
method: 'GET',
|
|
344
|
+
headers,
|
|
345
|
+
responseType: 'text',
|
|
346
|
+
timeout: { request: 30000 },
|
|
347
|
+
followRedirect: true,
|
|
348
|
+
throwHttpErrors: false,
|
|
349
|
+
});
|
|
350
|
+
const originalHtml = response.body;
|
|
351
|
+
const patched = this.applyKwikPatches(originalHtml, kwikUrl, cookieStr);
|
|
352
|
+
if (!patched)
|
|
353
|
+
return res.status(502).send('Failed to patch video gateway');
|
|
354
|
+
return res
|
|
355
|
+
.status(200)
|
|
356
|
+
.set('Content-Type', 'text/html; charset=utf-8')
|
|
357
|
+
.set('Cache-Control', 'private, max-age=120')
|
|
358
|
+
.send(patched);
|
|
359
|
+
}
|
|
360
|
+
catch (e) {
|
|
361
|
+
if (axios_1.default.isCancel(e))
|
|
362
|
+
return;
|
|
363
|
+
if (abortController.signal.aborted)
|
|
364
|
+
return;
|
|
365
|
+
if (!res.headersSent)
|
|
366
|
+
res.status(502).send('Gateway proxy error');
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
applyKwikPatches(html, kwikUrl, cookieStr = '') {
|
|
370
|
+
const safeReferer = JSON.stringify(kwikUrl.href).replace(/</g, '\\u003c');
|
|
371
|
+
const safeCookie = JSON.stringify(cookieStr).replace(/</g, '\\u003c');
|
|
372
|
+
const patched = html.replace(/\b(src|href|url)\s*[=:]\s*(["']?)(\/\/[^"'>)]+|\/(?!\/)[^"'>)]*)\2/gi, (match, attr, quote, path) => {
|
|
373
|
+
const full = path.startsWith('//') ? `https:${path}` : `${kwikUrl.origin}${path}`;
|
|
374
|
+
const prefix = match.startsWith('url') ? `url(` : `${attr}=`;
|
|
375
|
+
const suffix = match.startsWith('url') ? `)` : '';
|
|
376
|
+
return `${prefix}${quote}${full}${quote}${suffix}`;
|
|
377
|
+
});
|
|
378
|
+
const iconProxy = `/api/proxy?url=${encodeURIComponent(`${kwikUrl.origin}/app/js/vendor/plyr.svg`)}&referer=${encodeURIComponent(kwikUrl.href)}`;
|
|
379
|
+
const plyrPatch = `<script>if(window.Plyr) Plyr.defaults.iconUrl=${JSON.stringify(iconProxy).replace(/</g, '\\u003c')};</script>`;
|
|
380
|
+
const hlsPatch = `<script>
|
|
381
|
+
(function() {
|
|
382
|
+
var hook = function() {
|
|
383
|
+
if (!window.Hls) return;
|
|
384
|
+
var original = Hls.prototype.loadSource;
|
|
385
|
+
Hls.prototype.loadSource = function(src) {
|
|
386
|
+
if (typeof src === 'string' && src.includes('.m3u8')) {
|
|
387
|
+
src = window.location.origin + '/api/proxy?url=' + encodeURIComponent(src) + '&referer=' + encodeURIComponent(${safeReferer}) + (${safeCookie} ? '&cookie=' + encodeURIComponent(${safeCookie}) : '');
|
|
388
|
+
}
|
|
389
|
+
return original.call(this, src);
|
|
390
|
+
};
|
|
391
|
+
};
|
|
392
|
+
if (window.Hls) hook();
|
|
393
|
+
else {
|
|
394
|
+
var observer = new MutationObserver(function() {
|
|
395
|
+
if (window.Hls) { hook(); observer.disconnect(); }
|
|
396
|
+
});
|
|
397
|
+
observer.observe(document.documentElement, { childList: true, subtree: true });
|
|
398
|
+
}
|
|
399
|
+
})();
|
|
400
|
+
</script>`;
|
|
401
|
+
const endBridgePatch = `<script>
|
|
402
|
+
(function() {
|
|
403
|
+
var notified = false;
|
|
404
|
+
var notifyEnded = function() {
|
|
405
|
+
if (notified) return;
|
|
406
|
+
notified = true;
|
|
407
|
+
window.parent.postMessage({ type: 'ANI_WEB_MEDIA_ENDED' }, window.location.origin);
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
var attachToVideos = function(root) {
|
|
411
|
+
var scope = root || document;
|
|
412
|
+
var videos = scope.querySelectorAll ? scope.querySelectorAll('video') : [];
|
|
413
|
+
Array.prototype.forEach.call(videos, function(video) {
|
|
414
|
+
if (video.dataset && video.dataset.aniWebEndedBridge === 'true') return;
|
|
415
|
+
if (video.dataset) video.dataset.aniWebEndedBridge = 'true';
|
|
416
|
+
video.addEventListener('ended', notifyEnded, { once: true });
|
|
417
|
+
});
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
attachToVideos(document);
|
|
421
|
+
|
|
422
|
+
var observer = new MutationObserver(function() {
|
|
423
|
+
attachToVideos(document);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
observer.observe(document.documentElement, { childList: true, subtree: true });
|
|
427
|
+
})();
|
|
428
|
+
</script>`;
|
|
429
|
+
const beforePlyr = patched.replace(/(<script[^>]+\/plyr\.min\.js[^>]*><\/script>)/i, `$1${plyrPatch}`);
|
|
430
|
+
const final = beforePlyr
|
|
431
|
+
.replace(/(<script[^>]+hls(?:\.min)?\.js[^>]*><\/script>)/i, `$1${hlsPatch}`)
|
|
432
|
+
.replace(/<\/body>/i, `${endBridgePatch}</body>`);
|
|
433
|
+
return final === html ? null : final;
|
|
434
|
+
}
|
|
435
|
+
handleSubtitleProxy = async (req, res) => {
|
|
436
|
+
const { url, referer } = req.query;
|
|
437
|
+
if (!url)
|
|
438
|
+
return res.status(400).send('URL required');
|
|
439
|
+
const safeCheck = (0, security_utils_1.isSafeExternalUrl)(url);
|
|
440
|
+
if (!safeCheck.safe) {
|
|
441
|
+
return res.status(400).send(safeCheck.error || 'Invalid URL');
|
|
442
|
+
}
|
|
443
|
+
const abortController = new AbortController();
|
|
444
|
+
this.abortWhenClientLeaves(res, abortController);
|
|
445
|
+
try {
|
|
446
|
+
const headers = {
|
|
447
|
+
'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',
|
|
448
|
+
};
|
|
449
|
+
if (referer)
|
|
450
|
+
headers['Referer'] = referer;
|
|
451
|
+
const subUrl = url;
|
|
452
|
+
if (referer?.startsWith(ProxyController.KAA_ORIGIN) ||
|
|
453
|
+
Array.from(ProxyController.KAA_HOSTS).some((host) => subUrl.includes(host))) {
|
|
454
|
+
headers['Origin'] = ProxyController.KAA_ORIGIN;
|
|
455
|
+
}
|
|
456
|
+
const response = await exports.axiosInstance.get(url, {
|
|
457
|
+
headers,
|
|
458
|
+
responseType: 'text',
|
|
459
|
+
signal: abortController.signal,
|
|
460
|
+
});
|
|
461
|
+
const body = String(response.data ?? '').replace(/^\uFEFF/, '');
|
|
462
|
+
if (/#EXTM3U/i.test(body.slice(0, 1000))) {
|
|
463
|
+
const segUrls = body
|
|
464
|
+
.split('\n')
|
|
465
|
+
.map((l) => l.trim())
|
|
466
|
+
.filter((l) => l && !l.startsWith('#'))
|
|
467
|
+
.slice(0, 200)
|
|
468
|
+
.map((u) => {
|
|
469
|
+
try {
|
|
470
|
+
return new URL(u, subUrl).href;
|
|
471
|
+
}
|
|
472
|
+
catch {
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
})
|
|
476
|
+
.filter((u) => Boolean(u));
|
|
477
|
+
const parts = [];
|
|
478
|
+
for (const seg of segUrls) {
|
|
479
|
+
try {
|
|
480
|
+
const segRes = await exports.axiosInstance.get(seg, {
|
|
481
|
+
headers,
|
|
482
|
+
responseType: 'text',
|
|
483
|
+
signal: abortController.signal,
|
|
484
|
+
});
|
|
485
|
+
let segBody = String(segRes.data ?? '').replace(/^\uFEFF/, '');
|
|
486
|
+
if (/#EXTM3U/i.test(segBody.slice(0, 200)))
|
|
487
|
+
continue;
|
|
488
|
+
segBody = segBody
|
|
489
|
+
.replace(/\r\n/g, '\n')
|
|
490
|
+
.replace(/(\d{2}:\d{2}:\d{2}),(\d{3})/g, '$1.$2');
|
|
491
|
+
if (/^\s*WEBVTT/i.test(segBody)) {
|
|
492
|
+
segBody = segBody.replace(/^\s*WEBVTT[^\n]*\n(\n|.*\n)?/, '');
|
|
493
|
+
}
|
|
494
|
+
segBody = segBody.trim();
|
|
495
|
+
if (segBody)
|
|
496
|
+
parts.push(segBody);
|
|
497
|
+
if (parts.join('\n').length > 2_000_000)
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
catch (e) {
|
|
501
|
+
if (axios_1.default.isCancel(e))
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
if (parts.length === 0) {
|
|
506
|
+
return res.status(502).send('Subtitle playlist empty');
|
|
507
|
+
}
|
|
508
|
+
res.set('Content-Type', 'text/vtt; charset=utf-8');
|
|
509
|
+
return res.send(`WEBVTT\n\n${parts.join('\n\n')}\n`);
|
|
510
|
+
}
|
|
511
|
+
res.set('Content-Type', 'text/vtt; charset=utf-8');
|
|
512
|
+
if (/^\s*WEBVTT/i.test(body))
|
|
513
|
+
return res.send(body);
|
|
514
|
+
return res.send(`WEBVTT\n\n${body.replace(/\r\n/g, '\n').replace(/(\d{2}:\d{2}:\d{2}),(\d{3})/g, '$1.$2')}`);
|
|
515
|
+
}
|
|
516
|
+
catch (e) {
|
|
517
|
+
if (axios_1.default.isCancel(e))
|
|
518
|
+
return;
|
|
519
|
+
res.status(500).send('Proxy error');
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
handleImageProxy = async (req, res) => {
|
|
523
|
+
const { url, cookie, ua } = req.query;
|
|
524
|
+
if (!url)
|
|
525
|
+
return res.status(400).send('URL required');
|
|
526
|
+
const safeCheck = (0, security_utils_1.isSafeExternalUrl)(url);
|
|
527
|
+
if (!safeCheck.safe) {
|
|
528
|
+
return res.status(400).send(safeCheck.error || 'Invalid URL');
|
|
529
|
+
}
|
|
530
|
+
const targetUrl = url;
|
|
531
|
+
const abortController = new AbortController();
|
|
532
|
+
this.abortWhenClientLeaves(res, abortController);
|
|
533
|
+
let refererValue = 'https://anilist.co/';
|
|
534
|
+
const headers = {
|
|
535
|
+
'User-Agent': ua ||
|
|
536
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
537
|
+
};
|
|
538
|
+
try {
|
|
539
|
+
if (targetUrl.includes('animepahe')) {
|
|
540
|
+
refererValue = 'https://animepahe.pw/';
|
|
541
|
+
const rawCookie = cookie || '';
|
|
542
|
+
let sanitized = rawCookie.trim();
|
|
543
|
+
sanitized = sanitized.replace(/^cf_clearance/i, '');
|
|
544
|
+
sanitized = sanitized.replace(/^[:=]\s*/, '');
|
|
545
|
+
sanitized = sanitized.replace(/["']/g, '').trim();
|
|
546
|
+
headers['Cookie'] = `cf_clearance=${sanitized}`;
|
|
547
|
+
}
|
|
548
|
+
else if (targetUrl.includes('anilist.co')) {
|
|
549
|
+
refererValue = 'https://anilist.co/';
|
|
550
|
+
}
|
|
551
|
+
else if (targetUrl.includes('gogocdn.net')) {
|
|
552
|
+
refererValue = 'https://gogoanime.lu/';
|
|
553
|
+
}
|
|
554
|
+
else if (targetUrl.includes('animeya.cc')) {
|
|
555
|
+
refererValue = 'https://animeya.cc/';
|
|
556
|
+
}
|
|
557
|
+
else if (targetUrl.includes('myspacecat.pictures')) {
|
|
558
|
+
refererValue = 'https://oppai.stream/';
|
|
559
|
+
}
|
|
560
|
+
else if (targetUrl.includes('weeabo0.xyz')) {
|
|
561
|
+
refererValue = 'https://japaneseasmr.com/';
|
|
562
|
+
const rawCookie = cookie || '';
|
|
563
|
+
let sanitized = rawCookie.trim();
|
|
564
|
+
sanitized = sanitized.replace(/^cf_clearance/i, '');
|
|
565
|
+
sanitized = sanitized.replace(/^[:=]\s*/, '');
|
|
566
|
+
sanitized = sanitized.replace(/["']/g, '').trim();
|
|
567
|
+
if (sanitized)
|
|
568
|
+
headers['Cookie'] = `cf_clearance=${sanitized}`;
|
|
569
|
+
const uaParam = ua || '';
|
|
570
|
+
if (uaParam)
|
|
571
|
+
headers['User-Agent'] = uaParam;
|
|
572
|
+
}
|
|
573
|
+
headers['Referer'] = refererValue;
|
|
574
|
+
// Use got-scraping for a browser-like TLS fingerprint (needed for hosts
|
|
575
|
+
// like i.animepahe.pw that reject axios's fingerprint with 403). Retry a
|
|
576
|
+
// couple times since Cloudflare can intermittently challenge.
|
|
577
|
+
let lastStatus = 0;
|
|
578
|
+
let body = null;
|
|
579
|
+
let contentType = 'image/webp';
|
|
580
|
+
for (let attempt = 0; attempt < 3 && !body; attempt++) {
|
|
581
|
+
const resp = await (0, got_scraping_1.gotScraping)({
|
|
582
|
+
url: targetUrl,
|
|
583
|
+
method: 'GET',
|
|
584
|
+
headers,
|
|
585
|
+
responseType: 'buffer',
|
|
586
|
+
followRedirect: true,
|
|
587
|
+
throwHttpErrors: false,
|
|
588
|
+
timeout: { request: 30000 },
|
|
589
|
+
});
|
|
590
|
+
lastStatus = resp.statusCode;
|
|
591
|
+
if (resp.statusCode === 200 && resp.rawBody?.length) {
|
|
592
|
+
body = Buffer.from(resp.rawBody);
|
|
593
|
+
contentType = String(resp.headers['content-type'] || 'image/webp');
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
if (attempt < 2)
|
|
597
|
+
await new Promise((r) => setTimeout(r, 400 * (attempt + 1)));
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
if (body) {
|
|
601
|
+
res.set('Cache-Control', 'public, max-age=604800, immutable');
|
|
602
|
+
res.set('Content-Type', contentType);
|
|
603
|
+
res.set('Access-Control-Allow-Origin', '*');
|
|
604
|
+
return res.send(body);
|
|
605
|
+
}
|
|
606
|
+
this.sendPlaceholder(res);
|
|
607
|
+
}
|
|
608
|
+
catch (e) {
|
|
609
|
+
if (abortController.signal.aborted)
|
|
610
|
+
return;
|
|
611
|
+
const err = e;
|
|
612
|
+
if (!res.headersSent) {
|
|
613
|
+
this.sendPlaceholder(res);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
handleResolve = async (req, res) => {
|
|
618
|
+
const targetUrl = req.query.url;
|
|
619
|
+
if (!targetUrl || typeof targetUrl !== 'string') {
|
|
620
|
+
return res.status(400).json({ error: 'URL required' });
|
|
621
|
+
}
|
|
622
|
+
const safeCheck = (0, security_utils_1.isSafeExternalUrl)(targetUrl);
|
|
623
|
+
if (!safeCheck.safe) {
|
|
624
|
+
return res.status(400).json({ error: safeCheck.error || 'Invalid URL' });
|
|
625
|
+
}
|
|
626
|
+
try {
|
|
627
|
+
const response = await fetch(targetUrl, {
|
|
628
|
+
method: 'GET',
|
|
629
|
+
headers: {
|
|
630
|
+
'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',
|
|
631
|
+
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
632
|
+
},
|
|
633
|
+
redirect: 'follow',
|
|
634
|
+
});
|
|
635
|
+
res.json({
|
|
636
|
+
finalUrl: response.url,
|
|
637
|
+
status: response.status,
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
catch (e) {
|
|
641
|
+
const err = e;
|
|
642
|
+
res.status(500).json({ error: err.message || 'Resolve failed' });
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
sendPlaceholder(res) {
|
|
646
|
+
if (!res.headersSent)
|
|
647
|
+
res.set('Cache-Control', 'no-store');
|
|
648
|
+
const possiblePaths = [
|
|
649
|
+
path_1.default.join(config_1.CONFIG.PACKAGE_ROOT, 'client/public/placeholder.svg'),
|
|
650
|
+
path_1.default.join(config_1.CONFIG.PACKAGE_ROOT, 'client/dist/placeholder.svg'),
|
|
651
|
+
path_1.default.join(config_1.CONFIG.SERVER_ROOT, '..', 'client/public/placeholder.svg'),
|
|
652
|
+
];
|
|
653
|
+
for (const p of possiblePaths) {
|
|
654
|
+
if (fs_1.default.existsSync(p)) {
|
|
655
|
+
return res.sendFile(p, (err) => {
|
|
656
|
+
if (err && !res.headersSent) {
|
|
657
|
+
res.status(404).send('Not Found');
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
res.status(404).send('Not Found');
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
exports.ProxyController = ProxyController;
|