@serifpersia/dango 3.4.4 → 3.4.5

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 (86) hide show
  1. package/README.md +1 -1
  2. package/client/dist/assets/{AnimeCard-4CEz8Buu.js → AnimeCard-I3S6qw9d.js} +1 -1
  3. package/client/dist/assets/{AnimeInfo-DyX9qTwg.js → AnimeInfo-CLU2NSBR.js} +1 -1
  4. package/client/dist/assets/{AnimeInfoPage-Dvp_c8vK.js → AnimeInfoPage-4Mw9ArDy.js} +2 -2
  5. package/client/dist/assets/{Asmr-qg3lMAJn.js → Asmr-DN9XBc5Y.js} +1 -1
  6. package/client/dist/assets/{AsmrResetProgressModal-BpPlg_Vv.js → AsmrResetProgressModal-DyU1Z2Gw.js} +1 -1
  7. package/client/dist/assets/{AvSyncCalibrator-FJllOgOH.js → AvSyncCalibrator-DxL0mr_4.js} +1 -1
  8. package/client/dist/assets/{Home-A7gCeEST.js → Home-CeTeJ-gM.js} +1 -1
  9. package/client/dist/assets/{Insights-G9IXepHJ.js → Insights-i34-qV4C.js} +1 -1
  10. package/client/dist/assets/{LibraryListPage-Bh2qCJPh.js → LibraryListPage-DE-ASOX2.js} +1 -1
  11. package/client/dist/assets/{ListeningList-Czv4cTRl.js → ListeningList-DrNce5ll.js} +1 -1
  12. package/client/dist/assets/{Manga-qdAAS1z7.js → Manga-DOrf7Fk6.js} +1 -1
  13. package/client/dist/assets/{MangaInfoPage-1rTgZzf2.js → MangaInfoPage-CAZMLMGw.js} +1 -1
  14. package/client/dist/assets/{MangaReadPage-CeyxPG6w.js → MangaReadPage-DQVEo8_F.js} +1 -1
  15. package/client/dist/assets/{MangaResetProgressModal-C_ppotmh.js → MangaResetProgressModal-DOjpLAMp.js} +1 -1
  16. package/client/dist/assets/{Mature-Bkhxnr3K.js → Mature-D2xb9EGB.js} +1 -1
  17. package/client/dist/assets/{MediaCard-BQZsutEo.js → MediaCard-CKUEcFTd.js} +1 -1
  18. package/client/dist/assets/{Music-C0rGzSbi.js → Music-w_a34r7r.js} +1 -1
  19. package/client/dist/assets/{OptionListMenu-CyfpCBEy.js → OptionListMenu-DxZHBMIZ.js} +1 -1
  20. package/client/dist/assets/{Player-yhiTlpYU.js → Player-B8YHeATv.js} +2 -2
  21. package/client/dist/assets/{PlayerSettings-icy4SfQa.js → PlayerSettings-Ddlg-MvW.js} +1 -1
  22. package/client/dist/assets/{QueueOptionsButton-5eloKF5J.js → QueueOptionsButton-BMvAC6Ut.js} +1 -1
  23. package/client/dist/assets/{QueueRail-DaMjlKL4.js → QueueRail-CAoGJvHP.js} +1 -1
  24. package/client/dist/assets/{Radio-FxGqCxR4.js → Radio-6O7rAUv1.js} +1 -1
  25. package/client/dist/assets/{ReadingList-CCm3IAbt.js → ReadingList-k3g8oHvs.js} +1 -1
  26. package/client/dist/assets/{ResetProgressModal-CeI4V3Y5.js → ResetProgressModal-vFC6N-Qw.js} +1 -1
  27. package/client/dist/assets/{Search-qpILaAiQ.js → Search-Bh9Y6nIJ.js} +1 -1
  28. package/client/dist/assets/{Settings-Bw9M3NGz.js → Settings-DRumujfh.js} +1 -1
  29. package/client/dist/assets/{SkeletonGrid-0dY_xMq3.js → SkeletonGrid-BtzUnQE0.js} +1 -1
  30. package/client/dist/assets/{Trackers-Zy5GOs0P.js → Trackers-D0nqy8wu.js} +1 -1
  31. package/client/dist/assets/{Tv-DREwc7BX.js → Tv-DVebLMNS.js} +1 -1
  32. package/client/dist/assets/{TvInfo-CHIsVzZ0.js → TvInfo-BkECbOKN.js} +1 -1
  33. package/client/dist/assets/{TvPopup-D_tVTyZ2.js → TvPopup-D8WWY6o4.js} +1 -1
  34. package/client/dist/assets/{TvSearch-BVxedBW-.js → TvSearch-kWPep3gL.js} +1 -1
  35. package/client/dist/assets/{TvWatchlist-BEIwq9R9.js → TvWatchlist-dD4emIHx.js} +1 -1
  36. package/client/dist/assets/{Watchlist-CSngjqlz.js → Watchlist-cWxw38nU.js} +1 -1
  37. package/client/dist/assets/{hls-D48GVTkk.js → hls-uG_Y3i5s.js} +1 -1
  38. package/client/dist/assets/{index-CVB5dYhT.js → index-BWIWE3Qy.js} +2 -2
  39. package/client/dist/assets/{useAnimeInfoData-DxdOg_B7.js → useAnimeInfoData-DjPBg24u.js} +1 -1
  40. package/client/dist/assets/{useAsmr-ClckxnPe.js → useAsmr-DQyobNQX.js} +1 -1
  41. package/client/dist/assets/{useAsmrPopup-Bbgxefy_.js → useAsmrPopup-CDoroMBD.js} +1 -1
  42. package/client/dist/assets/{useMangaLibrary-B3_BUKVk.js → useMangaLibrary-C6L8mrnS.js} +1 -1
  43. package/client/dist/assets/{useMangaPopup-Celk4lzf.js → useMangaPopup-BSuMa_vA.js} +1 -1
  44. package/client/dist/assets/{useProviders-pwxPZPHh.js → useProviders-XIdvDof6.js} +1 -1
  45. package/client/dist/assets/{useTv-D9sQrDop.js → useTv-DzSYasru.js} +1 -1
  46. package/client/dist/assets/{useTvLibrary-UsQpgBTB.js → useTvLibrary-BnbHS-gg.js} +1 -1
  47. package/client/dist/index.html +1 -1
  48. package/package.json +4 -2
  49. package/server/dist/app-hono.js +2 -0
  50. package/server/dist/hono/asmr.js +10 -1
  51. package/server/dist/hono/music.js +122 -332
  52. package/server/dist/hono/proxy.js +3 -24
  53. package/server/dist/hono/tv.js +3 -56
  54. package/server/dist/lib/mal.js +4 -3
  55. package/server/dist/lib/potoken.js +135 -0
  56. package/server/dist/lib/tracker/manga-sync.service.js +3 -22
  57. package/server/dist/lib/tracker/sync-state.js +21 -0
  58. package/server/dist/lib/tracker/sync.service.js +3 -22
  59. package/server/dist/lib/ump.js +106 -0
  60. package/server/dist/providers/remote-loader.js +2 -1
  61. package/server/dist/repositories/asmr.repository.js +4 -53
  62. package/server/dist/repositories/library.repository.js +46 -0
  63. package/server/dist/repositories/manga.repository.js +3 -52
  64. package/server/dist/repositories/settings.repository.js +8 -0
  65. package/server/dist/repositories/tv.repository.js +3 -46
  66. package/server/dist/server.js +5 -1
  67. package/server/dist/sync.js +3 -5
  68. package/server/dist/trpc/routers/asmr-library.js +2 -12
  69. package/server/dist/trpc/routers/asmr-progress.js +10 -22
  70. package/server/dist/trpc/routers/data.js +17 -36
  71. package/server/dist/trpc/routers/discord.js +1 -8
  72. package/server/dist/trpc/routers/insights.js +15 -16
  73. package/server/dist/trpc/routers/manga-library.js +5 -22
  74. package/server/dist/trpc/routers/manga-progress.js +8 -20
  75. package/server/dist/trpc/routers/music.js +6 -11
  76. package/server/dist/trpc/routers/queue.js +1 -1
  77. package/server/dist/trpc/routers/tracker.js +1 -7
  78. package/server/dist/trpc/routers/tv-library.js +2 -12
  79. package/server/dist/trpc/routers/tv-progress.js +16 -28
  80. package/server/dist/trpc/routers/tv.js +5 -21
  81. package/server/dist/trpc/routers/watchlist.js +1 -8
  82. package/server/dist/trpc/validation.js +35 -0
  83. package/server/dist/utils/db-utils.js +8 -0
  84. package/server/dist/utils/http.utils.js +12 -0
  85. package/server/dist/utils/machine-id.js +1 -2
  86. package/server/package.json +4 -1
@@ -12,6 +12,7 @@ import logger from './logger.js';
12
12
  import { notifyServerExit } from './lib/ipc.js';
13
13
  import { CONFIG } from './config.js';
14
14
  import { initializeDatabase, initializeMangaDatabase, initializeTvDatabase, initializeAsmrDatabase, runFullSyncSequence, syncUp, waitForSync, KIND_SYNC, } from './sync.js';
15
+ import { isJasmrApi } from './hono/asmr.js';
15
16
  import { loadRemoteProviders } from './providers/remote-loader.js';
16
17
  import { discordRPCService } from './discord-rpc.js';
17
18
  import { discordGatewayService } from './discord-gateway.js';
@@ -38,7 +39,10 @@ const app = createHonoApp(() => ({ shuttingDown: isShuttingDown, dbReady: !!db }
38
39
  getProviders: () => providers,
39
40
  getMangaProvider: (name) => mangaProviders[name],
40
41
  getTvProvider: (name) => tvProviders[name],
41
- getJasmr: () => providers['jasmr'],
42
+ getJasmr: () => {
43
+ const mod = providers['jasmr'];
44
+ return isJasmrApi(mod) ? mod : undefined;
45
+ },
42
46
  });
43
47
  const apiCache = new AppCache({ ttlSeconds: 3600, maxKeys: 5000 });
44
48
  const providers = {};
@@ -151,13 +151,11 @@ class Mutex {
151
151
  });
152
152
  }
153
153
  unlock() {
154
- if (this._waiting.length > 0) {
155
- const resolve = this._waiting.shift();
154
+ const resolve = this._waiting.shift();
155
+ if (resolve)
156
156
  resolve();
157
- }
158
- else {
157
+ else
159
158
  this._locked = false;
160
- }
161
159
  }
162
160
  }
163
161
  const syncMutex = new Mutex();
@@ -2,23 +2,13 @@ import { TRPCError } from '@trpc/server';
2
2
  import { protectedProcedure, router } from '../index.js';
3
3
  import { performAsmrWriteTransactionAsync } from '../../sync.js';
4
4
  import { buildAsmrId, ASMR_STATUSES, AsmrLibraryRepository, AsmrProgressRepository, } from '../../repositories/asmr.repository.js';
5
- import { defineSchema, reqObj, reqStr, optStr, reqStrArray } from '../validation.js';
5
+ import { defineSchema, reqObj, reqStr, optStr, reqStrArray, readPageLimit } from '../validation.js';
6
6
  const asmrLibraryListInput = () => defineSchema((value) => {
7
7
  const obj = reqObj(value);
8
- const out = {};
8
+ const out = { ...readPageLimit(obj) };
9
9
  const status = optStr(obj, 'status');
10
10
  if (status !== undefined)
11
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
12
  return out;
23
13
  });
24
14
  const asmrLibraryIdInput = () => defineSchema((value) => ({
@@ -1,17 +1,10 @@
1
1
  import { protectedProcedure, router } from '../index.js';
2
2
  import { performAsmrWriteTransactionAsync } from '../../sync.js';
3
- import { dbAll } from '../../utils/db-utils.js';
3
+ import { adultNonListIds } from '../../utils/db-utils.js';
4
4
  import { SettingsRepository } from '../../repositories/settings.repository.js';
5
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
- }
6
+ import { defineSchema, reqObj, reqStr, reqStrArray, strOrNull } from '../validation.js';
7
+ const getAdultNonListWorkIds = (db) => adultNonListIds(db, 'asmr_progress', 'asmr_library', 'workId', 'COALESCE(l.isAdult, p.isAdult) = 1');
15
8
  const asmrProgressIdsInput = () => defineSchema((value) => ({
16
9
  ids: reqStrArray(reqObj(value), 'ids', true),
17
10
  }));
@@ -26,9 +19,9 @@ const asmrProgressSaveInput = () => defineSchema((value) => {
26
19
  trackLabel: obj.trackLabel,
27
20
  currentTime: obj.currentTime,
28
21
  duration: obj.duration,
29
- title: obj.title ?? null,
30
- thumbnail: obj.thumbnail ?? null,
31
- rjCode: obj.rjCode ?? null,
22
+ title: strOrNull(obj, 'title'),
23
+ thumbnail: strOrNull(obj, 'thumbnail'),
24
+ rjCode: strOrNull(obj, 'rjCode'),
32
25
  isAdult: obj.isAdult ?? null,
33
26
  };
34
27
  });
@@ -66,9 +59,9 @@ export const asmrProgressRouter = router({
66
59
  trackLabel: String(input.trackLabel || ''),
67
60
  currentTime: timeNum,
68
61
  duration: durationNum,
69
- title: (input.title ?? null),
70
- thumbnail: (input.thumbnail ?? null),
71
- rjCode: (input.rjCode ?? null),
62
+ title: input.title ?? null,
63
+ thumbnail: input.thumbnail ?? null,
64
+ rjCode: input.rjCode ?? null,
72
65
  isAdult: input.isAdult != null ? Number(input.isAdult) : null,
73
66
  });
74
67
  await AsmrLibraryRepository.touchProgress(tx, String(input.workId), {
@@ -101,12 +94,7 @@ export const asmrProgressRouter = router({
101
94
  .input(asmrContinueListeningInput())
102
95
  .query(async ({ ctx, input }) => {
103
96
  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;
97
+ const { ignoreAdult, listOnly } = await SettingsRepository.readContinueFlags(ctx.db, 'asmrIgnoreAdultContent', 'asmrCwWatchlistOnly');
110
98
  const rows = (await AsmrProgressRepository.getContinueListening(ctx.asmrDb, limit)).filter((row) => {
111
99
  if (ignoreAdult && row.isAdult === 1)
112
100
  return false;
@@ -1,6 +1,6 @@
1
1
  import { TRPCError } from '@trpc/server';
2
2
  import { protectedProcedure, router } from '../index.js';
3
- import { defineSchema, optStr, reqObj, reqStr } from '../validation.js';
3
+ import { badRequest, defineSchema, failed, optNum, optStr, readCount, reqObj, reqStr, } from '../validation.js';
4
4
  import { parseJsonBody } from '../../utils/http.utils.js';
5
5
  import { getTrending, getLatestReleases, getSeasonal, getSchedule, searchAnilist, searchAnilistByTitle, getSpotlightBanners, getBatchedHomeData, getGenreTagLists, anilistUnavailable, wasAnilistDownAtBoot, checkAnilistStatus, getShowMetaById, getAnilistEpisodes, parseMalId, getShowMetaByMalId, fromAnilistMedia, } from '../../lib/anilist.js';
6
6
  import { malAnimeDetail, malSearchMedia, malSearchTitle } from '../../lib/mal.js';
@@ -14,25 +14,6 @@ import { ShowsMetaRepository } from '../../repositories/shows-meta.repository.js
14
14
  import { WatchlistRepository } from '../../repositories/watchlist.repository.js';
15
15
  import { dbRun } from '../../utils/db-utils.js';
16
16
  import logger from '../../logger.js';
17
- function failed(message) {
18
- return new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message });
19
- }
20
- function badRequest(message) {
21
- return new TRPCError({ code: 'BAD_REQUEST', message });
22
- }
23
- function optInt(obj, name, fallback) {
24
- const value = obj[name];
25
- if (value === undefined || value === null || value === '')
26
- return fallback;
27
- return parseInt(String(value), 10) || fallback;
28
- }
29
- function optIntOrUndefined(obj, name) {
30
- const value = obj[name];
31
- if (value === undefined || value === null || value === '')
32
- return undefined;
33
- const parsed = parseInt(String(value), 10);
34
- return Number.isFinite(parsed) ? parsed : undefined;
35
- }
36
17
  async function cached(ctx, key, ttl, validate, produce) {
37
18
  const hit = ctx.apiCache.get(key);
38
19
  if (hit)
@@ -62,9 +43,9 @@ const dataLatestReleasesInput = () => defineSchema((value) => {
62
43
  if (format !== undefined)
63
44
  out.format = format;
64
45
  if (obj.page !== undefined)
65
- out.page = optInt(obj, 'page', 1);
46
+ out.page = readCount(obj, 'page', 1);
66
47
  if (obj.size !== undefined)
67
- out.size = optInt(obj, 'size', 12);
48
+ out.size = readCount(obj, 'size', 12);
68
49
  return out;
69
50
  });
70
51
  const dataSearchInput = () => defineSchema((value) => {
@@ -74,9 +55,9 @@ const dataSearchInput = () => defineSchema((value) => {
74
55
  if (query !== undefined)
75
56
  out.query = query;
76
57
  if (obj.page !== undefined)
77
- out.page = optInt(obj, 'page', 1);
58
+ out.page = readCount(obj, 'page', 1);
78
59
  if (obj.limit !== undefined)
79
- out.limit = optInt(obj, 'limit', 14);
60
+ out.limit = readCount(obj, 'limit', 14);
80
61
  const sortBy = optStr(obj, 'sortBy');
81
62
  if (sortBy !== undefined)
82
63
  out.sortBy = sortBy;
@@ -89,7 +70,7 @@ const dataSearchInput = () => defineSchema((value) => {
89
70
  const season = optStr(obj, 'season');
90
71
  if (season !== undefined)
91
72
  out.season = season;
92
- const year = optIntOrUndefined(obj, 'year');
73
+ const year = optNum(obj, 'year');
93
74
  if (year !== undefined)
94
75
  out.year = year;
95
76
  const country = optStr(obj, 'country');
@@ -104,10 +85,10 @@ const dataSearchInput = () => defineSchema((value) => {
104
85
  const excludeTags = optStr(obj, 'excludeTags');
105
86
  if (excludeTags !== undefined)
106
87
  out.excludeTags = excludeTags;
107
- const minScore = optIntOrUndefined(obj, 'minScore');
88
+ const minScore = optNum(obj, 'minScore');
108
89
  if (minScore !== undefined)
109
90
  out.minScore = minScore;
110
- const minEpisodes = optIntOrUndefined(obj, 'minEpisodes');
91
+ const minEpisodes = optNum(obj, 'minEpisodes');
111
92
  if (minEpisodes !== undefined)
112
93
  out.minEpisodes = minEpisodes;
113
94
  const adult = optStr(obj, 'adult');
@@ -138,9 +119,9 @@ const dataSeasonalInput = () => defineSchema((value) => {
138
119
  const obj = reqObj(value);
139
120
  const out = {};
140
121
  if (obj.page !== undefined)
141
- out.page = optInt(obj, 'page', 1);
122
+ out.page = readCount(obj, 'page', 1);
142
123
  if (obj.size !== undefined)
143
- out.size = optInt(obj, 'size', 14);
124
+ out.size = readCount(obj, 'size', 14);
144
125
  const format = optStr(obj, 'format');
145
126
  if (format !== undefined)
146
127
  out.format = format;
@@ -153,9 +134,9 @@ const dataPopularListInput = () => defineSchema((value) => {
153
134
  if (sort !== undefined)
154
135
  out.sort = sort;
155
136
  if (obj.page !== undefined)
156
- out.page = optInt(obj, 'page', 1);
137
+ out.page = readCount(obj, 'page', 1);
157
138
  if (obj.size !== undefined)
158
- out.size = optInt(obj, 'size', 20);
139
+ out.size = readCount(obj, 'size', 20);
159
140
  return out;
160
141
  });
161
142
  const dataHomeInput = () => defineSchema((value) => {
@@ -229,16 +210,16 @@ const dataMatureSearchInput = () => defineSchema((value) => {
229
210
  out[k] = v;
230
211
  }
231
212
  if (obj.page !== undefined)
232
- out.page = optInt(obj, 'page', 1);
213
+ out.page = readCount(obj, 'page', 1);
233
214
  if (obj.limit !== undefined)
234
- out.limit = optInt(obj, 'limit', 14);
235
- const year = optIntOrUndefined(obj, 'year');
215
+ out.limit = readCount(obj, 'limit', 14);
216
+ const year = optNum(obj, 'year');
236
217
  if (year !== undefined)
237
218
  out.year = year;
238
- const minScore = optIntOrUndefined(obj, 'minScore');
219
+ const minScore = optNum(obj, 'minScore');
239
220
  if (minScore !== undefined)
240
221
  out.minScore = minScore;
241
- const minEpisodes = optIntOrUndefined(obj, 'minEpisodes');
222
+ const minEpisodes = optNum(obj, 'minEpisodes');
242
223
  if (minEpisodes !== undefined)
243
224
  out.minEpisodes = minEpisodes;
244
225
  return out;
@@ -1,18 +1,11 @@
1
- import { TRPCError } from '@trpc/server';
2
1
  import { protectedProcedure, router } from '../index.js';
3
- import { defineSchema, optStr, reqObj } from '../validation.js';
2
+ import { badRequest, defineSchema, failed, optStr, reqObj } from '../validation.js';
4
3
  import { discordRPCService } from '../../discord-rpc.js';
5
4
  import { discordGatewayService } from '../../discord-gateway.js';
6
5
  import { getDlsitePoster, getMangadexCover } from '../../hono/watchlist.js';
7
6
  import { updateEnvFile } from '../../utils/env.utils.js';
8
7
  import { CONFIG } from '../../config.js';
9
8
  import logger from '../../logger.js';
10
- function failed(message) {
11
- return new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message });
12
- }
13
- function badRequest(message) {
14
- return new TRPCError({ code: 'BAD_REQUEST', message });
15
- }
16
9
  function maskToken(t) {
17
10
  if (!t)
18
11
  return 'none';
@@ -1,12 +1,21 @@
1
- import { TRPCError } from '@trpc/server';
2
1
  import { protectedProcedure, router } from '../index.js';
3
2
  import { getWatchInsights, getGenreCards } from '../../lib/insights.js';
4
3
  import { computeDiscordSyncStats, getCachedRecommendations, getLinkedDiscordUser, setLinkedDiscordUser, syncDiscordRoles, } from '../../lib/discord-roles-sync.service.js';
5
- import { defineSchema, reqObj } from '../validation.js';
4
+ import { defineSchema, optStr, reqObj, reqStr } from '../validation.js';
6
5
  const discordUserInput = () => defineSchema((value) => {
7
6
  if (value === undefined || value === null)
8
7
  return {};
9
- return { user: reqObj(value).user };
8
+ const raw = reqObj(value).user;
9
+ if (raw === undefined || raw === null)
10
+ return { user: null };
11
+ const obj = reqObj(raw);
12
+ return {
13
+ user: {
14
+ id: reqStr(obj, 'id'),
15
+ username: optStr(obj, 'username') ?? '',
16
+ avatar: optStr(obj, 'avatar') ?? null,
17
+ },
18
+ };
10
19
  });
11
20
  export const insightsRouter = router({
12
21
  summary: protectedProcedure.query(async ({ ctx }) => {
@@ -25,19 +34,9 @@ export const insightsRouter = router({
25
34
  return { user: await getLinkedDiscordUser(ctx.db) };
26
35
  }),
27
36
  setDiscordUser: protectedProcedure.input(discordUserInput()).mutation(async ({ ctx, input }) => {
28
- const { user } = input;
29
- if (user !== null &&
30
- user !== undefined &&
31
- (typeof user !== 'object' ||
32
- typeof user.id !== 'string' ||
33
- !user.id)) {
34
- throw new TRPCError({
35
- code: 'BAD_REQUEST',
36
- message: 'Invalid user payload: expected { id, ... } or null',
37
- });
38
- }
39
- await setLinkedDiscordUser(ctx.db, user || null);
40
- return { success: true, user: user || null };
37
+ const user = input.user ?? null;
38
+ await setLinkedDiscordUser(ctx.db, user);
39
+ return { success: true, user };
41
40
  }),
42
41
  discordSyncNow: protectedProcedure.mutation(async ({ ctx }) => {
43
42
  return syncDiscordRoles(ctx.db, true);
@@ -2,7 +2,7 @@ import { TRPCError } from '@trpc/server';
2
2
  import { protectedProcedure, router } from '../index.js';
3
3
  import { performMangaWriteTransactionAsync } from '../../sync.js';
4
4
  import { buildMangaId, MANGA_STATUSES, MangaLibraryRepository, MangaProgressRepository, } from '../../repositories/manga.repository.js';
5
- import { defineSchema, optStr, reqObj } from '../validation.js';
5
+ import { badRequest, defineSchema, failed, optStr, readCount, reqObj, reqStr, } from '../validation.js';
6
6
  function mangaDb(ctx) {
7
7
  const db = ctx.mangaDb;
8
8
  if (!db || db.isClosedCheck())
@@ -14,19 +14,6 @@ function normalizeId(provider, mangaId, idRaw) {
14
14
  return idRaw;
15
15
  return buildMangaId(provider, mangaId || idRaw || '');
16
16
  }
17
- function badRequest(message) {
18
- return new TRPCError({ code: 'BAD_REQUEST', message });
19
- }
20
- function failed(message) {
21
- return new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message });
22
- }
23
- function readCount(obj, name, fallback) {
24
- const raw = obj[name];
25
- if (raw === undefined || raw === null || raw === '')
26
- return fallback;
27
- const n = Math.floor(Number(raw));
28
- return Number.isFinite(n) ? n : fallback;
29
- }
30
17
  const mangaLibraryListInput = () => defineSchema((value) => {
31
18
  const obj = reqObj(value);
32
19
  const status = optStr(obj, 'status');
@@ -34,13 +21,9 @@ const mangaLibraryListInput = () => defineSchema((value) => {
34
21
  const limit = Math.min(Math.max(readCount(obj, 'limit', 24), 1), 100);
35
22
  return { status, page, limit };
36
23
  });
37
- const mangaLibraryIdInput = () => defineSchema((value) => {
38
- const obj = reqObj(value);
39
- const id = obj.id;
40
- if (typeof id !== 'string')
41
- throw new Error('id must be a string');
42
- return { id };
43
- });
24
+ const mangaLibraryIdInput = () => defineSchema((value) => ({
25
+ id: reqStr(reqObj(value), 'id'),
26
+ }));
44
27
  const mangaLibraryAddInput = () => defineSchema((value) => {
45
28
  const obj = reqObj(value);
46
29
  const out = {
@@ -179,7 +162,7 @@ export const mangaLibraryRouter = router({
179
162
  setStatus: protectedProcedure
180
163
  .input(mangaLibraryStatusInput())
181
164
  .mutation(async ({ ctx, input }) => {
182
- if (!input.id || !MANGA_STATUSES.includes(input.status)) {
165
+ if (!input.id || !input.status || !MANGA_STATUSES.includes(input.status)) {
183
166
  throw badRequest('id and a valid status are required');
184
167
  }
185
168
  try {
@@ -1,19 +1,12 @@
1
1
  import { protectedProcedure, router } from '../index.js';
2
2
  import { defineSchema, optStr, reqObj, reqStr, reqStrArray } from '../validation.js';
3
3
  import { performMangaWriteTransactionAsync } from '../../sync.js';
4
- import { dbAll } from '../../utils/db-utils.js';
4
+ import { adultNonListIds } from '../../utils/db-utils.js';
5
5
  import { SettingsRepository } from '../../repositories/settings.repository.js';
6
6
  import { MangaLibraryRepository, MangaProgressRepository, } from '../../repositories/manga.repository.js';
7
7
  const MANGA_ADULT_RATINGS = ['erotica', 'pornographic'];
8
8
  const isMangaContinueAdult = (row) => !!row.contentRating && MANGA_ADULT_RATINGS.includes(row.contentRating);
9
- async function getAdultNonListMangaIds(db) {
10
- const rows = await dbAll(db, `SELECT DISTINCT p.mangaId as mangaId
11
- FROM manga_progress p
12
- LEFT JOIN manga_library l ON l.id = p.mangaId
13
- WHERE COALESCE(l.contentRating, p.contentRating) IN ('erotica', 'pornographic')
14
- AND l.id IS NULL`);
15
- return rows.map((r) => r.mangaId);
16
- }
9
+ const getAdultNonListMangaIds = (db) => adultNonListIds(db, 'manga_progress', 'manga_library', 'mangaId', "COALESCE(l.contentRating, p.contentRating) IN ('erotica', 'pornographic')");
17
10
  const mangaIdInput = () => defineSchema((value) => ({
18
11
  mangaId: reqStr(reqObj(value), 'mangaId'),
19
12
  }));
@@ -102,11 +95,11 @@ export const mangaProgressRouter = router({
102
95
  chapterNumber: String(input.chapterNumber || ''),
103
96
  page: pageNum,
104
97
  pageCount: pageCountNum,
105
- title: (input.title ?? null),
106
- cover: (input.cover ?? null),
107
- provider: (input.provider ?? null),
108
- altTitle: (input.altTitle ?? null),
109
- contentRating: (input.contentRating ?? null),
98
+ title: input.title ?? null,
99
+ cover: input.cover ?? null,
100
+ provider: input.provider ?? null,
101
+ altTitle: input.altTitle ?? null,
102
+ contentRating: input.contentRating ?? null,
110
103
  });
111
104
  await MangaLibraryRepository.touchProgress(tx, String(input.mangaId), {
112
105
  chapterId: String(input.chapterId),
@@ -141,12 +134,7 @@ export const mangaProgressRouter = router({
141
134
  .query(async ({ ctx, input }) => {
142
135
  try {
143
136
  const limit = Math.min(Math.max(parseInt(String(input.limit)) || 24, 1), 100);
144
- const ignoreAdultRow = await SettingsRepository.getByKey(ctx.db, 'mangaIgnoreAdultContent');
145
- const ignoreAdult = ignoreAdultRow ? ignoreAdultRow.value !== 'false' : true;
146
- const listOnlyRow = await SettingsRepository.getByKey(ctx.db, 'mangaCwWatchlistOnly');
147
- const listOnly = listOnlyRow
148
- ? listOnlyRow.value === 'true' || listOnlyRow.value === '1'
149
- : false;
137
+ const { ignoreAdult, listOnly } = await SettingsRepository.readContinueFlags(ctx.db, 'mangaIgnoreAdultContent', 'mangaCwWatchlistOnly');
150
138
  const rows = (await MangaProgressRepository.getContinueReading(ctx.mangaDb, limit)).filter((row) => {
151
139
  if (ignoreAdult && isMangaContinueAdult(row))
152
140
  return false;
@@ -1,6 +1,6 @@
1
1
  import { TRPCError } from '@trpc/server';
2
2
  import { protectedProcedure, router } from '../index.js';
3
- import { defineSchema, optStr, reqObj } from '../validation.js';
3
+ import { badRequest, defineSchema, failed, optStr, reqObj } from '../validation.js';
4
4
  import logger from '../../logger.js';
5
5
  import { getInnertube, getAuthedInnertube, refreshAuthedInnertube, saveMusicCookie, getMusicAuthStatus, signOutMusic, isParserVariantError, } from '../../lib/ytmusic.js';
6
6
  const NAME_SUFFIXES = [
@@ -152,7 +152,8 @@ function upnextToTrack(node, trustLike = false) {
152
152
  }
153
153
  function parseUpnext(panel, trustLike) {
154
154
  const tracks = [];
155
- const contents = panel.contents ?? [];
155
+ const shaped = panel;
156
+ const contents = shaped.contents ?? [];
156
157
  for (const item of contents) {
157
158
  const track = upnextToTrack(item, trustLike);
158
159
  if (track)
@@ -160,7 +161,7 @@ function parseUpnext(panel, trustLike) {
160
161
  if (tracks.length >= 50)
161
162
  break;
162
163
  }
163
- const playlistId = panel.playlist_id ?? null;
164
+ const playlistId = shaped.playlist_id ?? null;
164
165
  return { tracks, playlistId };
165
166
  }
166
167
  function asMenuItems(menu) {
@@ -285,15 +286,9 @@ async function readLibrary(yt) {
285
286
  logger.info(`[music] library parsed tracks=${tracks.length} playlists=${playlists.length} gridItems=${gridItems.length}`);
286
287
  return { tracks, playlists };
287
288
  }
288
- function badRequest(message) {
289
- return new TRPCError({ code: 'BAD_REQUEST', message });
290
- }
291
289
  function unauthorized(message) {
292
290
  return new TRPCError({ code: 'UNAUTHORIZED', message });
293
291
  }
294
- function failed(message) {
295
- return new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message });
296
- }
297
292
  const musicSearchInput = () => defineSchema((value) => {
298
293
  const obj = reqObj(value);
299
294
  const out = {};
@@ -598,8 +593,8 @@ export const musicRouter = router({
598
593
  throw unauthorized('MUSIC_AUTH_REQUIRED');
599
594
  }
600
595
  const attempt = async (session) => {
601
- const actions = session.actions ??
602
- session.session?.actions;
596
+ const shaped = session;
597
+ const actions = shaped.actions ?? shaped.session?.actions;
603
598
  if (!actions)
604
599
  throw new Error('Actions unavailable');
605
600
  const panel = await session.music.getUpNext(id);
@@ -68,7 +68,7 @@ export const queueRouter = router({
68
68
  }),
69
69
  removeMany: protectedProcedure.input(queueRemoveManyInput()).mutation(async ({ ctx, input }) => {
70
70
  const showId = await getMigratedId(ctx.db, input.showId);
71
- let removed;
71
+ let removed = [];
72
72
  await performWriteTransactionAsync(ctx.db, async (tx) => {
73
73
  removed = ((await QueueRepository.getByShow(tx, showId)) || []).map((ep) => ep.episodeNumber);
74
74
  const toRemove = Array.isArray(input.episodeNumbers) && input.episodeNumbers.length
@@ -5,15 +5,9 @@ import { performWriteTransactionAsync } from '../../sync.js';
5
5
  import { AniListTracker } from '../../lib/tracker/anilist-tracker.js';
6
6
  import { syncAniList, importFromMalUsername, importFromUsername, } from '../../lib/tracker/sync.service.js';
7
7
  import { syncAniListManga, importFromUsernameManga, importMangaFromMalUsername, } from '../../lib/tracker/manga-sync.service.js';
8
- import { defineSchema, reqObj } from '../validation.js';
8
+ import { badRequest, defineSchema, failed, reqObj } from '../validation.js';
9
9
  const TRACKER_TOKEN_KEY = 'tracker_anilist_token';
10
10
  const TRACKER_USER_KEY = 'tracker_anilist_user';
11
- function badRequest(message) {
12
- return new TRPCError({ code: 'BAD_REQUEST', message });
13
- }
14
- function failed(message) {
15
- return new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message });
16
- }
17
11
  function unavailable(message) {
18
12
  return new TRPCError({ code: 'SERVICE_UNAVAILABLE', message });
19
13
  }
@@ -2,26 +2,16 @@ import { TRPCError } from '@trpc/server';
2
2
  import { protectedProcedure, router } from '../index.js';
3
3
  import { performTvWriteTransactionAsync } from '../../sync.js';
4
4
  import { buildTvId, TV_STATUSES, TvLibraryRepository, TvProgressRepository, } from '../../repositories/tv.repository.js';
5
- import { defineSchema, reqObj, reqStr, optStr, reqStrArray } from '../validation.js';
5
+ import { defineSchema, reqObj, reqStr, optStr, reqStrArray, readPageLimit } from '../validation.js';
6
6
  function normalizeMediaType(raw) {
7
7
  return String(raw).toLowerCase() === 'movie' ? 'movie' : 'tv';
8
8
  }
9
9
  const tvLibraryListInput = () => defineSchema((value) => {
10
10
  const obj = reqObj(value);
11
- const out = {};
11
+ const out = { ...readPageLimit(obj) };
12
12
  const status = optStr(obj, 'status');
13
13
  if (status !== undefined)
14
14
  out.status = status;
15
- if (obj.page !== undefined && obj.page !== null && obj.page !== '') {
16
- const page = Number(obj.page);
17
- if (Number.isFinite(page))
18
- out.page = page;
19
- }
20
- if (obj.limit !== undefined && obj.limit !== null && obj.limit !== '') {
21
- const limit = Number(obj.limit);
22
- if (Number.isFinite(limit))
23
- out.limit = limit;
24
- }
25
15
  return out;
26
16
  });
27
17
  const tvLibraryIdInput = () => defineSchema((value) => ({
@@ -1,19 +1,12 @@
1
1
  import { TRPCError } from '@trpc/server';
2
2
  import { protectedProcedure, router } from '../index.js';
3
3
  import { performTvWriteTransactionAsync } from '../../sync.js';
4
- import { dbAll } from '../../utils/db-utils.js';
4
+ import { adultNonListIds } from '../../utils/db-utils.js';
5
5
  import { SettingsRepository } from '../../repositories/settings.repository.js';
6
6
  import { normalizeTvMediaId, TvLibraryRepository, TvProgressRepository, } from '../../repositories/tv.repository.js';
7
- import { defineSchema, reqObj, reqStr } from '../validation.js';
7
+ import { defineSchema, reqObj, reqStr, strOrNull } from '../validation.js';
8
8
  const isTvContinueAdult = (row) => row.adult === true || row.adult === 1;
9
- async function getAdultNonListMediaIds(db) {
10
- const rows = await dbAll(db, `SELECT DISTINCT p.mediaId as mediaId
11
- FROM tv_progress p
12
- LEFT JOIN tv_library l ON l.id = p.mediaId
13
- WHERE COALESCE(l.adult, p.adult) = 1
14
- AND l.id IS NULL`);
15
- return rows.map((r) => r.mediaId);
16
- }
9
+ const getAdultNonListMediaIds = (db) => adultNonListIds(db, 'tv_progress', 'tv_library', 'mediaId', 'COALESCE(l.adult, p.adult) = 1');
17
10
  const tvProgressMediaInput = () => defineSchema((value) => ({
18
11
  mediaId: reqStr(reqObj(value), 'mediaId'),
19
12
  }));
@@ -30,13 +23,13 @@ const tvProgressSaveInput = () => defineSchema((value) => {
30
23
  currentTime: obj.currentTime,
31
24
  duration: obj.duration,
32
25
  completed: obj.completed,
33
- title: obj.title ?? null,
34
- poster: obj.poster ?? null,
35
- backdrop: obj.backdrop ?? null,
36
- year: obj.year ?? null,
37
- overview: obj.overview ?? null,
26
+ title: strOrNull(obj, 'title'),
27
+ poster: strOrNull(obj, 'poster'),
28
+ backdrop: strOrNull(obj, 'backdrop'),
29
+ year: strOrNull(obj, 'year'),
30
+ overview: strOrNull(obj, 'overview'),
38
31
  tmdbId: obj.tmdbId ?? null,
39
- mediaType: obj.mediaType ?? null,
32
+ mediaType: strOrNull(obj, 'mediaType'),
40
33
  adult: obj.adult ?? null,
41
34
  };
42
35
  });
@@ -95,13 +88,13 @@ export const tvProgressRouter = router({
95
88
  currentTime: storedTime,
96
89
  duration: durationNum,
97
90
  completed: completedNow,
98
- title: (input.title ?? null),
99
- poster: (input.poster ?? null),
100
- backdrop: (input.backdrop ?? null),
101
- year: (input.year ?? null),
102
- overview: (input.overview ?? null),
91
+ title: input.title ?? null,
92
+ poster: input.poster ?? null,
93
+ backdrop: input.backdrop ?? null,
94
+ year: input.year ?? null,
95
+ overview: input.overview ?? null,
103
96
  tmdbId: input.tmdbId != null ? Number(input.tmdbId) : null,
104
- mediaType: (input.mediaType ?? null),
97
+ mediaType: input.mediaType ?? null,
105
98
  adult: input.adult != null ? Number(input.adult) : null,
106
99
  });
107
100
  await TvLibraryRepository.touchProgress(tx, String(mediaId), {
@@ -139,12 +132,7 @@ export const tvProgressRouter = router({
139
132
  .input(tvContinueWatchingInput())
140
133
  .query(async ({ ctx, input }) => {
141
134
  const limit = Math.min(Math.max(parseInt(String(input.limit ?? 24)) || 24, 1), 100);
142
- const ignoreAdultRow = await SettingsRepository.getByKey(ctx.db, 'tvIgnoreAdultContent');
143
- const ignoreAdult = ignoreAdultRow ? ignoreAdultRow.value !== 'false' : true;
144
- const listOnlyRow = await SettingsRepository.getByKey(ctx.db, 'tvCwWatchlistOnly');
145
- const listOnly = listOnlyRow
146
- ? listOnlyRow.value === 'true' || listOnlyRow.value === '1'
147
- : false;
135
+ const { ignoreAdult, listOnly } = await SettingsRepository.readContinueFlags(ctx.db, 'tvIgnoreAdultContent', 'tvCwWatchlistOnly');
148
136
  const rows = (await TvProgressRepository.getContinueWatching(ctx.tvDb, limit)).filter((row) => {
149
137
  if (ignoreAdult && isTvContinueAdult(row))
150
138
  return false;