]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Add ability to filter by file type
authorChocobozzz <me@florianbigard.com>
Wed, 3 Nov 2021 10:32:41 +0000 (11:32 +0100)
committerChocobozzz <me@florianbigard.com>
Wed, 3 Nov 2021 10:32:41 +0000 (11:32 +0100)
client/src/app/+admin/overview/videos/video-admin.service.ts
client/src/app/+admin/overview/videos/video-list.component.html
client/src/app/+admin/overview/videos/video-list.component.ts
server/helpers/query.ts
server/middlewares/validators/videos/videos.ts
server/models/video/sql/videos-id-list-query-builder.ts
server/models/video/video.ts
server/tests/api/videos/videos-common-filters.ts
shared/models/search/videos-common-query.model.ts
support/doc/api/openapi.yaml

index d0854a2dc30b62e253c47aeb67e1c83468a42828..b90fe22d85e06f6dd2fc2c7ab3cfdc493ec41236 100644 (file)
@@ -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
index 67b554aaf5bcded6b4ef86cb61704bc380013627..134f646320d13d8a35f7ba28eff45c5e3ad85af2 100644 (file)
       </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="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="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="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>
index 8a15e8426415a8fee28d5854cfaa605d0537b376..635552cf52fd5f76bc9d4417d24cda6639774f46 100644 (file)
@@ -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
index 79cf076d13c99c4629cb9c273dd7771b79a2a3ee..97bbdfc65d61af0ad7cd42e4947837edaab0b772 100644 (file)
@@ -21,6 +21,8 @@ function pickCommonVideoQuery (query: VideosCommonQueryAfterSanitize) {
     'isLocal',
     'include',
     'skipCount',
+    'hasHLSFiles',
+    'hasWebtorrentFiles',
     'search'
   ])
 }
index 44233b653620ca71860248b0bc0c3aae89278ddc..5f123437912ef772ac0e98bf64ab52e9004c6f76 100644 (file)
@@ -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()
index 5064afafe568946a49cf880638af2df0d284620b..4a882e7905b3fdf08a2fb59b728fa0c24e5430ee 100644 (file)
@@ -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[]) {
index f9618c102fbdcd9450ba1bbd117c7586c0aee9fd..aef4fd20a0ae0365f873bc1fb28be3dd10c27661 100644 (file)
@@ -1030,6 +1030,8 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
     include?: VideoInclude
 
     hasFiles?: boolean // default false
+    hasWebtorrentFiles?: boolean
+    hasHLSFiles?: boolean
 
     categoryOneOf?: number[]
     licenceOneOf?: number[]
@@ -1053,9 +1055,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
 
     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<Partial<AttributesOnly<VideoModel>>> {
         'videoPlaylistId',
         'user',
         'historyOfUser',
+        'hasHLSFiles',
+        'hasWebtorrentFiles',
         'search'
       ]),
 
@@ -1103,27 +1105,39 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
     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<Partial<AttributesOnly<VideoModel>>> {
         'originallyPublishedEndDate',
         'durationMin',
         'durationMax',
+        'hasHLSFiles',
+        'hasWebtorrentFiles',
         'uuids',
         'search',
         '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) {
     return include & VideoInclude.BLACKLISTED ||
            include & VideoInclude.BLOCKED_OWNER ||
index 03c5c3b3ff198dba071b825c624bdd24d04d867e..4f22d4ac327394f8fafe4aeac93e1443bc39752b 100644 (file)
@@ -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 () {
index 55a98e302da3af327ec16873103b1d5b770c63d5..e9edb91b0df0bec2009b68d5c32725acb2946ee6 100644 (file)
@@ -26,6 +26,9 @@ export interface VideosCommonQuery {
   tagsOneOf?: string[]
   tagsAllOf?: string[]
 
+  hasHLSFiles?: boolean
+  hasWebtorrentFiles?: boolean
+
   skipCount?: boolean
 
   search?: string
index e9e7e1757e01bf1b07ace343708e1a80bb1f52bc..ec246bca0f9949dd4d27194e34ff71f2c8209455 100644 (file)
@@ -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