added the ability to save playlists

added local db system (lowdb)

playlists are now downloaded as a zip from the streaming menu
pull/11/head
Isaac Grynsztein 6 years ago
parent 0095ea1271
commit d245904c0d

@ -6,9 +6,23 @@ var config = require('config');
var https = require('https');
var express = require("express");
var bodyParser = require("body-parser");
var archiver = require('archiver');
const low = require('lowdb')
var URL = require('url').URL;
const shortid = require('shortid')
var app = express();
var URL = require('url').URL;
const FileSync = require('lowdb/adapters/FileSync')
const adapter = new FileSync('db.json');
const db = low(adapter)
// Set some defaults
db.defaults({ playlists: {
audio: [],
video: []
}}).write();
// check if debug mode
let debugMode = process.env.YTDL_MODE === 'debug';
@ -180,6 +194,44 @@ function getVideoFormatID(name)
}
}
async function createPlaylistZipFile(fileNames, type, outputName) {
return new Promise(async resolve => {
let zipFolderPath = path.join(__dirname, (type === 'audio') ? audioFolderPath : videoFolderPath);
// let name = fileNames[0].split(' ')[0] + fileNames[1].split(' ')[0];
let ext = (type === 'audio') ? '.mp3' : '.mp4';
let output = fs.createWriteStream(path.join(zipFolderPath, outputName + '.zip'));
var archive = archiver('zip', {
gzip: true,
zlib: { level: 9 } // Sets the compression level.
});
archive.on('error', function(err) {
console.log(err);
throw err;
});
// pipe archive data to the output file
archive.pipe(output);
for (let i = 0; i < fileNames.length; i++) {
let fileName = fileNames[i];
archive.file(zipFolderPath + fileName + ext, {name: fileName + ext})
}
await archive.finalize();
// wait a tiny bit for the zip to reload in fs
setTimeout(function() {
resolve(path.join(zipFolderPath,outputName + '.zip'));
}, 100);
});
}
function deleteAudioFile(name) {
return new Promise(resolve => {
// TODO: split descriptors into audio and video descriptors, as deleting an audio file will close all video file streams
@ -491,6 +543,7 @@ app.post('/fileStatusMp4', function(req, res) {
// gets all download mp3s
app.post('/getMp3s', function(req, res) {
var mp3s = [];
var playlists = db.get('playlists.audio').value();
var fullpath = audioFolderPath;
var files = fs.readdirSync(audioFolderPath);
@ -519,7 +572,8 @@ app.post('/getMp3s', function(req, res) {
}
res.send({
mp3s: mp3s
mp3s: mp3s,
playlists: playlists
});
res.end("yes");
});
@ -527,6 +581,7 @@ app.post('/getMp3s', function(req, res) {
// gets all download mp4s
app.post('/getMp4s', function(req, res) {
var mp4s = [];
var playlists = db.get('playlists.video').value();
var fullpath = videoFolderPath;
var files = fs.readdirSync(videoFolderPath);
@ -555,11 +610,56 @@ app.post('/getMp4s', function(req, res) {
}
res.send({
mp4s: mp4s
mp4s: mp4s,
playlists: playlists
});
res.end("yes");
});
app.post('/createPlaylist', async (req, res) => {
let playlistName = req.body.playlistName;
let fileNames = req.body.fileNames;
let type = req.body.type;
let thumbnailURL = req.body.thumbnailURL;
let new_playlist = {
'name': playlistName,
fileNames: fileNames,
id: shortid.generate(),
thumbnailURL: thumbnailURL
};
db.get(`playlists.${type}`)
.push(new_playlist)
.write();
res.send({
new_playlist: new_playlist,
success: !!new_playlist // always going to be true
})
});
app.post('/deletePlaylist', async (req, res) => {
let playlistID = req.body.playlistID;
let type = req.body.type;
let success = null;
try {
// removes playlist from playlists
db.get(`playlists.${type}`)
.remove({id: playlistID})
.write();
success = true;
} catch(e) {
success = false;
}
res.send({
success: success
})
});
// deletes mp3 file
app.post('/deleteMp3', async (req, res) => {
var name = req.body.name;
@ -600,15 +700,20 @@ app.post('/deleteMp4', async (req, res) => {
}
});
app.post('/downloadFile', function(req, res) {
let fileName = req.body.fileName;
app.post('/downloadFile', async (req, res) => {
let fileNames = req.body.fileNames;
let is_playlist = req.body.is_playlist;
let type = req.body.type;
let outputName = req.body.outputName;
let file = null;
if (type === 'audio') {
file = __dirname + '/' + 'audio/' + fileName + '.mp3';
} else if (type === 'video') {
file = __dirname + '/' + 'video/' + fileName + '.mp4';
if (!is_playlist) {
if (type === 'audio') {
file = __dirname + '/' + 'audio/' + fileNames + '.mp3';
} else if (type === 'video') {
file = __dirname + '/' + 'video/' + fileNames + '.mp4';
}
} else {
file = await createPlaylistZipFile(fileNames, type, outputName);
}
res.sendFile(file);

@ -17,10 +17,13 @@
},
"homepage": "https://github.com/Tzahi12345/hda-backend#readme",
"dependencies": {
"archiver": "^3.1.1",
"async": "^3.1.0",
"config": "^3.2.3",
"exe": "^1.0.2",
"express": "^4.17.1",
"lowdb": "^1.0.0",
"shortid": "^2.2.15",
"youtube-dl": "^2.3.0"
}
}

@ -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]
})

@ -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>

@ -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) {

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

@ -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>

@ -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();
});
});

@ -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);
}
}
}
}

@ -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>

@ -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'];

@ -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});
}
}

Loading…
Cancel
Save