media viewer consistency
This commit is contained in:
36
src/app/api/ai-tagging/translate-bulk/route.ts
Normal file
36
src/app/api/ai-tagging/translate-bulk/route.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { requireLibraryAccess } from '@/lib/auth'
|
||||
import { enqueueJob } from '@/lib/ai-jobs'
|
||||
import { getDb } from '@/lib/db'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
let body: { libraryId?: string; path?: string }
|
||||
try {
|
||||
body = await request.json()
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })
|
||||
}
|
||||
|
||||
const { libraryId, path: dirPath } = body
|
||||
if (!libraryId || typeof libraryId !== 'string') {
|
||||
return NextResponse.json({ error: 'libraryId is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const auth = await requireLibraryAccess(request, libraryId)
|
||||
if (auth instanceof NextResponse) return auth
|
||||
|
||||
const db = getDb()
|
||||
const prefix = dirPath
|
||||
? `${libraryId}:mixed_file:${encodeURIComponent(dirPath + '/')}`
|
||||
: `${libraryId}:mixed_file:`
|
||||
|
||||
// Only enqueue translate jobs for items that already have extracted text
|
||||
const items = db
|
||||
.prepare(
|
||||
'SELECT item_key FROM media_items WHERE item_key LIKE ? AND item_type = ? AND extracted_text IS NOT NULL'
|
||||
)
|
||||
.all(`${prefix}%`, 'mixed_file') as { item_key: string }[]
|
||||
|
||||
const jobIds = items.map(({ item_key }) => enqueueJob(item_key, 'translate', libraryId))
|
||||
return NextResponse.json({ jobIds, queued: jobIds.length }, { status: 202 })
|
||||
}
|
||||
@@ -33,18 +33,37 @@ export async function GET(request: NextRequest) {
|
||||
? scanDirectoryRecursive(root, libraryId, subpath)
|
||||
: scanDirectory(root, libraryId, subpath)
|
||||
|
||||
// Annotate image entries with whether they have extracted text
|
||||
// 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<string>()
|
||||
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' || 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 === '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)
|
||||
|
||||
Reference in New Issue
Block a user