Files
vue-pult/server/music-player.js
sasha 3e90269b0b Initial commit: Vue.js тир управления система
- Клиентская часть Vue 3 + Vite
- Серверная часть Node.js + WebSocket
- Система авторизации и смен
- Управление игровыми портами
- Поддержка тем (светлая/темная)
- Адаптивный дизайн

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-19 12:24:22 +03:00

521 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Музыкальный плеер для фоновой музыки с поддержкой плейлистов и радио
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;