@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.
- package/README.md +1 -1
- package/client/dist/assets/{AnimeCard-C3P9zWi9.js → AnimeCard-DKDJK8Mp.js} +1 -1
- package/client/dist/assets/{AnimeInfo-Et6687Tl.js → AnimeInfo-CTcvfjva.js} +1 -1
- package/client/dist/assets/{AnimeInfoPage-DX4eNnlH.js → AnimeInfoPage-B_UiacIN.js} +2 -2
- package/client/dist/assets/{Asmr-CCdEWrFi.js → Asmr-DMSN2vxl.js} +2 -2
- package/client/dist/assets/{AsmrResetProgressModal-DgEWBJOw.js → AsmrResetProgressModal-CRwfOatX.js} +1 -1
- package/client/dist/assets/{AvSyncCalibrator-CQ2fjf2E.js → AvSyncCalibrator-CRJsi-vz.js} +1 -1
- package/client/dist/assets/{Home-DUkUTTTy.js → Home-pwmK9_Eh.js} +1 -1
- package/client/dist/assets/{Insights-QCh4xW12.js → Insights-m7kFzg2A.js} +1 -1
- package/client/dist/assets/{LibraryListPage-BoTxRqHz.js → LibraryListPage-C1wpPvu6.js} +1 -1
- package/client/dist/assets/ListeningList-OzfEI1Ga.js +1 -0
- package/client/dist/assets/{Manga-HydR-L7o.js → Manga-ChRCExhK.js} +1 -1
- package/client/dist/assets/{MangaInfoPage-CBakEI9a.js → MangaInfoPage-DSHJPo2A.js} +1 -1
- package/client/dist/assets/{MangaReadPage-DwAR3iPy.js → MangaReadPage-CzOVY1wA.js} +1 -1
- package/client/dist/assets/{MangaResetProgressModal-DGzl8vsO.js → MangaResetProgressModal-CJfzdUBb.js} +1 -1
- package/client/dist/assets/{Mature-Pi0TxKuF.js → Mature-CifQMfhs.js} +1 -1
- package/client/dist/assets/{MediaCard--cuFxOCS.js → MediaCard-CM10yB9V.js} +1 -1
- package/client/dist/assets/{OptionListMenu-B3D39dGS.js → OptionListMenu-6Kovbsqw.js} +1 -1
- package/client/dist/assets/{Player-CsH4x_I_.js → Player-CK0hT-u4.js} +2 -2
- package/client/dist/assets/{PlayerSettings-DGxBsi3F.js → PlayerSettings-Bb66uqT9.js} +1 -1
- package/client/dist/assets/{QueueOptionsButton-Djq4Cpex.js → QueueOptionsButton-C_nTj8_q.js} +1 -1
- package/client/dist/assets/{QueueRail-DOrYgNVX.js → QueueRail-DUK7URAE.js} +1 -1
- package/client/dist/assets/{ReadingList-DwYoBrNk.js → ReadingList-Ck9LJThU.js} +1 -1
- package/client/dist/assets/{ResetProgressModal-CMF86mX6.js → ResetProgressModal-BUphIv0k.js} +1 -1
- package/client/dist/assets/{Search-DFkhuk-x.js → Search-zJyP15h6.js} +1 -1
- package/client/dist/assets/{Settings-DBB2r0VQ.js → Settings-BYdoSEUh.js} +1 -1
- package/client/dist/assets/{SkeletonGrid-DJelJgAv.js → SkeletonGrid-CqgbOhjX.js} +1 -1
- package/client/dist/assets/{Trackers-DGYXFhYZ.js → Trackers-BVjyv6CM.js} +1 -1
- package/client/dist/assets/{Tv-C5MDGD2Z.js → Tv-L13ylswR.js} +1 -1
- package/client/dist/assets/{TvInfo-DrKuK13u.js → TvInfo-Cvcm5U2D.js} +1 -1
- package/client/dist/assets/{TvPopup-DtHilRDT.js → TvPopup-DhxfKvYm.js} +1 -1
- package/client/dist/assets/{TvSearch-CzNxdu_w.js → TvSearch-P8bFE6_X.js} +1 -1
- package/client/dist/assets/{TvWatchlist-D7p4br6S.js → TvWatchlist-D5gVPjtb.js} +1 -1
- package/client/dist/assets/{Watchlist-DrOzswl2.js → Watchlist-Cjq8l3rU.js} +1 -1
- package/client/dist/assets/{hls-BGCX7mJy.js → hls-BkzuANAg.js} +1 -1
- package/client/dist/assets/{index-CnfZwsds.js → index-CLVgzKLk.js} +19 -19
- package/client/dist/assets/{useAnimeInfoData-DYXWBPF-.js → useAnimeInfoData-DZJZu-so.js} +1 -1
- package/client/dist/assets/{useAsmr-BbhQ59bk.js → useAsmr-CvrMGHuo.js} +1 -1
- package/client/dist/assets/useAsmrPopup-Bimy8Ouw.js +1 -0
- package/client/dist/assets/{useMangaLibrary-DuesWQaE.js → useMangaLibrary--MiN11uR.js} +1 -1
- package/client/dist/assets/{useMangaPopup-D57OVGUr.js → useMangaPopup-CR5XhKKS.js} +1 -1
- package/client/dist/assets/{useProviders-mpQb_dix.js → useProviders-DqdT4fA5.js} +1 -1
- package/client/dist/assets/{useTv-DGflCsSI.js → useTv-BL0HMDyP.js} +1 -1
- package/client/dist/assets/{useTvLibrary-BqeSmyc4.js → useTvLibrary-D4U4iqN8.js} +1 -1
- package/client/dist/index.html +1 -1
- package/package.json +3 -5
- package/server/dist/config.js +1 -174
- package/server/dist/server.js +1068 -289
- package/server/dist/shipped-defaults.js +1 -6
- package/server/package.json +4 -5
- package/client/dist/assets/Asmr.module-CdWlioTY.js +0 -1
- package/client/dist/assets/ListeningList-DguKjI_t.js +0 -1
- package/client/dist/assets/Music-BGH_0voi.css +0 -1
- package/client/dist/assets/Music-CQpyXTLH.js +0 -1
- package/client/dist/assets/Radio-CCHNFN7R.css +0 -1
- package/client/dist/assets/Radio-DF6g6odd.js +0 -1
- package/client/dist/assets/Radio.module-DGSZLpKM.js +0 -1
- package/client/dist/assets/useAsmrPopup-SymV1J-b.js +0 -1
- package/server/dist/app-auth.js +0 -157
- package/server/dist/app-hono.js +0 -598
- package/server/dist/db/drizzle.js +0 -16
- package/server/dist/db/schema-anime.js +0 -61
- package/server/dist/db/schema-asmr.js +0 -13
- package/server/dist/db/schema-manga.js +0 -31
- package/server/dist/db/schema-tv.js +0 -18
- package/server/dist/db.js +0 -217
- package/server/dist/discord-gateway.js +0 -385
- package/server/dist/discord-rpc.js +0 -413
- package/server/dist/github-sync.js +0 -508
- package/server/dist/google.js +0 -741
- package/server/dist/hono/asmr.js +0 -10
- package/server/dist/hono/auth.js +0 -49
- package/server/dist/hono/music.js +0 -209
- package/server/dist/hono/proxy.js +0 -827
- package/server/dist/hono/tv.js +0 -200
- package/server/dist/hono/watchlist.js +0 -341
- package/server/dist/lib/anilist.js +0 -1583
- package/server/dist/lib/anischedule.js +0 -191
- package/server/dist/lib/insights.js +0 -185
- package/server/dist/lib/ipc.js +0 -23
- package/server/dist/lib/kitsu.js +0 -517
- package/server/dist/lib/mal-import.js +0 -291
- package/server/dist/lib/mal.js +0 -957
- package/server/dist/lib/migration.js +0 -310
- package/server/dist/lib/offline-db.js +0 -395
- package/server/dist/lib/potoken.js +0 -135
- package/server/dist/lib/recommendations.service.js +0 -216
- package/server/dist/lib/temp-ids.js +0 -3
- package/server/dist/lib/tmdb.js +0 -72
- package/server/dist/lib/tracker/anilist-tracker.js +0 -308
- package/server/dist/lib/tracker/manga-sync.service.js +0 -773
- package/server/dist/lib/tracker/sync-state.js +0 -21
- package/server/dist/lib/tracker/sync.service.js +0 -476
- package/server/dist/lib/translate.js +0 -79
- package/server/dist/lib/ump.js +0 -106
- package/server/dist/lib/ytmusic.js +0 -162
- package/server/dist/logger.js +0 -16
- package/server/dist/providers/manga/manga.types.js +0 -1
- package/server/dist/providers/provider.interface.js +0 -1
- package/server/dist/providers/remote-loader.js +0 -614
- package/server/dist/providers/remote-types.js +0 -159
- package/server/dist/providers/title-matching.js +0 -226
- package/server/dist/providers/tv.types.js +0 -1
- package/server/dist/rclone.js +0 -120
- package/server/dist/repositories/asmr.repository.js +0 -77
- package/server/dist/repositories/insights.repository.js +0 -106
- package/server/dist/repositories/library.repository.js +0 -94
- package/server/dist/repositories/mal-cache.repository.js +0 -52
- package/server/dist/repositories/manga.repository.js +0 -95
- package/server/dist/repositories/notifications.repository.js +0 -60
- package/server/dist/repositories/queue.repository.js +0 -72
- package/server/dist/repositories/settings.repository.js +0 -30
- package/server/dist/repositories/shows-meta.repository.js +0 -58
- package/server/dist/repositories/temp-show-ids.repository.js +0 -63
- package/server/dist/repositories/tv.repository.js +0 -93
- package/server/dist/repositories/watched-episodes.repository.js +0 -82
- package/server/dist/repositories/watchlist.repository.js +0 -60
- package/server/dist/scripts/backfill-episode-duration.js +0 -42
- package/server/dist/sync-payload.js +0 -91
- package/server/dist/sync.js +0 -657
- package/server/dist/trpc/context.js +0 -7
- package/server/dist/trpc/errors.js +0 -15
- package/server/dist/trpc/index.js +0 -18
- package/server/dist/trpc/router.js +0 -49
- package/server/dist/trpc/routers/asmr-library.js +0 -112
- package/server/dist/trpc/routers/asmr-progress.js +0 -121
- package/server/dist/trpc/routers/asmr.js +0 -87
- package/server/dist/trpc/routers/auth.js +0 -194
- package/server/dist/trpc/routers/continue-watching.js +0 -81
- package/server/dist/trpc/routers/data.js +0 -1026
- package/server/dist/trpc/routers/discord.js +0 -285
- package/server/dist/trpc/routers/discovery.js +0 -11
- package/server/dist/trpc/routers/insights.js +0 -14
- package/server/dist/trpc/routers/manga-library.js +0 -263
- package/server/dist/trpc/routers/manga-progress.js +0 -169
- package/server/dist/trpc/routers/manga.js +0 -201
- package/server/dist/trpc/routers/music.js +0 -655
- package/server/dist/trpc/routers/notifications.js +0 -80
- package/server/dist/trpc/routers/progress.js +0 -109
- package/server/dist/trpc/routers/queue.js +0 -93
- package/server/dist/trpc/routers/radio.js +0 -145
- package/server/dist/trpc/routers/settings.js +0 -96
- package/server/dist/trpc/routers/tracker.js +0 -213
- package/server/dist/trpc/routers/tv-library.js +0 -135
- package/server/dist/trpc/routers/tv-progress.js +0 -159
- package/server/dist/trpc/routers/tv.js +0 -551
- package/server/dist/trpc/routers/watchlist.js +0 -550
- package/server/dist/trpc/validation.js +0 -258
- package/server/dist/utils/cache.utils.js +0 -25
- package/server/dist/utils/cookie.utils.js +0 -13
- package/server/dist/utils/db-utils.js +0 -17
- package/server/dist/utils/env.utils.js +0 -77
- package/server/dist/utils/http.utils.js +0 -82
- package/server/dist/utils/request-context.js +0 -2
- package/server/dist/utils/security.utils.js +0 -76
- /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
|
-
}
|
package/server/dist/lib/ipc.js
DELETED
|
@@ -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
|
-
}
|