2025-12-15 19:51:01 +00:00
|
|
|
|
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');
|
2025-12-15 21:04:26 +00:00
|
|
|
|
const { uploadMusicTrack, uploadMusicAlbum, cleanupOnError } = require('../middleware/upload');
|
|
|
|
|
|
const { uploadFile } = require('../utils/minio');
|
2025-12-15 19:51:01 +00:00
|
|
|
|
const Artist = require('../models/Artist');
|
|
|
|
|
|
const Album = require('../models/Album');
|
|
|
|
|
|
const Track = require('../models/Track');
|
|
|
|
|
|
const FavoriteTrack = require('../models/FavoriteTrack');
|
|
|
|
|
|
|
2025-12-15 21:04:26 +00:00
|
|
|
|
// Извлечь метаданные из аудио файла (из buffer или пути)
|
|
|
|
|
|
async function extractAudioMetadata(audioData, isBuffer = true) {
|
2025-12-15 19:51:01 +00:00
|
|
|
|
try {
|
2025-12-15 21:04:26 +00:00
|
|
|
|
const metadata = isBuffer
|
|
|
|
|
|
? await mm.parseBuffer(audioData, audioData.mimetype || 'audio/mpeg')
|
|
|
|
|
|
: await mm.parseFile(audioData);
|
2025-12-15 19:51:01 +00:00
|
|
|
|
|
|
|
|
|
|
// Извлечь обложку если есть
|
2025-12-15 21:04:26 +00:00
|
|
|
|
let coverImageUrl = null;
|
2025-12-15 19:51:01 +00:00
|
|
|
|
if (metadata.common.picture && metadata.common.picture.length > 0) {
|
|
|
|
|
|
const picture = metadata.common.picture[0];
|
2025-12-15 21:04:26 +00:00
|
|
|
|
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}`;
|
|
|
|
|
|
}
|
2025-12-15 19:51:01 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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,
|
2025-12-15 21:04:26 +00:00
|
|
|
|
coverImage: coverImageUrl
|
2025-12-15 19:51:01 +00:00
|
|
|
|
};
|
|
|
|
|
|
} 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Загрузка одного трека
|
2025-12-15 21:04:26 +00:00
|
|
|
|
router.post('/upload-track', authenticate, uploadMusicTrack, cleanupOnError(), async (req, res) => {
|
2025-12-15 19:51:01 +00:00
|
|
|
|
try {
|
2025-12-15 21:04:26 +00:00
|
|
|
|
if (!req.uploadedMusicFile) {
|
2025-12-15 19:51:01 +00:00
|
|
|
|
return res.status(400).json({ error: 'Файл не загружен' });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let { title, artistName, albumTitle, trackNumber, year, genre } = req.body;
|
|
|
|
|
|
|
2025-12-15 21:04:26 +00:00
|
|
|
|
// Извлечь метаданные из файла (используем buffer)
|
|
|
|
|
|
const metadata = await extractAudioMetadata(req.uploadedMusicFile.buffer, true);
|
2025-12-15 19:51:01 +00:00
|
|
|
|
|
|
|
|
|
|
// Использовать метаданные если поля не заполнены
|
2025-12-15 21:04:26 +00:00
|
|
|
|
title = title || metadata.title || path.basename(req.uploadedMusicFile.originalname, path.extname(req.uploadedMusicFile.originalname));
|
2025-12-15 19:51:01 +00:00
|
|
|
|
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
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-15 21:04:26 +00:00
|
|
|
|
// Создать трек (используем URL из MinIO или локальный путь)
|
|
|
|
|
|
const fileUrl = req.uploadedMusicFile.url;
|
2025-12-15 19:51:01 +00:00
|
|
|
|
|
|
|
|
|
|
const track = await Track.create({
|
|
|
|
|
|
title,
|
|
|
|
|
|
artist: artist._id,
|
|
|
|
|
|
album: album ? album._id : null,
|
|
|
|
|
|
fileUrl,
|
|
|
|
|
|
coverImage: metadata.coverImage || (album ? album.coverImage : null),
|
|
|
|
|
|
file: {
|
2025-12-15 21:04:26 +00:00
|
|
|
|
size: req.uploadedMusicFile.size,
|
|
|
|
|
|
mimeType: req.uploadedMusicFile.mimetype,
|
2025-12-15 19:51:01 +00:00
|
|
|
|
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 с извлечением метаданных
|
2025-12-15 21:04:26 +00:00
|
|
|
|
router.post('/upload-album', authenticate, uploadMusicAlbum, cleanupOnError(), async (req, res) => {
|
2025-12-15 19:51:01 +00:00
|
|
|
|
try {
|
2025-12-15 21:04:26 +00:00
|
|
|
|
if (!req.uploadedMusicFile) {
|
2025-12-15 19:51:01 +00:00
|
|
|
|
return res.status(400).json({ error: 'ZIP файл не загружен' });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-15 21:04:26 +00:00
|
|
|
|
const ext = path.extname(req.uploadedMusicFile.originalname).toLowerCase();
|
|
|
|
|
|
if (ext !== '.zip' && req.uploadedMusicFile.mimetype !== 'application/zip' && req.uploadedMusicFile.mimetype !== 'application/x-zip-compressed') {
|
2025-12-15 19:51:01 +00:00
|
|
|
|
return res.status(400).json({ error: 'Требуется ZIP файл' });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let { artistName, albumTitle, year, genre } = req.body;
|
|
|
|
|
|
|
2025-12-15 21:04:26 +00:00
|
|
|
|
// Распаковать ZIP из buffer
|
|
|
|
|
|
const zip = new AdmZip(req.uploadedMusicFile.buffer);
|
2025-12-15 19:51:01 +00:00
|
|
|
|
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];
|
2025-12-15 21:04:26 +00:00
|
|
|
|
const firstBuffer = zip.readFile(firstEntry);
|
2025-12-15 19:51:01 +00:00
|
|
|
|
|
2025-12-15 21:04:26 +00:00
|
|
|
|
// Извлечь метаданные из первого трека (из buffer)
|
|
|
|
|
|
const firstMetadata = await extractAudioMetadata(firstBuffer, true);
|
2025-12-15 19:51:01 +00:00
|
|
|
|
|
|
|
|
|
|
// Использовать метаданные для альбома если не указаны
|
|
|
|
|
|
artistName = artistName || firstMetadata.artist || 'Unknown Artist';
|
2025-12-15 21:04:26 +00:00
|
|
|
|
albumTitle = albumTitle || firstMetadata.album || path.basename(req.uploadedMusicFile.originalname, '.zip');
|
2025-12-15 19:51:01 +00:00
|
|
|
|
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);
|
2025-12-15 21:04:26 +00:00
|
|
|
|
|
|
|
|
|
|
// Извлечь файл из ZIP в buffer
|
|
|
|
|
|
const trackBuffer = zip.readFile(entry);
|
2025-12-15 19:51:01 +00:00
|
|
|
|
|
2025-12-15 21:04:26 +00:00
|
|
|
|
// Извлечь метаданные из трека (из buffer)
|
|
|
|
|
|
const trackMetadata = await extractAudioMetadata(trackBuffer, true);
|
2025-12-15 19:51:01 +00:00
|
|
|
|
|
|
|
|
|
|
// Определить название трека
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-15 21:04:26 +00:00
|
|
|
|
// Определить 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}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-15 19:51:01 +00:00
|
|
|
|
// Создать трек
|
|
|
|
|
|
const track = await Track.create({
|
|
|
|
|
|
title: trackTitle,
|
|
|
|
|
|
artist: trackArtistId,
|
|
|
|
|
|
album: album._id,
|
2025-12-15 21:04:26 +00:00
|
|
|
|
fileUrl: trackFileUrl,
|
2025-12-15 19:51:01 +00:00
|
|
|
|
coverImage: trackMetadata.coverImage || albumCoverImage,
|
|
|
|
|
|
file: {
|
2025-12-15 21:04:26 +00:00
|
|
|
|
size: trackBuffer.length,
|
|
|
|
|
|
mimeType: mimeType,
|
2025-12-15 19:51:01 +00:00
|
|
|
|
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,
|
2025-12-15 21:04:26 +00:00
|
|
|
|
message: `Альбом "${album.title}" успешно загружен. Добавлено треков: ${tracks.length}`
|
2025-12-15 19:51:01 +00:00
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Ошибка загрузки альбома:', error);
|
2025-12-15 21:04:26 +00:00
|
|
|
|
res.status(500).json({ error: 'Ошибка загрузки альбома' });
|
2025-12-15 19:51:01 +00:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Поиск треков, исполнителей, альбомов
|
|
|
|
|
|
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;
|
|
|
|
|
|
|