From cd9a960c37c6cae145d3255bc61da435e56bdaec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ante=20Br=C3=A4hler?= <58339175+antebrl@users.noreply.github.com> Date: Sun, 15 Dec 2024 21:32:01 +0100 Subject: [PATCH] Add m3u playlist support --- backend/controllers/ChannelController.js | 39 +++++++++++++- backend/services/ChannelService.js | 24 +++++++++ backend/socket/ChannelSocketHandler.js | 23 ++++++++ .../components/add_channel/ChannelModal.tsx | 53 ++++++++++++++++++- frontend/src/services/ApiService.ts | 19 +++++++ frontend/src/services/SocketService.ts | 7 +++ 6 files changed, 162 insertions(+), 3 deletions(-) diff --git a/backend/controllers/ChannelController.js b/backend/controllers/ChannelController.js index acd4921..046c99e 100644 --- a/backend/controllers/ChannelController.js +++ b/backend/controllers/ChannelController.js @@ -1,4 +1,6 @@ const ChannelService = require('../services/ChannelService'); +const fs = require('fs'); +const m3uParser = require('m3u8-parser'); module.exports = { getChannels(req, res) { @@ -38,4 +40,39 @@ module.exports = { res.status(500).json({ error: error.message }); } }, -}; \ No newline at end of file + + addPlaylist(req, res) { + try { + const { playlistUrl } = req.body; + const playlistContent = fs.readFileSync(playlistUrl, 'utf8'); + const parser = new m3uParser.Parser(); + parser.push(playlistContent); + parser.end(); + + const parsedPlaylist = parser.manifest; + const channels = parsedPlaylist.segments.map(segment => ({ + name: segment.title, + url: segment.uri, + avatar: '', + restream: false, + headersJson: '[]' + })); + + channels.forEach(channel => { + ChannelService.addChannel(channel.name, channel.url, channel.avatar, channel.restream, channel.headersJson); + }); + + res.status(201).json({ message: 'Playlist added successfully' }); + } catch (error) { + res.status(500).json({ error: error.message }); + } + } +}; + +const express = require('express'); +const router = express.Router(); +const ChannelController = require('../controllers/ChannelController'); + +router.post('/playlist', ChannelController.addPlaylist); + +module.exports = router; diff --git a/backend/services/ChannelService.js b/backend/services/ChannelService.js index c4ca7c9..e9053f6 100644 --- a/backend/services/ChannelService.js +++ b/backend/services/ChannelService.js @@ -1,5 +1,7 @@ const streamController = require('./streaming/StreamController'); const Channel = require('../models/Channel'); +const fs = require('fs'); +const m3uParser = require('@pawanpaudel93/m3u-parse'); class ChannelService { constructor() { @@ -112,6 +114,28 @@ class ChannelService { return channel; } + + addChannelsFromPlaylist(playlistUrl) { + const playlistContent = fs.readFileSync(playlistUrl, 'utf8'); + const parser = new m3uParser.Parser(); + parser.push(playlistContent); + parser.end(); + + const parsedPlaylist = parser.manifest; + const channels = parsedPlaylist.segments.map(segment => ({ + name: segment.title, + url: segment.uri, + avatar: '', + restream: false, + headersJson: '[]' + })); + + channels.forEach(channel => { + this.addChannel(channel.name, channel.url, channel.avatar, channel.restream, channel.headersJson); + }); + + return channels; + } } module.exports = new ChannelService(); diff --git a/backend/socket/ChannelSocketHandler.js b/backend/socket/ChannelSocketHandler.js index f672015..58ea7fc 100644 --- a/backend/socket/ChannelSocketHandler.js +++ b/backend/socket/ChannelSocketHandler.js @@ -42,4 +42,27 @@ module.exports = (io, socket) => { socket.emit('app-error', { message: err.message }); } }); + + socket.on('upload-playlist', async (data) => { + try { + let channels; + if (data.playlistUrl) { + channels = await ChannelService.addChannelsFromPlaylist(data.playlistUrl); + } else if (data instanceof FormData) { + const playlistFile = data.get('playlistFile'); + if (playlistFile) { + const playlistContent = await playlistFile.text(); + channels = await ChannelService.addChannelsFromPlaylist(playlistContent); + } + } + if (channels) { + channels.forEach(channel => { + io.emit('channel-added', channel); + }); + } + } catch (err) { + console.error(err); + socket.emit('app-error', { message: err.message }); + } + }); }; diff --git a/frontend/src/components/add_channel/ChannelModal.tsx b/frontend/src/components/add_channel/ChannelModal.tsx index ff0d9d0..35bcd6b 100644 --- a/frontend/src/components/add_channel/ChannelModal.tsx +++ b/frontend/src/components/add_channel/ChannelModal.tsx @@ -17,6 +17,8 @@ function ChannelModal({ isOpen, onClose, channel }: ChannelModalProps) { const [restream, setRestream] = useState(false); const [headers, setHeaders] = useState([]); const [isEditMode, setIsEditMode] = useState(false); + const [playlistUrl, setPlaylistUrl] = useState(''); + const [playlistFile, setPlaylistFile] = useState(null); useEffect(() => { if (channel) { @@ -36,7 +38,7 @@ function ChannelModal({ isOpen, onClose, channel }: ChannelModalProps) { } }, [channel]); - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!name.trim() || !url.trim()) return; @@ -52,6 +54,16 @@ function ChannelModal({ isOpen, onClose, channel }: ChannelModalProps) { ); } + if (playlistUrl.trim()) { + socketService.uploadPlaylist({ playlistUrl: playlistUrl.trim() }); + } + + if (playlistFile) { + const formData = new FormData(); + formData.append('playlistFile', playlistFile); + socketService.uploadPlaylist(formData); + } + onClose(); }; @@ -87,6 +99,16 @@ function ChannelModal({ isOpen, onClose, channel }: ChannelModalProps) { setHeaders(newHeaders); }; + const handlePlaylistUrlChange = (e: React.ChangeEvent) => { + setPlaylistUrl(e.target.value); + }; + + const handlePlaylistFileChange = (e: React.ChangeEvent) => { + if (e.target.files) { + setPlaylistFile(e.target.files[0]); + } + }; + if (!isOpen) return null; return ( @@ -209,6 +231,33 @@ function ChannelModal({ isOpen, onClose, channel }: ChannelModalProps) { )} +
+ + +
+ +
+ + +
+
{isEditMode && (