feat: add playlist update and delete functionality
This commit is contained in:
@@ -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 });
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
})
|
||||
|
||||
@@ -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();
|
||||
|
||||
62
backend/services/PlaylistService.js
Normal file
62
backend/services/PlaylistService.js
Normal 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();
|
||||
@@ -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 });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
47
backend/socket/PlaylistSocketHandler.js
Normal file
47
backend/socket/PlaylistSocketHandler.js
Normal 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 });
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -25,6 +25,8 @@ export interface Channel {
|
||||
avatar: string;
|
||||
restream: boolean;
|
||||
headers: CustomHeader[];
|
||||
group: string;
|
||||
playlist: string;
|
||||
}
|
||||
|
||||
export interface ChatMessage {
|
||||
|
||||
Reference in New Issue
Block a user