feat: implement channel management features (add, update, delete)

This commit is contained in:
Ante Brähler
2024-12-11 22:54:42 +00:00
parent fe3108013c
commit 57c6f6eb80
9 changed files with 244 additions and 28 deletions

View File

@@ -8,4 +8,34 @@ module.exports = {
getCurrentChannel(req, res) {
res.json(ChannelService.getCurrentChannel());
},
deleteChannel(req, res) {
try {
const { channelId } = req.params;
ChannelService.deleteChannel(channelId);
res.status(204).send();
} catch (error) {
res.status(500).json({ error: error.message });
}
},
updateChannel(req, res) {
try {
const { channelId } = req.params;
const updatedChannel = ChannelService.updateChannel(channelId, req.body);
res.json(updatedChannel);
} catch (error) {
res.status(500).json({ error: error.message });
}
},
addChannel(req, res) {
try {
const { name, url, avatar, restream, headersJson } = req.body;
const newChannel = ChannelService.addChannel(name, url, avatar, restream, headersJson);
res.status(201).json(newChannel);
} catch (error) {
res.status(500).json({ error: error.message });
}
},
};

View File

@@ -18,6 +18,9 @@ const apiRouter = express.Router();
apiRouter.get('/', channelController.getChannels);
apiRouter.get('/current', channelController.getCurrentChannel);
apiRouter.delete('/:channelId', channelController.deleteChannel);
apiRouter.put('/:channelId', channelController.updateChannel);
apiRouter.post('/', channelController.addChannel);
app.use('/api/channels', apiRouter);

View File

@@ -64,6 +64,54 @@ class ChannelService {
getCurrentChannel() {
return this.currentChannel;
}
deleteChannel(id) {
const channelIndex = this.channels.findIndex(channel => channel.id === id);
if (channelIndex === -1) {
throw new Error('Channel does not exist');
}
const [deletedChannel] = this.channels.splice(channelIndex, 1);
if (this.currentChannel.id === id) {
if (deletedChannel.restream) {
streamController.stop(deletedChannel.id);
}
this.currentChannel = this.channels.length > 0 ? this.channels[0] : null;
if(this.currentChannel?.restream) {
streamController.start(this.currentChannel);
}
}
return this.currentChannel;
}
updateChannel(id, updatedAttributes) {
const channelIndex = this.channels.findIndex(channel => channel.id === id);
if (channelIndex === -1) {
throw new Error('Channel does not exist');
}
const streamChanged = updatedAttributes.url != this.currentChannel.url ||
updatedAttributes.headers != this.currentChannel.headers ||
updatedAttributes.restream != this.currentChannel.restream;
const channel = this.channels[channelIndex];
Object.assign(channel, updatedAttributes);
if(this.currentChannel.id == id) {
if(streamChanged) {
streamController.stop(channel.id);
if(channel.restream) {
streamController.start(channel);
}
}
}
return channel;
}
}
module.exports = new ChannelService();

View File

@@ -21,4 +21,25 @@ module.exports = (io, socket) => {
socket.emit('app-error', { message: err.message });
}
});
socket.on('delete-channel', (id) => {
try {
const current = ChannelService.deleteChannel(id);
io.emit('channel-deleted', id); // Broadcast to all clients
io.emit('channel-selected', current);
} catch (err) {
console.error(err);
socket.emit('app-error', { message: err.message });
}
});
socket.on('update-channel', ({ id, updatedAttributes }) => {
try {
const updatedChannel = ChannelService.updateChannel(id, updatedAttributes);
io.emit('channel-updated', updatedChannel); // Broadcast to all clients
} catch (err) {
console.error(err);
socket.emit('app-error', { message: err.message });
}
});
};

View File

@@ -14,7 +14,6 @@ import ToastContainer from './components/notifications/ToastContainer';
function App() {
const [channels, setChannels] = useState<Channel[]>([]);
const [selectedChannel, setSelectedChannel] = useState<Channel | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const [syncEnabled, setSyncEnabled] = useState(() => {
@@ -22,9 +21,9 @@ function App() {
return savedValue !== null ? JSON.parse(savedValue) : true;
});
const [searchQuery, setSearchQuery] = useState('');
const [editChannel, setEditChannel] = useState<Channel | null>(null);
useEffect(() => {
apiService
.request<Channel[]>('/channels/', 'GET')
.then((data) => setChannels(data))
@@ -35,7 +34,6 @@ function App() {
.then((data) => setSelectedChannel(data))
.catch((error) => console.error('Error loading current channel:', error));
console.log('Subscribing to events');
const channelAddedListener = (channel: Channel) => {
setChannels((prevChannels) => [...prevChannels, channel]);
@@ -45,24 +43,63 @@ function App() {
setSelectedChannel(nextChannel);
};
const channelUpdatedListener = (updatedChannel: Channel) => {
setChannels((prevChannels) =>
prevChannels.map((channel) =>
channel.id === updatedChannel.id ? updatedChannel : channel
)
);
setSelectedChannel((selectedChannel: Channel) => {
if(selectedChannel?.id === updatedChannel.id) {
if((selectedChannel?.url != updatedChannel.url || JSON.stringify(selectedChannel?.headers) != JSON.stringify(updatedChannel.headers)) && selectedChannel?.restream == updatedChannel.restream){
setTimeout(() => {
window.location.reload();
}, 3000);
}
return updatedChannel;
}
return;
}
);
};
const channelDeletedListener = (deletedChannel: number) => {
setChannels((prevChannels) =>
prevChannels.filter((channel) => channel.id !== deletedChannel)
);
};
socketService.subscribeToEvent('channel-added', channelAddedListener);
socketService.subscribeToEvent('channel-selected', channelSelectedListener);
socketService.subscribeToEvent('channel-updated', channelUpdatedListener);
socketService.subscribeToEvent('channel-deleted', channelDeletedListener);
socketService.connect();
return () => {
socketService.unsubscribeFromEvent('channel-added', channelAddedListener);
socketService.unsubscribeFromEvent('channel-selected', channelSelectedListener);
socketService.unsubscribeFromEvent('channel-updated', channelUpdatedListener);
socketService.unsubscribeFromEvent('channel-deleted', channelDeletedListener);
socketService.disconnect();
console.log('WebSocket connection closed');
};
}, []);
const filteredChannels = channels.filter(channel =>
const filteredChannels = channels.filter((channel) =>
channel.name.toLowerCase().includes(searchQuery.toLowerCase())
);
const handleEditChannel = (channel: Channel) => {
setEditChannel(channel);
setIsModalOpen(true);
};
return (
<ToastProvider>
<div className="min-h-screen bg-gray-900 text-gray-100">
@@ -84,7 +121,10 @@ function App() {
</div>
<div className="flex items-center space-x-4">
<Users className="w-6 h-6 text-blue-500" />
<button onClick={() => setIsSettingsOpen(true)} className="p-2 hover:bg-gray-800 rounded-lg transition-colors">
<button
onClick={() => setIsSettingsOpen(true)}
className="p-2 hover:bg-gray-800 rounded-lg transition-colors"
>
<Settings className="w-6 h-6 text-blue-500" />
</button>
</div>
@@ -110,6 +150,7 @@ function App() {
channels={filteredChannels}
selectedChannel={selectedChannel}
setSearchQuery={setSearchQuery}
onEditChannel={handleEditChannel}
/>
</div>
@@ -124,7 +165,11 @@ function App() {
<AddChannelModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
onClose={() => {
setIsModalOpen(false)
setEditChannel(null);
}}
channel={editChannel}
/>
<SettingsModal

View File

@@ -6,9 +6,10 @@ interface ChannelListProps {
channels: Channel[];
selectedChannel: Channel | null;
setSearchQuery: React.Dispatch<React.SetStateAction<string>>;
onEditChannel: (channel: Channel) => void;
}
function ChannelList({ channels, selectedChannel, setSearchQuery }: ChannelListProps) {
function ChannelList({ channels, selectedChannel, setSearchQuery, onEditChannel }: ChannelListProps) {
const onSelectChannel = (channel: Channel) => {
setSearchQuery('');
@@ -16,12 +17,18 @@ function ChannelList({ channels, selectedChannel, setSearchQuery }: ChannelListP
socketService.setCurrentChannel(channel.id);
};
const onRightClickChannel = (event: React.MouseEvent, channel: Channel) => {
event.preventDefault();
onEditChannel(channel);
};
return (
<div className="flex space-x-3 hover:overflow-x-auto overflow-hidden pb-2 px-1 pt-1 scroll-container">
{channels.map((channel) => (
<button
key={channel.id}
onClick={() => onSelectChannel(channel)}
onContextMenu={(event) => onRightClickChannel(event, channel)}
className={`group relative p-2 rounded-lg transition-all ${
selectedChannel?.id === channel.id
? 'bg-blue-500 bg-opacity-20 ring-2 ring-blue-500'

View File

@@ -159,7 +159,7 @@ function VideoPlayer({ channel, syncEnabled }: VideoPlayerProps) {
title: 'Stream Error',
message: is403 && !channel.restream
? 'Access denied. Try with restream option for this channel.'
: 'The stream is not working. Check the source.',
: `The stream is not working. Check the source. ${data.response?.text}`,
duration: 5000,
});
return;
@@ -173,7 +173,7 @@ function VideoPlayer({ channel, syncEnabled }: VideoPlayerProps) {
hlsRef.current.destroy();
}
};
}, [channel?.url, syncEnabled]);
}, [channel?.url, channel?.restream, channel?.headers, syncEnabled]);
const handleVideoClick = (event: React.MouseEvent<HTMLVideoElement>) => {
if (videoRef.current?.muted) {

View File

@@ -1,25 +1,48 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { Plus, Trash2, X } from 'lucide-react';
import socketService from '../../services/SocketService';
import { CustomHeader } from '../../types';
import { CustomHeader, Channel } from '../../types';
import CustomHeaderInput from './CustomHeaderInput';
interface AddChannelModalProps {
isOpen: boolean;
onClose: () => void;
channel?: Channel | null;
}
function AddChannelModal({ isOpen, onClose }: AddChannelModalProps) {
function AddChannelModal({ isOpen, onClose, channel }: AddChannelModalProps) {
const [name, setName] = useState('');
const [url, setUrl] = useState('');
const [avatar, setAvatar] = useState('');
const [restream, setRestream] = useState(false);
const [headers, setHeaders] = useState<CustomHeader[]>([]);
const [isEditMode, setIsEditMode] = useState(false);
useEffect(() => {
if (channel) {
setName(channel.name);
setUrl(channel.url);
setAvatar(channel.avatar);
setRestream(channel.restream);
setHeaders(channel.headers);
setIsEditMode(true);
} else {
setName('');
setUrl('');
setAvatar('');
setRestream(false);
setHeaders([]);
setIsEditMode(false);
}
}, [channel]);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!name.trim() || !url.trim()) return;
if (isEditMode && channel) {
handleUpdate(channel.id);
} else {
socketService.addChannel(
name.trim(),
url.trim(),
@@ -27,10 +50,26 @@ function AddChannelModal({ isOpen, onClose }: AddChannelModalProps) {
restream,
JSON.stringify(headers)
);
}
setName('');
setUrl('');
setAvatar('');
onClose();
};
const handleDelete = () => {
if (channel) {
socketService.deleteChannel(channel.id);
onClose();
}
};
const handleUpdate = (id: number) => {
socketService.updateChannel(id, {
name: name.trim(),
url: url.trim(),
avatar: avatar.trim() || 'https://via.placeholder.com/64',
restream,
headers: headers,
});
onClose();
};
@@ -54,7 +93,7 @@ function AddChannelModal({ isOpen, onClose }: AddChannelModalProps) {
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4">
<div className="bg-gray-800 rounded-lg w-full max-w-md">
<div className="flex items-center justify-between p-4 border-b border-gray-700">
<h2 className="text-xl font-semibold">Add New Channel</h2>
<h2 className="text-xl font-semibold">{isEditMode ? 'Edit Channel' : 'Add New Channel'}</h2>
<button
onClick={onClose}
className="p-1 hover:bg-gray-700 rounded-full transition-colors"
@@ -171,6 +210,15 @@ function AddChannelModal({ isOpen, onClose }: AddChannelModalProps) {
)}
<div className="flex justify-end space-x-3">
{isEditMode && (
<button
type="button"
onClick={handleDelete}
className="px-4 py-2 bg-red-600 rounded-lg hover:bg-red-700 transition-colors"
>
Delete
</button>
)}
<button
type="button"
onClick={onClose}
@@ -182,7 +230,7 @@ function AddChannelModal({ isOpen, onClose }: AddChannelModalProps) {
type="submit"
className="px-4 py-2 bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors"
>
Add Channel
{isEditMode ? 'Update Channel' : 'Add Channel'}
</button>
</div>
</form>

View File

@@ -81,6 +81,20 @@ class SocketService {
this.socket.emit('set-current-channel', id);
}
// Channel löschen
deleteChannel(id: number) {
if (!this.socket) throw new Error('Socket is not connected.');
this.socket.emit('delete-channel', id);
}
// Channel aktualisieren
updateChannel(id: number, updatedAttributes: any) {
if (!this.socket) throw new Error('Socket is not connected.');
this.socket.emit('update-channel', { id, updatedAttributes });
}
}
const socketService = new SocketService();