parent
058ef6912c
commit
a159b8b517
|
@ -112,7 +112,8 @@ export class VideoAdminService {
|
|||
let include = VideoInclude.BLACKLISTED |
|
||||
VideoInclude.BLOCKED_OWNER |
|
||||
VideoInclude.NOT_PUBLISHED_STATE |
|
||||
VideoInclude.FILES
|
||||
VideoInclude.FILES |
|
||||
VideoInclude.SOURCE
|
||||
|
||||
let privacyOneOf = getAllPrivacies()
|
||||
|
||||
|
|
|
@ -70,14 +70,17 @@
|
|||
</td>
|
||||
|
||||
<td>
|
||||
<span class="pt-badge badge-blue" *ngIf="video.isLocal">Local</span>
|
||||
<span class="pt-badge badge-purple" *ngIf="!video.isLocal">Remote</span>
|
||||
@if (video.isLocal) {
|
||||
<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 *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="isServerBlocked(video)" class="pt-badge badge-red" i18n>Server muted</span>
|
||||
|
@ -86,10 +89,11 @@
|
|||
</td>
|
||||
|
||||
<td>
|
||||
<span *ngIf="hasHLS(video)" class="pt-badge badge-blue">HLS</span>
|
||||
<span *ngIf="hasWebVideos(video)" class="pt-badge badge-blue">Web Videos ({{ video.files.length }})</span>
|
||||
<span i18n *ngIf="video.isLive" class="pt-badge badge-blue">Live</span>
|
||||
<span i18n *ngIf="hasObjectStorage(video)" class="pt-badge badge-purple">Object storage</span>
|
||||
<span *ngIf="hasOriginalFile(video)" class="pt-badge badge-blue" i18n>Original file</span>
|
||||
<span *ngIf="hasHLS(video)" class="pt-badge badge-blue" i18n>HLS</span>
|
||||
<span *ngIf="hasWebVideos(video)" class="pt-badge badge-blue" i18n>Web Videos ({{ video.files.length }})</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>
|
||||
</td>
|
||||
|
@ -105,8 +109,26 @@
|
|||
<tr>
|
||||
<td class="video-info expand-cell" myAutoColspan>
|
||||
<div>
|
||||
<div *ngIf="hasWebVideos(video)">
|
||||
Web Videos:
|
||||
<div class="me-3" *ngIf="hasOriginalFile(video)">
|
||||
<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>
|
||||
<li *ngFor="let file of video.files">
|
||||
|
@ -124,7 +146,7 @@
|
|||
</div>
|
||||
|
||||
<div *ngIf="hasHLS(video)">
|
||||
HLS:
|
||||
<ng-container i18n>HLS:</ng-container>
|
||||
|
||||
<ul>
|
||||
<li *ngFor="let file of video.streamingPlaylists[0].files">
|
||||
|
|
|
@ -1,32 +1,32 @@
|
|||
import { SortMeta, SharedModule } from 'primeng/api'
|
||||
import { finalize } from 'rxjs/operators'
|
||||
import { DatePipe, NgClass, NgFor, NgIf } from '@angular/common'
|
||||
import { Component, OnInit, ViewChild } from '@angular/core'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { AuthService, ConfirmService, Notifier, RestPagination, RestTable } from '@app/core'
|
||||
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 { UserRight, VideoFile, VideoPrivacy, VideoState, VideoStreamingPlaylistType } from '@peertube/peertube-models'
|
||||
import { VideoAdminService } from './video-admin.service'
|
||||
import { BytesPipe } from '../../../shared/shared-main/angular/bytes.pipe'
|
||||
import { EmbedComponent } from '../../../shared/shared-main/video/embed.component'
|
||||
import { SharedModule, SortMeta } from 'primeng/api'
|
||||
import { TableModule } from 'primeng/table'
|
||||
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 { 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 {
|
||||
VideoActionsDisplayType,
|
||||
VideoActionsDropdownComponent
|
||||
} from '../../../shared/shared-video-miniature/video-actions-dropdown.component'
|
||||
import { TableExpanderIconComponent } from '../../../shared/shared-tables/table-expander-icon.component'
|
||||
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'
|
||||
import { VideoAdminService } from './video-admin.service'
|
||||
|
||||
@Component({
|
||||
selector: 'my-video-list',
|
||||
|
@ -187,6 +187,10 @@ export class VideoListComponent extends RestTable <Video> implements OnInit {
|
|||
return video.state.id === VideoState.TO_IMPORT
|
||||
}
|
||||
|
||||
hasOriginalFile (video: Video) {
|
||||
return !!video.videoSource?.fileDownloadUrl
|
||||
}
|
||||
|
||||
hasHLS (video: Video) {
|
||||
const p = video.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
|
||||
if (!p) return false
|
||||
|
@ -211,13 +215,10 @@ export class VideoListComponent extends RestTable <Video> implements OnInit {
|
|||
}
|
||||
|
||||
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)) {
|
||||
files = files.concat(video.streamingPlaylists[0].files)
|
||||
}
|
||||
|
||||
return files.reduce((p, f) => p += f.size, 0)
|
||||
return total
|
||||
}
|
||||
|
||||
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 () {
|
||||
this.loading = true
|
||||
|
||||
|
|
|
@ -16,7 +16,8 @@ import {
|
|||
VideoState,
|
||||
VideoStateType,
|
||||
VideoStreamingPlaylist,
|
||||
VideoStreamingPlaylistType
|
||||
VideoStreamingPlaylistType,
|
||||
VideoSource
|
||||
} from '@peertube/peertube-models'
|
||||
|
||||
export class Video implements VideoServerModel {
|
||||
|
@ -111,6 +112,8 @@ export class Video implements VideoServerModel {
|
|||
streamingPlaylists?: VideoStreamingPlaylist[]
|
||||
files?: VideoFile[]
|
||||
|
||||
videoSource?: VideoSource
|
||||
|
||||
static buildWatchUrl (video: Partial<Pick<Video, 'uuid' | 'shortUUID'>>) {
|
||||
return buildVideoWatchPath({ shortUUID: video.shortUUID || video.uuid })
|
||||
}
|
||||
|
@ -192,6 +195,7 @@ export class Video implements VideoServerModel {
|
|||
|
||||
this.streamingPlaylists = hash.streamingPlaylists
|
||||
this.files = hash.files
|
||||
this.videoSource = hash.videoSource
|
||||
|
||||
this.userHistory = hash.userHistory
|
||||
|
||||
|
|
|
@ -326,6 +326,11 @@ export class VideoService {
|
|||
.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: {
|
||||
videoIds: (number | string)[]
|
||||
type: 'hls' | 'web-video'
|
||||
|
|
|
@ -4,7 +4,8 @@ export const VideoInclude = {
|
|||
BLACKLISTED: 1 << 1,
|
||||
BLOCKED_OWNER: 1 << 2,
|
||||
FILES: 1 << 3,
|
||||
CAPTIONS: 1 << 4
|
||||
CAPTIONS: 1 << 4,
|
||||
SOURCE: 1 << 5
|
||||
} as const
|
||||
|
||||
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 { VideoPrivacyType } from './video-privacy.enum.js'
|
||||
import { VideoScheduleUpdate } from './video-schedule-update.model.js'
|
||||
import { VideoSource } from './video-source.model.js'
|
||||
import { VideoStateType } from './video-state.enum.js'
|
||||
import { VideoStreamingPlaylist } from './video-streaming-playlist.model.js'
|
||||
|
||||
|
@ -75,6 +76,8 @@ export interface VideoAdditionalAttributes {
|
|||
|
||||
files: VideoFile[]
|
||||
streamingPlaylists: VideoStreamingPlaylist[]
|
||||
|
||||
videoSource: VideoSource
|
||||
}
|
||||
|
||||
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 & {
|
||||
uuid: number | string
|
||||
}) {
|
||||
|
@ -273,12 +287,12 @@ export class VideosCommand extends AbstractCommand {
|
|||
const privacyOneOf = getAllPrivacies()
|
||||
|
||||
return this.list({
|
||||
...options,
|
||||
|
||||
include,
|
||||
nsfw,
|
||||
privacyOneOf,
|
||||
|
||||
...options,
|
||||
|
||||
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 () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
|
|
|
@ -48,7 +48,8 @@ describe('Test video filters validators', function () {
|
|||
const validIncludes = [
|
||||
VideoInclude.NONE,
|
||||
VideoInclude.BLOCKED_OWNER,
|
||||
VideoInclude.NOT_PUBLISHED_STATE | VideoInclude.BLACKLISTED
|
||||
VideoInclude.NOT_PUBLISHED_STATE | VideoInclude.BLACKLISTED,
|
||||
VideoInclude.SOURCE
|
||||
]
|
||||
|
||||
async function testEndpoints (options: {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
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 {
|
||||
ObjectStorageCommand,
|
||||
|
@ -104,6 +104,20 @@ describe('Test video source management', function () {
|
|||
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 () {
|
||||
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')
|
||||
})
|
||||
|
||||
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 () {
|
||||
this.timeout(60000)
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 { CreateJobArgument, CreateJobOptions, JobQueue } from '@server/lib/job-queue/index.js'
|
||||
import { Hooks } from '@server/lib/plugins/hooks.js'
|
||||
|
@ -19,6 +19,7 @@ import { logger, loggerTagsFactory } from '../../../helpers/logger.js'
|
|||
import {
|
||||
asyncMiddleware,
|
||||
authenticate,
|
||||
ensureUserHasRight,
|
||||
replaceVideoSourceResumableInitValidator,
|
||||
replaceVideoSourceResumableValidator,
|
||||
videoSourceGetLatestValidator
|
||||
|
@ -35,6 +36,14 @@ videoSourceRouter.get('/:id/source',
|
|||
getVideoLatestSource
|
||||
)
|
||||
|
||||
videoSourceRouter.delete('/:id/source/file',
|
||||
openapiOperationDoc({ operationId: 'deleteVideoSourceFile' }),
|
||||
authenticate,
|
||||
ensureUserHasRight(UserRight.MANAGE_VIDEO_FILES),
|
||||
asyncMiddleware(videoSourceGetLatestValidator),
|
||||
asyncMiddleware(deleteVideoLatestSourceFile)
|
||||
)
|
||||
|
||||
setupUploadResumableRoutes({
|
||||
routePath: '/:id/source/replace-resumable',
|
||||
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) {
|
||||
return res.json(res.locals.videoSource.toFormattedJSON())
|
||||
}
|
||||
|
|
|
@ -472,7 +472,7 @@ const commonVideosFiltersValidator = [
|
|||
if (req.query.include || req.query.privacyOneOf) {
|
||||
return res.fail({
|
||||
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
|
||||
blacklistInfo?: boolean
|
||||
files?: boolean
|
||||
source?: boolean
|
||||
blockedOwner?: boolean
|
||||
}
|
||||
}
|
||||
|
@ -41,6 +42,7 @@ export function guessAdditionalAttributesFromQuery (query: VideosCommonQueryAfte
|
|||
scheduledUpdate: !!(query.include & VideoInclude.NOT_PUBLISHED_STATE),
|
||||
blacklistInfo: !!(query.include & VideoInclude.BLACKLISTED),
|
||||
files: !!(query.include & VideoInclude.FILES),
|
||||
source: !!(query.include & VideoInclude.SOURCE),
|
||||
blockedOwner: !!(query.include & VideoInclude.BLOCKED_OWNER)
|
||||
}
|
||||
}
|
||||
|
@ -310,5 +312,9 @@ function buildAdditionalAttributes (video: MVideoFormattable, options: VideoForm
|
|||
result.files = videoFilesModelToFormattedJSON(video, video.VideoFiles)
|
||||
}
|
||||
|
||||
if (add?.source === true) {
|
||||
result.videoSource = video.VideoSource?.toFormattedJSON() || null
|
||||
}
|
||||
|
||||
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 () {
|
||||
this.addJoin(
|
||||
'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 { TrackerModel } from '@server/models/server/tracker.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 { TagModel } from '../../../tag.js'
|
||||
import { ThumbnailModel } from '../../../thumbnail.js'
|
||||
|
@ -39,6 +40,7 @@ export class VideoModelBuilder {
|
|||
private accountBlocklistDone: Set<any>
|
||||
private serverBlocklistDone: Set<any>
|
||||
private liveDone: Set<any>
|
||||
private sourceDone: Set<any>
|
||||
private redundancyDone: Set<any>
|
||||
private scheduleVideoUpdateDone: Set<any>
|
||||
|
||||
|
@ -108,6 +110,10 @@ export class VideoModelBuilder {
|
|||
this.setBlockedOwner(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.blacklistDone = new Set()
|
||||
this.liveDone = new Set()
|
||||
this.sourceDone = new Set()
|
||||
this.redundancyDone = new Set()
|
||||
this.scheduleVideoUpdateDone = new Set()
|
||||
|
||||
|
@ -391,6 +398,16 @@ export class VideoModelBuilder {
|
|||
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) {
|
||||
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 () {
|
||||
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" ' +
|
||||
'INNER JOIN "videoStreamingPlaylist" ON "videoStreamingPlaylist"."id" = "videoFile"."videoStreamingPlaylistId" ' +
|
||||
'AND "videoStreamingPlaylist"."videoId" = "video"."id"' +
|
||||
'INNER JOIN "videoStreamingPlaylist" ON "videoStreamingPlaylist"."id" = "videoFile"."videoStreamingPlaylistId" ' +
|
||||
'AND "videoStreamingPlaylist"."videoId" = "video"."id"' +
|
||||
')' +
|
||||
' + ' +
|
||||
'(SELECT COALESCE(SUM(size), 0) FROM "videoSource" WHERE "videoSource"."videoId" = "video"."id")' +
|
||||
') END' +
|
||||
') AS "localVideoFilesSize"'
|
||||
)
|
||||
|
|
|
@ -96,6 +96,10 @@ export class VideosModelListQueryBuilder extends AbstractVideoQueryBuilder {
|
|||
this.includeBlockedOwnerAndServer(options.serverAccountIdForBlock, options.user)
|
||||
}
|
||||
|
||||
if (options.include & VideoInclude.SOURCE) {
|
||||
this.includeVideoSource()
|
||||
}
|
||||
|
||||
const select = this.buildSelect()
|
||||
|
||||
this.query = `${select} FROM (${this.innerQuery}) AS "tmp" ${this.joins} ${this.innerSort}`
|
||||
|
|
|
@ -117,7 +117,9 @@ export class VideoSourceModel extends SequelizeModel<VideoSourceModel> {
|
|||
|
||||
resolution: {
|
||||
id: this.resolution,
|
||||
label: getResolutionLabel(this.resolution)
|
||||
label: this.resolution !== null
|
||||
? getResolutionLabel(this.resolution)
|
||||
: null
|
||||
},
|
||||
size: this.size,
|
||||
|
||||
|
|
|
@ -3558,6 +3558,24 @@ paths:
|
|||
schema:
|
||||
$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':
|
||||
post:
|
||||
summary: Initialize the resumable replacement of a video
|
||||
|
@ -6988,6 +7006,8 @@ components:
|
|||
- 2
|
||||
- 4
|
||||
- 8
|
||||
- 16
|
||||
- 32
|
||||
description: >
|
||||
**PeerTube >= 4.0** Include additional videos in results (can be combined using bitwise or operator)
|
||||
|
||||
|
@ -7000,6 +7020,10 @@ components:
|
|||
- `4` BLOCKED_OWNER
|
||||
|
||||
- `8` FILES
|
||||
|
||||
- `16` CAPTIONS
|
||||
|
||||
- `32` VIDEO SOURCE
|
||||
subscriptionsUris:
|
||||
name: uris
|
||||
in: query
|
||||
|
|
Loading…
Reference in New Issue