@serifpersia/dango 3.3.9 → 3.4.0

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-CJy1SXH3.js} +1 -1
  3. package/client/dist/assets/{AnimeInfo-e3z3er9t.js → AnimeInfo-BoXlVvZ-.js} +1 -1
  4. package/client/dist/assets/{AnimeInfoPage-B7Djlmy_.js → AnimeInfoPage-BxbA_7AV.js} +2 -2
  5. package/client/dist/assets/Asmr--Zc8G9zv.js +2 -0
  6. package/client/dist/assets/AsmrResetProgressModal-CKYm-8cp.js +1 -0
  7. package/client/dist/assets/AvSyncCalibrator-3yjZS7s6.js +10 -0
  8. package/client/dist/assets/Home-rrhOGQcg.js +1 -0
  9. package/client/dist/assets/Insights-Bay90Yeg.js +1 -0
  10. package/client/dist/assets/{LibraryListPage-B0qWqhb_.js → LibraryListPage-HzuDavbb.js} +1 -1
  11. package/client/dist/assets/{ListeningList-CSqD8j1C.js → ListeningList-DYcQnnsG.js} +1 -1
  12. package/client/dist/assets/{Manga-Cg2YuObF.js → Manga-D8CT9aO0.js} +1 -1
  13. package/client/dist/assets/{MangaInfoPage-DcArhEsy.js → MangaInfoPage-DGK6CYp1.js} +1 -1
  14. package/client/dist/assets/MangaReadPage-B9XoDKF9.js +1 -0
  15. package/client/dist/assets/{MangaResetProgressModal-D_yT3d1v.js → MangaResetProgressModal-QjZyIAjl.js} +1 -1
  16. package/client/dist/assets/Mature-CHt_-Hz5.js +1 -0
  17. package/client/dist/assets/MediaCard-B5584Cq9.js +1 -0
  18. package/client/dist/assets/Music-HeLrEZ2d.js +1 -0
  19. package/client/dist/assets/OptionListMenu-CJ90CdwI.js +1 -0
  20. package/client/dist/assets/Player-BYtEax_z.js +56 -0
  21. package/client/dist/assets/PlayerSettings-4-cUAY4B.js +1 -0
  22. package/client/dist/assets/QueueOptionsButton-Ba0dg_82.js +1 -0
  23. package/client/dist/assets/{QueueRail-B7f1-e6i.js → QueueRail-O7f4RqjB.js} +1 -1
  24. package/client/dist/assets/Radio-DPZxQnyD.js +1 -0
  25. package/client/dist/assets/{ReadingList-BaIem8H1.js → ReadingList-n6rr8ygy.js} +1 -1
  26. package/client/dist/assets/{ResetProgressModal-DES5Y-Fl.js → ResetProgressModal-CcStlbhP.js} +1 -1
  27. package/client/dist/assets/Search-C2wrWOdy.js +1 -0
  28. package/client/dist/assets/Settings-CluUYs8U.js +21 -0
  29. package/client/dist/assets/{SkeletonGrid-BxfgjP4i.js → SkeletonGrid-CLEPY9nP.js} +1 -1
  30. package/client/dist/assets/Trackers-CTGPxs0V.js +2 -0
  31. package/client/dist/assets/Tv-BwHlDdi8.js +4 -0
  32. package/client/dist/assets/Tv-ZEA45gXK.css +1 -0
  33. package/client/dist/assets/TvInfo-CXnrLekr.js +1 -0
  34. package/client/dist/assets/TvPopup-BekfiNcX.js +1 -0
  35. package/client/dist/assets/{TvSearch-BWDwWnHe.js → TvSearch-DuqR-z3v.js} +1 -1
  36. package/client/dist/assets/{TvWatchlist-D3n5_4MI.js → TvWatchlist-CBvgFrA0.js} +1 -1
  37. package/client/dist/assets/Watchlist-mj8mTOam.js +1 -0
  38. package/client/dist/assets/{hls-BO_L1WoN.js → hls-Boje3IOE.js} +1 -1
  39. package/client/dist/assets/index-BXwhFWB_.js +178 -0
  40. package/client/dist/assets/useAnimeInfoData-DJ7A4B4T.js +1 -0
  41. package/client/dist/assets/useAsmr-DIwIsJXF.js +1 -0
  42. package/client/dist/assets/{useAsmrPopup-D_o9-QlR.js → useAsmrPopup-e4zNN9ig.js} +1 -1
  43. package/client/dist/assets/useMangaLibrary-C9Lci5N4.js +1 -0
  44. package/client/dist/assets/{useMangaPopup-TFzkC-tG.js → useMangaPopup-B8nnpvyi.js} +1 -1
  45. package/client/dist/assets/useProviders-B5Qr4I1y.js +1 -0
  46. package/client/dist/assets/useTv-Dm8MCYlL.js +1 -0
  47. package/client/dist/assets/useTvLibrary-B7jEk2Q7.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 +2 -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
@@ -0,0 +1,1039 @@
1
+ import { TRPCError } from '@trpc/server';
2
+ import { protectedProcedure, router } from '../index.js';
3
+ import { defineSchema, optStr, reqObj, reqStr } from '../validation.js';
4
+ import { parseJsonBody } from '../../utils/http.utils.js';
5
+ import { getTrending, getLatestReleases, getSeasonal, getSchedule, searchAnilist, searchAnilistByTitle, getSpotlightBanners, getBatchedHomeData, getGenreTagLists, anilistUnavailable, wasAnilistDownAtBoot, checkAnilistStatus, getShowMetaById, getAnilistEpisodes, parseMalId, getShowMetaByMalId, fromAnilistMedia, } from '../../lib/anilist.js';
6
+ import { malAnimeDetail, malSearchMedia, malSearchTitle } from '../../lib/mal.js';
7
+ import { malCacheStore } from '../../repositories/mal-cache.repository.js';
8
+ import { getMigratedId } from '../../lib/migration.js';
9
+ import { isTempShowId } from '../../lib/temp-ids.js';
10
+ import { TempShowIdsRepository } from '../../repositories/temp-show-ids.repository.js';
11
+ import { pickBestMatch } from '../../providers/title-matching.js';
12
+ import { authRequiredError } from '../errors.js';
13
+ import { ShowsMetaRepository } from '../../repositories/shows-meta.repository.js';
14
+ import { WatchlistRepository } from '../../repositories/watchlist.repository.js';
15
+ import { dbRun } from '../../utils/db-utils.js';
16
+ import logger from '../../logger.js';
17
+ function failed(message) {
18
+ return new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message });
19
+ }
20
+ function badRequest(message) {
21
+ return new TRPCError({ code: 'BAD_REQUEST', message });
22
+ }
23
+ function optInt(obj, name, fallback) {
24
+ const value = obj[name];
25
+ if (value === undefined || value === null || value === '')
26
+ return fallback;
27
+ return parseInt(String(value), 10) || fallback;
28
+ }
29
+ function optIntOrUndefined(obj, name) {
30
+ const value = obj[name];
31
+ if (value === undefined || value === null || value === '')
32
+ return undefined;
33
+ const parsed = parseInt(String(value), 10);
34
+ return Number.isFinite(parsed) ? parsed : undefined;
35
+ }
36
+ async function cached(ctx, key, ttl, validate, produce) {
37
+ const hit = ctx.apiCache.get(key);
38
+ if (hit)
39
+ return hit;
40
+ const body = await produce();
41
+ if (validate(body)) {
42
+ if (ttl !== undefined)
43
+ ctx.apiCache.set(key, body, ttl);
44
+ else
45
+ ctx.apiCache.set(key, body);
46
+ }
47
+ return body;
48
+ }
49
+ const defaultValidate = (d) => Array.isArray(d) && d.length > 0;
50
+ const dataScheduleInput = () => defineSchema((value) => {
51
+ const obj = reqObj(value);
52
+ const out = { date: reqStr(obj, 'date') };
53
+ const format = optStr(obj, 'format');
54
+ if (format !== undefined)
55
+ out.format = format;
56
+ return out;
57
+ });
58
+ const dataLatestReleasesInput = () => defineSchema((value) => {
59
+ const obj = reqObj(value);
60
+ const out = {};
61
+ const format = optStr(obj, 'format');
62
+ if (format !== undefined)
63
+ out.format = format;
64
+ if (obj.page !== undefined)
65
+ out.page = optInt(obj, 'page', 1);
66
+ if (obj.size !== undefined)
67
+ out.size = optInt(obj, 'size', 12);
68
+ return out;
69
+ });
70
+ const dataSearchInput = () => defineSchema((value) => {
71
+ const obj = reqObj(value);
72
+ const out = {};
73
+ const query = optStr(obj, 'query');
74
+ if (query !== undefined)
75
+ out.query = query;
76
+ if (obj.page !== undefined)
77
+ out.page = optInt(obj, 'page', 1);
78
+ if (obj.limit !== undefined)
79
+ out.limit = optInt(obj, 'limit', 14);
80
+ const sortBy = optStr(obj, 'sortBy');
81
+ if (sortBy !== undefined)
82
+ out.sortBy = sortBy;
83
+ const type = optStr(obj, 'type');
84
+ if (type !== undefined)
85
+ out.type = type;
86
+ const status = optStr(obj, 'status');
87
+ if (status !== undefined)
88
+ out.status = status;
89
+ const season = optStr(obj, 'season');
90
+ if (season !== undefined)
91
+ out.season = season;
92
+ const year = optIntOrUndefined(obj, 'year');
93
+ if (year !== undefined)
94
+ out.year = year;
95
+ const country = optStr(obj, 'country');
96
+ if (country !== undefined)
97
+ out.country = country;
98
+ const genres = optStr(obj, 'genres');
99
+ if (genres !== undefined)
100
+ out.genres = genres;
101
+ const excludeGenres = optStr(obj, 'excludeGenres');
102
+ if (excludeGenres !== undefined)
103
+ out.excludeGenres = excludeGenres;
104
+ const excludeTags = optStr(obj, 'excludeTags');
105
+ if (excludeTags !== undefined)
106
+ out.excludeTags = excludeTags;
107
+ const minScore = optIntOrUndefined(obj, 'minScore');
108
+ if (minScore !== undefined)
109
+ out.minScore = minScore;
110
+ const minEpisodes = optIntOrUndefined(obj, 'minEpisodes');
111
+ if (minEpisodes !== undefined)
112
+ out.minEpisodes = minEpisodes;
113
+ const adult = optStr(obj, 'adult');
114
+ if (adult !== undefined)
115
+ out.adult = adult;
116
+ return out;
117
+ });
118
+ const dataMatureResolveInput = () => defineSchema((value) => {
119
+ const obj = reqObj(value);
120
+ const out = {};
121
+ const title = optStr(obj, 'title');
122
+ if (title !== undefined)
123
+ out.title = title;
124
+ return out;
125
+ });
126
+ const dataSkipTimesInput = () => defineSchema((value) => {
127
+ const obj = reqObj(value);
128
+ const out = {};
129
+ const showId = optStr(obj, 'showId');
130
+ if (showId !== undefined)
131
+ out.showId = showId;
132
+ const episodeNumber = optStr(obj, 'episodeNumber');
133
+ if (episodeNumber !== undefined)
134
+ out.episodeNumber = episodeNumber;
135
+ return out;
136
+ });
137
+ const dataSeasonalInput = () => defineSchema((value) => {
138
+ const obj = reqObj(value);
139
+ const out = {};
140
+ if (obj.page !== undefined)
141
+ out.page = optInt(obj, 'page', 1);
142
+ if (obj.size !== undefined)
143
+ out.size = optInt(obj, 'size', 14);
144
+ const format = optStr(obj, 'format');
145
+ if (format !== undefined)
146
+ out.format = format;
147
+ return out;
148
+ });
149
+ const dataPopularListInput = () => defineSchema((value) => {
150
+ const obj = reqObj(value);
151
+ const out = {};
152
+ const sort = optStr(obj, 'sort');
153
+ if (sort !== undefined)
154
+ out.sort = sort;
155
+ if (obj.page !== undefined)
156
+ out.page = optInt(obj, 'page', 1);
157
+ if (obj.size !== undefined)
158
+ out.size = optInt(obj, 'size', 20);
159
+ return out;
160
+ });
161
+ const dataHomeInput = () => defineSchema((value) => {
162
+ if (value === undefined || value === null)
163
+ return {};
164
+ const obj = reqObj(value);
165
+ const out = {};
166
+ const format = optStr(obj, 'format');
167
+ if (format !== undefined)
168
+ out.format = format;
169
+ return out;
170
+ });
171
+ const dataEpisodesInput = () => defineSchema((value) => {
172
+ if (value === undefined || value === null)
173
+ return {};
174
+ const obj = reqObj(value);
175
+ const out = {};
176
+ const showId = optStr(obj, 'showId');
177
+ if (showId !== undefined)
178
+ out.showId = showId;
179
+ const mode = optStr(obj, 'mode');
180
+ if (mode === 'sub' || mode === 'dub')
181
+ out.mode = mode;
182
+ const provider = optStr(obj, 'provider');
183
+ if (provider !== undefined)
184
+ out.provider = provider;
185
+ return out;
186
+ });
187
+ const dataShowMetaInput = () => defineSchema((value) => ({
188
+ id: reqStr(reqObj(value), 'id'),
189
+ }));
190
+ const dataVideoInput = () => defineSchema((value) => {
191
+ if (value === undefined || value === null)
192
+ return {};
193
+ const obj = reqObj(value);
194
+ const out = {};
195
+ const showId = optStr(obj, 'showId');
196
+ if (showId !== undefined)
197
+ out.showId = showId;
198
+ const episodeNumber = optStr(obj, 'episodeNumber');
199
+ if (episodeNumber !== undefined)
200
+ out.episodeNumber = episodeNumber;
201
+ const mode = optStr(obj, 'mode');
202
+ if (mode === 'sub' || mode === 'dub')
203
+ out.mode = mode;
204
+ const provider = optStr(obj, 'provider');
205
+ if (provider !== undefined)
206
+ out.provider = provider;
207
+ return out;
208
+ });
209
+ const dataMatureSearchInput = () => defineSchema((value) => {
210
+ const obj = reqObj(value);
211
+ const out = {};
212
+ for (const k of [
213
+ 'provider',
214
+ 'query',
215
+ 'type',
216
+ 'status',
217
+ 'season',
218
+ 'country',
219
+ 'genres',
220
+ 'genre',
221
+ 'excludeGenres',
222
+ 'sortBy',
223
+ 'order',
224
+ 'studio',
225
+ 'blacklist',
226
+ ]) {
227
+ const v = optStr(obj, k);
228
+ if (v !== undefined)
229
+ out[k] = v;
230
+ }
231
+ if (obj.page !== undefined)
232
+ out.page = optInt(obj, 'page', 1);
233
+ if (obj.limit !== undefined)
234
+ out.limit = optInt(obj, 'limit', 14);
235
+ const year = optIntOrUndefined(obj, 'year');
236
+ if (year !== undefined)
237
+ out.year = year;
238
+ const minScore = optIntOrUndefined(obj, 'minScore');
239
+ if (minScore !== undefined)
240
+ out.minScore = minScore;
241
+ const minEpisodes = optIntOrUndefined(obj, 'minEpisodes');
242
+ if (minEpisodes !== undefined)
243
+ out.minEpisodes = minEpisodes;
244
+ return out;
245
+ });
246
+ const dataMatureAllocateInput = () => defineSchema((value) => {
247
+ const obj = reqObj(value);
248
+ const out = {};
249
+ const provider = optStr(obj, 'provider');
250
+ if (provider !== undefined)
251
+ out.provider = provider;
252
+ const nativeId = optStr(obj, 'nativeId');
253
+ if (nativeId !== undefined)
254
+ out.nativeId = nativeId;
255
+ const title = optStr(obj, 'title');
256
+ if (title !== undefined)
257
+ out.title = title;
258
+ const thumbnail = optStr(obj, 'thumbnail');
259
+ if (thumbnail !== undefined)
260
+ out.thumbnail = thumbnail;
261
+ return out;
262
+ });
263
+ function getProvider(ctx, queryProvider) {
264
+ const providerName = queryProvider?.toLowerCase();
265
+ if (!providerName)
266
+ return null;
267
+ return ctx.getProviders()[providerName] || null;
268
+ }
269
+ function isMatureStreamingProvider(ctx, provider) {
270
+ const key = (provider || '').toLowerCase();
271
+ const providers = ctx.getProviders();
272
+ if (!key || !providers[key] || key === 'mal' || key === 'anilist')
273
+ return false;
274
+ const mature = matureStreamingIds(ctx);
275
+ return mature ? mature.has(key) : true;
276
+ }
277
+ function matureStreamingIds(ctx) {
278
+ const catalog = ctx.getCatalog();
279
+ if (!catalog || catalog.length === 0)
280
+ return null;
281
+ return new Set(catalog
282
+ .filter((c) => c.mature && (c.kind ?? 'anime') === 'anime' && c.loaded)
283
+ .map((c) => c.id.toLowerCase()));
284
+ }
285
+ async function resolveMatureStreamingId(ctx, title, wanted) {
286
+ const providers = ctx.getProviders();
287
+ const mature = matureStreamingIds(ctx);
288
+ const pool = mature ? [...mature] : Object.keys(providers);
289
+ const names = [wanted, ...pool].filter((p) => !!p && p !== 'mal' && p !== 'anilist' && !!providers[p]);
290
+ const seen = new Set();
291
+ for (const name of names) {
292
+ if (seen.has(name))
293
+ continue;
294
+ seen.add(name);
295
+ try {
296
+ const resolved = await providers[name].resolveShowId?.(title);
297
+ if (resolved)
298
+ return { provider: providers[name], nativeId: resolved };
299
+ }
300
+ catch {
301
+ // ignore
302
+ }
303
+ }
304
+ return null;
305
+ }
306
+ function parseListParam(value) {
307
+ if (typeof value !== 'string' || !value.trim())
308
+ return undefined;
309
+ const list = value
310
+ .split(',')
311
+ .map((v) => v.trim())
312
+ .filter(Boolean);
313
+ return list.length > 0 ? list : undefined;
314
+ }
315
+ export const dataRouter = router({
316
+ schedule: protectedProcedure.input(dataScheduleInput()).query(async ({ ctx, input }) => {
317
+ const date = input.date;
318
+ const format = input.format || 'TV';
319
+ return cached(ctx, `schedule-${date}-${format}`, 1800, defaultValidate, async () => {
320
+ try {
321
+ const dateObj = new Date(date + 'T00:00:00.000Z');
322
+ const adult = format === 'ADULT';
323
+ return await getSchedule(dateObj, adult ? undefined : format, adult);
324
+ }
325
+ catch (e) {
326
+ logger.error({ err: e, date }, 'Schedule fetch failed');
327
+ return [];
328
+ }
329
+ });
330
+ }),
331
+ latestReleases: protectedProcedure
332
+ .input(dataLatestReleasesInput())
333
+ .query(async ({ ctx, input }) => {
334
+ const formatStr = input.format || 'TV';
335
+ const pageNum = input.page || 1;
336
+ const sizeNum = input.size || 12;
337
+ return cached(ctx, `latest-releases-${formatStr}-${pageNum}-${sizeNum}`, 300, defaultValidate, async () => {
338
+ try {
339
+ return await getLatestReleases(formatStr, pageNum, sizeNum);
340
+ }
341
+ catch (e) {
342
+ logger.error({ err: e }, 'Latest releases fetch failed');
343
+ return [];
344
+ }
345
+ });
346
+ }),
347
+ search: protectedProcedure.input(dataSearchInput()).query(async ({ ctx, input }) => {
348
+ const queryEntries = {};
349
+ if (input.query !== undefined)
350
+ queryEntries.query = input.query;
351
+ if (input.page !== undefined)
352
+ queryEntries.page = String(input.page);
353
+ if (input.limit !== undefined)
354
+ queryEntries.limit = String(input.limit);
355
+ if (input.sortBy !== undefined)
356
+ queryEntries.sortBy = input.sortBy;
357
+ if (input.type !== undefined)
358
+ queryEntries.type = input.type;
359
+ if (input.status !== undefined)
360
+ queryEntries.status = input.status;
361
+ if (input.season !== undefined)
362
+ queryEntries.season = input.season;
363
+ if (input.year !== undefined)
364
+ queryEntries.year = String(input.year);
365
+ if (input.country !== undefined)
366
+ queryEntries.country = input.country;
367
+ if (input.genres !== undefined)
368
+ queryEntries.genres = input.genres;
369
+ if (input.excludeGenres !== undefined)
370
+ queryEntries.excludeGenres = input.excludeGenres;
371
+ if (input.excludeTags !== undefined)
372
+ queryEntries.excludeTags = input.excludeTags;
373
+ if (input.minScore !== undefined)
374
+ queryEntries.minScore = String(input.minScore);
375
+ if (input.minEpisodes !== undefined)
376
+ queryEntries.minEpisodes = String(input.minEpisodes);
377
+ if (input.adult !== undefined)
378
+ queryEntries.adult = input.adult;
379
+ return cached(ctx, `search-${JSON.stringify(queryEntries)}`, 1800, defaultValidate, async () => {
380
+ try {
381
+ return await searchAnilist({
382
+ query: input.query || '',
383
+ page: input.page || 1,
384
+ perPage: input.limit || 14,
385
+ format: input.type,
386
+ status: input.status,
387
+ season: input.season,
388
+ seasonYear: input.year,
389
+ countryOfOrigin: input.country,
390
+ genre: input.genres,
391
+ genre_not_in: parseListParam(input.excludeGenres),
392
+ tag_not_in: parseListParam(input.excludeTags),
393
+ averageScore_greater: input.minScore,
394
+ episodes_greater: input.minEpisodes,
395
+ isAdult: input.adult === 'true' ? true : input.adult === 'false' ? false : undefined,
396
+ sort: input.sortBy,
397
+ });
398
+ }
399
+ catch (e) {
400
+ logger.error({ err: e }, 'search failed');
401
+ return [];
402
+ }
403
+ });
404
+ }),
405
+ matureResolve: protectedProcedure
406
+ .input(dataMatureResolveInput())
407
+ .query(async ({ ctx, input }) => {
408
+ return cached(ctx, `mature-resolve-${input.title || ''}`, 3600, defaultValidate, async () => {
409
+ try {
410
+ const title = (input.title || '').trim();
411
+ if (!title)
412
+ throw badRequest('title is required');
413
+ const anilistResult = await searchAnilistByTitle(title);
414
+ if (anilistResult) {
415
+ const id = anilistResult.id;
416
+ return { id: typeof id === 'number' && id < 0 ? `mal-${Math.abs(id)}` : id };
417
+ }
418
+ const malResult = await malSearchTitle(malCacheStore(), title);
419
+ if (malResult)
420
+ return { id: malResult, provider: 'mal' };
421
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'No match found' });
422
+ }
423
+ catch (e) {
424
+ if (e instanceof TRPCError)
425
+ throw e;
426
+ logger.error({ err: e }, 'mature resolve failed');
427
+ throw failed('Resolve failed');
428
+ }
429
+ });
430
+ }),
431
+ skipTimes: protectedProcedure.input(dataSkipTimesInput()).query(async ({ input }) => {
432
+ try {
433
+ const showId = input.showId || '';
434
+ const episodeNumber = input.episodeNumber || '';
435
+ if (/^\d+$/.test(showId)) {
436
+ const skipRes = await fetch(`https://api.aniskip.com/v1/skip-times/${showId}/${episodeNumber}?types=op&types=ed`);
437
+ if (skipRes.ok) {
438
+ return await parseJsonBody(skipRes);
439
+ }
440
+ }
441
+ return { found: false, results: [] };
442
+ }
443
+ catch {
444
+ return { found: false, results: [] };
445
+ }
446
+ }),
447
+ seasonal: protectedProcedure.input(dataSeasonalInput()).query(async ({ ctx, input }) => {
448
+ const pageNum = input.page || 1;
449
+ const sizeNum = input.size || 14;
450
+ const formatStr = input.format;
451
+ return cached(ctx, `seasonal-${formatStr || 'ALL'}-${pageNum}-${sizeNum}`, 300, defaultValidate, async () => {
452
+ try {
453
+ return await getSeasonal(pageNum, sizeNum, formatStr);
454
+ }
455
+ catch (e) {
456
+ logger.error({ err: e }, 'Seasonal fetch failed');
457
+ return [];
458
+ }
459
+ });
460
+ }),
461
+ popularList: protectedProcedure.input(dataPopularListInput()).query(async ({ ctx, input }) => {
462
+ const sort = input.sort === 'POPULARITY_DESC' ? 'POPULARITY_DESC' : 'TRENDING_DESC';
463
+ const pageNum = input.page || 1;
464
+ const sizeNum = input.size || 20;
465
+ return cached(ctx, `popular-list-${input.sort || 'TRENDING_DESC'}-${pageNum}-${sizeNum}`, 300, defaultValidate, async () => {
466
+ try {
467
+ return await getTrending(pageNum, sizeNum, sort);
468
+ }
469
+ catch (e) {
470
+ logger.error({ err: e }, 'Popular list fetch failed');
471
+ return [];
472
+ }
473
+ });
474
+ }),
475
+ trending: protectedProcedure.query(async ({ ctx }) => {
476
+ return cached(ctx, 'trending', 300, defaultValidate, async () => {
477
+ try {
478
+ return await getTrending(1, 20, 'TRENDING_DESC', 'RELEASING');
479
+ }
480
+ catch (e) {
481
+ logger.error({ err: e }, 'Trending fetch failed');
482
+ return [];
483
+ }
484
+ });
485
+ }),
486
+ spotlight: protectedProcedure.query(async ({ ctx }) => {
487
+ return cached(ctx, 'spotlight', 300, defaultValidate, async () => {
488
+ try {
489
+ return await getSpotlightBanners(1, 20);
490
+ }
491
+ catch (e) {
492
+ logger.error({ err: e }, 'Spotlight fetch failed');
493
+ return [];
494
+ }
495
+ });
496
+ }),
497
+ home: protectedProcedure.input(dataHomeInput()).query(async ({ ctx, input }) => {
498
+ const formatStr = input.format;
499
+ return cached(ctx, `home-${formatStr || 'TV'}`, 300, defaultValidate, async () => {
500
+ try {
501
+ return await getBatchedHomeData(formatStr);
502
+ }
503
+ catch (e) {
504
+ logger.error({ err: e }, 'Batched home fetch failed');
505
+ return { trending: [], seasonal: [], spotlight: [] };
506
+ }
507
+ });
508
+ }),
509
+ genresAndTags: protectedProcedure.query(async () => {
510
+ try {
511
+ return await getGenreTagLists();
512
+ }
513
+ catch (e) {
514
+ logger.error({ err: e }, 'genres-and-tags failed');
515
+ throw failed('Failed to load genres');
516
+ }
517
+ }),
518
+ anilistStatus: protectedProcedure.query(async () => {
519
+ const available = !anilistUnavailable();
520
+ if (!available) {
521
+ checkAnilistStatus().catch(() => { });
522
+ }
523
+ return { available, wasDownAtBoot: wasAnilistDownAtBoot() };
524
+ }),
525
+ systemNotifications: protectedProcedure.query(async () => {
526
+ const notifications = [];
527
+ if (wasAnilistDownAtBoot() && anilistUnavailable()) {
528
+ const fb = 'MAL + Kitsu';
529
+ notifications.push({
530
+ id: 'system-anilist-down',
531
+ type: 'system',
532
+ title: 'AniList API',
533
+ 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.`,
534
+ icon: 'warning',
535
+ createdAt: Date.now(),
536
+ });
537
+ }
538
+ return notifications;
539
+ }),
540
+ episodes: protectedProcedure.input(dataEpisodesInput()).query(async ({ ctx, input }) => {
541
+ const showIdParam = input.showId;
542
+ return cached(ctx, `episodes-${showIdParam || ''}`, 3600, defaultValidate, async () => {
543
+ const showIdRaw = showIdParam;
544
+ if (!showIdRaw)
545
+ return { episodes: [] };
546
+ const db = ctx.db;
547
+ const providers = ctx.getProviders();
548
+ let showId = await getMigratedId(db, showIdRaw);
549
+ const epsMalId = parseMalId(showId);
550
+ if (epsMalId) {
551
+ try {
552
+ const malMeta = await getShowMetaByMalId(epsMalId);
553
+ if (malMeta?.anilistId && malMeta.anilistId > 0)
554
+ showId = String(malMeta.anilistId);
555
+ }
556
+ catch {
557
+ // ignore
558
+ }
559
+ }
560
+ if (isTempShowId(showId)) {
561
+ try {
562
+ const row = await TempShowIdsRepository.getById(db, showId);
563
+ if (!row || !isMatureStreamingProvider(ctx, row.provider))
564
+ return { episodes: [] };
565
+ const wanted = String(input.provider || '').toLowerCase();
566
+ const own = providers[row.provider];
567
+ if (own && (!wanted || wanted === row.provider)) {
568
+ const data = await own.getEpisodes(row.nativeId, input.mode);
569
+ if (data?.episodes?.length)
570
+ return data;
571
+ return { episodes: [] };
572
+ }
573
+ const hit = await resolveMatureStreamingId(ctx, row.title, wanted);
574
+ if (hit) {
575
+ const data = await hit.provider.getEpisodes(hit.nativeId, input.mode);
576
+ if (data?.episodes?.length)
577
+ return data;
578
+ }
579
+ }
580
+ catch {
581
+ // ignore
582
+ }
583
+ return { episodes: [] };
584
+ }
585
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(showId)) {
586
+ try {
587
+ if (providers['animepahe']) {
588
+ const data = await providers['animepahe'].getEpisodes(showId, input.mode);
589
+ return data || { episodes: [] };
590
+ }
591
+ }
592
+ catch {
593
+ return { episodes: [] };
594
+ }
595
+ }
596
+ const isNumeric = /^(mal-\d+|-?\d+)$/i.test(showId);
597
+ if (isNumeric) {
598
+ let episodes = [];
599
+ try {
600
+ episodes = await getAnilistEpisodes(showId);
601
+ }
602
+ catch (e) {
603
+ logger.error({ err: e, showId }, 'Episodes fetch failed');
604
+ }
605
+ if (episodes.length === 0) {
606
+ const absId = Math.abs(parseInt(showId, 10));
607
+ if (!Number.isFinite(absId))
608
+ return { episodes };
609
+ try {
610
+ const d = await malAnimeDetail(malCacheStore(), absId);
611
+ const total = d.detail?.episodes;
612
+ if (total && total > 0 && total <= 500) {
613
+ episodes = Array.from({ length: total }, (_, i) => (i + 1).toString());
614
+ }
615
+ }
616
+ catch {
617
+ // ignore
618
+ }
619
+ }
620
+ if (episodes.length === 0) {
621
+ const providerName = input.provider?.toLowerCase();
622
+ const provider = providerName ? providers[providerName] : undefined;
623
+ if (provider?.isDirectId?.(showId)) {
624
+ try {
625
+ const data = await provider.getEpisodes(showId, input.mode);
626
+ if (data?.episodes?.length)
627
+ return data;
628
+ }
629
+ catch {
630
+ // ignore
631
+ }
632
+ }
633
+ }
634
+ return { episodes };
635
+ }
636
+ const providerName = input.provider?.toLowerCase();
637
+ const provider = providerName ? providers[providerName] : undefined;
638
+ if (provider) {
639
+ try {
640
+ const data = await provider.getEpisodes(showId, input.mode);
641
+ if (data?.episodes?.length)
642
+ return data;
643
+ }
644
+ catch {
645
+ // ignore
646
+ }
647
+ }
648
+ return { episodes: [] };
649
+ });
650
+ }),
651
+ showMeta: protectedProcedure.input(dataShowMetaInput()).query(async ({ ctx, input }) => {
652
+ const showIdRaw = input.id;
653
+ return cached(ctx, `meta-${showIdRaw}`, 3600, (d) => !!d, async () => {
654
+ const db = ctx.db;
655
+ const id = await getMigratedId(db, showIdRaw);
656
+ if (isTempShowId(id)) {
657
+ const row = await TempShowIdsRepository.getById(db, id);
658
+ if (!row || !isMatureStreamingProvider(ctx, row.provider))
659
+ return {};
660
+ return {
661
+ _id: row.id,
662
+ id: row.id,
663
+ name: row.title,
664
+ englishName: row.title,
665
+ thumbnail: row.thumbnail || '',
666
+ type: 'TV',
667
+ isAdult: true,
668
+ status: 'UNKNOWN',
669
+ provider: row.provider,
670
+ nativeId: row.nativeId,
671
+ };
672
+ }
673
+ const isNumeric = /^(mal-\d+|-?\d+)$/i.test(id);
674
+ if (isNumeric) {
675
+ let meta = null;
676
+ try {
677
+ meta = await getShowMetaById(id);
678
+ }
679
+ catch (e) {
680
+ logger.warn({ err: e, id }, 'AniList show-meta fetch failed, trying local cache');
681
+ const localMeta = (await ShowsMetaRepository.getById(db, id));
682
+ if (localMeta) {
683
+ if (typeof localMeta.genres === 'string') {
684
+ try {
685
+ localMeta.genres = JSON.parse(localMeta.genres);
686
+ }
687
+ catch {
688
+ localMeta.genres = [];
689
+ }
690
+ }
691
+ return localMeta;
692
+ }
693
+ }
694
+ if (meta) {
695
+ const poster = meta.thumbnail?.trim() ? meta.thumbnail : undefined;
696
+ const hasPoster = !!poster;
697
+ try {
698
+ await ShowsMetaRepository.upsert(db, {
699
+ id,
700
+ name: meta.name,
701
+ thumbnail: poster,
702
+ nativeName: meta.nativeName,
703
+ englishName: meta.englishName,
704
+ popularityScore: meta.score ?? (meta.averageScore != null ? meta.averageScore / 10 : undefined),
705
+ genres: meta.genres
706
+ ? JSON.stringify(meta.genres.map((g) => (typeof g === 'string' ? g : g?.name)).filter(Boolean))
707
+ : undefined,
708
+ status: meta.status,
709
+ episodeCount: meta.episodeCount != null ? Number(meta.episodeCount) : undefined,
710
+ type: meta.type,
711
+ anilistId: meta.anilistId,
712
+ });
713
+ }
714
+ catch {
715
+ // ignore
716
+ }
717
+ if (hasPoster && poster) {
718
+ const existingWatchlist = (await WatchlistRepository.getById(db, id));
719
+ if (existingWatchlist && existingWatchlist.thumbnail !== poster) {
720
+ try {
721
+ await WatchlistRepository.updateThumbnail(db, id, poster);
722
+ }
723
+ catch {
724
+ // ignore
725
+ }
726
+ }
727
+ db.scheduleSave();
728
+ }
729
+ }
730
+ if (/^(mal-\d+|-\d+)$/i.test(id) && meta?.anilistId && meta.anilistId > 0) {
731
+ try {
732
+ dbRun(db, 'INSERT OR REPLACE INTO legacy_id_mapping (legacyId, numericId) VALUES (?, ?)', [id, String(meta.anilistId)]);
733
+ }
734
+ catch {
735
+ // ignore
736
+ }
737
+ }
738
+ return meta || {};
739
+ }
740
+ return {};
741
+ });
742
+ }),
743
+ matureSearch: protectedProcedure.input(dataMatureSearchInput()).query(async ({ ctx, input }) => {
744
+ const provider = (input.provider || 'anilist').toLowerCase();
745
+ if (provider !== 'anilist' && provider !== 'mal' && !isMatureStreamingProvider(ctx, provider)) {
746
+ throw badRequest('Unknown mature provider');
747
+ }
748
+ const queryEntries = {};
749
+ for (const [k, v] of Object.entries(input)) {
750
+ if (v !== undefined)
751
+ queryEntries[k] = String(v);
752
+ }
753
+ return cached(ctx, `mature-search-${JSON.stringify(queryEntries)}`, 600, (d) => !!d && typeof d === 'object' && Array.isArray(d.data), async () => {
754
+ try {
755
+ const query = input.query || '';
756
+ const page = input.page || 1;
757
+ const limit = input.limit || 14;
758
+ if (provider === 'anilist') {
759
+ const result = await searchAnilist({
760
+ query,
761
+ page,
762
+ perPage: limit,
763
+ format: input.type,
764
+ status: input.status,
765
+ season: input.season,
766
+ seasonYear: input.year,
767
+ countryOfOrigin: input.country,
768
+ genre: input.genres,
769
+ genre_not_in: parseListParam(input.excludeGenres),
770
+ isAdult: true,
771
+ sort: input.sortBy,
772
+ });
773
+ return { data: result, hasMore: result.length >= limit };
774
+ }
775
+ if (isMatureStreamingProvider(ctx, provider)) {
776
+ const streaming = ctx.getProviders()[provider];
777
+ if (streaming?.browse) {
778
+ const genre = input.genre || input.genres || undefined;
779
+ const result = await streaming.browse({
780
+ query,
781
+ page,
782
+ limit,
783
+ pageSize: limit,
784
+ genre,
785
+ genres: input.genres || genre,
786
+ order: input.order || input.sortBy || undefined,
787
+ sort: input.sortBy || input.order || undefined,
788
+ studio: input.studio || undefined,
789
+ blacklist: input.blacklist || undefined,
790
+ });
791
+ return {
792
+ data: result.shows,
793
+ hasMore: result.hasMore,
794
+ ...(result.total !== undefined ? { total: result.total } : {}),
795
+ ...(result.genres ? { genres: result.genres } : {}),
796
+ };
797
+ }
798
+ const shows = await streaming.search({ query });
799
+ return { data: shows.map((s) => ({ ...s, isAdult: true })), hasMore: false };
800
+ }
801
+ if (provider === 'mal') {
802
+ let result = [];
803
+ try {
804
+ result = await malSearchMedia(malCacheStore(), {
805
+ query,
806
+ page,
807
+ perPage: 50,
808
+ format: input.type,
809
+ status: input.status,
810
+ genre: input.genres || undefined,
811
+ genre_not_in: parseListParam(input.excludeGenres),
812
+ isAdult: true,
813
+ sort: input.sortBy,
814
+ averageScore_greater: input.minScore,
815
+ episodes_greater: input.minEpisodes,
816
+ });
817
+ }
818
+ catch (e) {
819
+ logger.error({ err: e }, 'mal mature search failed');
820
+ }
821
+ const shows = result.map((m) => {
822
+ const show = fromAnilistMedia(m);
823
+ return { ...show, isAdult: true };
824
+ });
825
+ const sliced = shows.slice(0, limit);
826
+ return { data: sliced, hasMore: shows.length >= limit };
827
+ }
828
+ return { data: [], hasMore: false };
829
+ }
830
+ catch (e) {
831
+ logger.error({ err: e }, 'mature search failed');
832
+ return { data: [], hasMore: false };
833
+ }
834
+ });
835
+ }),
836
+ matureFilters: protectedProcedure.query(async ({ ctx }) => {
837
+ try {
838
+ const catalog = ctx.getCatalog() ?? [];
839
+ const providers = {};
840
+ for (const item of catalog) {
841
+ if (!item.mature || (item.kind ?? 'anime') !== 'anime' || !item.loaded)
842
+ continue;
843
+ providers[item.id] = {
844
+ label: item.label,
845
+ ...(item.browse ? { browse: item.browse } : {}),
846
+ ...(item.facets?.genres?.length ? { genres: item.facets.genres } : {}),
847
+ ...(item.facets?.orders?.length ? { orders: item.facets.orders } : {}),
848
+ };
849
+ }
850
+ return { providers };
851
+ }
852
+ catch (e) {
853
+ logger.error({ err: e }, 'mature filters failed');
854
+ throw failed('Failed to load filters');
855
+ }
856
+ }),
857
+ matureAllocate: protectedProcedure
858
+ .input(dataMatureAllocateInput())
859
+ .mutation(async ({ ctx, input }) => {
860
+ try {
861
+ const provider = String(input.provider || '').toLowerCase();
862
+ const nativeId = String(input.nativeId || '').trim();
863
+ const title = String(input.title || '').trim();
864
+ const thumbnail = String(input.thumbnail || '');
865
+ if (!provider || !isMatureStreamingProvider(ctx, provider)) {
866
+ throw badRequest('Unknown provider');
867
+ }
868
+ if (!nativeId || !title) {
869
+ throw badRequest('nativeId and title are required');
870
+ }
871
+ const row = await TempShowIdsRepository.allocate(ctx.db, {
872
+ provider,
873
+ nativeId,
874
+ title,
875
+ thumbnail,
876
+ });
877
+ return { id: row.id };
878
+ }
879
+ catch (e) {
880
+ if (e instanceof TRPCError)
881
+ throw e;
882
+ logger.error({ err: e }, 'temp show allocate failed');
883
+ throw failed('Allocate failed');
884
+ }
885
+ }),
886
+ video: protectedProcedure.input(dataVideoInput()).query(async ({ ctx, input }) => {
887
+ const db = ctx.db;
888
+ const providers = ctx.getProviders();
889
+ try {
890
+ let showId = input.showId;
891
+ const videoMalId = parseMalId(showId);
892
+ if (videoMalId) {
893
+ try {
894
+ const malMeta = await getShowMetaByMalId(videoMalId);
895
+ if (malMeta?.anilistId && malMeta.anilistId > 0)
896
+ showId = String(malMeta.anilistId);
897
+ }
898
+ catch {
899
+ // ignore
900
+ }
901
+ }
902
+ if (isTempShowId(showId)) {
903
+ try {
904
+ const row = await TempShowIdsRepository.getById(db, showId);
905
+ if (!row || !isMatureStreamingProvider(ctx, row.provider))
906
+ return [];
907
+ const wanted = String(input.provider || '').toLowerCase();
908
+ const own = providers[row.provider];
909
+ if (own && (!wanted || wanted === row.provider)) {
910
+ const urls = await own.getStreamUrls(row.nativeId, input.episodeNumber, input.mode);
911
+ return urls || [];
912
+ }
913
+ const hit = await resolveMatureStreamingId(ctx, row.title, wanted);
914
+ if (hit) {
915
+ const urls = await hit.provider.getStreamUrls(hit.nativeId, input.episodeNumber, input.mode);
916
+ return urls || [];
917
+ }
918
+ }
919
+ catch (e) {
920
+ logger.error({ err: e, showId }, 'Temp show video fetch failed');
921
+ }
922
+ return [];
923
+ }
924
+ const providerName = input.provider;
925
+ const providerKey = providerName?.toLowerCase();
926
+ const stillMal = parseMalId(showId) !== null;
927
+ const isNativeId = !!(providerKey && providers[providerKey]?.isDirectId?.(showId));
928
+ const needsResolution = !isNativeId && (/^\d+$/.test(showId) || stillMal);
929
+ if (providerKey && needsResolution) {
930
+ const meta = (await ShowsMetaRepository.getById(db, showId));
931
+ let targetTitle = meta?.englishName || meta?.name;
932
+ let anilistShow = null;
933
+ if (!targetTitle) {
934
+ try {
935
+ anilistShow = await getShowMetaById(showId);
936
+ targetTitle = anilistShow?.englishName || anilistShow?.name;
937
+ if (anilistShow && targetTitle) {
938
+ await ShowsMetaRepository.upsert(db, {
939
+ id: showId,
940
+ name: anilistShow.name,
941
+ thumbnail: anilistShow.thumbnail,
942
+ nativeName: anilistShow.nativeName,
943
+ englishName: anilistShow.englishName,
944
+ genres: anilistShow.genres
945
+ ? JSON.stringify(anilistShow.genres.map((genre) => genre.name))
946
+ : undefined,
947
+ status: anilistShow.status,
948
+ episodeCount: anilistShow.episodeCount != null ? Number(anilistShow.episodeCount) : undefined,
949
+ type: anilistShow.type,
950
+ anilistId: anilistShow.anilistId,
951
+ });
952
+ }
953
+ }
954
+ catch (err) {
955
+ logger.warn({ err, provider: providerKey, showId }, '[Video] AniList metadata lookup failed while resolving numeric showId');
956
+ }
957
+ }
958
+ if (targetTitle) {
959
+ let romaji = anilistShow?.names?.romaji;
960
+ let nativeName = anilistShow?.names?.native;
961
+ let synonyms = anilistShow?.names?.synonyms;
962
+ if (!romaji || !nativeName) {
963
+ try {
964
+ const show = await getShowMetaById(showId);
965
+ romaji = romaji || show?.names?.romaji;
966
+ nativeName = nativeName || show?.names?.native;
967
+ synonyms = synonyms || show?.names?.synonyms;
968
+ }
969
+ catch {
970
+ // ignore
971
+ }
972
+ }
973
+ const titleTargets = [
974
+ targetTitle,
975
+ romaji,
976
+ nativeName,
977
+ ...(synonyms ?? []).slice(0, 2),
978
+ ].filter((t) => !!t && t.trim().length > 0);
979
+ const distinctTargets = [...new Set(titleTargets.map((t) => t.trim()))].filter((t, i, a) => a.findIndex((x) => x.toLowerCase() === t.toLowerCase()) === i);
980
+ let resolved = null;
981
+ for (const variant of distinctTargets) {
982
+ try {
983
+ resolved =
984
+ (await providers[providerKey]?.resolveShowId?.(variant, romaji, input.mode)) ?? null;
985
+ }
986
+ catch {
987
+ resolved = null;
988
+ }
989
+ if (resolved)
990
+ break;
991
+ }
992
+ if (resolved) {
993
+ showId = resolved;
994
+ }
995
+ else {
996
+ logger.warn({ provider: providerKey, showId, title: targetTitle, romaji }, '[Video] resolveShowId failed, attempting fallback provider search');
997
+ try {
998
+ const fallbackResults = await providers[providerKey]?.search?.({ query: targetTitle });
999
+ const fallbackMatch = pickBestMatch((fallbackResults || []).map((r) => ({
1000
+ title: r.name || r.englishName || '',
1001
+ id: r.id || r._id || '',
1002
+ })), distinctTargets);
1003
+ if (fallbackMatch) {
1004
+ showId = fallbackMatch.item.id;
1005
+ }
1006
+ else {
1007
+ logger.warn({ provider: providerKey, showId, title: targetTitle }, '[Video] fallback provider search returned no results');
1008
+ return [];
1009
+ }
1010
+ }
1011
+ catch (fallbackErr) {
1012
+ if (fallbackErr.message === 'AUTH_REQUIRED') {
1013
+ throw fallbackErr;
1014
+ }
1015
+ logger.error({ err: fallbackErr, provider: providerKey, showId, title: targetTitle }, '[Video] fallback provider search failed');
1016
+ return [];
1017
+ }
1018
+ }
1019
+ }
1020
+ else {
1021
+ logger.warn({ provider: providerKey, showId }, '[Video] numeric showId passed to provider but local meta missing title');
1022
+ return [];
1023
+ }
1024
+ }
1025
+ const provider = getProvider(ctx, input.provider);
1026
+ if (!provider)
1027
+ return [];
1028
+ const urls = await provider.getStreamUrls(showId, input.episodeNumber, input.mode);
1029
+ return urls || [];
1030
+ }
1031
+ catch (e) {
1032
+ if (e.message === 'AUTH_REQUIRED') {
1033
+ throw authRequiredError('animepahe');
1034
+ }
1035
+ logger.error({ err: e, provider: input.provider }, 'Provider video fetch failed');
1036
+ return [];
1037
+ }
1038
+ }),
1039
+ });