mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 01:56:16 -08:00
refactor: auto-update mechanism
This commit is contained in:
1 parent
02192a73be
commit
e0c6eb6790
9 files changed
+207
-89
No files matched your search
+100
-65
@@ -1,7 +1,11 @@
|
||||
import axios from 'axios';
|
||||
import { app, BrowserWindow, ipcMain, session } from 'electron';
|
||||
import { promises as fsPromises } from 'fs';
|
||||
import { parse } from 'iptv-playlist-parser';
|
||||
import { Channel } from '../shared/channel.interface';
|
||||
import {
|
||||
AUTO_UPDATE_PLAYLISTS,
|
||||
AUTO_UPDATE_PLAYLISTS_RESPONSE,
|
||||
CHANNEL_SET_USER_AGENT,
|
||||
DELETE_ALL_PLAYLISTS,
|
||||
EPG_ERROR,
|
||||
@@ -25,6 +29,7 @@ import {
|
||||
PLAYLIST_UPDATE,
|
||||
PLAYLIST_UPDATE_RESPONSE,
|
||||
} from '../shared/ipc-commands';
|
||||
import { Playlist } from '../shared/playlist.interface';
|
||||
import { createPlaylistObject } from '../shared/playlist.utils';
|
||||
import { ParsedPlaylist } from '../src/typings.d';
|
||||
|
||||
@@ -162,6 +167,35 @@ export class Api {
|
||||
);
|
||||
});
|
||||
})
|
||||
.on(
|
||||
AUTO_UPDATE_PLAYLISTS,
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
async (event, playlists: Partial<Playlist>[]) => {
|
||||
const results: any[] = [];
|
||||
let playlist: any;
|
||||
for (const element of playlists) {
|
||||
if (element.url && element._id) {
|
||||
playlist = await this.fetchPlaylistByUrl({
|
||||
id: element._id,
|
||||
title: element.title || '',
|
||||
url: element.url,
|
||||
});
|
||||
results.push(playlist);
|
||||
} else if (element.filePath && element._id) {
|
||||
playlist = await this.fetchPlaylistByFilePath({
|
||||
id: element._id,
|
||||
title: element.title || '',
|
||||
filePath: element.filePath,
|
||||
});
|
||||
results.push(playlist);
|
||||
}
|
||||
}
|
||||
event.sender.send(
|
||||
AUTO_UPDATE_PLAYLISTS_RESPONSE,
|
||||
results.filter((item) => item !== undefined)
|
||||
);
|
||||
}
|
||||
)
|
||||
.on(MIGRATE_PLAYLISTS, (event) => {
|
||||
this.getAllPlaylists().then((playlists) => {
|
||||
event.sender.send(MIGRATE_PLAYLISTS_RESPONSE, {
|
||||
@@ -211,33 +245,8 @@ export class Api {
|
||||
.on(EPG_FORCE_FETCH, (event, arg) =>
|
||||
this.workerWindow.webContents.send(EPG_FORCE_FETCH, arg)
|
||||
);
|
||||
|
||||
// this.refreshPlaylists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the update process for all the playlists with the enabled auto-refresh flag
|
||||
*/
|
||||
// TODO: implement the same mechanism for self-hosted PWAs (ignore vercel instance)
|
||||
/* refreshPlaylists(): void {
|
||||
this.getAllPlaylistsMeta().then((playlists) => {
|
||||
playlists.forEach((playlist) => {
|
||||
if (playlist.autoRefresh && playlist.autoRefresh === true) {
|
||||
if (playlist.url) {
|
||||
this.fetchPlaylistByUrl(playlist._id, playlist.url);
|
||||
} else if (playlist.filePath) {
|
||||
this.fetchPlaylistByFilePath(
|
||||
playlist._id,
|
||||
playlist.filePath
|
||||
);
|
||||
} else {
|
||||
console.log('skip...');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
} */
|
||||
|
||||
/**
|
||||
* Sets the user agent header for all http requests
|
||||
* @param userAgent user agent to use
|
||||
@@ -282,30 +291,39 @@ export class Api {
|
||||
* Converts the fetched playlist string to the playlist object, updates it in the database and sends the updated playlists array back to the renderer
|
||||
* @param id id of the playlist to update
|
||||
* @param playlistString updated playlist as string
|
||||
* @param event ipc event to send the response back to the renderer
|
||||
*/
|
||||
handlePlaylistRefresh(
|
||||
getRefreshedPlaylist(
|
||||
args: { id: string; title: string; filePath?: string; url?: string },
|
||||
playlistString: string,
|
||||
event?: Electron.IpcMainEvent
|
||||
playlistString: string
|
||||
) {
|
||||
if (event) {
|
||||
const parsedPlaylist: ParsedPlaylist =
|
||||
this.parsePlaylist(playlistString);
|
||||
const playlist = createPlaylistObject(
|
||||
args.title,
|
||||
parsedPlaylist,
|
||||
args.url ? args.url : args.filePath,
|
||||
args.url ? 'URL' : 'FILE'
|
||||
);
|
||||
event.sender.send(PLAYLIST_UPDATE_RESPONSE, {
|
||||
message: `Success! The playlist was successfully updated (${parsedPlaylist.items.length} channels)`,
|
||||
playlist: {
|
||||
...playlist,
|
||||
_id: args.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
const parsedPlaylist: ParsedPlaylist =
|
||||
this.parsePlaylist(playlistString);
|
||||
const playlist = createPlaylistObject(
|
||||
args.title,
|
||||
parsedPlaylist,
|
||||
args.url ? args.url : args.filePath,
|
||||
args.url ? 'URL' : 'FILE'
|
||||
);
|
||||
return {
|
||||
...playlist,
|
||||
_id: args.id,
|
||||
};
|
||||
}
|
||||
|
||||
sendPlaylistRefreshResponse(
|
||||
playlistId: string,
|
||||
playlist: Playlist,
|
||||
event: Electron.IpcMainEvent
|
||||
) {
|
||||
event.sender.send(PLAYLIST_UPDATE_RESPONSE, {
|
||||
message: `Success! The playlist was successfully updated (${
|
||||
(playlist.playlist.items as Channel[]).length
|
||||
} channels)`,
|
||||
playlist: {
|
||||
...playlist,
|
||||
_id: playlistId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -317,14 +335,24 @@ export class Api {
|
||||
async fetchPlaylistByUrl(
|
||||
args: { id: string; title: string; url?: string },
|
||||
event?: Electron.IpcMainEvent
|
||||
): Promise<void> {
|
||||
) {
|
||||
if (!args.url) return;
|
||||
try {
|
||||
await axios
|
||||
.get(args.url, { httpsAgent: agent })
|
||||
.then((result) =>
|
||||
this.handlePlaylistRefresh(args, result.data, event)
|
||||
const result = await axios.get(args.url, { httpsAgent: agent });
|
||||
|
||||
const refreshedPlaylist = this.getRefreshedPlaylist(
|
||||
args,
|
||||
result.data
|
||||
);
|
||||
if (event) {
|
||||
this.sendPlaylistRefreshResponse(
|
||||
refreshedPlaylist._id,
|
||||
refreshedPlaylist,
|
||||
event
|
||||
);
|
||||
} else {
|
||||
return refreshedPlaylist;
|
||||
}
|
||||
} catch (err) {
|
||||
if (event)
|
||||
event.sender.send(ERROR, {
|
||||
@@ -340,22 +368,28 @@ export class Api {
|
||||
* @param playlistString updated playlist as string
|
||||
* @param event ipc event to send the response back to the renderer
|
||||
*/
|
||||
fetchPlaylistByFilePath(
|
||||
async fetchPlaylistByFilePath(
|
||||
args: { id: string; title: string; filePath?: string },
|
||||
event?: Electron.IpcMainEvent
|
||||
): void {
|
||||
) {
|
||||
if (!args.filePath) return;
|
||||
try {
|
||||
fs.readFile(args.filePath, 'utf-8', (err, data) => {
|
||||
if (err) {
|
||||
this.handleFileNotFoundError(err, event);
|
||||
return;
|
||||
}
|
||||
let refreshedPlaylist: Playlist;
|
||||
|
||||
this.handlePlaylistRefresh(args, data, event);
|
||||
});
|
||||
try {
|
||||
const playlist = await fsPromises.readFile(args.filePath, 'utf-8');
|
||||
refreshedPlaylist = this.getRefreshedPlaylist(args, playlist);
|
||||
|
||||
if (event) {
|
||||
this.sendPlaylistRefreshResponse(
|
||||
refreshedPlaylist._id,
|
||||
refreshedPlaylist,
|
||||
event
|
||||
);
|
||||
} else {
|
||||
return refreshedPlaylist;
|
||||
}
|
||||
} catch (err) {
|
||||
this.handleFileNotFoundError(err, event);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,15 +428,16 @@ export class Api {
|
||||
}
|
||||
|
||||
/** @deprecated - used only for migration */
|
||||
removeAllPlaylists(event: Electron.IpcMainEvent) {
|
||||
async removeAllPlaylists(event: Electron.IpcMainEvent) {
|
||||
const removeCount = await db.remove({}, { multi: true });
|
||||
console.info(removeCount, ' playlists were removed');
|
||||
fs.unlink(dbPath, (err) => {
|
||||
if (err && err.code == 'ENOENT') {
|
||||
// file doesn't exist
|
||||
console.info("File doesn't exist, won't remove it.");
|
||||
} else if (err) {
|
||||
console.error('Error occurred while trying to remove file');
|
||||
} else {
|
||||
console.log(`${dbPath} was deleted`);
|
||||
console.info(`${dbPath} was deleted`);
|
||||
event.sender.send(IS_PLAYLISTS_MIGRATION_POSSIBLE_RESPONSE, {
|
||||
result: false,
|
||||
});
|
||||
|
||||
@@ -36,3 +36,7 @@ export const IS_PLAYLISTS_MIGRATION_POSSIBLE_RESPONSE =
|
||||
export const MIGRATE_PLAYLISTS = 'MIGRATE_PLAYLISTS';
|
||||
export const MIGRATE_PLAYLISTS_RESPONSE = 'MIGRATE_PLAYLISTS_RESPONSE';
|
||||
export const DELETE_ALL_PLAYLISTS = 'DELETE_ALL_PLAYLISTS';
|
||||
|
||||
// Auto-update
|
||||
export const AUTO_UPDATE_PLAYLISTS = 'AUTO_UPDATE';
|
||||
export const AUTO_UPDATE_PLAYLISTS_RESPONSE = 'AUTO_UPDATE_RESPONSE';
|
||||
@@ -11,11 +11,13 @@ import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { provideMockStore } from '@ngrx/store/testing';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import { MockModule, MockPipe, MockProviders } from 'ng-mocks';
|
||||
import { NgxIndexedDBService } from 'ngx-indexed-db';
|
||||
import { NgxWhatsNewModule } from 'ngx-whats-new';
|
||||
import { of } from 'rxjs';
|
||||
import { AppComponent } from './app.component';
|
||||
import { DataService } from './services/data.service';
|
||||
import { ElectronServiceStub } from './services/electron.service.stub';
|
||||
import { PlaylistsService } from './services/playlists.service';
|
||||
import { SettingsService } from './services/settings.service';
|
||||
import { WhatsNewService } from './services/whats-new.service';
|
||||
import { WhatsNewServiceStub } from './services/whats-new.service.stub';
|
||||
@@ -23,10 +25,6 @@ import { Language } from './settings/language.enum';
|
||||
import { Theme } from './settings/theme.enum';
|
||||
import { STORE_KEY } from './shared/enums/store-keys.enum';
|
||||
|
||||
class MatSnackBarStub {
|
||||
open(): void {}
|
||||
}
|
||||
|
||||
jest.spyOn(global.console, 'error').mockImplementation(() => {});
|
||||
|
||||
describe('AppComponent', () => {
|
||||
@@ -43,10 +41,14 @@ describe('AppComponent', () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [AppComponent, MockPipe(TranslatePipe)],
|
||||
providers: [
|
||||
{ provide: MatSnackBar, useClass: MatSnackBarStub },
|
||||
{ provide: WhatsNewService, useClass: WhatsNewServiceStub },
|
||||
MockProviders(TranslateService),
|
||||
SettingsService, // TODO: stub
|
||||
MockProviders(
|
||||
TranslateService,
|
||||
PlaylistsService,
|
||||
NgxIndexedDBService,
|
||||
MatSnackBar
|
||||
),
|
||||
SettingsService,
|
||||
{
|
||||
provide: DataService,
|
||||
useClass: ElectronServiceStub,
|
||||
@@ -70,6 +72,9 @@ describe('AppComponent', () => {
|
||||
translateService = TestBed.inject(TranslateService);
|
||||
whatsNewService = TestBed.inject(WhatsNewService);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
// TODO: investigate in detail
|
||||
component.triggerAutoUpdateMechanism = jest.fn();
|
||||
component.modals = [];
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
@@ -4,9 +4,11 @@ import { Router } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { ModalWindow } from 'ngx-whats-new/lib/modal-window.interface';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import * as semver from 'semver';
|
||||
import { IpcCommand } from '../../shared/ipc-command.class';
|
||||
import {
|
||||
AUTO_UPDATE_PLAYLISTS,
|
||||
EPG_ERROR,
|
||||
EPG_FETCH_DONE,
|
||||
ERROR,
|
||||
@@ -17,6 +19,7 @@ import {
|
||||
} from '../../shared/ipc-commands';
|
||||
import { DataService } from './services/data.service';
|
||||
import { EpgService } from './services/epg.service';
|
||||
import { PlaylistsService } from './services/playlists.service';
|
||||
import { SettingsService } from './services/settings.service';
|
||||
import { WhatsNewService } from './services/whats-new.service';
|
||||
import { Language } from './settings/language.enum';
|
||||
@@ -58,13 +61,11 @@ export class AppComponent {
|
||||
|
||||
listeners = [];
|
||||
|
||||
/**
|
||||
* Creates an instance of AppComponent
|
||||
*/
|
||||
constructor(
|
||||
private electronService: DataService,
|
||||
private epgService: EpgService,
|
||||
private ngZone: NgZone,
|
||||
private playlistService: PlaylistsService,
|
||||
private router: Router,
|
||||
private store: Store,
|
||||
private snackBar: MatSnackBar,
|
||||
@@ -93,16 +94,28 @@ export class AppComponent {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts all the functions to initialize the component
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
ngOnInit() {
|
||||
this.store.dispatch(PlaylistActions.loadPlaylists());
|
||||
this.translate.setDefaultLang(this.DEFAULT_LANG);
|
||||
|
||||
this.setRendererListeners();
|
||||
this.initSettings();
|
||||
this.handleWhatsNewDialog();
|
||||
|
||||
this.triggerAutoUpdateMechanism();
|
||||
}
|
||||
|
||||
async triggerAutoUpdateMechanism() {
|
||||
if (this.electronService.isElectron) {
|
||||
const playlistForAutoUpdate = await firstValueFrom(
|
||||
this.playlistService.getPlaylistsForAutoUpdate()
|
||||
);
|
||||
if (playlistForAutoUpdate && playlistForAutoUpdate.length > 0)
|
||||
this.electronService.sendIpcEvent(
|
||||
AUTO_UPDATE_PLAYLISTS,
|
||||
playlistForAutoUpdate
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,6 +18,7 @@ import { DataService } from '../../services/data.service';
|
||||
import * as PlaylistActions from '../../state/actions';
|
||||
import { selectAllPlaylistsMeta } from '../../state/selectors';
|
||||
import {
|
||||
AUTO_UPDATE_PLAYLISTS_RESPONSE,
|
||||
DELETE_ALL_PLAYLISTS,
|
||||
IS_PLAYLISTS_MIGRATION_POSSIBLE,
|
||||
IS_PLAYLISTS_MIGRATION_POSSIBLE_RESPONSE,
|
||||
@@ -49,7 +50,6 @@ export class RecentPlaylistsComponent implements OnDestroy {
|
||||
this.snackBar.open(response.message, null, { duration: 2000 });
|
||||
this.store.dispatch(
|
||||
PlaylistActions.updatePlaylist({
|
||||
// TODO: check if this is correct
|
||||
playlistId: response.playlist._id,
|
||||
playlist: response.playlist,
|
||||
})
|
||||
@@ -78,6 +78,16 @@ export class RecentPlaylistsComponent implements OnDestroy {
|
||||
);
|
||||
}
|
||||
),
|
||||
new IpcCommand(
|
||||
AUTO_UPDATE_PLAYLISTS_RESPONSE,
|
||||
(playlists: Playlist[]) => {
|
||||
this.store.dispatch(
|
||||
PlaylistActions.updateManyPlaylists({
|
||||
playlists,
|
||||
})
|
||||
);
|
||||
}
|
||||
),
|
||||
];
|
||||
|
||||
isMigrationPossible = false;
|
||||
|
||||
@@ -100,15 +100,15 @@ export class PlaylistsService {
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: for-loop
|
||||
updatePlaylistPosition(id: string, position: number) {
|
||||
return this.getPlaylistById(id).pipe(
|
||||
switchMap((playlist) =>
|
||||
this.dbService.update(DbStores.Playlists, {
|
||||
updateManyPlaylists(playlists: Playlist[]) {
|
||||
return combineLatest(
|
||||
playlists.map((playlist) => {
|
||||
return this.dbService.update(DbStores.Playlists, {
|
||||
...playlist,
|
||||
position,
|
||||
})
|
||||
)
|
||||
updateDate: Date.now(),
|
||||
autoRefresh: true,
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -167,4 +167,17 @@ export class PlaylistsService {
|
||||
addManyPlaylists(playlists: Playlist[]) {
|
||||
return this.dbService.bulkAdd(DbStores.Playlists, playlists as any); // TODO: update ngx-indexed-db
|
||||
}
|
||||
|
||||
getPlaylistsForAutoUpdate() {
|
||||
return this.dbService.getAll(DbStores.Playlists).pipe(
|
||||
map((playlists: Playlist[]) => {
|
||||
return playlists
|
||||
.filter((item) => item.autoRefresh)
|
||||
.map(
|
||||
({ playlist, header, items, favorites, ...rest }) =>
|
||||
rest
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,11 @@ export const updatePlaylist = createAction(
|
||||
props<{ playlist: Playlist; playlistId: string }>()
|
||||
);
|
||||
|
||||
export const updateManyPlaylists = createAction(
|
||||
`${PLAYLISTS_STORE_KEY} Update many playlists (auto-update mechanism)`,
|
||||
props<{ playlists: Playlist[] }>()
|
||||
);
|
||||
|
||||
export const parsePlaylist = createAction(
|
||||
`${PLAYLISTS_STORE_KEY} parse playlist`,
|
||||
props<{
|
||||
|
||||
@@ -173,6 +173,18 @@ export class PlaylistEffects {
|
||||
{ dispatch: false }
|
||||
);
|
||||
|
||||
updateManyPlaylists$ = createEffect(
|
||||
() => {
|
||||
return this.actions$.pipe(
|
||||
ofType(PlaylistActions.updateManyPlaylists),
|
||||
switchMap((action) =>
|
||||
this.playlistsService.updateManyPlaylists(action.playlists)
|
||||
)
|
||||
);
|
||||
},
|
||||
{ dispatch: false }
|
||||
);
|
||||
|
||||
constructor(
|
||||
private actions$: Actions,
|
||||
private playlistsService: PlaylistsService,
|
||||
|
||||
@@ -97,7 +97,13 @@ export const playlistReducer = createReducer(
|
||||
playlists: playlistsAdapter.updateOne(
|
||||
{
|
||||
id: action.playlistId,
|
||||
changes: { ...action.playlist, _id: action.playlistId },
|
||||
changes: {
|
||||
...action.playlist,
|
||||
_id: action.playlistId,
|
||||
updateDate: Date.now(),
|
||||
favorites: [],
|
||||
count: action.playlist.playlist.items.length,
|
||||
},
|
||||
},
|
||||
state.playlists
|
||||
),
|
||||
@@ -162,6 +168,21 @@ export const playlistReducer = createReducer(
|
||||
...state,
|
||||
channels: action.channels,
|
||||
};
|
||||
}),
|
||||
on(PlaylistActions.updateManyPlaylists, (state, action): PlaylistState => {
|
||||
return {
|
||||
...state,
|
||||
playlists: playlistsAdapter.updateMany(
|
||||
action.playlists.map((updatedPlaylist) => ({
|
||||
id: updatedPlaylist._id,
|
||||
changes: {
|
||||
...updatedPlaylist,
|
||||
updateDate: Date.now(),
|
||||
},
|
||||
})),
|
||||
state.playlists
|
||||
),
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
Reference in new issue
Block a user