feat: add playlist update and delete functionality

This commit is contained in:
Ante Brähler
2024-12-20 00:49:41 +01:00
parent 8973d9bd5d
commit f727477d47
10 changed files with 193 additions and 83 deletions

View File

@@ -31,8 +31,8 @@ module.exports = {
addChannel(req, res) {
try {
const { name, url, avatar, restream, headersJson, group } = req.body;
const newChannel = ChannelService.addChannel(name, url, avatar, restream, headersJson, group);
//const { name, url, avatar, restream, headersJson, group, playlist } = req.body;
const newChannel = ChannelService.addChannel(req.body);
res.status(201).json(newChannel);
} catch (error) {
res.status(500).json({ error: error.message });

View File

@@ -1,6 +1,6 @@
class Channel {
static nextId = 0;
constructor(name, url, avatar, restream, headers, group) {
constructor(name, url, avatar, restream, headers = [], group = null, playlist = null) {
this.id = Channel.nextId++;
this.name = name;
this.url = url;
@@ -8,6 +8,7 @@ class Channel {
this.restream = restream;
this.headers = headers;
this.group = group;
this.playlist = playlist;
}
}

View File

@@ -8,6 +8,7 @@ const ChannelSocketHandler = require('./socket/ChannelSocketHandler');
const channelController = require('./controllers/ChannelController');
const streamController = require('./services/streaming/StreamController');
const ChannelService = require('./services/ChannelService');
const PlaylistSocketHandler = require('./socket/PlaylistSocketHandler');
dotenv.config();
@@ -52,6 +53,7 @@ io.on('connection', socket => {
})
ChannelSocketHandler(io, socket);
PlaylistSocketHandler(io, socket);
ChatSocketHandler(io, socket);
})

View File

@@ -1,6 +1,5 @@
const streamController = require('./streaming/StreamController');
const Channel = require('../models/Channel');
const m3uParser = require('iptv-playlist-parser');
class ChannelService {
constructor() {
@@ -19,14 +18,14 @@ class ChannelService {
this.channels = [
//Some Test-channels to get started, remove this when using your own playlist
new Channel('Das Erste', process.env.DEFAULT_CHANNEL_URL, "https://upload.wikimedia.org/wikipedia/commons/thumb/5/56/Das_Erste-Logo_klein.svg/768px-Das_Erste-Logo_klein.svg.png", false, [], null),
new Channel('DAZN 1 DE', "https://xyzdddd.mizhls.ru/lb/premium426/index.m3u8", "https://upload.wikimedia.org/wikipedia/commons/4/49/DAZN_1.svg", true, daddyHeaders, null),
new Channel('beIN Sports 1', "https://xyzdddd.mizhls.ru/lb/premium61/index.m3u8","https://www.thesportsdb.com/images/media/channel/logo/BeIn_Sports_1_Australia.png", true, daddyHeaders, null),
new Channel('beIN Sports 2', "https://xyzdddd.mizhls.ru/lb/premium92/index.m3u8", "https://www.thesportsdb.com/images/media/channel/logo/BeIn_Sports_HD_2_France.png", true, daddyHeaders, null),
new Channel('Sky Sport Football', "https://xyzdddd.mizhls.ru/lb/premium35/index.m3u8", "https://raw.githubusercontent.com/tv-logo/tv-logos/main/countries/united-kingdom/sky-sports-football-uk.png", true, daddyHeaders, null),
new Channel('Sky Sports Premier League', "https://xyzdddd.mizhls.ru/lb/premium130/index.m3u8", "https://github.com/tv-logo/tv-logos/blob/main/countries/united-kingdom/sky-sports-premier-league-uk.png?raw=true", true, daddyHeaders, null),
new Channel('SuperSport Premier League', 'https://xyzdddd.mizhls.ru/lb/premium414/index.m3u8', "https://github.com/tv-logo/tv-logos/blob/8d25ddd79ca2f9cd033b808c45cccd2b3da563ee/countries/south-africa/supersport-premier-league-za.png?raw=true", true, daddyHeaders, null),
new Channel('NBA', "https://v14.thetvapp.to/hls/NBA28/index.m3u8?token=bFFITmZCbllna21WRUJra0xjN0JPN0w1VlBmSkNUcTl4Zml3a2tQSg==", "https://raw.githubusercontent.com/tv-logo/tv-logos/635e715cb2f2c6d28e9691861d3d331dd040285b/countries/united-states/nba-tv-icon-us.png", false, [], null),
new Channel('Das Erste', process.env.DEFAULT_CHANNEL_URL, "https://upload.wikimedia.org/wikipedia/commons/thumb/5/56/Das_Erste-Logo_klein.svg/768px-Das_Erste-Logo_klein.svg.png", false),
new Channel('DAZN 1 DE', "https://xyzdddd.mizhls.ru/lb/premium426/index.m3u8", "https://upload.wikimedia.org/wikipedia/commons/4/49/DAZN_1.svg", true, daddyHeaders),
new Channel('beIN Sports 1', "https://xyzdddd.mizhls.ru/lb/premium61/index.m3u8","https://www.thesportsdb.com/images/media/channel/logo/BeIn_Sports_1_Australia.png", true, daddyHeaders),
new Channel('beIN Sports 2', "https://xyzdddd.mizhls.ru/lb/premium92/index.m3u8", "https://www.thesportsdb.com/images/media/channel/logo/BeIn_Sports_HD_2_France.png", true, daddyHeaders),
new Channel('Sky Sport Football', "https://xyzdddd.mizhls.ru/lb/premium35/index.m3u8", "https://raw.githubusercontent.com/tv-logo/tv-logos/main/countries/united-kingdom/sky-sports-football-uk.png", true, daddyHeaders),
new Channel('Sky Sports Premier League', "https://xyzdddd.mizhls.ru/lb/premium130/index.m3u8", "https://github.com/tv-logo/tv-logos/blob/main/countries/united-kingdom/sky-sports-premier-league-uk.png?raw=true", true, daddyHeaders),
new Channel('SuperSport Premier League', 'https://xyzdddd.mizhls.ru/lb/premium414/index.m3u8', "https://github.com/tv-logo/tv-logos/blob/8d25ddd79ca2f9cd033b808c45cccd2b3da563ee/countries/south-africa/supersport-premier-league-za.png?raw=true", true, daddyHeaders),
new Channel('NBA', "https://v14.thetvapp.to/hls/NBA28/index.m3u8?token=bFFITmZCbllna21WRUJra0xjN0JPN0w1VlBmSkNUcTl4Zml3a2tQSg==", "https://raw.githubusercontent.com/tv-logo/tv-logos/635e715cb2f2c6d28e9691861d3d331dd040285b/countries/united-states/nba-tv-icon-us.png", false),
];
this.currentChannel = this.channels[0];
}
@@ -35,7 +34,7 @@ class ChannelService {
return this.channels;
}
addChannel(name, url, avatar, restream, headersJson, group) {
addChannel({name, url, avatar, restream, headersJson, group = false, playlist = false}) {
const existing = this.channels.find(channel => channel.url === url);
if (existing) {
@@ -43,7 +42,7 @@ class ChannelService {
}
const headers = JSON.parse(headersJson);
const newChannel = new Channel(name, url, avatar, restream, headers, group);
const newChannel = new Channel(name, url, avatar, restream, headers, group, playlist);
this.channels.push(newChannel);
return newChannel;
@@ -119,28 +118,6 @@ class ChannelService {
return channel;
}
async addChannelsFromPlaylist(playlistUrl, restream, headersJson) {
const response = await fetch(playlistUrl);
const content = await response.text();
const parsedPlaylist = m3uParser.parse(content);
// list of added channels
const channels = parsedPlaylist.items.map(channel => {
//TODO: add channel.http if not '' to headers
try {
return this.addChannel(channel.name, channel.url, channel.tvg.logo, restream, headersJson, channel.group.title);
} catch (error) {
console.error(error);
return null;
}
})
.filter(result => result !== null);
return channels;
}
}
module.exports = new ChannelService();

View File

@@ -0,0 +1,62 @@
const m3uParser = require('iptv-playlist-parser');
const ChannelService = require('./ChannelService');
class PlaylistService {
async addPlaylist(playlistUrl, restream, headersJson) {
const response = await fetch(playlistUrl);
const content = await response.text();
const parsedPlaylist = m3uParser.parse(content);
// list of added channels
const channels = parsedPlaylist.items.map(channel => {
//TODO: add channel.http if not '' to headers
try {
return ChannelService.addChannel({
name: channel.name,
url: channel.url,
avatar: channel.tvg.logo,
restream: restream,
headersJson: headersJson,
group: channel.group.title,
playlist: playlistUrl
});
} catch (error) {
console.error(error);
return null;
}
})
.filter(result => result !== null);
return channels;
}
updatePlaylist(playlistUrl, updatedAttributes) {
const channels = ChannelService
.getChannels()
.filter(channel => channel.playlist === playlistUrl);
for(var channel of channels) {
channel = ChannelService.updateChannel(channel.id, updatedAttributes);
}
return channels;
}
deletePlaylist(playlistUrl) {
const channels = ChannelService
.getChannels()
.filter(channel => channel.playlist === playlistUrl);
for(const channel of channels) {
ChannelService.deleteChannel(channel.id);
}
return channels;
}
}
module.exports = new PlaylistService();

View File

@@ -2,16 +2,15 @@ const ChannelService = require('../services/ChannelService');
module.exports = (io, socket) => {
socket.on('add-channel', ({ name, url, avatar, restream, headersJson}) => {
socket.on('add-channel', ({ name, url, avatar, restream, headersJson }) => {
try {
const newChannel = ChannelService.addChannel(name, url, avatar, restream, headersJson, null);
const newChannel = ChannelService.addChannel({ name: name, url: url, avatar: avatar, restream: restream, headersJson: headersJson });
io.emit('channel-added', newChannel); // Broadcast to all clients
} catch (err) {
socket.emit('app-error', { message: err.message });
}
});
socket.on('set-current-channel', (id) => {
try {
const nextChannel = ChannelService.setCurrentChannel(id);
@@ -26,7 +25,7 @@ module.exports = (io, socket) => {
try {
const current = ChannelService.deleteChannel(id);
io.emit('channel-deleted', id); // Broadcast to all clients
io.emit('channel-selected', current);
io.emit('channel-selected', current);
} catch (err) {
console.error(err);
socket.emit('app-error', { message: err.message });
@@ -42,19 +41,4 @@ module.exports = (io, socket) => {
socket.emit('app-error', { message: err.message });
}
});
socket.on('upload-playlist', async ({ playlistUrl, restream, headersJson }) => {
try {
channels = await ChannelService.addChannelsFromPlaylist(playlistUrl, restream, headersJson);
if (channels) {
channels.forEach(channel => {
io.emit('channel-added', channel);
});
}
} catch (err) {
console.error(err);
socket.emit('app-error', { message: err.message });
}
});
};

View File

@@ -0,0 +1,47 @@
const PlaylistService = require('../services/PlaylistService');
const ChannelService = require('../services/ChannelService');
const Channel = require('../models/Channel');
module.exports = (io, socket) => {
socket.on('add-playlist', async ({ playlist, restream, headersJson }) => {
try {
const channels = await PlaylistService.addPlaylist(playlist, restream, headersJson);
if (channels) {
channels.forEach(channel => {
io.emit('channel-added', channel);
});
}
} catch (err) {
console.error(err);
socket.emit('app-error', { message: err.message });
}
});
socket.on('update-playlist', ({ playlist, updatedAttributes }) => {
try {
const channels = PlaylistService.updatePlaylist(playlist, updatedAttributes);
channels.forEach(channel => {
io.emit('channel-updated', channel);
});
} catch (err) {
console.error(err);
socket.emit('app-error', { message: err.message });
}
});
socket.on('delete-playlist', (playlist) => {
try {
const channels = PlaylistService.deletePlaylist(playlist);
channels.forEach(channel => {
io.emit('channel-deleted', channel.id);
});
io.emit('channel-selected', ChannelService.getCurrentChannel());
} catch (err) {
console.error(err);
socket.emit('app-error', { message: err.message });
}
});
};

View File

@@ -18,7 +18,7 @@ function ChannelModal({ isOpen, onClose, channel }: ChannelModalProps) {
const [name, setName] = useState('');
const [url, setUrl] = useState('');
const [avatar, setAvatar] = useState('');
const [restream, setRestream] = useState(false);
const [restream, setRestream] = useState(true);
const [headers, setHeaders] = useState<CustomHeader[]>([]);
const [playlistUrl, setPlaylistUrl] = useState('');
@@ -32,6 +32,7 @@ function ChannelModal({ isOpen, onClose, channel }: ChannelModalProps) {
setAvatar(channel.avatar);
setRestream(channel.restream);
setHeaders(channel.headers);
setPlaylistUrl(channel.playlist);
setIsEditMode(true);
setMode('channel'); // Default to "channel" if a channel object exists
} else {
@@ -40,6 +41,7 @@ function ChannelModal({ isOpen, onClose, channel }: ChannelModalProps) {
setAvatar('');
setRestream(false);
setHeaders([]);
setPlaylistUrl('');
setIsEditMode(false);
setMode('channel'); // Default to "channel" if a channel object exists
}
@@ -78,7 +80,7 @@ function ChannelModal({ isOpen, onClose, channel }: ChannelModalProps) {
);
} else if (mode === 'playlist') {
if (!playlistUrl.trim()) return;
socketService.uploadPlaylist(playlistUrl.trim(), restream, JSON.stringify(headers));
socketService.addPlaylist(playlistUrl.trim(), restream, JSON.stringify(headers));
}
addToast({
@@ -91,16 +93,32 @@ function ChannelModal({ isOpen, onClose, channel }: ChannelModalProps) {
};
const handleUpdate = (id: number) => {
socketService.updateChannel(id, {
name: name.trim(),
url: url.trim(),
avatar: avatar.trim() || 'https://via.placeholder.com/64',
restream,
headers: headers,
});
if (mode === 'channel') {
socketService.updateChannel(id, {
name: name.trim(),
url: url.trim(),
avatar: avatar.trim() || 'https://via.placeholder.com/64',
restream,
headers: headers,
});
} else if (mode === 'playlist') {
if(channel!.playlist !== playlistUrl.trim()) {
// If the playlist URL has changed, we need to reload the playlist (delete old channels and fetch again)
socketService.deletePlaylist(channel!.playlist);
socketService.addPlaylist(playlistUrl.trim(), restream, JSON.stringify(headers));
} else {
socketService.updatePlaylist(playlistUrl.trim(), {
playlist: playlistUrl.trim(),
restream,
headers: headers,
});
}
}
addToast({
type: 'success',
title: 'Channel updated',
title: `${mode} updated`,
duration: 3000,
});
@@ -109,11 +127,15 @@ function ChannelModal({ isOpen, onClose, channel }: ChannelModalProps) {
const handleDelete = () => {
if (channel) {
socketService.deleteChannel(channel.id);
if (mode === 'channel') {
socketService.deleteChannel(channel.id);
} else if (mode === 'playlist') {
socketService.deletePlaylist(channel.playlist);
}
}
addToast({
type: 'error',
title: 'Channel deleted',
title: `${mode} deleted`,
duration: 3000,
});
onClose();
@@ -138,22 +160,20 @@ function ChannelModal({ isOpen, onClose, channel }: ChannelModalProps) {
</div>
{/* Slider */}
{!isEditMode && (
{(!isEditMode || channel?.playlist) && (
<div className="p-4 pb-0">
<div className="flex space-x-4 justify-center">
<button
onClick={() => setMode('channel')}
className={`px-4 py-2 rounded-lg border-2 ${
mode === 'channel' ? 'border-blue-600' : 'border-transparent'
} hover:border-blue-600 transition-colors`}
className={`px-4 py-2 rounded-lg border-2 ${mode === 'channel' ? 'border-blue-600' : 'border-transparent'
} hover:border-blue-600 transition-colors`}
>
Channel
</button>
<button
onClick={() => setMode('playlist')}
className={`px-4 py-2 rounded-lg border-2 ${
mode === 'playlist' ? 'border-blue-600' : 'border-transparent'
} hover:border-blue-600 transition-colors`}
className={`px-4 py-2 rounded-lg border-2 ${mode === 'playlist' ? 'border-blue-600' : 'border-transparent'
} hover:border-blue-600 transition-colors`}
>
Playlist
</button>

View File

@@ -49,7 +49,7 @@ class SocketService {
this.listeners.get(event)?.push(listener);
}
// Event abbestellen
// Unsubscribe from event
unsubscribeFromEvent<T>(event: string, listener: (data: T) => void) {
const eventListeners = this.listeners.get(event);
if (eventListeners) {
@@ -61,47 +61,62 @@ class SocketService {
}
// Nachricht senden
// Send chat message
sendMessage(userName: string, userAvatar: string, message: string, timestamp: string) {
if (!this.socket) throw new Error('Socket is not connected.');
this.socket.emit('send-message', { userName, userAvatar, message, timestamp });
}
// Channel hinzufügen
// Add channel
addChannel(name: string, url: string, avatar: string, restream: boolean, headersJson: string) {
if (!this.socket) throw new Error('Socket is not connected.');
this.socket.emit('add-channel', { name, url, avatar, restream, headersJson });
}
// Aktuellen Channel setzen
// Set current channel
setCurrentChannel(id: number) {
if (!this.socket) throw new Error('Socket is not connected.');
this.socket.emit('set-current-channel', id);
}
// Channel löschen
// Delete channel
deleteChannel(id: number) {
if (!this.socket) throw new Error('Socket is not connected.');
this.socket.emit('delete-channel', id);
}
// Channel aktualisieren
// Update channel
updateChannel(id: number, updatedAttributes: any) {
if (!this.socket) throw new Error('Socket is not connected.');
this.socket.emit('update-channel', { id, updatedAttributes });
}
// Playlist hochladen
uploadPlaylist(playlistUrl: string, restream: boolean, headersJson: string ) {
// Add playlist
addPlaylist(playlist: string, restream: boolean, headersJson: string) {
if (!this.socket) throw new Error('Socket is not connected.');
this.socket.emit('upload-playlist', { playlistUrl, restream, headersJson });
this.socket.emit('add-playlist', { playlist, restream, headersJson });
}
// Update playlist
updatePlaylist(playlist: string, updatedAttributes: any) {
if (!this.socket) throw new Error('Socket is not connected.');
this.socket.emit('update-playlist', { playlist, updatedAttributes });
}
// Delete playlist
deletePlaylist(playlist: string) {
if (!this.socket) throw new Error('Socket is not connected.');
this.socket.emit('delete-playlist', playlist);
}
}
const socketService = new SocketService();

View File

@@ -25,6 +25,8 @@ export interface Channel {
avatar: string;
restream: boolean;
headers: CustomHeader[];
group: string;
playlist: string;
}
export interface ChatMessage {