104 lines
3.2 KiB
TypeScript
104 lines
3.2 KiB
TypeScript
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<void>((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
|
|
}
|