diff options
-rw-r--r-- | .eslintrc.json | 1 | ||||
-rwxr-xr-x | scripts/prune-storage.ts | 2 | ||||
-rw-r--r-- | server/controllers/api/video-playlist.ts | 19 | ||||
-rw-r--r-- | server/controllers/api/videos/import.ts | 10 | ||||
-rw-r--r-- | server/controllers/api/videos/index.ts | 2 | ||||
-rw-r--r-- | server/lib/activitypub/playlist.ts | 2 | ||||
-rw-r--r-- | server/lib/activitypub/videos.ts | 154 | ||||
-rw-r--r-- | server/lib/files-cache/videos-preview-cache.ts | 2 | ||||
-rw-r--r-- | server/lib/job-queue/handlers/video-import.ts | 12 | ||||
-rw-r--r-- | server/lib/job-queue/handlers/video-live-ending.ts | 12 | ||||
-rw-r--r-- | server/lib/thumbnail.ts | 110 | ||||
-rw-r--r-- | server/models/video/thumbnail.ts | 42 | ||||
-rw-r--r-- | server/models/video/video-playlist.ts | 1 | ||||
-rw-r--r-- | server/tests/api/server/follows.ts | 2 | ||||
-rw-r--r-- | shared/extra-utils/server/servers.ts | 2 |
15 files changed, 264 insertions, 109 deletions
diff --git a/.eslintrc.json b/.eslintrc.json index db8e6b9c5..fa6fb1b6f 100644 --- a/.eslintrc.json +++ b/.eslintrc.json | |||
@@ -23,6 +23,7 @@ | |||
23 | "consistent-as-needed" | 23 | "consistent-as-needed" |
24 | ], | 24 | ], |
25 | "padded-blocks": "off", | 25 | "padded-blocks": "off", |
26 | "prefer-regex-literals": "off", | ||
26 | "no-async-promise-executor": "off", | 27 | "no-async-promise-executor": "off", |
27 | "dot-notation": "off", | 28 | "dot-notation": "off", |
28 | "promise/param-names": "off", | 29 | "promise/param-names": "off", |
diff --git a/scripts/prune-storage.ts b/scripts/prune-storage.ts index 788d97997..dcb1fcf90 100755 --- a/scripts/prune-storage.ts +++ b/scripts/prune-storage.ts | |||
@@ -95,7 +95,7 @@ function doesVideoExist (keepOnlyOwned: boolean) { | |||
95 | 95 | ||
96 | function doesThumbnailExist (keepOnlyOwned: boolean, type: ThumbnailType) { | 96 | function doesThumbnailExist (keepOnlyOwned: boolean, type: ThumbnailType) { |
97 | return async (file: string) => { | 97 | return async (file: string) => { |
98 | const thumbnail = await ThumbnailModel.loadWithVideoByName(file, type) | 98 | const thumbnail = await ThumbnailModel.loadByFilename(file, type) |
99 | if (!thumbnail) return false | 99 | if (!thumbnail) return false |
100 | 100 | ||
101 | if (keepOnlyOwned) { | 101 | if (keepOnlyOwned) { |
diff --git a/server/controllers/api/video-playlist.ts b/server/controllers/api/video-playlist.ts index f3dc8b2a9..aab16533d 100644 --- a/server/controllers/api/video-playlist.ts +++ b/server/controllers/api/video-playlist.ts | |||
@@ -173,7 +173,11 @@ async function addVideoPlaylist (req: express.Request, res: express.Response) { | |||
173 | 173 | ||
174 | const thumbnailField = req.files['thumbnailfile'] | 174 | const thumbnailField = req.files['thumbnailfile'] |
175 | const thumbnailModel = thumbnailField | 175 | const thumbnailModel = thumbnailField |
176 | ? await createPlaylistMiniatureFromExisting(thumbnailField[0].path, videoPlaylist, false) | 176 | ? await createPlaylistMiniatureFromExisting({ |
177 | inputPath: thumbnailField[0].path, | ||
178 | playlist: videoPlaylist, | ||
179 | automaticallyGenerated: false | ||
180 | }) | ||
177 | : undefined | 181 | : undefined |
178 | 182 | ||
179 | const videoPlaylistCreated = await sequelizeTypescript.transaction(async t => { | 183 | const videoPlaylistCreated = await sequelizeTypescript.transaction(async t => { |
@@ -211,7 +215,11 @@ async function updateVideoPlaylist (req: express.Request, res: express.Response) | |||
211 | 215 | ||
212 | const thumbnailField = req.files['thumbnailfile'] | 216 | const thumbnailField = req.files['thumbnailfile'] |
213 | const thumbnailModel = thumbnailField | 217 | const thumbnailModel = thumbnailField |
214 | ? await createPlaylistMiniatureFromExisting(thumbnailField[0].path, videoPlaylistInstance, false) | 218 | ? await createPlaylistMiniatureFromExisting({ |
219 | inputPath: thumbnailField[0].path, | ||
220 | playlist: videoPlaylistInstance, | ||
221 | automaticallyGenerated: false | ||
222 | }) | ||
215 | : undefined | 223 | : undefined |
216 | 224 | ||
217 | try { | 225 | try { |
@@ -474,7 +482,12 @@ async function generateThumbnailForPlaylist (videoPlaylist: MVideoPlaylistThumbn | |||
474 | } | 482 | } |
475 | 483 | ||
476 | const inputPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, videoMiniature.filename) | 484 | const inputPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, videoMiniature.filename) |
477 | const thumbnailModel = await createPlaylistMiniatureFromExisting(inputPath, videoPlaylist, true, true) | 485 | const thumbnailModel = await createPlaylistMiniatureFromExisting({ |
486 | inputPath, | ||
487 | playlist: videoPlaylist, | ||
488 | automaticallyGenerated: true, | ||
489 | keepOriginal: true | ||
490 | }) | ||
478 | 491 | ||
479 | thumbnailModel.videoPlaylistId = videoPlaylist.id | 492 | thumbnailModel.videoPlaylistId = videoPlaylist.id |
480 | 493 | ||
diff --git a/server/controllers/api/videos/import.ts b/server/controllers/api/videos/import.ts index c689cb6f9..3b9b887e2 100644 --- a/server/controllers/api/videos/import.ts +++ b/server/controllers/api/videos/import.ts | |||
@@ -282,7 +282,7 @@ async function processPreview (req: express.Request, video: MVideoThumbnail): Pr | |||
282 | 282 | ||
283 | async function processThumbnailFromUrl (url: string, video: MVideoThumbnail) { | 283 | async function processThumbnailFromUrl (url: string, video: MVideoThumbnail) { |
284 | try { | 284 | try { |
285 | return createVideoMiniatureFromUrl(url, video, ThumbnailType.MINIATURE) | 285 | return createVideoMiniatureFromUrl({ downloadUrl: url, video, type: ThumbnailType.MINIATURE }) |
286 | } catch (err) { | 286 | } catch (err) { |
287 | logger.warn('Cannot generate video thumbnail %s for %s.', url, video.url, { err }) | 287 | logger.warn('Cannot generate video thumbnail %s for %s.', url, video.url, { err }) |
288 | return undefined | 288 | return undefined |
@@ -291,14 +291,14 @@ async function processThumbnailFromUrl (url: string, video: MVideoThumbnail) { | |||
291 | 291 | ||
292 | async function processPreviewFromUrl (url: string, video: MVideoThumbnail) { | 292 | async function processPreviewFromUrl (url: string, video: MVideoThumbnail) { |
293 | try { | 293 | try { |
294 | return createVideoMiniatureFromUrl(url, video, ThumbnailType.PREVIEW) | 294 | return createVideoMiniatureFromUrl({ downloadUrl: url, video, type: ThumbnailType.PREVIEW }) |
295 | } catch (err) { | 295 | } catch (err) { |
296 | logger.warn('Cannot generate video preview %s for %s.', url, video.url, { err }) | 296 | logger.warn('Cannot generate video preview %s for %s.', url, video.url, { err }) |
297 | return undefined | 297 | return undefined |
298 | } | 298 | } |
299 | } | 299 | } |
300 | 300 | ||
301 | function insertIntoDB (parameters: { | 301 | async function insertIntoDB (parameters: { |
302 | video: MVideoThumbnail | 302 | video: MVideoThumbnail |
303 | thumbnailModel: MThumbnail | 303 | thumbnailModel: MThumbnail |
304 | previewModel: MThumbnail | 304 | previewModel: MThumbnail |
@@ -309,7 +309,7 @@ function insertIntoDB (parameters: { | |||
309 | }): Promise<MVideoImportFormattable> { | 309 | }): Promise<MVideoImportFormattable> { |
310 | const { video, thumbnailModel, previewModel, videoChannel, tags, videoImportAttributes, user } = parameters | 310 | const { video, thumbnailModel, previewModel, videoChannel, tags, videoImportAttributes, user } = parameters |
311 | 311 | ||
312 | return sequelizeTypescript.transaction(async t => { | 312 | const videoImport = await sequelizeTypescript.transaction(async t => { |
313 | const sequelizeOptions = { transaction: t } | 313 | const sequelizeOptions = { transaction: t } |
314 | 314 | ||
315 | // Save video object in database | 315 | // Save video object in database |
@@ -339,4 +339,6 @@ function insertIntoDB (parameters: { | |||
339 | 339 | ||
340 | return videoImport | 340 | return videoImport |
341 | }) | 341 | }) |
342 | |||
343 | return videoImport | ||
342 | } | 344 | } |
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index c2c5eb640..9504c40a4 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts | |||
@@ -215,7 +215,7 @@ async function addVideo (req: express.Request, res: express.Response) { | |||
215 | const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({ | 215 | const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({ |
216 | video, | 216 | video, |
217 | files: req.files, | 217 | files: req.files, |
218 | fallback: type => generateVideoMiniature(video, videoFile, type) | 218 | fallback: type => generateVideoMiniature({ video, videoFile, type }) |
219 | }) | 219 | }) |
220 | 220 | ||
221 | // Create the torrent file | 221 | // Create the torrent file |
diff --git a/server/lib/activitypub/playlist.ts b/server/lib/activitypub/playlist.ts index 53298e968..d5a3ef7c8 100644 --- a/server/lib/activitypub/playlist.ts +++ b/server/lib/activitypub/playlist.ts | |||
@@ -103,7 +103,7 @@ async function createOrUpdateVideoPlaylist (playlistObject: PlaylistObject, byAc | |||
103 | 103 | ||
104 | if (playlistObject.icon) { | 104 | if (playlistObject.icon) { |
105 | try { | 105 | try { |
106 | const thumbnailModel = await createPlaylistMiniatureFromUrl(playlistObject.icon.url, refreshedPlaylist) | 106 | const thumbnailModel = await createPlaylistMiniatureFromUrl({ downloadUrl: playlistObject.icon.url, playlist: refreshedPlaylist }) |
107 | await refreshedPlaylist.setAndSaveThumbnail(thumbnailModel, undefined) | 107 | await refreshedPlaylist.setAndSaveThumbnail(thumbnailModel, undefined) |
108 | } catch (err) { | 108 | } catch (err) { |
109 | logger.warn('Cannot generate thumbnail of %s.', playlistObject.id, { err }) | 109 | logger.warn('Cannot generate thumbnail of %s.', playlistObject.id, { err }) |
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts index 201ef0302..66981f43f 100644 --- a/server/lib/activitypub/videos.ts +++ b/server/lib/activitypub/videos.ts | |||
@@ -313,7 +313,11 @@ async function updateVideoFromAP (options: { | |||
313 | let thumbnailModel: MThumbnail | 313 | let thumbnailModel: MThumbnail |
314 | 314 | ||
315 | try { | 315 | try { |
316 | thumbnailModel = await createVideoMiniatureFromUrl(getThumbnailFromIcons(videoObject).url, video, ThumbnailType.MINIATURE) | 316 | thumbnailModel = await createVideoMiniatureFromUrl({ |
317 | downloadUrl: getThumbnailFromIcons(videoObject).url, | ||
318 | video, | ||
319 | type: ThumbnailType.MINIATURE | ||
320 | }) | ||
317 | } catch (err) { | 321 | } catch (err) { |
318 | logger.warn('Cannot generate thumbnail of %s.', videoObject.id, { err }) | 322 | logger.warn('Cannot generate thumbnail of %s.', videoObject.id, { err }) |
319 | } | 323 | } |
@@ -362,7 +366,12 @@ async function updateVideoFromAP (options: { | |||
362 | 366 | ||
363 | if (videoUpdated.getPreview()) { | 367 | if (videoUpdated.getPreview()) { |
364 | const previewUrl = getPreviewUrl(getPreviewFromIcons(videoObject), video) | 368 | const previewUrl = getPreviewUrl(getPreviewFromIcons(videoObject), video) |
365 | const previewModel = createPlaceholderThumbnail(previewUrl, video, ThumbnailType.PREVIEW, PREVIEWS_SIZE) | 369 | const previewModel = createPlaceholderThumbnail({ |
370 | fileUrl: previewUrl, | ||
371 | video, | ||
372 | type: ThumbnailType.PREVIEW, | ||
373 | size: PREVIEWS_SIZE | ||
374 | }) | ||
366 | await videoUpdated.addAndSaveThumbnail(previewModel, t) | 375 | await videoUpdated.addAndSaveThumbnail(previewModel, t) |
367 | } | 376 | } |
368 | 377 | ||
@@ -585,11 +594,14 @@ async function createVideo (videoObject: VideoObject, channel: MChannelAccountLi | |||
585 | const videoData = await videoActivityObjectToDBAttributes(channel, videoObject, videoObject.to) | 594 | const videoData = await videoActivityObjectToDBAttributes(channel, videoObject, videoObject.to) |
586 | const video = VideoModel.build(videoData) as MVideoThumbnail | 595 | const video = VideoModel.build(videoData) as MVideoThumbnail |
587 | 596 | ||
588 | const promiseThumbnail = createVideoMiniatureFromUrl(getThumbnailFromIcons(videoObject).url, video, ThumbnailType.MINIATURE) | 597 | const promiseThumbnail = createVideoMiniatureFromUrl({ |
589 | .catch(err => { | 598 | downloadUrl: getThumbnailFromIcons(videoObject).url, |
590 | logger.error('Cannot create miniature from url.', { err }) | 599 | video, |
591 | return undefined | 600 | type: ThumbnailType.MINIATURE |
592 | }) | 601 | }).catch(err => { |
602 | logger.error('Cannot create miniature from url.', { err }) | ||
603 | return undefined | ||
604 | }) | ||
593 | 605 | ||
594 | let thumbnailModel: MThumbnail | 606 | let thumbnailModel: MThumbnail |
595 | if (waitThumbnail === true) { | 607 | if (waitThumbnail === true) { |
@@ -597,81 +609,93 @@ async function createVideo (videoObject: VideoObject, channel: MChannelAccountLi | |||
597 | } | 609 | } |
598 | 610 | ||
599 | const { autoBlacklisted, videoCreated } = await sequelizeTypescript.transaction(async t => { | 611 | const { autoBlacklisted, videoCreated } = await sequelizeTypescript.transaction(async t => { |
600 | const sequelizeOptions = { transaction: t } | 612 | try { |
613 | const sequelizeOptions = { transaction: t } | ||
601 | 614 | ||
602 | const videoCreated = await video.save(sequelizeOptions) as MVideoFullLight | 615 | const videoCreated = await video.save(sequelizeOptions) as MVideoFullLight |
603 | videoCreated.VideoChannel = channel | 616 | videoCreated.VideoChannel = channel |
604 | 617 | ||
605 | if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t) | 618 | if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t) |
606 | 619 | ||
607 | const previewIcon = getPreviewFromIcons(videoObject) | 620 | const previewUrl = getPreviewUrl(getPreviewFromIcons(videoObject), videoCreated) |
608 | const previewUrl = getPreviewUrl(previewIcon, videoCreated) | 621 | const previewModel = createPlaceholderThumbnail({ |
609 | const previewModel = createPlaceholderThumbnail(previewUrl, videoCreated, ThumbnailType.PREVIEW, PREVIEWS_SIZE) | 622 | fileUrl: previewUrl, |
623 | video: videoCreated, | ||
624 | type: ThumbnailType.PREVIEW, | ||
625 | size: PREVIEWS_SIZE | ||
626 | }) | ||
610 | 627 | ||
611 | if (thumbnailModel) await videoCreated.addAndSaveThumbnail(previewModel, t) | 628 | if (thumbnailModel) await videoCreated.addAndSaveThumbnail(previewModel, t) |
612 | 629 | ||
613 | // Process files | 630 | // Process files |
614 | const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoObject.url) | 631 | const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoObject.url) |
615 | 632 | ||
616 | const videoFilePromises = videoFileAttributes.map(f => VideoFileModel.create(f, { transaction: t })) | 633 | const videoFilePromises = videoFileAttributes.map(f => VideoFileModel.create(f, { transaction: t })) |
617 | const videoFiles = await Promise.all(videoFilePromises) | 634 | const videoFiles = await Promise.all(videoFilePromises) |
618 | 635 | ||
619 | const streamingPlaylistsAttributes = streamingPlaylistActivityUrlToDBAttributes(videoCreated, videoObject, videoFiles) | 636 | const streamingPlaylistsAttributes = streamingPlaylistActivityUrlToDBAttributes(videoCreated, videoObject, videoFiles) |
620 | videoCreated.VideoStreamingPlaylists = [] | 637 | videoCreated.VideoStreamingPlaylists = [] |
621 | 638 | ||
622 | for (const playlistAttributes of streamingPlaylistsAttributes) { | 639 | for (const playlistAttributes of streamingPlaylistsAttributes) { |
623 | const playlistModel = await VideoStreamingPlaylistModel.create(playlistAttributes, { transaction: t }) | 640 | const playlistModel = await VideoStreamingPlaylistModel.create(playlistAttributes, { transaction: t }) |
624 | 641 | ||
625 | const playlistFiles = videoFileActivityUrlToDBAttributes(playlistModel, playlistAttributes.tagAPObject) | 642 | const playlistFiles = videoFileActivityUrlToDBAttributes(playlistModel, playlistAttributes.tagAPObject) |
626 | const videoFilePromises = playlistFiles.map(f => VideoFileModel.create(f, { transaction: t })) | 643 | const videoFilePromises = playlistFiles.map(f => VideoFileModel.create(f, { transaction: t })) |
627 | playlistModel.VideoFiles = await Promise.all(videoFilePromises) | 644 | playlistModel.VideoFiles = await Promise.all(videoFilePromises) |
628 | 645 | ||
629 | videoCreated.VideoStreamingPlaylists.push(playlistModel) | 646 | videoCreated.VideoStreamingPlaylists.push(playlistModel) |
630 | } | 647 | } |
631 | 648 | ||
632 | // Process tags | 649 | // Process tags |
633 | const tags = videoObject.tag | 650 | const tags = videoObject.tag |
634 | .filter(isAPHashTagObject) | 651 | .filter(isAPHashTagObject) |
635 | .map(t => t.name) | 652 | .map(t => t.name) |
636 | await setVideoTags({ video: videoCreated, tags, transaction: t }) | 653 | await setVideoTags({ video: videoCreated, tags, transaction: t }) |
637 | 654 | ||
638 | // Process captions | 655 | // Process captions |
639 | const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => { | 656 | const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => { |
640 | const caption = new VideoCaptionModel({ | 657 | const caption = new VideoCaptionModel({ |
641 | videoId: videoCreated.id, | 658 | videoId: videoCreated.id, |
642 | filename: VideoCaptionModel.generateCaptionName(c.identifier), | 659 | filename: VideoCaptionModel.generateCaptionName(c.identifier), |
643 | language: c.identifier, | 660 | language: c.identifier, |
644 | fileUrl: c.url | 661 | fileUrl: c.url |
645 | }) as MVideoCaption | 662 | }) as MVideoCaption |
646 | 663 | ||
647 | return VideoCaptionModel.insertOrReplaceLanguage(caption, t) | 664 | return VideoCaptionModel.insertOrReplaceLanguage(caption, t) |
648 | }) | 665 | }) |
649 | await Promise.all(videoCaptionsPromises) | 666 | await Promise.all(videoCaptionsPromises) |
650 | 667 | ||
651 | videoCreated.VideoFiles = videoFiles | 668 | videoCreated.VideoFiles = videoFiles |
652 | 669 | ||
653 | if (videoCreated.isLive) { | 670 | if (videoCreated.isLive) { |
654 | const videoLive = new VideoLiveModel({ | 671 | const videoLive = new VideoLiveModel({ |
655 | streamKey: null, | 672 | streamKey: null, |
656 | saveReplay: videoObject.liveSaveReplay, | 673 | saveReplay: videoObject.liveSaveReplay, |
657 | permanentLive: videoObject.permanentLive, | 674 | permanentLive: videoObject.permanentLive, |
658 | videoId: videoCreated.id | 675 | videoId: videoCreated.id |
659 | }) | 676 | }) |
660 | 677 | ||
661 | videoCreated.VideoLive = await videoLive.save({ transaction: t }) | 678 | videoCreated.VideoLive = await videoLive.save({ transaction: t }) |
662 | } | 679 | } |
663 | 680 | ||
664 | const autoBlacklisted = await autoBlacklistVideoIfNeeded({ | 681 | const autoBlacklisted = await autoBlacklistVideoIfNeeded({ |
665 | video: videoCreated, | 682 | video: videoCreated, |
666 | user: undefined, | 683 | user: undefined, |
667 | isRemote: true, | 684 | isRemote: true, |
668 | isNew: true, | 685 | isNew: true, |
669 | transaction: t | 686 | transaction: t |
670 | }) | 687 | }) |
671 | 688 | ||
672 | logger.info('Remote video with uuid %s inserted.', videoObject.uuid) | 689 | logger.info('Remote video with uuid %s inserted.', videoObject.uuid) |
673 | 690 | ||
674 | return { autoBlacklisted, videoCreated } | 691 | return { autoBlacklisted, videoCreated } |
692 | } catch (err) { | ||
693 | // FIXME: Use rollback hook when https://github.com/sequelize/sequelize/pull/13038 is released | ||
694 | // Remove thumbnail | ||
695 | if (thumbnailModel) await thumbnailModel.removeThumbnail() | ||
696 | |||
697 | throw err | ||
698 | } | ||
675 | }) | 699 | }) |
676 | 700 | ||
677 | if (waitThumbnail === false) { | 701 | if (waitThumbnail === false) { |
diff --git a/server/lib/files-cache/videos-preview-cache.ts b/server/lib/files-cache/videos-preview-cache.ts index 47488da74..ee72cd3f9 100644 --- a/server/lib/files-cache/videos-preview-cache.ts +++ b/server/lib/files-cache/videos-preview-cache.ts | |||
@@ -20,7 +20,7 @@ class VideosPreviewCache extends AbstractVideoStaticFileCache <string> { | |||
20 | } | 20 | } |
21 | 21 | ||
22 | async getFilePathImpl (filename: string) { | 22 | async getFilePathImpl (filename: string) { |
23 | const thumbnail = await ThumbnailModel.loadWithVideoByName(filename, ThumbnailType.PREVIEW) | 23 | const thumbnail = await ThumbnailModel.loadWithVideoByFilename(filename, ThumbnailType.PREVIEW) |
24 | if (!thumbnail) return undefined | 24 | if (!thumbnail) return undefined |
25 | 25 | ||
26 | if (thumbnail.Video.isOwned()) return { isOwned: true, path: thumbnail.getPath() } | 26 | if (thumbnail.Video.isOwned()) return { isOwned: true, path: thumbnail.getPath() } |
diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts index 1e5e52b58..0d00c1b9d 100644 --- a/server/lib/job-queue/handlers/video-import.ts +++ b/server/lib/job-queue/handlers/video-import.ts | |||
@@ -162,7 +162,11 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid | |||
162 | let thumbnailModel: MThumbnail | 162 | let thumbnailModel: MThumbnail |
163 | let thumbnailSave: object | 163 | let thumbnailSave: object |
164 | if (!videoImportWithFiles.Video.getMiniature()) { | 164 | if (!videoImportWithFiles.Video.getMiniature()) { |
165 | thumbnailModel = await generateVideoMiniature(videoImportWithFiles.Video, videoFile, ThumbnailType.MINIATURE) | 165 | thumbnailModel = await generateVideoMiniature({ |
166 | video: videoImportWithFiles.Video, | ||
167 | videoFile, | ||
168 | type: ThumbnailType.MINIATURE | ||
169 | }) | ||
166 | thumbnailSave = thumbnailModel.toJSON() | 170 | thumbnailSave = thumbnailModel.toJSON() |
167 | } | 171 | } |
168 | 172 | ||
@@ -170,7 +174,11 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid | |||
170 | let previewModel: MThumbnail | 174 | let previewModel: MThumbnail |
171 | let previewSave: object | 175 | let previewSave: object |
172 | if (!videoImportWithFiles.Video.getPreview()) { | 176 | if (!videoImportWithFiles.Video.getPreview()) { |
173 | previewModel = await generateVideoMiniature(videoImportWithFiles.Video, videoFile, ThumbnailType.PREVIEW) | 177 | previewModel = await generateVideoMiniature({ |
178 | video: videoImportWithFiles.Video, | ||
179 | videoFile, | ||
180 | type: ThumbnailType.PREVIEW | ||
181 | }) | ||
174 | previewSave = previewModel.toJSON() | 182 | previewSave = previewModel.toJSON() |
175 | } | 183 | } |
176 | 184 | ||
diff --git a/server/lib/job-queue/handlers/video-live-ending.ts b/server/lib/job-queue/handlers/video-live-ending.ts index db6cd3682..6d50635bb 100644 --- a/server/lib/job-queue/handlers/video-live-ending.ts +++ b/server/lib/job-queue/handlers/video-live-ending.ts | |||
@@ -122,11 +122,19 @@ async function saveLive (video: MVideo, live: MVideoLive) { | |||
122 | 122 | ||
123 | // Regenerate the thumbnail & preview? | 123 | // Regenerate the thumbnail & preview? |
124 | if (videoWithFiles.getMiniature().automaticallyGenerated === true) { | 124 | if (videoWithFiles.getMiniature().automaticallyGenerated === true) { |
125 | await generateVideoMiniature(videoWithFiles, videoWithFiles.getMaxQualityFile(), ThumbnailType.MINIATURE) | 125 | await generateVideoMiniature({ |
126 | video: videoWithFiles, | ||
127 | videoFile: videoWithFiles.getMaxQualityFile(), | ||
128 | type: ThumbnailType.MINIATURE | ||
129 | }) | ||
126 | } | 130 | } |
127 | 131 | ||
128 | if (videoWithFiles.getPreview().automaticallyGenerated === true) { | 132 | if (videoWithFiles.getPreview().automaticallyGenerated === true) { |
129 | await generateVideoMiniature(videoWithFiles, videoWithFiles.getMaxQualityFile(), ThumbnailType.PREVIEW) | 133 | await generateVideoMiniature({ |
134 | video: videoWithFiles, | ||
135 | videoFile: videoWithFiles.getMaxQualityFile(), | ||
136 | type: ThumbnailType.PREVIEW | ||
137 | }) | ||
130 | } | 138 | } |
131 | 139 | ||
132 | await publishAndFederateIfNeeded(videoWithFiles, true) | 140 | await publishAndFederateIfNeeded(videoWithFiles, true) |
diff --git a/server/lib/thumbnail.ts b/server/lib/thumbnail.ts index 33aa7159c..55478299c 100644 --- a/server/lib/thumbnail.ts +++ b/server/lib/thumbnail.ts | |||
@@ -1,33 +1,48 @@ | |||
1 | import { join } from 'path' | ||
2 | |||
3 | import { ThumbnailType } from '../../shared/models/videos/thumbnail.type' | ||
1 | import { generateImageFromVideoFile } from '../helpers/ffmpeg-utils' | 4 | import { generateImageFromVideoFile } from '../helpers/ffmpeg-utils' |
5 | import { processImage } from '../helpers/image-utils' | ||
6 | import { downloadImage } from '../helpers/requests' | ||
2 | import { CONFIG } from '../initializers/config' | 7 | import { CONFIG } from '../initializers/config' |
3 | import { ASSETS_PATH, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '../initializers/constants' | 8 | import { ASSETS_PATH, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '../initializers/constants' |
4 | import { ThumbnailModel } from '../models/video/thumbnail' | 9 | import { ThumbnailModel } from '../models/video/thumbnail' |
5 | import { ThumbnailType } from '../../shared/models/videos/thumbnail.type' | ||
6 | import { processImage } from '../helpers/image-utils' | ||
7 | import { join } from 'path' | ||
8 | import { downloadImage } from '../helpers/requests' | ||
9 | import { MVideoPlaylistThumbnail } from '../types/models/video/video-playlist' | ||
10 | import { MVideoFile, MVideoThumbnail } from '../types/models' | 10 | import { MVideoFile, MVideoThumbnail } from '../types/models' |
11 | import { MThumbnail } from '../types/models/video/thumbnail' | 11 | import { MThumbnail } from '../types/models/video/thumbnail' |
12 | import { MVideoPlaylistThumbnail } from '../types/models/video/video-playlist' | ||
12 | import { getVideoFilePath } from './video-paths' | 13 | import { getVideoFilePath } from './video-paths' |
13 | 14 | ||
14 | type ImageSize = { height: number, width: number } | 15 | type ImageSize = { height: number, width: number } |
15 | 16 | ||
16 | function createPlaylistMiniatureFromExisting ( | 17 | function createPlaylistMiniatureFromExisting (options: { |
17 | inputPath: string, | 18 | inputPath: string |
18 | playlist: MVideoPlaylistThumbnail, | 19 | playlist: MVideoPlaylistThumbnail |
19 | automaticallyGenerated: boolean, | 20 | automaticallyGenerated: boolean |
20 | keepOriginal = false, | 21 | keepOriginal?: boolean // default to false |
21 | size?: ImageSize | 22 | size?: ImageSize |
22 | ) { | 23 | }) { |
24 | const { inputPath, playlist, automaticallyGenerated, keepOriginal = false, size } = options | ||
23 | const { filename, outputPath, height, width, existingThumbnail } = buildMetadataFromPlaylist(playlist, size) | 25 | const { filename, outputPath, height, width, existingThumbnail } = buildMetadataFromPlaylist(playlist, size) |
24 | const type = ThumbnailType.MINIATURE | 26 | const type = ThumbnailType.MINIATURE |
25 | 27 | ||
26 | const thumbnailCreator = () => processImage(inputPath, outputPath, { width, height }, keepOriginal) | 28 | const thumbnailCreator = () => processImage(inputPath, outputPath, { width, height }, keepOriginal) |
27 | return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, automaticallyGenerated, existingThumbnail }) | 29 | return createThumbnailFromFunction({ |
30 | thumbnailCreator, | ||
31 | filename, | ||
32 | height, | ||
33 | width, | ||
34 | type, | ||
35 | automaticallyGenerated, | ||
36 | existingThumbnail | ||
37 | }) | ||
28 | } | 38 | } |
29 | 39 | ||
30 | function createPlaylistMiniatureFromUrl (downloadUrl: string, playlist: MVideoPlaylistThumbnail, size?: ImageSize) { | 40 | function createPlaylistMiniatureFromUrl (options: { |
41 | downloadUrl: string | ||
42 | playlist: MVideoPlaylistThumbnail | ||
43 | size?: ImageSize | ||
44 | }) { | ||
45 | const { downloadUrl, playlist, size } = options | ||
31 | const { filename, basePath, height, width, existingThumbnail } = buildMetadataFromPlaylist(playlist, size) | 46 | const { filename, basePath, height, width, existingThumbnail } = buildMetadataFromPlaylist(playlist, size) |
32 | const type = ThumbnailType.MINIATURE | 47 | const type = ThumbnailType.MINIATURE |
33 | 48 | ||
@@ -40,7 +55,13 @@ function createPlaylistMiniatureFromUrl (downloadUrl: string, playlist: MVideoPl | |||
40 | return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl }) | 55 | return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl }) |
41 | } | 56 | } |
42 | 57 | ||
43 | function createVideoMiniatureFromUrl (downloadUrl: string, video: MVideoThumbnail, type: ThumbnailType, size?: ImageSize) { | 58 | function createVideoMiniatureFromUrl (options: { |
59 | downloadUrl: string | ||
60 | video: MVideoThumbnail | ||
61 | type: ThumbnailType | ||
62 | size?: ImageSize | ||
63 | }) { | ||
64 | const { downloadUrl, video, type, size } = options | ||
44 | const { filename, basePath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size) | 65 | const { filename, basePath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size) |
45 | 66 | ||
46 | // Only save the file URL if it is a remote video | 67 | // Only save the file URL if it is a remote video |
@@ -58,17 +79,31 @@ function createVideoMiniatureFromExisting (options: { | |||
58 | type: ThumbnailType | 79 | type: ThumbnailType |
59 | automaticallyGenerated: boolean | 80 | automaticallyGenerated: boolean |
60 | size?: ImageSize | 81 | size?: ImageSize |
61 | keepOriginal?: boolean | 82 | keepOriginal?: boolean // default to false |
62 | }) { | 83 | }) { |
63 | const { inputPath, video, type, automaticallyGenerated, size, keepOriginal } = options | 84 | const { inputPath, video, type, automaticallyGenerated, size, keepOriginal = false } = options |
64 | 85 | ||
65 | const { filename, outputPath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size) | 86 | const { filename, outputPath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size) |
66 | const thumbnailCreator = () => processImage(inputPath, outputPath, { width, height }, keepOriginal) | 87 | const thumbnailCreator = () => processImage(inputPath, outputPath, { width, height }, keepOriginal) |
67 | 88 | ||
68 | return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, automaticallyGenerated, existingThumbnail }) | 89 | return createThumbnailFromFunction({ |
90 | thumbnailCreator, | ||
91 | filename, | ||
92 | height, | ||
93 | width, | ||
94 | type, | ||
95 | automaticallyGenerated, | ||
96 | existingThumbnail | ||
97 | }) | ||
69 | } | 98 | } |
70 | 99 | ||
71 | function generateVideoMiniature (video: MVideoThumbnail, videoFile: MVideoFile, type: ThumbnailType) { | 100 | function generateVideoMiniature (options: { |
101 | video: MVideoThumbnail | ||
102 | videoFile: MVideoFile | ||
103 | type: ThumbnailType | ||
104 | }) { | ||
105 | const { video, videoFile, type } = options | ||
106 | |||
72 | const input = getVideoFilePath(video, videoFile) | 107 | const input = getVideoFilePath(video, videoFile) |
73 | 108 | ||
74 | const { filename, basePath, height, width, existingThumbnail, outputPath } = buildMetadataFromVideo(video, type) | 109 | const { filename, basePath, height, width, existingThumbnail, outputPath } = buildMetadataFromVideo(video, type) |
@@ -76,10 +111,24 @@ function generateVideoMiniature (video: MVideoThumbnail, videoFile: MVideoFile, | |||
76 | ? () => processImage(ASSETS_PATH.DEFAULT_AUDIO_BACKGROUND, outputPath, { width, height }, true) | 111 | ? () => processImage(ASSETS_PATH.DEFAULT_AUDIO_BACKGROUND, outputPath, { width, height }, true) |
77 | : () => generateImageFromVideoFile(input, basePath, filename, { height, width }) | 112 | : () => generateImageFromVideoFile(input, basePath, filename, { height, width }) |
78 | 113 | ||
79 | return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, automaticallyGenerated: true, existingThumbnail }) | 114 | return createThumbnailFromFunction({ |
115 | thumbnailCreator, | ||
116 | filename, | ||
117 | height, | ||
118 | width, | ||
119 | type, | ||
120 | automaticallyGenerated: true, | ||
121 | existingThumbnail | ||
122 | }) | ||
80 | } | 123 | } |
81 | 124 | ||
82 | function createPlaceholderThumbnail (fileUrl: string, video: MVideoThumbnail, type: ThumbnailType, size: ImageSize) { | 125 | function createPlaceholderThumbnail (options: { |
126 | fileUrl: string | ||
127 | video: MVideoThumbnail | ||
128 | type: ThumbnailType | ||
129 | size: ImageSize | ||
130 | }) { | ||
131 | const { fileUrl, video, type, size } = options | ||
83 | const { filename, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size) | 132 | const { filename, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size) |
84 | 133 | ||
85 | const thumbnail = existingThumbnail || new ThumbnailModel() | 134 | const thumbnail = existingThumbnail || new ThumbnailModel() |
@@ -164,12 +213,22 @@ async function createThumbnailFromFunction (parameters: { | |||
164 | fileUrl?: string | 213 | fileUrl?: string |
165 | existingThumbnail?: MThumbnail | 214 | existingThumbnail?: MThumbnail |
166 | }) { | 215 | }) { |
167 | const { thumbnailCreator, filename, width, height, type, existingThumbnail, automaticallyGenerated = null, fileUrl = null } = parameters | 216 | const { |
168 | 217 | thumbnailCreator, | |
169 | // Remove old file | 218 | filename, |
170 | if (existingThumbnail) await existingThumbnail.removeThumbnail() | 219 | width, |
220 | height, | ||
221 | type, | ||
222 | existingThumbnail, | ||
223 | automaticallyGenerated = null, | ||
224 | fileUrl = null | ||
225 | } = parameters | ||
226 | |||
227 | const oldFilename = existingThumbnail | ||
228 | ? existingThumbnail.filename | ||
229 | : undefined | ||
171 | 230 | ||
172 | const thumbnail = existingThumbnail || new ThumbnailModel() | 231 | const thumbnail: MThumbnail = existingThumbnail || new ThumbnailModel() |
173 | 232 | ||
174 | thumbnail.filename = filename | 233 | thumbnail.filename = filename |
175 | thumbnail.height = height | 234 | thumbnail.height = height |
@@ -177,6 +236,7 @@ async function createThumbnailFromFunction (parameters: { | |||
177 | thumbnail.type = type | 236 | thumbnail.type = type |
178 | thumbnail.fileUrl = fileUrl | 237 | thumbnail.fileUrl = fileUrl |
179 | thumbnail.automaticallyGenerated = automaticallyGenerated | 238 | thumbnail.automaticallyGenerated = automaticallyGenerated |
239 | thumbnail.previousThumbnailFilename = oldFilename | ||
180 | 240 | ||
181 | await thumbnailCreator() | 241 | await thumbnailCreator() |
182 | 242 | ||
diff --git a/server/models/video/thumbnail.ts b/server/models/video/thumbnail.ts index 3cad6c668..3d885f654 100644 --- a/server/models/video/thumbnail.ts +++ b/server/models/video/thumbnail.ts | |||
@@ -3,6 +3,8 @@ import { join } from 'path' | |||
3 | import { | 3 | import { |
4 | AfterDestroy, | 4 | AfterDestroy, |
5 | AllowNull, | 5 | AllowNull, |
6 | BeforeCreate, | ||
7 | BeforeUpdate, | ||
6 | BelongsTo, | 8 | BelongsTo, |
7 | Column, | 9 | Column, |
8 | CreatedAt, | 10 | CreatedAt, |
@@ -14,7 +16,8 @@ import { | |||
14 | UpdatedAt | 16 | UpdatedAt |
15 | } from 'sequelize-typescript' | 17 | } from 'sequelize-typescript' |
16 | import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub' | 18 | import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub' |
17 | import { MThumbnailVideo, MVideoAccountLight } from '@server/types/models' | 19 | import { afterCommitIfTransaction } from '@server/helpers/database-utils' |
20 | import { MThumbnail, MThumbnailVideo, MVideoAccountLight } from '@server/types/models' | ||
18 | import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' | 21 | import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' |
19 | import { logger } from '../../helpers/logger' | 22 | import { logger } from '../../helpers/logger' |
20 | import { CONFIG } from '../../initializers/config' | 23 | import { CONFIG } from '../../initializers/config' |
@@ -96,6 +99,9 @@ export class ThumbnailModel extends Model { | |||
96 | @UpdatedAt | 99 | @UpdatedAt |
97 | updatedAt: Date | 100 | updatedAt: Date |
98 | 101 | ||
102 | // If this thumbnail replaced existing one, track the old name | ||
103 | previousThumbnailFilename: string | ||
104 | |||
99 | private static readonly types: { [ id in ThumbnailType ]: { label: string, directory: string, staticPath: string } } = { | 105 | private static readonly types: { [ id in ThumbnailType ]: { label: string, directory: string, staticPath: string } } = { |
100 | [ThumbnailType.MINIATURE]: { | 106 | [ThumbnailType.MINIATURE]: { |
101 | label: 'miniature', | 107 | label: 'miniature', |
@@ -109,6 +115,12 @@ export class ThumbnailModel extends Model { | |||
109 | } | 115 | } |
110 | } | 116 | } |
111 | 117 | ||
118 | @BeforeCreate | ||
119 | @BeforeUpdate | ||
120 | static removeOldFile (instance: ThumbnailModel, options) { | ||
121 | return afterCommitIfTransaction(options.transaction, () => instance.removePreviousFilenameIfNeeded()) | ||
122 | } | ||
123 | |||
112 | @AfterDestroy | 124 | @AfterDestroy |
113 | static removeFiles (instance: ThumbnailModel) { | 125 | static removeFiles (instance: ThumbnailModel) { |
114 | logger.info('Removing %s file %s.', ThumbnailModel.types[instance.type].label, instance.filename) | 126 | logger.info('Removing %s file %s.', ThumbnailModel.types[instance.type].label, instance.filename) |
@@ -118,7 +130,18 @@ export class ThumbnailModel extends Model { | |||
118 | .catch(err => logger.error('Cannot remove thumbnail file %s.', instance.filename, err)) | 130 | .catch(err => logger.error('Cannot remove thumbnail file %s.', instance.filename, err)) |
119 | } | 131 | } |
120 | 132 | ||
121 | static loadWithVideoByName (filename: string, thumbnailType: ThumbnailType): Promise<MThumbnailVideo> { | 133 | static loadByFilename (filename: string, thumbnailType: ThumbnailType): Promise<MThumbnail> { |
134 | const query = { | ||
135 | where: { | ||
136 | filename, | ||
137 | type: thumbnailType | ||
138 | } | ||
139 | } | ||
140 | |||
141 | return ThumbnailModel.findOne(query) | ||
142 | } | ||
143 | |||
144 | static loadWithVideoByFilename (filename: string, thumbnailType: ThumbnailType): Promise<MThumbnailVideo> { | ||
122 | const query = { | 145 | const query = { |
123 | where: { | 146 | where: { |
124 | filename, | 147 | filename, |
@@ -150,7 +173,22 @@ export class ThumbnailModel extends Model { | |||
150 | return join(directory, this.filename) | 173 | return join(directory, this.filename) |
151 | } | 174 | } |
152 | 175 | ||
176 | getPreviousPath () { | ||
177 | const directory = ThumbnailModel.types[this.type].directory | ||
178 | return join(directory, this.previousThumbnailFilename) | ||
179 | } | ||
180 | |||
153 | removeThumbnail () { | 181 | removeThumbnail () { |
154 | return remove(this.getPath()) | 182 | return remove(this.getPath()) |
155 | } | 183 | } |
184 | |||
185 | removePreviousFilenameIfNeeded () { | ||
186 | if (!this.previousThumbnailFilename) return | ||
187 | |||
188 | const previousPath = this.getPreviousPath() | ||
189 | remove(previousPath) | ||
190 | .catch(err => logger.error('Cannot remove previous thumbnail file %s.', previousPath, { err })) | ||
191 | |||
192 | this.previousThumbnailFilename = undefined | ||
193 | } | ||
156 | } | 194 | } |
diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts index 9e6ff1f81..49a406608 100644 --- a/server/models/video/video-playlist.ts +++ b/server/models/video/video-playlist.ts | |||
@@ -17,6 +17,7 @@ import { | |||
17 | Table, | 17 | Table, |
18 | UpdatedAt | 18 | UpdatedAt |
19 | } from 'sequelize-typescript' | 19 | } from 'sequelize-typescript' |
20 | import { v4 as uuidv4 } from 'uuid' | ||
20 | import { MAccountId, MChannelId } from '@server/types/models' | 21 | import { MAccountId, MChannelId } from '@server/types/models' |
21 | import { ActivityIconObject } from '../../../shared/models/activitypub/objects' | 22 | import { ActivityIconObject } from '../../../shared/models/activitypub/objects' |
22 | import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' | 23 | import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' |
diff --git a/server/tests/api/server/follows.ts b/server/tests/api/server/follows.ts index 6467238cd..eb9ab10eb 100644 --- a/server/tests/api/server/follows.ts +++ b/server/tests/api/server/follows.ts | |||
@@ -558,7 +558,7 @@ describe('Test follows', function () { | |||
558 | const caption1: VideoCaption = res.body.data[0] | 558 | const caption1: VideoCaption = res.body.data[0] |
559 | expect(caption1.language.id).to.equal('ar') | 559 | expect(caption1.language.id).to.equal('ar') |
560 | expect(caption1.language.label).to.equal('Arabic') | 560 | expect(caption1.language.label).to.equal('Arabic') |
561 | expect(caption1.captionPath).to.equal('/lazy-static/video-captions/' + video4.uuid + '-ar.vtt') | 561 | expect(caption1.captionPath).to.match(new RegExp('^/lazy-static/video-captions/.+-ar.vtt$')) |
562 | await testCaptionFile(servers[0].url, caption1.captionPath, 'Subtitle good 2.') | 562 | await testCaptionFile(servers[0].url, caption1.captionPath, 'Subtitle good 2.') |
563 | }) | 563 | }) |
564 | 564 | ||
diff --git a/shared/extra-utils/server/servers.ts b/shared/extra-utils/server/servers.ts index 424639f87..08d05ef36 100644 --- a/shared/extra-utils/server/servers.ts +++ b/shared/extra-utils/server/servers.ts | |||
@@ -5,7 +5,7 @@ import { ChildProcess, exec, fork } from 'child_process' | |||
5 | import { copy, ensureDir, pathExists, readdir, readFile, remove } from 'fs-extra' | 5 | import { copy, ensureDir, pathExists, readdir, readFile, remove } from 'fs-extra' |
6 | import { join } from 'path' | 6 | import { join } from 'path' |
7 | import { randomInt } from '../../core-utils/miscs/miscs' | 7 | import { randomInt } from '../../core-utils/miscs/miscs' |
8 | import { Video, VideoChannel } from '../../models/videos' | 8 | import { VideoChannel } from '../../models/videos' |
9 | import { buildServerDirectory, getFileSize, isGithubCI, root, wait } from '../miscs/miscs' | 9 | import { buildServerDirectory, getFileSize, isGithubCI, root, wait } from '../miscs/miscs' |
10 | import { makeGetRequest } from '../requests/requests' | 10 | import { makeGetRequest } from '../requests/requests' |
11 | 11 | ||