Add concept of video state, and add ability to wait transcoding before
authorChocobozzz <me@florianbigard.com>
Tue, 12 Jun 2018 18:04:58 +0000 (20:04 +0200)
committerChocobozzz <me@florianbigard.com>
Tue, 12 Jun 2018 18:37:51 +0000 (20:37 +0200)
publishing a video

54 files changed:
client/src/app/+my-account/my-account-videos/my-account-videos.component.html
client/src/app/+my-account/my-account-videos/my-account-videos.component.ts
client/src/app/shared/video/video-details.model.ts
client/src/app/shared/video/video-edit.model.ts
client/src/app/shared/video/video.model.ts
client/src/app/shared/video/video.service.ts
client/src/app/videos/+video-edit/shared/video-edit.component.html
client/src/app/videos/+video-edit/shared/video-edit.component.ts
client/src/app/videos/+video-edit/video-add.component.ts
client/src/app/videos/+video-watch/video-watch.component.html
client/src/app/videos/+video-watch/video-watch.component.scss
client/src/app/videos/+video-watch/video-watch.component.ts
package.json
server/controllers/activitypub/client.ts
server/controllers/activitypub/outbox.ts
server/controllers/api/users.ts
server/controllers/api/videos/index.ts
server/helpers/activitypub.ts
server/helpers/custom-validators/activitypub/videos.ts
server/helpers/custom-validators/videos.ts
server/helpers/utils.ts
server/initializers/constants.ts
server/initializers/migrations/0220-video-state.ts [new file with mode: 0644]
server/lib/activitypub/audience.ts
server/lib/activitypub/crawl.ts
server/lib/activitypub/process/process-update.ts
server/lib/activitypub/send/send-announce.ts
server/lib/activitypub/send/send-create.ts
server/lib/activitypub/send/send-like.ts
server/lib/activitypub/send/send-undo.ts
server/lib/activitypub/send/send-update.ts
server/lib/activitypub/videos.ts
server/lib/job-queue/handlers/video-file.ts
server/lib/job-queue/job-queue.ts
server/middlewares/cache.ts
server/middlewares/validators/videos.ts
server/models/video/video.ts
server/tests/api/check-params/videos.ts
server/tests/api/videos/multiple-servers.ts
server/tests/api/videos/services.ts
server/tests/api/videos/video-transcoder.ts
server/tests/cli/create-transcoding-job.ts
server/tests/utils/videos/videos.ts
server/tools/import-videos.ts
server/tools/upload.ts
shared/models/activitypub/objects/video-torrent-object.ts
shared/models/videos/index.ts
shared/models/videos/video-create.model.ts
shared/models/videos/video-state.enum.ts [new file with mode: 0644]
shared/models/videos/video-update.model.ts
shared/models/videos/video.model.ts
support/doc/api/html/index.html
support/doc/api/openapi.yaml
support/doc/tools.md

index 35a99d0b3ee32614c42831934cef0380dd2503fa..eb24de7a7ff5287ee0caf3a3ea1fe1ccfa5400d4 100644 (file)
@@ -18,7 +18,7 @@
       <div class="video-info">
         <a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a>
         <span i18n class="video-info-date-views">{{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span>
-        <div class="video-info-private">{{ video.privacy.label }}</div>
+        <div class="video-info-private">{{ video.privacy.label }} - {{ getStateLabel(video) }}</div>
       </div>
 
       <!-- Display only once -->
index eed4be01f8ae0f4549445a86568ed74fde687754..afc01073c0c6a2d0f6d4f25aa8a84b31849e2523 100644 (file)
@@ -12,6 +12,7 @@ import { AbstractVideoList } from '../../shared/video/abstract-video-list'
 import { Video } from '../../shared/video/video.model'
 import { VideoService } from '../../shared/video/video.service'
 import { I18n } from '@ngx-translate/i18n-polyfill'
+import { VideoState } from '../../../../../shared/models/videos'
 
 @Component({
   selector: 'my-account-videos',
@@ -59,7 +60,7 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni
   }
 
   isInSelectionMode () {
-    return Object.keys(this.checkedVideos).some(k => this.checkedVideos[k] === true)
+    return Object.keys(this.checkedVideos).some(k => this.checkedVideos[ k ] === true)
   }
 
   getVideosObservable (page: number) {
@@ -74,47 +75,68 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni
 
   async deleteSelectedVideos () {
     const toDeleteVideosIds = Object.keys(this.checkedVideos)
-      .filter(k => this.checkedVideos[k] === true)
-      .map(k => parseInt(k, 10))
+                                    .filter(k => this.checkedVideos[ k ] === true)
+                                    .map(k => parseInt(k, 10))
 
-    const res = await this.confirmService.confirm(`Do you really want to delete ${toDeleteVideosIds.length} videos?`, 'Delete')
+    const res = await this.confirmService.confirm(
+      this.i18n('Do you really want to delete {{deleteLength}} videos?', { deleteLength: toDeleteVideosIds.length }),
+      this.i18n('Delete')
+    )
     if (res === false) return
 
     const observables: Observable<any>[] = []
     for (const videoId of toDeleteVideosIds) {
-      const o = this.videoService
-                    .removeVideo(videoId)
+      const o = this.videoService.removeVideo(videoId)
                     .pipe(tap(() => this.spliceVideosById(videoId)))
 
       observables.push(o)
     }
 
-    observableFrom(observables).pipe(
-      concatAll())
+    observableFrom(observables)
+      .pipe(concatAll())
       .subscribe(
         res => {
-          this.notificationsService.success('Success', `${toDeleteVideosIds.length} videos deleted.`)
+          this.notificationsService.success(
+            this.i18n('Success'),
+            this.i18n('{{deleteLength}} videos deleted.', { deleteLength: toDeleteVideosIds.length })
+          )
+
           this.abortSelectionMode()
           this.reloadVideos()
         },
 
-        err => this.notificationsService.error('Error', err.message)
+        err => this.notificationsService.error(this.i18n('Error'), err.message)
       )
   }
 
   async deleteVideo (video: Video) {
-    const res = await this.confirmService.confirm(`Do you really want to delete ${video.name}?`, 'Delete')
+    const res = await this.confirmService.confirm(
+      this.i18n('Do you really want to delete {{videoName}}?', { videoName: video.name }),
+      this.i18n('Delete')
+    )
     if (res === false) return
 
     this.videoService.removeVideo(video.id)
-      .subscribe(
-        status => {
-          this.notificationsService.success('Success', `Video ${video.name} deleted.`)
-          this.reloadVideos()
-        },
+        .subscribe(
+          status => {
+            this.notificationsService.success(
+              this.i18n('Success'),
+              this.i18n('Video {{videoName}} deleted.', { videoName: video.name })
+            )
+            this.reloadVideos()
+          },
+
+          error => this.notificationsService.error(this.i18n('Error'), error.message)
+        )
+  }
 
-        error => this.notificationsService.error('Error', error.message)
-      )
+  getStateLabel (video: Video) {
+    if (video.state.id === VideoState.PUBLISHED) return this.i18n('Published')
+
+    if (video.state.id === VideoState.TO_TRANSCODE && video.waitTranscoding === true) return this.i18n('Waiting transcoding')
+    if (video.state.id === VideoState.TO_TRANSCODE) return this.i18n('To transcode')
+
+    return this.i18n('Unknown state')
   }
 
   protected buildVideoHeight () {
@@ -124,7 +146,7 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni
 
   private spliceVideosById (id: number) {
     for (const key of Object.keys(this.loadedPages)) {
-      const videos = this.loadedPages[key]
+      const videos = this.loadedPages[ key ]
       const index = videos.findIndex(v => v.id === id)
 
       if (index !== -1) {
index 19c350ab34fdcc146d80e0334587db944df8661f..e500ad6fc37208ef22c81aa92d8f1768b4cfbace 100644 (file)
@@ -1,4 +1,11 @@
-import { UserRight, VideoChannel, VideoDetails as VideoDetailsServerModel, VideoFile } from '../../../../../shared'
+import {
+  UserRight,
+  VideoChannel,
+  VideoConstant,
+  VideoDetails as VideoDetailsServerModel,
+  VideoFile,
+  VideoState
+} from '../../../../../shared'
 import { AuthUser } from '../../core'
 import { Video } from '../../shared/video/video.model'
 import { Account } from '@app/shared/account/account.model'
@@ -12,6 +19,9 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
   account: Account
   commentsEnabled: boolean
 
+  waitTranscoding: boolean
+  state: VideoConstant<VideoState>
+
   likesPercent: number
   dislikesPercent: number
 
index ad2929db5f194a47f739f407d2198e33d07c49d0..f045a3acd294817ed4194b5875da509ca3f78356 100644 (file)
@@ -1,7 +1,8 @@
 import { VideoDetails } from './video-details.model'
 import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy.enum'
+import { VideoUpdate } from '../../../../../shared/models/videos'
 
-export class VideoEdit {
+export class VideoEdit implements VideoUpdate {
   category: number
   licence: number
   language: string
@@ -10,6 +11,7 @@ export class VideoEdit {
   tags: string[]
   nsfw: boolean
   commentsEnabled: boolean
+  waitTranscoding: boolean
   channelId: number
   privacy: VideoPrivacy
   support: string
@@ -32,6 +34,7 @@ export class VideoEdit {
       this.tags = videoDetails.tags
       this.nsfw = videoDetails.nsfw
       this.commentsEnabled = videoDetails.commentsEnabled
+      this.waitTranscoding = videoDetails.waitTranscoding
       this.channelId = videoDetails.channel.id
       this.privacy = videoDetails.privacy.id
       this.support = videoDetails.support
@@ -42,7 +45,7 @@ export class VideoEdit {
 
   patch (values: Object) {
     Object.keys(values).forEach((key) => {
-      this[key] = values[key]
+      this[ key ] = values[ key ]
     })
   }
 
@@ -57,6 +60,7 @@ export class VideoEdit {
       tags: this.tags,
       nsfw: this.nsfw,
       commentsEnabled: this.commentsEnabled,
+      waitTranscoding: this.waitTranscoding,
       channelId: this.channelId,
       privacy: this.privacy
     }
index d37dc2c3ef969d431f962278b7e0d2045ca83109..48a4b4260f9ff37c7381591837304d5bb959d42d 100644 (file)
@@ -1,5 +1,5 @@
 import { User } from '../'
-import { Video as VideoServerModel, VideoPrivacy } from '../../../../../shared'
+import { Video as VideoServerModel, VideoPrivacy, VideoState } from '../../../../../shared'
 import { Avatar } from '../../../../../shared/models/avatars/avatar.model'
 import { VideoConstant } from '../../../../../shared/models/videos/video.model'
 import { getAbsoluteAPIUrl } from '../misc/utils'
@@ -36,6 +36,9 @@ export class Video implements VideoServerModel {
   dislikes: number
   nsfw: boolean
 
+  waitTranscoding?: boolean
+  state?: VideoConstant<VideoState>
+
   account: {
     id: number
     uuid: string
@@ -58,15 +61,14 @@ export class Video implements VideoServerModel {
 
   private static createDurationString (duration: number) {
     const hours = Math.floor(duration / 3600)
-    const minutes = Math.floor(duration % 3600 / 60)
+    const minutes = Math.floor((duration % 3600) / 60)
     const seconds = duration % 60
 
     const minutesPadding = minutes >= 10 ? '' : '0'
     const secondsPadding = seconds >= 10 ? '' : '0'
     const displayedHours = hours > 0 ? hours.toString() + ':' : ''
 
-    return displayedHours + minutesPadding +
-        minutes.toString() + ':' + secondsPadding + seconds.toString()
+    return displayedHours + minutesPadding + minutes.toString() + ':' + secondsPadding + seconds.toString()
   }
 
   constructor (hash: VideoServerModel, translations = {}) {
@@ -78,6 +80,8 @@ export class Video implements VideoServerModel {
     this.licence = hash.licence
     this.language = hash.language
     this.privacy = hash.privacy
+    this.waitTranscoding = hash.waitTranscoding
+    this.state = hash.state
     this.description = hash.description
     this.duration = hash.duration
     this.durationLabel = Video.createDurationString(hash.duration)
@@ -104,6 +108,8 @@ export class Video implements VideoServerModel {
     this.licence.label = peertubeTranslate(this.licence.label, translations)
     this.language.label = peertubeTranslate(this.language.label, translations)
     this.privacy.label = peertubeTranslate(this.privacy.label, translations)
+
+    if (this.state) this.state.label = peertubeTranslate(this.state.label, translations)
   }
 
   isVideoNSFWForUser (user: User, serverConfig: ServerConfig) {
index 58cb52efcbc6a8239b8afe9437a565c094610cfd..d63915ad238b06f851ae88917f727eaaa21e3e6e 100644 (file)
@@ -80,6 +80,7 @@ export class VideoService {
       privacy: video.privacy,
       tags: video.tags,
       nsfw: video.nsfw,
+      waitTranscoding: video.waitTranscoding,
       commentsEnabled: video.commentsEnabled,
       thumbnailfile: video.thumbnailfile,
       previewfile: video.previewfile
@@ -98,11 +99,11 @@ export class VideoService {
     const req = new HttpRequest('POST', VideoService.BASE_VIDEO_URL + 'upload', video, { reportProgress: true })
 
     return this.authHttp
-               .request<{ video: { id: number, uuid: string} }>(req)
+               .request<{ video: { id: number, uuid: string } }>(req)
                .pipe(catchError(this.restExtractor.handleError))
   }
 
-  getMyVideos (videoPagination: ComponentPagination, sort: VideoSortField): Observable<{ videos: Video[], totalVideos: number}> {
+  getMyVideos (videoPagination: ComponentPagination, sort: VideoSortField): Observable<{ videos: Video[], totalVideos: number }> {
     const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
 
     let params = new HttpParams()
@@ -120,7 +121,7 @@ export class VideoService {
     account: Account,
     videoPagination: ComponentPagination,
     sort: VideoSortField
-  ): Observable<{ videos: Video[], totalVideos: number}> {
+  ): Observable<{ videos: Video[], totalVideos: number }> {
     const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
 
     let params = new HttpParams()
@@ -138,7 +139,7 @@ export class VideoService {
     videoChannel: VideoChannel,
     videoPagination: ComponentPagination,
     sort: VideoSortField
-  ): Observable<{ videos: Video[], totalVideos: number}> {
+  ): Observable<{ videos: Video[], totalVideos: number }> {
     const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
 
     let params = new HttpParams()
@@ -156,7 +157,7 @@ export class VideoService {
     videoPagination: ComponentPagination,
     sort: VideoSortField,
     filter?: VideoFilter
-  ): Observable<{ videos: Video[], totalVideos: number}> {
+  ): Observable<{ videos: Video[], totalVideos: number }> {
     const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
 
     let params = new HttpParams()
@@ -225,7 +226,7 @@ export class VideoService {
     search: string,
     videoPagination: ComponentPagination,
     sort: VideoSortField
-  ): Observable<{ videos: Video[], totalVideos: number}> {
+  ): Observable<{ videos: Video[], totalVideos: number }> {
     const url = VideoService.BASE_VIDEO_URL + 'search'
 
     const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
@@ -295,18 +296,18 @@ export class VideoService {
 
   private extractVideos (result: ResultList<VideoServerModel>) {
     return this.serverService.localeObservable
-      .pipe(
-        map(translations => {
-          const videosJson = result.data
-          const totalVideos = result.total
-          const videos: Video[] = []
-
-          for (const videoJson of videosJson) {
-            videos.push(new Video(videoJson, translations))
-          }
-
-          return { videos, totalVideos }
-        })
-      )
+               .pipe(
+                 map(translations => {
+                   const videosJson = result.data
+                   const totalVideos = result.total
+                   const videos: Video[] = []
+
+                   for (const videoJson of videosJson) {
+                     videos.push(new Video(videoJson, translations))
+                   }
+
+                   return { videos, totalVideos }
+                 })
+               )
   }
 }
index c8cd0d679c05a1f70f671a2ae43a812779703510..379cf7948d320766091f63d9fc239d1ac578f58f 100644 (file)
           <label i18n for="commentsEnabled">Enable video comments</label>
         </div>
 
+        <div class="form-group form-group-checkbox">
+          <input type="checkbox" id="waitTranscoding" formControlName="waitTranscoding" />
+          <label for="waitTranscoding"></label>
+          <label i18n for="waitTranscoding">Wait transcoding before publishing the video</label>
+          <my-help
+            tooltipPlacement="top" helpType="custom" i18n-customHtml
+            customHtml="If you decide to not wait transcoding before publishing the video, it can be unplayable until it transcoding ends."
+          ></my-help>
+        </div>
+
       </div>
     </tab>
 
index 61515c0b0cb4069b958e0294814c6cd1947b75fa..ee4fd5dc1b24a0ee9b98b041cbdd0f31694a7b3f 100644 (file)
@@ -47,6 +47,7 @@ export class VideoEditComponent implements OnInit {
     const defaultValues = {
       nsfw: 'false',
       commentsEnabled: 'true',
+      waitTranscoding: 'true',
       tags: []
     }
     const obj = {
@@ -55,6 +56,7 @@ export class VideoEditComponent implements OnInit {
       channelId: this.videoValidatorsService.VIDEO_CHANNEL,
       nsfw: null,
       commentsEnabled: null,
+      waitTranscoding: null,
       category: this.videoValidatorsService.VIDEO_CATEGORY,
       licence: this.videoValidatorsService.VIDEO_LICENCE,
       language: this.videoValidatorsService.VIDEO_LANGUAGE,
@@ -74,13 +76,13 @@ export class VideoEditComponent implements OnInit {
     )
 
     // We will update the "support" field depending on the channel
-    this.form.controls['channelId']
+    this.form.controls[ 'channelId' ]
       .valueChanges
       .pipe(map(res => parseInt(res.toString(), 10)))
       .subscribe(
         newChannelId => {
-          const oldChannelId = parseInt(this.form.value['channelId'], 10)
-          const currentSupport = this.form.value['support']
+          const oldChannelId = parseInt(this.form.value[ 'channelId' ], 10)
+          const currentSupport = this.form.value[ 'support' ]
 
           // Not initialized yet
           if (isNaN(newChannelId)) return
index 332f757d73cacddfe4e71f17a5a56f67e5e0056c..85afd0caa1c2bf075c2548d712fd653c72a0a04a 100644 (file)
@@ -164,6 +164,7 @@ export class VideoAddComponent extends FormReactive implements OnInit, OnDestroy
 
     const privacy = this.firstStepPrivacyId.toString()
     const nsfw = false
+    const waitTranscoding = true
     const commentsEnabled = true
     const channelId = this.firstStepChannelId.toString()
 
@@ -173,6 +174,7 @@ export class VideoAddComponent extends FormReactive implements OnInit, OnDestroy
     formData.append('privacy', VideoPrivacy.PRIVATE.toString())
     formData.append('nsfw', '' + nsfw)
     formData.append('commentsEnabled', '' + commentsEnabled)
+    formData.append('waitTranscoding', '' + waitTranscoding)
     formData.append('channelId', '' + channelId)
     formData.append('videofile', videofile)
 
index 4c650b121badeaa58c642afe029a2e1a2eb0e429..8bd5c00ff6637772ea1ce6fa8e0cdc0f26b032fe 100644 (file)
@@ -3,6 +3,10 @@
   <div id="video-element-wrapper">
   </div>
 
+  <div i18n id="warning-transcoding" class="alert alert-warning" *ngIf="isVideoToTranscode()">
+    The video is being transcoded, it may not work properly.
+  </div>
+
   <!-- Video information -->
   <div *ngIf="video" class="margin-content video-bottom">
     <div class="video-info">
index 00e776a69a0f232d24fe3fed404c60194c973547..06dd75653ad5266fa7764a415815d0f68edc0a57 100644 (file)
   }
 }
 
+#warning-transcoding {
+  text-align: center;
+}
+
 #video-not-found {
   height: 300px;
   line-height: 300px;
index eefa43a73bde51ae4d543e1ccc7cd7627dbd4a51..498542fff211493ec5f4610869d2fb5f432424b8 100644 (file)
@@ -1,5 +1,5 @@
 import { catchError } from 'rxjs/operators'
-import { Component, ElementRef, LOCALE_ID, NgZone, OnDestroy, OnInit, ViewChild, Inject } from '@angular/core'
+import { Component, ElementRef, Inject, LOCALE_ID, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core'
 import { ActivatedRoute, Router } from '@angular/router'
 import { RedirectService } from '@app/core/routing/redirect.service'
 import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage'
@@ -10,7 +10,7 @@ import { Subscription } from 'rxjs'
 import * as videojs from 'video.js'
 import 'videojs-hotkeys'
 import * as WebTorrent from 'webtorrent'
-import { UserVideoRateType, VideoRateType } from '../../../../../shared'
+import { UserVideoRateType, VideoRateType, VideoState } from '../../../../../shared'
 import '../../../assets/player/peertube-videojs-plugin'
 import { AuthService, ConfirmService } from '../../core'
 import { RestExtractor, VideoBlacklistService } from '../../shared'
@@ -21,7 +21,7 @@ import { MarkdownService } from '../shared'
 import { VideoDownloadComponent } from './modal/video-download.component'
 import { VideoReportComponent } from './modal/video-report.component'
 import { VideoShareComponent } from './modal/video-share.component'
-import { getVideojsOptions, loadLocale, addContextMenu } from '../../../assets/player/peertube-player'
+import { addContextMenu, getVideojsOptions, loadLocale } from '../../../assets/player/peertube-player'
 import { ServerService } from '@app/core'
 import { I18n } from '@ngx-translate/i18n-polyfill'
 import { environment } from '../../../environments/environment'
@@ -91,21 +91,21 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
     }
 
     this.videoService.getVideos({ currentPage: 1, itemsPerPage: 5 }, '-createdAt')
-      .subscribe(
-        data => {
-          this.otherVideos = data.videos
-          this.updateOtherVideosDisplayed()
-        },
+        .subscribe(
+          data => {
+            this.otherVideos = data.videos
+            this.updateOtherVideosDisplayed()
+          },
 
-        err => console.error(err)
-      )
+          err => console.error(err)
+        )
 
     this.paramsSub = this.route.params.subscribe(routeParams => {
       if (this.player) {
         this.player.pause()
       }
 
-      const uuid = routeParams['uuid']
+      const uuid = routeParams[ 'uuid' ]
 
       // Video did not change
       if (this.video && this.video.uuid === uuid) return
@@ -113,13 +113,11 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
       this.videoService
           .getVideo(uuid)
           .pipe(catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 404 ])))
-          .subscribe(
-            video => {
-              const startTime = this.route.snapshot.queryParams.start
-              this.onVideoFetched(video, startTime)
-                  .catch(err => this.handleError(err))
-            }
-          )
+          .subscribe(video => {
+            const startTime = this.route.snapshot.queryParams.start
+            this.onVideoFetched(video, startTime)
+                .catch(err => this.handleError(err))
+          })
     })
   }
 
@@ -157,17 +155,17 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
     if (res === false) return
 
     this.videoBlacklistService.blacklistVideo(this.video.id)
-                              .subscribe(
-                                status => {
-                                  this.notificationsService.success(
-                                    this.i18n('Success'),
-                                    this.i18n('Video {{videoName}} had been blacklisted.', { videoName: this.video.name })
-                                  )
-                                  this.redirectService.redirectToHomepage()
-                                },
+        .subscribe(
+          () => {
+            this.notificationsService.success(
+              this.i18n('Success'),
+              this.i18n('Video {{videoName}} had been blacklisted.', { videoName: this.video.name })
+            )
+            this.redirectService.redirectToHomepage()
+          },
 
-                                error => this.notificationsService.error(this.i18n('Error'), error.message)
-                              )
+          error => this.notificationsService.error(this.i18n('Error'), error.message)
+        )
   }
 
   showMoreDescription () {
@@ -188,22 +186,22 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
     this.descriptionLoading = true
 
     this.videoService.loadCompleteDescription(this.video.descriptionPath)
-      .subscribe(
-        description => {
-          this.completeDescriptionShown = true
-          this.descriptionLoading = false
+        .subscribe(
+          description => {
+            this.completeDescriptionShown = true
+            this.descriptionLoading = false
 
-          this.shortVideoDescription = this.video.description
-          this.completeVideoDescription = description
+            this.shortVideoDescription = this.video.description
+            this.completeVideoDescription = description
 
-          this.updateVideoDescription(this.completeVideoDescription)
-        },
+            this.updateVideoDescription(this.completeVideoDescription)
+          },
 
-        error => {
-          this.descriptionLoading = false
-          this.notificationsService.error(this.i18n('Error'), error.message)
-        }
-      )
+          error => {
+            this.descriptionLoading = false
+            this.notificationsService.error(this.i18n('Error'), error.message)
+          }
+        )
   }
 
   showReportModal (event: Event) {
@@ -259,19 +257,19 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
     if (res === false) return
 
     this.videoService.removeVideo(this.video.id)
-      .subscribe(
-        status => {
-          this.notificationsService.success(
-            this.i18n('Success'),
-            this.i18n('Video {{videoName}} deleted.', { videoName: this.video.name })
-          )
+        .subscribe(
+          status => {
+            this.notificationsService.success(
+              this.i18n('Success'),
+              this.i18n('Video {{videoName}} deleted.', { videoName: this.video.name })
+            )
 
-          // Go back to the video-list.
-          this.redirectService.redirectToHomepage()
-        },
+            // Go back to the video-list.
+            this.redirectService.redirectToHomepage()
+          },
 
-        error => this.notificationsService.error(this.i18n('Error'), error.message)
-      )
+          error => this.notificationsService.error(this.i18n('Error'), error.message)
+        )
   }
 
   acceptedPrivacyConcern () {
@@ -279,6 +277,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
     this.hasAlreadyAcceptedPrivacyConcern = true
   }
 
+  isVideoToTranscode () {
+    return this.video && this.video.state.id === VideoState.TO_TRANSCODE
+  }
+
   private updateVideoDescription (description: string) {
     this.video.description = description
     this.setVideoDescriptionHTML()
@@ -294,10 +296,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
   }
 
   private setVideoLikesBarTooltipText () {
-    this.likesBarTooltipText = this.i18n(
-      '{{likesNumber}} likes / {{dislikesNumber}} dislikes',
-      { likesNumber: this.video.likes, dislikesNumber: this.video.dislikes }
-    )
+    this.likesBarTooltipText = this.i18n('{{likesNumber}} likes / {{dislikesNumber}} dislikes', {
+      likesNumber: this.video.likes,
+      dislikesNumber: this.video.dislikes
+    })
   }
 
   private handleError (err: any) {
@@ -320,15 +322,15 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
     if (this.isUserLoggedIn() === false) return
 
     this.videoService.getUserVideoRating(this.video.id)
-                     .subscribe(
-                       ratingObject => {
-                         if (ratingObject) {
-                           this.userRating = ratingObject.rating
-                         }
-                       },
+        .subscribe(
+          ratingObject => {
+            if (ratingObject) {
+              this.userRating = ratingObject.rating
+            }
+          },
 
-                       err => this.notificationsService.error(this.i18n('Error'), err.message)
-                      )
+          err => this.notificationsService.error(this.i18n('Error'), err.message)
+        )
   }
 
   private async onVideoFetched (video: VideoDetails, startTime = 0) {
@@ -409,14 +411,15 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
     }
 
     method.call(this.videoService, this.video.id)
-     .subscribe(
-      () => {
-        // Update the video like attribute
-        this.updateVideoRating(this.userRating, nextRating)
-        this.userRating = nextRating
-      },
-      err => this.notificationsService.error(this.i18n('Error'), err.message)
-     )
+          .subscribe(
+            () => {
+              // Update the video like attribute
+              this.updateVideoRating(this.userRating, nextRating)
+              this.userRating = nextRating
+            },
+
+            err => this.notificationsService.error(this.i18n('Error'), err.message)
+          )
   }
 
   private updateVideoRating (oldRating: UserVideoRateType, newRating: VideoRateType) {
index 8d25613b64a188a806c3f22bfc3f115dfbbea608..739978a18be3e1a22f1783da106e67e47b5ffda8 100644 (file)
@@ -68,7 +68,6 @@
     }
   },
   "lint-staged": {
-    "*.{css,md}": "precise-commits",
     "*.scss": [
       "sass-lint -c .sass-lint.yml",
       "git add"
     "maildev": "^1.0.0-rc3",
     "mocha": "^5.0.0",
     "nodemon": "^1.11.0",
-    "precise-commits": "^1.0.2",
     "prettier": "1.13.2",
     "prompt": "^1.0.0",
     "sass-lint": "^1.12.1",
index 1c780783c013c3ed623d3da7b1e136abcbb51498..ea8e25f6853c033e70bd6ac64656bebbcfe42c85 100644 (file)
@@ -123,11 +123,11 @@ async function accountFollowingController (req: express.Request, res: express.Re
 async function videoController (req: express.Request, res: express.Response, next: express.NextFunction) {
   const video: VideoModel = res.locals.video
 
-  const audience = await getAudience(video.VideoChannel.Account.Actor, undefined, video.privacy === VideoPrivacy.PUBLIC)
+  const audience = getAudience(video.VideoChannel.Account.Actor, video.privacy === VideoPrivacy.PUBLIC)
   const videoObject = audiencify(video.toActivityPubObject(), audience)
 
   if (req.path.endsWith('/activity')) {
-    const data = await createActivityData(video.url, video.VideoChannel.Account.Actor, videoObject, undefined, audience)
+    const data = createActivityData(video.url, video.VideoChannel.Account.Actor, videoObject, audience)
     return activityPubResponse(activityPubContextify(data), res)
   }
 
@@ -210,12 +210,12 @@ async function videoCommentController (req: express.Request, res: express.Respon
 
   const threadParentComments = await VideoCommentModel.listThreadParentComments(videoComment, undefined)
   const isPublic = true // Comments are always public
-  const audience = await getAudience(videoComment.Account.Actor, undefined, isPublic)
+  const audience = getAudience(videoComment.Account.Actor, isPublic)
 
   const videoCommentObject = audiencify(videoComment.toActivityPubObject(threadParentComments), audience)
 
   if (req.path.endsWith('/activity')) {
-    const data = await createActivityData(videoComment.url, videoComment.Account.Actor, videoCommentObject, undefined, audience)
+    const data = createActivityData(videoComment.url, videoComment.Account.Actor, videoCommentObject, audience)
     return activityPubResponse(activityPubContextify(data), res)
   }
 
index 2793ae2677739961f461458e2253e9504d9be0dd..ae7adcd4c30aad11ab782c97261009fd0442f587 100644 (file)
@@ -54,12 +54,12 @@ async function buildActivities (actor: ActorModel, start: number, count: number)
     // This is a shared video
     if (video.VideoShares !== undefined && video.VideoShares.length !== 0) {
       const videoShare = video.VideoShares[0]
-      const announceActivity = await announceActivityData(videoShare.url, actor, video.url, undefined, createActivityAudience)
+      const announceActivity = announceActivityData(videoShare.url, actor, video.url, createActivityAudience)
 
       activities.push(announceActivity)
     } else {
       const videoObject = video.toActivityPubObject()
-      const createActivity = await createActivityData(video.url, byActor, videoObject, undefined, createActivityAudience)
+      const createActivity = createActivityData(video.url, byActor, videoObject, createActivityAudience)
 
       activities.push(createActivity)
     }
index 8dff4b87c63a00b0aceed357f34dc265ac66d3bf..2b40c44d95887608850a2b0ea556c95952043c66 100644 (file)
@@ -166,7 +166,7 @@ export {
 
 async function getUserVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
   const user = res.locals.oauth.token.User as UserModel
-  const resultList = await VideoModel.listAccountVideosForApi(
+  const resultList = await VideoModel.listUserVideosForApi(
     user.Account.id,
     req.query.start as number,
     req.query.count as number,
@@ -174,7 +174,8 @@ async function getUserVideos (req: express.Request, res: express.Response, next:
     false // Display my NSFW videos
   )
 
-  return res.json(getFormattedObjects(resultList.data, resultList.total))
+  const additionalAttributes = { waitTranscoding: true, state: true }
+  return res.json(getFormattedObjects(resultList.data, resultList.total, { additionalAttributes }))
 }
 
 async function createUserRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) {
@@ -318,7 +319,7 @@ async function updateMe (req: express.Request, res: express.Response, next: expr
 }
 
 async function updateMyAvatar (req: express.Request, res: express.Response, next: express.NextFunction) {
-  const avatarPhysicalFile = req.files['avatarfile'][0]
+  const avatarPhysicalFile = req.files[ 'avatarfile' ][ 0 ]
   const user = res.locals.oauth.token.user
   const actor = user.Account.Actor
 
index 7f5e74626e401714667d6be9c53d702e04116250..9d9b2b0e19d2acb7083835205aa3698e98d1630b 100644 (file)
@@ -1,6 +1,6 @@
 import * as express from 'express'
 import { extname, join } from 'path'
-import { VideoCreate, VideoPrivacy, VideoUpdate } from '../../../../shared'
+import { VideoCreate, VideoPrivacy, VideoState, VideoUpdate } from '../../../../shared'
 import { renamePromise } from '../../../helpers/core-utils'
 import { retryTransactionWrapper } from '../../../helpers/database-utils'
 import { getVideoFileResolution } from '../../../helpers/ffmpeg-utils'
@@ -21,11 +21,11 @@ import {
 } from '../../../initializers'
 import {
   changeVideoChannelShare,
+  federateVideoIfNeeded,
   fetchRemoteVideoDescription,
-  getVideoActivityPubUrl,
-  shareVideoByServerAndChannel
+  getVideoActivityPubUrl
 } from '../../../lib/activitypub'
-import { sendCreateVideo, sendCreateView, sendUpdateVideo } from '../../../lib/activitypub/send'
+import { sendCreateView } from '../../../lib/activitypub/send'
 import { JobQueue } from '../../../lib/job-queue'
 import { Redis } from '../../../lib/redis'
 import {
@@ -51,7 +51,7 @@ import { videoCommentRouter } from './comment'
 import { rateVideoRouter } from './rate'
 import { VideoFilter } from '../../../../shared/models/videos/video-query.type'
 import { VideoSortField } from '../../../../client/src/app/shared/video/sort-field.type'
-import { isNSFWHidden, createReqFiles } from '../../../helpers/express-utils'
+import { createReqFiles, isNSFWHidden } from '../../../helpers/express-utils'
 
 const videosRouter = express.Router()
 
@@ -185,8 +185,10 @@ async function addVideo (req: express.Request, res: express.Response, videoPhysi
     category: videoInfo.category,
     licence: videoInfo.licence,
     language: videoInfo.language,
-    commentsEnabled: videoInfo.commentsEnabled,
-    nsfw: videoInfo.nsfw,
+    commentsEnabled: videoInfo.commentsEnabled || false,
+    waitTranscoding: videoInfo.waitTranscoding || false,
+    state: CONFIG.TRANSCODING.ENABLED ? VideoState.TO_TRANSCODE : VideoState.PUBLISHED,
+    nsfw: videoInfo.nsfw || false,
     description: videoInfo.description,
     support: videoInfo.support,
     privacy: videoInfo.privacy,
@@ -194,19 +196,20 @@ async function addVideo (req: express.Request, res: express.Response, videoPhysi
     channelId: res.locals.videoChannel.id
   }
   const video = new VideoModel(videoData)
-  video.url = getVideoActivityPubUrl(video)
+  video.url = getVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object
 
+  // Build the file object
   const { videoFileResolution } = await getVideoFileResolution(videoPhysicalFile.path)
-
   const videoFileData = {
     extname: extname(videoPhysicalFile.filename),
     resolution: videoFileResolution,
     size: videoPhysicalFile.size
   }
   const videoFile = new VideoFileModel(videoFileData)
+
+  // Move physical file
   const videoDir = CONFIG.STORAGE.VIDEOS_DIR
   const destination = join(videoDir, video.getVideoFilename(videoFile))
-
   await renamePromise(videoPhysicalFile.path, destination)
   // This is important in case if there is another attempt in the retry process
   videoPhysicalFile.filename = video.getVideoFilename(videoFile)
@@ -230,6 +233,7 @@ async function addVideo (req: express.Request, res: express.Response, videoPhysi
     await video.createPreview(videoFile)
   }
 
+  // Create the torrent file
   await video.createTorrentAndSetInfoHash(videoFile)
 
   const videoCreated = await sequelizeTypescript.transaction(async t => {
@@ -251,20 +255,14 @@ async function addVideo (req: express.Request, res: express.Response, videoPhysi
       video.Tags = tagInstances
     }
 
-    // Let transcoding job send the video to friends because the video file extension might change
-    if (CONFIG.TRANSCODING.ENABLED === true) return videoCreated
-    // Don't send video to remote servers, it is private
-    if (video.privacy === VideoPrivacy.PRIVATE) return videoCreated
-
-    await sendCreateVideo(video, t)
-    await shareVideoByServerAndChannel(video, t)
+    await federateVideoIfNeeded(video, true, t)
 
     logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid)
 
     return videoCreated
   })
 
-  if (CONFIG.TRANSCODING.ENABLED === true) {
+  if (video.state === VideoState.TO_TRANSCODE) {
     // Put uuid because we don't have id auto incremented for now
     const dataInput = {
       videoUUID: videoCreated.uuid,
@@ -318,6 +316,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.waitTranscoding !== undefined) videoInstance.set('waitTranscoding', videoInfoToUpdate.waitTranscoding)
       if (videoInfoToUpdate.support !== undefined) videoInstance.set('support', videoInfoToUpdate.support)
       if (videoInfoToUpdate.description !== undefined) videoInstance.set('description', videoInfoToUpdate.description)
       if (videoInfoToUpdate.commentsEnabled !== undefined) videoInstance.set('commentsEnabled', videoInfoToUpdate.commentsEnabled)
@@ -343,19 +342,13 @@ async function updateVideo (req: express.Request, res: express.Response) {
       // Video channel update?
       if (res.locals.videoChannel && videoInstanceUpdated.channelId !== res.locals.videoChannel.id) {
         await videoInstanceUpdated.$set('VideoChannel', res.locals.videoChannel, { transaction: t })
-        videoInstance.VideoChannel = res.locals.videoChannel
+        videoInstanceUpdated.VideoChannel = res.locals.videoChannel
 
         if (wasPrivateVideo === false) await changeVideoChannelShare(videoInstanceUpdated, oldVideoChannel, t)
       }
 
-      // Now we'll update the video's meta data to our friends
-      if (wasPrivateVideo === false) await sendUpdateVideo(videoInstanceUpdated, t)
-
-      // Video is not private anymore, send a create action to remote servers
-      if (wasPrivateVideo === true && videoInstanceUpdated.privacy !== VideoPrivacy.PRIVATE) {
-        await sendCreateVideo(videoInstanceUpdated, t)
-        await shareVideoByServerAndChannel(videoInstanceUpdated, t)
-      }
+      const isNewVideo = wasPrivateVideo && videoInstanceUpdated.privacy !== VideoPrivacy.PRIVATE
+      await federateVideoIfNeeded(videoInstanceUpdated, isNewVideo)
     })
 
     logger.info('Video with name %s and uuid %s updated.', videoInstance.name, videoInstance.uuid)
index d1f3ec02d711fedb0c79b65e43b0fec8b1295451..37a251697ac63da9a7a0ec9f07aa7291ed80661b 100644 (file)
@@ -8,22 +8,24 @@ import { signObject } from './peertube-crypto'
 import { pageToStartAndCount } from './core-utils'
 
 function activityPubContextify <T> (data: T) {
-  return Object.assign(data,{
+  return Object.assign(data, {
     '@context': [
       'https://www.w3.org/ns/activitystreams',
       'https://w3id.org/security/v1',
       {
-        'RsaSignature2017': 'https://w3id.org/security#RsaSignature2017',
-        'Hashtag': 'as:Hashtag',
-        'uuid': 'http://schema.org/identifier',
-        'category': 'http://schema.org/category',
-        'licence': 'http://schema.org/license',
-        'sensitive': 'as:sensitive',
-        'language': 'http://schema.org/inLanguage',
-        'views': 'http://schema.org/Number',
-        'size': 'http://schema.org/Number',
-        'commentsEnabled': 'http://schema.org/Boolean',
-        'support': 'http://schema.org/Text'
+        RsaSignature2017: 'https://w3id.org/security#RsaSignature2017',
+        Hashtag: 'as:Hashtag',
+        uuid: 'http://schema.org/identifier',
+        category: 'http://schema.org/category',
+        licence: 'http://schema.org/license',
+        sensitive: 'as:sensitive',
+        language: 'http://schema.org/inLanguage',
+        views: 'http://schema.org/Number',
+        stats: 'http://schema.org/Number',
+        size: 'http://schema.org/Number',
+        commentsEnabled: 'http://schema.org/Boolean',
+        waitTranscoding: 'http://schema.org/Boolean',
+        support: 'http://schema.org/Text'
       },
       {
         likes: {
index 7e1d57c347e40c21ecdddc7b362221ac3fb7dcd6..37c90a0c85f18b5847ef8316c09a1b31b431274c 100644 (file)
@@ -6,11 +6,13 @@ import {
   isVideoAbuseReasonValid,
   isVideoDurationValid,
   isVideoNameValid,
+  isVideoStateValid,
   isVideoTagValid,
   isVideoTruncatedDescriptionValid,
   isVideoViewsValid
 } from '../videos'
 import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc'
+import { VideoState } from '../../../../shared/models/videos'
 
 function sanitizeAndCheckVideoTorrentCreateActivity (activity: any) {
   return isBaseActivityValid(activity, 'Create') &&
@@ -50,6 +52,10 @@ function sanitizeAndCheckVideoTorrentObject (video: any) {
   if (!setRemoteVideoTruncatedContent(video)) return false
   if (!setValidAttributedTo(video)) return false
 
+  // Default attributes
+  if (!isVideoStateValid(video.state)) video.state = VideoState.PUBLISHED
+  if (!isBooleanValid(video.waitTranscoding)) video.waitTranscoding = false
+
   return isActivityPubUrlValid(video.id) &&
     isVideoNameValid(video.name) &&
     isActivityPubVideoDurationValid(video.duration) &&
index f365df985f54b0217dc165c17736650f55be400c..8496e679aad708c31d2f046c4ad52685ff213c56 100644 (file)
@@ -10,7 +10,8 @@ import {
   VIDEO_LICENCES,
   VIDEO_MIMETYPE_EXT,
   VIDEO_PRIVACIES,
-  VIDEO_RATE_TYPES
+  VIDEO_RATE_TYPES,
+  VIDEO_STATES
 } from '../../initializers'
 import { VideoModel } from '../../models/video/video'
 import { exists, isArray, isFileValid } from './misc'
@@ -21,11 +22,15 @@ const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS
 const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES
 
 function isVideoCategoryValid (value: any) {
-  return value === null || VIDEO_CATEGORIES[value] !== undefined
+  return value === null || VIDEO_CATEGORIES[ value ] !== undefined
+}
+
+function isVideoStateValid (value: any) {
+  return exists(value) && VIDEO_STATES[ value ] !== undefined
 }
 
 function isVideoLicenceValid (value: any) {
-  return value === null || VIDEO_LICENCES[value] !== undefined
+  return value === null || VIDEO_LICENCES[ value ] !== undefined
 }
 
 function isVideoLanguageValid (value: any) {
@@ -79,20 +84,22 @@ function isVideoRatingTypeValid (value: string) {
 
 const videoFileTypes = Object.keys(VIDEO_MIMETYPE_EXT).map(m => `(${m})`)
 const videoFileTypesRegex = videoFileTypes.join('|')
+
 function isVideoFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) {
   return isFileValid(files, videoFileTypesRegex, 'videofile')
 }
 
 const videoImageTypes = CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME
-  .map(v => v.replace('.', ''))
-  .join('|')
+                                          .map(v => v.replace('.', ''))
+                                          .join('|')
 const videoImageTypesRegex = `image/(${videoImageTypes})`
+
 function isVideoImage (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[], field: string) {
   return isFileValid(files, videoImageTypesRegex, field, true)
 }
 
 function isVideoPrivacyValid (value: string) {
-  return validator.isInt(value + '') && VIDEO_PRIVACIES[value] !== undefined
+  return validator.isInt(value + '') && VIDEO_PRIVACIES[ value ] !== undefined
 }
 
 function isVideoFileInfoHashValid (value: string) {
@@ -118,8 +125,8 @@ async function isVideoExist (id: string, res: Response) {
 
   if (!video) {
     res.status(404)
-      .json({ error: 'Video not found' })
-      .end()
+       .json({ error: 'Video not found' })
+       .end()
 
     return false
   }
@@ -169,6 +176,7 @@ export {
   isVideoTagsValid,
   isVideoAbuseReasonValid,
   isVideoFile,
+  isVideoStateValid,
   isVideoViewsValid,
   isVideoRatingTypeValid,
   isVideoDurationValid,
index e4556fa12d4a875b4665170785e3b940403d9dc1..8fa861281610b4b0aa67900ed29fd1a510d8eced 100644 (file)
@@ -1,6 +1,5 @@
 import { Model } from 'sequelize-typescript'
 import * as ipaddr from 'ipaddr.js'
-const isCidr = require('is-cidr')
 import { ResultList } from '../../shared'
 import { VideoResolution } from '../../shared/models/videos'
 import { CONFIG } from '../initializers'
@@ -10,6 +9,8 @@ import { ApplicationModel } from '../models/application/application'
 import { pseudoRandomBytesPromise } from './core-utils'
 import { logger } from './logger'
 
+const isCidr = require('is-cidr')
+
 async function generateRandomString (size: number) {
   const raw = await pseudoRandomBytesPromise(size)
 
@@ -17,22 +18,20 @@ async function generateRandomString (size: number) {
 }
 
 interface FormattableToJSON {
-  toFormattedJSON ()
+  toFormattedJSON (args?: any)
 }
 
-function getFormattedObjects<U, T extends FormattableToJSON> (objects: T[], objectsTotal: number) {
+function getFormattedObjects<U, T extends FormattableToJSON> (objects: T[], objectsTotal: number, formattedArg?: any) {
   const formattedObjects: U[] = []
 
   objects.forEach(object => {
-    formattedObjects.push(object.toFormattedJSON())
+    formattedObjects.push(object.toFormattedJSON(formattedArg))
   })
 
-  const res: ResultList<U> = {
+  return {
     total: objectsTotal,
     data: formattedObjects
-  }
-
-  return res
+  } as ResultList<U>
 }
 
 async function isSignupAllowed () {
@@ -87,16 +86,17 @@ function computeResolutionsToTranscode (videoFileHeight: number) {
   const resolutionsEnabled: number[] = []
   const configResolutions = CONFIG.TRANSCODING.RESOLUTIONS
 
+  // Put in the order we want to proceed jobs
   const resolutions = [
-    VideoResolution.H_240P,
-    VideoResolution.H_360P,
     VideoResolution.H_480P,
+    VideoResolution.H_360P,
     VideoResolution.H_720P,
+    VideoResolution.H_240P,
     VideoResolution.H_1080P
   ]
 
   for (const resolution of resolutions) {
-    if (configResolutions[resolution + 'p'] === true && videoFileHeight > resolution) {
+    if (configResolutions[ resolution + 'p' ] === true && videoFileHeight > resolution) {
       resolutionsEnabled.push(resolution)
     }
   }
index 79e4bb7f05772a23e8e891144b66c9dd885ee308..8dbc1b060805f96887cd49c2ec767986794793cb 100644 (file)
@@ -1,6 +1,6 @@
 import { IConfig } from 'config'
 import { dirname, join } from 'path'
-import { JobType, VideoRateType } from '../../shared/models'
+import { JobType, VideoRateType, VideoState } from '../../shared/models'
 import { ActivityPubActorType } from '../../shared/models/activitypub'
 import { FollowState } from '../../shared/models/actors'
 import { VideoPrivacy } from '../../shared/models/videos'
@@ -14,7 +14,7 @@ let config: IConfig = require('config')
 
 // ---------------------------------------------------------------------------
 
-const LAST_MIGRATION_VERSION = 215
+const LAST_MIGRATION_VERSION = 220
 
 // ---------------------------------------------------------------------------
 
@@ -326,6 +326,11 @@ const VIDEO_PRIVACIES = {
   [VideoPrivacy.PRIVATE]: 'Private'
 }
 
+const VIDEO_STATES = {
+  [VideoState.PUBLISHED]: 'Published',
+  [VideoState.TO_TRANSCODE]: 'To transcode'
+}
+
 const VIDEO_MIMETYPE_EXT = {
   'video/webm': '.webm',
   'video/ogg': '.ogv',
@@ -493,6 +498,7 @@ export {
   VIDEO_LANGUAGES,
   VIDEO_PRIVACIES,
   VIDEO_LICENCES,
+  VIDEO_STATES,
   VIDEO_RATE_TYPES,
   VIDEO_MIMETYPE_EXT,
   VIDEO_TRANSCODING_FPS,
diff --git a/server/initializers/migrations/0220-video-state.ts b/server/initializers/migrations/0220-video-state.ts
new file mode 100644 (file)
index 0000000..4917021
--- /dev/null
@@ -0,0 +1,62 @@
+import * as Sequelize from 'sequelize'
+
+async function up (utils: {
+  transaction: Sequelize.Transaction
+  queryInterface: Sequelize.QueryInterface
+  sequelize: Sequelize.Sequelize
+}): Promise<void> {
+  // waitingTranscoding column
+  {
+    const data = {
+      type: Sequelize.BOOLEAN,
+      allowNull: true,
+      defaultValue: null
+    }
+    await utils.queryInterface.addColumn('video', 'waitTranscoding', data)
+  }
+
+  {
+    const query = 'UPDATE video SET "waitTranscoding" = false'
+    await utils.sequelize.query(query)
+  }
+
+  {
+    const data = {
+      type: Sequelize.BOOLEAN,
+      allowNull: false,
+      defaultValue: null
+    }
+    await utils.queryInterface.changeColumn('video', 'waitTranscoding', data)
+  }
+
+  // state
+  {
+    const data = {
+      type: Sequelize.INTEGER,
+      allowNull: true,
+      defaultValue: null
+    }
+    await utils.queryInterface.addColumn('video', 'state', data)
+  }
+
+  {
+    // Published
+    const query = 'UPDATE video SET "state" = 1'
+    await utils.sequelize.query(query)
+  }
+
+  {
+    const data = {
+      type: Sequelize.INTEGER,
+      allowNull: false,
+      defaultValue: null
+    }
+    await utils.queryInterface.changeColumn('video', 'state', data)
+  }
+}
+
+function down (options) {
+  throw new Error('Not implemented.')
+}
+
+export { up, down }
index c1265dbcd3314435cd68f53e2c3cce8bc63d447d..7164135b6ff77c2bfdab16113e90b184fb978b75 100644 (file)
@@ -20,7 +20,7 @@ function getVideoCommentAudience (
   isOrigin = false
 ) {
   const to = [ ACTIVITY_PUB.PUBLIC ]
-  const cc = [ ]
+  const cc = []
 
   // Owner of the video we comment
   if (isOrigin === false) {
@@ -55,7 +55,7 @@ async function getActorsInvolvedInVideo (video: VideoModel, t: Transaction) {
   return actors
 }
 
-async function getAudience (actorSender: ActorModel, t: Transaction, isPublic = true) {
+function getAudience (actorSender: ActorModel, isPublic = true) {
   return buildAudience([ actorSender.followersUrl ], isPublic)
 }
 
@@ -67,14 +67,14 @@ function buildAudience (followerUrls: string[], isPublic = true) {
     to = [ ACTIVITY_PUB.PUBLIC ]
     cc = followerUrls
   } else { // Unlisted
-    to = [ ]
-    cc = [ ]
+    to = []
+    cc = []
   }
 
   return { to, cc }
 }
 
-function audiencify <T> (object: T, audience: ActivityAudience) {
+function audiencify<T> (object: T, audience: ActivityAudience) {
   return Object.assign(object, audience)
 }
 
index 7305b3969b4c5955253bf732b0de49b818ca39e0..d4fc786f71b973d69965981954d88342ad12c90e 100644 (file)
@@ -28,7 +28,7 @@ async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Pr
 
     if (Array.isArray(body.orderedItems)) {
       const items = body.orderedItems
-      logger.info('Processing %i ActivityPub items for %s.', items.length, nextLink)
+      logger.info('Processing %i ActivityPub items for %s.', items.length, options.uri)
 
       await handler(items)
     }
index 2750f48c3692eeb75195fd9a7bf2359a9a2a6b7f..77de8c1554352e3f40d92738113b834260d14918 100644 (file)
@@ -1,7 +1,6 @@
 import * as Bluebird from 'bluebird'
 import { ActivityUpdate } from '../../../../shared/models/activitypub'
 import { ActivityPubActor } from '../../../../shared/models/activitypub/activitypub-actor'
-import { VideoTorrentObject } from '../../../../shared/models/activitypub/objects'
 import { retryTransactionWrapper } from '../../../helpers/database-utils'
 import { logger } from '../../../helpers/logger'
 import { resetSequelizeInstance } from '../../../helpers/utils'
@@ -13,6 +12,7 @@ import { VideoChannelModel } from '../../../models/video/video-channel'
 import { VideoFileModel } from '../../../models/video/video-file'
 import { fetchAvatarIfExists, getOrCreateActorAndServerAndModel, updateActorAvatarInstance, updateActorInstance } from '../actor'
 import {
+  fetchRemoteVideo,
   generateThumbnailFromUrl,
   getOrCreateAccountAndVideoAndChannel,
   getOrCreateVideoChannel,
@@ -51,15 +51,18 @@ function processUpdateVideo (actor: ActorModel, activity: ActivityUpdate) {
 }
 
 async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) {
-  const videoAttributesToUpdate = activity.object as VideoTorrentObject
+  const videoUrl = activity.object.id
 
-  const res = await getOrCreateAccountAndVideoAndChannel(videoAttributesToUpdate.id)
+  const videoObject = await fetchRemoteVideo(videoUrl)
+  if (!videoObject) throw new Error('Cannot fetch remote video with url: ' + videoUrl)
+
+  const res = await getOrCreateAccountAndVideoAndChannel(videoObject.id)
 
   // Fetch video channel outside the transaction
-  const newVideoChannelActor = await getOrCreateVideoChannel(videoAttributesToUpdate)
+  const newVideoChannelActor = await getOrCreateVideoChannel(videoObject)
   const newVideoChannel = newVideoChannelActor.VideoChannel
 
-  logger.debug('Updating remote video "%s".', videoAttributesToUpdate.uuid)
+  logger.debug('Updating remote video "%s".', videoObject.uuid)
   let videoInstance = res.video
   let videoFieldsSave: any
 
@@ -77,7 +80,7 @@ async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) {
         throw new Error('Account ' + actor.url + ' does not own video channel ' + videoChannel.Actor.url)
       }
 
-      const videoData = await videoActivityObjectToDBAttributes(newVideoChannel, videoAttributesToUpdate, activity.to)
+      const videoData = await videoActivityObjectToDBAttributes(newVideoChannel, videoObject, activity.to)
       videoInstance.set('name', videoData.name)
       videoInstance.set('uuid', videoData.uuid)
       videoInstance.set('url', videoData.url)
@@ -88,6 +91,8 @@ async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) {
       videoInstance.set('support', videoData.support)
       videoInstance.set('nsfw', videoData.nsfw)
       videoInstance.set('commentsEnabled', videoData.commentsEnabled)
+      videoInstance.set('waitTranscoding', videoData.waitTranscoding)
+      videoInstance.set('state', videoData.state)
       videoInstance.set('duration', videoData.duration)
       videoInstance.set('createdAt', videoData.createdAt)
       videoInstance.set('updatedAt', videoData.updatedAt)
@@ -98,8 +103,8 @@ async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) {
       await videoInstance.save(sequelizeOptions)
 
       // Don't block on request
-      generateThumbnailFromUrl(videoInstance, videoAttributesToUpdate.icon)
-        .catch(err => logger.warn('Cannot generate thumbnail of %s.', videoAttributesToUpdate.id, { err }))
+      generateThumbnailFromUrl(videoInstance, videoObject.icon)
+        .catch(err => logger.warn('Cannot generate thumbnail of %s.', videoObject.id, { err }))
 
       // Remove old video files
       const videoFileDestroyTasks: Bluebird<void>[] = []
@@ -108,16 +113,16 @@ async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) {
       }
       await Promise.all(videoFileDestroyTasks)
 
-      const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoInstance, videoAttributesToUpdate)
+      const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoInstance, videoObject)
       const tasks = videoFileAttributes.map(f => VideoFileModel.create(f))
       await Promise.all(tasks)
 
-      const tags = videoAttributesToUpdate.tag.map(t => t.name)
+      const tags = videoObject.tag.map(t => t.name)
       const tagInstances = await TagModel.findOrCreateTags(tags, t)
       await videoInstance.$set('Tags', tagInstances, sequelizeOptions)
     })
 
-    logger.info('Remote video with uuid %s updated', videoAttributesToUpdate.uuid)
+    logger.info('Remote video with uuid %s updated', videoObject.uuid)
   } catch (err) {
     if (videoInstance !== undefined && videoFieldsSave !== undefined) {
       resetSequelizeInstance(videoInstance, videoFieldsSave)
index fa1d47259a958af993b3bdae32eb3e0228e42cc8..dfc099ff26948201d778ec7dbfb3a82a0f1bbba0 100644 (file)
@@ -11,7 +11,7 @@ async function buildVideoAnnounce (byActor: ActorModel, videoShare: VideoShareMo
 
   const accountsToForwardView = await getActorsInvolvedInVideo(video, t)
   const audience = getObjectFollowersAudience(accountsToForwardView)
-  return announceActivityData(videoShare.url, byActor, announcedObject, t, audience)
+  return announceActivityData(videoShare.url, byActor, announcedObject, audience)
 }
 
 async function sendVideoAnnounce (byActor: ActorModel, videoShare: VideoShareModel, video: VideoModel, t: Transaction) {
@@ -20,16 +20,8 @@ async function sendVideoAnnounce (byActor: ActorModel, videoShare: VideoShareMod
   return broadcastToFollowers(data, byActor, [ byActor ], t)
 }
 
-async function announceActivityData (
-  url: string,
-  byActor: ActorModel,
-  object: string,
-  t: Transaction,
-  audience?: ActivityAudience
-): Promise<ActivityAnnounce> {
-  if (!audience) {
-    audience = await getAudience(byActor, t)
-  }
+function announceActivityData (url: string, byActor: ActorModel, object: string, audience?: ActivityAudience): ActivityAnnounce {
+  if (!audience) audience = getAudience(byActor)
 
   return {
     type: 'Announce',
index 3ef4fcd3b1a02801793731ec9fa1f7c18fc78dbe..293947b052898d446373a1eb37eed10e49f56c05 100644 (file)
@@ -23,8 +23,8 @@ async function sendCreateVideo (video: VideoModel, t: Transaction) {
   const byActor = video.VideoChannel.Account.Actor
   const videoObject = video.toActivityPubObject()
 
-  const audience = await getAudience(byActor, t, video.privacy === VideoPrivacy.PUBLIC)
-  const data = await createActivityData(video.url, byActor, videoObject, t, audience)
+  const audience = getAudience(byActor, video.privacy === VideoPrivacy.PUBLIC)
+  const data = createActivityData(video.url, byActor, videoObject, audience)
 
   return broadcastToFollowers(data, byActor, [ byActor ], t)
 }
@@ -33,7 +33,7 @@ async function sendVideoAbuse (byActor: ActorModel, videoAbuse: VideoAbuseModel,
   const url = getVideoAbuseActivityPubUrl(videoAbuse)
 
   const audience = { to: [ video.VideoChannel.Account.Actor.url ], cc: [] }
-  const data = await createActivityData(url, byActor, videoAbuse.toActivityPubObject(), t, audience)
+  const data = createActivityData(url, byActor, videoAbuse.toActivityPubObject(), audience)
 
   return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl)
 }
@@ -57,7 +57,7 @@ async function sendCreateVideoComment (comment: VideoCommentModel, t: Transactio
     audience = getObjectFollowersAudience(actorsInvolvedInComment.concat(parentsCommentActors))
   }
 
-  const data = await createActivityData(comment.url, byActor, commentObject, t, audience)
+  const data = createActivityData(comment.url, byActor, commentObject, audience)
 
   // This was a reply, send it to the parent actors
   const actorsException = [ byActor ]
@@ -82,14 +82,14 @@ async function sendCreateView (byActor: ActorModel, video: VideoModel, t: Transa
   // Send to origin
   if (video.isOwned() === false) {
     const audience = getVideoAudience(video, actorsInvolvedInVideo)
-    const data = await createActivityData(url, byActor, viewActivityData, t, audience)
+    const data = createActivityData(url, byActor, viewActivityData, audience)
 
     return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl)
   }
 
   // Send to followers
   const audience = getObjectFollowersAudience(actorsInvolvedInVideo)
-  const data = await createActivityData(url, byActor, viewActivityData, t, audience)
+  const data = createActivityData(url, byActor, viewActivityData, audience)
 
   // Use the server actor to send the view
   const serverActor = await getServerActor()
@@ -106,34 +106,31 @@ async function sendCreateDislike (byActor: ActorModel, video: VideoModel, t: Tra
   // Send to origin
   if (video.isOwned() === false) {
     const audience = getVideoAudience(video, actorsInvolvedInVideo)
-    const data = await createActivityData(url, byActor, dislikeActivityData, t, audience)
+    const data = createActivityData(url, byActor, dislikeActivityData, audience)
 
     return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl)
   }
 
   // Send to followers
   const audience = getObjectFollowersAudience(actorsInvolvedInVideo)
-  const data = await createActivityData(url, byActor, dislikeActivityData, t, audience)
+  const data = createActivityData(url, byActor, dislikeActivityData, audience)
 
   const actorsException = [ byActor ]
   return broadcastToFollowers(data, byActor, actorsInvolvedInVideo, t, actorsException)
 }
 
-async function createActivityData (url: string,
-                                   byActor: ActorModel,
-                                   object: any,
-                                   t: Transaction,
-                                   audience?: ActivityAudience): Promise<ActivityCreate> {
-  if (!audience) {
-    audience = await getAudience(byActor, t)
-  }
-
-  return audiencify({
-    type: 'Create' as 'Create',
-    id: url + '/activity',
-    actor: byActor.url,
-    object: audiencify(object, audience)
-  }, audience)
+function createActivityData (url: string, byActor: ActorModel, object: any, audience?: ActivityAudience): ActivityCreate {
+  if (!audience) audience = getAudience(byActor)
+
+  return audiencify(
+    {
+      type: 'Create' as 'Create',
+      id: url + '/activity',
+      actor: byActor.url,
+      object: audiencify(object, audience)
+    },
+    audience
+  )
 }
 
 function createDislikeActivityData (byActor: ActorModel, video: VideoModel) {
index ddeb1fcd2e375bdc05fea4ebdaed0c6a3a3551cb..37ee7c09669951506d0f1c5c883fe3c731db4ca6 100644 (file)
@@ -14,36 +14,31 @@ async function sendLike (byActor: ActorModel, video: VideoModel, t: Transaction)
   // Send to origin
   if (video.isOwned() === false) {
     const audience = getVideoAudience(video, accountsInvolvedInVideo)
-    const data = await likeActivityData(url, byActor, video, t, audience)
+    const data = likeActivityData(url, byActor, video, audience)
 
     return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl)
   }
 
   // Send to followers
   const audience = getObjectFollowersAudience(accountsInvolvedInVideo)
-  const data = await likeActivityData(url, byActor, video, t, audience)
+  const data = likeActivityData(url, byActor, video, audience)
 
   const followersException = [ byActor ]
   return broadcastToFollowers(data, byActor, accountsInvolvedInVideo, t, followersException)
 }
 
-async function likeActivityData (
-  url: string,
-  byActor: ActorModel,
-  video: VideoModel,
-  t: Transaction,
-  audience?: ActivityAudience
-): Promise<ActivityLike> {
-  if (!audience) {
-    audience = await getAudience(byActor, t)
-  }
-
-  return audiencify({
-    type: 'Like' as 'Like',
-    id: url,
-    actor: byActor.url,
-    object: video.url
-  }, audience)
+function likeActivityData (url: string, byActor: ActorModel, video: VideoModel, audience?: ActivityAudience): ActivityLike {
+  if (!audience) audience = getAudience(byActor)
+
+  return audiencify(
+    {
+      type: 'Like' as 'Like',
+      id: url,
+      actor: byActor.url,
+      object: video.url
+    },
+    audience
+  )
 }
 
 // ---------------------------------------------------------------------------
index 9733e66dcbf449082508c4b9d586e2cfb4efacaf..33c3d24291cbf7550f2e108d00c1695650584dd6 100644 (file)
@@ -27,7 +27,7 @@ async function sendUndoFollow (actorFollow: ActorFollowModel, t: Transaction) {
   const undoUrl = getUndoActivityPubUrl(followUrl)
 
   const object = followActivityData(followUrl, me, following)
-  const data = await undoActivityData(undoUrl, me, object, t)
+  const data = undoActivityData(undoUrl, me, object)
 
   return unicastTo(data, me, following.inboxUrl)
 }
@@ -37,18 +37,18 @@ async function sendUndoLike (byActor: ActorModel, video: VideoModel, t: Transact
   const undoUrl = getUndoActivityPubUrl(likeUrl)
 
   const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, t)
-  const object = await likeActivityData(likeUrl, byActor, video, t)
+  const object = likeActivityData(likeUrl, byActor, video)
 
   // Send to origin
   if (video.isOwned() === false) {
     const audience = getVideoAudience(video, actorsInvolvedInVideo)
-    const data = await undoActivityData(undoUrl, byActor, object, t, audience)
+    const data = undoActivityData(undoUrl, byActor, object, audience)
 
     return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl)
   }
 
   const audience = getObjectFollowersAudience(actorsInvolvedInVideo)
-  const data = await undoActivityData(undoUrl, byActor, object, t, audience)
+  const data = undoActivityData(undoUrl, byActor, object, audience)
 
   const followersException = [ byActor ]
   return broadcastToFollowers(data, byActor, actorsInvolvedInVideo, t, followersException)
@@ -60,16 +60,16 @@ async function sendUndoDislike (byActor: ActorModel, video: VideoModel, t: Trans
 
   const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, t)
   const dislikeActivity = createDislikeActivityData(byActor, video)
-  const object = await createActivityData(dislikeUrl, byActor, dislikeActivity, t)
+  const object = createActivityData(dislikeUrl, byActor, dislikeActivity)
 
   if (video.isOwned() === false) {
     const audience = getVideoAudience(video, actorsInvolvedInVideo)
-    const data = await undoActivityData(undoUrl, byActor, object, t, audience)
+    const data = undoActivityData(undoUrl, byActor, object, audience)
 
     return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl)
   }
 
-  const data = await undoActivityData(undoUrl, byActor, object, t)
+  const data = undoActivityData(undoUrl, byActor, object)
 
   const followersException = [ byActor ]
   return broadcastToFollowers(data, byActor, actorsInvolvedInVideo, t, followersException)
@@ -80,7 +80,7 @@ async function sendUndoAnnounce (byActor: ActorModel, videoShare: VideoShareMode
 
   const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, t)
   const object = await buildVideoAnnounce(byActor, videoShare, video, t)
-  const data = await undoActivityData(undoUrl, byActor, object, t)
+  const data = undoActivityData(undoUrl, byActor, object)
 
   const followersException = [ byActor ]
   return broadcastToFollowers(data, byActor, actorsInvolvedInVideo, t, followersException)
@@ -97,21 +97,21 @@ export {
 
 // ---------------------------------------------------------------------------
 
-async function undoActivityData (
+function undoActivityData (
   url: string,
   byActor: ActorModel,
   object: ActivityFollow | ActivityLike | ActivityCreate | ActivityAnnounce,
-  t: Transaction,
   audience?: ActivityAudience
-): Promise<ActivityUndo> {
-  if (!audience) {
-    audience = await getAudience(byActor, t)
-  }
-
-  return audiencify({
-    type: 'Undo' as 'Undo',
-    id: url,
-    actor: byActor.url,
-    object
-  }, audience)
+): ActivityUndo {
+  if (!audience) audience = getAudience(byActor)
+
+  return audiencify(
+    {
+      type: 'Undo' as 'Undo',
+      id: url,
+      actor: byActor.url,
+      object
+    },
+    audience
+  )
 }
index d64b88343b73c4bafc9cb03cd0e754dcc10adc90..2fd374ec65312fd068f9fded3abdab243a34f07f 100644 (file)
@@ -15,9 +15,9 @@ async function sendUpdateVideo (video: VideoModel, t: Transaction) {
 
   const url = getUpdateActivityPubUrl(video.url, video.updatedAt.toISOString())
   const videoObject = video.toActivityPubObject()
-  const audience = await getAudience(byActor, t, video.privacy === VideoPrivacy.PUBLIC)
+  const audience = getAudience(byActor, video.privacy === VideoPrivacy.PUBLIC)
 
-  const data = await updateActivityData(url, byActor, videoObject, t, audience)
+  const data = updateActivityData(url, byActor, videoObject, audience)
 
   const actorsInvolved = await VideoShareModel.loadActorsByShare(video.id, t)
   actorsInvolved.push(byActor)
@@ -30,8 +30,8 @@ async function sendUpdateActor (accountOrChannel: AccountModel | VideoChannelMod
 
   const url = getUpdateActivityPubUrl(byActor.url, byActor.updatedAt.toISOString())
   const accountOrChannelObject = accountOrChannel.toActivityPubObject()
-  const audience = await getAudience(byActor, t)
-  const data = await updateActivityData(url, byActor, accountOrChannelObject, t, audience)
+  const audience = getAudience(byActor)
+  const data = updateActivityData(url, byActor, accountOrChannelObject, audience)
 
   let actorsInvolved: ActorModel[]
   if (accountOrChannel instanceof AccountModel) {
@@ -56,21 +56,17 @@ export {
 
 // ---------------------------------------------------------------------------
 
-async function updateActivityData (
-  url: string,
-  byActor: ActorModel,
-  object: any,
-  t: Transaction,
-  audience?: ActivityAudience
-): Promise<ActivityUpdate> {
-  if (!audience) {
-    audience = await getAudience(byActor, t)
-  }
+function updateActivityData (url: string, byActor: ActorModel, object: any, audience?: ActivityAudience): ActivityUpdate {
+  if (!audience) audience = getAudience(byActor)
 
-  return audiencify({
-    type: 'Update' as 'Update',
-    id: url,
-    actor: byActor.url,
-    object: audiencify(object, audience)
-  }, audience)
+  return audiencify(
+    {
+      type: 'Update' as 'Update',
+      id: url,
+      actor: byActor.url,
+      object: audiencify(object, audience
+      )
+    },
+    audience
+  )
 }
index 907f7e11e273e55a559af20f7f9762de0498846f..7ec8ca193ad38f0b79e9ea89b8b7f5713eb7022f 100644 (file)
@@ -1,8 +1,9 @@
 import * as Bluebird from 'bluebird'
+import * as sequelize from 'sequelize'
 import * as magnetUtil from 'magnet-uri'
 import { join } from 'path'
 import * as request from 'request'
-import { ActivityIconObject } from '../../../shared/index'
+import { ActivityIconObject, VideoState } from '../../../shared/index'
 import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
 import { VideoPrivacy, VideoRateType } from '../../../shared/models/videos'
 import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos'
@@ -21,6 +22,21 @@ import { VideoShareModel } from '../../models/video/video-share'
 import { getOrCreateActorAndServerAndModel } from './actor'
 import { addVideoComments } from './video-comments'
 import { crawlCollectionPage } from './crawl'
+import { sendCreateVideo, sendUpdateVideo } from './send'
+import { shareVideoByServerAndChannel } from './index'
+
+async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) {
+  // If the video is not private and published, we federate it
+  if (video.privacy !== VideoPrivacy.PRIVATE && video.state === VideoState.PUBLISHED) {
+    if (isNewVideo === true) {
+      // Now we'll add the video's meta data to our followers
+      await sendCreateVideo(video, transaction)
+      await shareVideoByServerAndChannel(video, transaction)
+    } else {
+      await sendUpdateVideo(video, transaction)
+    }
+  }
+}
 
 function fetchRemoteVideoPreview (video: VideoModel, reject: Function) {
   const host = video.VideoChannel.Account.Actor.Server.host
@@ -55,9 +71,11 @@ function generateThumbnailFromUrl (video: VideoModel, icon: ActivityIconObject)
   return doRequestAndSaveToFile(options, thumbnailPath)
 }
 
-async function videoActivityObjectToDBAttributes (videoChannel: VideoChannelModel,
-                                                  videoObject: VideoTorrentObject,
-                                                  to: string[] = []) {
+async function videoActivityObjectToDBAttributes (
+  videoChannel: VideoChannelModel,
+  videoObject: VideoTorrentObject,
+  to: string[] = []
+) {
   const privacy = to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1 ? VideoPrivacy.PUBLIC : VideoPrivacy.UNLISTED
   const duration = videoObject.duration.replace(/[^\d]+/, '')
 
@@ -90,6 +108,8 @@ async function videoActivityObjectToDBAttributes (videoChannel: VideoChannelMode
     support,
     nsfw: videoObject.sensitive,
     commentsEnabled: videoObject.commentsEnabled,
+    waitTranscoding: videoObject.waitTranscoding,
+    state: videoObject.state,
     channelId: videoChannel.id,
     duration: parseInt(duration, 10),
     createdAt: new Date(videoObject.published),
@@ -185,22 +205,20 @@ async function getOrCreateVideo (videoObject: VideoTorrentObject, channelActor:
 }
 
 async function getOrCreateAccountAndVideoAndChannel (videoObject: VideoTorrentObject | string, actor?: ActorModel) {
-  if (typeof videoObject === 'string') {
-    const videoUrl = videoObject
-
-    const videoFromDatabase = await VideoModel.loadByUrlAndPopulateAccount(videoUrl)
-    if (videoFromDatabase) {
-      return {
-        video: videoFromDatabase,
-        actor: videoFromDatabase.VideoChannel.Account.Actor,
-        channelActor: videoFromDatabase.VideoChannel.Actor
-      }
+  const videoUrl = typeof videoObject === 'string' ? videoObject : videoObject.id
+
+  const videoFromDatabase = await VideoModel.loadByUrlAndPopulateAccount(videoUrl)
+  if (videoFromDatabase) {
+    return {
+      video: videoFromDatabase,
+      actor: videoFromDatabase.VideoChannel.Account.Actor,
+      channelActor: videoFromDatabase.VideoChannel.Actor
     }
-
-    videoObject = await fetchRemoteVideo(videoUrl)
-    if (!videoObject) throw new Error('Cannot fetch remote video with url: ' + videoUrl)
   }
 
+  videoObject = await fetchRemoteVideo(videoUrl)
+  if (!videoObject) throw new Error('Cannot fetch remote video with url: ' + videoUrl)
+
   if (!actor) {
     const actorObj = videoObject.attributedTo.find(a => a.type === 'Person')
     if (!actorObj) throw new Error('Cannot find associated actor to video ' + videoObject.url)
@@ -291,20 +309,6 @@ async function addVideoShares (shareUrls: string[], instance: VideoModel) {
   }
 }
 
-export {
-  getOrCreateAccountAndVideoAndChannel,
-  fetchRemoteVideoPreview,
-  fetchRemoteVideoDescription,
-  generateThumbnailFromUrl,
-  videoActivityObjectToDBAttributes,
-  videoFileActivityUrlToDBAttributes,
-  getOrCreateVideo,
-  getOrCreateVideoChannel,
-  addVideoShares
-}
-
-// ---------------------------------------------------------------------------
-
 async function fetchRemoteVideo (videoUrl: string): Promise<VideoTorrentObject> {
   const options = {
     uri: videoUrl,
@@ -324,3 +328,17 @@ async function fetchRemoteVideo (videoUrl: string): Promise<VideoTorrentObject>
 
   return body
 }
+
+export {
+  federateVideoIfNeeded,
+  fetchRemoteVideo,
+  getOrCreateAccountAndVideoAndChannel,
+  fetchRemoteVideoPreview,
+  fetchRemoteVideoDescription,
+  generateThumbnailFromUrl,
+  videoActivityObjectToDBAttributes,
+  videoFileActivityUrlToDBAttributes,
+  getOrCreateVideo,
+  getOrCreateVideoChannel,
+  addVideoShares
+}
index 85f7dbfc2b83480f149e70d6f7144ec9219d4df6..f5ad076a6d9e0d720dd83cfe61fea2bb7b4599c3 100644 (file)
@@ -1,17 +1,16 @@
 import * as kue from 'kue'
-import { VideoResolution } from '../../../../shared'
-import { VideoPrivacy } from '../../../../shared/models/videos'
+import { VideoResolution, VideoState } from '../../../../shared'
 import { logger } from '../../../helpers/logger'
 import { computeResolutionsToTranscode } from '../../../helpers/utils'
-import { sequelizeTypescript } from '../../../initializers'
 import { VideoModel } from '../../../models/video/video'
-import { shareVideoByServerAndChannel } from '../../activitypub'
-import { sendCreateVideo, sendUpdateVideo } from '../../activitypub/send'
 import { JobQueue } from '../job-queue'
+import { federateVideoIfNeeded } from '../../activitypub'
+import { retryTransactionWrapper } from '../../../helpers/database-utils'
+import { sequelizeTypescript } from '../../../initializers'
 
 export type VideoFilePayload = {
   videoUUID: string
-  isNewVideo: boolean
+  isNewVideo?: boolean
   resolution?: VideoResolution
   isPortraitMode?: boolean
 }
@@ -52,10 +51,20 @@ async function processVideoFile (job: kue.Job) {
   // Transcoding in other resolution
   if (payload.resolution) {
     await video.transcodeOriginalVideofile(payload.resolution, payload.isPortraitMode)
-    await onVideoFileTranscoderOrImportSuccess(video)
+
+    const options = {
+      arguments: [ video ],
+      errorMessage: 'Cannot execute onVideoFileTranscoderOrImportSuccess with many retries.'
+    }
+    await retryTransactionWrapper(onVideoFileTranscoderOrImportSuccess, options)
   } else {
     await video.optimizeOriginalVideofile()
-    await onVideoFileOptimizerSuccess(video, payload.isNewVideo)
+
+    const options = {
+      arguments: [ video, payload.isNewVideo ],
+      errorMessage: 'Cannot execute onVideoFileOptimizerSuccess with many retries.'
+    }
+    await retryTransactionWrapper(onVideoFileOptimizerSuccess, options)
   }
 
   return video
@@ -64,68 +73,70 @@ async function processVideoFile (job: kue.Job) {
 async function onVideoFileTranscoderOrImportSuccess (video: VideoModel) {
   if (video === undefined) return undefined
 
-  // Maybe the video changed in database, refresh it
-  const videoDatabase = await VideoModel.loadByUUIDAndPopulateAccountAndServerAndTags(video.uuid)
-  // Video does not exist anymore
-  if (!videoDatabase) return undefined
+  return sequelizeTypescript.transaction(async t => {
+    // Maybe the video changed in database, refresh it
+    let videoDatabase = await VideoModel.loadByUUIDAndPopulateAccountAndServerAndTags(video.uuid, t)
+    // Video does not exist anymore
+    if (!videoDatabase) return undefined
 
-  if (video.privacy !== VideoPrivacy.PRIVATE) {
-    await sendUpdateVideo(video, undefined)
-  }
+    // We transcoded the video file in another format, now we can publish it
+    const oldState = videoDatabase.state
+    videoDatabase.state = VideoState.PUBLISHED
+    videoDatabase = await videoDatabase.save({ transaction: t })
+
+    // If the video was not published, we consider it is a new one for other instances
+    const isNewVideo = oldState !== VideoState.PUBLISHED
+    await federateVideoIfNeeded(videoDatabase, isNewVideo, t)
 
-  return undefined
+    return undefined
+  })
 }
 
 async function onVideoFileOptimizerSuccess (video: VideoModel, isNewVideo: boolean) {
   if (video === undefined) return undefined
 
-  // Maybe the video changed in database, refresh it
-  const videoDatabase = await VideoModel.loadByUUIDAndPopulateAccountAndServerAndTags(video.uuid)
-  // Video does not exist anymore
-  if (!videoDatabase) return undefined
-
-  if (video.privacy !== VideoPrivacy.PRIVATE) {
-    if (isNewVideo !== false) {
-      // Now we'll add the video's meta data to our followers
-      await sequelizeTypescript.transaction(async t => {
-        await sendCreateVideo(video, t)
-        await shareVideoByServerAndChannel(video, t)
-      })
-    } else {
-      await sendUpdateVideo(video, undefined)
-    }
-  }
-
-  const { videoFileResolution } = await videoDatabase.getOriginalFileResolution()
-
-  // Create transcoding jobs if there are enabled resolutions
-  const resolutionsEnabled = computeResolutionsToTranscode(videoFileResolution)
-  logger.info(
-    'Resolutions computed for video %s and origin file height of %d.', videoDatabase.uuid, videoFileResolution,
-    { resolutions: resolutionsEnabled }
-  )
+  // Outside the transaction (IO on disk)
+  const { videoFileResolution } = await video.getOriginalFileResolution()
+
+  return sequelizeTypescript.transaction(async t => {
+    // Maybe the video changed in database, refresh it
+    const videoDatabase = await VideoModel.loadByUUIDAndPopulateAccountAndServerAndTags(video.uuid, t)
+    // Video does not exist anymore
+    if (!videoDatabase) return undefined
+
+    // Create transcoding jobs if there are enabled resolutions
+    const resolutionsEnabled = computeResolutionsToTranscode(videoFileResolution)
+    logger.info(
+      'Resolutions computed for video %s and origin file height of %d.', videoDatabase.uuid, videoFileResolution,
+      { resolutions: resolutionsEnabled }
+    )
+
+    if (resolutionsEnabled.length !== 0) {
+      const tasks: Promise<any>[] = []
+
+      for (const resolution of resolutionsEnabled) {
+        const dataInput = {
+          videoUUID: videoDatabase.uuid,
+          resolution
+        }
+
+        const p = JobQueue.Instance.createJob({ type: 'video-file', payload: dataInput })
+        tasks.push(p)
+      }
 
-  if (resolutionsEnabled.length !== 0) {
-    const tasks: Promise<any>[] = []
+      await Promise.all(tasks)
 
-    for (const resolution of resolutionsEnabled) {
-      const dataInput = {
-        videoUUID: videoDatabase.uuid,
-        resolution,
-        isNewVideo
-      }
+      logger.info('Transcoding jobs created for uuid %s.', videoDatabase.uuid, { resolutionsEnabled })
+    } else {
+      // No transcoding to do, it's now published
+      video.state = VideoState.PUBLISHED
+      video = await video.save({ transaction: t })
 
-      const p = JobQueue.Instance.createJob({ type: 'video-file', payload: dataInput })
-      tasks.push(p)
+      logger.info('No transcoding jobs created for video %s (no resolutions).', video.uuid)
     }
 
-    await Promise.all(tasks)
-
-    logger.info('Transcoding jobs created for uuid %s.', videoDatabase.uuid, { resolutionsEnabled })
-  } else {
-    logger.info('No transcoding jobs created for video %s (no resolutions enabled).')
-    return undefined
-  }
+    return federateVideoIfNeeded(video, isNewVideo, t)
+  })
 }
 
 // ---------------------------------------------------------------------------
index bdfa19b6156b3b2dcd80c39b7f5938547f82830a..695fe0eea2bec605b5a1f573e10414397174c6c1 100644 (file)
@@ -79,6 +79,7 @@ class JobQueue {
           const res = await handlers[ handlerName ](job)
           return done(null, res)
         } catch (err) {
+          logger.error('Cannot execute job %d.', job.id, { err })
           return done(err)
         }
       })
index bf66596879f945f218a9f9e1a6a7b774edd73682..1de44db703429a171386489a5914d474441141b4 100644 (file)
@@ -14,7 +14,7 @@ function cacheRoute (lifetime: number) {
 
       // Not cached
       if (!cached) {
-        logger.debug('Not cached result for route %s.', req.originalUrl)
+        logger.debug('No cached results for route %s.', req.originalUrl)
 
         const sendSave = res.send.bind(res)
 
index c5c45fe586b0d4c4e5d49d738899b367ca496324..e181aebdbcdaa43e4431356952c6768d1b3cd561 100644 (file)
@@ -55,8 +55,13 @@ const videosAddValidator = [
     .customSanitizer(toValueOrNull)
     .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
   body('nsfw')
+    .optional()
     .toBoolean()
     .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
+  body('waitTranscoding')
+    .optional()
+    .toBoolean()
+    .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
   body('description')
     .optional()
     .customSanitizer(toValueOrNull)
@@ -70,6 +75,7 @@ const videosAddValidator = [
     .customSanitizer(toValueOrNull)
     .custom(isVideoTagsValid).withMessage('Should have correct tags'),
   body('commentsEnabled')
+    .optional()
     .toBoolean()
     .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
   body('privacy')
@@ -149,6 +155,10 @@ const videosUpdateValidator = [
     .optional()
     .toBoolean()
     .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
+  body('waitTranscoding')
+    .optional()
+    .toBoolean()
+    .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
   body('privacy')
     .optional()
     .toInt()
index 1cb1e6798407b9098fb1a17a8f778b9867a0e563..59c378efaa2f2ace4922596f6d08ab49c734f07c 100644 (file)
@@ -25,7 +25,7 @@ import {
   Table,
   UpdatedAt
 } from 'sequelize-typescript'
-import { VideoPrivacy, VideoResolution } from '../../../shared'
+import { VideoPrivacy, VideoResolution, VideoState } from '../../../shared'
 import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
 import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos'
 import { VideoFilter } from '../../../shared/models/videos/video-query.type'
@@ -47,7 +47,7 @@ import {
   isVideoLanguageValid,
   isVideoLicenceValid,
   isVideoNameValid,
-  isVideoPrivacyValid,
+  isVideoPrivacyValid, isVideoStateValid,
   isVideoSupportValid
 } from '../../helpers/custom-validators/videos'
 import { generateImageFromVideoFile, getVideoFileResolution, transcode } from '../../helpers/ffmpeg-utils'
@@ -66,7 +66,7 @@ import {
   VIDEO_EXT_MIMETYPE,
   VIDEO_LANGUAGES,
   VIDEO_LICENCES,
-  VIDEO_PRIVACIES
+  VIDEO_PRIVACIES, VIDEO_STATES
 } from '../../initializers'
 import {
   getVideoCommentsActivityPubUrl,
@@ -93,10 +93,7 @@ enum ScopeNames {
   AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST',
   WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS',
   WITH_TAGS = 'WITH_TAGS',
-  WITH_FILES = 'WITH_FILES',
-  WITH_SHARES = 'WITH_SHARES',
-  WITH_RATES = 'WITH_RATES',
-  WITH_COMMENTS = 'WITH_COMMENTS'
+  WITH_FILES = 'WITH_FILES'
 }
 
 @Scopes({
@@ -183,7 +180,20 @@ enum ScopeNames {
             ')'
           )
         },
-        privacy: VideoPrivacy.PUBLIC
+        // Always list public videos
+        privacy: VideoPrivacy.PUBLIC,
+        // Always list published videos, or videos that are being transcoded but on which we don't want to wait for transcoding
+        [ Sequelize.Op.or ]: [
+          {
+            state: VideoState.PUBLISHED
+          },
+          {
+            [ Sequelize.Op.and ]: {
+              state: VideoState.TO_TRANSCODE,
+              waitTranscoding: false
+            }
+          }
+        ]
       },
       include: [ videoChannelInclude ]
     }
@@ -272,42 +282,6 @@ enum ScopeNames {
         required: true
       }
     ]
-  },
-  [ScopeNames.WITH_SHARES]: {
-    include: [
-      {
-        ['separate' as any]: true,
-        model: () => VideoShareModel.unscoped()
-      }
-    ]
-  },
-  [ScopeNames.WITH_RATES]: {
-    include: [
-      {
-        ['separate' as any]: true,
-        model: () => AccountVideoRateModel,
-        include: [
-          {
-            model: () => AccountModel.unscoped(),
-            required: true,
-            include: [
-              {
-                attributes: [ 'url' ],
-                model: () => ActorModel.unscoped()
-              }
-            ]
-          }
-        ]
-      }
-    ]
-  },
-  [ScopeNames.WITH_COMMENTS]: {
-    include: [
-      {
-        ['separate' as any]: true,
-        model: () => VideoCommentModel.unscoped()
-      }
-    ]
   }
 })
 @Table({
@@ -335,7 +309,7 @@ enum ScopeNames {
       fields: [ 'channelId' ]
     },
     {
-      fields: [ 'id', 'privacy' ]
+      fields: [ 'id', 'privacy', 'state', 'waitTranscoding' ]
     },
     {
       fields: [ 'url'],
@@ -435,6 +409,16 @@ export class VideoModel extends Model<VideoModel> {
   @Column
   commentsEnabled: boolean
 
+  @AllowNull(false)
+  @Column
+  waitTranscoding: boolean
+
+  @AllowNull(false)
+  @Default(null)
+  @Is('VideoState', value => throwIfNotValid(value, isVideoStateValid, 'state'))
+  @Column
+  state: VideoState
+
   @CreatedAt
   createdAt: Date
 
@@ -671,7 +655,7 @@ export class VideoModel extends Model<VideoModel> {
     })
   }
 
-  static listAccountVideosForApi (accountId: number, start: number, count: number, sort: string, hideNSFW: boolean, withFiles = false) {
+  static listUserVideosForApi (accountId: number, start: number, count: number, sort: string, hideNSFW: boolean, withFiles = false) {
     const query: IFindOptions<VideoModel> = {
       offset: start,
       limit: count,
@@ -858,12 +842,13 @@ export class VideoModel extends Model<VideoModel> {
       .findOne(options)
   }
 
-  static loadByUUIDAndPopulateAccountAndServerAndTags (uuid: string) {
+  static loadByUUIDAndPopulateAccountAndServerAndTags (uuid: string, t?: Sequelize.Transaction) {
     const options = {
       order: [ [ 'Tags', 'name', 'ASC' ] ],
       where: {
         uuid
-      }
+      },
+      transaction: t
     }
 
     return VideoModel
@@ -905,31 +890,23 @@ export class VideoModel extends Model<VideoModel> {
   }
 
   private static getCategoryLabel (id: number) {
-    let categoryLabel = VIDEO_CATEGORIES[id]
-    if (!categoryLabel) categoryLabel = 'Misc'
-
-    return categoryLabel
+    return VIDEO_CATEGORIES[id] || 'Misc'
   }
 
   private static getLicenceLabel (id: number) {
-    let licenceLabel = VIDEO_LICENCES[id]
-    if (!licenceLabel) licenceLabel = 'Unknown'
-
-    return licenceLabel
+    return VIDEO_LICENCES[id] || 'Unknown'
   }
 
   private static getLanguageLabel (id: string) {
-    let languageLabel = VIDEO_LANGUAGES[id]
-    if (!languageLabel) languageLabel = 'Unknown'
-
-    return languageLabel
+    return VIDEO_LANGUAGES[id] || 'Unknown'
   }
 
   private static getPrivacyLabel (id: number) {
-    let privacyLabel = VIDEO_PRIVACIES[id]
-    if (!privacyLabel) privacyLabel = 'Unknown'
+    return VIDEO_PRIVACIES[id] || 'Unknown'
+  }
 
-    return privacyLabel
+  private static getStateLabel (id: number) {
+    return VIDEO_STATES[id] || 'Unknown'
   }
 
   getOriginalFile () {
@@ -1026,11 +1003,16 @@ export class VideoModel extends Model<VideoModel> {
     return join(STATIC_PATHS.PREVIEWS, this.getPreviewName())
   }
 
-  toFormattedJSON (): Video {
+  toFormattedJSON (options?: {
+    additionalAttributes: {
+      state: boolean,
+      waitTranscoding: boolean
+    }
+  }): Video {
     const formattedAccount = this.VideoChannel.Account.toFormattedJSON()
     const formattedVideoChannel = this.VideoChannel.toFormattedJSON()
 
-    return {
+    const videoObject: Video = {
       id: this.id,
       uuid: this.uuid,
       name: this.name,
@@ -1082,6 +1064,19 @@ export class VideoModel extends Model<VideoModel> {
         avatar: formattedVideoChannel.avatar
       }
     }
+
+    if (options) {
+      if (options.additionalAttributes.state) {
+        videoObject.state = {
+          id: this.state,
+          label: VideoModel.getStateLabel(this.state)
+        }
+      }
+
+      if (options.additionalAttributes.waitTranscoding) videoObject.waitTranscoding = this.waitTranscoding
+    }
+
+    return videoObject
   }
 
   toFormattedDetailsJSON (): VideoDetails {
@@ -1094,6 +1089,11 @@ export class VideoModel extends Model<VideoModel> {
       account: this.VideoChannel.Account.toFormattedJSON(),
       tags: map(this.Tags, 'name'),
       commentsEnabled: this.commentsEnabled,
+      waitTranscoding: this.waitTranscoding,
+      state: {
+        id: this.state,
+        label: VideoModel.getStateLabel(this.state)
+      },
       files: []
     }
 
@@ -1207,6 +1207,8 @@ export class VideoModel extends Model<VideoModel> {
       language,
       views: this.views,
       sensitive: this.nsfw,
+      waitTranscoding: this.waitTranscoding,
+      state: this.state,
       commentsEnabled: this.commentsEnabled,
       published: this.publishedAt.toISOString(),
       updated: this.updatedAt.toISOString(),
index bc6c7fc46deadd70a72a2527df07713b973d80d7..04bed3b441be808f74f022b0183a48c622246852 100644 (file)
@@ -175,6 +175,7 @@ describe('Test videos API validator', function () {
         language: 'pt',
         nsfw: false,
         commentsEnabled: true,
+        waitTranscoding: true,
         description: 'my super description',
         support: 'my super support text',
         tags: [ 'tag1', 'tag2' ],
@@ -224,20 +225,6 @@ describe('Test videos API validator', function () {
       await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
     })
 
-    it('Should fail without nsfw attribute', async function () {
-      const fields = omit(baseCorrectParams, 'nsfw')
-      const attaches = baseCorrectAttaches
-
-      await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
-    })
-
-    it('Should fail without commentsEnabled attribute', async function () {
-      const fields = omit(baseCorrectParams, 'commentsEnabled')
-      const attaches = baseCorrectAttaches
-
-      await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
-    })
-
     it('Should fail with a long description', async function () {
       const fields = immutableAssign(baseCorrectParams, { description: 'super'.repeat(2500) })
       const attaches = baseCorrectAttaches
index 5f9a766216da99c1c6abde69fcac9f8c78e82d8d..edc46a64452f2257b69d088d3eedce9ad1bed40e 100644 (file)
@@ -924,7 +924,7 @@ describe('Test multiple servers', function () {
 
   describe('With minimum parameters', function () {
     it('Should upload and propagate the video', async function () {
-      this.timeout(50000)
+      this.timeout(60000)
 
       const path = '/api/v1/videos/upload'
 
@@ -934,16 +934,14 @@ describe('Test multiple servers', function () {
         .set('Authorization', 'Bearer ' + servers[1].accessToken)
         .field('name', 'minimum parameters')
         .field('privacy', '1')
-        .field('nsfw', 'false')
         .field('channelId', '1')
-        .field('commentsEnabled', 'true')
 
       const filePath = join(__dirname, '..', '..', 'fixtures', 'video_short.webm')
 
       await req.attach('videofile', filePath)
         .expect(200)
 
-      await wait(25000)
+      await wait(40000)
 
       for (const server of servers) {
         const res = await getVideosList(server.url)
@@ -964,7 +962,7 @@ describe('Test multiple servers', function () {
           },
           isLocal,
           duration: 5,
-          commentsEnabled: true,
+          commentsEnabled: false,
           tags: [ ],
           privacy: VideoPrivacy.PUBLIC,
           channel: {
index 45b4a1a8112404ca1e289c29f3547ed5c1be267f..51db000a26a8be1498d413f8ce515300c723f7af 100644 (file)
@@ -32,7 +32,8 @@ describe('Test services', function () {
     const oembedUrl = 'http://localhost:9001/videos/watch/' + server.video.uuid
 
     const res = await getOEmbed(server.url, oembedUrl)
-    const expectedHtml = `<iframe width="560" height="315" src="http://localhost:9001/videos/embed/${server.video.uuid}" ` +
+    const expectedHtml = '<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts" ' +
+                         `src="http://localhost:9001/videos/embed/${server.video.uuid}" ` +
                          'frameborder="0" allowfullscreen></iframe>'
     const expectedThumbnailUrl = 'http://localhost:9001/static/previews/' + server.video.uuid + '.jpg'
 
index ef929960d73ff9bde3d626cd882d81dfd1528a51..1eace6491c0729281c4de105b9cc5df13102c7ea 100644 (file)
@@ -2,11 +2,22 @@
 
 import * as chai from 'chai'
 import 'mocha'
-import { VideoDetails } from '../../../../shared/models/videos'
+import { VideoDetails, VideoState } from '../../../../shared/models/videos'
 import { getVideoFileFPS } from '../../../helpers/ffmpeg-utils'
 import {
-  flushAndRunMultipleServers, flushTests, getVideo, getVideosList, killallServers, root, ServerInfo, setAccessTokensToServers, uploadVideo,
-  wait, webtorrentAdd
+  doubleFollow,
+  flushAndRunMultipleServers,
+  flushTests,
+  getMyVideos,
+  getVideo,
+  getVideosList,
+  killallServers,
+  root,
+  ServerInfo,
+  setAccessTokensToServers,
+  uploadVideo,
+  wait,
+  webtorrentAdd
 } from '../../utils'
 import { join } from 'path'
 
@@ -109,6 +120,63 @@ describe('Test video transcoding', function () {
     }
   })
 
+  it('Should wait transcoding before publishing the video', async function () {
+    this.timeout(80000)
+
+    await doubleFollow(servers[0], servers[1])
+
+    await wait(15000)
+
+    {
+      // Upload the video, but wait transcoding
+      const videoAttributes = {
+        name: 'waiting video',
+        fixture: 'video_short1.webm',
+        waitTranscoding: true
+      }
+      const resVideo = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, videoAttributes)
+      const videoId = resVideo.body.video.uuid
+
+      // Should be in transcode state
+      const { body } = await getVideo(servers[ 1 ].url, videoId)
+      expect(body.name).to.equal('waiting video')
+      expect(body.state.id).to.equal(VideoState.TO_TRANSCODE)
+      expect(body.state.label).to.equal('To transcode')
+      expect(body.waitTranscoding).to.be.true
+
+      // Should have my video
+      const resMyVideos = await getMyVideos(servers[1].url, servers[1].accessToken, 0, 10)
+      const videoToFindInMine = resMyVideos.body.data.find(v => v.name === 'waiting video')
+      expect(videoToFindInMine).not.to.be.undefined
+      expect(videoToFindInMine.state.id).to.equal(VideoState.TO_TRANSCODE)
+      expect(videoToFindInMine.state.label).to.equal('To transcode')
+      expect(videoToFindInMine.waitTranscoding).to.be.true
+
+      // Should not list this video
+      const resVideos = await getVideosList(servers[1].url)
+      const videoToFindInList = resVideos.body.data.find(v => v.name === 'waiting video')
+      expect(videoToFindInList).to.be.undefined
+
+      // Server 1 should not have the video yet
+      await getVideo(servers[0].url, videoId, 404)
+    }
+
+    await wait(30000)
+
+    for (const server of servers) {
+      const res = await getVideosList(server.url)
+      const videoToFind = res.body.data.find(v => v.name === 'waiting video')
+      expect(videoToFind).not.to.be.undefined
+
+      const res2 = await getVideo(server.url, videoToFind.id)
+      const videoDetails: VideoDetails = res2.body
+
+      expect(videoDetails.state.id).to.equal(VideoState.PUBLISHED)
+      expect(videoDetails.state.label).to.equal('Published')
+      expect(videoDetails.waitTranscoding).to.be.true
+    }
+  })
+
   after(async function () {
     killallServers(servers)
 
index 557dd8af988f506a8c2fdfbe31f7f722436ae0bd..fe1c0c03d7753a4a769293b83ed02bac14234ee6 100644 (file)
@@ -65,7 +65,7 @@ describe('Test create transcoding jobs', function () {
     const env = getEnvCli(servers[0])
     await execCLI(`${env} npm run create-transcoding-job -- -v ${video2UUID}`)
 
-    await wait(30000)
+    await wait(40000)
 
     for (const server of servers) {
       const res = await getVideosList(server.url)
index ab0ce12ecc92ba87a797e35feb3c5d2a90b05d1b..2c1d20ef10dff61cf119d5a51eb0417312b1dbf4 100644 (file)
@@ -27,6 +27,7 @@ type VideoAttributes = {
   language?: string
   nsfw?: boolean
   commentsEnabled?: boolean
+  waitTranscoding?: boolean
   description?: string
   tags?: string[]
   channelId?: number
@@ -326,6 +327,7 @@ async function uploadVideo (url: string, accessToken: string, videoAttributesArg
     language: 'zh',
     channelId: defaultChannelId,
     nsfw: true,
+    waitTranscoding: false,
     description: 'my super description',
     support: 'my super support text',
     tags: [ 'tag' ],
@@ -341,6 +343,7 @@ async function uploadVideo (url: string, accessToken: string, videoAttributesArg
               .field('name', attributes.name)
               .field('nsfw', JSON.stringify(attributes.nsfw))
               .field('commentsEnabled', JSON.stringify(attributes.commentsEnabled))
+              .field('waitTranscoding', JSON.stringify(attributes.waitTranscoding))
               .field('privacy', attributes.privacy.toString())
               .field('channelId', attributes.channelId)
 
index fd351ae7e5dcfba2c1862abeef479a1fe0893e67..e49fbb2f5ef3e0f2253703e0b94fb9581ecc3572 100644 (file)
@@ -176,6 +176,7 @@ async function uploadVideoOnPeerTube (videoInfo: any, videoPath: string, languag
     licence,
     language,
     nsfw: isNSFW(videoInfo),
+    waitTranscoding: true,
     commentsEnabled: true,
     description: videoInfo.description || undefined,
     support: undefined,
index 177d849f325b5ec1ce05d50d0d33a05e3a08765f..4d40c8c1a23824822a549cb38b567e9f9edadae3 100644 (file)
@@ -84,6 +84,7 @@ async function run () {
     fixture: program['file'],
     thumbnailfile: program['thumbnailPath'],
     previewfile: program['previewPath'],
+    waitTranscoding: true,
     privacy: program['privacy'],
     support: undefined
   }
index 767b6a2d015d08c2455d702c01a52e1db18b931e..c4071a6d99c2496b1b186c023d0470cf007185c8 100644 (file)
@@ -5,6 +5,7 @@ import {
   ActivityUrlObject
 } from './common-objects'
 import { ActivityPubOrderedCollection } from '../activitypub-ordered-collection'
+import { VideoState } from '../../videos'
 
 export interface VideoTorrentObject {
   type: 'Video'
@@ -19,6 +20,8 @@ export interface VideoTorrentObject {
   views: number
   sensitive: boolean
   commentsEnabled: boolean
+  waitTranscoding: boolean
+  state: VideoState
   published: string
   updated: string
   mediaType: 'text/markdown'
index 14a10f5d889b9a21d9069d887b18d1f0ce4729b9..9edfb559a099f775fc2e479e60dae924f9f0c4f9 100644 (file)
@@ -13,3 +13,4 @@ export * from './video-rate.type'
 export * from './video-resolution.enum'
 export * from './video-update.model'
 export * from './video.model'
+export * from './video-state.enum'
index 562bc1bf2cc917439e50c1082d46679d390a5661..2a1f622f61b3ebc3f824385fe613de084ad605e7 100644 (file)
@@ -7,7 +7,8 @@ export interface VideoCreate {
   description?: string
   support?: string
   channelId: number
-  nsfw: boolean
+  nsfw?: boolean
+  waitTranscoding?: boolean
   name: string
   tags?: string[]
   commentsEnabled?: boolean
diff --git a/shared/models/videos/video-state.enum.ts b/shared/models/videos/video-state.enum.ts
new file mode 100644 (file)
index 0000000..625aefa
--- /dev/null
@@ -0,0 +1,4 @@
+export enum VideoState {
+  PUBLISHED = 1,
+  TO_TRANSCODE = 2
+}
index c368d8464ce57f4ce69b9cc2c8a3d045ebc813c5..681b00b1874e8027be15ac2d8666373976315322 100644 (file)
@@ -11,6 +11,7 @@ export interface VideoUpdate {
   tags?: string[]
   commentsEnabled?: boolean
   nsfw?: boolean
+  waitTranscoding?: boolean
   channelId?: number
   thumbnailfile?: Blob
   previewfile?: Blob
index 1c86545d39982c07f8717c48db0ce980aa1a5037..857ca1fd97982560ae7a2bafbadab55e819c9b41 100644 (file)
@@ -1,4 +1,4 @@
-import { VideoResolution } from '../../index'
+import { VideoResolution, VideoState } from '../../index'
 import { Account } from '../actors'
 import { Avatar } from '../avatars/avatar.model'
 import { VideoChannel } from './video-channel.model'
@@ -41,6 +41,9 @@ export interface Video {
   dislikes: number
   nsfw: boolean
 
+  waitTranscoding?: boolean
+  state?: VideoConstant<VideoState>
+
   account: {
     id: number
     uuid: string
@@ -70,4 +73,8 @@ export interface VideoDetails extends Video {
   files: VideoFile[]
   account: Account
   commentsEnabled: boolean
+
+  // Not optional in details (unlike in Video)
+  waitTranscoding: boolean
+  state: VideoConstant<VideoState>
 }
index b75a2a8ba5fb431e1bf87a238c3a5b281f6c80f9..e1bf61b06297f4f01355e504e52d3303177e063a 100644 (file)
                       <p>Video description</p>
                     </div>
                   </div>
+                  <div class="prop-row prop-group">
+                    <div class="prop-name">
+                      <div class="prop-title">waitTranscoding</div>
+                      <div class="prop-subtitle"> in formData </div>
+                      <div class="prop-subtitle">
+                        <span class="json-property-type">boolean</span>
+                        <span class="json-property-range" title="Value limits"></span>
+                      </div>
+                    </div>
+                    <div class="prop-value">
+                      <p>Whether or not we wait transcoding before publish the video</p>
+                    </div>
+                  </div>
                   <div class="prop-row prop-group">
                     <div class="prop-name">
                       <div class="prop-title">support</div>
                       <p>Video category</p>
                     </div>
                   </div>
+                  <div class="prop-row prop-group">
+                    <div class="prop-name">
+                      <div class="prop-title">waitTranscoding</div>
+                      <div class="prop-subtitle"> in formData </div>
+                      <div class="prop-subtitle">
+                        <span class="json-property-type">boolean</span>
+                        <span class="json-property-range" title="Value limits"></span>
+                      </div>
+                    </div>
+                    <div class="prop-value">
+                      <p>Whether or not we wait transcoding before publish the video</p>
+                    </div>
+                  </div>
                   <div class="prop-row prop-group">
                     <div class="prop-name">
                       <div class="prop-title">licence</div>
index a1e286973cc8564ac83d236f7bd37e95fecd720b..be40af570d2c2742c1d96d96b9dc9d32f8557438 100644 (file)
@@ -682,6 +682,10 @@ paths:
           in: formData
           type: string
           description: 'Video description'
+        - name: waitTranscoding
+          in: formData
+          type: boolean
+          description: 'Whether or not we wait transcoding before publish the video'
         - name: support
           in: formData
           type: string
@@ -814,6 +818,10 @@ paths:
           in: formData
           type: number
           description: 'Video category'
+        - name: waitTranscoding
+          in: formData
+          type: boolean
+          description: 'Whether or not we wait transcoding before publish the video'
         - name: licence
           in: formData
           type: number
index 85ce0428d341349a962ca77915384842d855320c..26b44c83512ae434b281ac2719b3d51a3a1edd30 100644 (file)
@@ -63,13 +63,18 @@ $ node dist/server/tools/import-videos.js \
   * Vimeo: https://vimeo.com/xxxxxx
   * Dailymotion: https://www.dailymotion.com/xxxxx
 
- The script will get all public videos from Youtube, download them and upload to PeerTube.  
- Already downloaded videos will not be uploaded twice, so you can run and re-run the script in case of crash, disconnection...
+The script will get all public videos from Youtube, download them and upload to PeerTube.
+Already downloaded videos will not be uploaded twice, so you can run and re-run the script in case of crash, disconnection...
+
+Videos will be publicly available after transcoding (you can see them before that in your account on the web interface).
+
 
 ### upload.js
 
 You can use this script to import videos directly from the CLI.
 
+Videos will be publicly available after transcoding (you can see them before that in your account on the web interface).
+
 ```
 $ cd ${CLONE}
 $ node dist/server/tools/upload.js --help