@serifpersia/dango 3.6.5 → 3.6.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/client/dist/assets/{AnimeCard-JkXG40Es.js → AnimeCard-DraA_TF9.js} +1 -1
- package/client/dist/assets/{AnimeInfo-2L0UJkbB.js → AnimeInfo-BJ7pzs-P.js} +1 -1
- package/client/dist/assets/{AnimeInfoPage-BDs03vKk.js → AnimeInfoPage-B_esUr5C.js} +2 -2
- package/client/dist/assets/{Asmr-D2mVKEl9.js → Asmr-Ckuh2xfW.js} +1 -1
- package/client/dist/assets/{AsmrResetProgressModal-MQA_bNQM.js → AsmrResetProgressModal-BS6UFJN4.js} +1 -1
- package/client/dist/assets/AvSyncCalibrator-DwHCd9yB.js +10 -0
- package/client/dist/assets/{Home-DxMBHFmc.js → Home-DuM1EuvN.js} +1 -1
- package/client/dist/assets/{Insights-hmLWsSXY.js → Insights-D3Pd9c9V.js} +1 -1
- package/client/dist/assets/{LibraryListPage-CWI7hgUh.js → LibraryListPage-Dm2yVn0-.js} +1 -1
- package/client/dist/assets/{ListeningList-NOc28KAV.js → ListeningList-D-zt6EXZ.js} +1 -1
- package/client/dist/assets/{Manga-B5qNhI7v.js → Manga-JwprMKBk.js} +1 -1
- package/client/dist/assets/{MangaInfoPage-D4wIyb-V.js → MangaInfoPage-DaeyJl2p.js} +1 -1
- package/client/dist/assets/{MangaReadPage-Ce1VX7-0.js → MangaReadPage-COg0fHI9.js} +1 -1
- package/client/dist/assets/{MangaResetProgressModal-P3oY4_14.js → MangaResetProgressModal-kguXv0_M.js} +1 -1
- package/client/dist/assets/{Mature-D8EwGYQq.js → Mature-IHDaasrs.js} +1 -1
- package/client/dist/assets/{MediaCard-Dp8DjyXB.js → MediaCard-CpwsrXo5.js} +1 -1
- package/client/dist/assets/{Music-DULtVsAM.js → Music-Dks4tPeK.js} +1 -1
- package/client/dist/assets/{OptionListMenu-fIHdE-nq.js → OptionListMenu-11CcAsy8.js} +1 -1
- package/client/dist/assets/{Player-B8XeOmso.js → Player-DQoEQkHL.js} +6 -6
- package/client/dist/assets/{PlayerSettings-BycNCiCs.js → PlayerSettings-D4Ud4kv4.js} +1 -1
- package/client/dist/assets/{QueueOptionsButton-T1J37IZT.js → QueueOptionsButton-B8PlPKjC.js} +1 -1
- package/client/dist/assets/{QueueRail-BAwI-apz.js → QueueRail-B4Dok7U3.js} +1 -1
- package/client/dist/assets/{Radio-Bv9DyKtZ.js → Radio-BHK6LSk4.js} +1 -1
- package/client/dist/assets/{ReadingList-Cl0CcVp3.js → ReadingList-BCsxIz57.js} +1 -1
- package/client/dist/assets/{ResetProgressModal--N60WWCB.js → ResetProgressModal-BXHxSfy-.js} +1 -1
- package/client/dist/assets/{Search-DvgVDlUR.js → Search-PK1ds07O.js} +1 -1
- package/client/dist/assets/{Settings-dCz_-bJn.js → Settings-Bxk_x55l.js} +1 -1
- package/client/dist/assets/{SkeletonGrid-BZHKk1I6.js → SkeletonGrid-CpJCZSRN.js} +1 -1
- package/client/dist/assets/{Trackers-C1T5VcPu.js → Trackers-RieCh_84.js} +1 -1
- package/client/dist/assets/{Tv-Dhv_cg4y.js → Tv-DLytPJUj.js} +4 -4
- package/client/dist/assets/{TvInfo-DgaLK2H6.js → TvInfo-C8djKebR.js} +1 -1
- package/client/dist/assets/{TvPopup-DyX7f8kC.js → TvPopup-40JGGJDk.js} +1 -1
- package/client/dist/assets/{TvSearch-Bp0H6kue.js → TvSearch-A3hbospf.js} +1 -1
- package/client/dist/assets/{TvWatchlist-ohMtMeKa.js → TvWatchlist-DuXjqmZM.js} +1 -1
- package/client/dist/assets/{Watchlist-BbQtzsER.js → Watchlist-B9wGBh0u.js} +1 -1
- package/client/dist/assets/{hls-Bi1cjpLZ.js → hls-CZ5X3Ya-.js} +1 -1
- package/client/dist/assets/{index-BTnhzofk.js → index-dRhc9-ol.js} +2 -2
- package/client/dist/assets/{useAnimeInfoData-Cvmso3IK.js → useAnimeInfoData-ZzDtLfvD.js} +1 -1
- package/client/dist/assets/{useAsmr-CbzGCzmv.js → useAsmr-CKOdHgol.js} +1 -1
- package/client/dist/assets/{useAsmrPopup-DaREZ9Ie.js → useAsmrPopup-SFHPhpcl.js} +1 -1
- package/client/dist/assets/{useMangaLibrary-BVQRhsFh.js → useMangaLibrary-CKATe31P.js} +1 -1
- package/client/dist/assets/{useMangaPopup-CzsCbndG.js → useMangaPopup-C75QuYgY.js} +1 -1
- package/client/dist/assets/{useProviders-UuUPzft-.js → useProviders-CMtvZbEi.js} +1 -1
- package/client/dist/assets/{useTv-CSsbWKH9.js → useTv-DUX4zYMr.js} +1 -1
- package/client/dist/assets/{useTvLibrary-a_Y0c2FS.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 -905
- package/server/dist/hono/tv.js +5 -169
- 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 -605
- package/server/dist/providers/remote-types.js +1 -124
- 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 -307
- 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
- package/client/dist/assets/AvSyncCalibrator-DmoH5eRA.js +0 -10
- package/server/dist/utils/megaplay.utils.js +0 -40
|
@@ -1,213 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { protectedProcedure, router } from '../index.js';
|
|
3
|
-
import { SettingsRepository } from '../../repositories/settings.repository.js';
|
|
4
|
-
import { performWriteTransactionAsync } from '../../sync.js';
|
|
5
|
-
import { AniListTracker } from '../../lib/tracker/anilist-tracker.js';
|
|
6
|
-
import { syncAniList, importFromMalUsername, importFromUsername, } from '../../lib/tracker/sync.service.js';
|
|
7
|
-
import { syncAniListManga, importFromUsernameManga, importMangaFromMalUsername, } from '../../lib/tracker/manga-sync.service.js';
|
|
8
|
-
import { badRequest, defineSchema, failed, reqObj } from '../validation.js';
|
|
9
|
-
const TRACKER_TOKEN_KEY = 'tracker_anilist_token';
|
|
10
|
-
const TRACKER_USER_KEY = 'tracker_anilist_user';
|
|
11
|
-
function unavailable(message) {
|
|
12
|
-
return new TRPCError({ code: 'SERVICE_UNAVAILABLE', message });
|
|
13
|
-
}
|
|
14
|
-
function optBody(value) {
|
|
15
|
-
if (value === undefined || value === null)
|
|
16
|
-
return {};
|
|
17
|
-
return reqObj(value);
|
|
18
|
-
}
|
|
19
|
-
const trackerTokenInput = () => defineSchema((value) => {
|
|
20
|
-
const obj = optBody(value);
|
|
21
|
-
return { token: obj.token, code: obj.code, redirectUri: obj.redirectUri };
|
|
22
|
-
});
|
|
23
|
-
const trackerSyncInput = () => defineSchema((value) => ({
|
|
24
|
-
provider: optBody(value).provider,
|
|
25
|
-
}));
|
|
26
|
-
const trackerUsernameInput = () => defineSchema((value) => {
|
|
27
|
-
const obj = optBody(value);
|
|
28
|
-
return { username: obj.username, erase: obj.erase };
|
|
29
|
-
});
|
|
30
|
-
const trackerMalInput = () => defineSchema((value) => {
|
|
31
|
-
const obj = optBody(value);
|
|
32
|
-
return {
|
|
33
|
-
username: obj.username,
|
|
34
|
-
erase: obj.erase,
|
|
35
|
-
useOfflineDb: obj.useOfflineDb,
|
|
36
|
-
skipFallback: obj.skipFallback,
|
|
37
|
-
};
|
|
38
|
-
});
|
|
39
|
-
const trackerMangaSyncInput = () => defineSchema((value) => ({
|
|
40
|
-
direction: optBody(value).direction,
|
|
41
|
-
}));
|
|
42
|
-
function mapImportError(message) {
|
|
43
|
-
if (message.includes('private') || message.includes('not found')) {
|
|
44
|
-
return new TRPCError({ code: 'NOT_FOUND', message });
|
|
45
|
-
}
|
|
46
|
-
if (message.includes('blocked') || message.includes('HTTP 429')) {
|
|
47
|
-
return new TRPCError({ code: 'TOO_MANY_REQUESTS', message });
|
|
48
|
-
}
|
|
49
|
-
return failed(message);
|
|
50
|
-
}
|
|
51
|
-
export const trackerRouter = router({
|
|
52
|
-
status: protectedProcedure.query(async ({ ctx }) => {
|
|
53
|
-
try {
|
|
54
|
-
const tokenRow = await SettingsRepository.getByKey(ctx.db, TRACKER_TOKEN_KEY);
|
|
55
|
-
const userRow = await SettingsRepository.getByKey(ctx.db, TRACKER_USER_KEY);
|
|
56
|
-
let user = null;
|
|
57
|
-
if (userRow?.value) {
|
|
58
|
-
try {
|
|
59
|
-
user = JSON.parse(userRow.value);
|
|
60
|
-
}
|
|
61
|
-
catch {
|
|
62
|
-
user = null;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return { anilist: { connected: !!tokenRow?.value, user } };
|
|
66
|
-
}
|
|
67
|
-
catch {
|
|
68
|
-
throw failed('Failed to read tracker status');
|
|
69
|
-
}
|
|
70
|
-
}),
|
|
71
|
-
anilistAuth: protectedProcedure.input(trackerTokenInput()).mutation(async ({ ctx, input }) => {
|
|
72
|
-
const accessToken = typeof input.token === 'string' ? input.token.trim() : '';
|
|
73
|
-
if (!accessToken) {
|
|
74
|
-
throw badRequest('Access token is required');
|
|
75
|
-
}
|
|
76
|
-
try {
|
|
77
|
-
const tracker = new AniListTracker(accessToken);
|
|
78
|
-
const viewer = await tracker.getViewer();
|
|
79
|
-
await performWriteTransactionAsync(ctx.db, async (tx) => {
|
|
80
|
-
await SettingsRepository.upsert(tx, TRACKER_TOKEN_KEY, accessToken);
|
|
81
|
-
await SettingsRepository.upsert(tx, TRACKER_USER_KEY, JSON.stringify(viewer));
|
|
82
|
-
});
|
|
83
|
-
return { success: true, user: viewer };
|
|
84
|
-
}
|
|
85
|
-
catch (err) {
|
|
86
|
-
throw new TRPCError({
|
|
87
|
-
code: 'UNAUTHORIZED',
|
|
88
|
-
message: err instanceof Error ? err.message : 'Authentication failed',
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
}),
|
|
92
|
-
anilistDisconnect: protectedProcedure.mutation(async ({ ctx }) => {
|
|
93
|
-
try {
|
|
94
|
-
await performWriteTransactionAsync(ctx.db, async (tx) => {
|
|
95
|
-
await SettingsRepository.upsert(tx, TRACKER_TOKEN_KEY, '');
|
|
96
|
-
await SettingsRepository.upsert(tx, TRACKER_USER_KEY, '');
|
|
97
|
-
});
|
|
98
|
-
return { success: true };
|
|
99
|
-
}
|
|
100
|
-
catch {
|
|
101
|
-
throw failed('Failed to disconnect');
|
|
102
|
-
}
|
|
103
|
-
}),
|
|
104
|
-
sync: protectedProcedure.input(trackerSyncInput()).mutation(async ({ ctx, input }) => {
|
|
105
|
-
const { provider = 'anilist' } = input;
|
|
106
|
-
if (provider !== 'anilist') {
|
|
107
|
-
throw badRequest(`Provider "${provider}" is not supported yet`);
|
|
108
|
-
}
|
|
109
|
-
try {
|
|
110
|
-
const summary = await syncAniList(ctx.db);
|
|
111
|
-
return { success: true, summary };
|
|
112
|
-
}
|
|
113
|
-
catch (err) {
|
|
114
|
-
throw failed(err instanceof Error ? err.message : 'Sync failed');
|
|
115
|
-
}
|
|
116
|
-
}),
|
|
117
|
-
anilistImport: protectedProcedure
|
|
118
|
-
.input(trackerUsernameInput())
|
|
119
|
-
.mutation(async ({ ctx, input }) => {
|
|
120
|
-
const { username, erase } = input;
|
|
121
|
-
if (!username || typeof username !== 'string') {
|
|
122
|
-
throw badRequest('Username is required');
|
|
123
|
-
}
|
|
124
|
-
try {
|
|
125
|
-
const count = await importFromUsername(ctx.db, username.trim(), erase === true);
|
|
126
|
-
return { success: true, count };
|
|
127
|
-
}
|
|
128
|
-
catch (err) {
|
|
129
|
-
throw failed(err instanceof Error ? err.message : 'Import failed');
|
|
130
|
-
}
|
|
131
|
-
}),
|
|
132
|
-
malImport: protectedProcedure.input(trackerMalInput()).mutation(async ({ ctx, input }) => {
|
|
133
|
-
const { username, erase, useOfflineDb, skipFallback } = input;
|
|
134
|
-
if (!username || typeof username !== 'string') {
|
|
135
|
-
throw badRequest('MAL username is required');
|
|
136
|
-
}
|
|
137
|
-
try {
|
|
138
|
-
const count = await importFromMalUsername(ctx.db, username.trim(), {
|
|
139
|
-
erase: erase === true,
|
|
140
|
-
useOfflineDb: useOfflineDb !== false,
|
|
141
|
-
skipFallback: skipFallback === true,
|
|
142
|
-
});
|
|
143
|
-
return { success: true, count };
|
|
144
|
-
}
|
|
145
|
-
catch (err) {
|
|
146
|
-
throw mapImportError(err instanceof Error ? err.message : 'Import failed');
|
|
147
|
-
}
|
|
148
|
-
}),
|
|
149
|
-
mangaSync: protectedProcedure.input(trackerMangaSyncInput()).mutation(async ({ ctx, input }) => {
|
|
150
|
-
const { direction } = input;
|
|
151
|
-
try {
|
|
152
|
-
const mangaDb = ctx.mangaDb;
|
|
153
|
-
if (!mangaDb || mangaDb.isClosedCheck()) {
|
|
154
|
-
throw unavailable('Manga database is not ready');
|
|
155
|
-
}
|
|
156
|
-
const syncDirection = direction === 'pull-only' ? 'pull-only' : 'two-way';
|
|
157
|
-
const result = await syncAniListManga(ctx.db, mangaDb, {
|
|
158
|
-
direction: syncDirection,
|
|
159
|
-
});
|
|
160
|
-
return {
|
|
161
|
-
success: true,
|
|
162
|
-
summary: result.summary,
|
|
163
|
-
details: result.details,
|
|
164
|
-
direction: syncDirection,
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
catch (err) {
|
|
168
|
-
if (err instanceof TRPCError)
|
|
169
|
-
throw err;
|
|
170
|
-
throw failed(err instanceof Error ? err.message : 'Manga sync failed');
|
|
171
|
-
}
|
|
172
|
-
}),
|
|
173
|
-
mangaImport: protectedProcedure.input(trackerUsernameInput()).mutation(async ({ ctx, input }) => {
|
|
174
|
-
const { username, erase } = input;
|
|
175
|
-
if (!username || typeof username !== 'string') {
|
|
176
|
-
throw badRequest('Username is required');
|
|
177
|
-
}
|
|
178
|
-
try {
|
|
179
|
-
const mangaDb = ctx.mangaDb;
|
|
180
|
-
if (!mangaDb || mangaDb.isClosedCheck()) {
|
|
181
|
-
throw unavailable('Manga database is not ready');
|
|
182
|
-
}
|
|
183
|
-
const count = await importFromUsernameManga(ctx.db, mangaDb, username.trim(), erase === true);
|
|
184
|
-
return { success: true, count };
|
|
185
|
-
}
|
|
186
|
-
catch (err) {
|
|
187
|
-
if (err instanceof TRPCError)
|
|
188
|
-
throw err;
|
|
189
|
-
throw failed(err instanceof Error ? err.message : 'Import failed');
|
|
190
|
-
}
|
|
191
|
-
}),
|
|
192
|
-
mangaMalImport: protectedProcedure
|
|
193
|
-
.input(trackerUsernameInput())
|
|
194
|
-
.mutation(async ({ ctx, input }) => {
|
|
195
|
-
const { username, erase } = input;
|
|
196
|
-
if (!username || typeof username !== 'string') {
|
|
197
|
-
throw badRequest('Username is required');
|
|
198
|
-
}
|
|
199
|
-
try {
|
|
200
|
-
const mangaDb = ctx.mangaDb;
|
|
201
|
-
if (!mangaDb || mangaDb.isClosedCheck()) {
|
|
202
|
-
throw unavailable('Manga database is not ready');
|
|
203
|
-
}
|
|
204
|
-
const result = await importMangaFromMalUsername(ctx.db, mangaDb, username.trim(), erase === true);
|
|
205
|
-
return { success: true, ...result };
|
|
206
|
-
}
|
|
207
|
-
catch (err) {
|
|
208
|
-
if (err instanceof TRPCError)
|
|
209
|
-
throw err;
|
|
210
|
-
throw mapImportError(err instanceof Error ? err.message : 'Import failed');
|
|
211
|
-
}
|
|
212
|
-
}),
|
|
213
|
-
});
|
|
1
|
+
import{TRPCError as i}from"@trpc/server";import{protectedProcedure as s,router as g}from"../index.js";import{SettingsRepository as u}from"../../repositories/settings.repository.js";import{performWriteTransactionAsync as h}from"../../sync.js";import{AniListTracker as T}from"../../lib/tracker/anilist-tracker.js";import{syncAniList as I,importFromMalUsername as E,importFromUsername as M}from"../../lib/tracker/sync.service.js";import{syncAniListManga as U,importFromUsernameManga as S,importMangaFromMalUsername as R}from"../../lib/tracker/manga-sync.service.js";import{badRequest as m,defineSchema as p,failed as c,reqObj as A}from"../validation.js";const k="tracker_anilist_token",f="tracker_anilist_user";function y(r){return new i({code:"SERVICE_UNAVAILABLE",message:r})}function d(r){return r==null?{}:A(r)}const D=()=>p(r=>{const e=d(r);return{token:e.token,code:e.code,redirectUri:e.redirectUri}}),O=()=>p(r=>({provider:d(r).provider})),w=()=>p(r=>{const e=d(r);return{username:e.username,erase:e.erase}}),C=()=>p(r=>{const e=d(r);return{username:e.username,erase:e.erase,useOfflineDb:e.useOfflineDb,skipFallback:e.skipFallback}}),F=()=>p(r=>({direction:d(r).direction}));function b(r){return r.includes("private")||r.includes("not found")?new i({code:"NOT_FOUND",message:r}):r.includes("blocked")||r.includes("HTTP 429")?new i({code:"TOO_MANY_REQUESTS",message:r}):c(r)}const B=g({status:s.query(async({ctx:r})=>{try{const e=await u.getByKey(r.db,k),a=await u.getByKey(r.db,f);let n=null;if(a?.value)try{n=JSON.parse(a.value)}catch{n=null}return{anilist:{connected:!!e?.value,user:n}}}catch{throw c("Failed to read tracker status")}}),anilistAuth:s.input(D()).mutation(async({ctx:r,input:e})=>{const a=typeof e.token=="string"?e.token.trim():"";if(!a)throw m("Access token is required");try{const t=await new T(a).getViewer();return await h(r.db,async o=>{await u.upsert(o,k,a),await u.upsert(o,f,JSON.stringify(t))}),{success:!0,user:t}}catch(n){throw new i({code:"UNAUTHORIZED",message:n instanceof Error?n.message:"Authentication failed"})}}),anilistDisconnect:s.mutation(async({ctx:r})=>{try{return await h(r.db,async e=>{await u.upsert(e,k,""),await u.upsert(e,f,"")}),{success:!0}}catch{throw c("Failed to disconnect")}}),sync:s.input(O()).mutation(async({ctx:r,input:e})=>{const{provider:a="anilist"}=e;if(a!=="anilist")throw m(`Provider "${a}" is not supported yet`);try{return{success:!0,summary:await I(r.db)}}catch(n){throw c(n instanceof Error?n.message:"Sync failed")}}),anilistImport:s.input(w()).mutation(async({ctx:r,input:e})=>{const{username:a,erase:n}=e;if(!a||typeof a!="string")throw m("Username is required");try{return{success:!0,count:await M(r.db,a.trim(),n===!0)}}catch(t){throw c(t instanceof Error?t.message:"Import failed")}}),malImport:s.input(C()).mutation(async({ctx:r,input:e})=>{const{username:a,erase:n,useOfflineDb:t,skipFallback:o}=e;if(!a||typeof a!="string")throw m("MAL username is required");try{return{success:!0,count:await E(r.db,a.trim(),{erase:n===!0,useOfflineDb:t!==!1,skipFallback:o===!0})}}catch(l){throw b(l instanceof Error?l.message:"Import failed")}}),mangaSync:s.input(F()).mutation(async({ctx:r,input:e})=>{const{direction:a}=e;try{const n=r.mangaDb;if(!n||n.isClosedCheck())throw y("Manga database is not ready");const t=a==="pull-only"?"pull-only":"two-way",o=await U(r.db,n,{direction:t});return{success:!0,summary:o.summary,details:o.details,direction:t}}catch(n){throw n instanceof i?n:c(n instanceof Error?n.message:"Manga sync failed")}}),mangaImport:s.input(w()).mutation(async({ctx:r,input:e})=>{const{username:a,erase:n}=e;if(!a||typeof a!="string")throw m("Username is required");try{const t=r.mangaDb;if(!t||t.isClosedCheck())throw y("Manga database is not ready");return{success:!0,count:await S(r.db,t,a.trim(),n===!0)}}catch(t){throw t instanceof i?t:c(t instanceof Error?t.message:"Import failed")}}),mangaMalImport:s.input(w()).mutation(async({ctx:r,input:e})=>{const{username:a,erase:n}=e;if(!a||typeof a!="string")throw m("Username is required");try{const t=r.mangaDb;if(!t||t.isClosedCheck())throw y("Manga database is not ready");return{success:!0,...await R(r.db,t,a.trim(),n===!0)}}catch(t){throw t instanceof i?t:b(t instanceof Error?t.message:"Import failed")}})});export{B as trackerRouter};
|
|
@@ -1,135 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { protectedProcedure, router } from '../index.js';
|
|
3
|
-
import { performTvWriteTransactionAsync } from '../../sync.js';
|
|
4
|
-
import { buildTvId, TV_STATUSES, TvLibraryRepository, TvProgressRepository, } from '../../repositories/tv.repository.js';
|
|
5
|
-
import { defineSchema, reqObj, reqStr, optStr, reqStrArray, readPageLimit } from '../validation.js';
|
|
6
|
-
function normalizeMediaType(raw) {
|
|
7
|
-
return String(raw).toLowerCase() === 'movie' ? 'movie' : 'tv';
|
|
8
|
-
}
|
|
9
|
-
const tvLibraryListInput = () => defineSchema((value) => {
|
|
10
|
-
const obj = reqObj(value);
|
|
11
|
-
const out = { ...readPageLimit(obj) };
|
|
12
|
-
const status = optStr(obj, 'status');
|
|
13
|
-
if (status !== undefined)
|
|
14
|
-
out.status = status;
|
|
15
|
-
return out;
|
|
16
|
-
});
|
|
17
|
-
const tvLibraryIdInput = () => defineSchema((value) => ({
|
|
18
|
-
id: reqStr(reqObj(value), 'id'),
|
|
19
|
-
}));
|
|
20
|
-
const tvLibraryAddInput = () => defineSchema((value) => {
|
|
21
|
-
const obj = reqObj(value);
|
|
22
|
-
return {
|
|
23
|
-
tmdbId: obj.tmdbId,
|
|
24
|
-
mediaType: obj.mediaType,
|
|
25
|
-
id: obj.id,
|
|
26
|
-
title: obj.title,
|
|
27
|
-
poster: obj.poster,
|
|
28
|
-
backdrop: obj.backdrop,
|
|
29
|
-
year: obj.year,
|
|
30
|
-
overview: obj.overview,
|
|
31
|
-
status: obj.status,
|
|
32
|
-
adult: obj.adult,
|
|
33
|
-
};
|
|
34
|
-
});
|
|
35
|
-
const tvLibraryRemoveInput = () => defineSchema((value) => {
|
|
36
|
-
if (typeof value !== 'string' || value.length === 0)
|
|
37
|
-
throw new Error('id must be a string');
|
|
38
|
-
return value;
|
|
39
|
-
});
|
|
40
|
-
const tvLibraryStatusInput = () => defineSchema((value) => {
|
|
41
|
-
const obj = reqObj(value);
|
|
42
|
-
return { id: reqStr(obj, 'id'), status: reqStr(obj, 'status') };
|
|
43
|
-
});
|
|
44
|
-
const tvLibraryBatchStatusInput = () => defineSchema((value) => {
|
|
45
|
-
const obj = reqObj(value);
|
|
46
|
-
return { ids: reqStrArray(obj, 'ids', true), status: reqStr(obj, 'status') };
|
|
47
|
-
});
|
|
48
|
-
const tvLibraryRemoveManyInput = () => defineSchema((value) => {
|
|
49
|
-
if (!Array.isArray(value) || value.length === 0)
|
|
50
|
-
throw new Error('ids must be a non-empty array');
|
|
51
|
-
return value.map((id) => String(id));
|
|
52
|
-
});
|
|
53
|
-
export const tvLibraryRouter = router({
|
|
54
|
-
list: protectedProcedure.input(tvLibraryListInput()).query(async ({ ctx, input }) => {
|
|
55
|
-
const page = Math.max(parseInt(String(input.page ?? 1)) || 1, 1);
|
|
56
|
-
const limit = Math.min(Math.max(parseInt(String(input.limit ?? 24)) || 24, 1), 100);
|
|
57
|
-
const offset = (page - 1) * limit;
|
|
58
|
-
const rows = await TvLibraryRepository.getAll(ctx.tvDb, input.status, limit, offset);
|
|
59
|
-
const total = await TvLibraryRepository.getCount(ctx.tvDb, input.status);
|
|
60
|
-
return { data: rows, total, page, limit };
|
|
61
|
-
}),
|
|
62
|
-
ids: protectedProcedure.query(async ({ ctx }) => {
|
|
63
|
-
return { ids: await TvLibraryRepository.getIds(ctx.tvDb) };
|
|
64
|
-
}),
|
|
65
|
-
check: protectedProcedure.input(tvLibraryIdInput()).query(async ({ ctx, input }) => {
|
|
66
|
-
const item = await TvLibraryRepository.getById(ctx.tvDb, input.id);
|
|
67
|
-
return { inLibrary: !!item, status: item?.status ?? null };
|
|
68
|
-
}),
|
|
69
|
-
add: protectedProcedure.input(tvLibraryAddInput()).mutation(async ({ ctx, input }) => {
|
|
70
|
-
const tmdbIdNum = Number(input.tmdbId);
|
|
71
|
-
if (!tmdbIdNum || (!input.id && !input.mediaType)) {
|
|
72
|
-
throw new TRPCError({ code: 'BAD_REQUEST', message: 'tmdbId and mediaType are required' });
|
|
73
|
-
}
|
|
74
|
-
const mediaType = normalizeMediaType(input.mediaType || (String(input.id).startsWith('movie:') ? 'movie' : 'tv'));
|
|
75
|
-
const id = input.id && String(input.id).includes(':')
|
|
76
|
-
? String(input.id)
|
|
77
|
-
: buildTvId(mediaType, tmdbIdNum);
|
|
78
|
-
const cleanStatus = typeof input.status === 'string' && TV_STATUSES.includes(input.status)
|
|
79
|
-
? input.status
|
|
80
|
-
: 'Watching';
|
|
81
|
-
await performTvWriteTransactionAsync(ctx.tvDb, async (tx) => {
|
|
82
|
-
await TvLibraryRepository.upsert(tx, {
|
|
83
|
-
id,
|
|
84
|
-
tmdbId: tmdbIdNum,
|
|
85
|
-
mediaType,
|
|
86
|
-
title: String(input.title || ''),
|
|
87
|
-
poster: String(input.poster || ''),
|
|
88
|
-
backdrop: input.backdrop ? String(input.backdrop) : undefined,
|
|
89
|
-
year: input.year ? String(input.year) : undefined,
|
|
90
|
-
overview: input.overview ? String(input.overview) : undefined,
|
|
91
|
-
status: cleanStatus,
|
|
92
|
-
adult: input.adult === true,
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
return { success: true, id };
|
|
96
|
-
}),
|
|
97
|
-
remove: protectedProcedure.input(tvLibraryRemoveInput()).mutation(async ({ ctx, input }) => {
|
|
98
|
-
await performTvWriteTransactionAsync(ctx.tvDb, async (tx) => {
|
|
99
|
-
await TvLibraryRepository.delete(tx, String(input));
|
|
100
|
-
await TvProgressRepository.deleteByMedia(tx, String(input));
|
|
101
|
-
});
|
|
102
|
-
return { success: true };
|
|
103
|
-
}),
|
|
104
|
-
setStatus: protectedProcedure.input(tvLibraryStatusInput()).mutation(async ({ ctx, input }) => {
|
|
105
|
-
if (!input.id || !TV_STATUSES.includes(input.status)) {
|
|
106
|
-
throw new TRPCError({ code: 'BAD_REQUEST', message: 'id and a valid status are required' });
|
|
107
|
-
}
|
|
108
|
-
await performTvWriteTransactionAsync(ctx.tvDb, async (tx) => {
|
|
109
|
-
await TvLibraryRepository.updateStatus(tx, String(input.id), String(input.status));
|
|
110
|
-
});
|
|
111
|
-
return { success: true };
|
|
112
|
-
}),
|
|
113
|
-
batchStatus: protectedProcedure
|
|
114
|
-
.input(tvLibraryBatchStatusInput())
|
|
115
|
-
.mutation(async ({ ctx, input }) => {
|
|
116
|
-
if (!input.status || !TV_STATUSES.includes(input.status)) {
|
|
117
|
-
throw new TRPCError({ code: 'BAD_REQUEST', message: 'a valid status is required' });
|
|
118
|
-
}
|
|
119
|
-
const ids = input.ids.map((id) => String(id));
|
|
120
|
-
await performTvWriteTransactionAsync(ctx.tvDb, async (tx) => {
|
|
121
|
-
await TvLibraryRepository.updateStatusMany(tx, ids, String(input.status));
|
|
122
|
-
});
|
|
123
|
-
return { success: true, updated: ids.length };
|
|
124
|
-
}),
|
|
125
|
-
removeMany: protectedProcedure
|
|
126
|
-
.input(tvLibraryRemoveManyInput())
|
|
127
|
-
.mutation(async ({ ctx, input }) => {
|
|
128
|
-
const ids = input.map((id) => String(id));
|
|
129
|
-
await performTvWriteTransactionAsync(ctx.tvDb, async (tx) => {
|
|
130
|
-
await TvLibraryRepository.deleteMany(tx, ids);
|
|
131
|
-
await TvProgressRepository.deleteMany(tx, ids);
|
|
132
|
-
});
|
|
133
|
-
return { success: true, removed: ids.length };
|
|
134
|
-
}),
|
|
135
|
-
});
|
|
1
|
+
import{TRPCError as b}from"@trpc/server";import{protectedProcedure as n,router as g}from"../index.js";import{performTvWriteTransactionAsync as o}from"../../sync.js";import{buildTvId as w,TV_STATUSES as v,TvLibraryRepository as s,TvProgressRepository as p}from"../../repositories/tv.repository.js";import{defineSchema as i,reqObj as u,reqStr as y,optStr as S,reqStrArray as I,readPageLimit as T}from"../validation.js";function L(r){return String(r).toLowerCase()==="movie"?"movie":"tv"}const l=()=>i(r=>{const t=u(r),a={...T(t)},e=S(t,"status");return e!==void 0&&(a.status=e),a}),f=()=>i(r=>({id:y(u(r),"id")})),h=()=>i(r=>{const t=u(r);return{tmdbId:t.tmdbId,mediaType:t.mediaType,id:t.id,title:t.title,poster:t.poster,backdrop:t.backdrop,year:t.year,overview:t.overview,status:t.status,adult:t.adult}}),k=()=>i(r=>{if(typeof r!="string"||r.length===0)throw new Error("id must be a string");return r}),A=()=>i(r=>{const t=u(r);return{id:y(t,"id"),status:y(t,"status")}}),D=()=>i(r=>{const t=u(r);return{ids:I(t,"ids",!0),status:y(t,"status")}}),E=()=>i(r=>{if(!Array.isArray(r)||r.length===0)throw new Error("ids must be a non-empty array");return r.map(t=>String(t))}),x=g({list:n.input(l()).query(async({ctx:r,input:t})=>{const a=Math.max(parseInt(String(t.page??1))||1,1),e=Math.min(Math.max(parseInt(String(t.limit??24))||24,1),100),d=(a-1)*e,c=await s.getAll(r.tvDb,t.status,e,d),m=await s.getCount(r.tvDb,t.status);return{data:c,total:m,page:a,limit:e}}),ids:n.query(async({ctx:r})=>({ids:await s.getIds(r.tvDb)})),check:n.input(f()).query(async({ctx:r,input:t})=>{const a=await s.getById(r.tvDb,t.id);return{inLibrary:!!a,status:a?.status??null}}),add:n.input(h()).mutation(async({ctx:r,input:t})=>{const a=Number(t.tmdbId);if(!a||!t.id&&!t.mediaType)throw new b({code:"BAD_REQUEST",message:"tmdbId and mediaType are required"});const e=L(t.mediaType||(String(t.id).startsWith("movie:")?"movie":"tv")),d=t.id&&String(t.id).includes(":")?String(t.id):w(e,a),c=typeof t.status=="string"&&v.includes(t.status)?t.status:"Watching";return await o(r.tvDb,async m=>{await s.upsert(m,{id:d,tmdbId:a,mediaType:e,title:String(t.title||""),poster:String(t.poster||""),backdrop:t.backdrop?String(t.backdrop):void 0,year:t.year?String(t.year):void 0,overview:t.overview?String(t.overview):void 0,status:c,adult:t.adult===!0})}),{success:!0,id:d}}),remove:n.input(k()).mutation(async({ctx:r,input:t})=>(await o(r.tvDb,async a=>{await s.delete(a,String(t)),await p.deleteByMedia(a,String(t))}),{success:!0})),setStatus:n.input(A()).mutation(async({ctx:r,input:t})=>{if(!t.id||!v.includes(t.status))throw new b({code:"BAD_REQUEST",message:"id and a valid status are required"});return await o(r.tvDb,async a=>{await s.updateStatus(a,String(t.id),String(t.status))}),{success:!0}}),batchStatus:n.input(D()).mutation(async({ctx:r,input:t})=>{if(!t.status||!v.includes(t.status))throw new b({code:"BAD_REQUEST",message:"a valid status is required"});const a=t.ids.map(e=>String(e));return await o(r.tvDb,async e=>{await s.updateStatusMany(e,a,String(t.status))}),{success:!0,updated:a.length}}),removeMany:n.input(E()).mutation(async({ctx:r,input:t})=>{const a=t.map(e=>String(e));return await o(r.tvDb,async e=>{await s.deleteMany(e,a),await p.deleteMany(e,a)}),{success:!0,removed:a.length}})});export{x as tvLibraryRouter};
|
|
@@ -1,159 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { protectedProcedure, router } from '../index.js';
|
|
3
|
-
import { performTvWriteTransactionAsync } from '../../sync.js';
|
|
4
|
-
import { adultNonListIds } from '../../utils/db-utils.js';
|
|
5
|
-
import { SettingsRepository } from '../../repositories/settings.repository.js';
|
|
6
|
-
import { normalizeTvMediaId, TvLibraryRepository, TvProgressRepository, } from '../../repositories/tv.repository.js';
|
|
7
|
-
import { defineSchema, reqObj, reqStr, strOrNull } from '../validation.js';
|
|
8
|
-
const isTvContinueAdult = (row) => row.adult === true || row.adult === 1;
|
|
9
|
-
const getAdultNonListMediaIds = (db) => adultNonListIds(db, 'tv_progress', 'tv_library', 'mediaId', 'COALESCE(l.adult, p.adult) = 1');
|
|
10
|
-
const tvProgressMediaInput = () => defineSchema((value) => ({
|
|
11
|
-
mediaId: reqStr(reqObj(value), 'mediaId'),
|
|
12
|
-
}));
|
|
13
|
-
const tvProgressLatestInput = () => defineSchema((value) => {
|
|
14
|
-
const obj = reqObj(value);
|
|
15
|
-
return { mediaId: reqStr(obj, 'mediaId'), season: obj.season, episode: obj.episode };
|
|
16
|
-
});
|
|
17
|
-
const tvProgressSaveInput = () => defineSchema((value) => {
|
|
18
|
-
const obj = reqObj(value);
|
|
19
|
-
return {
|
|
20
|
-
mediaId: reqStr(obj, 'mediaId'),
|
|
21
|
-
season: obj.season,
|
|
22
|
-
episode: obj.episode,
|
|
23
|
-
currentTime: obj.currentTime,
|
|
24
|
-
duration: obj.duration,
|
|
25
|
-
completed: obj.completed,
|
|
26
|
-
title: strOrNull(obj, 'title'),
|
|
27
|
-
poster: strOrNull(obj, 'poster'),
|
|
28
|
-
backdrop: strOrNull(obj, 'backdrop'),
|
|
29
|
-
year: strOrNull(obj, 'year'),
|
|
30
|
-
overview: strOrNull(obj, 'overview'),
|
|
31
|
-
tmdbId: obj.tmdbId ?? null,
|
|
32
|
-
mediaType: strOrNull(obj, 'mediaType'),
|
|
33
|
-
adult: obj.adult ?? null,
|
|
34
|
-
};
|
|
35
|
-
});
|
|
36
|
-
const tvProgressRemoveInput = () => defineSchema((value) => {
|
|
37
|
-
const obj = reqObj(value);
|
|
38
|
-
return { mediaId: reqStr(obj, 'mediaId'), season: obj.season, episode: obj.episode };
|
|
39
|
-
});
|
|
40
|
-
const tvProgressRemoveManyInput = () => defineSchema((value) => {
|
|
41
|
-
if (!Array.isArray(value) || value.length === 0)
|
|
42
|
-
throw new Error('ids must be a non-empty array');
|
|
43
|
-
return value.map((id) => String(id));
|
|
44
|
-
});
|
|
45
|
-
const tvContinueWatchingInput = () => defineSchema((value) => {
|
|
46
|
-
const obj = reqObj(value);
|
|
47
|
-
const out = {};
|
|
48
|
-
if (obj.limit !== undefined && obj.limit !== null && obj.limit !== '') {
|
|
49
|
-
const limit = Number(obj.limit);
|
|
50
|
-
if (Number.isFinite(limit))
|
|
51
|
-
out.limit = limit;
|
|
52
|
-
}
|
|
53
|
-
return out;
|
|
54
|
-
});
|
|
55
|
-
export const tvProgressRouter = router({
|
|
56
|
-
getByMedia: protectedProcedure.input(tvProgressMediaInput()).query(async ({ ctx, input }) => {
|
|
57
|
-
const rows = await TvProgressRepository.getByMedia(ctx.tvDb, normalizeTvMediaId(input.mediaId));
|
|
58
|
-
return { progress: rows };
|
|
59
|
-
}),
|
|
60
|
-
latest: protectedProcedure.input(tvProgressLatestInput()).query(async ({ ctx, input }) => {
|
|
61
|
-
const mediaId = normalizeTvMediaId(input.mediaId);
|
|
62
|
-
const season = parseInt(String(input.season), 10);
|
|
63
|
-
const episode = parseInt(String(input.episode), 10);
|
|
64
|
-
if (Number.isFinite(season) && Number.isFinite(episode)) {
|
|
65
|
-
const row = await TvProgressRepository.getEpisode(ctx.tvDb, mediaId, season, episode);
|
|
66
|
-
return row || { currentTime: 0, duration: 0, completed: 0 };
|
|
67
|
-
}
|
|
68
|
-
const row = await TvProgressRepository.getLatest(ctx.tvDb, mediaId);
|
|
69
|
-
return row || { currentTime: 0, duration: 0, completed: 0 };
|
|
70
|
-
}),
|
|
71
|
-
save: protectedProcedure.input(tvProgressSaveInput()).mutation(async ({ ctx, input }) => {
|
|
72
|
-
const mediaId = normalizeTvMediaId(input.mediaId);
|
|
73
|
-
if (!mediaId) {
|
|
74
|
-
throw new TRPCError({ code: 'BAD_REQUEST', message: 'mediaId is required' });
|
|
75
|
-
}
|
|
76
|
-
const seasonNum = Math.max(Number(input.season) || 1, 1);
|
|
77
|
-
const episodeNum = Math.max(Number(input.episode) || 1, 1);
|
|
78
|
-
const timeNum = Math.max(Number(input.currentTime) || 0, 0);
|
|
79
|
-
const durationNum = Math.max(Number(input.duration) || 0, 0);
|
|
80
|
-
const clampedTime = durationNum > 0 ? Math.min(timeNum, durationNum) : timeNum;
|
|
81
|
-
const storedTime = durationNum > 0 && clampedTime >= durationNum * 0.8 ? durationNum : clampedTime;
|
|
82
|
-
const completedNow = Number(input.completed) === 1 || (durationNum > 0 && storedTime >= durationNum * 0.8) ? 1 : 0;
|
|
83
|
-
await performTvWriteTransactionAsync(ctx.tvDb, async (tx) => {
|
|
84
|
-
await TvProgressRepository.upsert(tx, {
|
|
85
|
-
mediaId: String(mediaId),
|
|
86
|
-
season: Math.round(seasonNum),
|
|
87
|
-
episode: Math.round(episodeNum),
|
|
88
|
-
currentTime: storedTime,
|
|
89
|
-
duration: durationNum,
|
|
90
|
-
completed: completedNow,
|
|
91
|
-
title: input.title ?? null,
|
|
92
|
-
poster: input.poster ?? null,
|
|
93
|
-
backdrop: input.backdrop ?? null,
|
|
94
|
-
year: input.year ?? null,
|
|
95
|
-
overview: input.overview ?? null,
|
|
96
|
-
tmdbId: input.tmdbId != null ? Number(input.tmdbId) : null,
|
|
97
|
-
mediaType: input.mediaType ?? null,
|
|
98
|
-
adult: input.adult != null ? Number(input.adult) : null,
|
|
99
|
-
});
|
|
100
|
-
await TvLibraryRepository.touchProgress(tx, String(mediaId), {
|
|
101
|
-
season: Math.round(seasonNum),
|
|
102
|
-
episode: Math.round(episodeNum),
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
return { success: true };
|
|
106
|
-
}),
|
|
107
|
-
remove: protectedProcedure.input(tvProgressRemoveInput()).mutation(async ({ ctx, input }) => {
|
|
108
|
-
const mediaId = normalizeTvMediaId(input.mediaId);
|
|
109
|
-
if (!mediaId) {
|
|
110
|
-
throw new TRPCError({ code: 'BAD_REQUEST', message: 'mediaId is required' });
|
|
111
|
-
}
|
|
112
|
-
await performTvWriteTransactionAsync(ctx.tvDb, async (tx) => {
|
|
113
|
-
if (input.season !== undefined && input.episode !== undefined) {
|
|
114
|
-
await TvProgressRepository.deleteEpisode(tx, String(mediaId), Number(input.season), Number(input.episode));
|
|
115
|
-
}
|
|
116
|
-
else {
|
|
117
|
-
await TvProgressRepository.deleteByMedia(tx, String(mediaId));
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
return { success: true };
|
|
121
|
-
}),
|
|
122
|
-
removeMany: protectedProcedure
|
|
123
|
-
.input(tvProgressRemoveManyInput())
|
|
124
|
-
.mutation(async ({ ctx, input }) => {
|
|
125
|
-
const ids = input.map((id) => normalizeTvMediaId(id));
|
|
126
|
-
await performTvWriteTransactionAsync(ctx.tvDb, async (tx) => {
|
|
127
|
-
await TvProgressRepository.deleteMany(tx, ids);
|
|
128
|
-
});
|
|
129
|
-
return { success: true, removed: ids.length };
|
|
130
|
-
}),
|
|
131
|
-
continueWatching: protectedProcedure
|
|
132
|
-
.input(tvContinueWatchingInput())
|
|
133
|
-
.query(async ({ ctx, input }) => {
|
|
134
|
-
const limit = Math.min(Math.max(parseInt(String(input.limit ?? 24)) || 24, 1), 100);
|
|
135
|
-
const { ignoreAdult, listOnly } = await SettingsRepository.readContinueFlags(ctx.db, 'tvIgnoreAdultContent', 'tvCwWatchlistOnly');
|
|
136
|
-
const rows = (await TvProgressRepository.getContinueWatching(ctx.tvDb, limit)).filter((row) => {
|
|
137
|
-
if (ignoreAdult && isTvContinueAdult(row))
|
|
138
|
-
return false;
|
|
139
|
-
if (listOnly && row.watchlistStatus !== 'Watching')
|
|
140
|
-
return false;
|
|
141
|
-
return true;
|
|
142
|
-
});
|
|
143
|
-
return { data: rows, total: rows.length };
|
|
144
|
-
}),
|
|
145
|
-
adultCount: protectedProcedure.query(async ({ ctx }) => {
|
|
146
|
-
const ids = await getAdultNonListMediaIds(ctx.tvDb);
|
|
147
|
-
return { count: ids.length };
|
|
148
|
-
}),
|
|
149
|
-
purgeAdult: protectedProcedure.mutation(async ({ ctx }) => {
|
|
150
|
-
const ids = await getAdultNonListMediaIds(ctx.tvDb);
|
|
151
|
-
if (ids.length > 0) {
|
|
152
|
-
await performTvWriteTransactionAsync(ctx.tvDb, async (tx) => {
|
|
153
|
-
for (const id of ids)
|
|
154
|
-
await TvProgressRepository.deleteByMedia(tx, id);
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
return { success: true, removed: ids.length };
|
|
158
|
-
}),
|
|
159
|
-
});
|
|
1
|
+
import{TRPCError as b}from"@trpc/server";import{protectedProcedure as a,router as h}from"../index.js";import{performTvWriteTransactionAsync as p}from"../../sync.js";import{adultNonListIds as f}from"../../utils/db-utils.js";import{SettingsRepository as M}from"../../repositories/settings.repository.js";import{normalizeTvMediaId as l,TvLibraryRepository as P,TvProgressRepository as s}from"../../repositories/tv.repository.js";import{defineSchema as u,reqObj as c,reqStr as g,strOrNull as m}from"../validation.js";const N=t=>t.adult===!0||t.adult===1,w=t=>f(t,"tv_progress","tv_library","mediaId","COALESCE(l.adult, p.adult) = 1"),S=()=>u(t=>({mediaId:g(c(t),"mediaId")})),k=()=>u(t=>{const e=c(t);return{mediaId:g(e,"mediaId"),season:e.season,episode:e.episode}}),C=()=>u(t=>{const e=c(t);return{mediaId:g(e,"mediaId"),season:e.season,episode:e.episode,currentTime:e.currentTime,duration:e.duration,completed:e.completed,title:m(e,"title"),poster:m(e,"poster"),backdrop:m(e,"backdrop"),year:m(e,"year"),overview:m(e,"overview"),tmdbId:e.tmdbId??null,mediaType:m(e,"mediaType"),adult:e.adult??null}}),D=()=>u(t=>{const e=c(t);return{mediaId:g(e,"mediaId"),season:e.season,episode:e.episode}}),R=()=>u(t=>{if(!Array.isArray(t)||t.length===0)throw new Error("ids must be a non-empty array");return t.map(e=>String(e))}),W=()=>u(t=>{const e=c(t),n={};if(e.limit!==void 0&&e.limit!==null&&e.limit!==""){const r=Number(e.limit);Number.isFinite(r)&&(n.limit=r)}return n}),O=h({getByMedia:a.input(S()).query(async({ctx:t,input:e})=>({progress:await s.getByMedia(t.tvDb,l(e.mediaId))})),latest:a.input(k()).query(async({ctx:t,input:e})=>{const n=l(e.mediaId),r=parseInt(String(e.season),10),i=parseInt(String(e.episode),10);return Number.isFinite(r)&&Number.isFinite(i)?await s.getEpisode(t.tvDb,n,r,i)||{currentTime:0,duration:0,completed:0}:await s.getLatest(t.tvDb,n)||{currentTime:0,duration:0,completed:0}}),save:a.input(C()).mutation(async({ctx:t,input:e})=>{const n=l(e.mediaId);if(!n)throw new b({code:"BAD_REQUEST",message:"mediaId is required"});const r=Math.max(Number(e.season)||1,1),i=Math.max(Number(e.episode)||1,1),d=Math.max(Number(e.currentTime)||0,0),o=Math.max(Number(e.duration)||0,0),v=o>0?Math.min(d,o):d,I=o>0&&v>=o*.8?o:v,T=Number(e.completed)===1||o>0&&I>=o*.8?1:0;return await p(t.tvDb,async y=>{await s.upsert(y,{mediaId:String(n),season:Math.round(r),episode:Math.round(i),currentTime:I,duration:o,completed:T,title:e.title??null,poster:e.poster??null,backdrop:e.backdrop??null,year:e.year??null,overview:e.overview??null,tmdbId:e.tmdbId!=null?Number(e.tmdbId):null,mediaType:e.mediaType??null,adult:e.adult!=null?Number(e.adult):null}),await P.touchProgress(y,String(n),{season:Math.round(r),episode:Math.round(i)})}),{success:!0}}),remove:a.input(D()).mutation(async({ctx:t,input:e})=>{const n=l(e.mediaId);if(!n)throw new b({code:"BAD_REQUEST",message:"mediaId is required"});return await p(t.tvDb,async r=>{e.season!==void 0&&e.episode!==void 0?await s.deleteEpisode(r,String(n),Number(e.season),Number(e.episode)):await s.deleteByMedia(r,String(n))}),{success:!0}}),removeMany:a.input(R()).mutation(async({ctx:t,input:e})=>{const n=e.map(r=>l(r));return await p(t.tvDb,async r=>{await s.deleteMany(r,n)}),{success:!0,removed:n.length}}),continueWatching:a.input(W()).query(async({ctx:t,input:e})=>{const n=Math.min(Math.max(parseInt(String(e.limit??24))||24,1),100),{ignoreAdult:r,listOnly:i}=await M.readContinueFlags(t.db,"tvIgnoreAdultContent","tvCwWatchlistOnly"),d=(await s.getContinueWatching(t.tvDb,n)).filter(o=>!(r&&N(o)||i&&o.watchlistStatus!=="Watching"));return{data:d,total:d.length}}),adultCount:a.query(async({ctx:t})=>({count:(await w(t.tvDb)).length})),purgeAdult:a.mutation(async({ctx:t})=>{const e=await w(t.tvDb);return e.length>0&&await p(t.tvDb,async n=>{for(const r of e)await s.deleteByMedia(n,r)}),{success:!0,removed:e.length}})});export{O as tvProgressRouter};
|