diff options
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 | ||
27 | const LAST_MIGRATION_VERSION = 695 | 27 | const 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 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async 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 | |||
35 | function down () { | ||
36 | throw new Error('Not implemented.') | ||
37 | } | ||
38 | |||
39 | export { | ||
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 | |||
11 | import { VideoModel } from '@server/models/video/video' | 11 | import { VideoModel } from '@server/models/video/video' |
12 | import { VideoJobInfoModel } from '@server/models/video/video-job-info' | 12 | import { VideoJobInfoModel } from '@server/models/video/video-job-info' |
13 | import { MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoWithAllFiles } from '@server/types/models' | 13 | import { MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoWithAllFiles } from '@server/types/models' |
14 | import { MoveObjectStoragePayload, VideoStorage } from '@shared/models' | 14 | import { MoveObjectStoragePayload, VideoState, VideoStorage } from '@shared/models' |
15 | 15 | ||
16 | const lTagsBase = loggerTagsFactory('move-object-storage') | 16 | const 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 | ||
94 | async function doAfterLastJob (video: MVideoWithAllFiles, isNewVideo: boolean) { | 94 | async 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 | ||
121 | async function onFileMoved (options: { | 127 | 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' | |||
8 | import { generateWebTorrentVideoFilename } from '@server/lib/paths' | 8 | import { generateWebTorrentVideoFilename } from '@server/lib/paths' |
9 | import { VideoTranscodingProfilesManager } from '@server/lib/transcoding/default-transcoding-profiles' | 9 | import { VideoTranscodingProfilesManager } from '@server/lib/transcoding/default-transcoding-profiles' |
10 | import { isAbleToUploadVideo } from '@server/lib/user' | 10 | import { isAbleToUploadVideo } from '@server/lib/user' |
11 | import { addMoveToObjectStorageJob, addOptimizeOrMergeAudioJob } from '@server/lib/video' | 11 | import { addOptimizeOrMergeAudioJob } from '@server/lib/video' |
12 | import { approximateIntroOutroAdditionalSize } from '@server/lib/video-editor' | 12 | import { approximateIntroOutroAdditionalSize } from '@server/lib/video-editor' |
13 | import { VideoPathManager } from '@server/lib/video-path-manager' | 13 | import { VideoPathManager } from '@server/lib/video-path-manager' |
14 | import { buildNextVideoState } from '@server/lib/video-state' | ||
15 | import { UserModel } from '@server/models/user/user' | 14 | import { UserModel } from '@server/models/user/user' |
16 | import { VideoModel } from '@server/models/video/video' | 15 | import { VideoModel } from '@server/models/video/video' |
17 | import { VideoFileModel } from '@server/models/video/video-file' | 16 | import { 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' |
39 | import { logger, loggerTagsFactory } from '../../../helpers/logger' | 37 | import { logger, loggerTagsFactory } from '../../../helpers/logger' |
40 | 38 | ||
@@ -42,14 +40,15 @@ const lTagsBase = loggerTagsFactory('video-edition') | |||
42 | 40 | ||
43 | async function processVideoEdition (job: Job) { | 41 | async 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 | ||
127 | const taskProcessors: { [id in VideoEditorTask['name']]: (options: TaskProcessorOptions) => Promise<any> } = { | 122 | const taskProcessors: { [id in VideoEditorTask['name']]: (options: TaskProcessorOptions) => Promise<any> } = { |
@@ -134,7 +129,7 @@ const taskProcessors: { [id in VideoEditorTask['name']]: (options: TaskProcessor | |||
134 | async function processTask (options: TaskProcessorOptions) { | 129 | async 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 | ||
139 | async function cleanupTMPLiveFiles (hlsDirectory: string) { | 139 | 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 | |||
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 | ||
174 | async function onVideoFirstWebTorrentTranscoding ( | 174 | async 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 @@ | |||
1 | import { logger } from '@server/helpers/logger' | ||
2 | import { WEBSERVER } from '@server/initializers/constants' | ||
3 | import { UserModel } from '@server/models/user/user' | ||
4 | import { UserNotificationModel } from '@server/models/user/user-notification' | ||
5 | import { MUserDefault, MUserWithNotificationSetting, MVideoFullLight, UserNotificationModelForApi } from '@server/types/models' | ||
6 | import { UserNotificationType } from '@shared/models' | ||
7 | import { AbstractNotification } from '../common/abstract-notification' | ||
8 | |||
9 | export 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 @@ | |||
1 | export * from './new-video-for-subscribers' | 1 | export * from './new-video-for-subscribers' |
2 | export * from './edition-finished-for-owner' | ||
2 | export * from './import-finished-for-owner' | 3 | export * from './import-finished-for-owner' |
3 | export * from './owned-publication-after-auto-unblacklist' | 4 | export * from './owned-publication-after-auto-unblacklist' |
4 | export * from './owned-publication-after-schedule-update' | 5 | 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 | | |||
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 | ||
36 | function moveToNextState (video: MVideoUUID, isNewVideo = true) { | 37 | function 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 | ||
60 | async function moveToExternalStorageState (video: MVideoFullLight, isNewVideo: boolean, transaction: Transaction) { | 67 | async 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 | ||
106 | async function moveToPublishedState (video: MVideoFullLight, isNewVideo: boolean, transaction: Transaction) { | 120 | async 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' | |||
6 | import { VideoJobInfoModel } from '@server/models/video/video-job-info' | 6 | import { VideoJobInfoModel } from '@server/models/video/video-job-info' |
7 | import { FilteredModelAttributes } from '@server/types' | 7 | import { FilteredModelAttributes } from '@server/types' |
8 | import { MThumbnail, MUserId, MVideoFile, MVideoTag, MVideoThumbnail, MVideoUUID } from '@server/types/models' | 8 | import { MThumbnail, MUserId, MVideoFile, MVideoTag, MVideoThumbnail, MVideoUUID } from '@server/types/models' |
9 | import { ThumbnailType, VideoCreate, VideoPrivacy, VideoTranscodingPayload } from '@shared/models' | 9 | import { ThumbnailType, VideoCreate, VideoPrivacy, VideoState, VideoTranscodingPayload } from '@shared/models' |
10 | import { CreateJobOptions, JobQueue } from './job-queue/job-queue' | 10 | import { CreateJobOptions, JobQueue } from './job-queue/job-queue' |
11 | import { updateVideoMiniatureFromExisting } from './thumbnail' | 11 | import { updateVideoMiniatureFromExisting } from './thumbnail' |
12 | import { CONFIG } from '@server/initializers/config' | 12 | import { 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 | |||
70 | async function setVideoTags (options: { | 72 | async 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 | ||
84 | async function addOptimizeOrMergeAudioJob (video: MVideoUUID, videoFile: MVideoFile, user: MUserId, isNewVideo = true) { | 86 | // --------------------------------------------------------------------------- |
87 | |||
88 | async 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 | ||
116 | async 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 | |||
123 | async function getTranscodingJobPriority (user: MUserId) { | 127 | async 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 | ||
138 | async 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 | |||
134 | export { | 153 | export { |
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' |
16 | import { wait } from '@shared/core-utils' | 17 | import { wait } from '@shared/core-utils' |
17 | import { buildUUID } from '@shared/extra-utils' | 18 | import { buildUUID } from '@shared/extra-utils' |
18 | import { UserNotification, UserNotificationType, VideoPrivacy } from '@shared/models' | 19 | import { UserNotification, UserNotificationType, VideoEditorTask, VideoPrivacy } from '@shared/models' |
19 | import { cleanupTests, PeerTubeServer, waitJobs } from '@shared/server-commands' | 20 | import { cleanupTests, PeerTubeServer, waitJobs } from '@shared/server-commands' |
20 | 21 | ||
21 | const expect = chai.expect | 22 | const expect = chai.expect |
@@ -23,10 +24,12 @@ const expect = chai.expect | |||
23 | describe('Test user notifications', function () { | 24 | describe('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 | ||
113 | async 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 | |||
112 | async function checkMyVideoImportIsFinished (options: CheckerBaseParams & { | 141 | async 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 @@ | |||
1 | import { ContextType } from '../activitypub/context' | 1 | import { ContextType } from '../activitypub/context' |
2 | import { VideoState } from '../videos' | ||
2 | import { VideoEditorTaskCut } from '../videos/editor' | 3 | import { VideoEditorTaskCut } from '../videos/editor' |
3 | import { VideoResolution } from '../videos/file/video-resolution.enum' | 4 | import { VideoResolution } from '../videos/file/video-resolution.enum' |
4 | import { SendEmailOptions } from './emailer.model' | 5 | import { SendEmailOptions } from './emailer.model' |
@@ -116,6 +117,9 @@ export type ManageVideoTorrentPayload = | |||
116 | interface BaseTranscodingPayload { | 117 | interface 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 | ||
121 | export interface HLSTranscodingPayload extends BaseTranscodingPayload { | 125 | export interface HLSTranscodingPayload extends BaseTranscodingPayload { |
@@ -171,6 +175,7 @@ export interface DeleteResumableUploadMetaFilePayload { | |||
171 | export interface MoveObjectStoragePayload { | 175 | export interface MoveObjectStoragePayload { |
172 | videoUUID: string | 176 | videoUUID: string |
173 | isNewVideo: boolean | 177 | isNewVideo: boolean |
178 | previousVideoState: VideoState | ||
174 | } | 179 | } |
175 | 180 | ||
176 | export type VideoEditorTaskCutPayload = VideoEditorTaskCut | 181 | 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 { | |||
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 | ||
36 | export interface VideoInfo { | 38 | 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 { | |||
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 | ||