/
josh
/
mp3-online
Обзор
Документация
Войти
/
josh
/
mp3-online
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
server.js
444 строки
16 KB
epo
First
09 фев 2026, 01:55
09 фев 2026, 01:55
a1e34bd
Код
Авторство
О чём код?
const express = require('express'); const fs = require('fs').promises; const path = require('path'); const cors = require('cors'); const app = express(); const PORT = process.env.PORT || 3000; const MUSIC_FOLDER = process.env.MUSIC_FOLDER || '/music'; // Middleware app.use(cors()); app.use(express.static(path.join(__dirname, 'public'))); // Кэш для плейлистов let playlistsCache = null; let lastCacheUpdate = 0; const CACHE_TTL = 30000; // 30 секунд // Функция для сканирования папки с музыкой (рекурсивно) async function scanMusicFolder(folderPath, basePath = '') { const items = await fs.readdir(folderPath, { withFileTypes: true }); const result = { files: [], folders: [] }; for (const item of items) { const fullPath = path.join(folderPath, item.name); const relativePath = basePath ? path.join(basePath, item.name) : item.name; if (item.isDirectory()) { // Рекурсивно сканируем подпапку const subfolder = await scanMusicFolder(fullPath, relativePath); result.folders.push({ name: item.name, path: relativePath, trackCount: subfolder.files.length, files: subfolder.files }); } else if (item.isFile() && item.name.toLowerCase().endsWith('.mp3')) { // Это MP3 файл const stats = await fs.stat(fullPath); const fileNameWithoutExt = item.name.replace(/\.mp3$/i, ''); let artist = 'Неизвестный исполнитель'; let title = fileNameWithoutExt; let album = basePath || 'Без альбома'; // Пытаемся извлечить метаданные из имени файла const parts = fileNameWithoutExt.split(' - '); if (parts.length >= 2) { artist = parts[0]; title = parts.slice(1).join(' - '); } result.files.push({ name: item.name, title: title, artist: artist, album: album, duration: '0:00', // В реальном приложении используйте библиотеку для MP3 size: stats.size, path: `/music/${encodeURIComponent(relativePath)}`, folder: basePath || '/', modified: stats.mtime, fullPath: fullPath }); } } // Сортируем файлы по имени result.files.sort((a, b) => a.title.localeCompare(b.title)); // Сортируем папки по имени result.folders.sort((a, b) => a.name.localeCompare(b.name)); return result; } // Функция для получения всех плейлистов async function getAllPlaylists() { const now = Date.now(); // Используем кэш, если он еще актуален if (playlistsCache && (now - lastCacheUpdate) < CACHE_TTL) { return playlistsCache; } try { console.log(`Сканирую папку: ${MUSIC_FOLDER}`); await fs.access(MUSIC_FOLDER); const scanResult = await scanMusicFolder(MUSIC_FOLDER); // Создаем структуру плейлистов const playlists = []; // Главный плейлист (все треки) playlists.push({ id: 'all', name: 'Все треки', type: 'main', trackCount: scanResult.files.length, path: '/', icon: '🎵' }); // Плейлисты по папкам scanResult.folders.forEach((folder, index) => { playlists.push({ id: `folder_${index}`, name: folder.name, type: 'folder', trackCount: folder.trackCount, path: folder.path, icon: '📁' }); }); // Плейлисты по артистам (группировка) const artists = {}; scanResult.files.forEach(track => { if (!artists[track.artist]) { artists[track.artist] = []; } artists[track.artist].push(track); }); Object.entries(artists).forEach(([artistName, tracks], index) => { if (tracks.length >= 2) { // Только если у артиста 2+ трека playlists.push({ id: `artist_${index}`, name: artistName, type: 'artist', trackCount: tracks.length, path: `artist:${artistName}`, icon: '🎤' }); } }); // Плейлисты по альбомам const albums = {}; scanResult.files.forEach(track => { if (!albums[track.album]) { albums[track.album] = []; } albums[track.album].push(track); }); Object.entries(albums).forEach(([albumName, tracks], index) => { if (tracks.length >= 2 && albumName !== 'Без альбома') { playlists.push({ id: `album_${index}`, name: albumName, type: 'album', trackCount: tracks.length, path: `album:${albumName}`, icon: '💿' }); } }); // Сохраняем в кэш playlistsCache = { playlists: playlists, allTracks: scanResult.files, folders: scanResult.folders, artists: artists, albums: albums, scanTime: now }; lastCacheUpdate = now; console.log(`Найдено: ${scanResult.files.length} треков, ${scanResult.folders.length} папок`); return playlistsCache; } catch (error) { console.error('Ошибка сканирования папки:', error); return { playlists: [], allTracks: [], folders: [], artists: {}, albums: {}, scanTime: Date.now() }; } } // API: Получить все плейлисты app.get('/api/playlists', async (req, res) => { try { const data = await getAllPlaylists(); res.json({ playlists: data.playlists, stats: { totalTracks: data.allTracks.length, totalFolders: data.folders.length, totalArtists: Object.keys(data.artists).length, totalAlbums: Object.keys(data.albums).length, lastScan: data.scanTime } }); } catch (error) { console.error('Ошибка получения плейлистов:', error); res.status(500).json({ error: 'Ошибка сервера' }); } }); // API: Получить треки для конкретного плейлиста app.get('/api/playlist/:id', async (req, res) => { try { const playlistId = req.params.id; const data = await getAllPlaylists(); let tracks = []; if (playlistId === 'all') { // Все треки tracks = data.allTracks; } else if (playlistId.startsWith('folder_')) { // Треки из папки const index = parseInt(playlistId.split('_')[1]); if (data.folders[index]) { tracks = data.folders[index].files; } } else if (playlistId.startsWith('artist_')) { // Треки артиста const index = parseInt(playlistId.split('_')[1]); const artistName = Object.keys(data.artists)[index]; if (artistName && data.artists[artistName]) { tracks = data.artists[artistName]; } } else if (playlistId.startsWith('album_')) { // Треки альбома const index = parseInt(playlistId.split('_')[1]); const albumName = Object.keys(data.albums)[index]; if (albumName && data.albums[albumName]) { tracks = data.albums[albumName]; } } // Добавляем ID к каждому треку tracks = tracks.map((track, index) => ({ ...track, id: index + 1 })); res.json({ playlistId: playlistId, tracks: tracks, count: tracks.length }); } catch (error) { console.error('Ошибка получения плейлиста:', error); res.status(500).json({ error: 'Ошибка сервера' }); } }); // API: Поиск треков app.get('/api/search', async (req, res) => { try { const query = (req.query.q || '').toLowerCase(); const data = await getAllPlaylists(); if (!query) { return res.json({ tracks: [], count: 0 }); } const results = data.allTracks.filter(track => track.title.toLowerCase().includes(query) || track.artist.toLowerCase().includes(query) || track.album.toLowerCase().includes(query) ); res.json({ query: query, tracks: results, count: results.length }); } catch (error) { console.error('Ошибка поиска:', error); res.status(500).json({ error: 'Ошибка сервера' }); } }); // API: Стриминг аудио (поддержка подпапок) app.get('/music/*', async (req, res) => { try { const filePath = decodeURIComponent(req.params[0]); const fullPath = path.join(MUSIC_FOLDER, filePath); console.log(`Запрос файла: ${filePath}`); // Проверяем безопасность пути const safePath = path.normalize(fullPath); if (!safePath.startsWith(path.normalize(MUSIC_FOLDER))) { return res.status(403).send('Доступ запрещен'); } // Проверяем существование файла await fs.access(fullPath); const stats = await fs.stat(fullPath); const fileSize = stats.size; const range = req.headers.range; if (range) { // Поддержка range-запросов const parts = range.replace(/bytes=/, "").split("-"); const start = parseInt(parts[0], 10); const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1; if (start >= fileSize) { return res.status(416).send('Запрашиваемый диапазон не достижим'); } const chunksize = (end - start) + 1; const head = { 'Content-Range': `bytes ${start}-${end}/${fileSize}`, 'Accept-Ranges': 'bytes', 'Content-Length': chunksize, 'Content-Type': 'audio/mpeg', }; res.writeHead(206, head); const readStream = require('fs').createReadStream(fullPath, { start, end }); readStream.pipe(res); readStream.on('error', (err) => { console.error('Ошибка чтения файла:', err); if (!res.headersSent) { res.status(500).send('Ошибка чтения файла'); } }); } else { // Отправляем весь файл const head = { 'Content-Length': fileSize, 'Content-Type': 'audio/mpeg', 'Accept-Ranges': 'bytes' }; res.writeHead(200, head); const readStream = require('fs').createReadStream(fullPath); readStream.pipe(res); readStream.on('error', (err) => { console.error('Ошибка чтения файла:', err); if (!res.headersSent) { res.status(500).send('Ошибка чтения файла'); } }); } } catch (err) { console.error(`Ошибка доступа к файлу:`, err); if (err.code === 'ENOENT') { res.status(404).send('Файл не найден'); } else { res.status(500).send('Внутренняя ошибка сервера'); } } }); // API: Информация о сервере app.get('/api/info', (req, res) => { res.json({ app: 'MP3 Docker Player с плейлистами', version: '2.0.0', musicFolder: MUSIC_FOLDER, port: PORT, features: ['подпапки-как-плейлисты', 'поиск', 'группировка-по-артистам', 'группировка-по-альбомам'] }); }); // API: Принудительное обновление кэша app.post('/api/refresh', async (req, res) => { playlistsCache = null; lastCacheUpdate = 0; await getAllPlaylists(); res.json({ status: 'Кэш обновлен' }); }); // API: Статистика app.get('/api/stats', async (req, res) => { try { const data = await getAllPlaylists(); res.json({ totalTracks: data.allTracks.length, totalFolders: data.folders.length, totalArtists: Object.keys(data.artists).length, totalAlbums: Object.keys(data.albums).length, lastScan: new Date(data.scanTime).toISOString(), cacheAge: Date.now() - data.scanTime }); } catch (error) { res.status(500).json({ error: 'Ошибка получения статистики' }); } }); // Проверка здоровья app.get('/health', (req, res) => { res.status(200).json({ status: 'OK', timestamp: new Date().toISOString(), cacheAge: Date.now() - lastCacheUpdate }); }); // Главная страница app.get('/', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html')); }); // Обработка 404 app.use((req, res) => { res.status(404).send('Страница не найдена'); }); // Обработка ошибок app.use((err, req, res, next) => { console.error('Ошибка сервера:', err.stack); res.status(500).send('Внутренняя ошибка сервера'); }); // Запуск сервера app.listen(PORT, () => { console.log(`========================================`); console.log(`🎵 MP3 Docker Player с плейлистами запущен!`); console.log(`📍 Порт: ${PORT}`); console.log(`📁 Папка с музыкой: ${MUSIC_FOLDER}`); console.log(`🌐 Доступно по адресу: http://localhost:${PORT}`); console.log(`========================================`); // Инициализируем кэш при запуске getAllPlaylists().then(data => { console.log(`✅ Инициализировано: ${data.allTracks.length} треков в ${data.folders.length} папках`); }).catch(err => { console.error('❌ Ошибка инициализации:', err); }); });