@serifpersia/dango 3.6.5 → 3.6.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/README.md +1 -1
  2. package/client/dist/assets/{AnimeCard-JkXG40Es.js → AnimeCard-DraA_TF9.js} +1 -1
  3. package/client/dist/assets/{AnimeInfo-2L0UJkbB.js → AnimeInfo-BJ7pzs-P.js} +1 -1
  4. package/client/dist/assets/{AnimeInfoPage-BDs03vKk.js → AnimeInfoPage-B_esUr5C.js} +2 -2
  5. package/client/dist/assets/{Asmr-D2mVKEl9.js → Asmr-Ckuh2xfW.js} +1 -1
  6. package/client/dist/assets/{AsmrResetProgressModal-MQA_bNQM.js → AsmrResetProgressModal-BS6UFJN4.js} +1 -1
  7. package/client/dist/assets/AvSyncCalibrator-DwHCd9yB.js +10 -0
  8. package/client/dist/assets/{Home-DxMBHFmc.js → Home-DuM1EuvN.js} +1 -1
  9. package/client/dist/assets/{Insights-hmLWsSXY.js → Insights-D3Pd9c9V.js} +1 -1
  10. package/client/dist/assets/{LibraryListPage-CWI7hgUh.js → LibraryListPage-Dm2yVn0-.js} +1 -1
  11. package/client/dist/assets/{ListeningList-NOc28KAV.js → ListeningList-D-zt6EXZ.js} +1 -1
  12. package/client/dist/assets/{Manga-B5qNhI7v.js → Manga-JwprMKBk.js} +1 -1
  13. package/client/dist/assets/{MangaInfoPage-D4wIyb-V.js → MangaInfoPage-DaeyJl2p.js} +1 -1
  14. package/client/dist/assets/{MangaReadPage-Ce1VX7-0.js → MangaReadPage-COg0fHI9.js} +1 -1
  15. package/client/dist/assets/{MangaResetProgressModal-P3oY4_14.js → MangaResetProgressModal-kguXv0_M.js} +1 -1
  16. package/client/dist/assets/{Mature-D8EwGYQq.js → Mature-IHDaasrs.js} +1 -1
  17. package/client/dist/assets/{MediaCard-Dp8DjyXB.js → MediaCard-CpwsrXo5.js} +1 -1
  18. package/client/dist/assets/{Music-DULtVsAM.js → Music-Dks4tPeK.js} +1 -1
  19. package/client/dist/assets/{OptionListMenu-fIHdE-nq.js → OptionListMenu-11CcAsy8.js} +1 -1
  20. package/client/dist/assets/{Player-B8XeOmso.js → Player-DQoEQkHL.js} +6 -6
  21. package/client/dist/assets/{PlayerSettings-BycNCiCs.js → PlayerSettings-D4Ud4kv4.js} +1 -1
  22. package/client/dist/assets/{QueueOptionsButton-T1J37IZT.js → QueueOptionsButton-B8PlPKjC.js} +1 -1
  23. package/client/dist/assets/{QueueRail-BAwI-apz.js → QueueRail-B4Dok7U3.js} +1 -1
  24. package/client/dist/assets/{Radio-Bv9DyKtZ.js → Radio-BHK6LSk4.js} +1 -1
  25. package/client/dist/assets/{ReadingList-Cl0CcVp3.js → ReadingList-BCsxIz57.js} +1 -1
  26. package/client/dist/assets/{ResetProgressModal--N60WWCB.js → ResetProgressModal-BXHxSfy-.js} +1 -1
  27. package/client/dist/assets/{Search-DvgVDlUR.js → Search-PK1ds07O.js} +1 -1
  28. package/client/dist/assets/{Settings-dCz_-bJn.js → Settings-Bxk_x55l.js} +1 -1
  29. package/client/dist/assets/{SkeletonGrid-BZHKk1I6.js → SkeletonGrid-CpJCZSRN.js} +1 -1
  30. package/client/dist/assets/{Trackers-C1T5VcPu.js → Trackers-RieCh_84.js} +1 -1
  31. package/client/dist/assets/{Tv-Dhv_cg4y.js → Tv-DLytPJUj.js} +4 -4
  32. package/client/dist/assets/{TvInfo-DgaLK2H6.js → TvInfo-C8djKebR.js} +1 -1
  33. package/client/dist/assets/{TvPopup-DyX7f8kC.js → TvPopup-40JGGJDk.js} +1 -1
  34. package/client/dist/assets/{TvSearch-Bp0H6kue.js → TvSearch-A3hbospf.js} +1 -1
  35. package/client/dist/assets/{TvWatchlist-ohMtMeKa.js → TvWatchlist-DuXjqmZM.js} +1 -1
  36. package/client/dist/assets/{Watchlist-BbQtzsER.js → Watchlist-B9wGBh0u.js} +1 -1
  37. package/client/dist/assets/{hls-Bi1cjpLZ.js → hls-CZ5X3Ya-.js} +1 -1
  38. package/client/dist/assets/{index-BTnhzofk.js → index-dRhc9-ol.js} +2 -2
  39. package/client/dist/assets/{useAnimeInfoData-Cvmso3IK.js → useAnimeInfoData-ZzDtLfvD.js} +1 -1
  40. package/client/dist/assets/{useAsmr-CbzGCzmv.js → useAsmr-CKOdHgol.js} +1 -1
  41. package/client/dist/assets/{useAsmrPopup-DaREZ9Ie.js → useAsmrPopup-SFHPhpcl.js} +1 -1
  42. package/client/dist/assets/{useMangaLibrary-BVQRhsFh.js → useMangaLibrary-CKATe31P.js} +1 -1
  43. package/client/dist/assets/{useMangaPopup-CzsCbndG.js → useMangaPopup-C75QuYgY.js} +1 -1
  44. package/client/dist/assets/{useProviders-UuUPzft-.js → useProviders-CMtvZbEi.js} +1 -1
  45. package/client/dist/assets/{useTv-CSsbWKH9.js → useTv-DUX4zYMr.js} +1 -1
  46. package/client/dist/assets/{useTvLibrary-a_Y0c2FS.js → useTvLibrary-By4Gt-R_.js} +1 -1
  47. package/client/dist/index.html +1 -1
  48. package/package.json +2 -2
  49. package/server/dist/app-auth.js +1 -157
  50. package/server/dist/app-hono.js +7 -557
  51. package/server/dist/config.js +1 -174
  52. package/server/dist/db/drizzle.js +1 -16
  53. package/server/dist/db/schema-anime.js +1 -61
  54. package/server/dist/db/schema-asmr.js +1 -13
  55. package/server/dist/db/schema-manga.js +1 -31
  56. package/server/dist/db/schema-tv.js +1 -18
  57. package/server/dist/db.js +1 -217
  58. package/server/dist/discord-gateway.js +1 -385
  59. package/server/dist/discord-rpc.js +1 -413
  60. package/server/dist/github-sync.js +1 -508
  61. package/server/dist/google.js +1 -741
  62. package/server/dist/hono/asmr.js +1 -10
  63. package/server/dist/hono/auth.js +3 -26
  64. package/server/dist/hono/music.js +1 -209
  65. package/server/dist/hono/proxy.js +36 -905
  66. package/server/dist/hono/tv.js +5 -169
  67. package/server/dist/hono/watchlist.js +2 -338
  68. package/server/dist/lib/anilist.js +26 -1458
  69. package/server/dist/lib/anischedule.js +1 -191
  70. package/server/dist/lib/insights.js +1 -185
  71. package/server/dist/lib/ipc.js +1 -23
  72. package/server/dist/lib/kitsu.js +1 -517
  73. package/server/dist/lib/mal-import.js +1 -291
  74. package/server/dist/lib/mal.js +1 -957
  75. package/server/dist/lib/migration.js +1 -310
  76. package/server/dist/lib/offline-db.js +4 -384
  77. package/server/dist/lib/potoken.js +1 -135
  78. package/server/dist/lib/recommendations.service.js +1 -216
  79. package/server/dist/lib/temp-ids.js +1 -3
  80. package/server/dist/lib/tmdb.js +1 -72
  81. package/server/dist/lib/tracker/anilist-tracker.js +8 -245
  82. package/server/dist/lib/tracker/manga-sync.service.js +1 -773
  83. package/server/dist/lib/tracker/sync-state.js +1 -21
  84. package/server/dist/lib/tracker/sync.service.js +1 -476
  85. package/server/dist/lib/translate.js +3 -79
  86. package/server/dist/lib/ump.js +1 -106
  87. package/server/dist/lib/ytmusic.js +1 -162
  88. package/server/dist/logger.js +1 -16
  89. package/server/dist/providers/manga/manga.types.js +0 -1
  90. package/server/dist/providers/provider.interface.js +0 -1
  91. package/server/dist/providers/remote-loader.js +2 -605
  92. package/server/dist/providers/remote-types.js +1 -124
  93. package/server/dist/providers/title-matching.js +1 -226
  94. package/server/dist/providers/tv.types.js +0 -1
  95. package/server/dist/rclone.js +2 -120
  96. package/server/dist/repositories/asmr.repository.js +11 -46
  97. package/server/dist/repositories/insights.repository.js +11 -25
  98. package/server/dist/repositories/library.repository.js +3 -94
  99. package/server/dist/repositories/mal-cache.repository.js +1 -52
  100. package/server/dist/repositories/manga.repository.js +14 -58
  101. package/server/dist/repositories/notifications.repository.js +6 -52
  102. package/server/dist/repositories/queue.repository.js +5 -58
  103. package/server/dist/repositories/settings.repository.js +2 -29
  104. package/server/dist/repositories/shows-meta.repository.js +3 -56
  105. package/server/dist/repositories/temp-show-ids.repository.js +3 -63
  106. package/server/dist/repositories/tv.repository.js +11 -54
  107. package/server/dist/repositories/watched-episodes.repository.js +6 -55
  108. package/server/dist/repositories/watchlist.repository.js +3 -54
  109. package/server/dist/scripts/backfill-episode-duration.js +1 -42
  110. package/server/dist/server.js +1 -307
  111. package/server/dist/shipped-defaults.js +1 -6
  112. package/server/dist/sync-payload.js +1 -91
  113. package/server/dist/sync.js +3 -647
  114. package/server/dist/trpc/context.js +1 -7
  115. package/server/dist/trpc/errors.js +1 -15
  116. package/server/dist/trpc/index.js +1 -18
  117. package/server/dist/trpc/router.js +1 -49
  118. package/server/dist/trpc/routers/asmr-library.js +1 -112
  119. package/server/dist/trpc/routers/asmr-progress.js +1 -121
  120. package/server/dist/trpc/routers/asmr.js +1 -87
  121. package/server/dist/trpc/routers/auth.js +1 -194
  122. package/server/dist/trpc/routers/continue-watching.js +2 -58
  123. package/server/dist/trpc/routers/data.js +1 -1026
  124. package/server/dist/trpc/routers/discord.js +1 -285
  125. package/server/dist/trpc/routers/discovery.js +1 -11
  126. package/server/dist/trpc/routers/insights.js +1 -14
  127. package/server/dist/trpc/routers/manga-library.js +1 -263
  128. package/server/dist/trpc/routers/manga-progress.js +1 -169
  129. package/server/dist/trpc/routers/manga.js +1 -201
  130. package/server/dist/trpc/routers/music.js +1 -655
  131. package/server/dist/trpc/routers/notifications.js +1 -80
  132. package/server/dist/trpc/routers/progress.js +1 -109
  133. package/server/dist/trpc/routers/queue.js +1 -93
  134. package/server/dist/trpc/routers/radio.js +1 -145
  135. package/server/dist/trpc/routers/settings.js +1 -96
  136. package/server/dist/trpc/routers/tracker.js +1 -213
  137. package/server/dist/trpc/routers/tv-library.js +1 -135
  138. package/server/dist/trpc/routers/tv-progress.js +1 -159
  139. package/server/dist/trpc/routers/tv.js +1 -551
  140. package/server/dist/trpc/routers/watchlist.js +2 -549
  141. package/server/dist/trpc/validation.js +1 -258
  142. package/server/dist/utils/cache.utils.js +1 -25
  143. package/server/dist/utils/cookie.utils.js +1 -13
  144. package/server/dist/utils/db-utils.js +5 -17
  145. package/server/dist/utils/env.utils.js +5 -77
  146. package/server/dist/utils/http.utils.js +1 -82
  147. package/server/dist/utils/request-context.js +1 -2
  148. package/server/dist/utils/security.utils.js +1 -76
  149. package/server/package.json +3 -2
  150. package/client/dist/assets/AvSyncCalibrator-DmoH5eRA.js +0 -10
  151. package/server/dist/utils/megaplay.utils.js +0 -40
@@ -1,24 +1,6 @@
1
- import { eq, inArray, sql } from 'drizzle-orm';
2
- import { getDrizzle } from '../db/drizzle.js';
3
- import { tvProgress } from '../db/schema-tv.js';
4
- import { makeLibraryRepo } from './library.repository.js';
5
- export const TV_STATUSES = ['Watching', 'Completed', 'On-Hold', 'Dropped', 'Planned'];
6
- export function buildTvId(mediaType, tmdbId) {
7
- const t = String(mediaType).toLowerCase() === 'movie' ? 'movie' : 'tv';
8
- return `${t}:${tmdbId}`;
9
- }
10
- export function normalizeTvMediaId(raw) {
11
- const s = String(raw ?? '').trim();
12
- const m = /^(movie|tv)[:-](\d+)$/i.exec(s);
13
- if (m)
14
- return `${m[1].toLowerCase()}:${m[2]}`;
15
- return s;
16
- }
17
- export const TvLibraryRepository = {
18
- ...makeLibraryRepo('tv_library'),
19
- upsert: (db, data) => getDrizzle(db).run(sql `
1
+ import{eq as n,inArray as d,sql as i}from"drizzle-orm";import{getDrizzle as s}from"../db/drizzle.js";import{tvProgress as a}from"../db/schema-tv.js";import{makeLibraryRepo as p}from"./library.repository.js";const g=["Watching","Completed","On-Hold","Dropped","Planned"];function v(r,e){return`${String(r).toLowerCase()==="movie"?"movie":"tv"}:${e}`}function D(r){const e=String(r??"").trim(),t=/^(movie|tv)[:-](\d+)$/i.exec(e);return t?`${t[1].toLowerCase()}:${t[2]}`:e}const T={...p("tv_library"),upsert:(r,e)=>s(r).run(i`
20
2
  INSERT INTO tv_library (id, tmdbId, mediaType, title, poster, backdrop, year, overview, status, adult, updatedAt)
21
- VALUES (${data.id}, ${data.tmdbId}, ${data.mediaType}, ${data.title}, ${data.poster}, ${data.backdrop || null}, ${data.year || null}, ${data.overview || null}, ${data.status}, ${data.adult ? 1 : 0}, strftime('%s', 'now'))
3
+ VALUES (${e.id}, ${e.tmdbId}, ${e.mediaType}, ${e.title}, ${e.poster}, ${e.backdrop||null}, ${e.year||null}, ${e.overview||null}, ${e.status}, ${e.adult?1:0}, strftime('%s', 'now'))
22
4
  ON CONFLICT(id) DO UPDATE SET
23
5
  tmdbId = EXCLUDED.tmdbId,
24
6
  mediaType = EXCLUDED.mediaType,
@@ -29,26 +11,13 @@ export const TvLibraryRepository = {
29
11
  overview = COALESCE(NULLIF(EXCLUDED.overview, ''), tv_library.overview),
30
12
  status = COALESCE(NULLIF(EXCLUDED.status, ''), tv_library.status),
31
13
  adult = COALESCE(EXCLUDED.adult, tv_library.adult),
32
- updatedAt = strftime('%s', 'now')`),
33
- touchProgress: (db, id, progress) => getDrizzle(db).run(sql `
34
- UPDATE tv_library SET lastSeason = ${progress.season}, lastEpisode = ${progress.episode}, updatedAt = strftime('%s', 'now') WHERE id = ${id}`),
35
- };
36
- export const TvProgressRepository = {
37
- getByMedia: (db, mediaId) => getDrizzle(db).all(sql `
38
- SELECT mediaId, season, episode, currentTime, duration, completed, updatedAt FROM tv_progress WHERE mediaId = ${mediaId} ORDER BY updatedAt DESC, rowid DESC`),
39
- getEpisode: async (db, mediaId, season, episode) => {
40
- const rows = await getDrizzle(db).all(sql `
41
- SELECT mediaId, season, episode, currentTime, duration, completed, updatedAt FROM tv_progress WHERE mediaId = ${mediaId} AND season = ${season} AND episode = ${episode}`);
42
- return rows[0];
43
- },
44
- getLatest: async (db, mediaId) => {
45
- const rows = await getDrizzle(db).all(sql `
46
- SELECT mediaId, season, episode, currentTime, duration, completed, updatedAt FROM tv_progress WHERE mediaId = ${mediaId} ORDER BY updatedAt DESC, rowid DESC LIMIT 1`);
47
- return rows[0];
48
- },
49
- upsert: (db, data) => getDrizzle(db).run(sql `
14
+ updatedAt = strftime('%s', 'now')`),touchProgress:(r,e,t)=>s(r).run(i`
15
+ UPDATE tv_library SET lastSeason = ${t.season}, lastEpisode = ${t.episode}, updatedAt = strftime('%s', 'now') WHERE id = ${e}`)},I={getByMedia:(r,e)=>s(r).all(i`
16
+ SELECT mediaId, season, episode, currentTime, duration, completed, updatedAt FROM tv_progress WHERE mediaId = ${e} ORDER BY updatedAt DESC, rowid DESC`),getEpisode:async(r,e,t,o)=>(await s(r).all(i`
17
+ SELECT mediaId, season, episode, currentTime, duration, completed, updatedAt FROM tv_progress WHERE mediaId = ${e} AND season = ${t} AND episode = ${o}`))[0],getLatest:async(r,e)=>(await s(r).all(i`
18
+ SELECT mediaId, season, episode, currentTime, duration, completed, updatedAt FROM tv_progress WHERE mediaId = ${e} ORDER BY updatedAt DESC, rowid DESC LIMIT 1`))[0],upsert:(r,e)=>s(r).run(i`
50
19
  INSERT INTO tv_progress (mediaId, season, episode, currentTime, duration, completed, title, poster, backdrop, year, overview, tmdbId, mediaType, adult, updatedAt)
51
- VALUES (${data.mediaId}, ${data.season}, ${data.episode}, ${data.currentTime}, ${data.duration}, ${data.completed}, ${data.title ?? null}, ${data.poster ?? null}, ${data.backdrop ?? null}, ${data.year ?? null}, ${data.overview ?? null}, ${data.tmdbId ?? null}, ${data.mediaType ?? null}, ${data.adult ?? null}, strftime('%s', 'now'))
20
+ VALUES (${e.mediaId}, ${e.season}, ${e.episode}, ${e.currentTime}, ${e.duration}, ${e.completed}, ${e.title??null}, ${e.poster??null}, ${e.backdrop??null}, ${e.year??null}, ${e.overview??null}, ${e.tmdbId??null}, ${e.mediaType??null}, ${e.adult??null}, strftime('%s', 'now'))
52
21
  ON CONFLICT(mediaId, season, episode) DO UPDATE SET
53
22
  currentTime = EXCLUDED.currentTime,
54
23
  duration = EXCLUDED.duration,
@@ -61,18 +30,8 @@ export const TvProgressRepository = {
61
30
  tmdbId = COALESCE(EXCLUDED.tmdbId, tv_progress.tmdbId),
62
31
  mediaType = COALESCE(EXCLUDED.mediaType, tv_progress.mediaType),
63
32
  adult = COALESCE(EXCLUDED.adult, tv_progress.adult),
64
- updatedAt = strftime('%s', 'now')`),
65
- deleteByMedia: (db, mediaId) => getDrizzle(db).delete(tvProgress).where(eq(tvProgress.mediaId, mediaId)),
66
- deleteMany: (db, ids) => {
67
- if (ids.length === 0)
68
- return Promise.resolve();
69
- return getDrizzle(db).delete(tvProgress).where(inArray(tvProgress.mediaId, ids));
70
- },
71
- deleteEpisode: (db, mediaId, season, episode) => getDrizzle(db).run(sql `
72
- DELETE FROM tv_progress WHERE mediaId = ${mediaId} AND season = ${season} AND episode = ${episode}`),
73
- getContinueWatching: (db, limit) => {
74
- const limitClause = typeof limit === 'number' ? `LIMIT ${limit}` : '';
75
- return getDrizzle(db).all(sql `
33
+ updatedAt = strftime('%s', 'now')`),deleteByMedia:(r,e)=>s(r).delete(a).where(n(a.mediaId,e)),deleteMany:(r,e)=>e.length===0?Promise.resolve():s(r).delete(a).where(d(a.mediaId,e)),deleteEpisode:(r,e,t,o)=>s(r).run(i`
34
+ DELETE FROM tv_progress WHERE mediaId = ${e} AND season = ${t} AND episode = ${o}`),getContinueWatching:(r,e)=>{const t=typeof e=="number"?`LIMIT ${e}`:"";return s(r).all(i`
76
35
  SELECT p.mediaId as id,
77
36
  COALESCE(l.tmdbId, p.tmdbId, CAST(SUBSTR(REPLACE(p.mediaId, '-', ':'), INSTR(REPLACE(p.mediaId, '-', ':'), ':') + 1) AS INTEGER)) as tmdbId,
78
37
  COALESCE(l.mediaType, p.mediaType, SUBSTR(REPLACE(p.mediaId, '-', ':'), 1, INSTR(REPLACE(p.mediaId, '-', ':'), ':') - 1)) as mediaType,
@@ -88,6 +47,4 @@ export const TvProgressRepository = {
88
47
  WHERE p.rn = 1
89
48
  AND (l.status IS NULL OR l.status = 'Watching')
90
49
  ORDER BY p.updatedAt DESC, p.progressRowid DESC
91
- ${sql.raw(limitClause)}`);
92
- },
93
- };
50
+ ${i.raw(t)}`)}};export{g as TV_STATUSES,T as TvLibraryRepository,I as TvProgressRepository,v as buildTvId,D as normalizeTvMediaId};
@@ -1,58 +1,11 @@
1
- import { eq, sql } from 'drizzle-orm';
2
- import { getDrizzle } from '../db/drizzle.js';
3
- import { watchedEpisodes } from '../db/schema-anime.js';
4
- export const WatchedEpisodesRepository = {
5
- getByShowAndEpisode: async (db, showId, episodeNumber) => {
6
- const rows = await getDrizzle(db).all(sql `
7
- SELECT currentTime, duration FROM watched_episodes WHERE showId = ${showId} AND episodeNumber = ${episodeNumber}`);
8
- return rows[0];
9
- },
10
- getWatchedEpisodeNumbers: async (db, showId) => {
11
- const rows = await getDrizzle(db).all(sql `SELECT episodeNumber FROM watched_episodes WHERE showId = ${showId}`);
12
- return rows.map((r) => r.episodeNumber);
13
- },
14
- getByShow: (db, showId) => getDrizzle(db).all(sql `
15
- SELECT showId, episodeNumber, currentTime, duration, watchedAt FROM watched_episodes WHERE showId = ${showId} ORDER BY CAST(episodeNumber AS REAL) ASC`),
16
- getLatestResumeProgress: async (db, showId) => {
17
- const rows = await getDrizzle(db).all(sql `
1
+ import{eq as o,sql as s}from"drizzle-orm";import{getDrizzle as r}from"../db/drizzle.js";import{watchedEpisodes as i}from"../db/schema-anime.js";const p={getByShowAndEpisode:async(t,e,a)=>(await r(t).all(s`
2
+ SELECT currentTime, duration FROM watched_episodes WHERE showId = ${e} AND episodeNumber = ${a}`))[0],getWatchedEpisodeNumbers:async(t,e)=>(await r(t).all(s`SELECT episodeNumber FROM watched_episodes WHERE showId = ${e}`)).map(n=>n.episodeNumber),getByShow:(t,e)=>r(t).all(s`
3
+ SELECT showId, episodeNumber, currentTime, duration, watchedAt FROM watched_episodes WHERE showId = ${e} ORDER BY CAST(episodeNumber AS REAL) ASC`),getLatestResumeProgress:async(t,e)=>(await r(t).all(s`
18
4
  SELECT showId, episodeNumber, currentTime, duration, watchedAt
19
5
  FROM watched_episodes
20
- WHERE showId = ${showId} AND currentTime > 5 AND (duration <= 0 OR currentTime < duration * 0.8)
6
+ WHERE showId = ${e} AND currentTime > 5 AND (duration <= 0 OR currentTime < duration * 0.8)
21
7
  ORDER BY watchedAt DESC
22
- LIMIT 1`);
23
- return rows[0];
24
- },
25
- upsert: (db, data) => getDrizzle(db)
26
- .insert(watchedEpisodes)
27
- .values({
28
- showId: data.showId,
29
- episodeNumber: data.episodeNumber,
30
- watchedAt: sql `CURRENT_TIMESTAMP`,
31
- currentTime: data.currentTime,
32
- duration: data.duration,
33
- })
34
- .onConflictDoUpdate({
35
- target: [watchedEpisodes.showId, watchedEpisodes.episodeNumber],
36
- set: {
37
- watchedAt: sql `CURRENT_TIMESTAMP`,
38
- currentTime: data.currentTime,
39
- duration: data.duration,
40
- },
41
- }),
42
- insertIfMissing: (db, data) => getDrizzle(db)
43
- .insert(watchedEpisodes)
44
- .values({
45
- showId: data.showId,
46
- episodeNumber: data.episodeNumber,
47
- watchedAt: data.watchedAt ? sql `${data.watchedAt}` : sql `CURRENT_TIMESTAMP`,
48
- currentTime: 0,
49
- duration: 0,
50
- })
51
- .onConflictDoNothing(),
52
- deleteByShow: (db, showId) => getDrizzle(db).delete(watchedEpisodes).where(eq(watchedEpisodes.showId, showId)),
53
- getContinueWatching: (db, limit) => {
54
- const limitClause = typeof limit === 'number' ? `LIMIT ${limit}` : '';
55
- return getDrizzle(db).all(sql `
8
+ LIMIT 1`))[0],upsert:(t,e)=>r(t).insert(i).values({showId:e.showId,episodeNumber:e.episodeNumber,watchedAt:s`CURRENT_TIMESTAMP`,currentTime:e.currentTime,duration:e.duration}).onConflictDoUpdate({target:[i.showId,i.episodeNumber],set:{watchedAt:s`CURRENT_TIMESTAMP`,currentTime:e.currentTime,duration:e.duration}}),insertIfMissing:(t,e)=>r(t).insert(i).values({showId:e.showId,episodeNumber:e.episodeNumber,watchedAt:e.watchedAt?s`${e.watchedAt}`:s`CURRENT_TIMESTAMP`,currentTime:0,duration:0}).onConflictDoNothing(),deleteByShow:(t,e)=>r(t).delete(i).where(o(i.showId,e)),getContinueWatching:(t,e)=>{const a=typeof e=="number"?`LIMIT ${e}`:"";return r(t).all(s`
56
9
  SELECT
57
10
  we.showId as _id,
58
11
  we.showId as id,
@@ -77,6 +30,4 @@ export const WatchedEpisodesRepository = {
77
30
  AND (w.status IS NULL OR w.status = 'Watching')
78
31
  AND (w.id IS NOT NULL OR sm.id IS NOT NULL)
79
32
  ORDER BY we.watchedAt DESC
80
- ${sql.raw(limitClause)}`);
81
- },
82
- };
33
+ ${s.raw(a)}`)}};export{p as WatchedEpisodesRepository};
@@ -1,60 +1,9 @@
1
- import { eq, inArray, sql } from 'drizzle-orm';
2
- import { getDrizzle } from '../db/drizzle.js';
3
- import { watchlist } from '../db/schema-anime.js';
4
- export const WatchlistRepository = {
5
- getById: async (db, id) => {
6
- const rows = await getDrizzle(db).all(sql `SELECT * FROM watchlist WHERE id = ${id}`);
7
- return rows[0];
8
- },
9
- exists: async (db, id) => {
10
- const rows = await getDrizzle(db).all(sql `SELECT EXISTS(SELECT 1 FROM watchlist WHERE id = ${id}) as inWatchlist`);
11
- return !!(rows[0] && rows[0].inWatchlist);
12
- },
13
- getAll: (db, status, limit, offset) => {
14
- const q = sql `SELECT * FROM watchlist`;
15
- if (status && status !== 'All') {
16
- q.append(sql ` WHERE status = ${status}`);
17
- }
18
- q.append(sql ` ORDER BY rowid DESC`);
19
- if (limit !== undefined && offset !== undefined) {
20
- q.append(sql ` LIMIT ${limit} OFFSET ${offset}`);
21
- }
22
- return getDrizzle(db).all(q);
23
- },
24
- getCount: async (db, status) => {
25
- const q = sql `SELECT COUNT(*) as total FROM watchlist`;
26
- if (status && status !== 'All') {
27
- q.append(sql ` WHERE status = ${status}`);
28
- }
29
- const rows = await getDrizzle(db).all(q);
30
- return rows[0]?.total || 0;
31
- },
32
- upsert: (db, data) => getDrizzle(db).run(sql `
33
- INSERT INTO watchlist (id, name, thumbnail, status, nativeName, englishName, type) VALUES (${data.id}, ${data.name}, ${data.thumbnail}, ${data.status}, ${data.nativeName}, ${data.englishName}, ${data.type})
1
+ import{eq as E,inArray as m,sql as n}from"drizzle-orm";import{getDrizzle as i}from"../db/drizzle.js";import{watchlist as s}from"../db/schema-anime.js";const L={getById:async(e,t)=>(await i(e).all(n`SELECT * FROM watchlist WHERE id = ${t}`))[0],exists:async(e,t)=>{const a=await i(e).all(n`SELECT EXISTS(SELECT 1 FROM watchlist WHERE id = ${t}) as inWatchlist`);return!!(a[0]&&a[0].inWatchlist)},getAll:(e,t,a,l)=>{const r=n`SELECT * FROM watchlist`;return t&&t!=="All"&&r.append(n` WHERE status = ${t}`),r.append(n` ORDER BY rowid DESC`),a!==void 0&&l!==void 0&&r.append(n` LIMIT ${a} OFFSET ${l}`),i(e).all(r)},getCount:async(e,t)=>{const a=n`SELECT COUNT(*) as total FROM watchlist`;return t&&t!=="All"&&a.append(n` WHERE status = ${t}`),(await i(e).all(a))[0]?.total||0},upsert:(e,t)=>i(e).run(n`
2
+ INSERT INTO watchlist (id, name, thumbnail, status, nativeName, englishName, type) VALUES (${t.id}, ${t.name}, ${t.thumbnail}, ${t.status}, ${t.nativeName}, ${t.englishName}, ${t.type})
34
3
  ON CONFLICT(id) DO UPDATE SET
35
4
  name = COALESCE(NULLIF(EXCLUDED.name, ''), watchlist.name),
36
5
  thumbnail = COALESCE(NULLIF(EXCLUDED.thumbnail, ''), watchlist.thumbnail),
37
6
  status = COALESCE(NULLIF(EXCLUDED.status, ''), watchlist.status),
38
7
  nativeName = COALESCE(NULLIF(EXCLUDED.nativeName, ''), watchlist.nativeName),
39
8
  englishName = COALESCE(NULLIF(EXCLUDED.englishName, ''), watchlist.englishName),
40
- type = COALESCE(NULLIF(EXCLUDED.type, ''), watchlist.type)`),
41
- updateStatus: (db, id, status) => getDrizzle(db).update(watchlist).set({ status }).where(eq(watchlist.id, id)),
42
- updateThumbnail: (db, id, thumbnail) => {
43
- if (!thumbnail || thumbnail.trim() === '')
44
- return Promise.resolve();
45
- return getDrizzle(db).update(watchlist).set({ thumbnail }).where(eq(watchlist.id, id));
46
- },
47
- delete: (db, id) => getDrizzle(db).delete(watchlist).where(eq(watchlist.id, id)),
48
- deleteMany: (db, ids) => {
49
- if (ids.length === 0)
50
- return Promise.resolve();
51
- return getDrizzle(db).delete(watchlist).where(inArray(watchlist.id, ids));
52
- },
53
- updateStatusMany: (db, ids, status) => {
54
- if (ids.length === 0)
55
- return Promise.resolve();
56
- return getDrizzle(db).update(watchlist).set({ status }).where(inArray(watchlist.id, ids));
57
- },
58
- getWatchingShows: (db) => getDrizzle(db).all(sql `SELECT id, name, thumbnail, nativeName, englishName FROM watchlist WHERE status = 'Watching'`),
59
- getMissingThumbnails: (db) => getDrizzle(db).all(sql `SELECT id, name FROM watchlist WHERE thumbnail IS NULL OR TRIM(thumbnail) = ''`),
60
- };
9
+ type = COALESCE(NULLIF(EXCLUDED.type, ''), watchlist.type)`),updateStatus:(e,t,a)=>i(e).update(s).set({status:a}).where(E(s.id,t)),updateThumbnail:(e,t,a)=>!a||a.trim()===""?Promise.resolve():i(e).update(s).set({thumbnail:a}).where(E(s.id,t)),delete:(e,t)=>i(e).delete(s).where(E(s.id,t)),deleteMany:(e,t)=>t.length===0?Promise.resolve():i(e).delete(s).where(m(s.id,t)),updateStatusMany:(e,t,a)=>t.length===0?Promise.resolve():i(e).update(s).set({status:a}).where(m(s.id,t)),getWatchingShows:e=>i(e).all(n`SELECT id, name, thumbnail, nativeName, englishName FROM watchlist WHERE status = 'Watching'`),getMissingThumbnails:e=>i(e).all(n`SELECT id, name FROM watchlist WHERE thumbnail IS NULL OR TRIM(thumbnail) = ''`)};export{L as WatchlistRepository};
@@ -1,42 +1 @@
1
- import path from 'node:path';
2
- import { CONFIG } from '../config.js';
3
- import { DatabaseWrapper } from '../db.js';
4
- import { anilistRequest } from '../lib/anilist.js';
5
- import { dbAll, dbRun } from '../utils/db-utils.js';
6
- const BATCH_SIZE = 50;
7
- async function main() {
8
- const dbName = CONFIG.IS_DEV ? CONFIG.DB_NAME_DEV : CONFIG.DB_NAME_PROD;
9
- const db = await DatabaseWrapper.create(path.join(CONFIG.ROOT, dbName));
10
- db.configure('busyTimeout', 10000);
11
- const cols = dbAll(db, 'PRAGMA table_info(shows_meta)');
12
- if (!cols.some((c) => c.name === 'episodeDuration')) {
13
- db.run('ALTER TABLE shows_meta ADD COLUMN episodeDuration INTEGER');
14
- }
15
- const missing = dbAll(db, 'SELECT id, anilistId FROM shows_meta WHERE anilistId IS NOT NULL AND episodeDuration IS NULL');
16
- if (missing.length === 0) {
17
- console.log('Nothing to backfill.');
18
- db.close();
19
- return;
20
- }
21
- let updated = 0;
22
- for (let i = 0; i < missing.length; i += BATCH_SIZE) {
23
- const batch = missing.slice(i, i + BATCH_SIZE);
24
- const res = await anilistRequest(`query ($ids: [Int]) { Page(perPage: ${BATCH_SIZE}) { media(id_in: $ids, type: ANIME) { id duration } } }`, { ids: batch.map((b) => b.anilistId) });
25
- for (const m of res?.data?.Page?.media ?? []) {
26
- if (!m.duration || m.duration <= 0)
27
- continue;
28
- const row = batch.find((b) => b.anilistId === m.id);
29
- if (!row)
30
- continue;
31
- dbRun(db, 'UPDATE shows_meta SET episodeDuration = ? WHERE id = ?', [m.duration, row.id]);
32
- updated++;
33
- }
34
- console.log(`Progress: ${Math.min(i + BATCH_SIZE, missing.length)}/${missing.length} (updated ${updated})`);
35
- }
36
- console.log(`Done. Updated ${updated} of ${missing.length} shows.`);
37
- db.close();
38
- }
39
- main().catch((err) => {
40
- console.error(err?.message || err);
41
- process.exit(1);
42
- });
1
+ import g from"node:path";import{CONFIG as s}from"../config.js";import{DatabaseWrapper as p}from"../db.js";import{anilistRequest as f}from"../lib/anilist.js";import{dbAll as l,dbRun as E}from"../utils/db-utils.js";const a=50;async function h(){const n=s.IS_DEV?s.DB_NAME_DEV:s.DB_NAME_PROD,o=await p.create(g.join(s.ROOT,n));o.configure("busyTimeout",1e4),l(o,"PRAGMA table_info(shows_meta)").some(i=>i.name==="episodeDuration")||o.run("ALTER TABLE shows_meta ADD COLUMN episodeDuration INTEGER");const e=l(o,"SELECT id, anilistId FROM shows_meta WHERE anilistId IS NOT NULL AND episodeDuration IS NULL");if(e.length===0){console.log("Nothing to backfill."),o.close();return}let r=0;for(let i=0;i<e.length;i+=a){const d=e.slice(i,i+a),c=await f(`query ($ids: [Int]) { Page(perPage: ${a}) { media(id_in: $ids, type: ANIME) { id duration } } }`,{ids:d.map(t=>t.anilistId)});for(const t of c?.data?.Page?.media??[]){if(!t.duration||t.duration<=0)continue;const m=d.find(u=>u.anilistId===t.id);m&&(E(o,"UPDATE shows_meta SET episodeDuration = ? WHERE id = ?",[t.duration,m.id]),r++)}console.log(`Progress: ${Math.min(i+a,e.length)}/${e.length} (updated ${r})`)}console.log(`Done. Updated ${r} of ${e.length} shows.`),o.close()}h().catch(n=>{console.error(n?.message||n),process.exit(1)});
@@ -1,307 +1 @@
1
- process.setMaxListeners(100);
2
- import { EventEmitter } from 'events';
3
- EventEmitter.defaultMaxListeners = 100;
4
- import path from 'path';
5
- import { AppCache } from './utils/cache.utils.js';
6
- import fs from 'fs';
7
- import crypto from 'crypto';
8
- import { serve } from '@hono/node-server';
9
- import { serveStatic } from '@hono/node-server/serve-static';
10
- import { getConnInfo } from '@hono/node-server/conninfo';
11
- import logger from './logger.js';
12
- import { notifyServerExit } from './lib/ipc.js';
13
- import { CONFIG } from './config.js';
14
- import { initializeDatabase, initializeMangaDatabase, initializeTvDatabase, initializeAsmrDatabase, runFullSyncSequence, syncUp, waitForSync, KIND_SYNC, } from './sync.js';
15
- import { isJasmrApi } from './hono/asmr.js';
16
- import { loadRemoteProviders } from './providers/remote-loader.js';
17
- import { discordRPCService } from './discord-rpc.js';
18
- import { discordGatewayService } from './discord-gateway.js';
19
- import { SettingsRepository } from './repositories/settings.repository.js';
20
- import { createHonoApp } from './app-hono.js';
21
- import { startWatchlistDiscovery, stopWatchlistDiscovery } from './hono/watchlist.js';
22
- import { checkAnilistStatus } from './lib/anilist.js';
23
- import { offlineDb } from './lib/offline-db.js';
24
- const app = createHonoApp(() => ({ shuttingDown: isShuttingDown, dbReady: !!db }), () => ({
25
- shuttingDown: isShuttingDown,
26
- dbsReady: !!db && !!mangaDb && !!tvDb && !!asmrDb,
27
- bootSyncing,
28
- }), () => ({ db, mangaDb, tvDb, asmrDb }), {
29
- initializeDatabase,
30
- setDb: (newDb) => {
31
- db = newDb;
32
- },
33
- }, {
34
- getCatalog: getProviderCatalog,
35
- refresh: refreshRemoteProviders,
36
- }, runSyncSequence, {
37
- getApiCache: () => apiCache,
38
- getProviders: () => providers,
39
- getMangaProvider: (name) => mangaProviders[name],
40
- getTvProvider: (name) => tvProviders[name],
41
- getJasmr: () => {
42
- const mod = providers['jasmr'];
43
- return isJasmrApi(mod) ? mod : undefined;
44
- },
45
- });
46
- const apiCache = new AppCache({ ttlSeconds: 3600, maxKeys: 5000 });
47
- const providers = {};
48
- const mangaProviders = {};
49
- const tvProviders = {};
50
- let remoteCatalog = [];
51
- function parseEnabledProviders() {
52
- return CONFIG.PROVIDER_ENABLED.split(',')
53
- .map((s) => s.trim().toLowerCase())
54
- .filter(Boolean);
55
- }
56
- function getProviderCatalog() {
57
- return remoteCatalog;
58
- }
59
- async function refreshRemoteProviders() {
60
- const url = CONFIG.PROVIDER_REPO_URL.trim();
61
- if (!url)
62
- return getProviderCatalog();
63
- try {
64
- const result = await loadRemoteProviders({
65
- cache: apiCache,
66
- registryUrl: url,
67
- enabledProviders: parseEnabledProviders(),
68
- });
69
- if (result.catalog.length === 0) {
70
- logger.warn('remote providers refresh returned nothing, keeping current set');
71
- return getProviderCatalog();
72
- }
73
- for (const key of Object.keys(providers)) {
74
- if (!(key in result.providers))
75
- delete providers[key];
76
- }
77
- Object.assign(providers, result.providers);
78
- for (const key of Object.keys(mangaProviders)) {
79
- if (!(key in result.mangaProviders))
80
- delete mangaProviders[key];
81
- }
82
- Object.assign(mangaProviders, result.mangaProviders);
83
- for (const key of Object.keys(tvProviders)) {
84
- if (!(key in result.tvProviders))
85
- delete tvProviders[key];
86
- }
87
- Object.assign(tvProviders, result.tvProviders);
88
- remoteCatalog = result.catalog;
89
- logger.info({
90
- count: Object.keys(result.providers).length,
91
- mangaCount: Object.keys(result.mangaProviders).length,
92
- tvCount: Object.keys(result.tvProviders).length,
93
- }, 'remote providers refreshed');
94
- }
95
- catch (err) {
96
- logger.error({ err }, 'remote providers refresh failed');
97
- }
98
- return getProviderCatalog();
99
- }
100
- let db;
101
- let mangaDb;
102
- let tvDb;
103
- let asmrDb;
104
- let isShuttingDown = false;
105
- let bootSyncing = true;
106
- async function runSyncSequence(database, mangaDatabase, preferredProvider) {
107
- const remoteFolder = CONFIG.IS_DEV ? CONFIG.REMOTE_FOLDER_DEV : CONFIG.REMOTE_FOLDER_PROD;
108
- await runFullSyncSequence({ db: database, mangaDb: mangaDatabase, tvDb, asmrDb }, { remoteFolder, preferredProvider });
109
- }
110
- startWatchlistDiscovery(() => db);
111
- if (!CONFIG.IS_DEV) {
112
- const frontendPath = path.join(CONFIG.PACKAGE_ROOT, 'client', 'dist');
113
- logger.info(`Serving frontend from: ${frontendPath}`);
114
- app.use('/*', serveStatic({ root: frontendPath }));
115
- app.get('*', async (c) => {
116
- if (c.req.path.startsWith('/api/'))
117
- return c.notFound();
118
- try {
119
- const html = await fs.promises.readFile(path.join(frontendPath, 'index.html'), 'utf8');
120
- return c.html(html);
121
- }
122
- catch (err) {
123
- logger.error({ err }, `Failed to serve index.html from ${frontendPath}`);
124
- return c.text('Server Error: Frontend build not found.', 500);
125
- }
126
- });
127
- }
128
- async function main() {
129
- const dbName = CONFIG.IS_DEV ? CONFIG.DB_NAME_DEV : CONFIG.DB_NAME_PROD;
130
- const dbPath = path.join(CONFIG.ROOT, dbName);
131
- const remoteFolder = CONFIG.IS_DEV ? CONFIG.REMOTE_FOLDER_DEV : CONFIG.REMOTE_FOLDER_PROD;
132
- db = await initializeDatabase(dbPath);
133
- logger.info(`Database initialized at ${dbPath}`);
134
- const mangaDbName = CONFIG.IS_DEV ? CONFIG.MANGA_DB_NAME_DEV : CONFIG.MANGA_DB_NAME_PROD;
135
- const mangaDbPath = path.join(CONFIG.ROOT, mangaDbName);
136
- mangaDb = await initializeMangaDatabase(mangaDbPath);
137
- logger.info(`Manga database initialized at ${mangaDbPath}`);
138
- const tvDbName = CONFIG.IS_DEV ? CONFIG.TV_DB_NAME_DEV : CONFIG.TV_DB_NAME_PROD;
139
- const tvDbPath = path.join(CONFIG.ROOT, tvDbName);
140
- tvDb = await initializeTvDatabase(tvDbPath);
141
- logger.info(`TV database initialized at ${tvDbPath}`);
142
- const asmrDbName = CONFIG.IS_DEV ? CONFIG.ASMR_DB_NAME_DEV : CONFIG.ASMR_DB_NAME_PROD;
143
- const asmrDbPath = path.join(CONFIG.ROOT, asmrDbName);
144
- asmrDb = await initializeAsmrDatabase(asmrDbPath);
145
- logger.info(`ASMR database initialized at ${asmrDbPath}`);
146
- await offlineDb.init(db);
147
- if (await offlineDb.checkWeeklyUpdateDue(db)) {
148
- logger.info('Weekly offline database update is due on startup, starting background update...');
149
- offlineDb.executeScheduledUpdate(db).catch((err) => {
150
- logger.warn({ err: err?.message }, 'Startup scheduled offline database update failed');
151
- });
152
- }
153
- const rpcEnabledSetting = await SettingsRepository.getByKey(db, 'discordRPCEnabled');
154
- const isRpcEnabled = rpcEnabledSetting ? rpcEnabledSetting.value === 'true' : true;
155
- await discordRPCService.setEnabled(isRpcEnabled);
156
- discordGatewayService.setEnabled(isRpcEnabled);
157
- checkAnilistStatus().catch(() => { });
158
- if (CONFIG.PROVIDER_REPO_URL.trim()) {
159
- setInterval(() => {
160
- refreshRemoteProviders().catch((err) => logger.error({ err }, 'periodic providers refresh failed'));
161
- }, CONFIG.PROVIDER_REPO_REFRESH_MS).unref();
162
- }
163
- const kindDbs = () => ({
164
- anime: db,
165
- manga: mangaDb,
166
- tv: tvDb,
167
- asmr: asmrDb,
168
- });
169
- for (const kind of Object.keys(KIND_SYNC)) {
170
- if (!fs.existsSync(KIND_SYNC[kind].manifestPath)) {
171
- fs.writeFileSync(KIND_SYNC[kind].manifestPath, JSON.stringify({ version: 0 }));
172
- }
173
- }
174
- const unsynced = {
175
- anime: false,
176
- manga: false,
177
- tv: false,
178
- asmr: false,
179
- };
180
- const watchers = Object.keys(KIND_SYNC).map((kind) => fs.watch(KIND_SYNC[kind].manifestPath, (eventType) => {
181
- if (eventType === 'change' || eventType === 'rename') {
182
- unsynced[kind] = true;
183
- }
184
- }));
185
- const server = serve({ fetch: app.fetch, port: CONFIG.PORT }, () => {
186
- logger.info(`Server running on http://localhost:${CONFIG.PORT}`);
187
- });
188
- runSyncSequence(db, mangaDb)
189
- .catch((err) => logger.error({ err }, 'background boot sync failed'))
190
- .finally(() => {
191
- bootSyncing = false;
192
- });
193
- refreshRemoteProviders().catch((err) => logger.error({ err }, 'background providers refresh failed'));
194
- const kindDbLabel = {
195
- anime: 'database',
196
- manga: 'manga database',
197
- tv: 'TV database',
198
- asmr: 'ASMR database',
199
- };
200
- const syncInterval = setInterval(async () => {
201
- for (const kind of Object.keys(KIND_SYNC)) {
202
- if (!unsynced[kind])
203
- continue;
204
- logger.info(`Uploading accumulated ${kindDbLabel[kind]} changes...`);
205
- unsynced[kind] = false;
206
- try {
207
- await syncUp(kindDbs()[kind], kind, remoteFolder);
208
- }
209
- catch (err) {
210
- logger.error({ err }, `Failed to upload ${kindDbLabel[kind]} changes`);
211
- unsynced[kind] = true;
212
- }
213
- }
214
- }, 300000);
215
- const offlineDbInterval = setInterval(async () => {
216
- if (await offlineDb.checkWeeklyUpdateDue(db)) {
217
- logger.info('Weekly offline database update triggered by periodic schedule...');
218
- offlineDb.executeScheduledUpdate(db).catch((err) => {
219
- logger.warn({ err: err?.message }, 'Interval scheduled offline database update failed');
220
- });
221
- }
222
- }, 6 * 60 * 60 * 1000);
223
- offlineDbInterval.unref();
224
- const shutdown = async (signal) => {
225
- if (isShuttingDown)
226
- return;
227
- isShuttingDown = true;
228
- stopWatchlistDiscovery();
229
- clearInterval(syncInterval);
230
- clearInterval(offlineDbInterval);
231
- discordRPCService.disconnect();
232
- discordGatewayService.shutdown();
233
- for (const watcher of watchers) {
234
- await watcher.close();
235
- }
236
- if (server) {
237
- await new Promise((resolve) => server.close(() => resolve()));
238
- }
239
- const kindFinalError = {
240
- anime: 'Final sync on shutdown failed',
241
- manga: 'Final manga sync on shutdown failed',
242
- tv: 'Final TV sync on shutdown failed',
243
- asmr: 'Final ASMR sync on shutdown failed',
244
- };
245
- for (const kind of Object.keys(KIND_SYNC)) {
246
- if (!unsynced[kind])
247
- continue;
248
- logger.info(`Sync on shutdown: uploading final ${kindDbLabel[kind]} changes...`);
249
- unsynced[kind] = false;
250
- try {
251
- await syncUp(kindDbs()[kind], kind, remoteFolder);
252
- }
253
- catch (e) {
254
- logger.error({ err: e }, kindFinalError[kind]);
255
- }
256
- }
257
- await waitForSync();
258
- asmrDb.close(() => { });
259
- tvDb.close(() => { });
260
- mangaDb.close(() => { });
261
- db.close(() => {
262
- notifyServerExit();
263
- if (signal === 'SIGUSR2') {
264
- process.kill(process.pid, 'SIGUSR2');
265
- }
266
- else {
267
- setTimeout(() => process.exit(0), 100).unref();
268
- }
269
- });
270
- };
271
- process.on('SIGINT', () => shutdown('SIGINT'));
272
- process.on('SIGTERM', () => shutdown('SIGTERM'));
273
- process.on('SIGHUP', () => shutdown('SIGHUP'));
274
- process.once('SIGUSR2', () => shutdown('SIGUSR2'));
275
- app.post('/api/internal/shutdown', (c) => {
276
- let remoteAddress;
277
- try {
278
- remoteAddress = getConnInfo(c).remote.address ?? '';
279
- }
280
- catch {
281
- remoteAddress = '';
282
- }
283
- const isLoopback = remoteAddress === '::1' ||
284
- remoteAddress === '127.0.0.1' ||
285
- remoteAddress === '::ffff:127.0.0.1';
286
- if (!isLoopback) {
287
- return c.json({ error: 'Forbidden' }, 403);
288
- }
289
- const expectedToken = process.env.INTERNAL_SHUTDOWN_TOKEN;
290
- const providedToken = c.req.header('x-internal-token');
291
- if (expectedToken) {
292
- const expBuf = Buffer.from(expectedToken);
293
- const provBuf = Buffer.from(typeof providedToken === 'string' ? providedToken : '');
294
- if (expBuf.length !== provBuf.length || !crypto.timingSafeEqual(expBuf, provBuf)) {
295
- logger.warn('Unauthorized internal shutdown attempt rejected: invalid token');
296
- return c.json({ error: 'Forbidden' }, 403);
297
- }
298
- }
299
- const response = c.json({ message: 'Shutting down' });
300
- setTimeout(() => shutdown(), 500);
301
- return response;
302
- });
303
- }
304
- main().catch((err) => {
305
- logger.error({ err }, 'Server failed to start');
306
- process.exit(1);
307
- });
1
+ process.setMaxListeners(100);import{EventEmitter as Z}from"events";Z.defaultMaxListeners=100;import l from"path";import{AppCache as ee}from"./utils/cache.utils.js";import D from"fs";import te from"crypto";import{serve as re}from"@hono/node-server";import{serveStatic as ae}from"@hono/node-server/serve-static";import{getConnInfo as oe}from"@hono/node-server/conninfo";import o from"./logger.js";import{notifyServerExit as ne}from"./lib/ipc.js";import{CONFIG as t}from"./config.js";import{initializeDatabase as U,initializeMangaDatabase as ie,initializeTvDatabase as se,initializeAsmrDatabase as de,runFullSyncSequence as ce,syncUp as W,waitForSync as fe,KIND_SYNC as c}from"./sync.js";import{isJasmrApi as le}from"./hono/asmr.js";import{loadRemoteProviders as me}from"./providers/remote-loader.js";import{discordRPCService as B}from"./discord-rpc.js";import{discordGatewayService as G}from"./discord-gateway.js";import{SettingsRepository as pe}from"./repositories/settings.repository.js";import{createHonoApp as ue}from"./app-hono.js";import{startWatchlistDiscovery as ge,stopWatchlistDiscovery as ve}from"./hono/watchlist.js";import{checkAnilistStatus as he}from"./lib/anilist.js";import{offlineDb as v}from"./lib/offline-db.js";const y=ue(()=>({shuttingDown:S,dbReady:!!i}),()=>({shuttingDown:S,dbsReady:!!i&&!!m&&!!p&&!!u,bootSyncing:z}),()=>({db:i,mangaDb:m,tvDb:p,asmrDb:u}),{initializeDatabase:U,setDb:r=>{i=r}},{getCatalog:P,refresh:w},q,{getApiCache:()=>K,getProviders:()=>h,getMangaProvider:r=>R[r],getTvProvider:r=>E[r],getProxyRules:()=>L,getJasmr:()=>{const r=h.jasmr;return le(r)?r:void 0}}),K=new ee({ttlSeconds:3600,maxKeys:5e3}),h={},R={},E={};let L=[],$=[];function be(){return t.PROVIDER_ENABLED.split(",").map(r=>r.trim().toLowerCase()).filter(Boolean)}function P(){return $}async function w(){const r=t.PROVIDER_REPO_URL.trim();if(!r)return P();try{const a=await me({cache:K,registryUrl:r,enabledProviders:be()});if(a.catalog.length===0)return o.warn("remote providers refresh returned nothing, keeping current set"),P();for(const n of Object.keys(h))n in a.providers||delete h[n];Object.assign(h,a.providers);for(const n of Object.keys(R))n in a.mangaProviders||delete R[n];Object.assign(R,a.mangaProviders);for(const n of Object.keys(E))n in a.tvProviders||delete E[n];Object.assign(E,a.tvProviders),L=a.proxyRules,$=a.catalog,o.info({count:Object.keys(a.providers).length,mangaCount:Object.keys(a.mangaProviders).length,tvCount:Object.keys(a.tvProviders).length},"remote providers refreshed")}catch(a){o.error({err:a},"remote providers refresh failed")}return P()}let i,m,p,u,S=!1,z=!0;async function q(r,a,n){const O=t.IS_DEV?t.REMOTE_FOLDER_DEV:t.REMOTE_FOLDER_PROD;await ce({db:r,mangaDb:a,tvDb:p,asmrDb:u},{remoteFolder:O,preferredProvider:n})}if(ge(()=>i),!t.IS_DEV){const r=l.join(t.PACKAGE_ROOT,"client","dist");o.info(`Serving frontend from: ${r}`),y.use("/*",ae({root:r})),y.get("*",async a=>{if(a.req.path.startsWith("/api/"))return a.notFound();try{const n=await D.promises.readFile(l.join(r,"index.html"),"utf8");return a.html(n)}catch(n){return o.error({err:n},`Failed to serve index.html from ${r}`),a.text("Server Error: Frontend build not found.",500)}})}async function De(){const r=t.IS_DEV?t.DB_NAME_DEV:t.DB_NAME_PROD,a=l.join(t.ROOT,r),n=t.IS_DEV?t.REMOTE_FOLDER_DEV:t.REMOTE_FOLDER_PROD;i=await U(a),o.info(`Database initialized at ${a}`);const O=t.IS_DEV?t.MANGA_DB_NAME_DEV:t.MANGA_DB_NAME_PROD,I=l.join(t.ROOT,O);m=await ie(I),o.info(`Manga database initialized at ${I}`);const H=t.IS_DEV?t.TV_DB_NAME_DEV:t.TV_DB_NAME_PROD,M=l.join(t.ROOT,H);p=await se(M),o.info(`TV database initialized at ${M}`);const J=t.IS_DEV?t.ASMR_DB_NAME_DEV:t.ASMR_DB_NAME_PROD,k=l.join(t.ROOT,J);u=await de(k),o.info(`ASMR database initialized at ${k}`),await v.init(i),await v.checkWeeklyUpdateDue(i)&&(o.info("Weekly offline database update is due on startup, starting background update..."),v.executeScheduledUpdate(i).catch(e=>{o.warn({err:e?.message},"Startup scheduled offline database update failed")}));const T=await pe.getByKey(i,"discordRPCEnabled"),j=T?T.value==="true":!0;await B.setEnabled(j),G.setEnabled(j),he().catch(()=>{}),t.PROVIDER_REPO_URL.trim()&&setInterval(()=>{w().catch(e=>o.error({err:e},"periodic providers refresh failed"))},t.PROVIDER_REPO_REFRESH_MS).unref();const A=()=>({anime:i,manga:m,tv:p,asmr:u});for(const e of Object.keys(c))D.existsSync(c[e].manifestPath)||D.writeFileSync(c[e].manifestPath,JSON.stringify({version:0}));const f={anime:!1,manga:!1,tv:!1,asmr:!1},Y=Object.keys(c).map(e=>D.watch(c[e].manifestPath,s=>{(s==="change"||s==="rename")&&(f[e]=!0)})),N=re({fetch:y.fetch,port:t.PORT},()=>{o.info(`Server running on http://localhost:${t.PORT}`)});q(i,m).catch(e=>o.error({err:e},"background boot sync failed")).finally(()=>{z=!1}),w().catch(e=>o.error({err:e},"background providers refresh failed"));const _={anime:"database",manga:"manga database",tv:"TV database",asmr:"ASMR database"},Q=setInterval(async()=>{for(const e of Object.keys(c))if(f[e]){o.info(`Uploading accumulated ${_[e]} changes...`),f[e]=!1;try{await W(A()[e],e,n)}catch(s){o.error({err:s},`Failed to upload ${_[e]} changes`),f[e]=!0}}},3e5),F=setInterval(async()=>{await v.checkWeeklyUpdateDue(i)&&(o.info("Weekly offline database update triggered by periodic schedule..."),v.executeScheduledUpdate(i).catch(e=>{o.warn({err:e?.message},"Interval scheduled offline database update failed")}))},360*60*1e3);F.unref();const g=async e=>{if(S)return;S=!0,ve(),clearInterval(Q),clearInterval(F),B.disconnect(),G.shutdown();for(const d of Y)await d.close();N&&await new Promise(d=>N.close(()=>d()));const s={anime:"Final sync on shutdown failed",manga:"Final manga sync on shutdown failed",tv:"Final TV sync on shutdown failed",asmr:"Final ASMR sync on shutdown failed"};for(const d of Object.keys(c))if(f[d]){o.info(`Sync on shutdown: uploading final ${_[d]} changes...`),f[d]=!1;try{await W(A()[d],d,n)}catch(b){o.error({err:b},s[d])}}await fe(),u.close(()=>{}),p.close(()=>{}),m.close(()=>{}),i.close(()=>{ne(),e==="SIGUSR2"?process.kill(process.pid,"SIGUSR2"):setTimeout(()=>process.exit(0),100).unref()})};process.on("SIGINT",()=>g("SIGINT")),process.on("SIGTERM",()=>g("SIGTERM")),process.on("SIGHUP",()=>g("SIGHUP")),process.once("SIGUSR2",()=>g("SIGUSR2")),y.post("/api/internal/shutdown",e=>{let s;try{s=oe(e).remote.address??""}catch{s=""}if(!(s==="::1"||s==="127.0.0.1"||s==="::ffff:127.0.0.1"))return e.json({error:"Forbidden"},403);const b=process.env.INTERNAL_SHUTDOWN_TOKEN,V=e.req.header("x-internal-token");if(b){const x=Buffer.from(b),C=Buffer.from(typeof V=="string"?V:"");if(x.length!==C.length||!te.timingSafeEqual(x,C))return o.warn("Unauthorized internal shutdown attempt rejected: invalid token"),e.json({error:"Forbidden"},403)}const X=e.json({message:"Shutting down"});return setTimeout(()=>g(),500),X})}De().catch(r=>{o.error({err:r},"Server failed to start"),process.exit(1)});
@@ -1,6 +1 @@
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
- "PROVIDER_REPO_URL": "https://raw.githubusercontent.com/serifpersia/dango-providers/main/registry.json"
6
- };
1
+ const t={GITHUB_CLIENT_ID:"Ov23liT1ZtPk7XtN9PZk",GOOGLE_AUTH_WORKER_URL:"https://google-auth-client.ramiserifpersia.workers.dev",DISCORD_CLIENT_ID:"1512253778022367426",PROVIDER_REPO_URL:"https://raw.githubusercontent.com/serifpersia/dango-providers/main/registry.json"};export{t as SHIPPED_DEFAULTS};