@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.
Files changed (156) hide show
  1. package/README.md +1 -1
  2. package/client/dist/assets/{AnimeCard-C3P9zWi9.js → AnimeCard-DKDJK8Mp.js} +1 -1
  3. package/client/dist/assets/{AnimeInfo-Et6687Tl.js → AnimeInfo-CTcvfjva.js} +1 -1
  4. package/client/dist/assets/{AnimeInfoPage-DX4eNnlH.js → AnimeInfoPage-B_UiacIN.js} +2 -2
  5. package/client/dist/assets/{Asmr-CCdEWrFi.js → Asmr-DMSN2vxl.js} +2 -2
  6. package/client/dist/assets/{AsmrResetProgressModal-DgEWBJOw.js → AsmrResetProgressModal-CRwfOatX.js} +1 -1
  7. package/client/dist/assets/{AvSyncCalibrator-CQ2fjf2E.js → AvSyncCalibrator-CRJsi-vz.js} +1 -1
  8. package/client/dist/assets/{Home-DUkUTTTy.js → Home-pwmK9_Eh.js} +1 -1
  9. package/client/dist/assets/{Insights-QCh4xW12.js → Insights-m7kFzg2A.js} +1 -1
  10. package/client/dist/assets/{LibraryListPage-BoTxRqHz.js → LibraryListPage-C1wpPvu6.js} +1 -1
  11. package/client/dist/assets/ListeningList-OzfEI1Ga.js +1 -0
  12. package/client/dist/assets/{Manga-HydR-L7o.js → Manga-ChRCExhK.js} +1 -1
  13. package/client/dist/assets/{MangaInfoPage-CBakEI9a.js → MangaInfoPage-DSHJPo2A.js} +1 -1
  14. package/client/dist/assets/{MangaReadPage-DwAR3iPy.js → MangaReadPage-CzOVY1wA.js} +1 -1
  15. package/client/dist/assets/{MangaResetProgressModal-DGzl8vsO.js → MangaResetProgressModal-CJfzdUBb.js} +1 -1
  16. package/client/dist/assets/{Mature-Pi0TxKuF.js → Mature-CifQMfhs.js} +1 -1
  17. package/client/dist/assets/{MediaCard--cuFxOCS.js → MediaCard-CM10yB9V.js} +1 -1
  18. package/client/dist/assets/{OptionListMenu-B3D39dGS.js → OptionListMenu-6Kovbsqw.js} +1 -1
  19. package/client/dist/assets/{Player-CsH4x_I_.js → Player-CK0hT-u4.js} +2 -2
  20. package/client/dist/assets/{PlayerSettings-DGxBsi3F.js → PlayerSettings-Bb66uqT9.js} +1 -1
  21. package/client/dist/assets/{QueueOptionsButton-Djq4Cpex.js → QueueOptionsButton-C_nTj8_q.js} +1 -1
  22. package/client/dist/assets/{QueueRail-DOrYgNVX.js → QueueRail-DUK7URAE.js} +1 -1
  23. package/client/dist/assets/{ReadingList-DwYoBrNk.js → ReadingList-Ck9LJThU.js} +1 -1
  24. package/client/dist/assets/{ResetProgressModal-CMF86mX6.js → ResetProgressModal-BUphIv0k.js} +1 -1
  25. package/client/dist/assets/{Search-DFkhuk-x.js → Search-zJyP15h6.js} +1 -1
  26. package/client/dist/assets/{Settings-DBB2r0VQ.js → Settings-BYdoSEUh.js} +1 -1
  27. package/client/dist/assets/{SkeletonGrid-DJelJgAv.js → SkeletonGrid-CqgbOhjX.js} +1 -1
  28. package/client/dist/assets/{Trackers-DGYXFhYZ.js → Trackers-BVjyv6CM.js} +1 -1
  29. package/client/dist/assets/{Tv-C5MDGD2Z.js → Tv-L13ylswR.js} +1 -1
  30. package/client/dist/assets/{TvInfo-DrKuK13u.js → TvInfo-Cvcm5U2D.js} +1 -1
  31. package/client/dist/assets/{TvPopup-DtHilRDT.js → TvPopup-DhxfKvYm.js} +1 -1
  32. package/client/dist/assets/{TvSearch-CzNxdu_w.js → TvSearch-P8bFE6_X.js} +1 -1
  33. package/client/dist/assets/{TvWatchlist-D7p4br6S.js → TvWatchlist-D5gVPjtb.js} +1 -1
  34. package/client/dist/assets/{Watchlist-DrOzswl2.js → Watchlist-Cjq8l3rU.js} +1 -1
  35. package/client/dist/assets/{hls-BGCX7mJy.js → hls-BkzuANAg.js} +1 -1
  36. package/client/dist/assets/{index-CnfZwsds.js → index-CLVgzKLk.js} +19 -19
  37. package/client/dist/assets/{useAnimeInfoData-DYXWBPF-.js → useAnimeInfoData-DZJZu-so.js} +1 -1
  38. package/client/dist/assets/{useAsmr-BbhQ59bk.js → useAsmr-CvrMGHuo.js} +1 -1
  39. package/client/dist/assets/useAsmrPopup-Bimy8Ouw.js +1 -0
  40. package/client/dist/assets/{useMangaLibrary-DuesWQaE.js → useMangaLibrary--MiN11uR.js} +1 -1
  41. package/client/dist/assets/{useMangaPopup-D57OVGUr.js → useMangaPopup-CR5XhKKS.js} +1 -1
  42. package/client/dist/assets/{useProviders-mpQb_dix.js → useProviders-DqdT4fA5.js} +1 -1
  43. package/client/dist/assets/{useTv-DGflCsSI.js → useTv-BL0HMDyP.js} +1 -1
  44. package/client/dist/assets/{useTvLibrary-BqeSmyc4.js → useTvLibrary-D4U4iqN8.js} +1 -1
  45. package/client/dist/index.html +1 -1
  46. package/package.json +3 -5
  47. package/server/dist/config.js +1 -174
  48. package/server/dist/server.js +1068 -289
  49. package/server/dist/shipped-defaults.js +1 -6
  50. package/server/package.json +4 -5
  51. package/client/dist/assets/Asmr.module-CdWlioTY.js +0 -1
  52. package/client/dist/assets/ListeningList-DguKjI_t.js +0 -1
  53. package/client/dist/assets/Music-BGH_0voi.css +0 -1
  54. package/client/dist/assets/Music-CQpyXTLH.js +0 -1
  55. package/client/dist/assets/Radio-CCHNFN7R.css +0 -1
  56. package/client/dist/assets/Radio-DF6g6odd.js +0 -1
  57. package/client/dist/assets/Radio.module-DGSZLpKM.js +0 -1
  58. package/client/dist/assets/useAsmrPopup-SymV1J-b.js +0 -1
  59. package/server/dist/app-auth.js +0 -157
  60. package/server/dist/app-hono.js +0 -598
  61. package/server/dist/db/drizzle.js +0 -16
  62. package/server/dist/db/schema-anime.js +0 -61
  63. package/server/dist/db/schema-asmr.js +0 -13
  64. package/server/dist/db/schema-manga.js +0 -31
  65. package/server/dist/db/schema-tv.js +0 -18
  66. package/server/dist/db.js +0 -217
  67. package/server/dist/discord-gateway.js +0 -385
  68. package/server/dist/discord-rpc.js +0 -413
  69. package/server/dist/github-sync.js +0 -508
  70. package/server/dist/google.js +0 -741
  71. package/server/dist/hono/asmr.js +0 -10
  72. package/server/dist/hono/auth.js +0 -49
  73. package/server/dist/hono/music.js +0 -209
  74. package/server/dist/hono/proxy.js +0 -827
  75. package/server/dist/hono/tv.js +0 -200
  76. package/server/dist/hono/watchlist.js +0 -341
  77. package/server/dist/lib/anilist.js +0 -1583
  78. package/server/dist/lib/anischedule.js +0 -191
  79. package/server/dist/lib/insights.js +0 -185
  80. package/server/dist/lib/ipc.js +0 -23
  81. package/server/dist/lib/kitsu.js +0 -517
  82. package/server/dist/lib/mal-import.js +0 -291
  83. package/server/dist/lib/mal.js +0 -957
  84. package/server/dist/lib/migration.js +0 -310
  85. package/server/dist/lib/offline-db.js +0 -395
  86. package/server/dist/lib/potoken.js +0 -135
  87. package/server/dist/lib/recommendations.service.js +0 -216
  88. package/server/dist/lib/temp-ids.js +0 -3
  89. package/server/dist/lib/tmdb.js +0 -72
  90. package/server/dist/lib/tracker/anilist-tracker.js +0 -308
  91. package/server/dist/lib/tracker/manga-sync.service.js +0 -773
  92. package/server/dist/lib/tracker/sync-state.js +0 -21
  93. package/server/dist/lib/tracker/sync.service.js +0 -476
  94. package/server/dist/lib/translate.js +0 -79
  95. package/server/dist/lib/ump.js +0 -106
  96. package/server/dist/lib/ytmusic.js +0 -162
  97. package/server/dist/logger.js +0 -16
  98. package/server/dist/providers/manga/manga.types.js +0 -1
  99. package/server/dist/providers/provider.interface.js +0 -1
  100. package/server/dist/providers/remote-loader.js +0 -614
  101. package/server/dist/providers/remote-types.js +0 -159
  102. package/server/dist/providers/title-matching.js +0 -226
  103. package/server/dist/providers/tv.types.js +0 -1
  104. package/server/dist/rclone.js +0 -120
  105. package/server/dist/repositories/asmr.repository.js +0 -77
  106. package/server/dist/repositories/insights.repository.js +0 -106
  107. package/server/dist/repositories/library.repository.js +0 -94
  108. package/server/dist/repositories/mal-cache.repository.js +0 -52
  109. package/server/dist/repositories/manga.repository.js +0 -95
  110. package/server/dist/repositories/notifications.repository.js +0 -60
  111. package/server/dist/repositories/queue.repository.js +0 -72
  112. package/server/dist/repositories/settings.repository.js +0 -30
  113. package/server/dist/repositories/shows-meta.repository.js +0 -58
  114. package/server/dist/repositories/temp-show-ids.repository.js +0 -63
  115. package/server/dist/repositories/tv.repository.js +0 -93
  116. package/server/dist/repositories/watched-episodes.repository.js +0 -82
  117. package/server/dist/repositories/watchlist.repository.js +0 -60
  118. package/server/dist/scripts/backfill-episode-duration.js +0 -42
  119. package/server/dist/sync-payload.js +0 -91
  120. package/server/dist/sync.js +0 -657
  121. package/server/dist/trpc/context.js +0 -7
  122. package/server/dist/trpc/errors.js +0 -15
  123. package/server/dist/trpc/index.js +0 -18
  124. package/server/dist/trpc/router.js +0 -49
  125. package/server/dist/trpc/routers/asmr-library.js +0 -112
  126. package/server/dist/trpc/routers/asmr-progress.js +0 -121
  127. package/server/dist/trpc/routers/asmr.js +0 -87
  128. package/server/dist/trpc/routers/auth.js +0 -194
  129. package/server/dist/trpc/routers/continue-watching.js +0 -81
  130. package/server/dist/trpc/routers/data.js +0 -1026
  131. package/server/dist/trpc/routers/discord.js +0 -285
  132. package/server/dist/trpc/routers/discovery.js +0 -11
  133. package/server/dist/trpc/routers/insights.js +0 -14
  134. package/server/dist/trpc/routers/manga-library.js +0 -263
  135. package/server/dist/trpc/routers/manga-progress.js +0 -169
  136. package/server/dist/trpc/routers/manga.js +0 -201
  137. package/server/dist/trpc/routers/music.js +0 -655
  138. package/server/dist/trpc/routers/notifications.js +0 -80
  139. package/server/dist/trpc/routers/progress.js +0 -109
  140. package/server/dist/trpc/routers/queue.js +0 -93
  141. package/server/dist/trpc/routers/radio.js +0 -145
  142. package/server/dist/trpc/routers/settings.js +0 -96
  143. package/server/dist/trpc/routers/tracker.js +0 -213
  144. package/server/dist/trpc/routers/tv-library.js +0 -135
  145. package/server/dist/trpc/routers/tv-progress.js +0 -159
  146. package/server/dist/trpc/routers/tv.js +0 -551
  147. package/server/dist/trpc/routers/watchlist.js +0 -550
  148. package/server/dist/trpc/validation.js +0 -258
  149. package/server/dist/utils/cache.utils.js +0 -25
  150. package/server/dist/utils/cookie.utils.js +0 -13
  151. package/server/dist/utils/db-utils.js +0 -17
  152. package/server/dist/utils/env.utils.js +0 -77
  153. package/server/dist/utils/http.utils.js +0 -82
  154. package/server/dist/utils/request-context.js +0 -2
  155. package/server/dist/utils/security.utils.js +0 -76
  156. /package/client/dist/assets/{Asmr-D1KuQwQy.css → useAsmrPopup-D1KuQwQy.css} +0 -0
@@ -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] })]);