From 3467e397e70574232ba131c97fd3a7d46263da06 Mon Sep 17 00:00:00 2001 From: noelorin Date: Sat, 26 Sep 2026 19:33:37 +0800 Subject: [PATCH] feat(models,dao): danmaku prefs/docs tables and match source --- packages/dao/src/danmaku-docs.ts | 42 ++++++++++++++++++++++++++++ packages/dao/src/danmaku-prefs.ts | 41 +++++++++++++++++++++++++++ packages/dao/src/index.ts | 2 ++ packages/dao/src/media-items.ts | 7 ++++- packages/models/src/danmaku-docs.ts | 32 +++++++++++++++++++++ packages/models/src/danmaku-prefs.ts | 28 +++++++++++++++++++ packages/models/src/index.ts | 4 +++ packages/models/src/media-items.ts | 2 ++ 8 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 packages/dao/src/danmaku-docs.ts create mode 100644 packages/dao/src/danmaku-prefs.ts create mode 100644 packages/models/src/danmaku-docs.ts create mode 100644 packages/models/src/danmaku-prefs.ts diff --git a/packages/dao/src/danmaku-docs.ts b/packages/dao/src/danmaku-docs.ts new file mode 100644 index 0000000..be09455 --- /dev/null +++ b/packages/dao/src/danmaku-docs.ts @@ -0,0 +1,42 @@ +import { and, eq } from "drizzle-orm"; +import { danmakuDocs, type DanmakuDocRow } from "@app/models"; +import { db } from "@app/db"; + +export const danmakuDocsDao = { + async get(userId: string, mediaItemId: string, source: string): Promise { + const [row] = await db + .select() + .from(danmakuDocs) + .where( + and( + eq(danmakuDocs.userId, userId), + eq(danmakuDocs.mediaItemId, mediaItemId), + eq(danmakuDocs.source, source), + ), + ) + .limit(1); + return row ?? null; + }, + async upsert(data: { + userId: string; + mediaItemId: string; + source: string; + xml: string; + byteSize: number; + }): Promise { + const existing = await danmakuDocsDao.get(data.userId, data.mediaItemId, data.source); + if (existing) { + const [row] = await db + .update(danmakuDocs) + .set({ xml: data.xml, byteSize: data.byteSize, updatedAt: new Date() }) + .where(eq(danmakuDocs.id, existing.id)) + .returning(); + if (!row) throw new Error("Failed to update danmaku doc"); + return row; + } + const rows = await db.insert(danmakuDocs).values(data).returning(); + const row = rows[0]; + if (!row) throw new Error("Failed to insert danmaku doc"); + return row; + }, +}; diff --git a/packages/dao/src/danmaku-prefs.ts b/packages/dao/src/danmaku-prefs.ts new file mode 100644 index 0000000..0ff2c03 --- /dev/null +++ b/packages/dao/src/danmaku-prefs.ts @@ -0,0 +1,41 @@ +import { eq } from "drizzle-orm"; +import { danmakuPrefs, type DanmakuPrefsRow } from "@app/models"; +import { db } from "@app/db"; + +export const danmakuPrefsDao = { + async getByUser(userId: string): Promise { + const [row] = await db + .select() + .from(danmakuPrefs) + .where(eq(danmakuPrefs.userId, userId)) + .limit(1); + return row ?? null; + }, + async upsert(data: { + userId: string; + enabled: boolean; + opacity: number; + density: number; + blockKeywords: string; + blockTypes: string; + }): Promise { + const existing = await db + .select() + .from(danmakuPrefs) + .where(eq(danmakuPrefs.userId, data.userId)) + .limit(1); + if (existing[0]) { + const [row] = await db + .update(danmakuPrefs) + .set({ ...data, updatedAt: new Date() }) + .where(eq(danmakuPrefs.id, existing[0].id)) + .returning(); + if (!row) throw new Error("Failed to update danmaku prefs"); + return row; + } + const rows = await db.insert(danmakuPrefs).values(data).returning(); + const row = rows[0]; + if (!row) throw new Error("Failed to insert danmaku prefs"); + return row; + }, +}; diff --git a/packages/dao/src/index.ts b/packages/dao/src/index.ts index 8edc9fb..a537aaf 100644 --- a/packages/dao/src/index.ts +++ b/packages/dao/src/index.ts @@ -11,3 +11,5 @@ export { mediaItemDao } from "./media-items.js"; export type { MediaItemRow } from "./media-items.js"; export { playbackProgressDao } from "./playback-progress.js"; export { danmakuCacheDao } from "./danmaku-cache.js"; +export { danmakuPrefsDao } from "./danmaku-prefs.js"; +export { danmakuDocsDao } from "./danmaku-docs.js"; diff --git a/packages/dao/src/media-items.ts b/packages/dao/src/media-items.ts index a40eee2..578c789 100644 --- a/packages/dao/src/media-items.ts +++ b/packages/dao/src/media-items.ts @@ -280,13 +280,18 @@ export const mediaItemDao = { async setDanmakuMatch( id: string, userId: string, - data: { episodeId: number; matchedHash?: string | null }, + data: { + episodeId: number; + matchedHash?: string | null; + source?: "auto" | "manual" | "none"; + }, ): Promise { const [row] = await db .update(mediaItems) .set({ dandanplayEpisodeId: data.episodeId, matchedHash: data.matchedHash ?? null, + danmakuMatchSource: data.source ?? "auto", updatedAt: new Date(), }) .where(and(eq(mediaItems.id, id), eq(mediaItems.userId, userId))) diff --git a/packages/models/src/danmaku-docs.ts b/packages/models/src/danmaku-docs.ts new file mode 100644 index 0000000..0d8b9cd --- /dev/null +++ b/packages/models/src/danmaku-docs.ts @@ -0,0 +1,32 @@ +import { sql } from "drizzle-orm"; +import { integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core"; +import { users } from "./users.js"; +import { mediaItems } from "./media-items.js"; + +export const danmakuDocs = sqliteTable( + "danmaku_docs", + { + id: text("id") + .primaryKey() + .$defaultFn(() => crypto.randomUUID()), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + mediaItemId: text("media_item_id") + .notNull() + .references(() => mediaItems.id, { onDelete: "cascade" }), + /** `open-network` | `local-xml` */ + source: text("source").notNull(), + xml: text("xml").notNull(), + byteSize: integer("byte_size").notNull().default(0), + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .default(sql`(unixepoch())`), + updatedAt: integer("updated_at", { mode: "timestamp" }) + .notNull() + .default(sql`(unixepoch())`), + }, + (t) => [uniqueIndex("danmaku_docs_user_media_src_uq").on(t.userId, t.mediaItemId, t.source)], +); +export type DanmakuDocRow = typeof danmakuDocs.$inferSelect; +export type NewDanmakuDocRow = typeof danmakuDocs.$inferInsert; diff --git a/packages/models/src/danmaku-prefs.ts b/packages/models/src/danmaku-prefs.ts new file mode 100644 index 0000000..48c70a5 --- /dev/null +++ b/packages/models/src/danmaku-prefs.ts @@ -0,0 +1,28 @@ +import { sql } from "drizzle-orm"; +import { integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core"; +import { users } from "./users.js"; + +export const danmakuPrefs = sqliteTable( + "danmaku_prefs", + { + id: text("id") + .primaryKey() + .$defaultFn(() => crypto.randomUUID()), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), + /** 整数百分比 0–100,API 层换算 0–1 小数 */ + opacity: integer("opacity").notNull().default(80), + /** 整数百分比 10–100 */ + density: integer("density").notNull().default(100), + blockKeywords: text("block_keywords").notNull().default("[]"), + blockTypes: text("block_types").notNull().default("[]"), + updatedAt: integer("updated_at", { mode: "timestamp" }) + .notNull() + .default(sql`(unixepoch())`), + }, + (t) => [uniqueIndex("danmaku_prefs_user_uq").on(t.userId)], +); +export type DanmakuPrefsRow = typeof danmakuPrefs.$inferSelect; +export type NewDanmakuPrefsRow = typeof danmakuPrefs.$inferInsert; diff --git a/packages/models/src/index.ts b/packages/models/src/index.ts index c8da20f..91cbf33 100644 --- a/packages/models/src/index.ts +++ b/packages/models/src/index.ts @@ -13,3 +13,7 @@ export { playbackProgress } from "./playback-progress.js"; export type { PlaybackProgress } from "./playback-progress.js"; export { danmakuCache } from "./danmaku-cache.js"; export type { DanmakuCacheRow } from "./danmaku-cache.js"; +export { danmakuPrefs } from "./danmaku-prefs.js"; +export type { DanmakuPrefsRow, NewDanmakuPrefsRow } from "./danmaku-prefs.js"; +export { danmakuDocs } from "./danmaku-docs.js"; +export type { DanmakuDocRow, NewDanmakuDocRow } from "./danmaku-docs.js"; diff --git a/packages/models/src/media-items.ts b/packages/models/src/media-items.ts index 23f1db6..afd0183 100644 --- a/packages/models/src/media-items.ts +++ b/packages/models/src/media-items.ts @@ -26,6 +26,8 @@ export const mediaItems = sqliteTable( /** 弹弹play 弹幕库编号(文件识别成功后落库,弹幕拉取直达) */ dandanplayEpisodeId: integer("dandanplay_episode_id"), matchedHash: text("matched_hash"), + /** auto=高置信自动 | manual=手选 | none */ + danmakuMatchSource: text("danmaku_match_source").notNull().default("none"), scannedAt: integer("scanned_at", { mode: "timestamp" }) .notNull() .default(sql`(unixepoch())`),