const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); // Cache for validated files to avoid re-validation on every request let validationCache = null; let cacheForPath = null; // Global shared playlist cache let sharedPlaylistFile = null; let sharedPlaylistTrackCount = 0; let sharedPlaylistPath = null; /** * Validate an audio file using ffprobe * @param {string} filePath - Path to audio file * @returns {boolean} True if file is valid and playable */ function validateAudioFile(filePath) { try { // Use ffprobe to check if file is valid and get audio info const output = execSync( `ffprobe -v error -select_streams a:0 -show_entries stream=codec_name,sample_rate,channels -of json "${filePath}"`, { encoding: 'utf8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] } ); const data = JSON.parse(output); // Check if audio stream exists and has valid properties if (!data.streams || data.streams.length === 0) { return false; } const stream = data.streams[0]; // Validate codec if (!stream.codec_name) { return false; } // Validate sample rate (should be reasonable) const sampleRate = parseInt(stream.sample_rate); if (!sampleRate || sampleRate < 8000 || sampleRate > 192000) { return false; } // Validate channels const channels = parseInt(stream.channels); if (!channels || channels < 1 || channels > 8) { return false; } return true; } catch (error) { return false; } } /** * Get a random music file from the music directory * @param {string} musicPath - Path to music directory * @returns {string|null} Path to random music file or null */ function getRandomMusicFile(musicPath) { try { if (!fs.existsSync(musicPath)) { return null; } const files = fs.readdirSync(musicPath).filter(f => f.endsWith('.ogg')); if (files.length === 0) { return null; } const randomFile = files[Math.floor(Math.random() * files.length)]; return path.join(musicPath, randomFile); } catch (error) { console.error('Error getting music file:', error); return null; } } /** * Get all valid music files for playlist generation (with caching) * @param {string} musicPath - Path to music directory * @returns {string[]} Array of validated music file paths */ function getAllMusicFiles(musicPath) { try { if (!fs.existsSync(musicPath)) { return []; } // Return cached results if available for this path if (validationCache && cacheForPath === musicPath) { return validationCache; } const files = fs.readdirSync(musicPath).filter(f => f.endsWith('.ogg')); const filePaths = files.map(f => path.join(musicPath, f)); console.log(`Validating ${filePaths.length} audio files...`); // Validate each file const validFiles = filePaths.filter(validateAudioFile); if (validFiles.length < filePaths.length) { console.log(`Warning: ${filePaths.length - validFiles.length} files skipped due to validation errors`); } // Cache the results validationCache = validFiles; cacheForPath = musicPath; return validFiles; } catch (error) { console.error('Error getting music files:', error); return []; } } /** * Shuffle array in place using Fisher-Yates algorithm * @param {Array} array - Array to shuffle * @returns {Array} Shuffled array (same reference) */ function shuffleArray(array) { for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } return array; } /** * Initialize shared playlist at startup * @param {string} musicPath - Path to music directory */ function initializeSharedPlaylist(musicPath) { const allMusicFiles = getAllMusicFiles(musicPath); if (allMusicFiles.length === 0) { return; } // Create a persistent shared playlist file const playlistFile = path.join('/tmp', `shared-playlist-${Date.now()}.txt`); // Build playlist content - repeat enough times for ~24 hours of playback const repetitions = Math.max(20, Math.ceil(480 / allMusicFiles.length)); const playlistLines = []; playlistLines.push('ffconcat version 1.0'); for (let i = 0; i < repetitions; i++) { const shuffled = shuffleArray([...allMusicFiles]); shuffled.forEach(f => { const escapedPath = f.replace(/'/g, "'\\''"); playlistLines.push(`file '${escapedPath}'`); playlistLines.push('# Auto-generated entry'); }); } fs.writeFileSync(playlistFile, playlistLines.join('\n')); const totalTracks = allMusicFiles.length * repetitions; // Cache the shared playlist sharedPlaylistFile = playlistFile; sharedPlaylistTrackCount = totalTracks; sharedPlaylistPath = musicPath; console.log(`Shared playlist created: ${totalTracks} tracks`); } /** * Create a rotated playlist starting at a random position * @param {string} musicPath - Path to music directory * @returns {{playlistFile: string, trackCount: number}|null} Playlist info or null */ function createPlaylist(musicPath) { // Check if we have a shared playlist if (!sharedPlaylistFile || !fs.existsSync(sharedPlaylistFile)) { console.warn('Warning: Shared playlist not found, initializing...'); initializeSharedPlaylist(musicPath); if (!sharedPlaylistFile) { return null; } } const allMusicFiles = getAllMusicFiles(musicPath); if (allMusicFiles.length === 0) { return null; } // Create a rotated copy of the playlist with random start const playlistFile = path.join('/tmp', `playlist-${Date.now()}-${Math.random().toString(36).substr(2, 9)}.txt`); const randomStart = Math.floor(Math.random() * allMusicFiles.length); const repetitions = Math.max(20, Math.ceil(480 / allMusicFiles.length)); const playlistLines = []; playlistLines.push('ffconcat version 1.0'); // First repetition with rotation const firstShuffle = shuffleArray([...allMusicFiles]); for (let i = 0; i < firstShuffle.length; i++) { const idx = (randomStart + i) % firstShuffle.length; const escapedPath = firstShuffle[idx].replace(/'/g, "'\\''"); playlistLines.push(`file '${escapedPath}'`); playlistLines.push('# Auto-generated entry'); } // Remaining repetitions for (let i = 1; i < repetitions; i++) { const shuffled = shuffleArray([...allMusicFiles]); shuffled.forEach(f => { const escapedPath = f.replace(/'/g, "'\\''"); playlistLines.push(`file '${escapedPath}'`); playlistLines.push('# Auto-generated entry'); }); } fs.writeFileSync(playlistFile, playlistLines.join('\n')); console.log(`Stream playlist created, starting at position ${randomStart}/${allMusicFiles.length}`); return { playlistFile, trackCount: allMusicFiles.length * repetitions }; } module.exports = { getRandomMusicFile, getAllMusicFiles, shuffleArray, createPlaylist, initializeSharedPlaylist };