@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.
Files changed (141) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +247 -0
  3. package/client/dist/assets/AnimeInfo-CMxs0nRH.css +1 -0
  4. package/client/dist/assets/AnimeInfo-DVdaocCl.js +1 -0
  5. package/client/dist/assets/AnimeInfoPage-Bx5EvGt-.js +2 -0
  6. package/client/dist/assets/Asmr-C2i9GKD4.css +1 -0
  7. package/client/dist/assets/Asmr-CIjmFtRH.js +2 -0
  8. package/client/dist/assets/Asmr.module-DPG2JhDJ.js +1 -0
  9. package/client/dist/assets/AvSyncCalibrator-CIYcyon5.css +1 -0
  10. package/client/dist/assets/AvSyncCalibrator-vqErH7Cm.js +10 -0
  11. package/client/dist/assets/ErrorMessage-BwSjzblw.js +1 -0
  12. package/client/dist/assets/ErrorMessage-CXoRbrLN.css +1 -0
  13. package/client/dist/assets/Home-D7YfGBUk.js +1 -0
  14. package/client/dist/assets/Home-DKCfEiwl.css +1 -0
  15. package/client/dist/assets/Insights-BljI64Ua.css +1 -0
  16. package/client/dist/assets/Insights-CLQGrlpX.js +1 -0
  17. package/client/dist/assets/Manga-DQ9HYnpk.js +1 -0
  18. package/client/dist/assets/Manga-H8Wb04w3.css +1 -0
  19. package/client/dist/assets/Map-CXFqXmsZ.css +1 -0
  20. package/client/dist/assets/Map-L_ryVBTq.js +1 -0
  21. package/client/dist/assets/Mature-BeWzAJ7B.js +1 -0
  22. package/client/dist/assets/Mature-DJmUUtlF.css +1 -0
  23. package/client/dist/assets/Player-CYpd__FP.js +30 -0
  24. package/client/dist/assets/Player-DreAhybc.css +1 -0
  25. package/client/dist/assets/PlayerRedirect-fxUAp9JV.js +1 -0
  26. package/client/dist/assets/PlayerSettings-BcsotxP9.js +1 -0
  27. package/client/dist/assets/PlayerSettings-Cqua1CDD.css +1 -0
  28. package/client/dist/assets/QueueRail-B9Pgbqc3.js +1 -0
  29. package/client/dist/assets/QueueRail-BnaltOP8.css +1 -0
  30. package/client/dist/assets/Radio-BvdY5twD.js +1 -0
  31. package/client/dist/assets/Radio-CUuYy3DY.css +1 -0
  32. package/client/dist/assets/Search-BzO-aRP7.css +1 -0
  33. package/client/dist/assets/Search-R9YLAXS1.js +1 -0
  34. package/client/dist/assets/Search.module-D8ZbQAiK.js +1 -0
  35. package/client/dist/assets/Settings-9kYcXdTR.js +21 -0
  36. package/client/dist/assets/Settings-DF8JThUL.css +1 -0
  37. package/client/dist/assets/SkeletonGrid-D5vRvX5q.js +1 -0
  38. package/client/dist/assets/SkeletonGrid-DEbYtO4H.css +1 -0
  39. package/client/dist/assets/SynopsisText-BlFw0R-0.js +1 -0
  40. package/client/dist/assets/SynopsisText-CUgOjOxt.css +1 -0
  41. package/client/dist/assets/Trackers-DfZlnnVq.js +2 -0
  42. package/client/dist/assets/Trackers-Dt9lR_uP.css +1 -0
  43. package/client/dist/assets/Tv-CVKC2YBs.css +1 -0
  44. package/client/dist/assets/Tv-Db1Wx_b7.js +15 -0
  45. package/client/dist/assets/Watchlist-B2nkNZqM.css +1 -0
  46. package/client/dist/assets/Watchlist-a77CYtzu.js +1 -0
  47. package/client/dist/assets/hls-6Owgw2Aj.js +1 -0
  48. package/client/dist/assets/hls-BOLXTnjL.js +42 -0
  49. package/client/dist/assets/index-B6qdXFtl.css +1 -0
  50. package/client/dist/assets/index-B8TPVaIS.js +30354 -0
  51. package/client/dist/assets/index-CpjgU8ou.js +178 -0
  52. package/client/dist/assets/useAnimeInfoData-BOfyi9UJ.js +1 -0
  53. package/client/dist/assets/useIsMobile-CM15uQn8.css +1 -0
  54. package/client/dist/assets/useIsMobile-Cb8HcKRj.js +1 -0
  55. package/client/dist/assets/useMatureConsent-CZMYBVXd.js +1 -0
  56. package/client/dist/assets/vendor-rLNfZm7Y.js +1 -0
  57. package/client/dist/av-sync-test.mp4 +0 -0
  58. package/client/dist/favicon.ico +0 -0
  59. package/client/dist/index.html +31 -0
  60. package/client/dist/logo-mochi.png +0 -0
  61. package/client/dist/logo-sakura.png +0 -0
  62. package/client/dist/logo.png +0 -0
  63. package/client/dist/placeholder.svg +4 -0
  64. package/client/dist/robots.txt +3 -0
  65. package/orchestrator.js +416 -0
  66. package/package.json +89 -0
  67. package/server/dist/app-auth.js +231 -0
  68. package/server/dist/config.js +133 -0
  69. package/server/dist/controllers/auth.controller.js +247 -0
  70. package/server/dist/controllers/data.controller.js +785 -0
  71. package/server/dist/controllers/insights.controller.js +197 -0
  72. package/server/dist/controllers/proxy.controller.js +665 -0
  73. package/server/dist/controllers/settings.controller.js +267 -0
  74. package/server/dist/controllers/watchlist.controller.js +967 -0
  75. package/server/dist/db.js +152 -0
  76. package/server/dist/discord-gateway.js +381 -0
  77. package/server/dist/discord-rpc.js +424 -0
  78. package/server/dist/github-sync.js +403 -0
  79. package/server/dist/google.js +549 -0
  80. package/server/dist/lib/anilist.js +1519 -0
  81. package/server/dist/lib/anischedule.js +202 -0
  82. package/server/dist/lib/ipc.js +32 -0
  83. package/server/dist/lib/kitsu.js +552 -0
  84. package/server/dist/lib/mal.js +886 -0
  85. package/server/dist/lib/migration.js +335 -0
  86. package/server/dist/lib/temp-ids.js +11 -0
  87. package/server/dist/lib/tmdb.js +71 -0
  88. package/server/dist/lib/tracker/anilist-tracker.js +250 -0
  89. package/server/dist/lib/tracker/sync.service.js +312 -0
  90. package/server/dist/logger.js +19 -0
  91. package/server/dist/providers/123anime.provider.js +240 -0
  92. package/server/dist/providers/anilight.provider.js +366 -0
  93. package/server/dist/providers/animepahe.provider.js +531 -0
  94. package/server/dist/providers/animeya.provider.js +746 -0
  95. package/server/dist/providers/base-provider.js +45 -0
  96. package/server/dist/providers/hn.provider.js +473 -0
  97. package/server/dist/providers/ht.provider.js +439 -0
  98. package/server/dist/providers/jasmr.provider.js +504 -0
  99. package/server/dist/providers/kaa.provider.js +388 -0
  100. package/server/dist/providers/manga/manga.types.js +10 -0
  101. package/server/dist/providers/manga/mangadex.provider.js +227 -0
  102. package/server/dist/providers/manga/mangapill.provider.js +232 -0
  103. package/server/dist/providers/megaplay.provider.js +583 -0
  104. package/server/dist/providers/op.provider.js +440 -0
  105. package/server/dist/providers/provider.interface.js +2 -0
  106. package/server/dist/providers/title-matching.js +234 -0
  107. package/server/dist/providers/wh.provider.js +600 -0
  108. package/server/dist/rclone.js +126 -0
  109. package/server/dist/repositories/insights.repository.js +43 -0
  110. package/server/dist/repositories/mal-cache.repository.js +66 -0
  111. package/server/dist/repositories/notifications.repository.js +40 -0
  112. package/server/dist/repositories/queue.repository.js +64 -0
  113. package/server/dist/repositories/settings.repository.js +15 -0
  114. package/server/dist/repositories/shows-meta.repository.js +43 -0
  115. package/server/dist/repositories/temp-show-ids.repository.js +54 -0
  116. package/server/dist/repositories/watched-episodes.repository.js +54 -0
  117. package/server/dist/repositories/watchlist.repository.js +74 -0
  118. package/server/dist/routes/asmr.routes.js +72 -0
  119. package/server/dist/routes/auth.routes.js +28 -0
  120. package/server/dist/routes/data.routes.js +50 -0
  121. package/server/dist/routes/discord-gateway.routes.js +57 -0
  122. package/server/dist/routes/insights.routes.js +12 -0
  123. package/server/dist/routes/lan-auth.routes.js +49 -0
  124. package/server/dist/routes/manga.routes.js +115 -0
  125. package/server/dist/routes/proxy.routes.js +15 -0
  126. package/server/dist/routes/radio.routes.js +143 -0
  127. package/server/dist/routes/settings.routes.js +27 -0
  128. package/server/dist/routes/tracker.routes.js +143 -0
  129. package/server/dist/routes/translate.routes.js +90 -0
  130. package/server/dist/routes/tv.routes.js +843 -0
  131. package/server/dist/routes/watchlist.routes.js +312 -0
  132. package/server/dist/server.js +310 -0
  133. package/server/dist/shipped-defaults.js +8 -0
  134. package/server/dist/sync-config.js +32 -0
  135. package/server/dist/sync.js +506 -0
  136. package/server/dist/utils/cookie.utils.js +17 -0
  137. package/server/dist/utils/db-utils.js +15 -0
  138. package/server/dist/utils/env.utils.js +83 -0
  139. package/server/dist/utils/machine-id.js +46 -0
  140. package/server/dist/utils/request-context.js +5 -0
  141. package/server/dist/utils/security.utils.js +99 -0
@@ -0,0 +1,388 @@
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.KaaProvider = void 0;
7
+ const logger_1 = __importDefault(require("../logger"));
8
+ const base_provider_1 = require("./base-provider");
9
+ const KAA_BASE = 'https://kaa.lt';
10
+ const KAA_HLS_BASE = 'https://hls.krussdomi.com/manifest';
11
+ const KAA_REFERER = 'https://krussdomi.com/';
12
+ const KAA_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36';
13
+ const KAA_HEADERS = {
14
+ 'User-Agent': KAA_UA,
15
+ Accept: 'application/json',
16
+ };
17
+ class KaaProvider extends base_provider_1.BaseProvider {
18
+ name = 'kaa';
19
+ stripSlug(showId) {
20
+ if (!showId)
21
+ return null;
22
+ const s = showId.trim();
23
+ if (/^\d+$/.test(s))
24
+ return null;
25
+ if (s.startsWith('kaa:')) {
26
+ const rest = s.slice(4);
27
+ return rest || null;
28
+ }
29
+ if (s.length < 3)
30
+ return null;
31
+ return s;
32
+ }
33
+ async kaaSearch(query) {
34
+ const res = await fetch(`${KAA_BASE}/api/fsearch`, {
35
+ method: 'POST',
36
+ headers: { ...KAA_HEADERS, 'Content-Type': 'application/json' },
37
+ body: JSON.stringify({ page: 1, query }),
38
+ signal: AbortSignal.timeout(15000),
39
+ });
40
+ if (!res.ok)
41
+ throw new Error(`KAA fsearch HTTP ${res.status} for "${query}"`);
42
+ const data = (await res.json());
43
+ return Array.isArray(data?.result) ? data.result : [];
44
+ }
45
+ async kaaShowInfo(slug) {
46
+ const res = await fetch(`${KAA_BASE}/api/show/${encodeURIComponent(slug)}`, {
47
+ headers: KAA_HEADERS,
48
+ signal: AbortSignal.timeout(15000),
49
+ });
50
+ if (!res.ok)
51
+ throw new Error(`KAA show HTTP ${res.status}: ${slug}`);
52
+ return (await res.json());
53
+ }
54
+ async kaaEpisodePage(slug, ep, lang) {
55
+ const res = await fetch(`${KAA_BASE}/api/show/${encodeURIComponent(slug)}/episodes?ep=${ep}&lang=${encodeURIComponent(lang)}`, { headers: KAA_HEADERS, signal: AbortSignal.timeout(15000) });
56
+ if (!res.ok)
57
+ throw new Error(`KAA episodes HTTP ${res.status}: ${slug}`);
58
+ return (await res.json());
59
+ }
60
+ async kaaAllEpisodes(slug, lang) {
61
+ const first = await this.kaaEpisodePage(slug, 1, lang);
62
+ const pages = Array.isArray(first.pages) ? first.pages : [];
63
+ const all = Array.isArray(first.result) ? [...first.result] : [];
64
+ if (pages.length > 1) {
65
+ const rest = await Promise.all(pages.slice(1).map(async (pg) => {
66
+ const startEp = pg.eps?.[0];
67
+ if (startEp == null)
68
+ return [];
69
+ try {
70
+ const d = await this.kaaEpisodePage(slug, startEp, lang);
71
+ return Array.isArray(d.result) ? d.result : [];
72
+ }
73
+ catch {
74
+ return [];
75
+ }
76
+ }));
77
+ for (const batch of rest)
78
+ all.push(...batch);
79
+ }
80
+ return all;
81
+ }
82
+ async kaaEpisodeServers(slug, fullEpSlug) {
83
+ const res = await fetch(`${KAA_BASE}/api/show/${encodeURIComponent(slug)}/episode/${encodeURIComponent(fullEpSlug)}`, { headers: KAA_HEADERS, signal: AbortSignal.timeout(15000) });
84
+ if (!res.ok)
85
+ throw new Error(`KAA episode servers HTTP ${res.status}: ${fullEpSlug}`);
86
+ return (await res.json());
87
+ }
88
+ langFor(mode) {
89
+ return mode === 'dub' ? 'en-US' : 'ja-JP';
90
+ }
91
+ toShow(item) {
92
+ const name = item.title_en || item.title || 'Unknown';
93
+ return {
94
+ _id: item.slug,
95
+ id: item.slug,
96
+ name,
97
+ englishName: item.title_en || item.title,
98
+ names: {
99
+ english: item.title_en,
100
+ },
101
+ type: item.type,
102
+ year: item.year ?? null,
103
+ episodeCount: item.episode_count ?? null,
104
+ };
105
+ }
106
+ async search(options) {
107
+ try {
108
+ const query = (options.query || '').trim();
109
+ if (!query)
110
+ return [];
111
+ const results = await this.kaaSearch(query);
112
+ return results.filter((r) => r.slug).map((r) => this.toShow(r));
113
+ }
114
+ catch (error) {
115
+ logger_1.default.error({ error }, '[KAA] Search failed');
116
+ return [];
117
+ }
118
+ }
119
+ async buildEpMap(slug, show, lang) {
120
+ const episodes = await this.kaaAllEpisodes(slug, lang);
121
+ const map = episodes
122
+ .filter((e) => Number.isInteger(e.episode_number) && e.episode_number >= 1 && e.slug)
123
+ .map((e) => ({
124
+ number: e.episode_number,
125
+ fullSlug: `ep-${e.episode_number}-${e.slug}`,
126
+ }));
127
+ if (map.length > 0)
128
+ return map;
129
+ if (show?.type === 'movie' && show?.watch_uri) {
130
+ const m = show.watch_uri.match(/\/(ep-(\d+)-([a-f0-9]+))$/i);
131
+ if (m)
132
+ return [{ number: 1, fullSlug: m[1] }];
133
+ }
134
+ return [];
135
+ }
136
+ async getEpisodes(showId, mode) {
137
+ try {
138
+ const slug = this.stripSlug(showId);
139
+ if (!slug)
140
+ return null;
141
+ const cacheKey = `kaa_eps_${slug}_${mode || 'sub'}`;
142
+ const cached = this.cache.get(cacheKey);
143
+ if (cached)
144
+ return cached;
145
+ const show = await this.kaaShowInfo(slug);
146
+ const locales = Array.isArray(show.locales) ? show.locales : [];
147
+ if (mode === 'dub' && !locales.includes('en-US'))
148
+ return null;
149
+ const epMap = await this.buildEpMap(slug, show, this.langFor(mode));
150
+ if (!epMap.length)
151
+ return null;
152
+ const episodes = epMap.map((e) => String(e.number));
153
+ const result = { episodes, description: '' };
154
+ this.cache.set(cacheKey, result, 3600);
155
+ return result;
156
+ }
157
+ catch (error) {
158
+ logger_1.default.error({ error, showId, mode }, '[KAA] getEpisodes failed');
159
+ return null;
160
+ }
161
+ }
162
+ proxyUrl(rawUrl) {
163
+ return `/api/proxy?url=${encodeURIComponent(rawUrl)}&referer=${encodeURIComponent(KAA_REFERER)}`;
164
+ }
165
+ parsePlayerSubtitles(html) {
166
+ const seen = new Set();
167
+ const tracks = [];
168
+ const objRe = /\[0,\{([^}]*)\}\]/g;
169
+ let obj;
170
+ while ((obj = objRe.exec(html)) !== null) {
171
+ const block = obj[1];
172
+ const langM = block.match(/"language"\s*:\s*\[0\s*,\s*"([^"]+)"/);
173
+ const nameM = block.match(/"name"\s*:\s*\[0\s*,\s*"([^"]+)"/);
174
+ const srcM = block.match(/"src"\s*:\s*\[0\s*,\s*"(https?:\/\/[^"]+\.(?:srt|vtt)[^"]*)"/);
175
+ if (!srcM)
176
+ continue;
177
+ const url = srcM[1].replace(/^https:\/\/\//, 'https://');
178
+ if (seen.has(url))
179
+ continue;
180
+ seen.add(url);
181
+ tracks.push({
182
+ language: langM?.[1] || 'en',
183
+ label: nameM?.[1] || langM?.[1] || 'English',
184
+ url,
185
+ });
186
+ }
187
+ return tracks;
188
+ }
189
+ async fetchCatStreamData(playerSrc) {
190
+ let masterUrl = null;
191
+ const subtitles = [];
192
+ try {
193
+ const res = await fetch(playerSrc, {
194
+ headers: {
195
+ 'User-Agent': KAA_UA,
196
+ Referer: 'https://kaa.lt/',
197
+ Origin: 'https://kaa.lt',
198
+ Accept: 'text/html',
199
+ },
200
+ signal: AbortSignal.timeout(15000),
201
+ });
202
+ if (!res.ok)
203
+ return { masterUrl, subtitles };
204
+ const html = (await res.text()).replace(/&quot;/g, '"');
205
+ const manifestMatch = html.match(/"manifest"\s*:\s*\[0\s*,\s*"(\/\/[^"]+\.m3u8[^"]*)"\]/);
206
+ if (manifestMatch)
207
+ masterUrl = manifestMatch[1].replace(/^\/\//, 'https://');
208
+ subtitles.push(...this.parsePlayerSubtitles(html));
209
+ }
210
+ catch {
211
+ // ignore
212
+ }
213
+ return { masterUrl, subtitles };
214
+ }
215
+ async getMasterLevels(masterUrl) {
216
+ try {
217
+ const res = await fetch(masterUrl, {
218
+ headers: { Referer: KAA_REFERER, Origin: 'https://krussdomi.com', 'User-Agent': KAA_UA },
219
+ signal: AbortSignal.timeout(10000),
220
+ });
221
+ if (!res.ok)
222
+ return [];
223
+ const text = await res.text();
224
+ if (!text.includes('#EXT-X-MEDIA') || !text.includes('TYPE=AUDIO')) {
225
+ logger_1.default.warn('[KAA] master playlist has no audio group');
226
+ }
227
+ const levels = [];
228
+ const lines = text.split('\n');
229
+ let index = 0;
230
+ for (const raw of lines) {
231
+ const line = raw.trim();
232
+ if (!line.startsWith('#EXT-X-STREAM-INF'))
233
+ continue;
234
+ const nameMatch = line.match(/NAME="([^"]+)"/) || line.match(/RESOLUTION=\d+x(\d+)/);
235
+ let label = nameMatch ? nameMatch[1] : 'HD';
236
+ label = label.endsWith('p') ? label : `${label}p`;
237
+ levels.push({ index: index++, label });
238
+ }
239
+ return levels;
240
+ }
241
+ catch {
242
+ return [];
243
+ }
244
+ }
245
+ async fetchSubtitles(playerSrc) {
246
+ try {
247
+ const res = await fetch(playerSrc, {
248
+ headers: {
249
+ 'User-Agent': KAA_UA,
250
+ Referer: 'https://kaa.lt/',
251
+ Origin: 'https://kaa.lt',
252
+ Accept: 'text/html',
253
+ },
254
+ signal: AbortSignal.timeout(15000),
255
+ });
256
+ if (!res.ok)
257
+ return [];
258
+ const html = (await res.text()).replace(/&quot;/g, '"');
259
+ return this.parsePlayerSubtitles(html);
260
+ }
261
+ catch {
262
+ return [];
263
+ }
264
+ }
265
+ async getStreamUrls(showId, episodeNumber, mode) {
266
+ try {
267
+ const slug = this.stripSlug(showId);
268
+ if (!slug)
269
+ return null;
270
+ const epNum = Number(episodeNumber);
271
+ if (!Number.isInteger(epNum) || epNum < 1)
272
+ return null;
273
+ const cacheKey = `kaa_stream_${slug}_${epNum}_${mode || 'sub'}`;
274
+ const cached = this.cache.get(cacheKey);
275
+ if (cached)
276
+ return cached;
277
+ const show = await this.kaaShowInfo(slug);
278
+ const locales = Array.isArray(show.locales) ? show.locales : [];
279
+ if (mode === 'dub' && !locales.includes('en-US'))
280
+ return null;
281
+ const epMap = await this.buildEpMap(slug, show, this.langFor(mode));
282
+ const ep = epMap.find((e) => e.number === epNum);
283
+ if (!ep)
284
+ return null;
285
+ const episodeData = await this.kaaEpisodeServers(slug, ep.fullSlug);
286
+ const servers = Array.isArray(episodeData.servers) ? episodeData.servers : [];
287
+ if (!servers.length)
288
+ return null;
289
+ const sources = [];
290
+ let playerSrc = '';
291
+ let catStreamData = null;
292
+ for (const s of servers) {
293
+ const src = s.src || '';
294
+ const isVidstream = src.includes('source=vidstream');
295
+ const isCatstream = src.includes('source=catstream');
296
+ if (!isVidstream && !isCatstream)
297
+ continue;
298
+ const m = src.match(/[?&]id=([^&]+)/);
299
+ if (!m)
300
+ continue;
301
+ if (!playerSrc)
302
+ playerSrc = src;
303
+ if (isCatstream) {
304
+ if (!catStreamData)
305
+ catStreamData = await this.fetchCatStreamData(src);
306
+ if (!catStreamData?.masterUrl)
307
+ continue;
308
+ const masterUrl = catStreamData.masterUrl;
309
+ const linkHeaders = { Referer: KAA_REFERER };
310
+ const links = [];
311
+ for (const level of await this.getMasterLevels(masterUrl)) {
312
+ links.push({
313
+ resolutionStr: level.label,
314
+ link: `/api/proxy?url=${encodeURIComponent(masterUrl)}` +
315
+ `&referer=${encodeURIComponent(KAA_REFERER)}&variant=${level.index}`,
316
+ hls: true,
317
+ headers: linkHeaders,
318
+ });
319
+ }
320
+ links.push({
321
+ resolutionStr: 'Auto',
322
+ link: this.proxyUrl(masterUrl),
323
+ hls: true,
324
+ headers: linkHeaders,
325
+ });
326
+ sources.push({
327
+ sourceName: s.name ? `KAA ${s.name}` : 'KAA',
328
+ links,
329
+ type: 'player',
330
+ actualEpisodeNumber: String(epNum),
331
+ });
332
+ continue;
333
+ }
334
+ const masterUrl = `${KAA_HLS_BASE}/${m[1]}/master.m3u8`;
335
+ const linkHeaders = { Referer: KAA_REFERER };
336
+ const links = [];
337
+ for (const level of await this.getMasterLevels(masterUrl)) {
338
+ links.push({
339
+ resolutionStr: level.label,
340
+ link: `/api/proxy?url=${encodeURIComponent(masterUrl)}` +
341
+ `&referer=${encodeURIComponent(KAA_REFERER)}&variant=${level.index}`,
342
+ hls: true,
343
+ headers: linkHeaders,
344
+ });
345
+ }
346
+ links.push({
347
+ resolutionStr: 'Auto',
348
+ link: this.proxyUrl(masterUrl),
349
+ hls: true,
350
+ headers: linkHeaders,
351
+ });
352
+ sources.push({
353
+ sourceName: s.name ? `KAA ${s.name}` : 'KAA',
354
+ links,
355
+ type: 'player',
356
+ actualEpisodeNumber: String(epNum),
357
+ });
358
+ }
359
+ if (!sources.length)
360
+ return null;
361
+ const subtitles = [];
362
+ const seen = new Set();
363
+ const push = (list) => {
364
+ for (const t of list || []) {
365
+ if (seen.has(t.url))
366
+ continue;
367
+ seen.add(t.url);
368
+ subtitles.push(t);
369
+ }
370
+ };
371
+ push(catStreamData?.subtitles);
372
+ if (!subtitles.length && playerSrc) {
373
+ push(await this.fetchSubtitles(playerSrc));
374
+ }
375
+ if (subtitles.length) {
376
+ for (const src of sources)
377
+ src.subtitles = subtitles;
378
+ }
379
+ this.cache.set(cacheKey, sources, 3600);
380
+ return sources;
381
+ }
382
+ catch (error) {
383
+ logger_1.default.error({ error, showId, episodeNumber, mode }, '[KAA] getStreamUrls failed');
384
+ return null;
385
+ }
386
+ }
387
+ }
388
+ exports.KaaProvider = KaaProvider;
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MATURE_RATINGS = exports.SAFE_RATINGS = void 0;
4
+ exports.SAFE_RATINGS = ['safe'];
5
+ exports.MATURE_RATINGS = [
6
+ 'safe',
7
+ 'suggestive',
8
+ 'erotica',
9
+ 'pornographic',
10
+ ];
@@ -0,0 +1,227 @@
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.MangaDexProvider = void 0;
7
+ const logger_1 = __importDefault(require("../../logger"));
8
+ const API = 'https://api.mangadex.org';
9
+ const UA = 'Dango/3.0 (+https://github.com/serifpersia/dango)';
10
+ const PER_PAGE = 24;
11
+ function pickTitle(titles) {
12
+ if (!titles)
13
+ return 'Unknown';
14
+ return titles.en || titles['ja-ro'] || Object.values(titles)[0] || 'Unknown';
15
+ }
16
+ function coverFromRelationships(id, rels) {
17
+ const cover = rels?.find((r) => r.type === 'cover_art');
18
+ const fileName = cover?.attributes?.fileName;
19
+ if (!fileName)
20
+ return '';
21
+ return `https://uploads.mangadex.org/covers/${id}/${fileName}.256.jpg`;
22
+ }
23
+ async function getJson(url) {
24
+ const res = await fetch(url, {
25
+ headers: { 'User-Agent': UA, Accept: 'application/json' },
26
+ signal: AbortSignal.timeout(20000),
27
+ });
28
+ if (!res.ok)
29
+ throw new Error(`MangaDex HTTP ${res.status}: ${url}`);
30
+ return res.json();
31
+ }
32
+ function cleanDescription(raw) {
33
+ return raw
34
+ .replace(/!?\[[^\]]*\]\([^)]*\)/g, (m) => {
35
+ const text = m.slice(m.indexOf('[') + 1, m.indexOf(']'));
36
+ return text || '';
37
+ })
38
+ .split('\n')
39
+ .filter((line) => {
40
+ const t = line.trim();
41
+ return t && !t.startsWith('|') && !/^\|\s*:?-+/.test(t) && t !== '---';
42
+ })
43
+ .join('\n')
44
+ .replace(/\n{3,}/g, '\n\n')
45
+ .replace(/\s+/g, (s) => (s.includes('\n') ? s : ' '))
46
+ .trim()
47
+ .slice(0, 2000);
48
+ }
49
+ function toCard(m) {
50
+ const a = m.attributes;
51
+ const alt = a.altTitles?.[0] ? Object.values(a.altTitles[0])[0] : undefined;
52
+ return {
53
+ id: m.id,
54
+ provider: 'mangadex',
55
+ title: pickTitle(a.title),
56
+ altTitle: alt,
57
+ cover: coverFromRelationships(m.id, m.relationships || []),
58
+ status: a.status,
59
+ year: a.year ?? null,
60
+ genres: a.tags
61
+ ?.filter((t) => t.attributes?.group === 'genre')
62
+ .map((t) => t.attributes?.name?.en || '')
63
+ .filter(Boolean)
64
+ .slice(0, 6) || [],
65
+ contentRating: a.contentRating,
66
+ };
67
+ }
68
+ class MangaDexProvider {
69
+ name = 'mangadex';
70
+ cache;
71
+ constructor(cache) {
72
+ this.cache = cache;
73
+ }
74
+ async search(options) {
75
+ const { query = '', page = 1, limit = PER_PAGE, sort = 'popular', ratings = ['safe'] } = options;
76
+ const offset = (Math.max(1, page) - 1) * limit;
77
+ const params = new URLSearchParams({
78
+ limit: String(Math.min(limit, 50)),
79
+ offset: String(offset),
80
+ 'includes[]': 'cover_art',
81
+ hasAvailableChapters: 'true',
82
+ });
83
+ if (query.trim())
84
+ params.set('title', query.trim());
85
+ for (const r of ratings)
86
+ params.append('contentRating[]', r);
87
+ if (options.status)
88
+ params.append('status[]', options.status);
89
+ switch (sort) {
90
+ case 'latest':
91
+ params.set('order[latestUploadedChapter]', 'desc');
92
+ break;
93
+ case 'rating':
94
+ params.set('order[rating]', 'desc');
95
+ break;
96
+ case 'followed':
97
+ params.set('order[followedCount]', 'desc');
98
+ break;
99
+ default:
100
+ params.set('order[relevance]', 'desc');
101
+ }
102
+ const cacheKey = `manga-md-search-${params.toString()}`;
103
+ const cached = this.cache.get(cacheKey);
104
+ if (cached)
105
+ return cached;
106
+ try {
107
+ const data = await getJson(`${API}/manga?${params.toString()}`);
108
+ const items = (data.data || []).map(toCard);
109
+ const total = data.total ?? 0;
110
+ const result = { items, hasNext: offset + items.length < total, total };
111
+ this.cache.set(cacheKey, result, 300);
112
+ return result;
113
+ }
114
+ catch (err) {
115
+ logger_1.default.error({ err }, '[MangaDex] search failed');
116
+ return { items: [], hasNext: false };
117
+ }
118
+ }
119
+ async getDetail(id, ratings = ['safe']) {
120
+ const cacheKey = `manga-md-detail-${id}-${ratings.join(',')}`;
121
+ const cached = this.cache.get(cacheKey);
122
+ if (cached)
123
+ return cached;
124
+ try {
125
+ const data = await getJson(`${API}/manga/${id}?includes[]=cover_art&includes[]=author&includes[]=artist`);
126
+ const m = data.data;
127
+ if (!m)
128
+ return null;
129
+ const card = toCard(m);
130
+ const rels = m.relationships || [];
131
+ const author = rels.find((r) => r.type === 'author')?.attributes?.name;
132
+ const artist = rels.find((r) => r.type === 'artist')?.attributes?.name;
133
+ const chapters = await this.getChapters(id, ratings);
134
+ const detail = {
135
+ ...card,
136
+ description: cleanDescription(m.attributes.description?.en || ''),
137
+ author,
138
+ artist,
139
+ chapters,
140
+ };
141
+ this.cache.set(cacheKey, detail, 600);
142
+ return detail;
143
+ }
144
+ catch (err) {
145
+ logger_1.default.error({ err, id }, '[MangaDex] detail failed');
146
+ return null;
147
+ }
148
+ }
149
+ async getChapters(id, ratings = ['safe']) {
150
+ const cacheKey = `manga-md-chapters-${id}-${ratings.join(',')}`;
151
+ const cached = this.cache.get(cacheKey);
152
+ if (cached)
153
+ return cached;
154
+ try {
155
+ const params = new URLSearchParams({
156
+ limit: '100',
157
+ offset: '0',
158
+ 'translatedLanguage[]': 'en',
159
+ 'order[chapter]': 'asc',
160
+ 'includes[]': 'scanlation_group',
161
+ });
162
+ for (const r of ratings)
163
+ params.append('contentRating[]', r);
164
+ const all = [];
165
+ const fetchFeed = async (withLang) => {
166
+ const out = [];
167
+ for (let page = 0; page < 5; page++) {
168
+ const p = new URLSearchParams(params);
169
+ if (!withLang)
170
+ p.delete('translatedLanguage[]');
171
+ p.set('offset', String(page * 100));
172
+ const data = await getJson(`${API}/manga/${id}/feed?${p.toString()}`);
173
+ for (const c of data.data || []) {
174
+ const group = c.relationships?.find((r) => r.type === 'scanlation_group')?.attributes
175
+ ?.name;
176
+ out.push({
177
+ id: c.id,
178
+ provider: 'mangadex',
179
+ mangaId: id,
180
+ number: c.attributes.chapter || '?',
181
+ title: c.attributes.title || undefined,
182
+ volume: c.attributes.volume || undefined,
183
+ language: c.attributes.translatedLanguage,
184
+ pages: c.attributes.pages,
185
+ group,
186
+ externalUrl: c.attributes.externalUrl || undefined,
187
+ publishedAt: c.attributes.publishAt,
188
+ });
189
+ }
190
+ if ((data.data || []).length < 100)
191
+ break;
192
+ }
193
+ return out;
194
+ };
195
+ const enChapters = await fetchFeed(true);
196
+ for (const c of enChapters)
197
+ all.push(c);
198
+ if (all.length === 0) {
199
+ for (const c of await fetchFeed(false))
200
+ all.push(c);
201
+ }
202
+ this.cache.set(cacheKey, all, 600);
203
+ return all;
204
+ }
205
+ catch (err) {
206
+ logger_1.default.error({ err, id }, '[MangaDex] chapters failed');
207
+ return [];
208
+ }
209
+ }
210
+ async getPages(chapterId) {
211
+ const cacheKey = `manga-md-pages-${chapterId}`;
212
+ const cached = this.cache.get(cacheKey);
213
+ if (cached)
214
+ return cached;
215
+ try {
216
+ const data = await getJson(`${API}/at-home/server/${chapterId}`);
217
+ const pages = (data.chapter?.data || []).map((f) => `${data.baseUrl}/data/${data.chapter.hash}/${f}`);
218
+ this.cache.set(cacheKey, pages, 300);
219
+ return pages;
220
+ }
221
+ catch (err) {
222
+ logger_1.default.error({ err, chapterId }, '[MangaDex] pages failed');
223
+ return [];
224
+ }
225
+ }
226
+ }
227
+ exports.MangaDexProvider = MangaDexProvider;