Support lazy download of remote video miniatures
This commit is contained in:
parent
f162d32da0
commit
bafaba0bcd
|
@ -6,6 +6,7 @@ import { FILES_CACHE, LAZY_STATIC_PATHS, STATIC_MAX_AGE } from '../initializers/
|
||||||
import {
|
import {
|
||||||
AvatarPermanentFileCache,
|
AvatarPermanentFileCache,
|
||||||
VideoCaptionsSimpleFileCache,
|
VideoCaptionsSimpleFileCache,
|
||||||
|
VideoMiniaturePermanentFileCache,
|
||||||
VideoPreviewsSimpleFileCache,
|
VideoPreviewsSimpleFileCache,
|
||||||
VideoStoryboardsSimpleFileCache,
|
VideoStoryboardsSimpleFileCache,
|
||||||
VideoTorrentsSimpleFileCache
|
VideoTorrentsSimpleFileCache
|
||||||
|
@ -39,6 +40,12 @@ lazyStaticRouter.use(
|
||||||
handleStaticError
|
handleStaticError
|
||||||
)
|
)
|
||||||
|
|
||||||
|
lazyStaticRouter.use(
|
||||||
|
LAZY_STATIC_PATHS.THUMBNAILS + ':filename',
|
||||||
|
asyncMiddleware(getThumbnail),
|
||||||
|
handleStaticError
|
||||||
|
)
|
||||||
|
|
||||||
lazyStaticRouter.use(
|
lazyStaticRouter.use(
|
||||||
LAZY_STATIC_PATHS.PREVIEWS + ':filename',
|
LAZY_STATIC_PATHS.PREVIEWS + ':filename',
|
||||||
asyncMiddleware(getPreview),
|
asyncMiddleware(getPreview),
|
||||||
|
@ -72,7 +79,6 @@ export {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const avatarPermanentFileCache = new AvatarPermanentFileCache()
|
const avatarPermanentFileCache = new AvatarPermanentFileCache()
|
||||||
|
|
||||||
function getActorImage (req: express.Request, res: express.Response, next: express.NextFunction) {
|
function getActorImage (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||||
|
@ -81,6 +87,17 @@ function getActorImage (req: express.Request, res: express.Response, next: expre
|
||||||
return avatarPermanentFileCache.lazyServe({ filename, res, next })
|
return avatarPermanentFileCache.lazyServe({ filename, res, next })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const videoMiniaturePermanentFileCache = new VideoMiniaturePermanentFileCache()
|
||||||
|
|
||||||
|
function getThumbnail (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||||
|
const filename = req.params.filename
|
||||||
|
|
||||||
|
return videoMiniaturePermanentFileCache.lazyServe({ filename, res, next })
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async function getPreview (req: express.Request, res: express.Response) {
|
async function getPreview (req: express.Request, res: express.Response) {
|
||||||
const result = await VideoPreviewsSimpleFileCache.Instance.getFilePath(req.params.filename)
|
const result = await VideoPreviewsSimpleFileCache.Instance.getFilePath(req.params.filename)
|
||||||
if (!result) return res.status(HttpStatusCode.NOT_FOUND_404).end()
|
if (!result) return res.status(HttpStatusCode.NOT_FOUND_404).end()
|
||||||
|
|
|
@ -72,7 +72,7 @@ staticRouter.use(
|
||||||
handleStaticError
|
handleStaticError
|
||||||
)
|
)
|
||||||
|
|
||||||
// Thumbnails path for express
|
// FIXME: deprecated in v6, to remove
|
||||||
const thumbnailsPhysicalPath = CONFIG.STORAGE.THUMBNAILS_DIR
|
const thumbnailsPhysicalPath = CONFIG.STORAGE.THUMBNAILS_DIR
|
||||||
staticRouter.use(
|
staticRouter.use(
|
||||||
STATIC_PATHS.THUMBNAILS,
|
STATIC_PATHS.THUMBNAILS,
|
||||||
|
|
|
@ -747,6 +747,7 @@ const NSFW_POLICY_TYPES: { [ id: string ]: NSFWPolicyType } = {
|
||||||
|
|
||||||
// Express static paths (router)
|
// Express static paths (router)
|
||||||
const STATIC_PATHS = {
|
const STATIC_PATHS = {
|
||||||
|
// TODO: deprecated in v6, to remove
|
||||||
THUMBNAILS: '/static/thumbnails/',
|
THUMBNAILS: '/static/thumbnails/',
|
||||||
|
|
||||||
WEBSEED: '/static/webseed/',
|
WEBSEED: '/static/webseed/',
|
||||||
|
@ -765,6 +766,7 @@ const STATIC_DOWNLOAD_PATHS = {
|
||||||
HLS_VIDEOS: '/download/streaming-playlists/hls/videos/'
|
HLS_VIDEOS: '/download/streaming-playlists/hls/videos/'
|
||||||
}
|
}
|
||||||
const LAZY_STATIC_PATHS = {
|
const LAZY_STATIC_PATHS = {
|
||||||
|
THUMBNAILS: '/lazy-static/thumbnails/',
|
||||||
BANNERS: '/lazy-static/banners/',
|
BANNERS: '/lazy-static/banners/',
|
||||||
AVATARS: '/lazy-static/avatars/',
|
AVATARS: '/lazy-static/avatars/',
|
||||||
PREVIEWS: '/lazy-static/previews/',
|
PREVIEWS: '/lazy-static/previews/',
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { CreationAttributes, Transaction } from 'sequelize/types'
|
import { CreationAttributes, Transaction } from 'sequelize/types'
|
||||||
import { deleteAllModels, filterNonExistingModels } from '@server/helpers/database-utils'
|
import { deleteAllModels, filterNonExistingModels } from '@server/helpers/database-utils'
|
||||||
import { logger, LoggerTagsFn } from '@server/helpers/logger'
|
import { logger, LoggerTagsFn } from '@server/helpers/logger'
|
||||||
import { updateRemoteThumbnail, updateVideoMiniatureFromUrl } from '@server/lib/thumbnail'
|
import { updateRemoteThumbnail } from '@server/lib/thumbnail'
|
||||||
import { setVideoTags } from '@server/lib/video'
|
import { setVideoTags } from '@server/lib/video'
|
||||||
import { StoryboardModel } from '@server/models/video/storyboard'
|
import { StoryboardModel } from '@server/models/video/storyboard'
|
||||||
import { VideoCaptionModel } from '@server/models/video/video-caption'
|
import { VideoCaptionModel } from '@server/models/video/video-caption'
|
||||||
|
@ -11,7 +11,6 @@ import { VideoStreamingPlaylistModel } from '@server/models/video/video-streamin
|
||||||
import {
|
import {
|
||||||
MStreamingPlaylistFiles,
|
MStreamingPlaylistFiles,
|
||||||
MStreamingPlaylistFilesVideo,
|
MStreamingPlaylistFilesVideo,
|
||||||
MThumbnail,
|
|
||||||
MVideoCaption,
|
MVideoCaption,
|
||||||
MVideoFile,
|
MVideoFile,
|
||||||
MVideoFullLight,
|
MVideoFullLight,
|
||||||
|
@ -42,16 +41,22 @@ export abstract class APVideoAbstractBuilder {
|
||||||
return getOrCreateAPActor(channel.id, 'all')
|
return getOrCreateAPActor(channel.id, 'all')
|
||||||
}
|
}
|
||||||
|
|
||||||
protected tryToGenerateThumbnail (video: MVideoThumbnail): Promise<MThumbnail> {
|
protected async setThumbnail (video: MVideoThumbnail, t?: Transaction) {
|
||||||
return updateVideoMiniatureFromUrl({
|
const miniatureIcon = getThumbnailFromIcons(this.videoObject)
|
||||||
downloadUrl: getThumbnailFromIcons(this.videoObject).url,
|
if (!miniatureIcon) {
|
||||||
video,
|
logger.warn('Cannot find thumbnail in video object', { object: this.videoObject })
|
||||||
type: ThumbnailType.MINIATURE
|
|
||||||
}).catch(err => {
|
|
||||||
logger.warn('Cannot generate thumbnail of %s.', this.videoObject.id, { err, ...this.lTags() })
|
|
||||||
|
|
||||||
return undefined
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const miniatureModel = updateRemoteThumbnail({
|
||||||
|
fileUrl: miniatureIcon.url,
|
||||||
|
video,
|
||||||
|
type: ThumbnailType.MINIATURE,
|
||||||
|
size: miniatureIcon,
|
||||||
|
onDisk: false // Lazy download remote thumbnails
|
||||||
})
|
})
|
||||||
|
|
||||||
|
await video.addAndSaveThumbnail(miniatureModel, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async setPreview (video: MVideoFullLight, t?: Transaction) {
|
protected async setPreview (video: MVideoFullLight, t?: Transaction) {
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { sequelizeTypescript } from '@server/initializers/database'
|
||||||
import { Hooks } from '@server/lib/plugins/hooks'
|
import { Hooks } from '@server/lib/plugins/hooks'
|
||||||
import { autoBlacklistVideoIfNeeded } from '@server/lib/video-blacklist'
|
import { autoBlacklistVideoIfNeeded } from '@server/lib/video-blacklist'
|
||||||
import { VideoModel } from '@server/models/video/video'
|
import { VideoModel } from '@server/models/video/video'
|
||||||
import { MThumbnail, MVideoFullLight, MVideoThumbnail } from '@server/types/models'
|
import { MVideoFullLight, MVideoThumbnail } from '@server/types/models'
|
||||||
import { VideoObject } from '@shared/models'
|
import { VideoObject } from '@shared/models'
|
||||||
import { APVideoAbstractBuilder } from './abstract-builder'
|
import { APVideoAbstractBuilder } from './abstract-builder'
|
||||||
import { getVideoAttributesFromObject } from './object-to-model-attributes'
|
import { getVideoAttributesFromObject } from './object-to-model-attributes'
|
||||||
|
@ -27,64 +27,37 @@ export class APVideoCreator extends APVideoAbstractBuilder {
|
||||||
const videoData = getVideoAttributesFromObject(channel, this.videoObject, this.videoObject.to)
|
const videoData = getVideoAttributesFromObject(channel, this.videoObject, this.videoObject.to)
|
||||||
const video = VideoModel.build({ ...videoData, likes: 0, dislikes: 0 }) as MVideoThumbnail
|
const video = VideoModel.build({ ...videoData, likes: 0, dislikes: 0 }) as MVideoThumbnail
|
||||||
|
|
||||||
const promiseThumbnail = this.tryToGenerateThumbnail(video)
|
|
||||||
|
|
||||||
let thumbnailModel: MThumbnail
|
|
||||||
if (waitThumbnail === true) {
|
|
||||||
thumbnailModel = await promiseThumbnail
|
|
||||||
}
|
|
||||||
|
|
||||||
const { autoBlacklisted, videoCreated } = await sequelizeTypescript.transaction(async t => {
|
const { autoBlacklisted, videoCreated } = await sequelizeTypescript.transaction(async t => {
|
||||||
try {
|
const videoCreated = await video.save({ transaction: t }) as MVideoFullLight
|
||||||
const videoCreated = await video.save({ transaction: t }) as MVideoFullLight
|
videoCreated.VideoChannel = channel
|
||||||
videoCreated.VideoChannel = channel
|
|
||||||
|
|
||||||
if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t)
|
await this.setThumbnail(videoCreated, t)
|
||||||
|
await this.setPreview(videoCreated, t)
|
||||||
|
await this.setWebTorrentFiles(videoCreated, t)
|
||||||
|
await this.setStreamingPlaylists(videoCreated, t)
|
||||||
|
await this.setTags(videoCreated, t)
|
||||||
|
await this.setTrackers(videoCreated, t)
|
||||||
|
await this.insertOrReplaceCaptions(videoCreated, t)
|
||||||
|
await this.insertOrReplaceLive(videoCreated, t)
|
||||||
|
await this.insertOrReplaceStoryboard(videoCreated, t)
|
||||||
|
|
||||||
await this.setPreview(videoCreated, t)
|
// We added a video in this channel, set it as updated
|
||||||
await this.setWebTorrentFiles(videoCreated, t)
|
await channel.setAsUpdated(t)
|
||||||
await this.setStreamingPlaylists(videoCreated, t)
|
|
||||||
await this.setTags(videoCreated, t)
|
|
||||||
await this.setTrackers(videoCreated, t)
|
|
||||||
await this.insertOrReplaceCaptions(videoCreated, t)
|
|
||||||
await this.insertOrReplaceLive(videoCreated, t)
|
|
||||||
await this.insertOrReplaceStoryboard(videoCreated, t)
|
|
||||||
|
|
||||||
// We added a video in this channel, set it as updated
|
const autoBlacklisted = await autoBlacklistVideoIfNeeded({
|
||||||
await channel.setAsUpdated(t)
|
video: videoCreated,
|
||||||
|
user: undefined,
|
||||||
const autoBlacklisted = await autoBlacklistVideoIfNeeded({
|
isRemote: true,
|
||||||
video: videoCreated,
|
isNew: true,
|
||||||
user: undefined,
|
transaction: t
|
||||||
isRemote: true,
|
|
||||||
isNew: true,
|
|
||||||
transaction: t
|
|
||||||
})
|
|
||||||
|
|
||||||
logger.info('Remote video with uuid %s inserted.', this.videoObject.uuid, this.lTags())
|
|
||||||
|
|
||||||
Hooks.runAction('action:activity-pub.remote-video.created', { video: videoCreated, videoAPObject: this.videoObject })
|
|
||||||
|
|
||||||
return { autoBlacklisted, videoCreated }
|
|
||||||
} catch (err) {
|
|
||||||
// FIXME: Use rollback hook when https://github.com/sequelize/sequelize/pull/13038 is released
|
|
||||||
if (thumbnailModel) await thumbnailModel.removeThumbnail()
|
|
||||||
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (waitThumbnail === false) {
|
|
||||||
// Error is already caught above
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
promiseThumbnail.then(thumbnailModel => {
|
|
||||||
if (!thumbnailModel) return
|
|
||||||
|
|
||||||
thumbnailModel = videoCreated.id
|
|
||||||
|
|
||||||
return thumbnailModel.save()
|
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
logger.info('Remote video with uuid %s inserted.', this.videoObject.uuid, this.lTags())
|
||||||
|
|
||||||
|
Hooks.runAction('action:activity-pub.remote-video.created', { video: videoCreated, videoAPObject: this.videoObject })
|
||||||
|
|
||||||
|
return { autoBlacklisted, videoCreated }
|
||||||
|
})
|
||||||
|
|
||||||
return { autoBlacklisted, videoCreated }
|
return { autoBlacklisted, videoCreated }
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,7 @@ export class APVideoUpdater extends APVideoAbstractBuilder {
|
||||||
try {
|
try {
|
||||||
const channelActor = await this.getOrCreateVideoChannelFromVideoObject()
|
const channelActor = await this.getOrCreateVideoChannelFromVideoObject()
|
||||||
|
|
||||||
const thumbnailModel = await this.tryToGenerateThumbnail(this.video)
|
const thumbnailModel = await this.setThumbnail(this.video)
|
||||||
|
|
||||||
this.checkChannelUpdateOrThrow(channelActor)
|
this.checkChannelUpdateOrThrow(channelActor)
|
||||||
|
|
||||||
|
@ -58,8 +58,13 @@ export class APVideoUpdater extends APVideoAbstractBuilder {
|
||||||
runInReadCommittedTransaction(t => this.setTags(videoUpdated, t)),
|
runInReadCommittedTransaction(t => this.setTags(videoUpdated, t)),
|
||||||
runInReadCommittedTransaction(t => this.setTrackers(videoUpdated, t)),
|
runInReadCommittedTransaction(t => this.setTrackers(videoUpdated, t)),
|
||||||
runInReadCommittedTransaction(t => this.setStoryboard(videoUpdated, t)),
|
runInReadCommittedTransaction(t => this.setStoryboard(videoUpdated, t)),
|
||||||
this.setOrDeleteLive(videoUpdated),
|
runInReadCommittedTransaction(t => {
|
||||||
this.setPreview(videoUpdated)
|
return Promise.all([
|
||||||
|
this.setPreview(videoUpdated, t),
|
||||||
|
this.setThumbnail(videoUpdated, t)
|
||||||
|
])
|
||||||
|
}),
|
||||||
|
this.setOrDeleteLive(videoUpdated)
|
||||||
])
|
])
|
||||||
|
|
||||||
await runInReadCommittedTransaction(t => this.setCaptions(videoUpdated, t))
|
await runInReadCommittedTransaction(t => this.setCaptions(videoUpdated, t))
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
|
import { CONFIG } from '@server/initializers/config'
|
||||||
import { ACTOR_IMAGES_SIZE } from '@server/initializers/constants'
|
import { ACTOR_IMAGES_SIZE } from '@server/initializers/constants'
|
||||||
import { ActorImageModel } from '@server/models/actor/actor-image'
|
import { ActorImageModel } from '@server/models/actor/actor-image'
|
||||||
import { MActorImage } from '@server/types/models'
|
import { MActorImage } from '@server/types/models'
|
||||||
import { AbstractPermanentFileCache } from './shared'
|
import { AbstractPermanentFileCache } from './shared'
|
||||||
import { CONFIG } from '@server/initializers/config'
|
|
||||||
|
|
||||||
export class AvatarPermanentFileCache extends AbstractPermanentFileCache<ActorImageModel> {
|
export class AvatarPermanentFileCache extends AbstractPermanentFileCache<MActorImage> {
|
||||||
|
|
||||||
constructor () {
|
constructor () {
|
||||||
super(CONFIG.STORAGE.ACTOR_IMAGES)
|
super(CONFIG.STORAGE.ACTOR_IMAGES)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
export * from './avatar-permanent-file-cache'
|
export * from './avatar-permanent-file-cache'
|
||||||
|
export * from './video-miniature-permanent-file-cache'
|
||||||
export * from './video-captions-simple-file-cache'
|
export * from './video-captions-simple-file-cache'
|
||||||
export * from './video-previews-simple-file-cache'
|
export * from './video-previews-simple-file-cache'
|
||||||
export * from './video-storyboards-simple-file-cache'
|
export * from './video-storyboards-simple-file-cache'
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { CONFIG } from '@server/initializers/config'
|
||||||
|
import { THUMBNAILS_SIZE } from '@server/initializers/constants'
|
||||||
|
import { ThumbnailModel } from '@server/models/video/thumbnail'
|
||||||
|
import { MThumbnail } from '@server/types/models'
|
||||||
|
import { ThumbnailType } from '@shared/models'
|
||||||
|
import { AbstractPermanentFileCache } from './shared'
|
||||||
|
|
||||||
|
export class VideoMiniaturePermanentFileCache extends AbstractPermanentFileCache<MThumbnail> {
|
||||||
|
|
||||||
|
constructor () {
|
||||||
|
super(CONFIG.STORAGE.THUMBNAILS_DIR)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected loadModel (filename: string) {
|
||||||
|
return ThumbnailModel.loadByFilename(filename, ThumbnailType.MINIATURE)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getImageSize (image: MThumbnail): { width: number, height: number } {
|
||||||
|
if (image.width && image.height) {
|
||||||
|
return {
|
||||||
|
height: image.height,
|
||||||
|
width: image.width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return THUMBNAILS_SIZE
|
||||||
|
}
|
||||||
|
}
|
|
@ -60,38 +60,6 @@ function updatePlaylistMiniatureFromUrl (options: {
|
||||||
return updateThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl, onDisk: true })
|
return updateThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl, onDisk: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateVideoMiniatureFromUrl (options: {
|
|
||||||
downloadUrl: string
|
|
||||||
video: MVideoThumbnail
|
|
||||||
type: ThumbnailType
|
|
||||||
size?: ImageSize
|
|
||||||
}) {
|
|
||||||
const { downloadUrl, video, type, size } = options
|
|
||||||
const { filename: updatedFilename, basePath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size)
|
|
||||||
|
|
||||||
// Only save the file URL if it is a remote video
|
|
||||||
const fileUrl = video.isOwned()
|
|
||||||
? null
|
|
||||||
: downloadUrl
|
|
||||||
|
|
||||||
const thumbnailUrlChanged = hasThumbnailUrlChanged(existingThumbnail, downloadUrl, video)
|
|
||||||
|
|
||||||
// Do not change the thumbnail filename if the file did not change
|
|
||||||
const filename = thumbnailUrlChanged
|
|
||||||
? updatedFilename
|
|
||||||
: existingThumbnail.filename
|
|
||||||
|
|
||||||
const thumbnailCreator = () => {
|
|
||||||
if (thumbnailUrlChanged) {
|
|
||||||
return downloadImageFromWorker({ url: downloadUrl, destDir: basePath, destName: filename, size: { width, height } })
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve()
|
|
||||||
}
|
|
||||||
|
|
||||||
return updateThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl, onDisk: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateLocalVideoMiniatureFromExisting (options: {
|
function updateLocalVideoMiniatureFromExisting (options: {
|
||||||
inputPath: string
|
inputPath: string
|
||||||
video: MVideoThumbnail
|
video: MVideoThumbnail
|
||||||
|
@ -157,6 +125,40 @@ function generateLocalVideoMiniature (options: {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function updateVideoMiniatureFromUrl (options: {
|
||||||
|
downloadUrl: string
|
||||||
|
video: MVideoThumbnail
|
||||||
|
type: ThumbnailType
|
||||||
|
size?: ImageSize
|
||||||
|
}) {
|
||||||
|
const { downloadUrl, video, type, size } = options
|
||||||
|
const { filename: updatedFilename, basePath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size)
|
||||||
|
|
||||||
|
// Only save the file URL if it is a remote video
|
||||||
|
const fileUrl = video.isOwned()
|
||||||
|
? null
|
||||||
|
: downloadUrl
|
||||||
|
|
||||||
|
const thumbnailUrlChanged = hasThumbnailUrlChanged(existingThumbnail, downloadUrl, video)
|
||||||
|
|
||||||
|
// Do not change the thumbnail filename if the file did not change
|
||||||
|
const filename = thumbnailUrlChanged
|
||||||
|
? updatedFilename
|
||||||
|
: existingThumbnail.filename
|
||||||
|
|
||||||
|
const thumbnailCreator = () => {
|
||||||
|
if (thumbnailUrlChanged) {
|
||||||
|
return downloadImageFromWorker({ url: downloadUrl, destDir: basePath, destName: filename, size: { width, height } })
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
return updateThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl, onDisk: true })
|
||||||
|
}
|
||||||
|
|
||||||
function updateRemoteThumbnail (options: {
|
function updateRemoteThumbnail (options: {
|
||||||
fileUrl: string
|
fileUrl: string
|
||||||
video: MVideoThumbnail
|
video: MVideoThumbnail
|
||||||
|
@ -167,12 +169,10 @@ function updateRemoteThumbnail (options: {
|
||||||
const { fileUrl, video, type, size, onDisk } = options
|
const { fileUrl, video, type, size, onDisk } = options
|
||||||
const { filename: generatedFilename, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size)
|
const { filename: generatedFilename, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size)
|
||||||
|
|
||||||
const thumbnailUrlChanged = hasThumbnailUrlChanged(existingThumbnail, fileUrl, video)
|
|
||||||
|
|
||||||
const thumbnail = existingThumbnail || new ThumbnailModel()
|
const thumbnail = existingThumbnail || new ThumbnailModel()
|
||||||
|
|
||||||
// Do not change the thumbnail filename if the file did not change
|
// Do not change the thumbnail filename if the file did not change
|
||||||
if (thumbnailUrlChanged) {
|
if (hasThumbnailUrlChanged(existingThumbnail, fileUrl, video)) {
|
||||||
thumbnail.filename = generatedFilename
|
thumbnail.filename = generatedFilename
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -262,13 +262,16 @@ async function forgeThumbnail ({ inputPath, video, downloadUrl, type }: {
|
||||||
type,
|
type,
|
||||||
automaticallyGenerated: false
|
automaticallyGenerated: false
|
||||||
})
|
})
|
||||||
} else if (downloadUrl) {
|
}
|
||||||
|
|
||||||
|
if (downloadUrl) {
|
||||||
try {
|
try {
|
||||||
return await updateVideoMiniatureFromUrl({ downloadUrl, video, type })
|
return await updateVideoMiniatureFromUrl({ downloadUrl, video, type })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn('Cannot process thumbnail %s from youtube-dl.', downloadUrl, { err })
|
logger.warn('Cannot process thumbnail %s from youtube-dl.', downloadUrl, { err })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ import { AttributesOnly } from '@shared/typescript-utils'
|
||||||
import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
|
import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
|
||||||
import { logger } from '../../helpers/logger'
|
import { logger } from '../../helpers/logger'
|
||||||
import { CONFIG } from '../../initializers/config'
|
import { CONFIG } from '../../initializers/config'
|
||||||
import { CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, STATIC_PATHS, WEBSERVER } from '../../initializers/constants'
|
import { CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, WEBSERVER } from '../../initializers/constants'
|
||||||
import { VideoModel } from './video'
|
import { VideoModel } from './video'
|
||||||
import { VideoPlaylistModel } from './video-playlist'
|
import { VideoPlaylistModel } from './video-playlist'
|
||||||
|
|
||||||
|
@ -110,7 +110,7 @@ export class ThumbnailModel extends Model<Partial<AttributesOnly<ThumbnailModel>
|
||||||
[ThumbnailType.MINIATURE]: {
|
[ThumbnailType.MINIATURE]: {
|
||||||
label: 'miniature',
|
label: 'miniature',
|
||||||
directory: CONFIG.STORAGE.THUMBNAILS_DIR,
|
directory: CONFIG.STORAGE.THUMBNAILS_DIR,
|
||||||
staticPath: STATIC_PATHS.THUMBNAILS
|
staticPath: LAZY_STATIC_PATHS.THUMBNAILS
|
||||||
},
|
},
|
||||||
[ThumbnailType.PREVIEW]: {
|
[ThumbnailType.PREVIEW]: {
|
||||||
label: 'preview',
|
label: 'preview',
|
||||||
|
@ -201,4 +201,8 @@ export class ThumbnailModel extends Model<Partial<AttributesOnly<ThumbnailModel>
|
||||||
|
|
||||||
this.previousThumbnailFilename = undefined
|
this.previousThumbnailFilename = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isOwned () {
|
||||||
|
return !this.fileUrl
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ import {
|
||||||
import {
|
import {
|
||||||
ACTIVITY_PUB,
|
ACTIVITY_PUB,
|
||||||
CONSTRAINTS_FIELDS,
|
CONSTRAINTS_FIELDS,
|
||||||
STATIC_PATHS,
|
LAZY_STATIC_PATHS,
|
||||||
THUMBNAILS_SIZE,
|
THUMBNAILS_SIZE,
|
||||||
VIDEO_PLAYLIST_PRIVACIES,
|
VIDEO_PLAYLIST_PRIVACIES,
|
||||||
VIDEO_PLAYLIST_TYPES,
|
VIDEO_PLAYLIST_TYPES,
|
||||||
|
@ -592,13 +592,13 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli
|
||||||
getThumbnailUrl () {
|
getThumbnailUrl () {
|
||||||
if (!this.hasThumbnail()) return null
|
if (!this.hasThumbnail()) return null
|
||||||
|
|
||||||
return WEBSERVER.URL + STATIC_PATHS.THUMBNAILS + this.Thumbnail.filename
|
return WEBSERVER.URL + LAZY_STATIC_PATHS.THUMBNAILS + this.Thumbnail.filename
|
||||||
}
|
}
|
||||||
|
|
||||||
getThumbnailStaticPath () {
|
getThumbnailStaticPath () {
|
||||||
if (!this.hasThumbnail()) return null
|
if (!this.hasThumbnail()) return null
|
||||||
|
|
||||||
return join(STATIC_PATHS.THUMBNAILS, this.Thumbnail.filename)
|
return join(LAZY_STATIC_PATHS.THUMBNAILS, this.Thumbnail.filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
getWatchStaticPath () {
|
getWatchStaticPath () {
|
||||||
|
|
|
@ -119,7 +119,7 @@ describe('Test video imports', function () {
|
||||||
expect(video.name).to.equal('small video - youtube')
|
expect(video.name).to.equal('small video - youtube')
|
||||||
|
|
||||||
{
|
{
|
||||||
expect(video.thumbnailPath).to.match(new RegExp(`^/static/thumbnails/.+.jpg$`))
|
expect(video.thumbnailPath).to.match(new RegExp(`^/lazy-static/thumbnails/.+.jpg$`))
|
||||||
expect(video.previewPath).to.match(new RegExp(`^/lazy-static/previews/.+.jpg$`))
|
expect(video.previewPath).to.match(new RegExp(`^/lazy-static/previews/.+.jpg$`))
|
||||||
|
|
||||||
const suffix = mode === 'yt-dlp'
|
const suffix = mode === 'yt-dlp'
|
||||||
|
|
|
@ -7129,7 +7129,7 @@ components:
|
||||||
maxLength: 120
|
maxLength: 120
|
||||||
thumbnailPath:
|
thumbnailPath:
|
||||||
type: string
|
type: string
|
||||||
example: /static/thumbnails/a65bc12f-9383-462e-81ae-8207e8b434ee.jpg
|
example: /lazy-static/thumbnails/a65bc12f-9383-462e-81ae-8207e8b434ee.jpg
|
||||||
previewPath:
|
previewPath:
|
||||||
type: string
|
type: string
|
||||||
example: /lazy-static/previews/a65bc12f-9383-462e-81ae-8207e8b434ee.jpg
|
example: /lazy-static/previews/a65bc12f-9383-462e-81ae-8207e8b434ee.jpg
|
||||||
|
|
Loading…
Reference in New Issue