@serifpersia/dango 3.6.7 → 3.6.9
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-C3P9zWi9.js → AnimeCard-DKDJK8Mp.js} +1 -1
- package/client/dist/assets/{AnimeInfo-Et6687Tl.js → AnimeInfo-CTcvfjva.js} +1 -1
- package/client/dist/assets/{AnimeInfoPage-DX4eNnlH.js → AnimeInfoPage-B_UiacIN.js} +2 -2
- package/client/dist/assets/{Asmr-CCdEWrFi.js → Asmr-DMSN2vxl.js} +2 -2
- package/client/dist/assets/{AsmrResetProgressModal-DgEWBJOw.js → AsmrResetProgressModal-CRwfOatX.js} +1 -1
- package/client/dist/assets/{AvSyncCalibrator-CQ2fjf2E.js → AvSyncCalibrator-CRJsi-vz.js} +1 -1
- package/client/dist/assets/{Home-DUkUTTTy.js → Home-pwmK9_Eh.js} +1 -1
- package/client/dist/assets/{Insights-QCh4xW12.js → Insights-m7kFzg2A.js} +1 -1
- package/client/dist/assets/{LibraryListPage-BoTxRqHz.js → LibraryListPage-C1wpPvu6.js} +1 -1
- package/client/dist/assets/ListeningList-OzfEI1Ga.js +1 -0
- package/client/dist/assets/{Manga-HydR-L7o.js → Manga-ChRCExhK.js} +1 -1
- package/client/dist/assets/{MangaInfoPage-CBakEI9a.js → MangaInfoPage-DSHJPo2A.js} +1 -1
- package/client/dist/assets/{MangaReadPage-DwAR3iPy.js → MangaReadPage-CzOVY1wA.js} +1 -1
- package/client/dist/assets/{MangaResetProgressModal-DGzl8vsO.js → MangaResetProgressModal-CJfzdUBb.js} +1 -1
- package/client/dist/assets/{Mature-Pi0TxKuF.js → Mature-CifQMfhs.js} +1 -1
- package/client/dist/assets/{MediaCard--cuFxOCS.js → MediaCard-CM10yB9V.js} +1 -1
- package/client/dist/assets/{OptionListMenu-B3D39dGS.js → OptionListMenu-6Kovbsqw.js} +1 -1
- package/client/dist/assets/{Player-CsH4x_I_.js → Player-CK0hT-u4.js} +2 -2
- package/client/dist/assets/{PlayerSettings-DGxBsi3F.js → PlayerSettings-Bb66uqT9.js} +1 -1
- package/client/dist/assets/{QueueOptionsButton-Djq4Cpex.js → QueueOptionsButton-C_nTj8_q.js} +1 -1
- package/client/dist/assets/{QueueRail-DOrYgNVX.js → QueueRail-DUK7URAE.js} +1 -1
- package/client/dist/assets/{ReadingList-DwYoBrNk.js → ReadingList-Ck9LJThU.js} +1 -1
- package/client/dist/assets/{ResetProgressModal-CMF86mX6.js → ResetProgressModal-BUphIv0k.js} +1 -1
- package/client/dist/assets/{Search-DFkhuk-x.js → Search-zJyP15h6.js} +1 -1
- package/client/dist/assets/{Settings-DBB2r0VQ.js → Settings-BYdoSEUh.js} +1 -1
- package/client/dist/assets/{SkeletonGrid-DJelJgAv.js → SkeletonGrid-CqgbOhjX.js} +1 -1
- package/client/dist/assets/{Trackers-DGYXFhYZ.js → Trackers-BVjyv6CM.js} +1 -1
- package/client/dist/assets/{Tv-C5MDGD2Z.js → Tv-L13ylswR.js} +1 -1
- package/client/dist/assets/{TvInfo-DrKuK13u.js → TvInfo-Cvcm5U2D.js} +1 -1
- package/client/dist/assets/{TvPopup-DtHilRDT.js → TvPopup-DhxfKvYm.js} +1 -1
- package/client/dist/assets/{TvSearch-CzNxdu_w.js → TvSearch-P8bFE6_X.js} +1 -1
- package/client/dist/assets/{TvWatchlist-D7p4br6S.js → TvWatchlist-D5gVPjtb.js} +1 -1
- package/client/dist/assets/{Watchlist-DrOzswl2.js → Watchlist-Cjq8l3rU.js} +1 -1
- package/client/dist/assets/{hls-BGCX7mJy.js → hls-BkzuANAg.js} +1 -1
- package/client/dist/assets/{index-CnfZwsds.js → index-CLVgzKLk.js} +19 -19
- package/client/dist/assets/{useAnimeInfoData-DYXWBPF-.js → useAnimeInfoData-DZJZu-so.js} +1 -1
- package/client/dist/assets/{useAsmr-BbhQ59bk.js → useAsmr-CvrMGHuo.js} +1 -1
- package/client/dist/assets/useAsmrPopup-Bimy8Ouw.js +1 -0
- package/client/dist/assets/{useMangaLibrary-DuesWQaE.js → useMangaLibrary--MiN11uR.js} +1 -1
- package/client/dist/assets/{useMangaPopup-D57OVGUr.js → useMangaPopup-CR5XhKKS.js} +1 -1
- package/client/dist/assets/{useProviders-mpQb_dix.js → useProviders-DqdT4fA5.js} +1 -1
- package/client/dist/assets/{useTv-DGflCsSI.js → useTv-BL0HMDyP.js} +1 -1
- package/client/dist/assets/{useTvLibrary-BqeSmyc4.js → useTvLibrary-D4U4iqN8.js} +1 -1
- package/client/dist/index.html +1 -1
- package/package.json +3 -5
- package/server/dist/config.js +1 -174
- package/server/dist/server.js +1068 -289
- package/server/dist/shipped-defaults.js +1 -6
- package/server/package.json +4 -5
- package/client/dist/assets/Asmr.module-CdWlioTY.js +0 -1
- package/client/dist/assets/ListeningList-DguKjI_t.js +0 -1
- package/client/dist/assets/Music-BGH_0voi.css +0 -1
- package/client/dist/assets/Music-CQpyXTLH.js +0 -1
- package/client/dist/assets/Radio-CCHNFN7R.css +0 -1
- package/client/dist/assets/Radio-DF6g6odd.js +0 -1
- package/client/dist/assets/Radio.module-DGSZLpKM.js +0 -1
- package/client/dist/assets/useAsmrPopup-SymV1J-b.js +0 -1
- package/server/dist/app-auth.js +0 -157
- package/server/dist/app-hono.js +0 -598
- package/server/dist/db/drizzle.js +0 -16
- package/server/dist/db/schema-anime.js +0 -61
- package/server/dist/db/schema-asmr.js +0 -13
- package/server/dist/db/schema-manga.js +0 -31
- package/server/dist/db/schema-tv.js +0 -18
- package/server/dist/db.js +0 -217
- package/server/dist/discord-gateway.js +0 -385
- package/server/dist/discord-rpc.js +0 -413
- package/server/dist/github-sync.js +0 -508
- package/server/dist/google.js +0 -741
- package/server/dist/hono/asmr.js +0 -10
- package/server/dist/hono/auth.js +0 -49
- package/server/dist/hono/music.js +0 -209
- package/server/dist/hono/proxy.js +0 -827
- package/server/dist/hono/tv.js +0 -200
- package/server/dist/hono/watchlist.js +0 -341
- package/server/dist/lib/anilist.js +0 -1583
- package/server/dist/lib/anischedule.js +0 -191
- package/server/dist/lib/insights.js +0 -185
- package/server/dist/lib/ipc.js +0 -23
- package/server/dist/lib/kitsu.js +0 -517
- package/server/dist/lib/mal-import.js +0 -291
- package/server/dist/lib/mal.js +0 -957
- package/server/dist/lib/migration.js +0 -310
- package/server/dist/lib/offline-db.js +0 -395
- package/server/dist/lib/potoken.js +0 -135
- package/server/dist/lib/recommendations.service.js +0 -216
- package/server/dist/lib/temp-ids.js +0 -3
- package/server/dist/lib/tmdb.js +0 -72
- package/server/dist/lib/tracker/anilist-tracker.js +0 -308
- package/server/dist/lib/tracker/manga-sync.service.js +0 -773
- package/server/dist/lib/tracker/sync-state.js +0 -21
- package/server/dist/lib/tracker/sync.service.js +0 -476
- package/server/dist/lib/translate.js +0 -79
- package/server/dist/lib/ump.js +0 -106
- package/server/dist/lib/ytmusic.js +0 -162
- package/server/dist/logger.js +0 -16
- package/server/dist/providers/manga/manga.types.js +0 -1
- package/server/dist/providers/provider.interface.js +0 -1
- package/server/dist/providers/remote-loader.js +0 -614
- package/server/dist/providers/remote-types.js +0 -159
- package/server/dist/providers/title-matching.js +0 -226
- package/server/dist/providers/tv.types.js +0 -1
- package/server/dist/rclone.js +0 -120
- package/server/dist/repositories/asmr.repository.js +0 -77
- package/server/dist/repositories/insights.repository.js +0 -106
- package/server/dist/repositories/library.repository.js +0 -94
- package/server/dist/repositories/mal-cache.repository.js +0 -52
- package/server/dist/repositories/manga.repository.js +0 -95
- package/server/dist/repositories/notifications.repository.js +0 -60
- package/server/dist/repositories/queue.repository.js +0 -72
- package/server/dist/repositories/settings.repository.js +0 -30
- package/server/dist/repositories/shows-meta.repository.js +0 -58
- package/server/dist/repositories/temp-show-ids.repository.js +0 -63
- package/server/dist/repositories/tv.repository.js +0 -93
- package/server/dist/repositories/watched-episodes.repository.js +0 -82
- package/server/dist/repositories/watchlist.repository.js +0 -60
- package/server/dist/scripts/backfill-episode-duration.js +0 -42
- package/server/dist/sync-payload.js +0 -91
- package/server/dist/sync.js +0 -657
- package/server/dist/trpc/context.js +0 -7
- package/server/dist/trpc/errors.js +0 -15
- package/server/dist/trpc/index.js +0 -18
- package/server/dist/trpc/router.js +0 -49
- package/server/dist/trpc/routers/asmr-library.js +0 -112
- package/server/dist/trpc/routers/asmr-progress.js +0 -121
- package/server/dist/trpc/routers/asmr.js +0 -87
- package/server/dist/trpc/routers/auth.js +0 -194
- package/server/dist/trpc/routers/continue-watching.js +0 -81
- package/server/dist/trpc/routers/data.js +0 -1026
- package/server/dist/trpc/routers/discord.js +0 -285
- package/server/dist/trpc/routers/discovery.js +0 -11
- package/server/dist/trpc/routers/insights.js +0 -14
- package/server/dist/trpc/routers/manga-library.js +0 -263
- package/server/dist/trpc/routers/manga-progress.js +0 -169
- package/server/dist/trpc/routers/manga.js +0 -201
- package/server/dist/trpc/routers/music.js +0 -655
- package/server/dist/trpc/routers/notifications.js +0 -80
- package/server/dist/trpc/routers/progress.js +0 -109
- package/server/dist/trpc/routers/queue.js +0 -93
- package/server/dist/trpc/routers/radio.js +0 -145
- package/server/dist/trpc/routers/settings.js +0 -96
- package/server/dist/trpc/routers/tracker.js +0 -213
- package/server/dist/trpc/routers/tv-library.js +0 -135
- package/server/dist/trpc/routers/tv-progress.js +0 -159
- package/server/dist/trpc/routers/tv.js +0 -551
- package/server/dist/trpc/routers/watchlist.js +0 -550
- package/server/dist/trpc/validation.js +0 -258
- package/server/dist/utils/cache.utils.js +0 -25
- package/server/dist/utils/cookie.utils.js +0 -13
- package/server/dist/utils/db-utils.js +0 -17
- package/server/dist/utils/env.utils.js +0 -77
- package/server/dist/utils/http.utils.js +0 -82
- package/server/dist/utils/request-context.js +0 -2
- package/server/dist/utils/security.utils.js +0 -76
- /package/client/dist/assets/{Asmr-D1KuQwQy.css → useAsmrPopup-D1KuQwQy.css} +0 -0
package/server/dist/app-hono.js
DELETED
|
@@ -1,598 +0,0 @@
|
|
|
1
|
-
import { Hono } from 'hono';
|
|
2
|
-
import { compress } from 'hono/compress';
|
|
3
|
-
import { getConnInfo } from '@hono/node-server/conninfo';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
import fs from 'fs';
|
|
6
|
-
import { XMLParser } from 'fast-xml-parser';
|
|
7
|
-
import { CONFIG } from './config.js';
|
|
8
|
-
import { isAllowedOrigin } from './utils/security.utils.js';
|
|
9
|
-
import { requestContext } from './utils/request-context.js';
|
|
10
|
-
import logger from './logger.js';
|
|
11
|
-
import { LAN_AUTH_PUBLIC_PATHS, buildLanCookie, clearAllLanSessions, clearLanCookie, createLanSession, getTokenFromHeaders, hasAppPassword, isLoopbackIp, revokeLanSession, setAppPassword, validateLanSession, verifyAppPassword, } from './app-auth.js';
|
|
12
|
-
import { importMangaFromMalXmlItems, } from './lib/tracker/manga-sync.service.js';
|
|
13
|
-
import { LibraryRepository } from './repositories/library.repository.js';
|
|
14
|
-
import { performAsmrWriteTransactionAsync, performMangaWriteTransactionAsync, performTvWriteTransactionAsync, performWriteTransactionAsync, KIND_SYNC, setKindManifestVersion, } from './sync.js';
|
|
15
|
-
import { ANIME_SYNC_TABLES, ASMR_SYNC_TABLES, MANGA_SYNC_TABLES, TV_SYNC_TABLES, exportTables, importTables, normalizePayload, readPayloadVersion, } from './sync-payload.js';
|
|
16
|
-
import { getMalImportStatus, prepareMalImport, executeMalImport } from './lib/mal-import.js';
|
|
17
|
-
import { registerMusic } from './hono/music.js';
|
|
18
|
-
import { registerAuth } from './hono/auth.js';
|
|
19
|
-
import { registerTv } from './hono/tv.js';
|
|
20
|
-
import { registerProxy } from './hono/proxy.js';
|
|
21
|
-
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
|
|
22
|
-
import { appRouter } from './trpc/router.js';
|
|
23
|
-
import { buildTrpcContext } from './trpc/context.js';
|
|
24
|
-
const malMangaXmlParser = new XMLParser({
|
|
25
|
-
isArray: (name) => name === 'manga',
|
|
26
|
-
parseTagValue: false,
|
|
27
|
-
trimValues: true,
|
|
28
|
-
});
|
|
29
|
-
function mangaXmlText(value) {
|
|
30
|
-
if (Array.isArray(value))
|
|
31
|
-
return String(value[0] ?? '');
|
|
32
|
-
return String(value ?? '');
|
|
33
|
-
}
|
|
34
|
-
function mapMalMangaXmlStatus(status) {
|
|
35
|
-
switch (status) {
|
|
36
|
-
case 'Plan to Read':
|
|
37
|
-
return 'Planned';
|
|
38
|
-
case 'On Hold':
|
|
39
|
-
return 'On-Hold';
|
|
40
|
-
case 'Reading':
|
|
41
|
-
case 'Completed':
|
|
42
|
-
case 'Dropped':
|
|
43
|
-
return status;
|
|
44
|
-
default:
|
|
45
|
-
return 'Planned';
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
export function getHonoClientIp(c) {
|
|
49
|
-
try {
|
|
50
|
-
return getConnInfo(c).remote.address;
|
|
51
|
-
}
|
|
52
|
-
catch {
|
|
53
|
-
return c.req.header('x-dango-client-ip') ?? undefined;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
const NOT_READY_HEADER = 'x-dango-not-ready';
|
|
57
|
-
function notReady(c, body) {
|
|
58
|
-
c.header(NOT_READY_HEADER, '1');
|
|
59
|
-
return typeof body === 'string' ? c.text(body, 503) : c.json(body, 503);
|
|
60
|
-
}
|
|
61
|
-
export async function honoRequestLogger(c, next) {
|
|
62
|
-
const start = process.hrtime.bigint();
|
|
63
|
-
const url = new URL(c.req.url);
|
|
64
|
-
const loggedUrl = `${url.pathname}${url.search}`;
|
|
65
|
-
await next();
|
|
66
|
-
const ms = Math.round((Number(process.hrtime.bigint() - start) / 1_000_000) * 10) / 10;
|
|
67
|
-
const status = c.res.status;
|
|
68
|
-
const payload = { method: c.req.method, url: loggedUrl, status, ms };
|
|
69
|
-
if (status >= 500 && !c.res.headers.get(NOT_READY_HEADER)) {
|
|
70
|
-
logger.warn(payload, 'request failed');
|
|
71
|
-
}
|
|
72
|
-
else if (!loggedUrl.startsWith('/api/proxy')) {
|
|
73
|
-
logger.trace(`${c.req.method} ${loggedUrl} ${status} ${ms}ms`);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
export async function honoRequestContextMiddleware(c, next) {
|
|
77
|
-
const store = new Map();
|
|
78
|
-
const ua = c.req.header('x-animepahe-ua');
|
|
79
|
-
if (ua)
|
|
80
|
-
store.set('ua', ua);
|
|
81
|
-
const cookie = c.req.header('x-animepahe-cookie');
|
|
82
|
-
if (cookie)
|
|
83
|
-
store.set('cookie', cookie);
|
|
84
|
-
const jasmrUa = c.req.header('x-jasmr-ua');
|
|
85
|
-
if (jasmrUa)
|
|
86
|
-
store.set('jasmr_ua', jasmrUa);
|
|
87
|
-
const jasmrCookie = c.req.header('x-jasmr-cookie');
|
|
88
|
-
if (jasmrCookie)
|
|
89
|
-
store.set('jasmr_cookie', jasmrCookie);
|
|
90
|
-
return requestContext.run(store, () => next());
|
|
91
|
-
}
|
|
92
|
-
async function restoreJsonBackup(c, dbs, buffer) {
|
|
93
|
-
let parsed;
|
|
94
|
-
try {
|
|
95
|
-
parsed = JSON.parse(buffer.toString('utf-8'));
|
|
96
|
-
}
|
|
97
|
-
catch {
|
|
98
|
-
return c.json({ error: 'Invalid backup file.' }, 400);
|
|
99
|
-
}
|
|
100
|
-
const databases = parsed?.databases;
|
|
101
|
-
if (!databases || typeof databases !== 'object') {
|
|
102
|
-
return c.json({ error: 'Invalid backup file: missing databases.' }, 400);
|
|
103
|
-
}
|
|
104
|
-
const kindDbs = {
|
|
105
|
-
anime: dbs.db,
|
|
106
|
-
manga: dbs.mangaDb,
|
|
107
|
-
tv: dbs.tvDb,
|
|
108
|
-
asmr: dbs.asmrDb,
|
|
109
|
-
};
|
|
110
|
-
const targets = Object.keys(KIND_SYNC).map((kind) => ({
|
|
111
|
-
key: kind,
|
|
112
|
-
db: kindDbs[kind],
|
|
113
|
-
tables: KIND_SYNC[kind].tables,
|
|
114
|
-
libraryTables: KIND_SYNC[kind].libraryTables,
|
|
115
|
-
backupName: KIND_SYNC[kind].backupName,
|
|
116
|
-
setVersion: (version) => setKindManifestVersion(kind, version),
|
|
117
|
-
}));
|
|
118
|
-
const present = targets.filter((t) => databases[t.key] !== undefined);
|
|
119
|
-
if (present.length === 0) {
|
|
120
|
-
return c.json({ error: 'Invalid backup file: no databases found.' }, 400);
|
|
121
|
-
}
|
|
122
|
-
const sections = new Map();
|
|
123
|
-
try {
|
|
124
|
-
for (const target of present) {
|
|
125
|
-
sections.set(target.key, normalizePayload(databases[target.key], target.tables, `backup ${target.key}`));
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
catch (err) {
|
|
129
|
-
logger.warn({ err }, 'Multi-database restore rejected: invalid payload');
|
|
130
|
-
return c.json({ error: 'Invalid backup file.' }, 400);
|
|
131
|
-
}
|
|
132
|
-
const restored = [];
|
|
133
|
-
try {
|
|
134
|
-
for (const target of present) {
|
|
135
|
-
const section = sections.get(target.key);
|
|
136
|
-
if (!section)
|
|
137
|
-
continue;
|
|
138
|
-
importTables(target.db, target.tables, section, {
|
|
139
|
-
libraryTables: target.libraryTables,
|
|
140
|
-
backupName: target.backupName,
|
|
141
|
-
});
|
|
142
|
-
await target.setVersion(readPayloadVersion(section));
|
|
143
|
-
restored.push(target.key);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
catch (err) {
|
|
147
|
-
logger.error({ err, restored }, 'Multi-database restore failed partway');
|
|
148
|
-
return c.json({ error: 'Restore failed partway.', restored }, 500);
|
|
149
|
-
}
|
|
150
|
-
logger.warn({ restored }, 'Databases restored from backup file by user request');
|
|
151
|
-
return c.json({ success: true, message: 'Databases restored.', restored });
|
|
152
|
-
}
|
|
153
|
-
function restoreLegacyDatabase(c, dbs, buffer, dbAdmin) {
|
|
154
|
-
const dbName = CONFIG.IS_DEV ? CONFIG.DB_NAME_DEV : CONFIG.DB_NAME_PROD;
|
|
155
|
-
const tempPath = path.join(CONFIG.ROOT, 'restore_temp.db');
|
|
156
|
-
const dbPath = path.join(CONFIG.ROOT, dbName);
|
|
157
|
-
try {
|
|
158
|
-
fs.writeFileSync(tempPath, buffer);
|
|
159
|
-
}
|
|
160
|
-
catch (err) {
|
|
161
|
-
logger.error({ err }, 'Failed to stage legacy database restore file');
|
|
162
|
-
return Promise.resolve(c.json({ error: 'Failed to stage restore file.' }, 500));
|
|
163
|
-
}
|
|
164
|
-
const db = dbs.db;
|
|
165
|
-
return new Promise((resolve) => {
|
|
166
|
-
db.close((closeErr) => {
|
|
167
|
-
if (closeErr) {
|
|
168
|
-
resolve(c.json({ error: 'Failed to close database.' }, 500));
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
try {
|
|
172
|
-
db.checkpoint();
|
|
173
|
-
}
|
|
174
|
-
catch (checkpointErr) {
|
|
175
|
-
logger.warn({ err: checkpointErr }, 'WAL checkpoint failed');
|
|
176
|
-
}
|
|
177
|
-
try {
|
|
178
|
-
if (fs.existsSync(`${dbPath}-wal`))
|
|
179
|
-
fs.unlinkSync(`${dbPath}-wal`);
|
|
180
|
-
if (fs.existsSync(`${dbPath}-shm`))
|
|
181
|
-
fs.unlinkSync(`${dbPath}-shm`);
|
|
182
|
-
}
|
|
183
|
-
catch (cleanupErr) {
|
|
184
|
-
logger.warn({ err: cleanupErr }, 'Failed to clean up WAL files');
|
|
185
|
-
}
|
|
186
|
-
fs.rename(tempPath, dbPath, async (renameErr) => {
|
|
187
|
-
if (renameErr) {
|
|
188
|
-
try {
|
|
189
|
-
dbAdmin.setDb(await dbAdmin.initializeDatabase(dbPath));
|
|
190
|
-
}
|
|
191
|
-
catch (e) {
|
|
192
|
-
logger.error({ err: e }, 'Failed to reopen DB after rename failure');
|
|
193
|
-
}
|
|
194
|
-
resolve(c.json({ error: 'Failed to replace database file.' }, 500));
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
try {
|
|
198
|
-
dbAdmin.setDb(await dbAdmin.initializeDatabase(dbPath));
|
|
199
|
-
resolve(c.json({ success: true, message: 'Database restored.' }));
|
|
200
|
-
}
|
|
201
|
-
catch (e) {
|
|
202
|
-
logger.error({ err: e }, 'Failed to initialize restored database');
|
|
203
|
-
resolve(c.json({ error: 'Failed to initialize restored database.' }, 500));
|
|
204
|
-
}
|
|
205
|
-
});
|
|
206
|
-
});
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
export function createHonoApp(getState, getGuardState, getDbs, dbAdmin, providerApi, runSync, media) {
|
|
210
|
-
const app = new Hono();
|
|
211
|
-
app.use(honoRequestLogger);
|
|
212
|
-
app.use(honoRequestContextMiddleware);
|
|
213
|
-
app.get('/api/health', (c) => {
|
|
214
|
-
const state = getState();
|
|
215
|
-
if (state.shuttingDown) {
|
|
216
|
-
return notReady(c, { status: 'shutting-down', ready: false });
|
|
217
|
-
}
|
|
218
|
-
if (!state.dbReady) {
|
|
219
|
-
return notReady(c, { status: 'starting', ready: false });
|
|
220
|
-
}
|
|
221
|
-
return c.json({ status: 'ok', ready: true });
|
|
222
|
-
});
|
|
223
|
-
app.use(async (c, next) => {
|
|
224
|
-
const guard = getGuardState();
|
|
225
|
-
if (guard.shuttingDown) {
|
|
226
|
-
return notReady(c, 'Server is shutting down...');
|
|
227
|
-
}
|
|
228
|
-
if (!guard.dbsReady) {
|
|
229
|
-
return notReady(c, 'Database initializing...');
|
|
230
|
-
}
|
|
231
|
-
if (guard.bootSyncing &&
|
|
232
|
-
c.req.method !== 'GET' &&
|
|
233
|
-
c.req.method !== 'HEAD' &&
|
|
234
|
-
c.req.method !== 'OPTIONS') {
|
|
235
|
-
return notReady(c, 'Sync in progress...');
|
|
236
|
-
}
|
|
237
|
-
await next();
|
|
238
|
-
});
|
|
239
|
-
const honoCompress = compress({ threshold: 1024 });
|
|
240
|
-
app.use(async (c, next) => {
|
|
241
|
-
if (c.req.header('x-no-compression')) {
|
|
242
|
-
await next();
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
await honoCompress(c, next);
|
|
246
|
-
});
|
|
247
|
-
app.use(async (c, next) => {
|
|
248
|
-
const origin = c.req.header('origin');
|
|
249
|
-
if (c.req.method === 'OPTIONS') {
|
|
250
|
-
if (!origin || !isAllowedOrigin(origin)) {
|
|
251
|
-
await next();
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
const reqHeaders = c.req.header('access-control-request-headers');
|
|
255
|
-
return new Response(null, {
|
|
256
|
-
status: 204,
|
|
257
|
-
headers: {
|
|
258
|
-
'Access-Control-Allow-Origin': origin,
|
|
259
|
-
'Access-Control-Allow-Credentials': 'true',
|
|
260
|
-
'Access-Control-Allow-Methods': 'GET,HEAD,PUT,PATCH,POST,DELETE',
|
|
261
|
-
...(reqHeaders ? { 'Access-Control-Allow-Headers': reqHeaders } : {}),
|
|
262
|
-
Vary: 'Origin',
|
|
263
|
-
},
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
if (!origin || isAllowedOrigin(origin)) {
|
|
267
|
-
if (origin)
|
|
268
|
-
c.header('Access-Control-Allow-Origin', origin);
|
|
269
|
-
c.header('Access-Control-Allow-Credentials', 'true');
|
|
270
|
-
c.header('Vary', 'Origin');
|
|
271
|
-
}
|
|
272
|
-
await next();
|
|
273
|
-
});
|
|
274
|
-
app.use(async (c, next) => {
|
|
275
|
-
const method = c.req.method.toUpperCase();
|
|
276
|
-
if (!['POST', 'PUT', 'DELETE', 'PATCH'].includes(method)) {
|
|
277
|
-
await next();
|
|
278
|
-
return;
|
|
279
|
-
}
|
|
280
|
-
const secFetchSite = c.req.header('sec-fetch-site');
|
|
281
|
-
if (secFetchSite === 'cross-site') {
|
|
282
|
-
const origin = c.req.header('origin');
|
|
283
|
-
if (!isAllowedOrigin(origin)) {
|
|
284
|
-
return c.json({ error: 'Forbidden: cross-site request rejected' }, 403);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
const origin = c.req.header('origin');
|
|
288
|
-
if (origin && !isAllowedOrigin(origin)) {
|
|
289
|
-
return c.json({ error: 'Forbidden: cross-origin request rejected' }, 403);
|
|
290
|
-
}
|
|
291
|
-
await next();
|
|
292
|
-
});
|
|
293
|
-
app.get('/api/auth/app-status', (c) => {
|
|
294
|
-
const authed = !hasAppPassword() ||
|
|
295
|
-
validateLanSession(getTokenFromHeaders(c.req.header('authorization'), c.req.header('cookie')));
|
|
296
|
-
return c.json({ hasPassword: hasAppPassword(), isAuthenticated: authed, port: CONFIG.PORT });
|
|
297
|
-
});
|
|
298
|
-
app.post('/api/auth/app-login', async (c) => {
|
|
299
|
-
const body = (await c.req.json().catch(() => undefined));
|
|
300
|
-
const password = String(body?.password || '');
|
|
301
|
-
if (!hasAppPassword()) {
|
|
302
|
-
return c.json({ success: true });
|
|
303
|
-
}
|
|
304
|
-
if (!password) {
|
|
305
|
-
return c.json({ error: 'Password required' }, 400);
|
|
306
|
-
}
|
|
307
|
-
if (!verifyAppPassword(password)) {
|
|
308
|
-
return c.json({ error: 'Invalid password' }, 401);
|
|
309
|
-
}
|
|
310
|
-
const session = createLanSession();
|
|
311
|
-
c.header('Set-Cookie', buildLanCookie(session.token, session.expiry));
|
|
312
|
-
return c.json({ success: true });
|
|
313
|
-
});
|
|
314
|
-
app.post('/api/auth/app-logout', (c) => {
|
|
315
|
-
revokeLanSession(getTokenFromHeaders(c.req.header('authorization'), c.req.header('cookie')));
|
|
316
|
-
c.header('Set-Cookie', clearLanCookie());
|
|
317
|
-
return c.json({ success: true });
|
|
318
|
-
});
|
|
319
|
-
app.post('/api/auth/app-setup', async (c) => {
|
|
320
|
-
if (!isLoopbackIp(c.req.header('x-dango-client-ip'))) {
|
|
321
|
-
return c.json({ error: 'Forbidden' }, 403);
|
|
322
|
-
}
|
|
323
|
-
const body = (await c.req.json().catch(() => undefined));
|
|
324
|
-
const password = String(body?.password || '');
|
|
325
|
-
try {
|
|
326
|
-
await setAppPassword(password);
|
|
327
|
-
clearAllLanSessions();
|
|
328
|
-
return c.json({ success: true, hasPassword: hasAppPassword() });
|
|
329
|
-
}
|
|
330
|
-
catch {
|
|
331
|
-
return c.json({ error: 'Failed to update password' }, 500);
|
|
332
|
-
}
|
|
333
|
-
});
|
|
334
|
-
app.use(async (c, next) => {
|
|
335
|
-
const path = c.req.path;
|
|
336
|
-
if (!path.startsWith('/api/')) {
|
|
337
|
-
await next();
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
|
-
if (LAN_AUTH_PUBLIC_PATHS.has(path)) {
|
|
341
|
-
await next();
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
344
|
-
if (path.startsWith('/api/internal/') && isLoopbackIp(getHonoClientIp(c))) {
|
|
345
|
-
await next();
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
if (!hasAppPassword()) {
|
|
349
|
-
await next();
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
if (validateLanSession(getTokenFromHeaders(c.req.header('authorization'), c.req.header('cookie')))) {
|
|
353
|
-
await next();
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
return c.json({ error: 'LAN_AUTH_REQUIRED' }, 401);
|
|
357
|
-
});
|
|
358
|
-
app.all('/api/trpc/*', (c) => fetchRequestHandler({
|
|
359
|
-
endpoint: '/api/trpc',
|
|
360
|
-
req: c.req.raw,
|
|
361
|
-
router: appRouter,
|
|
362
|
-
createContext: () => buildTrpcContext({
|
|
363
|
-
...getDbs(),
|
|
364
|
-
apiCache: media.getApiCache(),
|
|
365
|
-
getTvProvider: media.getTvProvider,
|
|
366
|
-
getMangaProvider: media.getMangaProvider,
|
|
367
|
-
getJasmr: media.getJasmr,
|
|
368
|
-
getProviders: media.getProviders,
|
|
369
|
-
getCatalog: providerApi.getCatalog,
|
|
370
|
-
}, c.req.header('authorization'), c.req.header('cookie')),
|
|
371
|
-
}));
|
|
372
|
-
app.post('/api/import/mal-xml', async (c) => {
|
|
373
|
-
const dbs = getDbs();
|
|
374
|
-
const fields = (await c.req.parseBody().catch(() => ({})));
|
|
375
|
-
const file = fields['xmlfile'];
|
|
376
|
-
const buffer = file instanceof File ? Buffer.from(await file.arrayBuffer()) : undefined;
|
|
377
|
-
const prepared = prepareMalImport(buffer, {
|
|
378
|
-
erase: fields['erase'],
|
|
379
|
-
useOfflineDb: fields['useOfflineDb'],
|
|
380
|
-
skipFallback: fields['skipFallback'],
|
|
381
|
-
});
|
|
382
|
-
if (!prepared.ok)
|
|
383
|
-
return c.json(prepared.body, prepared.status);
|
|
384
|
-
const stream = new TransformStream();
|
|
385
|
-
const writer = stream.writable.getWriter();
|
|
386
|
-
const encoder = new TextEncoder();
|
|
387
|
-
let disconnected = false;
|
|
388
|
-
c.req.raw.signal.addEventListener('abort', () => {
|
|
389
|
-
disconnected = true;
|
|
390
|
-
}, { once: true });
|
|
391
|
-
const sendEvent = (event, data) => {
|
|
392
|
-
if (disconnected)
|
|
393
|
-
return;
|
|
394
|
-
void writer
|
|
395
|
-
.write(encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`))
|
|
396
|
-
.catch(() => { });
|
|
397
|
-
};
|
|
398
|
-
void executeMalImport(dbs.db, prepared.import, sendEvent).finally(() => {
|
|
399
|
-
void writer.close().catch(() => { });
|
|
400
|
-
});
|
|
401
|
-
return new Response(stream.readable, {
|
|
402
|
-
headers: {
|
|
403
|
-
'Content-Type': 'text/event-stream',
|
|
404
|
-
'Cache-Control': 'no-cache',
|
|
405
|
-
Connection: 'keep-alive',
|
|
406
|
-
},
|
|
407
|
-
});
|
|
408
|
-
});
|
|
409
|
-
app.get('/api/providers', (c) => {
|
|
410
|
-
c.header('Cache-Control', 'public, max-age=300');
|
|
411
|
-
return c.json(providerApi.getCatalog());
|
|
412
|
-
});
|
|
413
|
-
app.post('/api/providers/refresh', async (c) => {
|
|
414
|
-
try {
|
|
415
|
-
return c.json(await providerApi.refresh());
|
|
416
|
-
}
|
|
417
|
-
catch (err) {
|
|
418
|
-
return c.json({ error: err.message || 'refresh failed' }, 500);
|
|
419
|
-
}
|
|
420
|
-
});
|
|
421
|
-
app.get('/api/tracker/anilist/callback', (c) => {
|
|
422
|
-
return c.html(`<!doctype html>
|
|
423
|
-
<html><head><meta charset="utf-8"><title>AniList — completing login</title></head>
|
|
424
|
-
<body style="font-family:system-ui;padding:24px;background:#0b1426;color:#e5eefc"><p>Completing AniList login…</p>
|
|
425
|
-
<script>
|
|
426
|
-
(function(){
|
|
427
|
-
var qs = new URLSearchParams(location.search);
|
|
428
|
-
var state = qs.get('state');
|
|
429
|
-
var hash = location.hash || '';
|
|
430
|
-
var hp = new URLSearchParams(hash.slice(1));
|
|
431
|
-
if (!state) state = hp.get('state');
|
|
432
|
-
var token = hp.get('access_token');
|
|
433
|
-
var error = hp.get('error') || qs.get('error');
|
|
434
|
-
var frontend = location.origin + '/trackers';
|
|
435
|
-
if (state) {
|
|
436
|
-
try {
|
|
437
|
-
var candidate = decodeURIComponent(state);
|
|
438
|
-
if (candidate.startsWith('/') && !candidate.startsWith('//')) {
|
|
439
|
-
frontend = location.origin + candidate;
|
|
440
|
-
} else {
|
|
441
|
-
var parsed = new URL(candidate, location.origin);
|
|
442
|
-
if (parsed.origin === location.origin) {
|
|
443
|
-
frontend = parsed.href;
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
} catch(e) {}
|
|
447
|
-
}
|
|
448
|
-
if (frontend.indexOf('/api/tracker/anilist/callback') !== -1) {
|
|
449
|
-
frontend = location.origin + '/trackers';
|
|
450
|
-
}
|
|
451
|
-
if (error) {
|
|
452
|
-
location.replace(frontend + (frontend.indexOf('?') !== -1 ? '&' : '?') + 'anilist=error&reason=' + encodeURIComponent(error));
|
|
453
|
-
return;
|
|
454
|
-
}
|
|
455
|
-
if (token) {
|
|
456
|
-
location.replace(frontend + hash);
|
|
457
|
-
return;
|
|
458
|
-
}
|
|
459
|
-
var code = qs.get('code');
|
|
460
|
-
if (code) {
|
|
461
|
-
location.replace(frontend + (frontend.indexOf('?') !== -1 ? '&' : '?') + 'anilist=error&reason=code_flow_removed');
|
|
462
|
-
return;
|
|
463
|
-
}
|
|
464
|
-
location.replace(frontend + (frontend.indexOf('?') !== -1 ? '&' : '?') + 'anilist=error&reason=no_token');
|
|
465
|
-
})();
|
|
466
|
-
</script></body></html>`);
|
|
467
|
-
});
|
|
468
|
-
app.post('/api/import/mal-xml-manga', async (c) => {
|
|
469
|
-
const fields = (await c.req.parseBody().catch(() => ({})));
|
|
470
|
-
const file = fields['xmlfile'];
|
|
471
|
-
if (!(file instanceof File))
|
|
472
|
-
return c.json({ error: 'No file' }, 400);
|
|
473
|
-
const eraseRaw = fields['erase'];
|
|
474
|
-
const erase = eraseRaw === true || eraseRaw === 'true' || eraseRaw === '1';
|
|
475
|
-
try {
|
|
476
|
-
const dbs = getDbs();
|
|
477
|
-
if (!dbs.mangaDb || dbs.mangaDb.isClosedCheck()) {
|
|
478
|
-
return notReady(c, { error: 'Manga database is not ready' });
|
|
479
|
-
}
|
|
480
|
-
let result;
|
|
481
|
-
try {
|
|
482
|
-
result = malMangaXmlParser.parse(Buffer.from(await file.arrayBuffer()).toString());
|
|
483
|
-
}
|
|
484
|
-
catch {
|
|
485
|
-
return c.json({ error: 'Invalid XML' }, 400);
|
|
486
|
-
}
|
|
487
|
-
const mangaList = result?.myanimelist?.manga || [];
|
|
488
|
-
if (mangaList.length === 0) {
|
|
489
|
-
return c.json({ error: 'No manga found in XML' }, 400);
|
|
490
|
-
}
|
|
491
|
-
const items = mangaList.map((item) => ({
|
|
492
|
-
malId: parseInt(mangaXmlText(item.series_mangadb_id), 10) || 0,
|
|
493
|
-
title: mangaXmlText(item.series_title),
|
|
494
|
-
status: mapMalMangaXmlStatus(mangaXmlText(item.my_status)),
|
|
495
|
-
chapters: Math.max(parseInt(mangaXmlText(item.my_read_chapters), 10) || 0, 0),
|
|
496
|
-
}));
|
|
497
|
-
const { imported, skipped } = await importMangaFromMalXmlItems(dbs.db, dbs.mangaDb, items, erase);
|
|
498
|
-
return c.json({ success: true, imported, skipped });
|
|
499
|
-
}
|
|
500
|
-
catch (err) {
|
|
501
|
-
const message = err instanceof Error ? err.message : 'Import failed';
|
|
502
|
-
return c.json({ error: message }, 500);
|
|
503
|
-
}
|
|
504
|
-
});
|
|
505
|
-
app.get('/api/backup-db', (c) => {
|
|
506
|
-
try {
|
|
507
|
-
const dbs = getDbs();
|
|
508
|
-
const payload = {
|
|
509
|
-
version: 1,
|
|
510
|
-
exportedAt: new Date().toISOString(),
|
|
511
|
-
databases: {
|
|
512
|
-
anime: exportTables(dbs.db, ANIME_SYNC_TABLES),
|
|
513
|
-
manga: exportTables(dbs.mangaDb, MANGA_SYNC_TABLES),
|
|
514
|
-
tv: exportTables(dbs.tvDb, TV_SYNC_TABLES),
|
|
515
|
-
asmr: exportTables(dbs.asmrDb, ASMR_SYNC_TABLES),
|
|
516
|
-
},
|
|
517
|
-
};
|
|
518
|
-
c.header('Content-Disposition', 'attachment; filename="dango-backup.json"');
|
|
519
|
-
return c.json(payload);
|
|
520
|
-
}
|
|
521
|
-
catch (err) {
|
|
522
|
-
logger.error({ err }, 'Multi-database backup failed');
|
|
523
|
-
return c.json({ error: 'Backup failed' }, 500);
|
|
524
|
-
}
|
|
525
|
-
});
|
|
526
|
-
app.post('/api/database/clear', async (c) => {
|
|
527
|
-
const body = (await c.req.json().catch(() => undefined));
|
|
528
|
-
if (body?.confirm !== true) {
|
|
529
|
-
return c.json({ error: 'Confirmation required' }, 400);
|
|
530
|
-
}
|
|
531
|
-
if (getMalImportStatus().running) {
|
|
532
|
-
return c.json({ error: 'An import is in progress' }, 409);
|
|
533
|
-
}
|
|
534
|
-
try {
|
|
535
|
-
const dbs = getDbs();
|
|
536
|
-
const before = await LibraryRepository.countAll(dbs.db);
|
|
537
|
-
const beforeManga = await LibraryRepository.countManga(dbs.mangaDb);
|
|
538
|
-
const beforeTv = await LibraryRepository.countTv(dbs.tvDb);
|
|
539
|
-
const beforeAsmr = await LibraryRepository.countAsmr(dbs.asmrDb);
|
|
540
|
-
await performWriteTransactionAsync(dbs.db, async (tx) => {
|
|
541
|
-
await LibraryRepository.clearAll(tx);
|
|
542
|
-
});
|
|
543
|
-
await performMangaWriteTransactionAsync(dbs.mangaDb, async (tx) => {
|
|
544
|
-
await LibraryRepository.clearManga(tx);
|
|
545
|
-
});
|
|
546
|
-
await performTvWriteTransactionAsync(dbs.tvDb, async (tx) => {
|
|
547
|
-
await LibraryRepository.clearTv(tx);
|
|
548
|
-
});
|
|
549
|
-
await performAsmrWriteTransactionAsync(dbs.asmrDb, async (tx) => {
|
|
550
|
-
await LibraryRepository.clearAsmr(tx);
|
|
551
|
-
});
|
|
552
|
-
logger.warn({ before, beforeManga, beforeTv, beforeAsmr }, 'Library database cleared by user request');
|
|
553
|
-
return c.json({
|
|
554
|
-
success: true,
|
|
555
|
-
deleted: before,
|
|
556
|
-
deletedManga: beforeManga,
|
|
557
|
-
deletedTv: beforeTv,
|
|
558
|
-
deletedAsmr: beforeAsmr,
|
|
559
|
-
});
|
|
560
|
-
}
|
|
561
|
-
catch {
|
|
562
|
-
return c.json({ error: 'DB error' }, 500);
|
|
563
|
-
}
|
|
564
|
-
});
|
|
565
|
-
app.post('/api/restore-db', async (c) => {
|
|
566
|
-
const dbs = getDbs();
|
|
567
|
-
let buffer;
|
|
568
|
-
let originalName = '';
|
|
569
|
-
try {
|
|
570
|
-
const body = await c.req.parseBody();
|
|
571
|
-
const file = body['dbfile'];
|
|
572
|
-
if (file instanceof File) {
|
|
573
|
-
buffer = Buffer.from(await file.arrayBuffer());
|
|
574
|
-
originalName = file.name || '';
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
catch {
|
|
578
|
-
return c.json({ error: 'No file uploaded.' }, 400);
|
|
579
|
-
}
|
|
580
|
-
if (!buffer)
|
|
581
|
-
return c.json({ error: 'No file uploaded.' }, 400);
|
|
582
|
-
const head = buffer.slice(0, 16).toString('utf-8').trimStart();
|
|
583
|
-
if (originalName.endsWith('.json') || head.startsWith('{')) {
|
|
584
|
-
return restoreJsonBackup(c, dbs, buffer);
|
|
585
|
-
}
|
|
586
|
-
return restoreLegacyDatabase(c, dbs, buffer, dbAdmin);
|
|
587
|
-
});
|
|
588
|
-
registerMusic(app);
|
|
589
|
-
registerAuth(app, getDbs, runSync);
|
|
590
|
-
registerTv(app, media.getTvProvider);
|
|
591
|
-
registerProxy(app, media.getProxyRules);
|
|
592
|
-
app.onError((err, c) => {
|
|
593
|
-
logger.error({ err, url: c.req.path, method: c.req.method }, 'Unhandled error');
|
|
594
|
-
const status = err.status || 500;
|
|
595
|
-
return c.json({ error: err.message || 'Internal Server Error', status }, status);
|
|
596
|
-
});
|
|
597
|
-
return app;
|
|
598
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { drizzle } from 'drizzle-orm/node-sqlite';
|
|
2
|
-
export function createDrizzle(db) {
|
|
3
|
-
return drizzle({ client: db.getClient() });
|
|
4
|
-
}
|
|
5
|
-
const cache = new WeakMap();
|
|
6
|
-
export function getDrizzle(db) {
|
|
7
|
-
let d = cache.get(db);
|
|
8
|
-
if (!d) {
|
|
9
|
-
d = createDrizzle(db);
|
|
10
|
-
cache.set(db, d);
|
|
11
|
-
}
|
|
12
|
-
return d;
|
|
13
|
-
}
|
|
14
|
-
export async function runTx(db, fn) {
|
|
15
|
-
await db.transact(fn);
|
|
16
|
-
}
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { sql } from 'drizzle-orm';
|
|
2
|
-
import { integer, primaryKey, real, sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
|
3
|
-
export const watchedEpisodes = sqliteTable('watched_episodes', {
|
|
4
|
-
showId: text('showId').notNull(),
|
|
5
|
-
episodeNumber: text('episodeNumber').notNull(),
|
|
6
|
-
watchedAt: text('watchedAt'),
|
|
7
|
-
currentTime: real('currentTime'),
|
|
8
|
-
duration: real('duration'),
|
|
9
|
-
}, (t) => [primaryKey({ columns: [t.showId, t.episodeNumber] })]);
|
|
10
|
-
export const watchlist = sqliteTable('watchlist', {
|
|
11
|
-
id: text('id').notNull(),
|
|
12
|
-
name: text('name'),
|
|
13
|
-
thumbnail: text('thumbnail'),
|
|
14
|
-
status: text('status'),
|
|
15
|
-
nativeName: text('nativeName'),
|
|
16
|
-
englishName: text('englishName'),
|
|
17
|
-
type: text('type'),
|
|
18
|
-
}, (t) => [primaryKey({ columns: [t.id] })]);
|
|
19
|
-
export const showsMeta = sqliteTable('shows_meta', {
|
|
20
|
-
id: text('id').notNull(),
|
|
21
|
-
name: text('name'),
|
|
22
|
-
thumbnail: text('thumbnail'),
|
|
23
|
-
nativeName: text('nativeName'),
|
|
24
|
-
englishName: text('englishName'),
|
|
25
|
-
episodeCount: integer('episodeCount'),
|
|
26
|
-
status: text('status'),
|
|
27
|
-
genres: text('genres'),
|
|
28
|
-
popularityScore: integer('popularityScore'),
|
|
29
|
-
type: text('type'),
|
|
30
|
-
anilistId: integer('anilistId'),
|
|
31
|
-
isAdult: integer('isAdult'),
|
|
32
|
-
episodeDuration: integer('episodeDuration'),
|
|
33
|
-
}, (t) => [primaryKey({ columns: [t.id] })]);
|
|
34
|
-
export const settings = sqliteTable('settings', {
|
|
35
|
-
key: text('key').notNull(),
|
|
36
|
-
value: text('value'),
|
|
37
|
-
}, (t) => [primaryKey({ columns: [t.key] })]);
|
|
38
|
-
export const queue = sqliteTable('queue', {
|
|
39
|
-
id: integer('id'),
|
|
40
|
-
showId: text('showId').notNull(),
|
|
41
|
-
episodeNumber: text('episodeNumber').notNull(),
|
|
42
|
-
queueOrder: integer('queue_order').notNull(),
|
|
43
|
-
});
|
|
44
|
-
export const discoveredNotifications = sqliteTable('discovered_notifications', {
|
|
45
|
-
showId: text('showId').notNull(),
|
|
46
|
-
episodeNumber: text('episodeNumber').notNull(),
|
|
47
|
-
discoveredAt: text('discoveredAt').default(sql `CURRENT_TIMESTAMP`),
|
|
48
|
-
}, (t) => [primaryKey({ columns: [t.showId, t.episodeNumber] })]);
|
|
49
|
-
export const dismissedNotifications = sqliteTable('dismissed_notifications', {
|
|
50
|
-
showId: text('showId').notNull(),
|
|
51
|
-
episodeNumber: text('episodeNumber').notNull(),
|
|
52
|
-
dismissedAt: text('dismissedAt').default(sql `CURRENT_TIMESTAMP`),
|
|
53
|
-
}, (t) => [primaryKey({ columns: [t.showId, t.episodeNumber] })]);
|
|
54
|
-
export const tempShowIds = sqliteTable('temp_show_ids', {
|
|
55
|
-
id: text('id').notNull(),
|
|
56
|
-
provider: text('provider').notNull(),
|
|
57
|
-
nativeId: text('nativeId').notNull(),
|
|
58
|
-
title: text('title').notNull(),
|
|
59
|
-
thumbnail: text('thumbnail'),
|
|
60
|
-
createdAt: integer('createdAt').notNull(),
|
|
61
|
-
}, (t) => [primaryKey({ columns: [t.id] })]);
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { integer, primaryKey, real, sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
|
2
|
-
export const asmrProgress = sqliteTable('asmr_progress', {
|
|
3
|
-
workId: text('workId').notNull(),
|
|
4
|
-
trackIndex: integer('trackIndex').notNull(),
|
|
5
|
-
trackLabel: text('trackLabel'),
|
|
6
|
-
currentTime: real('currentTime'),
|
|
7
|
-
duration: real('duration'),
|
|
8
|
-
updatedAt: integer('updatedAt'),
|
|
9
|
-
title: text('title'),
|
|
10
|
-
thumbnail: text('thumbnail'),
|
|
11
|
-
rjCode: text('rjCode'),
|
|
12
|
-
isAdult: integer('isAdult'),
|
|
13
|
-
}, (t) => [primaryKey({ columns: [t.workId, t.trackIndex] })]);
|