Remove previous thumbnail if needed

pull/3746/head
Chocobozzz 2021-02-16 08:50:40 +01:00 committed by Chocobozzz
parent 6302d599cd
commit a35a22797c
15 changed files with 274 additions and 119 deletions

View File

@ -23,6 +23,7 @@
"consistent-as-needed" "consistent-as-needed"
], ],
"padded-blocks": "off", "padded-blocks": "off",
"prefer-regex-literals": "off",
"no-async-promise-executor": "off", "no-async-promise-executor": "off",
"dot-notation": "off", "dot-notation": "off",
"promise/param-names": "off", "promise/param-names": "off",

View File

@ -95,7 +95,7 @@ function doesVideoExist (keepOnlyOwned: boolean) {
function doesThumbnailExist (keepOnlyOwned: boolean, type: ThumbnailType) { function doesThumbnailExist (keepOnlyOwned: boolean, type: ThumbnailType) {
return async (file: string) => { return async (file: string) => {
const thumbnail = await ThumbnailModel.loadWithVideoByName(file, type) const thumbnail = await ThumbnailModel.loadByFilename(file, type)
if (!thumbnail) return false if (!thumbnail) return false
if (keepOnlyOwned) { if (keepOnlyOwned) {

View File

@ -173,7 +173,11 @@ async function addVideoPlaylist (req: express.Request, res: express.Response) {
const thumbnailField = req.files['thumbnailfile'] const thumbnailField = req.files['thumbnailfile']
const thumbnailModel = thumbnailField const thumbnailModel = thumbnailField
? await createPlaylistMiniatureFromExisting(thumbnailField[0].path, videoPlaylist, false) ? await createPlaylistMiniatureFromExisting({
inputPath: thumbnailField[0].path,
playlist: videoPlaylist,
automaticallyGenerated: false
})
: undefined : undefined
const videoPlaylistCreated = await sequelizeTypescript.transaction(async t => { const videoPlaylistCreated = await sequelizeTypescript.transaction(async t => {
@ -211,7 +215,11 @@ async function updateVideoPlaylist (req: express.Request, res: express.Response)
const thumbnailField = req.files['thumbnailfile'] const thumbnailField = req.files['thumbnailfile']
const thumbnailModel = thumbnailField const thumbnailModel = thumbnailField
? await createPlaylistMiniatureFromExisting(thumbnailField[0].path, videoPlaylistInstance, false) ? await createPlaylistMiniatureFromExisting({
inputPath: thumbnailField[0].path,
playlist: videoPlaylistInstance,
automaticallyGenerated: false
})
: undefined : undefined
try { try {
@ -474,7 +482,12 @@ async function generateThumbnailForPlaylist (videoPlaylist: MVideoPlaylistThumbn
} }
const inputPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, videoMiniature.filename) const inputPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, videoMiniature.filename)
const thumbnailModel = await createPlaylistMiniatureFromExisting(inputPath, videoPlaylist, true, true) const thumbnailModel = await createPlaylistMiniatureFromExisting({
inputPath,
playlist: videoPlaylist,
automaticallyGenerated: true,
keepOriginal: true
})
thumbnailModel.videoPlaylistId = videoPlaylist.id thumbnailModel.videoPlaylistId = videoPlaylist.id

View File

@ -282,7 +282,7 @@ async function processPreview (req: express.Request, video: MVideoThumbnail): Pr
async function processThumbnailFromUrl (url: string, video: MVideoThumbnail) { async function processThumbnailFromUrl (url: string, video: MVideoThumbnail) {
try { try {
return createVideoMiniatureFromUrl(url, video, ThumbnailType.MINIATURE) return createVideoMiniatureFromUrl({ downloadUrl: url, video, type: ThumbnailType.MINIATURE })
} catch (err) { } catch (err) {
logger.warn('Cannot generate video thumbnail %s for %s.', url, video.url, { err }) logger.warn('Cannot generate video thumbnail %s for %s.', url, video.url, { err })
return undefined return undefined
@ -291,14 +291,14 @@ async function processThumbnailFromUrl (url: string, video: MVideoThumbnail) {
async function processPreviewFromUrl (url: string, video: MVideoThumbnail) { async function processPreviewFromUrl (url: string, video: MVideoThumbnail) {
try { try {
return createVideoMiniatureFromUrl(url, video, ThumbnailType.PREVIEW) return createVideoMiniatureFromUrl({ downloadUrl: url, video, type: ThumbnailType.PREVIEW })
} catch (err) { } catch (err) {
logger.warn('Cannot generate video preview %s for %s.', url, video.url, { err }) logger.warn('Cannot generate video preview %s for %s.', url, video.url, { err })
return undefined return undefined
} }
} }
function insertIntoDB (parameters: { async function insertIntoDB (parameters: {
video: MVideoThumbnail video: MVideoThumbnail
thumbnailModel: MThumbnail thumbnailModel: MThumbnail
previewModel: MThumbnail previewModel: MThumbnail
@ -309,7 +309,7 @@ function insertIntoDB (parameters: {
}): Promise<MVideoImportFormattable> { }): Promise<MVideoImportFormattable> {
const { video, thumbnailModel, previewModel, videoChannel, tags, videoImportAttributes, user } = parameters const { video, thumbnailModel, previewModel, videoChannel, tags, videoImportAttributes, user } = parameters
return sequelizeTypescript.transaction(async t => { const videoImport = await sequelizeTypescript.transaction(async t => {
const sequelizeOptions = { transaction: t } const sequelizeOptions = { transaction: t }
// Save video object in database // Save video object in database
@ -339,4 +339,6 @@ function insertIntoDB (parameters: {
return videoImport return videoImport
}) })
return videoImport
} }

View File

@ -215,7 +215,7 @@ async function addVideo (req: express.Request, res: express.Response) {
const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({ const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({
video, video,
files: req.files, files: req.files,
fallback: type => generateVideoMiniature(video, videoFile, type) fallback: type => generateVideoMiniature({ video, videoFile, type })
}) })
// Create the torrent file // Create the torrent file

View File

@ -103,7 +103,7 @@ async function createOrUpdateVideoPlaylist (playlistObject: PlaylistObject, byAc
if (playlistObject.icon) { if (playlistObject.icon) {
try { try {
const thumbnailModel = await createPlaylistMiniatureFromUrl(playlistObject.icon.url, refreshedPlaylist) const thumbnailModel = await createPlaylistMiniatureFromUrl({ downloadUrl: playlistObject.icon.url, playlist: refreshedPlaylist })
await refreshedPlaylist.setAndSaveThumbnail(thumbnailModel, undefined) await refreshedPlaylist.setAndSaveThumbnail(thumbnailModel, undefined)
} catch (err) { } catch (err) {
logger.warn('Cannot generate thumbnail of %s.', playlistObject.id, { err }) logger.warn('Cannot generate thumbnail of %s.', playlistObject.id, { err })

View File

@ -313,7 +313,11 @@ async function updateVideoFromAP (options: {
let thumbnailModel: MThumbnail let thumbnailModel: MThumbnail
try { try {
thumbnailModel = await createVideoMiniatureFromUrl(getThumbnailFromIcons(videoObject).url, video, ThumbnailType.MINIATURE) thumbnailModel = await createVideoMiniatureFromUrl({
downloadUrl: getThumbnailFromIcons(videoObject).url,
video,
type: ThumbnailType.MINIATURE
})
} catch (err) { } catch (err) {
logger.warn('Cannot generate thumbnail of %s.', videoObject.id, { err }) logger.warn('Cannot generate thumbnail of %s.', videoObject.id, { err })
} }
@ -362,7 +366,12 @@ async function updateVideoFromAP (options: {
if (videoUpdated.getPreview()) { if (videoUpdated.getPreview()) {
const previewUrl = getPreviewUrl(getPreviewFromIcons(videoObject), video) const previewUrl = getPreviewUrl(getPreviewFromIcons(videoObject), video)
const previewModel = createPlaceholderThumbnail(previewUrl, video, ThumbnailType.PREVIEW, PREVIEWS_SIZE) const previewModel = createPlaceholderThumbnail({
fileUrl: previewUrl,
video,
type: ThumbnailType.PREVIEW,
size: PREVIEWS_SIZE
})
await videoUpdated.addAndSaveThumbnail(previewModel, t) await videoUpdated.addAndSaveThumbnail(previewModel, t)
} }
@ -585,8 +594,11 @@ async function createVideo (videoObject: VideoObject, channel: MChannelAccountLi
const videoData = await videoActivityObjectToDBAttributes(channel, videoObject, videoObject.to) const videoData = await videoActivityObjectToDBAttributes(channel, videoObject, videoObject.to)
const video = VideoModel.build(videoData) as MVideoThumbnail const video = VideoModel.build(videoData) as MVideoThumbnail
const promiseThumbnail = createVideoMiniatureFromUrl(getThumbnailFromIcons(videoObject).url, video, ThumbnailType.MINIATURE) const promiseThumbnail = createVideoMiniatureFromUrl({
.catch(err => { downloadUrl: getThumbnailFromIcons(videoObject).url,
video,
type: ThumbnailType.MINIATURE
}).catch(err => {
logger.error('Cannot create miniature from url.', { err }) logger.error('Cannot create miniature from url.', { err })
return undefined return undefined
}) })
@ -597,6 +609,7 @@ async function createVideo (videoObject: VideoObject, channel: MChannelAccountLi
} }
const { autoBlacklisted, videoCreated } = await sequelizeTypescript.transaction(async t => { const { autoBlacklisted, videoCreated } = await sequelizeTypescript.transaction(async t => {
try {
const sequelizeOptions = { transaction: t } const sequelizeOptions = { transaction: t }
const videoCreated = await video.save(sequelizeOptions) as MVideoFullLight const videoCreated = await video.save(sequelizeOptions) as MVideoFullLight
@ -604,9 +617,13 @@ async function createVideo (videoObject: VideoObject, channel: MChannelAccountLi
if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t) if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t)
const previewIcon = getPreviewFromIcons(videoObject) const previewUrl = getPreviewUrl(getPreviewFromIcons(videoObject), videoCreated)
const previewUrl = getPreviewUrl(previewIcon, videoCreated) const previewModel = createPlaceholderThumbnail({
const previewModel = createPlaceholderThumbnail(previewUrl, videoCreated, ThumbnailType.PREVIEW, PREVIEWS_SIZE) fileUrl: previewUrl,
video: videoCreated,
type: ThumbnailType.PREVIEW,
size: PREVIEWS_SIZE
})
if (thumbnailModel) await videoCreated.addAndSaveThumbnail(previewModel, t) if (thumbnailModel) await videoCreated.addAndSaveThumbnail(previewModel, t)
@ -672,6 +689,13 @@ async function createVideo (videoObject: VideoObject, channel: MChannelAccountLi
logger.info('Remote video with uuid %s inserted.', videoObject.uuid) logger.info('Remote video with uuid %s inserted.', videoObject.uuid)
return { autoBlacklisted, videoCreated } return { autoBlacklisted, videoCreated }
} catch (err) {
// FIXME: Use rollback hook when https://github.com/sequelize/sequelize/pull/13038 is released
// Remove thumbnail
if (thumbnailModel) await thumbnailModel.removeThumbnail()
throw err
}
}) })
if (waitThumbnail === false) { if (waitThumbnail === false) {

View File

@ -20,7 +20,7 @@ class VideosPreviewCache extends AbstractVideoStaticFileCache <string> {
} }
async getFilePathImpl (filename: string) { async getFilePathImpl (filename: string) {
const thumbnail = await ThumbnailModel.loadWithVideoByName(filename, ThumbnailType.PREVIEW) const thumbnail = await ThumbnailModel.loadWithVideoByFilename(filename, ThumbnailType.PREVIEW)
if (!thumbnail) return undefined if (!thumbnail) return undefined
if (thumbnail.Video.isOwned()) return { isOwned: true, path: thumbnail.getPath() } if (thumbnail.Video.isOwned()) return { isOwned: true, path: thumbnail.getPath() }

View File

@ -162,7 +162,11 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid
let thumbnailModel: MThumbnail let thumbnailModel: MThumbnail
let thumbnailSave: object let thumbnailSave: object
if (!videoImportWithFiles.Video.getMiniature()) { if (!videoImportWithFiles.Video.getMiniature()) {
thumbnailModel = await generateVideoMiniature(videoImportWithFiles.Video, videoFile, ThumbnailType.MINIATURE) thumbnailModel = await generateVideoMiniature({
video: videoImportWithFiles.Video,
videoFile,
type: ThumbnailType.MINIATURE
})
thumbnailSave = thumbnailModel.toJSON() thumbnailSave = thumbnailModel.toJSON()
} }
@ -170,7 +174,11 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid
let previewModel: MThumbnail let previewModel: MThumbnail
let previewSave: object let previewSave: object
if (!videoImportWithFiles.Video.getPreview()) { if (!videoImportWithFiles.Video.getPreview()) {
previewModel = await generateVideoMiniature(videoImportWithFiles.Video, videoFile, ThumbnailType.PREVIEW) previewModel = await generateVideoMiniature({
video: videoImportWithFiles.Video,
videoFile,
type: ThumbnailType.PREVIEW
})
previewSave = previewModel.toJSON() previewSave = previewModel.toJSON()
} }

View File

@ -122,11 +122,19 @@ async function saveLive (video: MVideo, live: MVideoLive) {
// Regenerate the thumbnail & preview? // Regenerate the thumbnail & preview?
if (videoWithFiles.getMiniature().automaticallyGenerated === true) { if (videoWithFiles.getMiniature().automaticallyGenerated === true) {
await generateVideoMiniature(videoWithFiles, videoWithFiles.getMaxQualityFile(), ThumbnailType.MINIATURE) await generateVideoMiniature({
video: videoWithFiles,
videoFile: videoWithFiles.getMaxQualityFile(),
type: ThumbnailType.MINIATURE
})
} }
if (videoWithFiles.getPreview().automaticallyGenerated === true) { if (videoWithFiles.getPreview().automaticallyGenerated === true) {
await generateVideoMiniature(videoWithFiles, videoWithFiles.getMaxQualityFile(), ThumbnailType.PREVIEW) await generateVideoMiniature({
video: videoWithFiles,
videoFile: videoWithFiles.getMaxQualityFile(),
type: ThumbnailType.PREVIEW
})
} }
await publishAndFederateIfNeeded(videoWithFiles, true) await publishAndFederateIfNeeded(videoWithFiles, true)

View File

@ -1,33 +1,48 @@
import { join } from 'path'
import { ThumbnailType } from '../../shared/models/videos/thumbnail.type'
import { generateImageFromVideoFile } from '../helpers/ffmpeg-utils' import { generateImageFromVideoFile } from '../helpers/ffmpeg-utils'
import { processImage } from '../helpers/image-utils'
import { downloadImage } from '../helpers/requests'
import { CONFIG } from '../initializers/config' import { CONFIG } from '../initializers/config'
import { ASSETS_PATH, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '../initializers/constants' import { ASSETS_PATH, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '../initializers/constants'
import { ThumbnailModel } from '../models/video/thumbnail' import { ThumbnailModel } from '../models/video/thumbnail'
import { ThumbnailType } from '../../shared/models/videos/thumbnail.type'
import { processImage } from '../helpers/image-utils'
import { join } from 'path'
import { downloadImage } from '../helpers/requests'
import { MVideoPlaylistThumbnail } from '../types/models/video/video-playlist'
import { MVideoFile, MVideoThumbnail } from '../types/models' import { MVideoFile, MVideoThumbnail } from '../types/models'
import { MThumbnail } from '../types/models/video/thumbnail' import { MThumbnail } from '../types/models/video/thumbnail'
import { MVideoPlaylistThumbnail } from '../types/models/video/video-playlist'
import { getVideoFilePath } from './video-paths' import { getVideoFilePath } from './video-paths'
type ImageSize = { height: number, width: number } type ImageSize = { height: number, width: number }
function createPlaylistMiniatureFromExisting ( function createPlaylistMiniatureFromExisting (options: {
inputPath: string, inputPath: string
playlist: MVideoPlaylistThumbnail, playlist: MVideoPlaylistThumbnail
automaticallyGenerated: boolean, automaticallyGenerated: boolean
keepOriginal = false, keepOriginal?: boolean // default to false
size?: ImageSize size?: ImageSize
) { }) {
const { inputPath, playlist, automaticallyGenerated, keepOriginal = false, size } = options
const { filename, outputPath, height, width, existingThumbnail } = buildMetadataFromPlaylist(playlist, size) const { filename, outputPath, height, width, existingThumbnail } = buildMetadataFromPlaylist(playlist, size)
const type = ThumbnailType.MINIATURE const type = ThumbnailType.MINIATURE
const thumbnailCreator = () => processImage(inputPath, outputPath, { width, height }, keepOriginal) const thumbnailCreator = () => processImage(inputPath, outputPath, { width, height }, keepOriginal)
return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, automaticallyGenerated, existingThumbnail }) return createThumbnailFromFunction({
thumbnailCreator,
filename,
height,
width,
type,
automaticallyGenerated,
existingThumbnail
})
} }
function createPlaylistMiniatureFromUrl (downloadUrl: string, playlist: MVideoPlaylistThumbnail, size?: ImageSize) { function createPlaylistMiniatureFromUrl (options: {
downloadUrl: string
playlist: MVideoPlaylistThumbnail
size?: ImageSize
}) {
const { downloadUrl, playlist, size } = options
const { filename, basePath, height, width, existingThumbnail } = buildMetadataFromPlaylist(playlist, size) const { filename, basePath, height, width, existingThumbnail } = buildMetadataFromPlaylist(playlist, size)
const type = ThumbnailType.MINIATURE const type = ThumbnailType.MINIATURE
@ -40,7 +55,13 @@ function createPlaylistMiniatureFromUrl (downloadUrl: string, playlist: MVideoPl
return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl }) return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl })
} }
function createVideoMiniatureFromUrl (downloadUrl: string, video: MVideoThumbnail, type: ThumbnailType, size?: ImageSize) { function createVideoMiniatureFromUrl (options: {
downloadUrl: string
video: MVideoThumbnail
type: ThumbnailType
size?: ImageSize
}) {
const { downloadUrl, video, type, size } = options
const { filename, basePath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size) const { filename, basePath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size)
// Only save the file URL if it is a remote video // Only save the file URL if it is a remote video
@ -58,17 +79,31 @@ function createVideoMiniatureFromExisting (options: {
type: ThumbnailType type: ThumbnailType
automaticallyGenerated: boolean automaticallyGenerated: boolean
size?: ImageSize size?: ImageSize
keepOriginal?: boolean keepOriginal?: boolean // default to false
}) { }) {
const { inputPath, video, type, automaticallyGenerated, size, keepOriginal } = options const { inputPath, video, type, automaticallyGenerated, size, keepOriginal = false } = options
const { filename, outputPath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size) const { filename, outputPath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size)
const thumbnailCreator = () => processImage(inputPath, outputPath, { width, height }, keepOriginal) const thumbnailCreator = () => processImage(inputPath, outputPath, { width, height }, keepOriginal)
return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, automaticallyGenerated, existingThumbnail }) return createThumbnailFromFunction({
thumbnailCreator,
filename,
height,
width,
type,
automaticallyGenerated,
existingThumbnail
})
} }
function generateVideoMiniature (video: MVideoThumbnail, videoFile: MVideoFile, type: ThumbnailType) { function generateVideoMiniature (options: {
video: MVideoThumbnail
videoFile: MVideoFile
type: ThumbnailType
}) {
const { video, videoFile, type } = options
const input = getVideoFilePath(video, videoFile) const input = getVideoFilePath(video, videoFile)
const { filename, basePath, height, width, existingThumbnail, outputPath } = buildMetadataFromVideo(video, type) const { filename, basePath, height, width, existingThumbnail, outputPath } = buildMetadataFromVideo(video, type)
@ -76,10 +111,24 @@ function generateVideoMiniature (video: MVideoThumbnail, videoFile: MVideoFile,
? () => processImage(ASSETS_PATH.DEFAULT_AUDIO_BACKGROUND, outputPath, { width, height }, true) ? () => processImage(ASSETS_PATH.DEFAULT_AUDIO_BACKGROUND, outputPath, { width, height }, true)
: () => generateImageFromVideoFile(input, basePath, filename, { height, width }) : () => generateImageFromVideoFile(input, basePath, filename, { height, width })
return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, automaticallyGenerated: true, existingThumbnail }) return createThumbnailFromFunction({
thumbnailCreator,
filename,
height,
width,
type,
automaticallyGenerated: true,
existingThumbnail
})
} }
function createPlaceholderThumbnail (fileUrl: string, video: MVideoThumbnail, type: ThumbnailType, size: ImageSize) { function createPlaceholderThumbnail (options: {
fileUrl: string
video: MVideoThumbnail
type: ThumbnailType
size: ImageSize
}) {
const { fileUrl, video, type, size } = options
const { filename, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size) const { filename, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size)
const thumbnail = existingThumbnail || new ThumbnailModel() const thumbnail = existingThumbnail || new ThumbnailModel()
@ -164,12 +213,22 @@ async function createThumbnailFromFunction (parameters: {
fileUrl?: string fileUrl?: string
existingThumbnail?: MThumbnail existingThumbnail?: MThumbnail
}) { }) {
const { thumbnailCreator, filename, width, height, type, existingThumbnail, automaticallyGenerated = null, fileUrl = null } = parameters const {
thumbnailCreator,
filename,
width,
height,
type,
existingThumbnail,
automaticallyGenerated = null,
fileUrl = null
} = parameters
// Remove old file const oldFilename = existingThumbnail
if (existingThumbnail) await existingThumbnail.removeThumbnail() ? existingThumbnail.filename
: undefined
const thumbnail = existingThumbnail || new ThumbnailModel() const thumbnail: MThumbnail = existingThumbnail || new ThumbnailModel()
thumbnail.filename = filename thumbnail.filename = filename
thumbnail.height = height thumbnail.height = height
@ -177,6 +236,7 @@ async function createThumbnailFromFunction (parameters: {
thumbnail.type = type thumbnail.type = type
thumbnail.fileUrl = fileUrl thumbnail.fileUrl = fileUrl
thumbnail.automaticallyGenerated = automaticallyGenerated thumbnail.automaticallyGenerated = automaticallyGenerated
thumbnail.previousThumbnailFilename = oldFilename
await thumbnailCreator() await thumbnailCreator()

View File

@ -3,6 +3,8 @@ import { join } from 'path'
import { import {
AfterDestroy, AfterDestroy,
AllowNull, AllowNull,
BeforeCreate,
BeforeUpdate,
BelongsTo, BelongsTo,
Column, Column,
CreatedAt, CreatedAt,
@ -14,7 +16,8 @@ import {
UpdatedAt UpdatedAt
} from 'sequelize-typescript' } from 'sequelize-typescript'
import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub' import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub'
import { MThumbnailVideo, MVideoAccountLight } from '@server/types/models' import { afterCommitIfTransaction } from '@server/helpers/database-utils'
import { MThumbnail, MThumbnailVideo, MVideoAccountLight } from '@server/types/models'
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'
@ -96,6 +99,9 @@ export class ThumbnailModel extends Model {
@UpdatedAt @UpdatedAt
updatedAt: Date updatedAt: Date
// If this thumbnail replaced existing one, track the old name
previousThumbnailFilename: string
private static readonly types: { [ id in ThumbnailType ]: { label: string, directory: string, staticPath: string } } = { private static readonly types: { [ id in ThumbnailType ]: { label: string, directory: string, staticPath: string } } = {
[ThumbnailType.MINIATURE]: { [ThumbnailType.MINIATURE]: {
label: 'miniature', label: 'miniature',
@ -109,6 +115,12 @@ export class ThumbnailModel extends Model {
} }
} }
@BeforeCreate
@BeforeUpdate
static removeOldFile (instance: ThumbnailModel, options) {
return afterCommitIfTransaction(options.transaction, () => instance.removePreviousFilenameIfNeeded())
}
@AfterDestroy @AfterDestroy
static removeFiles (instance: ThumbnailModel) { static removeFiles (instance: ThumbnailModel) {
logger.info('Removing %s file %s.', ThumbnailModel.types[instance.type].label, instance.filename) logger.info('Removing %s file %s.', ThumbnailModel.types[instance.type].label, instance.filename)
@ -118,7 +130,18 @@ export class ThumbnailModel extends Model {
.catch(err => logger.error('Cannot remove thumbnail file %s.', instance.filename, err)) .catch(err => logger.error('Cannot remove thumbnail file %s.', instance.filename, err))
} }
static loadWithVideoByName (filename: string, thumbnailType: ThumbnailType): Promise<MThumbnailVideo> { static loadByFilename (filename: string, thumbnailType: ThumbnailType): Promise<MThumbnail> {
const query = {
where: {
filename,
type: thumbnailType
}
}
return ThumbnailModel.findOne(query)
}
static loadWithVideoByFilename (filename: string, thumbnailType: ThumbnailType): Promise<MThumbnailVideo> {
const query = { const query = {
where: { where: {
filename, filename,
@ -150,7 +173,22 @@ export class ThumbnailModel extends Model {
return join(directory, this.filename) return join(directory, this.filename)
} }
getPreviousPath () {
const directory = ThumbnailModel.types[this.type].directory
return join(directory, this.previousThumbnailFilename)
}
removeThumbnail () { removeThumbnail () {
return remove(this.getPath()) return remove(this.getPath())
} }
removePreviousFilenameIfNeeded () {
if (!this.previousThumbnailFilename) return
const previousPath = this.getPreviousPath()
remove(previousPath)
.catch(err => logger.error('Cannot remove previous thumbnail file %s.', previousPath, { err }))
this.previousThumbnailFilename = undefined
}
} }

View File

@ -17,6 +17,7 @@ import {
Table, Table,
UpdatedAt UpdatedAt
} from 'sequelize-typescript' } from 'sequelize-typescript'
import { v4 as uuidv4 } from 'uuid'
import { MAccountId, MChannelId } from '@server/types/models' import { MAccountId, MChannelId } from '@server/types/models'
import { ActivityIconObject } from '../../../shared/models/activitypub/objects' import { ActivityIconObject } from '../../../shared/models/activitypub/objects'
import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object'

View File

@ -558,7 +558,7 @@ describe('Test follows', function () {
const caption1: VideoCaption = res.body.data[0] const caption1: VideoCaption = res.body.data[0]
expect(caption1.language.id).to.equal('ar') expect(caption1.language.id).to.equal('ar')
expect(caption1.language.label).to.equal('Arabic') expect(caption1.language.label).to.equal('Arabic')
expect(caption1.captionPath).to.equal('/lazy-static/video-captions/' + video4.uuid + '-ar.vtt') expect(caption1.captionPath).to.match(new RegExp('^/lazy-static/video-captions/.+-ar.vtt$'))
await testCaptionFile(servers[0].url, caption1.captionPath, 'Subtitle good 2.') await testCaptionFile(servers[0].url, caption1.captionPath, 'Subtitle good 2.')
}) })

View File

@ -5,7 +5,7 @@ import { ChildProcess, exec, fork } from 'child_process'
import { copy, ensureDir, pathExists, readdir, readFile, remove } from 'fs-extra' import { copy, ensureDir, pathExists, readdir, readFile, remove } from 'fs-extra'
import { join } from 'path' import { join } from 'path'
import { randomInt } from '../../core-utils/miscs/miscs' import { randomInt } from '../../core-utils/miscs/miscs'
import { Video, VideoChannel } from '../../models/videos' import { VideoChannel } from '../../models/videos'
import { buildServerDirectory, getFileSize, isGithubCI, root, wait } from '../miscs/miscs' import { buildServerDirectory, getFileSize, isGithubCI, root, wait } from '../miscs/miscs'
import { makeGetRequest } from '../requests/requests' import { makeGetRequest } from '../requests/requests'