@serifpersia/dango 3.6.7 → 3.6.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/README.md +1 -1
  2. package/client/dist/assets/{AnimeCard-C3P9zWi9.js → AnimeCard-DKDJK8Mp.js} +1 -1
  3. package/client/dist/assets/{AnimeInfo-Et6687Tl.js → AnimeInfo-CTcvfjva.js} +1 -1
  4. package/client/dist/assets/{AnimeInfoPage-DX4eNnlH.js → AnimeInfoPage-B_UiacIN.js} +2 -2
  5. package/client/dist/assets/{Asmr-CCdEWrFi.js → Asmr-DMSN2vxl.js} +2 -2
  6. package/client/dist/assets/{AsmrResetProgressModal-DgEWBJOw.js → AsmrResetProgressModal-CRwfOatX.js} +1 -1
  7. package/client/dist/assets/{AvSyncCalibrator-CQ2fjf2E.js → AvSyncCalibrator-CRJsi-vz.js} +1 -1
  8. package/client/dist/assets/{Home-DUkUTTTy.js → Home-pwmK9_Eh.js} +1 -1
  9. package/client/dist/assets/{Insights-QCh4xW12.js → Insights-m7kFzg2A.js} +1 -1
  10. package/client/dist/assets/{LibraryListPage-BoTxRqHz.js → LibraryListPage-C1wpPvu6.js} +1 -1
  11. package/client/dist/assets/ListeningList-OzfEI1Ga.js +1 -0
  12. package/client/dist/assets/{Manga-HydR-L7o.js → Manga-ChRCExhK.js} +1 -1
  13. package/client/dist/assets/{MangaInfoPage-CBakEI9a.js → MangaInfoPage-DSHJPo2A.js} +1 -1
  14. package/client/dist/assets/{MangaReadPage-DwAR3iPy.js → MangaReadPage-CzOVY1wA.js} +1 -1
  15. package/client/dist/assets/{MangaResetProgressModal-DGzl8vsO.js → MangaResetProgressModal-CJfzdUBb.js} +1 -1
  16. package/client/dist/assets/{Mature-Pi0TxKuF.js → Mature-CifQMfhs.js} +1 -1
  17. package/client/dist/assets/{MediaCard--cuFxOCS.js → MediaCard-CM10yB9V.js} +1 -1
  18. package/client/dist/assets/{OptionListMenu-B3D39dGS.js → OptionListMenu-6Kovbsqw.js} +1 -1
  19. package/client/dist/assets/{Player-CsH4x_I_.js → Player-CK0hT-u4.js} +2 -2
  20. package/client/dist/assets/{PlayerSettings-DGxBsi3F.js → PlayerSettings-Bb66uqT9.js} +1 -1
  21. package/client/dist/assets/{QueueOptionsButton-Djq4Cpex.js → QueueOptionsButton-C_nTj8_q.js} +1 -1
  22. package/client/dist/assets/{QueueRail-DOrYgNVX.js → QueueRail-DUK7URAE.js} +1 -1
  23. package/client/dist/assets/{ReadingList-DwYoBrNk.js → ReadingList-Ck9LJThU.js} +1 -1
  24. package/client/dist/assets/{ResetProgressModal-CMF86mX6.js → ResetProgressModal-BUphIv0k.js} +1 -1
  25. package/client/dist/assets/{Search-DFkhuk-x.js → Search-zJyP15h6.js} +1 -1
  26. package/client/dist/assets/{Settings-DBB2r0VQ.js → Settings-BYdoSEUh.js} +1 -1
  27. package/client/dist/assets/{SkeletonGrid-DJelJgAv.js → SkeletonGrid-CqgbOhjX.js} +1 -1
  28. package/client/dist/assets/{Trackers-DGYXFhYZ.js → Trackers-BVjyv6CM.js} +1 -1
  29. package/client/dist/assets/{Tv-C5MDGD2Z.js → Tv-L13ylswR.js} +1 -1
  30. package/client/dist/assets/{TvInfo-DrKuK13u.js → TvInfo-Cvcm5U2D.js} +1 -1
  31. package/client/dist/assets/{TvPopup-DtHilRDT.js → TvPopup-DhxfKvYm.js} +1 -1
  32. package/client/dist/assets/{TvSearch-CzNxdu_w.js → TvSearch-P8bFE6_X.js} +1 -1
  33. package/client/dist/assets/{TvWatchlist-D7p4br6S.js → TvWatchlist-D5gVPjtb.js} +1 -1
  34. package/client/dist/assets/{Watchlist-DrOzswl2.js → Watchlist-Cjq8l3rU.js} +1 -1
  35. package/client/dist/assets/{hls-BGCX7mJy.js → hls-BkzuANAg.js} +1 -1
  36. package/client/dist/assets/{index-CnfZwsds.js → index-CLVgzKLk.js} +19 -19
  37. package/client/dist/assets/{useAnimeInfoData-DYXWBPF-.js → useAnimeInfoData-DZJZu-so.js} +1 -1
  38. package/client/dist/assets/{useAsmr-BbhQ59bk.js → useAsmr-CvrMGHuo.js} +1 -1
  39. package/client/dist/assets/useAsmrPopup-Bimy8Ouw.js +1 -0
  40. package/client/dist/assets/{useMangaLibrary-DuesWQaE.js → useMangaLibrary--MiN11uR.js} +1 -1
  41. package/client/dist/assets/{useMangaPopup-D57OVGUr.js → useMangaPopup-CR5XhKKS.js} +1 -1
  42. package/client/dist/assets/{useProviders-mpQb_dix.js → useProviders-DqdT4fA5.js} +1 -1
  43. package/client/dist/assets/{useTv-DGflCsSI.js → useTv-BL0HMDyP.js} +1 -1
  44. package/client/dist/assets/{useTvLibrary-BqeSmyc4.js → useTvLibrary-D4U4iqN8.js} +1 -1
  45. package/client/dist/index.html +1 -1
  46. package/package.json +3 -5
  47. package/server/dist/config.js +1 -174
  48. package/server/dist/server.js +1068 -289
  49. package/server/dist/shipped-defaults.js +1 -6
  50. package/server/package.json +4 -5
  51. package/client/dist/assets/Asmr.module-CdWlioTY.js +0 -1
  52. package/client/dist/assets/ListeningList-DguKjI_t.js +0 -1
  53. package/client/dist/assets/Music-BGH_0voi.css +0 -1
  54. package/client/dist/assets/Music-CQpyXTLH.js +0 -1
  55. package/client/dist/assets/Radio-CCHNFN7R.css +0 -1
  56. package/client/dist/assets/Radio-DF6g6odd.js +0 -1
  57. package/client/dist/assets/Radio.module-DGSZLpKM.js +0 -1
  58. package/client/dist/assets/useAsmrPopup-SymV1J-b.js +0 -1
  59. package/server/dist/app-auth.js +0 -157
  60. package/server/dist/app-hono.js +0 -598
  61. package/server/dist/db/drizzle.js +0 -16
  62. package/server/dist/db/schema-anime.js +0 -61
  63. package/server/dist/db/schema-asmr.js +0 -13
  64. package/server/dist/db/schema-manga.js +0 -31
  65. package/server/dist/db/schema-tv.js +0 -18
  66. package/server/dist/db.js +0 -217
  67. package/server/dist/discord-gateway.js +0 -385
  68. package/server/dist/discord-rpc.js +0 -413
  69. package/server/dist/github-sync.js +0 -508
  70. package/server/dist/google.js +0 -741
  71. package/server/dist/hono/asmr.js +0 -10
  72. package/server/dist/hono/auth.js +0 -49
  73. package/server/dist/hono/music.js +0 -209
  74. package/server/dist/hono/proxy.js +0 -827
  75. package/server/dist/hono/tv.js +0 -200
  76. package/server/dist/hono/watchlist.js +0 -341
  77. package/server/dist/lib/anilist.js +0 -1583
  78. package/server/dist/lib/anischedule.js +0 -191
  79. package/server/dist/lib/insights.js +0 -185
  80. package/server/dist/lib/ipc.js +0 -23
  81. package/server/dist/lib/kitsu.js +0 -517
  82. package/server/dist/lib/mal-import.js +0 -291
  83. package/server/dist/lib/mal.js +0 -957
  84. package/server/dist/lib/migration.js +0 -310
  85. package/server/dist/lib/offline-db.js +0 -395
  86. package/server/dist/lib/potoken.js +0 -135
  87. package/server/dist/lib/recommendations.service.js +0 -216
  88. package/server/dist/lib/temp-ids.js +0 -3
  89. package/server/dist/lib/tmdb.js +0 -72
  90. package/server/dist/lib/tracker/anilist-tracker.js +0 -308
  91. package/server/dist/lib/tracker/manga-sync.service.js +0 -773
  92. package/server/dist/lib/tracker/sync-state.js +0 -21
  93. package/server/dist/lib/tracker/sync.service.js +0 -476
  94. package/server/dist/lib/translate.js +0 -79
  95. package/server/dist/lib/ump.js +0 -106
  96. package/server/dist/lib/ytmusic.js +0 -162
  97. package/server/dist/logger.js +0 -16
  98. package/server/dist/providers/manga/manga.types.js +0 -1
  99. package/server/dist/providers/provider.interface.js +0 -1
  100. package/server/dist/providers/remote-loader.js +0 -614
  101. package/server/dist/providers/remote-types.js +0 -159
  102. package/server/dist/providers/title-matching.js +0 -226
  103. package/server/dist/providers/tv.types.js +0 -1
  104. package/server/dist/rclone.js +0 -120
  105. package/server/dist/repositories/asmr.repository.js +0 -77
  106. package/server/dist/repositories/insights.repository.js +0 -106
  107. package/server/dist/repositories/library.repository.js +0 -94
  108. package/server/dist/repositories/mal-cache.repository.js +0 -52
  109. package/server/dist/repositories/manga.repository.js +0 -95
  110. package/server/dist/repositories/notifications.repository.js +0 -60
  111. package/server/dist/repositories/queue.repository.js +0 -72
  112. package/server/dist/repositories/settings.repository.js +0 -30
  113. package/server/dist/repositories/shows-meta.repository.js +0 -58
  114. package/server/dist/repositories/temp-show-ids.repository.js +0 -63
  115. package/server/dist/repositories/tv.repository.js +0 -93
  116. package/server/dist/repositories/watched-episodes.repository.js +0 -82
  117. package/server/dist/repositories/watchlist.repository.js +0 -60
  118. package/server/dist/scripts/backfill-episode-duration.js +0 -42
  119. package/server/dist/sync-payload.js +0 -91
  120. package/server/dist/sync.js +0 -657
  121. package/server/dist/trpc/context.js +0 -7
  122. package/server/dist/trpc/errors.js +0 -15
  123. package/server/dist/trpc/index.js +0 -18
  124. package/server/dist/trpc/router.js +0 -49
  125. package/server/dist/trpc/routers/asmr-library.js +0 -112
  126. package/server/dist/trpc/routers/asmr-progress.js +0 -121
  127. package/server/dist/trpc/routers/asmr.js +0 -87
  128. package/server/dist/trpc/routers/auth.js +0 -194
  129. package/server/dist/trpc/routers/continue-watching.js +0 -81
  130. package/server/dist/trpc/routers/data.js +0 -1026
  131. package/server/dist/trpc/routers/discord.js +0 -285
  132. package/server/dist/trpc/routers/discovery.js +0 -11
  133. package/server/dist/trpc/routers/insights.js +0 -14
  134. package/server/dist/trpc/routers/manga-library.js +0 -263
  135. package/server/dist/trpc/routers/manga-progress.js +0 -169
  136. package/server/dist/trpc/routers/manga.js +0 -201
  137. package/server/dist/trpc/routers/music.js +0 -655
  138. package/server/dist/trpc/routers/notifications.js +0 -80
  139. package/server/dist/trpc/routers/progress.js +0 -109
  140. package/server/dist/trpc/routers/queue.js +0 -93
  141. package/server/dist/trpc/routers/radio.js +0 -145
  142. package/server/dist/trpc/routers/settings.js +0 -96
  143. package/server/dist/trpc/routers/tracker.js +0 -213
  144. package/server/dist/trpc/routers/tv-library.js +0 -135
  145. package/server/dist/trpc/routers/tv-progress.js +0 -159
  146. package/server/dist/trpc/routers/tv.js +0 -551
  147. package/server/dist/trpc/routers/watchlist.js +0 -550
  148. package/server/dist/trpc/validation.js +0 -258
  149. package/server/dist/utils/cache.utils.js +0 -25
  150. package/server/dist/utils/cookie.utils.js +0 -13
  151. package/server/dist/utils/db-utils.js +0 -17
  152. package/server/dist/utils/env.utils.js +0 -77
  153. package/server/dist/utils/http.utils.js +0 -82
  154. package/server/dist/utils/request-context.js +0 -2
  155. package/server/dist/utils/security.utils.js +0 -76
  156. /package/client/dist/assets/{Asmr-D1KuQwQy.css → useAsmrPopup-D1KuQwQy.css} +0 -0
@@ -1,191 +0,0 @@
1
- import { parseJsonBody } from '../utils/http.utils.js';
2
- const SUB_SCHEDULE_URL = 'https://raw.githubusercontent.com/RockinChaos/AniSchedule/master/raw/sub-schedule.json';
3
- const SUB_FEED_URL = 'https://raw.githubusercontent.com/RockinChaos/AniSchedule/master/raw/sub-episode-feed.json';
4
- const HENTAI_FEED_URL = 'https://raw.githubusercontent.com/RockinChaos/AniSchedule/master/raw/hentai-episode-feed.json';
5
- const SCHEDULE_TTL_MS = 15 * 60 * 1000;
6
- const FEED_TTL_MS = 10 * 60 * 1000;
7
- const FETCH_TIMEOUT_MS = 10000;
8
- const scheduleCache = { data: null, expiry: 0 };
9
- const subFeedCache = { data: null, expiry: 0 };
10
- const hentaiFeedCache = { data: null, expiry: 0 };
11
- const inFlight = new Map();
12
- async function fetchJson(url) {
13
- const controller = new AbortController();
14
- const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
15
- try {
16
- const response = await fetch(url, {
17
- signal: controller.signal,
18
- headers: { 'User-Agent': 'dango' },
19
- });
20
- if (!response.ok) {
21
- throw new Error(`AniSchedule fetch failed with status ${response.status}`);
22
- }
23
- const json = await parseJsonBody(response);
24
- if (Array.isArray(json) && json.length === 0) {
25
- throw new Error('AniSchedule returned an empty payload');
26
- }
27
- return json;
28
- }
29
- finally {
30
- clearTimeout(timer);
31
- }
32
- }
33
- function fetchCached(key, url, cache, ttlMs) {
34
- if (cache.data !== null && Date.now() < cache.expiry) {
35
- return Promise.resolve(cache.data);
36
- }
37
- const existing = inFlight.get(key);
38
- if (existing)
39
- return existing;
40
- const request = fetchJson(url)
41
- .then((data) => {
42
- cache.data = data;
43
- cache.expiry = Date.now() + ttlMs;
44
- return data;
45
- })
46
- .catch((err) => {
47
- inFlight.delete(key);
48
- throw err;
49
- });
50
- inFlight.set(key, request);
51
- return request;
52
- }
53
- export function getAniScheduleShows() {
54
- return fetchCached('anischedule:shows', SUB_SCHEDULE_URL, scheduleCache, SCHEDULE_TTL_MS);
55
- }
56
- export function getAniScheduleSubFeed() {
57
- return fetchCached('anischedule:subfeed', SUB_FEED_URL, subFeedCache, FEED_TTL_MS);
58
- }
59
- export function getAniScheduleHentaiFeed() {
60
- return fetchCached('anischedule:hentaifeed', HENTAI_FEED_URL, hentaiFeedCache, FEED_TTL_MS);
61
- }
62
- export function withScheduleFields(show, episode, airingAt) {
63
- const now = Math.floor(Date.now() / 1000);
64
- return {
65
- ...show,
66
- episodeNumber: episode,
67
- aired: airingAt <= now,
68
- nextAiring: { episode, timeUntilAiring: airingAt - now },
69
- nextEpisodeAirDate: new Date(airingAt * 1000).toLocaleDateString('en-US', {
70
- month: 'short',
71
- day: 'numeric',
72
- year: 'numeric',
73
- timeZone: 'UTC',
74
- }),
75
- airTime: new Date(airingAt * 1000).toLocaleTimeString('en-US', {
76
- hour: '2-digit',
77
- minute: '2-digit',
78
- timeZone: 'UTC',
79
- }),
80
- };
81
- }
82
- function fromAniScheduleShow(s) {
83
- const title = s.title;
84
- const name = title?.romaji || title?.english || title?.native || 'Unknown';
85
- return {
86
- _id: s.id.toString(),
87
- id: s.id.toString(),
88
- anilistId: s.id,
89
- name,
90
- englishName: title?.english,
91
- nativeName: title?.native,
92
- names: {
93
- romaji: title?.romaji,
94
- english: title?.english,
95
- native: title?.native,
96
- },
97
- thumbnail: s.coverImage?.extraLarge || s.coverImage?.medium || '',
98
- bannerImage: s.bannerImage || undefined,
99
- genres: s.genres?.map((g) => ({ name: g })),
100
- type: s.format,
101
- year: s.seasonYear ?? undefined,
102
- isAdult: s.isAdult,
103
- };
104
- }
105
- export async function getScheduleFromAniSchedule(date, format, adult = false) {
106
- const dayStart = Math.floor(date.getTime() / 1000);
107
- const dayEnd = dayStart + 86400;
108
- const now = Math.floor(Date.now() / 1000);
109
- const formatFilter = format && format !== 'ALL' ? format : undefined;
110
- const schedulePromise = getAniScheduleShows();
111
- const subFeedPromise = getAniScheduleSubFeed();
112
- const hentaiFeedPromise = adult ? getAniScheduleHentaiFeed() : Promise.resolve([]);
113
- const [shows, subFeed, hentaiFeed] = await Promise.all([
114
- schedulePromise,
115
- subFeedPromise,
116
- hentaiFeedPromise,
117
- ]);
118
- const showById = new Map();
119
- for (const show of shows) {
120
- showById.set(show.id, show);
121
- }
122
- const matchesAdult = (show) => (adult ? !!show.isAdult : !show.isAdult);
123
- const bestByShow = new Map();
124
- const missingMeta = new Map();
125
- const consider = (id, episode, airingAt, entryFormat) => {
126
- const show = showById.get(id);
127
- if (show) {
128
- if (formatFilter && show.format !== formatFilter)
129
- return;
130
- if (!matchesAdult(show))
131
- return;
132
- const key = show.idMal ?? show.id;
133
- const current = bestByShow.get(key);
134
- if (!current || airingAt > current.airingAt) {
135
- bestByShow.set(key, {
136
- show: fromAniScheduleShow(show),
137
- episode,
138
- airingAt,
139
- aired: airingAt <= now,
140
- });
141
- }
142
- return;
143
- }
144
- if (adult)
145
- return;
146
- if (formatFilter && entryFormat && entryFormat !== formatFilter)
147
- return;
148
- const existing = missingMeta.get(id);
149
- if (!existing || airingAt > existing.airingAt) {
150
- missingMeta.set(id, { id, episode, airingAt, format: entryFormat });
151
- }
152
- };
153
- for (const show of shows) {
154
- for (const node of show.airingSchedule.nodes) {
155
- if (node.airingAt >= dayStart && node.airingAt < dayEnd) {
156
- consider(show.id, node.episode, node.airingAt);
157
- }
158
- }
159
- }
160
- const feed = adult ? [...subFeed, ...hentaiFeed] : subFeed;
161
- for (const entry of feed) {
162
- const airedAt = Date.parse(entry.episode.airedAt) / 1000;
163
- if (airedAt >= dayStart && airedAt < dayEnd) {
164
- consider(entry.id, entry.episode.aired, airedAt, entry.format);
165
- }
166
- }
167
- const entries = Array.from(bestByShow.entries())
168
- .map(([, value]) => value)
169
- .sort((a, b) => a.airingAt - b.airingAt);
170
- return {
171
- entries,
172
- missingMeta: Array.from(missingMeta.values()),
173
- };
174
- }
175
- export async function getAiredEpisodesFromAniScheduleFeed(ids, startEpoch, endEpoch) {
176
- const idSet = new Set(ids);
177
- const [subFeed, hentaiFeed] = await Promise.all([
178
- getAniScheduleSubFeed(),
179
- getAniScheduleHentaiFeed(),
180
- ]);
181
- const results = [];
182
- for (const entry of [...subFeed, ...hentaiFeed]) {
183
- if (!idSet.has(entry.id))
184
- continue;
185
- const airedAt = Date.parse(entry.episode.airedAt) / 1000;
186
- if (airedAt >= startEpoch && airedAt <= endEpoch) {
187
- results.push({ mediaId: entry.id, episode: entry.episode.aired, airingAt: airedAt });
188
- }
189
- }
190
- return results;
191
- }
@@ -1,185 +0,0 @@
1
- import logger from '../logger.js';
2
- import { InsightsRepository } from '../repositories/insights.repository.js';
3
- export async function getWatchInsights(db) {
4
- const [core, activityGrid, hourlyDist, seasonality, allWatches, watchedShows, droppedWarning, velocities,] = (await Promise.all([
5
- InsightsRepository.getCoreStats(db),
6
- InsightsRepository.getActivityGrid(db),
7
- InsightsRepository.getHourlyDist(db),
8
- InsightsRepository.getSeasonality(db),
9
- InsightsRepository.getAllWatches(db),
10
- InsightsRepository.getWatchedShowsMeta(db),
11
- InsightsRepository.getDroppedShows(db),
12
- InsightsRepository.getCompletionVelocities(db),
13
- ]));
14
- const bingeFactor = activityGrid.length > 0 ? Math.max(...activityGrid.map((a) => a.count)) : 0;
15
- const sessions = [];
16
- if (allWatches.length > 0) {
17
- let currentSessionSeconds = allWatches[0].effectiveSeconds ?? allWatches[0].currentTime;
18
- for (let i = 1; i < allWatches.length; i++) {
19
- const prev = new Date(allWatches[i - 1].watchedAt).getTime();
20
- const curr = new Date(allWatches[i].watchedAt).getTime();
21
- const gap = curr - prev;
22
- if (Number.isFinite(gap) && gap > 0 && gap < 3600000) {
23
- currentSessionSeconds += allWatches[i].effectiveSeconds ?? allWatches[i].currentTime;
24
- }
25
- else {
26
- sessions.push(currentSessionSeconds);
27
- currentSessionSeconds = allWatches[i].effectiveSeconds ?? allWatches[i].currentTime;
28
- }
29
- }
30
- sessions.push(currentSessionSeconds);
31
- }
32
- const avgSessionMinutes = sessions.length > 0 ? Math.round(sessions.reduce((a, b) => a + b, 0) / sessions.length / 60) : 0;
33
- const genreCounts = {};
34
- let totalPopScore = 0;
35
- let popCount = 0;
36
- for (const show of watchedShows) {
37
- let genres = [];
38
- if (show.genres) {
39
- try {
40
- if (show.genres.startsWith('[')) {
41
- genres = JSON.parse(show.genres);
42
- }
43
- else {
44
- genres = show.genres.split(',').map((g) => g.trim());
45
- }
46
- }
47
- catch (e) {
48
- logger.warn({ err: e, showId: show.id }, 'Failed to parse genres for insights');
49
- }
50
- }
51
- for (const g of genres) {
52
- genreCounts[g] = (genreCounts[g] || 0) + 1;
53
- }
54
- if (show.popularityScore) {
55
- let score = show.popularityScore;
56
- if (score > 10)
57
- score = score / 10;
58
- totalPopScore += score;
59
- popCount++;
60
- }
61
- }
62
- const topGenre = Object.entries(genreCounts).sort((a, b) => b[1] - a[1])[0]?.[0];
63
- const personaMap = {
64
- Action: 'Shonen Warrior',
65
- Romance: 'Hopeless Romantic',
66
- Comedy: 'Chaos Enjoyer',
67
- 'Slice of Life': 'Vibe Seeker',
68
- Horror: 'Fearless Watcher',
69
- Fantasy: 'Isekai Traveller',
70
- 'Sci-Fi': 'Future Scientist',
71
- Drama: 'Feels Collector',
72
- };
73
- const persona = personaMap[topGenre || ''] || 'Anime Enthusiast';
74
- const avgCompletionDays = velocities.length > 0
75
- ? Math.round(velocities.reduce((a, b) => a + b.daysToFinish, 0) / velocities.length)
76
- : 0;
77
- return {
78
- totalHours: Math.round((core?.totalSeconds || 0) / 3600),
79
- totalEpisodes: core?.totalEpisodes || 0,
80
- totalAnime: core?.totalAnime || 0,
81
- completedAnime: core?.completedCount || 0,
82
- completionRate: core?.totalWatchlist > 0 ? Math.round((core.completedCount / core.totalWatchlist) * 100) : 0,
83
- persona,
84
- bingeFactor,
85
- avgSessionMinutes,
86
- avgCompletionDays,
87
- popularityScore: popCount > 0 ? Math.round(totalPopScore / popCount) : 0,
88
- genreSplit: Object.entries(genreCounts)
89
- .map(([name, count]) => ({ name, count }))
90
- .sort((a, b) => b.count - a.count)
91
- .slice(0, 8) || [],
92
- activityGrid: activityGrid || [],
93
- hourlyDist: Array.from({ length: 24 }, (_, i) => {
94
- const hour = i.toString().padStart(2, '0');
95
- return { hour, count: hourlyDist?.find((d) => d.hour === hour)?.count || 0 };
96
- }) || [],
97
- seasonality: Array.from({ length: 12 }, (_, i) => {
98
- const month = (i + 1).toString().padStart(2, '0');
99
- return {
100
- month,
101
- seconds: seasonality?.find((s) => s.month === month)?.seconds || 0,
102
- };
103
- }) || [],
104
- droppedShows: (droppedWarning || []).slice(0, 5),
105
- };
106
- }
107
- export async function getGenreCards(db) {
108
- const rows = await InsightsRepository.getWatchedEpisodesWithMeta(db);
109
- const genreData = {};
110
- for (const row of rows) {
111
- let genres = [];
112
- if (row.genres) {
113
- try {
114
- if (row.genres.startsWith('[')) {
115
- genres = JSON.parse(row.genres);
116
- }
117
- else {
118
- genres = row.genres.split(',').map((g) => g.trim());
119
- }
120
- }
121
- catch (e) {
122
- logger.warn({ err: e, showId: row.showId }, 'Failed to parse genres for genre cards');
123
- }
124
- }
125
- const timeWatched = (row.effectiveSeconds ?? row.currentTime) || 0;
126
- let score = row.popularityScore || 0;
127
- if (score > 10)
128
- score = score / 10;
129
- for (const genre of genres) {
130
- if (!genreData[genre]) {
131
- genreData[genre] = { count: 0, totalTime: 0, scores: [], showWatches: {}, showMeta: {} };
132
- }
133
- genreData[genre].count++;
134
- genreData[genre].totalTime += timeWatched;
135
- if (score > 0)
136
- genreData[genre].scores.push(score);
137
- genreData[genre].showWatches[row.showId] = (genreData[genre].showWatches[row.showId] || 0) + 1;
138
- if (row.name || row.thumbnail) {
139
- genreData[genre].showMeta[row.showId] = {
140
- name: row.name,
141
- nativeName: row.nativeName,
142
- englishName: row.englishName,
143
- thumbnail: row.thumbnail,
144
- };
145
- }
146
- }
147
- }
148
- const genreCards = Object.entries(genreData).map(([name, data]) => {
149
- const topShows = Object.entries(data.showWatches)
150
- .sort((a, b) => b[1] - a[1])
151
- .slice(0, 4)
152
- .map(([showId]) => ({
153
- id: showId,
154
- name: data.showMeta[showId]?.name || '',
155
- nativeName: data.showMeta[showId]?.nativeName,
156
- englishName: data.showMeta[showId]?.englishName,
157
- thumbnail: data.showMeta[showId]?.thumbnail || '',
158
- }));
159
- return {
160
- rank: 0,
161
- name,
162
- count: data.count,
163
- meanScore: data.scores.length > 0 ? data.scores.reduce((a, b) => a + b, 0) / data.scores.length : 0,
164
- timeWatched: formatTime(data.totalTime),
165
- topShows,
166
- };
167
- });
168
- genreCards.sort((a, b) => b.count - a.count);
169
- genreCards.forEach((card, i) => {
170
- card.rank = i + 1;
171
- });
172
- return genreCards;
173
- }
174
- function formatTime(seconds) {
175
- const days = Math.floor(seconds / 86400);
176
- const hours = Math.floor((seconds % 86400) / 3600);
177
- const minutes = Math.floor((seconds % 3600) / 60);
178
- if (days > 0) {
179
- return `${days} days ${hours > 0 ? `${hours} hours` : ''}`;
180
- }
181
- if (hours > 0) {
182
- return `${hours} hours${minutes > 0 ? ` ${minutes} mins` : ''}`;
183
- }
184
- return `${minutes} mins`;
185
- }
@@ -1,23 +0,0 @@
1
- import logger from '../logger.js';
2
- function sendToParent(message) {
3
- try {
4
- if (typeof process.send === 'function') {
5
- process.send(message);
6
- }
7
- }
8
- catch {
9
- // ignore
10
- }
11
- }
12
- export function notifySyncStart(message) {
13
- sendToParent({ type: 'SYNC_START', message });
14
- logger.info(`[SYNC_START] ${message}`);
15
- }
16
- export function notifySyncEnd() {
17
- sendToParent({ type: 'SYNC_END' });
18
- logger.info('[SYNC_END]');
19
- }
20
- export function notifyServerExit() {
21
- sendToParent({ type: 'SERVER_EXIT' });
22
- logger.info('[SERVER_EXIT]');
23
- }