aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2022-03-22 14:35:04 +0100
committerChocobozzz <me@florianbigard.com>2022-03-22 16:25:14 +0100
commit1808a1f8e4b7b102823492a2007a46929aebf189 (patch)
treea345140ec9a7a20c222ace3cda18ac999277c8c3
parent348c2ce3ff3fe2f25a31f08bfb36c88723a0ce46 (diff)
downloadPeerTube-1808a1f8e4b7b102823492a2007a46929aebf189.tar.gz
PeerTube-1808a1f8e4b7b102823492a2007a46929aebf189.tar.zst
PeerTube-1808a1f8e4b7b102823492a2007a46929aebf189.zip
Add video edition finished notification
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts6
-rw-r--r--client/src/app/shared/shared-main/users/user-notification.model.ts4
-rw-r--r--client/src/app/shared/shared-main/users/user-notifications.component.html10
-rw-r--r--scripts/create-move-video-storage-job.ts2
-rw-r--r--server/controllers/api/users/my-notifications.ts3
-rw-r--r--server/controllers/api/videos/upload.ts4
-rw-r--r--server/initializers/constants.ts2
-rw-r--r--server/initializers/migrations/0700-edition-finished-notification.ts42
-rw-r--r--server/lib/job-queue/handlers/move-to-object-storage.ts14
-rw-r--r--server/lib/job-queue/handlers/video-edition.ts29
-rw-r--r--server/lib/job-queue/handlers/video-file-import.ts2
-rw-r--r--server/lib/job-queue/handlers/video-import.ts4
-rw-r--r--server/lib/job-queue/handlers/video-live-ending.ts2
-rw-r--r--server/lib/job-queue/handlers/video-transcoding.ts6
-rw-r--r--server/lib/notifier/notifier.ts11
-rw-r--r--server/lib/notifier/shared/video-publication/abstract-owned-video-publication.ts2
-rw-r--r--server/lib/notifier/shared/video-publication/edition-finished-for-owner.ts57
-rw-r--r--server/lib/notifier/shared/video-publication/index.ts1
-rw-r--r--server/lib/user.ts3
-rw-r--r--server/lib/video-state.ts50
-rw-r--r--server/lib/video.ts37
-rw-r--r--server/models/user/user-notification-setting.ts10
-rw-r--r--server/tests/api/check-params/user-notifications.ts1
-rw-r--r--server/tests/api/notifications/user-notifications.ts41
-rw-r--r--server/tests/api/transcoding/video-editor.ts8
-rw-r--r--server/tests/shared/notifications.ts34
-rw-r--r--shared/models/server/job.model.ts5
-rw-r--r--shared/models/users/user-notification-setting.model.ts2
-rw-r--r--shared/models/users/user-notification.model.ts4
-rw-r--r--shared/server-commands/server/config-command.ts10
30 files changed, 336 insertions, 70 deletions
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 {
44 abuseNewMessage: $localize`An abuse report received a new message`, 44 abuseNewMessage: $localize`An abuse report received a new message`,
45 abuseStateChange: $localize`One of your abuse reports has been accepted or rejected by moderators`, 45 abuseStateChange: $localize`One of your abuse reports has been accepted or rejected by moderators`,
46 newPeerTubeVersion: $localize`A new PeerTube version is available`, 46 newPeerTubeVersion: $localize`A new PeerTube version is available`,
47 newPluginVersion: $localize`One of your plugin/theme has a new available version` 47 newPluginVersion: $localize`One of your plugin/theme has a new available version`,
48 myVideoEditionFinished: $localize`Video edition finished`
48 } 49 }
49 this.notificationSettingGroups = [ 50 this.notificationSettingGroups = [
50 { 51 {
@@ -62,7 +63,8 @@ export class MyAccountNotificationPreferencesComponent implements OnInit {
62 'newCommentOnMyVideo', 63 'newCommentOnMyVideo',
63 'blacklistOnMyVideo', 64 'blacklistOnMyVideo',
64 'myVideoPublished', 65 'myVideoPublished',
65 'myVideoImportFinished' 66 'myVideoImportFinished',
67 'myVideoEditionFinished'
66 ] 68 ]
67 }, 69 },
68 70
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 {
227 this.pluginUrl = `/admin/plugins/list-installed` 227 this.pluginUrl = `/admin/plugins/list-installed`
228 this.pluginQueryParams.pluginType = this.plugin.type + '' 228 this.pluginQueryParams.pluginType = this.plugin.type + ''
229 break 229 break
230
231 case UserNotificationType.MY_VIDEO_EDITION_FINISHED:
232 this.videoUrl = this.buildVideoUrl(this.video)
233 break
230 } 234 }
231 } catch (err) { 235 } catch (err) {
232 this.type = null 236 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 @@
203 <my-global-icon iconName="cog" aria-hidden="true"></my-global-icon> 203 <my-global-icon iconName="cog" aria-hidden="true"></my-global-icon>
204 204
205 <div class="message" i18n> 205 <div class="message" i18n>
206 <a (click)="markAsRead(notification)" [href]="notification.peertubeVersionLink" target="_blank" rel="noopener noreferrer">A new version of PeerTube</a> is available: {{ notification.peertube.latestVersion }} 206 <a (click)="markAsRead(notification)" [href]="notification.peertubeVersionLink" target="_blank" rel="noopener noreferrer">A new version of PeerTube</a> is available: {{ notification.peertube.latestVersion }}
207 </div>
208 </ng-container>
209
210 <ng-container *ngSwitchCase="19"> <!-- UserNotificationType.MY_VIDEO_EDITION_FINISHED -->
211 <my-global-icon iconName="film" aria-hidden="true"></my-global-icon>
212
213 <div class="message" i18n>
214 Your video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.video.name }}</a> edition has finished
207 </div> 215 </div>
208 </ng-container> 216 </ng-container>
209 217
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 () {
78 if (files.some(f => f.storage === VideoStorage.FILE_SYSTEM) || hls?.storage === VideoStorage.FILE_SYSTEM) { 78 if (files.some(f => f.storage === VideoStorage.FILE_SYSTEM) || hls?.storage === VideoStorage.FILE_SYSTEM) {
79 console.log('Processing video %s.', videoFull.name) 79 console.log('Processing video %s.', videoFull.name)
80 80
81 const success = await moveToExternalStorageState(videoFull, false, undefined) 81 const success = await moveToExternalStorageState({ video: videoFull, isNewVideo: false, transaction: undefined })
82 82
83 if (!success) { 83 if (!success) {
84 console.error( 84 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
82 abuseNewMessage: body.abuseNewMessage, 82 abuseNewMessage: body.abuseNewMessage,
83 abuseStateChange: body.abuseStateChange, 83 abuseStateChange: body.abuseStateChange,
84 newPeerTubeVersion: body.newPeerTubeVersion, 84 newPeerTubeVersion: body.newPeerTubeVersion,
85 newPluginVersion: body.newPluginVersion 85 newPluginVersion: body.newPluginVersion,
86 myVideoEditionFinished: body.myVideoEditionFinished
86 } 87 }
87 88
88 await UserNotificationSettingModel.update(values, query) 89 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: {
218 if (!refreshedVideo) return 218 if (!refreshedVideo) return
219 219
220 if (refreshedVideo.state === VideoState.TO_MOVE_TO_EXTERNAL_STORAGE) { 220 if (refreshedVideo.state === VideoState.TO_MOVE_TO_EXTERNAL_STORAGE) {
221 return addMoveToObjectStorageJob(refreshedVideo) 221 return addMoveToObjectStorageJob({ video: refreshedVideo, previousVideoState: undefined })
222 } 222 }
223 223
224 if (refreshedVideo.state === VideoState.TO_TRANSCODE) { 224 if (refreshedVideo.state === VideoState.TO_TRANSCODE) {
225 return addOptimizeOrMergeAudioJob(refreshedVideo, videoFile, user) 225 return addOptimizeOrMergeAudioJob({ video: refreshedVideo, videoFile, user })
226 } 226 }
227 }).catch(err => logger.error('Cannot add optimize/merge audio job for %s.', videoCreated.uuid, { err, ...lTags(videoCreated.uuid) })) 227 }).catch(err => logger.error('Cannot add optimize/merge audio job for %s.', videoCreated.uuid, { err, ...lTags(videoCreated.uuid) }))
228 228
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'
24 24
25// --------------------------------------------------------------------------- 25// ---------------------------------------------------------------------------
26 26
27const LAST_MIGRATION_VERSION = 695 27const LAST_MIGRATION_VERSION = 700
28 28
29// --------------------------------------------------------------------------- 29// ---------------------------------------------------------------------------
30 30
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 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 const { transaction } = utils
10
11 {
12 const data = {
13 type: Sequelize.INTEGER,
14 defaultValue: null,
15 allowNull: true
16 }
17 await utils.queryInterface.addColumn('userNotificationSetting', 'myVideoEditionFinished', data, { transaction })
18 }
19
20 {
21 const query = 'UPDATE "userNotificationSetting" SET "myVideoEditionFinished" = 1'
22 await utils.sequelize.query(query, { transaction })
23 }
24
25 {
26 const data = {
27 type: Sequelize.INTEGER,
28 defaultValue: null,
29 allowNull: false
30 }
31 await utils.queryInterface.changeColumn('userNotificationSetting', 'myVideoEditionFinished', data, { transaction })
32 }
33}
34
35function down () {
36 throw new Error('Not implemented.')
37}
38
39export {
40 up,
41 down
42}
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
11import { VideoModel } from '@server/models/video/video' 11import { VideoModel } from '@server/models/video/video'
12import { VideoJobInfoModel } from '@server/models/video/video-job-info' 12import { VideoJobInfoModel } from '@server/models/video/video-job-info'
13import { MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoWithAllFiles } from '@server/types/models' 13import { MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoWithAllFiles } from '@server/types/models'
14import { MoveObjectStoragePayload, VideoStorage } from '@shared/models' 14import { MoveObjectStoragePayload, VideoState, VideoStorage } from '@shared/models'
15 15
16const lTagsBase = loggerTagsFactory('move-object-storage') 16const lTagsBase = loggerTagsFactory('move-object-storage')
17 17
@@ -45,7 +45,7 @@ export async function processMoveToObjectStorage (job: Job) {
45 if (pendingMove === 0) { 45 if (pendingMove === 0) {
46 logger.info('Running cleanup after moving files to object storage (video %s in job %d)', video.uuid, job.id, lTags) 46 logger.info('Running cleanup after moving files to object storage (video %s in job %d)', video.uuid, job.id, lTags)
47 47
48 await doAfterLastJob(video, payload.isNewVideo) 48 await doAfterLastJob({ video, previousVideoState: payload.previousVideoState, isNewVideo: payload.isNewVideo })
49 } 49 }
50 } catch (err) { 50 } catch (err) {
51 logger.error('Cannot move video %s to object storage.', video.url, { err, ...lTags }) 51 logger.error('Cannot move video %s to object storage.', video.url, { err, ...lTags })
@@ -91,7 +91,13 @@ async function moveHLSFiles (video: MVideoWithAllFiles) {
91 } 91 }
92} 92}
93 93
94async function doAfterLastJob (video: MVideoWithAllFiles, isNewVideo: boolean) { 94async function doAfterLastJob (options: {
95 video: MVideoWithAllFiles
96 previousVideoState: VideoState
97 isNewVideo: boolean
98}) {
99 const { video, previousVideoState, isNewVideo } = options
100
95 for (const playlist of video.VideoStreamingPlaylists) { 101 for (const playlist of video.VideoStreamingPlaylists) {
96 if (playlist.storage === VideoStorage.OBJECT_STORAGE) continue 102 if (playlist.storage === VideoStorage.OBJECT_STORAGE) continue
97 103
@@ -115,7 +121,7 @@ async function doAfterLastJob (video: MVideoWithAllFiles, isNewVideo: boolean) {
115 await remove(getHLSDirectory(video)) 121 await remove(getHLSDirectory(video))
116 } 122 }
117 123
118 await moveToNextState(video, isNewVideo) 124 await moveToNextState({ video, previousVideoState, isNewVideo })
119} 125}
120 126
121async function onFileMoved (options: { 127async 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'
8import { generateWebTorrentVideoFilename } from '@server/lib/paths' 8import { generateWebTorrentVideoFilename } from '@server/lib/paths'
9import { VideoTranscodingProfilesManager } from '@server/lib/transcoding/default-transcoding-profiles' 9import { VideoTranscodingProfilesManager } from '@server/lib/transcoding/default-transcoding-profiles'
10import { isAbleToUploadVideo } from '@server/lib/user' 10import { isAbleToUploadVideo } from '@server/lib/user'
11import { addMoveToObjectStorageJob, addOptimizeOrMergeAudioJob } from '@server/lib/video' 11import { addOptimizeOrMergeAudioJob } from '@server/lib/video'
12import { approximateIntroOutroAdditionalSize } from '@server/lib/video-editor' 12import { approximateIntroOutroAdditionalSize } from '@server/lib/video-editor'
13import { VideoPathManager } from '@server/lib/video-path-manager' 13import { VideoPathManager } from '@server/lib/video-path-manager'
14import { buildNextVideoState } from '@server/lib/video-state'
15import { UserModel } from '@server/models/user/user' 14import { UserModel } from '@server/models/user/user'
16import { VideoModel } from '@server/models/video/video' 15import { VideoModel } from '@server/models/video/video'
17import { VideoFileModel } from '@server/models/video/video-file' 16import { VideoFileModel } from '@server/models/video/video-file'
@@ -33,8 +32,7 @@ import {
33 VideoEditorTaskCutPayload, 32 VideoEditorTaskCutPayload,
34 VideoEditorTaskIntroPayload, 33 VideoEditorTaskIntroPayload,
35 VideoEditorTaskOutroPayload, 34 VideoEditorTaskOutroPayload,
36 VideoEditorTaskWatermarkPayload, 35 VideoEditorTaskWatermarkPayload
37 VideoState
38} from '@shared/models' 36} from '@shared/models'
39import { logger, loggerTagsFactory } from '../../../helpers/logger' 37import { logger, loggerTagsFactory } from '../../../helpers/logger'
40 38
@@ -42,14 +40,15 @@ const lTagsBase = loggerTagsFactory('video-edition')
42 40
43async function processVideoEdition (job: Job) { 41async function processVideoEdition (job: Job) {
44 const payload = job.data as VideoEditionPayload 42 const payload = job.data as VideoEditionPayload
43 const lTags = lTagsBase(payload.videoUUID)
45 44
46 logger.info('Process video edition of %s in job %d.', payload.videoUUID, job.id) 45 logger.info('Process video edition of %s in job %d.', payload.videoUUID, job.id, lTags)
47 46
48 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(payload.videoUUID) 47 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(payload.videoUUID)
49 48
50 // No video, maybe deleted? 49 // No video, maybe deleted?
51 if (!video) { 50 if (!video) {
52 logger.info('Can\'t process job %d, video does not exist.', job.id, lTagsBase(payload.videoUUID)) 51 logger.info('Can\'t process job %d, video does not exist.', job.id, lTags)
53 return undefined 52 return undefined
54 } 53 }
55 54
@@ -69,7 +68,8 @@ async function processVideoEdition (job: Job) {
69 inputPath: tmpInputFilePath ?? originalFilePath, 68 inputPath: tmpInputFilePath ?? originalFilePath,
70 video, 69 video,
71 outputPath, 70 outputPath,
72 task 71 task,
72 lTags
73 }) 73 })
74 74
75 if (tmpInputFilePath) await remove(tmpInputFilePath) 75 if (tmpInputFilePath) await remove(tmpInputFilePath)
@@ -81,7 +81,7 @@ async function processVideoEdition (job: Job) {
81 return outputPath 81 return outputPath
82 }) 82 })
83 83
84 logger.info('Video edition ended for video %s.', video.uuid) 84 logger.info('Video edition ended for video %s.', video.uuid, lTags)
85 85
86 const newFile = await buildNewFile(video, editionResultPath) 86 const newFile = await buildNewFile(video, editionResultPath)
87 87
@@ -94,19 +94,13 @@ async function processVideoEdition (job: Job) {
94 94
95 await newFile.save() 95 await newFile.save()
96 96
97 video.state = buildNextVideoState()
98 video.duration = await getVideoStreamDuration(outputPath) 97 video.duration = await getVideoStreamDuration(outputPath)
99 await video.save() 98 await video.save()
100 99
101 await federateVideoIfNeeded(video, false, undefined) 100 await federateVideoIfNeeded(video, false, undefined)
102 101
103 if (video.state === VideoState.TO_TRANSCODE) { 102 const user = await UserModel.loadByVideoId(video.id)
104 const user = await UserModel.loadByVideoId(video.id) 103 await addOptimizeOrMergeAudioJob({ video, videoFile: newFile, user, isNewVideo: false })
105
106 await addOptimizeOrMergeAudioJob(video, newFile, user, false)
107 } else if (video.state === VideoState.TO_MOVE_TO_EXTERNAL_STORAGE) {
108 await addMoveToObjectStorageJob(video, false)
109 }
110} 104}
111 105
112// --------------------------------------------------------------------------- 106// ---------------------------------------------------------------------------
@@ -122,6 +116,7 @@ type TaskProcessorOptions <T extends VideoEditionTaskPayload = VideoEditionTaskP
122 outputPath: string 116 outputPath: string
123 video: MVideo 117 video: MVideo
124 task: T 118 task: T
119 lTags: { tags: string[] }
125} 120}
126 121
127const taskProcessors: { [id in VideoEditorTask['name']]: (options: TaskProcessorOptions) => Promise<any> } = { 122const taskProcessors: { [id in VideoEditorTask['name']]: (options: TaskProcessorOptions) => Promise<any> } = {
@@ -134,7 +129,7 @@ const taskProcessors: { [id in VideoEditorTask['name']]: (options: TaskProcessor
134async function processTask (options: TaskProcessorOptions) { 129async function processTask (options: TaskProcessorOptions) {
135 const { video, task } = options 130 const { video, task } = options
136 131
137 logger.info('Processing %s task for video %s.', task.name, video.uuid, { task }) 132 logger.info('Processing %s task for video %s.', task.name, video.uuid, { task, ...options.lTags })
138 133
139 const processor = taskProcessors[options.task.name] 134 const processor = taskProcessors[options.task.name]
140 if (!process) throw new Error('Unknown task ' + task.name) 135 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) {
28 await updateVideoFile(video, payload.filePath) 28 await updateVideoFile(video, payload.filePath)
29 29
30 if (CONFIG.OBJECT_STORAGE.ENABLED) { 30 if (CONFIG.OBJECT_STORAGE.ENABLED) {
31 await addMoveToObjectStorageJob(video) 31 await addMoveToObjectStorageJob({ video, previousVideoState: video.state })
32 } else { 32 } else {
33 await federateVideoIfNeeded(video, false) 33 await federateVideoIfNeeded(video, false)
34 } 34 }
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<string>, videoImport: MVid
254 } 254 }
255 255
256 if (video.state === VideoState.TO_MOVE_TO_EXTERNAL_STORAGE) { 256 if (video.state === VideoState.TO_MOVE_TO_EXTERNAL_STORAGE) {
257 return addMoveToObjectStorageJob(videoImportUpdated.Video) 257 return addMoveToObjectStorageJob({ video: videoImportUpdated.Video, previousVideoState: VideoState.TO_IMPORT })
258 } 258 }
259 259
260 // Create transcoding jobs? 260 // Create transcoding jobs?
261 if (video.state === VideoState.TO_TRANSCODE) { 261 if (video.state === VideoState.TO_TRANSCODE) {
262 await addOptimizeOrMergeAudioJob(videoImportUpdated.Video, videoFile, videoImport.User) 262 await addOptimizeOrMergeAudioJob({ video: videoImportUpdated.Video, videoFile, user: videoImport.User })
263 } 263 }
264 264
265 } catch (err) { 265 } 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
133 }) 133 })
134 } 134 }
135 135
136 await moveToNextState(videoWithFiles, false) 136 await moveToNextState({ video: videoWithFiles, isNewVideo: false })
137} 137}
138 138
139async function cleanupTMPLiveFiles (hlsDirectory: string) { 139async 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
168 } 168 }
169 169
170 await VideoJobInfoModel.decrease(video.uuid, 'pendingTranscode') 170 await VideoJobInfoModel.decrease(video.uuid, 'pendingTranscode')
171 await retryTransactionWrapper(moveToNextState, video, payload.isNewVideo) 171 await retryTransactionWrapper(moveToNextState, { video, isNewVideo: payload.isNewVideo })
172} 172}
173 173
174async function onVideoFirstWebTorrentTranscoding ( 174async function onVideoFirstWebTorrentTranscoding (
@@ -210,7 +210,7 @@ async function onVideoFirstWebTorrentTranscoding (
210 210
211 // Move to next state if there are no other resolutions to generate 211 // Move to next state if there are no other resolutions to generate
212 if (!hasHls && !hasNewResolutions) { 212 if (!hasHls && !hasNewResolutions) {
213 await retryTransactionWrapper(moveToNextState, videoDatabase, payload.isNewVideo) 213 await retryTransactionWrapper(moveToNextState, { video: videoDatabase, isNewVideo: payload.isNewVideo })
214 } 214 }
215} 215}
216 216
@@ -225,7 +225,7 @@ async function onNewWebTorrentFileResolution (
225 225
226 await VideoJobInfoModel.decrease(video.uuid, 'pendingTranscode') 226 await VideoJobInfoModel.decrease(video.uuid, 'pendingTranscode')
227 227
228 await retryTransactionWrapper(moveToNextState, video, payload.isNewVideo) 228 await retryTransactionWrapper(moveToNextState, { video, isNewVideo: payload.isNewVideo })
229} 229}
230 230
231// --------------------------------------------------------------------------- 231// ---------------------------------------------------------------------------
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 {
12 AbuseStateChangeForReporter, 12 AbuseStateChangeForReporter,
13 AutoFollowForInstance, 13 AutoFollowForInstance,
14 CommentMention, 14 CommentMention,
15 EditionFinishedForOwner,
15 FollowForInstance, 16 FollowForInstance,
16 FollowForUser, 17 FollowForUser,
17 ImportFinishedForOwner, 18 ImportFinishedForOwner,
@@ -53,7 +54,8 @@ class Notifier {
53 abuseStateChange: [ AbuseStateChangeForReporter ], 54 abuseStateChange: [ AbuseStateChangeForReporter ],
54 newAbuseMessage: [ NewAbuseMessageForReporter, NewAbuseMessageForModerators ], 55 newAbuseMessage: [ NewAbuseMessageForReporter, NewAbuseMessageForModerators ],
55 newPeertubeVersion: [ NewPeerTubeVersionForAdmins ], 56 newPeertubeVersion: [ NewPeerTubeVersionForAdmins ],
56 newPluginVersion: [ NewPluginVersionForAdmins ] 57 newPluginVersion: [ NewPluginVersionForAdmins ],
58 videoEditionFinished: [ EditionFinishedForOwner ]
57 } 59 }
58 60
59 private static instance: Notifier 61 private static instance: Notifier
@@ -198,6 +200,13 @@ class Notifier {
198 .catch(err => logger.error('Cannot notify on new plugin version %s.', plugin.name, { err })) 200 .catch(err => logger.error('Cannot notify on new plugin version %s.', plugin.name, { err }))
199 } 201 }
200 202
203 notifyOfFinishedVideoEdition (video: MVideoFullLight) {
204 const models = this.notificationModels.videoEditionFinished
205
206 this.sendNotifications(models, video)
207 .catch(err => logger.error('Cannot notify on finished edition %s.', video.url, { err }))
208 }
209
201 private async notify <T> (object: AbstractNotification<T>) { 210 private async notify <T> (object: AbstractNotification<T>) {
202 await object.prepare() 211 await object.prepare()
203 212
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
46 subject: `Your video ${this.payload.name} has been published`, 46 subject: `Your video ${this.payload.name} has been published`,
47 text: `Your video "${this.payload.name}" has been published.`, 47 text: `Your video "${this.payload.name}" has been published.`,
48 locals: { 48 locals: {
49 title: 'You video is live', 49 title: 'Your video is live',
50 action: { 50 action: {
51 text: 'View video', 51 text: 'View video',
52 url: videoUrl 52 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 @@
1import { logger } from '@server/helpers/logger'
2import { WEBSERVER } from '@server/initializers/constants'
3import { UserModel } from '@server/models/user/user'
4import { UserNotificationModel } from '@server/models/user/user-notification'
5import { MUserDefault, MUserWithNotificationSetting, MVideoFullLight, UserNotificationModelForApi } from '@server/types/models'
6import { UserNotificationType } from '@shared/models'
7import { AbstractNotification } from '../common/abstract-notification'
8
9export class EditionFinishedForOwner extends AbstractNotification <MVideoFullLight> {
10 private user: MUserDefault
11
12 async prepare () {
13 this.user = await UserModel.loadByVideoId(this.payload.id)
14 }
15
16 log () {
17 logger.info('Notifying user %s its video edition %s is finished.', this.user.username, this.payload.url)
18 }
19
20 getSetting (user: MUserWithNotificationSetting) {
21 return user.NotificationSetting.myVideoEditionFinished
22 }
23
24 getTargetUsers () {
25 if (!this.user) return []
26
27 return [ this.user ]
28 }
29
30 async createNotification (user: MUserWithNotificationSetting) {
31 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
32 type: UserNotificationType.MY_VIDEO_EDITION_FINISHED,
33 userId: user.id,
34 videoId: this.payload.id
35 })
36 notification.Video = this.payload
37
38 return notification
39 }
40
41 createEmail (to: string) {
42 const videoUrl = WEBSERVER.URL + this.payload.getWatchStaticPath()
43
44 return {
45 to,
46 subject: `Edition of your video ${this.payload.name} has finished`,
47 text: `Edition of your video ${this.payload.name} has finished.`,
48 locals: {
49 title: 'Video edition has finished',
50 action: {
51 text: 'View video',
52 url: videoUrl
53 }
54 }
55 }
56 }
57}
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 @@
1export * from './new-video-for-subscribers' 1export * from './new-video-for-subscribers'
2export * from './edition-finished-for-owner'
2export * from './import-finished-for-owner' 3export * from './import-finished-for-owner'
3export * from './owned-publication-after-auto-unblacklist' 4export * from './owned-publication-after-auto-unblacklist'
4export * from './owned-publication-after-schedule-update' 5export * 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 |
252 abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 252 abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
253 autoInstanceFollowing: UserNotificationSettingValue.WEB, 253 autoInstanceFollowing: UserNotificationSettingValue.WEB,
254 newPeerTubeVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 254 newPeerTubeVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
255 newPluginVersion: UserNotificationSettingValue.WEB 255 newPluginVersion: UserNotificationSettingValue.WEB,
256 myVideoEditionFinished: UserNotificationSettingValue.WEB
256 } 257 }
257 258
258 return UserNotificationSettingModel.create(values, { transaction: t }) 259 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) {
16 } 16 }
17 17
18 if ( 18 if (
19 currentState !== VideoState.TO_EDIT &&
19 currentState !== VideoState.TO_TRANSCODE && 20 currentState !== VideoState.TO_TRANSCODE &&
20 currentState !== VideoState.TO_MOVE_TO_EXTERNAL_STORAGE && 21 currentState !== VideoState.TO_MOVE_TO_EXTERNAL_STORAGE &&
21 CONFIG.TRANSCODING.ENABLED 22 CONFIG.TRANSCODING.ENABLED
@@ -33,7 +34,13 @@ function buildNextVideoState (currentState?: VideoState) {
33 return VideoState.PUBLISHED 34 return VideoState.PUBLISHED
34} 35}
35 36
36function moveToNextState (video: MVideoUUID, isNewVideo = true) { 37function moveToNextState (options: {
38 video: MVideoUUID
39 previousVideoState?: VideoState
40 isNewVideo?: boolean // Default true
41}) {
42 const { video, previousVideoState, isNewVideo = true } = options
43
37 return sequelizeTypescript.transaction(async t => { 44 return sequelizeTypescript.transaction(async t => {
38 // Maybe the video changed in database, refresh it 45 // Maybe the video changed in database, refresh it
39 const videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t) 46 const videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t)
@@ -48,28 +55,35 @@ function moveToNextState (video: MVideoUUID, isNewVideo = true) {
48 const newState = buildNextVideoState(videoDatabase.state) 55 const newState = buildNextVideoState(videoDatabase.state)
49 56
50 if (newState === VideoState.PUBLISHED) { 57 if (newState === VideoState.PUBLISHED) {
51 return moveToPublishedState(videoDatabase, isNewVideo, t) 58 return moveToPublishedState({ video: videoDatabase, previousVideoState, isNewVideo, transaction: t })
52 } 59 }
53 60
54 if (newState === VideoState.TO_MOVE_TO_EXTERNAL_STORAGE) { 61 if (newState === VideoState.TO_MOVE_TO_EXTERNAL_STORAGE) {
55 return moveToExternalStorageState(videoDatabase, isNewVideo, t) 62 return moveToExternalStorageState({ video: videoDatabase, isNewVideo, transaction: t })
56 } 63 }
57 }) 64 })
58} 65}
59 66
60async function moveToExternalStorageState (video: MVideoFullLight, isNewVideo: boolean, transaction: Transaction) { 67async function moveToExternalStorageState (options: {
68 video: MVideoFullLight
69 isNewVideo: boolean
70 transaction: Transaction
71}) {
72 const { video, isNewVideo, transaction } = options
73
61 const videoJobInfo = await VideoJobInfoModel.load(video.id, transaction) 74 const videoJobInfo = await VideoJobInfoModel.load(video.id, transaction)
62 const pendingTranscode = videoJobInfo?.pendingTranscode || 0 75 const pendingTranscode = videoJobInfo?.pendingTranscode || 0
63 76
64 // We want to wait all transcoding jobs before moving the video on an external storage 77 // We want to wait all transcoding jobs before moving the video on an external storage
65 if (pendingTranscode !== 0) return false 78 if (pendingTranscode !== 0) return false
66 79
80 const previousVideoState = video.state
67 await video.setNewState(VideoState.TO_MOVE_TO_EXTERNAL_STORAGE, isNewVideo, transaction) 81 await video.setNewState(VideoState.TO_MOVE_TO_EXTERNAL_STORAGE, isNewVideo, transaction)
68 82
69 logger.info('Creating external storage move job for video %s.', video.uuid, { tags: [ video.uuid ] }) 83 logger.info('Creating external storage move job for video %s.', video.uuid, { tags: [ video.uuid ] })
70 84
71 try { 85 try {
72 await addMoveToObjectStorageJob(video, isNewVideo) 86 await addMoveToObjectStorageJob({ video, previousVideoState, isNewVideo })
73 87
74 return true 88 return true
75 } catch (err) { 89 } catch (err) {
@@ -103,21 +117,33 @@ export {
103 117
104// --------------------------------------------------------------------------- 118// ---------------------------------------------------------------------------
105 119
106async function moveToPublishedState (video: MVideoFullLight, isNewVideo: boolean, transaction: Transaction) { 120async function moveToPublishedState (options: {
107 logger.info('Publishing video %s.', video.uuid, { tags: [ video.uuid ] }) 121 video: MVideoFullLight
122 isNewVideo: boolean
123 transaction: Transaction
124 previousVideoState?: VideoState
125}) {
126 const { video, isNewVideo, transaction, previousVideoState } = options
127 const previousState = previousVideoState ?? video.state
128
129 logger.info('Publishing video %s.', video.uuid, { previousState, tags: [ video.uuid ] })
108 130
109 const previousState = video.state
110 await video.setNewState(VideoState.PUBLISHED, isNewVideo, transaction) 131 await video.setNewState(VideoState.PUBLISHED, isNewVideo, transaction)
111 132
112 // If the video was not published, we consider it is a new one for other instances 133 // If the video was not published, we consider it is a new one for other instances
113 // Live videos are always federated, so it's not a new video 134 // Live videos are always federated, so it's not a new video
114 await federateVideoIfNeeded(video, isNewVideo, transaction) 135 await federateVideoIfNeeded(video, isNewVideo, transaction)
115 136
116 if (!isNewVideo) return 137 if (previousState === VideoState.TO_EDIT) {
138 Notifier.Instance.notifyOfFinishedVideoEdition(video)
139 return
140 }
117 141
118 Notifier.Instance.notifyOnNewVideoIfNeeded(video) 142 if (isNewVideo) {
143 Notifier.Instance.notifyOnNewVideoIfNeeded(video)
119 144
120 if (previousState === VideoState.TO_TRANSCODE) { 145 if (previousState === VideoState.TO_TRANSCODE) {
121 Notifier.Instance.notifyOnVideoPublishedAfterTranscoding(video) 146 Notifier.Instance.notifyOnVideoPublishedAfterTranscoding(video)
147 }
122 } 148 }
123} 149}
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'
6import { VideoJobInfoModel } from '@server/models/video/video-job-info' 6import { VideoJobInfoModel } from '@server/models/video/video-job-info'
7import { FilteredModelAttributes } from '@server/types' 7import { FilteredModelAttributes } from '@server/types'
8import { MThumbnail, MUserId, MVideoFile, MVideoTag, MVideoThumbnail, MVideoUUID } from '@server/types/models' 8import { MThumbnail, MUserId, MVideoFile, MVideoTag, MVideoThumbnail, MVideoUUID } from '@server/types/models'
9import { ThumbnailType, VideoCreate, VideoPrivacy, VideoTranscodingPayload } from '@shared/models' 9import { ThumbnailType, VideoCreate, VideoPrivacy, VideoState, VideoTranscodingPayload } from '@shared/models'
10import { CreateJobOptions, JobQueue } from './job-queue/job-queue' 10import { CreateJobOptions, JobQueue } from './job-queue/job-queue'
11import { updateVideoMiniatureFromExisting } from './thumbnail' 11import { updateVideoMiniatureFromExisting } from './thumbnail'
12import { CONFIG } from '@server/initializers/config' 12import { CONFIG } from '@server/initializers/config'
@@ -67,6 +67,8 @@ async function buildVideoThumbnailsFromReq (options: {
67 return Promise.all(promises) 67 return Promise.all(promises)
68} 68}
69 69
70// ---------------------------------------------------------------------------
71
70async function setVideoTags (options: { 72async function setVideoTags (options: {
71 video: MVideoTag 73 video: MVideoTag
72 tags: string[] 74 tags: string[]
@@ -81,7 +83,16 @@ async function setVideoTags (options: {
81 video.Tags = tagInstances 83 video.Tags = tagInstances
82} 84}
83 85
84async function addOptimizeOrMergeAudioJob (video: MVideoUUID, videoFile: MVideoFile, user: MUserId, isNewVideo = true) { 86// ---------------------------------------------------------------------------
87
88async function addOptimizeOrMergeAudioJob (options: {
89 video: MVideoUUID
90 videoFile: MVideoFile
91 user: MUserId
92 isNewVideo?: boolean // Default true
93}) {
94 const { video, videoFile, user, isNewVideo } = options
95
85 let dataInput: VideoTranscodingPayload 96 let dataInput: VideoTranscodingPayload
86 97
87 if (videoFile.isAudio()) { 98 if (videoFile.isAudio()) {
@@ -113,13 +124,6 @@ async function addTranscodingJob (payload: VideoTranscodingPayload, options: Cre
113 return JobQueue.Instance.createJobWithPromise({ type: 'video-transcoding', payload: payload }, options) 124 return JobQueue.Instance.createJobWithPromise({ type: 'video-transcoding', payload: payload }, options)
114} 125}
115 126
116async function addMoveToObjectStorageJob (video: MVideoUUID, isNewVideo = true) {
117 await VideoJobInfoModel.increaseOrCreate(video.uuid, 'pendingMove')
118
119 const dataInput = { videoUUID: video.uuid, isNewVideo }
120 return JobQueue.Instance.createJobWithPromise({ type: 'move-to-object-storage', payload: dataInput })
121}
122
123async function getTranscodingJobPriority (user: MUserId) { 127async function getTranscodingJobPriority (user: MUserId) {
124 const now = new Date() 128 const now = new Date()
125 const lastWeek = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 7) 129 const lastWeek = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 7)
@@ -131,6 +135,21 @@ async function getTranscodingJobPriority (user: MUserId) {
131 135
132// --------------------------------------------------------------------------- 136// ---------------------------------------------------------------------------
133 137
138async function addMoveToObjectStorageJob (options: {
139 video: MVideoUUID
140 previousVideoState: VideoState
141 isNewVideo?: boolean // Default true
142}) {
143 const { video, previousVideoState, isNewVideo = true } = options
144
145 await VideoJobInfoModel.increaseOrCreate(video.uuid, 'pendingMove')
146
147 const dataInput = { videoUUID: video.uuid, isNewVideo, previousVideoState }
148 return JobQueue.Instance.createJobWithPromise({ type: 'move-to-object-storage', payload: dataInput })
149}
150
151// ---------------------------------------------------------------------------
152
134export { 153export {
135 buildLocalVideoFromReq, 154 buildLocalVideoFromReq,
136 buildVideoThumbnailsFromReq, 155 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<Partial<AttributesOnly<U
175 @Column 175 @Column
176 newPluginVersion: UserNotificationSettingValue 176 newPluginVersion: UserNotificationSettingValue
177 177
178 @AllowNull(false)
179 @Default(null)
180 @Is(
181 'UserNotificationSettingMyVideoEditionFinished',
182 value => throwIfNotValid(value, isUserNotificationSettingValid, 'myVideoEditionFinished')
183 )
184 @Column
185 myVideoEditionFinished: UserNotificationSettingValue
186
178 @ForeignKey(() => UserModel) 187 @ForeignKey(() => UserModel)
179 @Column 188 @Column
180 userId: number 189 userId: number
@@ -216,6 +225,7 @@ export class UserNotificationSettingModel extends Model<Partial<AttributesOnly<U
216 abuseNewMessage: this.abuseNewMessage, 225 abuseNewMessage: this.abuseNewMessage,
217 abuseStateChange: this.abuseStateChange, 226 abuseStateChange: this.abuseStateChange,
218 newPeerTubeVersion: this.newPeerTubeVersion, 227 newPeerTubeVersion: this.newPeerTubeVersion,
228 myVideoEditionFinished: this.myVideoEditionFinished,
219 newPluginVersion: this.newPluginVersion 229 newPluginVersion: this.newPluginVersion
220 } 230 }
221 } 231 }
diff --git a/server/tests/api/check-params/user-notifications.ts b/server/tests/api/check-params/user-notifications.ts
index 4bc8084a1..93355e8b3 100644
--- a/server/tests/api/check-params/user-notifications.ts
+++ b/server/tests/api/check-params/user-notifications.ts
@@ -171,6 +171,7 @@ describe('Test user notifications API validators', function () {
171 abuseNewMessage: UserNotificationSettingValue.WEB, 171 abuseNewMessage: UserNotificationSettingValue.WEB,
172 abuseStateChange: UserNotificationSettingValue.WEB, 172 abuseStateChange: UserNotificationSettingValue.WEB,
173 newPeerTubeVersion: UserNotificationSettingValue.WEB, 173 newPeerTubeVersion: UserNotificationSettingValue.WEB,
174 myVideoEditionFinished: UserNotificationSettingValue.WEB,
174 newPluginVersion: UserNotificationSettingValue.WEB 175 newPluginVersion: UserNotificationSettingValue.WEB
175 } 176 }
176 177
diff --git a/server/tests/api/notifications/user-notifications.ts b/server/tests/api/notifications/user-notifications.ts
index f9f3e0e0e..c87686cb5 100644
--- a/server/tests/api/notifications/user-notifications.ts
+++ b/server/tests/api/notifications/user-notifications.ts
@@ -7,6 +7,7 @@ import {
7 checkMyVideoImportIsFinished, 7 checkMyVideoImportIsFinished,
8 checkNewActorFollow, 8 checkNewActorFollow,
9 checkNewVideoFromSubscription, 9 checkNewVideoFromSubscription,
10 checkVideoEditionIsFinished,
10 checkVideoIsPublished, 11 checkVideoIsPublished,
11 FIXTURE_URLS, 12 FIXTURE_URLS,
12 MockSmtpServer, 13 MockSmtpServer,
@@ -15,7 +16,7 @@ import {
15} from '@server/tests/shared' 16} from '@server/tests/shared'
16import { wait } from '@shared/core-utils' 17import { wait } from '@shared/core-utils'
17import { buildUUID } from '@shared/extra-utils' 18import { buildUUID } from '@shared/extra-utils'
18import { UserNotification, UserNotificationType, VideoPrivacy } from '@shared/models' 19import { UserNotification, UserNotificationType, VideoEditorTask, VideoPrivacy } from '@shared/models'
19import { cleanupTests, PeerTubeServer, waitJobs } from '@shared/server-commands' 20import { cleanupTests, PeerTubeServer, waitJobs } from '@shared/server-commands'
20 21
21const expect = chai.expect 22const expect = chai.expect
@@ -23,10 +24,12 @@ const expect = chai.expect
23describe('Test user notifications', function () { 24describe('Test user notifications', function () {
24 let servers: PeerTubeServer[] = [] 25 let servers: PeerTubeServer[] = []
25 let userAccessToken: string 26 let userAccessToken: string
27
26 let userNotifications: UserNotification[] = [] 28 let userNotifications: UserNotification[] = []
27 let adminNotifications: UserNotification[] = [] 29 let adminNotifications: UserNotification[] = []
28 let adminNotificationsServer2: UserNotification[] = [] 30 let adminNotificationsServer2: UserNotification[] = []
29 let emails: object[] = [] 31 let emails: object[] = []
32
30 let channelId: number 33 let channelId: number
31 34
32 before(async function () { 35 before(async function () {
@@ -320,6 +323,42 @@ describe('Test user notifications', function () {
320 }) 323 })
321 }) 324 })
322 325
326 describe('Video editor', function () {
327 let baseParams: CheckerBaseParams
328
329 before(() => {
330 baseParams = {
331 server: servers[1],
332 emails,
333 socketNotifications: adminNotificationsServer2,
334 token: servers[1].accessToken
335 }
336 })
337
338 it('Should send a notification after editor edition', async function () {
339 this.timeout(240000)
340
341 const { name, shortUUID, id } = await uploadRandomVideoOnServers(servers, 2, { waitTranscoding: true })
342
343 await waitJobs(servers)
344 await checkVideoIsPublished({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' })
345
346 const tasks: VideoEditorTask[] = [
347 {
348 name: 'cut',
349 options: {
350 start: 0,
351 end: 1
352 }
353 }
354 ]
355 await servers[1].videoEditor.createEditionTasks({ videoId: id, tasks })
356 await waitJobs(servers)
357
358 await checkVideoEditionIsFinished({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' })
359 })
360 })
361
323 describe('My video is imported', function () { 362 describe('My video is imported', function () {
324 let baseParams: CheckerBaseParams 363 let baseParams: CheckerBaseParams
325 364
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 () {
56 56
57 await servers[0].config.enableMinimumTranscoding() 57 await servers[0].config.enableMinimumTranscoding()
58 58
59 await servers[0].config.updateExistingSubConfig({ 59 await servers[0].config.enableEditor()
60 newConfig: {
61 videoEditor: {
62 enabled: true
63 }
64 }
65 })
66 }) 60 })
67 61
68 describe('Cutting', function () { 62 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 {
47 abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 47 abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
48 autoInstanceFollowing: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 48 autoInstanceFollowing: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
49 newPeerTubeVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 49 newPeerTubeVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
50 myVideoEditionFinished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
50 newPluginVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL 51 newPluginVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
51 } 52 }
52} 53}
@@ -109,6 +110,34 @@ async function checkVideoIsPublished (options: CheckerBaseParams & {
109 await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) 110 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
110} 111}
111 112
113async function checkVideoEditionIsFinished (options: CheckerBaseParams & {
114 videoName: string
115 shortUUID: string
116 checkType: CheckerType
117}) {
118 const { videoName, shortUUID } = options
119 const notificationType = UserNotificationType.MY_VIDEO_EDITION_FINISHED
120
121 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
122 if (checkType === 'presence') {
123 expect(notification).to.not.be.undefined
124 expect(notification.type).to.equal(notificationType)
125
126 checkVideo(notification.video, videoName, shortUUID)
127 checkActor(notification.video.channel)
128 } else {
129 expect(notification.video).to.satisfy(v => v === undefined || v.name !== videoName)
130 }
131 }
132
133 function emailNotificationFinder (email: object) {
134 const text: string = email['text']
135 return text.includes(shortUUID) && text.includes('Edition of your video')
136 }
137
138 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
139}
140
112async function checkMyVideoImportIsFinished (options: CheckerBaseParams & { 141async function checkMyVideoImportIsFinished (options: CheckerBaseParams & {
113 videoName: string 142 videoName: string
114 shortUUID: string 143 shortUUID: string
@@ -656,6 +685,8 @@ async function prepareNotificationsTest (serversCount = 3, overrideConfigArg: an
656 await setDefaultChannelAvatar(servers) 685 await setDefaultChannelAvatar(servers)
657 await setDefaultAccountAvatar(servers) 686 await setDefaultAccountAvatar(servers)
658 687
688 await servers[1].config.enableEditor()
689
659 if (serversCount > 1) { 690 if (serversCount > 1) {
660 await doubleFollow(servers[0], servers[1]) 691 await doubleFollow(servers[0], servers[1])
661 } 692 }
@@ -724,7 +755,8 @@ export {
724 checkNewCommentAbuseForModerators, 755 checkNewCommentAbuseForModerators,
725 checkNewAccountAbuseForModerators, 756 checkNewAccountAbuseForModerators,
726 checkNewPeerTubeVersion, 757 checkNewPeerTubeVersion,
727 checkNewPluginVersion 758 checkNewPluginVersion,
759 checkVideoEditionIsFinished
728} 760}
729 761
730// --------------------------------------------------------------------------- 762// ---------------------------------------------------------------------------
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 @@
1import { ContextType } from '../activitypub/context' 1import { ContextType } from '../activitypub/context'
2import { VideoState } from '../videos'
2import { VideoEditorTaskCut } from '../videos/editor' 3import { VideoEditorTaskCut } from '../videos/editor'
3import { VideoResolution } from '../videos/file/video-resolution.enum' 4import { VideoResolution } from '../videos/file/video-resolution.enum'
4import { SendEmailOptions } from './emailer.model' 5import { SendEmailOptions } from './emailer.model'
@@ -116,6 +117,9 @@ export type ManageVideoTorrentPayload =
116interface BaseTranscodingPayload { 117interface BaseTranscodingPayload {
117 videoUUID: string 118 videoUUID: string
118 isNewVideo?: boolean 119 isNewVideo?: boolean
120
121 // Custom notification when the task is finished
122 notification?: 'default' | 'video-edition'
119} 123}
120 124
121export interface HLSTranscodingPayload extends BaseTranscodingPayload { 125export interface HLSTranscodingPayload extends BaseTranscodingPayload {
@@ -171,6 +175,7 @@ export interface DeleteResumableUploadMetaFilePayload {
171export interface MoveObjectStoragePayload { 175export interface MoveObjectStoragePayload {
172 videoUUID: string 176 videoUUID: string
173 isNewVideo: boolean 177 isNewVideo: boolean
178 previousVideoState: VideoState
174} 179}
175 180
176export type VideoEditorTaskCutPayload = VideoEditorTaskCut 181export 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 {
27 27
28 newPeerTubeVersion: UserNotificationSettingValue 28 newPeerTubeVersion: UserNotificationSettingValue
29 newPluginVersion: UserNotificationSettingValue 29 newPluginVersion: UserNotificationSettingValue
30
31 myVideoEditionFinished: UserNotificationSettingValue
30} 32}
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 {
30 ABUSE_NEW_MESSAGE = 16, 30 ABUSE_NEW_MESSAGE = 16,
31 31
32 NEW_PLUGIN_VERSION = 17, 32 NEW_PLUGIN_VERSION = 17,
33 NEW_PEERTUBE_VERSION = 18 33 NEW_PEERTUBE_VERSION = 18,
34
35 MY_VIDEO_EDITION_FINISHED = 19
34} 36}
35 37
36export interface VideoInfo { 38export 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 {
111 }) 111 })
112 } 112 }
113 113
114 enableEditor () {
115 return this.updateExistingSubConfig({
116 newConfig: {
117 videoEditor: {
118 enabled: true
119 }
120 }
121 })
122 }
123
114 getConfig (options: OverrideCommandOptions = {}) { 124 getConfig (options: OverrideCommandOptions = {}) {
115 const path = '/api/v1/config' 125 const path = '/api/v1/config'
116 126