import fs from 'fs' import path from 'path' import { NextRequest, NextResponse } from 'next/server' import { getLibrary, resolveLibraryRoot, resolveAndJail } from '@/lib/libraries' import { scanDirectory, scanDirectoryRecursive } from '@/lib/files' import { requireLibraryAccess, requireAdmin } from '@/lib/auth' import { removeAllAssignmentsForItem } from '@/lib/tags' import { getDb } from '@/lib/db' export async function GET(request: NextRequest) { const { searchParams } = request.nextUrl const libraryId = searchParams.get('libraryId') const subpath = searchParams.get('path') ?? '' if (!libraryId) { return NextResponse.json({ error: 'Missing libraryId' }, { status: 400 }) } const auth = await requireLibraryAccess(request, libraryId) if (auth instanceof NextResponse) return auth const library = getLibrary(libraryId) if (!library) { return NextResponse.json({ error: 'Library not found' }, { status: 404 }) } if (library.type !== 'mixed') { return NextResponse.json({ error: 'Library is not a mixed library' }, { status: 400 }) } const root = resolveLibraryRoot(library) const recursive = request.nextUrl.searchParams.get('recursive') === 'true' const listing = recursive ? scanDirectoryRecursive(root, libraryId, subpath) : scanDirectory(root, libraryId, subpath) // Annotate image files with hasExtractedText, and directories if any descendant has extracted text const db = getDb() const rows = db .prepare('SELECT item_key FROM media_items WHERE library_id = ? AND extracted_text IS NOT NULL') .all(libraryId) as { item_key: string }[] const withText = new Set(rows.map((r) => r.item_key)) // Build a set of all ancestor directory relative paths that contain at least one item with text // e.g. item_key "lib:mixed_file:manga%2Fch1%2Fp1.jpg" → ancestors "manga", "manga/ch1" const dirsWithText = new Set() const keyPrefix = `${libraryId}:mixed_file:` for (const key of withText) { const decoded = decodeURIComponent(key.slice(keyPrefix.length)) const parts = decoded.split('/') for (let i = 1; i < parts.length; i++) { dirsWithText.add(parts.slice(0, i).join('/')) } } listing.entries = listing.entries.map((e) => { if (e.type === 'file') { if (e.mediaType !== 'image') return e const relPath = subpath ? path.join(subpath, e.name) : e.name const itemKey = `${libraryId}:mixed_file:${encodeURIComponent(relPath)}` return { ...e, hasExtractedText: withText.has(itemKey) } } if (e.type === 'directory') { const dirRel = subpath ? `${subpath}/${e.name}` : e.name if (dirsWithText.has(dirRel)) return { ...e, hasExtractedText: true } } return e }) return NextResponse.json(listing) } export async function DELETE(request: NextRequest) { const auth = await requireAdmin(request) if (auth instanceof NextResponse) return auth const { searchParams } = request.nextUrl const libraryId = searchParams.get('libraryId') const itemPath = searchParams.get('path') if (!libraryId || !itemPath) { return NextResponse.json({ error: 'Missing libraryId or path' }, { status: 400 }) } const library = getLibrary(libraryId) if (!library) { return NextResponse.json({ error: 'Library not found' }, { status: 404 }) } if (library.type !== 'mixed') { return NextResponse.json({ error: 'Library is not a mixed library' }, { status: 400 }) } const root = resolveLibraryRoot(library) let absPath: string try { absPath = resolveAndJail(root, itemPath) } catch { return NextResponse.json({ error: 'Invalid path' }, { status: 400 }) } try { const stat = fs.statSync(absPath) if (stat.isDirectory()) { fs.rmSync(absPath, { recursive: true, force: true }) } else { fs.unlinkSync(absPath) } } catch { return NextResponse.json({ error: 'Failed to delete' }, { status: 500 }) } const db = getDb() const itemKey = `${libraryId}:mixed_file:${encodeURIComponent(itemPath)}` removeAllAssignmentsForItem(itemKey) db.prepare('DELETE FROM media_items WHERE item_key = ?').run(itemKey) // For directories, also clean up children const prefix = `${libraryId}:mixed_file:${encodeURIComponent(itemPath + '/')}` const children = db.prepare('SELECT item_key FROM media_items WHERE item_key LIKE ?').all(`${prefix}%`) as { item_key: string }[] for (const child of children) { removeAllAssignmentsForItem(child.item_key) } db.prepare('DELETE FROM media_items WHERE item_key LIKE ?').run(`${prefix}%`) return new NextResponse(null, { status: 204 }) }