Add video edition finished notification
This commit is contained in:
parent
348c2ce3ff
commit
1808a1f8e4
|
@ -44,7 +44,8 @@ export class MyAccountNotificationPreferencesComponent implements OnInit {
|
|||
abuseNewMessage: $localize`An abuse report received a new message`,
|
||||
abuseStateChange: $localize`One of your abuse reports has been accepted or rejected by moderators`,
|
||||
newPeerTubeVersion: $localize`A new PeerTube version is available`,
|
||||
newPluginVersion: $localize`One of your plugin/theme has a new available version`
|
||||
newPluginVersion: $localize`One of your plugin/theme has a new available version`,
|
||||
myVideoEditionFinished: $localize`Video edition finished`
|
||||
}
|
||||
this.notificationSettingGroups = [
|
||||
{
|
||||
|
@ -62,7 +63,8 @@ export class MyAccountNotificationPreferencesComponent implements OnInit {
|
|||
'newCommentOnMyVideo',
|
||||
'blacklistOnMyVideo',
|
||||
'myVideoPublished',
|
||||
'myVideoImportFinished'
|
||||
'myVideoImportFinished',
|
||||
'myVideoEditionFinished'
|
||||
]
|
||||
},
|
||||
|
||||
|
|
|
@ -227,6 +227,10 @@ export class UserNotification implements UserNotificationServer {
|
|||
this.pluginUrl = `/admin/plugins/list-installed`
|
||||
this.pluginQueryParams.pluginType = this.plugin.type + ''
|
||||
break
|
||||
|
||||
case UserNotificationType.MY_VIDEO_EDITION_FINISHED:
|
||||
this.videoUrl = this.buildVideoUrl(this.video)
|
||||
break
|
||||
}
|
||||
} catch (err) {
|
||||
this.type = null
|
||||
|
|
|
@ -203,7 +203,15 @@
|
|||
<my-global-icon iconName="cog" aria-hidden="true"></my-global-icon>
|
||||
|
||||
<div class="message" i18n>
|
||||
<a (click)="markAsRead(notification)" [href]="notification.peertubeVersionLink" target="_blank" rel="noopener noreferrer">A new version of PeerTube</a> is available: {{ notification.peertube.latestVersion }}
|
||||
<a (click)="markAsRead(notification)" [href]="notification.peertubeVersionLink" target="_blank" rel="noopener noreferrer">A new version of PeerTube</a> is available: {{ notification.peertube.latestVersion }}
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="19"> <!-- UserNotificationType.MY_VIDEO_EDITION_FINISHED -->
|
||||
<my-global-icon iconName="film" aria-hidden="true"></my-global-icon>
|
||||
|
||||
<div class="message" i18n>
|
||||
Your video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.video.name }}</a> edition has finished
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
|
|
|
@ -78,7 +78,7 @@ async function run () {
|
|||
if (files.some(f => f.storage === VideoStorage.FILE_SYSTEM) || hls?.storage === VideoStorage.FILE_SYSTEM) {
|
||||
console.log('Processing video %s.', videoFull.name)
|
||||
|
||||
const success = await moveToExternalStorageState(videoFull, false, undefined)
|
||||
const success = await moveToExternalStorageState({ video: videoFull, isNewVideo: false, transaction: undefined })
|
||||
|
||||
if (!success) {
|
||||
console.error(
|
||||
|
|
|
@ -82,7 +82,8 @@ async function updateNotificationSettings (req: express.Request, res: express.Re
|
|||
abuseNewMessage: body.abuseNewMessage,
|
||||
abuseStateChange: body.abuseStateChange,
|
||||
newPeerTubeVersion: body.newPeerTubeVersion,
|
||||
newPluginVersion: body.newPluginVersion
|
||||
newPluginVersion: body.newPluginVersion,
|
||||
myVideoEditionFinished: body.myVideoEditionFinished
|
||||
}
|
||||
|
||||
await UserNotificationSettingModel.update(values, query)
|
||||
|
|
|
@ -218,11 +218,11 @@ async function addVideo (options: {
|
|||
if (!refreshedVideo) return
|
||||
|
||||
if (refreshedVideo.state === VideoState.TO_MOVE_TO_EXTERNAL_STORAGE) {
|
||||
return addMoveToObjectStorageJob(refreshedVideo)
|
||||
return addMoveToObjectStorageJob({ video: refreshedVideo, previousVideoState: undefined })
|
||||
}
|
||||
|
||||
if (refreshedVideo.state === VideoState.TO_TRANSCODE) {
|
||||
return addOptimizeOrMergeAudioJob(refreshedVideo, videoFile, user)
|
||||
return addOptimizeOrMergeAudioJob({ video: refreshedVideo, videoFile, user })
|
||||
}
|
||||
}).catch(err => logger.error('Cannot add optimize/merge audio job for %s.', videoCreated.uuid, { err, ...lTags(videoCreated.uuid) }))
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const LAST_MIGRATION_VERSION = 695
|
||||
const LAST_MIGRATION_VERSION = 700
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
import * as Sequelize from 'sequelize'
|
||||
|
||||
async function up (utils: {
|
||||
transaction: Sequelize.Transaction
|
||||
queryInterface: Sequelize.QueryInterface
|
||||
sequelize: Sequelize.Sequelize
|
||||
db: any
|
||||
}): Promise<void> {
|
||||
const { transaction } = utils
|
||||
|
||||
{
|
||||
const data = {
|
||||
type: Sequelize.INTEGER,
|
||||
defaultValue: null,
|
||||
allowNull: true
|
||||
}
|
||||
await utils.queryInterface.addColumn('userNotificationSetting', 'myVideoEditionFinished', data, { transaction })
|
||||
}
|
||||
|
||||
{
|
||||
const query = 'UPDATE "userNotificationSetting" SET "myVideoEditionFinished" = 1'
|
||||
await utils.sequelize.query(query, { transaction })
|
||||
}
|
||||
|
||||
{
|
||||
const data = {
|
||||
type: Sequelize.INTEGER,
|
||||
defaultValue: null,
|
||||
allowNull: false
|
||||
}
|
||||
await utils.queryInterface.changeColumn('userNotificationSetting', 'myVideoEditionFinished', data, { transaction })
|
||||
}
|
||||
}
|
||||
|
||||
function down () {
|
||||
throw new Error('Not implemented.')
|
||||
}
|
||||
|
||||
export {
|
||||
up,
|
||||
down
|
||||
}
|
|
@ -11,7 +11,7 @@ import { moveToFailedMoveToObjectStorageState, moveToNextState } from '@server/l
|
|||
import { VideoModel } from '@server/models/video/video'
|
||||
import { VideoJobInfoModel } from '@server/models/video/video-job-info'
|
||||
import { MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoWithAllFiles } from '@server/types/models'
|
||||
import { MoveObjectStoragePayload, VideoStorage } from '@shared/models'
|
||||
import { MoveObjectStoragePayload, VideoState, VideoStorage } from '@shared/models'
|
||||
|
||||
const lTagsBase = loggerTagsFactory('move-object-storage')
|
||||
|
||||
|
@ -45,7 +45,7 @@ export async function processMoveToObjectStorage (job: Job) {
|
|||
if (pendingMove === 0) {
|
||||
logger.info('Running cleanup after moving files to object storage (video %s in job %d)', video.uuid, job.id, lTags)
|
||||
|
||||
await doAfterLastJob(video, payload.isNewVideo)
|
||||
await doAfterLastJob({ video, previousVideoState: payload.previousVideoState, isNewVideo: payload.isNewVideo })
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('Cannot move video %s to object storage.', video.url, { err, ...lTags })
|
||||
|
@ -91,7 +91,13 @@ async function moveHLSFiles (video: MVideoWithAllFiles) {
|
|||
}
|
||||
}
|
||||
|
||||
async function doAfterLastJob (video: MVideoWithAllFiles, isNewVideo: boolean) {
|
||||
async function doAfterLastJob (options: {
|
||||
video: MVideoWithAllFiles
|
||||
previousVideoState: VideoState
|
||||
isNewVideo: boolean
|
||||
}) {
|
||||
const { video, previousVideoState, isNewVideo } = options
|
||||
|
||||
for (const playlist of video.VideoStreamingPlaylists) {
|
||||
if (playlist.storage === VideoStorage.OBJECT_STORAGE) continue
|
||||
|
||||
|
@ -115,7 +121,7 @@ async function doAfterLastJob (video: MVideoWithAllFiles, isNewVideo: boolean) {
|
|||
await remove(getHLSDirectory(video))
|
||||
}
|
||||
|
||||
await moveToNextState(video, isNewVideo)
|
||||
await moveToNextState({ video, previousVideoState, isNewVideo })
|
||||
}
|
||||
|
||||
async function onFileMoved (options: {
|
||||
|
|
|
@ -8,10 +8,9 @@ import { federateVideoIfNeeded } from '@server/lib/activitypub/videos'
|
|||
import { generateWebTorrentVideoFilename } from '@server/lib/paths'
|
||||
import { VideoTranscodingProfilesManager } from '@server/lib/transcoding/default-transcoding-profiles'
|
||||
import { isAbleToUploadVideo } from '@server/lib/user'
|
||||
import { addMoveToObjectStorageJob, addOptimizeOrMergeAudioJob } from '@server/lib/video'
|
||||
import { addOptimizeOrMergeAudioJob } from '@server/lib/video'
|
||||
import { approximateIntroOutroAdditionalSize } from '@server/lib/video-editor'
|
||||
import { VideoPathManager } from '@server/lib/video-path-manager'
|
||||
import { buildNextVideoState } from '@server/lib/video-state'
|
||||
import { UserModel } from '@server/models/user/user'
|
||||
import { VideoModel } from '@server/models/video/video'
|
||||
import { VideoFileModel } from '@server/models/video/video-file'
|
||||
|
@ -33,8 +32,7 @@ import {
|
|||
VideoEditorTaskCutPayload,
|
||||
VideoEditorTaskIntroPayload,
|
||||
VideoEditorTaskOutroPayload,
|
||||
VideoEditorTaskWatermarkPayload,
|
||||
VideoState
|
||||
VideoEditorTaskWatermarkPayload
|
||||
} from '@shared/models'
|
||||
import { logger, loggerTagsFactory } from '../../../helpers/logger'
|
||||
|
||||
|
@ -42,14 +40,15 @@ const lTagsBase = loggerTagsFactory('video-edition')
|
|||
|
||||
async function processVideoEdition (job: Job) {
|
||||
const payload = job.data as VideoEditionPayload
|
||||
const lTags = lTagsBase(payload.videoUUID)
|
||||
|
||||
logger.info('Process video edition of %s in job %d.', payload.videoUUID, job.id)
|
||||
logger.info('Process video edition of %s in job %d.', payload.videoUUID, job.id, lTags)
|
||||
|
||||
const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(payload.videoUUID)
|
||||
|
||||
// No video, maybe deleted?
|
||||
if (!video) {
|
||||
logger.info('Can\'t process job %d, video does not exist.', job.id, lTagsBase(payload.videoUUID))
|
||||
logger.info('Can\'t process job %d, video does not exist.', job.id, lTags)
|
||||
return undefined
|
||||
}
|
||||
|
||||
|
@ -69,7 +68,8 @@ async function processVideoEdition (job: Job) {
|
|||
inputPath: tmpInputFilePath ?? originalFilePath,
|
||||
video,
|
||||
outputPath,
|
||||
task
|
||||
task,
|
||||
lTags
|
||||
})
|
||||
|
||||
if (tmpInputFilePath) await remove(tmpInputFilePath)
|
||||
|
@ -81,7 +81,7 @@ async function processVideoEdition (job: Job) {
|
|||
return outputPath
|
||||
})
|
||||
|
||||
logger.info('Video edition ended for video %s.', video.uuid)
|
||||
logger.info('Video edition ended for video %s.', video.uuid, lTags)
|
||||
|
||||
const newFile = await buildNewFile(video, editionResultPath)
|
||||
|
||||
|
@ -94,19 +94,13 @@ async function processVideoEdition (job: Job) {
|
|||
|
||||
await newFile.save()
|
||||
|
||||
video.state = buildNextVideoState()
|
||||
video.duration = await getVideoStreamDuration(outputPath)
|
||||
await video.save()
|
||||
|
||||
await federateVideoIfNeeded(video, false, undefined)
|
||||
|
||||
if (video.state === VideoState.TO_TRANSCODE) {
|
||||
const user = await UserModel.loadByVideoId(video.id)
|
||||
|
||||
await addOptimizeOrMergeAudioJob(video, newFile, user, false)
|
||||
} else if (video.state === VideoState.TO_MOVE_TO_EXTERNAL_STORAGE) {
|
||||
await addMoveToObjectStorageJob(video, false)
|
||||
}
|
||||
const user = await UserModel.loadByVideoId(video.id)
|
||||
await addOptimizeOrMergeAudioJob({ video, videoFile: newFile, user, isNewVideo: false })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
@ -122,6 +116,7 @@ type TaskProcessorOptions <T extends VideoEditionTaskPayload = VideoEditionTaskP
|
|||
outputPath: string
|
||||
video: MVideo
|
||||
task: T
|
||||
lTags: { tags: string[] }
|
||||
}
|
||||
|
||||
const taskProcessors: { [id in VideoEditorTask['name']]: (options: TaskProcessorOptions) => Promise<any> } = {
|
||||
|
@ -134,7 +129,7 @@ const taskProcessors: { [id in VideoEditorTask['name']]: (options: TaskProcessor
|
|||
async function processTask (options: TaskProcessorOptions) {
|
||||
const { video, task } = options
|
||||
|
||||
logger.info('Processing %s task for video %s.', task.name, video.uuid, { task })
|
||||
logger.info('Processing %s task for video %s.', task.name, video.uuid, { task, ...options.lTags })
|
||||
|
||||
const processor = taskProcessors[options.task.name]
|
||||
if (!process) throw new Error('Unknown task ' + task.name)
|
||||
|
|
|
@ -28,7 +28,7 @@ async function processVideoFileImport (job: Job) {
|
|||
await updateVideoFile(video, payload.filePath)
|
||||
|
||||
if (CONFIG.OBJECT_STORAGE.ENABLED) {
|
||||
await addMoveToObjectStorageJob(video)
|
||||
await addMoveToObjectStorageJob({ video, previousVideoState: video.state })
|
||||
} else {
|
||||
await federateVideoIfNeeded(video, false)
|
||||
}
|
||||
|
|
|
@ -254,12 +254,12 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid
|
|||
}
|
||||
|
||||
if (video.state === VideoState.TO_MOVE_TO_EXTERNAL_STORAGE) {
|
||||
return addMoveToObjectStorageJob(videoImportUpdated.Video)
|
||||
return addMoveToObjectStorageJob({ video: videoImportUpdated.Video, previousVideoState: VideoState.TO_IMPORT })
|
||||
}
|
||||
|
||||
// Create transcoding jobs?
|
||||
if (video.state === VideoState.TO_TRANSCODE) {
|
||||
await addOptimizeOrMergeAudioJob(videoImportUpdated.Video, videoFile, videoImport.User)
|
||||
await addOptimizeOrMergeAudioJob({ video: videoImportUpdated.Video, videoFile, user: videoImport.User })
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
|
|
|
@ -133,7 +133,7 @@ async function saveLive (video: MVideo, live: MVideoLive, streamingPlaylist: MSt
|
|||
})
|
||||
}
|
||||
|
||||
await moveToNextState(videoWithFiles, false)
|
||||
await moveToNextState({ video: videoWithFiles, isNewVideo: false })
|
||||
}
|
||||
|
||||
async function cleanupTMPLiveFiles (hlsDirectory: string) {
|
||||
|
|
|
@ -168,7 +168,7 @@ async function onHlsPlaylistGeneration (video: MVideoFullLight, user: MUser, pay
|
|||
}
|
||||
|
||||
await VideoJobInfoModel.decrease(video.uuid, 'pendingTranscode')
|
||||
await retryTransactionWrapper(moveToNextState, video, payload.isNewVideo)
|
||||
await retryTransactionWrapper(moveToNextState, { video, isNewVideo: payload.isNewVideo })
|
||||
}
|
||||
|
||||
async function onVideoFirstWebTorrentTranscoding (
|
||||
|
@ -210,7 +210,7 @@ async function onVideoFirstWebTorrentTranscoding (
|
|||
|
||||
// Move to next state if there are no other resolutions to generate
|
||||
if (!hasHls && !hasNewResolutions) {
|
||||
await retryTransactionWrapper(moveToNextState, videoDatabase, payload.isNewVideo)
|
||||
await retryTransactionWrapper(moveToNextState, { video: videoDatabase, isNewVideo: payload.isNewVideo })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -225,7 +225,7 @@ async function onNewWebTorrentFileResolution (
|
|||
|
||||
await VideoJobInfoModel.decrease(video.uuid, 'pendingTranscode')
|
||||
|
||||
await retryTransactionWrapper(moveToNextState, video, payload.isNewVideo)
|
||||
await retryTransactionWrapper(moveToNextState, { video, isNewVideo: payload.isNewVideo })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
AbuseStateChangeForReporter,
|
||||
AutoFollowForInstance,
|
||||
CommentMention,
|
||||
EditionFinishedForOwner,
|
||||
FollowForInstance,
|
||||
FollowForUser,
|
||||
ImportFinishedForOwner,
|
||||
|
@ -53,7 +54,8 @@ class Notifier {
|
|||
abuseStateChange: [ AbuseStateChangeForReporter ],
|
||||
newAbuseMessage: [ NewAbuseMessageForReporter, NewAbuseMessageForModerators ],
|
||||
newPeertubeVersion: [ NewPeerTubeVersionForAdmins ],
|
||||
newPluginVersion: [ NewPluginVersionForAdmins ]
|
||||
newPluginVersion: [ NewPluginVersionForAdmins ],
|
||||
videoEditionFinished: [ EditionFinishedForOwner ]
|
||||
}
|
||||
|
||||
private static instance: Notifier
|
||||
|
@ -198,6 +200,13 @@ class Notifier {
|
|||
.catch(err => logger.error('Cannot notify on new plugin version %s.', plugin.name, { err }))
|
||||
}
|
||||
|
||||
notifyOfFinishedVideoEdition (video: MVideoFullLight) {
|
||||
const models = this.notificationModels.videoEditionFinished
|
||||
|
||||
this.sendNotifications(models, video)
|
||||
.catch(err => logger.error('Cannot notify on finished edition %s.', video.url, { err }))
|
||||
}
|
||||
|
||||
private async notify <T> (object: AbstractNotification<T>) {
|
||||
await object.prepare()
|
||||
|
||||
|
|
|
@ -46,7 +46,7 @@ export abstract class AbstractOwnedVideoPublication extends AbstractNotification
|
|||
subject: `Your video ${this.payload.name} has been published`,
|
||||
text: `Your video "${this.payload.name}" has been published.`,
|
||||
locals: {
|
||||
title: 'You video is live',
|
||||
title: 'Your video is live',
|
||||
action: {
|
||||
text: 'View video',
|
||||
url: videoUrl
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
import { logger } from '@server/helpers/logger'
|
||||
import { WEBSERVER } from '@server/initializers/constants'
|
||||
import { UserModel } from '@server/models/user/user'
|
||||
import { UserNotificationModel } from '@server/models/user/user-notification'
|
||||
import { MUserDefault, MUserWithNotificationSetting, MVideoFullLight, UserNotificationModelForApi } from '@server/types/models'
|
||||
import { UserNotificationType } from '@shared/models'
|
||||
import { AbstractNotification } from '../common/abstract-notification'
|
||||
|
||||
export class EditionFinishedForOwner extends AbstractNotification <MVideoFullLight> {
|
||||
private user: MUserDefault
|
||||
|
||||
async prepare () {
|
||||
this.user = await UserModel.loadByVideoId(this.payload.id)
|
||||
}
|
||||
|
||||
log () {
|
||||
logger.info('Notifying user %s its video edition %s is finished.', this.user.username, this.payload.url)
|
||||
}
|
||||
|
||||
getSetting (user: MUserWithNotificationSetting) {
|
||||
return user.NotificationSetting.myVideoEditionFinished
|
||||
}
|
||||
|
||||
getTargetUsers () {
|
||||
if (!this.user) return []
|
||||
|
||||
return [ this.user ]
|
||||
}
|
||||
|
||||
async createNotification (user: MUserWithNotificationSetting) {
|
||||
const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
|
||||
type: UserNotificationType.MY_VIDEO_EDITION_FINISHED,
|
||||
userId: user.id,
|
||||
videoId: this.payload.id
|
||||
})
|
||||
notification.Video = this.payload
|
||||
|
||||
return notification
|
||||
}
|
||||
|
||||
createEmail (to: string) {
|
||||
const videoUrl = WEBSERVER.URL + this.payload.getWatchStaticPath()
|
||||
|
||||
return {
|
||||
to,
|
||||
subject: `Edition of your video ${this.payload.name} has finished`,
|
||||
text: `Edition of your video ${this.payload.name} has finished.`,
|
||||
locals: {
|
||||
title: 'Video edition has finished',
|
||||
action: {
|
||||
text: 'View video',
|
||||
url: videoUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
export * from './new-video-for-subscribers'
|
||||
export * from './edition-finished-for-owner'
|
||||
export * from './import-finished-for-owner'
|
||||
export * from './owned-publication-after-auto-unblacklist'
|
||||
export * from './owned-publication-after-schedule-update'
|
||||
|
|
|
@ -252,7 +252,8 @@ function createDefaultUserNotificationSettings (user: MUserId, t: Transaction |
|
|||
abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
autoInstanceFollowing: UserNotificationSettingValue.WEB,
|
||||
newPeerTubeVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
newPluginVersion: UserNotificationSettingValue.WEB
|
||||
newPluginVersion: UserNotificationSettingValue.WEB,
|
||||
myVideoEditionFinished: UserNotificationSettingValue.WEB
|
||||
}
|
||||
|
||||
return UserNotificationSettingModel.create(values, { transaction: t })
|
||||
|
|
|
@ -16,6 +16,7 @@ function buildNextVideoState (currentState?: VideoState) {
|
|||
}
|
||||
|
||||
if (
|
||||
currentState !== VideoState.TO_EDIT &&
|
||||
currentState !== VideoState.TO_TRANSCODE &&
|
||||
currentState !== VideoState.TO_MOVE_TO_EXTERNAL_STORAGE &&
|
||||
CONFIG.TRANSCODING.ENABLED
|
||||
|
@ -33,7 +34,13 @@ function buildNextVideoState (currentState?: VideoState) {
|
|||
return VideoState.PUBLISHED
|
||||
}
|
||||
|
||||
function moveToNextState (video: MVideoUUID, isNewVideo = true) {
|
||||
function moveToNextState (options: {
|
||||
video: MVideoUUID
|
||||
previousVideoState?: VideoState
|
||||
isNewVideo?: boolean // Default true
|
||||
}) {
|
||||
const { video, previousVideoState, isNewVideo = true } = options
|
||||
|
||||
return sequelizeTypescript.transaction(async t => {
|
||||
// Maybe the video changed in database, refresh it
|
||||
const videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t)
|
||||
|
@ -48,28 +55,35 @@ function moveToNextState (video: MVideoUUID, isNewVideo = true) {
|
|||
const newState = buildNextVideoState(videoDatabase.state)
|
||||
|
||||
if (newState === VideoState.PUBLISHED) {
|
||||
return moveToPublishedState(videoDatabase, isNewVideo, t)
|
||||
return moveToPublishedState({ video: videoDatabase, previousVideoState, isNewVideo, transaction: t })
|
||||
}
|
||||
|
||||
if (newState === VideoState.TO_MOVE_TO_EXTERNAL_STORAGE) {
|
||||
return moveToExternalStorageState(videoDatabase, isNewVideo, t)
|
||||
return moveToExternalStorageState({ video: videoDatabase, isNewVideo, transaction: t })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function moveToExternalStorageState (video: MVideoFullLight, isNewVideo: boolean, transaction: Transaction) {
|
||||
async function moveToExternalStorageState (options: {
|
||||
video: MVideoFullLight
|
||||
isNewVideo: boolean
|
||||
transaction: Transaction
|
||||
}) {
|
||||
const { video, isNewVideo, transaction } = options
|
||||
|
||||
const videoJobInfo = await VideoJobInfoModel.load(video.id, transaction)
|
||||
const pendingTranscode = videoJobInfo?.pendingTranscode || 0
|
||||
|
||||
// We want to wait all transcoding jobs before moving the video on an external storage
|
||||
if (pendingTranscode !== 0) return false
|
||||
|
||||
const previousVideoState = video.state
|
||||
await video.setNewState(VideoState.TO_MOVE_TO_EXTERNAL_STORAGE, isNewVideo, transaction)
|
||||
|
||||
logger.info('Creating external storage move job for video %s.', video.uuid, { tags: [ video.uuid ] })
|
||||
|
||||
try {
|
||||
await addMoveToObjectStorageJob(video, isNewVideo)
|
||||
await addMoveToObjectStorageJob({ video, previousVideoState, isNewVideo })
|
||||
|
||||
return true
|
||||
} catch (err) {
|
||||
|
@ -103,21 +117,33 @@ export {
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function moveToPublishedState (video: MVideoFullLight, isNewVideo: boolean, transaction: Transaction) {
|
||||
logger.info('Publishing video %s.', video.uuid, { tags: [ video.uuid ] })
|
||||
async function moveToPublishedState (options: {
|
||||
video: MVideoFullLight
|
||||
isNewVideo: boolean
|
||||
transaction: Transaction
|
||||
previousVideoState?: VideoState
|
||||
}) {
|
||||
const { video, isNewVideo, transaction, previousVideoState } = options
|
||||
const previousState = previousVideoState ?? video.state
|
||||
|
||||
logger.info('Publishing video %s.', video.uuid, { previousState, tags: [ video.uuid ] })
|
||||
|
||||
const previousState = video.state
|
||||
await video.setNewState(VideoState.PUBLISHED, isNewVideo, transaction)
|
||||
|
||||
// If the video was not published, we consider it is a new one for other instances
|
||||
// Live videos are always federated, so it's not a new video
|
||||
await federateVideoIfNeeded(video, isNewVideo, transaction)
|
||||
|
||||
if (!isNewVideo) return
|
||||
if (previousState === VideoState.TO_EDIT) {
|
||||
Notifier.Instance.notifyOfFinishedVideoEdition(video)
|
||||
return
|
||||
}
|
||||
|
||||
Notifier.Instance.notifyOnNewVideoIfNeeded(video)
|
||||
if (isNewVideo) {
|
||||
Notifier.Instance.notifyOnNewVideoIfNeeded(video)
|
||||
|
||||
if (previousState === VideoState.TO_TRANSCODE) {
|
||||
Notifier.Instance.notifyOnVideoPublishedAfterTranscoding(video)
|
||||
if (previousState === VideoState.TO_TRANSCODE) {
|
||||
Notifier.Instance.notifyOnVideoPublishedAfterTranscoding(video)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import { VideoModel } from '@server/models/video/video'
|
|||
import { VideoJobInfoModel } from '@server/models/video/video-job-info'
|
||||
import { FilteredModelAttributes } from '@server/types'
|
||||
import { MThumbnail, MUserId, MVideoFile, MVideoTag, MVideoThumbnail, MVideoUUID } from '@server/types/models'
|
||||
import { ThumbnailType, VideoCreate, VideoPrivacy, VideoTranscodingPayload } from '@shared/models'
|
||||
import { ThumbnailType, VideoCreate, VideoPrivacy, VideoState, VideoTranscodingPayload } from '@shared/models'
|
||||
import { CreateJobOptions, JobQueue } from './job-queue/job-queue'
|
||||
import { updateVideoMiniatureFromExisting } from './thumbnail'
|
||||
import { CONFIG } from '@server/initializers/config'
|
||||
|
@ -67,6 +67,8 @@ async function buildVideoThumbnailsFromReq (options: {
|
|||
return Promise.all(promises)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function setVideoTags (options: {
|
||||
video: MVideoTag
|
||||
tags: string[]
|
||||
|
@ -81,7 +83,16 @@ async function setVideoTags (options: {
|
|||
video.Tags = tagInstances
|
||||
}
|
||||
|
||||
async function addOptimizeOrMergeAudioJob (video: MVideoUUID, videoFile: MVideoFile, user: MUserId, isNewVideo = true) {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function addOptimizeOrMergeAudioJob (options: {
|
||||
video: MVideoUUID
|
||||
videoFile: MVideoFile
|
||||
user: MUserId
|
||||
isNewVideo?: boolean // Default true
|
||||
}) {
|
||||
const { video, videoFile, user, isNewVideo } = options
|
||||
|
||||
let dataInput: VideoTranscodingPayload
|
||||
|
||||
if (videoFile.isAudio()) {
|
||||
|
@ -113,13 +124,6 @@ async function addTranscodingJob (payload: VideoTranscodingPayload, options: Cre
|
|||
return JobQueue.Instance.createJobWithPromise({ type: 'video-transcoding', payload: payload }, options)
|
||||
}
|
||||
|
||||
async function addMoveToObjectStorageJob (video: MVideoUUID, isNewVideo = true) {
|
||||
await VideoJobInfoModel.increaseOrCreate(video.uuid, 'pendingMove')
|
||||
|
||||
const dataInput = { videoUUID: video.uuid, isNewVideo }
|
||||
return JobQueue.Instance.createJobWithPromise({ type: 'move-to-object-storage', payload: dataInput })
|
||||
}
|
||||
|
||||
async function getTranscodingJobPriority (user: MUserId) {
|
||||
const now = new Date()
|
||||
const lastWeek = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 7)
|
||||
|
@ -131,6 +135,21 @@ async function getTranscodingJobPriority (user: MUserId) {
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function addMoveToObjectStorageJob (options: {
|
||||
video: MVideoUUID
|
||||
previousVideoState: VideoState
|
||||
isNewVideo?: boolean // Default true
|
||||
}) {
|
||||
const { video, previousVideoState, isNewVideo = true } = options
|
||||
|
||||
await VideoJobInfoModel.increaseOrCreate(video.uuid, 'pendingMove')
|
||||
|
||||
const dataInput = { videoUUID: video.uuid, isNewVideo, previousVideoState }
|
||||
return JobQueue.Instance.createJobWithPromise({ type: 'move-to-object-storage', payload: dataInput })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
buildLocalVideoFromReq,
|
||||
buildVideoThumbnailsFromReq,
|
||||
|
|
|
@ -175,6 +175,15 @@ export class UserNotificationSettingModel extends Model<Partial<AttributesOnly<U
|
|||
@Column
|
||||
newPluginVersion: UserNotificationSettingValue
|
||||
|
||||
@AllowNull(false)
|
||||
@Default(null)
|
||||
@Is(
|
||||
'UserNotificationSettingMyVideoEditionFinished',
|
||||
value => throwIfNotValid(value, isUserNotificationSettingValid, 'myVideoEditionFinished')
|
||||
)
|
||||
@Column
|
||||
myVideoEditionFinished: UserNotificationSettingValue
|
||||
|
||||
@ForeignKey(() => UserModel)
|
||||
@Column
|
||||
userId: number
|
||||
|
@ -216,6 +225,7 @@ export class UserNotificationSettingModel extends Model<Partial<AttributesOnly<U
|
|||
abuseNewMessage: this.abuseNewMessage,
|
||||
abuseStateChange: this.abuseStateChange,
|
||||
newPeerTubeVersion: this.newPeerTubeVersion,
|
||||
myVideoEditionFinished: this.myVideoEditionFinished,
|
||||
newPluginVersion: this.newPluginVersion
|
||||
}
|
||||
}
|
||||
|
|
|
@ -171,6 +171,7 @@ describe('Test user notifications API validators', function () {
|
|||
abuseNewMessage: UserNotificationSettingValue.WEB,
|
||||
abuseStateChange: UserNotificationSettingValue.WEB,
|
||||
newPeerTubeVersion: UserNotificationSettingValue.WEB,
|
||||
myVideoEditionFinished: UserNotificationSettingValue.WEB,
|
||||
newPluginVersion: UserNotificationSettingValue.WEB
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
checkMyVideoImportIsFinished,
|
||||
checkNewActorFollow,
|
||||
checkNewVideoFromSubscription,
|
||||
checkVideoEditionIsFinished,
|
||||
checkVideoIsPublished,
|
||||
FIXTURE_URLS,
|
||||
MockSmtpServer,
|
||||
|
@ -15,7 +16,7 @@ import {
|
|||
} from '@server/tests/shared'
|
||||
import { wait } from '@shared/core-utils'
|
||||
import { buildUUID } from '@shared/extra-utils'
|
||||
import { UserNotification, UserNotificationType, VideoPrivacy } from '@shared/models'
|
||||
import { UserNotification, UserNotificationType, VideoEditorTask, VideoPrivacy } from '@shared/models'
|
||||
import { cleanupTests, PeerTubeServer, waitJobs } from '@shared/server-commands'
|
||||
|
||||
const expect = chai.expect
|
||||
|
@ -23,10 +24,12 @@ const expect = chai.expect
|
|||
describe('Test user notifications', function () {
|
||||
let servers: PeerTubeServer[] = []
|
||||
let userAccessToken: string
|
||||
|
||||
let userNotifications: UserNotification[] = []
|
||||
let adminNotifications: UserNotification[] = []
|
||||
let adminNotificationsServer2: UserNotification[] = []
|
||||
let emails: object[] = []
|
||||
|
||||
let channelId: number
|
||||
|
||||
before(async function () {
|
||||
|
@ -320,6 +323,42 @@ describe('Test user notifications', function () {
|
|||
})
|
||||
})
|
||||
|
||||
describe('Video editor', function () {
|
||||
let baseParams: CheckerBaseParams
|
||||
|
||||
before(() => {
|
||||
baseParams = {
|
||||
server: servers[1],
|
||||
emails,
|
||||
socketNotifications: adminNotificationsServer2,
|
||||
token: servers[1].accessToken
|
||||
}
|
||||
})
|
||||
|
||||
it('Should send a notification after editor edition', async function () {
|
||||
this.timeout(240000)
|
||||
|
||||
const { name, shortUUID, id } = await uploadRandomVideoOnServers(servers, 2, { waitTranscoding: true })
|
||||
|
||||
await waitJobs(servers)
|
||||
await checkVideoIsPublished({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' })
|
||||
|
||||
const tasks: VideoEditorTask[] = [
|
||||
{
|
||||
name: 'cut',
|
||||
options: {
|
||||
start: 0,
|
||||
end: 1
|
||||
}
|
||||
}
|
||||
]
|
||||
await servers[1].videoEditor.createEditionTasks({ videoId: id, tasks })
|
||||
await waitJobs(servers)
|
||||
|
||||
await checkVideoEditionIsFinished({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('My video is imported', function () {
|
||||
let baseParams: CheckerBaseParams
|
||||
|
||||
|
|
|
@ -56,13 +56,7 @@ describe('Test video editor', function () {
|
|||
|
||||
await servers[0].config.enableMinimumTranscoding()
|
||||
|
||||
await servers[0].config.updateExistingSubConfig({
|
||||
newConfig: {
|
||||
videoEditor: {
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
})
|
||||
await servers[0].config.enableEditor()
|
||||
})
|
||||
|
||||
describe('Cutting', function () {
|
||||
|
|
|
@ -47,6 +47,7 @@ function getAllNotificationsSettings (): UserNotificationSetting {
|
|||
abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
autoInstanceFollowing: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
newPeerTubeVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
myVideoEditionFinished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
newPluginVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
|
||||
}
|
||||
}
|
||||
|
@ -109,6 +110,34 @@ async function checkVideoIsPublished (options: CheckerBaseParams & {
|
|||
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||
}
|
||||
|
||||
async function checkVideoEditionIsFinished (options: CheckerBaseParams & {
|
||||
videoName: string
|
||||
shortUUID: string
|
||||
checkType: CheckerType
|
||||
}) {
|
||||
const { videoName, shortUUID } = options
|
||||
const notificationType = UserNotificationType.MY_VIDEO_EDITION_FINISHED
|
||||
|
||||
function notificationChecker (notification: UserNotification, checkType: CheckerType) {
|
||||
if (checkType === 'presence') {
|
||||
expect(notification).to.not.be.undefined
|
||||
expect(notification.type).to.equal(notificationType)
|
||||
|
||||
checkVideo(notification.video, videoName, shortUUID)
|
||||
checkActor(notification.video.channel)
|
||||
} else {
|
||||
expect(notification.video).to.satisfy(v => v === undefined || v.name !== videoName)
|
||||
}
|
||||
}
|
||||
|
||||
function emailNotificationFinder (email: object) {
|
||||
const text: string = email['text']
|
||||
return text.includes(shortUUID) && text.includes('Edition of your video')
|
||||
}
|
||||
|
||||
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||
}
|
||||
|
||||
async function checkMyVideoImportIsFinished (options: CheckerBaseParams & {
|
||||
videoName: string
|
||||
shortUUID: string
|
||||
|
@ -656,6 +685,8 @@ async function prepareNotificationsTest (serversCount = 3, overrideConfigArg: an
|
|||
await setDefaultChannelAvatar(servers)
|
||||
await setDefaultAccountAvatar(servers)
|
||||
|
||||
await servers[1].config.enableEditor()
|
||||
|
||||
if (serversCount > 1) {
|
||||
await doubleFollow(servers[0], servers[1])
|
||||
}
|
||||
|
@ -724,7 +755,8 @@ export {
|
|||
checkNewCommentAbuseForModerators,
|
||||
checkNewAccountAbuseForModerators,
|
||||
checkNewPeerTubeVersion,
|
||||
checkNewPluginVersion
|
||||
checkNewPluginVersion,
|
||||
checkVideoEditionIsFinished
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { ContextType } from '../activitypub/context'
|
||||
import { VideoState } from '../videos'
|
||||
import { VideoEditorTaskCut } from '../videos/editor'
|
||||
import { VideoResolution } from '../videos/file/video-resolution.enum'
|
||||
import { SendEmailOptions } from './emailer.model'
|
||||
|
@ -116,6 +117,9 @@ export type ManageVideoTorrentPayload =
|
|||
interface BaseTranscodingPayload {
|
||||
videoUUID: string
|
||||
isNewVideo?: boolean
|
||||
|
||||
// Custom notification when the task is finished
|
||||
notification?: 'default' | 'video-edition'
|
||||
}
|
||||
|
||||
export interface HLSTranscodingPayload extends BaseTranscodingPayload {
|
||||
|
@ -171,6 +175,7 @@ export interface DeleteResumableUploadMetaFilePayload {
|
|||
export interface MoveObjectStoragePayload {
|
||||
videoUUID: string
|
||||
isNewVideo: boolean
|
||||
previousVideoState: VideoState
|
||||
}
|
||||
|
||||
export type VideoEditorTaskCutPayload = VideoEditorTaskCut
|
||||
|
|
|
@ -27,4 +27,6 @@ export interface UserNotificationSetting {
|
|||
|
||||
newPeerTubeVersion: UserNotificationSettingValue
|
||||
newPluginVersion: UserNotificationSettingValue
|
||||
|
||||
myVideoEditionFinished: UserNotificationSettingValue
|
||||
}
|
||||
|
|
|
@ -30,7 +30,9 @@ export const enum UserNotificationType {
|
|||
ABUSE_NEW_MESSAGE = 16,
|
||||
|
||||
NEW_PLUGIN_VERSION = 17,
|
||||
NEW_PEERTUBE_VERSION = 18
|
||||
NEW_PEERTUBE_VERSION = 18,
|
||||
|
||||
MY_VIDEO_EDITION_FINISHED = 19
|
||||
}
|
||||
|
||||
export interface VideoInfo {
|
||||
|
|
|
@ -111,6 +111,16 @@ export class ConfigCommand extends AbstractCommand {
|
|||
})
|
||||
}
|
||||
|
||||
enableEditor () {
|
||||
return this.updateExistingSubConfig({
|
||||
newConfig: {
|
||||
videoEditor: {
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
getConfig (options: OverrideCommandOptions = {}) {
|
||||
const path = '/api/v1/config'
|
||||
|
||||
|
|
Loading…
Reference in New Issue