Rebuild thumbnail and media file architecture

* Move to sharp library to improve image manipulation performance and
   support more image formats
 * Move video previews in thumbnails directory
 * Deprecate lazy static previews endpoint
 * Plugin `getFiles()` API now includes a `width`/`height` attribute
 * Deprecate previewfile for video publication/update.
   Use `thumbnailfile` instead
 * Deprecate `thumbnailPath` and `previewPath` of `Video` in favour of
   `thumbnails`
 * Deprecate `thumbnailPath` of `VideoPlaylist` in favour of
   `thumbnails`
 * Replace `torrentPath` by `torrentFilename` and `torrentStream` for
   remote torrents for `filter:api.download.torrent.allowed.result`
   plugin filter
 * Remove useless `fileUrl` from `UserExport` and `VideoSource`
 * Remove remote captions where we don't have a valid URL
 * cache directory is not deleted on each run anymore
 * Add permanent cache for video captions and storyboards
 * Remove `cache` admin configuration, use house-keeping script instead
This commit is contained in:
Chocobozzz
2026-02-13 06:20:56 +01:00
parent 7aac24485c
commit 526f1c6df5
246 changed files with 3074 additions and 3768 deletions
+1 -4
View File
@@ -11,7 +11,6 @@ type UploadOptions = {
username?: string
password?: string
thumbnail?: string
preview?: string
file?: string
videoName?: string
category?: number
@@ -38,7 +37,6 @@ export function defineUploadProgram () {
.option('-U, --username <username>', 'Username')
.option('-p, --password <token>', 'Password')
.option('-b, --thumbnail <thumbnailPath>', 'Thumbnail path')
.option('--preview <previewPath>', 'Preview path')
.option('-f, --file <file>', 'Video absolute file path')
.option('-n, --video-name <name>', 'Video name')
.option('-c, --category <category_number>', 'Category number', parseInt)
@@ -105,8 +103,7 @@ async function run (options: UploadOptions) {
...baseAttributes,
fixture: options.file,
thumbnailfile: options.thumbnail,
previewfile: options.preview
thumbnailfile: options.thumbnail
}
try {
@@ -169,7 +169,7 @@ export async function processAudioMergeTranscoding (options: ProcessOptions<Runn
let ffmpegProgress: number
let audioPath: string
let previewPath: string
let thumbnailPath: string
const outputPath = join(ConfigManager.Instance.getTranscodingDirectory(), `output-${buildUUID()}.mp4`)
@@ -187,7 +187,7 @@ export async function processAudioMergeTranscoding (options: ProcessOptions<Runn
)
audioPath = await downloadInputFile({ url: payload.input.audioFileUrl, runnerToken, job })
previewPath = await downloadInputFile({ url: payload.input.previewFileUrl, runnerToken, job })
thumbnailPath = await downloadInputFile({ url: payload.input.previewFileUrl, runnerToken, job })
logger.info(
`Downloaded input files ${payload.input.audioFileUrl} and ${payload.input.previewFileUrl} ` +
@@ -204,7 +204,7 @@ export async function processAudioMergeTranscoding (options: ProcessOptions<Runn
type: 'merge-audio',
audioPath,
videoInputPath: previewPath,
videoInputPath: thumbnailPath,
outputPath,
@@ -227,7 +227,7 @@ export async function processAudioMergeTranscoding (options: ProcessOptions<Runn
})
} finally {
if (audioPath) await remove(audioPath)
if (previewPath) await remove(previewPath)
if (thumbnailPath) await remove(thumbnailPath)
if (outputPath) await remove(outputPath)
if (updateProgressInterval) clearInterval(updateProgressInterval)
}
@@ -1,6 +1,6 @@
import { Component, OnInit, inject } from '@angular/core'
import { ServerService } from '@app/core'
import { Actor } from '@app/shared/shared-main/account/actor.model'
import { findAppropriateImageFileUrl } from '@root-helpers/images'
@Component({
selector: 'my-follower-image',
@@ -15,6 +15,6 @@ export class FollowerImageComponent implements OnInit {
avatarUrl: string
ngOnInit () {
this.avatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.server.getHTMLConfig().instance, 30)
this.avatarUrl = findAppropriateImageFileUrl(this.server.getHTMLConfig().instance.avatars, 30)
}
}
@@ -1,6 +1,6 @@
import { Component, OnInit, inject } from '@angular/core'
import { ServerService } from '@app/core'
import { Actor } from '@app/shared/shared-main/account/actor.model'
import { findAppropriateImageFileUrl } from '@root-helpers/images'
@Component({
selector: 'my-subscription-image',
@@ -15,6 +15,6 @@ export class SubscriptionImageComponent implements OnInit {
avatarUrl: string
ngOnInit () {
this.avatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.server.getHTMLConfig().instance, 30)
this.avatarUrl = findAppropriateImageFileUrl(this.server.getHTMLConfig().instance.avatars, 30)
}
}
+2 -2
View File
@@ -2,12 +2,12 @@ import { Component, OnInit, inject, viewChild } from '@angular/core'
import { RouterOutlet } from '@angular/router'
import { ServerService } from '@app/core'
import { GlobalIconComponent } from '@app/shared/shared-icons/global-icon.component'
import { Actor } from '@app/shared/shared-main/account/actor.model'
import { ButtonComponent } from '@app/shared/shared-main/buttons/button.component'
import { HorizontalMenuComponent, HorizontalMenuEntry } from '@app/shared/shared-main/menu/horizontal-menu.component'
import { SupportModalComponent } from '@app/shared/shared-support-modal/support-modal.component'
import { maxBy } from '@peertube/peertube-core-utils'
import { HTMLServerConfig } from '@peertube/peertube-models'
import { findAppropriateImageFileUrl } from '@root-helpers/images'
@Component({
selector: 'my-about',
@@ -34,7 +34,7 @@ export class AboutComponent implements OnInit {
? maxBy(this.config.instance.banners, 'width').fileUrl
: undefined
this.avatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.config.instance, 110)
this.avatarUrl = findAppropriateImageFileUrl(this.config.instance.avatars, 110)
this.menuEntries = [
{
@@ -10,75 +10,6 @@
Some files are not federated, and fetched when necessary. Define their caching policies.
</div>
</div>
<div class="content-col">
<ng-container formGroupName="cache">
<div class="form-group" formGroupName="previews">
<label i18n for="cachePreviewsSize">Number of previews to keep in cache</label>
<div class="number-with-unit">
<input
type="number" min="0" id="cachePreviewsSize" class="form-control"
formControlName="size" [ngClass]="{ 'input-error': formErrors.cache.previews.size }"
>
<span i18n>{getCacheSize('previews'), plural, =1 {cached image} other {cached images}}</span>
</div>
@if (formErrors.cache.previews.size) {
<div class="form-error" role="alert">{{ formErrors.cache.previews.size }}</div>
}
</div>
<div class="form-group" formGroupName="captions">
<label i18n for="cacheCaptionsSize">Number of video captions to keep in cache</label>
<div class="number-with-unit">
<input
type="number" min="0" id="cacheCaptionsSize" class="form-control"
formControlName="size" [ngClass]="{ 'input-error': formErrors.cache.captions.size }"
>
<span i18n>{getCacheSize('captions'), plural, =1 {cached caption} other {cached captions}}</span>
</div>
@if (formErrors.cache.captions.size) {
<div class="form-error" role="alert">{{ formErrors.cache.captions.size }}</div>
}
</div>
<div class="form-group" formGroupName="torrents">
<label i18n for="cacheTorrentsSize">Number of video torrents to keep in cache</label>
<div class="number-with-unit">
<input
type="number" min="0" id="cacheTorrentsSize" class="form-control"
formControlName="size" [ngClass]="{ 'input-error': formErrors.cache.torrents.size }"
>
<span i18n>{getCacheSize('torrents'), plural, =1 {cached torrent} other {cached torrents}}</span>
</div>
@if (formErrors.cache.torrents.size) {
<div class="form-error" role="alert">{{ formErrors.cache.torrents.size }}</div>
}
</div>
<div class="form-group" formGroupName="storyboards">
<label i18n for="cacheTorrentsSize">Number of video storyboard images to keep in cache</label>
<div class="number-with-unit">
<input
type="number" min="0" id="cacheStoryboardsSize" class="form-control"
formControlName="size" [ngClass]="{ 'input-error': formErrors.cache.storyboards.size }"
>
<span i18n>{getCacheSize('storyboards'), plural, =1 {cached storyboard} other {cached storyboards}}</span>
</div>
@if (formErrors.cache.storyboards.size) {
<div class="form-error" role="alert">{{ formErrors.cache.storyboards.size }}</div>
}
</div>
</ng-container>
</div>
</div>
<div class="pt-two-cols mt-4">
@@ -3,7 +3,7 @@ import { Component, inject, OnDestroy, OnInit } from '@angular/core'
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
import { ActivatedRoute } from '@angular/router'
import { CanComponentDeactivate } from '@app/core'
import { CACHE_SIZE_VALIDATOR, SERVICES_TWITTER_USERNAME_VALIDATOR } from '@app/shared/form-validators/custom-config-validators'
import { SERVICES_TWITTER_USERNAME_VALIDATOR } from '@app/shared/form-validators/custom-config-validators'
import {
BuildFormArgumentTyped,
FormDefaultTyped,
@@ -22,21 +22,6 @@ type Form = {
username: FormControl<string>
}>
}>
cache: FormGroup<{
previews: FormGroup<{
size: FormControl<number>
}>
captions: FormGroup<{
size: FormControl<number>
}>
torrents: FormGroup<{
size: FormControl<number>
}>
storyboards: FormGroup<{
size: FormControl<number>
}>
}>
}
@Component({
@@ -84,20 +69,6 @@ export class AdminConfigAdvancedComponent implements OnInit, OnDestroy, CanCompo
twitter: {
username: SERVICES_TWITTER_USERNAME_VALIDATOR
}
},
cache: {
previews: {
size: CACHE_SIZE_VALIDATOR
},
captions: {
size: CACHE_SIZE_VALIDATOR
},
torrents: {
size: CACHE_SIZE_VALIDATOR
},
storyboards: {
size: CACHE_SIZE_VALIDATOR
}
}
}
@@ -114,10 +85,6 @@ export class AdminConfigAdvancedComponent implements OnInit, OnDestroy, CanCompo
this.validationMessages = validationMessages
}
getCacheSize (type: 'captions' | 'previews' | 'torrents' | 'storyboards') {
return this.form.value.cache[type].size
}
save () {
this.adminConfigService.saveAndUpdateCurrent({
currentConfig: this.customConfig,
@@ -21,6 +21,7 @@ my-select-options {
}
my-image-input {
// Keep it sync with image size requested by the component
width: 223px;
height: 122px;
}
@@ -150,7 +150,8 @@ export class MyVideoPlaylistUpdateComponent extends MyVideoPlaylistEdit implemen
videoChannelId: this.videoPlaylistToUpdate.videoChannel ? this.videoPlaylistToUpdate.videoChannel.id : null
})
fetch(this.videoPlaylistToUpdate.thumbnailUrl)
// Keep it sync with image size set in the SASS file
fetch(this.videoPlaylistToUpdate.getThumbnailUrl(223))
.then(response => response.blob())
.then(data => {
this.form.patchValue({
@@ -22,5 +22,7 @@
my-video-playlist-miniature {
display: block;
// Sync wid
width: 130px;
}
@@ -49,8 +49,10 @@ my-video-miniature {
}
.other-videos:not(.display-as-row) my-video-miniature {
min-width: $video-thumbnail-medium-width;
max-width: $video-thumbnail-medium-width;
--co-miniature-max-width: #{$video-thumbnail-medium-width};
min-width: var(--co-miniature-min-width);
max-width: var(--co-miniature-max-width);
}
.display-as-row {
@@ -870,9 +870,9 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
!video.canBypassPassword(this.authUser),
videoPassword: () => videoPassword,
poster: video.isNSFWBlurForUser(loggedInOrAnonymousUser, this.serverConfig)
thumbnails: video.isNSFWBlurForUser(loggedInOrAnonymousUser, this.serverConfig)
? null
: video.previewUrl,
: video.thumbnails,
nsfwWarning: video.isNSFWHiddenOrWarned(loggedInOrAnonymousUser, this.serverConfig)
? {
@@ -980,7 +980,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
this.peertubePlayer.disable()
if (hasPlayed || !this.video.isNSFWBlurForUser(this.authUser || this.anonymousUser, this.serverConfig)) {
this.peertubePlayer.setPoster(this.video.previewPath)
this.peertubePlayer.setPoster(this.video.thumbnails)
}
}
@@ -22,14 +22,14 @@
</div>
@if (uploadingAudioFile) {
<div class="form-group audio-preview">
<label i18n for="previewfileUpload">Video background image</label>
<div class="form-group audio-thumbnail">
<label i18n for="thumbnailfileUpload">Video background image</label>
<div i18n class="audio-image-info">
Image that will be merged with your audio file.
<br />
The chosen image will be definitive and cannot be modified.
</div>
<my-image-input i18n-inputLabel inputLabel="Edit" inputName="previewfileUpload" [(ngModel)]="audioPreviewFile"></my-image-input>
<my-image-input i18n-inputLabel inputLabel="Edit" inputName="thumbnailfileUpload" [(ngModel)]="audioThumbnailFile"></my-image-input>
</div>
<div class="form-group upload-audio-button">
<my-button theme="primary" [label]="getAudioUploadLabel()" icon="upload" (click)="uploadAudio()"></my-button>
@@ -11,7 +11,7 @@
margin-bottom: 10px;
}
.audio-preview {
.audio-thumbnail {
margin: 30px 0;
my-image-input {
@@ -58,7 +58,7 @@ export class VideoUploadComponent implements OnInit, OnDestroy, AfterViewInit, C
readonly videoFileInput = viewChild<ElementRef<HTMLInputElement>>('videoFileInput')
uploadingAudioFile = false
audioPreviewFile: File
audioThumbnailFile: File
firstStep = true
firstStepChannelId: number
@@ -164,11 +164,11 @@ export class VideoUploadComponent implements OnInit, OnDestroy, AfterViewInit, C
this.firstStep = true
this.videoEdit = undefined
this.uploadingAudioFile = false
this.audioPreviewFile = undefined
this.audioThumbnailFile = undefined
}
uploadAudio () {
this.uploadFile(this.getInputVideoFile(), this.audioPreviewFile)
this.uploadFile(this.getInputVideoFile(), this.audioThumbnailFile)
}
getAudioUploadLabel () {
@@ -206,7 +206,7 @@ export class VideoUploadComponent implements OnInit, OnDestroy, AfterViewInit, C
return this.videoFileInput().nativeElement.files[0]
}
private uploadFile (file: File, previewfile?: File) {
private uploadFile (file: File, thumbnailfile?: File) {
const serverConfig = this.serverService.getHTMLConfig()
this.videoEdit = VideoEdit.createFromUpload(serverConfig, {
@@ -218,7 +218,7 @@ export class VideoUploadComponent implements OnInit, OnDestroy, AfterViewInit, C
this.manageController.setConfig({ manageType: 'upload', serverConfig: this.serverService.getHTMLConfig() })
this.manageController.setVideoEdit(this.videoEdit)
this.manageController.uploadNewVideo({ privacy: this.highestPrivacy(), file, previewfile })
this.manageController.uploadNewVideo({ privacy: this.highestPrivacy(), file, thumbnailfile })
this.manageController.silentRedirectOnUploading(this.route)
this.firstStep = false
@@ -164,7 +164,7 @@ export class ThumbnailManagerComponent implements OnInit, ControlValueAccessor {
const blob: Blob = this.dataURItoBlob(dataUrl)
const file = new File([ blob ], 'preview-file-from-frame.jpg', { type: 'image/jpeg' })
const file = new File([ blob ], 'thumbnail-file-from-frame.jpg', { type: 'image/jpeg' })
this.imageFile = file
@@ -13,20 +13,18 @@ import { resolveUrl, UploaderX } from 'ngx-uploadx'
* };
*/
export class UploaderXFormData extends UploaderX {
async getFileUrl (): Promise<string> {
const headers = {
'X-Upload-Content-Length': this.size.toString(),
'X-Upload-Content-Type': this.file.type || 'application/octet-stream'
}
const previewfile = this.metadata.previewfile as any as File
delete this.metadata.previewfile
const thumbnailfile = this.metadata.thumbnailfile as any as File
delete this.metadata.thumbnailfile
const data = objectToFormData(this.metadata)
if (previewfile !== undefined) {
data.append('previewfile', previewfile, previewfile.name)
data.append('thumbnailfile', previewfile, previewfile.name)
if (thumbnailfile !== undefined) {
data.append('thumbnailfile', thumbnailfile, thumbnailfile.name)
}
await this.request({
@@ -1,5 +1,5 @@
import { getOriginUrl } from '@app/helpers'
import { exists, omit, pick, secondsToTime } from '@peertube/peertube-core-utils'
import { AuthUser } from '@app/core'
import { exists, maxBy, omit, pick, secondsToTime } from '@peertube/peertube-core-utils'
import {
HTMLServerConfig,
LiveVideo,
@@ -28,7 +28,6 @@ import debug from 'debug'
import { Jsonify, SharedUnionFieldsDeep } from 'type-fest'
import { VideoCaptionWithPathEdit } from './video-caption-edit.model'
import { VideoChaptersEdit } from './video-chapters-edit.model'
import { AuthUser } from '@app/core'
const debugLogger = debug('peertube:video-manage:video-edit')
@@ -37,7 +36,7 @@ export type VideoEditPrivacyType = VideoPrivacyType | typeof VideoEdit.SPECIAL_S
type CommonUpdateForm =
& Omit<
VideoUpdate,
'privacy' | 'videoPasswords' | 'thumbnailfile' | 'scheduleUpdate' | 'originallyPublishedAt' | 'nsfwFlags'
'privacy' | 'videoPasswords' | 'previewfile' | 'scheduleUpdate' | 'originallyPublishedAt' | 'nsfwFlags'
>
& {
schedulePublicationAt?: Date
@@ -113,7 +112,7 @@ type UpdateFromAPIOptions = {
| 'aspectRatio'
| 'views'
| 'blacklisted'
| 'previewPath'
| 'thumbnails'
| 'state'
| 'isLive'
>
@@ -127,7 +126,7 @@ type UpdateFromAPIOptions = {
// ---------------------------------------------------------------------------
type CommonUpdate = Omit<VideoUpdate, 'thumbnailfile' | 'originallyPublishedAt' | 'scheduleUpdate'> & {
type CommonUpdate = Omit<VideoUpdate, 'previewfile' | 'originallyPublishedAt' | 'scheduleUpdate'> & {
originallyPublishedAt?: string
scheduleUpdate?: {
updateAt: string
@@ -196,8 +195,8 @@ export class VideoEdit {
}
private saveStore: {
common?: Omit<CommonUpdate, 'pluginData' | 'previewfile'>
previewfile?: { size: number }
common?: Omit<CommonUpdate, 'pluginData' | 'thumbnailfile'>
thumbnailfile?: { size: number }
live?: LiveUpdate
playerSettings?: PlayerVideoSettings
@@ -334,7 +333,7 @@ export class VideoEdit {
this.metadata.videoSource = videoSource
}
await this.loadPreview(video)
await this.loadThumbnail(video)
this.updateAfterChange()
}
@@ -396,7 +395,7 @@ export class VideoEdit {
if (saveInStore) {
const obj = buildObj({ loadPrivacy: true })
this.saveStore.common = omit(obj, [ 'pluginData', 'previewfile' ])
this.saveStore.common = omit(obj, [ 'pluginData', 'thumbnailfile' ])
// Apply plugin defaults so we correctly detect changes
const pluginDefaults = this.saveStore.pluginDefaults || {}
@@ -430,16 +429,18 @@ export class VideoEdit {
}
}
private async loadPreview (video: UpdateFromAPIOptions['video']) {
if (!video?.previewPath) return
private async loadThumbnail (video: UpdateFromAPIOptions['video']) {
if (!video?.thumbnails || video.thumbnails.length === 0) return
const bestThumbnail = maxBy(video.thumbnails, 'width')
try {
const response = await fetch(getOriginUrl() + video.previewPath)
const response = await fetch(bestThumbnail.fileUrl)
this.common.previewfile = await response.blob()
this.saveStore.previewfile = { size: this.common.previewfile.size }
this.common.thumbnailfile = await response.blob()
this.saveStore.thumbnailfile = { size: this.common.thumbnailfile.size }
} catch (err) {
logger.error('Failed to fetch video preview', err)
logger.error('Failed to fetch video thumbnail', err)
}
}
@@ -507,7 +508,7 @@ export class VideoEdit {
if (values.support !== undefined) this.common.support = values.support
if (values.commentsPolicy !== undefined) this.common.commentsPolicy = values.commentsPolicy
if (values.downloadEnabled !== undefined) this.common.downloadEnabled = values.downloadEnabled
if (values.previewfile !== undefined) this.common.previewfile = values.previewfile
if (values.thumbnailfile !== undefined) this.common.thumbnailfile = values.thumbnailfile
if (values.pluginData !== undefined) this.common.pluginData = values.pluginData
// ---------------------------------------------------------------------------
@@ -597,7 +598,7 @@ export class VideoEdit {
pluginData: this.common.pluginData,
previewfile: this.common.previewfile,
thumbnailfile: this.common.thumbnailfile,
videoPassword: this.common.videoPasswords && this.common.videoPasswords.length !== 0
? this.common.videoPasswords[0]
@@ -623,7 +624,7 @@ export class VideoEdit {
return json
}
toVideoUpdate (): Required<VideoUpdate> {
toVideoUpdate (): Required<Omit<VideoUpdate, 'previewfile'>> {
return {
...this.toVideoCreateOrUpdate(),
@@ -631,7 +632,7 @@ export class VideoEdit {
}
}
toVideoCreate (overriddenPrivacy: VideoPrivacyType): Required<Omit<VideoCreate, 'generateTranscription'>> {
toVideoCreate (overriddenPrivacy: VideoPrivacyType): Required<Omit<VideoCreate, 'generateTranscription' | 'previewfile'>> {
return {
...this.toVideoCreateOrUpdate(),
@@ -639,7 +640,7 @@ export class VideoEdit {
}
}
private toVideoCreateOrUpdate (): Required<SharedUnionFieldsDeep<VideoCreate | VideoUpdate>> {
private toVideoCreateOrUpdate (): Required<SharedUnionFieldsDeep<Omit<VideoCreate | VideoUpdate, 'previewfile'>>> {
return {
name: this.common.name,
category: this.common.category || null,
@@ -661,8 +662,7 @@ export class VideoEdit {
waitTranscoding: this.common.waitTranscoding,
commentsPolicy: this.common.commentsPolicy,
downloadEnabled: this.common.downloadEnabled,
thumbnailfile: this.common.previewfile,
previewfile: this.common.previewfile,
thumbnailfile: this.common.thumbnailfile,
scheduleUpdate: this.common.scheduleUpdate || null,
originallyPublishedAt: this.common.originallyPublishedAt || null
}
@@ -923,18 +923,18 @@ export class VideoEdit {
if (this.isNewVideo) return true
if (!this.saveStore.common) return true
let changes = !this.areSameObjects(omit(this.common, [ 'previewfile', 'pluginData' ]), this.saveStore.common)
let changes = !this.areSameObjects(omit(this.common, [ 'thumbnailfile', 'pluginData' ]), this.saveStore.common)
// Compare preview file
if (changes !== true && (this.common.previewfile || this.saveStore.previewfile)) {
changes = this.common.previewfile?.size !== this.saveStore.previewfile?.size
// Compare thumbnails
if (changes !== true && (this.common.thumbnailfile || this.saveStore.thumbnailfile)) {
changes = this.common.thumbnailfile?.size !== this.saveStore.thumbnailfile?.size
}
debugLogger('Check if has common changes', {
changes,
common: this.common,
saveCommon: this.saveStore.common,
savePreview: this.saveStore.previewfile
saveThumbnail: this.saveStore.thumbnailfile
})
return changes
@@ -111,9 +111,9 @@
</div>
<div class="form-group">
<label i18n for="previewfile">Thumbnail</label>
<label i18n for="thumbnailfile">Thumbnail</label>
<my-thumbnail-manager id="previewfile" formControlName="previewfile" [videoEdit]="videoEdit"></my-thumbnail-manager>
<my-thumbnail-manager id="thumbnailfile" formControlName="thumbnailfile" [videoEdit]="videoEdit"></my-thumbnail-manager>
</div>
<div class="form-group">
@@ -73,7 +73,7 @@ type Form = {
language: FormControl<string>
description: FormControl<string>
tags: FormArray<FormControl<string>>
previewfile: FormControl<Blob>
thumbnailfile: FormControl<Blob>
support: FormControl<string>
schedulePublicationAt: FormControl<Date>
pluginData: FormGroup
@@ -287,7 +287,7 @@ export class VideoMainInfoComponent implements OnInit, OnDestroy {
language: VIDEO_LANGUAGE_VALIDATOR,
description: VIDEO_DESCRIPTION_VALIDATOR,
tags: VIDEO_TAGS_ARRAY_VALIDATOR,
previewfile: null,
thumbnailfile: null,
support: VIDEO_SUPPORT_VALIDATOR,
schedulePublicationAt: VIDEO_SCHEDULE_PUBLICATION_AT_VALIDATOR
}
@@ -446,7 +446,7 @@ export class VideoManageController implements OnDestroy {
uploadNewVideo (options: {
file: File
privacy: VideoPrivacyType
previewfile?: File
thumbnailfile?: File
}) {
this.resetUploadState()
@@ -457,7 +457,7 @@ export class VideoManageController implements OnDestroy {
...this.videoEdit.toVideoCreate(options.privacy),
filename: options.file.name,
previewfile: options.previewfile
thumbnailfile: options.thumbnailfile
}
this.resumableUploadService.handleFiles(options.file, {
@@ -22,15 +22,6 @@ export const SERVICES_TWITTER_USERNAME_VALIDATOR: BuildFormValidator = {
}
}
export const CACHE_SIZE_VALIDATOR: BuildFormValidator = {
VALIDATORS: [ Validators.required, Validators.min(1), Validators.pattern('[0-9]+') ],
MESSAGES: {
required: $localize`Cache size is required.`,
min: $localize`Cache size must be greater than 1.`,
pattern: $localize`Cache size must be a number.`
}
}
export const SIGNUP_LIMIT_VALIDATOR: BuildFormValidator = {
VALIDATORS: [ Validators.required, Validators.min(-1), Validators.pattern('-?[0-9]+') ],
MESSAGES: {
@@ -3,8 +3,8 @@ import { Component, ElementRef, OnChanges, OnInit, booleanAttribute, inject, inp
import { RouterLink } from '@angular/router'
import { objectKeysTyped } from '@peertube/peertube-core-utils'
import { ActorImage } from '@peertube/peertube-models'
import { findAppropriateImageFileUrl } from '@root-helpers/images'
import { Account } from '../shared-main/account/account.model'
import { Actor } from '../shared-main/account/actor.model'
import { VideoChannel } from '../shared-main/channel/video-channel.model'
export type ActorAvatarInput = {
@@ -119,7 +119,7 @@ export class ActorAvatarComponent implements OnInit, OnChanges {
}
if (this.isAccount() || this.isChannel() || this.isInstance()) {
this.avatarUrl = Actor.GET_ACTOR_AVATAR_URL(actor, this.getSizeNumber())
this.avatarUrl = findAppropriateImageFileUrl(actor.avatars, this.getSizeNumber())
return
}
@@ -17,18 +17,6 @@ export abstract class Actor implements ServerActor {
isLocal: boolean
static GET_ACTOR_AVATAR_URL (actor: { avatars: Pick<ActorImage, 'width' | 'fileUrl'>[] }, size?: number) {
const avatarsAscWidth = actor.avatars.sort((a, b) => a.width - b.width)
const avatar = size && avatarsAscWidth.length > 1
? avatarsAscWidth.find(a => a.width >= size)
: avatarsAscWidth[avatarsAscWidth.length - 1] // Biggest one
if (!avatar) return ''
return avatar.fileUrl
}
static CREATE_BY_STRING (accountName: string, host: string, forceHostname = false) {
const thisHost = getBackendHost()
@@ -1,11 +1,12 @@
import { AuthUser } from '@app/core'
import { User } from '@app/core/users/user.model'
import { durationToString, getAPIUrl, getOriginUrl } from '@app/helpers'
import { durationToString, getOriginUrl } from '@app/helpers'
import { Actor } from '@app/shared/shared-main/account/actor.model'
import { buildVideoWatchPath, getAllFiles, peertubeTranslate } from '@peertube/peertube-core-utils'
import {
ActorImage,
HTMLServerConfig,
Thumbnail,
UserRight,
VideoConstant,
VideoFile,
@@ -49,17 +50,18 @@ export class Video implements VideoServerModel {
name: string
serverHost: string
thumbnailPath: string
thumbnailUrl: string
previewPath: string
previewUrl: string
thumbnails: Thumbnail[]
aspectRatio: number
isLive: boolean
liveSchedules: { startAt: Date | string }[]
previewPath: string
previewUrl: string
embedPath: string
embedUrl: string
@@ -153,6 +155,8 @@ export class Video implements VideoServerModel {
? hash.liveSchedules.map(schedule => ({ startAt: new Date(schedule.startAt.toString()) }))
: null
this.thumbnails = hash.thumbnails
this.duration = hash.duration
this.durationLabel = Video.buildDurationLabel(this)
@@ -163,16 +167,6 @@ export class Video implements VideoServerModel {
this.isLocal = hash.isLocal
this.name = hash.name
this.thumbnailPath = hash.thumbnailPath
this.thumbnailUrl = this.thumbnailPath
? hash.thumbnailUrl || (getAPIUrl() + hash.thumbnailPath)
: null
this.previewPath = hash.previewPath
this.previewUrl = this.previewPath
? hash.previewUrl || (getAPIUrl() + hash.previewPath)
: null
this.embedPath = hash.embedPath
this.embedUrl = hash.embedUrl || (getOriginUrl() + hash.embedPath)
@@ -3,8 +3,13 @@
@use "_mixins" as *;
@use "_miniature" as *;
@function getWidth($height) {
@return math.div(16, 9) * $height;
}
.root {
--co-image-height: 78px;
--co-image-width: #{getWidth(78px)};
display: flex;
align-items: center;
@@ -12,17 +17,19 @@
@media screen and (max-width: $mobile-view) {
--co-image-height: 64px;
--co-image-width: #{getWidth(64px)};
}
&.small {
--co-image-height: 45px;
--co-image-width: #{getWidth(45px)};
}
}
my-video-thumbnail {
display: block;
height: var(--co-image-height);
width: calc(#{math.div(16, 9)} * var(--co-image-height));
width: var(--co-image-width);
& + .name {
@include margin-left(0.5rem);
@@ -1,13 +1,12 @@
import { CommonModule } from '@angular/common'
import { booleanAttribute, Component, inject, input, OnChanges, output, viewChild } from '@angular/core'
import { booleanAttribute, Component, ElementRef, inject, input, OnChanges, output, viewChild } from '@angular/core'
import { RouterLink } from '@angular/router'
import { ScreenService } from '@app/core'
import { getAPIUrl } from '@app/helpers'
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'
import { Video as VideoServerModel, VideoState } from '@peertube/peertube-models'
import { findAppropriateImageFileUrl } from '@root-helpers/images'
import { GlobalIconComponent } from '../shared-icons/global-icon.component'
import { Video } from '../shared-main/video/video.model'
import { FromNowPipe } from '../shared-main/date/from-now.pipe'
import { Video } from '../shared-main/video/video.model'
export type VideoThumbnailInput = Pick<
VideoServerModel,
@@ -17,10 +16,7 @@ export type VideoThumbnailInput = Pick<
| 'shortUUID'
| 'isLive'
| 'state'
| 'previewPath'
| 'previewUrl'
| 'thumbnailPath'
| 'thumbnailUrl'
| 'thumbnails'
| 'userHistory'
| 'originallyPublishedAt'
| 'liveSchedules'
@@ -33,7 +29,7 @@ export type VideoThumbnailInput = Pick<
imports: [ CommonModule, RouterLink, NgbTooltip, GlobalIconComponent, FromNowPipe ]
})
export class VideoThumbnailComponent implements OnChanges {
private screenService = inject(ScreenService)
private el = inject(ElementRef)
readonly video = input.required<VideoThumbnailInput>()
@@ -103,11 +99,15 @@ export class VideoThumbnailComponent implements OnChanges {
const video = this.video()
if (!video) return ''
if (this.screenService.isInMobileView()) {
return video.previewUrl || getAPIUrl() + video.previewPath
}
const computedStyle = window.getComputedStyle(this.el.nativeElement)
return video.thumbnailUrl || getAPIUrl() + video.thumbnailPath
const cssVariable = computedStyle.getPropertyValue('--co-miniature-max-width') ||
computedStyle.getPropertyValue('--co-row-thumbnail-width') ||
computedStyle.getPropertyValue('--co-image-width')
const widthStr = cssVariable.replace('px', '').trim()
return findAppropriateImageFileUrl(video.thumbnails, +widthStr)
}
getProgressPercent () {
@@ -1,21 +1,26 @@
@use '_variables' as *;
@use '_mixins' as *;
@use '_miniature' as *;
@use "_variables" as *;
@use "_mixins" as *;
@use "_miniature" as *;
$thumbnail-width: 130px;
$thumbnail-height: 72px;
.video {
--co-miniature-width: #{$thumbnail-width};
--co-miniature-height: #{$thumbnail-height};
}
my-timestamp-input {
width: 100px;
}
my-video-thumbnail {
@include thumbnail-size-component($thumbnail-width, $thumbnail-height);
@include thumbnail-size-component(var(--co-miniature-width), var(--co-miniature-height));
}
.fake-thumbnail {
width: $thumbnail-width;
height: $thumbnail-height;
width: var(--co-miniature-width);
height: var(--co-miniature-height);
background-color: #ececec;
}
@@ -151,7 +156,6 @@ my-video-thumbnail,
}
.dropdown-menu {
.dropdown-item {
cursor: pointer;
@@ -3,7 +3,7 @@
[internalLink]="playlistRouterLink" [href]="playlistHref" [target]="playlistTarget" inheritParentStyle="true" inheritParentDimension="true"
[title]="playlist().displayName" class="miniature-thumbnail" tabindex="-1"
>
<img alt="" [attr.aria-label]="playlist().displayName" [attr.src]="playlist().thumbnailUrl" />
<img alt="" [attr.aria-label]="playlist().displayName" [attr.src]="getPlaylistThumbnailUrl()" />
<div class="miniature-playlist-info-overlay">
<ng-container i18n>{playlist().videosLength, plural, =0 {No videos} =1 {1 video} other {{{ playlist().videosLength }} videos}}</ng-container>
@@ -93,6 +93,7 @@
}
.miniature.display-as-row {
// Keep it sync with image size requested by the component
--co-row-thumbnail-width: #{$video-thumbnail-width};
--co-row-thumbnail-height: #{$video-thumbnail-height};
@@ -98,4 +98,9 @@ export class VideoPlaylistMiniatureComponent implements OnInit {
this.ownerRouterLink = [ '/search/lazy-load-channel', { url: playlist.videoChannel.url } ]
return
}
getPlaylistThumbnailUrl () {
// Keep it sync with image size requested by the SASS file
return this.playlist().getThumbnailUrl(280)
}
}
@@ -3,12 +3,14 @@ import { buildPlaylistWatchPath, peertubeTranslate } from '@peertube/peertube-co
import {
AccountSummary,
VideoPlaylist as ServerVideoPlaylist,
Thumbnail,
VideoChannelSummary,
VideoConstant,
VideoPlaylistPrivacyType,
VideoPlaylistType,
VideoPlaylistType_Type
} from '@peertube/peertube-models'
import { findAppropriateImageFileUrl } from '@root-helpers/images'
import { Actor } from '../shared-main/account/actor.model'
export class VideoPlaylist implements ServerVideoPlaylist {
@@ -36,6 +38,8 @@ export class VideoPlaylist implements ServerVideoPlaylist {
videoChannelPosition: number
videoChannel?: VideoChannelSummary
thumbnails: Thumbnail[]
thumbnailPath: string
thumbnailUrl: string
@@ -63,12 +67,6 @@ export class VideoPlaylist implements ServerVideoPlaylist {
this.description = hash.description
this.privacy = hash.privacy
this.thumbnailPath = hash.thumbnailPath
this.thumbnailUrl = this.thumbnailPath
? hash.thumbnailUrl || (getAPIUrl() + hash.thumbnailPath)
: getAPIUrl() + '/client/assets/images/default-playlist.jpg'
this.embedPath = hash.embedPath
this.embedUrl = hash.embedUrl || (getOriginUrl() + hash.embedPath)
@@ -95,4 +93,10 @@ export class VideoPlaylist implements ServerVideoPlaylist {
this.displayName = peertubeTranslate(this.displayName, translations)
}
}
getThumbnailUrl (width: number) {
const defaultUrl = getAPIUrl() + '/client/assets/images/default-playlist.jpg'
return findAppropriateImageFileUrl(this.thumbnails, width) || defaultUrl
}
}
+4 -3
View File
@@ -1,6 +1,7 @@
import { findAppropriateImage } from '@peertube/peertube-core-utils'
import { logger } from './logger'
function imageToDataURL (input: File | Blob) {
export function imageToDataURL (input: File | Blob) {
return new Promise<string>(res => {
const reader = new FileReader()
@@ -10,6 +11,6 @@ function imageToDataURL (input: File | Blob) {
})
}
export {
imageToDataURL
export function findAppropriateImageFileUrl<T extends { width: number, fileUrl: string }> (images: T[], width: number) {
return findAppropriateImage(images, width)?.fileUrl || ''
}
@@ -1,4 +1,5 @@
import { buildVideoLink, decorateVideoLink, isDefaultLocale, pick } from '@peertube/peertube-core-utils'
import { buildVideoLink, decorateVideoLink, findAppropriateImage, isDefaultLocale, pick } from '@peertube/peertube-core-utils'
import { Thumbnail } from '@peertube/peertube-models'
import { logger } from '@root-helpers/logger'
import { PluginsManager } from '@root-helpers/plugins-manager'
import { TranslationsManager } from '@root-helpers/translations-manager'
@@ -101,7 +102,7 @@ export class PeerTubePlayer {
async load (loadOptions: PeerTubePlayerLoadOptions) {
this.currentLoadOptions = loadOptions
this.setPoster('')
this.setPoster([])
this.disposeDynamicPluginsIfNeeded()
@@ -130,7 +131,7 @@ export class PeerTubePlayer {
this.player.autoplay(this.getAutoPlayValue(this.currentLoadOptions.autoplay))
if (!this.player.autoplay()) {
this.setPoster(loadOptions.poster)
this.setPoster(loadOptions.thumbnails)
}
this.player.trigger('video-change')
@@ -144,15 +145,20 @@ export class PeerTubePlayer {
if (this.player) this.player.dispose()
}
setPoster (url: string) {
setPoster (thumbnails: Thumbnail[]) {
// Use HTML video element to display poster
if (!this.player) {
this.options.playerElement().poster = url
const playerEl = this.options.playerElement()
this.options.playerElement().poster = findAppropriateImage(thumbnails, playerEl.clientWidth || window.innerWidth)?.fileUrl || ''
return
}
// Prefer using player poster API
this.player?.poster(url)
if (this.player) {
this.player.poster(findAppropriateImage(thumbnails, this.player.currentWidth())?.fileUrl || '')
}
this.options.playerElement().poster = ''
}
@@ -384,7 +390,10 @@ export class PeerTubePlayer {
})
}
getVideojsOptions (): VideojsPlayerOptions {
private getVideojsOptions (): VideojsPlayerOptions {
const poster =
findAppropriateImage(this.currentLoadOptions.thumbnails, this.options.playerElement().clientWidth || window.innerWidth)?.fileUrl || ''
const html5 = {
preloadTextTracks: false,
// Prevent a bug on iOS where the text tracks added by peertube plugin are removed on play
@@ -413,7 +422,7 @@ export class PeerTubePlayer {
videoRatio: () => this.currentLoadOptions.videoRatio,
poster: () => this.currentLoadOptions.poster,
poster: () => poster,
autoPlayerRatio: this.options.autoPlayerRatio
},
@@ -450,7 +459,7 @@ export class PeerTubePlayer {
autoplay: this.getAutoPlayValue(this.currentLoadOptions.autoplay),
poster: this.currentLoadOptions.poster,
poster,
preload: 'none' as 'none',
inactivityTimeout: this.options.inactivityTimeout,
@@ -161,7 +161,7 @@ $playlist-menu-width: 350px;
}
img {
width: 80px;
width: 80px; // Keep in sync with TS in "playlist-menu-item.ts"
height: 40px;
}
@@ -1,4 +1,4 @@
import { secondsToTime } from '@peertube/peertube-core-utils'
import { findAppropriateImage, secondsToTime } from '@peertube/peertube-core-utils'
import { VideoPlaylistElement } from '@peertube/peertube-models'
import videojs from 'video.js'
import { PlaylistItemOptions, VideojsComponent, VideojsComponentOptions, VideojsPlayer } from '../../types'
@@ -83,7 +83,9 @@ class PlaylistMenuItem extends Component {
positionBlock.appendChild(player)
const thumbnail = super.createEl('img', {
src: window.location.origin + videoElement.video.thumbnailPath
src: videoElement.video.thumbnails.length !== 0
? window.location.origin + findAppropriateImage(videoElement.video.thumbnails, 80).fileUrl // Keep 80 in sync with CSS
: ''
})
const infoBlock = super.createEl('div', {
@@ -1,4 +1,4 @@
import { LiveVideoLatencyModeType, PlayerMode, PlayerTheme, VideoChapter, VideoFile } from '@peertube/peertube-models'
import { LiveVideoLatencyModeType, PlayerMode, PlayerTheme, Thumbnail, VideoChapter, VideoFile } from '@peertube/peertube-models'
import { PluginsManager } from '@root-helpers/plugins-manager'
import { PeerTubeDockPluginOptions } from '../shared/dock/peertube-dock-plugin'
import { PlaylistPluginOptions, VideoJSCaption, VideojsPlayer, VideoJSStoryboard } from './peertube-videojs-typings'
@@ -58,7 +58,8 @@ export type PeerTubePlayerLoadOptions = {
autoplay: boolean
forceAutoplay: boolean
poster: string
thumbnails: Thumbnail[]
subtitle?: string
videoViewUrl: string
+1 -1
View File
@@ -399,7 +399,7 @@ export class PeerTubeEmbed {
this.peertubePlayer.unload()
this.peertubePlayer.disable()
this.peertubePlayer.setPoster(video.previewPath)
this.peertubePlayer.setPoster(video.thumbnails)
}
private async handlePasswordError (err: PeerTubeServerError) {
@@ -318,9 +318,9 @@ export class PlayerOptionsBuilder {
}
: undefined,
poster: nsfwBlur
thumbnails: nsfwBlur
? null
: getBackendUrl() + video.previewPath,
: video.thumbnails,
duration: video.duration,
videoRatio: video.aspectRatio,
-13
View File
@@ -481,9 +481,6 @@ thumbnails:
# Minimum value is 2
frames_to_analyze: 50
# Only two sizes are currently supported for now (not less, not more)
# 1 size for the thumbnail (displayed in video miniatures)
# 1 size for the preview (displayed in the video player)
sizes:
-
width: 280
@@ -528,16 +525,6 @@ download_generate_video:
# The ffmpeg process ends when users have downloaded the entire file or cancelled the download
max_parallel_downloads: 100
cache:
previews:
size: 500 # Max number of previews you want to cache
captions:
size: 500 # Max number of video captions/subtitles you want to cache
torrents:
size: 500 # Max number of video torrents you want to cache
storyboards:
size: 500 # Max number of video storyboards you want to cache
admin:
# Used to generate the root user at first startup
# And to receive emails from the contact form
-13
View File
@@ -479,9 +479,6 @@ thumbnails:
# Minimum value is 2
frames_to_analyze: 50
# Only two sizes are currently supported for now (not less, not more)
# 1 size for the thumbnail (displayed in video miniatures)
# 1 size for the preview (displayed in the video player)
sizes:
-
width: 280
@@ -538,16 +535,6 @@ download_generate_video:
#
###############################################################################
cache:
previews:
size: 500 # Max number of previews you want to cache
captions:
size: 500 # Max number of video captions/subtitles you want to cache
torrents:
size: 500 # Max number of video torrents you want to cache
storyboards:
size: 500 # Max number of video storyboards you want to cache
admin:
# Used to generate the root user at first startup
# And to receive emails from the contact form
+2 -6
View File
@@ -79,8 +79,7 @@
"test": "bash ./scripts/test.sh",
"tsc": "tsc",
"tsx": "tsx",
"update-host": "node ./dist/scripts/update-host.js",
"update-object-storage-url": "LOGGER_LEVEL=warn node ./dist/scripts/update-object-storage-url.js"
"update-host": "node ./dist/scripts/update-host.js"
},
"resolutions": {
"@types/express": "4.17.9",
@@ -94,7 +93,6 @@
"@aws-sdk/lib-storage": "^3.930.0",
"@aws-sdk/s3-request-presigner": "^3.930.0",
"@commander-js/extra-typings": "^14.0.0",
"@jimp/plugin-color": "^1.6.0",
"@misskey-dev/node-http-message-signatures": "^0.0.10",
"@node-oauth/oauth2-server": "^5.2.1",
"@opentelemetry/api": "^1.9.0",
@@ -160,7 +158,6 @@
"ip-anonymize": "^0.1.0",
"ipaddr.js": "2.2.0",
"iso-639-3": "3.0.1",
"jimp": "^0.22.12",
"js-yaml": "^4.1.1",
"jsonld": "~8.3.3",
"jsonwebtoken": "^9.0.3",
@@ -191,6 +188,7 @@
"sanitize-html": "~2.17.0",
"sequelize": "~6.37.7",
"sequelize-typescript": "^2.1.6",
"sharp": "^0.34.5",
"short-uuid": "^5.2.0",
"sitemap": "^9.0.0",
"socket.io": "^4.8.1",
@@ -251,12 +249,10 @@
"eslint-config-love": "^133.0.0",
"fast-xml-parser": "^5.3.1",
"i18next-parser": "^9.3.0",
"jpeg-js": "^0.4.4",
"jszip": "^3.10.1",
"maildev": "^2.2.1",
"mocha": "^11.7.5",
"pixelmatch": "^7.1.0",
"pngjs": "^7.0.0",
"proxy": "^2.2.0",
"rollup": "^4.53.2",
"rollup-plugin-dts": "^6.2.3",
+1 -2
View File
@@ -37,7 +37,6 @@ export function shuffle<T> (elements: T[]) {
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
;[ shuffled[i], shuffled[j] ] = [ shuffled[j], shuffled[i] ]
}
@@ -45,7 +44,7 @@ export function shuffle<T> (elements: T[]) {
}
export function sortBy<T> (obj: T[], key1: string, key2?: string): T[] {
return obj.sort((a, b) => {
return [ ...obj ].sort((a, b) => {
const elem1 = key2 ? a[key1][key2] : a[key1]
const elem2 = key2 ? b[key1][key2] : b[key1]
+7 -6
View File
@@ -1,13 +1,14 @@
export function findAppropriateImage<T extends { width: number, height: number }> (images: T[], wantedWidth: number) {
export function findAppropriateImage<T extends { width: number }> (images: T[], wantedWidth: number) {
if (!wantedWidth) throw new Error('Invalid width to find appropriate image')
if (!images || images.length === 0) return undefined
const imagesSorted = images.sort((a, b) => a.width - b.width)
let candidate: T
for (const image of imagesSorted) {
if (image.width >= wantedWidth) {
return image
for (const img of images) {
if (img.width >= wantedWidth && (!candidate || img.width < candidate.width)) {
candidate = img
}
}
return images[images.length - 1] // Biggest one
return candidate || images[0]
}
-16
View File
@@ -10,22 +10,6 @@ export class FFmpegImage {
this.commandWrapper = new FFmpegCommandWrapper(options)
}
processImage (options: {
path: string
destination: string
newSize?: { width: number, height: number }
}): Promise<void> {
const { path, destination, newSize } = options
const command = this.commandWrapper.buildCommand(path)
if (newSize) command.size(`${newSize.width ?? '?'}x${newSize.height ?? '?'}`)
command.output(destination)
return this.commandWrapper.runCommand({ silent: true })
}
// ---------------------------------------------------------------------------
async generateThumbnailFromVideo (options: {
@@ -1,4 +1,4 @@
import { UserActorImageJSON } from './actor-export.model.js'
import { ImageExportJSON } from './image-export.model.js'
export interface AccountExportJSON {
url: string
@@ -10,7 +10,7 @@ export interface AccountExportJSON {
updatedAt: string
createdAt: string
avatars: UserActorImageJSON[]
avatars: ImageExportJSON[]
archiveFiles: {
avatar: string | null
@@ -1,5 +1,5 @@
import { PlayerThemeChannelSetting } from '../../player/player-theme.type.js'
import { UserActorImageJSON } from './actor-export.model.js'
import { ImageExportJSON } from './image-export.model.js'
export interface ChannelExportJSON {
channels: {
@@ -13,8 +13,8 @@ export interface ChannelExportJSON {
updatedAt: string
createdAt: string
avatars: UserActorImageJSON[]
banners: UserActorImageJSON[]
avatars: ImageExportJSON[]
banners: ImageExportJSON[]
playerSettings?: {
theme: PlayerThemeChannelSetting
@@ -1,5 +1,6 @@
export interface UserActorImageJSON {
export interface ImageExportJSON {
width: number
height: number
url: string
createdAt: string
updatedAt: string
@@ -1,5 +1,4 @@
export * from './account-export.model.js'
export * from './actor-export.model.js'
export * from './auto-tag-policies-export.js'
export * from './blocklist-export.model.js'
export * from './channel-export.model.js'
@@ -7,6 +6,7 @@ export * from './comments-export.model.js'
export * from './dislikes-export.model.js'
export * from './followers-export.model.js'
export * from './following-export.model.js'
export * from './image-export.model.js'
export * from './likes-export.model.js'
export * from './user-settings-export.model.js'
export * from './user-video-history-export.js'
@@ -7,6 +7,7 @@ import {
VideoStateType,
VideoStreamingPlaylistType_Type
} from '../../videos/index.js'
import { ImageExportJSON } from './image-export.model.js'
export interface VideoExportJSON {
videos: {
@@ -51,6 +52,7 @@ export interface VideoExportJSON {
thumbnailUrl: string
previewUrl: string
thumbnails: ImageExportJSON[]
views: number
@@ -11,7 +11,6 @@ export interface UserExport {
// In bytes
size: number
fileUrl: string
privateDownloadUrl: string
createdAt: string | Date
@@ -1,8 +1,9 @@
import { Account } from '../../actors/account.model.js'
import { AbuseStateType } from './abuse-state.model.js'
import { AbusePredefinedReasonsString } from './abuse-reason.model.js'
import { VideoConstant } from '../../videos/video-constant.model.js'
import { VideoChannel } from '../../videos/channel/video-channel.model.js'
import { Thumbnail } from '../../videos/index.js'
import { VideoConstant } from '../../videos/video-constant.model.js'
import { AbusePredefinedReasonsString } from './abuse-reason.model.js'
import { AbuseStateType } from './abuse-state.model.js'
export interface AdminVideoAbuse {
id: number
@@ -17,7 +18,13 @@ export interface AdminVideoAbuse {
startAt: number | null
endAt: number | null
/**
* @deprecated use thumbnails instead
*/
thumbnailPath?: string
thumbnails: Thumbnail[]
channel?: VideoChannel
countReports: number
@@ -106,24 +106,6 @@ export interface CustomConfig {
}
}
cache: {
previews: {
size: number
}
captions: {
size: number
}
torrents: {
size: number
}
storyboards: {
size: number
}
}
signup: {
enabled: boolean
limit: number
+1 -1
View File
@@ -18,7 +18,7 @@ export * from './nsfw-flag.enum.js'
export * from './nsfw-policy.type.js'
export * from './storyboard.model.js'
export * from './thumbnail.type.js'
export * from './thumbnail/index.js'
export * from './video-constant.model.js'
export * from './video-create.model.js'
@@ -1,5 +1,6 @@
import { AccountSummary } from '../../actors/index.js'
import { VideoChannelSummary } from '../channel/index.js'
import { Thumbnail } from '../thumbnail/thumbnail.model.js'
import { VideoConstant } from '../video-constant.model.js'
import { VideoPlaylistPrivacyType } from './video-playlist-privacy.model.js'
import { VideoPlaylistType_Type } from './video-playlist-type.model.js'
@@ -17,9 +18,17 @@ export interface VideoPlaylist {
description: string
privacy: VideoConstant<VideoPlaylistPrivacyType>
/**
* @deprecated in 8.1, use thumbnails array instead
*/
thumbnailPath: string
/**
* @deprecated in 8.1, use thumbnails array instead
*/
thumbnailUrl?: string
thumbnails: Thumbnail[]
videosLength: number
type: VideoConstant<VideoPlaylistType_Type>
@@ -1,6 +0,0 @@
export const ThumbnailType = {
MINIATURE: 1,
PREVIEW: 2
} as const
export type ThumbnailType_Type = typeof ThumbnailType[keyof typeof ThumbnailType]
@@ -0,0 +1 @@
export * from './thumbnail.model.js'
@@ -0,0 +1,6 @@
export interface Thumbnail {
height: number
width: number
fileUrl: string
}
@@ -23,6 +23,7 @@ export interface VideoCreateUpdateCommon {
waitTranscoding?: boolean
channelId?: number
thumbnailfile?: Blob
// TODO: remove in v10, deprecated in 8.1
previewfile?: Blob
scheduleUpdate?: VideoScheduleUpdate
originallyPublishedAt?: Date | string
@@ -10,7 +10,6 @@ export interface VideoSource {
width?: number
height?: number
fileUrl: string
fileDownloadUrl: string
fps?: number
@@ -3,3 +3,5 @@ export const VideoStreamingPlaylistType = {
} as const
export type VideoStreamingPlaylistType_Type = typeof VideoStreamingPlaylistType[keyof typeof VideoStreamingPlaylistType]
export type VideoStreamingPlaylistTypeString = 'hls' | 'unknown'
+18 -1
View File
@@ -1,8 +1,9 @@
import { Account, AccountSummary } from '../actors/index.js'
import { VideoChannel, VideoChannelSummary } from './channel/video-channel.model.js'
import { VideoCommentPolicyType } from './comment/video-comment-policy.enum.js'
import { VideoFile } from './file/index.js'
import { VideoCommentPolicyType } from './index.js'
import { LiveVideoScheduleEdit } from './live/live-video-schedule.model.js'
import { Thumbnail } from './thumbnail/thumbnail.model.js'
import { VideoConstant } from './video-constant.model.js'
import { VideoPrivacyType } from './video-privacy.enum.js'
import { VideoScheduleUpdate } from './video-schedule-update.model.js'
@@ -37,12 +38,28 @@ export interface Video extends Partial<VideoAdditionalAttributes> {
isLive: boolean
liveSchedules?: LiveVideoScheduleEdit[]
/**
* @deprecated in 8.1, use thumbnails array instead
*/
thumbnailPath: string
/**
* @deprecated in 8.1, use thumbnails array instead
*/
thumbnailUrl?: string
/**
* @deprecated in 8.1, use thumbnails array instead
*/
previewPath: string
/**
* @deprecated in 8.1, use thumbnails array instead
*/
previewUrl?: string
thumbnails: Thumbnail[]
embedPath: string
embedUrl?: string
@@ -108,7 +108,6 @@ export class LiveCommand extends AbstractCommand {
options: OverrideCommandOptions & {
fields: Omit<LiveVideoCreate, 'channelId' | 'thumbnailfile' | 'previewfile'> & {
thumbnailfile?: string | Blob
previewfile?: string | Blob
channelId?: number
}
}
@@ -125,7 +124,6 @@ export class LiveCommand extends AbstractCommand {
const attaches: any = {}
if (fields.thumbnailfile) attaches.thumbnailfile = fields.thumbnailfile
if (fields.previewfile) attaches.previewfile = fields.previewfile
const body = await unwrapBody<{ video: VideoCreateResult }>(this.postUploadRequest({
...options,
@@ -135,7 +133,7 @@ export class LiveCommand extends AbstractCommand {
fields: {
channelId: defaultChannelId,
...omit(fields, [ 'thumbnailfile', 'previewfile' ])
...omit(fields, [ 'thumbnailfile' ])
},
implicitToken: true,
defaultExpectedStatus: HttpStatusCode.OK_200
@@ -5,7 +5,7 @@ import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
export class VideoImportsCommand extends AbstractCommand {
async importVideo (
options: OverrideCommandOptions & {
attributes: Partial<VideoImportCreate> | { torrentfile?: string, previewfile?: string, thumbnailfile?: string }
attributes: Partial<VideoImportCreate> | { torrentfile?: string, thumbnailfile?: string }
}
) {
const { attributes } = options
@@ -21,7 +21,6 @@ export class VideoImportsCommand extends AbstractCommand {
let attaches: any = {}
if (attributes.torrentfile) attaches = { torrentfile: attributes.torrentfile }
if (attributes.thumbnailfile) attaches = { thumbnailfile: attributes.thumbnailfile }
if (attributes.previewfile) attaches = { previewfile: attributes.previewfile }
return unwrapBody<VideoImport>(this.postUploadRequest({
...options,
@@ -28,6 +28,7 @@ import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
export type VideoEdit = Partial<Omit<VideoCreate, 'thumbnailfile' | 'previewfile'>> & {
fixture?: string
thumbnailfile?: string
// TODO: remove when previewfile is deleted from the server
previewfile?: string
}
@@ -371,10 +372,9 @@ export class VideosCommand extends AbstractCommand {
const path = '/api/v1/videos/' + id
// Upload request
if (attributes.thumbnailfile || attributes.previewfile) {
if (attributes.thumbnailfile) {
const attaches: any = {}
if (attributes.thumbnailfile) attaches.thumbnailfile = attributes.thumbnailfile
if (attributes.previewfile) attaches.previewfile = attributes.previewfile
return this.putUploadRequest({
...options,
@@ -382,8 +382,7 @@ export class VideosCommand extends AbstractCommand {
path,
fields: options.attributes,
attaches: {
thumbnailfile: attributes.thumbnailfile,
previewfile: attributes.previewfile
thumbnailfile: attributes.thumbnailfile
},
implicitToken: true,
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
@@ -673,16 +672,14 @@ export class VideosCommand extends AbstractCommand {
}
buildUploadFields (attributes: VideoEdit) {
return omit(attributes, [ 'fixture', 'thumbnailfile', 'previewfile' ])
return omit(attributes, [ 'fixture', 'thumbnailfile' ])
}
buildUploadAttaches (attributes: VideoEdit, includeFixture: boolean) {
const attaches: { [name: string]: string } = {}
for (const key of [ 'thumbnailfile', 'previewfile' ]) {
if (attributes[key]) attaches[key] = buildAbsoluteFixturePath(attributes[key])
}
if (attributes.thumbnailfile) attaches.thumbnailfile = buildAbsoluteFixturePath(attributes.thumbnailfile)
if (attributes.previewfile) attaches.previewfile = buildAbsoluteFixturePath(attributes.previewfile)
if (includeFixture && attributes.fixture) attaches.videofile = buildAbsoluteFixturePath(attributes.fixture)
return attaches
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

@@ -387,7 +387,7 @@ describe('Test managing runners', function () {
const { jobUUID, expectedStatus, videoUUID, runnerToken, jobToken } = options
const basePath = '/api/v1/runners/jobs/' + jobUUID + '/files/videos/' + videoUUID
const paths = [ `${basePath}/max-quality`, `${basePath}/previews/max-quality` ]
const paths = [ `${basePath}/max-quality`, `${basePath}/thumbnails/max-quality` ]
for (const path of paths) {
await makePostBodyRequest({ url: server.url, path, fields: { runnerToken, jobToken }, expectedStatus })
@@ -810,7 +810,7 @@ describe('Test managing runners', function () {
})
it('Should fail with an invalid vod audio merge payload', async function () {
const attributes = { name: 'audio_with_preview', previewfile: 'custom-preview.jpg', fixture: 'sample.ogg' }
const attributes = { name: 'audio_with_preview', thumbnailfile: 'custom-thumbnail-big.jpg', fixture: 'sample.ogg' }
await server.videos.upload({ attributes, mode: 'legacy' })
await waitJobs([ server ])
@@ -258,7 +258,7 @@ describe('Test video playlists API validator', function () {
attributes: {
displayName: 'display name',
privacy: VideoPlaylistPrivacy.UNLISTED,
thumbnailfile: 'custom-thumbnail.jpg',
thumbnailfile: 'custom-thumbnail-280x157.jpg',
videoChannelId: server.store.channel.id,
...attributes
@@ -293,7 +293,7 @@ describe('Test video studio API validator', function () {
it('Should succeed with the correct params', async function () {
this.timeout(360000)
await addWatermark('custom-thumbnail.jpg', HttpStatusCode.NO_CONTENT_204)
await addWatermark('custom-thumbnail-280x157.jpg', HttpStatusCode.NO_CONTENT_204)
await waitJobs([ server ])
})
@@ -325,8 +325,8 @@ describe('Test video studio API validator', function () {
})
it('Should fail with an invalid file', async function () {
await addIntroOutro('add-intro', 'custom-thumbnail.jpg')
await addIntroOutro('add-outro', 'custom-thumbnail.jpg')
await addIntroOutro('add-intro', 'custom-thumbnail-280x157.jpg')
await addIntroOutro('add-outro', 'custom-thumbnail-280x157.jpg')
})
it('Should fail with a file that does not contain video stream', async function () {
@@ -486,7 +486,7 @@ describe('Test videos API validator', function () {
it('Should fail with an incorrect thumbnail file', async function () {
const fields = baseCorrectParams
const attaches = {
thumbnailfile: buildAbsoluteFixturePath('video_short.mp4'),
thumbnailfile: buildAbsoluteFixturePath('video-720p.torrent'),
fixture: buildAbsoluteFixturePath('video_short.mp4')
}
@@ -506,7 +506,7 @@ describe('Test videos API validator', function () {
it('Should fail with an incorrect preview file', async function () {
const fields = baseCorrectParams
const attaches = {
previewfile: buildAbsoluteFixturePath('video_short.mp4'),
previewfile: buildAbsoluteFixturePath('video-720p.torrent'),
fixture: buildAbsoluteFixturePath('video_short.mp4')
}
+18 -23
View File
@@ -26,8 +26,8 @@ import {
waitUntilLiveReplacedByReplayOnAllServers,
waitUntilLiveWaitingOnAllServers
} from '@peertube/peertube-server-commands'
import { testImageGeneratedByFFmpeg } from '@tests/shared/checks.js'
import { checkLiveCleanup } from '@tests/shared/live.js'
import { checkThumbnails as _checkVideoThumbnails } from '@tests/shared/videos.js'
import { expect } from 'chai'
import { FfmpegCommand } from 'fluent-ffmpeg'
@@ -41,7 +41,6 @@ describe('Save replay setting', function () {
replay: boolean
replaySettings?: { privacy: VideoPrivacyType }
thumbnailfile?: string
previewfile?: string
}) {
if (liveVideoUUID) {
try {
@@ -59,8 +58,7 @@ describe('Save replay setting', function () {
saveReplay: options.replay,
replaySettings: options.replaySettings,
permanentLive: options.permanent,
thumbnailfile: options.thumbnailfile,
previewfile: options.previewfile
thumbnailfile: options.thumbnailfile
}
})
return uuid
@@ -150,12 +148,11 @@ describe('Save replay setting', function () {
}
}
async function checkVideoThumbnail (videoId: string, thumbnailfile: string, previewfile?: string) {
async function checkVideoThumbnails (videoId: string, thumbnails: string[]) {
for (const server of servers) {
const video = await server.videos.get({ id: videoId })
await testImageGeneratedByFFmpeg(server.url, thumbnailfile, video.thumbnailPath, '')
if (previewfile) await testImageGeneratedByFFmpeg(server.url, previewfile, video.previewPath, '')
await _checkVideoThumbnails({ video, server, thumbnails, strict: true })
}
}
@@ -301,7 +298,7 @@ describe('Save replay setting', function () {
await checkVideosExist(liveVideoUUID, 0, HttpStatusCode.OK_200)
await checkVideoState(liveVideoUUID, VideoState.WAITING_FOR_LIVE)
await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.PUBLIC)
await checkVideoThumbnail(liveVideoUUID, 'default-live-thumbnail.jpg', 'default-live-preview.jpg')
await checkVideoThumbnails(liveVideoUUID, [ 'default-live-thumbnail-280x157.jpg', 'default-live-thumbnail-850x480.jpg' ])
})
it('Should correctly have updated the live and federated it when streaming in the live', async function () {
@@ -315,7 +312,7 @@ describe('Save replay setting', function () {
await checkVideosExist(liveVideoUUID, 1, HttpStatusCode.OK_200)
await checkVideoState(liveVideoUUID, VideoState.PUBLISHED)
await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.PUBLIC)
await checkVideoThumbnail(liveVideoUUID, 'default-live-thumbnail.jpg', 'default-live-preview.jpg')
await checkVideoThumbnails(liveVideoUUID, [ 'default-live-thumbnail-280x157.jpg', 'default-live-thumbnail-850x480.jpg' ])
})
it('Should correctly have saved the live and federated it after the streaming', async function () {
@@ -368,8 +365,7 @@ describe('Save replay setting', function () {
attributes: {
name: 'video updated',
privacy: VideoPrivacy.PUBLIC,
thumbnailfile: 'custom-thumbnail.jpg',
previewfile: 'custom-preview.jpg'
thumbnailfile: 'custom-thumbnail-850x480.jpg'
}
})
await waitJobs(servers)
@@ -380,7 +376,7 @@ describe('Save replay setting', function () {
expect(video.isLive).to.be.false
expect(video.privacy.id).to.equal(VideoPrivacy.PUBLIC)
await checkVideoThumbnail(liveVideoUUID, 'custom-thumbnail.jpg', 'custom-preview.jpg')
await checkVideoThumbnails(liveVideoUUID, [ 'custom-thumbnail-280x157.jpg', 'custom-thumbnail-850x480.jpg' ])
}
})
@@ -437,8 +433,7 @@ describe('Save replay setting', function () {
permanent: true,
replay: true,
replaySettings: { privacy: VideoPrivacy.UNLISTED },
thumbnailfile: 'custom-thumbnail.jpg',
previewfile: 'custom-preview.jpg'
thumbnailfile: 'custom-thumbnail-850x480.jpg'
})
await waitJobs(servers)
@@ -446,7 +441,7 @@ describe('Save replay setting', function () {
await checkVideosExist(liveVideoUUID, 0, HttpStatusCode.OK_200)
await checkVideoState(liveVideoUUID, VideoState.WAITING_FOR_LIVE)
await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.PUBLIC)
await checkVideoThumbnail(liveVideoUUID, 'custom-thumbnail.jpg', 'custom-preview.jpg')
await checkVideoThumbnails(liveVideoUUID, [ 'custom-thumbnail-280x157.jpg', 'custom-thumbnail-850x480.jpg' ])
})
it('Should correctly have updated the live and federated it when streaming in the live', async function () {
@@ -518,15 +513,15 @@ describe('Save replay setting', function () {
await checkVideosExist(lastReplayUUID, 1, HttpStatusCode.OK_200)
await checkVideoState(lastReplayUUID, VideoState.PUBLISHED)
await checkVideoPrivacy(lastReplayUUID, VideoPrivacy.PUBLIC)
await checkVideoThumbnail(lastReplayUUID, 'custom-thumbnail-from-preview.jpg', 'custom-preview.jpg')
await checkVideoThumbnails(liveVideoUUID, [ 'custom-thumbnail-280x157.jpg', 'custom-thumbnail-850x480.jpg' ])
})
it('Should update the live replay thumbnail', async function () {
await servers[0].videos.update({ id: lastReplayUUID, attributes: { thumbnailfile: 'custom-thumbnail-2.jpg' } })
await servers[0].videos.update({ id: lastReplayUUID, attributes: { thumbnailfile: 'custom-thumbnail-2-850x480.jpg' } })
await waitJobs(servers)
await checkVideoThumbnail(liveVideoUUID, 'custom-thumbnail.jpg', 'custom-preview.jpg')
await checkVideoThumbnail(lastReplayUUID, 'custom-thumbnail-2.jpg')
await checkVideoThumbnails(liveVideoUUID, [ 'custom-thumbnail-280x157.jpg', 'custom-thumbnail-850x480.jpg' ])
await checkVideoThumbnails(lastReplayUUID, [ 'custom-thumbnail-2-280x157.jpg', 'custom-thumbnail-2-850x480.jpg' ])
})
})
@@ -676,18 +671,18 @@ describe('Save replay setting', function () {
const video = await findExternalSavedVideo(servers[0], liveVideoUUID)
lastReplayUUID = video.uuid
await checkVideoThumbnail(liveVideoUUID, 'default-live-thumbnail.jpg', 'default-live-preview.jpg')
await checkVideoThumbnails(liveVideoUUID, [ 'default-live-thumbnail-280x157.jpg', 'default-live-thumbnail-850x480.jpg' ])
})
it('Should update the live replay thumbnail', async function () {
await servers[0].videos.update({
id: lastReplayUUID,
attributes: { thumbnailfile: 'custom-thumbnail.jpg', previewfile: 'custom-preview.jpg' }
attributes: { thumbnailfile: 'custom-thumbnail-850x480.jpg' }
})
await waitJobs(servers)
await checkVideoThumbnail(liveVideoUUID, 'default-live-thumbnail.jpg', 'default-live-preview.jpg')
await checkVideoThumbnail(lastReplayUUID, 'custom-thumbnail.jpg', 'custom-preview.jpg')
await checkVideoThumbnails(liveVideoUUID, [ 'default-live-thumbnail-280x157.jpg', 'default-live-thumbnail-850x480.jpg' ])
await checkVideoThumbnails(lastReplayUUID, [ 'custom-thumbnail-280x157.jpg', 'custom-thumbnail-850x480.jpg' ])
})
})
})
+10 -8
View File
@@ -19,7 +19,6 @@ import {
doubleFollow,
killallServers,
LiveCommand,
makeGetRequest,
makeRawRequest,
PeerTubeServer,
sendRTMPStream,
@@ -30,9 +29,9 @@ import {
waitJobs,
waitUntilLivePublishedOnAllServers
} from '@peertube/peertube-server-commands'
import { testImageGeneratedByFFmpeg } from '@tests/shared/checks.js'
import { testLiveVideoResolutions } from '@tests/shared/live.js'
import { SQLCommand } from '@tests/shared/sql-command.js'
import { checkThumbnails } from '@tests/shared/videos.js'
import { expect } from 'chai'
import { basename, join } from 'path'
@@ -96,8 +95,7 @@ describe('Test live', function () {
replaySettings: { privacy: VideoPrivacy.PUBLIC },
latencyMode: LiveVideoLatencyMode.SMALL_LATENCY,
privacy: VideoPrivacy.PUBLIC,
previewfile: 'video_short1-preview.webm.jpg',
thumbnailfile: 'video_short1.webm.jpg'
thumbnailfile: 'video_short1.webm-thumbnail-850x480.jpg'
}
})
liveVideoUUID = live.uuid
@@ -127,8 +125,11 @@ describe('Test live', function () {
expect(video.downloadEnabled).to.be.false
expect(video.privacy.id).to.equal(VideoPrivacy.PUBLIC)
await testImageGeneratedByFFmpeg(server.url, 'video_short1-preview.webm', video.previewPath)
await testImageGeneratedByFFmpeg(server.url, 'video_short1.webm', video.thumbnailPath)
await checkThumbnails({
server,
video,
thumbnails: [ 'video_short1.webm-thumbnail-850x480.jpg', 'video_short1.webm.jpg' ]
})
const live = await server.live.get({ videoId: liveVideoUUID })
@@ -168,8 +169,9 @@ describe('Test live', function () {
expect(video.privacy.id).to.equal(VideoPrivacy.UNLISTED)
expect(video.nsfw).to.be.true
await makeGetRequest({ url: server.url, path: video.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 })
await makeGetRequest({ url: server.url, path: video.previewPath, expectedStatus: HttpStatusCode.OK_200 })
for (const t of video.thumbnails) {
await makeRawRequest({ url: t.fileUrl, expectedStatus: HttpStatusCode.OK_200 })
}
}
})
@@ -51,12 +51,21 @@ describe('Object storage for video static file privacy', function () {
}
}
for (const file of getAllFiles(video)) {
const internalFileUrl = await sqlCommand.getInternalFileUrl(file.id)
expectStartWith(internalFileUrl, ObjectStorageCommand.getScalewayBaseUrl())
const internalUrls = [
...video.files.map(file => {
return ObjectStorageCommand.getScalewayBaseUrl() +
`test:server-1-web-videos:private/${video.uuid}/${extractFilenameFromUrl(file.fileUrl)}`
}),
...video.streamingPlaylists[0].files.map(file => {
return ObjectStorageCommand.getScalewayBaseUrl() +
`test:server-1-streaming-playlists:hls/private/${video.uuid}/${extractFilenameFromUrl(file.fileUrl)}`
})
]
for (const internalUrl of internalUrls) {
const { text } = await makeRawRequest({
url: internalFileUrl,
url: internalUrl,
token: server.accessToken,
expectedStatus: HttpStatusCode.FORBIDDEN_403
})
@@ -1,9 +1,6 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
import { expect } from 'chai'
import { readFile } from 'fs/promises'
import { completeCheckHlsPlaylist } from '@tests/shared/streaming-playlists.js'
import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
import { maxBy } from '@peertube/peertube-core-utils'
import {
HttpStatusCode,
RunnerJobSuccessPayload,
@@ -16,17 +13,20 @@ import {
VODHLSTranscodingSuccess,
VODWebVideoTranscodingSuccess
} from '@peertube/peertube-models'
import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
import {
cleanupTests,
createMultipleServers,
doubleFollow,
makeGetRequest,
makeRawRequest,
PeerTubeServer,
setAccessTokensToServers,
setDefaultVideoChannel,
waitJobs
} from '@peertube/peertube-server-commands'
import { completeCheckHlsPlaylist } from '@tests/shared/streaming-playlists.js'
import { expect } from 'chai'
import { readFile } from 'fs/promises'
async function processAllJobs (server: PeerTubeServer, runnerToken: string) {
do {
@@ -435,7 +435,7 @@ describe('Test runner VOD transcoding', function () {
await servers[0].config.enableTranscoding({ hls: true, webVideo: true })
const attributes = { name: 'audio_with_preview', previewfile: 'custom-preview.jpg', fixture: 'sample.ogg' }
const attributes = { name: 'audio_with_preview', thumbnailfile: 'custom-thumbnail-big.jpg', fixture: 'sample.ogg' }
const { uuid } = await servers[0].videos.upload({ attributes, mode: 'legacy' })
videoUUID = uuid
@@ -470,9 +470,9 @@ describe('Test runner VOD transcoding', function () {
const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.previewFileUrl, jobToken, runnerToken })
const video = await servers[0].videos.get({ id: videoUUID })
const { body: inputFile } = await makeGetRequest({
url: servers[0].url,
path: video.previewPath,
const { body: inputFile } = await makeRawRequest({
url: maxBy(video.thumbnails, 'width').fileUrl,
expectedStatus: HttpStatusCode.OK_200
})
@@ -107,6 +107,7 @@ describe('Test index search', function () {
expect(video.licence.label).to.equal('Attribution - Share Alike')
expect(video.privacy.label).to.equal('Public')
expect(video.duration).to.equal(113)
// eslint-disable-next-line @typescript-eslint/no-deprecated
expect(video.thumbnailUrl.startsWith('https://framatube.org/lazy-static/thumbnails')).to.be.true
expect(video.account.host).to.equal('framatube.org')
@@ -375,6 +376,7 @@ describe('Test index search', function () {
const videoPlaylist = body.data[0]
expect(videoPlaylist.url).to.equal('https://peertube2.cpy.re/videos/watch/playlist/73804a40-da9a-40c2-b1eb-2c6d9eec8f0a')
// eslint-disable-next-line @typescript-eslint/no-deprecated
expect(videoPlaylist.thumbnailUrl).to.exist
expect(videoPlaylist.embedUrl).to.equal('https://peertube2.cpy.re/video-playlists/embed/fgei1ws1oa6FCaJ2qZPG29')
-19
View File
@@ -55,11 +55,6 @@ function checkInitialConfig (server: PeerTubeServer, data: CustomConfig) {
expect(data.client.browseVideos.defaultScope).to.equal('federated')
expect(data.client.menu.login.redirectOnSingleExternalAuth).to.be.false
expect(data.cache.previews.size).to.equal(1)
expect(data.cache.captions.size).to.equal(1)
expect(data.cache.torrents.size).to.equal(1)
expect(data.cache.storyboards.size).to.equal(1)
expect(data.signup.enabled).to.be.true
expect(data.signup.limit).to.equal(4)
expect(data.signup.minimumAge).to.equal(16)
@@ -249,20 +244,6 @@ function buildNewCustomConfig (server: PeerTubeServer): CustomConfig {
}
}
},
cache: {
previews: {
size: 2
},
captions: {
size: 3
},
torrents: {
size: 4
},
storyboards: {
size: 5
}
},
signup: {
enabled: false,
limit: 5,
+101 -19
View File
@@ -5,57 +5,139 @@ import {
cleanupTests,
createMultipleServers,
doubleFollow,
makeGetRequest,
makeRawRequest,
PeerTubeServer,
setAccessTokensToServers,
setDefaultAccountAvatar,
setDefaultChannelAvatar,
waitJobs
} from '@peertube/peertube-server-commands'
import { checkDirectoryIsEmpty } from '@tests/shared/directories.js'
import { expect } from 'chai'
import { join } from 'path'
describe('Test lazy static endpoinds', function () {
describe('Test lazy static endpoints', function () {
let servers: PeerTubeServer[]
let videoId: string
async function fetchRemoteData () {
{
const video = await servers[1].videos.get({ id: videoId })
for (const thumbnail of video.thumbnails) {
await makeRawRequest({ url: thumbnail.fileUrl, expectedStatus: HttpStatusCode.OK_200 })
}
const { storyboards } = await servers[1].storyboard.list({ id: video.uuid })
for (const storyboard of storyboards) {
await makeRawRequest({ url: storyboard.fileUrl, expectedStatus: HttpStatusCode.OK_200 })
}
const { data: captions } = await servers[1].captions.list({ videoId: video.uuid })
for (const caption of captions) {
await makeRawRequest({ url: caption.fileUrl, expectedStatus: HttpStatusCode.OK_200 })
}
}
{
const { data: accounts } = await servers[1].accounts.list()
const { data: channels } = await servers[1].channels.list()
for (const { avatars } of [ ...accounts, ...channels ]) {
for (const avatar of avatars) {
await makeRawRequest({ url: avatar.fileUrl, expectedStatus: HttpStatusCode.OK_200 })
}
}
}
}
async function checkCachedFiles (options: { populated: boolean }) {
if (options.populated) {
expect(await servers[1].servers.countFiles(join('cache', 'thumbnails'))).to.equal(2)
expect(await servers[1].servers.countFiles(join('cache', 'avatars'))).to.equal(2 * 4)
expect(await servers[1].servers.countFiles(join('cache', 'storyboards'))).to.equal(1)
expect(await servers[1].servers.countFiles(join('cache', 'video-captions'))).to.equal(1)
} else {
expect(await servers[1].servers.countFiles(join('cache', 'thumbnails'))).to.equal(0)
expect(await servers[1].servers.countFiles(join('cache', 'avatars'))).to.equal(0)
expect(await servers[1].servers.countFiles(join('cache', 'storyboards'))).to.equal(0)
expect(await servers[1].servers.countFiles(join('cache', 'video-captions'))).to.equal(0)
}
}
before(async function () {
this.timeout(240000)
servers = await createMultipleServers(2)
await setAccessTokensToServers(servers)
await setDefaultAccountAvatar(servers)
await setDefaultChannelAvatar(servers)
await servers[0].config.enableFileUpdate()
await doubleFollow(servers[0], servers[1])
const { uuid } = await servers[0].videos.quickUpload({ name: 'video' })
const { uuid } = await servers[0].videos.upload({
attributes: {
name: 'video',
thumbnailfile: 'custom-thumbnail-big.jpg'
}
})
videoId = uuid
await servers[0].captions.add({
language: 'ar',
videoId: uuid,
fixture: 'subtitle-good1.vtt'
})
await waitJobs(servers)
})
it('Should remove previous thumbnails/previews after an update', async function () {
it('Should remove previous data after an update', async function () {
this.timeout(60000)
const fetchRemoteImages = async () => {
const video = await servers[1].videos.get({ id: videoId })
await makeGetRequest({ url: servers[1].url, path: video.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 })
await makeGetRequest({ url: servers[1].url, path: video.previewPath, expectedStatus: HttpStatusCode.OK_200 })
}
await checkCachedFiles({ populated: false })
await fetchRemoteImages()
await fetchRemoteData()
// Update video
await servers[0].videos.update({
id: videoId,
attributes: { thumbnailfile: 'custom-thumbnail.jpg', previewfile: 'custom-preview.jpg' }
await checkCachedFiles({ populated: true })
// Will re-generate thumbnails and storyboard
await servers[0].videos.replaceSourceFile({ videoId, fixture: 'video_short_360p.mp4' })
await servers[0].captions.add({
language: 'ar',
videoId,
fixture: 'subtitle-good2.vtt'
})
await waitJobs(servers)
await fetchRemoteImages()
await fetchRemoteData()
await checkCachedFiles({ populated: true })
})
it('Should still have files after a server restart', async function () {
this.timeout(60000)
await servers[0].kill()
await servers[0].run()
await checkCachedFiles({ populated: true })
})
it('Should remove the video and remove cached files', async function () {
this.timeout(60000)
// Remove video
await servers[0].videos.remove({ id: videoId })
await waitJobs(servers)
await checkDirectoryIsEmpty(servers[1], 'thumbnails')
await checkDirectoryIsEmpty(servers[1], 'previews')
expect(await servers[1].servers.countFiles(join('cache', 'thumbnails'))).to.equal(0)
expect(await servers[1].servers.countFiles(join('cache', 'avatars'))).to.equal(2 * 4)
expect(await servers[1].servers.countFiles(join('cache', 'storyboards'))).to.equal(0)
expect(await servers[1].servers.countFiles(join('cache', 'video-captions'))).to.equal(0)
})
after(async function () {
+3 -2
View File
@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
import { maxBy } from '@peertube/peertube-core-utils'
import { HttpStatusCode, Video, VideoPlaylistPrivacy } from '@peertube/peertube-models'
import {
cleanupTests,
@@ -85,14 +86,14 @@ describe('Test services', function () {
`title="${video.name}" src="http://${servers[0].host}/videos/embed/${video.shortUUID}${suffix.output}" ` +
'style="border: none" allow="fullscreen"></iframe>'
const expectedThumbnailUrl = 'http://' + servers[0].host + video.previewPath
const thumbnail = maxBy(video.thumbnails, 'width')
expect(res.body.html).to.equal(expectedHtml)
expect(res.body.title).to.equal(video.name)
expect(res.body.author_name).to.equal(servers[0].store.channel.displayName)
expect(res.body.width).to.equal(560)
expect(res.body.height).to.equal(315)
expect(res.body.thumbnail_url).to.equal(expectedThumbnailUrl)
expect(res.body.thumbnail_url).to.equal(thumbnail.fileUrl)
expect(res.body.thumbnail_width).to.equal(850)
expect(res.body.thumbnail_height).to.equal(480)
}
@@ -8,7 +8,7 @@ import {
cleanupTests,
createMultipleServers,
doubleFollow,
makeGetRequest,
makeRawRequest,
PeerTubeServer,
setAccessTokensToServers,
waitJobs
@@ -306,7 +306,7 @@ describe('Test video transcoding', function () {
it('Should merge an audio file with the preview file', async function () {
this.timeout(60_000)
const attributes = { name: 'audio_with_preview', previewfile: 'custom-preview.jpg', fixture: 'sample.ogg' }
const attributes = { name: 'audio_with_preview', thumbnailfile: 'custom-thumbnail-big.jpg', fixture: 'sample.ogg' }
await servers[1].videos.upload({ attributes, mode })
await waitJobs(servers)
@@ -319,8 +319,9 @@ describe('Test video transcoding', function () {
expect(videoDetails.files).to.have.lengthOf(1)
await makeGetRequest({ url: server.url, path: videoDetails.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 })
await makeGetRequest({ url: server.url, path: videoDetails.previewPath, expectedStatus: HttpStatusCode.OK_200 })
for (const t of video.thumbnails) {
await makeRawRequest({ url: t.fileUrl, expectedStatus: HttpStatusCode.OK_200 })
}
const magnetUri = videoDetails.files[0].magnetUri
expect(magnetUri).to.contain('.mp4')
@@ -343,8 +344,9 @@ describe('Test video transcoding', function () {
expect(videoDetails.files).to.have.lengthOf(1)
await makeGetRequest({ url: server.url, path: videoDetails.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 })
await makeGetRequest({ url: server.url, path: videoDetails.previewPath, expectedStatus: HttpStatusCode.OK_200 })
for (const t of videoDetails.thumbnails) {
await makeRawRequest({ url: t.fileUrl, expectedStatus: HttpStatusCode.OK_200 })
}
const magnetUri = videoDetails.files[0].magnetUri
expect(magnetUri).to.contain('.mp4')
@@ -369,7 +371,7 @@ describe('Test video transcoding', function () {
}
})
const attributes = { name: 'audio_with_preview', previewfile: 'custom-preview.jpg', fixture: 'sample.ogg' }
const attributes = { name: 'audio_with_preview', thumbnailfile: 'custom-thumbnail-big.jpg', fixture: 'sample.ogg' }
const { id } = await servers[1].videos.upload({ attributes, mode })
await waitJobs(servers)
+1 -2
View File
@@ -479,8 +479,7 @@ function runTest (withObjectStorage: boolean) {
size: 23000
}
],
thumbnailfile: 'custom-thumbnail-from-preview',
previewfile: 'custom-preview'
thumbnails: [ 'custom-thumbnail-280x157.jpg', 'custom-thumbnail-850x480.jpg' ]
}
})
}
+2 -4
View File
@@ -138,8 +138,7 @@ describe('Test user videos', function () {
const video = data[0]
expect(video.name).to.equal('super user video')
expect(video.thumbnailPath).to.not.be.null
expect(video.previewPath).to.not.be.null
expect(video.thumbnails).to.have.lengthOf(2)
})
it('Should be able to filter by a specific channel in my videos', async function () {
@@ -154,8 +153,7 @@ describe('Test user videos', function () {
const video = data[0]
expect(video.name).to.equal('super user video')
expect(video.thumbnailPath).to.not.be.null
expect(video.previewPath).to.not.be.null
expect(video.thumbnails).to.have.lengthOf(2)
}
{

Some files were not shown because too many files have changed in this diff Show More