diff options
author | Chocobozzz <me@florianbigard.com> | 2023-06-07 08:53:14 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2023-06-29 10:19:33 +0200 |
commit | bafaba0bcda0c9fb553b9eebef3764994bb4ff60 (patch) | |
tree | bff9a580cda865cd81c91cd5e1b7b527df45dac1 /server/lib | |
parent | f162d32da098aa55f6de2367142faa166edb7c08 (diff) | |
download | PeerTube-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.ts | 25 | ||||
-rw-r--r-- | server/lib/activitypub/videos/shared/creator.ts | 81 | ||||
-rw-r--r-- | server/lib/activitypub/videos/updater.ts | 11 | ||||
-rw-r--r-- | server/lib/files-cache/avatar-permanent-file-cache.ts | 4 | ||||
-rw-r--r-- | server/lib/files-cache/index.ts | 1 | ||||
-rw-r--r-- | server/lib/files-cache/video-miniature-permanent-file-cache.ts | 28 | ||||
-rw-r--r-- | server/lib/thumbnail.ts | 70 | ||||
-rw-r--r-- | server/lib/video-pre-import.ts | 5 |
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 @@ | |||
1 | import { CreationAttributes, Transaction } from 'sequelize/types' | 1 | import { CreationAttributes, Transaction } from 'sequelize/types' |
2 | import { deleteAllModels, filterNonExistingModels } from '@server/helpers/database-utils' | 2 | import { deleteAllModels, filterNonExistingModels } from '@server/helpers/database-utils' |
3 | import { logger, LoggerTagsFn } from '@server/helpers/logger' | 3 | import { logger, LoggerTagsFn } from '@server/helpers/logger' |
4 | import { updateRemoteThumbnail, updateVideoMiniatureFromUrl } from '@server/lib/thumbnail' | 4 | import { updateRemoteThumbnail } from '@server/lib/thumbnail' |
5 | import { setVideoTags } from '@server/lib/video' | 5 | import { setVideoTags } from '@server/lib/video' |
6 | import { StoryboardModel } from '@server/models/video/storyboard' | 6 | import { StoryboardModel } from '@server/models/video/storyboard' |
7 | import { VideoCaptionModel } from '@server/models/video/video-caption' | 7 | import { VideoCaptionModel } from '@server/models/video/video-caption' |
@@ -11,7 +11,6 @@ import { VideoStreamingPlaylistModel } from '@server/models/video/video-streamin | |||
11 | import { | 11 | import { |
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' | |||
4 | import { Hooks } from '@server/lib/plugins/hooks' | 4 | import { Hooks } from '@server/lib/plugins/hooks' |
5 | import { autoBlacklistVideoIfNeeded } from '@server/lib/video-blacklist' | 5 | import { autoBlacklistVideoIfNeeded } from '@server/lib/video-blacklist' |
6 | import { VideoModel } from '@server/models/video/video' | 6 | import { VideoModel } from '@server/models/video/video' |
7 | import { MThumbnail, MVideoFullLight, MVideoThumbnail } from '@server/types/models' | 7 | import { MVideoFullLight, MVideoThumbnail } from '@server/types/models' |
8 | import { VideoObject } from '@shared/models' | 8 | import { VideoObject } from '@shared/models' |
9 | import { APVideoAbstractBuilder } from './abstract-builder' | 9 | import { APVideoAbstractBuilder } from './abstract-builder' |
10 | import { getVideoAttributesFromObject } from './object-to-model-attributes' | 10 | import { 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 @@ | |||
1 | import { CONFIG } from '@server/initializers/config' | ||
1 | import { ACTOR_IMAGES_SIZE } from '@server/initializers/constants' | 2 | import { ACTOR_IMAGES_SIZE } from '@server/initializers/constants' |
2 | import { ActorImageModel } from '@server/models/actor/actor-image' | 3 | import { ActorImageModel } from '@server/models/actor/actor-image' |
3 | import { MActorImage } from '@server/types/models' | 4 | import { MActorImage } from '@server/types/models' |
4 | import { AbstractPermanentFileCache } from './shared' | 5 | import { AbstractPermanentFileCache } from './shared' |
5 | import { CONFIG } from '@server/initializers/config' | ||
6 | 6 | ||
7 | export class AvatarPermanentFileCache extends AbstractPermanentFileCache<ActorImageModel> { | 7 | export 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 @@ | |||
1 | export * from './avatar-permanent-file-cache' | 1 | export * from './avatar-permanent-file-cache' |
2 | export * from './video-miniature-permanent-file-cache' | ||
2 | export * from './video-captions-simple-file-cache' | 3 | export * from './video-captions-simple-file-cache' |
3 | export * from './video-previews-simple-file-cache' | 4 | export * from './video-previews-simple-file-cache' |
4 | export * from './video-storyboards-simple-file-cache' | 5 | export * 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 @@ | |||
1 | import { CONFIG } from '@server/initializers/config' | ||
2 | import { THUMBNAILS_SIZE } from '@server/initializers/constants' | ||
3 | import { ThumbnailModel } from '@server/models/video/thumbnail' | ||
4 | import { MThumbnail } from '@server/types/models' | ||
5 | import { ThumbnailType } from '@shared/models' | ||
6 | import { AbstractPermanentFileCache } from './shared' | ||
7 | |||
8 | export 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 | ||
63 | function 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 | |||
95 | function updateLocalVideoMiniatureFromExisting (options: { | 63 | function 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 | |||
130 | function 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 | |||
160 | function updateRemoteThumbnail (options: { | 162 | function 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 | ||