]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Deprecate filter video query
authorChocobozzz <me@florianbigard.com>
Wed, 27 Oct 2021 12:37:04 +0000 (14:37 +0200)
committerChocobozzz <chocobozzz@cpy.re>
Fri, 29 Oct 2021 09:48:21 +0000 (11:48 +0200)
Introduce include and isLocal instead

52 files changed:
client/src/app/+admin/overview/videos/video-list.component.html
client/src/app/+admin/overview/videos/video-list.component.scss
client/src/app/+admin/overview/videos/video-list.component.ts
client/src/app/+videos/+video-watch/shared/information/video-alert.component.html
client/src/app/+videos/video-list/videos-list-common-page.component.ts
client/src/app/shared/shared-custom-markup/custom-markup.service.ts
client/src/app/shared/shared-custom-markup/peertube-custom-tags/videos-list-markup.component.ts
client/src/app/shared/shared-main/video/video.model.ts
client/src/app/shared/shared-main/video/video.service.ts
client/src/app/shared/shared-moderation/video-block.component.ts
client/src/app/shared/shared-video-miniature/video-actions-dropdown.component.ts
client/src/app/shared/shared-video-miniature/video-filters.model.ts
client/src/app/shared/shared-video-miniature/video-miniature.component.html
server/controllers/api/accounts.ts
server/controllers/api/overviews.ts
server/controllers/api/search/search-videos.ts
server/controllers/api/users/my-subscriptions.ts
server/controllers/api/video-channel.ts
server/controllers/api/videos/index.ts
server/controllers/bots.ts
server/controllers/feeds.ts
server/helpers/custom-validators/videos.ts
server/helpers/query.ts
server/middlewares/validators/videos/videos.ts
server/models/account/account.ts
server/models/server/server.ts
server/models/user/user-video-history.ts
server/models/video/formatter/video-format-utils.ts
server/models/video/sql/shared/abstract-videos-model-query-builder.ts
server/models/video/sql/shared/video-model-builder.ts
server/models/video/sql/shared/video-tables.ts
server/models/video/sql/video-model-get-query-builder.ts
server/models/video/sql/videos-id-list-query-builder.ts
server/models/video/sql/videos-model-list-query-builder.ts
server/models/video/video.ts
server/tests/api/check-params/index.ts
server/tests/api/check-params/videos-common-filters.ts [new file with mode: 0644]
server/tests/api/check-params/videos-filter.ts [deleted file]
server/tests/api/videos/index.ts
server/tests/api/videos/multiple-servers.ts
server/tests/api/videos/single-server.ts
server/tests/api/videos/videos-common-filters.ts [new file with mode: 0644]
server/tests/api/videos/videos-filter.ts [deleted file]
server/types/models/account/account.ts
shared/extra-utils/videos/videos-command.ts
shared/models/search/videos-common-query.model.ts
shared/models/search/videos-search-query.model.ts
shared/models/videos/index.ts
shared/models/videos/video-filter.type.ts [moved from shared/models/videos/video-query.type.ts with 100% similarity]
shared/models/videos/video-include.enum.ts [new file with mode: 0644]
shared/models/videos/video.model.ts
support/doc/api/openapi.yaml

index 1f1e9cc6e3b35b1c0588b51071a8b62303d7016b..6250c00fb862b4d2010e73570cb1496adc794ebc 100644 (file)
         <my-video-cell [video]="video"></my-video-cell>
       </td>
 
-      <td>
-        <span class="badge badge-blue" i18n>{{ video.privacy.label }}</span>
+      <td class="badges">
+        <span [ngClass]="getPrivacyBadgeClass(video.privacy.id)" class="badge" i18n>{{ video.privacy.label }}</span>
+
         <span *ngIf="video.nsfw" class="badge badge-red" i18n>NSFW</span>
-        <span *ngIf="video.blocked" class="badge badge-red" i18n>NSFW</span>
+
+        <span *ngIf="isUnpublished(video.state.id)" class="badge badge-yellow" i18n>Not published yet</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="isVideoBlocked(video)" class="badge badge-red" i18n>Blocked</span>
       </td>
 
       <td>
index fcdb457f25f7c25a6d03be66d59ae49df3ca5c66..250a917e4ee11b144ad45563af1ba282d5d5b614 100644 (file)
@@ -7,4 +7,6 @@ my-embed {
 
 .badge {
   @include table-badge;
+
+  margin-right: 5px;
 }
index a445bc209a813a978fc9e2fe2819d1a16639424d..dd9225e6a8ee80677d0915d1e3f10ea93bb33b3a 100644 (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 } from '@shared/models'
+import { UserRight, VideoPrivacy, VideoState } from '@shared/models'
 import { AdvancedInputFilter } from '@app/shared/shared-forms'
 import { VideoActionsDisplayType } from '@app/shared/shared-video-miniature'
 
@@ -28,8 +28,12 @@ export class VideoListComponent extends RestTable implements OnInit {
       title: $localize`Advanced filters`,
       children: [
         {
-          queryParams: { search: 'local:true' },
-          label: $localize`Only local videos`
+          queryParams: { search: 'isLocal:false' },
+          label: $localize`Remote videos`
+        },
+        {
+          queryParams: { search: 'isLocal:true' },
+          label: $localize`Local videos`
         }
       ]
     }
@@ -88,6 +92,28 @@ export class VideoListComponent extends RestTable implements OnInit {
     this.reloadData()
   }
 
+  getPrivacyBadgeClass (privacy: VideoPrivacy) {
+    if (privacy === VideoPrivacy.PUBLIC) return 'badge-blue'
+
+    return 'badge-yellow'
+  }
+
+  isUnpublished (state: VideoState) {
+    return state !== VideoState.PUBLISHED
+  }
+
+  isAccountBlocked (video: Video) {
+    return video.blockedOwner
+  }
+
+  isServerBlocked (video: Video) {
+    return video.blockedServer
+  }
+
+  isVideoBlocked (video: Video) {
+    return video.blacklisted
+  }
+
   protected reloadData () {
     this.selectedVideos = []
 
index e2dd44bf7cb4fb0e307795dcab6d2f5881505be4..33b5a47a0d23f8c9ff647fbbd97515717fb5375e 100644 (file)
@@ -24,5 +24,5 @@
 
 <div class="alert alert-danger" *ngIf="video?.blacklisted">
   <div class="blocked-label" i18n>This video is blocked.</div>
-  {{ video.blockedReason }}
+  {{ video.blacklistedReason }}
 </div>
index ba64d4feca8e4861b8644b96875d7fa39af8b4f1..d03b0961099480edc83bfc1e5927901eed276eec 100644 (file)
@@ -85,7 +85,7 @@ export class VideosListCommonPageComponent implements OnInit, OnDestroy, Disable
   getSyndicationItems (filters: VideoFilters) {
     const result = filters.toVideosAPIObject()
 
-    return this.videoService.getVideoFeedUrls(result.sort, result.filter)
+    return this.videoService.getVideoFeedUrls(result.sort, result.isLocal)
   }
 
   onFiltersChanged (filters: VideoFilters) {
index ab640d3482ff122ad5f1c0c5354acc95971429ff..a959b336d18b555619dbdddbf9b29b85cacefbd1 100644 (file)
@@ -7,7 +7,6 @@ import {
   ContainerMarkupData,
   EmbedMarkupData,
   PlaylistMiniatureMarkupData,
-  VideoFilter,
   VideoMiniatureMarkupData,
   VideosListMarkupData
 } from '@shared/models'
@@ -193,7 +192,7 @@ export class CustomMarkupService {
 
       isLive: this.buildBoolean(data.isLive),
 
-      filter: this.buildBoolean(data.onlyLocal) ? 'local' as VideoFilter : undefined
+      isLocal: this.buildBoolean(data.onlyLocal) ? true : undefined
     }
 
     this.dynamicElementService.setModel(component, model)
index 856e4368183441568f9bec6d2ead7fd024b9f958..0e4d5fb12afc55b9ca3fb94ae148f4fe9354853a 100644 (file)
@@ -1,7 +1,7 @@
 import { finalize } from 'rxjs/operators'
 import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
 import { AuthService, Notifier } from '@app/core'
-import { VideoFilter, VideoSortField } from '@shared/models'
+import { VideoSortField } from '@shared/models'
 import { Video, VideoService } from '../../shared-main'
 import { MiniatureDisplayOptions } from '../../shared-video-miniature'
 import { CustomMarkupComponent } from './shared'
@@ -21,7 +21,7 @@ export class VideosListMarkupComponent implements CustomMarkupComponent, OnInit
   @Input() languageOneOf: string[]
   @Input() count: number
   @Input() onlyDisplayTitle: boolean
-  @Input() filter: VideoFilter
+  @Input() isLocal: boolean
   @Input() isLive: boolean
   @Input() maxRows: number
   @Input() channelHandle: string
@@ -86,7 +86,7 @@ export class VideosListMarkupComponent implements CustomMarkupComponent, OnInit
       },
       categoryOneOf: this.categoryOneOf,
       languageOneOf: this.languageOneOf,
-      filter: this.filter,
+      isLocal: this.isLocal,
       isLive: this.isLive,
       sort: this.sort as VideoSortField,
       account: { nameWithHost: this.accountHandle },
index 10caec014ffb81d91e1ad86d278da32f361ff151..699eac7f1f3576d49f1b65084e4bedb686feeb56 100644 (file)
@@ -65,8 +65,12 @@ export class Video implements VideoServerModel {
   waitTranscoding?: boolean
   state?: VideoConstant<VideoState>
   scheduledUpdate?: VideoScheduleUpdate
+
   blacklisted?: boolean
-  blockedReason?: string
+  blacklistedReason?: string
+
+  blockedOwner?: boolean
+  blockedServer?: boolean
 
   account: {
     id: number
@@ -163,7 +167,10 @@ export class Video implements VideoServerModel {
     if (this.state) this.state.label = peertubeTranslate(this.state.label, translations)
 
     this.blacklisted = hash.blacklisted
-    this.blockedReason = hash.blacklistedReason
+    this.blacklistedReason = hash.blacklistedReason
+
+    this.blockedOwner = hash.blockedOwner
+    this.blockedServer = hash.blockedServer
 
     this.userHistory = hash.userHistory
 
index 9e3aa1e6a0093847d7c9be53838ce3d3ecf0bc91..0a3a51b0c62a3bdefaf0ef6747fb69666eee8d43 100644 (file)
@@ -18,7 +18,7 @@ import {
   VideoConstant,
   VideoDetails as VideoDetailsServerModel,
   VideoFileMetadata,
-  VideoFilter,
+  VideoInclude,
   VideoPrivacy,
   VideoSortField,
   VideoUpdate
@@ -34,11 +34,13 @@ import { Video } from './video.model'
 export type CommonVideoParams = {
   videoPagination?: ComponentPaginationLight
   sort: VideoSortField | SortMeta
-  filter?: VideoFilter
+  include?: VideoInclude
+  isLocal?: boolean
   categoryOneOf?: number[]
   languageOneOf?: string[]
   isLive?: boolean
   skipCount?: boolean
+
   // FIXME: remove?
   nsfwPolicy?: NSFWPolicyType
   nsfw?: BooleanBothQuery
@@ -202,12 +204,14 @@ export class VideoService {
   }
 
   getAdminVideos (
-    parameters: Omit<CommonVideoParams, 'filter'> & { pagination: RestPagination, search?: string }
+    parameters: CommonVideoParams & { pagination: RestPagination, search?: string }
   ): Observable<ResultList<Video>> {
     const { pagination, search } = parameters
 
+    const include = VideoInclude.BLACKLISTED | VideoInclude.BLOCKED_OWNER | VideoInclude.HIDDEN_PRIVACY | VideoInclude.NOT_PUBLISHED_STATE
+
     let params = new HttpParams()
-    params = this.buildCommonVideosParams({ params, ...parameters })
+    params = this.buildCommonVideosParams({ params, include, ...parameters })
 
     params = params.set('start', pagination.start.toString())
                    .set('count', pagination.count.toString())
@@ -216,8 +220,6 @@ export class VideoService {
       params = this.buildAdminParamsFromSearch(search, params)
     }
 
-    if (!params.has('filter')) params = params.set('filter', 'all')
-
     return this.authHttp
                .get<ResultList<Video>>(VideoService.BASE_VIDEO_URL, { params })
                .pipe(
@@ -266,10 +268,10 @@ export class VideoService {
     return feeds
   }
 
-  getVideoFeedUrls (sort: VideoSortField, filter?: VideoFilter, categoryOneOf?: number[]) {
+  getVideoFeedUrls (sort: VideoSortField, isLocal: boolean, categoryOneOf?: number[]) {
     let params = this.restService.addRestGetParams(new HttpParams(), undefined, sort)
 
-    if (filter) params = params.set('filter', filter)
+    if (isLocal) params = params.set('isLocal', isLocal)
 
     if (categoryOneOf) {
       for (const c of categoryOneOf) {
@@ -425,7 +427,8 @@ export class VideoService {
       params,
       videoPagination,
       sort,
-      filter,
+      isLocal,
+      include,
       categoryOneOf,
       languageOneOf,
       skipCount,
@@ -440,9 +443,10 @@ export class VideoService {
 
     let newParams = this.restService.addRestGetParams(params, pagination, sort)
 
-    if (filter) newParams = newParams.set('filter', filter)
     if (skipCount) newParams = newParams.set('skipCount', skipCount + '')
 
+    if (isLocal) newParams = newParams.set('isLocal', isLocal)
+    if (include) newParams = newParams.set('include', include)
     if (isLive) newParams = newParams.set('isLive', isLive)
     if (nsfw) newParams = newParams.set('nsfw', nsfw)
     if (nsfwPolicy) newParams = newParams.set('nsfw', this.nsfwPolicyToParam(nsfwPolicy))
@@ -454,13 +458,9 @@ export class VideoService {
 
   private buildAdminParamsFromSearch (search: string, params: HttpParams) {
     const filters = this.restService.parseQueryStringFilter(search, {
-      filter: {
-        prefix: 'local:',
-        handler: v => {
-          if (v === 'true') return 'all-local'
-
-          return 'all'
-        }
+      isLocal: {
+        prefix: 'isLocal:',
+        isBoolean: true
       }
     })
 
index f6c29dcfafafdc9c769dc12ff63c752c47ff3f36..a6180dd14680872064536f83a395c5f948dfb2b8 100644 (file)
@@ -61,7 +61,7 @@ export class VideoBlockComponent extends FormReactive implements OnInit {
             this.hide()
 
             this.video.blacklisted = true
-            this.video.blockedReason = reason
+            this.video.blacklistedReason = reason
 
             this.videoBlocked.emit()
           },
index 2ba091438bf003111c6803f29f03c634954cc381..feac79d4e82c1c2da0a84a4eaf20937737c3b718 100644 (file)
@@ -188,7 +188,7 @@ export class VideoActionsDropdownComponent implements OnChanges {
             this.notifier.success($localize`Video ${this.video.name} unblocked.`)
 
             this.video.blacklisted = false
-            this.video.blockedReason = null
+            this.video.blacklistedReason = null
 
             this.videoUnblocked.emit()
           },
index 920dc826c7018fd20555006b6e4f96b4f9607d0c..5ad7cf3f7a2bc1281517d0ba94c144d478b7c290 100644 (file)
@@ -1,6 +1,6 @@
 import { intoArray, toBoolean } from '@app/helpers'
 import { AttributesOnly } from '@shared/core-utils'
-import { BooleanBothQuery, NSFWPolicyType, VideoFilter, VideoSortField } from '@shared/models'
+import { BooleanBothQuery, NSFWPolicyType, VideoInclude, VideoSortField } from '@shared/models'
 
 type VideoFiltersKeys = {
   [ id in keyof AttributesOnly<VideoFilters> ]: any
@@ -196,14 +196,15 @@ export class VideoFilters {
   }
 
   toVideosAPIObject () {
-    let filter: VideoFilter
-
-    if (this.scope === 'local' && this.allVideos) {
-      filter = 'all-local'
-    } else if (this.scope === 'federated' && this.allVideos) {
-      filter = 'all'
-    } else if (this.scope === 'local') {
-      filter = 'local'
+    let isLocal: boolean
+    let include: VideoInclude
+
+    if (this.scope === 'local') {
+      isLocal = true
+    }
+
+    if (this.allVideos) {
+      include = VideoInclude.NOT_PUBLISHED_STATE | VideoInclude.HIDDEN_PRIVACY
     }
 
     let isLive: boolean
@@ -216,7 +217,8 @@ export class VideoFilters {
       languageOneOf: this.languageOneOf,
       categoryOneOf: this.categoryOneOf,
       search: this.search,
-      filter,
+      isLocal,
+      include,
       isLive
     }
   }
index b12495f9077a2d35136544c7c34f5e8a739f157c..30483831a64cc41b4bcd286457c2fb51f33dae2d 100644 (file)
@@ -55,7 +55,7 @@
 
       <div *ngIf="displayOptions.blacklistInfo && video.blacklisted" class="video-info-blocked">
         <span class="blocked-label" i18n>Blocked</span>
-        <span class="blocked-reason" *ngIf="video.blockedReason">{{ video.blockedReason }}</span>
+        <span class="blocked-reason" *ngIf="video.blacklistedReason">{{ video.blacklistedReason }}</span>
       </div>
 
       <div i18n *ngIf="displayOptions.nsfw && video.nsfw" class="video-info-nsfw">
index 8eb880d59a8a1f032e016d7b8599de66c2154e8d..44edffe38381d268bd13a2978fd58785da517ab0 100644 (file)
@@ -2,6 +2,7 @@ import express from 'express'
 import { pickCommonVideoQuery } from '@server/helpers/query'
 import { ActorFollowModel } from '@server/models/actor/actor-follow'
 import { getServerActor } from '@server/models/application/application'
+import { guessAdditionalAttributesFromQuery } from '@server/models/video/formatter/video-format-utils'
 import { buildNSFWFilter, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils'
 import { getFormattedObjects } from '../../helpers/utils'
 import { JobQueue } from '../../lib/job-queue'
@@ -169,17 +170,24 @@ async function listAccountPlaylists (req: express.Request, res: express.Response
 }
 
 async function listAccountVideos (req: express.Request, res: express.Response) {
+  const serverActor = await getServerActor()
+
   const account = res.locals.account
-  const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined
+
+  const displayOnlyForFollower = isUserAbleToSearchRemoteURI(res)
+    ? null
+    : {
+      actorId: serverActor.id,
+      orLocalVideos: true
+    }
+
   const countVideos = getCountVideos(req)
   const query = pickCommonVideoQuery(req.query)
 
   const apiOptions = await Hooks.wrapObject({
     ...query,
 
-    followerActorId,
-    search: req.query.search,
-    includeLocalVideos: true,
+    displayOnlyForFollower,
     nsfw: buildNSFWFilter(res, query.nsfw),
     withFiles: false,
     accountId: account.id,
@@ -193,7 +201,7 @@ async function listAccountVideos (req: express.Request, res: express.Response) {
     'filter:api.accounts.videos.list.result'
   )
 
-  return res.json(getFormattedObjects(resultList.data, resultList.total))
+  return res.json(getFormattedObjects(resultList.data, resultList.total, guessAdditionalAttributesFromQuery(query)))
 }
 
 async function listAccountRatings (req: express.Request, res: express.Response) {
index 5b16232e2a0399c35545c3a47f029dc5b300158b..68626a5081d60e9119731725f6bec17ab90ceb0f 100644 (file)
@@ -8,6 +8,7 @@ import { buildNSFWFilter } from '../../helpers/express-utils'
 import { MEMOIZE_TTL, OVERVIEWS } from '../../initializers/constants'
 import { asyncMiddleware, optionalAuthenticate, videosOverviewValidator } from '../../middlewares'
 import { TagModel } from '../../models/video/tag'
+import { getServerActor } from '@server/models/application/application'
 
 const overviewsRouter = express.Router()
 
@@ -109,11 +110,16 @@ async function getVideos (
   res: express.Response,
   where: { videoChannelId?: number, tagsOneOf?: string[], categoryOneOf?: number[] }
 ) {
+  const serverActor = await getServerActor()
+
   const query = await Hooks.wrapObject({
     start: 0,
     count: 12,
     sort: '-createdAt',
-    includeLocalVideos: true,
+    displayOnlyForFollower: {
+      actorId: serverActor.id,
+      orLocalVideos: true
+    },
     nsfw: buildNSFWFilter(res),
     user: res.locals.oauth ? res.locals.oauth.token.User : undefined,
     withFiles: false,
index 90946cb7499c21fc0d6ce6b2503a481867e2efb0..6db70acdfdb00d2ce2479d3977c0bb45f49008b9 100644 (file)
@@ -7,6 +7,8 @@ import { WEBSERVER } from '@server/initializers/constants'
 import { getOrCreateAPVideo } from '@server/lib/activitypub/videos'
 import { Hooks } from '@server/lib/plugins/hooks'
 import { buildMutedForSearchIndex, isSearchIndexSearch, isURISearch } from '@server/lib/search'
+import { getServerActor } from '@server/models/application/application'
+import { guessAdditionalAttributesFromQuery } from '@server/models/video/formatter/video-format-utils'
 import { HttpStatusCode, ResultList, Video } from '@shared/models'
 import { VideosSearchQueryAfterSanitize } from '../../../../shared/models/search'
 import { buildNSFWFilter, isUserAbleToSearchRemoteURI } from '../../../helpers/express-utils'
@@ -100,11 +102,15 @@ async function searchVideosIndex (query: VideosSearchQueryAfterSanitize, res: ex
 }
 
 async function searchVideosDB (query: VideosSearchQueryAfterSanitize, res: express.Response) {
+  const serverActor = await getServerActor()
+
   const apiOptions = await Hooks.wrapObject({
     ...query,
 
-    includeLocalVideos: true,
-    filter: query.filter,
+    displayOnlyForFollower: {
+      actorId: serverActor.id,
+      orLocalVideos: true
+    },
 
     nsfw: buildNSFWFilter(res, query.nsfw),
     user: res.locals.oauth
@@ -118,7 +124,7 @@ async function searchVideosDB (query: VideosSearchQueryAfterSanitize, res: expre
     'filter:api.search.videos.local.list.result'
   )
 
-  return res.json(getFormattedObjects(resultList.data, resultList.total))
+  return res.json(getFormattedObjects(resultList.data, resultList.total, guessAdditionalAttributesFromQuery(query)))
 }
 
 async function searchVideoURI (url: string, res: express.Response) {
index b2b4416734a8e35a055a9538fe2f5da4f07f0042..d9637818030e962d102302e5bc87ba0d4194d083 100644 (file)
@@ -2,6 +2,7 @@ import 'multer'
 import express from 'express'
 import { pickCommonVideoQuery } from '@server/helpers/query'
 import { sendUndoFollow } from '@server/lib/activitypub/send'
+import { guessAdditionalAttributesFromQuery } from '@server/models/video/formatter/video-format-utils'
 import { VideoChannelModel } from '@server/models/video/video-channel'
 import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
 import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils'
@@ -175,13 +176,15 @@ async function getUserSubscriptionVideos (req: express.Request, res: express.Res
   const resultList = await VideoModel.listForApi({
     ...query,
 
-    includeLocalVideos: false,
+    displayOnlyForFollower: {
+      actorId: user.Account.Actor.id,
+      orLocalVideos: false
+    },
     nsfw: buildNSFWFilter(res, query.nsfw),
     withFiles: false,
-    followerActorId: user.Account.Actor.id,
     user,
     countVideos
   })
 
-  return res.json(getFormattedObjects(resultList.data, resultList.total))
+  return res.json(getFormattedObjects(resultList.data, resultList.total, guessAdditionalAttributesFromQuery(query)))
 }
index 7bf7a68c925f6498eaa6ab2e32a6f2b4981628d0..f9c1a405d907038ca81ef7e5e9d8102a85b7af3d 100644 (file)
@@ -3,6 +3,7 @@ import { pickCommonVideoQuery } from '@server/helpers/query'
 import { Hooks } from '@server/lib/plugins/hooks'
 import { ActorFollowModel } from '@server/models/actor/actor-follow'
 import { getServerActor } from '@server/models/application/application'
+import { guessAdditionalAttributesFromQuery } from '@server/models/video/formatter/video-format-utils'
 import { MChannelBannerAccountDefault } from '@server/types/models'
 import { ActorImageType, VideoChannelCreate, VideoChannelUpdate } from '../../../shared'
 import { HttpStatusCode } from '../../../shared/models/http/http-error-codes'
@@ -327,16 +328,24 @@ async function listVideoChannelPlaylists (req: express.Request, res: express.Res
 }
 
 async function listVideoChannelVideos (req: express.Request, res: express.Response) {
+  const serverActor = await getServerActor()
+
   const videoChannelInstance = res.locals.videoChannel
-  const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined
+
+  const displayOnlyForFollower = isUserAbleToSearchRemoteURI(res)
+    ? null
+    : {
+      actorId: serverActor.id,
+      orLocalVideos: true
+    }
+
   const countVideos = getCountVideos(req)
   const query = pickCommonVideoQuery(req.query)
 
   const apiOptions = await Hooks.wrapObject({
     ...query,
 
-    followerActorId,
-    includeLocalVideos: true,
+    displayOnlyForFollower,
     nsfw: buildNSFWFilter(res, query.nsfw),
     withFiles: false,
     videoChannelId: videoChannelInstance.id,
@@ -350,7 +359,7 @@ async function listVideoChannelVideos (req: express.Request, res: express.Respon
     'filter:api.video-channels.videos.list.result'
   )
 
-  return res.json(getFormattedObjects(resultList.data, resultList.total))
+  return res.json(getFormattedObjects(resultList.data, resultList.total, guessAdditionalAttributesFromQuery(query)))
 }
 
 async function listVideoChannelFollowers (req: express.Request, res: express.Response) {
index c0c77f3f74d0d068863038f2cc77ac5281707a77..821ed7ff31c327e5cc1b5325a5acaf0aaf03692c 100644 (file)
@@ -5,6 +5,7 @@ import { doJSONRequest } from '@server/helpers/requests'
 import { LiveManager } from '@server/lib/live'
 import { openapiOperationDoc } from '@server/middlewares/doc'
 import { getServerActor } from '@server/models/application/application'
+import { guessAdditionalAttributesFromQuery } from '@server/models/video/formatter/video-format-utils'
 import { MVideoAccountLight } from '@server/types/models'
 import { HttpStatusCode } from '../../../../shared/models'
 import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
@@ -211,13 +212,18 @@ async function getVideoFileMetadata (req: express.Request, res: express.Response
 }
 
 async function listVideos (req: express.Request, res: express.Response) {
+  const serverActor = await getServerActor()
+
   const query = pickCommonVideoQuery(req.query)
   const countVideos = getCountVideos(req)
 
   const apiOptions = await Hooks.wrapObject({
     ...query,
 
-    includeLocalVideos: true,
+    displayOnlyForFollower: {
+      actorId: serverActor.id,
+      orLocalVideos: true
+    },
     nsfw: buildNSFWFilter(res, query.nsfw),
     withFiles: false,
     user: res.locals.oauth ? res.locals.oauth.token.User : undefined,
@@ -230,7 +236,7 @@ async function listVideos (req: express.Request, res: express.Response) {
     'filter:api.videos.list.result'
   )
 
-  return res.json(getFormattedObjects(resultList.data, resultList.total))
+  return res.json(getFormattedObjects(resultList.data, resultList.total, guessAdditionalAttributesFromQuery(query)))
 }
 
 async function removeVideo (_req: express.Request, res: express.Response) {
index 63db345bfe0ce0c866fa0d07a7bf366e6a6aa7c4..9f03de7e8d152ab5f400e1304ee3cd29a0dd1a1d 100644 (file)
@@ -1,3 +1,4 @@
+import { getServerActor } from '@server/models/application/application'
 import express from 'express'
 import { truncate } from 'lodash'
 import { SitemapStream, streamToPromise } from 'sitemap'
@@ -63,13 +64,18 @@ async function getSitemapAccountUrls () {
 }
 
 async function getSitemapLocalVideoUrls () {
+  const serverActor = await getServerActor()
+
   const { data } = await VideoModel.listForApi({
     start: 0,
     count: undefined,
     sort: 'createdAt',
-    includeLocalVideos: true,
+    displayOnlyForFollower: {
+      actorId: serverActor.id,
+      orLocalVideos: true
+    },
+    isLocal: true,
     nsfw: buildNSFWFilter(),
-    filter: 'local',
     withFiles: false,
     countVideos: false
   })
index 5ac2e43a1c65794e84767baabbdcb52544491540..1f6aebac3b8a53d57572030fefa1f7cae2bb5510 100644 (file)
@@ -1,7 +1,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 { VideoFilter } from '../../shared/models/videos/video-query.type'
 import { buildNSFWFilter } from '../helpers/express-utils'
 import { CONFIG } from '../initializers/config'
 import { FEEDS, PREVIEWS_SIZE, ROUTE_CACHE_LIFETIME, WEBSERVER } from '../initializers/constants'
@@ -160,13 +160,18 @@ async function generateVideoFeed (req: express.Request, res: express.Response) {
     videoChannelId: videoChannel ? videoChannel.id : null
   }
 
+  const server = await getServerActor()
   const { data } = await VideoModel.listForApi({
     start,
     count: FEEDS.COUNT,
     sort: req.query.sort,
-    includeLocalVideos: true,
+    displayOnlyForFollower: {
+      actorId: server.id,
+      orLocalVideos: true
+    },
     nsfw,
-    filter: req.query.filter as VideoFilter,
+    isLocal: req.query.isLocal,
+    include: req.query.include,
     withFiles: true,
     countVideos: false,
     ...options
@@ -196,14 +201,18 @@ async function generateVideoFeedForSubscriptions (req: express.Request, res: exp
     start,
     count: FEEDS.COUNT,
     sort: req.query.sort,
-    includeLocalVideos: false,
     nsfw,
-    filter: req.query.filter as VideoFilter,
+
+    isLocal: req.query.isLocal,
+    include: req.query.include,
 
     withFiles: true,
     countVideos: false,
 
-    followerActorId: res.locals.user.Account.Actor.id,
+    displayOnlyForFollower: {
+      actorId: res.locals.user.Account.Actor.id,
+      orLocalVideos: false
+    },
     user: res.locals.user
   })
 
index c3604fbad0dd2138622aba15e988aa01c0cca52d..1d56ade6f510c1801966d997b4047c21a24f96ce 100644 (file)
@@ -2,6 +2,7 @@ import { UploadFilesForCheck } from 'express'
 import { values } from 'lodash'
 import magnetUtil from 'magnet-uri'
 import validator from 'validator'
+import { VideoInclude } from '@shared/models'
 import { VideoFilter, VideoPrivacy, VideoRateType } from '../../../shared'
 import {
   CONSTRAINTS_FIELDS,
@@ -21,6 +22,10 @@ function isVideoFilterValid (filter: VideoFilter) {
   return filter === 'local' || filter === 'all-local' || filter === 'all'
 }
 
+function isVideoIncludeValid (include: VideoInclude) {
+  return exists(include) && validator.isInt('' + include)
+}
+
 function isVideoCategoryValid (value: any) {
   return value === null || VIDEO_CATEGORIES[value] !== undefined
 }
@@ -146,6 +151,7 @@ export {
   isVideoOriginallyPublishedAtValid,
   isVideoMagnetUriValid,
   isVideoStateValid,
+  isVideoIncludeValid,
   isVideoViewsValid,
   isVideoRatingTypeValid,
   isVideoFileExtnameValid,
index e711b15f2cde7bcd5e15545b5c5d8c49d2e76d86..79cf076d13c99c4629cb9c273dd7771b79a2a3ee 100644 (file)
@@ -18,8 +18,10 @@ function pickCommonVideoQuery (query: VideosCommonQueryAfterSanitize) {
     'languageOneOf',
     'tagsOneOf',
     'tagsAllOf',
-    'filter',
-    'skipCount'
+    'isLocal',
+    'include',
+    'skipCount',
+    'search'
   ])
 }
 
@@ -29,7 +31,6 @@ function pickSearchVideoQuery (query: VideosSearchQueryAfterSanitize) {
 
     ...pick(query, [
       'searchTarget',
-      'search',
       'host',
       'startDate',
       'endDate',
index e486887a7e284f21657c6bb643935cc6272a47c6..44233b653620ca71860248b0bc0c3aae89278ddc 100644 (file)
@@ -7,6 +7,7 @@ import { isAbleToUploadVideo } from '@server/lib/user'
 import { getServerActor } from '@server/models/application/application'
 import { ExpressPromiseHandler } from '@server/types/express'
 import { MUserAccountId, MVideoFullLight } from '@server/types/models'
+import { VideoInclude } from '@shared/models'
 import { ServerErrorCode, UserRight, VideoPrivacy } from '../../../../shared'
 import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
 import {
@@ -30,6 +31,7 @@ import {
   isVideoFileSizeValid,
   isVideoFilterValid,
   isVideoImage,
+  isVideoIncludeValid,
   isVideoLanguageValid,
   isVideoLicenceValid,
   isVideoNameValid,
@@ -487,6 +489,13 @@ const commonVideosFiltersValidator = [
   query('filter')
     .optional()
     .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'),
+  query('include')
+    .optional()
+    .custom(isVideoIncludeValid).withMessage('Should have a valid include attribute'),
+  query('isLocal')
+    .optional()
+    .customSanitizer(toBooleanOrNull)
+    .custom(isBooleanValid).withMessage('Should have a valid local boolean'),
   query('skipCount')
     .optional()
     .customSanitizer(toBooleanOrNull)
@@ -500,11 +509,23 @@ const commonVideosFiltersValidator = [
 
     if (areValidationErrors(req, res)) return
 
-    const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
-    if (
-      (req.query.filter === 'all-local' || req.query.filter === 'all') &&
-      (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)
-    ) {
+    // FIXME: deprecated in 4.0, to remove
+    {
+      if (req.query.filter === 'all-local') {
+        req.query.include = VideoInclude.NOT_PUBLISHED_STATE | VideoInclude.HIDDEN_PRIVACY
+        req.query.isLocal = true
+      } else if (req.query.filter === 'all') {
+        req.query.include = VideoInclude.NOT_PUBLISHED_STATE | VideoInclude.HIDDEN_PRIVACY
+      } else if (req.query.filter === 'local') {
+        req.query.isLocal = true
+      }
+
+      req.query.filter = undefined
+    }
+
+    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.'
index 37194a119bb85b6ff1ecbc8d3919c25e8f1c7f4e..056ec6857597edeb16030fb61918655f44cffa02 100644 (file)
@@ -228,10 +228,10 @@ export class AccountModel extends Model<Partial<AttributesOnly<AccountModel>>> {
       name: 'targetAccountId',
       allowNull: false
     },
-    as: 'BlockedAccounts',
+    as: 'BlockedBy',
     onDelete: 'CASCADE'
   })
-  BlockedAccounts: AccountBlocklistModel[]
+  BlockedBy: AccountBlocklistModel[]
 
   @BeforeDestroy
   static async sendDeleteIfOwned (instance: AccountModel, options) {
@@ -457,6 +457,6 @@ export class AccountModel extends Model<Partial<AttributesOnly<AccountModel>>> {
   }
 
   isBlocked () {
-    return this.BlockedAccounts && this.BlockedAccounts.length !== 0
+    return this.BlockedBy && this.BlockedBy.length !== 0
   }
 }
index 0d3c092e040d9536838017f1ddbbaa3585276ab5..edbe92f73fcc832402e1401c6466a168e610b4b6 100644 (file)
@@ -50,7 +50,7 @@ export class ServerModel extends Model<Partial<AttributesOnly<ServerModel>>> {
     },
     onDelete: 'CASCADE'
   })
-  BlockedByAccounts: ServerBlocklistModel[]
+  BlockedBy: ServerBlocklistModel[]
 
   static load (id: number, transaction?: Transaction): Promise<MServer> {
     const query = {
@@ -81,7 +81,7 @@ export class ServerModel extends Model<Partial<AttributesOnly<ServerModel>>> {
   }
 
   isBlocked () {
-    return this.BlockedByAccounts && this.BlockedByAccounts.length !== 0
+    return this.BlockedBy && this.BlockedBy.length !== 0
   }
 
   toFormattedJSON (this: MServerFormattable) {
index e3dc4a062d77f0d8640b989229eea8ac6e8bd10f..d633cc9d5f1f87d23338749c0e3534828ad92fca 100644 (file)
@@ -4,6 +4,7 @@ import { MUserAccountId, MUserId } from '@server/types/models'
 import { AttributesOnly } from '@shared/core-utils'
 import { VideoModel } from '../video/video'
 import { UserModel } from './user'
+import { getServerActor } from '../application/application'
 
 @Table({
   tableName: 'userVideoHistory',
@@ -56,14 +57,19 @@ export class UserVideoHistoryModel extends Model<Partial<AttributesOnly<UserVide
   })
   User: UserModel
 
-  static listForApi (user: MUserAccountId, start: number, count: number, search?: string) {
+  static async listForApi (user: MUserAccountId, start: number, count: number, search?: string) {
+    const serverActor = await getServerActor()
+
     return VideoModel.listForApi({
       start,
       count,
       search,
       sort: '-"userVideoHistory"."updatedAt"',
       nsfw: null, // All
-      includeLocalVideos: true,
+      displayOnlyForFollower: {
+        actorId: serverActor.id,
+        orLocalVideos: true
+      },
       withFiles: false,
       user,
       historyOfUser: user
index 0cbad568452e23a03a437047deb530f594ea427d..5dc2c2f1b947c754dc6c4b5c0526d67e0a16b1df 100644 (file)
@@ -1,9 +1,10 @@
 import { uuidToShort } from '@server/helpers/uuid'
 import { generateMagnetUri } from '@server/helpers/webtorrent'
 import { getLocalVideoFileMetadataUrl } from '@server/lib/video-urls'
+import { VideosCommonQueryAfterSanitize } from '@shared/models'
 import { VideoFile } from '@shared/models/videos/video-file.model'
 import { ActivityTagObject, ActivityUrlObject, VideoObject } from '../../../../shared/models/activitypub/objects'
-import { Video, VideoDetails } from '../../../../shared/models/videos'
+import { Video, VideoDetails, VideoInclude } from '../../../../shared/models/videos'
 import { VideoStreamingPlaylist } from '../../../../shared/models/videos/video-streaming-playlist.model'
 import { isArray } from '../../../helpers/custom-validators/misc'
 import {
@@ -22,6 +23,7 @@ import {
   getLocalVideoSharesActivityPubUrl
 } from '../../../lib/activitypub/url'
 import {
+  MServer,
   MStreamingPlaylistRedundanciesOpt,
   MVideo,
   MVideoAP,
@@ -34,15 +36,31 @@ import { VideoCaptionModel } from '../video-caption'
 
 export type VideoFormattingJSONOptions = {
   completeDescription?: boolean
-  additionalAttributes: {
+
+  additionalAttributes?: {
     state?: boolean
     waitTranscoding?: boolean
     scheduledUpdate?: boolean
     blacklistInfo?: boolean
+    blockedOwner?: boolean
   }
 }
 
-function videoModelToFormattedJSON (video: MVideoFormattable, options?: VideoFormattingJSONOptions): Video {
+function guessAdditionalAttributesFromQuery (query: VideosCommonQueryAfterSanitize): VideoFormattingJSONOptions {
+  if (!query || !query.include) return {}
+
+  return {
+    additionalAttributes: {
+      state: !!(query.include & VideoInclude.NOT_PUBLISHED_STATE),
+      waitTranscoding: !!(query.include & VideoInclude.NOT_PUBLISHED_STATE),
+      scheduledUpdate: !!(query.include & VideoInclude.NOT_PUBLISHED_STATE),
+      blacklistInfo: !!(query.include & VideoInclude.BLACKLISTED),
+      blockedOwner: !!(query.include & VideoInclude.BLOCKED_OWNER)
+    }
+  }
+}
+
+function videoModelToFormattedJSON (video: MVideoFormattable, options: VideoFormattingJSONOptions = {}): Video {
   const userHistory = isArray(video.UserVideoHistories) ? video.UserVideoHistories[0] : undefined
 
   const videoObject: Video = {
@@ -101,29 +119,35 @@ function videoModelToFormattedJSON (video: MVideoFormattable, options?: VideoFor
     pluginData: (video as any).pluginData
   }
 
-  if (options) {
-    if (options.additionalAttributes.state === true) {
-      videoObject.state = {
-        id: video.state,
-        label: getStateLabel(video.state)
-      }
+  const add = options.additionalAttributes
+  if (add?.state === true) {
+    videoObject.state = {
+      id: video.state,
+      label: getStateLabel(video.state)
     }
+  }
 
-    if (options.additionalAttributes.waitTranscoding === true) {
-      videoObject.waitTranscoding = video.waitTranscoding
-    }
+  if (add?.waitTranscoding === true) {
+    videoObject.waitTranscoding = video.waitTranscoding
+  }
 
-    if (options.additionalAttributes.scheduledUpdate === true && video.ScheduleVideoUpdate) {
-      videoObject.scheduledUpdate = {
-        updateAt: video.ScheduleVideoUpdate.updateAt,
-        privacy: video.ScheduleVideoUpdate.privacy || undefined
-      }
+  if (add?.scheduledUpdate === true && video.ScheduleVideoUpdate) {
+    videoObject.scheduledUpdate = {
+      updateAt: video.ScheduleVideoUpdate.updateAt,
+      privacy: video.ScheduleVideoUpdate.privacy || undefined
     }
+  }
 
-    if (options.additionalAttributes.blacklistInfo === true) {
-      videoObject.blacklisted = !!video.VideoBlacklist
-      videoObject.blacklistedReason = video.VideoBlacklist ? video.VideoBlacklist.reason : null
-    }
+  if (add?.blacklistInfo === true) {
+    videoObject.blacklisted = !!video.VideoBlacklist
+    videoObject.blacklistedReason = video.VideoBlacklist ? video.VideoBlacklist.reason : null
+  }
+
+  if (add?.blockedOwner === true) {
+    videoObject.blockedOwner = video.VideoChannel.Account.isBlocked()
+
+    const server = video.VideoChannel.Account.Actor.Server as MServer
+    videoObject.blockedServer = !!(server?.isBlocked())
   }
 
   return videoObject
@@ -464,6 +488,8 @@ export {
   videoModelToActivityPubObject,
   getActivityStreamDuration,
 
+  guessAdditionalAttributesFromQuery,
+
   getCategoryLabel,
   getLicenceLabel,
   getLanguageLabel,
index 0d7e645744c397a7d9d270f4b867b3c115a2cce3..29827db2ad1f98e1ff3d895e65315ea52a8ed917 100644 (file)
@@ -1,3 +1,5 @@
+import { createSafeIn } from '@server/models/utils'
+import { MUserAccountId } from '@server/types/models'
 import validator from 'validator'
 import { AbstractVideosQueryBuilder } from './abstract-videos-query-builder'
 import { VideoTables } from './video-tables'
@@ -188,6 +190,32 @@ export class AbstractVideosModelQueryBuilder extends AbstractVideosQueryBuilder
     }
   }
 
+  protected includeBlockedOwnerAndServer (serverAccountId: number, user?: MUserAccountId) {
+    const blockerIds = [ serverAccountId ]
+    if (user) blockerIds.push(user.Account.id)
+
+    const inClause = createSafeIn(this.sequelize, blockerIds)
+
+    this.addJoin(
+      'LEFT JOIN "accountBlocklist" AS "VideoChannel->Account->AccountBlocklist" ' +
+        'ON "VideoChannel->Account"."id" = "VideoChannel->Account->AccountBlocklist"."targetAccountId" ' +
+        'AND "VideoChannel->Account->AccountBlocklist"."accountId" IN (' + inClause + ')'
+    )
+
+    this.addJoin(
+      'LEFT JOIN "serverBlocklist" AS "VideoChannel->Account->Actor->Server->ServerBlocklist" ' +
+        'ON "VideoChannel->Account->Actor->Server->ServerBlocklist"."targetServerId" = "VideoChannel->Account->Actor"."serverId" ' +
+        'AND "VideoChannel->Account->Actor->Server->ServerBlocklist"."accountId" IN (' + inClause + ') '
+    )
+
+    this.attributes = {
+      ...this.attributes,
+
+      ...this.buildAttributesObject('VideoChannel->Account->AccountBlocklist', this.tables.getBlocklistAttributes()),
+      ...this.buildAttributesObject('VideoChannel->Account->Actor->Server->ServerBlocklist', this.tables.getBlocklistAttributes())
+    }
+  }
+
   protected includeScheduleUpdate () {
     this.addJoin(
       'LEFT OUTER JOIN "scheduleVideoUpdate" AS "ScheduleVideoUpdate" ON "video"."id" = "ScheduleVideoUpdate"."videoId"'
index 33a0181e9ec3f4331dc9e70ab37f9ee470cec9f7..0eac956610457089725a2edfb76764edec5fbe4d 100644 (file)
@@ -1,11 +1,14 @@
 
 import { AccountModel } from '@server/models/account/account'
+import { AccountBlocklistModel } from '@server/models/account/account-blocklist'
 import { ActorModel } from '@server/models/actor/actor'
 import { ActorImageModel } from '@server/models/actor/actor-image'
 import { VideoRedundancyModel } from '@server/models/redundancy/video-redundancy'
 import { ServerModel } from '@server/models/server/server'
+import { ServerBlocklistModel } from '@server/models/server/server-blocklist'
 import { TrackerModel } from '@server/models/server/tracker'
 import { UserVideoHistoryModel } from '@server/models/user/user-video-history'
+import { VideoInclude } from '@shared/models'
 import { ScheduleVideoUpdateModel } from '../../schedule-video-update'
 import { TagModel } from '../../tag'
 import { ThumbnailModel } from '../../thumbnail'
@@ -33,6 +36,8 @@ export class VideoModelBuilder {
   private thumbnailsDone: Set<any>
   private historyDone: Set<any>
   private blacklistDone: Set<any>
+  private accountBlocklistDone: Set<any>
+  private serverBlocklistDone: Set<any>
   private liveDone: Set<any>
   private redundancyDone: Set<any>
   private scheduleVideoUpdateDone: Set<any>
@@ -51,7 +56,14 @@ export class VideoModelBuilder {
 
   }
 
-  buildVideosFromRows (rows: SQLRow[], rowsWebTorrentFiles?: SQLRow[], rowsStreamingPlaylist?: SQLRow[]) {
+  buildVideosFromRows (options: {
+    rows: SQLRow[]
+    include?: VideoInclude
+    rowsWebTorrentFiles?: SQLRow[]
+    rowsStreamingPlaylist?: SQLRow[]
+  }) {
+    const { rows, rowsWebTorrentFiles, rowsStreamingPlaylist, include } = options
+
     this.reinit()
 
     for (const row of rows) {
@@ -77,6 +89,15 @@ export class VideoModelBuilder {
         this.setBlacklisted(row, videoModel)
         this.setScheduleVideoUpdate(row, videoModel)
         this.setLive(row, videoModel)
+      } else {
+        if (include & VideoInclude.BLACKLISTED) {
+          this.setBlacklisted(row, videoModel)
+        }
+
+        if (include & VideoInclude.BLOCKED_OWNER) {
+          this.setBlockedOwner(row, videoModel)
+          this.setBlockedServer(row, videoModel)
+        }
       }
     }
 
@@ -91,15 +112,18 @@ export class VideoModelBuilder {
     this.videoStreamingPlaylistMemo = {}
     this.videoFileMemo = {}
 
-    this.thumbnailsDone = new Set<number>()
-    this.historyDone = new Set<number>()
-    this.blacklistDone = new Set<number>()
-    this.liveDone = new Set<number>()
-    this.redundancyDone = new Set<number>()
-    this.scheduleVideoUpdateDone = new Set<number>()
+    this.thumbnailsDone = new Set()
+    this.historyDone = new Set()
+    this.blacklistDone = new Set()
+    this.liveDone = new Set()
+    this.redundancyDone = new Set()
+    this.scheduleVideoUpdateDone = new Set()
+
+    this.accountBlocklistDone = new Set()
+    this.serverBlocklistDone = new Set()
 
-    this.trackersDone = new Set<string>()
-    this.tagsDone = new Set<string>()
+    this.trackersDone = new Set()
+    this.tagsDone = new Set()
 
     this.videos = []
   }
@@ -162,6 +186,8 @@ export class VideoModelBuilder {
     const accountModel = new AccountModel(this.grab(row, this.tables.getAccountAttributes(), 'VideoChannel.Account'), this.buildOpts)
     accountModel.Actor = this.buildActor(row, 'VideoChannel.Account')
 
+    accountModel.BlockedBy = []
+
     channelModel.Account = accountModel
 
     videoModel.VideoChannel = channelModel
@@ -180,6 +206,8 @@ export class VideoModelBuilder {
       ? new ServerModel(this.grab(row, this.tables.getServerAttributes(), serverPrefix), this.buildOpts)
       : null
 
+    if (serverModel) serverModel.BlockedBy = []
+
     const actorModel = new ActorModel(this.grab(row, this.tables.getActorAttributes(), actorPrefix), this.buildOpts)
     actorModel.Avatar = avatarModel
     actorModel.Server = serverModel
@@ -297,6 +325,32 @@ export class VideoModelBuilder {
     this.blacklistDone.add(id)
   }
 
+  private setBlockedOwner (row: SQLRow, videoModel: VideoModel) {
+    const id = row['VideoChannel.Account.AccountBlocklist.id']
+    if (!id) return
+
+    const key = `${videoModel.id}-${id}`
+    if (this.accountBlocklistDone.has(key)) return
+
+    const attributes = this.grab(row, this.tables.getBlocklistAttributes(), 'VideoChannel.Account.AccountBlocklist')
+    videoModel.VideoChannel.Account.BlockedBy.push(new AccountBlocklistModel(attributes, this.buildOpts))
+
+    this.accountBlocklistDone.add(key)
+  }
+
+  private setBlockedServer (row: SQLRow, videoModel: VideoModel) {
+    const id = row['VideoChannel.Account.Actor.Server.ServerBlocklist.id']
+    if (!id || this.serverBlocklistDone.has(id)) return
+
+    const key = `${videoModel.id}-${id}`
+    if (this.serverBlocklistDone.has(key)) return
+
+    const attributes = this.grab(row, this.tables.getBlocklistAttributes(), 'VideoChannel.Account.Actor.Server.ServerBlocklist')
+    videoModel.VideoChannel.Account.Actor.Server.BlockedBy.push(new ServerBlocklistModel(attributes, this.buildOpts))
+
+    this.serverBlocklistDone.add(key)
+  }
+
   private setScheduleVideoUpdate (row: SQLRow, videoModel: VideoModel) {
     const id = row['ScheduleVideoUpdate.id']
     if (!id || this.scheduleVideoUpdateDone.has(id)) return
index 75823864d3568ef8ebf1534053c27bc3806faf6b..042b9d5daa2b1d8830f6d115ab1b7b78856bed54 100644 (file)
@@ -139,6 +139,10 @@ export class VideoTables {
     return [ 'id', 'reason', 'unfederated' ]
   }
 
+  getBlocklistAttributes () {
+    return [ 'id' ]
+  }
+
   getScheduleUpdateAttributes () {
     return [
       'id',
index f234e87783a104c6142fc93fcc31d63c4434a74a..d18ddae6781a8c66eeeac0d312b82ffb0383f9f5 100644 (file)
@@ -62,7 +62,11 @@ export class VideosModelGetQueryBuilder {
         : Promise.resolve(undefined)
     ])
 
-    const videos = this.videoModelBuilder.buildVideosFromRows(videoRows, webtorrentFilesRows, streamingPlaylistFilesRows)
+    const videos = this.videoModelBuilder.buildVideosFromRows({
+      rows: videoRows,
+      rowsWebTorrentFiles: webtorrentFilesRows,
+      rowsStreamingPlaylist: streamingPlaylistFilesRows
+    })
 
     if (videos.length > 1) {
       throw new Error('Video results is more than ')
index 7625c003d0fbc6cc4d9a90f3951e7d0798987c91..3eb547e75c85303688128833592652910d0edc80 100644 (file)
@@ -4,7 +4,7 @@ import { exists } from '@server/helpers/custom-validators/misc'
 import { WEBSERVER } from '@server/initializers/constants'
 import { buildDirectionAndField, createSafeIn } from '@server/models/utils'
 import { MUserAccountId, MUserId } from '@server/types/models'
-import { VideoFilter, VideoPrivacy, VideoState } from '@shared/models'
+import { VideoInclude, VideoPrivacy, VideoState } from '@shared/models'
 import { AbstractVideosQueryBuilder } from './shared/abstract-videos-query-builder'
 
 /**
@@ -13,21 +13,27 @@ import { AbstractVideosQueryBuilder } from './shared/abstract-videos-query-build
  *
  */
 
+export type DisplayOnlyForFollowerOptions = {
+  actorId: number
+  orLocalVideos: boolean
+}
+
 export type BuildVideosListQueryOptions = {
   attributes?: string[]
 
-  serverAccountId: number
-  followerActorId: number
-  includeLocalVideos: boolean
+  serverAccountIdForBlock: number
+
+  displayOnlyForFollower: DisplayOnlyForFollowerOptions
 
   count: number
   start: number
   sort: string
 
   nsfw?: boolean
-  filter?: VideoFilter
   host?: string
   isLive?: boolean
+  isLocal?: boolean
+  include?: VideoInclude
 
   categoryOneOf?: number[]
   licenceOneOf?: number[]
@@ -101,6 +107,7 @@ export class VideosIdListQueryBuilder extends AbstractVideosQueryBuilder {
 
   getIdsListQueryAndSort (options: BuildVideosListQueryOptions) {
     this.buildIdsListQuery(options)
+
     return { query: this.query, sort: this.sort, replacements: this.replacements }
   }
 
@@ -116,23 +123,30 @@ export class VideosIdListQueryBuilder extends AbstractVideosQueryBuilder {
       'INNER JOIN "actor" "accountActor" ON "account"."actorId" = "accountActor"."id"'
     ])
 
-    this.whereNotBlacklisted()
+    if (!(options.include & VideoInclude.BLACKLISTED)) {
+      this.whereNotBlacklisted()
+    }
 
-    if (options.serverAccountId) {
-      this.whereNotBlocked(options.serverAccountId, options.user)
+    if (options.serverAccountIdForBlock && !(options.include & VideoInclude.BLOCKED_OWNER)) {
+      this.whereNotBlocked(options.serverAccountIdForBlock, options.user)
     }
 
-    // Only list public/published videos
-    if (!options.filter || (options.filter !== 'all-local' && options.filter !== 'all')) {
-      this.whereStateAndPrivacyAvailable(options.user)
+    // Only list published videos
+    if (!(options.include & VideoInclude.NOT_PUBLISHED_STATE)) {
+      this.whereStateAvailable()
+    }
+
+    // Only list videos with the appropriate priavcy
+    if (!(options.include & VideoInclude.HIDDEN_PRIVACY)) {
+      this.wherePrivacyAvailable(options.user)
     }
 
     if (options.videoPlaylistId) {
       this.joinPlaylist(options.videoPlaylistId)
     }
 
-    if (options.filter && (options.filter === 'local' || options.filter === 'all-local')) {
-      this.whereOnlyLocal()
+    if (exists(options.isLocal)) {
+      this.whereLocal(options.isLocal)
     }
 
     if (options.host) {
@@ -147,8 +161,8 @@ export class VideosIdListQueryBuilder extends AbstractVideosQueryBuilder {
       this.whereChannelId(options.videoChannelId)
     }
 
-    if (options.followerActorId) {
-      this.whereFollowerActorId(options.followerActorId, options.includeLocalVideos)
+    if (options.displayOnlyForFollower) {
+      this.whereFollowerActorId(options.displayOnlyForFollower)
     }
 
     if (options.withFiles === true) {
@@ -282,12 +296,14 @@ export class VideosIdListQueryBuilder extends AbstractVideosQueryBuilder {
     this.replacements.videoPlaylistId = playlistId
   }
 
-  private whereStateAndPrivacyAvailable (user?: MUserAccountId) {
+  private whereStateAvailable () {
     this.and.push(
       `("video"."state" = ${VideoState.PUBLISHED} OR ` +
       `("video"."state" = ${VideoState.TO_TRANSCODE} AND "video"."waitTranscoding" IS false))`
     )
+  }
 
+  private wherePrivacyAvailable (user?: MUserAccountId) {
     if (user) {
       this.and.push(
         `("video"."privacy" = ${VideoPrivacy.PUBLIC} OR "video"."privacy" = ${VideoPrivacy.INTERNAL})`
@@ -299,8 +315,10 @@ export class VideosIdListQueryBuilder extends AbstractVideosQueryBuilder {
     }
   }
 
-  private whereOnlyLocal () {
-    this.and.push('"video"."remote" IS FALSE')
+  private whereLocal (isLocal: boolean) {
+    const isRemote = isLocal ? 'FALSE' : 'TRUE'
+
+    this.and.push('"video"."remote" IS ' + isRemote)
   }
 
   private whereHost (host: string) {
@@ -326,7 +344,7 @@ export class VideosIdListQueryBuilder extends AbstractVideosQueryBuilder {
     this.replacements.videoChannelId = channelId
   }
 
-  private whereFollowerActorId (followerActorId: number, includeLocalVideos: boolean) {
+  private whereFollowerActorId (options: { actorId: number, orLocalVideos: boolean }) {
     let query =
     '(' +
     '  EXISTS (' + // Videos shared by actors we follow
@@ -342,14 +360,14 @@ export class VideosIdListQueryBuilder extends AbstractVideosQueryBuilder {
     '    AND "actorFollow"."state" = \'accepted\'' +
     '  )'
 
-    if (includeLocalVideos) {
+    if (options.orLocalVideos) {
       query += '  OR "video"."remote" IS FALSE'
     }
 
     query += ')'
 
     this.and.push(query)
-    this.replacements.followerActorId = followerActorId
+    this.replacements.followerActorId = options.actorId
   }
 
   private whereFileExists () {
index e61c51de8f7d8285ee5f2ef1350d79d193a6f0f6..ef92bd2b04f187e4cb6ef614e592927654a0954f 100644 (file)
@@ -1,3 +1,4 @@
+import { VideoInclude } from '@shared/models'
 import { Sequelize } from 'sequelize'
 import { AbstractVideosModelQueryBuilder } from './shared/abstract-videos-model-query-builder'
 import { VideoModelBuilder } from './shared/video-model-builder'
@@ -28,7 +29,7 @@ export class VideosModelListQueryBuilder extends AbstractVideosModelQueryBuilder
     this.buildListQueryFromIdsQuery(options)
 
     return this.runQuery()
-      .then(rows => this.videoModelBuilder.buildVideosFromRows(rows))
+      .then(rows => this.videoModelBuilder.buildVideosFromRows({ rows, include: options.include }))
   }
 
   private buildInnerQuery (options: BuildVideosListQueryOptions) {
@@ -64,6 +65,14 @@ export class VideosModelListQueryBuilder extends AbstractVideosModelQueryBuilder
       this.includePlaylist(options.videoPlaylistId)
     }
 
+    if (options.include & VideoInclude.BLACKLISTED) {
+      this.includeBlacklisted()
+    }
+
+    if (options.include & VideoInclude.BLOCKED_OWNER) {
+      this.includeBlockedOwnerAndServer(options.serverAccountIdForBlock, options.user)
+    }
+
     const select = this.buildSelect()
 
     this.query = `${select} FROM (${this.innerQuery}) AS "tmp" ${this.joins} ${this.innerSort}`
index b5c46c86c8b652bd3bcb81fed0696b83ce7f545a..26be34329e4969d02c91c379008a4d2903a518b5 100644 (file)
@@ -34,12 +34,12 @@ import { VideoPathManager } from '@server/lib/video-path-manager'
 import { getServerActor } from '@server/models/application/application'
 import { ModelCache } from '@server/models/model-cache'
 import { AttributesOnly, buildVideoEmbedPath, buildVideoWatchPath, pick } from '@shared/core-utils'
+import { VideoInclude } from '@shared/models'
 import { VideoFile } from '@shared/models/videos/video-file.model'
 import { ResultList, UserRight, VideoPrivacy, VideoState } from '../../../shared'
 import { VideoObject } from '../../../shared/models/activitypub/objects'
 import { Video, VideoDetails, VideoRateType, VideoStorage } from '../../../shared/models/videos'
 import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
-import { VideoFilter } from '../../../shared/models/videos/video-query.type'
 import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
 import { peertubeTruncate } from '../../helpers/core-utils'
 import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
@@ -106,7 +106,7 @@ import {
 } from './formatter/video-format-utils'
 import { ScheduleVideoUpdateModel } from './schedule-video-update'
 import { VideosModelGetQueryBuilder } from './sql/video-model-get-query-builder'
-import { BuildVideosListQueryOptions, VideosIdListQueryBuilder } from './sql/videos-id-list-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'
 import { ThumbnailModel } from './thumbnail'
@@ -145,35 +145,6 @@ export type ForAPIOptions = {
   withAccountBlockerIds?: number[]
 }
 
-export type AvailableForListIDsOptions = {
-  serverAccountId: number
-  followerActorId: number
-  includeLocalVideos: boolean
-
-  attributesType?: 'none' | 'id' | 'all'
-
-  filter?: VideoFilter
-  categoryOneOf?: number[]
-  nsfw?: boolean
-  licenceOneOf?: number[]
-  languageOneOf?: string[]
-  tagsOneOf?: string[]
-  tagsAllOf?: string[]
-
-  withFiles?: boolean
-
-  accountId?: number
-  videoChannelId?: number
-
-  videoPlaylistId?: number
-
-  trendingDays?: number
-  user?: MUserAccountId
-  historyOfUser?: MUserId
-
-  baseWhere?: WhereOptions[]
-}
-
 @Scopes(() => ({
   [ScopeNames.WITH_IMMUTABLE_ATTRIBUTES]: {
     attributes: [ 'id', 'url', 'uuid', 'remote' ]
@@ -1054,10 +1025,10 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
     sort: string
 
     nsfw: boolean
-    filter?: VideoFilter
     isLive?: boolean
+    isLocal?: boolean
+    include?: VideoInclude
 
-    includeLocalVideos: boolean
     withFiles: boolean
 
     categoryOneOf?: number[]
@@ -1069,7 +1040,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
     accountId?: number
     videoChannelId?: number
 
-    followerActorId?: number
+    displayOnlyForFollower: DisplayOnlyForFollowerOptions | null
 
     videoPlaylistId?: number
 
@@ -1082,7 +1053,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
 
     search?: string
   }) {
-    if ((options.filter === 'all-local' || options.filter === 'all') && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) {
+    if (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')
     }
 
@@ -1096,11 +1067,6 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
 
     const serverActor = await getServerActor()
 
-    // followerActorId === null has a meaning, so just check undefined
-    const followerActorId = options.followerActorId !== undefined
-      ? options.followerActorId
-      : serverActor.id
-
     const queryOptions = {
       ...pick(options, [
         'start',
@@ -1113,19 +1079,19 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
         'languageOneOf',
         'tagsOneOf',
         'tagsAllOf',
-        'filter',
+        'isLocal',
+        'include',
+        'displayOnlyForFollower',
         'withFiles',
         'accountId',
         'videoChannelId',
         'videoPlaylistId',
-        'includeLocalVideos',
         'user',
         'historyOfUser',
         'search'
       ]),
 
-      followerActorId,
-      serverAccountId: serverActor.Account.id,
+      serverAccountIdForBlock: serverActor.Account.id,
       trendingDays,
       trendingAlgorithm
     }
@@ -1137,7 +1103,6 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
     start: number
     count: number
     sort: string
-    includeLocalVideos: boolean
     search?: string
     host?: string
     startDate?: string // ISO 8601
@@ -1146,6 +1111,8 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
     originallyPublishedEndDate?: string
     nsfw?: boolean
     isLive?: boolean
+    isLocal?: boolean
+    include?: VideoInclude
     categoryOneOf?: number[]
     licenceOneOf?: number[]
     languageOneOf?: string[]
@@ -1154,14 +1121,14 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
     durationMin?: number // seconds
     durationMax?: number // seconds
     user?: MUserAccountId
-    filter?: VideoFilter
     uuids?: string[]
+    displayOnlyForFollower: DisplayOnlyForFollowerOptions | null
   }) {
     const serverActor = await getServerActor()
 
     const queryOptions = {
       ...pick(options, [
-        'includeLocalVideos',
+        'include',
         'nsfw',
         'isLive',
         'categoryOneOf',
@@ -1170,7 +1137,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
         'tagsOneOf',
         'tagsAllOf',
         'user',
-        'filter',
+        'isLocal',
         'host',
         'start',
         'count',
@@ -1182,11 +1149,10 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
         'durationMin',
         'durationMax',
         'uuids',
-        'search'
+        'search',
+        'displayOnlyForFollower'
       ]),
-
-      followerActorId: serverActor.id,
-      serverAccountId: serverActor.Account.id
+      serverAccountIdForBlock: serverActor.Account.id
     }
 
     return VideoModel.getAvailableForApi(queryOptions)
@@ -1369,12 +1335,17 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
     // Sequelize could return null...
     if (!totalLocalVideoViews) totalLocalVideoViews = 0
 
+    const serverActor = await getServerActor()
+
     const { total: totalVideos } = await VideoModel.listForApi({
       start: 0,
       count: 0,
       sort: '-publishedAt',
       nsfw: buildNSFWFilter(),
-      includeLocalVideos: true,
+      displayOnlyForFollower: {
+        actorId: serverActor.id,
+        orLocalVideos: true
+      },
       withFiles: false
     })
 
@@ -1455,7 +1426,6 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
   // threshold corresponds to how many video the field should have to be returned
   static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) {
     const serverActor = await getServerActor()
-    const followerActorId = serverActor.id
 
     const queryOptions: BuildVideosListQueryOptions = {
       attributes: [ `"${field}"` ],
@@ -1464,9 +1434,11 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
       start: 0,
       sort: 'random',
       count,
-      serverAccountId: serverActor.Account.id,
-      followerActorId,
-      includeLocalVideos: true
+      serverAccountIdForBlock: serverActor.Account.id,
+      displayOnlyForFollower: {
+        actorId: serverActor.id,
+        orLocalVideos: true
+      }
     }
 
     const queryBuilder = new VideosIdListQueryBuilder(VideoModel.sequelize)
index a14e4d3e0b238c6320d469c0c91f66a7a58362d2..0882f817624a465ea48e78d1137ccd80e714023f 100644 (file)
@@ -27,6 +27,6 @@ import './video-comments'
 import './video-imports'
 import './video-playlists'
 import './videos'
-import './videos-filter'
+import './videos-common-filters'
 import './videos-history'
 import './videos-overviews'
diff --git a/server/tests/api/check-params/videos-common-filters.ts b/server/tests/api/check-params/videos-common-filters.ts
new file mode 100644 (file)
index 0000000..afe42b0
--- /dev/null
@@ -0,0 +1,203 @@
+/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
+
+import 'mocha'
+import {
+  cleanupTests,
+  createSingleServer,
+  makeGetRequest,
+  PeerTubeServer,
+  setAccessTokensToServers,
+  setDefaultVideoChannel
+} from '@shared/extra-utils'
+import { HttpStatusCode, UserRole, VideoInclude } from '@shared/models'
+
+describe('Test video filters validators', function () {
+  let server: PeerTubeServer
+  let userAccessToken: string
+  let moderatorAccessToken: string
+
+  // ---------------------------------------------------------------
+
+  before(async function () {
+    this.timeout(30000)
+
+    server = await createSingleServer(1)
+
+    await setAccessTokensToServers([ server ])
+    await setDefaultVideoChannel([ server ])
+
+    const user = { username: 'user1', password: 'my super password' }
+    await server.users.create({ username: user.username, password: user.password })
+    userAccessToken = await server.login.getAccessToken(user)
+
+    const moderator = { username: 'moderator', password: 'my super password' }
+    await server.users.create({ username: moderator.username, password: moderator.password, role: UserRole.MODERATOR })
+
+    moderatorAccessToken = await server.login.getAccessToken(moderator)
+  })
+
+  describe('When setting a deprecated video filter', function () {
+
+    async function testEndpoints (token: string, filter: string, expectedStatus: HttpStatusCode) {
+      const paths = [
+        '/api/v1/video-channels/root_channel/videos',
+        '/api/v1/accounts/root/videos',
+        '/api/v1/videos',
+        '/api/v1/search/videos'
+      ]
+
+      for (const path of paths) {
+        await makeGetRequest({
+          url: server.url,
+          path,
+          token,
+          query: {
+            filter
+          },
+          expectedStatus
+        })
+      }
+    }
+
+    it('Should fail with a bad filter', async function () {
+      await testEndpoints(server.accessToken, 'bad-filter', HttpStatusCode.BAD_REQUEST_400)
+    })
+
+    it('Should succeed with a good filter', async function () {
+      await testEndpoints(server.accessToken, 'local', HttpStatusCode.OK_200)
+    })
+
+    it('Should fail to list all-local/all with a simple user', async function () {
+      await testEndpoints(userAccessToken, 'all-local', HttpStatusCode.UNAUTHORIZED_401)
+      await testEndpoints(userAccessToken, 'all', HttpStatusCode.UNAUTHORIZED_401)
+    })
+
+    it('Should succeed to list all-local/all with a moderator', async function () {
+      await testEndpoints(moderatorAccessToken, 'all-local', HttpStatusCode.OK_200)
+      await testEndpoints(moderatorAccessToken, 'all', HttpStatusCode.OK_200)
+    })
+
+    it('Should succeed to list all-local/all with an admin', async function () {
+      await testEndpoints(server.accessToken, 'all-local', HttpStatusCode.OK_200)
+      await testEndpoints(server.accessToken, 'all', HttpStatusCode.OK_200)
+    })
+
+    // Because we cannot authenticate the user on the RSS endpoint
+    it('Should fail on the feeds endpoint with the all-local/all filter', async function () {
+      for (const filter of [ 'all', 'all-local' ]) {
+        await makeGetRequest({
+          url: server.url,
+          path: '/feeds/videos.json',
+          expectedStatus: HttpStatusCode.UNAUTHORIZED_401,
+          query: {
+            filter
+          }
+        })
+      }
+    })
+
+    it('Should succeed on the feeds endpoint with the local filter', async function () {
+      await makeGetRequest({
+        url: server.url,
+        path: '/feeds/videos.json',
+        expectedStatus: HttpStatusCode.OK_200,
+        query: {
+          filter: 'local'
+        }
+      })
+    })
+  })
+
+  describe('When setting video filters', function () {
+
+    const validIncludes = [
+      VideoInclude.NONE,
+      VideoInclude.HIDDEN_PRIVACY,
+      VideoInclude.NOT_PUBLISHED_STATE | VideoInclude.BLACKLISTED
+    ]
+
+    async function testEndpoints (options: {
+      token?: string
+      isLocal?: boolean
+      include?: VideoInclude
+      expectedStatus: HttpStatusCode
+    }) {
+      const paths = [
+        '/api/v1/video-channels/root_channel/videos',
+        '/api/v1/accounts/root/videos',
+        '/api/v1/videos',
+        '/api/v1/search/videos'
+      ]
+
+      for (const path of paths) {
+        await makeGetRequest({
+          url: server.url,
+          path,
+          token: options.token || server.accessToken,
+          query: {
+            isLocal: options.isLocal,
+            include: options.include
+          },
+          expectedStatus: options.expectedStatus
+        })
+      }
+    }
+
+    it('Should fail with a bad include', async function () {
+      await testEndpoints({ include: 'toto' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+    })
+
+    it('Should succeed with a good include', async function () {
+      for (const include of validIncludes) {
+        await testEndpoints({ include, expectedStatus: HttpStatusCode.OK_200 })
+      }
+    })
+
+    it('Should fail to include more videos with a simple user', async function () {
+      for (const include of validIncludes) {
+        await testEndpoints({ token: userAccessToken, include, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
+      }
+    })
+
+    it('Should succeed to list all local/all with a moderator', async function () {
+      for (const include of validIncludes) {
+        await testEndpoints({ token: moderatorAccessToken, include, expectedStatus: HttpStatusCode.OK_200 })
+      }
+    })
+
+    it('Should succeed to list all local/all with an admin', async function () {
+      for (const include of validIncludes) {
+        await testEndpoints({ token: server.accessToken, include, expectedStatus: HttpStatusCode.OK_200 })
+      }
+    })
+
+    // Because we cannot authenticate the user on the RSS endpoint
+    it('Should fail on the feeds endpoint with the all filter', async function () {
+      for (const include of [ VideoInclude.NOT_PUBLISHED_STATE ]) {
+        await makeGetRequest({
+          url: server.url,
+          path: '/feeds/videos.json',
+          expectedStatus: HttpStatusCode.UNAUTHORIZED_401,
+          query: {
+            include
+          }
+        })
+      }
+    })
+
+    it('Should succeed on the feeds endpoint with the local filter', async function () {
+      await makeGetRequest({
+        url: server.url,
+        path: '/feeds/videos.json',
+        expectedStatus: HttpStatusCode.OK_200,
+        query: {
+          isLocal: true
+        }
+      })
+    })
+  })
+
+  after(async function () {
+    await cleanupTests([ server ])
+  })
+})
diff --git a/server/tests/api/check-params/videos-filter.ts b/server/tests/api/check-params/videos-filter.ts
deleted file mode 100644 (file)
index d08570b..0000000
+++ /dev/null
@@ -1,114 +0,0 @@
-/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
-
-import 'mocha'
-import {
-  cleanupTests,
-  createSingleServer,
-  makeGetRequest,
-  PeerTubeServer,
-  setAccessTokensToServers,
-  setDefaultVideoChannel
-} from '@shared/extra-utils'
-import { HttpStatusCode, UserRole } from '@shared/models'
-
-async function testEndpoints (server: PeerTubeServer, token: string, filter: string, expectedStatus: HttpStatusCode) {
-  const paths = [
-    '/api/v1/video-channels/root_channel/videos',
-    '/api/v1/accounts/root/videos',
-    '/api/v1/videos',
-    '/api/v1/search/videos'
-  ]
-
-  for (const path of paths) {
-    await makeGetRequest({
-      url: server.url,
-      path,
-      token,
-      query: {
-        filter
-      },
-      expectedStatus
-    })
-  }
-}
-
-describe('Test video filters validators', function () {
-  let server: PeerTubeServer
-  let userAccessToken: string
-  let moderatorAccessToken: string
-
-  // ---------------------------------------------------------------
-
-  before(async function () {
-    this.timeout(30000)
-
-    server = await createSingleServer(1)
-
-    await setAccessTokensToServers([ server ])
-    await setDefaultVideoChannel([ server ])
-
-    const user = { username: 'user1', password: 'my super password' }
-    await server.users.create({ username: user.username, password: user.password })
-    userAccessToken = await server.login.getAccessToken(user)
-
-    const moderator = { username: 'moderator', password: 'my super password' }
-    await server.users.create({ username: moderator.username, password: moderator.password, role: UserRole.MODERATOR })
-
-    moderatorAccessToken = await server.login.getAccessToken(moderator)
-  })
-
-  describe('When setting a video filter', function () {
-
-    it('Should fail with a bad filter', async function () {
-      await testEndpoints(server, server.accessToken, 'bad-filter', HttpStatusCode.BAD_REQUEST_400)
-    })
-
-    it('Should succeed with a good filter', async function () {
-      await testEndpoints(server, server.accessToken, 'local', HttpStatusCode.OK_200)
-    })
-
-    it('Should fail to list all-local/all with a simple user', async function () {
-      await testEndpoints(server, userAccessToken, 'all-local', HttpStatusCode.UNAUTHORIZED_401)
-      await testEndpoints(server, userAccessToken, 'all', HttpStatusCode.UNAUTHORIZED_401)
-    })
-
-    it('Should succeed to list all-local/all with a moderator', async function () {
-      await testEndpoints(server, moderatorAccessToken, 'all-local', HttpStatusCode.OK_200)
-      await testEndpoints(server, moderatorAccessToken, 'all', HttpStatusCode.OK_200)
-    })
-
-    it('Should succeed to list all-local/all with an admin', async function () {
-      await testEndpoints(server, server.accessToken, 'all-local', HttpStatusCode.OK_200)
-      await testEndpoints(server, server.accessToken, 'all', HttpStatusCode.OK_200)
-    })
-
-    // Because we cannot authenticate the user on the RSS endpoint
-    it('Should fail on the feeds endpoint with the all-local/all filter', async function () {
-      for (const filter of [ 'all', 'all-local' ]) {
-        await makeGetRequest({
-          url: server.url,
-          path: '/feeds/videos.json',
-          expectedStatus: HttpStatusCode.UNAUTHORIZED_401,
-          query: {
-            filter
-          }
-        })
-      }
-    })
-
-    it('Should succeed on the feeds endpoint with the local filter', async function () {
-      await makeGetRequest({
-        url: server.url,
-        path: '/feeds/videos.json',
-        expectedStatus: HttpStatusCode.OK_200,
-        query: {
-          filter: 'local'
-        }
-      })
-    })
-  })
-
-  after(async function () {
-    await cleanupTests([ server ])
-  })
-})
index 5c07f89262abe0dd120dc50f67362510a5a214b5..c9c678e9d6799dfaa55b4f2b6f38d1db041e5bf2 100644 (file)
@@ -15,7 +15,7 @@ import './video-playlist-thumbnails'
 import './video-privacy'
 import './video-schedule-update'
 import './video-transcoder'
-import './videos-filter'
+import './videos-common-filters'
 import './videos-history'
 import './videos-overview'
 import './videos-views-cleaner'
index df9deb1e106bc6dcd3dd784dee1d4e8844a67897..9c255c1c55ef0a74ddb92801e0dbba4af758d96d 100644 (file)
@@ -349,7 +349,7 @@ describe('Test multiple servers', function () {
 
   describe('It should list local videos', function () {
     it('Should list only local videos on server 1', async function () {
-      const { data, total } = await servers[0].videos.list({ filter: 'local' })
+      const { data, total } = await servers[0].videos.list({ isLocal: true })
 
       expect(total).to.equal(1)
       expect(data).to.be.an('array')
@@ -358,7 +358,7 @@ describe('Test multiple servers', function () {
     })
 
     it('Should list only local videos on server 2', async function () {
-      const { data, total } = await servers[1].videos.list({ filter: 'local' })
+      const { data, total } = await servers[1].videos.list({ isLocal: true })
 
       expect(total).to.equal(1)
       expect(data).to.be.an('array')
@@ -367,7 +367,7 @@ describe('Test multiple servers', function () {
     })
 
     it('Should list only local videos on server 3', async function () {
-      const { data, total } = await servers[2].videos.list({ filter: 'local' })
+      const { data, total } = await servers[2].videos.list({ isLocal: true })
 
       expect(total).to.equal(2)
       expect(data).to.be.an('array')
index 29dac6ec1c6ddd64129aef1fabda4c177d212879..a0e4a156c584cc495ad29855c6a0ab8feee0207b 100644 (file)
@@ -354,19 +354,6 @@ describe('Test a single server', function () {
       await server.videos.update({ id: videoId, attributes })
     })
 
-    it('Should filter by tags and category', async function () {
-      {
-        const { data, total } = await server.videos.list({ tagsAllOf: [ 'tagup1', 'tagup2' ], categoryOneOf: [ 4 ] })
-        expect(total).to.equal(1)
-        expect(data[0].name).to.equal('my super video updated')
-      }
-
-      {
-        const { total } = await server.videos.list({ tagsAllOf: [ 'tagup1', 'tagup2' ], categoryOneOf: [ 3 ] })
-        expect(total).to.equal(0)
-      }
-    })
-
     it('Should have the video updated', async function () {
       this.timeout(60000)
 
diff --git a/server/tests/api/videos/videos-common-filters.ts b/server/tests/api/videos/videos-common-filters.ts
new file mode 100644 (file)
index 0000000..eb2d2ab
--- /dev/null
@@ -0,0 +1,403 @@
+/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
+
+import 'mocha'
+import { expect } from 'chai'
+import { pick } from '@shared/core-utils'
+import {
+  cleanupTests,
+  createMultipleServers,
+  doubleFollow,
+  makeGetRequest,
+  PeerTubeServer,
+  setAccessTokensToServers,
+  setDefaultVideoChannel,
+  waitJobs
+} from '@shared/extra-utils'
+import { HttpStatusCode, UserRole, Video, VideoInclude, VideoPrivacy } from '@shared/models'
+
+describe('Test videos filter', function () {
+  let servers: PeerTubeServer[]
+  let paths: string[]
+  let remotePaths: string[]
+
+  // ---------------------------------------------------------------
+
+  before(async function () {
+    this.timeout(160000)
+
+    servers = await createMultipleServers(2)
+
+    await setAccessTokensToServers(servers)
+    await setDefaultVideoChannel(servers)
+
+    for (const server of servers) {
+      const moderator = { username: 'moderator', password: 'my super password' }
+      await server.users.create({ username: moderator.username, password: moderator.password, role: UserRole.MODERATOR })
+      server['moderatorAccessToken'] = await server.login.getAccessToken(moderator)
+
+      await server.videos.upload({ attributes: { name: 'public ' + server.serverNumber } })
+
+      {
+        const attributes = { name: 'unlisted ' + server.serverNumber, privacy: VideoPrivacy.UNLISTED }
+        await server.videos.upload({ attributes })
+      }
+
+      {
+        const attributes = { name: 'private ' + server.serverNumber, privacy: VideoPrivacy.PRIVATE }
+        await server.videos.upload({ attributes })
+      }
+    }
+
+    await doubleFollow(servers[0], servers[1])
+
+    paths = [
+      `/api/v1/video-channels/root_channel/videos`,
+      `/api/v1/accounts/root/videos`,
+      '/api/v1/videos',
+      '/api/v1/search/videos'
+    ]
+
+    remotePaths = [
+      `/api/v1/video-channels/root_channel@${servers[1].host}/videos`,
+      `/api/v1/accounts/root@${servers[1].host}/videos`,
+      '/api/v1/videos',
+      '/api/v1/search/videos'
+    ]
+  })
+
+  describe('Check deprecated videos filter', function () {
+
+    async function getVideosNames (server: PeerTubeServer, token: string, filter: string, expectedStatus = HttpStatusCode.OK_200) {
+      const videosResults: Video[][] = []
+
+      for (const path of paths) {
+        const res = await makeGetRequest({
+          url: server.url,
+          path,
+          token,
+          query: {
+            sort: 'createdAt',
+            filter
+          },
+          expectedStatus
+        })
+
+        videosResults.push(res.body.data.map(v => v.name))
+      }
+
+      return videosResults
+    }
+
+    it('Should display local videos', async function () {
+      for (const server of servers) {
+        const namesResults = await getVideosNames(server, server.accessToken, 'local')
+        for (const names of namesResults) {
+          expect(names).to.have.lengthOf(1)
+          expect(names[0]).to.equal('public ' + server.serverNumber)
+        }
+      }
+    })
+
+    it('Should display all local videos by the admin or the moderator', async function () {
+      for (const server of servers) {
+        for (const token of [ server.accessToken, server['moderatorAccessToken'] ]) {
+
+          const namesResults = await getVideosNames(server, token, 'all-local')
+          for (const names of namesResults) {
+            expect(names).to.have.lengthOf(3)
+
+            expect(names[0]).to.equal('public ' + server.serverNumber)
+            expect(names[1]).to.equal('unlisted ' + server.serverNumber)
+            expect(names[2]).to.equal('private ' + server.serverNumber)
+          }
+        }
+      }
+    })
+
+    it('Should display all videos by the admin or the moderator', async function () {
+      for (const server of servers) {
+        for (const token of [ server.accessToken, server['moderatorAccessToken'] ]) {
+
+          const [ channelVideos, accountVideos, videos, searchVideos ] = await getVideosNames(server, token, 'all')
+          expect(channelVideos).to.have.lengthOf(3)
+          expect(accountVideos).to.have.lengthOf(3)
+
+          expect(videos).to.have.lengthOf(5)
+          expect(searchVideos).to.have.lengthOf(5)
+        }
+      }
+    })
+  })
+
+  describe('Check videos filters', function () {
+
+    async function listVideos (options: {
+      server: PeerTubeServer
+      path: string
+      isLocal?: boolean
+      include?: VideoInclude
+      category?: number
+      tagsAllOf?: string[]
+      token?: string
+      expectedStatus?: HttpStatusCode
+    }) {
+      const res = await makeGetRequest({
+        url: options.server.url,
+        path: options.path,
+        token: options.token ?? options.server.accessToken,
+        query: {
+          ...pick(options, [ 'isLocal', 'include', 'category', 'tagsAllOf' ]),
+
+          sort: 'createdAt'
+        },
+        expectedStatus: options.expectedStatus ?? HttpStatusCode.OK_200
+      })
+
+      return res.body.data as Video[]
+    }
+
+    async function getVideosNames (options: {
+      server: PeerTubeServer
+      isLocal?: boolean
+      include?: VideoInclude
+      token?: string
+      expectedStatus?: HttpStatusCode
+    }) {
+      const videosResults: string[][] = []
+
+      for (const path of paths) {
+        const videos = await listVideos({ ...options, path })
+
+        videosResults.push(videos.map(v => v.name))
+      }
+
+      return videosResults
+    }
+
+    it('Should display local videos', async function () {
+      for (const server of servers) {
+        const namesResults = await getVideosNames({ server, isLocal: true })
+
+        for (const names of namesResults) {
+          expect(names).to.have.lengthOf(1)
+          expect(names[0]).to.equal('public ' + server.serverNumber)
+        }
+      }
+    })
+
+    it('Should display local videos with hidden privacy by the admin or the moderator', async function () {
+      for (const server of servers) {
+        for (const token of [ server.accessToken, server['moderatorAccessToken'] ]) {
+
+          const namesResults = await getVideosNames({
+            server,
+            token,
+            isLocal: true,
+            include: VideoInclude.HIDDEN_PRIVACY
+          })
+
+          for (const names of namesResults) {
+            expect(names).to.have.lengthOf(3)
+
+            expect(names[0]).to.equal('public ' + server.serverNumber)
+            expect(names[1]).to.equal('unlisted ' + server.serverNumber)
+            expect(names[2]).to.equal('private ' + server.serverNumber)
+          }
+        }
+      }
+    })
+
+    it('Should display all videos by the admin or the moderator', async function () {
+      for (const server of servers) {
+        for (const token of [ server.accessToken, server['moderatorAccessToken'] ]) {
+
+          const [ channelVideos, accountVideos, videos, searchVideos ] = await getVideosNames({
+            server,
+            token,
+            include: VideoInclude.HIDDEN_PRIVACY
+          })
+
+          expect(channelVideos).to.have.lengthOf(3)
+          expect(accountVideos).to.have.lengthOf(3)
+
+          expect(videos).to.have.lengthOf(5)
+          expect(searchVideos).to.have.lengthOf(5)
+        }
+      }
+    })
+
+    it('Should display only remote videos', async function () {
+      this.timeout(40000)
+
+      await servers[1].videos.upload({ attributes: { name: 'remote video' } })
+
+      await waitJobs(servers)
+
+      const finder = (videos: Video[]) => videos.find(v => v.name === 'remote video')
+
+      for (const path of remotePaths) {
+        {
+          const videos = await listVideos({ server: servers[0], path })
+          const video = finder(videos)
+          expect(video).to.exist
+        }
+
+        {
+          const videos = await listVideos({ server: servers[0], path, isLocal: false })
+          const video = finder(videos)
+          expect(video).to.exist
+        }
+
+        {
+          const videos = await listVideos({ server: servers[0], path, isLocal: true })
+          const video = finder(videos)
+          expect(video).to.not.exist
+        }
+      }
+    })
+
+    it('Should include not published videos', async function () {
+      await servers[0].config.enableLive({ allowReplay: false, transcoding: false })
+      await servers[0].live.create({ fields: { name: 'live video', channelId: servers[0].store.channel.id, privacy: VideoPrivacy.PUBLIC } })
+
+      const finder = (videos: Video[]) => videos.find(v => v.name === 'live video')
+
+      for (const path of paths) {
+        {
+          const videos = await listVideos({ server: servers[0], path })
+          const video = finder(videos)
+          expect(video).to.not.exist
+          expect(videos[0].state).to.not.exist
+          expect(videos[0].waitTranscoding).to.not.exist
+        }
+
+        {
+          const videos = await listVideos({ server: servers[0], path, include: VideoInclude.NOT_PUBLISHED_STATE })
+          const video = finder(videos)
+          expect(video).to.exist
+          expect(video.state).to.exist
+        }
+      }
+    })
+
+    it('Should include blacklisted videos', async function () {
+      const { id } = await servers[0].videos.upload({ attributes: { name: 'blacklisted' } })
+
+      await servers[0].blacklist.add({ videoId: id })
+
+      const finder = (videos: Video[]) => videos.find(v => v.name === 'blacklisted')
+
+      for (const path of paths) {
+        {
+          const videos = await listVideos({ server: servers[0], path })
+          const video = finder(videos)
+          expect(video).to.not.exist
+          expect(videos[0].blacklisted).to.not.exist
+        }
+
+        {
+          const videos = await listVideos({ server: servers[0], path, include: VideoInclude.BLACKLISTED })
+          const video = finder(videos)
+          expect(video).to.exist
+          expect(video.blacklisted).to.be.true
+        }
+      }
+    })
+
+    it('Should include videos from muted account', async function () {
+      const finder = (videos: Video[]) => videos.find(v => v.name === 'remote video')
+
+      await servers[0].blocklist.addToServerBlocklist({ account: 'root@' + servers[1].host })
+
+      for (const path of remotePaths) {
+        {
+          const videos = await listVideos({ server: servers[0], path })
+          const video = finder(videos)
+          expect(video).to.not.exist
+
+          // Some paths won't have videos
+          if (videos[0]) {
+            expect(videos[0].blockedOwner).to.not.exist
+            expect(videos[0].blockedServer).to.not.exist
+          }
+        }
+
+        {
+          const videos = await listVideos({ server: servers[0], path, include: VideoInclude.BLOCKED_OWNER })
+
+          const video = finder(videos)
+          expect(video).to.exist
+          expect(video.blockedServer).to.be.false
+          expect(video.blockedOwner).to.be.true
+        }
+      }
+
+      await servers[0].blocklist.removeFromServerBlocklist({ account: 'root@' + servers[1].host })
+    })
+
+    it('Should include videos from muted server', async function () {
+      const finder = (videos: Video[]) => videos.find(v => v.name === 'remote video')
+
+      await servers[0].blocklist.addToServerBlocklist({ server: servers[1].host })
+
+      for (const path of remotePaths) {
+        {
+          const videos = await listVideos({ server: servers[0], path })
+          const video = finder(videos)
+          expect(video).to.not.exist
+
+          // Some paths won't have videos
+          if (videos[0]) {
+            expect(videos[0].blockedOwner).to.not.exist
+            expect(videos[0].blockedServer).to.not.exist
+          }
+        }
+
+        {
+          const videos = await listVideos({ server: servers[0], path, include: VideoInclude.BLOCKED_OWNER })
+          const video = finder(videos)
+          expect(video).to.exist
+          expect(video.blockedServer).to.be.true
+          expect(video.blockedOwner).to.be.false
+        }
+      }
+
+      await servers[0].blocklist.removeFromServerBlocklist({ server: servers[1].host })
+    })
+
+    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 } })
+
+      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')
+
+        }
+
+        {
+          const videos = await listVideos({ server: servers[0], path, tagsAllOf: [ 'tag1', 'tag3' ] })
+          expect(videos).to.have.lengthOf(0)
+        }
+
+        {
+          const { data, total } = await servers[0].videos.list({ tagsAllOf: [ 'tag3' ], categoryOneOf: [ 4 ] })
+          expect(total).to.equal(1)
+          expect(data[0].name).to.equal('tag filter with category')
+        }
+
+        {
+          const { total } = await servers[0].videos.list({ tagsAllOf: [ 'tag4' ], categoryOneOf: [ 4 ] })
+          expect(total).to.equal(0)
+        }
+      }
+    })
+  })
+
+  after(async function () {
+    await cleanupTests(servers)
+  })
+})
diff --git a/server/tests/api/videos/videos-filter.ts b/server/tests/api/videos/videos-filter.ts
deleted file mode 100644 (file)
index 2306807..0000000
+++ /dev/null
@@ -1,122 +0,0 @@
-/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
-
-import 'mocha'
-import { expect } from 'chai'
-import {
-  cleanupTests,
-  createMultipleServers,
-  doubleFollow,
-  makeGetRequest,
-  PeerTubeServer,
-  setAccessTokensToServers
-} from '@shared/extra-utils'
-import { HttpStatusCode, UserRole, Video, VideoPrivacy } from '@shared/models'
-
-async function getVideosNames (server: PeerTubeServer, token: string, filter: string, expectedStatus = HttpStatusCode.OK_200) {
-  const paths = [
-    '/api/v1/video-channels/root_channel/videos',
-    '/api/v1/accounts/root/videos',
-    '/api/v1/videos',
-    '/api/v1/search/videos'
-  ]
-
-  const videosResults: Video[][] = []
-
-  for (const path of paths) {
-    const res = await makeGetRequest({
-      url: server.url,
-      path,
-      token,
-      query: {
-        sort: 'createdAt',
-        filter
-      },
-      expectedStatus
-    })
-
-    videosResults.push(res.body.data.map(v => v.name))
-  }
-
-  return videosResults
-}
-
-describe('Test videos filter', function () {
-  let servers: PeerTubeServer[]
-
-  // ---------------------------------------------------------------
-
-  before(async function () {
-    this.timeout(160000)
-
-    servers = await createMultipleServers(2)
-
-    await setAccessTokensToServers(servers)
-
-    for (const server of servers) {
-      const moderator = { username: 'moderator', password: 'my super password' }
-      await server.users.create({ username: moderator.username, password: moderator.password, role: UserRole.MODERATOR })
-      server['moderatorAccessToken'] = await server.login.getAccessToken(moderator)
-
-      await server.videos.upload({ attributes: { name: 'public ' + server.serverNumber } })
-
-      {
-        const attributes = { name: 'unlisted ' + server.serverNumber, privacy: VideoPrivacy.UNLISTED }
-        await server.videos.upload({ attributes })
-      }
-
-      {
-        const attributes = { name: 'private ' + server.serverNumber, privacy: VideoPrivacy.PRIVATE }
-        await server.videos.upload({ attributes })
-      }
-    }
-
-    await doubleFollow(servers[0], servers[1])
-  })
-
-  describe('Check videos filter', function () {
-
-    it('Should display local videos', async function () {
-      for (const server of servers) {
-        const namesResults = await getVideosNames(server, server.accessToken, 'local')
-        for (const names of namesResults) {
-          expect(names).to.have.lengthOf(1)
-          expect(names[0]).to.equal('public ' + server.serverNumber)
-        }
-      }
-    })
-
-    it('Should display all local videos by the admin or the moderator', async function () {
-      for (const server of servers) {
-        for (const token of [ server.accessToken, server['moderatorAccessToken'] ]) {
-
-          const namesResults = await getVideosNames(server, token, 'all-local')
-          for (const names of namesResults) {
-            expect(names).to.have.lengthOf(3)
-
-            expect(names[0]).to.equal('public ' + server.serverNumber)
-            expect(names[1]).to.equal('unlisted ' + server.serverNumber)
-            expect(names[2]).to.equal('private ' + server.serverNumber)
-          }
-        }
-      }
-    })
-
-    it('Should display all videos by the admin or the moderator', async function () {
-      for (const server of servers) {
-        for (const token of [ server.accessToken, server['moderatorAccessToken'] ]) {
-
-          const [ channelVideos, accountVideos, videos, searchVideos ] = await getVideosNames(server, token, 'all')
-          expect(channelVideos).to.have.lengthOf(3)
-          expect(accountVideos).to.have.lengthOf(3)
-
-          expect(videos).to.have.lengthOf(5)
-          expect(searchVideos).to.have.lengthOf(5)
-        }
-      }
-    })
-  })
-
-  after(async function () {
-    await cleanupTests(servers)
-  })
-})
index 9848412919eeb9454ff165f70db6b0716ea3c4aa..abe0de27b13861569e4f5ce3b4658be687ab11e5 100644 (file)
@@ -84,7 +84,7 @@ export type MAccountSummary =
 
 export type MAccountSummaryBlocks =
   MAccountSummary &
-  Use<'BlockedAccounts', MAccountBlocklistId[]>
+  Use<'BlockedByAccounts', MAccountBlocklistId[]>
 
 export type MAccountAPI =
   MAccount &
index c1a9ec806e5cdfc23682297f2d56be7fa15f9feb..68241f06246ee943a0c9faf14e17a6cc825a86f2 100644 (file)
@@ -18,8 +18,7 @@ import {
   VideoDetails,
   VideoFileMetadata,
   VideoPrivacy,
-  VideosCommonQuery,
-  VideosWithSearchCommonQuery
+  VideosCommonQuery
 } from '@shared/models'
 import { buildAbsoluteFixturePath, wait } from '../miscs'
 import { unwrapBody } from '../requests'
@@ -246,7 +245,7 @@ export class VideosCommand extends AbstractCommand {
     })
   }
 
-  listByAccount (options: OverrideCommandOptions & VideosWithSearchCommonQuery & {
+  listByAccount (options: OverrideCommandOptions & VideosCommonQuery & {
     handle: string
   }) {
     const { handle, search } = options
@@ -262,7 +261,7 @@ export class VideosCommand extends AbstractCommand {
     })
   }
 
-  listByChannel (options: OverrideCommandOptions & VideosWithSearchCommonQuery & {
+  listByChannel (options: OverrideCommandOptions & VideosCommonQuery & {
     handle: string
   }) {
     const { handle } = options
@@ -605,7 +604,8 @@ export class VideosCommand extends AbstractCommand {
       'languageOneOf',
       'tagsOneOf',
       'tagsAllOf',
-      'filter',
+      'isLocal',
+      'include',
       'skipCount'
     ])
   }
index 2f2e9a9348f43a6c2f1e348ea41e5e527e40dc27..55a98e302da3af327ec16873103b1d5b770c63d5 100644 (file)
@@ -1,4 +1,4 @@
-import { VideoFilter } from '../videos'
+import { VideoInclude } from '../videos/video-include.enum'
 import { BooleanBothQuery } from './boolean-both-query.model'
 
 // These query parameters can be used with any endpoint that list videos
@@ -11,6 +11,12 @@ export interface VideosCommonQuery {
 
   isLive?: boolean
 
+  // FIXME: deprecated in 4.0 in favour of isLocal and include, to remove
+  filter?: never
+
+  isLocal?: boolean
+  include?: VideoInclude
+
   categoryOneOf?: number[]
 
   licenceOneOf?: number[]
@@ -20,17 +26,16 @@ export interface VideosCommonQuery {
   tagsOneOf?: string[]
   tagsAllOf?: string[]
 
-  filter?: VideoFilter
-
   skipCount?: boolean
+
+  search?: string
 }
 
 export interface VideosCommonQueryAfterSanitize extends VideosCommonQuery {
   start: number
   count: number
   sort: string
-}
 
-export interface VideosWithSearchCommonQuery extends VideosCommonQuery {
-  search?: string
+  // FIXME: deprecated in 4.0, to remove
+  filter?: never
 }
index a5436879d5fda9ecd50b3cf2ab8d2f47ef454511..447c72806f27cc1386437b0a5db144c5ea9bbc35 100644 (file)
@@ -23,4 +23,7 @@ export interface VideosSearchQueryAfterSanitize extends VideosSearchQuery {
   start: number
   count: number
   sort: string
+
+  // FIXME: deprecated in 4.0, to remove
+  filter?: never
 }
index 733c433a09de60af94e08368ea6cb9e1a6905efc..3d3eedcc670e0e2eb0f2b5e61b91eaba43b60d98 100644 (file)
@@ -19,7 +19,8 @@ export * from './video-file-metadata.model'
 export * from './video-file.model'
 
 export * from './video-privacy.enum'
-export * from './video-query.type'
+export * from './video-filter.type'
+export * from './video-include.enum'
 export * from './video-rate.type'
 export * from './video-resolution.enum'
 
diff --git a/shared/models/videos/video-include.enum.ts b/shared/models/videos/video-include.enum.ts
new file mode 100644 (file)
index 0000000..fa720b3
--- /dev/null
@@ -0,0 +1,7 @@
+export const enum VideoInclude {
+  NONE = 0,
+  NOT_PUBLISHED_STATE = 1 << 0,
+  HIDDEN_PRIVACY = 1 << 1,
+  BLACKLISTED = 1 << 2,
+  BLOCKED_OWNER = 1 << 3
+}
index 4a7e399a228ffa44a7fa51cf56090c97d88a40c0..dadde38af1aff5989f65b114c49f6ec9f1d2d338 100644 (file)
@@ -43,13 +43,6 @@ export interface Video {
   dislikes: number
   nsfw: boolean
 
-  waitTranscoding?: boolean
-  state?: VideoConstant<VideoState>
-  scheduledUpdate?: VideoScheduleUpdate
-
-  blacklisted?: boolean
-  blacklistedReason?: string
-
   account: AccountSummary
   channel: VideoChannelSummary
 
@@ -58,6 +51,17 @@ export interface Video {
   }
 
   pluginData?: any
+
+  // Additional attributes dependending on the query
+  waitTranscoding?: boolean
+  state?: VideoConstant<VideoState>
+  scheduledUpdate?: VideoScheduleUpdate
+
+  blacklisted?: boolean
+  blacklistedReason?: string
+
+  blockedOwner?: boolean
+  blockedServer?: boolean
 }
 
 export interface VideoDetails extends Video {
@@ -70,7 +74,7 @@ export interface VideoDetails extends Video {
   commentsEnabled: boolean
   downloadEnabled: boolean
 
-  // Not optional in details (unlike in Video)
+  // Not optional in details (unlike in parent Video)
   waitTranscoding: boolean
   state: VideoConstant<VideoState>
 
index ef4e7d04d27f84ce6e049dd44d1e818bef0e9bce..cdb4dd3432745bccb82af6b24254f3d2a5d11afc 100644 (file)
@@ -367,7 +367,8 @@ paths:
         - $ref: '#/components/parameters/licenceOneOf'
         - $ref: '#/components/parameters/languageOneOf'
         - $ref: '#/components/parameters/nsfw'
-        - $ref: '#/components/parameters/filter'
+        - $ref: '#/components/parameters/isLocal'
+        - $ref: '#/components/parameters/include'
         - $ref: '#/components/parameters/skipCount'
         - $ref: '#/components/parameters/start'
         - $ref: '#/components/parameters/count'
@@ -1300,7 +1301,8 @@ paths:
         - $ref: '#/components/parameters/licenceOneOf'
         - $ref: '#/components/parameters/languageOneOf'
         - $ref: '#/components/parameters/nsfw'
-        - $ref: '#/components/parameters/filter'
+        - $ref: '#/components/parameters/isLocal'
+        - $ref: '#/components/parameters/include'
         - $ref: '#/components/parameters/skipCount'
         - $ref: '#/components/parameters/start'
         - $ref: '#/components/parameters/count'
@@ -1620,7 +1622,8 @@ paths:
         - $ref: '#/components/parameters/licenceOneOf'
         - $ref: '#/components/parameters/languageOneOf'
         - $ref: '#/components/parameters/nsfw'
-        - $ref: '#/components/parameters/filter'
+        - $ref: '#/components/parameters/isLocal'
+        - $ref: '#/components/parameters/include'
         - $ref: '#/components/parameters/skipCount'
         - $ref: '#/components/parameters/start'
         - $ref: '#/components/parameters/count'
@@ -2856,7 +2859,8 @@ paths:
         - $ref: '#/components/parameters/licenceOneOf'
         - $ref: '#/components/parameters/languageOneOf'
         - $ref: '#/components/parameters/nsfw'
-        - $ref: '#/components/parameters/filter'
+        - $ref: '#/components/parameters/isLocal'
+        - $ref: '#/components/parameters/include'
         - $ref: '#/components/parameters/skipCount'
         - $ref: '#/components/parameters/start'
         - $ref: '#/components/parameters/count'
@@ -3576,7 +3580,8 @@ paths:
         - $ref: '#/components/parameters/licenceOneOf'
         - $ref: '#/components/parameters/languageOneOf'
         - $ref: '#/components/parameters/nsfw'
-        - $ref: '#/components/parameters/filter'
+        - $ref: '#/components/parameters/isLocal'
+        - $ref: '#/components/parameters/include'
         - $ref: '#/components/parameters/skipCount'
         - $ref: '#/components/parameters/start'
         - $ref: '#/components/parameters/count'
@@ -4078,7 +4083,8 @@ paths:
             type: string
         - $ref: '#/components/parameters/sort'
         - $ref: '#/components/parameters/nsfw'
-        - $ref: '#/components/parameters/filter'
+        - $ref: '#/components/parameters/isLocal'
+        - $ref: '#/components/parameters/include'
       responses:
         '204':
           description: successful operation
@@ -4159,7 +4165,8 @@ paths:
           required: true
         - $ref: '#/components/parameters/sort'
         - $ref: '#/components/parameters/nsfw'
-        - $ref: '#/components/parameters/filter'
+        - $ref: '#/components/parameters/isLocal'
+        - $ref: '#/components/parameters/include'
       responses:
         '204':
           description: successful operation
@@ -4792,20 +4799,37 @@ components:
         enum:
         - 'true'
         - 'false'
-    filter:
-      name: filter
+    isLocal:
+      name: isLocal
       in: query
       required: false
-      description: >
-        Special filters which might require special rights:
-         * `local` - only videos local to the instance
-         * `all-local` - only videos local to the instance, but showing private and unlisted videos (requires Admin privileges)
-         * `all` - all videos, showing private and unlisted videos (requires Admin privileges)
       schema:
-        type: string
+        type: boolean
+      description: 'Display only local or remote videos'
+    include:
+      name: include
+      in: query
+      required: false
+      schema:
+        type: integer
         enum:
-        - local
-        - all-local
+        - 0
+        - 1
+        - 2
+        - 4
+        - 8
+      description: >
+        Include additional videos in results (can be combined using bitwise or operator)
+
+        - `0` NONE
+
+        - `1` NOT_PUBLISHED_STATE
+
+        - `2` HIDDEN_PRIVACY
+
+        - `4` BLACKLISTED
+
+        - `8` BLOCKED
     subscriptionsUris:
       name: uris
       in: query
@@ -6995,7 +7019,7 @@ components:
       enum:
         - 0
         - 1
-        - 3
+        - 2
     Notification:
       properties:
         id: