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,6 +1,7 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import type { Game, GameSeries } from '@/types'
|
||||
import { getDb } from './db'
|
||||
import { HIDDEN_FILES, fileApiUrl, thumbnailApiUrl, findFile } from './media-utils'
|
||||
|
||||
/**
|
||||
@@ -119,3 +120,64 @@ export function scanGamesLibrary(libraryRoot: string, libraryId: string): (Game
|
||||
|
||||
return results.sort((a, b) => a.title.localeCompare(b.title))
|
||||
}
|
||||
|
||||
export function gamesFromDb(libraryId: string): (Game | GameSeries)[] {
|
||||
const db = getDb()
|
||||
|
||||
type DbRow = {
|
||||
item_key: string
|
||||
item_type: string
|
||||
parent_key: string | null
|
||||
title: string | null
|
||||
metadata: string | null
|
||||
}
|
||||
|
||||
const allRows = db
|
||||
.prepare(`SELECT item_key, item_type, parent_key, title, metadata
|
||||
FROM media_items
|
||||
WHERE library_id = ? AND item_type IN ('game', 'game_series')
|
||||
ORDER BY title`)
|
||||
.all(libraryId) as DbRow[]
|
||||
|
||||
const seriesMap = new Map<string, GameSeries>()
|
||||
const standaloneGames: Game[] = []
|
||||
|
||||
// First pass: build series
|
||||
for (const row of allRows) {
|
||||
if (row.item_type !== 'game_series') continue
|
||||
const meta = row.metadata ? JSON.parse(row.metadata) : {}
|
||||
const idPart = row.item_key.split(':game_series:')[1] ?? row.item_key
|
||||
seriesMap.set(row.item_key, {
|
||||
id: idPart,
|
||||
title: row.title ?? decodeURIComponent(idPart),
|
||||
coverUrl: meta.coverUrl ?? null,
|
||||
wideCoverUrl: meta.wideCoverUrl ?? null,
|
||||
games: [],
|
||||
})
|
||||
}
|
||||
|
||||
// Second pass: attach games
|
||||
for (const row of allRows) {
|
||||
if (row.item_type !== 'game') continue
|
||||
const meta = row.metadata ? JSON.parse(row.metadata) : {}
|
||||
const idPart = row.item_key.split(':game:')[1] ?? row.item_key
|
||||
const game: Game = {
|
||||
id: idPart,
|
||||
title: row.title ?? decodeURIComponent(idPart),
|
||||
coverUrl: meta.coverUrl ?? null,
|
||||
wideCoverUrl: meta.wideCoverUrl ?? null,
|
||||
zipFiles: meta.zipFiles ?? [],
|
||||
}
|
||||
if (row.parent_key && seriesMap.has(row.parent_key)) {
|
||||
seriesMap.get(row.parent_key)!.games.push(game)
|
||||
} else {
|
||||
standaloneGames.push(game)
|
||||
}
|
||||
}
|
||||
|
||||
const results: (Game | GameSeries)[] = [
|
||||
...Array.from(seriesMap.values()),
|
||||
...standaloneGames,
|
||||
]
|
||||
return results.sort((a, b) => a.title.localeCompare(b.title))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user