Add video privacy setting
authorChocobozzz <florian.bigard@gmail.com>
Tue, 31 Oct 2017 10:52:52 +0000 (11:52 +0100)
committerChocobozzz <florian.bigard@gmail.com>
Tue, 31 Oct 2017 10:53:13 +0000 (11:53 +0100)
57 files changed:
client/src/app/app.component.ts
client/src/app/core/menu/menu.component.html
client/src/app/core/server/server.service.ts
client/src/app/shared/forms/form-validators/video.ts
client/src/app/shared/search/search-field.type.ts
client/src/app/shared/search/search.component.html
client/src/app/shared/search/search.component.ts
client/src/app/videos/+video-edit/video-add.component.html
client/src/app/videos/+video-edit/video-add.component.ts
client/src/app/videos/+video-edit/video-update.component.html
client/src/app/videos/+video-edit/video-update.component.ts
client/src/app/videos/+video-watch/video-watch.component.html
client/src/app/videos/shared/video-details.model.ts
client/src/app/videos/shared/video-edit.model.ts
client/src/app/videos/shared/video.service.ts
client/src/app/videos/video-list/index.ts
client/src/app/videos/video-list/loader.component.html [deleted file]
client/src/app/videos/video-list/loader.component.ts [deleted file]
client/src/app/videos/video-list/my-videos.component.ts [new file with mode: 0644]
client/src/app/videos/video-list/shared/abstract-video-list.html [new file with mode: 0644]
client/src/app/videos/video-list/shared/abstract-video-list.scss [new file with mode: 0644]
client/src/app/videos/video-list/shared/abstract-video-list.ts [new file with mode: 0644]
client/src/app/videos/video-list/shared/index.ts [new file with mode: 0644]
client/src/app/videos/video-list/shared/loader.component.html [new file with mode: 0644]
client/src/app/videos/video-list/shared/loader.component.ts [new file with mode: 0644]
client/src/app/videos/video-list/shared/video-miniature.component.html [new file with mode: 0644]
client/src/app/videos/video-list/shared/video-miniature.component.scss [new file with mode: 0644]
client/src/app/videos/video-list/shared/video-miniature.component.ts [new file with mode: 0644]
client/src/app/videos/video-list/shared/video-sort.component.html [new file with mode: 0644]
client/src/app/videos/video-list/shared/video-sort.component.ts [new file with mode: 0644]
client/src/app/videos/video-list/video-list.component.html [deleted file]
client/src/app/videos/video-list/video-list.component.scss [deleted file]
client/src/app/videos/video-list/video-list.component.ts
client/src/app/videos/video-list/video-miniature.component.html [deleted file]
client/src/app/videos/video-list/video-miniature.component.scss [deleted file]
client/src/app/videos/video-list/video-miniature.component.ts [deleted file]
client/src/app/videos/video-list/video-sort.component.html [deleted file]
client/src/app/videos/video-list/video-sort.component.ts [deleted file]
client/src/app/videos/videos-routing.module.ts
client/src/app/videos/videos.module.ts
client/src/sass/video-js-custom.scss
server/controllers/api/remote/videos.ts
server/controllers/api/users.ts
server/controllers/api/videos/index.ts
server/helpers/custom-validators/videos.ts
server/initializers/constants.ts
server/initializers/migrations/0095-videos-privacy.ts [new file with mode: 0644]
server/middlewares/validators/videos.ts
server/models/video/video-interface.ts
server/models/video/video.ts
shared/models/pods/remote-video/remote-video-create-request.model.ts
shared/models/pods/remote-video/remote-video-update-request.model.ts
shared/models/videos/index.ts
shared/models/videos/video-create.model.ts
shared/models/videos/video-privacy.enum.ts [new file with mode: 0644]
shared/models/videos/video-update.model.ts
shared/models/videos/video.model.ts

index 984470d69257fe8056266a86438b26081990af18..bef1599fc0dc3b675ebbbcb00cb9cd689566ac43 100644 (file)
@@ -1,4 +1,4 @@
-import { Component, OnInit, ViewContainerRef } from '@angular/core'
+import { Component, OnInit } from '@angular/core'
 import { Router } from '@angular/router'
 
 import { AuthService, ServerService } from './core'
@@ -28,8 +28,7 @@ export class AppComponent implements OnInit {
   constructor (
     private router: Router,
     private authService: AuthService,
-    private serverService: ServerService,
-    private userService: UserService
+    private serverService: ServerService
   ) {}
 
   ngOnInit () {
@@ -45,6 +44,7 @@ export class AppComponent implements OnInit {
     this.serverService.loadVideoCategories()
     this.serverService.loadVideoLanguages()
     this.serverService.loadVideoLicences()
+    this.serverService.loadVideoPrivacies()
 
     // Do not display menu on small screens
     if (window.innerWidth < 600) {
index 2d8aace54c4f8552c771760c23af05c3d00f4666..fcde23fdd444ed56ab726d5dd6bf160bfa04204b 100644 (file)
       <span class="hidden-xs glyphicon glyphicon-user"></span>
       My account
     </a>
+
+    <a *ngIf="isLoggedIn" routerLink="/videos/mine" routerLinkActive="active">
+      <span class="hidden-xs glyphicon glyphicon-folder-open"></span>
+      My videos
+    </a>
   </div>
 
   <div class="panel-block">
index ae507afce3949be6076639fe77fb5afb20df722e..cbc4074c9daf40c8908a36c739217db0a0abf2b5 100644 (file)
@@ -19,6 +19,7 @@ export class ServerService {
   private videoCategories: Array<{ id: number, label: string }> = []
   private videoLicences: Array<{ id: number, label: string }> = []
   private videoLanguages: Array<{ id: number, label: string }> = []
+  private videoPrivacies: Array<{ id: number, label: string }> = []
 
   constructor (private http: HttpClient) {}
 
@@ -39,6 +40,10 @@ export class ServerService {
     return this.loadVideoAttributeEnum('languages', this.videoLanguages)
   }
 
+  loadVideoPrivacies () {
+    return this.loadVideoAttributeEnum('privacies', this.videoPrivacies)
+  }
+
   getConfig () {
     return this.config
   }
@@ -55,7 +60,14 @@ export class ServerService {
     return this.videoLanguages
   }
 
-  private loadVideoAttributeEnum (attributeName: 'categories' | 'licences' | 'languages', hashToPopulate: { id: number, label: string }[]) {
+  getVideoPrivacies () {
+    return this.videoPrivacies
+  }
+
+  private loadVideoAttributeEnum (
+    attributeName: 'categories' | 'licences' | 'languages' | 'privacies',
+    hashToPopulate: { id: number, label: string }[]
+  ) {
     return this.http.get(ServerService.BASE_VIDEO_URL + attributeName)
                .subscribe(data => {
                  Object.keys(data)
index 434773501231299ce9c8048db712e794a70b3d75..65f11f5da79b7758769fc68956682b6695f21261 100644 (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 = {
   VALIDATORS: [ Validators.required ],
   MESSAGES: {
index 63557898a9981eb3a85ed8d8e0ca592ec2137fae..ff0bb8de12a91fa6e183be0c58a618488182b7f1 100644 (file)
@@ -1 +1 @@
-export type SearchField = 'name' | 'author' | 'host' | 'magnetUri' | 'tags'
+export type SearchField = 'name' | 'author' | 'host' | 'tags'
index c6c6ff6a8b12e5c1457d2e79d9fc93bb10210646..0302447d0fab3c19ed6b6cfef6e940c62b83ba24 100644 (file)
@@ -6,12 +6,12 @@
 
   <input
     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">
     <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>
     <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">
index ecce20666e5442cea4d8b07e70d54a32681a1809..6e2827fe3a8b9f1b62ee6c865dfadb0f0a43eead 100644 (file)
@@ -16,10 +16,9 @@ export class SearchComponent implements OnInit {
     name: 'Name',
     author: 'Author',
     host: 'Pod Host',
-    magnetUri: 'Magnet URI',
     tags: 'Tags'
   }
-  searchCriterias: Search = {
+  searchCriteria: Search = {
     field: 'name',
     value: ''
   }
@@ -30,13 +29,13 @@ export class SearchComponent implements OnInit {
     // Subscribe if the search changed
     // Usually changed by videos list component
     this.searchService.updateSearch.subscribe(
-      newSearchCriterias => {
+      newSearchCriteria => {
         // Put a field by default
-        if (!newSearchCriterias.field) {
-          newSearchCriterias.field = 'name'
+        if (!newSearchCriteria.field) {
+          newSearchCriteria.field = 'name'
         }
 
-        this.searchCriterias = newSearchCriterias
+        this.searchCriteria = newSearchCriteria
       }
     )
   }
@@ -49,9 +48,9 @@ export class SearchComponent implements OnInit {
     $event.preventDefault()
     $event.stopPropagation()
 
-    this.searchCriterias.field = choice
+    this.searchCriteria.field = choice
 
-    if (this.searchCriterias.value) {
+    if (this.searchCriteria.value) {
       this.doSearch()
     }
   }
@@ -61,7 +60,7 @@ export class SearchComponent implements OnInit {
       this.router.navigate([ '/videos/list' ])
     }
 
-    this.searchService.searchUpdated.next(this.searchCriterias)
+    this.searchService.searchUpdated.next(this.searchCriteria)
   }
 
   getStringChoice (choiceKey: SearchField) {
index a70788ed82264e0a9e54a29460742217dd5a5673..b4e0f9f7c3ae6c7ec8e43d54f31ba41e5bfe9fd1 100644 (file)
         </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">
         <input
           type="checkbox" id="nsfw"
index 5b5557ed9fe45652534b9ca779ee2a002208eae1..c8094f79279dd8d13f36df13c37ab4a5a480e7a8 100644 (file)
@@ -13,7 +13,8 @@ import {
   VIDEO_DESCRIPTION,
   VIDEO_TAGS,
   VIDEO_CHANNEL,
-  VIDEO_FILE
+  VIDEO_FILE,
+  VIDEO_PRIVACY
 } from '../../shared'
 import { AuthService, ServerService } from '../../core'
 import { VideoService } from '../shared'
@@ -34,6 +35,7 @@ export class VideoAddComponent extends FormReactive implements OnInit {
   videoCategories = []
   videoLicences = []
   videoLanguages = []
+  videoPrivacies = []
   userVideoChannels = []
 
   tagValidators = VIDEO_TAGS.VALIDATORS
@@ -43,6 +45,7 @@ export class VideoAddComponent extends FormReactive implements OnInit {
   form: FormGroup
   formErrors = {
     name: '',
+    privacy: '',
     category: '',
     licence: '',
     language: '',
@@ -52,6 +55,7 @@ export class VideoAddComponent extends FormReactive implements OnInit {
   }
   validationMessages = {
     name: VIDEO_NAME.MESSAGES,
+    privacy: VIDEO_PRIVACY.MESSAGES,
     category: VIDEO_CATEGORY.MESSAGES,
     licence: VIDEO_LICENCE.MESSAGES,
     language: VIDEO_LANGUAGE.MESSAGES,
@@ -79,6 +83,7 @@ export class VideoAddComponent extends FormReactive implements OnInit {
     this.form = this.formBuilder.group({
       name: [ '', VIDEO_NAME.VALIDATORS ],
       nsfw: [ false ],
+      privacy: [ '', VIDEO_PRIVACY.VALIDATORS ],
       category: [ '', VIDEO_CATEGORY.VALIDATORS ],
       licence: [ '', VIDEO_LICENCE.VALIDATORS ],
       language: [ '', VIDEO_LANGUAGE.VALIDATORS ],
@@ -95,6 +100,7 @@ export class VideoAddComponent extends FormReactive implements OnInit {
     this.videoCategories = this.serverService.getVideoCategories()
     this.videoLicences = this.serverService.getVideoLicences()
     this.videoLanguages = this.serverService.getVideoLanguages()
+    this.videoPrivacies = this.serverService.getVideoPrivacies()
 
     this.buildForm()
 
@@ -139,6 +145,7 @@ export class VideoAddComponent extends FormReactive implements OnInit {
     const formValue: VideoCreate = this.form.value
 
     const name = formValue.name
+    const privacy = formValue.privacy
     const nsfw = formValue.nsfw
     const category = formValue.category
     const licence = formValue.licence
@@ -150,6 +157,7 @@ export class VideoAddComponent extends FormReactive implements OnInit {
 
     const formData = new FormData()
     formData.append('name', name)
+    formData.append('privacy', privacy.toString())
     formData.append('category', '' + category)
     formData.append('nsfw', '' + nsfw)
     formData.append('licence', '' + licence)
index ec040630e1feaed912bd334f0bd82662033d7421..b9c6139b2db58945e4900461fe99a8db2825b939 100644 (file)
       </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">
       <input
         type="checkbox" id="nsfw"
index 6ced77f1a4b2d26b6c9d2a9cd3029193d94504f9..be663575f5f49e59b0342ea9b9d1ade2ca1927b0 100644 (file)
@@ -1,7 +1,6 @@
 import { Component, OnInit } from '@angular/core'
 import { FormBuilder, FormGroup } from '@angular/forms'
 import { ActivatedRoute, Router } from '@angular/router'
-import { Observable } from 'rxjs/Observable'
 import 'rxjs/add/observable/forkJoin'
 
 import { NotificationsService } from 'angular2-notifications'
@@ -14,9 +13,11 @@ import {
   VIDEO_LICENCE,
   VIDEO_LANGUAGE,
   VIDEO_DESCRIPTION,
-  VIDEO_TAGS
+  VIDEO_TAGS,
+  VIDEO_PRIVACY
 } from '../../shared'
 import { VideoEdit, VideoService } from '../shared'
+import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy.enum'
 
 @Component({
   selector: 'my-videos-update',
@@ -29,6 +30,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
   videoCategories = []
   videoLicences = []
   videoLanguages = []
+  videoPrivacies = []
   video: VideoEdit
 
   tagValidators = VIDEO_TAGS.VALIDATORS
@@ -38,6 +40,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
   form: FormGroup
   formErrors = {
     name: '',
+    privacy: '',
     category: '',
     licence: '',
     language: '',
@@ -45,6 +48,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
   }
   validationMessages = {
     name: VIDEO_NAME.MESSAGES,
+    privacy: VIDEO_PRIVACY.MESSAGES,
     category: VIDEO_CATEGORY.MESSAGES,
     licence: VIDEO_LICENCE.MESSAGES,
     language: VIDEO_LANGUAGE.MESSAGES,
@@ -67,6 +71,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
   buildForm () {
     this.form = this.formBuilder.group({
       name: [ '', VIDEO_NAME.VALIDATORS ],
+      privacy: [ '', VIDEO_PRIVACY.VALIDATORS ],
       nsfw: [ false ],
       category: [ '', VIDEO_CATEGORY.VALIDATORS ],
       licence: [ '', VIDEO_LICENCE.VALIDATORS ],
@@ -84,6 +89,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
     this.videoCategories = this.serverService.getVideoCategories()
     this.videoLicences = this.serverService.getVideoLicences()
     this.videoLanguages = this.serverService.getVideoLanguages()
+    this.videoPrivacies = this.serverService.getVideoPrivacies()
 
     const uuid: string = this.route.snapshot.params['uuid']
 
@@ -98,6 +104,16 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
         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()
         },
 
index 71f986ccdbcd59849a94f914571765e122dbb5ed..53648a8d8bfa1168d8055f2a0b55fd6627a7a3e9 100644 (file)
@@ -22,7 +22,7 @@
   <div *ngIf="videoNotFound" id="video-not-found">Video not found :'(</div>
 </div>
 
-<!-- P2P informations -->
+<!-- P2P information -->
 <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-upload" class="col-md-4 col-sm-4 col-xs-4">Upload: {{ uploadSpeed | bytes }}/s</div>
     </div>
 
     <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">
         <span class="video-details-attribute-label">
           Category:
index 68ded5210161153a6737ad384775a71a4091f229..84f96a25ffe6fa1e04cfc9f431b3e7523a22f0ae 100644 (file)
@@ -5,7 +5,8 @@ import {
   VideoFile,
   VideoChannel,
   VideoResolution,
-  UserRight
+  UserRight,
+  VideoPrivacy
 } from '../../../../../shared'
 
 export class VideoDetails extends Video implements VideoDetailsServerModel {
@@ -41,10 +42,14 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
   descriptionPath: string
   files: VideoFile[]
   channel: VideoChannel
+  privacy: VideoPrivacy
+  privacyLabel: string
 
   constructor (hash: VideoDetailsServerModel) {
     super(hash)
 
+    this.privacy = hash.privacy
+    this.privacyLabel = hash.privacyLabel
     this.descriptionPath = hash.descriptionPath
     this.files = hash.files
     this.channel = hash.channel
index e0b7bf130c4cbba00a3c1901ec9ebcd4d131bffc..88d23a59f036ffa5449ccca7b0e5958029cb74c7 100644 (file)
@@ -1,4 +1,5 @@
 import { VideoDetails } from './video-details.model'
+import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy.enum'
 
 export class VideoEdit {
   category: number
@@ -9,6 +10,7 @@ export class VideoEdit {
   tags: string[]
   nsfw: boolean
   channel: number
+  privacy: VideoPrivacy
   uuid?: string
   id?: number
 
@@ -23,6 +25,7 @@ export class VideoEdit {
     this.tags = videoDetails.tags
     this.nsfw = videoDetails.nsfw
     this.channel = videoDetails.channel.id
+    this.privacy = videoDetails.privacy
   }
 
   patch (values: Object) {
@@ -40,7 +43,8 @@ export class VideoEdit {
       name: this.name,
       tags: this.tags,
       nsfw: this.nsfw,
-      channel: this.channel
+      channel: this.channel,
+      privacy: this.privacy
     }
   }
 }
index 7d5372334630a254ddb5007ccb4055eb8d191539..8459aa0d3a1d9196ec75d78bc417d2073b574dc4 100644 (file)
@@ -19,7 +19,6 @@ import {
   UserVideoRate,
   VideoRateType,
   VideoUpdate,
-  VideoAbuseCreate,
   UserVideoRateUpdate,
   Video as VideoServerModel,
   VideoDetails as VideoDetailsServerModel,
@@ -51,6 +50,7 @@ export class VideoService {
       licence: video.licence,
       language,
       description: video.description,
+      privacy: video.privacy,
       tags: video.tags,
       nsfw: video.nsfw
     }
@@ -63,22 +63,35 @@ export class VideoService {
   uploadVideo (video: FormData) {
     const req = new HttpRequest('POST', VideoService.BASE_VIDEO_URL + 'upload', video, { reportProgress: true })
 
-    return this.authHttp.request(req)
-                        .catch(this.restExtractor.handleError)
+    return this.authHttp
+      .request(req)
+      .catch(this.restExtractor.handleError)
   }
 
-  getVideos (videoPagination: VideoPagination, sort: SortField) {
+  getMyVideos (videoPagination: VideoPagination, sort: SortField): Observable<{ videos: Video[], totalVideos: number}> {
     const pagination = this.videoPaginationToRestPagination(videoPagination)
 
     let params = new HttpParams()
     params = this.restService.addRestGetParams(params, pagination, sort)
 
-    return this.authHttp.get(VideoService.BASE_VIDEO_URL, { params })
-                        .map(this.extractVideos)
-                        .catch((res) => this.restExtractor.handleError(res))
+    return this.authHttp.get(UserService.BASE_USERS_URL + '/me/videos', { params })
+      .map(this.extractVideos)
+      .catch((res) => this.restExtractor.handleError(res))
   }
 
-  searchVideos (search: Search, videoPagination: VideoPagination, sort: SortField) {
+  getVideos (videoPagination: VideoPagination, sort: SortField): Observable<{ videos: Video[], totalVideos: number}> {
+    const pagination = this.videoPaginationToRestPagination(videoPagination)
+
+    let params = new HttpParams()
+    params = this.restService.addRestGetParams(params, pagination, sort)
+
+    return this.authHttp
+      .get(VideoService.BASE_VIDEO_URL, { params })
+      .map(this.extractVideos)
+      .catch((res) => this.restExtractor.handleError(res))
+  }
+
+  searchVideos (search: Search, videoPagination: VideoPagination, sort: SortField): Observable<{ videos: Video[], totalVideos: number}> {
     const url = VideoService.BASE_VIDEO_URL + 'search/' + encodeURIComponent(search.value)
 
     const pagination = this.videoPaginationToRestPagination(videoPagination)
@@ -88,15 +101,17 @@ export class VideoService {
 
     if (search.field) params.set('field', search.field)
 
-    return this.authHttp.get<ResultList<VideoServerModel>>(url, { params })
-                        .map(this.extractVideos)
-                        .catch((res) => this.restExtractor.handleError(res))
+    return this.authHttp
+      .get<ResultList<VideoServerModel>>(url, { params })
+      .map(this.extractVideos)
+      .catch((res) => this.restExtractor.handleError(res))
   }
 
   removeVideo (id: number) {
-    return this.authHttp.delete(VideoService.BASE_VIDEO_URL + id)
-               .map(this.restExtractor.extractDataBool)
-               .catch((res) => this.restExtractor.handleError(res))
+    return this.authHttp
+      .delete(VideoService.BASE_VIDEO_URL + id)
+      .map(this.restExtractor.extractDataBool)
+      .catch((res) => this.restExtractor.handleError(res))
   }
 
   loadCompleteDescription (descriptionPath: string) {
@@ -117,8 +132,9 @@ export class VideoService {
   getUserVideoRating (id: number): Observable<UserVideoRate> {
     const url = UserService.BASE_USERS_URL + 'me/videos/' + id + '/rating'
 
-    return this.authHttp.get(url)
-                        .catch(res => this.restExtractor.handleError(res))
+    return this.authHttp
+      .get(url)
+      .catch(res => this.restExtractor.handleError(res))
   }
 
   private videoPaginationToRestPagination (videoPagination: VideoPagination) {
@@ -134,9 +150,10 @@ export class VideoService {
       rating: rateType
     }
 
-    return this.authHttp.put(url, body)
-                        .map(this.restExtractor.extractDataBool)
-                        .catch(res => this.restExtractor.handleError(res))
+    return this.authHttp
+      .put(url, body)
+      .map(this.restExtractor.extractDataBool)
+      .catch(res => this.restExtractor.handleError(res))
   }
 
   private extractVideos (result: ResultList<VideoServerModel>) {
index a490e6bb56819a8157ee08cb7de74f69f4b56f39..ed2bb1657135584b3a90e036d79eeffa08476476 100644 (file)
@@ -1,4 +1,3 @@
-export * from './loader.component'
+export * from './my-videos.component'
 export * from './video-list.component'
-export * from './video-miniature.component'
-export * from './video-sort.component'
+export * from './shared'
diff --git a/client/src/app/videos/video-list/loader.component.html b/client/src/app/videos/video-list/loader.component.html
deleted file mode 100644 (file)
index 38d0695..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-<div id="video-loading" *ngIf="loading">
-  <div class="glyphicon glyphicon-refresh glyphicon-refresh-animate"></div>
-</div>
diff --git a/client/src/app/videos/video-list/loader.component.ts b/client/src/app/videos/video-list/loader.component.ts
deleted file mode 100644 (file)
index f37d70c..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-import { Component, Input } from '@angular/core'
-
-@Component({
-  selector: 'my-loader',
-  styleUrls: [ ],
-  templateUrl: './loader.component.html'
-})
-
-export class LoaderComponent {
-  @Input() loading: boolean
-}
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 (file)
index 0000000..648741a
--- /dev/null
@@ -0,0 +1,36 @@
+import { Component, OnDestroy, OnInit } from '@angular/core'
+import { ActivatedRoute, Router } from '@angular/router'
+
+import { NotificationsService } from 'angular2-notifications'
+
+import { AbstractVideoList } from './shared'
+import { VideoService } from '../shared'
+
+@Component({
+  selector: 'my-videos',
+  styleUrls: [ './shared/abstract-video-list.scss' ],
+  templateUrl: './shared/abstract-video-list.html'
+})
+export class MyVideosComponent extends AbstractVideoList implements OnInit, OnDestroy {
+
+  constructor (
+    protected router: Router,
+    protected route: ActivatedRoute,
+    protected notificationsService: NotificationsService,
+    private videoService: VideoService
+  ) {
+    super()
+  }
+
+  ngOnInit () {
+    super.ngOnInit()
+  }
+
+  ngOnDestroy () {
+    this.subActivatedRoute.unsubscribe()
+  }
+
+  getVideosObservable () {
+    return this.videoService.getMyVideos(this.pagination, this.sort)
+  }
+}
diff --git a/client/src/app/videos/video-list/shared/abstract-video-list.html b/client/src/app/videos/video-list/shared/abstract-video-list.html
new file mode 100644 (file)
index 0000000..680fba3
--- /dev/null
@@ -0,0 +1,28 @@
+<div class="row">
+  <div class="content-padding">
+    <div class="videos-info">
+      <div class="col-md-9 col-xs-5 videos-total-results">
+        <span *ngIf="pagination.totalItems !== null">{{ pagination.totalItems }} videos</span>
+
+        <my-loader [loading]="loading | async"></my-loader>
+      </div>
+
+      <my-video-sort class="col-md-3 col-xs-7" [currentSort]="sort" (sort)="onSort($event)"></my-video-sort>
+    </div>
+  </div>
+</div>
+
+<div class="content-padding videos-miniatures">
+  <div class="no-video" *ngIf="isThereNoVideo()">There is no video.</div>
+
+  <my-video-miniature
+    class="ng-animate"
+    *ngFor="let video of videos" [video]="video" [user]="user" [currentSort]="sort"
+  >
+  </my-video-miniature>
+</div>
+
+<pagination *ngIf="pagination.totalItems !== null && pagination.totalItems !== 0"
+  [totalItems]="pagination.totalItems" [itemsPerPage]="pagination.itemsPerPage" [maxSize]="6" [boundaryLinks]="true" [rotate]="false"
+  [(ngModel)]="pagination.currentPage" (pageChanged)="onPageChanged($event)"
+></pagination>
diff --git a/client/src/app/videos/video-list/shared/abstract-video-list.scss b/client/src/app/videos/video-list/shared/abstract-video-list.scss
new file mode 100644 (file)
index 0000000..4b44096
--- /dev/null
@@ -0,0 +1,37 @@
+.videos-info {
+  @media screen and (max-width: 400px) {
+    margin-left: 0;
+  }
+
+  border-bottom: 1px solid #f1f1f1;
+  height: 40px;
+  line-height: 40px;
+
+  .videos-total-results {
+    font-size: 13px;
+  }
+
+  my-loader {
+    display: inline-block;
+    margin-left: 5px;
+  }
+}
+
+.videos-miniatures {
+  text-align: center;
+  padding-top: 0;
+
+  my-video-miniature {
+    text-align: left;
+  }
+
+  .no-video {
+    margin-top: 50px;
+    text-align: center;
+  }
+}
+
+pagination {
+  display: block;
+  text-align: center;
+}
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 (file)
index 0000000..87d5bc4
--- /dev/null
@@ -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 ])
+  }
+}
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 (file)
index 0000000..2c9804e
--- /dev/null
@@ -0,0 +1,4 @@
+export * from './abstract-video-list'
+export * from './loader.component'
+export * from './video-miniature.component'
+export * from './video-sort.component'
diff --git a/client/src/app/videos/video-list/shared/loader.component.html b/client/src/app/videos/video-list/shared/loader.component.html
new file mode 100644 (file)
index 0000000..38d0695
--- /dev/null
@@ -0,0 +1,3 @@
+<div id="video-loading" *ngIf="loading">
+  <div class="glyphicon glyphicon-refresh glyphicon-refresh-animate"></div>
+</div>
diff --git a/client/src/app/videos/video-list/shared/loader.component.ts b/client/src/app/videos/video-list/shared/loader.component.ts
new file mode 100644 (file)
index 0000000..f37d70c
--- /dev/null
@@ -0,0 +1,11 @@
+import { Component, Input } from '@angular/core'
+
+@Component({
+  selector: 'my-loader',
+  styleUrls: [ ],
+  templateUrl: './loader.component.html'
+})
+
+export class LoaderComponent {
+  @Input() loading: boolean
+}
diff --git a/client/src/app/videos/video-list/shared/video-miniature.component.html b/client/src/app/videos/video-list/shared/video-miniature.component.html
new file mode 100644 (file)
index 0000000..abe8702
--- /dev/null
@@ -0,0 +1,33 @@
+<div class="video-miniature">
+  <a
+    [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.description"
+    class="video-miniature-thumbnail"
+  >
+    <img [attr.src]="video.thumbnailUrl" alt="video thumbnail" [ngClass]="{ 'blur-filter': isVideoNSFWForThisUser() }" />
+
+    <div class="video-miniature-thumbnail-overlay">
+      <span class="video-miniature-thumbnail-overlay-views">{{ video.views }} views</span>
+      <span class="video-miniature-thumbnail-overlay-duration">{{ video.durationLabel }}</span>
+    </div>
+  </a>
+
+  <div class="video-miniature-information">
+    <span class="video-miniature-name">
+      <a
+        class="video-miniature-name"
+        [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name" [ngClass]="{ 'blur-filter': isVideoNSFWForThisUser() }"
+      >
+          {{ video.name }}
+      </a>
+    </span>
+
+    <div class="video-miniature-tags">
+      <span *ngFor="let tag of video.tags" class="video-miniature-tag">
+        <a [routerLink]="['/videos/list', { field: 'tags', search: tag, sort: currentSort }]" class="label label-primary">{{ tag }}</a>
+      </span>
+    </div>
+
+    <a [routerLink]="['/videos/list', { field: 'author', search: video.author, sort: currentSort }]" class="video-miniature-author">{{ video.by }}</a>
+    <span class="video-miniature-created-at">{{ video.createdAt | date:'short' }}</span>
+  </div>
+</div>
diff --git a/client/src/app/videos/video-list/shared/video-miniature.component.scss b/client/src/app/videos/video-list/shared/video-miniature.component.scss
new file mode 100644 (file)
index 0000000..066792d
--- /dev/null
@@ -0,0 +1,102 @@
+.video-miniature {
+  margin-top: 30px;
+  display: inline-block;
+  position: relative;
+  height: 190px;
+  width: 220px;
+  vertical-align: top;
+
+  .video-miniature-thumbnail {
+    display: inline-block;
+    position: relative;
+    border-radius: 3px;
+    overflow: hidden;
+
+    &:hover {
+      text-decoration: none !important;
+    }
+
+    img.blur-filter {
+      filter: blur(5px);
+      transform : scale(1.03);
+    }
+
+    .video-miniature-thumbnail-overlay {
+      position: absolute;
+      right: 0px;
+      bottom: 0px;
+      display: inline-block;
+      background-color: rgba(0, 0, 0, 0.7);
+      color: #fff;
+      padding: 3px 5px;
+      font-size: 11px;
+      font-weight: bold;
+      width: 100%;
+
+      .video-miniature-thumbnail-overlay-views {
+
+      }
+
+      .video-miniature-thumbnail-overlay-duration {
+        float: right;
+      }
+    }
+  }
+
+  .video-miniature-information {
+    width: 200px;
+
+    .video-miniature-name {
+      height: 23px;
+      display: block;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+      font-weight: bold;
+      transition: color 0.2s;
+      font-size: 15px;
+
+      &:hover {
+        text-decoration: none;
+      }
+
+      &.blur-filter {
+        filter: blur(3px);
+        padding-left: 4px;
+      }
+
+      .video-miniature-tags {
+        // Fix for chrome when tags are long
+        width: 201px;
+
+        .video-miniature-tag {
+          font-size: 13px;
+          cursor: pointer;
+          position: relative;
+          top: -2px;
+
+          .label {
+            transition: background-color 0.2s;
+          }
+        }
+      }
+    }
+
+    .video-miniature-author, .video-miniature-created-at {
+      display: block;
+      margin-left: 1px;
+      font-size: 11px;
+      color: $video-miniature-other-infos;
+      opacity: 0.9;
+    }
+
+    .video-miniature-author {
+      transition: color 0.2s;
+
+      &:hover {
+        color: #23527c;
+        text-decoration: none;
+      }
+    }
+  }
+}
diff --git a/client/src/app/videos/video-list/shared/video-miniature.component.ts b/client/src/app/videos/video-list/shared/video-miniature.component.ts
new file mode 100644 (file)
index 0000000..e5a8790
--- /dev/null
@@ -0,0 +1,19 @@
+import { Component, Input } from '@angular/core'
+
+import { SortField, Video } from '../../shared'
+import { User } from '../../../shared'
+
+@Component({
+  selector: 'my-video-miniature',
+  styleUrls: [ './video-miniature.component.scss' ],
+  templateUrl: './video-miniature.component.html'
+})
+export class VideoMiniatureComponent {
+  @Input() currentSort: SortField
+  @Input() user: User
+  @Input() video: Video
+
+  isVideoNSFWForThisUser () {
+    return this.video.isVideoNSFWForUser(this.user)
+  }
+}
diff --git a/client/src/app/videos/video-list/shared/video-sort.component.html b/client/src/app/videos/video-list/shared/video-sort.component.html
new file mode 100644 (file)
index 0000000..3bece0b
--- /dev/null
@@ -0,0 +1,5 @@
+<select class="form-control input-sm" [(ngModel)]="currentSort" (ngModelChange)="onSortChange()">
+  <option *ngFor="let choice of choiceKeys" [value]="choice">
+    {{ getStringChoice(choice) }}
+  </option>
+</select>
diff --git a/client/src/app/videos/video-list/shared/video-sort.component.ts b/client/src/app/videos/video-list/shared/video-sort.component.ts
new file mode 100644 (file)
index 0000000..8aa89d3
--- /dev/null
@@ -0,0 +1,39 @@
+import { Component, EventEmitter, Input, Output } from '@angular/core'
+
+import { SortField } from '../../shared'
+
+@Component({
+  selector: 'my-video-sort',
+  templateUrl: './video-sort.component.html'
+})
+
+export class VideoSortComponent {
+  @Output() sort = new EventEmitter<any>()
+
+  @Input() currentSort: SortField
+
+  sortChoices: { [ P in SortField ]: string } = {
+    'name': 'Name - Asc',
+    '-name': 'Name - Desc',
+    'duration': 'Duration - Asc',
+    '-duration': 'Duration - Desc',
+    'createdAt': 'Created Date - Asc',
+    '-createdAt': 'Created Date - Desc',
+    'views': 'Views - Asc',
+    '-views': 'Views - Desc',
+    'likes': 'Likes - Asc',
+    '-likes': 'Likes - Desc'
+  }
+
+  get choiceKeys () {
+    return Object.keys(this.sortChoices)
+  }
+
+  getStringChoice (choiceKey: SortField) {
+    return this.sortChoices[choiceKey]
+  }
+
+  onSortChange () {
+    this.sort.emit(this.currentSort)
+  }
+}
diff --git a/client/src/app/videos/video-list/video-list.component.html b/client/src/app/videos/video-list/video-list.component.html
deleted file mode 100644 (file)
index 680fba3..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-<div class="row">
-  <div class="content-padding">
-    <div class="videos-info">
-      <div class="col-md-9 col-xs-5 videos-total-results">
-        <span *ngIf="pagination.totalItems !== null">{{ pagination.totalItems }} videos</span>
-
-        <my-loader [loading]="loading | async"></my-loader>
-      </div>
-
-      <my-video-sort class="col-md-3 col-xs-7" [currentSort]="sort" (sort)="onSort($event)"></my-video-sort>
-    </div>
-  </div>
-</div>
-
-<div class="content-padding videos-miniatures">
-  <div class="no-video" *ngIf="isThereNoVideo()">There is no video.</div>
-
-  <my-video-miniature
-    class="ng-animate"
-    *ngFor="let video of videos" [video]="video" [user]="user" [currentSort]="sort"
-  >
-  </my-video-miniature>
-</div>
-
-<pagination *ngIf="pagination.totalItems !== null && pagination.totalItems !== 0"
-  [totalItems]="pagination.totalItems" [itemsPerPage]="pagination.itemsPerPage" [maxSize]="6" [boundaryLinks]="true" [rotate]="false"
-  [(ngModel)]="pagination.currentPage" (pageChanged)="onPageChanged($event)"
-></pagination>
diff --git a/client/src/app/videos/video-list/video-list.component.scss b/client/src/app/videos/video-list/video-list.component.scss
deleted file mode 100644 (file)
index 4b44096..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-.videos-info {
-  @media screen and (max-width: 400px) {
-    margin-left: 0;
-  }
-
-  border-bottom: 1px solid #f1f1f1;
-  height: 40px;
-  line-height: 40px;
-
-  .videos-total-results {
-    font-size: 13px;
-  }
-
-  my-loader {
-    display: inline-block;
-    margin-left: 5px;
-  }
-}
-
-.videos-miniatures {
-  text-align: center;
-  padding-top: 0;
-
-  my-video-miniature {
-    text-align: left;
-  }
-
-  .no-video {
-    margin-top: 50px;
-    text-align: center;
-  }
-}
-
-pagination {
-  display: block;
-  text-align: center;
-}
index bf6f60215e14b27a6842680c21dc82047f64f135..784162679320dd48b04833e5ed2e2f40584d6154 100644 (file)
@@ -1,51 +1,33 @@
 import { Component, OnDestroy, OnInit } from '@angular/core'
 import { ActivatedRoute, Router } from '@angular/router'
 import { Subscription } from 'rxjs/Subscription'
-import { BehaviorSubject } from 'rxjs/BehaviorSubject'
 
 import { NotificationsService } from 'angular2-notifications'
 
-import { AuthService } from '../../core'
-import {
-  SortField,
-  Video,
-  VideoService,
-  VideoPagination
-} from '../shared'
-import { Search, SearchField, SearchService, User } from '../../shared'
+import { VideoService } from '../shared'
+import { Search, SearchField, SearchService } from '../../shared'
+import { AbstractVideoList } from './shared'
 
 @Component({
   selector: 'my-videos-list',
-  styleUrls: [ './video-list.component.scss' ],
-  templateUrl: './video-list.component.html'
+  styleUrls: [ './shared/abstract-video-list.scss' ],
+  templateUrl: './shared/abstract-video-list.html'
 })
-export class VideoListComponent implements OnInit, OnDestroy {
-  loading: BehaviorSubject<boolean> = new BehaviorSubject(false)
-  pagination: VideoPagination = {
-    currentPage: 1,
-    itemsPerPage: 25,
-    totalItems: null
-  }
-  sort: SortField
-  user: User
-  videos: Video[] = []
-
+export class VideoListComponent extends AbstractVideoList implements OnInit, OnDestroy {
   private search: Search
-  private subActivatedRoute: Subscription
   private subSearch: Subscription
 
   constructor (
-    private authService: AuthService,
-    private notificationsService: NotificationsService,
-    private router: Router,
-    private route: ActivatedRoute,
+    protected router: Router,
+    protected route: ActivatedRoute,
+    protected notificationsService: NotificationsService,
     private videoService: VideoService,
     private searchService: SearchService
-  ) {}
+  ) {
+    super()
+  }
 
   ngOnInit () {
-    this.user = this.authService.getUser()
-
     // Subscribe to route changes
     this.subActivatedRoute = this.route.params.subscribe(routeParams => {
       this.loadRouteParams(routeParams)
@@ -66,14 +48,12 @@ export class VideoListComponent implements OnInit, OnDestroy {
   }
 
   ngOnDestroy () {
-    this.subActivatedRoute.unsubscribe()
+    super.ngOnDestroy()
+
     this.subSearch.unsubscribe()
   }
 
-  getVideos () {
-    this.loading.next(true)
-    this.videos = []
-
+  getVideosObservable () {
     let observable = null
     if (this.search.value) {
       observable = this.videoService.searchVideos(this.search, this.pagination, this.sort)
@@ -81,40 +61,11 @@ export class VideoListComponent implements OnInit, OnDestroy {
       observable = this.videoService.getVideos(this.pagination, this.sort)
     }
 
-    observable.subscribe(
-      ({ videos, totalVideos }) => {
-        this.videos = videos
-        this.pagination.totalItems = totalVideos
-
-        this.loading.next(false)
-      },
-      error => this.notificationsService.error('Error', error.text)
-    )
-  }
-
-  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()
+    return observable
   }
 
-  onSort (sort: SortField) {
-    this.sort = sort
-
-    this.navigateToNewParams()
-  }
-
-  private buildRouteParams () {
-    // There is always a sort and a current page
-    const params = {
-      sort: this.sort,
-      page: this.pagination.currentPage
-    }
+  protected buildRouteParams () {
+    const params = super.buildRouteParams()
 
     // Maybe there is a search
     if (this.search.value) {
@@ -125,7 +76,9 @@ export class VideoListComponent implements OnInit, OnDestroy {
     return params
   }
 
-  private loadRouteParams (routeParams: { [ key: string ]: any }) {
+  protected loadRouteParams (routeParams: { [ key: string ]: any }) {
+    super.loadRouteParams(routeParams)
+
     if (routeParams['search'] !== undefined) {
       this.search = {
         value: routeParams['search'],
@@ -137,18 +90,5 @@ export class VideoListComponent implements OnInit, OnDestroy {
         field: 'name'
       }
     }
-
-    this.sort = routeParams['sort'] as SortField || '-createdAt'
-
-    if (routeParams['page'] !== undefined) {
-      this.pagination.currentPage = parseInt(routeParams['page'], 10)
-    } else {
-      this.pagination.currentPage = 1
-    }
-  }
-
-  private navigateToNewParams () {
-    const routeParams = this.buildRouteParams()
-    this.router.navigate([ '/videos/list', routeParams ])
   }
 }
diff --git a/client/src/app/videos/video-list/video-miniature.component.html b/client/src/app/videos/video-list/video-miniature.component.html
deleted file mode 100644 (file)
index abe8702..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-<div class="video-miniature">
-  <a
-    [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.description"
-    class="video-miniature-thumbnail"
-  >
-    <img [attr.src]="video.thumbnailUrl" alt="video thumbnail" [ngClass]="{ 'blur-filter': isVideoNSFWForThisUser() }" />
-
-    <div class="video-miniature-thumbnail-overlay">
-      <span class="video-miniature-thumbnail-overlay-views">{{ video.views }} views</span>
-      <span class="video-miniature-thumbnail-overlay-duration">{{ video.durationLabel }}</span>
-    </div>
-  </a>
-
-  <div class="video-miniature-information">
-    <span class="video-miniature-name">
-      <a
-        class="video-miniature-name"
-        [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name" [ngClass]="{ 'blur-filter': isVideoNSFWForThisUser() }"
-      >
-          {{ video.name }}
-      </a>
-    </span>
-
-    <div class="video-miniature-tags">
-      <span *ngFor="let tag of video.tags" class="video-miniature-tag">
-        <a [routerLink]="['/videos/list', { field: 'tags', search: tag, sort: currentSort }]" class="label label-primary">{{ tag }}</a>
-      </span>
-    </div>
-
-    <a [routerLink]="['/videos/list', { field: 'author', search: video.author, sort: currentSort }]" class="video-miniature-author">{{ video.by }}</a>
-    <span class="video-miniature-created-at">{{ video.createdAt | date:'short' }}</span>
-  </div>
-</div>
diff --git a/client/src/app/videos/video-list/video-miniature.component.scss b/client/src/app/videos/video-list/video-miniature.component.scss
deleted file mode 100644 (file)
index 066792d..0000000
+++ /dev/null
@@ -1,102 +0,0 @@
-.video-miniature {
-  margin-top: 30px;
-  display: inline-block;
-  position: relative;
-  height: 190px;
-  width: 220px;
-  vertical-align: top;
-
-  .video-miniature-thumbnail {
-    display: inline-block;
-    position: relative;
-    border-radius: 3px;
-    overflow: hidden;
-
-    &:hover {
-      text-decoration: none !important;
-    }
-
-    img.blur-filter {
-      filter: blur(5px);
-      transform : scale(1.03);
-    }
-
-    .video-miniature-thumbnail-overlay {
-      position: absolute;
-      right: 0px;
-      bottom: 0px;
-      display: inline-block;
-      background-color: rgba(0, 0, 0, 0.7);
-      color: #fff;
-      padding: 3px 5px;
-      font-size: 11px;
-      font-weight: bold;
-      width: 100%;
-
-      .video-miniature-thumbnail-overlay-views {
-
-      }
-
-      .video-miniature-thumbnail-overlay-duration {
-        float: right;
-      }
-    }
-  }
-
-  .video-miniature-information {
-    width: 200px;
-
-    .video-miniature-name {
-      height: 23px;
-      display: block;
-      overflow: hidden;
-      text-overflow: ellipsis;
-      white-space: nowrap;
-      font-weight: bold;
-      transition: color 0.2s;
-      font-size: 15px;
-
-      &:hover {
-        text-decoration: none;
-      }
-
-      &.blur-filter {
-        filter: blur(3px);
-        padding-left: 4px;
-      }
-
-      .video-miniature-tags {
-        // Fix for chrome when tags are long
-        width: 201px;
-
-        .video-miniature-tag {
-          font-size: 13px;
-          cursor: pointer;
-          position: relative;
-          top: -2px;
-
-          .label {
-            transition: background-color 0.2s;
-          }
-        }
-      }
-    }
-
-    .video-miniature-author, .video-miniature-created-at {
-      display: block;
-      margin-left: 1px;
-      font-size: 11px;
-      color: $video-miniature-other-infos;
-      opacity: 0.9;
-    }
-
-    .video-miniature-author {
-      transition: color 0.2s;
-
-      &:hover {
-        color: #23527c;
-        text-decoration: none;
-      }
-    }
-  }
-}
diff --git a/client/src/app/videos/video-list/video-miniature.component.ts b/client/src/app/videos/video-list/video-miniature.component.ts
deleted file mode 100644 (file)
index 18434da..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-import { Component, Input } from '@angular/core'
-
-import { SortField, Video } from '../shared'
-import { User } from '../../shared'
-
-@Component({
-  selector: 'my-video-miniature',
-  styleUrls: [ './video-miniature.component.scss' ],
-  templateUrl: './video-miniature.component.html'
-})
-export class VideoMiniatureComponent {
-  @Input() currentSort: SortField
-  @Input() user: User
-  @Input() video: Video
-
-  isVideoNSFWForThisUser () {
-    return this.video.isVideoNSFWForUser(this.user)
-  }
-}
diff --git a/client/src/app/videos/video-list/video-sort.component.html b/client/src/app/videos/video-list/video-sort.component.html
deleted file mode 100644 (file)
index 3bece0b..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-<select class="form-control input-sm" [(ngModel)]="currentSort" (ngModelChange)="onSortChange()">
-  <option *ngFor="let choice of choiceKeys" [value]="choice">
-    {{ getStringChoice(choice) }}
-  </option>
-</select>
diff --git a/client/src/app/videos/video-list/video-sort.component.ts b/client/src/app/videos/video-list/video-sort.component.ts
deleted file mode 100644 (file)
index 64916bf..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-import { Component, EventEmitter, Input, Output } from '@angular/core'
-
-import { SortField } from '../shared'
-
-@Component({
-  selector: 'my-video-sort',
-  templateUrl: './video-sort.component.html'
-})
-
-export class VideoSortComponent {
-  @Output() sort = new EventEmitter<any>()
-
-  @Input() currentSort: SortField
-
-  sortChoices: { [ P in SortField ]: string } = {
-    'name': 'Name - Asc',
-    '-name': 'Name - Desc',
-    'duration': 'Duration - Asc',
-    '-duration': 'Duration - Desc',
-    'createdAt': 'Created Date - Asc',
-    '-createdAt': 'Created Date - Desc',
-    'views': 'Views - Asc',
-    '-views': 'Views - Desc',
-    'likes': 'Likes - Asc',
-    '-likes': 'Likes - Desc'
-  }
-
-  get choiceKeys () {
-    return Object.keys(this.sortChoices)
-  }
-
-  getStringChoice (choiceKey: SortField) {
-    return this.sortChoices[choiceKey]
-  }
-
-  onSortChange () {
-    this.sort.emit(this.currentSort)
-  }
-}
index d3869748bb42554ceedd0ab75f86a59f00473fa6..3ca3e54865d9fa446c0864df97f77e7bc1e74e64 100644 (file)
@@ -3,7 +3,7 @@ import { RouterModule, Routes } from '@angular/router'
 
 import { MetaGuard } from '@ngx-meta/core'
 
-import { VideoListComponent } from './video-list'
+import { VideoListComponent, MyVideosComponent } from './video-list'
 import { VideosComponent } from './videos.component'
 
 const videosRoutes: Routes = [
@@ -12,6 +12,15 @@ const videosRoutes: Routes = [
     component: VideosComponent,
     canActivateChild: [ MetaGuard ],
     children: [
+      {
+        path: 'mine',
+        component: MyVideosComponent,
+        data: {
+          meta: {
+            title: 'My videos'
+          }
+        }
+      },
       {
         path: 'list',
         component: VideoListComponent,
index 3a0c3feac122241f4387a152bf91db7e8aaee59d..ecc351b654c4b3b5abeb22e58efdba3e6dc12ec7 100644 (file)
@@ -2,7 +2,13 @@ import { NgModule } from '@angular/core'
 
 import { VideosRoutingModule } from './videos-routing.module'
 import { VideosComponent } from './videos.component'
-import { LoaderComponent, VideoListComponent, VideoMiniatureComponent, VideoSortComponent } from './video-list'
+import {
+  LoaderComponent,
+  VideoListComponent,
+  MyVideosComponent,
+  VideoMiniatureComponent,
+  VideoSortComponent
+} from './video-list'
 import { VideoService } from './shared'
 import { SharedModule } from '../shared'
 
@@ -16,6 +22,7 @@ import { SharedModule } from '../shared'
     VideosComponent,
 
     VideoListComponent,
+    MyVideosComponent,
     VideoMiniatureComponent,
     VideoSortComponent,
 
index c5f668f1784f2c7e02d47269f258bd0ffbcbb07b..6ad21988e5a2307fa200be92ff8a1a1669e236e2 100644 (file)
@@ -334,71 +334,34 @@ $slider-bg-color: lighten($primary-background-color, 33%);
 
 // Thanks: https://projects.lukehaas.me/css-loaders/
 .vjs-loading-spinner {
-  border: none;
-  opacity: 1;
+  margin: -25px 0 0 -25px;
+  position: absolute;
+  top: 50%;
+  left: 50%;
   font-size: 10px;
-  text-indent: -9999em;
-  width: 5em;
-  height: 5em;
-  border-radius: 50%;
-  background: #ffffff;
-  background: -moz-linear-gradient(left, #ffffff 10%, rgba(255, 255, 255, 0) 42%);
-  background: -webkit-linear-gradient(left, #ffffff 10%, rgba(255, 255, 255, 0) 42%);
-  background: -o-linear-gradient(left, #ffffff 10%, rgba(255, 255, 255, 0) 42%);
-  background: -ms-linear-gradient(left, #ffffff 10%, rgba(255, 255, 255, 0) 42%);
-  background: linear-gradient(to right, #ffffff 10%, rgba(255, 255, 255, 0) 42%);
   position: relative;
-  -webkit-animation: load3 1.4s infinite linear;
-  animation: load3 1.4s infinite linear;
-  -webkit-transform: translateZ(0);
-  -ms-transform: translateZ(0);
+  text-indent: -9999em;
+  border: 0.7em solid rgba(255, 255, 255, 0.2);
+  border-left-color: #ffffff;
   transform: translateZ(0);
+  animation: spinner 1.4s infinite linear;
 
   &:before {
-    width: 50%;
-    height: 50%;
-    background: #ffffff;
-    border-radius: 100% 0 0 0;
-    position: absolute;
-    top: 0;
-    left: 0;
-    content: '';
     animation: none !important;
-    margin: 0 !important;
   }
 
   &:after {
-    background: #000;
-    width: 75%;
-    height: 75%;
     border-radius: 50%;
-    content: '';
-    margin: auto;
-    position: absolute;
-    top: 0;
-    left: 0;
-    bottom: 0;
-    right: 0;
+    width: 6em;
+    height: 6em;
     animation: none !important;
   }
 
-  @-webkit-keyframes load3 {
-    0% {
-      -webkit-transform: rotate(0deg);
-      transform: rotate(0deg);
-    }
-    100% {
-      -webkit-transform: rotate(360deg);
-      transform: rotate(360deg);
-    }
-  }
-  @keyframes load3 {
+  @keyframes spinner {
     0% {
-      -webkit-transform: rotate(0deg);
       transform: rotate(0deg);
     }
     100% {
-      -webkit-transform: rotate(360deg);
       transform: rotate(360deg);
     }
   }
index 3ecc62ada1466a667c3983dd09898bd8432fc722..cba47f0a13061db84dd00b18356112dba88dc019 100644 (file)
@@ -267,7 +267,8 @@ async function addRemoteVideo (videoToCreateData: RemoteVideoCreateData, fromPod
       views: videoToCreateData.views,
       likes: videoToCreateData.likes,
       dislikes: videoToCreateData.dislikes,
-      remote: true
+      remote: true,
+      privacy: videoToCreateData.privacy
     }
 
     const video = db.Video.build(videoData)
@@ -334,6 +335,7 @@ async function updateRemoteVideo (videoAttributesToUpdate: RemoteVideoUpdateData
       videoInstance.set('views', videoAttributesToUpdate.views)
       videoInstance.set('likes', videoAttributesToUpdate.likes)
       videoInstance.set('dislikes', videoAttributesToUpdate.dislikes)
+      videoInstance.set('privacy', videoAttributesToUpdate.privacy)
 
       await videoInstance.save(sequelizeOptions)
 
index fdc9b0c879ef43653a4b5550e59032d9151a6d2f..dcd407fdf49b298cd6134a9878a6518196079544 100644 (file)
@@ -30,6 +30,8 @@ import {
 } from '../../../shared'
 import { createUserAuthorAndChannel } from '../../lib'
 import { UserInstance } from '../../models'
+import { videosSortValidator } from '../../middlewares/validators/sort'
+import { setVideosSort } from '../../middlewares/sort'
 
 const usersRouter = express.Router()
 
@@ -38,6 +40,15 @@ usersRouter.get('/me',
   asyncMiddleware(getUserInformation)
 )
 
+usersRouter.get('/me/videos',
+  authenticate,
+  paginationValidator,
+  videosSortValidator,
+  setVideosSort,
+  setPagination,
+  asyncMiddleware(getUserVideos)
+)
+
 usersRouter.get('/me/videos/:videoId/rating',
   authenticate,
   usersVideoRatingValidator,
@@ -101,6 +112,13 @@ export {
 
 // ---------------------------------------------------------------------------
 
+async function getUserVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
+  const user = res.locals.oauth.token.User
+  const resultList = await db.Video.listUserVideosForApi(user.id ,req.query.start, req.query.count, req.query.sort)
+
+  return res.json(getFormattedObjects(resultList.data, resultList.total))
+}
+
 async function createUserRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) {
   const options = {
     arguments: [ req, res ],
@@ -146,13 +164,14 @@ async function registerUser (req: express.Request, res: express.Response, next:
 }
 
 async function getUserInformation (req: express.Request, res: express.Response, next: express.NextFunction) {
+  // We did not load channels in res.locals.user
   const user = await db.User.loadByUsernameAndPopulateChannels(res.locals.oauth.token.user.username)
 
   return res.json(user.toFormattedJSON())
 }
 
 function getUser (req: express.Request, res: express.Response, next: express.NextFunction) {
-  return res.json(res.locals.user.toFormattedJSON())
+  return res.json(res.locals.oauth.token.User.toFormattedJSON())
 }
 
 async function getUserVideoRating (req: express.Request, res: express.Response, next: express.NextFunction) {
index 49f0e4630d2c9ea313ab0a30c03c9ca9e7085964..4dd09917b43a321b2ec66bec1bf50e92c0e3e786 100644 (file)
@@ -9,7 +9,8 @@ import {
   REQUEST_VIDEO_EVENT_TYPES,
   VIDEO_CATEGORIES,
   VIDEO_LICENCES,
-  VIDEO_LANGUAGES
+  VIDEO_LANGUAGES,
+  VIDEO_PRIVACIES
 } from '../../../initializers'
 import {
   addEventToRemoteVideo,
@@ -43,7 +44,7 @@ import {
   resetSequelizeInstance
 } from '../../../helpers'
 import { VideoInstance } from '../../../models'
-import { VideoCreate, VideoUpdate } from '../../../../shared'
+import { VideoCreate, VideoUpdate, VideoPrivacy } from '../../../../shared'
 
 import { abuseVideoRouter } from './abuse'
 import { blacklistRouter } from './blacklist'
@@ -84,6 +85,7 @@ videosRouter.use('/', videoChannelRouter)
 videosRouter.get('/categories', listVideoCategories)
 videosRouter.get('/licences', listVideoLicences)
 videosRouter.get('/languages', listVideoLanguages)
+videosRouter.get('/privacies', listVideoPrivacies)
 
 videosRouter.get('/',
   paginationValidator,
@@ -149,6 +151,10 @@ function listVideoLanguages (req: express.Request, res: express.Response) {
   res.json(VIDEO_LANGUAGES)
 }
 
+function listVideoPrivacies (req: express.Request, res: express.Response) {
+  res.json(VIDEO_PRIVACIES)
+}
+
 // Wrapper to video add that retry the function if there is a database error
 // We need this because we run the transaction in SERIALIZABLE isolation that can fail
 async function addVideoRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) {
@@ -179,6 +185,7 @@ async function addVideo (req: express.Request, res: express.Response, videoPhysi
       language: videoInfo.language,
       nsfw: videoInfo.nsfw,
       description: videoInfo.description,
+      privacy: videoInfo.privacy,
       duration: videoPhysicalFile['duration'], // duration was added by a previous middleware
       channelId: res.locals.videoChannel.id
     }
@@ -240,6 +247,8 @@ async function addVideo (req: express.Request, res: express.Response, videoPhysi
 
     // Let transcoding job send the video to friends because the video file extension might change
     if (CONFIG.TRANSCODING.ENABLED === true) return undefined
+    // Don't send video to remote pods, it is private
+    if (video.privacy === VideoPrivacy.PRIVATE) return undefined
 
     const remoteVideo = await video.toAddRemoteJSON()
     // Now we'll add the video's meta data to our friends
@@ -264,6 +273,7 @@ async function updateVideo (req: express.Request, res: express.Response) {
   const videoInstance = res.locals.video
   const videoFieldsSave = videoInstance.toJSON()
   const videoInfoToUpdate: VideoUpdate = req.body
+  const wasPrivateVideo = videoInstance.privacy === VideoPrivacy.PRIVATE
 
   try {
     await db.sequelize.transaction(async t => {
@@ -276,6 +286,7 @@ async function updateVideo (req: express.Request, res: express.Response) {
       if (videoInfoToUpdate.licence !== undefined) videoInstance.set('licence', videoInfoToUpdate.licence)
       if (videoInfoToUpdate.language !== undefined) videoInstance.set('language', videoInfoToUpdate.language)
       if (videoInfoToUpdate.nsfw !== undefined) videoInstance.set('nsfw', videoInfoToUpdate.nsfw)
+      if (videoInfoToUpdate.privacy !== undefined) videoInstance.set('privacy', videoInfoToUpdate.privacy)
       if (videoInfoToUpdate.description !== undefined) videoInstance.set('description', videoInfoToUpdate.description)
 
       await videoInstance.save(sequelizeOptions)
@@ -287,10 +298,17 @@ async function updateVideo (req: express.Request, res: express.Response) {
         videoInstance.Tags = tagInstances
       }
 
-      const json = videoInstance.toUpdateRemoteJSON()
-
       // Now we'll update the video's meta data to our friends
-      return updateVideoToFriends(json, t)
+      if (wasPrivateVideo === false) {
+        const json = videoInstance.toUpdateRemoteJSON()
+        return updateVideoToFriends(json, t)
+      }
+
+      // Video is not private anymore, send a create action to remote pods
+      if (wasPrivateVideo === true && videoInstance.privacy !== VideoPrivacy.PRIVATE) {
+        const remoteVideo = await videoInstance.toAddRemoteJSON()
+        return addVideoToFriends(remoteVideo, t)
+      }
     })
 
     logger.info('Video with name %s and uuid %s updated.', videoInstance.name, videoInstance.uuid)
index 5b9102275cebac16b4d7e5d842441f3efbdc3406..f3fdcaf2df00a9abb1885ab83cc9fadc399e7dfb 100644 (file)
@@ -11,6 +11,7 @@ import {
   VIDEO_LICENCES,
   VIDEO_LANGUAGES,
   VIDEO_RATE_TYPES,
+  VIDEO_PRIVACIES,
   database as db
 } from '../../initializers'
 import { isUserUsernameValid } from './users'
@@ -36,6 +37,15 @@ function isVideoLicenceValid (value: number) {
   return VIDEO_LICENCES[value] !== undefined
 }
 
+function isVideoPrivacyValid (value: string) {
+  return VIDEO_PRIVACIES[value] !== undefined
+}
+
+// Maybe we don't know the remote privacy setting, but that doesn't matter
+function isRemoteVideoPrivacyValid (value: string) {
+  return validator.isInt('' + value)
+}
+
 // Maybe we don't know the remote licence, but that doesn't matter
 function isRemoteVideoLicenceValid (value: string) {
   return validator.isInt('' + value)
@@ -195,6 +205,8 @@ export {
   isVideoDislikesValid,
   isVideoEventCountValid,
   isVideoFileSizeValid,
+  isVideoPrivacyValid,
+  isRemoteVideoPrivacyValid,
   isVideoFileResolutionValid,
   checkVideoExists,
   isRemoteVideoCategoryValid,
index adccb9f418b5234d9681782ee57e7301f7a64b52..d349abaf0f9f094d1c5ba477479f0423d51183cd 100644 (file)
@@ -12,10 +12,11 @@ import {
   RemoteVideoRequestType,
   JobState
 } from '../../shared/models'
+import { VideoPrivacy } from '../../shared/models/videos/video-privacy.enum'
 
 // ---------------------------------------------------------------------------
 
-const LAST_MIGRATION_VERSION = 90
+const LAST_MIGRATION_VERSION = 95
 
 // ---------------------------------------------------------------------------
 
@@ -196,6 +197,12 @@ const VIDEO_LANGUAGES = {
   14: 'Italian'
 }
 
+const VIDEO_PRIVACIES = {
+  [VideoPrivacy.PUBLIC]: 'Public',
+  [VideoPrivacy.UNLISTED]: 'Unlisted',
+  [VideoPrivacy.PRIVATE]: 'Private'
+}
+
 // ---------------------------------------------------------------------------
 
 // Score a pod has when we create it as a friend
@@ -394,6 +401,7 @@ export {
   THUMBNAILS_SIZE,
   VIDEO_CATEGORIES,
   VIDEO_LANGUAGES,
+  VIDEO_PRIVACIES,
   VIDEO_LICENCES,
   VIDEO_RATE_TYPES
 }
diff --git a/server/initializers/migrations/0095-videos-privacy.ts b/server/initializers/migrations/0095-videos-privacy.ts
new file mode 100644 (file)
index 0000000..4c2bf91
--- /dev/null
@@ -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
+}
index 0c07404c5dadf47f345688bb71b29464a7d43e75..e197d46069c09bc3b8ef0465b972078dbfa350eb 100644 (file)
@@ -20,9 +20,10 @@ import {
   isVideoRatingTypeValid,
   getDurationFromVideoFile,
   checkVideoExists,
-  isIdValid
+  isIdValid,
+  isVideoPrivacyValid
 } from '../../helpers'
-import { UserRight } from '../../../shared'
+import { UserRight, VideoPrivacy } from '../../../shared'
 
 const videosAddValidator = [
   body('videofile').custom((value, { req }) => isVideoFile(req.files)).withMessage(
@@ -36,6 +37,7 @@ const videosAddValidator = [
   body('nsfw').custom(isVideoNSFWValid).withMessage('Should have a valid NSFW attribute'),
   body('description').custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
   body('channelId').custom(isIdValid).withMessage('Should have correct video channel id'),
+  body('privacy').custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
   body('tags').optional().custom(isVideoTagsValid).withMessage('Should have correct tags'),
 
   (req: express.Request, res: express.Response, next: express.NextFunction) => {
@@ -110,6 +112,7 @@ const videosUpdateValidator = [
   body('licence').optional().custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
   body('language').optional().custom(isVideoLanguageValid).withMessage('Should have a valid language'),
   body('nsfw').optional().custom(isVideoNSFWValid).withMessage('Should have a valid NSFW attribute'),
+  body('privacy').custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
   body('description').optional().custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
   body('tags').optional().custom(isVideoTagsValid).withMessage('Should have correct tags'),
 
@@ -118,19 +121,27 @@ const videosUpdateValidator = [
 
     checkErrors(req, res, () => {
       checkVideoExists(req.params.id, res, () => {
+        const video = res.locals.video
+
         // We need to make additional checks
-        if (res.locals.video.isOwned() === false) {
+        if (video.isOwned() === false) {
           return res.status(403)
                     .json({ error: 'Cannot update video of another pod' })
                     .end()
         }
 
-        if (res.locals.video.VideoChannel.Author.userId !== res.locals.oauth.token.User.id) {
+        if (video.VideoChannel.Author.userId !== res.locals.oauth.token.User.id) {
           return res.status(403)
                     .json({ error: 'Cannot update video of another user' })
                     .end()
         }
 
+        if (video.privacy !== VideoPrivacy.PRIVATE && req.body.privacy === VideoPrivacy.PRIVATE) {
+          return res.status(409)
+            .json({ error: 'Cannot set "private" a video that was not private anymore.' })
+            .end()
+        }
+
         next()
       })
     })
index 587652f45f493966fb3870f1c1c84344a3c88c4e..cfe65f9aa7343896633476ea5e55c09a50bac3a2 100644 (file)
@@ -49,6 +49,7 @@ export namespace VideoMethods {
   export type ListOwnedByAuthor = (author: string) => Promise<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 = (
     value: string,
     field: string,
@@ -75,6 +76,7 @@ export interface VideoClass {
   generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData
   list: VideoMethods.List
   listForApi: VideoMethods.ListForApi
+  listUserVideosForApi: VideoMethods.ListUserVideosForApi
   listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags
   listOwnedByAuthor: VideoMethods.ListOwnedByAuthor
   load: VideoMethods.Load
@@ -97,6 +99,7 @@ export interface VideoAttributes {
   nsfw: boolean
   description: string
   duration: number
+  privacy: number
   views?: number
   likes?: number
   dislikes?: number
index 1877c506ae63bf07acbf85e5f359df39eb85c816..2c1bd6b6eab8db229d486cb6ce250a3f63e472cc 100644 (file)
@@ -18,6 +18,7 @@ import {
   isVideoNSFWValid,
   isVideoDescriptionValid,
   isVideoDurationValid,
+  isVideoPrivacyValid,
   readFileBufferPromise,
   unlinkPromise,
   renamePromise,
@@ -38,10 +39,11 @@ import {
   THUMBNAILS_SIZE,
   PREVIEWS_SIZE,
   CONSTRAINTS_FIELDS,
-  API_VERSION
+  API_VERSION,
+  VIDEO_PRIVACIES
 } from '../../initializers'
 import { removeVideoToFriends } from '../../lib'
-import { VideoResolution } from '../../../shared'
+import { VideoResolution, VideoPrivacy } from '../../../shared'
 import { VideoFileInstance, VideoFileModel } from './video-file-interface'
 
 import { addMethodsToModel, getSort } from '../utils'
@@ -79,6 +81,7 @@ let getTruncatedDescription: VideoMethods.GetTruncatedDescription
 let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData
 let list: VideoMethods.List
 let listForApi: VideoMethods.ListForApi
+let listUserVideosForApi: VideoMethods.ListUserVideosForApi
 let loadByHostAndUUID: VideoMethods.LoadByHostAndUUID
 let listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags
 let listOwnedByAuthor: VideoMethods.ListOwnedByAuthor
@@ -146,6 +149,16 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
           }
         }
       },
+      privacy: {
+        type: DataTypes.INTEGER,
+        allowNull: false,
+        validate: {
+          privacyValid: value => {
+            const res = isVideoPrivacyValid(value)
+            if (res === false) throw new Error('Video privacy is not valid.')
+          }
+        }
+      },
       nsfw: {
         type: DataTypes.BOOLEAN,
         allowNull: false,
@@ -245,6 +258,7 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
     generateThumbnailFromData,
     list,
     listForApi,
+    listUserVideosForApi,
     listOwnedAndPopulateAuthorAndTags,
     listOwnedByAuthor,
     load,
@@ -501,7 +515,13 @@ toFormattedJSON = function (this: VideoInstance) {
 toFormattedDetailsJSON = function (this: VideoInstance) {
   const formattedJson = this.toFormattedJSON()
 
+  // Maybe our pod is not up to date and there are new privacy settings since our version
+  let privacyLabel = VIDEO_PRIVACIES[this.privacy]
+  if (!privacyLabel) privacyLabel = 'Unknown'
+
   const detailsJson = {
+    privacyLabel,
+    privacy: this.privacy,
     descriptionPath: this.getDescriptionPath(),
     channel: this.VideoChannel.toFormattedJSON(),
     files: []
@@ -555,6 +575,7 @@ toAddRemoteJSON = function (this: VideoInstance) {
       views: this.views,
       likes: this.likes,
       dislikes: this.dislikes,
+      privacy: this.privacy,
       files: []
     }
 
@@ -587,6 +608,7 @@ toUpdateRemoteJSON = function (this: VideoInstance) {
     views: this.views,
     likes: this.likes,
     dislikes: this.dislikes,
+    privacy: this.privacy,
     files: []
   }
 
@@ -746,8 +768,39 @@ list = function () {
   return Video.findAll(query)
 }
 
+listUserVideosForApi = function (userId: number, start: number, count: number, sort: string) {
+  const query = {
+    distinct: true,
+    offset: start,
+    limit: count,
+    order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ],
+    include: [
+      {
+        model: Video['sequelize'].models.VideoChannel,
+        required: true,
+        include: [
+          {
+            model: Video['sequelize'].models.Author,
+            where: {
+              userId
+            },
+            required: true
+          }
+        ]
+      },
+      Video['sequelize'].models.Tag
+    ]
+  }
+
+  return Video.findAndCountAll(query).then(({ rows, count }) => {
+    return {
+      data: rows,
+      total: count
+    }
+  })
+}
+
 listForApi = function (start: number, count: number, sort: string) {
-  // Exclude blacklisted videos from the list
   const query = {
     distinct: true,
     offset: start,
@@ -768,8 +821,7 @@ listForApi = function (start: number, count: number, sort: string) {
           }
         ]
       },
-      Video['sequelize'].models.Tag,
-      Video['sequelize'].models.VideoFile
+      Video['sequelize'].models.Tag
     ],
     where: createBaseVideosWhere()
   }
@@ -969,10 +1021,6 @@ searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, s
     model: Video['sequelize'].models.Tag
   }
 
-  const videoFileInclude: Sequelize.IncludeOptions = {
-    model: Video['sequelize'].models.VideoFile
-  }
-
   const query: Sequelize.FindOptions<VideoAttributes> = {
     distinct: true,
     where: createBaseVideosWhere(),
@@ -981,12 +1029,7 @@ searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, s
     order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ]
   }
 
-  // Make an exact search with the magnet
-  if (field === 'magnetUri') {
-    videoFileInclude.where = {
-      infoHash: magnetUtil.decode(value).infoHash
-    }
-  } else if (field === 'tags') {
+  if (field === 'tags') {
     const escapedValue = Video['sequelize'].escape('%' + value + '%')
     query.where['id'][Sequelize.Op.in] = Video['sequelize'].literal(
       `(SELECT "VideoTags"."videoId"
@@ -1016,7 +1059,7 @@ searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, s
   }
 
   query.include = [
-    videoChannelInclude, tagInclude, videoFileInclude
+    videoChannelInclude, tagInclude
   ]
 
   return Video.findAndCountAll(query).then(({ rows, count }) => {
@@ -1035,7 +1078,8 @@ function createBaseVideosWhere () {
       [Sequelize.Op.notIn]: Video['sequelize'].literal(
         '(SELECT "BlacklistedVideos"."videoId" FROM "BlacklistedVideos")'
       )
-    }
+    },
+    privacy: VideoPrivacy.PUBLIC
   }
 }
 
index cb20dfa03d644b4240176b3e98e5964d0bc00693..9a382e65401d7baa740e2f1169728f457c2d4c3c 100644 (file)
@@ -16,6 +16,7 @@ export interface RemoteVideoCreateData {
   views: number
   likes: number
   dislikes: number
+  privacy: number
   thumbnailData: string
   files: {
     infoHash: string
index 8439cfa240f278cf9fd28e029c7ba241b5e1229a..924489c75ccd44bd8926ecca19e30efe41f56440 100644 (file)
@@ -15,6 +15,7 @@ export interface RemoteVideoUpdateData {
   views: number
   likes: number
   dislikes: number
+  privacy: number
   files: {
     infoHash: string
     extname: string
index 2a3912f06a8a72cd4549c9c85b2f230a7ca04976..14a10f5d889b9a21d9069d887b18d1f0ce4729b9 100644 (file)
@@ -8,6 +8,7 @@ export * from './video-channel-create.model'
 export * from './video-channel-update.model'
 export * from './video-channel.model'
 export * from './video-create.model'
+export * from './video-privacy.enum'
 export * from './video-rate.type'
 export * from './video-resolution.enum'
 export * from './video-update.model'
index 4d0e83520fe3a8da392bd23e000bf0de3b640575..e537c38a878a5fe68083c852c0dece96b2dea894 100644 (file)
@@ -1,3 +1,5 @@
+import { VideoPrivacy } from './video-privacy.enum'
+
 export interface VideoCreate {
   category: number
   licence: number
@@ -7,4 +9,5 @@ export interface VideoCreate {
   nsfw: boolean
   name: string
   tags: string[]
+  privacy: VideoPrivacy
 }
diff --git a/shared/models/videos/video-privacy.enum.ts b/shared/models/videos/video-privacy.enum.ts
new file mode 100644 (file)
index 0000000..29888c7
--- /dev/null
@@ -0,0 +1,5 @@
+export enum VideoPrivacy {
+  PUBLIC = 1,
+  UNLISTED = 2,
+  PRIVATE = 3
+}
index 29a82621ba0e3ef0f981a92301ba9b9c180b093b..0cf38fe6e2cda7a18a11b57b9c351efe3c1ffac4 100644 (file)
@@ -1,9 +1,12 @@
+import { VideoPrivacy } from './video-privacy.enum'
+
 export interface VideoUpdate {
   name?: string
   category?: number
   licence?: number
   language?: number
   description?: string
+  privacy?: VideoPrivacy
   tags?: string[]
   nsfw?: boolean
 }
index 1490d345c8c9093be7ee3b537dd4e20b5f807094..2f4ee246273d61283b98d9afd4fbcf46804f2031 100644 (file)
@@ -1,4 +1,5 @@
 import { VideoChannel } from './video-channel.model'
+import { VideoPrivacy } from './video-privacy.enum'
 
 export interface VideoFile {
   magnetUri: string
@@ -37,7 +38,9 @@ export interface Video {
 }
 
 export interface VideoDetails extends Video {
-  descriptionPath: string,
+  privacy: VideoPrivacy
+  privacyLabel: string
+  descriptionPath: string
   channel: VideoChannel
   files: VideoFile[]
 }