Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into add-yt-dlp

This commit is contained in:
Isaac Abadi
2021-07-20 21:55:18 -06:00
74 changed files with 7026 additions and 2310 deletions

View File

@@ -86,6 +86,7 @@ import { EditCategoryDialogComponent } from './dialogs/edit-category-dialog/edit
import { TwitchChatComponent } from './components/twitch-chat/twitch-chat.component';
import { LinkifyPipe, SeeMoreComponent } from './components/see-more/see-more.component';
import { H401Interceptor } from './http.interceptor';
import { ConcurrentStreamComponent } from './components/concurrent-stream/concurrent-stream.component';
registerLocaleData(es, 'es');
@@ -134,7 +135,8 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
CustomPlaylistsComponent,
EditCategoryDialogComponent,
TwitchChatComponent,
SeeMoreComponent
SeeMoreComponent,
ConcurrentStreamComponent
],
imports: [
CommonModule,

View File

@@ -0,0 +1,6 @@
<div class="buttons-container">
<button (click)="startWatching()" *ngIf="!watch_together_clicked" mat-flat-button>Watch together</button>
<button (click)="startServer()" *ngIf="watch_together_clicked && !started && server_mode && server_already_exists === false" mat-flat-button>Start stream</button>
<button (click)="startClient()" *ngIf="watch_together_clicked && !started && server_already_exists === true" mat-flat-button>Join stream</button>
<button style="margin-left: 10px;" (click)="stop()" *ngIf="watch_together_clicked" mat-flat-button>Stop</button>
</div>

View File

@@ -0,0 +1,7 @@
.buttons-container {
display: flex;
align-items: center;
justify-content: center;
margin-top: 15px;
margin-bottom: 15px;
}

View File

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

View File

@@ -0,0 +1,140 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { PostsService } from 'app/posts.services';
@Component({
selector: 'app-concurrent-stream',
templateUrl: './concurrent-stream.component.html',
styleUrls: ['./concurrent-stream.component.scss']
})
export class ConcurrentStreamComponent implements OnInit {
@Input() server_mode = false;
@Input() playback_timestamp;
@Input() playing;
@Input() uid;
@Output() setPlaybackTimestamp = new EventEmitter<any>();
@Output() togglePlayback = new EventEmitter<boolean>();
@Output() setPlaybackRate = new EventEmitter<number>();
started = false;
server_started = false;
watch_together_clicked = false;
server_already_exists = null;
check_timeout: any;
update_timeout: any;
PLAYBACK_TIMESTAMP_DIFFERENCE_THRESHOLD_PLAYBACK_MODIFICATION = 0.5;
PLAYBACK_TIMESTAMP_DIFFERENCE_THRESHOLD_SKIP = 2;
PLAYBACK_MODIFIER = 0.1;
playback_rate_modified = false;
constructor(private postsService: PostsService) { }
// flow: click start watching -> check for available stream to enable join button and if user, display "start stream"
// users who join a stream will send continuous requests for info on playback
ngOnInit(): void {
}
startServer() {
this.started = true;
this.server_started = true;
this.update_timeout = setInterval(() => {
this.updateStream();
}, 1000);
}
updateStream() {
this.postsService.updateConcurrentStream(this.uid, this.playback_timestamp, Date.now()/1000, this.playing).subscribe(res => {
});
}
startClient() {
this.started = true;
}
checkStream() {
if (this.server_started) { return; }
const current_playback_timestamp = this.playback_timestamp;
const current_unix_timestamp = Date.now()/1000;
this.postsService.checkConcurrentStream(this.uid).subscribe(res => {
const stream = res['stream'];
if (!stream) {
this.server_already_exists = false;
return;
}
this.server_already_exists = true;
// check whether client has joined the stream
if (!this.started) { return; }
if (!stream['playing'] && this.playing) {
// tell client to pause and set the timestamp to sync
this.togglePlayback.emit(false);
this.setPlaybackTimestamp.emit(stream['playback_timestamp']);
} else if (stream['playing']) {
// sync unpause state
if (!this.playing) { this.togglePlayback.emit(true); }
// sync time
const zeroed_local_unix_timestamp = current_unix_timestamp - current_playback_timestamp;
const zeroed_server_unix_timestamp = stream['unix_timestamp'] - stream['playback_timestamp'];
const seconds_behind_locally = zeroed_local_unix_timestamp - zeroed_server_unix_timestamp;
if (Math.abs(seconds_behind_locally) > this.PLAYBACK_TIMESTAMP_DIFFERENCE_THRESHOLD_SKIP) {
// skip to playback timestamp because the difference is too high
this.setPlaybackTimestamp.emit(this.playback_timestamp + seconds_behind_locally + 0.3);
this.playback_rate_modified = false;
} else if (!this.playback_rate_modified && Math.abs(seconds_behind_locally) > this.PLAYBACK_TIMESTAMP_DIFFERENCE_THRESHOLD_PLAYBACK_MODIFICATION) {
// increase playback speed to avoid skipping
let seconds_to_wait = (Math.abs(seconds_behind_locally)/this.PLAYBACK_MODIFIER);
seconds_to_wait += 0.3/this.PLAYBACK_MODIFIER;
this.playback_rate_modified = true;
if (seconds_behind_locally > 0) {
// increase speed
this.setPlaybackRate.emit(1 + this.PLAYBACK_MODIFIER);
setTimeout(() => {
this.setPlaybackRate.emit(1);
this.playback_rate_modified = false;
}, seconds_to_wait * 1000);
} else {
// decrease speed
this.setPlaybackRate.emit(1 - this.PLAYBACK_MODIFIER);
setTimeout(() => {
this.setPlaybackRate.emit(1);
this.playback_rate_modified = false;
}, seconds_to_wait * 1000);
}
}
}
});
}
startWatching() {
this.watch_together_clicked = true;
this.check_timeout = setInterval(() => {
this.checkStream();
}, 1000);
}
stop() {
if (this.check_timeout) { clearInterval(this.check_timeout); }
if (this.update_timeout) { clearInterval(this.update_timeout); }
this.started = false;
this.server_started = false;
this.watch_together_clicked = false;
}
}

View File

@@ -53,16 +53,15 @@ export class CustomPlaylistsComponent implements OnInit {
goToPlaylist(info_obj) {
const playlist = info_obj.file;
const playlistID = playlist.id;
const type = playlist.type;
if (playlist) {
if (this.postsService.config['Extra']['download_only_mode']) {
this.downloading_content[type][playlistID] = true;
this.downloadPlaylist(playlist.fileNames, type, playlist.name, playlistID);
this.downloadPlaylist(playlist.id, playlist.name);
} else {
localStorage.setItem('player_navigator', this.router.url);
const fileNames = playlist.fileNames;
this.router.navigate(['/player', {fileNames: fileNames.join('|nvr|'), type: type, id: playlistID, uid: playlistID, auto: playlist.auto}]);
const routeParams = {playlist_id: playlistID};
if (playlist.auto) { routeParams['auto'] = playlist.auto; }
this.router.navigate(['/player', routeParams]);
}
} else {
// playlist not found
@@ -70,11 +69,12 @@ export class CustomPlaylistsComponent implements OnInit {
}
}
downloadPlaylist(fileNames, type, zipName = null, playlistID = null) {
this.postsService.downloadFileFromServer(fileNames, type, zipName).subscribe(res => {
if (playlistID) { this.downloading_content[type][playlistID] = false };
const blob: Blob = res;
saveAs(blob, zipName + '.zip');
downloadPlaylist(playlist_id, playlist_name) {
this.downloading_content[playlist_id] = true;
this.postsService.downloadPlaylistFromServer(playlist_id).subscribe(res => {
this.downloading_content[playlist_id] = false;
const blob: any = res;
saveAs(blob, playlist_name + '.zip');
});
}
@@ -97,7 +97,7 @@ export class CustomPlaylistsComponent implements OnInit {
const index = args.index;
const dialogRef = this.dialog.open(ModifyPlaylistComponent, {
data: {
playlist: playlist,
playlist_id: playlist.id,
width: '65vw'
}
});

View File

@@ -1,21 +1,21 @@
<div style="padding: 20px;">
<div *ngFor="let session_downloads of downloads | keyvalue">
<ng-container *ngIf="keys(session_downloads.value).length > 0">
<div *ngFor="let session_downloads of downloads">
<ng-container *ngIf="keys(session_downloads).length > 2">
<mat-card style="padding-bottom: 30px; margin-bottom: 15px;">
<h4 style="text-align: center;"><ng-container i18n="Session ID">Session ID:</ng-container>&nbsp;{{session_downloads.key}}
<span *ngIf="session_downloads.key === postsService.session_id">&nbsp;<ng-container i18n="Current session">(current)</ng-container></span>
<h4 style="text-align: center;"><ng-container i18n="Session ID">Session ID:</ng-container>&nbsp;{{session_downloads['session_id']}}
<span *ngIf="session_downloads['session_id'] === postsService.session_id">&nbsp;<ng-container i18n="Current session">(current)</ng-container></span>
</h4>
<div class="container">
<div class="row">
<div *ngFor="let download of session_downloads.value | keyvalue: sort_downloads; let i = index;" class="col-12 my-1">
<mat-card *ngIf="download.value" class="mat-elevation-z3">
<app-download-item [download]="download.value" [queueNumber]="i+1" (cancelDownload)="clearDownload(session_downloads.key, download.value.uid)"></app-download-item>
<div *ngFor="let download of session_downloads | keyvalue: sort_downloads; let i = index;" class="col-12 my-1">
<mat-card *ngIf="download.key !== 'session_id' && download.key !== '_id' && download.value" class="mat-elevation-z3">
<app-download-item [download]="download.value" [queueNumber]="i+1" (cancelDownload)="clearDownload(session_downloads['session_id'], download.value.uid)"></app-download-item>
</mat-card>
</div>
</div>
</div>
<div>
<button style="top: 15px;" (click)="clearDownloads(session_downloads.key)" mat-stroked-button color="warn"><ng-container i18n="clear all downloads action button">Clear all downloads</ng-container></button>
<button style="top: 15px;" (click)="clearDownloads(session_downloads['session_id'])" mat-stroked-button color="warn"><ng-container i18n="clear all downloads action button">Clear all downloads</ng-container></button>
</div>
</mat-card>
</ng-container>

View File

@@ -35,7 +35,7 @@ import { Router } from '@angular/router';
export class DownloadsComponent implements OnInit, OnDestroy {
downloads_check_interval = 1000;
downloads = {};
downloads = [];
interval_id = null;
keys = Object.keys;
@@ -137,6 +137,7 @@ export class DownloadsComponent implements OnInit, OnDestroy {
this.downloads[session_id] = session_downloads_by_id;
} else {
for (let j = 0; j < session_download_ids.length; j++) {
if (session_download_ids[j] === 'session_id' || session_download_ids[j] === '_id') continue;
const download_id = session_download_ids[j];
const download = new_downloads_by_session[session_id][download_id]
if (!this.downloads[session_id][download_id]) {
@@ -156,11 +157,10 @@ export class DownloadsComponent implements OnInit, OnDestroy {
downloadsValid() {
let valid = false;
const keys = this.keys(this.downloads);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const value = this.downloads[key];
if (this.keys(value).length > 0) {
for (let i = 0; i < this.downloads.length; i++) {
const session_downloads = this.downloads[i];
if (!session_downloads) continue;
if (this.keys(session_downloads).length > 2) {
valid = true;
break;
}

View File

@@ -5,7 +5,7 @@
<mat-list-item role="listitem" *ngFor="let permission of available_permissions">
<h3 matLine>{{permissionToLabel[permission] ? permissionToLabel[permission] : permission}}</h3>
<span matLine>
<mat-radio-group [disabled]="permission === 'settings' && role.name === 'admin'" (change)="changeRolePermissions($event, permission, permissions[permission])" [(ngModel)]="permissions[permission]" [attr.aria-label]="'Give role permission for ' + permission">
<mat-radio-group [disabled]="permission === 'settings' && role.key === 'admin'" (change)="changeRolePermissions($event, permission, permissions[permission])" [(ngModel)]="permissions[permission]" [attr.aria-label]="'Give role permission for ' + permission">
<mat-radio-button value="yes"><ng-container i18n="Yes">Yes</ng-container></mat-radio-button>
<mat-radio-button value="no"><ng-container i18n="No">No</ng-container></mat-radio-button>
</mat-radio-group>

View File

@@ -47,7 +47,7 @@ export class ManageRoleComponent implements OnInit {
}
changeRolePermissions(change, permission) {
this.postsService.setRolePermission(this.role.name, permission, change.value).subscribe(res => {
this.postsService.setRolePermission(this.role.key, permission, change.value).subscribe(res => {
if (res['success']) {
} else {

View File

@@ -94,7 +94,7 @@
</div>
<button color="primary" [matMenuTriggerFor]="edit_roles_menu" class="edit-role" mat-raised-button><ng-container i18n="Edit role">Edit Role</ng-container></button>
<mat-menu #edit_roles_menu="matMenu">
<button (click)="openModifyRole(role)" mat-menu-item *ngFor="let role of roles">{{role.name}}</button>
<button (click)="openModifyRole(role)" mat-menu-item *ngFor="let role of roles">{{role.key}}</button>
</mat-menu>
</div>

View File

@@ -78,16 +78,7 @@ export class ModifyUsersComponent implements OnInit, AfterViewInit {
getRoles() {
this.postsService.getRoles().subscribe(res => {
this.roles = [];
const roles = res['roles'];
const role_names = Object.keys(roles);
for (let i = 0; i < role_names.length; i++) {
const role_name = role_names[i];
this.roles.push({
name: role_name,
permissions: roles[role_name]['permissions']
});
}
this.roles = res['roles'];
});
}

View File

@@ -166,15 +166,14 @@ export class RecentVideosComponent implements OnInit {
const sub = this.postsService.getSubscriptionByID(file.sub_id);
if (sub.streamingOnly) {
// streaming only mode subscriptions
!new_tab ? this.router.navigate(['/player', {name: file.id,
url: file.requested_formats ? file.requested_formats[0].url : file.url}])
: window.open(`/#/player;name=${file.id};url=${file.requested_formats ? file.requested_formats[0].url : file.url}`);
// !new_tab ? this.router.navigate(['/player', {name: file.id,
// url: file.requested_formats ? file.requested_formats[0].url : file.url}])
// : window.open(`/#/player;name=${file.id};url=${file.requested_formats ? file.requested_formats[0].url : file.url}`);
} else {
// normal subscriptions
!new_tab ? this.router.navigate(['/player', {fileNames: file.id,
type: file.isAudio ? 'audio' : 'video', subscriptionName: sub.name,
subPlaylist: sub.isPlaylist}])
: window.open(`/#/player;fileNames=${file.id};type=${file.isAudio ? 'audio' : 'video'};subscriptionName=${sub.name};subPlaylist=${sub.isPlaylist}`);
!new_tab ? this.router.navigate(['/player', {uid: file.uid,
type: file.isAudio ? 'audio' : 'video', sub_id: sub.id}])
: window.open(`/#/player;uid=${file.uid};type=${file.isAudio ? 'audio' : 'video'};sub_id=${sub.id}`);
}
} else {
// normal files
@@ -201,8 +200,7 @@ export class RecentVideosComponent implements OnInit {
const type = file.isAudio ? 'audio' : 'video';
const ext = type === 'audio' ? '.mp3' : '.mp4'
const sub = this.postsService.getSubscriptionByID(file.sub_id);
this.postsService.downloadFileFromServer(file.id, type, null, null, sub.name, sub.isPlaylist,
this.postsService.user ? this.postsService.user.uid : null, null).subscribe(res => {
this.postsService.downloadFileFromServer(file.uid).subscribe(res => {
const blob: Blob = res;
saveAs(blob, file.id + ext);
}, err => {
@@ -215,14 +213,14 @@ export class RecentVideosComponent implements OnInit {
const ext = type === 'audio' ? '.mp3' : '.mp4'
const name = file.id;
this.downloading_content[type][name] = true;
this.postsService.downloadFileFromServer(name, type).subscribe(res => {
this.postsService.downloadFileFromServer(file.uid).subscribe(res => {
this.downloading_content[type][name] = false;
const blob: Blob = res;
saveAs(blob, decodeURIComponent(name) + ext);
if (!this.postsService.config.Extra.file_manager_enabled) {
// tell server to delete the file once downloaded
this.postsService.deleteFile(name, type).subscribe(delRes => {
this.postsService.deleteFile(file.uid).subscribe(delRes => {
// reload mp4s
this.getAllFiles();
});
@@ -245,7 +243,7 @@ export class RecentVideosComponent implements OnInit {
}
deleteNormalFile(file, blacklistMode = false) {
this.postsService.deleteFile(file.uid, file.isAudio ? 'audio' : 'video', blacklistMode).subscribe(result => {
this.postsService.deleteFile(file.uid, blacklistMode).subscribe(result => {
if (result) {
this.postsService.openSnackBar('Delete success!', 'OK.');
this.removeFileCard(file);

View File

@@ -19,9 +19,9 @@
<mat-label *ngIf="type === 'audio'"><ng-container i18n="Audio files title">Audio files</ng-container></mat-label>
<mat-label *ngIf="type === 'video'"><ng-container i18n="Videos title">Videos</ng-container></mat-label>
<mat-select [formControl]="filesSelect" multiple required aria-required>
<ng-container *ngIf="filesToSelectFrom"><mat-option *ngFor="let file of filesToSelectFrom" [value]="file.id">{{file.id}}</mat-option></ng-container>
<ng-container *ngIf="audiosToSelectFrom && type === 'audio'"><mat-option *ngFor="let file of audiosToSelectFrom" [value]="file.id">{{file.id}}</mat-option></ng-container>
<ng-container *ngIf="videosToSelectFrom && type === 'video'"><mat-option *ngFor="let file of videosToSelectFrom" [value]="file.id">{{file.id}}</mat-option></ng-container>
<ng-container *ngIf="filesToSelectFrom"><mat-option *ngFor="let file of filesToSelectFrom" [value]="file.uid">{{file.id}}</mat-option></ng-container>
<ng-container *ngIf="audiosToSelectFrom && type === 'audio'"><mat-option *ngFor="let file of audiosToSelectFrom" [value]="file.uid">{{file.id}}</mat-option></ng-container>
<ng-container *ngIf="videosToSelectFrom && type === 'video'"><mat-option *ngFor="let file of videosToSelectFrom" [value]="file.uid">{{file.id}}</mat-option></ng-container>
</mat-select>
</mat-form-field>
<!-- No videos available -->

View File

@@ -51,9 +51,8 @@ export class CreatePlaylistComponent implements OnInit {
createPlaylist() {
const thumbnailURL = this.getThumbnailURL();
const duration = this.calculateDuration();
this.create_in_progress = true;
this.postsService.createPlaylist(this.name, this.filesSelect.value, this.type, thumbnailURL, duration).subscribe(res => {
this.postsService.createPlaylist(this.name, this.filesSelect.value, this.type, thumbnailURL).subscribe(res => {
this.create_in_progress = false;
if (res['success']) {
this.dialogRef.close(true);
@@ -78,36 +77,4 @@ export class CreatePlaylistComponent implements OnInit {
}
return null;
}
getDuration(file_id) {
let properFilesToSelectFrom = this.filesToSelectFrom;
if (!this.filesToSelectFrom) {
properFilesToSelectFrom = this.type === 'audio' ? this.audiosToSelectFrom : this.videosToSelectFrom;
}
for (let i = 0; i < properFilesToSelectFrom.length; i++) {
const file = properFilesToSelectFrom[i];
if (file.id === file_id) {
return file.duration;
}
}
return null;
}
calculateDuration() {
let sum = 0;
for (let i = 0; i < this.filesSelect.value.length; i++) {
const duration_val = this.getDuration(this.filesSelect.value[i]);
sum += typeof duration_val === 'string' ? this.durationStringToNumber(duration_val) : duration_val;
}
return sum;
}
durationStringToNumber(dur_str) {
let num_sum = 0;
const dur_str_parts = dur_str.split(':');
for (let i = dur_str_parts.length-1; i >= 0; i--) {
num_sum += parseInt(dur_str_parts[i])*(60**(dur_str_parts.length-1-i));
}
return num_sum;
}
}

View File

@@ -1,38 +1,40 @@
<h4 mat-dialog-title><ng-container i18n="Modify playlist dialog title">Modify playlist</ng-container></h4>
<mat-dialog-content>
<!-- Playlist info -->
<div>
<mat-form-field color="accent">
<input matInput placeholder="Name" i18n-placeholder="Name" [(ngModel)]="playlist.name">
</mat-form-field>
</div>
<div style="margin-bottom: 10px; height: 40px;">
<div style="float: left">
<span *ngIf="reverse_order === false" i18n="Normal order">Normal order&nbsp;</span>
<span *ngIf="reverse_order === true" i18n="Reverse order">Reverse order&nbsp;</span>
<button (click)="togglePlaylistOrder()" mat-icon-button><mat-icon>{{!reverse_order ? 'arrow_downward' : 'arrow_upward'}}</mat-icon></button>
<div *ngIf="playlist">
<!-- Playlist info -->
<div>
<mat-form-field color="accent">
<input matInput placeholder="Name" i18n-placeholder="Name" [(ngModel)]="playlist.name">
</mat-form-field>
</div>
<div style="float: right">
<button [disabled]="available_files.length === 0" mat-stroked-button [matMenuTriggerFor]="menu"><ng-container i18n="Add content">Add content</ng-container></button>
</div>
</div>
<div style="margin-bottom: 10px; height: 40px;">
<div style="float: left">
<span *ngIf="reverse_order === false" i18n="Normal order">Normal order&nbsp;</span>
<span *ngIf="reverse_order === true" i18n="Reverse order">Reverse order&nbsp;</span>
<button (click)="togglePlaylistOrder()" mat-icon-button><mat-icon>{{!reverse_order ? 'arrow_downward' : 'arrow_upward'}}</mat-icon></button>
</div>
<!-- Playlist order -->
<mat-button-toggle-group class="media-list" cdkDropList (cdkDropListDropped)="drop($event)" style="width: 80%; left: 9%" vertical name="videoSelect" aria-label="Video Select" #group="matButtonToggleGroup">
<!-- The following for loop can be optimized but it requires a pipe https://stackoverflow.com/a/35703364/8088021 -->
<mat-button-toggle class="media-box" cdkDrag *ngFor="let playlist_item of (reverse_order ? playlist.fileNames.slice().reverse() : playlist.fileNames); let i = index" [checked]="false"><div><div class="playlist-item-text">{{playlist_item}}</div> <button (click)="removeContent(i)" class="remove-item-button" mat-icon-button><mat-icon>cancel</mat-icon></button></div></mat-button-toggle>
</mat-button-toggle-group>
<mat-menu #menu="matMenu">
<button *ngFor="let file of available_files" (click)="addContent(file)" mat-menu-item>{{file}}</button>
</mat-menu>
<div style="float: right">
<button [disabled]="available_files.length === 0" mat-stroked-button [matMenuTriggerFor]="menu"><ng-container i18n="Add content">Add content</ng-container></button>
</div>
</div>
<!-- Playlist order -->
<mat-button-toggle-group class="media-list" cdkDropList (cdkDropListDropped)="drop($event)" style="width: 80%; left: 9%" vertical name="videoSelect" aria-label="Video Select" #group="matButtonToggleGroup">
<!-- The following for loop can be optimized but it requires a pipe https://stackoverflow.com/a/35703364/8088021 -->
<mat-button-toggle class="media-box" cdkDrag *ngFor="let playlist_item of (reverse_order ? playlist_file_objs.slice().reverse() : playlist_file_objs); let i = index" [checked]="false"><div><div class="playlist-item-text">{{playlist_item.title}}</div> <button (click)="removeContent(i)" class="remove-item-button" mat-icon-button><mat-icon>cancel</mat-icon></button></div></mat-button-toggle>
</mat-button-toggle-group>
<mat-menu #menu="matMenu">
<button *ngFor="let file of available_files" (click)="addContent(file)" mat-menu-item>{{file.title}}</button>
</mat-menu>
</div>
</mat-dialog-content>
<mat-dialog-actions>
<!-- Save -->
<button [disabled]="!playlistChanged()" (click)="updatePlaylist()" [disabled]="playlistChanged()" mat-raised-button color="accent"><ng-container i18n="Save">Save</ng-container></button>
<button [disabled]="!playlist || !playlistChanged()" (click)="updatePlaylist()" mat-raised-button color="accent"><ng-container i18n="Save">Save</ng-container></button>
</mat-dialog-actions>

View File

@@ -10,8 +10,12 @@ import { PostsService } from 'app/posts.services';
})
export class ModifyPlaylistComponent implements OnInit {
playlist_id = null;
original_playlist = null;
playlist = null;
playlist_file_objs = null;
available_files = [];
all_files = [];
playlist_updated = false;
@@ -23,9 +27,8 @@ export class ModifyPlaylistComponent implements OnInit {
ngOnInit(): void {
if (this.data) {
this.playlist = JSON.parse(JSON.stringify(this.data.playlist));
this.original_playlist = JSON.parse(JSON.stringify(this.data.playlist));
this.getFiles();
this.playlist_id = this.data.playlist_id;
this.getPlaylist();
}
this.reverse_order = localStorage.getItem('default_playlist_order_reversed') === 'true';
@@ -44,11 +47,12 @@ export class ModifyPlaylistComponent implements OnInit {
}
processFiles(new_files = null) {
if (new_files) { this.all_files = new_files.map(file => file.id); }
this.available_files = this.all_files.filter(e => !this.playlist.fileNames.includes(e))
if (new_files) { this.all_files = new_files; }
this.available_files = this.all_files.filter(e => !this.playlist_file_objs.includes(e))
}
updatePlaylist() {
this.playlist['uids'] = this.playlist_file_objs.map(playlist_file_obj => playlist_file_obj['uid'])
this.postsService.updatePlaylist(this.playlist).subscribe(res => {
this.playlist_updated = true;
this.postsService.openSnackBar('Playlist updated successfully.');
@@ -57,28 +61,30 @@ export class ModifyPlaylistComponent implements OnInit {
}
playlistChanged() {
return JSON.stringify(this.playlist) === JSON.stringify(this.original_playlist);
return JSON.stringify(this.playlist) !== JSON.stringify(this.original_playlist);
}
getPlaylist() {
this.postsService.getPlaylist(this.playlist.id, this.playlist.type, null).subscribe(res => {
this.postsService.getPlaylist(this.playlist_id, null, true).subscribe(res => {
if (res['playlist']) {
this.playlist = res['playlist'];
this.playlist_file_objs = res['file_objs'];
this.original_playlist = JSON.parse(JSON.stringify(this.playlist));
this.getFiles();
}
});
}
addContent(file) {
this.playlist.fileNames.push(file);
this.playlist_file_objs.push(file);
this.processFiles();
}
removeContent(index) {
if (this.reverse_order) {
index = this.playlist.fileNames.length - 1 - index;
index = this.playlist_file_objs.length - 1 - index;
}
this.playlist.fileNames.splice(index, 1);
this.playlist_file_objs.splice(index, 1);
this.processFiles();
}
@@ -89,10 +95,10 @@ export class ModifyPlaylistComponent implements OnInit {
drop(event: CdkDragDrop<string[]>) {
if (this.reverse_order) {
event.previousIndex = this.playlist.fileNames.length - 1 - event.previousIndex;
event.currentIndex = this.playlist.fileNames.length - 1 - event.currentIndex;
event.previousIndex = this.playlist_file_objs.length - 1 - event.previousIndex;
event.currentIndex = this.playlist_file_objs.length - 1 - event.currentIndex;
}
moveItemInArray(this.playlist.fileNames, event.previousIndex, event.currentIndex);
moveItemInArray(this.playlist_file_objs, event.previousIndex, event.currentIndex);
}
}

View File

@@ -1,7 +1,6 @@
<h4 mat-dialog-title>
<ng-container *ngIf="is_playlist" i18n="Share playlist dialog title">Share playlist</ng-container>
<ng-container *ngIf="!is_playlist && type === 'video'" i18n="Share video dialog title">Share video</ng-container>
<ng-container *ngIf="!is_playlist && type === 'audio'" i18n="Share audio dialog title">Share audio</ng-container>
<ng-container *ngIf="!is_playlist" i18n="Share video dialog title">Share file</ng-container>
</h4>
<mat-dialog-content>

View File

@@ -11,7 +11,6 @@ import { PostsService } from 'app/posts.services';
})
export class ShareMediaDialogComponent implements OnInit {
type = null;
uid = null;
uuid = null;
share_url = null;
@@ -26,14 +25,13 @@ export class ShareMediaDialogComponent implements OnInit {
ngOnInit(): void {
if (this.data) {
this.type = this.data.type;
this.uid = this.data.uid;
this.uuid = this.data.uuid;
this.sharing_enabled = this.data.sharing_enabled;
this.is_playlist = this.data.is_playlist;
this.current_timestamp = (this.data.current_timestamp / 1000).toFixed(2);
const arg = (this.is_playlist ? ';id=' : ';uid=');
const arg = (this.is_playlist ? ';playlist_id=' : ';uid=');
this.default_share_url = window.location.href.split(';')[0] + arg + this.uid;
if (this.uuid) {
this.default_share_url += ';uuid=' + this.uuid;
@@ -65,7 +63,7 @@ export class ShareMediaDialogComponent implements OnInit {
sharingChanged(event) {
if (event.checked) {
this.postsService.enableSharing(this.uid, this.type, this.is_playlist).subscribe(res => {
this.postsService.enableSharing(this.uid, this.is_playlist).subscribe(res => {
if (res['success']) {
this.openSnackBar('Sharing enabled.');
this.sharing_enabled = true;
@@ -76,7 +74,7 @@ export class ShareMediaDialogComponent implements OnInit {
this.openSnackBar('Failed to enable sharing - server error.');
});
} else {
this.postsService.disableSharing(this.uid, this.type, this.is_playlist).subscribe(res => {
this.postsService.disableSharing(this.uid, this.is_playlist).subscribe(res => {
if (res['success']) {
this.openSnackBar('Sharing disabled.');
this.sharing_enabled = false;

View File

@@ -56,7 +56,7 @@ export class FileCardComponent implements OnInit {
deleteFile(blacklistMode = false) {
if (!this.playlist) {
this.postsService.deleteFile(this.uid, this.isAudio ? 'audio' : 'video', blacklistMode).subscribe(result => {
this.postsService.deleteFile(this.uid, blacklistMode).subscribe(result => {
if (result) {
this.openSnackBar('Delete success!', 'OK.');
this.removeFile.emit(this.name);
@@ -84,7 +84,7 @@ export class FileCardComponent implements OnInit {
editPlaylistDialog() {
const dialogRef = this.dialog.open(ModifyPlaylistComponent, {
data: {
playlist: this.playlist,
playlist_id: this.playlist.id,
width: '65vw'
}
});

View File

@@ -14,7 +14,7 @@ export class H401Interceptor implements HttpInterceptor {
return next.handle(request).pipe(catchError(err => {
if (err.status === 401) {
localStorage.setItem('jwt_token', null);
if (this.router.url !== '/login') {
if (this.router.url !== '/login' && !this.router.url.includes('player')) {
this.router.navigate(['/login']).then(() => {
this.openSnackBar('Login expired, please login again.');
});

View File

@@ -124,6 +124,10 @@ mat-form-field.mat-form-field {
width: 100%;
}
.advanced-input-time {
margin-left: 10px;
}
.edit-button {
margin-left: 10px;
top: -5px;

View File

@@ -20,11 +20,16 @@
</ng-container>
</mat-label>
<mat-select [ngModelOptions]="{standalone: true}" [(ngModel)]="selectedQuality">
<ng-container *ngFor="let option of qualityOptions[(audioOnly) ? 'audio' : 'video']">
<mat-option *ngIf="option.value === '' || url && cachedAvailableFormats[url] && cachedAvailableFormats[url]['formats'] && cachedAvailableFormats[url]['formats'][(audioOnly) ? 'audio' : 'video'][option.value]" [value]="option.value">
{{option.label}}
<mat-option [value]="''">
Max
</mat-option>
</ng-container>
<ng-container *ngIf="url && cachedAvailableFormats && cachedAvailableFormats[url]?.formats">
<ng-container *ngFor="let option of cachedAvailableFormats[url]['formats'][audioOnly ? 'audio' : 'video']">
<mat-option *ngIf="option.key !== 'best_audio_format'" [value]="option">
{{option.key}}
</mat-option>
</ng-container>
</ng-container>
</mat-select>
<div class="spinner-div" *ngIf="url !== '' && cachedAvailableFormats[url] && cachedAvailableFormats[url]['formats_loading']">
<mat-spinner [diameter]="25"></mat-spinner>
@@ -129,7 +134,7 @@
</mat-hint>
</mat-form-field>
</div>
<div *ngIf="!youtubeAuthDisabledOverride" class="col-12 col-sm-6 mt-2">
<div *ngIf="!youtubeAuthDisabledOverride" class="col-12 col-sm-6 mt-3">
<mat-checkbox color="accent" [disabled]="current_download" (change)="youtubeAuthEnabledChanged($event)" [(ngModel)]="youtubeAuthEnabled" style="z-index: 999" [ngModelOptions]="{standalone: true}">
<ng-container i18n="Use authentication checkbox">
Use authentication
@@ -139,11 +144,26 @@
<input [(ngModel)]="youtubeUsername" [ngModelOptions]="{standalone: true}" [disabled]="!youtubeAuthEnabled" matInput placeholder="Username" i18n-placeholder="YT Username placeholder">
</mat-form-field>
</div>
<div *ngIf="!youtubeAuthDisabledOverride" class="col-12 col-sm-6 mt-2">
<div *ngIf="!youtubeAuthDisabledOverride" class="col-12 col-sm-6 mt-3">
<mat-form-field style="margin-top: 31px;" color="accent" class="advanced-input">
<input [(ngModel)]="youtubePassword" type="password" [ngModelOptions]="{standalone: true}" [disabled]="!youtubeAuthEnabled" matInput placeholder="Password" i18n-placeholder="YT Password placeholder">
</mat-form-field>
</div>
<div class="col-12 col-sm-6 mt-3">
<mat-checkbox color="accent" [disabled]="current_download" [(ngModel)]="cropFile" style="z-index: 999" [ngModelOptions]="{standalone: true}">
<ng-container i18n="Crop video checkbox">
Crop file
</ng-container>
</mat-checkbox>
<mat-form-field color="accent" class="advanced-input">
<input [(ngModel)]="cropFileStart" type="number" [ngModelOptions]="{standalone: true}" [disabled]="!cropFile" matInput placeholder="Crop from (seconds)" i18n-placeholder="Crom from placeholder">
</mat-form-field>
</div>
<div class="col-12 col-sm-6 mt-3">
<mat-form-field style="margin-top: 31px;" color="accent" class="advanced-input">
<input [(ngModel)]="cropFileEnd" type="number" [ngModelOptions]="{standalone: true}" [disabled]="!cropFile" matInput placeholder="Crop to (seconds)" i18n-placeholder="Crop to placeholder">
</mat-form-field>
</div>
</div>
</div>
</mat-expansion-panel>

View File

@@ -54,6 +54,9 @@ export class MainComponent implements OnInit {
youtubeAuthEnabled = false;
youtubeUsername = null;
youtubePassword = null;
cropFile = false;
cropFileStart = null;
cropFileEnd = null;
urlError = false;
path = '';
url = '';
@@ -339,12 +342,8 @@ export class MainComponent implements OnInit {
}
}
public goToFile(name, isAudio, uid) {
if (isAudio) {
this.downloadHelperMp3(name, uid, false, false, null, true);
} else {
this.downloadHelperMp4(name, uid, false, false, null, true);
}
public goToFile(container, isAudio, uid) {
this.downloadHelper(container, isAudio ? 'audio' : 'video', false, false, null, true);
}
public goToPlaylist(playlistID, type) {
@@ -352,7 +351,7 @@ export class MainComponent implements OnInit {
if (playlist) {
if (this.downloadOnlyMode) {
this.downloading_content[type][playlistID] = true;
this.downloadPlaylist(playlist.fileNames, type, playlist.name, playlistID);
this.downloadPlaylist(playlist);
} else {
localStorage.setItem('player_navigator', this.router.url);
const fileNames = playlist.fileNames;
@@ -376,56 +375,26 @@ export class MainComponent implements OnInit {
// download helpers
downloadHelperMp3(name, uid, is_playlist = false, forceView = false, new_download = null, navigate_mode = false) {
downloadHelper(container, type, is_playlist = false, force_view = false, new_download = null, navigate_mode = false) {
this.downloadingfile = false;
if (this.multiDownloadMode && !this.downloadOnlyMode && !navigate_mode) {
// do nothing
this.reloadRecentVideos();
} else {
// if download only mode, just download the file. no redirect
if (forceView === false && this.downloadOnlyMode && !this.iOS) {
if (force_view === false && this.downloadOnlyMode && !this.iOS) {
if (is_playlist) {
const zipName = name[0].split(' ')[0] + name[1].split(' ')[0];
this.downloadPlaylist(name, 'audio', zipName);
this.downloadPlaylist(container['uid']);
} else {
this.downloadAudioFile(decodeURI(name));
this.downloadFileFromServer(container, type);
}
this.reloadRecentVideos();
} else {
localStorage.setItem('player_navigator', this.router.url.split(';')[0]);
if (is_playlist) {
this.router.navigate(['/player', {fileNames: name.join('|nvr|'), type: 'audio'}]);
this.router.navigate(['/player', {playlist_id: container['id'], type: type}]);
} else {
this.router.navigate(['/player', {type: 'audio', uid: uid}]);
}
}
}
// remove download from current downloads
this.removeDownloadFromCurrentDownloads(new_download);
}
downloadHelperMp4(name, uid, is_playlist = false, forceView = false, new_download = null, navigate_mode = false) {
this.downloadingfile = false;
if (this.multiDownloadMode && !this.downloadOnlyMode && !navigate_mode) {
// do nothing
this.reloadRecentVideos();
} else {
// if download only mode, just download the file. no redirect
if (forceView === false && this.downloadOnlyMode) {
if (is_playlist) {
const zipName = name[0].split(' ')[0] + name[1].split(' ')[0];
this.downloadPlaylist(name, 'video', zipName);
} else {
this.downloadVideoFile(decodeURI(name));
}
this.reloadRecentVideos();
} else {
localStorage.setItem('player_navigator', this.router.url.split(';')[0]);
if (is_playlist) {
this.router.navigate(['/player', {fileNames: name.join('|nvr|'), type: 'video'}]);
} else {
this.router.navigate(['/player', {type: 'video', uid: uid}]);
this.router.navigate(['/player', {type: type, uid: container['uid']}]);
}
}
}
@@ -436,124 +405,85 @@ export class MainComponent implements OnInit {
// download click handler
downloadClicked() {
if (this.ValidURL(this.url)) {
this.urlError = false;
this.path = '';
// get common args
const customArgs = (this.customArgsEnabled ? this.customArgs : null);
const customOutput = (this.customOutputEnabled ? this.customOutput : null);
const youtubeUsername = (this.youtubeAuthEnabled && this.youtubeUsername ? this.youtubeUsername : null);
const youtubePassword = (this.youtubeAuthEnabled && this.youtubePassword ? this.youtubePassword : null);
// set advanced inputs
if (this.allowAdvancedDownload) {
if (customArgs) {
localStorage.setItem('customArgs', customArgs);
}
if (customOutput) {
localStorage.setItem('customOutput', customOutput);
}
if (youtubeUsername) {
localStorage.setItem('youtubeUsername', youtubeUsername);
}
}
if (this.audioOnly) {
// create download object
const new_download: Download = {
uid: uuid(),
type: 'audio',
percent_complete: 0,
url: this.url,
downloading: true,
is_playlist: this.url.includes('playlist'),
error: false
};
this.downloads.push(new_download);
if (!this.current_download && !this.multiDownloadMode) { this.current_download = new_download };
this.downloadingfile = true;
let customQualityConfiguration = null;
if (this.selectedQuality !== '') {
customQualityConfiguration = this.getSelectedAudioFormat();
}
this.postsService.makeMP3(this.url, (this.selectedQuality === '' ? null : this.selectedQuality),
customQualityConfiguration, customArgs, customOutput, youtubeUsername, youtubePassword, new_download.uid).subscribe(posts => {
// update download object
new_download.downloading = false;
new_download.percent_complete = 100;
const is_playlist = !!(posts['file_names']);
this.path = is_playlist ? posts['file_names'] : posts['audiopathEncoded'];
this.current_download = null;
if (this.path !== '-1') {
this.downloadHelperMp3(this.path, posts['uid'], is_playlist, false, new_download);
}
}, error => { // can't access server or failed to download for other reasons
this.downloadingfile = false;
this.current_download = null;
new_download['downloading'] = false;
// removes download from list of downloads
const downloads_index = this.downloads.indexOf(new_download);
if (downloads_index !== -1) {
this.downloads.splice(downloads_index)
}
this.openSnackBar('Download failed!', 'OK.');
});
} else {
// create download object
const new_download: Download = {
uid: uuid(),
type: 'video',
percent_complete: 0,
url: this.url,
downloading: true,
is_playlist: this.url.includes('playlist'),
error: false
};
this.downloads.push(new_download);
if (!this.current_download && !this.multiDownloadMode) { this.current_download = new_download };
this.downloadingfile = true;
const customQualityConfiguration = this.getSelectedVideoFormat();
this.postsService.makeMP4(this.url, (this.selectedQuality === '' ? null : this.selectedQuality),
customQualityConfiguration, customArgs, customOutput, youtubeUsername, youtubePassword, new_download.uid).subscribe(posts => {
// update download object
new_download.downloading = false;
new_download.percent_complete = 100;
const is_playlist = !!(posts['file_names']);
this.path = is_playlist ? posts['file_names'] : posts['videopathEncoded'];
this.current_download = null;
if (this.path !== '-1') {
this.downloadHelperMp4(this.path, posts['uid'], is_playlist, false, new_download);
}
}, error => { // can't access server
this.downloadingfile = false;
this.current_download = null;
new_download['downloading'] = false;
// removes download from list of downloads
const downloads_index = this.downloads.indexOf(new_download);
if (downloads_index !== -1) {
this.downloads.splice(downloads_index)
}
this.openSnackBar('Download failed!', 'OK.');
});
}
if (this.multiDownloadMode) {
this.url = '';
this.downloadingfile = false;
}
} else {
if (!this.ValidURL(this.url)) {
this.urlError = true;
return;
}
this.urlError = false;
// get common args
const customArgs = (this.customArgsEnabled ? this.customArgs : null);
const customOutput = (this.customOutputEnabled ? this.customOutput : null);
const youtubeUsername = (this.youtubeAuthEnabled && this.youtubeUsername ? this.youtubeUsername : null);
const youtubePassword = (this.youtubeAuthEnabled && this.youtubePassword ? this.youtubePassword : null);
// set advanced inputs
if (this.allowAdvancedDownload) {
if (customArgs) {
localStorage.setItem('customArgs', customArgs);
}
if (customOutput) {
localStorage.setItem('customOutput', customOutput);
}
if (youtubeUsername) {
localStorage.setItem('youtubeUsername', youtubeUsername);
}
}
const type = this.audioOnly ? 'audio' : 'video';
// create download object
const new_download: Download = {
uid: uuid(),
type: type,
percent_complete: 0,
url: this.url,
downloading: true,
is_playlist: this.url.includes('playlist'),
error: false
};
this.downloads.push(new_download);
if (!this.current_download && !this.multiDownloadMode) { this.current_download = new_download };
this.downloadingfile = true;
let customQualityConfiguration = type === 'audio' ? this.getSelectedAudioFormat() : this.getSelectedVideoFormat();
let cropFileSettings = null;
if (this.cropFile) {
cropFileSettings = {
cropFileStart: this.cropFileStart,
cropFileEnd: this.cropFileEnd
}
}
this.postsService.downloadFile(this.url, type, (this.selectedQuality === '' ? null : this.selectedQuality),
customQualityConfiguration, customArgs, customOutput, youtubeUsername, youtubePassword, new_download.uid, cropFileSettings).subscribe(res => {
// update download object
new_download.downloading = false;
new_download.percent_complete = 100;
const container = res['container'];
const is_playlist = res['file_uids'].length > 1;
this.current_download = null;
this.downloadHelper(container, type, is_playlist, false, new_download);
}, error => { // can't access server
this.downloadingfile = false;
this.current_download = null;
new_download['downloading'] = false;
// removes download from list of downloads
const downloads_index = this.downloads.indexOf(new_download);
if (downloads_index !== -1) {
this.downloads.splice(downloads_index)
}
this.openSnackBar('Download failed!', 'OK.');
});
if (this.multiDownloadMode) {
this.url = '';
this.downloadingfile = false;
}
}
@@ -570,23 +500,26 @@ export class MainComponent implements OnInit {
}
getSelectedAudioFormat() {
if (this.selectedQuality === '') { return null };
if (this.selectedQuality === '') { return null; }
const cachedFormatsExists = this.cachedAvailableFormats[this.url] && this.cachedAvailableFormats[this.url]['formats'];
if (cachedFormatsExists) {
const audio_formats = this.cachedAvailableFormats[this.url]['formats']['audio'];
return audio_formats[this.selectedQuality]['format_id'];
return this.selectedQuality['format_id'];
} else {
return null;
}
}
getSelectedVideoFormat() {
if (this.selectedQuality === '') { return null };
const cachedFormatsExists = this.cachedAvailableFormats[this.url] && this.cachedAvailableFormats[this.url]['formats'];
if (cachedFormatsExists) {
const video_formats = this.cachedAvailableFormats[this.url]['formats']['video'];
if (video_formats['best_audio_format'] && this.selectedQuality !== '') {
return video_formats[this.selectedQuality]['format_id'] + '+' + video_formats['best_audio_format'];
if (this.selectedQuality === '') { return null; }
const cachedFormats = this.cachedAvailableFormats[this.url] && this.cachedAvailableFormats[this.url]['formats'];
if (cachedFormats) {
const video_formats = cachedFormats['video'];
if (this.selectedQuality) {
let selected_video_format = this.selectedQuality['format_id'];
// add in audio format if necessary
if (!this.selectedQuality['acodec'] && cachedFormats['best_audio_format']) selected_video_format += `+${cachedFormats['best_audio_format']}`;
return selected_video_format;
}
}
return null;
@@ -614,41 +547,27 @@ export class MainComponent implements OnInit {
}
}
downloadAudioFile(name) {
this.downloading_content['audio'][name] = true;
this.postsService.downloadFileFromServer(name, 'audio').subscribe(res => {
this.downloading_content['audio'][name] = false;
downloadFileFromServer(file, type) {
const ext = type === 'audio' ? 'mp3' : 'mp4'
this.downloading_content[type][file.id] = true;
this.postsService.downloadFileFromServer(file.uid).subscribe(res => {
this.downloading_content[type][file.id] = false;
const blob: Blob = res;
saveAs(blob, decodeURIComponent(name) + '.mp3');
saveAs(blob, decodeURIComponent(file.id) + `.${ext}`);
if (!this.fileManagerEnabled) {
// tell server to delete the file once downloaded
this.postsService.deleteFile(name, 'video').subscribe(delRes => {
this.postsService.deleteFile(file.uid).subscribe(delRes => {
});
}
});
}
downloadVideoFile(name) {
this.downloading_content['video'][name] = true;
this.postsService.downloadFileFromServer(name, 'video').subscribe(res => {
this.downloading_content['video'][name] = false;
downloadPlaylist(playlist) {
this.postsService.downloadPlaylistFromServer(playlist.id).subscribe(res => {
if (playlist.id) { this.downloading_content[playlist.type][playlist.id] = false };
const blob: Blob = res;
saveAs(blob, decodeURIComponent(name) + '.mp4');
if (!this.fileManagerEnabled) {
// tell server to delete the file once downloaded
this.postsService.deleteFile(name, 'audio').subscribe(delRes => {
});
}
});
}
downloadPlaylist(fileNames, type, zipName = null, playlistID = null) {
this.postsService.downloadFileFromServer(fileNames, type, zipName).subscribe(res => {
if (playlistID) { this.downloading_content[type][playlistID] = false };
const blob: Blob = res;
saveAs(blob, zipName + '.zip');
saveAs(blob, playlist.name + '.zip');
});
}
@@ -728,9 +647,8 @@ export class MainComponent implements OnInit {
this.errorFormats(url);
return;
}
const parsed_infos = this.getAudioAndVideoFormats(infos.formats);
const available_formats = {audio: parsed_infos[0], video: parsed_infos[1]};
this.cachedAvailableFormats[url]['formats'] = available_formats;
this.cachedAvailableFormats[url]['formats'] = this.getAudioAndVideoFormats(infos.formats);
console.log(this.cachedAvailableFormats[url]['formats']);
}, err => {
this.errorFormats(url);
});
@@ -773,7 +691,7 @@ export class MainComponent implements OnInit {
if (audio_format) {
format_array.push('-f', audio_format);
} else if (this.selectedQuality) {
format_array.push('--audio-quality', this.selectedQuality);
format_array.push('--audio-quality', this.selectedQuality['format_id']);
}
// pushes formats
@@ -789,7 +707,7 @@ export class MainComponent implements OnInit {
if (video_format) {
format_array = ['-f', video_format];
} else if (this.selectedQuality) {
format_array = [`bestvideo[height=${this.selectedQuality}]+bestaudio/best[height=${this.selectedQuality}]`];
format_array = [`bestvideo[height=${this.selectedQuality['format_id']}]+bestaudio/best[height=${this.selectedQuality}]`];
}
// pushes formats
@@ -886,9 +804,11 @@ export class MainComponent implements OnInit {
}
}
getAudioAndVideoFormats(formats): any[] {
const audio_formats = {};
const video_formats = {};
getAudioAndVideoFormats(formats) {
const audio_formats: any = {};
const video_formats: any = {};
console.log(formats);
for (let i = 0; i < formats.length; i++) {
const format_obj = {type: null};
@@ -899,9 +819,12 @@ export class MainComponent implements OnInit {
format_obj.type = format_type;
if (format_obj.type === 'audio' && format.abr) {
const key = format.abr.toString() + 'K';
format_obj['key'] = key;
format_obj['bitrate'] = format.abr;
format_obj['format_id'] = format.format_id;
format_obj['ext'] = format.ext;
format_obj['label'] = key;
// don't overwrite if not m4a
if (audio_formats[key]) {
if (format.ext === 'm4a') {
@@ -912,11 +835,14 @@ export class MainComponent implements OnInit {
}
} else if (format_obj.type === 'video') {
// check if video format is mp4
const key = format.format_note.replace('p', '');
const key = `${format.height}p${Math.round(format.fps)}`;
if (format.ext === 'mp4' || format.ext === 'mkv' || format.ext === 'webm') {
format_obj['key'] = key;
format_obj['height'] = format.height;
format_obj['acodec'] = format.acodec;
format_obj['format_id'] = format.format_id;
format_obj['label'] = key;
format_obj['fps'] = Math.round(format.fps);
// no acodec means no overwrite
if (!(video_formats[key]) || format_obj['acodec'] !== 'none') {
@@ -926,9 +852,17 @@ export class MainComponent implements OnInit {
}
}
video_formats['best_audio_format'] = this.getBestAudioFormatForMp4(audio_formats);
const parsed_formats: any = {};
return [audio_formats, video_formats]
parsed_formats['best_audio_format'] = this.getBestAudioFormatForMp4(audio_formats);
parsed_formats['video'] = Object.values(video_formats);
parsed_formats['audio'] = Object.values(audio_formats);
parsed_formats['video'] = parsed_formats['video'].sort((a, b) => b.height - a.height || b.fps - a.fps);
parsed_formats['audio'] = parsed_formats['audio'].sort((a, b) => b.bitrate - a.bitrate);
return parsed_formats;
}
getBestAudioFormatForMp4(audio_formats) {

View File

@@ -9,7 +9,6 @@
.audio-styles {
height: 50px;
background-color: transparent;
width: 100%;
}

View File

@@ -1,14 +1,14 @@
<div style="height: 100%" *ngIf="playlist.length > 0 && show_player">
<div style="height: 100%" [ngClass]="(type === 'audio') ? null : 'container-video'">
<div style="height: 100%" [ngClass]="(currentItem.type === 'audio/mp3') ? null : 'container-video'">
<div style="max-width: 100%; margin-left: 0px; height: 100%">
<mat-drawer-container style="height: 100%" class="example-container" autosize>
<div style="height: fit-content" [ngClass]="(type === 'audio') ? 'audio-col' : 'video-col'">
<vg-player style="height: fit-content; max-height: 75vh" (onPlayerReady)="onPlayerReady($event)" [style.background-color]="(type === 'audio') ? 'transparent' : 'black'">
<video [ngClass]="(type === 'audio') ? 'audio-styles' : 'video-styles'" #media class="video-player" [vgMedia]="media" [src]="currentItem.src" id="singleVideo" preload="auto" controls>
<div style="height: fit-content" [ngClass]="(currentItem.type === 'audio/mp3') ? 'audio-col' : 'video-col'">
<vg-player style="height: fit-content; max-height: 75vh" (onPlayerReady)="onPlayerReady($event)" [style.background-color]="(currentItem.type === 'audio/mp3') ? postsService.theme.drawer_color : 'black'">
<video [ngClass]="(currentItem.type === 'audio/mp3') ? 'audio-styles' : 'video-styles'" #media class="video-player" [vgMedia]="media" [src]="currentItem.src" id="singleVideo" preload="auto" controls>
</video>
</vg-player>
</div>
<div *ngIf="db_file" style="height: fit-content; width: 100%; margin-top: 10px;">
<div style="height: fit-content; width: 100%; margin-top: 10px;">
<div class="container">
<div class="row">
<div class="col-2 col-lg-1">
@@ -27,14 +27,13 @@
</ng-container>
</div>
<div class="col-2">
<ng-container *ngIf="playlist.length > 1">
<ng-container *ngIf="db_playlist">
<button (click)="downloadContent()" [disabled]="downloading" mat-icon-button><mat-icon>save</mat-icon><mat-spinner *ngIf="downloading" class="spinner" [diameter]="35"></mat-spinner></button>
<button *ngIf="!id" color="accent" (click)="namePlaylistDialog()" mat-icon-button><mat-icon>favorite</mat-icon></button>
<button *ngIf="!is_shared && id && (!postsService.isLoggedIn || postsService.permissions.includes('sharing'))" (click)="openShareDialog()" mat-icon-button><mat-icon>share</mat-icon></button>
<button *ngIf="(!postsService.isLoggedIn || postsService.permissions.includes('sharing')) && !auto" (click)="openShareDialog()" mat-icon-button><mat-icon>share</mat-icon></button>
</ng-container>
<ng-container *ngIf="playlist.length === 1">
<ng-container *ngIf="db_file">
<button (click)="downloadFile()" [disabled]="downloading" mat-icon-button><mat-icon>save</mat-icon><mat-spinner *ngIf="downloading" class="spinner" [diameter]="35"></mat-spinner></button>
<button *ngIf="!is_shared && uid && uid !== 'false' && type !== 'subscription' && (!postsService.isLoggedIn || postsService.permissions.includes('sharing'))" (click)="openShareDialog()" mat-icon-button><mat-icon>share</mat-icon></button>
<button *ngIf="type !== 'subscription' && (!postsService.isLoggedIn || postsService.permissions.includes('sharing'))" (click)="openShareDialog()" mat-icon-button><mat-icon>share</mat-icon></button>
<button (click)="openFileInfoDialog()" *ngIf="db_file" mat-icon-button><mat-icon>info</mat-icon></button>
</ng-container>
<button *ngIf="db_file && db_file.url.includes('twitch.tv/videos/') && postsService['config']['API']['use_twitch_API']" (click)="drawer.toggle()" mat-icon-button><mat-icon>chat</mat-icon></button>
@@ -47,6 +46,9 @@
<mat-button-toggle cdkDrag *ngFor="let playlist_item of playlist; let i = index" [checked]="currentItem.title === playlist_item.title" (click)="onClickPlaylistItem(playlist_item, i)" class="toggle-button" [value]="playlist_item.title">{{playlist_item.label}}</mat-button-toggle>
</mat-button-toggle-group>
</div>
<app-concurrent-stream *ngIf="db_file && api && postsService.config" (setPlaybackRate)="setPlaybackRate($event)" (togglePlayback)="togglePlayback($event)" (setPlaybackTimestamp)="setPlaybackTimestamp($event)" [playing]="api.state === 'playing'" [uid]="uid" [playback_timestamp]="api.time.current/1000" [server_mode]="!postsService.config.Advanced.multi_user_mode || postsService.isLoggedIn"></app-concurrent-stream>
<mat-drawer #drawer class="example-sidenav" mode="side" position="end" [opened]="db_file && db_file['chat_exists'] && postsService['config']['API']['use_twitch_API']">
<ng-container *ngIf="api_ready && db_file && db_file.url.includes('twitch.tv/videos/')">
<app-twitch-chat #twitchchat [db_file]="db_file" [current_timestamp]="api.currentTime" [sub]="subscription"></app-twitch-chat>

View File

@@ -1,4 +1,4 @@
import { Component, OnInit, HostListener, EventEmitter, OnDestroy, AfterViewInit, ViewChild } from '@angular/core';
import { Component, OnInit, HostListener, EventEmitter, OnDestroy, AfterViewInit, ViewChild, ChangeDetectorRef } from '@angular/core';
import { VgApiService } from '@videogular/ngx-videogular/core';
import { PostsService } from 'app/posts.services';
import { ActivatedRoute, Router } from '@angular/router';
@@ -36,17 +36,16 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
api_ready = false;
// params
fileNames: string[];
uids: string[];
type: string;
id = null; // used for playlists (not subscription)
playlist_id = null; // used for playlists (not subscription)
uid = null; // used for non-subscription files (audio, video, playlist)
subscription = null;
subscriptionName = null;
sub_id = null;
subPlaylist = null;
uuid = null; // used for sharing in multi-user mode, uuid is the user that downloaded the video
timestamp = null;
is_shared = false;
auto = null;
db_playlist = null;
db_file = null;
@@ -56,8 +55,6 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
videoFolderPath = null;
subscriptionFolderPath = null;
sharingEnabled = null;
// url-mode params
url = null;
name = null;
@@ -79,15 +76,14 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
ngOnInit(): void {
this.innerWidth = window.innerWidth;
this.type = this.route.snapshot.paramMap.get('type');
this.id = this.route.snapshot.paramMap.get('id');
this.playlist_id = this.route.snapshot.paramMap.get('playlist_id');
this.uid = this.route.snapshot.paramMap.get('uid');
this.subscriptionName = this.route.snapshot.paramMap.get('subscriptionName');
this.subPlaylist = this.route.snapshot.paramMap.get('subPlaylist');
this.sub_id = this.route.snapshot.paramMap.get('sub_id');
this.url = this.route.snapshot.paramMap.get('url');
this.name = this.route.snapshot.paramMap.get('name');
this.uuid = this.route.snapshot.paramMap.get('uuid');
this.timestamp = this.route.snapshot.paramMap.get('timestamp');
this.auto = this.route.snapshot.paramMap.get('auto');
// loading config
if (this.postsService.initialized) {
@@ -102,6 +98,7 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
}
ngAfterViewInit() {
this.cdr.detectChanges();
this.postsService.sidenav.close();
}
@@ -111,7 +108,7 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
}
constructor(public postsService: PostsService, private route: ActivatedRoute, private dialog: MatDialog, private router: Router,
public snackBar: MatSnackBar) {
public snackBar: MatSnackBar, private cdr: ChangeDetectorRef) {
}
@@ -120,19 +117,14 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
this.audioFolderPath = this.postsService.config['Downloader']['path-audio'];
this.videoFolderPath = this.postsService.config['Downloader']['path-video'];
this.subscriptionFolderPath = this.postsService.config['Subscriptions']['subscriptions_base_path'];
this.fileNames = this.route.snapshot.paramMap.get('fileNames') ? this.route.snapshot.paramMap.get('fileNames').split('|nvr|') : null;
if (!this.fileNames && !this.type) {
this.is_shared = true;
}
if (this.uid && !this.id) {
this.getFile();
} else if (this.id) {
this.getPlaylistFiles();
} else if (this.subscriptionName) {
if (this.sub_id) {
this.getSubscription();
}
} else if (this.playlist_id) {
this.getPlaylistFiles();
} else if (this.uid) {
this.getFile();
}
if (this.url) {
// if a url is given, just stream the URL
@@ -147,14 +139,10 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
this.currentItem = this.playlist[0];
this.currentIndex = 0;
this.show_player = true;
} else if (this.fileNames && !this.subscriptionName) {
this.show_player = true;
this.parseFileNames();
}
}
getFile() {
const already_has_filenames = !!this.fileNames;
this.postsService.getFile(this.uid, null, this.uuid).subscribe(res => {
this.db_file = res['file'];
if (!this.db_file) {
@@ -165,57 +153,41 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
console.error('Failed to increment view count');
console.error(err);
});
this.sharingEnabled = this.db_file.sharingEnabled;
if (!this.fileNames) {
// means it's a shared video
if (!this.id) {
// regular video/audio file (not playlist)
this.fileNames = [this.db_file['id']];
this.type = this.db_file['isAudio'] ? 'audio' : 'video';
if (!already_has_filenames) { this.parseFileNames(); }
}
}
if (this.db_file['sharingEnabled'] || !this.uuid) {
this.show_player = true;
} else if (!already_has_filenames) {
this.openSnackBar('Error: Sharing has been disabled for this video!', 'Dismiss');
}
// regular video/audio file (not playlist)
this.uids = [this.db_file['uid']];
this.type = this.db_file['isAudio'] ? 'audio' : 'video';
this.parseFileNames();
});
}
getSubscription() {
this.postsService.getSubscription(null, this.subscriptionName).subscribe(res => {
this.postsService.getSubscription(this.sub_id).subscribe(res => {
const subscription = res['subscription'];
this.subscription = subscription;
if (this.fileNames) {
subscription.videos.forEach(video => {
if (video['id'] === this.fileNames[0]) {
this.db_file = video;
this.postsService.incrementViewCount(this.db_file['uid'], this.subscription['id'], this.uuid).subscribe(res => {}, err => {
console.error('Failed to increment view count');
console.error(err);
});
this.show_player = true;
this.parseFileNames();
}
});
} else {
console.log('no file name specified');
}
this.type === this.subscription.type;
subscription.videos.forEach(video => {
if (video['uid'] === this.uid) {
this.db_file = video;
this.postsService.incrementViewCount(this.db_file['uid'], this.sub_id, this.uuid).subscribe(res => {}, err => {
console.error('Failed to increment view count');
console.error(err);
});
this.uids = [this.db_file['uid']];
this.show_player = true;
this.parseFileNames();
}
});
}, err => {
this.openSnackBar(`Failed to find subscription ${this.subscriptionName}`, 'Dismiss');
this.openSnackBar(`Failed to find subscription ${this.sub_id}`, 'Dismiss');
});
}
getPlaylistFiles() {
if (this.route.snapshot.paramMap.get('auto') === 'true') {
this.show_player = true;
return;
}
this.postsService.getPlaylist(this.id, null, this.uuid).subscribe(res => {
this.postsService.getPlaylist(this.playlist_id, this.uuid, true).subscribe(res => {
if (res['playlist']) {
this.db_playlist = res['playlist'];
this.fileNames = this.db_playlist['fileNames'];
this.db_playlist['file_objs'] = res['file_objs'];
this.uids = this.db_playlist.uids;
this.type = res['type'];
this.show_player = true;
this.parseFileNames();
@@ -227,69 +199,49 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
});
}
parseFileNames() {
let fileType = null;
if (this.type === 'audio') {
fileType = 'audio/mp3';
} else if (this.type === 'video') {
fileType = 'video/mp4';
} else {
// error
console.error('Must have valid file type! Use \'audio\', \'video\', or \'subscription\'.');
}
parseFileNames() {
this.playlist = [];
for (let i = 0; i < this.fileNames.length; i++) {
const fileName = this.fileNames[i];
let baseLocation = null;
let fullLocation = null;
for (let i = 0; i < this.uids.length; i++) {
const uid = this.uids[i];
// adds user token if in multi-user-mode
const uuid_str = this.uuid ? `&uuid=${this.uuid}` : '';
const uid_str = (this.id || !this.db_file) ? '' : `&uid=${this.db_file.uid}`;
const type_str = (this.type || !this.db_file) ? `&type=${this.type}` : `&type=${this.db_file.type}`
const id_str = this.id ? `&id=${this.id}` : '';
const file_path_str = (!this.db_file) ? '' : `&file_path=${encodeURIComponent(this.db_file.path)}`;
const file_obj = this.playlist_id ? this.db_playlist['file_objs'][i] : this.db_file;
if (!this.subscriptionName) {
baseLocation = 'stream/';
fullLocation = this.baseStreamPath + baseLocation + encodeURIComponent(fileName) + `?test=test${type_str}${file_path_str}`;
} else {
// default to video but include subscription name param
baseLocation = 'stream/';
fullLocation = this.baseStreamPath + baseLocation + encodeURIComponent(fileName) + '?subName=' + this.subscriptionName +
'&subPlaylist=' + this.subPlaylist + `${file_path_str}${type_str}`;
}
const mime_type = file_obj.isAudio ? 'audio/mp3' : 'video/mp4'
let baseLocation = 'stream/';
let fullLocation = this.baseStreamPath + baseLocation + `?test=test&uid=${file_obj['uid']}`;
if (this.postsService.isLoggedIn) {
fullLocation += (this.subscriptionName ? '&' : '&') + `jwt=${this.postsService.token}`;
if (this.is_shared) { fullLocation += `${uuid_str}${uid_str}${type_str}${id_str}`; }
} else if (this.is_shared) {
fullLocation += (this.subscriptionName ? '&' : '?') + `test=test${uuid_str}${uid_str}${type_str}${id_str}`;
fullLocation += `&jwt=${this.postsService.token}`;
}
// if it has a slash (meaning it's in a directory), only get the file name for the label
let label = null;
const decodedName = decodeURIComponent(fileName);
const hasSlash = decodedName.includes('/') || decodedName.includes('\\');
if (hasSlash) {
label = decodedName.replace(/^.*[\\\/]/, '');
} else {
label = decodedName;
if (this.uuid) {
fullLocation += `&uuid=${this.uuid}`;
}
if (this.sub_id) {
fullLocation += `&sub_id=${this.sub_id}`;
} else if (this.playlist_id) {
fullLocation += `&playlist_id=${this.playlist_id}`;
}
const mediaObject: IMedia = {
title: fileName,
title: file_obj['title'],
src: fullLocation,
type: fileType,
label: label
type: mime_type,
label: file_obj['title']
}
this.playlist.push(mediaObject);
}
this.currentItem = this.playlist[this.currentIndex];
this.original_playlist = JSON.stringify(this.playlist);
this.show_player = true;
}
onPlayerReady(api: VgApiService) {
this.api = api;
this.api_ready = true;
this.cdr.detectChanges();
// checks if volume has been previously set. if so, use that as default
if (localStorage.getItem('player_volume')) {
@@ -354,15 +306,9 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
}
downloadContent() {
const fileNames = [];
for (let i = 0; i < this.playlist.length; i++) {
fileNames.push(this.playlist[i].title);
}
const zipName = fileNames[0].split(' ')[0] + fileNames[1].split(' ')[0];
const zipName = this.db_playlist.name;
this.downloading = true;
this.postsService.downloadFileFromServer(fileNames, this.type, zipName, null, null, null, null,
!this.uuid ? this.postsService.user.uid : this.uuid, this.id).subscribe(res => {
this.postsService.downloadPlaylistFromServer(this.playlist_id, this.uuid).subscribe(res => {
this.downloading = false;
const blob: Blob = res;
saveAs(blob, zipName + '.zip');
@@ -373,11 +319,10 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
}
downloadFile() {
const ext = (this.type === 'audio') ? '.mp3' : '.mp4';
const filename = this.playlist[0].title;
const ext = (this.playlist[0].type === 'audio/mp3') ? '.mp3' : '.mp4';
this.downloading = true;
this.postsService.downloadFileFromServer(filename, this.type, null, null, this.subscriptionName, this.subPlaylist,
this.is_shared ? this.db_file['uid'] : null, this.uuid).subscribe(res => {
this.postsService.downloadFileFromServer(this.uid, this.uuid, this.sub_id).subscribe(res => {
this.downloading = false;
const blob: Blob = res;
saveAs(blob, filename + ext);
@@ -387,50 +332,10 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
});
}
namePlaylistDialog() {
const done = new EventEmitter<any>();
const dialogRef = this.dialog.open(InputDialogComponent, {
width: '300px',
data: {
inputTitle: 'Name the playlist',
inputPlaceholder: 'Name',
submitText: 'Favorite',
doneEmitter: done
}
});
done.subscribe(name => {
// Eventually do additional checks on name
if (name) {
const fileNames = this.getFileNames();
this.postsService.createPlaylist(name, fileNames, this.type, null).subscribe(res => {
if (res['success']) {
dialogRef.close();
const new_playlist = res['new_playlist'];
this.db_playlist = new_playlist;
this.openSnackBar('Playlist \'' + name + '\' successfully created!', '')
this.playlistPostCreationHandler(new_playlist.id);
}
});
}
});
}
/*
createPlaylist(name) {
this.postsService.createPlaylist(name, this.fileNames, this.type, null).subscribe(res => {
if (res['success']) {
console.log('Success!');
}
});
}
*/
playlistPostCreationHandler(playlistID) {
// changes the route without moving from the current view or
// triggering a navigation event
this.id = playlistID;
this.playlist_id = playlistID;
this.router.navigateByUrl(this.router.url + ';id=' + playlistID);
}
@@ -445,11 +350,11 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
updatePlaylist() {
const fileNames = this.getFileNames();
this.playlist_updating = true;
this.postsService.updatePlaylistFiles(this.id, fileNames, this.type).subscribe(res => {
this.postsService.updatePlaylistFiles(this.playlist_id, fileNames, this.type).subscribe(res => {
this.playlist_updating = false;
if (res['success']) {
const fileNamesEncoded = fileNames.join('|nvr|');
this.router.navigate(['/player', {fileNames: fileNamesEncoded, type: this.type, id: this.id}]);
this.router.navigate(['/player', {fileNames: fileNamesEncoded, type: this.type, id: this.playlist_id}]);
this.openSnackBar('Successfully updated playlist.', '');
this.original_playlist = JSON.stringify(this.playlist);
} else {
@@ -461,10 +366,9 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
openShareDialog() {
const dialogRef = this.dialog.open(ShareMediaDialogComponent, {
data: {
uid: this.id ? this.id : this.uid,
type: this.type,
sharing_enabled: this.id ? this.db_playlist.sharingEnabled : this.db_file.sharingEnabled,
is_playlist: !!this.id,
uid: this.playlist_id ? this.playlist_id : this.uid,
sharing_enabled: this.playlist_id ? this.db_playlist.sharingEnabled : this.db_file.sharingEnabled,
is_playlist: !!this.playlist_id,
uuid: this.postsService.isLoggedIn ? this.postsService.user.uid : null,
current_timestamp: this.api.time.current
},
@@ -472,7 +376,7 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
});
dialogRef.afterClosed().subscribe(res => {
if (!this.id) {
if (!this.playlist_id) {
this.getFile();
} else {
this.getPlaylistFiles();
@@ -489,6 +393,22 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
})
}
setPlaybackTimestamp(time) {
this.api.seekTime(time);
}
togglePlayback(to_play) {
if (to_play) {
this.api.play();
} else {
this.api.pause();
}
}
setPlaybackRate(speed) {
this.api.playbackRate = speed;
}
// snackbar helper
public openSnackBar(message: string, action: string) {
this.snackBar.open(message, action, {

View File

@@ -171,33 +171,39 @@ export class PostsService implements CanActivate {
}
// tslint:disable-next-line: max-line-length
makeMP3(url: string, selectedQuality: string, customQualityConfiguration: string, customArgs: string = null, customOutput: string = null, youtubeUsername: string = null, youtubePassword: string = null, ui_uid = null) {
return this.http.post(this.path + 'tomp3', {url: url,
maxBitrate: selectedQuality,
customQualityConfiguration: customQualityConfiguration,
customArgs: customArgs,
customOutput: customOutput,
youtubeUsername: youtubeUsername,
youtubePassword: youtubePassword,
ui_uid: ui_uid}, this.httpOptions);
}
// tslint:disable-next-line: max-line-length
makeMP4(url: string, selectedQuality: string, customQualityConfiguration: string, customArgs: string = null, customOutput: string = null, youtubeUsername: string = null, youtubePassword: string = null, ui_uid = null) {
return this.http.post(this.path + 'tomp4', {url: url,
downloadFile(url: string, type: string, selectedQuality: string, customQualityConfiguration: string, customArgs: string = null, customOutput: string = null, youtubeUsername: string = null, youtubePassword: string = null, ui_uid = null, cropFileSettings = null) {
return this.http.post(this.path + 'downloadFile', {url: url,
selectedHeight: selectedQuality,
customQualityConfiguration: customQualityConfiguration,
customArgs: customArgs,
customOutput: customOutput,
youtubeUsername: youtubeUsername,
youtubePassword: youtubePassword,
ui_uid: ui_uid}, this.httpOptions);
ui_uid: ui_uid,
type: type,
cropFileSettings: cropFileSettings}, this.httpOptions);
}
getDBInfo() {
return this.http.post(this.path + 'getDBInfo', {}, this.httpOptions);
}
transferDB(local_to_remote) {
return this.http.post(this.path + 'transferDB', {local_to_remote: local_to_remote}, this.httpOptions);
}
testConnectionString() {
return this.http.post(this.path + 'testConnectionString', {}, this.httpOptions);
}
killAllDownloads() {
return this.http.post(this.path + 'killAllDownloads', {}, this.httpOptions);
}
restartServer() {
return this.http.post(this.path + 'restartServer', {}, this.httpOptions);
}
loadNavItems() {
if (isDevMode()) {
return this.http.get('./assets/default.json');
@@ -214,8 +220,8 @@ export class PostsService implements CanActivate {
return this.http.post(this.path + 'setConfig', {new_config_file: config}, this.httpOptions);
}
deleteFile(uid: string, type: string, blacklistMode = false) {
return this.http.post(this.path + 'deleteFile', {uid: uid, type: type, blacklistMode: blacklistMode}, this.httpOptions);
deleteFile(uid: string, blacklistMode = false) {
return this.http.post(this.path + 'deleteFile', {uid: uid, blacklistMode: blacklistMode}, this.httpOptions);
}
getMp3s() {
@@ -242,22 +248,43 @@ export class PostsService implements CanActivate {
return this.http.post(this.path + 'downloadTwitchChatByVODID', {id: id, type: type, vodId: vodId, uuid: uuid, sub: sub}, this.httpOptions);
}
downloadFileFromServer(fileName, type, outputName = null, fullPathProvided = null, subscriptionName = null, subPlaylist = null,
uid = null, uuid = null, id = null) {
return this.http.post(this.path + 'downloadFile', {fileNames: fileName,
type: type,
zip_mode: Array.isArray(fileName),
outputName: outputName,
fullPathProvided: fullPathProvided,
subscriptionName: subscriptionName,
subPlaylist: subPlaylist,
uuid: uuid,
downloadFileFromServer(uid, uuid = null, sub_id = null, is_playlist = null) {
return this.http.post(this.path + 'downloadFileFromServer', {
uid: uid,
id: id
uuid: uuid,
sub_id: sub_id,
is_playlist: is_playlist
},
{responseType: 'blob', params: this.httpOptions.params});
}
downloadPlaylistFromServer(playlist_id, uuid = null) {
return this.http.post(this.path + 'downloadFileFromServer', {
uuid: uuid,
playlist_id: playlist_id
},
{responseType: 'blob', params: this.httpOptions.params});
}
downloadSubFromServer(sub_id, uuid = null) {
return this.http.post(this.path + 'downloadFileFromServer', {
uuid: uuid,
sub_id: sub_id
},
{responseType: 'blob', params: this.httpOptions.params});
}
checkConcurrentStream(uid) {
return this.http.post(this.path + 'checkConcurrentStream', {uid: uid}, this.httpOptions);
}
updateConcurrentStream(uid, playback_timestamp, unix_timestamp, playing) {
return this.http.post(this.path + 'updateConcurrentStream', {uid: uid,
playback_timestamp: playback_timestamp,
unix_timestamp: unix_timestamp,
playing: playing}, this.httpOptions);
}
uploadCookiesFile(fileFormData) {
return this.http.post(this.path + 'uploadCookies', fileFormData, this.httpOptions);
}
@@ -282,29 +309,29 @@ export class PostsService implements CanActivate {
return this.http.post(this.path + 'generateNewAPIKey', {}, this.httpOptions);
}
enableSharing(uid, type, is_playlist) {
return this.http.post(this.path + 'enableSharing', {uid: uid, type: type, is_playlist: is_playlist}, this.httpOptions);
enableSharing(uid, is_playlist) {
return this.http.post(this.path + 'enableSharing', {uid: uid, is_playlist: is_playlist}, this.httpOptions);
}
disableSharing(uid, is_playlist) {
return this.http.post(this.path + 'disableSharing', {uid: uid, is_playlist: is_playlist}, this.httpOptions);
}
incrementViewCount(file_uid, sub_id, uuid) {
return this.http.post(this.path + 'incrementViewCount', {file_uid: file_uid, sub_id: sub_id, uuid: uuid}, this.httpOptions);
}
disableSharing(uid, type, is_playlist) {
return this.http.post(this.path + 'disableSharing', {uid: uid, type: type, is_playlist: is_playlist}, this.httpOptions);
}
createPlaylist(playlistName, fileNames, type, thumbnailURL, duration = null) {
createPlaylist(playlistName, uids, type, thumbnailURL) {
return this.http.post(this.path + 'createPlaylist', {playlistName: playlistName,
fileNames: fileNames,
uids: uids,
type: type,
thumbnailURL: thumbnailURL,
duration: duration}, this.httpOptions);
thumbnailURL: thumbnailURL}, this.httpOptions);
}
getPlaylist(playlistID, type, uuid = null) {
return this.http.post(this.path + 'getPlaylist', {playlistID: playlistID,
type: type, uuid: uuid}, this.httpOptions);
getPlaylist(playlist_id, uuid = null, include_file_metadata = false) {
return this.http.post(this.path + 'getPlaylist', {playlist_id: playlist_id,
uuid: uuid,
include_file_metadata: include_file_metadata}, this.httpOptions);
}
updatePlaylist(playlist) {
@@ -357,10 +384,12 @@ export class PostsService implements CanActivate {
}
updateSubscription(subscription) {
delete subscription['videos'];
return this.http.post(this.path + 'updateSubscription', {subscription: subscription}, this.httpOptions);
}
unsubscribe(sub, deleteMode = false) {
delete sub['videos'];
return this.http.post(this.path + 'unsubscribe', {sub: sub, deleteMode: deleteMode}, this.httpOptions)
}

View File

@@ -159,7 +159,7 @@ export const isoLangs = {
},
'nl': {
'name': 'Dutch',
'nativeName': 'Nederlands, Vlaams'
'nativeName': 'Nederlands'
},
'en': {
'name': 'English',

View File

@@ -140,7 +140,7 @@
<mat-divider></mat-divider>
<div *ngIf="new_config" class="container-fluid">
<div class="row">
<div class="col-12 mt-3 mb-2">
<div class="col-12 mt-3">
<h6 i18n="Categories">Categories</h6>
<div cdkDropList class="category-list" (cdkDropListDropped)="dropCategory($event)">
<div class="category-box" *ngFor="let category of postsService.categories" cdkDrag>
@@ -154,6 +154,9 @@
</div>
<button style="margin-top: 10px;" mat-mini-fab (click)="openAddCategoryDialog()"><mat-icon>add</mat-icon></button>
</div>
<div class="col-12 mt-2 mb-2">
<mat-checkbox [(ngModel)]="new_config['Extra']['allow_playlist_categorization']" matTooltip="With this setting enabled, if a single video matches a category, the entire playlist will receive that category." i18n-matTooltip="Allow playlist categorization setting tooltip"><ng-container i18n="Allow playlist categorization setting label">Allow playlist categorization</ng-container></mat-checkbox>
</div>
</div>
</div>
<mat-divider></mat-divider>
@@ -216,7 +219,7 @@
<div class="enable-api-key-div">
<mat-form-field class="text-field" color="accent">
<input [disabled]="!new_config['API']['use_API_key']" [(ngModel)]="new_config['API']['API_key']" matInput placeholder="Public API Key" i18n-placeholder="Public API Key setting placeholder" required>
<mat-hint><a target="_blank" href="https://stoplight.io/p/docs/gh/tzahi12345/youtubedl-material"><ng-container i18n="View API docs setting hint">View documentation</ng-container></a></mat-hint>
<mat-hint><a target="_blank" href="https://youtubedl-material.stoplight.io/docs/youtubedl-material/Public%20API%20v1.yaml"><ng-container i18n="View API docs setting hint">View documentation</ng-container></a></mat-hint>
</mat-form-field>
</div>
<div class="api-key-div">
@@ -277,6 +280,43 @@
</div>
</ng-template>
</mat-tab>
<!-- Database -->
<mat-tab label="Database" i18n-label="Database settings label">
<ng-template matTabContent>
<div *ngIf="new_config" class="container-fluid">
<div class="row">
<div class="col-12 mt-3">
<div *ngIf="db_info">
<h5 i18n="Database info title">Database Info</h5>
<p><ng-container i18n="Database location label">Database location:</ng-container>&nbsp;<strong>{{db_info['using_local_db'] ? 'Local' : 'MongoDB'}}</strong></p>
<h6 i18n="Records per table label">Records per table</h6>
<mat-list style="padding-top: 0px">
<mat-list-item style="height: 28px" *ngFor="let table_stats of db_info['stats_by_table'] | keyvalue">
{{table_stats.key}}: {{table_stats.value.records_count}}
</mat-list-item>
</mat-list>
<mat-form-field style="width: 100%; margin-top: 15px; margin-bottom: 10px" color="accent">
<input [(ngModel)]="new_config['Database']['mongodb_connection_string']" matInput placeholder="MongoDB Connection String" i18n-placeholder="MongoDB Connection String" required>
<mat-hint><ng-container i18n="MongoDB Connection String setting hint AKA preamble">Example:</ng-container>&nbsp;mongodb://127.0.0.1:27017/?compressors=zlib</mat-hint>
</mat-form-field>
<br>
<button (click)="testConnectionString()" [disabled]="testing_connection_string" mat-flat-button color="accent"><ng-container i18n="Test connection string button">Test connection string</ng-container></button>
<br>
<button class="transfer-db-button" [disabled]="db_transferring" color="accent" (click)="transferDB()" mat-raised-button><ng-container i18n="Transfer DB button">Transfer DB to </ng-container>{{db_info['using_local_db'] ? 'MongoDB' : 'Local'}}</button>
</div>
<div *ngIf="!db_info">
<ng-container i18n="Database info not retrieved error message">Database information could not be retrieved. Check the server logs for more information.</ng-container>
</div>
</div>
</div>
</div>
</ng-template>
</mat-tab>
<!-- Advanced -->
<mat-tab label="Advanced" i18n-label="Host settings label">
<ng-template matTabContent>
@@ -351,6 +391,14 @@
<div *ngIf="new_config" class="container-fluid mt-1">
<app-updater></app-updater>
</div>
<mat-divider></mat-divider>
<div *ngIf="new_config" class="container">
<div class="row">
<div class="col-12 mt-4">
<button (click)="restartServer()" mat-stroked-button color="warn"><ng-container i18n="Restart server button">Restart server</ng-container></button>
</div>
</div>
</div>
</ng-template>
</mat-tab>
<mat-tab *ngIf="postsService.config && postsService.config.Advanced.multi_user_mode" label="Users" i18n-label="Users settings label">

View File

@@ -77,8 +77,13 @@
}
.category-custom-placeholder {
background: #ccc;
border: dotted 3px #999;
min-height: 60px;
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
background: #ccc;
border: dotted 3px #999;
min-height: 60px;
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}
.transfer-db-button {
margin-top: 10px;
margin-bottom: 10px;
}

View File

@@ -20,7 +20,7 @@ import { EditCategoryDialogComponent } from 'app/dialogs/edit-category-dialog/ed
})
export class SettingsComponent implements OnInit {
all_locales = isoLangs;
supported_locales = ['en', 'es', 'de', 'fr', 'zh', 'nb', 'it', 'en-GB'];
supported_locales = ['en', 'es', 'de', 'fr', 'nl', 'zh', 'nb', 'it', 'en-GB'];
initialLocale = localStorage.getItem('locale');
initial_config = null;
@@ -29,6 +29,10 @@ export class SettingsComponent implements OnInit {
generated_bookmarklet_code = null;
bookmarkletAudioOnly = false;
db_info = null;
db_transferring = false;
testing_connection_string = false;
_settingsSame = true;
latestGithubRelease = null;
@@ -48,6 +52,7 @@ export class SettingsComponent implements OnInit {
ngOnInit() {
this.getConfig();
this.getDBInfo();
this.generated_bookmarklet_code = this.sanitizer.bypassSecurityTrustUrl(this.generateBookmarkletCode());
@@ -255,6 +260,68 @@ export class SettingsComponent implements OnInit {
});
}
restartServer() {
this.postsService.restartServer().subscribe(res => {
this.postsService.openSnackBar('Restarting!');
}, err => {
this.postsService.openSnackBar('Failed to restart the server.');
});
}
getDBInfo() {
this.postsService.getDBInfo().subscribe(res => {
this.db_info = res['db_info'];
});
}
transferDB() {
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
data: {
dialogTitle: 'Transfer DB',
dialogText: `Are you sure you want to transfer the DB?`,
submitText: 'Transfer',
}
});
dialogRef.afterClosed().subscribe(confirmed => {
if (confirmed) {
this._transferDB();
}
});
}
_transferDB() {
this.db_transferring = true;
this.postsService.transferDB(this.db_info['using_local_db']).subscribe(res => {
this.db_transferring = false;
const success = res['success'];
if (success) {
this.openSnackBar('Successfully transfered DB! Reloading info...');
this.getDBInfo();
} else {
this.openSnackBar('Failed to transfer DB -- transfer was aborted. Error: ' + res['error']);
}
}, err => {
this.db_transferring = false;
this.openSnackBar('Failed to transfer DB -- API call failed. See browser logs for details.');
console.error(err);
});
}
testConnectionString() {
this.testing_connection_string = true;
this.postsService.testConnectionString().subscribe(res => {
this.testing_connection_string = false;
if (res['success']) {
this.postsService.openSnackBar('Connection successful!');
} else {
this.postsService.openSnackBar('Connection failed! Error: ' + res['error']);
}
}, err => {
this.testing_connection_string = false;
this.postsService.openSnackBar('Connection failed! Error: Server error. See logs for more info.');
});
}
// snackbar helper
public openSnackBar(message: string, action: string = '') {
this.snackBar.open(message, action, {

View File

@@ -42,7 +42,7 @@ export class SubscriptionFileCardComponent implements OnInit {
goToFile() {
const emit_obj = {
name: this.file.id,
uid: this.file.uid,
url: this.file.requested_formats ? this.file.requested_formats[0].url : this.file.url
}
this.goToFileEmit.emit(emit_obj);

View File

@@ -103,15 +103,14 @@ export class SubscriptionComponent implements OnInit, OnDestroy {
}
goToFile(emit_obj) {
const name = emit_obj['name'];
const uid = emit_obj['uid'];
const url = emit_obj['url'];
localStorage.setItem('player_navigator', this.router.url);
if (this.subscription.streamingOnly) {
this.router.navigate(['/player', {name: name, url: url}]);
this.router.navigate(['/player', {uid: uid, url: url}]);
} else {
this.router.navigate(['/player', {fileNames: name,
type: this.subscription.type ? this.subscription.type : 'video', subscriptionName: this.subscription.name,
subPlaylist: this.subscription.isPlaylist}]);
this.router.navigate(['/player', {uid: uid,
sub_id: this.subscription.id}]);
}
}
@@ -154,7 +153,7 @@ export class SubscriptionComponent implements OnInit, OnDestroy {
}
this.downloading = true;
this.postsService.downloadFileFromServer(fileNames, 'video', this.subscription.name, true).subscribe(res => {
this.postsService.downloadSubFromServer(this.subscription.id).subscribe(res => {
this.downloading = false;
const blob: Blob = res;
saveAs(blob, this.subscription.name + '.zip');

View File

@@ -20,7 +20,8 @@
"download_only_mode": false,
"allow_multi_download_mode": true,
"settings_pin_required": false,
"enable_downloads_manager": true
"enable_downloads_manager": true,
"allow_playlist_categorization": true
},
"API": {
"use_API_key": false,
@@ -28,7 +29,8 @@
"use_youtube_API": false,
"youtube_API_key": "",
"use_twitch_API": false,
"twitch_API_key": ""
"twitch_API_key": "",
"twitch_auto_download_chat": true
},
"Themes": {
"default_theme": "default",
@@ -37,7 +39,7 @@
"Subscriptions": {
"allow_subscriptions": true,
"subscriptions_base_path": "subscriptions/",
"subscriptions_check_interval": "300",
"subscriptions_check_interval": "86400",
"subscriptions_use_youtubedl_archive": true
},
"Users": {
@@ -52,6 +54,10 @@
"searchFilter": "(uid={{username}})"
}
},
"Database": {
"mongodb_connection_string": "mongodb://127.0.0.1:27017/?compressors=zlib",
"use_local_db": false
},
"Advanced": {
"use_default_downloading_agent": true,
"custom_downloading_agent": "",
@@ -60,7 +66,7 @@
"jwt_expiration": 86400,
"logger_level": "debug",
"use_cookies": false,
"default_downloader": "youtube-dlc"
"default_downloader": "youtube-dl"
}
}
}
}

View File

@@ -0,0 +1,248 @@
{
"004b222ff9ef9dd4771b777950ca1d0e4cd4348a": "Over",
"994363f08f9fbfa3b3994ff7b35c6904fdff18d8": "Profiel",
"adb4562d2dbd3584370e44496969d58c511ecb63": "Donker",
"121cc5391cd2a5115bc2b3160379ee5b36cd7716": "Instellingen",
"92eee6be6de0b11c924e3ab27db30257159c0a7c": "Overzicht",
"6765b4c916060f6bc42d9bb69e80377dbcb5e4e9": "Inloggen",
"357064ca9d9ac859eb618e28e8126fa32be049e2": "Abonnementen",
"822fab38216f64e8166d368b59fe756ca39d301b": "Downloads",
"4a9889d36910edc8323d7bab60858ab3da6d91df": "Alleen audio",
"6a21ba5fb0ac804a525bf9ab168038c3ee88e661": "Downloaden",
"a38ae1082fec79ba1f379978337385a539a28e73": "Kwaliteit",
"4be966a9dcfbc9b54dfcc604b831c0289f847fa4": "URL gebruiken",
"d3f02f845e62cebd75fde451ab8479d2a8ad784d": "Bekijken",
"96a01fafe135afc58b0f8071a4ab00234495ce18": "Meerdere video's downloaden",
"6a3777f913cf3f288664f0632b9f24794fdcc24e": "Afbreken",
"322ed150e02666fe2259c5b4614eac7066f4ffa0": "Geavanceerd",
"4e4c721129466be9c3862294dc40241b64045998": "Aanvullende opties toekennen",
"ad2f8ac8b7de7945b80c8e424484da94e597125f": "Aanvullende opties",
"a6911c2157f1b775284bbe9654ce5eb30cf45d7f": "Je hoeft alleen de aanvullende opties op te geven, dus niet de url. Je kunt de opties scheiden met twee komma's: ,,",
"3a92a3443c65a52f37ca7efb8f453b35dbefbf29": "Aangepaste uitvoer gebruiken",
"d9c02face477f2f9cdaae318ccee5f89856851fb": "Aangepaste uitvoer",
"fcfd4675b4c90f08d18d3abede9a9a4dff4cfdc7": "Documentatie",
"19d1ae64d94d28a29b2c57ae8671aace906b5401": "Het pad is relatief aan het ingestelde downloadpad. Laat de extensie achterwege.",
"b7ffe7c6586d6f3f18a9246806a7c7d5538ab43e": "Geteste opdracht:",
"8fad10737d3e3735a6699a4d89cbf6c20f6bb55f": "Authenticatie gebruiken",
"08c74dc9762957593b91f6eb5d65efdfc975bf48": "Gebruikersnaam",
"c32ef07f8803a223a83ed17024b38e8d82292407": "Wachtwoord",
"17f0ea5d2d7a262b0e875acc70475f102aee84e6": "Afspeellijst maken",
"cff1428d10d59d14e45edec3c735a27b5482db59": "Naam",
"f61c6867295f3b53d23557021f2f4e0aa1d0b8fc": "Soort",
"f0baeb8b69d120073b6d60d34785889b0c3232c8": "Audio",
"2d1ea268a6a9f483dbc2cbfe19bf4256a57a6af4": "Video",
"f47e2d56dd8a145b2e9599da9730c049d52962a2": "Audiobestanden",
"a52dae09be10ca3a65da918533ced3d3f4992238": "Video's",
"a9806cf78ce00eb2613eeca11354a97e033377b8": "Abonneren op afspeellijst of kanaal",
"801b98c6f02fe3b32f6afa3ee854c99ed83474e6": "URL",
"93efc99ae087fc116de708ecd3ace86ca237cf30": "De url van de afspeellijst of het kanaal",
"08f5d0ef937ae17feb1b04aff15ad88911e87baf": "Aangepaste naam",
"ea30873bd3f0d5e4fb2378eec3f0a1db77634a28": "Alle uploads downloaden",
"d641b8fa5ac5e85114c733b1f7de6976bd091f70": "Maximumkwaliteit",
"c76a955642714b8949ff3e4b4990864a2e2cac95": "Audiomodus",
"408ca4911457e84a348cecf214f02c69289aa8f1": "Streamingmodus",
"f432e1a8d6adb12e612127978ce2e0ced933959c": "Deze worden toegevoegd ná de standaardopties.",
"98b6ec9ec138186d663e64770267b67334353d63": "Aangepaste bestandsuitvoer",
"d7b35c384aecd25a516200d6921836374613dfe7": "Annuleren",
"d0336848b0c375a1c25ba369b3481ee383217a4f": "Abonneren",
"28a678e9cabf86e44c32594c43fa0e890135c20f": "Video's downloaden die geüpload zijn in de afgelopen",
"e78c0d60ac39787f62c9159646fe0b3c1ed55a1d": "Soort:",
"c52db455cca9109ee47e1a612c3f4117c09eb71b": "URL:",
"ca3dbbc7f3e011bffe32a10a3ea45cc84f30ecf1": "ID:",
"f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8": "Sluiten",
"8efc77bf327659c0fec1f518cf48a98cdcd9dddf": "Archief exporteren",
"3042bd3ad8dffcfeca5fd1ae6159fd1047434e95": "De-abonneren",
"303e45ffae995c9817e510e38cb969e6bb3adcbf": "(onderbroken)",
"a44d86aa1e6c20ced07aca3a7c081d8db9ded1c6": "Archief:",
"616e206cb4f25bd5885fc35925365e43cf5fb929": "Naam:",
"c6eb45d085384903e53ab001a3513d1de6a1dbac": "Uploader:",
"109c6f4a5e46efb933612ededfaf52a13178b7e0": "Bestandsgrootte:",
"bd630d8669b16e5f264ec4649d9b469fe03e5ff4": "Pad:",
"a67e7d843cef735c79d5ef1c8ba4af3e758912bb": "Uploaddatum:",
"0cc1dec590ecd74bef71a865fb364779bc42a749": "Categorie:",
"d9e83ac17026e70ef6e9c0f3240a3b2450367f40": "youtube-dl-opties aanpassen",
"7fc1946abe2b40f60059c6cd19975d677095fd19": "Geteste nieuwe aanvullende opties",
"0b71824ae71972f236039bed43f8d2323e8fd570": "Optie toevoegen",
"c8b0e59eb491f2ac7505f0fbab747062e6b32b23": "Zoeken op categorie",
"9eeb91caef5a50256dd87e1c4b7b3e8216479377": "Optiewaarde gebruiken",
"7de2451ed3fb8d8b847979bd3f0c740b970f167b": "Optie toevoegen",
"b2623aee44b70c9a4ba1fce16c8a593b0a4c7974": "Aanpassen",
"25d8ad5eba2ec24e68295a27d6a4bb9b49e3dacd": "Optiewaarde",
"91ecce65f1d23f9419d1c953cd6b7bc7f91c110e": "Updater",
"b7ff2e2b909c53abe088fe60b9f4b6ac7757247f": "Gebruikersregistratie",
"024886ca34a6f309e3e51c2ed849320592c3faaa": "Gebruikersnaam",
"cfc2f436ec2beffb042e7511a73c89c372e86a6c": "Registreren",
"ebadf946ae90f13ecd0c70f09edbc0f983af8a0f": "Nieuwe cookies uploaden",
"a8b7b9c168fd936a75e500806a8c0d7755ef1198": "Let op: de nieuwe cookies overschrijven de oude. Daarnaast zijn de cookies procesgebonden en niet gebruikersgebonden.",
"98a8a42e5efffe17ab786636ed0139b4c7032d0e": "Slepen-en-neerzetten",
"4f389e41e4592f7f9bb76abdd8af4afdfb13f4f1": "Afspeellijst aanpassen",
"5caadefa4143cf6766a621b0f54f91f373a1f164": "Inhoud toevoegen",
"52c9a103b812f258bcddc3d90a6e3f46871d25fe": "Opslaan",
"33026f57ea65cd9c8a5d917a08083f71a718933a": "Normale volgorde",
"29376982b1205d9d6ea3d289e8e2f8e1ac2839b1": "Omgekeerde volgorde",
"d02888c485d3aeab6de628508f4a00312a722894": "Mijn video's",
"7e892ba15f2c6c17e83510e273b3e10fc32ea016": "Zoeken",
"73423607944a694ce6f9e55cfee329681bb4d9f9": "Geen video's gevonden.",
"3697f8583ea42868aa269489ad366103d94aece7": "Bewerken",
"07db550ae114d9faad3a0cbb68bcc16ab6cd31fc": "Onderbroken",
"c3b0b86523f1d10e84a71f9b188d54913a11af3b": "Categorie bewerken",
"2489eefea00931942b91f4a1ae109514b591e2e1": "Regels",
"e4eeb9106dbcbc91ca1ac3fb4068915998a70f37": "Regel toevoegen",
"792dc6a57f28a1066db283f2e736484f066005fd": "Twitch-chatgesprek downloaden",
"28f86ffd419b869711aa13f5e5ff54be6d70731c": "Aanpassen",
"826b25211922a1b46436589233cb6f1a163d89b7": "Verwijderen",
"321e4419a943044e674beb55b8039f42a9761ca5": "Informatie",
"e684046d73bcee88e82f7ff01e2852789a05fc32": "Aantal:",
"34504b488c24c27e68089be549f0eeae6ebaf30b": "Verwijderen en op zwarte lijst plaatsen",
"dad95154dcef3509b8cc705046061fd24994bbb7": "weergaven",
"5b3075e8dc3f3921ec316b0bd83b6d14a06c1a4f": "Aanpassingen opslaan",
"4d8a18b04a1f785ecd8021ac824e0dfd5881dbfc": "Het downloaden is voltooid",
"348cc5d553b18e862eb1c1770e5636f6b05ba130": "Er is een fout opgetreden",
"4f8b2bb476981727ab34ed40fde1218361f92c45": "Details",
"e9aff8e6df2e2bf6299ea27bb2894c70bc48bd4d": "Er is een fout opgetreden:",
"77b0c73840665945b25bd128709aa64c8f017e1c": "Gestart om:",
"08ff9375ec078065bcdd7637b7ea65fce2979266": "Afgerond om:",
"ad127117f9471612f47d01eae09709da444a36a4": "Bestandspad(en):",
"e2319dec5b4ccfb6ed9f55ccabd63650a8fdf547": "Mijn abonnementen",
"807cf11e6ac1cde912496f764c176bdfdd6b7e19": "Kanalen",
"47546e45bbb476baaaad38244db444c427ddc502": "Afspeellijsten",
"29b89f751593e1b347eef103891b7a1ff36ec03f": "De naam is niet beschikbaar omdat het kanaal nog wordt opgehaald.",
"4636cd4a1379c50d471e98786098c4d39e1e82ad": "Je hebt geen abonnementen.",
"2e0a410652cb07d069f576b61eab32586a18320d": "De naam is niet beschikbaar omdat de afspeellijst nog wordt opgehaald.",
"587b57ced54965d8874c3fd0e9dfedb987e5df04": "Je hebt geen abonnementen.",
"82421c3e46a0453a70c42900eab51d58d79e6599": "Algemeen",
"0ba25ad86a240576c4f20a2fada4722ebba77b1e": "Downloader",
"d5f69691f9f05711633128b5a3db696783266b58": "Diversen",
"bc2e854e111ecf2bd7db170da5e3c2ed08181d88": "Geavanceerd",
"4d13a9cd5ed3dcee0eab22cb25198d43886942be": "Gebruikers",
"eb3d5aefff38a814b76da74371cbf02c0789a1ef": "Logboeken",
"fe8fd36dbf5deee1d56564965787a782a66eba44": "{VAR_SELECT, select, true {Close} false {Cancel} other {otha}}",
"54c512cca1923ab72faf1a0bd98d3d172469629a": "De url waarvan deze app wordt geladen, zonder het poortnummer.",
"cb2741a46e3560f6bc6dfd99d385e86b08b26d72": "Poort",
"22e8f1d0423a3b784fe40fab187b92c06541b577": "Het gewenste poortnummer (standaard: 17442).",
"d4477669a560750d2064051a510ef4d7679e2f3e": "Meerdere gebruikers",
"2eb03565fcdce7a7a67abc277a936a32fcf51557": "Gebruikersbasispad",
"a64505c41150663968e277ec9b3ddaa5f4838798": "Het basispad voor gebruikers en hun gedownloade video's.",
"4e3120311801c4acd18de7146add2ee4a4417773": "Abonnementen toestaan",
"4bee2a4bef2d26d37c9b353c278e24e5cd309ce3": "Abonnementenbasispad",
"bc9892814ee2d119ae94378c905ea440a249b84a": "Het basispad voor video's van afspeellijsten en kanalen uit je abonnementen. Dit is relatief aan YTDL-Material's hoofdmap.",
"5bef4b25ba680da7fff06b86a91b1fc7e6a926e3": "Controletussenpoos",
"0f56a7449b77630c114615395bbda4cab398efd8": "In seconden (alleen cijfers).",
"13759b09a7f4074ceee8fa2f968f9815fdf63295": "Soms worden nieuwe video's gedownload voordat ze volledig verwerkt zijn. Met deze instelling wordt de volgende dag gecontroleerd of er een hogere kwaliteit beschikbaar is.",
"3d1a47dc18b7bd8b5d9e1eb44b235ed9c4a2b513": "Nieuwe uploads opnieuw downloaden",
"27a56aad79d8b61269ed303f11664cc78bcc2522": "Thema",
"ff7cee38a2259526c519f878e71b964f41db4348": "Standaard",
"7a6bacee4c31cb5c0ac2d24274fb4610d8858602": "Themawijziging toestaan",
"fe46ccaae902ce974e2441abe752399288298619": "Taal",
"ab2756805742e84ad0cc0468f4be2d8aa9f855a5": "Audiopad",
"c2c89cdf45d46ea64d2ed2f9ac15dfa4d77e26ca": "Het pad voor audiodownloads. Dit is relatief aan YTDL-Material's hoofdmap.",
"46826331da1949bd6fb74624447057099c9d20cd": "Videomap",
"17c92e6d47a213fa95b5aa344b3f258147123f93": "Het pad voor videodownloads. Dit is relatief aan YTDL-Material's hoofdmap.",
"cfe829634b1144bc44b6d38cf5584ea65db9804f": "Standaard bestandsuitvoer",
"1148fd45287ff09955b938756bc302042bcb29c7": "Dit pad is relatief aan bovenstaande downloadpaden. Laat de extensie achterwege.",
"ef418d4ece7c844f3a5e431da1aa59bedd88da7b": "Algemene aanvullende opties",
"6b995e7130b4d667eaab6c5f61b362ace486d26d": "Algemene aanvullende opties voor downloads op de overzichtspagina. Scheidt deze met komma's: ,,",
"04201f9d27abd7d6f58a4328ab98063ce1072006": "Categorieën",
"78e49b7339b4fa7184dd21bcaae107ce9b7076f6": "youtube-dl-archief gebruiken",
"ffc19f32b1cba0daefc0e5668f89346db1db83ad": "Miniatuurvoorbeeld opslaan",
"384de8f8f112c9e6092eb2698706d391553f3e8d": "Metagegevens opslaan",
"fb35145bfb84521e21b6385363d59221f436a573": "Alle downloads afbreken",
"61f8fd90b5f8cb20c70371feb2ee5e1fac5a9095": "Boventitel",
"78d3531417c0d4ba4c90f0d4ae741edc261ec8df": "Bestandsbeheer ingeschakeld",
"a5a1be0a5df07de9eec57f5d2a86ed0204b2e75a": "Downloadbeheer ingeschakeld",
"c33bd5392b39dbed36b8e5a1145163a15d45835f": "Kwaliteitskeuze toestaan",
"bda5508e24e0d77debb28bcd9194d8fefb1cfb92": "Downloadmodus",
"09d31c803a7252658694e1e3176b97f5655a3fe3": "Meerdere downloads toestaan",
"1c4dbce56d96b8974aac24a02f7ab2ee81415014": "Openbare api gebruiken",
"23bd81dcc30b74d06279a26d7a42e8901c1b124e": "Openbare api-sleutel",
"41016a73d8ad85e6cb26dffa0a8fab9fe8f60d8e": "Documentatie bekijken",
"00a94f58d9eb2e3aa561440eabea616d0c937fa2": "Let op: hiermee verwijder je je oude api-sleutel!",
"1b258b258b4cc475ceb2871305b61756b0134f4a": "Genereren",
"d5d7c61349f3b0859336066e6d453fc35d334fe5": "YouTube-api gebruiken",
"ce10d31febb3d9d60c160750570310f303a22c22": "YouTube-api-sleutel",
"8602e313cdfa7c4cc475ccbe86459fce3c3fd986": "Het genereren van een sleutel is eenvoudig.",
"d162f9fcd6a7187b391e004f072ab3da8377c47d": "Twitch-api gebruiken",
"8ae23bc4302a479f687f4b20a84c276182e2519c": "Twitch-api-sleutel",
"84ffcebac2709ca0785f4a1d5ba274433b5beabc": "Ook wel de client-id.",
"5fb1e0083c9b2a40ac8ae7dcb2618311c291b8b9": "Twitch-chatgesprekken automatisch downloaden",
"9b3cedfa83c6d7acb3210953289d1be4aab115c7": "Klik hier",
"7f09776373995003161235c0c8d02b7f91dbc4df": "om de officiële Chrome-extensie van YouTubeDL-Material te downloaden.",
"5b5296423906ab3371fdb2b5a5aaa83acaa2ee52": "Hiervoor dien je de extensie handmatig te laden en de frontend-url op te geven in de instellingen.",
"9a2ec6da48771128384887525bdcac992632c863": "om de officiële Firefox-extensie van YouTubeDL-Material te installeren.",
"eb81be6b49e195e5307811d1d08a19259d411f37": "Uitgebreide installatiehandleiding.",
"cb17ff8fe3961cf90f44bee97c88a3f3347a7e55": "Je hoeft alleen de frontend-url op te geven in de instellingen.",
"61b81b11aad0b9d970ece2fce18405f07eac69c2": "Sleep de link naar je bladwijzers en klaar is Kees! Ga vervolgens naar een YouTube-video en klik op de bladwijzer.",
"c505d6c5de63cc700f0aaf8a4b31fae9e18024e5": "Audio-bookmarklet genereren",
"ec71e08aee647ea4a71fd6b7510c54d84a797ca6": "Kies een downloader",
"5fab47f146b0a4b809dcebf3db9da94df6299ea1": "Standaard downloadagent gebruiken",
"c776eb4992b6c98f58cd89b20c1ea8ac37888521": "Kies een downloadagent",
"0c43af932e6a4ee85500e28f01b3538b4eb27bc4": "Logniveau",
"db6c192032f4cab809aad35215f0aa4765761897": "Inlogverloopdatum",
"dc3d990391c944d1fbfc7cfb402f7b5e112fb3a8": "Geavanceerd downloaden toestaan",
"431e5f3a0dde88768d1074baedd65266412b3f02": "Cookies gebruiken",
"80651a7ad1229ea6613557d3559f702cfa5aecf5": "Cookies instellen",
"37224420db54d4bc7696f157b779a7225f03ca9d": "Gebruikersregistratie toestaan",
"fa548cee6ea11c160a416cac3e6bdec0363883dc": "Authenticatiemethode",
"4f56ced9d6b85aeb1d4346433361d47ea72dac1a": "Intern",
"e3d7c5f019e79a3235a28ba24df24f11712c7627": "LDAP",
"1db9789b93069861019bd0ccaa5d4706b00afc61": "LDAP-url",
"f50fa6c09c8944aed504f6325f2913ee6c7a296a": "Bind DN",
"080cc6abcba236390fc22e79792d0d3443a3bd2a": "Bind-inloggegevens",
"cfa67d14d84fe0e9fadf251dc51ffc181173b662": "Zoekdatabank",
"e01d54ecc1a0fcf9525a3c100ed8b83d94e61c23": "Zoekfilter",
"cec82c0a545f37420d55a9b6c45c20546e82f94e": "Over YouTubeDL-Material",
"199c17e5d6a419313af3c325f06dcbb9645ca618": "is een opensource YouTube-downloader, gebouwd volgens Google's Material Design-specificaties. Je kunt naadloos je favoriete video's downloaden als audio- of videobestanden of abonneren op je favoriete kanalen of afspeellijsten om altijd de nieuwste video's binnen te halen.",
"bc0ad0ee6630acb7fcb7802ec79f5a0ee943c1a7": "bevat een aantal handige functies, zoals een uitgebreide api, Docker-ondersteuning en is volledig vertaalbaar. Meer functies zijn te vinden op onze GitHub-pagina (klik op het GitHub-pictogram).",
"a45e3b05f0529dc5246d70ef62304c94426d4c81": "Geïnstalleerde versie:",
"b33536f59b94ec935a16bd6869d836895dc5300c": "Heb je een bug aangetroffen of een idee?",
"e1f398f38ff1534303d4bb80bd6cece245f24016": "om een 'issue' te openen!",
"e22f3a5351944f3a1a10cfc7da6f65dfbe0037fe": "Bezig met controleren op updates...",
"a16e92385b4fd9677bb830a4b796b8b79c113290": "Update beschikbaar",
"189b28aaa19b3c51c6111ad039c4fd5e2a22e370": "Je kunt de update installeren via het instellingenmenu.",
"1372e61c5bd06100844bd43b98b016aabc468f62": "Kies een versie:",
"1f6d14a780a37a97899dc611881e6bc971268285": "Delen toestaan",
"6580b6a950d952df847cb3d8e7176720a740adc8": "Tijdstempel gebruiken",
"4f2ed9e71a7c981db3e50ae2fedb28aff2ec4e6c": "seconden",
"3a6e5a6aa78ca864f6542410c5dafb6334538106": "Kopiëren naar klembord",
"a249a5ae13e0835383885aaf697d2890cc3e53e9": "Afspeellijst delen",
"15da89490e04496ca9ea1e1b3d44fb5efd4a75d9": "Video delen",
"1d540dcd271b316545d070f9d182c372d923aadd": "Audio delen",
"a1ad8b1be9be43b5183bd2c3186d4e19496f2a0b": "Sessie-id:",
"b6c453e0e61faea184bbaf5c5b0a1e164f4de2a2": "Alle downloads wissen",
"eb98135e35af26a9a326ee69bd8ff104d36dd8ec": "(huidig)",
"7117fc42f860e86d983bfccfcf2654e5750f3406": "Geen downloads beschikbaar!",
"42ff677ec14f111e88bd6cdd30145378e994d1bf": "Mijn profiel",
"bb694b49d408265c91c62799c2b3a7e3151c824d": "Uitloggen",
"ac9d09de42edca1296371e4d801349c9096ac8de": "UID:",
"a5ed099ffc9e96f6970df843289ade8a7d20ab9f": "Aangemaakt:",
"fa96f2137af0a24e6d6d54c598c0af7d5d5ad344": "Je bent niet ingelogd.",
"a1dbca87b9f36d2b06a5cbcffb5814c4ae9b798a": "Beheerdersaccount aanmaken",
"2d2adf3ca26a676bca2269295b7455a26fd26980": "Er zijn geen beheerdersaccounts aangetroffen. Hiermee maak je een beheerdersaccount met wachtwoord aan - de gebruikersnaam is 'admin'.",
"70a67e04629f6d412db0a12d51820b480788d795": "Aanmaken",
"4d92a0395dd66778a931460118626c5794a3fc7a": "Gebruikers toevoegen",
"b0d7dd8a1b0349622d6e0c6e643e24a9ea0efa1d": "Rol aanpassen",
"746f64ddd9001ac456327cd9a3d5152203a4b93c": "Gebruikersnaam",
"52c1447c1ec9570a2a3025c7e566557b8d19ed92": "Rol",
"59a8c38db3091a63ac1cb9590188dc3a972acfb3": "Acties",
"2bd201aea09e43fbfd3cd15ec0499b6755302329": "Gebruiker beheren",
"95b95a9c79e4fd9ed41f6855e37b3b06af25bcab": "Gebruiker verwijderen",
"632e8b20c98e8eec4059a605a4b011bb476137af": "Gebruiker bewerken",
"29c97c8e76763bb15b6d515648fa5bd1eb0f7510": "Gebruikers-uid:",
"e70e209561583f360b1e9cefd2cbb1fe434b6229": "Nieuw wachtwoord",
"6498fa1b8f563988f769654a75411bb8060134b9": "Nieuw wachtwoord instellen",
"544e09cdc99a8978f48521d45f62db0da6dcf742": "Standaardrol gebruiken",
"4f20f2d5a6882190892e58b85f6ccbedfa737952": "Ja",
"3d3ae7deebc5949b0c1c78b9847886a94321d9fd": "Nee",
"57c6c05d8ebf4ef1180c2705033c044f655bb2c4": "Rol beheren",
"5009630cdf32ab4f1c78737b9617b8773512c05a": "Aantal regels:",
"8a0bda4c47f10b2423ff183acefbf70d4ab52ea2": "Logboeken wissen",
"24dc3ecf7ec2c2144910c4f3d38343828be03a4c": "Automatisch gegenereerd",
"ccf5ea825526ac490974336cb5c24352886abc07": "Bestand openen",
"5656a06f17c24b2d7eae9c221567b209743829a9": "Bestand openen op nieuw tabblad",
"a0720c36ee1057e5c54a86591b722485c62d7b1a": "Ga naar abonnement",
"94e01842dcee90531caa52e4147f70679bac87fe": "Verwijderen en opnieuw downloaden",
"2031adb51e07a41844e8ba7704b054e98345c9c1": "Permanent verwijderen",
"ddc31f2885b1b33a7651963254b0c197f2a64086": "Meer tonen.",
"56a2a773fbd5a6b9ac2e6b89d29d70a2ed0f3227": "Minder tonen.",
"2054791b822475aeaea95c0119113de3200f5e1c": "Duur:"
}

File diff suppressed because it is too large Load Diff

View File

@@ -10,7 +10,6 @@
<link href="https://fonts.googleapis.com/css?family=Roboto&display=swap" rel="stylesheet">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons|Material+Icons+Outlined" rel="stylesheet"/>
</head>
<body>
<app-root></app-root>

View File

@@ -1,5 +1,7 @@
/* You can add global styles to this file, and also import other style files */
@import '~material-icons/iconfont/material-icons.css';
@import '@angular/material/prebuilt-themes/indigo-pink.css';
//@import './app-theme';

View File

@@ -5,6 +5,7 @@ const THEMES_CONFIG = {
'alternate_color': 'gray',
'ghost_primary': '#f9f9f9',
'ghost_secondary': '#ecebeb',
'drawer_color': '#fafafa',
'css_label': 'default-theme',
'social_theme': 'material-light'
},
@@ -14,6 +15,7 @@ const THEMES_CONFIG = {
'alternate_color': '#695959',
'ghost_primary': '#444444',
'ghost_secondary': '#141414',
'drawer_color': '#303030',
'css_label': 'dark-theme',
'social_theme': 'material-dark'
},