aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/helpers/image-utils.ts6
-rw-r--r--server/initializers/constants.ts2
-rw-r--r--server/initializers/migrations/0635-actor-image-size.ts35
-rw-r--r--server/lib/activitypub/actor.ts35
-rw-r--r--server/lib/activitypub/videos.ts38
-rw-r--r--server/lib/actor-image.ts2
-rw-r--r--server/lib/thumbnail.ts10
-rw-r--r--server/models/account/actor-image.ts17
-rw-r--r--server/models/activitypub/actor.ts7
-rw-r--r--server/tests/api/videos/video-channels.ts11
10 files changed, 118 insertions, 45 deletions
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 @@
1import { copy, readFile, remove, rename } from 'fs-extra' 1import { copy, readFile, remove, rename } from 'fs-extra'
2import * as Jimp from 'jimp' 2import * as Jimp from 'jimp'
3import { extname } from 'path' 3import { extname } from 'path'
4import { v4 as uuidv4 } from 'uuid'
4import { convertWebPToJPG, processGIF } from './ffmpeg-utils' 5import { convertWebPToJPG, processGIF } from './ffmpeg-utils'
5import { logger } from './logger' 6import { logger } from './logger'
6 7
8function generateImageFilename (extension = '.jpg') {
9 return uuidv4() + extension
10}
11
7async function processImage ( 12async function processImage (
8 path: string, 13 path: string,
9 destination: string, 14 destination: string,
@@ -31,6 +36,7 @@ async function processImage (
31// --------------------------------------------------------------------------- 36// ---------------------------------------------------------------------------
32 37
33export { 38export {
39 generateImageFilename,
34 processImage 40 processImage
35} 41}
36 42
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'
24 24
25// --------------------------------------------------------------------------- 25// ---------------------------------------------------------------------------
26 26
27const LAST_MIGRATION_VERSION = 630 27const LAST_MIGRATION_VERSION = 635
28 28
29// --------------------------------------------------------------------------- 29// ---------------------------------------------------------------------------
30 30
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 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 {
10 const data = {
11 type: Sequelize.INTEGER,
12 defaultValue: null,
13 allowNull: true
14 }
15 await utils.queryInterface.addColumn('actorImage', 'height', data)
16 }
17
18 {
19 const data = {
20 type: Sequelize.INTEGER,
21 defaultValue: null,
22 allowNull: true
23 }
24 await utils.queryInterface.addColumn('actorImage', 'width', data)
25 }
26}
27
28function down (options) {
29 throw new Error('Not implemented.')
30}
31
32export {
33 up,
34 down
35}
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
170 } 170 }
171} 171}
172 172
173type ImageInfo = { name: string, onDisk?: boolean, fileUrl: string } 173type ImageInfo = {
174 name: string
175 fileUrl: string
176 height: number
177 width: number
178 onDisk?: boolean
179}
174async function updateActorImageInstance (actor: MActorImages, type: ActorImageType, imageInfo: ImageInfo | null, t: Transaction) { 180async function updateActorImageInstance (actor: MActorImages, type: ActorImageType, imageInfo: ImageInfo | null, t: Transaction) {
175 const oldImageModel = type === ActorImageType.AVATAR 181 const oldImageModel = type === ActorImageType.AVATAR
176 ? actor.Avatar 182 ? actor.Avatar
@@ -194,7 +200,9 @@ async function updateActorImageInstance (actor: MActorImages, type: ActorImageTy
194 filename: imageInfo.name, 200 filename: imageInfo.name,
195 onDisk: imageInfo.onDisk ?? false, 201 onDisk: imageInfo.onDisk ?? false,
196 fileUrl: imageInfo.fileUrl, 202 fileUrl: imageInfo.fileUrl,
197 type: type 203 height: imageInfo.height,
204 width: imageInfo.width,
205 type
198 }, { transaction: t }) 206 }, { transaction: t })
199 207
200 setActorImage(actor, type, imageModel) 208 setActorImage(actor, type, imageModel)
@@ -257,6 +265,8 @@ function getImageInfoIfExists (actorJSON: ActivityPubActor, type: ActorImageType
257 return { 265 return {
258 name: uuidv4() + extension, 266 name: uuidv4() + extension,
259 fileUrl: icon.url, 267 fileUrl: icon.url,
268 height: icon.height,
269 width: icon.width,
260 type 270 type
261 } 271 }
262} 272}
@@ -408,6 +418,8 @@ function saveActorAndServerAndModelIfNotExist (
408 const avatar = await ActorImageModel.create({ 418 const avatar = await ActorImageModel.create({
409 filename: result.avatar.name, 419 filename: result.avatar.name,
410 fileUrl: result.avatar.fileUrl, 420 fileUrl: result.avatar.fileUrl,
421 width: result.avatar.width,
422 height: result.avatar.height,
411 onDisk: false, 423 onDisk: false,
412 type: ActorImageType.AVATAR 424 type: ActorImageType.AVATAR
413 }, { transaction: t }) 425 }, { transaction: t })
@@ -420,6 +432,8 @@ function saveActorAndServerAndModelIfNotExist (
420 const banner = await ActorImageModel.create({ 432 const banner = await ActorImageModel.create({
421 filename: result.banner.name, 433 filename: result.banner.name,
422 fileUrl: result.banner.fileUrl, 434 fileUrl: result.banner.fileUrl,
435 width: result.banner.width,
436 height: result.banner.height,
423 onDisk: false, 437 onDisk: false,
424 type: ActorImageType.BANNER 438 type: ActorImageType.BANNER
425 }, { transaction: t }) 439 }, { transaction: t })
@@ -470,20 +484,21 @@ function saveActorAndServerAndModelIfNotExist (
470 } 484 }
471} 485}
472 486
487type ImageResult = {
488 name: string
489 fileUrl: string
490 height: number
491 width: number
492}
493
473type FetchRemoteActorResult = { 494type FetchRemoteActorResult = {
474 actor: MActor 495 actor: MActor
475 name: string 496 name: string
476 summary: string 497 summary: string
477 support?: string 498 support?: string
478 playlists?: string 499 playlists?: string
479 avatar?: { 500 avatar?: ImageResult
480 name: string 501 banner?: ImageResult
481 fileUrl: string
482 }
483 banner?: {
484 name: string
485 fileUrl: string
486 }
487 attributedTo: ActivityPubAttributedTo[] 502 attributedTo: ActivityPubAttributedTo[]
488} 503}
489async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: number, result: FetchRemoteActorResult }> { 504async 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 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { maxBy, minBy } from 'lodash' 2import { maxBy, minBy } from 'lodash'
3import * as magnetUtil from 'magnet-uri' 3import * as magnetUtil from 'magnet-uri'
4import { basename, join } from 'path' 4import { basename } from 'path'
5import { Transaction } from 'sequelize/types' 5import { Transaction } from 'sequelize/types'
6import { ActorImageModel } from '@server/models/account/actor-image'
7import { TrackerModel } from '@server/models/server/tracker' 6import { TrackerModel } from '@server/models/server/tracker'
8import { VideoLiveModel } from '@server/models/video/video-live' 7import { VideoLiveModel } from '@server/models/video/video-live'
9import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' 8import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
@@ -17,7 +16,7 @@ import {
17 ActivityUrlObject, 16 ActivityUrlObject,
18 ActivityVideoUrlObject 17 ActivityVideoUrlObject
19} from '../../../shared/index' 18} from '../../../shared/index'
20import { ActivityIconObject, ActivityTrackerUrlObject, VideoObject } from '../../../shared/models/activitypub/objects' 19import { ActivityTrackerUrlObject, VideoObject } from '../../../shared/models/activitypub/objects'
21import { VideoPrivacy } from '../../../shared/models/videos' 20import { VideoPrivacy } from '../../../shared/models/videos'
22import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' 21import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
23import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' 22import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
@@ -35,7 +34,6 @@ import { doJSONRequest, PeerTubeRequestError } from '../../helpers/requests'
35import { fetchVideoByUrl, getExtFromMimetype, VideoFetchByUrlType } from '../../helpers/video' 34import { fetchVideoByUrl, getExtFromMimetype, VideoFetchByUrlType } from '../../helpers/video'
36import { 35import {
37 ACTIVITY_PUB, 36 ACTIVITY_PUB,
38 LAZY_STATIC_PATHS,
39 MIMETYPES, 37 MIMETYPES,
40 P2P_MEDIA_LOADER_PEER_VERSION, 38 P2P_MEDIA_LOADER_PEER_VERSION,
41 PREVIEWS_SIZE, 39 PREVIEWS_SIZE,
@@ -368,13 +366,13 @@ async function updateVideoFromAP (options: {
368 366
369 if (thumbnailModel) await videoUpdated.addAndSaveThumbnail(thumbnailModel, t) 367 if (thumbnailModel) await videoUpdated.addAndSaveThumbnail(thumbnailModel, t)
370 368
371 if (videoUpdated.getPreview()) { 369 const previewIcon = getPreviewFromIcons(videoObject)
372 const previewUrl = getPreviewUrl(getPreviewFromIcons(videoObject), video) 370 if (videoUpdated.getPreview() && previewIcon) {
373 const previewModel = createPlaceholderThumbnail({ 371 const previewModel = createPlaceholderThumbnail({
374 fileUrl: previewUrl, 372 fileUrl: previewIcon.url,
375 video, 373 video,
376 type: ThumbnailType.PREVIEW, 374 type: ThumbnailType.PREVIEW,
377 size: PREVIEWS_SIZE 375 size: previewIcon
378 }) 376 })
379 await videoUpdated.addAndSaveThumbnail(previewModel, t) 377 await videoUpdated.addAndSaveThumbnail(previewModel, t)
380 } 378 }
@@ -629,15 +627,17 @@ async function createVideo (videoObject: VideoObject, channel: MChannelAccountLi
629 627
630 if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t) 628 if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t)
631 629
632 const previewUrl = getPreviewUrl(getPreviewFromIcons(videoObject), videoCreated) 630 const previewIcon = getPreviewFromIcons(videoObject)
633 const previewModel = createPlaceholderThumbnail({ 631 if (previewIcon) {
634 fileUrl: previewUrl, 632 const previewModel = createPlaceholderThumbnail({
635 video: videoCreated, 633 fileUrl: previewIcon.url,
636 type: ThumbnailType.PREVIEW, 634 video: videoCreated,
637 size: PREVIEWS_SIZE 635 type: ThumbnailType.PREVIEW,
638 }) 636 size: previewIcon
637 })
639 638
640 if (thumbnailModel) await videoCreated.addAndSaveThumbnail(previewModel, t) 639 await videoCreated.addAndSaveThumbnail(previewModel, t)
640 }
641 641
642 // Process files 642 // Process files
643 const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoObject.url) 643 const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoObject.url)
@@ -897,12 +897,6 @@ function getPreviewFromIcons (videoObject: VideoObject) {
897 return maxBy(validIcons, 'width') 897 return maxBy(validIcons, 'width')
898} 898}
899 899
900function getPreviewUrl (previewIcon: ActivityIconObject, video: MVideoWithHost) {
901 return previewIcon
902 ? previewIcon.url
903 : buildRemoteVideoBaseUrl(video, join(LAZY_STATIC_PATHS.PREVIEWS, ActorImageModel.generateFilename()))
904}
905
906function getTrackerUrls (object: VideoObject, video: MVideoWithHost) { 900function getTrackerUrls (object: VideoObject, video: MVideoWithHost) {
907 let wsFound = false 901 let wsFound = false
908 902
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 (
34 const actorImageInfo = { 34 const actorImageInfo = {
35 name: imageName, 35 name: imageName,
36 fileUrl: null, 36 fileUrl: null,
37 height: imageSize.height,
38 width: imageSize.width,
37 onDisk: true 39 onDisk: true
38 } 40 }
39 41
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 @@
1import { join } from 'path' 1import { join } from 'path'
2import { ActorImageModel } from '@server/models/account/actor-image' 2
3import { ThumbnailType } from '../../shared/models/videos/thumbnail.type' 3import { ThumbnailType } from '../../shared/models/videos/thumbnail.type'
4import { generateImageFromVideoFile } from '../helpers/ffmpeg-utils' 4import { generateImageFromVideoFile } from '../helpers/ffmpeg-utils'
5import { processImage } from '../helpers/image-utils' 5import { generateImageFilename, processImage } from '../helpers/image-utils'
6import { downloadImage } from '../helpers/requests' 6import { downloadImage } from '../helpers/requests'
7import { CONFIG } from '../initializers/config' 7import { CONFIG } from '../initializers/config'
8import { ASSETS_PATH, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '../initializers/constants' 8import { ASSETS_PATH, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '../initializers/constants'
@@ -12,7 +12,7 @@ import { MThumbnail } from '../types/models/video/thumbnail'
12import { MVideoPlaylistThumbnail } from '../types/models/video/video-playlist' 12import { MVideoPlaylistThumbnail } from '../types/models/video/video-playlist'
13import { getVideoFilePath } from './video-paths' 13import { getVideoFilePath } from './video-paths'
14 14
15type ImageSize = { height: number, width: number } 15type ImageSize = { height?: number, width?: number }
16 16
17function createPlaylistMiniatureFromExisting (options: { 17function createPlaylistMiniatureFromExisting (options: {
18 inputPath: string 18 inputPath: string
@@ -201,7 +201,7 @@ function buildMetadataFromVideo (video: MVideoThumbnail, type: ThumbnailType, si
201 : undefined 201 : undefined
202 202
203 if (type === ThumbnailType.MINIATURE) { 203 if (type === ThumbnailType.MINIATURE) {
204 const filename = ActorImageModel.generateFilename() 204 const filename = generateImageFilename()
205 const basePath = CONFIG.STORAGE.THUMBNAILS_DIR 205 const basePath = CONFIG.STORAGE.THUMBNAILS_DIR
206 206
207 return { 207 return {
@@ -215,7 +215,7 @@ function buildMetadataFromVideo (video: MVideoThumbnail, type: ThumbnailType, si
215 } 215 }
216 216
217 if (type === ThumbnailType.PREVIEW) { 217 if (type === ThumbnailType.PREVIEW) {
218 const filename = ActorImageModel.generateFilename() 218 const filename = generateImageFilename()
219 const basePath = CONFIG.STORAGE.PREVIEWS_DIR 219 const basePath = CONFIG.STORAGE.PREVIEWS_DIR
220 220
221 return { 221 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 @@
1import { remove } from 'fs-extra' 1import { remove } from 'fs-extra'
2import { join } from 'path' 2import { join } from 'path'
3import { AfterDestroy, AllowNull, Column, CreatedAt, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' 3import { AfterDestroy, AllowNull, Column, CreatedAt, Default, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
4import { v4 as uuidv4 } from 'uuid'
5import { MActorImageFormattable } from '@server/types/models' 4import { MActorImageFormattable } from '@server/types/models'
6import { ActorImageType } from '@shared/models' 5import { ActorImageType } from '@shared/models'
7import { ActorImage } from '../../../shared/models/actors/actor-image.model' 6import { ActorImage } from '../../../shared/models/actors/actor-image.model'
@@ -27,6 +26,16 @@ export class ActorImageModel extends Model {
27 filename: string 26 filename: string
28 27
29 @AllowNull(true) 28 @AllowNull(true)
29 @Default(null)
30 @Column
31 height: number
32
33 @AllowNull(true)
34 @Default(null)
35 @Column
36 width: number
37
38 @AllowNull(true)
30 @Is('ActorImageFileUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'fileUrl', true)) 39 @Is('ActorImageFileUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'fileUrl', true))
31 @Column 40 @Column
32 fileUrl: string 41 fileUrl: string
@@ -54,10 +63,6 @@ export class ActorImageModel extends Model {
54 .catch(err => logger.error('Cannot remove actor image file %s.', instance.filename, err)) 63 .catch(err => logger.error('Cannot remove actor image file %s.', instance.filename, err))
55 } 64 }
56 65
57 static generateFilename () {
58 return uuidv4() + '.jpg'
59 }
60
61 static loadByName (filename: string) { 66 static loadByName (filename: string) {
62 const query = { 67 const query = {
63 where: { 68 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 {
570 icon = { 570 icon = {
571 type: 'Image', 571 type: 'Image',
572 mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension], 572 mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension],
573 height: this.Avatar.height,
574 width: this.Avatar.width,
573 url: this.getAvatarUrl() 575 url: this.getAvatarUrl()
574 } 576 }
575 } 577 }
576 578
577 if (this.bannerId) { 579 if (this.bannerId) {
578 const extension = extname((this as MActorAPChannel).Banner.filename) 580 const banner = (this as MActorAPChannel).Banner
581 const extension = extname(banner.filename)
579 582
580 image = { 583 image = {
581 type: 'Image', 584 type: 'Image',
582 mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension], 585 mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension],
586 height: banner.height,
587 width: banner.width,
583 url: this.getBannerUrl() 588 url: this.getBannerUrl()
584 } 589 }
585 } 590 }
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 @@
2 2
3import 'mocha' 3import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
5import { basename } from 'path'
5import { 6import {
6 cleanupTests, 7 cleanupTests,
7 createUser, 8 createUser,
8 deleteVideoChannelImage, 9 deleteVideoChannelImage,
9 doubleFollow, 10 doubleFollow,
10 flushAndRunMultipleServers, 11 flushAndRunMultipleServers,
12 getActorImage,
11 getVideo, 13 getVideo,
12 getVideoChannel, 14 getVideoChannel,
13 getVideoChannelVideos, 15 getVideoChannelVideos,
@@ -31,6 +33,7 @@ import {
31} from '../../../../shared/extra-utils/index' 33} from '../../../../shared/extra-utils/index'
32import { waitJobs } from '../../../../shared/extra-utils/server/jobs' 34import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
33import { User, Video, VideoChannel, VideoDetails } from '../../../../shared/index' 35import { User, Video, VideoChannel, VideoDetails } from '../../../../shared/index'
36import { ACTOR_IMAGES_SIZE } from '@server/initializers/constants'
34 37
35const expect = chai.expect 38const expect = chai.expect
36 39
@@ -288,6 +291,10 @@ describe('Test video channels', function () {
288 const videoChannel = await findChannel(server, secondVideoChannelId) 291 const videoChannel = await findChannel(server, secondVideoChannelId)
289 292
290 await testImage(server.url, 'avatar-resized', videoChannel.avatar.path, '.png') 293 await testImage(server.url, 'avatar-resized', videoChannel.avatar.path, '.png')
294
295 const row = await getActorImage(server.internalServerNumber, basename(videoChannel.avatar.path))
296 expect(row.height).to.equal(ACTOR_IMAGES_SIZE.AVATARS.height)
297 expect(row.width).to.equal(ACTOR_IMAGES_SIZE.AVATARS.width)
291 } 298 }
292 }) 299 })
293 300
@@ -311,6 +318,10 @@ describe('Test video channels', function () {
311 const videoChannel = res.body 318 const videoChannel = res.body
312 319
313 await testImage(server.url, 'banner-resized', videoChannel.banner.path) 320 await testImage(server.url, 'banner-resized', videoChannel.banner.path)
321
322 const row = await getActorImage(server.internalServerNumber, basename(videoChannel.banner.path))
323 expect(row.height).to.equal(ACTOR_IMAGES_SIZE.BANNERS.height)
324 expect(row.width).to.equal(ACTOR_IMAGES_SIZE.BANNERS.width)
314 } 325 }
315 }) 326 })
316 327