streamflow/backend/routes/playlists.js
2025-12-17 00:42:43 +00:00

233 lines
7.1 KiB
JavaScript

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;