import fs from 'fs' import path from 'path' import type { Library } from '@/types' import { getDb } from './db' import { resolveLibraryRoot } from './libraries' import { parseMovieNfo } from './nfo' /** * Import NFO metadata for Movie items in a library. * - Reads .nfo file matching each movie file * - If importMetadataOnly=false: skip items that already have metadata (title/year/plot/genres) * - If importMetadataOnly=true: update all items regardless of existing metadata */ export async function importMovieMetadata( library: Library, importMetadataOnly: boolean = false ): Promise<{ imported: number; skipped: number }> { const db = getDb() const libraryRoot = resolveLibraryRoot(library) let imported = 0 let skipped = 0 // Get all movies in the library const movies = db .prepare( `SELECT item_key, file_path, title, year, plot, genres FROM media_items WHERE library_id = ? AND item_type = 'movie' AND file_path IS NOT NULL` ) .all(library.id) as Array<{ item_key: string; file_path: string; title: string | null; year: number | null; plot: string | null; genres: string | null }> const updateItem = db.prepare(` UPDATE media_items SET title = @title, year = @year, plot = @plot, genres = @genres WHERE item_key = @item_key `) const BATCH_SIZE = 50 for (let i = 0; i < movies.length; i += BATCH_SIZE) { const batch = movies.slice(i, i + BATCH_SIZE) db.transaction(() => { for (const item of batch) { // Check if we should skip this item if (!importMetadataOnly && hasMetadata(item)) { skipped++ continue } const videoPath = path.join(libraryRoot, item.file_path) const dir = path.dirname(videoPath) const baseNameWithoutExt = path.basename(videoPath, path.extname(videoPath)) const nfoPath = path.join(dir, `${baseNameWithoutExt}.nfo`) try { if (fs.existsSync(nfoPath)) { const nfoData = parseMovieNfo(nfoPath) if (nfoData) { updateItem.run({ item_key: item.item_key, title: nfoData.title ?? item.title, year: nfoData.year ?? item.year, plot: nfoData.plot ?? item.plot, genres: nfoData.genres.length > 0 ? JSON.stringify(nfoData.genres) : item.genres, }) imported++ } else { skipped++ } } else { skipped++ } } catch { skipped++ } } })() await new Promise((r) => setImmediate(r)) } console.log( `[movie-metadata] Imported metadata for ${imported} movies in "${library.name}" (${importMetadataOnly ? 'full' : 'incremental'})` ) return { imported, skipped } } /** * Check if a media item already has metadata populated. * Returns true if ANY of: title, year, plot, or genres are populated. */ function hasMetadata(item: { title: string | null year: number | null plot: string | null genres: string | null }): boolean { if (item.title) return true if (item.year) return true if (item.plot) return true if (item.genres) return true return false }