@serifpersia/dango 3.6.7 → 3.6.8
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-DraA_TF9.js} +1 -1
- package/client/dist/assets/{AnimeInfo-Et6687Tl.js → AnimeInfo-BJ7pzs-P.js} +1 -1
- package/client/dist/assets/{AnimeInfoPage-DX4eNnlH.js → AnimeInfoPage-B_esUr5C.js} +2 -2
- package/client/dist/assets/{Asmr-CCdEWrFi.js → Asmr-Ckuh2xfW.js} +1 -1
- package/client/dist/assets/{AsmrResetProgressModal-DgEWBJOw.js → AsmrResetProgressModal-BS6UFJN4.js} +1 -1
- package/client/dist/assets/{AvSyncCalibrator-CQ2fjf2E.js → AvSyncCalibrator-DwHCd9yB.js} +1 -1
- package/client/dist/assets/{Home-DUkUTTTy.js → Home-DuM1EuvN.js} +1 -1
- package/client/dist/assets/{Insights-QCh4xW12.js → Insights-D3Pd9c9V.js} +1 -1
- package/client/dist/assets/{LibraryListPage-BoTxRqHz.js → LibraryListPage-Dm2yVn0-.js} +1 -1
- package/client/dist/assets/{ListeningList-DguKjI_t.js → ListeningList-D-zt6EXZ.js} +1 -1
- package/client/dist/assets/{Manga-HydR-L7o.js → Manga-JwprMKBk.js} +1 -1
- package/client/dist/assets/{MangaInfoPage-CBakEI9a.js → MangaInfoPage-DaeyJl2p.js} +1 -1
- package/client/dist/assets/{MangaReadPage-DwAR3iPy.js → MangaReadPage-COg0fHI9.js} +1 -1
- package/client/dist/assets/{MangaResetProgressModal-DGzl8vsO.js → MangaResetProgressModal-kguXv0_M.js} +1 -1
- package/client/dist/assets/{Mature-Pi0TxKuF.js → Mature-IHDaasrs.js} +1 -1
- package/client/dist/assets/{MediaCard--cuFxOCS.js → MediaCard-CpwsrXo5.js} +1 -1
- package/client/dist/assets/{Music-CQpyXTLH.js → Music-Dks4tPeK.js} +1 -1
- package/client/dist/assets/{OptionListMenu-B3D39dGS.js → OptionListMenu-11CcAsy8.js} +1 -1
- package/client/dist/assets/{Player-CsH4x_I_.js → Player-DQoEQkHL.js} +2 -2
- package/client/dist/assets/{PlayerSettings-DGxBsi3F.js → PlayerSettings-D4Ud4kv4.js} +1 -1
- package/client/dist/assets/{QueueOptionsButton-Djq4Cpex.js → QueueOptionsButton-B8PlPKjC.js} +1 -1
- package/client/dist/assets/{QueueRail-DOrYgNVX.js → QueueRail-B4Dok7U3.js} +1 -1
- package/client/dist/assets/{Radio-DF6g6odd.js → Radio-BHK6LSk4.js} +1 -1
- package/client/dist/assets/{ReadingList-DwYoBrNk.js → ReadingList-BCsxIz57.js} +1 -1
- package/client/dist/assets/{ResetProgressModal-CMF86mX6.js → ResetProgressModal-BXHxSfy-.js} +1 -1
- package/client/dist/assets/{Search-DFkhuk-x.js → Search-PK1ds07O.js} +1 -1
- package/client/dist/assets/{Settings-DBB2r0VQ.js → Settings-Bxk_x55l.js} +1 -1
- package/client/dist/assets/{SkeletonGrid-DJelJgAv.js → SkeletonGrid-CpJCZSRN.js} +1 -1
- package/client/dist/assets/{Trackers-DGYXFhYZ.js → Trackers-RieCh_84.js} +1 -1
- package/client/dist/assets/{Tv-C5MDGD2Z.js → Tv-DLytPJUj.js} +1 -1
- package/client/dist/assets/{TvInfo-DrKuK13u.js → TvInfo-C8djKebR.js} +1 -1
- package/client/dist/assets/{TvPopup-DtHilRDT.js → TvPopup-40JGGJDk.js} +1 -1
- package/client/dist/assets/{TvSearch-CzNxdu_w.js → TvSearch-A3hbospf.js} +1 -1
- package/client/dist/assets/{TvWatchlist-D7p4br6S.js → TvWatchlist-DuXjqmZM.js} +1 -1
- package/client/dist/assets/{Watchlist-DrOzswl2.js → Watchlist-B9wGBh0u.js} +1 -1
- package/client/dist/assets/{hls-BGCX7mJy.js → hls-CZ5X3Ya-.js} +1 -1
- package/client/dist/assets/{index-CnfZwsds.js → index-dRhc9-ol.js} +2 -2
- package/client/dist/assets/{useAnimeInfoData-DYXWBPF-.js → useAnimeInfoData-ZzDtLfvD.js} +1 -1
- package/client/dist/assets/{useAsmr-BbhQ59bk.js → useAsmr-CKOdHgol.js} +1 -1
- package/client/dist/assets/{useAsmrPopup-SymV1J-b.js → useAsmrPopup-SFHPhpcl.js} +1 -1
- package/client/dist/assets/{useMangaLibrary-DuesWQaE.js → useMangaLibrary-CKATe31P.js} +1 -1
- package/client/dist/assets/{useMangaPopup-D57OVGUr.js → useMangaPopup-C75QuYgY.js} +1 -1
- package/client/dist/assets/{useProviders-mpQb_dix.js → useProviders-CMtvZbEi.js} +1 -1
- package/client/dist/assets/{useTv-DGflCsSI.js → useTv-DUX4zYMr.js} +1 -1
- package/client/dist/assets/{useTvLibrary-BqeSmyc4.js → useTvLibrary-By4Gt-R_.js} +1 -1
- package/client/dist/index.html +1 -1
- package/package.json +2 -2
- package/server/dist/app-auth.js +1 -157
- package/server/dist/app-hono.js +7 -557
- package/server/dist/config.js +1 -174
- package/server/dist/db/drizzle.js +1 -16
- package/server/dist/db/schema-anime.js +1 -61
- package/server/dist/db/schema-asmr.js +1 -13
- package/server/dist/db/schema-manga.js +1 -31
- package/server/dist/db/schema-tv.js +1 -18
- package/server/dist/db.js +1 -217
- package/server/dist/discord-gateway.js +1 -385
- package/server/dist/discord-rpc.js +1 -413
- package/server/dist/github-sync.js +1 -508
- package/server/dist/google.js +1 -741
- package/server/dist/hono/asmr.js +1 -10
- package/server/dist/hono/auth.js +3 -26
- package/server/dist/hono/music.js +1 -209
- package/server/dist/hono/proxy.js +36 -783
- package/server/dist/hono/tv.js +5 -200
- package/server/dist/hono/watchlist.js +2 -338
- package/server/dist/lib/anilist.js +26 -1458
- package/server/dist/lib/anischedule.js +1 -191
- package/server/dist/lib/insights.js +1 -185
- package/server/dist/lib/ipc.js +1 -23
- package/server/dist/lib/kitsu.js +1 -517
- package/server/dist/lib/mal-import.js +1 -291
- package/server/dist/lib/mal.js +1 -957
- package/server/dist/lib/migration.js +1 -310
- package/server/dist/lib/offline-db.js +4 -384
- package/server/dist/lib/potoken.js +1 -135
- package/server/dist/lib/recommendations.service.js +1 -216
- package/server/dist/lib/temp-ids.js +1 -3
- package/server/dist/lib/tmdb.js +1 -72
- package/server/dist/lib/tracker/anilist-tracker.js +8 -245
- package/server/dist/lib/tracker/manga-sync.service.js +1 -773
- package/server/dist/lib/tracker/sync-state.js +1 -21
- package/server/dist/lib/tracker/sync.service.js +1 -476
- package/server/dist/lib/translate.js +3 -79
- package/server/dist/lib/ump.js +1 -106
- package/server/dist/lib/ytmusic.js +1 -162
- package/server/dist/logger.js +1 -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 +2 -614
- package/server/dist/providers/remote-types.js +1 -159
- package/server/dist/providers/title-matching.js +1 -226
- package/server/dist/providers/tv.types.js +0 -1
- package/server/dist/rclone.js +2 -120
- package/server/dist/repositories/asmr.repository.js +11 -46
- package/server/dist/repositories/insights.repository.js +11 -25
- package/server/dist/repositories/library.repository.js +3 -94
- package/server/dist/repositories/mal-cache.repository.js +1 -52
- package/server/dist/repositories/manga.repository.js +14 -58
- package/server/dist/repositories/notifications.repository.js +6 -52
- package/server/dist/repositories/queue.repository.js +5 -58
- package/server/dist/repositories/settings.repository.js +2 -29
- package/server/dist/repositories/shows-meta.repository.js +3 -56
- package/server/dist/repositories/temp-show-ids.repository.js +3 -63
- package/server/dist/repositories/tv.repository.js +11 -54
- package/server/dist/repositories/watched-episodes.repository.js +6 -55
- package/server/dist/repositories/watchlist.repository.js +3 -54
- package/server/dist/scripts/backfill-episode-duration.js +1 -42
- package/server/dist/server.js +1 -310
- package/server/dist/shipped-defaults.js +1 -6
- package/server/dist/sync-payload.js +1 -91
- package/server/dist/sync.js +3 -647
- package/server/dist/trpc/context.js +1 -7
- package/server/dist/trpc/errors.js +1 -15
- package/server/dist/trpc/index.js +1 -18
- package/server/dist/trpc/router.js +1 -49
- package/server/dist/trpc/routers/asmr-library.js +1 -112
- package/server/dist/trpc/routers/asmr-progress.js +1 -121
- package/server/dist/trpc/routers/asmr.js +1 -87
- package/server/dist/trpc/routers/auth.js +1 -194
- package/server/dist/trpc/routers/continue-watching.js +2 -58
- package/server/dist/trpc/routers/data.js +1 -1026
- package/server/dist/trpc/routers/discord.js +1 -285
- package/server/dist/trpc/routers/discovery.js +1 -11
- package/server/dist/trpc/routers/insights.js +1 -14
- package/server/dist/trpc/routers/manga-library.js +1 -263
- package/server/dist/trpc/routers/manga-progress.js +1 -169
- package/server/dist/trpc/routers/manga.js +1 -201
- package/server/dist/trpc/routers/music.js +1 -655
- package/server/dist/trpc/routers/notifications.js +1 -80
- package/server/dist/trpc/routers/progress.js +1 -109
- package/server/dist/trpc/routers/queue.js +1 -93
- package/server/dist/trpc/routers/radio.js +1 -145
- package/server/dist/trpc/routers/settings.js +1 -96
- package/server/dist/trpc/routers/tracker.js +1 -213
- package/server/dist/trpc/routers/tv-library.js +1 -135
- package/server/dist/trpc/routers/tv-progress.js +1 -159
- package/server/dist/trpc/routers/tv.js +1 -551
- package/server/dist/trpc/routers/watchlist.js +2 -549
- package/server/dist/trpc/validation.js +1 -258
- package/server/dist/utils/cache.utils.js +1 -25
- package/server/dist/utils/cookie.utils.js +1 -13
- package/server/dist/utils/db-utils.js +5 -17
- package/server/dist/utils/env.utils.js +5 -77
- package/server/dist/utils/http.utils.js +1 -82
- package/server/dist/utils/request-context.js +1 -2
- package/server/dist/utils/security.utils.js +1 -76
- package/server/package.json +3 -2
package/server/dist/app-hono.js
CHANGED
|
@@ -1,427 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
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>
|
|
1
|
+
import{Hono as N}from"hono";import{compress as L}from"hono/compress";import{getConnInfo as O}from"@hono/node-server/conninfo";import M from"path";import v from"fs";import{XMLParser as F}from"fast-xml-parser";import{CONFIG as k}from"./config.js";import{isAllowedOrigin as D}from"./utils/security.utils.js";import{requestContext as B}from"./utils/request-context.js";import m from"./logger.js";import{LAN_AUTH_PUBLIC_PATHS as W,buildLanCookie as U,clearAllLanSessions as V,clearLanCookie as X,createLanSession as $,getTokenFromHeaders as T,hasAppPassword as A,isLoopbackIp as _,revokeLanSession as z,setAppPassword as J,validateLanSession as E,verifyAppPassword as G}from"./app-auth.js";import{importMangaFromMalXmlItems as Y}from"./lib/tracker/manga-sync.service.js";import{LibraryRepository as b}from"./repositories/library.repository.js";import{performAsmrWriteTransactionAsync as K,performMangaWriteTransactionAsync as Q,performTvWriteTransactionAsync as Z,performWriteTransactionAsync as ee,KIND_SYNC as P,setKindManifestVersion as re}from"./sync.js";import{ANIME_SYNC_TABLES as te,ASMR_SYNC_TABLES as ae,MANGA_SYNC_TABLES as oe,TV_SYNC_TABLES as ne,exportTables as S,importTables as se,normalizePayload as ie,readPayloadVersion as de}from"./sync-payload.js";import{getMalImportStatus as ce,prepareMalImport as le,executeMalImport as pe}from"./lib/mal-import.js";import{registerMusic as ue}from"./hono/music.js";import{registerAuth as fe}from"./hono/auth.js";import{registerTv as me}from"./hono/tv.js";import{registerProxy as ge}from"./hono/proxy.js";import{fetchRequestHandler as he}from"@trpc/server/adapters/fetch";import{appRouter as ye}from"./trpc/router.js";import{buildTrpcContext as be}from"./trpc/context.js";const we=new F({isArray:a=>a==="manga",parseTagValue:!1,trimValues:!0});function q(a){return Array.isArray(a)?String(a[0]??""):String(a??"")}function ve(a){switch(a){case"Plan to Read":return"Planned";case"On Hold":return"On-Hold";case"Reading":case"Completed":case"Dropped":return a;default:return"Planned"}}function ke(a){try{return O(a).remote.address}catch{return a.req.header("x-dango-client-ip")??void 0}}const H="x-dango-not-ready";function C(a,l){return a.header(H,"1"),typeof l=="string"?a.text(l,503):a.json(l,503)}async function Ce(a,l){const s=process.hrtime.bigint(),p=new URL(a.req.url),c=`${p.pathname}${p.search}`;await l();const g=Math.round(Number(process.hrtime.bigint()-s)/1e6*10)/10,n=a.res.status,o={method:a.req.method,url:c,status:n,ms:g};n>=500&&!a.res.headers.get(H)?m.warn(o,"request failed"):c.startsWith("/api/proxy")||m.trace(`${a.req.method} ${c} ${n} ${g}ms`)}async function Ae(a,l){const s=new Map,p=a.req.header("x-animepahe-ua");p&&s.set("ua",p);const c=a.req.header("x-animepahe-cookie");c&&s.set("cookie",c);const g=a.req.header("x-jasmr-ua");g&&s.set("jasmr_ua",g);const n=a.req.header("x-jasmr-cookie");return n&&s.set("jasmr_cookie",n),B.run(s,()=>l())}async function je(a,l,s){let p;try{p=JSON.parse(s.toString("utf-8"))}catch{return a.json({error:"Invalid backup file."},400)}const c=p?.databases;if(!c||typeof c!="object")return a.json({error:"Invalid backup file: missing databases."},400);const g={anime:l.db,manga:l.mangaDb,tv:l.tvDb,asmr:l.asmrDb},o=Object.keys(P).map(r=>({key:r,db:g[r],tables:P[r].tables,libraryTables:P[r].libraryTables,backupName:P[r].backupName,setVersion:t=>re(r,t)})).filter(r=>c[r.key]!==void 0);if(o.length===0)return a.json({error:"Invalid backup file: no databases found."},400);const h=new Map;try{for(const r of o)h.set(r.key,ie(c[r.key],r.tables,`backup ${r.key}`))}catch(r){return m.warn({err:r},"Multi-database restore rejected: invalid payload"),a.json({error:"Invalid backup file."},400)}const e=[];try{for(const r of o){const t=h.get(r.key);t&&(se(r.db,r.tables,t,{libraryTables:r.libraryTables,backupName:r.backupName}),await r.setVersion(de(t)),e.push(r.key))}}catch(r){return m.error({err:r,restored:e},"Multi-database restore failed partway"),a.json({error:"Restore failed partway.",restored:e},500)}return m.warn({restored:e},"Databases restored from backup file by user request"),a.json({success:!0,message:"Databases restored.",restored:e})}function De(a,l,s,p){const c=k.IS_DEV?k.DB_NAME_DEV:k.DB_NAME_PROD,g=M.join(k.ROOT,"restore_temp.db"),n=M.join(k.ROOT,c);try{v.writeFileSync(g,s)}catch(h){return m.error({err:h},"Failed to stage legacy database restore file"),Promise.resolve(a.json({error:"Failed to stage restore file."},500))}const o=l.db;return new Promise(h=>{o.close(e=>{if(e){h(a.json({error:"Failed to close database."},500));return}try{o.checkpoint()}catch(r){m.warn({err:r},"WAL checkpoint failed")}try{v.existsSync(`${n}-wal`)&&v.unlinkSync(`${n}-wal`),v.existsSync(`${n}-shm`)&&v.unlinkSync(`${n}-shm`)}catch(r){m.warn({err:r},"Failed to clean up WAL files")}v.rename(g,n,async r=>{if(r){try{p.setDb(await p.initializeDatabase(n))}catch(t){m.error({err:t},"Failed to reopen DB after rename failure")}h(a.json({error:"Failed to replace database file."},500));return}try{p.setDb(await p.initializeDatabase(n)),h(a.json({success:!0,message:"Database restored."}))}catch(t){m.error({err:t},"Failed to initialize restored database"),h(a.json({error:"Failed to initialize restored database."},500))}})})})}function Ye(a,l,s,p,c,g,n){const o=new N;o.use(Ce),o.use(Ae),o.get("/api/health",e=>{const r=a();return r.shuttingDown?C(e,{status:"shutting-down",ready:!1}):r.dbReady?e.json({status:"ok",ready:!0}):C(e,{status:"starting",ready:!1})}),o.use(async(e,r)=>{const t=l();if(t.shuttingDown)return C(e,"Server is shutting down...");if(!t.dbsReady)return C(e,"Database initializing...");if(t.bootSyncing&&e.req.method!=="GET"&&e.req.method!=="HEAD"&&e.req.method!=="OPTIONS")return C(e,"Sync in progress...");await r()});const h=L({threshold:1024});return o.use(async(e,r)=>{if(e.req.header("x-no-compression")){await r();return}await h(e,r)}),o.use(async(e,r)=>{const t=e.req.header("origin");if(e.req.method==="OPTIONS"){if(!t||!D(t)){await r();return}const i=e.req.header("access-control-request-headers");return new Response(null,{status:204,headers:{"Access-Control-Allow-Origin":t,"Access-Control-Allow-Credentials":"true","Access-Control-Allow-Methods":"GET,HEAD,PUT,PATCH,POST,DELETE",...i?{"Access-Control-Allow-Headers":i}:{},Vary:"Origin"}})}(!t||D(t))&&(t&&e.header("Access-Control-Allow-Origin",t),e.header("Access-Control-Allow-Credentials","true"),e.header("Vary","Origin")),await r()}),o.use(async(e,r)=>{const t=e.req.method.toUpperCase();if(!["POST","PUT","DELETE","PATCH"].includes(t)){await r();return}if(e.req.header("sec-fetch-site")==="cross-site"){const d=e.req.header("origin");if(!D(d))return e.json({error:"Forbidden: cross-site request rejected"},403)}const y=e.req.header("origin");if(y&&!D(y))return e.json({error:"Forbidden: cross-origin request rejected"},403);await r()}),o.get("/api/auth/app-status",e=>{const r=!A()||E(T(e.req.header("authorization"),e.req.header("cookie")));return e.json({hasPassword:A(),isAuthenticated:r,port:k.PORT})}),o.post("/api/auth/app-login",async e=>{const r=await e.req.json().catch(()=>{}),t=String(r?.password||"");if(!A())return e.json({success:!0});if(!t)return e.json({error:"Password required"},400);if(!G(t))return e.json({error:"Invalid password"},401);const i=$();return e.header("Set-Cookie",U(i.token,i.expiry)),e.json({success:!0})}),o.post("/api/auth/app-logout",e=>(z(T(e.req.header("authorization"),e.req.header("cookie"))),e.header("Set-Cookie",X()),e.json({success:!0}))),o.post("/api/auth/app-setup",async e=>{if(!_(e.req.header("x-dango-client-ip")))return e.json({error:"Forbidden"},403);const r=await e.req.json().catch(()=>{}),t=String(r?.password||"");try{return await J(t),V(),e.json({success:!0,hasPassword:A()})}catch{return e.json({error:"Failed to update password"},500)}}),o.use(async(e,r)=>{const t=e.req.path;if(!t.startsWith("/api/")){await r();return}if(W.has(t)){await r();return}if(t.startsWith("/api/internal/")&&_(ke(e))){await r();return}if(!A()){await r();return}if(E(T(e.req.header("authorization"),e.req.header("cookie")))){await r();return}return e.json({error:"LAN_AUTH_REQUIRED"},401)}),o.all("/api/trpc/*",e=>he({endpoint:"/api/trpc",req:e.req.raw,router:ye,createContext:()=>be({...s(),apiCache:n.getApiCache(),getTvProvider:n.getTvProvider,getMangaProvider:n.getMangaProvider,getJasmr:n.getJasmr,getProviders:n.getProviders,getCatalog:c.getCatalog},e.req.header("authorization"),e.req.header("cookie"))})),o.post("/api/import/mal-xml",async e=>{const r=s(),t=await e.req.parseBody().catch(()=>({})),i=t.xmlfile,y=i instanceof File?Buffer.from(await i.arrayBuffer()):void 0,d=le(y,{erase:t.erase,useOfflineDb:t.useOfflineDb,skipFallback:t.skipFallback});if(!d.ok)return e.json(d.body,d.status);const u=new TransformStream,f=u.writable.getWriter(),x=new TextEncoder;let j=!1;e.req.raw.signal.addEventListener("abort",()=>{j=!0},{once:!0});const R=(w,I)=>{j||f.write(x.encode(`event: ${w}
|
|
2
|
+
data: ${JSON.stringify(I)}
|
|
3
|
+
|
|
4
|
+
`)).catch(()=>{})};return pe(r.db,d.import,R).finally(()=>{f.close().catch(()=>{})}),new Response(u.readable,{headers:{"Content-Type":"text/event-stream","Cache-Control":"no-cache",Connection:"keep-alive"}})}),o.get("/api/providers",e=>(e.header("Cache-Control","public, max-age=300"),e.json(c.getCatalog()))),o.post("/api/providers/refresh",async e=>{try{return e.json(await c.refresh())}catch(r){return e.json({error:r.message||"refresh failed"},500)}}),o.get("/api/tracker/anilist/callback",e=>e.html(`<!doctype html>
|
|
5
|
+
<html><head><meta charset="utf-8"><title>AniList \u2014 completing login</title></head>
|
|
6
|
+
<body style="font-family:system-ui;padding:24px;background:#0b1426;color:#e5eefc"><p>Completing AniList login\u2026</p>
|
|
425
7
|
<script>
|
|
426
8
|
(function(){
|
|
427
9
|
var qs = new URLSearchParams(location.search);
|
|
@@ -463,136 +45,4 @@ export function createHonoApp(getState, getGuardState, getDbs, dbAdmin, provider
|
|
|
463
45
|
}
|
|
464
46
|
location.replace(frontend + (frontend.indexOf('?') !== -1 ? '&' : '?') + 'anilist=error&reason=no_token');
|
|
465
47
|
})();
|
|
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
|
-
}
|
|
48
|
+
</script></body></html>`)),o.post("/api/import/mal-xml-manga",async e=>{const r=await e.req.parseBody().catch(()=>({})),t=r.xmlfile;if(!(t instanceof File))return e.json({error:"No file"},400);const i=r.erase,y=i===!0||i==="true"||i==="1";try{const d=s();if(!d.mangaDb||d.mangaDb.isClosedCheck())return C(e,{error:"Manga database is not ready"});let u;try{u=we.parse(Buffer.from(await t.arrayBuffer()).toString())}catch{return e.json({error:"Invalid XML"},400)}const f=u?.myanimelist?.manga||[];if(f.length===0)return e.json({error:"No manga found in XML"},400);const x=f.map(w=>({malId:parseInt(q(w.series_mangadb_id),10)||0,title:q(w.series_title),status:ve(q(w.my_status)),chapters:Math.max(parseInt(q(w.my_read_chapters),10)||0,0)})),{imported:j,skipped:R}=await Y(d.db,d.mangaDb,x,y);return e.json({success:!0,imported:j,skipped:R})}catch(d){const u=d instanceof Error?d.message:"Import failed";return e.json({error:u},500)}}),o.get("/api/backup-db",e=>{try{const r=s(),t={version:1,exportedAt:new Date().toISOString(),databases:{anime:S(r.db,te),manga:S(r.mangaDb,oe),tv:S(r.tvDb,ne),asmr:S(r.asmrDb,ae)}};return e.header("Content-Disposition",'attachment; filename="dango-backup.json"'),e.json(t)}catch(r){return m.error({err:r},"Multi-database backup failed"),e.json({error:"Backup failed"},500)}}),o.post("/api/database/clear",async e=>{if((await e.req.json().catch(()=>{}))?.confirm!==!0)return e.json({error:"Confirmation required"},400);if(ce().running)return e.json({error:"An import is in progress"},409);try{const t=s(),i=await b.countAll(t.db),y=await b.countManga(t.mangaDb),d=await b.countTv(t.tvDb),u=await b.countAsmr(t.asmrDb);return await ee(t.db,async f=>{await b.clearAll(f)}),await Q(t.mangaDb,async f=>{await b.clearManga(f)}),await Z(t.tvDb,async f=>{await b.clearTv(f)}),await K(t.asmrDb,async f=>{await b.clearAsmr(f)}),m.warn({before:i,beforeManga:y,beforeTv:d,beforeAsmr:u},"Library database cleared by user request"),e.json({success:!0,deleted:i,deletedManga:y,deletedTv:d,deletedAsmr:u})}catch{return e.json({error:"DB error"},500)}}),o.post("/api/restore-db",async e=>{const r=s();let t,i="";try{const u=(await e.req.parseBody()).dbfile;u instanceof File&&(t=Buffer.from(await u.arrayBuffer()),i=u.name||"")}catch{return e.json({error:"No file uploaded."},400)}if(!t)return e.json({error:"No file uploaded."},400);const y=t.slice(0,16).toString("utf-8").trimStart();return i.endsWith(".json")||y.startsWith("{")?je(e,r,t):De(e,r,t,p)}),ue(o),fe(o,s,g),me(o,n.getTvProvider),ge(o,n.getProxyRules),o.onError((e,r)=>{m.error({err:e,url:r.req.path,method:r.req.method},"Unhandled error");const t=e.status||500;return r.json({error:e.message||"Internal Server Error",status:t},t)}),o}export{Ye as createHonoApp,ke as getHonoClientIp,Ae as honoRequestContextMiddleware,Ce as honoRequestLogger};
|