diff --git a/src/app/main/main.component.css b/src/app/main/main.component.css
index d0f9435..98e1899 100644
--- a/src/app/main/main.component.css
+++ b/src/app/main/main.component.css
@@ -36,6 +36,10 @@ mat-toolbar.top {
width: 100%;
}
+.example-80-width {
+ width: 80%
+}
+
mat-form-field.mat-form-field {
font-size: 24px;
}
@@ -65,4 +69,16 @@ mat-form-field.mat-form-field {
position: absolute;
right: -10px;
top: 5px;
+}
+
+.spinner-div {
+ display: inline-block;
+ position: absolute;
+ top: 15px;
+ right: -40px;
+}
+
+.margined {
+ margin-left: 20px;
+ margin-right: 20px;
}
\ No newline at end of file
diff --git a/src/app/main/main.component.html b/src/app/main/main.component.html
index f173278..65a3a4e 100644
--- a/src/app/main/main.component.html
+++ b/src/app/main/main.component.html
@@ -7,11 +7,32 @@
-
Only Audio
+
Only Audio
@@ -41,16 +62,18 @@
-
diff --git a/src/app/main/main.component.ts b/src/app/main/main.component.ts
index b9a5b7c..896b8c8 100644
--- a/src/app/main/main.component.ts
+++ b/src/app/main/main.component.ts
@@ -39,6 +39,8 @@ export class MainComponent implements OnInit {
audioFolderPath;
videoFolderPath;
+ cachedAvailableFormats = {};
+
// youtube api
youtubeSearchEnabled = false;
youtubeAPIKey = null;
@@ -52,7 +54,104 @@ export class MainComponent implements OnInit {
urlForm = new FormControl('', [Validators.required]);
+ qualityOptions = {
+ 'video': [
+ {
+ 'resolution': null,
+ 'value': '',
+ 'label': 'Max'
+ },
+ {
+ 'resolution': '3840x2160',
+ 'value': '2160',
+ 'label': '2160p (4K)'
+ },
+ {
+ 'resolution': '2560x1440',
+ 'value': '1440',
+ 'label': '1440p'
+ },
+ {
+ 'resolution': '1920x1080',
+ 'value': '1080',
+ 'label': '1080p'
+ },
+ {
+ 'resolution': '1280x720',
+ 'value': '720',
+ 'label': '720p'
+ },
+ {
+ 'resolution': '720x480',
+ 'value': '480',
+ 'label': '480p'
+ },
+ {
+ 'resolution': '480x360',
+ 'value': '360',
+ 'label': '360p'
+ },
+ {
+ 'resolution': '360x240',
+ 'value': '240',
+ 'label': '240p'
+ },
+ {
+ 'resolution': '256x144',
+ 'value': '144',
+ 'label': '144p'
+ }
+ ],
+ 'audio': [
+ {
+ 'kbitrate': null,
+ 'value': '',
+ 'label': 'Max'
+ },
+ {
+ 'kbitrate': '256',
+ 'value': '256K',
+ 'label': '256 Kbps'
+ },
+ {
+ 'kbitrate': '160',
+ 'value': '160K',
+ 'label': '160 Kbps'
+ },
+ {
+ 'kbitrate': '128',
+ 'value': '128K',
+ 'label': '128 Kbps'
+ },
+ {
+ 'kbitrate': '96',
+ 'value': '96K',
+ 'label': '96 Kbps'
+ },
+ {
+ 'kbitrate': '70',
+ 'value': '70K',
+ 'label': '70 Kbps'
+ },
+ {
+ 'kbitrate': '50',
+ 'value': '50K',
+ 'label': '50 Kbps'
+ },
+ {
+ 'kbitrate': '32',
+ 'value': '32K',
+ 'label': '32 Kbps'
+ }
+ ]
+ }
+
+ selectedQuality = '';
+ formats_loading = false;
+
@ViewChild('urlinput', { read: ElementRef, static: false }) urlInput: ElementRef;
+ last_valid_url = '';
+ last_url_check = 0;
constructor(private postsService: PostsService, private youtubeSearch: YoutubeSearchService, public snackBar: MatSnackBar,
private router: Router) {
@@ -67,7 +166,8 @@ export class MainComponent implements OnInit {
this.baseStreamPath = result['YoutubeDLMaterial']['Downloader']['path-base'];
this.audioFolderPath = result['YoutubeDLMaterial']['Downloader']['path-audio'];
this.videoFolderPath = result['YoutubeDLMaterial']['Downloader']['path-video'];
- this.youtubeSearchEnabled = result['YoutubeDLMaterial']['API'] && result['YoutubeDLMaterial']['API']['use_youtube_API'];
+ this.youtubeSearchEnabled = result['YoutubeDLMaterial']['API'] && result['YoutubeDLMaterial']['API']['use_youtube_API'] &&
+ result['YoutubeDLMaterial']['API']['youtube_API_key'];
this.youtubeAPIKey = this.youtubeSearchEnabled ? result['YoutubeDLMaterial']['API']['youtube_API_key'] : null;
this.postsService.path = backendUrl;
@@ -150,17 +250,18 @@ export class MainComponent implements OnInit {
if (forceView === false && this.downloadOnlyMode && !this.iOS) {
if (is_playlist) {
for (let i = 0; i < name.length; i++) {
- this.downloadAudioFile(name[i]);
+ this.downloadAudioFile(decodeURI(name[i]));
}
} else {
- this.downloadAudioFile(name);
+ this.downloadAudioFile(decodeURI(name));
}
} else {
if (is_playlist) {
this.router.navigate(['/player', {fileNames: name.join('|nvr|'), type: 'audio'}]);
// window.location.href = this.baseStreamPath + this.audioFolderPath + name[0] + '.mp3';
} else {
- window.location.href = this.baseStreamPath + this.audioFolderPath + name + '.mp3';
+ this.router.navigate(['/player', {fileNames: name, type: 'audio'}]);
+ // window.location.href = this.baseStreamPath + this.audioFolderPath + name + '.mp3';
}
}
@@ -177,10 +278,10 @@ export class MainComponent implements OnInit {
if (forceView === false && this.downloadOnlyMode) {
if (is_playlist) {
for (let i = 0; i < name.length; i++) {
- this.downloadVideoFile(name[i]);
+ this.downloadVideoFile(decodeURI(name[i]));
}
} else {
- this.downloadVideoFile(name);
+ this.downloadVideoFile(decodeURI(name));
}
} else {
if (is_playlist) {
@@ -206,7 +307,17 @@ export class MainComponent implements OnInit {
if (this.audioOnly) {
this.downloadingfile = true;
- this.postsService.makeMP3(this.url).subscribe(posts => {
+
+ let customQualityConfiguration = null;
+ if (this.selectedQuality !== '') {
+ const cachedFormatsExists = this.cachedAvailableFormats[this.url];
+ if (cachedFormatsExists) {
+ const audio_formats = this.cachedAvailableFormats[this.url]['audio'];
+ customQualityConfiguration = audio_formats[this.selectedQuality]['format_id'];
+ }
+ }
+ this.postsService.makeMP3(this.url, (this.selectedQuality === '' ? null : this.selectedQuality),
+ customQualityConfiguration).subscribe(posts => {
const is_playlist = !!(posts['file_names']);
this.path = is_playlist ? posts['file_names'] : posts['audiopathEncoded'];
if (this.path !== '-1') {
@@ -217,8 +328,20 @@ export class MainComponent implements OnInit {
this.openSnackBar('Download failed!', 'OK.');
});
} else {
+ let customQualityConfiguration = null;
+ const cachedFormatsExists = this.cachedAvailableFormats[this.url];
+ if (cachedFormatsExists) {
+ const video_formats = this.cachedAvailableFormats[this.url]['video'];
+ if (video_formats['best_audio_format'] && this.selectedQuality !== ''/* &&
+ video_formats[this.selectedQuality]['acodec'] === 'none'*/) {
+ console.log(this.selectedQuality);
+ customQualityConfiguration = video_formats[this.selectedQuality]['format_id'] + '+' + video_formats['best_audio_format'];
+ }
+ }
+
this.downloadingfile = true;
- this.postsService.makeMP4(this.url).subscribe(posts => {
+ this.postsService.makeMP4(this.url, (this.selectedQuality === '' ? null : this.selectedQuality),
+ customQualityConfiguration).subscribe(posts => {
const is_playlist = !!(posts['file_names']);
this.path = is_playlist ? posts['file_names'] : posts['videopathEncoded'];
if (this.path !== '-1') {
@@ -239,10 +362,13 @@ export class MainComponent implements OnInit {
const blob: Blob = res;
saveAs(blob, name + '.mp3');
- // tell server to delete the file once downloaded
- this.postsService.deleteFile(name, true).subscribe(delRes => {
-
- });
+ if (!this.fileManagerEnabled) {
+ // tell server to delete the file once downloaded
+ this.postsService.deleteFile(name, true).subscribe(delRes => {
+ // reload mp3s
+ this.getMp3s();
+ });
+ }
});
}
@@ -251,10 +377,13 @@ export class MainComponent implements OnInit {
const blob: Blob = res;
saveAs(blob, name + '.mp4');
- // tell server to delete the file once downloaded
- this.postsService.deleteFile(name, false).subscribe(delRes => {
-
- });
+ if (!this.fileManagerEnabled) {
+ // tell server to delete the file once downloaded
+ this.postsService.deleteFile(name, false).subscribe(delRes => {
+ // reload mp4s
+ this.getMp4s();
+ });
+ }
});
}
@@ -291,7 +420,22 @@ export class MainComponent implements OnInit {
// tslint:disable-next-line: max-line-length
const strRegex = /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)/;
const re = new RegExp(strRegex);
- return re.test(str);
+ const valid = re.test(str);
+
+ if (!valid) { return false; }
+
+ // tslint:disable-next-line: max-line-length
+ const youtubeStrRegex = /(?:http(?:s)?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:(?:watch)?\?(?:.*&)?v(?:i)?=|(?:embed|v|vi|user)\/))([^\?&\"'<> #]+)/;
+ const reYT = new RegExp(youtubeStrRegex);
+ const ytValid = reYT.test(str);
+ if (valid && ytValid && Date.now() - this.last_url_check > 1000) {
+ if (str !== this.last_valid_url) {
+ // get info
+ this.getURLInfo(str);
+ }
+ this.last_valid_url = str;
+ }
+ return valid;
}
// snackbar helper
@@ -301,6 +445,22 @@ export class MainComponent implements OnInit {
});
}
+ getURLInfo(url) {
+ console.log(this.cachedAvailableFormats[url]);
+ if (!(this.cachedAvailableFormats[url])) {
+ this.formats_loading = true;
+ console.log('has no cached formats available');
+ this.postsService.getFileInfo([url], 'irrelevant', true).subscribe(res => {
+ if (url === this.url) { this.formats_loading = false; }
+ const infos = res['result'];
+ const parsed_infos = this.getAudioAndVideoFormats(infos.formats);
+ console.log('got formats for ' + url);
+ const available_formats = {audio: parsed_infos[0], video: parsed_infos[1]};
+ this.cachedAvailableFormats[url] = available_formats;
+ });
+ }
+ }
+
attachToInput() {
Observable.fromEvent(this.urlInput.nativeElement, 'keyup')
.map((e: any) => e.target.value) // extract the value of input
@@ -334,5 +494,71 @@ export class MainComponent implements OnInit {
onResize(event) {
this.files_cols = (event.target.innerWidth <= 450) ? 2 : 4;
}
+
+ videoModeChanged(new_val) {
+ this.selectedQuality = '';
+ }
+
+ getAudioAndVideoFormats(formats): any[] {
+ const audio_formats = {};
+ const video_formats = {};
+
+ for (let i = 0; i < formats.length; i++) {
+ const format_obj = {type: null};
+
+ const format = formats[i];
+ const format_type = (format.vcodec === 'none') ? 'audio' : 'video';
+
+ format_obj.type = format_type;
+ // console.log(format);
+ if (format_obj.type === 'audio' && format.abr) {
+ const key = format.abr.toString() + 'K';
+ format_obj['bitrate'] = format.abr;
+ format_obj['format_id'] = format.format_id;
+ format_obj['ext'] = format.ext;
+ // don't overwrite if not m4a
+ if (audio_formats[key]) {
+ if (format.ext === 'm4a') {
+ audio_formats[key] = format_obj;
+ }
+ } else {
+ audio_formats[key] = format_obj;
+ }
+ } else if (format_obj.type === 'video') {
+ // check if video format is mp4
+ const key = format.height.toString();
+ if (format.ext === 'mp4') {
+ format_obj['height'] = format.height;
+ format_obj['acodec'] = format.acodec;
+ format_obj['format_id'] = format.format_id;
+
+ // no acodec means no overwrite
+ if (!(video_formats[key]) || format_obj['acodec'] !== 'none') {
+ video_formats[key] = format_obj;
+ }
+ }
+ }
+ }
+
+ video_formats['best_audio_format'] = this.getBestAudioFormatForMp4(audio_formats);
+
+ return [audio_formats, video_formats]
+ }
+
+ getBestAudioFormatForMp4(audio_formats) {
+ let best_audio_format_for_mp4 = null;
+ let best_audio_format_bitrate = 0;
+ const available_audio_format_keys = Object.keys(audio_formats);
+ for (let i = 0; i < available_audio_format_keys.length; i++) {
+ const audio_format_key = available_audio_format_keys[i];
+ const audio_format = audio_formats[audio_format_key];
+ const is_m4a = audio_format.ext === 'm4a';
+ if (is_m4a && audio_format.bitrate > best_audio_format_bitrate) {
+ best_audio_format_for_mp4 = audio_format.format_id;
+ best_audio_format_bitrate = audio_format.bitrate;
+ }
+ }
+ return best_audio_format_for_mp4;
+ }
}