@serifpersia/dango 3.3.9 → 3.4.1

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 (180) hide show
  1. package/README.md +1 -1
  2. package/client/dist/assets/{AnimeCard-URCiiy5W.js → AnimeCard-B0oMgXBm.js} +1 -1
  3. package/client/dist/assets/{AnimeInfo-e3z3er9t.js → AnimeInfo-o0Udf0og.js} +1 -1
  4. package/client/dist/assets/{AnimeInfoPage-B7Djlmy_.js → AnimeInfoPage-CojMIEuf.js} +2 -2
  5. package/client/dist/assets/Asmr-2V3yCCiK.js +2 -0
  6. package/client/dist/assets/AsmrResetProgressModal-C9R7HlNa.js +1 -0
  7. package/client/dist/assets/AvSyncCalibrator-Bo3820j3.js +10 -0
  8. package/client/dist/assets/Home-C8uXTzIM.js +1 -0
  9. package/client/dist/assets/Insights-CRqBfZuK.js +1 -0
  10. package/client/dist/assets/{LibraryListPage-B0qWqhb_.js → LibraryListPage-B_r51cRi.js} +1 -1
  11. package/client/dist/assets/{ListeningList-CSqD8j1C.js → ListeningList-BWnwvJTt.js} +1 -1
  12. package/client/dist/assets/{Manga-Cg2YuObF.js → Manga-CI5cZg3h.js} +1 -1
  13. package/client/dist/assets/{MangaInfoPage-DcArhEsy.js → MangaInfoPage-BtkOrO34.js} +1 -1
  14. package/client/dist/assets/MangaReadPage-Uv72AYdY.js +1 -0
  15. package/client/dist/assets/{MangaResetProgressModal-D_yT3d1v.js → MangaResetProgressModal-CPMEqZyi.js} +1 -1
  16. package/client/dist/assets/Mature-DXHWvoY5.js +1 -0
  17. package/client/dist/assets/MediaCard-DzA336CA.js +1 -0
  18. package/client/dist/assets/Music-C4dM0BQx.js +1 -0
  19. package/client/dist/assets/OptionListMenu-CpwxAC3J.js +1 -0
  20. package/client/dist/assets/Player-CpMvWTkH.js +56 -0
  21. package/client/dist/assets/PlayerSettings-pMU85k7p.js +1 -0
  22. package/client/dist/assets/QueueOptionsButton-1M7MdKzM.js +1 -0
  23. package/client/dist/assets/{QueueRail-B7f1-e6i.js → QueueRail-Bpla2xD8.js} +1 -1
  24. package/client/dist/assets/Radio-DlmTC-qG.js +1 -0
  25. package/client/dist/assets/{ReadingList-BaIem8H1.js → ReadingList-wJMDUybm.js} +1 -1
  26. package/client/dist/assets/{ResetProgressModal-DES5Y-Fl.js → ResetProgressModal-CByRM1Fh.js} +1 -1
  27. package/client/dist/assets/Search-B94D0tRM.js +1 -0
  28. package/client/dist/assets/Settings-CcU3oNY8.js +21 -0
  29. package/client/dist/assets/{SkeletonGrid-BxfgjP4i.js → SkeletonGrid-BJyZkg_K.js} +1 -1
  30. package/client/dist/assets/Trackers-vz-gqxtm.js +2 -0
  31. package/client/dist/assets/Tv-Rji_lvQo.js +4 -0
  32. package/client/dist/assets/Tv-ZEA45gXK.css +1 -0
  33. package/client/dist/assets/TvInfo-Ciujd6os.js +1 -0
  34. package/client/dist/assets/TvPopup-CqlVRF-Y.js +1 -0
  35. package/client/dist/assets/{TvSearch-BWDwWnHe.js → TvSearch-BcKXQue9.js} +1 -1
  36. package/client/dist/assets/{TvWatchlist-D3n5_4MI.js → TvWatchlist-sjsiup2h.js} +1 -1
  37. package/client/dist/assets/Watchlist-DAMdvt7Z.js +1 -0
  38. package/client/dist/assets/{hls-BO_L1WoN.js → hls-Do_n10_Y.js} +1 -1
  39. package/client/dist/assets/index-B5SLDCAi.js +178 -0
  40. package/client/dist/assets/useAnimeInfoData-DU9X90no.js +1 -0
  41. package/client/dist/assets/useAsmr-D6cSjbvt.js +1 -0
  42. package/client/dist/assets/{useAsmrPopup-D_o9-QlR.js → useAsmrPopup-DK1Vx-HV.js} +1 -1
  43. package/client/dist/assets/useMangaLibrary-DcxPn0RM.js +1 -0
  44. package/client/dist/assets/{useMangaPopup-TFzkC-tG.js → useMangaPopup-BLwvRZn0.js} +1 -1
  45. package/client/dist/assets/useProviders-D3L1Chqc.js +1 -0
  46. package/client/dist/assets/useTv-B546u-Rx.js +1 -0
  47. package/client/dist/assets/useTvLibrary-C2rPeCVT.js +1 -0
  48. package/client/dist/assets/{vendor-query-BXRECSI6.js → vendor-query-BUTye89G.js} +1 -1
  49. package/client/dist/index.html +2 -2
  50. package/orchestrator.js +1 -2
  51. package/package.json +6 -6
  52. package/server/dist/app-auth.js +9 -29
  53. package/server/dist/app-hono.js +621 -0
  54. package/server/dist/config.js +0 -1
  55. package/server/dist/db/drizzle.js +32 -0
  56. package/server/dist/db/schema-anime.js +64 -0
  57. package/server/dist/db/schema-asmr.js +25 -0
  58. package/server/dist/db/schema-manga.js +31 -0
  59. package/server/dist/db/schema-tv.js +33 -0
  60. package/server/dist/db.js +75 -4
  61. package/server/dist/discord-gateway.js +1 -1
  62. package/server/dist/google.js +0 -7
  63. package/server/dist/hono/asmr.js +1 -0
  64. package/server/dist/hono/auth.js +56 -0
  65. package/server/dist/hono/music.js +419 -0
  66. package/server/dist/{controllers/proxy.controller.js → hono/proxy.js} +390 -394
  67. package/server/dist/hono/tv.js +197 -0
  68. package/server/dist/hono/watchlist.js +733 -0
  69. package/server/dist/lib/anilist.js +3 -3
  70. package/server/dist/lib/insights.js +185 -0
  71. package/server/dist/lib/ipc.js +1 -2
  72. package/server/dist/lib/kitsu.js +1 -28
  73. package/server/dist/lib/mal-import.js +291 -0
  74. package/server/dist/lib/migration.js +20 -34
  75. package/server/dist/lib/offline-db.js +36 -36
  76. package/server/dist/lib/tracker/manga-sync.service.js +21 -21
  77. package/server/dist/lib/tracker/sync.service.js +20 -20
  78. package/server/dist/lib/translate.js +79 -0
  79. package/server/dist/lib/ytmusic.js +0 -2
  80. package/server/dist/providers/title-matching.js +0 -1
  81. package/server/dist/repositories/asmr.repository.js +90 -87
  82. package/server/dist/repositories/insights.repository.js +24 -13
  83. package/server/dist/repositories/library.repository.js +33 -93
  84. package/server/dist/repositories/manga.repository.js +108 -116
  85. package/server/dist/repositories/notifications.repository.js +50 -31
  86. package/server/dist/repositories/queue.repository.js +36 -25
  87. package/server/dist/repositories/settings.repository.js +18 -8
  88. package/server/dist/repositories/shows-meta.repository.js +54 -41
  89. package/server/dist/repositories/temp-show-ids.repository.js +43 -31
  90. package/server/dist/repositories/tv.repository.js +98 -115
  91. package/server/dist/repositories/watched-episodes.repository.js +52 -20
  92. package/server/dist/repositories/watchlist.repository.js +36 -48
  93. package/server/dist/server.js +59 -151
  94. package/server/dist/sync.js +41 -4
  95. package/server/dist/trpc/context.js +7 -0
  96. package/server/dist/trpc/errors.js +21 -0
  97. package/server/dist/trpc/index.js +19 -0
  98. package/server/dist/trpc/router.js +49 -0
  99. package/server/dist/trpc/routers/asmr-library.js +122 -0
  100. package/server/dist/trpc/routers/asmr-progress.js +133 -0
  101. package/server/dist/trpc/routers/asmr.js +87 -0
  102. package/server/dist/trpc/routers/auth.js +247 -0
  103. package/server/dist/trpc/routers/continue-watching.js +83 -0
  104. package/server/dist/trpc/routers/data.js +1039 -0
  105. package/server/dist/trpc/routers/discord.js +288 -0
  106. package/server/dist/trpc/routers/discovery.js +15 -0
  107. package/server/dist/trpc/routers/insights.js +45 -0
  108. package/server/dist/trpc/routers/manga-library.js +280 -0
  109. package/server/dist/trpc/routers/manga-progress.js +181 -0
  110. package/server/dist/trpc/routers/manga.js +201 -0
  111. package/server/dist/{controllers/music.controller.js → trpc/routers/music.js} +155 -288
  112. package/server/dist/trpc/routers/notifications.js +80 -0
  113. package/server/dist/trpc/routers/progress.js +15 -0
  114. package/server/dist/trpc/routers/queue.js +95 -0
  115. package/server/dist/{routes/radio.routes.js → trpc/routers/radio.js} +30 -23
  116. package/server/dist/trpc/routers/settings.js +105 -0
  117. package/server/dist/trpc/routers/tracker.js +219 -0
  118. package/server/dist/trpc/routers/tv-library.js +145 -0
  119. package/server/dist/trpc/routers/tv-progress.js +171 -0
  120. package/server/dist/trpc/routers/tv.js +565 -0
  121. package/server/dist/trpc/routers/watchlist.js +564 -0
  122. package/server/dist/trpc/validation.js +159 -0
  123. package/server/dist/utils/security.utils.js +0 -18
  124. package/server/package.json +5 -10
  125. package/client/dist/assets/Asmr-C28TfSvX.js +0 -2
  126. package/client/dist/assets/AsmrResetProgressModal-6Xh71AgR.js +0 -1
  127. package/client/dist/assets/AvSyncCalibrator-BwCtRW0P.js +0 -10
  128. package/client/dist/assets/Home-BJdbHYja.js +0 -1
  129. package/client/dist/assets/Insights-CjRy42YE.js +0 -1
  130. package/client/dist/assets/MangaReadPage-YCETZRte.js +0 -1
  131. package/client/dist/assets/Mature-BGxRWkV5.js +0 -1
  132. package/client/dist/assets/MediaCard-CAzoxsRS.js +0 -1
  133. package/client/dist/assets/Music-Cgo-yQ7E.js +0 -1
  134. package/client/dist/assets/OptionListMenu--Owe9Jle.js +0 -1
  135. package/client/dist/assets/Player-yRXG2BrP.js +0 -56
  136. package/client/dist/assets/PlayerSettings-DrZLa0CC.js +0 -1
  137. package/client/dist/assets/QueueOptionsButton-BVZyPyKf.js +0 -1
  138. package/client/dist/assets/Radio-BFjxlzuF.js +0 -1
  139. package/client/dist/assets/Search-BOBo0Ogh.js +0 -1
  140. package/client/dist/assets/Settings-DOvNQ0gX.js +0 -21
  141. package/client/dist/assets/Trackers-DZ9KqOWv.js +0 -2
  142. package/client/dist/assets/Tv-5mATEU4p.js +0 -4
  143. package/client/dist/assets/Tv-C2SWcVzN.css +0 -1
  144. package/client/dist/assets/TvInfo-CF-5C5Fs.js +0 -1
  145. package/client/dist/assets/TvPopup-Dfqifj3s.js +0 -1
  146. package/client/dist/assets/Watchlist-B8MHNsAU.js +0 -1
  147. package/client/dist/assets/index-D2RkUJIN.js +0 -178
  148. package/client/dist/assets/useAnimeInfoData-BWShDHqE.js +0 -1
  149. package/client/dist/assets/useAsmr-FyfD9G7b.js +0 -1
  150. package/client/dist/assets/useMangaLibrary-BJU5x8Vz.js +0 -1
  151. package/client/dist/assets/useProviders-DDRxa8vl.js +0 -1
  152. package/client/dist/assets/useTv-Bh6WmNnZ.js +0 -1
  153. package/client/dist/assets/useTvLibrary-_Pqm8re1.js +0 -1
  154. package/server/dist/controllers/asmr-library.controller.js +0 -288
  155. package/server/dist/controllers/auth.controller.js +0 -225
  156. package/server/dist/controllers/data.controller.js +0 -767
  157. package/server/dist/controllers/insights.controller.js +0 -203
  158. package/server/dist/controllers/manga-library.controller.js +0 -350
  159. package/server/dist/controllers/settings.controller.js +0 -609
  160. package/server/dist/controllers/tv-library.controller.js +0 -314
  161. package/server/dist/controllers/watchlist.controller.js +0 -1069
  162. package/server/dist/request-logger.js +0 -16
  163. package/server/dist/routes/asmr-library.routes.js +0 -23
  164. package/server/dist/routes/asmr.routes.js +0 -81
  165. package/server/dist/routes/auth.routes.js +0 -25
  166. package/server/dist/routes/data.routes.js +0 -47
  167. package/server/dist/routes/discord-gateway.routes.js +0 -51
  168. package/server/dist/routes/insights.routes.js +0 -35
  169. package/server/dist/routes/lan-auth.routes.js +0 -46
  170. package/server/dist/routes/manga-library.routes.js +0 -25
  171. package/server/dist/routes/manga.routes.js +0 -141
  172. package/server/dist/routes/music.routes.js +0 -20
  173. package/server/dist/routes/providers.routes.js +0 -17
  174. package/server/dist/routes/proxy.routes.js +0 -12
  175. package/server/dist/routes/settings.routes.js +0 -24
  176. package/server/dist/routes/tracker.routes.js +0 -287
  177. package/server/dist/routes/translate.routes.js +0 -88
  178. package/server/dist/routes/tv-library.routes.js +0 -23
  179. package/server/dist/routes/tv.routes.js +0 -629
  180. package/server/dist/routes/watchlist.routes.js +0 -341
@@ -1,767 +0,0 @@
1
- import { parseJsonBody } from '../utils/http.utils.js';
2
- import { pickBestMatch } from '../providers/title-matching.js';
3
- import { getTrending, getLatestReleases, getSeasonal, getShowMetaById, getAnilistEpisodes, getSchedule, searchAnilist, searchAnilistByTitle, parseMalId, getShowMetaByMalId, getSpotlightBanners, getBatchedHomeData, getGenreTagLists, anilistUnavailable, wasAnilistDownAtBoot, checkAnilistStatus, fromAnilistMedia, } from '../lib/anilist.js';
4
- import { malSearchMedia, malSearchTitle, malAnimeDetail } from '../lib/mal.js';
5
- import { malCacheStore } from '../repositories/mal-cache.repository.js';
6
- import { getMigratedId } from '../lib/migration.js';
7
- import { isTempShowId } from '../lib/temp-ids.js';
8
- import { TempShowIdsRepository } from '../repositories/temp-show-ids.repository.js';
9
- import { ShowsMetaRepository } from '../repositories/shows-meta.repository.js';
10
- import { WatchlistRepository } from '../repositories/watchlist.repository.js';
11
- import { dbRun } from '../utils/db-utils.js';
12
- import logger from '../logger.js';
13
- function parseListParam(value) {
14
- if (typeof value !== 'string' || !value.trim())
15
- return undefined;
16
- const list = value
17
- .split(',')
18
- .map((v) => v.trim())
19
- .filter(Boolean);
20
- return list.length > 0 ? list : undefined;
21
- }
22
- export class DataController {
23
- providers;
24
- getCatalog;
25
- constructor(providers, getCatalog) {
26
- this.providers = providers;
27
- this.getCatalog = getCatalog;
28
- }
29
- setDataCache(res, maxAgeSeconds, body) {
30
- const isEmpty = Array.isArray(body)
31
- ? body.length === 0
32
- : body == null || (typeof body === 'object' && Object.keys(body).length === 0);
33
- res.set('Cache-Control', isEmpty ? 'no-store' : `public, max-age=${maxAgeSeconds}`);
34
- }
35
- getProvider(req) {
36
- const providerName = req.query.provider?.toLowerCase();
37
- if (!providerName)
38
- return null;
39
- return this.providers[providerName] || null;
40
- }
41
- matureStreamingIds() {
42
- const catalog = this.getCatalog?.();
43
- if (!catalog || catalog.length === 0)
44
- return null;
45
- return new Set(catalog
46
- .filter((c) => c.mature && (c.kind ?? 'anime') === 'anime' && c.loaded)
47
- .map((c) => c.id.toLowerCase()));
48
- }
49
- isMatureStreamingProvider(provider) {
50
- const key = (provider || '').toLowerCase();
51
- if (!key || !this.providers[key] || key === 'mal' || key === 'anilist')
52
- return false;
53
- const mature = this.matureStreamingIds();
54
- return mature ? mature.has(key) : true;
55
- }
56
- async resolveMatureStreamingId(title, wanted) {
57
- const mature = this.matureStreamingIds();
58
- const pool = mature ? [...mature] : Object.keys(this.providers);
59
- const names = [wanted, ...pool].filter((p) => !!p && p !== 'mal' && p !== 'anilist' && !!this.providers[p]);
60
- const seen = new Set();
61
- for (const name of names) {
62
- if (seen.has(name))
63
- continue;
64
- seen.add(name);
65
- try {
66
- const resolved = await this.providers[name].resolveShowId?.(title);
67
- if (resolved)
68
- return { provider: this.providers[name], nativeId: resolved };
69
- }
70
- catch {
71
- // ignore
72
- }
73
- }
74
- return null;
75
- }
76
- getTrending = async (_req, res) => {
77
- try {
78
- const data = await getTrending(1, 20, 'TRENDING_DESC', 'RELEASING');
79
- this.setDataCache(res, 300, data);
80
- res.json(data);
81
- }
82
- catch (e) {
83
- logger.error({ err: e }, 'Trending fetch failed');
84
- res.set('Cache-Control', 'no-store').json([]);
85
- }
86
- };
87
- getSpotlight = async (_req, res) => {
88
- try {
89
- const data = await getSpotlightBanners(1, 20);
90
- this.setDataCache(res, 300, data);
91
- res.json(data);
92
- }
93
- catch (e) {
94
- logger.error({ err: e }, 'Spotlight fetch failed');
95
- res.set('Cache-Control', 'no-store').json([]);
96
- }
97
- };
98
- getPopularList = async (req, res) => {
99
- const sort = req.query.sort === 'POPULARITY_DESC' ? 'POPULARITY_DESC' : 'TRENDING_DESC';
100
- const page = parseInt(req.query.page) || 1;
101
- const size = parseInt(req.query.size) || 20;
102
- try {
103
- const data = await getTrending(page, size, sort);
104
- this.setDataCache(res, 300, data);
105
- res.json(data);
106
- }
107
- catch (e) {
108
- logger.error({ err: e }, 'Popular list fetch failed');
109
- res.set('Cache-Control', 'no-store').json([]);
110
- }
111
- };
112
- getSchedule = async (req, res) => {
113
- try {
114
- const date = new Date(req.params.date + 'T00:00:00.000Z');
115
- const format = req.query.format || undefined;
116
- const adult = format === 'ADULT';
117
- const data = await getSchedule(date, adult ? undefined : format, adult);
118
- this.setDataCache(res, 300, data);
119
- res.json(data);
120
- }
121
- catch (e) {
122
- logger.error({ err: e, date: req.params.date }, 'Schedule fetch failed');
123
- res.set('Cache-Control', 'no-store').json([]);
124
- }
125
- };
126
- getSkipTimes = async (req, res) => {
127
- try {
128
- const showId = req.params.showId;
129
- const episodeNumber = req.params.episodeNumber;
130
- if (/^\d+$/.test(showId)) {
131
- const skipRes = await fetch(`https://api.aniskip.com/v1/skip-times/${showId}/${episodeNumber}?types=op&types=ed`);
132
- if (skipRes.ok) {
133
- const data = await parseJsonBody(skipRes);
134
- return res.json(data);
135
- }
136
- }
137
- res.json({ found: false, results: [] });
138
- }
139
- catch {
140
- res.json({ found: false, results: [] });
141
- }
142
- };
143
- getVideo = async (req, res) => {
144
- try {
145
- let showId = req.query.showId;
146
- const videoMalId = parseMalId(showId);
147
- if (videoMalId) {
148
- try {
149
- const malMeta = await getShowMetaByMalId(videoMalId);
150
- if (malMeta?.anilistId && malMeta.anilistId > 0)
151
- showId = String(malMeta.anilistId);
152
- }
153
- catch {
154
- // ignore
155
- }
156
- }
157
- if (isTempShowId(showId)) {
158
- try {
159
- const row = TempShowIdsRepository.getById(req.db, showId);
160
- if (!row || !this.isMatureStreamingProvider(row.provider))
161
- return res.json([]);
162
- const wanted = String(req.query.provider || '').toLowerCase();
163
- const own = this.providers[row.provider];
164
- if (own && (!wanted || wanted === row.provider)) {
165
- const urls = await own.getStreamUrls(row.nativeId, req.query.episodeNumber, req.query.mode);
166
- return res.json(urls || []);
167
- }
168
- const hit = await this.resolveMatureStreamingId(row.title, wanted);
169
- if (hit) {
170
- const urls = await hit.provider.getStreamUrls(hit.nativeId, req.query.episodeNumber, req.query.mode);
171
- return res.json(urls || []);
172
- }
173
- }
174
- catch (e) {
175
- logger.error({ err: e, showId }, 'Temp show video fetch failed');
176
- }
177
- return res.json([]);
178
- }
179
- const providerName = req.query.provider;
180
- const providerKey = providerName?.toLowerCase();
181
- const stillMal = parseMalId(showId) !== null;
182
- const isNativeId = !!(providerKey && this.providers[providerKey]?.isDirectId?.(showId));
183
- const needsResolution = !isNativeId && (/^\d+$/.test(showId) || stillMal);
184
- if (providerKey && needsResolution) {
185
- const meta = (await ShowsMetaRepository.getById(req.db, showId));
186
- let targetTitle = meta?.englishName || meta?.name;
187
- let anilistShow = null;
188
- if (!targetTitle) {
189
- try {
190
- anilistShow = await getShowMetaById(showId);
191
- targetTitle = anilistShow?.englishName || anilistShow?.name;
192
- if (anilistShow && targetTitle) {
193
- await ShowsMetaRepository.upsert(req.db, {
194
- id: showId,
195
- name: anilistShow.name,
196
- thumbnail: anilistShow.thumbnail,
197
- nativeName: anilistShow.nativeName,
198
- englishName: anilistShow.englishName,
199
- genres: anilistShow.genres
200
- ? JSON.stringify(anilistShow.genres.map((genre) => genre.name))
201
- : undefined,
202
- status: anilistShow.status,
203
- episodeCount: anilistShow.episodeCount != null ? Number(anilistShow.episodeCount) : undefined,
204
- type: anilistShow.type,
205
- anilistId: anilistShow.anilistId,
206
- });
207
- }
208
- }
209
- catch (err) {
210
- logger.warn({ err, provider: providerKey, showId }, '[Video] AniList metadata lookup failed while resolving numeric showId');
211
- }
212
- }
213
- if (targetTitle) {
214
- let romaji = anilistShow?.names?.romaji;
215
- let nativeName = anilistShow?.names?.native;
216
- let synonyms = anilistShow?.names?.synonyms;
217
- if (!romaji || !nativeName) {
218
- try {
219
- const show = await getShowMetaById(showId);
220
- romaji = romaji || show?.names?.romaji;
221
- nativeName = nativeName || show?.names?.native;
222
- synonyms = synonyms || show?.names?.synonyms;
223
- }
224
- catch {
225
- // ignore
226
- }
227
- }
228
- const titleTargets = [
229
- targetTitle,
230
- romaji,
231
- nativeName,
232
- ...(synonyms ?? []).slice(0, 2),
233
- ].filter((t) => !!t && t.trim().length > 0);
234
- const distinctTargets = [...new Set(titleTargets.map((t) => t.trim()))].filter((t, i, a) => a.findIndex((x) => x.toLowerCase() === t.toLowerCase()) === i);
235
- let resolved = null;
236
- for (const variant of distinctTargets) {
237
- try {
238
- resolved =
239
- (await this.providers[providerKey]?.resolveShowId?.(variant, romaji, req.query.mode)) ?? null;
240
- }
241
- catch {
242
- resolved = null;
243
- }
244
- if (resolved)
245
- break;
246
- }
247
- if (resolved) {
248
- showId = resolved;
249
- }
250
- else {
251
- logger.warn({ provider: providerKey, showId, title: targetTitle, romaji }, '[Video] resolveShowId failed, attempting fallback provider search');
252
- try {
253
- const fallbackResults = await this.providers[providerKey]?.search?.({
254
- query: targetTitle,
255
- });
256
- const fallbackMatch = pickBestMatch((fallbackResults || []).map((r) => ({
257
- title: r.name || r.englishName || '',
258
- id: r.id || r._id || '',
259
- })), distinctTargets);
260
- if (fallbackMatch) {
261
- showId = fallbackMatch.item.id;
262
- }
263
- else {
264
- logger.warn({ provider: providerKey, showId, title: targetTitle }, '[Video] fallback provider search returned no results');
265
- return res.json([]);
266
- }
267
- }
268
- catch (fallbackErr) {
269
- if (fallbackErr.message === 'AUTH_REQUIRED') {
270
- throw fallbackErr;
271
- }
272
- logger.error({ err: fallbackErr, provider: providerKey, showId, title: targetTitle }, '[Video] fallback provider search failed');
273
- return res.json([]);
274
- }
275
- }
276
- }
277
- else {
278
- logger.warn({ provider: providerKey, showId }, '[Video] numeric showId passed to provider but local meta missing title');
279
- return res.json([]);
280
- }
281
- }
282
- const provider = this.getProvider(req);
283
- if (!provider)
284
- return res.json([]);
285
- const urls = await provider.getStreamUrls(showId, req.query.episodeNumber, req.query.mode);
286
- res.json(urls || []);
287
- }
288
- catch (e) {
289
- if (e.message === 'AUTH_REQUIRED') {
290
- return res.status(403).json({ error: 'AUTH_REQUIRED', provider: 'animepahe' });
291
- }
292
- logger.error({ err: e, provider: req.query.provider }, 'Provider video fetch failed');
293
- res.json([]);
294
- }
295
- };
296
- getEpisodes = async (req, res) => {
297
- const showIdRaw = req.query.showId;
298
- if (!showIdRaw) {
299
- return res.json({ episodes: [] });
300
- }
301
- let showId = await getMigratedId(req.db, showIdRaw);
302
- const epsMalId = parseMalId(showId);
303
- if (epsMalId) {
304
- try {
305
- const malMeta = await getShowMetaByMalId(epsMalId);
306
- if (malMeta?.anilistId && malMeta.anilistId > 0)
307
- showId = String(malMeta.anilistId);
308
- }
309
- catch {
310
- // ignore
311
- }
312
- }
313
- if (isTempShowId(showId)) {
314
- try {
315
- const row = TempShowIdsRepository.getById(req.db, showId);
316
- if (!row || !this.isMatureStreamingProvider(row.provider))
317
- return res.json({ episodes: [] });
318
- const wanted = String(req.query.provider || '').toLowerCase();
319
- const own = this.providers[row.provider];
320
- if (own && (!wanted || wanted === row.provider)) {
321
- const data = await own.getEpisodes(row.nativeId, req.query.mode);
322
- if (data?.episodes?.length)
323
- return res.json(data);
324
- return res.json({ episodes: [] });
325
- }
326
- const hit = await this.resolveMatureStreamingId(row.title, wanted);
327
- if (hit) {
328
- const data = await hit.provider.getEpisodes(hit.nativeId, req.query.mode);
329
- if (data?.episodes?.length)
330
- return res.json(data);
331
- }
332
- }
333
- catch {
334
- // ignore
335
- }
336
- return res.json({ episodes: [] });
337
- }
338
- if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(showId)) {
339
- try {
340
- if (this.providers['animepahe']) {
341
- const data = await this.providers['animepahe'].getEpisodes(showId, req.query.mode);
342
- return res.json(data || { episodes: [] });
343
- }
344
- }
345
- catch {
346
- return res.json({ episodes: [] });
347
- }
348
- }
349
- const isNumeric = /^(mal-\d+|-?\d+)$/i.test(showId);
350
- if (isNumeric) {
351
- let episodes = [];
352
- try {
353
- episodes = await getAnilistEpisodes(showId);
354
- }
355
- catch (e) {
356
- logger.error({ err: e, showId }, 'Episodes fetch failed');
357
- }
358
- if (episodes.length === 0) {
359
- const absId = Math.abs(parseInt(showId, 10));
360
- if (!Number.isFinite(absId)) {
361
- res.set('Cache-Control', 'public, max-age=3600').json({ episodes });
362
- return;
363
- }
364
- try {
365
- const d = await malAnimeDetail(malCacheStore(), absId);
366
- const total = d.detail?.episodes;
367
- if (total && total > 0 && total <= 500) {
368
- episodes = Array.from({ length: total }, (_, i) => (i + 1).toString());
369
- }
370
- }
371
- catch {
372
- // ignore
373
- }
374
- }
375
- if (episodes.length === 0) {
376
- const providerName = req.query.provider?.toLowerCase();
377
- const provider = providerName ? this.providers[providerName] : undefined;
378
- if (provider?.isDirectId?.(showId)) {
379
- try {
380
- const data = await provider.getEpisodes(showId, req.query.mode);
381
- if (data?.episodes?.length) {
382
- res.set('Cache-Control', 'no-store').json(data);
383
- return;
384
- }
385
- }
386
- catch {
387
- // ignore
388
- }
389
- }
390
- }
391
- res.set('Cache-Control', 'public, max-age=3600').json({ episodes });
392
- return;
393
- }
394
- const providerName = req.query.provider?.toLowerCase();
395
- const provider = providerName ? this.providers[providerName] : undefined;
396
- if (provider) {
397
- try {
398
- const data = await provider.getEpisodes(showId, req.query.mode);
399
- if (data?.episodes?.length) {
400
- return res.json(data);
401
- }
402
- }
403
- catch {
404
- // ignore
405
- }
406
- }
407
- res.json({ episodes: [] });
408
- };
409
- search = async (req, res) => {
410
- try {
411
- const query = req.query.query || '';
412
- const page = parseInt(req.query.page) || 1;
413
- const perPage = parseInt(req.query.limit) || 14;
414
- const sort = req.query.sortBy || undefined;
415
- const result = await searchAnilist({
416
- query,
417
- page,
418
- perPage,
419
- format: req.query.type,
420
- status: req.query.status,
421
- season: req.query.season,
422
- seasonYear: req.query.year ? parseInt(req.query.year) : undefined,
423
- countryOfOrigin: req.query.country,
424
- genre: req.query.genres,
425
- genre_not_in: parseListParam(req.query.excludeGenres),
426
- tag_not_in: parseListParam(req.query.excludeTags),
427
- averageScore_greater: req.query.minScore
428
- ? parseInt(req.query.minScore)
429
- : undefined,
430
- episodes_greater: req.query.minEpisodes
431
- ? parseInt(req.query.minEpisodes)
432
- : undefined,
433
- isAdult: req.query.adult === 'true' ? true : req.query.adult === 'false' ? false : undefined,
434
- sort,
435
- });
436
- return res.json(result);
437
- }
438
- catch (e) {
439
- logger.error({ err: e }, 'search failed');
440
- res.json([]);
441
- }
442
- };
443
- matureSearch = async (req, res) => {
444
- try {
445
- const provider = (req.query.provider || 'anilist').toLowerCase();
446
- const query = req.query.query || '';
447
- const page = parseInt(req.query.page) || 1;
448
- const limit = parseInt(req.query.limit) || 14;
449
- if (provider === 'anilist') {
450
- const result = await searchAnilist({
451
- query,
452
- page,
453
- perPage: limit,
454
- format: req.query.type,
455
- status: req.query.status,
456
- season: req.query.season,
457
- seasonYear: req.query.year ? parseInt(req.query.year) : undefined,
458
- countryOfOrigin: req.query.country,
459
- genre: req.query.genres,
460
- genre_not_in: parseListParam(req.query.excludeGenres),
461
- isAdult: true,
462
- sort: req.query.sortBy || undefined,
463
- });
464
- return res.json({ data: result, hasMore: result.length >= limit });
465
- }
466
- if (this.isMatureStreamingProvider(provider)) {
467
- const streaming = this.providers[provider];
468
- if (streaming?.browse) {
469
- const genre = req.query.genre || req.query.genres || undefined;
470
- const result = await streaming.browse({
471
- query,
472
- page,
473
- limit,
474
- pageSize: limit,
475
- genre,
476
- genres: req.query.genres || genre,
477
- order: req.query.order || req.query.sortBy || undefined,
478
- sort: req.query.sortBy || req.query.order || undefined,
479
- studio: req.query.studio || undefined,
480
- blacklist: req.query.blacklist || undefined,
481
- });
482
- return res.json({
483
- data: result.shows,
484
- hasMore: result.hasMore,
485
- ...(result.total !== undefined ? { total: result.total } : {}),
486
- ...(result.genres ? { genres: result.genres } : {}),
487
- });
488
- }
489
- const shows = await streaming.search({ query });
490
- return res.json({
491
- data: shows.map((s) => ({ ...s, isAdult: true })),
492
- hasMore: false,
493
- });
494
- }
495
- if (provider === 'mal') {
496
- const malPage = page;
497
- const malLimit = 50;
498
- let result = [];
499
- try {
500
- result = await malSearchMedia(malCacheStore(), {
501
- query,
502
- page: malPage,
503
- perPage: malLimit,
504
- format: req.query.type,
505
- status: req.query.status,
506
- genre: req.query.genres || undefined,
507
- genre_not_in: parseListParam(req.query.excludeGenres),
508
- isAdult: true,
509
- sort: req.query.sortBy || undefined,
510
- averageScore_greater: req.query.minScore
511
- ? parseInt(req.query.minScore)
512
- : undefined,
513
- episodes_greater: req.query.minEpisodes
514
- ? parseInt(req.query.minEpisodes)
515
- : undefined,
516
- });
517
- }
518
- catch (e) {
519
- logger.error({ err: e }, 'mal mature search failed');
520
- }
521
- const shows = result.map((m) => {
522
- const show = fromAnilistMedia(m);
523
- return { ...show, isAdult: true };
524
- });
525
- const sliced = shows.slice(0, limit);
526
- return res.json({ data: sliced, hasMore: shows.length >= limit });
527
- }
528
- return res.status(400).json({ error: 'Unknown mature provider' });
529
- }
530
- catch (e) {
531
- logger.error({ err: e }, 'mature search failed');
532
- res.json({ data: [], hasMore: false });
533
- }
534
- };
535
- resolveMature = async (req, res) => {
536
- try {
537
- const title = (req.query.title || '').trim();
538
- if (!title)
539
- return res.status(400).json({ error: 'title is required' });
540
- const anilistResult = await searchAnilistByTitle(title);
541
- if (anilistResult) {
542
- const id = anilistResult.id;
543
- return res.json({ id: typeof id === 'number' && id < 0 ? `mal-${Math.abs(id)}` : id });
544
- }
545
- const malResult = await malSearchTitle(malCacheStore(), title);
546
- if (malResult)
547
- return res.json({ id: malResult, provider: 'mal' });
548
- return res.status(404).json({ error: 'No match found' });
549
- }
550
- catch (e) {
551
- logger.error({ err: e }, 'mature resolve failed');
552
- res.status(500).json({ error: 'Resolve failed' });
553
- }
554
- };
555
- getMatureFilters = async (_req, res) => {
556
- try {
557
- const catalog = this.getCatalog?.() ?? [];
558
- const providers = {};
559
- for (const item of catalog) {
560
- if (!item.mature || (item.kind ?? 'anime') !== 'anime' || !item.loaded)
561
- continue;
562
- providers[item.id] = {
563
- label: item.label,
564
- ...(item.browse ? { browse: item.browse } : {}),
565
- ...(item.facets?.genres?.length ? { genres: item.facets.genres } : {}),
566
- ...(item.facets?.orders?.length ? { orders: item.facets.orders } : {}),
567
- };
568
- }
569
- return res.json({ providers });
570
- }
571
- catch (e) {
572
- logger.error({ err: e }, 'mature filters failed');
573
- res.status(500).json({ error: 'Failed to load filters' });
574
- }
575
- };
576
- getSeasonal = async (req, res) => {
577
- const page = parseInt(req.query.page) || 1;
578
- const size = parseInt(req.query.size) || 14;
579
- const format = req.query.format;
580
- try {
581
- const data = await getSeasonal(page, size, format);
582
- this.setDataCache(res, 300, data);
583
- res.json(data);
584
- }
585
- catch (e) {
586
- logger.error({ err: e }, 'Seasonal fetch failed');
587
- res.set('Cache-Control', 'no-store').json([]);
588
- }
589
- };
590
- getLatestReleases = async (req, res) => {
591
- const format = req.query.format || 'TV';
592
- const page = parseInt(req.query.page) || 1;
593
- const size = parseInt(req.query.size) || 12;
594
- try {
595
- const data = await getLatestReleases(format, page, size);
596
- this.setDataCache(res, 300, data);
597
- res.json(data);
598
- }
599
- catch (e) {
600
- logger.error({ err: e }, 'Latest releases fetch failed');
601
- res.set('Cache-Control', 'no-store').json([]);
602
- }
603
- };
604
- getShowMeta = async (req, res) => {
605
- const showIdRaw = req.params.id;
606
- const id = await getMigratedId(req.db, showIdRaw);
607
- if (isTempShowId(id)) {
608
- const row = TempShowIdsRepository.getById(req.db, id);
609
- if (!row || !this.isMatureStreamingProvider(row.provider)) {
610
- res.json({});
611
- return;
612
- }
613
- res.set('Cache-Control', 'public, max-age=300').json({
614
- _id: row.id,
615
- id: row.id,
616
- name: row.title,
617
- englishName: row.title,
618
- thumbnail: row.thumbnail || '',
619
- type: 'TV',
620
- isAdult: true,
621
- status: 'UNKNOWN',
622
- provider: row.provider,
623
- nativeId: row.nativeId,
624
- });
625
- return;
626
- }
627
- const isNumeric = /^(mal-\d+|-?\d+)$/i.test(id);
628
- if (isNumeric) {
629
- let meta = null;
630
- try {
631
- meta = await getShowMetaById(id);
632
- }
633
- catch (e) {
634
- logger.warn({ err: e, id }, 'AniList show-meta fetch failed, trying local cache');
635
- const localMeta = (await ShowsMetaRepository.getById(req.db, id));
636
- if (localMeta) {
637
- if (typeof localMeta.genres === 'string') {
638
- try {
639
- localMeta.genres = JSON.parse(localMeta.genres);
640
- }
641
- catch {
642
- localMeta.genres = [];
643
- }
644
- }
645
- res.set('Cache-Control', 'public, max-age=3600').json(localMeta);
646
- return;
647
- }
648
- }
649
- if (meta) {
650
- const poster = meta.thumbnail?.trim() ? meta.thumbnail : undefined;
651
- const hasPoster = !!poster;
652
- try {
653
- ShowsMetaRepository.upsert(req.db, {
654
- id,
655
- name: meta.name,
656
- thumbnail: poster,
657
- nativeName: meta.nativeName,
658
- englishName: meta.englishName,
659
- popularityScore: meta.score ?? (meta.averageScore != null ? meta.averageScore / 10 : undefined),
660
- genres: meta.genres
661
- ? JSON.stringify(meta.genres.map((g) => (typeof g === 'string' ? g : g?.name)).filter(Boolean))
662
- : undefined,
663
- status: meta.status,
664
- episodeCount: meta.episodeCount != null ? Number(meta.episodeCount) : undefined,
665
- type: meta.type,
666
- anilistId: meta.anilistId,
667
- });
668
- }
669
- catch {
670
- // ignore
671
- }
672
- if (hasPoster && poster) {
673
- const existingWatchlist = (await WatchlistRepository.getById(req.db, id));
674
- if (existingWatchlist && existingWatchlist.thumbnail !== poster) {
675
- try {
676
- await WatchlistRepository.updateThumbnail(req.db, id, poster);
677
- }
678
- catch {
679
- // ignore
680
- }
681
- }
682
- req.db.scheduleSave();
683
- }
684
- }
685
- if (/^(mal-\d+|-\d+)$/i.test(id) && meta?.anilistId && meta.anilistId > 0) {
686
- try {
687
- dbRun(req.db, 'INSERT OR REPLACE INTO legacy_id_mapping (legacyId, numericId) VALUES (?, ?)', [id, String(meta.anilistId)]);
688
- }
689
- catch {
690
- // ignore
691
- }
692
- }
693
- this.setDataCache(res, 3600, meta || {});
694
- res.json(meta || {});
695
- return;
696
- }
697
- res.json({});
698
- };
699
- allocateTempShow = async (req, res) => {
700
- try {
701
- const provider = String(req.body?.provider || '').toLowerCase();
702
- const nativeId = String(req.body?.nativeId || '').trim();
703
- const title = String(req.body?.title || '').trim();
704
- const thumbnail = String(req.body?.thumbnail || '');
705
- if (!provider || !this.isMatureStreamingProvider(provider)) {
706
- return res.status(400).json({ error: 'Unknown provider' });
707
- }
708
- if (!nativeId || !title) {
709
- return res.status(400).json({ error: 'nativeId and title are required' });
710
- }
711
- const row = TempShowIdsRepository.allocate(req.db, {
712
- provider,
713
- nativeId,
714
- title,
715
- thumbnail,
716
- });
717
- return res.json({ id: row.id });
718
- }
719
- catch (e) {
720
- logger.error({ err: e }, 'temp show allocate failed');
721
- res.status(500).json({ error: 'Allocate failed' });
722
- }
723
- };
724
- getAnilistStatus = async (_req, res) => {
725
- const available = !anilistUnavailable();
726
- if (!available) {
727
- checkAnilistStatus().catch(() => { });
728
- }
729
- res.json({ available, wasDownAtBoot: wasAnilistDownAtBoot() });
730
- };
731
- getGenresAndTags = async (_req, res) => {
732
- try {
733
- res.json(await getGenreTagLists());
734
- }
735
- catch (e) {
736
- logger.error({ err: e }, 'genres-and-tags failed');
737
- res.status(500).json({ error: 'Failed to load genres' });
738
- }
739
- };
740
- getSystemNotifications = async (_req, res) => {
741
- const notifications = [];
742
- if (wasAnilistDownAtBoot() && anilistUnavailable()) {
743
- const fb = 'MAL + Kitsu';
744
- notifications.push({
745
- id: 'system-anilist-down',
746
- type: 'system',
747
- title: 'AniList API',
748
- 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.`,
749
- icon: 'warning',
750
- createdAt: Date.now(),
751
- });
752
- }
753
- res.json(notifications);
754
- };
755
- getBatchedHome = async (req, res) => {
756
- try {
757
- const format = req.query.format || undefined;
758
- const data = await getBatchedHomeData(format);
759
- const isEmpty = data.trending.length === 0 && data.seasonal.length === 0 && data.spotlight.length === 0;
760
- res.set('Cache-Control', isEmpty ? 'no-store' : 'public, max-age=300').json(data);
761
- }
762
- catch (e) {
763
- logger.error({ err: e }, 'Batched home fetch failed');
764
- res.set('Cache-Control', 'no-store').json({ trending: [], seasonal: [], spotlight: [] });
765
- }
766
- };
767
- }