@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.
- package/README.md +1 -1
- package/client/dist/assets/{AnimeCard-4CEz8Buu.js → AnimeCard-BVdH0EeW.js} +1 -1
- package/client/dist/assets/{AnimeInfo-DyX9qTwg.js → AnimeInfo-Dw2Nop6Q.js} +1 -1
- package/client/dist/assets/{AnimeInfoPage-Dvp_c8vK.js → AnimeInfoPage-Cw-Tq3YX.js} +2 -2
- package/client/dist/assets/{Asmr-qg3lMAJn.js → Asmr-BrjzzyOi.js} +1 -1
- package/client/dist/assets/{AsmrResetProgressModal-BpPlg_Vv.js → AsmrResetProgressModal-CYjpcYpb.js} +1 -1
- package/client/dist/assets/{AvSyncCalibrator-FJllOgOH.js → AvSyncCalibrator-BuHurVgo.js} +3 -3
- package/client/dist/assets/AvSyncCalibrator-D0FYSwiz.css +1 -0
- package/client/dist/assets/{Home-A7gCeEST.js → Home-KdnP7sNM.js} +1 -1
- package/client/dist/assets/{Insights-G9IXepHJ.js → Insights-DDWry-t1.js} +1 -1
- package/client/dist/assets/{LibraryListPage-Bh2qCJPh.js → LibraryListPage-CV8Z9JlO.js} +1 -1
- package/client/dist/assets/{ListeningList-Czv4cTRl.js → ListeningList-DSMbMZT7.js} +1 -1
- package/client/dist/assets/{Manga-qdAAS1z7.js → Manga-J6XoqEKq.js} +1 -1
- package/client/dist/assets/{MangaInfoPage-1rTgZzf2.js → MangaInfoPage-N7TxsN5u.js} +1 -1
- package/client/dist/assets/{MangaReadPage-CeyxPG6w.js → MangaReadPage-D_VzSNWq.js} +1 -1
- package/client/dist/assets/{MangaResetProgressModal-C_ppotmh.js → MangaResetProgressModal-DyhQYBDy.js} +1 -1
- package/client/dist/assets/{Mature-Bkhxnr3K.js → Mature-Db53R-LR.js} +1 -1
- package/client/dist/assets/{MediaCard-BQZsutEo.js → MediaCard-BcLjlpfR.js} +1 -1
- package/client/dist/assets/{Music-C0rGzSbi.js → Music-DjxQjJOl.js} +1 -1
- package/client/dist/assets/{OptionListMenu-CyfpCBEy.js → OptionListMenu-DngTgHxH.js} +1 -1
- package/client/dist/assets/Player-Be_7Ne5p.js +56 -0
- package/client/dist/assets/{PlayerSettings-icy4SfQa.js → PlayerSettings-D47ItGDR.js} +1 -1
- package/client/dist/assets/{QueueOptionsButton-5eloKF5J.js → QueueOptionsButton-ClVTpH8J.js} +1 -1
- package/client/dist/assets/{QueueRail-DaMjlKL4.js → QueueRail-DeJKVRs5.js} +1 -1
- package/client/dist/assets/{Radio-FxGqCxR4.js → Radio-DU0ma3GA.js} +1 -1
- package/client/dist/assets/{ReadingList-CCm3IAbt.js → ReadingList-qXW7Dk5z.js} +1 -1
- package/client/dist/assets/{ResetProgressModal-CeI4V3Y5.js → ResetProgressModal-C6ukct_c.js} +1 -1
- package/client/dist/assets/{Search-qpILaAiQ.js → Search-DjjKA_Wu.js} +1 -1
- package/client/dist/assets/{Settings-Bw9M3NGz.js → Settings-DQt_S5Qw.js} +1 -1
- package/client/dist/assets/{SkeletonGrid-0dY_xMq3.js → SkeletonGrid-nx2HEi4P.js} +1 -1
- package/client/dist/assets/{Trackers-Zy5GOs0P.js → Trackers-BcGzeqj2.js} +1 -1
- package/client/dist/assets/Tv-COvyHdeJ.js +4 -0
- package/client/dist/assets/{TvInfo-CHIsVzZ0.js → TvInfo-CUd1agFy.js} +1 -1
- package/client/dist/assets/{TvPopup-D_tVTyZ2.js → TvPopup-CJeu3tgc.js} +1 -1
- package/client/dist/assets/{TvSearch-BVxedBW-.js → TvSearch-BIypjbU3.js} +1 -1
- package/client/dist/assets/{TvWatchlist-BEIwq9R9.js → TvWatchlist-DZrBdiqZ.js} +1 -1
- package/client/dist/assets/{Watchlist-CSngjqlz.js → Watchlist-Dw8cPe0n.js} +1 -1
- package/client/dist/assets/{hls-D48GVTkk.js → hls-B3_1VuDP.js} +1 -1
- package/client/dist/assets/{index-CVB5dYhT.js → index-gMxawd-E.js} +2 -2
- package/client/dist/assets/{useAnimeInfoData-DxdOg_B7.js → useAnimeInfoData-_iOLEAa8.js} +1 -1
- package/client/dist/assets/{useAsmr-ClckxnPe.js → useAsmr-Blk4v41t.js} +1 -1
- package/client/dist/assets/{useAsmrPopup-Bbgxefy_.js → useAsmrPopup-D9SQIgZa.js} +1 -1
- package/client/dist/assets/{useMangaLibrary-B3_BUKVk.js → useMangaLibrary-B60at6ni.js} +1 -1
- package/client/dist/assets/{useMangaPopup-Celk4lzf.js → useMangaPopup-DEfGjWSu.js} +1 -1
- package/client/dist/assets/{useProviders-pwxPZPHh.js → useProviders-Ca0bZWt7.js} +1 -1
- package/client/dist/assets/{useTv-D9sQrDop.js → useTv-WJCDL5_5.js} +1 -1
- package/client/dist/assets/{useTvLibrary-UsQpgBTB.js → useTvLibrary-Fgn5i9rJ.js} +1 -1
- package/client/dist/index.html +1 -1
- package/package.json +4 -2
- package/server/dist/app-hono.js +2 -0
- package/server/dist/hono/asmr.js +10 -1
- package/server/dist/hono/music.js +122 -332
- package/server/dist/hono/proxy.js +105 -99
- package/server/dist/hono/tv.js +5 -58
- package/server/dist/hono/watchlist.js +2 -291
- package/server/dist/lib/anilist.js +0 -3
- package/server/dist/lib/mal.js +4 -41
- package/server/dist/lib/offline-db.js +0 -6
- package/server/dist/lib/potoken.js +135 -0
- package/server/dist/lib/tracker/manga-sync.service.js +3 -22
- package/server/dist/lib/tracker/sync-state.js +21 -0
- package/server/dist/lib/tracker/sync.service.js +3 -22
- package/server/dist/lib/ump.js +106 -0
- package/server/dist/providers/remote-loader.js +17 -27
- package/server/dist/providers/title-matching.js +5 -5
- package/server/dist/repositories/asmr.repository.js +4 -53
- package/server/dist/repositories/library.repository.js +46 -0
- package/server/dist/repositories/manga.repository.js +3 -53
- package/server/dist/repositories/settings.repository.js +8 -0
- package/server/dist/repositories/tv.repository.js +3 -46
- package/server/dist/repositories/watched-episodes.repository.js +0 -2
- package/server/dist/server.js +5 -1
- package/server/dist/sync.js +3 -5
- package/server/dist/trpc/routers/asmr-library.js +2 -12
- package/server/dist/trpc/routers/asmr-progress.js +10 -22
- package/server/dist/trpc/routers/data.js +17 -36
- package/server/dist/trpc/routers/discord.js +1 -8
- package/server/dist/trpc/routers/insights.js +15 -16
- package/server/dist/trpc/routers/manga-library.js +5 -22
- package/server/dist/trpc/routers/manga-progress.js +8 -20
- package/server/dist/trpc/routers/music.js +6 -11
- package/server/dist/trpc/routers/queue.js +1 -1
- package/server/dist/trpc/routers/tracker.js +1 -7
- package/server/dist/trpc/routers/tv-library.js +2 -12
- package/server/dist/trpc/routers/tv-progress.js +16 -28
- package/server/dist/trpc/routers/tv.js +5 -21
- package/server/dist/trpc/routers/watchlist.js +1 -8
- package/server/dist/trpc/validation.js +35 -0
- package/server/dist/utils/db-utils.js +8 -0
- package/server/dist/utils/http.utils.js +12 -0
- package/server/dist/utils/machine-id.js +1 -2
- package/server/dist/utils/megaplay.utils.js +1 -2
- package/server/package.json +4 -1
- package/client/dist/assets/AvSyncCalibrator-C2wu1ydI.css +0 -1
- package/client/dist/assets/Player-yhiTlpYU.js +0 -56
- package/client/dist/assets/Tv-DREwc7BX.js +0 -4
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import logger from '../logger.js';
|
|
2
2
|
import { WatchlistRepository } from '../repositories/watchlist.repository.js';
|
|
3
|
-
import { WatchedEpisodesRepository
|
|
3
|
+
import { WatchedEpisodesRepository } from '../repositories/watched-episodes.repository.js';
|
|
4
4
|
import { ShowsMetaRepository } from '../repositories/shows-meta.repository.js';
|
|
5
5
|
import { NotificationsRepository } from '../repositories/notifications.repository.js';
|
|
6
|
-
import { SettingsRepository } from '../repositories/settings.repository.js';
|
|
7
6
|
import { dbAll } from '../utils/db-utils.js';
|
|
8
7
|
import { AppCache } from '../utils/cache.utils.js';
|
|
9
|
-
import {
|
|
10
|
-
import { offlineDb } from '../lib/offline-db.js';
|
|
8
|
+
import { getAiredEpisodesForShows, isAnilistRateLimited, batchGetShowStatuses, } from '../lib/anilist.js';
|
|
11
9
|
import { parseJsonBody } from '../utils/http.utils.js';
|
|
12
10
|
import { pickBestMatch } from '../providers/title-matching.js';
|
|
13
11
|
const BACKGROUND_DISCOVERY_INTERVAL_MS = 5 * 60 * 1000;
|
|
@@ -258,286 +256,6 @@ export function startWatchlistDiscovery(getDb) {
|
|
|
258
256
|
runDiscovery(false);
|
|
259
257
|
}, BACKGROUND_DISCOVERY_INTERVAL_MS);
|
|
260
258
|
}
|
|
261
|
-
async function showsMetaChanged(db, showId, candidate) {
|
|
262
|
-
const existing = (await ShowsMetaRepository.getById(db, showId));
|
|
263
|
-
if (!existing)
|
|
264
|
-
return true;
|
|
265
|
-
const differs = (incoming, stored) => {
|
|
266
|
-
if (incoming === undefined || incoming === null || incoming === '')
|
|
267
|
-
return false;
|
|
268
|
-
return String(incoming) !== String(stored ?? '');
|
|
269
|
-
};
|
|
270
|
-
return (differs(candidate.name, existing.name) ||
|
|
271
|
-
differs(candidate.thumbnail, existing.thumbnail) ||
|
|
272
|
-
differs(candidate.nativeName, existing.nativeName) ||
|
|
273
|
-
differs(candidate.englishName, existing.englishName) ||
|
|
274
|
-
differs(candidate.genres, existing.genres) ||
|
|
275
|
-
differs(candidate.popularityScore, existing.popularityScore) ||
|
|
276
|
-
differs(candidate.status, existing.status) ||
|
|
277
|
-
differs(candidate.episodeCount, existing.episodeCount) ||
|
|
278
|
-
differs(candidate.type, existing.type) ||
|
|
279
|
-
differs(candidate.anilistId, existing.anilistId) ||
|
|
280
|
-
differs(candidate.isAdult, existing.isAdult) ||
|
|
281
|
-
differs(candidate.episodeDuration, existing.episodeDuration));
|
|
282
|
-
}
|
|
283
|
-
function normalizeFilterValue(value) {
|
|
284
|
-
if (typeof value !== 'string')
|
|
285
|
-
return undefined;
|
|
286
|
-
const trimmed = value.trim();
|
|
287
|
-
return trimmed && trimmed !== 'ALL' ? trimmed : undefined;
|
|
288
|
-
}
|
|
289
|
-
function getWatchlistFilters(query) {
|
|
290
|
-
return {
|
|
291
|
-
query: normalizeFilterValue(query['query']),
|
|
292
|
-
type: normalizeFilterValue(query['type']),
|
|
293
|
-
season: normalizeFilterValue(query['season']),
|
|
294
|
-
year: normalizeFilterValue(query['year']),
|
|
295
|
-
genres: normalizeFilterValue(query['genres']),
|
|
296
|
-
excludeGenres: normalizeFilterValue(query['excludeGenres']),
|
|
297
|
-
sortBy: normalizeFilterValue(query['sortBy']),
|
|
298
|
-
titlePreference: ['name', 'nativeName', 'englishName'].includes(String(query['titlePreference']))
|
|
299
|
-
? query['titlePreference']
|
|
300
|
-
: 'name',
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
function matchesLocalFilters(row, filters) {
|
|
304
|
-
if (filters.query) {
|
|
305
|
-
const queryWords = new Set(filters.query
|
|
306
|
-
.toLowerCase()
|
|
307
|
-
.split(/\s+/)
|
|
308
|
-
.filter((w) => w.length >= 2));
|
|
309
|
-
const rowTitle = (row.englishName || row.name || row.nativeName || '').toLowerCase();
|
|
310
|
-
const titleWords = rowTitle.split(/\s+/);
|
|
311
|
-
const overlap = titleWords.filter((w) => queryWords.has(w)).length;
|
|
312
|
-
if (overlap < queryWords.size)
|
|
313
|
-
return false;
|
|
314
|
-
}
|
|
315
|
-
return true;
|
|
316
|
-
}
|
|
317
|
-
function sortFilteredRows(rows, filters) {
|
|
318
|
-
const getSortTitle = (row) => {
|
|
319
|
-
const preferredTitle = filters.titlePreference ? row[filters.titlePreference] : undefined;
|
|
320
|
-
return preferredTitle || row.name || '';
|
|
321
|
-
};
|
|
322
|
-
if (filters.sortBy === 'name_asc') {
|
|
323
|
-
return [...rows].sort((a, b) => getSortTitle(a).localeCompare(getSortTitle(b)));
|
|
324
|
-
}
|
|
325
|
-
if (filters.sortBy === 'name_desc') {
|
|
326
|
-
return [...rows].sort((a, b) => getSortTitle(b).localeCompare(getSortTitle(a)));
|
|
327
|
-
}
|
|
328
|
-
return rows;
|
|
329
|
-
}
|
|
330
|
-
async function getAnilistSeasonYearMatches(season, year) {
|
|
331
|
-
const matched = new Map();
|
|
332
|
-
if (!year || year === 'ALL') {
|
|
333
|
-
return matched;
|
|
334
|
-
}
|
|
335
|
-
const seasonYear = parseInt(year);
|
|
336
|
-
if (Number.isNaN(seasonYear))
|
|
337
|
-
return matched;
|
|
338
|
-
const perPage = 50;
|
|
339
|
-
let page = 1;
|
|
340
|
-
while (true) {
|
|
341
|
-
const searchVars = {
|
|
342
|
-
seasonYear,
|
|
343
|
-
page,
|
|
344
|
-
perPage,
|
|
345
|
-
};
|
|
346
|
-
if (season && season !== 'ALL') {
|
|
347
|
-
searchVars.season = season.toUpperCase();
|
|
348
|
-
}
|
|
349
|
-
const results = await searchAnilist(searchVars);
|
|
350
|
-
for (const show of results) {
|
|
351
|
-
if (show.anilistId) {
|
|
352
|
-
matched.set(show.anilistId, { title: show.names || {} });
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
if (results.length < perPage)
|
|
356
|
-
break;
|
|
357
|
-
page++;
|
|
358
|
-
if (page > 10)
|
|
359
|
-
break;
|
|
360
|
-
}
|
|
361
|
-
return matched;
|
|
362
|
-
}
|
|
363
|
-
function rowMatchesAnilistSeasonYear(row, anilistMatches) {
|
|
364
|
-
if (anilistMatches.size === 0)
|
|
365
|
-
return true;
|
|
366
|
-
if (/^\d+$/.test(row.id) && anilistMatches.has(parseInt(row.id))) {
|
|
367
|
-
return true;
|
|
368
|
-
}
|
|
369
|
-
const rowTitle = (row.englishName || row.name || row.nativeName || '').toLowerCase();
|
|
370
|
-
if (!rowTitle)
|
|
371
|
-
return false;
|
|
372
|
-
const rowWords = new Set(rowTitle.split(/\s+/).filter((w) => w.length >= 2));
|
|
373
|
-
if (rowWords.size === 0)
|
|
374
|
-
return false;
|
|
375
|
-
for (const [, media] of anilistMatches) {
|
|
376
|
-
const titles = [media.title?.romaji, media.title?.english, media.title?.native].filter(Boolean);
|
|
377
|
-
for (const title of titles) {
|
|
378
|
-
const titleWords = new Set(title
|
|
379
|
-
.toLowerCase()
|
|
380
|
-
.split(/\s+/)
|
|
381
|
-
.filter((w) => w.length >= 2));
|
|
382
|
-
const overlap = [...rowWords].filter((w) => titleWords.has(w)).length;
|
|
383
|
-
const minLen = Math.min(rowWords.size, titleWords.size);
|
|
384
|
-
if (minLen >= 2 && overlap / minLen >= 0.7)
|
|
385
|
-
return true;
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
return false;
|
|
389
|
-
}
|
|
390
|
-
async function filterWatchlistRows(rows, filters, db) {
|
|
391
|
-
let filtered = rows.filter((row) => matchesLocalFilters(row, filters));
|
|
392
|
-
if (filters.type === 'ADULT' && db && filtered.length > 0) {
|
|
393
|
-
const ids = filtered.map((r) => r.id);
|
|
394
|
-
const placeholders = ids.map(() => '?').join(',');
|
|
395
|
-
const adultRows = await dbAll(db, `SELECT id FROM shows_meta WHERE id IN (${placeholders}) AND isAdult = 1`, ids);
|
|
396
|
-
const adultSet = new Set(adultRows.map((r) => r.id));
|
|
397
|
-
filtered = filtered.filter((row) => adultSet.has(row.id));
|
|
398
|
-
}
|
|
399
|
-
else if (filters.type && filters.type !== 'ALL' && filters.type !== 'ADULT') {
|
|
400
|
-
filtered = filtered.filter((row) => row.type === filters.type);
|
|
401
|
-
}
|
|
402
|
-
if ((filters.genres || filters.excludeGenres) && db) {
|
|
403
|
-
const ids = filtered.map((r) => r.id);
|
|
404
|
-
const placeholders = ids.map(() => '?').join(',');
|
|
405
|
-
const genreRows = await dbAll(db, `SELECT id, genres FROM shows_meta WHERE id IN (${placeholders})`, ids);
|
|
406
|
-
const idToGenres = new Map(genreRows.map((r) => [r.id, r.genres ? JSON.parse(r.genres) : []]));
|
|
407
|
-
const includeList = filters.genres
|
|
408
|
-
?.split(',')
|
|
409
|
-
.map((g) => g.trim())
|
|
410
|
-
.filter(Boolean) || [];
|
|
411
|
-
const excludeList = filters.excludeGenres
|
|
412
|
-
?.split(',')
|
|
413
|
-
.map((g) => g.trim())
|
|
414
|
-
.filter(Boolean) || [];
|
|
415
|
-
filtered = filtered.filter((row) => {
|
|
416
|
-
const rowGenres = idToGenres.get(row.id) || [];
|
|
417
|
-
if (includeList.length && !includeList.every((g) => rowGenres.includes(g)))
|
|
418
|
-
return false;
|
|
419
|
-
if (excludeList.length && excludeList.some((g) => rowGenres.includes(g)))
|
|
420
|
-
return false;
|
|
421
|
-
return true;
|
|
422
|
-
});
|
|
423
|
-
}
|
|
424
|
-
if (((filters.season && filters.season !== 'ALL') || (filters.year && filters.year !== 'ALL')) &&
|
|
425
|
-
filtered.length > 0) {
|
|
426
|
-
const anilistMatches = await getAnilistSeasonYearMatches(filters.season, filters.year);
|
|
427
|
-
if (anilistMatches.size > 0) {
|
|
428
|
-
filtered = filtered.filter((row) => rowMatchesAnilistSeasonYear(row, anilistMatches));
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
return sortFilteredRows(filtered, filters);
|
|
432
|
-
}
|
|
433
|
-
async function backfillMissingPosters(db, rows) {
|
|
434
|
-
const missing = rows.filter((r) => !r.thumbnail || r.thumbnail.trim() === '');
|
|
435
|
-
if (missing.length === 0)
|
|
436
|
-
return;
|
|
437
|
-
const slice = missing.slice(0, 10);
|
|
438
|
-
await Promise.allSettled(slice.map(async (row) => {
|
|
439
|
-
try {
|
|
440
|
-
const meta = await getShowMetaById(row.id);
|
|
441
|
-
const poster = meta?.thumbnail?.trim();
|
|
442
|
-
if (!poster)
|
|
443
|
-
return;
|
|
444
|
-
row.thumbnail = poster;
|
|
445
|
-
const dual = row;
|
|
446
|
-
if (dual._id !== undefined)
|
|
447
|
-
dual._id = row.id;
|
|
448
|
-
try {
|
|
449
|
-
await ShowsMetaRepository.upsert(db, {
|
|
450
|
-
id: row.id,
|
|
451
|
-
thumbnail: poster,
|
|
452
|
-
popularityScore: meta?.score ?? (meta?.averageScore != null ? meta.averageScore / 10 : undefined),
|
|
453
|
-
});
|
|
454
|
-
}
|
|
455
|
-
catch {
|
|
456
|
-
// ignore
|
|
457
|
-
}
|
|
458
|
-
try {
|
|
459
|
-
await WatchlistRepository.updateThumbnail(db, row.id, poster);
|
|
460
|
-
}
|
|
461
|
-
catch {
|
|
462
|
-
// ignore
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
catch {
|
|
466
|
-
// ignore
|
|
467
|
-
}
|
|
468
|
-
}));
|
|
469
|
-
await sweepOfflinePosters(db);
|
|
470
|
-
}
|
|
471
|
-
async function sweepOfflinePosters(db) {
|
|
472
|
-
try {
|
|
473
|
-
db.run(`UPDATE shows_meta SET name = (SELECT w.name FROM watchlist w WHERE w.id = shows_meta.id)
|
|
474
|
-
WHERE (name IS NULL OR TRIM(name) = '')
|
|
475
|
-
AND EXISTS (SELECT 1 FROM watchlist w WHERE w.id = shows_meta.id AND w.name IS NOT NULL AND TRIM(w.name) != '')`);
|
|
476
|
-
}
|
|
477
|
-
catch {
|
|
478
|
-
// ignore
|
|
479
|
-
}
|
|
480
|
-
const [wlMissing, metaMissing] = await Promise.all([
|
|
481
|
-
WatchlistRepository.getMissingThumbnails(db),
|
|
482
|
-
ShowsMetaRepository.getShowIdsMissingMeta(db),
|
|
483
|
-
]);
|
|
484
|
-
const ids = [...new Set([...wlMissing.map((r) => r.id), ...metaMissing.map((r) => r.id)])].slice(0, 500);
|
|
485
|
-
if (ids.length === 0)
|
|
486
|
-
return;
|
|
487
|
-
for (const id of ids) {
|
|
488
|
-
const poster = resolveOfflinePoster(id);
|
|
489
|
-
if (poster)
|
|
490
|
-
await applyPoster(db, id, poster);
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
function resolveOfflinePoster(id) {
|
|
494
|
-
if (/^\d+$/.test(id)) {
|
|
495
|
-
return offlineDb.getByAnilistId(Number(id))?.thumbnail?.trim() || null;
|
|
496
|
-
}
|
|
497
|
-
const mal = /^mal-(\d+)$/i.exec(id)?.[1];
|
|
498
|
-
if (mal)
|
|
499
|
-
return offlineDb.getByMalId(Number(mal))?.thumbnail?.trim() || null;
|
|
500
|
-
return null;
|
|
501
|
-
}
|
|
502
|
-
async function applyPoster(db, id, poster) {
|
|
503
|
-
try {
|
|
504
|
-
await ShowsMetaRepository.upsert(db, { id, thumbnail: poster });
|
|
505
|
-
}
|
|
506
|
-
catch {
|
|
507
|
-
// ignore
|
|
508
|
-
}
|
|
509
|
-
try {
|
|
510
|
-
await WatchlistRepository.updateThumbnail(db, id, poster);
|
|
511
|
-
}
|
|
512
|
-
catch {
|
|
513
|
-
// ignore
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
async function getContinueWatchingData(db, limit) {
|
|
517
|
-
const rows = await WatchedEpisodesRepository.getContinueWatching(db, limit);
|
|
518
|
-
const ignoreAdultRow = await SettingsRepository.getByKey(db, 'ignoreAdultContent');
|
|
519
|
-
const ignoreAdult = ignoreAdultRow ? ignoreAdultRow.value !== 'false' : true;
|
|
520
|
-
const watchlistOnlyRow = await SettingsRepository.getByKey(db, 'cwWatchlistOnly');
|
|
521
|
-
const watchlistOnly = watchlistOnlyRow
|
|
522
|
-
? watchlistOnlyRow.value === 'true' || watchlistOnlyRow.value === '1'
|
|
523
|
-
: false;
|
|
524
|
-
const enrichedRows = rows
|
|
525
|
-
.filter((show) => {
|
|
526
|
-
if (ignoreAdult && show.isAdult)
|
|
527
|
-
return false;
|
|
528
|
-
if (watchlistOnly && show.watchlistStatus !== 'Watching')
|
|
529
|
-
return false;
|
|
530
|
-
return true;
|
|
531
|
-
})
|
|
532
|
-
.map((show) => ({
|
|
533
|
-
...show,
|
|
534
|
-
episodeCount: show.episodeCount,
|
|
535
|
-
type: show.type || show.smType,
|
|
536
|
-
thumbnail: show.thumbnail ?? '',
|
|
537
|
-
}));
|
|
538
|
-
await backfillMissingPosters(db, enrichedRows);
|
|
539
|
-
return enrichedRows;
|
|
540
|
-
}
|
|
541
259
|
export async function getAdultNonWatchlistShowIds(db) {
|
|
542
260
|
const rows = await dbAll(db, `SELECT DISTINCT we.showId as showId
|
|
543
261
|
FROM watched_episodes we
|
|
@@ -546,13 +264,6 @@ export async function getAdultNonWatchlistShowIds(db) {
|
|
|
546
264
|
WHERE w.id IS NULL`);
|
|
547
265
|
return rows.map((r) => r.showId);
|
|
548
266
|
}
|
|
549
|
-
async function resolveAvailableEpisodes(db, showId) {
|
|
550
|
-
const episodeData = await getAnilistEpisodes(showId);
|
|
551
|
-
const episodes = Array.isArray(episodeData) && episodeData.length
|
|
552
|
-
? [...episodeData].sort((a, b) => parseFloat(a) - parseFloat(b))
|
|
553
|
-
: [];
|
|
554
|
-
return episodes;
|
|
555
|
-
}
|
|
556
267
|
const dlsitePosterCache = new AppCache({ ttlSeconds: 3600, maxKeys: 2000 });
|
|
557
268
|
const mangadexCoverCache = new AppCache({ ttlSeconds: 3600, maxKeys: 2000 });
|
|
558
269
|
export async function getMangadexCover(title) {
|
|
@@ -21,9 +21,6 @@ export function anilistUnavailable() {
|
|
|
21
21
|
}
|
|
22
22
|
return false;
|
|
23
23
|
}
|
|
24
|
-
export function anilistIsDown() {
|
|
25
|
-
return anilistDownUntil > Date.now();
|
|
26
|
-
}
|
|
27
24
|
export function isAnilistRateLimited() {
|
|
28
25
|
return anilistRateLimited && anilistCooldownUntil > Date.now();
|
|
29
26
|
}
|
package/server/dist/lib/mal.js
CHANGED
|
@@ -153,44 +153,6 @@ function detailToSearchEntry(d) {
|
|
|
153
153
|
score: d.score,
|
|
154
154
|
};
|
|
155
155
|
}
|
|
156
|
-
export function toSearchEntry(e) {
|
|
157
|
-
if (!e || !e.malId || !e.title)
|
|
158
|
-
return null;
|
|
159
|
-
return {
|
|
160
|
-
id: -e.malId,
|
|
161
|
-
idMal: e.malId,
|
|
162
|
-
url: e.url ?? `https://myanimelist.net/anime/${e.malId}`,
|
|
163
|
-
title: e.title,
|
|
164
|
-
imageUrl: e.imageUrl ?? null,
|
|
165
|
-
synopsis: e.synopsis ?? null,
|
|
166
|
-
type: e.type ?? null,
|
|
167
|
-
episodes: e.episodes ?? null,
|
|
168
|
-
score: e.score ?? null,
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
export function toDetail(e, malId) {
|
|
172
|
-
if (!e || !e.title)
|
|
173
|
-
return null;
|
|
174
|
-
const genres = (Array.isArray(e.genres) ? e.genres : [])
|
|
175
|
-
.map((g) => (typeof g === 'string' ? g : g?.name))
|
|
176
|
-
.filter((g) => !!g);
|
|
177
|
-
return {
|
|
178
|
-
id: -malId,
|
|
179
|
-
idMal: malId,
|
|
180
|
-
url: e.url ?? `https://myanimelist.net/anime/${malId}`,
|
|
181
|
-
title: e.title,
|
|
182
|
-
titleEnglish: e.titleEnglish ?? null,
|
|
183
|
-
titleJapanese: e.titleJapanese ?? null,
|
|
184
|
-
imageUrl: e.imageUrl ?? e.images?.medium ?? null,
|
|
185
|
-
synopsis: e.synopsis ?? null,
|
|
186
|
-
type: e.type ?? null,
|
|
187
|
-
episodes: e.episodes ?? null,
|
|
188
|
-
status: e.status ?? null,
|
|
189
|
-
score: e.score ?? null,
|
|
190
|
-
rating: e.rating ?? null,
|
|
191
|
-
genres,
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
156
|
function mapMalStatus(status) {
|
|
195
157
|
if (!status)
|
|
196
158
|
return undefined;
|
|
@@ -603,9 +565,10 @@ export async function enrichCardTitles(store, items) {
|
|
|
603
565
|
export async function malSearchMedia(store, options = {}, enrichTitles = true) {
|
|
604
566
|
const { query, page = 1, format, status, season, seasonYear, genre, genre_in, genre_not_in, averageScore_greater, episodes_greater, isAdult, sort, } = options;
|
|
605
567
|
const params = new URLSearchParams();
|
|
606
|
-
const
|
|
607
|
-
|
|
608
|
-
|
|
568
|
+
const trimmedQuery = query?.trim();
|
|
569
|
+
const hasQuery = !!trimmedQuery;
|
|
570
|
+
if (trimmedQuery)
|
|
571
|
+
params.set('q', trimmedQuery);
|
|
609
572
|
params.set('cat', 'anime');
|
|
610
573
|
if (page > 1)
|
|
611
574
|
params.set('show', String((page - 1) * 50));
|
|
@@ -208,12 +208,6 @@ class OfflineDb {
|
|
|
208
208
|
getByAnilistId(anilistId) {
|
|
209
209
|
return this.anilistToEntry.get(anilistId) ?? null;
|
|
210
210
|
}
|
|
211
|
-
getAnilistIdByMal(malId) {
|
|
212
|
-
return this.malToEntry.get(malId)?.anilistId ?? null;
|
|
213
|
-
}
|
|
214
|
-
getMalIdByAnilist(anilistId) {
|
|
215
|
-
return this.anilistToMal.get(anilistId) ?? null;
|
|
216
|
-
}
|
|
217
211
|
async getOfflineDbInfo(db) {
|
|
218
212
|
const get = async (key) => (await SettingsRepository.getByKey(db, key))?.value ?? null;
|
|
219
213
|
const autoUpdate = await get('offlineDbAutoUpdateEnabled');
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import logger from '../logger.js';
|
|
2
|
+
import { parseJsonBody } from '../utils/http.utils.js';
|
|
3
|
+
import { getInnertube } from './ytmusic.js';
|
|
4
|
+
const REQUEST_KEY = 'O43z0dpjhgX20SCx4KAo';
|
|
5
|
+
const INTEGRITY_TTL_MS = 10 * 60 * 60 * 1000;
|
|
6
|
+
const POT_CACHE_MS = 6 * 60 * 60 * 1000;
|
|
7
|
+
let minter = null;
|
|
8
|
+
let minterFetchedAt = 0;
|
|
9
|
+
let bootstrapPromise = null;
|
|
10
|
+
let bootstrapFailedAt = 0;
|
|
11
|
+
const BOOTSTRAP_FAIL_COOLDOWN_MS = 60 * 1000;
|
|
12
|
+
const potCache = new Map();
|
|
13
|
+
// BotGuard needs browser globals to execute. They are installed once and kept
|
|
14
|
+
// for the process lifetime: the minter's callbacks read them lazily at mint
|
|
15
|
+
// time, long after bootstrap completes, so scoping them to bootstrap breaks
|
|
16
|
+
// minting with `window is not defined`.
|
|
17
|
+
async function setupDom() {
|
|
18
|
+
const g = globalThis;
|
|
19
|
+
if ('navigator' in g && g.yt)
|
|
20
|
+
return;
|
|
21
|
+
const { JSDOM, VirtualConsole } = await import('jsdom');
|
|
22
|
+
const virtualConsole = new VirtualConsole();
|
|
23
|
+
const dom = new JSDOM('<!DOCTYPE html><html lang="en"><head><title></title></head><body></body></html>', {
|
|
24
|
+
url: 'https://www.youtube.com',
|
|
25
|
+
referrer: 'https://www.youtube.com/',
|
|
26
|
+
virtualConsole,
|
|
27
|
+
});
|
|
28
|
+
Object.assign(g, {
|
|
29
|
+
window: dom.window,
|
|
30
|
+
document: dom.window.document,
|
|
31
|
+
location: dom.window.location,
|
|
32
|
+
origin: dom.window.origin,
|
|
33
|
+
});
|
|
34
|
+
if (!('navigator' in g)) {
|
|
35
|
+
Object.defineProperty(g, 'navigator', {
|
|
36
|
+
value: dom.window.navigator,
|
|
37
|
+
writable: true,
|
|
38
|
+
configurable: true,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function buildMinter() {
|
|
43
|
+
const [{ BotGuardClient }, { WebPoMinter }, { buildURL, getHeaders, USER_AGENT }] = await Promise.all([
|
|
44
|
+
import('bgutils-js/botguard'),
|
|
45
|
+
import('bgutils-js/webpo'),
|
|
46
|
+
import('bgutils-js/utils'),
|
|
47
|
+
]);
|
|
48
|
+
const yt = await getInnertube();
|
|
49
|
+
const pageResponse = await fetch('https://www.youtube.com', {
|
|
50
|
+
headers: {
|
|
51
|
+
accept: '*/*',
|
|
52
|
+
'accept-language': 'en-US,en;q=0.7',
|
|
53
|
+
'user-agent': USER_AGENT,
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
const pageHtml = await pageResponse.text();
|
|
57
|
+
const ytConfig = pageHtml.match(/ytcfg\.set\(({.+?})\);/s)?.[1];
|
|
58
|
+
if (ytConfig) {
|
|
59
|
+
const win = globalThis.window;
|
|
60
|
+
if (win)
|
|
61
|
+
win.yt = { config_: JSON.parse(ytConfig) };
|
|
62
|
+
}
|
|
63
|
+
const challenge = (await yt.getAttestationChallenge('ENGAGEMENT_TYPE_UNBOUND'));
|
|
64
|
+
const bgc = challenge.bg_challenge;
|
|
65
|
+
const interpreterUrl = bgc?.interpreter_url?.private_do_not_access_or_else_trusted_resource_url_wrapped_value;
|
|
66
|
+
if (!bgc?.program || !bgc.global_name || !interpreterUrl) {
|
|
67
|
+
throw new Error('No BotGuard challenge in response');
|
|
68
|
+
}
|
|
69
|
+
const bgScript = await (await fetch(`https:${interpreterUrl}`)).text();
|
|
70
|
+
if (!bgScript)
|
|
71
|
+
throw new Error('Could not load BotGuard VM');
|
|
72
|
+
new Function(bgScript)();
|
|
73
|
+
const botGuard = await BotGuardClient.create({
|
|
74
|
+
program: bgc.program,
|
|
75
|
+
globalName: bgc.global_name,
|
|
76
|
+
globalObject: globalThis,
|
|
77
|
+
});
|
|
78
|
+
const webPoSignalOutput = [];
|
|
79
|
+
const botguardResponse = await botGuard.snapshot({ webPoSignalOutput });
|
|
80
|
+
const itResponse = await fetch(buildURL('GenerateIT', true), {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
headers: getHeaders(),
|
|
83
|
+
body: JSON.stringify([REQUEST_KEY, botguardResponse]),
|
|
84
|
+
});
|
|
85
|
+
const [integrityToken, estimatedTtlSecs, mintRefreshThreshold, websafeFallbackToken] = await parseJsonBody(itResponse);
|
|
86
|
+
if (!integrityToken)
|
|
87
|
+
throw new Error('Integrity token request failed');
|
|
88
|
+
logger.info('[pot] minter ready (integrity ttl %ss)', estimatedTtlSecs);
|
|
89
|
+
return WebPoMinter.create({
|
|
90
|
+
integrityToken,
|
|
91
|
+
estimatedTtlSecs,
|
|
92
|
+
mintRefreshThreshold,
|
|
93
|
+
websafeFallbackToken,
|
|
94
|
+
}, webPoSignalOutput);
|
|
95
|
+
}
|
|
96
|
+
async function getMinter() {
|
|
97
|
+
if (minter && Date.now() - minterFetchedAt < INTEGRITY_TTL_MS)
|
|
98
|
+
return minter;
|
|
99
|
+
if (Date.now() - bootstrapFailedAt < BOOTSTRAP_FAIL_COOLDOWN_MS) {
|
|
100
|
+
throw new Error('PoToken bootstrap cooling down');
|
|
101
|
+
}
|
|
102
|
+
if (!bootstrapPromise) {
|
|
103
|
+
bootstrapPromise = (async () => {
|
|
104
|
+
await setupDom();
|
|
105
|
+
const m = await buildMinter();
|
|
106
|
+
minter = m;
|
|
107
|
+
minterFetchedAt = Date.now();
|
|
108
|
+
return m;
|
|
109
|
+
})().catch((err) => {
|
|
110
|
+
bootstrapPromise = null;
|
|
111
|
+
bootstrapFailedAt = Date.now();
|
|
112
|
+
throw err;
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return bootstrapPromise;
|
|
116
|
+
}
|
|
117
|
+
export async function mintPoToken(videoId) {
|
|
118
|
+
const cached = potCache.get(videoId);
|
|
119
|
+
if (cached && Date.now() - cached.fetchedAt < POT_CACHE_MS)
|
|
120
|
+
return cached.pot;
|
|
121
|
+
const m = await getMinter();
|
|
122
|
+
const pot = await m.mintAsWebsafeString(videoId);
|
|
123
|
+
potCache.set(videoId, { pot, fetchedAt: Date.now() });
|
|
124
|
+
return pot;
|
|
125
|
+
}
|
|
126
|
+
export function resetPoTokens(videoId) {
|
|
127
|
+
if (videoId)
|
|
128
|
+
potCache.delete(videoId);
|
|
129
|
+
else {
|
|
130
|
+
potCache.clear();
|
|
131
|
+
minter = null;
|
|
132
|
+
minterFetchedAt = 0;
|
|
133
|
+
bootstrapPromise = null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -5,30 +5,11 @@ import { performMangaWriteTransactionAsync, performWriteTransactionAsync } from
|
|
|
5
5
|
import logger from '../../logger.js';
|
|
6
6
|
import { searchAnilistMangaByTitle, getMangaMetaById, getMangaMetaByMalId } from '../anilist.js';
|
|
7
7
|
import { fetchMalMangaList, mapMalMangaStatusCode } from '../mal.js';
|
|
8
|
+
import { readTrackerSyncState, resolveStatus, } from './sync-state.js';
|
|
8
9
|
const TOKEN_KEY = 'tracker_anilist_token';
|
|
9
10
|
const MANGA_SYNC_STATE_KEY = 'tracker_anilist_manga_sync_state';
|
|
10
11
|
const MANGA_PROVIDER = 'anilist';
|
|
11
12
|
const MAX_TITLE_LOOKUPS_PER_SYNC = 25;
|
|
12
|
-
async function readMangaSyncState(db) {
|
|
13
|
-
const row = await SettingsRepository.getByKey(db, MANGA_SYNC_STATE_KEY);
|
|
14
|
-
if (!row?.value)
|
|
15
|
-
return {};
|
|
16
|
-
try {
|
|
17
|
-
return JSON.parse(row.value);
|
|
18
|
-
}
|
|
19
|
-
catch {
|
|
20
|
-
return {};
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
function resolveStatus(localStatus, remoteStatus, remoteUpdated, localLastSync) {
|
|
24
|
-
if (remoteUpdated > localLastSync && remoteStatus !== localStatus) {
|
|
25
|
-
return { status: remoteStatus, pull: true };
|
|
26
|
-
}
|
|
27
|
-
if (remoteStatus !== localStatus) {
|
|
28
|
-
return { status: localStatus, pull: false };
|
|
29
|
-
}
|
|
30
|
-
return { status: localStatus, pull: false };
|
|
31
|
-
}
|
|
32
13
|
function parseChapterProgress(value) {
|
|
33
14
|
if (value == null)
|
|
34
15
|
return 0;
|
|
@@ -122,7 +103,7 @@ export async function syncAniListManga(db, mangaDb, opts = {}) {
|
|
|
122
103
|
const viewer = await tracker.getViewer();
|
|
123
104
|
const remoteEntries = await tracker.fetchUserMangaList(viewer.id);
|
|
124
105
|
const localItems = await MangaLibraryRepository.getAll(mangaDb);
|
|
125
|
-
const syncState = await
|
|
106
|
+
const syncState = await readTrackerSyncState(db, MANGA_SYNC_STATE_KEY);
|
|
126
107
|
const remoteByMediaId = new Map();
|
|
127
108
|
for (const entry of remoteEntries)
|
|
128
109
|
remoteByMediaId.set(entry.mediaId, entry);
|
|
@@ -618,7 +599,7 @@ export async function insertMangaImportEntries(db, mangaDb, items, eraseAnilistR
|
|
|
618
599
|
if (items.length === 0)
|
|
619
600
|
return 0;
|
|
620
601
|
const now = Math.floor(Date.now() / 1000);
|
|
621
|
-
const syncState = await
|
|
602
|
+
const syncState = await readTrackerSyncState(db, MANGA_SYNC_STATE_KEY);
|
|
622
603
|
await performMangaWriteTransactionAsync(mangaDb, async (tx) => {
|
|
623
604
|
if (eraseAnilistRows) {
|
|
624
605
|
for (const row of await MangaLibraryRepository.getAll(tx)) {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { SettingsRepository } from '../../repositories/settings.repository.js';
|
|
2
|
+
export async function readTrackerSyncState(db, key) {
|
|
3
|
+
const row = await SettingsRepository.getByKey(db, key);
|
|
4
|
+
if (!row?.value)
|
|
5
|
+
return {};
|
|
6
|
+
try {
|
|
7
|
+
return JSON.parse(row.value);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return {};
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function resolveStatus(localStatus, remoteStatus, remoteUpdated, localLastSync) {
|
|
14
|
+
if (remoteUpdated > localLastSync && remoteStatus !== localStatus) {
|
|
15
|
+
return { status: remoteStatus, pull: true };
|
|
16
|
+
}
|
|
17
|
+
if (remoteStatus !== localStatus) {
|
|
18
|
+
return { status: localStatus, pull: false };
|
|
19
|
+
}
|
|
20
|
+
return { status: localStatus, pull: false };
|
|
21
|
+
}
|
|
@@ -11,33 +11,14 @@ import { fetchMalUserList } from '../mal.js';
|
|
|
11
11
|
import { offlineDb } from '../offline-db.js';
|
|
12
12
|
import { getShowMetaById, getShowMetaByMalId, isAnilistRateLimited, searchAnilistByTitle, } from '../anilist.js';
|
|
13
13
|
import { kitsuSearchAnime } from '../kitsu.js';
|
|
14
|
+
import { readTrackerSyncState, resolveStatus, } from './sync-state.js';
|
|
14
15
|
const TOKEN_KEY = 'tracker_anilist_token';
|
|
15
16
|
const USER_KEY = 'tracker_anilist_user';
|
|
16
17
|
const SYNC_STATE_KEY = 'tracker_anilist_sync_state';
|
|
17
|
-
async function readSyncState(db) {
|
|
18
|
-
const row = await SettingsRepository.getByKey(db, SYNC_STATE_KEY);
|
|
19
|
-
if (!row?.value)
|
|
20
|
-
return {};
|
|
21
|
-
try {
|
|
22
|
-
return JSON.parse(row.value);
|
|
23
|
-
}
|
|
24
|
-
catch {
|
|
25
|
-
return {};
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
18
|
async function getWatchedCount(db, showId) {
|
|
29
19
|
const row = await dbGet(db, 'SELECT COUNT(DISTINCT episodeNumber) as total FROM watched_episodes WHERE showId = ?', [showId]);
|
|
30
20
|
return row?.total ?? 0;
|
|
31
21
|
}
|
|
32
|
-
function resolveStatus(localStatus, remoteStatus, remoteUpdated, localLastSync) {
|
|
33
|
-
if (remoteUpdated > localLastSync && remoteStatus !== localStatus) {
|
|
34
|
-
return { status: remoteStatus, pull: true };
|
|
35
|
-
}
|
|
36
|
-
if (remoteStatus !== localStatus) {
|
|
37
|
-
return { status: localStatus, pull: false };
|
|
38
|
-
}
|
|
39
|
-
return { status: localStatus, pull: false };
|
|
40
|
-
}
|
|
41
22
|
export async function syncAniList(db) {
|
|
42
23
|
const tokenRow = await SettingsRepository.getByKey(db, TOKEN_KEY);
|
|
43
24
|
const token = tokenRow?.value;
|
|
@@ -47,7 +28,7 @@ export async function syncAniList(db) {
|
|
|
47
28
|
const viewer = await tracker.getViewer();
|
|
48
29
|
const remoteEntries = await tracker.fetchUserAnimeList(viewer.id);
|
|
49
30
|
const localItems = await WatchlistRepository.getAll(db);
|
|
50
|
-
const syncState = await
|
|
31
|
+
const syncState = await readTrackerSyncState(db, SYNC_STATE_KEY);
|
|
51
32
|
const remoteByMediaId = new Map();
|
|
52
33
|
for (const entry of remoteEntries)
|
|
53
34
|
remoteByMediaId.set(entry.mediaId, entry);
|
|
@@ -299,7 +280,7 @@ export async function importFromUsername(db, username, erase = false) {
|
|
|
299
280
|
if (entries.length === 0)
|
|
300
281
|
return 0;
|
|
301
282
|
const now = Math.floor(Date.now() / 1000);
|
|
302
|
-
const syncState = await
|
|
283
|
+
const syncState = await readTrackerSyncState(db, SYNC_STATE_KEY);
|
|
303
284
|
await performWriteTransactionAsync(db, async (tx) => {
|
|
304
285
|
if (erase)
|
|
305
286
|
await SettingsRepository.clearWatchlist(tx);
|