diff --git a/packages/models/src/danmaku-cache.ts b/packages/models/src/danmaku-cache.ts new file mode 100644 index 0000000..71f162c --- /dev/null +++ b/packages/models/src/danmaku-cache.ts @@ -0,0 +1,20 @@ +import { sql } from "drizzle-orm"; +import { integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core"; + +export const danmakuCache = sqliteTable( + "danmaku_cache", + { + id: text("id") + .primaryKey() + .$defaultFn(() => crypto.randomUUID()), + matchKey: text("match_key").notNull(), + payload: text("payload").notNull(), + source: text("source").notNull().default("open-network"), + expiresAt: integer("expires_at", { mode: "timestamp" }), + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .default(sql`(unixepoch())`), + }, + (t) => [uniqueIndex("danmaku_match_key_uq").on(t.matchKey)], +); +export type DanmakuCacheRow = typeof danmakuCache.$inferSelect; diff --git a/packages/models/src/index.ts b/packages/models/src/index.ts index f9be369..c8da20f 100644 --- a/packages/models/src/index.ts +++ b/packages/models/src/index.ts @@ -5,3 +5,11 @@ export { sessions } from "./sessions.js"; export type { Session, NewSession } from "./sessions.js"; export { passwordResets } from "./password-resets.js"; export type { PasswordReset, NewPasswordReset } from "./password-resets.js"; +export { mounts } from "./mounts.js"; +export type { Mount, NewMount } from "./mounts.js"; +export { mediaItems } from "./media-items.js"; +export type { MediaItem, NewMediaItem } from "./media-items.js"; +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"; diff --git a/packages/models/src/media-items.ts b/packages/models/src/media-items.ts new file mode 100644 index 0000000..bba4d60 --- /dev/null +++ b/packages/models/src/media-items.ts @@ -0,0 +1,37 @@ +import { sql } from "drizzle-orm"; +import { integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core"; +import { users } from "./users.js"; +import { mounts } from "./mounts.js"; + +export const mediaItems = sqliteTable( + "media_items", + { + id: text("id") + .primaryKey() + .$defaultFn(() => crypto.randomUUID()), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + mountId: text("mount_id").references(() => mounts.id, { onDelete: "set null" }), + path: text("path").notNull(), + rawName: text("raw_name").notNull(), + title: text("title").notNull(), + size: integer("size").notNull().default(0), + mime: text("mime"), + bangumiId: integer("bangumi_id"), + epNumber: integer("ep_number"), + scrapeStatus: text("scrape_status").notNull().default("pending"), + scrapedAt: integer("scraped_at", { mode: "timestamp" }), + posterUrl: text("poster_url"), + matchedHash: text("matched_hash"), + scannedAt: integer("scanned_at", { mode: "timestamp" }) + .notNull() + .default(sql`(unixepoch())`), + updatedAt: integer("updated_at", { mode: "timestamp" }) + .notNull() + .default(sql`(unixepoch())`), + }, + (t) => [uniqueIndex("media_user_path_uq").on(t.userId, t.path)], +); +export type MediaItem = typeof mediaItems.$inferSelect; +export type NewMediaItem = typeof mediaItems.$inferInsert; diff --git a/packages/models/src/mounts.ts b/packages/models/src/mounts.ts new file mode 100644 index 0000000..ec38551 --- /dev/null +++ b/packages/models/src/mounts.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"; + +export const mounts = sqliteTable( + "mounts", + { + id: text("id") + .primaryKey() + .$defaultFn(() => crypto.randomUUID()), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + name: text("name").notNull(), + type: text("type").notNull().default("webdav"), + baseUrl: text("base_url").notNull(), + username: text("username"), + /** AES-GCM 密文(base64),永不回传前端 */ + secretEnc: text("secret_enc").notNull().default(""), + rootPath: text("root_path").notNull().default("/"), + enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .default(sql`(unixepoch())`), + updatedAt: integer("updated_at", { mode: "timestamp" }) + .notNull() + .default(sql`(unixepoch())`), + }, + (t) => [uniqueIndex("mounts_user_name_uq").on(t.userId, t.name)], +); +export type Mount = typeof mounts.$inferSelect; +export type NewMount = typeof mounts.$inferInsert; diff --git a/packages/models/src/playback-progress.ts b/packages/models/src/playback-progress.ts new file mode 100644 index 0000000..6b493b1 --- /dev/null +++ b/packages/models/src/playback-progress.ts @@ -0,0 +1,26 @@ +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 playbackProgress = sqliteTable( + "playback_progress", + { + 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" }), + positionMs: integer("position_ms").notNull().default(0), + durationMs: integer("duration_ms").notNull().default(0), + updatedAt: integer("updated_at", { mode: "timestamp" }) + .notNull() + .default(sql`(unixepoch())`), + }, + (t) => [uniqueIndex("progress_user_media_uq").on(t.userId, t.mediaItemId)], +); +export type PlaybackProgress = typeof playbackProgress.$inferSelect;