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> <my-video-cell [video]="video"></my-video-cell>
</td> </td>
<td> <td class="badges">
<span class="badge badge-blue" i18n>{{ video.privacy.label }}</span> <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.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>
<td> <td>

View File

@ -7,4 +7,6 @@ my-embed {
.badge { .badge {
@include table-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 { ActivatedRoute, Router } from '@angular/router'
import { AuthService, ConfirmService, Notifier, RestPagination, RestTable } from '@app/core' import { AuthService, ConfirmService, Notifier, RestPagination, RestTable } from '@app/core'
import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' 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 { AdvancedInputFilter } from '@app/shared/shared-forms'
import { VideoActionsDisplayType } from '@app/shared/shared-video-miniature' import { VideoActionsDisplayType } from '@app/shared/shared-video-miniature'
@ -28,8 +28,12 @@ export class VideoListComponent extends RestTable implements OnInit {
title: $localize`Advanced filters`, title: $localize`Advanced filters`,
children: [ children: [
{ {
queryParams: { search: 'local:true' }, queryParams: { search: 'isLocal:false' },
label: $localize`Only local videos` 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() 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 () { protected reloadData () {
this.selectedVideos = [] this.selectedVideos = []

View File

@ -24,5 +24,5 @@
<div class="alert alert-danger" *ngIf="video?.blacklisted"> <div class="alert alert-danger" *ngIf="video?.blacklisted">
<div class="blocked-label" i18n>This video is blocked.</div> <div class="blocked-label" i18n>This video is blocked.</div>
{{ video.blockedReason }} {{ video.blacklistedReason }}
</div> </div>

View File

@ -85,7 +85,7 @@ export class VideosListCommonPageComponent implements OnInit, OnDestroy, Disable
getSyndicationItems (filters: VideoFilters) { getSyndicationItems (filters: VideoFilters) {
const result = filters.toVideosAPIObject() const result = filters.toVideosAPIObject()
return this.videoService.getVideoFeedUrls(result.sort, result.filter) return this.videoService.getVideoFeedUrls(result.sort, result.isLocal)
} }
onFiltersChanged (filters: VideoFilters) { onFiltersChanged (filters: VideoFilters) {

View File

@ -7,7 +7,6 @@ import {
ContainerMarkupData, ContainerMarkupData,
EmbedMarkupData, EmbedMarkupData,
PlaylistMiniatureMarkupData, PlaylistMiniatureMarkupData,
VideoFilter,
VideoMiniatureMarkupData, VideoMiniatureMarkupData,
VideosListMarkupData VideosListMarkupData
} from '@shared/models' } from '@shared/models'
@ -193,7 +192,7 @@ export class CustomMarkupService {
isLive: this.buildBoolean(data.isLive), 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) this.dynamicElementService.setModel(component, model)

View File

@ -1,7 +1,7 @@
import { finalize } from 'rxjs/operators' import { finalize } from 'rxjs/operators'
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
import { AuthService, Notifier } from '@app/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 { Video, VideoService } from '../../shared-main'
import { MiniatureDisplayOptions } from '../../shared-video-miniature' import { MiniatureDisplayOptions } from '../../shared-video-miniature'
import { CustomMarkupComponent } from './shared' import { CustomMarkupComponent } from './shared'
@ -21,7 +21,7 @@ export class VideosListMarkupComponent implements CustomMarkupComponent, OnInit
@Input() languageOneOf: string[] @Input() languageOneOf: string[]
@Input() count: number @Input() count: number
@Input() onlyDisplayTitle: boolean @Input() onlyDisplayTitle: boolean
@Input() filter: VideoFilter @Input() isLocal: boolean
@Input() isLive: boolean @Input() isLive: boolean
@Input() maxRows: number @Input() maxRows: number
@Input() channelHandle: string @Input() channelHandle: string
@ -86,7 +86,7 @@ export class VideosListMarkupComponent implements CustomMarkupComponent, OnInit
}, },
categoryOneOf: this.categoryOneOf, categoryOneOf: this.categoryOneOf,
languageOneOf: this.languageOneOf, languageOneOf: this.languageOneOf,
filter: this.filter, isLocal: this.isLocal,
isLive: this.isLive, isLive: this.isLive,
sort: this.sort as VideoSortField, sort: this.sort as VideoSortField,
account: { nameWithHost: this.accountHandle }, account: { nameWithHost: this.accountHandle },

View File

@ -65,8 +65,12 @@ export class Video implements VideoServerModel {
waitTranscoding?: boolean waitTranscoding?: boolean
state?: VideoConstant<VideoState> state?: VideoConstant<VideoState>
scheduledUpdate?: VideoScheduleUpdate scheduledUpdate?: VideoScheduleUpdate
blacklisted?: boolean blacklisted?: boolean
blockedReason?: string blacklistedReason?: string
blockedOwner?: boolean
blockedServer?: boolean
account: { account: {
id: number id: number
@ -163,7 +167,10 @@ export class Video implements VideoServerModel {
if (this.state) this.state.label = peertubeTranslate(this.state.label, translations) if (this.state) this.state.label = peertubeTranslate(this.state.label, translations)
this.blacklisted = hash.blacklisted this.blacklisted = hash.blacklisted
this.blockedReason = hash.blacklistedReason this.blacklistedReason = hash.blacklistedReason
this.blockedOwner = hash.blockedOwner
this.blockedServer = hash.blockedServer
this.userHistory = hash.userHistory this.userHistory = hash.userHistory

View File

@ -18,7 +18,7 @@ import {
VideoConstant, VideoConstant,
VideoDetails as VideoDetailsServerModel, VideoDetails as VideoDetailsServerModel,
VideoFileMetadata, VideoFileMetadata,
VideoFilter, VideoInclude,
VideoPrivacy, VideoPrivacy,
VideoSortField, VideoSortField,
VideoUpdate VideoUpdate
@ -34,11 +34,13 @@ import { Video } from './video.model'
export type CommonVideoParams = { export type CommonVideoParams = {
videoPagination?: ComponentPaginationLight videoPagination?: ComponentPaginationLight
sort: VideoSortField | SortMeta sort: VideoSortField | SortMeta
filter?: VideoFilter include?: VideoInclude
isLocal?: boolean
categoryOneOf?: number[] categoryOneOf?: number[]
languageOneOf?: string[] languageOneOf?: string[]
isLive?: boolean isLive?: boolean
skipCount?: boolean skipCount?: boolean
// FIXME: remove? // FIXME: remove?
nsfwPolicy?: NSFWPolicyType nsfwPolicy?: NSFWPolicyType
nsfw?: BooleanBothQuery nsfw?: BooleanBothQuery
@ -202,12 +204,14 @@ export class VideoService {
} }
getAdminVideos ( getAdminVideos (
parameters: Omit<CommonVideoParams, 'filter'> & { pagination: RestPagination, search?: string } parameters: CommonVideoParams & { pagination: RestPagination, search?: string }
): Observable<ResultList<Video>> { ): Observable<ResultList<Video>> {
const { pagination, search } = parameters const { pagination, search } = parameters
const include = VideoInclude.BLACKLISTED | VideoInclude.BLOCKED_OWNER | VideoInclude.HIDDEN_PRIVACY | VideoInclude.NOT_PUBLISHED_STATE
let params = new HttpParams() let params = new HttpParams()
params = this.buildCommonVideosParams({ params, ...parameters }) params = this.buildCommonVideosParams({ params, include, ...parameters })
params = params.set('start', pagination.start.toString()) params = params.set('start', pagination.start.toString())
.set('count', pagination.count.toString()) .set('count', pagination.count.toString())
@ -216,8 +220,6 @@ export class VideoService {
params = this.buildAdminParamsFromSearch(search, params) params = this.buildAdminParamsFromSearch(search, params)
} }
if (!params.has('filter')) params = params.set('filter', 'all')
return this.authHttp return this.authHttp
.get<ResultList<Video>>(VideoService.BASE_VIDEO_URL, { params }) .get<ResultList<Video>>(VideoService.BASE_VIDEO_URL, { params })
.pipe( .pipe(
@ -266,10 +268,10 @@ export class VideoService {
return feeds 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) 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) { if (categoryOneOf) {
for (const c of categoryOneOf) { for (const c of categoryOneOf) {
@ -425,7 +427,8 @@ export class VideoService {
params, params,
videoPagination, videoPagination,
sort, sort,
filter, isLocal,
include,
categoryOneOf, categoryOneOf,
languageOneOf, languageOneOf,
skipCount, skipCount,
@ -440,9 +443,10 @@ export class VideoService {
let newParams = this.restService.addRestGetParams(params, pagination, sort) let newParams = this.restService.addRestGetParams(params, pagination, sort)
if (filter) newParams = newParams.set('filter', filter)
if (skipCount) newParams = newParams.set('skipCount', skipCount + '') 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 (isLive) newParams = newParams.set('isLive', isLive)
if (nsfw) newParams = newParams.set('nsfw', nsfw) if (nsfw) newParams = newParams.set('nsfw', nsfw)
if (nsfwPolicy) newParams = newParams.set('nsfw', this.nsfwPolicyToParam(nsfwPolicy)) if (nsfwPolicy) newParams = newParams.set('nsfw', this.nsfwPolicyToParam(nsfwPolicy))
@ -454,13 +458,9 @@ export class VideoService {
private buildAdminParamsFromSearch (search: string, params: HttpParams) { private buildAdminParamsFromSearch (search: string, params: HttpParams) {
const filters = this.restService.parseQueryStringFilter(search, { const filters = this.restService.parseQueryStringFilter(search, {
filter: { isLocal: {
prefix: 'local:', prefix: 'isLocal:',
handler: v => { isBoolean: true
if (v === 'true') return 'all-local'
return 'all'
}
} }
}) })

View File

@ -61,7 +61,7 @@ export class VideoBlockComponent extends FormReactive implements OnInit {
this.hide() this.hide()
this.video.blacklisted = true this.video.blacklisted = true
this.video.blockedReason = reason this.video.blacklistedReason = reason
this.videoBlocked.emit() this.videoBlocked.emit()
}, },

View File

@ -188,7 +188,7 @@ export class VideoActionsDropdownComponent implements OnChanges {
this.notifier.success($localize`Video ${this.video.name} unblocked.`) this.notifier.success($localize`Video ${this.video.name} unblocked.`)
this.video.blacklisted = false this.video.blacklisted = false
this.video.blockedReason = null this.video.blacklistedReason = null
this.videoUnblocked.emit() this.videoUnblocked.emit()
}, },

View File

@ -1,6 +1,6 @@
import { intoArray, toBoolean } from '@app/helpers' import { intoArray, toBoolean } from '@app/helpers'
import { AttributesOnly } from '@shared/core-utils' import { AttributesOnly } from '@shared/core-utils'
import { BooleanBothQuery, NSFWPolicyType, VideoFilter, VideoSortField } from '@shared/models' import { BooleanBothQuery, NSFWPolicyType, VideoInclude, VideoSortField } from '@shared/models'
type VideoFiltersKeys = { type VideoFiltersKeys = {
[ id in keyof AttributesOnly<VideoFilters> ]: any [ id in keyof AttributesOnly<VideoFilters> ]: any
@ -196,14 +196,15 @@ export class VideoFilters {
} }
toVideosAPIObject () { toVideosAPIObject () {
let filter: VideoFilter let isLocal: boolean
let include: VideoInclude
if (this.scope === 'local' && this.allVideos) { if (this.scope === 'local') {
filter = 'all-local' isLocal = true
} else if (this.scope === 'federated' && this.allVideos) { }
filter = 'all'
} else if (this.scope === 'local') { if (this.allVideos) {
filter = 'local' include = VideoInclude.NOT_PUBLISHED_STATE | VideoInclude.HIDDEN_PRIVACY
} }
let isLive: boolean let isLive: boolean
@ -216,7 +217,8 @@ export class VideoFilters {
languageOneOf: this.languageOneOf, languageOneOf: this.languageOneOf,
categoryOneOf: this.categoryOneOf, categoryOneOf: this.categoryOneOf,
search: this.search, search: this.search,
filter, isLocal,
include,
isLive isLive
} }
} }

View File

@ -55,7 +55,7 @@
<div *ngIf="displayOptions.blacklistInfo && video.blacklisted" class="video-info-blocked"> <div *ngIf="displayOptions.blacklistInfo && video.blacklisted" class="video-info-blocked">
<span class="blocked-label" i18n>Blocked</span> <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>
<div i18n *ngIf="displayOptions.nsfw && video.nsfw" class="video-info-nsfw"> <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 { pickCommonVideoQuery } from '@server/helpers/query'
import { ActorFollowModel } from '@server/models/actor/actor-follow' import { ActorFollowModel } from '@server/models/actor/actor-follow'
import { getServerActor } from '@server/models/application/application' 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 { buildNSFWFilter, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils'
import { getFormattedObjects } from '../../helpers/utils' import { getFormattedObjects } from '../../helpers/utils'
import { JobQueue } from '../../lib/job-queue' 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) { async function listAccountVideos (req: express.Request, res: express.Response) {
const serverActor = await getServerActor()
const account = res.locals.account 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 countVideos = getCountVideos(req)
const query = pickCommonVideoQuery(req.query) const query = pickCommonVideoQuery(req.query)
const apiOptions = await Hooks.wrapObject({ const apiOptions = await Hooks.wrapObject({
...query, ...query,
followerActorId, displayOnlyForFollower,
search: req.query.search,
includeLocalVideos: true,
nsfw: buildNSFWFilter(res, query.nsfw), nsfw: buildNSFWFilter(res, query.nsfw),
withFiles: false, withFiles: false,
accountId: account.id, accountId: account.id,
@ -193,7 +201,7 @@ async function listAccountVideos (req: express.Request, res: express.Response) {
'filter:api.accounts.videos.list.result' '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) { 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 { MEMOIZE_TTL, OVERVIEWS } from '../../initializers/constants'
import { asyncMiddleware, optionalAuthenticate, videosOverviewValidator } from '../../middlewares' import { asyncMiddleware, optionalAuthenticate, videosOverviewValidator } from '../../middlewares'
import { TagModel } from '../../models/video/tag' import { TagModel } from '../../models/video/tag'
import { getServerActor } from '@server/models/application/application'
const overviewsRouter = express.Router() const overviewsRouter = express.Router()
@ -109,11 +110,16 @@ async function getVideos (
res: express.Response, res: express.Response,
where: { videoChannelId?: number, tagsOneOf?: string[], categoryOneOf?: number[] } where: { videoChannelId?: number, tagsOneOf?: string[], categoryOneOf?: number[] }
) { ) {
const serverActor = await getServerActor()
const query = await Hooks.wrapObject({ const query = await Hooks.wrapObject({
start: 0, start: 0,
count: 12, count: 12,
sort: '-createdAt', sort: '-createdAt',
includeLocalVideos: true, displayOnlyForFollower: {
actorId: serverActor.id,
orLocalVideos: true
},
nsfw: buildNSFWFilter(res), nsfw: buildNSFWFilter(res),
user: res.locals.oauth ? res.locals.oauth.token.User : undefined, user: res.locals.oauth ? res.locals.oauth.token.User : undefined,
withFiles: false, withFiles: false,

View File

@ -7,6 +7,8 @@ import { WEBSERVER } from '@server/initializers/constants'
import { getOrCreateAPVideo } from '@server/lib/activitypub/videos' import { getOrCreateAPVideo } from '@server/lib/activitypub/videos'
import { Hooks } from '@server/lib/plugins/hooks' import { Hooks } from '@server/lib/plugins/hooks'
import { buildMutedForSearchIndex, isSearchIndexSearch, isURISearch } from '@server/lib/search' 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 { HttpStatusCode, ResultList, Video } from '@shared/models'
import { VideosSearchQueryAfterSanitize } from '../../../../shared/models/search' import { VideosSearchQueryAfterSanitize } from '../../../../shared/models/search'
import { buildNSFWFilter, isUserAbleToSearchRemoteURI } from '../../../helpers/express-utils' 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) { async function searchVideosDB (query: VideosSearchQueryAfterSanitize, res: express.Response) {
const serverActor = await getServerActor()
const apiOptions = await Hooks.wrapObject({ const apiOptions = await Hooks.wrapObject({
...query, ...query,
includeLocalVideos: true, displayOnlyForFollower: {
filter: query.filter, actorId: serverActor.id,
orLocalVideos: true
},
nsfw: buildNSFWFilter(res, query.nsfw), nsfw: buildNSFWFilter(res, query.nsfw),
user: res.locals.oauth user: res.locals.oauth
@ -118,7 +124,7 @@ async function searchVideosDB (query: VideosSearchQueryAfterSanitize, res: expre
'filter:api.search.videos.local.list.result' '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) { async function searchVideoURI (url: string, res: express.Response) {

View File

@ -2,6 +2,7 @@ import 'multer'
import express from 'express' import express from 'express'
import { pickCommonVideoQuery } from '@server/helpers/query' import { pickCommonVideoQuery } from '@server/helpers/query'
import { sendUndoFollow } from '@server/lib/activitypub/send' 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 { VideoChannelModel } from '@server/models/video/video-channel'
import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils' 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({ const resultList = await VideoModel.listForApi({
...query, ...query,
includeLocalVideos: false, displayOnlyForFollower: {
actorId: user.Account.Actor.id,
orLocalVideos: false
},
nsfw: buildNSFWFilter(res, query.nsfw), nsfw: buildNSFWFilter(res, query.nsfw),
withFiles: false, withFiles: false,
followerActorId: user.Account.Actor.id,
user, user,
countVideos 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 { Hooks } from '@server/lib/plugins/hooks'
import { ActorFollowModel } from '@server/models/actor/actor-follow' import { ActorFollowModel } from '@server/models/actor/actor-follow'
import { getServerActor } from '@server/models/application/application' import { getServerActor } from '@server/models/application/application'
import { guessAdditionalAttributesFromQuery } from '@server/models/video/formatter/video-format-utils'
import { MChannelBannerAccountDefault } from '@server/types/models' import { MChannelBannerAccountDefault } from '@server/types/models'
import { ActorImageType, VideoChannelCreate, VideoChannelUpdate } from '../../../shared' import { ActorImageType, VideoChannelCreate, VideoChannelUpdate } from '../../../shared'
import { HttpStatusCode } from '../../../shared/models/http/http-error-codes' 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) { async function listVideoChannelVideos (req: express.Request, res: express.Response) {
const serverActor = await getServerActor()
const videoChannelInstance = res.locals.videoChannel 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 countVideos = getCountVideos(req)
const query = pickCommonVideoQuery(req.query) const query = pickCommonVideoQuery(req.query)
const apiOptions = await Hooks.wrapObject({ const apiOptions = await Hooks.wrapObject({
...query, ...query,
followerActorId, displayOnlyForFollower,
includeLocalVideos: true,
nsfw: buildNSFWFilter(res, query.nsfw), nsfw: buildNSFWFilter(res, query.nsfw),
withFiles: false, withFiles: false,
videoChannelId: videoChannelInstance.id, videoChannelId: videoChannelInstance.id,
@ -350,7 +359,7 @@ async function listVideoChannelVideos (req: express.Request, res: express.Respon
'filter:api.video-channels.videos.list.result' '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) { 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 { LiveManager } from '@server/lib/live'
import { openapiOperationDoc } from '@server/middlewares/doc' import { openapiOperationDoc } from '@server/middlewares/doc'
import { getServerActor } from '@server/models/application/application' import { getServerActor } from '@server/models/application/application'
import { guessAdditionalAttributesFromQuery } from '@server/models/video/formatter/video-format-utils'
import { MVideoAccountLight } from '@server/types/models' import { MVideoAccountLight } from '@server/types/models'
import { HttpStatusCode } from '../../../../shared/models' import { HttpStatusCode } from '../../../../shared/models'
import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' 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) { async function listVideos (req: express.Request, res: express.Response) {
const serverActor = await getServerActor()
const query = pickCommonVideoQuery(req.query) const query = pickCommonVideoQuery(req.query)
const countVideos = getCountVideos(req) const countVideos = getCountVideos(req)
const apiOptions = await Hooks.wrapObject({ const apiOptions = await Hooks.wrapObject({
...query, ...query,
includeLocalVideos: true, displayOnlyForFollower: {
actorId: serverActor.id,
orLocalVideos: true
},
nsfw: buildNSFWFilter(res, query.nsfw), nsfw: buildNSFWFilter(res, query.nsfw),
withFiles: false, withFiles: false,
user: res.locals.oauth ? res.locals.oauth.token.User : undefined, 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' '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) { 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 express from 'express'
import { truncate } from 'lodash' import { truncate } from 'lodash'
import { SitemapStream, streamToPromise } from 'sitemap' import { SitemapStream, streamToPromise } from 'sitemap'
@ -63,13 +64,18 @@ async function getSitemapAccountUrls () {
} }
async function getSitemapLocalVideoUrls () { async function getSitemapLocalVideoUrls () {
const serverActor = await getServerActor()
const { data } = await VideoModel.listForApi({ const { data } = await VideoModel.listForApi({
start: 0, start: 0,
count: undefined, count: undefined,
sort: 'createdAt', sort: 'createdAt',
includeLocalVideos: true, displayOnlyForFollower: {
actorId: serverActor.id,
orLocalVideos: true
},
isLocal: true,
nsfw: buildNSFWFilter(), nsfw: buildNSFWFilter(),
filter: 'local',
withFiles: false, withFiles: false,
countVideos: false countVideos: false
}) })

View File

@ -1,7 +1,7 @@
import express from 'express' import express from 'express'
import Feed from 'pfeed' import Feed from 'pfeed'
import { getServerActor } from '@server/models/application/application'
import { getCategoryLabel } from '@server/models/video/formatter/video-format-utils' 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 { buildNSFWFilter } from '../helpers/express-utils'
import { CONFIG } from '../initializers/config' import { CONFIG } from '../initializers/config'
import { FEEDS, PREVIEWS_SIZE, ROUTE_CACHE_LIFETIME, WEBSERVER } from '../initializers/constants' 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 videoChannelId: videoChannel ? videoChannel.id : null
} }
const server = await getServerActor()
const { data } = await VideoModel.listForApi({ const { data } = await VideoModel.listForApi({
start, start,
count: FEEDS.COUNT, count: FEEDS.COUNT,
sort: req.query.sort, sort: req.query.sort,
includeLocalVideos: true, displayOnlyForFollower: {
actorId: server.id,
orLocalVideos: true
},
nsfw, nsfw,
filter: req.query.filter as VideoFilter, isLocal: req.query.isLocal,
include: req.query.include,
withFiles: true, withFiles: true,
countVideos: false, countVideos: false,
...options ...options
@ -196,14 +201,18 @@ async function generateVideoFeedForSubscriptions (req: express.Request, res: exp
start, start,
count: FEEDS.COUNT, count: FEEDS.COUNT,
sort: req.query.sort, sort: req.query.sort,
includeLocalVideos: false,
nsfw, nsfw,
filter: req.query.filter as VideoFilter,
isLocal: req.query.isLocal,
include: req.query.include,
withFiles: true, withFiles: true,
countVideos: false, countVideos: false,
followerActorId: res.locals.user.Account.Actor.id, displayOnlyForFollower: {
actorId: res.locals.user.Account.Actor.id,
orLocalVideos: false
},
user: res.locals.user user: res.locals.user
}) })

View File

@ -2,6 +2,7 @@ import { UploadFilesForCheck } from 'express'
import { values } from 'lodash' import { values } from 'lodash'
import magnetUtil from 'magnet-uri' import magnetUtil from 'magnet-uri'
import validator from 'validator' import validator from 'validator'
import { VideoInclude } from '@shared/models'
import { VideoFilter, VideoPrivacy, VideoRateType } from '../../../shared' import { VideoFilter, VideoPrivacy, VideoRateType } from '../../../shared'
import { import {
CONSTRAINTS_FIELDS, CONSTRAINTS_FIELDS,
@ -21,6 +22,10 @@ function isVideoFilterValid (filter: VideoFilter) {
return filter === 'local' || filter === 'all-local' || filter === 'all' return filter === 'local' || filter === 'all-local' || filter === 'all'
} }
function isVideoIncludeValid (include: VideoInclude) {
return exists(include) && validator.isInt('' + include)
}
function isVideoCategoryValid (value: any) { function isVideoCategoryValid (value: any) {
return value === null || VIDEO_CATEGORIES[value] !== undefined return value === null || VIDEO_CATEGORIES[value] !== undefined
} }
@ -146,6 +151,7 @@ export {
isVideoOriginallyPublishedAtValid, isVideoOriginallyPublishedAtValid,
isVideoMagnetUriValid, isVideoMagnetUriValid,
isVideoStateValid, isVideoStateValid,
isVideoIncludeValid,
isVideoViewsValid, isVideoViewsValid,
isVideoRatingTypeValid, isVideoRatingTypeValid,
isVideoFileExtnameValid, isVideoFileExtnameValid,

View File

@ -18,8 +18,10 @@ function pickCommonVideoQuery (query: VideosCommonQueryAfterSanitize) {
'languageOneOf', 'languageOneOf',
'tagsOneOf', 'tagsOneOf',
'tagsAllOf', 'tagsAllOf',
'filter', 'isLocal',
'skipCount' 'include',
'skipCount',
'search'
]) ])
} }
@ -29,7 +31,6 @@ function pickSearchVideoQuery (query: VideosSearchQueryAfterSanitize) {
...pick(query, [ ...pick(query, [
'searchTarget', 'searchTarget',
'search',
'host', 'host',
'startDate', 'startDate',
'endDate', 'endDate',

View File

@ -7,6 +7,7 @@ import { isAbleToUploadVideo } from '@server/lib/user'
import { getServerActor } from '@server/models/application/application' import { getServerActor } from '@server/models/application/application'
import { ExpressPromiseHandler } from '@server/types/express' import { ExpressPromiseHandler } from '@server/types/express'
import { MUserAccountId, MVideoFullLight } from '@server/types/models' import { MUserAccountId, MVideoFullLight } from '@server/types/models'
import { VideoInclude } from '@shared/models'
import { ServerErrorCode, UserRight, VideoPrivacy } from '../../../../shared' import { ServerErrorCode, UserRight, VideoPrivacy } from '../../../../shared'
import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
import { import {
@ -30,6 +31,7 @@ import {
isVideoFileSizeValid, isVideoFileSizeValid,
isVideoFilterValid, isVideoFilterValid,
isVideoImage, isVideoImage,
isVideoIncludeValid,
isVideoLanguageValid, isVideoLanguageValid,
isVideoLicenceValid, isVideoLicenceValid,
isVideoNameValid, isVideoNameValid,
@ -487,6 +489,13 @@ const commonVideosFiltersValidator = [
query('filter') query('filter')
.optional() .optional()
.custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'), .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') query('skipCount')
.optional() .optional()
.customSanitizer(toBooleanOrNull) .customSanitizer(toBooleanOrNull)
@ -500,11 +509,23 @@ const commonVideosFiltersValidator = [
if (areValidationErrors(req, res)) return if (areValidationErrors(req, res)) return
const user = res.locals.oauth ? res.locals.oauth.token.User : undefined // FIXME: deprecated in 4.0, to remove
if ( {
(req.query.filter === 'all-local' || req.query.filter === 'all') && if (req.query.filter === 'all-local') {
(!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false) 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({ res.fail({
status: HttpStatusCode.UNAUTHORIZED_401, status: HttpStatusCode.UNAUTHORIZED_401,
message: 'You are not allowed to see all local videos.' 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', name: 'targetAccountId',
allowNull: false allowNull: false
}, },
as: 'BlockedAccounts', as: 'BlockedBy',
onDelete: 'CASCADE' onDelete: 'CASCADE'
}) })
BlockedAccounts: AccountBlocklistModel[] BlockedBy: AccountBlocklistModel[]
@BeforeDestroy @BeforeDestroy
static async sendDeleteIfOwned (instance: AccountModel, options) { static async sendDeleteIfOwned (instance: AccountModel, options) {
@ -457,6 +457,6 @@ export class AccountModel extends Model<Partial<AttributesOnly<AccountModel>>> {
} }
isBlocked () { 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' onDelete: 'CASCADE'
}) })
BlockedByAccounts: ServerBlocklistModel[] BlockedBy: ServerBlocklistModel[]
static load (id: number, transaction?: Transaction): Promise<MServer> { static load (id: number, transaction?: Transaction): Promise<MServer> {
const query = { const query = {
@ -81,7 +81,7 @@ export class ServerModel extends Model<Partial<AttributesOnly<ServerModel>>> {
} }
isBlocked () { isBlocked () {
return this.BlockedByAccounts && this.BlockedByAccounts.length !== 0 return this.BlockedBy && this.BlockedBy.length !== 0
} }
toFormattedJSON (this: MServerFormattable) { toFormattedJSON (this: MServerFormattable) {

View File

@ -4,6 +4,7 @@ import { MUserAccountId, MUserId } from '@server/types/models'
import { AttributesOnly } from '@shared/core-utils' import { AttributesOnly } from '@shared/core-utils'
import { VideoModel } from '../video/video' import { VideoModel } from '../video/video'
import { UserModel } from './user' import { UserModel } from './user'
import { getServerActor } from '../application/application'
@Table({ @Table({
tableName: 'userVideoHistory', tableName: 'userVideoHistory',
@ -56,14 +57,19 @@ export class UserVideoHistoryModel extends Model<Partial<AttributesOnly<UserVide
}) })
User: UserModel 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({ return VideoModel.listForApi({
start, start,
count, count,
search, search,
sort: '-"userVideoHistory"."updatedAt"', sort: '-"userVideoHistory"."updatedAt"',
nsfw: null, // All nsfw: null, // All
includeLocalVideos: true, displayOnlyForFollower: {
actorId: serverActor.id,
orLocalVideos: true
},
withFiles: false, withFiles: false,
user, user,
historyOfUser: user historyOfUser: user

View File

@ -1,9 +1,10 @@
import { uuidToShort } from '@server/helpers/uuid' import { uuidToShort } from '@server/helpers/uuid'
import { generateMagnetUri } from '@server/helpers/webtorrent' import { generateMagnetUri } from '@server/helpers/webtorrent'
import { getLocalVideoFileMetadataUrl } from '@server/lib/video-urls' import { getLocalVideoFileMetadataUrl } from '@server/lib/video-urls'
import { VideosCommonQueryAfterSanitize } from '@shared/models'
import { VideoFile } from '@shared/models/videos/video-file.model' import { VideoFile } from '@shared/models/videos/video-file.model'
import { ActivityTagObject, ActivityUrlObject, VideoObject } from '../../../../shared/models/activitypub/objects' 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 { VideoStreamingPlaylist } from '../../../../shared/models/videos/video-streaming-playlist.model'
import { isArray } from '../../../helpers/custom-validators/misc' import { isArray } from '../../../helpers/custom-validators/misc'
import { import {
@ -22,6 +23,7 @@ import {
getLocalVideoSharesActivityPubUrl getLocalVideoSharesActivityPubUrl
} from '../../../lib/activitypub/url' } from '../../../lib/activitypub/url'
import { import {
MServer,
MStreamingPlaylistRedundanciesOpt, MStreamingPlaylistRedundanciesOpt,
MVideo, MVideo,
MVideoAP, MVideoAP,
@ -34,15 +36,31 @@ import { VideoCaptionModel } from '../video-caption'
export type VideoFormattingJSONOptions = { export type VideoFormattingJSONOptions = {
completeDescription?: boolean completeDescription?: boolean
additionalAttributes: {
additionalAttributes?: {
state?: boolean state?: boolean
waitTranscoding?: boolean waitTranscoding?: boolean
scheduledUpdate?: boolean scheduledUpdate?: boolean
blacklistInfo?: 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 userHistory = isArray(video.UserVideoHistories) ? video.UserVideoHistories[0] : undefined
const videoObject: Video = { const videoObject: Video = {
@ -101,29 +119,35 @@ function videoModelToFormattedJSON (video: MVideoFormattable, options?: VideoFor
pluginData: (video as any).pluginData pluginData: (video as any).pluginData
} }
if (options) { const add = options.additionalAttributes
if (options.additionalAttributes.state === true) { if (add?.state === true) {
videoObject.state = { videoObject.state = {
id: video.state, id: video.state,
label: getStateLabel(video.state) label: getStateLabel(video.state)
}
} }
}
if (options.additionalAttributes.waitTranscoding === true) { if (add?.waitTranscoding === true) {
videoObject.waitTranscoding = video.waitTranscoding videoObject.waitTranscoding = video.waitTranscoding
} }
if (options.additionalAttributes.scheduledUpdate === true && video.ScheduleVideoUpdate) { if (add?.scheduledUpdate === true && video.ScheduleVideoUpdate) {
videoObject.scheduledUpdate = { videoObject.scheduledUpdate = {
updateAt: video.ScheduleVideoUpdate.updateAt, updateAt: video.ScheduleVideoUpdate.updateAt,
privacy: video.ScheduleVideoUpdate.privacy || undefined privacy: video.ScheduleVideoUpdate.privacy || undefined
}
} }
}
if (options.additionalAttributes.blacklistInfo === true) { if (add?.blacklistInfo === true) {
videoObject.blacklisted = !!video.VideoBlacklist videoObject.blacklisted = !!video.VideoBlacklist
videoObject.blacklistedReason = video.VideoBlacklist ? video.VideoBlacklist.reason : null 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 return videoObject
@ -464,6 +488,8 @@ export {
videoModelToActivityPubObject, videoModelToActivityPubObject,
getActivityStreamDuration, getActivityStreamDuration,
guessAdditionalAttributesFromQuery,
getCategoryLabel, getCategoryLabel,
getLicenceLabel, getLicenceLabel,
getLanguageLabel, getLanguageLabel,

View File

@ -1,3 +1,5 @@
import { createSafeIn } from '@server/models/utils'
import { MUserAccountId } from '@server/types/models'
import validator from 'validator' import validator from 'validator'
import { AbstractVideosQueryBuilder } from './abstract-videos-query-builder' import { AbstractVideosQueryBuilder } from './abstract-videos-query-builder'
import { VideoTables } from './video-tables' 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 () { protected includeScheduleUpdate () {
this.addJoin( this.addJoin(
'LEFT OUTER JOIN "scheduleVideoUpdate" AS "ScheduleVideoUpdate" ON "video"."id" = "ScheduleVideoUpdate"."videoId"' '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 { AccountModel } from '@server/models/account/account'
import { AccountBlocklistModel } from '@server/models/account/account-blocklist'
import { ActorModel } from '@server/models/actor/actor' import { ActorModel } from '@server/models/actor/actor'
import { ActorImageModel } from '@server/models/actor/actor-image' import { ActorImageModel } from '@server/models/actor/actor-image'
import { VideoRedundancyModel } from '@server/models/redundancy/video-redundancy' import { VideoRedundancyModel } from '@server/models/redundancy/video-redundancy'
import { ServerModel } from '@server/models/server/server' import { ServerModel } from '@server/models/server/server'
import { ServerBlocklistModel } from '@server/models/server/server-blocklist'
import { TrackerModel } from '@server/models/server/tracker' import { TrackerModel } from '@server/models/server/tracker'
import { UserVideoHistoryModel } from '@server/models/user/user-video-history' import { UserVideoHistoryModel } from '@server/models/user/user-video-history'
import { VideoInclude } from '@shared/models'
import { ScheduleVideoUpdateModel } from '../../schedule-video-update' import { ScheduleVideoUpdateModel } from '../../schedule-video-update'
import { TagModel } from '../../tag' import { TagModel } from '../../tag'
import { ThumbnailModel } from '../../thumbnail' import { ThumbnailModel } from '../../thumbnail'
@ -33,6 +36,8 @@ export class VideoModelBuilder {
private thumbnailsDone: Set<any> private thumbnailsDone: Set<any>
private historyDone: Set<any> private historyDone: Set<any>
private blacklistDone: Set<any> private blacklistDone: Set<any>
private accountBlocklistDone: Set<any>
private serverBlocklistDone: Set<any>
private liveDone: Set<any> private liveDone: Set<any>
private redundancyDone: Set<any> private redundancyDone: Set<any>
private scheduleVideoUpdateDone: 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() this.reinit()
for (const row of rows) { for (const row of rows) {
@ -77,6 +89,15 @@ export class VideoModelBuilder {
this.setBlacklisted(row, videoModel) this.setBlacklisted(row, videoModel)
this.setScheduleVideoUpdate(row, videoModel) this.setScheduleVideoUpdate(row, videoModel)
this.setLive(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.videoStreamingPlaylistMemo = {}
this.videoFileMemo = {} this.videoFileMemo = {}
this.thumbnailsDone = new Set<number>() this.thumbnailsDone = new Set()
this.historyDone = new Set<number>() this.historyDone = new Set()
this.blacklistDone = new Set<number>() this.blacklistDone = new Set()
this.liveDone = new Set<number>() this.liveDone = new Set()
this.redundancyDone = new Set<number>() this.redundancyDone = new Set()
this.scheduleVideoUpdateDone = new Set<number>() this.scheduleVideoUpdateDone = new Set()
this.trackersDone = new Set<string>() this.accountBlocklistDone = new Set()
this.tagsDone = new Set<string>() this.serverBlocklistDone = new Set()
this.trackersDone = new Set()
this.tagsDone = new Set()
this.videos = [] this.videos = []
} }
@ -162,6 +186,8 @@ export class VideoModelBuilder {
const accountModel = new AccountModel(this.grab(row, this.tables.getAccountAttributes(), 'VideoChannel.Account'), this.buildOpts) const accountModel = new AccountModel(this.grab(row, this.tables.getAccountAttributes(), 'VideoChannel.Account'), this.buildOpts)
accountModel.Actor = this.buildActor(row, 'VideoChannel.Account') accountModel.Actor = this.buildActor(row, 'VideoChannel.Account')
accountModel.BlockedBy = []
channelModel.Account = accountModel channelModel.Account = accountModel
videoModel.VideoChannel = channelModel videoModel.VideoChannel = channelModel
@ -180,6 +206,8 @@ export class VideoModelBuilder {
? new ServerModel(this.grab(row, this.tables.getServerAttributes(), serverPrefix), this.buildOpts) ? new ServerModel(this.grab(row, this.tables.getServerAttributes(), serverPrefix), this.buildOpts)
: null : null
if (serverModel) serverModel.BlockedBy = []
const actorModel = new ActorModel(this.grab(row, this.tables.getActorAttributes(), actorPrefix), this.buildOpts) const actorModel = new ActorModel(this.grab(row, this.tables.getActorAttributes(), actorPrefix), this.buildOpts)
actorModel.Avatar = avatarModel actorModel.Avatar = avatarModel
actorModel.Server = serverModel actorModel.Server = serverModel
@ -297,6 +325,32 @@ export class VideoModelBuilder {
this.blacklistDone.add(id) 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) { private setScheduleVideoUpdate (row: SQLRow, videoModel: VideoModel) {
const id = row['ScheduleVideoUpdate.id'] const id = row['ScheduleVideoUpdate.id']
if (!id || this.scheduleVideoUpdateDone.has(id)) return if (!id || this.scheduleVideoUpdateDone.has(id)) return

View File

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

View File

@ -62,7 +62,11 @@ export class VideosModelGetQueryBuilder {
: Promise.resolve(undefined) : 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) { if (videos.length > 1) {
throw new Error('Video results is more than ') 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 { WEBSERVER } from '@server/initializers/constants'
import { buildDirectionAndField, createSafeIn } from '@server/models/utils' import { buildDirectionAndField, createSafeIn } from '@server/models/utils'
import { MUserAccountId, MUserId } from '@server/types/models' 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' 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 = { export type BuildVideosListQueryOptions = {
attributes?: string[] attributes?: string[]
serverAccountId: number serverAccountIdForBlock: number
followerActorId: number
includeLocalVideos: boolean displayOnlyForFollower: DisplayOnlyForFollowerOptions
count: number count: number
start: number start: number
sort: string sort: string
nsfw?: boolean nsfw?: boolean
filter?: VideoFilter
host?: string host?: string
isLive?: boolean isLive?: boolean
isLocal?: boolean
include?: VideoInclude
categoryOneOf?: number[] categoryOneOf?: number[]
licenceOneOf?: number[] licenceOneOf?: number[]
@ -101,6 +107,7 @@ export class VideosIdListQueryBuilder extends AbstractVideosQueryBuilder {
getIdsListQueryAndSort (options: BuildVideosListQueryOptions) { getIdsListQueryAndSort (options: BuildVideosListQueryOptions) {
this.buildIdsListQuery(options) this.buildIdsListQuery(options)
return { query: this.query, sort: this.sort, replacements: this.replacements } 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"' 'INNER JOIN "actor" "accountActor" ON "account"."actorId" = "accountActor"."id"'
]) ])
this.whereNotBlacklisted() if (!(options.include & VideoInclude.BLACKLISTED)) {
this.whereNotBlacklisted()
if (options.serverAccountId) {
this.whereNotBlocked(options.serverAccountId, options.user)
} }
// Only list public/published videos if (options.serverAccountIdForBlock && !(options.include & VideoInclude.BLOCKED_OWNER)) {
if (!options.filter || (options.filter !== 'all-local' && options.filter !== 'all')) { this.whereNotBlocked(options.serverAccountIdForBlock, options.user)
this.whereStateAndPrivacyAvailable(options.user) }
// Only list published videos
if (!(options.include & VideoInclude.NOT_PUBLISHED_STATE)) {
this.whereStateAvailable()
}
// Only list videos with the appropriate priavcy
if (!(options.include & VideoInclude.HIDDEN_PRIVACY)) {
this.wherePrivacyAvailable(options.user)
} }
if (options.videoPlaylistId) { if (options.videoPlaylistId) {
this.joinPlaylist(options.videoPlaylistId) this.joinPlaylist(options.videoPlaylistId)
} }
if (options.filter && (options.filter === 'local' || options.filter === 'all-local')) { if (exists(options.isLocal)) {
this.whereOnlyLocal() this.whereLocal(options.isLocal)
} }
if (options.host) { if (options.host) {
@ -147,8 +161,8 @@ export class VideosIdListQueryBuilder extends AbstractVideosQueryBuilder {
this.whereChannelId(options.videoChannelId) this.whereChannelId(options.videoChannelId)
} }
if (options.followerActorId) { if (options.displayOnlyForFollower) {
this.whereFollowerActorId(options.followerActorId, options.includeLocalVideos) this.whereFollowerActorId(options.displayOnlyForFollower)
} }
if (options.withFiles === true) { if (options.withFiles === true) {
@ -282,12 +296,14 @@ export class VideosIdListQueryBuilder extends AbstractVideosQueryBuilder {
this.replacements.videoPlaylistId = playlistId this.replacements.videoPlaylistId = playlistId
} }
private whereStateAndPrivacyAvailable (user?: MUserAccountId) { private whereStateAvailable () {
this.and.push( this.and.push(
`("video"."state" = ${VideoState.PUBLISHED} OR ` + `("video"."state" = ${VideoState.PUBLISHED} OR ` +
`("video"."state" = ${VideoState.TO_TRANSCODE} AND "video"."waitTranscoding" IS false))` `("video"."state" = ${VideoState.TO_TRANSCODE} AND "video"."waitTranscoding" IS false))`
) )
}
private wherePrivacyAvailable (user?: MUserAccountId) {
if (user) { if (user) {
this.and.push( this.and.push(
`("video"."privacy" = ${VideoPrivacy.PUBLIC} OR "video"."privacy" = ${VideoPrivacy.INTERNAL})` `("video"."privacy" = ${VideoPrivacy.PUBLIC} OR "video"."privacy" = ${VideoPrivacy.INTERNAL})`
@ -299,8 +315,10 @@ export class VideosIdListQueryBuilder extends AbstractVideosQueryBuilder {
} }
} }
private whereOnlyLocal () { private whereLocal (isLocal: boolean) {
this.and.push('"video"."remote" IS FALSE') const isRemote = isLocal ? 'FALSE' : 'TRUE'
this.and.push('"video"."remote" IS ' + isRemote)
} }
private whereHost (host: string) { private whereHost (host: string) {
@ -326,7 +344,7 @@ export class VideosIdListQueryBuilder extends AbstractVideosQueryBuilder {
this.replacements.videoChannelId = channelId this.replacements.videoChannelId = channelId
} }
private whereFollowerActorId (followerActorId: number, includeLocalVideos: boolean) { private whereFollowerActorId (options: { actorId: number, orLocalVideos: boolean }) {
let query = let query =
'(' + '(' +
' EXISTS (' + // Videos shared by actors we follow ' EXISTS (' + // Videos shared by actors we follow
@ -342,14 +360,14 @@ export class VideosIdListQueryBuilder extends AbstractVideosQueryBuilder {
' AND "actorFollow"."state" = \'accepted\'' + ' AND "actorFollow"."state" = \'accepted\'' +
' )' ' )'
if (includeLocalVideos) { if (options.orLocalVideos) {
query += ' OR "video"."remote" IS FALSE' query += ' OR "video"."remote" IS FALSE'
} }
query += ')' query += ')'
this.and.push(query) this.and.push(query)
this.replacements.followerActorId = followerActorId this.replacements.followerActorId = options.actorId
} }
private whereFileExists () { private whereFileExists () {

View File

@ -1,3 +1,4 @@
import { VideoInclude } from '@shared/models'
import { Sequelize } from 'sequelize' import { Sequelize } from 'sequelize'
import { AbstractVideosModelQueryBuilder } from './shared/abstract-videos-model-query-builder' import { AbstractVideosModelQueryBuilder } from './shared/abstract-videos-model-query-builder'
import { VideoModelBuilder } from './shared/video-model-builder' import { VideoModelBuilder } from './shared/video-model-builder'
@ -28,7 +29,7 @@ export class VideosModelListQueryBuilder extends AbstractVideosModelQueryBuilder
this.buildListQueryFromIdsQuery(options) this.buildListQueryFromIdsQuery(options)
return this.runQuery() return this.runQuery()
.then(rows => this.videoModelBuilder.buildVideosFromRows(rows)) .then(rows => this.videoModelBuilder.buildVideosFromRows({ rows, include: options.include }))
} }
private buildInnerQuery (options: BuildVideosListQueryOptions) { private buildInnerQuery (options: BuildVideosListQueryOptions) {
@ -64,6 +65,14 @@ export class VideosModelListQueryBuilder extends AbstractVideosModelQueryBuilder
this.includePlaylist(options.videoPlaylistId) 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() const select = this.buildSelect()
this.query = `${select} FROM (${this.innerQuery}) AS "tmp" ${this.joins} ${this.innerSort}` 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 { getServerActor } from '@server/models/application/application'
import { ModelCache } from '@server/models/model-cache' import { ModelCache } from '@server/models/model-cache'
import { AttributesOnly, buildVideoEmbedPath, buildVideoWatchPath, pick } from '@shared/core-utils' import { AttributesOnly, buildVideoEmbedPath, buildVideoWatchPath, pick } from '@shared/core-utils'
import { VideoInclude } from '@shared/models'
import { VideoFile } from '@shared/models/videos/video-file.model' import { VideoFile } from '@shared/models/videos/video-file.model'
import { ResultList, UserRight, VideoPrivacy, VideoState } from '../../../shared' import { ResultList, UserRight, VideoPrivacy, VideoState } from '../../../shared'
import { VideoObject } from '../../../shared/models/activitypub/objects' import { VideoObject } from '../../../shared/models/activitypub/objects'
import { Video, VideoDetails, VideoRateType, VideoStorage } from '../../../shared/models/videos' import { Video, VideoDetails, VideoRateType, VideoStorage } from '../../../shared/models/videos'
import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' 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 { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
import { peertubeTruncate } from '../../helpers/core-utils' import { peertubeTruncate } from '../../helpers/core-utils'
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
@ -106,7 +106,7 @@ import {
} from './formatter/video-format-utils' } from './formatter/video-format-utils'
import { ScheduleVideoUpdateModel } from './schedule-video-update' import { ScheduleVideoUpdateModel } from './schedule-video-update'
import { VideosModelGetQueryBuilder } from './sql/video-model-get-query-builder' 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 { VideosModelListQueryBuilder } from './sql/videos-model-list-query-builder'
import { TagModel } from './tag' import { TagModel } from './tag'
import { ThumbnailModel } from './thumbnail' import { ThumbnailModel } from './thumbnail'
@ -145,35 +145,6 @@ export type ForAPIOptions = {
withAccountBlockerIds?: number[] 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(() => ({ @Scopes(() => ({
[ScopeNames.WITH_IMMUTABLE_ATTRIBUTES]: { [ScopeNames.WITH_IMMUTABLE_ATTRIBUTES]: {
attributes: [ 'id', 'url', 'uuid', 'remote' ] attributes: [ 'id', 'url', 'uuid', 'remote' ]
@ -1054,10 +1025,10 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
sort: string sort: string
nsfw: boolean nsfw: boolean
filter?: VideoFilter
isLive?: boolean isLive?: boolean
isLocal?: boolean
include?: VideoInclude
includeLocalVideos: boolean
withFiles: boolean withFiles: boolean
categoryOneOf?: number[] categoryOneOf?: number[]
@ -1069,7 +1040,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
accountId?: number accountId?: number
videoChannelId?: number videoChannelId?: number
followerActorId?: number displayOnlyForFollower: DisplayOnlyForFollowerOptions | null
videoPlaylistId?: number videoPlaylistId?: number
@ -1082,7 +1053,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
search?: string 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') 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() const serverActor = await getServerActor()
// followerActorId === null has a meaning, so just check undefined
const followerActorId = options.followerActorId !== undefined
? options.followerActorId
: serverActor.id
const queryOptions = { const queryOptions = {
...pick(options, [ ...pick(options, [
'start', 'start',
@ -1113,19 +1079,19 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
'languageOneOf', 'languageOneOf',
'tagsOneOf', 'tagsOneOf',
'tagsAllOf', 'tagsAllOf',
'filter', 'isLocal',
'include',
'displayOnlyForFollower',
'withFiles', 'withFiles',
'accountId', 'accountId',
'videoChannelId', 'videoChannelId',
'videoPlaylistId', 'videoPlaylistId',
'includeLocalVideos',
'user', 'user',
'historyOfUser', 'historyOfUser',
'search' 'search'
]), ]),
followerActorId, serverAccountIdForBlock: serverActor.Account.id,
serverAccountId: serverActor.Account.id,
trendingDays, trendingDays,
trendingAlgorithm trendingAlgorithm
} }
@ -1137,7 +1103,6 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
start: number start: number
count: number count: number
sort: string sort: string
includeLocalVideos: boolean
search?: string search?: string
host?: string host?: string
startDate?: string // ISO 8601 startDate?: string // ISO 8601
@ -1146,6 +1111,8 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
originallyPublishedEndDate?: string originallyPublishedEndDate?: string
nsfw?: boolean nsfw?: boolean
isLive?: boolean isLive?: boolean
isLocal?: boolean
include?: VideoInclude
categoryOneOf?: number[] categoryOneOf?: number[]
licenceOneOf?: number[] licenceOneOf?: number[]
languageOneOf?: string[] languageOneOf?: string[]
@ -1154,14 +1121,14 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
durationMin?: number // seconds durationMin?: number // seconds
durationMax?: number // seconds durationMax?: number // seconds
user?: MUserAccountId user?: MUserAccountId
filter?: VideoFilter
uuids?: string[] uuids?: string[]
displayOnlyForFollower: DisplayOnlyForFollowerOptions | null
}) { }) {
const serverActor = await getServerActor() const serverActor = await getServerActor()
const queryOptions = { const queryOptions = {
...pick(options, [ ...pick(options, [
'includeLocalVideos', 'include',
'nsfw', 'nsfw',
'isLive', 'isLive',
'categoryOneOf', 'categoryOneOf',
@ -1170,7 +1137,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
'tagsOneOf', 'tagsOneOf',
'tagsAllOf', 'tagsAllOf',
'user', 'user',
'filter', 'isLocal',
'host', 'host',
'start', 'start',
'count', 'count',
@ -1182,11 +1149,10 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
'durationMin', 'durationMin',
'durationMax', 'durationMax',
'uuids', 'uuids',
'search' 'search',
'displayOnlyForFollower'
]), ]),
serverAccountIdForBlock: serverActor.Account.id
followerActorId: serverActor.id,
serverAccountId: serverActor.Account.id
} }
return VideoModel.getAvailableForApi(queryOptions) return VideoModel.getAvailableForApi(queryOptions)
@ -1369,12 +1335,17 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
// Sequelize could return null... // Sequelize could return null...
if (!totalLocalVideoViews) totalLocalVideoViews = 0 if (!totalLocalVideoViews) totalLocalVideoViews = 0
const serverActor = await getServerActor()
const { total: totalVideos } = await VideoModel.listForApi({ const { total: totalVideos } = await VideoModel.listForApi({
start: 0, start: 0,
count: 0, count: 0,
sort: '-publishedAt', sort: '-publishedAt',
nsfw: buildNSFWFilter(), nsfw: buildNSFWFilter(),
includeLocalVideos: true, displayOnlyForFollower: {
actorId: serverActor.id,
orLocalVideos: true
},
withFiles: false 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 // threshold corresponds to how many video the field should have to be returned
static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) { static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) {
const serverActor = await getServerActor() const serverActor = await getServerActor()
const followerActorId = serverActor.id
const queryOptions: BuildVideosListQueryOptions = { const queryOptions: BuildVideosListQueryOptions = {
attributes: [ `"${field}"` ], attributes: [ `"${field}"` ],
@ -1464,9 +1434,11 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
start: 0, start: 0,
sort: 'random', sort: 'random',
count, count,
serverAccountId: serverActor.Account.id, serverAccountIdForBlock: serverActor.Account.id,
followerActorId, displayOnlyForFollower: {
includeLocalVideos: true actorId: serverActor.id,
orLocalVideos: true
}
} }
const queryBuilder = new VideosIdListQueryBuilder(VideoModel.sequelize) const queryBuilder = new VideosIdListQueryBuilder(VideoModel.sequelize)

View File

@ -27,6 +27,6 @@ import './video-comments'
import './video-imports' import './video-imports'
import './video-playlists' import './video-playlists'
import './videos' import './videos'
import './videos-filter' import './videos-common-filters'
import './videos-history' import './videos-history'
import './videos-overviews' 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-privacy'
import './video-schedule-update' import './video-schedule-update'
import './video-transcoder' import './video-transcoder'
import './videos-filter' import './videos-common-filters'
import './videos-history' import './videos-history'
import './videos-overview' import './videos-overview'
import './videos-views-cleaner' import './videos-views-cleaner'

View File

@ -349,7 +349,7 @@ describe('Test multiple servers', function () {
describe('It should list local videos', function () { describe('It should list local videos', function () {
it('Should list only local videos on server 1', async 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(total).to.equal(1)
expect(data).to.be.an('array') 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 () { 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(total).to.equal(1)
expect(data).to.be.an('array') 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 () { 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(total).to.equal(2)
expect(data).to.be.an('array') 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 }) 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 () { it('Should have the video updated', async function () {
this.timeout(60000) 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 = export type MAccountSummaryBlocks =
MAccountSummary & MAccountSummary &
Use<'BlockedAccounts', MAccountBlocklistId[]> Use<'BlockedByAccounts', MAccountBlocklistId[]>
export type MAccountAPI = export type MAccountAPI =
MAccount & MAccount &

View File

@ -18,8 +18,7 @@ import {
VideoDetails, VideoDetails,
VideoFileMetadata, VideoFileMetadata,
VideoPrivacy, VideoPrivacy,
VideosCommonQuery, VideosCommonQuery
VideosWithSearchCommonQuery
} from '@shared/models' } from '@shared/models'
import { buildAbsoluteFixturePath, wait } from '../miscs' import { buildAbsoluteFixturePath, wait } from '../miscs'
import { unwrapBody } from '../requests' import { unwrapBody } from '../requests'
@ -246,7 +245,7 @@ export class VideosCommand extends AbstractCommand {
}) })
} }
listByAccount (options: OverrideCommandOptions & VideosWithSearchCommonQuery & { listByAccount (options: OverrideCommandOptions & VideosCommonQuery & {
handle: string handle: string
}) { }) {
const { handle, search } = options const { handle, search } = options
@ -262,7 +261,7 @@ export class VideosCommand extends AbstractCommand {
}) })
} }
listByChannel (options: OverrideCommandOptions & VideosWithSearchCommonQuery & { listByChannel (options: OverrideCommandOptions & VideosCommonQuery & {
handle: string handle: string
}) { }) {
const { handle } = options const { handle } = options
@ -605,7 +604,8 @@ export class VideosCommand extends AbstractCommand {
'languageOneOf', 'languageOneOf',
'tagsOneOf', 'tagsOneOf',
'tagsAllOf', 'tagsAllOf',
'filter', 'isLocal',
'include',
'skipCount' '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' import { BooleanBothQuery } from './boolean-both-query.model'
// These query parameters can be used with any endpoint that list videos // These query parameters can be used with any endpoint that list videos
@ -11,6 +11,12 @@ export interface VideosCommonQuery {
isLive?: boolean isLive?: boolean
// FIXME: deprecated in 4.0 in favour of isLocal and include, to remove
filter?: never
isLocal?: boolean
include?: VideoInclude
categoryOneOf?: number[] categoryOneOf?: number[]
licenceOneOf?: number[] licenceOneOf?: number[]
@ -20,17 +26,16 @@ export interface VideosCommonQuery {
tagsOneOf?: string[] tagsOneOf?: string[]
tagsAllOf?: string[] tagsAllOf?: string[]
filter?: VideoFilter
skipCount?: boolean skipCount?: boolean
search?: string
} }
export interface VideosCommonQueryAfterSanitize extends VideosCommonQuery { export interface VideosCommonQueryAfterSanitize extends VideosCommonQuery {
start: number start: number
count: number count: number
sort: string sort: string
}
export interface VideosWithSearchCommonQuery extends VideosCommonQuery { // FIXME: deprecated in 4.0, to remove
search?: string filter?: never
} }

View File

@ -23,4 +23,7 @@ export interface VideosSearchQueryAfterSanitize extends VideosSearchQuery {
start: number start: number
count: number count: number
sort: string 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-file.model'
export * from './video-privacy.enum' 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-rate.type'
export * from './video-resolution.enum' 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 dislikes: number
nsfw: boolean nsfw: boolean
waitTranscoding?: boolean
state?: VideoConstant<VideoState>
scheduledUpdate?: VideoScheduleUpdate
blacklisted?: boolean
blacklistedReason?: string
account: AccountSummary account: AccountSummary
channel: VideoChannelSummary channel: VideoChannelSummary
@ -58,6 +51,17 @@ export interface Video {
} }
pluginData?: any 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 { export interface VideoDetails extends Video {
@ -70,7 +74,7 @@ export interface VideoDetails extends Video {
commentsEnabled: boolean commentsEnabled: boolean
downloadEnabled: boolean downloadEnabled: boolean
// Not optional in details (unlike in Video) // Not optional in details (unlike in parent Video)
waitTranscoding: boolean waitTranscoding: boolean
state: VideoConstant<VideoState> state: VideoConstant<VideoState>

View File

@ -367,7 +367,8 @@ paths:
- $ref: '#/components/parameters/licenceOneOf' - $ref: '#/components/parameters/licenceOneOf'
- $ref: '#/components/parameters/languageOneOf' - $ref: '#/components/parameters/languageOneOf'
- $ref: '#/components/parameters/nsfw' - $ref: '#/components/parameters/nsfw'
- $ref: '#/components/parameters/filter' - $ref: '#/components/parameters/isLocal'
- $ref: '#/components/parameters/include'
- $ref: '#/components/parameters/skipCount' - $ref: '#/components/parameters/skipCount'
- $ref: '#/components/parameters/start' - $ref: '#/components/parameters/start'
- $ref: '#/components/parameters/count' - $ref: '#/components/parameters/count'
@ -1300,7 +1301,8 @@ paths:
- $ref: '#/components/parameters/licenceOneOf' - $ref: '#/components/parameters/licenceOneOf'
- $ref: '#/components/parameters/languageOneOf' - $ref: '#/components/parameters/languageOneOf'
- $ref: '#/components/parameters/nsfw' - $ref: '#/components/parameters/nsfw'
- $ref: '#/components/parameters/filter' - $ref: '#/components/parameters/isLocal'
- $ref: '#/components/parameters/include'
- $ref: '#/components/parameters/skipCount' - $ref: '#/components/parameters/skipCount'
- $ref: '#/components/parameters/start' - $ref: '#/components/parameters/start'
- $ref: '#/components/parameters/count' - $ref: '#/components/parameters/count'
@ -1620,7 +1622,8 @@ paths:
- $ref: '#/components/parameters/licenceOneOf' - $ref: '#/components/parameters/licenceOneOf'
- $ref: '#/components/parameters/languageOneOf' - $ref: '#/components/parameters/languageOneOf'
- $ref: '#/components/parameters/nsfw' - $ref: '#/components/parameters/nsfw'
- $ref: '#/components/parameters/filter' - $ref: '#/components/parameters/isLocal'
- $ref: '#/components/parameters/include'
- $ref: '#/components/parameters/skipCount' - $ref: '#/components/parameters/skipCount'
- $ref: '#/components/parameters/start' - $ref: '#/components/parameters/start'
- $ref: '#/components/parameters/count' - $ref: '#/components/parameters/count'
@ -2856,7 +2859,8 @@ paths:
- $ref: '#/components/parameters/licenceOneOf' - $ref: '#/components/parameters/licenceOneOf'
- $ref: '#/components/parameters/languageOneOf' - $ref: '#/components/parameters/languageOneOf'
- $ref: '#/components/parameters/nsfw' - $ref: '#/components/parameters/nsfw'
- $ref: '#/components/parameters/filter' - $ref: '#/components/parameters/isLocal'
- $ref: '#/components/parameters/include'
- $ref: '#/components/parameters/skipCount' - $ref: '#/components/parameters/skipCount'
- $ref: '#/components/parameters/start' - $ref: '#/components/parameters/start'
- $ref: '#/components/parameters/count' - $ref: '#/components/parameters/count'
@ -3576,7 +3580,8 @@ paths:
- $ref: '#/components/parameters/licenceOneOf' - $ref: '#/components/parameters/licenceOneOf'
- $ref: '#/components/parameters/languageOneOf' - $ref: '#/components/parameters/languageOneOf'
- $ref: '#/components/parameters/nsfw' - $ref: '#/components/parameters/nsfw'
- $ref: '#/components/parameters/filter' - $ref: '#/components/parameters/isLocal'
- $ref: '#/components/parameters/include'
- $ref: '#/components/parameters/skipCount' - $ref: '#/components/parameters/skipCount'
- $ref: '#/components/parameters/start' - $ref: '#/components/parameters/start'
- $ref: '#/components/parameters/count' - $ref: '#/components/parameters/count'
@ -4078,7 +4083,8 @@ paths:
type: string type: string
- $ref: '#/components/parameters/sort' - $ref: '#/components/parameters/sort'
- $ref: '#/components/parameters/nsfw' - $ref: '#/components/parameters/nsfw'
- $ref: '#/components/parameters/filter' - $ref: '#/components/parameters/isLocal'
- $ref: '#/components/parameters/include'
responses: responses:
'204': '204':
description: successful operation description: successful operation
@ -4159,7 +4165,8 @@ paths:
required: true required: true
- $ref: '#/components/parameters/sort' - $ref: '#/components/parameters/sort'
- $ref: '#/components/parameters/nsfw' - $ref: '#/components/parameters/nsfw'
- $ref: '#/components/parameters/filter' - $ref: '#/components/parameters/isLocal'
- $ref: '#/components/parameters/include'
responses: responses:
'204': '204':
description: successful operation description: successful operation
@ -4792,20 +4799,37 @@ components:
enum: enum:
- 'true' - 'true'
- 'false' - 'false'
filter: isLocal:
name: filter name: isLocal
in: query in: query
required: false 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: schema:
type: string type: boolean
description: 'Display only local or remote videos'
include:
name: include
in: query
required: false
schema:
type: integer
enum: enum:
- local - 0
- all-local - 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: subscriptionsUris:
name: uris name: uris
in: query in: query
@ -6995,7 +7019,7 @@ components:
enum: enum:
- 0 - 0
- 1 - 1
- 3 - 2
Notification: Notification:
properties: properties:
id: id: