@serifpersia/dango 3.4.2 → 3.4.4

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 (93) hide show
  1. package/README.md +1 -1
  2. package/client/dist/assets/{AnimeCard-CIwTZI3D.js → AnimeCard-4CEz8Buu.js} +1 -1
  3. package/client/dist/assets/AnimeInfo-DyX9qTwg.js +1 -0
  4. package/client/dist/assets/AnimeInfoPage-Dvp_c8vK.js +2 -0
  5. package/client/dist/assets/Asmr-qg3lMAJn.js +2 -0
  6. package/client/dist/assets/{AsmrResetProgressModal-BDpA07Fa.js → AsmrResetProgressModal-BpPlg_Vv.js} +1 -1
  7. package/client/dist/assets/AvSyncCalibrator-C2wu1ydI.css +1 -0
  8. package/client/dist/assets/AvSyncCalibrator-FJllOgOH.js +10 -0
  9. package/client/dist/assets/Home-A7gCeEST.js +1 -0
  10. package/client/dist/assets/Insights-G9IXepHJ.js +1 -0
  11. package/client/dist/assets/{LibraryListPage-DWbcek1i.js → LibraryListPage-Bh2qCJPh.js} +1 -1
  12. package/client/dist/assets/{ListeningList-CcuxBzy1.js → ListeningList-Czv4cTRl.js} +1 -1
  13. package/client/dist/assets/{Manga-DpLaW39C.js → Manga-qdAAS1z7.js} +1 -1
  14. package/client/dist/assets/MangaInfoPage-1rTgZzf2.js +1 -0
  15. package/client/dist/assets/MangaReadPage-CeyxPG6w.js +1 -0
  16. package/client/dist/assets/{MangaResetProgressModal-CYguBG2f.js → MangaResetProgressModal-C_ppotmh.js} +1 -1
  17. package/client/dist/assets/Mature-Bkhxnr3K.js +1 -0
  18. package/client/dist/assets/{MediaCard-p29DqyKX.js → MediaCard-BQZsutEo.js} +1 -1
  19. package/client/dist/assets/Music-C0rGzSbi.js +1 -0
  20. package/client/dist/assets/{OptionListMenu-C1pvpu8X.js → OptionListMenu-CyfpCBEy.js} +1 -1
  21. package/client/dist/assets/Player-yhiTlpYU.js +56 -0
  22. package/client/dist/assets/PlayerSettings-icy4SfQa.js +1 -0
  23. package/client/dist/assets/{QueueOptionsButton-CWdoe1_I.js → QueueOptionsButton-5eloKF5J.js} +1 -1
  24. package/client/dist/assets/{QueueRail-BBpn9CII.js → QueueRail-DaMjlKL4.js} +1 -1
  25. package/client/dist/assets/Radio-FxGqCxR4.js +1 -0
  26. package/client/dist/assets/{ReadingList-D4xQ9tlw.js → ReadingList-CCm3IAbt.js} +1 -1
  27. package/client/dist/assets/{ResetProgressModal-JFlbXy7W.js → ResetProgressModal-CeI4V3Y5.js} +1 -1
  28. package/client/dist/assets/{Search-D-ocogP-.js → Search-qpILaAiQ.js} +1 -1
  29. package/client/dist/assets/Settings-Bw9M3NGz.js +21 -0
  30. package/client/dist/assets/{SkeletonGrid-N67HqYXx.js → SkeletonGrid-0dY_xMq3.js} +1 -1
  31. package/client/dist/assets/{Trackers-C_SPMGKx.js → Trackers-Zy5GOs0P.js} +2 -2
  32. package/client/dist/assets/Tv-Bx7tGS57.css +1 -0
  33. package/client/dist/assets/Tv-DREwc7BX.js +4 -0
  34. package/client/dist/assets/TvInfo-CHIsVzZ0.js +1 -0
  35. package/client/dist/assets/{TvPopup-DEB6Ufq4.js → TvPopup-D_tVTyZ2.js} +1 -1
  36. package/client/dist/assets/{TvSearch-grOUL0Bc.js → TvSearch-BVxedBW-.js} +1 -1
  37. package/client/dist/assets/{TvWatchlist-CBI21Vge.js → TvWatchlist-BEIwq9R9.js} +1 -1
  38. package/client/dist/assets/{Watchlist-5cDHUoC8.js → Watchlist-CSngjqlz.js} +1 -1
  39. package/client/dist/assets/{hls-DySVERd_.js → hls-D48GVTkk.js} +1 -1
  40. package/client/dist/assets/index-CVB5dYhT.js +178 -0
  41. package/client/dist/assets/{useAnimeInfoData-BMwVSGhZ.js → useAnimeInfoData-DxdOg_B7.js} +1 -1
  42. package/client/dist/assets/useAsmr-ClckxnPe.js +1 -0
  43. package/client/dist/assets/{useAsmrPopup-CY-6b88q.js → useAsmrPopup-Bbgxefy_.js} +1 -1
  44. package/client/dist/assets/useMangaLibrary-B3_BUKVk.js +1 -0
  45. package/client/dist/assets/{useMangaPopup-_pNXvRYJ.js → useMangaPopup-Celk4lzf.js} +1 -1
  46. package/client/dist/assets/{useProviders-CKSedfqb.js → useProviders-pwxPZPHh.js} +1 -1
  47. package/client/dist/assets/useTv-D9sQrDop.js +1 -0
  48. package/client/dist/assets/useTvLibrary-UsQpgBTB.js +1 -0
  49. package/client/dist/index.html +1 -1
  50. package/package.json +3 -3
  51. package/server/dist/app-hono.js +15 -37
  52. package/server/dist/db/drizzle.js +2 -18
  53. package/server/dist/db.js +3 -2
  54. package/server/dist/hono/tv.js +9 -5
  55. package/server/dist/hono/watchlist.js +10 -108
  56. package/server/dist/lib/migration.js +0 -5
  57. package/server/dist/providers/remote-loader.js +40 -1
  58. package/server/dist/providers/remote-types.js +9 -2
  59. package/server/dist/server.js +50 -182
  60. package/server/dist/sync.js +173 -704
  61. package/server/dist/trpc/routers/auth.js +2 -71
  62. package/server/dist/trpc/routers/continue-watching.js +0 -2
  63. package/server/dist/trpc/routers/data.js +0 -1
  64. package/server/dist/trpc/routers/discord.js +8 -0
  65. package/server/dist/trpc/routers/progress.js +95 -1
  66. package/server/dist/trpc/routers/queue.js +0 -2
  67. package/server/dist/trpc/routers/watchlist.js +0 -7
  68. package/server/dist/trpc/validation.js +64 -0
  69. package/server/package.json +2 -2
  70. package/client/dist/assets/AnimeInfo-DlfvDaR8.js +0 -1
  71. package/client/dist/assets/AnimeInfoPage-B3EPwtzx.js +0 -2
  72. package/client/dist/assets/Asmr-DCirAWM6.js +0 -2
  73. package/client/dist/assets/AvSyncCalibrator-BPa6KaGC.css +0 -1
  74. package/client/dist/assets/AvSyncCalibrator-BroSW86T.js +0 -10
  75. package/client/dist/assets/Home-DEjYtHbs.js +0 -1
  76. package/client/dist/assets/Insights-B3b6u8RW.js +0 -1
  77. package/client/dist/assets/MangaInfoPage-B7FPC4a_.js +0 -1
  78. package/client/dist/assets/MangaReadPage-Dx0qAAFe.js +0 -1
  79. package/client/dist/assets/Mature-AScDPmWW.js +0 -1
  80. package/client/dist/assets/Music-DFInMskb.js +0 -1
  81. package/client/dist/assets/Player-D-8zY3cx.js +0 -56
  82. package/client/dist/assets/PlayerSettings-CfJjs_0Q.js +0 -1
  83. package/client/dist/assets/Radio-DCgO5hAS.js +0 -1
  84. package/client/dist/assets/Settings-BsFbgkdy.js +0 -21
  85. package/client/dist/assets/Tv-D7s0Kqqx.js +0 -4
  86. package/client/dist/assets/Tv-ZEA45gXK.css +0 -1
  87. package/client/dist/assets/TvInfo-CrmCSFDF.js +0 -1
  88. package/client/dist/assets/index-HZNJp28e.js +0 -178
  89. package/client/dist/assets/useAsmr-BLc7FwPm.js +0 -1
  90. package/client/dist/assets/useMangaLibrary-XFRTq5Qg.js +0 -1
  91. package/client/dist/assets/useMatureConsent-Cra09TA0.js +0 -1
  92. package/client/dist/assets/useTv-Dizj6G4R.js +0 -1
  93. package/client/dist/assets/useTvLibrary-twjHQ89n.js +0 -1
@@ -11,12 +11,11 @@ import logger from './logger.js';
11
11
  import { LAN_AUTH_PUBLIC_PATHS, buildLanCookie, clearAllLanSessions, clearLanCookie, createLanSession, getTokenFromHeaders, hasAppPassword, isLoopbackIp, revokeLanSession, setAppPassword, validateLanSession, verifyAppPassword, } from './app-auth.js';
12
12
  import { importMangaFromMalXmlItems, } from './lib/tracker/manga-sync.service.js';
13
13
  import { LibraryRepository } from './repositories/library.repository.js';
14
- import { performAsmrWriteTransactionAsync, performMangaWriteTransactionAsync, performTvWriteTransactionAsync, performWriteTransactionAsync, setLocalAsmrManifestVersion, setLocalMangaManifestVersion, setLocalManifestVersion, setLocalTvManifestVersion, } from './sync.js';
14
+ import { performAsmrWriteTransactionAsync, performMangaWriteTransactionAsync, performTvWriteTransactionAsync, performWriteTransactionAsync, KIND_SYNC, setKindManifestVersion, } from './sync.js';
15
15
  import { ANIME_SYNC_TABLES, ASMR_SYNC_TABLES, MANGA_SYNC_TABLES, TV_SYNC_TABLES, exportTables, importTables, normalizePayload, readPayloadVersion, } from './sync-payload.js';
16
16
  import { getMalImportStatus, prepareMalImport, executeMalImport, } from './lib/mal-import.js';
17
17
  import { registerMusic } from './hono/music.js';
18
18
  import { registerAuth } from './hono/auth.js';
19
- import { registerWatchlist } from './hono/watchlist.js';
20
19
  import { registerTv } from './hono/tv.js';
21
20
  import { registerProxy } from './hono/proxy.js';
22
21
  import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
@@ -105,40 +104,20 @@ async function restoreJsonBackup(c, dbs, buffer) {
105
104
  if (!databases || typeof databases !== 'object') {
106
105
  return c.json({ error: 'Invalid backup file: missing databases.' }, 400);
107
106
  }
108
- const targets = [
109
- {
110
- key: 'anime',
111
- db: dbs.db,
112
- tables: ANIME_SYNC_TABLES,
113
- libraryTables: ['watchlist', 'watched_episodes'],
114
- backupName: 'pre-sync-backup.db',
115
- setVersion: setLocalManifestVersion,
116
- },
117
- {
118
- key: 'manga',
119
- db: dbs.mangaDb,
120
- tables: MANGA_SYNC_TABLES,
121
- libraryTables: ['manga_library', 'manga_progress'],
122
- backupName: 'pre-sync-manga-backup.db',
123
- setVersion: setLocalMangaManifestVersion,
124
- },
125
- {
126
- key: 'tv',
127
- db: dbs.tvDb,
128
- tables: TV_SYNC_TABLES,
129
- libraryTables: ['tv_library', 'tv_progress'],
130
- backupName: 'pre-sync-tv-backup.db',
131
- setVersion: setLocalTvManifestVersion,
132
- },
133
- {
134
- key: 'asmr',
135
- db: dbs.asmrDb,
136
- tables: ASMR_SYNC_TABLES,
137
- libraryTables: ['asmr_library', 'asmr_progress'],
138
- backupName: 'pre-sync-asmr-backup.db',
139
- setVersion: setLocalAsmrManifestVersion,
140
- },
141
- ];
107
+ const kindDbs = {
108
+ anime: dbs.db,
109
+ manga: dbs.mangaDb,
110
+ tv: dbs.tvDb,
111
+ asmr: dbs.asmrDb,
112
+ };
113
+ const targets = Object.keys(KIND_SYNC).map((kind) => ({
114
+ key: kind,
115
+ db: kindDbs[kind],
116
+ tables: KIND_SYNC[kind].tables,
117
+ libraryTables: KIND_SYNC[kind].libraryTables,
118
+ backupName: KIND_SYNC[kind].backupName,
119
+ setVersion: (version) => setKindManifestVersion(kind, version),
120
+ }));
142
121
  const present = targets.filter((t) => databases[t.key] !== undefined);
143
122
  if (present.length === 0) {
144
123
  return c.json({ error: 'Invalid backup file: no databases found.' }, 400);
@@ -609,7 +588,6 @@ export function createHonoApp(getState, getGuardState, getDbs, dbAdmin, provider
609
588
  });
610
589
  registerMusic(app);
611
590
  registerAuth(app, getDbs, runSync);
612
- registerWatchlist(app, getDbs);
613
591
  registerTv(app, media.getApiCache, media.getTvProvider);
614
592
  registerProxy(app);
615
593
  app.onError((err, c) => {
@@ -1,22 +1,6 @@
1
- import { drizzle } from 'drizzle-orm/sqlite-proxy';
1
+ import { drizzle } from 'drizzle-orm/node-sqlite';
2
2
  export function createDrizzle(db) {
3
- const callback = async (sql, params, method) => {
4
- const args = params;
5
- if (method === 'run') {
6
- db.run(sql, args);
7
- return { rows: [] };
8
- }
9
- if (method === 'get') {
10
- const row = db.get(sql, args);
11
- return { rows: row === undefined ? [] : [row] };
12
- }
13
- const rows = db.all(sql, args);
14
- if (method === 'values') {
15
- return { rows: rows.map((r) => Object.values(r)) };
16
- }
17
- return { rows };
18
- };
19
- return drizzle(callback);
3
+ return drizzle({ client: db.getClient() });
20
4
  }
21
5
  const cache = new WeakMap();
22
6
  export function getDrizzle(db) {
package/server/dist/db.js CHANGED
@@ -27,8 +27,9 @@ export class DatabaseWrapper {
27
27
  throw e;
28
28
  }
29
29
  }
30
- scheduleSave() { }
31
- async saveNow() { }
30
+ getClient() {
31
+ return this.db;
32
+ }
32
33
  configure(option, value) {
33
34
  if (option === 'busyTimeout') {
34
35
  this.db.exec(`PRAGMA busy_timeout = ${value}`);
@@ -1,3 +1,4 @@
1
+ import logger from '../logger.js';
1
2
  import { parseJsonBody } from '../utils/http.utils.js';
2
3
  import { getTmdbKey, TMDB_BASE, TMDB_IMAGE } from '../lib/tmdb.js';
3
4
  import { URL } from 'url';
@@ -161,7 +162,8 @@ export function registerTv(app, getApiCache, getTvProvider) {
161
162
  const chunks = [];
162
163
  const reader = fetchResp.body?.getReader();
163
164
  if (!reader) {
164
- return c.text('No response body', 500);
165
+ logger.warn({ host: safeCheck.url?.hostname }, 'tv stream-proxy: upstream sent no body');
166
+ return c.text('Upstream sent no body', 502);
165
167
  }
166
168
  try {
167
169
  while (true) {
@@ -181,14 +183,16 @@ export function registerTv(app, getApiCache, getTvProvider) {
181
183
  }
182
184
  return new Response(body, { status, headers: outHeaders });
183
185
  }
184
- catch {
185
- return c.text('Proxy error', 500);
186
+ catch (err) {
187
+ logger.warn({ err, host: safeCheck.url?.hostname }, 'tv stream-proxy: upstream body stream failed');
188
+ return c.text('Upstream stream failed', 502);
186
189
  }
187
190
  }
188
- catch {
191
+ catch (err) {
189
192
  if (abort.signal.aborted)
190
193
  return new Response(null, { status: 499 });
191
- return c.text('Proxy error', 500);
194
+ logger.warn({ err, host: safeCheck.url?.hostname }, 'tv stream-proxy: upstream fetch failed');
195
+ return c.text('Upstream fetch failed', 502);
192
196
  }
193
197
  finally {
194
198
  clearTimeout(timeout);
@@ -1,14 +1,12 @@
1
1
  import logger from '../logger.js';
2
- import { performWriteTransactionAsync } from '../sync.js';
3
2
  import { WatchlistRepository } from '../repositories/watchlist.repository.js';
4
3
  import { WatchedEpisodesRepository, } from '../repositories/watched-episodes.repository.js';
5
4
  import { ShowsMetaRepository } from '../repositories/shows-meta.repository.js';
6
5
  import { NotificationsRepository } from '../repositories/notifications.repository.js';
7
6
  import { SettingsRepository } from '../repositories/settings.repository.js';
8
- import { discordRPCService } from '../discord-rpc.js';
9
- import { dbAll, dbGet } from '../utils/db-utils.js';
7
+ import { dbAll } from '../utils/db-utils.js';
8
+ import { AppCache } from '../utils/cache.utils.js';
10
9
  import { searchAnilist, getAiredEpisodesForShows, getAnilistEpisodes, getShowMetaById, isAnilistRateLimited, batchGetShowStatuses, } from '../lib/anilist.js';
11
- import { getMigratedId } from '../lib/migration.js';
12
10
  import { offlineDb } from '../lib/offline-db.js';
13
11
  import { parseJsonBody } from '../utils/http.utils.js';
14
12
  import { pickBestMatch } from '../providers/title-matching.js';
@@ -147,8 +145,6 @@ export function startWatchlistDiscovery(getDb) {
147
145
  persisted++;
148
146
  }
149
147
  }
150
- if (persisted > 0)
151
- db.scheduleSave();
152
148
  }
153
149
  if (finishedShowIds.size > 0) {
154
150
  const monthStart = new Date(now);
@@ -192,8 +188,6 @@ export function startWatchlistDiscovery(getDb) {
192
188
  inserted = true;
193
189
  }
194
190
  }
195
- if (inserted)
196
- db.scheduleSave();
197
191
  }
198
192
  }
199
193
  const episodesByShow = new Map();
@@ -229,8 +223,6 @@ export function startWatchlistDiscovery(getDb) {
229
223
  inserted = true;
230
224
  }
231
225
  }
232
- if (inserted)
233
- db.scheduleSave();
234
226
  }
235
227
  await NotificationsRepository.cleanupWatchedNotifications(db);
236
228
  }
@@ -474,7 +466,6 @@ async function backfillMissingPosters(db, rows) {
474
466
  // ignore
475
467
  }
476
468
  }));
477
- db.scheduleSave();
478
469
  await sweepOfflinePosters(db);
479
470
  }
480
471
  async function sweepOfflinePosters(db) {
@@ -498,7 +489,6 @@ async function sweepOfflinePosters(db) {
498
489
  if (poster)
499
490
  await applyPoster(db, id, poster);
500
491
  }
501
- db.scheduleSave();
502
492
  }
503
493
  function resolveOfflinePoster(id) {
504
494
  if (/^\d+$/.test(id)) {
@@ -563,15 +553,15 @@ async function resolveAvailableEpisodes(db, showId) {
563
553
  : [];
564
554
  return episodes;
565
555
  }
566
- const dlsitePosterCache = new Map();
567
- const mangadexCoverCache = new Map();
556
+ const dlsitePosterCache = new AppCache({ ttlSeconds: 3600, maxKeys: 2000 });
557
+ const mangadexCoverCache = new AppCache({ ttlSeconds: 3600, maxKeys: 2000 });
568
558
  export async function getMangadexCover(title) {
569
559
  const key = title.trim().toLowerCase();
570
560
  if (!key)
571
561
  return null;
572
562
  const cached = mangadexCoverCache.get(key);
573
- if (cached && Date.now() - cached.ts < 3600_000)
574
- return cached.url;
563
+ if (cached !== undefined)
564
+ return cached;
575
565
  try {
576
566
  const params = new URLSearchParams({
577
567
  title: title.trim(),
@@ -607,7 +597,7 @@ export async function getMangadexCover(title) {
607
597
  }
608
598
  const match = pickBestMatch(candidates, [title]);
609
599
  const url = match ? match.item.cover : null;
610
- mangadexCoverCache.set(key, { url, ts: Date.now() });
600
+ mangadexCoverCache.set(key, url);
611
601
  return url;
612
602
  }
613
603
  catch {
@@ -619,8 +609,8 @@ export async function getDlsitePoster(rjCode) {
619
609
  if (!/^RJ\d{5,}$/.test(key))
620
610
  return null;
621
611
  const cached = dlsitePosterCache.get(key);
622
- if (cached && Date.now() - cached.ts < 3600_000)
623
- return cached.url;
612
+ if (cached !== undefined)
613
+ return cached;
624
614
  try {
625
615
  const res = await fetch(`https://www.dlsite.com/maniax/product/info/ajax?product_id=${key}`, {
626
616
  headers: { 'User-Agent': 'Mozilla/5.0', Accept: 'application/json' },
@@ -636,98 +626,10 @@ export async function getDlsitePoster(rjCode) {
636
626
  if (!img)
637
627
  return null;
638
628
  const url = img.startsWith('//') ? `https:${img}` : img;
639
- dlsitePosterCache.set(key, { url, ts: Date.now() });
629
+ dlsitePosterCache.set(key, url);
640
630
  return url;
641
631
  }
642
632
  catch {
643
633
  return null;
644
634
  }
645
635
  }
646
- export function registerWatchlist(app, getDbs) {
647
- const db = () => getDbs().db;
648
- app.post('/api/update-progress', async (c) => {
649
- const { showId: showIdRaw, episodeNumber, currentTime, duration, showName, showThumbnail, nativeName, englishName, genres, popularityScore, type, status, episodeCount, isPlaying, sessionId, isAdult, } = (await c.req.json());
650
- const showId = await getMigratedId(db(), showIdRaw);
651
- const titlePreferenceRow = await SettingsRepository.getByKey(db(), 'titlePreference');
652
- const titlePreference = titlePreferenceRow ? titlePreferenceRow.value : 'englishName';
653
- let displayName = showName;
654
- if (titlePreference === 'englishName' && englishName) {
655
- displayName = englishName;
656
- }
657
- else if (titlePreference === 'nativeName' && nativeName) {
658
- displayName = nativeName;
659
- }
660
- discordRPCService.updatePresence({
661
- title: displayName,
662
- episode: String(episodeNumber),
663
- totalEpisodes: episodeCount ? String(episodeCount) : undefined,
664
- currentTime: currentTime || 0,
665
- duration: duration || 0,
666
- thumbnail: showThumbnail || '',
667
- isPlaying: isPlaying !== false,
668
- sessionId: sessionId,
669
- isAdult: isAdult,
670
- });
671
- const genresStr = Array.isArray(genres) ? JSON.stringify(genres) : genres;
672
- const anilistId = /^\d+$/.test(showId)
673
- ? (dbGet(db(), 'SELECT anilistId FROM shows_meta WHERE id = ? AND anilistId IS NOT NULL', [showId])?.anilistId ?? parseInt(showId))
674
- : undefined;
675
- const metaCandidate = {
676
- name: showName,
677
- thumbnail: showThumbnail,
678
- nativeName: nativeName,
679
- englishName: englishName,
680
- genres: genresStr,
681
- popularityScore: popularityScore,
682
- status: status,
683
- episodeCount: episodeCount,
684
- type: type,
685
- anilistId,
686
- isAdult: typeof isAdult === 'boolean' ? (isAdult ? 1 : 0) : null,
687
- episodeDuration: duration > 0 ? Math.round(duration / 60) : undefined,
688
- };
689
- const metaChanged = await showsMetaChanged(db(), showId, metaCandidate);
690
- if (metaChanged) {
691
- await performWriteTransactionAsync(db(), async (tx) => {
692
- await ShowsMetaRepository.upsert(tx, {
693
- id: showId,
694
- ...metaCandidate,
695
- });
696
- });
697
- }
698
- await performWriteTransactionAsync(db(), async (tx) => {
699
- await WatchedEpisodesRepository.upsert(tx, {
700
- showId,
701
- episodeNumber: episodeNumber,
702
- currentTime: currentTime,
703
- duration: duration,
704
- });
705
- await NotificationsRepository.deleteSpecificDismissed(tx, showId, episodeNumber);
706
- await NotificationsRepository.deleteDiscovered(tx, showId, episodeNumber);
707
- });
708
- db().scheduleSave();
709
- return c.json({ success: true });
710
- });
711
- app.post('/api/discord/clear', async (c) => {
712
- const { sessionId } = (await c.req.json());
713
- if (!discordRPCService.isServiceEnabled) {
714
- return c.json({ success: true });
715
- }
716
- discordRPCService.clearPresence(sessionId);
717
- return c.json({ success: true });
718
- });
719
- app.post('/api/discord/heartbeat', async (c) => {
720
- const body = (await c.req.json().catch(() => undefined));
721
- const { sessionId, bye } = body ?? {};
722
- if (typeof sessionId !== 'string' || !discordRPCService.isServiceEnabled) {
723
- return c.json({ success: true });
724
- }
725
- if (bye) {
726
- discordRPCService.removeHeartbeat(sessionId);
727
- }
728
- else {
729
- discordRPCService.heartbeat(sessionId);
730
- }
731
- return c.json({ success: true });
732
- });
733
- }
@@ -62,7 +62,6 @@ async function consolidateFromNumeric(db, numericId) {
62
62
  canonicalId,
63
63
  ]);
64
64
  });
65
- db.scheduleSave();
66
65
  return canonicalId;
67
66
  }
68
67
  if (await WatchlistRepository.exists(db, canonicalId)) {
@@ -72,7 +71,6 @@ async function consolidateFromNumeric(db, numericId) {
72
71
  numericId,
73
72
  canonicalId,
74
73
  ]);
75
- db.scheduleSave();
76
74
  logger.info({ aliasId: numericId, canonicalId }, 'Mapped numeric alias to canonical ID');
77
75
  }
78
76
  return canonicalId;
@@ -133,7 +131,6 @@ async function consolidateFromNumeric(db, numericId) {
133
131
  numericId,
134
132
  ]);
135
133
  });
136
- db.scheduleSave();
137
134
  return numericId;
138
135
  }
139
136
  async function migrateId(db, legacyId) {
@@ -190,7 +187,6 @@ async function migrateId(db, legacyId) {
190
187
  legacyId,
191
188
  ]);
192
189
  });
193
- db.scheduleSave();
194
190
  logger.info({ legacyId, previousId: mapping.numericId, newId: canonicalId }, 'Canonicalized migrated show ID');
195
191
  return canonicalId;
196
192
  }
@@ -278,7 +274,6 @@ async function migrateId(db, legacyId) {
278
274
  ]);
279
275
  tx.run('DELETE FROM discovered_notifications WHERE showId = ?', [legacyId]);
280
276
  });
281
- db.scheduleSave();
282
277
  logger.info({ legacyId, newId, showName }, 'Successfully migrated legacy show ID to numeric ID');
283
278
  return newId;
284
279
  }
@@ -60,7 +60,7 @@ async function readModuleBytes(entryUrl) {
60
60
  return await fs.promises.readFile(filePath);
61
61
  }
62
62
  function resolveEntryUrl(entry, registryUrl) {
63
- const e = entry.trim();
63
+ const e = (entry ?? '').trim();
64
64
  if (isHttp(e) || e.startsWith('file://'))
65
65
  return e;
66
66
  if (isHttp(registryUrl))
@@ -176,6 +176,26 @@ function createCtx(cache) {
176
176
  buildQueryVariants,
177
177
  pickBestMatch,
178
178
  },
179
+ resolveBestShowId: async (title, romaji, searchFn) => {
180
+ const targets = [title, romaji].filter((t) => typeof t === 'string' && t.trim().length > 0);
181
+ if (targets.length === 0)
182
+ return null;
183
+ for (const variant of buildQueryVariants(title, romaji)) {
184
+ let results;
185
+ try {
186
+ results = await searchFn(variant);
187
+ }
188
+ catch {
189
+ continue;
190
+ }
191
+ if (!results || results.length === 0)
192
+ continue;
193
+ const match = pickBestMatch(results.filter((r) => r && typeof r.id === 'string' && r.id.length > 0), targets);
194
+ if (match)
195
+ return match.item.id;
196
+ }
197
+ return null;
198
+ },
179
199
  anilist: {
180
200
  request: (query, vars) => anilistRequest(query, vars),
181
201
  parseMalId,
@@ -277,7 +297,26 @@ function isValidTvProvider(p) {
277
297
  return (typeof o.name === 'string' &&
278
298
  (typeof o.getSources === 'function' || typeof o.getEmbedUrl === 'function'));
279
299
  }
300
+ function makeEmbedProvider(id, base) {
301
+ const root = base.trim().replace(/\/+$/, '');
302
+ return {
303
+ name: id,
304
+ getEmbedUrl: async (media) => {
305
+ const tmdbId = Number(media.tmdbId);
306
+ if (!tmdbId)
307
+ return null;
308
+ if (media.type === 'movie')
309
+ return `${root}/movie/${tmdbId}`;
310
+ return `${root}/tv/${tmdbId}/${media.season || 1}/${media.episode || 1}`;
311
+ },
312
+ };
313
+ }
280
314
  async function loadEntry(item, registryUrl, cacheDir, ctx, allowUnsigned) {
315
+ if (item.embedBase) {
316
+ const provider = makeEmbedProvider(item.id, item.embedBase);
317
+ instanceCache.set(`${item.kind ?? 'anime'}:${item.id}-${item.version}-embed`, { provider });
318
+ return { provider };
319
+ }
281
320
  const entryUrl = resolveEntryUrl(item.entry, registryUrl);
282
321
  await fs.promises.mkdir(cacheDir, { recursive: true });
283
322
  const fileName = `${item.id}-${item.version}.mjs`;
@@ -39,7 +39,13 @@ function parseEntry(raw, index) {
39
39
  if (typeof version !== 'string' || !version.trim()) {
40
40
  throw new Error(`registry.providers[${index}].version must be a non-empty string`);
41
41
  }
42
- if (typeof entry !== 'string' || !entry.trim()) {
42
+ const embedBase = p.embedBase;
43
+ if (embedBase !== undefined) {
44
+ if (typeof embedBase !== 'string' || !/^https:\/\/[^/]+/i.test(embedBase.trim())) {
45
+ throw new Error(`registry.providers[${index}].embedBase must be an https URL`);
46
+ }
47
+ }
48
+ if ((typeof entry !== 'string' || !entry.trim()) && embedBase === undefined) {
43
49
  throw new Error(`registry.providers[${index}].entry must be a non-empty string`);
44
50
  }
45
51
  if (p.sha256 !== undefined && (typeof p.sha256 !== 'string' || !SHA_RE.test(p.sha256.trim()))) {
@@ -78,7 +84,8 @@ function parseEntry(raw, index) {
78
84
  id: id.trim(),
79
85
  label: label.trim(),
80
86
  version: version.trim(),
81
- entry: entry.trim(),
87
+ entry: typeof entry === 'string' ? entry.trim() : undefined,
88
+ embedBase: typeof embedBase === 'string' ? embedBase.trim().replace(/\/+$/, '') : undefined,
82
89
  sha256: typeof p.sha256 === 'string' ? p.sha256.trim().toLowerCase() : undefined,
83
90
  minDango: typeof p.minDango === 'string' ? p.minDango : undefined,
84
91
  mature: typeof p.mature === 'boolean' ? p.mature : false,