From 2186386cca113506791583cb07d6ccacba7af4e0 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 12 Jun 2018 20:04:58 +0200 Subject: [PATCH] Add concept of video state, and add ability to wait transcoding before publishing a video --- .../my-account-videos.component.html | 2 +- .../my-account-videos.component.ts | 60 ++++--- .../app/shared/video/video-details.model.ts | 12 +- .../src/app/shared/video/video-edit.model.ts | 8 +- client/src/app/shared/video/video.model.ts | 14 +- client/src/app/shared/video/video.service.ts | 39 ++--- .../shared/video-edit.component.html | 10 ++ .../shared/video-edit.component.ts | 8 +- .../videos/+video-edit/video-add.component.ts | 2 + .../+video-watch/video-watch.component.html | 4 + .../+video-watch/video-watch.component.scss | 4 + .../+video-watch/video-watch.component.ts | 147 +++++++++--------- package.json | 2 - server/controllers/activitypub/client.ts | 8 +- server/controllers/activitypub/outbox.ts | 4 +- server/controllers/api/users.ts | 7 +- server/controllers/api/videos/index.ts | 47 +++--- server/helpers/activitypub.ts | 26 ++-- .../custom-validators/activitypub/videos.ts | 6 + server/helpers/custom-validators/videos.ts | 24 ++- server/helpers/utils.ts | 22 +-- server/initializers/constants.ts | 10 +- .../migrations/0220-video-state.ts | 62 ++++++++ server/lib/activitypub/audience.ts | 10 +- server/lib/activitypub/crawl.ts | 2 +- .../lib/activitypub/process/process-update.ts | 27 ++-- server/lib/activitypub/send/send-announce.ts | 14 +- server/lib/activitypub/send/send-create.ts | 43 +++-- server/lib/activitypub/send/send-like.ts | 33 ++-- server/lib/activitypub/send/send-undo.ts | 42 ++--- server/lib/activitypub/send/send-update.ts | 36 ++--- server/lib/activitypub/videos.ts | 80 ++++++---- server/lib/job-queue/handlers/video-file.ts | 127 ++++++++------- server/lib/job-queue/job-queue.ts | 1 + server/middlewares/cache.ts | 2 +- server/middlewares/validators/videos.ts | 10 ++ server/models/video/video.ts | 132 ++++++++-------- server/tests/api/check-params/videos.ts | 15 +- server/tests/api/videos/multiple-servers.ts | 8 +- server/tests/api/videos/services.ts | 3 +- server/tests/api/videos/video-transcoder.ts | 74 ++++++++- server/tests/cli/create-transcoding-job.ts | 2 +- server/tests/utils/videos/videos.ts | 3 + server/tools/import-videos.ts | 1 + server/tools/upload.ts | 1 + .../objects/video-torrent-object.ts | 3 + shared/models/videos/index.ts | 1 + shared/models/videos/video-create.model.ts | 3 +- shared/models/videos/video-state.enum.ts | 4 + shared/models/videos/video-update.model.ts | 1 + shared/models/videos/video.model.ts | 9 +- support/doc/api/html/index.html | 26 ++++ support/doc/api/openapi.yaml | 8 + support/doc/tools.md | 9 +- 54 files changed, 772 insertions(+), 486 deletions(-) create mode 100644 server/initializers/migrations/0220-video-state.ts create mode 100644 shared/models/videos/video-state.enum.ts diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.html b/client/src/app/+my-account/my-account-videos/my-account-videos.component.html index 35a99d0b3..eb24de7a7 100644 --- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.html +++ b/client/src/app/+my-account/my-account-videos/my-account-videos.component.html @@ -18,7 +18,7 @@
{{ video.name }} {{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views -
{{ video.privacy.label }}
+
{{ video.privacy.label }} - {{ getStateLabel(video) }}
diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts b/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts index eed4be01f..afc01073c 100644 --- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts +++ b/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts @@ -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[] = [] 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) { diff --git a/client/src/app/shared/video/video-details.model.ts b/client/src/app/shared/video/video-details.model.ts index 19c350ab3..e500ad6fc 100644 --- a/client/src/app/shared/video/video-details.model.ts +++ b/client/src/app/shared/video/video-details.model.ts @@ -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 + likesPercent: number dislikesPercent: number diff --git a/client/src/app/shared/video/video-edit.model.ts b/client/src/app/shared/video/video-edit.model.ts index ad2929db5..f045a3acd 100644 --- a/client/src/app/shared/video/video-edit.model.ts +++ b/client/src/app/shared/video/video-edit.model.ts @@ -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 } diff --git a/client/src/app/shared/video/video.model.ts b/client/src/app/shared/video/video.model.ts index d37dc2c3e..48a4b4260 100644 --- a/client/src/app/shared/video/video.model.ts +++ b/client/src/app/shared/video/video.model.ts @@ -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 + 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) { diff --git a/client/src/app/shared/video/video.service.ts b/client/src/app/shared/video/video.service.ts index 58cb52efc..d63915ad2 100644 --- a/client/src/app/shared/video/video.service.ts +++ b/client/src/app/shared/video/video.service.ts @@ -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) { 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 } + }) + ) } } diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.html b/client/src/app/videos/+video-edit/shared/video-edit.component.html index c8cd0d679..379cf7948 100644 --- a/client/src/app/videos/+video-edit/shared/video-edit.component.html +++ b/client/src/app/videos/+video-edit/shared/video-edit.component.html @@ -109,6 +109,16 @@ +
+ + + + +
+ diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.ts b/client/src/app/videos/+video-edit/shared/video-edit.component.ts index 61515c0b0..ee4fd5dc1 100644 --- a/client/src/app/videos/+video-edit/shared/video-edit.component.ts +++ b/client/src/app/videos/+video-edit/shared/video-edit.component.ts @@ -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 diff --git a/client/src/app/videos/+video-edit/video-add.component.ts b/client/src/app/videos/+video-edit/video-add.component.ts index 332f757d7..85afd0caa 100644 --- a/client/src/app/videos/+video-edit/video-add.component.ts +++ b/client/src/app/videos/+video-edit/video-add.component.ts @@ -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) diff --git a/client/src/app/videos/+video-watch/video-watch.component.html b/client/src/app/videos/+video-watch/video-watch.component.html index 4c650b121..8bd5c00ff 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.html +++ b/client/src/app/videos/+video-watch/video-watch.component.html @@ -3,6 +3,10 @@
+
+ The video is being transcoded, it may not work properly. +
+
diff --git a/client/src/app/videos/+video-watch/video-watch.component.scss b/client/src/app/videos/+video-watch/video-watch.component.scss index 00e776a69..06dd75653 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.scss +++ b/client/src/app/videos/+video-watch/video-watch.component.scss @@ -28,6 +28,10 @@ } } +#warning-transcoding { + text-align: center; +} + #video-not-found { height: 300px; line-height: 300px; diff --git a/client/src/app/videos/+video-watch/video-watch.component.ts b/client/src/app/videos/+video-watch/video-watch.component.ts index eefa43a73..498542fff 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.ts +++ b/client/src/app/videos/+video-watch/video-watch.component.ts @@ -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) { diff --git a/package.json b/package.json index 8d25613b6..739978a18 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,6 @@ } }, "lint-staged": { - "*.{css,md}": "precise-commits", "*.scss": [ "sass-lint -c .sass-lint.yml", "git add" @@ -166,7 +165,6 @@ "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", diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts index 1c780783c..ea8e25f68 100644 --- a/server/controllers/activitypub/client.ts +++ b/server/controllers/activitypub/client.ts @@ -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) } diff --git a/server/controllers/activitypub/outbox.ts b/server/controllers/activitypub/outbox.ts index 2793ae267..ae7adcd4c 100644 --- a/server/controllers/activitypub/outbox.ts +++ b/server/controllers/activitypub/outbox.ts @@ -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) } diff --git a/server/controllers/api/users.ts b/server/controllers/api/users.ts index 8dff4b87c..2b40c44d9 100644 --- a/server/controllers/api/users.ts +++ b/server/controllers/api/users.ts @@ -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 diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index 7f5e74626..9d9b2b0e1 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts @@ -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) diff --git a/server/helpers/activitypub.ts b/server/helpers/activitypub.ts index d1f3ec02d..37a251697 100644 --- a/server/helpers/activitypub.ts +++ b/server/helpers/activitypub.ts @@ -8,22 +8,24 @@ import { signObject } from './peertube-crypto' import { pageToStartAndCount } from './core-utils' function activityPubContextify (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: { diff --git a/server/helpers/custom-validators/activitypub/videos.ts b/server/helpers/custom-validators/activitypub/videos.ts index 7e1d57c34..37c90a0c8 100644 --- a/server/helpers/custom-validators/activitypub/videos.ts +++ b/server/helpers/custom-validators/activitypub/videos.ts @@ -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) && diff --git a/server/helpers/custom-validators/videos.ts b/server/helpers/custom-validators/videos.ts index f365df985..8496e679a 100644 --- a/server/helpers/custom-validators/videos.ts +++ b/server/helpers/custom-validators/videos.ts @@ -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, diff --git a/server/helpers/utils.ts b/server/helpers/utils.ts index e4556fa12..8fa861281 100644 --- a/server/helpers/utils.ts +++ b/server/helpers/utils.ts @@ -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 (objects: T[], objectsTotal: number) { +function getFormattedObjects (objects: T[], objectsTotal: number, formattedArg?: any) { const formattedObjects: U[] = [] objects.forEach(object => { - formattedObjects.push(object.toFormattedJSON()) + formattedObjects.push(object.toFormattedJSON(formattedArg)) }) - const res: ResultList = { + return { total: objectsTotal, data: formattedObjects - } - - return res + } as ResultList } 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) } } diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 79e4bb7f0..8dbc1b060 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -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 index 000000000..491702157 --- /dev/null +++ b/server/initializers/migrations/0220-video-state.ts @@ -0,0 +1,62 @@ +import * as Sequelize from 'sequelize' + +async function up (utils: { + transaction: Sequelize.Transaction + queryInterface: Sequelize.QueryInterface + sequelize: Sequelize.Sequelize +}): Promise { + // 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 } diff --git a/server/lib/activitypub/audience.ts b/server/lib/activitypub/audience.ts index c1265dbcd..7164135b6 100644 --- a/server/lib/activitypub/audience.ts +++ b/server/lib/activitypub/audience.ts @@ -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 (object: T, audience: ActivityAudience) { +function audiencify (object: T, audience: ActivityAudience) { return Object.assign(object, audience) } diff --git a/server/lib/activitypub/crawl.ts b/server/lib/activitypub/crawl.ts index 7305b3969..d4fc786f7 100644 --- a/server/lib/activitypub/crawl.ts +++ b/server/lib/activitypub/crawl.ts @@ -28,7 +28,7 @@ async function crawlCollectionPage (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) } diff --git a/server/lib/activitypub/process/process-update.ts b/server/lib/activitypub/process/process-update.ts index 2750f48c3..77de8c155 100644 --- a/server/lib/activitypub/process/process-update.ts +++ b/server/lib/activitypub/process/process-update.ts @@ -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[] = [] @@ -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) diff --git a/server/lib/activitypub/send/send-announce.ts b/server/lib/activitypub/send/send-announce.ts index fa1d47259..dfc099ff2 100644 --- a/server/lib/activitypub/send/send-announce.ts +++ b/server/lib/activitypub/send/send-announce.ts @@ -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 { - 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', diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts index 3ef4fcd3b..293947b05 100644 --- a/server/lib/activitypub/send/send-create.ts +++ b/server/lib/activitypub/send/send-create.ts @@ -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 { - 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) { diff --git a/server/lib/activitypub/send/send-like.ts b/server/lib/activitypub/send/send-like.ts index ddeb1fcd2..37ee7c096 100644 --- a/server/lib/activitypub/send/send-like.ts +++ b/server/lib/activitypub/send/send-like.ts @@ -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 { - 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 + ) } // --------------------------------------------------------------------------- diff --git a/server/lib/activitypub/send/send-undo.ts b/server/lib/activitypub/send/send-undo.ts index 9733e66dc..33c3d2429 100644 --- a/server/lib/activitypub/send/send-undo.ts +++ b/server/lib/activitypub/send/send-undo.ts @@ -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 { - 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 + ) } diff --git a/server/lib/activitypub/send/send-update.ts b/server/lib/activitypub/send/send-update.ts index d64b88343..2fd374ec6 100644 --- a/server/lib/activitypub/send/send-update.ts +++ b/server/lib/activitypub/send/send-update.ts @@ -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 { - 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 + ) } diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts index 907f7e11e..7ec8ca193 100644 --- a/server/lib/activitypub/videos.ts +++ b/server/lib/activitypub/videos.ts @@ -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 { const options = { uri: videoUrl, @@ -324,3 +328,17 @@ async function fetchRemoteVideo (videoUrl: string): Promise return body } + +export { + federateVideoIfNeeded, + fetchRemoteVideo, + getOrCreateAccountAndVideoAndChannel, + fetchRemoteVideoPreview, + fetchRemoteVideoDescription, + generateThumbnailFromUrl, + videoActivityObjectToDBAttributes, + videoFileActivityUrlToDBAttributes, + getOrCreateVideo, + getOrCreateVideoChannel, + addVideoShares +} diff --git a/server/lib/job-queue/handlers/video-file.ts b/server/lib/job-queue/handlers/video-file.ts index 85f7dbfc2..f5ad076a6 100644 --- a/server/lib/job-queue/handlers/video-file.ts +++ b/server/lib/job-queue/handlers/video-file.ts @@ -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[] = [] + + 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[] = [] + 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) + }) } // --------------------------------------------------------------------------- diff --git a/server/lib/job-queue/job-queue.ts b/server/lib/job-queue/job-queue.ts index bdfa19b61..695fe0eea 100644 --- a/server/lib/job-queue/job-queue.ts +++ b/server/lib/job-queue/job-queue.ts @@ -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) } }) diff --git a/server/middlewares/cache.ts b/server/middlewares/cache.ts index bf6659687..1de44db70 100644 --- a/server/middlewares/cache.ts +++ b/server/middlewares/cache.ts @@ -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) diff --git a/server/middlewares/validators/videos.ts b/server/middlewares/validators/videos.ts index c5c45fe58..e181aebdb 100644 --- a/server/middlewares/validators/videos.ts +++ b/server/middlewares/validators/videos.ts @@ -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() diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 1cb1e6798..59c378efa 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -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 { @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 { }) } - 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 = { offset: start, limit: count, @@ -858,12 +842,13 @@ export class VideoModel extends Model { .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 { } 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 { 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 { 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 { 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 { language, views: this.views, sensitive: this.nsfw, + waitTranscoding: this.waitTranscoding, + state: this.state, commentsEnabled: this.commentsEnabled, published: this.publishedAt.toISOString(), updated: this.updatedAt.toISOString(), diff --git a/server/tests/api/check-params/videos.ts b/server/tests/api/check-params/videos.ts index bc6c7fc46..04bed3b44 100644 --- a/server/tests/api/check-params/videos.ts +++ b/server/tests/api/check-params/videos.ts @@ -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 diff --git a/server/tests/api/videos/multiple-servers.ts b/server/tests/api/videos/multiple-servers.ts index 5f9a76621..edc46a644 100644 --- a/server/tests/api/videos/multiple-servers.ts +++ b/server/tests/api/videos/multiple-servers.ts @@ -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: { diff --git a/server/tests/api/videos/services.ts b/server/tests/api/videos/services.ts index 45b4a1a81..51db000a2 100644 --- a/server/tests/api/videos/services.ts +++ b/server/tests/api/videos/services.ts @@ -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 = `' const expectedThumbnailUrl = 'http://localhost:9001/static/previews/' + server.video.uuid + '.jpg' diff --git a/server/tests/api/videos/video-transcoder.ts b/server/tests/api/videos/video-transcoder.ts index ef929960d..1eace6491 100644 --- a/server/tests/api/videos/video-transcoder.ts +++ b/server/tests/api/videos/video-transcoder.ts @@ -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) diff --git a/server/tests/cli/create-transcoding-job.ts b/server/tests/cli/create-transcoding-job.ts index 557dd8af9..fe1c0c03d 100644 --- a/server/tests/cli/create-transcoding-job.ts +++ b/server/tests/cli/create-transcoding-job.ts @@ -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) diff --git a/server/tests/utils/videos/videos.ts b/server/tests/utils/videos/videos.ts index ab0ce12ec..2c1d20ef1 100644 --- a/server/tests/utils/videos/videos.ts +++ b/server/tests/utils/videos/videos.ts @@ -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) diff --git a/server/tools/import-videos.ts b/server/tools/import-videos.ts index fd351ae7e..e49fbb2f5 100644 --- a/server/tools/import-videos.ts +++ b/server/tools/import-videos.ts @@ -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, diff --git a/server/tools/upload.ts b/server/tools/upload.ts index 177d849f3..4d40c8c1a 100644 --- a/server/tools/upload.ts +++ b/server/tools/upload.ts @@ -84,6 +84,7 @@ async function run () { fixture: program['file'], thumbnailfile: program['thumbnailPath'], previewfile: program['previewPath'], + waitTranscoding: true, privacy: program['privacy'], support: undefined } diff --git a/shared/models/activitypub/objects/video-torrent-object.ts b/shared/models/activitypub/objects/video-torrent-object.ts index 767b6a2d0..c4071a6d9 100644 --- a/shared/models/activitypub/objects/video-torrent-object.ts +++ b/shared/models/activitypub/objects/video-torrent-object.ts @@ -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' diff --git a/shared/models/videos/index.ts b/shared/models/videos/index.ts index 14a10f5d8..9edfb559a 100644 --- a/shared/models/videos/index.ts +++ b/shared/models/videos/index.ts @@ -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' diff --git a/shared/models/videos/video-create.model.ts b/shared/models/videos/video-create.model.ts index 562bc1bf2..2a1f622f6 100644 --- a/shared/models/videos/video-create.model.ts +++ b/shared/models/videos/video-create.model.ts @@ -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 index 000000000..625aefae1 --- /dev/null +++ b/shared/models/videos/video-state.enum.ts @@ -0,0 +1,4 @@ +export enum VideoState { + PUBLISHED = 1, + TO_TRANSCODE = 2 +} diff --git a/shared/models/videos/video-update.model.ts b/shared/models/videos/video-update.model.ts index c368d8464..681b00b18 100644 --- a/shared/models/videos/video-update.model.ts +++ b/shared/models/videos/video-update.model.ts @@ -11,6 +11,7 @@ export interface VideoUpdate { tags?: string[] commentsEnabled?: boolean nsfw?: boolean + waitTranscoding?: boolean channelId?: number thumbnailfile?: Blob previewfile?: Blob diff --git a/shared/models/videos/video.model.ts b/shared/models/videos/video.model.ts index 1c86545d3..857ca1fd9 100644 --- a/shared/models/videos/video.model.ts +++ b/shared/models/videos/video.model.ts @@ -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 + 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 } diff --git a/support/doc/api/html/index.html b/support/doc/api/html/index.html index b75a2a8ba..e1bf61b06 100644 --- a/support/doc/api/html/index.html +++ b/support/doc/api/html/index.html @@ -3435,6 +3435,19 @@

Video description

+
+
+
waitTranscoding
+
in formData
+
+ boolean + +
+
+
+

Whether or not we wait transcoding before publish the video

+
+
support
@@ -4009,6 +4022,19 @@

Video category

+
+
+
waitTranscoding
+
in formData
+
+ boolean + +
+
+
+

Whether or not we wait transcoding before publish the video

+
+
licence
diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml index a1e286973..be40af570 100644 --- a/support/doc/api/openapi.yaml +++ b/support/doc/api/openapi.yaml @@ -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 diff --git a/support/doc/tools.md b/support/doc/tools.md index 85ce0428d..26b44c835 100644 --- a/support/doc/tools.md +++ b/support/doc/tools.md @@ -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 -- 2.25.1