@serifpersia/dango 3.3.4

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 (190) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +258 -0
  3. package/client/dist/assets/AnimeCard-B5A20jAI.js +1 -0
  4. package/client/dist/assets/AnimeInfo-0huPAq0O.js +1 -0
  5. package/client/dist/assets/AnimeInfo-BFWtHlPC.css +1 -0
  6. package/client/dist/assets/AnimeInfoPage-Dqr5QH6R.js +2 -0
  7. package/client/dist/assets/AnimeMetaDetails-BPvv5xIl.js +1 -0
  8. package/client/dist/assets/AnimeMetaDetails-BwNQnRCd.css +1 -0
  9. package/client/dist/assets/Asmr-B6ZiqkmU.js +2 -0
  10. package/client/dist/assets/Asmr-Cq2ua6Zk.css +1 -0
  11. package/client/dist/assets/Asmr.module-ChUXDZKx.js +1 -0
  12. package/client/dist/assets/AsmrResetProgressModal-Cqsoo3XC.js +1 -0
  13. package/client/dist/assets/AvSyncCalibrator-C1_iBSyL.css +1 -0
  14. package/client/dist/assets/AvSyncCalibrator-C8ZoIwMY.js +10 -0
  15. package/client/dist/assets/ErrorMessage-CXoRbrLN.css +1 -0
  16. package/client/dist/assets/ErrorMessage-z4f4WGgP.js +1 -0
  17. package/client/dist/assets/Home-DtRtTuI9.css +1 -0
  18. package/client/dist/assets/Home-thJhPfs1.js +1 -0
  19. package/client/dist/assets/Insights-DKtoVe6L.css +1 -0
  20. package/client/dist/assets/Insights-ZlgPrzGD.js +1 -0
  21. package/client/dist/assets/LibraryListPage-CCiee-Ot.js +1 -0
  22. package/client/dist/assets/ListeningList-ctAz_6Zi.js +1 -0
  23. package/client/dist/assets/Manga-BI9WMTx5.js +1 -0
  24. package/client/dist/assets/Manga-Dp_-l1Pw.css +1 -0
  25. package/client/dist/assets/Manga.module-BcfOdsNP.js +1 -0
  26. package/client/dist/assets/MangaInfoPage-DcrEr7Sk.js +1 -0
  27. package/client/dist/assets/MangaReadPage-SYg63m48.js +1 -0
  28. package/client/dist/assets/MangaResetProgressModal-DX-1PS4h.js +1 -0
  29. package/client/dist/assets/Map-CXFqXmsZ.css +1 -0
  30. package/client/dist/assets/Map-Dj51g5zL.js +1 -0
  31. package/client/dist/assets/Mature-DJmUUtlF.css +1 -0
  32. package/client/dist/assets/Mature-Dqh_9SA3.js +1 -0
  33. package/client/dist/assets/MediaCard-Be1ST9oj.js +1 -0
  34. package/client/dist/assets/MediaCard-DB6Yjgky.css +1 -0
  35. package/client/dist/assets/Music-BGH_0voi.css +1 -0
  36. package/client/dist/assets/Music-zsPwT8_5.js +1 -0
  37. package/client/dist/assets/OptionListMenu-Drc_vybW.js +1 -0
  38. package/client/dist/assets/Player-B036Ic3m.css +1 -0
  39. package/client/dist/assets/Player-BEtAYps2.js +56 -0
  40. package/client/dist/assets/PlayerRedirect-D70t8VUW.js +1 -0
  41. package/client/dist/assets/PlayerSettings-Bj6SsmQ2.css +1 -0
  42. package/client/dist/assets/PlayerSettings-Cwoji-P7.js +1 -0
  43. package/client/dist/assets/QueueOptionsButton-B6BMq66e.js +1 -0
  44. package/client/dist/assets/QueueOptionsButton-CM15uQn8.css +1 -0
  45. package/client/dist/assets/QueueRail-BUYEjdaq.css +1 -0
  46. package/client/dist/assets/QueueRail-VxpUDBSm.js +1 -0
  47. package/client/dist/assets/Radio-B2Q_Egj7.js +1 -0
  48. package/client/dist/assets/Radio-CCHNFN7R.css +1 -0
  49. package/client/dist/assets/Radio.module-DGSZLpKM.js +1 -0
  50. package/client/dist/assets/ReadingList-O0QPwXu7.js +1 -0
  51. package/client/dist/assets/ResetProgressModal-CR_ovLAu.js +1 -0
  52. package/client/dist/assets/Search-C8e2nlDD.js +1 -0
  53. package/client/dist/assets/Search-Cs45oAwI.css +1 -0
  54. package/client/dist/assets/Search.module-CMEai-B-.js +1 -0
  55. package/client/dist/assets/Settings-AuFxQcJF.css +1 -0
  56. package/client/dist/assets/Settings-BgPWBl2V.js +21 -0
  57. package/client/dist/assets/SkeletonGrid-CtHKKMLj.js +1 -0
  58. package/client/dist/assets/SkeletonGrid-DhPYefXr.css +1 -0
  59. package/client/dist/assets/SynopsisText-BcpwT5iw.js +1 -0
  60. package/client/dist/assets/SynopsisText-CYv2W3yR.css +1 -0
  61. package/client/dist/assets/Trackers-Bfoiyuzy.js +2 -0
  62. package/client/dist/assets/Trackers-D5XBOCDp.css +1 -0
  63. package/client/dist/assets/Tv-Ca0fCdko.js +3 -0
  64. package/client/dist/assets/Tv-CuEl3Iol.css +1 -0
  65. package/client/dist/assets/TvInfo-Bu36xJyO.css +1 -0
  66. package/client/dist/assets/TvInfo-D_scgw_h.js +1 -0
  67. package/client/dist/assets/TvPopup-YTI7dEbJ.js +1 -0
  68. package/client/dist/assets/TvSearch-CxU33UOf.js +1 -0
  69. package/client/dist/assets/TvWatchlist-B4X1fWEH.js +1 -0
  70. package/client/dist/assets/VirtualKeyboard-6ZNTym4R.js +1 -0
  71. package/client/dist/assets/VirtualKeyboard-KIPc5ZGH.css +1 -0
  72. package/client/dist/assets/Watchlist-BiDEU0iV.css +1 -0
  73. package/client/dist/assets/Watchlist-P6YMZUdF.js +1 -0
  74. package/client/dist/assets/Watchlist.module-XGtVQr1d.js +1 -0
  75. package/client/dist/assets/hls-BOLXTnjL.js +42 -0
  76. package/client/dist/assets/hls-C4cG9wQw.js +1 -0
  77. package/client/dist/assets/index-BOKlZEHq.js +30354 -0
  78. package/client/dist/assets/index-CUm1meqr.css +1 -0
  79. package/client/dist/assets/index-IZgh1beL.js +178 -0
  80. package/client/dist/assets/useAnimeInfoData-DeuwDSnI.js +1 -0
  81. package/client/dist/assets/useAsmr-CbMVRjmV.js +1 -0
  82. package/client/dist/assets/useAsmrPopup-D91Q3CBg.js +1 -0
  83. package/client/dist/assets/useIsMobile-Dm_b6Ob3.js +1 -0
  84. package/client/dist/assets/useMangaLibrary-BPvFb1lU.js +1 -0
  85. package/client/dist/assets/useMangaPopup-BOL2mQJs.js +1 -0
  86. package/client/dist/assets/useMatureConsent-CT7-KfzG.js +1 -0
  87. package/client/dist/assets/useProviders-BsRGONMA.js +1 -0
  88. package/client/dist/assets/useTv-D37MfTHA.js +1 -0
  89. package/client/dist/assets/useTvLibrary-Cwxgy_Bg.js +1 -0
  90. package/client/dist/assets/vendor-core-8R2fFbyN.js +1 -0
  91. package/client/dist/assets/vendor-icons-CDMuNDeL.js +1 -0
  92. package/client/dist/assets/vendor-query-BXRECSI6.js +1 -0
  93. package/client/dist/assets/vendor-ui-CUDCvRS_.js +1 -0
  94. package/client/dist/av-sync-test.mp4 +0 -0
  95. package/client/dist/favicon.ico +0 -0
  96. package/client/dist/index.html +46 -0
  97. package/client/dist/logo-mochi.png +0 -0
  98. package/client/dist/logo-sakura.png +0 -0
  99. package/client/dist/logo.png +0 -0
  100. package/client/dist/placeholder.svg +4 -0
  101. package/client/dist/robots.txt +3 -0
  102. package/orchestrator.js +534 -0
  103. package/package.json +85 -0
  104. package/server/dist/app-auth.js +177 -0
  105. package/server/dist/config.js +175 -0
  106. package/server/dist/controllers/asmr-library.controller.js +288 -0
  107. package/server/dist/controllers/auth.controller.js +225 -0
  108. package/server/dist/controllers/data.controller.js +766 -0
  109. package/server/dist/controllers/insights.controller.js +198 -0
  110. package/server/dist/controllers/manga-library.controller.js +296 -0
  111. package/server/dist/controllers/music.controller.js +698 -0
  112. package/server/dist/controllers/proxy.controller.js +820 -0
  113. package/server/dist/controllers/settings.controller.js +609 -0
  114. package/server/dist/controllers/tv-library.controller.js +310 -0
  115. package/server/dist/controllers/watchlist.controller.js +1015 -0
  116. package/server/dist/db.js +145 -0
  117. package/server/dist/discord-gateway.js +391 -0
  118. package/server/dist/discord-rpc.js +427 -0
  119. package/server/dist/github-sync.js +512 -0
  120. package/server/dist/google.js +748 -0
  121. package/server/dist/lib/anilist.js +1549 -0
  122. package/server/dist/lib/anischedule.js +194 -0
  123. package/server/dist/lib/discord-roles-sync.service.js +130 -0
  124. package/server/dist/lib/ipc.js +24 -0
  125. package/server/dist/lib/kitsu.js +544 -0
  126. package/server/dist/lib/mal.js +934 -0
  127. package/server/dist/lib/migration.js +329 -0
  128. package/server/dist/lib/offline-db.js +401 -0
  129. package/server/dist/lib/temp-ids.js +3 -0
  130. package/server/dist/lib/tmdb.js +72 -0
  131. package/server/dist/lib/tracker/anilist-tracker.js +245 -0
  132. package/server/dist/lib/tracker/sync.service.js +496 -0
  133. package/server/dist/lib/ytmusic.js +125 -0
  134. package/server/dist/logger.js +16 -0
  135. package/server/dist/providers/manga/manga.types.js +7 -0
  136. package/server/dist/providers/provider.interface.js +1 -0
  137. package/server/dist/providers/remote-loader.js +574 -0
  138. package/server/dist/providers/remote-types.js +117 -0
  139. package/server/dist/providers/title-matching.js +225 -0
  140. package/server/dist/providers/tv.types.js +1 -0
  141. package/server/dist/rclone.js +120 -0
  142. package/server/dist/repositories/asmr.repository.js +123 -0
  143. package/server/dist/repositories/insights.repository.js +85 -0
  144. package/server/dist/repositories/library.repository.js +108 -0
  145. package/server/dist/repositories/mal-cache.repository.js +55 -0
  146. package/server/dist/repositories/manga.repository.js +138 -0
  147. package/server/dist/repositories/notifications.repository.js +41 -0
  148. package/server/dist/repositories/queue.repository.js +61 -0
  149. package/server/dist/repositories/settings.repository.js +12 -0
  150. package/server/dist/repositories/shows-meta.repository.js +42 -0
  151. package/server/dist/repositories/temp-show-ids.repository.js +51 -0
  152. package/server/dist/repositories/tv.repository.js +151 -0
  153. package/server/dist/repositories/watched-episodes.repository.js +52 -0
  154. package/server/dist/repositories/watchlist.repository.js +71 -0
  155. package/server/dist/request-logger.js +16 -0
  156. package/server/dist/routes/asmr-library.routes.js +23 -0
  157. package/server/dist/routes/asmr.routes.js +81 -0
  158. package/server/dist/routes/auth.routes.js +25 -0
  159. package/server/dist/routes/data.routes.js +47 -0
  160. package/server/dist/routes/discord-gateway.routes.js +51 -0
  161. package/server/dist/routes/insights.routes.js +34 -0
  162. package/server/dist/routes/lan-auth.routes.js +46 -0
  163. package/server/dist/routes/manga-library.routes.js +23 -0
  164. package/server/dist/routes/manga.routes.js +141 -0
  165. package/server/dist/routes/music.routes.js +20 -0
  166. package/server/dist/routes/providers.routes.js +17 -0
  167. package/server/dist/routes/proxy.routes.js +12 -0
  168. package/server/dist/routes/radio.routes.js +138 -0
  169. package/server/dist/routes/settings.routes.js +24 -0
  170. package/server/dist/routes/tracker.routes.js +164 -0
  171. package/server/dist/routes/translate.routes.js +88 -0
  172. package/server/dist/routes/tv-library.routes.js +23 -0
  173. package/server/dist/routes/tv.routes.js +629 -0
  174. package/server/dist/routes/watchlist.routes.js +341 -0
  175. package/server/dist/scripts/backfill-episode-duration.js +42 -0
  176. package/server/dist/server.js +529 -0
  177. package/server/dist/shipped-defaults.js +7 -0
  178. package/server/dist/sync-config.js +23 -0
  179. package/server/dist/sync-payload.js +91 -0
  180. package/server/dist/sync.js +1128 -0
  181. package/server/dist/utils/cache.utils.js +25 -0
  182. package/server/dist/utils/cookie.utils.js +13 -0
  183. package/server/dist/utils/db-utils.js +9 -0
  184. package/server/dist/utils/env.utils.js +77 -0
  185. package/server/dist/utils/http.utils.js +70 -0
  186. package/server/dist/utils/machine-id.js +40 -0
  187. package/server/dist/utils/megaplay.utils.js +41 -0
  188. package/server/dist/utils/request-context.js +2 -0
  189. package/server/dist/utils/security.utils.js +94 -0
  190. package/server/package.json +44 -0
@@ -0,0 +1,529 @@
1
+ process.setMaxListeners(100);
2
+ import { EventEmitter } from 'events';
3
+ EventEmitter.defaultMaxListeners = 100;
4
+ import express from 'express';
5
+ import path from 'path';
6
+ import cors from 'cors';
7
+ import compression from 'compression';
8
+ import { AppCache } from './utils/cache.utils.js';
9
+ import fs from 'fs';
10
+ import crypto from 'crypto';
11
+ import logger from './logger.js';
12
+ import { notifyServerExit } from './lib/ipc.js';
13
+ import { crossSiteProtectionMiddleware, isAllowedOrigin } from './utils/security.utils.js';
14
+ import { requestLogger } from './request-logger.js';
15
+ import { githubSyncService } from './github-sync.js';
16
+ import { CONFIG } from './config.js';
17
+ import { initializeDatabase, initializeMangaDatabase, initializeTvDatabase, initializeAsmrDatabase, syncDownOnBoot, syncUp, mangaSyncDownOnBoot, mangaSyncUp, tvSyncDownOnBoot, tvSyncUp, asmrSyncDownOnBoot, asmrSyncUp, initSyncProvider, waitForSync, getActiveProvider, } from './sync.js';
18
+ import { createAuthRouter } from './routes/auth.routes.js';
19
+ import { createLanAuthRouter } from './routes/lan-auth.routes.js';
20
+ import { lanAuthMiddleware } from './app-auth.js';
21
+ import { createWatchlistRouter } from './routes/watchlist.routes.js';
22
+ import { createDataRouter } from './routes/data.routes.js';
23
+ import { createAsmrRouter } from './routes/asmr.routes.js';
24
+ import { createAsmrLibraryRouter } from './routes/asmr-library.routes.js';
25
+ import { createMangaRouter } from './routes/manga.routes.js';
26
+ import { createMangaLibraryRouter } from './routes/manga-library.routes.js';
27
+ import { createRadioRouter } from './routes/radio.routes.js';
28
+ import { createMusicRouter } from './routes/music.routes.js';
29
+ import { createTvRouter } from './routes/tv.routes.js';
30
+ import { createTvLibraryRouter } from './routes/tv-library.routes.js';
31
+ import { createProxyRouter } from './routes/proxy.routes.js';
32
+ import { createProvidersRouter } from './routes/providers.routes.js';
33
+ import { loadRemoteProviders } from './providers/remote-loader.js';
34
+ import { createSettingsRouter } from './routes/settings.routes.js';
35
+ import { createInsightsRouter } from './routes/insights.routes.js';
36
+ import { createTranslateRouter } from './routes/translate.routes.js';
37
+ import { createDiscordGatewayRouter } from './routes/discord-gateway.routes.js';
38
+ import { createTrackerRouter } from './routes/tracker.routes.js';
39
+ import { discordRPCService } from './discord-rpc.js';
40
+ import { discordGatewayService } from './discord-gateway.js';
41
+ import { SettingsRepository } from './repositories/settings.repository.js';
42
+ import { requestContext } from './utils/request-context.js';
43
+ import { checkAnilistStatus } from './lib/anilist.js';
44
+ import { offlineDb } from './lib/offline-db.js';
45
+ import { initDiscordRolesSync } from './lib/discord-roles-sync.service.js';
46
+ const app = express();
47
+ app.use(requestLogger);
48
+ app.get('/api/health', (_req, res) => {
49
+ if (isShuttingDown) {
50
+ return res.status(503).json({ status: 'shutting-down', ready: false });
51
+ }
52
+ if (!db) {
53
+ return res.status(503).json({ status: 'starting', ready: false });
54
+ }
55
+ res.json({ status: 'ok', ready: true });
56
+ });
57
+ app.use((req, res, next) => {
58
+ const store = new Map();
59
+ if (req.headers['x-animepahe-ua']) {
60
+ store.set('ua', req.headers['x-animepahe-ua']);
61
+ }
62
+ if (req.headers['x-animepahe-cookie']) {
63
+ store.set('cookie', req.headers['x-animepahe-cookie']);
64
+ }
65
+ if (req.headers['x-jasmr-ua']) {
66
+ store.set('jasmr_ua', req.headers['x-jasmr-ua']);
67
+ }
68
+ if (req.headers['x-jasmr-cookie']) {
69
+ store.set('jasmr_cookie', req.headers['x-jasmr-cookie']);
70
+ }
71
+ requestContext.run(store, next);
72
+ });
73
+ const apiCache = new AppCache({ ttlSeconds: 3600, maxKeys: 5000 });
74
+ const providers = {};
75
+ const mangaProviders = {};
76
+ const tvProviders = {};
77
+ let remoteCatalog = [];
78
+ function parseEnabledProviders() {
79
+ return CONFIG.PROVIDER_ENABLED.split(',')
80
+ .map((s) => s.trim().toLowerCase())
81
+ .filter(Boolean);
82
+ }
83
+ function getProviderCatalog() {
84
+ return remoteCatalog;
85
+ }
86
+ async function refreshRemoteProviders() {
87
+ const url = CONFIG.PROVIDER_REPO_URL.trim();
88
+ if (!url)
89
+ return getProviderCatalog();
90
+ try {
91
+ const result = await loadRemoteProviders({
92
+ cache: apiCache,
93
+ registryUrl: url,
94
+ enabledProviders: parseEnabledProviders(),
95
+ });
96
+ if (result.catalog.length === 0) {
97
+ logger.warn('remote providers refresh returned nothing, keeping current set');
98
+ return getProviderCatalog();
99
+ }
100
+ for (const key of Object.keys(providers)) {
101
+ if (!(key in result.providers))
102
+ delete providers[key];
103
+ }
104
+ Object.assign(providers, result.providers);
105
+ for (const key of Object.keys(mangaProviders)) {
106
+ if (!(key in result.mangaProviders))
107
+ delete mangaProviders[key];
108
+ }
109
+ Object.assign(mangaProviders, result.mangaProviders);
110
+ for (const key of Object.keys(tvProviders)) {
111
+ if (!(key in result.tvProviders))
112
+ delete tvProviders[key];
113
+ }
114
+ Object.assign(tvProviders, result.tvProviders);
115
+ remoteCatalog = result.catalog;
116
+ logger.info({
117
+ count: Object.keys(result.providers).length,
118
+ mangaCount: Object.keys(result.mangaProviders).length,
119
+ tvCount: Object.keys(result.tvProviders).length,
120
+ }, 'remote providers refreshed');
121
+ }
122
+ catch (err) {
123
+ logger.error({ err }, 'remote providers refresh failed');
124
+ }
125
+ return getProviderCatalog();
126
+ }
127
+ let db;
128
+ let mangaDb;
129
+ let tvDb;
130
+ let asmrDb;
131
+ let isShuttingDown = false;
132
+ let bootSyncing = true;
133
+ async function runSyncSequence(database, mangaDatabase, preferredProvider) {
134
+ const dbName = CONFIG.IS_DEV ? CONFIG.DB_NAME_DEV : CONFIG.DB_NAME_PROD;
135
+ const dbPath = path.join(CONFIG.ROOT, dbName);
136
+ const remoteFolder = CONFIG.IS_DEV ? CONFIG.REMOTE_FOLDER_DEV : CONFIG.REMOTE_FOLDER_PROD;
137
+ await initSyncProvider(preferredProvider);
138
+ if (getActiveProvider() === 'github' && githubSyncService.isAuthenticated()) {
139
+ try {
140
+ await githubSyncService.migrateFromAniWebSync();
141
+ }
142
+ catch (err) {
143
+ logger.error({ err }, 'GitHub sync migration from ani-web failed');
144
+ }
145
+ }
146
+ const didDownload = await syncDownOnBoot(database, dbPath, remoteFolder, () => {
147
+ return new Promise((resolve) => {
148
+ if (database && !database.isClosedCheck()) {
149
+ database.checkpoint();
150
+ database.close(() => resolve());
151
+ }
152
+ else {
153
+ resolve();
154
+ }
155
+ });
156
+ });
157
+ let currentDb = database;
158
+ if (didDownload) {
159
+ db = await initializeDatabase(dbPath);
160
+ currentDb = db;
161
+ logger.info('Database re-initialized after sync.');
162
+ }
163
+ try {
164
+ await syncUp(currentDb, dbPath, remoteFolder);
165
+ }
166
+ catch (err) {
167
+ logger.error({ err }, 'Sync up on boot failed');
168
+ }
169
+ try {
170
+ await mangaSyncDownOnBoot(mangaDatabase, remoteFolder);
171
+ }
172
+ catch (err) {
173
+ logger.error({ err }, 'Manga sync down on boot failed');
174
+ }
175
+ try {
176
+ await mangaSyncUp(mangaDatabase, remoteFolder);
177
+ }
178
+ catch (err) {
179
+ logger.error({ err }, 'Manga sync up on boot failed');
180
+ }
181
+ try {
182
+ await tvSyncDownOnBoot(tvDb, remoteFolder);
183
+ }
184
+ catch (err) {
185
+ logger.error({ err }, 'TV sync down on boot failed');
186
+ }
187
+ try {
188
+ await tvSyncUp(tvDb, remoteFolder);
189
+ }
190
+ catch (err) {
191
+ logger.error({ err }, 'TV sync up on boot failed');
192
+ }
193
+ try {
194
+ await asmrSyncDownOnBoot(asmrDb, remoteFolder);
195
+ }
196
+ catch (err) {
197
+ logger.error({ err }, 'ASMR sync down on boot failed');
198
+ }
199
+ try {
200
+ await asmrSyncUp(asmrDb, remoteFolder);
201
+ }
202
+ catch (err) {
203
+ logger.error({ err }, 'ASMR sync up on boot failed');
204
+ }
205
+ }
206
+ app.use((req, res, next) => {
207
+ if (isShuttingDown) {
208
+ return res.status(503).send('Server is shutting down...');
209
+ }
210
+ if (!db || !mangaDb || !tvDb || !asmrDb) {
211
+ return res.status(503).send('Database initializing...');
212
+ }
213
+ if (bootSyncing && req.method !== 'GET' && req.method !== 'HEAD' && req.method !== 'OPTIONS') {
214
+ return res.status(503).send('Sync in progress...');
215
+ }
216
+ req.db = db;
217
+ req.mangaDb = mangaDb;
218
+ req.tvDb = tvDb;
219
+ req.asmrDb = asmrDb;
220
+ next();
221
+ });
222
+ app.use(compression({
223
+ level: 2,
224
+ threshold: 1024,
225
+ filter: (req, res) => {
226
+ if (req.headers['x-no-compression']) {
227
+ return false;
228
+ }
229
+ return compression.filter(req, res);
230
+ },
231
+ }));
232
+ app.use(cors({
233
+ origin: (origin, callback) => {
234
+ callback(null, isAllowedOrigin(origin));
235
+ },
236
+ credentials: true,
237
+ }));
238
+ app.use(crossSiteProtectionMiddleware);
239
+ app.use(express.json({ limit: '10mb' }));
240
+ app.use('/api/auth', createLanAuthRouter());
241
+ app.use(lanAuthMiddleware);
242
+ app.use('/api/auth', createAuthRouter((database, mangaDatabase, preferred) => runSyncSequence(database, mangaDatabase, preferred)));
243
+ const { router: watchlistRouter, stopDiscovery } = createWatchlistRouter(() => db);
244
+ app.use('/api', watchlistRouter);
245
+ app.use('/api', createDataRouter(apiCache, providers, getProviderCatalog));
246
+ app.use('/api', createAsmrRouter(apiCache, () => providers['jasmr']));
247
+ app.use('/api', createAsmrLibraryRouter());
248
+ app.use('/api', createMangaRouter(apiCache, (name) => mangaProviders[name]));
249
+ app.use('/api', createMangaLibraryRouter());
250
+ app.use('/api', createRadioRouter(apiCache));
251
+ app.use('/api', createMusicRouter(apiCache));
252
+ app.use('/api', createTvRouter(apiCache, (name) => tvProviders[name]));
253
+ app.use('/api', createTvLibraryRouter());
254
+ app.use('/api', createProxyRouter());
255
+ app.use('/api', createProvidersRouter(getProviderCatalog, refreshRemoteProviders));
256
+ app.use('/api', createInsightsRouter());
257
+ app.use('/api', createTranslateRouter());
258
+ app.use('/api', createDiscordGatewayRouter());
259
+ app.use('/api', createTrackerRouter());
260
+ app.use('/api', createSettingsRouter(() => db, initializeDatabase, (newDb) => {
261
+ db = newDb;
262
+ }));
263
+ if (!CONFIG.IS_DEV) {
264
+ const frontendPath = path.join(CONFIG.PACKAGE_ROOT, 'client', 'dist');
265
+ logger.info(`Serving frontend from: ${frontendPath}`);
266
+ app.use(express.static(frontendPath));
267
+ app.get(/^(?!\/api).+/, (req, res) => {
268
+ res.sendFile('index.html', { root: frontendPath }, (err) => {
269
+ if (err) {
270
+ logger.error({ err }, `Failed to serve index.html from ${frontendPath}`);
271
+ if (!res.headersSent) {
272
+ res.status(500).send('Server Error: Frontend build not found.');
273
+ }
274
+ }
275
+ });
276
+ });
277
+ }
278
+ app.use((err, req, res, next) => {
279
+ if (err instanceof SyntaxError &&
280
+ 'body' in err &&
281
+ req.headers['content-type']?.includes('application/json')) {
282
+ if (!res.headersSent) {
283
+ return res.status(400).json({ error: 'Invalid JSON', status: 400 });
284
+ }
285
+ }
286
+ logger.error({ err, url: req.url, method: req.method }, 'Unhandled error');
287
+ if (res.headersSent) {
288
+ return next(err);
289
+ }
290
+ res.status(err.status || 500).json({
291
+ error: err.message || 'Internal Server Error',
292
+ status: err.status || 500,
293
+ });
294
+ });
295
+ async function main() {
296
+ const dbName = CONFIG.IS_DEV ? CONFIG.DB_NAME_DEV : CONFIG.DB_NAME_PROD;
297
+ const dbPath = path.join(CONFIG.ROOT, dbName);
298
+ const remoteFolder = CONFIG.IS_DEV ? CONFIG.REMOTE_FOLDER_DEV : CONFIG.REMOTE_FOLDER_PROD;
299
+ db = await initializeDatabase(dbPath);
300
+ logger.info(`Database initialized at ${dbPath}`);
301
+ const mangaDbName = CONFIG.IS_DEV ? CONFIG.MANGA_DB_NAME_DEV : CONFIG.MANGA_DB_NAME_PROD;
302
+ const mangaDbPath = path.join(CONFIG.ROOT, mangaDbName);
303
+ mangaDb = await initializeMangaDatabase(mangaDbPath);
304
+ logger.info(`Manga database initialized at ${mangaDbPath}`);
305
+ const tvDbName = CONFIG.IS_DEV ? CONFIG.TV_DB_NAME_DEV : CONFIG.TV_DB_NAME_PROD;
306
+ const tvDbPath = path.join(CONFIG.ROOT, tvDbName);
307
+ tvDb = await initializeTvDatabase(tvDbPath);
308
+ logger.info(`TV database initialized at ${tvDbPath}`);
309
+ const asmrDbName = CONFIG.IS_DEV ? CONFIG.ASMR_DB_NAME_DEV : CONFIG.ASMR_DB_NAME_PROD;
310
+ const asmrDbPath = path.join(CONFIG.ROOT, asmrDbName);
311
+ asmrDb = await initializeAsmrDatabase(asmrDbPath);
312
+ logger.info(`ASMR database initialized at ${asmrDbPath}`);
313
+ await offlineDb.init(db);
314
+ if (offlineDb.checkWeeklyUpdateDue(db)) {
315
+ logger.info('Weekly offline database update is due on startup, starting background update...');
316
+ offlineDb.executeScheduledUpdate(db).catch((err) => {
317
+ logger.warn({ err: err?.message }, 'Startup scheduled offline database update failed');
318
+ });
319
+ }
320
+ const rpcEnabledSetting = await SettingsRepository.getByKey(db, 'discordRPCEnabled');
321
+ const isRpcEnabled = rpcEnabledSetting ? rpcEnabledSetting.value === 'true' : true;
322
+ await discordRPCService.setEnabled(isRpcEnabled);
323
+ discordGatewayService.setEnabled(isRpcEnabled);
324
+ checkAnilistStatus().catch(() => { });
325
+ initDiscordRolesSync(db);
326
+ if (CONFIG.PROVIDER_REPO_URL.trim()) {
327
+ setInterval(() => {
328
+ refreshRemoteProviders().catch((err) => logger.error({ err }, 'periodic providers refresh failed'));
329
+ }, CONFIG.PROVIDER_REPO_REFRESH_MS).unref();
330
+ }
331
+ if (!fs.existsSync(CONFIG.LOCAL_MANIFEST_PATH)) {
332
+ fs.writeFileSync(CONFIG.LOCAL_MANIFEST_PATH, JSON.stringify({ version: 0 }));
333
+ }
334
+ if (!fs.existsSync(CONFIG.MANGA_LOCAL_MANIFEST_PATH)) {
335
+ fs.writeFileSync(CONFIG.MANGA_LOCAL_MANIFEST_PATH, JSON.stringify({ version: 0 }));
336
+ }
337
+ if (!fs.existsSync(CONFIG.TV_LOCAL_MANIFEST_PATH)) {
338
+ fs.writeFileSync(CONFIG.TV_LOCAL_MANIFEST_PATH, JSON.stringify({ version: 0 }));
339
+ }
340
+ if (!fs.existsSync(CONFIG.ASMR_LOCAL_MANIFEST_PATH)) {
341
+ fs.writeFileSync(CONFIG.ASMR_LOCAL_MANIFEST_PATH, JSON.stringify({ version: 0 }));
342
+ }
343
+ let hasUnsyncedChanges = false;
344
+ let hasMangaUnsyncedChanges = false;
345
+ let hasTvUnsyncedChanges = false;
346
+ let hasAsmrUnsyncedChanges = false;
347
+ const watcher = fs.watch(CONFIG.LOCAL_MANIFEST_PATH, (eventType) => {
348
+ if (eventType === 'change' || eventType === 'rename') {
349
+ hasUnsyncedChanges = true;
350
+ }
351
+ });
352
+ const mangaWatcher = fs.watch(CONFIG.MANGA_LOCAL_MANIFEST_PATH, (eventType) => {
353
+ if (eventType === 'change' || eventType === 'rename') {
354
+ hasMangaUnsyncedChanges = true;
355
+ }
356
+ });
357
+ const tvWatcher = fs.watch(CONFIG.TV_LOCAL_MANIFEST_PATH, (eventType) => {
358
+ if (eventType === 'change' || eventType === 'rename') {
359
+ hasTvUnsyncedChanges = true;
360
+ }
361
+ });
362
+ const asmrWatcher = fs.watch(CONFIG.ASMR_LOCAL_MANIFEST_PATH, (eventType) => {
363
+ if (eventType === 'change' || eventType === 'rename') {
364
+ hasAsmrUnsyncedChanges = true;
365
+ }
366
+ });
367
+ const expressServer = app.listen(CONFIG.PORT, () => {
368
+ logger.info(`Server running on http://localhost:${CONFIG.PORT}`);
369
+ });
370
+ runSyncSequence(db, mangaDb)
371
+ .catch((err) => logger.error({ err }, 'background boot sync failed'))
372
+ .finally(() => {
373
+ bootSyncing = false;
374
+ });
375
+ refreshRemoteProviders().catch((err) => logger.error({ err }, 'background providers refresh failed'));
376
+ const syncInterval = setInterval(async () => {
377
+ if (hasUnsyncedChanges) {
378
+ logger.info('Uploading accumulated database changes...');
379
+ hasUnsyncedChanges = false;
380
+ try {
381
+ await syncUp(db, dbPath, remoteFolder);
382
+ }
383
+ catch (err) {
384
+ logger.error({ err }, 'Failed to upload database changes');
385
+ hasUnsyncedChanges = true;
386
+ }
387
+ }
388
+ if (hasMangaUnsyncedChanges) {
389
+ logger.info('Uploading accumulated manga database changes...');
390
+ hasMangaUnsyncedChanges = false;
391
+ try {
392
+ await mangaSyncUp(mangaDb, remoteFolder);
393
+ }
394
+ catch (err) {
395
+ logger.error({ err }, 'Failed to upload manga database changes');
396
+ hasMangaUnsyncedChanges = true;
397
+ }
398
+ }
399
+ if (hasTvUnsyncedChanges) {
400
+ logger.info('Uploading accumulated TV database changes...');
401
+ hasTvUnsyncedChanges = false;
402
+ try {
403
+ await tvSyncUp(tvDb, remoteFolder);
404
+ }
405
+ catch (err) {
406
+ logger.error({ err }, 'Failed to upload TV database changes');
407
+ hasTvUnsyncedChanges = true;
408
+ }
409
+ }
410
+ if (hasAsmrUnsyncedChanges) {
411
+ logger.info('Uploading accumulated ASMR database changes...');
412
+ hasAsmrUnsyncedChanges = false;
413
+ try {
414
+ await asmrSyncUp(asmrDb, remoteFolder);
415
+ }
416
+ catch (err) {
417
+ logger.error({ err }, 'Failed to upload ASMR database changes');
418
+ hasAsmrUnsyncedChanges = true;
419
+ }
420
+ }
421
+ }, 300000);
422
+ const offlineDbInterval = setInterval(() => {
423
+ if (offlineDb.checkWeeklyUpdateDue(db)) {
424
+ logger.info('Weekly offline database update triggered by periodic schedule...');
425
+ offlineDb.executeScheduledUpdate(db).catch((err) => {
426
+ logger.warn({ err: err?.message }, 'Interval scheduled offline database update failed');
427
+ });
428
+ }
429
+ }, 6 * 60 * 60 * 1000);
430
+ offlineDbInterval.unref();
431
+ const shutdown = async (signal) => {
432
+ if (isShuttingDown)
433
+ return;
434
+ isShuttingDown = true;
435
+ stopDiscovery();
436
+ clearInterval(syncInterval);
437
+ clearInterval(offlineDbInterval);
438
+ discordRPCService.disconnect();
439
+ discordGatewayService.shutdown();
440
+ await watcher.close();
441
+ await mangaWatcher.close();
442
+ await tvWatcher.close();
443
+ await asmrWatcher.close();
444
+ if (expressServer) {
445
+ await new Promise((resolve) => expressServer.close(() => resolve()));
446
+ }
447
+ if (hasUnsyncedChanges) {
448
+ logger.info('Sync on shutdown: uploading final database changes...');
449
+ hasUnsyncedChanges = false;
450
+ try {
451
+ await syncUp(db, dbPath, remoteFolder);
452
+ }
453
+ catch (e) {
454
+ logger.error({ err: e }, 'Final sync on shutdown failed');
455
+ }
456
+ }
457
+ if (hasMangaUnsyncedChanges) {
458
+ logger.info('Sync on shutdown: uploading final manga database changes...');
459
+ hasMangaUnsyncedChanges = false;
460
+ try {
461
+ await mangaSyncUp(mangaDb, remoteFolder);
462
+ }
463
+ catch (e) {
464
+ logger.error({ err: e }, 'Final manga sync on shutdown failed');
465
+ }
466
+ }
467
+ if (hasTvUnsyncedChanges) {
468
+ logger.info('Sync on shutdown: uploading final TV database changes...');
469
+ hasTvUnsyncedChanges = false;
470
+ try {
471
+ await tvSyncUp(tvDb, remoteFolder);
472
+ }
473
+ catch (e) {
474
+ logger.error({ err: e }, 'Final TV sync on shutdown failed');
475
+ }
476
+ }
477
+ if (hasAsmrUnsyncedChanges) {
478
+ logger.info('Sync on shutdown: uploading final ASMR database changes...');
479
+ hasAsmrUnsyncedChanges = false;
480
+ try {
481
+ await asmrSyncUp(asmrDb, remoteFolder);
482
+ }
483
+ catch (e) {
484
+ logger.error({ err: e }, 'Final ASMR sync on shutdown failed');
485
+ }
486
+ }
487
+ await waitForSync();
488
+ asmrDb.close(() => { });
489
+ tvDb.close(() => { });
490
+ mangaDb.close(() => { });
491
+ db.close(() => {
492
+ notifyServerExit();
493
+ if (signal === 'SIGUSR2') {
494
+ process.kill(process.pid, 'SIGUSR2');
495
+ }
496
+ else {
497
+ setTimeout(() => process.exit(0), 100).unref();
498
+ }
499
+ });
500
+ };
501
+ process.on('SIGINT', () => shutdown('SIGINT'));
502
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
503
+ process.on('SIGHUP', () => shutdown('SIGHUP'));
504
+ process.once('SIGUSR2', () => shutdown('SIGUSR2'));
505
+ app.post('/api/internal/shutdown', (req, res) => {
506
+ const isLoopback = req.ip === '::1' || req.ip === '127.0.0.1' || req.ip === '::ffff:127.0.0.1';
507
+ if (!isLoopback) {
508
+ res.status(403).json({ error: 'Forbidden' });
509
+ return;
510
+ }
511
+ const expectedToken = process.env.INTERNAL_SHUTDOWN_TOKEN;
512
+ const providedToken = req.headers['x-internal-token'];
513
+ if (expectedToken) {
514
+ const expBuf = Buffer.from(expectedToken);
515
+ const provBuf = Buffer.from(typeof providedToken === 'string' ? providedToken : '');
516
+ if (expBuf.length !== provBuf.length || !crypto.timingSafeEqual(expBuf, provBuf)) {
517
+ logger.warn('Unauthorized internal shutdown attempt rejected: invalid token');
518
+ res.status(403).json({ error: 'Forbidden' });
519
+ return;
520
+ }
521
+ }
522
+ res.status(200).json({ message: 'Shutting down' });
523
+ setTimeout(() => shutdown(), 500);
524
+ });
525
+ }
526
+ main().catch((err) => {
527
+ logger.error({ err }, 'Server failed to start');
528
+ process.exit(1);
529
+ });
@@ -0,0 +1,7 @@
1
+ export const SHIPPED_DEFAULTS = {
2
+ "GITHUB_CLIENT_ID": "Ov23liT1ZtPk7XtN9PZk",
3
+ "GOOGLE_AUTH_WORKER_URL": "https://google-auth-client.ramiserifpersia.workers.dev",
4
+ "DISCORD_CLIENT_ID": "1512253778022367426",
5
+ "DISCORD_ROLES_WORKER_URL": "https://dango-discord-roles.ramiserifpersia.workers.dev",
6
+ "PROVIDER_REPO_URL": "https://raw.githubusercontent.com/serifpersia/dango-providers/main/registry.json"
7
+ };
@@ -0,0 +1,23 @@
1
+ import logger from './logger.js';
2
+ const log = logger.child({ module: 'SyncConfig' });
3
+ let activeRemote;
4
+ export function setActiveRemote(remote) {
5
+ log.info({ remote }, 'Setting active sync remote');
6
+ activeRemote = remote;
7
+ }
8
+ export function getActiveRemote() {
9
+ return activeRemote;
10
+ }
11
+ export async function initialize() {
12
+ if (activeRemote !== 'gdrive') {
13
+ log.info('Active remote is not gdrive, skipping gdrive-specific initialization.');
14
+ return;
15
+ }
16
+ log.info('gdrive is the active remote.');
17
+ }
18
+ export function getRemoteString(remoteDir) {
19
+ if (!activeRemote) {
20
+ throw new Error('Cannot get remote string: active remote is not set.');
21
+ }
22
+ return `${activeRemote}:${remoteDir}`;
23
+ }
@@ -0,0 +1,91 @@
1
+ import path from 'path';
2
+ import logger from './logger.js';
3
+ import { CONFIG } from './config.js';
4
+ import { dbAll, dbGet } from './utils/db-utils.js';
5
+ import { isTempSyncRow } from './lib/temp-ids.js';
6
+ const log = logger.child({ module: 'SyncPayload' });
7
+ export const ANIME_SYNC_TABLES = [
8
+ 'watchlist',
9
+ 'watched_episodes',
10
+ 'queue',
11
+ 'settings',
12
+ 'shows_meta',
13
+ 'sync_metadata',
14
+ 'dismissed_notifications',
15
+ 'discovered_notifications',
16
+ ];
17
+ export const MANGA_SYNC_TABLES = ['manga_library', 'manga_progress', 'sync_metadata'];
18
+ export const TV_SYNC_TABLES = ['tv_library', 'tv_progress', 'sync_metadata'];
19
+ export const ASMR_SYNC_TABLES = ['asmr_library', 'asmr_progress', 'sync_metadata'];
20
+ export function readPayloadVersion(payload) {
21
+ const meta = payload.tables['sync_metadata'];
22
+ const row = meta?.find((r) => r.key === 'db_version');
23
+ const v = row?.value;
24
+ return typeof v === 'number' ? v : Number(v || payload.version || 0);
25
+ }
26
+ export function normalizePayload(input, tables, label) {
27
+ if (!input || typeof input !== 'object' || !('tables' in input)) {
28
+ throw new Error(`Invalid ${label} sync payload.`);
29
+ }
30
+ const payload = input;
31
+ for (const table of tables) {
32
+ if (!Array.isArray(payload.tables?.[table])) {
33
+ throw new Error(`Invalid ${label} sync payload: missing ${String(table)}.`);
34
+ }
35
+ }
36
+ return payload;
37
+ }
38
+ export function exportTables(db, tables) {
39
+ const out = {};
40
+ for (const table of tables) {
41
+ const rows = dbAll(db, `SELECT * FROM "${String(table).replace(/"/g, '""')}"`);
42
+ out[table] = rows.filter((row) => !isTempSyncRow(row));
43
+ }
44
+ return {
45
+ version: readPayloadVersion({ version: 0, exportedAt: '', tables: out }),
46
+ exportedAt: new Date().toISOString(),
47
+ tables: out,
48
+ };
49
+ }
50
+ export function importTables(db, tables, payload, opts) {
51
+ const asRec = payload.tables;
52
+ const remoteLibraryRows = opts.libraryTables.reduce((n, t) => n + (asRec[t]?.length ?? 0), 0);
53
+ const localLibraryRows = opts.libraryTables.length === 0
54
+ ? 1
55
+ : (dbGet(db, `SELECT ${opts.libraryTables
56
+ .map((t) => `(SELECT COUNT(*) FROM "${String(t).replace(/"/g, '""')}")`)
57
+ .join(' + ')} AS n`)?.n ?? 0);
58
+ if (remoteLibraryRows === 0 && localLibraryRows > 0) {
59
+ throw new Error('Sync down refused: remote library is empty while local library has data');
60
+ }
61
+ try {
62
+ db.backup(path.join(CONFIG.ROOT, opts.backupName));
63
+ }
64
+ catch (err) {
65
+ log.warn({ err }, 'Pre-sync backup failed, continuing without backup');
66
+ }
67
+ const localColumns = new Map();
68
+ for (const table of tables) {
69
+ const cols = db.all(`PRAGMA table_info("${String(table)}")`);
70
+ localColumns.set(String(table), new Set(cols.map((c) => c.name)));
71
+ }
72
+ db.serialize(() => {
73
+ for (const table of tables) {
74
+ db.run(`DELETE FROM "${String(table).replace(/"/g, '""')}"`);
75
+ }
76
+ for (const table of tables) {
77
+ const known = localColumns.get(String(table));
78
+ for (const row of asRec[String(table)] || []) {
79
+ if (isTempSyncRow(row))
80
+ continue;
81
+ const columns = Object.keys(row).filter((c) => known?.has(c));
82
+ if (columns.length === 0)
83
+ continue;
84
+ const columnSql = columns.map((c) => `"${c.replace(/"/g, '""')}"`).join(', ');
85
+ const placeholders = columns.map(() => '?').join(', ');
86
+ const values = columns.map((c) => row[c]);
87
+ db.run(`INSERT INTO "${String(table).replace(/"/g, '""')}" (${columnSql}) VALUES (${placeholders})`, values);
88
+ }
89
+ }
90
+ });
91
+ }