aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <florian.bigard@gmail.com>2017-10-31 11:52:52 +0100
committerChocobozzz <florian.bigard@gmail.com>2017-10-31 11:53:13 +0100
commitfd45e8f43c2638478599ca75632518054461da85 (patch)
tree01e1fb5ddad53bde8fb2c48f348fb8add51cfdb3
parentb7a485121d71c95fcf5e432e4cc745cf91af4f93 (diff)
downloadPeerTube-fd45e8f43c2638478599ca75632518054461da85.tar.gz
PeerTube-fd45e8f43c2638478599ca75632518054461da85.tar.zst
PeerTube-fd45e8f43c2638478599ca75632518054461da85.zip
Add video privacy setting
-rw-r--r--client/src/app/app.component.ts6
-rw-r--r--client/src/app/core/menu/menu.component.html5
-rw-r--r--client/src/app/core/server/server.service.ts14
-rw-r--r--client/src/app/shared/forms/form-validators/video.ts7
-rw-r--r--client/src/app/shared/search/search-field.type.ts2
-rw-r--r--client/src/app/shared/search/search.component.html4
-rw-r--r--client/src/app/shared/search/search.component.ts17
-rw-r--r--client/src/app/videos/+video-edit/video-add.component.html12
-rw-r--r--client/src/app/videos/+video-edit/video-add.component.ts10
-rw-r--r--client/src/app/videos/+video-edit/video-update.component.html12
-rw-r--r--client/src/app/videos/+video-edit/video-update.component.ts20
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.html11
-rw-r--r--client/src/app/videos/shared/video-details.model.ts7
-rw-r--r--client/src/app/videos/shared/video-edit.model.ts6
-rw-r--r--client/src/app/videos/shared/video.service.ts55
-rw-r--r--client/src/app/videos/video-list/index.ts5
-rw-r--r--client/src/app/videos/video-list/my-videos.component.ts36
-rw-r--r--client/src/app/videos/video-list/shared/abstract-video-list.html (renamed from client/src/app/videos/video-list/video-list.component.html)0
-rw-r--r--client/src/app/videos/video-list/shared/abstract-video-list.scss (renamed from client/src/app/videos/video-list/video-list.component.scss)0
-rw-r--r--client/src/app/videos/video-list/shared/abstract-video-list.ts104
-rw-r--r--client/src/app/videos/video-list/shared/index.ts4
-rw-r--r--client/src/app/videos/video-list/shared/loader.component.html (renamed from client/src/app/videos/video-list/loader.component.html)0
-rw-r--r--client/src/app/videos/video-list/shared/loader.component.ts (renamed from client/src/app/videos/video-list/loader.component.ts)0
-rw-r--r--client/src/app/videos/video-list/shared/video-miniature.component.html (renamed from client/src/app/videos/video-list/video-miniature.component.html)0
-rw-r--r--client/src/app/videos/video-list/shared/video-miniature.component.scss (renamed from client/src/app/videos/video-list/video-miniature.component.scss)0
-rw-r--r--client/src/app/videos/video-list/shared/video-miniature.component.ts (renamed from client/src/app/videos/video-list/video-miniature.component.ts)4
-rw-r--r--client/src/app/videos/video-list/shared/video-sort.component.html (renamed from client/src/app/videos/video-list/video-sort.component.html)0
-rw-r--r--client/src/app/videos/video-list/shared/video-sort.component.ts (renamed from client/src/app/videos/video-list/video-sort.component.ts)2
-rw-r--r--client/src/app/videos/video-list/video-list.component.ts102
-rw-r--r--client/src/app/videos/videos-routing.module.ts11
-rw-r--r--client/src/app/videos/videos.module.ts9
-rw-r--r--client/src/sass/video-js-custom.scss59
-rw-r--r--server/controllers/api/remote/videos.ts4
-rw-r--r--server/controllers/api/users.ts21
-rw-r--r--server/controllers/api/videos/index.ts28
-rw-r--r--server/helpers/custom-validators/videos.ts12
-rw-r--r--server/initializers/constants.ts10
-rw-r--r--server/initializers/migrations/0095-videos-privacy.ts35
-rw-r--r--server/middlewares/validators/videos.ts19
-rw-r--r--server/models/video/video-interface.ts3
-rw-r--r--server/models/video/video.ts78
-rw-r--r--shared/models/pods/remote-video/remote-video-create-request.model.ts1
-rw-r--r--shared/models/pods/remote-video/remote-video-update-request.model.ts1
-rw-r--r--shared/models/videos/index.ts1
-rw-r--r--shared/models/videos/video-create.model.ts3
-rw-r--r--shared/models/videos/video-privacy.enum.ts5
-rw-r--r--shared/models/videos/video-update.model.ts3
-rw-r--r--shared/models/videos/video.model.ts5
48 files changed, 545 insertions, 208 deletions
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 @@
1import { Component, OnInit, ViewContainerRef } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { Router } from '@angular/router' 2import { Router } from '@angular/router'
3 3
4import { AuthService, ServerService } from './core' 4import { AuthService, ServerService } from './core'
@@ -28,8 +28,7 @@ export class AppComponent implements OnInit {
28 constructor ( 28 constructor (
29 private router: Router, 29 private router: Router,
30 private authService: AuthService, 30 private authService: AuthService,
31 private serverService: ServerService, 31 private serverService: ServerService
32 private userService: UserService
33 ) {} 32 ) {}
34 33
35 ngOnInit () { 34 ngOnInit () {
@@ -45,6 +44,7 @@ export class AppComponent implements OnInit {
45 this.serverService.loadVideoCategories() 44 this.serverService.loadVideoCategories()
46 this.serverService.loadVideoLanguages() 45 this.serverService.loadVideoLanguages()
47 this.serverService.loadVideoLicences() 46 this.serverService.loadVideoLicences()
47 this.serverService.loadVideoPrivacies()
48 48
49 // Do not display menu on small screens 49 // Do not display menu on small screens
50 if (window.innerWidth < 600) { 50 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 @@
23 <span class="hidden-xs glyphicon glyphicon-user"></span> 23 <span class="hidden-xs glyphicon glyphicon-user"></span>
24 My account 24 My account
25 </a> 25 </a>
26
27 <a *ngIf="isLoggedIn" routerLink="/videos/mine" routerLinkActive="active">
28 <span class="hidden-xs glyphicon glyphicon-folder-open"></span>
29 My videos
30 </a>
26 </div> 31 </div>
27 32
28 <div class="panel-block"> 33 <div class="panel-block">
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 {
19 private videoCategories: Array<{ id: number, label: string }> = [] 19 private videoCategories: Array<{ id: number, label: string }> = []
20 private videoLicences: Array<{ id: number, label: string }> = [] 20 private videoLicences: Array<{ id: number, label: string }> = []
21 private videoLanguages: Array<{ id: number, label: string }> = [] 21 private videoLanguages: Array<{ id: number, label: string }> = []
22 private videoPrivacies: Array<{ id: number, label: string }> = []
22 23
23 constructor (private http: HttpClient) {} 24 constructor (private http: HttpClient) {}
24 25
@@ -39,6 +40,10 @@ export class ServerService {
39 return this.loadVideoAttributeEnum('languages', this.videoLanguages) 40 return this.loadVideoAttributeEnum('languages', this.videoLanguages)
40 } 41 }
41 42
43 loadVideoPrivacies () {
44 return this.loadVideoAttributeEnum('privacies', this.videoPrivacies)
45 }
46
42 getConfig () { 47 getConfig () {
43 return this.config 48 return this.config
44 } 49 }
@@ -55,7 +60,14 @@ export class ServerService {
55 return this.videoLanguages 60 return this.videoLanguages
56 } 61 }
57 62
58 private loadVideoAttributeEnum (attributeName: 'categories' | 'licences' | 'languages', hashToPopulate: { id: number, label: string }[]) { 63 getVideoPrivacies () {
64 return this.videoPrivacies
65 }
66
67 private loadVideoAttributeEnum (
68 attributeName: 'categories' | 'licences' | 'languages' | 'privacies',
69 hashToPopulate: { id: number, label: string }[]
70 ) {
59 return this.http.get(ServerService.BASE_VIDEO_URL + attributeName) 71 return this.http.get(ServerService.BASE_VIDEO_URL + attributeName)
60 .subscribe(data => { 72 .subscribe(data => {
61 Object.keys(data) 73 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 = {
9 } 9 }
10} 10}
11 11
12export const VIDEO_PRIVACY = {
13 VALIDATORS: [ Validators.required ],
14 MESSAGES: {
15 'required': 'Video privacy is required.'
16 }
17}
18
12export const VIDEO_CATEGORY = { 19export const VIDEO_CATEGORY = {
13 VALIDATORS: [ Validators.required ], 20 VALIDATORS: [ Validators.required ],
14 MESSAGES: { 21 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 @@
6 6
7 <input 7 <input
8 type="text" id="search-video" name="search-video" class="form-control" placeholder="Search" class="form-control" 8 type="text" id="search-video" name="search-video" class="form-control" placeholder="Search" class="form-control"
9 [(ngModel)]="searchCriterias.value" (keyup.enter)="doSearch()" 9 [(ngModel)]="searchCriteria.value" (keyup.enter)="doSearch()"
10 > 10 >
11 11
12 <div class="input-group-btn" dropdown placement="bottom right"> 12 <div class="input-group-btn" dropdown placement="bottom right">
13 <button id="simple-btn-keyboard-nav" type="button" class="btn btn-default" dropdownToggle> 13 <button id="simple-btn-keyboard-nav" type="button" class="btn btn-default" dropdownToggle>
14 {{ getStringChoice(searchCriterias.field) }} <span class="caret"></span> 14 {{ getStringChoice(searchCriteria.field) }} <span class="caret"></span>
15 </button> 15 </button>
16 <ul class="dropdown-menu dropdown-menu-right" role="menu" aria-labelledby="simple-btn-keyboard-nav" *dropdownMenu> 16 <ul class="dropdown-menu dropdown-menu-right" role="menu" aria-labelledby="simple-btn-keyboard-nav" *dropdownMenu>
17 <li *ngFor="let choice of choiceKeys" class="dropdown-item" role="menu-item"> 17 <li *ngFor="let choice of choiceKeys" class="dropdown-item" role="menu-item">
diff --git a/client/src/app/shared/search/search.component.ts b/client/src/app/shared/search/search.component.ts
index ecce20666..6e2827fe3 100644
--- a/client/src/app/shared/search/search.component.ts
+++ b/client/src/app/shared/search/search.component.ts
@@ -16,10 +16,9 @@ export class SearchComponent implements OnInit {
16 name: 'Name', 16 name: 'Name',
17 author: 'Author', 17 author: 'Author',
18 host: 'Pod Host', 18 host: 'Pod Host',
19 magnetUri: 'Magnet URI',
20 tags: 'Tags' 19 tags: 'Tags'
21 } 20 }
22 searchCriterias: Search = { 21 searchCriteria: Search = {
23 field: 'name', 22 field: 'name',
24 value: '' 23 value: ''
25 } 24 }
@@ -30,13 +29,13 @@ export class SearchComponent implements OnInit {
30 // Subscribe if the search changed 29 // Subscribe if the search changed
31 // Usually changed by videos list component 30 // Usually changed by videos list component
32 this.searchService.updateSearch.subscribe( 31 this.searchService.updateSearch.subscribe(
33 newSearchCriterias => { 32 newSearchCriteria => {
34 // Put a field by default 33 // Put a field by default
35 if (!newSearchCriterias.field) { 34 if (!newSearchCriteria.field) {
36 newSearchCriterias.field = 'name' 35 newSearchCriteria.field = 'name'
37 } 36 }
38 37
39 this.searchCriterias = newSearchCriterias 38 this.searchCriteria = newSearchCriteria
40 } 39 }
41 ) 40 )
42 } 41 }
@@ -49,9 +48,9 @@ export class SearchComponent implements OnInit {
49 $event.preventDefault() 48 $event.preventDefault()
50 $event.stopPropagation() 49 $event.stopPropagation()
51 50
52 this.searchCriterias.field = choice 51 this.searchCriteria.field = choice
53 52
54 if (this.searchCriterias.value) { 53 if (this.searchCriteria.value) {
55 this.doSearch() 54 this.doSearch()
56 } 55 }
57 } 56 }
@@ -61,7 +60,7 @@ export class SearchComponent implements OnInit {
61 this.router.navigate([ '/videos/list' ]) 60 this.router.navigate([ '/videos/list' ])
62 } 61 }
63 62
64 this.searchService.searchUpdated.next(this.searchCriterias) 63 this.searchService.searchUpdated.next(this.searchCriteria)
65 } 64 }
66 65
67 getStringChoice (choiceKey: SearchField) { 66 getStringChoice (choiceKey: SearchField) {
diff --git a/client/src/app/videos/+video-edit/video-add.component.html b/client/src/app/videos/+video-edit/video-add.component.html
index a70788ed8..b4e0f9f7c 100644
--- a/client/src/app/videos/+video-edit/video-add.component.html
+++ b/client/src/app/videos/+video-edit/video-add.component.html
@@ -18,6 +18,18 @@
18 </div> 18 </div>
19 19
20 <div class="form-group"> 20 <div class="form-group">
21 <label for="privacy">Privacy</label>
22 <select class="form-control" id="privacy" formControlName="privacy">
23 <option></option>
24 <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option>
25 </select>
26
27 <div *ngIf="formErrors.privacy" class="alert alert-danger">
28 {{ formErrors.privacy }}
29 </div>
30 </div>
31
32 <div class="form-group">
21 <input 33 <input
22 type="checkbox" id="nsfw" 34 type="checkbox" id="nsfw"
23 formControlName="nsfw" 35 formControlName="nsfw"
diff --git a/client/src/app/videos/+video-edit/video-add.component.ts b/client/src/app/videos/+video-edit/video-add.component.ts
index 5b5557ed9..c8094f792 100644
--- a/client/src/app/videos/+video-edit/video-add.component.ts
+++ b/client/src/app/videos/+video-edit/video-add.component.ts
@@ -13,7 +13,8 @@ import {
13 VIDEO_DESCRIPTION, 13 VIDEO_DESCRIPTION,
14 VIDEO_TAGS, 14 VIDEO_TAGS,
15 VIDEO_CHANNEL, 15 VIDEO_CHANNEL,
16 VIDEO_FILE 16 VIDEO_FILE,
17 VIDEO_PRIVACY
17} from '../../shared' 18} from '../../shared'
18import { AuthService, ServerService } from '../../core' 19import { AuthService, ServerService } from '../../core'
19import { VideoService } from '../shared' 20import { VideoService } from '../shared'
@@ -34,6 +35,7 @@ export class VideoAddComponent extends FormReactive implements OnInit {
34 videoCategories = [] 35 videoCategories = []
35 videoLicences = [] 36 videoLicences = []
36 videoLanguages = [] 37 videoLanguages = []
38 videoPrivacies = []
37 userVideoChannels = [] 39 userVideoChannels = []
38 40
39 tagValidators = VIDEO_TAGS.VALIDATORS 41 tagValidators = VIDEO_TAGS.VALIDATORS
@@ -43,6 +45,7 @@ export class VideoAddComponent extends FormReactive implements OnInit {
43 form: FormGroup 45 form: FormGroup
44 formErrors = { 46 formErrors = {
45 name: '', 47 name: '',
48 privacy: '',
46 category: '', 49 category: '',
47 licence: '', 50 licence: '',
48 language: '', 51 language: '',
@@ -52,6 +55,7 @@ export class VideoAddComponent extends FormReactive implements OnInit {
52 } 55 }
53 validationMessages = { 56 validationMessages = {
54 name: VIDEO_NAME.MESSAGES, 57 name: VIDEO_NAME.MESSAGES,
58 privacy: VIDEO_PRIVACY.MESSAGES,
55 category: VIDEO_CATEGORY.MESSAGES, 59 category: VIDEO_CATEGORY.MESSAGES,
56 licence: VIDEO_LICENCE.MESSAGES, 60 licence: VIDEO_LICENCE.MESSAGES,
57 language: VIDEO_LANGUAGE.MESSAGES, 61 language: VIDEO_LANGUAGE.MESSAGES,
@@ -79,6 +83,7 @@ export class VideoAddComponent extends FormReactive implements OnInit {
79 this.form = this.formBuilder.group({ 83 this.form = this.formBuilder.group({
80 name: [ '', VIDEO_NAME.VALIDATORS ], 84 name: [ '', VIDEO_NAME.VALIDATORS ],
81 nsfw: [ false ], 85 nsfw: [ false ],
86 privacy: [ '', VIDEO_PRIVACY.VALIDATORS ],
82 category: [ '', VIDEO_CATEGORY.VALIDATORS ], 87 category: [ '', VIDEO_CATEGORY.VALIDATORS ],
83 licence: [ '', VIDEO_LICENCE.VALIDATORS ], 88 licence: [ '', VIDEO_LICENCE.VALIDATORS ],
84 language: [ '', VIDEO_LANGUAGE.VALIDATORS ], 89 language: [ '', VIDEO_LANGUAGE.VALIDATORS ],
@@ -95,6 +100,7 @@ export class VideoAddComponent extends FormReactive implements OnInit {
95 this.videoCategories = this.serverService.getVideoCategories() 100 this.videoCategories = this.serverService.getVideoCategories()
96 this.videoLicences = this.serverService.getVideoLicences() 101 this.videoLicences = this.serverService.getVideoLicences()
97 this.videoLanguages = this.serverService.getVideoLanguages() 102 this.videoLanguages = this.serverService.getVideoLanguages()
103 this.videoPrivacies = this.serverService.getVideoPrivacies()
98 104
99 this.buildForm() 105 this.buildForm()
100 106
@@ -139,6 +145,7 @@ export class VideoAddComponent extends FormReactive implements OnInit {
139 const formValue: VideoCreate = this.form.value 145 const formValue: VideoCreate = this.form.value
140 146
141 const name = formValue.name 147 const name = formValue.name
148 const privacy = formValue.privacy
142 const nsfw = formValue.nsfw 149 const nsfw = formValue.nsfw
143 const category = formValue.category 150 const category = formValue.category
144 const licence = formValue.licence 151 const licence = formValue.licence
@@ -150,6 +157,7 @@ export class VideoAddComponent extends FormReactive implements OnInit {
150 157
151 const formData = new FormData() 158 const formData = new FormData()
152 formData.append('name', name) 159 formData.append('name', name)
160 formData.append('privacy', privacy.toString())
153 formData.append('category', '' + category) 161 formData.append('category', '' + category)
154 formData.append('nsfw', '' + nsfw) 162 formData.append('nsfw', '' + nsfw)
155 formData.append('licence', '' + licence) 163 formData.append('licence', '' + licence)
diff --git a/client/src/app/videos/+video-edit/video-update.component.html b/client/src/app/videos/+video-edit/video-update.component.html
index ec040630e..b9c6139b2 100644
--- a/client/src/app/videos/+video-edit/video-update.component.html
+++ b/client/src/app/videos/+video-edit/video-update.component.html
@@ -18,6 +18,18 @@
18 </div> 18 </div>
19 19
20 <div class="form-group"> 20 <div class="form-group">
21 <label for="privacy">Privacy</label>
22 <select class="form-control" id="privacy" formControlName="privacy">
23 <option></option>
24 <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option>
25 </select>
26
27 <div *ngIf="formErrors.privacy" class="alert alert-danger">
28 {{ formErrors.privacy }}
29 </div>
30 </div>
31
32 <div class="form-group">
21 <input 33 <input
22 type="checkbox" id="nsfw" 34 type="checkbox" id="nsfw"
23 formControlName="nsfw" 35 formControlName="nsfw"
diff --git a/client/src/app/videos/+video-edit/video-update.component.ts b/client/src/app/videos/+video-edit/video-update.component.ts
index 6ced77f1a..be663575f 100644
--- a/client/src/app/videos/+video-edit/video-update.component.ts
+++ b/client/src/app/videos/+video-edit/video-update.component.ts
@@ -1,7 +1,6 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { FormBuilder, FormGroup } from '@angular/forms' 2import { FormBuilder, FormGroup } from '@angular/forms'
3import { ActivatedRoute, Router } from '@angular/router' 3import { ActivatedRoute, Router } from '@angular/router'
4import { Observable } from 'rxjs/Observable'
5import 'rxjs/add/observable/forkJoin' 4import 'rxjs/add/observable/forkJoin'
6 5
7import { NotificationsService } from 'angular2-notifications' 6import { NotificationsService } from 'angular2-notifications'
@@ -14,9 +13,11 @@ import {
14 VIDEO_LICENCE, 13 VIDEO_LICENCE,
15 VIDEO_LANGUAGE, 14 VIDEO_LANGUAGE,
16 VIDEO_DESCRIPTION, 15 VIDEO_DESCRIPTION,
17 VIDEO_TAGS 16 VIDEO_TAGS,
17 VIDEO_PRIVACY
18} from '../../shared' 18} from '../../shared'
19import { VideoEdit, VideoService } from '../shared' 19import { VideoEdit, VideoService } from '../shared'
20import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy.enum'
20 21
21@Component({ 22@Component({
22 selector: 'my-videos-update', 23 selector: 'my-videos-update',
@@ -29,6 +30,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
29 videoCategories = [] 30 videoCategories = []
30 videoLicences = [] 31 videoLicences = []
31 videoLanguages = [] 32 videoLanguages = []
33 videoPrivacies = []
32 video: VideoEdit 34 video: VideoEdit
33 35
34 tagValidators = VIDEO_TAGS.VALIDATORS 36 tagValidators = VIDEO_TAGS.VALIDATORS
@@ -38,6 +40,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
38 form: FormGroup 40 form: FormGroup
39 formErrors = { 41 formErrors = {
40 name: '', 42 name: '',
43 privacy: '',
41 category: '', 44 category: '',
42 licence: '', 45 licence: '',
43 language: '', 46 language: '',
@@ -45,6 +48,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
45 } 48 }
46 validationMessages = { 49 validationMessages = {
47 name: VIDEO_NAME.MESSAGES, 50 name: VIDEO_NAME.MESSAGES,
51 privacy: VIDEO_PRIVACY.MESSAGES,
48 category: VIDEO_CATEGORY.MESSAGES, 52 category: VIDEO_CATEGORY.MESSAGES,
49 licence: VIDEO_LICENCE.MESSAGES, 53 licence: VIDEO_LICENCE.MESSAGES,
50 language: VIDEO_LANGUAGE.MESSAGES, 54 language: VIDEO_LANGUAGE.MESSAGES,
@@ -67,6 +71,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
67 buildForm () { 71 buildForm () {
68 this.form = this.formBuilder.group({ 72 this.form = this.formBuilder.group({
69 name: [ '', VIDEO_NAME.VALIDATORS ], 73 name: [ '', VIDEO_NAME.VALIDATORS ],
74 privacy: [ '', VIDEO_PRIVACY.VALIDATORS ],
70 nsfw: [ false ], 75 nsfw: [ false ],
71 category: [ '', VIDEO_CATEGORY.VALIDATORS ], 76 category: [ '', VIDEO_CATEGORY.VALIDATORS ],
72 licence: [ '', VIDEO_LICENCE.VALIDATORS ], 77 licence: [ '', VIDEO_LICENCE.VALIDATORS ],
@@ -84,6 +89,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
84 this.videoCategories = this.serverService.getVideoCategories() 89 this.videoCategories = this.serverService.getVideoCategories()
85 this.videoLicences = this.serverService.getVideoLicences() 90 this.videoLicences = this.serverService.getVideoLicences()
86 this.videoLanguages = this.serverService.getVideoLanguages() 91 this.videoLanguages = this.serverService.getVideoLanguages()
92 this.videoPrivacies = this.serverService.getVideoPrivacies()
87 93
88 const uuid: string = this.route.snapshot.params['uuid'] 94 const uuid: string = this.route.snapshot.params['uuid']
89 95
@@ -98,6 +104,16 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
98 video => { 104 video => {
99 this.video = new VideoEdit(video) 105 this.video = new VideoEdit(video)
100 106
107 // We cannot set private a video that was not private anymore
108 if (video.privacy !== VideoPrivacy.PRIVATE) {
109 const newVideoPrivacies = []
110 for (const p of this.videoPrivacies) {
111 if (p.id !== VideoPrivacy.PRIVATE) newVideoPrivacies.push(p)
112 }
113
114 this.videoPrivacies = newVideoPrivacies
115 }
116
101 this.hydrateFormFromVideo() 117 this.hydrateFormFromVideo()
102 }, 118 },
103 119
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 @@
22 <div *ngIf="videoNotFound" id="video-not-found">Video not found :'(</div> 22 <div *ngIf="videoNotFound" id="video-not-found">Video not found :'(</div>
23</div> 23</div>
24 24
25<!-- P2P informations --> 25<!-- P2P information -->
26<div id="torrent-info" class="row"> 26<div id="torrent-info" class="row">
27 <div id="torrent-info-download" class="col-md-4 col-sm-4 col-xs-4">Download: {{ downloadSpeed | bytes }}/s</div> 27 <div id="torrent-info-download" class="col-md-4 col-sm-4 col-xs-4">Download: {{ downloadSpeed | bytes }}/s</div>
28 <div id="torrent-info-upload" class="col-md-4 col-sm-4 col-xs-4">Upload: {{ uploadSpeed | bytes }}/s</div> 28 <div id="torrent-info-upload" class="col-md-4 col-sm-4 col-xs-4">Upload: {{ uploadSpeed | bytes }}/s</div>
@@ -144,6 +144,15 @@
144 <div class="video-details-attributes col-xs-4 col-md-3"> 144 <div class="video-details-attributes col-xs-4 col-md-3">
145 <div class="video-details-attribute"> 145 <div class="video-details-attribute">
146 <span class="video-details-attribute-label"> 146 <span class="video-details-attribute-label">
147 Privacy:
148 </span>
149 <span class="video-details-attribute-value">
150 {{ video.privacyLabel }}
151 </span>
152 </div>
153
154 <div class="video-details-attribute">
155 <span class="video-details-attribute-label">
147 Category: 156 Category:
148 </span> 157 </span>
149 <span class="video-details-attribute-value"> 158 <span class="video-details-attribute-value">
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 {
5 VideoFile, 5 VideoFile,
6 VideoChannel, 6 VideoChannel,
7 VideoResolution, 7 VideoResolution,
8 UserRight 8 UserRight,
9 VideoPrivacy
9} from '../../../../../shared' 10} from '../../../../../shared'
10 11
11export class VideoDetails extends Video implements VideoDetailsServerModel { 12export class VideoDetails extends Video implements VideoDetailsServerModel {
@@ -41,10 +42,14 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
41 descriptionPath: string 42 descriptionPath: string
42 files: VideoFile[] 43 files: VideoFile[]
43 channel: VideoChannel 44 channel: VideoChannel
45 privacy: VideoPrivacy
46 privacyLabel: string
44 47
45 constructor (hash: VideoDetailsServerModel) { 48 constructor (hash: VideoDetailsServerModel) {
46 super(hash) 49 super(hash)
47 50
51 this.privacy = hash.privacy
52 this.privacyLabel = hash.privacyLabel
48 this.descriptionPath = hash.descriptionPath 53 this.descriptionPath = hash.descriptionPath
49 this.files = hash.files 54 this.files = hash.files
50 this.channel = hash.channel 55 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 @@
1import { VideoDetails } from './video-details.model' 1import { VideoDetails } from './video-details.model'
2import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy.enum'
2 3
3export class VideoEdit { 4export class VideoEdit {
4 category: number 5 category: number
@@ -9,6 +10,7 @@ export class VideoEdit {
9 tags: string[] 10 tags: string[]
10 nsfw: boolean 11 nsfw: boolean
11 channel: number 12 channel: number
13 privacy: VideoPrivacy
12 uuid?: string 14 uuid?: string
13 id?: number 15 id?: number
14 16
@@ -23,6 +25,7 @@ export class VideoEdit {
23 this.tags = videoDetails.tags 25 this.tags = videoDetails.tags
24 this.nsfw = videoDetails.nsfw 26 this.nsfw = videoDetails.nsfw
25 this.channel = videoDetails.channel.id 27 this.channel = videoDetails.channel.id
28 this.privacy = videoDetails.privacy
26 } 29 }
27 30
28 patch (values: Object) { 31 patch (values: Object) {
@@ -40,7 +43,8 @@ export class VideoEdit {
40 name: this.name, 43 name: this.name,
41 tags: this.tags, 44 tags: this.tags,
42 nsfw: this.nsfw, 45 nsfw: this.nsfw,
43 channel: this.channel 46 channel: this.channel,
47 privacy: this.privacy
44 } 48 }
45 } 49 }
46} 50}
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 {
19 UserVideoRate, 19 UserVideoRate,
20 VideoRateType, 20 VideoRateType,
21 VideoUpdate, 21 VideoUpdate,
22 VideoAbuseCreate,
23 UserVideoRateUpdate, 22 UserVideoRateUpdate,
24 Video as VideoServerModel, 23 Video as VideoServerModel,
25 VideoDetails as VideoDetailsServerModel, 24 VideoDetails as VideoDetailsServerModel,
@@ -51,6 +50,7 @@ export class VideoService {
51 licence: video.licence, 50 licence: video.licence,
52 language, 51 language,
53 description: video.description, 52 description: video.description,
53 privacy: video.privacy,
54 tags: video.tags, 54 tags: video.tags,
55 nsfw: video.nsfw 55 nsfw: video.nsfw
56 } 56 }
@@ -63,22 +63,35 @@ export class VideoService {
63 uploadVideo (video: FormData) { 63 uploadVideo (video: FormData) {
64 const req = new HttpRequest('POST', VideoService.BASE_VIDEO_URL + 'upload', video, { reportProgress: true }) 64 const req = new HttpRequest('POST', VideoService.BASE_VIDEO_URL + 'upload', video, { reportProgress: true })
65 65
66 return this.authHttp.request(req) 66 return this.authHttp
67 .catch(this.restExtractor.handleError) 67 .request(req)
68 .catch(this.restExtractor.handleError)
68 } 69 }
69 70
70 getVideos (videoPagination: VideoPagination, sort: SortField) { 71 getMyVideos (videoPagination: VideoPagination, sort: SortField): Observable<{ videos: Video[], totalVideos: number}> {
71 const pagination = this.videoPaginationToRestPagination(videoPagination) 72 const pagination = this.videoPaginationToRestPagination(videoPagination)
72 73
73 let params = new HttpParams() 74 let params = new HttpParams()
74 params = this.restService.addRestGetParams(params, pagination, sort) 75 params = this.restService.addRestGetParams(params, pagination, sort)
75 76
76 return this.authHttp.get(VideoService.BASE_VIDEO_URL, { params }) 77 return this.authHttp.get(UserService.BASE_USERS_URL + '/me/videos', { params })
77 .map(this.extractVideos) 78 .map(this.extractVideos)
78 .catch((res) => this.restExtractor.handleError(res)) 79 .catch((res) => this.restExtractor.handleError(res))
79 } 80 }
80 81
81 searchVideos (search: Search, videoPagination: VideoPagination, sort: SortField) { 82 getVideos (videoPagination: VideoPagination, sort: SortField): Observable<{ videos: Video[], totalVideos: number}> {
83 const pagination = this.videoPaginationToRestPagination(videoPagination)
84
85 let params = new HttpParams()
86 params = this.restService.addRestGetParams(params, pagination, sort)
87
88 return this.authHttp
89 .get(VideoService.BASE_VIDEO_URL, { params })
90 .map(this.extractVideos)
91 .catch((res) => this.restExtractor.handleError(res))
92 }
93
94 searchVideos (search: Search, videoPagination: VideoPagination, sort: SortField): Observable<{ videos: Video[], totalVideos: number}> {
82 const url = VideoService.BASE_VIDEO_URL + 'search/' + encodeURIComponent(search.value) 95 const url = VideoService.BASE_VIDEO_URL + 'search/' + encodeURIComponent(search.value)
83 96
84 const pagination = this.videoPaginationToRestPagination(videoPagination) 97 const pagination = this.videoPaginationToRestPagination(videoPagination)
@@ -88,15 +101,17 @@ export class VideoService {
88 101
89 if (search.field) params.set('field', search.field) 102 if (search.field) params.set('field', search.field)
90 103
91 return this.authHttp.get<ResultList<VideoServerModel>>(url, { params }) 104 return this.authHttp
92 .map(this.extractVideos) 105 .get<ResultList<VideoServerModel>>(url, { params })
93 .catch((res) => this.restExtractor.handleError(res)) 106 .map(this.extractVideos)
107 .catch((res) => this.restExtractor.handleError(res))
94 } 108 }
95 109
96 removeVideo (id: number) { 110 removeVideo (id: number) {
97 return this.authHttp.delete(VideoService.BASE_VIDEO_URL + id) 111 return this.authHttp
98 .map(this.restExtractor.extractDataBool) 112 .delete(VideoService.BASE_VIDEO_URL + id)
99 .catch((res) => this.restExtractor.handleError(res)) 113 .map(this.restExtractor.extractDataBool)
114 .catch((res) => this.restExtractor.handleError(res))
100 } 115 }
101 116
102 loadCompleteDescription (descriptionPath: string) { 117 loadCompleteDescription (descriptionPath: string) {
@@ -117,8 +132,9 @@ export class VideoService {
117 getUserVideoRating (id: number): Observable<UserVideoRate> { 132 getUserVideoRating (id: number): Observable<UserVideoRate> {
118 const url = UserService.BASE_USERS_URL + 'me/videos/' + id + '/rating' 133 const url = UserService.BASE_USERS_URL + 'me/videos/' + id + '/rating'
119 134
120 return this.authHttp.get(url) 135 return this.authHttp
121 .catch(res => this.restExtractor.handleError(res)) 136 .get(url)
137 .catch(res => this.restExtractor.handleError(res))
122 } 138 }
123 139
124 private videoPaginationToRestPagination (videoPagination: VideoPagination) { 140 private videoPaginationToRestPagination (videoPagination: VideoPagination) {
@@ -134,9 +150,10 @@ export class VideoService {
134 rating: rateType 150 rating: rateType
135 } 151 }
136 152
137 return this.authHttp.put(url, body) 153 return this.authHttp
138 .map(this.restExtractor.extractDataBool) 154 .put(url, body)
139 .catch(res => this.restExtractor.handleError(res)) 155 .map(this.restExtractor.extractDataBool)
156 .catch(res => this.restExtractor.handleError(res))
140 } 157 }
141 158
142 private extractVideos (result: ResultList<VideoServerModel>) { 159 private extractVideos (result: ResultList<VideoServerModel>) {
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 @@
1export * from './loader.component' 1export * from './my-videos.component'
2export * from './video-list.component' 2export * from './video-list.component'
3export * from './video-miniature.component' 3export * from './shared'
4export * from './video-sort.component'
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 @@
1import { Component, OnDestroy, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router'
3
4import { NotificationsService } from 'angular2-notifications'
5
6import { AbstractVideoList } from './shared'
7import { VideoService } from '../shared'
8
9@Component({
10 selector: 'my-videos',
11 styleUrls: [ './shared/abstract-video-list.scss' ],
12 templateUrl: './shared/abstract-video-list.html'
13})
14export class MyVideosComponent extends AbstractVideoList implements OnInit, OnDestroy {
15
16 constructor (
17 protected router: Router,
18 protected route: ActivatedRoute,
19 protected notificationsService: NotificationsService,
20 private videoService: VideoService
21 ) {
22 super()
23 }
24
25 ngOnInit () {
26 super.ngOnInit()
27 }
28
29 ngOnDestroy () {
30 this.subActivatedRoute.unsubscribe()
31 }
32
33 getVideosObservable () {
34 return this.videoService.getMyVideos(this.pagination, this.sort)
35 }
36}
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
index 680fba3f5..680fba3f5 100644
--- a/client/src/app/videos/video-list/video-list.component.html
+++ b/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
index 4b4409602..4b4409602 100644
--- a/client/src/app/videos/video-list/video-list.component.scss
+++ b/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 @@
1import { OnDestroy, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router'
3import { Subscription } from 'rxjs/Subscription'
4import { BehaviorSubject } from 'rxjs/BehaviorSubject'
5import { Observable } from 'rxjs/Observable'
6
7import { NotificationsService } from 'angular2-notifications'
8
9import {
10 SortField,
11 Video,
12 VideoPagination
13} from '../../shared'
14
15export abstract class AbstractVideoList implements OnInit, OnDestroy {
16 loading: BehaviorSubject<boolean> = new BehaviorSubject(false)
17 pagination: VideoPagination = {
18 currentPage: 1,
19 itemsPerPage: 25,
20 totalItems: null
21 }
22 sort: SortField
23 videos: Video[] = []
24
25 protected notificationsService: NotificationsService
26 protected router: Router
27 protected route: ActivatedRoute
28
29 protected subActivatedRoute: Subscription
30
31 abstract getVideosObservable (): Observable<{ videos: Video[], totalVideos: number}>
32
33 ngOnInit () {
34 // Subscribe to route changes
35 this.subActivatedRoute = this.route.params.subscribe(routeParams => {
36 this.loadRouteParams(routeParams)
37
38 this.getVideos()
39 })
40 }
41
42 ngOnDestroy () {
43 this.subActivatedRoute.unsubscribe()
44 }
45
46 getVideos () {
47 this.loading.next(true)
48 this.videos = []
49
50 const observable = this.getVideosObservable()
51
52 observable.subscribe(
53 ({ videos, totalVideos }) => {
54 this.videos = videos
55 this.pagination.totalItems = totalVideos
56
57 this.loading.next(false)
58 },
59 error => this.notificationsService.error('Error', error.text)
60 )
61 }
62
63 isThereNoVideo () {
64 return !this.loading.getValue() && this.videos.length === 0
65 }
66
67 onPageChanged (event: { page: number }) {
68 // Be sure the current page is set
69 this.pagination.currentPage = event.page
70
71 this.navigateToNewParams()
72 }
73
74 onSort (sort: SortField) {
75 this.sort = sort
76
77 this.navigateToNewParams()
78 }
79
80 protected buildRouteParams () {
81 // There is always a sort and a current page
82 const params = {
83 sort: this.sort,
84 page: this.pagination.currentPage
85 }
86
87 return params
88 }
89
90 protected loadRouteParams (routeParams: { [ key: string ]: any }) {
91 this.sort = routeParams['sort'] as SortField || '-createdAt'
92
93 if (routeParams['page'] !== undefined) {
94 this.pagination.currentPage = parseInt(routeParams['page'], 10)
95 } else {
96 this.pagination.currentPage = 1
97 }
98 }
99
100 protected navigateToNewParams () {
101 const routeParams = this.buildRouteParams()
102 this.router.navigate([ '/videos/list', routeParams ])
103 }
104}
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 @@
1export * from './abstract-video-list'
2export * from './loader.component'
3export * from './video-miniature.component'
4export * 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
index 38d06950e..38d06950e 100644
--- a/client/src/app/videos/video-list/loader.component.html
+++ b/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
index f37d70c85..f37d70c85 100644
--- a/client/src/app/videos/video-list/loader.component.ts
+++ b/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
index abe87025f..abe87025f 100644
--- a/client/src/app/videos/video-list/video-miniature.component.html
+++ b/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
index 066792d10..066792d10 100644
--- a/client/src/app/videos/video-list/video-miniature.component.scss
+++ b/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
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 @@
1import { Component, Input } from '@angular/core' 1import { Component, Input } from '@angular/core'
2 2
3import { SortField, Video } from '../shared' 3import { SortField, Video } from '../../shared'
4import { User } from '../../shared' 4import { User } from '../../../shared'
5 5
6@Component({ 6@Component({
7 selector: 'my-video-miniature', 7 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
index 3bece0b22..3bece0b22 100644
--- a/client/src/app/videos/video-list/video-sort.component.html
+++ b/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
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 @@
1import { Component, EventEmitter, Input, Output } from '@angular/core' 1import { Component, EventEmitter, Input, Output } from '@angular/core'
2 2
3import { SortField } from '../shared' 3import { SortField } from '../../shared'
4 4
5@Component({ 5@Component({
6 selector: 'my-video-sort', 6 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 @@
1import { Component, OnDestroy, OnInit } from '@angular/core' 1import { Component, OnDestroy, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router' 2import { ActivatedRoute, Router } from '@angular/router'
3import { Subscription } from 'rxjs/Subscription' 3import { Subscription } from 'rxjs/Subscription'
4import { BehaviorSubject } from 'rxjs/BehaviorSubject'
5 4
6import { NotificationsService } from 'angular2-notifications' 5import { NotificationsService } from 'angular2-notifications'
7 6
8import { AuthService } from '../../core' 7import { VideoService } from '../shared'
9import { 8import { Search, SearchField, SearchService } from '../../shared'
10 SortField, 9import { AbstractVideoList } from './shared'
11 Video,
12 VideoService,
13 VideoPagination
14} from '../shared'
15import { Search, SearchField, SearchService, User } from '../../shared'
16 10
17@Component({ 11@Component({
18 selector: 'my-videos-list', 12 selector: 'my-videos-list',
19 styleUrls: [ './video-list.component.scss' ], 13 styleUrls: [ './shared/abstract-video-list.scss' ],
20 templateUrl: './video-list.component.html' 14 templateUrl: './shared/abstract-video-list.html'
21}) 15})
22export class VideoListComponent implements OnInit, OnDestroy { 16export class VideoListComponent extends AbstractVideoList implements OnInit, OnDestroy {
23 loading: BehaviorSubject<boolean> = new BehaviorSubject(false)
24 pagination: VideoPagination = {
25 currentPage: 1,
26 itemsPerPage: 25,
27 totalItems: null
28 }
29 sort: SortField
30 user: User
31 videos: Video[] = []
32
33 private search: Search 17 private search: Search
34 private subActivatedRoute: Subscription
35 private subSearch: Subscription 18 private subSearch: Subscription
36 19
37 constructor ( 20 constructor (
38 private authService: AuthService, 21 protected router: Router,
39 private notificationsService: NotificationsService, 22 protected route: ActivatedRoute,
40 private router: Router, 23 protected notificationsService: NotificationsService,
41 private route: ActivatedRoute,
42 private videoService: VideoService, 24 private videoService: VideoService,
43 private searchService: SearchService 25 private searchService: SearchService
44 ) {} 26 ) {
27 super()
28 }
45 29
46 ngOnInit () { 30 ngOnInit () {
47 this.user = this.authService.getUser()
48
49 // Subscribe to route changes 31 // Subscribe to route changes
50 this.subActivatedRoute = this.route.params.subscribe(routeParams => { 32 this.subActivatedRoute = this.route.params.subscribe(routeParams => {
51 this.loadRouteParams(routeParams) 33 this.loadRouteParams(routeParams)
@@ -66,14 +48,12 @@ export class VideoListComponent implements OnInit, OnDestroy {
66 } 48 }
67 49
68 ngOnDestroy () { 50 ngOnDestroy () {
69 this.subActivatedRoute.unsubscribe() 51 super.ngOnDestroy()
52
70 this.subSearch.unsubscribe() 53 this.subSearch.unsubscribe()
71 } 54 }
72 55
73 getVideos () { 56 getVideosObservable () {
74 this.loading.next(true)
75 this.videos = []
76
77 let observable = null 57 let observable = null
78 if (this.search.value) { 58 if (this.search.value) {
79 observable = this.videoService.searchVideos(this.search, this.pagination, this.sort) 59 observable = this.videoService.searchVideos(this.search, this.pagination, this.sort)
@@ -81,40 +61,11 @@ export class VideoListComponent implements OnInit, OnDestroy {
81 observable = this.videoService.getVideos(this.pagination, this.sort) 61 observable = this.videoService.getVideos(this.pagination, this.sort)
82 } 62 }
83 63
84 observable.subscribe( 64 return observable
85 ({ videos, totalVideos }) => {
86 this.videos = videos
87 this.pagination.totalItems = totalVideos
88
89 this.loading.next(false)
90 },
91 error => this.notificationsService.error('Error', error.text)
92 )
93 }
94
95 isThereNoVideo () {
96 return !this.loading.getValue() && this.videos.length === 0
97 }
98
99 onPageChanged (event: { page: number }) {
100 // Be sure the current page is set
101 this.pagination.currentPage = event.page
102
103 this.navigateToNewParams()
104 } 65 }
105 66
106 onSort (sort: SortField) { 67 protected buildRouteParams () {
107 this.sort = sort 68 const params = super.buildRouteParams()
108
109 this.navigateToNewParams()
110 }
111
112 private buildRouteParams () {
113 // There is always a sort and a current page
114 const params = {
115 sort: this.sort,
116 page: this.pagination.currentPage
117 }
118 69
119 // Maybe there is a search 70 // Maybe there is a search
120 if (this.search.value) { 71 if (this.search.value) {
@@ -125,7 +76,9 @@ export class VideoListComponent implements OnInit, OnDestroy {
125 return params 76 return params
126 } 77 }
127 78
128 private loadRouteParams (routeParams: { [ key: string ]: any }) { 79 protected loadRouteParams (routeParams: { [ key: string ]: any }) {
80 super.loadRouteParams(routeParams)
81
129 if (routeParams['search'] !== undefined) { 82 if (routeParams['search'] !== undefined) {
130 this.search = { 83 this.search = {
131 value: routeParams['search'], 84 value: routeParams['search'],
@@ -137,18 +90,5 @@ export class VideoListComponent implements OnInit, OnDestroy {
137 field: 'name' 90 field: 'name'
138 } 91 }
139 } 92 }
140
141 this.sort = routeParams['sort'] as SortField || '-createdAt'
142
143 if (routeParams['page'] !== undefined) {
144 this.pagination.currentPage = parseInt(routeParams['page'], 10)
145 } else {
146 this.pagination.currentPage = 1
147 }
148 }
149
150 private navigateToNewParams () {
151 const routeParams = this.buildRouteParams()
152 this.router.navigate([ '/videos/list', routeParams ])
153 } 93 }
154} 94}
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'
3 3
4import { MetaGuard } from '@ngx-meta/core' 4import { MetaGuard } from '@ngx-meta/core'
5 5
6import { VideoListComponent } from './video-list' 6import { VideoListComponent, MyVideosComponent } from './video-list'
7import { VideosComponent } from './videos.component' 7import { VideosComponent } from './videos.component'
8 8
9const videosRoutes: Routes = [ 9const videosRoutes: Routes = [
@@ -13,6 +13,15 @@ const videosRoutes: Routes = [
13 canActivateChild: [ MetaGuard ], 13 canActivateChild: [ MetaGuard ],
14 children: [ 14 children: [
15 { 15 {
16 path: 'mine',
17 component: MyVideosComponent,
18 data: {
19 meta: {
20 title: 'My videos'
21 }
22 }
23 },
24 {
16 path: 'list', 25 path: 'list',
17 component: VideoListComponent, 26 component: VideoListComponent,
18 data: { 27 data: {
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'
2 2
3import { VideosRoutingModule } from './videos-routing.module' 3import { VideosRoutingModule } from './videos-routing.module'
4import { VideosComponent } from './videos.component' 4import { VideosComponent } from './videos.component'
5import { LoaderComponent, VideoListComponent, VideoMiniatureComponent, VideoSortComponent } from './video-list' 5import {
6 LoaderComponent,
7 VideoListComponent,
8 MyVideosComponent,
9 VideoMiniatureComponent,
10 VideoSortComponent
11} from './video-list'
6import { VideoService } from './shared' 12import { VideoService } from './shared'
7import { SharedModule } from '../shared' 13import { SharedModule } from '../shared'
8 14
@@ -16,6 +22,7 @@ import { SharedModule } from '../shared'
16 VideosComponent, 22 VideosComponent,
17 23
18 VideoListComponent, 24 VideoListComponent,
25 MyVideosComponent,
19 VideoMiniatureComponent, 26 VideoMiniatureComponent,
20 VideoSortComponent, 27 VideoSortComponent,
21 28
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%);
334 334
335// Thanks: https://projects.lukehaas.me/css-loaders/ 335// Thanks: https://projects.lukehaas.me/css-loaders/
336.vjs-loading-spinner { 336.vjs-loading-spinner {
337 border: none; 337 margin: -25px 0 0 -25px;
338 opacity: 1; 338 position: absolute;
339 top: 50%;
340 left: 50%;
339 font-size: 10px; 341 font-size: 10px;
340 text-indent: -9999em;
341 width: 5em;
342 height: 5em;
343 border-radius: 50%;
344 background: #ffffff;
345 background: -moz-linear-gradient(left, #ffffff 10%, rgba(255, 255, 255, 0) 42%);
346 background: -webkit-linear-gradient(left, #ffffff 10%, rgba(255, 255, 255, 0) 42%);
347 background: -o-linear-gradient(left, #ffffff 10%, rgba(255, 255, 255, 0) 42%);
348 background: -ms-linear-gradient(left, #ffffff 10%, rgba(255, 255, 255, 0) 42%);
349 background: linear-gradient(to right, #ffffff 10%, rgba(255, 255, 255, 0) 42%);
350 position: relative; 342 position: relative;
351 -webkit-animation: load3 1.4s infinite linear; 343 text-indent: -9999em;
352 animation: load3 1.4s infinite linear; 344 border: 0.7em solid rgba(255, 255, 255, 0.2);
353 -webkit-transform: translateZ(0); 345 border-left-color: #ffffff;
354 -ms-transform: translateZ(0);
355 transform: translateZ(0); 346 transform: translateZ(0);
347 animation: spinner 1.4s infinite linear;
356 348
357 &:before { 349 &:before {
358 width: 50%;
359 height: 50%;
360 background: #ffffff;
361 border-radius: 100% 0 0 0;
362 position: absolute;
363 top: 0;
364 left: 0;
365 content: '';
366 animation: none !important; 350 animation: none !important;
367 margin: 0 !important;
368 } 351 }
369 352
370 &:after { 353 &:after {
371 background: #000;
372 width: 75%;
373 height: 75%;
374 border-radius: 50%; 354 border-radius: 50%;
375 content: ''; 355 width: 6em;
376 margin: auto; 356 height: 6em;
377 position: absolute;
378 top: 0;
379 left: 0;
380 bottom: 0;
381 right: 0;
382 animation: none !important; 357 animation: none !important;
383 } 358 }
384 359
385 @-webkit-keyframes load3 { 360 @keyframes spinner {
386 0% {
387 -webkit-transform: rotate(0deg);
388 transform: rotate(0deg);
389 }
390 100% {
391 -webkit-transform: rotate(360deg);
392 transform: rotate(360deg);
393 }
394 }
395 @keyframes load3 {
396 0% { 361 0% {
397 -webkit-transform: rotate(0deg);
398 transform: rotate(0deg); 362 transform: rotate(0deg);
399 } 363 }
400 100% { 364 100% {
401 -webkit-transform: rotate(360deg);
402 transform: rotate(360deg); 365 transform: rotate(360deg);
403 } 366 }
404 } 367 }
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
267 views: videoToCreateData.views, 267 views: videoToCreateData.views,
268 likes: videoToCreateData.likes, 268 likes: videoToCreateData.likes,
269 dislikes: videoToCreateData.dislikes, 269 dislikes: videoToCreateData.dislikes,
270 remote: true 270 remote: true,
271 privacy: videoToCreateData.privacy
271 } 272 }
272 273
273 const video = db.Video.build(videoData) 274 const video = db.Video.build(videoData)
@@ -334,6 +335,7 @@ async function updateRemoteVideo (videoAttributesToUpdate: RemoteVideoUpdateData
334 videoInstance.set('views', videoAttributesToUpdate.views) 335 videoInstance.set('views', videoAttributesToUpdate.views)
335 videoInstance.set('likes', videoAttributesToUpdate.likes) 336 videoInstance.set('likes', videoAttributesToUpdate.likes)
336 videoInstance.set('dislikes', videoAttributesToUpdate.dislikes) 337 videoInstance.set('dislikes', videoAttributesToUpdate.dislikes)
338 videoInstance.set('privacy', videoAttributesToUpdate.privacy)
337 339
338 await videoInstance.save(sequelizeOptions) 340 await videoInstance.save(sequelizeOptions)
339 341
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 {
30} from '../../../shared' 30} from '../../../shared'
31import { createUserAuthorAndChannel } from '../../lib' 31import { createUserAuthorAndChannel } from '../../lib'
32import { UserInstance } from '../../models' 32import { UserInstance } from '../../models'
33import { videosSortValidator } from '../../middlewares/validators/sort'
34import { setVideosSort } from '../../middlewares/sort'
33 35
34const usersRouter = express.Router() 36const usersRouter = express.Router()
35 37
@@ -38,6 +40,15 @@ usersRouter.get('/me',
38 asyncMiddleware(getUserInformation) 40 asyncMiddleware(getUserInformation)
39) 41)
40 42
43usersRouter.get('/me/videos',
44 authenticate,
45 paginationValidator,
46 videosSortValidator,
47 setVideosSort,
48 setPagination,
49 asyncMiddleware(getUserVideos)
50)
51
41usersRouter.get('/me/videos/:videoId/rating', 52usersRouter.get('/me/videos/:videoId/rating',
42 authenticate, 53 authenticate,
43 usersVideoRatingValidator, 54 usersVideoRatingValidator,
@@ -101,6 +112,13 @@ export {
101 112
102// --------------------------------------------------------------------------- 113// ---------------------------------------------------------------------------
103 114
115async function getUserVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
116 const user = res.locals.oauth.token.User
117 const resultList = await db.Video.listUserVideosForApi(user.id ,req.query.start, req.query.count, req.query.sort)
118
119 return res.json(getFormattedObjects(resultList.data, resultList.total))
120}
121
104async function createUserRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) { 122async function createUserRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) {
105 const options = { 123 const options = {
106 arguments: [ req, res ], 124 arguments: [ req, res ],
@@ -146,13 +164,14 @@ async function registerUser (req: express.Request, res: express.Response, next:
146} 164}
147 165
148async function getUserInformation (req: express.Request, res: express.Response, next: express.NextFunction) { 166async function getUserInformation (req: express.Request, res: express.Response, next: express.NextFunction) {
167 // We did not load channels in res.locals.user
149 const user = await db.User.loadByUsernameAndPopulateChannels(res.locals.oauth.token.user.username) 168 const user = await db.User.loadByUsernameAndPopulateChannels(res.locals.oauth.token.user.username)
150 169
151 return res.json(user.toFormattedJSON()) 170 return res.json(user.toFormattedJSON())
152} 171}
153 172
154function getUser (req: express.Request, res: express.Response, next: express.NextFunction) { 173function getUser (req: express.Request, res: express.Response, next: express.NextFunction) {
155 return res.json(res.locals.user.toFormattedJSON()) 174 return res.json(res.locals.oauth.token.User.toFormattedJSON())
156} 175}
157 176
158async function getUserVideoRating (req: express.Request, res: express.Response, next: express.NextFunction) { 177async 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 {
9 REQUEST_VIDEO_EVENT_TYPES, 9 REQUEST_VIDEO_EVENT_TYPES,
10 VIDEO_CATEGORIES, 10 VIDEO_CATEGORIES,
11 VIDEO_LICENCES, 11 VIDEO_LICENCES,
12 VIDEO_LANGUAGES 12 VIDEO_LANGUAGES,
13 VIDEO_PRIVACIES
13} from '../../../initializers' 14} from '../../../initializers'
14import { 15import {
15 addEventToRemoteVideo, 16 addEventToRemoteVideo,
@@ -43,7 +44,7 @@ import {
43 resetSequelizeInstance 44 resetSequelizeInstance
44} from '../../../helpers' 45} from '../../../helpers'
45import { VideoInstance } from '../../../models' 46import { VideoInstance } from '../../../models'
46import { VideoCreate, VideoUpdate } from '../../../../shared' 47import { VideoCreate, VideoUpdate, VideoPrivacy } from '../../../../shared'
47 48
48import { abuseVideoRouter } from './abuse' 49import { abuseVideoRouter } from './abuse'
49import { blacklistRouter } from './blacklist' 50import { blacklistRouter } from './blacklist'
@@ -84,6 +85,7 @@ videosRouter.use('/', videoChannelRouter)
84videosRouter.get('/categories', listVideoCategories) 85videosRouter.get('/categories', listVideoCategories)
85videosRouter.get('/licences', listVideoLicences) 86videosRouter.get('/licences', listVideoLicences)
86videosRouter.get('/languages', listVideoLanguages) 87videosRouter.get('/languages', listVideoLanguages)
88videosRouter.get('/privacies', listVideoPrivacies)
87 89
88videosRouter.get('/', 90videosRouter.get('/',
89 paginationValidator, 91 paginationValidator,
@@ -149,6 +151,10 @@ function listVideoLanguages (req: express.Request, res: express.Response) {
149 res.json(VIDEO_LANGUAGES) 151 res.json(VIDEO_LANGUAGES)
150} 152}
151 153
154function listVideoPrivacies (req: express.Request, res: express.Response) {
155 res.json(VIDEO_PRIVACIES)
156}
157
152// Wrapper to video add that retry the function if there is a database error 158// Wrapper to video add that retry the function if there is a database error
153// We need this because we run the transaction in SERIALIZABLE isolation that can fail 159// We need this because we run the transaction in SERIALIZABLE isolation that can fail
154async function addVideoRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) { 160async 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
179 language: videoInfo.language, 185 language: videoInfo.language,
180 nsfw: videoInfo.nsfw, 186 nsfw: videoInfo.nsfw,
181 description: videoInfo.description, 187 description: videoInfo.description,
188 privacy: videoInfo.privacy,
182 duration: videoPhysicalFile['duration'], // duration was added by a previous middleware 189 duration: videoPhysicalFile['duration'], // duration was added by a previous middleware
183 channelId: res.locals.videoChannel.id 190 channelId: res.locals.videoChannel.id
184 } 191 }
@@ -240,6 +247,8 @@ async function addVideo (req: express.Request, res: express.Response, videoPhysi
240 247
241 // Let transcoding job send the video to friends because the video file extension might change 248 // Let transcoding job send the video to friends because the video file extension might change
242 if (CONFIG.TRANSCODING.ENABLED === true) return undefined 249 if (CONFIG.TRANSCODING.ENABLED === true) return undefined
250 // Don't send video to remote pods, it is private
251 if (video.privacy === VideoPrivacy.PRIVATE) return undefined
243 252
244 const remoteVideo = await video.toAddRemoteJSON() 253 const remoteVideo = await video.toAddRemoteJSON()
245 // Now we'll add the video's meta data to our friends 254 // 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) {
264 const videoInstance = res.locals.video 273 const videoInstance = res.locals.video
265 const videoFieldsSave = videoInstance.toJSON() 274 const videoFieldsSave = videoInstance.toJSON()
266 const videoInfoToUpdate: VideoUpdate = req.body 275 const videoInfoToUpdate: VideoUpdate = req.body
276 const wasPrivateVideo = videoInstance.privacy === VideoPrivacy.PRIVATE
267 277
268 try { 278 try {
269 await db.sequelize.transaction(async t => { 279 await db.sequelize.transaction(async t => {
@@ -276,6 +286,7 @@ async function updateVideo (req: express.Request, res: express.Response) {
276 if (videoInfoToUpdate.licence !== undefined) videoInstance.set('licence', videoInfoToUpdate.licence) 286 if (videoInfoToUpdate.licence !== undefined) videoInstance.set('licence', videoInfoToUpdate.licence)
277 if (videoInfoToUpdate.language !== undefined) videoInstance.set('language', videoInfoToUpdate.language) 287 if (videoInfoToUpdate.language !== undefined) videoInstance.set('language', videoInfoToUpdate.language)
278 if (videoInfoToUpdate.nsfw !== undefined) videoInstance.set('nsfw', videoInfoToUpdate.nsfw) 288 if (videoInfoToUpdate.nsfw !== undefined) videoInstance.set('nsfw', videoInfoToUpdate.nsfw)
289 if (videoInfoToUpdate.privacy !== undefined) videoInstance.set('privacy', videoInfoToUpdate.privacy)
279 if (videoInfoToUpdate.description !== undefined) videoInstance.set('description', videoInfoToUpdate.description) 290 if (videoInfoToUpdate.description !== undefined) videoInstance.set('description', videoInfoToUpdate.description)
280 291
281 await videoInstance.save(sequelizeOptions) 292 await videoInstance.save(sequelizeOptions)
@@ -287,10 +298,17 @@ async function updateVideo (req: express.Request, res: express.Response) {
287 videoInstance.Tags = tagInstances 298 videoInstance.Tags = tagInstances
288 } 299 }
289 300
290 const json = videoInstance.toUpdateRemoteJSON()
291
292 // Now we'll update the video's meta data to our friends 301 // Now we'll update the video's meta data to our friends
293 return updateVideoToFriends(json, t) 302 if (wasPrivateVideo === false) {
303 const json = videoInstance.toUpdateRemoteJSON()
304 return updateVideoToFriends(json, t)
305 }
306
307 // Video is not private anymore, send a create action to remote pods
308 if (wasPrivateVideo === true && videoInstance.privacy !== VideoPrivacy.PRIVATE) {
309 const remoteVideo = await videoInstance.toAddRemoteJSON()
310 return addVideoToFriends(remoteVideo, t)
311 }
294 }) 312 })
295 313
296 logger.info('Video with name %s and uuid %s updated.', videoInstance.name, videoInstance.uuid) 314 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 {
11 VIDEO_LICENCES, 11 VIDEO_LICENCES,
12 VIDEO_LANGUAGES, 12 VIDEO_LANGUAGES,
13 VIDEO_RATE_TYPES, 13 VIDEO_RATE_TYPES,
14 VIDEO_PRIVACIES,
14 database as db 15 database as db
15} from '../../initializers' 16} from '../../initializers'
16import { isUserUsernameValid } from './users' 17import { isUserUsernameValid } from './users'
@@ -36,6 +37,15 @@ function isVideoLicenceValid (value: number) {
36 return VIDEO_LICENCES[value] !== undefined 37 return VIDEO_LICENCES[value] !== undefined
37} 38}
38 39
40function isVideoPrivacyValid (value: string) {
41 return VIDEO_PRIVACIES[value] !== undefined
42}
43
44// Maybe we don't know the remote privacy setting, but that doesn't matter
45function isRemoteVideoPrivacyValid (value: string) {
46 return validator.isInt('' + value)
47}
48
39// Maybe we don't know the remote licence, but that doesn't matter 49// Maybe we don't know the remote licence, but that doesn't matter
40function isRemoteVideoLicenceValid (value: string) { 50function isRemoteVideoLicenceValid (value: string) {
41 return validator.isInt('' + value) 51 return validator.isInt('' + value)
@@ -195,6 +205,8 @@ export {
195 isVideoDislikesValid, 205 isVideoDislikesValid,
196 isVideoEventCountValid, 206 isVideoEventCountValid,
197 isVideoFileSizeValid, 207 isVideoFileSizeValid,
208 isVideoPrivacyValid,
209 isRemoteVideoPrivacyValid,
198 isVideoFileResolutionValid, 210 isVideoFileResolutionValid,
199 checkVideoExists, 211 checkVideoExists,
200 isRemoteVideoCategoryValid, 212 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 {
12 RemoteVideoRequestType, 12 RemoteVideoRequestType,
13 JobState 13 JobState
14} from '../../shared/models' 14} from '../../shared/models'
15import { VideoPrivacy } from '../../shared/models/videos/video-privacy.enum'
15 16
16// --------------------------------------------------------------------------- 17// ---------------------------------------------------------------------------
17 18
18const LAST_MIGRATION_VERSION = 90 19const LAST_MIGRATION_VERSION = 95
19 20
20// --------------------------------------------------------------------------- 21// ---------------------------------------------------------------------------
21 22
@@ -196,6 +197,12 @@ const VIDEO_LANGUAGES = {
196 14: 'Italian' 197 14: 'Italian'
197} 198}
198 199
200const VIDEO_PRIVACIES = {
201 [VideoPrivacy.PUBLIC]: 'Public',
202 [VideoPrivacy.UNLISTED]: 'Unlisted',
203 [VideoPrivacy.PRIVATE]: 'Private'
204}
205
199// --------------------------------------------------------------------------- 206// ---------------------------------------------------------------------------
200 207
201// Score a pod has when we create it as a friend 208// Score a pod has when we create it as a friend
@@ -394,6 +401,7 @@ export {
394 THUMBNAILS_SIZE, 401 THUMBNAILS_SIZE,
395 VIDEO_CATEGORIES, 402 VIDEO_CATEGORIES,
396 VIDEO_LANGUAGES, 403 VIDEO_LANGUAGES,
404 VIDEO_PRIVACIES,
397 VIDEO_LICENCES, 405 VIDEO_LICENCES,
398 VIDEO_RATE_TYPES 406 VIDEO_RATE_TYPES
399} 407}
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 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction,
5 queryInterface: Sequelize.QueryInterface,
6 sequelize: Sequelize.Sequelize,
7 db: any
8}): Promise<void> {
9 const q = utils.queryInterface
10
11 const data = {
12 type: Sequelize.INTEGER,
13 defaultValue: null,
14 allowNull: true
15 }
16 await q.addColumn('Videos', 'privacy', data)
17
18 const query = 'UPDATE "Videos" SET "privacy" = 1'
19 const options = {
20 type: Sequelize.QueryTypes.BULKUPDATE
21 }
22 await utils.sequelize.query(query, options)
23
24 data.allowNull = false
25 await q.changeColumn('Videos', 'privacy', data)
26}
27
28function down (options) {
29 throw new Error('Not implemented.')
30}
31
32export {
33 up,
34 down
35}
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 {
20 isVideoRatingTypeValid, 20 isVideoRatingTypeValid,
21 getDurationFromVideoFile, 21 getDurationFromVideoFile,
22 checkVideoExists, 22 checkVideoExists,
23 isIdValid 23 isIdValid,
24 isVideoPrivacyValid
24} from '../../helpers' 25} from '../../helpers'
25import { UserRight } from '../../../shared' 26import { UserRight, VideoPrivacy } from '../../../shared'
26 27
27const videosAddValidator = [ 28const videosAddValidator = [
28 body('videofile').custom((value, { req }) => isVideoFile(req.files)).withMessage( 29 body('videofile').custom((value, { req }) => isVideoFile(req.files)).withMessage(
@@ -36,6 +37,7 @@ const videosAddValidator = [
36 body('nsfw').custom(isVideoNSFWValid).withMessage('Should have a valid NSFW attribute'), 37 body('nsfw').custom(isVideoNSFWValid).withMessage('Should have a valid NSFW attribute'),
37 body('description').custom(isVideoDescriptionValid).withMessage('Should have a valid description'), 38 body('description').custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
38 body('channelId').custom(isIdValid).withMessage('Should have correct video channel id'), 39 body('channelId').custom(isIdValid).withMessage('Should have correct video channel id'),
40 body('privacy').custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
39 body('tags').optional().custom(isVideoTagsValid).withMessage('Should have correct tags'), 41 body('tags').optional().custom(isVideoTagsValid).withMessage('Should have correct tags'),
40 42
41 (req: express.Request, res: express.Response, next: express.NextFunction) => { 43 (req: express.Request, res: express.Response, next: express.NextFunction) => {
@@ -110,6 +112,7 @@ const videosUpdateValidator = [
110 body('licence').optional().custom(isVideoLicenceValid).withMessage('Should have a valid licence'), 112 body('licence').optional().custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
111 body('language').optional().custom(isVideoLanguageValid).withMessage('Should have a valid language'), 113 body('language').optional().custom(isVideoLanguageValid).withMessage('Should have a valid language'),
112 body('nsfw').optional().custom(isVideoNSFWValid).withMessage('Should have a valid NSFW attribute'), 114 body('nsfw').optional().custom(isVideoNSFWValid).withMessage('Should have a valid NSFW attribute'),
115 body('privacy').custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
113 body('description').optional().custom(isVideoDescriptionValid).withMessage('Should have a valid description'), 116 body('description').optional().custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
114 body('tags').optional().custom(isVideoTagsValid).withMessage('Should have correct tags'), 117 body('tags').optional().custom(isVideoTagsValid).withMessage('Should have correct tags'),
115 118
@@ -118,19 +121,27 @@ const videosUpdateValidator = [
118 121
119 checkErrors(req, res, () => { 122 checkErrors(req, res, () => {
120 checkVideoExists(req.params.id, res, () => { 123 checkVideoExists(req.params.id, res, () => {
124 const video = res.locals.video
125
121 // We need to make additional checks 126 // We need to make additional checks
122 if (res.locals.video.isOwned() === false) { 127 if (video.isOwned() === false) {
123 return res.status(403) 128 return res.status(403)
124 .json({ error: 'Cannot update video of another pod' }) 129 .json({ error: 'Cannot update video of another pod' })
125 .end() 130 .end()
126 } 131 }
127 132
128 if (res.locals.video.VideoChannel.Author.userId !== res.locals.oauth.token.User.id) { 133 if (video.VideoChannel.Author.userId !== res.locals.oauth.token.User.id) {
129 return res.status(403) 134 return res.status(403)
130 .json({ error: 'Cannot update video of another user' }) 135 .json({ error: 'Cannot update video of another user' })
131 .end() 136 .end()
132 } 137 }
133 138
139 if (video.privacy !== VideoPrivacy.PRIVATE && req.body.privacy === VideoPrivacy.PRIVATE) {
140 return res.status(409)
141 .json({ error: 'Cannot set "private" a video that was not private anymore.' })
142 .end()
143 }
144
134 next() 145 next()
135 }) 146 })
136 }) 147 })
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 {
49 export type ListOwnedByAuthor = (author: string) => Promise<VideoInstance[]> 49 export type ListOwnedByAuthor = (author: string) => Promise<VideoInstance[]>
50 50
51 export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList<VideoInstance> > 51 export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList<VideoInstance> >
52 export type ListUserVideosForApi = (userId: number, start: number, count: number, sort: string) => Promise< ResultList<VideoInstance> >
52 export type SearchAndPopulateAuthorAndPodAndTags = ( 53 export type SearchAndPopulateAuthorAndPodAndTags = (
53 value: string, 54 value: string,
54 field: string, 55 field: string,
@@ -75,6 +76,7 @@ export interface VideoClass {
75 generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData 76 generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData
76 list: VideoMethods.List 77 list: VideoMethods.List
77 listForApi: VideoMethods.ListForApi 78 listForApi: VideoMethods.ListForApi
79 listUserVideosForApi: VideoMethods.ListUserVideosForApi
78 listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags 80 listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags
79 listOwnedByAuthor: VideoMethods.ListOwnedByAuthor 81 listOwnedByAuthor: VideoMethods.ListOwnedByAuthor
80 load: VideoMethods.Load 82 load: VideoMethods.Load
@@ -97,6 +99,7 @@ export interface VideoAttributes {
97 nsfw: boolean 99 nsfw: boolean
98 description: string 100 description: string
99 duration: number 101 duration: number
102 privacy: number
100 views?: number 103 views?: number
101 likes?: number 104 likes?: number
102 dislikes?: number 105 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 {
18 isVideoNSFWValid, 18 isVideoNSFWValid,
19 isVideoDescriptionValid, 19 isVideoDescriptionValid,
20 isVideoDurationValid, 20 isVideoDurationValid,
21 isVideoPrivacyValid,
21 readFileBufferPromise, 22 readFileBufferPromise,
22 unlinkPromise, 23 unlinkPromise,
23 renamePromise, 24 renamePromise,
@@ -38,10 +39,11 @@ import {
38 THUMBNAILS_SIZE, 39 THUMBNAILS_SIZE,
39 PREVIEWS_SIZE, 40 PREVIEWS_SIZE,
40 CONSTRAINTS_FIELDS, 41 CONSTRAINTS_FIELDS,
41 API_VERSION 42 API_VERSION,
43 VIDEO_PRIVACIES
42} from '../../initializers' 44} from '../../initializers'
43import { removeVideoToFriends } from '../../lib' 45import { removeVideoToFriends } from '../../lib'
44import { VideoResolution } from '../../../shared' 46import { VideoResolution, VideoPrivacy } from '../../../shared'
45import { VideoFileInstance, VideoFileModel } from './video-file-interface' 47import { VideoFileInstance, VideoFileModel } from './video-file-interface'
46 48
47import { addMethodsToModel, getSort } from '../utils' 49import { addMethodsToModel, getSort } from '../utils'
@@ -79,6 +81,7 @@ let getTruncatedDescription: VideoMethods.GetTruncatedDescription
79let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData 81let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData
80let list: VideoMethods.List 82let list: VideoMethods.List
81let listForApi: VideoMethods.ListForApi 83let listForApi: VideoMethods.ListForApi
84let listUserVideosForApi: VideoMethods.ListUserVideosForApi
82let loadByHostAndUUID: VideoMethods.LoadByHostAndUUID 85let loadByHostAndUUID: VideoMethods.LoadByHostAndUUID
83let listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags 86let listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags
84let listOwnedByAuthor: VideoMethods.ListOwnedByAuthor 87let listOwnedByAuthor: VideoMethods.ListOwnedByAuthor
@@ -146,6 +149,16 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
146 } 149 }
147 } 150 }
148 }, 151 },
152 privacy: {
153 type: DataTypes.INTEGER,
154 allowNull: false,
155 validate: {
156 privacyValid: value => {
157 const res = isVideoPrivacyValid(value)
158 if (res === false) throw new Error('Video privacy is not valid.')
159 }
160 }
161 },
149 nsfw: { 162 nsfw: {
150 type: DataTypes.BOOLEAN, 163 type: DataTypes.BOOLEAN,
151 allowNull: false, 164 allowNull: false,
@@ -245,6 +258,7 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
245 generateThumbnailFromData, 258 generateThumbnailFromData,
246 list, 259 list,
247 listForApi, 260 listForApi,
261 listUserVideosForApi,
248 listOwnedAndPopulateAuthorAndTags, 262 listOwnedAndPopulateAuthorAndTags,
249 listOwnedByAuthor, 263 listOwnedByAuthor,
250 load, 264 load,
@@ -501,7 +515,13 @@ toFormattedJSON = function (this: VideoInstance) {
501toFormattedDetailsJSON = function (this: VideoInstance) { 515toFormattedDetailsJSON = function (this: VideoInstance) {
502 const formattedJson = this.toFormattedJSON() 516 const formattedJson = this.toFormattedJSON()
503 517
518 // Maybe our pod is not up to date and there are new privacy settings since our version
519 let privacyLabel = VIDEO_PRIVACIES[this.privacy]
520 if (!privacyLabel) privacyLabel = 'Unknown'
521
504 const detailsJson = { 522 const detailsJson = {
523 privacyLabel,
524 privacy: this.privacy,
505 descriptionPath: this.getDescriptionPath(), 525 descriptionPath: this.getDescriptionPath(),
506 channel: this.VideoChannel.toFormattedJSON(), 526 channel: this.VideoChannel.toFormattedJSON(),
507 files: [] 527 files: []
@@ -555,6 +575,7 @@ toAddRemoteJSON = function (this: VideoInstance) {
555 views: this.views, 575 views: this.views,
556 likes: this.likes, 576 likes: this.likes,
557 dislikes: this.dislikes, 577 dislikes: this.dislikes,
578 privacy: this.privacy,
558 files: [] 579 files: []
559 } 580 }
560 581
@@ -587,6 +608,7 @@ toUpdateRemoteJSON = function (this: VideoInstance) {
587 views: this.views, 608 views: this.views,
588 likes: this.likes, 609 likes: this.likes,
589 dislikes: this.dislikes, 610 dislikes: this.dislikes,
611 privacy: this.privacy,
590 files: [] 612 files: []
591 } 613 }
592 614
@@ -746,8 +768,39 @@ list = function () {
746 return Video.findAll(query) 768 return Video.findAll(query)
747} 769}
748 770
771listUserVideosForApi = function (userId: number, start: number, count: number, sort: string) {
772 const query = {
773 distinct: true,
774 offset: start,
775 limit: count,
776 order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ],
777 include: [
778 {
779 model: Video['sequelize'].models.VideoChannel,
780 required: true,
781 include: [
782 {
783 model: Video['sequelize'].models.Author,
784 where: {
785 userId
786 },
787 required: true
788 }
789 ]
790 },
791 Video['sequelize'].models.Tag
792 ]
793 }
794
795 return Video.findAndCountAll(query).then(({ rows, count }) => {
796 return {
797 data: rows,
798 total: count
799 }
800 })
801}
802
749listForApi = function (start: number, count: number, sort: string) { 803listForApi = function (start: number, count: number, sort: string) {
750 // Exclude blacklisted videos from the list
751 const query = { 804 const query = {
752 distinct: true, 805 distinct: true,
753 offset: start, 806 offset: start,
@@ -768,8 +821,7 @@ listForApi = function (start: number, count: number, sort: string) {
768 } 821 }
769 ] 822 ]
770 }, 823 },
771 Video['sequelize'].models.Tag, 824 Video['sequelize'].models.Tag
772 Video['sequelize'].models.VideoFile
773 ], 825 ],
774 where: createBaseVideosWhere() 826 where: createBaseVideosWhere()
775 } 827 }
@@ -969,10 +1021,6 @@ searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, s
969 model: Video['sequelize'].models.Tag 1021 model: Video['sequelize'].models.Tag
970 } 1022 }
971 1023
972 const videoFileInclude: Sequelize.IncludeOptions = {
973 model: Video['sequelize'].models.VideoFile
974 }
975
976 const query: Sequelize.FindOptions<VideoAttributes> = { 1024 const query: Sequelize.FindOptions<VideoAttributes> = {
977 distinct: true, 1025 distinct: true,
978 where: createBaseVideosWhere(), 1026 where: createBaseVideosWhere(),
@@ -981,12 +1029,7 @@ searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, s
981 order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ] 1029 order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ]
982 } 1030 }
983 1031
984 // Make an exact search with the magnet 1032 if (field === 'tags') {
985 if (field === 'magnetUri') {
986 videoFileInclude.where = {
987 infoHash: magnetUtil.decode(value).infoHash
988 }
989 } else if (field === 'tags') {
990 const escapedValue = Video['sequelize'].escape('%' + value + '%') 1033 const escapedValue = Video['sequelize'].escape('%' + value + '%')
991 query.where['id'][Sequelize.Op.in] = Video['sequelize'].literal( 1034 query.where['id'][Sequelize.Op.in] = Video['sequelize'].literal(
992 `(SELECT "VideoTags"."videoId" 1035 `(SELECT "VideoTags"."videoId"
@@ -1016,7 +1059,7 @@ searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, s
1016 } 1059 }
1017 1060
1018 query.include = [ 1061 query.include = [
1019 videoChannelInclude, tagInclude, videoFileInclude 1062 videoChannelInclude, tagInclude
1020 ] 1063 ]
1021 1064
1022 return Video.findAndCountAll(query).then(({ rows, count }) => { 1065 return Video.findAndCountAll(query).then(({ rows, count }) => {
@@ -1035,7 +1078,8 @@ function createBaseVideosWhere () {
1035 [Sequelize.Op.notIn]: Video['sequelize'].literal( 1078 [Sequelize.Op.notIn]: Video['sequelize'].literal(
1036 '(SELECT "BlacklistedVideos"."videoId" FROM "BlacklistedVideos")' 1079 '(SELECT "BlacklistedVideos"."videoId" FROM "BlacklistedVideos")'
1037 ) 1080 )
1038 } 1081 },
1082 privacy: VideoPrivacy.PUBLIC
1039 } 1083 }
1040} 1084}
1041 1085
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 {
16 views: number 16 views: number
17 likes: number 17 likes: number
18 dislikes: number 18 dislikes: number
19 privacy: number
19 thumbnailData: string 20 thumbnailData: string
20 files: { 21 files: {
21 infoHash: string 22 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 {
15 views: number 15 views: number
16 likes: number 16 likes: number
17 dislikes: number 17 dislikes: number
18 privacy: number
18 files: { 19 files: {
19 infoHash: string 20 infoHash: string
20 extname: string 21 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'
8export * from './video-channel-update.model' 8export * from './video-channel-update.model'
9export * from './video-channel.model' 9export * from './video-channel.model'
10export * from './video-create.model' 10export * from './video-create.model'
11export * from './video-privacy.enum'
11export * from './video-rate.type' 12export * from './video-rate.type'
12export * from './video-resolution.enum' 13export * from './video-resolution.enum'
13export * from './video-update.model' 14export * 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 @@
1import { VideoPrivacy } from './video-privacy.enum'
2
1export interface VideoCreate { 3export interface VideoCreate {
2 category: number 4 category: number
3 licence: number 5 licence: number
@@ -7,4 +9,5 @@ export interface VideoCreate {
7 nsfw: boolean 9 nsfw: boolean
8 name: string 10 name: string
9 tags: string[] 11 tags: string[]
12 privacy: VideoPrivacy
10} 13}
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 @@
1export enum VideoPrivacy {
2 PUBLIC = 1,
3 UNLISTED = 2,
4 PRIVATE = 3
5}
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 @@
1import { VideoPrivacy } from './video-privacy.enum'
2
1export interface VideoUpdate { 3export interface VideoUpdate {
2 name?: string 4 name?: string
3 category?: number 5 category?: number
4 licence?: number 6 licence?: number
5 language?: number 7 language?: number
6 description?: string 8 description?: string
9 privacy?: VideoPrivacy
7 tags?: string[] 10 tags?: string[]
8 nsfw?: boolean 11 nsfw?: boolean
9} 12}
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 @@
1import { VideoChannel } from './video-channel.model' 1import { VideoChannel } from './video-channel.model'
2import { VideoPrivacy } from './video-privacy.enum'
2 3
3export interface VideoFile { 4export interface VideoFile {
4 magnetUri: string 5 magnetUri: string
@@ -37,7 +38,9 @@ export interface Video {
37} 38}
38 39
39export interface VideoDetails extends Video { 40export interface VideoDetails extends Video {
40 descriptionPath: string, 41 privacy: VideoPrivacy
42 privacyLabel: string
43 descriptionPath: string
41 channel: VideoChannel 44 channel: VideoChannel
42 files: VideoFile[] 45 files: VideoFile[]
43} 46}