mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
refactor: switch to iptv-playlist-parser
This commit is contained in:
1 parent
4dabc85c6b
commit
19ef7e18bf
9 files changed
+97
-69
No files matched your search
Generated
+30
-11
@@ -7426,6 +7426,9 @@
|
||||
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
|
||||
"dev": true
|
||||
},
|
||||
"iptv-playlist-parser": {
|
||||
"version": "git+https://github.com/4gray/iptv-playlist-parser.git#519d10a58128496e246f24bc2cc36dd0ec288c6b"
|
||||
},
|
||||
"is-absolute-url": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-2.1.0.tgz",
|
||||
@@ -9014,11 +9017,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"m3u8-file-parser": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/m3u8-file-parser/-/m3u8-file-parser-0.2.3.tgz",
|
||||
"integrity": "sha512-Iz5aPr9J9PYBKyKK8+HYlg4Oqcwcg+TsxfoKVD1Mn0OqWES4Y82Na61XgMeLmKCujz+lxS/ygCNEdodgRLiOiQ=="
|
||||
},
|
||||
"magic-string": {
|
||||
"version": "0.25.7",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz",
|
||||
@@ -12168,7 +12166,14 @@
|
||||
"safe-buffer": "5.1.2",
|
||||
"tough-cookie": "2.4.3",
|
||||
"tunnel-agent": "0.6.0",
|
||||
"uuid": "3.3.3"
|
||||
"uuid": "3.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"uuid": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
|
||||
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"require-directory": {
|
||||
@@ -14324,7 +14329,7 @@
|
||||
"requires": {
|
||||
"debug": "3.2.6",
|
||||
"request": "2.88.0",
|
||||
"uuid": "3.3.3"
|
||||
"uuid": "3.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
@@ -14335,6 +14340,12 @@
|
||||
"requires": {
|
||||
"ms": "2.1.2"
|
||||
}
|
||||
},
|
||||
"uuid": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
|
||||
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -14526,9 +14537,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"uuid": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz",
|
||||
"integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ=="
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.2.0.tgz",
|
||||
"integrity": "sha512-CYpGiFTUrmI6OBMkAdjSDM0k5h8SkkiTP4WAjQgDgNB1S3Ou9VBEvr6q0Kv2H1mMk7IWfxYGpMH5sd5AvcIV2Q=="
|
||||
},
|
||||
"validate-npm-package-license": {
|
||||
"version": "3.0.4",
|
||||
@@ -15183,7 +15194,15 @@
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-colors": "3.2.4",
|
||||
"uuid": "3.3.3"
|
||||
"uuid": "3.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"uuid": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
|
||||
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"webpack-merge": {
|
||||
|
||||
+2
-1
@@ -26,13 +26,14 @@
|
||||
"@angular/service-worker": "~9.1.11",
|
||||
"@datorama/akita": "^4.23.2",
|
||||
"hls.js": "^0.12.4",
|
||||
"m3u8-file-parser": "^0.2.3",
|
||||
"iptv-playlist-parser": "git+https://github.com/4gray/iptv-playlist-parser.git",
|
||||
"material-icons": "^0.3.1",
|
||||
"ngx-filter-pipe": "^2.1.2",
|
||||
"ngx-indexed-db": "^5.0.4",
|
||||
"ngx-uploader": "^7.1.0",
|
||||
"rxjs": "~6.5.5",
|
||||
"tslib": "^1.13.0",
|
||||
"uuid": "^8.2.0",
|
||||
"zone.js": "~0.10.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
<mat-list id="top-panel">
|
||||
<mat-list-item>
|
||||
<mat-form-field class="full-width">
|
||||
<input matInput placeholder="Search channel {{channelList?.length}}" [(ngModel)]="searchTerm.inf.title">
|
||||
<input matInput placeholder="Search channel ({{channelList?.length}})" [(ngModel)]="searchTerm.name">
|
||||
</mat-form-field>
|
||||
<button mat-icon-button [routerLink]="'/'" matTooltip="Upload or select other playlist"><mat-icon>create_new_folder</mat-icon></button>
|
||||
<button mat-icon-button [routerLink]="'/'" matTooltip="Upload or select other playlist">
|
||||
<mat-icon>create_new_folder</mat-icon>
|
||||
</button>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
<mat-nav-list id="channels-list">
|
||||
<mat-list-item *ngFor=" let channel of channelList | filterBy: searchTerm; index as i" [class.active]="selected?.id === channel.id" (click)="selectChannel(channel)">
|
||||
{{i+1 + '. ' + channel?.inf?.title || 'Unnamed Channel'}}
|
||||
<mat-list-item *ngFor=" let channel of channelList | filterBy: searchTerm; index as i"
|
||||
[class.active]="selected?.id === channel.id" (click)="selectChannel(channel)">
|
||||
{{i+1 + '. ' + channel?.name || 'Unnamed Channel'}}
|
||||
<mat-divider></mat-divider>
|
||||
</mat-list-item>
|
||||
</mat-nav-list>
|
||||
@@ -22,16 +22,14 @@ export class ChannelListContainerComponent {
|
||||
*/
|
||||
@Output() changeChannel: EventEmitter<{
|
||||
url: string;
|
||||
title: string;
|
||||
name: string;
|
||||
}> = new EventEmitter();
|
||||
|
||||
/**
|
||||
* Search term for channel filter
|
||||
*/
|
||||
searchTerm: any = {
|
||||
inf: {
|
||||
title: '',
|
||||
},
|
||||
name: '',
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -40,6 +38,6 @@ export class ChannelListContainerComponent {
|
||||
*/
|
||||
selectChannel(channel: Channel): void {
|
||||
this.selected = channel;
|
||||
this.changeChannel.emit({ url: channel.url, title: channel.inf.title});
|
||||
this.changeChannel.emit({ url: channel.url, name: channel.name });
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
</mat-card>
|
||||
<br />
|
||||
<mat-card>
|
||||
<mat-card-subtitle>Add playlist</mat-card-subtitle>
|
||||
<mat-card-subtitle>Add playlist via file upload</mat-card-subtitle>
|
||||
<div class="drop-container" ngFileDrop [options]="options" (uploadOutput)="onUploadOutput($event)"
|
||||
[uploadInput]="uploadInput">
|
||||
<div>
|
||||
|
||||
@@ -6,12 +6,12 @@ import {
|
||||
humanizeBytes,
|
||||
UploaderOptions,
|
||||
} from 'ngx-uploader';
|
||||
import { ChannelStore, createChannel } from 'src/app/state';
|
||||
import { ChannelStore, createChannel, Channel } from 'src/app/state';
|
||||
import { M3uService } from 'src/app/services/m3u-service.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { NgxIndexedDBService } from 'ngx-indexed-db';
|
||||
import { PLAYLISTS_STORE } from 'src/app/db.config';
|
||||
import {MatSnackBar} from '@angular/material/snack-bar';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
|
||||
/**
|
||||
* Describes playlist interface
|
||||
@@ -74,18 +74,13 @@ export class PlaylistUploaderComponent {
|
||||
this.isLoading = true;
|
||||
if (this.files.length > 0) {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = fileLoadedEvent => {
|
||||
fileReader.onload = (fileLoadedEvent) => {
|
||||
const result = (fileLoadedEvent.target as FileReader)
|
||||
.result;
|
||||
|
||||
const array = (result as string).split('\n');
|
||||
const playlist = this.m3uService.convertArrayToPlaylist(
|
||||
array
|
||||
);
|
||||
this.savePlaylist(
|
||||
this.files[0].name,
|
||||
playlist
|
||||
);
|
||||
const playlist = this.m3uService.parsePlaylist(array);
|
||||
this.savePlaylist(this.files[0].name, playlist);
|
||||
|
||||
this.setPlaylist(playlist);
|
||||
};
|
||||
@@ -101,7 +96,7 @@ export class PlaylistUploaderComponent {
|
||||
typeof output.file !== 'undefined'
|
||||
) {
|
||||
const index = this.files.findIndex(
|
||||
file =>
|
||||
(file) =>
|
||||
typeof output.file !== 'undefined' &&
|
||||
file.id === output.file.id
|
||||
);
|
||||
@@ -138,9 +133,17 @@ export class PlaylistUploaderComponent {
|
||||
* @param playlist playlist to save
|
||||
*/
|
||||
savePlaylist(name: string, playlist: any): void {
|
||||
this.dbService.add<Playlist>('playlists', { filename: name, title: name, playlist, importDate: new Date().getMilliseconds(), lastUsage: new Date().getMilliseconds() }).then(() => {
|
||||
console.log('playlist saved!')
|
||||
});
|
||||
this.dbService
|
||||
.add<Playlist>('playlists', {
|
||||
filename: name,
|
||||
title: name,
|
||||
playlist,
|
||||
importDate: new Date().getMilliseconds(),
|
||||
lastUsage: new Date().getMilliseconds(),
|
||||
})
|
||||
.then(() => {
|
||||
console.log('playlist saved!');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -148,8 +151,8 @@ export class PlaylistUploaderComponent {
|
||||
*/
|
||||
getPlaylists(): void {
|
||||
this.dbService.getAll('playlists').then(
|
||||
(playlists: Playlist[]) => this.playlists = playlists,
|
||||
error => {
|
||||
(playlists: Playlist[]) => (this.playlists = playlists),
|
||||
(error) => {
|
||||
console.error(error);
|
||||
}
|
||||
);
|
||||
@@ -158,10 +161,13 @@ export class PlaylistUploaderComponent {
|
||||
/**
|
||||
* Sets the given playlist as active for the current session
|
||||
* @param playlist m3u playlist
|
||||
* // TODO: use typings from iptv-parser lib
|
||||
*/
|
||||
setPlaylist(playlist: any): void {
|
||||
this.channelStore.reset();
|
||||
const channels = playlist.segments.map(element => createChannel(element));
|
||||
setPlaylist(playlist: { header: any; items: Channel[] }): void {
|
||||
this.channelStore.reset();
|
||||
const channels = playlist.items.map((element) =>
|
||||
createChannel(element)
|
||||
);
|
||||
this.channelStore.upsertMany(channels);
|
||||
this.navigateToPlayer();
|
||||
}
|
||||
@@ -170,9 +176,11 @@ export class PlaylistUploaderComponent {
|
||||
* Removes the provided playlist from the indexedDb
|
||||
* @param playlist playlist to remove
|
||||
*/
|
||||
removePlaylist(playlist: Playlist) {
|
||||
removePlaylist(playlist: Playlist): void {
|
||||
this.dbService.delete(PLAYLISTS_STORE, playlist.id);
|
||||
this.getPlaylists();
|
||||
this.snackBar.open('Done! Playlist was removed.', null,{ duration: 2000 });
|
||||
this.snackBar.open('Done! Playlist was removed.', null, {
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
<mat-nav-list>
|
||||
<div mat-subheader>Recently added playlists</div>
|
||||
<div mat-subheader>Recently added playlists ({{playlists.length}})</div>
|
||||
<a mat-list-item *ngFor="let item of playlists; last as last" (click)="playlistClicked.emit(item.playlist);">
|
||||
<mat-icon mat-list-icon>folder</mat-icon>
|
||||
<div mat-line>{{ item.title || item.filename }}</div>
|
||||
<div mat-line>Channels: {{ item.playlist?.segments.length }}</div>
|
||||
<div mat-line>Channels: {{ item.playlist?.items?.length }}</div>
|
||||
<!--<button mat-icon-button color="primary" (click)="$event.stopPropagation(); renameClicked.emit(item)" matTooltip="Rename playlist">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>-->
|
||||
<button mat-icon-button color="primary" (click)="$event.stopPropagation(); removeClicked.emit(item)" matTooltip="Remove playlist">
|
||||
<button mat-icon-button color="primary" (click)="$event.stopPropagation(); removeClicked.emit(item)"
|
||||
matTooltip="Remove playlist">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
<mat-divider *ngIf="!last"></mat-divider>
|
||||
|
||||
@@ -1,30 +1,16 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import * as M3U8FileParser from 'm3u8-file-parser';
|
||||
import { parse } from 'iptv-playlist-parser';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class M3uService {
|
||||
/**
|
||||
* m3u8 parser
|
||||
*/
|
||||
m3u8FileParser = new M3U8FileParser();
|
||||
|
||||
/**
|
||||
* Converts string based array to playlist object
|
||||
* Parses string based array to playlist object
|
||||
* @param m3uArray m3u playlist as array with strings
|
||||
*/
|
||||
convertArrayToPlaylist(m3uArray: any[]): any {
|
||||
this.m3u8FileParser.read(m3uArray.join('\n'));
|
||||
return this.m3u8FileParser.getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts string to playlist structure
|
||||
* @param m3uString playlist as string
|
||||
*/
|
||||
convertStringToPlaylist(m3uString: string): any {
|
||||
this.m3u8FileParser.read(m3uString);
|
||||
return this.m3u8FileParser.getResult();
|
||||
parsePlaylist(m3uArray: any[]): any {
|
||||
const playlistAsString = m3uArray.join('\n');
|
||||
return parse(playlistAsString);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,27 @@
|
||||
import { ID, guid } from '@datorama/akita';
|
||||
|
||||
/**
|
||||
* Represents channel object
|
||||
* TODO: define channel interface in iptv-parser library
|
||||
*/
|
||||
export interface Channel {
|
||||
id: ID;
|
||||
inf: any;
|
||||
id: string;
|
||||
url: string;
|
||||
name: string;
|
||||
group: {
|
||||
title: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new channel object based on the given fields
|
||||
* @param params partial channel object
|
||||
*/
|
||||
export function createChannel(params: Partial<Channel>) {
|
||||
return {
|
||||
id: guid(),
|
||||
inf: params.inf,
|
||||
url: params.url
|
||||
name: params.name,
|
||||
group: params.group,
|
||||
url: params.url,
|
||||
} as Channel;
|
||||
}
|
||||
Reference in new issue
Block a user