mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
feat(pwa): auto-generate playlist with global favorites
This commit is contained in:
1 parent
7e7e9391f6
commit
98ff7f4fa4
5 files changed
+168
-35
No files matched your search
@@ -1,11 +1,20 @@
|
||||
<mat-nav-list cdkDropList (cdkDropListDropped)="drop($event)">
|
||||
<mat-list-item *ngIf="playlists.length === 0">
|
||||
<mat-icon mat-list-icon>cloud</mat-icon>
|
||||
<mat-icon class="favorites-icon" mat-list-icon>search</mat-icon>
|
||||
<div mat-line>{{ 'HOME.PLAYLISTS.NO_PLAYLISTS' | translate }}</div>
|
||||
<div mat-line class="meta">
|
||||
{{ 'HOME.PLAYLISTS.ADD_FIRST' | translate }}
|
||||
</div>
|
||||
</mat-list-item>
|
||||
<mat-list-item *ngIf="playlists.length > 0" (click)="playlistClicked.emit('GLOBAL_FAVORITES')">
|
||||
<mat-icon class="favorites-icon" mat-list-icon>star</mat-icon>
|
||||
<div mat-line>Global favorites</div>
|
||||
<div mat-line class="meta">
|
||||
Auto-generated playlist with aggregated favorites from all playlists
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
</mat-list-item>
|
||||
|
||||
<mat-list-item
|
||||
cdkDrag
|
||||
*ngFor="let item of playlists; last as last"
|
||||
|
||||
@@ -57,3 +57,7 @@ mat-nav-list {
|
||||
cursor: move;
|
||||
margin-left: -20px;
|
||||
}
|
||||
|
||||
.favorites-icon {
|
||||
margin-left: 16px;
|
||||
}
|
||||
+1
-1
@@ -99,7 +99,7 @@
|
||||
</mat-nav-list>
|
||||
</mat-tab>
|
||||
|
||||
<mat-tab [label]="'CHANNELS.FAVORITES' | translate">
|
||||
<mat-tab *ngIf="playlistId !== 'GLOBAL_FAVORITES'" [label]="'CHANNELS.FAVORITES' | translate">
|
||||
<mat-nav-list
|
||||
*ngIf="favorites$ | async as favorites"
|
||||
id="favorites-list"
|
||||
|
||||
+8
-4
@@ -5,11 +5,10 @@ import {
|
||||
Input,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import { Channel, ChannelStore, ChannelQuery } from '../../../state';
|
||||
|
||||
import * as _ from 'lodash';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import * as _ from 'lodash';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Channel, ChannelQuery, ChannelStore } from '../../../state';
|
||||
|
||||
@Component({
|
||||
selector: 'app-channel-list-container',
|
||||
@@ -61,6 +60,9 @@ export class ChannelListContainerComponent {
|
||||
}
|
||||
}
|
||||
|
||||
/** ID of the current playlist */
|
||||
playlistId: string;
|
||||
|
||||
/**
|
||||
* Creates an instance of ChannelListContainerComponent
|
||||
* @param channelQuery akita's channel query
|
||||
@@ -71,7 +73,9 @@ export class ChannelListContainerComponent {
|
||||
private channelQuery: ChannelQuery,
|
||||
private channelStore: ChannelStore,
|
||||
private snackBar: MatSnackBar
|
||||
) {}
|
||||
) {
|
||||
this.playlistId = this.channelQuery.getValue().playlistId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets clicked channel as selected and emits them to the parent component
|
||||
|
||||
+145
-29
@@ -1,10 +1,12 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { guid } from '@datorama/akita';
|
||||
import { Channel } from 'diagnostics_channel';
|
||||
import { parse } from 'iptv-playlist-parser';
|
||||
import { NgxIndexedDBService } from 'ngx-indexed-db';
|
||||
import { switchMap } from 'rxjs';
|
||||
import { catchError, combineLatest, map, switchMap, throwError } from 'rxjs';
|
||||
import {
|
||||
ERROR,
|
||||
PLAYLIST_GET_ALL,
|
||||
PLAYLIST_GET_ALL_RESPONSE,
|
||||
PLAYLIST_GET_BY_ID,
|
||||
@@ -16,6 +18,7 @@ import {
|
||||
PLAYLIST_SAVE_DETAILS,
|
||||
PLAYLIST_UPDATE,
|
||||
PLAYLIST_UPDATE_FAVORITES,
|
||||
PLAYLIST_UPDATE_POSITIONS,
|
||||
} from '../../../shared/ipc-commands';
|
||||
import {
|
||||
Playlist,
|
||||
@@ -26,12 +29,19 @@ import { ParsedPlaylist } from '../../typings';
|
||||
import { DbStores } from '../indexed-db.config';
|
||||
import { DataService } from './data.service';
|
||||
|
||||
/**
|
||||
* Id of the channel with favorite channels aggregated from all added playlists
|
||||
*/
|
||||
export const GLOBAL_FAVORITES_PLAYLIST_ID = 'GLOBAL_FAVORITES';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class PwaService extends DataService {
|
||||
/** Proxy URL to avoid CORS issues */
|
||||
corsProxyUrl = 'https://api.codetabs.com/v1/proxy?quest=';
|
||||
corsProxyUrl = AppConfig.production
|
||||
? 'https://iptvnator-playlist-parser-api.vercel.app/parse?url='
|
||||
: 'http://localhost:3000/parse?url=';
|
||||
|
||||
/**
|
||||
* Creates an instance of PwaService.
|
||||
@@ -53,6 +63,81 @@ export class PwaService extends DataService {
|
||||
return AppConfig.version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregates favorite channels as objects from all available playlists
|
||||
* @param playlists all available playlists
|
||||
* @returns favorite channels
|
||||
*/
|
||||
aggregateFavoriteChannels(playlists: Playlist[]): Channel[] {
|
||||
const favorites = [];
|
||||
playlists.forEach((playlist) => {
|
||||
if (playlist.favorites?.length > 0) {
|
||||
playlist.playlist.items.forEach((channel) => {
|
||||
if (playlist.favorites.includes(channel.id)) {
|
||||
favorites.push(channel);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return favorites;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a simplified playlist object which is used for global favorites
|
||||
* @param channels channels list
|
||||
* @returns simplified playlist object
|
||||
*/
|
||||
createFavoritesPlaylist(channels: Channel[]): Partial<Playlist> {
|
||||
return {
|
||||
id: GLOBAL_FAVORITES_PLAYLIST_ID,
|
||||
_id: GLOBAL_FAVORITES_PLAYLIST_ID,
|
||||
count: channels.length,
|
||||
playlist: {
|
||||
items: channels,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the count of favorite channels from all playlists
|
||||
*/
|
||||
getGlobalFavoritesCount() {
|
||||
return this.dbService.getAll(DbStores.Playlists).pipe(
|
||||
map((playlists: Playlist[]) => {
|
||||
let count = 0;
|
||||
playlists.forEach((playlist) => {
|
||||
if (playlist.favorites.length > 0) {
|
||||
count = count + playlist.favorites.length;
|
||||
}
|
||||
});
|
||||
return count;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message with playlist that contains favorite channels from all available playlists
|
||||
*/
|
||||
sendPlaylistWithGlobalFavorites() {
|
||||
this.dbService
|
||||
.getAll(DbStores.Playlists)
|
||||
.pipe(
|
||||
map((playlists: Playlist[]) => {
|
||||
const favoriteChannels =
|
||||
this.aggregateFavoriteChannels(playlists);
|
||||
const favPlaylist =
|
||||
this.createFavoritesPlaylist(favoriteChannels);
|
||||
return favPlaylist;
|
||||
})
|
||||
)
|
||||
.subscribe((playlist) => {
|
||||
window.postMessage({
|
||||
type: PLAYLIST_PARSE_RESPONSE,
|
||||
payload: playlist,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles incoming IPC commands
|
||||
* @param type ipc command type
|
||||
@@ -80,14 +165,18 @@ export class PwaService extends DataService {
|
||||
payload: playlistObject,
|
||||
});
|
||||
} else if (type === PLAYLIST_GET_BY_ID) {
|
||||
this.dbService
|
||||
.getByIndex(DbStores.Playlists, '_id', payload.id)
|
||||
.subscribe((playlist) => {
|
||||
window.postMessage({
|
||||
type: PLAYLIST_PARSE_RESPONSE,
|
||||
payload: playlist,
|
||||
if (payload.id === GLOBAL_FAVORITES_PLAYLIST_ID) {
|
||||
this.sendPlaylistWithGlobalFavorites();
|
||||
} else {
|
||||
this.dbService
|
||||
.getByIndex(DbStores.Playlists, '_id', payload.id)
|
||||
.subscribe((playlist) => {
|
||||
window.postMessage({
|
||||
type: PLAYLIST_PARSE_RESPONSE,
|
||||
payload: playlist,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
} else if (type === PLAYLIST_REMOVE_BY_ID) {
|
||||
this.dbService
|
||||
.delete(DbStores.Playlists, payload.id)
|
||||
@@ -98,12 +187,16 @@ export class PwaService extends DataService {
|
||||
});
|
||||
});
|
||||
} else if (type === PLAYLIST_GET_ALL) {
|
||||
this.dbService.getAll(DbStores.Playlists).subscribe((playlists) => {
|
||||
window.postMessage({
|
||||
type: PLAYLIST_GET_ALL_RESPONSE,
|
||||
payload: playlists,
|
||||
this.dbService
|
||||
.getAll(DbStores.Playlists)
|
||||
.subscribe((playlists: Playlist[]) => {
|
||||
window.postMessage({
|
||||
type: PLAYLIST_GET_ALL_RESPONSE,
|
||||
payload: playlists.sort(
|
||||
(a, b) => a.position - b.position
|
||||
),
|
||||
});
|
||||
});
|
||||
});
|
||||
} else if (type === PLAYLIST_SAVE_DETAILS) {
|
||||
this.dbService
|
||||
.getByID(DbStores.Playlists, payload._id)
|
||||
@@ -142,6 +235,14 @@ export class PwaService extends DataService {
|
||||
.get(`${this.corsProxyUrl}${payload.url}`, {
|
||||
responseType: 'text',
|
||||
})
|
||||
.pipe(
|
||||
catchError((error) => {
|
||||
window.postMessage({
|
||||
type: ERROR,
|
||||
});
|
||||
return throwError(() => error);
|
||||
})
|
||||
)
|
||||
.subscribe((response: any) => {
|
||||
const refreshedPlaylist =
|
||||
this.convertFileStringToPlaylist(response);
|
||||
@@ -171,6 +272,23 @@ export class PwaService extends DataService {
|
||||
});
|
||||
});
|
||||
});
|
||||
} else if (type === PLAYLIST_UPDATE_POSITIONS) {
|
||||
const requests = payload.map((playlist, index) => {
|
||||
return this.dbService
|
||||
.getByID(DbStores.Playlists, playlist._id)
|
||||
.pipe(
|
||||
switchMap((playlist: Playlist) => {
|
||||
return this.dbService.update(DbStores.Playlists, {
|
||||
...playlist,
|
||||
position: index,
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
combineLatest(requests).subscribe(() => {
|
||||
console.log('playlist positions were updated...');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,29 +298,27 @@ export class PwaService extends DataService {
|
||||
*/
|
||||
fetchFromUrl(payload: Partial<Playlist>): void {
|
||||
this.http
|
||||
.get(`${this.corsProxyUrl}${payload.url}`, {
|
||||
responseType: 'text',
|
||||
})
|
||||
.get(`${this.corsProxyUrl}${payload.url}`)
|
||||
.pipe(
|
||||
catchError((error) => {
|
||||
window.postMessage({
|
||||
type: ERROR,
|
||||
message: 'something went wrong',
|
||||
status: error.status,
|
||||
});
|
||||
return throwError(() => error);
|
||||
})
|
||||
)
|
||||
.subscribe((response: any) => {
|
||||
const parsedPlaylist =
|
||||
this.convertFileStringToPlaylist(response);
|
||||
const playlistObject = this.createPlaylistObject(
|
||||
payload.title,
|
||||
parsedPlaylist,
|
||||
payload.url,
|
||||
'URL'
|
||||
);
|
||||
|
||||
// save to db
|
||||
this.dbService
|
||||
.add(DbStores.Playlists, playlistObject)
|
||||
.add(DbStores.Playlists, response)
|
||||
.subscribe(() => {
|
||||
console.log('playlist was added...');
|
||||
});
|
||||
|
||||
window.postMessage({
|
||||
type: PLAYLIST_PARSE_RESPONSE,
|
||||
payload: playlistObject,
|
||||
payload: response,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user