add library management

This commit is contained in:
2026-03-25 16:40:01 -04:00
parent ff3cfe7ec3
commit 90528c4768
9 changed files with 552 additions and 53 deletions

View File

@@ -1,12 +1,17 @@
import path from 'path'
import fs from 'fs'
import type { Library } from '@/types'
import type { Library, LibraryType } from '@/types'
const CONFIG_PATH = path.resolve(process.cwd(), 'libraries.json')
const CONFIG_TMP = CONFIG_PATH + '.tmp'
export function getLibraries(): Library[] {
const raw = fs.readFileSync(CONFIG_PATH, 'utf-8')
return JSON.parse(raw) as Library[]
try {
const raw = fs.readFileSync(CONFIG_PATH, 'utf-8')
return JSON.parse(raw) as Library[]
} catch {
return []
}
}
export function getLibrary(id: string): Library | undefined {
@@ -36,3 +41,87 @@ export function resolveAndJail(libraryRoot: string, subpath: string): string {
}
return resolved
}
/**
* Slugifies a name into a URL-safe id.
* e.g. "My Games Library" → "my-games-library"
*/
function slugify(name: string): string {
return name
.toLowerCase()
.trim()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '')
}
/**
* Atomically writes the library list to disk.
*/
function writeLibraries(libraries: Library[]): void {
const json = JSON.stringify(libraries, null, 2) + '\n'
fs.writeFileSync(CONFIG_TMP, json, 'utf-8')
fs.renameSync(CONFIG_TMP, CONFIG_PATH)
}
/**
* Adds a new library. Validates the path exists as a directory.
* Throws a descriptive Error on failure.
*/
export function addLibrary(name: string, libraryPath: string, type: LibraryType): Library {
const trimmedName = name.trim()
const trimmedPath = libraryPath.trim()
if (!trimmedName) throw new Error('Name is required.')
if (!trimmedPath) throw new Error('Path is required.')
// Validate the path exists and is a directory
const resolved = path.isAbsolute(trimmedPath)
? trimmedPath
: path.resolve(process.cwd(), trimmedPath)
let stat: fs.Stats
try {
stat = fs.statSync(resolved)
} catch {
throw new Error(`Path does not exist: ${trimmedPath}`)
}
if (!stat.isDirectory()) {
throw new Error(`Path is not a directory: ${trimmedPath}`)
}
const libraries = getLibraries()
// Generate a unique id
const baseId = slugify(trimmedName) || 'library'
let id = baseId
let suffix = 2
while (libraries.some((lib) => lib.id === id)) {
id = `${baseId}-${suffix++}`
}
// Reject exact duplicate names (same name, case-insensitive)
const duplicate = libraries.find(
(lib) => lib.name.toLowerCase() === trimmedName.toLowerCase()
)
if (duplicate) {
throw new Error(`A library named "${trimmedName}" already exists.`)
}
const newLibrary: Library = { id, name: trimmedName, path: trimmedPath, type }
writeLibraries([...libraries, newLibrary])
return newLibrary
}
/**
* Removes a library by id. Returns true if removed, false if not found.
*/
export function removeLibrary(id: string): boolean {
const libraries = getLibraries()
const index = libraries.findIndex((lib) => lib.id === id)
if (index === -1) return false
const updated = libraries.filter((lib) => lib.id !== id)
writeLibraries(updated)
return true
}