@serifpersia/dango 3.3.4
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 +258 -0
- package/client/dist/assets/AnimeCard-B5A20jAI.js +1 -0
- package/client/dist/assets/AnimeInfo-0huPAq0O.js +1 -0
- package/client/dist/assets/AnimeInfo-BFWtHlPC.css +1 -0
- package/client/dist/assets/AnimeInfoPage-Dqr5QH6R.js +2 -0
- package/client/dist/assets/AnimeMetaDetails-BPvv5xIl.js +1 -0
- package/client/dist/assets/AnimeMetaDetails-BwNQnRCd.css +1 -0
- package/client/dist/assets/Asmr-B6ZiqkmU.js +2 -0
- package/client/dist/assets/Asmr-Cq2ua6Zk.css +1 -0
- package/client/dist/assets/Asmr.module-ChUXDZKx.js +1 -0
- package/client/dist/assets/AsmrResetProgressModal-Cqsoo3XC.js +1 -0
- package/client/dist/assets/AvSyncCalibrator-C1_iBSyL.css +1 -0
- package/client/dist/assets/AvSyncCalibrator-C8ZoIwMY.js +10 -0
- package/client/dist/assets/ErrorMessage-CXoRbrLN.css +1 -0
- package/client/dist/assets/ErrorMessage-z4f4WGgP.js +1 -0
- package/client/dist/assets/Home-DtRtTuI9.css +1 -0
- package/client/dist/assets/Home-thJhPfs1.js +1 -0
- package/client/dist/assets/Insights-DKtoVe6L.css +1 -0
- package/client/dist/assets/Insights-ZlgPrzGD.js +1 -0
- package/client/dist/assets/LibraryListPage-CCiee-Ot.js +1 -0
- package/client/dist/assets/ListeningList-ctAz_6Zi.js +1 -0
- package/client/dist/assets/Manga-BI9WMTx5.js +1 -0
- package/client/dist/assets/Manga-Dp_-l1Pw.css +1 -0
- package/client/dist/assets/Manga.module-BcfOdsNP.js +1 -0
- package/client/dist/assets/MangaInfoPage-DcrEr7Sk.js +1 -0
- package/client/dist/assets/MangaReadPage-SYg63m48.js +1 -0
- package/client/dist/assets/MangaResetProgressModal-DX-1PS4h.js +1 -0
- package/client/dist/assets/Map-CXFqXmsZ.css +1 -0
- package/client/dist/assets/Map-Dj51g5zL.js +1 -0
- package/client/dist/assets/Mature-DJmUUtlF.css +1 -0
- package/client/dist/assets/Mature-Dqh_9SA3.js +1 -0
- package/client/dist/assets/MediaCard-Be1ST9oj.js +1 -0
- package/client/dist/assets/MediaCard-DB6Yjgky.css +1 -0
- package/client/dist/assets/Music-BGH_0voi.css +1 -0
- package/client/dist/assets/Music-zsPwT8_5.js +1 -0
- package/client/dist/assets/OptionListMenu-Drc_vybW.js +1 -0
- package/client/dist/assets/Player-B036Ic3m.css +1 -0
- package/client/dist/assets/Player-BEtAYps2.js +56 -0
- package/client/dist/assets/PlayerRedirect-D70t8VUW.js +1 -0
- package/client/dist/assets/PlayerSettings-Bj6SsmQ2.css +1 -0
- package/client/dist/assets/PlayerSettings-Cwoji-P7.js +1 -0
- package/client/dist/assets/QueueOptionsButton-B6BMq66e.js +1 -0
- package/client/dist/assets/QueueOptionsButton-CM15uQn8.css +1 -0
- package/client/dist/assets/QueueRail-BUYEjdaq.css +1 -0
- package/client/dist/assets/QueueRail-VxpUDBSm.js +1 -0
- package/client/dist/assets/Radio-B2Q_Egj7.js +1 -0
- package/client/dist/assets/Radio-CCHNFN7R.css +1 -0
- package/client/dist/assets/Radio.module-DGSZLpKM.js +1 -0
- package/client/dist/assets/ReadingList-O0QPwXu7.js +1 -0
- package/client/dist/assets/ResetProgressModal-CR_ovLAu.js +1 -0
- package/client/dist/assets/Search-C8e2nlDD.js +1 -0
- package/client/dist/assets/Search-Cs45oAwI.css +1 -0
- package/client/dist/assets/Search.module-CMEai-B-.js +1 -0
- package/client/dist/assets/Settings-AuFxQcJF.css +1 -0
- package/client/dist/assets/Settings-BgPWBl2V.js +21 -0
- package/client/dist/assets/SkeletonGrid-CtHKKMLj.js +1 -0
- package/client/dist/assets/SkeletonGrid-DhPYefXr.css +1 -0
- package/client/dist/assets/SynopsisText-BcpwT5iw.js +1 -0
- package/client/dist/assets/SynopsisText-CYv2W3yR.css +1 -0
- package/client/dist/assets/Trackers-Bfoiyuzy.js +2 -0
- package/client/dist/assets/Trackers-D5XBOCDp.css +1 -0
- package/client/dist/assets/Tv-Ca0fCdko.js +3 -0
- package/client/dist/assets/Tv-CuEl3Iol.css +1 -0
- package/client/dist/assets/TvInfo-Bu36xJyO.css +1 -0
- package/client/dist/assets/TvInfo-D_scgw_h.js +1 -0
- package/client/dist/assets/TvPopup-YTI7dEbJ.js +1 -0
- package/client/dist/assets/TvSearch-CxU33UOf.js +1 -0
- package/client/dist/assets/TvWatchlist-B4X1fWEH.js +1 -0
- package/client/dist/assets/VirtualKeyboard-6ZNTym4R.js +1 -0
- package/client/dist/assets/VirtualKeyboard-KIPc5ZGH.css +1 -0
- package/client/dist/assets/Watchlist-BiDEU0iV.css +1 -0
- package/client/dist/assets/Watchlist-P6YMZUdF.js +1 -0
- package/client/dist/assets/Watchlist.module-XGtVQr1d.js +1 -0
- package/client/dist/assets/hls-BOLXTnjL.js +42 -0
- package/client/dist/assets/hls-C4cG9wQw.js +1 -0
- package/client/dist/assets/index-BOKlZEHq.js +30354 -0
- package/client/dist/assets/index-CUm1meqr.css +1 -0
- package/client/dist/assets/index-IZgh1beL.js +178 -0
- package/client/dist/assets/useAnimeInfoData-DeuwDSnI.js +1 -0
- package/client/dist/assets/useAsmr-CbMVRjmV.js +1 -0
- package/client/dist/assets/useAsmrPopup-D91Q3CBg.js +1 -0
- package/client/dist/assets/useIsMobile-Dm_b6Ob3.js +1 -0
- package/client/dist/assets/useMangaLibrary-BPvFb1lU.js +1 -0
- package/client/dist/assets/useMangaPopup-BOL2mQJs.js +1 -0
- package/client/dist/assets/useMatureConsent-CT7-KfzG.js +1 -0
- package/client/dist/assets/useProviders-BsRGONMA.js +1 -0
- package/client/dist/assets/useTv-D37MfTHA.js +1 -0
- package/client/dist/assets/useTvLibrary-Cwxgy_Bg.js +1 -0
- package/client/dist/assets/vendor-core-8R2fFbyN.js +1 -0
- package/client/dist/assets/vendor-icons-CDMuNDeL.js +1 -0
- package/client/dist/assets/vendor-query-BXRECSI6.js +1 -0
- package/client/dist/assets/vendor-ui-CUDCvRS_.js +1 -0
- package/client/dist/av-sync-test.mp4 +0 -0
- package/client/dist/favicon.ico +0 -0
- package/client/dist/index.html +46 -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 +534 -0
- package/package.json +85 -0
- package/server/dist/app-auth.js +177 -0
- package/server/dist/config.js +175 -0
- package/server/dist/controllers/asmr-library.controller.js +288 -0
- package/server/dist/controllers/auth.controller.js +225 -0
- package/server/dist/controllers/data.controller.js +766 -0
- package/server/dist/controllers/insights.controller.js +198 -0
- package/server/dist/controllers/manga-library.controller.js +296 -0
- package/server/dist/controllers/music.controller.js +698 -0
- package/server/dist/controllers/proxy.controller.js +820 -0
- package/server/dist/controllers/settings.controller.js +609 -0
- package/server/dist/controllers/tv-library.controller.js +310 -0
- package/server/dist/controllers/watchlist.controller.js +1015 -0
- package/server/dist/db.js +145 -0
- package/server/dist/discord-gateway.js +391 -0
- package/server/dist/discord-rpc.js +427 -0
- package/server/dist/github-sync.js +512 -0
- package/server/dist/google.js +748 -0
- package/server/dist/lib/anilist.js +1549 -0
- package/server/dist/lib/anischedule.js +194 -0
- package/server/dist/lib/discord-roles-sync.service.js +130 -0
- package/server/dist/lib/ipc.js +24 -0
- package/server/dist/lib/kitsu.js +544 -0
- package/server/dist/lib/mal.js +934 -0
- package/server/dist/lib/migration.js +329 -0
- package/server/dist/lib/offline-db.js +401 -0
- package/server/dist/lib/temp-ids.js +3 -0
- package/server/dist/lib/tmdb.js +72 -0
- package/server/dist/lib/tracker/anilist-tracker.js +245 -0
- package/server/dist/lib/tracker/sync.service.js +496 -0
- package/server/dist/lib/ytmusic.js +125 -0
- package/server/dist/logger.js +16 -0
- package/server/dist/providers/manga/manga.types.js +7 -0
- package/server/dist/providers/provider.interface.js +1 -0
- package/server/dist/providers/remote-loader.js +574 -0
- package/server/dist/providers/remote-types.js +117 -0
- package/server/dist/providers/title-matching.js +225 -0
- package/server/dist/providers/tv.types.js +1 -0
- package/server/dist/rclone.js +120 -0
- package/server/dist/repositories/asmr.repository.js +123 -0
- package/server/dist/repositories/insights.repository.js +85 -0
- package/server/dist/repositories/library.repository.js +108 -0
- package/server/dist/repositories/mal-cache.repository.js +55 -0
- package/server/dist/repositories/manga.repository.js +138 -0
- package/server/dist/repositories/notifications.repository.js +41 -0
- package/server/dist/repositories/queue.repository.js +61 -0
- package/server/dist/repositories/settings.repository.js +12 -0
- package/server/dist/repositories/shows-meta.repository.js +42 -0
- package/server/dist/repositories/temp-show-ids.repository.js +51 -0
- package/server/dist/repositories/tv.repository.js +151 -0
- package/server/dist/repositories/watched-episodes.repository.js +52 -0
- package/server/dist/repositories/watchlist.repository.js +71 -0
- package/server/dist/request-logger.js +16 -0
- package/server/dist/routes/asmr-library.routes.js +23 -0
- package/server/dist/routes/asmr.routes.js +81 -0
- package/server/dist/routes/auth.routes.js +25 -0
- package/server/dist/routes/data.routes.js +47 -0
- package/server/dist/routes/discord-gateway.routes.js +51 -0
- package/server/dist/routes/insights.routes.js +34 -0
- package/server/dist/routes/lan-auth.routes.js +46 -0
- package/server/dist/routes/manga-library.routes.js +23 -0
- package/server/dist/routes/manga.routes.js +141 -0
- package/server/dist/routes/music.routes.js +20 -0
- package/server/dist/routes/providers.routes.js +17 -0
- package/server/dist/routes/proxy.routes.js +12 -0
- package/server/dist/routes/radio.routes.js +138 -0
- package/server/dist/routes/settings.routes.js +24 -0
- package/server/dist/routes/tracker.routes.js +164 -0
- package/server/dist/routes/translate.routes.js +88 -0
- package/server/dist/routes/tv-library.routes.js +23 -0
- package/server/dist/routes/tv.routes.js +629 -0
- package/server/dist/routes/watchlist.routes.js +341 -0
- package/server/dist/scripts/backfill-episode-duration.js +42 -0
- package/server/dist/server.js +529 -0
- package/server/dist/shipped-defaults.js +7 -0
- package/server/dist/sync-config.js +23 -0
- package/server/dist/sync-payload.js +91 -0
- package/server/dist/sync.js +1128 -0
- package/server/dist/utils/cache.utils.js +25 -0
- package/server/dist/utils/cookie.utils.js +13 -0
- package/server/dist/utils/db-utils.js +9 -0
- package/server/dist/utils/env.utils.js +77 -0
- package/server/dist/utils/http.utils.js +70 -0
- package/server/dist/utils/machine-id.js +40 -0
- package/server/dist/utils/megaplay.utils.js +41 -0
- package/server/dist/utils/request-context.js +2 -0
- package/server/dist/utils/security.utils.js +94 -0
- package/server/package.json +44 -0
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
import { execFileSync } from 'node:child_process';
|
|
5
|
+
import { pathToFileURL } from 'url';
|
|
6
|
+
import * as cheerio from 'cheerio';
|
|
7
|
+
import { gotScraping } from 'got-scraping';
|
|
8
|
+
import logger from '../logger.js';
|
|
9
|
+
import { CONFIG } from '../config.js';
|
|
10
|
+
import { requestContext } from '../utils/request-context.js';
|
|
11
|
+
import { sanitizeCfClearance, buildCfClearanceCookie } from '../utils/cookie.utils.js';
|
|
12
|
+
import { decodeMaybeGzip, parseJsonBody } from '../utils/http.utils.js';
|
|
13
|
+
import { buildQueryVariants, pickBestMatch } from './title-matching.js';
|
|
14
|
+
import { anilistRequest, parseMalId, searchAnilistByTitle } from '../lib/anilist.js';
|
|
15
|
+
import { kitsuMetaByAnilistId } from '../lib/kitsu.js';
|
|
16
|
+
import { getTmdbKey, TMDB_BASE, TMDB_IMAGE } from '../lib/tmdb.js';
|
|
17
|
+
import { parseRegistry, } from './remote-types.js';
|
|
18
|
+
const FETCH_TIMEOUT_MS = 15000;
|
|
19
|
+
const GENERIC_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36';
|
|
20
|
+
function withTimeout(p, ms, label) {
|
|
21
|
+
let timer;
|
|
22
|
+
const timeout = new Promise((_, reject) => {
|
|
23
|
+
timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
|
|
24
|
+
});
|
|
25
|
+
return Promise.race([p, timeout]).finally(() => clearTimeout(timer));
|
|
26
|
+
}
|
|
27
|
+
function isHttp(url) {
|
|
28
|
+
return /^https?:\/\//i.test(url);
|
|
29
|
+
}
|
|
30
|
+
function stripFilePrefix(p) {
|
|
31
|
+
if (p.startsWith('file://')) {
|
|
32
|
+
const u = new URL(p);
|
|
33
|
+
return u.pathname;
|
|
34
|
+
}
|
|
35
|
+
return p;
|
|
36
|
+
}
|
|
37
|
+
function looksLikePath(p) {
|
|
38
|
+
if (isHttp(p) || p.startsWith('file://'))
|
|
39
|
+
return false;
|
|
40
|
+
return p.includes('/') || p.includes('\\') || p.endsWith('.json');
|
|
41
|
+
}
|
|
42
|
+
async function readManifestBytes(registryUrl) {
|
|
43
|
+
if (isHttp(registryUrl)) {
|
|
44
|
+
const res = await withTimeout(fetch(registryUrl, { headers: { 'User-Agent': GENERIC_UA, Accept: 'application/json' } }), FETCH_TIMEOUT_MS, 'registry fetch');
|
|
45
|
+
if (!res.ok)
|
|
46
|
+
throw new Error(`registry HTTP ${res.status}: ${registryUrl}`);
|
|
47
|
+
return decodeMaybeGzip(Buffer.from(await res.arrayBuffer()));
|
|
48
|
+
}
|
|
49
|
+
const filePath = path.resolve(stripFilePrefix(registryUrl));
|
|
50
|
+
return await fs.promises.readFile(filePath);
|
|
51
|
+
}
|
|
52
|
+
async function readModuleBytes(entryUrl) {
|
|
53
|
+
if (isHttp(entryUrl)) {
|
|
54
|
+
const res = await withTimeout(fetch(entryUrl, { headers: { 'User-Agent': GENERIC_UA, Accept: 'text/javascript,*/*' } }), FETCH_TIMEOUT_MS, 'provider fetch');
|
|
55
|
+
if (!res.ok)
|
|
56
|
+
throw new Error(`provider HTTP ${res.status}: ${entryUrl}`);
|
|
57
|
+
return decodeMaybeGzip(Buffer.from(await res.arrayBuffer()));
|
|
58
|
+
}
|
|
59
|
+
const filePath = path.resolve(stripFilePrefix(entryUrl));
|
|
60
|
+
return await fs.promises.readFile(filePath);
|
|
61
|
+
}
|
|
62
|
+
function resolveEntryUrl(entry, registryUrl) {
|
|
63
|
+
const e = entry.trim();
|
|
64
|
+
if (isHttp(e) || e.startsWith('file://'))
|
|
65
|
+
return e;
|
|
66
|
+
if (isHttp(registryUrl))
|
|
67
|
+
return new URL(e, registryUrl).href;
|
|
68
|
+
const baseDir = path.dirname(path.resolve(stripFilePrefix(registryUrl)));
|
|
69
|
+
return path.resolve(baseDir, e);
|
|
70
|
+
}
|
|
71
|
+
function sha256Hex(data) {
|
|
72
|
+
return crypto.createHash('sha256').update(data).digest('hex');
|
|
73
|
+
}
|
|
74
|
+
function base64UrlEncode(input) {
|
|
75
|
+
return Buffer.from(input)
|
|
76
|
+
.toString('base64')
|
|
77
|
+
.replace(/\+/g, '-')
|
|
78
|
+
.replace(/\//g, '_')
|
|
79
|
+
.replace(/=+$/, '');
|
|
80
|
+
}
|
|
81
|
+
function aes256CbcDecryptJson(b64url, keyUtf8, ivUtf8) {
|
|
82
|
+
let encrypted;
|
|
83
|
+
try {
|
|
84
|
+
encrypted = Buffer.from(b64url, 'base64url');
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
if (!encrypted.length || encrypted.length % 16 !== 0)
|
|
90
|
+
return null;
|
|
91
|
+
try {
|
|
92
|
+
const key = Buffer.alloc(32);
|
|
93
|
+
Buffer.from(keyUtf8).copy(key);
|
|
94
|
+
const decipher = crypto.createDecipheriv('aes-256-cbc', key, Buffer.from(ivUtf8));
|
|
95
|
+
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
|
96
|
+
return JSON.parse(decrypted.toString('utf8'));
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function curlRaw(url, headers) {
|
|
103
|
+
const args = [
|
|
104
|
+
'-sSL',
|
|
105
|
+
'-A',
|
|
106
|
+
GENERIC_UA,
|
|
107
|
+
...Object.entries(headers ?? {}).flatMap(([k, v]) => ['-H', `${k}: ${v}`]),
|
|
108
|
+
'--max-time',
|
|
109
|
+
'15',
|
|
110
|
+
'-w',
|
|
111
|
+
'\n__HTTP_STATUS__%{http_code}',
|
|
112
|
+
'--',
|
|
113
|
+
url,
|
|
114
|
+
];
|
|
115
|
+
let out;
|
|
116
|
+
try {
|
|
117
|
+
out = execFileSync('curl', args, { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 });
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
const m = out.match(/__HTTP_STATUS__(\d+)\s*$/);
|
|
123
|
+
const status = m ? parseInt(m[1], 10) : 0;
|
|
124
|
+
return { status, body: m && m.index !== undefined ? out.slice(0, m.index) : out };
|
|
125
|
+
}
|
|
126
|
+
function createCtx(cache) {
|
|
127
|
+
const fetchWithTimeout = async (url, init, ms = FETCH_TIMEOUT_MS) => {
|
|
128
|
+
return withTimeout(fetch(url, {
|
|
129
|
+
...init,
|
|
130
|
+
headers: { 'User-Agent': GENERIC_UA, ...init?.headers },
|
|
131
|
+
}), ms, `fetch ${url}`);
|
|
132
|
+
};
|
|
133
|
+
return {
|
|
134
|
+
cache: {
|
|
135
|
+
get: (key) => cache.get(key),
|
|
136
|
+
set: (key, value, ttlSeconds) => {
|
|
137
|
+
cache.set(key, value, ttlSeconds);
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
logger: {
|
|
141
|
+
info: (obj, msg) => {
|
|
142
|
+
logger.info(obj, msg);
|
|
143
|
+
},
|
|
144
|
+
warn: (obj, msg) => {
|
|
145
|
+
logger.warn(obj, msg);
|
|
146
|
+
},
|
|
147
|
+
error: (obj, msg) => {
|
|
148
|
+
logger.error(obj, msg);
|
|
149
|
+
},
|
|
150
|
+
debug: (obj, msg) => {
|
|
151
|
+
logger.debug(obj, msg);
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
fetchText: async (url, init) => {
|
|
155
|
+
try {
|
|
156
|
+
const res = await fetchWithTimeout(url, init);
|
|
157
|
+
if (!res.ok)
|
|
158
|
+
return null;
|
|
159
|
+
return await res.text();
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
fetchJson: async (url, init) => {
|
|
166
|
+
const res = await fetchWithTimeout(url, init);
|
|
167
|
+
if (!res.ok)
|
|
168
|
+
throw new Error(`HTTP ${res.status}: ${url}`);
|
|
169
|
+
return parseJsonBody(res);
|
|
170
|
+
},
|
|
171
|
+
proxyUrl: (rawUrl, referer) => {
|
|
172
|
+
return `/api/proxy?url=${encodeURIComponent(rawUrl)}&referer=${encodeURIComponent(referer)}`;
|
|
173
|
+
},
|
|
174
|
+
userAgent: GENERIC_UA,
|
|
175
|
+
titleMatch: {
|
|
176
|
+
buildQueryVariants,
|
|
177
|
+
pickBestMatch,
|
|
178
|
+
},
|
|
179
|
+
anilist: {
|
|
180
|
+
request: (query, vars) => anilistRequest(query, vars),
|
|
181
|
+
parseMalId,
|
|
182
|
+
searchByTitle: searchAnilistByTitle,
|
|
183
|
+
},
|
|
184
|
+
kitsu: {
|
|
185
|
+
metaByAnilistId: kitsuMetaByAnilistId,
|
|
186
|
+
},
|
|
187
|
+
tmdb: {
|
|
188
|
+
base: TMDB_BASE,
|
|
189
|
+
image: TMDB_IMAGE,
|
|
190
|
+
get: async (path) => {
|
|
191
|
+
try {
|
|
192
|
+
const key = await getTmdbKey();
|
|
193
|
+
const sep = path.includes('?') ? '&' : '?';
|
|
194
|
+
const res = await fetchWithTimeout(`${TMDB_BASE}${path}${sep}api_key=${key}`);
|
|
195
|
+
if (!res.ok)
|
|
196
|
+
return null;
|
|
197
|
+
return parseJsonBody(res);
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
request: {
|
|
205
|
+
get: (key) => requestContext.getStore()?.get(key),
|
|
206
|
+
},
|
|
207
|
+
cookies: {
|
|
208
|
+
sanitizeCfClearance,
|
|
209
|
+
buildCfClearanceCookie,
|
|
210
|
+
},
|
|
211
|
+
crypto: {
|
|
212
|
+
aes256CbcDecryptJson,
|
|
213
|
+
hmacSha256Base64Url: (key, message) => base64UrlEncode(crypto.createHmac('sha256', key).update(message).digest()),
|
|
214
|
+
},
|
|
215
|
+
scraping: {
|
|
216
|
+
fetch: async (urlOrOptions, maybeOptions) => {
|
|
217
|
+
const opts = (typeof urlOrOptions === 'string'
|
|
218
|
+
? { url: urlOrOptions, ...(maybeOptions ?? {}) }
|
|
219
|
+
: urlOrOptions);
|
|
220
|
+
const { url, ...rest } = opts;
|
|
221
|
+
const res = await gotScraping(url, {
|
|
222
|
+
method: 'GET',
|
|
223
|
+
responseType: 'text',
|
|
224
|
+
throwHttpErrors: false,
|
|
225
|
+
...rest,
|
|
226
|
+
});
|
|
227
|
+
return { statusCode: res.statusCode, body: String(res.body ?? ''), headers: res.headers };
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
curl: {
|
|
231
|
+
getJson: async (url, headers) => {
|
|
232
|
+
try {
|
|
233
|
+
const r = curlRaw(url, headers);
|
|
234
|
+
if (!r || r.status !== 200)
|
|
235
|
+
return null;
|
|
236
|
+
return JSON.parse(r.body);
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
getText: async (url, headers) => {
|
|
243
|
+
const r = curlRaw(url, headers);
|
|
244
|
+
if (!r || r.status !== 200)
|
|
245
|
+
return null;
|
|
246
|
+
return r.body;
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
cheerio: {
|
|
250
|
+
load: (html) => cheerio.load(html),
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
function isValidVideoProvider(p) {
|
|
255
|
+
if (!p || typeof p !== 'object')
|
|
256
|
+
return false;
|
|
257
|
+
const o = p;
|
|
258
|
+
return (typeof o.name === 'string' &&
|
|
259
|
+
typeof o.search === 'function' &&
|
|
260
|
+
typeof o.getEpisodes === 'function' &&
|
|
261
|
+
typeof o.getStreamUrls === 'function');
|
|
262
|
+
}
|
|
263
|
+
function isValidMangaProvider(p) {
|
|
264
|
+
if (!p || typeof p !== 'object')
|
|
265
|
+
return false;
|
|
266
|
+
const o = p;
|
|
267
|
+
return (typeof o.name === 'string' &&
|
|
268
|
+
typeof o.search === 'function' &&
|
|
269
|
+
typeof o.getDetail === 'function' &&
|
|
270
|
+
typeof o.getChapters === 'function' &&
|
|
271
|
+
typeof o.getPages === 'function');
|
|
272
|
+
}
|
|
273
|
+
function isValidTvProvider(p) {
|
|
274
|
+
if (!p || typeof p !== 'object')
|
|
275
|
+
return false;
|
|
276
|
+
const o = p;
|
|
277
|
+
return (typeof o.name === 'string' &&
|
|
278
|
+
(typeof o.getSources === 'function' || typeof o.getEmbedUrl === 'function'));
|
|
279
|
+
}
|
|
280
|
+
async function loadEntry(item, registryUrl, cacheDir, ctx, allowUnsigned) {
|
|
281
|
+
const entryUrl = resolveEntryUrl(item.entry, registryUrl);
|
|
282
|
+
await fs.promises.mkdir(cacheDir, { recursive: true });
|
|
283
|
+
const fileName = `${item.id}-${item.version}.mjs`;
|
|
284
|
+
const filePath = path.join(cacheDir, fileName);
|
|
285
|
+
const instanceKey = `${item.kind ?? 'anime'}:${item.id}-${item.version}-${(item.sha256 ?? 'nosha').slice(0, 12)}`;
|
|
286
|
+
let bytes = null;
|
|
287
|
+
try {
|
|
288
|
+
const cached = await fs.promises.readFile(filePath);
|
|
289
|
+
if (item.sha256 && sha256Hex(cached) === item.sha256) {
|
|
290
|
+
bytes = cached;
|
|
291
|
+
}
|
|
292
|
+
else if (!item.sha256 && allowUnsigned) {
|
|
293
|
+
bytes = cached;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
// ignore
|
|
298
|
+
}
|
|
299
|
+
if (bytes) {
|
|
300
|
+
const hit = instanceCache.get(instanceKey);
|
|
301
|
+
if (hit) {
|
|
302
|
+
logger.debug({ provider: item.id, version: item.version }, 'remote provider reused cached instance');
|
|
303
|
+
return { provider: hit.provider, facets: hit.facets };
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
try {
|
|
308
|
+
bytes = await readModuleBytes(entryUrl);
|
|
309
|
+
}
|
|
310
|
+
catch (err) {
|
|
311
|
+
return {
|
|
312
|
+
provider: null,
|
|
313
|
+
error: `download failed: ${err.message}`,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
if (item.sha256) {
|
|
317
|
+
const actual = sha256Hex(bytes);
|
|
318
|
+
if (actual !== item.sha256) {
|
|
319
|
+
return { provider: null, error: `sha256 mismatch for ${item.id}` };
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
else if (!allowUnsigned) {
|
|
323
|
+
return { provider: null, error: `missing sha256 for ${item.id}` };
|
|
324
|
+
}
|
|
325
|
+
await fs.promises.writeFile(filePath, bytes);
|
|
326
|
+
}
|
|
327
|
+
const cacheBuster = `${item.version}-${(item.sha256 ?? 'nosha').slice(0, 12)}`;
|
|
328
|
+
const result = await instantiateModule(filePath, cacheBuster, ctx, item);
|
|
329
|
+
if (result.provider && !result.error) {
|
|
330
|
+
instanceCache.set(instanceKey, { provider: result.provider, facets: result.facets });
|
|
331
|
+
}
|
|
332
|
+
return result;
|
|
333
|
+
}
|
|
334
|
+
const instanceCache = new Map();
|
|
335
|
+
function extractBrowseFacets(mod) {
|
|
336
|
+
let genres;
|
|
337
|
+
let orders;
|
|
338
|
+
for (const [key, value] of Object.entries(mod)) {
|
|
339
|
+
if (key === 'default' || !Array.isArray(value) || value.length === 0)
|
|
340
|
+
continue;
|
|
341
|
+
if (!value.every((v) => typeof v === 'string' && v.length > 0))
|
|
342
|
+
continue;
|
|
343
|
+
const list = value.filter((v) => v.length <= 64).slice(0, 500);
|
|
344
|
+
if (list.length === 0)
|
|
345
|
+
continue;
|
|
346
|
+
const name = key.toUpperCase();
|
|
347
|
+
if (name.includes('GENRE') || name.includes('TAG')) {
|
|
348
|
+
genres ??= list;
|
|
349
|
+
}
|
|
350
|
+
else if (name.includes('ORDER') || name.includes('SORT')) {
|
|
351
|
+
orders ??= list;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (!genres && !orders)
|
|
355
|
+
return undefined;
|
|
356
|
+
return { ...(genres ? { genres } : {}), ...(orders ? { orders } : {}) };
|
|
357
|
+
}
|
|
358
|
+
async function instantiateModule(filePath, cacheBuster, ctx, item) {
|
|
359
|
+
const id = item.id;
|
|
360
|
+
let mod;
|
|
361
|
+
try {
|
|
362
|
+
const importUrl = `${pathToFileURL(filePath).href}?v=${encodeURIComponent(cacheBuster)}`;
|
|
363
|
+
mod = (await withTimeout(import(importUrl), FETCH_TIMEOUT_MS, `import ${id}`));
|
|
364
|
+
}
|
|
365
|
+
catch (err) {
|
|
366
|
+
return {
|
|
367
|
+
provider: null,
|
|
368
|
+
error: `import failed: ${err.message}`,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
const factory = mod.default;
|
|
372
|
+
if (typeof factory !== 'function') {
|
|
373
|
+
return { provider: null, error: 'missing default factory export' };
|
|
374
|
+
}
|
|
375
|
+
let provider;
|
|
376
|
+
try {
|
|
377
|
+
provider = await withTimeout(Promise.resolve(factory(ctx)), 10000, `factory ${id}`);
|
|
378
|
+
}
|
|
379
|
+
catch (err) {
|
|
380
|
+
return {
|
|
381
|
+
provider: null,
|
|
382
|
+
error: `factory failed: ${err.message}`,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
if (item.kind === 'manga'
|
|
386
|
+
? !isValidMangaProvider(provider)
|
|
387
|
+
: item.kind === 'tv'
|
|
388
|
+
? !isValidTvProvider(provider)
|
|
389
|
+
: !isValidVideoProvider(provider)) {
|
|
390
|
+
return { provider: null, error: 'invalid provider shape' };
|
|
391
|
+
}
|
|
392
|
+
return { provider, facets: extractBrowseFacets(mod) };
|
|
393
|
+
}
|
|
394
|
+
async function loadCachedFallback(item, prev, cacheDir, ctx) {
|
|
395
|
+
if (!prev.sha256)
|
|
396
|
+
return { provider: null, error: 'no verified fallback' };
|
|
397
|
+
const filePath = path.join(cacheDir, `${prev.id}-${prev.version}.mjs`);
|
|
398
|
+
let bytes;
|
|
399
|
+
try {
|
|
400
|
+
bytes = await fs.promises.readFile(filePath);
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
return { provider: null, error: 'no cached fallback file' };
|
|
404
|
+
}
|
|
405
|
+
if (sha256Hex(bytes) !== prev.sha256) {
|
|
406
|
+
return { provider: null, error: 'cached fallback failed verification' };
|
|
407
|
+
}
|
|
408
|
+
const cacheBuster = `${prev.version}-${prev.sha256.slice(0, 12)}`;
|
|
409
|
+
return instantiateModule(filePath, cacheBuster, ctx, { ...item, version: prev.version });
|
|
410
|
+
}
|
|
411
|
+
export async function loadRemoteProviders(opts) {
|
|
412
|
+
const registryUrl = (opts.registryUrl ?? CONFIG.PROVIDER_REPO_URL ?? '').trim();
|
|
413
|
+
const out = { providers: {}, mangaProviders: {}, tvProviders: {}, catalog: [] };
|
|
414
|
+
if (!registryUrl)
|
|
415
|
+
return out;
|
|
416
|
+
const cacheDir = path.join(CONFIG.ROOT, 'remote-providers');
|
|
417
|
+
const cachedManifestPath = path.join(cacheDir, 'registry.json');
|
|
418
|
+
let prevRegistry = null;
|
|
419
|
+
try {
|
|
420
|
+
prevRegistry = parseRegistry(JSON.parse((await fs.promises.readFile(cachedManifestPath)).toString('utf8')));
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
// ignore
|
|
424
|
+
}
|
|
425
|
+
const prevById = new Map((prevRegistry?.providers ?? []).map((p) => [p.id.toLowerCase(), p]));
|
|
426
|
+
let raw;
|
|
427
|
+
try {
|
|
428
|
+
const bytes = await readManifestBytes(registryUrl);
|
|
429
|
+
raw = JSON.parse(bytes.toString('utf8'));
|
|
430
|
+
await fs.promises.mkdir(cacheDir, { recursive: true });
|
|
431
|
+
await fs.promises.writeFile(cachedManifestPath, JSON.stringify(raw));
|
|
432
|
+
}
|
|
433
|
+
catch (err) {
|
|
434
|
+
logger.error({ err, registryUrl }, 'remote providers: manifest fetch failed, trying disk cache');
|
|
435
|
+
try {
|
|
436
|
+
raw = JSON.parse((await fs.promises.readFile(cachedManifestPath)).toString('utf8'));
|
|
437
|
+
}
|
|
438
|
+
catch {
|
|
439
|
+
return out;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
let registry;
|
|
443
|
+
try {
|
|
444
|
+
registry = parseRegistry(raw);
|
|
445
|
+
}
|
|
446
|
+
catch (err) {
|
|
447
|
+
logger.error({ err, registryUrl }, 'remote providers: manifest parse failed');
|
|
448
|
+
return out;
|
|
449
|
+
}
|
|
450
|
+
const enabledSet = new Set((opts.enabledProviders ?? []).map((s) => s.toLowerCase()));
|
|
451
|
+
const explicitFilter = enabledSet.size > 0;
|
|
452
|
+
const ctx = createCtx(opts.cache);
|
|
453
|
+
const allowUnsigned = opts.allowUnsigned ??
|
|
454
|
+
(process.env.DANGO_ALLOW_UNSIGNED_PROVIDERS === '1' || looksLikePath(registryUrl));
|
|
455
|
+
for (const item of registry.providers) {
|
|
456
|
+
const key = item.id.toLowerCase();
|
|
457
|
+
if (item.enabledByDefault === false && (!explicitFilter || !enabledSet.has(key))) {
|
|
458
|
+
out.catalog.push(toCatalog(item, false, 'disabled by default'));
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
if (explicitFilter && !enabledSet.has(key)) {
|
|
462
|
+
out.catalog.push(toCatalog(item, false, 'not in enabledProviders'));
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
const { provider, facets, error } = await loadEntry(item, registryUrl, cacheDir, ctx, allowUnsigned);
|
|
466
|
+
if (provider && !error) {
|
|
467
|
+
if (item.kind === 'manga' && isValidMangaProvider(provider)) {
|
|
468
|
+
out.mangaProviders[key] = provider;
|
|
469
|
+
}
|
|
470
|
+
else if (item.kind === 'tv' && isValidTvProvider(provider)) {
|
|
471
|
+
out.tvProviders[key] = provider;
|
|
472
|
+
}
|
|
473
|
+
else if (item.kind !== 'manga' && item.kind !== 'tv' && isValidVideoProvider(provider)) {
|
|
474
|
+
out.providers[key] = provider;
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
out.catalog.push(toCatalog(item, false, 'invalid provider shape for kind'));
|
|
478
|
+
logger.warn({ provider: item.id }, 'remote provider kind mismatch, skipped');
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
out.catalog.push(withFacets(facets, withServers(item, provider, toCatalog(item, true))));
|
|
482
|
+
logger.info({ provider: item.id, version: item.version }, 'remote provider loaded');
|
|
483
|
+
}
|
|
484
|
+
else {
|
|
485
|
+
const prev = prevById.get(key);
|
|
486
|
+
if (prev && prev.version !== item.version) {
|
|
487
|
+
const fb = await loadCachedFallback(item, prev, cacheDir, ctx);
|
|
488
|
+
if (fb.provider && !fb.error) {
|
|
489
|
+
if (item.kind === 'manga' && isValidMangaProvider(fb.provider)) {
|
|
490
|
+
out.mangaProviders[key] = fb.provider;
|
|
491
|
+
}
|
|
492
|
+
else if (item.kind === 'tv' && isValidTvProvider(fb.provider)) {
|
|
493
|
+
out.tvProviders[key] = fb.provider;
|
|
494
|
+
}
|
|
495
|
+
else if (item.kind !== 'manga' &&
|
|
496
|
+
item.kind !== 'tv' &&
|
|
497
|
+
isValidVideoProvider(fb.provider)) {
|
|
498
|
+
out.providers[key] = fb.provider;
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
fb.error = 'cached fallback failed validation';
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
if (fb.provider && !fb.error) {
|
|
505
|
+
out.catalog.push({
|
|
506
|
+
...withFacets(fb.facets, withServers(item, fb.provider, toCatalog({ ...item, version: prev.version }, true))),
|
|
507
|
+
error: `serving cached ${prev.version}: ${error}`,
|
|
508
|
+
});
|
|
509
|
+
logger.warn({ provider: item.id, error, fallback: prev.version }, 'remote provider update failed, serving cached version');
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
out.catalog.push(toCatalog(item, false, error));
|
|
514
|
+
logger.warn({ provider: item.id, error }, 'remote provider skipped');
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
const failedIds = new Set(out.catalog.filter((c) => !c.loaded).map((c) => `${c.id}-`));
|
|
518
|
+
try {
|
|
519
|
+
const expected = new Set(registry.providers.map((item) => `${item.id}-${item.version}.mjs`.toLowerCase()));
|
|
520
|
+
const expectedInstances = new Set(registry.providers.map((item) => `${item.kind ?? 'anime'}:${item.id}-${item.version}-${(item.sha256 ?? 'nosha').slice(0, 12)}`.toLowerCase()));
|
|
521
|
+
for (const key of instanceCache.keys()) {
|
|
522
|
+
if (!expectedInstances.has(key.toLowerCase()))
|
|
523
|
+
instanceCache.delete(key);
|
|
524
|
+
}
|
|
525
|
+
const files = await fs.promises.readdir(cacheDir);
|
|
526
|
+
for (const file of files) {
|
|
527
|
+
if (!file.toLowerCase().endsWith('.mjs'))
|
|
528
|
+
continue;
|
|
529
|
+
if (!expected.has(file.toLowerCase())) {
|
|
530
|
+
if ([...failedIds].some((prefix) => file.toLowerCase().startsWith(prefix)))
|
|
531
|
+
continue;
|
|
532
|
+
await fs.promises.unlink(path.join(cacheDir, file));
|
|
533
|
+
logger.info({ file }, 'remote providers: removed superseded module');
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
catch (err) {
|
|
538
|
+
logger.warn({ err }, 'remote providers: superseded module cleanup failed');
|
|
539
|
+
}
|
|
540
|
+
return out;
|
|
541
|
+
}
|
|
542
|
+
function withServers(item, provider, cat) {
|
|
543
|
+
if (item.kind !== 'tv' || !provider || typeof provider !== 'object')
|
|
544
|
+
return cat;
|
|
545
|
+
const servers = provider.servers;
|
|
546
|
+
if (Array.isArray(servers) && servers.every((s) => typeof s === 'string') && servers.length > 0) {
|
|
547
|
+
return { ...cat, servers: [...servers] };
|
|
548
|
+
}
|
|
549
|
+
return cat;
|
|
550
|
+
}
|
|
551
|
+
function withFacets(facets, cat) {
|
|
552
|
+
const genres = (facets?.genres ?? []).length > 0 ? facets?.genres : undefined;
|
|
553
|
+
const orders = (facets?.orders ?? []).length > 0 ? facets?.orders : undefined;
|
|
554
|
+
if (!genres && !orders)
|
|
555
|
+
return cat;
|
|
556
|
+
return { ...cat, facets: { ...(genres ? { genres } : {}), ...(orders ? { orders } : {}) } };
|
|
557
|
+
}
|
|
558
|
+
function toCatalog(item, loaded, error) {
|
|
559
|
+
return {
|
|
560
|
+
id: item.id.toLowerCase(),
|
|
561
|
+
label: item.label,
|
|
562
|
+
version: item.version,
|
|
563
|
+
mature: item.mature,
|
|
564
|
+
kind: item.kind ?? 'anime',
|
|
565
|
+
sub: item.sub,
|
|
566
|
+
tier: item.tier,
|
|
567
|
+
modes: item.modes ?? (item.kind === 'tv' ? [] : ['sub', 'dub']),
|
|
568
|
+
browse: item.browse,
|
|
569
|
+
enabledByDefault: item.enabledByDefault !== false,
|
|
570
|
+
loaded,
|
|
571
|
+
remote: true,
|
|
572
|
+
error,
|
|
573
|
+
};
|
|
574
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
const ID_RE = /^[a-z0-9][a-z0-9-]*$/i;
|
|
2
|
+
const SHA_RE = /^[0-9a-f]{64}$/i;
|
|
3
|
+
export function parseRegistry(raw) {
|
|
4
|
+
if (!raw || typeof raw !== 'object')
|
|
5
|
+
throw new Error('registry: not an object');
|
|
6
|
+
const r = raw;
|
|
7
|
+
if (typeof r.version !== 'number')
|
|
8
|
+
throw new Error('registry.version must be a number');
|
|
9
|
+
if (!Array.isArray(r.providers))
|
|
10
|
+
throw new Error('registry.providers must be an array');
|
|
11
|
+
const providers = r.providers.map((p, i) => parseEntry(p, i));
|
|
12
|
+
const seen = new Set();
|
|
13
|
+
for (const p of providers) {
|
|
14
|
+
const key = p.id.toLowerCase();
|
|
15
|
+
if (seen.has(key))
|
|
16
|
+
throw new Error(`registry: duplicate provider id "${p.id}"`);
|
|
17
|
+
seen.add(key);
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
version: r.version,
|
|
21
|
+
updatedAt: typeof r.updatedAt === 'string' ? r.updatedAt : undefined,
|
|
22
|
+
providers,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function parseEntry(raw, index) {
|
|
26
|
+
if (!raw || typeof raw !== 'object')
|
|
27
|
+
throw new Error(`registry.providers[${index}]: not an object`);
|
|
28
|
+
const p = raw;
|
|
29
|
+
const id = p.id;
|
|
30
|
+
const entry = p.entry;
|
|
31
|
+
const label = p.label;
|
|
32
|
+
const version = p.version;
|
|
33
|
+
if (typeof id !== 'string' || !ID_RE.test(id.trim())) {
|
|
34
|
+
throw new Error(`registry.providers[${index}].id must match ${ID_RE}`);
|
|
35
|
+
}
|
|
36
|
+
if (typeof label !== 'string' || !label.trim()) {
|
|
37
|
+
throw new Error(`registry.providers[${index}].label must be a non-empty string`);
|
|
38
|
+
}
|
|
39
|
+
if (typeof version !== 'string' || !version.trim()) {
|
|
40
|
+
throw new Error(`registry.providers[${index}].version must be a non-empty string`);
|
|
41
|
+
}
|
|
42
|
+
if (typeof entry !== 'string' || !entry.trim()) {
|
|
43
|
+
throw new Error(`registry.providers[${index}].entry must be a non-empty string`);
|
|
44
|
+
}
|
|
45
|
+
if (p.sha256 !== undefined && (typeof p.sha256 !== 'string' || !SHA_RE.test(p.sha256.trim()))) {
|
|
46
|
+
throw new Error(`registry.providers[${index}].sha256 must be 64 hex chars`);
|
|
47
|
+
}
|
|
48
|
+
if (p.mature !== undefined && typeof p.mature !== 'boolean') {
|
|
49
|
+
throw new Error(`registry.providers[${index}].mature must be boolean`);
|
|
50
|
+
}
|
|
51
|
+
if (p.kind !== undefined &&
|
|
52
|
+
p.kind !== 'anime' &&
|
|
53
|
+
p.kind !== 'asmr' &&
|
|
54
|
+
p.kind !== 'manga' &&
|
|
55
|
+
p.kind !== 'tv') {
|
|
56
|
+
throw new Error(`registry.providers[${index}].kind must be anime|asmr|manga|tv`);
|
|
57
|
+
}
|
|
58
|
+
if (p.sub !== undefined && p.sub !== 'soft' && p.sub !== 'hard' && p.sub !== 'mixed') {
|
|
59
|
+
throw new Error(`registry.providers[${index}].sub must be soft|hard|mixed`);
|
|
60
|
+
}
|
|
61
|
+
if (p.tier !== undefined && p.tier !== 'direct' && p.tier !== 'embed' && p.tier !== 'cookie') {
|
|
62
|
+
throw new Error(`registry.providers[${index}].tier must be direct|embed|cookie`);
|
|
63
|
+
}
|
|
64
|
+
let modes;
|
|
65
|
+
if (p.modes !== undefined) {
|
|
66
|
+
if (!Array.isArray(p.modes))
|
|
67
|
+
throw new Error(`registry.providers[${index}].modes must be an array`);
|
|
68
|
+
modes = [];
|
|
69
|
+
for (const m of p.modes) {
|
|
70
|
+
if (m !== 'sub' && m !== 'dub') {
|
|
71
|
+
throw new Error(`registry.providers[${index}].modes must contain only sub|dub`);
|
|
72
|
+
}
|
|
73
|
+
if (!modes.includes(m))
|
|
74
|
+
modes.push(m);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
id: id.trim(),
|
|
79
|
+
label: label.trim(),
|
|
80
|
+
version: version.trim(),
|
|
81
|
+
entry: entry.trim(),
|
|
82
|
+
sha256: typeof p.sha256 === 'string' ? p.sha256.trim().toLowerCase() : undefined,
|
|
83
|
+
minDango: typeof p.minDango === 'string' ? p.minDango : undefined,
|
|
84
|
+
mature: typeof p.mature === 'boolean' ? p.mature : false,
|
|
85
|
+
kind: p.kind === 'asmr' || p.kind === 'manga' || p.kind === 'tv' ? p.kind : 'anime',
|
|
86
|
+
sub: p.sub,
|
|
87
|
+
tier: p.tier,
|
|
88
|
+
modes,
|
|
89
|
+
browse: parseBrowseCaps(p.browse, index),
|
|
90
|
+
auth: p.auth === 'cookie' ? 'cookie' : undefined,
|
|
91
|
+
enabledByDefault: p.enabledByDefault === false ? false : true,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function parseBrowseCaps(raw, index) {
|
|
95
|
+
if (raw === undefined)
|
|
96
|
+
return undefined;
|
|
97
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
98
|
+
throw new Error(`registry.providers[${index}].browse must be an object`);
|
|
99
|
+
}
|
|
100
|
+
const b = raw;
|
|
101
|
+
const caps = {};
|
|
102
|
+
for (const key of ['genre', 'order', 'studio', 'sort']) {
|
|
103
|
+
if (b[key] === undefined)
|
|
104
|
+
continue;
|
|
105
|
+
if (typeof b[key] !== 'boolean') {
|
|
106
|
+
throw new Error(`registry.providers[${index}].browse.${key} must be boolean`);
|
|
107
|
+
}
|
|
108
|
+
caps[key] = b[key];
|
|
109
|
+
}
|
|
110
|
+
if (b.pageSize !== undefined) {
|
|
111
|
+
if (typeof b.pageSize !== 'number' || !Number.isInteger(b.pageSize) || b.pageSize < 1) {
|
|
112
|
+
throw new Error(`registry.providers[${index}].browse.pageSize must be a positive integer`);
|
|
113
|
+
}
|
|
114
|
+
caps.pageSize = b.pageSize;
|
|
115
|
+
}
|
|
116
|
+
return caps;
|
|
117
|
+
}
|