Generate small versions of banners too
This commit is contained in:
parent
aaa5acbb0c
commit
11521f231f
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
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 |
|
@ -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 () {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 })
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
: []
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue