From 84531547bc0934a2abda586d539f7455b455d488 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 8 Apr 2021 11:23:45 +0200 Subject: [PATCH] Add size info in db for actor images --- scripts/regenerate-thumbnails.ts | 5 +-- server/helpers/image-utils.ts | 6 +++ server/initializers/constants.ts | 2 +- .../migrations/0635-actor-image-size.ts | 35 +++++++++++++++++ server/lib/activitypub/actor.ts | 35 ++++++++++++----- server/lib/activitypub/videos.ts | 38 ++++++++----------- server/lib/actor-image.ts | 2 + server/lib/thumbnail.ts | 10 ++--- server/models/account/actor-image.ts | 17 ++++++--- server/models/activitypub/actor.ts | 7 +++- server/tests/api/videos/video-channels.ts | 11 ++++++ shared/extra-utils/miscs/sql.ts | 6 +++ 12 files changed, 126 insertions(+), 48 deletions(-) create mode 100644 server/initializers/migrations/0635-actor-image-size.ts diff --git a/scripts/regenerate-thumbnails.ts b/scripts/regenerate-thumbnails.ts index 0213b8a22..b95343c0b 100644 --- a/scripts/regenerate-thumbnails.ts +++ b/scripts/regenerate-thumbnails.ts @@ -4,12 +4,11 @@ registerTSPaths() import * as Bluebird from 'bluebird' import * as program from 'commander' 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 { VideoModel } from '@server/models/video/video' import { MVideo } from '@server/types/models' import { initDatabaseModels } from '@server/initializers/database' -import { ActorImageModel } from '@server/models/account/actor-image' program .description('Regenerate local thumbnails using preview files') @@ -52,7 +51,7 @@ async function processVideo (videoArg: MVideo) { const oldPath = thumbnail.getPath() // Update thumbnail - thumbnail.filename = ActorImageModel.generateFilename() + thumbnail.filename = generateImageFilename() thumbnail.width = size.width thumbnail.height = size.height diff --git a/server/helpers/image-utils.ts b/server/helpers/image-utils.ts index 9285c12fc..6f6f8d4da 100644 --- a/server/helpers/image-utils.ts +++ b/server/helpers/image-utils.ts @@ -1,9 +1,14 @@ import { copy, readFile, remove, rename } from 'fs-extra' import * as Jimp from 'jimp' import { extname } from 'path' +import { v4 as uuidv4 } from 'uuid' import { convertWebPToJPG, processGIF } from './ffmpeg-utils' import { logger } from './logger' +function generateImageFilename (extension = '.jpg') { + return uuidv4() + extension +} + async function processImage ( path: string, destination: string, @@ -31,6 +36,7 @@ async function processImage ( // --------------------------------------------------------------------------- export { + generateImageFilename, processImage } diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 2637213a4..1802257df 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -24,7 +24,7 @@ import { CONFIG, registerConfigChangedHandler } from './config' // --------------------------------------------------------------------------- -const LAST_MIGRATION_VERSION = 630 +const LAST_MIGRATION_VERSION = 635 // --------------------------------------------------------------------------- diff --git a/server/initializers/migrations/0635-actor-image-size.ts b/server/initializers/migrations/0635-actor-image-size.ts new file mode 100644 index 000000000..d7c5da8c3 --- /dev/null +++ b/server/initializers/migrations/0635-actor-image-size.ts @@ -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 { + { + 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 +} diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts index 917fed6ec..eec951d4e 100644 --- a/server/lib/activitypub/actor.ts +++ b/server/lib/activitypub/actor.ts @@ -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) { const oldImageModel = type === ActorImageType.AVATAR ? actor.Avatar @@ -194,7 +200,9 @@ async function updateActorImageInstance (actor: MActorImages, type: ActorImageTy filename: imageInfo.name, onDisk: imageInfo.onDisk ?? false, fileUrl: imageInfo.fileUrl, - type: type + height: imageInfo.height, + width: imageInfo.width, + type }, { transaction: t }) setActorImage(actor, type, imageModel) @@ -257,6 +265,8 @@ function getImageInfoIfExists (actorJSON: ActivityPubActor, type: ActorImageType return { name: uuidv4() + extension, fileUrl: icon.url, + height: icon.height, + width: icon.width, type } } @@ -408,6 +418,8 @@ function saveActorAndServerAndModelIfNotExist ( const avatar = await ActorImageModel.create({ filename: result.avatar.name, fileUrl: result.avatar.fileUrl, + width: result.avatar.width, + height: result.avatar.height, onDisk: false, type: ActorImageType.AVATAR }, { transaction: t }) @@ -420,6 +432,8 @@ function saveActorAndServerAndModelIfNotExist ( const banner = await ActorImageModel.create({ filename: result.banner.name, fileUrl: result.banner.fileUrl, + width: result.banner.width, + height: result.banner.height, onDisk: false, type: ActorImageType.BANNER }, { transaction: t }) @@ -470,20 +484,21 @@ function saveActorAndServerAndModelIfNotExist ( } } +type ImageResult = { + name: string + fileUrl: string + height: number + width: number +} + type FetchRemoteActorResult = { actor: MActor name: string summary: string support?: string playlists?: string - avatar?: { - name: string - fileUrl: string - } - banner?: { - name: string - fileUrl: string - } + avatar?: ImageResult + banner?: ImageResult attributedTo: ActivityPubAttributedTo[] } async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: number, result: FetchRemoteActorResult }> { diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts index 492b97b9e..9014791c0 100644 --- a/server/lib/activitypub/videos.ts +++ b/server/lib/activitypub/videos.ts @@ -1,9 +1,8 @@ import * as Bluebird from 'bluebird' import { maxBy, minBy } from 'lodash' import * as magnetUtil from 'magnet-uri' -import { basename, join } from 'path' +import { basename } from 'path' import { Transaction } from 'sequelize/types' -import { ActorImageModel } from '@server/models/account/actor-image' import { TrackerModel } from '@server/models/server/tracker' import { VideoLiveModel } from '@server/models/video/video-live' import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' @@ -17,7 +16,7 @@ import { ActivityUrlObject, ActivityVideoUrlObject } 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 { ThumbnailType } from '../../../shared/models/videos/thumbnail.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 { ACTIVITY_PUB, - LAZY_STATIC_PATHS, MIMETYPES, P2P_MEDIA_LOADER_PEER_VERSION, PREVIEWS_SIZE, @@ -368,13 +366,13 @@ async function updateVideoFromAP (options: { if (thumbnailModel) await videoUpdated.addAndSaveThumbnail(thumbnailModel, t) - if (videoUpdated.getPreview()) { - const previewUrl = getPreviewUrl(getPreviewFromIcons(videoObject), video) + const previewIcon = getPreviewFromIcons(videoObject) + if (videoUpdated.getPreview() && previewIcon) { const previewModel = createPlaceholderThumbnail({ - fileUrl: previewUrl, + fileUrl: previewIcon.url, video, type: ThumbnailType.PREVIEW, - size: PREVIEWS_SIZE + size: previewIcon }) await videoUpdated.addAndSaveThumbnail(previewModel, t) } @@ -629,15 +627,17 @@ async function createVideo (videoObject: VideoObject, channel: MChannelAccountLi if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t) - const previewUrl = getPreviewUrl(getPreviewFromIcons(videoObject), videoCreated) - const previewModel = createPlaceholderThumbnail({ - fileUrl: previewUrl, - video: videoCreated, - type: ThumbnailType.PREVIEW, - size: PREVIEWS_SIZE - }) + const previewIcon = getPreviewFromIcons(videoObject) + if (previewIcon) { + const previewModel = createPlaceholderThumbnail({ + fileUrl: previewIcon.url, + video: videoCreated, + type: ThumbnailType.PREVIEW, + size: previewIcon + }) - if (thumbnailModel) await videoCreated.addAndSaveThumbnail(previewModel, t) + await videoCreated.addAndSaveThumbnail(previewModel, t) + } // Process files const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoObject.url) @@ -897,12 +897,6 @@ function getPreviewFromIcons (videoObject: VideoObject) { 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) { let wsFound = false diff --git a/server/lib/actor-image.ts b/server/lib/actor-image.ts index fa1a2a18a..f271f0b5b 100644 --- a/server/lib/actor-image.ts +++ b/server/lib/actor-image.ts @@ -34,6 +34,8 @@ async function updateLocalActorImageFile ( const actorImageInfo = { name: imageName, fileUrl: null, + height: imageSize.height, + width: imageSize.width, onDisk: true } diff --git a/server/lib/thumbnail.ts b/server/lib/thumbnail.ts index e1176ac08..cfee69cfc 100644 --- a/server/lib/thumbnail.ts +++ b/server/lib/thumbnail.ts @@ -1,8 +1,8 @@ import { join } from 'path' -import { ActorImageModel } from '@server/models/account/actor-image' + import { ThumbnailType } from '../../shared/models/videos/thumbnail.type' 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 { CONFIG } from '../initializers/config' 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 { getVideoFilePath } from './video-paths' -type ImageSize = { height: number, width: number } +type ImageSize = { height?: number, width?: number } function createPlaylistMiniatureFromExisting (options: { inputPath: string @@ -201,7 +201,7 @@ function buildMetadataFromVideo (video: MVideoThumbnail, type: ThumbnailType, si : undefined if (type === ThumbnailType.MINIATURE) { - const filename = ActorImageModel.generateFilename() + const filename = generateImageFilename() const basePath = CONFIG.STORAGE.THUMBNAILS_DIR return { @@ -215,7 +215,7 @@ function buildMetadataFromVideo (video: MVideoThumbnail, type: ThumbnailType, si } if (type === ThumbnailType.PREVIEW) { - const filename = ActorImageModel.generateFilename() + const filename = generateImageFilename() const basePath = CONFIG.STORAGE.PREVIEWS_DIR return { diff --git a/server/models/account/actor-image.ts b/server/models/account/actor-image.ts index f7438991a..ae05b4969 100644 --- a/server/models/account/actor-image.ts +++ b/server/models/account/actor-image.ts @@ -1,7 +1,6 @@ import { remove } from 'fs-extra' import { join } from 'path' -import { AfterDestroy, AllowNull, Column, CreatedAt, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' -import { v4 as uuidv4 } from 'uuid' +import { AfterDestroy, AllowNull, Column, CreatedAt, Default, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' import { MActorImageFormattable } from '@server/types/models' import { ActorImageType } from '@shared/models' import { ActorImage } from '../../../shared/models/actors/actor-image.model' @@ -26,6 +25,16 @@ export class ActorImageModel extends Model { @Column filename: string + @AllowNull(true) + @Default(null) + @Column + height: number + + @AllowNull(true) + @Default(null) + @Column + width: number + @AllowNull(true) @Is('ActorImageFileUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'fileUrl', true)) @Column @@ -54,10 +63,6 @@ export class ActorImageModel extends Model { .catch(err => logger.error('Cannot remove actor image file %s.', instance.filename, err)) } - static generateFilename () { - return uuidv4() + '.jpg' - } - static loadByName (filename: string) { const query = { where: { diff --git a/server/models/activitypub/actor.ts b/server/models/activitypub/actor.ts index 6595f11e2..a6c724f26 100644 --- a/server/models/activitypub/actor.ts +++ b/server/models/activitypub/actor.ts @@ -570,16 +570,21 @@ export class ActorModel extends Model { icon = { type: 'Image', mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension], + height: this.Avatar.height, + width: this.Avatar.width, url: this.getAvatarUrl() } } if (this.bannerId) { - const extension = extname((this as MActorAPChannel).Banner.filename) + const banner = (this as MActorAPChannel).Banner + const extension = extname(banner.filename) image = { type: 'Image', mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension], + height: banner.height, + width: banner.width, url: this.getBannerUrl() } } diff --git a/server/tests/api/videos/video-channels.ts b/server/tests/api/videos/video-channels.ts index e50582218..d12d58e75 100644 --- a/server/tests/api/videos/video-channels.ts +++ b/server/tests/api/videos/video-channels.ts @@ -2,12 +2,14 @@ import 'mocha' import * as chai from 'chai' +import { basename } from 'path' import { cleanupTests, createUser, deleteVideoChannelImage, doubleFollow, flushAndRunMultipleServers, + getActorImage, getVideo, getVideoChannel, getVideoChannelVideos, @@ -31,6 +33,7 @@ import { } from '../../../../shared/extra-utils/index' import { waitJobs } from '../../../../shared/extra-utils/server/jobs' import { User, Video, VideoChannel, VideoDetails } from '../../../../shared/index' +import { ACTOR_IMAGES_SIZE } from '@server/initializers/constants' const expect = chai.expect @@ -288,6 +291,10 @@ describe('Test video channels', function () { const videoChannel = await findChannel(server, secondVideoChannelId) 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 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) } }) diff --git a/shared/extra-utils/miscs/sql.ts b/shared/extra-utils/miscs/sql.ts index 35e493456..65a0aa5fe 100644 --- a/shared/extra-utils/miscs/sql.ts +++ b/shared/extra-utils/miscs/sql.ts @@ -82,6 +82,11 @@ async function countVideoViewsOf (internalServerNumber: number, uuid: string) { 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) { const seq = getSequelize(internalServerNumber) const options = { type: QueryTypes.SELECT as QueryTypes.SELECT } @@ -146,6 +151,7 @@ export { setPluginVersion, setPluginLatestVersion, selectQuery, + getActorImage, deleteAll, setTokenField, updateQuery,