diff --git a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts
index 09da979ab..187a3818a 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts
+++ b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts
@@ -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'
]
},
diff --git a/client/src/app/shared/shared-main/users/user-notification.model.ts b/client/src/app/shared/shared-main/users/user-notification.model.ts
index 1eb69d5a2..d1b36f347 100644
--- a/client/src/app/shared/shared-main/users/user-notification.model.ts
+++ b/client/src/app/shared/shared-main/users/user-notification.model.ts
@@ -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
diff --git a/client/src/app/shared/shared-main/users/user-notifications.component.html b/client/src/app/shared/shared-main/users/user-notifications.component.html
index 9af6da784..ff1259fb8 100644
--- a/client/src/app/shared/shared-main/users/user-notifications.component.html
+++ b/client/src/app/shared/shared-main/users/user-notifications.component.html
@@ -203,7 +203,15 @@
+
+
+
+
+
+
diff --git a/scripts/create-move-video-storage-job.ts b/scripts/create-move-video-storage-job.ts
index 7465c1ce0..18629aa27 100644
--- a/scripts/create-move-video-storage-job.ts
+++ b/scripts/create-move-video-storage-job.ts
@@ -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(
diff --git a/server/controllers/api/users/my-notifications.ts b/server/controllers/api/users/my-notifications.ts
index 58732158f..55184dc0f 100644
--- a/server/controllers/api/users/my-notifications.ts
+++ b/server/controllers/api/users/my-notifications.ts
@@ -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)
diff --git a/server/controllers/api/videos/upload.ts b/server/controllers/api/videos/upload.ts
index 14ae9d920..3afbedbb2 100644
--- a/server/controllers/api/videos/upload.ts
+++ b/server/controllers/api/videos/upload.ts
@@ -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) }))
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index aaf39e6ec..17d8ba556 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -24,7 +24,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
// ---------------------------------------------------------------------------
-const LAST_MIGRATION_VERSION = 695
+const LAST_MIGRATION_VERSION = 700
// ---------------------------------------------------------------------------
diff --git a/server/initializers/migrations/0700-edition-finished-notification.ts b/server/initializers/migrations/0700-edition-finished-notification.ts
new file mode 100644
index 000000000..103c0b456
--- /dev/null
+++ b/server/initializers/migrations/0700-edition-finished-notification.ts
@@ -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 {
+ 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
+}
diff --git a/server/lib/job-queue/handlers/move-to-object-storage.ts b/server/lib/job-queue/handlers/move-to-object-storage.ts
index 69b441176..f480b32cd 100644
--- a/server/lib/job-queue/handlers/move-to-object-storage.ts
+++ b/server/lib/job-queue/handlers/move-to-object-storage.ts
@@ -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: {
diff --git a/server/lib/job-queue/handlers/video-edition.ts b/server/lib/job-queue/handlers/video-edition.ts
index c5ba0452f..d2d2a4f65 100644
--- a/server/lib/job-queue/handlers/video-edition.ts
+++ b/server/lib/job-queue/handlers/video-edition.ts
@@ -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 Promise } = {
@@ -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)
diff --git a/server/lib/job-queue/handlers/video-file-import.ts b/server/lib/job-queue/handlers/video-file-import.ts
index 6b2d60317..110176d81 100644
--- a/server/lib/job-queue/handlers/video-file-import.ts
+++ b/server/lib/job-queue/handlers/video-file-import.ts
@@ -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)
}
diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts
index b3ca28c2f..d59a1b12f 100644
--- a/server/lib/job-queue/handlers/video-import.ts
+++ b/server/lib/job-queue/handlers/video-import.ts
@@ -254,12 +254,12 @@ async function processFile (downloader: () => Promise, 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) {
diff --git a/server/lib/job-queue/handlers/video-live-ending.ts b/server/lib/job-queue/handlers/video-live-ending.ts
index 497f6612a..f4de4b47c 100644
--- a/server/lib/job-queue/handlers/video-live-ending.ts
+++ b/server/lib/job-queue/handlers/video-live-ending.ts
@@ -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) {
diff --git a/server/lib/job-queue/handlers/video-transcoding.ts b/server/lib/job-queue/handlers/video-transcoding.ts
index 512979734..95ee6b384 100644
--- a/server/lib/job-queue/handlers/video-transcoding.ts
+++ b/server/lib/job-queue/handlers/video-transcoding.ts
@@ -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 })
}
// ---------------------------------------------------------------------------
diff --git a/server/lib/notifier/notifier.ts b/server/lib/notifier/notifier.ts
index 8b68d2e69..e34a82603 100644
--- a/server/lib/notifier/notifier.ts
+++ b/server/lib/notifier/notifier.ts
@@ -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 (object: AbstractNotification) {
await object.prepare()
diff --git a/server/lib/notifier/shared/video-publication/abstract-owned-video-publication.ts b/server/lib/notifier/shared/video-publication/abstract-owned-video-publication.ts
index fd06e080d..37435f898 100644
--- a/server/lib/notifier/shared/video-publication/abstract-owned-video-publication.ts
+++ b/server/lib/notifier/shared/video-publication/abstract-owned-video-publication.ts
@@ -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
diff --git a/server/lib/notifier/shared/video-publication/edition-finished-for-owner.ts b/server/lib/notifier/shared/video-publication/edition-finished-for-owner.ts
new file mode 100644
index 000000000..dec91f574
--- /dev/null
+++ b/server/lib/notifier/shared/video-publication/edition-finished-for-owner.ts
@@ -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 {
+ 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({
+ 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
+ }
+ }
+ }
+ }
+}
diff --git a/server/lib/notifier/shared/video-publication/index.ts b/server/lib/notifier/shared/video-publication/index.ts
index 940774504..57f3443b9 100644
--- a/server/lib/notifier/shared/video-publication/index.ts
+++ b/server/lib/notifier/shared/video-publication/index.ts
@@ -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'
diff --git a/server/lib/user.ts b/server/lib/user.ts
index ea755f4be..173d89d0b 100644
--- a/server/lib/user.ts
+++ b/server/lib/user.ts
@@ -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 })
diff --git a/server/lib/video-state.ts b/server/lib/video-state.ts
index 97ff540ed..f75f81704 100644
--- a/server/lib/video-state.ts
+++ b/server/lib/video-state.ts
@@ -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)
+ }
}
}
diff --git a/server/lib/video.ts b/server/lib/video.ts
index ec4256c1a..a98e45c60 100644
--- a/server/lib/video.ts
+++ b/server/lib/video.ts
@@ -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,
diff --git a/server/models/user/user-notification-setting.ts b/server/models/user/user-notification-setting.ts
index f03b19e41..b144f8377 100644
--- a/server/models/user/user-notification-setting.ts
+++ b/server/models/user/user-notification-setting.ts
@@ -175,6 +175,15 @@ export class UserNotificationSettingModel extends Model throwIfNotValid(value, isUserNotificationSettingValid, 'myVideoEditionFinished')
+ )
+ @Column
+ myVideoEditionFinished: UserNotificationSettingValue
+
@ForeignKey(() => UserModel)
@Column
userId: number
@@ -216,6 +225,7 @@ export class UserNotificationSettingModel extends Model {
+ 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
diff --git a/server/tests/api/transcoding/video-editor.ts b/server/tests/api/transcoding/video-editor.ts
index a9b6950cc..f70bd49e6 100644
--- a/server/tests/api/transcoding/video-editor.ts
+++ b/server/tests/api/transcoding/video-editor.ts
@@ -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 () {
diff --git a/server/tests/shared/notifications.ts b/server/tests/shared/notifications.ts
index 78d3787f0..f1ddbbbf7 100644
--- a/server/tests/shared/notifications.ts
+++ b/server/tests/shared/notifications.ts
@@ -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
}
// ---------------------------------------------------------------------------
diff --git a/shared/models/server/job.model.ts b/shared/models/server/job.model.ts
index d81b72696..3b4855eaa 100644
--- a/shared/models/server/job.model.ts
+++ b/shared/models/server/job.model.ts
@@ -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
diff --git a/shared/models/users/user-notification-setting.model.ts b/shared/models/users/user-notification-setting.model.ts
index 977e6b985..35656f14c 100644
--- a/shared/models/users/user-notification-setting.model.ts
+++ b/shared/models/users/user-notification-setting.model.ts
@@ -27,4 +27,6 @@ export interface UserNotificationSetting {
newPeerTubeVersion: UserNotificationSettingValue
newPluginVersion: UserNotificationSettingValue
+
+ myVideoEditionFinished: UserNotificationSettingValue
}
diff --git a/shared/models/users/user-notification.model.ts b/shared/models/users/user-notification.model.ts
index a2621fb5b..a2918194f 100644
--- a/shared/models/users/user-notification.model.ts
+++ b/shared/models/users/user-notification.model.ts
@@ -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 {
diff --git a/shared/server-commands/server/config-command.ts b/shared/server-commands/server/config-command.ts
index 1dd6e1ea4..35a1eec7c 100644
--- a/shared/server-commands/server/config-command.ts
+++ b/shared/server-commands/server/config-command.ts
@@ -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'