@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,71 @@
1
+ import { dbAll, dbGet, dbRun } from '../utils/db-utils.js';
2
+ export const WatchlistRepository = {
3
+ getById: (db, id) => dbGet(db, 'SELECT * FROM watchlist WHERE id = ?', [id]),
4
+ exists: (db, id) => {
5
+ const row = dbGet(db, 'SELECT EXISTS(SELECT 1 FROM watchlist WHERE id = ?) as inWatchlist', [id]);
6
+ return !!(row && row.inWatchlist);
7
+ },
8
+ getAll: (db, status, limit, offset) => {
9
+ let query = 'SELECT * FROM watchlist';
10
+ const params = [];
11
+ if (status && status !== 'All') {
12
+ query += ' WHERE status = ?';
13
+ params.push(status);
14
+ }
15
+ query += ' ORDER BY rowid DESC';
16
+ if (limit !== undefined && offset !== undefined) {
17
+ query += ' LIMIT ? OFFSET ?';
18
+ params.push(limit, offset);
19
+ }
20
+ return dbAll(db, query, params);
21
+ },
22
+ getCount: (db, status) => {
23
+ let query = 'SELECT COUNT(*) as total FROM watchlist';
24
+ const params = [];
25
+ if (status && status !== 'All') {
26
+ query += ' WHERE status = ?';
27
+ params.push(status);
28
+ }
29
+ const row = dbGet(db, query, params);
30
+ return row?.total || 0;
31
+ },
32
+ upsert: (db, data) => dbRun(db, `INSERT INTO watchlist (id, name, thumbnail, status, nativeName, englishName, type) VALUES (?, ?, ?, ?, ?, ?, ?)
33
+ ON CONFLICT(id) DO UPDATE SET
34
+ name = COALESCE(NULLIF(EXCLUDED.name, ''), watchlist.name),
35
+ thumbnail = COALESCE(NULLIF(EXCLUDED.thumbnail, ''), watchlist.thumbnail),
36
+ status = COALESCE(NULLIF(EXCLUDED.status, ''), watchlist.status),
37
+ nativeName = COALESCE(NULLIF(EXCLUDED.nativeName, ''), watchlist.nativeName),
38
+ englishName = COALESCE(NULLIF(EXCLUDED.englishName, ''), watchlist.englishName),
39
+ type = COALESCE(NULLIF(EXCLUDED.type, ''), watchlist.type)`, [
40
+ data.id,
41
+ data.name,
42
+ data.thumbnail,
43
+ data.status,
44
+ data.nativeName,
45
+ data.englishName,
46
+ data.type,
47
+ ]),
48
+ updateStatus: (db, id, status) => dbRun(db, 'UPDATE watchlist SET status = ? WHERE id = ?', [status, id]),
49
+ updateThumbnail: (db, id, thumbnail) => {
50
+ if (!thumbnail || thumbnail.trim() === '')
51
+ return Promise.resolve();
52
+ return dbRun(db, 'UPDATE watchlist SET thumbnail = ? WHERE id = ?', [thumbnail, id]);
53
+ },
54
+ delete: (db, id) => dbRun(db, 'DELETE FROM watchlist WHERE id = ?', [id]),
55
+ deleteMany: (db, ids) => {
56
+ if (ids.length === 0)
57
+ return Promise.resolve();
58
+ const placeholders = ids.map(() => '?').join(', ');
59
+ return dbRun(db, `DELETE FROM watchlist WHERE id IN (${placeholders})`, ids);
60
+ },
61
+ updateStatusMany: (db, ids, status) => {
62
+ if (ids.length === 0)
63
+ return Promise.resolve();
64
+ const placeholders = ids.map(() => '?').join(', ');
65
+ return dbRun(db, `UPDATE watchlist SET status = ? WHERE id IN (${placeholders})`, [
66
+ status,
67
+ ...ids,
68
+ ]);
69
+ },
70
+ getWatchingShows: (db) => dbAll(db, "SELECT id, name, thumbnail, nativeName, englishName FROM watchlist WHERE status = 'Watching'"),
71
+ };
@@ -0,0 +1,16 @@
1
+ import logger from './logger.js';
2
+ export function requestLogger(req, res, next) {
3
+ const start = process.hrtime.bigint();
4
+ const url = req.originalUrl || req.url;
5
+ res.on('finish', () => {
6
+ const ms = Math.round((Number(process.hrtime.bigint() - start) / 1_000_000) * 10) / 10;
7
+ const payload = { method: req.method, url, status: res.statusCode, ms };
8
+ if (res.statusCode >= 500) {
9
+ logger.warn(payload, 'request failed');
10
+ }
11
+ else if (!url.startsWith('/api/proxy')) {
12
+ logger.trace(`${req.method} ${url} ${res.statusCode} ${ms}ms`);
13
+ }
14
+ });
15
+ next();
16
+ }
@@ -0,0 +1,23 @@
1
+ import { Router } from 'express';
2
+ import { AsmrLibraryController } from '../controllers/asmr-library.controller.js';
3
+ export function createAsmrLibraryRouter() {
4
+ const router = Router();
5
+ const controller = new AsmrLibraryController();
6
+ router.get('/asmr/library', controller.getLibrary);
7
+ router.get('/asmr/library/ids', controller.getLibraryIds);
8
+ router.get('/asmr/library/check/:id', controller.checkLibrary);
9
+ router.post('/asmr/library/add', controller.addToLibrary);
10
+ router.post('/asmr/library/remove', controller.removeFromLibrary);
11
+ router.post('/asmr/library/status', controller.updateStatus);
12
+ router.post('/asmr/library/batch-status', controller.batchUpdateStatus);
13
+ router.post('/asmr/library/remove-many', controller.batchRemove);
14
+ router.post('/asmr/progress/remove-many', controller.batchRemoveProgress);
15
+ router.get('/asmr/progress/:workId', controller.getProgress);
16
+ router.get('/asmr/progress/:workId/latest', controller.getLatestProgress);
17
+ router.post('/asmr/progress', controller.saveProgress);
18
+ router.post('/asmr/progress/remove', controller.removeProgress);
19
+ router.get('/asmr/continue-listening', controller.getContinueListening);
20
+ router.get('/asmr/continue-listening/adult-count', controller.getAdultContinueListeningCount);
21
+ router.post('/asmr/continue-listening/purge-adult', controller.purgeAdultContinueListening);
22
+ return router;
23
+ }
@@ -0,0 +1,81 @@
1
+ import { Router } from 'express';
2
+ import logger from '../logger.js';
3
+ function makeCacheMiddleware(cache, keyFn, ttl) {
4
+ return (req, res, next) => {
5
+ const cacheKey = keyFn(req);
6
+ const cached = cache.get(cacheKey);
7
+ if (cached)
8
+ return res.json(cached);
9
+ const originalJson = res.json.bind(res);
10
+ res.json = (data) => {
11
+ if (res.statusCode >= 200 && res.statusCode < 400) {
12
+ if (ttl !== undefined)
13
+ cache.set(cacheKey, data, ttl);
14
+ else
15
+ cache.set(cacheKey, data);
16
+ }
17
+ return originalJson(data);
18
+ };
19
+ next();
20
+ };
21
+ }
22
+ export function createAsmrRouter(apiCache, getProvider) {
23
+ const router = Router();
24
+ router.get('/asmr/browse', makeCacheMiddleware(apiCache, (req) => `route-asmr-browse-${req.query.q || ''}-${req.query.page || 1}-${req.query.sort || ''}-${req.query.rating || ''}-${req.query.sort === 'random' ? Date.now() : ''}`, 300), async (req, res) => {
25
+ try {
26
+ const provider = getProvider();
27
+ if (!provider)
28
+ return res.json({ shows: [], hasNext: false });
29
+ const result = await provider.browse({
30
+ query: req.query.q,
31
+ page: parseInt(req.query.page) || 1,
32
+ sort: req.query.sort,
33
+ rating: req.query.rating,
34
+ });
35
+ res.json(result);
36
+ }
37
+ catch (err) {
38
+ if (err.message === 'AUTH_REQUIRED') {
39
+ return res.status(403).json({ error: 'AUTH_REQUIRED', provider: 'jasmr' });
40
+ }
41
+ logger.error({ err }, '[Asmr] browse failed');
42
+ res.json({ shows: [], hasNext: false });
43
+ }
44
+ });
45
+ router.get('/asmr/work/:rj', makeCacheMiddleware(apiCache, (req) => `route-asmr-work-${req.params.rj}`, 1800), async (req, res) => {
46
+ try {
47
+ const provider = getProvider();
48
+ if (!provider) {
49
+ return res.json({
50
+ rjCode: req.params.rj,
51
+ description: '',
52
+ tracks: [],
53
+ images: [],
54
+ chapters: [],
55
+ });
56
+ }
57
+ const rjCode = String(req.params.rj).trim().toUpperCase();
58
+ const episodes = await provider.getEpisodes(rjCode);
59
+ const [streams, images, chapters] = await Promise.all([
60
+ provider.getStreamUrls(rjCode, '1'),
61
+ provider.getImages(rjCode),
62
+ provider.getChapters(rjCode),
63
+ ]);
64
+ res.json({
65
+ rjCode,
66
+ description: episodes?.description || '',
67
+ tracks: streams?.[0]?.links || [],
68
+ images,
69
+ chapters,
70
+ });
71
+ }
72
+ catch (err) {
73
+ if (err.message === 'AUTH_REQUIRED') {
74
+ return res.status(403).json({ error: 'AUTH_REQUIRED', provider: 'jasmr' });
75
+ }
76
+ logger.error({ err, rj: req.params.rj }, '[Asmr] work fetch failed');
77
+ res.json({ rjCode: req.params.rj, description: '', tracks: [], images: [], chapters: [] });
78
+ }
79
+ });
80
+ return router;
81
+ }
@@ -0,0 +1,25 @@
1
+ import { Router } from 'express';
2
+ import { AuthController } from '../controllers/auth.controller.js';
3
+ export function createAuthRouter(runSyncSequence) {
4
+ const router = Router();
5
+ const controller = new AuthController(runSyncSequence);
6
+ router.get('/config-status', controller.getConfigStatus);
7
+ router.get('/google-auth', controller.getGoogleAuthSettings);
8
+ router.post('/google-auth', controller.updateGoogleAuthSettings);
9
+ router.get('/github/status', controller.getGitHubAuthStatus);
10
+ router.post('/github/start', controller.startGitHubDeviceAuth);
11
+ router.get('/github/poll', controller.pollGitHubDeviceAuth);
12
+ router.post('/github/logout', controller.logoutGitHub);
13
+ router.get('/github/auth', controller.getGitHubAuthOverride);
14
+ router.post('/github/auth', controller.updateGitHubAuthSettings);
15
+ router.get('/settings/rclone', controller.getRcloneSettings);
16
+ router.post('/settings/rclone', controller.updateRcloneSettings);
17
+ router.get('/settings/sync', controller.getSyncSettings);
18
+ router.post('/settings/sync', controller.updateSyncProvider);
19
+ router.get('/google', controller.getAuthUrl);
20
+ router.post('/google/login', controller.loginGoogle);
21
+ router.get('/google/callback', controller.handleCallback);
22
+ router.get('/user', controller.getUserProfile);
23
+ router.post('/logout', controller.logout);
24
+ return router;
25
+ }
@@ -0,0 +1,47 @@
1
+ import { Router } from 'express';
2
+ import { DataController } from '../controllers/data.controller.js';
3
+ function makeCacheMiddleware(cache, keyFn, ttl, validate = (d) => Array.isArray(d) && d.length > 0) {
4
+ return (req, res, next) => {
5
+ const cacheKey = keyFn(req);
6
+ const cached = cache.get(cacheKey);
7
+ if (cached)
8
+ return res.json(cached);
9
+ const originalJson = res.json.bind(res);
10
+ res.json = (data) => {
11
+ if (validate(data)) {
12
+ if (ttl !== undefined) {
13
+ cache.set(cacheKey, data, ttl);
14
+ }
15
+ else {
16
+ cache.set(cacheKey, data);
17
+ }
18
+ }
19
+ return originalJson(data);
20
+ };
21
+ next();
22
+ };
23
+ }
24
+ export function createDataRouter(apiCache, providers, getCatalog) {
25
+ const router = Router();
26
+ const controller = new DataController(providers, getCatalog);
27
+ router.get('/schedule/:date', makeCacheMiddleware(apiCache, (req) => `schedule-${req.params.date}-${req.query.format || 'TV'}`, 1800), controller.getSchedule);
28
+ router.get('/latest-releases', makeCacheMiddleware(apiCache, (req) => `latest-releases-${req.query.format || 'TV'}-${req.query.page || 1}-${req.query.size || 12}`, 300), controller.getLatestReleases);
29
+ router.get('/search', makeCacheMiddleware(apiCache, (req) => `search-${JSON.stringify(req.query)}`, 1800), controller.search);
30
+ router.get('/mature/search', makeCacheMiddleware(apiCache, (req) => `mature-search-${JSON.stringify(req.query)}`, 600, (d) => !!d && typeof d === 'object' && Array.isArray(d.data)), controller.matureSearch);
31
+ router.get('/mature/resolve', makeCacheMiddleware(apiCache, (req) => `mature-resolve-${req.query.title || ''}`, 3600), controller.resolveMature);
32
+ router.get('/mature/filters', controller.getMatureFilters);
33
+ router.post('/mature/allocate', controller.allocateTempShow);
34
+ router.get('/skip-times/:showId/:episodeNumber', controller.getSkipTimes);
35
+ router.get('/video', controller.getVideo);
36
+ router.get('/episodes', makeCacheMiddleware(apiCache, (req) => `episodes-${req.query.showId || ''}`, 3600), controller.getEpisodes);
37
+ router.get('/seasonal', makeCacheMiddleware(apiCache, (req) => `seasonal-${req.query.format || 'ALL'}-${req.query.page || 1}-${req.query.size || 14}`, 300), controller.getSeasonal);
38
+ router.get('/show-meta/:id', makeCacheMiddleware(apiCache, (req) => `meta-${req.params.id}`, 3600, (d) => !!d), controller.getShowMeta);
39
+ router.get('/popular-list', makeCacheMiddleware(apiCache, (req) => `popular-list-${req.query.sort || 'TRENDING_DESC'}-${req.query.page || 1}-${req.query.size || 20}`, 300), controller.getPopularList);
40
+ router.get('/trending', makeCacheMiddleware(apiCache, () => 'trending', 300), controller.getTrending);
41
+ router.get('/spotlight', makeCacheMiddleware(apiCache, () => 'spotlight', 300), controller.getSpotlight);
42
+ router.get('/home', makeCacheMiddleware(apiCache, (req) => `home-${req.query.format || 'TV'}`, 300), controller.getBatchedHome);
43
+ router.get('/genres-and-tags', controller.getGenresAndTags);
44
+ router.get('/anilist-status', controller.getAnilistStatus);
45
+ router.get('/system-notifications', controller.getSystemNotifications);
46
+ return router;
47
+ }
@@ -0,0 +1,51 @@
1
+ import { Router } from 'express';
2
+ import logger from '../logger.js';
3
+ import { discordGatewayService } from '../discord-gateway.js';
4
+ import { updateEnvFile } from '../utils/env.utils.js';
5
+ const log = logger.child({ module: 'DiscordGatewayRoutes' });
6
+ function mask(t) {
7
+ if (!t)
8
+ return 'none';
9
+ return t.slice(0, 8) + '...' + t.slice(-4);
10
+ }
11
+ export function createDiscordGatewayRouter() {
12
+ const router = Router();
13
+ router.get('/discord/gateway/status', (_req, res) => {
14
+ const hasToken = discordGatewayService.hasToken();
15
+ res.json({
16
+ hasToken,
17
+ masked: hasToken ? mask(process.env.DISCORD_GATEWAY_TOKEN) : null,
18
+ enabled: discordGatewayService.serviceEnabled,
19
+ });
20
+ });
21
+ router.post('/discord/gateway/save', async (req, res) => {
22
+ let token = (req.body.token || '').trim();
23
+ if (token.startsWith('"') && token.endsWith('"'))
24
+ token = token.slice(1, -1);
25
+ if (token.length < 30)
26
+ return res.status(400).json({ error: 'Token too short' });
27
+ try {
28
+ await updateEnvFile({ DISCORD_GATEWAY_TOKEN: token });
29
+ discordGatewayService.reloadToken();
30
+ log.info(`Discord Gateway token saved ${mask(token)} (not logged)`);
31
+ res.json({ ok: true, masked: mask(token) });
32
+ }
33
+ catch (e) {
34
+ log.error({ err: e }, 'Failed to save Discord Gateway token');
35
+ res.status(500).json({ error: 'Failed to save token' });
36
+ }
37
+ });
38
+ router.post('/discord/gateway/remove', async (_req, res) => {
39
+ try {
40
+ await updateEnvFile({ DISCORD_GATEWAY_TOKEN: '' });
41
+ discordGatewayService.reloadToken();
42
+ log.info('Discord Gateway token removed');
43
+ res.json({ ok: true });
44
+ }
45
+ catch (e) {
46
+ log.error({ err: e }, 'Failed to remove Discord Gateway token');
47
+ res.status(500).json({ error: 'Failed to remove token' });
48
+ }
49
+ });
50
+ return router;
51
+ }
@@ -0,0 +1,34 @@
1
+ import { Router } from 'express';
2
+ import { InsightsController } from '../controllers/insights.controller.js';
3
+ import { CONFIG } from '../config.js';
4
+ import { getLinkedDiscordUser, setLinkedDiscordUser, syncDiscordRoles, } from '../lib/discord-roles-sync.service.js';
5
+ export function createInsightsRouter() {
6
+ const router = Router();
7
+ const controller = new InsightsController();
8
+ router.get('/insights', controller.getWatchInsights);
9
+ router.get('/insights/genre-cards', controller.getGenreCards);
10
+ router.get('/insights/discord-sync-stats', controller.getDiscordSyncStats);
11
+ router.get('/discord-roles-config', (_req, res) => {
12
+ res.json({ workerUrl: CONFIG.DISCORD_ROLES_WORKER_URL || null });
13
+ });
14
+ router.get('/discord-user', async (req, res) => {
15
+ const user = await getLinkedDiscordUser(req.db);
16
+ res.json({ user });
17
+ });
18
+ router.post('/discord-user', async (req, res) => {
19
+ const { user } = req.body;
20
+ if (user !== null &&
21
+ user !== undefined &&
22
+ (typeof user !== 'object' || typeof user.id !== 'string' || !user.id)) {
23
+ res.status(400).json({ error: 'Invalid user payload: expected { id, ... } or null' });
24
+ return;
25
+ }
26
+ await setLinkedDiscordUser(req.db, user || null);
27
+ res.json({ success: true, user: user || null });
28
+ });
29
+ router.post('/discord-sync-now', async (req, res) => {
30
+ const result = await syncDiscordRoles(req.db, true);
31
+ res.json(result);
32
+ });
33
+ return router;
34
+ }
@@ -0,0 +1,46 @@
1
+ import { Router } from 'express';
2
+ import { hasAppPassword, verifyAppPassword, setAppPassword, createLanSession, revokeLanSession, clearAllLanSessions, isLanAuthenticated, isLoopbackRequest, buildLanCookie, clearLanCookie, getRequestToken, } from '../app-auth.js';
3
+ export function createLanAuthRouter() {
4
+ const router = Router();
5
+ router.get('/app-status', (req, res) => {
6
+ res.json({
7
+ hasPassword: hasAppPassword(),
8
+ isAuthenticated: isLanAuthenticated(req),
9
+ });
10
+ });
11
+ router.post('/app-login', (req, res) => {
12
+ const password = String(req.body?.password || '');
13
+ if (!hasAppPassword()) {
14
+ return res.json({ success: true });
15
+ }
16
+ if (!password) {
17
+ return res.status(400).json({ error: 'Password required' });
18
+ }
19
+ if (!verifyAppPassword(password)) {
20
+ return res.status(401).json({ error: 'Invalid password' });
21
+ }
22
+ const session = createLanSession();
23
+ res.setHeader('Set-Cookie', buildLanCookie(session.token, session.expiry));
24
+ res.json({ success: true });
25
+ });
26
+ router.post('/app-logout', (req, res) => {
27
+ revokeLanSession(getRequestToken(req));
28
+ res.setHeader('Set-Cookie', clearLanCookie());
29
+ res.json({ success: true });
30
+ });
31
+ router.post('/app-setup', (req, res) => {
32
+ if (!isLoopbackRequest(req)) {
33
+ return res.status(403).json({ error: 'Forbidden' });
34
+ }
35
+ const password = String(req.body?.password || '');
36
+ setAppPassword(password)
37
+ .then(() => {
38
+ clearAllLanSessions();
39
+ res.json({ success: true, hasPassword: hasAppPassword() });
40
+ })
41
+ .catch((err) => {
42
+ res.status(500).json({ error: 'Failed to update password' });
43
+ });
44
+ });
45
+ return router;
46
+ }
@@ -0,0 +1,23 @@
1
+ import { Router } from 'express';
2
+ import { MangaLibraryController } from '../controllers/manga-library.controller.js';
3
+ export function createMangaLibraryRouter() {
4
+ const router = Router();
5
+ const controller = new MangaLibraryController();
6
+ router.get('/manga/library', controller.getLibrary);
7
+ router.get('/manga/library/ids', controller.getLibraryIds);
8
+ router.get('/manga/library/check/:id', controller.checkLibrary);
9
+ router.post('/manga/library/add', controller.addToLibrary);
10
+ router.post('/manga/library/remove', controller.removeFromLibrary);
11
+ router.post('/manga/library/status', controller.updateStatus);
12
+ router.post('/manga/library/batch-status', controller.batchUpdateStatus);
13
+ router.post('/manga/library/remove-many', controller.batchRemove);
14
+ router.post('/manga/progress/remove-many', controller.batchRemoveProgress);
15
+ router.get('/manga/progress/:mangaId', controller.getProgress);
16
+ router.get('/manga/progress/:mangaId/latest', controller.getLatestProgress);
17
+ router.post('/manga/progress', controller.saveProgress);
18
+ router.post('/manga/progress/remove', controller.removeProgress);
19
+ router.get('/manga/continue-reading', controller.getContinueReading);
20
+ router.get('/manga/continue-reading/adult-count', controller.getAdultContinueReadingCount);
21
+ router.post('/manga/continue-reading/purge-adult', controller.purgeAdultContinueReading);
22
+ return router;
23
+ }
@@ -0,0 +1,141 @@
1
+ import { Router } from 'express';
2
+ import logger from '../logger.js';
3
+ import { parseJsonBody } from '../utils/http.utils.js';
4
+ function makeCacheMiddleware(cache, keyFn, ttl) {
5
+ return (req, res, next) => {
6
+ const cacheKey = keyFn(req);
7
+ const cached = cache.get(cacheKey);
8
+ if (cached)
9
+ return res.json(cached);
10
+ const originalJson = res.json.bind(res);
11
+ res.json = (data) => {
12
+ if (res.statusCode >= 200 && res.statusCode < 400) {
13
+ if (ttl !== undefined)
14
+ cache.set(cacheKey, data, ttl);
15
+ else
16
+ cache.set(cacheKey, data);
17
+ }
18
+ return originalJson(data);
19
+ };
20
+ next();
21
+ };
22
+ }
23
+ const SAFE = ['safe'];
24
+ function parseRatings(req) {
25
+ const mature = req.query.mature === '1';
26
+ const raw = String(req.query.rating || 'safe')
27
+ .split(',')
28
+ .map((s) => s.trim().toLowerCase())
29
+ .filter((s) => ['safe', 'suggestive', 'erotica', 'pornographic'].includes(s));
30
+ if (!mature)
31
+ return SAFE;
32
+ const exact = [...new Set(raw)];
33
+ return exact.length > 0 ? exact : SAFE;
34
+ }
35
+ export function createMangaRouter(apiCache, getProvider) {
36
+ const router = Router();
37
+ const pick = (name) => getProvider(name.toLowerCase());
38
+ router.get('/manga/search', makeCacheMiddleware(apiCache, (req) => `route-manga-search-${req.query.provider || ''}-${req.query.q || ''}-${req.query.page || 1}-${req.query.sort || ''}-${req.query.status || ''}-${req.query.type || ''}-${req.query.rating || ''}-${req.query.mature || ''}`, 300), async (req, res) => {
39
+ try {
40
+ const provider = req.query.provider;
41
+ const ratings = parseRatings(req);
42
+ const rawType = req.query.type || '';
43
+ const type = rawType === 'doujinshi' && req.query.mature !== '1' ? '' : rawType;
44
+ const options = {
45
+ query: req.query.q || '',
46
+ page: parseInt(req.query.page) || 1,
47
+ limit: Math.min(parseInt(req.query.limit) || 24, 50),
48
+ sort: req.query.sort || 'popular',
49
+ status: req.query.status || '',
50
+ type,
51
+ ratings,
52
+ };
53
+ const result = await pick(provider)?.search(options);
54
+ res.json(result ?? { items: [], hasNext: false });
55
+ }
56
+ catch (err) {
57
+ logger.error({ err }, '[Manga] search failed');
58
+ res.json({ items: [], hasNext: false });
59
+ }
60
+ });
61
+ router.get('/manga/info', makeCacheMiddleware(apiCache, (req) => `route-manga-info-${req.query.provider || ''}-${req.query.id || ''}-${req.query.rating || ''}-${req.query.mature || ''}`, 600), async (req, res) => {
62
+ try {
63
+ const provider = req.query.provider;
64
+ const id = String(req.query.id || '');
65
+ if (!id)
66
+ return res.status(400).json({ error: 'Missing id' });
67
+ const detail = await pick(provider)?.getDetail(id, parseRatings(req));
68
+ if (!detail)
69
+ return res.status(404).json({ error: 'Not found' });
70
+ res.json(detail);
71
+ }
72
+ catch (err) {
73
+ logger.error({ err }, '[Manga] info failed');
74
+ res.status(500).json({ error: 'Failed to load manga' });
75
+ }
76
+ });
77
+ router.get('/manga/chapters', makeCacheMiddleware(apiCache, (req) => `route-manga-chapters-${req.query.provider || ''}-${req.query.id || ''}-${req.query.rating || ''}-${req.query.mature || ''}`, 600), async (req, res) => {
78
+ try {
79
+ const provider = req.query.provider;
80
+ const id = String(req.query.id || '');
81
+ if (!id)
82
+ return res.status(400).json({ error: 'Missing id' });
83
+ const chapters = (await pick(provider)?.getChapters(id, parseRatings(req))) ?? [];
84
+ res.json({ chapters });
85
+ }
86
+ catch (err) {
87
+ logger.error({ err }, '[Manga] chapters failed');
88
+ res.json({ chapters: [] });
89
+ }
90
+ });
91
+ router.get('/manga/pages', makeCacheMiddleware(apiCache, (req) => `route-manga-pages-${req.query.provider || ''}-${req.query.id || ''}`, 300), async (req, res) => {
92
+ try {
93
+ const provider = req.query.provider;
94
+ const id = String(req.query.id || '');
95
+ if (!id)
96
+ return res.status(400).json({ error: 'Missing id' });
97
+ const pages = (await pick(provider)?.getPages(id)) ?? [];
98
+ res.json({ pages });
99
+ }
100
+ catch (err) {
101
+ logger.error({ err }, '[Manga] pages failed');
102
+ res.json({ pages: [] });
103
+ }
104
+ });
105
+ router.get('/manga/trending', makeCacheMiddleware(apiCache, () => 'route-manga-trending', 600), async (_req, res) => {
106
+ try {
107
+ const url = 'https://api.mangadex.org/manga?limit=6&order%5BfollowedCount%5D=desc&includes%5B%5D=cover_art&contentRating%5B%5D=safe&contentRating%5B%5D=suggestive';
108
+ const upstream = await fetch(url, {
109
+ headers: { 'User-Agent': 'dango/3.1.9' },
110
+ });
111
+ if (!upstream.ok)
112
+ return res.json([]);
113
+ const data = (await parseJsonBody(upstream));
114
+ const results = (data.data || []).map((m) => {
115
+ const title = m.attributes?.title?.en || Object.values(m.attributes?.title || {})[0] || '';
116
+ const desc = m.attributes?.description?.en || '';
117
+ const tags = (m.attributes?.tags || []).map((t) => t.attributes?.name?.en).filter(Boolean);
118
+ const coverArt = m.relationships?.find((r) => r.type === 'cover_art');
119
+ const fileName = coverArt?.attributes?.fileName;
120
+ const cover = fileName
121
+ ? `https://uploads.mangadex.org/covers/${m.id}/${fileName}.512.jpg`
122
+ : '';
123
+ return {
124
+ id: m.id,
125
+ title,
126
+ description: desc,
127
+ tags,
128
+ year: m.attributes?.year || null,
129
+ status: m.attributes?.status || null,
130
+ cover,
131
+ };
132
+ });
133
+ res.json(results);
134
+ }
135
+ catch (err) {
136
+ logger.error({ err }, '[Manga] trending failed');
137
+ res.json([]);
138
+ }
139
+ });
140
+ return router;
141
+ }
@@ -0,0 +1,20 @@
1
+ import { Router } from 'express';
2
+ import { MusicController } from '../controllers/music.controller.js';
3
+ export function createMusicRouter(apiCache) {
4
+ const router = Router();
5
+ const controller = new MusicController(apiCache);
6
+ router.get('/music/auth/status', controller.getAuthStatus);
7
+ router.post('/music/auth/start', controller.startAuth);
8
+ router.post('/music/auth/signout', controller.signOut);
9
+ router.get('/music/search', controller.search);
10
+ router.get('/music/stream', controller.stream);
11
+ router.get('/music/audio', controller.audio);
12
+ router.get('/music/track', controller.track);
13
+ router.get('/music/library', controller.library);
14
+ router.get('/music/playlist', controller.playlist);
15
+ router.get('/music/home', controller.home);
16
+ router.get('/music/upnext', controller.upnext);
17
+ router.get('/music/likes', controller.likedIds);
18
+ router.post('/music/like', controller.rate);
19
+ return router;
20
+ }
@@ -0,0 +1,17 @@
1
+ import { Router } from 'express';
2
+ export function createProvidersRouter(getCatalog, refresh) {
3
+ const router = Router();
4
+ router.get('/providers', (_req, res) => {
5
+ res.set('Cache-Control', 'public, max-age=300').json(getCatalog());
6
+ });
7
+ router.post('/providers/refresh', async (_req, res) => {
8
+ try {
9
+ const catalog = await refresh();
10
+ res.json(catalog);
11
+ }
12
+ catch (err) {
13
+ res.status(500).json({ error: err.message || 'refresh failed' });
14
+ }
15
+ });
16
+ return router;
17
+ }
@@ -0,0 +1,12 @@
1
+ import { Router } from 'express';
2
+ import { ProxyController } from '../controllers/proxy.controller.js';
3
+ export function createProxyRouter() {
4
+ const router = Router();
5
+ const controller = new ProxyController();
6
+ router.get('/proxy', controller.handleProxy);
7
+ router.get('/embed-proxy', controller.handleEmbedProxy);
8
+ router.get('/subtitle-proxy', controller.handleSubtitleProxy);
9
+ router.get('/image-proxy', controller.handleImageProxy);
10
+ router.get('/resolve', controller.handleResolve);
11
+ return router;
12
+ }