const express = require('express'); const router = express.Router(); const multer = require('multer'); const path = require('path'); const fs = require('fs'); const { authenticate } = require('../middleware/auth'); const { modifyLimiter, heavyLimiter, readLimiter } = require('../middleware/rateLimiter'); const { db } = require('../database/db'); const logger = require('../utils/logger'); const { parseM3U } = require('../utils/m3uParser'); const { validatePlaylist, validateIdParam, validateBulkDelete, createValidationMiddleware } = require('../middleware/inputValidation'); const { validatePlaylistName } = require('../utils/inputValidator'); const storage = multer.diskStorage({ destination: (req, file, cb) => { const uploadDir = path.join(__dirname, '../../data/uploads'); if (!fs.existsSync(uploadDir)) { fs.mkdirSync(uploadDir, { recursive: true, mode: 0o755 }); } cb(null, uploadDir); }, filename: (req, file, cb) => { const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); cb(null, 'playlist-' + uniqueSuffix + path.extname(file.originalname)); } }); const upload = multer({ storage, limits: { fileSize: 50 * 1024 * 1024 }, // 50MB fileFilter: (req, file, cb) => { if (file.mimetype === 'audio/x-mpegurl' || file.originalname.endsWith('.m3u') || file.originalname.endsWith('.m3u8')) { cb(null, true); } else { cb(new Error('Only M3U files are allowed')); } } }); // Get all playlists for user router.get('/', authenticate, readLimiter, (req, res) => { db.all( 'SELECT * FROM playlists WHERE user_id = ? ORDER BY created_at DESC', [req.user.userId], (err, playlists) => { if (err) { logger.error('Error fetching playlists:', err); return res.status(500).json({ error: 'Failed to fetch playlists' }); } res.json(playlists); } ); }); // Add playlist from URL router.post('/url', authenticate, modifyLimiter, validatePlaylist, async (req, res) => { const { name, url, username, password, category, type } = req.body; let playlistUrl = url; if (username && password) { playlistUrl = url.replace('username=', `username=${username}`).replace('password=', `password=${password}`); } db.run( 'INSERT INTO playlists (user_id, name, url, type, category) VALUES (?, ?, ?, ?, ?)', [req.user.userId, name, playlistUrl, type || 'live', category], async function(err) { if (err) { logger.error('Error adding playlist:', err); return res.status(500).json({ error: 'Failed to add playlist' }); } const playlistId = this.lastID; try { await parseM3U(playlistUrl, playlistId); res.status(201).json({ message: 'Playlist added successfully', id: playlistId }); } catch (error) { logger.error('Error parsing M3U:', error); res.status(500).json({ error: 'Failed to parse playlist' }); } } ); }); // Upload M3U file router.post('/upload', authenticate, heavyLimiter, upload.single('file'), async (req, res) => { if (!req.file) { return res.status(400).json({ error: 'No file uploaded' }); } const { name, category, type } = req.body; // Validate playlist name if provided if (name) { const validation = validatePlaylistName(name); if (!validation.valid) { // Clean up uploaded file if (fs.existsSync(req.file.path)) { fs.unlinkSync(req.file.path); } return res.status(400).json({ error: validation.errors.join(', ') }); } } const filename = req.file.filename; const filePath = req.file.path; db.run( 'INSERT INTO playlists (user_id, name, filename, type, category) VALUES (?, ?, ?, ?, ?)', [req.user.userId, name || req.file.originalname, filename, type || 'live', category], async function(err) { if (err) { logger.error('Error saving playlist:', err); return res.status(500).json({ error: 'Failed to save playlist' }); } const playlistId = this.lastID; try { await parseM3U(filePath, playlistId, true); res.status(201).json({ message: 'Playlist uploaded successfully', id: playlistId, filename }); } catch (error) { logger.error('Error parsing uploaded M3U:', error); res.status(500).json({ error: 'Failed to parse playlist' }); } } ); }); // Delete playlist router.delete('/:id', authenticate, modifyLimiter, validateIdParam, (req, res) => { const playlistId = req.params.id; db.get( 'SELECT * FROM playlists WHERE id = ? AND user_id = ?', [playlistId, req.user.userId], (err, playlist) => { if (err) { logger.error('Error fetching playlist:', err); return res.status(500).json({ error: 'Failed to delete playlist' }); } if (!playlist) { return res.status(404).json({ error: 'Playlist not found' }); } // Delete file if exists if (playlist.filename) { const filePath = path.join(__dirname, '../../data/uploads', playlist.filename); if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); } } db.run('DELETE FROM playlists WHERE id = ?', [playlistId], (err) => { if (err) { logger.error('Error deleting playlist:', err); return res.status(500).json({ error: 'Failed to delete playlist' }); } res.json({ message: 'Playlist deleted successfully' }); }); } ); }); // Bulk delete playlists router.post('/bulk-delete', authenticate, modifyLimiter, validateBulkDelete, (req, res) => { const { ids } = req.body; const placeholders = ids.map(() => '?').join(','); const query = `DELETE FROM playlists WHERE id IN (${placeholders}) AND user_id = ?`; db.run(query, [...ids, req.user.userId], function(err) { if (err) { logger.error('Error bulk deleting playlists:', err); return res.status(500).json({ error: 'Failed to delete playlists' }); } res.json({ message: 'Playlists deleted successfully', deleted: this.changes }); }); }); // Rename playlist const validatePlaylistRename = createValidationMiddleware({ params: { id: (value) => { const num = parseInt(value, 10); if (isNaN(num) || num < 1) { return { valid: false, errors: ['Invalid playlist ID'], sanitized: null }; } return { valid: true, errors: [], sanitized: num }; } }, body: { name: validatePlaylistName } }); router.patch('/:id', authenticate, modifyLimiter, validatePlaylistRename, (req, res) => { const { name } = req.body; const playlistId = req.params.id; db.run( 'UPDATE playlists SET name = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND user_id = ?', [name, playlistId, req.user.userId], function(err) { if (err) { logger.error('Error renaming playlist:', err); return res.status(500).json({ error: 'Failed to rename playlist' }); } if (this.changes === 0) { return res.status(404).json({ error: 'Playlist not found' }); } res.json({ message: 'Playlist renamed successfully' }); } ); }); module.exports = router;