added the ability to save playlists

added local db system (lowdb)

playlists are now downloaded as a zip from the streaming menu
This commit is contained in:
Isaac Grynsztein
2020-02-19 02:29:10 -05:00
parent 0095ea1271
commit d245904c0d
12 changed files with 336 additions and 24 deletions

View File

@@ -5,7 +5,8 @@ import {MatNativeDateModule, MatRadioModule, MatInputModule, MatButtonModule, Ma
MatProgressBarModule, MatExpansionModule,
MatGridList,
MatProgressSpinnerModule,
MatButtonToggleModule} from '@angular/material';
MatButtonToggleModule,
MatDialogModule} from '@angular/material';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import { AppComponent } from './app.component';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
@@ -22,13 +23,15 @@ import {VgCoreModule} from 'videogular2/compiled/core';
import {VgControlsModule} from 'videogular2/compiled/controls';
import {VgOverlayPlayModule} from 'videogular2/compiled/overlay-play';
import {VgBufferingModule} from 'videogular2/compiled/buffering';
import { InputDialogComponent } from './input-dialog/input-dialog.component';
@NgModule({
declarations: [
AppComponent,
FileCardComponent,
MainComponent,
PlayerComponent
PlayerComponent,
InputDialogComponent
],
imports: [
BrowserModule,
@@ -54,6 +57,7 @@ import {VgBufferingModule} from 'videogular2/compiled/buffering';
MatProgressBarModule,
MatProgressSpinnerModule,
MatButtonToggleModule,
MatDialogModule,
VgCoreModule,
VgControlsModule,
VgOverlayPlayModule,
@@ -61,6 +65,9 @@ import {VgBufferingModule} from 'videogular2/compiled/buffering';
RouterModule,
AppRoutingModule
],
entryComponents: [
InputDialogComponent
],
providers: [PostsService],
bootstrap: [AppComponent]
})

View File

@@ -1,9 +1,10 @@
<mat-card class="example-card">
<button (click)="deleteFile()" class="deleteButton" mat-icon-button><mat-icon>delete_forever</mat-icon></button>
<div style="padding:5px">
<b><a href="javascript:void(0)" (click)="mainComponent.goToFile(name, isAudio)">{{title}}</a></b>
<b><a href="javascript:void(0)" (click)="!isPlaylist ? mainComponent.goToFile(name, isAudio) : mainComponent.goToPlaylist(name, type)">{{title}}</a></b>
<br/>
ID: {{name}}
<div *ngIf="isPlaylist">Count: {{count}}</div>
</div>

View File

@@ -17,21 +17,30 @@ export class FileCardComponent implements OnInit {
@Input() thumbnailURL: string;
@Input() isAudio = true;
@Output() removeFile: EventEmitter<string> = new EventEmitter<string>();
@Input() isPlaylist = false;
@Input() count = null;
type;
constructor(private postsService: PostsService, public snackBar: MatSnackBar, public mainComponent: MainComponent) { }
ngOnInit() {
this.type = this.isAudio ? 'audio' : 'video';
}
deleteFile() {
this.postsService.deleteFile(this.name, this.isAudio).subscribe(result => {
if (result === true) {
this.openSnackBar('Delete success!', 'OK.');
this.removeFile.emit(this.name);
} else {
this.openSnackBar('Delete failed!', 'OK.');
}
});
if (!this.isPlaylist) {
this.postsService.deleteFile(this.name, this.isAudio).subscribe(result => {
if (result === true) {
this.openSnackBar('Delete success!', 'OK.');
this.removeFile.emit(this.name);
} else {
this.openSnackBar('Delete failed!', 'OK.');
}
});
} else {
this.removeFile.emit(this.name);
}
}
public openSnackBar(message: string, action: string) {

View File

@@ -0,0 +1,3 @@
.mat-spinner {
margin-left: 5%;
}

View File

@@ -0,0 +1,16 @@
<h4 mat-dialog-title>{{inputTitle}}</h4>
<mat-dialog-content>
<div>
<mat-form-field>
<input matInput (keyup.enter)="enterPressed()" [(ngModel)]="inputText" [placeholder]="inputPlaceholder">
</mat-form-field>
</div>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button mat-dialog-close>Cancel</button>
<!-- The mat-dialog-close directive optionally accepts a value as a result for the dialog. -->
<button mat-button [disabled]="!inputText" type="submit" (click)="enterPressed()">{{submitText}}</button>
<div class="mat-spinner" *ngIf="inputSubmitted">
<mat-spinner [diameter]="25"></mat-spinner>
</div>
</mat-dialog-actions>

View File

@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { InputDialogComponent } from './input-dialog.component';
describe('InputDialogComponent', () => {
let component: InputDialogComponent;
let fixture: ComponentFixture<InputDialogComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ InputDialogComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(InputDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,50 @@
import { Component, OnInit, Input, Inject, EventEmitter } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
@Component({
selector: 'app-input-dialog',
templateUrl: './input-dialog.component.html',
styleUrls: ['./input-dialog.component.css']
})
export class InputDialogComponent implements OnInit {
inputTitle: string;
inputPlaceholder: string;
submitText: string;
inputText = '';
inputSubmitted = false;
doneEmitter: EventEmitter<any> = null;
onlyEmitOnDone = false;
constructor(public dialogRef: MatDialogRef<InputDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: any) { }
ngOnInit() {
this.inputTitle = this.data.inputTitle;
this.inputPlaceholder = this.data.inputPlaceholder;
this.submitText = this.data.submitText;
// checks if emitter exists, if so don't autoclose as it should be handled by caller
if (this.data.doneEmitter) {
this.doneEmitter = this.data.doneEmitter;
this.onlyEmitOnDone = true;
}
}
enterPressed() {
// validates input -- TODO: add custom validator
if (this.inputText) {
// only emit if emitter is passed
if (this.onlyEmitOnDone) {
this.doneEmitter.emit(this.inputText);
this.inputSubmitted = true;
} else {
this.dialogRef.close(this.inputText);
}
}
}
}

View File

@@ -91,12 +91,19 @@
</mat-panel-description>
</mat-expansion-panel-header>
<div *ngIf="mp3s.length > 0;else nomp3s">
<mat-grid-list (window:resize)="onResize($event)" [cols]="files_cols" rowHeight="150px">
<mat-grid-list style="margin-bottom: 15px;" (window:resize)="onResize($event)" [cols]="files_cols" rowHeight="150px">
<mat-grid-tile *ngFor="let file of mp3s; index as i;">
<app-file-card (removeFile)="removeFromMp3($event)" [title]="file.title" [name]="file.id" [thumbnailURL]="file.thumbnailURL"
[length]="file.duration" [isAudio]="true"></app-file-card>
</mat-grid-tile>
</mat-grid-list>
<mat-divider *ngIf="playlists.audio.length > 0"></mat-divider>
<mat-grid-list *ngIf="playlists.audio.length > 0" (window:resize)="onResize($event)" [cols]="files_cols" rowHeight="150px">
<mat-grid-tile *ngFor="let playlist of playlists.audio; let i = index;">
<app-file-card (removeFile)="removePlaylistMp3(playlist.id, i)" [title]="playlist.name" [name]="playlist.id" [thumbnailURL]="playlist_thumbnails[playlist.id]"
[length]="null" [isAudio]="true" [isPlaylist]="true" [count]="playlist.fileNames.length"></app-file-card>
</mat-grid-tile>
</mat-grid-list>
</div>
</mat-expansion-panel>
@@ -110,12 +117,19 @@
</mat-panel-description>
</mat-expansion-panel-header>
<div *ngIf="mp4s.length > 0;else nomp4s">
<mat-grid-list (window:resize)="onResize($event)" [cols]="files_cols" rowHeight="150px">
<mat-grid-list style="margin-bottom: 15px;" (window:resize)="onResize($event)" [cols]="files_cols" rowHeight="150px">
<mat-grid-tile *ngFor="let file of mp4s; index as i;">
<app-file-card (removeFile)="removeFromMp4($event)" [title]="file.title" [name]="file.id" [thumbnailURL]="file.thumbnailURL"
[length]="file.duration" [isAudio]="false"></app-file-card>
</mat-grid-tile>
</mat-grid-list>
<mat-divider *ngIf="playlists.video.length > 0"></mat-divider>
<mat-grid-list *ngIf="playlists.video.length > 0" (window:resize)="onResize($event)" [cols]="files_cols" rowHeight="150px">
<mat-grid-tile *ngFor="let playlist of playlists.video; let i = index;">
<app-file-card (removeFile)="removePlaylistMp4(playlist.id, i)" [title]="playlist.name" [name]="playlist.id" [thumbnailURL]="playlist_thumbnails[playlist.id]"
[length]="null" [isAudio]="false" [isPlaylist]="true" [count]="playlist.fileNames.length"></app-file-card>
</mat-grid-tile>
</mat-grid-list>
</div>
</mat-expansion-panel>
</mat-accordion>

View File

@@ -51,6 +51,8 @@ export class MainComponent implements OnInit {
mp3s: any[] = [];
mp4s: any[] = [];
files_cols = (window.innerWidth <= 450) ? 2 : 4;
playlists = {'audio': [], 'video': []};
playlist_thumbnails = {};
urlForm = new FormControl('', [Validators.required]);
@@ -194,7 +196,23 @@ export class MainComponent implements OnInit {
getMp3s() {
this.postsService.getMp3s().subscribe(result => {
const mp3s = result['mp3s'];
const playlists = result['playlists'];
this.mp3s = mp3s;
this.playlists.audio = playlists;
// get thumbnail url by using first video. this is a temporary hack
for (let i = 0; i < this.playlists.audio.length; i++) {
const playlist = this.playlists.audio[i];
let videoToExtractThumbnail = null;
for (let j = 0; j < this.mp3s.length; j++) {
if (this.mp3s[j].id === playlist.fileNames[0]) {
// found the corresponding file
videoToExtractThumbnail = this.mp3s[j];
}
}
this.playlist_thumbnails[playlist.id] = videoToExtractThumbnail.thumbnailURL;
}
}, error => {
console.log(error);
});
@@ -203,7 +221,23 @@ export class MainComponent implements OnInit {
getMp4s() {
this.postsService.getMp4s().subscribe(result => {
const mp4s = result['mp4s'];
const playlists = result['playlists'];
this.mp4s = mp4s;
this.playlists.video = playlists;
// get thumbnail url by using first video. this is a temporary hack
for (let i = 0; i < this.playlists.video.length; i++) {
const playlist = this.playlists.video[i];
let videoToExtractThumbnail = null;
for (let j = 0; j < this.mp4s.length; j++) {
if (this.mp4s[j].id === playlist.fileNames[0]) {
// found the corresponding file
videoToExtractThumbnail = this.mp4s[j];
}
}
this.playlist_thumbnails[playlist.id] = videoToExtractThumbnail.thumbnailURL;
}
},
error => {
console.log(error);
@@ -218,6 +252,17 @@ export class MainComponent implements OnInit {
}
}
public goToPlaylist(playlistID, type) {
for (let i = 0; i < this.playlists[type].length; i++) {
const playlist = this.playlists[type][i];
if (playlist.id === playlistID) {
// found the playlist, now go to it
const fileNames = playlist.fileNames;
this.router.navigate(['/player', {fileNames: fileNames.join('|nvr|'), type: type, id: playlistID}]);
}
}
}
public removeFromMp3(name: string) {
for (let i = 0; i < this.mp3s.length; i++) {
if (this.mp3s[i].id === name) {
@@ -226,6 +271,15 @@ export class MainComponent implements OnInit {
}
}
public removePlaylistMp3(playlistID, index) {
this.postsService.removePlaylist(playlistID, 'audio').subscribe(res => {
if (res['success']) {
this.playlists.audio.splice(index, 1);
}
this.getMp3s();
});
}
public removeFromMp4(name: string) {
for (let i = 0; i < this.mp4s.length; i++) {
if (this.mp4s[i].id === name) {
@@ -234,6 +288,16 @@ export class MainComponent implements OnInit {
}
}
public removePlaylistMp4(playlistID, index) {
this.postsService.removePlaylist(playlistID, 'video').subscribe(res => {
if (res['success']) {
this.playlists.video.splice(index, 1);
}
this.getMp4s();
});
}
// app initialization.
ngOnInit() {
this.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window['MSStream'];

View File

@@ -82,13 +82,28 @@ export class PostsService {
return this.http.post(this.path + 'getMp4s', {});
}
downloadFileFromServer(fileName, type) {
return this.http.post(this.path + 'downloadFile', {fileName: fileName, type: type}, {responseType: 'blob'});
downloadFileFromServer(fileName, type, outputName = null) {
return this.http.post(this.path + 'downloadFile', {fileNames: fileName,
type: type,
is_playlist: Array.isArray(fileName),
outputName: outputName},
{responseType: 'blob'});
}
getFileInfo(fileNames, type, urlMode) {
return this.http.post(this.path + 'getVideoInfos', {fileNames: fileNames, type: type, urlMode: urlMode});
}
createPlaylist(playlistName, fileNames, type, thumbnailURL) {
return this.http.post(this.path + 'createPlaylist', {playlistName: playlistName,
fileNames: fileNames,
type: type,
thumbnailURL: thumbnailURL});
}
removePlaylist(playlistID, type) {
return this.http.post(this.path + 'deletePlaylist', {playlistID: playlistID, type: type});
}
}