feat: added playlist option
All checks were successful
Build and Push Docker Image / docker (push) Successful in 10s
All checks were successful
Build and Push Docker Image / docker (push) Successful in 10s
This commit is contained in:
@@ -5,24 +5,118 @@ import { DATA_DIR } from '../config.js';
|
||||
import { loadYearsIndex } from '../years.js';
|
||||
import { shuffle } from './state.js';
|
||||
|
||||
export async function loadDeck() {
|
||||
const years = loadYearsIndex();
|
||||
const files = fs.readdirSync(DATA_DIR).filter((f) => /\.(mp3|wav|m4a|ogg)$/i.test(f));
|
||||
const tracks = await Promise.all(
|
||||
files.map(async (f) => {
|
||||
const fp = path.join(DATA_DIR, f);
|
||||
let year = null,
|
||||
title = path.parse(f).name,
|
||||
artist = '';
|
||||
try {
|
||||
const meta = await mmParseFile(fp, { duration: false });
|
||||
title = meta.common.title || title;
|
||||
artist = meta.common.artist || artist;
|
||||
year = meta.common.year || null;
|
||||
} catch {}
|
||||
const y = years[f]?.year ?? year;
|
||||
return { id: f, file: f, title, artist, year: y };
|
||||
})
|
||||
);
|
||||
/**
|
||||
* 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 complete metadata
|
||||
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 = jsonMeta?.year ?? meta.common.year ?? 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);
|
||||
|
||||
// Optional: Log progress for large playlists
|
||||
if (files.length > 100 && (i + BATCH_SIZE) % 100 === 0) {
|
||||
console.log(
|
||||
`Loading playlist ${playlistId}: ${Math.min(i + BATCH_SIZE, files.length)}/${files.length} tracks processed`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Loaded ${tracks.length} tracks from playlist: ${playlistId}`);
|
||||
return shuffle(tracks);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user