feat(dao): mount/media/progress/danmaku-cache daos
This commit is contained in:
parent
f94aabe8a5
commit
2f5000cc97
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { and, eq, gt, isNull, or } from "drizzle-orm";
|
||||||
|
import { danmakuCache, type DanmakuCacheRow } from "@app/models";
|
||||||
|
import { db } from "@app/db";
|
||||||
|
|
||||||
|
export const danmakuCacheDao = {
|
||||||
|
async getValid(matchKey: string): Promise<DanmakuCacheRow | null> {
|
||||||
|
const now = new Date();
|
||||||
|
const [row] = await db
|
||||||
|
.select()
|
||||||
|
.from(danmakuCache)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(danmakuCache.matchKey, matchKey),
|
||||||
|
or(isNull(danmakuCache.expiresAt), gt(danmakuCache.expiresAt, now)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
return row ?? null;
|
||||||
|
},
|
||||||
|
async upsert(data: {
|
||||||
|
matchKey: string;
|
||||||
|
payload: string;
|
||||||
|
source: string;
|
||||||
|
expiresAt: Date | null;
|
||||||
|
}): Promise<DanmakuCacheRow> {
|
||||||
|
const existing = await db
|
||||||
|
.select()
|
||||||
|
.from(danmakuCache)
|
||||||
|
.where(eq(danmakuCache.matchKey, data.matchKey))
|
||||||
|
.limit(1);
|
||||||
|
if (existing[0]) {
|
||||||
|
const [row] = await db
|
||||||
|
.update(danmakuCache)
|
||||||
|
.set({ payload: data.payload, source: data.source, expiresAt: data.expiresAt })
|
||||||
|
.where(eq(danmakuCache.id, existing[0].id))
|
||||||
|
.returning();
|
||||||
|
if (!row) throw new Error("Failed to update danmaku cache");
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
const rows = await db.insert(danmakuCache).values(data).returning();
|
||||||
|
const row = rows[0];
|
||||||
|
if (!row) throw new Error("Failed to insert danmaku cache");
|
||||||
|
return row;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -5,3 +5,9 @@ export { sessionDao } from "./sessions.js";
|
||||||
export type { SessionRow } from "./sessions.js";
|
export type { SessionRow } from "./sessions.js";
|
||||||
export { passwordResetDao } from "./password-resets.js";
|
export { passwordResetDao } from "./password-resets.js";
|
||||||
export type { PasswordResetRow } from "./password-resets.js";
|
export type { PasswordResetRow } from "./password-resets.js";
|
||||||
|
export { mountDao } from "./mounts.js";
|
||||||
|
export type { MountRow } from "./mounts.js";
|
||||||
|
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";
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
import { and, desc, eq, like, or, sql } from "drizzle-orm";
|
||||||
|
import { mediaItems, type MediaItem } from "@app/models";
|
||||||
|
import { db } from "@app/db";
|
||||||
|
|
||||||
|
export type MediaItemRow = MediaItem;
|
||||||
|
|
||||||
|
export const mediaItemDao = {
|
||||||
|
async list(
|
||||||
|
userId: string,
|
||||||
|
params: {
|
||||||
|
limit: number;
|
||||||
|
offset: number;
|
||||||
|
scrapeStatus?: string | undefined;
|
||||||
|
q?: string | undefined;
|
||||||
|
},
|
||||||
|
): Promise<{ rows: MediaItemRow[]; total: number }> {
|
||||||
|
const conds = [eq(mediaItems.userId, userId)];
|
||||||
|
if (params.scrapeStatus) conds.push(eq(mediaItems.scrapeStatus, params.scrapeStatus));
|
||||||
|
if (params.q) {
|
||||||
|
const likeQ = `%${params.q}%`;
|
||||||
|
const qCond = or(like(mediaItems.title, likeQ), like(mediaItems.rawName, likeQ));
|
||||||
|
if (qCond) conds.push(qCond);
|
||||||
|
}
|
||||||
|
const where = and(...conds);
|
||||||
|
const rows = await db
|
||||||
|
.select()
|
||||||
|
.from(mediaItems)
|
||||||
|
.where(where)
|
||||||
|
.orderBy(desc(mediaItems.updatedAt))
|
||||||
|
.limit(params.limit)
|
||||||
|
.offset(params.offset);
|
||||||
|
const [countRow] = await db
|
||||||
|
.select({ count: sql<number>`count(*)` })
|
||||||
|
.from(mediaItems)
|
||||||
|
.where(where);
|
||||||
|
return { rows, total: Number(countRow?.["count"] ?? 0) };
|
||||||
|
},
|
||||||
|
async getByIdForUser(id: string, userId: string): Promise<MediaItemRow | null> {
|
||||||
|
const [row] = await db
|
||||||
|
.select()
|
||||||
|
.from(mediaItems)
|
||||||
|
.where(and(eq(mediaItems.id, id), eq(mediaItems.userId, userId)))
|
||||||
|
.limit(1);
|
||||||
|
return row ?? null;
|
||||||
|
},
|
||||||
|
async getByUserPath(userId: string, path: string): Promise<MediaItemRow | null> {
|
||||||
|
const [row] = await db
|
||||||
|
.select()
|
||||||
|
.from(mediaItems)
|
||||||
|
.where(and(eq(mediaItems.userId, userId), eq(mediaItems.path, path)))
|
||||||
|
.limit(1);
|
||||||
|
return row ?? null;
|
||||||
|
},
|
||||||
|
async upsertFromScan(data: {
|
||||||
|
userId: string;
|
||||||
|
mountId: string;
|
||||||
|
path: string;
|
||||||
|
rawName: string;
|
||||||
|
title: string;
|
||||||
|
size: number;
|
||||||
|
mime?: string | undefined;
|
||||||
|
}): Promise<MediaItemRow> {
|
||||||
|
const existing = await this.getByUserPath(data.userId, data.path);
|
||||||
|
if (existing) {
|
||||||
|
const [row] = await db
|
||||||
|
.update(mediaItems)
|
||||||
|
.set({
|
||||||
|
size: data.size,
|
||||||
|
mime: data.mime,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
scannedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(eq(mediaItems.id, existing.id))
|
||||||
|
.returning();
|
||||||
|
if (!row) throw new Error("Failed to update media item");
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
const rows = await db
|
||||||
|
.insert(mediaItems)
|
||||||
|
.values({
|
||||||
|
userId: data.userId,
|
||||||
|
mountId: data.mountId,
|
||||||
|
path: data.path,
|
||||||
|
rawName: data.rawName,
|
||||||
|
title: data.title,
|
||||||
|
size: data.size,
|
||||||
|
mime: data.mime,
|
||||||
|
scrapeStatus: "pending",
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
const row = rows[0];
|
||||||
|
if (!row) throw new Error("Failed to insert media item");
|
||||||
|
return row;
|
||||||
|
},
|
||||||
|
async updateScrape(
|
||||||
|
id: string,
|
||||||
|
userId: string,
|
||||||
|
data: {
|
||||||
|
bangumiId?: number | null;
|
||||||
|
epNumber?: number | null;
|
||||||
|
scrapeStatus: string;
|
||||||
|
scrapedAt?: Date | null;
|
||||||
|
posterUrl?: string | null;
|
||||||
|
title?: string;
|
||||||
|
},
|
||||||
|
): Promise<MediaItemRow | null> {
|
||||||
|
const [row] = await db
|
||||||
|
.update(mediaItems)
|
||||||
|
.set({ ...data, updatedAt: new Date() })
|
||||||
|
.where(and(eq(mediaItems.id, id), eq(mediaItems.userId, userId)))
|
||||||
|
.returning();
|
||||||
|
return row ?? null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { and, desc, eq } from "drizzle-orm";
|
||||||
|
import { mounts, type Mount, type NewMount } from "@app/models";
|
||||||
|
import { db } from "@app/db";
|
||||||
|
|
||||||
|
export type MountRow = Mount;
|
||||||
|
|
||||||
|
export const mountDao = {
|
||||||
|
async listByUser(userId: string): Promise<MountRow[]> {
|
||||||
|
return db
|
||||||
|
.select()
|
||||||
|
.from(mounts)
|
||||||
|
.where(eq(mounts.userId, userId))
|
||||||
|
.orderBy(desc(mounts.createdAt));
|
||||||
|
},
|
||||||
|
async getByIdForUser(id: string, userId: string): Promise<MountRow | null> {
|
||||||
|
const [row] = await db
|
||||||
|
.select()
|
||||||
|
.from(mounts)
|
||||||
|
.where(and(eq(mounts.id, id), eq(mounts.userId, userId)))
|
||||||
|
.limit(1);
|
||||||
|
return row ?? null;
|
||||||
|
},
|
||||||
|
async create(data: NewMount): Promise<MountRow> {
|
||||||
|
const rows = await db.insert(mounts).values(data).returning();
|
||||||
|
const row = rows[0];
|
||||||
|
if (!row) throw new Error("Failed to create mount");
|
||||||
|
return row;
|
||||||
|
},
|
||||||
|
async update(
|
||||||
|
id: string,
|
||||||
|
userId: string,
|
||||||
|
data: Partial<Omit<NewMount, "id" | "userId">>,
|
||||||
|
): Promise<MountRow | null> {
|
||||||
|
const [row] = await db
|
||||||
|
.update(mounts)
|
||||||
|
.set({ ...data, updatedAt: new Date() })
|
||||||
|
.where(and(eq(mounts.id, id), eq(mounts.userId, userId)))
|
||||||
|
.returning();
|
||||||
|
return row ?? null;
|
||||||
|
},
|
||||||
|
async delete(id: string, userId: string): Promise<void> {
|
||||||
|
await db.delete(mounts).where(and(eq(mounts.id, id), eq(mounts.userId, userId)));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import { playbackProgress, type PlaybackProgress } from "@app/models";
|
||||||
|
import { db } from "@app/db";
|
||||||
|
|
||||||
|
export const playbackProgressDao = {
|
||||||
|
async get(userId: string, mediaItemId: string): Promise<PlaybackProgress | null> {
|
||||||
|
const [row] = await db
|
||||||
|
.select()
|
||||||
|
.from(playbackProgress)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(playbackProgress.userId, userId),
|
||||||
|
eq(playbackProgress.mediaItemId, mediaItemId),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
return row ?? null;
|
||||||
|
},
|
||||||
|
async upsert(
|
||||||
|
userId: string,
|
||||||
|
mediaItemId: string,
|
||||||
|
positionMs: number,
|
||||||
|
durationMs: number,
|
||||||
|
): Promise<PlaybackProgress> {
|
||||||
|
const existing = await this.get(userId, mediaItemId);
|
||||||
|
if (existing) {
|
||||||
|
const [row] = await db
|
||||||
|
.update(playbackProgress)
|
||||||
|
.set({ positionMs, durationMs, updatedAt: new Date() })
|
||||||
|
.where(eq(playbackProgress.id, existing.id))
|
||||||
|
.returning();
|
||||||
|
if (!row) throw new Error("Failed to update progress");
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
const rows = await db
|
||||||
|
.insert(playbackProgress)
|
||||||
|
.values({ userId, mediaItemId, positionMs, durationMs })
|
||||||
|
.returning();
|
||||||
|
const row = rows[0];
|
||||||
|
if (!row) throw new Error("Failed to insert progress");
|
||||||
|
return row;
|
||||||
|
},
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue