Skip to content

Commit

Permalink
Add ability to list and delete original file
Browse files Browse the repository at this point in the history
In admin
  • Loading branch information
Chocobozzz committed Mar 26, 2024
1 parent 058ef69 commit a159b8b
Show file tree
Hide file tree
Showing 21 changed files with 294 additions and 44 deletions.
3 changes: 2 additions & 1 deletion client/src/app/+admin/overview/videos/video-admin.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
42 changes: 32 additions & 10 deletions client/src/app/+admin/overview/videos/video-list.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,17 @@ <h1>
</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>
Expand All @@ -86,10 +89,11 @@ <h1>
</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>
Expand All @@ -105,8 +109,26 @@ <h1>
<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">
Expand All @@ -124,7 +146,7 @@ <h1>
</div>

<div *ngIf="hasHLS(video)">
HLS:
<ng-container i18n>HLS:</ng-container>

<ul>
<li *ngFor="let file of video.streamingPlaylists[0].files">
Expand Down
61 changes: 39 additions & 22 deletions client/src/app/+admin/overview/videos/video-list.component.ts
Original file line number Diff line number Diff line change
@@ -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 { 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 { AutoColspanDirective } from '../../../shared/shared-main/angular/auto-colspan.directive'
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',
Expand Down Expand Up @@ -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
Expand All @@ -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') {
Expand All @@ -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

Expand Down
6 changes: 5 additions & 1 deletion client/src/app/shared/shared-main/video/video.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import {
VideoState,
VideoStateType,
VideoStreamingPlaylist,
VideoStreamingPlaylistType
VideoStreamingPlaylistType,
VideoSource
} from '@peertube/peertube-models'

export class Video implements VideoServerModel {
Expand Down Expand Up @@ -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 })
}
Expand Down Expand Up @@ -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

Expand Down
5 changes: 5 additions & 0 deletions client/src/app/shared/shared-main/video/video.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
3 changes: 2 additions & 1 deletion packages/models/src/videos/video-include.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]
3 changes: 3 additions & 0 deletions packages/models/src/videos/video.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -75,6 +76,8 @@ export interface VideoAdditionalAttributes {

files: VideoFile[]
streamingPlaylists: VideoStreamingPlaylist[]

videoSource: VideoSource
}

export interface VideoDetails extends Video {
Expand Down
18 changes: 16 additions & 2 deletions packages/server-commands/src/videos/videos-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}) {
Expand Down Expand Up @@ -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 })
})
}
Expand Down
32 changes: 32 additions & 0 deletions packages/tests/src/api/check-params/video-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ])
})
Expand Down
3 changes: 2 additions & 1 deletion packages/tests/src/api/check-params/videos-common-filters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
43 changes: 42 additions & 1 deletion packages/tests/src/api/videos/video-source.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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] })
})
Expand Down Expand Up @@ -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)

Expand Down
Loading

0 comments on commit a159b8b

Please sign in to comment.