@serifpersia/dango 3.6.5 → 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.
Files changed (151) hide show
  1. package/README.md +1 -1
  2. package/client/dist/assets/{AnimeCard-JkXG40Es.js → AnimeCard-DraA_TF9.js} +1 -1
  3. package/client/dist/assets/{AnimeInfo-2L0UJkbB.js → AnimeInfo-BJ7pzs-P.js} +1 -1
  4. package/client/dist/assets/{AnimeInfoPage-BDs03vKk.js → AnimeInfoPage-B_esUr5C.js} +2 -2
  5. package/client/dist/assets/{Asmr-D2mVKEl9.js → Asmr-Ckuh2xfW.js} +1 -1
  6. package/client/dist/assets/{AsmrResetProgressModal-MQA_bNQM.js → AsmrResetProgressModal-BS6UFJN4.js} +1 -1
  7. package/client/dist/assets/AvSyncCalibrator-DwHCd9yB.js +10 -0
  8. package/client/dist/assets/{Home-DxMBHFmc.js → Home-DuM1EuvN.js} +1 -1
  9. package/client/dist/assets/{Insights-hmLWsSXY.js → Insights-D3Pd9c9V.js} +1 -1
  10. package/client/dist/assets/{LibraryListPage-CWI7hgUh.js → LibraryListPage-Dm2yVn0-.js} +1 -1
  11. package/client/dist/assets/{ListeningList-NOc28KAV.js → ListeningList-D-zt6EXZ.js} +1 -1
  12. package/client/dist/assets/{Manga-B5qNhI7v.js → Manga-JwprMKBk.js} +1 -1
  13. package/client/dist/assets/{MangaInfoPage-D4wIyb-V.js → MangaInfoPage-DaeyJl2p.js} +1 -1
  14. package/client/dist/assets/{MangaReadPage-Ce1VX7-0.js → MangaReadPage-COg0fHI9.js} +1 -1
  15. package/client/dist/assets/{MangaResetProgressModal-P3oY4_14.js → MangaResetProgressModal-kguXv0_M.js} +1 -1
  16. package/client/dist/assets/{Mature-D8EwGYQq.js → Mature-IHDaasrs.js} +1 -1
  17. package/client/dist/assets/{MediaCard-Dp8DjyXB.js → MediaCard-CpwsrXo5.js} +1 -1
  18. package/client/dist/assets/{Music-DULtVsAM.js → Music-Dks4tPeK.js} +1 -1
  19. package/client/dist/assets/{OptionListMenu-fIHdE-nq.js → OptionListMenu-11CcAsy8.js} +1 -1
  20. package/client/dist/assets/{Player-B8XeOmso.js → Player-DQoEQkHL.js} +6 -6
  21. package/client/dist/assets/{PlayerSettings-BycNCiCs.js → PlayerSettings-D4Ud4kv4.js} +1 -1
  22. package/client/dist/assets/{QueueOptionsButton-T1J37IZT.js → QueueOptionsButton-B8PlPKjC.js} +1 -1
  23. package/client/dist/assets/{QueueRail-BAwI-apz.js → QueueRail-B4Dok7U3.js} +1 -1
  24. package/client/dist/assets/{Radio-Bv9DyKtZ.js → Radio-BHK6LSk4.js} +1 -1
  25. package/client/dist/assets/{ReadingList-Cl0CcVp3.js → ReadingList-BCsxIz57.js} +1 -1
  26. package/client/dist/assets/{ResetProgressModal--N60WWCB.js → ResetProgressModal-BXHxSfy-.js} +1 -1
  27. package/client/dist/assets/{Search-DvgVDlUR.js → Search-PK1ds07O.js} +1 -1
  28. package/client/dist/assets/{Settings-dCz_-bJn.js → Settings-Bxk_x55l.js} +1 -1
  29. package/client/dist/assets/{SkeletonGrid-BZHKk1I6.js → SkeletonGrid-CpJCZSRN.js} +1 -1
  30. package/client/dist/assets/{Trackers-C1T5VcPu.js → Trackers-RieCh_84.js} +1 -1
  31. package/client/dist/assets/{Tv-Dhv_cg4y.js → Tv-DLytPJUj.js} +4 -4
  32. package/client/dist/assets/{TvInfo-DgaLK2H6.js → TvInfo-C8djKebR.js} +1 -1
  33. package/client/dist/assets/{TvPopup-DyX7f8kC.js → TvPopup-40JGGJDk.js} +1 -1
  34. package/client/dist/assets/{TvSearch-Bp0H6kue.js → TvSearch-A3hbospf.js} +1 -1
  35. package/client/dist/assets/{TvWatchlist-ohMtMeKa.js → TvWatchlist-DuXjqmZM.js} +1 -1
  36. package/client/dist/assets/{Watchlist-BbQtzsER.js → Watchlist-B9wGBh0u.js} +1 -1
  37. package/client/dist/assets/{hls-Bi1cjpLZ.js → hls-CZ5X3Ya-.js} +1 -1
  38. package/client/dist/assets/{index-BTnhzofk.js → index-dRhc9-ol.js} +2 -2
  39. package/client/dist/assets/{useAnimeInfoData-Cvmso3IK.js → useAnimeInfoData-ZzDtLfvD.js} +1 -1
  40. package/client/dist/assets/{useAsmr-CbzGCzmv.js → useAsmr-CKOdHgol.js} +1 -1
  41. package/client/dist/assets/{useAsmrPopup-DaREZ9Ie.js → useAsmrPopup-SFHPhpcl.js} +1 -1
  42. package/client/dist/assets/{useMangaLibrary-BVQRhsFh.js → useMangaLibrary-CKATe31P.js} +1 -1
  43. package/client/dist/assets/{useMangaPopup-CzsCbndG.js → useMangaPopup-C75QuYgY.js} +1 -1
  44. package/client/dist/assets/{useProviders-UuUPzft-.js → useProviders-CMtvZbEi.js} +1 -1
  45. package/client/dist/assets/{useTv-CSsbWKH9.js → useTv-DUX4zYMr.js} +1 -1
  46. package/client/dist/assets/{useTvLibrary-a_Y0c2FS.js → useTvLibrary-By4Gt-R_.js} +1 -1
  47. package/client/dist/index.html +1 -1
  48. package/package.json +2 -2
  49. package/server/dist/app-auth.js +1 -157
  50. package/server/dist/app-hono.js +7 -557
  51. package/server/dist/config.js +1 -174
  52. package/server/dist/db/drizzle.js +1 -16
  53. package/server/dist/db/schema-anime.js +1 -61
  54. package/server/dist/db/schema-asmr.js +1 -13
  55. package/server/dist/db/schema-manga.js +1 -31
  56. package/server/dist/db/schema-tv.js +1 -18
  57. package/server/dist/db.js +1 -217
  58. package/server/dist/discord-gateway.js +1 -385
  59. package/server/dist/discord-rpc.js +1 -413
  60. package/server/dist/github-sync.js +1 -508
  61. package/server/dist/google.js +1 -741
  62. package/server/dist/hono/asmr.js +1 -10
  63. package/server/dist/hono/auth.js +3 -26
  64. package/server/dist/hono/music.js +1 -209
  65. package/server/dist/hono/proxy.js +36 -905
  66. package/server/dist/hono/tv.js +5 -169
  67. package/server/dist/hono/watchlist.js +2 -338
  68. package/server/dist/lib/anilist.js +26 -1458
  69. package/server/dist/lib/anischedule.js +1 -191
  70. package/server/dist/lib/insights.js +1 -185
  71. package/server/dist/lib/ipc.js +1 -23
  72. package/server/dist/lib/kitsu.js +1 -517
  73. package/server/dist/lib/mal-import.js +1 -291
  74. package/server/dist/lib/mal.js +1 -957
  75. package/server/dist/lib/migration.js +1 -310
  76. package/server/dist/lib/offline-db.js +4 -384
  77. package/server/dist/lib/potoken.js +1 -135
  78. package/server/dist/lib/recommendations.service.js +1 -216
  79. package/server/dist/lib/temp-ids.js +1 -3
  80. package/server/dist/lib/tmdb.js +1 -72
  81. package/server/dist/lib/tracker/anilist-tracker.js +8 -245
  82. package/server/dist/lib/tracker/manga-sync.service.js +1 -773
  83. package/server/dist/lib/tracker/sync-state.js +1 -21
  84. package/server/dist/lib/tracker/sync.service.js +1 -476
  85. package/server/dist/lib/translate.js +3 -79
  86. package/server/dist/lib/ump.js +1 -106
  87. package/server/dist/lib/ytmusic.js +1 -162
  88. package/server/dist/logger.js +1 -16
  89. package/server/dist/providers/manga/manga.types.js +0 -1
  90. package/server/dist/providers/provider.interface.js +0 -1
  91. package/server/dist/providers/remote-loader.js +2 -605
  92. package/server/dist/providers/remote-types.js +1 -124
  93. package/server/dist/providers/title-matching.js +1 -226
  94. package/server/dist/providers/tv.types.js +0 -1
  95. package/server/dist/rclone.js +2 -120
  96. package/server/dist/repositories/asmr.repository.js +11 -46
  97. package/server/dist/repositories/insights.repository.js +11 -25
  98. package/server/dist/repositories/library.repository.js +3 -94
  99. package/server/dist/repositories/mal-cache.repository.js +1 -52
  100. package/server/dist/repositories/manga.repository.js +14 -58
  101. package/server/dist/repositories/notifications.repository.js +6 -52
  102. package/server/dist/repositories/queue.repository.js +5 -58
  103. package/server/dist/repositories/settings.repository.js +2 -29
  104. package/server/dist/repositories/shows-meta.repository.js +3 -56
  105. package/server/dist/repositories/temp-show-ids.repository.js +3 -63
  106. package/server/dist/repositories/tv.repository.js +11 -54
  107. package/server/dist/repositories/watched-episodes.repository.js +6 -55
  108. package/server/dist/repositories/watchlist.repository.js +3 -54
  109. package/server/dist/scripts/backfill-episode-duration.js +1 -42
  110. package/server/dist/server.js +1 -307
  111. package/server/dist/shipped-defaults.js +1 -6
  112. package/server/dist/sync-payload.js +1 -91
  113. package/server/dist/sync.js +3 -647
  114. package/server/dist/trpc/context.js +1 -7
  115. package/server/dist/trpc/errors.js +1 -15
  116. package/server/dist/trpc/index.js +1 -18
  117. package/server/dist/trpc/router.js +1 -49
  118. package/server/dist/trpc/routers/asmr-library.js +1 -112
  119. package/server/dist/trpc/routers/asmr-progress.js +1 -121
  120. package/server/dist/trpc/routers/asmr.js +1 -87
  121. package/server/dist/trpc/routers/auth.js +1 -194
  122. package/server/dist/trpc/routers/continue-watching.js +2 -58
  123. package/server/dist/trpc/routers/data.js +1 -1026
  124. package/server/dist/trpc/routers/discord.js +1 -285
  125. package/server/dist/trpc/routers/discovery.js +1 -11
  126. package/server/dist/trpc/routers/insights.js +1 -14
  127. package/server/dist/trpc/routers/manga-library.js +1 -263
  128. package/server/dist/trpc/routers/manga-progress.js +1 -169
  129. package/server/dist/trpc/routers/manga.js +1 -201
  130. package/server/dist/trpc/routers/music.js +1 -655
  131. package/server/dist/trpc/routers/notifications.js +1 -80
  132. package/server/dist/trpc/routers/progress.js +1 -109
  133. package/server/dist/trpc/routers/queue.js +1 -93
  134. package/server/dist/trpc/routers/radio.js +1 -145
  135. package/server/dist/trpc/routers/settings.js +1 -96
  136. package/server/dist/trpc/routers/tracker.js +1 -213
  137. package/server/dist/trpc/routers/tv-library.js +1 -135
  138. package/server/dist/trpc/routers/tv-progress.js +1 -159
  139. package/server/dist/trpc/routers/tv.js +1 -551
  140. package/server/dist/trpc/routers/watchlist.js +2 -549
  141. package/server/dist/trpc/validation.js +1 -258
  142. package/server/dist/utils/cache.utils.js +1 -25
  143. package/server/dist/utils/cookie.utils.js +1 -13
  144. package/server/dist/utils/db-utils.js +5 -17
  145. package/server/dist/utils/env.utils.js +5 -77
  146. package/server/dist/utils/http.utils.js +1 -82
  147. package/server/dist/utils/request-context.js +1 -2
  148. package/server/dist/utils/security.utils.js +1 -76
  149. package/server/package.json +3 -2
  150. package/client/dist/assets/AvSyncCalibrator-DmoH5eRA.js +0 -10
  151. package/server/dist/utils/megaplay.utils.js +0 -40
@@ -1,605 +1,2 @@
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 { base64UrlEncode } from '../utils/megaplay.utils.js';
13
- import { decodeMaybeGzip, parseJsonBody } from '../utils/http.utils.js';
14
- import { buildQueryVariants, pickBestMatch } from './title-matching.js';
15
- import { anilistRequest, parseMalId, searchAnilistByTitle } from '../lib/anilist.js';
16
- import { kitsuMetaByAnilistId } from '../lib/kitsu.js';
17
- import { getTmdbKey, TMDB_BASE, TMDB_IMAGE } from '../lib/tmdb.js';
18
- import { parseRegistry, } from './remote-types.js';
19
- const FETCH_TIMEOUT_MS = 15000;
20
- 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';
21
- function withTimeout(p, ms, label) {
22
- let timer;
23
- const timeout = new Promise((_, reject) => {
24
- timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
25
- });
26
- return Promise.race([p, timeout]).finally(() => clearTimeout(timer));
27
- }
28
- function isHttp(url) {
29
- return /^https?:\/\//i.test(url);
30
- }
31
- function stripFilePrefix(p) {
32
- if (p.startsWith('file://')) {
33
- const u = new URL(p);
34
- return u.pathname;
35
- }
36
- return p;
37
- }
38
- function looksLikePath(p) {
39
- if (isHttp(p) || p.startsWith('file://'))
40
- return false;
41
- return p.includes('/') || p.includes('\\') || p.endsWith('.json');
42
- }
43
- async function readManifestBytes(registryUrl) {
44
- if (isHttp(registryUrl)) {
45
- const res = await withTimeout(fetch(registryUrl, { headers: { 'User-Agent': GENERIC_UA, Accept: 'application/json' } }), FETCH_TIMEOUT_MS, 'registry fetch');
46
- if (!res.ok)
47
- throw new Error(`registry HTTP ${res.status}: ${registryUrl}`);
48
- return decodeMaybeGzip(Buffer.from(await res.arrayBuffer()));
49
- }
50
- const filePath = path.resolve(stripFilePrefix(registryUrl));
51
- return await fs.promises.readFile(filePath);
52
- }
53
- async function readModuleBytes(entryUrl) {
54
- if (isHttp(entryUrl)) {
55
- const res = await withTimeout(fetch(entryUrl, { headers: { 'User-Agent': GENERIC_UA, Accept: 'text/javascript,*/*' } }), FETCH_TIMEOUT_MS, 'provider fetch');
56
- if (!res.ok)
57
- throw new Error(`provider HTTP ${res.status}: ${entryUrl}`);
58
- return decodeMaybeGzip(Buffer.from(await res.arrayBuffer()));
59
- }
60
- const filePath = path.resolve(stripFilePrefix(entryUrl));
61
- return await fs.promises.readFile(filePath);
62
- }
63
- function resolveEntryUrl(entry, registryUrl) {
64
- const e = (entry ?? '').trim();
65
- if (isHttp(e) || e.startsWith('file://'))
66
- return e;
67
- if (isHttp(registryUrl))
68
- return new URL(e, registryUrl).href;
69
- const baseDir = path.dirname(path.resolve(stripFilePrefix(registryUrl)));
70
- return path.resolve(baseDir, e);
71
- }
72
- function sha256Hex(data) {
73
- return crypto.createHash('sha256').update(data).digest('hex');
74
- }
75
- function aes256CbcDecryptJson(b64url, keyUtf8, ivUtf8) {
76
- let encrypted;
77
- try {
78
- encrypted = Buffer.from(b64url, 'base64url');
79
- }
80
- catch {
81
- return null;
82
- }
83
- if (!encrypted.length || encrypted.length % 16 !== 0)
84
- return null;
85
- try {
86
- const key = Buffer.alloc(32);
87
- Buffer.from(keyUtf8).copy(key);
88
- const decipher = crypto.createDecipheriv('aes-256-cbc', key, Buffer.from(ivUtf8));
89
- const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
90
- return JSON.parse(decrypted.toString('utf8'));
91
- }
92
- catch {
93
- return null;
94
- }
95
- }
96
- function curlRaw(url, headers) {
97
- const args = [
98
- '-sSL',
99
- '-A',
100
- GENERIC_UA,
101
- ...Object.entries(headers ?? {}).flatMap(([k, v]) => ['-H', `${k}: ${v}`]),
102
- '--max-time',
103
- '15',
104
- '-w',
105
- '\n__HTTP_STATUS__%{http_code}',
106
- '--',
107
- url,
108
- ];
109
- let out;
110
- try {
111
- out = execFileSync('curl', args, { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 });
112
- }
113
- catch {
114
- return null;
115
- }
116
- const m = out.match(/__HTTP_STATUS__(\d+)\s*$/);
117
- const status = m ? parseInt(m[1], 10) : 0;
118
- return { status, body: m && m.index !== undefined ? out.slice(0, m.index) : out };
119
- }
120
- function createCtx(cache) {
121
- const fetchWithTimeout = async (url, init, ms = FETCH_TIMEOUT_MS) => {
122
- return withTimeout(fetch(url, {
123
- ...init,
124
- headers: { 'User-Agent': GENERIC_UA, ...init?.headers },
125
- }), ms, `fetch ${url}`);
126
- };
127
- return {
128
- cache: {
129
- get: (key) => cache.get(key),
130
- set: (key, value, ttlSeconds) => {
131
- cache.set(key, value, ttlSeconds);
132
- },
133
- },
134
- logger: {
135
- info: (obj, msg) => {
136
- logger.info(obj, msg);
137
- },
138
- warn: (obj, msg) => {
139
- logger.warn(obj, msg);
140
- },
141
- error: (obj, msg) => {
142
- logger.error(obj, msg);
143
- },
144
- debug: (obj, msg) => {
145
- logger.debug(obj, msg);
146
- },
147
- },
148
- fetchText: async (url, init) => {
149
- try {
150
- const res = await fetchWithTimeout(url, init);
151
- if (!res.ok)
152
- return null;
153
- return await res.text();
154
- }
155
- catch {
156
- return null;
157
- }
158
- },
159
- fetchJson: async (url, init) => {
160
- const res = await fetchWithTimeout(url, init);
161
- if (!res.ok)
162
- throw new Error(`HTTP ${res.status}: ${url}`);
163
- return parseJsonBody(res);
164
- },
165
- proxyUrl: (rawUrl, referer) => {
166
- return `/api/proxy?url=${encodeURIComponent(rawUrl)}&referer=${encodeURIComponent(referer)}`;
167
- },
168
- userAgent: GENERIC_UA,
169
- titleMatch: {
170
- buildQueryVariants,
171
- pickBestMatch,
172
- },
173
- resolveBestShowId: async (title, romaji, searchFn) => {
174
- const targets = [title, romaji].filter((t) => typeof t === 'string' && t.trim().length > 0);
175
- if (targets.length === 0)
176
- return null;
177
- for (const variant of buildQueryVariants(title, romaji)) {
178
- let results;
179
- try {
180
- results = await searchFn(variant);
181
- }
182
- catch (e) {
183
- if (e.message === 'AUTH_REQUIRED')
184
- throw e;
185
- continue;
186
- }
187
- if (!results || results.length === 0)
188
- continue;
189
- const match = pickBestMatch(results.filter((r) => r && typeof r.id === 'string' && r.id.length > 0), targets);
190
- if (match)
191
- return match.item.id;
192
- }
193
- return null;
194
- },
195
- anilist: {
196
- request: (query, vars) => anilistRequest(query, vars),
197
- parseMalId,
198
- searchByTitle: searchAnilistByTitle,
199
- },
200
- kitsu: {
201
- metaByAnilistId: kitsuMetaByAnilistId,
202
- },
203
- tmdb: {
204
- base: TMDB_BASE,
205
- image: TMDB_IMAGE,
206
- get: async (path) => {
207
- try {
208
- const key = await getTmdbKey();
209
- const sep = path.includes('?') ? '&' : '?';
210
- const res = await fetchWithTimeout(`${TMDB_BASE}${path}${sep}api_key=${key}`);
211
- if (!res.ok)
212
- return null;
213
- return parseJsonBody(res);
214
- }
215
- catch {
216
- return null;
217
- }
218
- },
219
- },
220
- request: {
221
- get: (key) => requestContext.getStore()?.get(key),
222
- },
223
- cookies: {
224
- sanitizeCfClearance,
225
- buildCfClearanceCookie,
226
- },
227
- crypto: {
228
- aes256CbcDecryptJson,
229
- hmacSha256Base64Url: (key, message) => base64UrlEncode(crypto.createHmac('sha256', key).update(message).digest()),
230
- },
231
- scraping: {
232
- fetch: async (urlOrOptions, maybeOptions) => {
233
- const opts = (typeof urlOrOptions === 'string'
234
- ? { url: urlOrOptions, ...(maybeOptions ?? {}) }
235
- : urlOrOptions);
236
- const { url, ...rest } = opts;
237
- const res = await gotScraping({
238
- url: url,
239
- method: 'GET',
240
- responseType: 'text',
241
- throwHttpErrors: false,
242
- ...rest,
243
- });
244
- return { statusCode: res.statusCode, body: String(res.body ?? ''), headers: res.headers };
245
- },
246
- },
247
- curl: {
248
- getJson: async (url, headers) => {
249
- try {
250
- const r = curlRaw(url, headers);
251
- if (!r || r.status !== 200)
252
- return null;
253
- return JSON.parse(r.body);
254
- }
255
- catch {
256
- return null;
257
- }
258
- },
259
- getText: async (url, headers) => {
260
- const r = curlRaw(url, headers);
261
- if (!r || r.status !== 200)
262
- return null;
263
- return r.body;
264
- },
265
- },
266
- cheerio: {
267
- load: (html) => cheerio.load(html),
268
- },
269
- };
270
- }
271
- function shapeOf(p) {
272
- if (!p || typeof p !== 'object')
273
- return null;
274
- const o = p;
275
- return typeof o.name === 'string' ? o : null;
276
- }
277
- function hasFns(o, fns) {
278
- return fns.every((f) => typeof o[f] === 'function');
279
- }
280
- function isValidVideoProvider(p) {
281
- const o = shapeOf(p);
282
- return !!o && hasFns(o, ['search', 'getEpisodes', 'getStreamUrls']);
283
- }
284
- function isValidMangaProvider(p) {
285
- const o = shapeOf(p);
286
- return !!o && hasFns(o, ['search', 'getDetail', 'getChapters', 'getPages']);
287
- }
288
- function isValidTvProvider(p) {
289
- const o = shapeOf(p);
290
- return !!o && (typeof o.getSources === 'function' || typeof o.getEmbedUrl === 'function');
291
- }
292
- function makeEmbedProvider(id, base) {
293
- const root = base.trim().replace(/\/+$/, '');
294
- return {
295
- name: id,
296
- getEmbedUrl: async (media) => {
297
- const tmdbId = Number(media.tmdbId);
298
- if (!tmdbId)
299
- return null;
300
- if (media.type === 'movie')
301
- return `${root}/movie/${tmdbId}`;
302
- return `${root}/tv/${tmdbId}/${media.season || 1}/${media.episode || 1}`;
303
- },
304
- };
305
- }
306
- async function loadEntry(item, registryUrl, cacheDir, ctx, allowUnsigned) {
307
- if (item.embedBase) {
308
- const provider = makeEmbedProvider(item.id, item.embedBase);
309
- instanceCache.set(`${item.kind ?? 'anime'}:${item.id}-${item.version}-embed`, { provider });
310
- return { provider };
311
- }
312
- const entryUrl = resolveEntryUrl(item.entry, registryUrl);
313
- await fs.promises.mkdir(cacheDir, { recursive: true });
314
- const fileName = `${item.id}-${item.version}.mjs`;
315
- const filePath = path.join(cacheDir, fileName);
316
- const instanceKey = `${item.kind ?? 'anime'}:${item.id}-${item.version}-${(item.sha256 ?? 'nosha').slice(0, 12)}`;
317
- let bytes = null;
318
- try {
319
- const cached = await fs.promises.readFile(filePath);
320
- if (item.sha256 && sha256Hex(cached) === item.sha256) {
321
- bytes = cached;
322
- }
323
- else if (!item.sha256 && allowUnsigned) {
324
- bytes = cached;
325
- }
326
- }
327
- catch {
328
- // ignore
329
- }
330
- if (bytes) {
331
- const hit = instanceCache.get(instanceKey);
332
- if (hit) {
333
- logger.debug({ provider: item.id, version: item.version }, 'remote provider reused cached instance');
334
- return { provider: hit.provider, facets: hit.facets };
335
- }
336
- }
337
- else {
338
- try {
339
- bytes = await readModuleBytes(entryUrl);
340
- }
341
- catch (err) {
342
- return {
343
- provider: null,
344
- error: `download failed: ${err.message}`,
345
- };
346
- }
347
- if (item.sha256) {
348
- const actual = sha256Hex(bytes);
349
- if (actual !== item.sha256) {
350
- return { provider: null, error: `sha256 mismatch for ${item.id}` };
351
- }
352
- }
353
- else if (!allowUnsigned) {
354
- return { provider: null, error: `missing sha256 for ${item.id}` };
355
- }
356
- await fs.promises.writeFile(filePath, bytes);
357
- }
358
- const cacheBuster = `${item.version}-${(item.sha256 ?? 'nosha').slice(0, 12)}`;
359
- const result = await instantiateModule(filePath, cacheBuster, ctx, item);
360
- if (result.provider && !result.error) {
361
- instanceCache.set(instanceKey, { provider: result.provider, facets: result.facets });
362
- }
363
- return result;
364
- }
365
- const instanceCache = new Map();
366
- function extractBrowseFacets(mod) {
367
- let genres;
368
- let orders;
369
- for (const [key, value] of Object.entries(mod)) {
370
- if (key === 'default' || !Array.isArray(value) || value.length === 0)
371
- continue;
372
- if (!value.every((v) => typeof v === 'string' && v.length > 0))
373
- continue;
374
- const list = value.filter((v) => v.length <= 64).slice(0, 500);
375
- if (list.length === 0)
376
- continue;
377
- const name = key.toUpperCase();
378
- if (name.includes('GENRE') || name.includes('TAG')) {
379
- genres ??= list;
380
- }
381
- else if (name.includes('ORDER') || name.includes('SORT')) {
382
- orders ??= list;
383
- }
384
- }
385
- if (!genres && !orders)
386
- return undefined;
387
- return { ...(genres ? { genres } : {}), ...(orders ? { orders } : {}) };
388
- }
389
- async function instantiateModule(filePath, cacheBuster, ctx, item) {
390
- const id = item.id;
391
- let mod;
392
- try {
393
- const importUrl = `${pathToFileURL(filePath).href}?v=${encodeURIComponent(cacheBuster)}`;
394
- mod = (await withTimeout(import(importUrl), FETCH_TIMEOUT_MS, `import ${id}`));
395
- }
396
- catch (err) {
397
- return {
398
- provider: null,
399
- error: `import failed: ${err.message}`,
400
- };
401
- }
402
- const factory = mod.default;
403
- if (typeof factory !== 'function') {
404
- return { provider: null, error: 'missing default factory export' };
405
- }
406
- let provider;
407
- try {
408
- provider = await withTimeout(Promise.resolve(factory(ctx)), 10000, `factory ${id}`);
409
- }
410
- catch (err) {
411
- return {
412
- provider: null,
413
- error: `factory failed: ${err.message}`,
414
- };
415
- }
416
- if (item.kind === 'manga'
417
- ? !isValidMangaProvider(provider)
418
- : item.kind === 'tv'
419
- ? !isValidTvProvider(provider)
420
- : !isValidVideoProvider(provider)) {
421
- return { provider: null, error: 'invalid provider shape' };
422
- }
423
- return { provider, facets: extractBrowseFacets(mod) };
424
- }
425
- async function loadCachedFallback(item, prev, cacheDir, ctx) {
426
- if (!prev.sha256)
427
- return { provider: null, error: 'no verified fallback' };
428
- const filePath = path.join(cacheDir, `${prev.id}-${prev.version}.mjs`);
429
- let bytes;
430
- try {
431
- bytes = await fs.promises.readFile(filePath);
432
- }
433
- catch {
434
- return { provider: null, error: 'no cached fallback file' };
435
- }
436
- if (sha256Hex(bytes) !== prev.sha256) {
437
- return { provider: null, error: 'cached fallback failed verification' };
438
- }
439
- const cacheBuster = `${prev.version}-${prev.sha256.slice(0, 12)}`;
440
- return instantiateModule(filePath, cacheBuster, ctx, { ...item, version: prev.version });
441
- }
442
- export async function loadRemoteProviders(opts) {
443
- const registryUrl = (opts.registryUrl ?? CONFIG.PROVIDER_REPO_URL ?? '').trim();
444
- const out = { providers: {}, mangaProviders: {}, tvProviders: {}, catalog: [] };
445
- if (!registryUrl)
446
- return out;
447
- const cacheDir = path.join(CONFIG.ROOT, 'remote-providers');
448
- const cachedManifestPath = path.join(cacheDir, 'registry.json');
449
- let prevRegistry = null;
450
- try {
451
- prevRegistry = parseRegistry(JSON.parse((await fs.promises.readFile(cachedManifestPath)).toString('utf8')));
452
- }
453
- catch {
454
- // ignore
455
- }
456
- const prevById = new Map((prevRegistry?.providers ?? []).map((p) => [p.id.toLowerCase(), p]));
457
- let raw;
458
- try {
459
- const bytes = await readManifestBytes(registryUrl);
460
- raw = JSON.parse(bytes.toString('utf8'));
461
- await fs.promises.mkdir(cacheDir, { recursive: true });
462
- await fs.promises.writeFile(cachedManifestPath, JSON.stringify(raw));
463
- }
464
- catch (err) {
465
- logger.error({ err, registryUrl }, 'remote providers: manifest fetch failed, trying disk cache');
466
- try {
467
- raw = JSON.parse((await fs.promises.readFile(cachedManifestPath)).toString('utf8'));
468
- }
469
- catch {
470
- return out;
471
- }
472
- }
473
- let registry;
474
- try {
475
- registry = parseRegistry(raw);
476
- }
477
- catch (err) {
478
- logger.error({ err, registryUrl }, 'remote providers: manifest parse failed');
479
- return out;
480
- }
481
- const enabledSet = new Set((opts.enabledProviders ?? []).map((s) => s.toLowerCase()));
482
- const explicitFilter = enabledSet.size > 0;
483
- const ctx = createCtx(opts.cache);
484
- const allowUnsigned = opts.allowUnsigned ??
485
- (process.env.DANGO_ALLOW_UNSIGNED_PROVIDERS === '1' || looksLikePath(registryUrl));
486
- for (const item of registry.providers) {
487
- const key = item.id.toLowerCase();
488
- if (item.enabledByDefault === false && (!explicitFilter || !enabledSet.has(key))) {
489
- out.catalog.push(toCatalog(item, false, 'disabled by default'));
490
- continue;
491
- }
492
- if (explicitFilter && !enabledSet.has(key)) {
493
- out.catalog.push(toCatalog(item, false, 'not in enabledProviders'));
494
- continue;
495
- }
496
- const { provider, facets, error } = await loadEntry(item, registryUrl, cacheDir, ctx, allowUnsigned);
497
- if (provider && !error) {
498
- if (item.kind === 'manga' && isValidMangaProvider(provider)) {
499
- out.mangaProviders[key] = provider;
500
- }
501
- else if (item.kind === 'tv' && isValidTvProvider(provider)) {
502
- out.tvProviders[key] = provider;
503
- }
504
- else if (item.kind !== 'manga' && item.kind !== 'tv' && isValidVideoProvider(provider)) {
505
- out.providers[key] = provider;
506
- }
507
- else {
508
- out.catalog.push(toCatalog(item, false, 'invalid provider shape for kind'));
509
- logger.warn({ provider: item.id }, 'remote provider kind mismatch, skipped');
510
- continue;
511
- }
512
- out.catalog.push(withFacets(facets, withServers(item, provider, toCatalog(item, true))));
513
- logger.info({ provider: item.id, version: item.version }, 'remote provider loaded');
514
- }
515
- else {
516
- const prev = prevById.get(key);
517
- if (prev && prev.version !== item.version) {
518
- const fb = await loadCachedFallback(item, prev, cacheDir, ctx);
519
- if (fb.provider && !fb.error) {
520
- if (item.kind === 'manga' && isValidMangaProvider(fb.provider)) {
521
- out.mangaProviders[key] = fb.provider;
522
- }
523
- else if (item.kind === 'tv' && isValidTvProvider(fb.provider)) {
524
- out.tvProviders[key] = fb.provider;
525
- }
526
- else if (item.kind !== 'manga' &&
527
- item.kind !== 'tv' &&
528
- isValidVideoProvider(fb.provider)) {
529
- out.providers[key] = fb.provider;
530
- }
531
- else {
532
- fb.error = 'cached fallback failed validation';
533
- }
534
- }
535
- if (fb.provider && !fb.error) {
536
- out.catalog.push({
537
- ...withFacets(fb.facets, withServers(item, fb.provider, toCatalog({ ...item, version: prev.version }, true))),
538
- error: `serving cached ${prev.version}: ${error}`,
539
- });
540
- logger.warn({ provider: item.id, error, fallback: prev.version }, 'remote provider update failed, serving cached version');
541
- continue;
542
- }
543
- }
544
- out.catalog.push(toCatalog(item, false, error));
545
- logger.warn({ provider: item.id, error }, 'remote provider skipped');
546
- }
547
- }
548
- const failedIds = new Set(out.catalog.filter((c) => !c.loaded).map((c) => `${c.id}-`));
549
- try {
550
- const expected = new Set(registry.providers.map((item) => `${item.id}-${item.version}.mjs`.toLowerCase()));
551
- const expectedInstances = new Set(registry.providers.map((item) => `${item.kind ?? 'anime'}:${item.id}-${item.version}-${(item.sha256 ?? 'nosha').slice(0, 12)}`.toLowerCase()));
552
- for (const key of instanceCache.keys()) {
553
- if (!expectedInstances.has(key.toLowerCase()))
554
- instanceCache.delete(key);
555
- }
556
- const files = await fs.promises.readdir(cacheDir);
557
- for (const file of files) {
558
- if (!file.toLowerCase().endsWith('.mjs'))
559
- continue;
560
- if (!expected.has(file.toLowerCase())) {
561
- if ([...failedIds].some((prefix) => file.toLowerCase().startsWith(prefix)))
562
- continue;
563
- await fs.promises.unlink(path.join(cacheDir, file));
564
- logger.info({ file }, 'remote providers: removed superseded module');
565
- }
566
- }
567
- }
568
- catch (err) {
569
- logger.warn({ err }, 'remote providers: superseded module cleanup failed');
570
- }
571
- return out;
572
- }
573
- function withServers(item, provider, cat) {
574
- if (item.kind !== 'tv' || !provider || typeof provider !== 'object')
575
- return cat;
576
- const servers = provider.servers;
577
- if (Array.isArray(servers) && servers.every((s) => typeof s === 'string') && servers.length > 0) {
578
- return { ...cat, servers: [...servers] };
579
- }
580
- return cat;
581
- }
582
- function withFacets(facets, cat) {
583
- const genres = (facets?.genres ?? []).length > 0 ? facets?.genres : undefined;
584
- const orders = (facets?.orders ?? []).length > 0 ? facets?.orders : undefined;
585
- if (!genres && !orders)
586
- return cat;
587
- return { ...cat, facets: { ...(genres ? { genres } : {}), ...(orders ? { orders } : {}) } };
588
- }
589
- function toCatalog(item, loaded, error) {
590
- return {
591
- id: item.id.toLowerCase(),
592
- label: item.label,
593
- version: item.version,
594
- mature: item.mature,
595
- kind: item.kind ?? 'anime',
596
- sub: item.sub,
597
- tier: item.tier,
598
- modes: item.modes ?? (item.kind === 'tv' ? [] : ['sub', 'dub']),
599
- browse: item.browse,
600
- enabledByDefault: item.enabledByDefault !== false,
601
- loaded,
602
- remote: true,
603
- error,
604
- };
605
- }
1
+ import m from"fs";import y from"path";import E from"crypto";import{execFileSync as K}from"node:child_process";import{pathToFileURL as Q}from"url";import*as X from"cheerio";import{gotScraping as Y}from"got-scraping";import g from"../logger.js";import{CONFIG as M}from"../config.js";import{requestContext as Z}from"../utils/request-context.js";import{sanitizeCfClearance as ee,buildCfClearanceCookie as re}from"../utils/cookie.utils.js";import{decodeMaybeGzip as U,parseJsonBody as j}from"../utils/http.utils.js";import{buildQueryVariants as O,pickBestMatch as D}from"./title-matching.js";import{anilistRequest as te,parseMalId as ne,searchAnilistByTitle as oe}from"../lib/anilist.js";import{kitsuMetaByAnilistId as se}from"../lib/kitsu.js";import{getTmdbKey as ie,TMDB_BASE as N,TMDB_IMAGE as ae}from"../lib/tmdb.js";import{parseRegistry as H}from"./remote-types.js";const x=15e3,b="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";function T(r,n,e){let t;const o=new Promise((a,c)=>{t=setTimeout(()=>c(new Error(`${e} timed out after ${n}ms`)),n)});return Promise.race([r,o]).finally(()=>clearTimeout(t))}function $(r){return/^https?:\/\//i.test(r)}function S(r){return r.startsWith("file://")?new URL(r).pathname:r}function ce(r){return $(r)||r.startsWith("file://")?!1:r.includes("/")||r.includes("\\")||r.endsWith(".json")}async function de(r){if($(r)){const e=await T(fetch(r,{headers:{"User-Agent":b,Accept:"application/json"}}),x,"registry fetch");if(!e.ok)throw new Error(`registry HTTP ${e.status}: ${r}`);return U(Buffer.from(await e.arrayBuffer()))}const n=y.resolve(S(r));return await m.promises.readFile(n)}async function ue(r){if($(r)){const e=await T(fetch(r,{headers:{"User-Agent":b,Accept:"text/javascript,*/*"}}),x,"provider fetch");if(!e.ok)throw new Error(`provider HTTP ${e.status}: ${r}`);return U(Buffer.from(await e.arrayBuffer()))}const n=y.resolve(S(r));return await m.promises.readFile(n)}function le(r,n){const e=(r??"").trim();if($(e)||e.startsWith("file://"))return e;if($(n))return new URL(e,n).href;const t=y.dirname(y.resolve(S(n)));return y.resolve(t,e)}function I(r){return E.createHash("sha256").update(r).digest("hex")}function fe(r,n,e){let t;try{t=Buffer.from(r,"base64url")}catch{return null}if(!t.length||t.length%16!==0)return null;try{const o=Buffer.alloc(32);Buffer.from(n).copy(o);const a=E.createDecipheriv("aes-256-cbc",o,Buffer.from(e)),c=Buffer.concat([a.update(t),a.final()]);return JSON.parse(c.toString("utf8"))}catch{return null}}function W(r,n){const e=["-sSL","-A",b,...Object.entries(n??{}).flatMap(([c,i])=>["-H",`${c}: ${i}`]),"--max-time","15","-w",`
2
+ __HTTP_STATUS__%{http_code}`,"--",r];let t;try{t=K("curl",e,{encoding:"utf-8",maxBuffer:10*1024*1024})}catch{return null}const o=t.match(/__HTTP_STATUS__(\d+)\s*$/);return{status:o?parseInt(o[1],10):0,body:o&&o.index!==void 0?t.slice(0,o.index):t}}function pe(r){const n=async(e,t,o=x)=>T(fetch(e,{...t,headers:{"User-Agent":b,...t?.headers}}),o,`fetch ${e}`);return{cache:{get:e=>r.get(e),set:(e,t,o)=>{r.set(e,t,o)}},logger:{info:(e,t)=>{g.info(e,t)},warn:(e,t)=>{g.warn(e,t)},error:(e,t)=>{g.error(e,t)},debug:(e,t)=>{g.debug(e,t)}},fetchText:async(e,t)=>{try{const o=await n(e,t);return o.ok?await o.text():null}catch{return null}},fetchJson:async(e,t)=>{const o=await n(e,t);if(!o.ok)throw new Error(`HTTP ${o.status}: ${e}`);return j(o)},proxyUrl:(e,t)=>`/api/proxy?url=${encodeURIComponent(e)}&referer=${encodeURIComponent(t)}`,userAgent:b,titleMatch:{buildQueryVariants:O,pickBestMatch:D},resolveBestShowId:async(e,t,o)=>{const a=[e,t].filter(c=>typeof c=="string"&&c.trim().length>0);if(a.length===0)return null;for(const c of O(e,t)){let i;try{i=await o(c)}catch(u){if(u.message==="AUTH_REQUIRED")throw u;continue}if(!i||i.length===0)continue;const l=D(i.filter(u=>u&&typeof u.id=="string"&&u.id.length>0),a);if(l)return l.item.id}return null},anilist:{request:(e,t)=>te(e,t),parseMalId:ne,searchByTitle:oe},kitsu:{metaByAnilistId:se},tmdb:{base:N,image:ae,get:async e=>{try{const t=await ie(),o=e.includes("?")?"&":"?",a=await n(`${N}${e}${o}api_key=${t}`);return a.ok?j(a):null}catch{return null}}},request:{get:e=>Z.getStore()?.get(e)},cookies:{sanitizeCfClearance:ee,buildCfClearanceCookie:re},crypto:{aes256CbcDecryptJson:fe,hmacSha256Base64Url:(e,t)=>E.createHmac("sha256",e).update(t).digest("base64url")},scraping:{fetch:async(e,t)=>{const o=typeof e=="string"?{url:e,...t??{}}:e,{url:a,...c}=o,i=await Y({url:a,method:"GET",responseType:"text",throwHttpErrors:!1,...c});return{statusCode:i.statusCode,body:String(i.body??""),headers:i.headers}}},curl:{getJson:async(e,t)=>{try{const o=W(e,t);return!o||o.status!==200?null:JSON.parse(o.body)}catch{return null}},getText:async(e,t)=>{const o=W(e,t);return!o||o.status!==200?null:o.body}},cheerio:{load:e=>X.load(e)}}}function A(r){if(!r||typeof r!="object")return null;const n=r;return typeof n.name=="string"?n:null}function G(r,n){return n.every(e=>typeof r[e]=="function")}function F(r){const n=A(r);return!!n&&G(n,["search","getEpisodes","getStreamUrls"])}function L(r){const n=A(r);return!!n&&G(n,["search","getDetail","getChapters","getPages"])}function _(r){const n=A(r);return!!n&&(typeof n.getSources=="function"||typeof n.getEmbedUrl=="function")}function ge(r,n){const e=n.trim().replace(/\/+$/,"");return{name:r,getEmbedUrl:async t=>{const o=Number(t.tmdbId);return o?t.type==="movie"?`${e}/movie/${o}`:`${e}/tv/${o}/${t.season||1}/${t.episode||1}`:null}}}async function me(r,n,e,t,o){if(r.embedBase){const f=ge(r.id,r.embedBase);return C.set(`${r.kind??"anime"}:${r.id}-${r.version}-embed`,{provider:f}),{provider:f}}const a=le(r.entry,n);await m.promises.mkdir(e,{recursive:!0});const c=`${r.id}-${r.version}.mjs`,i=y.join(e,c),l=`${r.kind??"anime"}:${r.id}-${r.version}-${(r.sha256??"nosha").slice(0,12)}`;let u=null;try{const f=await m.promises.readFile(i);(r.sha256&&I(f)===r.sha256||!r.sha256&&o)&&(u=f)}catch{}if(u){const f=C.get(l);if(f)return g.debug({provider:r.id,version:r.version},"remote provider reused cached instance"),{provider:f.provider,facets:f.facets}}else{try{u=await ue(a)}catch(f){return{provider:null,error:`download failed: ${f.message}`}}if(r.sha256){if(I(u)!==r.sha256)return{provider:null,error:`sha256 mismatch for ${r.id}`}}else if(!o)return{provider:null,error:`missing sha256 for ${r.id}`};await m.promises.writeFile(i,u)}const B=`${r.version}-${(r.sha256??"nosha").slice(0,12)}`,w=await J(i,B,t,r);return w.provider&&!w.error&&C.set(l,{provider:w.provider,facets:w.facets}),w}const C=new Map;function ve(r){let n,e;for(const[t,o]of Object.entries(r)){if(t==="default"||!Array.isArray(o)||o.length===0||!o.every(i=>typeof i=="string"&&i.length>0))continue;const a=o.filter(i=>i.length<=64).slice(0,500);if(a.length===0)continue;const c=t.toUpperCase();c.includes("GENRE")||c.includes("TAG")?n??=a:(c.includes("ORDER")||c.includes("SORT"))&&(e??=a)}if(!(!n&&!e))return{...n?{genres:n}:{},...e?{orders:e}:{}}}async function J(r,n,e,t){const o=t.id;let a;try{const l=`${Q(r).href}?v=${encodeURIComponent(n)}`;a=await T(import(l),x,`import ${o}`)}catch(l){return{provider:null,error:`import failed: ${l.message}`}}const c=a.default;if(typeof c!="function")return{provider:null,error:"missing default factory export"};let i;try{i=await T(Promise.resolve(c(e)),1e4,`factory ${o}`)}catch(l){return{provider:null,error:`factory failed: ${l.message}`}}return(t.kind==="manga"?!L(i):t.kind==="tv"?!_(i):!F(i))?{provider:null,error:"invalid provider shape"}:{provider:i,facets:ve(a)}}async function he(r,n,e,t){if(!n.sha256)return{provider:null,error:"no verified fallback"};const o=y.join(e,`${n.id}-${n.version}.mjs`);let a;try{a=await m.promises.readFile(o)}catch{return{provider:null,error:"no cached fallback file"}}if(I(a)!==n.sha256)return{provider:null,error:"cached fallback failed verification"};const c=`${n.version}-${n.sha256.slice(0,12)}`;return J(o,c,t,{...r,version:n.version})}async function Me(r){const n=(r.registryUrl??M.PROVIDER_REPO_URL??"").trim(),e={providers:{},mangaProviders:{},tvProviders:{},catalog:[],proxyRules:[]};if(!n)return e;const t=y.join(M.ROOT,"remote-providers"),o=y.join(t,"registry.json");let a=null;try{a=H(JSON.parse((await m.promises.readFile(o)).toString("utf8")))}catch{}const c=new Map((a?.providers??[]).map(s=>[s.id.toLowerCase(),s]));let i;try{const s=await de(n);i=JSON.parse(s.toString("utf8")),await m.promises.mkdir(t,{recursive:!0}),await m.promises.writeFile(o,JSON.stringify(i))}catch(s){g.error({err:s,registryUrl:n},"remote providers: manifest fetch failed, trying disk cache");try{i=JSON.parse((await m.promises.readFile(o)).toString("utf8"))}catch{return e}}let l;try{l=H(i)}catch(s){return g.error({err:s,registryUrl:n},"remote providers: manifest parse failed"),e}const u=new Set((r.enabledProviders??[]).map(s=>s.toLowerCase())),B=u.size>0,w=pe(r.cache),f=r.allowUnsigned??(process.env.DANGO_ALLOW_UNSIGNED_PROVIDERS==="1"||ce(n));for(const s of l.providers){const v=s.id.toLowerCase();if(s.enabledByDefault===!1&&(!B||!u.has(v))){e.catalog.push(P(s,!1,"disabled by default"));continue}if(B&&!u.has(v)){e.catalog.push(P(s,!1,"not in enabledProviders"));continue}const{provider:h,facets:d,error:k}=await me(s,n,t,w,f);if(h&&!k){if(s.kind==="manga"&&L(h))e.mangaProviders[v]=h;else if(s.kind==="tv"&&_(h))e.tvProviders[v]=h;else if(s.kind!=="manga"&&s.kind!=="tv"&&F(h))e.providers[v]=h;else{e.catalog.push(P(s,!1,"invalid provider shape for kind")),g.warn({provider:s.id},"remote provider kind mismatch, skipped");continue}e.catalog.push(V(d,q(s,h,P(s,!0)))),s.proxy&&e.proxyRules.push(s.proxy),g.info({provider:s.id,version:s.version},"remote provider loaded")}else{const R=c.get(v);if(R&&R.version!==s.version){const p=await he(s,R,t,w);if(p.provider&&!p.error&&(s.kind==="manga"&&L(p.provider)?e.mangaProviders[v]=p.provider:s.kind==="tv"&&_(p.provider)?e.tvProviders[v]=p.provider:s.kind!=="manga"&&s.kind!=="tv"&&F(p.provider)?e.providers[v]=p.provider:p.error="cached fallback failed validation"),p.provider&&!p.error){e.catalog.push({...V(p.facets,q(s,p.provider,P({...s,version:R.version},!0))),error:`serving cached ${R.version}: ${k}`}),s.proxy&&e.proxyRules.push(s.proxy),g.warn({provider:s.id,error:k,fallback:R.version},"remote provider update failed, serving cached version");continue}}e.catalog.push(P(s,!1,k)),g.warn({provider:s.id,error:k},"remote provider skipped")}}const z=new Set(e.catalog.filter(s=>!s.loaded).map(s=>`${s.id}-`));try{const s=new Set(l.providers.map(d=>`${d.id}-${d.version}.mjs`.toLowerCase())),v=new Set(l.providers.map(d=>`${d.kind??"anime"}:${d.id}-${d.version}-${(d.sha256??"nosha").slice(0,12)}`.toLowerCase()));for(const d of C.keys())v.has(d.toLowerCase())||C.delete(d);const h=await m.promises.readdir(t);for(const d of h)if(d.toLowerCase().endsWith(".mjs")&&!s.has(d.toLowerCase())){if([...z].some(k=>d.toLowerCase().startsWith(k)))continue;await m.promises.unlink(y.join(t,d)),g.info({file:d},"remote providers: removed superseded module")}}catch(s){g.warn({err:s},"remote providers: superseded module cleanup failed")}return e}function q(r,n,e){if(r.kind!=="tv"||!n||typeof n!="object")return e;const t=n.servers;return Array.isArray(t)&&t.every(o=>typeof o=="string")&&t.length>0?{...e,servers:[...t]}:e}function V(r,n){const e=(r?.genres??[]).length>0?r?.genres:void 0,t=(r?.orders??[]).length>0?r?.orders:void 0;return!e&&!t?n:{...n,facets:{...e?{genres:e}:{},...t?{orders:t}:{}}}}function P(r,n,e){return{id:r.id.toLowerCase(),label:r.label,version:r.version,mature:r.mature,kind:r.kind??"anime",sub:r.sub,tier:r.tier,modes:r.modes??(r.kind==="tv"?[]:["sub","dub"]),browse:r.browse,enabledByDefault:r.enabledByDefault!==!1,loaded:n,remote:!0,error:e}}export{Me as loadRemoteProviders};