Add size info in db for actor images

This commit is contained in:
Chocobozzz 2021-04-08 11:23:45 +02:00
parent a0eeb45f14
commit 84531547bc
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
12 changed files with 126 additions and 48 deletions

View File

@ -4,12 +4,11 @@ registerTSPaths()
import * as Bluebird from 'bluebird' import * as Bluebird from 'bluebird'
import * as program from 'commander' import * as program from 'commander'
import { pathExists, remove } from 'fs-extra' import { pathExists, remove } from 'fs-extra'
import { processImage } from '@server/helpers/image-utils' import { generateImageFilename, processImage } from '@server/helpers/image-utils'
import { THUMBNAILS_SIZE } from '@server/initializers/constants' import { THUMBNAILS_SIZE } from '@server/initializers/constants'
import { VideoModel } from '@server/models/video/video' import { VideoModel } from '@server/models/video/video'
import { MVideo } from '@server/types/models' import { MVideo } from '@server/types/models'
import { initDatabaseModels } from '@server/initializers/database' import { initDatabaseModels } from '@server/initializers/database'
import { ActorImageModel } from '@server/models/account/actor-image'
program program
.description('Regenerate local thumbnails using preview files') .description('Regenerate local thumbnails using preview files')
@ -52,7 +51,7 @@ async function processVideo (videoArg: MVideo) {
const oldPath = thumbnail.getPath() const oldPath = thumbnail.getPath()
// Update thumbnail // Update thumbnail
thumbnail.filename = ActorImageModel.generateFilename() thumbnail.filename = generateImageFilename()
thumbnail.width = size.width thumbnail.width = size.width
thumbnail.height = size.height thumbnail.height = size.height

View File

@ -1,9 +1,14 @@
import { copy, readFile, remove, rename } from 'fs-extra' import { copy, readFile, remove, rename } from 'fs-extra'
import * as Jimp from 'jimp' import * as Jimp from 'jimp'
import { extname } from 'path' import { extname } from 'path'
import { v4 as uuidv4 } from 'uuid'
import { convertWebPToJPG, processGIF } from './ffmpeg-utils' import { convertWebPToJPG, processGIF } from './ffmpeg-utils'
import { logger } from './logger' import { logger } from './logger'
function generateImageFilename (extension = '.jpg') {
return uuidv4() + extension
}
async function processImage ( async function processImage (
path: string, path: string,
destination: string, destination: string,
@ -31,6 +36,7 @@ async function processImage (
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export { export {
generateImageFilename,
processImage processImage
} }

View File

@ -24,7 +24,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const LAST_MIGRATION_VERSION = 630 const LAST_MIGRATION_VERSION = 635
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -0,0 +1,35 @@
import * as Sequelize from 'sequelize'
async function up (utils: {
transaction: Sequelize.Transaction
queryInterface: Sequelize.QueryInterface
sequelize: Sequelize.Sequelize
db: any
}): Promise<void> {
{
const data = {
type: Sequelize.INTEGER,
defaultValue: null,
allowNull: true
}
await utils.queryInterface.addColumn('actorImage', 'height', data)
}
{
const data = {
type: Sequelize.INTEGER,
defaultValue: null,
allowNull: true
}
await utils.queryInterface.addColumn('actorImage', 'width', data)
}
}
function down (options) {
throw new Error('Not implemented.')
}
export {
up,
down
}

View File

@ -170,7 +170,13 @@ async function updateActorInstance (actorInstance: ActorModel, attributes: Activ
} }
} }
type ImageInfo = { name: string, onDisk?: boolean, fileUrl: string } type ImageInfo = {
name: string
fileUrl: string
height: number
width: number
onDisk?: boolean
}
async function updateActorImageInstance (actor: MActorImages, type: ActorImageType, imageInfo: ImageInfo | null, t: Transaction) { async function updateActorImageInstance (actor: MActorImages, type: ActorImageType, imageInfo: ImageInfo | null, t: Transaction) {
const oldImageModel = type === ActorImageType.AVATAR const oldImageModel = type === ActorImageType.AVATAR
? actor.Avatar ? actor.Avatar
@ -194,7 +200,9 @@ async function updateActorImageInstance (actor: MActorImages, type: ActorImageTy
filename: imageInfo.name, filename: imageInfo.name,
onDisk: imageInfo.onDisk ?? false, onDisk: imageInfo.onDisk ?? false,
fileUrl: imageInfo.fileUrl, fileUrl: imageInfo.fileUrl,
type: type height: imageInfo.height,
width: imageInfo.width,
type
}, { transaction: t }) }, { transaction: t })
setActorImage(actor, type, imageModel) setActorImage(actor, type, imageModel)
@ -257,6 +265,8 @@ function getImageInfoIfExists (actorJSON: ActivityPubActor, type: ActorImageType
return { return {
name: uuidv4() + extension, name: uuidv4() + extension,
fileUrl: icon.url, fileUrl: icon.url,
height: icon.height,
width: icon.width,
type type
} }
} }
@ -408,6 +418,8 @@ function saveActorAndServerAndModelIfNotExist (
const avatar = await ActorImageModel.create({ const avatar = await ActorImageModel.create({
filename: result.avatar.name, filename: result.avatar.name,
fileUrl: result.avatar.fileUrl, fileUrl: result.avatar.fileUrl,
width: result.avatar.width,
height: result.avatar.height,
onDisk: false, onDisk: false,
type: ActorImageType.AVATAR type: ActorImageType.AVATAR
}, { transaction: t }) }, { transaction: t })
@ -420,6 +432,8 @@ function saveActorAndServerAndModelIfNotExist (
const banner = await ActorImageModel.create({ const banner = await ActorImageModel.create({
filename: result.banner.name, filename: result.banner.name,
fileUrl: result.banner.fileUrl, fileUrl: result.banner.fileUrl,
width: result.banner.width,
height: result.banner.height,
onDisk: false, onDisk: false,
type: ActorImageType.BANNER type: ActorImageType.BANNER
}, { transaction: t }) }, { transaction: t })
@ -470,20 +484,21 @@ function saveActorAndServerAndModelIfNotExist (
} }
} }
type ImageResult = {
name: string
fileUrl: string
height: number
width: number
}
type FetchRemoteActorResult = { type FetchRemoteActorResult = {
actor: MActor actor: MActor
name: string name: string
summary: string summary: string
support?: string support?: string
playlists?: string playlists?: string
avatar?: { avatar?: ImageResult
name: string banner?: ImageResult
fileUrl: string
}
banner?: {
name: string
fileUrl: string
}
attributedTo: ActivityPubAttributedTo[] attributedTo: ActivityPubAttributedTo[]
} }
async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: number, result: FetchRemoteActorResult }> { async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: number, result: FetchRemoteActorResult }> {

View File

@ -1,9 +1,8 @@
import * as Bluebird from 'bluebird' import * as Bluebird from 'bluebird'
import { maxBy, minBy } from 'lodash' import { maxBy, minBy } from 'lodash'
import * as magnetUtil from 'magnet-uri' import * as magnetUtil from 'magnet-uri'
import { basename, join } from 'path' import { basename } from 'path'
import { Transaction } from 'sequelize/types' import { Transaction } from 'sequelize/types'
import { ActorImageModel } from '@server/models/account/actor-image'
import { TrackerModel } from '@server/models/server/tracker' import { TrackerModel } from '@server/models/server/tracker'
import { VideoLiveModel } from '@server/models/video/video-live' import { VideoLiveModel } from '@server/models/video/video-live'
import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
@ -17,7 +16,7 @@ import {
ActivityUrlObject, ActivityUrlObject,
ActivityVideoUrlObject ActivityVideoUrlObject
} from '../../../shared/index' } from '../../../shared/index'
import { ActivityIconObject, ActivityTrackerUrlObject, VideoObject } from '../../../shared/models/activitypub/objects' import { ActivityTrackerUrlObject, VideoObject } from '../../../shared/models/activitypub/objects'
import { VideoPrivacy } from '../../../shared/models/videos' import { VideoPrivacy } from '../../../shared/models/videos'
import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
@ -35,7 +34,6 @@ import { doJSONRequest, PeerTubeRequestError } from '../../helpers/requests'
import { fetchVideoByUrl, getExtFromMimetype, VideoFetchByUrlType } from '../../helpers/video' import { fetchVideoByUrl, getExtFromMimetype, VideoFetchByUrlType } from '../../helpers/video'
import { import {
ACTIVITY_PUB, ACTIVITY_PUB,
LAZY_STATIC_PATHS,
MIMETYPES, MIMETYPES,
P2P_MEDIA_LOADER_PEER_VERSION, P2P_MEDIA_LOADER_PEER_VERSION,
PREVIEWS_SIZE, PREVIEWS_SIZE,
@ -368,13 +366,13 @@ async function updateVideoFromAP (options: {
if (thumbnailModel) await videoUpdated.addAndSaveThumbnail(thumbnailModel, t) if (thumbnailModel) await videoUpdated.addAndSaveThumbnail(thumbnailModel, t)
if (videoUpdated.getPreview()) { const previewIcon = getPreviewFromIcons(videoObject)
const previewUrl = getPreviewUrl(getPreviewFromIcons(videoObject), video) if (videoUpdated.getPreview() && previewIcon) {
const previewModel = createPlaceholderThumbnail({ const previewModel = createPlaceholderThumbnail({
fileUrl: previewUrl, fileUrl: previewIcon.url,
video, video,
type: ThumbnailType.PREVIEW, type: ThumbnailType.PREVIEW,
size: PREVIEWS_SIZE size: previewIcon
}) })
await videoUpdated.addAndSaveThumbnail(previewModel, t) await videoUpdated.addAndSaveThumbnail(previewModel, t)
} }
@ -629,15 +627,17 @@ async function createVideo (videoObject: VideoObject, channel: MChannelAccountLi
if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t) if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t)
const previewUrl = getPreviewUrl(getPreviewFromIcons(videoObject), videoCreated) const previewIcon = getPreviewFromIcons(videoObject)
const previewModel = createPlaceholderThumbnail({ if (previewIcon) {
fileUrl: previewUrl, const previewModel = createPlaceholderThumbnail({
video: videoCreated, fileUrl: previewIcon.url,
type: ThumbnailType.PREVIEW, video: videoCreated,
size: PREVIEWS_SIZE type: ThumbnailType.PREVIEW,
}) size: previewIcon
})
if (thumbnailModel) await videoCreated.addAndSaveThumbnail(previewModel, t) await videoCreated.addAndSaveThumbnail(previewModel, t)
}
// Process files // Process files
const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoObject.url) const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoObject.url)
@ -897,12 +897,6 @@ function getPreviewFromIcons (videoObject: VideoObject) {
return maxBy(validIcons, 'width') return maxBy(validIcons, 'width')
} }
function getPreviewUrl (previewIcon: ActivityIconObject, video: MVideoWithHost) {
return previewIcon
? previewIcon.url
: buildRemoteVideoBaseUrl(video, join(LAZY_STATIC_PATHS.PREVIEWS, ActorImageModel.generateFilename()))
}
function getTrackerUrls (object: VideoObject, video: MVideoWithHost) { function getTrackerUrls (object: VideoObject, video: MVideoWithHost) {
let wsFound = false let wsFound = false

View File

@ -34,6 +34,8 @@ async function updateLocalActorImageFile (
const actorImageInfo = { const actorImageInfo = {
name: imageName, name: imageName,
fileUrl: null, fileUrl: null,
height: imageSize.height,
width: imageSize.width,
onDisk: true onDisk: true
} }

View File

@ -1,8 +1,8 @@
import { join } from 'path' import { join } from 'path'
import { ActorImageModel } from '@server/models/account/actor-image'
import { ThumbnailType } from '../../shared/models/videos/thumbnail.type' 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 { generateImageFilename, processImage } from '../helpers/image-utils'
import { downloadImage } from '../helpers/requests' 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'
@ -12,7 +12,7 @@ import { MThumbnail } from '../types/models/video/thumbnail'
import { MVideoPlaylistThumbnail } from '../types/models/video/video-playlist' 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 (options: { function createPlaylistMiniatureFromExisting (options: {
inputPath: string inputPath: string
@ -201,7 +201,7 @@ function buildMetadataFromVideo (video: MVideoThumbnail, type: ThumbnailType, si
: undefined : undefined
if (type === ThumbnailType.MINIATURE) { if (type === ThumbnailType.MINIATURE) {
const filename = ActorImageModel.generateFilename() const filename = generateImageFilename()
const basePath = CONFIG.STORAGE.THUMBNAILS_DIR const basePath = CONFIG.STORAGE.THUMBNAILS_DIR
return { return {
@ -215,7 +215,7 @@ function buildMetadataFromVideo (video: MVideoThumbnail, type: ThumbnailType, si
} }
if (type === ThumbnailType.PREVIEW) { if (type === ThumbnailType.PREVIEW) {
const filename = ActorImageModel.generateFilename() const filename = generateImageFilename()
const basePath = CONFIG.STORAGE.PREVIEWS_DIR const basePath = CONFIG.STORAGE.PREVIEWS_DIR
return { return {

View File

@ -1,7 +1,6 @@
import { remove } from 'fs-extra' import { remove } from 'fs-extra'
import { join } from 'path' import { join } from 'path'
import { AfterDestroy, AllowNull, Column, CreatedAt, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' import { AfterDestroy, AllowNull, Column, CreatedAt, Default, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
import { v4 as uuidv4 } from 'uuid'
import { MActorImageFormattable } from '@server/types/models' import { MActorImageFormattable } from '@server/types/models'
import { ActorImageType } from '@shared/models' import { ActorImageType } from '@shared/models'
import { ActorImage } from '../../../shared/models/actors/actor-image.model' import { ActorImage } from '../../../shared/models/actors/actor-image.model'
@ -26,6 +25,16 @@ export class ActorImageModel extends Model {
@Column @Column
filename: string filename: string
@AllowNull(true)
@Default(null)
@Column
height: number
@AllowNull(true)
@Default(null)
@Column
width: number
@AllowNull(true) @AllowNull(true)
@Is('ActorImageFileUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'fileUrl', true)) @Is('ActorImageFileUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'fileUrl', true))
@Column @Column
@ -54,10 +63,6 @@ export class ActorImageModel extends Model {
.catch(err => logger.error('Cannot remove actor image file %s.', instance.filename, err)) .catch(err => logger.error('Cannot remove actor image file %s.', instance.filename, err))
} }
static generateFilename () {
return uuidv4() + '.jpg'
}
static loadByName (filename: string) { static loadByName (filename: string) {
const query = { const query = {
where: { where: {

View File

@ -570,16 +570,21 @@ export class ActorModel extends Model {
icon = { icon = {
type: 'Image', type: 'Image',
mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension], mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension],
height: this.Avatar.height,
width: this.Avatar.width,
url: this.getAvatarUrl() url: this.getAvatarUrl()
} }
} }
if (this.bannerId) { if (this.bannerId) {
const extension = extname((this as MActorAPChannel).Banner.filename) const banner = (this as MActorAPChannel).Banner
const extension = extname(banner.filename)
image = { image = {
type: 'Image', type: 'Image',
mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension], mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension],
height: banner.height,
width: banner.width,
url: this.getBannerUrl() url: this.getBannerUrl()
} }
} }

View File

@ -2,12 +2,14 @@
import 'mocha' import 'mocha'
import * as chai from 'chai' import * as chai from 'chai'
import { basename } from 'path'
import { import {
cleanupTests, cleanupTests,
createUser, createUser,
deleteVideoChannelImage, deleteVideoChannelImage,
doubleFollow, doubleFollow,
flushAndRunMultipleServers, flushAndRunMultipleServers,
getActorImage,
getVideo, getVideo,
getVideoChannel, getVideoChannel,
getVideoChannelVideos, getVideoChannelVideos,
@ -31,6 +33,7 @@ import {
} from '../../../../shared/extra-utils/index' } from '../../../../shared/extra-utils/index'
import { waitJobs } from '../../../../shared/extra-utils/server/jobs' import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
import { User, Video, VideoChannel, VideoDetails } from '../../../../shared/index' import { User, Video, VideoChannel, VideoDetails } from '../../../../shared/index'
import { ACTOR_IMAGES_SIZE } from '@server/initializers/constants'
const expect = chai.expect const expect = chai.expect
@ -288,6 +291,10 @@ describe('Test video channels', function () {
const videoChannel = await findChannel(server, secondVideoChannelId) const videoChannel = await findChannel(server, secondVideoChannelId)
await testImage(server.url, 'avatar-resized', videoChannel.avatar.path, '.png') await testImage(server.url, 'avatar-resized', videoChannel.avatar.path, '.png')
const row = await getActorImage(server.internalServerNumber, basename(videoChannel.avatar.path))
expect(row.height).to.equal(ACTOR_IMAGES_SIZE.AVATARS.height)
expect(row.width).to.equal(ACTOR_IMAGES_SIZE.AVATARS.width)
} }
}) })
@ -311,6 +318,10 @@ describe('Test video channels', function () {
const videoChannel = res.body const videoChannel = res.body
await testImage(server.url, 'banner-resized', videoChannel.banner.path) await testImage(server.url, 'banner-resized', videoChannel.banner.path)
const row = await getActorImage(server.internalServerNumber, basename(videoChannel.banner.path))
expect(row.height).to.equal(ACTOR_IMAGES_SIZE.BANNERS.height)
expect(row.width).to.equal(ACTOR_IMAGES_SIZE.BANNERS.width)
} }
}) })

View File

@ -82,6 +82,11 @@ async function countVideoViewsOf (internalServerNumber: number, uuid: string) {
return parseInt(total + '', 10) return parseInt(total + '', 10)
} }
function getActorImage (internalServerNumber: number, filename: string) {
return selectQuery(internalServerNumber, `SELECT * FROM "actorImage" WHERE filename = '${filename}'`)
.then(rows => rows[0])
}
function selectQuery (internalServerNumber: number, query: string) { function selectQuery (internalServerNumber: number, query: string) {
const seq = getSequelize(internalServerNumber) const seq = getSequelize(internalServerNumber)
const options = { type: QueryTypes.SELECT as QueryTypes.SELECT } const options = { type: QueryTypes.SELECT as QueryTypes.SELECT }
@ -146,6 +151,7 @@ export {
setPluginVersion, setPluginVersion,
setPluginLatestVersion, setPluginLatestVersion,
selectQuery, selectQuery,
getActorImage,
deleteAll, deleteAll,
setTokenField, setTokenField,
updateQuery, updateQuery,