const express = require('express'); const router = express.Router(); const multer = require('multer'); const path = require('path'); const fs = require('fs').promises; const AdmZip = require('adm-zip'); const { authenticate } = require('../middleware/auth'); const Artist = require('../models/Artist'); const Album = require('../models/Album'); const Track = require('../models/Track'); const FavoriteTrack = require('../models/FavoriteTrack'); // Настройка multer для загрузки файлов const storage = multer.diskStorage({ destination: async (req, file, cb) => { const uploadDir = path.join(__dirname, '../uploads/music'); try { await fs.mkdir(uploadDir, { recursive: true }); cb(null, uploadDir); } catch (error) { cb(error); } }, filename: (req, file, cb) => { const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); const ext = path.extname(file.originalname); cb(null, `${uniqueSuffix}${ext}`); } }); const upload = multer({ storage, limits: { fileSize: 50 * 1024 * 1024 // 50MB для треков }, fileFilter: (req, file, cb) => { const allowedMimeTypes = ['audio/mpeg', 'audio/mp3', 'audio/wav', 'audio/ogg', 'audio/mp4', 'application/zip']; if (allowedMimeTypes.includes(file.mimetype)) { cb(null, true); } else { cb(new Error('Неподдерживаемый формат файла')); } } }); // Вспомогательная функция для поиска или создания исполнителя 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, upload.single('track'), async (req, res) => { try { if (!req.file) { return res.status(400).json({ error: 'Файл не загружен' }); } const { title, artistName, albumTitle, trackNumber, year, genre } = req.body; if (!title || !artistName) { // Удалить загруженный файл await fs.unlink(req.file.path).catch(() => {}); return res.status(400).json({ error: 'Название трека и исполнитель обязательны' }); } // Найти или создать исполнителя 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, year: year ? parseInt(year) : null, genre: genre || '', addedBy: req.user._id }); } } // Создать трек const fileUrl = `/uploads/music/${req.file.filename}`; const track = await Track.create({ title, artist: artist._id, album: album ? album._id : null, fileUrl, file: { size: req.file.size, mimeType: req.file.mimetype, duration: 0 // TODO: извлечь из метаданных }, 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; await album.save(); } // Populate для ответа await track.populate('artist album'); res.json({ success: true, track }); } catch (error) { console.error('Ошибка загрузки трека:', error); // Удалить файл в случае ошибки if (req.file) { await fs.unlink(req.file.path).catch(() => {}); } res.status(500).json({ error: 'Ошибка загрузки трека' }); } }); // Загрузка альбома из ZIP router.post('/upload-album', authenticate, upload.single('album'), async (req, res) => { try { if (!req.file) { return res.status(400).json({ error: 'ZIP файл не загружен' }); } if (req.file.mimetype !== 'application/zip') { await fs.unlink(req.file.path).catch(() => {}); return res.status(400).json({ error: 'Требуется ZIP файл' }); } const { artistName, albumTitle, year, genre } = req.body; if (!artistName || !albumTitle) { await fs.unlink(req.file.path).catch(() => {}); return res.status(400).json({ error: 'Исполнитель и название альбома обязательны' }); } // Распаковать ZIP const zip = new AdmZip(req.file.path); 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) { await fs.unlink(req.file.path).catch(() => {}); return res.status(400).json({ error: 'В архиве нет аудио файлов' }); } // Найти или создать исполнителя const artist = await findOrCreateArtist(artistName, req.user._id); // Создать альбом const album = await Album.create({ title: albumTitle, artist: artist._id, year: year ? parseInt(year) : null, genre: genre || '', addedBy: req.user._id }); // Извлечь и сохранить треки const tracks = []; const uploadDir = path.join(__dirname, '../uploads/music'); for (let i = 0; i < audioFiles.length; i++) { const entry = audioFiles[i]; const fileName = path.basename(entry.entryName); const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); const ext = path.extname(fileName); const newFileName = `${uniqueSuffix}${ext}`; const filePath = path.join(uploadDir, newFileName); // Извлечь файл zip.extractEntryTo(entry, uploadDir, false, true, false, newFileName); // Получить размер файла const stats = await fs.stat(filePath); // Создать трек const track = await Track.create({ title: fileName.replace(ext, ''), artist: artist._id, album: album._id, fileUrl: `/uploads/music/${newFileName}`, file: { size: stats.size, mimeType: 'audio/mpeg', // TODO: определить правильный MIME type duration: 0 }, trackNumber: i + 1, year: year ? parseInt(year) : null, genre: genre || '', addedBy: req.user._id }); tracks.push(track); } // Удалить ZIP после обработки await fs.unlink(req.file.path).catch(() => {}); // Обновить статистику artist.stats.tracks += tracks.length; artist.stats.albums += 1; await artist.save(); album.stats.tracks = tracks.length; await album.save(); // Populate для ответа await album.populate('artist'); res.json({ success: true, album, tracks, message: `Загружено ${tracks.length} треков` }); } catch (error) { console.error('Ошибка загрузки альбома:', error); if (req.file) { await fs.unlink(req.file.path).catch(() => {}); } 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;