const express = require('express'); const router = express.Router(); const path = require('path'); const fs = require('fs').promises; const AdmZip = require('adm-zip'); const mm = require('music-metadata'); const { authenticate } = require('../middleware/auth'); const { uploadMusicTrack, uploadMusicAlbum, cleanupOnError } = require('../middleware/upload'); const { uploadFile } = require('../utils/minio'); const Artist = require('../models/Artist'); const Album = require('../models/Album'); const Track = require('../models/Track'); const FavoriteTrack = require('../models/FavoriteTrack'); // Извлечь метаданные из аудио файла (из buffer или пути) async function extractAudioMetadata(audioData, isBuffer = true) { try { const metadata = isBuffer ? await mm.parseBuffer(audioData, audioData.mimetype || 'audio/mpeg') : await mm.parseFile(audioData); // Извлечь обложку если есть let coverImageUrl = null; if (metadata.common.picture && metadata.common.picture.length > 0) { const picture = metadata.common.picture[0]; const coverExt = picture.format.split('/')[1] || 'jpg'; const coverFileName = `cover-${Date.now()}.${coverExt}`; // Загрузить обложку в MinIO если включен const { isEnabled: isMinioEnabled } = require('../utils/minio'); if (isMinioEnabled()) { try { coverImageUrl = await uploadFile( Buffer.from(picture.data), coverFileName, `image/${coverExt}`, 'music' ); } catch (uploadError) { console.error('Ошибка загрузки обложки в MinIO:', uploadError); // Fallback: сохранить локально если MinIO недоступен const coverPath = path.join(__dirname, '../uploads/music', coverFileName); await fs.writeFile(coverPath, picture.data); coverImageUrl = `/uploads/music/${coverFileName}`; } } else { // Локальное хранилище const coverPath = path.join(__dirname, '../uploads/music', coverFileName); await fs.writeFile(coverPath, picture.data); coverImageUrl = `/uploads/music/${coverFileName}`; } } return { title: metadata.common.title || null, artist: metadata.common.artist || metadata.common.artists?.[0] || null, album: metadata.common.album || null, year: metadata.common.year || null, genre: metadata.common.genre?.[0] || null, trackNumber: metadata.common.track?.no || null, duration: metadata.format.duration || 0, coverImage: coverImageUrl }; } catch (error) { console.error('Ошибка извлечения метаданных:', error); return { title: null, artist: null, album: null, year: null, genre: null, trackNumber: null, duration: 0, coverImage: null }; } } // Вспомогательная функция для поиска или создания исполнителя async function findOrCreateArtist(artistName, userId) { const normalizedName = artistName.toLowerCase().replace(/\s+/g, ''); let artist = await Artist.findOne({ normalizedName }); if (!artist) { artist = await Artist.create({ name: artistName, normalizedName, addedBy: userId }); } return artist; } // Загрузка одного трека router.post('/upload-track', authenticate, uploadMusicTrack, cleanupOnError(), async (req, res) => { try { if (!req.uploadedMusicFile) { return res.status(400).json({ error: 'Файл не загружен' }); } let { title, artistName, albumTitle, trackNumber, year, genre } = req.body; // Извлечь метаданные из файла (используем buffer) const metadata = await extractAudioMetadata(req.uploadedMusicFile.buffer, true); // Использовать метаданные если поля не заполнены title = title || metadata.title || path.basename(req.uploadedMusicFile.originalname, path.extname(req.uploadedMusicFile.originalname)); artistName = artistName || metadata.artist || 'Unknown Artist'; albumTitle = albumTitle || metadata.album; trackNumber = trackNumber || metadata.trackNumber; year = year || metadata.year; genre = genre || metadata.genre; // Найти или создать исполнителя const artist = await findOrCreateArtist(artistName, req.user._id); // Найти или создать альбом (если указан) let album = null; if (albumTitle) { album = await Album.findOne({ title: albumTitle, artist: artist._id }); if (!album) { album = await Album.create({ title: albumTitle, artist: artist._id, coverImage: metadata.coverImage, year: year ? parseInt(year) : null, genre: genre || '', addedBy: req.user._id }); } } // Создать трек (используем URL из MinIO или локальный путь) const fileUrl = req.uploadedMusicFile.url; const track = await Track.create({ title, artist: artist._id, album: album ? album._id : null, fileUrl, coverImage: metadata.coverImage || (album ? album.coverImage : null), file: { size: req.uploadedMusicFile.size, mimeType: req.uploadedMusicFile.mimetype, duration: Math.round(metadata.duration) }, trackNumber: trackNumber ? parseInt(trackNumber) : 0, year: year ? parseInt(year) : null, genre: genre || '', addedBy: req.user._id }); // Обновить статистику artist.stats.tracks += 1; await artist.save(); if (album) { album.stats.tracks += 1; album.stats.duration += Math.round(metadata.duration); await album.save(); } // Populate для ответа await track.populate('artist album'); res.json({ success: true, track }); } catch (error) { console.error('Ошибка загрузки трека:', error); res.status(500).json({ error: 'Ошибка загрузки трека' }); } }); // Загрузка альбома из ZIP с извлечением метаданных router.post('/upload-album', authenticate, uploadMusicAlbum, cleanupOnError(), async (req, res) => { try { if (!req.uploadedMusicFile) { return res.status(400).json({ error: 'ZIP файл не загружен' }); } const ext = path.extname(req.uploadedMusicFile.originalname).toLowerCase(); if (ext !== '.zip' && req.uploadedMusicFile.mimetype !== 'application/zip' && req.uploadedMusicFile.mimetype !== 'application/x-zip-compressed') { return res.status(400).json({ error: 'Требуется ZIP файл' }); } let { artistName, albumTitle, year, genre } = req.body; // Распаковать ZIP из buffer const zip = new AdmZip(req.uploadedMusicFile.buffer); const zipEntries = zip.getEntries(); // Фильтровать аудио файлы const audioExtensions = ['.mp3', '.wav', '.ogg', '.m4a', '.flac']; const audioFiles = zipEntries.filter(entry => { const ext = path.extname(entry.entryName).toLowerCase(); return audioExtensions.includes(ext) && !entry.isDirectory; }); if (audioFiles.length === 0) { return res.status(400).json({ error: 'В архиве нет аудио файлов' }); } // Извлечь первый трек для получения метаданных альбома const firstEntry = audioFiles[0]; const firstBuffer = zip.readFile(firstEntry); // Извлечь метаданные из первого трека (из buffer) const firstMetadata = await extractAudioMetadata(firstBuffer, true); // Использовать метаданные для альбома если не указаны artistName = artistName || firstMetadata.artist || 'Unknown Artist'; albumTitle = albumTitle || firstMetadata.album || path.basename(req.uploadedMusicFile.originalname, '.zip'); year = year || firstMetadata.year; genre = genre || firstMetadata.genre; const albumCoverImage = firstMetadata.coverImage; // Найти или создать исполнителя const artist = await findOrCreateArtist(artistName, req.user._id); // Создать альбом const album = await Album.create({ title: albumTitle, artist: artist._id, coverImage: albumCoverImage, year: year ? parseInt(year) : null, genre: genre || '', addedBy: req.user._id }); // Извлечь и сохранить треки const tracks = []; let totalDuration = 0; for (let i = 0; i < audioFiles.length; i++) { const entry = audioFiles[i]; const fileName = path.basename(entry.entryName); const ext = path.extname(fileName); // Извлечь файл из ZIP в buffer const trackBuffer = zip.readFile(entry); // Извлечь метаданные из трека (из buffer) const trackMetadata = await extractAudioMetadata(trackBuffer, true); // Определить название трека const trackTitle = trackMetadata.title || fileName.replace(ext, ''); const trackArtist = trackMetadata.artist || artistName; // Если исполнитель трека отличается от исполнителя альбома, найти или создать let trackArtistId = artist._id; if (trackArtist !== artistName) { const trackArtistObj = await findOrCreateArtist(trackArtist, req.user._id); trackArtistId = trackArtistObj._id; } // Определить MIME тип const mimeTypeMap = { '.mp3': 'audio/mpeg', '.wav': 'audio/wav', '.ogg': 'audio/ogg', '.m4a': 'audio/mp4', '.flac': 'audio/flac' }; const mimeType = mimeTypeMap[ext.toLowerCase()] || 'audio/mpeg'; // Загрузить трек в MinIO или сохранить локально let trackFileUrl; const { isEnabled: isMinioEnabled } = require('../utils/minio'); if (isMinioEnabled()) { trackFileUrl = await uploadFile( trackBuffer, fileName, mimeType, 'music' ); } else { // Локальное хранилище const uploadDir = path.join(__dirname, '../uploads/music'); if (!(await fs.access(uploadDir).catch(() => false))) { await fs.mkdir(uploadDir, { recursive: true }); } const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); const newFileName = `${uniqueSuffix}${ext}`; const filePath = path.join(uploadDir, newFileName); await fs.writeFile(filePath, trackBuffer); trackFileUrl = `/uploads/music/${newFileName}`; } // Создать трек const track = await Track.create({ title: trackTitle, artist: trackArtistId, album: album._id, fileUrl: trackFileUrl, coverImage: trackMetadata.coverImage || albumCoverImage, file: { size: trackBuffer.length, mimeType: mimeType, duration: Math.round(trackMetadata.duration) }, trackNumber: trackMetadata.trackNumber || (i + 1), year: trackMetadata.year || year ? parseInt(year) : null, genre: trackMetadata.genre || genre || '', addedBy: req.user._id }); tracks.push(track); totalDuration += Math.round(trackMetadata.duration); } // Обновить статистику artist.stats.tracks += tracks.length; artist.stats.albums += 1; await artist.save(); album.stats.tracks = tracks.length; album.stats.duration = totalDuration; await album.save(); // Populate для ответа await album.populate('artist'); res.json({ success: true, album, tracks, message: `Альбом "${album.title}" успешно загружен. Добавлено треков: ${tracks.length}` }); } catch (error) { console.error('Ошибка загрузки альбома:', error); res.status(500).json({ error: 'Ошибка загрузки альбома' }); } }); // Поиск треков, исполнителей, альбомов router.get('/search', authenticate, async (req, res) => { try { const { q, type = 'all', limit = 20, page = 1 } = req.query; if (!q || q.trim().length < 1) { return res.json({ tracks: [], artists: [], albums: [] }); } const skip = (parseInt(page) - 1) * parseInt(limit); const limitNum = parseInt(limit); const searchRegex = new RegExp(q.trim(), 'i'); let tracks = []; let artists = []; let albums = []; if (type === 'all' || type === 'tracks') { tracks = await Track.find({ title: searchRegex }) .populate('artist album') .sort({ 'stats.plays': -1, createdAt: -1 }) .limit(limitNum) .skip(skip); } if (type === 'all' || type === 'artists') { artists = await Artist.find({ name: searchRegex }) .sort({ 'stats.tracks': -1, createdAt: -1 }) .limit(limitNum) .skip(skip); } if (type === 'all' || type === 'albums') { albums = await Album.find({ title: searchRegex }) .populate('artist') .sort({ 'stats.tracks': -1, createdAt: -1 }) .limit(limitNum) .skip(skip); } res.json({ tracks, artists, albums }); } catch (error) { console.error('Ошибка поиска:', error); res.status(500).json({ error: 'Ошибка поиска' }); } }); // Получить список треков router.get('/tracks', authenticate, async (req, res) => { try { const { limit = 50, page = 1, artistId, albumId } = req.query; const skip = (parseInt(page) - 1) * parseInt(limit); const limitNum = parseInt(limit); const query = {}; if (artistId) query.artist = artistId; if (albumId) query.album = albumId; const tracks = await Track.find(query) .populate('artist album') .sort({ createdAt: -1 }) .limit(limitNum) .skip(skip); const total = await Track.countDocuments(query); res.json({ tracks, total, page: parseInt(page), pages: Math.ceil(total / limitNum) }); } catch (error) { console.error('Ошибка получения треков:', error); res.status(500).json({ error: 'Ошибка получения треков' }); } }); // Получить трек по ID router.get('/tracks/:trackId', authenticate, async (req, res) => { try { const track = await Track.findById(req.params.trackId) .populate('artist album'); if (!track) { return res.status(404).json({ error: 'Трек не найден' }); } res.json({ track }); } catch (error) { console.error('Ошибка получения трека:', error); res.status(500).json({ error: 'Ошибка получения трека' }); } }); // Получить альбом по ID с треками router.get('/albums/:albumId', authenticate, async (req, res) => { try { const album = await Album.findById(req.params.albumId) .populate('artist'); if (!album) { return res.status(404).json({ error: 'Альбом не найден' }); } const tracks = await Track.find({ album: album._id }) .populate('artist') .sort({ trackNumber: 1 }); res.json({ album, tracks }); } catch (error) { console.error('Ошибка получения альбома:', error); res.status(500).json({ error: 'Ошибка получения альбома' }); } }); // Добавить трек в избранное router.post('/favorites/:trackId', authenticate, async (req, res) => { try { const track = await Track.findById(req.params.trackId); if (!track) { return res.status(404).json({ error: 'Трек не найден' }); } // Проверить, не добавлен ли уже const existing = await FavoriteTrack.findOne({ user: req.user._id, track: track._id }); if (existing) { return res.status(400).json({ error: 'Трек уже в избранном' }); } await FavoriteTrack.create({ user: req.user._id, track: track._id }); // Обновить счетчик track.stats.favorites += 1; await track.save(); res.json({ success: true, message: 'Трек добавлен в избранное' }); } catch (error) { console.error('Ошибка добавления в избранное:', error); res.status(500).json({ error: 'Ошибка добавления в избранное' }); } }); // Удалить трек из избранного router.delete('/favorites/:trackId', authenticate, async (req, res) => { try { const favorite = await FavoriteTrack.findOneAndDelete({ user: req.user._id, track: req.params.trackId }); if (!favorite) { return res.status(404).json({ error: 'Трек не найден в избранном' }); } // Обновить счетчик const track = await Track.findById(req.params.trackId); if (track) { track.stats.favorites = Math.max(0, track.stats.favorites - 1); await track.save(); } res.json({ success: true, message: 'Трек удален из избранного' }); } catch (error) { console.error('Ошибка удаления из избранного:', error); res.status(500).json({ error: 'Ошибка удаления из избранного' }); } }); // Получить избранные треки пользователя router.get('/favorites', authenticate, async (req, res) => { try { const { limit = 50, page = 1 } = req.query; const skip = (parseInt(page) - 1) * parseInt(limit); const limitNum = parseInt(limit); const favorites = await FavoriteTrack.find({ user: req.user._id }) .populate({ path: 'track', populate: { path: 'artist album' } }) .sort({ createdAt: -1 }) .limit(limitNum) .skip(skip); const total = await FavoriteTrack.countDocuments({ user: req.user._id }); const tracks = favorites.map(f => f.track).filter(t => t); // Фильтр на случай удаленных треков res.json({ tracks, total, page: parseInt(page), pages: Math.ceil(total / limitNum) }); } catch (error) { console.error('Ошибка получения избранного:', error); res.status(500).json({ error: 'Ошибка получения избранного' }); } }); // Увеличить счетчик прослушиваний router.post('/tracks/:trackId/play', authenticate, async (req, res) => { try { const track = await Track.findById(req.params.trackId); if (!track) { return res.status(404).json({ error: 'Трек не найден' }); } track.stats.plays += 1; await track.save(); // Также обновить статистику исполнителя const artist = await Artist.findById(track.artist); if (artist) { artist.stats.plays += 1; await artist.save(); } // И альбома if (track.album) { const album = await Album.findById(track.album); if (album) { album.stats.plays += 1; await album.save(); } } res.json({ success: true }); } catch (error) { console.error('Ошибка обновления счетчика:', error); res.status(500).json({ error: 'Ошибка обновления счетчика' }); } }); module.exports = router;