aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.eslintrc.json1
-rwxr-xr-xscripts/prune-storage.ts2
-rw-r--r--server/controllers/api/video-playlist.ts19
-rw-r--r--server/controllers/api/videos/import.ts10
-rw-r--r--server/controllers/api/videos/index.ts2
-rw-r--r--server/lib/activitypub/playlist.ts2
-rw-r--r--server/lib/activitypub/videos.ts154
-rw-r--r--server/lib/files-cache/videos-preview-cache.ts2
-rw-r--r--server/lib/job-queue/handlers/video-import.ts12
-rw-r--r--server/lib/job-queue/handlers/video-live-ending.ts12
-rw-r--r--server/lib/thumbnail.ts110
-rw-r--r--server/models/video/thumbnail.ts42
-rw-r--r--server/models/video/video-playlist.ts1
-rw-r--r--server/tests/api/server/follows.ts2
-rw-r--r--shared/extra-utils/server/servers.ts2
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
96function doesThumbnailExist (keepOnlyOwned: boolean, type: ThumbnailType) { 96function 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
283async function processThumbnailFromUrl (url: string, video: MVideoThumbnail) { 283async 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
292async function processPreviewFromUrl (url: string, video: MVideoThumbnail) { 292async 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
301function insertIntoDB (parameters: { 301async 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 @@
1import { join } from 'path'
2
3import { ThumbnailType } from '../../shared/models/videos/thumbnail.type'
1import { generateImageFromVideoFile } from '../helpers/ffmpeg-utils' 4import { generateImageFromVideoFile } from '../helpers/ffmpeg-utils'
5import { processImage } from '../helpers/image-utils'
6import { downloadImage } from '../helpers/requests'
2import { CONFIG } from '../initializers/config' 7import { CONFIG } from '../initializers/config'
3import { ASSETS_PATH, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '../initializers/constants' 8import { ASSETS_PATH, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '../initializers/constants'
4import { ThumbnailModel } from '../models/video/thumbnail' 9import { ThumbnailModel } from '../models/video/thumbnail'
5import { ThumbnailType } from '../../shared/models/videos/thumbnail.type'
6import { processImage } from '../helpers/image-utils'
7import { join } from 'path'
8import { downloadImage } from '../helpers/requests'
9import { MVideoPlaylistThumbnail } from '../types/models/video/video-playlist'
10import { MVideoFile, MVideoThumbnail } from '../types/models' 10import { MVideoFile, MVideoThumbnail } from '../types/models'
11import { MThumbnail } from '../types/models/video/thumbnail' 11import { MThumbnail } from '../types/models/video/thumbnail'
12import { MVideoPlaylistThumbnail } from '../types/models/video/video-playlist'
12import { getVideoFilePath } from './video-paths' 13import { getVideoFilePath } from './video-paths'
13 14
14type ImageSize = { height: number, width: number } 15type ImageSize = { height: number, width: number }
15 16
16function createPlaylistMiniatureFromExisting ( 17function 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
30function createPlaylistMiniatureFromUrl (downloadUrl: string, playlist: MVideoPlaylistThumbnail, size?: ImageSize) { 40function 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
43function createVideoMiniatureFromUrl (downloadUrl: string, video: MVideoThumbnail, type: ThumbnailType, size?: ImageSize) { 58function 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
71function generateVideoMiniature (video: MVideoThumbnail, videoFile: MVideoFile, type: ThumbnailType) { 100function 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
82function createPlaceholderThumbnail (fileUrl: string, video: MVideoThumbnail, type: ThumbnailType, size: ImageSize) { 125function 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'
3import { 3import {
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'
16import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub' 18import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub'
17import { MThumbnailVideo, MVideoAccountLight } from '@server/types/models' 19import { afterCommitIfTransaction } from '@server/helpers/database-utils'
20import { MThumbnail, MThumbnailVideo, MVideoAccountLight } from '@server/types/models'
18import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' 21import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
19import { logger } from '../../helpers/logger' 22import { logger } from '../../helpers/logger'
20import { CONFIG } from '../../initializers/config' 23import { 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'
20import { v4 as uuidv4 } from 'uuid'
20import { MAccountId, MChannelId } from '@server/types/models' 21import { MAccountId, MChannelId } from '@server/types/models'
21import { ActivityIconObject } from '../../../shared/models/activitypub/objects' 22import { ActivityIconObject } from '../../../shared/models/activitypub/objects'
22import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' 23import { 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'
5import { copy, ensureDir, pathExists, readdir, readFile, remove } from 'fs-extra' 5import { copy, ensureDir, pathExists, readdir, readFile, remove } from 'fs-extra'
6import { join } from 'path' 6import { join } from 'path'
7import { randomInt } from '../../core-utils/miscs/miscs' 7import { randomInt } from '../../core-utils/miscs/miscs'
8import { Video, VideoChannel } from '../../models/videos' 8import { VideoChannel } from '../../models/videos'
9import { buildServerDirectory, getFileSize, isGithubCI, root, wait } from '../miscs/miscs' 9import { buildServerDirectory, getFileSize, isGithubCI, root, wait } from '../miscs/miscs'
10import { makeGetRequest } from '../requests/requests' 10import { makeGetRequest } from '../requests/requests'
11 11