feat: add m3u playlists
This commit is contained in:
@@ -1,6 +1,4 @@
|
||||
const ChannelService = require('../services/ChannelService');
|
||||
const fs = require('fs');
|
||||
const m3uParser = require('m3u8-parser');
|
||||
|
||||
module.exports = {
|
||||
getChannels(req, res) {
|
||||
@@ -33,8 +31,8 @@ module.exports = {
|
||||
|
||||
addChannel(req, res) {
|
||||
try {
|
||||
const { name, url, avatar, restream, headersJson } = req.body;
|
||||
const newChannel = ChannelService.addChannel(name, url, avatar, restream, headersJson);
|
||||
const { name, url, avatar, restream, headersJson, group } = req.body;
|
||||
const newChannel = ChannelService.addChannel(name, url, avatar, restream, headersJson, group);
|
||||
res.status(201).json(newChannel);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
class Channel {
|
||||
static nextId = 0;
|
||||
constructor(name, url, avatar, restream, headers) {
|
||||
constructor(name, url, avatar, restream, headers, group) {
|
||||
this.id = Channel.nextId++;
|
||||
this.name = name;
|
||||
this.url = url;
|
||||
this.avatar = avatar;
|
||||
this.restream = restream;
|
||||
this.headers = headers;
|
||||
this.group = group;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
59
backend/package-lock.json
generated
59
backend/package-lock.json
generated
@@ -12,6 +12,7 @@
|
||||
"child_process": "^1.0.2",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.21.1",
|
||||
"iptv-playlist-parser": "^0.13.0",
|
||||
"socket.io": "^4.8.1"
|
||||
}
|
||||
},
|
||||
@@ -512,6 +513,56 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/iptv-playlist-parser": {
|
||||
"version": "0.13.0",
|
||||
"resolved": "https://registry.npmjs.org/iptv-playlist-parser/-/iptv-playlist-parser-0.13.0.tgz",
|
||||
"integrity": "sha512-As51+8A7AcFzV9Y8mt30TIbRkBn6l0TGuL9lIG2bPcqb+YYRVzfjsqqugz3eWbEmziEKEsLzexnqPSO7ZzQc0A==",
|
||||
"dependencies": {
|
||||
"is-valid-path": "^0.1.1",
|
||||
"validator": "^13.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
|
||||
"integrity": "sha512-7Q+VbVafe6x2T+Tu6NcOf6sRklazEPmBoB3IWk3WdGZM2iGUwU/Oe3Wtq5lSEkDTTlpp8yx+5t4pzO/i9Ty1ww==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-glob": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
|
||||
"integrity": "sha512-a1dBeB19NXsf/E0+FHqkagizel/LQw2DjSQpvQrj3zT+jYPpaUCryPnrQajXKFLCMuf4I6FhRpaGtw4lPrG6Eg==",
|
||||
"dependencies": {
|
||||
"is-extglob": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-invalid-path": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-invalid-path/-/is-invalid-path-0.1.0.tgz",
|
||||
"integrity": "sha512-aZMG0T3F34mTg4eTdszcGXx54oiZ4NtHSft3hWNJMGJXUUqdIj3cOZuHcU0nCWWcY3jd7yRe/3AEm3vSNTpBGQ==",
|
||||
"dependencies": {
|
||||
"is-glob": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-valid-path": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-valid-path/-/is-valid-path-0.1.1.tgz",
|
||||
"integrity": "sha512-+kwPrVDu9Ms03L90Qaml+79+6DZHqHyRoANI6IsZJ/g8frhnfchDOBCa0RbQ6/kdHt5CS5OeIEyrYznNuVN+8A==",
|
||||
"dependencies": {
|
||||
"is-invalid-path": "^0.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
@@ -932,6 +983,14 @@
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/validator": {
|
||||
"version": "13.12.0",
|
||||
"resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz",
|
||||
"integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"child_process": "^1.0.2",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.21.1",
|
||||
"iptv-playlist-parser": "^0.13.0",
|
||||
"socket.io": "^4.8.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
const streamController = require('./streaming/StreamController');
|
||||
const Channel = require('../models/Channel');
|
||||
const m3uParser = require('@pawanpaudel93/m3u-parse');
|
||||
const fetch = require('node-fetch');
|
||||
const m3uParser = require('iptv-playlist-parser');
|
||||
|
||||
class ChannelService {
|
||||
constructor() {
|
||||
@@ -20,14 +19,14 @@ class ChannelService {
|
||||
|
||||
this.channels = [
|
||||
//Some Test-channels to get started
|
||||
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, []),
|
||||
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),
|
||||
];
|
||||
this.currentChannel = this.channels[0];
|
||||
}
|
||||
@@ -36,7 +35,7 @@ class ChannelService {
|
||||
return this.channels;
|
||||
}
|
||||
|
||||
addChannel(name, url, avatar, restream, headersJson) {
|
||||
addChannel(name, url, avatar, restream, headersJson, group) {
|
||||
const existing = this.channels.find(channel => channel.url === url);
|
||||
|
||||
if (existing && restream == existing.restream) {
|
||||
@@ -44,7 +43,7 @@ class ChannelService {
|
||||
}
|
||||
|
||||
const headers = JSON.parse(headersJson);
|
||||
const newChannel = new Channel(name, url, avatar, restream, headers);
|
||||
const newChannel = new Channel(name, url, avatar, restream, headers, group);
|
||||
this.channels.push(newChannel);
|
||||
|
||||
return newChannel;
|
||||
@@ -121,25 +120,18 @@ class ChannelService {
|
||||
return channel;
|
||||
}
|
||||
|
||||
async addChannelsFromPlaylist(playlistUrl) {
|
||||
async addChannelsFromPlaylist(playlistUrl, restream, headersJson) {
|
||||
|
||||
const response = await fetch(playlistUrl);
|
||||
const playlistContent = await response.text();
|
||||
const parser = new m3uParser.Parser();
|
||||
parser.push(playlistContent);
|
||||
parser.end();
|
||||
const content = await response.text();
|
||||
|
||||
const parsedPlaylist = parser.manifest;
|
||||
const channels = parsedPlaylist.segments.map(segment => ({
|
||||
name: segment.title,
|
||||
url: segment.uri,
|
||||
avatar: '',
|
||||
restream: false,
|
||||
headersJson: '[]'
|
||||
}));
|
||||
const parsedPlaylist = m3uParser.parse(content);
|
||||
|
||||
channels.forEach(channel => {
|
||||
this.addChannel(channel.name, channel.url, channel.avatar, channel.restream, channel.headersJson);
|
||||
});
|
||||
// list of added channels
|
||||
const channels = parsedPlaylist.items.map(channel =>
|
||||
//TODO: add channel.http if not '' to headers
|
||||
this.addChannel(channel.name, channel.url, channel.tvg.logo, restream, headersJson, channel.group.title)
|
||||
);
|
||||
|
||||
return channels;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ module.exports = (io, socket) => {
|
||||
|
||||
socket.on('add-channel', ({ name, url, avatar, restream, headersJson}) => {
|
||||
try {
|
||||
const newChannel = ChannelService.addChannel(name, url, avatar, restream, headersJson);
|
||||
const newChannel = ChannelService.addChannel(name, url, avatar, restream, headersJson, null);
|
||||
io.emit('channel-added', newChannel); // Broadcast to all clients
|
||||
} catch (err) {
|
||||
socket.emit('app-error', { message: err.message });
|
||||
@@ -43,18 +43,10 @@ module.exports = (io, socket) => {
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('upload-playlist', async (data) => {
|
||||
socket.on('upload-playlist', async ({ playlistUrl, restream, headersJson }) => {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
channels = await ChannelService.addChannelsFromPlaylist(playlistUrl, restream, headersJson);
|
||||
if (channels) {
|
||||
channels.forEach(channel => {
|
||||
io.emit('channel-added', channel);
|
||||
|
||||
@@ -11,14 +11,16 @@ interface ChannelModalProps {
|
||||
}
|
||||
|
||||
function ChannelModal({ isOpen, onClose, channel }: ChannelModalProps) {
|
||||
const [mode, setMode] = useState<'channel' | 'playlist'>('channel');
|
||||
const [isEditMode, setIsEditMode] = useState(false);
|
||||
|
||||
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);
|
||||
|
||||
const [playlistUrl, setPlaylistUrl] = useState('');
|
||||
const [playlistFile, setPlaylistFile] = useState<File | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (channel) {
|
||||
@@ -28,20 +30,29 @@ function ChannelModal({ isOpen, onClose, channel }: ChannelModalProps) {
|
||||
setRestream(channel.restream);
|
||||
setHeaders(channel.headers);
|
||||
setIsEditMode(true);
|
||||
} else {
|
||||
setName('');
|
||||
setUrl('');
|
||||
setAvatar('');
|
||||
setRestream(false);
|
||||
setHeaders([]);
|
||||
setIsEditMode(false);
|
||||
setMode('channel'); // Default to "channel" if a channel object exists
|
||||
}
|
||||
}, [channel]);
|
||||
|
||||
|
||||
const addHeader = () => {
|
||||
setHeaders([...headers, { key: '', value: '' }]);
|
||||
};
|
||||
const removeHeader = (index: number) => {
|
||||
setHeaders(headers.filter((_, i) => i !== index));
|
||||
};
|
||||
const updateHeader = (index: number, field: 'key' | 'value', value: string) => {
|
||||
const newHeaders = [...headers];
|
||||
newHeaders[index] = { ...newHeaders[index], [field]: value };
|
||||
setHeaders(newHeaders);
|
||||
};
|
||||
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!name.trim() || !url.trim()) return;
|
||||
|
||||
if (mode === 'channel') {
|
||||
if (!name.trim() || !url.trim()) return;
|
||||
if (isEditMode && channel) {
|
||||
handleUpdate(channel.id);
|
||||
} else {
|
||||
@@ -53,27 +64,14 @@ function ChannelModal({ isOpen, onClose, channel }: ChannelModalProps) {
|
||||
JSON.stringify(headers)
|
||||
);
|
||||
}
|
||||
|
||||
if (playlistUrl.trim()) {
|
||||
socketService.uploadPlaylist({ playlistUrl: playlistUrl.trim() });
|
||||
}
|
||||
|
||||
if (playlistFile) {
|
||||
const formData = new FormData();
|
||||
formData.append('playlistFile', playlistFile);
|
||||
socketService.uploadPlaylist(formData);
|
||||
} else if (mode === 'playlist') {
|
||||
if (!playlistUrl.trim()) return;
|
||||
socketService.uploadPlaylist(playlistUrl.trim(), restream, JSON.stringify(headers));
|
||||
}
|
||||
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
if (channel) {
|
||||
socketService.deleteChannel(channel.id);
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdate = (id: number) => {
|
||||
socketService.updateChannel(id, {
|
||||
name: name.trim(),
|
||||
@@ -85,37 +83,16 @@ function ChannelModal({ isOpen, onClose, channel }: ChannelModalProps) {
|
||||
onClose();
|
||||
};
|
||||
|
||||
const addHeader = () => {
|
||||
setHeaders([...headers, { key: '', value: '' }]);
|
||||
};
|
||||
|
||||
const removeHeader = (index: number) => {
|
||||
setHeaders(headers.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const updateHeader = (index: number, field: 'key' | 'value', value: string) => {
|
||||
const newHeaders = [...headers];
|
||||
newHeaders[index] = { ...newHeaders[index], [field]: value };
|
||||
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 (
|
||||
<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">
|
||||
{/* Header mit Slider */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-700">
|
||||
<h2 className="text-xl font-semibold">{isEditMode ? 'Edit Channel' : 'Add New Channel'}</h2>
|
||||
<h2 className="text-xl font-semibold">
|
||||
{isEditMode ? (mode === 'channel' ? 'Edit Channel' : 'Edit Playlist') : mode === 'channel' ? 'Add New Channel' : 'Add New Playlist'}
|
||||
</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1 hover:bg-gray-700 rounded-full transition-colors"
|
||||
@@ -124,7 +101,35 @@ function ChannelModal({ isOpen, onClose, channel }: ChannelModalProps) {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Slider */}
|
||||
{!isEditMode && (
|
||||
<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`}
|
||||
>
|
||||
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`}
|
||||
>
|
||||
Playlist
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Form */}
|
||||
<form onSubmit={handleSubmit} className="p-4 space-y-4">
|
||||
{mode === 'channel' && (
|
||||
<>
|
||||
{/* Channel fields */}
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium mb-1">
|
||||
Channel Name
|
||||
@@ -138,7 +143,6 @@ function ChannelModal({ isOpen, onClose, channel }: ChannelModalProps) {
|
||||
placeholder="Enter channel name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="url" className="block text-sm font-medium mb-1">
|
||||
Stream URL
|
||||
@@ -152,9 +156,8 @@ function ChannelModal({ isOpen, onClose, channel }: ChannelModalProps) {
|
||||
placeholder="Enter stream URL"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="url" className="block text-sm font-medium mb-1">
|
||||
<label htmlFor="avatar" className="block text-sm font-medium mb-1">
|
||||
Avatar URL
|
||||
</label>
|
||||
<input
|
||||
@@ -166,7 +169,6 @@ function ChannelModal({ isOpen, onClose, channel }: ChannelModalProps) {
|
||||
placeholder="Enter channel avatar URL"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Restream through backend</label>
|
||||
<div className="flex items-center space-x-4">
|
||||
@@ -194,7 +196,56 @@ function ChannelModal({ isOpen, onClose, channel }: ChannelModalProps) {
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{mode === 'playlist' && (
|
||||
<>
|
||||
{/* Playlist fields */}
|
||||
<div>
|
||||
<label htmlFor="playlistUrl" className="block text-sm font-medium mb-1">
|
||||
M3U Playlist URL
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
id="playlistUrl"
|
||||
value={playlistUrl}
|
||||
onChange={(e) => setPlaylistUrl(e.target.value)}
|
||||
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 className="block text-sm font-medium mb-1">Restream through backend</label>
|
||||
<div className="flex items-center space-x-4">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
name="restream"
|
||||
value="yes"
|
||||
checked={restream}
|
||||
className="form-radio text-blue-600"
|
||||
onChange={() => setRestream(true)}
|
||||
/>
|
||||
<span className="ml-2">Yes</span>
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
name="restream"
|
||||
value="no"
|
||||
className="form-radio text-blue-600"
|
||||
checked={!restream}
|
||||
onChange={() => setRestream(false)}
|
||||
/>
|
||||
<span className="ml-2">No</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Custom Headers */}
|
||||
{restream && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -231,38 +282,12 @@ 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>
|
||||
|
||||
{/* Buttons */}
|
||||
<div className="flex justify-end space-x-3">
|
||||
{isEditMode && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDelete}
|
||||
onClick={() => channel && handleUpdate(channel.id)}
|
||||
className="px-4 py-2 bg-red-600 rounded-lg hover:bg-red-700 transition-colors"
|
||||
>
|
||||
Delete
|
||||
@@ -279,7 +304,7 @@ function ChannelModal({ isOpen, onClose, channel }: ChannelModalProps) {
|
||||
type="submit"
|
||||
className="px-4 py-2 bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
{isEditMode ? 'Update Channel' : 'Add Channel'}
|
||||
{isEditMode ? 'Update' : 'Add'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -97,10 +97,10 @@ class SocketService {
|
||||
}
|
||||
|
||||
// Playlist hochladen
|
||||
uploadPlaylist(data: FormData | { playlistUrl: string }) {
|
||||
uploadPlaylist(playlistUrl: string, restream: boolean, headersJson: string ) {
|
||||
if (!this.socket) throw new Error('Socket is not connected.');
|
||||
|
||||
this.socket.emit('upload-playlist', data);
|
||||
this.socket.emit('upload-playlist', { playlistUrl, restream, headersJson });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user