From d324756edb836672f12284cd18e642a658b273d8 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 3 Nov 2021 11:32:41 +0100 Subject: [PATCH] Add ability to filter by file type --- .../overview/videos/video-admin.service.ts | 36 ++++++++- .../overview/videos/video-list.component.html | 6 +- .../overview/videos/video-list.component.ts | 12 ++- server/helpers/query.ts | 2 + .../middlewares/validators/videos/videos.ts | 21 +++-- .../video/sql/videos-id-list-query-builder.ts | 45 ++++++++--- server/models/video/video.ts | 44 +++++++--- .../tests/api/videos/videos-common-filters.ts | 80 ++++++++++++++++++- .../search/videos-common-query.model.ts | 3 + support/doc/api/openapi.yaml | 28 +++++++ 10 files changed, 237 insertions(+), 40 deletions(-) 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