DB-first library reads, mixed library indexing, and manual NFO refresh
- API reads now serve from media_items cache instead of scanning the filesystem on every request; scans (manual or scheduled) remain the write path - NFO metadata is no longer parsed automatically during scans; title falls back to folder/filename — metadata can be refreshed per-item via the kabob menu - Mixed libraries are now indexed in media_items (new mixed_file item type) with file_path stored; scanMixed walks recursively and upserts all files - Added file_path column to media_items and migrated item_type CHECK constraint to include mixed_file via safe table-recreation migration - New POST /api/nfo-refresh endpoint reads the .nfo for a single item and patches its DB row (supports movie, tv_series, tv_episode) - Added "Refresh metadata" button to movie and TV series kabob menus Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import type { Movie } from '@/types'
|
||||
import { parseMovieNfo } from './nfo'
|
||||
import { getDb } from './db'
|
||||
import { HIDDEN_FILES, VIDEO_EXTENSIONS, fileApiUrl, thumbnailApiUrl, findFile } from './media-utils'
|
||||
|
||||
function findVideoFile(dir: string): string | null {
|
||||
@@ -16,7 +16,7 @@ function findVideoFile(dir: string): string | null {
|
||||
) ?? null
|
||||
}
|
||||
|
||||
function findNfoFile(dir: string, dirName: string): string | null {
|
||||
export function findNfoFile(dir: string, dirName: string): string | null {
|
||||
// Try {dirName}.nfo first, then movie.nfo, then any .nfo
|
||||
const candidates = [`${dirName}.nfo`, 'movie.nfo']
|
||||
let entries: string[]
|
||||
@@ -26,9 +26,8 @@ function findNfoFile(dir: string, dirName: string): string | null {
|
||||
return null
|
||||
}
|
||||
for (const candidate of candidates) {
|
||||
if (entries.find((e) => e.toLowerCase() === candidate.toLowerCase())) {
|
||||
return entries.find((e) => e.toLowerCase() === candidate.toLowerCase())!
|
||||
}
|
||||
const match = entries.find((e) => e.toLowerCase() === candidate.toLowerCase())
|
||||
if (match) return match
|
||||
}
|
||||
return entries.find((e) => path.extname(e).toLowerCase() === '.nfo') ?? null
|
||||
}
|
||||
@@ -52,9 +51,6 @@ export function scanMoviesLibrary(libraryRoot: string, libraryId: string): Movie
|
||||
const videoFile = findVideoFile(moviePath)
|
||||
if (!videoFile) continue
|
||||
|
||||
const nfoFile = findNfoFile(moviePath, dirName)
|
||||
const nfo = nfoFile ? parseMovieNfo(path.join(moviePath, nfoFile)) : null
|
||||
|
||||
const posterFile = findFile(moviePath, /^(poster|cover|folder)$/i)
|
||||
const backdropFile = findFile(moviePath, /^(backdrop|fanart|background)$/i)
|
||||
|
||||
@@ -63,12 +59,12 @@ export function scanMoviesLibrary(libraryRoot: string, libraryId: string): Movie
|
||||
|
||||
movies.push({
|
||||
id,
|
||||
title: nfo?.title ?? dirName,
|
||||
year: nfo?.year ?? null,
|
||||
plot: nfo?.plot ?? null,
|
||||
rating: nfo?.rating ?? null,
|
||||
genres: nfo?.genres ?? [],
|
||||
runtime: nfo?.runtime ?? null,
|
||||
title: dirName,
|
||||
year: null,
|
||||
plot: null,
|
||||
rating: null,
|
||||
genres: [],
|
||||
runtime: null,
|
||||
posterUrl: posterFile
|
||||
? thumbnailApiUrl(libraryId, path.join(dirName, posterFile))
|
||||
: null,
|
||||
@@ -81,3 +77,35 @@ export function scanMoviesLibrary(libraryRoot: string, libraryId: string): Movie
|
||||
|
||||
return movies.sort((a, b) => a.title.localeCompare(b.title))
|
||||
}
|
||||
|
||||
export function moviesFromDb(libraryId: string): Movie[] {
|
||||
const db = getDb()
|
||||
const rows = db
|
||||
.prepare(`SELECT * FROM media_items WHERE library_id = ? AND item_type = 'movie' ORDER BY title`)
|
||||
.all(libraryId) as Array<{
|
||||
item_key: string
|
||||
title: string | null
|
||||
year: number | null
|
||||
plot: string | null
|
||||
genres: string | null
|
||||
metadata: string | null
|
||||
file_path: string | null
|
||||
}>
|
||||
|
||||
return rows.map((row) => {
|
||||
const meta = row.metadata ? JSON.parse(row.metadata) : {}
|
||||
const idPart = row.item_key.split(':movie:')[1] ?? row.item_key
|
||||
return {
|
||||
id: idPart,
|
||||
title: row.title ?? decodeURIComponent(idPart),
|
||||
year: row.year ?? null,
|
||||
plot: row.plot ?? null,
|
||||
rating: meta.rating ?? null,
|
||||
genres: row.genres ? JSON.parse(row.genres) : [],
|
||||
runtime: meta.runtime ?? null,
|
||||
posterUrl: meta.posterUrl ?? null,
|
||||
backdropUrl: meta.backdropUrl ?? null,
|
||||
videoPath: row.file_path ?? '',
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user