UNPKG

@notreallyr8/mp3

Version:

CLI music player with playlists, shuffle, multi-format support

154 lines (134 loc) 3.88 kB
import MP3 from '@notreallyr8/mp3'; import fs from 'fs'; import path from 'path'; import ffmpeg from 'fluent-ffmpeg'; import Speaker from 'speaker'; const AUDIO_EXTENSIONS = ['.mp3', '.flac', '.wav', '.ogg', '.m4a']; function shuffleArray(arr) { const array = arr.slice(); 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; } export class PlaylistPlayer { constructor(name, folder) { this.name = name; this.folder = folder; this.songs = []; this.shuffleOrder = []; this.currentIndex = 0; this.isShuffle = true; this.isPlaying = false; this.currentProcess = null; this.currentPlayer = null; } loadSongs() { if (!fs.existsSync(this.folder)) { console.error(`Folder "${this.folder}" does not exist!`); return false; } this.songs = fs.readdirSync(this.folder) .filter(f => AUDIO_EXTENSIONS.includes(path.extname(f).toLowerCase())); if (this.songs.length === 0) { console.error(`No audio files found in "${this.folder}"`); return false; } this.shuffleOrder = shuffleArray([...Array(this.songs.length).keys()]); this.currentIndex = 0; return true; } getCurrentSong() { const index = this.isShuffle ? this.shuffleOrder[this.currentIndex] : this.currentIndex; return this.songs[index]; } stop() { if (this.currentProcess) { this.currentProcess.kill(); this.currentProcess = null; } if (this.currentPlayer) { // MP3 player does not support stop, so just nullify this.currentPlayer = null; } this.isPlaying = false; } async play() { this.stop(); if (this.songs.length === 0) { console.log('No songs loaded.'); return; } const song = this.getCurrentSong(); const fullPath = path.join(this.folder, song); console.log(`Playing [${this.name}] - ${song}`); const ext = path.extname(song).toLowerCase(); if (ext === '.mp3') { this.currentPlayer = new MP3(fullPath); this.isPlaying = true; try { await this.currentPlayer.play(); this.isPlaying = false; await this.next(); } catch (err) { console.error('Playback error:', err); } } else { // Use ffmpeg + speaker for other formats this.isPlaying = true; return new Promise((resolve) => { this.currentProcess = ffmpeg(fullPath) .format('s16le') .audioChannels(2) .audioFrequency(44100) .on('error', err => { console.error('Playback error:', err); this.isPlaying = false; resolve(); }) .on('end', async () => { this.isPlaying = false; await this.next(); resolve(); }) .pipe(); const speaker = new Speaker({ channels: 2, bitDepth: 16, sampleRate: 44100, }); this.currentProcess.pipe(speaker); }); } } async next() { if (this.songs.length === 0) return; this.currentIndex++; if (this.currentIndex >= this.songs.length) { this.currentIndex = 0; if (this.isShuffle) { this.shuffleOrder = shuffleArray([...Array(this.songs.length).keys()]); } } await this.play(); } async previous() { if (this.songs.length === 0) return; this.currentIndex--; if (this.currentIndex < 0) { this.currentIndex = this.songs.length - 1; } await this.play(); } toggleShuffle() { this.isShuffle = !this.isShuffle; if (this.isShuffle) { this.shuffleOrder = shuffleArray([...Array(this.songs.length).keys()]); this.currentIndex = 0; } console.log(`Shuffle mode: ${this.isShuffle ? 'ON' : 'OFF'}`); } }