From fd45e8f43c2638478599ca75632518054461da85 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 31 Oct 2017 11:52:52 +0100 Subject: [PATCH] Add video privacy setting --- client/src/app/app.component.ts | 6 +- client/src/app/core/menu/menu.component.html | 5 + client/src/app/core/server/server.service.ts | 14 ++- .../app/shared/forms/form-validators/video.ts | 7 ++ .../app/shared/search/search-field.type.ts | 2 +- .../app/shared/search/search.component.html | 4 +- .../src/app/shared/search/search.component.ts | 17 ++- .../+video-edit/video-add.component.html | 12 ++ .../videos/+video-edit/video-add.component.ts | 10 +- .../+video-edit/video-update.component.html | 12 ++ .../+video-edit/video-update.component.ts | 20 +++- .../+video-watch/video-watch.component.html | 11 +- .../app/videos/shared/video-details.model.ts | 7 +- .../src/app/videos/shared/video-edit.model.ts | 6 +- client/src/app/videos/shared/video.service.ts | 55 +++++---- client/src/app/videos/video-list/index.ts | 5 +- .../videos/video-list/my-videos.component.ts | 36 ++++++ .../abstract-video-list.html} | 0 .../abstract-video-list.scss} | 0 .../video-list/shared/abstract-video-list.ts | 104 ++++++++++++++++++ .../src/app/videos/video-list/shared/index.ts | 4 + .../{ => shared}/loader.component.html | 0 .../{ => shared}/loader.component.ts | 0 .../video-miniature.component.html | 0 .../video-miniature.component.scss | 0 .../{ => shared}/video-miniature.component.ts | 4 +- .../{ => shared}/video-sort.component.html | 0 .../{ => shared}/video-sort.component.ts | 2 +- .../videos/video-list/video-list.component.ts | 102 ++++------------- .../src/app/videos/videos-routing.module.ts | 11 +- client/src/app/videos/videos.module.ts | 9 +- client/src/sass/video-js-custom.scss | 59 ++-------- server/controllers/api/remote/videos.ts | 4 +- server/controllers/api/users.ts | 21 +++- server/controllers/api/videos/index.ts | 28 ++++- server/helpers/custom-validators/videos.ts | 12 ++ server/initializers/constants.ts | 10 +- .../migrations/0095-videos-privacy.ts | 35 ++++++ server/middlewares/validators/videos.ts | 19 +++- server/models/video/video-interface.ts | 3 + server/models/video/video.ts | 78 ++++++++++--- .../remote-video-create-request.model.ts | 1 + .../remote-video-update-request.model.ts | 1 + shared/models/videos/index.ts | 1 + shared/models/videos/video-create.model.ts | 3 + shared/models/videos/video-privacy.enum.ts | 5 + shared/models/videos/video-update.model.ts | 3 + shared/models/videos/video.model.ts | 5 +- 48 files changed, 545 insertions(+), 208 deletions(-) create mode 100644 client/src/app/videos/video-list/my-videos.component.ts rename client/src/app/videos/video-list/{video-list.component.html => shared/abstract-video-list.html} (100%) rename client/src/app/videos/video-list/{video-list.component.scss => shared/abstract-video-list.scss} (100%) create mode 100644 client/src/app/videos/video-list/shared/abstract-video-list.ts create mode 100644 client/src/app/videos/video-list/shared/index.ts rename client/src/app/videos/video-list/{ => shared}/loader.component.html (100%) rename client/src/app/videos/video-list/{ => shared}/loader.component.ts (100%) rename client/src/app/videos/video-list/{ => shared}/video-miniature.component.html (100%) rename client/src/app/videos/video-list/{ => shared}/video-miniature.component.scss (100%) rename client/src/app/videos/video-list/{ => shared}/video-miniature.component.ts (82%) rename client/src/app/videos/video-list/{ => shared}/video-sort.component.html (100%) rename client/src/app/videos/video-list/{ => shared}/video-sort.component.ts (95%) create mode 100644 server/initializers/migrations/0095-videos-privacy.ts create mode 100644 shared/models/videos/video-privacy.enum.ts diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts index 984470d69..bef1599fc 100644 --- a/client/src/app/app.component.ts +++ b/client/src/app/app.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, ViewContainerRef } from '@angular/core' +import { Component, OnInit } from '@angular/core' import { Router } from '@angular/router' import { AuthService, ServerService } from './core' @@ -28,8 +28,7 @@ export class AppComponent implements OnInit { constructor ( private router: Router, private authService: AuthService, - private serverService: ServerService, - private userService: UserService + private serverService: ServerService ) {} ngOnInit () { @@ -45,6 +44,7 @@ export class AppComponent implements OnInit { this.serverService.loadVideoCategories() this.serverService.loadVideoLanguages() this.serverService.loadVideoLicences() + this.serverService.loadVideoPrivacies() // Do not display menu on small screens if (window.innerWidth < 600) { diff --git a/client/src/app/core/menu/menu.component.html b/client/src/app/core/menu/menu.component.html index 2d8aace54..fcde23fdd 100644 --- a/client/src/app/core/menu/menu.component.html +++ b/client/src/app/core/menu/menu.component.html @@ -23,6 +23,11 @@ My account + + + + My videos +
diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts index ae507afce..cbc4074c9 100644 --- a/client/src/app/core/server/server.service.ts +++ b/client/src/app/core/server/server.service.ts @@ -19,6 +19,7 @@ export class ServerService { private videoCategories: Array<{ id: number, label: string }> = [] private videoLicences: Array<{ id: number, label: string }> = [] private videoLanguages: Array<{ id: number, label: string }> = [] + private videoPrivacies: Array<{ id: number, label: string }> = [] constructor (private http: HttpClient) {} @@ -39,6 +40,10 @@ export class ServerService { return this.loadVideoAttributeEnum('languages', this.videoLanguages) } + loadVideoPrivacies () { + return this.loadVideoAttributeEnum('privacies', this.videoPrivacies) + } + getConfig () { return this.config } @@ -55,7 +60,14 @@ export class ServerService { return this.videoLanguages } - private loadVideoAttributeEnum (attributeName: 'categories' | 'licences' | 'languages', hashToPopulate: { id: number, label: string }[]) { + getVideoPrivacies () { + return this.videoPrivacies + } + + private loadVideoAttributeEnum ( + attributeName: 'categories' | 'licences' | 'languages' | 'privacies', + hashToPopulate: { id: number, label: string }[] + ) { return this.http.get(ServerService.BASE_VIDEO_URL + attributeName) .subscribe(data => { Object.keys(data) diff --git a/client/src/app/shared/forms/form-validators/video.ts b/client/src/app/shared/forms/form-validators/video.ts index 434773501..65f11f5da 100644 --- a/client/src/app/shared/forms/form-validators/video.ts +++ b/client/src/app/shared/forms/form-validators/video.ts @@ -9,6 +9,13 @@ export const VIDEO_NAME = { } } +export const VIDEO_PRIVACY = { + VALIDATORS: [ Validators.required ], + MESSAGES: { + 'required': 'Video privacy is required.' + } +} + export const VIDEO_CATEGORY = { VALIDATORS: [ Validators.required ], MESSAGES: { diff --git a/client/src/app/shared/search/search-field.type.ts b/client/src/app/shared/search/search-field.type.ts index 63557898a..ff0bb8de1 100644 --- a/client/src/app/shared/search/search-field.type.ts +++ b/client/src/app/shared/search/search-field.type.ts @@ -1 +1 @@ -export type SearchField = 'name' | 'author' | 'host' | 'magnetUri' | 'tags' +export type SearchField = 'name' | 'author' | 'host' | 'tags' diff --git a/client/src/app/shared/search/search.component.html b/client/src/app/shared/search/search.component.html index c6c6ff6a8..0302447d0 100644 --- a/client/src/app/shared/search/search.component.html +++ b/client/src/app/shared/search/search.component.html @@ -6,12 +6,12 @@
+
+ + + +
+ {{ formErrors.privacy }} +
+
+
+
+ + + +
+ {{ formErrors.privacy }} +
+
+
{ this.video = new VideoEdit(video) + // We cannot set private a video that was not private anymore + if (video.privacy !== VideoPrivacy.PRIVATE) { + const newVideoPrivacies = [] + for (const p of this.videoPrivacies) { + if (p.id !== VideoPrivacy.PRIVATE) newVideoPrivacies.push(p) + } + + this.videoPrivacies = newVideoPrivacies + } + this.hydrateFormFromVideo() }, diff --git a/client/src/app/videos/+video-watch/video-watch.component.html b/client/src/app/videos/+video-watch/video-watch.component.html index 71f986ccd..53648a8d8 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.html +++ b/client/src/app/videos/+video-watch/video-watch.component.html @@ -22,7 +22,7 @@
Video not found :'(
- +
Download: {{ downloadSpeed | bytes }}/s
Upload: {{ uploadSpeed | bytes }}/s
@@ -142,6 +142,15 @@
+
+ + Privacy: + + + {{ video.privacyLabel }} + +
+
Category: diff --git a/client/src/app/videos/shared/video-details.model.ts b/client/src/app/videos/shared/video-details.model.ts index 68ded5210..84f96a25f 100644 --- a/client/src/app/videos/shared/video-details.model.ts +++ b/client/src/app/videos/shared/video-details.model.ts @@ -5,7 +5,8 @@ import { VideoFile, VideoChannel, VideoResolution, - UserRight + UserRight, + VideoPrivacy } from '../../../../../shared' export class VideoDetails extends Video implements VideoDetailsServerModel { @@ -41,10 +42,14 @@ export class VideoDetails extends Video implements VideoDetailsServerModel { descriptionPath: string files: VideoFile[] channel: VideoChannel + privacy: VideoPrivacy + privacyLabel: string constructor (hash: VideoDetailsServerModel) { super(hash) + this.privacy = hash.privacy + this.privacyLabel = hash.privacyLabel this.descriptionPath = hash.descriptionPath this.files = hash.files this.channel = hash.channel diff --git a/client/src/app/videos/shared/video-edit.model.ts b/client/src/app/videos/shared/video-edit.model.ts index e0b7bf130..88d23a59f 100644 --- a/client/src/app/videos/shared/video-edit.model.ts +++ b/client/src/app/videos/shared/video-edit.model.ts @@ -1,4 +1,5 @@ import { VideoDetails } from './video-details.model' +import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy.enum' export class VideoEdit { category: number @@ -9,6 +10,7 @@ export class VideoEdit { tags: string[] nsfw: boolean channel: number + privacy: VideoPrivacy uuid?: string id?: number @@ -23,6 +25,7 @@ export class VideoEdit { this.tags = videoDetails.tags this.nsfw = videoDetails.nsfw this.channel = videoDetails.channel.id + this.privacy = videoDetails.privacy } patch (values: Object) { @@ -40,7 +43,8 @@ export class VideoEdit { name: this.name, tags: this.tags, nsfw: this.nsfw, - channel: this.channel + channel: this.channel, + privacy: this.privacy } } } diff --git a/client/src/app/videos/shared/video.service.ts b/client/src/app/videos/shared/video.service.ts index 7d5372334..8459aa0d3 100644 --- a/client/src/app/videos/shared/video.service.ts +++ b/client/src/app/videos/shared/video.service.ts @@ -19,7 +19,6 @@ import { UserVideoRate, VideoRateType, VideoUpdate, - VideoAbuseCreate, UserVideoRateUpdate, Video as VideoServerModel, VideoDetails as VideoDetailsServerModel, @@ -51,6 +50,7 @@ export class VideoService { licence: video.licence, language, description: video.description, + privacy: video.privacy, tags: video.tags, nsfw: video.nsfw } @@ -63,22 +63,35 @@ export class VideoService { uploadVideo (video: FormData) { const req = new HttpRequest('POST', VideoService.BASE_VIDEO_URL + 'upload', video, { reportProgress: true }) - return this.authHttp.request(req) - .catch(this.restExtractor.handleError) + return this.authHttp + .request(req) + .catch(this.restExtractor.handleError) } - getVideos (videoPagination: VideoPagination, sort: SortField) { + getMyVideos (videoPagination: VideoPagination, sort: SortField): Observable<{ videos: Video[], totalVideos: number}> { const pagination = this.videoPaginationToRestPagination(videoPagination) let params = new HttpParams() params = this.restService.addRestGetParams(params, pagination, sort) - return this.authHttp.get(VideoService.BASE_VIDEO_URL, { params }) - .map(this.extractVideos) - .catch((res) => this.restExtractor.handleError(res)) + return this.authHttp.get(UserService.BASE_USERS_URL + '/me/videos', { params }) + .map(this.extractVideos) + .catch((res) => this.restExtractor.handleError(res)) } - searchVideos (search: Search, videoPagination: VideoPagination, sort: SortField) { + getVideos (videoPagination: VideoPagination, sort: SortField): Observable<{ videos: Video[], totalVideos: number}> { + const pagination = this.videoPaginationToRestPagination(videoPagination) + + let params = new HttpParams() + params = this.restService.addRestGetParams(params, pagination, sort) + + return this.authHttp + .get(VideoService.BASE_VIDEO_URL, { params }) + .map(this.extractVideos) + .catch((res) => this.restExtractor.handleError(res)) + } + + searchVideos (search: Search, videoPagination: VideoPagination, sort: SortField): Observable<{ videos: Video[], totalVideos: number}> { const url = VideoService.BASE_VIDEO_URL + 'search/' + encodeURIComponent(search.value) const pagination = this.videoPaginationToRestPagination(videoPagination) @@ -88,15 +101,17 @@ export class VideoService { if (search.field) params.set('field', search.field) - return this.authHttp.get>(url, { params }) - .map(this.extractVideos) - .catch((res) => this.restExtractor.handleError(res)) + return this.authHttp + .get>(url, { params }) + .map(this.extractVideos) + .catch((res) => this.restExtractor.handleError(res)) } removeVideo (id: number) { - return this.authHttp.delete(VideoService.BASE_VIDEO_URL + id) - .map(this.restExtractor.extractDataBool) - .catch((res) => this.restExtractor.handleError(res)) + return this.authHttp + .delete(VideoService.BASE_VIDEO_URL + id) + .map(this.restExtractor.extractDataBool) + .catch((res) => this.restExtractor.handleError(res)) } loadCompleteDescription (descriptionPath: string) { @@ -117,8 +132,9 @@ export class VideoService { getUserVideoRating (id: number): Observable { const url = UserService.BASE_USERS_URL + 'me/videos/' + id + '/rating' - return this.authHttp.get(url) - .catch(res => this.restExtractor.handleError(res)) + return this.authHttp + .get(url) + .catch(res => this.restExtractor.handleError(res)) } private videoPaginationToRestPagination (videoPagination: VideoPagination) { @@ -134,9 +150,10 @@ export class VideoService { rating: rateType } - return this.authHttp.put(url, body) - .map(this.restExtractor.extractDataBool) - .catch(res => this.restExtractor.handleError(res)) + return this.authHttp + .put(url, body) + .map(this.restExtractor.extractDataBool) + .catch(res => this.restExtractor.handleError(res)) } private extractVideos (result: ResultList) { diff --git a/client/src/app/videos/video-list/index.ts b/client/src/app/videos/video-list/index.ts index a490e6bb5..ed2bb1657 100644 --- a/client/src/app/videos/video-list/index.ts +++ b/client/src/app/videos/video-list/index.ts @@ -1,4 +1,3 @@ -export * from './loader.component' +export * from './my-videos.component' export * from './video-list.component' -export * from './video-miniature.component' -export * from './video-sort.component' +export * from './shared' diff --git a/client/src/app/videos/video-list/my-videos.component.ts b/client/src/app/videos/video-list/my-videos.component.ts new file mode 100644 index 000000000..648741a40 --- /dev/null +++ b/client/src/app/videos/video-list/my-videos.component.ts @@ -0,0 +1,36 @@ +import { Component, OnDestroy, OnInit } from '@angular/core' +import { ActivatedRoute, Router } from '@angular/router' + +import { NotificationsService } from 'angular2-notifications' + +import { AbstractVideoList } from './shared' +import { VideoService } from '../shared' + +@Component({ + selector: 'my-videos', + styleUrls: [ './shared/abstract-video-list.scss' ], + templateUrl: './shared/abstract-video-list.html' +}) +export class MyVideosComponent extends AbstractVideoList implements OnInit, OnDestroy { + + constructor ( + protected router: Router, + protected route: ActivatedRoute, + protected notificationsService: NotificationsService, + private videoService: VideoService + ) { + super() + } + + ngOnInit () { + super.ngOnInit() + } + + ngOnDestroy () { + this.subActivatedRoute.unsubscribe() + } + + getVideosObservable () { + return this.videoService.getMyVideos(this.pagination, this.sort) + } +} diff --git a/client/src/app/videos/video-list/video-list.component.html b/client/src/app/videos/video-list/shared/abstract-video-list.html similarity index 100% rename from client/src/app/videos/video-list/video-list.component.html rename to client/src/app/videos/video-list/shared/abstract-video-list.html diff --git a/client/src/app/videos/video-list/video-list.component.scss b/client/src/app/videos/video-list/shared/abstract-video-list.scss similarity index 100% rename from client/src/app/videos/video-list/video-list.component.scss rename to client/src/app/videos/video-list/shared/abstract-video-list.scss diff --git a/client/src/app/videos/video-list/shared/abstract-video-list.ts b/client/src/app/videos/video-list/shared/abstract-video-list.ts new file mode 100644 index 000000000..87d5bc48a --- /dev/null +++ b/client/src/app/videos/video-list/shared/abstract-video-list.ts @@ -0,0 +1,104 @@ +import { OnDestroy, OnInit } from '@angular/core' +import { ActivatedRoute, Router } from '@angular/router' +import { Subscription } from 'rxjs/Subscription' +import { BehaviorSubject } from 'rxjs/BehaviorSubject' +import { Observable } from 'rxjs/Observable' + +import { NotificationsService } from 'angular2-notifications' + +import { + SortField, + Video, + VideoPagination +} from '../../shared' + +export abstract class AbstractVideoList implements OnInit, OnDestroy { + loading: BehaviorSubject = new BehaviorSubject(false) + pagination: VideoPagination = { + currentPage: 1, + itemsPerPage: 25, + totalItems: null + } + sort: SortField + videos: Video[] = [] + + protected notificationsService: NotificationsService + protected router: Router + protected route: ActivatedRoute + + protected subActivatedRoute: Subscription + + abstract getVideosObservable (): Observable<{ videos: Video[], totalVideos: number}> + + ngOnInit () { + // Subscribe to route changes + this.subActivatedRoute = this.route.params.subscribe(routeParams => { + this.loadRouteParams(routeParams) + + this.getVideos() + }) + } + + ngOnDestroy () { + this.subActivatedRoute.unsubscribe() + } + + getVideos () { + this.loading.next(true) + this.videos = [] + + const observable = this.getVideosObservable() + + observable.subscribe( + ({ videos, totalVideos }) => { + this.videos = videos + this.pagination.totalItems = totalVideos + + this.loading.next(false) + }, + error => this.notificationsService.error('Error', error.text) + ) + } + + isThereNoVideo () { + return !this.loading.getValue() && this.videos.length === 0 + } + + onPageChanged (event: { page: number }) { + // Be sure the current page is set + this.pagination.currentPage = event.page + + this.navigateToNewParams() + } + + onSort (sort: SortField) { + this.sort = sort + + this.navigateToNewParams() + } + + protected buildRouteParams () { + // There is always a sort and a current page + const params = { + sort: this.sort, + page: this.pagination.currentPage + } + + return params + } + + protected loadRouteParams (routeParams: { [ key: string ]: any }) { + this.sort = routeParams['sort'] as SortField || '-createdAt' + + if (routeParams['page'] !== undefined) { + this.pagination.currentPage = parseInt(routeParams['page'], 10) + } else { + this.pagination.currentPage = 1 + } + } + + protected navigateToNewParams () { + const routeParams = this.buildRouteParams() + this.router.navigate([ '/videos/list', routeParams ]) + } +} diff --git a/client/src/app/videos/video-list/shared/index.ts b/client/src/app/videos/video-list/shared/index.ts new file mode 100644 index 000000000..2c9804e6d --- /dev/null +++ b/client/src/app/videos/video-list/shared/index.ts @@ -0,0 +1,4 @@ +export * from './abstract-video-list' +export * from './loader.component' +export * from './video-miniature.component' +export * from './video-sort.component' diff --git a/client/src/app/videos/video-list/loader.component.html b/client/src/app/videos/video-list/shared/loader.component.html similarity index 100% rename from client/src/app/videos/video-list/loader.component.html rename to client/src/app/videos/video-list/shared/loader.component.html diff --git a/client/src/app/videos/video-list/loader.component.ts b/client/src/app/videos/video-list/shared/loader.component.ts similarity index 100% rename from client/src/app/videos/video-list/loader.component.ts rename to client/src/app/videos/video-list/shared/loader.component.ts diff --git a/client/src/app/videos/video-list/video-miniature.component.html b/client/src/app/videos/video-list/shared/video-miniature.component.html similarity index 100% rename from client/src/app/videos/video-list/video-miniature.component.html rename to client/src/app/videos/video-list/shared/video-miniature.component.html diff --git a/client/src/app/videos/video-list/video-miniature.component.scss b/client/src/app/videos/video-list/shared/video-miniature.component.scss similarity index 100% rename from client/src/app/videos/video-list/video-miniature.component.scss rename to client/src/app/videos/video-list/shared/video-miniature.component.scss diff --git a/client/src/app/videos/video-list/video-miniature.component.ts b/client/src/app/videos/video-list/shared/video-miniature.component.ts similarity index 82% rename from client/src/app/videos/video-list/video-miniature.component.ts rename to client/src/app/videos/video-list/shared/video-miniature.component.ts index 18434dad2..e5a87907b 100644 --- a/client/src/app/videos/video-list/video-miniature.component.ts +++ b/client/src/app/videos/video-list/shared/video-miniature.component.ts @@ -1,7 +1,7 @@ import { Component, Input } from '@angular/core' -import { SortField, Video } from '../shared' -import { User } from '../../shared' +import { SortField, Video } from '../../shared' +import { User } from '../../../shared' @Component({ selector: 'my-video-miniature', diff --git a/client/src/app/videos/video-list/video-sort.component.html b/client/src/app/videos/video-list/shared/video-sort.component.html similarity index 100% rename from client/src/app/videos/video-list/video-sort.component.html rename to client/src/app/videos/video-list/shared/video-sort.component.html diff --git a/client/src/app/videos/video-list/video-sort.component.ts b/client/src/app/videos/video-list/shared/video-sort.component.ts similarity index 95% rename from client/src/app/videos/video-list/video-sort.component.ts rename to client/src/app/videos/video-list/shared/video-sort.component.ts index 64916bf16..8aa89d32b 100644 --- a/client/src/app/videos/video-list/video-sort.component.ts +++ b/client/src/app/videos/video-list/shared/video-sort.component.ts @@ -1,6 +1,6 @@ import { Component, EventEmitter, Input, Output } from '@angular/core' -import { SortField } from '../shared' +import { SortField } from '../../shared' @Component({ selector: 'my-video-sort', diff --git a/client/src/app/videos/video-list/video-list.component.ts b/client/src/app/videos/video-list/video-list.component.ts index bf6f60215..784162679 100644 --- a/client/src/app/videos/video-list/video-list.component.ts +++ b/client/src/app/videos/video-list/video-list.component.ts @@ -1,51 +1,33 @@ import { Component, OnDestroy, OnInit } from '@angular/core' import { ActivatedRoute, Router } from '@angular/router' import { Subscription } from 'rxjs/Subscription' -import { BehaviorSubject } from 'rxjs/BehaviorSubject' import { NotificationsService } from 'angular2-notifications' -import { AuthService } from '../../core' -import { - SortField, - Video, - VideoService, - VideoPagination -} from '../shared' -import { Search, SearchField, SearchService, User } from '../../shared' +import { VideoService } from '../shared' +import { Search, SearchField, SearchService } from '../../shared' +import { AbstractVideoList } from './shared' @Component({ selector: 'my-videos-list', - styleUrls: [ './video-list.component.scss' ], - templateUrl: './video-list.component.html' + styleUrls: [ './shared/abstract-video-list.scss' ], + templateUrl: './shared/abstract-video-list.html' }) -export class VideoListComponent implements OnInit, OnDestroy { - loading: BehaviorSubject = new BehaviorSubject(false) - pagination: VideoPagination = { - currentPage: 1, - itemsPerPage: 25, - totalItems: null - } - sort: SortField - user: User - videos: Video[] = [] - +export class VideoListComponent extends AbstractVideoList implements OnInit, OnDestroy { private search: Search - private subActivatedRoute: Subscription private subSearch: Subscription constructor ( - private authService: AuthService, - private notificationsService: NotificationsService, - private router: Router, - private route: ActivatedRoute, + protected router: Router, + protected route: ActivatedRoute, + protected notificationsService: NotificationsService, private videoService: VideoService, private searchService: SearchService - ) {} + ) { + super() + } ngOnInit () { - this.user = this.authService.getUser() - // Subscribe to route changes this.subActivatedRoute = this.route.params.subscribe(routeParams => { this.loadRouteParams(routeParams) @@ -66,14 +48,12 @@ export class VideoListComponent implements OnInit, OnDestroy { } ngOnDestroy () { - this.subActivatedRoute.unsubscribe() + super.ngOnDestroy() + this.subSearch.unsubscribe() } - getVideos () { - this.loading.next(true) - this.videos = [] - + getVideosObservable () { let observable = null if (this.search.value) { observable = this.videoService.searchVideos(this.search, this.pagination, this.sort) @@ -81,40 +61,11 @@ export class VideoListComponent implements OnInit, OnDestroy { observable = this.videoService.getVideos(this.pagination, this.sort) } - observable.subscribe( - ({ videos, totalVideos }) => { - this.videos = videos - this.pagination.totalItems = totalVideos - - this.loading.next(false) - }, - error => this.notificationsService.error('Error', error.text) - ) + return observable } - isThereNoVideo () { - return !this.loading.getValue() && this.videos.length === 0 - } - - onPageChanged (event: { page: number }) { - // Be sure the current page is set - this.pagination.currentPage = event.page - - this.navigateToNewParams() - } - - onSort (sort: SortField) { - this.sort = sort - - this.navigateToNewParams() - } - - private buildRouteParams () { - // There is always a sort and a current page - const params = { - sort: this.sort, - page: this.pagination.currentPage - } + protected buildRouteParams () { + const params = super.buildRouteParams() // Maybe there is a search if (this.search.value) { @@ -125,7 +76,9 @@ export class VideoListComponent implements OnInit, OnDestroy { return params } - private loadRouteParams (routeParams: { [ key: string ]: any }) { + protected loadRouteParams (routeParams: { [ key: string ]: any }) { + super.loadRouteParams(routeParams) + if (routeParams['search'] !== undefined) { this.search = { value: routeParams['search'], @@ -137,18 +90,5 @@ export class VideoListComponent implements OnInit, OnDestroy { field: 'name' } } - - this.sort = routeParams['sort'] as SortField || '-createdAt' - - if (routeParams['page'] !== undefined) { - this.pagination.currentPage = parseInt(routeParams['page'], 10) - } else { - this.pagination.currentPage = 1 - } - } - - private navigateToNewParams () { - const routeParams = this.buildRouteParams() - this.router.navigate([ '/videos/list', routeParams ]) } } diff --git a/client/src/app/videos/videos-routing.module.ts b/client/src/app/videos/videos-routing.module.ts index d3869748b..3ca3e5486 100644 --- a/client/src/app/videos/videos-routing.module.ts +++ b/client/src/app/videos/videos-routing.module.ts @@ -3,7 +3,7 @@ import { RouterModule, Routes } from '@angular/router' import { MetaGuard } from '@ngx-meta/core' -import { VideoListComponent } from './video-list' +import { VideoListComponent, MyVideosComponent } from './video-list' import { VideosComponent } from './videos.component' const videosRoutes: Routes = [ @@ -12,6 +12,15 @@ const videosRoutes: Routes = [ component: VideosComponent, canActivateChild: [ MetaGuard ], children: [ + { + path: 'mine', + component: MyVideosComponent, + data: { + meta: { + title: 'My videos' + } + } + }, { path: 'list', component: VideoListComponent, diff --git a/client/src/app/videos/videos.module.ts b/client/src/app/videos/videos.module.ts index 3a0c3feac..ecc351b65 100644 --- a/client/src/app/videos/videos.module.ts +++ b/client/src/app/videos/videos.module.ts @@ -2,7 +2,13 @@ import { NgModule } from '@angular/core' import { VideosRoutingModule } from './videos-routing.module' import { VideosComponent } from './videos.component' -import { LoaderComponent, VideoListComponent, VideoMiniatureComponent, VideoSortComponent } from './video-list' +import { + LoaderComponent, + VideoListComponent, + MyVideosComponent, + VideoMiniatureComponent, + VideoSortComponent +} from './video-list' import { VideoService } from './shared' import { SharedModule } from '../shared' @@ -16,6 +22,7 @@ import { SharedModule } from '../shared' VideosComponent, VideoListComponent, + MyVideosComponent, VideoMiniatureComponent, VideoSortComponent, diff --git a/client/src/sass/video-js-custom.scss b/client/src/sass/video-js-custom.scss index c5f668f17..6ad21988e 100644 --- a/client/src/sass/video-js-custom.scss +++ b/client/src/sass/video-js-custom.scss @@ -334,71 +334,34 @@ $slider-bg-color: lighten($primary-background-color, 33%); // Thanks: https://projects.lukehaas.me/css-loaders/ .vjs-loading-spinner { - border: none; - opacity: 1; + margin: -25px 0 0 -25px; + position: absolute; + top: 50%; + left: 50%; font-size: 10px; - text-indent: -9999em; - width: 5em; - height: 5em; - border-radius: 50%; - background: #ffffff; - background: -moz-linear-gradient(left, #ffffff 10%, rgba(255, 255, 255, 0) 42%); - background: -webkit-linear-gradient(left, #ffffff 10%, rgba(255, 255, 255, 0) 42%); - background: -o-linear-gradient(left, #ffffff 10%, rgba(255, 255, 255, 0) 42%); - background: -ms-linear-gradient(left, #ffffff 10%, rgba(255, 255, 255, 0) 42%); - background: linear-gradient(to right, #ffffff 10%, rgba(255, 255, 255, 0) 42%); position: relative; - -webkit-animation: load3 1.4s infinite linear; - animation: load3 1.4s infinite linear; - -webkit-transform: translateZ(0); - -ms-transform: translateZ(0); + text-indent: -9999em; + border: 0.7em solid rgba(255, 255, 255, 0.2); + border-left-color: #ffffff; transform: translateZ(0); + animation: spinner 1.4s infinite linear; &:before { - width: 50%; - height: 50%; - background: #ffffff; - border-radius: 100% 0 0 0; - position: absolute; - top: 0; - left: 0; - content: ''; animation: none !important; - margin: 0 !important; } &:after { - background: #000; - width: 75%; - height: 75%; border-radius: 50%; - content: ''; - margin: auto; - position: absolute; - top: 0; - left: 0; - bottom: 0; - right: 0; + width: 6em; + height: 6em; animation: none !important; } - @-webkit-keyframes load3 { + @keyframes spinner { 0% { - -webkit-transform: rotate(0deg); transform: rotate(0deg); } 100% { - -webkit-transform: rotate(360deg); - transform: rotate(360deg); - } - } - @keyframes load3 { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(360deg); transform: rotate(360deg); } } diff --git a/server/controllers/api/remote/videos.ts b/server/controllers/api/remote/videos.ts index 3ecc62ada..cba47f0a1 100644 --- a/server/controllers/api/remote/videos.ts +++ b/server/controllers/api/remote/videos.ts @@ -267,7 +267,8 @@ async function addRemoteVideo (videoToCreateData: RemoteVideoCreateData, fromPod views: videoToCreateData.views, likes: videoToCreateData.likes, dislikes: videoToCreateData.dislikes, - remote: true + remote: true, + privacy: videoToCreateData.privacy } const video = db.Video.build(videoData) @@ -334,6 +335,7 @@ async function updateRemoteVideo (videoAttributesToUpdate: RemoteVideoUpdateData videoInstance.set('views', videoAttributesToUpdate.views) videoInstance.set('likes', videoAttributesToUpdate.likes) videoInstance.set('dislikes', videoAttributesToUpdate.dislikes) + videoInstance.set('privacy', videoAttributesToUpdate.privacy) await videoInstance.save(sequelizeOptions) diff --git a/server/controllers/api/users.ts b/server/controllers/api/users.ts index fdc9b0c87..dcd407fdf 100644 --- a/server/controllers/api/users.ts +++ b/server/controllers/api/users.ts @@ -30,6 +30,8 @@ import { } from '../../../shared' import { createUserAuthorAndChannel } from '../../lib' import { UserInstance } from '../../models' +import { videosSortValidator } from '../../middlewares/validators/sort' +import { setVideosSort } from '../../middlewares/sort' const usersRouter = express.Router() @@ -38,6 +40,15 @@ usersRouter.get('/me', asyncMiddleware(getUserInformation) ) +usersRouter.get('/me/videos', + authenticate, + paginationValidator, + videosSortValidator, + setVideosSort, + setPagination, + asyncMiddleware(getUserVideos) +) + usersRouter.get('/me/videos/:videoId/rating', authenticate, usersVideoRatingValidator, @@ -101,6 +112,13 @@ export { // --------------------------------------------------------------------------- +async function getUserVideos (req: express.Request, res: express.Response, next: express.NextFunction) { + const user = res.locals.oauth.token.User + const resultList = await db.Video.listUserVideosForApi(user.id ,req.query.start, req.query.count, req.query.sort) + + return res.json(getFormattedObjects(resultList.data, resultList.total)) +} + async function createUserRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) { const options = { arguments: [ req, res ], @@ -146,13 +164,14 @@ async function registerUser (req: express.Request, res: express.Response, next: } async function getUserInformation (req: express.Request, res: express.Response, next: express.NextFunction) { + // We did not load channels in res.locals.user const user = await db.User.loadByUsernameAndPopulateChannels(res.locals.oauth.token.user.username) return res.json(user.toFormattedJSON()) } function getUser (req: express.Request, res: express.Response, next: express.NextFunction) { - return res.json(res.locals.user.toFormattedJSON()) + return res.json(res.locals.oauth.token.User.toFormattedJSON()) } async function getUserVideoRating (req: express.Request, res: express.Response, next: express.NextFunction) { diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index 49f0e4630..4dd09917b 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts @@ -9,7 +9,8 @@ import { REQUEST_VIDEO_EVENT_TYPES, VIDEO_CATEGORIES, VIDEO_LICENCES, - VIDEO_LANGUAGES + VIDEO_LANGUAGES, + VIDEO_PRIVACIES } from '../../../initializers' import { addEventToRemoteVideo, @@ -43,7 +44,7 @@ import { resetSequelizeInstance } from '../../../helpers' import { VideoInstance } from '../../../models' -import { VideoCreate, VideoUpdate } from '../../../../shared' +import { VideoCreate, VideoUpdate, VideoPrivacy } from '../../../../shared' import { abuseVideoRouter } from './abuse' import { blacklistRouter } from './blacklist' @@ -84,6 +85,7 @@ videosRouter.use('/', videoChannelRouter) videosRouter.get('/categories', listVideoCategories) videosRouter.get('/licences', listVideoLicences) videosRouter.get('/languages', listVideoLanguages) +videosRouter.get('/privacies', listVideoPrivacies) videosRouter.get('/', paginationValidator, @@ -149,6 +151,10 @@ function listVideoLanguages (req: express.Request, res: express.Response) { res.json(VIDEO_LANGUAGES) } +function listVideoPrivacies (req: express.Request, res: express.Response) { + res.json(VIDEO_PRIVACIES) +} + // Wrapper to video add that retry the function if there is a database error // We need this because we run the transaction in SERIALIZABLE isolation that can fail async function addVideoRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) { @@ -179,6 +185,7 @@ async function addVideo (req: express.Request, res: express.Response, videoPhysi language: videoInfo.language, nsfw: videoInfo.nsfw, description: videoInfo.description, + privacy: videoInfo.privacy, duration: videoPhysicalFile['duration'], // duration was added by a previous middleware channelId: res.locals.videoChannel.id } @@ -240,6 +247,8 @@ async function addVideo (req: express.Request, res: express.Response, videoPhysi // Let transcoding job send the video to friends because the video file extension might change if (CONFIG.TRANSCODING.ENABLED === true) return undefined + // Don't send video to remote pods, it is private + if (video.privacy === VideoPrivacy.PRIVATE) return undefined const remoteVideo = await video.toAddRemoteJSON() // Now we'll add the video's meta data to our friends @@ -264,6 +273,7 @@ async function updateVideo (req: express.Request, res: express.Response) { const videoInstance = res.locals.video const videoFieldsSave = videoInstance.toJSON() const videoInfoToUpdate: VideoUpdate = req.body + const wasPrivateVideo = videoInstance.privacy === VideoPrivacy.PRIVATE try { await db.sequelize.transaction(async t => { @@ -276,6 +286,7 @@ async function updateVideo (req: express.Request, res: express.Response) { if (videoInfoToUpdate.licence !== undefined) videoInstance.set('licence', videoInfoToUpdate.licence) if (videoInfoToUpdate.language !== undefined) videoInstance.set('language', videoInfoToUpdate.language) if (videoInfoToUpdate.nsfw !== undefined) videoInstance.set('nsfw', videoInfoToUpdate.nsfw) + if (videoInfoToUpdate.privacy !== undefined) videoInstance.set('privacy', videoInfoToUpdate.privacy) if (videoInfoToUpdate.description !== undefined) videoInstance.set('description', videoInfoToUpdate.description) await videoInstance.save(sequelizeOptions) @@ -287,10 +298,17 @@ async function updateVideo (req: express.Request, res: express.Response) { videoInstance.Tags = tagInstances } - const json = videoInstance.toUpdateRemoteJSON() - // Now we'll update the video's meta data to our friends - return updateVideoToFriends(json, t) + if (wasPrivateVideo === false) { + const json = videoInstance.toUpdateRemoteJSON() + return updateVideoToFriends(json, t) + } + + // Video is not private anymore, send a create action to remote pods + if (wasPrivateVideo === true && videoInstance.privacy !== VideoPrivacy.PRIVATE) { + const remoteVideo = await videoInstance.toAddRemoteJSON() + return addVideoToFriends(remoteVideo, t) + } }) logger.info('Video with name %s and uuid %s updated.', videoInstance.name, videoInstance.uuid) diff --git a/server/helpers/custom-validators/videos.ts b/server/helpers/custom-validators/videos.ts index 5b9102275..f3fdcaf2d 100644 --- a/server/helpers/custom-validators/videos.ts +++ b/server/helpers/custom-validators/videos.ts @@ -11,6 +11,7 @@ import { VIDEO_LICENCES, VIDEO_LANGUAGES, VIDEO_RATE_TYPES, + VIDEO_PRIVACIES, database as db } from '../../initializers' import { isUserUsernameValid } from './users' @@ -36,6 +37,15 @@ function isVideoLicenceValid (value: number) { return VIDEO_LICENCES[value] !== undefined } +function isVideoPrivacyValid (value: string) { + return VIDEO_PRIVACIES[value] !== undefined +} + +// Maybe we don't know the remote privacy setting, but that doesn't matter +function isRemoteVideoPrivacyValid (value: string) { + return validator.isInt('' + value) +} + // Maybe we don't know the remote licence, but that doesn't matter function isRemoteVideoLicenceValid (value: string) { return validator.isInt('' + value) @@ -195,6 +205,8 @@ export { isVideoDislikesValid, isVideoEventCountValid, isVideoFileSizeValid, + isVideoPrivacyValid, + isRemoteVideoPrivacyValid, isVideoFileResolutionValid, checkVideoExists, isRemoteVideoCategoryValid, diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index adccb9f41..d349abaf0 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -12,10 +12,11 @@ import { RemoteVideoRequestType, JobState } from '../../shared/models' +import { VideoPrivacy } from '../../shared/models/videos/video-privacy.enum' // --------------------------------------------------------------------------- -const LAST_MIGRATION_VERSION = 90 +const LAST_MIGRATION_VERSION = 95 // --------------------------------------------------------------------------- @@ -196,6 +197,12 @@ const VIDEO_LANGUAGES = { 14: 'Italian' } +const VIDEO_PRIVACIES = { + [VideoPrivacy.PUBLIC]: 'Public', + [VideoPrivacy.UNLISTED]: 'Unlisted', + [VideoPrivacy.PRIVATE]: 'Private' +} + // --------------------------------------------------------------------------- // Score a pod has when we create it as a friend @@ -394,6 +401,7 @@ export { THUMBNAILS_SIZE, VIDEO_CATEGORIES, VIDEO_LANGUAGES, + VIDEO_PRIVACIES, VIDEO_LICENCES, VIDEO_RATE_TYPES } diff --git a/server/initializers/migrations/0095-videos-privacy.ts b/server/initializers/migrations/0095-videos-privacy.ts new file mode 100644 index 000000000..4c2bf91d0 --- /dev/null +++ b/server/initializers/migrations/0095-videos-privacy.ts @@ -0,0 +1,35 @@ +import * as Sequelize from 'sequelize' + +async function up (utils: { + transaction: Sequelize.Transaction, + queryInterface: Sequelize.QueryInterface, + sequelize: Sequelize.Sequelize, + db: any +}): Promise { + const q = utils.queryInterface + + const data = { + type: Sequelize.INTEGER, + defaultValue: null, + allowNull: true + } + await q.addColumn('Videos', 'privacy', data) + + const query = 'UPDATE "Videos" SET "privacy" = 1' + const options = { + type: Sequelize.QueryTypes.BULKUPDATE + } + await utils.sequelize.query(query, options) + + data.allowNull = false + await q.changeColumn('Videos', 'privacy', data) +} + +function down (options) { + throw new Error('Not implemented.') +} + +export { + up, + down +} diff --git a/server/middlewares/validators/videos.ts b/server/middlewares/validators/videos.ts index 0c07404c5..e197d4606 100644 --- a/server/middlewares/validators/videos.ts +++ b/server/middlewares/validators/videos.ts @@ -20,9 +20,10 @@ import { isVideoRatingTypeValid, getDurationFromVideoFile, checkVideoExists, - isIdValid + isIdValid, + isVideoPrivacyValid } from '../../helpers' -import { UserRight } from '../../../shared' +import { UserRight, VideoPrivacy } from '../../../shared' const videosAddValidator = [ body('videofile').custom((value, { req }) => isVideoFile(req.files)).withMessage( @@ -36,6 +37,7 @@ const videosAddValidator = [ body('nsfw').custom(isVideoNSFWValid).withMessage('Should have a valid NSFW attribute'), body('description').custom(isVideoDescriptionValid).withMessage('Should have a valid description'), body('channelId').custom(isIdValid).withMessage('Should have correct video channel id'), + body('privacy').custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'), body('tags').optional().custom(isVideoTagsValid).withMessage('Should have correct tags'), (req: express.Request, res: express.Response, next: express.NextFunction) => { @@ -110,6 +112,7 @@ const videosUpdateValidator = [ body('licence').optional().custom(isVideoLicenceValid).withMessage('Should have a valid licence'), body('language').optional().custom(isVideoLanguageValid).withMessage('Should have a valid language'), body('nsfw').optional().custom(isVideoNSFWValid).withMessage('Should have a valid NSFW attribute'), + body('privacy').custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'), body('description').optional().custom(isVideoDescriptionValid).withMessage('Should have a valid description'), body('tags').optional().custom(isVideoTagsValid).withMessage('Should have correct tags'), @@ -118,19 +121,27 @@ const videosUpdateValidator = [ checkErrors(req, res, () => { checkVideoExists(req.params.id, res, () => { + const video = res.locals.video + // We need to make additional checks - if (res.locals.video.isOwned() === false) { + if (video.isOwned() === false) { return res.status(403) .json({ error: 'Cannot update video of another pod' }) .end() } - if (res.locals.video.VideoChannel.Author.userId !== res.locals.oauth.token.User.id) { + if (video.VideoChannel.Author.userId !== res.locals.oauth.token.User.id) { return res.status(403) .json({ error: 'Cannot update video of another user' }) .end() } + if (video.privacy !== VideoPrivacy.PRIVATE && req.body.privacy === VideoPrivacy.PRIVATE) { + return res.status(409) + .json({ error: 'Cannot set "private" a video that was not private anymore.' }) + .end() + } + next() }) }) diff --git a/server/models/video/video-interface.ts b/server/models/video/video-interface.ts index 587652f45..cfe65f9aa 100644 --- a/server/models/video/video-interface.ts +++ b/server/models/video/video-interface.ts @@ -49,6 +49,7 @@ export namespace VideoMethods { export type ListOwnedByAuthor = (author: string) => Promise export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList > + export type ListUserVideosForApi = (userId: number, start: number, count: number, sort: string) => Promise< ResultList > export type SearchAndPopulateAuthorAndPodAndTags = ( value: string, field: string, @@ -75,6 +76,7 @@ export interface VideoClass { generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData list: VideoMethods.List listForApi: VideoMethods.ListForApi + listUserVideosForApi: VideoMethods.ListUserVideosForApi listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags listOwnedByAuthor: VideoMethods.ListOwnedByAuthor load: VideoMethods.Load @@ -97,6 +99,7 @@ export interface VideoAttributes { nsfw: boolean description: string duration: number + privacy: number views?: number likes?: number dislikes?: number diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 1877c506a..2c1bd6b6e 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -18,6 +18,7 @@ import { isVideoNSFWValid, isVideoDescriptionValid, isVideoDurationValid, + isVideoPrivacyValid, readFileBufferPromise, unlinkPromise, renamePromise, @@ -38,10 +39,11 @@ import { THUMBNAILS_SIZE, PREVIEWS_SIZE, CONSTRAINTS_FIELDS, - API_VERSION + API_VERSION, + VIDEO_PRIVACIES } from '../../initializers' import { removeVideoToFriends } from '../../lib' -import { VideoResolution } from '../../../shared' +import { VideoResolution, VideoPrivacy } from '../../../shared' import { VideoFileInstance, VideoFileModel } from './video-file-interface' import { addMethodsToModel, getSort } from '../utils' @@ -79,6 +81,7 @@ let getTruncatedDescription: VideoMethods.GetTruncatedDescription let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData let list: VideoMethods.List let listForApi: VideoMethods.ListForApi +let listUserVideosForApi: VideoMethods.ListUserVideosForApi let loadByHostAndUUID: VideoMethods.LoadByHostAndUUID let listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags let listOwnedByAuthor: VideoMethods.ListOwnedByAuthor @@ -146,6 +149,16 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da } } }, + privacy: { + type: DataTypes.INTEGER, + allowNull: false, + validate: { + privacyValid: value => { + const res = isVideoPrivacyValid(value) + if (res === false) throw new Error('Video privacy is not valid.') + } + } + }, nsfw: { type: DataTypes.BOOLEAN, allowNull: false, @@ -245,6 +258,7 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da generateThumbnailFromData, list, listForApi, + listUserVideosForApi, listOwnedAndPopulateAuthorAndTags, listOwnedByAuthor, load, @@ -501,7 +515,13 @@ toFormattedJSON = function (this: VideoInstance) { toFormattedDetailsJSON = function (this: VideoInstance) { const formattedJson = this.toFormattedJSON() + // Maybe our pod is not up to date and there are new privacy settings since our version + let privacyLabel = VIDEO_PRIVACIES[this.privacy] + if (!privacyLabel) privacyLabel = 'Unknown' + const detailsJson = { + privacyLabel, + privacy: this.privacy, descriptionPath: this.getDescriptionPath(), channel: this.VideoChannel.toFormattedJSON(), files: [] @@ -555,6 +575,7 @@ toAddRemoteJSON = function (this: VideoInstance) { views: this.views, likes: this.likes, dislikes: this.dislikes, + privacy: this.privacy, files: [] } @@ -587,6 +608,7 @@ toUpdateRemoteJSON = function (this: VideoInstance) { views: this.views, likes: this.likes, dislikes: this.dislikes, + privacy: this.privacy, files: [] } @@ -746,8 +768,39 @@ list = function () { return Video.findAll(query) } +listUserVideosForApi = function (userId: number, start: number, count: number, sort: string) { + const query = { + distinct: true, + offset: start, + limit: count, + order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ], + include: [ + { + model: Video['sequelize'].models.VideoChannel, + required: true, + include: [ + { + model: Video['sequelize'].models.Author, + where: { + userId + }, + required: true + } + ] + }, + Video['sequelize'].models.Tag + ] + } + + return Video.findAndCountAll(query).then(({ rows, count }) => { + return { + data: rows, + total: count + } + }) +} + listForApi = function (start: number, count: number, sort: string) { - // Exclude blacklisted videos from the list const query = { distinct: true, offset: start, @@ -768,8 +821,7 @@ listForApi = function (start: number, count: number, sort: string) { } ] }, - Video['sequelize'].models.Tag, - Video['sequelize'].models.VideoFile + Video['sequelize'].models.Tag ], where: createBaseVideosWhere() } @@ -969,10 +1021,6 @@ searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, s model: Video['sequelize'].models.Tag } - const videoFileInclude: Sequelize.IncludeOptions = { - model: Video['sequelize'].models.VideoFile - } - const query: Sequelize.FindOptions = { distinct: true, where: createBaseVideosWhere(), @@ -981,12 +1029,7 @@ searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, s order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ] } - // Make an exact search with the magnet - if (field === 'magnetUri') { - videoFileInclude.where = { - infoHash: magnetUtil.decode(value).infoHash - } - } else if (field === 'tags') { + if (field === 'tags') { const escapedValue = Video['sequelize'].escape('%' + value + '%') query.where['id'][Sequelize.Op.in] = Video['sequelize'].literal( `(SELECT "VideoTags"."videoId" @@ -1016,7 +1059,7 @@ searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, s } query.include = [ - videoChannelInclude, tagInclude, videoFileInclude + videoChannelInclude, tagInclude ] return Video.findAndCountAll(query).then(({ rows, count }) => { @@ -1035,7 +1078,8 @@ function createBaseVideosWhere () { [Sequelize.Op.notIn]: Video['sequelize'].literal( '(SELECT "BlacklistedVideos"."videoId" FROM "BlacklistedVideos")' ) - } + }, + privacy: VideoPrivacy.PUBLIC } } diff --git a/shared/models/pods/remote-video/remote-video-create-request.model.ts b/shared/models/pods/remote-video/remote-video-create-request.model.ts index cb20dfa03..9a382e654 100644 --- a/shared/models/pods/remote-video/remote-video-create-request.model.ts +++ b/shared/models/pods/remote-video/remote-video-create-request.model.ts @@ -16,6 +16,7 @@ export interface RemoteVideoCreateData { views: number likes: number dislikes: number + privacy: number thumbnailData: string files: { infoHash: string diff --git a/shared/models/pods/remote-video/remote-video-update-request.model.ts b/shared/models/pods/remote-video/remote-video-update-request.model.ts index 8439cfa24..924489c75 100644 --- a/shared/models/pods/remote-video/remote-video-update-request.model.ts +++ b/shared/models/pods/remote-video/remote-video-update-request.model.ts @@ -15,6 +15,7 @@ export interface RemoteVideoUpdateData { views: number likes: number dislikes: number + privacy: number files: { infoHash: string extname: string diff --git a/shared/models/videos/index.ts b/shared/models/videos/index.ts index 2a3912f06..14a10f5d8 100644 --- a/shared/models/videos/index.ts +++ b/shared/models/videos/index.ts @@ -8,6 +8,7 @@ export * from './video-channel-create.model' export * from './video-channel-update.model' export * from './video-channel.model' export * from './video-create.model' +export * from './video-privacy.enum' export * from './video-rate.type' export * from './video-resolution.enum' export * from './video-update.model' diff --git a/shared/models/videos/video-create.model.ts b/shared/models/videos/video-create.model.ts index 4d0e83520..e537c38a8 100644 --- a/shared/models/videos/video-create.model.ts +++ b/shared/models/videos/video-create.model.ts @@ -1,3 +1,5 @@ +import { VideoPrivacy } from './video-privacy.enum' + export interface VideoCreate { category: number licence: number @@ -7,4 +9,5 @@ export interface VideoCreate { nsfw: boolean name: string tags: string[] + privacy: VideoPrivacy } diff --git a/shared/models/videos/video-privacy.enum.ts b/shared/models/videos/video-privacy.enum.ts new file mode 100644 index 000000000..29888c7b8 --- /dev/null +++ b/shared/models/videos/video-privacy.enum.ts @@ -0,0 +1,5 @@ +export enum VideoPrivacy { + PUBLIC = 1, + UNLISTED = 2, + PRIVATE = 3 +} diff --git a/shared/models/videos/video-update.model.ts b/shared/models/videos/video-update.model.ts index 29a82621b..0cf38fe6e 100644 --- a/shared/models/videos/video-update.model.ts +++ b/shared/models/videos/video-update.model.ts @@ -1,9 +1,12 @@ +import { VideoPrivacy } from './video-privacy.enum' + export interface VideoUpdate { name?: string category?: number licence?: number language?: number description?: string + privacy?: VideoPrivacy tags?: string[] nsfw?: boolean } diff --git a/shared/models/videos/video.model.ts b/shared/models/videos/video.model.ts index 1490d345c..2f4ee2462 100644 --- a/shared/models/videos/video.model.ts +++ b/shared/models/videos/video.model.ts @@ -1,4 +1,5 @@ import { VideoChannel } from './video-channel.model' +import { VideoPrivacy } from './video-privacy.enum' export interface VideoFile { magnetUri: string @@ -37,7 +38,9 @@ export interface Video { } export interface VideoDetails extends Video { - descriptionPath: string, + privacy: VideoPrivacy + privacyLabel: string + descriptionPath: string channel: VideoChannel files: VideoFile[] }