@serifpersia/dango 3.3.9 → 3.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/README.md +1 -1
  2. package/client/dist/assets/{AnimeCard-URCiiy5W.js → AnimeCard-B0oMgXBm.js} +1 -1
  3. package/client/dist/assets/{AnimeInfo-e3z3er9t.js → AnimeInfo-o0Udf0og.js} +1 -1
  4. package/client/dist/assets/{AnimeInfoPage-B7Djlmy_.js → AnimeInfoPage-CojMIEuf.js} +2 -2
  5. package/client/dist/assets/Asmr-2V3yCCiK.js +2 -0
  6. package/client/dist/assets/AsmrResetProgressModal-C9R7HlNa.js +1 -0
  7. package/client/dist/assets/AvSyncCalibrator-Bo3820j3.js +10 -0
  8. package/client/dist/assets/Home-C8uXTzIM.js +1 -0
  9. package/client/dist/assets/Insights-CRqBfZuK.js +1 -0
  10. package/client/dist/assets/{LibraryListPage-B0qWqhb_.js → LibraryListPage-B_r51cRi.js} +1 -1
  11. package/client/dist/assets/{ListeningList-CSqD8j1C.js → ListeningList-BWnwvJTt.js} +1 -1
  12. package/client/dist/assets/{Manga-Cg2YuObF.js → Manga-CI5cZg3h.js} +1 -1
  13. package/client/dist/assets/{MangaInfoPage-DcArhEsy.js → MangaInfoPage-BtkOrO34.js} +1 -1
  14. package/client/dist/assets/MangaReadPage-Uv72AYdY.js +1 -0
  15. package/client/dist/assets/{MangaResetProgressModal-D_yT3d1v.js → MangaResetProgressModal-CPMEqZyi.js} +1 -1
  16. package/client/dist/assets/Mature-DXHWvoY5.js +1 -0
  17. package/client/dist/assets/MediaCard-DzA336CA.js +1 -0
  18. package/client/dist/assets/Music-C4dM0BQx.js +1 -0
  19. package/client/dist/assets/OptionListMenu-CpwxAC3J.js +1 -0
  20. package/client/dist/assets/Player-CpMvWTkH.js +56 -0
  21. package/client/dist/assets/PlayerSettings-pMU85k7p.js +1 -0
  22. package/client/dist/assets/QueueOptionsButton-1M7MdKzM.js +1 -0
  23. package/client/dist/assets/{QueueRail-B7f1-e6i.js → QueueRail-Bpla2xD8.js} +1 -1
  24. package/client/dist/assets/Radio-DlmTC-qG.js +1 -0
  25. package/client/dist/assets/{ReadingList-BaIem8H1.js → ReadingList-wJMDUybm.js} +1 -1
  26. package/client/dist/assets/{ResetProgressModal-DES5Y-Fl.js → ResetProgressModal-CByRM1Fh.js} +1 -1
  27. package/client/dist/assets/Search-B94D0tRM.js +1 -0
  28. package/client/dist/assets/Settings-CcU3oNY8.js +21 -0
  29. package/client/dist/assets/{SkeletonGrid-BxfgjP4i.js → SkeletonGrid-BJyZkg_K.js} +1 -1
  30. package/client/dist/assets/Trackers-vz-gqxtm.js +2 -0
  31. package/client/dist/assets/Tv-Rji_lvQo.js +4 -0
  32. package/client/dist/assets/Tv-ZEA45gXK.css +1 -0
  33. package/client/dist/assets/TvInfo-Ciujd6os.js +1 -0
  34. package/client/dist/assets/TvPopup-CqlVRF-Y.js +1 -0
  35. package/client/dist/assets/{TvSearch-BWDwWnHe.js → TvSearch-BcKXQue9.js} +1 -1
  36. package/client/dist/assets/{TvWatchlist-D3n5_4MI.js → TvWatchlist-sjsiup2h.js} +1 -1
  37. package/client/dist/assets/Watchlist-DAMdvt7Z.js +1 -0
  38. package/client/dist/assets/{hls-BO_L1WoN.js → hls-Do_n10_Y.js} +1 -1
  39. package/client/dist/assets/index-B5SLDCAi.js +178 -0
  40. package/client/dist/assets/useAnimeInfoData-DU9X90no.js +1 -0
  41. package/client/dist/assets/useAsmr-D6cSjbvt.js +1 -0
  42. package/client/dist/assets/{useAsmrPopup-D_o9-QlR.js → useAsmrPopup-DK1Vx-HV.js} +1 -1
  43. package/client/dist/assets/useMangaLibrary-DcxPn0RM.js +1 -0
  44. package/client/dist/assets/{useMangaPopup-TFzkC-tG.js → useMangaPopup-BLwvRZn0.js} +1 -1
  45. package/client/dist/assets/useProviders-D3L1Chqc.js +1 -0
  46. package/client/dist/assets/useTv-B546u-Rx.js +1 -0
  47. package/client/dist/assets/useTvLibrary-C2rPeCVT.js +1 -0
  48. package/client/dist/assets/{vendor-query-BXRECSI6.js → vendor-query-BUTye89G.js} +1 -1
  49. package/client/dist/index.html +2 -2
  50. package/orchestrator.js +1 -2
  51. package/package.json +6 -6
  52. package/server/dist/app-auth.js +9 -29
  53. package/server/dist/app-hono.js +621 -0
  54. package/server/dist/config.js +0 -1
  55. package/server/dist/db/drizzle.js +32 -0
  56. package/server/dist/db/schema-anime.js +64 -0
  57. package/server/dist/db/schema-asmr.js +25 -0
  58. package/server/dist/db/schema-manga.js +31 -0
  59. package/server/dist/db/schema-tv.js +33 -0
  60. package/server/dist/db.js +75 -4
  61. package/server/dist/discord-gateway.js +1 -1
  62. package/server/dist/google.js +0 -7
  63. package/server/dist/hono/asmr.js +1 -0
  64. package/server/dist/hono/auth.js +56 -0
  65. package/server/dist/hono/music.js +419 -0
  66. package/server/dist/{controllers/proxy.controller.js → hono/proxy.js} +390 -394
  67. package/server/dist/hono/tv.js +197 -0
  68. package/server/dist/hono/watchlist.js +733 -0
  69. package/server/dist/lib/anilist.js +3 -3
  70. package/server/dist/lib/insights.js +185 -0
  71. package/server/dist/lib/ipc.js +1 -2
  72. package/server/dist/lib/kitsu.js +1 -28
  73. package/server/dist/lib/mal-import.js +291 -0
  74. package/server/dist/lib/migration.js +20 -34
  75. package/server/dist/lib/offline-db.js +36 -36
  76. package/server/dist/lib/tracker/manga-sync.service.js +21 -21
  77. package/server/dist/lib/tracker/sync.service.js +20 -20
  78. package/server/dist/lib/translate.js +79 -0
  79. package/server/dist/lib/ytmusic.js +0 -2
  80. package/server/dist/providers/title-matching.js +0 -1
  81. package/server/dist/repositories/asmr.repository.js +90 -87
  82. package/server/dist/repositories/insights.repository.js +24 -13
  83. package/server/dist/repositories/library.repository.js +33 -93
  84. package/server/dist/repositories/manga.repository.js +108 -116
  85. package/server/dist/repositories/notifications.repository.js +50 -31
  86. package/server/dist/repositories/queue.repository.js +36 -25
  87. package/server/dist/repositories/settings.repository.js +18 -8
  88. package/server/dist/repositories/shows-meta.repository.js +54 -41
  89. package/server/dist/repositories/temp-show-ids.repository.js +43 -31
  90. package/server/dist/repositories/tv.repository.js +98 -115
  91. package/server/dist/repositories/watched-episodes.repository.js +52 -20
  92. package/server/dist/repositories/watchlist.repository.js +36 -48
  93. package/server/dist/server.js +59 -151
  94. package/server/dist/sync.js +41 -4
  95. package/server/dist/trpc/context.js +7 -0
  96. package/server/dist/trpc/errors.js +21 -0
  97. package/server/dist/trpc/index.js +19 -0
  98. package/server/dist/trpc/router.js +49 -0
  99. package/server/dist/trpc/routers/asmr-library.js +122 -0
  100. package/server/dist/trpc/routers/asmr-progress.js +133 -0
  101. package/server/dist/trpc/routers/asmr.js +87 -0
  102. package/server/dist/trpc/routers/auth.js +247 -0
  103. package/server/dist/trpc/routers/continue-watching.js +83 -0
  104. package/server/dist/trpc/routers/data.js +1039 -0
  105. package/server/dist/trpc/routers/discord.js +288 -0
  106. package/server/dist/trpc/routers/discovery.js +15 -0
  107. package/server/dist/trpc/routers/insights.js +45 -0
  108. package/server/dist/trpc/routers/manga-library.js +280 -0
  109. package/server/dist/trpc/routers/manga-progress.js +181 -0
  110. package/server/dist/trpc/routers/manga.js +201 -0
  111. package/server/dist/{controllers/music.controller.js → trpc/routers/music.js} +155 -288
  112. package/server/dist/trpc/routers/notifications.js +80 -0
  113. package/server/dist/trpc/routers/progress.js +15 -0
  114. package/server/dist/trpc/routers/queue.js +95 -0
  115. package/server/dist/{routes/radio.routes.js → trpc/routers/radio.js} +30 -23
  116. package/server/dist/trpc/routers/settings.js +105 -0
  117. package/server/dist/trpc/routers/tracker.js +219 -0
  118. package/server/dist/trpc/routers/tv-library.js +145 -0
  119. package/server/dist/trpc/routers/tv-progress.js +171 -0
  120. package/server/dist/trpc/routers/tv.js +565 -0
  121. package/server/dist/trpc/routers/watchlist.js +564 -0
  122. package/server/dist/trpc/validation.js +159 -0
  123. package/server/dist/utils/security.utils.js +0 -18
  124. package/server/package.json +5 -10
  125. package/client/dist/assets/Asmr-C28TfSvX.js +0 -2
  126. package/client/dist/assets/AsmrResetProgressModal-6Xh71AgR.js +0 -1
  127. package/client/dist/assets/AvSyncCalibrator-BwCtRW0P.js +0 -10
  128. package/client/dist/assets/Home-BJdbHYja.js +0 -1
  129. package/client/dist/assets/Insights-CjRy42YE.js +0 -1
  130. package/client/dist/assets/MangaReadPage-YCETZRte.js +0 -1
  131. package/client/dist/assets/Mature-BGxRWkV5.js +0 -1
  132. package/client/dist/assets/MediaCard-CAzoxsRS.js +0 -1
  133. package/client/dist/assets/Music-Cgo-yQ7E.js +0 -1
  134. package/client/dist/assets/OptionListMenu--Owe9Jle.js +0 -1
  135. package/client/dist/assets/Player-yRXG2BrP.js +0 -56
  136. package/client/dist/assets/PlayerSettings-DrZLa0CC.js +0 -1
  137. package/client/dist/assets/QueueOptionsButton-BVZyPyKf.js +0 -1
  138. package/client/dist/assets/Radio-BFjxlzuF.js +0 -1
  139. package/client/dist/assets/Search-BOBo0Ogh.js +0 -1
  140. package/client/dist/assets/Settings-DOvNQ0gX.js +0 -21
  141. package/client/dist/assets/Trackers-DZ9KqOWv.js +0 -2
  142. package/client/dist/assets/Tv-5mATEU4p.js +0 -4
  143. package/client/dist/assets/Tv-C2SWcVzN.css +0 -1
  144. package/client/dist/assets/TvInfo-CF-5C5Fs.js +0 -1
  145. package/client/dist/assets/TvPopup-Dfqifj3s.js +0 -1
  146. package/client/dist/assets/Watchlist-B8MHNsAU.js +0 -1
  147. package/client/dist/assets/index-D2RkUJIN.js +0 -178
  148. package/client/dist/assets/useAnimeInfoData-BWShDHqE.js +0 -1
  149. package/client/dist/assets/useAsmr-FyfD9G7b.js +0 -1
  150. package/client/dist/assets/useMangaLibrary-BJU5x8Vz.js +0 -1
  151. package/client/dist/assets/useProviders-DDRxa8vl.js +0 -1
  152. package/client/dist/assets/useTv-Bh6WmNnZ.js +0 -1
  153. package/client/dist/assets/useTvLibrary-_Pqm8re1.js +0 -1
  154. package/server/dist/controllers/asmr-library.controller.js +0 -288
  155. package/server/dist/controllers/auth.controller.js +0 -225
  156. package/server/dist/controllers/data.controller.js +0 -767
  157. package/server/dist/controllers/insights.controller.js +0 -203
  158. package/server/dist/controllers/manga-library.controller.js +0 -350
  159. package/server/dist/controllers/settings.controller.js +0 -609
  160. package/server/dist/controllers/tv-library.controller.js +0 -314
  161. package/server/dist/controllers/watchlist.controller.js +0 -1069
  162. package/server/dist/request-logger.js +0 -16
  163. package/server/dist/routes/asmr-library.routes.js +0 -23
  164. package/server/dist/routes/asmr.routes.js +0 -81
  165. package/server/dist/routes/auth.routes.js +0 -25
  166. package/server/dist/routes/data.routes.js +0 -47
  167. package/server/dist/routes/discord-gateway.routes.js +0 -51
  168. package/server/dist/routes/insights.routes.js +0 -35
  169. package/server/dist/routes/lan-auth.routes.js +0 -46
  170. package/server/dist/routes/manga-library.routes.js +0 -25
  171. package/server/dist/routes/manga.routes.js +0 -141
  172. package/server/dist/routes/music.routes.js +0 -20
  173. package/server/dist/routes/providers.routes.js +0 -17
  174. package/server/dist/routes/proxy.routes.js +0 -12
  175. package/server/dist/routes/settings.routes.js +0 -24
  176. package/server/dist/routes/tracker.routes.js +0 -287
  177. package/server/dist/routes/translate.routes.js +0 -88
  178. package/server/dist/routes/tv-library.routes.js +0 -23
  179. package/server/dist/routes/tv.routes.js +0 -629
  180. package/server/dist/routes/watchlist.routes.js +0 -341
@@ -0,0 +1,133 @@
1
+ import { protectedProcedure, router } from '../index.js';
2
+ import { performAsmrWriteTransactionAsync } from '../../sync.js';
3
+ import { dbAll } from '../../utils/db-utils.js';
4
+ import { SettingsRepository } from '../../repositories/settings.repository.js';
5
+ import { AsmrLibraryRepository, AsmrProgressRepository, } from '../../repositories/asmr.repository.js';
6
+ import { defineSchema, reqObj, reqStr, reqStrArray } from '../validation.js';
7
+ async function getAdultNonListWorkIds(db) {
8
+ const rows = await dbAll(db, `SELECT DISTINCT p.workId as workId
9
+ FROM asmr_progress p
10
+ LEFT JOIN asmr_library l ON l.id = p.workId
11
+ WHERE COALESCE(l.isAdult, p.isAdult) = 1
12
+ AND l.id IS NULL`);
13
+ return rows.map((r) => r.workId);
14
+ }
15
+ const asmrProgressIdsInput = () => defineSchema((value) => ({
16
+ ids: reqStrArray(reqObj(value), 'ids', true),
17
+ }));
18
+ const asmrProgressWorkInput = () => defineSchema((value) => ({
19
+ workId: reqStr(reqObj(value), 'workId'),
20
+ }));
21
+ const asmrProgressSaveInput = () => defineSchema((value) => {
22
+ const obj = reqObj(value);
23
+ return {
24
+ workId: reqStr(obj, 'workId'),
25
+ trackIndex: obj.trackIndex,
26
+ trackLabel: obj.trackLabel,
27
+ currentTime: obj.currentTime,
28
+ duration: obj.duration,
29
+ title: obj.title ?? null,
30
+ thumbnail: obj.thumbnail ?? null,
31
+ rjCode: obj.rjCode ?? null,
32
+ isAdult: obj.isAdult ?? null,
33
+ };
34
+ });
35
+ const asmrProgressRemoveInput = () => defineSchema((value) => {
36
+ const obj = reqObj(value);
37
+ return { workId: reqStr(obj, 'workId'), trackIndex: obj.trackIndex ?? undefined };
38
+ });
39
+ const asmrContinueListeningInput = () => defineSchema((value) => {
40
+ const obj = reqObj(value);
41
+ const out = {};
42
+ if (obj.limit !== undefined && obj.limit !== null && obj.limit !== '') {
43
+ const limit = Number(obj.limit);
44
+ if (Number.isFinite(limit))
45
+ out.limit = limit;
46
+ }
47
+ return out;
48
+ });
49
+ export const asmrProgressRouter = router({
50
+ getByWork: protectedProcedure.input(asmrProgressWorkInput()).query(async ({ ctx, input }) => {
51
+ const rows = await AsmrProgressRepository.getByWork(ctx.asmrDb, input.workId);
52
+ return { progress: rows };
53
+ }),
54
+ latest: protectedProcedure.input(asmrProgressWorkInput()).query(async ({ ctx, input }) => {
55
+ const row = await AsmrProgressRepository.getLatest(ctx.asmrDb, input.workId);
56
+ return row || { currentTime: 0, duration: 0 };
57
+ }),
58
+ save: protectedProcedure.input(asmrProgressSaveInput()).mutation(async ({ ctx, input }) => {
59
+ const trackIndexNum = Math.max(Math.round(Number(input.trackIndex)) || 0, 0);
60
+ const timeNum = Math.max(Number(input.currentTime) || 0, 0);
61
+ const durationNum = Math.max(Number(input.duration) || 0, 0);
62
+ await performAsmrWriteTransactionAsync(ctx.asmrDb, async (tx) => {
63
+ await AsmrProgressRepository.upsert(tx, {
64
+ workId: String(input.workId),
65
+ trackIndex: trackIndexNum,
66
+ trackLabel: String(input.trackLabel || ''),
67
+ currentTime: timeNum,
68
+ duration: durationNum,
69
+ title: (input.title ?? null),
70
+ thumbnail: (input.thumbnail ?? null),
71
+ rjCode: (input.rjCode ?? null),
72
+ isAdult: input.isAdult != null ? Number(input.isAdult) : null,
73
+ });
74
+ await AsmrLibraryRepository.touchProgress(tx, String(input.workId), {
75
+ trackIndex: trackIndexNum,
76
+ trackLabel: String(input.trackLabel || ''),
77
+ position: timeNum,
78
+ });
79
+ });
80
+ return { success: true };
81
+ }),
82
+ remove: protectedProcedure.input(asmrProgressRemoveInput()).mutation(async ({ ctx, input }) => {
83
+ await performAsmrWriteTransactionAsync(ctx.asmrDb, async (tx) => {
84
+ if (input.trackIndex !== undefined && input.trackIndex !== null) {
85
+ await AsmrProgressRepository.deleteTrack(tx, String(input.workId), Number(input.trackIndex));
86
+ }
87
+ else {
88
+ await AsmrProgressRepository.deleteByWork(tx, String(input.workId));
89
+ }
90
+ });
91
+ return { success: true };
92
+ }),
93
+ removeMany: protectedProcedure.input(asmrProgressIdsInput()).mutation(async ({ ctx, input }) => {
94
+ const ids = input.ids.map((id) => String(id));
95
+ await performAsmrWriteTransactionAsync(ctx.asmrDb, async (tx) => {
96
+ await AsmrProgressRepository.deleteMany(tx, ids);
97
+ });
98
+ return { success: true, removed: ids.length };
99
+ }),
100
+ continueListening: protectedProcedure
101
+ .input(asmrContinueListeningInput())
102
+ .query(async ({ ctx, input }) => {
103
+ const limit = Math.min(Math.max(parseInt(String(input.limit ?? 24)) || 24, 1), 100);
104
+ const ignoreAdultRow = await SettingsRepository.getByKey(ctx.db, 'asmrIgnoreAdultContent');
105
+ const ignoreAdult = ignoreAdultRow ? ignoreAdultRow.value !== 'false' : true;
106
+ const listOnlyRow = await SettingsRepository.getByKey(ctx.db, 'asmrCwWatchlistOnly');
107
+ const listOnly = listOnlyRow
108
+ ? listOnlyRow.value === 'true' || listOnlyRow.value === '1'
109
+ : false;
110
+ const rows = (await AsmrProgressRepository.getContinueListening(ctx.asmrDb, limit)).filter((row) => {
111
+ if (ignoreAdult && row.isAdult === 1)
112
+ return false;
113
+ if (listOnly && row.watchlistStatus !== 'Listening')
114
+ return false;
115
+ return true;
116
+ });
117
+ return { data: rows, total: rows.length };
118
+ }),
119
+ adultCount: protectedProcedure.query(async ({ ctx }) => {
120
+ const ids = await getAdultNonListWorkIds(ctx.asmrDb);
121
+ return { count: ids.length };
122
+ }),
123
+ purgeAdult: protectedProcedure.mutation(async ({ ctx }) => {
124
+ const ids = await getAdultNonListWorkIds(ctx.asmrDb);
125
+ if (ids.length > 0) {
126
+ await performAsmrWriteTransactionAsync(ctx.asmrDb, async (tx) => {
127
+ for (const id of ids)
128
+ await AsmrProgressRepository.deleteByWork(tx, id);
129
+ });
130
+ }
131
+ return { success: true, removed: ids.length };
132
+ }),
133
+ });
@@ -0,0 +1,87 @@
1
+ import { protectedProcedure, router } from '../index.js';
2
+ import { defineSchema, optStr, reqObj, reqStr } from '../validation.js';
3
+ import { authRequiredError } from '../errors.js';
4
+ import logger from '../../logger.js';
5
+ async function cached(ctx, key, ttl, produce) {
6
+ const hit = ctx.apiCache.get(key);
7
+ if (hit)
8
+ return hit;
9
+ const body = await produce();
10
+ if (ttl !== undefined)
11
+ ctx.apiCache.set(key, body, ttl);
12
+ else
13
+ ctx.apiCache.set(key, body);
14
+ return body;
15
+ }
16
+ const asmrBrowseInput = () => defineSchema((value) => {
17
+ if (value === undefined || value === null)
18
+ return {};
19
+ const obj = reqObj(value);
20
+ const out = {};
21
+ const q = optStr(obj, 'q');
22
+ if (q !== undefined)
23
+ out.q = q;
24
+ if (obj.page !== undefined)
25
+ out.page = parseInt(String(obj.page), 10) || 1;
26
+ const sort = optStr(obj, 'sort');
27
+ if (sort !== undefined)
28
+ out.sort = sort;
29
+ const rating = optStr(obj, 'rating');
30
+ if (rating !== undefined)
31
+ out.rating = rating;
32
+ return out;
33
+ });
34
+ const asmrWorkInput = () => defineSchema((value) => ({ rj: reqStr(reqObj(value), 'rj') }));
35
+ export const asmrRouter = router({
36
+ browse: protectedProcedure.input(asmrBrowseInput()).query(async ({ ctx, input }) => {
37
+ const q = input.q || '';
38
+ const page = input.page || 1;
39
+ const sort = input.sort || '';
40
+ const rating = input.rating || '';
41
+ return cached(ctx, `route-asmr-browse-${q}-${page}-${sort}-${rating}-${sort === 'random' ? Date.now() : ''}`, 300, async () => {
42
+ try {
43
+ const provider = ctx.getJasmr();
44
+ if (!provider)
45
+ return { shows: [], hasNext: false };
46
+ return await provider.browse({ query: q, page, sort, rating });
47
+ }
48
+ catch (err) {
49
+ if (err.message === 'AUTH_REQUIRED')
50
+ throw authRequiredError('jasmr');
51
+ logger.error({ err }, '[Asmr] browse failed');
52
+ return { shows: [], hasNext: false };
53
+ }
54
+ });
55
+ }),
56
+ work: protectedProcedure.input(asmrWorkInput()).query(async ({ ctx, input }) => {
57
+ const rj = input.rj;
58
+ return cached(ctx, `route-asmr-work-${rj}`, 1800, async () => {
59
+ try {
60
+ const provider = ctx.getJasmr();
61
+ if (!provider) {
62
+ return { rjCode: rj, description: '', tracks: [], images: [], chapters: [] };
63
+ }
64
+ const rjCode = String(rj).trim().toUpperCase();
65
+ const episodes = await provider.getEpisodes(rjCode);
66
+ const [streams, images, chapters] = await Promise.all([
67
+ provider.getStreamUrls(rjCode, '1'),
68
+ provider.getImages(rjCode),
69
+ provider.getChapters(rjCode),
70
+ ]);
71
+ return {
72
+ rjCode,
73
+ description: episodes?.description || '',
74
+ tracks: streams?.[0]?.links || [],
75
+ images,
76
+ chapters,
77
+ };
78
+ }
79
+ catch (err) {
80
+ if (err.message === 'AUTH_REQUIRED')
81
+ throw authRequiredError('jasmr');
82
+ logger.error({ err, rj }, '[Asmr] work fetch failed');
83
+ return { rjCode: rj, description: '', tracks: [], images: [], chapters: [] };
84
+ }
85
+ });
86
+ }),
87
+ });
@@ -0,0 +1,247 @@
1
+ import path from 'path';
2
+ import logger from '../../logger.js';
3
+ import { googleDriveService } from '../../google.js';
4
+ import { githubSyncService } from '../../github-sync.js';
5
+ import { initSyncProvider, getActiveProvider, syncDownOnBoot, syncUp, mangaSyncDownOnBoot, mangaSyncUp, tvSyncDownOnBoot, tvSyncUp, asmrSyncDownOnBoot, asmrSyncUp, initializeDatabase, } from '../../sync.js';
6
+ import { CONFIG } from '../../config.js';
7
+ import { rcloneService } from '../../rclone.js';
8
+ import { updateEnvFile } from '../../utils/env.utils.js';
9
+ import { protectedProcedure, router } from '../index.js';
10
+ import { defineSchema, reqObj } from '../validation.js';
11
+ async function runSyncFromCtx(ctx, database, preferredProvider) {
12
+ const dbName = CONFIG.IS_DEV ? CONFIG.DB_NAME_DEV : CONFIG.DB_NAME_PROD;
13
+ const dbPath = path.join(CONFIG.ROOT, dbName);
14
+ const remoteFolder = CONFIG.IS_DEV ? CONFIG.REMOTE_FOLDER_DEV : CONFIG.REMOTE_FOLDER_PROD;
15
+ await initSyncProvider(preferredProvider);
16
+ if (getActiveProvider() === 'github' && githubSyncService.isAuthenticated()) {
17
+ try {
18
+ await githubSyncService.migrateFromAniWebSync();
19
+ }
20
+ catch (err) {
21
+ logger.error({ err }, 'GitHub sync migration from ani-web failed');
22
+ }
23
+ }
24
+ const didDownload = await syncDownOnBoot(database, dbPath, remoteFolder, () => {
25
+ return new Promise((resolve) => {
26
+ if (database && !database.isClosedCheck()) {
27
+ database.checkpoint();
28
+ database.close(() => resolve());
29
+ }
30
+ else {
31
+ resolve();
32
+ }
33
+ });
34
+ });
35
+ let currentDb = database;
36
+ if (didDownload) {
37
+ currentDb = await initializeDatabase(dbPath);
38
+ logger.info('Database re-initialized after sync.');
39
+ }
40
+ try {
41
+ await syncUp(currentDb, dbPath, remoteFolder);
42
+ }
43
+ catch (err) {
44
+ logger.error({ err }, 'Sync up on boot failed');
45
+ }
46
+ try {
47
+ await mangaSyncDownOnBoot(ctx.mangaDb, remoteFolder);
48
+ }
49
+ catch (err) {
50
+ logger.error({ err }, 'Manga sync down on boot failed');
51
+ }
52
+ try {
53
+ await mangaSyncUp(ctx.mangaDb, remoteFolder);
54
+ }
55
+ catch (err) {
56
+ logger.error({ err }, 'Manga sync up on boot failed');
57
+ }
58
+ try {
59
+ await tvSyncDownOnBoot(ctx.tvDb, remoteFolder);
60
+ }
61
+ catch (err) {
62
+ logger.error({ err }, 'TV sync down on boot failed');
63
+ }
64
+ try {
65
+ await tvSyncUp(ctx.tvDb, remoteFolder);
66
+ }
67
+ catch (err) {
68
+ logger.error({ err }, 'TV sync up on boot failed');
69
+ }
70
+ try {
71
+ await asmrSyncDownOnBoot(ctx.asmrDb, remoteFolder);
72
+ }
73
+ catch (err) {
74
+ logger.error({ err }, 'ASMR sync down on boot failed');
75
+ }
76
+ try {
77
+ await asmrSyncUp(ctx.asmrDb, remoteFolder);
78
+ }
79
+ catch (err) {
80
+ logger.error({ err }, 'ASMR sync up on boot failed');
81
+ }
82
+ }
83
+ const googleAuthSaveInput = () => defineSchema((value) => {
84
+ const obj = reqObj(value);
85
+ return { clientId: obj.clientId, clientSecret: obj.clientSecret, workerUrl: obj.workerUrl };
86
+ });
87
+ const githubAuthSaveInput = () => defineSchema((value) => {
88
+ const obj = reqObj(value);
89
+ if (typeof obj.clientId !== 'string')
90
+ throw new Error('clientId required');
91
+ return { clientId: obj.clientId };
92
+ });
93
+ const syncProviderInput = () => defineSchema((value) => ({
94
+ provider: reqObj(value).provider,
95
+ }));
96
+ const rcloneSaveInput = () => defineSchema((value) => ({
97
+ remote: reqObj(value).remote,
98
+ }));
99
+ export const authRouter = router({
100
+ configStatus: protectedProcedure.query(async () => {
101
+ const useWorker = !!CONFIG.GOOGLE_AUTH_WORKER_URL;
102
+ const hasLegacyConfig = !!process.env.GOOGLE_CLIENT_ID && !!process.env.GOOGLE_CLIENT_SECRET;
103
+ return { hasConfig: useWorker || hasLegacyConfig, useWorker };
104
+ }),
105
+ googleAuth: protectedProcedure.query(async () => {
106
+ return {
107
+ useWorker: !!CONFIG.GOOGLE_AUTH_WORKER_URL,
108
+ hasCustomWorkerUrl: !!process.env.GOOGLE_AUTH_WORKER_URL,
109
+ hasCustomClientId: !!process.env.GOOGLE_CLIENT_ID,
110
+ hasClientSecret: !!process.env.GOOGLE_CLIENT_SECRET,
111
+ };
112
+ }),
113
+ saveGoogleAuth: protectedProcedure.input(googleAuthSaveInput()).mutation(async ({ input }) => {
114
+ const { clientId, clientSecret, workerUrl } = input;
115
+ const updates = {};
116
+ if (typeof clientId === 'string') {
117
+ updates.GOOGLE_CLIENT_ID = clientId;
118
+ }
119
+ if (typeof clientSecret === 'string') {
120
+ updates.GOOGLE_CLIENT_SECRET = clientSecret;
121
+ }
122
+ if (typeof workerUrl === 'string') {
123
+ updates.GOOGLE_AUTH_WORKER_URL = workerUrl;
124
+ }
125
+ await updateEnvFile(updates);
126
+ if (typeof workerUrl === 'string') {
127
+ ;
128
+ CONFIG.GOOGLE_AUTH_WORKER_URL = workerUrl;
129
+ }
130
+ if (typeof clientId === 'string') {
131
+ ;
132
+ CONFIG.GOOGLE_CLIENT_ID = clientId || undefined;
133
+ }
134
+ if (typeof clientSecret === 'string') {
135
+ ;
136
+ CONFIG.GOOGLE_CLIENT_SECRET =
137
+ clientSecret || undefined;
138
+ }
139
+ await initSyncProvider();
140
+ return { success: true };
141
+ }),
142
+ githubAuth: protectedProcedure.query(async () => {
143
+ return { hasCustomClientId: !!process.env.GITHUB_CLIENT_ID };
144
+ }),
145
+ saveGithubAuth: protectedProcedure.input(githubAuthSaveInput()).mutation(async ({ input }) => {
146
+ await updateEnvFile({ GITHUB_CLIENT_ID: input.clientId });
147
+ return { success: true };
148
+ }),
149
+ rcloneSettings: protectedProcedure.query(async () => {
150
+ const remotes = await rcloneService.listRemotes();
151
+ return {
152
+ remote: CONFIG.RCLONE_REMOTE || '',
153
+ availableRemotes: remotes,
154
+ activeRemote: rcloneService.isActive() ? rcloneService.getRemoteName() : null,
155
+ };
156
+ }),
157
+ syncSettings: protectedProcedure.query(async () => {
158
+ return {
159
+ activeProvider: process.env.SYNC_PROVIDER || 'default',
160
+ actualActiveProvider: getActiveProvider(),
161
+ authenticatedProviders: {
162
+ github: githubSyncService.isAuthenticated(),
163
+ google: googleDriveService.isAuthenticated(),
164
+ rclone: rcloneService.isActive(),
165
+ },
166
+ };
167
+ }),
168
+ saveSyncProvider: protectedProcedure.input(syncProviderInput()).mutation(async ({ input }) => {
169
+ const { provider } = input;
170
+ const value = provider === 'default' ? '' : provider;
171
+ await updateEnvFile({ SYNC_PROVIDER: value });
172
+ await initSyncProvider();
173
+ return { success: true, activeProvider: process.env.SYNC_PROVIDER || 'default' };
174
+ }),
175
+ githubStatus: protectedProcedure.query(async () => {
176
+ try {
177
+ const user = await githubSyncService.getUserProfile();
178
+ return {
179
+ authenticated: !!user,
180
+ user,
181
+ device: githubSyncService.getDeviceState(),
182
+ hasCustomClientId: !!process.env.GITHUB_CLIENT_ID,
183
+ };
184
+ }
185
+ catch (error) {
186
+ logger.error({ err: error }, 'Failed to fetch GitHub auth status');
187
+ return {
188
+ authenticated: false,
189
+ user: null,
190
+ device: githubSyncService.getDeviceState(),
191
+ hasCustomClientId: !!process.env.GITHUB_CLIENT_ID,
192
+ };
193
+ }
194
+ }),
195
+ githubStart: protectedProcedure.mutation(async ({ ctx }) => {
196
+ const state = await githubSyncService.startDeviceAuth(ctx.db, (db, provider) => runSyncFromCtx(ctx, db, provider));
197
+ return state;
198
+ }),
199
+ githubPoll: protectedProcedure.query(async () => {
200
+ return githubSyncService.getDeviceState();
201
+ }),
202
+ githubLogout: protectedProcedure.mutation(async () => {
203
+ await githubSyncService.logout();
204
+ await updateEnvFile({ SYNC_PROVIDER: '' });
205
+ await initSyncProvider();
206
+ return { success: true };
207
+ }),
208
+ saveRclone: protectedProcedure.input(rcloneSaveInput()).mutation(async ({ ctx, input }) => {
209
+ const { remote } = input;
210
+ await updateEnvFile({
211
+ RCLONE_REMOTE: remote,
212
+ SYNC_PROVIDER: 'rclone',
213
+ });
214
+ await runSyncFromCtx(ctx, ctx.db, 'rclone');
215
+ return { success: true };
216
+ }),
217
+ googleAuthUrl: protectedProcedure.query(async () => {
218
+ const url = await googleDriveService.getAuthUrl();
219
+ return { url };
220
+ }),
221
+ googleLogin: protectedProcedure.mutation(async ({ ctx }) => {
222
+ if (googleDriveService.isAuthenticated()) {
223
+ const user = await googleDriveService.getUserProfile();
224
+ if (user) {
225
+ await updateEnvFile({ SYNC_PROVIDER: 'google' });
226
+ await runSyncFromCtx(ctx, ctx.db, 'google');
227
+ return { url: null, authenticated: true };
228
+ }
229
+ else {
230
+ logger.warn('Google tokens found but invalid. Clearing and requesting new auth.');
231
+ await googleDriveService.logout();
232
+ }
233
+ }
234
+ const url = await googleDriveService.getAuthUrl();
235
+ return { url, authenticated: false };
236
+ }),
237
+ user: protectedProcedure.query(async () => {
238
+ const user = await googleDriveService.getUserProfile();
239
+ return user;
240
+ }),
241
+ logout: protectedProcedure.mutation(async () => {
242
+ await googleDriveService.logout();
243
+ await updateEnvFile({ SYNC_PROVIDER: '' });
244
+ await initSyncProvider();
245
+ return { success: true };
246
+ }),
247
+ });
@@ -0,0 +1,83 @@
1
+ import { protectedProcedure, router } from '../index.js';
2
+ import { performWriteTransactionAsync } from '../../sync.js';
3
+ import { WatchedEpisodesRepository } from '../../repositories/watched-episodes.repository.js';
4
+ import { NotificationsRepository } from '../../repositories/notifications.repository.js';
5
+ import { getMigratedId } from '../../lib/migration.js';
6
+ import { getAdultNonWatchlistShowIds } from '../../hono/watchlist.js';
7
+ import { dbAll } from '../../utils/db-utils.js';
8
+ import { idsInput, showIdInput } from '../validation.js';
9
+ export const continueWatchingRouter = router({
10
+ remove: protectedProcedure.input(showIdInput()).mutation(async ({ ctx, input }) => {
11
+ const showId = await getMigratedId(ctx.db, input.showId);
12
+ await performWriteTransactionAsync(ctx.db, async (tx) => {
13
+ await WatchedEpisodesRepository.deleteByShow(tx, showId);
14
+ await NotificationsRepository.deleteByShow(tx, showId);
15
+ });
16
+ return { success: true };
17
+ }),
18
+ removeMany: protectedProcedure.input(idsInput()).mutation(async ({ ctx, input }) => {
19
+ const ids = await Promise.all(input.ids.map((id) => getMigratedId(ctx.db, id)));
20
+ await performWriteTransactionAsync(ctx.db, async (tx) => {
21
+ for (const id of ids) {
22
+ await WatchedEpisodesRepository.deleteByShow(tx, id);
23
+ await NotificationsRepository.deleteByShow(tx, id);
24
+ }
25
+ });
26
+ ctx.db.scheduleSave();
27
+ return { success: true, removed: ids.length };
28
+ }),
29
+ adultCount: protectedProcedure.query(async ({ ctx }) => {
30
+ const ids = await getAdultNonWatchlistShowIds(ctx.db);
31
+ return { count: ids.length };
32
+ }),
33
+ purgeAdult: protectedProcedure.mutation(async ({ ctx }) => {
34
+ const ids = await getAdultNonWatchlistShowIds(ctx.db);
35
+ if (ids.length > 0) {
36
+ await performWriteTransactionAsync(ctx.db, async (tx) => {
37
+ for (const id of ids) {
38
+ await WatchedEpisodesRepository.deleteByShow(tx, id);
39
+ await NotificationsRepository.deleteByShow(tx, id);
40
+ }
41
+ });
42
+ ctx.db.scheduleSave();
43
+ }
44
+ return { success: true, removed: ids.length };
45
+ }),
46
+ thisWeek: protectedProcedure.query(async ({ ctx }) => {
47
+ const rows = await dbAll(ctx.db, `SELECT
48
+ w.id, w.name, w.thumbnail, w.nativeName, w.englishName, w.type,
49
+ dn.episodeNumber, dn.discoveredAt
50
+ FROM discovered_notifications dn
51
+ JOIN watchlist w ON dn.showId = w.id
52
+ WHERE w.status = 'Watching'
53
+ AND EXISTS (
54
+ SELECT 1 FROM discovered_notifications dn_recent
55
+ WHERE dn_recent.showId = dn.showId
56
+ AND dn_recent.discoveredAt >= datetime('now', '-7 days')
57
+ )
58
+ AND CAST(dn.episodeNumber AS INTEGER) = (
59
+ SELECT MIN(CAST(dn2.episodeNumber AS INTEGER))
60
+ FROM discovered_notifications dn2
61
+ WHERE dn2.showId = dn.showId
62
+ AND NOT EXISTS (
63
+ SELECT 1 FROM watched_episodes we2
64
+ WHERE we2.showId = dn2.showId AND we2.episodeNumber = dn2.episodeNumber
65
+ )
66
+ )
67
+ AND NOT EXISTS (
68
+ SELECT 1 FROM watched_episodes we
69
+ WHERE we.showId = dn.showId AND we.episodeNumber = dn.episodeNumber
70
+ )
71
+ ORDER BY CAST(dn.episodeNumber AS INTEGER) ASC`);
72
+ return rows.map((row) => ({
73
+ _id: row.id,
74
+ id: row.id,
75
+ name: row.name,
76
+ thumbnail: row.thumbnail || '',
77
+ nativeName: row.nativeName,
78
+ englishName: row.englishName,
79
+ type: row.type,
80
+ episodeNumber: parseInt(row.episodeNumber) || row.episodeNumber,
81
+ }));
82
+ }),
83
+ });