Browse Source

Add size info in db for actor images

Chocobozzz 3 years ago
parent
commit
84531547bc

+ 2 - 3
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
 

+ 6 - 0
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
 }
 

+ 1 - 1
server/initializers/constants.ts

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

+ 35 - 0
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<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
+}

+ 25 - 10
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 }> {

+ 16 - 22
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
 

+ 2 - 0
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
       }
 

+ 5 - 5
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 {

+ 11 - 6
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: {

+ 6 - 1
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()
       }
     }

+ 11 - 0
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)
     }
   })
 

+ 6 - 0
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,