UNPKG

@sinedied/mini-scraper

Version:

Artwork scraper for handheld emulators.

202 lines (201 loc) 8.19 kB
import path from 'node:path'; import fs from 'node:fs/promises'; import createDebug from 'debug'; import { composeImageTo, resizeImageTo } from '../image.js'; import { ArtType } from '../art.js'; import { getArtTypes, getMachine } from '../libretro.js'; import { pathExists } from '../file.js'; const debug = createDebug('muos'); const separateArtworks = false; const artworkBasePath = '/MUOS/info/catalogue/'; // Maps machines to MUOS catalogue folders const machineFolders = { 'Nintendo - Game Boy Color': 'Nintendo Game Boy Color', 'Nintendo - Game Boy Advance': 'Nintendo Game Boy Advance', 'Nintendo - Game Boy': 'Nintendo Game Boy', 'Nintendo - Super Nintendo Entertainment System': 'Nintendo SNES-SFC', 'Nintendo - Nintendo 64DD': 'Nintendo N64', 'Nintendo - Nintendo 64': 'Nintendo N64', 'Nintendo - Family Computer Disk System': 'Nintendo NES-Famicom', 'Nintendo - Nintendo Entertainment System': 'Nintendo NES-Famicom', 'Nintendo - Nintendo DSi': 'Nintendo DS', 'Nintendo - Nintendo DS': 'Nintendo DS', 'Nintendo - Pokemon Mini': 'Nintendo Pokemon Mini', 'Nintendo - Virtual Boy': 'Nintendo Virtual Boy', 'Handheld Electronic Game': 'Handheld Electronic - Game and Watch', 'Sega - 32X': 'Sega 32X', 'Sega - Dreamcast': 'Sega Dreamcast', 'Sega - Mega-CD - Sega CD': 'Sega Mega CD - Sega CD', 'Sega - Mega Drive - Genesis': 'Sega Mega Drive - Genesis', 'Sega - Game Gear': 'Sega Game Gear', 'Sega - Master System - Mark III': 'Sega Master System', 'Sega - Saturn': 'Sega Saturn', 'Sony - PlayStation Portable': 'Sony Playstation Portable', 'Sony - PlayStation': 'Sony PlayStation', 'Sega - Naomi 2': 'Sega Atomiswave Naomi', 'Sega - Naomi': 'Sega Atomiswave Naomi', 'Amstrad - CPC': 'Amstrad', 'Atari - ST': 'Atari ST-STE-TT-Falcon', 'Atari - 2600': 'Atari 2600', 'Atari - 5200': 'Atari 5200', 'Atari - 7800': 'Atari 7800', 'Atari - Jaguar': 'Atari Jaguar', 'Atari - Lynx': 'Atari Lynx', 'Bandai - WonderSwan Color': 'Bandai WonderSwan-Color', 'Bandai - WonderSwan': 'Bandai WonderSwan-Color', 'Coleco - ColecoVision': 'ColecoVision', 'Commodore - Amiga': 'Commodore Amiga', 'Commodore - VIC-20': 'Commodore VIC-20', 'Commodore - 64': 'Commodore C64', 'FBNeo - Arcade Games': 'Arcade', 'GCE - Vectrex': 'GCE-Vectrex', 'GamePark - GP32': undefined, MAME: 'Arcade', 'Microsoft - MSX2': 'Microsoft - MSX', 'Microsoft - MSX': 'Microsoft - MSX', 'Mattel - Intellivision': 'Mattel - Intellivision', 'NEC - PC Engine CD - TurboGrafx-CD': 'NEC PC Engine CD', 'NEC - PC Engine SuperGrafx': 'NEC PC Engine SuperGrafx', 'NEC - PC Engine - TurboGrafx 16': 'NEC PC Engine', 'SNK - Neo Geo CD': 'SNK Neo Geo CD', 'SNK - Neo Geo Pocket Color': 'SNK Neo Geo Pocket - Color', 'SNK - Neo Geo Pocket': 'SNK Neo Geo Pocket - Color', 'SNK - Neo Geo': 'SNK Neo Geo', 'Magnavox - Odyssey2': 'Odyssey2 - VideoPac', 'TIC-80': 'TIC-80', 'Sharp - X68000': 'Sharp X68000', 'Watara - Supervision': 'Watara Supervision', DOS: 'DOS', DOOM: 'DOOM', ScummVM: 'ScummVM', Atomiswave: 'Sega Atomiswave Naomi' }; // Maps artwork types to folders const artFolders = { [ArtType.Boxart]: 'box', [ArtType.Snap]: 'preview', [ArtType.Title]: 'preview' }; const iniFiles = ['theme/override/muxfavourite.txt', 'theme/override/muxhistory.txt', 'theme/override/muxplore.txt']; let volumeRootPath; let hasUpdatedIni = false; export async function useSeparateArtworks(_options) { return separateArtworks; } export async function getArtPath(filePath, machine, type = ArtType.Boxart) { const machineFolder = machineFolders[machine]; if (!machineFolder) { throw new Error(`Machine "${machine}" not supported by MUOS`); } if (!type) { throw new Error(`Artwork type not specified for "${machine}"`); } const fileName = path.basename(filePath, path.extname(filePath)); const root = await findVolumeRoot(filePath); return path.join(root, artworkBasePath, machineFolder, artFolders[type], `${fileName}.png`); } export async function exportArtwork(art1Url, art2Url, artPath, options) { if (separateArtworks) { if (!art1Url) return false; debug(`Found art URL: "${art1Url}"`); await resizeImageTo(art1Url, artPath, { width: options.width, height: options.height }); return true; } const artTypes = getArtTypes(options); if (artTypes.art2 && (art1Url ?? art2Url)) { debug(`Found art URL(s): "${art1Url}" / "${art2Url}"`); await composeImageTo(art1Url, art2Url, artPath, { width: options.width, height: options.height }); } else if (art1Url) { debug(`Found art URL: "${art1Url}"`); await resizeImageTo(art1Url, artPath, { width: options.width, height: options.height }); } else { return false; } return true; } export async function cleanupArtwork(targetPath, romFolders, _options) { let removed = 0; for (const romFolder of romFolders) { const machine = getMachine(romFolder, true) ?? ''; const machineFolder = machineFolders[machine]; if (!machineFolder) { debug(`Machine "${machine}" not supported by MUOS, skipping`); continue; } const root = await findVolumeRoot(targetPath); const machineArtPath = path.join(root, artworkBasePath, machineFolder); await fs.rm(machineArtPath, { recursive: true }); removed++; } console.info(`Removed ${removed} folders`); } export async function prepareMachine(folderPath, _machine, options) { const root = await findVolumeRoot(folderPath); await updateIniFiles(root, options); } async function findVolumeRoot(targetPath) { if (volumeRootPath) { return volumeRootPath; } const absolutePath = path.resolve(targetPath); const parts = absolutePath.split(path.sep); for (let i = parts.length; i > 0; i--) { const currentPath = parts.slice(0, i).join(path.sep); const autorunPath = path.join(currentPath, 'autorun.inf'); try { await fs.access(autorunPath); volumeRootPath = currentPath; debug(`Found muOS root at "${currentPath}"`); return currentPath; } catch { // Ignore errors } } debug(`Could not determine muOS root for "${targetPath}" (looking for autorun.inf)`); volumeRootPath = path.dirname(targetPath); return volumeRootPath; } async function updateIniFiles(rootPath, options) { // Only do it once per run if (hasUpdatedIni) return; hasUpdatedIni = true; const iniFilesToUpdate = iniFiles.map((file) => path.join(rootPath, file)); for (const iniFile of iniFilesToUpdate) { try { await fs.mkdir(path.dirname(iniFile), { recursive: true }); if (await pathExists(iniFile)) { let content = await fs.readFile(iniFile, 'utf8'); if (content.includes('CONTENT_WIDTH')) { content = content.replace(/^CONTENT_WIDTH=\d+$/m, `CONTENT_WIDTH=${options.width}`); console.info(`Updated theme override: "${iniFile}" with CONTENT_WIDTH=${options.width}`); await fs.writeFile(iniFile, content); } else { console.info(`Theme override file "${iniFile}" exists, but CONTENT_WIDTH not set, skipping update`); } continue; } // Create the file if it doesn't exist const content = `[misc]\nCONTENT_WIDTH=${options.width}\n`; await fs.writeFile(iniFile, content); console.info(`Created theme override: "${iniFile}"`); } catch (_error) { const error = _error; console.error(`Failed to update theme override file "${iniFile}": ${error?.message}`); } } } const muos = { useSeparateArtworks, getArtPath, prepareMachine, exportArtwork, cleanupArtwork }; export default muos;