1
0
Fork 0

Add video file size info in admin videos list

This commit is contained in:
Chocobozzz 2021-10-29 10:54:27 +02:00 committed by Chocobozzz
parent 2760b454a7
commit 3c10840fa9
26 changed files with 161 additions and 65 deletions

View file

@ -37,6 +37,7 @@
<th style="width: 60px;"></th>
<th i18n>Video</th>
<th i18n>Info</th>
<th i18n>Files</th>
<th style="width: 150px;" i18n pSortableColumn="publishedAt">Published <p-sortIcon field="publishedAt"></p-sortIcon></th>
</tr>
</ng-template>
@ -63,8 +64,8 @@
<my-video-cell [video]="video"></my-video-cell>
</td>
<td class="badges">
<span [ngClass]="getPrivacyBadgeClass(video.privacy.id)" class="badge" i18n>{{ video.privacy.label }}</span>
<td>
<span [ngClass]="getPrivacyBadgeClass(video.privacy.id)" class="badge">{{ video.privacy.label }}</span>
<span *ngIf="video.nsfw" class="badge badge-red" i18n>NSFW</span>
@ -76,6 +77,13 @@
<span *ngIf="isVideoBlocked(video)" class="badge badge-red" i18n>Blocked</span>
</td>
<td>
<span *ngIf="isHLS(video)" class="badge badge-blue">HLS</span>
<span *ngIf="isWebTorrent(video)" class="badge badge-blue">WebTorrent</span>
<span *ngIf="!video.remote">{{ getFilesSize(video) | bytes: 1 }}</span>
</td>
<td>
{{ video.publishedAt | date: 'short' }}
</td>
@ -85,8 +93,30 @@
<ng-template pTemplate="rowexpansion" let-video>
<tr>
<td colspan="50">
<my-embed [video]="video"></my-embed>
<td class="video-info expand-cell" colspan="7">
<div>
<div *ngIf="isWebTorrent(video)">
WebTorrent:
<ul>
<li *ngFor="let file of video.files">
{{ file.resolution.label }}: {{ file.size | bytes: 1 }}
</li>
</ul>
</div>
<div *ngIf="isHLS(video)">
HLS:
<ul>
<li *ngFor="let file of video.streamingPlaylists[0].files">
{{ file.resolution.label }}: {{ file.size | bytes: 1 }}
</li>
</ul>
</div>
<my-embed class="ml-auto" [video]="video"></my-embed>
</div>
</td>
</tr>
</ng-template>

View file

@ -3,6 +3,7 @@
my-embed {
display: block;
max-width: 500px;
width: 50%;
}
.badge {
@ -10,3 +11,7 @@ my-embed {
margin-right: 5px;
}
.video-info > div {
display: flex;
}

View file

@ -3,7 +3,7 @@ import { Component, OnInit } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
import { AuthService, ConfirmService, Notifier, RestPagination, RestTable } from '@app/core'
import { DropdownAction, Video, VideoService } from '@app/shared/shared-main'
import { UserRight, VideoPrivacy, VideoState } from '@shared/models'
import { UserRight, VideoPrivacy, VideoState, VideoStreamingPlaylistType } from '@shared/models'
import { AdvancedInputFilter } from '@app/shared/shared-forms'
import { VideoActionsDisplayType } from '@app/shared/shared-video-miniature'
@ -114,6 +114,24 @@ export class VideoListComponent extends RestTable implements OnInit {
return video.blacklisted
}
isHLS (video: Video) {
return video.streamingPlaylists.some(p => p.type === VideoStreamingPlaylistType.HLS)
}
isWebTorrent (video: Video) {
return video.files.length !== 0
}
getFilesSize (video: Video) {
let files = video.files
if (this.isHLS(video)) {
files = files.concat(video.streamingPlaylists[0].files)
}
return files.reduce((p, f) => p += f.size, 0)
}
protected reloadData () {
this.selectedVideos = []

View file

@ -15,7 +15,6 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
support: string
channel: VideoChannel
tags: string[]
files: VideoFile[]
account: Account
commentsEnabled: boolean
downloadEnabled: boolean
@ -28,13 +27,13 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
trackerUrls: string[]
files: VideoFile[]
streamingPlaylists: VideoStreamingPlaylist[]
constructor (hash: VideoDetailsServerModel, translations = {}) {
super(hash, translations)
this.descriptionPath = hash.descriptionPath
this.files = hash.files
this.channel = new VideoChannel(hash.channel)
this.account = new Account(hash.account)
this.tags = hash.tags
@ -43,7 +42,6 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
this.downloadEnabled = hash.downloadEnabled
this.trackerUrls = hash.trackerUrls
this.streamingPlaylists = hash.streamingPlaylists
this.buildLikeAndDislikePercents()
}

View file

@ -10,9 +10,11 @@ import {
UserRight,
Video as VideoServerModel,
VideoConstant,
VideoFile,
VideoPrivacy,
VideoScheduleUpdate,
VideoState
VideoState,
VideoStreamingPlaylist
} from '@shared/models'
export class Video implements VideoServerModel {
@ -96,6 +98,9 @@ export class Video implements VideoServerModel {
pluginData?: any
streamingPlaylists?: VideoStreamingPlaylist[]
files?: VideoFile[]
static buildWatchUrl (video: Partial<Pick<Video, 'uuid' | 'shortUUID'>>) {
return buildVideoWatchPath({ shortUUID: video.shortUUID || video.uuid })
}
@ -172,6 +177,9 @@ export class Video implements VideoServerModel {
this.blockedOwner = hash.blockedOwner
this.blockedServer = hash.blockedServer
this.streamingPlaylists = hash.streamingPlaylists
this.files = hash.files
this.userHistory = hash.userHistory
this.originInstanceHost = this.account.host

View file

@ -208,7 +208,11 @@ export class VideoService {
): Observable<ResultList<Video>> {
const { pagination, search } = parameters
const include = VideoInclude.BLACKLISTED | VideoInclude.BLOCKED_OWNER | VideoInclude.HIDDEN_PRIVACY | VideoInclude.NOT_PUBLISHED_STATE
const include = VideoInclude.BLACKLISTED |
VideoInclude.BLOCKED_OWNER |
VideoInclude.HIDDEN_PRIVACY |
VideoInclude.NOT_PUBLISHED_STATE |
VideoInclude.FILES
let params = new HttpParams()
params = this.buildCommonVideosParams({ params, include, ...parameters })

View file

@ -189,7 +189,6 @@ async function listAccountVideos (req: express.Request, res: express.Response) {
displayOnlyForFollower,
nsfw: buildNSFWFilter(res, query.nsfw),
withFiles: false,
accountId: account.id,
user: res.locals.oauth ? res.locals.oauth.token.User : undefined,
countVideos

View file

@ -122,7 +122,6 @@ async function getVideos (
},
nsfw: buildNSFWFilter(res),
user: res.locals.oauth ? res.locals.oauth.token.User : undefined,
withFiles: false,
countVideos: false,
...where

View file

@ -181,7 +181,6 @@ async function getUserSubscriptionVideos (req: express.Request, res: express.Res
orLocalVideos: false
},
nsfw: buildNSFWFilter(res, query.nsfw),
withFiles: false,
user,
countVideos
})

View file

@ -347,7 +347,6 @@ async function listVideoChannelVideos (req: express.Request, res: express.Respon
displayOnlyForFollower,
nsfw: buildNSFWFilter(res, query.nsfw),
withFiles: false,
videoChannelId: videoChannelInstance.id,
user: res.locals.oauth ? res.locals.oauth.token.User : undefined,
countVideos

View file

@ -225,7 +225,6 @@ async function listVideos (req: express.Request, res: express.Response) {
orLocalVideos: true
},
nsfw: buildNSFWFilter(res, query.nsfw),
withFiles: false,
user: res.locals.oauth ? res.locals.oauth.token.User : undefined,
countVideos
}, 'filter:api.videos.list.params')

View file

@ -76,7 +76,6 @@ async function getSitemapLocalVideoUrls () {
},
isLocal: true,
nsfw: buildNSFWFilter(),
withFiles: false,
countVideos: false
})

View file

@ -2,6 +2,7 @@ import express from 'express'
import Feed from 'pfeed'
import { getServerActor } from '@server/models/application/application'
import { getCategoryLabel } from '@server/models/video/formatter/video-format-utils'
import { VideoInclude } from '@shared/models'
import { buildNSFWFilter } from '../helpers/express-utils'
import { CONFIG } from '../initializers/config'
import { FEEDS, PREVIEWS_SIZE, ROUTE_CACHE_LIFETIME, WEBSERVER } from '../initializers/constants'
@ -171,8 +172,8 @@ async function generateVideoFeed (req: express.Request, res: express.Response) {
},
nsfw,
isLocal: req.query.isLocal,
include: req.query.include,
withFiles: true,
include: req.query.include | VideoInclude.FILES,
hasFiles: true,
countVideos: false,
...options
})
@ -204,9 +205,10 @@ async function generateVideoFeedForSubscriptions (req: express.Request, res: exp
nsfw,
isLocal: req.query.isLocal,
include: req.query.include,
withFiles: true,
hasFiles: true,
include: req.query.include | VideoInclude.FILES,
countVideos: false,
displayOnlyForFollower: {

View file

@ -99,7 +99,7 @@ export type SummaryOptions = {
queryInclude.push({
attributes: [ 'id' ],
model: AccountBlocklistModel.unscoped(),
as: 'BlockedAccounts',
as: 'BlockedBy',
required: false,
where: {
accountId: {

View file

@ -70,7 +70,6 @@ export class UserVideoHistoryModel extends Model<Partial<AttributesOnly<UserVide
actorId: serverActor.id,
orLocalVideos: true
},
withFiles: false,
user,
historyOfUser: user
})

View file

@ -42,6 +42,7 @@ export type VideoFormattingJSONOptions = {
waitTranscoding?: boolean
scheduledUpdate?: boolean
blacklistInfo?: boolean
files?: boolean
blockedOwner?: boolean
}
}
@ -55,6 +56,7 @@ function guessAdditionalAttributesFromQuery (query: VideosCommonQueryAfterSaniti
waitTranscoding: !!(query.include & VideoInclude.NOT_PUBLISHED_STATE),
scheduledUpdate: !!(query.include & VideoInclude.NOT_PUBLISHED_STATE),
blacklistInfo: !!(query.include & VideoInclude.BLACKLISTED),
files: !!(query.include & VideoInclude.FILES),
blockedOwner: !!(query.include & VideoInclude.BLOCKED_OWNER)
}
}
@ -150,22 +152,26 @@ function videoModelToFormattedJSON (video: MVideoFormattable, options: VideoForm
videoObject.blockedServer = !!(server?.isBlocked())
}
if (add?.files === true) {
videoObject.streamingPlaylists = streamingPlaylistsModelToFormattedJSON(video, video.VideoStreamingPlaylists)
videoObject.files = videoFilesModelToFormattedJSON(video, video.VideoFiles)
}
return videoObject
}
function videoModelToFormattedDetailsJSON (video: MVideoFormattableDetails): VideoDetails {
const formattedJson = video.toFormattedJSON({
const videoJSON = video.toFormattedJSON({
additionalAttributes: {
scheduledUpdate: true,
blacklistInfo: true
blacklistInfo: true,
files: true
}
})
}) as Video & Required<Pick<Video, 'files' | 'streamingPlaylists'>>
const tags = video.Tags ? video.Tags.map(t => t.name) : []
const streamingPlaylists = streamingPlaylistsModelToFormattedJSON(video, video.VideoStreamingPlaylists)
const detailsJson = {
const detailsJSON = {
support: video.support,
descriptionPath: video.getDescriptionAPIPath(),
channel: video.VideoChannel.toFormattedJSON(),
@ -179,20 +185,14 @@ function videoModelToFormattedDetailsJSON (video: MVideoFormattableDetails): Vid
label: getStateLabel(video.state)
},
trackerUrls: video.getTrackerUrls(),
files: [],
streamingPlaylists
trackerUrls: video.getTrackerUrls()
}
// Format and sort video files
detailsJson.files = videoFilesModelToFormattedJSON(video, video.VideoFiles)
return Object.assign(formattedJson, detailsJson)
return Object.assign(videoJSON, detailsJSON)
}
function streamingPlaylistsModelToFormattedJSON (
video: MVideoFormattableDetails,
video: MVideoFormattable,
playlists: MStreamingPlaylistRedundanciesOpt[]
): VideoStreamingPlaylist[] {
if (isArray(playlists) === false) return []
@ -223,7 +223,7 @@ function sortByResolutionDesc (fileA: MVideoFile, fileB: MVideoFile) {
}
function videoFilesModelToFormattedJSON (
video: MVideoFormattableDetails,
video: MVideoFormattable,
videoFiles: MVideoFileRedundanciesOpt[],
includeMagnet = true
): VideoFile[] {

View file

@ -32,7 +32,7 @@ export type BuildVideoGetQueryOptions = {
logging?: boolean
}
export class VideosModelGetQueryBuilder {
export class VideoModelGetQueryBuilder {
videoQueryBuilder: VideosModelGetQuerySubBuilder
webtorrentFilesQueryBuilder: VideoFileQueryBuilder
streamingPlaylistFilesQueryBuilder: VideoFileQueryBuilder
@ -53,11 +53,11 @@ export class VideosModelGetQueryBuilder {
const [ videoRows, webtorrentFilesRows, streamingPlaylistFilesRows ] = await Promise.all([
this.videoQueryBuilder.queryVideos(options),
VideosModelGetQueryBuilder.videoFilesInclude.has(options.type)
VideoModelGetQueryBuilder.videoFilesInclude.has(options.type)
? this.webtorrentFilesQueryBuilder.queryWebTorrentVideos(options)
: Promise.resolve(undefined),
VideosModelGetQueryBuilder.videoFilesInclude.has(options.type)
VideoModelGetQueryBuilder.videoFilesInclude.has(options.type)
? this.streamingPlaylistFilesQueryBuilder.queryStreamingPlaylistVideos(options)
: Promise.resolve(undefined)
])

View file

@ -43,7 +43,7 @@ export type BuildVideosListQueryOptions = {
uuids?: string[]
withFiles?: boolean
hasFiles?: boolean
accountId?: number
videoChannelId?: number
@ -165,7 +165,7 @@ export class VideosIdListQueryBuilder extends AbstractVideosQueryBuilder {
this.whereFollowerActorId(options.displayOnlyForFollower)
}
if (options.withFiles === true) {
if (options.hasFiles === true) {
this.whereFileExists()
}

View file

@ -52,7 +52,7 @@ export class VideosModelListQueryBuilder extends AbstractVideosModelQueryBuilder
this.includeAccounts()
this.includeThumbnails()
if (options.withFiles) {
if (options.include & VideoInclude.FILES) {
this.includeWebtorrentFiles()
this.includeStreamingPlaylistFiles()
}

View file

@ -105,7 +105,7 @@ import {
videoModelToFormattedJSON
} from './formatter/video-format-utils'
import { ScheduleVideoUpdateModel } from './schedule-video-update'
import { VideosModelGetQueryBuilder } from './sql/video-model-get-query-builder'
import { VideoModelGetQueryBuilder } from './sql/video-model-get-query-builder'
import { BuildVideosListQueryOptions, DisplayOnlyForFollowerOptions, VideosIdListQueryBuilder } from './sql/videos-id-list-query-builder'
import { VideosModelListQueryBuilder } from './sql/videos-model-list-query-builder'
import { TagModel } from './tag'
@ -1029,7 +1029,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
isLocal?: boolean
include?: VideoInclude
withFiles: boolean
hasFiles?: boolean // default false
categoryOneOf?: number[]
licenceOneOf?: number[]
@ -1053,7 +1053,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
search?: string
}) {
if (options.include && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) {
if (VideoModel.isPrivateInclude(options.include) && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) {
throw new Error('Try to filter all-local but no user has not the see all videos right')
}
@ -1082,7 +1082,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
'isLocal',
'include',
'displayOnlyForFollower',
'withFiles',
'hasFiles',
'accountId',
'videoChannelId',
'videoPlaylistId',
@ -1229,13 +1229,13 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
}
static load (id: number | string, transaction?: Transaction): Promise<MVideoThumbnail> {
const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize)
const queryBuilder = new VideoModelGetQueryBuilder(VideoModel.sequelize)
return queryBuilder.queryVideo({ id, transaction, type: 'thumbnails' })
}
static loadWithBlacklist (id: number | string, transaction?: Transaction): Promise<MVideoThumbnailBlacklist> {
const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize)
const queryBuilder = new VideoModelGetQueryBuilder(VideoModel.sequelize)
return queryBuilder.queryVideo({ id, transaction, type: 'thumbnails-blacklist' })
}
@ -1279,31 +1279,31 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
}
static loadOnlyId (id: number | string, transaction?: Transaction): Promise<MVideoId> {
const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize)
const queryBuilder = new VideoModelGetQueryBuilder(VideoModel.sequelize)
return queryBuilder.queryVideo({ id, transaction, type: 'id' })
}
static loadWithFiles (id: number | string, transaction?: Transaction, logging?: boolean): Promise<MVideoWithAllFiles> {
const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize)
const queryBuilder = new VideoModelGetQueryBuilder(VideoModel.sequelize)
return queryBuilder.queryVideo({ id, transaction, type: 'all-files', logging })
}
static loadByUrl (url: string, transaction?: Transaction): Promise<MVideoThumbnail> {
const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize)
const queryBuilder = new VideoModelGetQueryBuilder(VideoModel.sequelize)
return queryBuilder.queryVideo({ url, transaction, type: 'thumbnails' })
}
static loadByUrlAndPopulateAccount (url: string, transaction?: Transaction): Promise<MVideoAccountLightBlacklistAllFiles> {
const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize)
const queryBuilder = new VideoModelGetQueryBuilder(VideoModel.sequelize)
return queryBuilder.queryVideo({ url, transaction, type: 'account-blacklist-files' })
}
static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Transaction, userId?: number): Promise<MVideoFullLight> {
const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize)
const queryBuilder = new VideoModelGetQueryBuilder(VideoModel.sequelize)
return queryBuilder.queryVideo({ id, transaction: t, type: 'full-light', userId })
}
@ -1314,7 +1314,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
userId?: number
}): Promise<MVideoDetails> {
const { id, transaction, userId } = parameters
const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize)
const queryBuilder = new VideoModelGetQueryBuilder(VideoModel.sequelize)
return queryBuilder.queryVideo({ id, transaction, type: 'api', userId })
}
@ -1345,8 +1345,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
displayOnlyForFollower: {
actorId: serverActor.id,
orLocalVideos: true
},
withFiles: false
}
})
return {
@ -1490,6 +1489,13 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
}
}
private static isPrivateInclude (include: VideoInclude) {
return include & VideoInclude.BLACKLISTED ||
include & VideoInclude.BLOCKED_OWNER ||
include & VideoInclude.HIDDEN_PRIVACY ||
include & VideoInclude.NOT_PUBLISHED_STATE
}
isBlacklisted () {
return !!this.VideoBlacklist
}

View file

@ -13,7 +13,7 @@ import {
setDefaultVideoChannel,
waitJobs
} from '@shared/extra-utils'
import { HttpStatusCode, UserRole, Video, VideoInclude, VideoPrivacy } from '@shared/models'
import { HttpStatusCode, UserRole, Video, VideoDetails, VideoInclude, VideoPrivacy } from '@shared/models'
describe('Test videos filter', function () {
let servers: PeerTubeServer[]
@ -365,6 +365,32 @@ describe('Test videos filter', function () {
await servers[0].blocklist.removeFromServerBlocklist({ server: servers[1].host })
})
it('Should include video files', async function () {
for (const path of paths) {
{
const videos = await listVideos({ server: servers[0], path })
for (const video of videos) {
const videoWithFiles = video as VideoDetails
expect(videoWithFiles.files).to.not.exist
expect(videoWithFiles.streamingPlaylists).to.not.exist
}
}
{
const videos = await listVideos({ server: servers[0], path, include: VideoInclude.FILES })
for (const video of videos) {
const videoWithFiles = video as VideoDetails
expect(videoWithFiles.files).to.exist
expect(videoWithFiles.files).to.have.length.at.least(1)
}
}
}
})
it('Should filter by tags and category', async function () {
await servers[0].videos.upload({ attributes: { name: 'tag filter', tags: [ 'tag1', 'tag2' ] } })
await servers[0].videos.upload({ attributes: { name: 'tag filter with category', tags: [ 'tag3' ], category: 4 } })

View file

@ -23,7 +23,7 @@ type Use<K extends keyof AccountModel, M> = PickWith<AccountModel, K, M>
export type MAccount =
Omit<AccountModel, 'Actor' | 'User' | 'Application' | 'VideoChannels' | 'VideoPlaylists' |
'VideoComments' | 'BlockedAccounts'>
'VideoComments' | 'BlockedBy'>
// ############################################################################
@ -84,7 +84,7 @@ export type MAccountSummary =
export type MAccountSummaryBlocks =
MAccountSummary &
Use<'BlockedByAccounts', MAccountBlocklistId[]>
Use<'BlockedBy', MAccountBlocklistId[]>
export type MAccountAPI =
MAccount &

View file

@ -15,7 +15,7 @@ export type MServerRedundancyAllowed = Pick<MServer, 'redundancyAllowed'>
export type MServerHostBlocks =
MServerHost &
Use<'BlockedByAccounts', MAccountBlocklistId[]>
Use<'BlockedBy', MAccountBlocklistId[]>
// ############################################################################

View file

@ -210,7 +210,9 @@ export type MVideoFormattable =
PickWithOpt<VideoModel, 'UserVideoHistories', MUserVideoHistoryTime[]> &
Use<'VideoChannel', MChannelAccountSummaryFormattable> &
PickWithOpt<VideoModel, 'ScheduleVideoUpdate', Pick<MScheduleVideoUpdate, 'updateAt' | 'privacy'>> &
PickWithOpt<VideoModel, 'VideoBlacklist', Pick<MVideoBlacklist, 'reason'>>
PickWithOpt<VideoModel, 'VideoBlacklist', Pick<MVideoBlacklist, 'reason'>> &
PickWithOpt<VideoModel, 'VideoStreamingPlaylists', MStreamingPlaylistFiles[]> &
PickWithOpt<VideoModel, 'VideoFiles', MVideoFile[]>
export type MVideoFormattableDetails =
MVideoFormattable &

View file

@ -3,5 +3,6 @@ export const enum VideoInclude {
NOT_PUBLISHED_STATE = 1 << 0,
HIDDEN_PRIVACY = 1 << 1,
BLACKLISTED = 1 << 2,
BLOCKED_OWNER = 1 << 3
BLOCKED_OWNER = 1 << 3,
FILES = 1 << 4
}

View file

@ -62,6 +62,9 @@ export interface Video {
blockedOwner?: boolean
blockedServer?: boolean
files?: VideoFile[]
streamingPlaylists?: VideoStreamingPlaylist[]
}
export interface VideoDetails extends Video {
@ -70,7 +73,6 @@ export interface VideoDetails extends Video {
channel: VideoChannel
account: Account
tags: string[]
files: VideoFile[]
commentsEnabled: boolean
downloadEnabled: boolean
@ -80,5 +82,6 @@ export interface VideoDetails extends Video {
trackerUrls: string[]
files: VideoFile[]
streamingPlaylists: VideoStreamingPlaylist[]
}