@serifpersia/dango 3.4.4 → 3.4.5
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-I3S6qw9d.js} +1 -1
- package/client/dist/assets/{AnimeInfo-DyX9qTwg.js → AnimeInfo-CLU2NSBR.js} +1 -1
- package/client/dist/assets/{AnimeInfoPage-Dvp_c8vK.js → AnimeInfoPage-4Mw9ArDy.js} +2 -2
- package/client/dist/assets/{Asmr-qg3lMAJn.js → Asmr-DN9XBc5Y.js} +1 -1
- package/client/dist/assets/{AsmrResetProgressModal-BpPlg_Vv.js → AsmrResetProgressModal-DyU1Z2Gw.js} +1 -1
- package/client/dist/assets/{AvSyncCalibrator-FJllOgOH.js → AvSyncCalibrator-DxL0mr_4.js} +1 -1
- package/client/dist/assets/{Home-A7gCeEST.js → Home-CeTeJ-gM.js} +1 -1
- package/client/dist/assets/{Insights-G9IXepHJ.js → Insights-i34-qV4C.js} +1 -1
- package/client/dist/assets/{LibraryListPage-Bh2qCJPh.js → LibraryListPage-DE-ASOX2.js} +1 -1
- package/client/dist/assets/{ListeningList-Czv4cTRl.js → ListeningList-DrNce5ll.js} +1 -1
- package/client/dist/assets/{Manga-qdAAS1z7.js → Manga-DOrf7Fk6.js} +1 -1
- package/client/dist/assets/{MangaInfoPage-1rTgZzf2.js → MangaInfoPage-CAZMLMGw.js} +1 -1
- package/client/dist/assets/{MangaReadPage-CeyxPG6w.js → MangaReadPage-DQVEo8_F.js} +1 -1
- package/client/dist/assets/{MangaResetProgressModal-C_ppotmh.js → MangaResetProgressModal-DOjpLAMp.js} +1 -1
- package/client/dist/assets/{Mature-Bkhxnr3K.js → Mature-D2xb9EGB.js} +1 -1
- package/client/dist/assets/{MediaCard-BQZsutEo.js → MediaCard-CKUEcFTd.js} +1 -1
- package/client/dist/assets/{Music-C0rGzSbi.js → Music-w_a34r7r.js} +1 -1
- package/client/dist/assets/{OptionListMenu-CyfpCBEy.js → OptionListMenu-DxZHBMIZ.js} +1 -1
- package/client/dist/assets/{Player-yhiTlpYU.js → Player-B8YHeATv.js} +2 -2
- package/client/dist/assets/{PlayerSettings-icy4SfQa.js → PlayerSettings-Ddlg-MvW.js} +1 -1
- package/client/dist/assets/{QueueOptionsButton-5eloKF5J.js → QueueOptionsButton-BMvAC6Ut.js} +1 -1
- package/client/dist/assets/{QueueRail-DaMjlKL4.js → QueueRail-CAoGJvHP.js} +1 -1
- package/client/dist/assets/{Radio-FxGqCxR4.js → Radio-6O7rAUv1.js} +1 -1
- package/client/dist/assets/{ReadingList-CCm3IAbt.js → ReadingList-k3g8oHvs.js} +1 -1
- package/client/dist/assets/{ResetProgressModal-CeI4V3Y5.js → ResetProgressModal-vFC6N-Qw.js} +1 -1
- package/client/dist/assets/{Search-qpILaAiQ.js → Search-Bh9Y6nIJ.js} +1 -1
- package/client/dist/assets/{Settings-Bw9M3NGz.js → Settings-DRumujfh.js} +1 -1
- package/client/dist/assets/{SkeletonGrid-0dY_xMq3.js → SkeletonGrid-BtzUnQE0.js} +1 -1
- package/client/dist/assets/{Trackers-Zy5GOs0P.js → Trackers-D0nqy8wu.js} +1 -1
- package/client/dist/assets/{Tv-DREwc7BX.js → Tv-DVebLMNS.js} +1 -1
- package/client/dist/assets/{TvInfo-CHIsVzZ0.js → TvInfo-BkECbOKN.js} +1 -1
- package/client/dist/assets/{TvPopup-D_tVTyZ2.js → TvPopup-D8WWY6o4.js} +1 -1
- package/client/dist/assets/{TvSearch-BVxedBW-.js → TvSearch-kWPep3gL.js} +1 -1
- package/client/dist/assets/{TvWatchlist-BEIwq9R9.js → TvWatchlist-dD4emIHx.js} +1 -1
- package/client/dist/assets/{Watchlist-CSngjqlz.js → Watchlist-cWxw38nU.js} +1 -1
- package/client/dist/assets/{hls-D48GVTkk.js → hls-uG_Y3i5s.js} +1 -1
- package/client/dist/assets/{index-CVB5dYhT.js → index-BWIWE3Qy.js} +2 -2
- package/client/dist/assets/{useAnimeInfoData-DxdOg_B7.js → useAnimeInfoData-DjPBg24u.js} +1 -1
- package/client/dist/assets/{useAsmr-ClckxnPe.js → useAsmr-DQyobNQX.js} +1 -1
- package/client/dist/assets/{useAsmrPopup-Bbgxefy_.js → useAsmrPopup-CDoroMBD.js} +1 -1
- package/client/dist/assets/{useMangaLibrary-B3_BUKVk.js → useMangaLibrary-C6L8mrnS.js} +1 -1
- package/client/dist/assets/{useMangaPopup-Celk4lzf.js → useMangaPopup-BSuMa_vA.js} +1 -1
- package/client/dist/assets/{useProviders-pwxPZPHh.js → useProviders-XIdvDof6.js} +1 -1
- package/client/dist/assets/{useTv-D9sQrDop.js → useTv-DzSYasru.js} +1 -1
- package/client/dist/assets/{useTvLibrary-UsQpgBTB.js → useTvLibrary-BnbHS-gg.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 +3 -24
- package/server/dist/hono/tv.js +3 -56
- package/server/dist/lib/mal.js +4 -3
- 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 +2 -1
- 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 -52
- package/server/dist/repositories/settings.repository.js +8 -0
- package/server/dist/repositories/tv.repository.js +3 -46
- 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/package.json +4 -1
|
@@ -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);
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
const MEDIA_HEADER = 20;
|
|
2
|
+
const MEDIA = 21;
|
|
3
|
+
const STREAM_PROTECTION_STATUS = 58;
|
|
4
|
+
function readVarint(buf, pos) {
|
|
5
|
+
if (pos >= buf.length)
|
|
6
|
+
return null;
|
|
7
|
+
const prefix = buf[pos];
|
|
8
|
+
let size = 0;
|
|
9
|
+
for (let shift = 1; shift <= 5; shift++) {
|
|
10
|
+
if ((prefix & (128 >> (shift - 1))) === 0) {
|
|
11
|
+
size = shift;
|
|
12
|
+
break;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
if (size < 1 || pos + size > buf.length)
|
|
16
|
+
return null;
|
|
17
|
+
if (size === 1)
|
|
18
|
+
return [prefix, pos + 1];
|
|
19
|
+
if (size === 5) {
|
|
20
|
+
const value = buf[pos + 1] + buf[pos + 2] * 256 + buf[pos + 3] * 65536 + buf[pos + 4] * 16777216;
|
|
21
|
+
return [value >>> 0, pos + 5];
|
|
22
|
+
}
|
|
23
|
+
const lowBits = 8 - size;
|
|
24
|
+
let value = prefix & ((1 << lowBits) - 1);
|
|
25
|
+
for (let i = 1; i < size; i++) {
|
|
26
|
+
value += buf[pos + i] * 2 ** (lowBits + 8 * (i - 1));
|
|
27
|
+
}
|
|
28
|
+
return [value, pos + size];
|
|
29
|
+
}
|
|
30
|
+
function readStatusField(payload) {
|
|
31
|
+
let pos = 0;
|
|
32
|
+
while (pos < payload.length) {
|
|
33
|
+
const key = payload[pos++];
|
|
34
|
+
const field = key >> 3;
|
|
35
|
+
const wire = key & 7;
|
|
36
|
+
if (wire !== 0)
|
|
37
|
+
break;
|
|
38
|
+
const decoded = readVarint(payload, pos);
|
|
39
|
+
if (!decoded)
|
|
40
|
+
break;
|
|
41
|
+
if (field === 1)
|
|
42
|
+
return decoded[0];
|
|
43
|
+
pos = decoded[1];
|
|
44
|
+
}
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
export function parseUmpResponse(buf, carry) {
|
|
48
|
+
const mediaChunks = [];
|
|
49
|
+
let status = 1;
|
|
50
|
+
let nextCarry = carry;
|
|
51
|
+
let pos = 0;
|
|
52
|
+
if (nextCarry) {
|
|
53
|
+
const t = readVarint(buf, pos);
|
|
54
|
+
const s = t && readVarint(buf, t[1]);
|
|
55
|
+
if (t && s && t[0] === MEDIA_HEADER) {
|
|
56
|
+
pos = s[1] + s[0];
|
|
57
|
+
}
|
|
58
|
+
const ct = readVarint(buf, pos);
|
|
59
|
+
const cs = ct && readVarint(buf, ct[1]);
|
|
60
|
+
if (ct && cs && ct[0] === nextCarry.pendingType) {
|
|
61
|
+
pos = cs[1];
|
|
62
|
+
const take = Math.min(cs[0], buf.length - pos);
|
|
63
|
+
nextCarry.chunks.push(buf.subarray(pos, pos + take));
|
|
64
|
+
nextCarry.remaining = cs[0] - take;
|
|
65
|
+
pos += take;
|
|
66
|
+
if (nextCarry.remaining <= 0) {
|
|
67
|
+
const full = Buffer.concat(nextCarry.chunks);
|
|
68
|
+
nextCarry = null;
|
|
69
|
+
if (ct[0] === MEDIA && full.length > 1)
|
|
70
|
+
mediaChunks.push(full.subarray(1));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
nextCarry = null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
while (pos < buf.length) {
|
|
78
|
+
const t = readVarint(buf, pos);
|
|
79
|
+
if (!t)
|
|
80
|
+
break;
|
|
81
|
+
const s = readVarint(buf, t[1]);
|
|
82
|
+
if (!s)
|
|
83
|
+
break;
|
|
84
|
+
const [type, size] = [t[0], s[0]];
|
|
85
|
+
pos = s[1];
|
|
86
|
+
const available = buf.length - pos;
|
|
87
|
+
if (size > available) {
|
|
88
|
+
nextCarry = {
|
|
89
|
+
pendingType: type,
|
|
90
|
+
remaining: size - available,
|
|
91
|
+
chunks: [buf.subarray(pos)],
|
|
92
|
+
};
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
const payload = buf.subarray(pos, pos + size);
|
|
96
|
+
pos += size;
|
|
97
|
+
if (type === MEDIA) {
|
|
98
|
+
if (payload.length > 1)
|
|
99
|
+
mediaChunks.push(payload.subarray(1));
|
|
100
|
+
}
|
|
101
|
+
else if (type === STREAM_PROTECTION_STATUS) {
|
|
102
|
+
status = readStatusField(payload);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return { media: Buffer.concat(mediaChunks), status, carry: nextCarry };
|
|
106
|
+
}
|
|
@@ -238,7 +238,8 @@ function createCtx(cache) {
|
|
|
238
238
|
? { url: urlOrOptions, ...(maybeOptions ?? {}) }
|
|
239
239
|
: urlOrOptions);
|
|
240
240
|
const { url, ...rest } = opts;
|
|
241
|
-
const res = await gotScraping(
|
|
241
|
+
const res = await gotScraping({
|
|
242
|
+
url: url,
|
|
242
243
|
method: 'GET',
|
|
243
244
|
responseType: 'text',
|
|
244
245
|
throwHttpErrors: false,
|
|
@@ -1,48 +1,13 @@
|
|
|
1
1
|
import { eq, inArray, sql } from 'drizzle-orm';
|
|
2
2
|
import { getDrizzle } from '../db/drizzle.js';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
'Completed',
|
|
7
|
-
'On-Hold',
|
|
8
|
-
'Dropped',
|
|
9
|
-
'Planned',
|
|
10
|
-
];
|
|
3
|
+
import { asmrProgress } from '../db/schema-asmr.js';
|
|
4
|
+
import { makeLibraryRepo } from './library.repository.js';
|
|
5
|
+
export const ASMR_STATUSES = ['Listening', 'Completed', 'On-Hold', 'Dropped', 'Planned'];
|
|
11
6
|
export function buildAsmrId(rjCode) {
|
|
12
7
|
return String(rjCode).trim().toUpperCase();
|
|
13
8
|
}
|
|
14
9
|
export const AsmrLibraryRepository = {
|
|
15
|
-
|
|
16
|
-
const rows = await getDrizzle(db).all(sql `SELECT * FROM asmr_library WHERE id = ${id}`);
|
|
17
|
-
return rows[0];
|
|
18
|
-
},
|
|
19
|
-
exists: async (db, id) => {
|
|
20
|
-
const rows = await getDrizzle(db).all(sql `SELECT EXISTS(SELECT 1 FROM asmr_library WHERE id = ${id}) as inLibrary`);
|
|
21
|
-
return !!(rows[0] && rows[0].inLibrary);
|
|
22
|
-
},
|
|
23
|
-
getAll: (db, status, limit, offset) => {
|
|
24
|
-
const q = sql `SELECT * FROM asmr_library`;
|
|
25
|
-
if (status && status !== 'All') {
|
|
26
|
-
q.append(sql ` WHERE status = ${status}`);
|
|
27
|
-
}
|
|
28
|
-
q.append(sql ` ORDER BY updatedAt DESC`);
|
|
29
|
-
if (limit !== undefined && offset !== undefined) {
|
|
30
|
-
q.append(sql ` LIMIT ${limit} OFFSET ${offset}`);
|
|
31
|
-
}
|
|
32
|
-
return getDrizzle(db).all(q);
|
|
33
|
-
},
|
|
34
|
-
getCount: async (db, status) => {
|
|
35
|
-
if (status && status !== 'All') {
|
|
36
|
-
const rows = await getDrizzle(db).all(sql `SELECT COUNT(*) as total FROM asmr_library WHERE status = ${status}`);
|
|
37
|
-
return rows[0]?.total || 0;
|
|
38
|
-
}
|
|
39
|
-
const rows = await getDrizzle(db).all(sql `SELECT COUNT(*) as total FROM asmr_library`);
|
|
40
|
-
return rows[0]?.total || 0;
|
|
41
|
-
},
|
|
42
|
-
getIds: async (db) => {
|
|
43
|
-
const rows = await getDrizzle(db).all(sql `SELECT id FROM asmr_library`);
|
|
44
|
-
return rows.map((r) => r.id);
|
|
45
|
-
},
|
|
10
|
+
...makeLibraryRepo('asmr_library'),
|
|
46
11
|
upsert: (db, data) => getDrizzle(db).run(sql `
|
|
47
12
|
INSERT INTO asmr_library (id, rjCode, title, thumbnail, status, isAdult, updatedAt)
|
|
48
13
|
VALUES (${data.id}, ${data.rjCode}, ${data.title}, ${data.thumbnail}, ${data.status}, ${data.isAdult ? 1 : 0}, strftime('%s', 'now'))
|
|
@@ -53,22 +18,8 @@ export const AsmrLibraryRepository = {
|
|
|
53
18
|
status = COALESCE(NULLIF(EXCLUDED.status, ''), asmr_library.status),
|
|
54
19
|
isAdult = COALESCE(EXCLUDED.isAdult, asmr_library.isAdult),
|
|
55
20
|
updatedAt = strftime('%s', 'now')`),
|
|
56
|
-
updateStatus: (db, id, status) => getDrizzle(db).run(sql `
|
|
57
|
-
UPDATE asmr_library SET status = ${status}, updatedAt = strftime('%s', 'now') WHERE id = ${id}`),
|
|
58
|
-
updateStatusMany: (db, ids, status) => {
|
|
59
|
-
if (ids.length === 0)
|
|
60
|
-
return Promise.resolve();
|
|
61
|
-
return getDrizzle(db).run(sql `
|
|
62
|
-
UPDATE asmr_library SET status = ${status}, updatedAt = strftime('%s', 'now') WHERE id IN (${sql.join(ids.map((id) => sql `${id}`), sql `, `)})`);
|
|
63
|
-
},
|
|
64
21
|
touchProgress: (db, id, progress) => getDrizzle(db).run(sql `
|
|
65
22
|
UPDATE asmr_library SET lastTrackIndex = ${progress.trackIndex}, lastTrackLabel = ${progress.trackLabel}, lastPosition = ${progress.position}, updatedAt = strftime('%s', 'now') WHERE id = ${id}`),
|
|
66
|
-
delete: (db, id) => getDrizzle(db).delete(asmrLibrary).where(eq(asmrLibrary.id, id)),
|
|
67
|
-
deleteMany: (db, ids) => {
|
|
68
|
-
if (ids.length === 0)
|
|
69
|
-
return Promise.resolve();
|
|
70
|
-
return getDrizzle(db).delete(asmrLibrary).where(inArray(asmrLibrary.id, ids));
|
|
71
|
-
},
|
|
72
23
|
};
|
|
73
24
|
export const AsmrProgressRepository = {
|
|
74
25
|
getByWork: (db, workId) => getDrizzle(db).all(sql `
|
|
@@ -36,6 +36,52 @@ async function clearTables(db, tables) {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
+
export function makeLibraryRepo(table) {
|
|
40
|
+
const t = sql.raw(`"${table}"`);
|
|
41
|
+
return {
|
|
42
|
+
getById: async (db, id) => {
|
|
43
|
+
const rows = await getDrizzle(db).all(sql `SELECT * FROM ${t} WHERE id = ${id}`);
|
|
44
|
+
return rows[0];
|
|
45
|
+
},
|
|
46
|
+
getAll: (db, status, limit, offset) => {
|
|
47
|
+
const q = sql `SELECT * FROM ${t}`;
|
|
48
|
+
if (status && status !== 'All') {
|
|
49
|
+
q.append(sql ` WHERE status = ${status}`);
|
|
50
|
+
}
|
|
51
|
+
q.append(sql ` ORDER BY updatedAt DESC`);
|
|
52
|
+
if (limit !== undefined && offset !== undefined) {
|
|
53
|
+
q.append(sql ` LIMIT ${limit} OFFSET ${offset}`);
|
|
54
|
+
}
|
|
55
|
+
return getDrizzle(db).all(q);
|
|
56
|
+
},
|
|
57
|
+
getCount: async (db, status) => {
|
|
58
|
+
if (status && status !== 'All') {
|
|
59
|
+
const rows = await getDrizzle(db).all(sql `SELECT COUNT(*) as total FROM ${t} WHERE status = ${status}`);
|
|
60
|
+
return rows[0]?.total || 0;
|
|
61
|
+
}
|
|
62
|
+
const rows = await getDrizzle(db).all(sql `SELECT COUNT(*) as total FROM ${t}`);
|
|
63
|
+
return rows[0]?.total || 0;
|
|
64
|
+
},
|
|
65
|
+
getIds: async (db) => {
|
|
66
|
+
const rows = await getDrizzle(db).all(sql `SELECT id FROM ${t}`);
|
|
67
|
+
return rows.map((r) => r.id);
|
|
68
|
+
},
|
|
69
|
+
updateStatus: (db, id, status) => getDrizzle(db).run(sql `
|
|
70
|
+
UPDATE ${t} SET status = ${status}, updatedAt = strftime('%s', 'now') WHERE id = ${id}`),
|
|
71
|
+
updateStatusMany: (db, ids, status) => {
|
|
72
|
+
if (ids.length === 0)
|
|
73
|
+
return Promise.resolve();
|
|
74
|
+
return getDrizzle(db).run(sql `
|
|
75
|
+
UPDATE ${t} SET status = ${status}, updatedAt = strftime('%s', 'now') WHERE id IN (${sql.join(ids.map((id) => sql `${id}`), sql `, `)})`);
|
|
76
|
+
},
|
|
77
|
+
delete: (db, id) => getDrizzle(db).run(sql `DELETE FROM ${t} WHERE id = ${id}`),
|
|
78
|
+
deleteMany: (db, ids) => {
|
|
79
|
+
if (ids.length === 0)
|
|
80
|
+
return Promise.resolve();
|
|
81
|
+
return getDrizzle(db).run(sql `DELETE FROM ${t} WHERE id IN (${sql.join(ids.map((id) => sql `${id}`), sql `, `)})`);
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
39
85
|
export const LibraryRepository = {
|
|
40
86
|
countAll: (db) => countTables(db, LIBRARY_TABLES),
|
|
41
87
|
clearAll: (db) => clearTables(db, LIBRARY_TABLES),
|
|
@@ -1,48 +1,13 @@
|
|
|
1
1
|
import { eq, inArray, sql } from 'drizzle-orm';
|
|
2
2
|
import { getDrizzle } from '../db/drizzle.js';
|
|
3
3
|
import { mangaLibrary, mangaProgress } from '../db/schema-manga.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
'Completed',
|
|
7
|
-
'On-Hold',
|
|
8
|
-
'Dropped',
|
|
9
|
-
'Planned',
|
|
10
|
-
];
|
|
4
|
+
import { makeLibraryRepo } from './library.repository.js';
|
|
5
|
+
export const MANGA_STATUSES = ['Reading', 'Completed', 'On-Hold', 'Dropped', 'Planned'];
|
|
11
6
|
export function buildMangaId(provider, mangaId) {
|
|
12
7
|
return `${provider.toLowerCase()}:${mangaId}`;
|
|
13
8
|
}
|
|
14
9
|
export const MangaLibraryRepository = {
|
|
15
|
-
|
|
16
|
-
const rows = await getDrizzle(db).all(sql `SELECT * FROM manga_library WHERE id = ${id}`);
|
|
17
|
-
return rows[0];
|
|
18
|
-
},
|
|
19
|
-
exists: async (db, id) => {
|
|
20
|
-
const rows = await getDrizzle(db).all(sql `SELECT EXISTS(SELECT 1 FROM manga_library WHERE id = ${id}) as inLibrary`);
|
|
21
|
-
return !!(rows[0] && rows[0].inLibrary);
|
|
22
|
-
},
|
|
23
|
-
getAll: (db, status, limit, offset) => {
|
|
24
|
-
const q = sql `SELECT * FROM manga_library`;
|
|
25
|
-
if (status && status !== 'All') {
|
|
26
|
-
q.append(sql ` WHERE status = ${status}`);
|
|
27
|
-
}
|
|
28
|
-
q.append(sql ` ORDER BY updatedAt DESC`);
|
|
29
|
-
if (limit !== undefined && offset !== undefined) {
|
|
30
|
-
q.append(sql ` LIMIT ${limit} OFFSET ${offset}`);
|
|
31
|
-
}
|
|
32
|
-
return getDrizzle(db).all(q);
|
|
33
|
-
},
|
|
34
|
-
getCount: async (db, status) => {
|
|
35
|
-
if (status && status !== 'All') {
|
|
36
|
-
const rows = await getDrizzle(db).all(sql `SELECT COUNT(*) as total FROM manga_library WHERE status = ${status}`);
|
|
37
|
-
return rows[0]?.total || 0;
|
|
38
|
-
}
|
|
39
|
-
const rows = await getDrizzle(db).all(sql `SELECT COUNT(*) as total FROM manga_library`);
|
|
40
|
-
return rows[0]?.total || 0;
|
|
41
|
-
},
|
|
42
|
-
getIds: async (db) => {
|
|
43
|
-
const rows = await getDrizzle(db).all(sql `SELECT id FROM manga_library`);
|
|
44
|
-
return rows.map((r) => r.id);
|
|
45
|
-
},
|
|
10
|
+
...makeLibraryRepo('manga_library'),
|
|
46
11
|
upsert: (db, data) => getDrizzle(db).run(sql `
|
|
47
12
|
INSERT INTO manga_library (id, provider, mangaId, title, cover, status, author, altTitle, contentRating, anilistId, anilistIdSource, updatedAt)
|
|
48
13
|
VALUES (${data.id}, ${data.provider}, ${data.mangaId}, ${data.title}, ${data.cover}, ${data.status}, ${data.author || null}, ${data.altTitle || null}, ${data.contentRating || null}, ${data.anilistId ?? null}, ${data.anilistIdSource ?? null}, strftime('%s', 'now'))
|
|
@@ -64,24 +29,10 @@ export const MangaLibraryRepository = {
|
|
|
64
29
|
.where(eq(mangaLibrary.id, id))
|
|
65
30
|
: getDrizzle(db).update(mangaLibrary).set({ anilistId }).where(eq(mangaLibrary.id, id)),
|
|
66
31
|
getByAnilistId: (db, anilistId) => getDrizzle(db).all(sql `SELECT * FROM manga_library WHERE anilistId = ${anilistId}`),
|
|
67
|
-
updateStatus: (db, id, status) => getDrizzle(db).run(sql `
|
|
68
|
-
UPDATE manga_library SET status = ${status}, updatedAt = strftime('%s', 'now') WHERE id = ${id}`),
|
|
69
|
-
updateStatusMany: (db, ids, status) => {
|
|
70
|
-
if (ids.length === 0)
|
|
71
|
-
return Promise.resolve();
|
|
72
|
-
return getDrizzle(db).run(sql `
|
|
73
|
-
UPDATE manga_library SET status = ${status}, updatedAt = strftime('%s', 'now') WHERE id IN (${sql.join(ids.map((id) => sql `${id}`), sql `, `)})`);
|
|
74
|
-
},
|
|
75
32
|
touchProgress: (db, id, progress) => getDrizzle(db).run(sql `
|
|
76
33
|
UPDATE manga_library SET lastChapterId = ${progress.chapterId}, lastChapterNumber = ${progress.chapterNumber}, lastPage = ${progress.page}, updatedAt = strftime('%s', 'now') WHERE id = ${id}`),
|
|
77
34
|
setProgressPointer: (db, id, chapterNumber) => getDrizzle(db).run(sql `
|
|
78
35
|
UPDATE manga_library SET lastChapterNumber = ${chapterNumber}, updatedAt = strftime('%s', 'now') WHERE id = ${id}`),
|
|
79
|
-
delete: (db, id) => getDrizzle(db).delete(mangaLibrary).where(eq(mangaLibrary.id, id)),
|
|
80
|
-
deleteMany: (db, ids) => {
|
|
81
|
-
if (ids.length === 0)
|
|
82
|
-
return Promise.resolve();
|
|
83
|
-
return getDrizzle(db).delete(mangaLibrary).where(inArray(mangaLibrary.id, ids));
|
|
84
|
-
},
|
|
85
36
|
};
|
|
86
37
|
export const MangaProgressRepository = {
|
|
87
38
|
getByManga: (db, mangaId) => getDrizzle(db).all(sql `
|
|
@@ -11,6 +11,14 @@ export const SettingsRepository = {
|
|
|
11
11
|
.values({ key, value })
|
|
12
12
|
.onConflictDoUpdate({ target: settings.key, set: { value } }),
|
|
13
13
|
deleteByKey: (db, key) => getDrizzle(db).delete(settings).where(eq(settings.key, key)),
|
|
14
|
+
readContinueFlags: async (db, ignoreKey, listOnlyKey) => {
|
|
15
|
+
const ignoreRow = await SettingsRepository.getByKey(db, ignoreKey);
|
|
16
|
+
const listOnlyRow = await SettingsRepository.getByKey(db, listOnlyKey);
|
|
17
|
+
return {
|
|
18
|
+
ignoreAdult: ignoreRow ? ignoreRow.value !== 'false' : true,
|
|
19
|
+
listOnly: listOnlyRow ? listOnlyRow.value === 'true' || listOnlyRow.value === '1' : false,
|
|
20
|
+
};
|
|
21
|
+
},
|
|
14
22
|
clearWatchlist: (db) => getDrizzle(db).delete(watchlist),
|
|
15
23
|
upsertWatchlistBatch: (db, shows) => {
|
|
16
24
|
if (shows.length === 0)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { eq, inArray, sql } from 'drizzle-orm';
|
|
2
2
|
import { getDrizzle } from '../db/drizzle.js';
|
|
3
|
-
import {
|
|
3
|
+
import { tvProgress } from '../db/schema-tv.js';
|
|
4
|
+
import { makeLibraryRepo } from './library.repository.js';
|
|
4
5
|
export const TV_STATUSES = ['Watching', 'Completed', 'On-Hold', 'Dropped', 'Planned'];
|
|
5
6
|
export function buildTvId(mediaType, tmdbId) {
|
|
6
7
|
const t = String(mediaType).toLowerCase() === 'movie' ? 'movie' : 'tv';
|
|
@@ -14,37 +15,7 @@ export function normalizeTvMediaId(raw) {
|
|
|
14
15
|
return s;
|
|
15
16
|
}
|
|
16
17
|
export const TvLibraryRepository = {
|
|
17
|
-
|
|
18
|
-
const rows = await getDrizzle(db).all(sql `SELECT * FROM tv_library WHERE id = ${id}`);
|
|
19
|
-
return rows[0];
|
|
20
|
-
},
|
|
21
|
-
exists: async (db, id) => {
|
|
22
|
-
const rows = await getDrizzle(db).all(sql `SELECT EXISTS(SELECT 1 FROM tv_library WHERE id = ${id}) as inLibrary`);
|
|
23
|
-
return !!(rows[0] && rows[0].inLibrary);
|
|
24
|
-
},
|
|
25
|
-
getAll: (db, status, limit, offset) => {
|
|
26
|
-
const q = sql `SELECT * FROM tv_library`;
|
|
27
|
-
if (status && status !== 'All') {
|
|
28
|
-
q.append(sql ` WHERE status = ${status}`);
|
|
29
|
-
}
|
|
30
|
-
q.append(sql ` ORDER BY updatedAt DESC`);
|
|
31
|
-
if (limit !== undefined && offset !== undefined) {
|
|
32
|
-
q.append(sql ` LIMIT ${limit} OFFSET ${offset}`);
|
|
33
|
-
}
|
|
34
|
-
return getDrizzle(db).all(q);
|
|
35
|
-
},
|
|
36
|
-
getCount: async (db, status) => {
|
|
37
|
-
if (status && status !== 'All') {
|
|
38
|
-
const rows = await getDrizzle(db).all(sql `SELECT COUNT(*) as total FROM tv_library WHERE status = ${status}`);
|
|
39
|
-
return rows[0]?.total || 0;
|
|
40
|
-
}
|
|
41
|
-
const rows = await getDrizzle(db).all(sql `SELECT COUNT(*) as total FROM tv_library`);
|
|
42
|
-
return rows[0]?.total || 0;
|
|
43
|
-
},
|
|
44
|
-
getIds: async (db) => {
|
|
45
|
-
const rows = await getDrizzle(db).all(sql `SELECT id FROM tv_library`);
|
|
46
|
-
return rows.map((r) => r.id);
|
|
47
|
-
},
|
|
18
|
+
...makeLibraryRepo('tv_library'),
|
|
48
19
|
upsert: (db, data) => getDrizzle(db).run(sql `
|
|
49
20
|
INSERT INTO tv_library (id, tmdbId, mediaType, title, poster, backdrop, year, overview, status, adult, updatedAt)
|
|
50
21
|
VALUES (${data.id}, ${data.tmdbId}, ${data.mediaType}, ${data.title}, ${data.poster}, ${data.backdrop || null}, ${data.year || null}, ${data.overview || null}, ${data.status}, ${data.adult ? 1 : 0}, strftime('%s', 'now'))
|
|
@@ -59,22 +30,8 @@ export const TvLibraryRepository = {
|
|
|
59
30
|
status = COALESCE(NULLIF(EXCLUDED.status, ''), tv_library.status),
|
|
60
31
|
adult = COALESCE(EXCLUDED.adult, tv_library.adult),
|
|
61
32
|
updatedAt = strftime('%s', 'now')`),
|
|
62
|
-
updateStatus: (db, id, status) => getDrizzle(db).run(sql `
|
|
63
|
-
UPDATE tv_library SET status = ${status}, updatedAt = strftime('%s', 'now') WHERE id = ${id}`),
|
|
64
|
-
updateStatusMany: (db, ids, status) => {
|
|
65
|
-
if (ids.length === 0)
|
|
66
|
-
return Promise.resolve();
|
|
67
|
-
return getDrizzle(db).run(sql `
|
|
68
|
-
UPDATE tv_library SET status = ${status}, updatedAt = strftime('%s', 'now') WHERE id IN (${sql.join(ids.map((id) => sql `${id}`), sql `, `)})`);
|
|
69
|
-
},
|
|
70
33
|
touchProgress: (db, id, progress) => getDrizzle(db).run(sql `
|
|
71
34
|
UPDATE tv_library SET lastSeason = ${progress.season}, lastEpisode = ${progress.episode}, updatedAt = strftime('%s', 'now') WHERE id = ${id}`),
|
|
72
|
-
delete: (db, id) => getDrizzle(db).delete(tvLibrary).where(eq(tvLibrary.id, id)),
|
|
73
|
-
deleteMany: (db, ids) => {
|
|
74
|
-
if (ids.length === 0)
|
|
75
|
-
return Promise.resolve();
|
|
76
|
-
return getDrizzle(db).delete(tvLibrary).where(inArray(tvLibrary.id, ids));
|
|
77
|
-
},
|
|
78
35
|
};
|
|
79
36
|
export const TvProgressRepository = {
|
|
80
37
|
getByMedia: (db, mediaId) => getDrizzle(db).all(sql `
|