Initial commit: StreamFlow IPTV platform
This commit is contained in:
commit
73a8ae9ffd
1240 changed files with 278451 additions and 0 deletions
312
backend/routes/channels.js
Normal file
312
backend/routes/channels.js
Normal file
|
|
@ -0,0 +1,312 @@
|
|||
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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue