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