@serifpersia/dango 3.6.7 → 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.
- package/README.md +1 -1
- package/client/dist/assets/{AnimeCard-C3P9zWi9.js → AnimeCard-DraA_TF9.js} +1 -1
- package/client/dist/assets/{AnimeInfo-Et6687Tl.js → AnimeInfo-BJ7pzs-P.js} +1 -1
- package/client/dist/assets/{AnimeInfoPage-DX4eNnlH.js → AnimeInfoPage-B_esUr5C.js} +2 -2
- package/client/dist/assets/{Asmr-CCdEWrFi.js → Asmr-Ckuh2xfW.js} +1 -1
- package/client/dist/assets/{AsmrResetProgressModal-DgEWBJOw.js → AsmrResetProgressModal-BS6UFJN4.js} +1 -1
- package/client/dist/assets/{AvSyncCalibrator-CQ2fjf2E.js → AvSyncCalibrator-DwHCd9yB.js} +1 -1
- package/client/dist/assets/{Home-DUkUTTTy.js → Home-DuM1EuvN.js} +1 -1
- package/client/dist/assets/{Insights-QCh4xW12.js → Insights-D3Pd9c9V.js} +1 -1
- package/client/dist/assets/{LibraryListPage-BoTxRqHz.js → LibraryListPage-Dm2yVn0-.js} +1 -1
- package/client/dist/assets/{ListeningList-DguKjI_t.js → ListeningList-D-zt6EXZ.js} +1 -1
- package/client/dist/assets/{Manga-HydR-L7o.js → Manga-JwprMKBk.js} +1 -1
- package/client/dist/assets/{MangaInfoPage-CBakEI9a.js → MangaInfoPage-DaeyJl2p.js} +1 -1
- package/client/dist/assets/{MangaReadPage-DwAR3iPy.js → MangaReadPage-COg0fHI9.js} +1 -1
- package/client/dist/assets/{MangaResetProgressModal-DGzl8vsO.js → MangaResetProgressModal-kguXv0_M.js} +1 -1
- package/client/dist/assets/{Mature-Pi0TxKuF.js → Mature-IHDaasrs.js} +1 -1
- package/client/dist/assets/{MediaCard--cuFxOCS.js → MediaCard-CpwsrXo5.js} +1 -1
- package/client/dist/assets/{Music-CQpyXTLH.js → Music-Dks4tPeK.js} +1 -1
- package/client/dist/assets/{OptionListMenu-B3D39dGS.js → OptionListMenu-11CcAsy8.js} +1 -1
- package/client/dist/assets/{Player-CsH4x_I_.js → Player-DQoEQkHL.js} +2 -2
- package/client/dist/assets/{PlayerSettings-DGxBsi3F.js → PlayerSettings-D4Ud4kv4.js} +1 -1
- package/client/dist/assets/{QueueOptionsButton-Djq4Cpex.js → QueueOptionsButton-B8PlPKjC.js} +1 -1
- package/client/dist/assets/{QueueRail-DOrYgNVX.js → QueueRail-B4Dok7U3.js} +1 -1
- package/client/dist/assets/{Radio-DF6g6odd.js → Radio-BHK6LSk4.js} +1 -1
- package/client/dist/assets/{ReadingList-DwYoBrNk.js → ReadingList-BCsxIz57.js} +1 -1
- package/client/dist/assets/{ResetProgressModal-CMF86mX6.js → ResetProgressModal-BXHxSfy-.js} +1 -1
- package/client/dist/assets/{Search-DFkhuk-x.js → Search-PK1ds07O.js} +1 -1
- package/client/dist/assets/{Settings-DBB2r0VQ.js → Settings-Bxk_x55l.js} +1 -1
- package/client/dist/assets/{SkeletonGrid-DJelJgAv.js → SkeletonGrid-CpJCZSRN.js} +1 -1
- package/client/dist/assets/{Trackers-DGYXFhYZ.js → Trackers-RieCh_84.js} +1 -1
- package/client/dist/assets/{Tv-C5MDGD2Z.js → Tv-DLytPJUj.js} +1 -1
- package/client/dist/assets/{TvInfo-DrKuK13u.js → TvInfo-C8djKebR.js} +1 -1
- package/client/dist/assets/{TvPopup-DtHilRDT.js → TvPopup-40JGGJDk.js} +1 -1
- package/client/dist/assets/{TvSearch-CzNxdu_w.js → TvSearch-A3hbospf.js} +1 -1
- package/client/dist/assets/{TvWatchlist-D7p4br6S.js → TvWatchlist-DuXjqmZM.js} +1 -1
- package/client/dist/assets/{Watchlist-DrOzswl2.js → Watchlist-B9wGBh0u.js} +1 -1
- package/client/dist/assets/{hls-BGCX7mJy.js → hls-CZ5X3Ya-.js} +1 -1
- package/client/dist/assets/{index-CnfZwsds.js → index-dRhc9-ol.js} +2 -2
- package/client/dist/assets/{useAnimeInfoData-DYXWBPF-.js → useAnimeInfoData-ZzDtLfvD.js} +1 -1
- package/client/dist/assets/{useAsmr-BbhQ59bk.js → useAsmr-CKOdHgol.js} +1 -1
- package/client/dist/assets/{useAsmrPopup-SymV1J-b.js → useAsmrPopup-SFHPhpcl.js} +1 -1
- package/client/dist/assets/{useMangaLibrary-DuesWQaE.js → useMangaLibrary-CKATe31P.js} +1 -1
- package/client/dist/assets/{useMangaPopup-D57OVGUr.js → useMangaPopup-C75QuYgY.js} +1 -1
- package/client/dist/assets/{useProviders-mpQb_dix.js → useProviders-CMtvZbEi.js} +1 -1
- package/client/dist/assets/{useTv-DGflCsSI.js → useTv-DUX4zYMr.js} +1 -1
- package/client/dist/assets/{useTvLibrary-BqeSmyc4.js → useTvLibrary-By4Gt-R_.js} +1 -1
- package/client/dist/index.html +1 -1
- package/package.json +2 -2
- package/server/dist/app-auth.js +1 -157
- package/server/dist/app-hono.js +7 -557
- package/server/dist/config.js +1 -174
- package/server/dist/db/drizzle.js +1 -16
- package/server/dist/db/schema-anime.js +1 -61
- package/server/dist/db/schema-asmr.js +1 -13
- package/server/dist/db/schema-manga.js +1 -31
- package/server/dist/db/schema-tv.js +1 -18
- package/server/dist/db.js +1 -217
- package/server/dist/discord-gateway.js +1 -385
- package/server/dist/discord-rpc.js +1 -413
- package/server/dist/github-sync.js +1 -508
- package/server/dist/google.js +1 -741
- package/server/dist/hono/asmr.js +1 -10
- package/server/dist/hono/auth.js +3 -26
- package/server/dist/hono/music.js +1 -209
- package/server/dist/hono/proxy.js +36 -783
- package/server/dist/hono/tv.js +5 -200
- package/server/dist/hono/watchlist.js +2 -338
- package/server/dist/lib/anilist.js +26 -1458
- package/server/dist/lib/anischedule.js +1 -191
- package/server/dist/lib/insights.js +1 -185
- package/server/dist/lib/ipc.js +1 -23
- package/server/dist/lib/kitsu.js +1 -517
- package/server/dist/lib/mal-import.js +1 -291
- package/server/dist/lib/mal.js +1 -957
- package/server/dist/lib/migration.js +1 -310
- package/server/dist/lib/offline-db.js +4 -384
- package/server/dist/lib/potoken.js +1 -135
- package/server/dist/lib/recommendations.service.js +1 -216
- package/server/dist/lib/temp-ids.js +1 -3
- package/server/dist/lib/tmdb.js +1 -72
- package/server/dist/lib/tracker/anilist-tracker.js +8 -245
- package/server/dist/lib/tracker/manga-sync.service.js +1 -773
- package/server/dist/lib/tracker/sync-state.js +1 -21
- package/server/dist/lib/tracker/sync.service.js +1 -476
- package/server/dist/lib/translate.js +3 -79
- package/server/dist/lib/ump.js +1 -106
- package/server/dist/lib/ytmusic.js +1 -162
- package/server/dist/logger.js +1 -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 +2 -614
- package/server/dist/providers/remote-types.js +1 -159
- package/server/dist/providers/title-matching.js +1 -226
- package/server/dist/providers/tv.types.js +0 -1
- package/server/dist/rclone.js +2 -120
- package/server/dist/repositories/asmr.repository.js +11 -46
- package/server/dist/repositories/insights.repository.js +11 -25
- package/server/dist/repositories/library.repository.js +3 -94
- package/server/dist/repositories/mal-cache.repository.js +1 -52
- package/server/dist/repositories/manga.repository.js +14 -58
- package/server/dist/repositories/notifications.repository.js +6 -52
- package/server/dist/repositories/queue.repository.js +5 -58
- package/server/dist/repositories/settings.repository.js +2 -29
- package/server/dist/repositories/shows-meta.repository.js +3 -56
- package/server/dist/repositories/temp-show-ids.repository.js +3 -63
- package/server/dist/repositories/tv.repository.js +11 -54
- package/server/dist/repositories/watched-episodes.repository.js +6 -55
- package/server/dist/repositories/watchlist.repository.js +3 -54
- package/server/dist/scripts/backfill-episode-duration.js +1 -42
- package/server/dist/server.js +1 -310
- package/server/dist/shipped-defaults.js +1 -6
- package/server/dist/sync-payload.js +1 -91
- package/server/dist/sync.js +3 -647
- package/server/dist/trpc/context.js +1 -7
- package/server/dist/trpc/errors.js +1 -15
- package/server/dist/trpc/index.js +1 -18
- package/server/dist/trpc/router.js +1 -49
- package/server/dist/trpc/routers/asmr-library.js +1 -112
- package/server/dist/trpc/routers/asmr-progress.js +1 -121
- package/server/dist/trpc/routers/asmr.js +1 -87
- package/server/dist/trpc/routers/auth.js +1 -194
- package/server/dist/trpc/routers/continue-watching.js +2 -58
- package/server/dist/trpc/routers/data.js +1 -1026
- package/server/dist/trpc/routers/discord.js +1 -285
- package/server/dist/trpc/routers/discovery.js +1 -11
- package/server/dist/trpc/routers/insights.js +1 -14
- package/server/dist/trpc/routers/manga-library.js +1 -263
- package/server/dist/trpc/routers/manga-progress.js +1 -169
- package/server/dist/trpc/routers/manga.js +1 -201
- package/server/dist/trpc/routers/music.js +1 -655
- package/server/dist/trpc/routers/notifications.js +1 -80
- package/server/dist/trpc/routers/progress.js +1 -109
- package/server/dist/trpc/routers/queue.js +1 -93
- package/server/dist/trpc/routers/radio.js +1 -145
- package/server/dist/trpc/routers/settings.js +1 -96
- package/server/dist/trpc/routers/tracker.js +1 -213
- package/server/dist/trpc/routers/tv-library.js +1 -135
- package/server/dist/trpc/routers/tv-progress.js +1 -159
- package/server/dist/trpc/routers/tv.js +1 -551
- package/server/dist/trpc/routers/watchlist.js +2 -549
- package/server/dist/trpc/validation.js +1 -258
- package/server/dist/utils/cache.utils.js +1 -25
- package/server/dist/utils/cookie.utils.js +1 -13
- package/server/dist/utils/db-utils.js +5 -17
- package/server/dist/utils/env.utils.js +5 -77
- package/server/dist/utils/http.utils.js +1 -82
- package/server/dist/utils/request-context.js +1 -2
- package/server/dist/utils/security.utils.js +1 -76
- package/server/package.json +3 -2
|
@@ -1,285 +1 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { badRequest, defineSchema, failed, optStr, reqObj } from '../validation.js';
|
|
3
|
-
import { discordRPCService } from '../../discord-rpc.js';
|
|
4
|
-
import { discordGatewayService } from '../../discord-gateway.js';
|
|
5
|
-
import { getDlsitePoster, getMangadexCover } from '../../hono/watchlist.js';
|
|
6
|
-
import { updateEnvFile } from '../../utils/env.utils.js';
|
|
7
|
-
import logger from '../../logger.js';
|
|
8
|
-
function maskToken(t) {
|
|
9
|
-
if (!t)
|
|
10
|
-
return 'none';
|
|
11
|
-
return t.slice(0, 8) + '...' + t.slice(-4);
|
|
12
|
-
}
|
|
13
|
-
const gatewayLog = logger.child({ module: 'DiscordGatewayRoutes' });
|
|
14
|
-
function optUnknown(obj, name) {
|
|
15
|
-
return obj[name];
|
|
16
|
-
}
|
|
17
|
-
function cleanProxyThumb(value) {
|
|
18
|
-
let thumb = String(value || '');
|
|
19
|
-
if (thumb.includes('/api/proxy') || thumb.includes('/api/image-proxy')) {
|
|
20
|
-
const match = thumb.match(/url=([^&]+)/);
|
|
21
|
-
if (match) {
|
|
22
|
-
try {
|
|
23
|
-
thumb = decodeURIComponent(match[1]);
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
return '';
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
if (!thumb.startsWith('https://'))
|
|
31
|
-
return '';
|
|
32
|
-
if (thumb.includes('localhost') || thumb.includes('127.0.0.1'))
|
|
33
|
-
return '';
|
|
34
|
-
return thumb;
|
|
35
|
-
}
|
|
36
|
-
const presenceInput = () => defineSchema((value) => {
|
|
37
|
-
if (value === undefined || value === null)
|
|
38
|
-
return {};
|
|
39
|
-
return { ...reqObj(value) };
|
|
40
|
-
});
|
|
41
|
-
export const discordRouter = router({
|
|
42
|
-
pageStatus: protectedProcedure.input(presenceInput()).mutation(async ({ input }) => {
|
|
43
|
-
const page = optUnknown(input, 'page');
|
|
44
|
-
if (typeof page !== 'string' || !discordRPCService.isServiceEnabled) {
|
|
45
|
-
return { success: true };
|
|
46
|
-
}
|
|
47
|
-
discordRPCService.setIdleStatus(page);
|
|
48
|
-
return { success: true };
|
|
49
|
-
}),
|
|
50
|
-
heartbeat: protectedProcedure.input(presenceInput()).mutation(async ({ input }) => {
|
|
51
|
-
const sessionId = optUnknown(input, 'sessionId');
|
|
52
|
-
const bye = optUnknown(input, 'bye');
|
|
53
|
-
if (typeof sessionId !== 'string' || !discordRPCService.isServiceEnabled) {
|
|
54
|
-
return { success: true };
|
|
55
|
-
}
|
|
56
|
-
if (bye) {
|
|
57
|
-
discordRPCService.removeHeartbeat(sessionId);
|
|
58
|
-
}
|
|
59
|
-
else {
|
|
60
|
-
discordRPCService.heartbeat(sessionId);
|
|
61
|
-
}
|
|
62
|
-
return { success: true };
|
|
63
|
-
}),
|
|
64
|
-
clear: protectedProcedure.input(presenceInput()).mutation(async ({ input }) => {
|
|
65
|
-
const sessionId = optUnknown(input, 'sessionId');
|
|
66
|
-
if (!discordRPCService.isServiceEnabled) {
|
|
67
|
-
return { success: true };
|
|
68
|
-
}
|
|
69
|
-
discordRPCService.clearPresence(typeof sessionId === 'string' ? sessionId : undefined);
|
|
70
|
-
return { success: true };
|
|
71
|
-
}),
|
|
72
|
-
asmrPresence: protectedProcedure.input(presenceInput()).mutation(async ({ input }) => {
|
|
73
|
-
if (!discordRPCService.isServiceEnabled)
|
|
74
|
-
return { success: true };
|
|
75
|
-
const sessionId = optUnknown(input, 'sessionId');
|
|
76
|
-
if (typeof sessionId === 'string')
|
|
77
|
-
discordRPCService.heartbeat(sessionId);
|
|
78
|
-
const thumbnail = optUnknown(input, 'thumbnail');
|
|
79
|
-
const thumbnails = optUnknown(input, 'thumbnails');
|
|
80
|
-
const rjCode = optUnknown(input, 'rjCode');
|
|
81
|
-
let thumb = String(thumbnail || '');
|
|
82
|
-
const thumbs = Array.isArray(thumbnails) ? thumbnails.map(String).filter(Boolean) : undefined;
|
|
83
|
-
const needsDlsite = !thumb || thumb.includes('weeabo0.xyz') || thumb.includes('japaneseasmr.com');
|
|
84
|
-
if (needsDlsite && rjCode) {
|
|
85
|
-
const dlsiteThumb = await getDlsitePoster(String(rjCode));
|
|
86
|
-
if (dlsiteThumb)
|
|
87
|
-
thumb = dlsiteThumb;
|
|
88
|
-
}
|
|
89
|
-
if (thumb.includes('/api/image-proxy') || thumb.includes('weeabo0.xyz'))
|
|
90
|
-
thumb = '';
|
|
91
|
-
discordRPCService.updatePresence({
|
|
92
|
-
title: String(optUnknown(input, 'title') || 'ASMR').slice(0, 128),
|
|
93
|
-
episode: String(optUnknown(input, 'trackLabel') || '').slice(0, 64),
|
|
94
|
-
totalEpisodes: '',
|
|
95
|
-
currentTime: Number(optUnknown(input, 'currentTime')) || 0,
|
|
96
|
-
duration: Number(optUnknown(input, 'duration')) || 0,
|
|
97
|
-
thumbnail: thumb,
|
|
98
|
-
thumbnails: thumbs,
|
|
99
|
-
isPlaying: !!optUnknown(input, 'isPlaying'),
|
|
100
|
-
providerName: 'ASMR',
|
|
101
|
-
sessionId: typeof sessionId === 'string' ? sessionId : undefined,
|
|
102
|
-
isAdult: !!optUnknown(input, 'isAdult'),
|
|
103
|
-
});
|
|
104
|
-
return { success: true };
|
|
105
|
-
}),
|
|
106
|
-
radioPresence: protectedProcedure.input(presenceInput()).mutation(async ({ input }) => {
|
|
107
|
-
if (!discordRPCService.isServiceEnabled)
|
|
108
|
-
return { success: true };
|
|
109
|
-
const sessionId = optUnknown(input, 'sessionId');
|
|
110
|
-
if (typeof sessionId === 'string')
|
|
111
|
-
discordRPCService.heartbeat(sessionId);
|
|
112
|
-
let thumb = String(optUnknown(input, 'thumbnail') || '');
|
|
113
|
-
if (thumb.includes('/api/image-proxy')) {
|
|
114
|
-
const match = thumb.match(/url=([^&]+)/);
|
|
115
|
-
if (match)
|
|
116
|
-
thumb = decodeURIComponent(match[1]);
|
|
117
|
-
}
|
|
118
|
-
if (thumb && !thumb.startsWith('https://'))
|
|
119
|
-
thumb = '';
|
|
120
|
-
if (thumb.includes('localhost') || thumb.includes('127.0.0.1'))
|
|
121
|
-
thumb = '';
|
|
122
|
-
const stationLabel = String(optUnknown(input, 'stationLabel') || '');
|
|
123
|
-
discordRPCService.updatePresence({
|
|
124
|
-
title: String(optUnknown(input, 'title') || 'Radio').slice(0, 128),
|
|
125
|
-
episode: stationLabel.slice(0, 64),
|
|
126
|
-
totalEpisodes: '',
|
|
127
|
-
stateLine: (stationLabel || 'Radio').slice(0, 64),
|
|
128
|
-
currentTime: Number(optUnknown(input, 'currentTime')) || 0,
|
|
129
|
-
duration: 0,
|
|
130
|
-
thumbnail: thumb,
|
|
131
|
-
isPlaying: !!optUnknown(input, 'isPlaying'),
|
|
132
|
-
providerName: 'Radio',
|
|
133
|
-
sessionId: typeof sessionId === 'string' ? sessionId : undefined,
|
|
134
|
-
isAdult: false,
|
|
135
|
-
});
|
|
136
|
-
return { success: true };
|
|
137
|
-
}),
|
|
138
|
-
musicPresence: protectedProcedure.input(presenceInput()).mutation(async ({ input }) => {
|
|
139
|
-
if (!discordRPCService.isServiceEnabled)
|
|
140
|
-
return { success: true };
|
|
141
|
-
const sessionId = optUnknown(input, 'sessionId');
|
|
142
|
-
if (typeof sessionId === 'string')
|
|
143
|
-
discordRPCService.heartbeat(sessionId);
|
|
144
|
-
let thumb = String(optUnknown(input, 'thumbnail') || '');
|
|
145
|
-
if (thumb.includes('/api/image-proxy')) {
|
|
146
|
-
const match = thumb.match(/url=([^&]+)/);
|
|
147
|
-
if (match)
|
|
148
|
-
thumb = decodeURIComponent(match[1]);
|
|
149
|
-
}
|
|
150
|
-
if (thumb && !thumb.startsWith('https://'))
|
|
151
|
-
thumb = '';
|
|
152
|
-
if (thumb.includes('localhost') || thumb.includes('127.0.0.1'))
|
|
153
|
-
thumb = '';
|
|
154
|
-
const artistLabel = String(optUnknown(input, 'artistLabel') || '');
|
|
155
|
-
discordRPCService.updatePresence({
|
|
156
|
-
title: String(optUnknown(input, 'title') || 'Music').slice(0, 128),
|
|
157
|
-
episode: artistLabel.slice(0, 64),
|
|
158
|
-
totalEpisodes: '',
|
|
159
|
-
stateLine: (artistLabel || 'Music').slice(0, 64),
|
|
160
|
-
currentTime: Number(optUnknown(input, 'currentTime')) || 0,
|
|
161
|
-
duration: Number(optUnknown(input, 'duration')) || 0,
|
|
162
|
-
thumbnail: thumb,
|
|
163
|
-
isPlaying: !!optUnknown(input, 'isPlaying'),
|
|
164
|
-
providerName: 'Music',
|
|
165
|
-
sessionId: typeof sessionId === 'string' ? sessionId : undefined,
|
|
166
|
-
isAdult: false,
|
|
167
|
-
});
|
|
168
|
-
return { success: true };
|
|
169
|
-
}),
|
|
170
|
-
mangaPresence: protectedProcedure.input(presenceInput()).mutation(async ({ input }) => {
|
|
171
|
-
if (!discordRPCService.isServiceEnabled)
|
|
172
|
-
return { success: true };
|
|
173
|
-
const sessionId = optUnknown(input, 'sessionId');
|
|
174
|
-
if (typeof sessionId === 'string')
|
|
175
|
-
discordRPCService.heartbeat(sessionId);
|
|
176
|
-
const thumbnails = optUnknown(input, 'thumbnails');
|
|
177
|
-
const title = optUnknown(input, 'title');
|
|
178
|
-
let thumb = cleanProxyThumb(optUnknown(input, 'thumbnail'));
|
|
179
|
-
let thumbs = Array.isArray(thumbnails)
|
|
180
|
-
? thumbnails.map(cleanProxyThumb).filter(Boolean)
|
|
181
|
-
: undefined;
|
|
182
|
-
if (!thumb && title) {
|
|
183
|
-
const fallback = await getMangadexCover(String(title));
|
|
184
|
-
if (fallback) {
|
|
185
|
-
thumb = fallback;
|
|
186
|
-
thumbs = [fallback, ...(thumbs || [])];
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
const chapterLabel = String(optUnknown(input, 'chapterLabel') || '');
|
|
190
|
-
discordRPCService.updatePresence({
|
|
191
|
-
title: String(title || 'Manga').slice(0, 128),
|
|
192
|
-
episode: chapterLabel.slice(0, 64),
|
|
193
|
-
totalEpisodes: '',
|
|
194
|
-
stateLine: (chapterLabel ? `Reading ${chapterLabel}` : 'Reading').slice(0, 64),
|
|
195
|
-
currentTime: 0,
|
|
196
|
-
duration: 0,
|
|
197
|
-
thumbnail: thumb,
|
|
198
|
-
thumbnails: thumbs,
|
|
199
|
-
isPlaying: optUnknown(input, 'isPlaying') !== false,
|
|
200
|
-
providerName: 'Manga',
|
|
201
|
-
sessionId: typeof sessionId === 'string' ? sessionId : undefined,
|
|
202
|
-
isAdult: optUnknown(input, 'isAdult') === true,
|
|
203
|
-
});
|
|
204
|
-
return { success: true };
|
|
205
|
-
}),
|
|
206
|
-
tvPresence: protectedProcedure.input(presenceInput()).mutation(async ({ input }) => {
|
|
207
|
-
if (!discordRPCService.isServiceEnabled)
|
|
208
|
-
return { success: true };
|
|
209
|
-
const sessionId = optUnknown(input, 'sessionId');
|
|
210
|
-
if (typeof sessionId === 'string')
|
|
211
|
-
discordRPCService.heartbeat(sessionId);
|
|
212
|
-
const thumbnail = optUnknown(input, 'thumbnail');
|
|
213
|
-
let thumb = String(thumbnail || '');
|
|
214
|
-
if (thumb.includes('/api/image-proxy')) {
|
|
215
|
-
const match = thumb.match(/url=([^&]+)/);
|
|
216
|
-
if (match)
|
|
217
|
-
thumb = decodeURIComponent(match[1]);
|
|
218
|
-
}
|
|
219
|
-
if (thumb.includes('localhost') || thumb.includes('127.0.0.1'))
|
|
220
|
-
thumb = '';
|
|
221
|
-
const episodeLabel = String(optUnknown(input, 'episodeLabel') || '');
|
|
222
|
-
discordRPCService.updatePresence({
|
|
223
|
-
title: String(optUnknown(input, 'title') || 'TV').slice(0, 128),
|
|
224
|
-
episode: episodeLabel.slice(0, 64),
|
|
225
|
-
totalEpisodes: '',
|
|
226
|
-
stateLine: (episodeLabel || 'Movie').slice(0, 64),
|
|
227
|
-
currentTime: Number(optUnknown(input, 'currentTime')) || 0,
|
|
228
|
-
duration: Number(optUnknown(input, 'duration')) || 0,
|
|
229
|
-
thumbnail: thumb,
|
|
230
|
-
isPlaying: !!optUnknown(input, 'isPlaying'),
|
|
231
|
-
providerName: 'TV',
|
|
232
|
-
sessionId: typeof sessionId === 'string' ? sessionId : undefined,
|
|
233
|
-
isAdult: optUnknown(input, 'isAdult') === true,
|
|
234
|
-
});
|
|
235
|
-
return { success: true };
|
|
236
|
-
}),
|
|
237
|
-
gatewayStatus: protectedProcedure.query(() => {
|
|
238
|
-
const hasToken = discordGatewayService.hasToken();
|
|
239
|
-
return {
|
|
240
|
-
hasToken,
|
|
241
|
-
masked: hasToken ? maskToken(process.env.DISCORD_GATEWAY_TOKEN) : null,
|
|
242
|
-
enabled: discordGatewayService.serviceEnabled,
|
|
243
|
-
};
|
|
244
|
-
}),
|
|
245
|
-
gatewaySave: protectedProcedure
|
|
246
|
-
.input(defineSchema((value) => {
|
|
247
|
-
if (value === undefined || value === null)
|
|
248
|
-
return {};
|
|
249
|
-
const obj = reqObj(value);
|
|
250
|
-
const out = {};
|
|
251
|
-
const token = optStr(obj, 'token');
|
|
252
|
-
if (token !== undefined)
|
|
253
|
-
out.token = token;
|
|
254
|
-
return out;
|
|
255
|
-
}))
|
|
256
|
-
.mutation(async ({ input }) => {
|
|
257
|
-
let token = (input.token || '').trim();
|
|
258
|
-
if (token.startsWith('"') && token.endsWith('"'))
|
|
259
|
-
token = token.slice(1, -1);
|
|
260
|
-
if (token.length < 30)
|
|
261
|
-
throw badRequest('Token too short');
|
|
262
|
-
try {
|
|
263
|
-
await updateEnvFile({ DISCORD_GATEWAY_TOKEN: token });
|
|
264
|
-
discordGatewayService.reloadToken();
|
|
265
|
-
gatewayLog.info(`Discord Gateway token saved ${maskToken(token)} (not logged)`);
|
|
266
|
-
return { ok: true, masked: maskToken(token) };
|
|
267
|
-
}
|
|
268
|
-
catch (e) {
|
|
269
|
-
gatewayLog.error({ err: e }, 'Failed to save Discord Gateway token');
|
|
270
|
-
throw failed('Failed to save token');
|
|
271
|
-
}
|
|
272
|
-
}),
|
|
273
|
-
gatewayRemove: protectedProcedure.mutation(async () => {
|
|
274
|
-
try {
|
|
275
|
-
await updateEnvFile({ DISCORD_GATEWAY_TOKEN: '' });
|
|
276
|
-
discordGatewayService.reloadToken();
|
|
277
|
-
gatewayLog.info('Discord Gateway token removed');
|
|
278
|
-
return { ok: true };
|
|
279
|
-
}
|
|
280
|
-
catch (e) {
|
|
281
|
-
gatewayLog.error({ err: e }, 'Failed to remove Discord Gateway token');
|
|
282
|
-
throw failed('Failed to remove token');
|
|
283
|
-
}
|
|
284
|
-
}),
|
|
285
|
-
});
|
|
1
|
+
import{protectedProcedure as c,router as I}from"../index.js";import{badRequest as P,defineSchema as h,failed as y,optStr as v,reqObj as p}from"../validation.js";import{discordRPCService as r}from"../../discord-rpc.js";import{discordGatewayService as m}from"../../discord-gateway.js";import{getDlsitePoster as T,getMangadexCover as w}from"../../hono/watchlist.js";import{updateEnvFile as S}from"../../utils/env.utils.js";import E from"../../logger.js";function g(e){return e?e.slice(0,8)+"..."+e.slice(-4):"none"}const f=E.child({module:"DiscordGatewayRoutes"});function s(e,t){return e[t]}function k(e){let t=String(e||"");if(t.includes("/api/proxy")||t.includes("/api/image-proxy")){const i=t.match(/url=([^&]+)/);if(i)try{t=decodeURIComponent(i[1])}catch{return""}}return!t.startsWith("https://")||t.includes("localhost")||t.includes("127.0.0.1")?"":t}const u=()=>h(e=>e==null?{}:{...p(e)}),G=I({pageStatus:c.input(u()).mutation(async({input:e})=>{const t=s(e,"page");return typeof t!="string"||!r.isServiceEnabled?{success:!0}:(r.setIdleStatus(t),{success:!0})}),heartbeat:c.input(u()).mutation(async({input:e})=>{const t=s(e,"sessionId"),i=s(e,"bye");return typeof t!="string"||!r.isServiceEnabled?{success:!0}:(i?r.removeHeartbeat(t):r.heartbeat(t),{success:!0})}),clear:c.input(u()).mutation(async({input:e})=>{const t=s(e,"sessionId");return r.isServiceEnabled?(r.clearPresence(typeof t=="string"?t:void 0),{success:!0}):{success:!0}}),asmrPresence:c.input(u()).mutation(async({input:e})=>{if(!r.isServiceEnabled)return{success:!0};const t=s(e,"sessionId");typeof t=="string"&&r.heartbeat(t);const i=s(e,"thumbnail"),n=s(e,"thumbnails"),o=s(e,"rjCode");let a=String(i||"");const d=Array.isArray(n)?n.map(String).filter(Boolean):void 0;if((!a||a.includes("weeabo0.xyz")||a.includes("japaneseasmr.com"))&&o){const b=await T(String(o));b&&(a=b)}return(a.includes("/api/image-proxy")||a.includes("weeabo0.xyz"))&&(a=""),r.updatePresence({title:String(s(e,"title")||"ASMR").slice(0,128),episode:String(s(e,"trackLabel")||"").slice(0,64),totalEpisodes:"",currentTime:Number(s(e,"currentTime"))||0,duration:Number(s(e,"duration"))||0,thumbnail:a,thumbnails:d,isPlaying:!!s(e,"isPlaying"),providerName:"ASMR",sessionId:typeof t=="string"?t:void 0,isAdult:!!s(e,"isAdult")}),{success:!0}}),radioPresence:c.input(u()).mutation(async({input:e})=>{if(!r.isServiceEnabled)return{success:!0};const t=s(e,"sessionId");typeof t=="string"&&r.heartbeat(t);let i=String(s(e,"thumbnail")||"");if(i.includes("/api/image-proxy")){const o=i.match(/url=([^&]+)/);o&&(i=decodeURIComponent(o[1]))}i&&!i.startsWith("https://")&&(i=""),(i.includes("localhost")||i.includes("127.0.0.1"))&&(i="");const n=String(s(e,"stationLabel")||"");return r.updatePresence({title:String(s(e,"title")||"Radio").slice(0,128),episode:n.slice(0,64),totalEpisodes:"",stateLine:(n||"Radio").slice(0,64),currentTime:Number(s(e,"currentTime"))||0,duration:0,thumbnail:i,isPlaying:!!s(e,"isPlaying"),providerName:"Radio",sessionId:typeof t=="string"?t:void 0,isAdult:!1}),{success:!0}}),musicPresence:c.input(u()).mutation(async({input:e})=>{if(!r.isServiceEnabled)return{success:!0};const t=s(e,"sessionId");typeof t=="string"&&r.heartbeat(t);let i=String(s(e,"thumbnail")||"");if(i.includes("/api/image-proxy")){const o=i.match(/url=([^&]+)/);o&&(i=decodeURIComponent(o[1]))}i&&!i.startsWith("https://")&&(i=""),(i.includes("localhost")||i.includes("127.0.0.1"))&&(i="");const n=String(s(e,"artistLabel")||"");return r.updatePresence({title:String(s(e,"title")||"Music").slice(0,128),episode:n.slice(0,64),totalEpisodes:"",stateLine:(n||"Music").slice(0,64),currentTime:Number(s(e,"currentTime"))||0,duration:Number(s(e,"duration"))||0,thumbnail:i,isPlaying:!!s(e,"isPlaying"),providerName:"Music",sessionId:typeof t=="string"?t:void 0,isAdult:!1}),{success:!0}}),mangaPresence:c.input(u()).mutation(async({input:e})=>{if(!r.isServiceEnabled)return{success:!0};const t=s(e,"sessionId");typeof t=="string"&&r.heartbeat(t);const i=s(e,"thumbnails"),n=s(e,"title");let o=k(s(e,"thumbnail")),a=Array.isArray(i)?i.map(k).filter(Boolean):void 0;if(!o&&n){const l=await w(String(n));l&&(o=l,a=[l,...a||[]])}const d=String(s(e,"chapterLabel")||"");return r.updatePresence({title:String(n||"Manga").slice(0,128),episode:d.slice(0,64),totalEpisodes:"",stateLine:(d?`Reading ${d}`:"Reading").slice(0,64),currentTime:0,duration:0,thumbnail:o,thumbnails:a,isPlaying:s(e,"isPlaying")!==!1,providerName:"Manga",sessionId:typeof t=="string"?t:void 0,isAdult:s(e,"isAdult")===!0}),{success:!0}}),tvPresence:c.input(u()).mutation(async({input:e})=>{if(!r.isServiceEnabled)return{success:!0};const t=s(e,"sessionId");typeof t=="string"&&r.heartbeat(t);const i=s(e,"thumbnail");let n=String(i||"");if(n.includes("/api/image-proxy")){const a=n.match(/url=([^&]+)/);a&&(n=decodeURIComponent(a[1]))}(n.includes("localhost")||n.includes("127.0.0.1"))&&(n="");const o=String(s(e,"episodeLabel")||"");return r.updatePresence({title:String(s(e,"title")||"TV").slice(0,128),episode:o.slice(0,64),totalEpisodes:"",stateLine:(o||"Movie").slice(0,64),currentTime:Number(s(e,"currentTime"))||0,duration:Number(s(e,"duration"))||0,thumbnail:n,isPlaying:!!s(e,"isPlaying"),providerName:"TV",sessionId:typeof t=="string"?t:void 0,isAdult:s(e,"isAdult")===!0}),{success:!0}}),gatewayStatus:c.query(()=>{const e=m.hasToken();return{hasToken:e,masked:e?g(process.env.DISCORD_GATEWAY_TOKEN):null,enabled:m.serviceEnabled}}),gatewaySave:c.input(h(e=>{if(e==null)return{};const t=p(e),i={},n=v(t,"token");return n!==void 0&&(i.token=n),i})).mutation(async({input:e})=>{let t=(e.token||"").trim();if(t.startsWith('"')&&t.endsWith('"')&&(t=t.slice(1,-1)),t.length<30)throw P("Token too short");try{return await S({DISCORD_GATEWAY_TOKEN:t}),m.reloadToken(),f.info(`Discord Gateway token saved ${g(t)} (not logged)`),{ok:!0,masked:g(t)}}catch(i){throw f.error({err:i},"Failed to save Discord Gateway token"),y("Failed to save token")}}),gatewayRemove:c.mutation(async()=>{try{return await S({DISCORD_GATEWAY_TOKEN:""}),m.reloadToken(),f.info("Discord Gateway token removed"),{ok:!0}}catch(e){throw f.error({err:e},"Failed to remove Discord Gateway token"),y("Failed to remove token")}})});export{G as discordRouter};
|
|
@@ -1,11 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { getWatchlistDiscoveryStatus, triggerWatchlistDiscovery } from '../../hono/watchlist.js';
|
|
3
|
-
export const discoveryRouter = router({
|
|
4
|
-
status: protectedProcedure.query(() => {
|
|
5
|
-
return getWatchlistDiscoveryStatus();
|
|
6
|
-
}),
|
|
7
|
-
refresh: protectedProcedure.mutation(() => {
|
|
8
|
-
const started = triggerWatchlistDiscovery();
|
|
9
|
-
return { success: true, started, ...getWatchlistDiscoveryStatus() };
|
|
10
|
-
}),
|
|
11
|
-
});
|
|
1
|
+
import{protectedProcedure as t,router as e}from"../index.js";import{getWatchlistDiscoveryStatus as r,triggerWatchlistDiscovery as s}from"../../hono/watchlist.js";const i=e({status:t.query(()=>r()),refresh:t.mutation(()=>({success:!0,started:s(),...r()}))});export{i as discoveryRouter};
|
|
@@ -1,14 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { getWatchInsights, getGenreCards } from '../../lib/insights.js';
|
|
3
|
-
import { getCachedRecommendations } from '../../lib/recommendations.service.js';
|
|
4
|
-
export const insightsRouter = router({
|
|
5
|
-
summary: protectedProcedure.query(async ({ ctx }) => {
|
|
6
|
-
return getWatchInsights(ctx.db);
|
|
7
|
-
}),
|
|
8
|
-
genreCards: protectedProcedure.query(async ({ ctx }) => {
|
|
9
|
-
return getGenreCards(ctx.db);
|
|
10
|
-
}),
|
|
11
|
-
recommendations: protectedProcedure.query(async ({ ctx }) => {
|
|
12
|
-
return { candidates: await getCachedRecommendations(ctx.db) };
|
|
13
|
-
}),
|
|
14
|
-
});
|
|
1
|
+
import{protectedProcedure as e,router as t}from"../index.js";import{getWatchInsights as n,getGenreCards as o}from"../../lib/insights.js";import{getCachedRecommendations as a}from"../../lib/recommendations.service.js";const c=t({summary:e.query(async({ctx:r})=>n(r.db)),genreCards:e.query(async({ctx:r})=>o(r.db)),recommendations:e.query(async({ctx:r})=>({candidates:await a(r.db)}))});export{c as insightsRouter};
|
|
@@ -1,263 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { protectedProcedure, router } from '../index.js';
|
|
3
|
-
import { performMangaWriteTransactionAsync } from '../../sync.js';
|
|
4
|
-
import { buildMangaId, MANGA_STATUSES, MangaLibraryRepository, MangaProgressRepository, } from '../../repositories/manga.repository.js';
|
|
5
|
-
import { badRequest, defineSchema, failed, optStr, readCount, reqObj, reqStr, } from '../validation.js';
|
|
6
|
-
function mangaDb(ctx) {
|
|
7
|
-
const db = ctx.mangaDb;
|
|
8
|
-
if (!db || db.isClosedCheck())
|
|
9
|
-
throw new Error('Manga database is not ready');
|
|
10
|
-
return db;
|
|
11
|
-
}
|
|
12
|
-
function normalizeId(provider, mangaId, idRaw) {
|
|
13
|
-
if (idRaw && idRaw.includes(':'))
|
|
14
|
-
return idRaw;
|
|
15
|
-
return buildMangaId(provider, mangaId || idRaw || '');
|
|
16
|
-
}
|
|
17
|
-
const mangaLibraryListInput = () => defineSchema((value) => {
|
|
18
|
-
const obj = reqObj(value);
|
|
19
|
-
const status = optStr(obj, 'status');
|
|
20
|
-
const page = Math.max(readCount(obj, 'page', 1), 1);
|
|
21
|
-
const limit = Math.min(Math.max(readCount(obj, 'limit', 24), 1), 100);
|
|
22
|
-
return { status, page, limit };
|
|
23
|
-
});
|
|
24
|
-
const mangaLibraryIdInput = () => defineSchema((value) => ({
|
|
25
|
-
id: reqStr(reqObj(value), 'id'),
|
|
26
|
-
}));
|
|
27
|
-
const mangaLibraryAddInput = () => defineSchema((value) => {
|
|
28
|
-
const obj = reqObj(value);
|
|
29
|
-
const out = {
|
|
30
|
-
provider: optStr(obj, 'provider'),
|
|
31
|
-
mangaId: optStr(obj, 'mangaId'),
|
|
32
|
-
id: optStr(obj, 'id'),
|
|
33
|
-
title: optStr(obj, 'title'),
|
|
34
|
-
cover: optStr(obj, 'cover'),
|
|
35
|
-
status: optStr(obj, 'status'),
|
|
36
|
-
author: optStr(obj, 'author'),
|
|
37
|
-
altTitle: optStr(obj, 'altTitle'),
|
|
38
|
-
contentRating: optStr(obj, 'contentRating'),
|
|
39
|
-
};
|
|
40
|
-
if (typeof obj.silent === 'boolean')
|
|
41
|
-
out.silent = obj.silent;
|
|
42
|
-
return out;
|
|
43
|
-
});
|
|
44
|
-
const mangaLibraryRemoveInput = () => defineSchema((value) => {
|
|
45
|
-
if (typeof value !== 'string')
|
|
46
|
-
throw new Error('id must be a string');
|
|
47
|
-
return value;
|
|
48
|
-
});
|
|
49
|
-
const mangaLibraryStatusInput = () => defineSchema((value) => {
|
|
50
|
-
const obj = reqObj(value);
|
|
51
|
-
return { id: optStr(obj, 'id'), status: optStr(obj, 'status') };
|
|
52
|
-
});
|
|
53
|
-
const mangaLibraryLinkInput = () => defineSchema((value) => {
|
|
54
|
-
const obj = reqObj(value);
|
|
55
|
-
return { id: optStr(obj, 'id'), anilistId: obj.anilistId };
|
|
56
|
-
});
|
|
57
|
-
const mangaLibraryBatchStatusInput = () => defineSchema((value) => {
|
|
58
|
-
const obj = reqObj(value);
|
|
59
|
-
const idsRaw = obj.ids;
|
|
60
|
-
const ids = Array.isArray(idsRaw) ? idsRaw.map((id) => String(id)) : [];
|
|
61
|
-
return { ids, status: optStr(obj, 'status') };
|
|
62
|
-
});
|
|
63
|
-
const mangaLibraryRemoveManyInput = () => defineSchema((value) => {
|
|
64
|
-
if (!Array.isArray(value))
|
|
65
|
-
throw new Error('ids must be a non-empty array');
|
|
66
|
-
return value.map((id) => String(id));
|
|
67
|
-
});
|
|
68
|
-
export const mangaLibraryRouter = router({
|
|
69
|
-
list: protectedProcedure.input(mangaLibraryListInput()).query(async ({ ctx, input }) => {
|
|
70
|
-
try {
|
|
71
|
-
const offset = (input.page - 1) * input.limit;
|
|
72
|
-
const db = mangaDb(ctx);
|
|
73
|
-
const rows = await MangaLibraryRepository.getAll(db, input.status, input.limit, offset);
|
|
74
|
-
const total = await MangaLibraryRepository.getCount(db, input.status);
|
|
75
|
-
return { data: rows, total, page: input.page, limit: input.limit };
|
|
76
|
-
}
|
|
77
|
-
catch (err) {
|
|
78
|
-
if (err instanceof TRPCError)
|
|
79
|
-
throw err;
|
|
80
|
-
throw failed('Failed to load manga library');
|
|
81
|
-
}
|
|
82
|
-
}),
|
|
83
|
-
ids: protectedProcedure.query(async ({ ctx }) => {
|
|
84
|
-
try {
|
|
85
|
-
return { ids: await MangaLibraryRepository.getIds(mangaDb(ctx)) };
|
|
86
|
-
}
|
|
87
|
-
catch {
|
|
88
|
-
return { ids: [] };
|
|
89
|
-
}
|
|
90
|
-
}),
|
|
91
|
-
check: protectedProcedure.input(mangaLibraryIdInput()).query(async ({ ctx, input }) => {
|
|
92
|
-
try {
|
|
93
|
-
const item = await MangaLibraryRepository.getById(mangaDb(ctx), input.id);
|
|
94
|
-
return { inLibrary: !!item, status: item?.status ?? null };
|
|
95
|
-
}
|
|
96
|
-
catch {
|
|
97
|
-
return { inLibrary: false, status: null };
|
|
98
|
-
}
|
|
99
|
-
}),
|
|
100
|
-
entry: protectedProcedure.input(mangaLibraryIdInput()).query(async ({ ctx, input }) => {
|
|
101
|
-
try {
|
|
102
|
-
const item = await MangaLibraryRepository.getById(mangaDb(ctx), input.id);
|
|
103
|
-
if (!item)
|
|
104
|
-
throw new TRPCError({ code: 'NOT_FOUND', message: 'Not in library' });
|
|
105
|
-
return { item };
|
|
106
|
-
}
|
|
107
|
-
catch (err) {
|
|
108
|
-
if (err instanceof TRPCError)
|
|
109
|
-
throw err;
|
|
110
|
-
throw failed('Failed to load entry');
|
|
111
|
-
}
|
|
112
|
-
}),
|
|
113
|
-
add: protectedProcedure.input(mangaLibraryAddInput()).mutation(async ({ ctx, input }) => {
|
|
114
|
-
const provider = input.provider || '';
|
|
115
|
-
const mangaId = input.mangaId || '';
|
|
116
|
-
const idRaw = input.id || '';
|
|
117
|
-
if (!provider || (!mangaId && !idRaw)) {
|
|
118
|
-
throw badRequest('provider and mangaId are required');
|
|
119
|
-
}
|
|
120
|
-
const id = normalizeId(String(provider), String(mangaId || ''), idRaw ? String(idRaw) : undefined);
|
|
121
|
-
const cleanStatus = typeof input.status === 'string' && MANGA_STATUSES.includes(input.status)
|
|
122
|
-
? input.status
|
|
123
|
-
: 'Reading';
|
|
124
|
-
try {
|
|
125
|
-
await performMangaWriteTransactionAsync(mangaDb(ctx), async (tx) => {
|
|
126
|
-
await MangaLibraryRepository.upsert(tx, {
|
|
127
|
-
id,
|
|
128
|
-
provider: String(provider).toLowerCase(),
|
|
129
|
-
mangaId: String(mangaId || idRaw || ''),
|
|
130
|
-
title: String(input.title || ''),
|
|
131
|
-
cover: String(input.cover || ''),
|
|
132
|
-
status: cleanStatus,
|
|
133
|
-
author: input.author ? String(input.author) : undefined,
|
|
134
|
-
altTitle: input.altTitle ? String(input.altTitle) : undefined,
|
|
135
|
-
contentRating: input.contentRating ? String(input.contentRating) : undefined,
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
return { success: true, id };
|
|
139
|
-
}
|
|
140
|
-
catch (err) {
|
|
141
|
-
if (err instanceof TRPCError)
|
|
142
|
-
throw err;
|
|
143
|
-
throw failed('Failed to bookmark manga');
|
|
144
|
-
}
|
|
145
|
-
}),
|
|
146
|
-
remove: protectedProcedure.input(mangaLibraryRemoveInput()).mutation(async ({ ctx, input }) => {
|
|
147
|
-
if (!input)
|
|
148
|
-
throw badRequest('id is required');
|
|
149
|
-
try {
|
|
150
|
-
await performMangaWriteTransactionAsync(mangaDb(ctx), async (tx) => {
|
|
151
|
-
await MangaLibraryRepository.delete(tx, String(input));
|
|
152
|
-
await MangaProgressRepository.deleteByManga(tx, String(input));
|
|
153
|
-
});
|
|
154
|
-
return { success: true };
|
|
155
|
-
}
|
|
156
|
-
catch (err) {
|
|
157
|
-
if (err instanceof TRPCError)
|
|
158
|
-
throw err;
|
|
159
|
-
throw failed('Failed to remove bookmark');
|
|
160
|
-
}
|
|
161
|
-
}),
|
|
162
|
-
setStatus: protectedProcedure
|
|
163
|
-
.input(mangaLibraryStatusInput())
|
|
164
|
-
.mutation(async ({ ctx, input }) => {
|
|
165
|
-
if (!input.id || !input.status || !MANGA_STATUSES.includes(input.status)) {
|
|
166
|
-
throw badRequest('id and a valid status are required');
|
|
167
|
-
}
|
|
168
|
-
try {
|
|
169
|
-
await performMangaWriteTransactionAsync(mangaDb(ctx), async (tx) => {
|
|
170
|
-
await MangaLibraryRepository.updateStatus(tx, String(input.id), String(input.status));
|
|
171
|
-
});
|
|
172
|
-
return { success: true };
|
|
173
|
-
}
|
|
174
|
-
catch (err) {
|
|
175
|
-
if (err instanceof TRPCError)
|
|
176
|
-
throw err;
|
|
177
|
-
throw failed('Failed to update status');
|
|
178
|
-
}
|
|
179
|
-
}),
|
|
180
|
-
link: protectedProcedure.input(mangaLibraryLinkInput()).mutation(async ({ ctx, input }) => {
|
|
181
|
-
const targetId = typeof input.id === 'string' ? input.id : '';
|
|
182
|
-
const mediaId = Number(input.anilistId);
|
|
183
|
-
if (!targetId || !Number.isInteger(mediaId) || mediaId <= 0) {
|
|
184
|
-
throw badRequest('id and a numeric anilistId are required');
|
|
185
|
-
}
|
|
186
|
-
try {
|
|
187
|
-
const db = mangaDb(ctx);
|
|
188
|
-
const row = await MangaLibraryRepository.getById(db, targetId);
|
|
189
|
-
if (!row)
|
|
190
|
-
throw new TRPCError({ code: 'NOT_FOUND', message: 'Library entry not found' });
|
|
191
|
-
if (String(row.provider).toLowerCase() === 'anilist') {
|
|
192
|
-
throw badRequest('Link a provider bookmark to the AniList entry, not the reverse');
|
|
193
|
-
}
|
|
194
|
-
let progressMigrated = false;
|
|
195
|
-
await performMangaWriteTransactionAsync(db, async (tx) => {
|
|
196
|
-
await MangaLibraryRepository.setAnilistId(tx, targetId, mediaId, 'manual');
|
|
197
|
-
const orphanId = `anilist:${mediaId}`;
|
|
198
|
-
if (orphanId !== targetId) {
|
|
199
|
-
const orphan = await MangaLibraryRepository.getById(tx, orphanId);
|
|
200
|
-
const target = await MangaLibraryRepository.getById(tx, targetId);
|
|
201
|
-
const targetProgress = await MangaProgressRepository.getByManga(tx, targetId);
|
|
202
|
-
if (orphan?.lastChapterNumber &&
|
|
203
|
-
!target?.lastChapterNumber &&
|
|
204
|
-
targetProgress.length === 0) {
|
|
205
|
-
await MangaLibraryRepository.setProgressPointer(tx, targetId, orphan.lastChapterNumber);
|
|
206
|
-
await MangaProgressRepository.moveSyntheticChapters(tx, orphanId, targetId);
|
|
207
|
-
progressMigrated = true;
|
|
208
|
-
}
|
|
209
|
-
await MangaLibraryRepository.delete(tx, orphanId);
|
|
210
|
-
await MangaProgressRepository.deleteByManga(tx, orphanId);
|
|
211
|
-
}
|
|
212
|
-
});
|
|
213
|
-
return { success: true, id: targetId, progressMigrated };
|
|
214
|
-
}
|
|
215
|
-
catch (err) {
|
|
216
|
-
if (err instanceof TRPCError)
|
|
217
|
-
throw err;
|
|
218
|
-
throw failed('Failed to link entry');
|
|
219
|
-
}
|
|
220
|
-
}),
|
|
221
|
-
batchStatus: protectedProcedure
|
|
222
|
-
.input(mangaLibraryBatchStatusInput())
|
|
223
|
-
.mutation(async ({ ctx, input }) => {
|
|
224
|
-
if (!Array.isArray(input.ids) || input.ids.length === 0) {
|
|
225
|
-
throw badRequest('ids must be a non-empty array');
|
|
226
|
-
}
|
|
227
|
-
if (!input.status || !MANGA_STATUSES.includes(input.status)) {
|
|
228
|
-
throw badRequest('a valid status is required');
|
|
229
|
-
}
|
|
230
|
-
const ids = input.ids.map((id) => String(id));
|
|
231
|
-
try {
|
|
232
|
-
await performMangaWriteTransactionAsync(mangaDb(ctx), async (tx) => {
|
|
233
|
-
await MangaLibraryRepository.updateStatusMany(tx, ids, String(input.status));
|
|
234
|
-
});
|
|
235
|
-
return { success: true, updated: ids.length };
|
|
236
|
-
}
|
|
237
|
-
catch (err) {
|
|
238
|
-
if (err instanceof TRPCError)
|
|
239
|
-
throw err;
|
|
240
|
-
throw failed('Failed to update statuses');
|
|
241
|
-
}
|
|
242
|
-
}),
|
|
243
|
-
removeMany: protectedProcedure
|
|
244
|
-
.input(mangaLibraryRemoveManyInput())
|
|
245
|
-
.mutation(async ({ ctx, input }) => {
|
|
246
|
-
if (!Array.isArray(input) || input.length === 0) {
|
|
247
|
-
throw badRequest('ids must be a non-empty array');
|
|
248
|
-
}
|
|
249
|
-
const ids = input.map((id) => String(id));
|
|
250
|
-
try {
|
|
251
|
-
await performMangaWriteTransactionAsync(mangaDb(ctx), async (tx) => {
|
|
252
|
-
await MangaLibraryRepository.deleteMany(tx, ids);
|
|
253
|
-
await MangaProgressRepository.deleteMany(tx, ids);
|
|
254
|
-
});
|
|
255
|
-
return { success: true, removed: ids.length };
|
|
256
|
-
}
|
|
257
|
-
catch (err) {
|
|
258
|
-
if (err instanceof TRPCError)
|
|
259
|
-
throw err;
|
|
260
|
-
throw failed('Failed to remove bookmarks');
|
|
261
|
-
}
|
|
262
|
-
}),
|
|
263
|
-
});
|
|
1
|
+
import{TRPCError as d}from"@trpc/server";import{protectedProcedure as c,router as k}from"../index.js";import{performMangaWriteTransactionAsync as h}from"../../sync.js";import{buildMangaId as R,MANGA_STATUSES as I,MangaLibraryRepository as i,MangaProgressRepository as f}from"../../repositories/manga.repository.js";import{badRequest as g,defineSchema as m,failed as y,optStr as s,readCount as v,reqObj as w,reqStr as q}from"../validation.js";function u(r){const t=r.mangaDb;if(!t||t.isClosedCheck())throw new Error("Manga database is not ready");return t}function T(r,t,a){return a&&a.includes(":")?a:R(r,t||a||"")}const C=()=>m(r=>{const t=w(r),a=s(t,"status"),n=Math.max(v(t,"page",1),1),e=Math.min(Math.max(v(t,"limit",24),1),100);return{status:a,page:n,limit:e}}),L=()=>m(r=>({id:q(w(r),"id")})),N=()=>m(r=>{const t=w(r),a={provider:s(t,"provider"),mangaId:s(t,"mangaId"),id:s(t,"id"),title:s(t,"title"),cover:s(t,"cover"),status:s(t,"status"),author:s(t,"author"),altTitle:s(t,"altTitle"),contentRating:s(t,"contentRating")};return typeof t.silent=="boolean"&&(a.silent=t.silent),a}),F=()=>m(r=>{if(typeof r!="string")throw new Error("id must be a string");return r}),B=()=>m(r=>{const t=w(r);return{id:s(t,"id"),status:s(t,"status")}}),D=()=>m(r=>{const t=w(r);return{id:s(t,"id"),anilistId:t.anilistId}}),j=()=>m(r=>{const t=w(r),a=t.ids;return{ids:Array.isArray(a)?a.map(e=>String(e)):[],status:s(t,"status")}}),P=()=>m(r=>{if(!Array.isArray(r))throw new Error("ids must be a non-empty array");return r.map(t=>String(t))}),z=k({list:c.input(C()).query(async({ctx:r,input:t})=>{try{const a=(t.page-1)*t.limit,n=u(r),e=await i.getAll(n,t.status,t.limit,a),l=await i.getCount(n,t.status);return{data:e,total:l,page:t.page,limit:t.limit}}catch(a){throw a instanceof d?a:y("Failed to load manga library")}}),ids:c.query(async({ctx:r})=>{try{return{ids:await i.getIds(u(r))}}catch{return{ids:[]}}}),check:c.input(L()).query(async({ctx:r,input:t})=>{try{const a=await i.getById(u(r),t.id);return{inLibrary:!!a,status:a?.status??null}}catch{return{inLibrary:!1,status:null}}}),entry:c.input(L()).query(async({ctx:r,input:t})=>{try{const a=await i.getById(u(r),t.id);if(!a)throw new d({code:"NOT_FOUND",message:"Not in library"});return{item:a}}catch(a){throw a instanceof d?a:y("Failed to load entry")}}),add:c.input(N()).mutation(async({ctx:r,input:t})=>{const a=t.provider||"",n=t.mangaId||"",e=t.id||"";if(!a||!n&&!e)throw g("provider and mangaId are required");const l=T(String(a),String(n||""),e?String(e):void 0),p=typeof t.status=="string"&&I.includes(t.status)?t.status:"Reading";try{return await h(u(r),async o=>{await i.upsert(o,{id:l,provider:String(a).toLowerCase(),mangaId:String(n||e||""),title:String(t.title||""),cover:String(t.cover||""),status:p,author:t.author?String(t.author):void 0,altTitle:t.altTitle?String(t.altTitle):void 0,contentRating:t.contentRating?String(t.contentRating):void 0})}),{success:!0,id:l}}catch(o){throw o instanceof d?o:y("Failed to bookmark manga")}}),remove:c.input(F()).mutation(async({ctx:r,input:t})=>{if(!t)throw g("id is required");try{return await h(u(r),async a=>{await i.delete(a,String(t)),await f.deleteByManga(a,String(t))}),{success:!0}}catch(a){throw a instanceof d?a:y("Failed to remove bookmark")}}),setStatus:c.input(B()).mutation(async({ctx:r,input:t})=>{if(!t.id||!t.status||!I.includes(t.status))throw g("id and a valid status are required");try{return await h(u(r),async a=>{await i.updateStatus(a,String(t.id),String(t.status))}),{success:!0}}catch(a){throw a instanceof d?a:y("Failed to update status")}}),link:c.input(D()).mutation(async({ctx:r,input:t})=>{const a=typeof t.id=="string"?t.id:"",n=Number(t.anilistId);if(!a||!Number.isInteger(n)||n<=0)throw g("id and a numeric anilistId are required");try{const e=u(r),l=await i.getById(e,a);if(!l)throw new d({code:"NOT_FOUND",message:"Library entry not found"});if(String(l.provider).toLowerCase()==="anilist")throw g("Link a provider bookmark to the AniList entry, not the reverse");let p=!1;return await h(e,async o=>{await i.setAnilistId(o,a,n,"manual");const b=`anilist:${n}`;if(b!==a){const S=await i.getById(o,b),M=await i.getById(o,a),A=await f.getByManga(o,a);S?.lastChapterNumber&&!M?.lastChapterNumber&&A.length===0&&(await i.setProgressPointer(o,a,S.lastChapterNumber),await f.moveSyntheticChapters(o,b,a),p=!0),await i.delete(o,b),await f.deleteByManga(o,b)}}),{success:!0,id:a,progressMigrated:p}}catch(e){throw e instanceof d?e:y("Failed to link entry")}}),batchStatus:c.input(j()).mutation(async({ctx:r,input:t})=>{if(!Array.isArray(t.ids)||t.ids.length===0)throw g("ids must be a non-empty array");if(!t.status||!I.includes(t.status))throw g("a valid status is required");const a=t.ids.map(n=>String(n));try{return await h(u(r),async n=>{await i.updateStatusMany(n,a,String(t.status))}),{success:!0,updated:a.length}}catch(n){throw n instanceof d?n:y("Failed to update statuses")}}),removeMany:c.input(P()).mutation(async({ctx:r,input:t})=>{if(!Array.isArray(t)||t.length===0)throw g("ids must be a non-empty array");const a=t.map(n=>String(n));try{return await h(u(r),async n=>{await i.deleteMany(n,a),await f.deleteMany(n,a)}),{success:!0,removed:a.length}}catch(n){throw n instanceof d?n:y("Failed to remove bookmarks")}})});export{z as mangaLibraryRouter};
|