aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2018-06-12 20:04:58 +0200
committerChocobozzz <me@florianbigard.com>2018-06-12 20:37:51 +0200
commit2186386cca113506791583cb07d6ccacba7af4e0 (patch)
tree3c214c0b5fbd64332624267fa6e51fd4a9cf6474
parent6ccdf3a23ecec5ba2eeaf487fd1fafdc7606b4bf (diff)
downloadPeerTube-2186386cca113506791583cb07d6ccacba7af4e0.tar.gz
PeerTube-2186386cca113506791583cb07d6ccacba7af4e0.tar.zst
PeerTube-2186386cca113506791583cb07d6ccacba7af4e0.zip
Add concept of video state, and add ability to wait transcoding before
publishing a video
-rw-r--r--client/src/app/+my-account/my-account-videos/my-account-videos.component.html2
-rw-r--r--client/src/app/+my-account/my-account-videos/my-account-videos.component.ts60
-rw-r--r--client/src/app/shared/video/video-details.model.ts12
-rw-r--r--client/src/app/shared/video/video-edit.model.ts8
-rw-r--r--client/src/app/shared/video/video.model.ts14
-rw-r--r--client/src/app/shared/video/video.service.ts39
-rw-r--r--client/src/app/videos/+video-edit/shared/video-edit.component.html10
-rw-r--r--client/src/app/videos/+video-edit/shared/video-edit.component.ts8
-rw-r--r--client/src/app/videos/+video-edit/video-add.component.ts2
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.html4
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.scss4
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.ts147
-rw-r--r--package.json2
-rw-r--r--server/controllers/activitypub/client.ts8
-rw-r--r--server/controllers/activitypub/outbox.ts4
-rw-r--r--server/controllers/api/users.ts7
-rw-r--r--server/controllers/api/videos/index.ts47
-rw-r--r--server/helpers/activitypub.ts26
-rw-r--r--server/helpers/custom-validators/activitypub/videos.ts6
-rw-r--r--server/helpers/custom-validators/videos.ts24
-rw-r--r--server/helpers/utils.ts22
-rw-r--r--server/initializers/constants.ts10
-rw-r--r--server/initializers/migrations/0220-video-state.ts62
-rw-r--r--server/lib/activitypub/audience.ts10
-rw-r--r--server/lib/activitypub/crawl.ts2
-rw-r--r--server/lib/activitypub/process/process-update.ts27
-rw-r--r--server/lib/activitypub/send/send-announce.ts14
-rw-r--r--server/lib/activitypub/send/send-create.ts43
-rw-r--r--server/lib/activitypub/send/send-like.ts33
-rw-r--r--server/lib/activitypub/send/send-undo.ts42
-rw-r--r--server/lib/activitypub/send/send-update.ts36
-rw-r--r--server/lib/activitypub/videos.ts80
-rw-r--r--server/lib/job-queue/handlers/video-file.ts127
-rw-r--r--server/lib/job-queue/job-queue.ts1
-rw-r--r--server/middlewares/cache.ts2
-rw-r--r--server/middlewares/validators/videos.ts10
-rw-r--r--server/models/video/video.ts132
-rw-r--r--server/tests/api/check-params/videos.ts15
-rw-r--r--server/tests/api/videos/multiple-servers.ts8
-rw-r--r--server/tests/api/videos/services.ts3
-rw-r--r--server/tests/api/videos/video-transcoder.ts74
-rw-r--r--server/tests/cli/create-transcoding-job.ts2
-rw-r--r--server/tests/utils/videos/videos.ts3
-rw-r--r--server/tools/import-videos.ts1
-rw-r--r--server/tools/upload.ts1
-rw-r--r--shared/models/activitypub/objects/video-torrent-object.ts3
-rw-r--r--shared/models/videos/index.ts1
-rw-r--r--shared/models/videos/video-create.model.ts3
-rw-r--r--shared/models/videos/video-state.enum.ts4
-rw-r--r--shared/models/videos/video-update.model.ts1
-rw-r--r--shared/models/videos/video.model.ts9
-rw-r--r--support/doc/api/html/index.html26
-rw-r--r--support/doc/api/openapi.yaml8
-rw-r--r--support/doc/tools.md9
54 files changed, 772 insertions, 486 deletions
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 @@
18 <div class="video-info"> 18 <div class="video-info">
19 <a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a> 19 <a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a>
20 <span i18n class="video-info-date-views">{{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span> 20 <span i18n class="video-info-date-views">{{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span>
21 <div class="video-info-private">{{ video.privacy.label }}</div> 21 <div class="video-info-private">{{ video.privacy.label }} - {{ getStateLabel(video) }}</div>
22 </div> 22 </div>
23 23
24 <!-- Display only once --> 24 <!-- Display only once -->
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'
12import { Video } from '../../shared/video/video.model' 12import { Video } from '../../shared/video/video.model'
13import { VideoService } from '../../shared/video/video.service' 13import { VideoService } from '../../shared/video/video.service'
14import { I18n } from '@ngx-translate/i18n-polyfill' 14import { I18n } from '@ngx-translate/i18n-polyfill'
15import { VideoState } from '../../../../../shared/models/videos'
15 16
16@Component({ 17@Component({
17 selector: 'my-account-videos', 18 selector: 'my-account-videos',
@@ -59,7 +60,7 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni
59 } 60 }
60 61
61 isInSelectionMode () { 62 isInSelectionMode () {
62 return Object.keys(this.checkedVideos).some(k => this.checkedVideos[k] === true) 63 return Object.keys(this.checkedVideos).some(k => this.checkedVideos[ k ] === true)
63 } 64 }
64 65
65 getVideosObservable (page: number) { 66 getVideosObservable (page: number) {
@@ -74,47 +75,68 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni
74 75
75 async deleteSelectedVideos () { 76 async deleteSelectedVideos () {
76 const toDeleteVideosIds = Object.keys(this.checkedVideos) 77 const toDeleteVideosIds = Object.keys(this.checkedVideos)
77 .filter(k => this.checkedVideos[k] === true) 78 .filter(k => this.checkedVideos[ k ] === true)
78 .map(k => parseInt(k, 10)) 79 .map(k => parseInt(k, 10))
79 80
80 const res = await this.confirmService.confirm(`Do you really want to delete ${toDeleteVideosIds.length} videos?`, 'Delete') 81 const res = await this.confirmService.confirm(
82 this.i18n('Do you really want to delete {{deleteLength}} videos?', { deleteLength: toDeleteVideosIds.length }),
83 this.i18n('Delete')
84 )
81 if (res === false) return 85 if (res === false) return
82 86
83 const observables: Observable<any>[] = [] 87 const observables: Observable<any>[] = []
84 for (const videoId of toDeleteVideosIds) { 88 for (const videoId of toDeleteVideosIds) {
85 const o = this.videoService 89 const o = this.videoService.removeVideo(videoId)
86 .removeVideo(videoId)
87 .pipe(tap(() => this.spliceVideosById(videoId))) 90 .pipe(tap(() => this.spliceVideosById(videoId)))
88 91
89 observables.push(o) 92 observables.push(o)
90 } 93 }
91 94
92 observableFrom(observables).pipe( 95 observableFrom(observables)
93 concatAll()) 96 .pipe(concatAll())
94 .subscribe( 97 .subscribe(
95 res => { 98 res => {
96 this.notificationsService.success('Success', `${toDeleteVideosIds.length} videos deleted.`) 99 this.notificationsService.success(
100 this.i18n('Success'),
101 this.i18n('{{deleteLength}} videos deleted.', { deleteLength: toDeleteVideosIds.length })
102 )
103
97 this.abortSelectionMode() 104 this.abortSelectionMode()
98 this.reloadVideos() 105 this.reloadVideos()
99 }, 106 },
100 107
101 err => this.notificationsService.error('Error', err.message) 108 err => this.notificationsService.error(this.i18n('Error'), err.message)
102 ) 109 )
103 } 110 }
104 111
105 async deleteVideo (video: Video) { 112 async deleteVideo (video: Video) {
106 const res = await this.confirmService.confirm(`Do you really want to delete ${video.name}?`, 'Delete') 113 const res = await this.confirmService.confirm(
114 this.i18n('Do you really want to delete {{videoName}}?', { videoName: video.name }),
115 this.i18n('Delete')
116 )
107 if (res === false) return 117 if (res === false) return
108 118
109 this.videoService.removeVideo(video.id) 119 this.videoService.removeVideo(video.id)
110 .subscribe( 120 .subscribe(
111 status => { 121 status => {
112 this.notificationsService.success('Success', `Video ${video.name} deleted.`) 122 this.notificationsService.success(
113 this.reloadVideos() 123 this.i18n('Success'),
114 }, 124 this.i18n('Video {{videoName}} deleted.', { videoName: video.name })
125 )
126 this.reloadVideos()
127 },
128
129 error => this.notificationsService.error(this.i18n('Error'), error.message)
130 )
131 }
115 132
116 error => this.notificationsService.error('Error', error.message) 133 getStateLabel (video: Video) {
117 ) 134 if (video.state.id === VideoState.PUBLISHED) return this.i18n('Published')
135
136 if (video.state.id === VideoState.TO_TRANSCODE && video.waitTranscoding === true) return this.i18n('Waiting transcoding')
137 if (video.state.id === VideoState.TO_TRANSCODE) return this.i18n('To transcode')
138
139 return this.i18n('Unknown state')
118 } 140 }
119 141
120 protected buildVideoHeight () { 142 protected buildVideoHeight () {
@@ -124,7 +146,7 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni
124 146
125 private spliceVideosById (id: number) { 147 private spliceVideosById (id: number) {
126 for (const key of Object.keys(this.loadedPages)) { 148 for (const key of Object.keys(this.loadedPages)) {
127 const videos = this.loadedPages[key] 149 const videos = this.loadedPages[ key ]
128 const index = videos.findIndex(v => v.id === id) 150 const index = videos.findIndex(v => v.id === id)
129 151
130 if (index !== -1) { 152 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 @@
1import { UserRight, VideoChannel, VideoDetails as VideoDetailsServerModel, VideoFile } from '../../../../../shared' 1import {
2 UserRight,
3 VideoChannel,
4 VideoConstant,
5 VideoDetails as VideoDetailsServerModel,
6 VideoFile,
7 VideoState
8} from '../../../../../shared'
2import { AuthUser } from '../../core' 9import { AuthUser } from '../../core'
3import { Video } from '../../shared/video/video.model' 10import { Video } from '../../shared/video/video.model'
4import { Account } from '@app/shared/account/account.model' 11import { Account } from '@app/shared/account/account.model'
@@ -12,6 +19,9 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
12 account: Account 19 account: Account
13 commentsEnabled: boolean 20 commentsEnabled: boolean
14 21
22 waitTranscoding: boolean
23 state: VideoConstant<VideoState>
24
15 likesPercent: number 25 likesPercent: number
16 dislikesPercent: number 26 dislikesPercent: number
17 27
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 @@
1import { VideoDetails } from './video-details.model' 1import { VideoDetails } from './video-details.model'
2import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy.enum' 2import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy.enum'
3import { VideoUpdate } from '../../../../../shared/models/videos'
3 4
4export class VideoEdit { 5export class VideoEdit implements VideoUpdate {
5 category: number 6 category: number
6 licence: number 7 licence: number
7 language: string 8 language: string
@@ -10,6 +11,7 @@ export class VideoEdit {
10 tags: string[] 11 tags: string[]
11 nsfw: boolean 12 nsfw: boolean
12 commentsEnabled: boolean 13 commentsEnabled: boolean
14 waitTranscoding: boolean
13 channelId: number 15 channelId: number
14 privacy: VideoPrivacy 16 privacy: VideoPrivacy
15 support: string 17 support: string
@@ -32,6 +34,7 @@ export class VideoEdit {
32 this.tags = videoDetails.tags 34 this.tags = videoDetails.tags
33 this.nsfw = videoDetails.nsfw 35 this.nsfw = videoDetails.nsfw
34 this.commentsEnabled = videoDetails.commentsEnabled 36 this.commentsEnabled = videoDetails.commentsEnabled
37 this.waitTranscoding = videoDetails.waitTranscoding
35 this.channelId = videoDetails.channel.id 38 this.channelId = videoDetails.channel.id
36 this.privacy = videoDetails.privacy.id 39 this.privacy = videoDetails.privacy.id
37 this.support = videoDetails.support 40 this.support = videoDetails.support
@@ -42,7 +45,7 @@ export class VideoEdit {
42 45
43 patch (values: Object) { 46 patch (values: Object) {
44 Object.keys(values).forEach((key) => { 47 Object.keys(values).forEach((key) => {
45 this[key] = values[key] 48 this[ key ] = values[ key ]
46 }) 49 })
47 } 50 }
48 51
@@ -57,6 +60,7 @@ export class VideoEdit {
57 tags: this.tags, 60 tags: this.tags,
58 nsfw: this.nsfw, 61 nsfw: this.nsfw,
59 commentsEnabled: this.commentsEnabled, 62 commentsEnabled: this.commentsEnabled,
63 waitTranscoding: this.waitTranscoding,
60 channelId: this.channelId, 64 channelId: this.channelId,
61 privacy: this.privacy 65 privacy: this.privacy
62 } 66 }
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 @@
1import { User } from '../' 1import { User } from '../'
2import { Video as VideoServerModel, VideoPrivacy } from '../../../../../shared' 2import { Video as VideoServerModel, VideoPrivacy, VideoState } from '../../../../../shared'
3import { Avatar } from '../../../../../shared/models/avatars/avatar.model' 3import { Avatar } from '../../../../../shared/models/avatars/avatar.model'
4import { VideoConstant } from '../../../../../shared/models/videos/video.model' 4import { VideoConstant } from '../../../../../shared/models/videos/video.model'
5import { getAbsoluteAPIUrl } from '../misc/utils' 5import { getAbsoluteAPIUrl } from '../misc/utils'
@@ -36,6 +36,9 @@ export class Video implements VideoServerModel {
36 dislikes: number 36 dislikes: number
37 nsfw: boolean 37 nsfw: boolean
38 38
39 waitTranscoding?: boolean
40 state?: VideoConstant<VideoState>
41
39 account: { 42 account: {
40 id: number 43 id: number
41 uuid: string 44 uuid: string
@@ -58,15 +61,14 @@ export class Video implements VideoServerModel {
58 61
59 private static createDurationString (duration: number) { 62 private static createDurationString (duration: number) {
60 const hours = Math.floor(duration / 3600) 63 const hours = Math.floor(duration / 3600)
61 const minutes = Math.floor(duration % 3600 / 60) 64 const minutes = Math.floor((duration % 3600) / 60)
62 const seconds = duration % 60 65 const seconds = duration % 60
63 66
64 const minutesPadding = minutes >= 10 ? '' : '0' 67 const minutesPadding = minutes >= 10 ? '' : '0'
65 const secondsPadding = seconds >= 10 ? '' : '0' 68 const secondsPadding = seconds >= 10 ? '' : '0'
66 const displayedHours = hours > 0 ? hours.toString() + ':' : '' 69 const displayedHours = hours > 0 ? hours.toString() + ':' : ''
67 70
68 return displayedHours + minutesPadding + 71 return displayedHours + minutesPadding + minutes.toString() + ':' + secondsPadding + seconds.toString()
69 minutes.toString() + ':' + secondsPadding + seconds.toString()
70 } 72 }
71 73
72 constructor (hash: VideoServerModel, translations = {}) { 74 constructor (hash: VideoServerModel, translations = {}) {
@@ -78,6 +80,8 @@ export class Video implements VideoServerModel {
78 this.licence = hash.licence 80 this.licence = hash.licence
79 this.language = hash.language 81 this.language = hash.language
80 this.privacy = hash.privacy 82 this.privacy = hash.privacy
83 this.waitTranscoding = hash.waitTranscoding
84 this.state = hash.state
81 this.description = hash.description 85 this.description = hash.description
82 this.duration = hash.duration 86 this.duration = hash.duration
83 this.durationLabel = Video.createDurationString(hash.duration) 87 this.durationLabel = Video.createDurationString(hash.duration)
@@ -104,6 +108,8 @@ export class Video implements VideoServerModel {
104 this.licence.label = peertubeTranslate(this.licence.label, translations) 108 this.licence.label = peertubeTranslate(this.licence.label, translations)
105 this.language.label = peertubeTranslate(this.language.label, translations) 109 this.language.label = peertubeTranslate(this.language.label, translations)
106 this.privacy.label = peertubeTranslate(this.privacy.label, translations) 110 this.privacy.label = peertubeTranslate(this.privacy.label, translations)
111
112 if (this.state) this.state.label = peertubeTranslate(this.state.label, translations)
107 } 113 }
108 114
109 isVideoNSFWForUser (user: User, serverConfig: ServerConfig) { 115 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 {
80 privacy: video.privacy, 80 privacy: video.privacy,
81 tags: video.tags, 81 tags: video.tags,
82 nsfw: video.nsfw, 82 nsfw: video.nsfw,
83 waitTranscoding: video.waitTranscoding,
83 commentsEnabled: video.commentsEnabled, 84 commentsEnabled: video.commentsEnabled,
84 thumbnailfile: video.thumbnailfile, 85 thumbnailfile: video.thumbnailfile,
85 previewfile: video.previewfile 86 previewfile: video.previewfile
@@ -98,11 +99,11 @@ export class VideoService {
98 const req = new HttpRequest('POST', VideoService.BASE_VIDEO_URL + 'upload', video, { reportProgress: true }) 99 const req = new HttpRequest('POST', VideoService.BASE_VIDEO_URL + 'upload', video, { reportProgress: true })
99 100
100 return this.authHttp 101 return this.authHttp
101 .request<{ video: { id: number, uuid: string} }>(req) 102 .request<{ video: { id: number, uuid: string } }>(req)
102 .pipe(catchError(this.restExtractor.handleError)) 103 .pipe(catchError(this.restExtractor.handleError))
103 } 104 }
104 105
105 getMyVideos (videoPagination: ComponentPagination, sort: VideoSortField): Observable<{ videos: Video[], totalVideos: number}> { 106 getMyVideos (videoPagination: ComponentPagination, sort: VideoSortField): Observable<{ videos: Video[], totalVideos: number }> {
106 const pagination = this.restService.componentPaginationToRestPagination(videoPagination) 107 const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
107 108
108 let params = new HttpParams() 109 let params = new HttpParams()
@@ -120,7 +121,7 @@ export class VideoService {
120 account: Account, 121 account: Account,
121 videoPagination: ComponentPagination, 122 videoPagination: ComponentPagination,
122 sort: VideoSortField 123 sort: VideoSortField
123 ): Observable<{ videos: Video[], totalVideos: number}> { 124 ): Observable<{ videos: Video[], totalVideos: number }> {
124 const pagination = this.restService.componentPaginationToRestPagination(videoPagination) 125 const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
125 126
126 let params = new HttpParams() 127 let params = new HttpParams()
@@ -138,7 +139,7 @@ export class VideoService {
138 videoChannel: VideoChannel, 139 videoChannel: VideoChannel,
139 videoPagination: ComponentPagination, 140 videoPagination: ComponentPagination,
140 sort: VideoSortField 141 sort: VideoSortField
141 ): Observable<{ videos: Video[], totalVideos: number}> { 142 ): Observable<{ videos: Video[], totalVideos: number }> {
142 const pagination = this.restService.componentPaginationToRestPagination(videoPagination) 143 const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
143 144
144 let params = new HttpParams() 145 let params = new HttpParams()
@@ -156,7 +157,7 @@ export class VideoService {
156 videoPagination: ComponentPagination, 157 videoPagination: ComponentPagination,
157 sort: VideoSortField, 158 sort: VideoSortField,
158 filter?: VideoFilter 159 filter?: VideoFilter
159 ): Observable<{ videos: Video[], totalVideos: number}> { 160 ): Observable<{ videos: Video[], totalVideos: number }> {
160 const pagination = this.restService.componentPaginationToRestPagination(videoPagination) 161 const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
161 162
162 let params = new HttpParams() 163 let params = new HttpParams()
@@ -225,7 +226,7 @@ export class VideoService {
225 search: string, 226 search: string,
226 videoPagination: ComponentPagination, 227 videoPagination: ComponentPagination,
227 sort: VideoSortField 228 sort: VideoSortField
228 ): Observable<{ videos: Video[], totalVideos: number}> { 229 ): Observable<{ videos: Video[], totalVideos: number }> {
229 const url = VideoService.BASE_VIDEO_URL + 'search' 230 const url = VideoService.BASE_VIDEO_URL + 'search'
230 231
231 const pagination = this.restService.componentPaginationToRestPagination(videoPagination) 232 const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
@@ -295,18 +296,18 @@ export class VideoService {
295 296
296 private extractVideos (result: ResultList<VideoServerModel>) { 297 private extractVideos (result: ResultList<VideoServerModel>) {
297 return this.serverService.localeObservable 298 return this.serverService.localeObservable
298 .pipe( 299 .pipe(
299 map(translations => { 300 map(translations => {
300 const videosJson = result.data 301 const videosJson = result.data
301 const totalVideos = result.total 302 const totalVideos = result.total
302 const videos: Video[] = [] 303 const videos: Video[] = []
303 304
304 for (const videoJson of videosJson) { 305 for (const videoJson of videosJson) {
305 videos.push(new Video(videoJson, translations)) 306 videos.push(new Video(videoJson, translations))
306 } 307 }
307 308
308 return { videos, totalVideos } 309 return { videos, totalVideos }
309 }) 310 })
310 ) 311 )
311 } 312 }
312} 313}
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 @@
109 <label i18n for="commentsEnabled">Enable video comments</label> 109 <label i18n for="commentsEnabled">Enable video comments</label>
110 </div> 110 </div>
111 111
112 <div class="form-group form-group-checkbox">
113 <input type="checkbox" id="waitTranscoding" formControlName="waitTranscoding" />
114 <label for="waitTranscoding"></label>
115 <label i18n for="waitTranscoding">Wait transcoding before publishing the video</label>
116 <my-help
117 tooltipPlacement="top" helpType="custom" i18n-customHtml
118 customHtml="If you decide to not wait transcoding before publishing the video, it can be unplayable until it transcoding ends."
119 ></my-help>
120 </div>
121
112 </div> 122 </div>
113 </tab> 123 </tab>
114 124
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 {
47 const defaultValues = { 47 const defaultValues = {
48 nsfw: 'false', 48 nsfw: 'false',
49 commentsEnabled: 'true', 49 commentsEnabled: 'true',
50 waitTranscoding: 'true',
50 tags: [] 51 tags: []
51 } 52 }
52 const obj = { 53 const obj = {
@@ -55,6 +56,7 @@ export class VideoEditComponent implements OnInit {
55 channelId: this.videoValidatorsService.VIDEO_CHANNEL, 56 channelId: this.videoValidatorsService.VIDEO_CHANNEL,
56 nsfw: null, 57 nsfw: null,
57 commentsEnabled: null, 58 commentsEnabled: null,
59 waitTranscoding: null,
58 category: this.videoValidatorsService.VIDEO_CATEGORY, 60 category: this.videoValidatorsService.VIDEO_CATEGORY,
59 licence: this.videoValidatorsService.VIDEO_LICENCE, 61 licence: this.videoValidatorsService.VIDEO_LICENCE,
60 language: this.videoValidatorsService.VIDEO_LANGUAGE, 62 language: this.videoValidatorsService.VIDEO_LANGUAGE,
@@ -74,13 +76,13 @@ export class VideoEditComponent implements OnInit {
74 ) 76 )
75 77
76 // We will update the "support" field depending on the channel 78 // We will update the "support" field depending on the channel
77 this.form.controls['channelId'] 79 this.form.controls[ 'channelId' ]
78 .valueChanges 80 .valueChanges
79 .pipe(map(res => parseInt(res.toString(), 10))) 81 .pipe(map(res => parseInt(res.toString(), 10)))
80 .subscribe( 82 .subscribe(
81 newChannelId => { 83 newChannelId => {
82 const oldChannelId = parseInt(this.form.value['channelId'], 10) 84 const oldChannelId = parseInt(this.form.value[ 'channelId' ], 10)
83 const currentSupport = this.form.value['support'] 85 const currentSupport = this.form.value[ 'support' ]
84 86
85 // Not initialized yet 87 // Not initialized yet
86 if (isNaN(newChannelId)) return 88 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
164 164
165 const privacy = this.firstStepPrivacyId.toString() 165 const privacy = this.firstStepPrivacyId.toString()
166 const nsfw = false 166 const nsfw = false
167 const waitTranscoding = true
167 const commentsEnabled = true 168 const commentsEnabled = true
168 const channelId = this.firstStepChannelId.toString() 169 const channelId = this.firstStepChannelId.toString()
169 170
@@ -173,6 +174,7 @@ export class VideoAddComponent extends FormReactive implements OnInit, OnDestroy
173 formData.append('privacy', VideoPrivacy.PRIVATE.toString()) 174 formData.append('privacy', VideoPrivacy.PRIVATE.toString())
174 formData.append('nsfw', '' + nsfw) 175 formData.append('nsfw', '' + nsfw)
175 formData.append('commentsEnabled', '' + commentsEnabled) 176 formData.append('commentsEnabled', '' + commentsEnabled)
177 formData.append('waitTranscoding', '' + waitTranscoding)
176 formData.append('channelId', '' + channelId) 178 formData.append('channelId', '' + channelId)
177 formData.append('videofile', videofile) 179 formData.append('videofile', videofile)
178 180
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 @@
3 <div id="video-element-wrapper"> 3 <div id="video-element-wrapper">
4 </div> 4 </div>
5 5
6 <div i18n id="warning-transcoding" class="alert alert-warning" *ngIf="isVideoToTranscode()">
7 The video is being transcoded, it may not work properly.
8 </div>
9
6 <!-- Video information --> 10 <!-- Video information -->
7 <div *ngIf="video" class="margin-content video-bottom"> 11 <div *ngIf="video" class="margin-content video-bottom">
8 <div class="video-info"> 12 <div class="video-info">
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 @@
28 } 28 }
29} 29}
30 30
31#warning-transcoding {
32 text-align: center;
33}
34
31#video-not-found { 35#video-not-found {
32 height: 300px; 36 height: 300px;
33 line-height: 300px; 37 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 @@
1import { catchError } from 'rxjs/operators' 1import { catchError } from 'rxjs/operators'
2import { Component, ElementRef, LOCALE_ID, NgZone, OnDestroy, OnInit, ViewChild, Inject } from '@angular/core' 2import { Component, ElementRef, Inject, LOCALE_ID, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core'
3import { ActivatedRoute, Router } from '@angular/router' 3import { ActivatedRoute, Router } from '@angular/router'
4import { RedirectService } from '@app/core/routing/redirect.service' 4import { RedirectService } from '@app/core/routing/redirect.service'
5import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage' 5import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage'
@@ -10,7 +10,7 @@ import { Subscription } from 'rxjs'
10import * as videojs from 'video.js' 10import * as videojs from 'video.js'
11import 'videojs-hotkeys' 11import 'videojs-hotkeys'
12import * as WebTorrent from 'webtorrent' 12import * as WebTorrent from 'webtorrent'
13import { UserVideoRateType, VideoRateType } from '../../../../../shared' 13import { UserVideoRateType, VideoRateType, VideoState } from '../../../../../shared'
14import '../../../assets/player/peertube-videojs-plugin' 14import '../../../assets/player/peertube-videojs-plugin'
15import { AuthService, ConfirmService } from '../../core' 15import { AuthService, ConfirmService } from '../../core'
16import { RestExtractor, VideoBlacklistService } from '../../shared' 16import { RestExtractor, VideoBlacklistService } from '../../shared'
@@ -21,7 +21,7 @@ import { MarkdownService } from '../shared'
21import { VideoDownloadComponent } from './modal/video-download.component' 21import { VideoDownloadComponent } from './modal/video-download.component'
22import { VideoReportComponent } from './modal/video-report.component' 22import { VideoReportComponent } from './modal/video-report.component'
23import { VideoShareComponent } from './modal/video-share.component' 23import { VideoShareComponent } from './modal/video-share.component'
24import { getVideojsOptions, loadLocale, addContextMenu } from '../../../assets/player/peertube-player' 24import { addContextMenu, getVideojsOptions, loadLocale } from '../../../assets/player/peertube-player'
25import { ServerService } from '@app/core' 25import { ServerService } from '@app/core'
26import { I18n } from '@ngx-translate/i18n-polyfill' 26import { I18n } from '@ngx-translate/i18n-polyfill'
27import { environment } from '../../../environments/environment' 27import { environment } from '../../../environments/environment'
@@ -91,21 +91,21 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
91 } 91 }
92 92
93 this.videoService.getVideos({ currentPage: 1, itemsPerPage: 5 }, '-createdAt') 93 this.videoService.getVideos({ currentPage: 1, itemsPerPage: 5 }, '-createdAt')
94 .subscribe( 94 .subscribe(
95 data => { 95 data => {
96 this.otherVideos = data.videos 96 this.otherVideos = data.videos
97 this.updateOtherVideosDisplayed() 97 this.updateOtherVideosDisplayed()
98 }, 98 },
99 99
100 err => console.error(err) 100 err => console.error(err)
101 ) 101 )
102 102
103 this.paramsSub = this.route.params.subscribe(routeParams => { 103 this.paramsSub = this.route.params.subscribe(routeParams => {
104 if (this.player) { 104 if (this.player) {
105 this.player.pause() 105 this.player.pause()
106 } 106 }
107 107
108 const uuid = routeParams['uuid'] 108 const uuid = routeParams[ 'uuid' ]
109 109
110 // Video did not change 110 // Video did not change
111 if (this.video && this.video.uuid === uuid) return 111 if (this.video && this.video.uuid === uuid) return
@@ -113,13 +113,11 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
113 this.videoService 113 this.videoService
114 .getVideo(uuid) 114 .getVideo(uuid)
115 .pipe(catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 404 ]))) 115 .pipe(catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 404 ])))
116 .subscribe( 116 .subscribe(video => {
117 video => { 117 const startTime = this.route.snapshot.queryParams.start
118 const startTime = this.route.snapshot.queryParams.start 118 this.onVideoFetched(video, startTime)
119 this.onVideoFetched(video, startTime) 119 .catch(err => this.handleError(err))
120 .catch(err => this.handleError(err)) 120 })
121 }
122 )
123 }) 121 })
124 } 122 }
125 123
@@ -157,17 +155,17 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
157 if (res === false) return 155 if (res === false) return
158 156
159 this.videoBlacklistService.blacklistVideo(this.video.id) 157 this.videoBlacklistService.blacklistVideo(this.video.id)
160 .subscribe( 158 .subscribe(
161 status => { 159 () => {
162 this.notificationsService.success( 160 this.notificationsService.success(
163 this.i18n('Success'), 161 this.i18n('Success'),
164 this.i18n('Video {{videoName}} had been blacklisted.', { videoName: this.video.name }) 162 this.i18n('Video {{videoName}} had been blacklisted.', { videoName: this.video.name })
165 ) 163 )
166 this.redirectService.redirectToHomepage() 164 this.redirectService.redirectToHomepage()
167 }, 165 },
168 166
169 error => this.notificationsService.error(this.i18n('Error'), error.message) 167 error => this.notificationsService.error(this.i18n('Error'), error.message)
170 ) 168 )
171 } 169 }
172 170
173 showMoreDescription () { 171 showMoreDescription () {
@@ -188,22 +186,22 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
188 this.descriptionLoading = true 186 this.descriptionLoading = true
189 187
190 this.videoService.loadCompleteDescription(this.video.descriptionPath) 188 this.videoService.loadCompleteDescription(this.video.descriptionPath)
191 .subscribe( 189 .subscribe(
192 description => { 190 description => {
193 this.completeDescriptionShown = true 191 this.completeDescriptionShown = true
194 this.descriptionLoading = false 192 this.descriptionLoading = false
195 193
196 this.shortVideoDescription = this.video.description 194 this.shortVideoDescription = this.video.description
197 this.completeVideoDescription = description 195 this.completeVideoDescription = description
198 196
199 this.updateVideoDescription(this.completeVideoDescription) 197 this.updateVideoDescription(this.completeVideoDescription)
200 }, 198 },
201 199
202 error => { 200 error => {
203 this.descriptionLoading = false 201 this.descriptionLoading = false
204 this.notificationsService.error(this.i18n('Error'), error.message) 202 this.notificationsService.error(this.i18n('Error'), error.message)
205 } 203 }
206 ) 204 )
207 } 205 }
208 206
209 showReportModal (event: Event) { 207 showReportModal (event: Event) {
@@ -259,19 +257,19 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
259 if (res === false) return 257 if (res === false) return
260 258
261 this.videoService.removeVideo(this.video.id) 259 this.videoService.removeVideo(this.video.id)
262 .subscribe( 260 .subscribe(
263 status => { 261 status => {
264 this.notificationsService.success( 262 this.notificationsService.success(
265 this.i18n('Success'), 263 this.i18n('Success'),
266 this.i18n('Video {{videoName}} deleted.', { videoName: this.video.name }) 264 this.i18n('Video {{videoName}} deleted.', { videoName: this.video.name })
267 ) 265 )
268 266
269 // Go back to the video-list. 267 // Go back to the video-list.
270 this.redirectService.redirectToHomepage() 268 this.redirectService.redirectToHomepage()
271 }, 269 },
272 270
273 error => this.notificationsService.error(this.i18n('Error'), error.message) 271 error => this.notificationsService.error(this.i18n('Error'), error.message)
274 ) 272 )
275 } 273 }
276 274
277 acceptedPrivacyConcern () { 275 acceptedPrivacyConcern () {
@@ -279,6 +277,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
279 this.hasAlreadyAcceptedPrivacyConcern = true 277 this.hasAlreadyAcceptedPrivacyConcern = true
280 } 278 }
281 279
280 isVideoToTranscode () {
281 return this.video && this.video.state.id === VideoState.TO_TRANSCODE
282 }
283
282 private updateVideoDescription (description: string) { 284 private updateVideoDescription (description: string) {
283 this.video.description = description 285 this.video.description = description
284 this.setVideoDescriptionHTML() 286 this.setVideoDescriptionHTML()
@@ -294,10 +296,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
294 } 296 }
295 297
296 private setVideoLikesBarTooltipText () { 298 private setVideoLikesBarTooltipText () {
297 this.likesBarTooltipText = this.i18n( 299 this.likesBarTooltipText = this.i18n('{{likesNumber}} likes / {{dislikesNumber}} dislikes', {
298 '{{likesNumber}} likes / {{dislikesNumber}} dislikes', 300 likesNumber: this.video.likes,
299 { likesNumber: this.video.likes, dislikesNumber: this.video.dislikes } 301 dislikesNumber: this.video.dislikes
300 ) 302 })
301 } 303 }
302 304
303 private handleError (err: any) { 305 private handleError (err: any) {
@@ -320,15 +322,15 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
320 if (this.isUserLoggedIn() === false) return 322 if (this.isUserLoggedIn() === false) return
321 323
322 this.videoService.getUserVideoRating(this.video.id) 324 this.videoService.getUserVideoRating(this.video.id)
323 .subscribe( 325 .subscribe(
324 ratingObject => { 326 ratingObject => {
325 if (ratingObject) { 327 if (ratingObject) {
326 this.userRating = ratingObject.rating 328 this.userRating = ratingObject.rating
327 } 329 }
328 }, 330 },
329 331
330 err => this.notificationsService.error(this.i18n('Error'), err.message) 332 err => this.notificationsService.error(this.i18n('Error'), err.message)
331 ) 333 )
332 } 334 }
333 335
334 private async onVideoFetched (video: VideoDetails, startTime = 0) { 336 private async onVideoFetched (video: VideoDetails, startTime = 0) {
@@ -409,14 +411,15 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
409 } 411 }
410 412
411 method.call(this.videoService, this.video.id) 413 method.call(this.videoService, this.video.id)
412 .subscribe( 414 .subscribe(
413 () => { 415 () => {
414 // Update the video like attribute 416 // Update the video like attribute
415 this.updateVideoRating(this.userRating, nextRating) 417 this.updateVideoRating(this.userRating, nextRating)
416 this.userRating = nextRating 418 this.userRating = nextRating
417 }, 419 },
418 err => this.notificationsService.error(this.i18n('Error'), err.message) 420
419 ) 421 err => this.notificationsService.error(this.i18n('Error'), err.message)
422 )
420 } 423 }
421 424
422 private updateVideoRating (oldRating: UserVideoRateType, newRating: VideoRateType) { 425 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 @@
68 } 68 }
69 }, 69 },
70 "lint-staged": { 70 "lint-staged": {
71 "*.{css,md}": "precise-commits",
72 "*.scss": [ 71 "*.scss": [
73 "sass-lint -c .sass-lint.yml", 72 "sass-lint -c .sass-lint.yml",
74 "git add" 73 "git add"
@@ -166,7 +165,6 @@
166 "maildev": "^1.0.0-rc3", 165 "maildev": "^1.0.0-rc3",
167 "mocha": "^5.0.0", 166 "mocha": "^5.0.0",
168 "nodemon": "^1.11.0", 167 "nodemon": "^1.11.0",
169 "precise-commits": "^1.0.2",
170 "prettier": "1.13.2", 168 "prettier": "1.13.2",
171 "prompt": "^1.0.0", 169 "prompt": "^1.0.0",
172 "sass-lint": "^1.12.1", 170 "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
123async function videoController (req: express.Request, res: express.Response, next: express.NextFunction) { 123async function videoController (req: express.Request, res: express.Response, next: express.NextFunction) {
124 const video: VideoModel = res.locals.video 124 const video: VideoModel = res.locals.video
125 125
126 const audience = await getAudience(video.VideoChannel.Account.Actor, undefined, video.privacy === VideoPrivacy.PUBLIC) 126 const audience = getAudience(video.VideoChannel.Account.Actor, video.privacy === VideoPrivacy.PUBLIC)
127 const videoObject = audiencify(video.toActivityPubObject(), audience) 127 const videoObject = audiencify(video.toActivityPubObject(), audience)
128 128
129 if (req.path.endsWith('/activity')) { 129 if (req.path.endsWith('/activity')) {
130 const data = await createActivityData(video.url, video.VideoChannel.Account.Actor, videoObject, undefined, audience) 130 const data = createActivityData(video.url, video.VideoChannel.Account.Actor, videoObject, audience)
131 return activityPubResponse(activityPubContextify(data), res) 131 return activityPubResponse(activityPubContextify(data), res)
132 } 132 }
133 133
@@ -210,12 +210,12 @@ async function videoCommentController (req: express.Request, res: express.Respon
210 210
211 const threadParentComments = await VideoCommentModel.listThreadParentComments(videoComment, undefined) 211 const threadParentComments = await VideoCommentModel.listThreadParentComments(videoComment, undefined)
212 const isPublic = true // Comments are always public 212 const isPublic = true // Comments are always public
213 const audience = await getAudience(videoComment.Account.Actor, undefined, isPublic) 213 const audience = getAudience(videoComment.Account.Actor, isPublic)
214 214
215 const videoCommentObject = audiencify(videoComment.toActivityPubObject(threadParentComments), audience) 215 const videoCommentObject = audiencify(videoComment.toActivityPubObject(threadParentComments), audience)
216 216
217 if (req.path.endsWith('/activity')) { 217 if (req.path.endsWith('/activity')) {
218 const data = await createActivityData(videoComment.url, videoComment.Account.Actor, videoCommentObject, undefined, audience) 218 const data = createActivityData(videoComment.url, videoComment.Account.Actor, videoCommentObject, audience)
219 return activityPubResponse(activityPubContextify(data), res) 219 return activityPubResponse(activityPubContextify(data), res)
220 } 220 }
221 221
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)
54 // This is a shared video 54 // This is a shared video
55 if (video.VideoShares !== undefined && video.VideoShares.length !== 0) { 55 if (video.VideoShares !== undefined && video.VideoShares.length !== 0) {
56 const videoShare = video.VideoShares[0] 56 const videoShare = video.VideoShares[0]
57 const announceActivity = await announceActivityData(videoShare.url, actor, video.url, undefined, createActivityAudience) 57 const announceActivity = announceActivityData(videoShare.url, actor, video.url, createActivityAudience)
58 58
59 activities.push(announceActivity) 59 activities.push(announceActivity)
60 } else { 60 } else {
61 const videoObject = video.toActivityPubObject() 61 const videoObject = video.toActivityPubObject()
62 const createActivity = await createActivityData(video.url, byActor, videoObject, undefined, createActivityAudience) 62 const createActivity = createActivityData(video.url, byActor, videoObject, createActivityAudience)
63 63
64 activities.push(createActivity) 64 activities.push(createActivity)
65 } 65 }
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 {
166 166
167async function getUserVideos (req: express.Request, res: express.Response, next: express.NextFunction) { 167async function getUserVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
168 const user = res.locals.oauth.token.User as UserModel 168 const user = res.locals.oauth.token.User as UserModel
169 const resultList = await VideoModel.listAccountVideosForApi( 169 const resultList = await VideoModel.listUserVideosForApi(
170 user.Account.id, 170 user.Account.id,
171 req.query.start as number, 171 req.query.start as number,
172 req.query.count as number, 172 req.query.count as number,
@@ -174,7 +174,8 @@ async function getUserVideos (req: express.Request, res: express.Response, next:
174 false // Display my NSFW videos 174 false // Display my NSFW videos
175 ) 175 )
176 176
177 return res.json(getFormattedObjects(resultList.data, resultList.total)) 177 const additionalAttributes = { waitTranscoding: true, state: true }
178 return res.json(getFormattedObjects(resultList.data, resultList.total, { additionalAttributes }))
178} 179}
179 180
180async function createUserRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) { 181async 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
318} 319}
319 320
320async function updateMyAvatar (req: express.Request, res: express.Response, next: express.NextFunction) { 321async function updateMyAvatar (req: express.Request, res: express.Response, next: express.NextFunction) {
321 const avatarPhysicalFile = req.files['avatarfile'][0] 322 const avatarPhysicalFile = req.files[ 'avatarfile' ][ 0 ]
322 const user = res.locals.oauth.token.user 323 const user = res.locals.oauth.token.user
323 const actor = user.Account.Actor 324 const actor = user.Account.Actor
324 325
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 @@
1import * as express from 'express' 1import * as express from 'express'
2import { extname, join } from 'path' 2import { extname, join } from 'path'
3import { VideoCreate, VideoPrivacy, VideoUpdate } from '../../../../shared' 3import { VideoCreate, VideoPrivacy, VideoState, VideoUpdate } from '../../../../shared'
4import { renamePromise } from '../../../helpers/core-utils' 4import { renamePromise } from '../../../helpers/core-utils'
5import { retryTransactionWrapper } from '../../../helpers/database-utils' 5import { retryTransactionWrapper } from '../../../helpers/database-utils'
6import { getVideoFileResolution } from '../../../helpers/ffmpeg-utils' 6import { getVideoFileResolution } from '../../../helpers/ffmpeg-utils'
@@ -21,11 +21,11 @@ import {
21} from '../../../initializers' 21} from '../../../initializers'
22import { 22import {
23 changeVideoChannelShare, 23 changeVideoChannelShare,
24 federateVideoIfNeeded,
24 fetchRemoteVideoDescription, 25 fetchRemoteVideoDescription,
25 getVideoActivityPubUrl, 26 getVideoActivityPubUrl
26 shareVideoByServerAndChannel
27} from '../../../lib/activitypub' 27} from '../../../lib/activitypub'
28import { sendCreateVideo, sendCreateView, sendUpdateVideo } from '../../../lib/activitypub/send' 28import { sendCreateView } from '../../../lib/activitypub/send'
29import { JobQueue } from '../../../lib/job-queue' 29import { JobQueue } from '../../../lib/job-queue'
30import { Redis } from '../../../lib/redis' 30import { Redis } from '../../../lib/redis'
31import { 31import {
@@ -51,7 +51,7 @@ import { videoCommentRouter } from './comment'
51import { rateVideoRouter } from './rate' 51import { rateVideoRouter } from './rate'
52import { VideoFilter } from '../../../../shared/models/videos/video-query.type' 52import { VideoFilter } from '../../../../shared/models/videos/video-query.type'
53import { VideoSortField } from '../../../../client/src/app/shared/video/sort-field.type' 53import { VideoSortField } from '../../../../client/src/app/shared/video/sort-field.type'
54import { isNSFWHidden, createReqFiles } from '../../../helpers/express-utils' 54import { createReqFiles, isNSFWHidden } from '../../../helpers/express-utils'
55 55
56const videosRouter = express.Router() 56const videosRouter = express.Router()
57 57
@@ -185,8 +185,10 @@ async function addVideo (req: express.Request, res: express.Response, videoPhysi
185 category: videoInfo.category, 185 category: videoInfo.category,
186 licence: videoInfo.licence, 186 licence: videoInfo.licence,
187 language: videoInfo.language, 187 language: videoInfo.language,
188 commentsEnabled: videoInfo.commentsEnabled, 188 commentsEnabled: videoInfo.commentsEnabled || false,
189 nsfw: videoInfo.nsfw, 189 waitTranscoding: videoInfo.waitTranscoding || false,
190 state: CONFIG.TRANSCODING.ENABLED ? VideoState.TO_TRANSCODE : VideoState.PUBLISHED,
191 nsfw: videoInfo.nsfw || false,
190 description: videoInfo.description, 192 description: videoInfo.description,
191 support: videoInfo.support, 193 support: videoInfo.support,
192 privacy: videoInfo.privacy, 194 privacy: videoInfo.privacy,
@@ -194,19 +196,20 @@ async function addVideo (req: express.Request, res: express.Response, videoPhysi
194 channelId: res.locals.videoChannel.id 196 channelId: res.locals.videoChannel.id
195 } 197 }
196 const video = new VideoModel(videoData) 198 const video = new VideoModel(videoData)
197 video.url = getVideoActivityPubUrl(video) 199 video.url = getVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object
198 200
201 // Build the file object
199 const { videoFileResolution } = await getVideoFileResolution(videoPhysicalFile.path) 202 const { videoFileResolution } = await getVideoFileResolution(videoPhysicalFile.path)
200
201 const videoFileData = { 203 const videoFileData = {
202 extname: extname(videoPhysicalFile.filename), 204 extname: extname(videoPhysicalFile.filename),
203 resolution: videoFileResolution, 205 resolution: videoFileResolution,
204 size: videoPhysicalFile.size 206 size: videoPhysicalFile.size
205 } 207 }
206 const videoFile = new VideoFileModel(videoFileData) 208 const videoFile = new VideoFileModel(videoFileData)
209
210 // Move physical file
207 const videoDir = CONFIG.STORAGE.VIDEOS_DIR 211 const videoDir = CONFIG.STORAGE.VIDEOS_DIR
208 const destination = join(videoDir, video.getVideoFilename(videoFile)) 212 const destination = join(videoDir, video.getVideoFilename(videoFile))
209
210 await renamePromise(videoPhysicalFile.path, destination) 213 await renamePromise(videoPhysicalFile.path, destination)
211 // This is important in case if there is another attempt in the retry process 214 // This is important in case if there is another attempt in the retry process
212 videoPhysicalFile.filename = video.getVideoFilename(videoFile) 215 videoPhysicalFile.filename = video.getVideoFilename(videoFile)
@@ -230,6 +233,7 @@ async function addVideo (req: express.Request, res: express.Response, videoPhysi
230 await video.createPreview(videoFile) 233 await video.createPreview(videoFile)
231 } 234 }
232 235
236 // Create the torrent file
233 await video.createTorrentAndSetInfoHash(videoFile) 237 await video.createTorrentAndSetInfoHash(videoFile)
234 238
235 const videoCreated = await sequelizeTypescript.transaction(async t => { 239 const videoCreated = await sequelizeTypescript.transaction(async t => {
@@ -251,20 +255,14 @@ async function addVideo (req: express.Request, res: express.Response, videoPhysi
251 video.Tags = tagInstances 255 video.Tags = tagInstances
252 } 256 }
253 257
254 // Let transcoding job send the video to friends because the video file extension might change 258 await federateVideoIfNeeded(video, true, t)
255 if (CONFIG.TRANSCODING.ENABLED === true) return videoCreated
256 // Don't send video to remote servers, it is private
257 if (video.privacy === VideoPrivacy.PRIVATE) return videoCreated
258
259 await sendCreateVideo(video, t)
260 await shareVideoByServerAndChannel(video, t)
261 259
262 logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid) 260 logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid)
263 261
264 return videoCreated 262 return videoCreated
265 }) 263 })
266 264
267 if (CONFIG.TRANSCODING.ENABLED === true) { 265 if (video.state === VideoState.TO_TRANSCODE) {
268 // Put uuid because we don't have id auto incremented for now 266 // Put uuid because we don't have id auto incremented for now
269 const dataInput = { 267 const dataInput = {
270 videoUUID: videoCreated.uuid, 268 videoUUID: videoCreated.uuid,
@@ -318,6 +316,7 @@ async function updateVideo (req: express.Request, res: express.Response) {
318 if (videoInfoToUpdate.licence !== undefined) videoInstance.set('licence', videoInfoToUpdate.licence) 316 if (videoInfoToUpdate.licence !== undefined) videoInstance.set('licence', videoInfoToUpdate.licence)
319 if (videoInfoToUpdate.language !== undefined) videoInstance.set('language', videoInfoToUpdate.language) 317 if (videoInfoToUpdate.language !== undefined) videoInstance.set('language', videoInfoToUpdate.language)
320 if (videoInfoToUpdate.nsfw !== undefined) videoInstance.set('nsfw', videoInfoToUpdate.nsfw) 318 if (videoInfoToUpdate.nsfw !== undefined) videoInstance.set('nsfw', videoInfoToUpdate.nsfw)
319 if (videoInfoToUpdate.waitTranscoding !== undefined) videoInstance.set('waitTranscoding', videoInfoToUpdate.waitTranscoding)
321 if (videoInfoToUpdate.support !== undefined) videoInstance.set('support', videoInfoToUpdate.support) 320 if (videoInfoToUpdate.support !== undefined) videoInstance.set('support', videoInfoToUpdate.support)
322 if (videoInfoToUpdate.description !== undefined) videoInstance.set('description', videoInfoToUpdate.description) 321 if (videoInfoToUpdate.description !== undefined) videoInstance.set('description', videoInfoToUpdate.description)
323 if (videoInfoToUpdate.commentsEnabled !== undefined) videoInstance.set('commentsEnabled', videoInfoToUpdate.commentsEnabled) 322 if (videoInfoToUpdate.commentsEnabled !== undefined) videoInstance.set('commentsEnabled', videoInfoToUpdate.commentsEnabled)
@@ -343,19 +342,13 @@ async function updateVideo (req: express.Request, res: express.Response) {
343 // Video channel update? 342 // Video channel update?
344 if (res.locals.videoChannel && videoInstanceUpdated.channelId !== res.locals.videoChannel.id) { 343 if (res.locals.videoChannel && videoInstanceUpdated.channelId !== res.locals.videoChannel.id) {
345 await videoInstanceUpdated.$set('VideoChannel', res.locals.videoChannel, { transaction: t }) 344 await videoInstanceUpdated.$set('VideoChannel', res.locals.videoChannel, { transaction: t })
346 videoInstance.VideoChannel = res.locals.videoChannel 345 videoInstanceUpdated.VideoChannel = res.locals.videoChannel
347 346
348 if (wasPrivateVideo === false) await changeVideoChannelShare(videoInstanceUpdated, oldVideoChannel, t) 347 if (wasPrivateVideo === false) await changeVideoChannelShare(videoInstanceUpdated, oldVideoChannel, t)
349 } 348 }
350 349
351 // Now we'll update the video's meta data to our friends 350 const isNewVideo = wasPrivateVideo && videoInstanceUpdated.privacy !== VideoPrivacy.PRIVATE
352 if (wasPrivateVideo === false) await sendUpdateVideo(videoInstanceUpdated, t) 351 await federateVideoIfNeeded(videoInstanceUpdated, isNewVideo)
353
354 // Video is not private anymore, send a create action to remote servers
355 if (wasPrivateVideo === true && videoInstanceUpdated.privacy !== VideoPrivacy.PRIVATE) {
356 await sendCreateVideo(videoInstanceUpdated, t)
357 await shareVideoByServerAndChannel(videoInstanceUpdated, t)
358 }
359 }) 352 })
360 353
361 logger.info('Video with name %s and uuid %s updated.', videoInstance.name, videoInstance.uuid) 354 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'
8import { pageToStartAndCount } from './core-utils' 8import { pageToStartAndCount } from './core-utils'
9 9
10function activityPubContextify <T> (data: T) { 10function activityPubContextify <T> (data: T) {
11 return Object.assign(data,{ 11 return Object.assign(data, {
12 '@context': [ 12 '@context': [
13 'https://www.w3.org/ns/activitystreams', 13 'https://www.w3.org/ns/activitystreams',
14 'https://w3id.org/security/v1', 14 'https://w3id.org/security/v1',
15 { 15 {
16 'RsaSignature2017': 'https://w3id.org/security#RsaSignature2017', 16 RsaSignature2017: 'https://w3id.org/security#RsaSignature2017',
17 'Hashtag': 'as:Hashtag', 17 Hashtag: 'as:Hashtag',
18 'uuid': 'http://schema.org/identifier', 18 uuid: 'http://schema.org/identifier',
19 'category': 'http://schema.org/category', 19 category: 'http://schema.org/category',
20 'licence': 'http://schema.org/license', 20 licence: 'http://schema.org/license',
21 'sensitive': 'as:sensitive', 21 sensitive: 'as:sensitive',
22 'language': 'http://schema.org/inLanguage', 22 language: 'http://schema.org/inLanguage',
23 'views': 'http://schema.org/Number', 23 views: 'http://schema.org/Number',
24 'size': 'http://schema.org/Number', 24 stats: 'http://schema.org/Number',
25 'commentsEnabled': 'http://schema.org/Boolean', 25 size: 'http://schema.org/Number',
26 'support': 'http://schema.org/Text' 26 commentsEnabled: 'http://schema.org/Boolean',
27 waitTranscoding: 'http://schema.org/Boolean',
28 support: 'http://schema.org/Text'
27 }, 29 },
28 { 30 {
29 likes: { 31 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 {
6 isVideoAbuseReasonValid, 6 isVideoAbuseReasonValid,
7 isVideoDurationValid, 7 isVideoDurationValid,
8 isVideoNameValid, 8 isVideoNameValid,
9 isVideoStateValid,
9 isVideoTagValid, 10 isVideoTagValid,
10 isVideoTruncatedDescriptionValid, 11 isVideoTruncatedDescriptionValid,
11 isVideoViewsValid 12 isVideoViewsValid
12} from '../videos' 13} from '../videos'
13import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc' 14import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc'
15import { VideoState } from '../../../../shared/models/videos'
14 16
15function sanitizeAndCheckVideoTorrentCreateActivity (activity: any) { 17function sanitizeAndCheckVideoTorrentCreateActivity (activity: any) {
16 return isBaseActivityValid(activity, 'Create') && 18 return isBaseActivityValid(activity, 'Create') &&
@@ -50,6 +52,10 @@ function sanitizeAndCheckVideoTorrentObject (video: any) {
50 if (!setRemoteVideoTruncatedContent(video)) return false 52 if (!setRemoteVideoTruncatedContent(video)) return false
51 if (!setValidAttributedTo(video)) return false 53 if (!setValidAttributedTo(video)) return false
52 54
55 // Default attributes
56 if (!isVideoStateValid(video.state)) video.state = VideoState.PUBLISHED
57 if (!isBooleanValid(video.waitTranscoding)) video.waitTranscoding = false
58
53 return isActivityPubUrlValid(video.id) && 59 return isActivityPubUrlValid(video.id) &&
54 isVideoNameValid(video.name) && 60 isVideoNameValid(video.name) &&
55 isActivityPubVideoDurationValid(video.duration) && 61 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 {
10 VIDEO_LICENCES, 10 VIDEO_LICENCES,
11 VIDEO_MIMETYPE_EXT, 11 VIDEO_MIMETYPE_EXT,
12 VIDEO_PRIVACIES, 12 VIDEO_PRIVACIES,
13 VIDEO_RATE_TYPES 13 VIDEO_RATE_TYPES,
14 VIDEO_STATES
14} from '../../initializers' 15} from '../../initializers'
15import { VideoModel } from '../../models/video/video' 16import { VideoModel } from '../../models/video/video'
16import { exists, isArray, isFileValid } from './misc' 17import { exists, isArray, isFileValid } from './misc'
@@ -21,11 +22,15 @@ const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS
21const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES 22const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES
22 23
23function isVideoCategoryValid (value: any) { 24function isVideoCategoryValid (value: any) {
24 return value === null || VIDEO_CATEGORIES[value] !== undefined 25 return value === null || VIDEO_CATEGORIES[ value ] !== undefined
26}
27
28function isVideoStateValid (value: any) {
29 return exists(value) && VIDEO_STATES[ value ] !== undefined
25} 30}
26 31
27function isVideoLicenceValid (value: any) { 32function isVideoLicenceValid (value: any) {
28 return value === null || VIDEO_LICENCES[value] !== undefined 33 return value === null || VIDEO_LICENCES[ value ] !== undefined
29} 34}
30 35
31function isVideoLanguageValid (value: any) { 36function isVideoLanguageValid (value: any) {
@@ -79,20 +84,22 @@ function isVideoRatingTypeValid (value: string) {
79 84
80const videoFileTypes = Object.keys(VIDEO_MIMETYPE_EXT).map(m => `(${m})`) 85const videoFileTypes = Object.keys(VIDEO_MIMETYPE_EXT).map(m => `(${m})`)
81const videoFileTypesRegex = videoFileTypes.join('|') 86const videoFileTypesRegex = videoFileTypes.join('|')
87
82function isVideoFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) { 88function isVideoFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) {
83 return isFileValid(files, videoFileTypesRegex, 'videofile') 89 return isFileValid(files, videoFileTypesRegex, 'videofile')
84} 90}
85 91
86const videoImageTypes = CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME 92const videoImageTypes = CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME
87 .map(v => v.replace('.', '')) 93 .map(v => v.replace('.', ''))
88 .join('|') 94 .join('|')
89const videoImageTypesRegex = `image/(${videoImageTypes})` 95const videoImageTypesRegex = `image/(${videoImageTypes})`
96
90function isVideoImage (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[], field: string) { 97function isVideoImage (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[], field: string) {
91 return isFileValid(files, videoImageTypesRegex, field, true) 98 return isFileValid(files, videoImageTypesRegex, field, true)
92} 99}
93 100
94function isVideoPrivacyValid (value: string) { 101function isVideoPrivacyValid (value: string) {
95 return validator.isInt(value + '') && VIDEO_PRIVACIES[value] !== undefined 102 return validator.isInt(value + '') && VIDEO_PRIVACIES[ value ] !== undefined
96} 103}
97 104
98function isVideoFileInfoHashValid (value: string) { 105function isVideoFileInfoHashValid (value: string) {
@@ -118,8 +125,8 @@ async function isVideoExist (id: string, res: Response) {
118 125
119 if (!video) { 126 if (!video) {
120 res.status(404) 127 res.status(404)
121 .json({ error: 'Video not found' }) 128 .json({ error: 'Video not found' })
122 .end() 129 .end()
123 130
124 return false 131 return false
125 } 132 }
@@ -169,6 +176,7 @@ export {
169 isVideoTagsValid, 176 isVideoTagsValid,
170 isVideoAbuseReasonValid, 177 isVideoAbuseReasonValid,
171 isVideoFile, 178 isVideoFile,
179 isVideoStateValid,
172 isVideoViewsValid, 180 isVideoViewsValid,
173 isVideoRatingTypeValid, 181 isVideoRatingTypeValid,
174 isVideoDurationValid, 182 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 @@
1import { Model } from 'sequelize-typescript' 1import { Model } from 'sequelize-typescript'
2import * as ipaddr from 'ipaddr.js' 2import * as ipaddr from 'ipaddr.js'
3const isCidr = require('is-cidr')
4import { ResultList } from '../../shared' 3import { ResultList } from '../../shared'
5import { VideoResolution } from '../../shared/models/videos' 4import { VideoResolution } from '../../shared/models/videos'
6import { CONFIG } from '../initializers' 5import { CONFIG } from '../initializers'
@@ -10,6 +9,8 @@ import { ApplicationModel } from '../models/application/application'
10import { pseudoRandomBytesPromise } from './core-utils' 9import { pseudoRandomBytesPromise } from './core-utils'
11import { logger } from './logger' 10import { logger } from './logger'
12 11
12const isCidr = require('is-cidr')
13
13async function generateRandomString (size: number) { 14async function generateRandomString (size: number) {
14 const raw = await pseudoRandomBytesPromise(size) 15 const raw = await pseudoRandomBytesPromise(size)
15 16
@@ -17,22 +18,20 @@ async function generateRandomString (size: number) {
17} 18}
18 19
19interface FormattableToJSON { 20interface FormattableToJSON {
20 toFormattedJSON () 21 toFormattedJSON (args?: any)
21} 22}
22 23
23function getFormattedObjects<U, T extends FormattableToJSON> (objects: T[], objectsTotal: number) { 24function getFormattedObjects<U, T extends FormattableToJSON> (objects: T[], objectsTotal: number, formattedArg?: any) {
24 const formattedObjects: U[] = [] 25 const formattedObjects: U[] = []
25 26
26 objects.forEach(object => { 27 objects.forEach(object => {
27 formattedObjects.push(object.toFormattedJSON()) 28 formattedObjects.push(object.toFormattedJSON(formattedArg))
28 }) 29 })
29 30
30 const res: ResultList<U> = { 31 return {
31 total: objectsTotal, 32 total: objectsTotal,
32 data: formattedObjects 33 data: formattedObjects
33 } 34 } as ResultList<U>
34
35 return res
36} 35}
37 36
38async function isSignupAllowed () { 37async function isSignupAllowed () {
@@ -87,16 +86,17 @@ function computeResolutionsToTranscode (videoFileHeight: number) {
87 const resolutionsEnabled: number[] = [] 86 const resolutionsEnabled: number[] = []
88 const configResolutions = CONFIG.TRANSCODING.RESOLUTIONS 87 const configResolutions = CONFIG.TRANSCODING.RESOLUTIONS
89 88
89 // Put in the order we want to proceed jobs
90 const resolutions = [ 90 const resolutions = [
91 VideoResolution.H_240P,
92 VideoResolution.H_360P,
93 VideoResolution.H_480P, 91 VideoResolution.H_480P,
92 VideoResolution.H_360P,
94 VideoResolution.H_720P, 93 VideoResolution.H_720P,
94 VideoResolution.H_240P,
95 VideoResolution.H_1080P 95 VideoResolution.H_1080P
96 ] 96 ]
97 97
98 for (const resolution of resolutions) { 98 for (const resolution of resolutions) {
99 if (configResolutions[resolution + 'p'] === true && videoFileHeight > resolution) { 99 if (configResolutions[ resolution + 'p' ] === true && videoFileHeight > resolution) {
100 resolutionsEnabled.push(resolution) 100 resolutionsEnabled.push(resolution)
101 } 101 }
102 } 102 }
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 @@
1import { IConfig } from 'config' 1import { IConfig } from 'config'
2import { dirname, join } from 'path' 2import { dirname, join } from 'path'
3import { JobType, VideoRateType } from '../../shared/models' 3import { JobType, VideoRateType, VideoState } from '../../shared/models'
4import { ActivityPubActorType } from '../../shared/models/activitypub' 4import { ActivityPubActorType } from '../../shared/models/activitypub'
5import { FollowState } from '../../shared/models/actors' 5import { FollowState } from '../../shared/models/actors'
6import { VideoPrivacy } from '../../shared/models/videos' 6import { VideoPrivacy } from '../../shared/models/videos'
@@ -14,7 +14,7 @@ let config: IConfig = require('config')
14 14
15// --------------------------------------------------------------------------- 15// ---------------------------------------------------------------------------
16 16
17const LAST_MIGRATION_VERSION = 215 17const LAST_MIGRATION_VERSION = 220
18 18
19// --------------------------------------------------------------------------- 19// ---------------------------------------------------------------------------
20 20
@@ -326,6 +326,11 @@ const VIDEO_PRIVACIES = {
326 [VideoPrivacy.PRIVATE]: 'Private' 326 [VideoPrivacy.PRIVATE]: 'Private'
327} 327}
328 328
329const VIDEO_STATES = {
330 [VideoState.PUBLISHED]: 'Published',
331 [VideoState.TO_TRANSCODE]: 'To transcode'
332}
333
329const VIDEO_MIMETYPE_EXT = { 334const VIDEO_MIMETYPE_EXT = {
330 'video/webm': '.webm', 335 'video/webm': '.webm',
331 'video/ogg': '.ogv', 336 'video/ogg': '.ogv',
@@ -493,6 +498,7 @@ export {
493 VIDEO_LANGUAGES, 498 VIDEO_LANGUAGES,
494 VIDEO_PRIVACIES, 499 VIDEO_PRIVACIES,
495 VIDEO_LICENCES, 500 VIDEO_LICENCES,
501 VIDEO_STATES,
496 VIDEO_RATE_TYPES, 502 VIDEO_RATE_TYPES,
497 VIDEO_MIMETYPE_EXT, 503 VIDEO_MIMETYPE_EXT,
498 VIDEO_TRANSCODING_FPS, 504 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 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7}): Promise<void> {
8 // waitingTranscoding column
9 {
10 const data = {
11 type: Sequelize.BOOLEAN,
12 allowNull: true,
13 defaultValue: null
14 }
15 await utils.queryInterface.addColumn('video', 'waitTranscoding', data)
16 }
17
18 {
19 const query = 'UPDATE video SET "waitTranscoding" = false'
20 await utils.sequelize.query(query)
21 }
22
23 {
24 const data = {
25 type: Sequelize.BOOLEAN,
26 allowNull: false,
27 defaultValue: null
28 }
29 await utils.queryInterface.changeColumn('video', 'waitTranscoding', data)
30 }
31
32 // state
33 {
34 const data = {
35 type: Sequelize.INTEGER,
36 allowNull: true,
37 defaultValue: null
38 }
39 await utils.queryInterface.addColumn('video', 'state', data)
40 }
41
42 {
43 // Published
44 const query = 'UPDATE video SET "state" = 1'
45 await utils.sequelize.query(query)
46 }
47
48 {
49 const data = {
50 type: Sequelize.INTEGER,
51 allowNull: false,
52 defaultValue: null
53 }
54 await utils.queryInterface.changeColumn('video', 'state', data)
55 }
56}
57
58function down (options) {
59 throw new Error('Not implemented.')
60}
61
62export { 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 (
20 isOrigin = false 20 isOrigin = false
21) { 21) {
22 const to = [ ACTIVITY_PUB.PUBLIC ] 22 const to = [ ACTIVITY_PUB.PUBLIC ]
23 const cc = [ ] 23 const cc = []
24 24
25 // Owner of the video we comment 25 // Owner of the video we comment
26 if (isOrigin === false) { 26 if (isOrigin === false) {
@@ -55,7 +55,7 @@ async function getActorsInvolvedInVideo (video: VideoModel, t: Transaction) {
55 return actors 55 return actors
56} 56}
57 57
58async function getAudience (actorSender: ActorModel, t: Transaction, isPublic = true) { 58function getAudience (actorSender: ActorModel, isPublic = true) {
59 return buildAudience([ actorSender.followersUrl ], isPublic) 59 return buildAudience([ actorSender.followersUrl ], isPublic)
60} 60}
61 61
@@ -67,14 +67,14 @@ function buildAudience (followerUrls: string[], isPublic = true) {
67 to = [ ACTIVITY_PUB.PUBLIC ] 67 to = [ ACTIVITY_PUB.PUBLIC ]
68 cc = followerUrls 68 cc = followerUrls
69 } else { // Unlisted 69 } else { // Unlisted
70 to = [ ] 70 to = []
71 cc = [ ] 71 cc = []
72 } 72 }
73 73
74 return { to, cc } 74 return { to, cc }
75} 75}
76 76
77function audiencify <T> (object: T, audience: ActivityAudience) { 77function audiencify<T> (object: T, audience: ActivityAudience) {
78 return Object.assign(object, audience) 78 return Object.assign(object, audience)
79} 79}
80 80
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 <T> (uri: string, handler: (items: T[]) => Pr
28 28
29 if (Array.isArray(body.orderedItems)) { 29 if (Array.isArray(body.orderedItems)) {
30 const items = body.orderedItems 30 const items = body.orderedItems
31 logger.info('Processing %i ActivityPub items for %s.', items.length, nextLink) 31 logger.info('Processing %i ActivityPub items for %s.', items.length, options.uri)
32 32
33 await handler(items) 33 await handler(items)
34 } 34 }
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 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { ActivityUpdate } from '../../../../shared/models/activitypub' 2import { ActivityUpdate } from '../../../../shared/models/activitypub'
3import { ActivityPubActor } from '../../../../shared/models/activitypub/activitypub-actor' 3import { ActivityPubActor } from '../../../../shared/models/activitypub/activitypub-actor'
4import { VideoTorrentObject } from '../../../../shared/models/activitypub/objects'
5import { retryTransactionWrapper } from '../../../helpers/database-utils' 4import { retryTransactionWrapper } from '../../../helpers/database-utils'
6import { logger } from '../../../helpers/logger' 5import { logger } from '../../../helpers/logger'
7import { resetSequelizeInstance } from '../../../helpers/utils' 6import { resetSequelizeInstance } from '../../../helpers/utils'
@@ -13,6 +12,7 @@ import { VideoChannelModel } from '../../../models/video/video-channel'
13import { VideoFileModel } from '../../../models/video/video-file' 12import { VideoFileModel } from '../../../models/video/video-file'
14import { fetchAvatarIfExists, getOrCreateActorAndServerAndModel, updateActorAvatarInstance, updateActorInstance } from '../actor' 13import { fetchAvatarIfExists, getOrCreateActorAndServerAndModel, updateActorAvatarInstance, updateActorInstance } from '../actor'
15import { 14import {
15 fetchRemoteVideo,
16 generateThumbnailFromUrl, 16 generateThumbnailFromUrl,
17 getOrCreateAccountAndVideoAndChannel, 17 getOrCreateAccountAndVideoAndChannel,
18 getOrCreateVideoChannel, 18 getOrCreateVideoChannel,
@@ -51,15 +51,18 @@ function processUpdateVideo (actor: ActorModel, activity: ActivityUpdate) {
51} 51}
52 52
53async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) { 53async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) {
54 const videoAttributesToUpdate = activity.object as VideoTorrentObject 54 const videoUrl = activity.object.id
55 55
56 const res = await getOrCreateAccountAndVideoAndChannel(videoAttributesToUpdate.id) 56 const videoObject = await fetchRemoteVideo(videoUrl)
57 if (!videoObject) throw new Error('Cannot fetch remote video with url: ' + videoUrl)
58
59 const res = await getOrCreateAccountAndVideoAndChannel(videoObject.id)
57 60
58 // Fetch video channel outside the transaction 61 // Fetch video channel outside the transaction
59 const newVideoChannelActor = await getOrCreateVideoChannel(videoAttributesToUpdate) 62 const newVideoChannelActor = await getOrCreateVideoChannel(videoObject)
60 const newVideoChannel = newVideoChannelActor.VideoChannel 63 const newVideoChannel = newVideoChannelActor.VideoChannel
61 64
62 logger.debug('Updating remote video "%s".', videoAttributesToUpdate.uuid) 65 logger.debug('Updating remote video "%s".', videoObject.uuid)
63 let videoInstance = res.video 66 let videoInstance = res.video
64 let videoFieldsSave: any 67 let videoFieldsSave: any
65 68
@@ -77,7 +80,7 @@ async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) {
77 throw new Error('Account ' + actor.url + ' does not own video channel ' + videoChannel.Actor.url) 80 throw new Error('Account ' + actor.url + ' does not own video channel ' + videoChannel.Actor.url)
78 } 81 }
79 82
80 const videoData = await videoActivityObjectToDBAttributes(newVideoChannel, videoAttributesToUpdate, activity.to) 83 const videoData = await videoActivityObjectToDBAttributes(newVideoChannel, videoObject, activity.to)
81 videoInstance.set('name', videoData.name) 84 videoInstance.set('name', videoData.name)
82 videoInstance.set('uuid', videoData.uuid) 85 videoInstance.set('uuid', videoData.uuid)
83 videoInstance.set('url', videoData.url) 86 videoInstance.set('url', videoData.url)
@@ -88,6 +91,8 @@ async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) {
88 videoInstance.set('support', videoData.support) 91 videoInstance.set('support', videoData.support)
89 videoInstance.set('nsfw', videoData.nsfw) 92 videoInstance.set('nsfw', videoData.nsfw)
90 videoInstance.set('commentsEnabled', videoData.commentsEnabled) 93 videoInstance.set('commentsEnabled', videoData.commentsEnabled)
94 videoInstance.set('waitTranscoding', videoData.waitTranscoding)
95 videoInstance.set('state', videoData.state)
91 videoInstance.set('duration', videoData.duration) 96 videoInstance.set('duration', videoData.duration)
92 videoInstance.set('createdAt', videoData.createdAt) 97 videoInstance.set('createdAt', videoData.createdAt)
93 videoInstance.set('updatedAt', videoData.updatedAt) 98 videoInstance.set('updatedAt', videoData.updatedAt)
@@ -98,8 +103,8 @@ async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) {
98 await videoInstance.save(sequelizeOptions) 103 await videoInstance.save(sequelizeOptions)
99 104
100 // Don't block on request 105 // Don't block on request
101 generateThumbnailFromUrl(videoInstance, videoAttributesToUpdate.icon) 106 generateThumbnailFromUrl(videoInstance, videoObject.icon)
102 .catch(err => logger.warn('Cannot generate thumbnail of %s.', videoAttributesToUpdate.id, { err })) 107 .catch(err => logger.warn('Cannot generate thumbnail of %s.', videoObject.id, { err }))
103 108
104 // Remove old video files 109 // Remove old video files
105 const videoFileDestroyTasks: Bluebird<void>[] = [] 110 const videoFileDestroyTasks: Bluebird<void>[] = []
@@ -108,16 +113,16 @@ async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) {
108 } 113 }
109 await Promise.all(videoFileDestroyTasks) 114 await Promise.all(videoFileDestroyTasks)
110 115
111 const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoInstance, videoAttributesToUpdate) 116 const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoInstance, videoObject)
112 const tasks = videoFileAttributes.map(f => VideoFileModel.create(f)) 117 const tasks = videoFileAttributes.map(f => VideoFileModel.create(f))
113 await Promise.all(tasks) 118 await Promise.all(tasks)
114 119
115 const tags = videoAttributesToUpdate.tag.map(t => t.name) 120 const tags = videoObject.tag.map(t => t.name)
116 const tagInstances = await TagModel.findOrCreateTags(tags, t) 121 const tagInstances = await TagModel.findOrCreateTags(tags, t)
117 await videoInstance.$set('Tags', tagInstances, sequelizeOptions) 122 await videoInstance.$set('Tags', tagInstances, sequelizeOptions)
118 }) 123 })
119 124
120 logger.info('Remote video with uuid %s updated', videoAttributesToUpdate.uuid) 125 logger.info('Remote video with uuid %s updated', videoObject.uuid)
121 } catch (err) { 126 } catch (err) {
122 if (videoInstance !== undefined && videoFieldsSave !== undefined) { 127 if (videoInstance !== undefined && videoFieldsSave !== undefined) {
123 resetSequelizeInstance(videoInstance, videoFieldsSave) 128 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
11 11
12 const accountsToForwardView = await getActorsInvolvedInVideo(video, t) 12 const accountsToForwardView = await getActorsInvolvedInVideo(video, t)
13 const audience = getObjectFollowersAudience(accountsToForwardView) 13 const audience = getObjectFollowersAudience(accountsToForwardView)
14 return announceActivityData(videoShare.url, byActor, announcedObject, t, audience) 14 return announceActivityData(videoShare.url, byActor, announcedObject, audience)
15} 15}
16 16
17async function sendVideoAnnounce (byActor: ActorModel, videoShare: VideoShareModel, video: VideoModel, t: Transaction) { 17async function sendVideoAnnounce (byActor: ActorModel, videoShare: VideoShareModel, video: VideoModel, t: Transaction) {
@@ -20,16 +20,8 @@ async function sendVideoAnnounce (byActor: ActorModel, videoShare: VideoShareMod
20 return broadcastToFollowers(data, byActor, [ byActor ], t) 20 return broadcastToFollowers(data, byActor, [ byActor ], t)
21} 21}
22 22
23async function announceActivityData ( 23function announceActivityData (url: string, byActor: ActorModel, object: string, audience?: ActivityAudience): ActivityAnnounce {
24 url: string, 24 if (!audience) audience = getAudience(byActor)
25 byActor: ActorModel,
26 object: string,
27 t: Transaction,
28 audience?: ActivityAudience
29): Promise<ActivityAnnounce> {
30 if (!audience) {
31 audience = await getAudience(byActor, t)
32 }
33 25
34 return { 26 return {
35 type: 'Announce', 27 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) {
23 const byActor = video.VideoChannel.Account.Actor 23 const byActor = video.VideoChannel.Account.Actor
24 const videoObject = video.toActivityPubObject() 24 const videoObject = video.toActivityPubObject()
25 25
26 const audience = await getAudience(byActor, t, video.privacy === VideoPrivacy.PUBLIC) 26 const audience = getAudience(byActor, video.privacy === VideoPrivacy.PUBLIC)
27 const data = await createActivityData(video.url, byActor, videoObject, t, audience) 27 const data = createActivityData(video.url, byActor, videoObject, audience)
28 28
29 return broadcastToFollowers(data, byActor, [ byActor ], t) 29 return broadcastToFollowers(data, byActor, [ byActor ], t)
30} 30}
@@ -33,7 +33,7 @@ async function sendVideoAbuse (byActor: ActorModel, videoAbuse: VideoAbuseModel,
33 const url = getVideoAbuseActivityPubUrl(videoAbuse) 33 const url = getVideoAbuseActivityPubUrl(videoAbuse)
34 34
35 const audience = { to: [ video.VideoChannel.Account.Actor.url ], cc: [] } 35 const audience = { to: [ video.VideoChannel.Account.Actor.url ], cc: [] }
36 const data = await createActivityData(url, byActor, videoAbuse.toActivityPubObject(), t, audience) 36 const data = createActivityData(url, byActor, videoAbuse.toActivityPubObject(), audience)
37 37
38 return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl) 38 return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl)
39} 39}
@@ -57,7 +57,7 @@ async function sendCreateVideoComment (comment: VideoCommentModel, t: Transactio
57 audience = getObjectFollowersAudience(actorsInvolvedInComment.concat(parentsCommentActors)) 57 audience = getObjectFollowersAudience(actorsInvolvedInComment.concat(parentsCommentActors))
58 } 58 }
59 59
60 const data = await createActivityData(comment.url, byActor, commentObject, t, audience) 60 const data = createActivityData(comment.url, byActor, commentObject, audience)
61 61
62 // This was a reply, send it to the parent actors 62 // This was a reply, send it to the parent actors
63 const actorsException = [ byActor ] 63 const actorsException = [ byActor ]
@@ -82,14 +82,14 @@ async function sendCreateView (byActor: ActorModel, video: VideoModel, t: Transa
82 // Send to origin 82 // Send to origin
83 if (video.isOwned() === false) { 83 if (video.isOwned() === false) {
84 const audience = getVideoAudience(video, actorsInvolvedInVideo) 84 const audience = getVideoAudience(video, actorsInvolvedInVideo)
85 const data = await createActivityData(url, byActor, viewActivityData, t, audience) 85 const data = createActivityData(url, byActor, viewActivityData, audience)
86 86
87 return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl) 87 return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl)
88 } 88 }
89 89
90 // Send to followers 90 // Send to followers
91 const audience = getObjectFollowersAudience(actorsInvolvedInVideo) 91 const audience = getObjectFollowersAudience(actorsInvolvedInVideo)
92 const data = await createActivityData(url, byActor, viewActivityData, t, audience) 92 const data = createActivityData(url, byActor, viewActivityData, audience)
93 93
94 // Use the server actor to send the view 94 // Use the server actor to send the view
95 const serverActor = await getServerActor() 95 const serverActor = await getServerActor()
@@ -106,34 +106,31 @@ async function sendCreateDislike (byActor: ActorModel, video: VideoModel, t: Tra
106 // Send to origin 106 // Send to origin
107 if (video.isOwned() === false) { 107 if (video.isOwned() === false) {
108 const audience = getVideoAudience(video, actorsInvolvedInVideo) 108 const audience = getVideoAudience(video, actorsInvolvedInVideo)
109 const data = await createActivityData(url, byActor, dislikeActivityData, t, audience) 109 const data = createActivityData(url, byActor, dislikeActivityData, audience)
110 110
111 return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl) 111 return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl)
112 } 112 }
113 113
114 // Send to followers 114 // Send to followers
115 const audience = getObjectFollowersAudience(actorsInvolvedInVideo) 115 const audience = getObjectFollowersAudience(actorsInvolvedInVideo)
116 const data = await createActivityData(url, byActor, dislikeActivityData, t, audience) 116 const data = createActivityData(url, byActor, dislikeActivityData, audience)
117 117
118 const actorsException = [ byActor ] 118 const actorsException = [ byActor ]
119 return broadcastToFollowers(data, byActor, actorsInvolvedInVideo, t, actorsException) 119 return broadcastToFollowers(data, byActor, actorsInvolvedInVideo, t, actorsException)
120} 120}
121 121
122async function createActivityData (url: string, 122function createActivityData (url: string, byActor: ActorModel, object: any, audience?: ActivityAudience): ActivityCreate {
123 byActor: ActorModel, 123 if (!audience) audience = getAudience(byActor)
124 object: any, 124
125 t: Transaction, 125 return audiencify(
126 audience?: ActivityAudience): Promise<ActivityCreate> { 126 {
127 if (!audience) { 127 type: 'Create' as 'Create',
128 audience = await getAudience(byActor, t) 128 id: url + '/activity',
129 } 129 actor: byActor.url,
130 130 object: audiencify(object, audience)
131 return audiencify({ 131 },
132 type: 'Create' as 'Create', 132 audience
133 id: url + '/activity', 133 )
134 actor: byActor.url,
135 object: audiencify(object, audience)
136 }, audience)
137} 134}
138 135
139function createDislikeActivityData (byActor: ActorModel, video: VideoModel) { 136function 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)
14 // Send to origin 14 // Send to origin
15 if (video.isOwned() === false) { 15 if (video.isOwned() === false) {
16 const audience = getVideoAudience(video, accountsInvolvedInVideo) 16 const audience = getVideoAudience(video, accountsInvolvedInVideo)
17 const data = await likeActivityData(url, byActor, video, t, audience) 17 const data = likeActivityData(url, byActor, video, audience)
18 18
19 return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl) 19 return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl)
20 } 20 }
21 21
22 // Send to followers 22 // Send to followers
23 const audience = getObjectFollowersAudience(accountsInvolvedInVideo) 23 const audience = getObjectFollowersAudience(accountsInvolvedInVideo)
24 const data = await likeActivityData(url, byActor, video, t, audience) 24 const data = likeActivityData(url, byActor, video, audience)
25 25
26 const followersException = [ byActor ] 26 const followersException = [ byActor ]
27 return broadcastToFollowers(data, byActor, accountsInvolvedInVideo, t, followersException) 27 return broadcastToFollowers(data, byActor, accountsInvolvedInVideo, t, followersException)
28} 28}
29 29
30async function likeActivityData ( 30function likeActivityData (url: string, byActor: ActorModel, video: VideoModel, audience?: ActivityAudience): ActivityLike {
31 url: string, 31 if (!audience) audience = getAudience(byActor)
32 byActor: ActorModel, 32
33 video: VideoModel, 33 return audiencify(
34 t: Transaction, 34 {
35 audience?: ActivityAudience 35 type: 'Like' as 'Like',
36): Promise<ActivityLike> { 36 id: url,
37 if (!audience) { 37 actor: byActor.url,
38 audience = await getAudience(byActor, t) 38 object: video.url
39 } 39 },
40 40 audience
41 return audiencify({ 41 )
42 type: 'Like' as 'Like',
43 id: url,
44 actor: byActor.url,
45 object: video.url
46 }, audience)
47} 42}
48 43
49// --------------------------------------------------------------------------- 44// ---------------------------------------------------------------------------
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) {
27 const undoUrl = getUndoActivityPubUrl(followUrl) 27 const undoUrl = getUndoActivityPubUrl(followUrl)
28 28
29 const object = followActivityData(followUrl, me, following) 29 const object = followActivityData(followUrl, me, following)
30 const data = await undoActivityData(undoUrl, me, object, t) 30 const data = undoActivityData(undoUrl, me, object)
31 31
32 return unicastTo(data, me, following.inboxUrl) 32 return unicastTo(data, me, following.inboxUrl)
33} 33}
@@ -37,18 +37,18 @@ async function sendUndoLike (byActor: ActorModel, video: VideoModel, t: Transact
37 const undoUrl = getUndoActivityPubUrl(likeUrl) 37 const undoUrl = getUndoActivityPubUrl(likeUrl)
38 38
39 const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, t) 39 const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, t)
40 const object = await likeActivityData(likeUrl, byActor, video, t) 40 const object = likeActivityData(likeUrl, byActor, video)
41 41
42 // Send to origin 42 // Send to origin
43 if (video.isOwned() === false) { 43 if (video.isOwned() === false) {
44 const audience = getVideoAudience(video, actorsInvolvedInVideo) 44 const audience = getVideoAudience(video, actorsInvolvedInVideo)
45 const data = await undoActivityData(undoUrl, byActor, object, t, audience) 45 const data = undoActivityData(undoUrl, byActor, object, audience)
46 46
47 return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl) 47 return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl)
48 } 48 }
49 49
50 const audience = getObjectFollowersAudience(actorsInvolvedInVideo) 50 const audience = getObjectFollowersAudience(actorsInvolvedInVideo)
51 const data = await undoActivityData(undoUrl, byActor, object, t, audience) 51 const data = undoActivityData(undoUrl, byActor, object, audience)
52 52
53 const followersException = [ byActor ] 53 const followersException = [ byActor ]
54 return broadcastToFollowers(data, byActor, actorsInvolvedInVideo, t, followersException) 54 return broadcastToFollowers(data, byActor, actorsInvolvedInVideo, t, followersException)
@@ -60,16 +60,16 @@ async function sendUndoDislike (byActor: ActorModel, video: VideoModel, t: Trans
60 60
61 const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, t) 61 const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, t)
62 const dislikeActivity = createDislikeActivityData(byActor, video) 62 const dislikeActivity = createDislikeActivityData(byActor, video)
63 const object = await createActivityData(dislikeUrl, byActor, dislikeActivity, t) 63 const object = createActivityData(dislikeUrl, byActor, dislikeActivity)
64 64
65 if (video.isOwned() === false) { 65 if (video.isOwned() === false) {
66 const audience = getVideoAudience(video, actorsInvolvedInVideo) 66 const audience = getVideoAudience(video, actorsInvolvedInVideo)
67 const data = await undoActivityData(undoUrl, byActor, object, t, audience) 67 const data = undoActivityData(undoUrl, byActor, object, audience)
68 68
69 return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl) 69 return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl)
70 } 70 }
71 71
72 const data = await undoActivityData(undoUrl, byActor, object, t) 72 const data = undoActivityData(undoUrl, byActor, object)
73 73
74 const followersException = [ byActor ] 74 const followersException = [ byActor ]
75 return broadcastToFollowers(data, byActor, actorsInvolvedInVideo, t, followersException) 75 return broadcastToFollowers(data, byActor, actorsInvolvedInVideo, t, followersException)
@@ -80,7 +80,7 @@ async function sendUndoAnnounce (byActor: ActorModel, videoShare: VideoShareMode
80 80
81 const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, t) 81 const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, t)
82 const object = await buildVideoAnnounce(byActor, videoShare, video, t) 82 const object = await buildVideoAnnounce(byActor, videoShare, video, t)
83 const data = await undoActivityData(undoUrl, byActor, object, t) 83 const data = undoActivityData(undoUrl, byActor, object)
84 84
85 const followersException = [ byActor ] 85 const followersException = [ byActor ]
86 return broadcastToFollowers(data, byActor, actorsInvolvedInVideo, t, followersException) 86 return broadcastToFollowers(data, byActor, actorsInvolvedInVideo, t, followersException)
@@ -97,21 +97,21 @@ export {
97 97
98// --------------------------------------------------------------------------- 98// ---------------------------------------------------------------------------
99 99
100async function undoActivityData ( 100function undoActivityData (
101 url: string, 101 url: string,
102 byActor: ActorModel, 102 byActor: ActorModel,
103 object: ActivityFollow | ActivityLike | ActivityCreate | ActivityAnnounce, 103 object: ActivityFollow | ActivityLike | ActivityCreate | ActivityAnnounce,
104 t: Transaction,
105 audience?: ActivityAudience 104 audience?: ActivityAudience
106): Promise<ActivityUndo> { 105): ActivityUndo {
107 if (!audience) { 106 if (!audience) audience = getAudience(byActor)
108 audience = await getAudience(byActor, t) 107
109 } 108 return audiencify(
110 109 {
111 return audiencify({ 110 type: 'Undo' as 'Undo',
112 type: 'Undo' as 'Undo', 111 id: url,
113 id: url, 112 actor: byActor.url,
114 actor: byActor.url, 113 object
115 object 114 },
116 }, audience) 115 audience
116 )
117} 117}
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) {
15 15
16 const url = getUpdateActivityPubUrl(video.url, video.updatedAt.toISOString()) 16 const url = getUpdateActivityPubUrl(video.url, video.updatedAt.toISOString())
17 const videoObject = video.toActivityPubObject() 17 const videoObject = video.toActivityPubObject()
18 const audience = await getAudience(byActor, t, video.privacy === VideoPrivacy.PUBLIC) 18 const audience = getAudience(byActor, video.privacy === VideoPrivacy.PUBLIC)
19 19
20 const data = await updateActivityData(url, byActor, videoObject, t, audience) 20 const data = updateActivityData(url, byActor, videoObject, audience)
21 21
22 const actorsInvolved = await VideoShareModel.loadActorsByShare(video.id, t) 22 const actorsInvolved = await VideoShareModel.loadActorsByShare(video.id, t)
23 actorsInvolved.push(byActor) 23 actorsInvolved.push(byActor)
@@ -30,8 +30,8 @@ async function sendUpdateActor (accountOrChannel: AccountModel | VideoChannelMod
30 30
31 const url = getUpdateActivityPubUrl(byActor.url, byActor.updatedAt.toISOString()) 31 const url = getUpdateActivityPubUrl(byActor.url, byActor.updatedAt.toISOString())
32 const accountOrChannelObject = accountOrChannel.toActivityPubObject() 32 const accountOrChannelObject = accountOrChannel.toActivityPubObject()
33 const audience = await getAudience(byActor, t) 33 const audience = getAudience(byActor)
34 const data = await updateActivityData(url, byActor, accountOrChannelObject, t, audience) 34 const data = updateActivityData(url, byActor, accountOrChannelObject, audience)
35 35
36 let actorsInvolved: ActorModel[] 36 let actorsInvolved: ActorModel[]
37 if (accountOrChannel instanceof AccountModel) { 37 if (accountOrChannel instanceof AccountModel) {
@@ -56,21 +56,17 @@ export {
56 56
57// --------------------------------------------------------------------------- 57// ---------------------------------------------------------------------------
58 58
59async function updateActivityData ( 59function updateActivityData (url: string, byActor: ActorModel, object: any, audience?: ActivityAudience): ActivityUpdate {
60 url: string, 60 if (!audience) audience = getAudience(byActor)
61 byActor: ActorModel,
62 object: any,
63 t: Transaction,
64 audience?: ActivityAudience
65): Promise<ActivityUpdate> {
66 if (!audience) {
67 audience = await getAudience(byActor, t)
68 }
69 61
70 return audiencify({ 62 return audiencify(
71 type: 'Update' as 'Update', 63 {
72 id: url, 64 type: 'Update' as 'Update',
73 actor: byActor.url, 65 id: url,
74 object: audiencify(object, audience) 66 actor: byActor.url,
75 }, audience) 67 object: audiencify(object, audience
68 )
69 },
70 audience
71 )
76} 72}
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 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import * as sequelize from 'sequelize'
2import * as magnetUtil from 'magnet-uri' 3import * as magnetUtil from 'magnet-uri'
3import { join } from 'path' 4import { join } from 'path'
4import * as request from 'request' 5import * as request from 'request'
5import { ActivityIconObject } from '../../../shared/index' 6import { ActivityIconObject, VideoState } from '../../../shared/index'
6import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' 7import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
7import { VideoPrivacy, VideoRateType } from '../../../shared/models/videos' 8import { VideoPrivacy, VideoRateType } from '../../../shared/models/videos'
8import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos' 9import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos'
@@ -21,6 +22,21 @@ import { VideoShareModel } from '../../models/video/video-share'
21import { getOrCreateActorAndServerAndModel } from './actor' 22import { getOrCreateActorAndServerAndModel } from './actor'
22import { addVideoComments } from './video-comments' 23import { addVideoComments } from './video-comments'
23import { crawlCollectionPage } from './crawl' 24import { crawlCollectionPage } from './crawl'
25import { sendCreateVideo, sendUpdateVideo } from './send'
26import { shareVideoByServerAndChannel } from './index'
27
28async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) {
29 // If the video is not private and published, we federate it
30 if (video.privacy !== VideoPrivacy.PRIVATE && video.state === VideoState.PUBLISHED) {
31 if (isNewVideo === true) {
32 // Now we'll add the video's meta data to our followers
33 await sendCreateVideo(video, transaction)
34 await shareVideoByServerAndChannel(video, transaction)
35 } else {
36 await sendUpdateVideo(video, transaction)
37 }
38 }
39}
24 40
25function fetchRemoteVideoPreview (video: VideoModel, reject: Function) { 41function fetchRemoteVideoPreview (video: VideoModel, reject: Function) {
26 const host = video.VideoChannel.Account.Actor.Server.host 42 const host = video.VideoChannel.Account.Actor.Server.host
@@ -55,9 +71,11 @@ function generateThumbnailFromUrl (video: VideoModel, icon: ActivityIconObject)
55 return doRequestAndSaveToFile(options, thumbnailPath) 71 return doRequestAndSaveToFile(options, thumbnailPath)
56} 72}
57 73
58async function videoActivityObjectToDBAttributes (videoChannel: VideoChannelModel, 74async function videoActivityObjectToDBAttributes (
59 videoObject: VideoTorrentObject, 75 videoChannel: VideoChannelModel,
60 to: string[] = []) { 76 videoObject: VideoTorrentObject,
77 to: string[] = []
78) {
61 const privacy = to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1 ? VideoPrivacy.PUBLIC : VideoPrivacy.UNLISTED 79 const privacy = to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1 ? VideoPrivacy.PUBLIC : VideoPrivacy.UNLISTED
62 const duration = videoObject.duration.replace(/[^\d]+/, '') 80 const duration = videoObject.duration.replace(/[^\d]+/, '')
63 81
@@ -90,6 +108,8 @@ async function videoActivityObjectToDBAttributes (videoChannel: VideoChannelMode
90 support, 108 support,
91 nsfw: videoObject.sensitive, 109 nsfw: videoObject.sensitive,
92 commentsEnabled: videoObject.commentsEnabled, 110 commentsEnabled: videoObject.commentsEnabled,
111 waitTranscoding: videoObject.waitTranscoding,
112 state: videoObject.state,
93 channelId: videoChannel.id, 113 channelId: videoChannel.id,
94 duration: parseInt(duration, 10), 114 duration: parseInt(duration, 10),
95 createdAt: new Date(videoObject.published), 115 createdAt: new Date(videoObject.published),
@@ -185,22 +205,20 @@ async function getOrCreateVideo (videoObject: VideoTorrentObject, channelActor:
185} 205}
186 206
187async function getOrCreateAccountAndVideoAndChannel (videoObject: VideoTorrentObject | string, actor?: ActorModel) { 207async function getOrCreateAccountAndVideoAndChannel (videoObject: VideoTorrentObject | string, actor?: ActorModel) {
188 if (typeof videoObject === 'string') { 208 const videoUrl = typeof videoObject === 'string' ? videoObject : videoObject.id
189 const videoUrl = videoObject 209
190 210 const videoFromDatabase = await VideoModel.loadByUrlAndPopulateAccount(videoUrl)
191 const videoFromDatabase = await VideoModel.loadByUrlAndPopulateAccount(videoUrl) 211 if (videoFromDatabase) {
192 if (videoFromDatabase) { 212 return {
193 return { 213 video: videoFromDatabase,
194 video: videoFromDatabase, 214 actor: videoFromDatabase.VideoChannel.Account.Actor,
195 actor: videoFromDatabase.VideoChannel.Account.Actor, 215 channelActor: videoFromDatabase.VideoChannel.Actor
196 channelActor: videoFromDatabase.VideoChannel.Actor
197 }
198 } 216 }
199
200 videoObject = await fetchRemoteVideo(videoUrl)
201 if (!videoObject) throw new Error('Cannot fetch remote video with url: ' + videoUrl)
202 } 217 }
203 218
219 videoObject = await fetchRemoteVideo(videoUrl)
220 if (!videoObject) throw new Error('Cannot fetch remote video with url: ' + videoUrl)
221
204 if (!actor) { 222 if (!actor) {
205 const actorObj = videoObject.attributedTo.find(a => a.type === 'Person') 223 const actorObj = videoObject.attributedTo.find(a => a.type === 'Person')
206 if (!actorObj) throw new Error('Cannot find associated actor to video ' + videoObject.url) 224 if (!actorObj) throw new Error('Cannot find associated actor to video ' + videoObject.url)
@@ -291,20 +309,6 @@ async function addVideoShares (shareUrls: string[], instance: VideoModel) {
291 } 309 }
292} 310}
293 311
294export {
295 getOrCreateAccountAndVideoAndChannel,
296 fetchRemoteVideoPreview,
297 fetchRemoteVideoDescription,
298 generateThumbnailFromUrl,
299 videoActivityObjectToDBAttributes,
300 videoFileActivityUrlToDBAttributes,
301 getOrCreateVideo,
302 getOrCreateVideoChannel,
303 addVideoShares
304}
305
306// ---------------------------------------------------------------------------
307
308async function fetchRemoteVideo (videoUrl: string): Promise<VideoTorrentObject> { 312async function fetchRemoteVideo (videoUrl: string): Promise<VideoTorrentObject> {
309 const options = { 313 const options = {
310 uri: videoUrl, 314 uri: videoUrl,
@@ -324,3 +328,17 @@ async function fetchRemoteVideo (videoUrl: string): Promise<VideoTorrentObject>
324 328
325 return body 329 return body
326} 330}
331
332export {
333 federateVideoIfNeeded,
334 fetchRemoteVideo,
335 getOrCreateAccountAndVideoAndChannel,
336 fetchRemoteVideoPreview,
337 fetchRemoteVideoDescription,
338 generateThumbnailFromUrl,
339 videoActivityObjectToDBAttributes,
340 videoFileActivityUrlToDBAttributes,
341 getOrCreateVideo,
342 getOrCreateVideoChannel,
343 addVideoShares
344}
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 @@
1import * as kue from 'kue' 1import * as kue from 'kue'
2import { VideoResolution } from '../../../../shared' 2import { VideoResolution, VideoState } from '../../../../shared'
3import { VideoPrivacy } from '../../../../shared/models/videos'
4import { logger } from '../../../helpers/logger' 3import { logger } from '../../../helpers/logger'
5import { computeResolutionsToTranscode } from '../../../helpers/utils' 4import { computeResolutionsToTranscode } from '../../../helpers/utils'
6import { sequelizeTypescript } from '../../../initializers'
7import { VideoModel } from '../../../models/video/video' 5import { VideoModel } from '../../../models/video/video'
8import { shareVideoByServerAndChannel } from '../../activitypub'
9import { sendCreateVideo, sendUpdateVideo } from '../../activitypub/send'
10import { JobQueue } from '../job-queue' 6import { JobQueue } from '../job-queue'
7import { federateVideoIfNeeded } from '../../activitypub'
8import { retryTransactionWrapper } from '../../../helpers/database-utils'
9import { sequelizeTypescript } from '../../../initializers'
11 10
12export type VideoFilePayload = { 11export type VideoFilePayload = {
13 videoUUID: string 12 videoUUID: string
14 isNewVideo: boolean 13 isNewVideo?: boolean
15 resolution?: VideoResolution 14 resolution?: VideoResolution
16 isPortraitMode?: boolean 15 isPortraitMode?: boolean
17} 16}
@@ -52,10 +51,20 @@ async function processVideoFile (job: kue.Job) {
52 // Transcoding in other resolution 51 // Transcoding in other resolution
53 if (payload.resolution) { 52 if (payload.resolution) {
54 await video.transcodeOriginalVideofile(payload.resolution, payload.isPortraitMode) 53 await video.transcodeOriginalVideofile(payload.resolution, payload.isPortraitMode)
55 await onVideoFileTranscoderOrImportSuccess(video) 54
55 const options = {
56 arguments: [ video ],
57 errorMessage: 'Cannot execute onVideoFileTranscoderOrImportSuccess with many retries.'
58 }
59 await retryTransactionWrapper(onVideoFileTranscoderOrImportSuccess, options)
56 } else { 60 } else {
57 await video.optimizeOriginalVideofile() 61 await video.optimizeOriginalVideofile()
58 await onVideoFileOptimizerSuccess(video, payload.isNewVideo) 62
63 const options = {
64 arguments: [ video, payload.isNewVideo ],
65 errorMessage: 'Cannot execute onVideoFileOptimizerSuccess with many retries.'
66 }
67 await retryTransactionWrapper(onVideoFileOptimizerSuccess, options)
59 } 68 }
60 69
61 return video 70 return video
@@ -64,68 +73,70 @@ async function processVideoFile (job: kue.Job) {
64async function onVideoFileTranscoderOrImportSuccess (video: VideoModel) { 73async function onVideoFileTranscoderOrImportSuccess (video: VideoModel) {
65 if (video === undefined) return undefined 74 if (video === undefined) return undefined
66 75
67 // Maybe the video changed in database, refresh it 76 return sequelizeTypescript.transaction(async t => {
68 const videoDatabase = await VideoModel.loadByUUIDAndPopulateAccountAndServerAndTags(video.uuid) 77 // Maybe the video changed in database, refresh it
69 // Video does not exist anymore 78 let videoDatabase = await VideoModel.loadByUUIDAndPopulateAccountAndServerAndTags(video.uuid, t)
70 if (!videoDatabase) return undefined 79 // Video does not exist anymore
80 if (!videoDatabase) return undefined
71 81
72 if (video.privacy !== VideoPrivacy.PRIVATE) { 82 // We transcoded the video file in another format, now we can publish it
73 await sendUpdateVideo(video, undefined) 83 const oldState = videoDatabase.state
74 } 84 videoDatabase.state = VideoState.PUBLISHED
85 videoDatabase = await videoDatabase.save({ transaction: t })
86
87 // If the video was not published, we consider it is a new one for other instances
88 const isNewVideo = oldState !== VideoState.PUBLISHED
89 await federateVideoIfNeeded(videoDatabase, isNewVideo, t)
75 90
76 return undefined 91 return undefined
92 })
77} 93}
78 94
79async function onVideoFileOptimizerSuccess (video: VideoModel, isNewVideo: boolean) { 95async function onVideoFileOptimizerSuccess (video: VideoModel, isNewVideo: boolean) {
80 if (video === undefined) return undefined 96 if (video === undefined) return undefined
81 97
82 // Maybe the video changed in database, refresh it 98 // Outside the transaction (IO on disk)
83 const videoDatabase = await VideoModel.loadByUUIDAndPopulateAccountAndServerAndTags(video.uuid) 99 const { videoFileResolution } = await video.getOriginalFileResolution()
84 // Video does not exist anymore 100
85 if (!videoDatabase) return undefined 101 return sequelizeTypescript.transaction(async t => {
86 102 // Maybe the video changed in database, refresh it
87 if (video.privacy !== VideoPrivacy.PRIVATE) { 103 const videoDatabase = await VideoModel.loadByUUIDAndPopulateAccountAndServerAndTags(video.uuid, t)
88 if (isNewVideo !== false) { 104 // Video does not exist anymore
89 // Now we'll add the video's meta data to our followers 105 if (!videoDatabase) return undefined
90 await sequelizeTypescript.transaction(async t => { 106
91 await sendCreateVideo(video, t) 107 // Create transcoding jobs if there are enabled resolutions
92 await shareVideoByServerAndChannel(video, t) 108 const resolutionsEnabled = computeResolutionsToTranscode(videoFileResolution)
93 }) 109 logger.info(
94 } else { 110 'Resolutions computed for video %s and origin file height of %d.', videoDatabase.uuid, videoFileResolution,
95 await sendUpdateVideo(video, undefined) 111 { resolutions: resolutionsEnabled }
96 } 112 )
97 } 113
98 114 if (resolutionsEnabled.length !== 0) {
99 const { videoFileResolution } = await videoDatabase.getOriginalFileResolution() 115 const tasks: Promise<any>[] = []
100 116
101 // Create transcoding jobs if there are enabled resolutions 117 for (const resolution of resolutionsEnabled) {
102 const resolutionsEnabled = computeResolutionsToTranscode(videoFileResolution) 118 const dataInput = {
103 logger.info( 119 videoUUID: videoDatabase.uuid,
104 'Resolutions computed for video %s and origin file height of %d.', videoDatabase.uuid, videoFileResolution, 120 resolution
105 { resolutions: resolutionsEnabled } 121 }
106 ) 122
123 const p = JobQueue.Instance.createJob({ type: 'video-file', payload: dataInput })
124 tasks.push(p)
125 }
107 126
108 if (resolutionsEnabled.length !== 0) { 127 await Promise.all(tasks)
109 const tasks: Promise<any>[] = []
110 128
111 for (const resolution of resolutionsEnabled) { 129 logger.info('Transcoding jobs created for uuid %s.', videoDatabase.uuid, { resolutionsEnabled })
112 const dataInput = { 130 } else {
113 videoUUID: videoDatabase.uuid, 131 // No transcoding to do, it's now published
114 resolution, 132 video.state = VideoState.PUBLISHED
115 isNewVideo 133 video = await video.save({ transaction: t })
116 }
117 134
118 const p = JobQueue.Instance.createJob({ type: 'video-file', payload: dataInput }) 135 logger.info('No transcoding jobs created for video %s (no resolutions).', video.uuid)
119 tasks.push(p)
120 } 136 }
121 137
122 await Promise.all(tasks) 138 return federateVideoIfNeeded(video, isNewVideo, t)
123 139 })
124 logger.info('Transcoding jobs created for uuid %s.', videoDatabase.uuid, { resolutionsEnabled })
125 } else {
126 logger.info('No transcoding jobs created for video %s (no resolutions enabled).')
127 return undefined
128 }
129} 140}
130 141
131// --------------------------------------------------------------------------- 142// ---------------------------------------------------------------------------
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 {
79 const res = await handlers[ handlerName ](job) 79 const res = await handlers[ handlerName ](job)
80 return done(null, res) 80 return done(null, res)
81 } catch (err) { 81 } catch (err) {
82 logger.error('Cannot execute job %d.', job.id, { err })
82 return done(err) 83 return done(err)
83 } 84 }
84 }) 85 })
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) {
14 14
15 // Not cached 15 // Not cached
16 if (!cached) { 16 if (!cached) {
17 logger.debug('Not cached result for route %s.', req.originalUrl) 17 logger.debug('No cached results for route %s.', req.originalUrl)
18 18
19 const sendSave = res.send.bind(res) 19 const sendSave = res.send.bind(res)
20 20
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 = [
55 .customSanitizer(toValueOrNull) 55 .customSanitizer(toValueOrNull)
56 .custom(isVideoLanguageValid).withMessage('Should have a valid language'), 56 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
57 body('nsfw') 57 body('nsfw')
58 .optional()
58 .toBoolean() 59 .toBoolean()
59 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'), 60 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
61 body('waitTranscoding')
62 .optional()
63 .toBoolean()
64 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
60 body('description') 65 body('description')
61 .optional() 66 .optional()
62 .customSanitizer(toValueOrNull) 67 .customSanitizer(toValueOrNull)
@@ -70,6 +75,7 @@ const videosAddValidator = [
70 .customSanitizer(toValueOrNull) 75 .customSanitizer(toValueOrNull)
71 .custom(isVideoTagsValid).withMessage('Should have correct tags'), 76 .custom(isVideoTagsValid).withMessage('Should have correct tags'),
72 body('commentsEnabled') 77 body('commentsEnabled')
78 .optional()
73 .toBoolean() 79 .toBoolean()
74 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'), 80 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
75 body('privacy') 81 body('privacy')
@@ -149,6 +155,10 @@ const videosUpdateValidator = [
149 .optional() 155 .optional()
150 .toBoolean() 156 .toBoolean()
151 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'), 157 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
158 body('waitTranscoding')
159 .optional()
160 .toBoolean()
161 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
152 body('privacy') 162 body('privacy')
153 .optional() 163 .optional()
154 .toInt() 164 .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 {
25 Table, 25 Table,
26 UpdatedAt 26 UpdatedAt
27} from 'sequelize-typescript' 27} from 'sequelize-typescript'
28import { VideoPrivacy, VideoResolution } from '../../../shared' 28import { VideoPrivacy, VideoResolution, VideoState } from '../../../shared'
29import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' 29import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
30import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos' 30import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos'
31import { VideoFilter } from '../../../shared/models/videos/video-query.type' 31import { VideoFilter } from '../../../shared/models/videos/video-query.type'
@@ -47,7 +47,7 @@ import {
47 isVideoLanguageValid, 47 isVideoLanguageValid,
48 isVideoLicenceValid, 48 isVideoLicenceValid,
49 isVideoNameValid, 49 isVideoNameValid,
50 isVideoPrivacyValid, 50 isVideoPrivacyValid, isVideoStateValid,
51 isVideoSupportValid 51 isVideoSupportValid
52} from '../../helpers/custom-validators/videos' 52} from '../../helpers/custom-validators/videos'
53import { generateImageFromVideoFile, getVideoFileResolution, transcode } from '../../helpers/ffmpeg-utils' 53import { generateImageFromVideoFile, getVideoFileResolution, transcode } from '../../helpers/ffmpeg-utils'
@@ -66,7 +66,7 @@ import {
66 VIDEO_EXT_MIMETYPE, 66 VIDEO_EXT_MIMETYPE,
67 VIDEO_LANGUAGES, 67 VIDEO_LANGUAGES,
68 VIDEO_LICENCES, 68 VIDEO_LICENCES,
69 VIDEO_PRIVACIES 69 VIDEO_PRIVACIES, VIDEO_STATES
70} from '../../initializers' 70} from '../../initializers'
71import { 71import {
72 getVideoCommentsActivityPubUrl, 72 getVideoCommentsActivityPubUrl,
@@ -93,10 +93,7 @@ enum ScopeNames {
93 AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST', 93 AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST',
94 WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS', 94 WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS',
95 WITH_TAGS = 'WITH_TAGS', 95 WITH_TAGS = 'WITH_TAGS',
96 WITH_FILES = 'WITH_FILES', 96 WITH_FILES = 'WITH_FILES'
97 WITH_SHARES = 'WITH_SHARES',
98 WITH_RATES = 'WITH_RATES',
99 WITH_COMMENTS = 'WITH_COMMENTS'
100} 97}
101 98
102@Scopes({ 99@Scopes({
@@ -183,7 +180,20 @@ enum ScopeNames {
183 ')' 180 ')'
184 ) 181 )
185 }, 182 },
186 privacy: VideoPrivacy.PUBLIC 183 // Always list public videos
184 privacy: VideoPrivacy.PUBLIC,
185 // Always list published videos, or videos that are being transcoded but on which we don't want to wait for transcoding
186 [ Sequelize.Op.or ]: [
187 {
188 state: VideoState.PUBLISHED
189 },
190 {
191 [ Sequelize.Op.and ]: {
192 state: VideoState.TO_TRANSCODE,
193 waitTranscoding: false
194 }
195 }
196 ]
187 }, 197 },
188 include: [ videoChannelInclude ] 198 include: [ videoChannelInclude ]
189 } 199 }
@@ -272,42 +282,6 @@ enum ScopeNames {
272 required: true 282 required: true
273 } 283 }
274 ] 284 ]
275 },
276 [ScopeNames.WITH_SHARES]: {
277 include: [
278 {
279 ['separate' as any]: true,
280 model: () => VideoShareModel.unscoped()
281 }
282 ]
283 },
284 [ScopeNames.WITH_RATES]: {
285 include: [
286 {
287 ['separate' as any]: true,
288 model: () => AccountVideoRateModel,
289 include: [
290 {
291 model: () => AccountModel.unscoped(),
292 required: true,
293 include: [
294 {
295 attributes: [ 'url' ],
296 model: () => ActorModel.unscoped()
297 }
298 ]
299 }
300 ]
301 }
302 ]
303 },
304 [ScopeNames.WITH_COMMENTS]: {
305 include: [
306 {
307 ['separate' as any]: true,
308 model: () => VideoCommentModel.unscoped()
309 }
310 ]
311 } 285 }
312}) 286})
313@Table({ 287@Table({
@@ -335,7 +309,7 @@ enum ScopeNames {
335 fields: [ 'channelId' ] 309 fields: [ 'channelId' ]
336 }, 310 },
337 { 311 {
338 fields: [ 'id', 'privacy' ] 312 fields: [ 'id', 'privacy', 'state', 'waitTranscoding' ]
339 }, 313 },
340 { 314 {
341 fields: [ 'url'], 315 fields: [ 'url'],
@@ -435,6 +409,16 @@ export class VideoModel extends Model<VideoModel> {
435 @Column 409 @Column
436 commentsEnabled: boolean 410 commentsEnabled: boolean
437 411
412 @AllowNull(false)
413 @Column
414 waitTranscoding: boolean
415
416 @AllowNull(false)
417 @Default(null)
418 @Is('VideoState', value => throwIfNotValid(value, isVideoStateValid, 'state'))
419 @Column
420 state: VideoState
421
438 @CreatedAt 422 @CreatedAt
439 createdAt: Date 423 createdAt: Date
440 424
@@ -671,7 +655,7 @@ export class VideoModel extends Model<VideoModel> {
671 }) 655 })
672 } 656 }
673 657
674 static listAccountVideosForApi (accountId: number, start: number, count: number, sort: string, hideNSFW: boolean, withFiles = false) { 658 static listUserVideosForApi (accountId: number, start: number, count: number, sort: string, hideNSFW: boolean, withFiles = false) {
675 const query: IFindOptions<VideoModel> = { 659 const query: IFindOptions<VideoModel> = {
676 offset: start, 660 offset: start,
677 limit: count, 661 limit: count,
@@ -858,12 +842,13 @@ export class VideoModel extends Model<VideoModel> {
858 .findOne(options) 842 .findOne(options)
859 } 843 }
860 844
861 static loadByUUIDAndPopulateAccountAndServerAndTags (uuid: string) { 845 static loadByUUIDAndPopulateAccountAndServerAndTags (uuid: string, t?: Sequelize.Transaction) {
862 const options = { 846 const options = {
863 order: [ [ 'Tags', 'name', 'ASC' ] ], 847 order: [ [ 'Tags', 'name', 'ASC' ] ],
864 where: { 848 where: {
865 uuid 849 uuid
866 } 850 },
851 transaction: t
867 } 852 }
868 853
869 return VideoModel 854 return VideoModel
@@ -905,31 +890,23 @@ export class VideoModel extends Model<VideoModel> {
905 } 890 }
906 891
907 private static getCategoryLabel (id: number) { 892 private static getCategoryLabel (id: number) {
908 let categoryLabel = VIDEO_CATEGORIES[id] 893 return VIDEO_CATEGORIES[id] || 'Misc'
909 if (!categoryLabel) categoryLabel = 'Misc'
910
911 return categoryLabel
912 } 894 }
913 895
914 private static getLicenceLabel (id: number) { 896 private static getLicenceLabel (id: number) {
915 let licenceLabel = VIDEO_LICENCES[id] 897 return VIDEO_LICENCES[id] || 'Unknown'
916 if (!licenceLabel) licenceLabel = 'Unknown'
917
918 return licenceLabel
919 } 898 }
920 899
921 private static getLanguageLabel (id: string) { 900 private static getLanguageLabel (id: string) {
922 let languageLabel = VIDEO_LANGUAGES[id] 901 return VIDEO_LANGUAGES[id] || 'Unknown'
923 if (!languageLabel) languageLabel = 'Unknown'
924
925 return languageLabel
926 } 902 }
927 903
928 private static getPrivacyLabel (id: number) { 904 private static getPrivacyLabel (id: number) {
929 let privacyLabel = VIDEO_PRIVACIES[id] 905 return VIDEO_PRIVACIES[id] || 'Unknown'
930 if (!privacyLabel) privacyLabel = 'Unknown' 906 }
931 907
932 return privacyLabel 908 private static getStateLabel (id: number) {
909 return VIDEO_STATES[id] || 'Unknown'
933 } 910 }
934 911
935 getOriginalFile () { 912 getOriginalFile () {
@@ -1026,11 +1003,16 @@ export class VideoModel extends Model<VideoModel> {
1026 return join(STATIC_PATHS.PREVIEWS, this.getPreviewName()) 1003 return join(STATIC_PATHS.PREVIEWS, this.getPreviewName())
1027 } 1004 }
1028 1005
1029 toFormattedJSON (): Video { 1006 toFormattedJSON (options?: {
1007 additionalAttributes: {
1008 state: boolean,
1009 waitTranscoding: boolean
1010 }
1011 }): Video {
1030 const formattedAccount = this.VideoChannel.Account.toFormattedJSON() 1012 const formattedAccount = this.VideoChannel.Account.toFormattedJSON()
1031 const formattedVideoChannel = this.VideoChannel.toFormattedJSON() 1013 const formattedVideoChannel = this.VideoChannel.toFormattedJSON()
1032 1014
1033 return { 1015 const videoObject: Video = {
1034 id: this.id, 1016 id: this.id,
1035 uuid: this.uuid, 1017 uuid: this.uuid,
1036 name: this.name, 1018 name: this.name,
@@ -1082,6 +1064,19 @@ export class VideoModel extends Model<VideoModel> {
1082 avatar: formattedVideoChannel.avatar 1064 avatar: formattedVideoChannel.avatar
1083 } 1065 }
1084 } 1066 }
1067
1068 if (options) {
1069 if (options.additionalAttributes.state) {
1070 videoObject.state = {
1071 id: this.state,
1072 label: VideoModel.getStateLabel(this.state)
1073 }
1074 }
1075
1076 if (options.additionalAttributes.waitTranscoding) videoObject.waitTranscoding = this.waitTranscoding
1077 }
1078
1079 return videoObject
1085 } 1080 }
1086 1081
1087 toFormattedDetailsJSON (): VideoDetails { 1082 toFormattedDetailsJSON (): VideoDetails {
@@ -1094,6 +1089,11 @@ export class VideoModel extends Model<VideoModel> {
1094 account: this.VideoChannel.Account.toFormattedJSON(), 1089 account: this.VideoChannel.Account.toFormattedJSON(),
1095 tags: map(this.Tags, 'name'), 1090 tags: map(this.Tags, 'name'),
1096 commentsEnabled: this.commentsEnabled, 1091 commentsEnabled: this.commentsEnabled,
1092 waitTranscoding: this.waitTranscoding,
1093 state: {
1094 id: this.state,
1095 label: VideoModel.getStateLabel(this.state)
1096 },
1097 files: [] 1097 files: []
1098 } 1098 }
1099 1099
@@ -1207,6 +1207,8 @@ export class VideoModel extends Model<VideoModel> {
1207 language, 1207 language,
1208 views: this.views, 1208 views: this.views,
1209 sensitive: this.nsfw, 1209 sensitive: this.nsfw,
1210 waitTranscoding: this.waitTranscoding,
1211 state: this.state,
1210 commentsEnabled: this.commentsEnabled, 1212 commentsEnabled: this.commentsEnabled,
1211 published: this.publishedAt.toISOString(), 1213 published: this.publishedAt.toISOString(),
1212 updated: this.updatedAt.toISOString(), 1214 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 () {
175 language: 'pt', 175 language: 'pt',
176 nsfw: false, 176 nsfw: false,
177 commentsEnabled: true, 177 commentsEnabled: true,
178 waitTranscoding: true,
178 description: 'my super description', 179 description: 'my super description',
179 support: 'my super support text', 180 support: 'my super support text',
180 tags: [ 'tag1', 'tag2' ], 181 tags: [ 'tag1', 'tag2' ],
@@ -224,20 +225,6 @@ describe('Test videos API validator', function () {
224 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) 225 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
225 }) 226 })
226 227
227 it('Should fail without nsfw attribute', async function () {
228 const fields = omit(baseCorrectParams, 'nsfw')
229 const attaches = baseCorrectAttaches
230
231 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
232 })
233
234 it('Should fail without commentsEnabled attribute', async function () {
235 const fields = omit(baseCorrectParams, 'commentsEnabled')
236 const attaches = baseCorrectAttaches
237
238 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
239 })
240
241 it('Should fail with a long description', async function () { 228 it('Should fail with a long description', async function () {
242 const fields = immutableAssign(baseCorrectParams, { description: 'super'.repeat(2500) }) 229 const fields = immutableAssign(baseCorrectParams, { description: 'super'.repeat(2500) })
243 const attaches = baseCorrectAttaches 230 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 () {
924 924
925 describe('With minimum parameters', function () { 925 describe('With minimum parameters', function () {
926 it('Should upload and propagate the video', async function () { 926 it('Should upload and propagate the video', async function () {
927 this.timeout(50000) 927 this.timeout(60000)
928 928
929 const path = '/api/v1/videos/upload' 929 const path = '/api/v1/videos/upload'
930 930
@@ -934,16 +934,14 @@ describe('Test multiple servers', function () {
934 .set('Authorization', 'Bearer ' + servers[1].accessToken) 934 .set('Authorization', 'Bearer ' + servers[1].accessToken)
935 .field('name', 'minimum parameters') 935 .field('name', 'minimum parameters')
936 .field('privacy', '1') 936 .field('privacy', '1')
937 .field('nsfw', 'false')
938 .field('channelId', '1') 937 .field('channelId', '1')
939 .field('commentsEnabled', 'true')
940 938
941 const filePath = join(__dirname, '..', '..', 'fixtures', 'video_short.webm') 939 const filePath = join(__dirname, '..', '..', 'fixtures', 'video_short.webm')
942 940
943 await req.attach('videofile', filePath) 941 await req.attach('videofile', filePath)
944 .expect(200) 942 .expect(200)
945 943
946 await wait(25000) 944 await wait(40000)
947 945
948 for (const server of servers) { 946 for (const server of servers) {
949 const res = await getVideosList(server.url) 947 const res = await getVideosList(server.url)
@@ -964,7 +962,7 @@ describe('Test multiple servers', function () {
964 }, 962 },
965 isLocal, 963 isLocal,
966 duration: 5, 964 duration: 5,
967 commentsEnabled: true, 965 commentsEnabled: false,
968 tags: [ ], 966 tags: [ ],
969 privacy: VideoPrivacy.PUBLIC, 967 privacy: VideoPrivacy.PUBLIC,
970 channel: { 968 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 () {
32 const oembedUrl = 'http://localhost:9001/videos/watch/' + server.video.uuid 32 const oembedUrl = 'http://localhost:9001/videos/watch/' + server.video.uuid
33 33
34 const res = await getOEmbed(server.url, oembedUrl) 34 const res = await getOEmbed(server.url, oembedUrl)
35 const expectedHtml = `<iframe width="560" height="315" src="http://localhost:9001/videos/embed/${server.video.uuid}" ` + 35 const expectedHtml = '<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts" ' +
36 `src="http://localhost:9001/videos/embed/${server.video.uuid}" ` +
36 'frameborder="0" allowfullscreen></iframe>' 37 'frameborder="0" allowfullscreen></iframe>'
37 const expectedThumbnailUrl = 'http://localhost:9001/static/previews/' + server.video.uuid + '.jpg' 38 const expectedThumbnailUrl = 'http://localhost:9001/static/previews/' + server.video.uuid + '.jpg'
38 39
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 @@
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { VideoDetails } from '../../../../shared/models/videos' 5import { VideoDetails, VideoState } from '../../../../shared/models/videos'
6import { getVideoFileFPS } from '../../../helpers/ffmpeg-utils' 6import { getVideoFileFPS } from '../../../helpers/ffmpeg-utils'
7import { 7import {
8 flushAndRunMultipleServers, flushTests, getVideo, getVideosList, killallServers, root, ServerInfo, setAccessTokensToServers, uploadVideo, 8 doubleFollow,
9 wait, webtorrentAdd 9 flushAndRunMultipleServers,
10 flushTests,
11 getMyVideos,
12 getVideo,
13 getVideosList,
14 killallServers,
15 root,
16 ServerInfo,
17 setAccessTokensToServers,
18 uploadVideo,
19 wait,
20 webtorrentAdd
10} from '../../utils' 21} from '../../utils'
11import { join } from 'path' 22import { join } from 'path'
12 23
@@ -109,6 +120,63 @@ describe('Test video transcoding', function () {
109 } 120 }
110 }) 121 })
111 122
123 it('Should wait transcoding before publishing the video', async function () {
124 this.timeout(80000)
125
126 await doubleFollow(servers[0], servers[1])
127
128 await wait(15000)
129
130 {
131 // Upload the video, but wait transcoding
132 const videoAttributes = {
133 name: 'waiting video',
134 fixture: 'video_short1.webm',
135 waitTranscoding: true
136 }
137 const resVideo = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, videoAttributes)
138 const videoId = resVideo.body.video.uuid
139
140 // Should be in transcode state
141 const { body } = await getVideo(servers[ 1 ].url, videoId)
142 expect(body.name).to.equal('waiting video')
143 expect(body.state.id).to.equal(VideoState.TO_TRANSCODE)
144 expect(body.state.label).to.equal('To transcode')
145 expect(body.waitTranscoding).to.be.true
146
147 // Should have my video
148 const resMyVideos = await getMyVideos(servers[1].url, servers[1].accessToken, 0, 10)
149 const videoToFindInMine = resMyVideos.body.data.find(v => v.name === 'waiting video')
150 expect(videoToFindInMine).not.to.be.undefined
151 expect(videoToFindInMine.state.id).to.equal(VideoState.TO_TRANSCODE)
152 expect(videoToFindInMine.state.label).to.equal('To transcode')
153 expect(videoToFindInMine.waitTranscoding).to.be.true
154
155 // Should not list this video
156 const resVideos = await getVideosList(servers[1].url)
157 const videoToFindInList = resVideos.body.data.find(v => v.name === 'waiting video')
158 expect(videoToFindInList).to.be.undefined
159
160 // Server 1 should not have the video yet
161 await getVideo(servers[0].url, videoId, 404)
162 }
163
164 await wait(30000)
165
166 for (const server of servers) {
167 const res = await getVideosList(server.url)
168 const videoToFind = res.body.data.find(v => v.name === 'waiting video')
169 expect(videoToFind).not.to.be.undefined
170
171 const res2 = await getVideo(server.url, videoToFind.id)
172 const videoDetails: VideoDetails = res2.body
173
174 expect(videoDetails.state.id).to.equal(VideoState.PUBLISHED)
175 expect(videoDetails.state.label).to.equal('Published')
176 expect(videoDetails.waitTranscoding).to.be.true
177 }
178 })
179
112 after(async function () { 180 after(async function () {
113 killallServers(servers) 181 killallServers(servers)
114 182
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 () {
65 const env = getEnvCli(servers[0]) 65 const env = getEnvCli(servers[0])
66 await execCLI(`${env} npm run create-transcoding-job -- -v ${video2UUID}`) 66 await execCLI(`${env} npm run create-transcoding-job -- -v ${video2UUID}`)
67 67
68 await wait(30000) 68 await wait(40000)
69 69
70 for (const server of servers) { 70 for (const server of servers) {
71 const res = await getVideosList(server.url) 71 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 = {
27 language?: string 27 language?: string
28 nsfw?: boolean 28 nsfw?: boolean
29 commentsEnabled?: boolean 29 commentsEnabled?: boolean
30 waitTranscoding?: boolean
30 description?: string 31 description?: string
31 tags?: string[] 32 tags?: string[]
32 channelId?: number 33 channelId?: number
@@ -326,6 +327,7 @@ async function uploadVideo (url: string, accessToken: string, videoAttributesArg
326 language: 'zh', 327 language: 'zh',
327 channelId: defaultChannelId, 328 channelId: defaultChannelId,
328 nsfw: true, 329 nsfw: true,
330 waitTranscoding: false,
329 description: 'my super description', 331 description: 'my super description',
330 support: 'my super support text', 332 support: 'my super support text',
331 tags: [ 'tag' ], 333 tags: [ 'tag' ],
@@ -341,6 +343,7 @@ async function uploadVideo (url: string, accessToken: string, videoAttributesArg
341 .field('name', attributes.name) 343 .field('name', attributes.name)
342 .field('nsfw', JSON.stringify(attributes.nsfw)) 344 .field('nsfw', JSON.stringify(attributes.nsfw))
343 .field('commentsEnabled', JSON.stringify(attributes.commentsEnabled)) 345 .field('commentsEnabled', JSON.stringify(attributes.commentsEnabled))
346 .field('waitTranscoding', JSON.stringify(attributes.waitTranscoding))
344 .field('privacy', attributes.privacy.toString()) 347 .field('privacy', attributes.privacy.toString())
345 .field('channelId', attributes.channelId) 348 .field('channelId', attributes.channelId)
346 349
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
176 licence, 176 licence,
177 language, 177 language,
178 nsfw: isNSFW(videoInfo), 178 nsfw: isNSFW(videoInfo),
179 waitTranscoding: true,
179 commentsEnabled: true, 180 commentsEnabled: true,
180 description: videoInfo.description || undefined, 181 description: videoInfo.description || undefined,
181 support: undefined, 182 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 () {
84 fixture: program['file'], 84 fixture: program['file'],
85 thumbnailfile: program['thumbnailPath'], 85 thumbnailfile: program['thumbnailPath'],
86 previewfile: program['previewPath'], 86 previewfile: program['previewPath'],
87 waitTranscoding: true,
87 privacy: program['privacy'], 88 privacy: program['privacy'],
88 support: undefined 89 support: undefined
89 } 90 }
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 {
5 ActivityUrlObject 5 ActivityUrlObject
6} from './common-objects' 6} from './common-objects'
7import { ActivityPubOrderedCollection } from '../activitypub-ordered-collection' 7import { ActivityPubOrderedCollection } from '../activitypub-ordered-collection'
8import { VideoState } from '../../videos'
8 9
9export interface VideoTorrentObject { 10export interface VideoTorrentObject {
10 type: 'Video' 11 type: 'Video'
@@ -19,6 +20,8 @@ export interface VideoTorrentObject {
19 views: number 20 views: number
20 sensitive: boolean 21 sensitive: boolean
21 commentsEnabled: boolean 22 commentsEnabled: boolean
23 waitTranscoding: boolean
24 state: VideoState
22 published: string 25 published: string
23 updated: string 26 updated: string
24 mediaType: 'text/markdown' 27 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'
13export * from './video-resolution.enum' 13export * from './video-resolution.enum'
14export * from './video-update.model' 14export * from './video-update.model'
15export * from './video.model' 15export * from './video.model'
16export * 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 {
7 description?: string 7 description?: string
8 support?: string 8 support?: string
9 channelId: number 9 channelId: number
10 nsfw: boolean 10 nsfw?: boolean
11 waitTranscoding?: boolean
11 name: string 12 name: string
12 tags?: string[] 13 tags?: string[]
13 commentsEnabled?: boolean 14 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 @@
1export enum VideoState {
2 PUBLISHED = 1,
3 TO_TRANSCODE = 2
4}
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 {
11 tags?: string[] 11 tags?: string[]
12 commentsEnabled?: boolean 12 commentsEnabled?: boolean
13 nsfw?: boolean 13 nsfw?: boolean
14 waitTranscoding?: boolean
14 channelId?: number 15 channelId?: number
15 thumbnailfile?: Blob 16 thumbnailfile?: Blob
16 previewfile?: Blob 17 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 @@
1import { VideoResolution } from '../../index' 1import { VideoResolution, VideoState } from '../../index'
2import { Account } from '../actors' 2import { Account } from '../actors'
3import { Avatar } from '../avatars/avatar.model' 3import { Avatar } from '../avatars/avatar.model'
4import { VideoChannel } from './video-channel.model' 4import { VideoChannel } from './video-channel.model'
@@ -41,6 +41,9 @@ export interface Video {
41 dislikes: number 41 dislikes: number
42 nsfw: boolean 42 nsfw: boolean
43 43
44 waitTranscoding?: boolean
45 state?: VideoConstant<VideoState>
46
44 account: { 47 account: {
45 id: number 48 id: number
46 uuid: string 49 uuid: string
@@ -70,4 +73,8 @@ export interface VideoDetails extends Video {
70 files: VideoFile[] 73 files: VideoFile[]
71 account: Account 74 account: Account
72 commentsEnabled: boolean 75 commentsEnabled: boolean
76
77 // Not optional in details (unlike in Video)
78 waitTranscoding: boolean
79 state: VideoConstant<VideoState>
73} 80}
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
@@ -3437,6 +3437,19 @@
3437 </div> 3437 </div>
3438 <div class="prop-row prop-group"> 3438 <div class="prop-row prop-group">
3439 <div class="prop-name"> 3439 <div class="prop-name">
3440 <div class="prop-title">waitTranscoding</div>
3441 <div class="prop-subtitle"> in formData </div>
3442 <div class="prop-subtitle">
3443 <span class="json-property-type">boolean</span>
3444 <span class="json-property-range" title="Value limits"></span>
3445 </div>
3446 </div>
3447 <div class="prop-value">
3448 <p>Whether or not we wait transcoding before publish the video</p>
3449 </div>
3450 </div>
3451 <div class="prop-row prop-group">
3452 <div class="prop-name">
3440 <div class="prop-title">support</div> 3453 <div class="prop-title">support</div>
3441 <div class="prop-subtitle"> in formData </div> 3454 <div class="prop-subtitle"> in formData </div>
3442 <div class="prop-subtitle"> 3455 <div class="prop-subtitle">
@@ -4011,6 +4024,19 @@
4011 </div> 4024 </div>
4012 <div class="prop-row prop-group"> 4025 <div class="prop-row prop-group">
4013 <div class="prop-name"> 4026 <div class="prop-name">
4027 <div class="prop-title">waitTranscoding</div>
4028 <div class="prop-subtitle"> in formData </div>
4029 <div class="prop-subtitle">
4030 <span class="json-property-type">boolean</span>
4031 <span class="json-property-range" title="Value limits"></span>
4032 </div>
4033 </div>
4034 <div class="prop-value">
4035 <p>Whether or not we wait transcoding before publish the video</p>
4036 </div>
4037 </div>
4038 <div class="prop-row prop-group">
4039 <div class="prop-name">
4014 <div class="prop-title">licence</div> 4040 <div class="prop-title">licence</div>
4015 <div class="prop-subtitle"> in formData </div> 4041 <div class="prop-subtitle"> in formData </div>
4016 <div class="prop-subtitle"> 4042 <div class="prop-subtitle">
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:
682 in: formData 682 in: formData
683 type: string 683 type: string
684 description: 'Video description' 684 description: 'Video description'
685 - name: waitTranscoding
686 in: formData
687 type: boolean
688 description: 'Whether or not we wait transcoding before publish the video'
685 - name: support 689 - name: support
686 in: formData 690 in: formData
687 type: string 691 type: string
@@ -814,6 +818,10 @@ paths:
814 in: formData 818 in: formData
815 type: number 819 type: number
816 description: 'Video category' 820 description: 'Video category'
821 - name: waitTranscoding
822 in: formData
823 type: boolean
824 description: 'Whether or not we wait transcoding before publish the video'
817 - name: licence 825 - name: licence
818 in: formData 826 in: formData
819 type: number 827 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 \
63 * Vimeo: https://vimeo.com/xxxxxx 63 * Vimeo: https://vimeo.com/xxxxxx
64 * Dailymotion: https://www.dailymotion.com/xxxxx 64 * Dailymotion: https://www.dailymotion.com/xxxxx
65 65
66 The script will get all public videos from Youtube, download them and upload to PeerTube. 66The script will get all public videos from Youtube, download them and upload to PeerTube.
67 Already downloaded videos will not be uploaded twice, so you can run and re-run the script in case of crash, disconnection... 67Already downloaded videos will not be uploaded twice, so you can run and re-run the script in case of crash, disconnection...
68
69Videos will be publicly available after transcoding (you can see them before that in your account on the web interface).
70
68 71
69### upload.js 72### upload.js
70 73
71You can use this script to import videos directly from the CLI. 74You can use this script to import videos directly from the CLI.
72 75
76Videos will be publicly available after transcoding (you can see them before that in your account on the web interface).
77
73``` 78```
74$ cd ${CLONE} 79$ cd ${CLONE}
75$ node dist/server/tools/upload.js --help 80$ node dist/server/tools/upload.js --help