@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
@@ -1,74 +1,45 @@
1
1
  process.setMaxListeners(100);
2
2
  import { EventEmitter } from 'events';
3
3
  EventEmitter.defaultMaxListeners = 100;
4
- import express from 'express';
5
4
  import path from 'path';
6
- import cors from 'cors';
7
- import compression from 'compression';
8
5
  import { AppCache } from './utils/cache.utils.js';
9
6
  import fs from 'fs';
10
7
  import crypto from 'crypto';
8
+ import { serve } from '@hono/node-server';
9
+ import { serveStatic } from '@hono/node-server/serve-static';
10
+ import { getConnInfo } from '@hono/node-server/conninfo';
11
11
  import logger from './logger.js';
12
12
  import { notifyServerExit } from './lib/ipc.js';
13
- import { crossSiteProtectionMiddleware, isAllowedOrigin } from './utils/security.utils.js';
14
- import { requestLogger } from './request-logger.js';
15
13
  import { githubSyncService } from './github-sync.js';
16
14
  import { CONFIG } from './config.js';
17
15
  import { initializeDatabase, initializeMangaDatabase, initializeTvDatabase, initializeAsmrDatabase, syncDownOnBoot, syncUp, mangaSyncDownOnBoot, mangaSyncUp, tvSyncDownOnBoot, tvSyncUp, asmrSyncDownOnBoot, asmrSyncUp, initSyncProvider, waitForSync, getActiveProvider, } from './sync.js';
18
- import { createAuthRouter } from './routes/auth.routes.js';
19
- import { createLanAuthRouter } from './routes/lan-auth.routes.js';
20
- import { lanAuthMiddleware } from './app-auth.js';
21
- import { createWatchlistRouter } from './routes/watchlist.routes.js';
22
- import { createDataRouter } from './routes/data.routes.js';
23
- import { createAsmrRouter } from './routes/asmr.routes.js';
24
- import { createAsmrLibraryRouter } from './routes/asmr-library.routes.js';
25
- import { createMangaRouter } from './routes/manga.routes.js';
26
- import { createMangaLibraryRouter } from './routes/manga-library.routes.js';
27
- import { createRadioRouter } from './routes/radio.routes.js';
28
- import { createMusicRouter } from './routes/music.routes.js';
29
- import { createTvRouter } from './routes/tv.routes.js';
30
- import { createTvLibraryRouter } from './routes/tv-library.routes.js';
31
- import { createProxyRouter } from './routes/proxy.routes.js';
32
- import { createProvidersRouter } from './routes/providers.routes.js';
33
16
  import { loadRemoteProviders } from './providers/remote-loader.js';
34
- import { createSettingsRouter } from './routes/settings.routes.js';
35
- import { createInsightsRouter } from './routes/insights.routes.js';
36
- import { createTranslateRouter } from './routes/translate.routes.js';
37
- import { createDiscordGatewayRouter } from './routes/discord-gateway.routes.js';
38
- import { createTrackerRouter } from './routes/tracker.routes.js';
39
17
  import { discordRPCService } from './discord-rpc.js';
40
18
  import { discordGatewayService } from './discord-gateway.js';
41
19
  import { SettingsRepository } from './repositories/settings.repository.js';
42
- import { requestContext } from './utils/request-context.js';
20
+ import { createHonoApp } from './app-hono.js';
21
+ import { startWatchlistDiscovery, stopWatchlistDiscovery } from './hono/watchlist.js';
43
22
  import { checkAnilistStatus } from './lib/anilist.js';
44
23
  import { offlineDb } from './lib/offline-db.js';
45
24
  import { initDiscordRolesSync } from './lib/discord-roles-sync.service.js';
46
- const app = express();
47
- app.use(requestLogger);
48
- app.get('/api/health', (_req, res) => {
49
- if (isShuttingDown) {
50
- return res.status(503).json({ status: 'shutting-down', ready: false });
51
- }
52
- if (!db) {
53
- return res.status(503).json({ status: 'starting', ready: false });
54
- }
55
- res.json({ status: 'ok', ready: true });
56
- });
57
- app.use((req, res, next) => {
58
- const store = new Map();
59
- if (req.headers['x-animepahe-ua']) {
60
- store.set('ua', req.headers['x-animepahe-ua']);
61
- }
62
- if (req.headers['x-animepahe-cookie']) {
63
- store.set('cookie', req.headers['x-animepahe-cookie']);
64
- }
65
- if (req.headers['x-jasmr-ua']) {
66
- store.set('jasmr_ua', req.headers['x-jasmr-ua']);
67
- }
68
- if (req.headers['x-jasmr-cookie']) {
69
- store.set('jasmr_cookie', req.headers['x-jasmr-cookie']);
70
- }
71
- requestContext.run(store, next);
25
+ const app = createHonoApp(() => ({ shuttingDown: isShuttingDown, dbReady: !!db }), () => ({
26
+ shuttingDown: isShuttingDown,
27
+ dbsReady: !!db && !!mangaDb && !!tvDb && !!asmrDb,
28
+ bootSyncing,
29
+ }), () => ({ db, mangaDb, tvDb, asmrDb }), {
30
+ initializeDatabase,
31
+ setDb: (newDb) => {
32
+ db = newDb;
33
+ },
34
+ }, {
35
+ getCatalog: getProviderCatalog,
36
+ refresh: refreshRemoteProviders,
37
+ }, runSyncSequence, {
38
+ getApiCache: () => apiCache,
39
+ getProviders: () => providers,
40
+ getMangaProvider: (name) => mangaProviders[name],
41
+ getTvProvider: (name) => tvProviders[name],
42
+ getJasmr: () => providers['jasmr'],
72
43
  });
73
44
  const apiCache = new AppCache({ ttlSeconds: 3600, maxKeys: 5000 });
74
45
  const providers = {};
@@ -203,95 +174,24 @@ async function runSyncSequence(database, mangaDatabase, preferredProvider) {
203
174
  logger.error({ err }, 'ASMR sync up on boot failed');
204
175
  }
205
176
  }
206
- app.use((req, res, next) => {
207
- if (isShuttingDown) {
208
- return res.status(503).send('Server is shutting down...');
209
- }
210
- if (!db || !mangaDb || !tvDb || !asmrDb) {
211
- return res.status(503).send('Database initializing...');
212
- }
213
- if (bootSyncing && req.method !== 'GET' && req.method !== 'HEAD' && req.method !== 'OPTIONS') {
214
- return res.status(503).send('Sync in progress...');
215
- }
216
- req.db = db;
217
- req.mangaDb = mangaDb;
218
- req.tvDb = tvDb;
219
- req.asmrDb = asmrDb;
220
- next();
221
- });
222
- app.use(compression({
223
- level: 2,
224
- threshold: 1024,
225
- filter: (req, res) => {
226
- if (req.headers['x-no-compression']) {
227
- return false;
228
- }
229
- return compression.filter(req, res);
230
- },
231
- }));
232
- app.use(cors({
233
- origin: (origin, callback) => {
234
- callback(null, isAllowedOrigin(origin));
235
- },
236
- credentials: true,
237
- }));
238
- app.use(crossSiteProtectionMiddleware);
239
- app.use(express.json({ limit: '10mb' }));
240
- app.use('/api/auth', createLanAuthRouter());
241
- app.use(lanAuthMiddleware);
242
- app.use('/api/auth', createAuthRouter((database, mangaDatabase, preferred) => runSyncSequence(database, mangaDatabase, preferred)));
243
- const { router: watchlistRouter, stopDiscovery } = createWatchlistRouter(() => db);
244
- app.use('/api', watchlistRouter);
245
- app.use('/api', createDataRouter(apiCache, providers, getProviderCatalog));
246
- app.use('/api', createAsmrRouter(apiCache, () => providers['jasmr']));
247
- app.use('/api', createAsmrLibraryRouter());
248
- app.use('/api', createMangaRouter(apiCache, (name) => mangaProviders[name]));
249
- app.use('/api', createMangaLibraryRouter());
250
- app.use('/api', createRadioRouter(apiCache));
251
- app.use('/api', createMusicRouter(apiCache));
252
- app.use('/api', createTvRouter(apiCache, (name) => tvProviders[name]));
253
- app.use('/api', createTvLibraryRouter());
254
- app.use('/api', createProxyRouter());
255
- app.use('/api', createProvidersRouter(getProviderCatalog, refreshRemoteProviders));
256
- app.use('/api', createInsightsRouter());
257
- app.use('/api', createTranslateRouter());
258
- app.use('/api', createDiscordGatewayRouter());
259
- app.use('/api', createTrackerRouter());
260
- app.use('/api', createSettingsRouter(() => db, initializeDatabase, (newDb) => {
261
- db = newDb;
262
- }));
177
+ startWatchlistDiscovery(() => db);
263
178
  if (!CONFIG.IS_DEV) {
264
179
  const frontendPath = path.join(CONFIG.PACKAGE_ROOT, 'client', 'dist');
265
180
  logger.info(`Serving frontend from: ${frontendPath}`);
266
- app.use(express.static(frontendPath));
267
- app.get(/^(?!\/api).+/, (req, res) => {
268
- res.sendFile('index.html', { root: frontendPath }, (err) => {
269
- if (err) {
270
- logger.error({ err }, `Failed to serve index.html from ${frontendPath}`);
271
- if (!res.headersSent) {
272
- res.status(500).send('Server Error: Frontend build not found.');
273
- }
274
- }
275
- });
276
- });
277
- }
278
- app.use((err, req, res, next) => {
279
- if (err instanceof SyntaxError &&
280
- 'body' in err &&
281
- req.headers['content-type']?.includes('application/json')) {
282
- if (!res.headersSent) {
283
- return res.status(400).json({ error: 'Invalid JSON', status: 400 });
181
+ app.use('/*', serveStatic({ root: frontendPath }));
182
+ app.get('*', async (c) => {
183
+ if (c.req.path.startsWith('/api/'))
184
+ return c.notFound();
185
+ try {
186
+ const html = await fs.promises.readFile(path.join(frontendPath, 'index.html'), 'utf8');
187
+ return c.html(html);
188
+ }
189
+ catch (err) {
190
+ logger.error({ err }, `Failed to serve index.html from ${frontendPath}`);
191
+ return c.text('Server Error: Frontend build not found.', 500);
284
192
  }
285
- }
286
- logger.error({ err, url: req.url, method: req.method }, 'Unhandled error');
287
- if (res.headersSent) {
288
- return next(err);
289
- }
290
- res.status(err.status || 500).json({
291
- error: err.message || 'Internal Server Error',
292
- status: err.status || 500,
293
193
  });
294
- });
194
+ }
295
195
  async function main() {
296
196
  const dbName = CONFIG.IS_DEV ? CONFIG.DB_NAME_DEV : CONFIG.DB_NAME_PROD;
297
197
  const dbPath = path.join(CONFIG.ROOT, dbName);
@@ -311,7 +211,7 @@ async function main() {
311
211
  asmrDb = await initializeAsmrDatabase(asmrDbPath);
312
212
  logger.info(`ASMR database initialized at ${asmrDbPath}`);
313
213
  await offlineDb.init(db);
314
- if (offlineDb.checkWeeklyUpdateDue(db)) {
214
+ if (await offlineDb.checkWeeklyUpdateDue(db)) {
315
215
  logger.info('Weekly offline database update is due on startup, starting background update...');
316
216
  offlineDb.executeScheduledUpdate(db).catch((err) => {
317
217
  logger.warn({ err: err?.message }, 'Startup scheduled offline database update failed');
@@ -364,7 +264,7 @@ async function main() {
364
264
  hasAsmrUnsyncedChanges = true;
365
265
  }
366
266
  });
367
- const expressServer = app.listen(CONFIG.PORT, () => {
267
+ const server = serve({ fetch: app.fetch, port: CONFIG.PORT }, () => {
368
268
  logger.info(`Server running on http://localhost:${CONFIG.PORT}`);
369
269
  });
370
270
  runSyncSequence(db, mangaDb)
@@ -419,8 +319,8 @@ async function main() {
419
319
  }
420
320
  }
421
321
  }, 300000);
422
- const offlineDbInterval = setInterval(() => {
423
- if (offlineDb.checkWeeklyUpdateDue(db)) {
322
+ const offlineDbInterval = setInterval(async () => {
323
+ if (await offlineDb.checkWeeklyUpdateDue(db)) {
424
324
  logger.info('Weekly offline database update triggered by periodic schedule...');
425
325
  offlineDb.executeScheduledUpdate(db).catch((err) => {
426
326
  logger.warn({ err: err?.message }, 'Interval scheduled offline database update failed');
@@ -432,7 +332,7 @@ async function main() {
432
332
  if (isShuttingDown)
433
333
  return;
434
334
  isShuttingDown = true;
435
- stopDiscovery();
335
+ stopWatchlistDiscovery();
436
336
  clearInterval(syncInterval);
437
337
  clearInterval(offlineDbInterval);
438
338
  discordRPCService.disconnect();
@@ -441,8 +341,8 @@ async function main() {
441
341
  await mangaWatcher.close();
442
342
  await tvWatcher.close();
443
343
  await asmrWatcher.close();
444
- if (expressServer) {
445
- await new Promise((resolve) => expressServer.close(() => resolve()));
344
+ if (server) {
345
+ await new Promise((resolve) => server.close(() => resolve()));
446
346
  }
447
347
  if (hasUnsyncedChanges) {
448
348
  logger.info('Sync on shutdown: uploading final database changes...');
@@ -502,25 +402,33 @@ async function main() {
502
402
  process.on('SIGTERM', () => shutdown('SIGTERM'));
503
403
  process.on('SIGHUP', () => shutdown('SIGHUP'));
504
404
  process.once('SIGUSR2', () => shutdown('SIGUSR2'));
505
- app.post('/api/internal/shutdown', (req, res) => {
506
- const isLoopback = req.ip === '::1' || req.ip === '127.0.0.1' || req.ip === '::ffff:127.0.0.1';
405
+ app.post('/api/internal/shutdown', (c) => {
406
+ let remoteAddress;
407
+ try {
408
+ remoteAddress = getConnInfo(c).remote.address ?? '';
409
+ }
410
+ catch {
411
+ remoteAddress = '';
412
+ }
413
+ const isLoopback = remoteAddress === '::1' ||
414
+ remoteAddress === '127.0.0.1' ||
415
+ remoteAddress === '::ffff:127.0.0.1';
507
416
  if (!isLoopback) {
508
- res.status(403).json({ error: 'Forbidden' });
509
- return;
417
+ return c.json({ error: 'Forbidden' }, 403);
510
418
  }
511
419
  const expectedToken = process.env.INTERNAL_SHUTDOWN_TOKEN;
512
- const providedToken = req.headers['x-internal-token'];
420
+ const providedToken = c.req.header('x-internal-token');
513
421
  if (expectedToken) {
514
422
  const expBuf = Buffer.from(expectedToken);
515
423
  const provBuf = Buffer.from(typeof providedToken === 'string' ? providedToken : '');
516
424
  if (expBuf.length !== provBuf.length || !crypto.timingSafeEqual(expBuf, provBuf)) {
517
425
  logger.warn('Unauthorized internal shutdown attempt rejected: invalid token');
518
- res.status(403).json({ error: 'Forbidden' });
519
- return;
426
+ return c.json({ error: 'Forbidden' }, 403);
520
427
  }
521
428
  }
522
- res.status(200).json({ message: 'Shutting down' });
429
+ const response = c.json({ message: 'Shutting down' });
523
430
  setTimeout(() => shutdown(), 500);
431
+ return response;
524
432
  });
525
433
  }
526
434
  main().catch((err) => {
@@ -7,6 +7,7 @@ import { rcloneService } from './rclone.js';
7
7
  import { githubSyncService } from './github-sync.js';
8
8
  import { CONFIG } from './config.js';
9
9
  import { DatabaseWrapper } from './db.js';
10
+ import { runTx } from './db/drizzle.js';
10
11
  import { dbGet } from './utils/db-utils.js';
11
12
  import { TempShowIdsRepository } from './repositories/temp-show-ids.repository.js';
12
13
  import { malCachePruneExpired } from './repositories/mal-cache.repository.js';
@@ -15,8 +16,6 @@ import { ANIME_SYNC_TABLES, exportTables, importTables, MANGA_SYNC_TABLES, TV_SY
15
16
  const log = logger.child({ module: 'Sync' });
16
17
  export const SYNC_TABLES = ANIME_SYNC_TABLES;
17
18
  export { MANGA_SYNC_TABLES, TV_SYNC_TABLES, ASMR_SYNC_TABLES };
18
- // anime_id_map (offline MAL<->AniList metadata) is intentionally local-only:
19
- // it is rebuilt weekly from the upstream JSON dump and never synced.
20
19
  async function exportSyncPayload(db) {
21
20
  return exportTables(db, SYNC_TABLES);
22
21
  }
@@ -568,6 +567,15 @@ export async function performWriteTransaction(db, runnable) {
568
567
  const newVersion = row?.value ?? 1;
569
568
  await setLocalManifestVersion(newVersion);
570
569
  }
570
+ export async function performWriteTransactionAsync(db, runnable) {
571
+ await runTx(db, async (tx) => {
572
+ await runnable(tx);
573
+ tx.run("UPDATE sync_metadata SET value = value + 1 WHERE key = 'db_version'");
574
+ });
575
+ const row = dbGet(db, "SELECT value FROM sync_metadata WHERE key = 'db_version'");
576
+ const newVersion = row?.value ?? 1;
577
+ await setLocalManifestVersion(newVersion);
578
+ }
571
579
  export async function performMangaWriteTransaction(db, runnable) {
572
580
  db.serialize(() => {
573
581
  runnable(db);
@@ -577,6 +585,15 @@ export async function performMangaWriteTransaction(db, runnable) {
577
585
  const newVersion = row?.value ?? 1;
578
586
  await setLocalMangaManifestVersion(newVersion);
579
587
  }
588
+ export async function performMangaWriteTransactionAsync(db, runnable) {
589
+ await runTx(db, async (tx) => {
590
+ await runnable(tx);
591
+ tx.run("UPDATE sync_metadata SET value = value + 1 WHERE key = 'db_version'");
592
+ });
593
+ const row = dbGet(db, "SELECT value FROM sync_metadata WHERE key = 'db_version'");
594
+ const newVersion = row?.value ?? 1;
595
+ await setLocalMangaManifestVersion(newVersion);
596
+ }
580
597
  export async function performTvWriteTransaction(db, runnable) {
581
598
  db.serialize(() => {
582
599
  runnable(db);
@@ -586,6 +603,15 @@ export async function performTvWriteTransaction(db, runnable) {
586
603
  const newVersion = row?.value ?? 1;
587
604
  await setLocalTvManifestVersion(newVersion);
588
605
  }
606
+ export async function performTvWriteTransactionAsync(db, runnable) {
607
+ await runTx(db, async (tx) => {
608
+ await runnable(tx);
609
+ tx.run("UPDATE sync_metadata SET value = value + 1 WHERE key = 'db_version'");
610
+ });
611
+ const row = dbGet(db, "SELECT value FROM sync_metadata WHERE key = 'db_version'");
612
+ const newVersion = row?.value ?? 1;
613
+ await setLocalTvManifestVersion(newVersion);
614
+ }
589
615
  export async function performAsmrWriteTransaction(db, runnable) {
590
616
  db.serialize(() => {
591
617
  runnable(db);
@@ -595,6 +621,15 @@ export async function performAsmrWriteTransaction(db, runnable) {
595
621
  const newVersion = row?.value ?? 1;
596
622
  await setLocalAsmrManifestVersion(newVersion);
597
623
  }
624
+ export async function performAsmrWriteTransactionAsync(db, runnable) {
625
+ await runTx(db, async (tx) => {
626
+ await runnable(tx);
627
+ tx.run("UPDATE sync_metadata SET value = value + 1 WHERE key = 'db_version'");
628
+ });
629
+ const row = dbGet(db, "SELECT value FROM sync_metadata WHERE key = 'db_version'");
630
+ const newVersion = row?.value ?? 1;
631
+ await setLocalAsmrManifestVersion(newVersion);
632
+ }
598
633
  export async function mangaSyncDownOnBoot(db, remoteFolderName) {
599
634
  let localVersion = await getLocalMangaManifestVersion();
600
635
  if (localVersion === 0 && db) {
@@ -1023,13 +1058,15 @@ export async function initializeTvDatabase(dbPath) {
1023
1058
  UPDATE tv_progress SET completed = 1
1024
1059
  WHERE mediaId = NEW.mediaId AND season = NEW.season AND episode = NEW.episode;
1025
1060
  END`);
1061
+ db.run(`DROP TRIGGER IF EXISTS trg_tv_progress_completed_update`);
1026
1062
  db.run(`CREATE TRIGGER IF NOT EXISTS trg_tv_progress_completed_update
1027
1063
  AFTER UPDATE OF currentTime, duration ON tv_progress
1028
- WHEN OLD.completed = 1 OR (NEW.duration > 0 AND NEW.currentTime >= NEW.duration * 0.8)
1064
+ WHEN NEW.duration > 0 AND NEW.currentTime >= NEW.duration * 0.8
1029
1065
  BEGIN
1030
1066
  UPDATE tv_progress SET completed = 1
1031
1067
  WHERE mediaId = NEW.mediaId AND season = NEW.season AND episode = NEW.episode;
1032
1068
  END`);
1069
+ db.run(`UPDATE tv_progress SET completed = 0 WHERE completed = 1 AND NOT (duration > 0 AND currentTime >= duration * 0.8)`);
1033
1070
  db.run(`UPDATE OR IGNORE tv_progress SET mediaId = REPLACE(mediaId, '-', ':') WHERE mediaId LIKE 'tv-%' OR mediaId LIKE 'movie-%'`);
1034
1071
  db.run(`DELETE FROM tv_progress WHERE mediaId LIKE 'tv-%' OR mediaId LIKE 'movie-%'`);
1035
1072
  return db;
@@ -1099,7 +1136,7 @@ export async function initializeDatabase(dbPath) {
1099
1136
  db.run(`CREATE TABLE IF NOT EXISTS temp_show_ids (id TEXT PRIMARY KEY, provider TEXT NOT NULL, nativeId TEXT NOT NULL, title TEXT NOT NULL, thumbnail TEXT, createdAt INTEGER NOT NULL)`);
1100
1137
  db.run(`CREATE UNIQUE INDEX IF NOT EXISTS idx_temp_show_ids_provider_native ON temp_show_ids(provider, nativeId)`);
1101
1138
  try {
1102
- const purged = TempShowIdsRepository.purge(db);
1139
+ const purged = await TempShowIdsRepository.purge(db);
1103
1140
  if (purged > 0)
1104
1141
  logger.info({ purged }, 'Purged stale temp show ids on boot');
1105
1142
  }
@@ -0,0 +1,7 @@
1
+ import { getTokenFromHeaders, hasAppPassword, validateLanSession } from '../app-auth.js';
2
+ export function buildTrpcContext(deps, authorization, cookie) {
3
+ return {
4
+ ...deps,
5
+ lanAuthed: !hasAppPassword() || validateLanSession(getTokenFromHeaders(authorization, cookie)),
6
+ };
7
+ }
@@ -0,0 +1,21 @@
1
+ import { TRPCError } from '@trpc/server';
2
+ export function isAuthErrorCause(value) {
3
+ if (typeof value !== 'object' || value === null)
4
+ return false;
5
+ const code = value.code;
6
+ return code === 'AUTH_REQUIRED' || code === 'LAN_AUTH_REQUIRED';
7
+ }
8
+ export function authRequiredError(provider) {
9
+ return new TRPCError({
10
+ code: 'FORBIDDEN',
11
+ message: 'AUTH_REQUIRED',
12
+ cause: { code: 'AUTH_REQUIRED', provider },
13
+ });
14
+ }
15
+ export function lanAuthRequiredError() {
16
+ return new TRPCError({
17
+ code: 'UNAUTHORIZED',
18
+ message: 'LAN_AUTH_REQUIRED',
19
+ cause: { code: 'LAN_AUTH_REQUIRED' },
20
+ });
21
+ }
@@ -0,0 +1,19 @@
1
+ import { initTRPC } from '@trpc/server';
2
+ import { lanAuthRequiredError } from './errors.js';
3
+ const t = initTRPC.context().create({
4
+ errorFormatter: ({ shape, error }) => ({
5
+ ...shape,
6
+ data: {
7
+ ...shape.data,
8
+ cause: error.cause ?? null,
9
+ },
10
+ }),
11
+ });
12
+ export const router = t.router;
13
+ export const publicProcedure = t.procedure;
14
+ const lanGuard = t.middleware(({ ctx, next }) => {
15
+ if (!ctx.lanAuthed)
16
+ throw lanAuthRequiredError();
17
+ return next({ ctx });
18
+ });
19
+ export const protectedProcedure = t.procedure.use(lanGuard);
@@ -0,0 +1,49 @@
1
+ import { router } from './index.js';
2
+ import { settingsRouter } from './routers/settings.js';
3
+ import { queueRouter } from './routers/queue.js';
4
+ import { watchlistRouter } from './routers/watchlist.js';
5
+ import { progressRouter } from './routers/progress.js';
6
+ import { notificationsRouter } from './routers/notifications.js';
7
+ import { continueWatchingRouter } from './routers/continue-watching.js';
8
+ import { discoveryRouter } from './routers/discovery.js';
9
+ import { mangaLibraryRouter } from './routers/manga-library.js';
10
+ import { mangaProgressRouter } from './routers/manga-progress.js';
11
+ import { asmrLibraryRouter } from './routers/asmr-library.js';
12
+ import { asmrProgressRouter } from './routers/asmr-progress.js';
13
+ import { tvLibraryRouter } from './routers/tv-library.js';
14
+ import { tvProgressRouter } from './routers/tv-progress.js';
15
+ import { tvRouter } from './routers/tv.js';
16
+ import { mangaRouter } from './routers/manga.js';
17
+ import { asmrRouter } from './routers/asmr.js';
18
+ import { musicRouter } from './routers/music.js';
19
+ import { radioRouter } from './routers/radio.js';
20
+ import { trackerRouter } from './routers/tracker.js';
21
+ import { insightsRouter } from './routers/insights.js';
22
+ import { authRouter } from './routers/auth.js';
23
+ import { dataRouter } from './routers/data.js';
24
+ import { discordRouter } from './routers/discord.js';
25
+ export const appRouter = router({
26
+ settings: settingsRouter,
27
+ queue: queueRouter,
28
+ watchlist: watchlistRouter,
29
+ progress: progressRouter,
30
+ notifications: notificationsRouter,
31
+ continueWatching: continueWatchingRouter,
32
+ discovery: discoveryRouter,
33
+ mangaLibrary: mangaLibraryRouter,
34
+ mangaProgress: mangaProgressRouter,
35
+ asmrLibrary: asmrLibraryRouter,
36
+ asmrProgress: asmrProgressRouter,
37
+ tvLibrary: tvLibraryRouter,
38
+ tvProgress: tvProgressRouter,
39
+ tv: tvRouter,
40
+ manga: mangaRouter,
41
+ asmr: asmrRouter,
42
+ music: musicRouter,
43
+ radio: radioRouter,
44
+ tracker: trackerRouter,
45
+ insights: insightsRouter,
46
+ auth: authRouter,
47
+ data: dataRouter,
48
+ discord: discordRouter,
49
+ });
@@ -0,0 +1,122 @@
1
+ import { TRPCError } from '@trpc/server';
2
+ import { protectedProcedure, router } from '../index.js';
3
+ import { performAsmrWriteTransactionAsync } from '../../sync.js';
4
+ import { buildAsmrId, ASMR_STATUSES, AsmrLibraryRepository, AsmrProgressRepository, } from '../../repositories/asmr.repository.js';
5
+ import { defineSchema, reqObj, reqStr, optStr, reqStrArray } from '../validation.js';
6
+ const asmrLibraryListInput = () => defineSchema((value) => {
7
+ const obj = reqObj(value);
8
+ const out = {};
9
+ const status = optStr(obj, 'status');
10
+ if (status !== undefined)
11
+ out.status = status;
12
+ if (obj.page !== undefined && obj.page !== null && obj.page !== '') {
13
+ const page = Number(obj.page);
14
+ if (Number.isFinite(page))
15
+ out.page = page;
16
+ }
17
+ if (obj.limit !== undefined && obj.limit !== null && obj.limit !== '') {
18
+ const limit = Number(obj.limit);
19
+ if (Number.isFinite(limit))
20
+ out.limit = limit;
21
+ }
22
+ return out;
23
+ });
24
+ const asmrLibraryIdInput = () => defineSchema((value) => ({
25
+ id: reqStr(reqObj(value), 'id'),
26
+ }));
27
+ const asmrLibraryAddInput = () => defineSchema((value) => {
28
+ const obj = reqObj(value);
29
+ return {
30
+ rjCode: optStr(obj, 'rjCode'),
31
+ id: optStr(obj, 'id'),
32
+ title: optStr(obj, 'title'),
33
+ thumbnail: optStr(obj, 'thumbnail'),
34
+ status: optStr(obj, 'status'),
35
+ isAdult: obj.isAdult,
36
+ };
37
+ });
38
+ const asmrLibraryStatusInput = () => defineSchema((value) => {
39
+ const obj = reqObj(value);
40
+ return { id: reqStr(obj, 'id'), status: reqStr(obj, 'status') };
41
+ });
42
+ const asmrLibraryBatchStatusInput = () => defineSchema((value) => {
43
+ const obj = reqObj(value);
44
+ return { ids: reqStrArray(obj, 'ids', true), status: reqStr(obj, 'status') };
45
+ });
46
+ const asmrLibraryIdsInput = () => defineSchema((value) => ({
47
+ ids: reqStrArray(reqObj(value), 'ids', true),
48
+ }));
49
+ export const asmrLibraryRouter = router({
50
+ list: protectedProcedure.input(asmrLibraryListInput()).query(async ({ ctx, input }) => {
51
+ const page = Math.max(parseInt(String(input.page ?? 1)) || 1, 1);
52
+ const limit = Math.min(Math.max(parseInt(String(input.limit ?? 24)) || 24, 1), 100);
53
+ const offset = (page - 1) * limit;
54
+ const rows = await AsmrLibraryRepository.getAll(ctx.asmrDb, input.status, limit, offset);
55
+ const total = await AsmrLibraryRepository.getCount(ctx.asmrDb, input.status);
56
+ return { data: rows, total, page, limit };
57
+ }),
58
+ ids: protectedProcedure.query(async ({ ctx }) => {
59
+ return { ids: await AsmrLibraryRepository.getIds(ctx.asmrDb) };
60
+ }),
61
+ check: protectedProcedure.input(asmrLibraryIdInput()).query(async ({ ctx, input }) => {
62
+ const item = await AsmrLibraryRepository.getById(ctx.asmrDb, input.id);
63
+ return { inLibrary: !!item, status: item?.status ?? null };
64
+ }),
65
+ add: protectedProcedure.input(asmrLibraryAddInput()).mutation(async ({ ctx, input }) => {
66
+ const rawCode = String(input.rjCode || input.id || '').trim();
67
+ if (!rawCode) {
68
+ throw new TRPCError({ code: 'BAD_REQUEST', message: 'rjCode is required' });
69
+ }
70
+ const id = input.id && input.id.trim() ? buildAsmrId(input.id) : buildAsmrId(rawCode);
71
+ const cleanStatus = typeof input.status === 'string' && ASMR_STATUSES.includes(input.status)
72
+ ? input.status
73
+ : 'Listening';
74
+ await performAsmrWriteTransactionAsync(ctx.asmrDb, async (tx) => {
75
+ await AsmrLibraryRepository.upsert(tx, {
76
+ id,
77
+ rjCode: buildAsmrId(rawCode),
78
+ title: String(input.title || ''),
79
+ thumbnail: String(input.thumbnail || ''),
80
+ status: cleanStatus,
81
+ isAdult: input.isAdult === true || input.isAdult === 1,
82
+ });
83
+ });
84
+ return { success: true, id };
85
+ }),
86
+ remove: protectedProcedure.input(asmrLibraryIdInput()).mutation(async ({ ctx, input }) => {
87
+ await performAsmrWriteTransactionAsync(ctx.asmrDb, async (tx) => {
88
+ await AsmrLibraryRepository.delete(tx, String(input.id));
89
+ await AsmrProgressRepository.deleteByWork(tx, String(input.id));
90
+ });
91
+ return { success: true };
92
+ }),
93
+ setStatus: protectedProcedure.input(asmrLibraryStatusInput()).mutation(async ({ ctx, input }) => {
94
+ if (!input.id || !ASMR_STATUSES.includes(input.status)) {
95
+ throw new TRPCError({ code: 'BAD_REQUEST', message: 'id and a valid status are required' });
96
+ }
97
+ await performAsmrWriteTransactionAsync(ctx.asmrDb, async (tx) => {
98
+ await AsmrLibraryRepository.updateStatus(tx, String(input.id), String(input.status));
99
+ });
100
+ return { success: true };
101
+ }),
102
+ batchStatus: protectedProcedure
103
+ .input(asmrLibraryBatchStatusInput())
104
+ .mutation(async ({ ctx, input }) => {
105
+ if (!input.status || !ASMR_STATUSES.includes(input.status)) {
106
+ throw new TRPCError({ code: 'BAD_REQUEST', message: 'a valid status is required' });
107
+ }
108
+ const ids = input.ids.map((id) => String(id));
109
+ await performAsmrWriteTransactionAsync(ctx.asmrDb, async (tx) => {
110
+ await AsmrLibraryRepository.updateStatusMany(tx, ids, String(input.status));
111
+ });
112
+ return { success: true, updated: ids.length };
113
+ }),
114
+ removeMany: protectedProcedure.input(asmrLibraryIdsInput()).mutation(async ({ ctx, input }) => {
115
+ const ids = input.ids.map((id) => String(id));
116
+ await performAsmrWriteTransactionAsync(ctx.asmrDb, async (tx) => {
117
+ await AsmrLibraryRepository.deleteMany(tx, ids);
118
+ await AsmrProgressRepository.deleteMany(tx, ids);
119
+ });
120
+ return { success: true, removed: ids.length };
121
+ }),
122
+ });