diff options
author | Chocobozzz <me@florianbigard.com> | 2018-06-12 20:04:58 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2018-06-12 20:37:51 +0200 |
commit | 2186386cca113506791583cb07d6ccacba7af4e0 (patch) | |
tree | 3c214c0b5fbd64332624267fa6e51fd4a9cf6474 | |
parent | 6ccdf3a23ecec5ba2eeaf487fd1fafdc7606b4bf (diff) | |
download | PeerTube-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
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' | |||
12 | import { Video } from '../../shared/video/video.model' | 12 | import { Video } from '../../shared/video/video.model' |
13 | import { VideoService } from '../../shared/video/video.service' | 13 | import { VideoService } from '../../shared/video/video.service' |
14 | import { I18n } from '@ngx-translate/i18n-polyfill' | 14 | import { I18n } from '@ngx-translate/i18n-polyfill' |
15 | import { 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 @@ | |||
1 | import { UserRight, VideoChannel, VideoDetails as VideoDetailsServerModel, VideoFile } from '../../../../../shared' | 1 | import { |
2 | UserRight, | ||
3 | VideoChannel, | ||
4 | VideoConstant, | ||
5 | VideoDetails as VideoDetailsServerModel, | ||
6 | VideoFile, | ||
7 | VideoState | ||
8 | } from '../../../../../shared' | ||
2 | import { AuthUser } from '../../core' | 9 | import { AuthUser } from '../../core' |
3 | import { Video } from '../../shared/video/video.model' | 10 | import { Video } from '../../shared/video/video.model' |
4 | import { Account } from '@app/shared/account/account.model' | 11 | import { 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 @@ | |||
1 | import { VideoDetails } from './video-details.model' | 1 | import { VideoDetails } from './video-details.model' |
2 | import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy.enum' | 2 | import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy.enum' |
3 | import { VideoUpdate } from '../../../../../shared/models/videos' | ||
3 | 4 | ||
4 | export class VideoEdit { | 5 | export 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 @@ | |||
1 | import { User } from '../' | 1 | import { User } from '../' |
2 | import { Video as VideoServerModel, VideoPrivacy } from '../../../../../shared' | 2 | import { Video as VideoServerModel, VideoPrivacy, VideoState } from '../../../../../shared' |
3 | import { Avatar } from '../../../../../shared/models/avatars/avatar.model' | 3 | import { Avatar } from '../../../../../shared/models/avatars/avatar.model' |
4 | import { VideoConstant } from '../../../../../shared/models/videos/video.model' | 4 | import { VideoConstant } from '../../../../../shared/models/videos/video.model' |
5 | import { getAbsoluteAPIUrl } from '../misc/utils' | 5 | import { 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 @@ | |||
1 | import { catchError } from 'rxjs/operators' | 1 | import { catchError } from 'rxjs/operators' |
2 | import { Component, ElementRef, LOCALE_ID, NgZone, OnDestroy, OnInit, ViewChild, Inject } from '@angular/core' | 2 | import { Component, ElementRef, Inject, LOCALE_ID, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core' |
3 | import { ActivatedRoute, Router } from '@angular/router' | 3 | import { ActivatedRoute, Router } from '@angular/router' |
4 | import { RedirectService } from '@app/core/routing/redirect.service' | 4 | import { RedirectService } from '@app/core/routing/redirect.service' |
5 | import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage' | 5 | import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage' |
@@ -10,7 +10,7 @@ import { Subscription } from 'rxjs' | |||
10 | import * as videojs from 'video.js' | 10 | import * as videojs from 'video.js' |
11 | import 'videojs-hotkeys' | 11 | import 'videojs-hotkeys' |
12 | import * as WebTorrent from 'webtorrent' | 12 | import * as WebTorrent from 'webtorrent' |
13 | import { UserVideoRateType, VideoRateType } from '../../../../../shared' | 13 | import { UserVideoRateType, VideoRateType, VideoState } from '../../../../../shared' |
14 | import '../../../assets/player/peertube-videojs-plugin' | 14 | import '../../../assets/player/peertube-videojs-plugin' |
15 | import { AuthService, ConfirmService } from '../../core' | 15 | import { AuthService, ConfirmService } from '../../core' |
16 | import { RestExtractor, VideoBlacklistService } from '../../shared' | 16 | import { RestExtractor, VideoBlacklistService } from '../../shared' |
@@ -21,7 +21,7 @@ import { MarkdownService } from '../shared' | |||
21 | import { VideoDownloadComponent } from './modal/video-download.component' | 21 | import { VideoDownloadComponent } from './modal/video-download.component' |
22 | import { VideoReportComponent } from './modal/video-report.component' | 22 | import { VideoReportComponent } from './modal/video-report.component' |
23 | import { VideoShareComponent } from './modal/video-share.component' | 23 | import { VideoShareComponent } from './modal/video-share.component' |
24 | import { getVideojsOptions, loadLocale, addContextMenu } from '../../../assets/player/peertube-player' | 24 | import { addContextMenu, getVideojsOptions, loadLocale } from '../../../assets/player/peertube-player' |
25 | import { ServerService } from '@app/core' | 25 | import { ServerService } from '@app/core' |
26 | import { I18n } from '@ngx-translate/i18n-polyfill' | 26 | import { I18n } from '@ngx-translate/i18n-polyfill' |
27 | import { environment } from '../../../environments/environment' | 27 | import { 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 | |||
123 | async function videoController (req: express.Request, res: express.Response, next: express.NextFunction) { | 123 | async 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 | ||
167 | async function getUserVideos (req: express.Request, res: express.Response, next: express.NextFunction) { | 167 | async 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 | ||
180 | async function createUserRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) { | 181 | async function createUserRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) { |
@@ -318,7 +319,7 @@ async function updateMe (req: express.Request, res: express.Response, next: expr | |||
318 | } | 319 | } |
319 | 320 | ||
320 | async function updateMyAvatar (req: express.Request, res: express.Response, next: express.NextFunction) { | 321 | async 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 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { extname, join } from 'path' | 2 | import { extname, join } from 'path' |
3 | import { VideoCreate, VideoPrivacy, VideoUpdate } from '../../../../shared' | 3 | import { VideoCreate, VideoPrivacy, VideoState, VideoUpdate } from '../../../../shared' |
4 | import { renamePromise } from '../../../helpers/core-utils' | 4 | import { renamePromise } from '../../../helpers/core-utils' |
5 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 5 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
6 | import { getVideoFileResolution } from '../../../helpers/ffmpeg-utils' | 6 | import { getVideoFileResolution } from '../../../helpers/ffmpeg-utils' |
@@ -21,11 +21,11 @@ import { | |||
21 | } from '../../../initializers' | 21 | } from '../../../initializers' |
22 | import { | 22 | import { |
23 | changeVideoChannelShare, | 23 | changeVideoChannelShare, |
24 | federateVideoIfNeeded, | ||
24 | fetchRemoteVideoDescription, | 25 | fetchRemoteVideoDescription, |
25 | getVideoActivityPubUrl, | 26 | getVideoActivityPubUrl |
26 | shareVideoByServerAndChannel | ||
27 | } from '../../../lib/activitypub' | 27 | } from '../../../lib/activitypub' |
28 | import { sendCreateVideo, sendCreateView, sendUpdateVideo } from '../../../lib/activitypub/send' | 28 | import { sendCreateView } from '../../../lib/activitypub/send' |
29 | import { JobQueue } from '../../../lib/job-queue' | 29 | import { JobQueue } from '../../../lib/job-queue' |
30 | import { Redis } from '../../../lib/redis' | 30 | import { Redis } from '../../../lib/redis' |
31 | import { | 31 | import { |
@@ -51,7 +51,7 @@ import { videoCommentRouter } from './comment' | |||
51 | import { rateVideoRouter } from './rate' | 51 | import { rateVideoRouter } from './rate' |
52 | import { VideoFilter } from '../../../../shared/models/videos/video-query.type' | 52 | import { VideoFilter } from '../../../../shared/models/videos/video-query.type' |
53 | import { VideoSortField } from '../../../../client/src/app/shared/video/sort-field.type' | 53 | import { VideoSortField } from '../../../../client/src/app/shared/video/sort-field.type' |
54 | import { isNSFWHidden, createReqFiles } from '../../../helpers/express-utils' | 54 | import { createReqFiles, isNSFWHidden } from '../../../helpers/express-utils' |
55 | 55 | ||
56 | const videosRouter = express.Router() | 56 | const 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' | |||
8 | import { pageToStartAndCount } from './core-utils' | 8 | import { pageToStartAndCount } from './core-utils' |
9 | 9 | ||
10 | function activityPubContextify <T> (data: T) { | 10 | function 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' |
13 | import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc' | 14 | import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc' |
15 | import { VideoState } from '../../../../shared/models/videos' | ||
14 | 16 | ||
15 | function sanitizeAndCheckVideoTorrentCreateActivity (activity: any) { | 17 | function 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' |
15 | import { VideoModel } from '../../models/video/video' | 16 | import { VideoModel } from '../../models/video/video' |
16 | import { exists, isArray, isFileValid } from './misc' | 17 | import { exists, isArray, isFileValid } from './misc' |
@@ -21,11 +22,15 @@ const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS | |||
21 | const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES | 22 | const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES |
22 | 23 | ||
23 | function isVideoCategoryValid (value: any) { | 24 | function isVideoCategoryValid (value: any) { |
24 | return value === null || VIDEO_CATEGORIES[value] !== undefined | 25 | return value === null || VIDEO_CATEGORIES[ value ] !== undefined |
26 | } | ||
27 | |||
28 | function isVideoStateValid (value: any) { | ||
29 | return exists(value) && VIDEO_STATES[ value ] !== undefined | ||
25 | } | 30 | } |
26 | 31 | ||
27 | function isVideoLicenceValid (value: any) { | 32 | function isVideoLicenceValid (value: any) { |
28 | return value === null || VIDEO_LICENCES[value] !== undefined | 33 | return value === null || VIDEO_LICENCES[ value ] !== undefined |
29 | } | 34 | } |
30 | 35 | ||
31 | function isVideoLanguageValid (value: any) { | 36 | function isVideoLanguageValid (value: any) { |
@@ -79,20 +84,22 @@ function isVideoRatingTypeValid (value: string) { | |||
79 | 84 | ||
80 | const videoFileTypes = Object.keys(VIDEO_MIMETYPE_EXT).map(m => `(${m})`) | 85 | const videoFileTypes = Object.keys(VIDEO_MIMETYPE_EXT).map(m => `(${m})`) |
81 | const videoFileTypesRegex = videoFileTypes.join('|') | 86 | const videoFileTypesRegex = videoFileTypes.join('|') |
87 | |||
82 | function isVideoFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) { | 88 | function 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 | ||
86 | const videoImageTypes = CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME | 92 | const videoImageTypes = CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME |
87 | .map(v => v.replace('.', '')) | 93 | .map(v => v.replace('.', '')) |
88 | .join('|') | 94 | .join('|') |
89 | const videoImageTypesRegex = `image/(${videoImageTypes})` | 95 | const videoImageTypesRegex = `image/(${videoImageTypes})` |
96 | |||
90 | function isVideoImage (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[], field: string) { | 97 | function 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 | ||
94 | function isVideoPrivacyValid (value: string) { | 101 | function 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 | ||
98 | function isVideoFileInfoHashValid (value: string) { | 105 | function 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 @@ | |||
1 | import { Model } from 'sequelize-typescript' | 1 | import { Model } from 'sequelize-typescript' |
2 | import * as ipaddr from 'ipaddr.js' | 2 | import * as ipaddr from 'ipaddr.js' |
3 | const isCidr = require('is-cidr') | ||
4 | import { ResultList } from '../../shared' | 3 | import { ResultList } from '../../shared' |
5 | import { VideoResolution } from '../../shared/models/videos' | 4 | import { VideoResolution } from '../../shared/models/videos' |
6 | import { CONFIG } from '../initializers' | 5 | import { CONFIG } from '../initializers' |
@@ -10,6 +9,8 @@ import { ApplicationModel } from '../models/application/application' | |||
10 | import { pseudoRandomBytesPromise } from './core-utils' | 9 | import { pseudoRandomBytesPromise } from './core-utils' |
11 | import { logger } from './logger' | 10 | import { logger } from './logger' |
12 | 11 | ||
12 | const isCidr = require('is-cidr') | ||
13 | |||
13 | async function generateRandomString (size: number) { | 14 | async 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 | ||
19 | interface FormattableToJSON { | 20 | interface FormattableToJSON { |
20 | toFormattedJSON () | 21 | toFormattedJSON (args?: any) |
21 | } | 22 | } |
22 | 23 | ||
23 | function getFormattedObjects<U, T extends FormattableToJSON> (objects: T[], objectsTotal: number) { | 24 | function 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 | ||
38 | async function isSignupAllowed () { | 37 | async 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 @@ | |||
1 | import { IConfig } from 'config' | 1 | import { IConfig } from 'config' |
2 | import { dirname, join } from 'path' | 2 | import { dirname, join } from 'path' |
3 | import { JobType, VideoRateType } from '../../shared/models' | 3 | import { JobType, VideoRateType, VideoState } from '../../shared/models' |
4 | import { ActivityPubActorType } from '../../shared/models/activitypub' | 4 | import { ActivityPubActorType } from '../../shared/models/activitypub' |
5 | import { FollowState } from '../../shared/models/actors' | 5 | import { FollowState } from '../../shared/models/actors' |
6 | import { VideoPrivacy } from '../../shared/models/videos' | 6 | import { VideoPrivacy } from '../../shared/models/videos' |
@@ -14,7 +14,7 @@ let config: IConfig = require('config') | |||
14 | 14 | ||
15 | // --------------------------------------------------------------------------- | 15 | // --------------------------------------------------------------------------- |
16 | 16 | ||
17 | const LAST_MIGRATION_VERSION = 215 | 17 | const 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 | ||
329 | const VIDEO_STATES = { | ||
330 | [VideoState.PUBLISHED]: 'Published', | ||
331 | [VideoState.TO_TRANSCODE]: 'To transcode' | ||
332 | } | ||
333 | |||
329 | const VIDEO_MIMETYPE_EXT = { | 334 | const 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 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async 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 | |||
58 | function down (options) { | ||
59 | throw new Error('Not implemented.') | ||
60 | } | ||
61 | |||
62 | export { up, down } | ||
diff --git a/server/lib/activitypub/audience.ts b/server/lib/activitypub/audience.ts index c1265dbcd..7164135b6 100644 --- a/server/lib/activitypub/audience.ts +++ b/server/lib/activitypub/audience.ts | |||
@@ -20,7 +20,7 @@ function getVideoCommentAudience ( | |||
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 | ||
58 | async function getAudience (actorSender: ActorModel, t: Transaction, isPublic = true) { | 58 | function 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 | ||
77 | function audiencify <T> (object: T, audience: ActivityAudience) { | 77 | function 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 @@ | |||
1 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import { ActivityUpdate } from '../../../../shared/models/activitypub' | 2 | import { ActivityUpdate } from '../../../../shared/models/activitypub' |
3 | import { ActivityPubActor } from '../../../../shared/models/activitypub/activitypub-actor' | 3 | import { ActivityPubActor } from '../../../../shared/models/activitypub/activitypub-actor' |
4 | import { VideoTorrentObject } from '../../../../shared/models/activitypub/objects' | ||
5 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 4 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
6 | import { logger } from '../../../helpers/logger' | 5 | import { logger } from '../../../helpers/logger' |
7 | import { resetSequelizeInstance } from '../../../helpers/utils' | 6 | import { resetSequelizeInstance } from '../../../helpers/utils' |
@@ -13,6 +12,7 @@ import { VideoChannelModel } from '../../../models/video/video-channel' | |||
13 | import { VideoFileModel } from '../../../models/video/video-file' | 12 | import { VideoFileModel } from '../../../models/video/video-file' |
14 | import { fetchAvatarIfExists, getOrCreateActorAndServerAndModel, updateActorAvatarInstance, updateActorInstance } from '../actor' | 13 | import { fetchAvatarIfExists, getOrCreateActorAndServerAndModel, updateActorAvatarInstance, updateActorInstance } from '../actor' |
15 | import { | 14 | import { |
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 | ||
53 | async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) { | 53 | async 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 | ||
17 | async function sendVideoAnnounce (byActor: ActorModel, videoShare: VideoShareModel, video: VideoModel, t: Transaction) { | 17 | async 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 | ||
23 | async function announceActivityData ( | 23 | function 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 | ||
122 | async function createActivityData (url: string, | 122 | function 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 | ||
139 | function createDislikeActivityData (byActor: ActorModel, video: VideoModel) { | 136 | function createDislikeActivityData (byActor: ActorModel, video: VideoModel) { |
diff --git a/server/lib/activitypub/send/send-like.ts b/server/lib/activitypub/send/send-like.ts index ddeb1fcd2..37ee7c096 100644 --- a/server/lib/activitypub/send/send-like.ts +++ b/server/lib/activitypub/send/send-like.ts | |||
@@ -14,36 +14,31 @@ async function sendLike (byActor: ActorModel, video: VideoModel, t: Transaction) | |||
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 | ||
30 | async function likeActivityData ( | 30 | function 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 | ||
100 | async function undoActivityData ( | 100 | function 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 | ||
59 | async function updateActivityData ( | 59 | function 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 @@ | |||
1 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import * as sequelize from 'sequelize' | ||
2 | import * as magnetUtil from 'magnet-uri' | 3 | import * as magnetUtil from 'magnet-uri' |
3 | import { join } from 'path' | 4 | import { join } from 'path' |
4 | import * as request from 'request' | 5 | import * as request from 'request' |
5 | import { ActivityIconObject } from '../../../shared/index' | 6 | import { ActivityIconObject, VideoState } from '../../../shared/index' |
6 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' | 7 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' |
7 | import { VideoPrivacy, VideoRateType } from '../../../shared/models/videos' | 8 | import { VideoPrivacy, VideoRateType } from '../../../shared/models/videos' |
8 | import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos' | 9 | import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos' |
@@ -21,6 +22,21 @@ import { VideoShareModel } from '../../models/video/video-share' | |||
21 | import { getOrCreateActorAndServerAndModel } from './actor' | 22 | import { getOrCreateActorAndServerAndModel } from './actor' |
22 | import { addVideoComments } from './video-comments' | 23 | import { addVideoComments } from './video-comments' |
23 | import { crawlCollectionPage } from './crawl' | 24 | import { crawlCollectionPage } from './crawl' |
25 | import { sendCreateVideo, sendUpdateVideo } from './send' | ||
26 | import { shareVideoByServerAndChannel } from './index' | ||
27 | |||
28 | async 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 | ||
25 | function fetchRemoteVideoPreview (video: VideoModel, reject: Function) { | 41 | function 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 | ||
58 | async function videoActivityObjectToDBAttributes (videoChannel: VideoChannelModel, | 74 | async 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 | ||
187 | async function getOrCreateAccountAndVideoAndChannel (videoObject: VideoTorrentObject | string, actor?: ActorModel) { | 207 | async 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 | ||
294 | export { | ||
295 | getOrCreateAccountAndVideoAndChannel, | ||
296 | fetchRemoteVideoPreview, | ||
297 | fetchRemoteVideoDescription, | ||
298 | generateThumbnailFromUrl, | ||
299 | videoActivityObjectToDBAttributes, | ||
300 | videoFileActivityUrlToDBAttributes, | ||
301 | getOrCreateVideo, | ||
302 | getOrCreateVideoChannel, | ||
303 | addVideoShares | ||
304 | } | ||
305 | |||
306 | // --------------------------------------------------------------------------- | ||
307 | |||
308 | async function fetchRemoteVideo (videoUrl: string): Promise<VideoTorrentObject> { | 312 | async 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 | |||
332 | export { | ||
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 @@ | |||
1 | import * as kue from 'kue' | 1 | import * as kue from 'kue' |
2 | import { VideoResolution } from '../../../../shared' | 2 | import { VideoResolution, VideoState } from '../../../../shared' |
3 | import { VideoPrivacy } from '../../../../shared/models/videos' | ||
4 | import { logger } from '../../../helpers/logger' | 3 | import { logger } from '../../../helpers/logger' |
5 | import { computeResolutionsToTranscode } from '../../../helpers/utils' | 4 | import { computeResolutionsToTranscode } from '../../../helpers/utils' |
6 | import { sequelizeTypescript } from '../../../initializers' | ||
7 | import { VideoModel } from '../../../models/video/video' | 5 | import { VideoModel } from '../../../models/video/video' |
8 | import { shareVideoByServerAndChannel } from '../../activitypub' | ||
9 | import { sendCreateVideo, sendUpdateVideo } from '../../activitypub/send' | ||
10 | import { JobQueue } from '../job-queue' | 6 | import { JobQueue } from '../job-queue' |
7 | import { federateVideoIfNeeded } from '../../activitypub' | ||
8 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | ||
9 | import { sequelizeTypescript } from '../../../initializers' | ||
11 | 10 | ||
12 | export type VideoFilePayload = { | 11 | export 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) { | |||
64 | async function onVideoFileTranscoderOrImportSuccess (video: VideoModel) { | 73 | async 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 | ||
79 | async function onVideoFileOptimizerSuccess (video: VideoModel, isNewVideo: boolean) { | 95 | async 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' |
28 | import { VideoPrivacy, VideoResolution } from '../../../shared' | 28 | import { VideoPrivacy, VideoResolution, VideoState } from '../../../shared' |
29 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' | 29 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' |
30 | import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos' | 30 | import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos' |
31 | import { VideoFilter } from '../../../shared/models/videos/video-query.type' | 31 | import { 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' |
53 | import { generateImageFromVideoFile, getVideoFileResolution, transcode } from '../../helpers/ffmpeg-utils' | 53 | import { 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' |
71 | import { | 71 | import { |
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 | ||
3 | import * as chai from 'chai' | 3 | import * as chai from 'chai' |
4 | import 'mocha' | 4 | import 'mocha' |
5 | import { VideoDetails } from '../../../../shared/models/videos' | 5 | import { VideoDetails, VideoState } from '../../../../shared/models/videos' |
6 | import { getVideoFileFPS } from '../../../helpers/ffmpeg-utils' | 6 | import { getVideoFileFPS } from '../../../helpers/ffmpeg-utils' |
7 | import { | 7 | import { |
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' |
11 | import { join } from 'path' | 22 | import { 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' |
7 | import { ActivityPubOrderedCollection } from '../activitypub-ordered-collection' | 7 | import { ActivityPubOrderedCollection } from '../activitypub-ordered-collection' |
8 | import { VideoState } from '../../videos' | ||
8 | 9 | ||
9 | export interface VideoTorrentObject { | 10 | export 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' | |||
13 | export * from './video-resolution.enum' | 13 | export * from './video-resolution.enum' |
14 | export * from './video-update.model' | 14 | export * from './video-update.model' |
15 | export * from './video.model' | 15 | export * from './video.model' |
16 | export * from './video-state.enum' | ||
diff --git a/shared/models/videos/video-create.model.ts b/shared/models/videos/video-create.model.ts index 562bc1bf2..2a1f622f6 100644 --- a/shared/models/videos/video-create.model.ts +++ b/shared/models/videos/video-create.model.ts | |||
@@ -7,7 +7,8 @@ export interface VideoCreate { | |||
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 @@ | |||
1 | export 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 @@ | |||
1 | import { VideoResolution } from '../../index' | 1 | import { VideoResolution, VideoState } from '../../index' |
2 | import { Account } from '../actors' | 2 | import { Account } from '../actors' |
3 | import { Avatar } from '../avatars/avatar.model' | 3 | import { Avatar } from '../avatars/avatar.model' |
4 | import { VideoChannel } from './video-channel.model' | 4 | import { 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. | 66 | The 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... | 67 | Already downloaded videos will not be uploaded twice, so you can run and re-run the script in case of crash, disconnection... |
68 | |||
69 | Videos 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 | ||
71 | You can use this script to import videos directly from the CLI. | 74 | You can use this script to import videos directly from the CLI. |
72 | 75 | ||
76 | Videos 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 |