@serifpersia/dango 3.3.9 → 3.4.1
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-URCiiy5W.js → AnimeCard-B0oMgXBm.js} +1 -1
- package/client/dist/assets/{AnimeInfo-e3z3er9t.js → AnimeInfo-o0Udf0og.js} +1 -1
- package/client/dist/assets/{AnimeInfoPage-B7Djlmy_.js → AnimeInfoPage-CojMIEuf.js} +2 -2
- package/client/dist/assets/Asmr-2V3yCCiK.js +2 -0
- package/client/dist/assets/AsmrResetProgressModal-C9R7HlNa.js +1 -0
- package/client/dist/assets/AvSyncCalibrator-Bo3820j3.js +10 -0
- package/client/dist/assets/Home-C8uXTzIM.js +1 -0
- package/client/dist/assets/Insights-CRqBfZuK.js +1 -0
- package/client/dist/assets/{LibraryListPage-B0qWqhb_.js → LibraryListPage-B_r51cRi.js} +1 -1
- package/client/dist/assets/{ListeningList-CSqD8j1C.js → ListeningList-BWnwvJTt.js} +1 -1
- package/client/dist/assets/{Manga-Cg2YuObF.js → Manga-CI5cZg3h.js} +1 -1
- package/client/dist/assets/{MangaInfoPage-DcArhEsy.js → MangaInfoPage-BtkOrO34.js} +1 -1
- package/client/dist/assets/MangaReadPage-Uv72AYdY.js +1 -0
- package/client/dist/assets/{MangaResetProgressModal-D_yT3d1v.js → MangaResetProgressModal-CPMEqZyi.js} +1 -1
- package/client/dist/assets/Mature-DXHWvoY5.js +1 -0
- package/client/dist/assets/MediaCard-DzA336CA.js +1 -0
- package/client/dist/assets/Music-C4dM0BQx.js +1 -0
- package/client/dist/assets/OptionListMenu-CpwxAC3J.js +1 -0
- package/client/dist/assets/Player-CpMvWTkH.js +56 -0
- package/client/dist/assets/PlayerSettings-pMU85k7p.js +1 -0
- package/client/dist/assets/QueueOptionsButton-1M7MdKzM.js +1 -0
- package/client/dist/assets/{QueueRail-B7f1-e6i.js → QueueRail-Bpla2xD8.js} +1 -1
- package/client/dist/assets/Radio-DlmTC-qG.js +1 -0
- package/client/dist/assets/{ReadingList-BaIem8H1.js → ReadingList-wJMDUybm.js} +1 -1
- package/client/dist/assets/{ResetProgressModal-DES5Y-Fl.js → ResetProgressModal-CByRM1Fh.js} +1 -1
- package/client/dist/assets/Search-B94D0tRM.js +1 -0
- package/client/dist/assets/Settings-CcU3oNY8.js +21 -0
- package/client/dist/assets/{SkeletonGrid-BxfgjP4i.js → SkeletonGrid-BJyZkg_K.js} +1 -1
- package/client/dist/assets/Trackers-vz-gqxtm.js +2 -0
- package/client/dist/assets/Tv-Rji_lvQo.js +4 -0
- package/client/dist/assets/Tv-ZEA45gXK.css +1 -0
- package/client/dist/assets/TvInfo-Ciujd6os.js +1 -0
- package/client/dist/assets/TvPopup-CqlVRF-Y.js +1 -0
- package/client/dist/assets/{TvSearch-BWDwWnHe.js → TvSearch-BcKXQue9.js} +1 -1
- package/client/dist/assets/{TvWatchlist-D3n5_4MI.js → TvWatchlist-sjsiup2h.js} +1 -1
- package/client/dist/assets/Watchlist-DAMdvt7Z.js +1 -0
- package/client/dist/assets/{hls-BO_L1WoN.js → hls-Do_n10_Y.js} +1 -1
- package/client/dist/assets/index-B5SLDCAi.js +178 -0
- package/client/dist/assets/useAnimeInfoData-DU9X90no.js +1 -0
- package/client/dist/assets/useAsmr-D6cSjbvt.js +1 -0
- package/client/dist/assets/{useAsmrPopup-D_o9-QlR.js → useAsmrPopup-DK1Vx-HV.js} +1 -1
- package/client/dist/assets/useMangaLibrary-DcxPn0RM.js +1 -0
- package/client/dist/assets/{useMangaPopup-TFzkC-tG.js → useMangaPopup-BLwvRZn0.js} +1 -1
- package/client/dist/assets/useProviders-D3L1Chqc.js +1 -0
- package/client/dist/assets/useTv-B546u-Rx.js +1 -0
- package/client/dist/assets/useTvLibrary-C2rPeCVT.js +1 -0
- package/client/dist/assets/{vendor-query-BXRECSI6.js → vendor-query-BUTye89G.js} +1 -1
- package/client/dist/index.html +2 -2
- package/orchestrator.js +1 -2
- package/package.json +6 -6
- package/server/dist/app-auth.js +9 -29
- package/server/dist/app-hono.js +621 -0
- package/server/dist/config.js +0 -1
- package/server/dist/db/drizzle.js +32 -0
- package/server/dist/db/schema-anime.js +64 -0
- package/server/dist/db/schema-asmr.js +25 -0
- package/server/dist/db/schema-manga.js +31 -0
- package/server/dist/db/schema-tv.js +33 -0
- package/server/dist/db.js +75 -4
- package/server/dist/discord-gateway.js +1 -1
- package/server/dist/google.js +0 -7
- package/server/dist/hono/asmr.js +1 -0
- package/server/dist/hono/auth.js +56 -0
- package/server/dist/hono/music.js +419 -0
- package/server/dist/{controllers/proxy.controller.js → hono/proxy.js} +390 -394
- package/server/dist/hono/tv.js +197 -0
- package/server/dist/hono/watchlist.js +733 -0
- package/server/dist/lib/anilist.js +3 -3
- package/server/dist/lib/insights.js +185 -0
- package/server/dist/lib/ipc.js +1 -2
- package/server/dist/lib/kitsu.js +1 -28
- package/server/dist/lib/mal-import.js +291 -0
- package/server/dist/lib/migration.js +20 -34
- package/server/dist/lib/offline-db.js +36 -36
- package/server/dist/lib/tracker/manga-sync.service.js +21 -21
- package/server/dist/lib/tracker/sync.service.js +20 -20
- package/server/dist/lib/translate.js +79 -0
- package/server/dist/lib/ytmusic.js +0 -2
- package/server/dist/providers/title-matching.js +0 -1
- package/server/dist/repositories/asmr.repository.js +90 -87
- package/server/dist/repositories/insights.repository.js +24 -13
- package/server/dist/repositories/library.repository.js +33 -93
- package/server/dist/repositories/manga.repository.js +108 -116
- package/server/dist/repositories/notifications.repository.js +50 -31
- package/server/dist/repositories/queue.repository.js +36 -25
- package/server/dist/repositories/settings.repository.js +18 -8
- package/server/dist/repositories/shows-meta.repository.js +54 -41
- package/server/dist/repositories/temp-show-ids.repository.js +43 -31
- package/server/dist/repositories/tv.repository.js +98 -115
- package/server/dist/repositories/watched-episodes.repository.js +52 -20
- package/server/dist/repositories/watchlist.repository.js +36 -48
- package/server/dist/server.js +59 -151
- package/server/dist/sync.js +41 -4
- package/server/dist/trpc/context.js +7 -0
- package/server/dist/trpc/errors.js +21 -0
- package/server/dist/trpc/index.js +19 -0
- package/server/dist/trpc/router.js +49 -0
- package/server/dist/trpc/routers/asmr-library.js +122 -0
- package/server/dist/trpc/routers/asmr-progress.js +133 -0
- package/server/dist/trpc/routers/asmr.js +87 -0
- package/server/dist/trpc/routers/auth.js +247 -0
- package/server/dist/trpc/routers/continue-watching.js +83 -0
- package/server/dist/trpc/routers/data.js +1039 -0
- package/server/dist/trpc/routers/discord.js +288 -0
- package/server/dist/trpc/routers/discovery.js +15 -0
- package/server/dist/trpc/routers/insights.js +45 -0
- package/server/dist/trpc/routers/manga-library.js +280 -0
- package/server/dist/trpc/routers/manga-progress.js +181 -0
- package/server/dist/trpc/routers/manga.js +201 -0
- package/server/dist/{controllers/music.controller.js → trpc/routers/music.js} +155 -288
- package/server/dist/trpc/routers/notifications.js +80 -0
- package/server/dist/trpc/routers/progress.js +15 -0
- package/server/dist/trpc/routers/queue.js +95 -0
- package/server/dist/{routes/radio.routes.js → trpc/routers/radio.js} +30 -23
- package/server/dist/trpc/routers/settings.js +105 -0
- package/server/dist/trpc/routers/tracker.js +219 -0
- package/server/dist/trpc/routers/tv-library.js +145 -0
- package/server/dist/trpc/routers/tv-progress.js +171 -0
- package/server/dist/trpc/routers/tv.js +565 -0
- package/server/dist/trpc/routers/watchlist.js +564 -0
- package/server/dist/trpc/validation.js +159 -0
- package/server/dist/utils/security.utils.js +0 -18
- package/server/package.json +5 -10
- package/client/dist/assets/Asmr-C28TfSvX.js +0 -2
- package/client/dist/assets/AsmrResetProgressModal-6Xh71AgR.js +0 -1
- package/client/dist/assets/AvSyncCalibrator-BwCtRW0P.js +0 -10
- package/client/dist/assets/Home-BJdbHYja.js +0 -1
- package/client/dist/assets/Insights-CjRy42YE.js +0 -1
- package/client/dist/assets/MangaReadPage-YCETZRte.js +0 -1
- package/client/dist/assets/Mature-BGxRWkV5.js +0 -1
- package/client/dist/assets/MediaCard-CAzoxsRS.js +0 -1
- package/client/dist/assets/Music-Cgo-yQ7E.js +0 -1
- package/client/dist/assets/OptionListMenu--Owe9Jle.js +0 -1
- package/client/dist/assets/Player-yRXG2BrP.js +0 -56
- package/client/dist/assets/PlayerSettings-DrZLa0CC.js +0 -1
- package/client/dist/assets/QueueOptionsButton-BVZyPyKf.js +0 -1
- package/client/dist/assets/Radio-BFjxlzuF.js +0 -1
- package/client/dist/assets/Search-BOBo0Ogh.js +0 -1
- package/client/dist/assets/Settings-DOvNQ0gX.js +0 -21
- package/client/dist/assets/Trackers-DZ9KqOWv.js +0 -2
- package/client/dist/assets/Tv-5mATEU4p.js +0 -4
- package/client/dist/assets/Tv-C2SWcVzN.css +0 -1
- package/client/dist/assets/TvInfo-CF-5C5Fs.js +0 -1
- package/client/dist/assets/TvPopup-Dfqifj3s.js +0 -1
- package/client/dist/assets/Watchlist-B8MHNsAU.js +0 -1
- package/client/dist/assets/index-D2RkUJIN.js +0 -178
- package/client/dist/assets/useAnimeInfoData-BWShDHqE.js +0 -1
- package/client/dist/assets/useAsmr-FyfD9G7b.js +0 -1
- package/client/dist/assets/useMangaLibrary-BJU5x8Vz.js +0 -1
- package/client/dist/assets/useProviders-DDRxa8vl.js +0 -1
- package/client/dist/assets/useTv-Bh6WmNnZ.js +0 -1
- package/client/dist/assets/useTvLibrary-_Pqm8re1.js +0 -1
- package/server/dist/controllers/asmr-library.controller.js +0 -288
- package/server/dist/controllers/auth.controller.js +0 -225
- package/server/dist/controllers/data.controller.js +0 -767
- package/server/dist/controllers/insights.controller.js +0 -203
- package/server/dist/controllers/manga-library.controller.js +0 -350
- package/server/dist/controllers/settings.controller.js +0 -609
- package/server/dist/controllers/tv-library.controller.js +0 -314
- package/server/dist/controllers/watchlist.controller.js +0 -1069
- package/server/dist/request-logger.js +0 -16
- package/server/dist/routes/asmr-library.routes.js +0 -23
- package/server/dist/routes/asmr.routes.js +0 -81
- package/server/dist/routes/auth.routes.js +0 -25
- package/server/dist/routes/data.routes.js +0 -47
- package/server/dist/routes/discord-gateway.routes.js +0 -51
- package/server/dist/routes/insights.routes.js +0 -35
- package/server/dist/routes/lan-auth.routes.js +0 -46
- package/server/dist/routes/manga-library.routes.js +0 -25
- package/server/dist/routes/manga.routes.js +0 -141
- package/server/dist/routes/music.routes.js +0 -20
- package/server/dist/routes/providers.routes.js +0 -17
- package/server/dist/routes/proxy.routes.js +0 -12
- package/server/dist/routes/settings.routes.js +0 -24
- package/server/dist/routes/tracker.routes.js +0 -287
- package/server/dist/routes/translate.routes.js +0 -88
- package/server/dist/routes/tv-library.routes.js +0 -23
- package/server/dist/routes/tv.routes.js +0 -629
- package/server/dist/routes/watchlist.routes.js +0 -341
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
import logger from '../logger.js';
|
|
2
|
-
import { InsightsRepository } from '../repositories/insights.repository.js';
|
|
3
|
-
import { computeDiscordSyncStats, getCachedRecommendations, } from '../lib/discord-roles-sync.service.js';
|
|
4
|
-
export class InsightsController {
|
|
5
|
-
getWatchInsights = async (req, res) => {
|
|
6
|
-
const db = req.db;
|
|
7
|
-
const [core, activityGrid, hourlyDist, seasonality, allWatches, watchedShows, droppedWarning, velocities,] = (await Promise.all([
|
|
8
|
-
InsightsRepository.getCoreStats(db),
|
|
9
|
-
InsightsRepository.getActivityGrid(db),
|
|
10
|
-
InsightsRepository.getHourlyDist(db),
|
|
11
|
-
InsightsRepository.getSeasonality(db),
|
|
12
|
-
InsightsRepository.getAllWatches(db),
|
|
13
|
-
InsightsRepository.getWatchedShowsMeta(db),
|
|
14
|
-
InsightsRepository.getDroppedShows(db),
|
|
15
|
-
InsightsRepository.getCompletionVelocities(db),
|
|
16
|
-
]));
|
|
17
|
-
const bingeFactor = activityGrid.length > 0 ? Math.max(...activityGrid.map((a) => a.count)) : 0;
|
|
18
|
-
const sessions = [];
|
|
19
|
-
if (allWatches.length > 0) {
|
|
20
|
-
let currentSessionSeconds = allWatches[0].effectiveSeconds ?? allWatches[0].currentTime;
|
|
21
|
-
for (let i = 1; i < allWatches.length; i++) {
|
|
22
|
-
const prev = new Date(allWatches[i - 1].watchedAt).getTime();
|
|
23
|
-
const curr = new Date(allWatches[i].watchedAt).getTime();
|
|
24
|
-
const gap = curr - prev;
|
|
25
|
-
if (Number.isFinite(gap) && gap > 0 && gap < 3600000) {
|
|
26
|
-
currentSessionSeconds += allWatches[i].effectiveSeconds ?? allWatches[i].currentTime;
|
|
27
|
-
}
|
|
28
|
-
else {
|
|
29
|
-
sessions.push(currentSessionSeconds);
|
|
30
|
-
currentSessionSeconds = allWatches[i].effectiveSeconds ?? allWatches[i].currentTime;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
sessions.push(currentSessionSeconds);
|
|
34
|
-
}
|
|
35
|
-
const avgSessionMinutes = sessions.length > 0
|
|
36
|
-
? Math.round(sessions.reduce((a, b) => a + b, 0) / sessions.length / 60)
|
|
37
|
-
: 0;
|
|
38
|
-
const genreCounts = {};
|
|
39
|
-
let totalPopScore = 0;
|
|
40
|
-
let popCount = 0;
|
|
41
|
-
for (const show of watchedShows) {
|
|
42
|
-
let genres = [];
|
|
43
|
-
if (show.genres) {
|
|
44
|
-
try {
|
|
45
|
-
if (show.genres.startsWith('[')) {
|
|
46
|
-
genres = JSON.parse(show.genres);
|
|
47
|
-
}
|
|
48
|
-
else {
|
|
49
|
-
genres = show.genres.split(',').map((g) => g.trim());
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
catch (e) {
|
|
53
|
-
logger.warn({ err: e, showId: show.id }, 'Failed to parse genres for insights');
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
for (const g of genres) {
|
|
57
|
-
genreCounts[g] = (genreCounts[g] || 0) + 1;
|
|
58
|
-
}
|
|
59
|
-
if (show.popularityScore) {
|
|
60
|
-
let score = show.popularityScore;
|
|
61
|
-
if (score > 10)
|
|
62
|
-
score = score / 10;
|
|
63
|
-
totalPopScore += score;
|
|
64
|
-
popCount++;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
const topGenre = Object.entries(genreCounts).sort((a, b) => b[1] - a[1])[0]?.[0];
|
|
68
|
-
const personaMap = {
|
|
69
|
-
Action: 'Shonen Warrior',
|
|
70
|
-
Romance: 'Hopeless Romantic',
|
|
71
|
-
Comedy: 'Chaos Enjoyer',
|
|
72
|
-
'Slice of Life': 'Vibe Seeker',
|
|
73
|
-
Horror: 'Fearless Watcher',
|
|
74
|
-
Fantasy: 'Isekai Traveller',
|
|
75
|
-
'Sci-Fi': 'Future Scientist',
|
|
76
|
-
Drama: 'Feels Collector',
|
|
77
|
-
};
|
|
78
|
-
const persona = personaMap[topGenre || ''] || 'Anime Enthusiast';
|
|
79
|
-
const avgCompletionDays = velocities.length > 0
|
|
80
|
-
? Math.round(velocities.reduce((a, b) => a + b.daysToFinish, 0) / velocities.length)
|
|
81
|
-
: 0;
|
|
82
|
-
res.json({
|
|
83
|
-
totalHours: Math.round((core?.totalSeconds || 0) / 3600),
|
|
84
|
-
totalEpisodes: core?.totalEpisodes || 0,
|
|
85
|
-
totalAnime: core?.totalAnime || 0,
|
|
86
|
-
completedAnime: core?.completedCount || 0,
|
|
87
|
-
completionRate: core?.totalWatchlist > 0
|
|
88
|
-
? Math.round((core.completedCount / core.totalWatchlist) * 100)
|
|
89
|
-
: 0,
|
|
90
|
-
persona,
|
|
91
|
-
bingeFactor,
|
|
92
|
-
avgSessionMinutes,
|
|
93
|
-
avgCompletionDays,
|
|
94
|
-
popularityScore: popCount > 0 ? Math.round(totalPopScore / popCount) : 0,
|
|
95
|
-
genreSplit: Object.entries(genreCounts)
|
|
96
|
-
.map(([name, count]) => ({ name, count }))
|
|
97
|
-
.sort((a, b) => b.count - a.count)
|
|
98
|
-
.slice(0, 8) || [],
|
|
99
|
-
activityGrid: activityGrid || [],
|
|
100
|
-
hourlyDist: Array.from({ length: 24 }, (_, i) => {
|
|
101
|
-
const hour = i.toString().padStart(2, '0');
|
|
102
|
-
return { hour, count: hourlyDist?.find((d) => d.hour === hour)?.count || 0 };
|
|
103
|
-
}) || [],
|
|
104
|
-
seasonality: Array.from({ length: 12 }, (_, i) => {
|
|
105
|
-
const month = (i + 1).toString().padStart(2, '0');
|
|
106
|
-
return {
|
|
107
|
-
month,
|
|
108
|
-
seconds: seasonality?.find((s) => s.month === month)?.seconds || 0,
|
|
109
|
-
};
|
|
110
|
-
}) || [],
|
|
111
|
-
droppedShows: (droppedWarning || []).slice(0, 5),
|
|
112
|
-
});
|
|
113
|
-
};
|
|
114
|
-
formatTime(seconds) {
|
|
115
|
-
const days = Math.floor(seconds / 86400);
|
|
116
|
-
const hours = Math.floor((seconds % 86400) / 3600);
|
|
117
|
-
const minutes = Math.floor((seconds % 3600) / 60);
|
|
118
|
-
if (days > 0) {
|
|
119
|
-
return `${days} days ${hours > 0 ? `${hours} hours` : ''}`;
|
|
120
|
-
}
|
|
121
|
-
if (hours > 0) {
|
|
122
|
-
return `${hours} hours${minutes > 0 ? ` ${minutes} mins` : ''}`;
|
|
123
|
-
}
|
|
124
|
-
return `${minutes} mins`;
|
|
125
|
-
}
|
|
126
|
-
getGenreCards = async (req, res) => {
|
|
127
|
-
const db = req.db;
|
|
128
|
-
const rows = await InsightsRepository.getWatchedEpisodesWithMeta(db);
|
|
129
|
-
const genreData = {};
|
|
130
|
-
for (const row of rows) {
|
|
131
|
-
let genres = [];
|
|
132
|
-
if (row.genres) {
|
|
133
|
-
try {
|
|
134
|
-
if (row.genres.startsWith('[')) {
|
|
135
|
-
genres = JSON.parse(row.genres);
|
|
136
|
-
}
|
|
137
|
-
else {
|
|
138
|
-
genres = row.genres.split(',').map((g) => g.trim());
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
catch (e) {
|
|
142
|
-
logger.warn({ err: e, showId: row.showId }, 'Failed to parse genres for genre cards');
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
const timeWatched = (row.effectiveSeconds ?? row.currentTime) || 0;
|
|
146
|
-
let score = row.popularityScore || 0;
|
|
147
|
-
if (score > 10)
|
|
148
|
-
score = score / 10;
|
|
149
|
-
for (const genre of genres) {
|
|
150
|
-
if (!genreData[genre]) {
|
|
151
|
-
genreData[genre] = { count: 0, totalTime: 0, scores: [], showWatches: {}, showMeta: {} };
|
|
152
|
-
}
|
|
153
|
-
genreData[genre].count++;
|
|
154
|
-
genreData[genre].totalTime += timeWatched;
|
|
155
|
-
if (score > 0)
|
|
156
|
-
genreData[genre].scores.push(score);
|
|
157
|
-
genreData[genre].showWatches[row.showId] =
|
|
158
|
-
(genreData[genre].showWatches[row.showId] || 0) + 1;
|
|
159
|
-
if (row.name || row.thumbnail) {
|
|
160
|
-
genreData[genre].showMeta[row.showId] = {
|
|
161
|
-
name: row.name,
|
|
162
|
-
nativeName: row.nativeName,
|
|
163
|
-
englishName: row.englishName,
|
|
164
|
-
thumbnail: row.thumbnail,
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
const genreCards = Object.entries(genreData).map(([name, data]) => {
|
|
170
|
-
const topShows = Object.entries(data.showWatches)
|
|
171
|
-
.sort((a, b) => b[1] - a[1])
|
|
172
|
-
.slice(0, 4)
|
|
173
|
-
.map(([showId]) => ({
|
|
174
|
-
id: showId,
|
|
175
|
-
name: data.showMeta[showId]?.name || '',
|
|
176
|
-
nativeName: data.showMeta[showId]?.nativeName,
|
|
177
|
-
englishName: data.showMeta[showId]?.englishName,
|
|
178
|
-
thumbnail: data.showMeta[showId]?.thumbnail || '',
|
|
179
|
-
}));
|
|
180
|
-
return {
|
|
181
|
-
rank: 0,
|
|
182
|
-
name,
|
|
183
|
-
count: data.count,
|
|
184
|
-
meanScore: data.scores.length > 0 ? data.scores.reduce((a, b) => a + b, 0) / data.scores.length : 0,
|
|
185
|
-
timeWatched: this.formatTime(data.totalTime),
|
|
186
|
-
topShows,
|
|
187
|
-
};
|
|
188
|
-
});
|
|
189
|
-
genreCards.sort((a, b) => b.count - a.count);
|
|
190
|
-
genreCards.forEach((card, i) => {
|
|
191
|
-
card.rank = i + 1;
|
|
192
|
-
});
|
|
193
|
-
res.json(genreCards);
|
|
194
|
-
};
|
|
195
|
-
getDiscordSyncStats = async (req, res) => {
|
|
196
|
-
const db = req.db;
|
|
197
|
-
res.json(await computeDiscordSyncStats(db));
|
|
198
|
-
};
|
|
199
|
-
getRecommendations = async (req, res) => {
|
|
200
|
-
const db = req.db;
|
|
201
|
-
res.json({ candidates: await getCachedRecommendations(db) });
|
|
202
|
-
};
|
|
203
|
-
}
|
|
@@ -1,350 +0,0 @@
|
|
|
1
|
-
import logger from '../logger.js';
|
|
2
|
-
import { performMangaWriteTransaction } from '../sync.js';
|
|
3
|
-
import { dbAll } from '../utils/db-utils.js';
|
|
4
|
-
import { SettingsRepository } from '../repositories/settings.repository.js';
|
|
5
|
-
import { buildMangaId, MANGA_STATUSES, MangaLibraryRepository, MangaProgressRepository, } from '../repositories/manga.repository.js';
|
|
6
|
-
const MANGA_ADULT_RATINGS = ['erotica', 'pornographic'];
|
|
7
|
-
const isMangaContinueAdult = (row) => !!row.contentRating && MANGA_ADULT_RATINGS.includes(row.contentRating);
|
|
8
|
-
function mangaDb(req) {
|
|
9
|
-
const db = req.mangaDb;
|
|
10
|
-
if (!db || db.isClosedCheck())
|
|
11
|
-
throw new Error('Manga database is not ready');
|
|
12
|
-
return db;
|
|
13
|
-
}
|
|
14
|
-
function normalizeId(provider, mangaId, idRaw) {
|
|
15
|
-
if (idRaw && idRaw.includes(':'))
|
|
16
|
-
return idRaw;
|
|
17
|
-
return buildMangaId(provider, mangaId || idRaw || '');
|
|
18
|
-
}
|
|
19
|
-
export class MangaLibraryController {
|
|
20
|
-
getLibrary = async (req, res) => {
|
|
21
|
-
try {
|
|
22
|
-
const { status, page: pageStr, limit: limitStr } = req.query;
|
|
23
|
-
const page = Math.max(parseInt(pageStr) || 1, 1);
|
|
24
|
-
const limit = Math.min(Math.max(parseInt(limitStr) || 24, 1), 100);
|
|
25
|
-
const offset = (page - 1) * limit;
|
|
26
|
-
const db = mangaDb(req);
|
|
27
|
-
const rows = MangaLibraryRepository.getAll(db, status, limit, offset);
|
|
28
|
-
const total = MangaLibraryRepository.getCount(db, status);
|
|
29
|
-
res.json({ data: rows, total, page, limit });
|
|
30
|
-
}
|
|
31
|
-
catch (err) {
|
|
32
|
-
logger.error({ err }, 'Failed to get manga library');
|
|
33
|
-
res.status(500).json({ error: 'Failed to load manga library' });
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
checkLibrary = async (req, res) => {
|
|
37
|
-
try {
|
|
38
|
-
const item = MangaLibraryRepository.getById(mangaDb(req), req.params.id);
|
|
39
|
-
res.json({ inLibrary: !!item, status: item?.status ?? null });
|
|
40
|
-
}
|
|
41
|
-
catch {
|
|
42
|
-
res.json({ inLibrary: false, status: null });
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
|
-
getEntry = async (req, res) => {
|
|
46
|
-
try {
|
|
47
|
-
const item = MangaLibraryRepository.getById(mangaDb(req), req.params.id);
|
|
48
|
-
if (!item)
|
|
49
|
-
return res.status(404).json({ error: 'Not in library' });
|
|
50
|
-
res.json({ item });
|
|
51
|
-
}
|
|
52
|
-
catch {
|
|
53
|
-
res.status(500).json({ error: 'Failed to load entry' });
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
|
-
linkAnilist = async (req, res) => {
|
|
57
|
-
const { id, anilistId } = req.body ?? {};
|
|
58
|
-
const targetId = typeof id === 'string' ? id : '';
|
|
59
|
-
const mediaId = Number(anilistId);
|
|
60
|
-
if (!targetId || !Number.isInteger(mediaId) || mediaId <= 0) {
|
|
61
|
-
return res.status(400).json({ error: 'id and a numeric anilistId are required' });
|
|
62
|
-
}
|
|
63
|
-
try {
|
|
64
|
-
const db = mangaDb(req);
|
|
65
|
-
const row = MangaLibraryRepository.getById(db, targetId);
|
|
66
|
-
if (!row)
|
|
67
|
-
return res.status(404).json({ error: 'Library entry not found' });
|
|
68
|
-
if (String(row.provider).toLowerCase() === 'anilist') {
|
|
69
|
-
return res
|
|
70
|
-
.status(400)
|
|
71
|
-
.json({ error: 'Link a provider bookmark to the AniList entry, not the reverse' });
|
|
72
|
-
}
|
|
73
|
-
let progressMigrated = false;
|
|
74
|
-
await performMangaWriteTransaction(db, (tx) => {
|
|
75
|
-
MangaLibraryRepository.setAnilistId(tx, targetId, mediaId, 'manual');
|
|
76
|
-
const orphanId = `anilist:${mediaId}`;
|
|
77
|
-
if (orphanId !== targetId) {
|
|
78
|
-
const orphan = MangaLibraryRepository.getById(tx, orphanId);
|
|
79
|
-
const target = MangaLibraryRepository.getById(tx, targetId);
|
|
80
|
-
const targetProgress = MangaProgressRepository.getByManga(tx, targetId);
|
|
81
|
-
if (orphan?.lastChapterNumber &&
|
|
82
|
-
!target?.lastChapterNumber &&
|
|
83
|
-
targetProgress.length === 0) {
|
|
84
|
-
MangaLibraryRepository.setProgressPointer(tx, targetId, orphan.lastChapterNumber);
|
|
85
|
-
MangaProgressRepository.moveSyntheticChapters(tx, orphanId, targetId);
|
|
86
|
-
progressMigrated = true;
|
|
87
|
-
}
|
|
88
|
-
MangaLibraryRepository.delete(tx, orphanId);
|
|
89
|
-
MangaProgressRepository.deleteByManga(tx, orphanId);
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
res.json({ success: true, id: targetId, progressMigrated });
|
|
93
|
-
}
|
|
94
|
-
catch (err) {
|
|
95
|
-
logger.error({ err }, 'Failed to link manga to AniList');
|
|
96
|
-
res.status(500).json({ error: 'Failed to link entry' });
|
|
97
|
-
}
|
|
98
|
-
};
|
|
99
|
-
getLibraryIds = async (req, res) => {
|
|
100
|
-
try {
|
|
101
|
-
res.json({ ids: MangaLibraryRepository.getIds(mangaDb(req)) });
|
|
102
|
-
}
|
|
103
|
-
catch {
|
|
104
|
-
res.json({ ids: [] });
|
|
105
|
-
}
|
|
106
|
-
};
|
|
107
|
-
addToLibrary = async (req, res) => {
|
|
108
|
-
const { provider, mangaId, id: idRaw, title, cover, status, author, altTitle, contentRating, } = req.body ?? {};
|
|
109
|
-
if (!provider || (!mangaId && !idRaw)) {
|
|
110
|
-
return res.status(400).json({ error: 'provider and mangaId are required' });
|
|
111
|
-
}
|
|
112
|
-
const id = normalizeId(String(provider), String(mangaId || ''), idRaw ? String(idRaw) : undefined);
|
|
113
|
-
const cleanStatus = typeof status === 'string' && MANGA_STATUSES.includes(status)
|
|
114
|
-
? status
|
|
115
|
-
: 'Reading';
|
|
116
|
-
try {
|
|
117
|
-
await performMangaWriteTransaction(mangaDb(req), (tx) => {
|
|
118
|
-
MangaLibraryRepository.upsert(tx, {
|
|
119
|
-
id,
|
|
120
|
-
provider: String(provider).toLowerCase(),
|
|
121
|
-
mangaId: String(mangaId || idRaw || ''),
|
|
122
|
-
title: String(title || ''),
|
|
123
|
-
cover: String(cover || ''),
|
|
124
|
-
status: cleanStatus,
|
|
125
|
-
author: author ? String(author) : undefined,
|
|
126
|
-
altTitle: altTitle ? String(altTitle) : undefined,
|
|
127
|
-
contentRating: contentRating ? String(contentRating) : undefined,
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
res.json({ success: true, id });
|
|
131
|
-
}
|
|
132
|
-
catch (err) {
|
|
133
|
-
logger.error({ err }, 'Failed to add manga to library');
|
|
134
|
-
res.status(500).json({ error: 'Failed to bookmark manga' });
|
|
135
|
-
}
|
|
136
|
-
};
|
|
137
|
-
removeFromLibrary = async (req, res) => {
|
|
138
|
-
const { id } = req.body ?? {};
|
|
139
|
-
if (!id)
|
|
140
|
-
return res.status(400).json({ error: 'id is required' });
|
|
141
|
-
try {
|
|
142
|
-
await performMangaWriteTransaction(mangaDb(req), (tx) => {
|
|
143
|
-
MangaLibraryRepository.delete(tx, String(id));
|
|
144
|
-
MangaProgressRepository.deleteByManga(tx, String(id));
|
|
145
|
-
});
|
|
146
|
-
res.json({ success: true });
|
|
147
|
-
}
|
|
148
|
-
catch (err) {
|
|
149
|
-
logger.error({ err }, 'Failed to remove manga from library');
|
|
150
|
-
res.status(500).json({ error: 'Failed to remove bookmark' });
|
|
151
|
-
}
|
|
152
|
-
};
|
|
153
|
-
updateStatus = async (req, res) => {
|
|
154
|
-
const { id, status } = req.body ?? {};
|
|
155
|
-
if (!id || !MANGA_STATUSES.includes(status)) {
|
|
156
|
-
return res.status(400).json({ error: 'id and a valid status are required' });
|
|
157
|
-
}
|
|
158
|
-
try {
|
|
159
|
-
await performMangaWriteTransaction(mangaDb(req), (tx) => {
|
|
160
|
-
MangaLibraryRepository.updateStatus(tx, String(id), String(status));
|
|
161
|
-
});
|
|
162
|
-
res.json({ success: true });
|
|
163
|
-
}
|
|
164
|
-
catch (err) {
|
|
165
|
-
logger.error({ err }, 'Failed to update manga status');
|
|
166
|
-
res.status(500).json({ error: 'Failed to update status' });
|
|
167
|
-
}
|
|
168
|
-
};
|
|
169
|
-
getProgress = async (req, res) => {
|
|
170
|
-
try {
|
|
171
|
-
const rows = MangaProgressRepository.getByManga(mangaDb(req), req.params.mangaId);
|
|
172
|
-
res.json({ progress: rows });
|
|
173
|
-
}
|
|
174
|
-
catch {
|
|
175
|
-
res.json({ progress: [] });
|
|
176
|
-
}
|
|
177
|
-
};
|
|
178
|
-
getLatestProgress = async (req, res) => {
|
|
179
|
-
try {
|
|
180
|
-
const row = MangaProgressRepository.getLatest(mangaDb(req), req.params.mangaId);
|
|
181
|
-
res.json(row || { page: 0, pageCount: 0 });
|
|
182
|
-
}
|
|
183
|
-
catch {
|
|
184
|
-
res.json({ page: 0, pageCount: 0 });
|
|
185
|
-
}
|
|
186
|
-
};
|
|
187
|
-
saveProgress = async (req, res) => {
|
|
188
|
-
const { mangaId, chapterId, chapterNumber, page, pageCount, title, cover, provider, altTitle, contentRating, } = req.body ?? {};
|
|
189
|
-
if (!mangaId || !chapterId) {
|
|
190
|
-
return res.status(400).json({ error: 'mangaId and chapterId are required' });
|
|
191
|
-
}
|
|
192
|
-
const pageNum = Math.max(Number(page) || 0, 0);
|
|
193
|
-
const pageCountNum = Math.max(Number(pageCount) || 0, 0);
|
|
194
|
-
try {
|
|
195
|
-
await performMangaWriteTransaction(mangaDb(req), (tx) => {
|
|
196
|
-
MangaProgressRepository.upsert(tx, {
|
|
197
|
-
mangaId: String(mangaId),
|
|
198
|
-
chapterId: String(chapterId),
|
|
199
|
-
chapterNumber: String(chapterNumber || ''),
|
|
200
|
-
page: pageNum,
|
|
201
|
-
pageCount: pageCountNum,
|
|
202
|
-
title: title ?? null,
|
|
203
|
-
cover: cover ?? null,
|
|
204
|
-
provider: provider ?? null,
|
|
205
|
-
altTitle: altTitle ?? null,
|
|
206
|
-
contentRating: contentRating ?? null,
|
|
207
|
-
});
|
|
208
|
-
MangaLibraryRepository.touchProgress(tx, String(mangaId), {
|
|
209
|
-
chapterId: String(chapterId),
|
|
210
|
-
chapterNumber: String(chapterNumber || ''),
|
|
211
|
-
page: pageNum,
|
|
212
|
-
});
|
|
213
|
-
});
|
|
214
|
-
res.json({ success: true });
|
|
215
|
-
}
|
|
216
|
-
catch (err) {
|
|
217
|
-
logger.error({ err }, 'Failed to save manga progress');
|
|
218
|
-
res.status(500).json({ error: 'Failed to save progress' });
|
|
219
|
-
}
|
|
220
|
-
};
|
|
221
|
-
getContinueReading = async (req, res) => {
|
|
222
|
-
try {
|
|
223
|
-
const limit = Math.min(Math.max(parseInt(req.query.limit) || 24, 1), 100);
|
|
224
|
-
const ignoreAdultRow = await SettingsRepository.getByKey(req.db, 'mangaIgnoreAdultContent');
|
|
225
|
-
const ignoreAdult = ignoreAdultRow ? ignoreAdultRow.value !== 'false' : true;
|
|
226
|
-
const listOnlyRow = await SettingsRepository.getByKey(req.db, 'mangaCwWatchlistOnly');
|
|
227
|
-
const listOnly = listOnlyRow
|
|
228
|
-
? listOnlyRow.value === 'true' || listOnlyRow.value === '1'
|
|
229
|
-
: false;
|
|
230
|
-
const rows = MangaProgressRepository.getContinueReading(mangaDb(req), limit).filter((row) => {
|
|
231
|
-
if (ignoreAdult && isMangaContinueAdult(row))
|
|
232
|
-
return false;
|
|
233
|
-
if (listOnly && row.watchlistStatus !== 'Reading')
|
|
234
|
-
return false;
|
|
235
|
-
return true;
|
|
236
|
-
});
|
|
237
|
-
res.json({ data: rows, total: rows.length });
|
|
238
|
-
}
|
|
239
|
-
catch {
|
|
240
|
-
res.json({ data: [], total: 0 });
|
|
241
|
-
}
|
|
242
|
-
};
|
|
243
|
-
async getAdultNonListMangaIds(db) {
|
|
244
|
-
const rows = await dbAll(db, `SELECT DISTINCT p.mangaId as mangaId
|
|
245
|
-
FROM manga_progress p
|
|
246
|
-
LEFT JOIN manga_library l ON l.id = p.mangaId
|
|
247
|
-
WHERE COALESCE(l.contentRating, p.contentRating) IN ('erotica', 'pornographic')
|
|
248
|
-
AND l.id IS NULL`);
|
|
249
|
-
return rows.map((r) => r.mangaId);
|
|
250
|
-
}
|
|
251
|
-
getAdultContinueReadingCount = async (req, res) => {
|
|
252
|
-
try {
|
|
253
|
-
const ids = await this.getAdultNonListMangaIds(mangaDb(req));
|
|
254
|
-
res.json({ count: ids.length });
|
|
255
|
-
}
|
|
256
|
-
catch {
|
|
257
|
-
res.json({ count: 0 });
|
|
258
|
-
}
|
|
259
|
-
};
|
|
260
|
-
purgeAdultContinueReading = async (req, res) => {
|
|
261
|
-
try {
|
|
262
|
-
const ids = await this.getAdultNonListMangaIds(mangaDb(req));
|
|
263
|
-
if (ids.length > 0) {
|
|
264
|
-
await performMangaWriteTransaction(mangaDb(req), (tx) => {
|
|
265
|
-
MangaProgressRepository.deleteMany(tx, ids);
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
res.json({ success: true, removed: ids.length });
|
|
269
|
-
}
|
|
270
|
-
catch (err) {
|
|
271
|
-
logger.error({ err }, 'Failed to purge adult manga progress');
|
|
272
|
-
res.status(500).json({ error: 'Failed to purge adult entries' });
|
|
273
|
-
}
|
|
274
|
-
};
|
|
275
|
-
removeProgress = async (req, res) => {
|
|
276
|
-
const { mangaId, chapterId } = req.body ?? {};
|
|
277
|
-
if (!mangaId)
|
|
278
|
-
return res.status(400).json({ error: 'mangaId is required' });
|
|
279
|
-
try {
|
|
280
|
-
await performMangaWriteTransaction(mangaDb(req), (tx) => {
|
|
281
|
-
if (chapterId) {
|
|
282
|
-
MangaProgressRepository.deleteChapter(tx, String(mangaId), String(chapterId));
|
|
283
|
-
}
|
|
284
|
-
else {
|
|
285
|
-
MangaProgressRepository.deleteByManga(tx, String(mangaId));
|
|
286
|
-
}
|
|
287
|
-
});
|
|
288
|
-
res.json({ success: true });
|
|
289
|
-
}
|
|
290
|
-
catch (err) {
|
|
291
|
-
logger.error({ err }, 'Failed to remove manga progress');
|
|
292
|
-
res.status(500).json({ error: 'Failed to reset progress' });
|
|
293
|
-
}
|
|
294
|
-
};
|
|
295
|
-
batchUpdateStatus = async (req, res) => {
|
|
296
|
-
const { ids: idsRaw, status } = req.body ?? {};
|
|
297
|
-
if (!Array.isArray(idsRaw) || idsRaw.length === 0) {
|
|
298
|
-
return res.status(400).json({ error: 'ids must be a non-empty array' });
|
|
299
|
-
}
|
|
300
|
-
if (!status || !MANGA_STATUSES.includes(status)) {
|
|
301
|
-
return res.status(400).json({ error: 'a valid status is required' });
|
|
302
|
-
}
|
|
303
|
-
const ids = idsRaw.map((id) => String(id));
|
|
304
|
-
try {
|
|
305
|
-
await performMangaWriteTransaction(mangaDb(req), (tx) => {
|
|
306
|
-
MangaLibraryRepository.updateStatusMany(tx, ids, String(status));
|
|
307
|
-
});
|
|
308
|
-
res.json({ success: true, updated: ids.length });
|
|
309
|
-
}
|
|
310
|
-
catch (err) {
|
|
311
|
-
logger.error({ err }, 'Failed to batch update manga status');
|
|
312
|
-
res.status(500).json({ error: 'Failed to update statuses' });
|
|
313
|
-
}
|
|
314
|
-
};
|
|
315
|
-
batchRemove = async (req, res) => {
|
|
316
|
-
const { ids: idsRaw } = req.body ?? {};
|
|
317
|
-
if (!Array.isArray(idsRaw) || idsRaw.length === 0) {
|
|
318
|
-
return res.status(400).json({ error: 'ids must be a non-empty array' });
|
|
319
|
-
}
|
|
320
|
-
const ids = idsRaw.map((id) => String(id));
|
|
321
|
-
try {
|
|
322
|
-
await performMangaWriteTransaction(mangaDb(req), (tx) => {
|
|
323
|
-
MangaLibraryRepository.deleteMany(tx, ids);
|
|
324
|
-
MangaProgressRepository.deleteMany(tx, ids);
|
|
325
|
-
});
|
|
326
|
-
res.json({ success: true, removed: ids.length });
|
|
327
|
-
}
|
|
328
|
-
catch (err) {
|
|
329
|
-
logger.error({ err }, 'Failed to batch remove manga');
|
|
330
|
-
res.status(500).json({ error: 'Failed to remove bookmarks' });
|
|
331
|
-
}
|
|
332
|
-
};
|
|
333
|
-
batchRemoveProgress = async (req, res) => {
|
|
334
|
-
const { ids: idsRaw } = req.body ?? {};
|
|
335
|
-
if (!Array.isArray(idsRaw) || idsRaw.length === 0) {
|
|
336
|
-
return res.status(400).json({ error: 'ids must be a non-empty array' });
|
|
337
|
-
}
|
|
338
|
-
const ids = idsRaw.map((id) => String(id));
|
|
339
|
-
try {
|
|
340
|
-
await performMangaWriteTransaction(mangaDb(req), (tx) => {
|
|
341
|
-
MangaProgressRepository.deleteMany(tx, ids);
|
|
342
|
-
});
|
|
343
|
-
res.json({ success: true, removed: ids.length });
|
|
344
|
-
}
|
|
345
|
-
catch (err) {
|
|
346
|
-
logger.error({ err }, 'Failed to batch remove manga progress');
|
|
347
|
-
res.status(500).json({ error: 'Failed to reset progress' });
|
|
348
|
-
}
|
|
349
|
-
};
|
|
350
|
-
}
|