The deployment owner adds folders of photos and videos in the media settings. A per-folder background job indexes them (relative path, kind, size, capture date, dimensions, duration), re-indexes every six hours, and drops files that are gone. The owner's own runs get two tools: search_media (words in folder and file names, kind, capture date range, newest first) and import_media (copies one file into the conversation as an artifact). Files are never changed; hidden entries and symlinks are skipped, and an import must resolve inside its folder. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wy1md9QE1ayJbDCwJ4vAQs
83 lines
2.8 KiB
TypeScript
83 lines
2.8 KiB
TypeScript
import path from "node:path";
|
|
import { ORPCError } from "@orpc/server";
|
|
import type { JobPublisher } from "@rakazo/adapter-kit";
|
|
import { mediaLibrarySyncJob } from "@rakazo/adapter-kit";
|
|
import { checkMediaLibraryFolder, mediaLibrarySourceView } from "@rakazo/adapters";
|
|
import type { Actor, MediaLibrarySourceAddInput } from "@rakazo/contracts";
|
|
import type { PrismaClient } from "@rakazo/db";
|
|
|
|
export interface MediaLibraryRouteDeps {
|
|
prisma: PrismaClient;
|
|
jobs: JobPublisher;
|
|
}
|
|
|
|
/** Library folders are paths on the server, so only its owner adds them, like host folders. */
|
|
function requireDeploymentOwner(actor: Actor) {
|
|
if (!actor.isDeploymentOwner) throw new ORPCError("FORBIDDEN");
|
|
}
|
|
|
|
async function ownSource(deps: MediaLibraryRouteDeps, actor: Actor, sourceId: string) {
|
|
const source = await deps.prisma.mediaLibrarySource.findFirst({
|
|
where: { id: sourceId, userId: actor.userId },
|
|
});
|
|
if (!source) throw new ORPCError("NOT_FOUND");
|
|
return source;
|
|
}
|
|
|
|
export async function listMediaLibrarySources(deps: MediaLibraryRouteDeps, actor: Actor) {
|
|
requireDeploymentOwner(actor);
|
|
const rows = await deps.prisma.mediaLibrarySource.findMany({
|
|
where: { userId: actor.userId },
|
|
orderBy: { createdAt: "asc" },
|
|
});
|
|
return rows.map(mediaLibrarySourceView);
|
|
}
|
|
|
|
export async function addMediaLibrarySource(
|
|
deps: MediaLibraryRouteDeps,
|
|
actor: Actor,
|
|
input: MediaLibrarySourceAddInput,
|
|
) {
|
|
requireDeploymentOwner(actor);
|
|
const problem = await checkMediaLibraryFolder(input.path);
|
|
if (problem) throw new ORPCError("BAD_REQUEST", { message: problem });
|
|
const folder = path.resolve(input.path);
|
|
const existing = await deps.prisma.mediaLibrarySource.findUnique({
|
|
where: { userId_path: { userId: actor.userId, path: folder } },
|
|
});
|
|
if (existing) return mediaLibrarySourceView(existing);
|
|
const source = await deps.prisma.mediaLibrarySource.create({
|
|
data: { userId: actor.userId, path: folder, status: "syncing" },
|
|
});
|
|
await deps.jobs.enqueue(mediaLibrarySyncJob(source.id));
|
|
return mediaLibrarySourceView(source);
|
|
}
|
|
|
|
export async function removeMediaLibrarySource(
|
|
deps: MediaLibraryRouteDeps,
|
|
actor: Actor,
|
|
sourceId: string,
|
|
) {
|
|
requireDeploymentOwner(actor);
|
|
await ownSource(deps, actor, sourceId);
|
|
await deps.prisma.mediaLibrarySource.deleteMany({
|
|
where: { id: sourceId, userId: actor.userId },
|
|
});
|
|
return { ok: true as const };
|
|
}
|
|
|
|
export async function syncMediaLibrarySourceNow(
|
|
deps: MediaLibraryRouteDeps,
|
|
actor: Actor,
|
|
sourceId: string,
|
|
) {
|
|
requireDeploymentOwner(actor);
|
|
await ownSource(deps, actor, sourceId);
|
|
const source = await deps.prisma.mediaLibrarySource.update({
|
|
where: { id: sourceId },
|
|
data: { status: "syncing", error: null },
|
|
});
|
|
await deps.jobs.enqueue(mediaLibrarySyncJob(sourceId));
|
|
return mediaLibrarySourceView(source);
|
|
}
|