streamflow/backend/routes/channels.js

313 lines
8.9 KiB
JavaScript
Raw Normal View History

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, readLimiter } = require('../middleware/rateLimiter');
const { db } = require('../database/db');
const logger = require('../utils/logger');
const {
validateIdParam,
validateChannelUpdate,
validatePagination,
validateSearch
} = require('../middleware/inputValidation');
// Configure multer for logo uploads
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const uploadDir = path.join(__dirname, '../uploads/logos');
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
cb(null, uploadDir);
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, 'channel-' + uniqueSuffix + path.extname(file.originalname));
}
});
const upload = multer({
storage: storage,
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB limit
fileFilter: (req, file, cb) => {
const allowedTypes = /jpeg|jpg|png|gif|svg|webp/;
const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
const mimetype = allowedTypes.test(file.mimetype);
if (mimetype && extname) {
return cb(null, true);
}
cb(new Error('Only image files are allowed'));
}
});
// Get channels with filters
router.get('/', authenticate, readLimiter, validatePagination, validateSearch, (req, res) => {
const { playlistId, isRadio, groupName, search } = req.query;
const limit = req.sanitizedQuery?.limit || 100;
const offset = req.sanitizedQuery?.offset || 0;
let query = `
SELECT c.*, p.name as playlist_name,
COALESCE(c.custom_logo, c.logo) as logo
FROM channels c
JOIN playlists p ON c.playlist_id = p.id
WHERE p.user_id = ? AND c.is_active = 1
`;
const params = [req.user.userId];
if (playlistId) {
query += ' AND c.playlist_id = ?';
params.push(playlistId);
}
if (isRadio !== undefined) {
query += ' AND c.is_radio = ?';
params.push(isRadio === 'true' ? 1 : 0);
}
if (groupName) {
query += ' AND c.group_name = ?';
params.push(groupName);
}
if (search) {
query += ' AND (c.name LIKE ? OR c.group_name LIKE ?)';
params.push(`%${search}%`, `%${search}%`);
}
query += ' ORDER BY c.name LIMIT ? OFFSET ?';
params.push(parseInt(limit), parseInt(offset));
db.all(query, params, (err, channels) => {
if (err) {
return res.status(500).json({ error: 'Failed to fetch channels' });
}
res.json(channels);
});
});
// Get channel groups
router.get('/groups', authenticate, readLimiter, (req, res) => {
const { playlistId, isRadio } = req.query;
// Validate playlist ID if provided
if (playlistId && (isNaN(parseInt(playlistId)) || parseInt(playlistId) < 1)) {
return res.status(400).json({ error: 'Invalid playlist ID' });
}
let query = `
SELECT DISTINCT c.group_name, COUNT(*) as count
FROM channels c
JOIN playlists p ON c.playlist_id = p.id
WHERE p.user_id = ? AND c.is_active = 1
`;
const params = [req.user.userId];
if (playlistId) {
query += ' AND c.playlist_id = ?';
params.push(playlistId);
}
if (isRadio !== undefined) {
query += ' AND c.is_radio = ?';
params.push(isRadio === 'true' ? 1 : 0);
}
query += ' GROUP BY c.group_name ORDER BY c.group_name';
db.all(query, params, (err, groups) => {
if (err) {
return res.status(500).json({ error: 'Failed to fetch groups' });
}
res.json(groups);
});
});
// Upload custom logo for channel
router.post('/:id/logo', authenticate, modifyLimiter, validateIdParam, upload.single('logo'), (req, res) => {
const channelId = req.params.id;
const userId = req.user.userId;
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
// Verify channel belongs to user
db.get(
`SELECT c.*, c.custom_logo as old_logo
FROM channels c
JOIN playlists p ON c.playlist_id = p.id
WHERE c.id = ? AND p.user_id = ?`,
[channelId, userId],
(err, channel) => {
if (err) {
return res.status(500).json({ error: 'Database error' });
}
if (!channel) {
// Delete uploaded file if channel not found
fs.unlinkSync(req.file.path);
return res.status(404).json({ error: 'Channel not found' });
}
// Delete old custom logo if exists
if (channel.old_logo) {
const oldLogoPath = path.join(__dirname, '..', channel.old_logo);
if (fs.existsSync(oldLogoPath)) {
fs.unlinkSync(oldLogoPath);
}
}
// Save new logo path
const logoPath = `/uploads/logos/${req.file.filename}`;
db.run(
'UPDATE channels SET custom_logo = ? WHERE id = ?',
[logoPath, channelId],
(err) => {
if (err) {
return res.status(500).json({ error: 'Failed to update logo' });
}
res.json({
message: 'Logo uploaded successfully',
logoUrl: logoPath
});
}
);
}
);
});
// Delete custom logo for a channel
router.delete('/:id/logo', authenticate, modifyLimiter, (req, res) => {
const channelId = req.params.id;
const userId = req.user.userId;
// Verify channel belongs to user and get current logo
db.get(
`SELECT c.*, c.custom_logo
FROM channels c
JOIN playlists p ON c.playlist_id = p.id
WHERE c.id = ? AND p.user_id = ?`,
[channelId, userId],
(err, channel) => {
if (err) {
return res.status(500).json({ error: 'Database error' });
}
if (!channel) {
return res.status(404).json({ error: 'Channel not found' });
}
if (!channel.custom_logo) {
return res.status(400).json({ error: 'No custom logo to delete' });
}
// Delete file from filesystem
const logoPath = path.join(__dirname, '..', channel.custom_logo);
if (fs.existsSync(logoPath)) {
fs.unlinkSync(logoPath);
}
// Remove logo from database
db.run(
'UPDATE channels SET custom_logo = NULL WHERE id = ?',
[channelId],
(err) => {
if (err) {
return res.status(500).json({ error: 'Failed to delete logo' });
}
res.json({ message: 'Logo deleted successfully' });
}
);
}
);
});
// Get single channel by ID
router.get('/:id', authenticate, readLimiter, (req, res) => {
const channelId = req.params.id;
const userId = req.user.userId;
db.get(
`SELECT c.*, p.name as playlist_name,
COALESCE(c.custom_logo, c.logo) as logo
FROM channels c
JOIN playlists p ON c.playlist_id = p.id
WHERE c.id = ? AND p.user_id = ?`,
[channelId, userId],
(err, channel) => {
if (err) {
return res.status(500).json({ error: 'Database error' });
}
if (!channel) {
return res.status(404).json({ error: 'Channel not found' });
}
res.json(channel);
}
);
});
// Delete channel from playlist
router.delete('/:id', authenticate, modifyLimiter, validateIdParam, (req, res) => {
const channelId = req.params.id;
const userId = req.user.userId;
// Verify channel belongs to user
db.get(
`SELECT c.id, c.playlist_id, c.custom_logo
FROM channels c
JOIN playlists p ON c.playlist_id = p.id
WHERE c.id = ? AND p.user_id = ?`,
[channelId, userId],
(err, channel) => {
if (err) {
logger.error('Error fetching channel for deletion:', err);
return res.status(500).json({ error: 'Database error' });
}
if (!channel) {
return res.status(404).json({ error: 'Channel not found' });
}
// Delete custom logo file if exists
if (channel.custom_logo) {
const logoPath = path.join(__dirname, '..', channel.custom_logo);
if (fs.existsSync(logoPath)) {
try {
fs.unlinkSync(logoPath);
} catch (err) {
logger.error('Error deleting custom logo file:', err);
}
}
}
// Delete channel from database
db.run('DELETE FROM channels WHERE id = ?', [channelId], function(err) {
if (err) {
logger.error('Error deleting channel:', err);
return res.status(500).json({ error: 'Failed to delete channel' });
}
// Update playlist channel count
db.run(
'UPDATE playlists SET channel_count = (SELECT COUNT(*) FROM channels WHERE playlist_id = ?) WHERE id = ?',
[channel.playlist_id, channel.playlist_id],
(updateErr) => {
if (updateErr) {
logger.error('Error updating playlist count:', updateErr);
}
}
);
logger.info(`Channel ${channelId} deleted by user ${userId}`);
res.json({
message: 'Channel deleted successfully',
deletedId: channelId
});
});
}
);
});
module.exports = router;