mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
refactor(nx): created shared and m3u-utils library
This commit is contained in:
1 parent
99ed5df404
commit
920afa6850
59 files changed
+566
-225
No files matched your search
@@ -1,6 +1,8 @@
|
||||
import { contextBridge, ipcRenderer } from 'electron';
|
||||
|
||||
contextBridge.exposeInMainWorld('electron', {
|
||||
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
|
||||
platform: process.platform,
|
||||
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
|
||||
platform: process.platform,
|
||||
fetchPlaylistByUrl: (url: string) =>
|
||||
ipcRenderer.invoke('fetch-playlist-by-url', url),
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* This module is responsible on handling all the inter process communications
|
||||
* between the frontend to the electron backend.
|
||||
*/
|
||||
|
||||
import axios from 'axios';
|
||||
import { ipcMain } from 'electron';
|
||||
import { parse } from 'iptv-playlist-parser';
|
||||
import { createPlaylistObject, getFilenameFromUrl } from 'm3u-utils';
|
||||
|
||||
export default class PlaylistEvents {
|
||||
static bootstrapPlaylistEvents(): Electron.IpcMain {
|
||||
return ipcMain;
|
||||
}
|
||||
}
|
||||
|
||||
const https = require('https');
|
||||
|
||||
// Retrieve app version
|
||||
ipcMain.handle('fetch-playlist-by-url', async (event, url) => {
|
||||
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(
|
||||
playlistName,
|
||||
parsedPlaylist,
|
||||
url,
|
||||
'URL'
|
||||
);
|
||||
|
||||
return playlistObject;
|
||||
} catch (error) {
|
||||
console.error('Error fetching playlist:', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
@@ -1,29 +1,30 @@
|
||||
import SquirrelEvents from './app/events/squirrel.events';
|
||||
import ElectronEvents from './app/events/electron.events';
|
||||
import UpdateEvents from './app/events/update.events';
|
||||
import { app, BrowserWindow } from 'electron';
|
||||
import App from './app/app';
|
||||
import ElectronEvents from './app/events/electron.events';
|
||||
import PlaylistEvents from './app/events/playlist.events';
|
||||
import SquirrelEvents from './app/events/squirrel.events';
|
||||
|
||||
export default class Main {
|
||||
static initialize() {
|
||||
if (SquirrelEvents.handleEvents()) {
|
||||
// squirrel event handled (except first run event) and app will exit in 1000ms, so don't do anything else
|
||||
app.quit();
|
||||
static initialize() {
|
||||
if (SquirrelEvents.handleEvents()) {
|
||||
// squirrel event handled (except first run event) and app will exit in 1000ms, so don't do anything else
|
||||
app.quit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bootstrapApp() {
|
||||
App.main(app, BrowserWindow);
|
||||
}
|
||||
|
||||
static bootstrapAppEvents() {
|
||||
ElectronEvents.bootstrapElectronEvents();
|
||||
|
||||
// initialize auto updater service
|
||||
if (!App.isDevelopmentMode()) {
|
||||
// UpdateEvents.initAutoUpdateService();
|
||||
static bootstrapApp() {
|
||||
App.main(app, BrowserWindow);
|
||||
}
|
||||
|
||||
static bootstrapAppEvents() {
|
||||
ElectronEvents.bootstrapElectronEvents();
|
||||
PlaylistEvents.bootstrapPlaylistEvents();
|
||||
|
||||
// initialize auto updater service
|
||||
if (!App.isDevelopmentMode()) {
|
||||
// UpdateEvents.initAutoUpdateService();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handle setup events as quickly as possible
|
||||
|
||||
@@ -38,7 +38,7 @@ export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader {
|
||||
* Conditionally imports the necessary service based on the current environment
|
||||
*/
|
||||
export function DataFactory() {
|
||||
if (isTauri()) {
|
||||
if (window.electron) {
|
||||
return new TauriService();
|
||||
}
|
||||
return new PwaService();
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
|
||||
import { Component, NgZone } from '@angular/core';
|
||||
import { Component, inject, OnDestroy } from '@angular/core';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { Playlist } from 'shared-interfaces';
|
||||
import { ERROR, PLAYLIST_PARSE_RESPONSE } from '../../shared/ipc-commands';
|
||||
import { Playlist } from '../../shared/playlist.interface';
|
||||
import { DataService } from '../services/data.service';
|
||||
import { HeaderComponent } from '../shared/components/header/header.component';
|
||||
import { addPlaylist } from '../state/actions';
|
||||
@@ -16,13 +15,17 @@ import { RecentPlaylistsComponent } from './recent-playlists/recent-playlists.co
|
||||
templateUrl: './home.component.html',
|
||||
styleUrls: ['./home.component.scss'],
|
||||
imports: [
|
||||
HeaderComponent,
|
||||
MatProgressBarModule,
|
||||
RecentPlaylistsComponent,
|
||||
TranslatePipe
|
||||
],
|
||||
HeaderComponent,
|
||||
MatProgressBarModule,
|
||||
RecentPlaylistsComponent,
|
||||
TranslatePipe,
|
||||
],
|
||||
})
|
||||
export class HomeComponent {
|
||||
export class HomeComponent implements OnDestroy {
|
||||
private dataService = inject(DataService);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
private readonly store = inject(Store);
|
||||
|
||||
/** Loading spinner state */
|
||||
isLoading = false;
|
||||
|
||||
@@ -48,12 +51,7 @@ export class HomeComponent {
|
||||
|
||||
listeners = [];
|
||||
|
||||
constructor(
|
||||
private dataService: DataService,
|
||||
private ngZone: NgZone,
|
||||
private snackBar: MatSnackBar,
|
||||
private readonly store: Store
|
||||
) {
|
||||
constructor() {
|
||||
this.setRendererListeners();
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import { isTauri } from '@tauri-apps/api/core';
|
||||
import { save } from '@tauri-apps/plugin-dialog';
|
||||
import { writeTextFile } from '@tauri-apps/plugin-fs';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { Playlist } from '../../../../shared/playlist.interface';
|
||||
import { Playlist } from 'shared-interfaces';
|
||||
import { DatabaseService } from '../../../services/database.service';
|
||||
import { PlaylistsService } from '../../../services/playlists.service';
|
||||
import { PlaylistMeta } from '../../../shared/playlist-meta.type';
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
HostListener,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
@@ -23,8 +22,7 @@ import { Store } from '@ngrx/store';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import { NgxSkeletonLoaderComponent } from 'ngx-skeleton-loader';
|
||||
import { BehaviorSubject, combineLatest, map } from 'rxjs';
|
||||
import { GLOBAL_FAVORITES_PLAYLIST_ID } from '../../../shared/constants';
|
||||
import { Playlist } from '../../../shared/playlist.interface';
|
||||
import { GLOBAL_FAVORITES_PLAYLIST_ID, Playlist } from 'shared-interfaces';
|
||||
import { DataService } from '../../services/data.service';
|
||||
import { DatabaseService } from '../../services/database.service';
|
||||
import { SortService } from '../../services/sort.service';
|
||||
|
||||
@@ -11,8 +11,8 @@ import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { Playlist } from 'shared-interfaces';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { Playlist } from '../../../shared/playlist.interface';
|
||||
import { DataService } from '../../services/data.service';
|
||||
import { addPlaylist } from '../../state/actions';
|
||||
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
formControlName="playlistUrl"
|
||||
/>
|
||||
</mat-form-field>
|
||||
@if (!isTauri) {
|
||||
<p>
|
||||
{{ 'HOME.URL_UPLOAD.CORS_NOTE' | translate }}
|
||||
</p>
|
||||
@if (!isDesktop) {
|
||||
<p>
|
||||
{{ 'HOME.URL_UPLOAD.CORS_NOTE' | translate }}
|
||||
</p>
|
||||
}
|
||||
<button
|
||||
class="add-button"
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
import { MatButton } from '@angular/material/button';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { isTauri } from '@tauri-apps/api/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-url-upload',
|
||||
@@ -22,7 +21,7 @@ export class UrlUploadComponent implements OnInit {
|
||||
@Output() urlAdded: EventEmitter<string> = new EventEmitter();
|
||||
|
||||
form: FormGroup;
|
||||
isTauri = isTauri();
|
||||
readonly isDesktop = window.electron;
|
||||
|
||||
ngOnInit() {
|
||||
const urlRegex = '(https?://.*?)';
|
||||
|
||||
@@ -12,8 +12,8 @@ import { MatIcon } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { Playlist } from 'shared-interfaces';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { Playlist } from '../../../shared/playlist.interface';
|
||||
import {
|
||||
PortalStatus,
|
||||
PortalStatusService,
|
||||
@@ -102,7 +102,9 @@ export class XtreamCodeImportComponent {
|
||||
this.isTestingConnection = true;
|
||||
const serverUrlAsString = this.form.value.serverUrl as string;
|
||||
const url = new URL(serverUrlAsString);
|
||||
const serverUrl = `${url.protocol}//${url.hostname}${url.port ? ':' + url.port : ''}`;
|
||||
const serverUrl = `${url.protocol}//${url.hostname}${
|
||||
url.port ? ':' + url.port : ''
|
||||
}`;
|
||||
|
||||
try {
|
||||
this.connectionStatus =
|
||||
@@ -134,7 +136,9 @@ export class XtreamCodeImportComponent {
|
||||
addPlaylist() {
|
||||
const serverUrlAsString = this.form.value.serverUrl as string;
|
||||
const url = new URL(serverUrlAsString);
|
||||
const serverUrl = `${url.protocol}//${url.hostname}${url.port ? ':' + url.port : ''}`;
|
||||
const serverUrl = `${url.protocol}//${url.hostname}${
|
||||
url.port ? ':' + url.port : ''
|
||||
}`;
|
||||
this.store.dispatch(
|
||||
addPlaylist({
|
||||
playlist: {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
Component,
|
||||
ElementRef,
|
||||
inject,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
@@ -9,7 +10,7 @@ import {
|
||||
} from '@angular/core';
|
||||
import Artplayer from 'artplayer';
|
||||
import Hls from 'hls.js';
|
||||
import { Channel } from '../../../../shared/channel.interface';
|
||||
import { Channel } from 'shared-interfaces';
|
||||
|
||||
Artplayer.AUTO_PLAYBACK_TIMEOUT = 10000;
|
||||
|
||||
@@ -38,7 +39,7 @@ export class ArtPlayerComponent implements OnInit, OnDestroy, OnChanges {
|
||||
|
||||
private player: Artplayer;
|
||||
|
||||
constructor(private elementRef: ElementRef) {}
|
||||
private readonly elementRef = inject(ElementRef);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.initPlayer();
|
||||
|
||||
+8
-9
@@ -9,6 +9,7 @@ import {
|
||||
Component,
|
||||
ElementRef,
|
||||
HostListener,
|
||||
inject,
|
||||
Input,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
@@ -25,7 +26,7 @@ import { Store } from '@ngrx/store';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import * as _ from 'lodash';
|
||||
import { map, skipWhile } from 'rxjs';
|
||||
import { Channel } from '../../../../shared/channel.interface';
|
||||
import { Channel } from 'shared-interfaces';
|
||||
import { EpgService } from '../../../services/epg.service';
|
||||
import { FilterPipe } from '../../../shared/pipes/filter.pipe';
|
||||
import * as PlaylistActions from '../../../state/actions';
|
||||
@@ -58,6 +59,11 @@ import { ChannelListItemComponent } from './channel-list-item/channel-list-item.
|
||||
],
|
||||
})
|
||||
export class ChannelListContainerComponent {
|
||||
private readonly epgService = inject(EpgService);
|
||||
private readonly snackBar = inject(MatSnackBar);
|
||||
private readonly store = inject(Store);
|
||||
private readonly translateService = inject(TranslateService);
|
||||
|
||||
/**
|
||||
* Channels array
|
||||
* Create local copy of the store for local manipulations without updates in the store
|
||||
@@ -67,7 +73,7 @@ export class ChannelListContainerComponent {
|
||||
return this._channelList;
|
||||
}
|
||||
|
||||
@Input('channelList')
|
||||
@Input()
|
||||
set channelList(value: Channel[]) {
|
||||
this._channelList = value;
|
||||
this.groupedChannels = _.default.groupBy(value, 'group.title');
|
||||
@@ -118,13 +124,6 @@ export class ChannelListContainerComponent {
|
||||
)
|
||||
);
|
||||
|
||||
constructor(
|
||||
private readonly epgService: EpgService,
|
||||
private readonly snackBar: MatSnackBar,
|
||||
private readonly store: Store,
|
||||
private readonly translateService: TranslateService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Sets clicked channel as selected and emits them to the parent component
|
||||
* @param channel selected channel
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
Component,
|
||||
ElementRef,
|
||||
inject,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
@@ -9,9 +10,9 @@ import {
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import Hls from 'hls.js';
|
||||
import { Channel } from '../../../../shared/channel.interface';
|
||||
import { getExtensionFromUrl } from 'm3u-utils';
|
||||
import { Channel } from 'shared-interfaces';
|
||||
import { CHANNEL_SET_USER_AGENT } from '../../../../shared/ipc-commands';
|
||||
import { getExtensionFromUrl } from '../../../../shared/playlist.utils';
|
||||
import { DataService } from '../../../services/data.service';
|
||||
|
||||
/**
|
||||
@@ -26,12 +27,9 @@ import { DataService } from '../../../services/data.service';
|
||||
export class HtmlVideoPlayerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
/** Channel to play */
|
||||
@Input() channel: Channel;
|
||||
dataService: DataService; // Declare the dataService property
|
||||
@Input() volume = 1;
|
||||
|
||||
constructor(dataService: DataService) {
|
||||
this.dataService = dataService; // Inject the DataService
|
||||
}
|
||||
private readonly dataService = inject(DataService);
|
||||
|
||||
/** Video player DOM element */
|
||||
@ViewChild('videoPlayer', { static: true })
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NgStyle } from '@angular/common';
|
||||
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import moment from 'moment';
|
||||
import { Channel } from '../../../../shared/channel.interface';
|
||||
import { Channel } from 'shared-interfaces';
|
||||
import { MomentDatePipe } from '../../../shared/pipes/moment-date.pipe';
|
||||
import { EpgProgram } from '../../models/epg-program.model';
|
||||
|
||||
@@ -9,7 +9,7 @@ import { EpgProgram } from '../../models/epg-program.model';
|
||||
imports: [MomentDatePipe, NgStyle],
|
||||
selector: 'app-info-overlay',
|
||||
templateUrl: './info-overlay.component.html',
|
||||
styleUrls: ['./info-overlay.component.scss']
|
||||
styleUrls: ['./info-overlay.component.scss'],
|
||||
})
|
||||
export class InfoOverlayComponent implements OnChanges {
|
||||
/** Active channel */
|
||||
|
||||
@@ -20,7 +20,7 @@ import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { addDays, differenceInMinutes, format, parse, subDays } from 'date-fns';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { Channel } from '../../../../shared/channel.interface';
|
||||
import { Channel } from 'shared-interfaces';
|
||||
import { MomentDatePipe } from '../../../shared/pipes/moment-date.pipe';
|
||||
import { EpgChannel } from '../../models/epg-channel.model';
|
||||
import { EpgProgram } from '../../models/epg-program.model';
|
||||
|
||||
@@ -6,7 +6,7 @@ import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { Router, RouterLink } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { Channel } from '../../../../../shared/channel.interface';
|
||||
import { Channel } from 'shared-interfaces';
|
||||
import { RecentPlaylistsComponent } from '../../../../home/recent-playlists/recent-playlists.component';
|
||||
import * as PlaylistActions from '../../../../state/actions';
|
||||
import { selectPlaylistTitle } from '../../../../state/selectors';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { Component, EventEmitter, inject, Input, Output } from '@angular/core';
|
||||
import { MatButton, MatIconButton } from '@angular/material/button';
|
||||
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
@@ -7,7 +7,7 @@ import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { Channel } from '../../../../../shared/channel.interface';
|
||||
import { Channel } from 'shared-interfaces';
|
||||
import { SettingsComponent } from '../../../../settings/settings.component';
|
||||
import { updateFavorites } from '../../../../state/actions';
|
||||
import {
|
||||
@@ -37,15 +37,13 @@ export class ToolbarComponent {
|
||||
@Output() toggleLeftDrawerClicked = new EventEmitter<void>();
|
||||
@Output() toggleRightDrawerClicked = new EventEmitter<void>();
|
||||
|
||||
private readonly dialog = inject(MatDialog);
|
||||
private readonly store = inject(Store);
|
||||
|
||||
readonly favorites$ = this.store.select(selectFavorites);
|
||||
readonly isEpgAvailable$ = this.store.select(selectIsEpgAvailable);
|
||||
readonly playlistId$ = this.store.select(selectActivePlaylistId);
|
||||
|
||||
constructor(
|
||||
private readonly dialog: MatDialog,
|
||||
private readonly store: Store
|
||||
) {}
|
||||
|
||||
updateFavoriteStatus(channel: Channel) {
|
||||
this.store.dispatch(updateFavorites({ channel }));
|
||||
}
|
||||
|
||||
@@ -17,14 +17,13 @@ import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { StorageMap } from '@ngx-pwa/local-storage';
|
||||
import { Observable, combineLatestWith, filter, map, switchMap } from 'rxjs';
|
||||
import { Channel } from '../../../../shared/channel.interface';
|
||||
import { Channel, Playlist } from 'shared-interfaces';
|
||||
import {
|
||||
CHANNEL_SET_USER_AGENT,
|
||||
ERROR,
|
||||
PLAYLIST_PARSE_BY_URL,
|
||||
PLAYLIST_PARSE_RESPONSE,
|
||||
} from '../../../../shared/ipc-commands';
|
||||
import { Playlist } from '../../../../shared/playlist.interface';
|
||||
import { DataService } from '../../../services/data.service';
|
||||
import { PlaylistsService } from '../../../services/playlists.service';
|
||||
import { SettingsStore } from '../../../services/settings-store.service';
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from '@angular/core';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { StorageMap } from '@ngx-pwa/local-storage';
|
||||
import { getExtensionFromUrl } from '../../../shared/playlist.utils';
|
||||
import { getExtensionFromUrl } from 'm3u-utils';
|
||||
import { ArtPlayerComponent } from '../../player/components/art-player/art-player.component';
|
||||
import { HtmlVideoPlayerComponent } from '../../player/components/html-video-player/html-video-player.component';
|
||||
import { VjsPlayerComponent } from '../../player/components/vjs-player/vjs-player.component';
|
||||
|
||||
@@ -4,19 +4,19 @@ import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { parse } from 'iptv-playlist-parser';
|
||||
import { NgxIndexedDBService } from 'ngx-indexed-db';
|
||||
import { combineLatest, EMPTY, map, switchMap } from 'rxjs';
|
||||
import { Channel } from '../../shared/channel.interface';
|
||||
import { GLOBAL_FAVORITES_PLAYLIST_ID } from '../../shared/constants';
|
||||
import {
|
||||
Playlist,
|
||||
PlaylistUpdateState,
|
||||
} from '../../shared/playlist.interface';
|
||||
import {
|
||||
aggregateFavoriteChannels,
|
||||
createFavoritesPlaylist,
|
||||
createPlaylistObject,
|
||||
} from '../../shared/playlist.utils';
|
||||
} from 'm3u-utils';
|
||||
import { NgxIndexedDBService } from 'ngx-indexed-db';
|
||||
import { combineLatest, EMPTY, map, switchMap } from 'rxjs';
|
||||
import {
|
||||
Channel,
|
||||
GLOBAL_FAVORITES_PLAYLIST_ID,
|
||||
Playlist,
|
||||
PlaylistUpdateState,
|
||||
} from 'shared-interfaces';
|
||||
import { XtreamItem } from '../../shared/xtream-item.interface';
|
||||
import { XtreamSerieItem } from '../../shared/xtream-serie-item.interface';
|
||||
import { DbStores } from '../indexed-db.config';
|
||||
|
||||
@@ -6,6 +6,8 @@ import { SwUpdate } from '@angular/service-worker';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { catchError, firstValueFrom, throwError } from 'rxjs';
|
||||
import { Playlist } from 'shared-interfaces';
|
||||
import { AppConfig } from '../../environments/environment';
|
||||
import {
|
||||
ERROR,
|
||||
PLAYLIST_PARSE_BY_URL,
|
||||
@@ -15,8 +17,6 @@ import {
|
||||
XTREAM_REQUEST,
|
||||
XTREAM_RESPONSE,
|
||||
} from '../../shared/ipc-commands';
|
||||
import { Playlist } from '../../shared/playlist.interface';
|
||||
import { AppConfig } from '../../environments/environment';
|
||||
import * as PlaylistActions from '../state/actions';
|
||||
import { DataService } from './data.service';
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ import { listen } from '@tauri-apps/api/event';
|
||||
import { readTextFile } from '@tauri-apps/plugin-fs';
|
||||
import { fetch } from '@tauri-apps/plugin-http';
|
||||
import { parse } from 'iptv-playlist-parser';
|
||||
import { createPlaylistObject } from 'm3u-utils';
|
||||
import { Playlist } from 'shared-interfaces';
|
||||
import { AppConfig } from '../../environments/environment';
|
||||
import {
|
||||
AUTO_UPDATE_PLAYLISTS,
|
||||
AUTO_UPDATE_PLAYLISTS_RESPONSE,
|
||||
@@ -17,9 +20,6 @@ import {
|
||||
PLAYLIST_UPDATE_RESPONSE,
|
||||
XTREAM_RESPONSE,
|
||||
} from '../../shared/ipc-commands';
|
||||
import { Playlist } from '../../shared/playlist.interface';
|
||||
import { createPlaylistObject, getFilenameFromUrl } from '../../shared/playlist.utils';
|
||||
import { AppConfig } from '../../environments/environment';
|
||||
import { DataService } from './data.service';
|
||||
|
||||
@Injectable({
|
||||
@@ -37,7 +37,7 @@ export class TauriService extends DataService {
|
||||
|
||||
private async setupEventListeners() {
|
||||
// Listen for player errors
|
||||
this.eventListeners['player-error'] = await listen(
|
||||
/* this.eventListeners['player-error'] = await listen(
|
||||
'player-error',
|
||||
(event) => {
|
||||
console.error('Player error:', event);
|
||||
@@ -47,7 +47,7 @@ export class TauriService extends DataService {
|
||||
{ duration: 5000 }
|
||||
);
|
||||
}
|
||||
);
|
||||
); */
|
||||
}
|
||||
|
||||
getAppVersion(): string {
|
||||
@@ -206,18 +206,26 @@ export class TauriService extends DataService {
|
||||
}
|
||||
|
||||
async fetchFromUrl(payload: Partial<Playlist>) {
|
||||
try {
|
||||
window.electron.fetchPlaylistByUrl(payload.url).then((result) => {
|
||||
window.postMessage({
|
||||
type: PLAYLIST_PARSE_RESPONSE,
|
||||
payload: { ...result, isTemporary: payload.isTemporary },
|
||||
});
|
||||
});
|
||||
/* try {
|
||||
const response = await fetch(payload.url);
|
||||
const responseBody = await response.text();
|
||||
const parsedPlaylist = parse(responseBody);
|
||||
|
||||
// Extract playlist name from URL, use "Imported from URL" as fallback
|
||||
const extractedName = payload.url && payload.url.length > 1
|
||||
? getFilenameFromUrl(payload.url)
|
||||
: '';
|
||||
const playlistName = !extractedName || extractedName === 'Untitled playlist'
|
||||
? 'Imported from URL'
|
||||
: extractedName;
|
||||
const extractedName =
|
||||
payload.url && payload.url.length > 1
|
||||
? getFilenameFromUrl(payload.url)
|
||||
: '';
|
||||
const playlistName =
|
||||
!extractedName || extractedName === 'Untitled playlist'
|
||||
? 'Imported from URL'
|
||||
: extractedName;
|
||||
|
||||
const playlist = createPlaylistObject(
|
||||
playlistName,
|
||||
@@ -236,7 +244,7 @@ export class TauriService extends DataService {
|
||||
message: this.getErrorMessageByStatusCode(error.status),
|
||||
status: error.status,
|
||||
});
|
||||
}
|
||||
} */
|
||||
}
|
||||
|
||||
getErrorMessageByStatusCode(status: number) {
|
||||
|
||||
@@ -34,12 +34,12 @@ import { Store } from '@ngrx/store';
|
||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import { take } from 'rxjs';
|
||||
import * as semver from 'semver';
|
||||
import { Playlist } from 'shared-interfaces';
|
||||
import {
|
||||
SET_MPV_PLAYER_PATH,
|
||||
SET_VLC_PLAYER_PATH,
|
||||
SETTINGS_UPDATE,
|
||||
} from '../../shared/ipc-commands';
|
||||
import { Playlist } from '../../shared/playlist.interface';
|
||||
import { DataService } from '../services/data.service';
|
||||
import { DialogService } from '../services/dialog.service';
|
||||
import { EpgService } from '../services/epg.service';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Channel } from '../../shared/channel.interface';
|
||||
import { Channel } from 'shared-interfaces';
|
||||
|
||||
/**
|
||||
* Creates new channel object based on the given fields
|
||||
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import { getFilenameFromUrl } from 'm3u-utils';
|
||||
import { PLAYLIST_PARSE_BY_URL } from '../../../../shared/ipc-commands';
|
||||
import { getFilenameFromUrl } from '../../../../shared/playlist.utils';
|
||||
import { FileUploadComponent } from '../../../home/file-upload/file-upload.component';
|
||||
import { StalkerPortalImportComponent } from '../../../home/stalker-portal-import/stalker-portal-import.component';
|
||||
import { TextImportComponent } from '../../../home/text-import/text-import.component';
|
||||
@@ -32,7 +32,7 @@ export type PlaylistType = 'xtream' | 'url' | 'text' | 'file' | 'stalker';
|
||||
UrlUploadComponent,
|
||||
],
|
||||
selector: 'app-add-playlist',
|
||||
templateUrl: './add-playlist-dialog.component.html'
|
||||
templateUrl: './add-playlist-dialog.component.html',
|
||||
})
|
||||
export class AddPlaylistDialogComponent {
|
||||
playlistType!: PlaylistType;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Playlist } from '../../shared/playlist.interface';
|
||||
import { Playlist } from 'shared-interfaces';
|
||||
|
||||
export type PlaylistMeta = Pick<
|
||||
Playlist,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { createAction, props } from '@ngrx/store';
|
||||
import { Channel } from '../../shared/channel.interface';
|
||||
import { Playlist } from '../../shared/playlist.interface';
|
||||
import { Channel, Playlist } from 'shared-interfaces';
|
||||
import { EpgProgram } from '../player/models/epg-program.model';
|
||||
import { PlaylistMeta } from '../shared/playlist-meta.type';
|
||||
|
||||
|
||||
@@ -15,13 +15,13 @@ import {
|
||||
tap,
|
||||
withLatestFrom,
|
||||
} from 'rxjs';
|
||||
import { Playlist } from 'shared-interfaces';
|
||||
import {
|
||||
CHANNEL_SET_USER_AGENT,
|
||||
EPG_GET_PROGRAM,
|
||||
OPEN_MPV_PLAYER,
|
||||
OPEN_VLC_PLAYER,
|
||||
} from '../../shared/ipc-commands';
|
||||
import { Playlist } from '../../shared/playlist.interface';
|
||||
import { DataService } from '../services/data.service';
|
||||
import { PlaylistsService } from '../services/playlists.service';
|
||||
import { Settings, VideoPlayer } from '../settings/settings.interface';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createFeatureSelector, createSelector } from '@ngrx/store';
|
||||
import { GLOBAL_FAVORITES_PLAYLIST_ID } from '../../shared/constants';
|
||||
import { GLOBAL_FAVORITES_PLAYLIST_ID } from 'shared-interfaces';
|
||||
import * as fromPlaylistMetaState from './playlists.state';
|
||||
import * as fromPlaylistState from './reducers';
|
||||
import { selectRouteParam } from './router.selectors';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Channel } from '../../shared/channel.interface';
|
||||
import { Channel } from 'shared-interfaces';
|
||||
import { initialPlaylistMetaState, PlaylistMetaState } from './playlists.state';
|
||||
|
||||
import { EpgProgram } from '../player/models/epg-program.model';
|
||||
|
||||
Vendored
+2
@@ -1,4 +1,5 @@
|
||||
import 'jest-extended';
|
||||
import { Playlist } from 'shared-interfaces';
|
||||
|
||||
declare module 'video.js' {
|
||||
export interface VideoJsPlayer {
|
||||
@@ -11,6 +12,7 @@ declare global {
|
||||
electron: {
|
||||
getAppVersion: () => Promise<string>;
|
||||
platform: string;
|
||||
fetchPlaylistByUrl: (url: string) => Promise<Playlist>;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { ParsedPlaylist, ParsedPlaylistItem } from '../typings';
|
||||
import { Channel } from './channel.interface';
|
||||
import { GLOBAL_FAVORITES_PLAYLIST_ID } from './constants';
|
||||
import { Playlist } from './playlist.interface';
|
||||
|
||||
/**
|
||||
* Aggregates favorite channels as objects from all available playlists
|
||||
* @param playlists all available playlists
|
||||
* @returns an array with favorite channels from all playlists
|
||||
*/
|
||||
export function aggregateFavoriteChannels(playlists: Playlist[]): Channel[] {
|
||||
const favorites: Channel[] = [];
|
||||
playlists.forEach((playlist) => {
|
||||
if (playlist.favorites?.length && playlist.favorites.length > 0) {
|
||||
playlist?.playlist?.items.forEach((channel: Channel) => {
|
||||
if (
|
||||
playlist.favorites &&
|
||||
(playlist.favorites.includes(channel.id) ||
|
||||
playlist.favorites.includes(channel.url))
|
||||
) {
|
||||
favorites.push(channel);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return favorites;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a simplified playlist object which is used for global favorites
|
||||
* @param channels channels list
|
||||
* @returns simplified playlist object
|
||||
*/
|
||||
export function createFavoritesPlaylist(
|
||||
channels: Channel[]
|
||||
): Partial<Playlist> {
|
||||
return {
|
||||
_id: GLOBAL_FAVORITES_PLAYLIST_ID,
|
||||
count: channels.length,
|
||||
playlist: {
|
||||
items: channels,
|
||||
},
|
||||
filename: 'Global favorites',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns last segment (part after last slash "/") of the given URL
|
||||
* @param value URL as string
|
||||
*/
|
||||
export const getFilenameFromUrl = (value: string): string => {
|
||||
if (value && value.length > 1) {
|
||||
return value.substring(value.lastIndexOf('/') + 1);
|
||||
}
|
||||
return 'Untitled playlist';
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a playlist object
|
||||
* @param name name of the playlist
|
||||
* @param playlist playlist to save
|
||||
* @param urlOrPath absolute fs path or url of the playlist
|
||||
* @param uploadType upload type - by file or via an url
|
||||
*/
|
||||
export const createPlaylistObject = (
|
||||
name: string,
|
||||
playlist: ParsedPlaylist,
|
||||
urlOrPath?: string,
|
||||
uploadType?: 'URL' | 'FILE' | 'TEXT'
|
||||
): Playlist => {
|
||||
return {
|
||||
_id: uuidv4(),
|
||||
filename: name,
|
||||
title: name,
|
||||
count: playlist.items.length,
|
||||
playlist: {
|
||||
...playlist,
|
||||
items: playlist.items.map((item: ParsedPlaylistItem) => ({
|
||||
...item,
|
||||
id: uuidv4(),
|
||||
})),
|
||||
},
|
||||
importDate: new Date().toISOString(),
|
||||
lastUsage: new Date().toISOString(),
|
||||
favorites: [],
|
||||
autoRefresh: false,
|
||||
...(uploadType === 'URL' ? { url: urlOrPath } : {}),
|
||||
...(uploadType === 'FILE' ? { filePath: urlOrPath } : {}),
|
||||
};
|
||||
};
|
||||
|
||||
export const getExtensionFromUrl = (url: string) => {
|
||||
return url.split(/[#?]/)[0].split('.').pop()?.trim();
|
||||
};
|
||||
Vendored
+2
-1
@@ -8,7 +8,7 @@ interface Window {
|
||||
require: any;
|
||||
}
|
||||
|
||||
export interface ParsedPlaylist {
|
||||
/* export interface ParsedPlaylist {
|
||||
header: {
|
||||
attrs: {
|
||||
'x-tvg-url': string;
|
||||
@@ -37,3 +37,4 @@ export interface ParsedPlaylistItem {
|
||||
url: string;
|
||||
raw: string;
|
||||
}
|
||||
*/
|
||||
@@ -0,0 +1,21 @@
|
||||
import baseConfig from '../../../eslint.config.mjs';
|
||||
|
||||
export default [
|
||||
...baseConfig,
|
||||
{
|
||||
files: ['**/*.json'],
|
||||
rules: {
|
||||
'@nx/dependency-checks': [
|
||||
'error',
|
||||
{
|
||||
ignoredFiles: [
|
||||
'{projectRoot}/eslint.config.{js,cjs,mjs,ts,cts,mts}',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
languageOptions: {
|
||||
parser: await import('jsonc-eslint-parser'),
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,13 @@
|
||||
export default {
|
||||
displayName: 'shared-interfaces',
|
||||
preset: '../../../jest.preset.js',
|
||||
testEnvironment: 'node',
|
||||
transform: {
|
||||
'^.+\\.[tj]s$': [
|
||||
'ts-jest',
|
||||
{ tsconfig: '<rootDir>/tsconfig.spec.json' },
|
||||
],
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'js', 'html'],
|
||||
coverageDirectory: '../../../coverage/libs/shared/interfaces',
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "shared-interfaces",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "commonjs",
|
||||
"main": "./src/index.js",
|
||||
"types": "./src/index.d.ts",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "shared-interfaces",
|
||||
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/shared/interfaces/src",
|
||||
"projectType": "library",
|
||||
"tags": ["shared"],
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nx/js:tsc",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"outputPath": "dist/libs/shared/interfaces",
|
||||
"main": "libs/shared/interfaces/src/index.ts",
|
||||
"tsConfig": "libs/shared/interfaces/tsconfig.lib.json",
|
||||
"assets": []
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||
"options": {
|
||||
"jestConfig": "libs/shared/interfaces/jest.config.ts"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './lib/channel.interface';
|
||||
export * from './lib/constants';
|
||||
export * from './lib/parsed-playlist.interface';
|
||||
export * from './lib/playlist.interface';
|
||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,29 @@
|
||||
export interface ParsedPlaylist {
|
||||
header: {
|
||||
attrs: {
|
||||
'x-tvg-url': string;
|
||||
};
|
||||
raw: string;
|
||||
};
|
||||
items: ParsedPlaylistItem[];
|
||||
}
|
||||
|
||||
export interface ParsedPlaylistItem {
|
||||
name: string;
|
||||
tvg: {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
logo: string;
|
||||
rec: string;
|
||||
};
|
||||
group: {
|
||||
title: string;
|
||||
};
|
||||
http: {
|
||||
referrer: string;
|
||||
'user-agent': string;
|
||||
};
|
||||
url: string;
|
||||
raw: string;
|
||||
}
|
||||
File renamed without changes.
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"importHelpers": true,
|
||||
"noImplicitOverride": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noPropertyAccessFromIndexSignature": true
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node10",
|
||||
"types": ["jest", "node"]
|
||||
},
|
||||
"include": [
|
||||
"jest.config.ts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import baseConfig from '../../../eslint.config.mjs';
|
||||
|
||||
export default [
|
||||
...baseConfig,
|
||||
{
|
||||
files: ['**/*.json'],
|
||||
rules: {
|
||||
'@nx/dependency-checks': [
|
||||
'error',
|
||||
{
|
||||
ignoredFiles: [
|
||||
'{projectRoot}/eslint.config.{js,cjs,mjs,ts,cts,mts}',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
languageOptions: {
|
||||
parser: await import('jsonc-eslint-parser'),
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,13 @@
|
||||
export default {
|
||||
displayName: 'm3u-utils',
|
||||
preset: '../../../jest.preset.js',
|
||||
testEnvironment: 'node',
|
||||
transform: {
|
||||
'^.+\\.[tj]s$': [
|
||||
'ts-jest',
|
||||
{ tsconfig: '<rootDir>/tsconfig.spec.json' },
|
||||
],
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'js', 'html'],
|
||||
coverageDirectory: '../../../coverage/libs/shared/m3u-utils',
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "m3u-utils",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "commonjs",
|
||||
"main": "./src/index.js",
|
||||
"types": "./src/index.d.ts",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "m3u-utils",
|
||||
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/shared/m3u-utils/src",
|
||||
"projectType": "library",
|
||||
"tags": ["shared", "m3u"],
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nx/js:tsc",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"outputPath": "dist/libs/shared/m3u-utils",
|
||||
"main": "libs/shared/m3u-utils/src/index.ts",
|
||||
"tsConfig": "libs/shared/m3u-utils/tsconfig.lib.json",
|
||||
"assets": []
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||
"options": {
|
||||
"jestConfig": "libs/shared/m3u-utils/jest.config.ts"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './lib/playlist.utils';
|
||||
@@ -0,0 +1,98 @@
|
||||
import {
|
||||
Channel,
|
||||
GLOBAL_FAVORITES_PLAYLIST_ID,
|
||||
ParsedPlaylist,
|
||||
ParsedPlaylistItem,
|
||||
Playlist,
|
||||
} from 'shared-interfaces';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
/**
|
||||
* Aggregates favorite channels as objects from all available playlists
|
||||
* @param playlists all available playlists
|
||||
* @returns an array with favorite channels from all playlists
|
||||
*/
|
||||
export function aggregateFavoriteChannels(playlists: Playlist[]): Channel[] {
|
||||
const favorites: Channel[] = [];
|
||||
playlists.forEach((playlist) => {
|
||||
if (playlist.favorites?.length && playlist.favorites.length > 0) {
|
||||
playlist?.playlist?.items.forEach((channel: Channel) => {
|
||||
if (
|
||||
playlist.favorites &&
|
||||
(playlist.favorites.includes(channel.id) ||
|
||||
playlist.favorites.includes(channel.url))
|
||||
) {
|
||||
favorites.push(channel);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return favorites;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a simplified playlist object which is used for global favorites
|
||||
* @param channels channels list
|
||||
* @returns simplified playlist object
|
||||
*/
|
||||
export function createFavoritesPlaylist(
|
||||
channels: Channel[]
|
||||
): Partial<Playlist> {
|
||||
return {
|
||||
_id: GLOBAL_FAVORITES_PLAYLIST_ID,
|
||||
count: channels.length,
|
||||
playlist: {
|
||||
items: channels,
|
||||
},
|
||||
filename: 'Global favorites',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns last segment (part after last slash "/") of the given URL
|
||||
* @param value URL as string
|
||||
*/
|
||||
export const getFilenameFromUrl = (value: string): string => {
|
||||
if (value && value.length > 1) {
|
||||
return value.substring(value.lastIndexOf('/') + 1);
|
||||
}
|
||||
return 'Untitled playlist';
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a playlist object
|
||||
* @param name name of the playlist
|
||||
* @param playlist playlist to save
|
||||
* @param urlOrPath absolute fs path or url of the playlist
|
||||
* @param uploadType upload type - by file or via an url
|
||||
*/
|
||||
export const createPlaylistObject = (
|
||||
name: string,
|
||||
playlist: ParsedPlaylist,
|
||||
urlOrPath?: string,
|
||||
uploadType?: 'URL' | 'FILE' | 'TEXT'
|
||||
): Playlist => {
|
||||
return {
|
||||
_id: uuidv4(),
|
||||
filename: name,
|
||||
title: name,
|
||||
count: playlist.items.length,
|
||||
playlist: {
|
||||
...playlist,
|
||||
items: playlist.items.map((item: ParsedPlaylistItem) => ({
|
||||
...item,
|
||||
id: uuidv4(),
|
||||
})),
|
||||
},
|
||||
importDate: new Date().toISOString(),
|
||||
lastUsage: new Date().toISOString(),
|
||||
favorites: [],
|
||||
autoRefresh: false,
|
||||
...(uploadType === 'URL' ? { url: urlOrPath } : {}),
|
||||
...(uploadType === 'FILE' ? { filePath: urlOrPath } : {}),
|
||||
};
|
||||
};
|
||||
|
||||
export const getExtensionFromUrl = (url: string) => {
|
||||
return url.split(/[#?]/)[0].split('.').pop()?.trim();
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"importHelpers": true,
|
||||
"noImplicitOverride": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noPropertyAccessFromIndexSignature": true
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node10",
|
||||
"types": ["jest", "node"]
|
||||
},
|
||||
"include": [
|
||||
"jest.config.ts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
@@ -12,7 +12,10 @@
|
||||
"!{projectRoot}/src/test-setup.[jt]s",
|
||||
"!{projectRoot}/test-setup.[jt]s"
|
||||
],
|
||||
"sharedGlobals": ["{workspaceRoot}/.github/workflows/ci.yml"]
|
||||
"sharedGlobals": [
|
||||
"{workspaceRoot}/.github/workflows/ci.yml",
|
||||
"{workspaceRoot}/.github/workflows/ci.yml"
|
||||
]
|
||||
},
|
||||
"nxCloudId": "68be99a8c4fb535f60d0d8e8",
|
||||
"targetDefaults": {
|
||||
@@ -46,6 +49,11 @@
|
||||
"codeCoverage": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@nx/js:tsc": {
|
||||
"cache": true,
|
||||
"dependsOn": ["^build"],
|
||||
"inputs": ["production", "^production"]
|
||||
}
|
||||
},
|
||||
"plugins": [
|
||||
|
||||
Generated
+1
@@ -130,6 +130,7 @@
|
||||
"jest-environment-node": "^30.0.2",
|
||||
"jest-preset-angular": "~15.0.0",
|
||||
"jest-util": "^30.0.2",
|
||||
"jsonc-eslint-parser": "^2.1.0",
|
||||
"ng-mocks": "14.13.2",
|
||||
"node-polyfill-webpack-plugin": "1.1.4",
|
||||
"npm-run-all": "4.1.5",
|
||||
|
||||
+12
-11
@@ -56,6 +56,16 @@
|
||||
"@ngx-pwa/local-storage": "20.0.0",
|
||||
"@rx-angular/cdk": "20.0.0",
|
||||
"@rx-angular/template": "20.0.0",
|
||||
"@tauri-apps/api": "2.3.0",
|
||||
"@tauri-apps/plugin-dialog": "2.2.0",
|
||||
"@tauri-apps/plugin-fs": "2.2.0",
|
||||
"@tauri-apps/plugin-http": "2.3.0",
|
||||
"@tauri-apps/plugin-os": "2.2.0",
|
||||
"@tauri-apps/plugin-process": "2.2.0",
|
||||
"@tauri-apps/plugin-shell": "2.2.0",
|
||||
"@tauri-apps/plugin-sql": "2.2.0",
|
||||
"@tauri-apps/plugin-updater": "2.5.1",
|
||||
"@tauri-apps/plugin-window-state": "2.2.1",
|
||||
"@videojs/http-streaming": "2.15.0",
|
||||
"@yangkghjh/videojs-aspect-ratio-panel": "0.0.1",
|
||||
"artplayer": "5.3.0",
|
||||
@@ -76,17 +86,7 @@
|
||||
"video.js": "7.20.3",
|
||||
"videojs-contrib-quality-levels": "2.2.0",
|
||||
"videojs-hls-quality-selector": "1.1.4",
|
||||
"zone.js": "~0.15.1",
|
||||
"@tauri-apps/api": "2.3.0",
|
||||
"@tauri-apps/plugin-dialog": "2.2.0",
|
||||
"@tauri-apps/plugin-fs": "2.2.0",
|
||||
"@tauri-apps/plugin-http": "2.3.0",
|
||||
"@tauri-apps/plugin-os": "2.2.0",
|
||||
"@tauri-apps/plugin-process": "2.2.0",
|
||||
"@tauri-apps/plugin-shell": "2.2.0",
|
||||
"@tauri-apps/plugin-sql": "2.2.0",
|
||||
"@tauri-apps/plugin-updater": "2.5.1",
|
||||
"@tauri-apps/plugin-window-state": "2.2.1"
|
||||
"zone.js": "~0.15.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/core": "~20.1.0",
|
||||
@@ -156,6 +156,7 @@
|
||||
"jest-environment-node": "^30.0.2",
|
||||
"jest-preset-angular": "~15.0.0",
|
||||
"jest-util": "^30.0.2",
|
||||
"jsonc-eslint-parser": "^2.1.0",
|
||||
"ng-mocks": "14.13.2",
|
||||
"node-polyfill-webpack-plugin": "1.1.4",
|
||||
"npm-run-all": "4.1.5",
|
||||
|
||||
+3
-1
@@ -15,7 +15,9 @@
|
||||
"skipDefaultLibCheck": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@package": ["./package.json"]
|
||||
"@package": ["./package.json"],
|
||||
"m3u-utils": ["libs/shared/m3u-utils/src/index.ts"],
|
||||
"shared-interfaces": ["libs/shared/interfaces/src/index.ts"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "tmp"]
|
||||
|
||||
Reference in new issue
Block a user