parent
058ef6912c
commit
a159b8b517
|
@ -112,7 +112,8 @@ export class VideoAdminService {
|
||||||
let include = VideoInclude.BLACKLISTED |
|
let include = VideoInclude.BLACKLISTED |
|
||||||
VideoInclude.BLOCKED_OWNER |
|
VideoInclude.BLOCKED_OWNER |
|
||||||
VideoInclude.NOT_PUBLISHED_STATE |
|
VideoInclude.NOT_PUBLISHED_STATE |
|
||||||
VideoInclude.FILES
|
VideoInclude.FILES |
|
||||||
|
VideoInclude.SOURCE
|
||||||
|
|
||||||
let privacyOneOf = getAllPrivacies()
|
let privacyOneOf = getAllPrivacies()
|
||||||
|
|
||||||
|
|
|
@ -70,14 +70,17 @@
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<span class="pt-badge badge-blue" *ngIf="video.isLocal">Local</span>
|
@if (video.isLocal) {
|
||||||
<span class="pt-badge badge-purple" *ngIf="!video.isLocal">Remote</span>
|
<span class="pt-badge badge-blue" i18n>Local</span>
|
||||||
|
} @else {
|
||||||
|
<span class="pt-badge badge-purple" i18n>Remote</span>
|
||||||
|
}
|
||||||
|
|
||||||
<span [ngClass]="getPrivacyBadgeClass(video)" class="pt-badge">{{ video.privacy.label }}</span>
|
<span [ngClass]="getPrivacyBadgeClass(video)" class="pt-badge">{{ video.privacy.label }}</span>
|
||||||
|
|
||||||
<span *ngIf="video.nsfw" class="pt-badge badge-red" i18n>NSFW</span>
|
<span *ngIf="video.nsfw" class="pt-badge badge-red" i18n>NSFW</span>
|
||||||
|
|
||||||
<span *ngIf="isUnpublished(video)" class="pt-badge badge-yellow" i18n>{{ video.state.label }}</span>
|
<span *ngIf="isUnpublished(video)" class="pt-badge badge-yellow">{{ video.state.label }}</span>
|
||||||
|
|
||||||
<span *ngIf="isAccountBlocked(video)" class="pt-badge badge-red" i18n>Account muted</span>
|
<span *ngIf="isAccountBlocked(video)" class="pt-badge badge-red" i18n>Account muted</span>
|
||||||
<span *ngIf="isServerBlocked(video)" class="pt-badge badge-red" i18n>Server muted</span>
|
<span *ngIf="isServerBlocked(video)" class="pt-badge badge-red" i18n>Server muted</span>
|
||||||
|
@ -86,10 +89,11 @@
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<span *ngIf="hasHLS(video)" class="pt-badge badge-blue">HLS</span>
|
<span *ngIf="hasOriginalFile(video)" class="pt-badge badge-blue" i18n>Original file</span>
|
||||||
<span *ngIf="hasWebVideos(video)" class="pt-badge badge-blue">Web Videos ({{ video.files.length }})</span>
|
<span *ngIf="hasHLS(video)" class="pt-badge badge-blue" i18n>HLS</span>
|
||||||
<span i18n *ngIf="video.isLive" class="pt-badge badge-blue">Live</span>
|
<span *ngIf="hasWebVideos(video)" class="pt-badge badge-blue" i18n>Web Videos ({{ video.files.length }})</span>
|
||||||
<span i18n *ngIf="hasObjectStorage(video)" class="pt-badge badge-purple">Object storage</span>
|
<span *ngIf="video.isLive" class="pt-badge badge-blue" i18n>Live</span>
|
||||||
|
<span *ngIf="hasObjectStorage(video)" class="pt-badge badge-purple" i18n>Object storage</span>
|
||||||
|
|
||||||
<span *ngIf="!isImport(video) && !video.isLive && video.isLocal">{{ getFilesSize(video) | bytes: 1 }}</span>
|
<span *ngIf="!isImport(video) && !video.isLive && video.isLocal">{{ getFilesSize(video) | bytes: 1 }}</span>
|
||||||
</td>
|
</td>
|
||||||
|
@ -105,8 +109,26 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td class="video-info expand-cell" myAutoColspan>
|
<td class="video-info expand-cell" myAutoColspan>
|
||||||
<div>
|
<div>
|
||||||
<div *ngIf="hasWebVideos(video)">
|
<div class="me-3" *ngIf="hasOriginalFile(video)">
|
||||||
Web Videos:
|
<ng-container i18n>Original file:</ng-container>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
{{ video.videoSource.inputFilename }}: {{ video.videoSource.size | bytes: 1 }}
|
||||||
|
|
||||||
|
<button
|
||||||
|
*ngIf="canRemoveOneFile(video)" class="border-0 p-0"
|
||||||
|
i18n-title title="Delete this file"
|
||||||
|
(click)="removeVideoSourceFile(video)"
|
||||||
|
>
|
||||||
|
<my-global-icon iconName="delete"></my-global-icon>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="me-3" *ngIf="hasWebVideos(video)">
|
||||||
|
<ng-container i18n>Web Videos:</ng-container>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li *ngFor="let file of video.files">
|
<li *ngFor="let file of video.files">
|
||||||
|
@ -124,7 +146,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="hasHLS(video)">
|
<div *ngIf="hasHLS(video)">
|
||||||
HLS:
|
<ng-container i18n>HLS:</ng-container>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li *ngFor="let file of video.streamingPlaylists[0].files">
|
<li *ngFor="let file of video.streamingPlaylists[0].files">
|
||||||
|
|
|
@ -1,32 +1,32 @@
|
||||||
import { SortMeta, SharedModule } from 'primeng/api'
|
import { DatePipe, NgClass, NgFor, NgIf } from '@angular/common'
|
||||||
import { finalize } from 'rxjs/operators'
|
|
||||||
import { Component, OnInit, ViewChild } from '@angular/core'
|
import { Component, OnInit, ViewChild } from '@angular/core'
|
||||||
import { ActivatedRoute, Router } from '@angular/router'
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
import { AuthService, ConfirmService, Notifier, RestPagination, RestTable } from '@app/core'
|
import { AuthService, ConfirmService, Notifier, RestPagination, RestTable } from '@app/core'
|
||||||
import { formatICU, getAbsoluteAPIUrl } from '@app/helpers'
|
import { formatICU, getAbsoluteAPIUrl } from '@app/helpers'
|
||||||
|
import { Video } from '@app/shared/shared-main/video/video.model'
|
||||||
|
import { VideoService } from '@app/shared/shared-main/video/video.service'
|
||||||
|
import { VideoBlockComponent } from '@app/shared/shared-moderation/video-block.component'
|
||||||
|
import { VideoBlockService } from '@app/shared/shared-moderation/video-block.service'
|
||||||
|
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { getAllFiles } from '@peertube/peertube-core-utils'
|
import { getAllFiles } from '@peertube/peertube-core-utils'
|
||||||
import { UserRight, VideoFile, VideoPrivacy, VideoState, VideoStreamingPlaylistType } from '@peertube/peertube-models'
|
import { UserRight, VideoFile, VideoPrivacy, VideoState, VideoStreamingPlaylistType } from '@peertube/peertube-models'
|
||||||
import { VideoAdminService } from './video-admin.service'
|
import { SharedModule, SortMeta } from 'primeng/api'
|
||||||
import { BytesPipe } from '../../../shared/shared-main/angular/bytes.pipe'
|
import { TableModule } from 'primeng/table'
|
||||||
import { EmbedComponent } from '../../../shared/shared-main/video/embed.component'
|
import { finalize } from 'rxjs/operators'
|
||||||
|
import { AdvancedInputFilter, AdvancedInputFilterComponent } from '../../../shared/shared-forms/advanced-input-filter.component'
|
||||||
|
import { GlobalIconComponent } from '../../../shared/shared-icons/global-icon.component'
|
||||||
import { AutoColspanDirective } from '../../../shared/shared-main/angular/auto-colspan.directive'
|
import { AutoColspanDirective } from '../../../shared/shared-main/angular/auto-colspan.directive'
|
||||||
|
import { BytesPipe } from '../../../shared/shared-main/angular/bytes.pipe'
|
||||||
|
import { ActionDropdownComponent, DropdownAction } from '../../../shared/shared-main/buttons/action-dropdown.component'
|
||||||
|
import { ButtonComponent } from '../../../shared/shared-main/buttons/button.component'
|
||||||
|
import { EmbedComponent } from '../../../shared/shared-main/video/embed.component'
|
||||||
|
import { TableExpanderIconComponent } from '../../../shared/shared-tables/table-expander-icon.component'
|
||||||
import { VideoCellComponent } from '../../../shared/shared-tables/video-cell.component'
|
import { VideoCellComponent } from '../../../shared/shared-tables/video-cell.component'
|
||||||
import {
|
import {
|
||||||
VideoActionsDisplayType,
|
VideoActionsDisplayType,
|
||||||
VideoActionsDropdownComponent
|
VideoActionsDropdownComponent
|
||||||
} from '../../../shared/shared-video-miniature/video-actions-dropdown.component'
|
} from '../../../shared/shared-video-miniature/video-actions-dropdown.component'
|
||||||
import { TableExpanderIconComponent } from '../../../shared/shared-tables/table-expander-icon.component'
|
import { VideoAdminService } from './video-admin.service'
|
||||||
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'
|
|
||||||
import { ButtonComponent } from '../../../shared/shared-main/buttons/button.component'
|
|
||||||
import { AdvancedInputFilter, AdvancedInputFilterComponent } from '../../../shared/shared-forms/advanced-input-filter.component'
|
|
||||||
import { ActionDropdownComponent, DropdownAction } from '../../../shared/shared-main/buttons/action-dropdown.component'
|
|
||||||
import { NgClass, NgIf, NgFor, DatePipe } from '@angular/common'
|
|
||||||
import { TableModule } from 'primeng/table'
|
|
||||||
import { GlobalIconComponent } from '../../../shared/shared-icons/global-icon.component'
|
|
||||||
import { VideoService } from '@app/shared/shared-main/video/video.service'
|
|
||||||
import { Video } from '@app/shared/shared-main/video/video.model'
|
|
||||||
import { VideoBlockComponent } from '@app/shared/shared-moderation/video-block.component'
|
|
||||||
import { VideoBlockService } from '@app/shared/shared-moderation/video-block.service'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-video-list',
|
selector: 'my-video-list',
|
||||||
|
@ -187,6 +187,10 @@ export class VideoListComponent extends RestTable <Video> implements OnInit {
|
||||||
return video.state.id === VideoState.TO_IMPORT
|
return video.state.id === VideoState.TO_IMPORT
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasOriginalFile (video: Video) {
|
||||||
|
return !!video.videoSource?.fileDownloadUrl
|
||||||
|
}
|
||||||
|
|
||||||
hasHLS (video: Video) {
|
hasHLS (video: Video) {
|
||||||
const p = video.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
|
const p = video.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
|
||||||
if (!p) return false
|
if (!p) return false
|
||||||
|
@ -211,13 +215,10 @@ export class VideoListComponent extends RestTable <Video> implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
getFilesSize (video: Video) {
|
getFilesSize (video: Video) {
|
||||||
let files = video.files
|
let total = getAllFiles(video).reduce((p, f) => p += f.size, 0)
|
||||||
|
total += video.videoSource?.size || 0
|
||||||
|
|
||||||
if (this.hasHLS(video)) {
|
return total
|
||||||
files = files.concat(video.streamingPlaylists[0].files)
|
|
||||||
}
|
|
||||||
|
|
||||||
return files.reduce((p, f) => p += f.size, 0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeVideoFile (video: Video, file: VideoFile, type: 'hls' | 'web-videos') {
|
async removeVideoFile (video: Video, file: VideoFile, type: 'hls' | 'web-videos') {
|
||||||
|
@ -236,6 +237,22 @@ export class VideoListComponent extends RestTable <Video> implements OnInit {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async removeVideoSourceFile (video: Video) {
|
||||||
|
const message = $localize`Are you sure you want to delete the original file of this video?`
|
||||||
|
const res = await this.confirmService.confirm(message, $localize`Delete original file`)
|
||||||
|
if (res === false) return
|
||||||
|
|
||||||
|
this.videoService.removeSourceFile(video.uuid)
|
||||||
|
.subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.notifier.success($localize`Original file removed.`)
|
||||||
|
this.reloadData()
|
||||||
|
},
|
||||||
|
|
||||||
|
error: err => this.notifier.error(err.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
protected reloadDataInternal () {
|
protected reloadDataInternal () {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,8 @@ import {
|
||||||
VideoState,
|
VideoState,
|
||||||
VideoStateType,
|
VideoStateType,
|
||||||
VideoStreamingPlaylist,
|
VideoStreamingPlaylist,
|
||||||
VideoStreamingPlaylistType
|
VideoStreamingPlaylistType,
|
||||||
|
VideoSource
|
||||||
} from '@peertube/peertube-models'
|
} from '@peertube/peertube-models'
|
||||||
|
|
||||||
export class Video implements VideoServerModel {
|
export class Video implements VideoServerModel {
|
||||||
|
@ -111,6 +112,8 @@ export class Video implements VideoServerModel {
|
||||||
streamingPlaylists?: VideoStreamingPlaylist[]
|
streamingPlaylists?: VideoStreamingPlaylist[]
|
||||||
files?: VideoFile[]
|
files?: VideoFile[]
|
||||||
|
|
||||||
|
videoSource?: VideoSource
|
||||||
|
|
||||||
static buildWatchUrl (video: Partial<Pick<Video, 'uuid' | 'shortUUID'>>) {
|
static buildWatchUrl (video: Partial<Pick<Video, 'uuid' | 'shortUUID'>>) {
|
||||||
return buildVideoWatchPath({ shortUUID: video.shortUUID || video.uuid })
|
return buildVideoWatchPath({ shortUUID: video.shortUUID || video.uuid })
|
||||||
}
|
}
|
||||||
|
@ -192,6 +195,7 @@ export class Video implements VideoServerModel {
|
||||||
|
|
||||||
this.streamingPlaylists = hash.streamingPlaylists
|
this.streamingPlaylists = hash.streamingPlaylists
|
||||||
this.files = hash.files
|
this.files = hash.files
|
||||||
|
this.videoSource = hash.videoSource
|
||||||
|
|
||||||
this.userHistory = hash.userHistory
|
this.userHistory = hash.userHistory
|
||||||
|
|
||||||
|
|
|
@ -326,6 +326,11 @@ export class VideoService {
|
||||||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
removeSourceFile (videoId: number | string) {
|
||||||
|
return this.authHttp.delete(VideoService.BASE_VIDEO_URL + '/' + videoId + '/source/file')
|
||||||
|
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||||
|
}
|
||||||
|
|
||||||
runTranscoding (options: {
|
runTranscoding (options: {
|
||||||
videoIds: (number | string)[]
|
videoIds: (number | string)[]
|
||||||
type: 'hls' | 'web-video'
|
type: 'hls' | 'web-video'
|
||||||
|
|
|
@ -4,7 +4,8 @@ export const VideoInclude = {
|
||||||
BLACKLISTED: 1 << 1,
|
BLACKLISTED: 1 << 1,
|
||||||
BLOCKED_OWNER: 1 << 2,
|
BLOCKED_OWNER: 1 << 2,
|
||||||
FILES: 1 << 3,
|
FILES: 1 << 3,
|
||||||
CAPTIONS: 1 << 4
|
CAPTIONS: 1 << 4,
|
||||||
|
SOURCE: 1 << 5
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type VideoIncludeType = typeof VideoInclude[keyof typeof VideoInclude]
|
export type VideoIncludeType = typeof VideoInclude[keyof typeof VideoInclude]
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { VideoFile } from './file/index.js'
|
||||||
import { VideoConstant } from './video-constant.model.js'
|
import { VideoConstant } from './video-constant.model.js'
|
||||||
import { VideoPrivacyType } from './video-privacy.enum.js'
|
import { VideoPrivacyType } from './video-privacy.enum.js'
|
||||||
import { VideoScheduleUpdate } from './video-schedule-update.model.js'
|
import { VideoScheduleUpdate } from './video-schedule-update.model.js'
|
||||||
|
import { VideoSource } from './video-source.model.js'
|
||||||
import { VideoStateType } from './video-state.enum.js'
|
import { VideoStateType } from './video-state.enum.js'
|
||||||
import { VideoStreamingPlaylist } from './video-streaming-playlist.model.js'
|
import { VideoStreamingPlaylist } from './video-streaming-playlist.model.js'
|
||||||
|
|
||||||
|
@ -75,6 +76,8 @@ export interface VideoAdditionalAttributes {
|
||||||
|
|
||||||
files: VideoFile[]
|
files: VideoFile[]
|
||||||
streamingPlaylists: VideoStreamingPlaylist[]
|
streamingPlaylists: VideoStreamingPlaylist[]
|
||||||
|
|
||||||
|
videoSource: VideoSource
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VideoDetails extends Video {
|
export interface VideoDetails extends Video {
|
||||||
|
|
|
@ -183,6 +183,20 @@ export class VideosCommand extends AbstractCommand {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deleteSource (options: OverrideCommandOptions & {
|
||||||
|
id: number | string
|
||||||
|
}) {
|
||||||
|
const path = '/api/v1/videos/' + options.id + '/source/file'
|
||||||
|
|
||||||
|
return this.deleteRequest({
|
||||||
|
...options,
|
||||||
|
|
||||||
|
path,
|
||||||
|
implicitToken: true,
|
||||||
|
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async getId (options: OverrideCommandOptions & {
|
async getId (options: OverrideCommandOptions & {
|
||||||
uuid: number | string
|
uuid: number | string
|
||||||
}) {
|
}) {
|
||||||
|
@ -273,12 +287,12 @@ export class VideosCommand extends AbstractCommand {
|
||||||
const privacyOneOf = getAllPrivacies()
|
const privacyOneOf = getAllPrivacies()
|
||||||
|
|
||||||
return this.list({
|
return this.list({
|
||||||
...options,
|
|
||||||
|
|
||||||
include,
|
include,
|
||||||
nsfw,
|
nsfw,
|
||||||
privacyOneOf,
|
privacyOneOf,
|
||||||
|
|
||||||
|
...options,
|
||||||
|
|
||||||
token: this.buildCommonRequestToken({ ...options, implicitToken: true })
|
token: this.buildCommonRequestToken({ ...options, implicitToken: true })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -209,6 +209,38 @@ describe('Test video sources API validator', function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('When deleting video source file', function () {
|
||||||
|
let userAccessToken: string
|
||||||
|
|
||||||
|
let videoId: string
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
userAccessToken = await server.users.generateUserAndToken('user56')
|
||||||
|
|
||||||
|
await server.config.enableMinimumTranscoding({ keepOriginal: true })
|
||||||
|
const { uuid } = await server.videos.quickUpload({ name: 'with source' })
|
||||||
|
videoId = uuid
|
||||||
|
|
||||||
|
await waitJobs([ server ])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail without token', async function () {
|
||||||
|
await server.videos.deleteSource({ id: videoId, token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with another user', async function () {
|
||||||
|
await server.videos.deleteSource({ id: videoId, token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with an unknown video', async function () {
|
||||||
|
await server.videos.deleteSource({ id: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should succeed with the correct params', async function () {
|
||||||
|
await server.videos.deleteSource({ id: videoId })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
after(async function () {
|
after(async function () {
|
||||||
await cleanupTests([ server ])
|
await cleanupTests([ server ])
|
||||||
})
|
})
|
||||||
|
|
|
@ -48,7 +48,8 @@ describe('Test video filters validators', function () {
|
||||||
const validIncludes = [
|
const validIncludes = [
|
||||||
VideoInclude.NONE,
|
VideoInclude.NONE,
|
||||||
VideoInclude.BLOCKED_OWNER,
|
VideoInclude.BLOCKED_OWNER,
|
||||||
VideoInclude.NOT_PUBLISHED_STATE | VideoInclude.BLACKLISTED
|
VideoInclude.NOT_PUBLISHED_STATE | VideoInclude.BLACKLISTED,
|
||||||
|
VideoInclude.SOURCE
|
||||||
]
|
]
|
||||||
|
|
||||||
async function testEndpoints (options: {
|
async function testEndpoints (options: {
|
||||||
|
|
|
@ -1,6 +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 { getAllFiles } from '@peertube/peertube-core-utils'
|
import { getAllFiles } from '@peertube/peertube-core-utils'
|
||||||
import { HttpStatusCode, VideoPrivacy } from '@peertube/peertube-models'
|
import { HttpStatusCode, VideoInclude, VideoPrivacy } from '@peertube/peertube-models'
|
||||||
import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils'
|
import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils'
|
||||||
import {
|
import {
|
||||||
ObjectStorageCommand,
|
ObjectStorageCommand,
|
||||||
|
@ -104,6 +104,20 @@ describe('Test video source management', function () {
|
||||||
expect(source.metadata?.streams).to.be.an('array')
|
expect(source.metadata?.streams).to.be.an('array')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('Should include video source file when listing videos in admin', async function () {
|
||||||
|
const { total, data } = await servers[0].videos.listAllForAdmin({ include: VideoInclude.SOURCE, sort: 'publishedAt' })
|
||||||
|
expect(total).to.equal(2)
|
||||||
|
expect(data).to.have.lengthOf(2)
|
||||||
|
|
||||||
|
expect(data[0].videoSource).to.exist
|
||||||
|
expect(data[0].videoSource.inputFilename).to.equal(fixture1)
|
||||||
|
expect(data[0].videoSource.fileDownloadUrl).to.be.null
|
||||||
|
|
||||||
|
expect(data[1].videoSource).to.exist
|
||||||
|
expect(data[1].videoSource.inputFilename).to.equal(fixture2)
|
||||||
|
expect(data[1].videoSource.fileDownloadUrl).to.exist
|
||||||
|
})
|
||||||
|
|
||||||
it('Should have kept original video file', 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] })
|
await checkSourceFile({ server: servers[0], fsCount: 1, fixture: fixture2, uuid: uuids[uuids.length - 1] })
|
||||||
})
|
})
|
||||||
|
@ -135,6 +149,33 @@ describe('Test video source management', function () {
|
||||||
expect(source.metadata?.streams).to.be.an('array')
|
expect(source.metadata?.streams).to.be.an('array')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('Should delete video source file', async function () {
|
||||||
|
await servers[0].videos.deleteSource({ id: uuids[uuids.length - 1] })
|
||||||
|
|
||||||
|
const { total, data } = await servers[0].videos.listAllForAdmin({ include: VideoInclude.SOURCE, sort: 'publishedAt' })
|
||||||
|
expect(total).to.equal(3)
|
||||||
|
expect(data).to.have.lengthOf(3)
|
||||||
|
|
||||||
|
expect(data[0].videoSource).to.exist
|
||||||
|
expect(data[0].videoSource.inputFilename).to.equal(fixture1)
|
||||||
|
expect(data[0].videoSource.fileDownloadUrl).to.be.null
|
||||||
|
|
||||||
|
expect(data[1].videoSource).to.exist
|
||||||
|
expect(data[1].videoSource.inputFilename).to.equal(fixture2)
|
||||||
|
expect(data[1].videoSource.fileDownloadUrl).to.exist
|
||||||
|
|
||||||
|
expect(data[2].videoSource).to.exist
|
||||||
|
expect(data[2].videoSource.fileDownloadUrl).to.not.exist
|
||||||
|
expect(data[2].videoSource.createdAt).to.exist
|
||||||
|
expect(data[2].videoSource.fps).to.be.null
|
||||||
|
expect(data[2].videoSource.height).to.be.null
|
||||||
|
expect(data[2].videoSource.width).to.be.null
|
||||||
|
expect(data[2].videoSource.resolution.id).to.be.null
|
||||||
|
expect(data[2].videoSource.resolution.label).to.be.null
|
||||||
|
expect(data[2].videoSource.size).to.be.null
|
||||||
|
expect(data[2].videoSource.metadata).to.be.null
|
||||||
|
})
|
||||||
|
|
||||||
it('Should delete all videos and do not have original files anymore', async function () {
|
it('Should delete all videos and do not have original files anymore', async function () {
|
||||||
this.timeout(60000)
|
this.timeout(60000)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { buildAspectRatio } from '@peertube/peertube-core-utils'
|
import { buildAspectRatio } from '@peertube/peertube-core-utils'
|
||||||
import { VideoState } from '@peertube/peertube-models'
|
import { HttpStatusCode, UserRight, 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'
|
||||||
|
@ -19,6 +19,7 @@ import { logger, loggerTagsFactory } from '../../../helpers/logger.js'
|
||||||
import {
|
import {
|
||||||
asyncMiddleware,
|
asyncMiddleware,
|
||||||
authenticate,
|
authenticate,
|
||||||
|
ensureUserHasRight,
|
||||||
replaceVideoSourceResumableInitValidator,
|
replaceVideoSourceResumableInitValidator,
|
||||||
replaceVideoSourceResumableValidator,
|
replaceVideoSourceResumableValidator,
|
||||||
videoSourceGetLatestValidator
|
videoSourceGetLatestValidator
|
||||||
|
@ -35,6 +36,14 @@ videoSourceRouter.get('/:id/source',
|
||||||
getVideoLatestSource
|
getVideoLatestSource
|
||||||
)
|
)
|
||||||
|
|
||||||
|
videoSourceRouter.delete('/:id/source/file',
|
||||||
|
openapiOperationDoc({ operationId: 'deleteVideoSourceFile' }),
|
||||||
|
authenticate,
|
||||||
|
ensureUserHasRight(UserRight.MANAGE_VIDEO_FILES),
|
||||||
|
asyncMiddleware(videoSourceGetLatestValidator),
|
||||||
|
asyncMiddleware(deleteVideoLatestSourceFile)
|
||||||
|
)
|
||||||
|
|
||||||
setupUploadResumableRoutes({
|
setupUploadResumableRoutes({
|
||||||
routePath: '/:id/source/replace-resumable',
|
routePath: '/:id/source/replace-resumable',
|
||||||
router: videoSourceRouter,
|
router: videoSourceRouter,
|
||||||
|
@ -52,6 +61,24 @@ export {
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async function deleteVideoLatestSourceFile (req: express.Request, res: express.Response) {
|
||||||
|
const videoSource = res.locals.videoSource
|
||||||
|
const video = res.locals.videoAll
|
||||||
|
|
||||||
|
await video.removeOriginalFile(videoSource)
|
||||||
|
|
||||||
|
videoSource.keptOriginalFilename = null
|
||||||
|
videoSource.fps = null
|
||||||
|
videoSource.resolution = null
|
||||||
|
videoSource.width = null
|
||||||
|
videoSource.height = null
|
||||||
|
videoSource.metadata = null
|
||||||
|
videoSource.size = null
|
||||||
|
await videoSource.save()
|
||||||
|
|
||||||
|
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||||
|
}
|
||||||
|
|
||||||
function getVideoLatestSource (req: express.Request, res: express.Response) {
|
function getVideoLatestSource (req: express.Request, res: express.Response) {
|
||||||
return res.json(res.locals.videoSource.toFormattedJSON())
|
return res.json(res.locals.videoSource.toFormattedJSON())
|
||||||
}
|
}
|
||||||
|
|
|
@ -472,7 +472,7 @@ const commonVideosFiltersValidator = [
|
||||||
if (req.query.include || req.query.privacyOneOf) {
|
if (req.query.include || req.query.privacyOneOf) {
|
||||||
return res.fail({
|
return res.fail({
|
||||||
status: HttpStatusCode.UNAUTHORIZED_401,
|
status: HttpStatusCode.UNAUTHORIZED_401,
|
||||||
message: 'You are not allowed to see all videos.'
|
message: 'You are not allowed to see all videos or specify a custom include.'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ export type VideoFormattingJSONOptions = {
|
||||||
scheduledUpdate?: boolean
|
scheduledUpdate?: boolean
|
||||||
blacklistInfo?: boolean
|
blacklistInfo?: boolean
|
||||||
files?: boolean
|
files?: boolean
|
||||||
|
source?: boolean
|
||||||
blockedOwner?: boolean
|
blockedOwner?: boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,6 +42,7 @@ export function guessAdditionalAttributesFromQuery (query: VideosCommonQueryAfte
|
||||||
scheduledUpdate: !!(query.include & VideoInclude.NOT_PUBLISHED_STATE),
|
scheduledUpdate: !!(query.include & VideoInclude.NOT_PUBLISHED_STATE),
|
||||||
blacklistInfo: !!(query.include & VideoInclude.BLACKLISTED),
|
blacklistInfo: !!(query.include & VideoInclude.BLACKLISTED),
|
||||||
files: !!(query.include & VideoInclude.FILES),
|
files: !!(query.include & VideoInclude.FILES),
|
||||||
|
source: !!(query.include & VideoInclude.SOURCE),
|
||||||
blockedOwner: !!(query.include & VideoInclude.BLOCKED_OWNER)
|
blockedOwner: !!(query.include & VideoInclude.BLOCKED_OWNER)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -310,5 +312,9 @@ function buildAdditionalAttributes (video: MVideoFormattable, options: VideoForm
|
||||||
result.files = videoFilesModelToFormattedJSON(video, video.VideoFiles)
|
result.files = videoFilesModelToFormattedJSON(video, video.VideoFiles)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (add?.source === true) {
|
||||||
|
result.videoSource = video.VideoSource?.toFormattedJSON() || null
|
||||||
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
|
@ -247,6 +247,18 @@ export class AbstractVideoQueryBuilder extends AbstractRunQuery {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected includeVideoSource () {
|
||||||
|
this.addJoin(
|
||||||
|
'LEFT OUTER JOIN "videoSource" AS "VideoSource" ON "video"."id" = "VideoSource"."videoId"'
|
||||||
|
)
|
||||||
|
|
||||||
|
this.attributes = {
|
||||||
|
...this.attributes,
|
||||||
|
|
||||||
|
...this.buildAttributesObject('VideoSource', this.tables.getVideoSourceAttributes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected includeTrackers () {
|
protected includeTrackers () {
|
||||||
this.addJoin(
|
this.addJoin(
|
||||||
'LEFT OUTER JOIN (' +
|
'LEFT OUTER JOIN (' +
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { ServerBlocklistModel } from '@server/models/server/server-blocklist.js'
|
||||||
import { ServerModel } from '@server/models/server/server.js'
|
import { ServerModel } from '@server/models/server/server.js'
|
||||||
import { TrackerModel } from '@server/models/server/tracker.js'
|
import { TrackerModel } from '@server/models/server/tracker.js'
|
||||||
import { UserVideoHistoryModel } from '@server/models/user/user-video-history.js'
|
import { UserVideoHistoryModel } from '@server/models/user/user-video-history.js'
|
||||||
|
import { VideoSourceModel } from '@server/models/video/video-source.js'
|
||||||
import { ScheduleVideoUpdateModel } from '../../../schedule-video-update.js'
|
import { ScheduleVideoUpdateModel } from '../../../schedule-video-update.js'
|
||||||
import { TagModel } from '../../../tag.js'
|
import { TagModel } from '../../../tag.js'
|
||||||
import { ThumbnailModel } from '../../../thumbnail.js'
|
import { ThumbnailModel } from '../../../thumbnail.js'
|
||||||
|
@ -39,6 +40,7 @@ export class VideoModelBuilder {
|
||||||
private accountBlocklistDone: Set<any>
|
private accountBlocklistDone: Set<any>
|
||||||
private serverBlocklistDone: Set<any>
|
private serverBlocklistDone: Set<any>
|
||||||
private liveDone: Set<any>
|
private liveDone: Set<any>
|
||||||
|
private sourceDone: Set<any>
|
||||||
private redundancyDone: Set<any>
|
private redundancyDone: Set<any>
|
||||||
private scheduleVideoUpdateDone: Set<any>
|
private scheduleVideoUpdateDone: Set<any>
|
||||||
|
|
||||||
|
@ -108,6 +110,10 @@ export class VideoModelBuilder {
|
||||||
this.setBlockedOwner(row, videoModel)
|
this.setBlockedOwner(row, videoModel)
|
||||||
this.setBlockedServer(row, videoModel)
|
this.setBlockedServer(row, videoModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (include & VideoInclude.SOURCE) {
|
||||||
|
this.setSource(row, videoModel)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,6 +133,7 @@ export class VideoModelBuilder {
|
||||||
this.historyDone = new Set()
|
this.historyDone = new Set()
|
||||||
this.blacklistDone = new Set()
|
this.blacklistDone = new Set()
|
||||||
this.liveDone = new Set()
|
this.liveDone = new Set()
|
||||||
|
this.sourceDone = new Set()
|
||||||
this.redundancyDone = new Set()
|
this.redundancyDone = new Set()
|
||||||
this.scheduleVideoUpdateDone = new Set()
|
this.scheduleVideoUpdateDone = new Set()
|
||||||
|
|
||||||
|
@ -391,6 +398,16 @@ export class VideoModelBuilder {
|
||||||
this.liveDone.add(id)
|
this.liveDone.add(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private setSource (row: SQLRow, videoModel: VideoModel) {
|
||||||
|
const id = row['VideoSource.id']
|
||||||
|
if (!id || this.sourceDone.has(id)) return
|
||||||
|
|
||||||
|
const attributes = this.grab(row, this.tables.getVideoSourceAttributes(), 'VideoSource')
|
||||||
|
videoModel.VideoSource = new VideoSourceModel(attributes, this.buildOpts)
|
||||||
|
|
||||||
|
this.sourceDone.add(id)
|
||||||
|
}
|
||||||
|
|
||||||
private grab (row: SQLRow, attributes: string[], prefix: string) {
|
private grab (row: SQLRow, attributes: string[], prefix: string) {
|
||||||
const result: { [ id: string ]: string | number } = {}
|
const result: { [ id: string ]: string | number } = {}
|
||||||
|
|
||||||
|
|
|
@ -168,6 +168,21 @@ export class VideoTableAttributes {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getVideoSourceAttributes () {
|
||||||
|
return [
|
||||||
|
'id',
|
||||||
|
'inputFilename',
|
||||||
|
'keptOriginalFilename',
|
||||||
|
'resolution',
|
||||||
|
'size',
|
||||||
|
'width',
|
||||||
|
'height',
|
||||||
|
'fps',
|
||||||
|
'metadata',
|
||||||
|
'createdAt'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
getTrackerAttributes () {
|
getTrackerAttributes () {
|
||||||
return [ 'id', 'url' ]
|
return [ 'id', 'url' ]
|
||||||
}
|
}
|
||||||
|
|
|
@ -702,9 +702,11 @@ export class VideosIdListQueryBuilder extends AbstractRunQuery {
|
||||||
'(SELECT COALESCE(SUM(size), 0) FROM "videoFile" WHERE "videoFile"."videoId" = "video"."id")' +
|
'(SELECT COALESCE(SUM(size), 0) FROM "videoFile" WHERE "videoFile"."videoId" = "video"."id")' +
|
||||||
' + ' +
|
' + ' +
|
||||||
'(SELECT COALESCE(SUM(size), 0) FROM "videoFile" ' +
|
'(SELECT COALESCE(SUM(size), 0) FROM "videoFile" ' +
|
||||||
'INNER JOIN "videoStreamingPlaylist" ON "videoStreamingPlaylist"."id" = "videoFile"."videoStreamingPlaylistId" ' +
|
'INNER JOIN "videoStreamingPlaylist" ON "videoStreamingPlaylist"."id" = "videoFile"."videoStreamingPlaylistId" ' +
|
||||||
'AND "videoStreamingPlaylist"."videoId" = "video"."id"' +
|
'AND "videoStreamingPlaylist"."videoId" = "video"."id"' +
|
||||||
')' +
|
')' +
|
||||||
|
' + ' +
|
||||||
|
'(SELECT COALESCE(SUM(size), 0) FROM "videoSource" WHERE "videoSource"."videoId" = "video"."id")' +
|
||||||
') END' +
|
') END' +
|
||||||
') AS "localVideoFilesSize"'
|
') AS "localVideoFilesSize"'
|
||||||
)
|
)
|
||||||
|
|
|
@ -96,6 +96,10 @@ export class VideosModelListQueryBuilder extends AbstractVideoQueryBuilder {
|
||||||
this.includeBlockedOwnerAndServer(options.serverAccountIdForBlock, options.user)
|
this.includeBlockedOwnerAndServer(options.serverAccountIdForBlock, options.user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.include & VideoInclude.SOURCE) {
|
||||||
|
this.includeVideoSource()
|
||||||
|
}
|
||||||
|
|
||||||
const select = this.buildSelect()
|
const select = this.buildSelect()
|
||||||
|
|
||||||
this.query = `${select} FROM (${this.innerQuery}) AS "tmp" ${this.joins} ${this.innerSort}`
|
this.query = `${select} FROM (${this.innerQuery}) AS "tmp" ${this.joins} ${this.innerSort}`
|
||||||
|
|
|
@ -117,7 +117,9 @@ export class VideoSourceModel extends SequelizeModel<VideoSourceModel> {
|
||||||
|
|
||||||
resolution: {
|
resolution: {
|
||||||
id: this.resolution,
|
id: this.resolution,
|
||||||
label: getResolutionLabel(this.resolution)
|
label: this.resolution !== null
|
||||||
|
? getResolutionLabel(this.resolution)
|
||||||
|
: null
|
||||||
},
|
},
|
||||||
size: this.size,
|
size: this.size,
|
||||||
|
|
||||||
|
|
|
@ -3558,6 +3558,24 @@ paths:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/VideoSource'
|
$ref: '#/components/schemas/VideoSource'
|
||||||
|
|
||||||
|
'/api/v1/videos/{id}/source/file':
|
||||||
|
delete:
|
||||||
|
summary: Delete video source file
|
||||||
|
operationId: deleteVideoSourceFile
|
||||||
|
tags:
|
||||||
|
- Video
|
||||||
|
security:
|
||||||
|
- OAuth2:
|
||||||
|
- admin
|
||||||
|
- moderator
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/idOrUUID'
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
description: successful operation
|
||||||
|
'404':
|
||||||
|
description: video source not found
|
||||||
|
|
||||||
'/api/v1/videos/{id}/source/replace-resumable':
|
'/api/v1/videos/{id}/source/replace-resumable':
|
||||||
post:
|
post:
|
||||||
summary: Initialize the resumable replacement of a video
|
summary: Initialize the resumable replacement of a video
|
||||||
|
@ -6988,6 +7006,8 @@ components:
|
||||||
- 2
|
- 2
|
||||||
- 4
|
- 4
|
||||||
- 8
|
- 8
|
||||||
|
- 16
|
||||||
|
- 32
|
||||||
description: >
|
description: >
|
||||||
**PeerTube >= 4.0** Include additional videos in results (can be combined using bitwise or operator)
|
**PeerTube >= 4.0** Include additional videos in results (can be combined using bitwise or operator)
|
||||||
|
|
||||||
|
@ -7000,6 +7020,10 @@ components:
|
||||||
- `4` BLOCKED_OWNER
|
- `4` BLOCKED_OWNER
|
||||||
|
|
||||||
- `8` FILES
|
- `8` FILES
|
||||||
|
|
||||||
|
- `16` CAPTIONS
|
||||||
|
|
||||||
|
- `32` VIDEO SOURCE
|
||||||
subscriptionsUris:
|
subscriptionsUris:
|
||||||
name: uris
|
name: uris
|
||||||
in: query
|
in: query
|
||||||
|
|
Loading…
Reference in New Issue