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