@serifpersia/dango 3.4.1 → 3.4.3
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-B0oMgXBm.js → AnimeCard-Chlfp7Cm.js} +1 -1
- package/client/dist/assets/AnimeInfo-C1XZcEzj.js +1 -0
- package/client/dist/assets/AnimeInfoPage-BvNYYbe1.js +2 -0
- package/client/dist/assets/{AnimeMetaDetails-BPvv5xIl.js → AnimeMetaDetails-BogOi_bM.js} +1 -1
- package/client/dist/assets/Asmr-_jVaYDfT.js +2 -0
- package/client/dist/assets/{AsmrResetProgressModal-C9R7HlNa.js → AsmrResetProgressModal-ngn9hdA0.js} +1 -1
- package/client/dist/assets/AvSyncCalibrator-C2wu1ydI.css +1 -0
- package/client/dist/assets/AvSyncCalibrator-Rcv7aubm.js +10 -0
- package/client/dist/assets/{ErrorMessage-z4f4WGgP.js → ErrorMessage-DAxegITP.js} +1 -1
- package/client/dist/assets/Home-B6bte0tw.css +1 -0
- package/client/dist/assets/Home-CEbLBbmD.js +1 -0
- package/client/dist/assets/Insights-Bia4rfp0.js +1 -0
- package/client/dist/assets/{LibraryListPage-B_r51cRi.js → LibraryListPage-BVHcMPqZ.js} +1 -1
- package/client/dist/assets/{ListeningList-BWnwvJTt.js → ListeningList-DpRNQKk7.js} +1 -1
- package/client/dist/assets/{Manga-CI5cZg3h.js → Manga-wHddjSTh.js} +1 -1
- package/client/dist/assets/MangaInfoPage-CVz8z5wn.js +1 -0
- package/client/dist/assets/MangaReadPage-CC3cgtIZ.js +1 -0
- package/client/dist/assets/{MangaResetProgressModal-CPMEqZyi.js → MangaResetProgressModal-DD7a0log.js} +1 -1
- package/client/dist/assets/{Map-Dj51g5zL.js → Map-BtXwpBOC.js} +1 -1
- package/client/dist/assets/Mature-D8uFpIDj.js +1 -0
- package/client/dist/assets/{MediaCard-DzA336CA.js → MediaCard-6Yy_-Whn.js} +1 -1
- package/client/dist/assets/Music-BgM0bcAU.js +1 -0
- package/client/dist/assets/{OptionListMenu-CpwxAC3J.js → OptionListMenu-C9saW22f.js} +1 -1
- package/client/dist/assets/Player-EwAWFxqQ.js +56 -0
- package/client/dist/assets/{PlayerRedirect-D70t8VUW.js → PlayerRedirect-fv2tzEUu.js} +1 -1
- package/client/dist/assets/PlayerSettings-BYqhlu3F.js +1 -0
- package/client/dist/assets/{QueueOptionsButton-1M7MdKzM.js → QueueOptionsButton-BJhkcl2X.js} +1 -1
- package/client/dist/assets/{QueueRail-Bpla2xD8.js → QueueRail-BLfkt_s5.js} +1 -1
- package/client/dist/assets/Radio-D2Er2JEU.js +1 -0
- package/client/dist/assets/{ReadingList-wJMDUybm.js → ReadingList-DWOnwYO1.js} +1 -1
- package/client/dist/assets/{ResetProgressModal-CByRM1Fh.js → ResetProgressModal-HUj5zATK.js} +1 -1
- package/client/dist/assets/{Search-B94D0tRM.js → Search-CzD9J9ip.js} +1 -1
- package/client/dist/assets/Settings-BgUothvZ.js +21 -0
- package/client/dist/assets/{SkeletonGrid-BJyZkg_K.js → SkeletonGrid-C7Gj9ODL.js} +1 -1
- package/client/dist/assets/{SynopsisText-BcpwT5iw.js → SynopsisText-B3Sa5pRv.js} +1 -1
- package/client/dist/assets/{Trackers-vz-gqxtm.js → Trackers-BoC2z0Nd.js} +2 -2
- package/client/dist/assets/Tv-Bx7tGS57.css +1 -0
- package/client/dist/assets/Tv-Q8TIMWaP.js +4 -0
- package/client/dist/assets/TvInfo-C6JoNbev.js +1 -0
- package/client/dist/assets/{TvPopup-CqlVRF-Y.js → TvPopup-BArrygql.js} +1 -1
- package/client/dist/assets/{TvSearch-BcKXQue9.js → TvSearch-BsoJ7WZp.js} +1 -1
- package/client/dist/assets/{TvWatchlist-sjsiup2h.js → TvWatchlist-BFmAd2fE.js} +1 -1
- package/client/dist/assets/{VirtualKeyboard-6ZNTym4R.js → VirtualKeyboard-BmqZPZXR.js} +1 -1
- package/client/dist/assets/{Watchlist-DAMdvt7Z.js → Watchlist-DG5Y--YA.js} +1 -1
- package/client/dist/assets/{hls-Do_n10_Y.js → hls-QeZPjik8.js} +1 -1
- package/client/dist/assets/index-Cc1Up_NI.css +1 -0
- package/client/dist/assets/index-Rm4h42Uz.js +178 -0
- package/client/dist/assets/{useAnimeInfoData-DU9X90no.js → useAnimeInfoData-B9O3Rr_u.js} +1 -1
- package/client/dist/assets/useAsmr-Da7AVA9e.js +1 -0
- package/client/dist/assets/{useAsmrPopup-DK1Vx-HV.js → useAsmrPopup-B_S5s4q3.js} +1 -1
- package/client/dist/assets/{useIsMobile-Dm_b6Ob3.js → useIsMobile-Dvx1uJ82.js} +1 -1
- package/client/dist/assets/useMangaLibrary-COpztbMg.js +1 -0
- package/client/dist/assets/{useMangaPopup-BLwvRZn0.js → useMangaPopup-BJeHmEpe.js} +1 -1
- package/client/dist/assets/{useProviders-D3L1Chqc.js → useProviders-BWRYjB77.js} +1 -1
- package/client/dist/assets/useTv-DtiSJ756.js +1 -0
- package/client/dist/assets/useTvLibrary-DFfgk9YS.js +1 -0
- package/client/dist/assets/{vendor-core-8R2fFbyN.js → vendor-core-DfaOB3Rn.js} +1 -1
- package/client/dist/assets/{vendor-icons-CDMuNDeL.js → vendor-icons-DrybRNxP.js} +1 -1
- package/client/dist/assets/{vendor-query-BUTye89G.js → vendor-query-BBT5_sHx.js} +1 -1
- package/client/dist/assets/{vendor-ui-CUDCvRS_.js → vendor-ui-3qrARCPr.js} +1 -1
- package/client/dist/index.html +5 -5
- package/orchestrator.js +0 -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-o0Udf0og.js +0 -1
- package/client/dist/assets/AnimeInfoPage-CojMIEuf.js +0 -2
- package/client/dist/assets/Asmr-2V3yCCiK.js +0 -2
- package/client/dist/assets/AvSyncCalibrator-BPa6KaGC.css +0 -1
- package/client/dist/assets/AvSyncCalibrator-Bo3820j3.js +0 -10
- package/client/dist/assets/Home-C8uXTzIM.js +0 -1
- package/client/dist/assets/Home-DtRtTuI9.css +0 -1
- package/client/dist/assets/Insights-CRqBfZuK.js +0 -1
- package/client/dist/assets/MangaInfoPage-BtkOrO34.js +0 -1
- package/client/dist/assets/MangaReadPage-Uv72AYdY.js +0 -1
- package/client/dist/assets/Mature-DXHWvoY5.js +0 -1
- package/client/dist/assets/Music-C4dM0BQx.js +0 -1
- package/client/dist/assets/Player-CpMvWTkH.js +0 -56
- package/client/dist/assets/PlayerSettings-pMU85k7p.js +0 -1
- package/client/dist/assets/Radio-DlmTC-qG.js +0 -1
- package/client/dist/assets/Settings-CcU3oNY8.js +0 -21
- package/client/dist/assets/Tv-Rji_lvQo.js +0 -4
- package/client/dist/assets/Tv-ZEA45gXK.css +0 -1
- package/client/dist/assets/TvInfo-Ciujd6os.js +0 -1
- package/client/dist/assets/index-B5SLDCAi.js +0 -178
- package/client/dist/assets/index-CUm1meqr.css +0 -1
- package/client/dist/assets/useAsmr-D6cSjbvt.js +0 -1
- package/client/dist/assets/useMangaLibrary-DcxPn0RM.js +0 -1
- package/client/dist/assets/useMatureConsent-CT7-KfzG.js +0 -1
- package/client/dist/assets/useTv-B546u-Rx.js +0 -1
- package/client/dist/assets/useTvLibrary-C2rPeCVT.js +0 -1
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,
|
package/server/dist/server.js
CHANGED
|
@@ -10,9 +10,8 @@ import { serveStatic } from '@hono/node-server/serve-static';
|
|
|
10
10
|
import { getConnInfo } from '@hono/node-server/conninfo';
|
|
11
11
|
import logger from './logger.js';
|
|
12
12
|
import { notifyServerExit } from './lib/ipc.js';
|
|
13
|
-
import { githubSyncService } from './github-sync.js';
|
|
14
13
|
import { CONFIG } from './config.js';
|
|
15
|
-
import { initializeDatabase, initializeMangaDatabase, initializeTvDatabase, initializeAsmrDatabase,
|
|
14
|
+
import { initializeDatabase, initializeMangaDatabase, initializeTvDatabase, initializeAsmrDatabase, runFullSyncSequence, syncUp, waitForSync, KIND_SYNC, } from './sync.js';
|
|
16
15
|
import { loadRemoteProviders } from './providers/remote-loader.js';
|
|
17
16
|
import { discordRPCService } from './discord-rpc.js';
|
|
18
17
|
import { discordGatewayService } from './discord-gateway.js';
|
|
@@ -102,77 +101,8 @@ let asmrDb;
|
|
|
102
101
|
let isShuttingDown = false;
|
|
103
102
|
let bootSyncing = true;
|
|
104
103
|
async function runSyncSequence(database, mangaDatabase, preferredProvider) {
|
|
105
|
-
const dbName = CONFIG.IS_DEV ? CONFIG.DB_NAME_DEV : CONFIG.DB_NAME_PROD;
|
|
106
|
-
const dbPath = path.join(CONFIG.ROOT, dbName);
|
|
107
104
|
const remoteFolder = CONFIG.IS_DEV ? CONFIG.REMOTE_FOLDER_DEV : CONFIG.REMOTE_FOLDER_PROD;
|
|
108
|
-
await
|
|
109
|
-
if (getActiveProvider() === 'github' && githubSyncService.isAuthenticated()) {
|
|
110
|
-
try {
|
|
111
|
-
await githubSyncService.migrateFromAniWebSync();
|
|
112
|
-
}
|
|
113
|
-
catch (err) {
|
|
114
|
-
logger.error({ err }, 'GitHub sync migration from ani-web failed');
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
const didDownload = await syncDownOnBoot(database, dbPath, remoteFolder, () => {
|
|
118
|
-
return new Promise((resolve) => {
|
|
119
|
-
if (database && !database.isClosedCheck()) {
|
|
120
|
-
database.checkpoint();
|
|
121
|
-
database.close(() => resolve());
|
|
122
|
-
}
|
|
123
|
-
else {
|
|
124
|
-
resolve();
|
|
125
|
-
}
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
let currentDb = database;
|
|
129
|
-
if (didDownload) {
|
|
130
|
-
db = await initializeDatabase(dbPath);
|
|
131
|
-
currentDb = db;
|
|
132
|
-
logger.info('Database re-initialized after sync.');
|
|
133
|
-
}
|
|
134
|
-
try {
|
|
135
|
-
await syncUp(currentDb, dbPath, remoteFolder);
|
|
136
|
-
}
|
|
137
|
-
catch (err) {
|
|
138
|
-
logger.error({ err }, 'Sync up on boot failed');
|
|
139
|
-
}
|
|
140
|
-
try {
|
|
141
|
-
await mangaSyncDownOnBoot(mangaDatabase, remoteFolder);
|
|
142
|
-
}
|
|
143
|
-
catch (err) {
|
|
144
|
-
logger.error({ err }, 'Manga sync down on boot failed');
|
|
145
|
-
}
|
|
146
|
-
try {
|
|
147
|
-
await mangaSyncUp(mangaDatabase, remoteFolder);
|
|
148
|
-
}
|
|
149
|
-
catch (err) {
|
|
150
|
-
logger.error({ err }, 'Manga sync up on boot failed');
|
|
151
|
-
}
|
|
152
|
-
try {
|
|
153
|
-
await tvSyncDownOnBoot(tvDb, remoteFolder);
|
|
154
|
-
}
|
|
155
|
-
catch (err) {
|
|
156
|
-
logger.error({ err }, 'TV sync down on boot failed');
|
|
157
|
-
}
|
|
158
|
-
try {
|
|
159
|
-
await tvSyncUp(tvDb, remoteFolder);
|
|
160
|
-
}
|
|
161
|
-
catch (err) {
|
|
162
|
-
logger.error({ err }, 'TV sync up on boot failed');
|
|
163
|
-
}
|
|
164
|
-
try {
|
|
165
|
-
await asmrSyncDownOnBoot(asmrDb, remoteFolder);
|
|
166
|
-
}
|
|
167
|
-
catch (err) {
|
|
168
|
-
logger.error({ err }, 'ASMR sync down on boot failed');
|
|
169
|
-
}
|
|
170
|
-
try {
|
|
171
|
-
await asmrSyncUp(asmrDb, remoteFolder);
|
|
172
|
-
}
|
|
173
|
-
catch (err) {
|
|
174
|
-
logger.error({ err }, 'ASMR sync up on boot failed');
|
|
175
|
-
}
|
|
105
|
+
await runFullSyncSequence({ db: database, mangaDb: mangaDatabase, tvDb, asmrDb }, { remoteFolder, preferredProvider });
|
|
176
106
|
}
|
|
177
107
|
startWatchlistDiscovery(() => db);
|
|
178
108
|
if (!CONFIG.IS_DEV) {
|
|
@@ -228,42 +158,28 @@ async function main() {
|
|
|
228
158
|
refreshRemoteProviders().catch((err) => logger.error({ err }, 'periodic providers refresh failed'));
|
|
229
159
|
}, CONFIG.PROVIDER_REPO_REFRESH_MS).unref();
|
|
230
160
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
if (!fs.existsSync(CONFIG.TV_LOCAL_MANIFEST_PATH)) {
|
|
238
|
-
fs.writeFileSync(CONFIG.TV_LOCAL_MANIFEST_PATH, JSON.stringify({ version: 0 }));
|
|
239
|
-
}
|
|
240
|
-
if (!fs.existsSync(CONFIG.ASMR_LOCAL_MANIFEST_PATH)) {
|
|
241
|
-
fs.writeFileSync(CONFIG.ASMR_LOCAL_MANIFEST_PATH, JSON.stringify({ version: 0 }));
|
|
242
|
-
}
|
|
243
|
-
let hasUnsyncedChanges = false;
|
|
244
|
-
let hasMangaUnsyncedChanges = false;
|
|
245
|
-
let hasTvUnsyncedChanges = false;
|
|
246
|
-
let hasAsmrUnsyncedChanges = false;
|
|
247
|
-
const watcher = fs.watch(CONFIG.LOCAL_MANIFEST_PATH, (eventType) => {
|
|
248
|
-
if (eventType === 'change' || eventType === 'rename') {
|
|
249
|
-
hasUnsyncedChanges = true;
|
|
250
|
-
}
|
|
161
|
+
const kindDbs = () => ({
|
|
162
|
+
anime: db,
|
|
163
|
+
manga: mangaDb,
|
|
164
|
+
tv: tvDb,
|
|
165
|
+
asmr: asmrDb,
|
|
251
166
|
});
|
|
252
|
-
const
|
|
253
|
-
if (
|
|
254
|
-
|
|
167
|
+
for (const kind of Object.keys(KIND_SYNC)) {
|
|
168
|
+
if (!fs.existsSync(KIND_SYNC[kind].manifestPath)) {
|
|
169
|
+
fs.writeFileSync(KIND_SYNC[kind].manifestPath, JSON.stringify({ version: 0 }));
|
|
255
170
|
}
|
|
256
|
-
}
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
171
|
+
}
|
|
172
|
+
const unsynced = {
|
|
173
|
+
anime: false,
|
|
174
|
+
manga: false,
|
|
175
|
+
tv: false,
|
|
176
|
+
asmr: false,
|
|
177
|
+
};
|
|
178
|
+
const watchers = Object.keys(KIND_SYNC).map((kind) => fs.watch(KIND_SYNC[kind].manifestPath, (eventType) => {
|
|
263
179
|
if (eventType === 'change' || eventType === 'rename') {
|
|
264
|
-
|
|
180
|
+
unsynced[kind] = true;
|
|
265
181
|
}
|
|
266
|
-
});
|
|
182
|
+
}));
|
|
267
183
|
const server = serve({ fetch: app.fetch, port: CONFIG.PORT }, () => {
|
|
268
184
|
logger.info(`Server running on http://localhost:${CONFIG.PORT}`);
|
|
269
185
|
});
|
|
@@ -273,49 +189,24 @@ async function main() {
|
|
|
273
189
|
bootSyncing = false;
|
|
274
190
|
});
|
|
275
191
|
refreshRemoteProviders().catch((err) => logger.error({ err }, 'background providers refresh failed'));
|
|
192
|
+
const kindDbLabel = {
|
|
193
|
+
anime: 'database',
|
|
194
|
+
manga: 'manga database',
|
|
195
|
+
tv: 'TV database',
|
|
196
|
+
asmr: 'ASMR database',
|
|
197
|
+
};
|
|
276
198
|
const syncInterval = setInterval(async () => {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}
|
|
283
|
-
catch (err) {
|
|
284
|
-
logger.error({ err }, 'Failed to upload database changes');
|
|
285
|
-
hasUnsyncedChanges = true;
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
if (hasMangaUnsyncedChanges) {
|
|
289
|
-
logger.info('Uploading accumulated manga database changes...');
|
|
290
|
-
hasMangaUnsyncedChanges = false;
|
|
291
|
-
try {
|
|
292
|
-
await mangaSyncUp(mangaDb, remoteFolder);
|
|
293
|
-
}
|
|
294
|
-
catch (err) {
|
|
295
|
-
logger.error({ err }, 'Failed to upload manga database changes');
|
|
296
|
-
hasMangaUnsyncedChanges = true;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
if (hasTvUnsyncedChanges) {
|
|
300
|
-
logger.info('Uploading accumulated TV database changes...');
|
|
301
|
-
hasTvUnsyncedChanges = false;
|
|
199
|
+
for (const kind of Object.keys(KIND_SYNC)) {
|
|
200
|
+
if (!unsynced[kind])
|
|
201
|
+
continue;
|
|
202
|
+
logger.info(`Uploading accumulated ${kindDbLabel[kind]} changes...`);
|
|
203
|
+
unsynced[kind] = false;
|
|
302
204
|
try {
|
|
303
|
-
await
|
|
205
|
+
await syncUp(kindDbs()[kind], kind, remoteFolder);
|
|
304
206
|
}
|
|
305
207
|
catch (err) {
|
|
306
|
-
logger.error({ err },
|
|
307
|
-
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
if (hasAsmrUnsyncedChanges) {
|
|
311
|
-
logger.info('Uploading accumulated ASMR database changes...');
|
|
312
|
-
hasAsmrUnsyncedChanges = false;
|
|
313
|
-
try {
|
|
314
|
-
await asmrSyncUp(asmrDb, remoteFolder);
|
|
315
|
-
}
|
|
316
|
-
catch (err) {
|
|
317
|
-
logger.error({ err }, 'Failed to upload ASMR database changes');
|
|
318
|
-
hasAsmrUnsyncedChanges = true;
|
|
208
|
+
logger.error({ err }, `Failed to upload ${kindDbLabel[kind]} changes`);
|
|
209
|
+
unsynced[kind] = true;
|
|
319
210
|
}
|
|
320
211
|
}
|
|
321
212
|
}, 300000);
|
|
@@ -337,51 +228,28 @@ async function main() {
|
|
|
337
228
|
clearInterval(offlineDbInterval);
|
|
338
229
|
discordRPCService.disconnect();
|
|
339
230
|
discordGatewayService.shutdown();
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
await asmrWatcher.close();
|
|
231
|
+
for (const watcher of watchers) {
|
|
232
|
+
await watcher.close();
|
|
233
|
+
}
|
|
344
234
|
if (server) {
|
|
345
235
|
await new Promise((resolve) => server.close(() => resolve()));
|
|
346
236
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
logger.info('Sync on shutdown: uploading final manga database changes...');
|
|
359
|
-
hasMangaUnsyncedChanges = false;
|
|
360
|
-
try {
|
|
361
|
-
await mangaSyncUp(mangaDb, remoteFolder);
|
|
362
|
-
}
|
|
363
|
-
catch (e) {
|
|
364
|
-
logger.error({ err: e }, 'Final manga sync on shutdown failed');
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
if (hasTvUnsyncedChanges) {
|
|
368
|
-
logger.info('Sync on shutdown: uploading final TV database changes...');
|
|
369
|
-
hasTvUnsyncedChanges = false;
|
|
370
|
-
try {
|
|
371
|
-
await tvSyncUp(tvDb, remoteFolder);
|
|
372
|
-
}
|
|
373
|
-
catch (e) {
|
|
374
|
-
logger.error({ err: e }, 'Final TV sync on shutdown failed');
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
if (hasAsmrUnsyncedChanges) {
|
|
378
|
-
logger.info('Sync on shutdown: uploading final ASMR database changes...');
|
|
379
|
-
hasAsmrUnsyncedChanges = false;
|
|
237
|
+
const kindFinalError = {
|
|
238
|
+
anime: 'Final sync on shutdown failed',
|
|
239
|
+
manga: 'Final manga sync on shutdown failed',
|
|
240
|
+
tv: 'Final TV sync on shutdown failed',
|
|
241
|
+
asmr: 'Final ASMR sync on shutdown failed',
|
|
242
|
+
};
|
|
243
|
+
for (const kind of Object.keys(KIND_SYNC)) {
|
|
244
|
+
if (!unsynced[kind])
|
|
245
|
+
continue;
|
|
246
|
+
logger.info(`Sync on shutdown: uploading final ${kindDbLabel[kind]} changes...`);
|
|
247
|
+
unsynced[kind] = false;
|
|
380
248
|
try {
|
|
381
|
-
await
|
|
249
|
+
await syncUp(kindDbs()[kind], kind, remoteFolder);
|
|
382
250
|
}
|
|
383
251
|
catch (e) {
|
|
384
|
-
logger.error({ err: e },
|
|
252
|
+
logger.error({ err: e }, kindFinalError[kind]);
|
|
385
253
|
}
|
|
386
254
|
}
|
|
387
255
|
await waitForSync();
|