Add m3u playlist support

This commit is contained in:
Ante Brähler
2024-12-15 21:32:01 +01:00
parent ee4786b991
commit cd9a960c37
6 changed files with 162 additions and 3 deletions

View File

@@ -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 });
}
},
};
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;

View File

@@ -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();

View File

@@ -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 });
}
});
};

View File

@@ -17,6 +17,8 @@ function ChannelModal({ isOpen, onClose, channel }: ChannelModalProps) {
const [restream, setRestream] = useState(false);
const [headers, setHeaders] = useState<CustomHeader[]>([]);
const [isEditMode, setIsEditMode] = useState(false);
const [playlistUrl, setPlaylistUrl] = useState('');
const [playlistFile, setPlaylistFile] = useState<File | null>(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<HTMLInputElement>) => {
setPlaylistUrl(e.target.value);
};
const handlePlaylistFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
setPlaylistFile(e.target.files[0]);
}
};
if (!isOpen) return null;
return (
@@ -209,6 +231,33 @@ function ChannelModal({ isOpen, onClose, channel }: ChannelModalProps) {
</div>
)}
<div>
<label htmlFor="playlistUrl" className="block text-sm font-medium mb-1">
M3U Playlist URL
</label>
<input
type="url"
id="playlistUrl"
value={playlistUrl}
onChange={handlePlaylistUrlChange}
className="w-full bg-gray-700 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Enter M3U playlist URL"
/>
</div>
<div>
<label htmlFor="playlistFile" className="block text-sm font-medium mb-1">
M3U Playlist File
</label>
<input
type="file"
id="playlistFile"
onChange={handlePlaylistFileChange}
className="w-full bg-gray-700 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
accept=".m3u"
/>
</div>
<div className="flex justify-end space-x-3">
{isEditMode && (
<button
@@ -239,4 +288,4 @@ function ChannelModal({ isOpen, onClose, channel }: ChannelModalProps) {
);
}
export default ChannelModal;
export default ChannelModal;

View File

@@ -36,6 +36,25 @@ const apiService = {
throw error;
}
},
async uploadPlaylist(data: FormData | { playlistUrl: string }): Promise<void> {
try {
const options: RequestInit = {
method: 'POST',
body: data instanceof FormData ? data : JSON.stringify(data),
headers: data instanceof FormData ? {} : { 'Content-Type': 'application/json' },
};
const response = await fetch(`${API_BASE_URL}/api/playlist`, options);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
} catch (error) {
console.error('Error uploading playlist:', error);
throw error;
}
},
};
export default apiService;

View File

@@ -95,6 +95,13 @@ class SocketService {
this.socket.emit('update-channel', { id, updatedAttributes });
}
// Playlist hochladen
uploadPlaylist(data: FormData | { playlistUrl: string }) {
if (!this.socket) throw new Error('Socket is not connected.');
this.socket.emit('upload-playlist', data);
}
}
const socketService = new SocketService();