import express from 'express'; import multer from 'multer'; import { S3Service } from '../services/s3Service.js'; import { AudioMetadataService } from '../services/audioMetadataService.js'; import { MusicFile } from '../models/MusicFile.js'; import { Song } from '../models/Song.js'; const router = express.Router(); // Configure multer for memory storage const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 100 * 1024 * 1024, // 100MB limit }, fileFilter: (req, file, cb) => { const audioMetadataService = new AudioMetadataService(); if (audioMetadataService.isAudioFile(file.originalname)) { cb(null, true); } else { cb(new Error('Only audio files are allowed')); } }, }); // Initialize services const s3Service = new S3Service({ endpoint: process.env.S3_ENDPOINT || 'http://localhost:9000', accessKeyId: process.env.S3_ACCESS_KEY_ID || 'minioadmin', secretAccessKey: process.env.S3_SECRET_ACCESS_KEY || 'minioadmin', bucketName: process.env.S3_BUCKET_NAME || 'music-files', region: process.env.S3_REGION || 'us-east-1', }); const audioMetadataService = new AudioMetadataService(); /** * Upload a single music file */ router.post('/upload', upload.single('file'), async (req, res) => { try { if (!req.file) { return res.status(400).json({ error: 'No file uploaded' }); } const { buffer, originalname, mimetype } = req.file; // Upload to S3 const uploadResult = await s3Service.uploadFile(buffer, originalname, mimetype); // Extract audio metadata const metadata = await audioMetadataService.extractMetadata(buffer, originalname); // Save to database const musicFile = new MusicFile({ originalName: originalname, s3Key: uploadResult.key, s3Url: uploadResult.url, contentType: mimetype, size: uploadResult.size, ...metadata, }); await musicFile.save(); res.json({ message: 'File uploaded successfully', musicFile, }); } catch (error) { console.error('Upload error:', error); res.status(500).json({ error: 'Failed to upload file' }); } }); /** * Upload multiple music files */ router.post('/batch-upload', upload.array('files', 10), async (req, res) => { try { if (!req.files || req.files.length === 0) { return res.status(400).json({ error: 'No files uploaded' }); } const results = []; for (const file of req.files) { try { const { buffer, originalname, mimetype } = file; // Upload to S3 const uploadResult = await s3Service.uploadFile(buffer, originalname, mimetype); // Extract audio metadata const metadata = await audioMetadataService.extractMetadata(buffer, originalname); // Save to database const musicFile = new MusicFile({ originalName: originalname, s3Key: uploadResult.key, s3Url: uploadResult.url, contentType: mimetype, size: uploadResult.size, ...metadata, }); await musicFile.save(); results.push({ success: true, musicFile }); } catch (error) { console.error(`Error uploading ${file.originalname}:`, error); results.push({ success: false, fileName: file.originalname, error: error.message }); } } res.json({ message: 'Batch upload completed', results, }); } catch (error) { console.error('Batch upload error:', error); res.status(500).json({ error: 'Failed to upload files' }); } }); /** * Get all music files (from database) */ router.get('/files', async (req, res) => { try { const musicFiles = await MusicFile.find({}).sort({ uploadedAt: -1 }); res.json({ musicFiles }); } catch (error) { console.error('Error fetching music files:', error); res.status(500).json({ error: 'Failed to fetch music files' }); } }); /** * Sync S3 files with database - recursively list all files in S3 and sync */ router.post('/sync-s3', async (req, res) => { try { console.log('Starting S3 sync...'); // Get all files from S3 recursively const s3Files = await s3Service.listAllFiles(); console.log(`Found ${s3Files.length} files in S3 bucket`); const results = { total: s3Files.length, synced: 0, skipped: 0, errors: 0, newFiles: 0 }; for (const s3File of s3Files) { try { // Check if file already exists in database const existingFile = await MusicFile.findOne({ s3Key: s3File.key }); if (existingFile) { results.skipped++; continue; } // Extract filename from S3 key const filename = s3File.key.split('/').pop() || s3File.key; // Check if it's an audio file if (!audioMetadataService.isAudioFile(filename)) { results.skipped++; continue; } // Get file content to extract metadata try { const fileBuffer = await s3Service.getFileContent(s3File.key); const metadata = await audioMetadataService.extractMetadata(fileBuffer, filename); // Save to database const musicFile = new MusicFile({ originalName: filename, s3Key: s3File.key, s3Url: `${process.env.S3_ENDPOINT}/${process.env.S3_BUCKET_NAME}/${s3File.key}`, contentType: 'audio/mpeg', // Default, will be updated by metadata size: s3File.size, ...metadata, }); await musicFile.save(); results.synced++; results.newFiles++; } catch (metadataError) { console.error(`Error extracting metadata for ${s3File.key}:`, metadataError); // Still save the file without metadata const musicFile = new MusicFile({ originalName: filename, s3Key: s3File.key, s3Url: `${process.env.S3_ENDPOINT}/${process.env.S3_BUCKET_NAME}/${s3File.key}`, contentType: 'audio/mpeg', size: s3File.size, }); await musicFile.save(); results.synced++; results.newFiles++; } } catch (error) { console.error(`Error processing ${s3File.key}:`, error); results.errors++; } } console.log('S3 sync completed:', results); res.json({ message: 'S3 sync completed', results }); } catch (error) { console.error('S3 sync error:', error); res.status(500).json({ error: 'Failed to sync S3 files' }); } }); /** * Get streaming URL for a music file */ router.get('/:id/stream', async (req, res) => { try { const musicFile = await MusicFile.findById(req.params.id); if (!musicFile) { return res.status(404).json({ error: 'Music file not found' }); } // Use presigned URL for secure access instead of direct URL const presignedUrl = await s3Service.getPresignedUrl(musicFile.s3Key, 3600); // 1 hour expiry res.json({ streamingUrl: presignedUrl, musicFile, }); } catch (error) { console.error('Streaming error:', error); res.status(500).json({ error: 'Failed to get streaming URL' }); } }); /** * Get presigned URL for secure access */ router.get('/:id/presigned', async (req, res) => { try { const musicFile = await MusicFile.findById(req.params.id); if (!musicFile) { return res.status(404).json({ error: 'Music file not found' }); } const expiresIn = parseInt(req.query.expiresIn as string) || 3600; const presignedUrl = await s3Service.getPresignedUrl(musicFile.s3Key, expiresIn); res.json({ presignedUrl, expiresIn, }); } catch (error) { console.error('Presigned URL error:', error); res.status(500).json({ error: 'Failed to generate presigned URL' }); } }); /** * Get music file metadata */ router.get('/:id/metadata', async (req, res) => { try { const musicFile = await MusicFile.findById(req.params.id); if (!musicFile) { return res.status(404).json({ error: 'Music file not found' }); } res.json(musicFile); } catch (error) { console.error('Metadata error:', error); res.status(500).json({ error: 'Failed to get metadata' }); } }); /** * List all music files with pagination and search */ router.get('/', async (req, res) => { try { const page = parseInt(req.query.page as string) || 1; const limit = parseInt(req.query.limit as string) || 20; const search = req.query.search as string; const artist = req.query.artist as string; const album = req.query.album as string; const genre = req.query.genre as string; const query: any = {}; // Build search query if (search) { query.$text = { $search: search }; } if (artist) { query.artist = { $regex: artist, $options: 'i' }; } if (album) { query.album = { $regex: album, $options: 'i' }; } if (genre) { query.genre = { $regex: genre, $options: 'i' }; } const skip = (page - 1) * limit; const [musicFiles, total] = await Promise.all([ MusicFile.find(query) .sort({ uploadedAt: -1 }) .skip(skip) .limit(limit) .populate('songId'), MusicFile.countDocuments(query), ]); res.json({ musicFiles, pagination: { page, limit, total, pages: Math.ceil(total / limit), }, }); } catch (error) { console.error('List error:', error); res.status(500).json({ error: 'Failed to list music files' }); } }); /** * Delete a music file */ router.delete('/:id', async (req, res) => { try { const musicFile = await MusicFile.findById(req.params.id); if (!musicFile) { return res.status(404).json({ error: 'Music file not found' }); } // Delete from S3 await s3Service.deleteFile(musicFile.s3Key); // Delete from database await MusicFile.findByIdAndDelete(req.params.id); res.json({ message: 'Music file deleted successfully' }); } catch (error) { console.error('Delete error:', error); res.status(500).json({ error: 'Failed to delete music file' }); } }); /** * Link music file to existing song */ router.post('/:id/link-song/:songId', async (req, res) => { try { const { id, songId } = req.params; const [musicFile, song] = await Promise.all([ MusicFile.findById(id), Song.findById(songId), ]); if (!musicFile) { return res.status(404).json({ error: 'Music file not found' }); } if (!song) { return res.status(404).json({ error: 'Song not found' }); } musicFile.songId = song._id; await musicFile.save(); res.json({ message: 'Music file linked to song successfully', musicFile, }); } catch (error) { console.error('Link error:', error); res.status(500).json({ error: 'Failed to link music file to song' }); } }); export { router as musicRouter };