@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,785 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.DataController = void 0;
40
+ const title_matching_1 = require("../providers/title-matching");
41
+ const anilist_1 = require("../lib/anilist");
42
+ const mal_1 = require("../lib/mal");
43
+ const mal_cache_repository_1 = require("../repositories/mal-cache.repository");
44
+ const migration_1 = require("../lib/migration");
45
+ const temp_ids_1 = require("../lib/temp-ids");
46
+ const temp_show_ids_repository_1 = require("../repositories/temp-show-ids.repository");
47
+ const shows_meta_repository_1 = require("../repositories/shows-meta.repository");
48
+ const watchlist_repository_1 = require("../repositories/watchlist.repository");
49
+ const db_utils_1 = require("../utils/db-utils");
50
+ const logger_1 = __importDefault(require("../logger"));
51
+ class DataController {
52
+ providers;
53
+ constructor(providers) {
54
+ this.providers = providers;
55
+ }
56
+ setDataCache(res, maxAgeSeconds, body) {
57
+ const isEmpty = Array.isArray(body)
58
+ ? body.length === 0
59
+ : body == null || (typeof body === 'object' && Object.keys(body).length === 0);
60
+ res.set('Cache-Control', isEmpty ? 'no-store' : `public, max-age=${maxAgeSeconds}`);
61
+ }
62
+ getProvider(req) {
63
+ const providerName = req.query.provider?.toLowerCase();
64
+ if (!providerName)
65
+ return null;
66
+ return this.providers[providerName] || null;
67
+ }
68
+ async resolveMatureStreamingId(title, wanted) {
69
+ const names = [wanted, 'wh', 'op'].filter((p) => !!p && p !== 'mal' && !!this.providers[p]);
70
+ const seen = new Set();
71
+ for (const name of names) {
72
+ if (seen.has(name))
73
+ continue;
74
+ seen.add(name);
75
+ try {
76
+ const resolved = await this.providers[name].resolveShowId?.(title);
77
+ if (resolved)
78
+ return { provider: this.providers[name], nativeId: resolved };
79
+ }
80
+ catch {
81
+ // ignore
82
+ }
83
+ }
84
+ return null;
85
+ }
86
+ getTrending = async (_req, res) => {
87
+ try {
88
+ const data = await (0, anilist_1.getTrending)(1, 20, 'TRENDING_DESC', 'RELEASING');
89
+ this.setDataCache(res, 300, data);
90
+ res.json(data);
91
+ }
92
+ catch (e) {
93
+ logger_1.default.error({ err: e }, 'Trending fetch failed');
94
+ res.set('Cache-Control', 'no-store').json([]);
95
+ }
96
+ };
97
+ getSpotlight = async (_req, res) => {
98
+ try {
99
+ const data = await (0, anilist_1.getSpotlightBanners)(1, 20);
100
+ this.setDataCache(res, 300, data);
101
+ res.json(data);
102
+ }
103
+ catch (e) {
104
+ logger_1.default.error({ err: e }, 'Spotlight fetch failed');
105
+ res.set('Cache-Control', 'no-store').json([]);
106
+ }
107
+ };
108
+ getPopularList = async (req, res) => {
109
+ const sort = req.query.sort === 'POPULARITY_DESC' ? 'POPULARITY_DESC' : 'TRENDING_DESC';
110
+ const page = parseInt(req.query.page) || 1;
111
+ const size = parseInt(req.query.size) || 20;
112
+ try {
113
+ const data = await (0, anilist_1.getTrending)(page, size, sort);
114
+ this.setDataCache(res, 300, data);
115
+ res.json(data);
116
+ }
117
+ catch (e) {
118
+ logger_1.default.error({ err: e }, 'Popular list fetch failed');
119
+ res.set('Cache-Control', 'no-store').json([]);
120
+ }
121
+ };
122
+ getSchedule = async (req, res) => {
123
+ try {
124
+ const date = new Date(req.params.date + 'T00:00:00.000Z');
125
+ const format = req.query.format || undefined;
126
+ const adult = format === 'ADULT';
127
+ const data = await (0, anilist_1.getSchedule)(date, adult ? undefined : format, adult);
128
+ this.setDataCache(res, 300, data);
129
+ res.json(data);
130
+ }
131
+ catch (e) {
132
+ logger_1.default.error({ err: e, date: req.params.date }, 'Schedule fetch failed');
133
+ res.set('Cache-Control', 'no-store').json([]);
134
+ }
135
+ };
136
+ getSkipTimes = async (req, res) => {
137
+ try {
138
+ const showId = req.params.showId;
139
+ const episodeNumber = req.params.episodeNumber;
140
+ if (/^\d+$/.test(showId)) {
141
+ const skipRes = await fetch(`https://api.aniskip.com/v1/skip-times/${showId}/${episodeNumber}?types=op&types=ed`);
142
+ if (skipRes.ok) {
143
+ const data = await skipRes.json();
144
+ return res.json(data);
145
+ }
146
+ }
147
+ res.json({ found: false, results: [] });
148
+ }
149
+ catch {
150
+ res.json({ found: false, results: [] });
151
+ }
152
+ };
153
+ getVideo = async (req, res) => {
154
+ try {
155
+ let showId = req.query.showId;
156
+ const videoMalId = (0, anilist_1.parseMalId)(showId);
157
+ if (videoMalId) {
158
+ try {
159
+ const malMeta = await (0, anilist_1.getShowMetaByMalId)(videoMalId);
160
+ if (malMeta?.anilistId && malMeta.anilistId > 0)
161
+ showId = String(malMeta.anilistId);
162
+ }
163
+ catch {
164
+ // ignore
165
+ }
166
+ }
167
+ if ((0, temp_ids_1.isTempShowId)(showId)) {
168
+ try {
169
+ const row = temp_show_ids_repository_1.TempShowIdsRepository.getById(req.db, showId);
170
+ if (!row || !(0, temp_ids_1.isTempMatureProvider)(row.provider))
171
+ return res.json([]);
172
+ const wanted = String(req.query.provider || '').toLowerCase();
173
+ const own = this.providers[row.provider];
174
+ if (own && (!wanted || wanted === row.provider)) {
175
+ const urls = await own.getStreamUrls(row.nativeId, req.query.episodeNumber, req.query.mode);
176
+ return res.json(urls || []);
177
+ }
178
+ const hit = await this.resolveMatureStreamingId(row.title, wanted);
179
+ if (hit) {
180
+ const urls = await hit.provider.getStreamUrls(hit.nativeId, req.query.episodeNumber, req.query.mode);
181
+ return res.json(urls || []);
182
+ }
183
+ }
184
+ catch (e) {
185
+ logger_1.default.error({ err: e, showId }, 'Temp show video fetch failed');
186
+ }
187
+ return res.json([]);
188
+ }
189
+ const providerName = req.query.provider;
190
+ const providerKey = providerName?.toLowerCase();
191
+ const stillMal = (0, anilist_1.parseMalId)(showId) !== null;
192
+ const needsResolution = providerKey !== 'megaplay' && (/^\d+$/.test(showId) || stillMal);
193
+ if (providerKey && needsResolution) {
194
+ const meta = (await shows_meta_repository_1.ShowsMetaRepository.getById(req.db, showId));
195
+ let targetTitle = meta?.englishName || meta?.name;
196
+ let anilistShow = null;
197
+ if (!targetTitle) {
198
+ try {
199
+ anilistShow = await (0, anilist_1.getShowMetaById)(showId);
200
+ targetTitle = anilistShow?.englishName || anilistShow?.name;
201
+ if (anilistShow && targetTitle) {
202
+ await shows_meta_repository_1.ShowsMetaRepository.upsert(req.db, {
203
+ id: showId,
204
+ name: anilistShow.name,
205
+ thumbnail: anilistShow.thumbnail,
206
+ nativeName: anilistShow.nativeName,
207
+ englishName: anilistShow.englishName,
208
+ genres: anilistShow.genres
209
+ ? JSON.stringify(anilistShow.genres.map((genre) => genre.name))
210
+ : undefined,
211
+ status: anilistShow.status,
212
+ episodeCount: anilistShow.episodeCount != null ? Number(anilistShow.episodeCount) : undefined,
213
+ type: anilistShow.type,
214
+ anilistId: anilistShow.anilistId,
215
+ });
216
+ }
217
+ }
218
+ catch (err) {
219
+ logger_1.default.warn({ err, provider: providerKey, showId }, '[Video] AniList metadata lookup failed while resolving numeric showId');
220
+ }
221
+ }
222
+ if (targetTitle) {
223
+ let romaji = anilistShow?.names?.romaji;
224
+ let nativeName = anilistShow?.names?.native;
225
+ let synonyms = anilistShow?.names?.synonyms;
226
+ if (!romaji || !nativeName) {
227
+ try {
228
+ const show = await (0, anilist_1.getShowMetaById)(showId);
229
+ romaji = romaji || show?.names?.romaji;
230
+ nativeName = nativeName || show?.names?.native;
231
+ synonyms = synonyms || show?.names?.synonyms;
232
+ }
233
+ catch {
234
+ // ignore
235
+ }
236
+ }
237
+ const titleTargets = [
238
+ targetTitle,
239
+ romaji,
240
+ nativeName,
241
+ ...(synonyms ?? []).slice(0, 2),
242
+ ].filter((t) => !!t && t.trim().length > 0);
243
+ const distinctTargets = [...new Set(titleTargets.map((t) => t.trim()))].filter((t, i, a) => a.findIndex((x) => x.toLowerCase() === t.toLowerCase()) === i);
244
+ let resolved = null;
245
+ for (const variant of distinctTargets) {
246
+ try {
247
+ resolved =
248
+ (await this.providers[providerKey]?.resolveShowId?.(variant, romaji, req.query.mode)) ?? null;
249
+ }
250
+ catch {
251
+ resolved = null;
252
+ }
253
+ if (resolved)
254
+ break;
255
+ }
256
+ if (resolved) {
257
+ showId = resolved;
258
+ }
259
+ else {
260
+ logger_1.default.warn({ provider: providerKey, showId, title: targetTitle, romaji }, '[Video] resolveShowId failed, attempting fallback provider search');
261
+ try {
262
+ const fallbackResults = await this.providers[providerKey]?.search?.({
263
+ query: targetTitle,
264
+ });
265
+ const fallbackMatch = (0, title_matching_1.pickBestMatch)((fallbackResults || []).map((r) => ({
266
+ title: r.name || r.englishName || '',
267
+ id: r.id || r._id || '',
268
+ })), distinctTargets);
269
+ if (fallbackMatch) {
270
+ showId = fallbackMatch.item.id;
271
+ }
272
+ else {
273
+ logger_1.default.warn({ provider: providerKey, showId, title: targetTitle }, '[Video] fallback provider search returned no results');
274
+ return res.json([]);
275
+ }
276
+ }
277
+ catch (fallbackErr) {
278
+ if (fallbackErr.message === 'AUTH_REQUIRED') {
279
+ throw fallbackErr;
280
+ }
281
+ logger_1.default.error({ err: fallbackErr, provider: providerKey, showId, title: targetTitle }, '[Video] fallback provider search failed');
282
+ return res.json([]);
283
+ }
284
+ }
285
+ }
286
+ else {
287
+ logger_1.default.warn({ provider: providerKey, showId }, '[Video] numeric showId passed to provider but local meta missing title');
288
+ return res.json([]);
289
+ }
290
+ }
291
+ const provider = this.getProvider(req);
292
+ if (!provider)
293
+ return res.json([]);
294
+ const urls = await provider.getStreamUrls(showId, req.query.episodeNumber, req.query.mode);
295
+ res.json(urls || []);
296
+ }
297
+ catch (e) {
298
+ if (e.message === 'AUTH_REQUIRED') {
299
+ return res.status(403).json({ error: 'AUTH_REQUIRED', provider: 'animepahe' });
300
+ }
301
+ logger_1.default.error({ err: e, provider: req.query.provider }, 'Provider video fetch failed');
302
+ res.json([]);
303
+ }
304
+ };
305
+ getEpisodes = async (req, res) => {
306
+ const showIdRaw = req.query.showId;
307
+ if (!showIdRaw) {
308
+ return res.json({ episodes: [] });
309
+ }
310
+ let showId = await (0, migration_1.getMigratedId)(req.db, showIdRaw);
311
+ const epsMalId = (0, anilist_1.parseMalId)(showId);
312
+ if (epsMalId) {
313
+ try {
314
+ const malMeta = await (0, anilist_1.getShowMetaByMalId)(epsMalId);
315
+ if (malMeta?.anilistId && malMeta.anilistId > 0)
316
+ showId = String(malMeta.anilistId);
317
+ }
318
+ catch {
319
+ // ignore
320
+ }
321
+ }
322
+ if ((0, temp_ids_1.isTempShowId)(showId)) {
323
+ try {
324
+ const row = temp_show_ids_repository_1.TempShowIdsRepository.getById(req.db, showId);
325
+ if (!row || !(0, temp_ids_1.isTempMatureProvider)(row.provider))
326
+ return res.json({ episodes: [] });
327
+ const wanted = String(req.query.provider || '').toLowerCase();
328
+ const own = this.providers[row.provider];
329
+ if (own && (!wanted || wanted === row.provider)) {
330
+ const data = await own.getEpisodes(row.nativeId, req.query.mode);
331
+ if (data?.episodes?.length)
332
+ return res.json(data);
333
+ return res.json({ episodes: [] });
334
+ }
335
+ const hit = await this.resolveMatureStreamingId(row.title, wanted);
336
+ if (hit) {
337
+ const data = await hit.provider.getEpisodes(hit.nativeId, req.query.mode);
338
+ if (data?.episodes?.length)
339
+ return res.json(data);
340
+ }
341
+ }
342
+ catch {
343
+ // ignore
344
+ }
345
+ return res.json({ episodes: [] });
346
+ }
347
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(showId)) {
348
+ try {
349
+ if (this.providers['animepahe']) {
350
+ const data = await this.providers['animepahe'].getEpisodes(showId, req.query.mode);
351
+ return res.json(data || { episodes: [] });
352
+ }
353
+ }
354
+ catch {
355
+ return res.json({ episodes: [] });
356
+ }
357
+ }
358
+ const isNumeric = /^(mal-\d+|-?\d+)$/i.test(showId);
359
+ if (isNumeric) {
360
+ let episodes = [];
361
+ try {
362
+ episodes = await (0, anilist_1.getAnilistEpisodes)(showId);
363
+ }
364
+ catch (e) {
365
+ logger_1.default.error({ err: e, showId }, 'Episodes fetch failed');
366
+ }
367
+ if (episodes.length === 0) {
368
+ const absId = Math.abs(parseInt(showId, 10));
369
+ if (!Number.isFinite(absId)) {
370
+ res.set('Cache-Control', 'public, max-age=3600').json({ episodes });
371
+ return;
372
+ }
373
+ try {
374
+ const d = await (0, mal_1.malAnimeDetail)((0, mal_cache_repository_1.malCacheStore)(), absId);
375
+ const total = d.detail?.episodes;
376
+ if (total && total > 0 && total <= 500) {
377
+ episodes = Array.from({ length: total }, (_, i) => (i + 1).toString());
378
+ }
379
+ }
380
+ catch {
381
+ // ignore
382
+ }
383
+ }
384
+ res.set('Cache-Control', 'public, max-age=3600').json({ episodes });
385
+ return;
386
+ }
387
+ const providerName = req.query.provider?.toLowerCase();
388
+ const provider = providerName ? this.providers[providerName] : undefined;
389
+ if (provider) {
390
+ try {
391
+ const data = await provider.getEpisodes(showId, req.query.mode);
392
+ if (data?.episodes?.length) {
393
+ return res.json(data);
394
+ }
395
+ }
396
+ catch {
397
+ // ignore
398
+ }
399
+ }
400
+ res.json({ episodes: [] });
401
+ };
402
+ search = async (req, res) => {
403
+ try {
404
+ const query = req.query.query || '';
405
+ const page = parseInt(req.query.page) || 1;
406
+ const perPage = parseInt(req.query.limit) || 14;
407
+ const sort = req.query.sortBy || undefined;
408
+ const result = await (0, anilist_1.searchAnilist)({
409
+ query,
410
+ page,
411
+ perPage,
412
+ format: req.query.type,
413
+ status: req.query.status,
414
+ season: req.query.season,
415
+ seasonYear: req.query.year ? parseInt(req.query.year) : undefined,
416
+ countryOfOrigin: req.query.country,
417
+ genre: req.query.genres,
418
+ genre_not_in: req.query.excludeGenres
419
+ ? req.query.excludeGenres.split(',')
420
+ : undefined,
421
+ tag_not_in: req.query.excludeTags
422
+ ? req.query.excludeTags.split(',')
423
+ : undefined,
424
+ averageScore_greater: req.query.minScore
425
+ ? parseInt(req.query.minScore)
426
+ : undefined,
427
+ episodes_greater: req.query.minEpisodes
428
+ ? parseInt(req.query.minEpisodes)
429
+ : undefined,
430
+ isAdult: req.query.adult === 'true' ? true : req.query.adult === 'false' ? false : undefined,
431
+ sort,
432
+ });
433
+ return res.json(result);
434
+ }
435
+ catch (e) {
436
+ logger_1.default.error({ err: e }, 'search failed');
437
+ res.json([]);
438
+ }
439
+ };
440
+ matureSearch = async (req, res) => {
441
+ try {
442
+ const provider = (req.query.provider || 'anilist').toLowerCase();
443
+ const query = req.query.query || '';
444
+ const page = parseInt(req.query.page) || 1;
445
+ const limit = parseInt(req.query.limit) || 14;
446
+ if (provider === 'anilist') {
447
+ const result = await (0, anilist_1.searchAnilist)({
448
+ query,
449
+ page,
450
+ perPage: limit,
451
+ format: req.query.type,
452
+ status: req.query.status,
453
+ season: req.query.season,
454
+ seasonYear: req.query.year ? parseInt(req.query.year) : undefined,
455
+ countryOfOrigin: req.query.country,
456
+ genre: req.query.genres,
457
+ genre_not_in: req.query.excludeGenres
458
+ ? req.query.excludeGenres.split(',')
459
+ : undefined,
460
+ isAdult: true,
461
+ sort: req.query.sortBy || undefined,
462
+ });
463
+ return res.json({ data: result, hasMore: result.length >= limit });
464
+ }
465
+ if (provider === 'wh') {
466
+ const p = this.providers['wh'];
467
+ const result = await p.browse({
468
+ query,
469
+ page,
470
+ genre: req.query.genre || undefined,
471
+ });
472
+ return res.json({ data: result.shows, hasMore: result.hasMore });
473
+ }
474
+ if (provider === 'op') {
475
+ const p = this.providers['op'];
476
+ const result = await p.browse({
477
+ query,
478
+ page,
479
+ limit,
480
+ order: req.query.order || undefined,
481
+ genres: req.query.genres || undefined,
482
+ blacklist: req.query.blacklist || undefined,
483
+ studio: req.query.studio || undefined,
484
+ });
485
+ return res.json({ data: result.shows, hasMore: result.hasMore, total: result.total });
486
+ }
487
+ if (provider === 'ht') {
488
+ const p = this.providers['ht'];
489
+ const result = await p.browse({
490
+ query,
491
+ limit: 40,
492
+ genre: req.query.genre || undefined,
493
+ page,
494
+ });
495
+ return res.json({ data: result.shows, hasMore: result.hasMore });
496
+ }
497
+ if (provider === 'hn') {
498
+ const p = this.providers['hn'];
499
+ if (!p?.browse) {
500
+ const shows = await this.providers['hn'].search({ query });
501
+ return res.json({
502
+ data: shows.map((s) => ({ ...s, isAdult: true })),
503
+ hasMore: false,
504
+ });
505
+ }
506
+ const result = await p.browse({
507
+ query,
508
+ page,
509
+ pageSize: limit,
510
+ sort: req.query.sortBy || undefined,
511
+ genre: req.query.genre || undefined,
512
+ });
513
+ return res.json({ data: result.shows, hasMore: result.hasMore, genres: result.genres });
514
+ }
515
+ if (provider === 'mal') {
516
+ const malPage = page;
517
+ const malLimit = 50;
518
+ let result = [];
519
+ try {
520
+ result = await (0, mal_1.malSearchMedia)((0, mal_cache_repository_1.malCacheStore)(), {
521
+ query,
522
+ page: malPage,
523
+ perPage: malLimit,
524
+ format: req.query.type,
525
+ status: req.query.status,
526
+ genre: undefined,
527
+ genre_not_in: req.query.excludeGenres
528
+ ? req.query.excludeGenres.split(',')
529
+ : undefined,
530
+ isAdult: true,
531
+ sort: req.query.sortBy || undefined,
532
+ averageScore_greater: req.query.minScore
533
+ ? parseInt(req.query.minScore)
534
+ : undefined,
535
+ episodes_greater: req.query.minEpisodes
536
+ ? parseInt(req.query.minEpisodes)
537
+ : undefined,
538
+ });
539
+ }
540
+ catch (e) {
541
+ logger_1.default.error({ err: e }, 'mal mature search failed');
542
+ }
543
+ const shows = result.map((m) => {
544
+ const show = (0, anilist_1.fromAnilistMedia)(m);
545
+ return { ...show, isAdult: true };
546
+ });
547
+ const sliced = shows.slice(0, limit);
548
+ return res.json({ data: sliced, hasMore: shows.length >= limit });
549
+ }
550
+ return res.status(400).json({ error: 'Unknown mature provider' });
551
+ }
552
+ catch (e) {
553
+ logger_1.default.error({ err: e }, 'mature search failed');
554
+ res.json({ data: [], hasMore: false });
555
+ }
556
+ };
557
+ resolveMature = async (req, res) => {
558
+ try {
559
+ const title = (req.query.title || '').trim();
560
+ if (!title)
561
+ return res.status(400).json({ error: 'title is required' });
562
+ const anilistResult = await (0, anilist_1.searchAnilistByTitle)(title);
563
+ if (anilistResult) {
564
+ const id = anilistResult.id;
565
+ return res.json({ id: typeof id === 'number' && id < 0 ? `mal-${Math.abs(id)}` : id });
566
+ }
567
+ const malResult = await (0, mal_1.malSearchTitle)((0, mal_cache_repository_1.malCacheStore)(), title);
568
+ if (malResult)
569
+ return res.json({ id: malResult, provider: 'mal' });
570
+ return res.status(404).json({ error: 'No match found' });
571
+ }
572
+ catch (e) {
573
+ logger_1.default.error({ err: e }, 'mature resolve failed');
574
+ res.status(500).json({ error: 'Resolve failed' });
575
+ }
576
+ };
577
+ getMatureFilters = async (_req, res) => {
578
+ try {
579
+ const { WH_GENRES } = await Promise.resolve().then(() => __importStar(require('../providers/wh.provider')));
580
+ const { OP_TAGS, OP_ORDERS } = await Promise.resolve().then(() => __importStar(require('../providers/op.provider')));
581
+ const { HT_GENRES } = await Promise.resolve().then(() => __importStar(require('../providers/ht.provider')));
582
+ return res.json({
583
+ whGenres: WH_GENRES,
584
+ opTags: OP_TAGS,
585
+ opOrders: OP_ORDERS,
586
+ htGenres: HT_GENRES,
587
+ });
588
+ }
589
+ catch (e) {
590
+ logger_1.default.error({ err: e }, 'mature filters failed');
591
+ res.status(500).json({ error: 'Failed to load filters' });
592
+ }
593
+ };
594
+ getSeasonal = async (req, res) => {
595
+ const page = parseInt(req.query.page) || 1;
596
+ const size = parseInt(req.query.size) || 14;
597
+ const format = req.query.format;
598
+ try {
599
+ const data = await (0, anilist_1.getSeasonal)(page, size, format);
600
+ this.setDataCache(res, 300, data);
601
+ res.json(data);
602
+ }
603
+ catch (e) {
604
+ logger_1.default.error({ err: e }, 'Seasonal fetch failed');
605
+ res.set('Cache-Control', 'no-store').json([]);
606
+ }
607
+ };
608
+ getLatestReleases = async (req, res) => {
609
+ const format = req.query.format || 'TV';
610
+ const page = parseInt(req.query.page) || 1;
611
+ const size = parseInt(req.query.size) || 12;
612
+ try {
613
+ const data = await (0, anilist_1.getLatestReleases)(format, page, size);
614
+ this.setDataCache(res, 300, data);
615
+ res.json(data);
616
+ }
617
+ catch (e) {
618
+ logger_1.default.error({ err: e }, 'Latest releases fetch failed');
619
+ res.set('Cache-Control', 'no-store').json([]);
620
+ }
621
+ };
622
+ getShowMeta = async (req, res) => {
623
+ const showIdRaw = req.params.id;
624
+ const id = await (0, migration_1.getMigratedId)(req.db, showIdRaw);
625
+ if ((0, temp_ids_1.isTempShowId)(id)) {
626
+ const row = temp_show_ids_repository_1.TempShowIdsRepository.getById(req.db, id);
627
+ if (!row || !(0, temp_ids_1.isTempMatureProvider)(row.provider)) {
628
+ res.json({});
629
+ return;
630
+ }
631
+ res.set('Cache-Control', 'public, max-age=300').json({
632
+ _id: row.id,
633
+ id: row.id,
634
+ name: row.title,
635
+ englishName: row.title,
636
+ thumbnail: row.thumbnail || '',
637
+ type: 'TV',
638
+ isAdult: true,
639
+ status: 'UNKNOWN',
640
+ provider: row.provider,
641
+ nativeId: row.nativeId,
642
+ });
643
+ return;
644
+ }
645
+ const isNumeric = /^(mal-\d+|-?\d+)$/i.test(id);
646
+ if (isNumeric) {
647
+ let meta = null;
648
+ try {
649
+ meta = await (0, anilist_1.getShowMetaById)(id);
650
+ }
651
+ catch (e) {
652
+ logger_1.default.warn({ err: e, id }, 'AniList show-meta fetch failed, trying local cache');
653
+ const localMeta = (await shows_meta_repository_1.ShowsMetaRepository.getById(req.db, id));
654
+ if (localMeta) {
655
+ if (typeof localMeta.genres === 'string') {
656
+ try {
657
+ localMeta.genres = JSON.parse(localMeta.genres);
658
+ }
659
+ catch {
660
+ localMeta.genres = [];
661
+ }
662
+ }
663
+ res.set('Cache-Control', 'public, max-age=3600').json(localMeta);
664
+ return;
665
+ }
666
+ }
667
+ if (meta) {
668
+ const poster = meta.thumbnail?.trim() ? meta.thumbnail : undefined;
669
+ const hasPoster = !!poster;
670
+ try {
671
+ shows_meta_repository_1.ShowsMetaRepository.upsert(req.db, {
672
+ id,
673
+ name: meta.name,
674
+ thumbnail: poster,
675
+ nativeName: meta.nativeName,
676
+ englishName: meta.englishName,
677
+ genres: meta.genres
678
+ ? JSON.stringify(meta.genres.map((g) => (typeof g === 'string' ? g : g?.name)).filter(Boolean))
679
+ : undefined,
680
+ status: meta.status,
681
+ episodeCount: meta.episodeCount != null ? Number(meta.episodeCount) : undefined,
682
+ type: meta.type,
683
+ anilistId: meta.anilistId,
684
+ });
685
+ }
686
+ catch {
687
+ // ignore
688
+ }
689
+ if (hasPoster && poster) {
690
+ const existingWatchlist = (await watchlist_repository_1.WatchlistRepository.getById(req.db, id));
691
+ if (existingWatchlist && existingWatchlist.thumbnail !== poster) {
692
+ try {
693
+ await watchlist_repository_1.WatchlistRepository.updateThumbnail(req.db, id, poster);
694
+ }
695
+ catch {
696
+ // ignore
697
+ }
698
+ }
699
+ req.db.scheduleSave();
700
+ }
701
+ }
702
+ if (/^(mal-\d+|-\d+)$/i.test(id) && meta?.anilistId && meta.anilistId > 0) {
703
+ try {
704
+ (0, db_utils_1.dbRun)(req.db, 'INSERT OR REPLACE INTO legacy_id_mapping (legacyId, numericId) VALUES (?, ?)', [id, String(meta.anilistId)]);
705
+ }
706
+ catch {
707
+ // ignore
708
+ }
709
+ }
710
+ this.setDataCache(res, 3600, meta || {});
711
+ res.json(meta || {});
712
+ return;
713
+ }
714
+ res.json({});
715
+ };
716
+ allocateTempShow = async (req, res) => {
717
+ try {
718
+ const provider = String(req.body?.provider || '').toLowerCase();
719
+ const nativeId = String(req.body?.nativeId || '').trim();
720
+ const title = String(req.body?.title || '').trim();
721
+ const thumbnail = String(req.body?.thumbnail || '');
722
+ if (!provider || !this.providers[provider] || !(0, temp_ids_1.isTempMatureProvider)(provider)) {
723
+ return res.status(400).json({ error: 'Unknown provider' });
724
+ }
725
+ if (!nativeId || !title) {
726
+ return res.status(400).json({ error: 'nativeId and title are required' });
727
+ }
728
+ const row = temp_show_ids_repository_1.TempShowIdsRepository.allocate(req.db, {
729
+ provider,
730
+ nativeId,
731
+ title,
732
+ thumbnail,
733
+ });
734
+ return res.json({ id: row.id });
735
+ }
736
+ catch (e) {
737
+ logger_1.default.error({ err: e }, 'temp show allocate failed');
738
+ res.status(500).json({ error: 'Allocate failed' });
739
+ }
740
+ };
741
+ getAnilistStatus = async (_req, res) => {
742
+ const available = !(0, anilist_1.anilistUnavailable)();
743
+ if (!available) {
744
+ (0, anilist_1.checkAnilistStatus)().catch(() => { });
745
+ }
746
+ res.json({ available, wasDownAtBoot: (0, anilist_1.wasAnilistDownAtBoot)() });
747
+ };
748
+ getGenresAndTags = async (_req, res) => {
749
+ try {
750
+ res.json(await (0, anilist_1.getGenreTagLists)());
751
+ }
752
+ catch (e) {
753
+ logger_1.default.error({ err: e }, 'genres-and-tags failed');
754
+ res.status(500).json({ error: 'Failed to load genres' });
755
+ }
756
+ };
757
+ getSystemNotifications = async (_req, res) => {
758
+ const notifications = [];
759
+ if ((0, anilist_1.wasAnilistDownAtBoot)() && (0, anilist_1.anilistUnavailable)()) {
760
+ const fb = 'MAL + Kitsu';
761
+ notifications.push({
762
+ id: 'system-anilist-down',
763
+ type: 'system',
764
+ title: 'AniList API',
765
+ message: `AniList metadata API is currently down. dango is experiencing degraded performance. ${fb} is being used as a fallback in the meantime. Some features may be limited until service is restored.`,
766
+ icon: 'warning',
767
+ createdAt: Date.now(),
768
+ });
769
+ }
770
+ res.json(notifications);
771
+ };
772
+ getBatchedHome = async (req, res) => {
773
+ try {
774
+ const format = req.query.format || undefined;
775
+ const data = await (0, anilist_1.getBatchedHomeData)(format);
776
+ const isEmpty = data.trending.length === 0 && data.seasonal.length === 0 && data.spotlight.length === 0;
777
+ res.set('Cache-Control', isEmpty ? 'no-store' : 'public, max-age=300').json(data);
778
+ }
779
+ catch (e) {
780
+ logger_1.default.error({ err: e }, 'Batched home fetch failed');
781
+ res.set('Cache-Control', 'no-store').json({ trending: [], seasonal: [], spotlight: [] });
782
+ }
783
+ };
784
+ }
785
+ exports.DataController = DataController;