1
0
Fork 0

Add video privacy setting

This commit is contained in:
Chocobozzz 2017-10-31 11:52:52 +01:00
parent b7a485121d
commit fd45e8f43c
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
48 changed files with 545 additions and 208 deletions

View File

@ -1,4 +1,4 @@
import { Component, OnInit, ViewContainerRef } from '@angular/core' import { Component, OnInit } from '@angular/core'
import { Router } from '@angular/router' import { Router } from '@angular/router'
import { AuthService, ServerService } from './core' import { AuthService, ServerService } from './core'
@ -28,8 +28,7 @@ export class AppComponent implements OnInit {
constructor ( constructor (
private router: Router, private router: Router,
private authService: AuthService, private authService: AuthService,
private serverService: ServerService, private serverService: ServerService
private userService: UserService
) {} ) {}
ngOnInit () { ngOnInit () {
@ -45,6 +44,7 @@ export class AppComponent implements OnInit {
this.serverService.loadVideoCategories() this.serverService.loadVideoCategories()
this.serverService.loadVideoLanguages() this.serverService.loadVideoLanguages()
this.serverService.loadVideoLicences() this.serverService.loadVideoLicences()
this.serverService.loadVideoPrivacies()
// Do not display menu on small screens // Do not display menu on small screens
if (window.innerWidth < 600) { if (window.innerWidth < 600) {

View File

@ -23,6 +23,11 @@
<span class="hidden-xs glyphicon glyphicon-user"></span> <span class="hidden-xs glyphicon glyphicon-user"></span>
My account My account
</a> </a>
<a *ngIf="isLoggedIn" routerLink="/videos/mine" routerLinkActive="active">
<span class="hidden-xs glyphicon glyphicon-folder-open"></span>
My videos
</a>
</div> </div>
<div class="panel-block"> <div class="panel-block">

View File

@ -19,6 +19,7 @@ export class ServerService {
private videoCategories: Array<{ id: number, label: string }> = [] private videoCategories: Array<{ id: number, label: string }> = []
private videoLicences: Array<{ id: number, label: string }> = [] private videoLicences: Array<{ id: number, label: string }> = []
private videoLanguages: Array<{ id: number, label: string }> = [] private videoLanguages: Array<{ id: number, label: string }> = []
private videoPrivacies: Array<{ id: number, label: string }> = []
constructor (private http: HttpClient) {} constructor (private http: HttpClient) {}
@ -39,6 +40,10 @@ export class ServerService {
return this.loadVideoAttributeEnum('languages', this.videoLanguages) return this.loadVideoAttributeEnum('languages', this.videoLanguages)
} }
loadVideoPrivacies () {
return this.loadVideoAttributeEnum('privacies', this.videoPrivacies)
}
getConfig () { getConfig () {
return this.config return this.config
} }
@ -55,7 +60,14 @@ export class ServerService {
return this.videoLanguages 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) return this.http.get(ServerService.BASE_VIDEO_URL + attributeName)
.subscribe(data => { .subscribe(data => {
Object.keys(data) Object.keys(data)

View File

@ -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 = { export const VIDEO_CATEGORY = {
VALIDATORS: [ Validators.required ], VALIDATORS: [ Validators.required ],
MESSAGES: { MESSAGES: {

View File

@ -1 +1 @@
export type SearchField = 'name' | 'author' | 'host' | 'magnetUri' | 'tags' export type SearchField = 'name' | 'author' | 'host' | 'tags'

View File

@ -6,12 +6,12 @@
<input <input
type="text" id="search-video" name="search-video" class="form-control" placeholder="Search" class="form-control" type="text" id="search-video" name="search-video" class="form-control" placeholder="Search" class="form-control"
[(ngModel)]="searchCriterias.value" (keyup.enter)="doSearch()" [(ngModel)]="searchCriteria.value" (keyup.enter)="doSearch()"
> >
<div class="input-group-btn" dropdown placement="bottom right"> <div class="input-group-btn" dropdown placement="bottom right">
<button id="simple-btn-keyboard-nav" type="button" class="btn btn-default" dropdownToggle> <button id="simple-btn-keyboard-nav" type="button" class="btn btn-default" dropdownToggle>
{{ getStringChoice(searchCriterias.field) }} <span class="caret"></span> {{ getStringChoice(searchCriteria.field) }} <span class="caret"></span>
</button> </button>
<ul class="dropdown-menu dropdown-menu-right" role="menu" aria-labelledby="simple-btn-keyboard-nav" *dropdownMenu> <ul class="dropdown-menu dropdown-menu-right" role="menu" aria-labelledby="simple-btn-keyboard-nav" *dropdownMenu>
<li *ngFor="let choice of choiceKeys" class="dropdown-item" role="menu-item"> <li *ngFor="let choice of choiceKeys" class="dropdown-item" role="menu-item">

View File

@ -16,10 +16,9 @@ export class SearchComponent implements OnInit {
name: 'Name', name: 'Name',
author: 'Author', author: 'Author',
host: 'Pod Host', host: 'Pod Host',
magnetUri: 'Magnet URI',
tags: 'Tags' tags: 'Tags'
} }
searchCriterias: Search = { searchCriteria: Search = {
field: 'name', field: 'name',
value: '' value: ''
} }
@ -30,13 +29,13 @@ export class SearchComponent implements OnInit {
// Subscribe if the search changed // Subscribe if the search changed
// Usually changed by videos list component // Usually changed by videos list component
this.searchService.updateSearch.subscribe( this.searchService.updateSearch.subscribe(
newSearchCriterias => { newSearchCriteria => {
// Put a field by default // Put a field by default
if (!newSearchCriterias.field) { if (!newSearchCriteria.field) {
newSearchCriterias.field = 'name' newSearchCriteria.field = 'name'
} }
this.searchCriterias = newSearchCriterias this.searchCriteria = newSearchCriteria
} }
) )
} }
@ -49,9 +48,9 @@ export class SearchComponent implements OnInit {
$event.preventDefault() $event.preventDefault()
$event.stopPropagation() $event.stopPropagation()
this.searchCriterias.field = choice this.searchCriteria.field = choice
if (this.searchCriterias.value) { if (this.searchCriteria.value) {
this.doSearch() this.doSearch()
} }
} }
@ -61,7 +60,7 @@ export class SearchComponent implements OnInit {
this.router.navigate([ '/videos/list' ]) this.router.navigate([ '/videos/list' ])
} }
this.searchService.searchUpdated.next(this.searchCriterias) this.searchService.searchUpdated.next(this.searchCriteria)
} }
getStringChoice (choiceKey: SearchField) { getStringChoice (choiceKey: SearchField) {

View File

@ -17,6 +17,18 @@
</div> </div>
</div> </div>
<div class="form-group">
<label for="privacy">Privacy</label>
<select class="form-control" id="privacy" formControlName="privacy">
<option></option>
<option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option>
</select>
<div *ngIf="formErrors.privacy" class="alert alert-danger">
{{ formErrors.privacy }}
</div>
</div>
<div class="form-group"> <div class="form-group">
<input <input
type="checkbox" id="nsfw" type="checkbox" id="nsfw"

View File

@ -13,7 +13,8 @@ import {
VIDEO_DESCRIPTION, VIDEO_DESCRIPTION,
VIDEO_TAGS, VIDEO_TAGS,
VIDEO_CHANNEL, VIDEO_CHANNEL,
VIDEO_FILE VIDEO_FILE,
VIDEO_PRIVACY
} from '../../shared' } from '../../shared'
import { AuthService, ServerService } from '../../core' import { AuthService, ServerService } from '../../core'
import { VideoService } from '../shared' import { VideoService } from '../shared'
@ -34,6 +35,7 @@ export class VideoAddComponent extends FormReactive implements OnInit {
videoCategories = [] videoCategories = []
videoLicences = [] videoLicences = []
videoLanguages = [] videoLanguages = []
videoPrivacies = []
userVideoChannels = [] userVideoChannels = []
tagValidators = VIDEO_TAGS.VALIDATORS tagValidators = VIDEO_TAGS.VALIDATORS
@ -43,6 +45,7 @@ export class VideoAddComponent extends FormReactive implements OnInit {
form: FormGroup form: FormGroup
formErrors = { formErrors = {
name: '', name: '',
privacy: '',
category: '', category: '',
licence: '', licence: '',
language: '', language: '',
@ -52,6 +55,7 @@ export class VideoAddComponent extends FormReactive implements OnInit {
} }
validationMessages = { validationMessages = {
name: VIDEO_NAME.MESSAGES, name: VIDEO_NAME.MESSAGES,
privacy: VIDEO_PRIVACY.MESSAGES,
category: VIDEO_CATEGORY.MESSAGES, category: VIDEO_CATEGORY.MESSAGES,
licence: VIDEO_LICENCE.MESSAGES, licence: VIDEO_LICENCE.MESSAGES,
language: VIDEO_LANGUAGE.MESSAGES, language: VIDEO_LANGUAGE.MESSAGES,
@ -79,6 +83,7 @@ export class VideoAddComponent extends FormReactive implements OnInit {
this.form = this.formBuilder.group({ this.form = this.formBuilder.group({
name: [ '', VIDEO_NAME.VALIDATORS ], name: [ '', VIDEO_NAME.VALIDATORS ],
nsfw: [ false ], nsfw: [ false ],
privacy: [ '', VIDEO_PRIVACY.VALIDATORS ],
category: [ '', VIDEO_CATEGORY.VALIDATORS ], category: [ '', VIDEO_CATEGORY.VALIDATORS ],
licence: [ '', VIDEO_LICENCE.VALIDATORS ], licence: [ '', VIDEO_LICENCE.VALIDATORS ],
language: [ '', VIDEO_LANGUAGE.VALIDATORS ], language: [ '', VIDEO_LANGUAGE.VALIDATORS ],
@ -95,6 +100,7 @@ export class VideoAddComponent extends FormReactive implements OnInit {
this.videoCategories = this.serverService.getVideoCategories() this.videoCategories = this.serverService.getVideoCategories()
this.videoLicences = this.serverService.getVideoLicences() this.videoLicences = this.serverService.getVideoLicences()
this.videoLanguages = this.serverService.getVideoLanguages() this.videoLanguages = this.serverService.getVideoLanguages()
this.videoPrivacies = this.serverService.getVideoPrivacies()
this.buildForm() this.buildForm()
@ -139,6 +145,7 @@ export class VideoAddComponent extends FormReactive implements OnInit {
const formValue: VideoCreate = this.form.value const formValue: VideoCreate = this.form.value
const name = formValue.name const name = formValue.name
const privacy = formValue.privacy
const nsfw = formValue.nsfw const nsfw = formValue.nsfw
const category = formValue.category const category = formValue.category
const licence = formValue.licence const licence = formValue.licence
@ -150,6 +157,7 @@ export class VideoAddComponent extends FormReactive implements OnInit {
const formData = new FormData() const formData = new FormData()
formData.append('name', name) formData.append('name', name)
formData.append('privacy', privacy.toString())
formData.append('category', '' + category) formData.append('category', '' + category)
formData.append('nsfw', '' + nsfw) formData.append('nsfw', '' + nsfw)
formData.append('licence', '' + licence) formData.append('licence', '' + licence)

View File

@ -17,6 +17,18 @@
</div> </div>
</div> </div>
<div class="form-group">
<label for="privacy">Privacy</label>
<select class="form-control" id="privacy" formControlName="privacy">
<option></option>
<option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option>
</select>
<div *ngIf="formErrors.privacy" class="alert alert-danger">
{{ formErrors.privacy }}
</div>
</div>
<div class="form-group"> <div class="form-group">
<input <input
type="checkbox" id="nsfw" type="checkbox" id="nsfw"

View File

@ -1,7 +1,6 @@
import { Component, OnInit } from '@angular/core' import { Component, OnInit } from '@angular/core'
import { FormBuilder, FormGroup } from '@angular/forms' import { FormBuilder, FormGroup } from '@angular/forms'
import { ActivatedRoute, Router } from '@angular/router' import { ActivatedRoute, Router } from '@angular/router'
import { Observable } from 'rxjs/Observable'
import 'rxjs/add/observable/forkJoin' import 'rxjs/add/observable/forkJoin'
import { NotificationsService } from 'angular2-notifications' import { NotificationsService } from 'angular2-notifications'
@ -14,9 +13,11 @@ import {
VIDEO_LICENCE, VIDEO_LICENCE,
VIDEO_LANGUAGE, VIDEO_LANGUAGE,
VIDEO_DESCRIPTION, VIDEO_DESCRIPTION,
VIDEO_TAGS VIDEO_TAGS,
VIDEO_PRIVACY
} from '../../shared' } from '../../shared'
import { VideoEdit, VideoService } from '../shared' import { VideoEdit, VideoService } from '../shared'
import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy.enum'
@Component({ @Component({
selector: 'my-videos-update', selector: 'my-videos-update',
@ -29,6 +30,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
videoCategories = [] videoCategories = []
videoLicences = [] videoLicences = []
videoLanguages = [] videoLanguages = []
videoPrivacies = []
video: VideoEdit video: VideoEdit
tagValidators = VIDEO_TAGS.VALIDATORS tagValidators = VIDEO_TAGS.VALIDATORS
@ -38,6 +40,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
form: FormGroup form: FormGroup
formErrors = { formErrors = {
name: '', name: '',
privacy: '',
category: '', category: '',
licence: '', licence: '',
language: '', language: '',
@ -45,6 +48,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
} }
validationMessages = { validationMessages = {
name: VIDEO_NAME.MESSAGES, name: VIDEO_NAME.MESSAGES,
privacy: VIDEO_PRIVACY.MESSAGES,
category: VIDEO_CATEGORY.MESSAGES, category: VIDEO_CATEGORY.MESSAGES,
licence: VIDEO_LICENCE.MESSAGES, licence: VIDEO_LICENCE.MESSAGES,
language: VIDEO_LANGUAGE.MESSAGES, language: VIDEO_LANGUAGE.MESSAGES,
@ -67,6 +71,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
buildForm () { buildForm () {
this.form = this.formBuilder.group({ this.form = this.formBuilder.group({
name: [ '', VIDEO_NAME.VALIDATORS ], name: [ '', VIDEO_NAME.VALIDATORS ],
privacy: [ '', VIDEO_PRIVACY.VALIDATORS ],
nsfw: [ false ], nsfw: [ false ],
category: [ '', VIDEO_CATEGORY.VALIDATORS ], category: [ '', VIDEO_CATEGORY.VALIDATORS ],
licence: [ '', VIDEO_LICENCE.VALIDATORS ], licence: [ '', VIDEO_LICENCE.VALIDATORS ],
@ -84,6 +89,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
this.videoCategories = this.serverService.getVideoCategories() this.videoCategories = this.serverService.getVideoCategories()
this.videoLicences = this.serverService.getVideoLicences() this.videoLicences = this.serverService.getVideoLicences()
this.videoLanguages = this.serverService.getVideoLanguages() this.videoLanguages = this.serverService.getVideoLanguages()
this.videoPrivacies = this.serverService.getVideoPrivacies()
const uuid: string = this.route.snapshot.params['uuid'] const uuid: string = this.route.snapshot.params['uuid']
@ -98,6 +104,16 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
video => { video => {
this.video = new VideoEdit(video) 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() this.hydrateFormFromVideo()
}, },

View File

@ -22,7 +22,7 @@
<div *ngIf="videoNotFound" id="video-not-found">Video not found :'(</div> <div *ngIf="videoNotFound" id="video-not-found">Video not found :'(</div>
</div> </div>
<!-- P2P informations --> <!-- P2P information -->
<div id="torrent-info" class="row"> <div id="torrent-info" class="row">
<div id="torrent-info-download" class="col-md-4 col-sm-4 col-xs-4">Download: {{ downloadSpeed | bytes }}/s</div> <div id="torrent-info-download" class="col-md-4 col-sm-4 col-xs-4">Download: {{ downloadSpeed | bytes }}/s</div>
<div id="torrent-info-upload" class="col-md-4 col-sm-4 col-xs-4">Upload: {{ uploadSpeed | bytes }}/s</div> <div id="torrent-info-upload" class="col-md-4 col-sm-4 col-xs-4">Upload: {{ uploadSpeed | bytes }}/s</div>
@ -142,6 +142,15 @@
</div> </div>
<div class="video-details-attributes col-xs-4 col-md-3"> <div class="video-details-attributes col-xs-4 col-md-3">
<div class="video-details-attribute">
<span class="video-details-attribute-label">
Privacy:
</span>
<span class="video-details-attribute-value">
{{ video.privacyLabel }}
</span>
</div>
<div class="video-details-attribute"> <div class="video-details-attribute">
<span class="video-details-attribute-label"> <span class="video-details-attribute-label">
Category: Category:

View File

@ -5,7 +5,8 @@ import {
VideoFile, VideoFile,
VideoChannel, VideoChannel,
VideoResolution, VideoResolution,
UserRight UserRight,
VideoPrivacy
} from '../../../../../shared' } from '../../../../../shared'
export class VideoDetails extends Video implements VideoDetailsServerModel { export class VideoDetails extends Video implements VideoDetailsServerModel {
@ -41,10 +42,14 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
descriptionPath: string descriptionPath: string
files: VideoFile[] files: VideoFile[]
channel: VideoChannel channel: VideoChannel
privacy: VideoPrivacy
privacyLabel: string
constructor (hash: VideoDetailsServerModel) { constructor (hash: VideoDetailsServerModel) {
super(hash) super(hash)
this.privacy = hash.privacy
this.privacyLabel = hash.privacyLabel
this.descriptionPath = hash.descriptionPath this.descriptionPath = hash.descriptionPath
this.files = hash.files this.files = hash.files
this.channel = hash.channel this.channel = hash.channel

View File

@ -1,4 +1,5 @@
import { VideoDetails } from './video-details.model' import { VideoDetails } from './video-details.model'
import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy.enum'
export class VideoEdit { export class VideoEdit {
category: number category: number
@ -9,6 +10,7 @@ export class VideoEdit {
tags: string[] tags: string[]
nsfw: boolean nsfw: boolean
channel: number channel: number
privacy: VideoPrivacy
uuid?: string uuid?: string
id?: number id?: number
@ -23,6 +25,7 @@ export class VideoEdit {
this.tags = videoDetails.tags this.tags = videoDetails.tags
this.nsfw = videoDetails.nsfw this.nsfw = videoDetails.nsfw
this.channel = videoDetails.channel.id this.channel = videoDetails.channel.id
this.privacy = videoDetails.privacy
} }
patch (values: Object) { patch (values: Object) {
@ -40,7 +43,8 @@ export class VideoEdit {
name: this.name, name: this.name,
tags: this.tags, tags: this.tags,
nsfw: this.nsfw, nsfw: this.nsfw,
channel: this.channel channel: this.channel,
privacy: this.privacy
} }
} }
} }

View File

@ -19,7 +19,6 @@ import {
UserVideoRate, UserVideoRate,
VideoRateType, VideoRateType,
VideoUpdate, VideoUpdate,
VideoAbuseCreate,
UserVideoRateUpdate, UserVideoRateUpdate,
Video as VideoServerModel, Video as VideoServerModel,
VideoDetails as VideoDetailsServerModel, VideoDetails as VideoDetailsServerModel,
@ -51,6 +50,7 @@ export class VideoService {
licence: video.licence, licence: video.licence,
language, language,
description: video.description, description: video.description,
privacy: video.privacy,
tags: video.tags, tags: video.tags,
nsfw: video.nsfw nsfw: video.nsfw
} }
@ -63,22 +63,35 @@ export class VideoService {
uploadVideo (video: FormData) { uploadVideo (video: FormData) {
const req = new HttpRequest('POST', VideoService.BASE_VIDEO_URL + 'upload', video, { reportProgress: true }) const req = new HttpRequest('POST', VideoService.BASE_VIDEO_URL + 'upload', video, { reportProgress: true })
return this.authHttp.request(req) return this.authHttp
.catch(this.restExtractor.handleError) .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) const pagination = this.videoPaginationToRestPagination(videoPagination)
let params = new HttpParams() let params = new HttpParams()
params = this.restService.addRestGetParams(params, pagination, sort) params = this.restService.addRestGetParams(params, pagination, sort)
return this.authHttp.get(VideoService.BASE_VIDEO_URL, { params }) return this.authHttp.get(UserService.BASE_USERS_URL + '/me/videos', { params })
.map(this.extractVideos) .map(this.extractVideos)
.catch((res) => this.restExtractor.handleError(res)) .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 url = VideoService.BASE_VIDEO_URL + 'search/' + encodeURIComponent(search.value)
const pagination = this.videoPaginationToRestPagination(videoPagination) const pagination = this.videoPaginationToRestPagination(videoPagination)
@ -88,15 +101,17 @@ export class VideoService {
if (search.field) params.set('field', search.field) if (search.field) params.set('field', search.field)
return this.authHttp.get<ResultList<VideoServerModel>>(url, { params }) return this.authHttp
.map(this.extractVideos) .get<ResultList<VideoServerModel>>(url, { params })
.catch((res) => this.restExtractor.handleError(res)) .map(this.extractVideos)
.catch((res) => this.restExtractor.handleError(res))
} }
removeVideo (id: number) { removeVideo (id: number) {
return this.authHttp.delete(VideoService.BASE_VIDEO_URL + id) return this.authHttp
.map(this.restExtractor.extractDataBool) .delete(VideoService.BASE_VIDEO_URL + id)
.catch((res) => this.restExtractor.handleError(res)) .map(this.restExtractor.extractDataBool)
.catch((res) => this.restExtractor.handleError(res))
} }
loadCompleteDescription (descriptionPath: string) { loadCompleteDescription (descriptionPath: string) {
@ -117,8 +132,9 @@ export class VideoService {
getUserVideoRating (id: number): Observable<UserVideoRate> { getUserVideoRating (id: number): Observable<UserVideoRate> {
const url = UserService.BASE_USERS_URL + 'me/videos/' + id + '/rating' const url = UserService.BASE_USERS_URL + 'me/videos/' + id + '/rating'
return this.authHttp.get(url) return this.authHttp
.catch(res => this.restExtractor.handleError(res)) .get(url)
.catch(res => this.restExtractor.handleError(res))
} }
private videoPaginationToRestPagination (videoPagination: VideoPagination) { private videoPaginationToRestPagination (videoPagination: VideoPagination) {
@ -134,9 +150,10 @@ export class VideoService {
rating: rateType rating: rateType
} }
return this.authHttp.put(url, body) return this.authHttp
.map(this.restExtractor.extractDataBool) .put(url, body)
.catch(res => this.restExtractor.handleError(res)) .map(this.restExtractor.extractDataBool)
.catch(res => this.restExtractor.handleError(res))
} }
private extractVideos (result: ResultList<VideoServerModel>) { private extractVideos (result: ResultList<VideoServerModel>) {

View File

@ -1,4 +1,3 @@
export * from './loader.component' export * from './my-videos.component'
export * from './video-list.component' export * from './video-list.component'
export * from './video-miniature.component' export * from './shared'
export * from './video-sort.component'

View File

@ -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)
}
}

View File

@ -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<boolean> = 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 ])
}
}

View File

@ -0,0 +1,4 @@
export * from './abstract-video-list'
export * from './loader.component'
export * from './video-miniature.component'
export * from './video-sort.component'

View File

@ -1,7 +1,7 @@
import { Component, Input } from '@angular/core' import { Component, Input } from '@angular/core'
import { SortField, Video } from '../shared' import { SortField, Video } from '../../shared'
import { User } from '../../shared' import { User } from '../../../shared'
@Component({ @Component({
selector: 'my-video-miniature', selector: 'my-video-miniature',

View File

@ -1,6 +1,6 @@
import { Component, EventEmitter, Input, Output } from '@angular/core' import { Component, EventEmitter, Input, Output } from '@angular/core'
import { SortField } from '../shared' import { SortField } from '../../shared'
@Component({ @Component({
selector: 'my-video-sort', selector: 'my-video-sort',

View File

@ -1,51 +1,33 @@
import { Component, OnDestroy, OnInit } from '@angular/core' import { Component, OnDestroy, OnInit } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router' import { ActivatedRoute, Router } from '@angular/router'
import { Subscription } from 'rxjs/Subscription' import { Subscription } from 'rxjs/Subscription'
import { BehaviorSubject } from 'rxjs/BehaviorSubject'
import { NotificationsService } from 'angular2-notifications' import { NotificationsService } from 'angular2-notifications'
import { AuthService } from '../../core' import { VideoService } from '../shared'
import { import { Search, SearchField, SearchService } from '../../shared'
SortField, import { AbstractVideoList } from './shared'
Video,
VideoService,
VideoPagination
} from '../shared'
import { Search, SearchField, SearchService, User } from '../../shared'
@Component({ @Component({
selector: 'my-videos-list', selector: 'my-videos-list',
styleUrls: [ './video-list.component.scss' ], styleUrls: [ './shared/abstract-video-list.scss' ],
templateUrl: './video-list.component.html' templateUrl: './shared/abstract-video-list.html'
}) })
export class VideoListComponent implements OnInit, OnDestroy { export class VideoListComponent extends AbstractVideoList implements OnInit, OnDestroy {
loading: BehaviorSubject<boolean> = new BehaviorSubject(false)
pagination: VideoPagination = {
currentPage: 1,
itemsPerPage: 25,
totalItems: null
}
sort: SortField
user: User
videos: Video[] = []
private search: Search private search: Search
private subActivatedRoute: Subscription
private subSearch: Subscription private subSearch: Subscription
constructor ( constructor (
private authService: AuthService, protected router: Router,
private notificationsService: NotificationsService, protected route: ActivatedRoute,
private router: Router, protected notificationsService: NotificationsService,
private route: ActivatedRoute,
private videoService: VideoService, private videoService: VideoService,
private searchService: SearchService private searchService: SearchService
) {} ) {
super()
}
ngOnInit () { ngOnInit () {
this.user = this.authService.getUser()
// Subscribe to route changes // Subscribe to route changes
this.subActivatedRoute = this.route.params.subscribe(routeParams => { this.subActivatedRoute = this.route.params.subscribe(routeParams => {
this.loadRouteParams(routeParams) this.loadRouteParams(routeParams)
@ -66,14 +48,12 @@ export class VideoListComponent implements OnInit, OnDestroy {
} }
ngOnDestroy () { ngOnDestroy () {
this.subActivatedRoute.unsubscribe() super.ngOnDestroy()
this.subSearch.unsubscribe() this.subSearch.unsubscribe()
} }
getVideos () { getVideosObservable () {
this.loading.next(true)
this.videos = []
let observable = null let observable = null
if (this.search.value) { if (this.search.value) {
observable = this.videoService.searchVideos(this.search, this.pagination, this.sort) 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 = this.videoService.getVideos(this.pagination, this.sort)
} }
observable.subscribe( return observable
({ videos, totalVideos }) => {
this.videos = videos
this.pagination.totalItems = totalVideos
this.loading.next(false)
},
error => this.notificationsService.error('Error', error.text)
)
} }
isThereNoVideo () { protected buildRouteParams () {
return !this.loading.getValue() && this.videos.length === 0 const params = super.buildRouteParams()
}
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
}
// Maybe there is a search // Maybe there is a search
if (this.search.value) { if (this.search.value) {
@ -125,7 +76,9 @@ export class VideoListComponent implements OnInit, OnDestroy {
return params return params
} }
private loadRouteParams (routeParams: { [ key: string ]: any }) { protected loadRouteParams (routeParams: { [ key: string ]: any }) {
super.loadRouteParams(routeParams)
if (routeParams['search'] !== undefined) { if (routeParams['search'] !== undefined) {
this.search = { this.search = {
value: routeParams['search'], value: routeParams['search'],
@ -137,18 +90,5 @@ export class VideoListComponent implements OnInit, OnDestroy {
field: 'name' 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 ])
} }
} }

View File

@ -3,7 +3,7 @@ import { RouterModule, Routes } from '@angular/router'
import { MetaGuard } from '@ngx-meta/core' import { MetaGuard } from '@ngx-meta/core'
import { VideoListComponent } from './video-list' import { VideoListComponent, MyVideosComponent } from './video-list'
import { VideosComponent } from './videos.component' import { VideosComponent } from './videos.component'
const videosRoutes: Routes = [ const videosRoutes: Routes = [
@ -12,6 +12,15 @@ const videosRoutes: Routes = [
component: VideosComponent, component: VideosComponent,
canActivateChild: [ MetaGuard ], canActivateChild: [ MetaGuard ],
children: [ children: [
{
path: 'mine',
component: MyVideosComponent,
data: {
meta: {
title: 'My videos'
}
}
},
{ {
path: 'list', path: 'list',
component: VideoListComponent, component: VideoListComponent,

View File

@ -2,7 +2,13 @@ import { NgModule } from '@angular/core'
import { VideosRoutingModule } from './videos-routing.module' import { VideosRoutingModule } from './videos-routing.module'
import { VideosComponent } from './videos.component' 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 { VideoService } from './shared'
import { SharedModule } from '../shared' import { SharedModule } from '../shared'
@ -16,6 +22,7 @@ import { SharedModule } from '../shared'
VideosComponent, VideosComponent,
VideoListComponent, VideoListComponent,
MyVideosComponent,
VideoMiniatureComponent, VideoMiniatureComponent,
VideoSortComponent, VideoSortComponent,

View File

@ -334,71 +334,34 @@ $slider-bg-color: lighten($primary-background-color, 33%);
// Thanks: https://projects.lukehaas.me/css-loaders/ // Thanks: https://projects.lukehaas.me/css-loaders/
.vjs-loading-spinner { .vjs-loading-spinner {
border: none; margin: -25px 0 0 -25px;
opacity: 1; position: absolute;
top: 50%;
left: 50%;
font-size: 10px; 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; position: relative;
-webkit-animation: load3 1.4s infinite linear; text-indent: -9999em;
animation: load3 1.4s infinite linear; border: 0.7em solid rgba(255, 255, 255, 0.2);
-webkit-transform: translateZ(0); border-left-color: #ffffff;
-ms-transform: translateZ(0);
transform: translateZ(0); transform: translateZ(0);
animation: spinner 1.4s infinite linear;
&:before { &:before {
width: 50%;
height: 50%;
background: #ffffff;
border-radius: 100% 0 0 0;
position: absolute;
top: 0;
left: 0;
content: '';
animation: none !important; animation: none !important;
margin: 0 !important;
} }
&:after { &:after {
background: #000;
width: 75%;
height: 75%;
border-radius: 50%; border-radius: 50%;
content: ''; width: 6em;
margin: auto; height: 6em;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
animation: none !important; animation: none !important;
} }
@-webkit-keyframes load3 { @keyframes spinner {
0% { 0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg); transform: rotate(0deg);
} }
100% { 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); transform: rotate(360deg);
} }
} }

View File

@ -267,7 +267,8 @@ async function addRemoteVideo (videoToCreateData: RemoteVideoCreateData, fromPod
views: videoToCreateData.views, views: videoToCreateData.views,
likes: videoToCreateData.likes, likes: videoToCreateData.likes,
dislikes: videoToCreateData.dislikes, dislikes: videoToCreateData.dislikes,
remote: true remote: true,
privacy: videoToCreateData.privacy
} }
const video = db.Video.build(videoData) const video = db.Video.build(videoData)
@ -334,6 +335,7 @@ async function updateRemoteVideo (videoAttributesToUpdate: RemoteVideoUpdateData
videoInstance.set('views', videoAttributesToUpdate.views) videoInstance.set('views', videoAttributesToUpdate.views)
videoInstance.set('likes', videoAttributesToUpdate.likes) videoInstance.set('likes', videoAttributesToUpdate.likes)
videoInstance.set('dislikes', videoAttributesToUpdate.dislikes) videoInstance.set('dislikes', videoAttributesToUpdate.dislikes)
videoInstance.set('privacy', videoAttributesToUpdate.privacy)
await videoInstance.save(sequelizeOptions) await videoInstance.save(sequelizeOptions)

View File

@ -30,6 +30,8 @@ import {
} from '../../../shared' } from '../../../shared'
import { createUserAuthorAndChannel } from '../../lib' import { createUserAuthorAndChannel } from '../../lib'
import { UserInstance } from '../../models' import { UserInstance } from '../../models'
import { videosSortValidator } from '../../middlewares/validators/sort'
import { setVideosSort } from '../../middlewares/sort'
const usersRouter = express.Router() const usersRouter = express.Router()
@ -38,6 +40,15 @@ usersRouter.get('/me',
asyncMiddleware(getUserInformation) asyncMiddleware(getUserInformation)
) )
usersRouter.get('/me/videos',
authenticate,
paginationValidator,
videosSortValidator,
setVideosSort,
setPagination,
asyncMiddleware(getUserVideos)
)
usersRouter.get('/me/videos/:videoId/rating', usersRouter.get('/me/videos/:videoId/rating',
authenticate, authenticate,
usersVideoRatingValidator, 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) { async function createUserRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) {
const options = { const options = {
arguments: [ req, res ], 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) { 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) const user = await db.User.loadByUsernameAndPopulateChannels(res.locals.oauth.token.user.username)
return res.json(user.toFormattedJSON()) return res.json(user.toFormattedJSON())
} }
function getUser (req: express.Request, res: express.Response, next: express.NextFunction) { 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) { async function getUserVideoRating (req: express.Request, res: express.Response, next: express.NextFunction) {

View File

@ -9,7 +9,8 @@ import {
REQUEST_VIDEO_EVENT_TYPES, REQUEST_VIDEO_EVENT_TYPES,
VIDEO_CATEGORIES, VIDEO_CATEGORIES,
VIDEO_LICENCES, VIDEO_LICENCES,
VIDEO_LANGUAGES VIDEO_LANGUAGES,
VIDEO_PRIVACIES
} from '../../../initializers' } from '../../../initializers'
import { import {
addEventToRemoteVideo, addEventToRemoteVideo,
@ -43,7 +44,7 @@ import {
resetSequelizeInstance resetSequelizeInstance
} from '../../../helpers' } from '../../../helpers'
import { VideoInstance } from '../../../models' import { VideoInstance } from '../../../models'
import { VideoCreate, VideoUpdate } from '../../../../shared' import { VideoCreate, VideoUpdate, VideoPrivacy } from '../../../../shared'
import { abuseVideoRouter } from './abuse' import { abuseVideoRouter } from './abuse'
import { blacklistRouter } from './blacklist' import { blacklistRouter } from './blacklist'
@ -84,6 +85,7 @@ videosRouter.use('/', videoChannelRouter)
videosRouter.get('/categories', listVideoCategories) videosRouter.get('/categories', listVideoCategories)
videosRouter.get('/licences', listVideoLicences) videosRouter.get('/licences', listVideoLicences)
videosRouter.get('/languages', listVideoLanguages) videosRouter.get('/languages', listVideoLanguages)
videosRouter.get('/privacies', listVideoPrivacies)
videosRouter.get('/', videosRouter.get('/',
paginationValidator, paginationValidator,
@ -149,6 +151,10 @@ function listVideoLanguages (req: express.Request, res: express.Response) {
res.json(VIDEO_LANGUAGES) 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 // 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 // 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) { 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, language: videoInfo.language,
nsfw: videoInfo.nsfw, nsfw: videoInfo.nsfw,
description: videoInfo.description, description: videoInfo.description,
privacy: videoInfo.privacy,
duration: videoPhysicalFile['duration'], // duration was added by a previous middleware duration: videoPhysicalFile['duration'], // duration was added by a previous middleware
channelId: res.locals.videoChannel.id 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 // Let transcoding job send the video to friends because the video file extension might change
if (CONFIG.TRANSCODING.ENABLED === true) return undefined 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() const remoteVideo = await video.toAddRemoteJSON()
// Now we'll add the video's meta data to our friends // 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 videoInstance = res.locals.video
const videoFieldsSave = videoInstance.toJSON() const videoFieldsSave = videoInstance.toJSON()
const videoInfoToUpdate: VideoUpdate = req.body const videoInfoToUpdate: VideoUpdate = req.body
const wasPrivateVideo = videoInstance.privacy === VideoPrivacy.PRIVATE
try { try {
await db.sequelize.transaction(async t => { 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.licence !== undefined) videoInstance.set('licence', videoInfoToUpdate.licence)
if (videoInfoToUpdate.language !== undefined) videoInstance.set('language', videoInfoToUpdate.language) if (videoInfoToUpdate.language !== undefined) videoInstance.set('language', videoInfoToUpdate.language)
if (videoInfoToUpdate.nsfw !== undefined) videoInstance.set('nsfw', videoInfoToUpdate.nsfw) 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) if (videoInfoToUpdate.description !== undefined) videoInstance.set('description', videoInfoToUpdate.description)
await videoInstance.save(sequelizeOptions) await videoInstance.save(sequelizeOptions)
@ -287,10 +298,17 @@ async function updateVideo (req: express.Request, res: express.Response) {
videoInstance.Tags = tagInstances videoInstance.Tags = tagInstances
} }
const json = videoInstance.toUpdateRemoteJSON()
// Now we'll update the video's meta data to our friends // 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) logger.info('Video with name %s and uuid %s updated.', videoInstance.name, videoInstance.uuid)

View File

@ -11,6 +11,7 @@ import {
VIDEO_LICENCES, VIDEO_LICENCES,
VIDEO_LANGUAGES, VIDEO_LANGUAGES,
VIDEO_RATE_TYPES, VIDEO_RATE_TYPES,
VIDEO_PRIVACIES,
database as db database as db
} from '../../initializers' } from '../../initializers'
import { isUserUsernameValid } from './users' import { isUserUsernameValid } from './users'
@ -36,6 +37,15 @@ function isVideoLicenceValid (value: number) {
return VIDEO_LICENCES[value] !== undefined 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 // Maybe we don't know the remote licence, but that doesn't matter
function isRemoteVideoLicenceValid (value: string) { function isRemoteVideoLicenceValid (value: string) {
return validator.isInt('' + value) return validator.isInt('' + value)
@ -195,6 +205,8 @@ export {
isVideoDislikesValid, isVideoDislikesValid,
isVideoEventCountValid, isVideoEventCountValid,
isVideoFileSizeValid, isVideoFileSizeValid,
isVideoPrivacyValid,
isRemoteVideoPrivacyValid,
isVideoFileResolutionValid, isVideoFileResolutionValid,
checkVideoExists, checkVideoExists,
isRemoteVideoCategoryValid, isRemoteVideoCategoryValid,

View File

@ -12,10 +12,11 @@ import {
RemoteVideoRequestType, RemoteVideoRequestType,
JobState JobState
} from '../../shared/models' } 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' 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 // Score a pod has when we create it as a friend
@ -394,6 +401,7 @@ export {
THUMBNAILS_SIZE, THUMBNAILS_SIZE,
VIDEO_CATEGORIES, VIDEO_CATEGORIES,
VIDEO_LANGUAGES, VIDEO_LANGUAGES,
VIDEO_PRIVACIES,
VIDEO_LICENCES, VIDEO_LICENCES,
VIDEO_RATE_TYPES VIDEO_RATE_TYPES
} }

View File

@ -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<void> {
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
}

View File

@ -20,9 +20,10 @@ import {
isVideoRatingTypeValid, isVideoRatingTypeValid,
getDurationFromVideoFile, getDurationFromVideoFile,
checkVideoExists, checkVideoExists,
isIdValid isIdValid,
isVideoPrivacyValid
} from '../../helpers' } from '../../helpers'
import { UserRight } from '../../../shared' import { UserRight, VideoPrivacy } from '../../../shared'
const videosAddValidator = [ const videosAddValidator = [
body('videofile').custom((value, { req }) => isVideoFile(req.files)).withMessage( 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('nsfw').custom(isVideoNSFWValid).withMessage('Should have a valid NSFW attribute'),
body('description').custom(isVideoDescriptionValid).withMessage('Should have a valid description'), body('description').custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
body('channelId').custom(isIdValid).withMessage('Should have correct video channel id'), 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'), body('tags').optional().custom(isVideoTagsValid).withMessage('Should have correct tags'),
(req: express.Request, res: express.Response, next: express.NextFunction) => { (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('licence').optional().custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
body('language').optional().custom(isVideoLanguageValid).withMessage('Should have a valid language'), body('language').optional().custom(isVideoLanguageValid).withMessage('Should have a valid language'),
body('nsfw').optional().custom(isVideoNSFWValid).withMessage('Should have a valid NSFW attribute'), 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('description').optional().custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
body('tags').optional().custom(isVideoTagsValid).withMessage('Should have correct tags'), body('tags').optional().custom(isVideoTagsValid).withMessage('Should have correct tags'),
@ -118,19 +121,27 @@ const videosUpdateValidator = [
checkErrors(req, res, () => { checkErrors(req, res, () => {
checkVideoExists(req.params.id, res, () => { checkVideoExists(req.params.id, res, () => {
const video = res.locals.video
// We need to make additional checks // We need to make additional checks
if (res.locals.video.isOwned() === false) { if (video.isOwned() === false) {
return res.status(403) return res.status(403)
.json({ error: 'Cannot update video of another pod' }) .json({ error: 'Cannot update video of another pod' })
.end() .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) return res.status(403)
.json({ error: 'Cannot update video of another user' }) .json({ error: 'Cannot update video of another user' })
.end() .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() next()
}) })
}) })

View File

@ -49,6 +49,7 @@ export namespace VideoMethods {
export type ListOwnedByAuthor = (author: string) => Promise<VideoInstance[]> export type ListOwnedByAuthor = (author: string) => Promise<VideoInstance[]>
export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList<VideoInstance> > export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList<VideoInstance> >
export type ListUserVideosForApi = (userId: number, start: number, count: number, sort: string) => Promise< ResultList<VideoInstance> >
export type SearchAndPopulateAuthorAndPodAndTags = ( export type SearchAndPopulateAuthorAndPodAndTags = (
value: string, value: string,
field: string, field: string,
@ -75,6 +76,7 @@ export interface VideoClass {
generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData
list: VideoMethods.List list: VideoMethods.List
listForApi: VideoMethods.ListForApi listForApi: VideoMethods.ListForApi
listUserVideosForApi: VideoMethods.ListUserVideosForApi
listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags
listOwnedByAuthor: VideoMethods.ListOwnedByAuthor listOwnedByAuthor: VideoMethods.ListOwnedByAuthor
load: VideoMethods.Load load: VideoMethods.Load
@ -97,6 +99,7 @@ export interface VideoAttributes {
nsfw: boolean nsfw: boolean
description: string description: string
duration: number duration: number
privacy: number
views?: number views?: number
likes?: number likes?: number
dislikes?: number dislikes?: number

View File

@ -18,6 +18,7 @@ import {
isVideoNSFWValid, isVideoNSFWValid,
isVideoDescriptionValid, isVideoDescriptionValid,
isVideoDurationValid, isVideoDurationValid,
isVideoPrivacyValid,
readFileBufferPromise, readFileBufferPromise,
unlinkPromise, unlinkPromise,
renamePromise, renamePromise,
@ -38,10 +39,11 @@ import {
THUMBNAILS_SIZE, THUMBNAILS_SIZE,
PREVIEWS_SIZE, PREVIEWS_SIZE,
CONSTRAINTS_FIELDS, CONSTRAINTS_FIELDS,
API_VERSION API_VERSION,
VIDEO_PRIVACIES
} from '../../initializers' } from '../../initializers'
import { removeVideoToFriends } from '../../lib' import { removeVideoToFriends } from '../../lib'
import { VideoResolution } from '../../../shared' import { VideoResolution, VideoPrivacy } from '../../../shared'
import { VideoFileInstance, VideoFileModel } from './video-file-interface' import { VideoFileInstance, VideoFileModel } from './video-file-interface'
import { addMethodsToModel, getSort } from '../utils' import { addMethodsToModel, getSort } from '../utils'
@ -79,6 +81,7 @@ let getTruncatedDescription: VideoMethods.GetTruncatedDescription
let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData
let list: VideoMethods.List let list: VideoMethods.List
let listForApi: VideoMethods.ListForApi let listForApi: VideoMethods.ListForApi
let listUserVideosForApi: VideoMethods.ListUserVideosForApi
let loadByHostAndUUID: VideoMethods.LoadByHostAndUUID let loadByHostAndUUID: VideoMethods.LoadByHostAndUUID
let listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags let listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags
let listOwnedByAuthor: VideoMethods.ListOwnedByAuthor 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: { nsfw: {
type: DataTypes.BOOLEAN, type: DataTypes.BOOLEAN,
allowNull: false, allowNull: false,
@ -245,6 +258,7 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
generateThumbnailFromData, generateThumbnailFromData,
list, list,
listForApi, listForApi,
listUserVideosForApi,
listOwnedAndPopulateAuthorAndTags, listOwnedAndPopulateAuthorAndTags,
listOwnedByAuthor, listOwnedByAuthor,
load, load,
@ -501,7 +515,13 @@ toFormattedJSON = function (this: VideoInstance) {
toFormattedDetailsJSON = function (this: VideoInstance) { toFormattedDetailsJSON = function (this: VideoInstance) {
const formattedJson = this.toFormattedJSON() 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 = { const detailsJson = {
privacyLabel,
privacy: this.privacy,
descriptionPath: this.getDescriptionPath(), descriptionPath: this.getDescriptionPath(),
channel: this.VideoChannel.toFormattedJSON(), channel: this.VideoChannel.toFormattedJSON(),
files: [] files: []
@ -555,6 +575,7 @@ toAddRemoteJSON = function (this: VideoInstance) {
views: this.views, views: this.views,
likes: this.likes, likes: this.likes,
dislikes: this.dislikes, dislikes: this.dislikes,
privacy: this.privacy,
files: [] files: []
} }
@ -587,6 +608,7 @@ toUpdateRemoteJSON = function (this: VideoInstance) {
views: this.views, views: this.views,
likes: this.likes, likes: this.likes,
dislikes: this.dislikes, dislikes: this.dislikes,
privacy: this.privacy,
files: [] files: []
} }
@ -746,8 +768,39 @@ list = function () {
return Video.findAll(query) 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) { listForApi = function (start: number, count: number, sort: string) {
// Exclude blacklisted videos from the list
const query = { const query = {
distinct: true, distinct: true,
offset: start, offset: start,
@ -768,8 +821,7 @@ listForApi = function (start: number, count: number, sort: string) {
} }
] ]
}, },
Video['sequelize'].models.Tag, Video['sequelize'].models.Tag
Video['sequelize'].models.VideoFile
], ],
where: createBaseVideosWhere() where: createBaseVideosWhere()
} }
@ -969,10 +1021,6 @@ searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, s
model: Video['sequelize'].models.Tag model: Video['sequelize'].models.Tag
} }
const videoFileInclude: Sequelize.IncludeOptions = {
model: Video['sequelize'].models.VideoFile
}
const query: Sequelize.FindOptions<VideoAttributes> = { const query: Sequelize.FindOptions<VideoAttributes> = {
distinct: true, distinct: true,
where: createBaseVideosWhere(), where: createBaseVideosWhere(),
@ -981,12 +1029,7 @@ searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, s
order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ] order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ]
} }
// Make an exact search with the magnet if (field === 'tags') {
if (field === 'magnetUri') {
videoFileInclude.where = {
infoHash: magnetUtil.decode(value).infoHash
}
} else if (field === 'tags') {
const escapedValue = Video['sequelize'].escape('%' + value + '%') const escapedValue = Video['sequelize'].escape('%' + value + '%')
query.where['id'][Sequelize.Op.in] = Video['sequelize'].literal( query.where['id'][Sequelize.Op.in] = Video['sequelize'].literal(
`(SELECT "VideoTags"."videoId" `(SELECT "VideoTags"."videoId"
@ -1016,7 +1059,7 @@ searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, s
} }
query.include = [ query.include = [
videoChannelInclude, tagInclude, videoFileInclude videoChannelInclude, tagInclude
] ]
return Video.findAndCountAll(query).then(({ rows, count }) => { return Video.findAndCountAll(query).then(({ rows, count }) => {
@ -1035,7 +1078,8 @@ function createBaseVideosWhere () {
[Sequelize.Op.notIn]: Video['sequelize'].literal( [Sequelize.Op.notIn]: Video['sequelize'].literal(
'(SELECT "BlacklistedVideos"."videoId" FROM "BlacklistedVideos")' '(SELECT "BlacklistedVideos"."videoId" FROM "BlacklistedVideos")'
) )
} },
privacy: VideoPrivacy.PUBLIC
} }
} }

View File

@ -16,6 +16,7 @@ export interface RemoteVideoCreateData {
views: number views: number
likes: number likes: number
dislikes: number dislikes: number
privacy: number
thumbnailData: string thumbnailData: string
files: { files: {
infoHash: string infoHash: string

View File

@ -15,6 +15,7 @@ export interface RemoteVideoUpdateData {
views: number views: number
likes: number likes: number
dislikes: number dislikes: number
privacy: number
files: { files: {
infoHash: string infoHash: string
extname: string extname: string

View File

@ -8,6 +8,7 @@ export * from './video-channel-create.model'
export * from './video-channel-update.model' export * from './video-channel-update.model'
export * from './video-channel.model' export * from './video-channel.model'
export * from './video-create.model' export * from './video-create.model'
export * from './video-privacy.enum'
export * from './video-rate.type' export * from './video-rate.type'
export * from './video-resolution.enum' export * from './video-resolution.enum'
export * from './video-update.model' export * from './video-update.model'

View File

@ -1,3 +1,5 @@
import { VideoPrivacy } from './video-privacy.enum'
export interface VideoCreate { export interface VideoCreate {
category: number category: number
licence: number licence: number
@ -7,4 +9,5 @@ export interface VideoCreate {
nsfw: boolean nsfw: boolean
name: string name: string
tags: string[] tags: string[]
privacy: VideoPrivacy
} }

View File

@ -0,0 +1,5 @@
export enum VideoPrivacy {
PUBLIC = 1,
UNLISTED = 2,
PRIVATE = 3
}

View File

@ -1,9 +1,12 @@
import { VideoPrivacy } from './video-privacy.enum'
export interface VideoUpdate { export interface VideoUpdate {
name?: string name?: string
category?: number category?: number
licence?: number licence?: number
language?: number language?: number
description?: string description?: string
privacy?: VideoPrivacy
tags?: string[] tags?: string[]
nsfw?: boolean nsfw?: boolean
} }

View File

@ -1,4 +1,5 @@
import { VideoChannel } from './video-channel.model' import { VideoChannel } from './video-channel.model'
import { VideoPrivacy } from './video-privacy.enum'
export interface VideoFile { export interface VideoFile {
magnetUri: string magnetUri: string
@ -37,7 +38,9 @@ export interface Video {
} }
export interface VideoDetails extends Video { export interface VideoDetails extends Video {
descriptionPath: string, privacy: VideoPrivacy
privacyLabel: string
descriptionPath: string
channel: VideoChannel channel: VideoChannel
files: VideoFile[] files: VideoFile[]
} }