Add config option to keep original video file (basic first version) (#6157)
* testing not removing old file and adding columb to db * implement feature * remove unnecessary config changes * use only keptOriginalFileName, change keptOriginalFileName to keptOriginalFilename for consistency with with videoFile table, slight refactor with basename() * save original video files to dedicated directory original-video-files * begin implementing object storage (bucket) support --------- Co-authored-by: chagai.friedlander <chagai.friedlander@fairkom.eu> Co-authored-by: Ian <ian.kraft@hotmail.com> Co-authored-by: Chocobozzz <me@florianbigard.com>
This commit is contained in:
parent
ae31e90c30
commit
e57c3024f4
|
@ -226,6 +226,9 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
||||||
concurrency: CONCURRENCY_VALIDATOR,
|
concurrency: CONCURRENCY_VALIDATOR,
|
||||||
resolutions: {},
|
resolutions: {},
|
||||||
alwaysTranscodeOriginalResolution: null,
|
alwaysTranscodeOriginalResolution: null,
|
||||||
|
originalFile: {
|
||||||
|
keep: null
|
||||||
|
},
|
||||||
hls: {
|
hls: {
|
||||||
enabled: null
|
enabled: null
|
||||||
},
|
},
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
<ng-container ngProjectAs="extra">
|
<ng-container ngProjectAs="extra">
|
||||||
|
|
||||||
<div class="callout callout-light pt-2 pb-0">
|
<div class="callout callout-light pt-2 pb-0">
|
||||||
<h3 class="callout-title" i18n>Input formats</h3>
|
<h3 class="callout-title" i18n>Input</h3>
|
||||||
|
|
||||||
<div class="form-group" [ngClass]="getTranscodingDisabledClass()">
|
<div class="form-group" [ngClass]="getTranscodingDisabledClass()">
|
||||||
<my-peertube-checkbox
|
<my-peertube-checkbox
|
||||||
|
@ -63,10 +63,21 @@
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</my-peertube-checkbox>
|
</my-peertube-checkbox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" formGroupName="originalFile" [ngClass]="getTranscodingDisabledClass()">
|
||||||
|
<my-peertube-checkbox
|
||||||
|
inputName="transcodingOriginalFileKeep" formControlName="keep"
|
||||||
|
i18n-labelText labelText="Keep a version of the input file"
|
||||||
|
>
|
||||||
|
<ng-container ngProjectAs="description">
|
||||||
|
<div i18n>If enabled, the input file is not deleted after transcoding but moved in a dedicated folder or object storage</div>
|
||||||
|
</ng-container>
|
||||||
|
</my-peertube-checkbox>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="callout callout-light pt-2 mt-2 pb-0">
|
<div class="callout callout-light pt-2 mt-2 pb-0">
|
||||||
<h3 class="callout-title" i18n>Output formats</h3>
|
<h3 class="callout-title" i18n>Output</h3>
|
||||||
|
|
||||||
<ng-container formGroupName="webVideos">
|
<ng-container formGroupName="webVideos">
|
||||||
<div class="form-group" [ngClass]="getTranscodingDisabledClass()">
|
<div class="form-group" [ngClass]="getTranscodingDisabledClass()">
|
||||||
|
|
|
@ -405,7 +405,7 @@ export class VideoService {
|
||||||
|
|
||||||
getSource (videoId: number) {
|
getSource (videoId: number) {
|
||||||
return this.authHttp
|
return this.authHttp
|
||||||
.get<{ source: VideoSource }>(VideoService.BASE_VIDEO_URL + '/' + videoId + '/source')
|
.get<VideoSource>(VideoService.BASE_VIDEO_URL + '/' + videoId + '/source')
|
||||||
.pipe(
|
.pipe(
|
||||||
catchError(err => {
|
catchError(err => {
|
||||||
if (err.status === 404) {
|
if (err.status === 404) {
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-body">
|
<div class="modal-body" [ngClass]="{ 'opacity-0': !loaded }">
|
||||||
<div class="alert alert-warning" *ngIf="isConfidentialVideo()" i18n>
|
<div class="alert alert-warning" *ngIf="isConfidentialVideo()" i18n>
|
||||||
The following link contains a private token and should not be shared with anyone.
|
The following link contains a private token and should not be shared with anyone.
|
||||||
</div>
|
</div>
|
||||||
|
@ -45,14 +45,31 @@
|
||||||
<!-- Video tab -->
|
<!-- Video tab -->
|
||||||
<ng-container *ngIf="type === 'video'">
|
<ng-container *ngIf="type === 'video'">
|
||||||
<div ngbNav #resolutionNav="ngbNav" class="nav-tabs" [activeId]="resolutionId" (activeIdChange)="onResolutionIdChange($event)">
|
<div ngbNav #resolutionNav="ngbNav" class="nav-tabs" [activeId]="resolutionId" (activeIdChange)="onResolutionIdChange($event)">
|
||||||
<ng-container *ngFor="let file of getVideoFiles()" [ngbNavItem]="file.resolution.id">
|
|
||||||
<a ngbNavLink i18n>{{ file.resolution.label }}</a>
|
|
||||||
|
|
||||||
<ng-template ngbNavContent>
|
<ng-template #rootNavContent>
|
||||||
<div class="nav-content">
|
<div class="nav-content">
|
||||||
<my-input-text [show]="true" [readonly]="true" [withCopy]="true" [withToggle]="false" [value]="getLink()"></my-input-text>
|
<my-input-text [show]="true" [readonly]="true" [withCopy]="true" [withToggle]="false" [value]="getLink()"></my-input-text>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-container *ngIf="originalVideoFile" ngbNavItem="original">
|
||||||
|
<a ngbNavLink i18n>
|
||||||
|
<ng-container>Original file</ng-container>
|
||||||
|
|
||||||
|
<my-global-icon ngbTooltip="Other users cannot download the original file" iconName="shield"></my-global-icon>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<ng-template ngbNavContent>
|
||||||
|
<ng-template [ngTemplateOutlet]="rootNavContent"></ng-template>
|
||||||
|
</ng-template>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngFor="let file of getVideoFiles()" [ngbNavItem]="file.resolution.id">
|
||||||
|
<a ngbNavLink>{{ file.resolution.label }}</a>
|
||||||
|
|
||||||
|
<ng-template ngbNavContent>
|
||||||
|
<ng-template [ngTemplateOutlet]="rootNavContent"></ng-template>
|
||||||
|
</ng-template>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -60,47 +77,59 @@
|
||||||
|
|
||||||
<div class="advanced-filters" [ngbCollapse]="isAdvancedCustomizationCollapsed" [animation]="true">
|
<div class="advanced-filters" [ngbCollapse]="isAdvancedCustomizationCollapsed" [animation]="true">
|
||||||
<div ngbNav #navMetadata="ngbNav" class="nav-tabs nav-metadata">
|
<div ngbNav #navMetadata="ngbNav" class="nav-tabs nav-metadata">
|
||||||
<ng-container ngbNavItem>
|
|
||||||
<a ngbNavLink i18n>Format</a>
|
<ng-template #metadataInfo let-item>
|
||||||
<ng-template ngbNavContent>
|
<div class="metadata-attribute">
|
||||||
<div class="file-metadata">
|
<span>{{ item.value.label }}</span>
|
||||||
<div class="metadata-attribute metadata-attribute-tags" *ngFor="let item of videoFileMetadataFormat | keyvalue">
|
|
||||||
<span i18n class="metadata-attribute-label">{{ item.value.label }}</span>
|
@if (item.value.value) {
|
||||||
<span class="metadata-attribute-value">{{ item.value.value }}</span>
|
<span>{{ item.value.value }}</span>
|
||||||
</div>
|
} @else {
|
||||||
|
<span i18n>Unknown</span>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-container ngbNavItem [disabled]="videoFileMetadataVideoStream === undefined">
|
<ng-container ngbNavItem>
|
||||||
<a ngbNavLink i18n>Video stream</a>
|
<a ngbNavLink i18n>Format</a>
|
||||||
|
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<div class="file-metadata">
|
<div class="file-metadata">
|
||||||
<div class="metadata-attribute metadata-attribute-tags" *ngFor="let item of videoFileMetadataVideoStream | keyvalue">
|
@for (item of videoFileMetadataFormat | keyvalue; track item) {
|
||||||
<span i18n class="metadata-attribute-label">{{ item.value.label }}</span>
|
<ng-template [ngTemplateOutlet]="metadataInfo" [ngTemplateOutletContext]="{ $implicit: item }"></ng-template>
|
||||||
<span class="metadata-attribute-value">{{ item.value.value }}</span>
|
}
|
||||||
</div>
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container ngbNavItem [disabled]="videoFileMetadataVideoStream === undefined">
|
||||||
|
<a ngbNavLink i18n>Video stream</a>
|
||||||
|
|
||||||
|
<ng-template ngbNavContent>
|
||||||
|
<div class="file-metadata">
|
||||||
|
@for (item of videoFileMetadataVideoStream | keyvalue; track item) {
|
||||||
|
<ng-template [ngTemplateOutlet]="metadataInfo" [ngTemplateOutletContext]="{ $implicit: item }"></ng-template>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container ngbNavItem [disabled]="videoFileMetadataAudioStream === undefined">
|
<ng-container ngbNavItem [disabled]="videoFileMetadataAudioStream === undefined">
|
||||||
<a ngbNavLink i18n>Audio stream</a>
|
<a ngbNavLink i18n>Audio stream</a>
|
||||||
|
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<div class="file-metadata">
|
<div class="file-metadata">
|
||||||
<div class="metadata-attribute metadata-attribute-tags" *ngFor="let item of videoFileMetadataAudioStream | keyvalue">
|
@for (item of videoFileMetadataAudioStream | keyvalue; track item) {
|
||||||
<span i18n class="metadata-attribute-label">{{ item.value.label }}</span>
|
<ng-template [ngTemplateOutlet]="metadataInfo" [ngTemplateOutletContext]="{ $implicit: item }"></ng-template>
|
||||||
<span class="metadata-attribute-value">{{ item.value.value }}</span>
|
}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="getFileMetadata()" [ngbNavOutlet]="navMetadata"></div>
|
<div *ngIf="hasMetadata()" [ngbNavOutlet]="navMetadata"></div>
|
||||||
|
|
||||||
<div class="download-type">
|
<div [hidden]="originalVideoFile" class="download-type">
|
||||||
<div class="peertube-radio-container">
|
<div class="peertube-radio-container">
|
||||||
<input type="radio" name="download" id="download-direct" [(ngModel)]="downloadType" value="direct">
|
<input type="radio" name="download" id="download-direct" [(ngModel)]="downloadType" value="direct">
|
||||||
<label i18n for="download-direct">Direct download</label>
|
<label i18n for="download-direct">Direct download</label>
|
||||||
|
@ -121,17 +150,13 @@
|
||||||
<ng-container *ngIf="isAdvancedCustomizationCollapsed">
|
<ng-container *ngIf="isAdvancedCustomizationCollapsed">
|
||||||
<span class="chevron-down"></span>
|
<span class="chevron-down"></span>
|
||||||
|
|
||||||
<ng-container i18n>
|
<ng-container i18n>More information/options</ng-container>
|
||||||
Advanced
|
|
||||||
</ng-container>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container *ngIf="!isAdvancedCustomizationCollapsed">
|
<ng-container *ngIf="!isAdvancedCustomizationCollapsed">
|
||||||
<span class="chevron-up"></span>
|
<span class="chevron-up"></span>
|
||||||
|
|
||||||
<ng-container i18n>
|
<ng-container i18n>Less information/options</ng-container>
|
||||||
Simple
|
|
||||||
</ng-container>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</button>
|
</button>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
|
@ -5,6 +5,13 @@
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
my-global-icon[iconName=shield] {
|
||||||
|
@include margin-left(10px);
|
||||||
|
|
||||||
|
width: 16px;
|
||||||
|
margin-top: -3px;
|
||||||
|
}
|
||||||
|
|
||||||
.advanced-filters-button {
|
.advanced-filters-button {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -53,7 +60,7 @@
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
|
|
||||||
.metadata-attribute-label {
|
> span:first-child {
|
||||||
@include padding-right(5px);
|
@include padding-right(5px);
|
||||||
|
|
||||||
min-width: 142px;
|
min-width: 142px;
|
||||||
|
@ -61,22 +68,4 @@
|
||||||
color: pvar(--greyForegroundColor);
|
color: pvar(--greyForegroundColor);
|
||||||
font-weight: $font-bold;
|
font-weight: $font-bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.metadata-attribute-value {
|
|
||||||
@include disable-default-a-behaviour;
|
|
||||||
|
|
||||||
color: pvar(--mainForegroundColor);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.metadata-attribute-tags {
|
|
||||||
.metadata-attribute-value:not(:nth-child(2)) {
|
|
||||||
&::before {
|
|
||||||
content: ', ';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,30 +1,31 @@
|
||||||
import { mapValues } from 'lodash-es'
|
import { KeyValuePipe, NgClass, NgFor, NgIf, NgTemplateOutlet } from '@angular/common'
|
||||||
import { firstValueFrom } from 'rxjs'
|
|
||||||
import { tap } from 'rxjs/operators'
|
|
||||||
import { Component, ElementRef, Inject, Input, LOCALE_ID, ViewChild } from '@angular/core'
|
import { Component, ElementRef, Inject, Input, LOCALE_ID, ViewChild } from '@angular/core'
|
||||||
import { HooksService } from '@app/core'
|
import { FormsModule } from '@angular/forms'
|
||||||
|
import { AuthService, HooksService } from '@app/core'
|
||||||
import {
|
import {
|
||||||
|
NgbCollapse,
|
||||||
NgbModal,
|
NgbModal,
|
||||||
NgbModalRef,
|
NgbModalRef,
|
||||||
NgbNav,
|
NgbNav,
|
||||||
|
NgbNavContent,
|
||||||
NgbNavItem,
|
NgbNavItem,
|
||||||
NgbNavLink,
|
NgbNavLink,
|
||||||
NgbNavLinkBase,
|
NgbNavLinkBase,
|
||||||
NgbNavContent,
|
|
||||||
NgbNavOutlet,
|
NgbNavOutlet,
|
||||||
NgbCollapse
|
NgbTooltip
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { objectKeysTyped, pick } from '@peertube/peertube-core-utils'
|
||||||
|
import { VideoCaption, VideoFile, VideoFileMetadata, VideoSource } from '@peertube/peertube-models'
|
||||||
import { logger } from '@root-helpers/logger'
|
import { logger } from '@root-helpers/logger'
|
||||||
import { videoRequiresFileToken } from '@root-helpers/video'
|
import { videoRequiresFileToken } from '@root-helpers/video'
|
||||||
import { objectKeysTyped, pick } from '@peertube/peertube-core-utils'
|
import { mapValues } from 'lodash-es'
|
||||||
import { VideoCaption, VideoFile } from '@peertube/peertube-models'
|
import { firstValueFrom, of } from 'rxjs'
|
||||||
|
import { tap } from 'rxjs/operators'
|
||||||
import { InputTextComponent } from '../shared-forms/input-text.component'
|
import { InputTextComponent } from '../shared-forms/input-text.component'
|
||||||
import { GlobalIconComponent } from '../shared-icons/global-icon.component'
|
import { GlobalIconComponent } from '../shared-icons/global-icon.component'
|
||||||
import { FormsModule } from '@angular/forms'
|
|
||||||
import { NgIf, NgFor, KeyValuePipe } from '@angular/common'
|
|
||||||
import { VideoDetails } from '../shared-main/video/video-details.model'
|
|
||||||
import { BytesPipe } from '../shared-main/angular/bytes.pipe'
|
import { BytesPipe } from '../shared-main/angular/bytes.pipe'
|
||||||
import { NumberFormatterPipe } from '../shared-main/angular/number-formatter.pipe'
|
import { NumberFormatterPipe } from '../shared-main/angular/number-formatter.pipe'
|
||||||
|
import { VideoDetails } from '../shared-main/video/video-details.model'
|
||||||
import { VideoFileTokenService } from '../shared-main/video/video-file-token.service'
|
import { VideoFileTokenService } from '../shared-main/video/video-file-token.service'
|
||||||
import { VideoService } from '../shared-main/video/video.service'
|
import { VideoService } from '../shared-main/video/video.service'
|
||||||
|
|
||||||
|
@ -49,7 +50,10 @@ type FileMetadata = { [key: string]: { label: string, value: string | number } }
|
||||||
InputTextComponent,
|
InputTextComponent,
|
||||||
NgbNavOutlet,
|
NgbNavOutlet,
|
||||||
NgbCollapse,
|
NgbCollapse,
|
||||||
KeyValuePipe
|
KeyValuePipe,
|
||||||
|
NgbTooltip,
|
||||||
|
NgTemplateOutlet,
|
||||||
|
NgClass
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class VideoDownloadComponent {
|
export class VideoDownloadComponent {
|
||||||
|
@ -59,7 +63,7 @@ export class VideoDownloadComponent {
|
||||||
|
|
||||||
downloadType: 'direct' | 'torrent' = 'direct'
|
downloadType: 'direct' | 'torrent' = 'direct'
|
||||||
|
|
||||||
resolutionId: number | string = -1
|
resolutionId: number | 'original' = -1
|
||||||
subtitleLanguageId: string
|
subtitleLanguageId: string
|
||||||
|
|
||||||
videoFileMetadataFormat: FileMetadata
|
videoFileMetadataFormat: FileMetadata
|
||||||
|
@ -72,6 +76,10 @@ export class VideoDownloadComponent {
|
||||||
|
|
||||||
videoFileToken: string
|
videoFileToken: string
|
||||||
|
|
||||||
|
originalVideoFile: VideoSource
|
||||||
|
|
||||||
|
loaded = false
|
||||||
|
|
||||||
private activeModal: NgbModalRef
|
private activeModal: NgbModalRef
|
||||||
|
|
||||||
private bytesPipe: BytesPipe
|
private bytesPipe: BytesPipe
|
||||||
|
@ -83,6 +91,7 @@ export class VideoDownloadComponent {
|
||||||
constructor (
|
constructor (
|
||||||
@Inject(LOCALE_ID) private localeId: string,
|
@Inject(LOCALE_ID) private localeId: string,
|
||||||
private modalService: NgbModal,
|
private modalService: NgbModal,
|
||||||
|
private authService: AuthService,
|
||||||
private videoService: VideoService,
|
private videoService: VideoService,
|
||||||
private videoFileTokenService: VideoFileTokenService,
|
private videoFileTokenService: VideoFileTokenService,
|
||||||
private hooks: HooksService
|
private hooks: HooksService
|
||||||
|
@ -110,7 +119,10 @@ export class VideoDownloadComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
show (video: VideoDetails, videoCaptions?: VideoCaption[]) {
|
show (video: VideoDetails, videoCaptions?: VideoCaption[]) {
|
||||||
|
this.loaded = false
|
||||||
|
|
||||||
this.videoFileToken = undefined
|
this.videoFileToken = undefined
|
||||||
|
this.originalVideoFile = undefined
|
||||||
|
|
||||||
this.video = video
|
this.video = video
|
||||||
this.videoCaptions = videoCaptions
|
this.videoCaptions = videoCaptions
|
||||||
|
@ -125,16 +137,40 @@ export class VideoDownloadComponent {
|
||||||
this.subtitleLanguageId = this.videoCaptions[0].language.id
|
this.subtitleLanguageId = this.videoCaptions[0].language.id
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isConfidentialVideo()) {
|
this.getOriginalVideoFileObs()
|
||||||
this.videoFileTokenService.getVideoFileToken({ videoUUID: this.video.uuid, videoPassword: this.videoPassword })
|
.subscribe(source => {
|
||||||
.subscribe(({ token }) => this.videoFileToken = token)
|
if (source?.fileDownloadUrl) {
|
||||||
|
this.originalVideoFile = source
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.originalVideoFile || this.isConfidentialVideo()) {
|
||||||
|
this.videoFileTokenService.getVideoFileToken({ videoUUID: this.video.uuid, videoPassword: this.videoPassword })
|
||||||
|
.subscribe(({ token }) => {
|
||||||
|
this.videoFileToken = token
|
||||||
|
|
||||||
|
this.loaded = true
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.loaded = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
this.activeModal.shown.subscribe(() => {
|
this.activeModal.shown.subscribe(() => {
|
||||||
this.hooks.runAction('action:modal.video-download.shown', 'common')
|
this.hooks.runAction('action:modal.video-download.shown', 'common')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getOriginalVideoFileObs () {
|
||||||
|
if (!this.authService.isLoggedIn()) return of(undefined)
|
||||||
|
const user = this.authService.getUser()
|
||||||
|
|
||||||
|
if (!this.video.isOwnerOrHasSeeAllVideosRight(user)) return of(undefined)
|
||||||
|
|
||||||
|
return this.videoService.getSource(this.video.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
onClose () {
|
onClose () {
|
||||||
this.video = undefined
|
this.video = undefined
|
||||||
this.videoCaptions = undefined
|
this.videoCaptions = undefined
|
||||||
|
@ -152,28 +188,29 @@ export class VideoDownloadComponent {
|
||||||
: this.getVideoFileLink()
|
: this.getVideoFileLink()
|
||||||
}
|
}
|
||||||
|
|
||||||
async onResolutionIdChange (resolutionId: number) {
|
async onResolutionIdChange (resolutionId: number | 'original') {
|
||||||
this.resolutionId = resolutionId
|
this.resolutionId = resolutionId
|
||||||
|
|
||||||
|
let metadata: VideoFileMetadata
|
||||||
|
|
||||||
|
if (this.resolutionId === 'original') {
|
||||||
|
metadata = this.originalVideoFile.metadata
|
||||||
|
} else {
|
||||||
const videoFile = this.getVideoFile()
|
const videoFile = this.getVideoFile()
|
||||||
|
if (!videoFile) return
|
||||||
|
|
||||||
if (!videoFile.metadata) {
|
if (!videoFile.metadata && videoFile.metadataUrl) {
|
||||||
if (!videoFile.metadataUrl) return
|
|
||||||
|
|
||||||
await this.hydrateMetadataFromMetadataUrl(videoFile)
|
await this.hydrateMetadataFromMetadataUrl(videoFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!videoFile.metadata) return
|
metadata = videoFile.metadata
|
||||||
|
}
|
||||||
|
|
||||||
this.videoFileMetadataFormat = videoFile
|
if (!metadata) return
|
||||||
? this.getMetadataFormat(videoFile.metadata.format)
|
|
||||||
: undefined
|
this.videoFileMetadataFormat = this.getMetadataFormat(metadata.format)
|
||||||
this.videoFileMetadataVideoStream = videoFile
|
this.videoFileMetadataVideoStream = this.getMetadataStream(metadata.streams, 'video')
|
||||||
? this.getMetadataStream(videoFile.metadata.streams, 'video')
|
this.videoFileMetadataAudioStream = this.getMetadataStream(metadata.streams, 'audio')
|
||||||
: undefined
|
|
||||||
this.videoFileMetadataAudioStream = videoFile
|
|
||||||
? this.getMetadataStream(videoFile.metadata.streams, 'audio')
|
|
||||||
: undefined
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubtitleIdChange (subtitleId: string) {
|
onSubtitleIdChange (subtitleId: string) {
|
||||||
|
@ -185,6 +222,8 @@ export class VideoDownloadComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
getVideoFile () {
|
getVideoFile () {
|
||||||
|
if (this.resolutionId === 'original') return undefined
|
||||||
|
|
||||||
const file = this.getVideoFiles()
|
const file = this.getVideoFiles()
|
||||||
.find(f => f.resolution.id === this.resolutionId)
|
.find(f => f.resolution.id === this.resolutionId)
|
||||||
|
|
||||||
|
@ -197,13 +236,17 @@ export class VideoDownloadComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
getVideoFileLink () {
|
getVideoFileLink () {
|
||||||
const file = this.getVideoFile()
|
const suffix = this.resolutionId === 'original' || this.isConfidentialVideo()
|
||||||
if (!file) return ''
|
|
||||||
|
|
||||||
const suffix = this.isConfidentialVideo()
|
|
||||||
? '?videoFileToken=' + this.videoFileToken
|
? '?videoFileToken=' + this.videoFileToken
|
||||||
: ''
|
: ''
|
||||||
|
|
||||||
|
if (this.resolutionId === 'original') {
|
||||||
|
return this.originalVideoFile.fileDownloadUrl + suffix
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = this.getVideoFile()
|
||||||
|
if (!file) return ''
|
||||||
|
|
||||||
switch (this.downloadType) {
|
switch (this.downloadType) {
|
||||||
case 'direct':
|
case 'direct':
|
||||||
return file.fileDownloadUrl + suffix
|
return file.fileDownloadUrl + suffix
|
||||||
|
@ -237,19 +280,15 @@ export class VideoDownloadComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
isConfidentialVideo () {
|
isConfidentialVideo () {
|
||||||
return videoRequiresFileToken(this.video)
|
return this.resolutionId === 'original' || videoRequiresFileToken(this.video)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switchToType (type: DownloadType) {
|
switchToType (type: DownloadType) {
|
||||||
this.type = type
|
this.type = type
|
||||||
}
|
}
|
||||||
|
|
||||||
getFileMetadata () {
|
hasMetadata () {
|
||||||
const file = this.getVideoFile()
|
return !!this.videoFileMetadataFormat
|
||||||
if (!file) return undefined
|
|
||||||
|
|
||||||
return file.metadata
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getMetadataFormat (format: any) {
|
private getMetadataFormat (format: any) {
|
||||||
|
@ -282,7 +321,9 @@ export class VideoDownloadComponent {
|
||||||
profile: (value: string) => ({ label: $localize`Profile`, value }),
|
profile: (value: string) => ({ label: $localize`Profile`, value }),
|
||||||
bit_rate: (value: number | string) => ({
|
bit_rate: (value: number | string) => ({
|
||||||
label: $localize`Bitrate`,
|
label: $localize`Bitrate`,
|
||||||
value: `${this.numbersPipe.transform(+value)}bps`
|
value: isNaN(+value)
|
||||||
|
? undefined
|
||||||
|
: `${this.numbersPipe.transform(+value)}bps`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512" fill="currentColor"
|
xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 512 512" fill="currentColor"
|
||||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||||
>
|
>
|
||||||
<path d="M466.5 83.7l-192-80a48.15 48.15 0 0 0-36.9 0l-192 80C27.7 91.1 16 108.6 16 128c0 198.5 114.5 335.7 221.5 380.3 11.8 4.9 25.1 4.9 36.9 0C360.1 472.6 496 349.3 496 128c0-19.4-11.7-36.9-29.5-44.3zM256.1 446.3l-.1-381 175.9 73.3c-3.3 151.4-82.1 261.1-175.8 307.7z"></path>
|
<path d="M466.5 83.7l-192-80a48.15 48.15 0 0 0-36.9 0l-192 80C27.7 91.1 16 108.6 16 128c0 198.5 114.5 335.7 221.5 380.3 11.8 4.9 25.1 4.9 36.9 0C360.1 472.6 496 349.3 496 128c0-19.4-11.7-36.9-29.5-44.3zM256.1 446.3l-.1-381 175.9 73.3c-3.3 151.4-82.1 261.1-175.8 307.7z"></path>
|
||||||
|
|
Before Width: | Height: | Size: 486 B After Width: | Height: | Size: 484 B |
|
@ -152,6 +152,7 @@ storage:
|
||||||
avatars: 'storage/avatars/'
|
avatars: 'storage/avatars/'
|
||||||
web_videos: 'storage/web-videos/'
|
web_videos: 'storage/web-videos/'
|
||||||
streaming_playlists: 'storage/streaming-playlists/'
|
streaming_playlists: 'storage/streaming-playlists/'
|
||||||
|
original_video_files: 'storage/original-video-files/'
|
||||||
redundancy: 'storage/redundancy/'
|
redundancy: 'storage/redundancy/'
|
||||||
logs: 'storage/logs/'
|
logs: 'storage/logs/'
|
||||||
previews: 'storage/previews/'
|
previews: 'storage/previews/'
|
||||||
|
@ -238,6 +239,12 @@ object_storage:
|
||||||
prefix: ''
|
prefix: ''
|
||||||
base_url: ''
|
base_url: ''
|
||||||
|
|
||||||
|
# Same settings but for original video files
|
||||||
|
original_video_files:
|
||||||
|
bucket_name: 'original-video-files'
|
||||||
|
prefix: ''
|
||||||
|
base_url: ''
|
||||||
|
|
||||||
log:
|
log:
|
||||||
level: 'info' # 'debug' | 'info' | 'warn' | 'error'
|
level: 'info' # 'debug' | 'info' | 'warn' | 'error'
|
||||||
|
|
||||||
|
@ -526,6 +533,11 @@ video_channels:
|
||||||
transcoding:
|
transcoding:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
||||||
|
original_file:
|
||||||
|
# If false the uploaded file is deleted after transcoding
|
||||||
|
# If yes it is not deleted but moved in a dedicated folder or object storage
|
||||||
|
keep: false
|
||||||
|
|
||||||
# Allow your users to upload .mkv, .mov, .avi, .wmv, .flv, .f4v, .3g2, .3gp, .mts, m2ts, .mxf, .nut videos
|
# Allow your users to upload .mkv, .mov, .avi, .wmv, .flv, .f4v, .3g2, .3gp, .mts, m2ts, .mxf, .nut videos
|
||||||
allow_additional_extensions: true
|
allow_additional_extensions: true
|
||||||
|
|
||||||
|
@ -844,7 +856,7 @@ services:
|
||||||
# Cards configuration to format video in Twitter/X
|
# Cards configuration to format video in Twitter/X
|
||||||
# All other social media (Facebook, Mastodon, etc.) are supported out of the box
|
# All other social media (Facebook, Mastodon, etc.) are supported out of the box
|
||||||
twitter:
|
twitter:
|
||||||
# Indicates the Twitter account for the website or platform where the content was published
|
# Indicates the Twitter/X account for the website or platform where the content was published
|
||||||
# This is just an information injected in HTML that is required by Twitter/X
|
# This is just an information injected in HTML that is required by Twitter/X
|
||||||
username: '@Chocobozzz'
|
username: '@Chocobozzz'
|
||||||
|
|
||||||
|
|
|
@ -128,3 +128,6 @@ geo_ip:
|
||||||
|
|
||||||
video_studio:
|
video_studio:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
||||||
|
transcoding:
|
||||||
|
keep_original_file: false
|
||||||
|
|
|
@ -150,6 +150,7 @@ storage:
|
||||||
avatars: '/var/www/peertube/storage/avatars/'
|
avatars: '/var/www/peertube/storage/avatars/'
|
||||||
web_videos: '/var/www/peertube/storage/web-videos/'
|
web_videos: '/var/www/peertube/storage/web-videos/'
|
||||||
streaming_playlists: '/var/www/peertube/storage/streaming-playlists/'
|
streaming_playlists: '/var/www/peertube/storage/streaming-playlists/'
|
||||||
|
original_video_files: '/var/www/peertube/storage/original-video-files/'
|
||||||
redundancy: '/var/www/peertube/storage/redundancy/'
|
redundancy: '/var/www/peertube/storage/redundancy/'
|
||||||
logs: '/var/www/peertube/storage/logs/'
|
logs: '/var/www/peertube/storage/logs/'
|
||||||
previews: '/var/www/peertube/storage/previews/'
|
previews: '/var/www/peertube/storage/previews/'
|
||||||
|
@ -236,6 +237,12 @@ object_storage:
|
||||||
prefix: ''
|
prefix: ''
|
||||||
base_url: ''
|
base_url: ''
|
||||||
|
|
||||||
|
# Same settings but for original video files
|
||||||
|
original_video_files:
|
||||||
|
bucket_name: 'original-video-files'
|
||||||
|
prefix: ''
|
||||||
|
base_url: ''
|
||||||
|
|
||||||
log:
|
log:
|
||||||
level: 'info' # 'debug' | 'info' | 'warn' | 'error'
|
level: 'info' # 'debug' | 'info' | 'warn' | 'error'
|
||||||
|
|
||||||
|
@ -536,6 +543,11 @@ video_channels:
|
||||||
transcoding:
|
transcoding:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
||||||
|
original_file:
|
||||||
|
# If false the uploaded file is deleted after transcoding
|
||||||
|
# If yes it is not deleted but moved in a dedicated folder or object storage
|
||||||
|
keep: false
|
||||||
|
|
||||||
# Allow your users to upload .mkv, .mov, .avi, .wmv, .flv, .f4v, .3g2, .3gp, .mts, m2ts, .mxf, .nut videos
|
# Allow your users to upload .mkv, .mov, .avi, .wmv, .flv, .f4v, .3g2, .3gp, .mts, m2ts, .mxf, .nut videos
|
||||||
allow_additional_extensions: true
|
allow_additional_extensions: true
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ storage:
|
||||||
avatars: 'test1/avatars/'
|
avatars: 'test1/avatars/'
|
||||||
web_videos: 'test1/web-videos/'
|
web_videos: 'test1/web-videos/'
|
||||||
streaming_playlists: 'test1/streaming-playlists/'
|
streaming_playlists: 'test1/streaming-playlists/'
|
||||||
|
original_video_files: 'test1/original-video-files/'
|
||||||
redundancy: 'test1/redundancy/'
|
redundancy: 'test1/redundancy/'
|
||||||
logs: 'test1/logs/'
|
logs: 'test1/logs/'
|
||||||
previews: 'test1/previews/'
|
previews: 'test1/previews/'
|
||||||
|
|
|
@ -15,6 +15,7 @@ storage:
|
||||||
avatars: 'test2/avatars/'
|
avatars: 'test2/avatars/'
|
||||||
web_videos: 'test2/web-videos/'
|
web_videos: 'test2/web-videos/'
|
||||||
streaming_playlists: 'test2/streaming-playlists/'
|
streaming_playlists: 'test2/streaming-playlists/'
|
||||||
|
original_video_files: 'test2/original-video-files/'
|
||||||
redundancy: 'test2/redundancy/'
|
redundancy: 'test2/redundancy/'
|
||||||
logs: 'test2/logs/'
|
logs: 'test2/logs/'
|
||||||
previews: 'test2/previews/'
|
previews: 'test2/previews/'
|
||||||
|
|
|
@ -15,6 +15,7 @@ storage:
|
||||||
avatars: 'test3/avatars/'
|
avatars: 'test3/avatars/'
|
||||||
web_videos: 'test3/web-videos/'
|
web_videos: 'test3/web-videos/'
|
||||||
streaming_playlists: 'test3/streaming-playlists/'
|
streaming_playlists: 'test3/streaming-playlists/'
|
||||||
|
original_video_files: 'test3/original-video-files/'
|
||||||
redundancy: 'test3/redundancy/'
|
redundancy: 'test3/redundancy/'
|
||||||
logs: 'test3/logs/'
|
logs: 'test3/logs/'
|
||||||
previews: 'test3/previews/'
|
previews: 'test3/previews/'
|
||||||
|
|
|
@ -15,6 +15,7 @@ storage:
|
||||||
avatars: 'test4/avatars/'
|
avatars: 'test4/avatars/'
|
||||||
web_videos: 'test4/web-videos/'
|
web_videos: 'test4/web-videos/'
|
||||||
streaming_playlists: 'test4/streaming-playlists/'
|
streaming_playlists: 'test4/streaming-playlists/'
|
||||||
|
original_video_files: 'test4/original-video-files/'
|
||||||
redundancy: 'test4/redundancy/'
|
redundancy: 'test4/redundancy/'
|
||||||
logs: 'test4/logs/'
|
logs: 'test4/logs/'
|
||||||
previews: 'test4/previews/'
|
previews: 'test4/previews/'
|
||||||
|
|
|
@ -15,6 +15,7 @@ storage:
|
||||||
avatars: 'test5/avatars/'
|
avatars: 'test5/avatars/'
|
||||||
web_videos: 'test5/web-videos/'
|
web_videos: 'test5/web-videos/'
|
||||||
streaming_playlists: 'test5/streaming-playlists/'
|
streaming_playlists: 'test5/streaming-playlists/'
|
||||||
|
original_video_files: 'test5/original-video-files/'
|
||||||
redundancy: 'test5/redundancy/'
|
redundancy: 'test5/redundancy/'
|
||||||
logs: 'test5/logs/'
|
logs: 'test5/logs/'
|
||||||
previews: 'test5/previews/'
|
previews: 'test5/previews/'
|
||||||
|
|
|
@ -15,6 +15,7 @@ storage:
|
||||||
avatars: 'test6/avatars/'
|
avatars: 'test6/avatars/'
|
||||||
web_videos: 'test6/web-videos/'
|
web_videos: 'test6/web-videos/'
|
||||||
streaming_playlists: 'test6/streaming-playlists/'
|
streaming_playlists: 'test6/streaming-playlists/'
|
||||||
|
original_video_files: 'test6/original-video-files/'
|
||||||
redundancy: 'test6/redundancy/'
|
redundancy: 'test6/redundancy/'
|
||||||
logs: 'test6/logs/'
|
logs: 'test6/logs/'
|
||||||
previews: 'test6/previews/'
|
previews: 'test6/previews/'
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import {
|
import {
|
||||||
LiveVideoLatencyModeType,
|
LiveVideoLatencyModeType,
|
||||||
|
VideoFileMetadata,
|
||||||
VideoPrivacyType,
|
VideoPrivacyType,
|
||||||
VideoStateType,
|
VideoStateType,
|
||||||
VideoStreamingPlaylistType_Type
|
VideoStreamingPlaylistType_Type
|
||||||
|
@ -85,7 +86,17 @@ export interface VideoExportJSON {
|
||||||
}[]
|
}[]
|
||||||
|
|
||||||
source?: {
|
source?: {
|
||||||
filename: string
|
inputFilename: string
|
||||||
|
|
||||||
|
resolution: number
|
||||||
|
size: number
|
||||||
|
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
|
||||||
|
fps: number
|
||||||
|
|
||||||
|
metadata: VideoFileMetadata
|
||||||
}
|
}
|
||||||
|
|
||||||
archiveFiles: {
|
archiveFiles: {
|
||||||
|
|
|
@ -117,6 +117,10 @@ export interface CustomConfig {
|
||||||
transcoding: {
|
transcoding: {
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
|
|
||||||
|
originalFile: {
|
||||||
|
keep: boolean
|
||||||
|
}
|
||||||
|
|
||||||
allowAdditionalExtensions: boolean
|
allowAdditionalExtensions: boolean
|
||||||
allowAudioFiles: boolean
|
allowAudioFiles: boolean
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,23 @@
|
||||||
|
import { VideoFileMetadata } from './file/index.js'
|
||||||
|
import { VideoConstant } from './video-constant.model.js'
|
||||||
|
|
||||||
export interface VideoSource {
|
export interface VideoSource {
|
||||||
filename: string
|
inputFilename: string
|
||||||
|
|
||||||
|
resolution?: VideoConstant<number>
|
||||||
|
size?: number // Bytes
|
||||||
|
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
|
||||||
|
fileDownloadUrl: string
|
||||||
|
|
||||||
|
fps?: number
|
||||||
|
|
||||||
|
metadata?: VideoFileMetadata
|
||||||
|
|
||||||
createdAt: string | Date
|
createdAt: string | Date
|
||||||
|
|
||||||
|
// TODO: remove, deprecated in 6.1
|
||||||
|
filename: string
|
||||||
}
|
}
|
||||||
|
|
|
@ -106,6 +106,19 @@ export class ConfigCommand extends AbstractCommand {
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
keepSourceFile () {
|
||||||
|
return this.updateExistingSubConfig({
|
||||||
|
newConfig: {
|
||||||
|
transcoding: {
|
||||||
|
originalFile: {
|
||||||
|
keep: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
enableChannelSync () {
|
enableChannelSync () {
|
||||||
return this.setChannelSyncEnabled(true)
|
return this.setChannelSyncEnabled(true)
|
||||||
}
|
}
|
||||||
|
@ -234,13 +247,17 @@ export class ConfigCommand extends AbstractCommand {
|
||||||
webVideo?: boolean // default true
|
webVideo?: boolean // default true
|
||||||
hls?: boolean // default true
|
hls?: boolean // default true
|
||||||
with0p?: boolean // default false
|
with0p?: boolean // default false
|
||||||
|
keepOriginal?: boolean // default false
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const { webVideo = true, hls = true, with0p = false } = options
|
const { webVideo = true, hls = true, with0p = false, keepOriginal = false } = options
|
||||||
|
|
||||||
return this.updateExistingSubConfig({
|
return this.updateExistingSubConfig({
|
||||||
newConfig: {
|
newConfig: {
|
||||||
transcoding: {
|
transcoding: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
originalFile: {
|
||||||
|
keep: keepOriginal
|
||||||
|
},
|
||||||
|
|
||||||
allowAudioFiles: true,
|
allowAudioFiles: true,
|
||||||
allowAdditionalExtensions: true,
|
allowAdditionalExtensions: true,
|
||||||
|
@ -261,13 +278,17 @@ export class ConfigCommand extends AbstractCommand {
|
||||||
enableMinimumTranscoding (options: {
|
enableMinimumTranscoding (options: {
|
||||||
webVideo?: boolean // default true
|
webVideo?: boolean // default true
|
||||||
hls?: boolean // default true
|
hls?: boolean // default true
|
||||||
|
keepOriginal?: boolean // default false
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const { webVideo = true, hls = true } = options
|
const { webVideo = true, hls = true, keepOriginal = false } = options
|
||||||
|
|
||||||
return this.updateExistingSubConfig({
|
return this.updateExistingSubConfig({
|
||||||
newConfig: {
|
newConfig: {
|
||||||
transcoding: {
|
transcoding: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
originalFile: {
|
||||||
|
keep: keepOriginal
|
||||||
|
},
|
||||||
|
|
||||||
allowAudioFiles: true,
|
allowAudioFiles: true,
|
||||||
allowAdditionalExtensions: true,
|
allowAdditionalExtensions: true,
|
||||||
|
@ -560,6 +581,9 @@ export class ConfigCommand extends AbstractCommand {
|
||||||
},
|
},
|
||||||
transcoding: {
|
transcoding: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
originalFile: {
|
||||||
|
keep: false
|
||||||
|
},
|
||||||
remoteRunners: {
|
remoteRunners: {
|
||||||
enabled: false
|
enabled: false
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { randomInt } from 'crypto'
|
|
||||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||||
|
import { randomInt } from 'crypto'
|
||||||
import { makePostBodyRequest } from '../requests/index.js'
|
import { makePostBodyRequest } from '../requests/index.js'
|
||||||
|
|
||||||
export class ObjectStorageCommand {
|
export class ObjectStorageCommand {
|
||||||
|
@ -50,6 +50,14 @@ export class ObjectStorageCommand {
|
||||||
|
|
||||||
web_videos: {
|
web_videos: {
|
||||||
bucket_name: this.getMockWebVideosBucketName()
|
bucket_name: this.getMockWebVideosBucketName()
|
||||||
|
},
|
||||||
|
|
||||||
|
user_exports: {
|
||||||
|
bucket_name: this.getMockUserExportBucketName()
|
||||||
|
},
|
||||||
|
|
||||||
|
original_video_files: {
|
||||||
|
bucket_name: this.getMockOriginalFileBucketName()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -63,6 +71,14 @@ export class ObjectStorageCommand {
|
||||||
return `http://${this.getMockStreamingPlaylistsBucketName()}.${ObjectStorageCommand.getMockEndpointHost()}/`
|
return `http://${this.getMockStreamingPlaylistsBucketName()}.${ObjectStorageCommand.getMockEndpointHost()}/`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getMockUserExportBaseUrl () {
|
||||||
|
return `http://${this.getMockUserExportBucketName()}.${ObjectStorageCommand.getMockEndpointHost()}/`
|
||||||
|
}
|
||||||
|
|
||||||
|
getMockOriginalFileBaseUrl () {
|
||||||
|
return `http://${this.getMockOriginalFileBucketName()}.${ObjectStorageCommand.getMockEndpointHost()}/`
|
||||||
|
}
|
||||||
|
|
||||||
async prepareDefaultMockBuckets () {
|
async prepareDefaultMockBuckets () {
|
||||||
await this.createMockBucket(this.getMockStreamingPlaylistsBucketName())
|
await this.createMockBucket(this.getMockStreamingPlaylistsBucketName())
|
||||||
await this.createMockBucket(this.getMockWebVideosBucketName())
|
await this.createMockBucket(this.getMockWebVideosBucketName())
|
||||||
|
@ -100,6 +116,14 @@ export class ObjectStorageCommand {
|
||||||
return this.getMockBucketName(name)
|
return this.getMockBucketName(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getMockUserExportBucketName (name = 'user-exports') {
|
||||||
|
return this.getMockBucketName(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
getMockOriginalFileBucketName (name = 'original-video-files') {
|
||||||
|
return this.getMockBucketName(name)
|
||||||
|
}
|
||||||
|
|
||||||
getMockBucketName (name: string) {
|
getMockBucketName (name: string) {
|
||||||
return `${this.seed}-${name}`
|
return `${this.seed}-${name}`
|
||||||
}
|
}
|
||||||
|
|
|
@ -379,6 +379,7 @@ export class PeerTubeServer {
|
||||||
avatars: this.getDirectoryPath('avatars') + '/',
|
avatars: this.getDirectoryPath('avatars') + '/',
|
||||||
web_videos: this.getDirectoryPath('web-videos') + '/',
|
web_videos: this.getDirectoryPath('web-videos') + '/',
|
||||||
streaming_playlists: this.getDirectoryPath('streaming-playlists') + '/',
|
streaming_playlists: this.getDirectoryPath('streaming-playlists') + '/',
|
||||||
|
original_video_files: this.getDirectoryPath('original-video-files') + '/',
|
||||||
redundancy: this.getDirectoryPath('redundancy') + '/',
|
redundancy: this.getDirectoryPath('redundancy') + '/',
|
||||||
logs: this.getDirectoryPath('logs') + '/',
|
logs: this.getDirectoryPath('logs') + '/',
|
||||||
previews: this.getDirectoryPath('previews') + '/',
|
previews: this.getDirectoryPath('previews') + '/',
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { exec } from 'child_process'
|
|
||||||
import { copy, ensureDir, remove } from 'fs-extra/esm'
|
|
||||||
import { readdir, readFile } from 'fs/promises'
|
|
||||||
import { basename, join } from 'path'
|
|
||||||
import { wait } from '@peertube/peertube-core-utils'
|
import { wait } from '@peertube/peertube-core-utils'
|
||||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||||
import { getFileSize, isGithubCI, root } from '@peertube/peertube-node-utils'
|
import { isGithubCI, root } from '@peertube/peertube-node-utils'
|
||||||
|
import { exec } from 'child_process'
|
||||||
|
import { copy, ensureDir, remove } from 'fs-extra/esm'
|
||||||
|
import { readFile, readdir } from 'fs/promises'
|
||||||
|
import { basename, join } from 'path'
|
||||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||||
|
|
||||||
export class ServersCommand extends AbstractCommand {
|
export class ServersCommand extends AbstractCommand {
|
||||||
|
@ -84,6 +84,8 @@ export class ServersCommand extends AbstractCommand {
|
||||||
return files.length
|
return files.length
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
buildWebVideoFilePath (fileUrl: string) {
|
buildWebVideoFilePath (fileUrl: string) {
|
||||||
return this.buildDirectory(join('web-videos', basename(fileUrl)))
|
return this.buildDirectory(join('web-videos', basename(fileUrl)))
|
||||||
}
|
}
|
||||||
|
@ -92,13 +94,9 @@ export class ServersCommand extends AbstractCommand {
|
||||||
return this.buildDirectory(join('streaming-playlists', 'hls', videoUUID, basename(fileUrl)))
|
return this.buildDirectory(join('streaming-playlists', 'hls', videoUUID, basename(fileUrl)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
getLogContent () {
|
getLogContent () {
|
||||||
return readFile(this.buildDirectory('logs/peertube.log'))
|
return readFile(this.buildDirectory('logs/peertube.log'))
|
||||||
}
|
}
|
||||||
|
|
||||||
async getServerFileSize (subPath: string) {
|
|
||||||
const path = this.server.servers.buildDirectory(subPath)
|
|
||||||
|
|
||||||
return getFileSize(path)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { HttpStatusCode, ResultList, UserExport, UserExportRequestResult, UserExportState } from '@peertube/peertube-models'
|
|
||||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
|
||||||
import { wait } from '@peertube/peertube-core-utils'
|
import { wait } from '@peertube/peertube-core-utils'
|
||||||
import { unwrapBody } from '../requests/requests.js'
|
import { HttpStatusCode, ResultList, UserExport, UserExportRequestResult, UserExportState } from '@peertube/peertube-models'
|
||||||
|
import { writeFile } from 'fs/promises'
|
||||||
|
import { makeRawRequest, unwrapBody } from '../requests/requests.js'
|
||||||
|
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||||
|
|
||||||
export class UserExportsCommand extends AbstractCommand {
|
export class UserExportsCommand extends AbstractCommand {
|
||||||
|
|
||||||
|
@ -49,6 +50,22 @@ export class UserExportsCommand extends AbstractCommand {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async downloadLatestArchive (options: OverrideCommandOptions & {
|
||||||
|
userId: number
|
||||||
|
destination: string
|
||||||
|
}) {
|
||||||
|
const { data } = await this.list(options)
|
||||||
|
|
||||||
|
const res = await makeRawRequest({
|
||||||
|
url: data[0].privateDownloadUrl,
|
||||||
|
responseType: 'arraybuffer',
|
||||||
|
redirects: 1,
|
||||||
|
expectedStatus: HttpStatusCode.OK_200
|
||||||
|
})
|
||||||
|
|
||||||
|
await writeFile(options.destination, res.body)
|
||||||
|
}
|
||||||
|
|
||||||
async deleteAllArchives (options: OverrideCommandOptions & {
|
async deleteAllArchives (options: OverrideCommandOptions & {
|
||||||
userId: number
|
userId: number
|
||||||
}) {
|
}) {
|
||||||
|
|
|
@ -19,244 +19,7 @@ describe('Test config API validators', function () {
|
||||||
let server: PeerTubeServer
|
let server: PeerTubeServer
|
||||||
let userAccessToken: string
|
let userAccessToken: string
|
||||||
|
|
||||||
const updateParams: CustomConfig = {
|
let updateParams: CustomConfig
|
||||||
instance: {
|
|
||||||
name: 'PeerTube updated',
|
|
||||||
shortDescription: 'my short description',
|
|
||||||
description: 'my super description',
|
|
||||||
terms: 'my super terms',
|
|
||||||
codeOfConduct: 'my super coc',
|
|
||||||
|
|
||||||
creationReason: 'my super reason',
|
|
||||||
moderationInformation: 'my super moderation information',
|
|
||||||
administrator: 'Kuja',
|
|
||||||
maintenanceLifetime: 'forever',
|
|
||||||
businessModel: 'my super business model',
|
|
||||||
hardwareInformation: '2vCore 3GB RAM',
|
|
||||||
|
|
||||||
languages: [ 'en', 'es' ],
|
|
||||||
categories: [ 1, 2 ],
|
|
||||||
|
|
||||||
isNSFW: true,
|
|
||||||
defaultNSFWPolicy: 'blur',
|
|
||||||
|
|
||||||
defaultClientRoute: '/videos/recently-added',
|
|
||||||
|
|
||||||
customizations: {
|
|
||||||
javascript: 'alert("coucou")',
|
|
||||||
css: 'body { background-color: red; }'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
theme: {
|
|
||||||
default: 'default'
|
|
||||||
},
|
|
||||||
services: {
|
|
||||||
twitter: {
|
|
||||||
username: '@MySuperUsername'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
client: {
|
|
||||||
videos: {
|
|
||||||
miniature: {
|
|
||||||
preferAuthorDisplayName: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
menu: {
|
|
||||||
login: {
|
|
||||||
redirectOnSingleExternalAuth: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
cache: {
|
|
||||||
previews: {
|
|
||||||
size: 2
|
|
||||||
},
|
|
||||||
captions: {
|
|
||||||
size: 3
|
|
||||||
},
|
|
||||||
torrents: {
|
|
||||||
size: 4
|
|
||||||
},
|
|
||||||
storyboards: {
|
|
||||||
size: 5
|
|
||||||
}
|
|
||||||
},
|
|
||||||
signup: {
|
|
||||||
enabled: false,
|
|
||||||
limit: 5,
|
|
||||||
requiresApproval: false,
|
|
||||||
requiresEmailVerification: false,
|
|
||||||
minimumAge: 16
|
|
||||||
},
|
|
||||||
admin: {
|
|
||||||
email: 'superadmin1@example.com'
|
|
||||||
},
|
|
||||||
contactForm: {
|
|
||||||
enabled: false
|
|
||||||
},
|
|
||||||
user: {
|
|
||||||
history: {
|
|
||||||
videos: {
|
|
||||||
enabled: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
videoQuota: 5242881,
|
|
||||||
videoQuotaDaily: 318742,
|
|
||||||
defaultChannelName: 'Main $1 channel'
|
|
||||||
},
|
|
||||||
videoChannels: {
|
|
||||||
maxPerUser: 20
|
|
||||||
},
|
|
||||||
transcoding: {
|
|
||||||
enabled: true,
|
|
||||||
remoteRunners: {
|
|
||||||
enabled: true
|
|
||||||
},
|
|
||||||
allowAdditionalExtensions: true,
|
|
||||||
allowAudioFiles: true,
|
|
||||||
concurrency: 1,
|
|
||||||
threads: 1,
|
|
||||||
profile: 'vod_profile',
|
|
||||||
resolutions: {
|
|
||||||
'0p': false,
|
|
||||||
'144p': false,
|
|
||||||
'240p': false,
|
|
||||||
'360p': true,
|
|
||||||
'480p': true,
|
|
||||||
'720p': false,
|
|
||||||
'1080p': false,
|
|
||||||
'1440p': false,
|
|
||||||
'2160p': false
|
|
||||||
},
|
|
||||||
alwaysTranscodeOriginalResolution: false,
|
|
||||||
webVideos: {
|
|
||||||
enabled: true
|
|
||||||
},
|
|
||||||
hls: {
|
|
||||||
enabled: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
live: {
|
|
||||||
enabled: true,
|
|
||||||
|
|
||||||
allowReplay: false,
|
|
||||||
latencySetting: {
|
|
||||||
enabled: false
|
|
||||||
},
|
|
||||||
maxDuration: 30,
|
|
||||||
maxInstanceLives: -1,
|
|
||||||
maxUserLives: 50,
|
|
||||||
|
|
||||||
transcoding: {
|
|
||||||
enabled: true,
|
|
||||||
remoteRunners: {
|
|
||||||
enabled: true
|
|
||||||
},
|
|
||||||
threads: 4,
|
|
||||||
profile: 'live_profile',
|
|
||||||
resolutions: {
|
|
||||||
'144p': true,
|
|
||||||
'240p': true,
|
|
||||||
'360p': true,
|
|
||||||
'480p': true,
|
|
||||||
'720p': true,
|
|
||||||
'1080p': true,
|
|
||||||
'1440p': true,
|
|
||||||
'2160p': true
|
|
||||||
},
|
|
||||||
alwaysTranscodeOriginalResolution: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
videoStudio: {
|
|
||||||
enabled: true,
|
|
||||||
remoteRunners: {
|
|
||||||
enabled: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
videoFile: {
|
|
||||||
update: {
|
|
||||||
enabled: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
import: {
|
|
||||||
videos: {
|
|
||||||
concurrency: 1,
|
|
||||||
http: {
|
|
||||||
enabled: false
|
|
||||||
},
|
|
||||||
torrent: {
|
|
||||||
enabled: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
videoChannelSynchronization: {
|
|
||||||
enabled: false,
|
|
||||||
maxPerUser: 10
|
|
||||||
},
|
|
||||||
users: {
|
|
||||||
enabled: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
export: {
|
|
||||||
users: {
|
|
||||||
enabled: false,
|
|
||||||
maxUserVideoQuota: 40,
|
|
||||||
exportExpiration: 10
|
|
||||||
}
|
|
||||||
},
|
|
||||||
trending: {
|
|
||||||
videos: {
|
|
||||||
algorithms: {
|
|
||||||
enabled: [ 'hot', 'most-viewed', 'most-liked' ],
|
|
||||||
default: 'most-viewed'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
autoBlacklist: {
|
|
||||||
videos: {
|
|
||||||
ofUsers: {
|
|
||||||
enabled: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
followers: {
|
|
||||||
instance: {
|
|
||||||
enabled: false,
|
|
||||||
manualApproval: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
followings: {
|
|
||||||
instance: {
|
|
||||||
autoFollowBack: {
|
|
||||||
enabled: true
|
|
||||||
},
|
|
||||||
autoFollowIndex: {
|
|
||||||
enabled: true,
|
|
||||||
indexUrl: 'https://index.example.com'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
broadcastMessage: {
|
|
||||||
enabled: true,
|
|
||||||
dismissable: true,
|
|
||||||
message: 'super message',
|
|
||||||
level: 'warning'
|
|
||||||
},
|
|
||||||
search: {
|
|
||||||
remoteUri: {
|
|
||||||
users: true,
|
|
||||||
anonymous: true
|
|
||||||
},
|
|
||||||
searchIndex: {
|
|
||||||
enabled: true,
|
|
||||||
url: 'https://search.joinpeertube.org',
|
|
||||||
disableLocalSearch: true,
|
|
||||||
isDefaultSearch: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
storyboards: {
|
|
||||||
enabled: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
@ -266,6 +29,7 @@ describe('Test config API validators', function () {
|
||||||
server = await createSingleServer(1)
|
server = await createSingleServer(1)
|
||||||
|
|
||||||
await setAccessTokensToServers([ server ])
|
await setAccessTokensToServers([ server ])
|
||||||
|
updateParams = await server.config.getCustomConfig()
|
||||||
|
|
||||||
const user = {
|
const user = {
|
||||||
username: 'user1',
|
username: 'user1',
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
import { HttpStatusCode, VideoSource } from '@peertube/peertube-models'
|
||||||
import {
|
import {
|
||||||
|
PeerTubeServer,
|
||||||
cleanupTests,
|
cleanupTests,
|
||||||
createSingleServer,
|
createSingleServer,
|
||||||
PeerTubeServer,
|
makeRawRequest,
|
||||||
setAccessTokensToServers,
|
setAccessTokensToServers,
|
||||||
setDefaultVideoChannel,
|
setDefaultVideoChannel,
|
||||||
waitJobs
|
waitJobs
|
||||||
|
@ -148,6 +149,66 @@ describe('Test video sources API validator', function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('When downloading the source file', function () {
|
||||||
|
let videoFileToken: string
|
||||||
|
let videoId: string
|
||||||
|
let source: VideoSource
|
||||||
|
let user3: string
|
||||||
|
let user4: string
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
this.timeout(60000)
|
||||||
|
|
||||||
|
user3 = await server.users.generateUserAndToken('user3')
|
||||||
|
user4 = await server.users.generateUserAndToken('user4')
|
||||||
|
|
||||||
|
await server.config.enableMinimumTranscoding({ hls: true, keepOriginal: true, webVideo: true })
|
||||||
|
|
||||||
|
const { uuid } = await server.videos.quickUpload({ name: 'video', token: user3 })
|
||||||
|
|
||||||
|
videoId = uuid
|
||||||
|
videoFileToken = await server.videoToken.getVideoFileToken({ videoId: uuid, token: user3 })
|
||||||
|
|
||||||
|
await waitJobs([ server ])
|
||||||
|
|
||||||
|
source = await server.videos.getSource({ id: videoId, token: user3 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with an invalid filename', async function () {
|
||||||
|
await makeRawRequest({ url: server.url + '/download/original-video-files/hello.mp4', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail without header token or video file token', async function () {
|
||||||
|
await makeRawRequest({ url: source.fileDownloadUrl, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with an invalid header token', async function () {
|
||||||
|
await makeRawRequest({ url: source.fileDownloadUrl, token: 'toto', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with an invalid video file token', async function () {
|
||||||
|
await makeRawRequest({ url: source.fileDownloadUrl, query: { videoFileToken: 'toto' }, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with header token of another user', async function () {
|
||||||
|
await makeRawRequest({ url: source.fileDownloadUrl, token: user4, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with video file token of another user', async function () {
|
||||||
|
const videoFileToken = await server.videoToken.getVideoFileToken({ videoId: uuid, token: user4 })
|
||||||
|
|
||||||
|
await makeRawRequest({ url: source.fileDownloadUrl, query: { videoFileToken }, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should succeed with a valid header token', async function () {
|
||||||
|
await makeRawRequest({ url: source.fileDownloadUrl, token: user3, expectedStatus: HttpStatusCode.OK_200 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should succeed with a valid header token', async function () {
|
||||||
|
await makeRawRequest({ url: source.fileDownloadUrl, query: { videoFileToken }, expectedStatus: HttpStatusCode.OK_200 })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
after(async function () {
|
after(async function () {
|
||||||
await cleanupTests([ server ])
|
await cleanupTests([ server ])
|
||||||
})
|
})
|
||||||
|
|
|
@ -84,6 +84,7 @@ function checkInitialConfig (server: PeerTubeServer, data: CustomConfig) {
|
||||||
expect(data.transcoding.alwaysTranscodeOriginalResolution).to.be.true
|
expect(data.transcoding.alwaysTranscodeOriginalResolution).to.be.true
|
||||||
expect(data.transcoding.webVideos.enabled).to.be.true
|
expect(data.transcoding.webVideos.enabled).to.be.true
|
||||||
expect(data.transcoding.hls.enabled).to.be.true
|
expect(data.transcoding.hls.enabled).to.be.true
|
||||||
|
expect(data.transcoding.originalFile.keep).to.be.false
|
||||||
|
|
||||||
expect(data.live.enabled).to.be.false
|
expect(data.live.enabled).to.be.false
|
||||||
expect(data.live.allowReplay).to.be.false
|
expect(data.live.allowReplay).to.be.false
|
||||||
|
@ -205,6 +206,7 @@ function checkUpdatedConfig (data: CustomConfig) {
|
||||||
expect(data.transcoding.alwaysTranscodeOriginalResolution).to.be.false
|
expect(data.transcoding.alwaysTranscodeOriginalResolution).to.be.false
|
||||||
expect(data.transcoding.hls.enabled).to.be.false
|
expect(data.transcoding.hls.enabled).to.be.false
|
||||||
expect(data.transcoding.webVideos.enabled).to.be.true
|
expect(data.transcoding.webVideos.enabled).to.be.true
|
||||||
|
expect(data.transcoding.originalFile.keep).to.be.true
|
||||||
|
|
||||||
expect(data.live.enabled).to.be.true
|
expect(data.live.enabled).to.be.true
|
||||||
expect(data.live.allowReplay).to.be.true
|
expect(data.live.allowReplay).to.be.true
|
||||||
|
@ -349,6 +351,9 @@ const newCustomConfig: CustomConfig = {
|
||||||
remoteRunners: {
|
remoteRunners: {
|
||||||
enabled: true
|
enabled: true
|
||||||
},
|
},
|
||||||
|
originalFile: {
|
||||||
|
keep: true
|
||||||
|
},
|
||||||
allowAdditionalExtensions: true,
|
allowAdditionalExtensions: true,
|
||||||
allowAudioFiles: true,
|
allowAudioFiles: true,
|
||||||
threads: 1,
|
threads: 1,
|
||||||
|
|
|
@ -1,14 +1,6 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||||
|
|
||||||
import { MockSmtpServer } from '@tests/shared/mock-servers/index.js'
|
import { wait } from '@peertube/peertube-core-utils'
|
||||||
import {
|
|
||||||
cleanupTests, getRedirectionUrl, makeActivityPubRawRequest,
|
|
||||||
makeRawRequest,
|
|
||||||
ObjectStorageCommand,
|
|
||||||
PeerTubeServer,
|
|
||||||
waitJobs
|
|
||||||
} from '@peertube/peertube-server-commands'
|
|
||||||
import { expect } from 'chai'
|
|
||||||
import {
|
import {
|
||||||
AccountExportJSON, ActivityPubActor,
|
AccountExportJSON, ActivityPubActor,
|
||||||
ActivityPubOrderedCollection,
|
ActivityPubOrderedCollection,
|
||||||
|
@ -34,6 +26,15 @@ import {
|
||||||
VideoPlaylistType,
|
VideoPlaylistType,
|
||||||
VideoPrivacy
|
VideoPrivacy
|
||||||
} from '@peertube/peertube-models'
|
} from '@peertube/peertube-models'
|
||||||
|
import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils'
|
||||||
|
import {
|
||||||
|
cleanupTests, getRedirectionUrl, makeActivityPubRawRequest,
|
||||||
|
makeRawRequest,
|
||||||
|
ObjectStorageCommand,
|
||||||
|
PeerTubeServer,
|
||||||
|
waitJobs
|
||||||
|
} from '@peertube/peertube-server-commands'
|
||||||
|
import { expectStartWith } from '@tests/shared/checks.js'
|
||||||
import {
|
import {
|
||||||
checkExportFileExists,
|
checkExportFileExists,
|
||||||
checkFileExistsInZIP,
|
checkFileExistsInZIP,
|
||||||
|
@ -44,8 +45,8 @@ import {
|
||||||
prepareImportExportTests,
|
prepareImportExportTests,
|
||||||
regenerateExport
|
regenerateExport
|
||||||
} from '@tests/shared/import-export.js'
|
} from '@tests/shared/import-export.js'
|
||||||
import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils'
|
import { MockSmtpServer } from '@tests/shared/mock-servers/index.js'
|
||||||
import { wait } from '@peertube/peertube-core-utils'
|
import { expect } from 'chai'
|
||||||
|
|
||||||
function runTest (withObjectStorage: boolean) {
|
function runTest (withObjectStorage: boolean) {
|
||||||
let server: PeerTubeServer
|
let server: PeerTubeServer
|
||||||
|
@ -69,10 +70,12 @@ function runTest (withObjectStorage: boolean) {
|
||||||
|
|
||||||
let noahExportId: number
|
let noahExportId: number
|
||||||
|
|
||||||
|
let objectStorage: ObjectStorageCommand
|
||||||
|
|
||||||
before(async function () {
|
before(async function () {
|
||||||
this.timeout(240000)
|
this.timeout(240000)
|
||||||
|
|
||||||
const objectStorage = withObjectStorage
|
objectStorage = withObjectStorage
|
||||||
? new ObjectStorageCommand()
|
? new ObjectStorageCommand()
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
@ -126,6 +129,10 @@ function runTest (withObjectStorage: boolean) {
|
||||||
expect(data[0].size).to.be.greaterThan(0)
|
expect(data[0].size).to.be.greaterThan(0)
|
||||||
expect(data[0].state.id).to.equal(UserExportState.COMPLETED)
|
expect(data[0].state.id).to.equal(UserExportState.COMPLETED)
|
||||||
expect(data[0].state.label).to.equal('Completed')
|
expect(data[0].state.label).to.equal('Completed')
|
||||||
|
|
||||||
|
if (objectStorage) {
|
||||||
|
expectStartWith(await getRedirectionUrl(data[0].privateDownloadUrl), objectStorage.getMockUserExportBaseUrl())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await waitJobs([ server ])
|
await waitJobs([ server ])
|
||||||
|
@ -526,6 +533,14 @@ function runTest (withObjectStorage: boolean) {
|
||||||
for (const url of urls) {
|
for (const url of urls) {
|
||||||
await makeRawRequest({ url, expectedStatus: HttpStatusCode.OK_200 })
|
await makeRawRequest({ url, expectedStatus: HttpStatusCode.OK_200 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expect(publicVideo.source.inputFilename).to.equal('video_short.webm')
|
||||||
|
expect(publicVideo.source.fps).to.equal(25)
|
||||||
|
expect(publicVideo.source.height).to.equal(720)
|
||||||
|
expect(publicVideo.source.width).to.equal(1280)
|
||||||
|
expect(publicVideo.source.metadata?.streams).to.exist
|
||||||
|
expect(publicVideo.source.resolution).to.equal(720)
|
||||||
|
expect(publicVideo.source.size).to.equal(218910)
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,11 +1,5 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||||
|
|
||||||
import { MockSmtpServer } from '@tests/shared/mock-servers/index.js'
|
|
||||||
import {
|
|
||||||
cleanupTests, makeRawRequest,
|
|
||||||
ObjectStorageCommand,
|
|
||||||
PeerTubeServer, waitJobs
|
|
||||||
} from '@peertube/peertube-server-commands'
|
|
||||||
import {
|
import {
|
||||||
HttpStatusCode,
|
HttpStatusCode,
|
||||||
LiveVideoLatencyMode,
|
LiveVideoLatencyMode,
|
||||||
|
@ -17,14 +11,20 @@ import {
|
||||||
VideoPrivacy,
|
VideoPrivacy,
|
||||||
VideoState
|
VideoState
|
||||||
} from '@peertube/peertube-models'
|
} from '@peertube/peertube-models'
|
||||||
import { prepareImportExportTests } from '@tests/shared/import-export.js'
|
|
||||||
import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils'
|
import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils'
|
||||||
import { writeFile } from 'fs/promises'
|
import {
|
||||||
import { join } from 'path'
|
ObjectStorageCommand,
|
||||||
import { expect } from 'chai'
|
PeerTubeServer,
|
||||||
import { testImage, testAvatarSize } from '@tests/shared/checks.js'
|
cleanupTests,
|
||||||
import { completeVideoCheck } from '@tests/shared/videos.js'
|
waitJobs
|
||||||
|
} from '@peertube/peertube-server-commands'
|
||||||
|
import { testAvatarSize, testImage } from '@tests/shared/checks.js'
|
||||||
|
import { prepareImportExportTests } from '@tests/shared/import-export.js'
|
||||||
|
import { MockSmtpServer } from '@tests/shared/mock-servers/index.js'
|
||||||
import { completeCheckHlsPlaylist } from '@tests/shared/streaming-playlists.js'
|
import { completeCheckHlsPlaylist } from '@tests/shared/streaming-playlists.js'
|
||||||
|
import { completeVideoCheck } from '@tests/shared/videos.js'
|
||||||
|
import { expect } from 'chai'
|
||||||
|
import { join } from 'path'
|
||||||
|
|
||||||
function runTest (withObjectStorage: boolean) {
|
function runTest (withObjectStorage: boolean) {
|
||||||
let server: PeerTubeServer
|
let server: PeerTubeServer
|
||||||
|
@ -115,17 +115,8 @@ function runTest (withObjectStorage: boolean) {
|
||||||
await server.userExports.request({ userId: noahId, withVideoFiles: true })
|
await server.userExports.request({ userId: noahId, withVideoFiles: true })
|
||||||
await server.userExports.waitForCreation({ userId: noahId })
|
await server.userExports.waitForCreation({ userId: noahId })
|
||||||
|
|
||||||
const { data } = await server.userExports.list({ userId: noahId })
|
|
||||||
|
|
||||||
const res = await makeRawRequest({
|
|
||||||
url: data[0].privateDownloadUrl,
|
|
||||||
responseType: 'arraybuffer',
|
|
||||||
redirects: 1,
|
|
||||||
expectedStatus: HttpStatusCode.OK_200
|
|
||||||
})
|
|
||||||
|
|
||||||
archivePath = join(server.getDirectoryPath('tmp'), 'archive.zip')
|
archivePath = join(server.getDirectoryPath('tmp'), 'archive.zip')
|
||||||
await writeFile(archivePath, res.body)
|
await server.userExports.downloadLatestArchive({ userId: noahId, destination: archivePath })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should import an archive with video files', async function () {
|
it('Should import an archive with video files', async function () {
|
||||||
|
@ -444,6 +435,11 @@ function runTest (withObjectStorage: boolean) {
|
||||||
|
|
||||||
const source = await remoteServer.videos.getSource({ id: otherVideo.uuid })
|
const source = await remoteServer.videos.getSource({ id: otherVideo.uuid })
|
||||||
expect(source.filename).to.equal('video_short.webm')
|
expect(source.filename).to.equal('video_short.webm')
|
||||||
|
expect(source.inputFilename).to.equal('video_short.webm')
|
||||||
|
expect(source.fileDownloadUrl).to.not.exist
|
||||||
|
|
||||||
|
expect(source.metadata?.format).to.exist
|
||||||
|
expect(source.metadata?.streams).to.be.an('array')
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -572,6 +568,57 @@ function runTest (withObjectStorage: boolean) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('Should import original file if included in the export', async function () {
|
||||||
|
this.timeout(120000)
|
||||||
|
|
||||||
|
await server.config.enableMinimumTranscoding({ keepOriginal: true })
|
||||||
|
await remoteServer.config.keepSourceFile()
|
||||||
|
|
||||||
|
const archivePath = join(server.getDirectoryPath('tmp'), 'archive2.zip')
|
||||||
|
const fixture = 'video_short1.webm'
|
||||||
|
|
||||||
|
{
|
||||||
|
const { token, userId } = await server.users.generate('claire')
|
||||||
|
|
||||||
|
await server.videos.quickUpload({ name: 'claire video', token, fixture })
|
||||||
|
|
||||||
|
await waitJobs([ server ])
|
||||||
|
|
||||||
|
await server.userExports.request({ userId, token, withVideoFiles: true })
|
||||||
|
await server.userExports.waitForCreation({ userId, token })
|
||||||
|
|
||||||
|
await server.userExports.downloadLatestArchive({ userId, token, destination: archivePath })
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const { token, userId } = await remoteServer.users.generate('external_claire')
|
||||||
|
|
||||||
|
await remoteServer.userImports.importArchive({ fixture: archivePath, userId, token })
|
||||||
|
await waitJobs([ remoteServer ])
|
||||||
|
|
||||||
|
{
|
||||||
|
const { data } = await remoteServer.videos.listMyVideos({ token })
|
||||||
|
expect(data).to.have.lengthOf(1)
|
||||||
|
|
||||||
|
const source = await remoteServer.videos.getSource({ id: data[0].id })
|
||||||
|
expect(source.filename).to.equal(fixture)
|
||||||
|
expect(source.inputFilename).to.equal(fixture)
|
||||||
|
expect(source.fileDownloadUrl).to.exist
|
||||||
|
|
||||||
|
expect(source.metadata?.format).to.exist
|
||||||
|
expect(source.metadata?.streams).to.be.an('array')
|
||||||
|
expect(source.metadata.format['format_name']).to.include('webm')
|
||||||
|
|
||||||
|
expect(source.createdAt).to.exist
|
||||||
|
expect(source.fps).to.equal(25)
|
||||||
|
expect(source.height).to.equal(720)
|
||||||
|
expect(source.width).to.equal(1280)
|
||||||
|
expect(source.resolution.id).to.equal(720)
|
||||||
|
expect(source.size).to.equal(572456)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
after(async function () {
|
after(async function () {
|
||||||
MockSmtpServer.Instance.kill()
|
MockSmtpServer.Instance.kill()
|
||||||
|
|
||||||
|
|
|
@ -1,24 +1,26 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||||
import { expect } from 'chai'
|
|
||||||
import { getAllFiles } from '@peertube/peertube-core-utils'
|
import { getAllFiles } from '@peertube/peertube-core-utils'
|
||||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
import { HttpStatusCode, VideoPrivacy } from '@peertube/peertube-models'
|
||||||
import { expectStartWith } from '@tests/shared/checks.js'
|
|
||||||
import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils'
|
import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils'
|
||||||
import {
|
import {
|
||||||
cleanupTests,
|
|
||||||
createMultipleServers,
|
|
||||||
doubleFollow,
|
|
||||||
makeGetRequest,
|
|
||||||
makeRawRequest,
|
|
||||||
ObjectStorageCommand,
|
ObjectStorageCommand,
|
||||||
PeerTubeServer,
|
PeerTubeServer,
|
||||||
|
cleanupTests,
|
||||||
|
createMultipleServers,
|
||||||
|
doubleFollow, makeGetRequest,
|
||||||
|
makeRawRequest,
|
||||||
setAccessTokensToServers,
|
setAccessTokensToServers,
|
||||||
setDefaultAccountAvatar,
|
setDefaultAccountAvatar,
|
||||||
setDefaultVideoChannel,
|
setDefaultVideoChannel,
|
||||||
waitJobs
|
waitJobs
|
||||||
} from '@peertube/peertube-server-commands'
|
} from '@peertube/peertube-server-commands'
|
||||||
|
import { expectStartWith } from '@tests/shared/checks.js'
|
||||||
|
import { checkDirectoryIsEmpty } from '@tests/shared/directories.js'
|
||||||
|
import { FIXTURE_URLS } from '@tests/shared/tests.js'
|
||||||
|
import { checkSourceFile } from '@tests/shared/videos.js'
|
||||||
|
import { expect } from 'chai'
|
||||||
|
|
||||||
describe('Test a video file replacement', function () {
|
describe('Test video source management', function () {
|
||||||
let servers: PeerTubeServer[] = []
|
let servers: PeerTubeServer[] = []
|
||||||
|
|
||||||
let replaceDate: Date
|
let replaceDate: Date
|
||||||
|
@ -36,6 +38,7 @@ describe('Test a video file replacement', function () {
|
||||||
await setDefaultAccountAvatar(servers)
|
await setDefaultAccountAvatar(servers)
|
||||||
|
|
||||||
await servers[0].config.enableFileUpdate()
|
await servers[0].config.enableFileUpdate()
|
||||||
|
await servers[0].config.enableMinimumTranscoding()
|
||||||
|
|
||||||
userToken = await servers[0].users.generateUserAndToken('user1')
|
userToken = await servers[0].users.generateUserAndToken('user1')
|
||||||
|
|
||||||
|
@ -44,30 +47,95 @@ describe('Test a video file replacement', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Getting latest video source', () => {
|
describe('Getting latest video source', () => {
|
||||||
const fixture = 'video_short.webm'
|
const fixture1 = 'video_short.webm'
|
||||||
|
const fixture2 = 'video_short1.webm'
|
||||||
|
|
||||||
const uuids: string[] = []
|
const uuids: string[] = []
|
||||||
|
|
||||||
it('Should get the source filename with legacy upload', async function () {
|
it('Should get the source filename with legacy upload with disabled keep original file', async function () {
|
||||||
this.timeout(30000)
|
this.timeout(30000)
|
||||||
|
|
||||||
const { uuid } = await servers[0].videos.upload({ attributes: { name: 'my video', fixture }, mode: 'legacy' })
|
const { uuid } = await servers[0].videos.upload({ attributes: { name: 'my video', fixture: fixture1 }, mode: 'legacy' })
|
||||||
uuids.push(uuid)
|
uuids.push(uuid)
|
||||||
|
|
||||||
|
await waitJobs(servers)
|
||||||
|
|
||||||
const source = await servers[0].videos.getSource({ id: uuid })
|
const source = await servers[0].videos.getSource({ id: uuid })
|
||||||
expect(source.filename).to.equal(fixture)
|
expect(source.filename).to.equal(fixture1)
|
||||||
|
expect(source.inputFilename).to.equal(fixture1)
|
||||||
|
expect(source.fileDownloadUrl).to.be.null
|
||||||
|
|
||||||
|
expect(source.createdAt).to.exist
|
||||||
|
expect(source.fps).to.equal(25)
|
||||||
|
expect(source.height).to.equal(720)
|
||||||
|
expect(source.width).to.equal(1280)
|
||||||
|
expect(source.resolution.id).to.equal(720)
|
||||||
|
expect(source.size).to.equal(218910)
|
||||||
|
|
||||||
|
expect(source.metadata?.format).to.exist
|
||||||
|
expect(source.metadata?.streams).to.be.an('array')
|
||||||
|
|
||||||
|
await checkDirectoryIsEmpty(servers[0], 'original-video-files')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should get the source filename with resumable upload', async function () {
|
it('Should get the source filename with resumable upload and enabled keep original file', async function () {
|
||||||
this.timeout(30000)
|
this.timeout(30000)
|
||||||
|
|
||||||
const { uuid } = await servers[0].videos.upload({ attributes: { name: 'my video', fixture }, mode: 'resumable' })
|
await servers[0].config.keepSourceFile()
|
||||||
|
|
||||||
|
const { uuid } = await servers[0].videos.upload({ attributes: { name: 'my video', fixture: fixture2 }, mode: 'resumable' })
|
||||||
uuids.push(uuid)
|
uuids.push(uuid)
|
||||||
|
|
||||||
|
await waitJobs(servers)
|
||||||
|
|
||||||
const source = await servers[0].videos.getSource({ id: uuid })
|
const source = await servers[0].videos.getSource({ id: uuid })
|
||||||
expect(source.filename).to.equal(fixture)
|
expect(source.filename).to.equal(fixture2)
|
||||||
|
expect(source.inputFilename).to.equal(fixture2)
|
||||||
|
expect(source.fileDownloadUrl).to.exist
|
||||||
|
|
||||||
|
expect(source.createdAt).to.exist
|
||||||
|
expect(source.fps).to.equal(25)
|
||||||
|
expect(source.height).to.equal(720)
|
||||||
|
expect(source.width).to.equal(1280)
|
||||||
|
expect(source.resolution.id).to.equal(720)
|
||||||
|
expect(source.size).to.equal(572456)
|
||||||
|
|
||||||
|
expect(source.metadata?.format).to.exist
|
||||||
|
expect(source.metadata?.streams).to.be.an('array')
|
||||||
})
|
})
|
||||||
|
|
||||||
after(async function () {
|
it('Should have kept original video file', async function () {
|
||||||
|
await checkSourceFile({ server: servers[0], fsCount: 1, fixture: fixture2, uuid: uuids[uuids.length - 1] })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should transcode a file but do not replace original file', async function () {
|
||||||
|
await servers[0].videos.runTranscoding({ transcodingType: 'web-video', videoId: uuids[0] })
|
||||||
|
await servers[0].videos.runTranscoding({ transcodingType: 'web-video', videoId: uuids[1] })
|
||||||
|
|
||||||
|
await checkSourceFile({ server: servers[0], fsCount: 1, fixture: fixture2, uuid: uuids[uuids.length - 1] })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should also keep audio files', async function () {
|
||||||
|
const fixture = 'sample.ogg'
|
||||||
|
const { uuid } = await servers[0].videos.quickUpload({ name: 'audio', fixture })
|
||||||
|
uuids.push(uuid)
|
||||||
|
|
||||||
|
await waitJobs(servers)
|
||||||
|
const source = await checkSourceFile({ server: servers[0], fsCount: 2, fixture, uuid })
|
||||||
|
|
||||||
|
expect(source.createdAt).to.exist
|
||||||
|
expect(source.fps).to.equal(0)
|
||||||
|
expect(source.height).to.equal(0)
|
||||||
|
expect(source.width).to.equal(0)
|
||||||
|
expect(source.resolution.id).to.equal(0)
|
||||||
|
expect(source.resolution.label).to.equal('Audio')
|
||||||
|
expect(source.size).to.equal(105243)
|
||||||
|
|
||||||
|
expect(source.metadata?.format).to.exist
|
||||||
|
expect(source.metadata?.streams).to.be.an('array')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should delete all videos and do not have original files anymore', async function () {
|
||||||
this.timeout(60000)
|
this.timeout(60000)
|
||||||
|
|
||||||
for (const uuid of uuids) {
|
for (const uuid of uuids) {
|
||||||
|
@ -75,6 +143,23 @@ describe('Test a video file replacement', function () {
|
||||||
}
|
}
|
||||||
|
|
||||||
await waitJobs(servers)
|
await waitJobs(servers)
|
||||||
|
|
||||||
|
await checkDirectoryIsEmpty(servers[0], 'original-video-files')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should not have source on import', async function () {
|
||||||
|
const { video: { uuid } } = await servers[0].videoImports.importVideo({
|
||||||
|
attributes: {
|
||||||
|
channelId: servers[0].store.channel.id,
|
||||||
|
targetUrl: FIXTURE_URLS.goodVideo,
|
||||||
|
privacy: VideoPrivacy.PUBLIC
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await waitJobs(servers)
|
||||||
|
|
||||||
|
await servers[0].videos.getSource({ id: uuid, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||||
|
await checkDirectoryIsEmpty(servers[0], 'original-video-files')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -110,18 +195,25 @@ describe('Test a video file replacement', function () {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('Should not have kept original video file', async function () {
|
||||||
|
await checkDirectoryIsEmpty(servers[0], 'original-video-files')
|
||||||
|
})
|
||||||
|
|
||||||
it('Should replace a video file with transcoding enabled', async function () {
|
it('Should replace a video file with transcoding enabled', async function () {
|
||||||
this.timeout(240000)
|
this.timeout(240000)
|
||||||
|
|
||||||
const previousPaths: string[] = []
|
const previousPaths: string[] = []
|
||||||
|
|
||||||
await servers[0].config.enableTranscoding({ hls: true, webVideo: true, with0p: true })
|
await servers[0].config.enableTranscoding({ hls: true, webVideo: true, with0p: true, keepOriginal: true })
|
||||||
|
|
||||||
const { uuid: videoUUID } = await servers[0].videos.quickUpload({ name: 'fs with transcoding', fixture: 'video_short_720p.mp4' })
|
const uploadFixture = 'video_short_720p.mp4'
|
||||||
|
const { uuid: videoUUID } = await servers[0].videos.quickUpload({ name: 'fs with transcoding', fixture: uploadFixture })
|
||||||
uuid = videoUUID
|
uuid = videoUUID
|
||||||
|
|
||||||
await waitJobs(servers)
|
await waitJobs(servers)
|
||||||
|
|
||||||
|
await checkSourceFile({ server: servers[0], fsCount: 1, uuid, fixture: uploadFixture })
|
||||||
|
|
||||||
for (const server of servers) {
|
for (const server of servers) {
|
||||||
const video = await server.videos.get({ id: uuid })
|
const video = await server.videos.get({ id: uuid })
|
||||||
expect(video.inputFileUpdatedAt).to.be.null
|
expect(video.inputFileUpdatedAt).to.be.null
|
||||||
|
@ -151,9 +243,23 @@ describe('Test a video file replacement', function () {
|
||||||
|
|
||||||
replaceDate = new Date()
|
replaceDate = new Date()
|
||||||
|
|
||||||
await servers[0].videos.replaceSourceFile({ videoId: uuid, fixture: 'video_short_360p.mp4' })
|
const replaceFixture = 'video_short_360p.mp4'
|
||||||
|
await servers[0].videos.replaceSourceFile({ videoId: uuid, fixture: replaceFixture })
|
||||||
await waitJobs(servers)
|
await waitJobs(servers)
|
||||||
|
|
||||||
|
const source = await checkSourceFile({ server: servers[0], fsCount: 1, uuid, fixture: replaceFixture })
|
||||||
|
|
||||||
|
expect(source.createdAt).to.exist
|
||||||
|
expect(source.fps).to.equal(25)
|
||||||
|
expect(source.height).to.equal(360)
|
||||||
|
expect(source.width).to.equal(640)
|
||||||
|
expect(source.resolution.id).to.equal(360)
|
||||||
|
expect(source.resolution.label).to.equal('360p')
|
||||||
|
expect(source.size).to.equal(30620)
|
||||||
|
|
||||||
|
expect(source.metadata?.format).to.exist
|
||||||
|
expect(source.metadata?.streams).to.be.an('array')
|
||||||
|
|
||||||
for (const server of servers) {
|
for (const server of servers) {
|
||||||
const video = await server.videos.get({ id: uuid })
|
const video = await server.videos.get({ id: uuid })
|
||||||
|
|
||||||
|
@ -189,35 +295,36 @@ describe('Test a video file replacement', function () {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await servers[0].config.enableMinimumTranscoding()
|
await servers[0].config.enableMinimumTranscoding({ keepOriginal: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should have cleaned up old files', async function () {
|
it('Should have cleaned up old files', async function () {
|
||||||
{
|
{
|
||||||
const count = await servers[0].servers.countFiles('storyboards')
|
const count = await servers[0].servers.countFiles('storyboards')
|
||||||
expect(count).to.equal(2)
|
expect(count).to.equal(3)
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const count = await servers[0].servers.countFiles('web-videos')
|
const count = await servers[0].servers.countFiles('web-videos')
|
||||||
expect(count).to.equal(5 + 1) // +1 for private directory
|
expect(count).to.equal(6 + 1) // +1 for private directory
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const count = await servers[0].servers.countFiles('streaming-playlists/hls')
|
const count = await servers[0].servers.countFiles('streaming-playlists/hls')
|
||||||
expect(count).to.equal(1 + 1) // +1 for private directory
|
expect(count).to.equal(2 + 1) // +1 for private directory
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const count = await servers[0].servers.countFiles('torrents')
|
const count = await servers[0].servers.countFiles('torrents')
|
||||||
expect(count).to.equal(9)
|
expect(count).to.equal(11)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should have the correct source input', async function () {
|
it('Should have the correct source input filename', async function () {
|
||||||
const source = await servers[0].videos.getSource({ id: uuid })
|
const source = await servers[0].videos.getSource({ id: uuid })
|
||||||
|
|
||||||
expect(source.filename).to.equal('video_short_360p.mp4')
|
expect(source.filename).to.equal('video_short_360p.mp4')
|
||||||
|
expect(source.inputFilename).to.equal('video_short_360p.mp4')
|
||||||
expect(new Date(source.createdAt)).to.be.above(replaceDate)
|
expect(new Date(source.createdAt)).to.be.above(replaceDate)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -367,6 +474,9 @@ describe('Test a video file replacement', function () {
|
||||||
expect(files[0].resolution.id).to.equal(360)
|
expect(files[0].resolution.id).to.equal(360)
|
||||||
expectStartWith(files[0].fileUrl, objectStorage.getMockWebVideosBaseUrl())
|
expectStartWith(files[0].fileUrl, objectStorage.getMockWebVideosBaseUrl())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const source = await servers[0].videos.getSource({ id: uuid })
|
||||||
|
expect(source.fileDownloadUrl).to.not.exist
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should replace a video file with transcoding enabled', async function () {
|
it('Should replace a video file with transcoding enabled', async function () {
|
||||||
|
@ -374,16 +484,25 @@ describe('Test a video file replacement', function () {
|
||||||
|
|
||||||
const previousPaths: string[] = []
|
const previousPaths: string[] = []
|
||||||
|
|
||||||
await servers[0].config.enableTranscoding({ hls: true, webVideo: true, with0p: true })
|
await servers[0].config.enableTranscoding({ hls: true, webVideo: true, with0p: true, keepOriginal: true })
|
||||||
|
|
||||||
|
const fixture1 = 'video_short_360p.mp4'
|
||||||
const { uuid: videoUUID } = await servers[0].videos.quickUpload({
|
const { uuid: videoUUID } = await servers[0].videos.quickUpload({
|
||||||
name: 'object storage with transcoding',
|
name: 'object storage with transcoding',
|
||||||
fixture: 'video_short_360p.mp4'
|
fixture: fixture1
|
||||||
})
|
})
|
||||||
uuid = videoUUID
|
uuid = videoUUID
|
||||||
|
|
||||||
await waitJobs(servers)
|
await waitJobs(servers)
|
||||||
|
|
||||||
|
await checkSourceFile({
|
||||||
|
server: servers[0],
|
||||||
|
fixture: fixture1,
|
||||||
|
fsCount: 0,
|
||||||
|
uuid,
|
||||||
|
objectStorageBaseUrl: objectStorage?.getMockOriginalFileBaseUrl()
|
||||||
|
})
|
||||||
|
|
||||||
for (const server of servers) {
|
for (const server of servers) {
|
||||||
const video = await server.videos.get({ id: uuid })
|
const video = await server.videos.get({ id: uuid })
|
||||||
|
|
||||||
|
@ -403,9 +522,18 @@ describe('Test a video file replacement', function () {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await servers[0].videos.replaceSourceFile({ videoId: uuid, fixture: 'video_short_240p.mp4' })
|
const fixture2 = 'video_short_240p.mp4'
|
||||||
|
await servers[0].videos.replaceSourceFile({ videoId: uuid, fixture: fixture2 })
|
||||||
await waitJobs(servers)
|
await waitJobs(servers)
|
||||||
|
|
||||||
|
await checkSourceFile({
|
||||||
|
server: servers[0],
|
||||||
|
fixture: fixture2,
|
||||||
|
fsCount: 0,
|
||||||
|
uuid,
|
||||||
|
objectStorageBaseUrl: objectStorage?.getMockOriginalFileBaseUrl()
|
||||||
|
})
|
||||||
|
|
||||||
for (const server of servers) {
|
for (const server of servers) {
|
||||||
const video = await server.videos.get({ id: uuid })
|
const video = await server.videos.get({ id: uuid })
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
export * from './client-cli.js'
|
export * from './client-cli.js'
|
||||||
export * from './live-transcoding.js'
|
export * from './live-transcoding.js'
|
||||||
|
export * from './replace-file.js'
|
||||||
export * from './studio-transcoding.js'
|
export * from './studio-transcoding.js'
|
||||||
export * from './vod-transcoding.js'
|
export * from './vod-transcoding.js'
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||||
|
import { getAllFiles } from '@peertube/peertube-core-utils'
|
||||||
|
import {
|
||||||
|
cleanupTests,
|
||||||
|
createSingleServer,
|
||||||
|
PeerTubeServer,
|
||||||
|
setAccessTokensToServers,
|
||||||
|
setDefaultVideoChannel,
|
||||||
|
waitJobs
|
||||||
|
} from '@peertube/peertube-server-commands'
|
||||||
|
import { PeerTubeRunnerProcess } from '@tests/shared/peertube-runner-process.js'
|
||||||
|
import { checkSourceFile } from '@tests/shared/videos.js'
|
||||||
|
import { expect } from 'chai'
|
||||||
|
|
||||||
|
describe('Test replace file using peertube-runner program', function () {
|
||||||
|
let server: PeerTubeServer
|
||||||
|
let peertubeRunner: PeerTubeRunnerProcess
|
||||||
|
let uuid: string
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
this.timeout(120_000)
|
||||||
|
|
||||||
|
server = await createSingleServer(1)
|
||||||
|
|
||||||
|
await setAccessTokensToServers([ server ])
|
||||||
|
await setDefaultVideoChannel([ server ])
|
||||||
|
|
||||||
|
await server.config.enableRemoteTranscoding()
|
||||||
|
await server.config.enableFileUpdate()
|
||||||
|
await server.config.enableMinimumTranscoding({ hls: true, keepOriginal: true, webVideo: true })
|
||||||
|
|
||||||
|
const registrationToken = await server.runnerRegistrationTokens.getFirstRegistrationToken()
|
||||||
|
|
||||||
|
peertubeRunner = new PeerTubeRunnerProcess(server)
|
||||||
|
await peertubeRunner.runServer()
|
||||||
|
await peertubeRunner.registerPeerTubeInstance({ registrationToken, runnerName: 'runner' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should upload a webm video, transcode it and keep original file', async function () {
|
||||||
|
this.timeout(240000)
|
||||||
|
|
||||||
|
const fixture = 'video_short.webm';
|
||||||
|
({ uuid } = await server.videos.quickUpload({ name: 'video', fixture }))
|
||||||
|
|
||||||
|
await waitJobs(server, { runnerJobs: true })
|
||||||
|
|
||||||
|
const video = await server.videos.get({ id: uuid })
|
||||||
|
|
||||||
|
const files = getAllFiles(video)
|
||||||
|
expect(files).to.have.lengthOf(4)
|
||||||
|
expect(files[0].resolution.id).to.equal(720)
|
||||||
|
|
||||||
|
await checkSourceFile({ server, fsCount: 1, fixture, uuid })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should upload an audio file, transcode it and keep original file', async function () {
|
||||||
|
const fixture = 'sample.ogg'
|
||||||
|
const { uuid } = await server.videos.quickUpload({ name: 'audio', fixture })
|
||||||
|
|
||||||
|
await waitJobs([ server ], { runnerJobs: true })
|
||||||
|
await checkSourceFile({ server, fsCount: 2, fixture, uuid })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should replace the video', async function () {
|
||||||
|
const fixture = 'video_short_360p.mp4'
|
||||||
|
await server.videos.replaceSourceFile({ videoId: uuid, fixture })
|
||||||
|
await waitJobs(server, { runnerJobs: true })
|
||||||
|
|
||||||
|
const video = await server.videos.get({ id: uuid })
|
||||||
|
|
||||||
|
const files = getAllFiles(video)
|
||||||
|
expect(files).to.have.lengthOf(4)
|
||||||
|
expect(files[0].resolution.id).to.equal(360)
|
||||||
|
|
||||||
|
await checkSourceFile({ server, fsCount: 2, fixture, uuid })
|
||||||
|
})
|
||||||
|
|
||||||
|
after(async function () {
|
||||||
|
if (peertubeRunner) {
|
||||||
|
await peertubeRunner.unregisterPeerTubeInstance({ runnerName: 'runner' })
|
||||||
|
peertubeRunner.kill()
|
||||||
|
}
|
||||||
|
|
||||||
|
await cleanupTests([ server ])
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,23 +1,23 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */
|
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */
|
||||||
|
|
||||||
|
import { uuidRegex } from '@peertube/peertube-core-utils'
|
||||||
|
import { HttpStatusCode, HttpStatusCodeType, VideoCaption, VideoDetails, VideoPrivacy, VideoResolution } from '@peertube/peertube-models'
|
||||||
|
import { buildAbsoluteFixturePath, getFileSize, getFilenameFromUrl, getLowercaseExtension } from '@peertube/peertube-node-utils'
|
||||||
|
import { PeerTubeServer, VideoEdit, getRedirectionUrl, makeRawRequest, waitJobs } from '@peertube/peertube-server-commands'
|
||||||
|
import {
|
||||||
|
VIDEO_CATEGORIES,
|
||||||
|
VIDEO_LANGUAGES,
|
||||||
|
VIDEO_LICENCES,
|
||||||
|
VIDEO_PRIVACIES,
|
||||||
|
loadLanguages
|
||||||
|
} from '@peertube/peertube-server/core/initializers/constants.js'
|
||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import { pathExists } from 'fs-extra/esm'
|
import { pathExists } from 'fs-extra/esm'
|
||||||
import { readdir } from 'fs/promises'
|
import { readdir } from 'fs/promises'
|
||||||
import { basename, join } from 'path'
|
import { basename, join } from 'path'
|
||||||
import { uuidRegex } from '@peertube/peertube-core-utils'
|
|
||||||
import { HttpStatusCode, HttpStatusCodeType, VideoCaption, VideoDetails, VideoPrivacy, VideoResolution } from '@peertube/peertube-models'
|
|
||||||
import {
|
|
||||||
loadLanguages,
|
|
||||||
VIDEO_CATEGORIES,
|
|
||||||
VIDEO_LANGUAGES,
|
|
||||||
VIDEO_LICENCES,
|
|
||||||
VIDEO_PRIVACIES
|
|
||||||
} from '@peertube/peertube-server/core/initializers/constants.js'
|
|
||||||
import { getLowercaseExtension } from '@peertube/peertube-node-utils'
|
|
||||||
import { makeRawRequest, PeerTubeServer, VideoEdit, waitJobs } from '@peertube/peertube-server-commands'
|
|
||||||
import { dateIsValid, expectStartWith, testImageGeneratedByFFmpeg } from './checks.js'
|
import { dateIsValid, expectStartWith, testImageGeneratedByFFmpeg } from './checks.js'
|
||||||
import { checkWebTorrentWorks } from './webtorrent.js'
|
|
||||||
import { completeCheckHlsPlaylist } from './streaming-playlists.js'
|
import { completeCheckHlsPlaylist } from './streaming-playlists.js'
|
||||||
|
import { checkWebTorrentWorks } from './webtorrent.js'
|
||||||
|
|
||||||
export async function completeWebVideoFilesCheck (options: {
|
export async function completeWebVideoFilesCheck (options: {
|
||||||
server: PeerTubeServer
|
server: PeerTubeServer
|
||||||
|
@ -369,3 +369,40 @@ export async function uploadRandomVideoOnServers (
|
||||||
|
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function checkSourceFile (options: {
|
||||||
|
server: PeerTubeServer
|
||||||
|
fsCount: number
|
||||||
|
uuid: string
|
||||||
|
fixture: string
|
||||||
|
objectStorageBaseUrl?: string // default false
|
||||||
|
}) {
|
||||||
|
const { server, fsCount, fixture, uuid, objectStorageBaseUrl } = options
|
||||||
|
|
||||||
|
const source = await server.videos.getSource({ id: uuid })
|
||||||
|
const fixtureFileSize = await getFileSize(buildAbsoluteFixturePath(fixture))
|
||||||
|
|
||||||
|
if (fsCount > 0) {
|
||||||
|
expect(await server.servers.countFiles('original-video-files')).to.equal(fsCount)
|
||||||
|
|
||||||
|
const keptFilePath = join(server.servers.buildDirectory('original-video-files'), getFilenameFromUrl(source.fileDownloadUrl))
|
||||||
|
expect(await getFileSize(keptFilePath)).to.equal(fixtureFileSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(source.fileDownloadUrl).to.exist
|
||||||
|
if (objectStorageBaseUrl) {
|
||||||
|
const token = await server.videoToken.getVideoFileToken({ videoId: uuid })
|
||||||
|
expectStartWith(await getRedirectionUrl(source.fileDownloadUrl + '?videoFileToken=' + token), objectStorageBaseUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { body } = await makeRawRequest({
|
||||||
|
url: source.fileDownloadUrl,
|
||||||
|
token: server.accessToken,
|
||||||
|
redirects: 1,
|
||||||
|
expectedStatus: HttpStatusCode.OK_200
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(body).to.have.lengthOf(fixtureFileSize)
|
||||||
|
|
||||||
|
return source
|
||||||
|
}
|
||||||
|
|
|
@ -320,6 +320,9 @@ function customConfig (): CustomConfig {
|
||||||
},
|
},
|
||||||
transcoding: {
|
transcoding: {
|
||||||
enabled: CONFIG.TRANSCODING.ENABLED,
|
enabled: CONFIG.TRANSCODING.ENABLED,
|
||||||
|
originalFile: {
|
||||||
|
keep: CONFIG.TRANSCODING.ORIGINAL_FILE.KEEP
|
||||||
|
},
|
||||||
remoteRunners: {
|
remoteRunners: {
|
||||||
enabled: CONFIG.TRANSCODING.REMOTE_RUNNERS.ENABLED
|
enabled: CONFIG.TRANSCODING.REMOTE_RUNNERS.ENABLED
|
||||||
},
|
},
|
||||||
|
|
|
@ -184,9 +184,9 @@ async function addLiveVideo (req: express.Request, res: express.Response) {
|
||||||
duration: 0,
|
duration: 0,
|
||||||
state: VideoState.WAITING_FOR_LIVE,
|
state: VideoState.WAITING_FOR_LIVE,
|
||||||
isLive: true,
|
isLive: true,
|
||||||
filename: null
|
inputFilename: null
|
||||||
},
|
},
|
||||||
videoFilePath: undefined,
|
videoFile: undefined,
|
||||||
user: res.locals.oauth.token.User,
|
user: res.locals.oauth.token.User,
|
||||||
thumbnails
|
thumbnails
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
import express from 'express'
|
import { buildAspectRatio } from '@peertube/peertube-core-utils'
|
||||||
import { move } from 'fs-extra/esm'
|
import { VideoState } from '@peertube/peertube-models'
|
||||||
import { sequelizeTypescript } from '@server/initializers/database.js'
|
import { sequelizeTypescript } from '@server/initializers/database.js'
|
||||||
import { CreateJobArgument, CreateJobOptions, JobQueue } from '@server/lib/job-queue/index.js'
|
import { CreateJobArgument, CreateJobOptions, JobQueue } from '@server/lib/job-queue/index.js'
|
||||||
import { Hooks } from '@server/lib/plugins/hooks.js'
|
import { Hooks } from '@server/lib/plugins/hooks.js'
|
||||||
import { regenerateMiniaturesIfNeeded } from '@server/lib/thumbnail.js'
|
import { regenerateMiniaturesIfNeeded } from '@server/lib/thumbnail.js'
|
||||||
import { setupUploadResumableRoutes } from '@server/lib/uploadx.js'
|
import { setupUploadResumableRoutes } from '@server/lib/uploadx.js'
|
||||||
import { buildMoveJob, buildStoryboardJobIfNeeded } from '@server/lib/video-jobs.js'
|
|
||||||
import { autoBlacklistVideoIfNeeded } from '@server/lib/video-blacklist.js'
|
import { autoBlacklistVideoIfNeeded } from '@server/lib/video-blacklist.js'
|
||||||
import { buildNewFile } from '@server/lib/video-file.js'
|
import { buildNewFile, createVideoSource } from '@server/lib/video-file.js'
|
||||||
|
import { buildMoveJob, buildStoryboardJobIfNeeded } from '@server/lib/video-jobs.js'
|
||||||
import { VideoPathManager } from '@server/lib/video-path-manager.js'
|
import { VideoPathManager } from '@server/lib/video-path-manager.js'
|
||||||
import { buildNextVideoState } from '@server/lib/video-state.js'
|
import { buildNextVideoState } from '@server/lib/video-state.js'
|
||||||
import { openapiOperationDoc } from '@server/middlewares/doc.js'
|
import { openapiOperationDoc } from '@server/middlewares/doc.js'
|
||||||
import { VideoModel } from '@server/models/video/video.js'
|
import { VideoModel } from '@server/models/video/video.js'
|
||||||
import { VideoSourceModel } from '@server/models/video/video-source.js'
|
|
||||||
import { MStreamingPlaylistFiles, MVideo, MVideoFile, MVideoFullLight } from '@server/types/models/index.js'
|
import { MStreamingPlaylistFiles, MVideo, MVideoFile, MVideoFullLight } from '@server/types/models/index.js'
|
||||||
import { VideoState } from '@peertube/peertube-models'
|
import express from 'express'
|
||||||
|
import { move } from 'fs-extra/esm'
|
||||||
import { logger, loggerTagsFactory } from '../../../helpers/logger.js'
|
import { logger, loggerTagsFactory } from '../../../helpers/logger.js'
|
||||||
import {
|
import {
|
||||||
asyncMiddleware,
|
asyncMiddleware,
|
||||||
|
@ -23,7 +23,6 @@ import {
|
||||||
replaceVideoSourceResumableValidator,
|
replaceVideoSourceResumableValidator,
|
||||||
videoSourceGetLatestValidator
|
videoSourceGetLatestValidator
|
||||||
} from '../../../middlewares/index.js'
|
} from '../../../middlewares/index.js'
|
||||||
import { buildAspectRatio } from '@peertube/peertube-core-utils'
|
|
||||||
|
|
||||||
const lTags = loggerTagsFactory('api', 'video')
|
const lTags = loggerTagsFactory('api', 'video')
|
||||||
|
|
||||||
|
@ -61,7 +60,7 @@ async function replaceVideoSourceResumable (req: express.Request, res: express.R
|
||||||
const videoPhysicalFile = res.locals.updateVideoFileResumable
|
const videoPhysicalFile = res.locals.updateVideoFileResumable
|
||||||
const user = res.locals.oauth.token.User
|
const user = res.locals.oauth.token.User
|
||||||
|
|
||||||
const videoFile = await buildNewFile({ path: videoPhysicalFile.path, mode: 'web-video' })
|
const videoFile = await buildNewFile({ path: videoPhysicalFile.path, mode: 'web-video', ffprobe: res.locals.ffprobe })
|
||||||
const originalFilename = videoPhysicalFile.originalname
|
const originalFilename = videoPhysicalFile.originalname
|
||||||
|
|
||||||
const videoFileMutexReleaser = await VideoPathManager.Instance.lockFiles(res.locals.videoAll.uuid)
|
const videoFileMutexReleaser = await VideoPathManager.Instance.lockFiles(res.locals.videoAll.uuid)
|
||||||
|
@ -114,13 +113,15 @@ async function replaceVideoSourceResumable (req: express.Request, res: express.R
|
||||||
|
|
||||||
await removeOldFiles({ video, files: oldWebVideoFiles, playlists: oldStreamingPlaylists })
|
await removeOldFiles({ video, files: oldWebVideoFiles, playlists: oldStreamingPlaylists })
|
||||||
|
|
||||||
const source = await VideoSourceModel.create({
|
const source = await createVideoSource({
|
||||||
filename: originalFilename,
|
inputFilename: originalFilename,
|
||||||
videoId: video.id,
|
inputProbe: res.locals.ffprobe,
|
||||||
|
inputPath: destination,
|
||||||
|
video,
|
||||||
createdAt: inputFileUpdatedAt
|
createdAt: inputFileUpdatedAt
|
||||||
})
|
})
|
||||||
|
|
||||||
await regenerateMiniaturesIfNeeded(video)
|
await regenerateMiniaturesIfNeeded(video, res.locals.ffprobe)
|
||||||
await video.VideoChannel.setAsUpdated()
|
await video.VideoChannel.setAsUpdated()
|
||||||
await addVideoJobsAfterUpload(video, video.getMaxQualityFile())
|
await addVideoJobsAfterUpload(video, video.getMaxQualityFile())
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import express from 'express'
|
import { ffprobePromise, getChaptersFromContainer } from '@peertube/peertube-ffmpeg'
|
||||||
|
import { HttpStatusCode, ThumbnailType, VideoCreate } from '@peertube/peertube-models'
|
||||||
|
import { uuidToShort } from '@peertube/peertube-node-utils'
|
||||||
import { getResumableUploadPath } from '@server/helpers/upload.js'
|
import { getResumableUploadPath } from '@server/helpers/upload.js'
|
||||||
|
import { LocalVideoCreator } from '@server/lib/local-video-creator.js'
|
||||||
import { Redis } from '@server/lib/redis.js'
|
import { Redis } from '@server/lib/redis.js'
|
||||||
import { setupUploadResumableRoutes, uploadx } from '@server/lib/uploadx.js'
|
import { setupUploadResumableRoutes, uploadx } from '@server/lib/uploadx.js'
|
||||||
import { buildNextVideoState } from '@server/lib/video-state.js'
|
import { buildNextVideoState } from '@server/lib/video-state.js'
|
||||||
import { openapiOperationDoc } from '@server/middlewares/doc.js'
|
import { openapiOperationDoc } from '@server/middlewares/doc.js'
|
||||||
import { uuidToShort } from '@peertube/peertube-node-utils'
|
import express from 'express'
|
||||||
import { HttpStatusCode, ThumbnailType, VideoCreate } from '@peertube/peertube-models'
|
import { VideoAuditView, auditLoggerFactory, getAuditIdFromRes } from '../../../helpers/audit-logger.js'
|
||||||
import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger.js'
|
|
||||||
import { createReqFiles } from '../../../helpers/express-utils.js'
|
import { createReqFiles } from '../../../helpers/express-utils.js'
|
||||||
import { logger, loggerTagsFactory } from '../../../helpers/logger.js'
|
import { logger, loggerTagsFactory } from '../../../helpers/logger.js'
|
||||||
import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../../initializers/constants.js'
|
import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../../initializers/constants.js'
|
||||||
|
@ -19,8 +21,6 @@ import {
|
||||||
videosAddResumableInitValidator,
|
videosAddResumableInitValidator,
|
||||||
videosAddResumableValidator
|
videosAddResumableValidator
|
||||||
} from '../../../middlewares/index.js'
|
} from '../../../middlewares/index.js'
|
||||||
import { ffprobePromise, getChaptersFromContainer } from '@peertube/peertube-ffmpeg'
|
|
||||||
import { LocalVideoCreator } from '@server/lib/local-video-creator.js'
|
|
||||||
|
|
||||||
const lTags = loggerTagsFactory('api', 'video')
|
const lTags = loggerTagsFactory('api', 'video')
|
||||||
const auditLogger = auditLoggerFactory('videos')
|
const auditLogger = auditLoggerFactory('videos')
|
||||||
|
@ -134,7 +134,12 @@ async function addVideo (options: {
|
||||||
|
|
||||||
const localVideoCreator = new LocalVideoCreator({
|
const localVideoCreator = new LocalVideoCreator({
|
||||||
lTags,
|
lTags,
|
||||||
videoFilePath: videoPhysicalFile.path,
|
|
||||||
|
videoFile: {
|
||||||
|
path: videoPhysicalFile.path,
|
||||||
|
probe: res.locals.ffprobe
|
||||||
|
},
|
||||||
|
|
||||||
user: res.locals.oauth.token.User,
|
user: res.locals.oauth.token.User,
|
||||||
channel: res.locals.videoChannel,
|
channel: res.locals.videoChannel,
|
||||||
|
|
||||||
|
@ -148,7 +153,7 @@ async function addVideo (options: {
|
||||||
...videoInfo,
|
...videoInfo,
|
||||||
|
|
||||||
duration: videoPhysicalFile.duration,
|
duration: videoPhysicalFile.duration,
|
||||||
filename: videoPhysicalFile.originalname,
|
inputFilename: videoPhysicalFile.originalname,
|
||||||
state: buildNextVideoState(),
|
state: buildNextVideoState(),
|
||||||
isLive: false
|
isLive: false
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import cors from 'cors'
|
import { forceNumber } from '@peertube/peertube-core-utils'
|
||||||
import express from 'express'
|
import { FileStorage, HttpStatusCode, VideoStreamingPlaylistType } from '@peertube/peertube-models'
|
||||||
import { logger } from '@server/helpers/logger.js'
|
import { logger } from '@server/helpers/logger.js'
|
||||||
import { VideoTorrentsSimpleFileCache } from '@server/lib/files-cache/index.js'
|
import { VideoTorrentsSimpleFileCache } from '@server/lib/files-cache/index.js'
|
||||||
import {
|
import {
|
||||||
generateHLSFilePresignedUrl,
|
generateHLSFilePresignedUrl,
|
||||||
|
generateOriginalFilePresignedUrl,
|
||||||
generateUserExportPresignedUrl,
|
generateUserExportPresignedUrl,
|
||||||
generateWebVideoPresignedUrl
|
generateWebVideoPresignedUrl
|
||||||
} from '@server/lib/object-storage/index.js'
|
} from '@server/lib/object-storage/index.js'
|
||||||
|
import { getFSUserExportFilePath } from '@server/lib/paths.js'
|
||||||
import { Hooks } from '@server/lib/plugins/hooks.js'
|
import { Hooks } from '@server/lib/plugins/hooks.js'
|
||||||
import { VideoPathManager } from '@server/lib/video-path-manager.js'
|
import { VideoPathManager } from '@server/lib/video-path-manager.js'
|
||||||
import {
|
import {
|
||||||
|
@ -17,15 +19,16 @@ import {
|
||||||
MVideoFile,
|
MVideoFile,
|
||||||
MVideoFullLight
|
MVideoFullLight
|
||||||
} from '@server/types/models/index.js'
|
} from '@server/types/models/index.js'
|
||||||
import { forceNumber } from '@peertube/peertube-core-utils'
|
import { MVideoSource } from '@server/types/models/video/video-source.js'
|
||||||
import { HttpStatusCode, FileStorage, VideoStreamingPlaylistType } from '@peertube/peertube-models'
|
import cors from 'cors'
|
||||||
|
import express from 'express'
|
||||||
import { STATIC_DOWNLOAD_PATHS } from '../initializers/constants.js'
|
import { STATIC_DOWNLOAD_PATHS } from '../initializers/constants.js'
|
||||||
import {
|
import {
|
||||||
asyncMiddleware, optionalAuthenticate,
|
asyncMiddleware, optionalAuthenticate,
|
||||||
|
originalVideoFileDownloadValidator,
|
||||||
userExportDownloadValidator,
|
userExportDownloadValidator,
|
||||||
videosDownloadValidator
|
videosDownloadValidator
|
||||||
} from '../middlewares/index.js'
|
} from '../middlewares/index.js'
|
||||||
import { getFSUserExportFilePath } from '@server/lib/paths.js'
|
|
||||||
|
|
||||||
const downloadRouter = express.Router()
|
const downloadRouter = express.Router()
|
||||||
|
|
||||||
|
@ -40,7 +43,7 @@ downloadRouter.use(
|
||||||
STATIC_DOWNLOAD_PATHS.VIDEOS + ':id-:resolution([0-9]+).:extension',
|
STATIC_DOWNLOAD_PATHS.VIDEOS + ':id-:resolution([0-9]+).:extension',
|
||||||
optionalAuthenticate,
|
optionalAuthenticate,
|
||||||
asyncMiddleware(videosDownloadValidator),
|
asyncMiddleware(videosDownloadValidator),
|
||||||
asyncMiddleware(downloadVideoFile)
|
asyncMiddleware(downloadWebVideoFile)
|
||||||
)
|
)
|
||||||
|
|
||||||
downloadRouter.use(
|
downloadRouter.use(
|
||||||
|
@ -51,11 +54,18 @@ downloadRouter.use(
|
||||||
)
|
)
|
||||||
|
|
||||||
downloadRouter.use(
|
downloadRouter.use(
|
||||||
STATIC_DOWNLOAD_PATHS.USER_EXPORT + ':filename',
|
STATIC_DOWNLOAD_PATHS.USER_EXPORTS + ':filename',
|
||||||
asyncMiddleware(userExportDownloadValidator), // Include JWT token authentication
|
asyncMiddleware(userExportDownloadValidator), // Include JWT token authentication
|
||||||
asyncMiddleware(downloadUserExport)
|
asyncMiddleware(downloadUserExport)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
downloadRouter.use(
|
||||||
|
STATIC_DOWNLOAD_PATHS.ORIGINAL_VIDEO_FILE + ':filename',
|
||||||
|
optionalAuthenticate,
|
||||||
|
asyncMiddleware(originalVideoFileDownloadValidator),
|
||||||
|
asyncMiddleware(downloadOriginalFile)
|
||||||
|
)
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
@ -91,7 +101,7 @@ async function downloadTorrent (req: express.Request, res: express.Response) {
|
||||||
return res.download(result.path, result.downloadName)
|
return res.download(result.path, result.downloadName)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadVideoFile (req: express.Request, res: express.Response) {
|
async function downloadWebVideoFile (req: express.Request, res: express.Response) {
|
||||||
const video = res.locals.videoAll
|
const video = res.locals.videoAll
|
||||||
|
|
||||||
const videoFile = getVideoFile(req, video.VideoFiles)
|
const videoFile = getVideoFile(req, video.VideoFiles)
|
||||||
|
@ -184,6 +194,19 @@ function downloadUserExport (req: express.Request, res: express.Response) {
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function downloadOriginalFile (req: express.Request, res: express.Response) {
|
||||||
|
const videoSource = res.locals.videoSource
|
||||||
|
|
||||||
|
const downloadFilename = videoSource.inputFilename
|
||||||
|
|
||||||
|
if (videoSource.storage === FileStorage.OBJECT_STORAGE) {
|
||||||
|
return redirectOriginalFileToObjectStorage({ res, videoSource, downloadFilename })
|
||||||
|
}
|
||||||
|
|
||||||
|
res.download(VideoPathManager.Instance.getFSOriginalVideoFilePath(videoSource.keptOriginalFilename), downloadFilename)
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function getVideoFile (req: express.Request, files: MVideoFile[]) {
|
function getVideoFile (req: express.Request, files: MVideoFile[]) {
|
||||||
|
@ -262,3 +285,17 @@ async function redirectUserExportToObjectStorage (options: {
|
||||||
|
|
||||||
return res.redirect(url)
|
return res.redirect(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function redirectOriginalFileToObjectStorage (options: {
|
||||||
|
res: express.Response
|
||||||
|
downloadFilename: string
|
||||||
|
videoSource: MVideoSource
|
||||||
|
}) {
|
||||||
|
const { res, downloadFilename, videoSource } = options
|
||||||
|
|
||||||
|
const url = await generateOriginalFilePresignedUrl({ videoSource, downloadFilename })
|
||||||
|
|
||||||
|
logger.debug('Generating pre-signed URL %s for original video file %s', url, videoSource.keptOriginalFilename)
|
||||||
|
|
||||||
|
return res.redirect(url)
|
||||||
|
}
|
||||||
|
|
|
@ -31,12 +31,12 @@ const privateWebVideoStaticMiddlewares = CONFIG.STATIC_FILES.PRIVATE_FILES_REQUI
|
||||||
staticRouter.use(
|
staticRouter.use(
|
||||||
[ STATIC_PATHS.PRIVATE_WEB_VIDEOS, STATIC_PATHS.LEGACY_PRIVATE_WEB_VIDEOS ],
|
[ STATIC_PATHS.PRIVATE_WEB_VIDEOS, STATIC_PATHS.LEGACY_PRIVATE_WEB_VIDEOS ],
|
||||||
...privateWebVideoStaticMiddlewares,
|
...privateWebVideoStaticMiddlewares,
|
||||||
express.static(DIRECTORIES.VIDEOS.PRIVATE, { fallthrough: false }),
|
express.static(DIRECTORIES.WEB_VIDEOS.PRIVATE, { fallthrough: false }),
|
||||||
handleStaticError
|
handleStaticError
|
||||||
)
|
)
|
||||||
staticRouter.use(
|
staticRouter.use(
|
||||||
[ STATIC_PATHS.WEB_VIDEOS, STATIC_PATHS.LEGACY_WEB_VIDEOS ],
|
[ STATIC_PATHS.WEB_VIDEOS, STATIC_PATHS.LEGACY_WEB_VIDEOS ],
|
||||||
express.static(DIRECTORIES.VIDEOS.PUBLIC, { fallthrough: false }),
|
express.static(DIRECTORIES.WEB_VIDEOS.PUBLIC, { fallthrough: false }),
|
||||||
handleStaticError
|
handleStaticError
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -303,7 +303,7 @@ function checkLiveConfig () {
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkObjectStorageConfig () {
|
function checkObjectStorageConfig () {
|
||||||
if (CONFIG.OBJECT_STORAGE.ENABLED === true) {
|
if (CONFIG.OBJECT_STORAGE.ENABLED !== true) return
|
||||||
|
|
||||||
if (!CONFIG.OBJECT_STORAGE.WEB_VIDEOS.BUCKET_NAME) {
|
if (!CONFIG.OBJECT_STORAGE.WEB_VIDEOS.BUCKET_NAME) {
|
||||||
throw new Error('videos_bucket should be set when object storage support is enabled.')
|
throw new Error('videos_bucket should be set when object storage support is enabled.')
|
||||||
|
@ -313,25 +313,59 @@ function checkObjectStorageConfig () {
|
||||||
throw new Error('streaming_playlists_bucket should be set when object storage support is enabled.')
|
throw new Error('streaming_playlists_bucket should be set when object storage support is enabled.')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check web videos and hls videos are not in the same bucket or directory
|
||||||
if (
|
if (
|
||||||
CONFIG.OBJECT_STORAGE.WEB_VIDEOS.BUCKET_NAME === CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS.BUCKET_NAME &&
|
CONFIG.OBJECT_STORAGE.WEB_VIDEOS.BUCKET_NAME === CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS.BUCKET_NAME &&
|
||||||
CONFIG.OBJECT_STORAGE.WEB_VIDEOS.PREFIX === CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS.PREFIX
|
CONFIG.OBJECT_STORAGE.WEB_VIDEOS.PREFIX === CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS.PREFIX
|
||||||
) {
|
) {
|
||||||
if (CONFIG.OBJECT_STORAGE.WEB_VIDEOS.PREFIX === '') {
|
if (CONFIG.OBJECT_STORAGE.WEB_VIDEOS.PREFIX === '') {
|
||||||
throw new Error('Object storage bucket prefixes should be set when the same bucket is used for both types of video.')
|
throw new Error('Bucket prefixes should be set when the same bucket is used for both types of video.')
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Object storage bucket prefixes should be set to different values when the same bucket is used for both types of video.'
|
'Bucket prefixes should be set to different values when the same bucket is used for both types of video.'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (CONFIG.TRANSCODING.ORIGINAL_FILE.KEEP) {
|
||||||
|
|
||||||
|
if (!CONFIG.OBJECT_STORAGE.ORIGINAL_VIDEO_FILES.BUCKET_NAME) {
|
||||||
|
throw new Error('original_video_files_bucket should be set when object storage support is enabled.')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check web videos/hls videos are not in the same bucket or directory as original video files
|
||||||
|
if (
|
||||||
|
CONFIG.OBJECT_STORAGE.WEB_VIDEOS.BUCKET_NAME === CONFIG.OBJECT_STORAGE.ORIGINAL_VIDEO_FILES.BUCKET_NAME &&
|
||||||
|
CONFIG.OBJECT_STORAGE.WEB_VIDEOS.PREFIX === CONFIG.OBJECT_STORAGE.ORIGINAL_VIDEO_FILES.PREFIX
|
||||||
|
) {
|
||||||
|
if (CONFIG.OBJECT_STORAGE.WEB_VIDEOS.PREFIX === '') {
|
||||||
|
throw new Error('Bucket prefixes should be set when the same bucket is used for both original and web video files.')
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
'Bucket prefixes should be set to different values when the same bucket is used for both original and web video files.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS.BUCKET_NAME === CONFIG.OBJECT_STORAGE.ORIGINAL_VIDEO_FILES.BUCKET_NAME &&
|
||||||
|
CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS.PREFIX === CONFIG.OBJECT_STORAGE.ORIGINAL_VIDEO_FILES.PREFIX
|
||||||
|
) {
|
||||||
|
if (CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS.PREFIX === '') {
|
||||||
|
throw new Error('Bucket prefixes should be set when the same bucket is used for both original and hls files.')
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
'Bucket prefixes should be set to different values when the same bucket is used for both original and hls files.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (CONFIG.OBJECT_STORAGE.MAX_UPLOAD_PART > parseBytes('250MB')) {
|
if (CONFIG.OBJECT_STORAGE.MAX_UPLOAD_PART > parseBytes('250MB')) {
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
logger.warn(`Object storage max upload part seems to have a big value (${CONFIG.OBJECT_STORAGE.MAX_UPLOAD_PART} bytes). Consider using a lower one (like 100MB).`)
|
logger.warn(`Object storage max upload part seems to have a big value (${CONFIG.OBJECT_STORAGE.MAX_UPLOAD_PART} bytes). Consider using a lower one (like 100MB).`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function checkVideoStudioConfig () {
|
function checkVideoStudioConfig () {
|
||||||
if (CONFIG.VIDEO_STUDIO.ENABLED === true && CONFIG.TRANSCODING.ENABLED === false) {
|
if (CONFIG.VIDEO_STUDIO.ENABLED === true && CONFIG.TRANSCODING.ENABLED === false) {
|
||||||
|
|
|
@ -32,8 +32,8 @@ function checkMissedConfig () {
|
||||||
'signup.enabled', 'signup.limit', 'signup.requires_approval', 'signup.requires_email_verification', 'signup.minimum_age',
|
'signup.enabled', 'signup.limit', 'signup.requires_approval', 'signup.requires_email_verification', 'signup.minimum_age',
|
||||||
'signup.filters.cidr.whitelist', 'signup.filters.cidr.blacklist',
|
'signup.filters.cidr.whitelist', 'signup.filters.cidr.blacklist',
|
||||||
'redundancy.videos.strategies', 'redundancy.videos.check_interval',
|
'redundancy.videos.strategies', 'redundancy.videos.check_interval',
|
||||||
'transcoding.enabled', 'transcoding.threads', 'transcoding.allow_additional_extensions', 'transcoding.web_videos.enabled',
|
'transcoding.enabled', 'transcoding.original_file.keep', 'transcoding.threads', 'transcoding.allow_additional_extensions',
|
||||||
'transcoding.hls.enabled', 'transcoding.profile', 'transcoding.concurrency',
|
'transcoding.web_videos.enabled', 'transcoding.hls.enabled', 'transcoding.profile', 'transcoding.concurrency',
|
||||||
'transcoding.resolutions.0p', 'transcoding.resolutions.144p', 'transcoding.resolutions.240p', 'transcoding.resolutions.360p',
|
'transcoding.resolutions.0p', 'transcoding.resolutions.144p', 'transcoding.resolutions.240p', 'transcoding.resolutions.360p',
|
||||||
'transcoding.resolutions.480p', 'transcoding.resolutions.720p', 'transcoding.resolutions.1080p', 'transcoding.resolutions.1440p',
|
'transcoding.resolutions.480p', 'transcoding.resolutions.720p', 'transcoding.resolutions.1080p', 'transcoding.resolutions.1440p',
|
||||||
'transcoding.resolutions.2160p', 'transcoding.always_transcode_original_resolution', 'transcoding.remote_runners.enabled',
|
'transcoding.resolutions.2160p', 'transcoding.always_transcode_original_resolution', 'transcoding.remote_runners.enabled',
|
||||||
|
@ -66,7 +66,8 @@ function checkMissedConfig () {
|
||||||
'object_storage.upload_acl.private', 'object_storage.proxy.proxify_private_files', 'object_storage.credentials.access_key_id',
|
'object_storage.upload_acl.private', 'object_storage.proxy.proxify_private_files', 'object_storage.credentials.access_key_id',
|
||||||
'object_storage.credentials.secret_access_key', 'object_storage.max_upload_part', 'object_storage.streaming_playlists.bucket_name',
|
'object_storage.credentials.secret_access_key', 'object_storage.max_upload_part', 'object_storage.streaming_playlists.bucket_name',
|
||||||
'object_storage.streaming_playlists.prefix', 'object_storage.streaming_playlists.base_url', 'object_storage.web_videos.bucket_name',
|
'object_storage.streaming_playlists.prefix', 'object_storage.streaming_playlists.base_url', 'object_storage.web_videos.bucket_name',
|
||||||
'object_storage.web_videos.prefix', 'object_storage.web_videos.base_url',
|
'object_storage.web_videos.prefix', 'object_storage.web_videos.base_url', 'object_storage.original_video_files.bucket_name',
|
||||||
|
'object_storage.original_video_files.prefix', 'object_storage.original_video_files.base_url',
|
||||||
'theme.default',
|
'theme.default',
|
||||||
'feeds.videos.count', 'feeds.comments.count',
|
'feeds.videos.count', 'feeds.comments.count',
|
||||||
'geo_ip.enabled', 'geo_ip.country.database_url', 'geo_ip.city.database_url',
|
'geo_ip.enabled', 'geo_ip.country.database_url', 'geo_ip.city.database_url',
|
||||||
|
|
|
@ -114,6 +114,7 @@ const CONFIG = {
|
||||||
LOG_DIR: buildPath(config.get<string>('storage.logs')),
|
LOG_DIR: buildPath(config.get<string>('storage.logs')),
|
||||||
WEB_VIDEOS_DIR: buildPath(config.get<string>('storage.web_videos')),
|
WEB_VIDEOS_DIR: buildPath(config.get<string>('storage.web_videos')),
|
||||||
STREAMING_PLAYLISTS_DIR: buildPath(config.get<string>('storage.streaming_playlists')),
|
STREAMING_PLAYLISTS_DIR: buildPath(config.get<string>('storage.streaming_playlists')),
|
||||||
|
ORIGINAL_VIDEO_FILES_DIR: buildPath(config.get<string>('storage.original_video_files')),
|
||||||
REDUNDANCY_DIR: buildPath(config.get<string>('storage.redundancy')),
|
REDUNDANCY_DIR: buildPath(config.get<string>('storage.redundancy')),
|
||||||
THUMBNAILS_DIR: buildPath(config.get<string>('storage.thumbnails')),
|
THUMBNAILS_DIR: buildPath(config.get<string>('storage.thumbnails')),
|
||||||
STORYBOARDS_DIR: buildPath(config.get<string>('storage.storyboards')),
|
STORYBOARDS_DIR: buildPath(config.get<string>('storage.storyboards')),
|
||||||
|
@ -159,6 +160,11 @@ const CONFIG = {
|
||||||
BUCKET_NAME: config.get<string>('object_storage.user_exports.bucket_name'),
|
BUCKET_NAME: config.get<string>('object_storage.user_exports.bucket_name'),
|
||||||
PREFIX: config.get<string>('object_storage.user_exports.prefix'),
|
PREFIX: config.get<string>('object_storage.user_exports.prefix'),
|
||||||
BASE_URL: config.get<string>('object_storage.user_exports.base_url')
|
BASE_URL: config.get<string>('object_storage.user_exports.base_url')
|
||||||
|
},
|
||||||
|
ORIGINAL_VIDEO_FILES: {
|
||||||
|
BUCKET_NAME: config.get<string>('object_storage.original_video_files.bucket_name'),
|
||||||
|
PREFIX: config.get<string>('object_storage.original_video_files.prefix'),
|
||||||
|
BASE_URL: config.get<string>('object_storage.original_video_files.base_url')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
WEBSERVER: {
|
WEBSERVER: {
|
||||||
|
@ -412,6 +418,9 @@ const CONFIG = {
|
||||||
},
|
},
|
||||||
TRANSCODING: {
|
TRANSCODING: {
|
||||||
get ENABLED () { return config.get<boolean>('transcoding.enabled') },
|
get ENABLED () { return config.get<boolean>('transcoding.enabled') },
|
||||||
|
ORIGINAL_FILE: {
|
||||||
|
get KEEP () { return config.get<boolean>('transcoding.original_file.keep') }
|
||||||
|
},
|
||||||
get ALLOW_ADDITIONAL_EXTENSIONS () { return config.get<boolean>('transcoding.allow_additional_extensions') },
|
get ALLOW_ADDITIONAL_EXTENSIONS () { return config.get<boolean>('transcoding.allow_additional_extensions') },
|
||||||
get ALLOW_AUDIO_FILES () { return config.get<boolean>('transcoding.allow_audio_files') },
|
get ALLOW_AUDIO_FILES () { return config.get<boolean>('transcoding.allow_audio_files') },
|
||||||
get THREADS () { return config.get<number>('transcoding.threads') },
|
get THREADS () { return config.get<number>('transcoding.threads') },
|
||||||
|
|
|
@ -45,7 +45,7 @@ import { cpus } from 'os'
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const LAST_MIGRATION_VERSION = 825
|
const LAST_MIGRATION_VERSION = 830
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@ -857,7 +857,8 @@ const STATIC_DOWNLOAD_PATHS = {
|
||||||
TORRENTS: '/download/torrents/',
|
TORRENTS: '/download/torrents/',
|
||||||
VIDEOS: '/download/videos/',
|
VIDEOS: '/download/videos/',
|
||||||
HLS_VIDEOS: '/download/streaming-playlists/hls/videos/',
|
HLS_VIDEOS: '/download/streaming-playlists/hls/videos/',
|
||||||
USER_EXPORT: '/download/user-export/'
|
USER_EXPORTS: '/download/user-exports/',
|
||||||
|
ORIGINAL_VIDEO_FILE: '/download/original-video-files/'
|
||||||
}
|
}
|
||||||
const LAZY_STATIC_PATHS = {
|
const LAZY_STATIC_PATHS = {
|
||||||
THUMBNAILS: '/lazy-static/thumbnails/',
|
THUMBNAILS: '/lazy-static/thumbnails/',
|
||||||
|
@ -981,11 +982,13 @@ const DIRECTORIES = {
|
||||||
PRIVATE: join(CONFIG.STORAGE.STREAMING_PLAYLISTS_DIR, 'hls', 'private')
|
PRIVATE: join(CONFIG.STORAGE.STREAMING_PLAYLISTS_DIR, 'hls', 'private')
|
||||||
},
|
},
|
||||||
|
|
||||||
VIDEOS: {
|
WEB_VIDEOS: {
|
||||||
PUBLIC: CONFIG.STORAGE.WEB_VIDEOS_DIR,
|
PUBLIC: CONFIG.STORAGE.WEB_VIDEOS_DIR,
|
||||||
PRIVATE: join(CONFIG.STORAGE.WEB_VIDEOS_DIR, 'private')
|
PRIVATE: join(CONFIG.STORAGE.WEB_VIDEOS_DIR, 'private')
|
||||||
},
|
},
|
||||||
|
|
||||||
|
ORIGINAL_VIDEOS: CONFIG.STORAGE.ORIGINAL_VIDEO_FILES_DIR,
|
||||||
|
|
||||||
HLS_REDUNDANCY: join(CONFIG.STORAGE.REDUNDANCY_DIR, 'hls')
|
HLS_REDUNDANCY: join(CONFIG.STORAGE.REDUNDANCY_DIR, 'hls')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -96,8 +96,8 @@ function createDirectoriesIfNotExist () {
|
||||||
|
|
||||||
tasks.push(ensureDir(DIRECTORIES.HLS_STREAMING_PLAYLIST.PRIVATE))
|
tasks.push(ensureDir(DIRECTORIES.HLS_STREAMING_PLAYLIST.PRIVATE))
|
||||||
tasks.push(ensureDir(DIRECTORIES.HLS_STREAMING_PLAYLIST.PUBLIC))
|
tasks.push(ensureDir(DIRECTORIES.HLS_STREAMING_PLAYLIST.PUBLIC))
|
||||||
tasks.push(ensureDir(DIRECTORIES.VIDEOS.PUBLIC))
|
tasks.push(ensureDir(DIRECTORIES.WEB_VIDEOS.PUBLIC))
|
||||||
tasks.push(ensureDir(DIRECTORIES.VIDEOS.PRIVATE))
|
tasks.push(ensureDir(DIRECTORIES.WEB_VIDEOS.PRIVATE))
|
||||||
|
|
||||||
// Resumable upload directory
|
// Resumable upload directory
|
||||||
tasks.push(ensureDir(DIRECTORIES.RESUMABLE_UPLOAD))
|
tasks.push(ensureDir(DIRECTORIES.RESUMABLE_UPLOAD))
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
import * as Sequelize from 'sequelize'
|
||||||
|
|
||||||
|
async function up (utils: {
|
||||||
|
transaction: Sequelize.Transaction
|
||||||
|
queryInterface: Sequelize.QueryInterface
|
||||||
|
sequelize: Sequelize.Sequelize
|
||||||
|
}): Promise<void> {
|
||||||
|
const { transaction } = utils
|
||||||
|
|
||||||
|
{
|
||||||
|
await utils.queryInterface.addColumn('videoSource', 'keptOriginalFilename', {
|
||||||
|
type: Sequelize.STRING,
|
||||||
|
allowNull: true
|
||||||
|
}, { transaction })
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
await utils.queryInterface.addColumn('videoSource', 'storage', {
|
||||||
|
type: Sequelize.INTEGER,
|
||||||
|
allowNull: true
|
||||||
|
}, { transaction })
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
await utils.queryInterface.addColumn('videoSource', 'resolution', {
|
||||||
|
type: Sequelize.INTEGER,
|
||||||
|
allowNull: true
|
||||||
|
}, { transaction })
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
await utils.queryInterface.addColumn('videoSource', 'width', {
|
||||||
|
type: Sequelize.INTEGER,
|
||||||
|
allowNull: true
|
||||||
|
}, { transaction })
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
await utils.queryInterface.addColumn('videoSource', 'height', {
|
||||||
|
type: Sequelize.INTEGER,
|
||||||
|
allowNull: true
|
||||||
|
}, { transaction })
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
await utils.queryInterface.addColumn('videoSource', 'fps', {
|
||||||
|
type: Sequelize.INTEGER,
|
||||||
|
allowNull: true
|
||||||
|
}, { transaction })
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
await utils.queryInterface.addColumn('videoSource', 'size', {
|
||||||
|
type: Sequelize.INTEGER,
|
||||||
|
allowNull: true
|
||||||
|
}, { transaction })
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
await utils.queryInterface.addColumn('videoSource', 'metadata', {
|
||||||
|
type: Sequelize.JSONB,
|
||||||
|
allowNull: true
|
||||||
|
}, { transaction })
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
await utils.queryInterface.addColumn('videoSource', 'fileUrl', {
|
||||||
|
type: Sequelize.STRING,
|
||||||
|
allowNull: true
|
||||||
|
}, { transaction })
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
await utils.queryInterface.renameColumn('videoSource', 'filename', 'inputFilename', { transaction })
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
await utils.queryInterface.addColumn('userExport', 'fileUrl', {
|
||||||
|
type: Sequelize.STRING,
|
||||||
|
allowNull: true
|
||||||
|
}, { transaction })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function down (options) {
|
||||||
|
throw new Error('Not implemented.')
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
down, up
|
||||||
|
}
|
|
@ -166,7 +166,8 @@ async function saveReplayToExternalVideo (options: {
|
||||||
const thumbnails = await generateLocalVideoMiniature({
|
const thumbnails = await generateLocalVideoMiniature({
|
||||||
video: replayVideo,
|
video: replayVideo,
|
||||||
videoFile: replayVideo.getMaxQualityFile(),
|
videoFile: replayVideo.getMaxQualityFile(),
|
||||||
types: [ ThumbnailType.MINIATURE, ThumbnailType.PREVIEW ]
|
types: [ ThumbnailType.MINIATURE, ThumbnailType.PREVIEW ],
|
||||||
|
ffprobe: undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
for (const thumbnail of thumbnails) {
|
for (const thumbnail of thumbnails) {
|
||||||
|
@ -238,7 +239,7 @@ async function replaceLiveByReplay (options: {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Regenerate the thumbnail & preview?
|
// Regenerate the thumbnail & preview?
|
||||||
await regenerateMiniaturesIfNeeded(videoWithFiles)
|
await regenerateMiniaturesIfNeeded(videoWithFiles, undefined)
|
||||||
|
|
||||||
// We consider this is a new video
|
// We consider this is a new video
|
||||||
await moveToNextState({ video: videoWithFiles, isNewVideo: true })
|
await moveToNextState({ video: videoWithFiles, isNewVideo: true })
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { buildAspectRatio } from '@peertube/peertube-core-utils'
|
import { buildAspectRatio } from '@peertube/peertube-core-utils'
|
||||||
import { ffprobePromise } from '@peertube/peertube-ffmpeg'
|
|
||||||
import {
|
import {
|
||||||
LiveVideoCreate,
|
LiveVideoCreate,
|
||||||
LiveVideoLatencyMode,
|
LiveVideoLatencyMode,
|
||||||
|
@ -18,11 +17,10 @@ import { ScheduleVideoUpdateModel } from '@server/models/video/schedule-video-up
|
||||||
import { VideoLiveReplaySettingModel } from '@server/models/video/video-live-replay-setting.js'
|
import { VideoLiveReplaySettingModel } from '@server/models/video/video-live-replay-setting.js'
|
||||||
import { VideoLiveModel } from '@server/models/video/video-live.js'
|
import { VideoLiveModel } from '@server/models/video/video-live.js'
|
||||||
import { VideoPasswordModel } from '@server/models/video/video-password.js'
|
import { VideoPasswordModel } from '@server/models/video/video-password.js'
|
||||||
import { VideoSourceModel } from '@server/models/video/video-source.js'
|
|
||||||
import { VideoModel } from '@server/models/video/video.js'
|
import { VideoModel } from '@server/models/video/video.js'
|
||||||
import { MChannel, MChannelAccountLight, MThumbnail, MUser, MVideoFile, MVideoFullLight } from '@server/types/models/index.js'
|
import { MChannel, MChannelAccountLight, MThumbnail, MUser, MVideoFile, MVideoFullLight } from '@server/types/models/index.js'
|
||||||
import { FilteredModelAttributes } from '@server/types/sequelize.js'
|
import { FilteredModelAttributes } from '@server/types/sequelize.js'
|
||||||
import Ffmpeg from 'fluent-ffmpeg'
|
import { FfprobeData } from 'fluent-ffmpeg'
|
||||||
import { move } from 'fs-extra/esm'
|
import { move } from 'fs-extra/esm'
|
||||||
import { getLocalVideoActivityPubUrl } from './activitypub/url.js'
|
import { getLocalVideoActivityPubUrl } from './activitypub/url.js'
|
||||||
import { federateVideoIfNeeded } from './activitypub/videos/federate.js'
|
import { federateVideoIfNeeded } from './activitypub/videos/federate.js'
|
||||||
|
@ -30,7 +28,7 @@ import { Hooks } from './plugins/hooks.js'
|
||||||
import { generateLocalVideoMiniature, updateLocalVideoMiniatureFromExisting } from './thumbnail.js'
|
import { generateLocalVideoMiniature, updateLocalVideoMiniatureFromExisting } from './thumbnail.js'
|
||||||
import { autoBlacklistVideoIfNeeded } from './video-blacklist.js'
|
import { autoBlacklistVideoIfNeeded } from './video-blacklist.js'
|
||||||
import { replaceChapters, replaceChaptersFromDescriptionIfNeeded } from './video-chapters.js'
|
import { replaceChapters, replaceChaptersFromDescriptionIfNeeded } from './video-chapters.js'
|
||||||
import { buildNewFile } from './video-file.js'
|
import { buildNewFile, createVideoSource } from './video-file.js'
|
||||||
import { addVideoJobsAfterCreation } from './video-jobs.js'
|
import { addVideoJobsAfterCreation } from './video-jobs.js'
|
||||||
import { VideoPathManager } from './video-path-manager.js'
|
import { VideoPathManager } from './video-path-manager.js'
|
||||||
import { setVideoTags } from './video.js'
|
import { setVideoTags } from './video.js'
|
||||||
|
@ -39,7 +37,7 @@ type VideoAttributes = Omit<VideoCreate, 'channelId'> & {
|
||||||
duration: number
|
duration: number
|
||||||
isLive: boolean
|
isLive: boolean
|
||||||
state: VideoStateType
|
state: VideoStateType
|
||||||
filename: string
|
inputFilename: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type LiveAttributes = Pick<LiveVideoCreate, 'permanentLive' | 'latencyMode' | 'saveReplay' | 'replaySettings'> & {
|
type LiveAttributes = Pick<LiveVideoCreate, 'permanentLive' | 'latencyMode' | 'saveReplay' | 'replaySettings'> & {
|
||||||
|
@ -64,6 +62,8 @@ export class LocalVideoCreator {
|
||||||
private readonly lTags: LoggerTagsFn
|
private readonly lTags: LoggerTagsFn
|
||||||
|
|
||||||
private readonly videoFilePath: string | undefined
|
private readonly videoFilePath: string | undefined
|
||||||
|
private readonly videoFileProbe: FfprobeData
|
||||||
|
|
||||||
private readonly videoAttributes: VideoAttributes
|
private readonly videoAttributes: VideoAttributes
|
||||||
private readonly liveAttributes: LiveAttributes | undefined
|
private readonly liveAttributes: LiveAttributes | undefined
|
||||||
|
|
||||||
|
@ -72,12 +72,15 @@ export class LocalVideoCreator {
|
||||||
|
|
||||||
private video: MVideoFullLight
|
private video: MVideoFullLight
|
||||||
private videoFile: MVideoFile
|
private videoFile: MVideoFile
|
||||||
private ffprobe: Ffmpeg.FfprobeData
|
private videoPath: string
|
||||||
|
|
||||||
constructor (private readonly options: {
|
constructor (private readonly options: {
|
||||||
lTags: LoggerTagsFn
|
lTags: LoggerTagsFn
|
||||||
|
|
||||||
videoFilePath: string
|
videoFile: {
|
||||||
|
path: string
|
||||||
|
probe: FfprobeData
|
||||||
|
}
|
||||||
|
|
||||||
videoAttributes: VideoAttributes
|
videoAttributes: VideoAttributes
|
||||||
liveAttributes: LiveAttributes
|
liveAttributes: LiveAttributes
|
||||||
|
@ -93,7 +96,8 @@ export class LocalVideoCreator {
|
||||||
finalFallback: ChaptersOption | undefined
|
finalFallback: ChaptersOption | undefined
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
this.videoFilePath = options.videoFilePath
|
this.videoFilePath = options.videoFile?.path
|
||||||
|
this.videoFileProbe = options.videoFile?.probe
|
||||||
|
|
||||||
this.videoAttributes = options.videoAttributes
|
this.videoAttributes = options.videoAttributes
|
||||||
this.liveAttributes = options.liveAttributes
|
this.liveAttributes = options.liveAttributes
|
||||||
|
@ -112,11 +116,10 @@ export class LocalVideoCreator {
|
||||||
this.video.url = getLocalVideoActivityPubUrl(this.video)
|
this.video.url = getLocalVideoActivityPubUrl(this.video)
|
||||||
|
|
||||||
if (this.videoFilePath) {
|
if (this.videoFilePath) {
|
||||||
this.ffprobe = await ffprobePromise(this.videoFilePath)
|
this.videoFile = await buildNewFile({ path: this.videoFilePath, mode: 'web-video', ffprobe: this.videoFileProbe })
|
||||||
this.videoFile = await buildNewFile({ path: this.videoFilePath, mode: 'web-video', ffprobe: this.ffprobe })
|
|
||||||
|
|
||||||
const destination = VideoPathManager.Instance.getFSVideoFileOutputPath(this.video, this.videoFile)
|
this.videoPath = VideoPathManager.Instance.getFSVideoFileOutputPath(this.video, this.videoFile)
|
||||||
await move(this.videoFilePath, destination)
|
await move(this.videoFilePath, this.videoPath)
|
||||||
|
|
||||||
this.video.aspectRatio = buildAspectRatio({ width: this.videoFile.width, height: this.videoFile.height })
|
this.video.aspectRatio = buildAspectRatio({ width: this.videoFile.width, height: this.videoFile.height })
|
||||||
}
|
}
|
||||||
|
@ -166,13 +169,6 @@ export class LocalVideoCreator {
|
||||||
transaction
|
transaction
|
||||||
})
|
})
|
||||||
|
|
||||||
if (this.videoAttributes.filename) {
|
|
||||||
await VideoSourceModel.create({
|
|
||||||
filename: this.videoAttributes.filename,
|
|
||||||
videoId: this.video.id
|
|
||||||
}, { transaction })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.videoAttributes.privacy === VideoPrivacy.PASSWORD_PROTECTED) {
|
if (this.videoAttributes.privacy === VideoPrivacy.PASSWORD_PROTECTED) {
|
||||||
await VideoPasswordModel.addPasswords(this.videoAttributes.videoPasswords, this.video.id, transaction)
|
await VideoPasswordModel.addPasswords(this.videoAttributes.videoPasswords, this.video.id, transaction)
|
||||||
}
|
}
|
||||||
|
@ -197,6 +193,7 @@ export class LocalVideoCreator {
|
||||||
videoLive.videoId = this.video.id
|
videoLive.videoId = this.video.id
|
||||||
this.video.VideoLive = await videoLive.save({ transaction })
|
this.video.VideoLive = await videoLive.save({ transaction })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.videoFile) {
|
if (this.videoFile) {
|
||||||
transaction.afterCommit(() => {
|
transaction.afterCommit(() => {
|
||||||
addVideoJobsAfterCreation({ video: this.video, videoFile: this.videoFile })
|
addVideoJobsAfterCreation({ video: this.video, videoFile: this.videoFile })
|
||||||
|
@ -218,6 +215,15 @@ export class LocalVideoCreator {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (this.videoAttributes.inputFilename) {
|
||||||
|
await createVideoSource({
|
||||||
|
inputFilename: this.videoAttributes.inputFilename,
|
||||||
|
inputPath: this.videoPath,
|
||||||
|
inputProbe: this.videoFileProbe,
|
||||||
|
video: this.video
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Channel has a new content, set as updated
|
// Channel has a new content, set as updated
|
||||||
await this.channel.setAsUpdated()
|
await this.channel.setAsUpdated()
|
||||||
|
|
||||||
|
@ -248,7 +254,12 @@ export class LocalVideoCreator {
|
||||||
return [
|
return [
|
||||||
...await Promise.all(promises),
|
...await Promise.all(promises),
|
||||||
|
|
||||||
...await generateLocalVideoMiniature({ video: this.video, videoFile: this.videoFile, types: toGenerate, ffprobe: this.ffprobe })
|
...await generateLocalVideoMiniature({
|
||||||
|
video: this.video,
|
||||||
|
videoFile: this.videoFile,
|
||||||
|
types: toGenerate,
|
||||||
|
ffprobe: this.videoFileProbe
|
||||||
|
})
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,25 +1,22 @@
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { MStreamingPlaylistVideo } from '@server/types/models/index.js'
|
import { MStreamingPlaylistVideo } from '@server/types/models/index.js'
|
||||||
|
|
||||||
function generateHLSObjectStorageKey (playlist: MStreamingPlaylistVideo, filename: string) {
|
export function generateHLSObjectStorageKey (playlist: MStreamingPlaylistVideo, filename: string) {
|
||||||
return join(generateHLSObjectBaseStorageKey(playlist), filename)
|
return join(generateHLSObjectBaseStorageKey(playlist), filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateHLSObjectBaseStorageKey (playlist: MStreamingPlaylistVideo) {
|
export function generateHLSObjectBaseStorageKey (playlist: MStreamingPlaylistVideo) {
|
||||||
return join(playlist.getStringType(), playlist.Video.uuid)
|
return join(playlist.getStringType(), playlist.Video.uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateWebVideoObjectStorageKey (filename: string) {
|
export function generateWebVideoObjectStorageKey (filename: string) {
|
||||||
return filename
|
return filename
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateUserExportObjectStorageKey (filename: string) {
|
export function generateOriginalVideoObjectStorageKey (filename: string) {
|
||||||
return filename
|
return filename
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export function generateUserExportObjectStorageKey (filename: string) {
|
||||||
generateHLSObjectStorageKey,
|
return filename
|
||||||
generateHLSObjectBaseStorageKey,
|
|
||||||
generateWebVideoObjectStorageKey,
|
|
||||||
generateUserExportObjectStorageKey
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,14 @@
|
||||||
import { CONFIG } from '@server/initializers/config.js'
|
import { CONFIG } from '@server/initializers/config.js'
|
||||||
import { MStreamingPlaylistVideo, MUserExport, MVideoFile } from '@server/types/models/index.js'
|
import { MStreamingPlaylistVideo, MUserExport, MVideoFile } from '@server/types/models/index.js'
|
||||||
import { generateHLSObjectStorageKey, generateUserExportObjectStorageKey, generateWebVideoObjectStorageKey } from './keys.js'
|
import { MVideoSource } from '@server/types/models/video/video-source.js'
|
||||||
|
import {
|
||||||
|
generateHLSObjectStorageKey,
|
||||||
|
generateOriginalVideoObjectStorageKey,
|
||||||
|
generateUserExportObjectStorageKey,
|
||||||
|
generateWebVideoObjectStorageKey
|
||||||
|
} from './keys.js'
|
||||||
import { buildKey, getClient } from './shared/index.js'
|
import { buildKey, getClient } from './shared/index.js'
|
||||||
import { getHLSPublicFileUrl, getWebVideoPublicFileUrl } from './urls.js'
|
import { getObjectStoragePublicFileUrl } from './urls.js'
|
||||||
|
|
||||||
export async function generateWebVideoPresignedUrl (options: {
|
export async function generateWebVideoPresignedUrl (options: {
|
||||||
file: MVideoFile
|
file: MVideoFile
|
||||||
|
@ -16,7 +22,7 @@ export async function generateWebVideoPresignedUrl (options: {
|
||||||
downloadFilename
|
downloadFilename
|
||||||
})
|
})
|
||||||
|
|
||||||
return getWebVideoPublicFileUrl(url)
|
return getObjectStoragePublicFileUrl(url, CONFIG.OBJECT_STORAGE.WEB_VIDEOS)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateHLSFilePresignedUrl (options: {
|
export async function generateHLSFilePresignedUrl (options: {
|
||||||
|
@ -32,7 +38,7 @@ export async function generateHLSFilePresignedUrl (options: {
|
||||||
downloadFilename
|
downloadFilename
|
||||||
})
|
})
|
||||||
|
|
||||||
return getHLSPublicFileUrl(url)
|
return getObjectStoragePublicFileUrl(url, CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateUserExportPresignedUrl (options: {
|
export async function generateUserExportPresignedUrl (options: {
|
||||||
|
@ -47,7 +53,22 @@ export async function generateUserExportPresignedUrl (options: {
|
||||||
downloadFilename
|
downloadFilename
|
||||||
})
|
})
|
||||||
|
|
||||||
return getHLSPublicFileUrl(url)
|
return getObjectStoragePublicFileUrl(url, CONFIG.OBJECT_STORAGE.USER_EXPORTS)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateOriginalFilePresignedUrl (options: {
|
||||||
|
videoSource: MVideoSource
|
||||||
|
downloadFilename: string
|
||||||
|
}) {
|
||||||
|
const { videoSource, downloadFilename } = options
|
||||||
|
|
||||||
|
const url = await generatePresignedUrl({
|
||||||
|
bucket: CONFIG.OBJECT_STORAGE.ORIGINAL_VIDEO_FILES.BUCKET_NAME,
|
||||||
|
key: buildKey(generateOriginalVideoObjectStorageKey(videoSource.keptOriginalFilename), CONFIG.OBJECT_STORAGE.ORIGINAL_VIDEO_FILES),
|
||||||
|
downloadFilename
|
||||||
|
})
|
||||||
|
|
||||||
|
return getObjectStoragePublicFileUrl(url, CONFIG.OBJECT_STORAGE.ORIGINAL_VIDEO_FILES)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
@ -1,23 +1,15 @@
|
||||||
import { CONFIG } from '@server/initializers/config.js'
|
|
||||||
import { OBJECT_STORAGE_PROXY_PATHS, WEBSERVER } from '@server/initializers/constants.js'
|
import { OBJECT_STORAGE_PROXY_PATHS, WEBSERVER } from '@server/initializers/constants.js'
|
||||||
import { MVideoUUID } from '@server/types/models/index.js'
|
import { MVideoUUID } from '@server/types/models/index.js'
|
||||||
import { BucketInfo, buildKey, getEndpointParsed } from './shared/index.js'
|
import { BucketInfo, buildKey, getEndpointParsed } from './shared/index.js'
|
||||||
|
|
||||||
function getInternalUrl (config: BucketInfo, keyWithoutPrefix: string) {
|
export function getInternalUrl (config: BucketInfo, keyWithoutPrefix: string) {
|
||||||
return getBaseUrl(config) + buildKey(keyWithoutPrefix, config)
|
return getBaseUrl(config) + buildKey(keyWithoutPrefix, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function getWebVideoPublicFileUrl (fileUrl: string) {
|
export function getObjectStoragePublicFileUrl (fileUrl: string, objectStorageConfig: { BASE_URL: string }) {
|
||||||
const baseUrl = CONFIG.OBJECT_STORAGE.WEB_VIDEOS.BASE_URL
|
const baseUrl = objectStorageConfig.BASE_URL
|
||||||
if (!baseUrl) return fileUrl
|
|
||||||
|
|
||||||
return replaceByBaseUrl(fileUrl, baseUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getHLSPublicFileUrl (fileUrl: string) {
|
|
||||||
const baseUrl = CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS.BASE_URL
|
|
||||||
if (!baseUrl) return fileUrl
|
if (!baseUrl) return fileUrl
|
||||||
|
|
||||||
return replaceByBaseUrl(fileUrl, baseUrl)
|
return replaceByBaseUrl(fileUrl, baseUrl)
|
||||||
|
@ -25,28 +17,13 @@ function getHLSPublicFileUrl (fileUrl: string) {
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function getHLSPrivateFileUrl (video: MVideoUUID, filename: string) {
|
export function getHLSPrivateFileUrl (video: MVideoUUID, filename: string) {
|
||||||
return WEBSERVER.URL + OBJECT_STORAGE_PROXY_PATHS.STREAMING_PLAYLISTS.PRIVATE_HLS + video.uuid + `/${filename}`
|
return WEBSERVER.URL + OBJECT_STORAGE_PROXY_PATHS.STREAMING_PLAYLISTS.PRIVATE_HLS + video.uuid + `/${filename}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWebVideoPrivateFileUrl (filename: string) {
|
export function getWebVideoPrivateFileUrl (filename: string) {
|
||||||
return WEBSERVER.URL + OBJECT_STORAGE_PROXY_PATHS.PRIVATE_WEB_VIDEOS + filename
|
return WEBSERVER.URL + OBJECT_STORAGE_PROXY_PATHS.PRIVATE_WEB_VIDEOS + filename
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
export {
|
|
||||||
getInternalUrl,
|
|
||||||
|
|
||||||
getWebVideoPublicFileUrl,
|
|
||||||
getHLSPublicFileUrl,
|
|
||||||
|
|
||||||
getHLSPrivateFileUrl,
|
|
||||||
getWebVideoPrivateFileUrl,
|
|
||||||
|
|
||||||
replaceByBaseUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function getBaseUrl (bucketInfo: BucketInfo, baseUrl?: string) {
|
function getBaseUrl (bucketInfo: BucketInfo, baseUrl?: string) {
|
||||||
|
|
|
@ -1,14 +1,20 @@
|
||||||
import { basename, join } from 'path'
|
|
||||||
import { logger } from '@server/helpers/logger.js'
|
import { logger } from '@server/helpers/logger.js'
|
||||||
import { CONFIG } from '@server/initializers/config.js'
|
import { CONFIG } from '@server/initializers/config.js'
|
||||||
import { MStreamingPlaylistVideo, MVideo, MVideoFile } from '@server/types/models/index.js'
|
import { MStreamingPlaylistVideo, MVideo, MVideoFile } from '@server/types/models/index.js'
|
||||||
|
import { MVideoSource } from '@server/types/models/video/video-source.js'
|
||||||
|
import { basename, join } from 'path'
|
||||||
import { getHLSDirectory } from '../paths.js'
|
import { getHLSDirectory } from '../paths.js'
|
||||||
import { VideoPathManager } from '../video-path-manager.js'
|
import { VideoPathManager } from '../video-path-manager.js'
|
||||||
import { generateHLSObjectBaseStorageKey, generateHLSObjectStorageKey, generateWebVideoObjectStorageKey } from './keys.js'
|
import {
|
||||||
|
generateHLSObjectBaseStorageKey,
|
||||||
|
generateHLSObjectStorageKey,
|
||||||
|
generateOriginalVideoObjectStorageKey,
|
||||||
|
generateWebVideoObjectStorageKey
|
||||||
|
} from './keys.js'
|
||||||
import {
|
import {
|
||||||
createObjectReadStream,
|
createObjectReadStream,
|
||||||
listKeysOfPrefix,
|
|
||||||
lTags,
|
lTags,
|
||||||
|
listKeysOfPrefix,
|
||||||
makeAvailable,
|
makeAvailable,
|
||||||
removeObject,
|
removeObject,
|
||||||
removeObjectByFullKey,
|
removeObjectByFullKey,
|
||||||
|
@ -19,13 +25,13 @@ import {
|
||||||
updatePrefixACL
|
updatePrefixACL
|
||||||
} from './shared/index.js'
|
} from './shared/index.js'
|
||||||
|
|
||||||
function listHLSFileKeysOf (playlist: MStreamingPlaylistVideo) {
|
export function listHLSFileKeysOf (playlist: MStreamingPlaylistVideo) {
|
||||||
return listKeysOfPrefix(generateHLSObjectBaseStorageKey(playlist), CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS)
|
return listKeysOfPrefix(generateHLSObjectBaseStorageKey(playlist), CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function storeHLSFileFromFilename (playlist: MStreamingPlaylistVideo, filename: string) {
|
export function storeHLSFileFromFilename (playlist: MStreamingPlaylistVideo, filename: string) {
|
||||||
return storeObject({
|
return storeObject({
|
||||||
inputPath: join(getHLSDirectory(playlist.Video), filename),
|
inputPath: join(getHLSDirectory(playlist.Video), filename),
|
||||||
objectStorageKey: generateHLSObjectStorageKey(playlist, filename),
|
objectStorageKey: generateHLSObjectStorageKey(playlist, filename),
|
||||||
|
@ -34,7 +40,7 @@ function storeHLSFileFromFilename (playlist: MStreamingPlaylistVideo, filename:
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function storeHLSFileFromPath (playlist: MStreamingPlaylistVideo, path: string) {
|
export function storeHLSFileFromPath (playlist: MStreamingPlaylistVideo, path: string) {
|
||||||
return storeObject({
|
return storeObject({
|
||||||
inputPath: path,
|
inputPath: path,
|
||||||
objectStorageKey: generateHLSObjectStorageKey(playlist, basename(path)),
|
objectStorageKey: generateHLSObjectStorageKey(playlist, basename(path)),
|
||||||
|
@ -43,7 +49,7 @@ function storeHLSFileFromPath (playlist: MStreamingPlaylistVideo, path: string)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function storeHLSFileFromContent (playlist: MStreamingPlaylistVideo, path: string, content: string) {
|
export function storeHLSFileFromContent (playlist: MStreamingPlaylistVideo, path: string, content: string) {
|
||||||
return storeContent({
|
return storeContent({
|
||||||
content,
|
content,
|
||||||
inputPath: path,
|
inputPath: path,
|
||||||
|
@ -55,7 +61,7 @@ function storeHLSFileFromContent (playlist: MStreamingPlaylistVideo, path: strin
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function storeWebVideoFile (video: MVideo, file: MVideoFile) {
|
export function storeWebVideoFile (video: MVideo, file: MVideoFile) {
|
||||||
return storeObject({
|
return storeObject({
|
||||||
inputPath: VideoPathManager.Instance.getFSVideoFileOutputPath(video, file),
|
inputPath: VideoPathManager.Instance.getFSVideoFileOutputPath(video, file),
|
||||||
objectStorageKey: generateWebVideoObjectStorageKey(file.filename),
|
objectStorageKey: generateWebVideoObjectStorageKey(file.filename),
|
||||||
|
@ -66,7 +72,18 @@ function storeWebVideoFile (video: MVideo, file: MVideoFile) {
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async function updateWebVideoFileACL (video: MVideo, file: MVideoFile) {
|
export function storeOriginalVideoFile (inputPath: string, filename: string) {
|
||||||
|
return storeObject({
|
||||||
|
inputPath,
|
||||||
|
objectStorageKey: generateOriginalVideoObjectStorageKey(filename),
|
||||||
|
bucketInfo: CONFIG.OBJECT_STORAGE.ORIGINAL_VIDEO_FILES,
|
||||||
|
isPrivate: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export async function updateWebVideoFileACL (video: MVideo, file: MVideoFile) {
|
||||||
await updateObjectACL({
|
await updateObjectACL({
|
||||||
objectStorageKey: generateWebVideoObjectStorageKey(file.filename),
|
objectStorageKey: generateWebVideoObjectStorageKey(file.filename),
|
||||||
bucketInfo: CONFIG.OBJECT_STORAGE.WEB_VIDEOS,
|
bucketInfo: CONFIG.OBJECT_STORAGE.WEB_VIDEOS,
|
||||||
|
@ -74,7 +91,7 @@ async function updateWebVideoFileACL (video: MVideo, file: MVideoFile) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateHLSFilesACL (playlist: MStreamingPlaylistVideo) {
|
export async function updateHLSFilesACL (playlist: MStreamingPlaylistVideo) {
|
||||||
await updatePrefixACL({
|
await updatePrefixACL({
|
||||||
prefix: generateHLSObjectBaseStorageKey(playlist),
|
prefix: generateHLSObjectBaseStorageKey(playlist),
|
||||||
bucketInfo: CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS,
|
bucketInfo: CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS,
|
||||||
|
@ -84,31 +101,37 @@ async function updateHLSFilesACL (playlist: MStreamingPlaylistVideo) {
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function removeHLSObjectStorage (playlist: MStreamingPlaylistVideo) {
|
export function removeHLSObjectStorage (playlist: MStreamingPlaylistVideo) {
|
||||||
return removePrefix(generateHLSObjectBaseStorageKey(playlist), CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS)
|
return removePrefix(generateHLSObjectBaseStorageKey(playlist), CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS)
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeHLSFileObjectStorageByFilename (playlist: MStreamingPlaylistVideo, filename: string) {
|
export function removeHLSFileObjectStorageByFilename (playlist: MStreamingPlaylistVideo, filename: string) {
|
||||||
return removeObject(generateHLSObjectStorageKey(playlist, filename), CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS)
|
return removeObject(generateHLSObjectStorageKey(playlist, filename), CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS)
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeHLSFileObjectStorageByPath (playlist: MStreamingPlaylistVideo, path: string) {
|
export function removeHLSFileObjectStorageByPath (playlist: MStreamingPlaylistVideo, path: string) {
|
||||||
return removeObject(generateHLSObjectStorageKey(playlist, basename(path)), CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS)
|
return removeObject(generateHLSObjectStorageKey(playlist, basename(path)), CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS)
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeHLSFileObjectStorageByFullKey (key: string) {
|
export function removeHLSFileObjectStorageByFullKey (key: string) {
|
||||||
return removeObjectByFullKey(key, CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS)
|
return removeObjectByFullKey(key, CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function removeWebVideoObjectStorage (videoFile: MVideoFile) {
|
export function removeWebVideoObjectStorage (videoFile: MVideoFile) {
|
||||||
return removeObject(generateWebVideoObjectStorageKey(videoFile.filename), CONFIG.OBJECT_STORAGE.WEB_VIDEOS)
|
return removeObject(generateWebVideoObjectStorageKey(videoFile.filename), CONFIG.OBJECT_STORAGE.WEB_VIDEOS)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async function makeHLSFileAvailable (playlist: MStreamingPlaylistVideo, filename: string, destination: string) {
|
export function removeOriginalFileObjectStorage (videoSource: MVideoSource) {
|
||||||
|
return removeObject(generateOriginalVideoObjectStorageKey(videoSource.keptOriginalFilename), CONFIG.OBJECT_STORAGE.ORIGINAL_VIDEO_FILES)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export async function makeHLSFileAvailable (playlist: MStreamingPlaylistVideo, filename: string, destination: string) {
|
||||||
const key = generateHLSObjectStorageKey(playlist, filename)
|
const key = generateHLSObjectStorageKey(playlist, filename)
|
||||||
|
|
||||||
logger.info('Fetching HLS file %s from object storage to %s.', key, destination, lTags())
|
logger.info('Fetching HLS file %s from object storage to %s.', key, destination, lTags())
|
||||||
|
@ -122,7 +145,7 @@ async function makeHLSFileAvailable (playlist: MStreamingPlaylistVideo, filename
|
||||||
return destination
|
return destination
|
||||||
}
|
}
|
||||||
|
|
||||||
async function makeWebVideoFileAvailable (filename: string, destination: string) {
|
export async function makeWebVideoFileAvailable (filename: string, destination: string) {
|
||||||
const key = generateWebVideoObjectStorageKey(filename)
|
const key = generateWebVideoObjectStorageKey(filename)
|
||||||
|
|
||||||
logger.info('Fetching Web Video file %s from object storage to %s.', key, destination, lTags())
|
logger.info('Fetching Web Video file %s from object storage to %s.', key, destination, lTags())
|
||||||
|
@ -138,7 +161,7 @@ async function makeWebVideoFileAvailable (filename: string, destination: string)
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function getWebVideoFileReadStream (options: {
|
export function getWebVideoFileReadStream (options: {
|
||||||
filename: string
|
filename: string
|
||||||
rangeHeader: string
|
rangeHeader: string
|
||||||
}) {
|
}) {
|
||||||
|
@ -153,7 +176,7 @@ function getWebVideoFileReadStream (options: {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function getHLSFileReadStream (options: {
|
export function getHLSFileReadStream (options: {
|
||||||
playlist: MStreamingPlaylistVideo
|
playlist: MStreamingPlaylistVideo
|
||||||
filename: string
|
filename: string
|
||||||
rangeHeader: string
|
rangeHeader: string
|
||||||
|
@ -169,29 +192,17 @@ function getHLSFileReadStream (options: {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
export function getOriginalFileReadStream (options: {
|
||||||
|
keptOriginalFilename: string
|
||||||
|
rangeHeader: string
|
||||||
|
}) {
|
||||||
|
const { keptOriginalFilename, rangeHeader } = options
|
||||||
|
|
||||||
export {
|
const key = generateOriginalVideoObjectStorageKey(keptOriginalFilename)
|
||||||
listHLSFileKeysOf,
|
|
||||||
|
|
||||||
storeWebVideoFile,
|
return createObjectReadStream({
|
||||||
storeHLSFileFromFilename,
|
key,
|
||||||
storeHLSFileFromPath,
|
bucketInfo: CONFIG.OBJECT_STORAGE.ORIGINAL_VIDEO_FILES,
|
||||||
storeHLSFileFromContent,
|
rangeHeader
|
||||||
|
})
|
||||||
updateWebVideoFileACL,
|
|
||||||
updateHLSFilesACL,
|
|
||||||
|
|
||||||
removeHLSObjectStorage,
|
|
||||||
removeHLSFileObjectStorageByFilename,
|
|
||||||
removeHLSFileObjectStorageByPath,
|
|
||||||
removeHLSFileObjectStorageByFullKey,
|
|
||||||
|
|
||||||
removeWebVideoObjectStorage,
|
|
||||||
|
|
||||||
makeWebVideoFileAvailable,
|
|
||||||
makeHLSFileAvailable,
|
|
||||||
|
|
||||||
getWebVideoFileReadStream,
|
|
||||||
getHLSFileReadStream
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -100,7 +100,7 @@ function generateLocalVideoMiniature (options: {
|
||||||
video: MVideoThumbnail
|
video: MVideoThumbnail
|
||||||
videoFile: MVideoFile
|
videoFile: MVideoFile
|
||||||
types: ThumbnailType_Type[]
|
types: ThumbnailType_Type[]
|
||||||
ffprobe?: FfprobeData
|
ffprobe: FfprobeData
|
||||||
}): Promise<MThumbnail[]> {
|
}): Promise<MThumbnail[]> {
|
||||||
const { video, videoFile, types, ffprobe } = options
|
const { video, videoFile, types, ffprobe } = options
|
||||||
|
|
||||||
|
@ -223,7 +223,7 @@ function updateRemoteVideoThumbnail (options: {
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async function regenerateMiniaturesIfNeeded (video: MVideoWithAllFiles) {
|
async function regenerateMiniaturesIfNeeded (video: MVideoWithAllFiles, ffprobe: FfprobeData) {
|
||||||
const thumbnailsToGenerate: ThumbnailType_Type[] = []
|
const thumbnailsToGenerate: ThumbnailType_Type[] = []
|
||||||
|
|
||||||
if (video.getMiniature().automaticallyGenerated === true) {
|
if (video.getMiniature().automaticallyGenerated === true) {
|
||||||
|
@ -237,6 +237,7 @@ async function regenerateMiniaturesIfNeeded (video: MVideoWithAllFiles) {
|
||||||
const models = await generateLocalVideoMiniature({
|
const models = await generateLocalVideoMiniature({
|
||||||
video,
|
video,
|
||||||
videoFile: video.getMaxQualityFile(),
|
videoFile: video.getMaxQualityFile(),
|
||||||
|
ffprobe,
|
||||||
types: thumbnailsToGenerate
|
types: thumbnailsToGenerate
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { ffprobePromise, getVideoStreamDimensionsInfo, getVideoStreamFPS, hasAudioStream, isAudioFile } from '@peertube/peertube-ffmpeg'
|
||||||
import { computeOutputFPS } from '@server/helpers/ffmpeg/index.js'
|
import { computeOutputFPS } from '@server/helpers/ffmpeg/index.js'
|
||||||
import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
|
import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
|
||||||
import { CONFIG } from '@server/initializers/config.js'
|
import { CONFIG } from '@server/initializers/config.js'
|
||||||
|
@ -11,7 +12,6 @@ import {
|
||||||
import { VideoPathManager } from '@server/lib/video-path-manager.js'
|
import { VideoPathManager } from '@server/lib/video-path-manager.js'
|
||||||
import { MUserId, MVideoFile, MVideoFullLight, MVideoWithFileThumbnail } from '@server/types/models/index.js'
|
import { MUserId, MVideoFile, MVideoFullLight, MVideoWithFileThumbnail } from '@server/types/models/index.js'
|
||||||
import { MRunnerJob } from '@server/types/models/runners/index.js'
|
import { MRunnerJob } from '@server/types/models/runners/index.js'
|
||||||
import { ffprobePromise, getVideoStreamDimensionsInfo, getVideoStreamFPS, hasAudioStream, isAudioFile } from '@peertube/peertube-ffmpeg'
|
|
||||||
import { getTranscodingJobPriority } from '../../transcoding-priority.js'
|
import { getTranscodingJobPriority } from '../../transcoding-priority.js'
|
||||||
import { buildOriginalFileResolution, computeResolutionsToTranscode } from '../../transcoding-resolutions.js'
|
import { buildOriginalFileResolution, computeResolutionsToTranscode } from '../../transcoding-resolutions.js'
|
||||||
import { AbstractJobBuilder } from './abstract-job-builder.js'
|
import { AbstractJobBuilder } from './abstract-job-builder.js'
|
||||||
|
@ -60,11 +60,7 @@ export class TranscodingRunnerJobBuilder extends AbstractJobBuilder {
|
||||||
const fps = computeOutputFPS({ inputFPS, resolution: maxResolution })
|
const fps = computeOutputFPS({ inputFPS, resolution: maxResolution })
|
||||||
const priority = await getTranscodingJobPriority({ user, type: 'vod', fallback: 0 })
|
const priority = await getTranscodingJobPriority({ user, type: 'vod', fallback: 0 })
|
||||||
|
|
||||||
const deleteInputFileId = isAudioInput || maxResolution !== resolution
|
const jobPayload = { video, resolution: maxResolution, fps, isNewVideo, priority, deleteInputFileId: videoFile.id }
|
||||||
? videoFile.id
|
|
||||||
: null
|
|
||||||
|
|
||||||
const jobPayload = { video, resolution: maxResolution, fps, isNewVideo, priority, deleteInputFileId }
|
|
||||||
|
|
||||||
const mainRunnerJob = videoFile.isAudio()
|
const mainRunnerJob = videoFile.isAudio()
|
||||||
? await new VODAudioMergeTranscodingJobHandler().create(jobPayload)
|
? await new VODAudioMergeTranscodingJobHandler().create(jobPayload)
|
||||||
|
|
|
@ -1,22 +1,22 @@
|
||||||
import { Job } from 'bullmq'
|
import { buildAspectRatio } from '@peertube/peertube-core-utils'
|
||||||
import { move, remove } from 'fs-extra/esm'
|
import { TranscodeVODOptionsType, getVideoStreamDuration } from '@peertube/peertube-ffmpeg'
|
||||||
import { copyFile } from 'fs/promises'
|
|
||||||
import { basename, join } from 'path'
|
|
||||||
import { computeOutputFPS } from '@server/helpers/ffmpeg/index.js'
|
import { computeOutputFPS } from '@server/helpers/ffmpeg/index.js'
|
||||||
import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent.js'
|
import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent.js'
|
||||||
import { VideoModel } from '@server/models/video/video.js'
|
import { VideoModel } from '@server/models/video/video.js'
|
||||||
import { MVideoFile, MVideoFullLight } from '@server/types/models/index.js'
|
import { MVideoFile, MVideoFullLight } from '@server/types/models/index.js'
|
||||||
import { getVideoStreamDuration, TranscodeVODOptionsType } from '@peertube/peertube-ffmpeg'
|
import { Job } from 'bullmq'
|
||||||
|
import { move, remove } from 'fs-extra/esm'
|
||||||
|
import { copyFile } from 'fs/promises'
|
||||||
|
import { basename, join } from 'path'
|
||||||
import { CONFIG } from '../../initializers/config.js'
|
import { CONFIG } from '../../initializers/config.js'
|
||||||
import { VideoFileModel } from '../../models/video/video-file.js'
|
import { VideoFileModel } from '../../models/video/video-file.js'
|
||||||
import { JobQueue } from '../job-queue/index.js'
|
import { JobQueue } from '../job-queue/index.js'
|
||||||
import { generateWebVideoFilename } from '../paths.js'
|
import { generateWebVideoFilename } from '../paths.js'
|
||||||
import { buildNewFile } from '../video-file.js'
|
import { buildNewFile, saveNewOriginalFileIfNeeded } from '../video-file.js'
|
||||||
|
import { buildStoryboardJobIfNeeded } from '../video-jobs.js'
|
||||||
import { VideoPathManager } from '../video-path-manager.js'
|
import { VideoPathManager } from '../video-path-manager.js'
|
||||||
import { buildFFmpegVOD } from './shared/index.js'
|
import { buildFFmpegVOD } from './shared/index.js'
|
||||||
import { buildOriginalFileResolution } from './transcoding-resolutions.js'
|
import { buildOriginalFileResolution } from './transcoding-resolutions.js'
|
||||||
import { buildStoryboardJobIfNeeded } from '../video-jobs.js'
|
|
||||||
import { buildAspectRatio } from '@peertube/peertube-core-utils'
|
|
||||||
|
|
||||||
// Optimize the original video file and replace it. The resolution is not changed.
|
// Optimize the original video file and replace it. The resolution is not changed.
|
||||||
export async function optimizeOriginalVideofile (options: {
|
export async function optimizeOriginalVideofile (options: {
|
||||||
|
@ -73,7 +73,7 @@ export async function optimizeOriginalVideofile (options: {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transcode the original video file to a lower resolution compatible with web browsers
|
// Transcode the original/old/source video file to a lower resolution compatible with web browsers
|
||||||
export async function transcodeNewWebVideoResolution (options: {
|
export async function transcodeNewWebVideoResolution (options: {
|
||||||
video: MVideoFullLight
|
video: MVideoFullLight
|
||||||
resolution: number
|
resolution: number
|
||||||
|
@ -162,7 +162,6 @@ export async function mergeAudioVideofile (options: {
|
||||||
try {
|
try {
|
||||||
await buildFFmpegVOD(job).transcode(transcodeOptions)
|
await buildFFmpegVOD(job).transcode(transcodeOptions)
|
||||||
|
|
||||||
await remove(audioInputPath)
|
|
||||||
await remove(tmpPreviewPath)
|
await remove(tmpPreviewPath)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await remove(tmpPreviewPath)
|
await remove(tmpPreviewPath)
|
||||||
|
@ -213,14 +212,16 @@ export async function onWebVideoFileTranscoding (options: {
|
||||||
|
|
||||||
await createTorrentAndSetInfoHash(video, videoFile)
|
await createTorrentAndSetInfoHash(video, videoFile)
|
||||||
|
|
||||||
const oldFile = await VideoFileModel.loadWebVideoFile({ videoId: video.id, fps: videoFile.fps, resolution: videoFile.resolution })
|
|
||||||
if (oldFile) await video.removeWebVideoFile(oldFile)
|
|
||||||
|
|
||||||
if (deleteWebInputVideoFile) {
|
if (deleteWebInputVideoFile) {
|
||||||
|
await saveNewOriginalFileIfNeeded(video, deleteWebInputVideoFile)
|
||||||
|
|
||||||
await video.removeWebVideoFile(deleteWebInputVideoFile)
|
await video.removeWebVideoFile(deleteWebInputVideoFile)
|
||||||
await deleteWebInputVideoFile.destroy()
|
await deleteWebInputVideoFile.destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const existingFile = await VideoFileModel.loadWebVideoFile({ videoId: video.id, fps: videoFile.fps, resolution: videoFile.resolution })
|
||||||
|
if (existingFile) await video.removeWebVideoFile(existingFile)
|
||||||
|
|
||||||
await VideoFileModel.customUpsert(videoFile, 'video', undefined)
|
await VideoFileModel.customUpsert(videoFile, 'video', undefined)
|
||||||
video.VideoFiles = await video.$get('VideoFiles')
|
video.VideoFiles = await video.$get('VideoFiles')
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,19 @@
|
||||||
import { VideoModel } from '@server/models/video/video.js'
|
import { pick } from '@peertube/peertube-core-utils'
|
||||||
|
import { ActivityCreate, FileStorage, VideoExportJSON, VideoObject, VideoPrivacy } from '@peertube/peertube-models'
|
||||||
|
import { logger } from '@server/helpers/logger.js'
|
||||||
|
import { USER_EXPORT_MAX_ITEMS } from '@server/initializers/constants.js'
|
||||||
|
import { audiencify, getAudience } from '@server/lib/activitypub/audience.js'
|
||||||
|
import { buildCreateActivity } from '@server/lib/activitypub/send/send-create.js'
|
||||||
|
import { buildChaptersAPHasPart } from '@server/lib/activitypub/video-chapters.js'
|
||||||
|
import { getHLSFileReadStream, getOriginalFileReadStream, getWebVideoFileReadStream } from '@server/lib/object-storage/videos.js'
|
||||||
|
import { VideoPathManager } from '@server/lib/video-path-manager.js'
|
||||||
import { VideoCaptionModel } from '@server/models/video/video-caption.js'
|
import { VideoCaptionModel } from '@server/models/video/video-caption.js'
|
||||||
import { VideoChannelModel } from '@server/models/video/video-channel.js'
|
import { VideoChannelModel } from '@server/models/video/video-channel.js'
|
||||||
|
import { VideoChapterModel } from '@server/models/video/video-chapter.js'
|
||||||
import { VideoLiveModel } from '@server/models/video/video-live.js'
|
import { VideoLiveModel } from '@server/models/video/video-live.js'
|
||||||
import { ExportResult, AbstractUserExporter } from './abstract-user-exporter.js'
|
import { VideoPasswordModel } from '@server/models/video/video-password.js'
|
||||||
|
import { VideoSourceModel } from '@server/models/video/video-source.js'
|
||||||
|
import { VideoModel } from '@server/models/video/video.js'
|
||||||
import {
|
import {
|
||||||
MStreamingPlaylistFiles,
|
MStreamingPlaylistFiles,
|
||||||
MThumbnail, MVideo, MVideoAP, MVideoCaption,
|
MThumbnail, MVideo, MVideoAP, MVideoCaption,
|
||||||
|
@ -12,23 +23,12 @@ import {
|
||||||
MVideoFullLight, MVideoLiveWithSetting,
|
MVideoFullLight, MVideoLiveWithSetting,
|
||||||
MVideoPassword
|
MVideoPassword
|
||||||
} from '@server/types/models/index.js'
|
} from '@server/types/models/index.js'
|
||||||
import { logger } from '@server/helpers/logger.js'
|
import { MVideoSource } from '@server/types/models/video/video-source.js'
|
||||||
import { ActivityCreate, VideoExportJSON, VideoObject, VideoPrivacy, FileStorage } from '@peertube/peertube-models'
|
|
||||||
import Bluebird from 'bluebird'
|
import Bluebird from 'bluebird'
|
||||||
import { getHLSFileReadStream, getWebVideoFileReadStream } from '@server/lib/object-storage/videos.js'
|
|
||||||
import { createReadStream } from 'fs'
|
import { createReadStream } from 'fs'
|
||||||
import { VideoPathManager } from '@server/lib/video-path-manager.js'
|
|
||||||
import { extname, join } from 'path'
|
import { extname, join } from 'path'
|
||||||
import { Readable } from 'stream'
|
import { Readable } from 'stream'
|
||||||
import { getAudience, audiencify } from '@server/lib/activitypub/audience.js'
|
import { AbstractUserExporter, ExportResult } from './abstract-user-exporter.js'
|
||||||
import { buildCreateActivity } from '@server/lib/activitypub/send/send-create.js'
|
|
||||||
import { pick } from '@peertube/peertube-core-utils'
|
|
||||||
import { VideoPasswordModel } from '@server/models/video/video-password.js'
|
|
||||||
import { MVideoSource } from '@server/types/models/video/video-source.js'
|
|
||||||
import { VideoSourceModel } from '@server/models/video/video-source.js'
|
|
||||||
import { VideoChapterModel } from '@server/models/video/video-chapter.js'
|
|
||||||
import { buildChaptersAPHasPart } from '@server/lib/activitypub/video-chapters.js'
|
|
||||||
import { USER_EXPORT_MAX_ITEMS } from '@server/initializers/constants.js'
|
|
||||||
|
|
||||||
export class VideosExporter extends AbstractUserExporter <VideoExportJSON> {
|
export class VideosExporter extends AbstractUserExporter <VideoExportJSON> {
|
||||||
|
|
||||||
|
@ -89,7 +89,7 @@ export class VideosExporter extends AbstractUserExporter <VideoExportJSON> {
|
||||||
// Then fetch more attributes for AP serialization
|
// Then fetch more attributes for AP serialization
|
||||||
const videoAP = await video.lightAPToFullAP(undefined)
|
const videoAP = await video.lightAPToFullAP(undefined)
|
||||||
|
|
||||||
const { relativePathsFromJSON, staticFiles } = this.exportVideoFiles({ video, captions })
|
const { relativePathsFromJSON, staticFiles } = await this.exportVideoFiles({ video, captions })
|
||||||
|
|
||||||
return {
|
return {
|
||||||
json: this.exportVideoJSON({ video, captions, live, passwords, source, chapters, archiveFiles: relativePathsFromJSON }),
|
json: this.exportVideoJSON({ video, captions, live, passwords, source, chapters, archiveFiles: relativePathsFromJSON }),
|
||||||
|
@ -168,9 +168,7 @@ export class VideosExporter extends AbstractUserExporter <VideoExportJSON> {
|
||||||
|
|
||||||
streamingPlaylists: this.exportStreamingPlaylistsJSON(video, video.VideoStreamingPlaylists),
|
streamingPlaylists: this.exportStreamingPlaylistsJSON(video, video.VideoStreamingPlaylists),
|
||||||
|
|
||||||
source: source
|
source: this.exportVideoSourceJSON(source),
|
||||||
? { filename: source.filename }
|
|
||||||
: null,
|
|
||||||
|
|
||||||
archiveFiles
|
archiveFiles
|
||||||
}
|
}
|
||||||
|
@ -228,6 +226,24 @@ export class VideosExporter extends AbstractUserExporter <VideoExportJSON> {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private exportVideoSourceJSON (source: MVideoSource) {
|
||||||
|
if (!source) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
inputFilename: source.inputFilename,
|
||||||
|
|
||||||
|
resolution: source.resolution,
|
||||||
|
size: source.size,
|
||||||
|
|
||||||
|
width: source.width,
|
||||||
|
height: source.height,
|
||||||
|
|
||||||
|
fps: source.fps,
|
||||||
|
|
||||||
|
metadata: source.metadata
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
private async exportVideoAP (video: MVideoAP, chapters: MVideoChapter[]): Promise<ActivityCreate<VideoObject>> {
|
private async exportVideoAP (video: MVideoAP, chapters: MVideoChapter[]): Promise<ActivityCreate<VideoObject>> {
|
||||||
|
@ -271,7 +287,7 @@ export class VideosExporter extends AbstractUserExporter <VideoExportJSON> {
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
private exportVideoFiles (options: {
|
private async exportVideoFiles (options: {
|
||||||
video: MVideoFullLight
|
video: MVideoFullLight
|
||||||
captions: MVideoCaption[]
|
captions: MVideoCaption[]
|
||||||
}) {
|
}) {
|
||||||
|
@ -284,15 +300,27 @@ export class VideosExporter extends AbstractUserExporter <VideoExportJSON> {
|
||||||
captions: {} as { [ lang: string ]: string }
|
captions: {} as { [ lang: string ]: string }
|
||||||
}
|
}
|
||||||
|
|
||||||
const videoFile = video.getMaxQualityFile()
|
if (this.options.withVideoFiles) {
|
||||||
|
const source = await VideoSourceModel.loadLatest(video.id)
|
||||||
|
const maxQualityFile = video.getMaxQualityFile()
|
||||||
|
|
||||||
|
// Prefer using original file if possible
|
||||||
|
const file = source?.keptOriginalFilename
|
||||||
|
? source
|
||||||
|
: maxQualityFile
|
||||||
|
|
||||||
|
if (file) {
|
||||||
|
const videoPath = this.getArchiveVideoFilePath(video, file)
|
||||||
|
|
||||||
if (this.options.withVideoFiles && videoFile) {
|
|
||||||
staticFiles.push({
|
staticFiles.push({
|
||||||
archivePath: this.getArchiveVideoFilePath(video, videoFile),
|
archivePath: videoPath,
|
||||||
createrReadStream: () => this.generateVideoFileReadStream(video, videoFile)
|
createrReadStream: () => file === source
|
||||||
|
? this.generateVideoSourceReadStream(source)
|
||||||
|
: this.generateVideoFileReadStream(video, maxQualityFile)
|
||||||
})
|
})
|
||||||
|
|
||||||
relativePathsFromJSON.videoFile = join(this.relativeStaticDirPath, this.getArchiveVideoFilePath(video, videoFile))
|
relativePathsFromJSON.videoFile = join(this.relativeStaticDirPath, videoPath)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const caption of captions) {
|
for (const caption of captions) {
|
||||||
|
@ -317,6 +345,16 @@ export class VideosExporter extends AbstractUserExporter <VideoExportJSON> {
|
||||||
return { staticFiles, relativePathsFromJSON }
|
return { staticFiles, relativePathsFromJSON }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async generateVideoSourceReadStream (source: MVideoSource): Promise<Readable> {
|
||||||
|
if (source.storage === FileStorage.FILE_SYSTEM) {
|
||||||
|
return createReadStream(VideoPathManager.Instance.getFSOriginalVideoFilePath(source.keptOriginalFilename))
|
||||||
|
}
|
||||||
|
|
||||||
|
const { stream } = await getOriginalFileReadStream({ keptOriginalFilename: source.keptOriginalFilename, rangeHeader: undefined })
|
||||||
|
|
||||||
|
return stream
|
||||||
|
}
|
||||||
|
|
||||||
private async generateVideoFileReadStream (video: MVideoFullLight, videoFile: MVideoFile): Promise<Readable> {
|
private async generateVideoFileReadStream (video: MVideoFullLight, videoFile: MVideoFile): Promise<Readable> {
|
||||||
if (videoFile.storage === FileStorage.FILE_SYSTEM) {
|
if (videoFile.storage === FileStorage.FILE_SYSTEM) {
|
||||||
return createReadStream(VideoPathManager.Instance.getFSVideoFileOutputPath(video, videoFile))
|
return createReadStream(VideoPathManager.Instance.getFSVideoFileOutputPath(video, videoFile))
|
||||||
|
@ -329,8 +367,8 @@ export class VideosExporter extends AbstractUserExporter <VideoExportJSON> {
|
||||||
return stream
|
return stream
|
||||||
}
|
}
|
||||||
|
|
||||||
private getArchiveVideoFilePath (video: MVideo, videoFile: MVideoFile) {
|
private getArchiveVideoFilePath (video: MVideo, file: { filename?: string, keptOriginalFilename?: string }) {
|
||||||
return join('video-files', video.uuid + extname(videoFile.filename))
|
return join('video-files', video.uuid + extname(file.filename || file.keptOriginalFilename))
|
||||||
}
|
}
|
||||||
|
|
||||||
private getArchiveCaptionFilePath (video: MVideo, caption: MVideoCaptionLanguageUrl) {
|
private getArchiveCaptionFilePath (video: MVideo, caption: MVideoCaptionLanguageUrl) {
|
||||||
|
|
|
@ -1,15 +1,12 @@
|
||||||
import { LiveVideoLatencyMode, ThumbnailType, VideoExportJSON, VideoPrivacy, VideoState } from '@peertube/peertube-models'
|
|
||||||
import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
|
|
||||||
import { Hooks } from '@server/lib/plugins/hooks.js'
|
|
||||||
import { buildNextVideoState } from '@server/lib/video-state.js'
|
|
||||||
import { VideoModel } from '@server/models/video/video.js'
|
|
||||||
import { pick } from '@peertube/peertube-core-utils'
|
import { pick } from '@peertube/peertube-core-utils'
|
||||||
import { buildUUID, getFileSize } from '@peertube/peertube-node-utils'
|
|
||||||
import { MChannelId, MVideoFullLight } from '@server/types/models/index.js'
|
|
||||||
import { ffprobePromise, getVideoStreamDuration } from '@peertube/peertube-ffmpeg'
|
import { ffprobePromise, getVideoStreamDuration } from '@peertube/peertube-ffmpeg'
|
||||||
import { VideoChannelModel } from '@server/models/video/video-channel.js'
|
import { LiveVideoLatencyMode, ThumbnailType, VideoExportJSON, VideoPrivacy, VideoState } from '@peertube/peertube-models'
|
||||||
import { AbstractUserImporter } from './abstract-user-importer.js'
|
import { buildUUID, getFileSize } from '@peertube/peertube-node-utils'
|
||||||
import { isUserQuotaValid } from '@server/lib/user.js'
|
import { isArray, isBooleanValid, isUUIDValid } from '@server/helpers/custom-validators/misc.js'
|
||||||
|
import { isVideoCaptionLanguageValid } from '@server/helpers/custom-validators/video-captions.js'
|
||||||
|
import { isVideoChannelUsernameValid } from '@server/helpers/custom-validators/video-channels.js'
|
||||||
|
import { isVideoChapterTimecodeValid, isVideoChapterTitleValid } from '@server/helpers/custom-validators/video-chapters.js'
|
||||||
|
import { isLiveLatencyModeValid } from '@server/helpers/custom-validators/video-lives.js'
|
||||||
import {
|
import {
|
||||||
isPasswordValid,
|
isPasswordValid,
|
||||||
isVideoCategoryValid,
|
isVideoCategoryValid,
|
||||||
|
@ -25,17 +22,21 @@ import {
|
||||||
isVideoSupportValid,
|
isVideoSupportValid,
|
||||||
isVideoTagValid
|
isVideoTagValid
|
||||||
} from '@server/helpers/custom-validators/videos.js'
|
} from '@server/helpers/custom-validators/videos.js'
|
||||||
import { isVideoChannelUsernameValid } from '@server/helpers/custom-validators/video-channels.js'
|
import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
|
||||||
import { CONSTRAINTS_FIELDS } from '@server/initializers/constants.js'
|
|
||||||
import { isArray, isBooleanValid, isUUIDValid } from '@server/helpers/custom-validators/misc.js'
|
|
||||||
import { CONFIG } from '@server/initializers/config.js'
|
import { CONFIG } from '@server/initializers/config.js'
|
||||||
import { isVideoCaptionLanguageValid } from '@server/helpers/custom-validators/video-captions.js'
|
import { CONSTRAINTS_FIELDS } from '@server/initializers/constants.js'
|
||||||
import { isLiveLatencyModeValid } from '@server/helpers/custom-validators/video-lives.js'
|
|
||||||
import { parse } from 'path'
|
|
||||||
import { isLocalVideoFileAccepted } from '@server/lib/moderation.js'
|
|
||||||
import { LocalVideoCreator, ThumbnailOptions } from '@server/lib/local-video-creator.js'
|
import { LocalVideoCreator, ThumbnailOptions } from '@server/lib/local-video-creator.js'
|
||||||
import { isVideoChapterTimecodeValid, isVideoChapterTitleValid } from '@server/helpers/custom-validators/video-chapters.js'
|
import { isLocalVideoFileAccepted } from '@server/lib/moderation.js'
|
||||||
|
import { Hooks } from '@server/lib/plugins/hooks.js'
|
||||||
|
import { isUserQuotaValid } from '@server/lib/user.js'
|
||||||
import { createLocalCaption } from '@server/lib/video-captions.js'
|
import { createLocalCaption } from '@server/lib/video-captions.js'
|
||||||
|
import { buildNextVideoState } from '@server/lib/video-state.js'
|
||||||
|
import { VideoChannelModel } from '@server/models/video/video-channel.js'
|
||||||
|
import { VideoModel } from '@server/models/video/video.js'
|
||||||
|
import { MChannelId, MVideoFullLight } from '@server/types/models/index.js'
|
||||||
|
import { FfprobeData } from 'fluent-ffmpeg'
|
||||||
|
import { parse } from 'path'
|
||||||
|
import { AbstractUserImporter } from './abstract-user-importer.js'
|
||||||
|
|
||||||
const lTags = loggerTagsFactory('user-import')
|
const lTags = loggerTagsFactory('user-import')
|
||||||
|
|
||||||
|
@ -69,7 +70,7 @@ export class VideosImporter extends AbstractUserImporter <VideoExportJSON, Impor
|
||||||
if (!isBooleanValid(o.downloadEnabled)) o.downloadEnabled = CONFIG.DEFAULTS.PUBLISH.DOWNLOAD_ENABLED
|
if (!isBooleanValid(o.downloadEnabled)) o.downloadEnabled = CONFIG.DEFAULTS.PUBLISH.DOWNLOAD_ENABLED
|
||||||
if (!isBooleanValid(o.waitTranscoding)) o.waitTranscoding = true
|
if (!isBooleanValid(o.waitTranscoding)) o.waitTranscoding = true
|
||||||
|
|
||||||
if (!isVideoSourceFilenameValid(o.source?.filename)) o.source = undefined
|
if (!isVideoSourceFilenameValid(o.source?.inputFilename)) o.source = undefined
|
||||||
|
|
||||||
if (!isVideoOriginallyPublishedAtValid(o.originallyPublishedAt)) o.originallyPublishedAt = null
|
if (!isVideoOriginallyPublishedAtValid(o.originallyPublishedAt)) o.originallyPublishedAt = null
|
||||||
|
|
||||||
|
@ -149,6 +150,7 @@ export class VideosImporter extends AbstractUserImporter <VideoExportJSON, Impor
|
||||||
|
|
||||||
let duration = 0
|
let duration = 0
|
||||||
|
|
||||||
|
let ffprobe: FfprobeData
|
||||||
if (videoFilePath) {
|
if (videoFilePath) {
|
||||||
if (await isUserQuotaValid({ userId: this.user.id, uploadSize: videoSize, checkDaily: false }) === false) {
|
if (await isUserQuotaValid({ userId: this.user.id, uploadSize: videoSize, checkDaily: false }) === false) {
|
||||||
throw new Error(`Cannot import video ${videoImportData.name} for user ${this.user.username} because of exceeded quota`)
|
throw new Error(`Cannot import video ${videoImportData.name} for user ${this.user.username} because of exceeded quota`)
|
||||||
|
@ -156,7 +158,7 @@ export class VideosImporter extends AbstractUserImporter <VideoExportJSON, Impor
|
||||||
|
|
||||||
await this.checkVideoFileIsAcceptedOrThrow({ videoFilePath, size: videoSize, channel: videoChannel, videoImportData })
|
await this.checkVideoFileIsAcceptedOrThrow({ videoFilePath, size: videoSize, channel: videoChannel, videoImportData })
|
||||||
|
|
||||||
const ffprobe = await ffprobePromise(videoFilePath)
|
ffprobe = await ffprobePromise(videoFilePath)
|
||||||
duration = await getVideoStreamDuration(videoFilePath, ffprobe)
|
duration = await getVideoStreamDuration(videoFilePath, ffprobe)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,7 +178,11 @@ export class VideosImporter extends AbstractUserImporter <VideoExportJSON, Impor
|
||||||
|
|
||||||
const localVideoCreator = new LocalVideoCreator({
|
const localVideoCreator = new LocalVideoCreator({
|
||||||
lTags,
|
lTags,
|
||||||
videoFilePath,
|
|
||||||
|
videoFile: videoFilePath
|
||||||
|
? { path: videoFilePath, probe: ffprobe }
|
||||||
|
: undefined,
|
||||||
|
|
||||||
user: this.user,
|
user: this.user,
|
||||||
channel: videoChannel,
|
channel: videoChannel,
|
||||||
|
|
||||||
|
@ -206,7 +212,9 @@ export class VideosImporter extends AbstractUserImporter <VideoExportJSON, Impor
|
||||||
|
|
||||||
videoPasswords: videoImportData.passwords,
|
videoPasswords: videoImportData.passwords,
|
||||||
duration,
|
duration,
|
||||||
filename: videoImportData.source?.filename,
|
|
||||||
|
inputFilename: videoImportData.source?.inputFilename,
|
||||||
|
|
||||||
state: videoImportData.isLive
|
state: videoImportData.isLive
|
||||||
? VideoState.WAITING_FOR_LIVE
|
? VideoState.WAITING_FOR_LIVE
|
||||||
: buildNextVideoState()
|
: buildNextVideoState()
|
||||||
|
|
|
@ -48,7 +48,7 @@ export class UserExporter {
|
||||||
|
|
||||||
if (exportModel.storage === FileStorage.FILE_SYSTEM) {
|
if (exportModel.storage === FileStorage.FILE_SYSTEM) {
|
||||||
output = createWriteStream(getFSUserExportFilePath(exportModel))
|
output = createWriteStream(getFSUserExportFilePath(exportModel))
|
||||||
endPromise = new Promise<void>(res => output.on('close', () => res()))
|
endPromise = new Promise<string>(res => output.on('close', () => res('')))
|
||||||
} else {
|
} else {
|
||||||
output = new PassThrough()
|
output = new PassThrough()
|
||||||
endPromise = storeUserExportFile(output as PassThrough, exportModel)
|
endPromise = storeUserExportFile(output as PassThrough, exportModel)
|
||||||
|
@ -56,12 +56,16 @@ export class UserExporter {
|
||||||
|
|
||||||
await this.createZip({ exportModel, user, output })
|
await this.createZip({ exportModel, user, output })
|
||||||
|
|
||||||
await endPromise
|
const fileUrl = await endPromise
|
||||||
|
|
||||||
|
if (exportModel.storage === FileStorage.OBJECT_STORAGE) {
|
||||||
|
exportModel.fileUrl = fileUrl
|
||||||
|
exportModel.size = await getUserExportFileObjectStorageSize(exportModel)
|
||||||
|
} else if (exportModel.storage === FileStorage.FILE_SYSTEM) {
|
||||||
|
exportModel.size = await getFileSize(getFSUserExportFilePath(exportModel))
|
||||||
|
}
|
||||||
|
|
||||||
exportModel.state = UserExportState.COMPLETED
|
exportModel.state = UserExportState.COMPLETED
|
||||||
exportModel.size = exportModel.storage === FileStorage.FILE_SYSTEM
|
|
||||||
? await getFileSize(getFSUserExportFilePath(exportModel))
|
|
||||||
: await getUserExportFileObjectStorageSize(exportModel)
|
|
||||||
|
|
||||||
await saveInTransactionWithRetries(exportModel)
|
await saveInTransactionWithRetries(exportModel)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -1,16 +1,20 @@
|
||||||
import { FfprobeData } from 'fluent-ffmpeg'
|
|
||||||
import { VideoFileMetadata, VideoResolution } from '@peertube/peertube-models'
|
|
||||||
import { logger } from '@server/helpers/logger.js'
|
|
||||||
import { VideoFileModel } from '@server/models/video/video-file.js'
|
|
||||||
import { MVideoWithAllFiles } from '@server/types/models/index.js'
|
|
||||||
import { getFileSize, getLowercaseExtension } from '@peertube/peertube-node-utils'
|
|
||||||
import { ffprobePromise, getVideoStreamDimensionsInfo, getVideoStreamFPS, isAudioFile } from '@peertube/peertube-ffmpeg'
|
import { ffprobePromise, getVideoStreamDimensionsInfo, getVideoStreamFPS, isAudioFile } from '@peertube/peertube-ffmpeg'
|
||||||
|
import { FileStorage, VideoFileMetadata, VideoResolution } from '@peertube/peertube-models'
|
||||||
|
import { getFileSize, getLowercaseExtension } from '@peertube/peertube-node-utils'
|
||||||
|
import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
|
||||||
|
import { CONFIG } from '@server/initializers/config.js'
|
||||||
|
import { MIMETYPES } from '@server/initializers/constants.js'
|
||||||
|
import { VideoFileModel } from '@server/models/video/video-file.js'
|
||||||
|
import { VideoSourceModel } from '@server/models/video/video-source.js'
|
||||||
|
import { MVideo, MVideoFile, MVideoId, MVideoWithAllFiles } from '@server/types/models/index.js'
|
||||||
|
import { FfprobeData } from 'fluent-ffmpeg'
|
||||||
|
import { move, remove } from 'fs-extra'
|
||||||
import { lTags } from './object-storage/shared/index.js'
|
import { lTags } from './object-storage/shared/index.js'
|
||||||
|
import { storeOriginalVideoFile } from './object-storage/videos.js'
|
||||||
import { generateHLSVideoFilename, generateWebVideoFilename } from './paths.js'
|
import { generateHLSVideoFilename, generateWebVideoFilename } from './paths.js'
|
||||||
import { VideoPathManager } from './video-path-manager.js'
|
import { VideoPathManager } from './video-path-manager.js'
|
||||||
import { MIMETYPES } from '@server/initializers/constants.js'
|
|
||||||
|
|
||||||
async function buildNewFile (options: {
|
export async function buildNewFile (options: {
|
||||||
path: string
|
path: string
|
||||||
mode: 'web-video' | 'hls'
|
mode: 'web-video' | 'hls'
|
||||||
ffprobe?: FfprobeData
|
ffprobe?: FfprobeData
|
||||||
|
@ -48,7 +52,7 @@ async function buildNewFile (options: {
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async function removeHLSPlaylist (video: MVideoWithAllFiles) {
|
export async function removeHLSPlaylist (video: MVideoWithAllFiles) {
|
||||||
const hls = video.getHLSPlaylist()
|
const hls = video.getHLSPlaylist()
|
||||||
if (!hls) return
|
if (!hls) return
|
||||||
|
|
||||||
|
@ -64,7 +68,7 @@ async function removeHLSPlaylist (video: MVideoWithAllFiles) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeHLSFile (video: MVideoWithAllFiles, fileToDeleteId: number) {
|
export async function removeHLSFile (video: MVideoWithAllFiles, fileToDeleteId: number) {
|
||||||
logger.info('Deleting HLS file %d of %s.', fileToDeleteId, video.url, lTags(video.uuid))
|
logger.info('Deleting HLS file %d of %s.', fileToDeleteId, video.url, lTags(video.uuid))
|
||||||
|
|
||||||
const hls = video.getHLSPlaylist()
|
const hls = video.getHLSPlaylist()
|
||||||
|
@ -92,7 +96,7 @@ async function removeHLSFile (video: MVideoWithAllFiles, fileToDeleteId: number)
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async function removeAllWebVideoFiles (video: MVideoWithAllFiles) {
|
export async function removeAllWebVideoFiles (video: MVideoWithAllFiles) {
|
||||||
const videoFileMutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid)
|
const videoFileMutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -109,7 +113,7 @@ async function removeAllWebVideoFiles (video: MVideoWithAllFiles) {
|
||||||
return video
|
return video
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeWebVideoFile (video: MVideoWithAllFiles, fileToDeleteId: number) {
|
export async function removeWebVideoFile (video: MVideoWithAllFiles, fileToDeleteId: number) {
|
||||||
const files = video.VideoFiles
|
const files = video.VideoFiles
|
||||||
|
|
||||||
if (files.length === 1) {
|
if (files.length === 1) {
|
||||||
|
@ -132,13 +136,13 @@ async function removeWebVideoFile (video: MVideoWithAllFiles, fileToDeleteId: nu
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async function buildFileMetadata (path: string, existingProbe?: FfprobeData) {
|
export async function buildFileMetadata (path: string, existingProbe?: FfprobeData) {
|
||||||
const metadata = existingProbe || await ffprobePromise(path)
|
const metadata = existingProbe || await ffprobePromise(path)
|
||||||
|
|
||||||
return new VideoFileMetadata(metadata)
|
return new VideoFileMetadata(metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getVideoFileMimeType (extname: string, isAudio: boolean) {
|
export function getVideoFileMimeType (extname: string, isAudio: boolean) {
|
||||||
return isAudio && extname === '.mp4' // We use .mp4 even for audio file only
|
return isAudio && extname === '.mp4' // We use .mp4 even for audio file only
|
||||||
? MIMETYPES.AUDIO.EXT_MIMETYPE['.m4a']
|
? MIMETYPES.AUDIO.EXT_MIMETYPE['.m4a']
|
||||||
: MIMETYPES.VIDEO.EXT_MIMETYPE[extname]
|
: MIMETYPES.VIDEO.EXT_MIMETYPE[extname]
|
||||||
|
@ -146,14 +150,84 @@ function getVideoFileMimeType (extname: string, isAudio: boolean) {
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export {
|
export async function createVideoSource (options: {
|
||||||
buildNewFile,
|
inputFilename: string
|
||||||
|
inputProbe: FfprobeData
|
||||||
|
inputPath: string
|
||||||
|
video: MVideoId
|
||||||
|
createdAt?: Date
|
||||||
|
}) {
|
||||||
|
const { inputFilename, inputPath, inputProbe, video, createdAt } = options
|
||||||
|
|
||||||
removeHLSPlaylist,
|
const videoSource = new VideoSourceModel({
|
||||||
removeHLSFile,
|
inputFilename,
|
||||||
removeAllWebVideoFiles,
|
videoId: video.id,
|
||||||
removeWebVideoFile,
|
createdAt
|
||||||
|
})
|
||||||
|
|
||||||
buildFileMetadata,
|
if (inputPath) {
|
||||||
getVideoFileMimeType
|
const probe = inputProbe ?? await ffprobePromise(inputPath)
|
||||||
|
|
||||||
|
if (await isAudioFile(inputPath, probe)) {
|
||||||
|
videoSource.fps = 0
|
||||||
|
videoSource.resolution = VideoResolution.H_NOVIDEO
|
||||||
|
videoSource.width = 0
|
||||||
|
videoSource.height = 0
|
||||||
|
} else {
|
||||||
|
const dimensions = await getVideoStreamDimensionsInfo(inputPath, probe)
|
||||||
|
videoSource.fps = await getVideoStreamFPS(inputPath, probe)
|
||||||
|
videoSource.resolution = dimensions.resolution
|
||||||
|
videoSource.width = dimensions.width
|
||||||
|
videoSource.height = dimensions.height
|
||||||
|
}
|
||||||
|
|
||||||
|
videoSource.metadata = await buildFileMetadata(inputPath, probe)
|
||||||
|
videoSource.size = await getFileSize(inputPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return videoSource.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveNewOriginalFileIfNeeded (video: MVideo, videoFile: MVideoFile) {
|
||||||
|
if (!CONFIG.TRANSCODING.ORIGINAL_FILE.KEEP) return
|
||||||
|
|
||||||
|
const videoSource = await VideoSourceModel.loadLatest(video.id)
|
||||||
|
|
||||||
|
// Already have saved an original file
|
||||||
|
if (!videoSource || videoSource.keptOriginalFilename) return
|
||||||
|
videoSource.keptOriginalFilename = videoFile.filename
|
||||||
|
|
||||||
|
const lTags = loggerTagsFactory(video.uuid)
|
||||||
|
|
||||||
|
logger.info(`Storing original video file ${videoSource.keptOriginalFilename} of video ${video.name}`, lTags())
|
||||||
|
|
||||||
|
const sourcePath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, videoFile)
|
||||||
|
|
||||||
|
if (CONFIG.OBJECT_STORAGE.ENABLED) {
|
||||||
|
const fileUrl = await storeOriginalVideoFile(sourcePath, videoSource.keptOriginalFilename)
|
||||||
|
await remove(sourcePath)
|
||||||
|
|
||||||
|
videoSource.storage = FileStorage.OBJECT_STORAGE
|
||||||
|
videoSource.fileUrl = fileUrl
|
||||||
|
} else {
|
||||||
|
const destinationPath = VideoPathManager.Instance.getFSOriginalVideoFilePath(videoSource.keptOriginalFilename)
|
||||||
|
await move(sourcePath, destinationPath)
|
||||||
|
|
||||||
|
videoSource.storage = FileStorage.FILE_SYSTEM
|
||||||
|
}
|
||||||
|
|
||||||
|
await videoSource.save()
|
||||||
|
|
||||||
|
// Delete previously kept video files
|
||||||
|
const allSources = await VideoSourceModel.listAll(video.id)
|
||||||
|
for (const oldSource of allSources) {
|
||||||
|
if (!oldSource.keptOriginalFilename) continue
|
||||||
|
if (oldSource.id === videoSource.id) continue
|
||||||
|
|
||||||
|
try {
|
||||||
|
await video.removeOriginalFile(oldSource)
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('Cannot delete old original file ' + oldSource.keptOriginalFilename, { err, ...lTags() })
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import { Mutex } from 'async-mutex'
|
|
||||||
import { remove } from 'fs-extra/esm'
|
|
||||||
import { extname, join } from 'path'
|
|
||||||
import { FileStorage } from '@peertube/peertube-models'
|
import { FileStorage } from '@peertube/peertube-models'
|
||||||
|
import { buildUUID } from '@peertube/peertube-node-utils'
|
||||||
import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
|
import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
|
||||||
import { extractVideo } from '@server/helpers/video.js'
|
import { extractVideo } from '@server/helpers/video.js'
|
||||||
import { CONFIG } from '@server/initializers/config.js'
|
import { CONFIG } from '@server/initializers/config.js'
|
||||||
|
@ -13,7 +11,9 @@ import {
|
||||||
MVideoFileStreamingPlaylistVideo,
|
MVideoFileStreamingPlaylistVideo,
|
||||||
MVideoFileVideo
|
MVideoFileVideo
|
||||||
} from '@server/types/models/index.js'
|
} from '@server/types/models/index.js'
|
||||||
import { buildUUID } from '@peertube/peertube-node-utils'
|
import { Mutex } from 'async-mutex'
|
||||||
|
import { remove } from 'fs-extra/esm'
|
||||||
|
import { extname, join } from 'path'
|
||||||
import { makeHLSFileAvailable, makeWebVideoFileAvailable } from './object-storage/index.js'
|
import { makeHLSFileAvailable, makeWebVideoFileAvailable } from './object-storage/index.js'
|
||||||
import { getHLSDirectory, getHLSRedundancyDirectory, getHlsResolutionPlaylistFilename } from './paths.js'
|
import { getHLSDirectory, getHLSRedundancyDirectory, getHlsResolutionPlaylistFilename } from './paths.js'
|
||||||
import { isVideoInPrivateDirectory } from './video-privacy.js'
|
import { isVideoInPrivateDirectory } from './video-privacy.js'
|
||||||
|
@ -56,10 +56,14 @@ class VideoPathManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isVideoInPrivateDirectory(video.privacy)) {
|
if (isVideoInPrivateDirectory(video.privacy)) {
|
||||||
return join(DIRECTORIES.VIDEOS.PRIVATE, videoFile.filename)
|
return join(DIRECTORIES.WEB_VIDEOS.PRIVATE, videoFile.filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
return join(DIRECTORIES.VIDEOS.PUBLIC, videoFile.filename)
|
return join(DIRECTORIES.WEB_VIDEOS.PUBLIC, videoFile.filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
getFSOriginalVideoFilePath (filename: string) {
|
||||||
|
return join(DIRECTORIES.ORIGINAL_VIDEOS, filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
async makeAvailableVideoFile <T> (videoFile: MVideoFileVideo | MVideoFileStreamingPlaylistVideo, cb: MakeAvailableCB<T>) {
|
async makeAvailableVideoFile <T> (videoFile: MVideoFileVideo | MVideoFileStreamingPlaylistVideo, cb: MakeAvailableCB<T>) {
|
||||||
|
|
|
@ -101,10 +101,10 @@ async function moveWebVideoFileOnFS (type: MoveType, video: MVideo, file: MVideo
|
||||||
|
|
||||||
function getWebVideoDirectories (moveType: MoveType) {
|
function getWebVideoDirectories (moveType: MoveType) {
|
||||||
if (moveType === 'private-to-public') {
|
if (moveType === 'private-to-public') {
|
||||||
return { old: DIRECTORIES.VIDEOS.PRIVATE, new: DIRECTORIES.VIDEOS.PUBLIC }
|
return { old: DIRECTORIES.WEB_VIDEOS.PRIVATE, new: DIRECTORIES.WEB_VIDEOS.PUBLIC }
|
||||||
}
|
}
|
||||||
|
|
||||||
return { old: DIRECTORIES.VIDEOS.PUBLIC, new: DIRECTORIES.VIDEOS.PRIVATE }
|
return { old: DIRECTORIES.WEB_VIDEOS.PUBLIC, new: DIRECTORIES.WEB_VIDEOS.PRIVATE }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
@ -41,6 +41,7 @@ const customConfigUpdateValidator = [
|
||||||
body('videoChannels.maxPerUser').isInt(),
|
body('videoChannels.maxPerUser').isInt(),
|
||||||
|
|
||||||
body('transcoding.enabled').isBoolean(),
|
body('transcoding.enabled').isBoolean(),
|
||||||
|
body('transcoding.originalFile.keep').isBoolean(),
|
||||||
body('transcoding.allowAdditionalExtensions').isBoolean(),
|
body('transcoding.allowAdditionalExtensions').isBoolean(),
|
||||||
body('transcoding.threads').isInt(),
|
body('transcoding.threads').isInt(),
|
||||||
body('transcoding.concurrency').isInt({ min: 1 }),
|
body('transcoding.concurrency').isInt({ min: 1 }),
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { Request, Response } from 'express'
|
|
||||||
import { HttpStatusCode, ServerErrorCode, UserRight, UserRightType, VideoPrivacy } from '@peertube/peertube-models'
|
import { HttpStatusCode, ServerErrorCode, UserRight, UserRightType, VideoPrivacy } from '@peertube/peertube-models'
|
||||||
import { exists } from '@server/helpers/custom-validators/misc.js'
|
import { exists } from '@server/helpers/custom-validators/misc.js'
|
||||||
import { loadVideo, VideoLoadType } from '@server/lib/model-loaders/index.js'
|
import { VideoLoadType, loadVideo } from '@server/lib/model-loaders/index.js'
|
||||||
import { isUserQuotaValid } from '@server/lib/user.js'
|
import { isUserQuotaValid } from '@server/lib/user.js'
|
||||||
import { VideoTokensManager } from '@server/lib/video-tokens-manager.js'
|
import { VideoTokensManager } from '@server/lib/video-tokens-manager.js'
|
||||||
import { authenticatePromise } from '@server/middlewares/auth.js'
|
import { authenticatePromise } from '@server/middlewares/auth.js'
|
||||||
|
@ -20,10 +19,12 @@ import {
|
||||||
MVideoId,
|
MVideoId,
|
||||||
MVideoImmutable,
|
MVideoImmutable,
|
||||||
MVideoThumbnail,
|
MVideoThumbnail,
|
||||||
|
MVideoUUID,
|
||||||
MVideoWithRights
|
MVideoWithRights
|
||||||
} from '@server/types/models/index.js'
|
} from '@server/types/models/index.js'
|
||||||
|
import { Request, Response } from 'express'
|
||||||
|
|
||||||
async function doesVideoExist (id: number | string, res: Response, fetchType: VideoLoadType = 'all') {
|
export async function doesVideoExist (id: number | string, res: Response, fetchType: VideoLoadType = 'all') {
|
||||||
const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined
|
const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined
|
||||||
|
|
||||||
const video = await loadVideo(id, fetchType, userId)
|
const video = await loadVideo(id, fetchType, userId)
|
||||||
|
@ -64,7 +65,7 @@ async function doesVideoExist (id: number | string, res: Response, fetchType: Vi
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async function doesVideoFileOfVideoExist (id: number, videoIdOrUUID: number | string, res: Response) {
|
export async function doesVideoFileOfVideoExist (id: number, videoIdOrUUID: number | string, res: Response) {
|
||||||
if (!await VideoFileModel.doesVideoExistForVideoFile(id, videoIdOrUUID)) {
|
if (!await VideoFileModel.doesVideoExistForVideoFile(id, videoIdOrUUID)) {
|
||||||
res.fail({
|
res.fail({
|
||||||
status: HttpStatusCode.NOT_FOUND_404,
|
status: HttpStatusCode.NOT_FOUND_404,
|
||||||
|
@ -78,7 +79,7 @@ async function doesVideoFileOfVideoExist (id: number, videoIdOrUUID: number | st
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAccountId, res: Response) {
|
export async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAccountId, res: Response) {
|
||||||
const videoChannel = await VideoChannelModel.loadAndPopulateAccount(channelId)
|
const videoChannel = await VideoChannelModel.loadAndPopulateAccount(channelId)
|
||||||
|
|
||||||
if (videoChannel === null) {
|
if (videoChannel === null) {
|
||||||
|
@ -105,7 +106,7 @@ async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAcc
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async function checkCanSeeVideo (options: {
|
export async function checkCanSeeVideo (options: {
|
||||||
req: Request
|
req: Request
|
||||||
res: Response
|
res: Response
|
||||||
paramId: string
|
paramId: string
|
||||||
|
@ -128,7 +129,7 @@ async function checkCanSeeVideo (options: {
|
||||||
throw new Error('Unknown video privacy when checking video right ' + video.url)
|
throw new Error('Unknown video privacy when checking video right ' + video.url)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkCanSeeUserAuthVideo (options: {
|
export async function checkCanSeeUserAuthVideo (options: {
|
||||||
req: Request
|
req: Request
|
||||||
res: Response
|
res: Response
|
||||||
video: MVideoId | MVideoWithRights
|
video: MVideoId | MVideoWithRights
|
||||||
|
@ -174,7 +175,7 @@ async function checkCanSeeUserAuthVideo (options: {
|
||||||
return fail()
|
return fail()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkCanSeePasswordProtectedVideo (options: {
|
export async function checkCanSeePasswordProtectedVideo (options: {
|
||||||
req: Request
|
req: Request
|
||||||
res: Response
|
res: Response
|
||||||
video: MVideo
|
video: MVideo
|
||||||
|
@ -215,13 +216,13 @@ async function checkCanSeePasswordProtectedVideo (options: {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
function canUserAccessVideo (user: MUser, video: MVideoWithRights | MVideoAccountLight, right: UserRightType) {
|
export function canUserAccessVideo (user: MUser, video: MVideoWithRights | MVideoAccountLight, right: UserRightType) {
|
||||||
const isOwnedByUser = video.VideoChannel.Account.userId === user.id
|
const isOwnedByUser = video.VideoChannel.Account.userId === user.id
|
||||||
|
|
||||||
return isOwnedByUser || user.hasRight(right)
|
return isOwnedByUser || user.hasRight(right)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getVideoWithRights (video: MVideoWithRights): Promise<MVideoWithRights> {
|
export async function getVideoWithRights (video: MVideoWithRights): Promise<MVideoWithRights> {
|
||||||
return video.VideoChannel?.Account?.userId
|
return video.VideoChannel?.Account?.userId
|
||||||
? video
|
? video
|
||||||
: VideoModel.loadFull(video.id)
|
: VideoModel.loadFull(video.id)
|
||||||
|
@ -229,7 +230,7 @@ async function getVideoWithRights (video: MVideoWithRights): Promise<MVideoWithR
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async function checkCanAccessVideoStaticFiles (options: {
|
export async function checkCanAccessVideoStaticFiles (options: {
|
||||||
video: MVideo
|
video: MVideo
|
||||||
req: Request
|
req: Request
|
||||||
res: Response
|
res: Response
|
||||||
|
@ -241,23 +242,51 @@ async function checkCanAccessVideoStaticFiles (options: {
|
||||||
return checkCanSeeVideo(options)
|
return checkCanSeeVideo(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
const videoFileToken = req.query.videoFileToken
|
assignVideoTokenIfNeeded(req, res, video)
|
||||||
if (videoFileToken && VideoTokensManager.Instance.hasToken({ token: videoFileToken, videoUUID: video.uuid })) {
|
|
||||||
const user = VideoTokensManager.Instance.getUserFromToken({ token: videoFileToken })
|
|
||||||
|
|
||||||
res.locals.videoFileToken = { user }
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (res.locals.videoFileToken) return true
|
||||||
if (!video.hasPrivateStaticPath()) return true
|
if (!video.hasPrivateStaticPath()) return true
|
||||||
|
|
||||||
res.sendStatus(HttpStatusCode.FORBIDDEN_403)
|
res.sendStatus(HttpStatusCode.FORBIDDEN_403)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function checkCanAccessVideoSourceFile (options: {
|
||||||
|
videoId: number
|
||||||
|
req: Request
|
||||||
|
res: Response
|
||||||
|
}) {
|
||||||
|
const { req, res, videoId } = options
|
||||||
|
|
||||||
|
const video = await VideoModel.loadFull(videoId)
|
||||||
|
|
||||||
|
if (res.locals.oauth?.token.User) {
|
||||||
|
if (canUserAccessVideo(res.locals.oauth.token.User, video, UserRight.SEE_ALL_VIDEOS) === true) return true
|
||||||
|
|
||||||
|
res.sendStatus(HttpStatusCode.FORBIDDEN_403)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
assignVideoTokenIfNeeded(req, res, video)
|
||||||
|
if (res.locals.videoFileToken) return true
|
||||||
|
|
||||||
|
res.sendStatus(HttpStatusCode.FORBIDDEN_403)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function assignVideoTokenIfNeeded (req: Request, res: Response, video: MVideoUUID) {
|
||||||
|
const videoFileToken = req.query.videoFileToken
|
||||||
|
|
||||||
|
if (videoFileToken && VideoTokensManager.Instance.hasToken({ token: videoFileToken, videoUUID: video.uuid })) {
|
||||||
|
const user = VideoTokensManager.Instance.getUserFromToken({ token: videoFileToken })
|
||||||
|
|
||||||
|
res.locals.videoFileToken = { user }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right: UserRightType, res: Response, onlyOwned = true) {
|
export function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right: UserRightType, res: Response, onlyOwned = true) {
|
||||||
// Retrieve the user who did the request
|
// Retrieve the user who did the request
|
||||||
if (onlyOwned && video.isOwned() === false) {
|
if (onlyOwned && video.isOwned() === false) {
|
||||||
res.fail({
|
res.fail({
|
||||||
|
@ -284,7 +313,7 @@ function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right:
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async function checkUserQuota (user: MUserId, videoFileSize: number, res: Response) {
|
export async function checkUserQuota (user: MUserId, videoFileSize: number, res: Response) {
|
||||||
if (await isUserQuotaValid({ userId: user.id, uploadSize: videoFileSize }) === false) {
|
if (await isUserQuotaValid({ userId: user.id, uploadSize: videoFileSize }) === false) {
|
||||||
res.fail({
|
res.fail({
|
||||||
status: HttpStatusCode.PAYLOAD_TOO_LARGE_413,
|
status: HttpStatusCode.PAYLOAD_TOO_LARGE_413,
|
||||||
|
@ -296,16 +325,3 @@ async function checkUserQuota (user: MUserId, videoFileSize: number, res: Respon
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
export {
|
|
||||||
doesVideoChannelOfAccountExist,
|
|
||||||
doesVideoExist,
|
|
||||||
doesVideoFileOfVideoExist,
|
|
||||||
|
|
||||||
checkCanAccessVideoStaticFiles,
|
|
||||||
checkUserCanManageVideo,
|
|
||||||
checkCanSeeVideo,
|
|
||||||
checkUserQuota
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { logger } from '@server/helpers/logger.js'
|
import { logger } from '@server/helpers/logger.js'
|
||||||
import { getVideoStreamDuration } from '@peertube/peertube-ffmpeg'
|
import { ffprobePromise, getVideoStreamDuration } from '@peertube/peertube-ffmpeg'
|
||||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||||
|
|
||||||
export async function addDurationToVideoFileIfNeeded (options: {
|
export async function addDurationToVideoFileIfNeeded (options: {
|
||||||
|
@ -11,7 +11,7 @@ export async function addDurationToVideoFileIfNeeded (options: {
|
||||||
const { res, middlewareName, videoFile } = options
|
const { res, middlewareName, videoFile } = options
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!videoFile.duration) await addDurationToVideo(videoFile)
|
if (!videoFile.duration) await addDurationToVideo(res, videoFile)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('Invalid input file in ' + middlewareName, { err })
|
logger.error('Invalid input file in ' + middlewareName, { err })
|
||||||
|
|
||||||
|
@ -29,8 +29,11 @@ export async function addDurationToVideoFileIfNeeded (options: {
|
||||||
// Private
|
// Private
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async function addDurationToVideo (videoFile: { path: string, duration?: number }) {
|
async function addDurationToVideo (res: express.Response, videoFile: { path: string, duration?: number }) {
|
||||||
const duration = await getVideoStreamDuration(videoFile.path)
|
const probe = await ffprobePromise(videoFile.path)
|
||||||
|
res.locals.ffprobe = probe
|
||||||
|
|
||||||
|
const duration = await getVideoStreamDuration(videoFile.path, probe)
|
||||||
|
|
||||||
// FFmpeg may not be able to guess video duration
|
// FFmpeg may not be able to guess video duration
|
||||||
// For example with m2v files: https://trac.ffmpeg.org/ticket/9726#comment:2
|
// For example with m2v files: https://trac.ffmpeg.org/ticket/9726#comment:2
|
||||||
|
|
|
@ -1,12 +1,19 @@
|
||||||
import express from 'express'
|
import { HttpStatusCode, UserRight } from '@peertube/peertube-models'
|
||||||
import { getVideoWithAttributes } from '@server/helpers/video.js'
|
import { getVideoWithAttributes } from '@server/helpers/video.js'
|
||||||
import { CONFIG } from '@server/initializers/config.js'
|
import { CONFIG } from '@server/initializers/config.js'
|
||||||
import { buildUploadXFile, safeUploadXCleanup } from '@server/lib/uploadx.js'
|
import { buildUploadXFile, safeUploadXCleanup } from '@server/lib/uploadx.js'
|
||||||
import { VideoSourceModel } from '@server/models/video/video-source.js'
|
import { VideoSourceModel } from '@server/models/video/video-source.js'
|
||||||
import { MVideoFullLight } from '@server/types/models/index.js'
|
import { MVideoFullLight } from '@server/types/models/index.js'
|
||||||
import { HttpStatusCode, UserRight } from '@peertube/peertube-models'
|
|
||||||
import { Metadata as UploadXMetadata } from '@uploadx/core'
|
import { Metadata as UploadXMetadata } from '@uploadx/core'
|
||||||
import { areValidationErrors, checkUserCanManageVideo, doesVideoExist, isValidVideoIdParam } from '../shared/index.js'
|
import express from 'express'
|
||||||
|
import { param } from 'express-validator'
|
||||||
|
import {
|
||||||
|
areValidationErrors,
|
||||||
|
checkCanAccessVideoSourceFile,
|
||||||
|
checkUserCanManageVideo,
|
||||||
|
doesVideoExist,
|
||||||
|
isValidVideoIdParam
|
||||||
|
} from '../shared/index.js'
|
||||||
import { addDurationToVideoFileIfNeeded, checkVideoFileCanBeEdited, commonVideoFileChecks, isVideoFileAccepted } from './shared/index.js'
|
import { addDurationToVideoFileIfNeeded, checkVideoFileCanBeEdited, commonVideoFileChecks, isVideoFileAccepted } from './shared/index.js'
|
||||||
|
|
||||||
export const videoSourceGetLatestValidator = [
|
export const videoSourceGetLatestValidator = [
|
||||||
|
@ -71,6 +78,28 @@ export const replaceVideoSourceResumableInitValidator = [
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export const originalVideoFileDownloadValidator = [
|
||||||
|
param('filename').exists(),
|
||||||
|
|
||||||
|
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
|
if (areValidationErrors(req, res)) return
|
||||||
|
|
||||||
|
const videoSource = await VideoSourceModel.loadByKeptOriginalFilename(req.params.filename)
|
||||||
|
if (!videoSource) {
|
||||||
|
return res.fail({
|
||||||
|
status: HttpStatusCode.NOT_FOUND_404,
|
||||||
|
message: 'Original video file not found'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await checkCanAccessVideoSourceFile({ req, res, videoId: videoSource.videoId })) return
|
||||||
|
|
||||||
|
res.locals.videoSource = videoSource
|
||||||
|
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Private
|
// Private
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import express from 'express'
|
|
||||||
import { body, param, query, ValidationChain } from 'express-validator'
|
|
||||||
import { arrayify } from '@peertube/peertube-core-utils'
|
import { arrayify } from '@peertube/peertube-core-utils'
|
||||||
import { HttpStatusCode, ServerErrorCode, UserRight, VideoState } from '@peertube/peertube-models'
|
import { HttpStatusCode, ServerErrorCode, UserRight, VideoState } from '@peertube/peertube-models'
|
||||||
import { Redis } from '@server/lib/redis.js'
|
import { Redis } from '@server/lib/redis.js'
|
||||||
|
@ -7,6 +5,8 @@ import { buildUploadXFile, safeUploadXCleanup } from '@server/lib/uploadx.js'
|
||||||
import { getServerActor } from '@server/models/application/application.js'
|
import { getServerActor } from '@server/models/application/application.js'
|
||||||
import { ExpressPromiseHandler } from '@server/types/express-handler.js'
|
import { ExpressPromiseHandler } from '@server/types/express-handler.js'
|
||||||
import { MUserAccountId, MVideoFullLight } from '@server/types/models/index.js'
|
import { MUserAccountId, MVideoFullLight } from '@server/types/models/index.js'
|
||||||
|
import express from 'express'
|
||||||
|
import { ValidationChain, body, param, query } from 'express-validator'
|
||||||
import {
|
import {
|
||||||
exists,
|
exists,
|
||||||
isBooleanValid,
|
isBooleanValid,
|
||||||
|
@ -41,8 +41,7 @@ import { CONFIG } from '../../../initializers/config.js'
|
||||||
import { CONSTRAINTS_FIELDS, OVERVIEWS } from '../../../initializers/constants.js'
|
import { CONSTRAINTS_FIELDS, OVERVIEWS } from '../../../initializers/constants.js'
|
||||||
import { VideoModel } from '../../../models/video/video.js'
|
import { VideoModel } from '../../../models/video/video.js'
|
||||||
import {
|
import {
|
||||||
areValidationErrors,
|
areValidationErrors, checkCanAccessVideoStaticFiles,
|
||||||
checkCanAccessVideoStaticFiles,
|
|
||||||
checkCanSeeVideo,
|
checkCanSeeVideo,
|
||||||
checkUserCanManageVideo,
|
checkUserCanManageVideo,
|
||||||
doesVideoChannelOfAccountExist,
|
doesVideoChannelOfAccountExist,
|
||||||
|
@ -501,23 +500,19 @@ const commonVideosFiltersValidator = [
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export {
|
export {
|
||||||
videosAddLegacyValidator,
|
|
||||||
videosAddResumableValidator,
|
|
||||||
videosAddResumableInitValidator,
|
|
||||||
|
|
||||||
videosUpdateValidator,
|
|
||||||
videosGetValidator,
|
|
||||||
videoFileMetadataGetValidator,
|
|
||||||
videosDownloadValidator,
|
|
||||||
checkVideoFollowConstraints,
|
checkVideoFollowConstraints,
|
||||||
videosCustomGetValidator,
|
|
||||||
videosRemoveValidator,
|
|
||||||
|
|
||||||
getCommonVideoEditAttributes,
|
|
||||||
|
|
||||||
commonVideosFiltersValidator,
|
commonVideosFiltersValidator,
|
||||||
|
getCommonVideoEditAttributes,
|
||||||
videosOverviewValidator
|
videoFileMetadataGetValidator,
|
||||||
|
videosAddLegacyValidator,
|
||||||
|
videosAddResumableInitValidator,
|
||||||
|
videosAddResumableValidator,
|
||||||
|
videosCustomGetValidator,
|
||||||
|
videosDownloadValidator,
|
||||||
|
videosGetValidator,
|
||||||
|
videosOverviewValidator,
|
||||||
|
videosRemoveValidator,
|
||||||
|
videosUpdateValidator
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
@ -62,6 +62,10 @@ export class UserExportModel extends SequelizeModel<UserExportModel> {
|
||||||
@Column
|
@Column
|
||||||
storage: FileStorageType
|
storage: FileStorageType
|
||||||
|
|
||||||
|
@AllowNull(true)
|
||||||
|
@Column
|
||||||
|
fileUrl: string
|
||||||
|
|
||||||
@ForeignKey(() => UserModel)
|
@ForeignKey(() => UserModel)
|
||||||
@Column
|
@Column
|
||||||
userId: number
|
userId: number
|
||||||
|
@ -188,7 +192,7 @@ export class UserExportModel extends SequelizeModel<UserExportModel> {
|
||||||
getFileDownloadUrl () {
|
getFileDownloadUrl () {
|
||||||
if (this.state !== UserExportState.COMPLETED) return null
|
if (this.state !== UserExportState.COMPLETED) return null
|
||||||
|
|
||||||
return WEBSERVER.URL + join(STATIC_DOWNLOAD_PATHS.USER_EXPORT, this.filename) + '?jwt=' + this.generateJWT()
|
return WEBSERVER.URL + join(STATIC_DOWNLOAD_PATHS.USER_EXPORTS, this.filename) + '?jwt=' + this.generateJWT()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
@ -1,8 +1,3 @@
|
||||||
import { generateMagnetUri } from '@server/helpers/webtorrent.js'
|
|
||||||
import { tracer } from '@server/lib/opentelemetry/tracing.js'
|
|
||||||
import { getLocalVideoFileMetadataUrl } from '@server/lib/video-urls.js'
|
|
||||||
import { VideoViewsManager } from '@server/lib/views/video-views-manager.js'
|
|
||||||
import { uuidToShort } from '@peertube/peertube-node-utils'
|
|
||||||
import {
|
import {
|
||||||
Video,
|
Video,
|
||||||
VideoAdditionalAttributes,
|
VideoAdditionalAttributes,
|
||||||
|
@ -12,6 +7,11 @@ import {
|
||||||
VideosCommonQueryAfterSanitize,
|
VideosCommonQueryAfterSanitize,
|
||||||
VideoStreamingPlaylist
|
VideoStreamingPlaylist
|
||||||
} from '@peertube/peertube-models'
|
} from '@peertube/peertube-models'
|
||||||
|
import { uuidToShort } from '@peertube/peertube-node-utils'
|
||||||
|
import { generateMagnetUri } from '@server/helpers/webtorrent.js'
|
||||||
|
import { tracer } from '@server/lib/opentelemetry/tracing.js'
|
||||||
|
import { getLocalVideoFileMetadataUrl } from '@server/lib/video-urls.js'
|
||||||
|
import { VideoViewsManager } from '@server/lib/views/video-views-manager.js'
|
||||||
import { isArray } from '../../../helpers/custom-validators/misc.js'
|
import { isArray } from '../../../helpers/custom-validators/misc.js'
|
||||||
import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES, VIDEO_STATES } from '../../../initializers/constants.js'
|
import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES, VIDEO_STATES } from '../../../initializers/constants.js'
|
||||||
import { MServer, MStreamingPlaylistRedundanciesOpt, MVideoFormattable, MVideoFormattableDetails } from '../../../types/models/index.js'
|
import { MServer, MStreamingPlaylistRedundanciesOpt, MVideoFormattable, MVideoFormattableDetails } from '../../../types/models/index.js'
|
||||||
|
@ -211,9 +211,7 @@ export function videoFilesModelToFormattedJSON (
|
||||||
|
|
||||||
resolution: {
|
resolution: {
|
||||||
id: videoFile.resolution,
|
id: videoFile.resolution,
|
||||||
label: videoFile.resolution === 0
|
label: getResolutionLabel(videoFile.resolution)
|
||||||
? 'Audio'
|
|
||||||
: `${videoFile.resolution}p`
|
|
||||||
},
|
},
|
||||||
|
|
||||||
width: videoFile.width,
|
width: videoFile.width,
|
||||||
|
@ -259,6 +257,12 @@ export function getStateLabel (id: number) {
|
||||||
return VIDEO_STATES[id] || 'Unknown'
|
return VIDEO_STATES[id] || 'Unknown'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getResolutionLabel (resolution: number) {
|
||||||
|
if (resolution === 0) return 'Audio'
|
||||||
|
|
||||||
|
return `${resolution}p`
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Private
|
// Private
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import { ActivityVideoUrlObject, VideoResolution, FileStorage, type FileStorageType } from '@peertube/peertube-models'
|
import { ActivityVideoUrlObject, FileStorage, VideoResolution, type FileStorageType } from '@peertube/peertube-models'
|
||||||
import { logger } from '@server/helpers/logger.js'
|
import { logger } from '@server/helpers/logger.js'
|
||||||
import { extractVideo } from '@server/helpers/video.js'
|
import { extractVideo } from '@server/helpers/video.js'
|
||||||
import { CONFIG } from '@server/initializers/config.js'
|
import { CONFIG } from '@server/initializers/config.js'
|
||||||
import { buildRemoteUrl } from '@server/lib/activitypub/url.js'
|
import { buildRemoteUrl } from '@server/lib/activitypub/url.js'
|
||||||
import {
|
import {
|
||||||
getHLSPrivateFileUrl,
|
getHLSPrivateFileUrl,
|
||||||
getHLSPublicFileUrl,
|
getObjectStoragePublicFileUrl,
|
||||||
getWebVideoPrivateFileUrl,
|
getWebVideoPrivateFileUrl
|
||||||
getWebVideoPublicFileUrl
|
|
||||||
} from '@server/lib/object-storage/index.js'
|
} from '@server/lib/object-storage/index.js'
|
||||||
import { getFSTorrentFilePath } from '@server/lib/paths.js'
|
import { getFSTorrentFilePath } from '@server/lib/paths.js'
|
||||||
|
import { getVideoFileMimeType } from '@server/lib/video-file.js'
|
||||||
import { isVideoInPrivateDirectory } from '@server/lib/video-privacy.js'
|
import { isVideoInPrivateDirectory } from '@server/lib/video-privacy.js'
|
||||||
import { MStreamingPlaylistVideo, MVideo, MVideoWithHost, isStreamingPlaylist } from '@server/types/models/index.js'
|
import { MStreamingPlaylistVideo, MVideo, MVideoWithHost, isStreamingPlaylist } from '@server/types/models/index.js'
|
||||||
import { remove } from 'fs-extra/esm'
|
import { remove } from 'fs-extra/esm'
|
||||||
|
@ -51,7 +51,6 @@ import { VideoRedundancyModel } from '../redundancy/video-redundancy.js'
|
||||||
import { SequelizeModel, doesExist, parseAggregateResult, throwIfNotValid } from '../shared/index.js'
|
import { SequelizeModel, doesExist, parseAggregateResult, throwIfNotValid } from '../shared/index.js'
|
||||||
import { VideoStreamingPlaylistModel } from './video-streaming-playlist.js'
|
import { VideoStreamingPlaylistModel } from './video-streaming-playlist.js'
|
||||||
import { VideoModel } from './video.js'
|
import { VideoModel } from './video.js'
|
||||||
import { getVideoFileMimeType } from '@server/lib/video-file.js'
|
|
||||||
|
|
||||||
export enum ScopeNames {
|
export enum ScopeNames {
|
||||||
WITH_VIDEO = 'WITH_VIDEO',
|
WITH_VIDEO = 'WITH_VIDEO',
|
||||||
|
@ -534,10 +533,10 @@ export class VideoFileModel extends SequelizeModel<VideoFileModel> {
|
||||||
|
|
||||||
private getPublicObjectStorageUrl () {
|
private getPublicObjectStorageUrl () {
|
||||||
if (this.isHLS()) {
|
if (this.isHLS()) {
|
||||||
return getHLSPublicFileUrl(this.fileUrl)
|
return getObjectStoragePublicFileUrl(this.fileUrl, CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS)
|
||||||
}
|
}
|
||||||
|
|
||||||
return getWebVideoPublicFileUrl(this.fileUrl)
|
return getObjectStoragePublicFileUrl(this.fileUrl, CONFIG.OBJECT_STORAGE.WEB_VIDEOS)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
|
import type { FileStorageType, VideoSource } from '@peertube/peertube-models'
|
||||||
|
import { STATIC_DOWNLOAD_PATHS, WEBSERVER } from '@server/initializers/constants.js'
|
||||||
|
import { join } from 'path'
|
||||||
import { Transaction } from 'sequelize'
|
import { Transaction } from 'sequelize'
|
||||||
import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, Table, UpdatedAt } from 'sequelize-typescript'
|
import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Table, UpdatedAt } from 'sequelize-typescript'
|
||||||
import { VideoSource } from '@peertube/peertube-models'
|
|
||||||
import { SequelizeModel, getSort } from '../shared/index.js'
|
import { SequelizeModel, getSort } from '../shared/index.js'
|
||||||
|
import { getResolutionLabel } from './formatter/video-api-format.js'
|
||||||
import { VideoModel } from './video.js'
|
import { VideoModel } from './video.js'
|
||||||
|
import { MVideoSource } from '@server/types/models/video/video-source.js'
|
||||||
|
|
||||||
@Table({
|
@Table({
|
||||||
tableName: 'videoSource',
|
tableName: 'videoSource',
|
||||||
|
@ -12,6 +16,10 @@ import { VideoModel } from './video.js'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fields: [ { name: 'createdAt', order: 'DESC' } ]
|
fields: [ { name: 'createdAt', order: 'DESC' } ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fields: [ 'keptOriginalFilename' ],
|
||||||
|
unique: true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
@ -24,7 +32,43 @@ export class VideoSourceModel extends SequelizeModel<VideoSourceModel> {
|
||||||
|
|
||||||
@AllowNull(false)
|
@AllowNull(false)
|
||||||
@Column
|
@Column
|
||||||
filename: string
|
inputFilename: string
|
||||||
|
|
||||||
|
@AllowNull(true)
|
||||||
|
@Column
|
||||||
|
keptOriginalFilename: string
|
||||||
|
|
||||||
|
@AllowNull(true)
|
||||||
|
@Column
|
||||||
|
resolution: number
|
||||||
|
|
||||||
|
@AllowNull(true)
|
||||||
|
@Column
|
||||||
|
width: number
|
||||||
|
|
||||||
|
@AllowNull(true)
|
||||||
|
@Column
|
||||||
|
height: number
|
||||||
|
|
||||||
|
@AllowNull(true)
|
||||||
|
@Column
|
||||||
|
fps: number
|
||||||
|
|
||||||
|
@AllowNull(true)
|
||||||
|
@Column(DataType.BIGINT)
|
||||||
|
size: number
|
||||||
|
|
||||||
|
@AllowNull(true)
|
||||||
|
@Column(DataType.JSONB)
|
||||||
|
metadata: any
|
||||||
|
|
||||||
|
@AllowNull(true)
|
||||||
|
@Column
|
||||||
|
storage: FileStorageType
|
||||||
|
|
||||||
|
@AllowNull(true)
|
||||||
|
@Column
|
||||||
|
fileUrl: string
|
||||||
|
|
||||||
@ForeignKey(() => VideoModel)
|
@ForeignKey(() => VideoModel)
|
||||||
@Column
|
@Column
|
||||||
|
@ -39,16 +83,51 @@ export class VideoSourceModel extends SequelizeModel<VideoSourceModel> {
|
||||||
Video: Awaited<VideoModel>
|
Video: Awaited<VideoModel>
|
||||||
|
|
||||||
static loadLatest (videoId: number, transaction?: Transaction) {
|
static loadLatest (videoId: number, transaction?: Transaction) {
|
||||||
return VideoSourceModel.findOne({
|
return VideoSourceModel.findOne<MVideoSource>({
|
||||||
where: { videoId },
|
where: { videoId },
|
||||||
order: getSort('-createdAt'),
|
order: getSort('-createdAt'),
|
||||||
transaction
|
transaction
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static loadByKeptOriginalFilename (keptOriginalFilename: string) {
|
||||||
|
return VideoSourceModel.findOne<MVideoSource>({
|
||||||
|
where: { keptOriginalFilename }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static listAll (videoId: number, transaction?: Transaction) {
|
||||||
|
return VideoSourceModel.findAll<MVideoSource>({
|
||||||
|
where: { videoId },
|
||||||
|
transaction
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
getFileDownloadUrl () {
|
||||||
|
if (!this.keptOriginalFilename) return null
|
||||||
|
|
||||||
|
return WEBSERVER.URL + join(STATIC_DOWNLOAD_PATHS.ORIGINAL_VIDEO_FILE, this.keptOriginalFilename)
|
||||||
|
}
|
||||||
|
|
||||||
toFormattedJSON (): VideoSource {
|
toFormattedJSON (): VideoSource {
|
||||||
return {
|
return {
|
||||||
filename: this.filename,
|
filename: this.inputFilename,
|
||||||
|
inputFilename: this.inputFilename,
|
||||||
|
fileDownloadUrl: this.getFileDownloadUrl(),
|
||||||
|
|
||||||
|
resolution: {
|
||||||
|
id: this.resolution,
|
||||||
|
label: getResolutionLabel(this.resolution)
|
||||||
|
},
|
||||||
|
size: this.size,
|
||||||
|
|
||||||
|
width: this.width,
|
||||||
|
height: this.height,
|
||||||
|
|
||||||
|
fps: this.fps,
|
||||||
|
|
||||||
|
metadata: this.metadata,
|
||||||
|
|
||||||
createdAt: this.createdAt.toISOString()
|
createdAt: this.createdAt.toISOString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import {
|
||||||
} from '@peertube/peertube-models'
|
} from '@peertube/peertube-models'
|
||||||
import { sha1 } from '@peertube/peertube-node-utils'
|
import { sha1 } from '@peertube/peertube-node-utils'
|
||||||
import { CONFIG } from '@server/initializers/config.js'
|
import { CONFIG } from '@server/initializers/config.js'
|
||||||
import { getHLSPrivateFileUrl, getHLSPublicFileUrl } from '@server/lib/object-storage/index.js'
|
import { getHLSPrivateFileUrl, getObjectStoragePublicFileUrl } from '@server/lib/object-storage/index.js'
|
||||||
import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename } from '@server/lib/paths.js'
|
import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename } from '@server/lib/paths.js'
|
||||||
import { isVideoInPrivateDirectory } from '@server/lib/video-privacy.js'
|
import { isVideoInPrivateDirectory } from '@server/lib/video-privacy.js'
|
||||||
import { VideoFileModel } from '@server/models/video/video-file.js'
|
import { VideoFileModel } from '@server/models/video/video-file.js'
|
||||||
|
@ -266,7 +266,7 @@ export class VideoStreamingPlaylistModel extends SequelizeModel<VideoStreamingPl
|
||||||
return getHLSPrivateFileUrl(video, this.playlistFilename)
|
return getHLSPrivateFileUrl(video, this.playlistFilename)
|
||||||
}
|
}
|
||||||
|
|
||||||
return getHLSPublicFileUrl(this.playlistUrl)
|
return getObjectStoragePublicFileUrl(this.playlistUrl, CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
@ -288,7 +288,7 @@ export class VideoStreamingPlaylistModel extends SequelizeModel<VideoStreamingPl
|
||||||
return getHLSPrivateFileUrl(video, this.segmentsSha256Filename)
|
return getHLSPrivateFileUrl(video, this.segmentsSha256Filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
return getHLSPublicFileUrl(this.segmentsSha256Url)
|
return getObjectStoragePublicFileUrl(this.segmentsSha256Url, CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { buildVideoEmbedPath, buildVideoWatchPath, pick, wait } from '@peertube/peertube-core-utils'
|
import { buildVideoEmbedPath, buildVideoWatchPath, pick, wait } from '@peertube/peertube-core-utils'
|
||||||
import { ffprobePromise, getAudioStream, getVideoStreamDimensionsInfo, getVideoStreamFPS, hasAudioStream } from '@peertube/peertube-ffmpeg'
|
import { ffprobePromise, getAudioStream, getVideoStreamDimensionsInfo, getVideoStreamFPS, hasAudioStream } from '@peertube/peertube-ffmpeg'
|
||||||
import {
|
import {
|
||||||
|
FileStorage,
|
||||||
ResultList,
|
ResultList,
|
||||||
ThumbnailType,
|
ThumbnailType,
|
||||||
UserRight,
|
UserRight,
|
||||||
|
@ -13,7 +14,6 @@ import {
|
||||||
VideoPrivacy,
|
VideoPrivacy,
|
||||||
VideoRateType,
|
VideoRateType,
|
||||||
VideoState,
|
VideoState,
|
||||||
FileStorage,
|
|
||||||
VideoStreamingPlaylistType,
|
VideoStreamingPlaylistType,
|
||||||
type VideoPrivacyType,
|
type VideoPrivacyType,
|
||||||
type VideoStateType
|
type VideoStateType
|
||||||
|
@ -25,6 +25,7 @@ import { LiveManager } from '@server/lib/live/live-manager.js'
|
||||||
import {
|
import {
|
||||||
removeHLSFileObjectStorageByFilename,
|
removeHLSFileObjectStorageByFilename,
|
||||||
removeHLSObjectStorage,
|
removeHLSObjectStorage,
|
||||||
|
removeOriginalFileObjectStorage,
|
||||||
removeWebVideoObjectStorage
|
removeWebVideoObjectStorage
|
||||||
} from '@server/lib/object-storage/index.js'
|
} from '@server/lib/object-storage/index.js'
|
||||||
import { tracer } from '@server/lib/opentelemetry/tracing.js'
|
import { tracer } from '@server/lib/opentelemetry/tracing.js'
|
||||||
|
@ -34,6 +35,7 @@ import { VideoPathManager } from '@server/lib/video-path-manager.js'
|
||||||
import { isVideoInPrivateDirectory } from '@server/lib/video-privacy.js'
|
import { isVideoInPrivateDirectory } from '@server/lib/video-privacy.js'
|
||||||
import { getServerActor } from '@server/models/application/application.js'
|
import { getServerActor } from '@server/models/application/application.js'
|
||||||
import { ModelCache } from '@server/models/shared/model-cache.js'
|
import { ModelCache } from '@server/models/shared/model-cache.js'
|
||||||
|
import { MVideoSource } from '@server/types/models/video/video-source.js'
|
||||||
import Bluebird from 'bluebird'
|
import Bluebird from 'bluebird'
|
||||||
import { remove } from 'fs-extra/esm'
|
import { remove } from 'fs-extra/esm'
|
||||||
import maxBy from 'lodash-es/maxBy.js'
|
import maxBy from 'lodash-es/maxBy.js'
|
||||||
|
@ -867,6 +869,12 @@ export class VideoModel extends SequelizeModel<VideoModel> {
|
||||||
for (const p of instance.VideoStreamingPlaylists) {
|
for (const p of instance.VideoStreamingPlaylists) {
|
||||||
tasks.push(instance.removeStreamingPlaylistFiles(p))
|
tasks.push(instance.removeStreamingPlaylistFiles(p))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove source files
|
||||||
|
const promiseRemoveSources = VideoSourceModel.listAll(instance.id, options.transaction)
|
||||||
|
.then(sources => Promise.all(sources.map(s => instance.removeOriginalFile(s))))
|
||||||
|
|
||||||
|
tasks.push(promiseRemoveSources)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do not wait video deletion because we could be in a transaction
|
// Do not wait video deletion because we could be in a transaction
|
||||||
|
@ -2022,6 +2030,17 @@ export class VideoModel extends SequelizeModel<VideoModel> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async removeOriginalFile (videoSource: MVideoSource) {
|
||||||
|
if (!videoSource.keptOriginalFilename) return
|
||||||
|
|
||||||
|
const filePath = VideoPathManager.Instance.getFSOriginalVideoFilePath(videoSource.keptOriginalFilename)
|
||||||
|
await remove(filePath)
|
||||||
|
|
||||||
|
if (videoSource.storage === FileStorage.OBJECT_STORAGE) {
|
||||||
|
await removeOriginalFileObjectStorage(videoSource)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
isOutdated () {
|
isOutdated () {
|
||||||
if (this.isOwned()) return false
|
if (this.isOwned()) return false
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import { OutgoingHttpHeaders } from 'http'
|
|
||||||
import { Writable } from 'stream'
|
|
||||||
import { HttpMethodType, PeerTubeProblemDocumentData, VideoCreate } from '@peertube/peertube-models'
|
import { HttpMethodType, PeerTubeProblemDocumentData, VideoCreate } from '@peertube/peertube-models'
|
||||||
import { RegisterServerAuthExternalOptions } from '@server/types/index.js'
|
import { RegisterServerAuthExternalOptions } from '@server/types/index.js'
|
||||||
import {
|
import {
|
||||||
|
@ -29,7 +27,10 @@ import { MPlugin, MServer, MServerBlocklist } from '@server/types/models/server.
|
||||||
import { MVideoImportDefault } from '@server/types/models/video/video-import.js'
|
import { MVideoImportDefault } from '@server/types/models/video/video-import.js'
|
||||||
import { MVideoPlaylistElement, MVideoPlaylistElementVideoUrlPlaylistPrivacy } from '@server/types/models/video/video-playlist-element.js'
|
import { MVideoPlaylistElement, MVideoPlaylistElementVideoUrlPlaylistPrivacy } from '@server/types/models/video/video-playlist-element.js'
|
||||||
import { MAccountVideoRateAccountVideo } from '@server/types/models/video/video-rate.js'
|
import { MAccountVideoRateAccountVideo } from '@server/types/models/video/video-rate.js'
|
||||||
import { File as UploadXFile, Metadata } from '@uploadx/core'
|
import { Metadata, File as UploadXFile } from '@uploadx/core'
|
||||||
|
import { FfprobeData } from 'fluent-ffmpeg'
|
||||||
|
import { OutgoingHttpHeaders } from 'http'
|
||||||
|
import { Writable } from 'stream'
|
||||||
import { RegisteredPlugin } from '../../lib/plugins/plugin-manager.js'
|
import { RegisteredPlugin } from '../../lib/plugins/plugin-manager.js'
|
||||||
import {
|
import {
|
||||||
MAccountDefault,
|
MAccountDefault,
|
||||||
|
@ -127,6 +128,8 @@ declare module 'express' {
|
||||||
|
|
||||||
docUrl?: string
|
docUrl?: string
|
||||||
|
|
||||||
|
ffprobe?: FfprobeData
|
||||||
|
|
||||||
videoAPI?: MVideoFormattableDetails
|
videoAPI?: MVideoFormattableDetails
|
||||||
videoAll?: MVideoFullLight
|
videoAll?: MVideoFullLight
|
||||||
onlyImmutableVideo?: MVideoImmutable
|
onlyImmutableVideo?: MVideoImmutable
|
||||||
|
|
|
@ -19,7 +19,7 @@ async function run () {
|
||||||
console.log('Moving private video files in dedicated folders.')
|
console.log('Moving private video files in dedicated folders.')
|
||||||
|
|
||||||
await ensureDir(DIRECTORIES.HLS_STREAMING_PLAYLIST.PRIVATE)
|
await ensureDir(DIRECTORIES.HLS_STREAMING_PLAYLIST.PRIVATE)
|
||||||
await ensureDir(DIRECTORIES.VIDEOS.PRIVATE)
|
await ensureDir(DIRECTORIES.WEB_VIDEOS.PRIVATE)
|
||||||
|
|
||||||
await initDatabaseModels(true)
|
await initDatabaseModels(true)
|
||||||
|
|
||||||
|
|
|
@ -38,8 +38,8 @@ async function run () {
|
||||||
console.log('Detecting files to remove, it could take a while...')
|
console.log('Detecting files to remove, it could take a while...')
|
||||||
|
|
||||||
toDelete = toDelete.concat(
|
toDelete = toDelete.concat(
|
||||||
await pruneDirectory(DIRECTORIES.VIDEOS.PUBLIC, doesWebVideoFileExist()),
|
await pruneDirectory(DIRECTORIES.WEB_VIDEOS.PUBLIC, doesWebVideoFileExist()),
|
||||||
await pruneDirectory(DIRECTORIES.VIDEOS.PRIVATE, doesWebVideoFileExist()),
|
await pruneDirectory(DIRECTORIES.WEB_VIDEOS.PRIVATE, doesWebVideoFileExist()),
|
||||||
|
|
||||||
await pruneDirectory(DIRECTORIES.HLS_STREAMING_PLAYLIST.PRIVATE, doesHLSPlaylistExist()),
|
await pruneDirectory(DIRECTORIES.HLS_STREAMING_PLAYLIST.PRIVATE, doesHLSPlaylistExist()),
|
||||||
await pruneDirectory(DIRECTORIES.HLS_STREAMING_PLAYLIST.PUBLIC, doesHLSPlaylistExist()),
|
await pruneDirectory(DIRECTORIES.HLS_STREAMING_PLAYLIST.PUBLIC, doesHLSPlaylistExist()),
|
||||||
|
@ -97,7 +97,7 @@ async function pruneDirectory (directory: string, existFun: ExistFun) {
|
||||||
function doesWebVideoFileExist () {
|
function doesWebVideoFileExist () {
|
||||||
return (filePath: string) => {
|
return (filePath: string) => {
|
||||||
// Don't delete private directory
|
// Don't delete private directory
|
||||||
if (filePath === DIRECTORIES.VIDEOS.PRIVATE) return true
|
if (filePath === DIRECTORIES.WEB_VIDEOS.PRIVATE) return true
|
||||||
|
|
||||||
return VideoFileModel.doesOwnedWebVideoFileExist(basename(filePath))
|
return VideoFileModel.doesOwnedWebVideoFileExist(basename(filePath))
|
||||||
}
|
}
|
||||||
|
|
|
@ -8136,6 +8136,29 @@ components:
|
||||||
properties:
|
properties:
|
||||||
filename:
|
filename:
|
||||||
type: string
|
type: string
|
||||||
|
deprecated: true
|
||||||
|
description: 'Deprecated in 6.1, use inputFilename instead'
|
||||||
|
inputFilename:
|
||||||
|
type: string
|
||||||
|
description: 'Uploaded/imported filename'
|
||||||
|
fileDownloadUrl:
|
||||||
|
type: string
|
||||||
|
description: "**PeerTube >= 6.1** If enabled by the admin, the video source file is kept on the server and can be downloaded by the owner"
|
||||||
|
resolution:
|
||||||
|
$ref: '#/components/schemas/VideoResolutionConstant'
|
||||||
|
description: "**PeerTube >= 6.1**"
|
||||||
|
size:
|
||||||
|
type: integer
|
||||||
|
description: "**PeerTube >= 6.1** Video file size in bytes"
|
||||||
|
fps:
|
||||||
|
type: number
|
||||||
|
description: "**PeerTube >= 6.1** Frames per second of the video file"
|
||||||
|
width:
|
||||||
|
type: number
|
||||||
|
description: "**PeerTube >= 6.1** Video stream width"
|
||||||
|
height:
|
||||||
|
type: number
|
||||||
|
description: "**PeerTube >= 6.1** Video stream height"
|
||||||
createdAt:
|
createdAt:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
|
@ -8792,6 +8815,11 @@ components:
|
||||||
properties:
|
properties:
|
||||||
enabled:
|
enabled:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
originalFile:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
keep:
|
||||||
|
type: boolean
|
||||||
allowAdditionalExtensions:
|
allowAdditionalExtensions:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: Allow your users to upload .mkv, .mov, .avi, .wmv, .flv, .f4v, .3g2, .3gp, .mts, m2ts, .mxf, .nut videos
|
description: Allow your users to upload .mkv, .mov, .avi, .wmv, .flv, .f4v, .3g2, .3gp, .mts, m2ts, .mxf, .nut videos
|
||||||
|
|
Loading…
Reference in New Issue