Generate small versions of banners too

pull/6302/head
Chocobozzz 2024-03-27 14:00:40 +01:00
parent aaa5acbb0c
commit 11521f231f
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
31 changed files with 184 additions and 345 deletions

View File

@ -1,16 +1,17 @@
import { ViewportScroller, NgIf, NgFor } from '@angular/common'
import { NgFor, NgIf, ViewportScroller } from '@angular/common'
import { AfterViewChecked, Component, ElementRef, OnInit, ViewChild } from '@angular/core'
import { ActivatedRoute, RouterLink } from '@angular/router'
import { Notifier, ServerService } from '@app/core'
import { AboutHTML } from '@app/shared/shared-main/instance/instance.service'
import { maxBy } from '@peertube/peertube-core-utils'
import { HTMLServerConfig, ServerStats } from '@peertube/peertube-models'
import { copyToClipboard } from '@root-helpers/utils'
import { CustomMarkupContainerComponent } from '../../shared/shared-custom-markup/custom-markup-container.component'
import { InstanceFeaturesTableComponent } from '../../shared/shared-instance/instance-features-table.component'
import { PluginSelectorDirective } from '../../shared/shared-main/plugins/plugin-selector.directive'
import { ResolverData } from './about-instance.resolver'
import { ContactAdminModalComponent } from './contact-admin-modal.component'
import { InstanceStatisticsComponent } from './instance-statistics.component'
import { InstanceFeaturesTableComponent } from '../../shared/shared-instance/instance-features-table.component'
import { PluginSelectorDirective } from '../../shared/shared-main/plugins/plugin-selector.directive'
import { CustomMarkupContainerComponent } from '../../shared/shared-custom-markup/custom-markup-container.component'
import { AboutHTML } from '@app/shared/shared-main/instance/instance.service'
@Component({
selector: 'my-about-instance',
@ -82,7 +83,7 @@ export class AboutInstanceComponent implements OnInit, AfterViewChecked {
this.shortDescription = about.instance.shortDescription
this.instanceBannerUrl = about.instance.banners.length !== 0
? about.instance.banners[0].path
? maxBy(about.instance.banners, 'width').path
: undefined
this.serverConfig = this.serverService.getHTMLConfig()

View File

@ -1,22 +1,23 @@
import { SelectOptionsItem } from 'src/types/select-options-item.model'
import { NgClass, NgIf } from '@angular/common'
import { HttpErrorResponse } from '@angular/common/http'
import { Component, Input, OnInit } from '@angular/core'
import { FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
import { Notifier, ServerService } from '@app/core'
import { HttpErrorResponse } from '@angular/common/http'
import { genericUploadErrorHandler } from '@app/helpers'
import { ActorImage, HTMLServerConfig } from '@peertube/peertube-models'
import { HelpComponent } from '../../../shared/shared-main/misc/help.component'
import { PeerTubeTemplateDirective } from '../../../shared/shared-main/angular/peertube-template.directive'
import { PeertubeCheckboxComponent } from '../../../shared/shared-forms/peertube-checkbox.component'
import { RouterLink } from '@angular/router'
import { SelectCheckboxComponent } from '../../../shared/shared-forms/select/select-checkbox.component'
import { MarkdownTextareaComponent } from '../../../shared/shared-forms/markdown-textarea.component'
import { CustomMarkupHelpComponent } from '../../../shared/shared-custom-markup/custom-markup-help.component'
import { NgClass, NgIf } from '@angular/common'
import { ActorBannerEditComponent } from '../../../shared/shared-actor-image-edit/actor-banner-edit.component'
import { ActorAvatarEditComponent } from '../../../shared/shared-actor-image-edit/actor-avatar-edit.component'
import { InstanceService } from '@app/shared/shared-main/instance/instance.service'
import { Notifier, ServerService } from '@app/core'
import { genericUploadErrorHandler } from '@app/helpers'
import { CustomMarkupService } from '@app/shared/shared-custom-markup/custom-markup.service'
import { InstanceService } from '@app/shared/shared-main/instance/instance.service'
import { maxBy } from '@peertube/peertube-core-utils'
import { ActorImage, HTMLServerConfig } from '@peertube/peertube-models'
import { SelectOptionsItem } from 'src/types/select-options-item.model'
import { ActorAvatarEditComponent } from '../../../shared/shared-actor-image-edit/actor-avatar-edit.component'
import { ActorBannerEditComponent } from '../../../shared/shared-actor-image-edit/actor-banner-edit.component'
import { CustomMarkupHelpComponent } from '../../../shared/shared-custom-markup/custom-markup-help.component'
import { MarkdownTextareaComponent } from '../../../shared/shared-forms/markdown-textarea.component'
import { PeertubeCheckboxComponent } from '../../../shared/shared-forms/peertube-checkbox.component'
import { SelectCheckboxComponent } from '../../../shared/shared-forms/select/select-checkbox.component'
import { PeerTubeTemplateDirective } from '../../../shared/shared-main/angular/peertube-template.directive'
import { HelpComponent } from '../../../shared/shared-main/misc/help.component'
@Component({
selector: 'my-edit-instance-information',
@ -127,7 +128,7 @@ export class EditInstanceInformationComponent implements OnInit {
}
private updateActorImages () {
this.instanceBannerUrl = this.serverConfig.instance.banners?.[0]?.path
this.instanceBannerUrl = maxBy(this.serverConfig.instance.banners, 'width')?.path
this.instanceAvatars = this.serverConfig.instance.avatars
}

View File

@ -1,23 +1,23 @@
import { ChartData, ChartOptions, TooltipItem, TooltipModel } from 'chart.js'
import { max, maxBy, min, minBy } from 'lodash-es'
import { Subject, first, map, switchMap } from 'rxjs'
import { NgFor, NgIf } from '@angular/common'
import { Component } from '@angular/core'
import { AuthService, ComponentPagination, ConfirmService, hasMoreItems, Notifier, ScreenService } from '@app/core'
import { formatICU } from '@app/helpers'
import { NumberFormatterPipe } from '../../shared/shared-main/angular/number-formatter.pipe'
import { ChartModule } from 'primeng/chart'
import { DeferLoadingDirective } from '../../shared/shared-main/angular/defer-loading.directive'
import { DeleteButtonComponent } from '../../shared/shared-main/buttons/delete-button.component'
import { EditButtonComponent } from '../../shared/shared-main/buttons/edit-button.component'
import { ActorAvatarComponent } from '../../shared/shared-actor-image/actor-avatar.component'
import { InfiniteScrollerDirective } from '../../shared/shared-main/angular/infinite-scroller.directive'
import { AdvancedInputFilterComponent } from '../../shared/shared-forms/advanced-input-filter.component'
import { ChannelsSetupMessageComponent } from '../../shared/shared-main/misc/channels-setup-message.component'
import { RouterLink } from '@angular/router'
import { NgIf, NgFor } from '@angular/common'
import { GlobalIconComponent } from '../../shared/shared-icons/global-icon.component'
import { AuthService, ComponentPagination, ConfirmService, Notifier, ScreenService, hasMoreItems } from '@app/core'
import { formatICU } from '@app/helpers'
import { VideoChannel } from '@app/shared/shared-main/video-channel/video-channel.model'
import { VideoChannelService } from '@app/shared/shared-main/video-channel/video-channel.service'
import { maxBy, minBy } from '@peertube/peertube-core-utils'
import { ChartData, ChartOptions, TooltipItem, TooltipModel } from 'chart.js'
import { ChartModule } from 'primeng/chart'
import { Subject, first, map, switchMap } from 'rxjs'
import { ActorAvatarComponent } from '../../shared/shared-actor-image/actor-avatar.component'
import { AdvancedInputFilterComponent } from '../../shared/shared-forms/advanced-input-filter.component'
import { GlobalIconComponent } from '../../shared/shared-icons/global-icon.component'
import { DeferLoadingDirective } from '../../shared/shared-main/angular/defer-loading.directive'
import { InfiniteScrollerDirective } from '../../shared/shared-main/angular/infinite-scroller.directive'
import { NumberFormatterPipe } from '../../shared/shared-main/angular/number-formatter.pipe'
import { DeleteButtonComponent } from '../../shared/shared-main/buttons/delete-button.component'
import { EditButtonComponent } from '../../shared/shared-main/buttons/edit-button.component'
import { ChannelsSetupMessageComponent } from '../../shared/shared-main/misc/channels-setup-message.component'
@Component({
templateUrl: './my-video-channels.component.html',
@ -156,23 +156,8 @@ export class MyVideoChannelsComponent {
}
private buildChartOptions () {
// chart options that depend on chart data:
// we don't want to skew values and have min at 0, so we define what the floor/ceiling is here
const videoChannelsMinimumDailyViews = min(
// compute local minimum daily views for each channel, by their "views" attribute
this.videoChannels.map(v => minBy(
v.viewsPerDay,
day => day.views
).views) // the object returned is a ViewPerDate, so we still need to get the views attribute
)
const videoChannelsMaximumDailyViews = max(
// compute local maximum daily views for each channel, by their "views" attribute
this.videoChannels.map(v => maxBy(
v.viewsPerDay,
day => day.views
).views) // the object returned is a ViewPerDate, so we still need to get the views attribute
)
const channelsMinimumDailyViews = Math.min(...this.videoChannels.map(v => minBy(v.viewsPerDay, 'views').views))
const channelsMaximumDailyViews = Math.max(...this.videoChannels.map(v => maxBy(v.viewsPerDay, 'views').views))
this.chartOptions = {
plugins: {
@ -199,8 +184,8 @@ export class MyVideoChannelsComponent {
},
y: {
display: false,
min: Math.max(0, videoChannelsMinimumDailyViews - (3 * videoChannelsMaximumDailyViews / 100)),
max: Math.max(1, videoChannelsMaximumDailyViews)
min: Math.max(0, channelsMinimumDailyViews - (3 * channelsMaximumDailyViews / 100)),
max: Math.max(1, channelsMaximumDailyViews)
}
},
layout: {

View File

@ -1,7 +1,7 @@
import { minBy } from 'lodash-es'
import { minBy } from '@peertube/peertube-core-utils'
import { VideoChannel } from '@peertube/peertube-models'
import { first, map } from 'rxjs/operators'
import { SelectChannelItem } from 'src/types/select-options-item.model'
import { VideoChannel } from '@peertube/peertube-models'
import { AuthService } from '../../core/auth'
function listUserChannelsForSelect (authService: AuthService) {

View File

@ -1,7 +1,8 @@
import { NgClass, NgIf } from '@angular/common'
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'
import { CustomMarkupComponent } from './shared'
import { ServerService } from '@app/core'
import { NgIf, NgClass } from '@angular/common'
import { maxBy } from '@peertube/peertube-core-utils'
import { CustomMarkupComponent } from './shared'
/*
* Markup component that creates the img HTML element containing the instance banner
@ -28,7 +29,7 @@ export class InstanceBannerMarkupComponent implements OnInit, CustomMarkupCompon
ngOnInit () {
const { instance } = this.server.getHTMLConfig()
this.instanceBannerUrl = instance.banners?.[0]?.path
this.instanceBannerUrl = maxBy(instance.banners, 'width')?.path
this.cd.markForCheck()
}
}

View File

@ -1,6 +1,7 @@
import { NgClass, NgIf } from '@angular/common'
import { Component, Input, OnInit, booleanAttribute } from '@angular/core'
import { ServerService } from '@app/core'
import { NgIf, NgClass } from '@angular/common'
import { maxBy } from '@peertube/peertube-core-utils'
@Component({
selector: 'my-instance-banner',
@ -20,6 +21,6 @@ export class InstanceBannerComponent implements OnInit {
ngOnInit () {
const { instance } = this.server.getHTMLConfig()
this.instanceBannerUrl = instance.banners?.[0]?.path
this.instanceBannerUrl = maxBy(instance.banners, 'width')?.path
}
}

View File

@ -1,6 +1,7 @@
import { getAbsoluteAPIUrl } from '@app/helpers'
import { Account as ServerAccount, ActorImage, VideoChannel as ServerVideoChannel, ViewsPerDate } from '@peertube/peertube-models'
import { Actor } from '../account/actor.model'
import { maxBy } from '@peertube/peertube-core-utils'
export class VideoChannel extends Actor implements ServerVideoChannel {
displayName: string
@ -35,7 +36,7 @@ export class VideoChannel extends Actor implements ServerVideoChannel {
return ''
}
const banner = channel.banners[0]
const banner = maxBy(channel.banners, 'width')
if (!banner) return ''
if (banner.url) return banner.url

View File

@ -1,6 +1,4 @@
import { VideoFile } from '@peertube/peertube-models'
function toTitleCase (str: string) {
export function toTitleCase (str: string) {
return str.charAt(0).toUpperCase() + str.slice(1)
}
@ -10,36 +8,14 @@ const dictionaryBytes = [
{ max: 1073741824, type: 'MB', decimals: 0 },
{ max: 1.0995116e12, type: 'GB', decimals: 1 }
]
function bytes (value: number) {
export function bytes (value: number) {
const format = dictionaryBytes.find(d => value < d.max) || dictionaryBytes[dictionaryBytes.length - 1]
const calc = (value / (format.max / 1024)).toFixed(format.decimals)
return [ calc, format.type ]
}
function videoFileMaxByResolution (files: VideoFile[]) {
let max = files[0]
for (let i = 1; i < files.length; i++) {
const file = files[i]
if (max.resolution.id < file.resolution.id) max = file
}
return max
}
function videoFileMinByResolution (files: VideoFile[]) {
let min = files[0]
for (let i = 1; i < files.length; i++) {
const file = files[i]
if (min.resolution.id > file.resolution.id) min = file
}
return min
}
function getRtcConfig () {
export function getRtcConfig () {
return {
iceServers: [
{
@ -52,19 +28,6 @@ function getRtcConfig () {
}
}
function isSameOrigin (current: string, target: string) {
export function isSameOrigin (current: string, target: string) {
return new URL(current).origin === new URL(target).origin
}
// ---------------------------------------------------------------------------
export {
getRtcConfig,
toTitleCase,
videoFileMaxByResolution,
videoFileMinByResolution,
bytes,
isSameOrigin
}

View File

@ -1,101 +0,0 @@
import { HTMLServerConfig, Video, VideoFile } from '@peertube/peertube-models'
function toTitleCase (str: string) {
return str.charAt(0).toUpperCase() + str.slice(1)
}
function isWebRTCDisabled () {
return !!((window as any).RTCPeerConnection || (window as any).mozRTCPeerConnection || (window as any).webkitRTCPeerConnection) === false
}
function isP2PEnabled (video: Video, config: HTMLServerConfig, userP2PEnabled: boolean) {
if (video.isLocal && config.tracker.enabled === false) return false
if (isWebRTCDisabled()) return false
return userP2PEnabled
}
function isIOS () {
if (/iPad|iPhone|iPod/.test(navigator.platform)) {
return true
}
// Detect iPad Desktop mode
return !!(navigator.maxTouchPoints &&
navigator.maxTouchPoints > 2 &&
navigator.platform.includes('MacIntel'))
}
function isSafari () {
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent)
}
// https://github.com/danrevah/ngx-pipes/blob/master/src/pipes/math/bytes.ts
// Don't import all Angular stuff, just copy the code with shame
const dictionaryBytes: { max: number, type: string }[] = [
{ max: 1024, type: 'B' },
{ max: 1048576, type: 'KB' },
{ max: 1073741824, type: 'MB' },
{ max: 1.0995116e12, type: 'GB' }
]
function bytes (value: number) {
const format = dictionaryBytes.find(d => value < d.max) || dictionaryBytes[dictionaryBytes.length - 1]
const calc = Math.floor(value / (format.max / 1024)).toString()
return [ calc, format.type ]
}
function isMobile () {
return /iPhone|iPad|iPod|Android/i.test(navigator.userAgent)
}
function videoFileMaxByResolution (files: VideoFile[]) {
let max = files[0]
for (let i = 1; i < files.length; i++) {
const file = files[i]
if (max.resolution.id < file.resolution.id) max = file
}
return max
}
function videoFileMinByResolution (files: VideoFile[]) {
let min = files[0]
for (let i = 1; i < files.length; i++) {
const file = files[i]
if (min.resolution.id > file.resolution.id) min = file
}
return min
}
function getRtcConfig () {
return {
iceServers: [
{
urls: 'stun:stun.stunprotocol.org'
},
{
urls: 'stun:stun.framasoft.org'
}
]
}
}
// ---------------------------------------------------------------------------
export {
getRtcConfig,
toTitleCase,
isWebRTCDisabled,
isP2PEnabled,
videoFileMaxByResolution,
videoFileMinByResolution,
isMobile,
bytes,
isIOS,
isSafari
}

View File

@ -43,3 +43,23 @@ export function sortBy (obj: any[], key1: string, key2?: string) {
return 1
})
}
export function maxBy <T> (arr: T[], property: keyof T) {
let result: T
for (const obj of arr) {
if (!result || result[property] < obj[property]) result = obj
}
return result
}
export function minBy <T> (arr: T[], property: keyof T) {
let result: T
for (const obj of arr) {
if (!result || result[property] > obj[property]) result = obj
}
return result
}

View File

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -1,19 +1,19 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
import { expect } from 'chai'
import { parallelTests } from '@peertube/peertube-node-utils'
import { ActorImageType, CustomConfig, HttpStatusCode } from '@peertube/peertube-models'
import { parallelTests } from '@peertube/peertube-node-utils'
import {
PeerTubeServer,
cleanupTests,
createSingleServer,
killallServers,
makeActivityPubGetRequest,
makeGetRequest,
makeRawRequest,
PeerTubeServer,
setAccessTokensToServers
} from '@peertube/peertube-server-commands'
import { testFileExistsOrNot, testImage, testAvatarSize } from '@tests/shared/checks.js'
import { testAvatarSize, testFileExistsOnFSOrNot, testImage } from '@tests/shared/checks.js'
import { expect } from 'chai'
import { basename } from 'path'
function checkInitialConfig (server: PeerTubeServer, data: CustomConfig) {
@ -703,18 +703,21 @@ describe('Test config', function () {
}
describe('Banner', function () {
let bannerPath: string
const bannerPaths: string[] = []
it('Should update instance banner', async function () {
await server.config.updateInstanceImage({ type: ActorImageType.BANNER, fixture: 'banner.jpg' })
const { banners } = await checkAndGetServerImages()
expect(banners).to.have.lengthOf(1)
expect(banners).to.have.lengthOf(2)
bannerPath = banners[0].path
await testImage(server.url, 'banner-resized', bannerPath)
await testFileExistsOrNot(server, 'avatars', basename(bannerPath), true)
for (const banner of banners) {
await testImage(server.url, `banner-resized-${banner.width}`, banner.path)
await testFileExistsOnFSOrNot(server, 'avatars', basename(banner.path), true)
bannerPaths.push(banner.path)
}
})
it('Should re-update an existing instance banner', async function () {
@ -727,12 +730,14 @@ describe('Test config', function () {
const { banners } = await checkAndGetServerImages()
expect(banners).to.have.lengthOf(0)
await testFileExistsOrNot(server, 'avatars', basename(bannerPath), false)
for (const bannerPath of bannerPaths) {
await testFileExistsOnFSOrNot(server, 'avatars', basename(bannerPath), false)
}
})
})
describe('Avatar', function () {
let avatarPath: string
const avatarPaths: string[] = []
it('Should update instance avatar', async function () {
for (const extension of [ '.png', '.gif' ]) {
@ -744,10 +749,10 @@ describe('Test config', function () {
for (const avatar of avatars) {
await testAvatarSize({ url: server.url, avatar, imageName: `avatar-resized-${avatar.width}x${avatar.width}` })
}
await testFileExistsOnFSOrNot(server, 'avatars', basename(avatar.path), true)
avatarPath = avatars[0].path
await testFileExistsOrNot(server, 'avatars', basename(avatarPath), true)
avatarPaths.push(avatar.path)
}
}
})
@ -768,7 +773,9 @@ describe('Test config', function () {
const { avatars } = await checkAndGetServerImages()
expect(avatars).to.have.lengthOf(0)
await testFileExistsOrNot(server, 'avatars', basename(avatarPath), false)
for (const avatarPath of avatarPaths) {
await testFileExistsOnFSOrNot(server, 'avatars', basename(avatarPath), false)
}
})
it('Should not have the avatars anymore in the AP representation of the instance', async function () {

View File

@ -444,7 +444,7 @@ function runTest (withObjectStorage: boolean) {
expect(secondaryChannel.support).to.equal('noah support')
expect(secondaryChannel.avatars).to.have.lengthOf(4)
expect(secondaryChannel.banners).to.have.lengthOf(1)
expect(secondaryChannel.banners).to.have.lengthOf(2)
const urls = [ ...secondaryChannel.avatars, ...secondaryChannel.banners ].map(a => a.url)
for (const url of urls) {

View File

@ -198,7 +198,9 @@ function runTest (withObjectStorage: boolean) {
expect(importedSecond.description).to.equal('noah description')
expect(importedSecond.support).to.equal('noah support')
await testImage(remoteServer.url, 'banner-resized', importedSecond.banners[0].path)
for (const banner of importedSecond.banners) {
await testImage(remoteServer.url, `banner-user-import-resized-${banner.width}`, banner.path)
}
for (const avatar of importedSecond.avatars) {
await testImage(remoteServer.url, `avatar-resized-${avatar.width}x${avatar.width}`, avatar.path, '.png')

View File

@ -1,22 +1,22 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
import { expect } from 'chai'
import { basename } from 'path'
import { ACTOR_IMAGES_SIZE } from '@peertube/peertube-server/core/initializers/constants.js'
import { testFileExistsOrNot, testImage } from '@tests/shared/checks.js'
import { SQLCommand } from '@tests/shared/sql-command.js'
import { wait } from '@peertube/peertube-core-utils'
import { ActorImageType, User, VideoChannel } from '@peertube/peertube-models'
import {
PeerTubeServer,
cleanupTests,
createMultipleServers,
doubleFollow,
PeerTubeServer,
setAccessTokensToServers,
setDefaultAccountAvatar,
setDefaultVideoChannel,
waitJobs
} from '@peertube/peertube-server-commands'
import { ACTOR_IMAGES_SIZE } from '@peertube/peertube-server/core/initializers/constants.js'
import { testFileExistsOnFSOrNot, testImage } from '@tests/shared/checks.js'
import { SQLCommand } from '@tests/shared/sql-command.js'
import { expect } from 'chai'
import { basename } from 'path'
async function findChannel (server: PeerTubeServer, channelId: number) {
const body = await server.channels.list({ sort: '-name' })
@ -294,7 +294,7 @@ describe('Test video channels', function () {
for (const avatar of videoChannel.avatars) {
avatarPaths[server.port] = avatar.path
await testImage(server.url, `avatar-resized-${avatar.width}x${avatar.width}`, avatarPaths[server.port], '.png')
await testFileExistsOrNot(server, 'avatars', basename(avatarPaths[server.port]), true)
await testFileExistsOnFSOrNot(server, 'avatars', basename(avatarPaths[server.port]), true)
const row = await sqlCommands[i].getActorImage(basename(avatarPaths[server.port]))
@ -320,14 +320,18 @@ describe('Test video channels', function () {
const server = servers[i]
const videoChannel = await server.channels.get({ channelName: 'second_video_channel@' + servers[0].host })
const expectedSizes = ACTOR_IMAGES_SIZE[ActorImageType.BANNER]
bannerPaths[server.port] = videoChannel.banners[0].path
await testImage(server.url, 'banner-resized', bannerPaths[server.port])
await testFileExistsOrNot(server, 'avatars', basename(bannerPaths[server.port]), true)
expect(videoChannel.banners.length).to.equal(expectedSizes.length, 'Expected banners to be generated in all sizes')
const row = await sqlCommands[i].getActorImage(basename(bannerPaths[server.port]))
expect(row.height).to.equal(ACTOR_IMAGES_SIZE[ActorImageType.BANNER][0].height)
expect(row.width).to.equal(ACTOR_IMAGES_SIZE[ActorImageType.BANNER][0].width)
for (const banner of videoChannel.banners) {
bannerPaths[server.port] = banner.path
await testImage(server.url, `banner-resized-${banner.width}`, bannerPaths[server.port])
await testFileExistsOnFSOrNot(server, 'avatars', basename(bannerPaths[server.port]), true)
const row = await sqlCommands[i].getActorImage(basename(bannerPaths[server.port]))
expect(expectedSizes.some(({ height, width }) => row.height === height && row.width === width)).to.equal(true)
}
}
})
@ -357,7 +361,7 @@ describe('Test video channels', function () {
for (const server of servers) {
const videoChannel = await findChannel(server, secondVideoChannelId)
await testFileExistsOrNot(server, 'avatars', basename(avatarPaths[server.port]), false)
await testFileExistsOnFSOrNot(server, 'avatars', basename(avatarPaths[server.port]), false)
expect(videoChannel.avatars).to.be.empty
}
@ -372,7 +376,7 @@ describe('Test video channels', function () {
for (const server of servers) {
const videoChannel = await findChannel(server, secondVideoChannelId)
await testFileExistsOrNot(server, 'avatars', basename(bannerPaths[server.port]), false)
await testFileExistsOnFSOrNot(server, 'avatars', basename(bannerPaths[server.port]), false)
expect(videoChannel.banners).to.be.empty
}

View File

@ -114,7 +114,7 @@ async function testImage (url: string, imageName: string, imageHTTPPath: string,
}
}
async function testFileExistsOrNot (server: PeerTubeServer, directory: string, filePath: string, exist: boolean) {
async function testFileExistsOnFSOrNot (server: PeerTubeServer, directory: string, filePath: string, exist: boolean) {
const base = server.servers.buildDirectory(directory)
expect(await pathExists(join(base, filePath))).to.equal(exist)
@ -182,7 +182,7 @@ export {
testAvatarSize,
testImage,
expectLogDoesNotContain,
testFileExistsOrNot,
testFileExistsOnFSOrNot,
expectStartWith,
expectNotStartWith,
expectEndWith,

View File

@ -27,7 +27,7 @@ import { resolve } from 'path'
import { MockSmtpServer } from './mock-servers/mock-email.js'
import { getAllNotificationsSettings } from './notifications.js'
import { getFilenameFromUrl } from '@peertube/peertube-node-utils'
import { testFileExistsOrNot } from './checks.js'
import { testFileExistsOnFSOrNot } from './checks.js'
type ExportOutbox = ActivityPubOrderedCollection<ActivityCreate<VideoObject | VideoCommentObject>>
@ -101,10 +101,10 @@ export async function checkExportFileExists (options: {
return makeRawRequest({ url: redirectedUrl, expectedStatus: HttpStatusCode.OK_200 })
}
return testFileExistsOrNot(server, 'tmp-persistent', filename, true)
return testFileExistsOnFSOrNot(server, 'tmp-persistent', filename, true)
}
await testFileExistsOrNot(server, 'tmp-persistent', filename, false)
await testFileExistsOnFSOrNot(server, 'tmp-persistent', filename, false)
if (withObjectStorage) {
await makeRawRequest({ url: redirectedUrl, expectedStatus: HttpStatusCode.NOT_FOUND_404 })

View File

@ -1,14 +1,13 @@
import express from 'express'
import { Feed } from '@peertube/feed'
import { CustomTag, CustomXMLNS, Person } from '@peertube/feed/lib/typings/index.js'
import { maxBy, pick } from '@peertube/peertube-core-utils'
import { ActorImageType } from '@peertube/peertube-models'
import { mdToOneLinePlainText } from '@server/helpers/markdown.js'
import { CONFIG } from '@server/initializers/config.js'
import { WEBSERVER } from '@server/initializers/constants.js'
import { getBiggestActorImage } from '@server/lib/actor-image.js'
import { UserModel } from '@server/models/user/user.js'
import { MAccountDefault, MChannelBannerAccountDefault, MUser, MVideoFullLight } from '@server/types/models/index.js'
import { pick } from '@peertube/peertube-core-utils'
import { ActorImageType } from '@peertube/peertube-models'
import express from 'express'
export function initFeed (parameters: {
name: string
@ -105,12 +104,12 @@ export async function buildFeedMetadata (options: {
accountLink = videoChannel.Account.getClientUrl()
if (videoChannel.Actor.hasImage(ActorImageType.AVATAR)) {
const videoChannelAvatar = getBiggestActorImage(videoChannel.Actor.Avatars)
const videoChannelAvatar = maxBy(videoChannel.Actor.Avatars, 'width')
imageUrl = WEBSERVER.URL + videoChannelAvatar.getStaticPath()
}
if (videoChannel.Account.Actor.hasImage(ActorImageType.AVATAR)) {
const accountAvatar = getBiggestActorImage(videoChannel.Account.Actor.Avatars)
const accountAvatar = maxBy(videoChannel.Account.Actor.Avatars, 'width')
accountImageUrl = WEBSERVER.URL + accountAvatar.getStaticPath()
}
@ -123,7 +122,7 @@ export async function buildFeedMetadata (options: {
accountLink = link
if (account.Actor.hasImage(ActorImageType.AVATAR)) {
const accountAvatar = getBiggestActorImage(account.Actor.Avatars)
const accountAvatar = maxBy(account.Actor.Avatars, 'width')
imageUrl = WEBSERVER.URL + accountAvatar?.getStaticPath()
accountImageUrl = imageUrl
}

View File

@ -1,21 +1,20 @@
import express from 'express'
import { extname } from 'path'
import { Feed } from '@peertube/feed'
import { CustomTag, CustomXMLNS, LiveItemStatus } from '@peertube/feed/lib/typings/index.js'
import { getBiggestActorImage } from '@server/lib/actor-image.js'
import { maxBy, sortObjectComparator } from '@peertube/peertube-core-utils'
import { ActorImageType, VideoFile, VideoInclude, VideoResolution, VideoState } from '@peertube/peertube-models'
import { InternalEventEmitter } from '@server/lib/internal-event-emitter.js'
import { Hooks } from '@server/lib/plugins/hooks.js'
import { getVideoFileMimeType } from '@server/lib/video-file.js'
import { buildPodcastGroupsCache, cacheRouteFactory, videoFeedsPodcastSetCacheKey } from '@server/middlewares/index.js'
import { MVideo, MVideoCaptionVideo, MVideoFullLight } from '@server/types/models/index.js'
import { sortObjectComparator } from '@peertube/peertube-core-utils'
import { ActorImageType, VideoFile, VideoInclude, VideoResolution, VideoState } from '@peertube/peertube-models'
import express from 'express'
import { extname } from 'path'
import { buildNSFWFilter } from '../../helpers/express-utils.js'
import { MIMETYPES, ROUTE_CACHE_LIFETIME, WEBSERVER } from '../../initializers/constants.js'
import { asyncMiddleware, setFeedPodcastContentType, videoFeedsPodcastValidator } from '../../middlewares/index.js'
import { VideoModel } from '../../models/video/video.js'
import { VideoCaptionModel } from '../../models/video/video-caption.js'
import { VideoModel } from '../../models/video/video.js'
import { buildFeedMetadata, getCommonVideoFeedAttributes, getVideosForFeeds, initFeed } from './shared/index.js'
import { getVideoFileMimeType } from '@server/lib/video-file.js'
const videoPodcastFeedsRouter = express.Router()
@ -151,7 +150,7 @@ async function generatePodcastItem (options: {
let personImage: string
if (account.Actor.hasImage(ActorImageType.AVATAR)) {
const avatar = getBiggestActorImage(account.Actor.Avatars)
const avatar = maxBy(account.Actor.Avatars, 'width')
personImage = WEBSERVER.URL + avatar.getStaticPath()
}

View File

@ -898,7 +898,7 @@ const PREVIEWS_SIZE = {
minWidth: 400
}
const ACTOR_IMAGES_SIZE: { [key in ActorImageType_Type]: { width: number, height: number }[] } = {
[ActorImageType.AVATAR]: [
[ActorImageType.AVATAR]: [ // 1/1 ratio
{
width: 1500,
height: 1500
@ -916,10 +916,14 @@ const ACTOR_IMAGES_SIZE: { [key in ActorImageType_Type]: { width: number, height
height: 48
}
],
[ActorImageType.BANNER]: [
[ActorImageType.BANNER]: [ // 6/1 ratio
{
width: 1920,
height: 317 // 6/1 ratio
height: 317
},
{
width: 600,
height: 100
}
]
}

View File

@ -1,4 +1,4 @@
import { arrayify } from '@peertube/peertube-core-utils'
import { arrayify, maxBy, minBy } from '@peertube/peertube-core-utils'
import {
ActivityHashTagObject,
ActivityMagnetUrlObject,
@ -24,13 +24,11 @@ import { VideoFileModel } from '@server/models/video/video-file.js'
import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist.js'
import { FilteredModelAttributes } from '@server/types/index.js'
import { MChannelId, MStreamingPlaylistVideo, MVideo, MVideoId, isStreamingPlaylist } from '@server/types/models/index.js'
import maxBy from 'lodash-es/maxBy.js'
import minBy from 'lodash-es/minBy.js'
import { decode as magnetUriDecode } from 'magnet-uri'
import { basename, extname } from 'path'
import { getDurationFromActivityStream } from '../../activity.js'
function getThumbnailFromIcons (videoObject: VideoObject) {
export function getThumbnailFromIcons (videoObject: VideoObject) {
let validIcons = videoObject.icon.filter(i => i.width > THUMBNAILS_SIZE.minWidth)
// Fallback if there are not valid icons
if (validIcons.length === 0) validIcons = videoObject.icon
@ -38,19 +36,19 @@ function getThumbnailFromIcons (videoObject: VideoObject) {
return minBy(validIcons, 'width')
}
function getPreviewFromIcons (videoObject: VideoObject) {
export function getPreviewFromIcons (videoObject: VideoObject) {
const validIcons = videoObject.icon.filter(i => i.width > PREVIEWS_SIZE.minWidth)
return maxBy(validIcons, 'width')
}
function getTagsFromObject (videoObject: VideoObject) {
export function getTagsFromObject (videoObject: VideoObject) {
return videoObject.tag
.filter(isAPHashTagObject)
.map(t => t.name)
}
function getFileAttributesFromUrl (
export function getFileAttributesFromUrl (
videoOrPlaylist: MVideo | MStreamingPlaylistVideo,
urls: (ActivityTagObject | ActivityUrlObject)[]
) {
@ -117,7 +115,7 @@ function getFileAttributesFromUrl (
return attributes
}
function getStreamingPlaylistAttributesFromObject (video: MVideoId, videoObject: VideoObject) {
export function getStreamingPlaylistAttributesFromObject (video: MVideoId, videoObject: VideoObject) {
const playlistUrls = videoObject.url.filter(u => isAPStreamingPlaylistUrlObject(u)) as ActivityPlaylistUrlObject[]
if (playlistUrls.length === 0) return []
@ -154,7 +152,7 @@ function getStreamingPlaylistAttributesFromObject (video: MVideoId, videoObject:
return attributes
}
function getLiveAttributesFromObject (video: MVideoId, videoObject: VideoObject) {
export function getLiveAttributesFromObject (video: MVideoId, videoObject: VideoObject) {
return {
saveReplay: videoObject.liveSaveReplay,
permanentLive: videoObject.permanentLive,
@ -163,7 +161,7 @@ function getLiveAttributesFromObject (video: MVideoId, videoObject: VideoObject)
}
}
function getCaptionAttributesFromObject (video: MVideoId, videoObject: VideoObject) {
export function getCaptionAttributesFromObject (video: MVideoId, videoObject: VideoObject) {
return videoObject.subtitleLanguage.map(c => ({
videoId: video.id,
filename: VideoCaptionModel.generateCaptionName(c.identifier),
@ -172,7 +170,7 @@ function getCaptionAttributesFromObject (video: MVideoId, videoObject: VideoObje
}))
}
function getStoryboardAttributeFromObject (video: MVideoId, videoObject: VideoObject) {
export function getStoryboardAttributeFromObject (video: MVideoId, videoObject: VideoObject) {
if (!isArray(videoObject.preview)) return undefined
const storyboard = videoObject.preview.find(p => p.rel.includes('storyboard'))
@ -192,7 +190,7 @@ function getStoryboardAttributeFromObject (video: MVideoId, videoObject: VideoOb
}
}
function getVideoAttributesFromObject (videoChannel: MChannelId, videoObject: VideoObject, to: string[] = []) {
export function getVideoAttributesFromObject (videoChannel: MChannelId, videoObject: VideoObject, to: string[] = []) {
const privacy = to.includes(ACTIVITY_PUB.PUBLIC)
? VideoPrivacy.PUBLIC
: VideoPrivacy.UNLISTED
@ -247,23 +245,7 @@ function getVideoAttributesFromObject (videoChannel: MChannelId, videoObject: Vi
}
// ---------------------------------------------------------------------------
export {
getThumbnailFromIcons,
getPreviewFromIcons,
getTagsFromObject,
getFileAttributesFromUrl,
getStreamingPlaylistAttributesFromObject,
getLiveAttributesFromObject,
getCaptionAttributesFromObject,
getStoryboardAttributeFromObject,
getVideoAttributesFromObject
}
// Private
// ---------------------------------------------------------------------------
function isAPVideoUrlObject (url: any): url is ActivityVideoUrlObject {

View File

@ -1,14 +0,0 @@
import maxBy from 'lodash-es/maxBy.js'
function getBiggestActorImage <T extends { width: number }> (images: T[]) {
const image = maxBy(images, 'width')
// If width is null, maxBy won't return a value
if (!image) return images[0]
return image
}
export {
getBiggestActorImage
}

View File

@ -1,14 +1,13 @@
import { escapeHTML } from '@peertube/peertube-core-utils'
import { escapeHTML, maxBy } from '@peertube/peertube-core-utils'
import { HttpStatusCode } from '@peertube/peertube-models'
import express from 'express'
import { CONFIG } from '../../../initializers/config.js'
import { AccountModel } from '@server/models/account/account.js'
import { ActorImageModel } from '@server/models/actor/actor-image.js'
import { VideoChannelModel } from '@server/models/video/video-channel.js'
import { MAccountHost, MChannelHost } from '@server/types/models/index.js'
import { getBiggestActorImage } from '@server/lib/actor-image.js'
import { ActorImageModel } from '@server/models/actor/actor-image.js'
import { TagsHtml } from './tags-html.js'
import express from 'express'
import { CONFIG } from '../../../initializers/config.js'
import { PageHtml } from './page-html.js'
import { TagsHtml } from './tags-html.js'
export class ActorHtml {
@ -60,7 +59,7 @@ export class ActorHtml {
const siteName = CONFIG.INSTANCE.NAME
const title = entity.getDisplayName()
const avatar = getBiggestActorImage(entity.Actor.Avatars)
const avatar = maxBy(entity.Actor.Avatars, 'width')
const image = {
url: ActorImageModel.getImageUrl(avatar),
width: avatar?.width,

View File

@ -13,7 +13,10 @@ export abstract class ActorExporter <T> extends AbstractUserExporter<T> {
name: actor.preferredUsername,
avatars: this.exportActorImageJSON(actor.Avatars),
avatars: actor.hasImage(ActorImageType.AVATAR)
? this.exportActorImageJSON(actor.Avatars)
: [],
banners: actor.hasImage(ActorImageType.BANNER)
? this.exportActorImageJSON(actor.Banners)
: []

View File

@ -1,12 +1,10 @@
import { forceNumber } from '@peertube/peertube-core-utils'
import { forceNumber, maxBy } from '@peertube/peertube-core-utils'
import { ActivityIconObject, ActorImageType, ActorImageType_Type, type ActivityPubActorType } from '@peertube/peertube-models'
import { getLowercaseExtension } from '@peertube/peertube-node-utils'
import { AttributesOnly } from '@peertube/peertube-typescript-utils'
import { activityPubContextify } from '@server/helpers/activity-pub-utils.js'
import { getContextFilter } from '@server/lib/activitypub/context.js'
import { getBiggestActorImage } from '@server/lib/actor-image.js'
import { ModelCache } from '@server/models/shared/model-cache.js'
import { col, fn, literal, Op, QueryTypes, Transaction, where } from 'sequelize'
import { Op, QueryTypes, Transaction, col, fn, literal, where } from 'sequelize'
import {
AllowNull,
BelongsTo,
@ -33,16 +31,14 @@ import { isActivityPubUrlValid } from '../../helpers/custom-validators/activityp
import {
ACTIVITY_PUB,
ACTIVITY_PUB_ACTOR_TYPES,
CONSTRAINTS_FIELDS,
MIMETYPES,
SERVER_ACTOR_NAME,
CONSTRAINTS_FIELDS, SERVER_ACTOR_NAME,
WEBSERVER
} from '../../initializers/constants.js'
import {
MActor,
MActorAccountChannelId,
MActorAPAccount,
MActorAPChannel,
MActorAccountChannelId,
MActorFollowersUrl,
MActorFormattable,
MActorFull,
@ -61,7 +57,6 @@ import { VideoChannelModel } from '../video/video-channel.js'
import { VideoModel } from '../video/video.js'
import { ActorFollowModel } from './actor-follow.js'
import { ActorImageModel } from './actor-image.js'
import maxBy from 'lodash-es/maxBy.js'
enum ScopeNames {
FULL = 'FULL'
@ -562,24 +557,15 @@ export class ActorModel extends SequelizeModel<ActorModel> {
}
toActivityPubObject (this: MActorAPChannel | MActorAPAccount, name: string) {
let icon: ActivityIconObject[]
let image: ActivityIconObject
let icon: ActivityIconObject[] // Avatars
let image: ActivityIconObject[] // Banners
if (this.hasImage(ActorImageType.AVATAR)) {
icon = this.Avatars.map(a => a.toActivityPubObject())
}
if (this.hasImage(ActorImageType.BANNER)) {
const banner = getBiggestActorImage((this as MActorAPChannel).Banners)
const extension = getLowercaseExtension(banner.filename)
image = {
type: 'Image',
mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension],
height: banner.height,
width: banner.width,
url: ActorImageModel.getImageUrl(banner)
}
image = (this as MActorAPChannel).Banners.map(b => b.toActivityPubObject())
}
const json = {

View File

@ -1,7 +1,6 @@
import { forceNumber } from '@peertube/peertube-core-utils'
import { forceNumber, maxBy } from '@peertube/peertube-core-utils'
import { UserNotification, type UserNotificationType_Type } from '@peertube/peertube-models'
import { uuidToShort } from '@peertube/peertube-node-utils'
import { getBiggestActorImage } from '@server/lib/actor-image.js'
import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/types/models/user/index.js'
import { ModelIndexesOptions, Op, WhereOptions } from 'sequelize'
import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Table, UpdatedAt } from 'sequelize-typescript'
@ -518,7 +517,7 @@ export class UserNotificationModel extends SequelizeModel<UserNotificationModel>
if (!avatars || avatars.length === 0) return { avatar: undefined, avatars: [] }
return {
avatar: this.formatAvatar(getBiggestActorImage(avatars)),
avatar: this.formatAvatar(maxBy(avatars, 'width')),
avatars: avatars.map(a => this.formatAvatar(a))
}

View File

@ -1,4 +1,4 @@
import { buildVideoEmbedPath, buildVideoWatchPath, pick, wait } from '@peertube/peertube-core-utils'
import { buildVideoEmbedPath, buildVideoWatchPath, maxBy, minBy, pick, wait } from '@peertube/peertube-core-utils'
import { ffprobePromise, getAudioStream, getVideoStreamDimensionsInfo, getVideoStreamFPS, hasAudioStream } from '@peertube/peertube-ffmpeg'
import {
FileStorage,
@ -38,8 +38,6 @@ import { ModelCache } from '@server/models/shared/model-cache.js'
import { MVideoSource } from '@server/types/models/video/video-source.js'
import Bluebird from 'bluebird'
import { remove } from 'fs-extra/esm'
import maxBy from 'lodash-es/maxBy.js'
import minBy from 'lodash-es/minBy.js'
import { FindOptions, IncludeOptions, Includeable, Op, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize'
import {
AfterCreate,
@ -1711,9 +1709,9 @@ export class VideoModel extends SequelizeModel<VideoModel> {
return this.VideoChannel.Account.Actor.Server?.isBlocked() || this.VideoChannel.Account.isBlocked()
}
getQualityFileBy<T extends MVideoWithFile> (this: T, fun: (files: MVideoFile[], it: (file: MVideoFile) => number) => MVideoFile) {
getQualityFileBy<T extends MVideoWithFile> (this: T, fun: (files: MVideoFile[], property: 'resolution') => MVideoFile) {
const files = this.getAllFiles()
const file = fun(files, file => file.resolution)
const file = fun(files, 'resolution')
if (!file) return undefined
if (file.videoId) {

View File

@ -1,3 +1,4 @@
import { maxBy, minBy } from '@peertube/peertube-core-utils'
import { ActorImageType } from '@peertube/peertube-models'
import { buildUUID, getLowercaseExtension } from '@peertube/peertube-node-utils'
import { getImageSize, processImage } from '@server/helpers/image-utils.js'
@ -6,13 +7,11 @@ import { ACTOR_IMAGES_SIZE } from '@server/initializers/constants.js'
import { initDatabaseModels } from '@server/initializers/database.js'
import { updateActorImages } from '@server/lib/activitypub/actors/index.js'
import { sendUpdateActor } from '@server/lib/activitypub/send/index.js'
import { getBiggestActorImage } from '@server/lib/actor-image.js'
import { JobQueue } from '@server/lib/job-queue/index.js'
import { AccountModel } from '@server/models/account/account.js'
import { ActorModel } from '@server/models/actor/actor.js'
import { VideoChannelModel } from '@server/models/video/video-channel.js'
import { MAccountDefault, MActorDefault, MChannelDefault } from '@server/types/models/index.js'
import minBy from 'lodash-es/minBy.js'
import { join } from 'path'
run()
@ -100,7 +99,7 @@ async function generateSmallerAvatarIfNeeded (accountOrChannel: MAccountDefault
}
async function generateSmallerAvatar (actor: MActorDefault) {
const bigAvatar = getBiggestActorImage(actor.Avatars)
const bigAvatar = maxBy(actor.Avatars, 'width')
const imageSize = minBy(ACTOR_IMAGES_SIZE[ActorImageType.AVATAR], 'width')
const sourceFilename = bigAvatar.filename