@serifpersia/dango 3.4.4 → 3.4.6

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 (96) hide show
  1. package/README.md +1 -1
  2. package/client/dist/assets/{AnimeCard-4CEz8Buu.js → AnimeCard-BVdH0EeW.js} +1 -1
  3. package/client/dist/assets/{AnimeInfo-DyX9qTwg.js → AnimeInfo-Dw2Nop6Q.js} +1 -1
  4. package/client/dist/assets/{AnimeInfoPage-Dvp_c8vK.js → AnimeInfoPage-Cw-Tq3YX.js} +2 -2
  5. package/client/dist/assets/{Asmr-qg3lMAJn.js → Asmr-BrjzzyOi.js} +1 -1
  6. package/client/dist/assets/{AsmrResetProgressModal-BpPlg_Vv.js → AsmrResetProgressModal-CYjpcYpb.js} +1 -1
  7. package/client/dist/assets/{AvSyncCalibrator-FJllOgOH.js → AvSyncCalibrator-BuHurVgo.js} +3 -3
  8. package/client/dist/assets/AvSyncCalibrator-D0FYSwiz.css +1 -0
  9. package/client/dist/assets/{Home-A7gCeEST.js → Home-KdnP7sNM.js} +1 -1
  10. package/client/dist/assets/{Insights-G9IXepHJ.js → Insights-DDWry-t1.js} +1 -1
  11. package/client/dist/assets/{LibraryListPage-Bh2qCJPh.js → LibraryListPage-CV8Z9JlO.js} +1 -1
  12. package/client/dist/assets/{ListeningList-Czv4cTRl.js → ListeningList-DSMbMZT7.js} +1 -1
  13. package/client/dist/assets/{Manga-qdAAS1z7.js → Manga-J6XoqEKq.js} +1 -1
  14. package/client/dist/assets/{MangaInfoPage-1rTgZzf2.js → MangaInfoPage-N7TxsN5u.js} +1 -1
  15. package/client/dist/assets/{MangaReadPage-CeyxPG6w.js → MangaReadPage-D_VzSNWq.js} +1 -1
  16. package/client/dist/assets/{MangaResetProgressModal-C_ppotmh.js → MangaResetProgressModal-DyhQYBDy.js} +1 -1
  17. package/client/dist/assets/{Mature-Bkhxnr3K.js → Mature-Db53R-LR.js} +1 -1
  18. package/client/dist/assets/{MediaCard-BQZsutEo.js → MediaCard-BcLjlpfR.js} +1 -1
  19. package/client/dist/assets/{Music-C0rGzSbi.js → Music-DjxQjJOl.js} +1 -1
  20. package/client/dist/assets/{OptionListMenu-CyfpCBEy.js → OptionListMenu-DngTgHxH.js} +1 -1
  21. package/client/dist/assets/Player-Be_7Ne5p.js +56 -0
  22. package/client/dist/assets/{PlayerSettings-icy4SfQa.js → PlayerSettings-D47ItGDR.js} +1 -1
  23. package/client/dist/assets/{QueueOptionsButton-5eloKF5J.js → QueueOptionsButton-ClVTpH8J.js} +1 -1
  24. package/client/dist/assets/{QueueRail-DaMjlKL4.js → QueueRail-DeJKVRs5.js} +1 -1
  25. package/client/dist/assets/{Radio-FxGqCxR4.js → Radio-DU0ma3GA.js} +1 -1
  26. package/client/dist/assets/{ReadingList-CCm3IAbt.js → ReadingList-qXW7Dk5z.js} +1 -1
  27. package/client/dist/assets/{ResetProgressModal-CeI4V3Y5.js → ResetProgressModal-C6ukct_c.js} +1 -1
  28. package/client/dist/assets/{Search-qpILaAiQ.js → Search-DjjKA_Wu.js} +1 -1
  29. package/client/dist/assets/{Settings-Bw9M3NGz.js → Settings-DQt_S5Qw.js} +1 -1
  30. package/client/dist/assets/{SkeletonGrid-0dY_xMq3.js → SkeletonGrid-nx2HEi4P.js} +1 -1
  31. package/client/dist/assets/{Trackers-Zy5GOs0P.js → Trackers-BcGzeqj2.js} +1 -1
  32. package/client/dist/assets/Tv-COvyHdeJ.js +4 -0
  33. package/client/dist/assets/{TvInfo-CHIsVzZ0.js → TvInfo-CUd1agFy.js} +1 -1
  34. package/client/dist/assets/{TvPopup-D_tVTyZ2.js → TvPopup-CJeu3tgc.js} +1 -1
  35. package/client/dist/assets/{TvSearch-BVxedBW-.js → TvSearch-BIypjbU3.js} +1 -1
  36. package/client/dist/assets/{TvWatchlist-BEIwq9R9.js → TvWatchlist-DZrBdiqZ.js} +1 -1
  37. package/client/dist/assets/{Watchlist-CSngjqlz.js → Watchlist-Dw8cPe0n.js} +1 -1
  38. package/client/dist/assets/{hls-D48GVTkk.js → hls-B3_1VuDP.js} +1 -1
  39. package/client/dist/assets/{index-CVB5dYhT.js → index-gMxawd-E.js} +2 -2
  40. package/client/dist/assets/{useAnimeInfoData-DxdOg_B7.js → useAnimeInfoData-_iOLEAa8.js} +1 -1
  41. package/client/dist/assets/{useAsmr-ClckxnPe.js → useAsmr-Blk4v41t.js} +1 -1
  42. package/client/dist/assets/{useAsmrPopup-Bbgxefy_.js → useAsmrPopup-D9SQIgZa.js} +1 -1
  43. package/client/dist/assets/{useMangaLibrary-B3_BUKVk.js → useMangaLibrary-B60at6ni.js} +1 -1
  44. package/client/dist/assets/{useMangaPopup-Celk4lzf.js → useMangaPopup-DEfGjWSu.js} +1 -1
  45. package/client/dist/assets/{useProviders-pwxPZPHh.js → useProviders-Ca0bZWt7.js} +1 -1
  46. package/client/dist/assets/{useTv-D9sQrDop.js → useTv-WJCDL5_5.js} +1 -1
  47. package/client/dist/assets/{useTvLibrary-UsQpgBTB.js → useTvLibrary-Fgn5i9rJ.js} +1 -1
  48. package/client/dist/index.html +1 -1
  49. package/package.json +4 -2
  50. package/server/dist/app-hono.js +2 -0
  51. package/server/dist/hono/asmr.js +10 -1
  52. package/server/dist/hono/music.js +122 -332
  53. package/server/dist/hono/proxy.js +105 -99
  54. package/server/dist/hono/tv.js +5 -58
  55. package/server/dist/hono/watchlist.js +2 -291
  56. package/server/dist/lib/anilist.js +0 -3
  57. package/server/dist/lib/mal.js +4 -41
  58. package/server/dist/lib/offline-db.js +0 -6
  59. package/server/dist/lib/potoken.js +135 -0
  60. package/server/dist/lib/tracker/manga-sync.service.js +3 -22
  61. package/server/dist/lib/tracker/sync-state.js +21 -0
  62. package/server/dist/lib/tracker/sync.service.js +3 -22
  63. package/server/dist/lib/ump.js +106 -0
  64. package/server/dist/providers/remote-loader.js +17 -27
  65. package/server/dist/providers/title-matching.js +5 -5
  66. package/server/dist/repositories/asmr.repository.js +4 -53
  67. package/server/dist/repositories/library.repository.js +46 -0
  68. package/server/dist/repositories/manga.repository.js +3 -53
  69. package/server/dist/repositories/settings.repository.js +8 -0
  70. package/server/dist/repositories/tv.repository.js +3 -46
  71. package/server/dist/repositories/watched-episodes.repository.js +0 -2
  72. package/server/dist/server.js +5 -1
  73. package/server/dist/sync.js +3 -5
  74. package/server/dist/trpc/routers/asmr-library.js +2 -12
  75. package/server/dist/trpc/routers/asmr-progress.js +10 -22
  76. package/server/dist/trpc/routers/data.js +17 -36
  77. package/server/dist/trpc/routers/discord.js +1 -8
  78. package/server/dist/trpc/routers/insights.js +15 -16
  79. package/server/dist/trpc/routers/manga-library.js +5 -22
  80. package/server/dist/trpc/routers/manga-progress.js +8 -20
  81. package/server/dist/trpc/routers/music.js +6 -11
  82. package/server/dist/trpc/routers/queue.js +1 -1
  83. package/server/dist/trpc/routers/tracker.js +1 -7
  84. package/server/dist/trpc/routers/tv-library.js +2 -12
  85. package/server/dist/trpc/routers/tv-progress.js +16 -28
  86. package/server/dist/trpc/routers/tv.js +5 -21
  87. package/server/dist/trpc/routers/watchlist.js +1 -8
  88. package/server/dist/trpc/validation.js +35 -0
  89. package/server/dist/utils/db-utils.js +8 -0
  90. package/server/dist/utils/http.utils.js +12 -0
  91. package/server/dist/utils/machine-id.js +1 -2
  92. package/server/dist/utils/megaplay.utils.js +1 -2
  93. package/server/package.json +4 -1
  94. package/client/dist/assets/AvSyncCalibrator-C2wu1ydI.css +0 -1
  95. package/client/dist/assets/Player-yhiTlpYU.js +0 -56
  96. package/client/dist/assets/Tv-DREwc7BX.js +0 -4
@@ -0,0 +1,106 @@
1
+ const MEDIA_HEADER = 20;
2
+ const MEDIA = 21;
3
+ const STREAM_PROTECTION_STATUS = 58;
4
+ function readVarint(buf, pos) {
5
+ if (pos >= buf.length)
6
+ return null;
7
+ const prefix = buf[pos];
8
+ let size = 0;
9
+ for (let shift = 1; shift <= 5; shift++) {
10
+ if ((prefix & (128 >> (shift - 1))) === 0) {
11
+ size = shift;
12
+ break;
13
+ }
14
+ }
15
+ if (size < 1 || pos + size > buf.length)
16
+ return null;
17
+ if (size === 1)
18
+ return [prefix, pos + 1];
19
+ if (size === 5) {
20
+ const value = buf[pos + 1] + buf[pos + 2] * 256 + buf[pos + 3] * 65536 + buf[pos + 4] * 16777216;
21
+ return [value >>> 0, pos + 5];
22
+ }
23
+ const lowBits = 8 - size;
24
+ let value = prefix & ((1 << lowBits) - 1);
25
+ for (let i = 1; i < size; i++) {
26
+ value += buf[pos + i] * 2 ** (lowBits + 8 * (i - 1));
27
+ }
28
+ return [value, pos + size];
29
+ }
30
+ function readStatusField(payload) {
31
+ let pos = 0;
32
+ while (pos < payload.length) {
33
+ const key = payload[pos++];
34
+ const field = key >> 3;
35
+ const wire = key & 7;
36
+ if (wire !== 0)
37
+ break;
38
+ const decoded = readVarint(payload, pos);
39
+ if (!decoded)
40
+ break;
41
+ if (field === 1)
42
+ return decoded[0];
43
+ pos = decoded[1];
44
+ }
45
+ return 1;
46
+ }
47
+ export function parseUmpResponse(buf, carry) {
48
+ const mediaChunks = [];
49
+ let status = 1;
50
+ let nextCarry = carry;
51
+ let pos = 0;
52
+ if (nextCarry) {
53
+ const t = readVarint(buf, pos);
54
+ const s = t && readVarint(buf, t[1]);
55
+ if (t && s && t[0] === MEDIA_HEADER) {
56
+ pos = s[1] + s[0];
57
+ }
58
+ const ct = readVarint(buf, pos);
59
+ const cs = ct && readVarint(buf, ct[1]);
60
+ if (ct && cs && ct[0] === nextCarry.pendingType) {
61
+ pos = cs[1];
62
+ const take = Math.min(cs[0], buf.length - pos);
63
+ nextCarry.chunks.push(buf.subarray(pos, pos + take));
64
+ nextCarry.remaining = cs[0] - take;
65
+ pos += take;
66
+ if (nextCarry.remaining <= 0) {
67
+ const full = Buffer.concat(nextCarry.chunks);
68
+ nextCarry = null;
69
+ if (ct[0] === MEDIA && full.length > 1)
70
+ mediaChunks.push(full.subarray(1));
71
+ }
72
+ }
73
+ else {
74
+ nextCarry = null;
75
+ }
76
+ }
77
+ while (pos < buf.length) {
78
+ const t = readVarint(buf, pos);
79
+ if (!t)
80
+ break;
81
+ const s = readVarint(buf, t[1]);
82
+ if (!s)
83
+ break;
84
+ const [type, size] = [t[0], s[0]];
85
+ pos = s[1];
86
+ const available = buf.length - pos;
87
+ if (size > available) {
88
+ nextCarry = {
89
+ pendingType: type,
90
+ remaining: size - available,
91
+ chunks: [buf.subarray(pos)],
92
+ };
93
+ break;
94
+ }
95
+ const payload = buf.subarray(pos, pos + size);
96
+ pos += size;
97
+ if (type === MEDIA) {
98
+ if (payload.length > 1)
99
+ mediaChunks.push(payload.subarray(1));
100
+ }
101
+ else if (type === STREAM_PROTECTION_STATUS) {
102
+ status = readStatusField(payload);
103
+ }
104
+ }
105
+ return { media: Buffer.concat(mediaChunks), status, carry: nextCarry };
106
+ }
@@ -9,6 +9,7 @@ import logger from '../logger.js';
9
9
  import { CONFIG } from '../config.js';
10
10
  import { requestContext } from '../utils/request-context.js';
11
11
  import { sanitizeCfClearance, buildCfClearanceCookie } from '../utils/cookie.utils.js';
12
+ import { base64UrlEncode } from '../utils/megaplay.utils.js';
12
13
  import { decodeMaybeGzip, parseJsonBody } from '../utils/http.utils.js';
13
14
  import { buildQueryVariants, pickBestMatch } from './title-matching.js';
14
15
  import { anilistRequest, parseMalId, searchAnilistByTitle } from '../lib/anilist.js';
@@ -71,13 +72,6 @@ function resolveEntryUrl(entry, registryUrl) {
71
72
  function sha256Hex(data) {
72
73
  return crypto.createHash('sha256').update(data).digest('hex');
73
74
  }
74
- function base64UrlEncode(input) {
75
- return Buffer.from(input)
76
- .toString('base64')
77
- .replace(/\+/g, '-')
78
- .replace(/\//g, '_')
79
- .replace(/=+$/, '');
80
- }
81
75
  function aes256CbcDecryptJson(b64url, keyUtf8, ivUtf8) {
82
76
  let encrypted;
83
77
  try {
@@ -238,7 +232,8 @@ function createCtx(cache) {
238
232
  ? { url: urlOrOptions, ...(maybeOptions ?? {}) }
239
233
  : urlOrOptions);
240
234
  const { url, ...rest } = opts;
241
- const res = await gotScraping(url, {
235
+ const res = await gotScraping({
236
+ url: url,
242
237
  method: 'GET',
243
238
  responseType: 'text',
244
239
  throwHttpErrors: false,
@@ -271,31 +266,26 @@ function createCtx(cache) {
271
266
  },
272
267
  };
273
268
  }
274
- function isValidVideoProvider(p) {
269
+ function shapeOf(p) {
275
270
  if (!p || typeof p !== 'object')
276
- return false;
271
+ return null;
277
272
  const o = p;
278
- return (typeof o.name === 'string' &&
279
- typeof o.search === 'function' &&
280
- typeof o.getEpisodes === 'function' &&
281
- typeof o.getStreamUrls === 'function');
273
+ return typeof o.name === 'string' ? o : null;
274
+ }
275
+ function hasFns(o, fns) {
276
+ return fns.every((f) => typeof o[f] === 'function');
277
+ }
278
+ function isValidVideoProvider(p) {
279
+ const o = shapeOf(p);
280
+ return !!o && hasFns(o, ['search', 'getEpisodes', 'getStreamUrls']);
282
281
  }
283
282
  function isValidMangaProvider(p) {
284
- if (!p || typeof p !== 'object')
285
- return false;
286
- const o = p;
287
- return (typeof o.name === 'string' &&
288
- typeof o.search === 'function' &&
289
- typeof o.getDetail === 'function' &&
290
- typeof o.getChapters === 'function' &&
291
- typeof o.getPages === 'function');
283
+ const o = shapeOf(p);
284
+ return !!o && hasFns(o, ['search', 'getDetail', 'getChapters', 'getPages']);
292
285
  }
293
286
  function isValidTvProvider(p) {
294
- if (!p || typeof p !== 'object')
295
- return false;
296
- const o = p;
297
- return (typeof o.name === 'string' &&
298
- (typeof o.getSources === 'function' || typeof o.getEmbedUrl === 'function'));
287
+ const o = shapeOf(p);
288
+ return !!o && (typeof o.getSources === 'function' || typeof o.getEmbedUrl === 'function');
299
289
  }
300
290
  function makeEmbedProvider(id, base) {
301
291
  const root = base.trim().replace(/\/+$/, '');
@@ -1,14 +1,14 @@
1
- export function normalizeCompact(s) {
1
+ function normalizeCompact(s) {
2
2
  return s.toLowerCase().replace(/[^a-z0-9]/g, '');
3
3
  }
4
- export function tokenize(s) {
4
+ function tokenize(s) {
5
5
  return s
6
6
  .toLowerCase()
7
7
  .replace(/[^\p{L}\p{N}\s]/gu, ' ')
8
8
  .split(/\s+/)
9
9
  .filter((w) => w.length >= 2);
10
10
  }
11
- export function bigramDice(a, b) {
11
+ function bigramDice(a, b) {
12
12
  if (a.length < 2 || b.length < 2)
13
13
  return a === b ? 1 : 0;
14
14
  const grams = (s) => {
@@ -129,10 +129,10 @@ function detectSeasonMarker(s) {
129
129
  return trailingNum[1];
130
130
  return '';
131
131
  }
132
- export function distinctiveTokens(s) {
132
+ function distinctiveTokens(s) {
133
133
  return [...new Set(tokenize(s).filter((w) => !STOPWORDS.has(w)))];
134
134
  }
135
- export function titleSimilarity(query, candidate) {
135
+ function titleSimilarity(query, candidate) {
136
136
  const q = normalizeCompact(query);
137
137
  const c = normalizeCompact(candidate);
138
138
  if (!q || !c)
@@ -1,48 +1,13 @@
1
1
  import { eq, inArray, sql } from 'drizzle-orm';
2
2
  import { getDrizzle } from '../db/drizzle.js';
3
- import { asmrLibrary, asmrProgress } from '../db/schema-asmr.js';
4
- export const ASMR_STATUSES = [
5
- 'Listening',
6
- 'Completed',
7
- 'On-Hold',
8
- 'Dropped',
9
- 'Planned',
10
- ];
3
+ import { asmrProgress } from '../db/schema-asmr.js';
4
+ import { makeLibraryRepo } from './library.repository.js';
5
+ export const ASMR_STATUSES = ['Listening', 'Completed', 'On-Hold', 'Dropped', 'Planned'];
11
6
  export function buildAsmrId(rjCode) {
12
7
  return String(rjCode).trim().toUpperCase();
13
8
  }
14
9
  export const AsmrLibraryRepository = {
15
- getById: async (db, id) => {
16
- const rows = await getDrizzle(db).all(sql `SELECT * FROM asmr_library WHERE id = ${id}`);
17
- return rows[0];
18
- },
19
- exists: async (db, id) => {
20
- const rows = await getDrizzle(db).all(sql `SELECT EXISTS(SELECT 1 FROM asmr_library WHERE id = ${id}) as inLibrary`);
21
- return !!(rows[0] && rows[0].inLibrary);
22
- },
23
- getAll: (db, status, limit, offset) => {
24
- const q = sql `SELECT * FROM asmr_library`;
25
- if (status && status !== 'All') {
26
- q.append(sql ` WHERE status = ${status}`);
27
- }
28
- q.append(sql ` ORDER BY updatedAt DESC`);
29
- if (limit !== undefined && offset !== undefined) {
30
- q.append(sql ` LIMIT ${limit} OFFSET ${offset}`);
31
- }
32
- return getDrizzle(db).all(q);
33
- },
34
- getCount: async (db, status) => {
35
- if (status && status !== 'All') {
36
- const rows = await getDrizzle(db).all(sql `SELECT COUNT(*) as total FROM asmr_library WHERE status = ${status}`);
37
- return rows[0]?.total || 0;
38
- }
39
- const rows = await getDrizzle(db).all(sql `SELECT COUNT(*) as total FROM asmr_library`);
40
- return rows[0]?.total || 0;
41
- },
42
- getIds: async (db) => {
43
- const rows = await getDrizzle(db).all(sql `SELECT id FROM asmr_library`);
44
- return rows.map((r) => r.id);
45
- },
10
+ ...makeLibraryRepo('asmr_library'),
46
11
  upsert: (db, data) => getDrizzle(db).run(sql `
47
12
  INSERT INTO asmr_library (id, rjCode, title, thumbnail, status, isAdult, updatedAt)
48
13
  VALUES (${data.id}, ${data.rjCode}, ${data.title}, ${data.thumbnail}, ${data.status}, ${data.isAdult ? 1 : 0}, strftime('%s', 'now'))
@@ -53,22 +18,8 @@ export const AsmrLibraryRepository = {
53
18
  status = COALESCE(NULLIF(EXCLUDED.status, ''), asmr_library.status),
54
19
  isAdult = COALESCE(EXCLUDED.isAdult, asmr_library.isAdult),
55
20
  updatedAt = strftime('%s', 'now')`),
56
- updateStatus: (db, id, status) => getDrizzle(db).run(sql `
57
- UPDATE asmr_library SET status = ${status}, updatedAt = strftime('%s', 'now') WHERE id = ${id}`),
58
- updateStatusMany: (db, ids, status) => {
59
- if (ids.length === 0)
60
- return Promise.resolve();
61
- return getDrizzle(db).run(sql `
62
- UPDATE asmr_library SET status = ${status}, updatedAt = strftime('%s', 'now') WHERE id IN (${sql.join(ids.map((id) => sql `${id}`), sql `, `)})`);
63
- },
64
21
  touchProgress: (db, id, progress) => getDrizzle(db).run(sql `
65
22
  UPDATE asmr_library SET lastTrackIndex = ${progress.trackIndex}, lastTrackLabel = ${progress.trackLabel}, lastPosition = ${progress.position}, updatedAt = strftime('%s', 'now') WHERE id = ${id}`),
66
- delete: (db, id) => getDrizzle(db).delete(asmrLibrary).where(eq(asmrLibrary.id, id)),
67
- deleteMany: (db, ids) => {
68
- if (ids.length === 0)
69
- return Promise.resolve();
70
- return getDrizzle(db).delete(asmrLibrary).where(inArray(asmrLibrary.id, ids));
71
- },
72
23
  };
73
24
  export const AsmrProgressRepository = {
74
25
  getByWork: (db, workId) => getDrizzle(db).all(sql `
@@ -36,6 +36,52 @@ async function clearTables(db, tables) {
36
36
  }
37
37
  }
38
38
  }
39
+ export function makeLibraryRepo(table) {
40
+ const t = sql.raw(`"${table}"`);
41
+ return {
42
+ getById: async (db, id) => {
43
+ const rows = await getDrizzle(db).all(sql `SELECT * FROM ${t} WHERE id = ${id}`);
44
+ return rows[0];
45
+ },
46
+ getAll: (db, status, limit, offset) => {
47
+ const q = sql `SELECT * FROM ${t}`;
48
+ if (status && status !== 'All') {
49
+ q.append(sql ` WHERE status = ${status}`);
50
+ }
51
+ q.append(sql ` ORDER BY updatedAt DESC`);
52
+ if (limit !== undefined && offset !== undefined) {
53
+ q.append(sql ` LIMIT ${limit} OFFSET ${offset}`);
54
+ }
55
+ return getDrizzle(db).all(q);
56
+ },
57
+ getCount: async (db, status) => {
58
+ if (status && status !== 'All') {
59
+ const rows = await getDrizzle(db).all(sql `SELECT COUNT(*) as total FROM ${t} WHERE status = ${status}`);
60
+ return rows[0]?.total || 0;
61
+ }
62
+ const rows = await getDrizzle(db).all(sql `SELECT COUNT(*) as total FROM ${t}`);
63
+ return rows[0]?.total || 0;
64
+ },
65
+ getIds: async (db) => {
66
+ const rows = await getDrizzle(db).all(sql `SELECT id FROM ${t}`);
67
+ return rows.map((r) => r.id);
68
+ },
69
+ updateStatus: (db, id, status) => getDrizzle(db).run(sql `
70
+ UPDATE ${t} SET status = ${status}, updatedAt = strftime('%s', 'now') WHERE id = ${id}`),
71
+ updateStatusMany: (db, ids, status) => {
72
+ if (ids.length === 0)
73
+ return Promise.resolve();
74
+ return getDrizzle(db).run(sql `
75
+ UPDATE ${t} SET status = ${status}, updatedAt = strftime('%s', 'now') WHERE id IN (${sql.join(ids.map((id) => sql `${id}`), sql `, `)})`);
76
+ },
77
+ delete: (db, id) => getDrizzle(db).run(sql `DELETE FROM ${t} WHERE id = ${id}`),
78
+ deleteMany: (db, ids) => {
79
+ if (ids.length === 0)
80
+ return Promise.resolve();
81
+ return getDrizzle(db).run(sql `DELETE FROM ${t} WHERE id IN (${sql.join(ids.map((id) => sql `${id}`), sql `, `)})`);
82
+ },
83
+ };
84
+ }
39
85
  export const LibraryRepository = {
40
86
  countAll: (db) => countTables(db, LIBRARY_TABLES),
41
87
  clearAll: (db) => clearTables(db, LIBRARY_TABLES),
@@ -1,48 +1,13 @@
1
1
  import { eq, inArray, sql } from 'drizzle-orm';
2
2
  import { getDrizzle } from '../db/drizzle.js';
3
3
  import { mangaLibrary, mangaProgress } from '../db/schema-manga.js';
4
- export const MANGA_STATUSES = [
5
- 'Reading',
6
- 'Completed',
7
- 'On-Hold',
8
- 'Dropped',
9
- 'Planned',
10
- ];
4
+ import { makeLibraryRepo } from './library.repository.js';
5
+ export const MANGA_STATUSES = ['Reading', 'Completed', 'On-Hold', 'Dropped', 'Planned'];
11
6
  export function buildMangaId(provider, mangaId) {
12
7
  return `${provider.toLowerCase()}:${mangaId}`;
13
8
  }
14
9
  export const MangaLibraryRepository = {
15
- getById: async (db, id) => {
16
- const rows = await getDrizzle(db).all(sql `SELECT * FROM manga_library WHERE id = ${id}`);
17
- return rows[0];
18
- },
19
- exists: async (db, id) => {
20
- const rows = await getDrizzle(db).all(sql `SELECT EXISTS(SELECT 1 FROM manga_library WHERE id = ${id}) as inLibrary`);
21
- return !!(rows[0] && rows[0].inLibrary);
22
- },
23
- getAll: (db, status, limit, offset) => {
24
- const q = sql `SELECT * FROM manga_library`;
25
- if (status && status !== 'All') {
26
- q.append(sql ` WHERE status = ${status}`);
27
- }
28
- q.append(sql ` ORDER BY updatedAt DESC`);
29
- if (limit !== undefined && offset !== undefined) {
30
- q.append(sql ` LIMIT ${limit} OFFSET ${offset}`);
31
- }
32
- return getDrizzle(db).all(q);
33
- },
34
- getCount: async (db, status) => {
35
- if (status && status !== 'All') {
36
- const rows = await getDrizzle(db).all(sql `SELECT COUNT(*) as total FROM manga_library WHERE status = ${status}`);
37
- return rows[0]?.total || 0;
38
- }
39
- const rows = await getDrizzle(db).all(sql `SELECT COUNT(*) as total FROM manga_library`);
40
- return rows[0]?.total || 0;
41
- },
42
- getIds: async (db) => {
43
- const rows = await getDrizzle(db).all(sql `SELECT id FROM manga_library`);
44
- return rows.map((r) => r.id);
45
- },
10
+ ...makeLibraryRepo('manga_library'),
46
11
  upsert: (db, data) => getDrizzle(db).run(sql `
47
12
  INSERT INTO manga_library (id, provider, mangaId, title, cover, status, author, altTitle, contentRating, anilistId, anilistIdSource, updatedAt)
48
13
  VALUES (${data.id}, ${data.provider}, ${data.mangaId}, ${data.title}, ${data.cover}, ${data.status}, ${data.author || null}, ${data.altTitle || null}, ${data.contentRating || null}, ${data.anilistId ?? null}, ${data.anilistIdSource ?? null}, strftime('%s', 'now'))
@@ -63,25 +28,10 @@ export const MangaLibraryRepository = {
63
28
  .set({ anilistId, anilistIdSource: source })
64
29
  .where(eq(mangaLibrary.id, id))
65
30
  : getDrizzle(db).update(mangaLibrary).set({ anilistId }).where(eq(mangaLibrary.id, id)),
66
- getByAnilistId: (db, anilistId) => getDrizzle(db).all(sql `SELECT * FROM manga_library WHERE anilistId = ${anilistId}`),
67
- updateStatus: (db, id, status) => getDrizzle(db).run(sql `
68
- UPDATE manga_library SET status = ${status}, updatedAt = strftime('%s', 'now') WHERE id = ${id}`),
69
- updateStatusMany: (db, ids, status) => {
70
- if (ids.length === 0)
71
- return Promise.resolve();
72
- return getDrizzle(db).run(sql `
73
- UPDATE manga_library SET status = ${status}, updatedAt = strftime('%s', 'now') WHERE id IN (${sql.join(ids.map((id) => sql `${id}`), sql `, `)})`);
74
- },
75
31
  touchProgress: (db, id, progress) => getDrizzle(db).run(sql `
76
32
  UPDATE manga_library SET lastChapterId = ${progress.chapterId}, lastChapterNumber = ${progress.chapterNumber}, lastPage = ${progress.page}, updatedAt = strftime('%s', 'now') WHERE id = ${id}`),
77
33
  setProgressPointer: (db, id, chapterNumber) => getDrizzle(db).run(sql `
78
34
  UPDATE manga_library SET lastChapterNumber = ${chapterNumber}, updatedAt = strftime('%s', 'now') WHERE id = ${id}`),
79
- delete: (db, id) => getDrizzle(db).delete(mangaLibrary).where(eq(mangaLibrary.id, id)),
80
- deleteMany: (db, ids) => {
81
- if (ids.length === 0)
82
- return Promise.resolve();
83
- return getDrizzle(db).delete(mangaLibrary).where(inArray(mangaLibrary.id, ids));
84
- },
85
35
  };
86
36
  export const MangaProgressRepository = {
87
37
  getByManga: (db, mangaId) => getDrizzle(db).all(sql `
@@ -11,6 +11,14 @@ export const SettingsRepository = {
11
11
  .values({ key, value })
12
12
  .onConflictDoUpdate({ target: settings.key, set: { value } }),
13
13
  deleteByKey: (db, key) => getDrizzle(db).delete(settings).where(eq(settings.key, key)),
14
+ readContinueFlags: async (db, ignoreKey, listOnlyKey) => {
15
+ const ignoreRow = await SettingsRepository.getByKey(db, ignoreKey);
16
+ const listOnlyRow = await SettingsRepository.getByKey(db, listOnlyKey);
17
+ return {
18
+ ignoreAdult: ignoreRow ? ignoreRow.value !== 'false' : true,
19
+ listOnly: listOnlyRow ? listOnlyRow.value === 'true' || listOnlyRow.value === '1' : false,
20
+ };
21
+ },
14
22
  clearWatchlist: (db) => getDrizzle(db).delete(watchlist),
15
23
  upsertWatchlistBatch: (db, shows) => {
16
24
  if (shows.length === 0)
@@ -1,6 +1,7 @@
1
1
  import { eq, inArray, sql } from 'drizzle-orm';
2
2
  import { getDrizzle } from '../db/drizzle.js';
3
- import { tvLibrary, tvProgress } from '../db/schema-tv.js';
3
+ import { tvProgress } from '../db/schema-tv.js';
4
+ import { makeLibraryRepo } from './library.repository.js';
4
5
  export const TV_STATUSES = ['Watching', 'Completed', 'On-Hold', 'Dropped', 'Planned'];
5
6
  export function buildTvId(mediaType, tmdbId) {
6
7
  const t = String(mediaType).toLowerCase() === 'movie' ? 'movie' : 'tv';
@@ -14,37 +15,7 @@ export function normalizeTvMediaId(raw) {
14
15
  return s;
15
16
  }
16
17
  export const TvLibraryRepository = {
17
- getById: async (db, id) => {
18
- const rows = await getDrizzle(db).all(sql `SELECT * FROM tv_library WHERE id = ${id}`);
19
- return rows[0];
20
- },
21
- exists: async (db, id) => {
22
- const rows = await getDrizzle(db).all(sql `SELECT EXISTS(SELECT 1 FROM tv_library WHERE id = ${id}) as inLibrary`);
23
- return !!(rows[0] && rows[0].inLibrary);
24
- },
25
- getAll: (db, status, limit, offset) => {
26
- const q = sql `SELECT * FROM tv_library`;
27
- if (status && status !== 'All') {
28
- q.append(sql ` WHERE status = ${status}`);
29
- }
30
- q.append(sql ` ORDER BY updatedAt DESC`);
31
- if (limit !== undefined && offset !== undefined) {
32
- q.append(sql ` LIMIT ${limit} OFFSET ${offset}`);
33
- }
34
- return getDrizzle(db).all(q);
35
- },
36
- getCount: async (db, status) => {
37
- if (status && status !== 'All') {
38
- const rows = await getDrizzle(db).all(sql `SELECT COUNT(*) as total FROM tv_library WHERE status = ${status}`);
39
- return rows[0]?.total || 0;
40
- }
41
- const rows = await getDrizzle(db).all(sql `SELECT COUNT(*) as total FROM tv_library`);
42
- return rows[0]?.total || 0;
43
- },
44
- getIds: async (db) => {
45
- const rows = await getDrizzle(db).all(sql `SELECT id FROM tv_library`);
46
- return rows.map((r) => r.id);
47
- },
18
+ ...makeLibraryRepo('tv_library'),
48
19
  upsert: (db, data) => getDrizzle(db).run(sql `
49
20
  INSERT INTO tv_library (id, tmdbId, mediaType, title, poster, backdrop, year, overview, status, adult, updatedAt)
50
21
  VALUES (${data.id}, ${data.tmdbId}, ${data.mediaType}, ${data.title}, ${data.poster}, ${data.backdrop || null}, ${data.year || null}, ${data.overview || null}, ${data.status}, ${data.adult ? 1 : 0}, strftime('%s', 'now'))
@@ -59,22 +30,8 @@ export const TvLibraryRepository = {
59
30
  status = COALESCE(NULLIF(EXCLUDED.status, ''), tv_library.status),
60
31
  adult = COALESCE(EXCLUDED.adult, tv_library.adult),
61
32
  updatedAt = strftime('%s', 'now')`),
62
- updateStatus: (db, id, status) => getDrizzle(db).run(sql `
63
- UPDATE tv_library SET status = ${status}, updatedAt = strftime('%s', 'now') WHERE id = ${id}`),
64
- updateStatusMany: (db, ids, status) => {
65
- if (ids.length === 0)
66
- return Promise.resolve();
67
- return getDrizzle(db).run(sql `
68
- UPDATE tv_library SET status = ${status}, updatedAt = strftime('%s', 'now') WHERE id IN (${sql.join(ids.map((id) => sql `${id}`), sql `, `)})`);
69
- },
70
33
  touchProgress: (db, id, progress) => getDrizzle(db).run(sql `
71
34
  UPDATE tv_library SET lastSeason = ${progress.season}, lastEpisode = ${progress.episode}, updatedAt = strftime('%s', 'now') WHERE id = ${id}`),
72
- delete: (db, id) => getDrizzle(db).delete(tvLibrary).where(eq(tvLibrary.id, id)),
73
- deleteMany: (db, ids) => {
74
- if (ids.length === 0)
75
- return Promise.resolve();
76
- return getDrizzle(db).delete(tvLibrary).where(inArray(tvLibrary.id, ids));
77
- },
78
35
  };
79
36
  export const TvProgressRepository = {
80
37
  getByMedia: (db, mediaId) => getDrizzle(db).all(sql `
@@ -79,6 +79,4 @@ export const WatchedEpisodesRepository = {
79
79
  ORDER BY we.watchedAt DESC
80
80
  ${sql.raw(limitClause)}`);
81
81
  },
82
- getEpisodesForShows: (db, showIds) => getDrizzle(db).all(sql `
83
- SELECT showId, episodeNumber, currentTime, duration, watchedAt FROM watched_episodes WHERE showId IN (${sql.join(showIds.map((id) => sql `${id}`), sql `, `)})`),
84
82
  };
@@ -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;