diff --git a/client/src/app/+admin/overview/videos/video-admin.service.ts b/client/src/app/+admin/overview/videos/video-admin.service.ts
index d0854a2dc..b90fe22d8 100644
--- a/client/src/app/+admin/overview/videos/video-admin.service.ts
+++ b/client/src/app/+admin/overview/videos/video-admin.service.ts
@@ -45,11 +45,33 @@ export class VideoAdminService {
children: [
{
queryParams: { search: 'isLive:false' },
- label: $localize`VOD videos`
+ label: $localize`VOD`
},
{
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: [
{
queryParams: { search: 'excludeMuted' },
@@ -94,6 +116,14 @@ export class VideoAdminService {
prefix: 'isLocal:',
isBoolean: true
},
+ hasHLSFiles: {
+ prefix: 'hls:',
+ isBoolean: true
+ },
+ hasWebtorrentFiles: {
+ prefix: 'webtorrent:',
+ isBoolean: true
+ },
isLive: {
prefix: 'isLive:',
isBoolean: true
diff --git a/client/src/app/+admin/overview/videos/video-list.component.html b/client/src/app/+admin/overview/videos/video-list.component.html
index 67b554aaf..134f64632 100644
--- a/client/src/app/+admin/overview/videos/video-list.component.html
+++ b/client/src/app/+admin/overview/videos/video-list.component.html
@@ -66,11 +66,11 @@
- {{ video.privacy.label }}
+ {{ video.privacy.label }}
NSFW
- {{ video.state.label }}
+ {{ video.state.label }}
Account muted
Server muted
@@ -83,7 +83,7 @@
WebTorrent
Live
- {{ getFilesSize(video) | bytes: 1 }}
+ {{ getFilesSize(video) | bytes: 1 }}
|
diff --git a/client/src/app/+admin/overview/videos/video-list.component.ts b/client/src/app/+admin/overview/videos/video-list.component.ts
index 8a15e8426..635552cf5 100644
--- a/client/src/app/+admin/overview/videos/video-list.component.ts
+++ b/client/src/app/+admin/overview/videos/video-list.component.ts
@@ -85,14 +85,14 @@ export class VideoListComponent extends RestTable implements OnInit {
this.reloadData()
}
- getPrivacyBadgeClass (privacy: VideoPrivacy) {
- if (privacy === VideoPrivacy.PUBLIC) return 'badge-blue'
+ getPrivacyBadgeClass (video: Video) {
+ if (video.privacy.id === VideoPrivacy.PUBLIC) return 'badge-blue'
return 'badge-yellow'
}
- isUnpublished (state: VideoState) {
- return state !== VideoState.LIVE_ENDED && state !== VideoState.PUBLISHED
+ isUnpublished (video: Video) {
+ return video.state.id !== VideoState.LIVE_ENDED && video.state.id !== VideoState.PUBLISHED
}
isAccountBlocked (video: Video) {
@@ -107,6 +107,10 @@ export class VideoListComponent extends RestTable implements OnInit {
return video.blacklisted
}
+ isImport (video: Video) {
+ return video.state.id === VideoState.TO_IMPORT
+ }
+
isHLS (video: Video) {
const p = video.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
if (!p) return false
diff --git a/server/helpers/query.ts b/server/helpers/query.ts
index 79cf076d1..97bbdfc65 100644
--- a/server/helpers/query.ts
+++ b/server/helpers/query.ts
@@ -21,6 +21,8 @@ function pickCommonVideoQuery (query: VideosCommonQueryAfterSanitize) {
'isLocal',
'include',
'skipCount',
+ 'hasHLSFiles',
+ 'hasWebtorrentFiles',
'search'
])
}
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts
index 44233b653..5f1234379 100644
--- a/server/middlewares/validators/videos/videos.ts
+++ b/server/middlewares/validators/videos/videos.ts
@@ -496,6 +496,14 @@ const commonVideosFiltersValidator = [
.optional()
.customSanitizer(toBooleanOrNull)
.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')
.optional()
.customSanitizer(toBooleanOrNull)
@@ -525,12 +533,13 @@ const commonVideosFiltersValidator = [
const user = res.locals.oauth?.token.User
- if (req.query.include && (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) !== true)) {
- res.fail({
- status: HttpStatusCode.UNAUTHORIZED_401,
- message: 'You are not allowed to see all local videos.'
- })
- return
+ if ((!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) !== true)) {
+ if (req.query.include) {
+ return res.fail({
+ status: HttpStatusCode.UNAUTHORIZED_401,
+ message: 'You are not allowed to see all videos.'
+ })
+ }
}
return next()
diff --git a/server/models/video/sql/videos-id-list-query-builder.ts b/server/models/video/sql/videos-id-list-query-builder.ts
index 5064afafe..4a882e790 100644
--- a/server/models/video/sql/videos-id-list-query-builder.ts
+++ b/server/models/video/sql/videos-id-list-query-builder.ts
@@ -44,6 +44,8 @@ export type BuildVideosListQueryOptions = {
uuids?: string[]
hasFiles?: boolean
+ hasHLSFiles?: boolean
+ hasWebtorrentFiles?: boolean
accountId?: number
videoChannelId?: number
@@ -169,6 +171,14 @@ export class VideosIdListQueryBuilder extends AbstractRunQuery {
this.whereFileExists()
}
+ if (exists(options.hasWebtorrentFiles)) {
+ this.whereWebTorrentFileExists(options.hasWebtorrentFiles)
+ }
+
+ if (exists(options.hasHLSFiles)) {
+ this.whereHLSFileExists(options.hasHLSFiles)
+ }
+
if (options.tagsOneOf) {
this.whereTagsOneOf(options.tagsOneOf)
}
@@ -371,16 +381,31 @@ export class VideosIdListQueryBuilder extends AbstractRunQuery {
}
private whereFileExists () {
- this.and.push(
- '(' +
- ' EXISTS (SELECT 1 FROM "videoFile" WHERE "videoFile"."videoId" = "video"."id") ' +
- ' OR EXISTS (' +
- ' SELECT 1 FROM "videoStreamingPlaylist" ' +
- ' INNER JOIN "videoFile" ON "videoFile"."videoStreamingPlaylistId" = "videoStreamingPlaylist"."id" ' +
- ' WHERE "videoStreamingPlaylist"."videoId" = "video"."id"' +
- ' )' +
- ')'
- )
+ this.and.push(`(${this.buildWebTorrentFileExistsQuery(true)} OR ${this.buildHLSFileExistsQuery(true)})`)
+ }
+
+ 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" ' +
+ ' INNER JOIN "videoFile" ON "videoFile"."videoStreamingPlaylistId" = "videoStreamingPlaylist"."id" ' +
+ ' WHERE "videoStreamingPlaylist"."videoId" = "video"."id"' +
+ ')'
}
private whereTagsOneOf (tagsOneOf: string[]) {
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index f9618c102..aef4fd20a 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -1030,6 +1030,8 @@ export class VideoModel extends Model>> {
include?: VideoInclude
hasFiles?: boolean // default false
+ hasWebtorrentFiles?: boolean
+ hasHLSFiles?: boolean
categoryOneOf?: number[]
licenceOneOf?: number[]
@@ -1053,9 +1055,7 @@ export class VideoModel extends Model>> {
search?: string
}) {
- 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')
- }
+ VideoModel.throwIfPrivateIncludeWithoutUser(options.include, options.user)
const trendingDays = options.sort.endsWith('trending')
? CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS
@@ -1088,6 +1088,8 @@ export class VideoModel extends Model>> {
'videoPlaylistId',
'user',
'historyOfUser',
+ 'hasHLSFiles',
+ 'hasWebtorrentFiles',
'search'
]),
@@ -1103,27 +1105,39 @@ export class VideoModel extends Model>> {
start: number
count: number
sort: string
- search?: string
- host?: string
- startDate?: string // ISO 8601
- endDate?: string // ISO 8601
- originallyPublishedStartDate?: string
- originallyPublishedEndDate?: string
+
nsfw?: boolean
isLive?: boolean
isLocal?: boolean
include?: VideoInclude
+
categoryOneOf?: number[]
licenceOneOf?: number[]
languageOneOf?: string[]
tagsOneOf?: 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
durationMax?: number // seconds
- user?: MUserAccountId
uuids?: string[]
- displayOnlyForFollower: DisplayOnlyForFollowerOptions | null
}) {
+ VideoModel.throwIfPrivateIncludeWithoutUser(options.include, options.user)
+
const serverActor = await getServerActor()
const queryOptions = {
@@ -1148,6 +1162,8 @@ export class VideoModel extends Model>> {
'originallyPublishedEndDate',
'durationMin',
'durationMax',
+ 'hasHLSFiles',
+ 'hasWebtorrentFiles',
'uuids',
'search',
'displayOnlyForFollower'
@@ -1489,6 +1505,12 @@ export class VideoModel extends Model>> {
}
}
+ 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) {
return include & VideoInclude.BLACKLISTED ||
include & VideoInclude.BLOCKED_OWNER ||
diff --git a/server/tests/api/videos/videos-common-filters.ts b/server/tests/api/videos/videos-common-filters.ts
index 03c5c3b3f..4f22d4ac3 100644
--- a/server/tests/api/videos/videos-common-filters.ts
+++ b/server/tests/api/videos/videos-common-filters.ts
@@ -135,6 +135,8 @@ describe('Test videos filter', function () {
server: PeerTubeServer
path: string
isLocal?: boolean
+ hasWebtorrentFiles?: boolean
+ hasHLSFiles?: boolean
include?: VideoInclude
category?: number
tagsAllOf?: string[]
@@ -146,7 +148,7 @@ describe('Test videos filter', function () {
path: options.path,
token: options.token ?? options.server.accessToken,
query: {
- ...pick(options, [ 'isLocal', 'include', 'category', 'tagsAllOf' ]),
+ ...pick(options, [ 'isLocal', 'include', 'category', 'tagsAllOf', 'hasWebtorrentFiles', 'hasHLSFiles' ]),
sort: 'createdAt'
},
@@ -397,11 +399,9 @@ describe('Test videos filter', function () {
for (const path of paths) {
{
-
const videos = await listVideos({ server: servers[0], path, tagsAllOf: [ 'tag1', 'tag2' ] })
expect(videos).to.have.lengthOf(1)
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 () {
diff --git a/shared/models/search/videos-common-query.model.ts b/shared/models/search/videos-common-query.model.ts
index 55a98e302..e9edb91b0 100644
--- a/shared/models/search/videos-common-query.model.ts
+++ b/shared/models/search/videos-common-query.model.ts
@@ -26,6 +26,9 @@ export interface VideosCommonQuery {
tagsOneOf?: string[]
tagsAllOf?: string[]
+ hasHLSFiles?: boolean
+ hasWebtorrentFiles?: boolean
+
skipCount?: boolean
search?: string
diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml
index e9e7e1757..ec246bca0 100644
--- a/support/doc/api/openapi.yaml
+++ b/support/doc/api/openapi.yaml
@@ -369,6 +369,8 @@ paths:
- $ref: '#/components/parameters/nsfw'
- $ref: '#/components/parameters/isLocal'
- $ref: '#/components/parameters/include'
+ - $ref: '#/components/parameters/hasHLSFiles'
+ - $ref: '#/components/parameters/hasWebtorrentFiles'
- $ref: '#/components/parameters/skipCount'
- $ref: '#/components/parameters/start'
- $ref: '#/components/parameters/count'
@@ -1303,6 +1305,8 @@ paths:
- $ref: '#/components/parameters/nsfw'
- $ref: '#/components/parameters/isLocal'
- $ref: '#/components/parameters/include'
+ - $ref: '#/components/parameters/hasHLSFiles'
+ - $ref: '#/components/parameters/hasWebtorrentFiles'
- $ref: '#/components/parameters/skipCount'
- $ref: '#/components/parameters/start'
- $ref: '#/components/parameters/count'
@@ -1624,6 +1628,8 @@ paths:
- $ref: '#/components/parameters/nsfw'
- $ref: '#/components/parameters/isLocal'
- $ref: '#/components/parameters/include'
+ - $ref: '#/components/parameters/hasHLSFiles'
+ - $ref: '#/components/parameters/hasWebtorrentFiles'
- $ref: '#/components/parameters/skipCount'
- $ref: '#/components/parameters/start'
- $ref: '#/components/parameters/count'
@@ -2861,6 +2867,8 @@ paths:
- $ref: '#/components/parameters/nsfw'
- $ref: '#/components/parameters/isLocal'
- $ref: '#/components/parameters/include'
+ - $ref: '#/components/parameters/hasHLSFiles'
+ - $ref: '#/components/parameters/hasWebtorrentFiles'
- $ref: '#/components/parameters/skipCount'
- $ref: '#/components/parameters/start'
- $ref: '#/components/parameters/count'
@@ -3582,6 +3590,8 @@ paths:
- $ref: '#/components/parameters/nsfw'
- $ref: '#/components/parameters/isLocal'
- $ref: '#/components/parameters/include'
+ - $ref: '#/components/parameters/hasHLSFiles'
+ - $ref: '#/components/parameters/hasWebtorrentFiles'
- $ref: '#/components/parameters/skipCount'
- $ref: '#/components/parameters/start'
- $ref: '#/components/parameters/count'
@@ -4085,6 +4095,8 @@ paths:
- $ref: '#/components/parameters/nsfw'
- $ref: '#/components/parameters/isLocal'
- $ref: '#/components/parameters/include'
+ - $ref: '#/components/parameters/hasHLSFiles'
+ - $ref: '#/components/parameters/hasWebtorrentFiles'
responses:
'204':
description: successful operation
@@ -4167,6 +4179,8 @@ paths:
- $ref: '#/components/parameters/nsfw'
- $ref: '#/components/parameters/isLocal'
- $ref: '#/components/parameters/include'
+ - $ref: '#/components/parameters/hasHLSFiles'
+ - $ref: '#/components/parameters/hasWebtorrentFiles'
responses:
'204':
description: successful operation
@@ -4806,6 +4820,20 @@ components:
schema:
type: boolean
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:
name: include
in: query
|