Skip to main content
The LibraryService class handles game discovery, cataloging, and management. It scans ROM directories, computes file hashes for reliable identification, and maintains a persistent library with support for zip archives.

Constructor

const libraryService = new LibraryService();
Automatically loads the library configuration and existing game data from disk.

Configuration

getConfig()

Get the current library configuration.
const config = libraryService.getConfig();
Returns: LibraryConfig
LibraryConfig
object
systems
GameSystem[]
Array of configured game systems
romsBasePath
string
Base directory for ROM files
scanRecursive
boolean
Whether to scan subdirectories
autoScan
boolean
Whether to auto-scan on startup

setRomsBasePath()

Set the base directory for ROM files. Each system gets a subdirectory under this path.
await libraryService.setRomsBasePath('/Users/username/ROMs');
basePath
string
required
Absolute path to the ROMs base directory
Returns: Promise<void>

System management

getSystems()

Get all configured game systems.
const systems = libraryService.getSystems();
Returns: GameSystem[]
GameSystem
object
id
string
Unique system identifier (e.g., 'nes', 'snes')
name
string
Display name (e.g., 'Nintendo Entertainment System')
shortName
string
Short name for folders (e.g., 'NES')
manufacturer
string
Console manufacturer
generation
number
Console generation number
releaseYear
number
Release year
extensions
string[]
Supported file extensions (e.g., ['.nes', '.unf'])
romsPath
string
Directory path where ROMs for this system are stored

addSystem()

Add a new game system to the configuration.
await libraryService.addSystem({
  id: 'psx',
  name: 'PlayStation',
  shortName: 'PSX',
  manufacturer: 'Sony',
  generation: 5,
  releaseYear: 1994,
  extensions: ['.bin', '.cue', '.iso'],
  romsPath: '/path/to/psx/roms'
});
system
GameSystem
required
System configuration object
Returns: Promise<void>

removeSystem()

Remove a system and all its games from the library.
await libraryService.removeSystem('psx');
systemId
string
required
System identifier
Returns: Promise<void>
This also deletes cached ROM files extracted from zip archives for this system.

updateSystemPath()

Update the ROM directory path for a system.
await libraryService.updateSystemPath('nes', '/new/path/to/nes/roms');
systemId
string
required
System identifier
romsPath
string
required
New directory path
Returns: Promise<void>

Game management

getGames()

Get all games, optionally filtered by system.
// Get all games
const allGames = libraryService.getGames();

// Get NES games only
const nesGames = libraryService.getGames('nes');
systemId
string
Optional system filter
Returns: Game[]
Game
object
id
string
Unique game ID (SHA-256 hash of ROM content)
title
string
Game title
system
string
System display name
systemId
string
System identifier
romPath
string
Path to the ROM file
romMtime
number
File modification timestamp (for cache invalidation)
romHashes
RomHashes
ROM hashes (CRC32, SHA-1, MD5) used for matching against databases
sourceArchivePath
string
Original zip file path (if extracted from zip)
coverArt
string
Cover art URL (e.g., artwork://abc123.png)
coverArtAspectRatio
number
Aspect ratio for dynamic card sizing (0.4-1.8)
metadata
object
Game metadata (developer, publisher, genre, description, etc.)

getGame()

Get a single game by ID.
const game = libraryService.getGame('abc123def456...');
gameId
string
required
Game ID (SHA-256 hash)
Returns: Game | undefined

addGame()

Manually add a single game to the library.
const game = await libraryService.addGame(
  '/path/to/game.nes',
  'nes'
);
romPath
string
required
Full path to the ROM file
systemId
string
required
System identifier
Returns: Promise<Game | null> - Returns null if the file is not a valid ROM for the system
For zip files on non-arcade systems, this automatically extracts the first matching ROM and caches it.

removeGame()

Remove a game from the library.
await libraryService.removeGame('abc123def456...');
gameId
string
required
Game ID
Returns: Promise<void>
If the game was extracted from a zip archive, the cached ROM file is automatically deleted.

updateGame()

Update game metadata.
await libraryService.updateGame('abc123def456...', {
  title: 'Super Mario Bros.',
  metadata: {
    developer: 'Nintendo',
    releaseDate: '1985-09-13'
  }
});
gameId
string
required
Game ID
updates
Partial<Game>
required
Fields to update
Returns: Promise<void>

Scanning

scanDirectory()

Scan a directory for ROM files and add them to the library. This is the core scanning method that powers all other scan operations.
// Scan without system filter (auto-detect from folder names)
const games = await libraryService.scanDirectory('/path/to/roms');

// Scan with system filter
const nesGames = await libraryService.scanDirectory('/path/to/nes/roms', 'nes');
directoryPath
string
required
Directory to scan
systemId
string
Optional system filter. If omitted, auto-detects system from folder names.
Returns: Promise<Game[]> - Array of discovered games Emits: scanProgress event for each game processed
ScanProgressEvent
object
Progress event emitted during scanning
game
Game
The game that was just discovered or re-verified
isNew
boolean
Whether this game is newly added (true) or already existed (false)
processed
number
Number of files processed so far
total
number
Total number of ROM files found
skipped
number
Number of files skipped via mtime cache (no re-hash needed)

scanSystemFolders()

Scan all configured system ROM directories.
const allGames = await libraryService.scanSystemFolders();
Returns: Promise<Game[]> - Array of all discovered games across all systems Emits: scanProgress event for each game processed

ROM hashing

computeRomHashes()

Compute CRC32, SHA-1, and MD5 hashes for a ROM file in a single pass. Also returns the SHA-256 game ID.
const { gameId, hashes } = await libraryService.computeRomHashes('/path/to/game.nes');

console.log(gameId); // SHA-256 hash (64 hex chars)
console.log(hashes.crc32); // CRC32 (8 hex chars)
console.log(hashes.sha1); // SHA-1 (40 hex chars)
console.log(hashes.md5); // MD5 (32 hex chars)
romPath
string
required
Path to the ROM file
Returns: Promise<{ gameId: string; hashes: RomHashes }>
RomHashes
object
crc32
string
CRC32 checksum (8 hex chars)
sha1
string
SHA-1 hash (40 hex chars)
md5
string
MD5 hash (32 hex chars)
Throws: Error if the file is unreadable
This method streams the file and computes all hashes in parallel, making it efficient for large ROM files.

Events

LibraryService extends EventEmitter and emits the following events:
scanProgress
ScanProgressEvent
Emitted for each game discovered during a scan. This allows the UI to show real-time progress and display games as they’re found.

Performance optimizations

LibraryService implements several optimizations for fast scanning:

mtime cache

Files are only re-hashed if their modification time has changed. Scanning unchanged directories is near-instant.

Reverse indexes

O(1) lookups by ROM path and archive path using internal maps:
// Internal implementation (not exposed in public API)
private romPathIndex: Map<string, string> = new Map();
private archivePathIndex: Map<string, string> = new Map();

Prioritized processing

New (unknown) files are processed first so they appear in the UI immediately. Known files are verified in the background.

Bounded concurrency

Hashes 4 files in parallel to maximize throughput without overwhelming the disk I/O:
const HASH_CONCURRENCY = 4;

Zip extraction caching

Extracted ROMs from zip archives are cached with hash-prefixed filenames to avoid re-extraction on subsequent scans.

Usage example

import { LibraryService } from './LibraryService';

const libraryService = new LibraryService();

// Listen for scan progress
libraryService.on('scanProgress', (event) => {
  console.log(`Found: ${event.game.title} (${event.processed}/${event.total})`);
  if (event.isNew) {
    console.log('  → New game!');
  }
});

// Configure ROMs base path
await libraryService.setRomsBasePath('/Users/username/ROMs');

// Scan all system folders
const games = await libraryService.scanSystemFolders();
console.log(`Library: ${games.length} games`);

// Get games for a specific system
const nesGames = libraryService.getGames('nes');

// Update game metadata
const game = nesGames[0];
await libraryService.updateGame(game.id, {
  title: 'Better Title',
  metadata: { developer: 'Nintendo' }
});

Error handling

Scanning continues even if individual files fail:
try {
  const games = await libraryService.scanDirectory('/path/to/roms');
  console.log(`Found ${games.length} games`);
} catch (error) {
  // Only fatal directory access errors throw
  console.error('Scan failed:', error.message);
}

// Individual file errors are logged but don't throw
// Check logs for: "Skipping unreadable ROM: <path>"

Source reference

  • Implementation: apps/desktop/src/main/services/LibraryService.ts:53
  • Usage example: apps/desktop/src/main/ipc/handlers.ts:22

Build docs developers (and LLMs) love