This commit is contained in:
103
src/lib/movie-metadata.ts
Normal file
103
src/lib/movie-metadata.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user