@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.
- package/README.md +1 -1
- package/client/dist/assets/{AnimeCard-CIwTZI3D.js → AnimeCard-4CEz8Buu.js} +1 -1
- package/client/dist/assets/AnimeInfo-DyX9qTwg.js +1 -0
- package/client/dist/assets/AnimeInfoPage-Dvp_c8vK.js +2 -0
- package/client/dist/assets/Asmr-qg3lMAJn.js +2 -0
- package/client/dist/assets/{AsmrResetProgressModal-BDpA07Fa.js → AsmrResetProgressModal-BpPlg_Vv.js} +1 -1
- package/client/dist/assets/AvSyncCalibrator-C2wu1ydI.css +1 -0
- package/client/dist/assets/AvSyncCalibrator-FJllOgOH.js +10 -0
- package/client/dist/assets/Home-A7gCeEST.js +1 -0
- package/client/dist/assets/Insights-G9IXepHJ.js +1 -0
- package/client/dist/assets/{LibraryListPage-DWbcek1i.js → LibraryListPage-Bh2qCJPh.js} +1 -1
- package/client/dist/assets/{ListeningList-CcuxBzy1.js → ListeningList-Czv4cTRl.js} +1 -1
- package/client/dist/assets/{Manga-DpLaW39C.js → Manga-qdAAS1z7.js} +1 -1
- package/client/dist/assets/MangaInfoPage-1rTgZzf2.js +1 -0
- package/client/dist/assets/MangaReadPage-CeyxPG6w.js +1 -0
- package/client/dist/assets/{MangaResetProgressModal-CYguBG2f.js → MangaResetProgressModal-C_ppotmh.js} +1 -1
- package/client/dist/assets/Mature-Bkhxnr3K.js +1 -0
- package/client/dist/assets/{MediaCard-p29DqyKX.js → MediaCard-BQZsutEo.js} +1 -1
- package/client/dist/assets/Music-C0rGzSbi.js +1 -0
- package/client/dist/assets/{OptionListMenu-C1pvpu8X.js → OptionListMenu-CyfpCBEy.js} +1 -1
- package/client/dist/assets/Player-yhiTlpYU.js +56 -0
- package/client/dist/assets/PlayerSettings-icy4SfQa.js +1 -0
- package/client/dist/assets/{QueueOptionsButton-CWdoe1_I.js → QueueOptionsButton-5eloKF5J.js} +1 -1
- package/client/dist/assets/{QueueRail-BBpn9CII.js → QueueRail-DaMjlKL4.js} +1 -1
- package/client/dist/assets/Radio-FxGqCxR4.js +1 -0
- package/client/dist/assets/{ReadingList-D4xQ9tlw.js → ReadingList-CCm3IAbt.js} +1 -1
- package/client/dist/assets/{ResetProgressModal-JFlbXy7W.js → ResetProgressModal-CeI4V3Y5.js} +1 -1
- package/client/dist/assets/{Search-D-ocogP-.js → Search-qpILaAiQ.js} +1 -1
- package/client/dist/assets/Settings-Bw9M3NGz.js +21 -0
- package/client/dist/assets/{SkeletonGrid-N67HqYXx.js → SkeletonGrid-0dY_xMq3.js} +1 -1
- package/client/dist/assets/{Trackers-C_SPMGKx.js → Trackers-Zy5GOs0P.js} +2 -2
- package/client/dist/assets/Tv-Bx7tGS57.css +1 -0
- package/client/dist/assets/Tv-DREwc7BX.js +4 -0
- package/client/dist/assets/TvInfo-CHIsVzZ0.js +1 -0
- package/client/dist/assets/{TvPopup-DEB6Ufq4.js → TvPopup-D_tVTyZ2.js} +1 -1
- package/client/dist/assets/{TvSearch-grOUL0Bc.js → TvSearch-BVxedBW-.js} +1 -1
- package/client/dist/assets/{TvWatchlist-CBI21Vge.js → TvWatchlist-BEIwq9R9.js} +1 -1
- package/client/dist/assets/{Watchlist-5cDHUoC8.js → Watchlist-CSngjqlz.js} +1 -1
- package/client/dist/assets/{hls-DySVERd_.js → hls-D48GVTkk.js} +1 -1
- package/client/dist/assets/index-CVB5dYhT.js +178 -0
- package/client/dist/assets/{useAnimeInfoData-BMwVSGhZ.js → useAnimeInfoData-DxdOg_B7.js} +1 -1
- package/client/dist/assets/useAsmr-ClckxnPe.js +1 -0
- package/client/dist/assets/{useAsmrPopup-CY-6b88q.js → useAsmrPopup-Bbgxefy_.js} +1 -1
- package/client/dist/assets/useMangaLibrary-B3_BUKVk.js +1 -0
- package/client/dist/assets/{useMangaPopup-_pNXvRYJ.js → useMangaPopup-Celk4lzf.js} +1 -1
- package/client/dist/assets/{useProviders-CKSedfqb.js → useProviders-pwxPZPHh.js} +1 -1
- package/client/dist/assets/useTv-D9sQrDop.js +1 -0
- package/client/dist/assets/useTvLibrary-UsQpgBTB.js +1 -0
- package/client/dist/index.html +1 -1
- package/package.json +3 -3
- package/server/dist/app-hono.js +15 -37
- package/server/dist/db/drizzle.js +2 -18
- package/server/dist/db.js +3 -2
- package/server/dist/hono/tv.js +9 -5
- package/server/dist/hono/watchlist.js +10 -108
- package/server/dist/lib/migration.js +0 -5
- package/server/dist/providers/remote-loader.js +40 -1
- package/server/dist/providers/remote-types.js +9 -2
- package/server/dist/server.js +50 -182
- package/server/dist/sync.js +173 -704
- package/server/dist/trpc/routers/auth.js +2 -71
- package/server/dist/trpc/routers/continue-watching.js +0 -2
- package/server/dist/trpc/routers/data.js +0 -1
- package/server/dist/trpc/routers/discord.js +8 -0
- package/server/dist/trpc/routers/progress.js +95 -1
- package/server/dist/trpc/routers/queue.js +0 -2
- package/server/dist/trpc/routers/watchlist.js +0 -7
- package/server/dist/trpc/validation.js +64 -0
- package/server/package.json +2 -2
- package/client/dist/assets/AnimeInfo-DlfvDaR8.js +0 -1
- package/client/dist/assets/AnimeInfoPage-B3EPwtzx.js +0 -2
- package/client/dist/assets/Asmr-DCirAWM6.js +0 -2
- package/client/dist/assets/AvSyncCalibrator-BPa6KaGC.css +0 -1
- package/client/dist/assets/AvSyncCalibrator-BroSW86T.js +0 -10
- package/client/dist/assets/Home-DEjYtHbs.js +0 -1
- package/client/dist/assets/Insights-B3b6u8RW.js +0 -1
- package/client/dist/assets/MangaInfoPage-B7FPC4a_.js +0 -1
- package/client/dist/assets/MangaReadPage-Dx0qAAFe.js +0 -1
- package/client/dist/assets/Mature-AScDPmWW.js +0 -1
- package/client/dist/assets/Music-DFInMskb.js +0 -1
- package/client/dist/assets/Player-D-8zY3cx.js +0 -56
- package/client/dist/assets/PlayerSettings-CfJjs_0Q.js +0 -1
- package/client/dist/assets/Radio-DCgO5hAS.js +0 -1
- package/client/dist/assets/Settings-BsFbgkdy.js +0 -21
- package/client/dist/assets/Tv-D7s0Kqqx.js +0 -4
- package/client/dist/assets/Tv-ZEA45gXK.css +0 -1
- package/client/dist/assets/TvInfo-CrmCSFDF.js +0 -1
- package/client/dist/assets/index-HZNJp28e.js +0 -178
- package/client/dist/assets/useAsmr-BLc7FwPm.js +0 -1
- package/client/dist/assets/useMangaLibrary-XFRTq5Qg.js +0 -1
- package/client/dist/assets/useMatureConsent-Cra09TA0.js +0 -1
- package/client/dist/assets/useTv-Dizj6G4R.js +0 -1
- package/client/dist/assets/useTvLibrary-twjHQ89n.js +0 -1
package/server/dist/app-hono.js
CHANGED
|
@@ -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,
|
|
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
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
1
|
+
import { drizzle } from 'drizzle-orm/node-sqlite';
|
|
2
2
|
export function createDrizzle(db) {
|
|
3
|
-
|
|
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
|
-
|
|
31
|
-
|
|
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}`);
|
package/server/dist/hono/tv.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
9
|
-
import {
|
|
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
|
|
567
|
-
const mangadexCoverCache = new
|
|
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
|
|
574
|
-
return cached
|
|
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,
|
|
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
|
|
623
|
-
return cached
|
|
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,
|
|
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
|
-
|
|
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,
|