@serifpersia/dango 3.6.7 → 3.6.9

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 (156) hide show
  1. package/README.md +1 -1
  2. package/client/dist/assets/{AnimeCard-C3P9zWi9.js → AnimeCard-DKDJK8Mp.js} +1 -1
  3. package/client/dist/assets/{AnimeInfo-Et6687Tl.js → AnimeInfo-CTcvfjva.js} +1 -1
  4. package/client/dist/assets/{AnimeInfoPage-DX4eNnlH.js → AnimeInfoPage-B_UiacIN.js} +2 -2
  5. package/client/dist/assets/{Asmr-CCdEWrFi.js → Asmr-DMSN2vxl.js} +2 -2
  6. package/client/dist/assets/{AsmrResetProgressModal-DgEWBJOw.js → AsmrResetProgressModal-CRwfOatX.js} +1 -1
  7. package/client/dist/assets/{AvSyncCalibrator-CQ2fjf2E.js → AvSyncCalibrator-CRJsi-vz.js} +1 -1
  8. package/client/dist/assets/{Home-DUkUTTTy.js → Home-pwmK9_Eh.js} +1 -1
  9. package/client/dist/assets/{Insights-QCh4xW12.js → Insights-m7kFzg2A.js} +1 -1
  10. package/client/dist/assets/{LibraryListPage-BoTxRqHz.js → LibraryListPage-C1wpPvu6.js} +1 -1
  11. package/client/dist/assets/ListeningList-OzfEI1Ga.js +1 -0
  12. package/client/dist/assets/{Manga-HydR-L7o.js → Manga-ChRCExhK.js} +1 -1
  13. package/client/dist/assets/{MangaInfoPage-CBakEI9a.js → MangaInfoPage-DSHJPo2A.js} +1 -1
  14. package/client/dist/assets/{MangaReadPage-DwAR3iPy.js → MangaReadPage-CzOVY1wA.js} +1 -1
  15. package/client/dist/assets/{MangaResetProgressModal-DGzl8vsO.js → MangaResetProgressModal-CJfzdUBb.js} +1 -1
  16. package/client/dist/assets/{Mature-Pi0TxKuF.js → Mature-CifQMfhs.js} +1 -1
  17. package/client/dist/assets/{MediaCard--cuFxOCS.js → MediaCard-CM10yB9V.js} +1 -1
  18. package/client/dist/assets/{OptionListMenu-B3D39dGS.js → OptionListMenu-6Kovbsqw.js} +1 -1
  19. package/client/dist/assets/{Player-CsH4x_I_.js → Player-CK0hT-u4.js} +2 -2
  20. package/client/dist/assets/{PlayerSettings-DGxBsi3F.js → PlayerSettings-Bb66uqT9.js} +1 -1
  21. package/client/dist/assets/{QueueOptionsButton-Djq4Cpex.js → QueueOptionsButton-C_nTj8_q.js} +1 -1
  22. package/client/dist/assets/{QueueRail-DOrYgNVX.js → QueueRail-DUK7URAE.js} +1 -1
  23. package/client/dist/assets/{ReadingList-DwYoBrNk.js → ReadingList-Ck9LJThU.js} +1 -1
  24. package/client/dist/assets/{ResetProgressModal-CMF86mX6.js → ResetProgressModal-BUphIv0k.js} +1 -1
  25. package/client/dist/assets/{Search-DFkhuk-x.js → Search-zJyP15h6.js} +1 -1
  26. package/client/dist/assets/{Settings-DBB2r0VQ.js → Settings-BYdoSEUh.js} +1 -1
  27. package/client/dist/assets/{SkeletonGrid-DJelJgAv.js → SkeletonGrid-CqgbOhjX.js} +1 -1
  28. package/client/dist/assets/{Trackers-DGYXFhYZ.js → Trackers-BVjyv6CM.js} +1 -1
  29. package/client/dist/assets/{Tv-C5MDGD2Z.js → Tv-L13ylswR.js} +1 -1
  30. package/client/dist/assets/{TvInfo-DrKuK13u.js → TvInfo-Cvcm5U2D.js} +1 -1
  31. package/client/dist/assets/{TvPopup-DtHilRDT.js → TvPopup-DhxfKvYm.js} +1 -1
  32. package/client/dist/assets/{TvSearch-CzNxdu_w.js → TvSearch-P8bFE6_X.js} +1 -1
  33. package/client/dist/assets/{TvWatchlist-D7p4br6S.js → TvWatchlist-D5gVPjtb.js} +1 -1
  34. package/client/dist/assets/{Watchlist-DrOzswl2.js → Watchlist-Cjq8l3rU.js} +1 -1
  35. package/client/dist/assets/{hls-BGCX7mJy.js → hls-BkzuANAg.js} +1 -1
  36. package/client/dist/assets/{index-CnfZwsds.js → index-CLVgzKLk.js} +19 -19
  37. package/client/dist/assets/{useAnimeInfoData-DYXWBPF-.js → useAnimeInfoData-DZJZu-so.js} +1 -1
  38. package/client/dist/assets/{useAsmr-BbhQ59bk.js → useAsmr-CvrMGHuo.js} +1 -1
  39. package/client/dist/assets/useAsmrPopup-Bimy8Ouw.js +1 -0
  40. package/client/dist/assets/{useMangaLibrary-DuesWQaE.js → useMangaLibrary--MiN11uR.js} +1 -1
  41. package/client/dist/assets/{useMangaPopup-D57OVGUr.js → useMangaPopup-CR5XhKKS.js} +1 -1
  42. package/client/dist/assets/{useProviders-mpQb_dix.js → useProviders-DqdT4fA5.js} +1 -1
  43. package/client/dist/assets/{useTv-DGflCsSI.js → useTv-BL0HMDyP.js} +1 -1
  44. package/client/dist/assets/{useTvLibrary-BqeSmyc4.js → useTvLibrary-D4U4iqN8.js} +1 -1
  45. package/client/dist/index.html +1 -1
  46. package/package.json +3 -5
  47. package/server/dist/config.js +1 -174
  48. package/server/dist/server.js +1068 -289
  49. package/server/dist/shipped-defaults.js +1 -6
  50. package/server/package.json +4 -5
  51. package/client/dist/assets/Asmr.module-CdWlioTY.js +0 -1
  52. package/client/dist/assets/ListeningList-DguKjI_t.js +0 -1
  53. package/client/dist/assets/Music-BGH_0voi.css +0 -1
  54. package/client/dist/assets/Music-CQpyXTLH.js +0 -1
  55. package/client/dist/assets/Radio-CCHNFN7R.css +0 -1
  56. package/client/dist/assets/Radio-DF6g6odd.js +0 -1
  57. package/client/dist/assets/Radio.module-DGSZLpKM.js +0 -1
  58. package/client/dist/assets/useAsmrPopup-SymV1J-b.js +0 -1
  59. package/server/dist/app-auth.js +0 -157
  60. package/server/dist/app-hono.js +0 -598
  61. package/server/dist/db/drizzle.js +0 -16
  62. package/server/dist/db/schema-anime.js +0 -61
  63. package/server/dist/db/schema-asmr.js +0 -13
  64. package/server/dist/db/schema-manga.js +0 -31
  65. package/server/dist/db/schema-tv.js +0 -18
  66. package/server/dist/db.js +0 -217
  67. package/server/dist/discord-gateway.js +0 -385
  68. package/server/dist/discord-rpc.js +0 -413
  69. package/server/dist/github-sync.js +0 -508
  70. package/server/dist/google.js +0 -741
  71. package/server/dist/hono/asmr.js +0 -10
  72. package/server/dist/hono/auth.js +0 -49
  73. package/server/dist/hono/music.js +0 -209
  74. package/server/dist/hono/proxy.js +0 -827
  75. package/server/dist/hono/tv.js +0 -200
  76. package/server/dist/hono/watchlist.js +0 -341
  77. package/server/dist/lib/anilist.js +0 -1583
  78. package/server/dist/lib/anischedule.js +0 -191
  79. package/server/dist/lib/insights.js +0 -185
  80. package/server/dist/lib/ipc.js +0 -23
  81. package/server/dist/lib/kitsu.js +0 -517
  82. package/server/dist/lib/mal-import.js +0 -291
  83. package/server/dist/lib/mal.js +0 -957
  84. package/server/dist/lib/migration.js +0 -310
  85. package/server/dist/lib/offline-db.js +0 -395
  86. package/server/dist/lib/potoken.js +0 -135
  87. package/server/dist/lib/recommendations.service.js +0 -216
  88. package/server/dist/lib/temp-ids.js +0 -3
  89. package/server/dist/lib/tmdb.js +0 -72
  90. package/server/dist/lib/tracker/anilist-tracker.js +0 -308
  91. package/server/dist/lib/tracker/manga-sync.service.js +0 -773
  92. package/server/dist/lib/tracker/sync-state.js +0 -21
  93. package/server/dist/lib/tracker/sync.service.js +0 -476
  94. package/server/dist/lib/translate.js +0 -79
  95. package/server/dist/lib/ump.js +0 -106
  96. package/server/dist/lib/ytmusic.js +0 -162
  97. package/server/dist/logger.js +0 -16
  98. package/server/dist/providers/manga/manga.types.js +0 -1
  99. package/server/dist/providers/provider.interface.js +0 -1
  100. package/server/dist/providers/remote-loader.js +0 -614
  101. package/server/dist/providers/remote-types.js +0 -159
  102. package/server/dist/providers/title-matching.js +0 -226
  103. package/server/dist/providers/tv.types.js +0 -1
  104. package/server/dist/rclone.js +0 -120
  105. package/server/dist/repositories/asmr.repository.js +0 -77
  106. package/server/dist/repositories/insights.repository.js +0 -106
  107. package/server/dist/repositories/library.repository.js +0 -94
  108. package/server/dist/repositories/mal-cache.repository.js +0 -52
  109. package/server/dist/repositories/manga.repository.js +0 -95
  110. package/server/dist/repositories/notifications.repository.js +0 -60
  111. package/server/dist/repositories/queue.repository.js +0 -72
  112. package/server/dist/repositories/settings.repository.js +0 -30
  113. package/server/dist/repositories/shows-meta.repository.js +0 -58
  114. package/server/dist/repositories/temp-show-ids.repository.js +0 -63
  115. package/server/dist/repositories/tv.repository.js +0 -93
  116. package/server/dist/repositories/watched-episodes.repository.js +0 -82
  117. package/server/dist/repositories/watchlist.repository.js +0 -60
  118. package/server/dist/scripts/backfill-episode-duration.js +0 -42
  119. package/server/dist/sync-payload.js +0 -91
  120. package/server/dist/sync.js +0 -657
  121. package/server/dist/trpc/context.js +0 -7
  122. package/server/dist/trpc/errors.js +0 -15
  123. package/server/dist/trpc/index.js +0 -18
  124. package/server/dist/trpc/router.js +0 -49
  125. package/server/dist/trpc/routers/asmr-library.js +0 -112
  126. package/server/dist/trpc/routers/asmr-progress.js +0 -121
  127. package/server/dist/trpc/routers/asmr.js +0 -87
  128. package/server/dist/trpc/routers/auth.js +0 -194
  129. package/server/dist/trpc/routers/continue-watching.js +0 -81
  130. package/server/dist/trpc/routers/data.js +0 -1026
  131. package/server/dist/trpc/routers/discord.js +0 -285
  132. package/server/dist/trpc/routers/discovery.js +0 -11
  133. package/server/dist/trpc/routers/insights.js +0 -14
  134. package/server/dist/trpc/routers/manga-library.js +0 -263
  135. package/server/dist/trpc/routers/manga-progress.js +0 -169
  136. package/server/dist/trpc/routers/manga.js +0 -201
  137. package/server/dist/trpc/routers/music.js +0 -655
  138. package/server/dist/trpc/routers/notifications.js +0 -80
  139. package/server/dist/trpc/routers/progress.js +0 -109
  140. package/server/dist/trpc/routers/queue.js +0 -93
  141. package/server/dist/trpc/routers/radio.js +0 -145
  142. package/server/dist/trpc/routers/settings.js +0 -96
  143. package/server/dist/trpc/routers/tracker.js +0 -213
  144. package/server/dist/trpc/routers/tv-library.js +0 -135
  145. package/server/dist/trpc/routers/tv-progress.js +0 -159
  146. package/server/dist/trpc/routers/tv.js +0 -551
  147. package/server/dist/trpc/routers/watchlist.js +0 -550
  148. package/server/dist/trpc/validation.js +0 -258
  149. package/server/dist/utils/cache.utils.js +0 -25
  150. package/server/dist/utils/cookie.utils.js +0 -13
  151. package/server/dist/utils/db-utils.js +0 -17
  152. package/server/dist/utils/env.utils.js +0 -77
  153. package/server/dist/utils/http.utils.js +0 -82
  154. package/server/dist/utils/request-context.js +0 -2
  155. package/server/dist/utils/security.utils.js +0 -76
  156. /package/client/dist/assets/{Asmr-D1KuQwQy.css → useAsmrPopup-D1KuQwQy.css} +0 -0
@@ -1,213 +0,0 @@
1
- import { TRPCError } from '@trpc/server';
2
- import { protectedProcedure, router } from '../index.js';
3
- import { SettingsRepository } from '../../repositories/settings.repository.js';
4
- import { performWriteTransactionAsync } from '../../sync.js';
5
- import { AniListTracker } from '../../lib/tracker/anilist-tracker.js';
6
- import { syncAniList, importFromMalUsername, importFromUsername, } from '../../lib/tracker/sync.service.js';
7
- import { syncAniListManga, importFromUsernameManga, importMangaFromMalUsername, } from '../../lib/tracker/manga-sync.service.js';
8
- import { badRequest, defineSchema, failed, reqObj } from '../validation.js';
9
- const TRACKER_TOKEN_KEY = 'tracker_anilist_token';
10
- const TRACKER_USER_KEY = 'tracker_anilist_user';
11
- function unavailable(message) {
12
- return new TRPCError({ code: 'SERVICE_UNAVAILABLE', message });
13
- }
14
- function optBody(value) {
15
- if (value === undefined || value === null)
16
- return {};
17
- return reqObj(value);
18
- }
19
- const trackerTokenInput = () => defineSchema((value) => {
20
- const obj = optBody(value);
21
- return { token: obj.token, code: obj.code, redirectUri: obj.redirectUri };
22
- });
23
- const trackerSyncInput = () => defineSchema((value) => ({
24
- provider: optBody(value).provider,
25
- }));
26
- const trackerUsernameInput = () => defineSchema((value) => {
27
- const obj = optBody(value);
28
- return { username: obj.username, erase: obj.erase };
29
- });
30
- const trackerMalInput = () => defineSchema((value) => {
31
- const obj = optBody(value);
32
- return {
33
- username: obj.username,
34
- erase: obj.erase,
35
- useOfflineDb: obj.useOfflineDb,
36
- skipFallback: obj.skipFallback,
37
- };
38
- });
39
- const trackerMangaSyncInput = () => defineSchema((value) => ({
40
- direction: optBody(value).direction,
41
- }));
42
- function mapImportError(message) {
43
- if (message.includes('private') || message.includes('not found')) {
44
- return new TRPCError({ code: 'NOT_FOUND', message });
45
- }
46
- if (message.includes('blocked') || message.includes('HTTP 429')) {
47
- return new TRPCError({ code: 'TOO_MANY_REQUESTS', message });
48
- }
49
- return failed(message);
50
- }
51
- export const trackerRouter = router({
52
- status: protectedProcedure.query(async ({ ctx }) => {
53
- try {
54
- const tokenRow = await SettingsRepository.getByKey(ctx.db, TRACKER_TOKEN_KEY);
55
- const userRow = await SettingsRepository.getByKey(ctx.db, TRACKER_USER_KEY);
56
- let user = null;
57
- if (userRow?.value) {
58
- try {
59
- user = JSON.parse(userRow.value);
60
- }
61
- catch {
62
- user = null;
63
- }
64
- }
65
- return { anilist: { connected: !!tokenRow?.value, user } };
66
- }
67
- catch {
68
- throw failed('Failed to read tracker status');
69
- }
70
- }),
71
- anilistAuth: protectedProcedure.input(trackerTokenInput()).mutation(async ({ ctx, input }) => {
72
- const accessToken = typeof input.token === 'string' ? input.token.trim() : '';
73
- if (!accessToken) {
74
- throw badRequest('Access token is required');
75
- }
76
- try {
77
- const tracker = new AniListTracker(accessToken);
78
- const viewer = await tracker.getViewer();
79
- await performWriteTransactionAsync(ctx.db, async (tx) => {
80
- await SettingsRepository.upsert(tx, TRACKER_TOKEN_KEY, accessToken);
81
- await SettingsRepository.upsert(tx, TRACKER_USER_KEY, JSON.stringify(viewer));
82
- });
83
- return { success: true, user: viewer };
84
- }
85
- catch (err) {
86
- throw new TRPCError({
87
- code: 'UNAUTHORIZED',
88
- message: err instanceof Error ? err.message : 'Authentication failed',
89
- });
90
- }
91
- }),
92
- anilistDisconnect: protectedProcedure.mutation(async ({ ctx }) => {
93
- try {
94
- await performWriteTransactionAsync(ctx.db, async (tx) => {
95
- await SettingsRepository.upsert(tx, TRACKER_TOKEN_KEY, '');
96
- await SettingsRepository.upsert(tx, TRACKER_USER_KEY, '');
97
- });
98
- return { success: true };
99
- }
100
- catch {
101
- throw failed('Failed to disconnect');
102
- }
103
- }),
104
- sync: protectedProcedure.input(trackerSyncInput()).mutation(async ({ ctx, input }) => {
105
- const { provider = 'anilist' } = input;
106
- if (provider !== 'anilist') {
107
- throw badRequest(`Provider "${provider}" is not supported yet`);
108
- }
109
- try {
110
- const summary = await syncAniList(ctx.db);
111
- return { success: true, summary };
112
- }
113
- catch (err) {
114
- throw failed(err instanceof Error ? err.message : 'Sync failed');
115
- }
116
- }),
117
- anilistImport: protectedProcedure
118
- .input(trackerUsernameInput())
119
- .mutation(async ({ ctx, input }) => {
120
- const { username, erase } = input;
121
- if (!username || typeof username !== 'string') {
122
- throw badRequest('Username is required');
123
- }
124
- try {
125
- const count = await importFromUsername(ctx.db, username.trim(), erase === true);
126
- return { success: true, count };
127
- }
128
- catch (err) {
129
- throw failed(err instanceof Error ? err.message : 'Import failed');
130
- }
131
- }),
132
- malImport: protectedProcedure.input(trackerMalInput()).mutation(async ({ ctx, input }) => {
133
- const { username, erase, useOfflineDb, skipFallback } = input;
134
- if (!username || typeof username !== 'string') {
135
- throw badRequest('MAL username is required');
136
- }
137
- try {
138
- const count = await importFromMalUsername(ctx.db, username.trim(), {
139
- erase: erase === true,
140
- useOfflineDb: useOfflineDb !== false,
141
- skipFallback: skipFallback === true,
142
- });
143
- return { success: true, count };
144
- }
145
- catch (err) {
146
- throw mapImportError(err instanceof Error ? err.message : 'Import failed');
147
- }
148
- }),
149
- mangaSync: protectedProcedure.input(trackerMangaSyncInput()).mutation(async ({ ctx, input }) => {
150
- const { direction } = input;
151
- try {
152
- const mangaDb = ctx.mangaDb;
153
- if (!mangaDb || mangaDb.isClosedCheck()) {
154
- throw unavailable('Manga database is not ready');
155
- }
156
- const syncDirection = direction === 'pull-only' ? 'pull-only' : 'two-way';
157
- const result = await syncAniListManga(ctx.db, mangaDb, {
158
- direction: syncDirection,
159
- });
160
- return {
161
- success: true,
162
- summary: result.summary,
163
- details: result.details,
164
- direction: syncDirection,
165
- };
166
- }
167
- catch (err) {
168
- if (err instanceof TRPCError)
169
- throw err;
170
- throw failed(err instanceof Error ? err.message : 'Manga sync failed');
171
- }
172
- }),
173
- mangaImport: protectedProcedure.input(trackerUsernameInput()).mutation(async ({ ctx, input }) => {
174
- const { username, erase } = input;
175
- if (!username || typeof username !== 'string') {
176
- throw badRequest('Username is required');
177
- }
178
- try {
179
- const mangaDb = ctx.mangaDb;
180
- if (!mangaDb || mangaDb.isClosedCheck()) {
181
- throw unavailable('Manga database is not ready');
182
- }
183
- const count = await importFromUsernameManga(ctx.db, mangaDb, username.trim(), erase === true);
184
- return { success: true, count };
185
- }
186
- catch (err) {
187
- if (err instanceof TRPCError)
188
- throw err;
189
- throw failed(err instanceof Error ? err.message : 'Import failed');
190
- }
191
- }),
192
- mangaMalImport: protectedProcedure
193
- .input(trackerUsernameInput())
194
- .mutation(async ({ ctx, input }) => {
195
- const { username, erase } = input;
196
- if (!username || typeof username !== 'string') {
197
- throw badRequest('Username is required');
198
- }
199
- try {
200
- const mangaDb = ctx.mangaDb;
201
- if (!mangaDb || mangaDb.isClosedCheck()) {
202
- throw unavailable('Manga database is not ready');
203
- }
204
- const result = await importMangaFromMalUsername(ctx.db, mangaDb, username.trim(), erase === true);
205
- return { success: true, ...result };
206
- }
207
- catch (err) {
208
- if (err instanceof TRPCError)
209
- throw err;
210
- throw mapImportError(err instanceof Error ? err.message : 'Import failed');
211
- }
212
- }),
213
- });
@@ -1,135 +0,0 @@
1
- import { TRPCError } from '@trpc/server';
2
- import { protectedProcedure, router } from '../index.js';
3
- import { performTvWriteTransactionAsync } from '../../sync.js';
4
- import { buildTvId, TV_STATUSES, TvLibraryRepository, TvProgressRepository, } from '../../repositories/tv.repository.js';
5
- import { defineSchema, reqObj, reqStr, optStr, reqStrArray, readPageLimit } from '../validation.js';
6
- function normalizeMediaType(raw) {
7
- return String(raw).toLowerCase() === 'movie' ? 'movie' : 'tv';
8
- }
9
- const tvLibraryListInput = () => defineSchema((value) => {
10
- const obj = reqObj(value);
11
- const out = { ...readPageLimit(obj) };
12
- const status = optStr(obj, 'status');
13
- if (status !== undefined)
14
- out.status = status;
15
- return out;
16
- });
17
- const tvLibraryIdInput = () => defineSchema((value) => ({
18
- id: reqStr(reqObj(value), 'id'),
19
- }));
20
- const tvLibraryAddInput = () => defineSchema((value) => {
21
- const obj = reqObj(value);
22
- return {
23
- tmdbId: obj.tmdbId,
24
- mediaType: obj.mediaType,
25
- id: obj.id,
26
- title: obj.title,
27
- poster: obj.poster,
28
- backdrop: obj.backdrop,
29
- year: obj.year,
30
- overview: obj.overview,
31
- status: obj.status,
32
- adult: obj.adult,
33
- };
34
- });
35
- const tvLibraryRemoveInput = () => defineSchema((value) => {
36
- if (typeof value !== 'string' || value.length === 0)
37
- throw new Error('id must be a string');
38
- return value;
39
- });
40
- const tvLibraryStatusInput = () => defineSchema((value) => {
41
- const obj = reqObj(value);
42
- return { id: reqStr(obj, 'id'), status: reqStr(obj, 'status') };
43
- });
44
- const tvLibraryBatchStatusInput = () => defineSchema((value) => {
45
- const obj = reqObj(value);
46
- return { ids: reqStrArray(obj, 'ids', true), status: reqStr(obj, 'status') };
47
- });
48
- const tvLibraryRemoveManyInput = () => defineSchema((value) => {
49
- if (!Array.isArray(value) || value.length === 0)
50
- throw new Error('ids must be a non-empty array');
51
- return value.map((id) => String(id));
52
- });
53
- export const tvLibraryRouter = router({
54
- list: protectedProcedure.input(tvLibraryListInput()).query(async ({ ctx, input }) => {
55
- const page = Math.max(parseInt(String(input.page ?? 1)) || 1, 1);
56
- const limit = Math.min(Math.max(parseInt(String(input.limit ?? 24)) || 24, 1), 100);
57
- const offset = (page - 1) * limit;
58
- const rows = await TvLibraryRepository.getAll(ctx.tvDb, input.status, limit, offset);
59
- const total = await TvLibraryRepository.getCount(ctx.tvDb, input.status);
60
- return { data: rows, total, page, limit };
61
- }),
62
- ids: protectedProcedure.query(async ({ ctx }) => {
63
- return { ids: await TvLibraryRepository.getIds(ctx.tvDb) };
64
- }),
65
- check: protectedProcedure.input(tvLibraryIdInput()).query(async ({ ctx, input }) => {
66
- const item = await TvLibraryRepository.getById(ctx.tvDb, input.id);
67
- return { inLibrary: !!item, status: item?.status ?? null };
68
- }),
69
- add: protectedProcedure.input(tvLibraryAddInput()).mutation(async ({ ctx, input }) => {
70
- const tmdbIdNum = Number(input.tmdbId);
71
- if (!tmdbIdNum || (!input.id && !input.mediaType)) {
72
- throw new TRPCError({ code: 'BAD_REQUEST', message: 'tmdbId and mediaType are required' });
73
- }
74
- const mediaType = normalizeMediaType(input.mediaType || (String(input.id).startsWith('movie:') ? 'movie' : 'tv'));
75
- const id = input.id && String(input.id).includes(':')
76
- ? String(input.id)
77
- : buildTvId(mediaType, tmdbIdNum);
78
- const cleanStatus = typeof input.status === 'string' && TV_STATUSES.includes(input.status)
79
- ? input.status
80
- : 'Watching';
81
- await performTvWriteTransactionAsync(ctx.tvDb, async (tx) => {
82
- await TvLibraryRepository.upsert(tx, {
83
- id,
84
- tmdbId: tmdbIdNum,
85
- mediaType,
86
- title: String(input.title || ''),
87
- poster: String(input.poster || ''),
88
- backdrop: input.backdrop ? String(input.backdrop) : undefined,
89
- year: input.year ? String(input.year) : undefined,
90
- overview: input.overview ? String(input.overview) : undefined,
91
- status: cleanStatus,
92
- adult: input.adult === true,
93
- });
94
- });
95
- return { success: true, id };
96
- }),
97
- remove: protectedProcedure.input(tvLibraryRemoveInput()).mutation(async ({ ctx, input }) => {
98
- await performTvWriteTransactionAsync(ctx.tvDb, async (tx) => {
99
- await TvLibraryRepository.delete(tx, String(input));
100
- await TvProgressRepository.deleteByMedia(tx, String(input));
101
- });
102
- return { success: true };
103
- }),
104
- setStatus: protectedProcedure.input(tvLibraryStatusInput()).mutation(async ({ ctx, input }) => {
105
- if (!input.id || !TV_STATUSES.includes(input.status)) {
106
- throw new TRPCError({ code: 'BAD_REQUEST', message: 'id and a valid status are required' });
107
- }
108
- await performTvWriteTransactionAsync(ctx.tvDb, async (tx) => {
109
- await TvLibraryRepository.updateStatus(tx, String(input.id), String(input.status));
110
- });
111
- return { success: true };
112
- }),
113
- batchStatus: protectedProcedure
114
- .input(tvLibraryBatchStatusInput())
115
- .mutation(async ({ ctx, input }) => {
116
- if (!input.status || !TV_STATUSES.includes(input.status)) {
117
- throw new TRPCError({ code: 'BAD_REQUEST', message: 'a valid status is required' });
118
- }
119
- const ids = input.ids.map((id) => String(id));
120
- await performTvWriteTransactionAsync(ctx.tvDb, async (tx) => {
121
- await TvLibraryRepository.updateStatusMany(tx, ids, String(input.status));
122
- });
123
- return { success: true, updated: ids.length };
124
- }),
125
- removeMany: protectedProcedure
126
- .input(tvLibraryRemoveManyInput())
127
- .mutation(async ({ ctx, input }) => {
128
- const ids = input.map((id) => String(id));
129
- await performTvWriteTransactionAsync(ctx.tvDb, async (tx) => {
130
- await TvLibraryRepository.deleteMany(tx, ids);
131
- await TvProgressRepository.deleteMany(tx, ids);
132
- });
133
- return { success: true, removed: ids.length };
134
- }),
135
- });
@@ -1,159 +0,0 @@
1
- import { TRPCError } from '@trpc/server';
2
- import { protectedProcedure, router } from '../index.js';
3
- import { performTvWriteTransactionAsync } from '../../sync.js';
4
- import { adultNonListIds } from '../../utils/db-utils.js';
5
- import { SettingsRepository } from '../../repositories/settings.repository.js';
6
- import { normalizeTvMediaId, TvLibraryRepository, TvProgressRepository, } from '../../repositories/tv.repository.js';
7
- import { defineSchema, reqObj, reqStr, strOrNull } from '../validation.js';
8
- const isTvContinueAdult = (row) => row.adult === true || row.adult === 1;
9
- const getAdultNonListMediaIds = (db) => adultNonListIds(db, 'tv_progress', 'tv_library', 'mediaId', 'COALESCE(l.adult, p.adult) = 1');
10
- const tvProgressMediaInput = () => defineSchema((value) => ({
11
- mediaId: reqStr(reqObj(value), 'mediaId'),
12
- }));
13
- const tvProgressLatestInput = () => defineSchema((value) => {
14
- const obj = reqObj(value);
15
- return { mediaId: reqStr(obj, 'mediaId'), season: obj.season, episode: obj.episode };
16
- });
17
- const tvProgressSaveInput = () => defineSchema((value) => {
18
- const obj = reqObj(value);
19
- return {
20
- mediaId: reqStr(obj, 'mediaId'),
21
- season: obj.season,
22
- episode: obj.episode,
23
- currentTime: obj.currentTime,
24
- duration: obj.duration,
25
- completed: obj.completed,
26
- title: strOrNull(obj, 'title'),
27
- poster: strOrNull(obj, 'poster'),
28
- backdrop: strOrNull(obj, 'backdrop'),
29
- year: strOrNull(obj, 'year'),
30
- overview: strOrNull(obj, 'overview'),
31
- tmdbId: obj.tmdbId ?? null,
32
- mediaType: strOrNull(obj, 'mediaType'),
33
- adult: obj.adult ?? null,
34
- };
35
- });
36
- const tvProgressRemoveInput = () => defineSchema((value) => {
37
- const obj = reqObj(value);
38
- return { mediaId: reqStr(obj, 'mediaId'), season: obj.season, episode: obj.episode };
39
- });
40
- const tvProgressRemoveManyInput = () => defineSchema((value) => {
41
- if (!Array.isArray(value) || value.length === 0)
42
- throw new Error('ids must be a non-empty array');
43
- return value.map((id) => String(id));
44
- });
45
- const tvContinueWatchingInput = () => defineSchema((value) => {
46
- const obj = reqObj(value);
47
- const out = {};
48
- if (obj.limit !== undefined && obj.limit !== null && obj.limit !== '') {
49
- const limit = Number(obj.limit);
50
- if (Number.isFinite(limit))
51
- out.limit = limit;
52
- }
53
- return out;
54
- });
55
- export const tvProgressRouter = router({
56
- getByMedia: protectedProcedure.input(tvProgressMediaInput()).query(async ({ ctx, input }) => {
57
- const rows = await TvProgressRepository.getByMedia(ctx.tvDb, normalizeTvMediaId(input.mediaId));
58
- return { progress: rows };
59
- }),
60
- latest: protectedProcedure.input(tvProgressLatestInput()).query(async ({ ctx, input }) => {
61
- const mediaId = normalizeTvMediaId(input.mediaId);
62
- const season = parseInt(String(input.season), 10);
63
- const episode = parseInt(String(input.episode), 10);
64
- if (Number.isFinite(season) && Number.isFinite(episode)) {
65
- const row = await TvProgressRepository.getEpisode(ctx.tvDb, mediaId, season, episode);
66
- return row || { currentTime: 0, duration: 0, completed: 0 };
67
- }
68
- const row = await TvProgressRepository.getLatest(ctx.tvDb, mediaId);
69
- return row || { currentTime: 0, duration: 0, completed: 0 };
70
- }),
71
- save: protectedProcedure.input(tvProgressSaveInput()).mutation(async ({ ctx, input }) => {
72
- const mediaId = normalizeTvMediaId(input.mediaId);
73
- if (!mediaId) {
74
- throw new TRPCError({ code: 'BAD_REQUEST', message: 'mediaId is required' });
75
- }
76
- const seasonNum = Math.max(Number(input.season) || 1, 1);
77
- const episodeNum = Math.max(Number(input.episode) || 1, 1);
78
- const timeNum = Math.max(Number(input.currentTime) || 0, 0);
79
- const durationNum = Math.max(Number(input.duration) || 0, 0);
80
- const clampedTime = durationNum > 0 ? Math.min(timeNum, durationNum) : timeNum;
81
- const storedTime = durationNum > 0 && clampedTime >= durationNum * 0.8 ? durationNum : clampedTime;
82
- const completedNow = Number(input.completed) === 1 || (durationNum > 0 && storedTime >= durationNum * 0.8) ? 1 : 0;
83
- await performTvWriteTransactionAsync(ctx.tvDb, async (tx) => {
84
- await TvProgressRepository.upsert(tx, {
85
- mediaId: String(mediaId),
86
- season: Math.round(seasonNum),
87
- episode: Math.round(episodeNum),
88
- currentTime: storedTime,
89
- duration: durationNum,
90
- completed: completedNow,
91
- title: input.title ?? null,
92
- poster: input.poster ?? null,
93
- backdrop: input.backdrop ?? null,
94
- year: input.year ?? null,
95
- overview: input.overview ?? null,
96
- tmdbId: input.tmdbId != null ? Number(input.tmdbId) : null,
97
- mediaType: input.mediaType ?? null,
98
- adult: input.adult != null ? Number(input.adult) : null,
99
- });
100
- await TvLibraryRepository.touchProgress(tx, String(mediaId), {
101
- season: Math.round(seasonNum),
102
- episode: Math.round(episodeNum),
103
- });
104
- });
105
- return { success: true };
106
- }),
107
- remove: protectedProcedure.input(tvProgressRemoveInput()).mutation(async ({ ctx, input }) => {
108
- const mediaId = normalizeTvMediaId(input.mediaId);
109
- if (!mediaId) {
110
- throw new TRPCError({ code: 'BAD_REQUEST', message: 'mediaId is required' });
111
- }
112
- await performTvWriteTransactionAsync(ctx.tvDb, async (tx) => {
113
- if (input.season !== undefined && input.episode !== undefined) {
114
- await TvProgressRepository.deleteEpisode(tx, String(mediaId), Number(input.season), Number(input.episode));
115
- }
116
- else {
117
- await TvProgressRepository.deleteByMedia(tx, String(mediaId));
118
- }
119
- });
120
- return { success: true };
121
- }),
122
- removeMany: protectedProcedure
123
- .input(tvProgressRemoveManyInput())
124
- .mutation(async ({ ctx, input }) => {
125
- const ids = input.map((id) => normalizeTvMediaId(id));
126
- await performTvWriteTransactionAsync(ctx.tvDb, async (tx) => {
127
- await TvProgressRepository.deleteMany(tx, ids);
128
- });
129
- return { success: true, removed: ids.length };
130
- }),
131
- continueWatching: protectedProcedure
132
- .input(tvContinueWatchingInput())
133
- .query(async ({ ctx, input }) => {
134
- const limit = Math.min(Math.max(parseInt(String(input.limit ?? 24)) || 24, 1), 100);
135
- const { ignoreAdult, listOnly } = await SettingsRepository.readContinueFlags(ctx.db, 'tvIgnoreAdultContent', 'tvCwWatchlistOnly');
136
- const rows = (await TvProgressRepository.getContinueWatching(ctx.tvDb, limit)).filter((row) => {
137
- if (ignoreAdult && isTvContinueAdult(row))
138
- return false;
139
- if (listOnly && row.watchlistStatus !== 'Watching')
140
- return false;
141
- return true;
142
- });
143
- return { data: rows, total: rows.length };
144
- }),
145
- adultCount: protectedProcedure.query(async ({ ctx }) => {
146
- const ids = await getAdultNonListMediaIds(ctx.tvDb);
147
- return { count: ids.length };
148
- }),
149
- purgeAdult: protectedProcedure.mutation(async ({ ctx }) => {
150
- const ids = await getAdultNonListMediaIds(ctx.tvDb);
151
- if (ids.length > 0) {
152
- await performTvWriteTransactionAsync(ctx.tvDb, async (tx) => {
153
- for (const id of ids)
154
- await TvProgressRepository.deleteByMedia(tx, id);
155
- });
156
- }
157
- return { success: true, removed: ids.length };
158
- }),
159
- });