diff --git a/packages/trpc/src/index.ts b/packages/trpc/src/index.ts index 0ede111..0eb77f4 100644 --- a/packages/trpc/src/index.ts +++ b/packages/trpc/src/index.ts @@ -2,6 +2,7 @@ // Router export { appRouter } from "./router/index.js"; export type { AppRouter, RouterInputs, RouterOutputs, TrpcContext } from "./router/index.js"; +export { mediaRouter } from "./router/media.router.js"; // Service layer — usable without tRPC (tests, scripts) export { userService } from "./services/user.service.js"; diff --git a/packages/trpc/src/router/index.ts b/packages/trpc/src/router/index.ts index 1edae1c..c9197fb 100644 --- a/packages/trpc/src/router/index.ts +++ b/packages/trpc/src/router/index.ts @@ -8,6 +8,7 @@ import { userSchemas } from "@app/types"; import { userService } from "../services/user.service.js"; import type { TrpcContext } from "../context.js"; import { authRouter } from "./auth.router.js"; +import { mediaRouter } from "./media.router.js"; // ─── User router ──────────────────────────────────────────────────────── export const userRouter = t.router({ @@ -34,6 +35,7 @@ export const userRouter = t.router({ export const appRouter = t.router({ user: userRouter, auth: authRouter, + media: mediaRouter, }); // ─── Type re-exports ──────────────────────────────────────────────────── diff --git a/packages/trpc/src/router/media.router.ts b/packages/trpc/src/router/media.router.ts new file mode 100644 index 0000000..d201eb6 --- /dev/null +++ b/packages/trpc/src/router/media.router.ts @@ -0,0 +1,85 @@ +import { + danmakuSchemas, + librarySchemas, + mountSchemas, + playbackSchemas, + scrapeSchemas, +} from "@app/types"; +import { t } from "../context.js"; +import { danmakuService } from "../services/danmaku.service.js"; +import { libraryService } from "../services/library.service.js"; +import { mountService } from "../services/mount.service.js"; +import { playbackService } from "../services/playback.service.js"; +import { protectedProcedure } from "../services/procedure.js"; +import { scrapeService } from "../services/scrape.service.js"; + +export const mediaRouter = t.router({ + mountList: protectedProcedure + .input(mountSchemas.list) + .query(({ ctx }) => mountService.list(ctx.userId)), + mountCreate: protectedProcedure + .input(mountSchemas.create) + .mutation(({ ctx, input }) => mountService.create(ctx.userId, input)), + mountUpdate: protectedProcedure + .input(mountSchemas.update) + .mutation(({ ctx, input }) => mountService.update(ctx.userId, input)), + mountDelete: protectedProcedure + .input(mountSchemas.delete) + .mutation(({ ctx, input }) => mountService.delete(ctx.userId, input.id)), + mountTest: protectedProcedure + .input(mountSchemas.test) + .mutation(({ input }) => mountService.test(input)), + mountListDir: protectedProcedure + .input(mountSchemas.listDir) + .query(({ ctx, input }) => mountService.listDir(ctx.userId, input)), + + libraryList: protectedProcedure + .input(librarySchemas.list) + .query(({ ctx, input }) => libraryService.list(ctx.userId, input)), + libraryGet: protectedProcedure + .input(librarySchemas.get) + .query(({ ctx, input }) => libraryService.get(ctx.userId, input.id)), + libraryScan: protectedProcedure + .input(librarySchemas.scan) + .mutation(({ ctx, input }) => libraryService.scan(ctx.userId, input.mountId)), + + scrapeSearch: protectedProcedure + .input(scrapeSchemas.search) + .query(({ input }) => scrapeService.search(input.q)), + scrapeBind: protectedProcedure + .input(scrapeSchemas.bind) + .mutation(({ ctx, input }) => + scrapeService.bind(ctx.userId, input.mediaItemId, input.bangumiId), + ), + scrapeRescrape: protectedProcedure + .input(scrapeSchemas.rescrape) + .mutation(({ ctx, input }) => + scrapeService + .autoScrape(ctx.userId, input.mediaItemId) + .then(() => ({ success: true as const })), + ), + + playbackReport: protectedProcedure + .input(playbackSchemas.report) + .mutation(({ ctx, input }) => playbackService.report(ctx.userId, input)), + playbackGet: protectedProcedure + .input(playbackSchemas.get) + .query(({ ctx, input }) => playbackService.get(ctx.userId, input.mediaItemId)), + + danmakuFetch: protectedProcedure + .input(danmakuSchemas.fetch) + .query(({ ctx, input }) => danmakuService.fetch(ctx.userId, input.mediaItemId)), + danmakuImportMeta: protectedProcedure + .input(danmakuSchemas.importMeta) + .mutation(({ ctx, input }) => + danmakuService.importMeta(ctx.userId, input.mediaItemId, input.byteSize), + ), + danmakuGetSettings: protectedProcedure.input(danmakuSchemas.getSettings).query(() => ({ + enabled: true, + opacity: 0.8, + density: 1, + })), + danmakuSaveSettings: protectedProcedure + .input(danmakuSchemas.settings) + .mutation(({ input }) => input), +}); diff --git a/packages/trpc/src/services/procedure.ts b/packages/trpc/src/services/procedure.ts new file mode 100644 index 0000000..8e54945 --- /dev/null +++ b/packages/trpc/src/services/procedure.ts @@ -0,0 +1,21 @@ +import { TRPCError } from "@trpc/server"; +import { t } from "../context.js"; +import { authService } from "./auth.service.js"; + +/** 需要登录;解析出 userId 注入 ctx。 */ +export const protectedProcedure = t.procedure.use(async ({ ctx, next }) => { + if (!ctx.sessionToken) { + throw new TRPCError({ code: "UNAUTHORIZED", message: "请先登录" }); + } + const user = await authService.getSessionUser(ctx.sessionToken); + if (!user) { + throw new TRPCError({ code: "UNAUTHORIZED", message: "请先登录" }); + } + return next({ + ctx: { + sessionToken: ctx.sessionToken, + setCookie: ctx.setCookie, + userId: user.id, + }, + }); +});