@serifpersia/dango 3.3.9 → 3.4.0
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-CJy1SXH3.js} +1 -1
- package/client/dist/assets/{AnimeInfo-e3z3er9t.js → AnimeInfo-BoXlVvZ-.js} +1 -1
- package/client/dist/assets/{AnimeInfoPage-B7Djlmy_.js → AnimeInfoPage-BxbA_7AV.js} +2 -2
- package/client/dist/assets/Asmr--Zc8G9zv.js +2 -0
- package/client/dist/assets/AsmrResetProgressModal-CKYm-8cp.js +1 -0
- package/client/dist/assets/AvSyncCalibrator-3yjZS7s6.js +10 -0
- package/client/dist/assets/Home-rrhOGQcg.js +1 -0
- package/client/dist/assets/Insights-Bay90Yeg.js +1 -0
- package/client/dist/assets/{LibraryListPage-B0qWqhb_.js → LibraryListPage-HzuDavbb.js} +1 -1
- package/client/dist/assets/{ListeningList-CSqD8j1C.js → ListeningList-DYcQnnsG.js} +1 -1
- package/client/dist/assets/{Manga-Cg2YuObF.js → Manga-D8CT9aO0.js} +1 -1
- package/client/dist/assets/{MangaInfoPage-DcArhEsy.js → MangaInfoPage-DGK6CYp1.js} +1 -1
- package/client/dist/assets/MangaReadPage-B9XoDKF9.js +1 -0
- package/client/dist/assets/{MangaResetProgressModal-D_yT3d1v.js → MangaResetProgressModal-QjZyIAjl.js} +1 -1
- package/client/dist/assets/Mature-CHt_-Hz5.js +1 -0
- package/client/dist/assets/MediaCard-B5584Cq9.js +1 -0
- package/client/dist/assets/Music-HeLrEZ2d.js +1 -0
- package/client/dist/assets/OptionListMenu-CJ90CdwI.js +1 -0
- package/client/dist/assets/Player-BYtEax_z.js +56 -0
- package/client/dist/assets/PlayerSettings-4-cUAY4B.js +1 -0
- package/client/dist/assets/QueueOptionsButton-Ba0dg_82.js +1 -0
- package/client/dist/assets/{QueueRail-B7f1-e6i.js → QueueRail-O7f4RqjB.js} +1 -1
- package/client/dist/assets/Radio-DPZxQnyD.js +1 -0
- package/client/dist/assets/{ReadingList-BaIem8H1.js → ReadingList-n6rr8ygy.js} +1 -1
- package/client/dist/assets/{ResetProgressModal-DES5Y-Fl.js → ResetProgressModal-CcStlbhP.js} +1 -1
- package/client/dist/assets/Search-C2wrWOdy.js +1 -0
- package/client/dist/assets/Settings-CluUYs8U.js +21 -0
- package/client/dist/assets/{SkeletonGrid-BxfgjP4i.js → SkeletonGrid-CLEPY9nP.js} +1 -1
- package/client/dist/assets/Trackers-CTGPxs0V.js +2 -0
- package/client/dist/assets/Tv-BwHlDdi8.js +4 -0
- package/client/dist/assets/Tv-ZEA45gXK.css +1 -0
- package/client/dist/assets/TvInfo-CXnrLekr.js +1 -0
- package/client/dist/assets/TvPopup-BekfiNcX.js +1 -0
- package/client/dist/assets/{TvSearch-BWDwWnHe.js → TvSearch-DuqR-z3v.js} +1 -1
- package/client/dist/assets/{TvWatchlist-D3n5_4MI.js → TvWatchlist-CBvgFrA0.js} +1 -1
- package/client/dist/assets/Watchlist-mj8mTOam.js +1 -0
- package/client/dist/assets/{hls-BO_L1WoN.js → hls-Boje3IOE.js} +1 -1
- package/client/dist/assets/index-BXwhFWB_.js +178 -0
- package/client/dist/assets/useAnimeInfoData-DJ7A4B4T.js +1 -0
- package/client/dist/assets/useAsmr-DIwIsJXF.js +1 -0
- package/client/dist/assets/{useAsmrPopup-D_o9-QlR.js → useAsmrPopup-e4zNN9ig.js} +1 -1
- package/client/dist/assets/useMangaLibrary-C9Lci5N4.js +1 -0
- package/client/dist/assets/{useMangaPopup-TFzkC-tG.js → useMangaPopup-B8nnpvyi.js} +1 -1
- package/client/dist/assets/useProviders-B5Qr4I1y.js +1 -0
- package/client/dist/assets/useTv-Dm8MCYlL.js +1 -0
- package/client/dist/assets/useTvLibrary-B7jEk2Q7.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 +2 -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
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { protectedProcedure, router } from '../index.js';
|
|
2
|
+
import { defineSchema, optStr, reqObj, reqStr, reqStrArray } from '../validation.js';
|
|
3
|
+
import { performMangaWriteTransactionAsync } from '../../sync.js';
|
|
4
|
+
import { dbAll } from '../../utils/db-utils.js';
|
|
5
|
+
import { SettingsRepository } from '../../repositories/settings.repository.js';
|
|
6
|
+
import { MangaLibraryRepository, MangaProgressRepository, } from '../../repositories/manga.repository.js';
|
|
7
|
+
const MANGA_ADULT_RATINGS = ['erotica', 'pornographic'];
|
|
8
|
+
const isMangaContinueAdult = (row) => !!row.contentRating && MANGA_ADULT_RATINGS.includes(row.contentRating);
|
|
9
|
+
async function getAdultNonListMangaIds(db) {
|
|
10
|
+
const rows = await dbAll(db, `SELECT DISTINCT p.mangaId as mangaId
|
|
11
|
+
FROM manga_progress p
|
|
12
|
+
LEFT JOIN manga_library l ON l.id = p.mangaId
|
|
13
|
+
WHERE COALESCE(l.contentRating, p.contentRating) IN ('erotica', 'pornographic')
|
|
14
|
+
AND l.id IS NULL`);
|
|
15
|
+
return rows.map((r) => r.mangaId);
|
|
16
|
+
}
|
|
17
|
+
const mangaIdInput = () => defineSchema((value) => ({
|
|
18
|
+
mangaId: reqStr(reqObj(value), 'mangaId'),
|
|
19
|
+
}));
|
|
20
|
+
const mangaProgressSaveInput = () => defineSchema((value) => {
|
|
21
|
+
const obj = reqObj(value);
|
|
22
|
+
const out = {
|
|
23
|
+
mangaId: reqStr(obj, 'mangaId'),
|
|
24
|
+
chapterId: reqStr(obj, 'chapterId'),
|
|
25
|
+
};
|
|
26
|
+
if (obj.chapterNumber !== undefined && obj.chapterNumber !== null)
|
|
27
|
+
out.chapterNumber = String(obj.chapterNumber);
|
|
28
|
+
if (obj.page !== undefined && obj.page !== null) {
|
|
29
|
+
const page = Number(obj.page);
|
|
30
|
+
if (!Number.isNaN(page))
|
|
31
|
+
out.page = page;
|
|
32
|
+
}
|
|
33
|
+
if (obj.pageCount !== undefined && obj.pageCount !== null) {
|
|
34
|
+
const pageCount = Number(obj.pageCount);
|
|
35
|
+
if (!Number.isNaN(pageCount))
|
|
36
|
+
out.pageCount = pageCount;
|
|
37
|
+
}
|
|
38
|
+
const title = optStr(obj, 'title');
|
|
39
|
+
if (title !== undefined)
|
|
40
|
+
out.title = title;
|
|
41
|
+
const cover = optStr(obj, 'cover');
|
|
42
|
+
if (cover !== undefined)
|
|
43
|
+
out.cover = cover;
|
|
44
|
+
const provider = optStr(obj, 'provider');
|
|
45
|
+
if (provider !== undefined)
|
|
46
|
+
out.provider = provider;
|
|
47
|
+
const altTitle = optStr(obj, 'altTitle');
|
|
48
|
+
if (altTitle !== undefined)
|
|
49
|
+
out.altTitle = altTitle;
|
|
50
|
+
const contentRating = optStr(obj, 'contentRating');
|
|
51
|
+
if (contentRating !== undefined)
|
|
52
|
+
out.contentRating = contentRating;
|
|
53
|
+
return out;
|
|
54
|
+
});
|
|
55
|
+
const mangaProgressRemoveInput = () => defineSchema((value) => {
|
|
56
|
+
const obj = reqObj(value);
|
|
57
|
+
const out = { mangaId: reqStr(obj, 'mangaId') };
|
|
58
|
+
const chapterId = optStr(obj, 'chapterId');
|
|
59
|
+
if (chapterId !== undefined)
|
|
60
|
+
out.chapterId = chapterId;
|
|
61
|
+
return out;
|
|
62
|
+
});
|
|
63
|
+
const mangaProgressRemoveManyInput = () => defineSchema((value) => ({
|
|
64
|
+
ids: reqStrArray(reqObj(value), 'ids', true),
|
|
65
|
+
}));
|
|
66
|
+
const mangaContinueReadingInput = () => defineSchema((value) => {
|
|
67
|
+
const obj = reqObj(value);
|
|
68
|
+
const out = {};
|
|
69
|
+
if (obj.limit !== undefined && obj.limit !== null) {
|
|
70
|
+
const limit = Number(obj.limit);
|
|
71
|
+
if (!Number.isNaN(limit))
|
|
72
|
+
out.limit = limit;
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
});
|
|
76
|
+
export const mangaProgressRouter = router({
|
|
77
|
+
byManga: protectedProcedure.input(mangaIdInput()).query(async ({ ctx, input }) => {
|
|
78
|
+
try {
|
|
79
|
+
const rows = await MangaProgressRepository.getByManga(ctx.mangaDb, input.mangaId);
|
|
80
|
+
return { progress: rows };
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return { progress: [] };
|
|
84
|
+
}
|
|
85
|
+
}),
|
|
86
|
+
latest: protectedProcedure.input(mangaIdInput()).query(async ({ ctx, input }) => {
|
|
87
|
+
try {
|
|
88
|
+
const row = await MangaProgressRepository.getLatest(ctx.mangaDb, input.mangaId);
|
|
89
|
+
return row || { page: 0, pageCount: 0 };
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return { page: 0, pageCount: 0 };
|
|
93
|
+
}
|
|
94
|
+
}),
|
|
95
|
+
save: protectedProcedure.input(mangaProgressSaveInput()).mutation(async ({ ctx, input }) => {
|
|
96
|
+
const pageNum = Math.max(Number(input.page) || 0, 0);
|
|
97
|
+
const pageCountNum = Math.max(Number(input.pageCount) || 0, 0);
|
|
98
|
+
await performMangaWriteTransactionAsync(ctx.mangaDb, async (tx) => {
|
|
99
|
+
await MangaProgressRepository.upsert(tx, {
|
|
100
|
+
mangaId: String(input.mangaId),
|
|
101
|
+
chapterId: String(input.chapterId),
|
|
102
|
+
chapterNumber: String(input.chapterNumber || ''),
|
|
103
|
+
page: pageNum,
|
|
104
|
+
pageCount: pageCountNum,
|
|
105
|
+
title: (input.title ?? null),
|
|
106
|
+
cover: (input.cover ?? null),
|
|
107
|
+
provider: (input.provider ?? null),
|
|
108
|
+
altTitle: (input.altTitle ?? null),
|
|
109
|
+
contentRating: (input.contentRating ?? null),
|
|
110
|
+
});
|
|
111
|
+
await MangaLibraryRepository.touchProgress(tx, String(input.mangaId), {
|
|
112
|
+
chapterId: String(input.chapterId),
|
|
113
|
+
chapterNumber: String(input.chapterNumber || ''),
|
|
114
|
+
page: pageNum,
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
return { success: true };
|
|
118
|
+
}),
|
|
119
|
+
remove: protectedProcedure.input(mangaProgressRemoveInput()).mutation(async ({ ctx, input }) => {
|
|
120
|
+
await performMangaWriteTransactionAsync(ctx.mangaDb, async (tx) => {
|
|
121
|
+
if (input.chapterId) {
|
|
122
|
+
await MangaProgressRepository.deleteChapter(tx, String(input.mangaId), String(input.chapterId));
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
await MangaProgressRepository.deleteByManga(tx, String(input.mangaId));
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
return { success: true };
|
|
129
|
+
}),
|
|
130
|
+
removeMany: protectedProcedure
|
|
131
|
+
.input(mangaProgressRemoveManyInput())
|
|
132
|
+
.mutation(async ({ ctx, input }) => {
|
|
133
|
+
const ids = input.ids.map((id) => String(id));
|
|
134
|
+
await performMangaWriteTransactionAsync(ctx.mangaDb, async (tx) => {
|
|
135
|
+
await MangaProgressRepository.deleteMany(tx, ids);
|
|
136
|
+
});
|
|
137
|
+
return { success: true, removed: ids.length };
|
|
138
|
+
}),
|
|
139
|
+
continueReading: protectedProcedure
|
|
140
|
+
.input(mangaContinueReadingInput())
|
|
141
|
+
.query(async ({ ctx, input }) => {
|
|
142
|
+
try {
|
|
143
|
+
const limit = Math.min(Math.max(parseInt(String(input.limit)) || 24, 1), 100);
|
|
144
|
+
const ignoreAdultRow = await SettingsRepository.getByKey(ctx.db, 'mangaIgnoreAdultContent');
|
|
145
|
+
const ignoreAdult = ignoreAdultRow ? ignoreAdultRow.value !== 'false' : true;
|
|
146
|
+
const listOnlyRow = await SettingsRepository.getByKey(ctx.db, 'mangaCwWatchlistOnly');
|
|
147
|
+
const listOnly = listOnlyRow
|
|
148
|
+
? listOnlyRow.value === 'true' || listOnlyRow.value === '1'
|
|
149
|
+
: false;
|
|
150
|
+
const rows = (await MangaProgressRepository.getContinueReading(ctx.mangaDb, limit)).filter((row) => {
|
|
151
|
+
if (ignoreAdult && isMangaContinueAdult(row))
|
|
152
|
+
return false;
|
|
153
|
+
if (listOnly && row.watchlistStatus !== 'Reading')
|
|
154
|
+
return false;
|
|
155
|
+
return true;
|
|
156
|
+
});
|
|
157
|
+
return { data: rows, total: rows.length };
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return { data: [], total: 0 };
|
|
161
|
+
}
|
|
162
|
+
}),
|
|
163
|
+
adultCount: protectedProcedure.query(async ({ ctx }) => {
|
|
164
|
+
try {
|
|
165
|
+
const ids = await getAdultNonListMangaIds(ctx.mangaDb);
|
|
166
|
+
return { count: ids.length };
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return { count: 0 };
|
|
170
|
+
}
|
|
171
|
+
}),
|
|
172
|
+
purgeAdult: protectedProcedure.mutation(async ({ ctx }) => {
|
|
173
|
+
const ids = await getAdultNonListMangaIds(ctx.mangaDb);
|
|
174
|
+
if (ids.length > 0) {
|
|
175
|
+
await performMangaWriteTransactionAsync(ctx.mangaDb, async (tx) => {
|
|
176
|
+
await MangaProgressRepository.deleteMany(tx, ids);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return { success: true, removed: ids.length };
|
|
180
|
+
}),
|
|
181
|
+
});
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { TRPCError } from '@trpc/server';
|
|
2
|
+
import { protectedProcedure, router } from '../index.js';
|
|
3
|
+
import { parseJsonBody } from '../../utils/http.utils.js';
|
|
4
|
+
import { defineSchema, optStr, reqObj } from '../validation.js';
|
|
5
|
+
import logger from '../../logger.js';
|
|
6
|
+
const SAFE = ['safe'];
|
|
7
|
+
function parseRatings(query) {
|
|
8
|
+
const mature = query['mature'] === '1';
|
|
9
|
+
const raw = String(query['rating'] || 'safe')
|
|
10
|
+
.split(',')
|
|
11
|
+
.map((s) => s.trim().toLowerCase())
|
|
12
|
+
.filter((s) => ['safe', 'suggestive', 'erotica', 'pornographic'].includes(s));
|
|
13
|
+
if (!mature)
|
|
14
|
+
return SAFE;
|
|
15
|
+
const exact = [...new Set(raw)];
|
|
16
|
+
return exact.length > 0 ? exact : SAFE;
|
|
17
|
+
}
|
|
18
|
+
function pick(ctx, name) {
|
|
19
|
+
return ctx.getMangaProvider(name.toLowerCase());
|
|
20
|
+
}
|
|
21
|
+
async function cached(ctx, key, ttl, produce) {
|
|
22
|
+
const hit = ctx.apiCache.get(key);
|
|
23
|
+
if (hit)
|
|
24
|
+
return hit;
|
|
25
|
+
const result = await produce();
|
|
26
|
+
if ((result.status ?? 200) >= 200 && (result.status ?? 200) < 400) {
|
|
27
|
+
if (ttl !== undefined)
|
|
28
|
+
ctx.apiCache.set(key, result.body, ttl);
|
|
29
|
+
else
|
|
30
|
+
ctx.apiCache.set(key, result.body);
|
|
31
|
+
}
|
|
32
|
+
if (result.status !== undefined && result.status !== 200) {
|
|
33
|
+
throw new TRPCError({
|
|
34
|
+
code: result.status === 400
|
|
35
|
+
? 'BAD_REQUEST'
|
|
36
|
+
: result.status === 404
|
|
37
|
+
? 'NOT_FOUND'
|
|
38
|
+
: 'INTERNAL_SERVER_ERROR',
|
|
39
|
+
message: result.body?.error || 'Request failed',
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return result.body;
|
|
43
|
+
}
|
|
44
|
+
const mangaSearchInput = () => defineSchema((value) => {
|
|
45
|
+
const obj = reqObj(value);
|
|
46
|
+
const out = {};
|
|
47
|
+
for (const k of ['provider', 'q', 'sort', 'status', 'type', 'rating', 'mature']) {
|
|
48
|
+
const v = optStr(obj, k);
|
|
49
|
+
if (v !== undefined)
|
|
50
|
+
out[k] = v;
|
|
51
|
+
}
|
|
52
|
+
if (obj.page !== undefined)
|
|
53
|
+
out.page = parseInt(String(obj.page), 10) || 1;
|
|
54
|
+
if (obj.limit !== undefined)
|
|
55
|
+
out.limit = Math.min(parseInt(String(obj.limit), 10) || 24, 50);
|
|
56
|
+
return out;
|
|
57
|
+
});
|
|
58
|
+
const mangaInfoInput = () => defineSchema((value) => {
|
|
59
|
+
const obj = reqObj(value);
|
|
60
|
+
const out = {};
|
|
61
|
+
for (const k of ['provider', 'id', 'rating', 'mature']) {
|
|
62
|
+
const v = optStr(obj, k);
|
|
63
|
+
if (v !== undefined)
|
|
64
|
+
out[k] = v;
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
});
|
|
68
|
+
const mangaPagesInput = () => defineSchema((value) => {
|
|
69
|
+
const obj = reqObj(value);
|
|
70
|
+
const out = {};
|
|
71
|
+
const provider = optStr(obj, 'provider');
|
|
72
|
+
if (provider !== undefined)
|
|
73
|
+
out.provider = provider;
|
|
74
|
+
const id = optStr(obj, 'id');
|
|
75
|
+
if (id !== undefined)
|
|
76
|
+
out.id = id;
|
|
77
|
+
return out;
|
|
78
|
+
});
|
|
79
|
+
export const mangaRouter = router({
|
|
80
|
+
trending: protectedProcedure.query(async ({ ctx }) => {
|
|
81
|
+
const cacheKey = 'route-manga-trending';
|
|
82
|
+
const cached = ctx.apiCache.get(cacheKey);
|
|
83
|
+
if (cached)
|
|
84
|
+
return cached;
|
|
85
|
+
try {
|
|
86
|
+
const url = 'https://api.mangadex.org/manga?limit=6&order%5BfollowedCount%5D=desc&includes%5B%5D=cover_art&contentRating%5B%5D=safe&contentRating%5B%5D=suggestive';
|
|
87
|
+
const upstream = await fetch(url, {
|
|
88
|
+
headers: { 'User-Agent': 'dango/3.1.9' },
|
|
89
|
+
});
|
|
90
|
+
if (!upstream.ok)
|
|
91
|
+
return [];
|
|
92
|
+
const data = await parseJsonBody(upstream);
|
|
93
|
+
const results = (data.data || []).map((m) => {
|
|
94
|
+
const title = m.attributes?.title?.en || Object.values(m.attributes?.title || {})[0] || '';
|
|
95
|
+
const desc = m.attributes?.description?.en || '';
|
|
96
|
+
const tags = (m.attributes?.tags || [])
|
|
97
|
+
.map((t) => t.attributes?.name?.en)
|
|
98
|
+
.filter(Boolean);
|
|
99
|
+
const coverArt = m.relationships?.find((r) => r.type === 'cover_art');
|
|
100
|
+
const fileName = coverArt?.attributes?.fileName;
|
|
101
|
+
const cover = fileName
|
|
102
|
+
? `https://uploads.mangadex.org/covers/${m.id}/${fileName}.512.jpg`
|
|
103
|
+
: '';
|
|
104
|
+
return {
|
|
105
|
+
id: m.id,
|
|
106
|
+
title,
|
|
107
|
+
description: desc,
|
|
108
|
+
tags,
|
|
109
|
+
year: m.attributes?.year || null,
|
|
110
|
+
status: m.attributes?.status || null,
|
|
111
|
+
cover,
|
|
112
|
+
};
|
|
113
|
+
});
|
|
114
|
+
ctx.apiCache.set(cacheKey, results, 600);
|
|
115
|
+
return results;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
}),
|
|
121
|
+
search: protectedProcedure.input(mangaSearchInput()).query(async ({ ctx, input }) => {
|
|
122
|
+
const provider = input.provider || '';
|
|
123
|
+
const q = input.q || '';
|
|
124
|
+
const page = input.page || 1;
|
|
125
|
+
return cached(ctx, `route-manga-search-${provider}-${q}-${page}-${input.sort || ''}-${input.status || ''}-${input.type || ''}-${input.rating || ''}-${input.mature || ''}`, 300, async () => {
|
|
126
|
+
try {
|
|
127
|
+
const ratings = parseRatings({ rating: input.rating, mature: input.mature });
|
|
128
|
+
const rawType = input.type || '';
|
|
129
|
+
const type = rawType === 'doujinshi' && input.mature !== '1' ? '' : rawType;
|
|
130
|
+
const result = await pick(ctx, provider)?.search({
|
|
131
|
+
query: q,
|
|
132
|
+
page,
|
|
133
|
+
limit: input.limit || 24,
|
|
134
|
+
sort: input.sort || 'popular',
|
|
135
|
+
status: input.status || '',
|
|
136
|
+
type,
|
|
137
|
+
ratings,
|
|
138
|
+
});
|
|
139
|
+
return { body: result ?? { items: [], hasNext: false } };
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
logger.error({ err }, '[Manga] search failed');
|
|
143
|
+
return { body: { items: [], hasNext: false } };
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
}),
|
|
147
|
+
info: protectedProcedure.input(mangaInfoInput()).query(async ({ ctx, input }) => {
|
|
148
|
+
const provider = String(input.provider || '');
|
|
149
|
+
const id = String(input.id || '');
|
|
150
|
+
const rating = String(input.rating || '');
|
|
151
|
+
const mature = String(input.mature || '');
|
|
152
|
+
return cached(ctx, `route-manga-info-${provider}-${id}-${rating}-${mature}`, 600, async () => {
|
|
153
|
+
try {
|
|
154
|
+
if (!id)
|
|
155
|
+
return { status: 400, body: { error: 'Missing id' } };
|
|
156
|
+
const detail = await pick(ctx, provider)?.getDetail(id, parseRatings({ rating, mature }));
|
|
157
|
+
if (!detail)
|
|
158
|
+
return { status: 404, body: { error: 'Not found' } };
|
|
159
|
+
return { body: detail };
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
logger.error({ err }, '[Manga] info failed');
|
|
163
|
+
return { status: 500, body: { error: 'Failed to load manga' } };
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}),
|
|
167
|
+
chapters: protectedProcedure.input(mangaInfoInput()).query(async ({ ctx, input }) => {
|
|
168
|
+
const provider = String(input.provider || '');
|
|
169
|
+
const id = String(input.id || '');
|
|
170
|
+
const rating = String(input.rating || '');
|
|
171
|
+
const mature = String(input.mature || '');
|
|
172
|
+
return cached(ctx, `route-manga-chapters-${provider}-${id}-${rating}-${mature}`, 600, async () => {
|
|
173
|
+
try {
|
|
174
|
+
if (!id)
|
|
175
|
+
return { status: 400, body: { error: 'Missing id' } };
|
|
176
|
+
const chapters = (await pick(ctx, provider)?.getChapters(id, parseRatings({ rating, mature }))) ?? [];
|
|
177
|
+
return { body: { chapters } };
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
logger.error({ err }, '[Manga] chapters failed');
|
|
181
|
+
return { body: { chapters: [] } };
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
}),
|
|
185
|
+
pages: protectedProcedure.input(mangaPagesInput()).query(async ({ ctx, input }) => {
|
|
186
|
+
const provider = String(input.provider || '');
|
|
187
|
+
const id = String(input.id || '');
|
|
188
|
+
return cached(ctx, `route-manga-pages-${provider}-${id}`, 300, async () => {
|
|
189
|
+
try {
|
|
190
|
+
if (!id)
|
|
191
|
+
return { status: 400, body: { error: 'Missing id' } };
|
|
192
|
+
const pages = (await pick(ctx, provider)?.getPages(id)) ?? [];
|
|
193
|
+
return { body: { pages } };
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
logger.error({ err }, '[Manga] pages failed');
|
|
197
|
+
return { body: { pages: [] } };
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
}),
|
|
201
|
+
});
|