aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2023-06-07 08:53:14 +0200
committerChocobozzz <me@florianbigard.com>2023-06-29 10:19:33 +0200
commitbafaba0bcda0c9fb553b9eebef3764994bb4ff60 (patch)
treebff9a580cda865cd81c91cd5e1b7b527df45dac1 /server/lib
parentf162d32da098aa55f6de2367142faa166edb7c08 (diff)
downloadPeerTube-bafaba0bcda0c9fb553b9eebef3764994bb4ff60.tar.gz
PeerTube-bafaba0bcda0c9fb553b9eebef3764994bb4ff60.tar.zst
PeerTube-bafaba0bcda0c9fb553b9eebef3764994bb4ff60.zip
Support lazy download of remote video miniatures
Diffstat (limited to 'server/lib')
-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
8 files changed, 120 insertions, 105 deletions
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