Added ability to modify file metadata

improved-downloads-management
Isaac Abadi 3 years ago
parent 53a181e04d
commit 9cf8b87c6e

@ -129,6 +129,27 @@ paths:
description: User is not authorized to view the file.
security:
- Auth query parameter: []
/api/updateFile:
post:
tags:
- files
summary: Updates file database object
description: Updates a file db object using its uid and a change object.
operationId: post-updateFile
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateFileRequest'
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/SuccessObject'
security:
- Auth query parameter: []
/api/enableSharing:
post:
tags:
@ -1509,6 +1530,8 @@ components:
properties:
success:
type: boolean
error:
type: string
FileType:
type: string
enum:
@ -1738,6 +1761,18 @@ components:
type: boolean
file:
$ref: '#/components/schemas/DatabaseFile'
UpdateFileRequest:
required:
- uid
- change_obj
type: object
properties:
uid:
type: string
description: Video UID
change_obj:
type: object
description: Object with fields to update as keys and their new values
SharingToggle:
required:
- uid
@ -2321,6 +2356,9 @@ components:
type: string
thumbnailURL:
type: string
description: Backup if thumbnailPath is not defined
thumbnailPath:
type: string
isAudio:
type: boolean
duration:
@ -2332,6 +2370,7 @@ components:
type: string
size:
type: number
description: In bytes
path:
type: string
upload_date:
@ -2340,6 +2379,12 @@ components:
type: string
sharingEnabled:
type: boolean
category:
$ref: '#/components/schemas/Category'
view_count:
type: number
local_view_count:
type: number
Playlist:
required:
- uids
@ -2369,6 +2414,8 @@ components:
type: number
user_uid:
type: string
auto:
type: boolean
Download:
required:
- url

@ -950,6 +950,24 @@ app.post('/api/getAllFiles', optionalJwt, async function (req, res) {
});
});
app.post('/api/updateFile', optionalJwt, async function (req, res) {
const uid = req.body.uid;
const change_obj = req.body.change_obj;
const file = await db_api.updateRecord('files', {uid: uid}, change_obj);
if (!file) {
res.send({
success: false,
error: 'File could not be found'
});
} else {
res.send({
success: true
});
}
});
app.post('/api/checkConcurrentStream', async (req, res) => {
const uid = req.body.uid;

@ -100,6 +100,7 @@ export type { UpdateCategoriesRequest } from './models/UpdateCategoriesRequest';
export type { UpdateCategoryRequest } from './models/UpdateCategoryRequest';
export type { UpdateConcurrentStreamRequest } from './models/UpdateConcurrentStreamRequest';
export type { UpdateConcurrentStreamResponse } from './models/UpdateConcurrentStreamResponse';
export type { UpdateFileRequest } from './models/UpdateFileRequest';
export type { UpdatePlaylistRequest } from './models/UpdatePlaylistRequest';
export type { UpdaterStatus } from './models/UpdaterStatus';
export type { UpdateServerRequest } from './models/UpdateServerRequest';

@ -2,10 +2,16 @@
/* tslint:disable */
/* eslint-disable */
import type { Category } from './Category';
export type DatabaseFile = {
id: string;
title: string;
/**
* Backup if thumbnailPath is not defined
*/
thumbnailURL: string;
thumbnailPath?: string;
isAudio: boolean;
/**
* In seconds
@ -13,9 +19,15 @@ export type DatabaseFile = {
duration: number;
url: string;
uploader: string;
/**
* In bytes
*/
size: number;
path: string;
upload_date: string;
uid: string;
sharingEnabled?: boolean;
category?: Category;
view_count?: number;
local_view_count?: number;
};

@ -4,4 +4,5 @@
export type SuccessObject = {
success: boolean;
error?: string;
};

@ -0,0 +1,14 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type UpdateFileRequest = {
/**
* Video UID
*/
uid: string;
/**
* Object with fields to update as keys and their new values
*/
change_obj: any;
};

@ -1,7 +1,7 @@
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NgModule, LOCALE_ID } from '@angular/core';
import { registerLocaleData, CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { registerLocaleData, CommonModule, DatePipe } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
import { MatButtonToggleModule } from '@angular/material/button-toggle';
import { MatCardModule } from '@angular/material/card';
@ -189,7 +189,8 @@ registerLocaleData(es, 'es');
],
providers: [
PostsService,
{ provide: HTTP_INTERCEPTORS, useClass: H401Interceptor, multi: true }
{ provide: HTTP_INTERCEPTORS, useClass: H401Interceptor, multi: true },
DatePipe
],
exports: [
HighlightPipe,

@ -1,36 +1,64 @@
<h4 mat-dialog-title>{{file.title}}</h4>
<mat-dialog-content>
<div class="info-item">
<div class="info-item-label"><strong><ng-container i18n="Video name property">Name:</ng-container>&nbsp;</strong></div>
<div class="info-item-value">{{file.title}}</div>
</div>
<div class="info-item">
<div class="info-item-label"><strong><ng-container i18n="Video URL property">URL:</ng-container>&nbsp;</strong></div>
<div class="info-item-value"><a target="_blank" [href]="file.url">{{file.url}}</a></div>
</div>
<div class="info-item">
<div class="info-item-label"><strong><ng-container i18n="Video ID property">Uploader:</ng-container>&nbsp;</strong></div>
<div class="info-item-value">{{file.uploader ? file.uploader : 'N/A'}}</div>
<div style="width: 100%; position: relative;">
<button style="position: absolute; right: 16px; top: 8px;" mat-icon-button (click)="editing = !editing"><mat-icon>edit</mat-icon></button>
</div>
<mat-form-field class="info-field">
<input [(ngModel)]="new_file.title" matInput placeholder="Name" i18n-placeholder="Name" [disabled]="!editing">
</mat-form-field>
<mat-form-field class="info-field">
<input [(ngModel)]="new_file.url" matInput placeholder="URL" i18n-placeholder="URL" [disabled]="!editing">
<button mat-icon-button matSuffix (click)="window.open(new_file.url, '_blank')">
<mat-icon>link</mat-icon>
</button>
</mat-form-field>
<mat-form-field class="info-field">
<input [(ngModel)]="new_file.uploader" matInput placeholder="Uploader" i18n-placeholder="Uploader" [disabled]="!editing">
</mat-form-field>
<mat-form-field class="info-field">
<mat-label i18n="Upload date">Upload date</mat-label>
<input [value]="upload_date" matInput [matDatepicker]="picker" (dateChange)="uploadDateChanged($event)" [disabled]="!editing">
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker>
</mat-form-field>
<mat-form-field class="info-field">
<input [(ngModel)]="new_file.thumbnailPath" matInput placeholder="Thumbnail path" i18n-placeholder="Thumbnail path" [disabled]="!editing">
</mat-form-field>
<mat-form-field class="info-field">
<input [(ngModel)]="new_file.thumbnailURL" matInput placeholder="Thumbnail URL" i18n-placeholder="Thumbnail URL" [disabled]="!editing || new_file.thumbnailPath">
</mat-form-field>
<mat-form-field class="info-field">
<mat-select placeholder="Category" i18n-placeholder="Category" [value]="category" (valueChange)="categoryChanged($event)" [compareWith]="categoryComparisonFunction" [disabled]="!editing">
<mat-option [value]="{}">
N/A
</mat-option>
<mat-option *ngFor="let available_category of postsService.categories | keyvalue" [value]="available_category">
{{available_category.value.name}}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="info-field">
<input type="number" [(ngModel)]="new_file.view_count" matInput placeholder="View count" i18n-placeholder="View count" [disabled]="!editing">
</mat-form-field>
<mat-form-field class="info-field">
<input type="number" [(ngModel)]="new_file.local_view_count" matInput placeholder="Local view count" i18n-placeholder="Local view count" [disabled]="!editing">
</mat-form-field>
<mat-divider style="margin-bottom: 16px;"></mat-divider>
<div class="info-item">
<div class="info-item-label"><strong><ng-container i18n="Video file size property">File size:</ng-container>&nbsp;</strong></div>
<div class="info-item-value">{{file.size ? filesize(file.size) : 'N/A'}}</div>
<div class="info-item-value">{{new_file.size ? filesize(new_file.size) : 'N/A'}}</div>
</div>
<div class="info-item">
<div class="info-item-label"><strong><ng-container i18n="Video path property">Path:</ng-container>&nbsp;</strong></div>
<div class="info-item-value">{{file.path ? file.path : 'N/A'}}</div>
</div>
<div class="info-item">
<div class="info-item-label"><strong><ng-container i18n="Video upload date property">Upload Date:</ng-container>&nbsp;</strong></div>
<div class="info-item-value">{{file.upload_date ? file.upload_date : 'N/A'}}</div>
</div>
<div class="info-item">
<div class="info-item-label"><strong><ng-container i18n="Category property">Category:</ng-container>&nbsp;</strong></div>
<div class="info-item-value"><ng-container *ngIf="file.category"><mat-chip-list><mat-chip>{{file.category.name}}</mat-chip></mat-chip-list></ng-container><ng-container *ngIf="!file.category">N/A</ng-container></div>
<div class="info-item-value">{{new_file.path ? new_file.path : 'N/A'}}</div>
</div>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button mat-dialog-close><ng-container i18n="Close subscription info button">Close</ng-container></button>
<button mat-button mat-dialog-close><ng-container i18n="Close video info button">Close</ng-container></button>
<button mat-button [disabled]="!metadataChanged()" (click)="saveChanges()"><ng-container i18n="Save video info button">Save</ng-container></button>
</mat-dialog-actions>

@ -17,6 +17,10 @@
vertical-align: top;
}
.info-field {
width: 90%;
}
.a-wrap {
word-wrap: break-word
}

@ -1,6 +1,9 @@
import { Component, OnInit, Inject } from '@angular/core';
import filesize from 'filesize';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { PostsService } from 'app/posts.services';
import { Category, DatabaseFile } from 'api-types';
import { DatePipe } from '@angular/common';
@Component({
selector: 'app-video-info-dialog',
@ -8,15 +11,75 @@ import { MAT_DIALOG_DATA } from '@angular/material/dialog';
styleUrls: ['./video-info-dialog.component.scss']
})
export class VideoInfoDialogComponent implements OnInit {
file: any;
file: DatabaseFile;
new_file: DatabaseFile;
filesize;
constructor(@Inject(MAT_DIALOG_DATA) public data: any) { }
window = window;
upload_date: Date;
category: Category;
editing = false;
constructor(@Inject(MAT_DIALOG_DATA) public data: any, public postsService: PostsService, private datePipe: DatePipe) { }
ngOnInit(): void {
this.filesize = filesize;
if (this.data) {
this.file = this.data.file;
this.initializeFile(this.data.file);
}
this.postsService.reloadCategories();
}
initializeFile(file: DatabaseFile): void {
this.file = file;
this.new_file = JSON.parse(JSON.stringify(file));
// use UTC for the date picker. not the cleanest approach but it allows it to match the upload date
this.upload_date = new Date(this.new_file.upload_date);
this.upload_date.setMinutes( this.upload_date.getMinutes() + this.upload_date.getTimezoneOffset() );
this.category = this.file.category ? this.category : {};
// we need to align whether missing category is null or undefined. this line helps with that.
if (!this.file.category) { this.new_file.category = null; this.file.category = null; }
}
saveChanges(): void {
const change_obj = {};
const keys = Object.keys(this.file);
keys.forEach(key => {
if (this.file[key] !== this.new_file[key]) change_obj[key] = this.new_file[key];
});
this.postsService.updateFile(this.file.uid, change_obj).subscribe(res => {
this.getFile();
});
}
getFile(): void {
this.postsService.getFile(this.file.uid).subscribe(res => {
this.file = res['file'];
this.initializeFile(this.file);
});
}
uploadDateChanged(event): void {
this.new_file.upload_date = this.datePipe.transform(event.value, 'yyyy-MM-dd');
}
categoryChanged(event): void {
this.new_file.category = Object.keys(event).length ? {uid: event.uid, name: event.name} : null;
}
categoryComparisonFunction(option: Category, value: Category): boolean {
// can't access properties of null/undefined values, prehandle these
if (!option && !value) return true;
else if (!option || !value) return false;
return option.uid === value.uid;
}
metadataChanged(): boolean {
return JSON.stringify(this.file) !== JSON.stringify(this.new_file);
}
}

@ -70,7 +70,7 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
@ViewChild('twitchchat') twitchChat: TwitchChatComponent;
@HostListener('window:resize', ['$event'])
onResize(event) {
onResize(): void {
this.innerWidth = window.innerWidth;
}
@ -98,12 +98,12 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
}
}
ngAfterViewInit() {
ngAfterViewInit(): void {
this.cdr.detectChanges();
this.postsService.sidenav.close();
}
ngOnDestroy() {
ngOnDestroy(): void {
// prevents volume save feature from running in the background
clearInterval(this.save_volume_timer);
}
@ -112,7 +112,7 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
public snackBar: MatSnackBar, private cdr: ChangeDetectorRef) {
}
processConfig() {
processConfig(): void {
this.baseStreamPath = this.postsService.path;
this.audioFolderPath = this.postsService.config['Downloader']['path-audio'];
this.videoFolderPath = this.postsService.config['Downloader']['path-video'];
@ -143,14 +143,14 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
}
}
getFile() {
this.postsService.getFile(this.uid, null, this.uuid).subscribe(res => {
getFile(): void {
this.postsService.getFile(this.uid, this.uuid).subscribe(res => {
this.db_file = res['file'];
if (!this.db_file) {
this.openSnackBar('Failed to get file information from the server.', 'Dismiss');
this.postsService.openSnackBar('Failed to get file information from the server.', 'Dismiss');
return;
}
this.postsService.incrementViewCount(this.db_file['uid'], null, this.uuid).subscribe(res => {}, err => {
this.postsService.incrementViewCount(this.db_file['uid'], null, this.uuid).subscribe(() => undefined, err => {
console.error('Failed to increment view count');
console.error(err);
});
@ -161,19 +161,19 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
});
}
getSubscription() {
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();
}, err => {
this.openSnackBar(`Failed to find subscription ${this.sub_id}`, 'Dismiss');
}, () => {
this.postsService.openSnackBar(`Failed to find subscription ${this.sub_id}`, 'Dismiss');
});
}
getPlaylistFiles() {
getPlaylistFiles(): void {
this.postsService.getPlaylist(this.playlist_id, this.uuid, true).subscribe(res => {
if (res['playlist']) {
this.db_playlist = res['playlist'];
@ -183,14 +183,14 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
this.show_player = true;
this.parseFileNames();
} else {
this.openSnackBar('Failed to load playlist!', '');
this.postsService.openSnackBar('Failed to load playlist!', '');
}
}, err => {
this.openSnackBar('Failed to load playlist!', '');
}, () => {
this.postsService.openSnackBar('Failed to load playlist!', '');
});
}
parseFileNames() {
parseFileNames(): void {
this.playlist = [];
for (let i = 0; i < this.uids.length; i++) {
let file_obj = null;
@ -204,7 +204,7 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
const mime_type = file_obj.isAudio ? 'audio/mp3' : 'video/mp4'
let baseLocation = 'stream/';
const baseLocation = 'stream/';
let fullLocation = this.baseStreamPath + baseLocation + `?test=test&uid=${file_obj['uid']}`;
if (this.postsService.isLoggedIn) {
@ -238,7 +238,7 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
this.show_player = true;
}
onPlayerReady(api: VgApiService) {
onPlayerReady(api: VgApiService): void {
this.api = api;
this.api_ready = true;
this.cdr.detectChanges();
@ -258,14 +258,14 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
}
}
saveVolume(api) {
saveVolume(api: VgApiService): void {
if (this.original_volume !== api.volume) {
localStorage.setItem('player_volume', api.volume)
this.original_volume = api.volume;
}
}
nextVideo() {
nextVideo(): void {
if (this.currentIndex === this.playlist.length - 1) {
// dont continue playing
// this.currentIndex = 0;
@ -276,17 +276,16 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
this.currentItem = this.playlist[ this.currentIndex ];
}
playVideo() {
playVideo(): void {
this.api.play();
}
onClickPlaylistItem(item: IMedia, index: number) {
// console.log('new current item is ' + item.title + ' at index ' + index);
onClickPlaylistItem(item: IMedia, index: number): void {
this.currentIndex = index;
this.currentItem = item;
}
getFileNames() {
getFileNames(): string[] {
const fileNames = [];
for (let i = 0; i < this.playlist.length; i++) {
fileNames.push(this.playlist[i].title);
@ -294,11 +293,11 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
return fileNames;
}
decodeURI(string) {
return decodeURI(string);
decodeURI(uri: string): string {
return decodeURI(uri);
}
downloadContent() {
downloadContent(): void {
const zipName = this.db_playlist.name;
this.downloading = true;
this.postsService.downloadPlaylistFromServer(this.playlist_id, this.uuid).subscribe(res => {
@ -311,7 +310,7 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
});
}
downloadFile() {
downloadFile(): void {
const filename = this.playlist[0].title;
const ext = (this.playlist[0].type === 'audio/mp3') ? '.mp3' : '.mp4';
this.downloading = true;
@ -325,22 +324,22 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
});
}
playlistPostCreationHandler(playlistID) {
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[]>) {
drop(event: CdkDragDrop<string[]>): void {
moveItemInArray(this.playlist, event.previousIndex, event.currentIndex);
}
playlistChanged() {
playlistChanged(): boolean {
return JSON.stringify(this.playlist) !== this.original_playlist;
}
openShareDialog() {
openShareDialog(): void {
const dialogRef = this.dialog.open(ShareMediaDialogComponent, {
data: {
uid: this.playlist_id ? this.playlist_id : this.uid,
@ -361,7 +360,7 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
});
}
openFileInfoDialog() {
openFileInfoDialog(): void {
this.dialog.open(VideoInfoDialogComponent, {
data: {
file: this.db_file,
@ -370,11 +369,11 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
})
}
setPlaybackTimestamp(time) {
setPlaybackTimestamp(time: number): void {
this.api.seekTime(time);
}
togglePlayback(to_play) {
togglePlayback(to_play: boolean): void {
if (to_play) {
this.api.play();
} else {
@ -382,22 +381,14 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
}
}
setPlaybackRate(speed) {
setPlaybackRate(speed: number): void {
this.api.playbackRate = speed;
}
shuffleArray(array) {
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]];
}
}
// snackbar helper
public openSnackBar(message: string, action: string) {
this.snackBar.open(message, action, {
duration: 2000,
});
}
}

@ -95,7 +95,9 @@ import {
UpdateTaskDataRequest,
RestoreDBBackupRequest,
Schedule,
ClearDownloadsRequest
ClearDownloadsRequest,
Category,
UpdateFileRequest
} from '../api-types';
import { isoLangs } from './settings/locales_list';
import { Title } from '@angular/platform-browser';
@ -145,7 +147,7 @@ export class PostsService implements CanActivate {
// global vars
config = null;
subscriptions = null;
categories = null;
categories: Category[] = null;
sidenav = null;
locale = isoLangs['en'];
version_info = null;
@ -348,8 +350,8 @@ export class PostsService implements CanActivate {
return this.http.get<GetMp4sResponse>(this.path + 'getMp4s', this.httpOptions);
}
getFile(uid: string, type: FileType, uuid: string = null) {
const body: GetFileRequest = {uid: uid, type: type, uuid: uuid};
getFile(uid: string, uuid: string = null) {
const body: GetFileRequest = {uid: uid, uuid: uuid};
return this.http.post<GetFileResponse>(this.path + 'getFile', body, this.httpOptions);
}
@ -357,6 +359,11 @@ export class PostsService implements CanActivate {
return this.http.post<GetAllFilesResponse>(this.path + 'getAllFiles', {sort: sort, range: range, text_search: text_search, file_type_filter: file_type_filter}, this.httpOptions);
}
updateFile(uid: string, change_obj: Object) {
const body: UpdateFileRequest = {uid: uid, change_obj: change_obj};
return this.http.post<SuccessObject>(this.path + 'updateFile', body, this.httpOptions);
}
downloadFileFromServer(uid: string, uuid: string = null) {
const body: DownloadFileRequest = {
uid: uid,

Loading…
Cancel
Save