Files
hitstar/src/server/game/deck.js
2025-10-12 23:30:45 +02:00

117 lines
4.0 KiB
JavaScript

import fs from 'fs';
import path from 'path';
import { parseFile as mmParseFile } from 'music-metadata';
import { DATA_DIR } from '../config.js';
import { loadYearsIndex } from '../years.js';
import { shuffle } from './state.js';
/**
* Get list of available playlists (subdirectories in data folder).
* Also includes a special "default" playlist if there are audio files in the root data directory.
* @returns {Array<{id: string, name: string, trackCount: number}>}
*/
export function getAvailablePlaylists() {
try {
const entries = fs.readdirSync(DATA_DIR, { withFileTypes: true });
const playlists = [];
// Check for files in root directory (legacy/default playlist)
const rootFiles = entries.filter(
(e) => e.isFile() && /\.(mp3|wav|m4a|ogg|opus)$/i.test(e.name)
);
if (rootFiles.length > 0) {
playlists.push({
id: 'default',
name: 'Default (Root Folder)',
trackCount: rootFiles.length,
});
}
// Check subdirectories for playlists
const subdirs = entries.filter((e) => e.isDirectory());
for (const dir of subdirs) {
const dirPath = path.join(DATA_DIR, dir.name);
const dirFiles = fs.readdirSync(dirPath).filter((f) => /\.(mp3|wav|m4a|ogg|opus)$/i.test(f));
if (dirFiles.length > 0) {
playlists.push({
id: dir.name,
name: dir.name,
trackCount: dirFiles.length,
});
}
}
return playlists;
} catch (error) {
console.error('Error reading playlists:', error);
return [];
}
}
/**
* Load a deck of tracks from a specific playlist.
* @param {string} [playlistId='default'] - The playlist ID to load from
* @returns {Promise<Array>} Shuffled array of track objects
*/
export async function loadDeck(playlistId = 'default') {
// Load the years index for this specific playlist
const years = loadYearsIndex(playlistId);
// Determine the directory to load from
const targetDir = playlistId === 'default' ? DATA_DIR : path.join(DATA_DIR, playlistId);
// Validate that the directory exists
if (!fs.existsSync(targetDir)) {
console.error(`Playlist directory not found: ${targetDir}`);
return [];
}
const files = fs.readdirSync(targetDir).filter((f) => /\.(mp3|wav|m4a|ogg|opus)$/i.test(f));
if (files.length === 0) {
console.warn(`No audio files found in playlist: ${playlistId}`);
return [];
}
// Process files in batches to avoid "too many open files" error
const BATCH_SIZE = 50; // Process 50 files at a time
const tracks = [];
for (let i = 0; i < files.length; i += BATCH_SIZE) {
const batch = files.slice(i, i + BATCH_SIZE);
const batchTracks = await Promise.all(
batch.map(async (f) => {
const fp = path.join(targetDir, f);
// For file paths, we need to include the playlist subdirectory if not default
const relativeFile = playlistId === 'default' ? f : path.join(playlistId, f);
// Get metadata from JSON first (priority), then from audio file as fallback
const jsonMeta = years[f];
let year = jsonMeta?.year ?? null;
let title = jsonMeta?.title ?? path.parse(f).name;
let artist = jsonMeta?.artist ?? '';
// Only parse audio file if JSON doesn't have title or artist metadata
// Note: year is ONLY taken from years.json, never from audio file
if (!jsonMeta || !jsonMeta.title || !jsonMeta.artist) {
try {
const meta = await mmParseFile(fp, { duration: false });
title = jsonMeta?.title || meta.common.title || title;
artist = jsonMeta?.artist || meta.common.artist || artist;
// year remains from years.json only - do not use meta.common.year
} catch (err) {
// Log error but continue processing
console.warn(`Failed to parse metadata for ${f}:`, err.message);
}
}
return { id: f, file: relativeFile, title, artist, year };
})
);
tracks.push(...batchTracks);
}
console.log(`Loaded ${tracks.length} tracks from playlist: ${playlistId}`);
return shuffle(tracks);
}