feat: auto update playlist option

This commit is contained in:
antebrl
2025-01-18 00:14:55 +00:00
parent 96e4ab927b
commit 456f97e1c5
12 changed files with 150 additions and 12 deletions

View File

@@ -1,6 +1,6 @@
class Channel {
static nextId = 0;
constructor(name, url, avatar, mode, headers = [], group = null, playlist = null, playlistName = null) {
constructor(name, url, avatar, mode, headers = [], group = null, playlist = null, playlistName = null, playlistUpdate = false) {
this.id = Channel.nextId++;
this.name = name;
this.url = url;
@@ -11,6 +11,7 @@ class Channel {
this.group = group;
this.playlist = playlist;
this.playlistName = playlistName;
this.playlistUpdate = this.playlistUpdate;
}
restream() {

View File

@@ -0,0 +1,16 @@
class Playlist {
static nextId = 0;
constructor(playlist, playlistName, mode, playlistUpdate, headers = []) {
this.headers = headers;
this.mode = mode;
this.playlist = playlist;
this.playlistName = playlistName;
this.playlistUpdate = playlistUpdate;
}
static from(json){
return Object.assign(new Playlist(), json);
}
}
module.exports = Playlist;

View File

@@ -13,6 +13,7 @@
"dotenv": "^16.4.5",
"express": "^4.21.1",
"iptv-playlist-parser": "^0.13.0",
"node-cron": "^3.0.3",
"request": "^2.88.2",
"socket.io": "^4.8.1"
}
@@ -868,6 +869,25 @@
"node": ">= 0.6"
}
},
"node_modules/node-cron": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz",
"integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==",
"dependencies": {
"uuid": "8.3.2"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/node-cron/node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/oauth-sign": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",

View File

@@ -23,6 +23,7 @@
"dotenv": "^16.4.5",
"express": "^4.21.1",
"iptv-playlist-parser": "^0.13.0",
"node-cron": "^3.0.3",
"request": "^2.88.2",
"socket.io": "^4.8.1"
}

View File

@@ -10,6 +10,7 @@ const channelController = require('./controllers/ChannelController');
const streamController = require('./services/restream/StreamController');
const ChannelService = require('./services/ChannelService');
const PlaylistSocketHandler = require('./socket/PlaylistSocketHandler');
const PlaylistUpdater = require('./services/PlaylistUpdater');
dotenv.config();
@@ -41,6 +42,7 @@ const server = app.listen(PORT, async () => {
if (ChannelService.getCurrentChannel().restream()) {
await streamController.start(ChannelService.getCurrentChannel());
}
PlaylistUpdater.startScheduler();
});

View File

@@ -29,7 +29,7 @@ class ChannelService {
return filtered;
}
addChannel({ name, url, avatar, mode, headersJson, group = null, playlist = null, playlistName = null }, save = true) {
addChannel({ name, url, avatar, mode, headersJson, group = null, playlist = null, playlistName = null, playlistUpdate = false }, save = true) {
// const existing = this.channels.find(channel => channel.url === url);
// if (existing) {
// throw new Error('Channel already exists');
@@ -42,7 +42,7 @@ class ChannelService {
} catch (error) {
}
const newChannel = new Channel(name, url, avatar, mode, headers, group, playlist, playlistName);
const newChannel = new Channel(name, url, avatar, mode, headers, group, playlist, playlistName, playlistUpdate);
this.channels.push(newChannel);
if(save) ChannelStorage.save(this.channels);

View File

@@ -5,7 +5,7 @@ const StreamedSuSession = require('./session/StreamedSuSession');
class PlaylistService {
async addPlaylist(playlist, playlistName, mode, headersJson) {
async addPlaylist(playlist, playlistName, mode, playlistUpdate, headersJson) {
console.log('Adding playlist', playlist);
@@ -43,7 +43,8 @@ class PlaylistService {
headersJson: headersJson,
group: channel.group.title,
playlist: playlist,
playlistName: playlistName
playlistName: playlistName,
playlistUpdate: playlistUpdate
}, false);
} catch (error) {
console.error(error);

View File

@@ -0,0 +1,56 @@
const cron = require('node-cron');
const PlaylistService = require('./PlaylistService');
class PlaylistUpdater {
constructor() {
this.playlists = new Set();
}
contains(playlist) {
for (const existingPlaylist of this.playlists) {
if (existingPlaylist.playlist === playlist) {
return true;
}
}
return false;
}
register(playlist) {
console.log('Registering playlist:', playlist);
this.playlists.add(playlist);
}
delete(playlist) {
for (const existingPlaylist of this.playlists) {
if (existingPlaylist.playlist === playlist) {
this.playlists.delete(existingPlaylist);
break;
}
}
}
startScheduler() {
// Cron-Job: "0 3 * * *" -> Every day at 3 am
cron.schedule('0 3 * * *', () => {
this.updatePlaylists();
});
}
updatePlaylists() {
console.log('Updating playlists at:', new Date());
this.playlists.forEach(async (playlist) => {
try {
//Fetch playlist again
await PlaylistService.deletePlaylist(playlist.playlist);
await PlaylistService.addPlaylist(playlist.playlist, playlist.playlistName, playlist.mode, playlist.playlistUpdate, playlist.headers);
} catch (error) {
console.error(`Error while updating playlist ${playlist.name}:`, error);
}
});
}
}
module.exports = new PlaylistUpdater();

View File

@@ -1,16 +1,22 @@
const PlaylistService = require('../services/PlaylistService');
const ChannelService = require('../services/ChannelService');
const Channel = require('../models/Channel');
const PlaylistUpdater = require('../services/PlaylistUpdater');
const Playlist = require('../models/Playlist');
async function handleAddPlaylist({ playlist, playlistName, mode, headers }, io, socket) {
async function handleAddPlaylist({ playlist, playlistName, mode, playlistUpdate, headers }, io, socket) {
try {
const channels = await PlaylistService.addPlaylist(playlist, playlistName, mode, headers);
const channels = await PlaylistService.addPlaylist(playlist, playlistName, mode, playlistUpdate, headers);
if (channels) {
channels.forEach(channel => {
io.emit('channel-added', channel);
});
}
if(playlistUpdate && !PlaylistUpdater.contains(playlist)) {
PlaylistUpdater.register(new Playlist(playlist, playlistName, mode, playlistUpdate, headers));
}
} catch (err) {
console.error(err);
socket.emit('app-error', { message: err.message });
@@ -31,6 +37,14 @@ async function handleUpdatePlaylist({ playlist, updatedAttributes }, io, socket)
channels.forEach(channel => {
io.emit('channel-updated', channel);
});
if(PlaylistUpdater.contains(playlist)) {
PlaylistUpdater.delete(playlist);
}
if(updatedAttributes.playlistUpdate) {
PlaylistUpdater.register(new Playlist(playlist, updatedAttributes.playlistName, updatedAttributes.mode, updatedAttributes.playlistUpdate, updatedAttributes.headers));
}
} catch (err) {
console.error(err);
socket.emit('app-error', { message: err.message });
@@ -45,6 +59,10 @@ async function handleDeletePlaylist(playlist, io, socket) {
io.emit('channel-deleted', channel.id);
});
io.emit('channel-selected', ChannelService.getCurrentChannel());
if(PlaylistUpdater.contains(playlist)) {
PlaylistUpdater.delete(playlist);
}
} catch (err) {
console.error(err);
socket.emit('app-error', { message: err.message });

View File

@@ -12,7 +12,7 @@ interface ChannelModalProps {
}
function ChannelModal({ onClose, channel }: ChannelModalProps) {
const [type, setType] = useState<'channel' | 'playlist'>('channel');
const [type, setType] = useState<'channel' | 'playlist'>('playlist');
const [isEditMode, setIsEditMode] = useState(false);
const [inputMethod, setInputMethod] = useState<'url' | 'text'>('url');
@@ -25,6 +25,7 @@ function ChannelModal({ onClose, channel }: ChannelModalProps) {
const [playlistName, setPlaylistName] = useState('');
const [playlistUrl, setPlaylistUrl] = useState('');
const [playlistText, setPlaylistText] = useState('');
const [playlistUpdate, setPlaylistUpdate] = useState(false);
const { addToast } = useContext(ToastContext);
@@ -36,6 +37,7 @@ function ChannelModal({ onClose, channel }: ChannelModalProps) {
setMode(channel.mode);
setHeaders(channel.headers);
setPlaylistName(channel.playlistName);
setPlaylistUpdate(channel.playlistUpdate);
setIsEditMode(true);
setType('channel');
@@ -62,8 +64,9 @@ function ChannelModal({ onClose, channel }: ChannelModalProps) {
setPlaylistName('');
setPlaylistUrl('');
setPlaylistText('');
setPlaylistUpdate(false);
setIsEditMode(false);
setType('channel');
setType('playlist');
setInputMethod('url');
}
}, [channel]);
@@ -107,6 +110,7 @@ function ChannelModal({ onClose, channel }: ChannelModalProps) {
inputMethod === 'url' ? playlistUrl.trim() : playlistText.trim(),
playlistName.trim(),
mode,
playlistUpdate,
JSON.stringify(headers)
);
}
@@ -134,6 +138,7 @@ function ChannelModal({ onClose, channel }: ChannelModalProps) {
socketService.updatePlaylist(channel!.playlist, {
playlist: newPlaylist,
playlistName: playlistName.trim(),
playlistUpdate: playlistUpdate,
mode: mode,
headers: headers,
});
@@ -411,6 +416,23 @@ function ChannelModal({ onClose, channel }: ChannelModalProps) {
</label>
</div>
</div>
{/* Playlist auto-update toggle */}
<div className="flex items-center justify-between">
<div>
<label className="block text-sm font-medium">Playlist Auto Update</label>
<p className="text-sm text-gray-400">Automatically update playlist once a day</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
className="sr-only peer"
checked={playlistUpdate}
onChange={(e) => setPlaylistUpdate(e.target.checked)}
/>
<div className="w-11 h-6 bg-gray-600 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800 rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-500"></div>
</label>
</div>
</>
)}

View File

@@ -98,10 +98,10 @@ class SocketService {
}
// Add playlist
addPlaylist(playlist: string, playlistName: string, mode: ChannelMode, headers: string) {
addPlaylist(playlist: string, playlistName: string, mode: ChannelMode, playlistUpdate: boolean, headers: string) {
if (!this.socket) throw new Error('Socket is not connected.');
this.socket.emit('add-playlist', { playlist, playlistName, mode, headers });
this.socket.emit('add-playlist', { playlist, playlistName, mode, playlistUpdate, headers });
}
// Update playlist

View File

@@ -30,6 +30,7 @@ export interface Channel {
group: string;
playlist: string;
playlistName: string;
playlistUpdate: boolean;
}
export interface ChatMessage {