@serifpersia/dango 3.3.8 → 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-D4qo4289.js → AnimeCard-CJy1SXH3.js} +1 -1
  3. package/client/dist/assets/{AnimeInfo-BdRRm4So.js → AnimeInfo-BoXlVvZ-.js} +1 -1
  4. package/client/dist/assets/{AnimeInfoPage-DeJNAKMR.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-D3hTqTRU.js → LibraryListPage-HzuDavbb.js} +1 -1
  11. package/client/dist/assets/{ListeningList-BgWsfu40.js → ListeningList-DYcQnnsG.js} +1 -1
  12. package/client/dist/assets/{Manga-Dn-CYins.js → Manga-D8CT9aO0.js} +1 -1
  13. package/client/dist/assets/{MangaInfoPage-EmQoK7Bv.js → MangaInfoPage-DGK6CYp1.js} +1 -1
  14. package/client/dist/assets/MangaReadPage-B9XoDKF9.js +1 -0
  15. package/client/dist/assets/{MangaResetProgressModal-Dian37PQ.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-B7DeSFoA.js → QueueRail-O7f4RqjB.js} +1 -1
  24. package/client/dist/assets/Radio-DPZxQnyD.js +1 -0
  25. package/client/dist/assets/{ReadingList-BT8kUjCB.js → ReadingList-n6rr8ygy.js} +1 -1
  26. package/client/dist/assets/{ResetProgressModal-DWeiO-hs.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-B5loz7ii.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-BYwCf5kO.js → TvSearch-DuqR-z3v.js} +1 -1
  36. package/client/dist/assets/{TvWatchlist-BiJU6pKT.js → TvWatchlist-CBvgFrA0.js} +1 -1
  37. package/client/dist/assets/Watchlist-mj8mTOam.js +1 -0
  38. package/client/dist/assets/{hls-C11FOWHj.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-cT_Uu41e.js → useAsmrPopup-e4zNN9ig.js} +1 -1
  43. package/client/dist/assets/useMangaLibrary-C9Lci5N4.js +1 -0
  44. package/client/dist/assets/{useMangaPopup-PnmSctHu.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 +15 -4
  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} +235 -295
  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-D0EGRYsv.js +0 -2
  126. package/client/dist/assets/AsmrResetProgressModal-Gs0GmVaT.js +0 -1
  127. package/client/dist/assets/AvSyncCalibrator-D8hqarVB.js +0 -10
  128. package/client/dist/assets/Home-BnBzjFkW.js +0 -1
  129. package/client/dist/assets/Insights-i9QgY3nE.js +0 -1
  130. package/client/dist/assets/MangaReadPage-xJB8K2YU.js +0 -1
  131. package/client/dist/assets/Mature-DQtXKOXc.js +0 -1
  132. package/client/dist/assets/MediaCard-DpKoY-1j.js +0 -1
  133. package/client/dist/assets/Music-BP43Zqmq.js +0 -1
  134. package/client/dist/assets/OptionListMenu-Bj9ebsW3.js +0 -1
  135. package/client/dist/assets/Player-O2Cy2GF1.js +0 -56
  136. package/client/dist/assets/PlayerSettings-DYZ272Yp.js +0 -1
  137. package/client/dist/assets/QueueOptionsButton-lv26-6jp.js +0 -1
  138. package/client/dist/assets/Radio-qQBYnVfY.js +0 -1
  139. package/client/dist/assets/Search-BkvPYN33.js +0 -1
  140. package/client/dist/assets/Settings-CyZEBF6S.js +0 -21
  141. package/client/dist/assets/Trackers-JK-aXBYC.js +0 -2
  142. package/client/dist/assets/Tv-BwXWJxMy.js +0 -4
  143. package/client/dist/assets/Tv-C2SWcVzN.css +0 -1
  144. package/client/dist/assets/TvInfo-Dzyq6ucH.js +0 -1
  145. package/client/dist/assets/TvPopup-PlCU30FO.js +0 -1
  146. package/client/dist/assets/Watchlist-BOEeGXH9.js +0 -1
  147. package/client/dist/assets/index-CtoKRNLc.js +0 -178
  148. package/client/dist/assets/useAnimeInfoData-kr3WOIWH.js +0 -1
  149. package/client/dist/assets/useAsmr-CB9THf0m.js +0 -1
  150. package/client/dist/assets/useMangaLibrary-rM6z3_0E.js +0 -1
  151. package/client/dist/assets/useProviders-B3VB7A9M.js +0 -1
  152. package/client/dist/assets/useTv-BTUKMH7Q.js +0 -1
  153. package/client/dist/assets/useTvLibrary-C0sX-ilZ.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,145 @@
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 } 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 = {};
12
+ const status = optStr(obj, 'status');
13
+ if (status !== undefined)
14
+ out.status = status;
15
+ if (obj.page !== undefined && obj.page !== null && obj.page !== '') {
16
+ const page = Number(obj.page);
17
+ if (Number.isFinite(page))
18
+ out.page = page;
19
+ }
20
+ if (obj.limit !== undefined && obj.limit !== null && obj.limit !== '') {
21
+ const limit = Number(obj.limit);
22
+ if (Number.isFinite(limit))
23
+ out.limit = limit;
24
+ }
25
+ return out;
26
+ });
27
+ const tvLibraryIdInput = () => defineSchema((value) => ({
28
+ id: reqStr(reqObj(value), 'id'),
29
+ }));
30
+ const tvLibraryAddInput = () => defineSchema((value) => {
31
+ const obj = reqObj(value);
32
+ return {
33
+ tmdbId: obj.tmdbId,
34
+ mediaType: obj.mediaType,
35
+ id: obj.id,
36
+ title: obj.title,
37
+ poster: obj.poster,
38
+ backdrop: obj.backdrop,
39
+ year: obj.year,
40
+ overview: obj.overview,
41
+ status: obj.status,
42
+ adult: obj.adult,
43
+ };
44
+ });
45
+ const tvLibraryRemoveInput = () => defineSchema((value) => {
46
+ if (typeof value !== 'string' || value.length === 0)
47
+ throw new Error('id must be a string');
48
+ return value;
49
+ });
50
+ const tvLibraryStatusInput = () => defineSchema((value) => {
51
+ const obj = reqObj(value);
52
+ return { id: reqStr(obj, 'id'), status: reqStr(obj, 'status') };
53
+ });
54
+ const tvLibraryBatchStatusInput = () => defineSchema((value) => {
55
+ const obj = reqObj(value);
56
+ return { ids: reqStrArray(obj, 'ids', true), status: reqStr(obj, 'status') };
57
+ });
58
+ const tvLibraryRemoveManyInput = () => defineSchema((value) => {
59
+ if (!Array.isArray(value) || value.length === 0)
60
+ throw new Error('ids must be a non-empty array');
61
+ return value.map((id) => String(id));
62
+ });
63
+ export const tvLibraryRouter = router({
64
+ list: protectedProcedure.input(tvLibraryListInput()).query(async ({ ctx, input }) => {
65
+ const page = Math.max(parseInt(String(input.page ?? 1)) || 1, 1);
66
+ const limit = Math.min(Math.max(parseInt(String(input.limit ?? 24)) || 24, 1), 100);
67
+ const offset = (page - 1) * limit;
68
+ const rows = await TvLibraryRepository.getAll(ctx.tvDb, input.status, limit, offset);
69
+ const total = await TvLibraryRepository.getCount(ctx.tvDb, input.status);
70
+ return { data: rows, total, page, limit };
71
+ }),
72
+ ids: protectedProcedure.query(async ({ ctx }) => {
73
+ return { ids: await TvLibraryRepository.getIds(ctx.tvDb) };
74
+ }),
75
+ check: protectedProcedure.input(tvLibraryIdInput()).query(async ({ ctx, input }) => {
76
+ const item = await TvLibraryRepository.getById(ctx.tvDb, input.id);
77
+ return { inLibrary: !!item, status: item?.status ?? null };
78
+ }),
79
+ add: protectedProcedure.input(tvLibraryAddInput()).mutation(async ({ ctx, input }) => {
80
+ const tmdbIdNum = Number(input.tmdbId);
81
+ if (!tmdbIdNum || (!input.id && !input.mediaType)) {
82
+ throw new TRPCError({ code: 'BAD_REQUEST', message: 'tmdbId and mediaType are required' });
83
+ }
84
+ const mediaType = normalizeMediaType(input.mediaType || (String(input.id).startsWith('movie:') ? 'movie' : 'tv'));
85
+ const id = input.id && String(input.id).includes(':')
86
+ ? String(input.id)
87
+ : buildTvId(mediaType, tmdbIdNum);
88
+ const cleanStatus = typeof input.status === 'string' && TV_STATUSES.includes(input.status)
89
+ ? input.status
90
+ : 'Watching';
91
+ await performTvWriteTransactionAsync(ctx.tvDb, async (tx) => {
92
+ await TvLibraryRepository.upsert(tx, {
93
+ id,
94
+ tmdbId: tmdbIdNum,
95
+ mediaType,
96
+ title: String(input.title || ''),
97
+ poster: String(input.poster || ''),
98
+ backdrop: input.backdrop ? String(input.backdrop) : undefined,
99
+ year: input.year ? String(input.year) : undefined,
100
+ overview: input.overview ? String(input.overview) : undefined,
101
+ status: cleanStatus,
102
+ adult: input.adult === true,
103
+ });
104
+ });
105
+ return { success: true, id };
106
+ }),
107
+ remove: protectedProcedure.input(tvLibraryRemoveInput()).mutation(async ({ ctx, input }) => {
108
+ await performTvWriteTransactionAsync(ctx.tvDb, async (tx) => {
109
+ await TvLibraryRepository.delete(tx, String(input));
110
+ await TvProgressRepository.deleteByMedia(tx, String(input));
111
+ });
112
+ return { success: true };
113
+ }),
114
+ setStatus: protectedProcedure.input(tvLibraryStatusInput()).mutation(async ({ ctx, input }) => {
115
+ if (!input.id || !TV_STATUSES.includes(input.status)) {
116
+ throw new TRPCError({ code: 'BAD_REQUEST', message: 'id and a valid status are required' });
117
+ }
118
+ await performTvWriteTransactionAsync(ctx.tvDb, async (tx) => {
119
+ await TvLibraryRepository.updateStatus(tx, String(input.id), String(input.status));
120
+ });
121
+ return { success: true };
122
+ }),
123
+ batchStatus: protectedProcedure
124
+ .input(tvLibraryBatchStatusInput())
125
+ .mutation(async ({ ctx, input }) => {
126
+ if (!input.status || !TV_STATUSES.includes(input.status)) {
127
+ throw new TRPCError({ code: 'BAD_REQUEST', message: 'a valid status is required' });
128
+ }
129
+ const ids = input.ids.map((id) => String(id));
130
+ await performTvWriteTransactionAsync(ctx.tvDb, async (tx) => {
131
+ await TvLibraryRepository.updateStatusMany(tx, ids, String(input.status));
132
+ });
133
+ return { success: true, updated: ids.length };
134
+ }),
135
+ removeMany: protectedProcedure
136
+ .input(tvLibraryRemoveManyInput())
137
+ .mutation(async ({ ctx, input }) => {
138
+ const ids = input.map((id) => String(id));
139
+ await performTvWriteTransactionAsync(ctx.tvDb, async (tx) => {
140
+ await TvLibraryRepository.deleteMany(tx, ids);
141
+ await TvProgressRepository.deleteMany(tx, ids);
142
+ });
143
+ return { success: true, removed: ids.length };
144
+ }),
145
+ });
@@ -0,0 +1,171 @@
1
+ import { TRPCError } from '@trpc/server';
2
+ import { protectedProcedure, router } from '../index.js';
3
+ import { performTvWriteTransactionAsync } from '../../sync.js';
4
+ import { dbAll } 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 } from '../validation.js';
8
+ const isTvContinueAdult = (row) => row.adult === true || row.adult === 1;
9
+ async function getAdultNonListMediaIds(db) {
10
+ const rows = await dbAll(db, `SELECT DISTINCT p.mediaId as mediaId
11
+ FROM tv_progress p
12
+ LEFT JOIN tv_library l ON l.id = p.mediaId
13
+ WHERE COALESCE(l.adult, p.adult) = 1
14
+ AND l.id IS NULL`);
15
+ return rows.map((r) => r.mediaId);
16
+ }
17
+ const tvProgressMediaInput = () => defineSchema((value) => ({
18
+ mediaId: reqStr(reqObj(value), 'mediaId'),
19
+ }));
20
+ const tvProgressLatestInput = () => defineSchema((value) => {
21
+ const obj = reqObj(value);
22
+ return { mediaId: reqStr(obj, 'mediaId'), season: obj.season, episode: obj.episode };
23
+ });
24
+ const tvProgressSaveInput = () => defineSchema((value) => {
25
+ const obj = reqObj(value);
26
+ return {
27
+ mediaId: reqStr(obj, 'mediaId'),
28
+ season: obj.season,
29
+ episode: obj.episode,
30
+ currentTime: obj.currentTime,
31
+ duration: obj.duration,
32
+ completed: obj.completed,
33
+ title: obj.title ?? null,
34
+ poster: obj.poster ?? null,
35
+ backdrop: obj.backdrop ?? null,
36
+ year: obj.year ?? null,
37
+ overview: obj.overview ?? null,
38
+ tmdbId: obj.tmdbId ?? null,
39
+ mediaType: obj.mediaType ?? null,
40
+ adult: obj.adult ?? null,
41
+ };
42
+ });
43
+ const tvProgressRemoveInput = () => defineSchema((value) => {
44
+ const obj = reqObj(value);
45
+ return { mediaId: reqStr(obj, 'mediaId'), season: obj.season, episode: obj.episode };
46
+ });
47
+ const tvProgressRemoveManyInput = () => defineSchema((value) => {
48
+ if (!Array.isArray(value) || value.length === 0)
49
+ throw new Error('ids must be a non-empty array');
50
+ return value.map((id) => String(id));
51
+ });
52
+ const tvContinueWatchingInput = () => defineSchema((value) => {
53
+ const obj = reqObj(value);
54
+ const out = {};
55
+ if (obj.limit !== undefined && obj.limit !== null && obj.limit !== '') {
56
+ const limit = Number(obj.limit);
57
+ if (Number.isFinite(limit))
58
+ out.limit = limit;
59
+ }
60
+ return out;
61
+ });
62
+ export const tvProgressRouter = router({
63
+ getByMedia: protectedProcedure.input(tvProgressMediaInput()).query(async ({ ctx, input }) => {
64
+ const rows = await TvProgressRepository.getByMedia(ctx.tvDb, normalizeTvMediaId(input.mediaId));
65
+ return { progress: rows };
66
+ }),
67
+ latest: protectedProcedure.input(tvProgressLatestInput()).query(async ({ ctx, input }) => {
68
+ const mediaId = normalizeTvMediaId(input.mediaId);
69
+ const season = parseInt(String(input.season), 10);
70
+ const episode = parseInt(String(input.episode), 10);
71
+ if (Number.isFinite(season) && Number.isFinite(episode)) {
72
+ const row = await TvProgressRepository.getEpisode(ctx.tvDb, mediaId, season, episode);
73
+ return row || { currentTime: 0, duration: 0, completed: 0 };
74
+ }
75
+ const row = await TvProgressRepository.getLatest(ctx.tvDb, mediaId);
76
+ return row || { currentTime: 0, duration: 0, completed: 0 };
77
+ }),
78
+ save: protectedProcedure.input(tvProgressSaveInput()).mutation(async ({ ctx, input }) => {
79
+ const mediaId = normalizeTvMediaId(input.mediaId);
80
+ if (!mediaId) {
81
+ throw new TRPCError({ code: 'BAD_REQUEST', message: 'mediaId is required' });
82
+ }
83
+ const seasonNum = Math.max(Number(input.season) || 1, 1);
84
+ const episodeNum = Math.max(Number(input.episode) || 1, 1);
85
+ const timeNum = Math.max(Number(input.currentTime) || 0, 0);
86
+ const durationNum = Math.max(Number(input.duration) || 0, 0);
87
+ const clampedTime = durationNum > 0 ? Math.min(timeNum, durationNum) : timeNum;
88
+ const storedTime = durationNum > 0 && clampedTime >= durationNum * 0.8 ? durationNum : clampedTime;
89
+ const completedNow = Number(input.completed) === 1 || (durationNum > 0 && storedTime >= durationNum * 0.8) ? 1 : 0;
90
+ await performTvWriteTransactionAsync(ctx.tvDb, async (tx) => {
91
+ await TvProgressRepository.upsert(tx, {
92
+ mediaId: String(mediaId),
93
+ season: Math.round(seasonNum),
94
+ episode: Math.round(episodeNum),
95
+ currentTime: storedTime,
96
+ duration: durationNum,
97
+ completed: completedNow,
98
+ title: (input.title ?? null),
99
+ poster: (input.poster ?? null),
100
+ backdrop: (input.backdrop ?? null),
101
+ year: (input.year ?? null),
102
+ overview: (input.overview ?? null),
103
+ tmdbId: input.tmdbId != null ? Number(input.tmdbId) : null,
104
+ mediaType: (input.mediaType ?? null),
105
+ adult: input.adult != null ? Number(input.adult) : null,
106
+ });
107
+ await TvLibraryRepository.touchProgress(tx, String(mediaId), {
108
+ season: Math.round(seasonNum),
109
+ episode: Math.round(episodeNum),
110
+ });
111
+ });
112
+ return { success: true };
113
+ }),
114
+ remove: protectedProcedure.input(tvProgressRemoveInput()).mutation(async ({ ctx, input }) => {
115
+ const mediaId = normalizeTvMediaId(input.mediaId);
116
+ if (!mediaId) {
117
+ throw new TRPCError({ code: 'BAD_REQUEST', message: 'mediaId is required' });
118
+ }
119
+ await performTvWriteTransactionAsync(ctx.tvDb, async (tx) => {
120
+ if (input.season !== undefined && input.episode !== undefined) {
121
+ await TvProgressRepository.deleteEpisode(tx, String(mediaId), Number(input.season), Number(input.episode));
122
+ }
123
+ else {
124
+ await TvProgressRepository.deleteByMedia(tx, String(mediaId));
125
+ }
126
+ });
127
+ return { success: true };
128
+ }),
129
+ removeMany: protectedProcedure
130
+ .input(tvProgressRemoveManyInput())
131
+ .mutation(async ({ ctx, input }) => {
132
+ const ids = input.map((id) => normalizeTvMediaId(id));
133
+ await performTvWriteTransactionAsync(ctx.tvDb, async (tx) => {
134
+ await TvProgressRepository.deleteMany(tx, ids);
135
+ });
136
+ return { success: true, removed: ids.length };
137
+ }),
138
+ continueWatching: protectedProcedure
139
+ .input(tvContinueWatchingInput())
140
+ .query(async ({ ctx, input }) => {
141
+ const limit = Math.min(Math.max(parseInt(String(input.limit ?? 24)) || 24, 1), 100);
142
+ const ignoreAdultRow = await SettingsRepository.getByKey(ctx.db, 'tvIgnoreAdultContent');
143
+ const ignoreAdult = ignoreAdultRow ? ignoreAdultRow.value !== 'false' : true;
144
+ const listOnlyRow = await SettingsRepository.getByKey(ctx.db, 'tvCwWatchlistOnly');
145
+ const listOnly = listOnlyRow
146
+ ? listOnlyRow.value === 'true' || listOnlyRow.value === '1'
147
+ : false;
148
+ const rows = (await TvProgressRepository.getContinueWatching(ctx.tvDb, limit)).filter((row) => {
149
+ if (ignoreAdult && isTvContinueAdult(row))
150
+ return false;
151
+ if (listOnly && row.watchlistStatus !== 'Watching')
152
+ return false;
153
+ return true;
154
+ });
155
+ return { data: rows, total: rows.length };
156
+ }),
157
+ adultCount: protectedProcedure.query(async ({ ctx }) => {
158
+ const ids = await getAdultNonListMediaIds(ctx.tvDb);
159
+ return { count: ids.length };
160
+ }),
161
+ purgeAdult: protectedProcedure.mutation(async ({ ctx }) => {
162
+ const ids = await getAdultNonListMediaIds(ctx.tvDb);
163
+ if (ids.length > 0) {
164
+ await performTvWriteTransactionAsync(ctx.tvDb, async (tx) => {
165
+ for (const id of ids)
166
+ await TvProgressRepository.deleteByMedia(tx, id);
167
+ });
168
+ }
169
+ return { success: true, removed: ids.length };
170
+ }),
171
+ });