mirror of
https://github.com/Tzahi12345/YoutubeDL-Material.git
synced 2026-03-12 07:40:57 +03:00
* Fixed download spinner in player component * Downloads UI is more mobile friendly (#905) * Code cleanup * Fixed size of actions in home screen downloads * Errored downloads now display their stage as "Error" in the UI * Moved personal settings from about dialog to profile dialog * Profile dialog can now be opened without logging in/without multi-user mode * Fixed issue where archive dialog could be accessed from anywhere * Misc internationalization improvements * Combined download stage and download progress columns * Added back loading spinner to download actions * Adjusted thresholds for consolidating download action buttons * Implemented virtual scrolling for notifications (helps if many notifications exist) * Fixed minor console error
412 lines
12 KiB
TypeScript
412 lines
12 KiB
TypeScript
import { Component, OnInit, HostListener, 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';
|
|
import { MatDialog } from '@angular/material/dialog';
|
|
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
|
import { ShareMediaDialogComponent } from '../dialogs/share-media-dialog/share-media-dialog.component';
|
|
import { DatabaseFile, FileType, Playlist } from '../../api-types';
|
|
import { TwitchChatComponent } from 'app/components/twitch-chat/twitch-chat.component';
|
|
import { VideoInfoDialogComponent } from 'app/dialogs/video-info-dialog/video-info-dialog.component';
|
|
|
|
export interface IMedia {
|
|
title: string;
|
|
src: string;
|
|
type: string;
|
|
label: string;
|
|
url: string;
|
|
uid?: string;
|
|
}
|
|
|
|
@Component({
|
|
selector: 'app-player',
|
|
templateUrl: './player.component.html',
|
|
styleUrls: ['./player.component.css']
|
|
})
|
|
export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
|
|
|
|
playlist: Array<IMedia> = [];
|
|
original_playlist: string = null;
|
|
playlist_updating = false;
|
|
|
|
show_player = false;
|
|
|
|
currentIndex = 0;
|
|
currentItem: IMedia = null;
|
|
api: VgApiService;
|
|
api_ready = false;
|
|
|
|
// params
|
|
uids: string[];
|
|
type: FileType;
|
|
playlist_id = null; // used for playlists (not subscription)
|
|
file_objs: DatabaseFile[] = []; // used for playlists
|
|
uid = null; // used for non-subscription files (audio, video, playlist)
|
|
subscription = 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;
|
|
auto = null;
|
|
|
|
db_playlist: Playlist = null;
|
|
db_file: DatabaseFile = null;
|
|
|
|
baseStreamPath = null;
|
|
audioFolderPath = null;
|
|
videoFolderPath = null;
|
|
subscriptionFolderPath = null;
|
|
|
|
// url-mode params
|
|
url = null;
|
|
name = null;
|
|
|
|
downloading = false;
|
|
|
|
save_volume_timer = null;
|
|
original_volume = null;
|
|
|
|
@ViewChild('twitchchat') twitchChat: TwitchChatComponent;
|
|
|
|
ngOnInit(): void {
|
|
this.playlist_id = this.route.snapshot.paramMap.get('playlist_id');
|
|
this.uid = this.route.snapshot.paramMap.get('uid');
|
|
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) {
|
|
this.processConfig();
|
|
} else {
|
|
this.postsService.service_initialized.subscribe(init => { // loads settings
|
|
if (init) {
|
|
this.processConfig();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
ngAfterViewInit(): void {
|
|
this.cdr.detectChanges();
|
|
this.postsService.sidenav.close();
|
|
}
|
|
|
|
ngOnDestroy(): void {
|
|
// prevents volume save feature from running in the background
|
|
clearInterval(this.save_volume_timer);
|
|
}
|
|
|
|
constructor(public postsService: PostsService, private route: ActivatedRoute, private dialog: MatDialog, private router: Router,
|
|
private cdr: ChangeDetectorRef) {
|
|
|
|
}
|
|
processConfig(): void {
|
|
this.baseStreamPath = this.postsService.path;
|
|
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'];
|
|
|
|
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
|
|
this.playlist = [];
|
|
const imedia: IMedia = {
|
|
title: this.name,
|
|
label: this.name,
|
|
src: this.url,
|
|
type: 'video/mp4',
|
|
url: this.url,
|
|
uid: this.uid
|
|
}
|
|
this.playlist.push(imedia);
|
|
this.updateCurrentItem(this.playlist[0], 0);
|
|
this.show_player = true;
|
|
}
|
|
}
|
|
|
|
getFile(): void {
|
|
this.postsService.getFile(this.uid, this.uuid).subscribe(res => {
|
|
this.db_file = res['file'];
|
|
if (!this.db_file) {
|
|
this.postsService.openSnackBar($localize`Failed to get file information from the server.`, 'Dismiss');
|
|
return;
|
|
}
|
|
this.postsService.incrementViewCount(this.db_file['uid'], null, this.uuid).subscribe(() => undefined, err => {
|
|
console.error('Failed to increment view count');
|
|
console.error(err);
|
|
});
|
|
// regular video/audio file (not playlist)
|
|
this.uids = [this.db_file['uid']];
|
|
this.type = this.db_file['isAudio'] ? 'audio' as FileType : 'video' as FileType;
|
|
this.parseFileNames();
|
|
});
|
|
}
|
|
|
|
getSubscription(): void {
|
|
this.postsService.getSubscription(this.sub_id).subscribe(res => {
|
|
const subscription = res['subscription'];
|
|
this.subscription = subscription;
|
|
this.type === this.subscription.type;
|
|
this.uids = this.subscription.videos.map(video => video['uid']);
|
|
this.parseFileNames();
|
|
}, () => {
|
|
// TODO: Make translatable
|
|
this.postsService.openSnackBar(`Failed to find subscription ${this.sub_id}`, 'Dismiss');
|
|
});
|
|
}
|
|
|
|
getPlaylistFiles(): void {
|
|
this.postsService.getPlaylist(this.playlist_id, this.uuid, true).subscribe(res => {
|
|
if (res['playlist']) {
|
|
this.db_playlist = res['playlist'];
|
|
this.file_objs = res['file_objs'];
|
|
this.uids = this.db_playlist.uids;
|
|
this.type = res['type'];
|
|
this.show_player = true;
|
|
this.parseFileNames();
|
|
} else {
|
|
this.postsService.openSnackBar($localize`Failed to load playlist!`);
|
|
}
|
|
}, () => {
|
|
this.postsService.openSnackBar($localize`Failed to load playlist!`);
|
|
});
|
|
}
|
|
|
|
parseFileNames(): void {
|
|
this.playlist = [];
|
|
for (let i = 0; i < this.uids.length; i++) {
|
|
let file_obj = null;
|
|
if (this.playlist_id) {
|
|
file_obj = this.file_objs[i];
|
|
} else if (this.sub_id) {
|
|
file_obj = this.subscription['videos'][i];
|
|
} else {
|
|
file_obj = this.db_file;
|
|
}
|
|
|
|
const mime_type = file_obj.isAudio ? 'audio/mp3' : 'video/mp4'
|
|
|
|
const baseLocation = 'stream/';
|
|
let fullLocation = this.baseStreamPath + baseLocation + `?test=test&uid=${file_obj['uid']}`;
|
|
|
|
if (this.postsService.isLoggedIn) {
|
|
fullLocation += `&jwt=${this.postsService.token}`;
|
|
}
|
|
|
|
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: file_obj['title'],
|
|
src: fullLocation,
|
|
type: mime_type,
|
|
label: file_obj['title'],
|
|
url: file_obj['url'],
|
|
uid: file_obj['uid']
|
|
}
|
|
this.playlist.push(mediaObject);
|
|
}
|
|
if (this.db_playlist && this.db_playlist['randomize_order']) {
|
|
this.shuffleArray(this.playlist);
|
|
}
|
|
this.currentItem = this.playlist[this.currentIndex];
|
|
this.original_playlist = JSON.stringify(this.playlist);
|
|
this.show_player = true;
|
|
}
|
|
|
|
onPlayerReady(api: VgApiService): void {
|
|
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')) {
|
|
this.api.volume = parseFloat(localStorage.getItem('player_volume'));
|
|
}
|
|
|
|
this.save_volume_timer = setInterval(() => this.saveVolume(this.api), 2000)
|
|
|
|
this.api.getDefaultMedia().subscriptions.loadedMetadata.subscribe(this.playVideo.bind(this));
|
|
this.api.getDefaultMedia().subscriptions.ended.subscribe(this.nextVideo.bind(this));
|
|
|
|
if (this.timestamp) {
|
|
this.api.seekTime(+this.timestamp);
|
|
}
|
|
}
|
|
|
|
saveVolume(api: VgApiService): void {
|
|
if (this.original_volume !== api.volume) {
|
|
localStorage.setItem('player_volume', api.volume)
|
|
this.original_volume = api.volume;
|
|
}
|
|
}
|
|
|
|
nextVideo(): void {
|
|
if (this.currentIndex === this.playlist.length - 1) {
|
|
// dont continue playing
|
|
// this.currentIndex = 0;
|
|
return;
|
|
}
|
|
|
|
this.updateCurrentItem(this.playlist[this.currentIndex], ++this.currentIndex);
|
|
}
|
|
|
|
updateCurrentItem(newCurrentItem: IMedia, newCurrentIndex: number) {
|
|
this.currentItem = newCurrentItem;
|
|
this.currentIndex = newCurrentIndex;
|
|
}
|
|
|
|
playVideo(): void {
|
|
this.api.play();
|
|
}
|
|
|
|
onClickPlaylistItem(item: IMedia, index: number): void {
|
|
this.currentIndex = index;
|
|
this.currentItem = item;
|
|
this.updateCurrentItem(this.currentItem, this.currentIndex);
|
|
}
|
|
|
|
getFileNames(): string[] {
|
|
const fileNames = [];
|
|
for (let i = 0; i < this.playlist.length; i++) {
|
|
fileNames.push(this.playlist[i].title);
|
|
}
|
|
return fileNames;
|
|
}
|
|
|
|
decodeURI(uri: string): string {
|
|
return decodeURI(uri);
|
|
}
|
|
|
|
downloadContent(): void {
|
|
const zipName = this.db_playlist.name;
|
|
this.downloading = true;
|
|
this.postsService.downloadPlaylistFromServer(this.playlist_id, this.uuid).subscribe(res => {
|
|
this.downloading = false;
|
|
const blob: Blob = res;
|
|
saveAs(blob, zipName + '.zip');
|
|
}, err => {
|
|
console.error(err);
|
|
this.downloading = false;
|
|
});
|
|
}
|
|
|
|
downloadFile(): void {
|
|
const filename = this.playlist[0].title;
|
|
const ext = (this.playlist[0].type === 'audio/mp3') ? '.mp3' : '.mp4';
|
|
this.downloading = true;
|
|
this.postsService.downloadFileFromServer(this.uid, this.uuid).subscribe(res => {
|
|
this.downloading = false;
|
|
const blob: Blob = res;
|
|
saveAs(blob, filename + ext);
|
|
}, err => {
|
|
console.error(err);
|
|
this.downloading = false;
|
|
});
|
|
}
|
|
|
|
playlistPostCreationHandler(playlistID: string): void {
|
|
// changes the route without moving from the current view or
|
|
// triggering a navigation event
|
|
this.playlist_id = playlistID;
|
|
this.router.navigateByUrl(this.router.url + ';id=' + playlistID);
|
|
}
|
|
|
|
drop(event: CdkDragDrop<string[]>): void {
|
|
moveItemInArray(this.playlist, event.previousIndex, event.currentIndex);
|
|
}
|
|
|
|
playlistChanged(): boolean {
|
|
return JSON.stringify(this.playlist) !== this.original_playlist;
|
|
}
|
|
|
|
openShareDialog(): void {
|
|
const dialogRef = this.dialog.open(ShareMediaDialogComponent, {
|
|
data: {
|
|
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 : this.uuid,
|
|
current_timestamp: this.api.time.current
|
|
},
|
|
width: '60vw'
|
|
});
|
|
|
|
dialogRef.afterClosed().subscribe(() => {
|
|
if (!this.playlist_id) {
|
|
this.getFile();
|
|
} else {
|
|
this.getPlaylistFiles();
|
|
}
|
|
});
|
|
}
|
|
|
|
openFileInfoDialog(): void {
|
|
let file_obj = this.db_file;
|
|
const original_uid = this.currentItem.uid;
|
|
if (this.db_playlist) {
|
|
const idx = this.getPlaylistFileIndexUID(original_uid);
|
|
file_obj = this.file_objs[idx];
|
|
}
|
|
const dialogRef = this.dialog.open(VideoInfoDialogComponent, {
|
|
data: {
|
|
file: file_obj,
|
|
},
|
|
minWidth: '50vw'
|
|
});
|
|
|
|
dialogRef.afterClosed().subscribe(() => {
|
|
if (this.db_file) this.db_file = dialogRef.componentInstance.file;
|
|
else if (this.db_playlist) {
|
|
const idx = this.getPlaylistFileIndexUID(original_uid);
|
|
this.file_objs[idx] = dialogRef.componentInstance.file;
|
|
}
|
|
});
|
|
}
|
|
|
|
getPlaylistFileIndexUID(uid: string): number {
|
|
return this.file_objs.findIndex(file_obj => file_obj['uid'] === uid);
|
|
}
|
|
|
|
setPlaybackTimestamp(time: number): void {
|
|
this.api.seekTime(time);
|
|
}
|
|
|
|
togglePlayback(to_play: boolean): void {
|
|
if (to_play) {
|
|
this.api.play();
|
|
} else {
|
|
this.api.pause();
|
|
}
|
|
}
|
|
|
|
setPlaybackRate(speed: number): void {
|
|
this.api.playbackRate = speed;
|
|
}
|
|
|
|
shuffleArray(array: unknown[]): void {
|
|
for (let i = array.length - 1; i > 0; i--) {
|
|
const j = Math.floor(Math.random() * (i + 1));
|
|
[array[i], array[j]] = [array[j], array[i]];
|
|
}
|
|
}
|
|
}
|