-import { Component, OnInit, ViewContainerRef } from '@angular/core'
+import { Component, OnInit } from '@angular/core'
import { Router } from '@angular/router'
import { AuthService, ServerService } from './core'
constructor (
private router: Router,
private authService: AuthService,
- private serverService: ServerService,
- private userService: UserService
+ private serverService: ServerService
) {}
ngOnInit () {
this.serverService.loadVideoCategories()
this.serverService.loadVideoLanguages()
this.serverService.loadVideoLicences()
+ this.serverService.loadVideoPrivacies()
// Do not display menu on small screens
if (window.innerWidth < 600) {
<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">
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) {}
return this.loadVideoAttributeEnum('languages', this.videoLanguages)
}
+ loadVideoPrivacies () {
+ return this.loadVideoAttributeEnum('privacies', this.videoPrivacies)
+ }
+
getConfig () {
return this.config
}
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)
}
}
+export const VIDEO_PRIVACY = {
+ VALIDATORS: [ Validators.required ],
+ MESSAGES: {
+ 'required': 'Video privacy is required.'
+ }
+}
+
export const VIDEO_CATEGORY = {
VALIDATORS: [ Validators.required ],
MESSAGES: {
-export type SearchField = 'name' | 'author' | 'host' | 'magnetUri' | 'tags'
+export type SearchField = 'name' | 'author' | 'host' | 'tags'
<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">
name: 'Name',
author: 'Author',
host: 'Pod Host',
- magnetUri: 'Magnet URI',
tags: 'Tags'
}
- searchCriterias: Search = {
+ searchCriteria: Search = {
field: 'name',
value: ''
}
// 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
}
)
}
$event.preventDefault()
$event.stopPropagation()
- this.searchCriterias.field = choice
+ this.searchCriteria.field = choice
- if (this.searchCriterias.value) {
+ if (this.searchCriteria.value) {
this.doSearch()
}
}
this.router.navigate([ '/videos/list' ])
}
- this.searchService.searchUpdated.next(this.searchCriterias)
+ this.searchService.searchUpdated.next(this.searchCriteria)
}
getStringChoice (choiceKey: SearchField) {
</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"
VIDEO_DESCRIPTION,
VIDEO_TAGS,
VIDEO_CHANNEL,
- VIDEO_FILE
+ VIDEO_FILE,
+ VIDEO_PRIVACY
} from '../../shared'
import { AuthService, ServerService } from '../../core'
import { VideoService } from '../shared'
videoCategories = []
videoLicences = []
videoLanguages = []
+ videoPrivacies = []
userVideoChannels = []
tagValidators = VIDEO_TAGS.VALIDATORS
form: FormGroup
formErrors = {
name: '',
+ privacy: '',
category: '',
licence: '',
language: '',
}
validationMessages = {
name: VIDEO_NAME.MESSAGES,
+ privacy: VIDEO_PRIVACY.MESSAGES,
category: VIDEO_CATEGORY.MESSAGES,
licence: VIDEO_LICENCE.MESSAGES,
language: VIDEO_LANGUAGE.MESSAGES,
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 ],
this.videoCategories = this.serverService.getVideoCategories()
this.videoLicences = this.serverService.getVideoLicences()
this.videoLanguages = this.serverService.getVideoLanguages()
+ this.videoPrivacies = this.serverService.getVideoPrivacies()
this.buildForm()
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
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)
</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"
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'
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',
videoCategories = []
videoLicences = []
videoLanguages = []
+ videoPrivacies = []
video: VideoEdit
tagValidators = VIDEO_TAGS.VALIDATORS
form: FormGroup
formErrors = {
name: '',
+ privacy: '',
category: '',
licence: '',
language: '',
}
validationMessages = {
name: VIDEO_NAME.MESSAGES,
+ privacy: VIDEO_PRIVACY.MESSAGES,
category: VIDEO_CATEGORY.MESSAGES,
licence: VIDEO_LICENCE.MESSAGES,
language: VIDEO_LANGUAGE.MESSAGES,
buildForm () {
this.form = this.formBuilder.group({
name: [ '', VIDEO_NAME.VALIDATORS ],
+ privacy: [ '', VIDEO_PRIVACY.VALIDATORS ],
nsfw: [ false ],
category: [ '', VIDEO_CATEGORY.VALIDATORS ],
licence: [ '', VIDEO_LICENCE.VALIDATORS ],
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']
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()
},
<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:
VideoFile,
VideoChannel,
VideoResolution,
- UserRight
+ UserRight,
+ VideoPrivacy
} from '../../../../../shared'
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
import { VideoDetails } from './video-details.model'
+import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy.enum'
export class VideoEdit {
category: number
tags: string[]
nsfw: boolean
channel: number
+ privacy: VideoPrivacy
uuid?: string
id?: number
this.tags = videoDetails.tags
this.nsfw = videoDetails.nsfw
this.channel = videoDetails.channel.id
+ this.privacy = videoDetails.privacy
}
patch (values: Object) {
name: this.name,
tags: this.tags,
nsfw: this.nsfw,
- channel: this.channel
+ channel: this.channel,
+ privacy: this.privacy
}
}
}
UserVideoRate,
VideoRateType,
VideoUpdate,
- VideoAbuseCreate,
UserVideoRateUpdate,
Video as VideoServerModel,
VideoDetails as VideoDetailsServerModel,
licence: video.licence,
language,
description: video.description,
+ privacy: video.privacy,
tags: video.tags,
nsfw: video.nsfw
}
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)
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) {
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) {
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>) {
-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'
+++ /dev/null
-<div id="video-loading" *ngIf="loading">
- <div class="glyphicon glyphicon-refresh glyphicon-refresh-animate"></div>
-</div>
+++ /dev/null
-import { Component, Input } from '@angular/core'
-
-@Component({
- selector: 'my-loader',
- styleUrls: [ ],
- templateUrl: './loader.component.html'
-})
-
-export class LoaderComponent {
- @Input() loading: boolean
-}
--- /dev/null
+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)
+ }
+}
--- /dev/null
+<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>
--- /dev/null
+.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;
+}
--- /dev/null
+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 ])
+ }
+}
--- /dev/null
+export * from './abstract-video-list'
+export * from './loader.component'
+export * from './video-miniature.component'
+export * from './video-sort.component'
--- /dev/null
+<div id="video-loading" *ngIf="loading">
+ <div class="glyphicon glyphicon-refresh glyphicon-refresh-animate"></div>
+</div>
--- /dev/null
+import { Component, Input } from '@angular/core'
+
+@Component({
+ selector: 'my-loader',
+ styleUrls: [ ],
+ templateUrl: './loader.component.html'
+})
+
+export class LoaderComponent {
+ @Input() loading: boolean
+}
--- /dev/null
+<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>
--- /dev/null
+.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;
+ }
+ }
+ }
+}
--- /dev/null
+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)
+ }
+}
--- /dev/null
+<select class="form-control input-sm" [(ngModel)]="currentSort" (ngModelChange)="onSortChange()">
+ <option *ngFor="let choice of choiceKeys" [value]="choice">
+ {{ getStringChoice(choice) }}
+ </option>
+</select>
--- /dev/null
+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)
+ }
+}
+++ /dev/null
-<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>
+++ /dev/null
-.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;
-}
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)
}
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)
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) {
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'],
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 ])
}
}
+++ /dev/null
-<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>
+++ /dev/null
-.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;
- }
- }
- }
-}
+++ /dev/null
-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)
- }
-}
+++ /dev/null
-<select class="form-control input-sm" [(ngModel)]="currentSort" (ngModelChange)="onSortChange()">
- <option *ngFor="let choice of choiceKeys" [value]="choice">
- {{ getStringChoice(choice) }}
- </option>
-</select>
+++ /dev/null
-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)
- }
-}
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 = [
component: VideosComponent,
canActivateChild: [ MetaGuard ],
children: [
+ {
+ path: 'mine',
+ component: MyVideosComponent,
+ data: {
+ meta: {
+ title: 'My videos'
+ }
+ }
+ },
{
path: 'list',
component: VideoListComponent,
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'
VideosComponent,
VideoListComponent,
+ MyVideosComponent,
VideoMiniatureComponent,
VideoSortComponent,
// 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);
}
}
views: videoToCreateData.views,
likes: videoToCreateData.likes,
dislikes: videoToCreateData.dislikes,
- remote: true
+ remote: true,
+ privacy: videoToCreateData.privacy
}
const video = db.Video.build(videoData)
videoInstance.set('views', videoAttributesToUpdate.views)
videoInstance.set('likes', videoAttributesToUpdate.likes)
videoInstance.set('dislikes', videoAttributesToUpdate.dislikes)
+ videoInstance.set('privacy', videoAttributesToUpdate.privacy)
await videoInstance.save(sequelizeOptions)
} 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()
asyncMiddleware(getUserInformation)
)
+usersRouter.get('/me/videos',
+ authenticate,
+ paginationValidator,
+ videosSortValidator,
+ setVideosSort,
+ setPagination,
+ asyncMiddleware(getUserVideos)
+)
+
usersRouter.get('/me/videos/:videoId/rating',
authenticate,
usersVideoRatingValidator,
// ---------------------------------------------------------------------------
+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 ],
}
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) {
REQUEST_VIDEO_EVENT_TYPES,
VIDEO_CATEGORIES,
VIDEO_LICENCES,
- VIDEO_LANGUAGES
+ VIDEO_LANGUAGES,
+ VIDEO_PRIVACIES
} from '../../../initializers'
import {
addEventToRemoteVideo,
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'
videosRouter.get('/categories', listVideoCategories)
videosRouter.get('/licences', listVideoLicences)
videosRouter.get('/languages', listVideoLanguages)
+videosRouter.get('/privacies', listVideoPrivacies)
videosRouter.get('/',
paginationValidator,
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) {
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
}
// 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
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 => {
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)
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)
VIDEO_LICENCES,
VIDEO_LANGUAGES,
VIDEO_RATE_TYPES,
+ VIDEO_PRIVACIES,
database as db
} from '../../initializers'
import { isUserUsernameValid } from './users'
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)
isVideoDislikesValid,
isVideoEventCountValid,
isVideoFileSizeValid,
+ isVideoPrivacyValid,
+ isRemoteVideoPrivacyValid,
isVideoFileResolutionValid,
checkVideoExists,
isRemoteVideoCategoryValid,
RemoteVideoRequestType,
JobState
} from '../../shared/models'
+import { VideoPrivacy } from '../../shared/models/videos/video-privacy.enum'
// ---------------------------------------------------------------------------
-const LAST_MIGRATION_VERSION = 90
+const LAST_MIGRATION_VERSION = 95
// ---------------------------------------------------------------------------
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
THUMBNAILS_SIZE,
VIDEO_CATEGORIES,
VIDEO_LANGUAGES,
+ VIDEO_PRIVACIES,
VIDEO_LICENCES,
VIDEO_RATE_TYPES
}
--- /dev/null
+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
+}
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(
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) => {
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'),
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()
})
})
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,
generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData
list: VideoMethods.List
listForApi: VideoMethods.ListForApi
+ listUserVideosForApi: VideoMethods.ListUserVideosForApi
listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags
listOwnedByAuthor: VideoMethods.ListOwnedByAuthor
load: VideoMethods.Load
nsfw: boolean
description: string
duration: number
+ privacy: number
views?: number
likes?: number
dislikes?: number
isVideoNSFWValid,
isVideoDescriptionValid,
isVideoDurationValid,
+ isVideoPrivacyValid,
readFileBufferPromise,
unlinkPromise,
renamePromise,
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'
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
}
}
},
+ 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,
generateThumbnailFromData,
list,
listForApi,
+ listUserVideosForApi,
listOwnedAndPopulateAuthorAndTags,
listOwnedByAuthor,
load,
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: []
views: this.views,
likes: this.likes,
dislikes: this.dislikes,
+ privacy: this.privacy,
files: []
}
views: this.views,
likes: this.likes,
dislikes: this.dislikes,
+ privacy: this.privacy,
files: []
}
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,
}
]
},
- Video['sequelize'].models.Tag,
- Video['sequelize'].models.VideoFile
+ Video['sequelize'].models.Tag
],
where: createBaseVideosWhere()
}
model: Video['sequelize'].models.Tag
}
- const videoFileInclude: Sequelize.IncludeOptions = {
- model: Video['sequelize'].models.VideoFile
- }
-
const query: Sequelize.FindOptions<VideoAttributes> = {
distinct: true,
where: createBaseVideosWhere(),
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"
}
query.include = [
- videoChannelInclude, tagInclude, videoFileInclude
+ videoChannelInclude, tagInclude
]
return Video.findAndCountAll(query).then(({ rows, count }) => {
[Sequelize.Op.notIn]: Video['sequelize'].literal(
'(SELECT "BlacklistedVideos"."videoId" FROM "BlacklistedVideos")'
)
- }
+ },
+ privacy: VideoPrivacy.PUBLIC
}
}
views: number
likes: number
dislikes: number
+ privacy: number
thumbnailData: string
files: {
infoHash: string
views: number
likes: number
dislikes: number
+ privacy: number
files: {
infoHash: string
extname: string
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'
+import { VideoPrivacy } from './video-privacy.enum'
+
export interface VideoCreate {
category: number
licence: number
nsfw: boolean
name: string
tags: string[]
+ privacy: VideoPrivacy
}
--- /dev/null
+export enum VideoPrivacy {
+ PUBLIC = 1,
+ UNLISTED = 2,
+ PRIVATE = 3
+}
+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
}
import { VideoChannel } from './video-channel.model'
+import { VideoPrivacy } from './video-privacy.enum'
export interface VideoFile {
magnetUri: string
}
export interface VideoDetails extends Video {
- descriptionPath: string,
+ privacy: VideoPrivacy
+ privacyLabel: string
+ descriptionPath: string
channel: VideoChannel
files: VideoFile[]
}