diff --git a/client/src/app/+admin/overview/videos/video-list.component.html b/client/src/app/+admin/overview/videos/video-list.component.html
index 1f1e9cc6e..6250c00fb 100644
--- a/client/src/app/+admin/overview/videos/video-list.component.html
+++ b/client/src/app/+admin/overview/videos/video-list.component.html
@@ -63,10 +63,17 @@
-
- {{ video.privacy.label }}
+ |
+ {{ video.privacy.label }}
+
NSFW
- NSFW
+
+ Not published yet
+
+ Account muted
+ Server muted
+
+ Blocked
|
diff --git a/client/src/app/+admin/overview/videos/video-list.component.scss b/client/src/app/+admin/overview/videos/video-list.component.scss
index fcdb457f2..250a917e4 100644
--- a/client/src/app/+admin/overview/videos/video-list.component.scss
+++ b/client/src/app/+admin/overview/videos/video-list.component.scss
@@ -7,4 +7,6 @@ my-embed {
.badge {
@include table-badge;
+
+ margin-right: 5px;
}
diff --git a/client/src/app/+admin/overview/videos/video-list.component.ts b/client/src/app/+admin/overview/videos/video-list.component.ts
index a445bc209..dd9225e6a 100644
--- a/client/src/app/+admin/overview/videos/video-list.component.ts
+++ b/client/src/app/+admin/overview/videos/video-list.component.ts
@@ -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 = []
diff --git a/client/src/app/+videos/+video-watch/shared/information/video-alert.component.html b/client/src/app/+videos/+video-watch/shared/information/video-alert.component.html
index e2dd44bf7..33b5a47a0 100644
--- a/client/src/app/+videos/+video-watch/shared/information/video-alert.component.html
+++ b/client/src/app/+videos/+video-watch/shared/information/video-alert.component.html
@@ -24,5 +24,5 @@
This video is blocked.
- {{ video.blockedReason }}
+ {{ video.blacklistedReason }}
diff --git a/client/src/app/+videos/video-list/videos-list-common-page.component.ts b/client/src/app/+videos/video-list/videos-list-common-page.component.ts
index ba64d4fec..d03b09610 100644
--- a/client/src/app/+videos/video-list/videos-list-common-page.component.ts
+++ b/client/src/app/+videos/video-list/videos-list-common-page.component.ts
@@ -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) {
diff --git a/client/src/app/shared/shared-custom-markup/custom-markup.service.ts b/client/src/app/shared/shared-custom-markup/custom-markup.service.ts
index ab640d348..a959b336d 100644
--- a/client/src/app/shared/shared-custom-markup/custom-markup.service.ts
+++ b/client/src/app/shared/shared-custom-markup/custom-markup.service.ts
@@ -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)
diff --git a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/videos-list-markup.component.ts b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/videos-list-markup.component.ts
index 856e43681..0e4d5fb12 100644
--- a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/videos-list-markup.component.ts
+++ b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/videos-list-markup.component.ts
@@ -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 },
diff --git a/client/src/app/shared/shared-main/video/video.model.ts b/client/src/app/shared/shared-main/video/video.model.ts
index 10caec014..699eac7f1 100644
--- a/client/src/app/shared/shared-main/video/video.model.ts
+++ b/client/src/app/shared/shared-main/video/video.model.ts
@@ -65,8 +65,12 @@ export class Video implements VideoServerModel {
waitTranscoding?: boolean
state?: VideoConstant
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
diff --git a/client/src/app/shared/shared-main/video/video.service.ts b/client/src/app/shared/shared-main/video/video.service.ts
index 9e3aa1e6a..0a3a51b0c 100644
--- a/client/src/app/shared/shared-main/video/video.service.ts
+++ b/client/src/app/shared/shared-main/video/video.service.ts
@@ -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 & { pagination: RestPagination, search?: string }
+ parameters: CommonVideoParams & { pagination: RestPagination, search?: string }
): Observable> {
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>(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
}
})
diff --git a/client/src/app/shared/shared-moderation/video-block.component.ts b/client/src/app/shared/shared-moderation/video-block.component.ts
index f6c29dcfa..a6180dd14 100644
--- a/client/src/app/shared/shared-moderation/video-block.component.ts
+++ b/client/src/app/shared/shared-moderation/video-block.component.ts
@@ -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()
},
diff --git a/client/src/app/shared/shared-video-miniature/video-actions-dropdown.component.ts b/client/src/app/shared/shared-video-miniature/video-actions-dropdown.component.ts
index 2ba091438..feac79d4e 100644
--- a/client/src/app/shared/shared-video-miniature/video-actions-dropdown.component.ts
+++ b/client/src/app/shared/shared-video-miniature/video-actions-dropdown.component.ts
@@ -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()
},
diff --git a/client/src/app/shared/shared-video-miniature/video-filters.model.ts b/client/src/app/shared/shared-video-miniature/video-filters.model.ts
index 920dc826c..5ad7cf3f7 100644
--- a/client/src/app/shared/shared-video-miniature/video-filters.model.ts
+++ b/client/src/app/shared/shared-video-miniature/video-filters.model.ts
@@ -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 ]: any
@@ -196,14 +196,15 @@ export class VideoFilters {
}
toVideosAPIObject () {
- let filter: VideoFilter
+ let isLocal: boolean
+ let include: VideoInclude
- 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'
+ 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
}
}
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.html b/client/src/app/shared/shared-video-miniature/video-miniature.component.html
index b12495f90..30483831a 100644
--- a/client/src/app/shared/shared-video-miniature/video-miniature.component.html
+++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.html
@@ -55,7 +55,7 @@
Blocked
- {{ video.blockedReason }}
+ {{ video.blacklistedReason }}
diff --git a/server/controllers/api/accounts.ts b/server/controllers/api/accounts.ts
index 8eb880d59..44edffe38 100644
--- a/server/controllers/api/accounts.ts
+++ b/server/controllers/api/accounts.ts
@@ -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) {
diff --git a/server/controllers/api/overviews.ts b/server/controllers/api/overviews.ts
index 5b16232e2..68626a508 100644
--- a/server/controllers/api/overviews.ts
+++ b/server/controllers/api/overviews.ts
@@ -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,
diff --git a/server/controllers/api/search/search-videos.ts b/server/controllers/api/search/search-videos.ts
index 90946cb74..6db70acdf 100644
--- a/server/controllers/api/search/search-videos.ts
+++ b/server/controllers/api/search/search-videos.ts
@@ -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) {
diff --git a/server/controllers/api/users/my-subscriptions.ts b/server/controllers/api/users/my-subscriptions.ts
index b2b441673..d96378180 100644
--- a/server/controllers/api/users/my-subscriptions.ts
+++ b/server/controllers/api/users/my-subscriptions.ts
@@ -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)))
}
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts
index 7bf7a68c9..f9c1a405d 100644
--- a/server/controllers/api/video-channel.ts
+++ b/server/controllers/api/video-channel.ts
@@ -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) {
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index c0c77f3f7..821ed7ff3 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -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) {
diff --git a/server/controllers/bots.ts b/server/controllers/bots.ts
index 63db345bf..9f03de7e8 100644
--- a/server/controllers/bots.ts
+++ b/server/controllers/bots.ts
@@ -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
})
diff --git a/server/controllers/feeds.ts b/server/controllers/feeds.ts
index 5ac2e43a1..1f6aebac3 100644
--- a/server/controllers/feeds.ts
+++ b/server/controllers/feeds.ts
@@ -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
})
diff --git a/server/helpers/custom-validators/videos.ts b/server/helpers/custom-validators/videos.ts
index c3604fbad..1d56ade6f 100644
--- a/server/helpers/custom-validators/videos.ts
+++ b/server/helpers/custom-validators/videos.ts
@@ -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,
diff --git a/server/helpers/query.ts b/server/helpers/query.ts
index e711b15f2..79cf076d1 100644
--- a/server/helpers/query.ts
+++ b/server/helpers/query.ts
@@ -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',
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts
index e486887a7..44233b653 100644
--- a/server/middlewares/validators/videos/videos.ts
+++ b/server/middlewares/validators/videos/videos.ts
@@ -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.'
diff --git a/server/models/account/account.ts b/server/models/account/account.ts
index 37194a119..056ec6857 100644
--- a/server/models/account/account.ts
+++ b/server/models/account/account.ts
@@ -228,10 +228,10 @@ export class AccountModel extends Model >> {
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>> {
}
isBlocked () {
- return this.BlockedAccounts && this.BlockedAccounts.length !== 0
+ return this.BlockedBy && this.BlockedBy.length !== 0
}
}
diff --git a/server/models/server/server.ts b/server/models/server/server.ts
index 0d3c092e0..edbe92f73 100644
--- a/server/models/server/server.ts
+++ b/server/models/server/server.ts
@@ -50,7 +50,7 @@ export class ServerModel extends Model>> {
},
onDelete: 'CASCADE'
})
- BlockedByAccounts: ServerBlocklistModel[]
+ BlockedBy: ServerBlocklistModel[]
static load (id: number, transaction?: Transaction): Promise {
const query = {
@@ -81,7 +81,7 @@ export class ServerModel extends Model>> {
}
isBlocked () {
- return this.BlockedByAccounts && this.BlockedByAccounts.length !== 0
+ return this.BlockedBy && this.BlockedBy.length !== 0
}
toFormattedJSON (this: MServerFormattable) {
diff --git a/server/models/user/user-video-history.ts b/server/models/user/user-video-history.ts
index e3dc4a062..d633cc9d5 100644
--- a/server/models/user/user-video-history.ts
+++ b/server/models/user/user-video-history.ts
@@ -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 ModelAccount->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"'
diff --git a/server/models/video/sql/shared/video-model-builder.ts b/server/models/video/sql/shared/video-model-builder.ts
index 33a0181e9..0eac95661 100644
--- a/server/models/video/sql/shared/video-model-builder.ts
+++ b/server/models/video/sql/shared/video-model-builder.ts
@@ -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
private historyDone: Set
private blacklistDone: Set
+ private accountBlocklistDone: Set
+ private serverBlocklistDone: Set
private liveDone: Set
private redundancyDone: Set
private scheduleVideoUpdateDone: Set
@@ -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()
- this.historyDone = new Set()
- this.blacklistDone = new Set()
- this.liveDone = new Set()
- this.redundancyDone = new Set()
- this.scheduleVideoUpdateDone = new Set()
+ 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.trackersDone = new Set()
- this.tagsDone = new Set()
+ this.accountBlocklistDone = new Set()
+ this.serverBlocklistDone = new Set()
+
+ 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
diff --git a/server/models/video/sql/shared/video-tables.ts b/server/models/video/sql/shared/video-tables.ts
index 75823864d..042b9d5da 100644
--- a/server/models/video/sql/shared/video-tables.ts
+++ b/server/models/video/sql/shared/video-tables.ts
@@ -139,6 +139,10 @@ export class VideoTables {
return [ 'id', 'reason', 'unfederated' ]
}
+ getBlocklistAttributes () {
+ return [ 'id' ]
+ }
+
getScheduleUpdateAttributes () {
return [
'id',
diff --git a/server/models/video/sql/video-model-get-query-builder.ts b/server/models/video/sql/video-model-get-query-builder.ts
index f234e8778..d18ddae67 100644
--- a/server/models/video/sql/video-model-get-query-builder.ts
+++ b/server/models/video/sql/video-model-get-query-builder.ts
@@ -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 ')
diff --git a/server/models/video/sql/videos-id-list-query-builder.ts b/server/models/video/sql/videos-id-list-query-builder.ts
index 7625c003d..3eb547e75 100644
--- a/server/models/video/sql/videos-id-list-query-builder.ts
+++ b/server/models/video/sql/videos-id-list-query-builder.ts
@@ -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.serverAccountId) {
- this.whereNotBlocked(options.serverAccountId, options.user)
+ if (!(options.include & VideoInclude.BLACKLISTED)) {
+ this.whereNotBlacklisted()
}
- // Only list public/published videos
- if (!options.filter || (options.filter !== 'all-local' && options.filter !== 'all')) {
- this.whereStateAndPrivacyAvailable(options.user)
+ if (options.serverAccountIdForBlock && !(options.include & VideoInclude.BLOCKED_OWNER)) {
+ this.whereNotBlocked(options.serverAccountIdForBlock, 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 () {
diff --git a/server/models/video/sql/videos-model-list-query-builder.ts b/server/models/video/sql/videos-model-list-query-builder.ts
index e61c51de8..ef92bd2b0 100644
--- a/server/models/video/sql/videos-model-list-query-builder.ts
+++ b/server/models/video/sql/videos-model-list-query-builder.ts
@@ -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}`
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index b5c46c86c..26be34329 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -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>> {
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>> {
accountId?: number
videoChannelId?: number
- followerActorId?: number
+ displayOnlyForFollower: DisplayOnlyForFollowerOptions | null
videoPlaylistId?: number
@@ -1082,7 +1053,7 @@ export class VideoModel extends Model>> {
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>> {
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>> {
'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>> {
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>> {
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>> {
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>> {
'tagsOneOf',
'tagsAllOf',
'user',
- 'filter',
+ 'isLocal',
'host',
'start',
'count',
@@ -1182,11 +1149,10 @@ export class VideoModel extends Model>> {
'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>> {
// 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>> {
// 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>> {
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)
diff --git a/server/tests/api/check-params/index.ts b/server/tests/api/check-params/index.ts
index a14e4d3e0..0882f8176 100644
--- a/server/tests/api/check-params/index.ts
+++ b/server/tests/api/check-params/index.ts
@@ -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
index 000000000..afe42b0d5
--- /dev/null
+++ b/server/tests/api/check-params/videos-common-filters.ts
@@ -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
index d08570bbe..000000000
--- a/server/tests/api/check-params/videos-filter.ts
+++ /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 ])
- })
-})
diff --git a/server/tests/api/videos/index.ts b/server/tests/api/videos/index.ts
index 5c07f8926..c9c678e9d 100644
--- a/server/tests/api/videos/index.ts
+++ b/server/tests/api/videos/index.ts
@@ -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'
diff --git a/server/tests/api/videos/multiple-servers.ts b/server/tests/api/videos/multiple-servers.ts
index df9deb1e1..9c255c1c5 100644
--- a/server/tests/api/videos/multiple-servers.ts
+++ b/server/tests/api/videos/multiple-servers.ts
@@ -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')
diff --git a/server/tests/api/videos/single-server.ts b/server/tests/api/videos/single-server.ts
index 29dac6ec1..a0e4a156c 100644
--- a/server/tests/api/videos/single-server.ts
+++ b/server/tests/api/videos/single-server.ts
@@ -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
index 000000000..eb2d2ab50
--- /dev/null
+++ b/server/tests/api/videos/videos-common-filters.ts
@@ -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
index 2306807bf..000000000
--- a/server/tests/api/videos/videos-filter.ts
+++ /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)
- })
-})
diff --git a/server/types/models/account/account.ts b/server/types/models/account/account.ts
index 984841291..abe0de27b 100644
--- a/server/types/models/account/account.ts
+++ b/server/types/models/account/account.ts
@@ -84,7 +84,7 @@ export type MAccountSummary =
export type MAccountSummaryBlocks =
MAccountSummary &
- Use<'BlockedAccounts', MAccountBlocklistId[]>
+ Use<'BlockedByAccounts', MAccountBlocklistId[]>
export type MAccountAPI =
MAccount &
diff --git a/shared/extra-utils/videos/videos-command.ts b/shared/extra-utils/videos/videos-command.ts
index c1a9ec806..68241f062 100644
--- a/shared/extra-utils/videos/videos-command.ts
+++ b/shared/extra-utils/videos/videos-command.ts
@@ -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'
])
}
diff --git a/shared/models/search/videos-common-query.model.ts b/shared/models/search/videos-common-query.model.ts
index 2f2e9a934..55a98e302 100644
--- a/shared/models/search/videos-common-query.model.ts
+++ b/shared/models/search/videos-common-query.model.ts
@@ -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
}
diff --git a/shared/models/search/videos-search-query.model.ts b/shared/models/search/videos-search-query.model.ts
index a5436879d..447c72806 100644
--- a/shared/models/search/videos-search-query.model.ts
+++ b/shared/models/search/videos-search-query.model.ts
@@ -23,4 +23,7 @@ export interface VideosSearchQueryAfterSanitize extends VideosSearchQuery {
start: number
count: number
sort: string
+
+ // FIXME: deprecated in 4.0, to remove
+ filter?: never
}
diff --git a/shared/models/videos/index.ts b/shared/models/videos/index.ts
index 733c433a0..3d3eedcc6 100644
--- a/shared/models/videos/index.ts
+++ b/shared/models/videos/index.ts
@@ -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-query.type.ts b/shared/models/videos/video-filter.type.ts
similarity index 100%
rename from shared/models/videos/video-query.type.ts
rename to shared/models/videos/video-filter.type.ts
diff --git a/shared/models/videos/video-include.enum.ts b/shared/models/videos/video-include.enum.ts
new file mode 100644
index 000000000..fa720b348
--- /dev/null
+++ b/shared/models/videos/video-include.enum.ts
@@ -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
+}
diff --git a/shared/models/videos/video.model.ts b/shared/models/videos/video.model.ts
index 4a7e399a2..dadde38af 100644
--- a/shared/models/videos/video.model.ts
+++ b/shared/models/videos/video.model.ts
@@ -43,13 +43,6 @@ export interface Video {
dislikes: number
nsfw: boolean
- waitTranscoding?: boolean
- state?: VideoConstant
- 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
+ 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
diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml
index ef4e7d04d..cdb4dd343 100644
--- a/support/doc/api/openapi.yaml
+++ b/support/doc/api/openapi.yaml
@@ -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:
|