diff --git a/DELETE_MUSIC.md b/DELETE_MUSIC.md index e809b39..cc8dc4d 100644 --- a/DELETE_MUSIC.md +++ b/DELETE_MUSIC.md @@ -12,6 +12,18 @@ ## Использование +### Шаг 1: Проверка подключения и данных + +Сначала проверьте, что скрипт может подключиться к базе и видит данные: + +```cmd +node backend/scripts/testDeleteMusic.js +``` + +Этот скрипт покажет статистику без удаления данных. + +### Шаг 2: Удаление данных + ### Для Windows (CMD/PowerShell): ```cmd @@ -32,6 +44,8 @@ node scripts/deleteAllMusic.js node backend/scripts/deleteAllMusic.js ``` +**Примечание:** Скрипт покажет предупреждение и подождет 3 секунды перед удалением. Нажмите Ctrl+C для отмены, если передумали. + ## Что делает скрипт 1. **Подключается к MongoDB** используя настройки из `.env` diff --git a/backend/middleware/upload.js b/backend/middleware/upload.js index d66cce5..0b63ee0 100644 --- a/backend/middleware/upload.js +++ b/backend/middleware/upload.js @@ -174,13 +174,127 @@ function cleanupOnError() { }; } +// Специальная конфигурация для музыки (больший лимит размера) +const musicMulterConfig = { + storage: tempStorage, + limits: { + fileSize: 105 * 1024 * 1024, // 105MB для ZIP альбомов + files: 1 + }, + fileFilter: (req, file, cb) => { + const allowedMimeTypes = ['audio/mpeg', 'audio/mp3', 'audio/wav', 'audio/ogg', 'audio/mp4', 'audio/m4a', 'application/zip', 'application/x-zip-compressed']; + const ext = path.extname(file.originalname).toLowerCase(); + const allowedExts = ['.mp3', '.wav', '.ogg', '.m4a', '.flac', '.zip']; + + if (allowedMimeTypes.includes(file.mimetype) || allowedExts.includes(ext)) { + cb(null, true); + } else { + cb(new Error('Неподдерживаемый формат файла. Разрешены: MP3, WAV, OGG, M4A, FLAC, ZIP')); + } + } +}; + +/** + * Middleware для загрузки музыки (треки и ZIP альбомы) + */ +function createMusicUploadMiddleware(fieldName = 'track') { + const upload = multer(musicMulterConfig); + const multerMiddleware = upload.single(fieldName); + + return async (req, res, next) => { + multerMiddleware(req, res, async (err) => { + if (err) { + log('error', 'Ошибка multer (музыка)', { error: err.message }); + return res.status(400).json({ error: err.message }); + } + + if (!req.file) { + return next(); + } + + try { + // Если MinIO включен, загрузить туда + if (isMinioEnabled()) { + try { + const fileUrl = await uploadFile( + req.file.buffer, + req.file.originalname, + req.file.mimetype, + 'music' + ); + req.uploadedMusicFile = { + url: fileUrl, + buffer: req.file.buffer, + originalname: req.file.originalname, + mimetype: req.file.mimetype, + size: req.file.size + }; + req.uploadMethod = 'minio'; + + log('info', 'Музыкальный файл загружен в MinIO', { + filename: req.file.originalname, + size: req.file.size, + url: fileUrl + }); + } catch (uploadError) { + log('error', 'Ошибка загрузки в MinIO', { + error: uploadError.message, + filename: req.file.originalname + }); + throw uploadError; + } + } else { + // Локальное хранилище (fallback) + const uploadDir = path.join(__dirname, '../uploads/music'); + + if (!fs.existsSync(uploadDir)) { + fs.mkdirSync(uploadDir, { recursive: true }); + } + + const timestamp = Date.now(); + const random = Math.round(Math.random() * 1E9); + const ext = path.extname(req.file.originalname); + const filename = `${timestamp}-${random}${ext}`; + const filepath = path.join(uploadDir, filename); + + fs.writeFileSync(filepath, req.file.buffer); + + const relativePath = `/uploads/music/${filename}`; + req.uploadedMusicFile = { + url: relativePath, + path: filepath, + buffer: req.file.buffer, + originalname: req.file.originalname, + mimetype: req.file.mimetype, + size: req.file.size + }; + req.uploadMethod = 'local'; + + log('info', 'Музыкальный файл загружен локально', { + filename, + path: relativePath + }); + } + + next(); + } catch (error) { + log('error', 'Ошибка обработки музыкального файла', { error: error.message }); + return res.status(500).json({ error: 'Ошибка загрузки файла' }); + } + }); + }; +} + module.exports = { createUploadMiddleware, cleanupOnError, + createMusicUploadMiddleware, // Готовые middleware для разных случаев uploadPostImages: createUploadMiddleware('images', 5, 'posts'), uploadAvatar: createUploadMiddleware('avatar', 1, 'avatars'), - uploadChannelMedia: createUploadMiddleware('images', 10, 'channel') + uploadChannelMedia: createUploadMiddleware('images', 10, 'channel'), + uploadMusicTrack: createMusicUploadMiddleware('track'), + uploadMusicAlbum: createMusicUploadMiddleware('album') }; diff --git a/backend/routes/music.js b/backend/routes/music.js index 7e745cc..e86415c 100644 --- a/backend/routes/music.js +++ b/backend/routes/music.js @@ -1,65 +1,54 @@ 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 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'); -// Настройка 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: 105 * 1024 * 1024 // 105MB для ZIP альбомов (соответствует express.json limit) - }, - fileFilter: (req, file, cb) => { - const allowedMimeTypes = ['audio/mpeg', 'audio/mp3', 'audio/wav', 'audio/ogg', 'audio/mp4', 'audio/m4a', 'application/zip', 'application/x-zip-compressed']; - const ext = path.extname(file.originalname).toLowerCase(); - const allowedExts = ['.mp3', '.wav', '.ogg', '.m4a', '.flac', '.zip']; - - if (allowedMimeTypes.includes(file.mimetype) || allowedExts.includes(ext)) { - cb(null, true); - } else { - cb(new Error('Неподдерживаемый формат файла')); - } - } -}); - -// Извлечь метаданные из аудио файла -async function extractAudioMetadata(filePath) { +// Извлечь метаданные из аудио файла (из buffer или пути) +async function extractAudioMetadata(audioData, isBuffer = true) { try { - const metadata = await mm.parseFile(filePath); + const metadata = isBuffer + ? await mm.parseBuffer(audioData, audioData.mimetype || 'audio/mpeg') + : await mm.parseFile(audioData); // Извлечь обложку если есть - let coverImagePath = null; + let coverImageUrl = null; if (metadata.common.picture && metadata.common.picture.length > 0) { const picture = metadata.common.picture[0]; - const coverFileName = `cover-${Date.now()}.${picture.format.split('/')[1] || 'jpg'}`; - coverImagePath = path.join(__dirname, '../uploads/music', coverFileName); - await fs.writeFile(coverImagePath, picture.data); - coverImagePath = `/uploads/music/${coverFileName}`; + 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 { @@ -70,7 +59,7 @@ async function extractAudioMetadata(filePath) { genre: metadata.common.genre?.[0] || null, trackNumber: metadata.common.track?.no || null, duration: metadata.format.duration || 0, - coverImage: coverImagePath + coverImage: coverImageUrl }; } catch (error) { console.error('Ошибка извлечения метаданных:', error); @@ -105,19 +94,19 @@ async function findOrCreateArtist(artistName, userId) { } // Загрузка одного трека -router.post('/upload-track', authenticate, upload.single('track'), async (req, res) => { +router.post('/upload-track', authenticate, uploadMusicTrack, cleanupOnError(), async (req, res) => { try { - if (!req.file) { + if (!req.uploadedMusicFile) { return res.status(400).json({ error: 'Файл не загружен' }); } let { title, artistName, albumTitle, trackNumber, year, genre } = req.body; - // Извлечь метаданные из файла - const metadata = await extractAudioMetadata(req.file.path); + // Извлечь метаданные из файла (используем buffer) + const metadata = await extractAudioMetadata(req.uploadedMusicFile.buffer, true); // Использовать метаданные если поля не заполнены - title = title || metadata.title || path.basename(req.file.originalname, path.extname(req.file.originalname)); + 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; @@ -144,8 +133,8 @@ router.post('/upload-track', authenticate, upload.single('track'), async (req, r } } - // Создать трек - const fileUrl = `/uploads/music/${req.file.filename}`; + // Создать трек (используем URL из MinIO или локальный путь) + const fileUrl = req.uploadedMusicFile.url; const track = await Track.create({ title, @@ -154,8 +143,8 @@ router.post('/upload-track', authenticate, upload.single('track'), async (req, r fileUrl, coverImage: metadata.coverImage || (album ? album.coverImage : null), file: { - size: req.file.size, - mimeType: req.file.mimetype, + size: req.uploadedMusicFile.size, + mimeType: req.uploadedMusicFile.mimetype, duration: Math.round(metadata.duration) }, trackNumber: trackNumber ? parseInt(trackNumber) : 0, @@ -183,33 +172,26 @@ router.post('/upload-track', authenticate, upload.single('track'), async (req, r }); } 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) => { +router.post('/upload-album', authenticate, uploadMusicAlbum, cleanupOnError(), async (req, res) => { try { - if (!req.file) { + if (!req.uploadedMusicFile) { return res.status(400).json({ error: 'ZIP файл не загружен' }); } - const ext = path.extname(req.file.originalname).toLowerCase(); - if (ext !== '.zip' && req.file.mimetype !== 'application/zip' && req.file.mimetype !== 'application/x-zip-compressed') { - await fs.unlink(req.file.path).catch(() => {}); + 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 - const zip = new AdmZip(req.file.path); + // Распаковать ZIP из buffer + const zip = new AdmZip(req.uploadedMusicFile.buffer); const zipEntries = zip.getEntries(); // Фильтровать аудио файлы @@ -220,33 +202,24 @@ router.post('/upload-album', authenticate, upload.single('album'), async (req, r }); if (audioFiles.length === 0) { - await fs.unlink(req.file.path).catch(() => {}); return res.status(400).json({ error: 'В архиве нет аудио файлов' }); } - const uploadDir = path.join(__dirname, '../uploads/music'); - // Извлечь первый трек для получения метаданных альбома const firstEntry = audioFiles[0]; - const firstFileName = path.basename(firstEntry.entryName); - const firstTempName = `temp-${Date.now()}${path.extname(firstFileName)}`; - const firstTempPath = path.join(uploadDir, firstTempName); + const firstBuffer = zip.readFile(firstEntry); - zip.extractEntryTo(firstEntry, uploadDir, false, true, false, firstTempName); - - // Извлечь метаданные из первого трека - const firstMetadata = await extractAudioMetadata(firstTempPath); + // Извлечь метаданные из первого трека (из buffer) + const firstMetadata = await extractAudioMetadata(firstBuffer, true); // Использовать метаданные для альбома если не указаны artistName = artistName || firstMetadata.artist || 'Unknown Artist'; - albumTitle = albumTitle || firstMetadata.album || path.basename(req.file.originalname, '.zip'); + albumTitle = albumTitle || firstMetadata.album || path.basename(req.uploadedMusicFile.originalname, '.zip'); year = year || firstMetadata.year; genre = genre || firstMetadata.genre; const albumCoverImage = firstMetadata.coverImage; - // Удалить временный файл - await fs.unlink(firstTempPath).catch(() => {}); // Найти или создать исполнителя const artist = await findOrCreateArtist(artistName, req.user._id); @@ -268,19 +241,13 @@ router.post('/upload-album', authenticate, upload.single('album'), async (req, r 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 в buffer + const trackBuffer = zip.readFile(entry); - // Извлечь файл - zip.extractEntryTo(entry, uploadDir, false, true, false, newFileName); - - // Извлечь метаданные из трека - const trackMetadata = await extractAudioMetadata(filePath); - - // Получить размер файла - const stats = await fs.stat(filePath); + // Извлечь метаданные из трека (из buffer) + const trackMetadata = await extractAudioMetadata(trackBuffer, true); // Определить название трека const trackTitle = trackMetadata.title || fileName.replace(ext, ''); @@ -293,16 +260,49 @@ router.post('/upload-album', authenticate, upload.single('album'), async (req, r 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: `/uploads/music/${newFileName}`, + fileUrl: trackFileUrl, coverImage: trackMetadata.coverImage || albumCoverImage, file: { - size: stats.size, - mimeType: req.file.mimetype, + size: trackBuffer.length, + mimeType: mimeType, duration: Math.round(trackMetadata.duration) }, trackNumber: trackMetadata.trackNumber || (i + 1), @@ -315,9 +315,6 @@ router.post('/upload-album', authenticate, upload.single('album'), async (req, r totalDuration += Math.round(trackMetadata.duration); } - // Удалить ZIP после обработки - await fs.unlink(req.file.path).catch(() => {}); - // Обновить статистику artist.stats.tracks += tracks.length; artist.stats.albums += 1; @@ -334,16 +331,11 @@ router.post('/upload-album', authenticate, upload.single('album'), async (req, r success: true, album, tracks, - message: `Загружено ${tracks.length} треков из альбома "${albumTitle}"` + message: `Альбом "${album.title}" успешно загружен. Добавлено треков: ${tracks.length}` }); } catch (error) { console.error('Ошибка загрузки альбома:', error); - - if (req.file) { - await fs.unlink(req.file.path).catch(() => {}); - } - - res.status(500).json({ error: 'Ошибка загрузки альбома: ' + error.message }); + res.status(500).json({ error: 'Ошибка загрузки альбома' }); } }); diff --git a/backend/scripts/deleteAllMusic.js b/backend/scripts/deleteAllMusic.js index cb19cb7..38c7023 100644 --- a/backend/scripts/deleteAllMusic.js +++ b/backend/scripts/deleteAllMusic.js @@ -18,13 +18,18 @@ const Album = require('../models/Album'); const FavoriteTrack = require('../models/FavoriteTrack'); const Post = require('../models/Post'); -const config = require('../config/index'); +// Используем напрямую из переменных окружения, как в других скриптах +const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/nakama'; async function deleteAllMusic() { try { console.log('🔌 Подключение к MongoDB...'); - await mongoose.connect(config.mongoUri); - console.log('✅ Подключено к MongoDB\n'); + console.log(' URI:', mongoUri.replace(/\/\/.*@/, '//***@')); // Скрываем пароль + + await mongoose.connect(mongoUri); + console.log('✅ Подключено к MongoDB'); + console.log(' База данных:', mongoose.connection.db.databaseName); + console.log(''); // Подсчет перед удалением const tracksCount = await Track.countDocuments(); @@ -41,6 +46,7 @@ async function deleteAllMusic() { if (tracksCount === 0 && albumsCount === 0) { console.log('✅ В базе данных нет треков и альбомов для удаления'); await mongoose.disconnect(); + process.exit(0); return; } @@ -53,21 +59,54 @@ async function deleteAllMusic() { // 2. Обнулить attachedTrack во всех постах console.log('\n2️⃣ Обнуление прикрепленных треков в постах...'); - const postsResult = await Post.updateMany( - { attachedTrack: { $ne: null } }, - { $set: { attachedTrack: null } } - ); - console.log(` ✅ Обновлено постов: ${postsResult.modifiedCount}`); + const postsWithTracksBefore = await Post.countDocuments({ attachedTrack: { $ne: null } }); + console.log(` Найдено постов с прикрепленными треками: ${postsWithTracksBefore}`); + + if (postsWithTracksBefore > 0) { + const postsResult = await Post.updateMany( + { attachedTrack: { $ne: null } }, + { $set: { attachedTrack: null } } + ); + console.log(` ✅ Обновлено постов: ${postsResult.modifiedCount}`); + } else { + console.log(' ℹ️ Посты с прикрепленными треками не найдены'); + } // 3. Удалить все треки console.log('\n3️⃣ Удаление треков...'); - const tracksResult = await Track.deleteMany({}); - console.log(` ✅ Удалено треков: ${tracksResult.deletedCount}`); + + // Проверить сколько треков найдено + const tracksBeforeDelete = await Track.countDocuments(); + console.log(` Найдено треков для удаления: ${tracksBeforeDelete}`); + + if (tracksBeforeDelete > 0) { + const tracksResult = await Track.deleteMany({}); + console.log(` ✅ Удалено треков: ${tracksResult.deletedCount}`); + + if (tracksResult.deletedCount !== tracksBeforeDelete) { + console.warn(` ⚠️ Предупреждение: количество удаленных (${tracksResult.deletedCount}) не совпадает с найденными (${tracksBeforeDelete})`); + } + } else { + console.log(' ℹ️ Треки не найдены'); + } // 4. Удалить все альбомы console.log('\n4️⃣ Удаление альбомов...'); - const albumsResult = await Album.deleteMany({}); - console.log(` ✅ Удалено альбомов: ${albumsResult.deletedCount}`); + + // Проверить сколько альбомов найдено + const albumsBeforeDelete = await Album.countDocuments(); + console.log(` Найдено альбомов для удаления: ${albumsBeforeDelete}`); + + if (albumsBeforeDelete > 0) { + const albumsResult = await Album.deleteMany({}); + console.log(` ✅ Удалено альбомов: ${albumsResult.deletedCount}`); + + if (albumsResult.deletedCount !== albumsBeforeDelete) { + console.warn(` ⚠️ Предупреждение: количество удаленных (${albumsResult.deletedCount}) не совпадает с найденными (${albumsBeforeDelete})`); + } + } else { + console.log(' ℹ️ Альбомы не найдены'); + } // Финальная статистика console.log('\n📊 Финальная статистика:'); @@ -84,11 +123,18 @@ async function deleteAllMusic() { console.log('\n✅ Все альбомы и треки успешно удалены!'); } catch (error) { - console.error('❌ Ошибка при удалении:', error); + console.error('\n❌ Ошибка при удалении:'); + console.error(' Сообщение:', error.message); + console.error(' Стек:', error.stack); + if (error.name === 'MongoServerError') { + console.error(' Код ошибки:', error.code); + } process.exit(1); } finally { - await mongoose.disconnect(); - console.log('\n🔌 Отключено от MongoDB'); + if (mongoose.connection.readyState === 1) { + await mongoose.disconnect(); + console.log('\n🔌 Отключено от MongoDB'); + } process.exit(0); } } diff --git a/backend/scripts/testDeleteMusic.js b/backend/scripts/testDeleteMusic.js new file mode 100644 index 0000000..669599d --- /dev/null +++ b/backend/scripts/testDeleteMusic.js @@ -0,0 +1,60 @@ +/** + * Тестовый скрипт для проверки подключения и подсчета записей + * Использование: node backend/scripts/testDeleteMusic.js + */ + +require('dotenv').config({ path: require('path').join(__dirname, '../.env') }); +const mongoose = require('mongoose'); + +const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/nakama'; + +async function testConnection() { + try { + console.log('🔌 Подключение к MongoDB...'); + console.log(' URI:', mongoUri.replace(/\/\/.*@/, '//***@')); + + await mongoose.connect(mongoUri); + console.log('✅ Подключено к MongoDB'); + console.log(' База данных:', mongoose.connection.db.databaseName); + console.log(''); + + // Загрузить модели + const Track = require('../models/Track'); + const Album = require('../models/Album'); + const FavoriteTrack = require('../models/FavoriteTrack'); + const Post = require('../models/Post'); + + // Подсчет + const tracksCount = await Track.countDocuments(); + const albumsCount = await Album.countDocuments(); + const favoritesCount = await FavoriteTrack.countDocuments(); + const postsWithTracksCount = await Post.countDocuments({ attachedTrack: { $ne: null } }); + + console.log('📊 Статистика:'); + console.log(` - Треков: ${tracksCount}`); + console.log(` - Альбомов: ${albumsCount}`); + console.log(` - Избранных треков: ${favoritesCount}`); + console.log(` - Постов с прикрепленными треками: ${postsWithTracksCount}`); + + // Показать несколько примеров треков + if (tracksCount > 0) { + console.log('\n📝 Примеры треков (первые 5):'); + const sampleTracks = await Track.find().limit(5).select('title artist fileUrl').populate('artist', 'name'); + sampleTracks.forEach((track, index) => { + console.log(` ${index + 1}. "${track.title}" - ${track.artist?.name || 'Unknown'} (${track.fileUrl})`); + }); + } + + await mongoose.disconnect(); + console.log('\n✅ Тест завершен'); + process.exit(0); + } catch (error) { + console.error('\n❌ Ошибка:'); + console.error(' Сообщение:', error.message); + console.error(' Стек:', error.stack); + process.exit(1); + } +} + +testConnection(); + diff --git a/frontend/src/contexts/MusicPlayerContext.jsx b/frontend/src/contexts/MusicPlayerContext.jsx index e9ba1a5..ebf5438 100644 --- a/frontend/src/contexts/MusicPlayerContext.jsx +++ b/frontend/src/contexts/MusicPlayerContext.jsx @@ -104,20 +104,75 @@ export const MusicPlayerProvider = ({ children }) => { // Загрузить трек if (audioRef.current) { - // Сформировать полный URL для трека + // fileUrl уже должен быть полным URL (MinIO или локальный) let audioUrl = track.fileUrl - // Если относительный URL, добавить базовый URL API - if (audioUrl && audioUrl.startsWith('/')) { + if (!audioUrl) { + throw new Error('URL трека не указан') + } + + // Если относительный URL (локальное хранилище), добавить базовый URL + if (audioUrl.startsWith('/uploads/')) { const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:3000/api' audioUrl = apiUrl.replace('/api', '') + audioUrl } + // Убедиться что URL валидный + if (!audioUrl.startsWith('http') && !audioUrl.startsWith('/')) { + console.error('Неверный URL трека:', audioUrl) + throw new Error('Неверный формат URL трека') + } + + // Очистить предыдущие обработчики + const oldHandlers = { + error: audioRef.current.onerror, + canplaythrough: audioRef.current.oncanplaythrough + } + + // Установить источник audioRef.current.src = audioUrl audioRef.current.volume = volume - audioRef.current.load() // Перезагрузить источник - await audioRef.current.play() - setIsPlaying(true) + + // Загрузить источник и дождаться готовности + audioRef.current.load() + + try { + // Попытка воспроизведения (может потребоваться взаимодействие пользователя) + await audioRef.current.play() + setIsPlaying(true) + } catch (playError) { + // Если воспроизведение не удалось, попробуем дождаться загрузки + console.warn('Ошибка воспроизведения, ожидание загрузки:', playError) + + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error('Таймаут загрузки аудио файла')) + }, 10000) // 10 секунд + + audioRef.current.oncanplaythrough = () => { + clearTimeout(timeout) + resolve() + } + + audioRef.current.onerror = (e) => { + clearTimeout(timeout) + const errorMsg = audioRef.current?.error + ? `Ошибка загрузки (код ${audioRef.current.error.code}): ${audioRef.current.error.message}` + : 'Ошибка загрузки аудио файла' + console.error('Ошибка загрузки аудио:', errorMsg, audioUrl) + reject(new Error(errorMsg)) + } + }) + + // Повторная попытка воспроизведения + try { + await audioRef.current.play() + setIsPlaying(true) + } catch (retryError) { + console.error('Не удалось воспроизвести трек после загрузки:', retryError) + throw retryError + } + } } // Записать прослушивание diff --git a/frontend/src/pages/Media.jsx b/frontend/src/pages/Media.jsx index 31a1c6d..edac762 100644 --- a/frontend/src/pages/Media.jsx +++ b/frontend/src/pages/Media.jsx @@ -6,7 +6,7 @@ import './Media.css' // Иконка лисы из Icons8 const FoxIcon = ({ size = 48 }) => (