aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--server/controllers/lazy-static.ts19
-rw-r--r--server/controllers/static.ts2
-rw-r--r--server/initializers/constants.ts2
-rw-r--r--server/lib/activitypub/videos/shared/abstract-builder.ts25
-rw-r--r--server/lib/activitypub/videos/shared/creator.ts81
-rw-r--r--server/lib/activitypub/videos/updater.ts11
-rw-r--r--server/lib/files-cache/avatar-permanent-file-cache.ts4
-rw-r--r--server/lib/files-cache/index.ts1
-rw-r--r--server/lib/files-cache/video-miniature-permanent-file-cache.ts28
-rw-r--r--server/lib/thumbnail.ts70
-rw-r--r--server/lib/video-pre-import.ts5
-rw-r--r--server/models/video/thumbnail.ts8
-rw-r--r--server/models/video/video-playlist.ts6
-rw-r--r--server/tests/api/videos/video-imports.ts2
-rw-r--r--support/doc/api/openapi.yaml2
15 files changed, 152 insertions, 114 deletions
diff --git a/server/controllers/lazy-static.ts b/server/controllers/lazy-static.ts
index 8e18b0642..dad30365c 100644
--- a/server/controllers/lazy-static.ts
+++ b/server/controllers/lazy-static.ts
@@ -6,6 +6,7 @@ import { FILES_CACHE, LAZY_STATIC_PATHS, STATIC_MAX_AGE } from '../initializers/
6import { 6import {
7 AvatarPermanentFileCache, 7 AvatarPermanentFileCache,
8 VideoCaptionsSimpleFileCache, 8 VideoCaptionsSimpleFileCache,
9 VideoMiniaturePermanentFileCache,
9 VideoPreviewsSimpleFileCache, 10 VideoPreviewsSimpleFileCache,
10 VideoStoryboardsSimpleFileCache, 11 VideoStoryboardsSimpleFileCache,
11 VideoTorrentsSimpleFileCache 12 VideoTorrentsSimpleFileCache
@@ -40,6 +41,12 @@ lazyStaticRouter.use(
40) 41)
41 42
42lazyStaticRouter.use( 43lazyStaticRouter.use(
44 LAZY_STATIC_PATHS.THUMBNAILS + ':filename',
45 asyncMiddleware(getThumbnail),
46 handleStaticError
47)
48
49lazyStaticRouter.use(
43 LAZY_STATIC_PATHS.PREVIEWS + ':filename', 50 LAZY_STATIC_PATHS.PREVIEWS + ':filename',
44 asyncMiddleware(getPreview), 51 asyncMiddleware(getPreview),
45 handleStaticError 52 handleStaticError
@@ -72,7 +79,6 @@ export {
72} 79}
73 80
74// --------------------------------------------------------------------------- 81// ---------------------------------------------------------------------------
75
76const avatarPermanentFileCache = new AvatarPermanentFileCache() 82const avatarPermanentFileCache = new AvatarPermanentFileCache()
77 83
78function getActorImage (req: express.Request, res: express.Response, next: express.NextFunction) { 84function getActorImage (req: express.Request, res: express.Response, next: express.NextFunction) {
@@ -81,6 +87,17 @@ function getActorImage (req: express.Request, res: express.Response, next: expre
81 return avatarPermanentFileCache.lazyServe({ filename, res, next }) 87 return avatarPermanentFileCache.lazyServe({ filename, res, next })
82} 88}
83 89
90// ---------------------------------------------------------------------------
91const videoMiniaturePermanentFileCache = new VideoMiniaturePermanentFileCache()
92
93function getThumbnail (req: express.Request, res: express.Response, next: express.NextFunction) {
94 const filename = req.params.filename
95
96 return videoMiniaturePermanentFileCache.lazyServe({ filename, res, next })
97}
98
99// ---------------------------------------------------------------------------
100
84async function getPreview (req: express.Request, res: express.Response) { 101async function getPreview (req: express.Request, res: express.Response) {
85 const result = await VideoPreviewsSimpleFileCache.Instance.getFilePath(req.params.filename) 102 const result = await VideoPreviewsSimpleFileCache.Instance.getFilePath(req.params.filename)
86 if (!result) return res.status(HttpStatusCode.NOT_FOUND_404).end() 103 if (!result) return res.status(HttpStatusCode.NOT_FOUND_404).end()
diff --git a/server/controllers/static.ts b/server/controllers/static.ts
index 9baff94c0..bbd0dd011 100644
--- a/server/controllers/static.ts
+++ b/server/controllers/static.ts
@@ -72,7 +72,7 @@ staticRouter.use(
72 handleStaticError 72 handleStaticError
73) 73)
74 74
75// Thumbnails path for express 75// FIXME: deprecated in v6, to remove
76const thumbnailsPhysicalPath = CONFIG.STORAGE.THUMBNAILS_DIR 76const thumbnailsPhysicalPath = CONFIG.STORAGE.THUMBNAILS_DIR
77staticRouter.use( 77staticRouter.use(
78 STATIC_PATHS.THUMBNAILS, 78 STATIC_PATHS.THUMBNAILS,
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 511aa91cc..ced18eef0 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -747,6 +747,7 @@ const NSFW_POLICY_TYPES: { [ id: string ]: NSFWPolicyType } = {
747 747
748// Express static paths (router) 748// Express static paths (router)
749const STATIC_PATHS = { 749const STATIC_PATHS = {
750 // TODO: deprecated in v6, to remove
750 THUMBNAILS: '/static/thumbnails/', 751 THUMBNAILS: '/static/thumbnails/',
751 752
752 WEBSEED: '/static/webseed/', 753 WEBSEED: '/static/webseed/',
@@ -765,6 +766,7 @@ const STATIC_DOWNLOAD_PATHS = {
765 HLS_VIDEOS: '/download/streaming-playlists/hls/videos/' 766 HLS_VIDEOS: '/download/streaming-playlists/hls/videos/'
766} 767}
767const LAZY_STATIC_PATHS = { 768const LAZY_STATIC_PATHS = {
769 THUMBNAILS: '/lazy-static/thumbnails/',
768 BANNERS: '/lazy-static/banners/', 770 BANNERS: '/lazy-static/banners/',
769 AVATARS: '/lazy-static/avatars/', 771 AVATARS: '/lazy-static/avatars/',
770 PREVIEWS: '/lazy-static/previews/', 772 PREVIEWS: '/lazy-static/previews/',
diff --git a/server/lib/activitypub/videos/shared/abstract-builder.ts b/server/lib/activitypub/videos/shared/abstract-builder.ts
index e50bf29dc..4f74316d3 100644
--- a/server/lib/activitypub/videos/shared/abstract-builder.ts
+++ b/server/lib/activitypub/videos/shared/abstract-builder.ts
@@ -1,7 +1,7 @@
1import { CreationAttributes, Transaction } from 'sequelize/types' 1import { CreationAttributes, Transaction } from 'sequelize/types'
2import { deleteAllModels, filterNonExistingModels } from '@server/helpers/database-utils' 2import { deleteAllModels, filterNonExistingModels } from '@server/helpers/database-utils'
3import { logger, LoggerTagsFn } from '@server/helpers/logger' 3import { logger, LoggerTagsFn } from '@server/helpers/logger'
4import { updateRemoteThumbnail, updateVideoMiniatureFromUrl } from '@server/lib/thumbnail' 4import { updateRemoteThumbnail } from '@server/lib/thumbnail'
5import { setVideoTags } from '@server/lib/video' 5import { setVideoTags } from '@server/lib/video'
6import { StoryboardModel } from '@server/models/video/storyboard' 6import { StoryboardModel } from '@server/models/video/storyboard'
7import { VideoCaptionModel } from '@server/models/video/video-caption' 7import { VideoCaptionModel } from '@server/models/video/video-caption'
@@ -11,7 +11,6 @@ import { VideoStreamingPlaylistModel } from '@server/models/video/video-streamin
11import { 11import {
12 MStreamingPlaylistFiles, 12 MStreamingPlaylistFiles,
13 MStreamingPlaylistFilesVideo, 13 MStreamingPlaylistFilesVideo,
14 MThumbnail,
15 MVideoCaption, 14 MVideoCaption,
16 MVideoFile, 15 MVideoFile,
17 MVideoFullLight, 16 MVideoFullLight,
@@ -42,16 +41,22 @@ export abstract class APVideoAbstractBuilder {
42 return getOrCreateAPActor(channel.id, 'all') 41 return getOrCreateAPActor(channel.id, 'all')
43 } 42 }
44 43
45 protected tryToGenerateThumbnail (video: MVideoThumbnail): Promise<MThumbnail> { 44 protected async setThumbnail (video: MVideoThumbnail, t?: Transaction) {
46 return updateVideoMiniatureFromUrl({ 45 const miniatureIcon = getThumbnailFromIcons(this.videoObject)
47 downloadUrl: getThumbnailFromIcons(this.videoObject).url, 46 if (!miniatureIcon) {
48 video, 47 logger.warn('Cannot find thumbnail in video object', { object: this.videoObject })
49 type: ThumbnailType.MINIATURE
50 }).catch(err => {
51 logger.warn('Cannot generate thumbnail of %s.', this.videoObject.id, { err, ...this.lTags() })
52
53 return undefined 48 return undefined
49 }
50
51 const miniatureModel = updateRemoteThumbnail({
52 fileUrl: miniatureIcon.url,
53 video,
54 type: ThumbnailType.MINIATURE,
55 size: miniatureIcon,
56 onDisk: false // Lazy download remote thumbnails
54 }) 57 })
58
59 await video.addAndSaveThumbnail(miniatureModel, t)
55 } 60 }
56 61
57 protected async setPreview (video: MVideoFullLight, t?: Transaction) { 62 protected async setPreview (video: MVideoFullLight, t?: Transaction) {
diff --git a/server/lib/activitypub/videos/shared/creator.ts b/server/lib/activitypub/videos/shared/creator.ts
index e6d7bc23c..3d646ef66 100644
--- a/server/lib/activitypub/videos/shared/creator.ts
+++ b/server/lib/activitypub/videos/shared/creator.ts
@@ -4,7 +4,7 @@ import { sequelizeTypescript } from '@server/initializers/database'
4import { Hooks } from '@server/lib/plugins/hooks' 4import { Hooks } from '@server/lib/plugins/hooks'
5import { autoBlacklistVideoIfNeeded } from '@server/lib/video-blacklist' 5import { autoBlacklistVideoIfNeeded } from '@server/lib/video-blacklist'
6import { VideoModel } from '@server/models/video/video' 6import { VideoModel } from '@server/models/video/video'
7import { MThumbnail, MVideoFullLight, MVideoThumbnail } from '@server/types/models' 7import { MVideoFullLight, MVideoThumbnail } from '@server/types/models'
8import { VideoObject } from '@shared/models' 8import { VideoObject } from '@shared/models'
9import { APVideoAbstractBuilder } from './abstract-builder' 9import { APVideoAbstractBuilder } from './abstract-builder'
10import { getVideoAttributesFromObject } from './object-to-model-attributes' 10import { getVideoAttributesFromObject } from './object-to-model-attributes'
@@ -27,65 +27,38 @@ export class APVideoCreator extends APVideoAbstractBuilder {
27 const videoData = getVideoAttributesFromObject(channel, this.videoObject, this.videoObject.to) 27 const videoData = getVideoAttributesFromObject(channel, this.videoObject, this.videoObject.to)
28 const video = VideoModel.build({ ...videoData, likes: 0, dislikes: 0 }) as MVideoThumbnail 28 const video = VideoModel.build({ ...videoData, likes: 0, dislikes: 0 }) as MVideoThumbnail
29 29
30 const promiseThumbnail = this.tryToGenerateThumbnail(video)
31
32 let thumbnailModel: MThumbnail
33 if (waitThumbnail === true) {
34 thumbnailModel = await promiseThumbnail
35 }
36
37 const { autoBlacklisted, videoCreated } = await sequelizeTypescript.transaction(async t => { 30 const { autoBlacklisted, videoCreated } = await sequelizeTypescript.transaction(async t => {
38 try { 31 const videoCreated = await video.save({ transaction: t }) as MVideoFullLight
39 const videoCreated = await video.save({ transaction: t }) as MVideoFullLight 32 videoCreated.VideoChannel = channel
40 videoCreated.VideoChannel = channel 33
41 34 await this.setThumbnail(videoCreated, t)
42 if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t) 35 await this.setPreview(videoCreated, t)
43 36 await this.setWebTorrentFiles(videoCreated, t)
44 await this.setPreview(videoCreated, t) 37 await this.setStreamingPlaylists(videoCreated, t)
45 await this.setWebTorrentFiles(videoCreated, t) 38 await this.setTags(videoCreated, t)
46 await this.setStreamingPlaylists(videoCreated, t) 39 await this.setTrackers(videoCreated, t)
47 await this.setTags(videoCreated, t) 40 await this.insertOrReplaceCaptions(videoCreated, t)
48 await this.setTrackers(videoCreated, t) 41 await this.insertOrReplaceLive(videoCreated, t)
49 await this.insertOrReplaceCaptions(videoCreated, t) 42 await this.insertOrReplaceStoryboard(videoCreated, t)
50 await this.insertOrReplaceLive(videoCreated, t) 43
51 await this.insertOrReplaceStoryboard(videoCreated, t) 44 // We added a video in this channel, set it as updated
52 45 await channel.setAsUpdated(t)
53 // We added a video in this channel, set it as updated 46
54 await channel.setAsUpdated(t) 47 const autoBlacklisted = await autoBlacklistVideoIfNeeded({
55 48 video: videoCreated,
56 const autoBlacklisted = await autoBlacklistVideoIfNeeded({ 49 user: undefined,
57 video: videoCreated, 50 isRemote: true,
58 user: undefined, 51 isNew: true,
59 isRemote: true, 52 transaction: t
60 isNew: true, 53 })
61 transaction: t
62 })
63
64 logger.info('Remote video with uuid %s inserted.', this.videoObject.uuid, this.lTags())
65 54
66 Hooks.runAction('action:activity-pub.remote-video.created', { video: videoCreated, videoAPObject: this.videoObject }) 55 logger.info('Remote video with uuid %s inserted.', this.videoObject.uuid, this.lTags())
67 56
68 return { autoBlacklisted, videoCreated } 57 Hooks.runAction('action:activity-pub.remote-video.created', { video: videoCreated, videoAPObject: this.videoObject })
69 } catch (err) {
70 // FIXME: Use rollback hook when https://github.com/sequelize/sequelize/pull/13038 is released
71 if (thumbnailModel) await thumbnailModel.removeThumbnail()
72 58
73 throw err 59 return { autoBlacklisted, videoCreated }
74 }
75 }) 60 })
76 61
77 if (waitThumbnail === false) {
78 // Error is already caught above
79 // eslint-disable-next-line @typescript-eslint/no-floating-promises
80 promiseThumbnail.then(thumbnailModel => {
81 if (!thumbnailModel) return
82
83 thumbnailModel = videoCreated.id
84
85 return thumbnailModel.save()
86 })
87 }
88
89 return { autoBlacklisted, videoCreated } 62 return { autoBlacklisted, videoCreated }
90 } 63 }
91} 64}
diff --git a/server/lib/activitypub/videos/updater.ts b/server/lib/activitypub/videos/updater.ts
index 3a0886523..c98bce662 100644
--- a/server/lib/activitypub/videos/updater.ts
+++ b/server/lib/activitypub/videos/updater.ts
@@ -41,7 +41,7 @@ export class APVideoUpdater extends APVideoAbstractBuilder {
41 try { 41 try {
42 const channelActor = await this.getOrCreateVideoChannelFromVideoObject() 42 const channelActor = await this.getOrCreateVideoChannelFromVideoObject()
43 43
44 const thumbnailModel = await this.tryToGenerateThumbnail(this.video) 44 const thumbnailModel = await this.setThumbnail(this.video)
45 45
46 this.checkChannelUpdateOrThrow(channelActor) 46 this.checkChannelUpdateOrThrow(channelActor)
47 47
@@ -58,8 +58,13 @@ export class APVideoUpdater extends APVideoAbstractBuilder {
58 runInReadCommittedTransaction(t => this.setTags(videoUpdated, t)), 58 runInReadCommittedTransaction(t => this.setTags(videoUpdated, t)),
59 runInReadCommittedTransaction(t => this.setTrackers(videoUpdated, t)), 59 runInReadCommittedTransaction(t => this.setTrackers(videoUpdated, t)),
60 runInReadCommittedTransaction(t => this.setStoryboard(videoUpdated, t)), 60 runInReadCommittedTransaction(t => this.setStoryboard(videoUpdated, t)),
61 this.setOrDeleteLive(videoUpdated), 61 runInReadCommittedTransaction(t => {
62 this.setPreview(videoUpdated) 62 return Promise.all([
63 this.setPreview(videoUpdated, t),
64 this.setThumbnail(videoUpdated, t)
65 ])
66 }),
67 this.setOrDeleteLive(videoUpdated)
63 ]) 68 ])
64 69
65 await runInReadCommittedTransaction(t => this.setCaptions(videoUpdated, t)) 70 await runInReadCommittedTransaction(t => this.setCaptions(videoUpdated, t))
diff --git a/server/lib/files-cache/avatar-permanent-file-cache.ts b/server/lib/files-cache/avatar-permanent-file-cache.ts
index 89228c5a5..1d77c5bc1 100644
--- a/server/lib/files-cache/avatar-permanent-file-cache.ts
+++ b/server/lib/files-cache/avatar-permanent-file-cache.ts
@@ -1,10 +1,10 @@
1import { CONFIG } from '@server/initializers/config'
1import { ACTOR_IMAGES_SIZE } from '@server/initializers/constants' 2import { ACTOR_IMAGES_SIZE } from '@server/initializers/constants'
2import { ActorImageModel } from '@server/models/actor/actor-image' 3import { ActorImageModel } from '@server/models/actor/actor-image'
3import { MActorImage } from '@server/types/models' 4import { MActorImage } from '@server/types/models'
4import { AbstractPermanentFileCache } from './shared' 5import { AbstractPermanentFileCache } from './shared'
5import { CONFIG } from '@server/initializers/config'
6 6
7export class AvatarPermanentFileCache extends AbstractPermanentFileCache<ActorImageModel> { 7export class AvatarPermanentFileCache extends AbstractPermanentFileCache<MActorImage> {
8 8
9 constructor () { 9 constructor () {
10 super(CONFIG.STORAGE.ACTOR_IMAGES) 10 super(CONFIG.STORAGE.ACTOR_IMAGES)
diff --git a/server/lib/files-cache/index.ts b/server/lib/files-cache/index.ts
index cc11d5385..5630a9b80 100644
--- a/server/lib/files-cache/index.ts
+++ b/server/lib/files-cache/index.ts
@@ -1,4 +1,5 @@
1export * from './avatar-permanent-file-cache' 1export * from './avatar-permanent-file-cache'
2export * from './video-miniature-permanent-file-cache'
2export * from './video-captions-simple-file-cache' 3export * from './video-captions-simple-file-cache'
3export * from './video-previews-simple-file-cache' 4export * from './video-previews-simple-file-cache'
4export * from './video-storyboards-simple-file-cache' 5export * from './video-storyboards-simple-file-cache'
diff --git a/server/lib/files-cache/video-miniature-permanent-file-cache.ts b/server/lib/files-cache/video-miniature-permanent-file-cache.ts
new file mode 100644
index 000000000..35d9466f7
--- /dev/null
+++ b/server/lib/files-cache/video-miniature-permanent-file-cache.ts
@@ -0,0 +1,28 @@
1import { CONFIG } from '@server/initializers/config'
2import { THUMBNAILS_SIZE } from '@server/initializers/constants'
3import { ThumbnailModel } from '@server/models/video/thumbnail'
4import { MThumbnail } from '@server/types/models'
5import { ThumbnailType } from '@shared/models'
6import { AbstractPermanentFileCache } from './shared'
7
8export class VideoMiniaturePermanentFileCache extends AbstractPermanentFileCache<MThumbnail> {
9
10 constructor () {
11 super(CONFIG.STORAGE.THUMBNAILS_DIR)
12 }
13
14 protected loadModel (filename: string) {
15 return ThumbnailModel.loadByFilename(filename, ThumbnailType.MINIATURE)
16 }
17
18 protected getImageSize (image: MThumbnail): { width: number, height: number } {
19 if (image.width && image.height) {
20 return {
21 height: image.height,
22 width: image.width
23 }
24 }
25
26 return THUMBNAILS_SIZE
27 }
28}
diff --git a/server/lib/thumbnail.ts b/server/lib/thumbnail.ts
index e792567ff..90f5dc2c8 100644
--- a/server/lib/thumbnail.ts
+++ b/server/lib/thumbnail.ts
@@ -60,38 +60,6 @@ function updatePlaylistMiniatureFromUrl (options: {
60 return updateThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl, onDisk: true }) 60 return updateThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl, onDisk: true })
61} 61}
62 62
63function updateVideoMiniatureFromUrl (options: {
64 downloadUrl: string
65 video: MVideoThumbnail
66 type: ThumbnailType
67 size?: ImageSize
68}) {
69 const { downloadUrl, video, type, size } = options
70 const { filename: updatedFilename, basePath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size)
71
72 // Only save the file URL if it is a remote video
73 const fileUrl = video.isOwned()
74 ? null
75 : downloadUrl
76
77 const thumbnailUrlChanged = hasThumbnailUrlChanged(existingThumbnail, downloadUrl, video)
78
79 // Do not change the thumbnail filename if the file did not change
80 const filename = thumbnailUrlChanged
81 ? updatedFilename
82 : existingThumbnail.filename
83
84 const thumbnailCreator = () => {
85 if (thumbnailUrlChanged) {
86 return downloadImageFromWorker({ url: downloadUrl, destDir: basePath, destName: filename, size: { width, height } })
87 }
88
89 return Promise.resolve()
90 }
91
92 return updateThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl, onDisk: true })
93}
94
95function updateLocalVideoMiniatureFromExisting (options: { 63function updateLocalVideoMiniatureFromExisting (options: {
96 inputPath: string 64 inputPath: string
97 video: MVideoThumbnail 65 video: MVideoThumbnail
@@ -157,6 +125,40 @@ function generateLocalVideoMiniature (options: {
157 }) 125 })
158} 126}
159 127
128// ---------------------------------------------------------------------------
129
130function updateVideoMiniatureFromUrl (options: {
131 downloadUrl: string
132 video: MVideoThumbnail
133 type: ThumbnailType
134 size?: ImageSize
135}) {
136 const { downloadUrl, video, type, size } = options
137 const { filename: updatedFilename, basePath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size)
138
139 // Only save the file URL if it is a remote video
140 const fileUrl = video.isOwned()
141 ? null
142 : downloadUrl
143
144 const thumbnailUrlChanged = hasThumbnailUrlChanged(existingThumbnail, downloadUrl, video)
145
146 // Do not change the thumbnail filename if the file did not change
147 const filename = thumbnailUrlChanged
148 ? updatedFilename
149 : existingThumbnail.filename
150
151 const thumbnailCreator = () => {
152 if (thumbnailUrlChanged) {
153 return downloadImageFromWorker({ url: downloadUrl, destDir: basePath, destName: filename, size: { width, height } })
154 }
155
156 return Promise.resolve()
157 }
158
159 return updateThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl, onDisk: true })
160}
161
160function updateRemoteThumbnail (options: { 162function updateRemoteThumbnail (options: {
161 fileUrl: string 163 fileUrl: string
162 video: MVideoThumbnail 164 video: MVideoThumbnail
@@ -167,12 +169,10 @@ function updateRemoteThumbnail (options: {
167 const { fileUrl, video, type, size, onDisk } = options 169 const { fileUrl, video, type, size, onDisk } = options
168 const { filename: generatedFilename, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size) 170 const { filename: generatedFilename, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size)
169 171
170 const thumbnailUrlChanged = hasThumbnailUrlChanged(existingThumbnail, fileUrl, video)
171
172 const thumbnail = existingThumbnail || new ThumbnailModel() 172 const thumbnail = existingThumbnail || new ThumbnailModel()
173 173
174 // Do not change the thumbnail filename if the file did not change 174 // Do not change the thumbnail filename if the file did not change
175 if (thumbnailUrlChanged) { 175 if (hasThumbnailUrlChanged(existingThumbnail, fileUrl, video)) {
176 thumbnail.filename = generatedFilename 176 thumbnail.filename = generatedFilename
177 } 177 }
178 178
diff --git a/server/lib/video-pre-import.ts b/server/lib/video-pre-import.ts
index ef9c38731..1471d4091 100644
--- a/server/lib/video-pre-import.ts
+++ b/server/lib/video-pre-import.ts
@@ -262,13 +262,16 @@ async function forgeThumbnail ({ inputPath, video, downloadUrl, type }: {
262 type, 262 type,
263 automaticallyGenerated: false 263 automaticallyGenerated: false
264 }) 264 })
265 } else if (downloadUrl) { 265 }
266
267 if (downloadUrl) {
266 try { 268 try {
267 return await updateVideoMiniatureFromUrl({ downloadUrl, video, type }) 269 return await updateVideoMiniatureFromUrl({ downloadUrl, video, type })
268 } catch (err) { 270 } catch (err) {
269 logger.warn('Cannot process thumbnail %s from youtube-dl.', downloadUrl, { err }) 271 logger.warn('Cannot process thumbnail %s from youtube-dl.', downloadUrl, { err })
270 } 272 }
271 } 273 }
274
272 return null 275 return null
273} 276}
274 277
diff --git a/server/models/video/thumbnail.ts b/server/models/video/thumbnail.ts
index 2a1f6a7b4..1722acdb4 100644
--- a/server/models/video/thumbnail.ts
+++ b/server/models/video/thumbnail.ts
@@ -21,7 +21,7 @@ import { AttributesOnly } from '@shared/typescript-utils'
21import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' 21import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
22import { logger } from '../../helpers/logger' 22import { logger } from '../../helpers/logger'
23import { CONFIG } from '../../initializers/config' 23import { CONFIG } from '../../initializers/config'
24import { CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, STATIC_PATHS, WEBSERVER } from '../../initializers/constants' 24import { CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, WEBSERVER } from '../../initializers/constants'
25import { VideoModel } from './video' 25import { VideoModel } from './video'
26import { VideoPlaylistModel } from './video-playlist' 26import { VideoPlaylistModel } from './video-playlist'
27 27
@@ -110,7 +110,7 @@ export class ThumbnailModel extends Model<Partial<AttributesOnly<ThumbnailModel>
110 [ThumbnailType.MINIATURE]: { 110 [ThumbnailType.MINIATURE]: {
111 label: 'miniature', 111 label: 'miniature',
112 directory: CONFIG.STORAGE.THUMBNAILS_DIR, 112 directory: CONFIG.STORAGE.THUMBNAILS_DIR,
113 staticPath: STATIC_PATHS.THUMBNAILS 113 staticPath: LAZY_STATIC_PATHS.THUMBNAILS
114 }, 114 },
115 [ThumbnailType.PREVIEW]: { 115 [ThumbnailType.PREVIEW]: {
116 label: 'preview', 116 label: 'preview',
@@ -201,4 +201,8 @@ export class ThumbnailModel extends Model<Partial<AttributesOnly<ThumbnailModel>
201 201
202 this.previousThumbnailFilename = undefined 202 this.previousThumbnailFilename = undefined
203 } 203 }
204
205 isOwned () {
206 return !this.fileUrl
207 }
204} 208}
diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts
index faf4bea78..15999d409 100644
--- a/server/models/video/video-playlist.ts
+++ b/server/models/video/video-playlist.ts
@@ -32,7 +32,7 @@ import {
32import { 32import {
33 ACTIVITY_PUB, 33 ACTIVITY_PUB,
34 CONSTRAINTS_FIELDS, 34 CONSTRAINTS_FIELDS,
35 STATIC_PATHS, 35 LAZY_STATIC_PATHS,
36 THUMBNAILS_SIZE, 36 THUMBNAILS_SIZE,
37 VIDEO_PLAYLIST_PRIVACIES, 37 VIDEO_PLAYLIST_PRIVACIES,
38 VIDEO_PLAYLIST_TYPES, 38 VIDEO_PLAYLIST_TYPES,
@@ -592,13 +592,13 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli
592 getThumbnailUrl () { 592 getThumbnailUrl () {
593 if (!this.hasThumbnail()) return null 593 if (!this.hasThumbnail()) return null
594 594
595 return WEBSERVER.URL + STATIC_PATHS.THUMBNAILS + this.Thumbnail.filename 595 return WEBSERVER.URL + LAZY_STATIC_PATHS.THUMBNAILS + this.Thumbnail.filename
596 } 596 }
597 597
598 getThumbnailStaticPath () { 598 getThumbnailStaticPath () {
599 if (!this.hasThumbnail()) return null 599 if (!this.hasThumbnail()) return null
600 600
601 return join(STATIC_PATHS.THUMBNAILS, this.Thumbnail.filename) 601 return join(LAZY_STATIC_PATHS.THUMBNAILS, this.Thumbnail.filename)
602 } 602 }
603 603
604 getWatchStaticPath () { 604 getWatchStaticPath () {
diff --git a/server/tests/api/videos/video-imports.ts b/server/tests/api/videos/video-imports.ts
index a684a55a0..4f3149d52 100644
--- a/server/tests/api/videos/video-imports.ts
+++ b/server/tests/api/videos/video-imports.ts
@@ -119,7 +119,7 @@ describe('Test video imports', function () {
119 expect(video.name).to.equal('small video - youtube') 119 expect(video.name).to.equal('small video - youtube')
120 120
121 { 121 {
122 expect(video.thumbnailPath).to.match(new RegExp(`^/static/thumbnails/.+.jpg$`)) 122 expect(video.thumbnailPath).to.match(new RegExp(`^/lazy-static/thumbnails/.+.jpg$`))
123 expect(video.previewPath).to.match(new RegExp(`^/lazy-static/previews/.+.jpg$`)) 123 expect(video.previewPath).to.match(new RegExp(`^/lazy-static/previews/.+.jpg$`))
124 124
125 const suffix = mode === 'yt-dlp' 125 const suffix = mode === 'yt-dlp'
diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml
index cd0e6ffd8..edf9990e3 100644
--- a/support/doc/api/openapi.yaml
+++ b/support/doc/api/openapi.yaml
@@ -7129,7 +7129,7 @@ components:
7129 maxLength: 120 7129 maxLength: 120
7130 thumbnailPath: 7130 thumbnailPath:
7131 type: string 7131 type: string
7132 example: /static/thumbnails/a65bc12f-9383-462e-81ae-8207e8b434ee.jpg 7132 example: /lazy-static/thumbnails/a65bc12f-9383-462e-81ae-8207e8b434ee.jpg
7133 previewPath: 7133 previewPath:
7134 type: string 7134 type: string
7135 example: /lazy-static/previews/a65bc12f-9383-462e-81ae-8207e8b434ee.jpg 7135 example: /lazy-static/previews/a65bc12f-9383-462e-81ae-8207e8b434ee.jpg