- Клиентская часть Vue 3 + Vite - Серверная часть Node.js + WebSocket - Система авторизации и смен - Управление игровыми портами - Поддержка тем (светлая/темная) - Адаптивный дизайн 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
521 lines
22 KiB
JavaScript
521 lines
22 KiB
JavaScript
// Музыкальный плеер для фоновой музыки с поддержкой плейлистов и радио
|
||
const Sound = require('./node-aplay');
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const { spawn } = require('child_process');
|
||
const EnhancedRadioPlayer = require('./enhanced-radio-player');
|
||
|
||
class MusicPlayer {
|
||
constructor() {
|
||
this.currentSound = null;
|
||
this.currentTrack = null;
|
||
this.currentTrackIndex = -1;
|
||
this.isPlaying = false;
|
||
this.isPaused = false;
|
||
this.isTransitioning = false; // Защита от множественных вызовов
|
||
this.volume = 70; // Громкость от 0 до 100
|
||
this.duckedVolume = 7; // Приглушенная громкость во время баркера (7%)
|
||
this.isDucked = false;
|
||
this.repeatMode = false; // false - плейлист зациклен, true - повтор одного трека
|
||
this.playlist = [];
|
||
this.onTrackEnd = null;
|
||
this.volumeProcess = null;
|
||
this.mode = 'local'; // 'local' или 'radio'
|
||
this.radioUrl = null;
|
||
this.radioProcess = null;
|
||
this.radioPlayer = new EnhancedRadioPlayer();
|
||
|
||
console.log('[MUSIC] 🎵 Музыкальный плеер инициализирован');
|
||
console.log('[MUSIC] 📁 Папка с музыкой:', path.join(__dirname, 'audio/music'));
|
||
|
||
// Загружаем плейлист при инициализации
|
||
this._loadPlaylist();
|
||
}
|
||
|
||
// Загрузка плейлиста из папки
|
||
_loadPlaylist() {
|
||
try {
|
||
const musicDir = path.join(__dirname, 'audio/music');
|
||
if (!fs.existsSync(musicDir)) {
|
||
fs.mkdirSync(musicDir, { recursive: true });
|
||
}
|
||
|
||
const files = fs.readdirSync(musicDir);
|
||
const musicFiles = files.filter(file => {
|
||
const ext = path.extname(file).toLowerCase();
|
||
return ['.mp3', '.wav', '.ogg'].includes(ext);
|
||
});
|
||
|
||
this.playlist = musicFiles.map(file => ({
|
||
id: path.parse(file).name,
|
||
name: path.parse(file).name,
|
||
path: path.join(musicDir, file),
|
||
extension: path.extname(file)
|
||
}));
|
||
|
||
console.log('[MUSIC] 📁 Загружено треков в плейлист:', this.playlist.length);
|
||
if (this.playlist.length > 0) {
|
||
console.log('[MUSIC] 🎵 Плейлист:', this.playlist.map(t => t.name).join(', '));
|
||
} else {
|
||
console.log('[MUSIC] ⚠️ Музыкальные файлы не найдены в папке:', musicDir);
|
||
}
|
||
} catch (err) {
|
||
console.error('[MUSIC] ❌ Ошибка при загрузке плейлиста:', err.message);
|
||
this.playlist = [];
|
||
}
|
||
}
|
||
|
||
// Получение списка музыкальных файлов
|
||
getAvailableTracks() {
|
||
// Перезагружаем плейлист для актуальности
|
||
this._loadPlaylist();
|
||
console.log('[MUSIC] 📋 Готовый список треков:', this.playlist);
|
||
return this.playlist;
|
||
}
|
||
|
||
// Остановка текущего воспроизведения
|
||
stop() {
|
||
console.log('[MUSIC] ⏹️ Остановка воспроизведения');
|
||
|
||
// Сбрасываем флаг перехода
|
||
this.isTransitioning = false;
|
||
|
||
if (this.mode === 'radio') {
|
||
console.log('[MUSIC] ⏹️ Останавливаем радио');
|
||
this.radioPlayer.stop();
|
||
if (this.radioProcess) {
|
||
this.radioProcess.kill();
|
||
this.radioProcess = null;
|
||
}
|
||
} else if (this.currentSound) {
|
||
console.log('[MUSIC] ⏹️ Останавливаем текущий трек');
|
||
try {
|
||
this.currentSound.stop();
|
||
} catch (err) {
|
||
console.log('[MUSIC] ⚠️ Ошибка при остановке:', err.message);
|
||
}
|
||
this.currentSound = null;
|
||
}
|
||
|
||
// Останавливаем процесс регулировки громкости
|
||
if (this.volumeProcess) {
|
||
this.volumeProcess.kill();
|
||
this.volumeProcess = null;
|
||
}
|
||
|
||
this.isPlaying = false;
|
||
this.isPaused = false;
|
||
this.currentTrack = null;
|
||
this.currentTrackIndex = -1;
|
||
this.isDucked = false;
|
||
this.onTrackEnd = null; // Очищаем коллбэки
|
||
|
||
return this;
|
||
}
|
||
|
||
// Воспроизведение трека по ID или следующего в плейлисте
|
||
play(trackId = null, onStart = null, onEnd = null) {
|
||
console.log('[MUSIC] 🎵 Запуск воспроизведения:', trackId || 'следующий трек');
|
||
|
||
// Защита от множественных вызовов
|
||
if (this.isTransitioning) {
|
||
console.log('[MUSIC] ⚠️ Уже идет переключение трека, игнорируем');
|
||
return this;
|
||
}
|
||
|
||
// Останавливаем предыдущее воспроизведение
|
||
this.stop();
|
||
|
||
// Если режим радио
|
||
if (this.mode === 'radio' && this.radioUrl) {
|
||
return this._playRadio();
|
||
}
|
||
|
||
// Локальный режим
|
||
if (this.playlist.length === 0) {
|
||
console.log('[MUSIC] ❌ Плейлист пуст');
|
||
return this;
|
||
}
|
||
|
||
let track;
|
||
|
||
if (trackId) {
|
||
// Ищем трек по ID
|
||
const trackIndex = this.playlist.findIndex(t => t.id === trackId);
|
||
if (trackIndex === -1) {
|
||
console.error('[MUSIC] ❌ Трек не найден:', trackId);
|
||
return this;
|
||
}
|
||
this.currentTrackIndex = trackIndex;
|
||
track = this.playlist[trackIndex];
|
||
} else {
|
||
// Берем следующий трек в плейлисте
|
||
this.currentTrackIndex = (this.currentTrackIndex + 1) % this.playlist.length;
|
||
track = this.playlist[this.currentTrackIndex];
|
||
}
|
||
|
||
// Проверяем существование файла
|
||
if (!fs.existsSync(track.path)) {
|
||
console.error('[MUSIC] ❌ Файл не найден:', track.path);
|
||
// Пробуем следующий трек
|
||
this.currentTrackIndex--; // Компенсируем увеличение
|
||
return this.play(null, onStart, onEnd);
|
||
}
|
||
|
||
this.currentTrack = track.path;
|
||
this.onTrackEnd = onEnd;
|
||
this.isTransitioning = true;
|
||
|
||
// Создаем новый экземпляр Sound
|
||
this.currentSound = new Sound(track.path);
|
||
this.isPlaying = true;
|
||
this.isPaused = false;
|
||
|
||
// Запускаем воспроизведение
|
||
this.currentSound.play(
|
||
// onStart
|
||
() => {
|
||
console.log('[MUSIC] ▶️ Воспроизведение началось:', track.name);
|
||
this.isTransitioning = false;
|
||
if (onStart) onStart();
|
||
|
||
// Применяем громкость после небольшой задержки
|
||
setTimeout(() => {
|
||
if (this.isPlaying && this.currentSound) {
|
||
this._applyVolume();
|
||
}
|
||
}, 500);
|
||
},
|
||
// onEnd
|
||
() => {
|
||
console.log('[MUSIC] ⏹️ Воспроизведение завершено:', track.name);
|
||
|
||
// Сохраняем текущее состояние
|
||
const wasPlaying = this.isPlaying;
|
||
const shouldContinue = wasPlaying && !this.isPaused;
|
||
|
||
this.isPlaying = false;
|
||
this.currentSound = null;
|
||
this.isTransitioning = false;
|
||
|
||
// Вызываем callback если он есть
|
||
if (onEnd) {
|
||
onEnd();
|
||
}
|
||
|
||
// Автоматический переход только если музыка не была остановлена
|
||
if (shouldContinue) {
|
||
// Проверяем режим повтора
|
||
if (this.repeatMode) {
|
||
// Повтор одного трека
|
||
console.log('[MUSIC] 🔁 Повторное воспроизведение трека');
|
||
setTimeout(() => {
|
||
this.play(track.id);
|
||
}, 500);
|
||
} else {
|
||
// Переход к следующему треку
|
||
console.log('[MUSIC] ⏭️ Переход к следующему треку');
|
||
setTimeout(() => {
|
||
this.play(null);
|
||
}, 500);
|
||
}
|
||
}
|
||
}
|
||
);
|
||
|
||
return this;
|
||
}
|
||
|
||
// Воспроизведение радио
|
||
_playRadio() {
|
||
if (!this.radioUrl) {
|
||
console.error('[MUSIC] ❌ URL радио не установлен');
|
||
return this;
|
||
}
|
||
|
||
// Добавляем протокол если его нет
|
||
let url = this.radioUrl;
|
||
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
||
url = 'http://' + url;
|
||
this.radioUrl = url;
|
||
}
|
||
|
||
console.log('[MUSIC] 📻 Запуск радио:', url);
|
||
|
||
try {
|
||
// Используем новый радио плеер
|
||
this.radioPlayer.setVolume(this.volume);
|
||
this.radioPlayer.play(url);
|
||
|
||
this.isPlaying = true;
|
||
this.isPaused = false;
|
||
this.currentTrack = 'Радио';
|
||
|
||
// Проверяем статус через 3 секунды
|
||
setTimeout(() => {
|
||
if (!this.radioPlayer.isPlaying) {
|
||
console.log('[MUSIC] ❌ Радио не запустилось');
|
||
console.log('[MUSIC] 💡 Проверьте URL радио. Рабочие примеры:');
|
||
console.log('[MUSIC] 💡 nashe1.hostingradio.ru/nashe-128.mp3');
|
||
console.log('[MUSIC] 💡 dfm.hostingradio.ru/dfm96.aacp');
|
||
console.log('[MUSIC] 💡 europaplus.hostingradio.ru:8014/europaplus320.mp3');
|
||
this.isPlaying = false;
|
||
}
|
||
}, 3000);
|
||
|
||
} catch (err) {
|
||
console.error('[MUSIC] ❌ Ошибка запуска радио:', err);
|
||
this.isPlaying = false;
|
||
}
|
||
|
||
return this;
|
||
}
|
||
|
||
// Пауза
|
||
pause() {
|
||
if (this.mode === 'radio') {
|
||
console.log('[MUSIC] ⚠️ Пауза не поддерживается для радио');
|
||
return this;
|
||
}
|
||
|
||
if (this.isPlaying && !this.isPaused && this.currentSound) {
|
||
console.log('[MUSIC] ⏸️ Пауза');
|
||
// Сохраняем текущий индекс трека для возобновления
|
||
const savedIndex = this.currentTrackIndex;
|
||
const savedTrack = this.currentTrack;
|
||
|
||
// Флаг паузы должен быть установлен ДО остановки
|
||
this.isPaused = true;
|
||
|
||
try {
|
||
this.currentSound.stop();
|
||
} catch (err) {
|
||
console.log('[MUSIC] ⚠️ Ошибка при паузе:', err.message);
|
||
}
|
||
|
||
this.isPlaying = false;
|
||
|
||
// Восстанавливаем индекс и трек после остановки
|
||
this.currentTrackIndex = savedIndex;
|
||
this.currentTrack = savedTrack;
|
||
}
|
||
return this;
|
||
}
|
||
|
||
// Возобновление воспроизведения
|
||
resume() {
|
||
if (this.mode === 'radio') {
|
||
console.log('[MUSIC] ⚠️ Возобновление не поддерживается для радио, используйте play');
|
||
return this._playRadio();
|
||
}
|
||
|
||
if (this.isPaused && this.currentTrackIndex >= 0) {
|
||
console.log('[MUSIC] ▶️ Возобновление воспроизведения');
|
||
this.isPaused = false;
|
||
const track = this.playlist[this.currentTrackIndex];
|
||
if (track) {
|
||
// Воспроизводим тот же трек заново
|
||
// К сожалению, node-aplay не поддерживает возобновление с позиции
|
||
this.play(track.id, null, this.onTrackEnd);
|
||
}
|
||
}
|
||
return this;
|
||
}
|
||
|
||
// Переход к следующему треку
|
||
nextTrack() {
|
||
if (this.mode === 'radio') {
|
||
console.log('[MUSIC] ⚠️ Переключение треков не поддерживается для радио');
|
||
return this;
|
||
}
|
||
|
||
console.log('[MUSIC] ⏭️ Переключение на следующий трек');
|
||
// Сбрасываем режим паузы и очищаем коллбэки
|
||
this.isPaused = false;
|
||
this.onTrackEnd = null;
|
||
this.play(null);
|
||
return this;
|
||
}
|
||
|
||
// Переход к предыдущему треку
|
||
previousTrack() {
|
||
if (this.mode === 'radio') {
|
||
console.log('[MUSIC] ⚠️ Переключение треков не поддерживается для радио');
|
||
return this;
|
||
}
|
||
|
||
console.log('[MUSIC] ⏮️ Переключение на предыдущий трек');
|
||
// Сбрасываем режим паузы и очищаем коллбэки
|
||
this.isPaused = false;
|
||
this.onTrackEnd = null;
|
||
this.currentTrackIndex = this.currentTrackIndex - 2;
|
||
if (this.currentTrackIndex < -1) {
|
||
this.currentTrackIndex = this.playlist.length - 2;
|
||
}
|
||
this.play(null);
|
||
return this;
|
||
}
|
||
|
||
// Применение громкости через системные средства
|
||
_applyVolume() {
|
||
const platform = require('os').platform();
|
||
const currentVolume = this.isDucked ? this.duckedVolume : this.volume;
|
||
|
||
console.log('[MUSIC] 🔊 Применяем громкость:', currentVolume + '%');
|
||
|
||
if (this.mode === 'radio' && this.isPlaying) {
|
||
// Для радио используем новый плеер
|
||
this.radioPlayer.setVolume(currentVolume);
|
||
return;
|
||
}
|
||
|
||
if (platform === 'win32') {
|
||
// Метод 1: Попробуем использовать nircmd если доступен
|
||
const nircmdPath = path.join(__dirname, 'tools', 'nircmd.exe');
|
||
if (fs.existsSync(nircmdPath) && this.currentSound && this.currentSound.process) {
|
||
// Устанавливаем громкость для конкретного процесса
|
||
spawn(nircmdPath, ['setappvolume', this.currentSound.process.pid.toString(), currentVolume / 100]);
|
||
console.log('[MUSIC] ✅ Громкость установлена через nircmd');
|
||
} else {
|
||
// Метод 2: Простая установка громкости через nircmd (если он есть в системе)
|
||
spawn('nircmd', ['setsysvolume', Math.floor(655.35 * currentVolume).toString()], { stdio: 'ignore' })
|
||
.on('error', () => {
|
||
// Метод 3: Используем системную громкость через PowerShell
|
||
console.log('[MUSIC] 🔄 Пробуем установить громкость через PowerShell...');
|
||
const volumeScript = `
|
||
Add-Type -TypeDefinition @'
|
||
using System.Runtime.InteropServices;
|
||
[Guid("5CDF2C82-841E-4546-9722-0CF74078229A"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||
interface IAudioEndpointVolume {
|
||
int f(); int g(); int h(); int i();
|
||
int SetMasterVolumeLevelScalar(float fLevel, System.Guid pguidEventContext);
|
||
int j();
|
||
int GetMasterVolumeLevelScalar(out float pfLevel);
|
||
int k(); int l(); int m(); int n();
|
||
int SetMute([MarshalAs(UnmanagedType.Bool)] bool bMute, System.Guid pguidEventContext);
|
||
int GetMute(out bool pbMute);
|
||
}
|
||
[Guid("D666063F-1587-4E43-81F1-B948E807363F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||
interface IMMDevice {
|
||
int Activate(ref System.Guid id, int clsCtx, int activationParams, out IAudioEndpointVolume aev);
|
||
}
|
||
[Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||
interface IMMDeviceEnumerator {
|
||
int f();
|
||
int GetDefaultAudioEndpoint(int dataFlow, int role, out IMMDevice endpoint);
|
||
}
|
||
[ComImport, Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")] class MMDeviceEnumeratorComObject { }
|
||
public class Audio {
|
||
static IAudioEndpointVolume Vol() {
|
||
var enumerator = new MMDeviceEnumeratorComObject() as IMMDeviceEnumerator;
|
||
IMMDevice dev = null;
|
||
Marshal.ThrowExceptionForHR(enumerator.GetDefaultAudioEndpoint(0, 1, out dev));
|
||
IAudioEndpointVolume epv = null;
|
||
var epvid = typeof(IAudioEndpointVolume).GUID;
|
||
Marshal.ThrowExceptionForHR(dev.Activate(ref epvid, 23, 0, out epv));
|
||
return epv;
|
||
}
|
||
public static void SetVolume(float level) {
|
||
Marshal.ThrowExceptionForHR(Vol().SetMasterVolumeLevelScalar(level, System.Guid.Empty));
|
||
}
|
||
}
|
||
'@
|
||
[Audio]::SetVolume(${currentVolume / 100})
|
||
`;
|
||
|
||
spawn('powershell', ['-Command', volumeScript], { stdio: 'ignore' });
|
||
console.log('[MUSIC] 🔊 Громкость установлена через системный микшер');
|
||
});
|
||
|
||
if (!fs.existsSync(nircmdPath)) {
|
||
console.log('[MUSIC] 💡 Для управления громкости только музыки скачайте nircmd.exe');
|
||
console.log('[MUSIC] 💡 с https://www.nirsoft.net/utils/nircmd.html');
|
||
console.log('[MUSIC] 💡 и поместите в папку old_server/tools/');
|
||
}
|
||
}
|
||
} else if (platform === 'linux') {
|
||
// Для Linux можно использовать amixer
|
||
spawn('amixer', ['set', 'Master', currentVolume + '%']);
|
||
}
|
||
}
|
||
|
||
// Установка громкости
|
||
setVolume(volume) {
|
||
this.volume = Math.max(0, Math.min(100, volume));
|
||
console.log('[MUSIC] 🔊 Установлена громкость:', this.volume);
|
||
|
||
// Применяем громкость если музыка играет
|
||
if (this.isPlaying) {
|
||
this._applyVolume();
|
||
}
|
||
|
||
return this;
|
||
}
|
||
|
||
// Приглушение звука (во время баркера)
|
||
duck() {
|
||
if (!this.isDucked && this.isPlaying) {
|
||
console.log('[MUSIC] 🔇 Приглушаем музыку для баркера');
|
||
this.isDucked = true;
|
||
this._applyVolume();
|
||
}
|
||
return this;
|
||
}
|
||
|
||
// Восстановление звука (после баркера)
|
||
unduck() {
|
||
if (this.isDucked && this.isPlaying) {
|
||
console.log('[MUSIC] 🔊 Восстанавливаем громкость музыки');
|
||
this.isDucked = false;
|
||
this._applyVolume();
|
||
}
|
||
return this;
|
||
}
|
||
|
||
// Включение/выключение режима повтора
|
||
setRepeatMode(enabled) {
|
||
this.repeatMode = enabled;
|
||
console.log('[MUSIC] 🔁 Режим повтора:', enabled ? 'одного трека' : 'плейлиста');
|
||
return this;
|
||
}
|
||
|
||
// Установка режима воспроизведения
|
||
setMode(mode, radioUrl = null) {
|
||
if (mode !== 'local' && mode !== 'radio') {
|
||
console.error('[MUSIC] ❌ Неверный режим:', mode);
|
||
return this;
|
||
}
|
||
|
||
console.log('[MUSIC] 🎵 Переключение режима:', mode);
|
||
|
||
// Останавливаем текущее воспроизведение
|
||
this.stop();
|
||
|
||
this.mode = mode;
|
||
if (mode === 'radio') {
|
||
this.radioUrl = radioUrl;
|
||
console.log('[MUSIC] 📻 URL радио установлен:', radioUrl);
|
||
}
|
||
|
||
return this;
|
||
}
|
||
|
||
// Получение текущего статуса
|
||
getStatus() {
|
||
return {
|
||
isPlaying: this.isPlaying,
|
||
isPaused: this.isPaused,
|
||
currentTrack: this.currentTrack ? path.parse(this.currentTrack).name : null,
|
||
currentTrackIndex: this.currentTrackIndex,
|
||
totalTracks: this.playlist.length,
|
||
volume: this.volume,
|
||
isDucked: this.isDucked,
|
||
repeatMode: this.repeatMode,
|
||
mode: this.mode,
|
||
radioUrl: this.radioUrl,
|
||
playlist: this.playlist.map(t => t.name)
|
||
};
|
||
}
|
||
}
|
||
|
||
module.exports = MusicPlayer; |