Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

32 edit and delete playlist #33

Merged
merged 2 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# IPTV StreamHub

A simple IPTV `restream` and `synchronization` application with web frontend. Share your iptv playlist and watch it together with your friends.

A simple IPTV `restream` and `synchronization` (watch2gether) application with web frontend. Share your iptv playlist and watch it together with your friends.

## ✨ Features
**Restreaming** - Proxy your iptv streams through the backend.
Expand All @@ -19,6 +18,7 @@ A simple IPTV `restream` and `synchronization` application with web frontend. Sh
- Helps with CORS issues.
- Synchronize IPTV streaming with multiple devices: Synchronized playback and channel selection.
- Share your iptv and watch together with your friends.
- The actual iptv stream-url is unvisible to them if you restream [upcomming feature]

## 🛠️ Architecture

Expand All @@ -45,7 +45,8 @@ docker compose up -d
```
Open http://localhost

⚠️ Be aware that a restreamed synchonized channel (also some of the preview channels) may take some time to load (20-25s) ⚠️ -> To reduce loading time deactivate synchronization in the ⚙️ or edit the delay in the [config]((docker-compose.yml))!
> [!IMPORTANT]
> If a channel/playlist won't work, please try with `restream through backend` option enabled. This fixes most of the problems! It leads to longer initial loading times. If you don't need synchronization, turn it off in the ⚙️ or set the delay in the [config](docker-compose.yml).


### Run components seperately
Expand All @@ -61,6 +62,12 @@ Be aware, that this'll require additional configuration/adaption and won't be of
![Frontend Preview](/frontend/ressources/frontend-preview.png)
![Add channel](/frontend/ressources/add-channel.png)

## FAQ & Common Mistakes

> Error: `Bind for 0.0.0.0:80 failed: port is already allocated`

To fix this, change the [port mapping in the docker-compose](docker-compose.yml#L40) to `X:80` e.g. `8080:80`. Make also sure that port X is open in the firewall configuration if you want to expose the application.

## Contribute & Contact
Feel free to open discussions and issues for any type of requests. Don't hesitate to contact me, if you have any problems with the setup.

Expand Down
4 changes: 2 additions & 2 deletions backend/controllers/ChannelController.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
3 changes: 2 additions & 1 deletion backend/models/Channel.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
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;
this.avatar = avatar;
this.restream = restream;
this.headers = headers;
this.group = group;
this.playlist = playlist;
}
}

Expand Down
2 changes: 2 additions & 0 deletions backend/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -52,6 +53,7 @@ io.on('connection', socket => {
})

ChannelSocketHandler(io, socket);
PlaylistSocketHandler(io, socket);

ChatSocketHandler(io, socket);
})
43 changes: 10 additions & 33 deletions backend/services/ChannelService.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
const streamController = require('./streaming/StreamController');
const Channel = require('../models/Channel');
const m3uParser = require('iptv-playlist-parser');

class ChannelService {
constructor() {
Expand All @@ -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];
}
Expand All @@ -35,15 +34,15 @@ 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) {
throw new Error('Channel already exists');
}

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;
Expand Down Expand Up @@ -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 changes: 62 additions & 0 deletions backend/services/PlaylistService.js
Original file line number Diff line number Diff line change
@@ -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();
22 changes: 3 additions & 19 deletions backend/socket/ChannelSocketHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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 });
Expand All @@ -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 changes: 47 additions & 0 deletions backend/socket/PlaylistSocketHandler.js
Original file line number Diff line number Diff line change
@@ -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 });
}
});
};
Loading