@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.
- package/README.md +1 -1
- package/client/dist/assets/{AnimeCard-D4qo4289.js → AnimeCard-CJy1SXH3.js} +1 -1
- package/client/dist/assets/{AnimeInfo-BdRRm4So.js → AnimeInfo-BoXlVvZ-.js} +1 -1
- package/client/dist/assets/{AnimeInfoPage-DeJNAKMR.js → AnimeInfoPage-BxbA_7AV.js} +2 -2
- package/client/dist/assets/Asmr--Zc8G9zv.js +2 -0
- package/client/dist/assets/AsmrResetProgressModal-CKYm-8cp.js +1 -0
- package/client/dist/assets/AvSyncCalibrator-3yjZS7s6.js +10 -0
- package/client/dist/assets/Home-rrhOGQcg.js +1 -0
- package/client/dist/assets/Insights-Bay90Yeg.js +1 -0
- package/client/dist/assets/{LibraryListPage-D3hTqTRU.js → LibraryListPage-HzuDavbb.js} +1 -1
- package/client/dist/assets/{ListeningList-BgWsfu40.js → ListeningList-DYcQnnsG.js} +1 -1
- package/client/dist/assets/{Manga-Dn-CYins.js → Manga-D8CT9aO0.js} +1 -1
- package/client/dist/assets/{MangaInfoPage-EmQoK7Bv.js → MangaInfoPage-DGK6CYp1.js} +1 -1
- package/client/dist/assets/MangaReadPage-B9XoDKF9.js +1 -0
- package/client/dist/assets/{MangaResetProgressModal-Dian37PQ.js → MangaResetProgressModal-QjZyIAjl.js} +1 -1
- package/client/dist/assets/Mature-CHt_-Hz5.js +1 -0
- package/client/dist/assets/MediaCard-B5584Cq9.js +1 -0
- package/client/dist/assets/Music-HeLrEZ2d.js +1 -0
- package/client/dist/assets/OptionListMenu-CJ90CdwI.js +1 -0
- package/client/dist/assets/Player-BYtEax_z.js +56 -0
- package/client/dist/assets/PlayerSettings-4-cUAY4B.js +1 -0
- package/client/dist/assets/QueueOptionsButton-Ba0dg_82.js +1 -0
- package/client/dist/assets/{QueueRail-B7DeSFoA.js → QueueRail-O7f4RqjB.js} +1 -1
- package/client/dist/assets/Radio-DPZxQnyD.js +1 -0
- package/client/dist/assets/{ReadingList-BT8kUjCB.js → ReadingList-n6rr8ygy.js} +1 -1
- package/client/dist/assets/{ResetProgressModal-DWeiO-hs.js → ResetProgressModal-CcStlbhP.js} +1 -1
- package/client/dist/assets/Search-C2wrWOdy.js +1 -0
- package/client/dist/assets/Settings-CluUYs8U.js +21 -0
- package/client/dist/assets/{SkeletonGrid-B5loz7ii.js → SkeletonGrid-CLEPY9nP.js} +1 -1
- package/client/dist/assets/Trackers-CTGPxs0V.js +2 -0
- package/client/dist/assets/Tv-BwHlDdi8.js +4 -0
- package/client/dist/assets/Tv-ZEA45gXK.css +1 -0
- package/client/dist/assets/TvInfo-CXnrLekr.js +1 -0
- package/client/dist/assets/TvPopup-BekfiNcX.js +1 -0
- package/client/dist/assets/{TvSearch-BYwCf5kO.js → TvSearch-DuqR-z3v.js} +1 -1
- package/client/dist/assets/{TvWatchlist-BiJU6pKT.js → TvWatchlist-CBvgFrA0.js} +1 -1
- package/client/dist/assets/Watchlist-mj8mTOam.js +1 -0
- package/client/dist/assets/{hls-C11FOWHj.js → hls-Boje3IOE.js} +1 -1
- package/client/dist/assets/index-BXwhFWB_.js +178 -0
- package/client/dist/assets/useAnimeInfoData-DJ7A4B4T.js +1 -0
- package/client/dist/assets/useAsmr-DIwIsJXF.js +1 -0
- package/client/dist/assets/{useAsmrPopup-cT_Uu41e.js → useAsmrPopup-e4zNN9ig.js} +1 -1
- package/client/dist/assets/useMangaLibrary-C9Lci5N4.js +1 -0
- package/client/dist/assets/{useMangaPopup-PnmSctHu.js → useMangaPopup-B8nnpvyi.js} +1 -1
- package/client/dist/assets/useProviders-B5Qr4I1y.js +1 -0
- package/client/dist/assets/useTv-Dm8MCYlL.js +1 -0
- package/client/dist/assets/useTvLibrary-B7jEk2Q7.js +1 -0
- package/client/dist/assets/{vendor-query-BXRECSI6.js → vendor-query-BUTye89G.js} +1 -1
- package/client/dist/index.html +2 -2
- package/orchestrator.js +1 -2
- package/package.json +2 -6
- package/server/dist/app-auth.js +9 -29
- package/server/dist/app-hono.js +621 -0
- package/server/dist/config.js +0 -1
- package/server/dist/db/drizzle.js +32 -0
- package/server/dist/db/schema-anime.js +64 -0
- package/server/dist/db/schema-asmr.js +25 -0
- package/server/dist/db/schema-manga.js +31 -0
- package/server/dist/db/schema-tv.js +33 -0
- package/server/dist/db.js +75 -4
- package/server/dist/discord-gateway.js +1 -1
- package/server/dist/google.js +0 -7
- package/server/dist/hono/asmr.js +1 -0
- package/server/dist/hono/auth.js +56 -0
- package/server/dist/hono/music.js +419 -0
- package/server/dist/{controllers/proxy.controller.js → hono/proxy.js} +390 -394
- package/server/dist/hono/tv.js +197 -0
- package/server/dist/hono/watchlist.js +733 -0
- package/server/dist/lib/anilist.js +3 -3
- package/server/dist/lib/insights.js +185 -0
- package/server/dist/lib/ipc.js +1 -2
- package/server/dist/lib/kitsu.js +1 -28
- package/server/dist/lib/mal-import.js +291 -0
- package/server/dist/lib/migration.js +20 -34
- package/server/dist/lib/offline-db.js +36 -36
- package/server/dist/lib/tracker/manga-sync.service.js +21 -21
- package/server/dist/lib/tracker/sync.service.js +20 -20
- package/server/dist/lib/translate.js +79 -0
- package/server/dist/lib/ytmusic.js +15 -4
- package/server/dist/providers/title-matching.js +0 -1
- package/server/dist/repositories/asmr.repository.js +90 -87
- package/server/dist/repositories/insights.repository.js +24 -13
- package/server/dist/repositories/library.repository.js +33 -93
- package/server/dist/repositories/manga.repository.js +108 -116
- package/server/dist/repositories/notifications.repository.js +50 -31
- package/server/dist/repositories/queue.repository.js +36 -25
- package/server/dist/repositories/settings.repository.js +18 -8
- package/server/dist/repositories/shows-meta.repository.js +54 -41
- package/server/dist/repositories/temp-show-ids.repository.js +43 -31
- package/server/dist/repositories/tv.repository.js +98 -115
- package/server/dist/repositories/watched-episodes.repository.js +52 -20
- package/server/dist/repositories/watchlist.repository.js +36 -48
- package/server/dist/server.js +59 -151
- package/server/dist/sync.js +41 -4
- package/server/dist/trpc/context.js +7 -0
- package/server/dist/trpc/errors.js +21 -0
- package/server/dist/trpc/index.js +19 -0
- package/server/dist/trpc/router.js +49 -0
- package/server/dist/trpc/routers/asmr-library.js +122 -0
- package/server/dist/trpc/routers/asmr-progress.js +133 -0
- package/server/dist/trpc/routers/asmr.js +87 -0
- package/server/dist/trpc/routers/auth.js +247 -0
- package/server/dist/trpc/routers/continue-watching.js +83 -0
- package/server/dist/trpc/routers/data.js +1039 -0
- package/server/dist/trpc/routers/discord.js +288 -0
- package/server/dist/trpc/routers/discovery.js +15 -0
- package/server/dist/trpc/routers/insights.js +45 -0
- package/server/dist/trpc/routers/manga-library.js +280 -0
- package/server/dist/trpc/routers/manga-progress.js +181 -0
- package/server/dist/trpc/routers/manga.js +201 -0
- package/server/dist/{controllers/music.controller.js → trpc/routers/music.js} +235 -295
- package/server/dist/trpc/routers/notifications.js +80 -0
- package/server/dist/trpc/routers/progress.js +15 -0
- package/server/dist/trpc/routers/queue.js +95 -0
- package/server/dist/{routes/radio.routes.js → trpc/routers/radio.js} +30 -23
- package/server/dist/trpc/routers/settings.js +105 -0
- package/server/dist/trpc/routers/tracker.js +219 -0
- package/server/dist/trpc/routers/tv-library.js +145 -0
- package/server/dist/trpc/routers/tv-progress.js +171 -0
- package/server/dist/trpc/routers/tv.js +565 -0
- package/server/dist/trpc/routers/watchlist.js +564 -0
- package/server/dist/trpc/validation.js +159 -0
- package/server/dist/utils/security.utils.js +0 -18
- package/server/package.json +5 -10
- package/client/dist/assets/Asmr-D0EGRYsv.js +0 -2
- package/client/dist/assets/AsmrResetProgressModal-Gs0GmVaT.js +0 -1
- package/client/dist/assets/AvSyncCalibrator-D8hqarVB.js +0 -10
- package/client/dist/assets/Home-BnBzjFkW.js +0 -1
- package/client/dist/assets/Insights-i9QgY3nE.js +0 -1
- package/client/dist/assets/MangaReadPage-xJB8K2YU.js +0 -1
- package/client/dist/assets/Mature-DQtXKOXc.js +0 -1
- package/client/dist/assets/MediaCard-DpKoY-1j.js +0 -1
- package/client/dist/assets/Music-BP43Zqmq.js +0 -1
- package/client/dist/assets/OptionListMenu-Bj9ebsW3.js +0 -1
- package/client/dist/assets/Player-O2Cy2GF1.js +0 -56
- package/client/dist/assets/PlayerSettings-DYZ272Yp.js +0 -1
- package/client/dist/assets/QueueOptionsButton-lv26-6jp.js +0 -1
- package/client/dist/assets/Radio-qQBYnVfY.js +0 -1
- package/client/dist/assets/Search-BkvPYN33.js +0 -1
- package/client/dist/assets/Settings-CyZEBF6S.js +0 -21
- package/client/dist/assets/Trackers-JK-aXBYC.js +0 -2
- package/client/dist/assets/Tv-BwXWJxMy.js +0 -4
- package/client/dist/assets/Tv-C2SWcVzN.css +0 -1
- package/client/dist/assets/TvInfo-Dzyq6ucH.js +0 -1
- package/client/dist/assets/TvPopup-PlCU30FO.js +0 -1
- package/client/dist/assets/Watchlist-BOEeGXH9.js +0 -1
- package/client/dist/assets/index-CtoKRNLc.js +0 -178
- package/client/dist/assets/useAnimeInfoData-kr3WOIWH.js +0 -1
- package/client/dist/assets/useAsmr-CB9THf0m.js +0 -1
- package/client/dist/assets/useMangaLibrary-rM6z3_0E.js +0 -1
- package/client/dist/assets/useProviders-B3VB7A9M.js +0 -1
- package/client/dist/assets/useTv-BTUKMH7Q.js +0 -1
- package/client/dist/assets/useTvLibrary-C0sX-ilZ.js +0 -1
- package/server/dist/controllers/asmr-library.controller.js +0 -288
- package/server/dist/controllers/auth.controller.js +0 -225
- package/server/dist/controllers/data.controller.js +0 -767
- package/server/dist/controllers/insights.controller.js +0 -203
- package/server/dist/controllers/manga-library.controller.js +0 -350
- package/server/dist/controllers/settings.controller.js +0 -609
- package/server/dist/controllers/tv-library.controller.js +0 -314
- package/server/dist/controllers/watchlist.controller.js +0 -1069
- package/server/dist/request-logger.js +0 -16
- package/server/dist/routes/asmr-library.routes.js +0 -23
- package/server/dist/routes/asmr.routes.js +0 -81
- package/server/dist/routes/auth.routes.js +0 -25
- package/server/dist/routes/data.routes.js +0 -47
- package/server/dist/routes/discord-gateway.routes.js +0 -51
- package/server/dist/routes/insights.routes.js +0 -35
- package/server/dist/routes/lan-auth.routes.js +0 -46
- package/server/dist/routes/manga-library.routes.js +0 -25
- package/server/dist/routes/manga.routes.js +0 -141
- package/server/dist/routes/music.routes.js +0 -20
- package/server/dist/routes/providers.routes.js +0 -17
- package/server/dist/routes/proxy.routes.js +0 -12
- package/server/dist/routes/settings.routes.js +0 -24
- package/server/dist/routes/tracker.routes.js +0 -287
- package/server/dist/routes/translate.routes.js +0 -88
- package/server/dist/routes/tv-library.routes.js +0 -23
- package/server/dist/routes/tv.routes.js +0 -629
- package/server/dist/routes/watchlist.routes.js +0 -341
package/server/dist/server.js
CHANGED
|
@@ -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 {
|
|
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 =
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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(
|
|
267
|
-
app.get(
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
|
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
|
-
|
|
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 (
|
|
445
|
-
await new Promise((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', (
|
|
506
|
-
|
|
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
|
-
|
|
509
|
-
return;
|
|
417
|
+
return c.json({ error: 'Forbidden' }, 403);
|
|
510
418
|
}
|
|
511
419
|
const expectedToken = process.env.INTERNAL_SHUTDOWN_TOKEN;
|
|
512
|
-
const providedToken = req.
|
|
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
|
-
|
|
519
|
-
return;
|
|
426
|
+
return c.json({ error: 'Forbidden' }, 403);
|
|
520
427
|
}
|
|
521
428
|
}
|
|
522
|
-
|
|
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) => {
|
package/server/dist/sync.js
CHANGED
|
@@ -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
|
|
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
|
+
});
|