@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,191 +1 @@
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
+ import{parseJsonBody as L}from"../utils/http.utils.js";const _="https://raw.githubusercontent.com/RockinChaos/AniSchedule/master/raw/sub-schedule.json",v="https://raw.githubusercontent.com/RockinChaos/AniSchedule/master/raw/sub-episode-feed.json",j="https://raw.githubusercontent.com/RockinChaos/AniSchedule/master/raw/hentai-episode-feed.json",R=900*1e3,F=600*1e3,N=1e4,k={data:null,expiry:0},B={data:null,expiry:0},H={data:null,expiry:0},S=new Map;async function J(e){const n=new AbortController,r=setTimeout(()=>n.abort(),N);try{const i=await fetch(e,{signal:n.signal,headers:{"User-Agent":"dango"}});if(!i.ok)throw new Error(`AniSchedule fetch failed with status ${i.status}`);const a=await L(i);if(Array.isArray(a)&&a.length===0)throw new Error("AniSchedule returned an empty payload");return a}finally{clearTimeout(r)}}function b(e,n,r,i){if(r.data!==null&&Date.now()<r.expiry)return Promise.resolve(r.data);const a=S.get(e);if(a)return a;const u=J(n).then(s=>(r.data=s,r.expiry=Date.now()+i,s)).catch(s=>{throw S.delete(e),s});return S.set(e,u),u}function Y(){return b("anischedule:shows",_,k,R)}function D(){return b("anischedule:subfeed",v,B,F)}function M(){return b("anischedule:hentaifeed",j,H,F)}function G(e,n,r){const i=Math.floor(Date.now()/1e3);return{...e,episodeNumber:n,aired:r<=i,nextAiring:{episode:n,timeUntilAiring:r-i},nextEpisodeAirDate:new Date(r*1e3).toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric",timeZone:"UTC"}),airTime:new Date(r*1e3).toLocaleTimeString("en-US",{hour:"2-digit",minute:"2-digit",timeZone:"UTC"})}}function Z(e){const n=e.title,r=n?.romaji||n?.english||n?.native||"Unknown";return{_id:e.id.toString(),id:e.id.toString(),anilistId:e.id,name:r,englishName:n?.english,nativeName:n?.native,names:{romaji:n?.romaji,english:n?.english,native:n?.native},thumbnail:e.coverImage?.extraLarge||e.coverImage?.medium||"",bannerImage:e.bannerImage||void 0,genres:e.genres?.map(i=>({name:i})),type:e.format,year:e.seasonYear??void 0,isAdult:e.isAdult}}async function K(e,n,r=!1){const i=Math.floor(e.getTime()/1e3),a=i+86400,u=Math.floor(Date.now()/1e3),s=n&&n!=="ALL"?n:void 0,d=Y(),l=D(),P=r?M():Promise.resolve([]),[A,w,C]=await Promise.all([d,l,P]),p=new Map;for(const t of A)p.set(t.id,t);const U=t=>r?!!t.isAdult:!t.isAdult,h=new Map,g=new Map,y=(t,o,m,f)=>{const c=p.get(t);if(c){if(s&&c.format!==s||!U(c))return;const T=c.idMal??c.id,x=h.get(T);(!x||m>x.airingAt)&&h.set(T,{show:Z(c),episode:o,airingAt:m,aired:m<=u});return}if(r||s&&f&&f!==s)return;const E=g.get(t);(!E||m>E.airingAt)&&g.set(t,{id:t,episode:o,airingAt:m,format:f})};for(const t of A)for(const o of t.airingSchedule.nodes)o.airingAt>=i&&o.airingAt<a&&y(t.id,o.episode,o.airingAt);const I=r?[...w,...C]:w;for(const t of I){const o=Date.parse(t.episode.airedAt)/1e3;o>=i&&o<a&&y(t.id,t.episode.aired,o,t.format)}return{entries:Array.from(h.entries()).map(([,t])=>t).sort((t,o)=>t.airingAt-o.airingAt),missingMeta:Array.from(g.values())}}async function Q(e,n,r){const i=new Set(e),[a,u]=await Promise.all([D(),M()]),s=[];for(const d of[...a,...u]){if(!i.has(d.id))continue;const l=Date.parse(d.episode.airedAt)/1e3;l>=n&&l<=r&&s.push({mediaId:d.id,episode:d.episode.aired,airingAt:l})}return s}export{Q as getAiredEpisodesFromAniScheduleFeed,M as getAniScheduleHentaiFeed,Y as getAniScheduleShows,D as getAniScheduleSubFeed,K as getScheduleFromAniSchedule,G as withScheduleFields};
@@ -1,185 +1 @@
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
+ import w from"../logger.js";import{InsightsRepository as l}from"../repositories/insights.repository.js";async function F(c){const[m,i,h,n,e,u,a,o]=await Promise.all([l.getCoreStats(c),l.getActivityGrid(c),l.getHourlyDist(c),l.getSeasonality(c),l.getAllWatches(c),l.getWatchedShowsMeta(c),l.getDroppedShows(c),l.getCompletionVelocities(c)]),b=i.length>0?Math.max(...i.map(t=>t.count)):0,g=[];if(e.length>0){let t=e[0].effectiveSeconds??e[0].currentTime;for(let r=1;r<e.length;r++){const s=new Date(e[r-1].watchedAt).getTime(),S=new Date(e[r].watchedAt).getTime()-s;Number.isFinite(S)&&S>0&&S<36e5?t+=e[r].effectiveSeconds??e[r].currentTime:(g.push(t),t=e[r].effectiveSeconds??e[r].currentTime)}g.push(t)}const W=g.length>0?Math.round(g.reduce((t,r)=>t+r,0)/g.length/60):0,p={};let y=0,f=0;for(const t of u){let r=[];if(t.genres)try{t.genres.startsWith("[")?r=JSON.parse(t.genres):r=t.genres.split(",").map(s=>s.trim())}catch(s){w.warn({err:s,showId:t.id},"Failed to parse genres for insights")}for(const s of r)p[s]=(p[s]||0)+1;if(t.popularityScore){let s=t.popularityScore;s>10&&(s=s/10),y+=s,f++}}const M=Object.entries(p).sort((t,r)=>r[1]-t[1])[0]?.[0],v={Action:"Shonen Warrior",Romance:"Hopeless Romantic",Comedy:"Chaos Enjoyer","Slice of Life":"Vibe Seeker",Horror:"Fearless Watcher",Fantasy:"Isekai Traveller","Sci-Fi":"Future Scientist",Drama:"Feels Collector"}[M||""]||"Anime Enthusiast",C=o.length>0?Math.round(o.reduce((t,r)=>t+r.daysToFinish,0)/o.length):0;return{totalHours:Math.round((m?.totalSeconds||0)/3600),totalEpisodes:m?.totalEpisodes||0,totalAnime:m?.totalAnime||0,completedAnime:m?.completedCount||0,completionRate:m?.totalWatchlist>0?Math.round(m.completedCount/m.totalWatchlist*100):0,persona:v,bingeFactor:b,avgSessionMinutes:W,avgCompletionDays:C,popularityScore:f>0?Math.round(y/f):0,genreSplit:Object.entries(p).map(([t,r])=>({name:t,count:r})).sort((t,r)=>r.count-t.count).slice(0,8)||[],activityGrid:i||[],hourlyDist:Array.from({length:24},(t,r)=>{const s=r.toString().padStart(2,"0");return{hour:s,count:h?.find(d=>d.hour===s)?.count||0}})||[],seasonality:Array.from({length:12},(t,r)=>{const s=(r+1).toString().padStart(2,"0");return{month:s,seconds:n?.find(d=>d.month===s)?.seconds||0}})||[],droppedShows:(a||[]).slice(0,5)}}async function E(c){const m=await l.getWatchedEpisodesWithMeta(c),i={};for(const n of m){let e=[];if(n.genres)try{n.genres.startsWith("[")?e=JSON.parse(n.genres):e=n.genres.split(",").map(o=>o.trim())}catch(o){w.warn({err:o,showId:n.showId},"Failed to parse genres for genre cards")}const u=(n.effectiveSeconds??n.currentTime)||0;let a=n.popularityScore||0;a>10&&(a=a/10);for(const o of e)i[o]||(i[o]={count:0,totalTime:0,scores:[],showWatches:{},showMeta:{}}),i[o].count++,i[o].totalTime+=u,a>0&&i[o].scores.push(a),i[o].showWatches[n.showId]=(i[o].showWatches[n.showId]||0)+1,(n.name||n.thumbnail)&&(i[o].showMeta[n.showId]={name:n.name,nativeName:n.nativeName,englishName:n.englishName,thumbnail:n.thumbnail})}const h=Object.entries(i).map(([n,e])=>{const u=Object.entries(e.showWatches).sort((a,o)=>o[1]-a[1]).slice(0,4).map(([a])=>({id:a,name:e.showMeta[a]?.name||"",nativeName:e.showMeta[a]?.nativeName,englishName:e.showMeta[a]?.englishName,thumbnail:e.showMeta[a]?.thumbnail||""}));return{rank:0,name:n,count:e.count,meanScore:e.scores.length>0?e.scores.reduce((a,o)=>a+o,0)/e.scores.length:0,timeWatched:T(e.totalTime),topShows:u}});return h.sort((n,e)=>e.count-n.count),h.forEach((n,e)=>{n.rank=e+1}),h}function T(c){const m=Math.floor(c/86400),i=Math.floor(c%86400/3600),h=Math.floor(c%3600/60);return m>0?`${m} days ${i>0?`${i} hours`:""}`:i>0?`${i} hours${h>0?` ${h} mins`:""}`:`${h} mins`}export{E as getGenreCards,F as getWatchInsights};
@@ -1,23 +1 @@
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
- }
1
+ import n from"../logger.js";function e(t){try{typeof process.send=="function"&&process.send(t)}catch{}}function i(t){e({type:"SYNC_START",message:t}),n.info(`[SYNC_START] ${t}`)}function r(){e({type:"SYNC_END"}),n.info("[SYNC_END]")}function S(){e({type:"SERVER_EXIT"}),n.info("[SERVER_EXIT]")}export{S as notifyServerExit,r as notifySyncEnd,i as notifySyncStart};