1
0
Fork 0

Add ability to filter by file type

This commit is contained in:
Chocobozzz 2021-11-03 11:32:41 +01:00
parent d5d9c5b79e
commit d324756edb
No known key found for this signature in database
GPG key ID: 583A612D890159BE
10 changed files with 237 additions and 40 deletions

View file

@ -45,11 +45,33 @@ export class VideoAdminService {
children: [ children: [
{ {
queryParams: { search: 'isLive:false' }, queryParams: { search: 'isLive:false' },
label: $localize`VOD videos` label: $localize`VOD`
}, },
{ {
queryParams: { search: 'isLive:true' }, queryParams: { search: 'isLive:true' },
label: $localize`Live videos` label: $localize`Live`
}
]
},
{
title: $localize`Video files`,
children: [
{
queryParams: { search: 'webtorrent:true' },
label: $localize`With WebTorrent`
},
{
queryParams: { search: 'webtorrent:false' },
label: $localize`Without WebTorrent`
},
{
queryParams: { search: 'hls:true' },
label: $localize`With HLS`
},
{
queryParams: { search: 'hls:false' },
label: $localize`Without HLS`
} }
] ]
}, },
@ -69,7 +91,7 @@ export class VideoAdminService {
}, },
{ {
title: $localize`Include/Exclude`, title: $localize`Exclude`,
children: [ children: [
{ {
queryParams: { search: 'excludeMuted' }, queryParams: { search: 'excludeMuted' },
@ -94,6 +116,14 @@ export class VideoAdminService {
prefix: 'isLocal:', prefix: 'isLocal:',
isBoolean: true isBoolean: true
}, },
hasHLSFiles: {
prefix: 'hls:',
isBoolean: true
},
hasWebtorrentFiles: {
prefix: 'webtorrent:',
isBoolean: true
},
isLive: { isLive: {
prefix: 'isLive:', prefix: 'isLive:',
isBoolean: true isBoolean: true

View file

@ -66,11 +66,11 @@
</td> </td>
<td> <td>
<span [ngClass]="getPrivacyBadgeClass(video.privacy.id)" class="badge">{{ video.privacy.label }}</span> <span [ngClass]="getPrivacyBadgeClass(video)" class="badge">{{ video.privacy.label }}</span>
<span *ngIf="video.nsfw" class="badge badge-red" i18n>NSFW</span> <span *ngIf="video.nsfw" class="badge badge-red" i18n>NSFW</span>
<span *ngIf="isUnpublished(video.state.id)" class="badge badge-yellow" i18n>{{ video.state.label }}</span> <span *ngIf="isUnpublished(video)" class="badge badge-yellow" i18n>{{ video.state.label }}</span>
<span *ngIf="isAccountBlocked(video)" class="badge badge-red" i18n>Account muted</span> <span *ngIf="isAccountBlocked(video)" class="badge badge-red" i18n>Account muted</span>
<span *ngIf="isServerBlocked(video)" class="badge badge-red" i18n>Server muted</span> <span *ngIf="isServerBlocked(video)" class="badge badge-red" i18n>Server muted</span>
@ -83,7 +83,7 @@
<span *ngIf="isWebTorrent(video)" class="badge badge-blue">WebTorrent</span> <span *ngIf="isWebTorrent(video)" class="badge badge-blue">WebTorrent</span>
<span *ngIf="video.isLive" class="badge badge-blue">Live</span> <span *ngIf="video.isLive" class="badge badge-blue">Live</span>
<span *ngIf="!video.isLive && video.isLocal">{{ getFilesSize(video) | bytes: 1 }}</span> <span *ngIf="!isImport(video) && !video.isLive && video.isLocal">{{ getFilesSize(video) | bytes: 1 }}</span>
</td> </td>
<td> <td>

View file

@ -85,14 +85,14 @@ export class VideoListComponent extends RestTable implements OnInit {
this.reloadData() this.reloadData()
} }
getPrivacyBadgeClass (privacy: VideoPrivacy) { getPrivacyBadgeClass (video: Video) {
if (privacy === VideoPrivacy.PUBLIC) return 'badge-blue' if (video.privacy.id === VideoPrivacy.PUBLIC) return 'badge-blue'
return 'badge-yellow' return 'badge-yellow'
} }
isUnpublished (state: VideoState) { isUnpublished (video: Video) {
return state !== VideoState.LIVE_ENDED && state !== VideoState.PUBLISHED return video.state.id !== VideoState.LIVE_ENDED && video.state.id !== VideoState.PUBLISHED
} }
isAccountBlocked (video: Video) { isAccountBlocked (video: Video) {
@ -107,6 +107,10 @@ export class VideoListComponent extends RestTable implements OnInit {
return video.blacklisted return video.blacklisted
} }
isImport (video: Video) {
return video.state.id === VideoState.TO_IMPORT
}
isHLS (video: Video) { isHLS (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

View file

@ -21,6 +21,8 @@ function pickCommonVideoQuery (query: VideosCommonQueryAfterSanitize) {
'isLocal', 'isLocal',
'include', 'include',
'skipCount', 'skipCount',
'hasHLSFiles',
'hasWebtorrentFiles',
'search' 'search'
]) ])
} }

View file

@ -496,6 +496,14 @@ const commonVideosFiltersValidator = [
.optional() .optional()
.customSanitizer(toBooleanOrNull) .customSanitizer(toBooleanOrNull)
.custom(isBooleanValid).withMessage('Should have a valid local boolean'), .custom(isBooleanValid).withMessage('Should have a valid local boolean'),
query('hasHLSFiles')
.optional()
.customSanitizer(toBooleanOrNull)
.custom(isBooleanValid).withMessage('Should have a valid has hls boolean'),
query('hasWebtorrentFiles')
.optional()
.customSanitizer(toBooleanOrNull)
.custom(isBooleanValid).withMessage('Should have a valid has webtorrent boolean'),
query('skipCount') query('skipCount')
.optional() .optional()
.customSanitizer(toBooleanOrNull) .customSanitizer(toBooleanOrNull)
@ -525,12 +533,13 @@ const commonVideosFiltersValidator = [
const user = res.locals.oauth?.token.User const user = res.locals.oauth?.token.User
if (req.query.include && (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) !== true)) { if ((!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) !== true)) {
res.fail({ if (req.query.include) {
return res.fail({
status: HttpStatusCode.UNAUTHORIZED_401, status: HttpStatusCode.UNAUTHORIZED_401,
message: 'You are not allowed to see all local videos.' message: 'You are not allowed to see all videos.'
}) })
return }
} }
return next() return next()

View file

@ -44,6 +44,8 @@ export type BuildVideosListQueryOptions = {
uuids?: string[] uuids?: string[]
hasFiles?: boolean hasFiles?: boolean
hasHLSFiles?: boolean
hasWebtorrentFiles?: boolean
accountId?: number accountId?: number
videoChannelId?: number videoChannelId?: number
@ -169,6 +171,14 @@ export class VideosIdListQueryBuilder extends AbstractRunQuery {
this.whereFileExists() this.whereFileExists()
} }
if (exists(options.hasWebtorrentFiles)) {
this.whereWebTorrentFileExists(options.hasWebtorrentFiles)
}
if (exists(options.hasHLSFiles)) {
this.whereHLSFileExists(options.hasHLSFiles)
}
if (options.tagsOneOf) { if (options.tagsOneOf) {
this.whereTagsOneOf(options.tagsOneOf) this.whereTagsOneOf(options.tagsOneOf)
} }
@ -371,16 +381,31 @@ export class VideosIdListQueryBuilder extends AbstractRunQuery {
} }
private whereFileExists () { private whereFileExists () {
this.and.push( this.and.push(`(${this.buildWebTorrentFileExistsQuery(true)} OR ${this.buildHLSFileExistsQuery(true)})`)
'(' + }
' EXISTS (SELECT 1 FROM "videoFile" WHERE "videoFile"."videoId" = "video"."id") ' +
' OR EXISTS (' + private whereWebTorrentFileExists (exists: boolean) {
this.and.push(this.buildWebTorrentFileExistsQuery(exists))
}
private whereHLSFileExists (exists: boolean) {
this.and.push(this.buildHLSFileExistsQuery(exists))
}
private buildWebTorrentFileExistsQuery (exists: boolean) {
const prefix = exists ? '' : 'NOT '
return prefix + 'EXISTS (SELECT 1 FROM "videoFile" WHERE "videoFile"."videoId" = "video"."id")'
}
private buildHLSFileExistsQuery (exists: boolean) {
const prefix = exists ? '' : 'NOT '
return prefix + 'EXISTS (' +
' SELECT 1 FROM "videoStreamingPlaylist" ' + ' SELECT 1 FROM "videoStreamingPlaylist" ' +
' INNER JOIN "videoFile" ON "videoFile"."videoStreamingPlaylistId" = "videoStreamingPlaylist"."id" ' + ' INNER JOIN "videoFile" ON "videoFile"."videoStreamingPlaylistId" = "videoStreamingPlaylist"."id" ' +
' WHERE "videoStreamingPlaylist"."videoId" = "video"."id"' + ' WHERE "videoStreamingPlaylist"."videoId" = "video"."id"' +
' )' +
')' ')'
)
} }
private whereTagsOneOf (tagsOneOf: string[]) { private whereTagsOneOf (tagsOneOf: string[]) {

View file

@ -1030,6 +1030,8 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
include?: VideoInclude include?: VideoInclude
hasFiles?: boolean // default false hasFiles?: boolean // default false
hasWebtorrentFiles?: boolean
hasHLSFiles?: boolean
categoryOneOf?: number[] categoryOneOf?: number[]
licenceOneOf?: number[] licenceOneOf?: number[]
@ -1053,9 +1055,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
search?: string search?: string
}) { }) {
if (VideoModel.isPrivateInclude(options.include) && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) { VideoModel.throwIfPrivateIncludeWithoutUser(options.include, options.user)
throw new Error('Try to filter all-local but no user has not the see all videos right')
}
const trendingDays = options.sort.endsWith('trending') const trendingDays = options.sort.endsWith('trending')
? CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS ? CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS
@ -1088,6 +1088,8 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
'videoPlaylistId', 'videoPlaylistId',
'user', 'user',
'historyOfUser', 'historyOfUser',
'hasHLSFiles',
'hasWebtorrentFiles',
'search' 'search'
]), ]),
@ -1103,27 +1105,39 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
start: number start: number
count: number count: number
sort: string sort: string
search?: string
host?: string
startDate?: string // ISO 8601
endDate?: string // ISO 8601
originallyPublishedStartDate?: string
originallyPublishedEndDate?: string
nsfw?: boolean nsfw?: boolean
isLive?: boolean isLive?: boolean
isLocal?: boolean isLocal?: boolean
include?: VideoInclude include?: VideoInclude
categoryOneOf?: number[] categoryOneOf?: number[]
licenceOneOf?: number[] licenceOneOf?: number[]
languageOneOf?: string[] languageOneOf?: string[]
tagsOneOf?: string[] tagsOneOf?: string[]
tagsAllOf?: string[] tagsAllOf?: string[]
displayOnlyForFollower: DisplayOnlyForFollowerOptions | null
user?: MUserAccountId
hasWebtorrentFiles?: boolean
hasHLSFiles?: boolean
search?: string
host?: string
startDate?: string // ISO 8601
endDate?: string // ISO 8601
originallyPublishedStartDate?: string
originallyPublishedEndDate?: string
durationMin?: number // seconds durationMin?: number // seconds
durationMax?: number // seconds durationMax?: number // seconds
user?: MUserAccountId
uuids?: string[] uuids?: string[]
displayOnlyForFollower: DisplayOnlyForFollowerOptions | null
}) { }) {
VideoModel.throwIfPrivateIncludeWithoutUser(options.include, options.user)
const serverActor = await getServerActor() const serverActor = await getServerActor()
const queryOptions = { const queryOptions = {
@ -1148,6 +1162,8 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
'originallyPublishedEndDate', 'originallyPublishedEndDate',
'durationMin', 'durationMin',
'durationMax', 'durationMax',
'hasHLSFiles',
'hasWebtorrentFiles',
'uuids', 'uuids',
'search', 'search',
'displayOnlyForFollower' 'displayOnlyForFollower'
@ -1489,6 +1505,12 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
} }
} }
private static throwIfPrivateIncludeWithoutUser (include: VideoInclude, user: MUserAccountId) {
if (VideoModel.isPrivateInclude(include) && !user?.hasRight(UserRight.SEE_ALL_VIDEOS)) {
throw new Error('Try to filter all-local but no user has not the see all videos right')
}
}
private static isPrivateInclude (include: VideoInclude) { private static isPrivateInclude (include: VideoInclude) {
return include & VideoInclude.BLACKLISTED || return include & VideoInclude.BLACKLISTED ||
include & VideoInclude.BLOCKED_OWNER || include & VideoInclude.BLOCKED_OWNER ||

View file

@ -135,6 +135,8 @@ describe('Test videos filter', function () {
server: PeerTubeServer server: PeerTubeServer
path: string path: string
isLocal?: boolean isLocal?: boolean
hasWebtorrentFiles?: boolean
hasHLSFiles?: boolean
include?: VideoInclude include?: VideoInclude
category?: number category?: number
tagsAllOf?: string[] tagsAllOf?: string[]
@ -146,7 +148,7 @@ describe('Test videos filter', function () {
path: options.path, path: options.path,
token: options.token ?? options.server.accessToken, token: options.token ?? options.server.accessToken,
query: { query: {
...pick(options, [ 'isLocal', 'include', 'category', 'tagsAllOf' ]), ...pick(options, [ 'isLocal', 'include', 'category', 'tagsAllOf', 'hasWebtorrentFiles', 'hasHLSFiles' ]),
sort: 'createdAt' sort: 'createdAt'
}, },
@ -397,11 +399,9 @@ describe('Test videos filter', function () {
for (const path of paths) { for (const path of paths) {
{ {
const videos = await listVideos({ server: servers[0], path, tagsAllOf: [ 'tag1', 'tag2' ] }) const videos = await listVideos({ server: servers[0], path, tagsAllOf: [ 'tag1', 'tag2' ] })
expect(videos).to.have.lengthOf(1) expect(videos).to.have.lengthOf(1)
expect(videos[0].name).to.equal('tag filter') expect(videos[0].name).to.equal('tag filter')
} }
{ {
@ -421,6 +421,80 @@ describe('Test videos filter', function () {
} }
} }
}) })
it('Should filter by HLS or WebTorrent files', async function () {
this.timeout(360000)
const finderFactory = (name: string) => (videos: Video[]) => videos.some(v => v.name === name)
await servers[0].config.enableTranscoding(true, false)
await servers[0].videos.upload({ attributes: { name: 'webtorrent video' } })
const hasWebtorrent = finderFactory('webtorrent video')
await waitJobs(servers)
await servers[0].config.enableTranscoding(false, true)
await servers[0].videos.upload({ attributes: { name: 'hls video' } })
const hasHLS = finderFactory('hls video')
await waitJobs(servers)
await servers[0].config.enableTranscoding(true, true)
await servers[0].videos.upload({ attributes: { name: 'hls and webtorrent video' } })
const hasBoth = finderFactory('hls and webtorrent video')
await waitJobs(servers)
for (const path of paths) {
{
const videos = await listVideos({ server: servers[0], path, hasWebtorrentFiles: true })
expect(hasWebtorrent(videos)).to.be.true
expect(hasHLS(videos)).to.be.false
expect(hasBoth(videos)).to.be.true
}
{
const videos = await listVideos({ server: servers[0], path, hasWebtorrentFiles: false })
expect(hasWebtorrent(videos)).to.be.false
expect(hasHLS(videos)).to.be.true
expect(hasBoth(videos)).to.be.false
}
{
const videos = await listVideos({ server: servers[0], path, hasHLSFiles: true })
expect(hasWebtorrent(videos)).to.be.false
expect(hasHLS(videos)).to.be.true
expect(hasBoth(videos)).to.be.true
}
{
const videos = await listVideos({ server: servers[0], path, hasHLSFiles: false })
expect(hasWebtorrent(videos)).to.be.true
expect(hasHLS(videos)).to.be.false
expect(hasBoth(videos)).to.be.false
}
{
const videos = await listVideos({ server: servers[0], path, hasHLSFiles: false, hasWebtorrentFiles: false })
expect(hasWebtorrent(videos)).to.be.false
expect(hasHLS(videos)).to.be.false
expect(hasBoth(videos)).to.be.false
}
{
const videos = await listVideos({ server: servers[0], path, hasHLSFiles: true, hasWebtorrentFiles: true })
expect(hasWebtorrent(videos)).to.be.false
expect(hasHLS(videos)).to.be.false
expect(hasBoth(videos)).to.be.true
}
}
})
}) })
after(async function () { after(async function () {

View file

@ -26,6 +26,9 @@ export interface VideosCommonQuery {
tagsOneOf?: string[] tagsOneOf?: string[]
tagsAllOf?: string[] tagsAllOf?: string[]
hasHLSFiles?: boolean
hasWebtorrentFiles?: boolean
skipCount?: boolean skipCount?: boolean
search?: string search?: string

View file

@ -369,6 +369,8 @@ paths:
- $ref: '#/components/parameters/nsfw' - $ref: '#/components/parameters/nsfw'
- $ref: '#/components/parameters/isLocal' - $ref: '#/components/parameters/isLocal'
- $ref: '#/components/parameters/include' - $ref: '#/components/parameters/include'
- $ref: '#/components/parameters/hasHLSFiles'
- $ref: '#/components/parameters/hasWebtorrentFiles'
- $ref: '#/components/parameters/skipCount' - $ref: '#/components/parameters/skipCount'
- $ref: '#/components/parameters/start' - $ref: '#/components/parameters/start'
- $ref: '#/components/parameters/count' - $ref: '#/components/parameters/count'
@ -1303,6 +1305,8 @@ paths:
- $ref: '#/components/parameters/nsfw' - $ref: '#/components/parameters/nsfw'
- $ref: '#/components/parameters/isLocal' - $ref: '#/components/parameters/isLocal'
- $ref: '#/components/parameters/include' - $ref: '#/components/parameters/include'
- $ref: '#/components/parameters/hasHLSFiles'
- $ref: '#/components/parameters/hasWebtorrentFiles'
- $ref: '#/components/parameters/skipCount' - $ref: '#/components/parameters/skipCount'
- $ref: '#/components/parameters/start' - $ref: '#/components/parameters/start'
- $ref: '#/components/parameters/count' - $ref: '#/components/parameters/count'
@ -1624,6 +1628,8 @@ paths:
- $ref: '#/components/parameters/nsfw' - $ref: '#/components/parameters/nsfw'
- $ref: '#/components/parameters/isLocal' - $ref: '#/components/parameters/isLocal'
- $ref: '#/components/parameters/include' - $ref: '#/components/parameters/include'
- $ref: '#/components/parameters/hasHLSFiles'
- $ref: '#/components/parameters/hasWebtorrentFiles'
- $ref: '#/components/parameters/skipCount' - $ref: '#/components/parameters/skipCount'
- $ref: '#/components/parameters/start' - $ref: '#/components/parameters/start'
- $ref: '#/components/parameters/count' - $ref: '#/components/parameters/count'
@ -2861,6 +2867,8 @@ paths:
- $ref: '#/components/parameters/nsfw' - $ref: '#/components/parameters/nsfw'
- $ref: '#/components/parameters/isLocal' - $ref: '#/components/parameters/isLocal'
- $ref: '#/components/parameters/include' - $ref: '#/components/parameters/include'
- $ref: '#/components/parameters/hasHLSFiles'
- $ref: '#/components/parameters/hasWebtorrentFiles'
- $ref: '#/components/parameters/skipCount' - $ref: '#/components/parameters/skipCount'
- $ref: '#/components/parameters/start' - $ref: '#/components/parameters/start'
- $ref: '#/components/parameters/count' - $ref: '#/components/parameters/count'
@ -3582,6 +3590,8 @@ paths:
- $ref: '#/components/parameters/nsfw' - $ref: '#/components/parameters/nsfw'
- $ref: '#/components/parameters/isLocal' - $ref: '#/components/parameters/isLocal'
- $ref: '#/components/parameters/include' - $ref: '#/components/parameters/include'
- $ref: '#/components/parameters/hasHLSFiles'
- $ref: '#/components/parameters/hasWebtorrentFiles'
- $ref: '#/components/parameters/skipCount' - $ref: '#/components/parameters/skipCount'
- $ref: '#/components/parameters/start' - $ref: '#/components/parameters/start'
- $ref: '#/components/parameters/count' - $ref: '#/components/parameters/count'
@ -4085,6 +4095,8 @@ paths:
- $ref: '#/components/parameters/nsfw' - $ref: '#/components/parameters/nsfw'
- $ref: '#/components/parameters/isLocal' - $ref: '#/components/parameters/isLocal'
- $ref: '#/components/parameters/include' - $ref: '#/components/parameters/include'
- $ref: '#/components/parameters/hasHLSFiles'
- $ref: '#/components/parameters/hasWebtorrentFiles'
responses: responses:
'204': '204':
description: successful operation description: successful operation
@ -4167,6 +4179,8 @@ paths:
- $ref: '#/components/parameters/nsfw' - $ref: '#/components/parameters/nsfw'
- $ref: '#/components/parameters/isLocal' - $ref: '#/components/parameters/isLocal'
- $ref: '#/components/parameters/include' - $ref: '#/components/parameters/include'
- $ref: '#/components/parameters/hasHLSFiles'
- $ref: '#/components/parameters/hasWebtorrentFiles'
responses: responses:
'204': '204':
description: successful operation description: successful operation
@ -4806,6 +4820,20 @@ components:
schema: schema:
type: boolean type: boolean
description: '**PeerTube >= 4.0** Display only local or remote videos' description: '**PeerTube >= 4.0** Display only local or remote videos'
hasHLSFiles:
name: hasHLSFiles
in: query
required: false
schema:
type: boolean
description: '**PeerTube >= 4.0** Display only videos that have HLS files'
hasWebtorrentFiles:
name: hasWebtorrentFiles
in: query
required: false
schema:
type: boolean
description: '**PeerTube >= 4.0** Display only videos that have WebTorrent files'
include: include:
name: include name: include
in: query in: query