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
@@ -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,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.0 KiB |
|
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')
|
||||
}
|
||||
|
||||
|
||||
@@ -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' ])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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' ]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
{
|
||||
|
||||