diff --git a/Public API v1.yaml b/Public API v1.yaml index c37ef59..f554398 100644 --- a/Public API v1.yaml +++ b/Public API v1.yaml @@ -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 diff --git a/backend/app.js b/backend/app.js index 4ea7a3d..50c6bb9 100644 --- a/backend/app.js +++ b/backend/app.js @@ -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; diff --git a/src/api-types/index.ts b/src/api-types/index.ts index 22733c1..db5be04 100644 --- a/src/api-types/index.ts +++ b/src/api-types/index.ts @@ -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'; diff --git a/src/api-types/models/DatabaseFile.ts b/src/api-types/models/DatabaseFile.ts index ed5cd72..796c2fe 100644 --- a/src/api-types/models/DatabaseFile.ts +++ b/src/api-types/models/DatabaseFile.ts @@ -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; }; \ No newline at end of file diff --git a/src/api-types/models/SuccessObject.ts b/src/api-types/models/SuccessObject.ts index 2410019..9cfdf8b 100644 --- a/src/api-types/models/SuccessObject.ts +++ b/src/api-types/models/SuccessObject.ts @@ -4,4 +4,5 @@ export type SuccessObject = { success: boolean; + error?: string; }; \ No newline at end of file diff --git a/src/api-types/models/UpdateFileRequest.ts b/src/api-types/models/UpdateFileRequest.ts new file mode 100644 index 0000000..cada2e5 --- /dev/null +++ b/src/api-types/models/UpdateFileRequest.ts @@ -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; +}; \ No newline at end of file diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 0e9bf59..a9e2ad4 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -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, diff --git a/src/app/dialogs/video-info-dialog/video-info-dialog.component.html b/src/app/dialogs/video-info-dialog/video-info-dialog.component.html index e62d1c9..c9c0cc9 100644 --- a/src/app/dialogs/video-info-dialog/video-info-dialog.component.html +++ b/src/app/dialogs/video-info-dialog/video-info-dialog.component.html @@ -1,36 +1,64 @@

{{file.title}}

-
-
Name: 
-
{{file.title}}
-
-
-
URL: 
- -
-
-
Uploader: 
-
{{file.uploader ? file.uploader : 'N/A'}}
+
+
+ + + + + + + + + + + + Upload date + + + + + + + + + + + + + + N/A + + + {{available_category.value.name}} + + + + + + + + + + + +
File size: 
-
{{file.size ? filesize(file.size) : 'N/A'}}
+
{{new_file.size ? filesize(new_file.size) : 'N/A'}}
Path: 
-
{{file.path ? file.path : 'N/A'}}
-
-
-
Upload Date: 
-
{{file.upload_date ? file.upload_date : 'N/A'}}
-
-
-
Category: 
-
{{file.category.name}}N/A
+
{{new_file.path ? new_file.path : 'N/A'}}
+ - + + \ No newline at end of file diff --git a/src/app/dialogs/video-info-dialog/video-info-dialog.component.scss b/src/app/dialogs/video-info-dialog/video-info-dialog.component.scss index e90fa27..96d2ace 100644 --- a/src/app/dialogs/video-info-dialog/video-info-dialog.component.scss +++ b/src/app/dialogs/video-info-dialog/video-info-dialog.component.scss @@ -17,6 +17,10 @@ vertical-align: top; } +.info-field { + width: 90%; +} + .a-wrap { word-wrap: break-word } \ No newline at end of file diff --git a/src/app/dialogs/video-info-dialog/video-info-dialog.component.ts b/src/app/dialogs/video-info-dialog/video-info-dialog.component.ts index 4bfc8e3..8524bca 100644 --- a/src/app/dialogs/video-info-dialog/video-info-dialog.component.ts +++ b/src/app/dialogs/video-info-dialog/video-info-dialog.component.ts @@ -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); } } diff --git a/src/app/player/player.component.ts b/src/app/player/player.component.ts index 9c82d1a..a0676e4 100644 --- a/src/app/player/player.component.ts +++ b/src/app/player/player.component.ts @@ -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) { + drop(event: CdkDragDrop): 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, - }); - } - } diff --git a/src/app/posts.services.ts b/src/app/posts.services.ts index d2d9046..c384f33 100644 --- a/src/app/posts.services.ts +++ b/src/app/posts.services.ts @@ -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(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(this.path + 'getFile', body, this.httpOptions); } @@ -357,6 +359,11 @@ export class PostsService implements CanActivate { return this.http.post(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(this.path + 'updateFile', body, this.httpOptions); + } + downloadFileFromServer(uid: string, uuid: string = null) { const body: DownloadFileRequest = { uid: uid,