Skip to main content
The ArtworkService class manages the artwork and metadata pipeline: hashing ROMs, querying ScreenScraper, downloading images, and updating the library. It extends EventEmitter to report progress to the renderer.

Constructor

const artworkService = new ArtworkService(libraryService);
libraryService
LibraryService
required
LibraryService instance for accessing game data
Automatically loads configuration from disk and backfills aspect ratios for legacy games.

Credentials management

hasCredentials()

Check whether ScreenScraper user credentials are configured.
if (!artworkService.hasCredentials()) {
  console.log('Please configure ScreenScraper credentials');
}
Returns: boolean

validateCredentials()

Validate credentials against ScreenScraper before storing them. Makes a lightweight API call to check if the credentials are accepted.
const result = await artworkService.validateCredentials(
  'myUsername',
  'myPassword'
);

if (result.valid) {
  console.log('Credentials are valid');
} else {
  console.error(`Invalid: ${result.error}`);
  console.error(`Error code: ${result.errorCode}`);
}
userId
string
required
ScreenScraper username
userPassword
string
required
ScreenScraper password
Returns: Promise<{ valid: boolean; error?: string; errorCode?: ArtworkErrorCode }>
valid
boolean
Whether credentials are valid
error
string
Error message (if validation failed)
errorCode
ArtworkErrorCode
Structured error code: 'auth-failed', 'rate-limited', 'config-error', 'network-error', 'not-found'

setCredentials()

Store ScreenScraper user credentials to disk.
await artworkService.setCredentials('myUsername', 'myPassword');
userId
string
required
ScreenScraper username
userPassword
string
required
ScreenScraper password
Returns: Promise<void>
Credentials are stored in plain text in artwork-config.json. For production use, consider encrypting them with Electron’s safeStorage API.

clearCredentials()

Remove stored credentials.
await artworkService.clearCredentials();
Returns: Promise<void>

Artwork syncing

syncGame()

Run the full artwork pipeline for a single game: query ScreenScraper by hash, fall back to name search, download artwork, and update metadata.
const success = await artworkService.syncGame('abc123def456...');

if (success) {
  console.log('Artwork downloaded');
} else {
  console.log('Game not found in ScreenScraper');
}
gameId
string
required
Game ID (from LibraryService)
force
boolean
Force re-sync even if artwork already exists (default: false)
Returns: Promise<boolean> - True if artwork was found and downloaded Throws: ScreenScraperError with errorCode: 'auth-failed' if credentials are invalid
This method respects rate limiting (1.1s between requests) and includes automatic retry with backoff on rate limit errors.

syncAllGames()

Sync artwork for all games that don’t have cover art yet. Processes games serially with rate limiting.
const status = await artworkService.syncAllGames();

console.log(`Found: ${status.found}`);
console.log(`Not found: ${status.notFound}`);
console.log(`Errors: ${status.errors}`);
Returns: Promise<ArtworkSyncStatus>
ArtworkSyncStatus
object
inProgress
boolean
Whether sync is still running
processed
number
Number of games processed
total
number
Total games to process
found
number
Games with artwork found
notFound
number
Games not found in ScreenScraper
errors
number
Number of errors encountered
Emits:
  • progress - For each game (see ArtworkProgress below)
  • syncComplete - When batch finishes (with final status)
If a sync is already in progress, this returns immediately with inProgress: true.

syncGames()

Sync artwork for a specific list of game IDs. Used for auto-sync after ROM import to avoid re-syncing the entire library.
const gameIds = ['abc123...', 'def456...'];
const status = await artworkService.syncGames(gameIds);
gameIds
string[]
required
Array of game IDs to sync
Returns: Promise<ArtworkSyncStatus> Emits: Same events as syncAllGames()
Automatically filters out games that already have cover art or don’t exist in the library.

cancelSync()

Cancel an in-progress bulk sync.
artworkService.cancelSync();
Returns: void
The current game will finish processing, but no additional games will be synced.

getSyncStatus()

Get current sync status.
const status = artworkService.getSyncStatus();

if (status.inProgress) {
  console.log('Sync is running');
}
Returns: { inProgress: boolean }

Artwork storage

getArtworkDirectory()

Returns the artwork directory path, creating it if needed.
const artworkDir = artworkService.getArtworkDirectory();
// e.g., '/Users/username/Library/Application Support/GameLord/artwork'
Returns: string

downloadArtwork()

Download an image from a URL to the local artwork directory. Includes a 30-second timeout and automatic redirect following.
const localPath = await artworkService.downloadArtwork(
  'https://example.com/image.png',
  'game-id.png'
);
imageUrl
string
required
Source image URL
filename
string
required
Destination filename (relative to artwork directory)
Returns: Promise<string> - Full local path to the saved file Throws: Error if download fails or times out

Events

ArtworkService extends EventEmitter and emits the following events:
progress
ArtworkProgress
Emitted for each game during batch sync
gameId
string
Game ID
gameTitle
string
Game title
phase
string
Current phase: 'hashing', 'querying', 'downloading', 'done', 'not-found', 'error'
current
number
Current game index (1-based)
total
number
Total games in batch
coverArt
string
Cover art URL (only in 'done' phase)
coverArtAspectRatio
number
Aspect ratio (only in 'done' phase)
error
string
Error message (only in 'error' phase)
errorCode
ArtworkErrorCode
Error code (only in 'error' phase)
syncComplete
ArtworkSyncStatus
Emitted when a batch sync finishes (successfully or after cancellation)

Rate limiting

ArtworkService enforces ScreenScraper’s rate limits:
  • 1.1 seconds minimum delay between API requests
  • 10 seconds backoff after a 429 (rate limited) response
  • 30 seconds timeout for image downloads
Rate limits are automatically managed internally. You don’t need to implement throttling in your own code.

Error handling

All errors from ScreenScraper include a structured errorCode:
try {
  await artworkService.syncGame(gameId);
} catch (error) {
  if (error instanceof ScreenScraperError) {
    switch (error.errorCode) {
      case 'auth-failed':
        console.error('Invalid credentials');
        break;
      case 'rate-limited':
        console.error('Rate limit exceeded');
        break;
      case 'not-found':
        console.error('Game not in database');
        break;
      case 'network-error':
        console.error('Network issue');
        break;
      case 'config-error':
        console.error('Missing configuration');
        break;
    }
  }
}
Auth and config errors (auth-failed, config-error) stop batch syncs immediately since continuing would fail for all games.

Usage example

import { ArtworkService } from './ArtworkService';
import { LibraryService } from './LibraryService';

const libraryService = new LibraryService();
const artworkService = new ArtworkService(libraryService);

// Set up credentials
const validation = await artworkService.validateCredentials(
  'username',
  'password'
);

if (validation.valid) {
  await artworkService.setCredentials('username', 'password');
}

// Listen for progress
artworkService.on('progress', (progress) => {
  console.log(`[${progress.current}/${progress.total}] ${progress.gameTitle}`);
  console.log(`  Phase: ${progress.phase}`);
  
  if (progress.phase === 'done') {
    console.log(`  Cover art: ${progress.coverArt}`);
  } else if (progress.phase === 'error') {
    console.error(`  Error: ${progress.error}`);
  }
});

artworkService.on('syncComplete', (status) => {
  console.log(`\nSync complete:`);
  console.log(`  Found: ${status.found}`);
  console.log(`  Not found: ${status.notFound}`);
  console.log(`  Errors: ${status.errors}`);
});

// Sync all games
await artworkService.syncAllGames();

// Or sync specific games
const newGameIds = ['abc123...', 'def456...'];
await artworkService.syncGames(newGameIds);

Implementation details

Aspect ratio clamping

Cover art aspect ratios are clamped to [0.4, 1.8] to accommodate both portrait and landscape box art:
const rawRatio = dimensions.width / dimensions.height;
const coverArtAspectRatio = Math.max(0.4, Math.min(1.8, rawRatio));

Hash matching priority

The service queries ScreenScraper in this order:
  1. MD5 hash (fastest, most reliable)
  2. Fallback to name search (if hash lookup fails)
MD5 hashes are pre-computed during ROM scan and stored in game.romHashes.md5.

Artwork URL priority

When multiple artwork types are available, the service prefers:
  1. boxArt2d (front cover)
  2. boxArt3d (3D box render)
  3. screenshot (in-game screenshot)

Source reference

  • Implementation: apps/desktop/src/main/services/ArtworkService.ts:36
  • Usage example: apps/desktop/src/main/ipc/handlers.ts:23

Build docs developers (and LLMs) love