@serifpersia/dango 3.3.4

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 (190) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +258 -0
  3. package/client/dist/assets/AnimeCard-B5A20jAI.js +1 -0
  4. package/client/dist/assets/AnimeInfo-0huPAq0O.js +1 -0
  5. package/client/dist/assets/AnimeInfo-BFWtHlPC.css +1 -0
  6. package/client/dist/assets/AnimeInfoPage-Dqr5QH6R.js +2 -0
  7. package/client/dist/assets/AnimeMetaDetails-BPvv5xIl.js +1 -0
  8. package/client/dist/assets/AnimeMetaDetails-BwNQnRCd.css +1 -0
  9. package/client/dist/assets/Asmr-B6ZiqkmU.js +2 -0
  10. package/client/dist/assets/Asmr-Cq2ua6Zk.css +1 -0
  11. package/client/dist/assets/Asmr.module-ChUXDZKx.js +1 -0
  12. package/client/dist/assets/AsmrResetProgressModal-Cqsoo3XC.js +1 -0
  13. package/client/dist/assets/AvSyncCalibrator-C1_iBSyL.css +1 -0
  14. package/client/dist/assets/AvSyncCalibrator-C8ZoIwMY.js +10 -0
  15. package/client/dist/assets/ErrorMessage-CXoRbrLN.css +1 -0
  16. package/client/dist/assets/ErrorMessage-z4f4WGgP.js +1 -0
  17. package/client/dist/assets/Home-DtRtTuI9.css +1 -0
  18. package/client/dist/assets/Home-thJhPfs1.js +1 -0
  19. package/client/dist/assets/Insights-DKtoVe6L.css +1 -0
  20. package/client/dist/assets/Insights-ZlgPrzGD.js +1 -0
  21. package/client/dist/assets/LibraryListPage-CCiee-Ot.js +1 -0
  22. package/client/dist/assets/ListeningList-ctAz_6Zi.js +1 -0
  23. package/client/dist/assets/Manga-BI9WMTx5.js +1 -0
  24. package/client/dist/assets/Manga-Dp_-l1Pw.css +1 -0
  25. package/client/dist/assets/Manga.module-BcfOdsNP.js +1 -0
  26. package/client/dist/assets/MangaInfoPage-DcrEr7Sk.js +1 -0
  27. package/client/dist/assets/MangaReadPage-SYg63m48.js +1 -0
  28. package/client/dist/assets/MangaResetProgressModal-DX-1PS4h.js +1 -0
  29. package/client/dist/assets/Map-CXFqXmsZ.css +1 -0
  30. package/client/dist/assets/Map-Dj51g5zL.js +1 -0
  31. package/client/dist/assets/Mature-DJmUUtlF.css +1 -0
  32. package/client/dist/assets/Mature-Dqh_9SA3.js +1 -0
  33. package/client/dist/assets/MediaCard-Be1ST9oj.js +1 -0
  34. package/client/dist/assets/MediaCard-DB6Yjgky.css +1 -0
  35. package/client/dist/assets/Music-BGH_0voi.css +1 -0
  36. package/client/dist/assets/Music-zsPwT8_5.js +1 -0
  37. package/client/dist/assets/OptionListMenu-Drc_vybW.js +1 -0
  38. package/client/dist/assets/Player-B036Ic3m.css +1 -0
  39. package/client/dist/assets/Player-BEtAYps2.js +56 -0
  40. package/client/dist/assets/PlayerRedirect-D70t8VUW.js +1 -0
  41. package/client/dist/assets/PlayerSettings-Bj6SsmQ2.css +1 -0
  42. package/client/dist/assets/PlayerSettings-Cwoji-P7.js +1 -0
  43. package/client/dist/assets/QueueOptionsButton-B6BMq66e.js +1 -0
  44. package/client/dist/assets/QueueOptionsButton-CM15uQn8.css +1 -0
  45. package/client/dist/assets/QueueRail-BUYEjdaq.css +1 -0
  46. package/client/dist/assets/QueueRail-VxpUDBSm.js +1 -0
  47. package/client/dist/assets/Radio-B2Q_Egj7.js +1 -0
  48. package/client/dist/assets/Radio-CCHNFN7R.css +1 -0
  49. package/client/dist/assets/Radio.module-DGSZLpKM.js +1 -0
  50. package/client/dist/assets/ReadingList-O0QPwXu7.js +1 -0
  51. package/client/dist/assets/ResetProgressModal-CR_ovLAu.js +1 -0
  52. package/client/dist/assets/Search-C8e2nlDD.js +1 -0
  53. package/client/dist/assets/Search-Cs45oAwI.css +1 -0
  54. package/client/dist/assets/Search.module-CMEai-B-.js +1 -0
  55. package/client/dist/assets/Settings-AuFxQcJF.css +1 -0
  56. package/client/dist/assets/Settings-BgPWBl2V.js +21 -0
  57. package/client/dist/assets/SkeletonGrid-CtHKKMLj.js +1 -0
  58. package/client/dist/assets/SkeletonGrid-DhPYefXr.css +1 -0
  59. package/client/dist/assets/SynopsisText-BcpwT5iw.js +1 -0
  60. package/client/dist/assets/SynopsisText-CYv2W3yR.css +1 -0
  61. package/client/dist/assets/Trackers-Bfoiyuzy.js +2 -0
  62. package/client/dist/assets/Trackers-D5XBOCDp.css +1 -0
  63. package/client/dist/assets/Tv-Ca0fCdko.js +3 -0
  64. package/client/dist/assets/Tv-CuEl3Iol.css +1 -0
  65. package/client/dist/assets/TvInfo-Bu36xJyO.css +1 -0
  66. package/client/dist/assets/TvInfo-D_scgw_h.js +1 -0
  67. package/client/dist/assets/TvPopup-YTI7dEbJ.js +1 -0
  68. package/client/dist/assets/TvSearch-CxU33UOf.js +1 -0
  69. package/client/dist/assets/TvWatchlist-B4X1fWEH.js +1 -0
  70. package/client/dist/assets/VirtualKeyboard-6ZNTym4R.js +1 -0
  71. package/client/dist/assets/VirtualKeyboard-KIPc5ZGH.css +1 -0
  72. package/client/dist/assets/Watchlist-BiDEU0iV.css +1 -0
  73. package/client/dist/assets/Watchlist-P6YMZUdF.js +1 -0
  74. package/client/dist/assets/Watchlist.module-XGtVQr1d.js +1 -0
  75. package/client/dist/assets/hls-BOLXTnjL.js +42 -0
  76. package/client/dist/assets/hls-C4cG9wQw.js +1 -0
  77. package/client/dist/assets/index-BOKlZEHq.js +30354 -0
  78. package/client/dist/assets/index-CUm1meqr.css +1 -0
  79. package/client/dist/assets/index-IZgh1beL.js +178 -0
  80. package/client/dist/assets/useAnimeInfoData-DeuwDSnI.js +1 -0
  81. package/client/dist/assets/useAsmr-CbMVRjmV.js +1 -0
  82. package/client/dist/assets/useAsmrPopup-D91Q3CBg.js +1 -0
  83. package/client/dist/assets/useIsMobile-Dm_b6Ob3.js +1 -0
  84. package/client/dist/assets/useMangaLibrary-BPvFb1lU.js +1 -0
  85. package/client/dist/assets/useMangaPopup-BOL2mQJs.js +1 -0
  86. package/client/dist/assets/useMatureConsent-CT7-KfzG.js +1 -0
  87. package/client/dist/assets/useProviders-BsRGONMA.js +1 -0
  88. package/client/dist/assets/useTv-D37MfTHA.js +1 -0
  89. package/client/dist/assets/useTvLibrary-Cwxgy_Bg.js +1 -0
  90. package/client/dist/assets/vendor-core-8R2fFbyN.js +1 -0
  91. package/client/dist/assets/vendor-icons-CDMuNDeL.js +1 -0
  92. package/client/dist/assets/vendor-query-BXRECSI6.js +1 -0
  93. package/client/dist/assets/vendor-ui-CUDCvRS_.js +1 -0
  94. package/client/dist/av-sync-test.mp4 +0 -0
  95. package/client/dist/favicon.ico +0 -0
  96. package/client/dist/index.html +46 -0
  97. package/client/dist/logo-mochi.png +0 -0
  98. package/client/dist/logo-sakura.png +0 -0
  99. package/client/dist/logo.png +0 -0
  100. package/client/dist/placeholder.svg +4 -0
  101. package/client/dist/robots.txt +3 -0
  102. package/orchestrator.js +534 -0
  103. package/package.json +85 -0
  104. package/server/dist/app-auth.js +177 -0
  105. package/server/dist/config.js +175 -0
  106. package/server/dist/controllers/asmr-library.controller.js +288 -0
  107. package/server/dist/controllers/auth.controller.js +225 -0
  108. package/server/dist/controllers/data.controller.js +766 -0
  109. package/server/dist/controllers/insights.controller.js +198 -0
  110. package/server/dist/controllers/manga-library.controller.js +296 -0
  111. package/server/dist/controllers/music.controller.js +698 -0
  112. package/server/dist/controllers/proxy.controller.js +820 -0
  113. package/server/dist/controllers/settings.controller.js +609 -0
  114. package/server/dist/controllers/tv-library.controller.js +310 -0
  115. package/server/dist/controllers/watchlist.controller.js +1015 -0
  116. package/server/dist/db.js +145 -0
  117. package/server/dist/discord-gateway.js +391 -0
  118. package/server/dist/discord-rpc.js +427 -0
  119. package/server/dist/github-sync.js +512 -0
  120. package/server/dist/google.js +748 -0
  121. package/server/dist/lib/anilist.js +1549 -0
  122. package/server/dist/lib/anischedule.js +194 -0
  123. package/server/dist/lib/discord-roles-sync.service.js +130 -0
  124. package/server/dist/lib/ipc.js +24 -0
  125. package/server/dist/lib/kitsu.js +544 -0
  126. package/server/dist/lib/mal.js +934 -0
  127. package/server/dist/lib/migration.js +329 -0
  128. package/server/dist/lib/offline-db.js +401 -0
  129. package/server/dist/lib/temp-ids.js +3 -0
  130. package/server/dist/lib/tmdb.js +72 -0
  131. package/server/dist/lib/tracker/anilist-tracker.js +245 -0
  132. package/server/dist/lib/tracker/sync.service.js +496 -0
  133. package/server/dist/lib/ytmusic.js +125 -0
  134. package/server/dist/logger.js +16 -0
  135. package/server/dist/providers/manga/manga.types.js +7 -0
  136. package/server/dist/providers/provider.interface.js +1 -0
  137. package/server/dist/providers/remote-loader.js +574 -0
  138. package/server/dist/providers/remote-types.js +117 -0
  139. package/server/dist/providers/title-matching.js +225 -0
  140. package/server/dist/providers/tv.types.js +1 -0
  141. package/server/dist/rclone.js +120 -0
  142. package/server/dist/repositories/asmr.repository.js +123 -0
  143. package/server/dist/repositories/insights.repository.js +85 -0
  144. package/server/dist/repositories/library.repository.js +108 -0
  145. package/server/dist/repositories/mal-cache.repository.js +55 -0
  146. package/server/dist/repositories/manga.repository.js +138 -0
  147. package/server/dist/repositories/notifications.repository.js +41 -0
  148. package/server/dist/repositories/queue.repository.js +61 -0
  149. package/server/dist/repositories/settings.repository.js +12 -0
  150. package/server/dist/repositories/shows-meta.repository.js +42 -0
  151. package/server/dist/repositories/temp-show-ids.repository.js +51 -0
  152. package/server/dist/repositories/tv.repository.js +151 -0
  153. package/server/dist/repositories/watched-episodes.repository.js +52 -0
  154. package/server/dist/repositories/watchlist.repository.js +71 -0
  155. package/server/dist/request-logger.js +16 -0
  156. package/server/dist/routes/asmr-library.routes.js +23 -0
  157. package/server/dist/routes/asmr.routes.js +81 -0
  158. package/server/dist/routes/auth.routes.js +25 -0
  159. package/server/dist/routes/data.routes.js +47 -0
  160. package/server/dist/routes/discord-gateway.routes.js +51 -0
  161. package/server/dist/routes/insights.routes.js +34 -0
  162. package/server/dist/routes/lan-auth.routes.js +46 -0
  163. package/server/dist/routes/manga-library.routes.js +23 -0
  164. package/server/dist/routes/manga.routes.js +141 -0
  165. package/server/dist/routes/music.routes.js +20 -0
  166. package/server/dist/routes/providers.routes.js +17 -0
  167. package/server/dist/routes/proxy.routes.js +12 -0
  168. package/server/dist/routes/radio.routes.js +138 -0
  169. package/server/dist/routes/settings.routes.js +24 -0
  170. package/server/dist/routes/tracker.routes.js +164 -0
  171. package/server/dist/routes/translate.routes.js +88 -0
  172. package/server/dist/routes/tv-library.routes.js +23 -0
  173. package/server/dist/routes/tv.routes.js +629 -0
  174. package/server/dist/routes/watchlist.routes.js +341 -0
  175. package/server/dist/scripts/backfill-episode-duration.js +42 -0
  176. package/server/dist/server.js +529 -0
  177. package/server/dist/shipped-defaults.js +7 -0
  178. package/server/dist/sync-config.js +23 -0
  179. package/server/dist/sync-payload.js +91 -0
  180. package/server/dist/sync.js +1128 -0
  181. package/server/dist/utils/cache.utils.js +25 -0
  182. package/server/dist/utils/cookie.utils.js +13 -0
  183. package/server/dist/utils/db-utils.js +9 -0
  184. package/server/dist/utils/env.utils.js +77 -0
  185. package/server/dist/utils/http.utils.js +70 -0
  186. package/server/dist/utils/machine-id.js +40 -0
  187. package/server/dist/utils/megaplay.utils.js +41 -0
  188. package/server/dist/utils/request-context.js +2 -0
  189. package/server/dist/utils/security.utils.js +94 -0
  190. package/server/package.json +44 -0
@@ -0,0 +1,310 @@
1
+ import logger from '../logger.js';
2
+ import { performTvWriteTransaction } from '../sync.js';
3
+ import { dbAll } from '../utils/db-utils.js';
4
+ import { SettingsRepository } from '../repositories/settings.repository.js';
5
+ import { buildTvId, normalizeTvMediaId, TV_STATUSES, TvLibraryRepository, TvProgressRepository, } from '../repositories/tv.repository.js';
6
+ const isTvContinueAdult = (row) => row.adult === true || row.adult === 1;
7
+ function tvDb(req) {
8
+ const db = req.tvDb;
9
+ if (!db || db.isClosedCheck())
10
+ throw new Error('TV database is not ready');
11
+ return db;
12
+ }
13
+ function normalizeMediaType(raw) {
14
+ return String(raw).toLowerCase() === 'movie' ? 'movie' : 'tv';
15
+ }
16
+ export class TvLibraryController {
17
+ getLibrary = async (req, res) => {
18
+ try {
19
+ const { status, page: pageStr, limit: limitStr } = req.query;
20
+ const page = Math.max(parseInt(pageStr) || 1, 1);
21
+ const limit = Math.min(Math.max(parseInt(limitStr) || 24, 1), 100);
22
+ const offset = (page - 1) * limit;
23
+ const db = tvDb(req);
24
+ const rows = TvLibraryRepository.getAll(db, status, limit, offset);
25
+ const total = TvLibraryRepository.getCount(db, status);
26
+ res.json({ data: rows, total, page, limit });
27
+ }
28
+ catch (err) {
29
+ logger.error({ err }, 'Failed to get TV library');
30
+ res.status(500).json({ error: 'Failed to load TV library' });
31
+ }
32
+ };
33
+ checkLibrary = async (req, res) => {
34
+ try {
35
+ const item = TvLibraryRepository.getById(tvDb(req), req.params.id);
36
+ res.json({ inLibrary: !!item, status: item?.status ?? null });
37
+ }
38
+ catch {
39
+ res.json({ inLibrary: false, status: null });
40
+ }
41
+ };
42
+ getLibraryIds = async (req, res) => {
43
+ try {
44
+ res.json({ ids: TvLibraryRepository.getIds(tvDb(req)) });
45
+ }
46
+ catch {
47
+ res.json({ ids: [] });
48
+ }
49
+ };
50
+ addToLibrary = async (req, res) => {
51
+ const { tmdbId, mediaType: mediaTypeRaw, id: idRaw, title, poster, backdrop, year, overview, status, adult, } = req.body ?? {};
52
+ const tmdbIdNum = Number(tmdbId);
53
+ if (!tmdbIdNum || (!idRaw && !mediaTypeRaw)) {
54
+ return res.status(400).json({ error: 'tmdbId and mediaType are required' });
55
+ }
56
+ const mediaType = normalizeMediaType(mediaTypeRaw || (String(idRaw).startsWith('movie:') ? 'movie' : 'tv'));
57
+ const id = idRaw && String(idRaw).includes(':') ? String(idRaw) : buildTvId(mediaType, tmdbIdNum);
58
+ const cleanStatus = typeof status === 'string' && TV_STATUSES.includes(status) ? status : 'Watching';
59
+ try {
60
+ await performTvWriteTransaction(tvDb(req), (tx) => {
61
+ TvLibraryRepository.upsert(tx, {
62
+ id,
63
+ tmdbId: tmdbIdNum,
64
+ mediaType,
65
+ title: String(title || ''),
66
+ poster: String(poster || ''),
67
+ backdrop: backdrop ? String(backdrop) : undefined,
68
+ year: year ? String(year) : undefined,
69
+ overview: overview ? String(overview) : undefined,
70
+ status: cleanStatus,
71
+ adult: adult === true,
72
+ });
73
+ });
74
+ res.json({ success: true, id });
75
+ }
76
+ catch (err) {
77
+ logger.error({ err }, 'Failed to add TV title to library');
78
+ res.status(500).json({ error: 'Failed to add to TV watchlist' });
79
+ }
80
+ };
81
+ removeFromLibrary = async (req, res) => {
82
+ const { id } = req.body ?? {};
83
+ if (!id)
84
+ return res.status(400).json({ error: 'id is required' });
85
+ try {
86
+ await performTvWriteTransaction(tvDb(req), (tx) => {
87
+ TvLibraryRepository.delete(tx, String(id));
88
+ TvProgressRepository.deleteByMedia(tx, String(id));
89
+ });
90
+ res.json({ success: true });
91
+ }
92
+ catch (err) {
93
+ logger.error({ err }, 'Failed to remove TV title from library');
94
+ res.status(500).json({ error: 'Failed to remove from TV watchlist' });
95
+ }
96
+ };
97
+ updateStatus = async (req, res) => {
98
+ const { id, status } = req.body ?? {};
99
+ if (!id || !TV_STATUSES.includes(status)) {
100
+ return res.status(400).json({ error: 'id and a valid status are required' });
101
+ }
102
+ try {
103
+ await performTvWriteTransaction(tvDb(req), (tx) => {
104
+ TvLibraryRepository.updateStatus(tx, String(id), String(status));
105
+ });
106
+ res.json({ success: true });
107
+ }
108
+ catch (err) {
109
+ logger.error({ err }, 'Failed to update TV status');
110
+ res.status(500).json({ error: 'Failed to update status' });
111
+ }
112
+ };
113
+ getProgress = async (req, res) => {
114
+ try {
115
+ const rows = TvProgressRepository.getByMedia(tvDb(req), normalizeTvMediaId(req.params.mediaId));
116
+ res.json({ progress: rows });
117
+ }
118
+ catch {
119
+ res.json({ progress: [] });
120
+ }
121
+ };
122
+ getLatestProgress = async (req, res) => {
123
+ try {
124
+ const db = tvDb(req);
125
+ const mediaId = normalizeTvMediaId(req.params.mediaId);
126
+ const season = parseInt(req.query.season, 10);
127
+ const episode = parseInt(req.query.episode, 10);
128
+ if (Number.isFinite(season) && Number.isFinite(episode)) {
129
+ const row = TvProgressRepository.getEpisode(db, mediaId, season, episode);
130
+ res.json(row || { currentTime: 0, duration: 0 });
131
+ return;
132
+ }
133
+ const row = TvProgressRepository.getLatest(db, mediaId);
134
+ res.json(row || { currentTime: 0, duration: 0 });
135
+ }
136
+ catch {
137
+ res.json({ currentTime: 0, duration: 0 });
138
+ }
139
+ };
140
+ saveProgress = async (req, res) => {
141
+ const { mediaId: mediaIdRaw, season, episode, currentTime, duration, title, poster, backdrop, year, overview, tmdbId, mediaType, adult, } = req.body ?? {};
142
+ const mediaId = normalizeTvMediaId(mediaIdRaw);
143
+ if (!mediaId) {
144
+ return res.status(400).json({ error: 'mediaId is required' });
145
+ }
146
+ const seasonNum = Math.max(Number(season) || 1, 1);
147
+ const episodeNum = Math.max(Number(episode) || 1, 1);
148
+ const timeNum = Math.max(Number(currentTime) || 0, 0);
149
+ const durationNum = Math.max(Number(duration) || 0, 0);
150
+ try {
151
+ await performTvWriteTransaction(tvDb(req), (tx) => {
152
+ TvProgressRepository.upsert(tx, {
153
+ mediaId: String(mediaId),
154
+ season: Math.round(seasonNum),
155
+ episode: Math.round(episodeNum),
156
+ currentTime: timeNum,
157
+ duration: durationNum,
158
+ title: title ?? null,
159
+ poster: poster ?? null,
160
+ backdrop: backdrop ?? null,
161
+ year: year ?? null,
162
+ overview: overview ?? null,
163
+ tmdbId: tmdbId != null ? Number(tmdbId) : null,
164
+ mediaType: mediaType ?? null,
165
+ adult: adult != null ? Number(adult) : null,
166
+ });
167
+ TvLibraryRepository.touchProgress(tx, String(mediaId), {
168
+ season: Math.round(seasonNum),
169
+ episode: Math.round(episodeNum),
170
+ });
171
+ });
172
+ res.json({ success: true });
173
+ }
174
+ catch (err) {
175
+ logger.error({ err }, 'Failed to save TV progress');
176
+ res.status(500).json({ error: 'Failed to save progress' });
177
+ }
178
+ };
179
+ getContinueWatching = async (req, res) => {
180
+ try {
181
+ const limit = Math.min(Math.max(parseInt(req.query.limit) || 24, 1), 100);
182
+ const ignoreAdultRow = await SettingsRepository.getByKey(req.db, 'tvIgnoreAdultContent');
183
+ const ignoreAdult = ignoreAdultRow ? ignoreAdultRow.value !== 'false' : true;
184
+ const listOnlyRow = await SettingsRepository.getByKey(req.db, 'tvCwWatchlistOnly');
185
+ const listOnly = listOnlyRow
186
+ ? listOnlyRow.value === 'true' || listOnlyRow.value === '1'
187
+ : false;
188
+ const rows = TvProgressRepository.getContinueWatching(tvDb(req), limit).filter((row) => {
189
+ if (ignoreAdult && isTvContinueAdult(row))
190
+ return false;
191
+ if (listOnly && row.watchlistStatus !== 'Watching')
192
+ return false;
193
+ return true;
194
+ });
195
+ res.json({ data: rows, total: rows.length });
196
+ }
197
+ catch {
198
+ res.json({ data: [], total: 0 });
199
+ }
200
+ };
201
+ async getAdultNonListMediaIds(db) {
202
+ const rows = await dbAll(db, `SELECT DISTINCT p.mediaId as mediaId
203
+ FROM tv_progress p
204
+ LEFT JOIN tv_library l ON l.id = p.mediaId
205
+ WHERE COALESCE(l.adult, p.adult) = 1
206
+ AND l.id IS NULL`);
207
+ return rows.map((r) => r.mediaId);
208
+ }
209
+ getAdultContinueWatchingCount = async (req, res) => {
210
+ try {
211
+ const ids = await this.getAdultNonListMediaIds(tvDb(req));
212
+ res.json({ count: ids.length });
213
+ }
214
+ catch {
215
+ res.json({ count: 0 });
216
+ }
217
+ };
218
+ purgeAdultContinueWatching = async (req, res) => {
219
+ try {
220
+ const ids = await this.getAdultNonListMediaIds(tvDb(req));
221
+ if (ids.length > 0) {
222
+ await performTvWriteTransaction(tvDb(req), (tx) => {
223
+ for (const id of ids)
224
+ TvProgressRepository.deleteByMedia(tx, id);
225
+ });
226
+ }
227
+ res.json({ success: true, removed: ids.length });
228
+ }
229
+ catch (err) {
230
+ logger.error({ err }, 'Failed to purge adult TV progress');
231
+ res.status(500).json({ error: 'Failed to purge adult entries' });
232
+ }
233
+ };
234
+ removeProgress = async (req, res) => {
235
+ const { mediaId: mediaIdRaw, season, episode } = req.body ?? {};
236
+ const mediaId = normalizeTvMediaId(mediaIdRaw);
237
+ if (!mediaId)
238
+ return res.status(400).json({ error: 'mediaId is required' });
239
+ try {
240
+ await performTvWriteTransaction(tvDb(req), (tx) => {
241
+ if (season !== undefined && episode !== undefined) {
242
+ TvProgressRepository.deleteEpisode(tx, String(mediaId), Number(season), Number(episode));
243
+ }
244
+ else {
245
+ TvProgressRepository.deleteByMedia(tx, String(mediaId));
246
+ }
247
+ });
248
+ res.json({ success: true });
249
+ }
250
+ catch (err) {
251
+ logger.error({ err }, 'Failed to remove TV progress');
252
+ res.status(500).json({ error: 'Failed to reset progress' });
253
+ }
254
+ };
255
+ batchUpdateStatus = async (req, res) => {
256
+ const { ids: idsRaw, status } = req.body ?? {};
257
+ if (!Array.isArray(idsRaw) || idsRaw.length === 0) {
258
+ return res.status(400).json({ error: 'ids must be a non-empty array' });
259
+ }
260
+ if (!status || !TV_STATUSES.includes(status)) {
261
+ return res.status(400).json({ error: 'a valid status is required' });
262
+ }
263
+ const ids = idsRaw.map((id) => String(id));
264
+ try {
265
+ await performTvWriteTransaction(tvDb(req), (tx) => {
266
+ TvLibraryRepository.updateStatusMany(tx, ids, String(status));
267
+ });
268
+ res.json({ success: true, updated: ids.length });
269
+ }
270
+ catch (err) {
271
+ logger.error({ err }, 'Failed to batch update TV status');
272
+ res.status(500).json({ error: 'Failed to update statuses' });
273
+ }
274
+ };
275
+ batchRemove = async (req, res) => {
276
+ const { ids: idsRaw } = req.body ?? {};
277
+ if (!Array.isArray(idsRaw) || idsRaw.length === 0) {
278
+ return res.status(400).json({ error: 'ids must be a non-empty array' });
279
+ }
280
+ const ids = idsRaw.map((id) => String(id));
281
+ try {
282
+ await performTvWriteTransaction(tvDb(req), (tx) => {
283
+ TvLibraryRepository.deleteMany(tx, ids);
284
+ TvProgressRepository.deleteMany(tx, ids);
285
+ });
286
+ res.json({ success: true, removed: ids.length });
287
+ }
288
+ catch (err) {
289
+ logger.error({ err }, 'Failed to batch remove TV titles');
290
+ res.status(500).json({ error: 'Failed to remove from TV watchlist' });
291
+ }
292
+ };
293
+ batchRemoveProgress = async (req, res) => {
294
+ const { ids: idsRaw } = req.body ?? {};
295
+ if (!Array.isArray(idsRaw) || idsRaw.length === 0) {
296
+ return res.status(400).json({ error: 'ids must be a non-empty array' });
297
+ }
298
+ const ids = idsRaw.map((id) => normalizeTvMediaId(id));
299
+ try {
300
+ await performTvWriteTransaction(tvDb(req), (tx) => {
301
+ TvProgressRepository.deleteMany(tx, ids);
302
+ });
303
+ res.json({ success: true, removed: ids.length });
304
+ }
305
+ catch (err) {
306
+ logger.error({ err }, 'Failed to batch remove TV progress');
307
+ res.status(500).json({ error: 'Failed to reset progress' });
308
+ }
309
+ };
310
+ }