Update files
This commit is contained in:
parent
56538d098f
commit
9d90c4c1f0
|
|
@ -12,6 +12,18 @@
|
||||||
|
|
||||||
## Использование
|
## Использование
|
||||||
|
|
||||||
|
### Шаг 1: Проверка подключения и данных
|
||||||
|
|
||||||
|
Сначала проверьте, что скрипт может подключиться к базе и видит данные:
|
||||||
|
|
||||||
|
```cmd
|
||||||
|
node backend/scripts/testDeleteMusic.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Этот скрипт покажет статистику без удаления данных.
|
||||||
|
|
||||||
|
### Шаг 2: Удаление данных
|
||||||
|
|
||||||
### Для Windows (CMD/PowerShell):
|
### Для Windows (CMD/PowerShell):
|
||||||
|
|
||||||
```cmd
|
```cmd
|
||||||
|
|
@ -32,6 +44,8 @@ node scripts/deleteAllMusic.js
|
||||||
node backend/scripts/deleteAllMusic.js
|
node backend/scripts/deleteAllMusic.js
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Примечание:** Скрипт покажет предупреждение и подождет 3 секунды перед удалением. Нажмите Ctrl+C для отмены, если передумали.
|
||||||
|
|
||||||
## Что делает скрипт
|
## Что делает скрипт
|
||||||
|
|
||||||
1. **Подключается к MongoDB** используя настройки из `.env`
|
1. **Подключается к MongoDB** используя настройки из `.env`
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
module.exports = {
|
||||||
createUploadMiddleware,
|
createUploadMiddleware,
|
||||||
cleanupOnError,
|
cleanupOnError,
|
||||||
|
createMusicUploadMiddleware,
|
||||||
|
|
||||||
// Готовые middleware для разных случаев
|
// Готовые middleware для разных случаев
|
||||||
uploadPostImages: createUploadMiddleware('images', 5, 'posts'),
|
uploadPostImages: createUploadMiddleware('images', 5, 'posts'),
|
||||||
uploadAvatar: createUploadMiddleware('avatar', 1, 'avatars'),
|
uploadAvatar: createUploadMiddleware('avatar', 1, 'avatars'),
|
||||||
uploadChannelMedia: createUploadMiddleware('images', 10, 'channel')
|
uploadChannelMedia: createUploadMiddleware('images', 10, 'channel'),
|
||||||
|
uploadMusicTrack: createMusicUploadMiddleware('track'),
|
||||||
|
uploadMusicAlbum: createMusicUploadMiddleware('album')
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,65 +1,54 @@
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const multer = require('multer');
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs').promises;
|
const fs = require('fs').promises;
|
||||||
const AdmZip = require('adm-zip');
|
const AdmZip = require('adm-zip');
|
||||||
const mm = require('music-metadata');
|
const mm = require('music-metadata');
|
||||||
const { authenticate } = require('../middleware/auth');
|
const { authenticate } = require('../middleware/auth');
|
||||||
|
const { uploadMusicTrack, uploadMusicAlbum, cleanupOnError } = require('../middleware/upload');
|
||||||
|
const { uploadFile } = require('../utils/minio');
|
||||||
const Artist = require('../models/Artist');
|
const Artist = require('../models/Artist');
|
||||||
const Album = require('../models/Album');
|
const Album = require('../models/Album');
|
||||||
const Track = require('../models/Track');
|
const Track = require('../models/Track');
|
||||||
const FavoriteTrack = require('../models/FavoriteTrack');
|
const FavoriteTrack = require('../models/FavoriteTrack');
|
||||||
|
|
||||||
// Настройка multer для загрузки файлов
|
// Извлечь метаданные из аудио файла (из buffer или пути)
|
||||||
const storage = multer.diskStorage({
|
async function extractAudioMetadata(audioData, isBuffer = true) {
|
||||||
destination: async (req, file, cb) => {
|
|
||||||
const uploadDir = path.join(__dirname, '../uploads/music');
|
|
||||||
try {
|
try {
|
||||||
await fs.mkdir(uploadDir, { recursive: true });
|
const metadata = isBuffer
|
||||||
cb(null, uploadDir);
|
? await mm.parseBuffer(audioData, audioData.mimetype || 'audio/mpeg')
|
||||||
} catch (error) {
|
: await mm.parseFile(audioData);
|
||||||
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) {
|
|
||||||
try {
|
|
||||||
const metadata = await mm.parseFile(filePath);
|
|
||||||
|
|
||||||
// Извлечь обложку если есть
|
// Извлечь обложку если есть
|
||||||
let coverImagePath = null;
|
let coverImageUrl = null;
|
||||||
if (metadata.common.picture && metadata.common.picture.length > 0) {
|
if (metadata.common.picture && metadata.common.picture.length > 0) {
|
||||||
const picture = metadata.common.picture[0];
|
const picture = metadata.common.picture[0];
|
||||||
const coverFileName = `cover-${Date.now()}.${picture.format.split('/')[1] || 'jpg'}`;
|
const coverExt = picture.format.split('/')[1] || 'jpg';
|
||||||
coverImagePath = path.join(__dirname, '../uploads/music', coverFileName);
|
const coverFileName = `cover-${Date.now()}.${coverExt}`;
|
||||||
await fs.writeFile(coverImagePath, picture.data);
|
|
||||||
coverImagePath = `/uploads/music/${coverFileName}`;
|
// Загрузить обложку в 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 {
|
return {
|
||||||
|
|
@ -70,7 +59,7 @@ async function extractAudioMetadata(filePath) {
|
||||||
genre: metadata.common.genre?.[0] || null,
|
genre: metadata.common.genre?.[0] || null,
|
||||||
trackNumber: metadata.common.track?.no || null,
|
trackNumber: metadata.common.track?.no || null,
|
||||||
duration: metadata.format.duration || 0,
|
duration: metadata.format.duration || 0,
|
||||||
coverImage: coverImagePath
|
coverImage: coverImageUrl
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка извлечения метаданных:', 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 {
|
try {
|
||||||
if (!req.file) {
|
if (!req.uploadedMusicFile) {
|
||||||
return res.status(400).json({ error: 'Файл не загружен' });
|
return res.status(400).json({ error: 'Файл не загружен' });
|
||||||
}
|
}
|
||||||
|
|
||||||
let { title, artistName, albumTitle, trackNumber, year, genre } = req.body;
|
let { title, artistName, albumTitle, trackNumber, year, genre } = req.body;
|
||||||
|
|
||||||
// Извлечь метаданные из файла
|
// Извлечь метаданные из файла (используем buffer)
|
||||||
const metadata = await extractAudioMetadata(req.file.path);
|
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';
|
artistName = artistName || metadata.artist || 'Unknown Artist';
|
||||||
albumTitle = albumTitle || metadata.album;
|
albumTitle = albumTitle || metadata.album;
|
||||||
trackNumber = trackNumber || metadata.trackNumber;
|
trackNumber = trackNumber || metadata.trackNumber;
|
||||||
|
|
@ -144,8 +133,8 @@ router.post('/upload-track', authenticate, upload.single('track'), async (req, r
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Создать трек
|
// Создать трек (используем URL из MinIO или локальный путь)
|
||||||
const fileUrl = `/uploads/music/${req.file.filename}`;
|
const fileUrl = req.uploadedMusicFile.url;
|
||||||
|
|
||||||
const track = await Track.create({
|
const track = await Track.create({
|
||||||
title,
|
title,
|
||||||
|
|
@ -154,8 +143,8 @@ router.post('/upload-track', authenticate, upload.single('track'), async (req, r
|
||||||
fileUrl,
|
fileUrl,
|
||||||
coverImage: metadata.coverImage || (album ? album.coverImage : null),
|
coverImage: metadata.coverImage || (album ? album.coverImage : null),
|
||||||
file: {
|
file: {
|
||||||
size: req.file.size,
|
size: req.uploadedMusicFile.size,
|
||||||
mimeType: req.file.mimetype,
|
mimeType: req.uploadedMusicFile.mimetype,
|
||||||
duration: Math.round(metadata.duration)
|
duration: Math.round(metadata.duration)
|
||||||
},
|
},
|
||||||
trackNumber: trackNumber ? parseInt(trackNumber) : 0,
|
trackNumber: trackNumber ? parseInt(trackNumber) : 0,
|
||||||
|
|
@ -183,33 +172,26 @@ router.post('/upload-track', authenticate, upload.single('track'), async (req, r
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка загрузки трека:', error);
|
console.error('Ошибка загрузки трека:', error);
|
||||||
|
|
||||||
// Удалить файл в случае ошибки
|
|
||||||
if (req.file) {
|
|
||||||
await fs.unlink(req.file.path).catch(() => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(500).json({ error: 'Ошибка загрузки трека' });
|
res.status(500).json({ error: 'Ошибка загрузки трека' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Загрузка альбома из ZIP с извлечением метаданных
|
// Загрузка альбома из ZIP с извлечением метаданных
|
||||||
router.post('/upload-album', authenticate, upload.single('album'), async (req, res) => {
|
router.post('/upload-album', authenticate, uploadMusicAlbum, cleanupOnError(), async (req, res) => {
|
||||||
try {
|
try {
|
||||||
if (!req.file) {
|
if (!req.uploadedMusicFile) {
|
||||||
return res.status(400).json({ error: 'ZIP файл не загружен' });
|
return res.status(400).json({ error: 'ZIP файл не загружен' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const ext = path.extname(req.file.originalname).toLowerCase();
|
const ext = path.extname(req.uploadedMusicFile.originalname).toLowerCase();
|
||||||
if (ext !== '.zip' && req.file.mimetype !== 'application/zip' && req.file.mimetype !== 'application/x-zip-compressed') {
|
if (ext !== '.zip' && req.uploadedMusicFile.mimetype !== 'application/zip' && req.uploadedMusicFile.mimetype !== 'application/x-zip-compressed') {
|
||||||
await fs.unlink(req.file.path).catch(() => {});
|
|
||||||
return res.status(400).json({ error: 'Требуется ZIP файл' });
|
return res.status(400).json({ error: 'Требуется ZIP файл' });
|
||||||
}
|
}
|
||||||
|
|
||||||
let { artistName, albumTitle, year, genre } = req.body;
|
let { artistName, albumTitle, year, genre } = req.body;
|
||||||
|
|
||||||
// Распаковать ZIP
|
// Распаковать ZIP из buffer
|
||||||
const zip = new AdmZip(req.file.path);
|
const zip = new AdmZip(req.uploadedMusicFile.buffer);
|
||||||
const zipEntries = zip.getEntries();
|
const zipEntries = zip.getEntries();
|
||||||
|
|
||||||
// Фильтровать аудио файлы
|
// Фильтровать аудио файлы
|
||||||
|
|
@ -220,33 +202,24 @@ router.post('/upload-album', authenticate, upload.single('album'), async (req, r
|
||||||
});
|
});
|
||||||
|
|
||||||
if (audioFiles.length === 0) {
|
if (audioFiles.length === 0) {
|
||||||
await fs.unlink(req.file.path).catch(() => {});
|
|
||||||
return res.status(400).json({ error: 'В архиве нет аудио файлов' });
|
return res.status(400).json({ error: 'В архиве нет аудио файлов' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const uploadDir = path.join(__dirname, '../uploads/music');
|
|
||||||
|
|
||||||
// Извлечь первый трек для получения метаданных альбома
|
// Извлечь первый трек для получения метаданных альбома
|
||||||
const firstEntry = audioFiles[0];
|
const firstEntry = audioFiles[0];
|
||||||
const firstFileName = path.basename(firstEntry.entryName);
|
const firstBuffer = zip.readFile(firstEntry);
|
||||||
const firstTempName = `temp-${Date.now()}${path.extname(firstFileName)}`;
|
|
||||||
const firstTempPath = path.join(uploadDir, firstTempName);
|
|
||||||
|
|
||||||
zip.extractEntryTo(firstEntry, uploadDir, false, true, false, firstTempName);
|
// Извлечь метаданные из первого трека (из buffer)
|
||||||
|
const firstMetadata = await extractAudioMetadata(firstBuffer, true);
|
||||||
// Извлечь метаданные из первого трека
|
|
||||||
const firstMetadata = await extractAudioMetadata(firstTempPath);
|
|
||||||
|
|
||||||
// Использовать метаданные для альбома если не указаны
|
// Использовать метаданные для альбома если не указаны
|
||||||
artistName = artistName || firstMetadata.artist || 'Unknown Artist';
|
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;
|
year = year || firstMetadata.year;
|
||||||
genre = genre || firstMetadata.genre;
|
genre = genre || firstMetadata.genre;
|
||||||
|
|
||||||
const albumCoverImage = firstMetadata.coverImage;
|
const albumCoverImage = firstMetadata.coverImage;
|
||||||
|
|
||||||
// Удалить временный файл
|
|
||||||
await fs.unlink(firstTempPath).catch(() => {});
|
|
||||||
|
|
||||||
// Найти или создать исполнителя
|
// Найти или создать исполнителя
|
||||||
const artist = await findOrCreateArtist(artistName, req.user._id);
|
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++) {
|
for (let i = 0; i < audioFiles.length; i++) {
|
||||||
const entry = audioFiles[i];
|
const entry = audioFiles[i];
|
||||||
const fileName = path.basename(entry.entryName);
|
const fileName = path.basename(entry.entryName);
|
||||||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
|
||||||
const ext = path.extname(fileName);
|
const ext = path.extname(fileName);
|
||||||
const newFileName = `${uniqueSuffix}${ext}`;
|
|
||||||
const filePath = path.join(uploadDir, newFileName);
|
|
||||||
|
|
||||||
// Извлечь файл
|
// Извлечь файл из ZIP в buffer
|
||||||
zip.extractEntryTo(entry, uploadDir, false, true, false, newFileName);
|
const trackBuffer = zip.readFile(entry);
|
||||||
|
|
||||||
// Извлечь метаданные из трека
|
// Извлечь метаданные из трека (из buffer)
|
||||||
const trackMetadata = await extractAudioMetadata(filePath);
|
const trackMetadata = await extractAudioMetadata(trackBuffer, true);
|
||||||
|
|
||||||
// Получить размер файла
|
|
||||||
const stats = await fs.stat(filePath);
|
|
||||||
|
|
||||||
// Определить название трека
|
// Определить название трека
|
||||||
const trackTitle = trackMetadata.title || fileName.replace(ext, '');
|
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;
|
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({
|
const track = await Track.create({
|
||||||
title: trackTitle,
|
title: trackTitle,
|
||||||
artist: trackArtistId,
|
artist: trackArtistId,
|
||||||
album: album._id,
|
album: album._id,
|
||||||
fileUrl: `/uploads/music/${newFileName}`,
|
fileUrl: trackFileUrl,
|
||||||
coverImage: trackMetadata.coverImage || albumCoverImage,
|
coverImage: trackMetadata.coverImage || albumCoverImage,
|
||||||
file: {
|
file: {
|
||||||
size: stats.size,
|
size: trackBuffer.length,
|
||||||
mimeType: req.file.mimetype,
|
mimeType: mimeType,
|
||||||
duration: Math.round(trackMetadata.duration)
|
duration: Math.round(trackMetadata.duration)
|
||||||
},
|
},
|
||||||
trackNumber: trackMetadata.trackNumber || (i + 1),
|
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);
|
totalDuration += Math.round(trackMetadata.duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Удалить ZIP после обработки
|
|
||||||
await fs.unlink(req.file.path).catch(() => {});
|
|
||||||
|
|
||||||
// Обновить статистику
|
// Обновить статистику
|
||||||
artist.stats.tracks += tracks.length;
|
artist.stats.tracks += tracks.length;
|
||||||
artist.stats.albums += 1;
|
artist.stats.albums += 1;
|
||||||
|
|
@ -334,16 +331,11 @@ router.post('/upload-album', authenticate, upload.single('album'), async (req, r
|
||||||
success: true,
|
success: true,
|
||||||
album,
|
album,
|
||||||
tracks,
|
tracks,
|
||||||
message: `Загружено ${tracks.length} треков из альбома "${albumTitle}"`
|
message: `Альбом "${album.title}" успешно загружен. Добавлено треков: ${tracks.length}`
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка загрузки альбома:', error);
|
console.error('Ошибка загрузки альбома:', error);
|
||||||
|
res.status(500).json({ error: 'Ошибка загрузки альбома' });
|
||||||
if (req.file) {
|
|
||||||
await fs.unlink(req.file.path).catch(() => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(500).json({ error: 'Ошибка загрузки альбома: ' + error.message });
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,13 +18,18 @@ const Album = require('../models/Album');
|
||||||
const FavoriteTrack = require('../models/FavoriteTrack');
|
const FavoriteTrack = require('../models/FavoriteTrack');
|
||||||
const Post = require('../models/Post');
|
const Post = require('../models/Post');
|
||||||
|
|
||||||
const config = require('../config/index');
|
// Используем напрямую из переменных окружения, как в других скриптах
|
||||||
|
const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/nakama';
|
||||||
|
|
||||||
async function deleteAllMusic() {
|
async function deleteAllMusic() {
|
||||||
try {
|
try {
|
||||||
console.log('🔌 Подключение к MongoDB...');
|
console.log('🔌 Подключение к MongoDB...');
|
||||||
await mongoose.connect(config.mongoUri);
|
console.log(' URI:', mongoUri.replace(/\/\/.*@/, '//***@')); // Скрываем пароль
|
||||||
console.log('✅ Подключено к MongoDB\n');
|
|
||||||
|
await mongoose.connect(mongoUri);
|
||||||
|
console.log('✅ Подключено к MongoDB');
|
||||||
|
console.log(' База данных:', mongoose.connection.db.databaseName);
|
||||||
|
console.log('');
|
||||||
|
|
||||||
// Подсчет перед удалением
|
// Подсчет перед удалением
|
||||||
const tracksCount = await Track.countDocuments();
|
const tracksCount = await Track.countDocuments();
|
||||||
|
|
@ -41,6 +46,7 @@ async function deleteAllMusic() {
|
||||||
if (tracksCount === 0 && albumsCount === 0) {
|
if (tracksCount === 0 && albumsCount === 0) {
|
||||||
console.log('✅ В базе данных нет треков и альбомов для удаления');
|
console.log('✅ В базе данных нет треков и альбомов для удаления');
|
||||||
await mongoose.disconnect();
|
await mongoose.disconnect();
|
||||||
|
process.exit(0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,22 +59,55 @@ async function deleteAllMusic() {
|
||||||
|
|
||||||
// 2. Обнулить attachedTrack во всех постах
|
// 2. Обнулить attachedTrack во всех постах
|
||||||
console.log('\n2️⃣ Обнуление прикрепленных треков в постах...');
|
console.log('\n2️⃣ Обнуление прикрепленных треков в постах...');
|
||||||
|
const postsWithTracksBefore = await Post.countDocuments({ attachedTrack: { $ne: null } });
|
||||||
|
console.log(` Найдено постов с прикрепленными треками: ${postsWithTracksBefore}`);
|
||||||
|
|
||||||
|
if (postsWithTracksBefore > 0) {
|
||||||
const postsResult = await Post.updateMany(
|
const postsResult = await Post.updateMany(
|
||||||
{ attachedTrack: { $ne: null } },
|
{ attachedTrack: { $ne: null } },
|
||||||
{ $set: { attachedTrack: null } }
|
{ $set: { attachedTrack: null } }
|
||||||
);
|
);
|
||||||
console.log(` ✅ Обновлено постов: ${postsResult.modifiedCount}`);
|
console.log(` ✅ Обновлено постов: ${postsResult.modifiedCount}`);
|
||||||
|
} else {
|
||||||
|
console.log(' ℹ️ Посты с прикрепленными треками не найдены');
|
||||||
|
}
|
||||||
|
|
||||||
// 3. Удалить все треки
|
// 3. Удалить все треки
|
||||||
console.log('\n3️⃣ Удаление треков...');
|
console.log('\n3️⃣ Удаление треков...');
|
||||||
|
|
||||||
|
// Проверить сколько треков найдено
|
||||||
|
const tracksBeforeDelete = await Track.countDocuments();
|
||||||
|
console.log(` Найдено треков для удаления: ${tracksBeforeDelete}`);
|
||||||
|
|
||||||
|
if (tracksBeforeDelete > 0) {
|
||||||
const tracksResult = await Track.deleteMany({});
|
const tracksResult = await Track.deleteMany({});
|
||||||
console.log(` ✅ Удалено треков: ${tracksResult.deletedCount}`);
|
console.log(` ✅ Удалено треков: ${tracksResult.deletedCount}`);
|
||||||
|
|
||||||
|
if (tracksResult.deletedCount !== tracksBeforeDelete) {
|
||||||
|
console.warn(` ⚠️ Предупреждение: количество удаленных (${tracksResult.deletedCount}) не совпадает с найденными (${tracksBeforeDelete})`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(' ℹ️ Треки не найдены');
|
||||||
|
}
|
||||||
|
|
||||||
// 4. Удалить все альбомы
|
// 4. Удалить все альбомы
|
||||||
console.log('\n4️⃣ Удаление альбомов...');
|
console.log('\n4️⃣ Удаление альбомов...');
|
||||||
|
|
||||||
|
// Проверить сколько альбомов найдено
|
||||||
|
const albumsBeforeDelete = await Album.countDocuments();
|
||||||
|
console.log(` Найдено альбомов для удаления: ${albumsBeforeDelete}`);
|
||||||
|
|
||||||
|
if (albumsBeforeDelete > 0) {
|
||||||
const albumsResult = await Album.deleteMany({});
|
const albumsResult = await Album.deleteMany({});
|
||||||
console.log(` ✅ Удалено альбомов: ${albumsResult.deletedCount}`);
|
console.log(` ✅ Удалено альбомов: ${albumsResult.deletedCount}`);
|
||||||
|
|
||||||
|
if (albumsResult.deletedCount !== albumsBeforeDelete) {
|
||||||
|
console.warn(` ⚠️ Предупреждение: количество удаленных (${albumsResult.deletedCount}) не совпадает с найденными (${albumsBeforeDelete})`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(' ℹ️ Альбомы не найдены');
|
||||||
|
}
|
||||||
|
|
||||||
// Финальная статистика
|
// Финальная статистика
|
||||||
console.log('\n📊 Финальная статистика:');
|
console.log('\n📊 Финальная статистика:');
|
||||||
const finalTracksCount = await Track.countDocuments();
|
const finalTracksCount = await Track.countDocuments();
|
||||||
|
|
@ -84,11 +123,18 @@ async function deleteAllMusic() {
|
||||||
console.log('\n✅ Все альбомы и треки успешно удалены!');
|
console.log('\n✅ Все альбомы и треки успешно удалены!');
|
||||||
|
|
||||||
} catch (error) {
|
} 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);
|
process.exit(1);
|
||||||
} finally {
|
} finally {
|
||||||
|
if (mongoose.connection.readyState === 1) {
|
||||||
await mongoose.disconnect();
|
await mongoose.disconnect();
|
||||||
console.log('\n🔌 Отключено от MongoDB');
|
console.log('\n🔌 Отключено от MongoDB');
|
||||||
|
}
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
||||||
|
|
@ -104,20 +104,75 @@ export const MusicPlayerProvider = ({ children }) => {
|
||||||
|
|
||||||
// Загрузить трек
|
// Загрузить трек
|
||||||
if (audioRef.current) {
|
if (audioRef.current) {
|
||||||
// Сформировать полный URL для трека
|
// fileUrl уже должен быть полным URL (MinIO или локальный)
|
||||||
let audioUrl = track.fileUrl
|
let audioUrl = track.fileUrl
|
||||||
|
|
||||||
// Если относительный URL, добавить базовый URL API
|
if (!audioUrl) {
|
||||||
if (audioUrl && audioUrl.startsWith('/')) {
|
throw new Error('URL трека не указан')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если относительный URL (локальное хранилище), добавить базовый URL
|
||||||
|
if (audioUrl.startsWith('/uploads/')) {
|
||||||
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:3000/api'
|
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:3000/api'
|
||||||
audioUrl = apiUrl.replace('/api', '') + audioUrl
|
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.src = audioUrl
|
||||||
audioRef.current.volume = volume
|
audioRef.current.volume = volume
|
||||||
audioRef.current.load() // Перезагрузить источник
|
|
||||||
|
// Загрузить источник и дождаться готовности
|
||||||
|
audioRef.current.load()
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Попытка воспроизведения (может потребоваться взаимодействие пользователя)
|
||||||
await audioRef.current.play()
|
await audioRef.current.play()
|
||||||
setIsPlaying(true)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Записать прослушивание
|
// Записать прослушивание
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import './Media.css'
|
||||||
// Иконка лисы из Icons8
|
// Иконка лисы из Icons8
|
||||||
const FoxIcon = ({ size = 48 }) => (
|
const FoxIcon = ({ size = 48 }) => (
|
||||||
<img
|
<img
|
||||||
src="https://img.icons8.com/windows/32/fox.png"
|
src="https://img.icons8.com/softteal-color/96/fox.png"
|
||||||
alt="Fox"
|
alt="Fox"
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue