mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 20:16:15 -08:00
Merge pull request #648 from 4gray/fix/m3u-auto-refresh-mechanism
fix/m3u-auto-refresh-mechanism
This commit is contained in:
10 files changed
+228
-85
No files matched your search
@@ -47,7 +47,7 @@ contextBridge.exposeInMainWorld('electron', {
|
||||
origin
|
||||
),
|
||||
autoUpdatePlaylists: (playlists) =>
|
||||
ipcRenderer.invoke('AUTO_UPDATE_PLAYLISTS', playlists),
|
||||
ipcRenderer.invoke('AUTO_UPDATE', playlists),
|
||||
fetchEpg: (urls: string[]) =>
|
||||
ipcRenderer.invoke('FETCH_EPG', { url: urls }),
|
||||
getChannelPrograms: (channelId: string) =>
|
||||
|
||||
@@ -18,29 +18,63 @@ export default class PlaylistEvents {
|
||||
|
||||
const https = require('https');
|
||||
|
||||
/**
|
||||
* Fetches and parses a playlist from a URL
|
||||
* @param url - The URL to fetch the playlist from
|
||||
* @param title - Optional title for the playlist
|
||||
* @returns Parsed playlist object
|
||||
*/
|
||||
async function fetchPlaylistFromUrl(
|
||||
url: string,
|
||||
title?: string
|
||||
): Promise<any> {
|
||||
const agent = new https.Agent({
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
const result = await axios.get(url, { httpsAgent: agent });
|
||||
const parsedPlaylist = parse(result.data);
|
||||
|
||||
const extractedName =
|
||||
url && url.length > 1 ? getFilenameFromUrl(url) : '';
|
||||
const playlistName =
|
||||
!extractedName || extractedName === 'Untitled playlist'
|
||||
? 'Imported from URL'
|
||||
: extractedName;
|
||||
|
||||
const playlistObject = createPlaylistObject(
|
||||
title ?? playlistName,
|
||||
parsedPlaylist,
|
||||
url,
|
||||
'URL'
|
||||
);
|
||||
|
||||
return playlistObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads and parses a playlist from a file path
|
||||
* @param filePath - The path to the playlist file
|
||||
* @param title - Title for the playlist
|
||||
* @returns Parsed playlist object
|
||||
*/
|
||||
async function fetchPlaylistFromFile(
|
||||
filePath: string,
|
||||
title: string
|
||||
): Promise<any> {
|
||||
const fileContent = await readFile(filePath, 'utf-8');
|
||||
const parsedPlaylist = parse(fileContent);
|
||||
const playlistObject = createPlaylistObject(
|
||||
title,
|
||||
parsedPlaylist,
|
||||
filePath,
|
||||
'FILE'
|
||||
);
|
||||
return playlistObject;
|
||||
}
|
||||
|
||||
ipcMain.handle('fetch-playlist-by-url', async (event, url, title?: string) => {
|
||||
try {
|
||||
const agent = new https.Agent({
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
const result = await axios.get(url, { httpsAgent: agent });
|
||||
const parsedPlaylist = parse(result.data);
|
||||
|
||||
const extractedName =
|
||||
url && url.length > 1 ? getFilenameFromUrl(url) : '';
|
||||
const playlistName =
|
||||
!extractedName || extractedName === 'Untitled playlist'
|
||||
? 'Imported from URL'
|
||||
: extractedName;
|
||||
|
||||
const playlistObject = createPlaylistObject(
|
||||
title ?? playlistName,
|
||||
parsedPlaylist,
|
||||
url,
|
||||
'URL'
|
||||
);
|
||||
|
||||
return playlistObject;
|
||||
return await fetchPlaylistFromUrl(url, title);
|
||||
} catch (error) {
|
||||
console.error('Error fetching playlist:', error);
|
||||
throw error;
|
||||
@@ -50,15 +84,12 @@ ipcMain.handle('fetch-playlist-by-url', async (event, url, title?: string) => {
|
||||
ipcMain.handle(
|
||||
'update-playlist-from-file-path',
|
||||
async (event, filePath, title) => {
|
||||
const playlist = await readFile(filePath, 'utf-8');
|
||||
const parsedPlaylist = parse(playlist);
|
||||
const playlistObject = createPlaylistObject(
|
||||
title,
|
||||
parsedPlaylist,
|
||||
filePath,
|
||||
'FILE'
|
||||
);
|
||||
return playlistObject;
|
||||
try {
|
||||
return await fetchPlaylistFromFile(filePath, title);
|
||||
} catch (error) {
|
||||
console.error('Error reading playlist from file:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -79,28 +110,66 @@ ipcMain.handle('open-playlist-from-file', async () => {
|
||||
const filePath = filePaths[0];
|
||||
|
||||
try {
|
||||
const fileContent = await readFile(filePath, 'utf-8');
|
||||
|
||||
const parsedPlaylist = parse(fileContent);
|
||||
const playlistObject = createPlaylistObject(
|
||||
'from file',
|
||||
parsedPlaylist,
|
||||
filePath,
|
||||
'FILE'
|
||||
);
|
||||
|
||||
return playlistObject;
|
||||
return await fetchPlaylistFromFile(filePath, 'from file');
|
||||
} catch (error) {
|
||||
console.error('Error reading or parsing the file:', error);
|
||||
throw new Error('Failed to process the selected file.');
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle(AUTO_UPDATE_PLAYLISTS, async (event, playlistUrls) => {
|
||||
// TODO: Implement auto-update logic
|
||||
for (const url of playlistUrls) {
|
||||
console.log(`Auto-updating playlist from ${url}`);
|
||||
ipcMain.handle(AUTO_UPDATE_PLAYLISTS, async (event, playlists) => {
|
||||
console.log(`Auto-updating ${playlists.length} playlist(s)...`);
|
||||
|
||||
const updatedPlaylists = [];
|
||||
|
||||
for (const playlist of playlists) {
|
||||
try {
|
||||
let playlistObject;
|
||||
|
||||
if (playlist.importDate && playlist.url) {
|
||||
// Update from URL
|
||||
console.log(
|
||||
`Updating playlist "${playlist.title}" from URL: ${playlist.url}`
|
||||
);
|
||||
playlistObject = await fetchPlaylistFromUrl(
|
||||
playlist.url,
|
||||
playlist.title
|
||||
);
|
||||
} else if (playlist.filePath) {
|
||||
// Update from file path
|
||||
console.log(
|
||||
`Updating playlist "${playlist.title}" from file: ${playlist.filePath}`
|
||||
);
|
||||
playlistObject = await fetchPlaylistFromFile(
|
||||
playlist.filePath,
|
||||
playlist.title
|
||||
);
|
||||
} else {
|
||||
console.warn(
|
||||
`Skipping playlist "${playlist.title}": no URL or file path found`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Preserve the original _id and autoRefresh setting
|
||||
updatedPlaylists.push({
|
||||
...playlistObject,
|
||||
_id: playlist._id,
|
||||
autoRefresh: playlist.autoRefresh,
|
||||
});
|
||||
|
||||
console.log(`Successfully updated playlist "${playlist.title}"`);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to update playlist "${playlist.title}":`,
|
||||
error
|
||||
);
|
||||
// Continue with other playlists even if one fails
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Auto-update completed: ${updatedPlaylists.length} updated`);
|
||||
return updatedPlaylists;
|
||||
});
|
||||
|
||||
ipcMain.handle('save-file-dialog', async (event, defaultPath, filters) => {
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { Component, inject, OnInit } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { Router, RouterOutlet } from '@angular/router';
|
||||
import { Actions, ofType } from '@ngrx/effects';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { filter, take } from 'rxjs';
|
||||
/* import * as semver from 'semver'; */
|
||||
import * as PlaylistActions from 'm3u-state';
|
||||
import { selectAllPlaylistsMeta } from 'm3u-state';
|
||||
import { DataService, EpgService } from 'services';
|
||||
import {
|
||||
AUTO_UPDATE_PLAYLISTS,
|
||||
Language,
|
||||
OPEN_FILE,
|
||||
Settings,
|
||||
@@ -23,6 +27,7 @@ import { SearchResultsComponent } from './xtream-tauri/search-results/search-res
|
||||
imports: [RouterOutlet],
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
private actions$ = inject(Actions);
|
||||
private dataService = inject(DataService);
|
||||
private dialog = inject(MatDialog);
|
||||
private epgService = inject(EpgService);
|
||||
@@ -31,12 +36,6 @@ export class AppComponent implements OnInit {
|
||||
private translate = inject(TranslateService);
|
||||
private settingsService = inject(SettingsService);
|
||||
|
||||
/** List of ipc commands with function mapping */
|
||||
/* private readonly commandsList = [
|
||||
new IpcCommand(VIEW_ADD_PLAYLIST, () => this.navigateToRoute('/')),
|
||||
new IpcCommand(VIEW_SETTINGS, () => this.navigateToRoute('/settings')),
|
||||
]; */
|
||||
|
||||
/** Default language as fallback */
|
||||
private readonly DEFAULT_LANG = Language.ENGLISH;
|
||||
|
||||
@@ -80,6 +79,7 @@ export class AppComponent implements OnInit {
|
||||
this.translate.setDefaultLang(this.DEFAULT_LANG);
|
||||
|
||||
this.initSettings();
|
||||
this.triggerAutoUpdatePlaylists();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -106,8 +106,6 @@ export class AppComponent implements OnInit {
|
||||
this.epgService.fetchEpg(settings.epgUrl);
|
||||
}
|
||||
|
||||
// TODO: trigger auto-refresh mechanism for playlists
|
||||
|
||||
if (settings.theme) {
|
||||
this.settingsService.changeTheme(settings.theme);
|
||||
} else {
|
||||
@@ -163,4 +161,42 @@ export class AppComponent implements OnInit {
|
||||
disableClose: false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers auto-update for playlists that have autoRefresh enabled
|
||||
*/
|
||||
private triggerAutoUpdatePlaylists(): void {
|
||||
// Wait for playlists to be loaded successfully
|
||||
this.actions$
|
||||
.pipe(
|
||||
ofType(PlaylistActions.loadPlaylistsSuccess),
|
||||
take(1) // Only trigger once on app startup
|
||||
)
|
||||
.subscribe(() => {
|
||||
// Get all playlists from store
|
||||
this.store
|
||||
.select(selectAllPlaylistsMeta)
|
||||
.pipe(
|
||||
take(1),
|
||||
filter((playlists) => playlists.length > 0)
|
||||
)
|
||||
.subscribe((playlists) => {
|
||||
// Filter playlists with autoRefresh enabled
|
||||
const playlistsToUpdate = playlists.filter(
|
||||
(playlist) => playlist.autoRefresh === true
|
||||
);
|
||||
|
||||
// Trigger auto-update if there are playlists to update
|
||||
if (playlistsToUpdate.length > 0) {
|
||||
console.log(
|
||||
`Auto-updating ${playlistsToUpdate.length} playlist(s) on startup`
|
||||
);
|
||||
this.dataService.sendIpcEvent(
|
||||
AUTO_UPDATE_PLAYLISTS,
|
||||
playlistsToUpdate
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,14 @@
|
||||
import { Overlay, OverlayRef } from '@angular/cdk/overlay';
|
||||
import { ComponentPortal } from '@angular/cdk/portal';
|
||||
import { AsyncPipe, CommonModule } from '@angular/common';
|
||||
import { Component, Injector, OnInit, effect, inject } from '@angular/core';
|
||||
import {
|
||||
Component,
|
||||
InjectionToken,
|
||||
Injector,
|
||||
OnInit,
|
||||
effect,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||
@@ -27,7 +34,6 @@ import {
|
||||
import { Observable, combineLatestWith, filter, map, switchMap } from 'rxjs';
|
||||
import { DataService, PlaylistsService } from 'services';
|
||||
import {
|
||||
COMPONENT_OVERLAY_REF,
|
||||
Channel,
|
||||
PLAYLIST_PARSE_BY_URL,
|
||||
STORE_KEY,
|
||||
@@ -67,6 +73,10 @@ export class VideoPlayerComponent implements OnInit {
|
||||
private readonly storage = inject(StorageMap);
|
||||
private readonly store = inject(Store);
|
||||
|
||||
private readonly COMPONENT_OVERLAY_REF = new InjectionToken<OverlayRef>(
|
||||
'COMPONENT_OVERLAY_REF'
|
||||
);
|
||||
|
||||
/** Active selected channel */
|
||||
readonly activeChannel$ = this.store
|
||||
.select(selectActive)
|
||||
@@ -204,7 +214,7 @@ export class VideoPlayerComponent implements OnInit {
|
||||
const injector = Injector.create({
|
||||
providers: [
|
||||
{
|
||||
provide: COMPONENT_OVERLAY_REF,
|
||||
provide: this.COMPONENT_OVERLAY_REF,
|
||||
useValue: this.overlayRef,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -67,7 +67,8 @@
|
||||
"MY_PLAYLISTS_SUBTITLE": "all available playlists",
|
||||
"PLAYLIST_UPDATE_SUCCESS": "Success! The playlist was successfully updated.",
|
||||
"PLAYLIST_UPDATE_ERROR": "Error updating playlist details.",
|
||||
"AUTO_REFRESH_UPDATE_SUCCESS": "Success! The playlists were successfully updated"
|
||||
"AUTO_REFRESH_UPDATE_SUCCESS": "Success! The playlists were successfully updated",
|
||||
"AUTO_REFRESH_ENABLED": "Auto-refresh enabled"
|
||||
},
|
||||
"FILE_UPLOAD": {
|
||||
"DRAG_DROP": "Drag & drop files here",
|
||||
|
||||
@@ -17,7 +17,6 @@ export * from './lib/stalker-portal-actions.enum';
|
||||
export * from './lib/store-keys.enum';
|
||||
export * from './lib/stream-format.enum';
|
||||
export * from './lib/theme.enum';
|
||||
export * from './lib/tokens';
|
||||
export * from './lib/xtream-category.interface';
|
||||
export * from './lib/xtream-code-actions';
|
||||
export * from './lib/xtream-item.interface';
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { InjectionToken } from '@angular/core';
|
||||
|
||||
export const COMPONENT_OVERLAY_REF = new InjectionToken(
|
||||
'COMPONENT_OVERLAY_REF'
|
||||
);
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Component,
|
||||
ElementRef,
|
||||
inject,
|
||||
InjectionToken,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
@@ -20,12 +21,7 @@ import { MomentDatePipe } from '@iptvnator/pipes';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { addDays, differenceInMinutes, format, parse, subDays } from 'date-fns';
|
||||
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
|
||||
import {
|
||||
Channel,
|
||||
COMPONENT_OVERLAY_REF,
|
||||
EpgChannel,
|
||||
EpgProgram,
|
||||
} from 'shared-interfaces';
|
||||
import { Channel, EpgChannel, EpgProgram } from 'shared-interfaces';
|
||||
import { EpgItemDescriptionComponent } from '../epg-list/epg-item-description/epg-item-description.component';
|
||||
|
||||
interface EnrichedProgram extends EpgProgram {
|
||||
@@ -93,7 +89,13 @@ export class MultiEpgContainerComponent
|
||||
|
||||
private readonly dialog = inject(MatDialog);
|
||||
private readonly cdr = inject(ChangeDetectorRef);
|
||||
private readonly overlayRef = inject<OverlayRef>(COMPONENT_OVERLAY_REF);
|
||||
private readonly COMPONENT_OVERLAY_REF = new InjectionToken<OverlayRef>(
|
||||
'COMPONENT_OVERLAY_REF'
|
||||
);
|
||||
|
||||
private readonly overlayRef = inject<OverlayRef>(
|
||||
this.COMPONENT_OVERLAY_REF
|
||||
);
|
||||
|
||||
ngOnInit() {
|
||||
this.calculateCurrentTimeBar();
|
||||
|
||||
+30
-14
@@ -1,21 +1,37 @@
|
||||
<mat-list-item (click)="playlistClicked.emit(item._id)">
|
||||
@if (item.url) {
|
||||
<mat-icon
|
||||
matListItemIcon
|
||||
class="upload-type-icon"
|
||||
[matTooltip]="
|
||||
('HOME.PLAYLISTS.ADDED_VIA_URL' | translate) + ' ' + item.url
|
||||
"
|
||||
>cloud</mat-icon
|
||||
>
|
||||
<div class="icon-container" matListItemIcon>
|
||||
<mat-icon
|
||||
class="upload-type-icon"
|
||||
[matTooltip]="
|
||||
('HOME.PLAYLISTS.ADDED_VIA_URL' | translate) + ' ' + item.url
|
||||
"
|
||||
>cloud</mat-icon
|
||||
>
|
||||
@if (item.autoRefresh) {
|
||||
<mat-icon
|
||||
class="auto-refresh-indicator"
|
||||
[matTooltip]="'HOME.PLAYLISTS.AUTO_REFRESH_ENABLED' | translate"
|
||||
>autorenew</mat-icon
|
||||
>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (!item.url && !item.serverUrl && !item.macAddress) {
|
||||
<mat-icon
|
||||
matListItemIcon
|
||||
class="upload-type-icon"
|
||||
[matTooltip]="'HOME.PLAYLISTS.ADDED_VIA_FILE' | translate"
|
||||
>folder</mat-icon
|
||||
>
|
||||
<div class="icon-container" matListItemIcon>
|
||||
<mat-icon
|
||||
class="upload-type-icon"
|
||||
[matTooltip]="'HOME.PLAYLISTS.ADDED_VIA_FILE' | translate"
|
||||
>folder</mat-icon
|
||||
>
|
||||
@if (item.autoRefresh) {
|
||||
<mat-icon
|
||||
class="auto-refresh-indicator"
|
||||
[matTooltip]="'HOME.PLAYLISTS.AUTO_REFRESH_ENABLED' | translate"
|
||||
>autorenew</mat-icon
|
||||
>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (item.serverUrl) {
|
||||
<div class="icon-container" matListItemIcon>
|
||||
|
||||
+15
@@ -84,6 +84,21 @@ mat-list-item {
|
||||
background-color: #444444;
|
||||
}
|
||||
|
||||
.auto-refresh-indicator {
|
||||
position: absolute;
|
||||
right: -2px;
|
||||
bottom: -2px;
|
||||
font-size: 14px !important;
|
||||
width: 14px !important;
|
||||
height: 14px !important;
|
||||
color: #4caf50;
|
||||
background-color: var(--background-color, #fff);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 480px) {
|
||||
.upload-type-icon,
|
||||
.icon-container {
|
||||
|
||||
Reference in new issue
Block a user