233 lines
7.1 KiB
JavaScript
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;
|