Deprecate filter video query

Introduce include and isLocal instead
This commit is contained in:
Chocobozzz 2021-10-27 14:37:04 +02:00 committed by Chocobozzz
parent e4611b5491
commit 2760b454a7
52 changed files with 1135 additions and 489 deletions

View File

@ -63,10 +63,17 @@
<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>

View File

@ -7,4 +7,6 @@ my-embed {
.badge {
@include table-badge;
margin-right: 5px;
}

View 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 = []

View 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>

View 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) {

View 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)

View 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 },

View 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

View 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
}
})

View 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()
},

View 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()
},

View 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
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
}
}

View 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">

View 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) {

View 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,

View 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) {

View 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)))
}

View 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) {

View 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) {

View 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
})

View 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
})

View 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,

View 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',

View 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.'

View 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
}
}

View 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) {

View 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

View 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) {
const add = options.additionalAttributes
if (add?.state === true) {
videoObject.state = {
id: video.state,
label: getStateLabel(video.state)
}
}
if (options.additionalAttributes.waitTranscoding === true) {
if (add?.waitTranscoding === true) {
videoObject.waitTranscoding = video.waitTranscoding
}
if (options.additionalAttributes.scheduledUpdate === true && video.ScheduleVideoUpdate) {
if (add?.scheduledUpdate === true && video.ScheduleVideoUpdate) {
videoObject.scheduledUpdate = {
updateAt: video.ScheduleVideoUpdate.updateAt,
privacy: video.ScheduleVideoUpdate.privacy || undefined
}
}
if (options.additionalAttributes.blacklistInfo === true) {
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,

View 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"'

View 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.trackersDone = new Set<string>()
this.tagsDone = new Set<string>()
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

View File

@ -139,6 +139,10 @@ export class VideoTables {
return [ 'id', 'reason', 'unfederated' ]
}
getBlocklistAttributes () {
return [ 'id' ]
}
getScheduleUpdateAttributes () {
return [
'id',

View 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 ')

View 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"'
])
if (!(options.include & VideoInclude.BLACKLISTED)) {
this.whereNotBlacklisted()
if (options.serverAccountId) {
this.whereNotBlocked(options.serverAccountId, options.user)
}
// 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 () {

View 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}`

View 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)

View 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'

View File

@ -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 ])
})
})

View File

@ -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 ])
})
})

View 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'

View 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')

View 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)

View File

@ -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)
})
})

View File

@ -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)
})
})

View File

@ -84,7 +84,7 @@ export type MAccountSummary =
export type MAccountSummaryBlocks =
MAccountSummary &
Use<'BlockedAccounts', MAccountBlocklistId[]>
Use<'BlockedByAccounts', MAccountBlocklistId[]>
export type MAccountAPI =
MAccount &

View 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'
])
}

View 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
}

View 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
}

View 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'

View File

@ -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
}

View 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>

View 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: