From 1808a1f8e4b7b102823492a2007a46929aebf189 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 22 Mar 2022 14:35:04 +0100 Subject: [PATCH] Add video edition finished notification --- ...ount-notification-preferences.component.ts | 6 +- .../users/user-notification.model.ts | 4 ++ .../users/user-notifications.component.html | 10 +++- scripts/create-move-video-storage-job.ts | 2 +- .../controllers/api/users/my-notifications.ts | 3 +- server/controllers/api/videos/upload.ts | 4 +- server/initializers/constants.ts | 2 +- .../0700-edition-finished-notification.ts | 42 ++++++++++++++ .../handlers/move-to-object-storage.ts | 14 +++-- .../lib/job-queue/handlers/video-edition.ts | 29 ++++------ .../job-queue/handlers/video-file-import.ts | 2 +- server/lib/job-queue/handlers/video-import.ts | 4 +- .../job-queue/handlers/video-live-ending.ts | 2 +- .../job-queue/handlers/video-transcoding.ts | 6 +- server/lib/notifier/notifier.ts | 11 +++- .../abstract-owned-video-publication.ts | 2 +- .../edition-finished-for-owner.ts | 57 +++++++++++++++++++ .../shared/video-publication/index.ts | 1 + server/lib/user.ts | 3 +- server/lib/video-state.ts | 50 ++++++++++++---- server/lib/video.ts | 37 +++++++++--- .../models/user/user-notification-setting.ts | 10 ++++ .../api/check-params/user-notifications.ts | 1 + .../api/notifications/user-notifications.ts | 41 ++++++++++++- server/tests/api/transcoding/video-editor.ts | 8 +-- server/tests/shared/notifications.ts | 34 ++++++++++- shared/models/server/job.model.ts | 5 ++ .../users/user-notification-setting.model.ts | 2 + .../models/users/user-notification.model.ts | 4 +- .../server-commands/server/config-command.ts | 10 ++++ 30 files changed, 336 insertions(+), 70 deletions(-) create mode 100644 server/initializers/migrations/0700-edition-finished-notification.ts create mode 100644 server/lib/notifier/shared/video-publication/edition-finished-for-owner.ts 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 @@
- A new version of PeerTube is available: {{ notification.peertube.latestVersion }} + A new version of PeerTube is available: {{ notification.peertube.latestVersion }} +
+ + + + + +
+ Your video {{ notification.video.name }} edition has finished
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' -- 2.41.0