aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/controllers/api/accounts.ts18
-rw-r--r--server/controllers/api/overviews.ts8
-rw-r--r--server/controllers/api/search/search-videos.ts12
-rw-r--r--server/controllers/api/users/my-subscriptions.ts9
-rw-r--r--server/controllers/api/video-channel.ts17
-rw-r--r--server/controllers/api/videos/index.ts10
-rw-r--r--server/controllers/bots.ts10
-rw-r--r--server/controllers/feeds.ts21
-rw-r--r--server/helpers/custom-validators/videos.ts6
-rw-r--r--server/helpers/query.ts7
-rw-r--r--server/middlewares/validators/videos/videos.ts31
-rw-r--r--server/models/account/account.ts6
-rw-r--r--server/models/server/server.ts4
-rw-r--r--server/models/user/user-video-history.ts10
-rw-r--r--server/models/video/formatter/video-format-utils.ts68
-rw-r--r--server/models/video/sql/shared/abstract-videos-model-query-builder.ts28
-rw-r--r--server/models/video/sql/shared/video-model-builder.ts72
-rw-r--r--server/models/video/sql/shared/video-tables.ts4
-rw-r--r--server/models/video/sql/video-model-get-query-builder.ts6
-rw-r--r--server/models/video/sql/videos-id-list-query-builder.ts60
-rw-r--r--server/models/video/sql/videos-model-list-query-builder.ts11
-rw-r--r--server/models/video/video.ts86
-rw-r--r--server/tests/api/check-params/index.ts2
-rw-r--r--server/tests/api/check-params/videos-common-filters.ts203
-rw-r--r--server/tests/api/check-params/videos-filter.ts114
-rw-r--r--server/tests/api/videos/index.ts2
-rw-r--r--server/tests/api/videos/multiple-servers.ts6
-rw-r--r--server/tests/api/videos/single-server.ts13
-rw-r--r--server/tests/api/videos/videos-common-filters.ts403
-rw-r--r--server/tests/api/videos/videos-filter.ts122
-rw-r--r--server/types/models/account/account.ts2
31 files changed, 965 insertions, 406 deletions
diff --git a/server/controllers/api/accounts.ts b/server/controllers/api/accounts.ts
index 8eb880d59..44edffe38 100644
--- a/server/controllers/api/accounts.ts
+++ b/server/controllers/api/accounts.ts
@@ -2,6 +2,7 @@ import express from 'express'
2import { pickCommonVideoQuery } from '@server/helpers/query' 2import { pickCommonVideoQuery } from '@server/helpers/query'
3import { ActorFollowModel } from '@server/models/actor/actor-follow' 3import { ActorFollowModel } from '@server/models/actor/actor-follow'
4import { getServerActor } from '@server/models/application/application' 4import { getServerActor } from '@server/models/application/application'
5import { guessAdditionalAttributesFromQuery } from '@server/models/video/formatter/video-format-utils'
5import { buildNSFWFilter, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' 6import { buildNSFWFilter, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils'
6import { getFormattedObjects } from '../../helpers/utils' 7import { getFormattedObjects } from '../../helpers/utils'
7import { JobQueue } from '../../lib/job-queue' 8import { JobQueue } from '../../lib/job-queue'
@@ -169,17 +170,24 @@ async function listAccountPlaylists (req: express.Request, res: express.Response
169} 170}
170 171
171async function listAccountVideos (req: express.Request, res: express.Response) { 172async function listAccountVideos (req: express.Request, res: express.Response) {
173 const serverActor = await getServerActor()
174
172 const account = res.locals.account 175 const account = res.locals.account
173 const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined 176
177 const displayOnlyForFollower = isUserAbleToSearchRemoteURI(res)
178 ? null
179 : {
180 actorId: serverActor.id,
181 orLocalVideos: true
182 }
183
174 const countVideos = getCountVideos(req) 184 const countVideos = getCountVideos(req)
175 const query = pickCommonVideoQuery(req.query) 185 const query = pickCommonVideoQuery(req.query)
176 186
177 const apiOptions = await Hooks.wrapObject({ 187 const apiOptions = await Hooks.wrapObject({
178 ...query, 188 ...query,
179 189
180 followerActorId, 190 displayOnlyForFollower,
181 search: req.query.search,
182 includeLocalVideos: true,
183 nsfw: buildNSFWFilter(res, query.nsfw), 191 nsfw: buildNSFWFilter(res, query.nsfw),
184 withFiles: false, 192 withFiles: false,
185 accountId: account.id, 193 accountId: account.id,
@@ -193,7 +201,7 @@ async function listAccountVideos (req: express.Request, res: express.Response) {
193 'filter:api.accounts.videos.list.result' 201 'filter:api.accounts.videos.list.result'
194 ) 202 )
195 203
196 return res.json(getFormattedObjects(resultList.data, resultList.total)) 204 return res.json(getFormattedObjects(resultList.data, resultList.total, guessAdditionalAttributesFromQuery(query)))
197} 205}
198 206
199async function listAccountRatings (req: express.Request, res: express.Response) { 207async function listAccountRatings (req: express.Request, res: express.Response) {
diff --git a/server/controllers/api/overviews.ts b/server/controllers/api/overviews.ts
index 5b16232e2..68626a508 100644
--- a/server/controllers/api/overviews.ts
+++ b/server/controllers/api/overviews.ts
@@ -8,6 +8,7 @@ import { buildNSFWFilter } from '../../helpers/express-utils'
8import { MEMOIZE_TTL, OVERVIEWS } from '../../initializers/constants' 8import { MEMOIZE_TTL, OVERVIEWS } from '../../initializers/constants'
9import { asyncMiddleware, optionalAuthenticate, videosOverviewValidator } from '../../middlewares' 9import { asyncMiddleware, optionalAuthenticate, videosOverviewValidator } from '../../middlewares'
10import { TagModel } from '../../models/video/tag' 10import { TagModel } from '../../models/video/tag'
11import { getServerActor } from '@server/models/application/application'
11 12
12const overviewsRouter = express.Router() 13const overviewsRouter = express.Router()
13 14
@@ -109,11 +110,16 @@ async function getVideos (
109 res: express.Response, 110 res: express.Response,
110 where: { videoChannelId?: number, tagsOneOf?: string[], categoryOneOf?: number[] } 111 where: { videoChannelId?: number, tagsOneOf?: string[], categoryOneOf?: number[] }
111) { 112) {
113 const serverActor = await getServerActor()
114
112 const query = await Hooks.wrapObject({ 115 const query = await Hooks.wrapObject({
113 start: 0, 116 start: 0,
114 count: 12, 117 count: 12,
115 sort: '-createdAt', 118 sort: '-createdAt',
116 includeLocalVideos: true, 119 displayOnlyForFollower: {
120 actorId: serverActor.id,
121 orLocalVideos: true
122 },
117 nsfw: buildNSFWFilter(res), 123 nsfw: buildNSFWFilter(res),
118 user: res.locals.oauth ? res.locals.oauth.token.User : undefined, 124 user: res.locals.oauth ? res.locals.oauth.token.User : undefined,
119 withFiles: false, 125 withFiles: false,
diff --git a/server/controllers/api/search/search-videos.ts b/server/controllers/api/search/search-videos.ts
index 90946cb74..6db70acdf 100644
--- a/server/controllers/api/search/search-videos.ts
+++ b/server/controllers/api/search/search-videos.ts
@@ -7,6 +7,8 @@ import { WEBSERVER } from '@server/initializers/constants'
7import { getOrCreateAPVideo } from '@server/lib/activitypub/videos' 7import { getOrCreateAPVideo } from '@server/lib/activitypub/videos'
8import { Hooks } from '@server/lib/plugins/hooks' 8import { Hooks } from '@server/lib/plugins/hooks'
9import { buildMutedForSearchIndex, isSearchIndexSearch, isURISearch } from '@server/lib/search' 9import { buildMutedForSearchIndex, isSearchIndexSearch, isURISearch } from '@server/lib/search'
10import { getServerActor } from '@server/models/application/application'
11import { guessAdditionalAttributesFromQuery } from '@server/models/video/formatter/video-format-utils'
10import { HttpStatusCode, ResultList, Video } from '@shared/models' 12import { HttpStatusCode, ResultList, Video } from '@shared/models'
11import { VideosSearchQueryAfterSanitize } from '../../../../shared/models/search' 13import { VideosSearchQueryAfterSanitize } from '../../../../shared/models/search'
12import { buildNSFWFilter, isUserAbleToSearchRemoteURI } from '../../../helpers/express-utils' 14import { buildNSFWFilter, isUserAbleToSearchRemoteURI } from '../../../helpers/express-utils'
@@ -100,11 +102,15 @@ async function searchVideosIndex (query: VideosSearchQueryAfterSanitize, res: ex
100} 102}
101 103
102async function searchVideosDB (query: VideosSearchQueryAfterSanitize, res: express.Response) { 104async function searchVideosDB (query: VideosSearchQueryAfterSanitize, res: express.Response) {
105 const serverActor = await getServerActor()
106
103 const apiOptions = await Hooks.wrapObject({ 107 const apiOptions = await Hooks.wrapObject({
104 ...query, 108 ...query,
105 109
106 includeLocalVideos: true, 110 displayOnlyForFollower: {
107 filter: query.filter, 111 actorId: serverActor.id,
112 orLocalVideos: true
113 },
108 114
109 nsfw: buildNSFWFilter(res, query.nsfw), 115 nsfw: buildNSFWFilter(res, query.nsfw),
110 user: res.locals.oauth 116 user: res.locals.oauth
@@ -118,7 +124,7 @@ async function searchVideosDB (query: VideosSearchQueryAfterSanitize, res: expre
118 'filter:api.search.videos.local.list.result' 124 'filter:api.search.videos.local.list.result'
119 ) 125 )
120 126
121 return res.json(getFormattedObjects(resultList.data, resultList.total)) 127 return res.json(getFormattedObjects(resultList.data, resultList.total, guessAdditionalAttributesFromQuery(query)))
122} 128}
123 129
124async function searchVideoURI (url: string, res: express.Response) { 130async function searchVideoURI (url: string, res: express.Response) {
diff --git a/server/controllers/api/users/my-subscriptions.ts b/server/controllers/api/users/my-subscriptions.ts
index b2b441673..d96378180 100644
--- a/server/controllers/api/users/my-subscriptions.ts
+++ b/server/controllers/api/users/my-subscriptions.ts
@@ -2,6 +2,7 @@ import 'multer'
2import express from 'express' 2import express from 'express'
3import { pickCommonVideoQuery } from '@server/helpers/query' 3import { pickCommonVideoQuery } from '@server/helpers/query'
4import { sendUndoFollow } from '@server/lib/activitypub/send' 4import { sendUndoFollow } from '@server/lib/activitypub/send'
5import { guessAdditionalAttributesFromQuery } from '@server/models/video/formatter/video-format-utils'
5import { VideoChannelModel } from '@server/models/video/video-channel' 6import { VideoChannelModel } from '@server/models/video/video-channel'
6import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' 7import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
7import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils' 8import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils'
@@ -175,13 +176,15 @@ async function getUserSubscriptionVideos (req: express.Request, res: express.Res
175 const resultList = await VideoModel.listForApi({ 176 const resultList = await VideoModel.listForApi({
176 ...query, 177 ...query,
177 178
178 includeLocalVideos: false, 179 displayOnlyForFollower: {
180 actorId: user.Account.Actor.id,
181 orLocalVideos: false
182 },
179 nsfw: buildNSFWFilter(res, query.nsfw), 183 nsfw: buildNSFWFilter(res, query.nsfw),
180 withFiles: false, 184 withFiles: false,
181 followerActorId: user.Account.Actor.id,
182 user, 185 user,
183 countVideos 186 countVideos
184 }) 187 })
185 188
186 return res.json(getFormattedObjects(resultList.data, resultList.total)) 189 return res.json(getFormattedObjects(resultList.data, resultList.total, guessAdditionalAttributesFromQuery(query)))
187} 190}
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts
index 7bf7a68c9..f9c1a405d 100644
--- a/server/controllers/api/video-channel.ts
+++ b/server/controllers/api/video-channel.ts
@@ -3,6 +3,7 @@ import { pickCommonVideoQuery } from '@server/helpers/query'
3import { Hooks } from '@server/lib/plugins/hooks' 3import { Hooks } from '@server/lib/plugins/hooks'
4import { ActorFollowModel } from '@server/models/actor/actor-follow' 4import { ActorFollowModel } from '@server/models/actor/actor-follow'
5import { getServerActor } from '@server/models/application/application' 5import { getServerActor } from '@server/models/application/application'
6import { guessAdditionalAttributesFromQuery } from '@server/models/video/formatter/video-format-utils'
6import { MChannelBannerAccountDefault } from '@server/types/models' 7import { MChannelBannerAccountDefault } from '@server/types/models'
7import { ActorImageType, VideoChannelCreate, VideoChannelUpdate } from '../../../shared' 8import { ActorImageType, VideoChannelCreate, VideoChannelUpdate } from '../../../shared'
8import { HttpStatusCode } from '../../../shared/models/http/http-error-codes' 9import { HttpStatusCode } from '../../../shared/models/http/http-error-codes'
@@ -327,16 +328,24 @@ async function listVideoChannelPlaylists (req: express.Request, res: express.Res
327} 328}
328 329
329async function listVideoChannelVideos (req: express.Request, res: express.Response) { 330async function listVideoChannelVideos (req: express.Request, res: express.Response) {
331 const serverActor = await getServerActor()
332
330 const videoChannelInstance = res.locals.videoChannel 333 const videoChannelInstance = res.locals.videoChannel
331 const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined 334
335 const displayOnlyForFollower = isUserAbleToSearchRemoteURI(res)
336 ? null
337 : {
338 actorId: serverActor.id,
339 orLocalVideos: true
340 }
341
332 const countVideos = getCountVideos(req) 342 const countVideos = getCountVideos(req)
333 const query = pickCommonVideoQuery(req.query) 343 const query = pickCommonVideoQuery(req.query)
334 344
335 const apiOptions = await Hooks.wrapObject({ 345 const apiOptions = await Hooks.wrapObject({
336 ...query, 346 ...query,
337 347
338 followerActorId, 348 displayOnlyForFollower,
339 includeLocalVideos: true,
340 nsfw: buildNSFWFilter(res, query.nsfw), 349 nsfw: buildNSFWFilter(res, query.nsfw),
341 withFiles: false, 350 withFiles: false,
342 videoChannelId: videoChannelInstance.id, 351 videoChannelId: videoChannelInstance.id,
@@ -350,7 +359,7 @@ async function listVideoChannelVideos (req: express.Request, res: express.Respon
350 'filter:api.video-channels.videos.list.result' 359 'filter:api.video-channels.videos.list.result'
351 ) 360 )
352 361
353 return res.json(getFormattedObjects(resultList.data, resultList.total)) 362 return res.json(getFormattedObjects(resultList.data, resultList.total, guessAdditionalAttributesFromQuery(query)))
354} 363}
355 364
356async function listVideoChannelFollowers (req: express.Request, res: express.Response) { 365async function listVideoChannelFollowers (req: express.Request, res: express.Response) {
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index c0c77f3f7..821ed7ff3 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -5,6 +5,7 @@ import { doJSONRequest } from '@server/helpers/requests'
5import { LiveManager } from '@server/lib/live' 5import { LiveManager } from '@server/lib/live'
6import { openapiOperationDoc } from '@server/middlewares/doc' 6import { openapiOperationDoc } from '@server/middlewares/doc'
7import { getServerActor } from '@server/models/application/application' 7import { getServerActor } from '@server/models/application/application'
8import { guessAdditionalAttributesFromQuery } from '@server/models/video/formatter/video-format-utils'
8import { MVideoAccountLight } from '@server/types/models' 9import { MVideoAccountLight } from '@server/types/models'
9import { HttpStatusCode } from '../../../../shared/models' 10import { HttpStatusCode } from '../../../../shared/models'
10import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' 11import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
@@ -211,13 +212,18 @@ async function getVideoFileMetadata (req: express.Request, res: express.Response
211} 212}
212 213
213async function listVideos (req: express.Request, res: express.Response) { 214async function listVideos (req: express.Request, res: express.Response) {
215 const serverActor = await getServerActor()
216
214 const query = pickCommonVideoQuery(req.query) 217 const query = pickCommonVideoQuery(req.query)
215 const countVideos = getCountVideos(req) 218 const countVideos = getCountVideos(req)
216 219
217 const apiOptions = await Hooks.wrapObject({ 220 const apiOptions = await Hooks.wrapObject({
218 ...query, 221 ...query,
219 222
220 includeLocalVideos: true, 223 displayOnlyForFollower: {
224 actorId: serverActor.id,
225 orLocalVideos: true
226 },
221 nsfw: buildNSFWFilter(res, query.nsfw), 227 nsfw: buildNSFWFilter(res, query.nsfw),
222 withFiles: false, 228 withFiles: false,
223 user: res.locals.oauth ? res.locals.oauth.token.User : undefined, 229 user: res.locals.oauth ? res.locals.oauth.token.User : undefined,
@@ -230,7 +236,7 @@ async function listVideos (req: express.Request, res: express.Response) {
230 'filter:api.videos.list.result' 236 'filter:api.videos.list.result'
231 ) 237 )
232 238
233 return res.json(getFormattedObjects(resultList.data, resultList.total)) 239 return res.json(getFormattedObjects(resultList.data, resultList.total, guessAdditionalAttributesFromQuery(query)))
234} 240}
235 241
236async function removeVideo (_req: express.Request, res: express.Response) { 242async function removeVideo (_req: express.Request, res: express.Response) {
diff --git a/server/controllers/bots.ts b/server/controllers/bots.ts
index 63db345bf..9f03de7e8 100644
--- a/server/controllers/bots.ts
+++ b/server/controllers/bots.ts
@@ -1,3 +1,4 @@
1import { getServerActor } from '@server/models/application/application'
1import express from 'express' 2import express from 'express'
2import { truncate } from 'lodash' 3import { truncate } from 'lodash'
3import { SitemapStream, streamToPromise } from 'sitemap' 4import { SitemapStream, streamToPromise } from 'sitemap'
@@ -63,13 +64,18 @@ async function getSitemapAccountUrls () {
63} 64}
64 65
65async function getSitemapLocalVideoUrls () { 66async function getSitemapLocalVideoUrls () {
67 const serverActor = await getServerActor()
68
66 const { data } = await VideoModel.listForApi({ 69 const { data } = await VideoModel.listForApi({
67 start: 0, 70 start: 0,
68 count: undefined, 71 count: undefined,
69 sort: 'createdAt', 72 sort: 'createdAt',
70 includeLocalVideos: true, 73 displayOnlyForFollower: {
74 actorId: serverActor.id,
75 orLocalVideos: true
76 },
77 isLocal: true,
71 nsfw: buildNSFWFilter(), 78 nsfw: buildNSFWFilter(),
72 filter: 'local',
73 withFiles: false, 79 withFiles: false,
74 countVideos: false 80 countVideos: false
75 }) 81 })
diff --git a/server/controllers/feeds.ts b/server/controllers/feeds.ts
index 5ac2e43a1..1f6aebac3 100644
--- a/server/controllers/feeds.ts
+++ b/server/controllers/feeds.ts
@@ -1,7 +1,7 @@
1import express from 'express' 1import express from 'express'
2import Feed from 'pfeed' 2import Feed from 'pfeed'
3import { getServerActor } from '@server/models/application/application'
3import { getCategoryLabel } from '@server/models/video/formatter/video-format-utils' 4import { getCategoryLabel } from '@server/models/video/formatter/video-format-utils'
4import { VideoFilter } from '../../shared/models/videos/video-query.type'
5import { buildNSFWFilter } from '../helpers/express-utils' 5import { buildNSFWFilter } from '../helpers/express-utils'
6import { CONFIG } from '../initializers/config' 6import { CONFIG } from '../initializers/config'
7import { FEEDS, PREVIEWS_SIZE, ROUTE_CACHE_LIFETIME, WEBSERVER } from '../initializers/constants' 7import { FEEDS, PREVIEWS_SIZE, ROUTE_CACHE_LIFETIME, WEBSERVER } from '../initializers/constants'
@@ -160,13 +160,18 @@ async function generateVideoFeed (req: express.Request, res: express.Response) {
160 videoChannelId: videoChannel ? videoChannel.id : null 160 videoChannelId: videoChannel ? videoChannel.id : null
161 } 161 }
162 162
163 const server = await getServerActor()
163 const { data } = await VideoModel.listForApi({ 164 const { data } = await VideoModel.listForApi({
164 start, 165 start,
165 count: FEEDS.COUNT, 166 count: FEEDS.COUNT,
166 sort: req.query.sort, 167 sort: req.query.sort,
167 includeLocalVideos: true, 168 displayOnlyForFollower: {
169 actorId: server.id,
170 orLocalVideos: true
171 },
168 nsfw, 172 nsfw,
169 filter: req.query.filter as VideoFilter, 173 isLocal: req.query.isLocal,
174 include: req.query.include,
170 withFiles: true, 175 withFiles: true,
171 countVideos: false, 176 countVideos: false,
172 ...options 177 ...options
@@ -196,14 +201,18 @@ async function generateVideoFeedForSubscriptions (req: express.Request, res: exp
196 start, 201 start,
197 count: FEEDS.COUNT, 202 count: FEEDS.COUNT,
198 sort: req.query.sort, 203 sort: req.query.sort,
199 includeLocalVideos: false,
200 nsfw, 204 nsfw,
201 filter: req.query.filter as VideoFilter, 205
206 isLocal: req.query.isLocal,
207 include: req.query.include,
202 208
203 withFiles: true, 209 withFiles: true,
204 countVideos: false, 210 countVideos: false,
205 211
206 followerActorId: res.locals.user.Account.Actor.id, 212 displayOnlyForFollower: {
213 actorId: res.locals.user.Account.Actor.id,
214 orLocalVideos: false
215 },
207 user: res.locals.user 216 user: res.locals.user
208 }) 217 })
209 218
diff --git a/server/helpers/custom-validators/videos.ts b/server/helpers/custom-validators/videos.ts
index c3604fbad..1d56ade6f 100644
--- a/server/helpers/custom-validators/videos.ts
+++ b/server/helpers/custom-validators/videos.ts
@@ -2,6 +2,7 @@ import { UploadFilesForCheck } from 'express'
2import { values } from 'lodash' 2import { values } from 'lodash'
3import magnetUtil from 'magnet-uri' 3import magnetUtil from 'magnet-uri'
4import validator from 'validator' 4import validator from 'validator'
5import { VideoInclude } from '@shared/models'
5import { VideoFilter, VideoPrivacy, VideoRateType } from '../../../shared' 6import { VideoFilter, VideoPrivacy, VideoRateType } from '../../../shared'
6import { 7import {
7 CONSTRAINTS_FIELDS, 8 CONSTRAINTS_FIELDS,
@@ -21,6 +22,10 @@ function isVideoFilterValid (filter: VideoFilter) {
21 return filter === 'local' || filter === 'all-local' || filter === 'all' 22 return filter === 'local' || filter === 'all-local' || filter === 'all'
22} 23}
23 24
25function isVideoIncludeValid (include: VideoInclude) {
26 return exists(include) && validator.isInt('' + include)
27}
28
24function isVideoCategoryValid (value: any) { 29function isVideoCategoryValid (value: any) {
25 return value === null || VIDEO_CATEGORIES[value] !== undefined 30 return value === null || VIDEO_CATEGORIES[value] !== undefined
26} 31}
@@ -146,6 +151,7 @@ export {
146 isVideoOriginallyPublishedAtValid, 151 isVideoOriginallyPublishedAtValid,
147 isVideoMagnetUriValid, 152 isVideoMagnetUriValid,
148 isVideoStateValid, 153 isVideoStateValid,
154 isVideoIncludeValid,
149 isVideoViewsValid, 155 isVideoViewsValid,
150 isVideoRatingTypeValid, 156 isVideoRatingTypeValid,
151 isVideoFileExtnameValid, 157 isVideoFileExtnameValid,
diff --git a/server/helpers/query.ts b/server/helpers/query.ts
index e711b15f2..79cf076d1 100644
--- a/server/helpers/query.ts
+++ b/server/helpers/query.ts
@@ -18,8 +18,10 @@ function pickCommonVideoQuery (query: VideosCommonQueryAfterSanitize) {
18 'languageOneOf', 18 'languageOneOf',
19 'tagsOneOf', 19 'tagsOneOf',
20 'tagsAllOf', 20 'tagsAllOf',
21 'filter', 21 'isLocal',
22 'skipCount' 22 'include',
23 'skipCount',
24 'search'
23 ]) 25 ])
24} 26}
25 27
@@ -29,7 +31,6 @@ function pickSearchVideoQuery (query: VideosSearchQueryAfterSanitize) {
29 31
30 ...pick(query, [ 32 ...pick(query, [
31 'searchTarget', 33 'searchTarget',
32 'search',
33 'host', 34 'host',
34 'startDate', 35 'startDate',
35 'endDate', 36 'endDate',
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts
index e486887a7..44233b653 100644
--- a/server/middlewares/validators/videos/videos.ts
+++ b/server/middlewares/validators/videos/videos.ts
@@ -7,6 +7,7 @@ import { isAbleToUploadVideo } from '@server/lib/user'
7import { getServerActor } from '@server/models/application/application' 7import { getServerActor } from '@server/models/application/application'
8import { ExpressPromiseHandler } from '@server/types/express' 8import { ExpressPromiseHandler } from '@server/types/express'
9import { MUserAccountId, MVideoFullLight } from '@server/types/models' 9import { MUserAccountId, MVideoFullLight } from '@server/types/models'
10import { VideoInclude } from '@shared/models'
10import { ServerErrorCode, UserRight, VideoPrivacy } from '../../../../shared' 11import { ServerErrorCode, UserRight, VideoPrivacy } from '../../../../shared'
11import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' 12import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
12import { 13import {
@@ -30,6 +31,7 @@ import {
30 isVideoFileSizeValid, 31 isVideoFileSizeValid,
31 isVideoFilterValid, 32 isVideoFilterValid,
32 isVideoImage, 33 isVideoImage,
34 isVideoIncludeValid,
33 isVideoLanguageValid, 35 isVideoLanguageValid,
34 isVideoLicenceValid, 36 isVideoLicenceValid,
35 isVideoNameValid, 37 isVideoNameValid,
@@ -487,6 +489,13 @@ const commonVideosFiltersValidator = [
487 query('filter') 489 query('filter')
488 .optional() 490 .optional()
489 .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'), 491 .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'),
492 query('include')
493 .optional()
494 .custom(isVideoIncludeValid).withMessage('Should have a valid include attribute'),
495 query('isLocal')
496 .optional()
497 .customSanitizer(toBooleanOrNull)
498 .custom(isBooleanValid).withMessage('Should have a valid local boolean'),
490 query('skipCount') 499 query('skipCount')
491 .optional() 500 .optional()
492 .customSanitizer(toBooleanOrNull) 501 .customSanitizer(toBooleanOrNull)
@@ -500,11 +509,23 @@ const commonVideosFiltersValidator = [
500 509
501 if (areValidationErrors(req, res)) return 510 if (areValidationErrors(req, res)) return
502 511
503 const user = res.locals.oauth ? res.locals.oauth.token.User : undefined 512 // FIXME: deprecated in 4.0, to remove
504 if ( 513 {
505 (req.query.filter === 'all-local' || req.query.filter === 'all') && 514 if (req.query.filter === 'all-local') {
506 (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false) 515 req.query.include = VideoInclude.NOT_PUBLISHED_STATE | VideoInclude.HIDDEN_PRIVACY
507 ) { 516 req.query.isLocal = true
517 } else if (req.query.filter === 'all') {
518 req.query.include = VideoInclude.NOT_PUBLISHED_STATE | VideoInclude.HIDDEN_PRIVACY
519 } else if (req.query.filter === 'local') {
520 req.query.isLocal = true
521 }
522
523 req.query.filter = undefined
524 }
525
526 const user = res.locals.oauth?.token.User
527
528 if (req.query.include && (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) !== true)) {
508 res.fail({ 529 res.fail({
509 status: HttpStatusCode.UNAUTHORIZED_401, 530 status: HttpStatusCode.UNAUTHORIZED_401,
510 message: 'You are not allowed to see all local videos.' 531 message: 'You are not allowed to see all local videos.'
diff --git a/server/models/account/account.ts b/server/models/account/account.ts
index 37194a119..056ec6857 100644
--- a/server/models/account/account.ts
+++ b/server/models/account/account.ts
@@ -228,10 +228,10 @@ export class AccountModel extends Model<Partial<AttributesOnly<AccountModel>>> {
228 name: 'targetAccountId', 228 name: 'targetAccountId',
229 allowNull: false 229 allowNull: false
230 }, 230 },
231 as: 'BlockedAccounts', 231 as: 'BlockedBy',
232 onDelete: 'CASCADE' 232 onDelete: 'CASCADE'
233 }) 233 })
234 BlockedAccounts: AccountBlocklistModel[] 234 BlockedBy: AccountBlocklistModel[]
235 235
236 @BeforeDestroy 236 @BeforeDestroy
237 static async sendDeleteIfOwned (instance: AccountModel, options) { 237 static async sendDeleteIfOwned (instance: AccountModel, options) {
@@ -457,6 +457,6 @@ export class AccountModel extends Model<Partial<AttributesOnly<AccountModel>>> {
457 } 457 }
458 458
459 isBlocked () { 459 isBlocked () {
460 return this.BlockedAccounts && this.BlockedAccounts.length !== 0 460 return this.BlockedBy && this.BlockedBy.length !== 0
461 } 461 }
462} 462}
diff --git a/server/models/server/server.ts b/server/models/server/server.ts
index 0d3c092e0..edbe92f73 100644
--- a/server/models/server/server.ts
+++ b/server/models/server/server.ts
@@ -50,7 +50,7 @@ export class ServerModel extends Model<Partial<AttributesOnly<ServerModel>>> {
50 }, 50 },
51 onDelete: 'CASCADE' 51 onDelete: 'CASCADE'
52 }) 52 })
53 BlockedByAccounts: ServerBlocklistModel[] 53 BlockedBy: ServerBlocklistModel[]
54 54
55 static load (id: number, transaction?: Transaction): Promise<MServer> { 55 static load (id: number, transaction?: Transaction): Promise<MServer> {
56 const query = { 56 const query = {
@@ -81,7 +81,7 @@ export class ServerModel extends Model<Partial<AttributesOnly<ServerModel>>> {
81 } 81 }
82 82
83 isBlocked () { 83 isBlocked () {
84 return this.BlockedByAccounts && this.BlockedByAccounts.length !== 0 84 return this.BlockedBy && this.BlockedBy.length !== 0
85 } 85 }
86 86
87 toFormattedJSON (this: MServerFormattable) { 87 toFormattedJSON (this: MServerFormattable) {
diff --git a/server/models/user/user-video-history.ts b/server/models/user/user-video-history.ts
index e3dc4a062..d633cc9d5 100644
--- a/server/models/user/user-video-history.ts
+++ b/server/models/user/user-video-history.ts
@@ -4,6 +4,7 @@ import { MUserAccountId, MUserId } from '@server/types/models'
4import { AttributesOnly } from '@shared/core-utils' 4import { AttributesOnly } from '@shared/core-utils'
5import { VideoModel } from '../video/video' 5import { VideoModel } from '../video/video'
6import { UserModel } from './user' 6import { UserModel } from './user'
7import { getServerActor } from '../application/application'
7 8
8@Table({ 9@Table({
9 tableName: 'userVideoHistory', 10 tableName: 'userVideoHistory',
@@ -56,14 +57,19 @@ export class UserVideoHistoryModel extends Model<Partial<AttributesOnly<UserVide
56 }) 57 })
57 User: UserModel 58 User: UserModel
58 59
59 static listForApi (user: MUserAccountId, start: number, count: number, search?: string) { 60 static async listForApi (user: MUserAccountId, start: number, count: number, search?: string) {
61 const serverActor = await getServerActor()
62
60 return VideoModel.listForApi({ 63 return VideoModel.listForApi({
61 start, 64 start,
62 count, 65 count,
63 search, 66 search,
64 sort: '-"userVideoHistory"."updatedAt"', 67 sort: '-"userVideoHistory"."updatedAt"',
65 nsfw: null, // All 68 nsfw: null, // All
66 includeLocalVideos: true, 69 displayOnlyForFollower: {
70 actorId: serverActor.id,
71 orLocalVideos: true
72 },
67 withFiles: false, 73 withFiles: false,
68 user, 74 user,
69 historyOfUser: user 75 historyOfUser: user
diff --git a/server/models/video/formatter/video-format-utils.ts b/server/models/video/formatter/video-format-utils.ts
index 0cbad5684..5dc2c2f1b 100644
--- a/server/models/video/formatter/video-format-utils.ts
+++ b/server/models/video/formatter/video-format-utils.ts
@@ -1,9 +1,10 @@
1import { uuidToShort } from '@server/helpers/uuid' 1import { uuidToShort } from '@server/helpers/uuid'
2import { generateMagnetUri } from '@server/helpers/webtorrent' 2import { generateMagnetUri } from '@server/helpers/webtorrent'
3import { getLocalVideoFileMetadataUrl } from '@server/lib/video-urls' 3import { getLocalVideoFileMetadataUrl } from '@server/lib/video-urls'
4import { VideosCommonQueryAfterSanitize } from '@shared/models'
4import { VideoFile } from '@shared/models/videos/video-file.model' 5import { VideoFile } from '@shared/models/videos/video-file.model'
5import { ActivityTagObject, ActivityUrlObject, VideoObject } from '../../../../shared/models/activitypub/objects' 6import { ActivityTagObject, ActivityUrlObject, VideoObject } from '../../../../shared/models/activitypub/objects'
6import { Video, VideoDetails } from '../../../../shared/models/videos' 7import { Video, VideoDetails, VideoInclude } from '../../../../shared/models/videos'
7import { VideoStreamingPlaylist } from '../../../../shared/models/videos/video-streaming-playlist.model' 8import { VideoStreamingPlaylist } from '../../../../shared/models/videos/video-streaming-playlist.model'
8import { isArray } from '../../../helpers/custom-validators/misc' 9import { isArray } from '../../../helpers/custom-validators/misc'
9import { 10import {
@@ -22,6 +23,7 @@ import {
22 getLocalVideoSharesActivityPubUrl 23 getLocalVideoSharesActivityPubUrl
23} from '../../../lib/activitypub/url' 24} from '../../../lib/activitypub/url'
24import { 25import {
26 MServer,
25 MStreamingPlaylistRedundanciesOpt, 27 MStreamingPlaylistRedundanciesOpt,
26 MVideo, 28 MVideo,
27 MVideoAP, 29 MVideoAP,
@@ -34,15 +36,31 @@ import { VideoCaptionModel } from '../video-caption'
34 36
35export type VideoFormattingJSONOptions = { 37export type VideoFormattingJSONOptions = {
36 completeDescription?: boolean 38 completeDescription?: boolean
37 additionalAttributes: { 39
40 additionalAttributes?: {
38 state?: boolean 41 state?: boolean
39 waitTranscoding?: boolean 42 waitTranscoding?: boolean
40 scheduledUpdate?: boolean 43 scheduledUpdate?: boolean
41 blacklistInfo?: boolean 44 blacklistInfo?: boolean
45 blockedOwner?: boolean
42 } 46 }
43} 47}
44 48
45function videoModelToFormattedJSON (video: MVideoFormattable, options?: VideoFormattingJSONOptions): Video { 49function guessAdditionalAttributesFromQuery (query: VideosCommonQueryAfterSanitize): VideoFormattingJSONOptions {
50 if (!query || !query.include) return {}
51
52 return {
53 additionalAttributes: {
54 state: !!(query.include & VideoInclude.NOT_PUBLISHED_STATE),
55 waitTranscoding: !!(query.include & VideoInclude.NOT_PUBLISHED_STATE),
56 scheduledUpdate: !!(query.include & VideoInclude.NOT_PUBLISHED_STATE),
57 blacklistInfo: !!(query.include & VideoInclude.BLACKLISTED),
58 blockedOwner: !!(query.include & VideoInclude.BLOCKED_OWNER)
59 }
60 }
61}
62
63function videoModelToFormattedJSON (video: MVideoFormattable, options: VideoFormattingJSONOptions = {}): Video {
46 const userHistory = isArray(video.UserVideoHistories) ? video.UserVideoHistories[0] : undefined 64 const userHistory = isArray(video.UserVideoHistories) ? video.UserVideoHistories[0] : undefined
47 65
48 const videoObject: Video = { 66 const videoObject: Video = {
@@ -101,29 +119,35 @@ function videoModelToFormattedJSON (video: MVideoFormattable, options?: VideoFor
101 pluginData: (video as any).pluginData 119 pluginData: (video as any).pluginData
102 } 120 }
103 121
104 if (options) { 122 const add = options.additionalAttributes
105 if (options.additionalAttributes.state === true) { 123 if (add?.state === true) {
106 videoObject.state = { 124 videoObject.state = {
107 id: video.state, 125 id: video.state,
108 label: getStateLabel(video.state) 126 label: getStateLabel(video.state)
109 }
110 } 127 }
128 }
111 129
112 if (options.additionalAttributes.waitTranscoding === true) { 130 if (add?.waitTranscoding === true) {
113 videoObject.waitTranscoding = video.waitTranscoding 131 videoObject.waitTranscoding = video.waitTranscoding
114 } 132 }
115 133
116 if (options.additionalAttributes.scheduledUpdate === true && video.ScheduleVideoUpdate) { 134 if (add?.scheduledUpdate === true && video.ScheduleVideoUpdate) {
117 videoObject.scheduledUpdate = { 135 videoObject.scheduledUpdate = {
118 updateAt: video.ScheduleVideoUpdate.updateAt, 136 updateAt: video.ScheduleVideoUpdate.updateAt,
119 privacy: video.ScheduleVideoUpdate.privacy || undefined 137 privacy: video.ScheduleVideoUpdate.privacy || undefined
120 }
121 } 138 }
139 }
122 140
123 if (options.additionalAttributes.blacklistInfo === true) { 141 if (add?.blacklistInfo === true) {
124 videoObject.blacklisted = !!video.VideoBlacklist 142 videoObject.blacklisted = !!video.VideoBlacklist
125 videoObject.blacklistedReason = video.VideoBlacklist ? video.VideoBlacklist.reason : null 143 videoObject.blacklistedReason = video.VideoBlacklist ? video.VideoBlacklist.reason : null
126 } 144 }
145
146 if (add?.blockedOwner === true) {
147 videoObject.blockedOwner = video.VideoChannel.Account.isBlocked()
148
149 const server = video.VideoChannel.Account.Actor.Server as MServer
150 videoObject.blockedServer = !!(server?.isBlocked())
127 } 151 }
128 152
129 return videoObject 153 return videoObject
@@ -464,6 +488,8 @@ export {
464 videoModelToActivityPubObject, 488 videoModelToActivityPubObject,
465 getActivityStreamDuration, 489 getActivityStreamDuration,
466 490
491 guessAdditionalAttributesFromQuery,
492
467 getCategoryLabel, 493 getCategoryLabel,
468 getLicenceLabel, 494 getLicenceLabel,
469 getLanguageLabel, 495 getLanguageLabel,
diff --git a/server/models/video/sql/shared/abstract-videos-model-query-builder.ts b/server/models/video/sql/shared/abstract-videos-model-query-builder.ts
index 0d7e64574..29827db2a 100644
--- a/server/models/video/sql/shared/abstract-videos-model-query-builder.ts
+++ b/server/models/video/sql/shared/abstract-videos-model-query-builder.ts
@@ -1,3 +1,5 @@
1import { createSafeIn } from '@server/models/utils'
2import { MUserAccountId } from '@server/types/models'
1import validator from 'validator' 3import validator from 'validator'
2import { AbstractVideosQueryBuilder } from './abstract-videos-query-builder' 4import { AbstractVideosQueryBuilder } from './abstract-videos-query-builder'
3import { VideoTables } from './video-tables' 5import { VideoTables } from './video-tables'
@@ -188,6 +190,32 @@ export class AbstractVideosModelQueryBuilder extends AbstractVideosQueryBuilder
188 } 190 }
189 } 191 }
190 192
193 protected includeBlockedOwnerAndServer (serverAccountId: number, user?: MUserAccountId) {
194 const blockerIds = [ serverAccountId ]
195 if (user) blockerIds.push(user.Account.id)
196
197 const inClause = createSafeIn(this.sequelize, blockerIds)
198
199 this.addJoin(
200 'LEFT JOIN "accountBlocklist" AS "VideoChannel->Account->AccountBlocklist" ' +
201 'ON "VideoChannel->Account"."id" = "VideoChannel->Account->AccountBlocklist"."targetAccountId" ' +
202 'AND "VideoChannel->Account->AccountBlocklist"."accountId" IN (' + inClause + ')'
203 )
204
205 this.addJoin(
206 'LEFT JOIN "serverBlocklist" AS "VideoChannel->Account->Actor->Server->ServerBlocklist" ' +
207 'ON "VideoChannel->Account->Actor->Server->ServerBlocklist"."targetServerId" = "VideoChannel->Account->Actor"."serverId" ' +
208 'AND "VideoChannel->Account->Actor->Server->ServerBlocklist"."accountId" IN (' + inClause + ') '
209 )
210
211 this.attributes = {
212 ...this.attributes,
213
214 ...this.buildAttributesObject('VideoChannel->Account->AccountBlocklist', this.tables.getBlocklistAttributes()),
215 ...this.buildAttributesObject('VideoChannel->Account->Actor->Server->ServerBlocklist', this.tables.getBlocklistAttributes())
216 }
217 }
218
191 protected includeScheduleUpdate () { 219 protected includeScheduleUpdate () {
192 this.addJoin( 220 this.addJoin(
193 'LEFT OUTER JOIN "scheduleVideoUpdate" AS "ScheduleVideoUpdate" ON "video"."id" = "ScheduleVideoUpdate"."videoId"' 221 'LEFT OUTER JOIN "scheduleVideoUpdate" AS "ScheduleVideoUpdate" ON "video"."id" = "ScheduleVideoUpdate"."videoId"'
diff --git a/server/models/video/sql/shared/video-model-builder.ts b/server/models/video/sql/shared/video-model-builder.ts
index 33a0181e9..0eac95661 100644
--- a/server/models/video/sql/shared/video-model-builder.ts
+++ b/server/models/video/sql/shared/video-model-builder.ts
@@ -1,11 +1,14 @@
1 1
2import { AccountModel } from '@server/models/account/account' 2import { AccountModel } from '@server/models/account/account'
3import { AccountBlocklistModel } from '@server/models/account/account-blocklist'
3import { ActorModel } from '@server/models/actor/actor' 4import { ActorModel } from '@server/models/actor/actor'
4import { ActorImageModel } from '@server/models/actor/actor-image' 5import { ActorImageModel } from '@server/models/actor/actor-image'
5import { VideoRedundancyModel } from '@server/models/redundancy/video-redundancy' 6import { VideoRedundancyModel } from '@server/models/redundancy/video-redundancy'
6import { ServerModel } from '@server/models/server/server' 7import { ServerModel } from '@server/models/server/server'
8import { ServerBlocklistModel } from '@server/models/server/server-blocklist'
7import { TrackerModel } from '@server/models/server/tracker' 9import { TrackerModel } from '@server/models/server/tracker'
8import { UserVideoHistoryModel } from '@server/models/user/user-video-history' 10import { UserVideoHistoryModel } from '@server/models/user/user-video-history'
11import { VideoInclude } from '@shared/models'
9import { ScheduleVideoUpdateModel } from '../../schedule-video-update' 12import { ScheduleVideoUpdateModel } from '../../schedule-video-update'
10import { TagModel } from '../../tag' 13import { TagModel } from '../../tag'
11import { ThumbnailModel } from '../../thumbnail' 14import { ThumbnailModel } from '../../thumbnail'
@@ -33,6 +36,8 @@ export class VideoModelBuilder {
33 private thumbnailsDone: Set<any> 36 private thumbnailsDone: Set<any>
34 private historyDone: Set<any> 37 private historyDone: Set<any>
35 private blacklistDone: Set<any> 38 private blacklistDone: Set<any>
39 private accountBlocklistDone: Set<any>
40 private serverBlocklistDone: Set<any>
36 private liveDone: Set<any> 41 private liveDone: Set<any>
37 private redundancyDone: Set<any> 42 private redundancyDone: Set<any>
38 private scheduleVideoUpdateDone: Set<any> 43 private scheduleVideoUpdateDone: Set<any>
@@ -51,7 +56,14 @@ export class VideoModelBuilder {
51 56
52 } 57 }
53 58
54 buildVideosFromRows (rows: SQLRow[], rowsWebTorrentFiles?: SQLRow[], rowsStreamingPlaylist?: SQLRow[]) { 59 buildVideosFromRows (options: {
60 rows: SQLRow[]
61 include?: VideoInclude
62 rowsWebTorrentFiles?: SQLRow[]
63 rowsStreamingPlaylist?: SQLRow[]
64 }) {
65 const { rows, rowsWebTorrentFiles, rowsStreamingPlaylist, include } = options
66
55 this.reinit() 67 this.reinit()
56 68
57 for (const row of rows) { 69 for (const row of rows) {
@@ -77,6 +89,15 @@ export class VideoModelBuilder {
77 this.setBlacklisted(row, videoModel) 89 this.setBlacklisted(row, videoModel)
78 this.setScheduleVideoUpdate(row, videoModel) 90 this.setScheduleVideoUpdate(row, videoModel)
79 this.setLive(row, videoModel) 91 this.setLive(row, videoModel)
92 } else {
93 if (include & VideoInclude.BLACKLISTED) {
94 this.setBlacklisted(row, videoModel)
95 }
96
97 if (include & VideoInclude.BLOCKED_OWNER) {
98 this.setBlockedOwner(row, videoModel)
99 this.setBlockedServer(row, videoModel)
100 }
80 } 101 }
81 } 102 }
82 103
@@ -91,15 +112,18 @@ export class VideoModelBuilder {
91 this.videoStreamingPlaylistMemo = {} 112 this.videoStreamingPlaylistMemo = {}
92 this.videoFileMemo = {} 113 this.videoFileMemo = {}
93 114
94 this.thumbnailsDone = new Set<number>() 115 this.thumbnailsDone = new Set()
95 this.historyDone = new Set<number>() 116 this.historyDone = new Set()
96 this.blacklistDone = new Set<number>() 117 this.blacklistDone = new Set()
97 this.liveDone = new Set<number>() 118 this.liveDone = new Set()
98 this.redundancyDone = new Set<number>() 119 this.redundancyDone = new Set()
99 this.scheduleVideoUpdateDone = new Set<number>() 120 this.scheduleVideoUpdateDone = new Set()
121
122 this.accountBlocklistDone = new Set()
123 this.serverBlocklistDone = new Set()
100 124
101 this.trackersDone = new Set<string>() 125 this.trackersDone = new Set()
102 this.tagsDone = new Set<string>() 126 this.tagsDone = new Set()
103 127
104 this.videos = [] 128 this.videos = []
105 } 129 }
@@ -162,6 +186,8 @@ export class VideoModelBuilder {
162 const accountModel = new AccountModel(this.grab(row, this.tables.getAccountAttributes(), 'VideoChannel.Account'), this.buildOpts) 186 const accountModel = new AccountModel(this.grab(row, this.tables.getAccountAttributes(), 'VideoChannel.Account'), this.buildOpts)
163 accountModel.Actor = this.buildActor(row, 'VideoChannel.Account') 187 accountModel.Actor = this.buildActor(row, 'VideoChannel.Account')
164 188
189 accountModel.BlockedBy = []
190
165 channelModel.Account = accountModel 191 channelModel.Account = accountModel
166 192
167 videoModel.VideoChannel = channelModel 193 videoModel.VideoChannel = channelModel
@@ -180,6 +206,8 @@ export class VideoModelBuilder {
180 ? new ServerModel(this.grab(row, this.tables.getServerAttributes(), serverPrefix), this.buildOpts) 206 ? new ServerModel(this.grab(row, this.tables.getServerAttributes(), serverPrefix), this.buildOpts)
181 : null 207 : null
182 208
209 if (serverModel) serverModel.BlockedBy = []
210
183 const actorModel = new ActorModel(this.grab(row, this.tables.getActorAttributes(), actorPrefix), this.buildOpts) 211 const actorModel = new ActorModel(this.grab(row, this.tables.getActorAttributes(), actorPrefix), this.buildOpts)
184 actorModel.Avatar = avatarModel 212 actorModel.Avatar = avatarModel
185 actorModel.Server = serverModel 213 actorModel.Server = serverModel
@@ -297,6 +325,32 @@ export class VideoModelBuilder {
297 this.blacklistDone.add(id) 325 this.blacklistDone.add(id)
298 } 326 }
299 327
328 private setBlockedOwner (row: SQLRow, videoModel: VideoModel) {
329 const id = row['VideoChannel.Account.AccountBlocklist.id']
330 if (!id) return
331
332 const key = `${videoModel.id}-${id}`
333 if (this.accountBlocklistDone.has(key)) return
334
335 const attributes = this.grab(row, this.tables.getBlocklistAttributes(), 'VideoChannel.Account.AccountBlocklist')
336 videoModel.VideoChannel.Account.BlockedBy.push(new AccountBlocklistModel(attributes, this.buildOpts))
337
338 this.accountBlocklistDone.add(key)
339 }
340
341 private setBlockedServer (row: SQLRow, videoModel: VideoModel) {
342 const id = row['VideoChannel.Account.Actor.Server.ServerBlocklist.id']
343 if (!id || this.serverBlocklistDone.has(id)) return
344
345 const key = `${videoModel.id}-${id}`
346 if (this.serverBlocklistDone.has(key)) return
347
348 const attributes = this.grab(row, this.tables.getBlocklistAttributes(), 'VideoChannel.Account.Actor.Server.ServerBlocklist')
349 videoModel.VideoChannel.Account.Actor.Server.BlockedBy.push(new ServerBlocklistModel(attributes, this.buildOpts))
350
351 this.serverBlocklistDone.add(key)
352 }
353
300 private setScheduleVideoUpdate (row: SQLRow, videoModel: VideoModel) { 354 private setScheduleVideoUpdate (row: SQLRow, videoModel: VideoModel) {
301 const id = row['ScheduleVideoUpdate.id'] 355 const id = row['ScheduleVideoUpdate.id']
302 if (!id || this.scheduleVideoUpdateDone.has(id)) return 356 if (!id || this.scheduleVideoUpdateDone.has(id)) return
diff --git a/server/models/video/sql/shared/video-tables.ts b/server/models/video/sql/shared/video-tables.ts
index 75823864d..042b9d5da 100644
--- a/server/models/video/sql/shared/video-tables.ts
+++ b/server/models/video/sql/shared/video-tables.ts
@@ -139,6 +139,10 @@ export class VideoTables {
139 return [ 'id', 'reason', 'unfederated' ] 139 return [ 'id', 'reason', 'unfederated' ]
140 } 140 }
141 141
142 getBlocklistAttributes () {
143 return [ 'id' ]
144 }
145
142 getScheduleUpdateAttributes () { 146 getScheduleUpdateAttributes () {
143 return [ 147 return [
144 'id', 148 'id',
diff --git a/server/models/video/sql/video-model-get-query-builder.ts b/server/models/video/sql/video-model-get-query-builder.ts
index f234e8778..d18ddae67 100644
--- a/server/models/video/sql/video-model-get-query-builder.ts
+++ b/server/models/video/sql/video-model-get-query-builder.ts
@@ -62,7 +62,11 @@ export class VideosModelGetQueryBuilder {
62 : Promise.resolve(undefined) 62 : Promise.resolve(undefined)
63 ]) 63 ])
64 64
65 const videos = this.videoModelBuilder.buildVideosFromRows(videoRows, webtorrentFilesRows, streamingPlaylistFilesRows) 65 const videos = this.videoModelBuilder.buildVideosFromRows({
66 rows: videoRows,
67 rowsWebTorrentFiles: webtorrentFilesRows,
68 rowsStreamingPlaylist: streamingPlaylistFilesRows
69 })
66 70
67 if (videos.length > 1) { 71 if (videos.length > 1) {
68 throw new Error('Video results is more than ') 72 throw new Error('Video results is more than ')
diff --git a/server/models/video/sql/videos-id-list-query-builder.ts b/server/models/video/sql/videos-id-list-query-builder.ts
index 7625c003d..3eb547e75 100644
--- a/server/models/video/sql/videos-id-list-query-builder.ts
+++ b/server/models/video/sql/videos-id-list-query-builder.ts
@@ -4,7 +4,7 @@ import { exists } from '@server/helpers/custom-validators/misc'
4import { WEBSERVER } from '@server/initializers/constants' 4import { WEBSERVER } from '@server/initializers/constants'
5import { buildDirectionAndField, createSafeIn } from '@server/models/utils' 5import { buildDirectionAndField, createSafeIn } from '@server/models/utils'
6import { MUserAccountId, MUserId } from '@server/types/models' 6import { MUserAccountId, MUserId } from '@server/types/models'
7import { VideoFilter, VideoPrivacy, VideoState } from '@shared/models' 7import { VideoInclude, VideoPrivacy, VideoState } from '@shared/models'
8import { AbstractVideosQueryBuilder } from './shared/abstract-videos-query-builder' 8import { AbstractVideosQueryBuilder } from './shared/abstract-videos-query-builder'
9 9
10/** 10/**
@@ -13,21 +13,27 @@ import { AbstractVideosQueryBuilder } from './shared/abstract-videos-query-build
13 * 13 *
14 */ 14 */
15 15
16export type DisplayOnlyForFollowerOptions = {
17 actorId: number
18 orLocalVideos: boolean
19}
20
16export type BuildVideosListQueryOptions = { 21export type BuildVideosListQueryOptions = {
17 attributes?: string[] 22 attributes?: string[]
18 23
19 serverAccountId: number 24 serverAccountIdForBlock: number
20 followerActorId: number 25
21 includeLocalVideos: boolean 26 displayOnlyForFollower: DisplayOnlyForFollowerOptions
22 27
23 count: number 28 count: number
24 start: number 29 start: number
25 sort: string 30 sort: string
26 31
27 nsfw?: boolean 32 nsfw?: boolean
28 filter?: VideoFilter
29 host?: string 33 host?: string
30 isLive?: boolean 34 isLive?: boolean
35 isLocal?: boolean
36 include?: VideoInclude
31 37
32 categoryOneOf?: number[] 38 categoryOneOf?: number[]
33 licenceOneOf?: number[] 39 licenceOneOf?: number[]
@@ -101,6 +107,7 @@ export class VideosIdListQueryBuilder extends AbstractVideosQueryBuilder {
101 107
102 getIdsListQueryAndSort (options: BuildVideosListQueryOptions) { 108 getIdsListQueryAndSort (options: BuildVideosListQueryOptions) {
103 this.buildIdsListQuery(options) 109 this.buildIdsListQuery(options)
110
104 return { query: this.query, sort: this.sort, replacements: this.replacements } 111 return { query: this.query, sort: this.sort, replacements: this.replacements }
105 } 112 }
106 113
@@ -116,23 +123,30 @@ export class VideosIdListQueryBuilder extends AbstractVideosQueryBuilder {
116 'INNER JOIN "actor" "accountActor" ON "account"."actorId" = "accountActor"."id"' 123 'INNER JOIN "actor" "accountActor" ON "account"."actorId" = "accountActor"."id"'
117 ]) 124 ])
118 125
119 this.whereNotBlacklisted() 126 if (!(options.include & VideoInclude.BLACKLISTED)) {
127 this.whereNotBlacklisted()
128 }
120 129
121 if (options.serverAccountId) { 130 if (options.serverAccountIdForBlock && !(options.include & VideoInclude.BLOCKED_OWNER)) {
122 this.whereNotBlocked(options.serverAccountId, options.user) 131 this.whereNotBlocked(options.serverAccountIdForBlock, options.user)
123 } 132 }
124 133
125 // Only list public/published videos 134 // Only list published videos
126 if (!options.filter || (options.filter !== 'all-local' && options.filter !== 'all')) { 135 if (!(options.include & VideoInclude.NOT_PUBLISHED_STATE)) {
127 this.whereStateAndPrivacyAvailable(options.user) 136 this.whereStateAvailable()
137 }
138
139 // Only list videos with the appropriate priavcy
140 if (!(options.include & VideoInclude.HIDDEN_PRIVACY)) {
141 this.wherePrivacyAvailable(options.user)
128 } 142 }
129 143
130 if (options.videoPlaylistId) { 144 if (options.videoPlaylistId) {
131 this.joinPlaylist(options.videoPlaylistId) 145 this.joinPlaylist(options.videoPlaylistId)
132 } 146 }
133 147
134 if (options.filter && (options.filter === 'local' || options.filter === 'all-local')) { 148 if (exists(options.isLocal)) {
135 this.whereOnlyLocal() 149 this.whereLocal(options.isLocal)
136 } 150 }
137 151
138 if (options.host) { 152 if (options.host) {
@@ -147,8 +161,8 @@ export class VideosIdListQueryBuilder extends AbstractVideosQueryBuilder {
147 this.whereChannelId(options.videoChannelId) 161 this.whereChannelId(options.videoChannelId)
148 } 162 }
149 163
150 if (options.followerActorId) { 164 if (options.displayOnlyForFollower) {
151 this.whereFollowerActorId(options.followerActorId, options.includeLocalVideos) 165 this.whereFollowerActorId(options.displayOnlyForFollower)
152 } 166 }
153 167
154 if (options.withFiles === true) { 168 if (options.withFiles === true) {
@@ -282,12 +296,14 @@ export class VideosIdListQueryBuilder extends AbstractVideosQueryBuilder {
282 this.replacements.videoPlaylistId = playlistId 296 this.replacements.videoPlaylistId = playlistId
283 } 297 }
284 298
285 private whereStateAndPrivacyAvailable (user?: MUserAccountId) { 299 private whereStateAvailable () {
286 this.and.push( 300 this.and.push(
287 `("video"."state" = ${VideoState.PUBLISHED} OR ` + 301 `("video"."state" = ${VideoState.PUBLISHED} OR ` +
288 `("video"."state" = ${VideoState.TO_TRANSCODE} AND "video"."waitTranscoding" IS false))` 302 `("video"."state" = ${VideoState.TO_TRANSCODE} AND "video"."waitTranscoding" IS false))`
289 ) 303 )
304 }
290 305
306 private wherePrivacyAvailable (user?: MUserAccountId) {
291 if (user) { 307 if (user) {
292 this.and.push( 308 this.and.push(
293 `("video"."privacy" = ${VideoPrivacy.PUBLIC} OR "video"."privacy" = ${VideoPrivacy.INTERNAL})` 309 `("video"."privacy" = ${VideoPrivacy.PUBLIC} OR "video"."privacy" = ${VideoPrivacy.INTERNAL})`
@@ -299,8 +315,10 @@ export class VideosIdListQueryBuilder extends AbstractVideosQueryBuilder {
299 } 315 }
300 } 316 }
301 317
302 private whereOnlyLocal () { 318 private whereLocal (isLocal: boolean) {
303 this.and.push('"video"."remote" IS FALSE') 319 const isRemote = isLocal ? 'FALSE' : 'TRUE'
320
321 this.and.push('"video"."remote" IS ' + isRemote)
304 } 322 }
305 323
306 private whereHost (host: string) { 324 private whereHost (host: string) {
@@ -326,7 +344,7 @@ export class VideosIdListQueryBuilder extends AbstractVideosQueryBuilder {
326 this.replacements.videoChannelId = channelId 344 this.replacements.videoChannelId = channelId
327 } 345 }
328 346
329 private whereFollowerActorId (followerActorId: number, includeLocalVideos: boolean) { 347 private whereFollowerActorId (options: { actorId: number, orLocalVideos: boolean }) {
330 let query = 348 let query =
331 '(' + 349 '(' +
332 ' EXISTS (' + // Videos shared by actors we follow 350 ' EXISTS (' + // Videos shared by actors we follow
@@ -342,14 +360,14 @@ export class VideosIdListQueryBuilder extends AbstractVideosQueryBuilder {
342 ' AND "actorFollow"."state" = \'accepted\'' + 360 ' AND "actorFollow"."state" = \'accepted\'' +
343 ' )' 361 ' )'
344 362
345 if (includeLocalVideos) { 363 if (options.orLocalVideos) {
346 query += ' OR "video"."remote" IS FALSE' 364 query += ' OR "video"."remote" IS FALSE'
347 } 365 }
348 366
349 query += ')' 367 query += ')'
350 368
351 this.and.push(query) 369 this.and.push(query)
352 this.replacements.followerActorId = followerActorId 370 this.replacements.followerActorId = options.actorId
353 } 371 }
354 372
355 private whereFileExists () { 373 private whereFileExists () {
diff --git a/server/models/video/sql/videos-model-list-query-builder.ts b/server/models/video/sql/videos-model-list-query-builder.ts
index e61c51de8..ef92bd2b0 100644
--- a/server/models/video/sql/videos-model-list-query-builder.ts
+++ b/server/models/video/sql/videos-model-list-query-builder.ts
@@ -1,3 +1,4 @@
1import { VideoInclude } from '@shared/models'
1import { Sequelize } from 'sequelize' 2import { Sequelize } from 'sequelize'
2import { AbstractVideosModelQueryBuilder } from './shared/abstract-videos-model-query-builder' 3import { AbstractVideosModelQueryBuilder } from './shared/abstract-videos-model-query-builder'
3import { VideoModelBuilder } from './shared/video-model-builder' 4import { VideoModelBuilder } from './shared/video-model-builder'
@@ -28,7 +29,7 @@ export class VideosModelListQueryBuilder extends AbstractVideosModelQueryBuilder
28 this.buildListQueryFromIdsQuery(options) 29 this.buildListQueryFromIdsQuery(options)
29 30
30 return this.runQuery() 31 return this.runQuery()
31 .then(rows => this.videoModelBuilder.buildVideosFromRows(rows)) 32 .then(rows => this.videoModelBuilder.buildVideosFromRows({ rows, include: options.include }))
32 } 33 }
33 34
34 private buildInnerQuery (options: BuildVideosListQueryOptions) { 35 private buildInnerQuery (options: BuildVideosListQueryOptions) {
@@ -64,6 +65,14 @@ export class VideosModelListQueryBuilder extends AbstractVideosModelQueryBuilder
64 this.includePlaylist(options.videoPlaylistId) 65 this.includePlaylist(options.videoPlaylistId)
65 } 66 }
66 67
68 if (options.include & VideoInclude.BLACKLISTED) {
69 this.includeBlacklisted()
70 }
71
72 if (options.include & VideoInclude.BLOCKED_OWNER) {
73 this.includeBlockedOwnerAndServer(options.serverAccountIdForBlock, options.user)
74 }
75
67 const select = this.buildSelect() 76 const select = this.buildSelect()
68 77
69 this.query = `${select} FROM (${this.innerQuery}) AS "tmp" ${this.joins} ${this.innerSort}` 78 this.query = `${select} FROM (${this.innerQuery}) AS "tmp" ${this.joins} ${this.innerSort}`
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index b5c46c86c..26be34329 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -34,12 +34,12 @@ import { VideoPathManager } from '@server/lib/video-path-manager'
34import { getServerActor } from '@server/models/application/application' 34import { getServerActor } from '@server/models/application/application'
35import { ModelCache } from '@server/models/model-cache' 35import { ModelCache } from '@server/models/model-cache'
36import { AttributesOnly, buildVideoEmbedPath, buildVideoWatchPath, pick } from '@shared/core-utils' 36import { AttributesOnly, buildVideoEmbedPath, buildVideoWatchPath, pick } from '@shared/core-utils'
37import { VideoInclude } from '@shared/models'
37import { VideoFile } from '@shared/models/videos/video-file.model' 38import { VideoFile } from '@shared/models/videos/video-file.model'
38import { ResultList, UserRight, VideoPrivacy, VideoState } from '../../../shared' 39import { ResultList, UserRight, VideoPrivacy, VideoState } from '../../../shared'
39import { VideoObject } from '../../../shared/models/activitypub/objects' 40import { VideoObject } from '../../../shared/models/activitypub/objects'
40import { Video, VideoDetails, VideoRateType, VideoStorage } from '../../../shared/models/videos' 41import { Video, VideoDetails, VideoRateType, VideoStorage } from '../../../shared/models/videos'
41import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' 42import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
42import { VideoFilter } from '../../../shared/models/videos/video-query.type'
43import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' 43import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
44import { peertubeTruncate } from '../../helpers/core-utils' 44import { peertubeTruncate } from '../../helpers/core-utils'
45import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 45import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
@@ -106,7 +106,7 @@ import {
106} from './formatter/video-format-utils' 106} from './formatter/video-format-utils'
107import { ScheduleVideoUpdateModel } from './schedule-video-update' 107import { ScheduleVideoUpdateModel } from './schedule-video-update'
108import { VideosModelGetQueryBuilder } from './sql/video-model-get-query-builder' 108import { VideosModelGetQueryBuilder } from './sql/video-model-get-query-builder'
109import { BuildVideosListQueryOptions, VideosIdListQueryBuilder } from './sql/videos-id-list-query-builder' 109import { BuildVideosListQueryOptions, DisplayOnlyForFollowerOptions, VideosIdListQueryBuilder } from './sql/videos-id-list-query-builder'
110import { VideosModelListQueryBuilder } from './sql/videos-model-list-query-builder' 110import { VideosModelListQueryBuilder } from './sql/videos-model-list-query-builder'
111import { TagModel } from './tag' 111import { TagModel } from './tag'
112import { ThumbnailModel } from './thumbnail' 112import { ThumbnailModel } from './thumbnail'
@@ -145,35 +145,6 @@ export type ForAPIOptions = {
145 withAccountBlockerIds?: number[] 145 withAccountBlockerIds?: number[]
146} 146}
147 147
148export type AvailableForListIDsOptions = {
149 serverAccountId: number
150 followerActorId: number
151 includeLocalVideos: boolean
152
153 attributesType?: 'none' | 'id' | 'all'
154
155 filter?: VideoFilter
156 categoryOneOf?: number[]
157 nsfw?: boolean
158 licenceOneOf?: number[]
159 languageOneOf?: string[]
160 tagsOneOf?: string[]
161 tagsAllOf?: string[]
162
163 withFiles?: boolean
164
165 accountId?: number
166 videoChannelId?: number
167
168 videoPlaylistId?: number
169
170 trendingDays?: number
171 user?: MUserAccountId
172 historyOfUser?: MUserId
173
174 baseWhere?: WhereOptions[]
175}
176
177@Scopes(() => ({ 148@Scopes(() => ({
178 [ScopeNames.WITH_IMMUTABLE_ATTRIBUTES]: { 149 [ScopeNames.WITH_IMMUTABLE_ATTRIBUTES]: {
179 attributes: [ 'id', 'url', 'uuid', 'remote' ] 150 attributes: [ 'id', 'url', 'uuid', 'remote' ]
@@ -1054,10 +1025,10 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1054 sort: string 1025 sort: string
1055 1026
1056 nsfw: boolean 1027 nsfw: boolean
1057 filter?: VideoFilter
1058 isLive?: boolean 1028 isLive?: boolean
1029 isLocal?: boolean
1030 include?: VideoInclude
1059 1031
1060 includeLocalVideos: boolean
1061 withFiles: boolean 1032 withFiles: boolean
1062 1033
1063 categoryOneOf?: number[] 1034 categoryOneOf?: number[]
@@ -1069,7 +1040,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1069 accountId?: number 1040 accountId?: number
1070 videoChannelId?: number 1041 videoChannelId?: number
1071 1042
1072 followerActorId?: number 1043 displayOnlyForFollower: DisplayOnlyForFollowerOptions | null
1073 1044
1074 videoPlaylistId?: number 1045 videoPlaylistId?: number
1075 1046
@@ -1082,7 +1053,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1082 1053
1083 search?: string 1054 search?: string
1084 }) { 1055 }) {
1085 if ((options.filter === 'all-local' || options.filter === 'all') && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) { 1056 if (options.include && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) {
1086 throw new Error('Try to filter all-local but no user has not the see all videos right') 1057 throw new Error('Try to filter all-local but no user has not the see all videos right')
1087 } 1058 }
1088 1059
@@ -1096,11 +1067,6 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1096 1067
1097 const serverActor = await getServerActor() 1068 const serverActor = await getServerActor()
1098 1069
1099 // followerActorId === null has a meaning, so just check undefined
1100 const followerActorId = options.followerActorId !== undefined
1101 ? options.followerActorId
1102 : serverActor.id
1103
1104 const queryOptions = { 1070 const queryOptions = {
1105 ...pick(options, [ 1071 ...pick(options, [
1106 'start', 1072 'start',
@@ -1113,19 +1079,19 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1113 'languageOneOf', 1079 'languageOneOf',
1114 'tagsOneOf', 1080 'tagsOneOf',
1115 'tagsAllOf', 1081 'tagsAllOf',
1116 'filter', 1082 'isLocal',
1083 'include',
1084 'displayOnlyForFollower',
1117 'withFiles', 1085 'withFiles',
1118 'accountId', 1086 'accountId',
1119 'videoChannelId', 1087 'videoChannelId',
1120 'videoPlaylistId', 1088 'videoPlaylistId',
1121 'includeLocalVideos',
1122 'user', 1089 'user',
1123 'historyOfUser', 1090 'historyOfUser',
1124 'search' 1091 'search'
1125 ]), 1092 ]),
1126 1093
1127 followerActorId, 1094 serverAccountIdForBlock: serverActor.Account.id,
1128 serverAccountId: serverActor.Account.id,
1129 trendingDays, 1095 trendingDays,
1130 trendingAlgorithm 1096 trendingAlgorithm
1131 } 1097 }
@@ -1137,7 +1103,6 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1137 start: number 1103 start: number
1138 count: number 1104 count: number
1139 sort: string 1105 sort: string
1140 includeLocalVideos: boolean
1141 search?: string 1106 search?: string
1142 host?: string 1107 host?: string
1143 startDate?: string // ISO 8601 1108 startDate?: string // ISO 8601
@@ -1146,6 +1111,8 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1146 originallyPublishedEndDate?: string 1111 originallyPublishedEndDate?: string
1147 nsfw?: boolean 1112 nsfw?: boolean
1148 isLive?: boolean 1113 isLive?: boolean
1114 isLocal?: boolean
1115 include?: VideoInclude
1149 categoryOneOf?: number[] 1116 categoryOneOf?: number[]
1150 licenceOneOf?: number[] 1117 licenceOneOf?: number[]
1151 languageOneOf?: string[] 1118 languageOneOf?: string[]
@@ -1154,14 +1121,14 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1154 durationMin?: number // seconds 1121 durationMin?: number // seconds
1155 durationMax?: number // seconds 1122 durationMax?: number // seconds
1156 user?: MUserAccountId 1123 user?: MUserAccountId
1157 filter?: VideoFilter
1158 uuids?: string[] 1124 uuids?: string[]
1125 displayOnlyForFollower: DisplayOnlyForFollowerOptions | null
1159 }) { 1126 }) {
1160 const serverActor = await getServerActor() 1127 const serverActor = await getServerActor()
1161 1128
1162 const queryOptions = { 1129 const queryOptions = {
1163 ...pick(options, [ 1130 ...pick(options, [
1164 'includeLocalVideos', 1131 'include',
1165 'nsfw', 1132 'nsfw',
1166 'isLive', 1133 'isLive',
1167 'categoryOneOf', 1134 'categoryOneOf',
@@ -1170,7 +1137,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1170 'tagsOneOf', 1137 'tagsOneOf',
1171 'tagsAllOf', 1138 'tagsAllOf',
1172 'user', 1139 'user',
1173 'filter', 1140 'isLocal',
1174 'host', 1141 'host',
1175 'start', 1142 'start',
1176 'count', 1143 'count',
@@ -1182,11 +1149,10 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1182 'durationMin', 1149 'durationMin',
1183 'durationMax', 1150 'durationMax',
1184 'uuids', 1151 'uuids',
1185 'search' 1152 'search',
1153 'displayOnlyForFollower'
1186 ]), 1154 ]),
1187 1155 serverAccountIdForBlock: serverActor.Account.id
1188 followerActorId: serverActor.id,
1189 serverAccountId: serverActor.Account.id
1190 } 1156 }
1191 1157
1192 return VideoModel.getAvailableForApi(queryOptions) 1158 return VideoModel.getAvailableForApi(queryOptions)
@@ -1369,12 +1335,17 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1369 // Sequelize could return null... 1335 // Sequelize could return null...
1370 if (!totalLocalVideoViews) totalLocalVideoViews = 0 1336 if (!totalLocalVideoViews) totalLocalVideoViews = 0
1371 1337
1338 const serverActor = await getServerActor()
1339
1372 const { total: totalVideos } = await VideoModel.listForApi({ 1340 const { total: totalVideos } = await VideoModel.listForApi({
1373 start: 0, 1341 start: 0,
1374 count: 0, 1342 count: 0,
1375 sort: '-publishedAt', 1343 sort: '-publishedAt',
1376 nsfw: buildNSFWFilter(), 1344 nsfw: buildNSFWFilter(),
1377 includeLocalVideos: true, 1345 displayOnlyForFollower: {
1346 actorId: serverActor.id,
1347 orLocalVideos: true
1348 },
1378 withFiles: false 1349 withFiles: false
1379 }) 1350 })
1380 1351
@@ -1455,7 +1426,6 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1455 // threshold corresponds to how many video the field should have to be returned 1426 // threshold corresponds to how many video the field should have to be returned
1456 static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) { 1427 static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) {
1457 const serverActor = await getServerActor() 1428 const serverActor = await getServerActor()
1458 const followerActorId = serverActor.id
1459 1429
1460 const queryOptions: BuildVideosListQueryOptions = { 1430 const queryOptions: BuildVideosListQueryOptions = {
1461 attributes: [ `"${field}"` ], 1431 attributes: [ `"${field}"` ],
@@ -1464,9 +1434,11 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1464 start: 0, 1434 start: 0,
1465 sort: 'random', 1435 sort: 'random',
1466 count, 1436 count,
1467 serverAccountId: serverActor.Account.id, 1437 serverAccountIdForBlock: serverActor.Account.id,
1468 followerActorId, 1438 displayOnlyForFollower: {
1469 includeLocalVideos: true 1439 actorId: serverActor.id,
1440 orLocalVideos: true
1441 }
1470 } 1442 }
1471 1443
1472 const queryBuilder = new VideosIdListQueryBuilder(VideoModel.sequelize) 1444 const queryBuilder = new VideosIdListQueryBuilder(VideoModel.sequelize)
diff --git a/server/tests/api/check-params/index.ts b/server/tests/api/check-params/index.ts
index a14e4d3e0..0882f8176 100644
--- a/server/tests/api/check-params/index.ts
+++ b/server/tests/api/check-params/index.ts
@@ -27,6 +27,6 @@ import './video-comments'
27import './video-imports' 27import './video-imports'
28import './video-playlists' 28import './video-playlists'
29import './videos' 29import './videos'
30import './videos-filter' 30import './videos-common-filters'
31import './videos-history' 31import './videos-history'
32import './videos-overviews' 32import './videos-overviews'
diff --git a/server/tests/api/check-params/videos-common-filters.ts b/server/tests/api/check-params/videos-common-filters.ts
new file mode 100644
index 000000000..afe42b0d5
--- /dev/null
+++ b/server/tests/api/check-params/videos-common-filters.ts
@@ -0,0 +1,203 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import 'mocha'
4import {
5 cleanupTests,
6 createSingleServer,
7 makeGetRequest,
8 PeerTubeServer,
9 setAccessTokensToServers,
10 setDefaultVideoChannel
11} from '@shared/extra-utils'
12import { HttpStatusCode, UserRole, VideoInclude } from '@shared/models'
13
14describe('Test video filters validators', function () {
15 let server: PeerTubeServer
16 let userAccessToken: string
17 let moderatorAccessToken: string
18
19 // ---------------------------------------------------------------
20
21 before(async function () {
22 this.timeout(30000)
23
24 server = await createSingleServer(1)
25
26 await setAccessTokensToServers([ server ])
27 await setDefaultVideoChannel([ server ])
28
29 const user = { username: 'user1', password: 'my super password' }
30 await server.users.create({ username: user.username, password: user.password })
31 userAccessToken = await server.login.getAccessToken(user)
32
33 const moderator = { username: 'moderator', password: 'my super password' }
34 await server.users.create({ username: moderator.username, password: moderator.password, role: UserRole.MODERATOR })
35
36 moderatorAccessToken = await server.login.getAccessToken(moderator)
37 })
38
39 describe('When setting a deprecated video filter', function () {
40
41 async function testEndpoints (token: string, filter: string, expectedStatus: HttpStatusCode) {
42 const paths = [
43 '/api/v1/video-channels/root_channel/videos',
44 '/api/v1/accounts/root/videos',
45 '/api/v1/videos',
46 '/api/v1/search/videos'
47 ]
48
49 for (const path of paths) {
50 await makeGetRequest({
51 url: server.url,
52 path,
53 token,
54 query: {
55 filter
56 },
57 expectedStatus
58 })
59 }
60 }
61
62 it('Should fail with a bad filter', async function () {
63 await testEndpoints(server.accessToken, 'bad-filter', HttpStatusCode.BAD_REQUEST_400)
64 })
65
66 it('Should succeed with a good filter', async function () {
67 await testEndpoints(server.accessToken, 'local', HttpStatusCode.OK_200)
68 })
69
70 it('Should fail to list all-local/all with a simple user', async function () {
71 await testEndpoints(userAccessToken, 'all-local', HttpStatusCode.UNAUTHORIZED_401)
72 await testEndpoints(userAccessToken, 'all', HttpStatusCode.UNAUTHORIZED_401)
73 })
74
75 it('Should succeed to list all-local/all with a moderator', async function () {
76 await testEndpoints(moderatorAccessToken, 'all-local', HttpStatusCode.OK_200)
77 await testEndpoints(moderatorAccessToken, 'all', HttpStatusCode.OK_200)
78 })
79
80 it('Should succeed to list all-local/all with an admin', async function () {
81 await testEndpoints(server.accessToken, 'all-local', HttpStatusCode.OK_200)
82 await testEndpoints(server.accessToken, 'all', HttpStatusCode.OK_200)
83 })
84
85 // Because we cannot authenticate the user on the RSS endpoint
86 it('Should fail on the feeds endpoint with the all-local/all filter', async function () {
87 for (const filter of [ 'all', 'all-local' ]) {
88 await makeGetRequest({
89 url: server.url,
90 path: '/feeds/videos.json',
91 expectedStatus: HttpStatusCode.UNAUTHORIZED_401,
92 query: {
93 filter
94 }
95 })
96 }
97 })
98
99 it('Should succeed on the feeds endpoint with the local filter', async function () {
100 await makeGetRequest({
101 url: server.url,
102 path: '/feeds/videos.json',
103 expectedStatus: HttpStatusCode.OK_200,
104 query: {
105 filter: 'local'
106 }
107 })
108 })
109 })
110
111 describe('When setting video filters', function () {
112
113 const validIncludes = [
114 VideoInclude.NONE,
115 VideoInclude.HIDDEN_PRIVACY,
116 VideoInclude.NOT_PUBLISHED_STATE | VideoInclude.BLACKLISTED
117 ]
118
119 async function testEndpoints (options: {
120 token?: string
121 isLocal?: boolean
122 include?: VideoInclude
123 expectedStatus: HttpStatusCode
124 }) {
125 const paths = [
126 '/api/v1/video-channels/root_channel/videos',
127 '/api/v1/accounts/root/videos',
128 '/api/v1/videos',
129 '/api/v1/search/videos'
130 ]
131
132 for (const path of paths) {
133 await makeGetRequest({
134 url: server.url,
135 path,
136 token: options.token || server.accessToken,
137 query: {
138 isLocal: options.isLocal,
139 include: options.include
140 },
141 expectedStatus: options.expectedStatus
142 })
143 }
144 }
145
146 it('Should fail with a bad include', async function () {
147 await testEndpoints({ include: 'toto' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
148 })
149
150 it('Should succeed with a good include', async function () {
151 for (const include of validIncludes) {
152 await testEndpoints({ include, expectedStatus: HttpStatusCode.OK_200 })
153 }
154 })
155
156 it('Should fail to include more videos with a simple user', async function () {
157 for (const include of validIncludes) {
158 await testEndpoints({ token: userAccessToken, include, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
159 }
160 })
161
162 it('Should succeed to list all local/all with a moderator', async function () {
163 for (const include of validIncludes) {
164 await testEndpoints({ token: moderatorAccessToken, include, expectedStatus: HttpStatusCode.OK_200 })
165 }
166 })
167
168 it('Should succeed to list all local/all with an admin', async function () {
169 for (const include of validIncludes) {
170 await testEndpoints({ token: server.accessToken, include, expectedStatus: HttpStatusCode.OK_200 })
171 }
172 })
173
174 // Because we cannot authenticate the user on the RSS endpoint
175 it('Should fail on the feeds endpoint with the all filter', async function () {
176 for (const include of [ VideoInclude.NOT_PUBLISHED_STATE ]) {
177 await makeGetRequest({
178 url: server.url,
179 path: '/feeds/videos.json',
180 expectedStatus: HttpStatusCode.UNAUTHORIZED_401,
181 query: {
182 include
183 }
184 })
185 }
186 })
187
188 it('Should succeed on the feeds endpoint with the local filter', async function () {
189 await makeGetRequest({
190 url: server.url,
191 path: '/feeds/videos.json',
192 expectedStatus: HttpStatusCode.OK_200,
193 query: {
194 isLocal: true
195 }
196 })
197 })
198 })
199
200 after(async function () {
201 await cleanupTests([ server ])
202 })
203})
diff --git a/server/tests/api/check-params/videos-filter.ts b/server/tests/api/check-params/videos-filter.ts
deleted file mode 100644
index d08570bbe..000000000
--- a/server/tests/api/check-params/videos-filter.ts
+++ /dev/null
@@ -1,114 +0,0 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import 'mocha'
4import {
5 cleanupTests,
6 createSingleServer,
7 makeGetRequest,
8 PeerTubeServer,
9 setAccessTokensToServers,
10 setDefaultVideoChannel
11} from '@shared/extra-utils'
12import { HttpStatusCode, UserRole } from '@shared/models'
13
14async function testEndpoints (server: PeerTubeServer, token: string, filter: string, expectedStatus: HttpStatusCode) {
15 const paths = [
16 '/api/v1/video-channels/root_channel/videos',
17 '/api/v1/accounts/root/videos',
18 '/api/v1/videos',
19 '/api/v1/search/videos'
20 ]
21
22 for (const path of paths) {
23 await makeGetRequest({
24 url: server.url,
25 path,
26 token,
27 query: {
28 filter
29 },
30 expectedStatus
31 })
32 }
33}
34
35describe('Test video filters validators', function () {
36 let server: PeerTubeServer
37 let userAccessToken: string
38 let moderatorAccessToken: string
39
40 // ---------------------------------------------------------------
41
42 before(async function () {
43 this.timeout(30000)
44
45 server = await createSingleServer(1)
46
47 await setAccessTokensToServers([ server ])
48 await setDefaultVideoChannel([ server ])
49
50 const user = { username: 'user1', password: 'my super password' }
51 await server.users.create({ username: user.username, password: user.password })
52 userAccessToken = await server.login.getAccessToken(user)
53
54 const moderator = { username: 'moderator', password: 'my super password' }
55 await server.users.create({ username: moderator.username, password: moderator.password, role: UserRole.MODERATOR })
56
57 moderatorAccessToken = await server.login.getAccessToken(moderator)
58 })
59
60 describe('When setting a video filter', function () {
61
62 it('Should fail with a bad filter', async function () {
63 await testEndpoints(server, server.accessToken, 'bad-filter', HttpStatusCode.BAD_REQUEST_400)
64 })
65
66 it('Should succeed with a good filter', async function () {
67 await testEndpoints(server, server.accessToken, 'local', HttpStatusCode.OK_200)
68 })
69
70 it('Should fail to list all-local/all with a simple user', async function () {
71 await testEndpoints(server, userAccessToken, 'all-local', HttpStatusCode.UNAUTHORIZED_401)
72 await testEndpoints(server, userAccessToken, 'all', HttpStatusCode.UNAUTHORIZED_401)
73 })
74
75 it('Should succeed to list all-local/all with a moderator', async function () {
76 await testEndpoints(server, moderatorAccessToken, 'all-local', HttpStatusCode.OK_200)
77 await testEndpoints(server, moderatorAccessToken, 'all', HttpStatusCode.OK_200)
78 })
79
80 it('Should succeed to list all-local/all with an admin', async function () {
81 await testEndpoints(server, server.accessToken, 'all-local', HttpStatusCode.OK_200)
82 await testEndpoints(server, server.accessToken, 'all', HttpStatusCode.OK_200)
83 })
84
85 // Because we cannot authenticate the user on the RSS endpoint
86 it('Should fail on the feeds endpoint with the all-local/all filter', async function () {
87 for (const filter of [ 'all', 'all-local' ]) {
88 await makeGetRequest({
89 url: server.url,
90 path: '/feeds/videos.json',
91 expectedStatus: HttpStatusCode.UNAUTHORIZED_401,
92 query: {
93 filter
94 }
95 })
96 }
97 })
98
99 it('Should succeed on the feeds endpoint with the local filter', async function () {
100 await makeGetRequest({
101 url: server.url,
102 path: '/feeds/videos.json',
103 expectedStatus: HttpStatusCode.OK_200,
104 query: {
105 filter: 'local'
106 }
107 })
108 })
109 })
110
111 after(async function () {
112 await cleanupTests([ server ])
113 })
114})
diff --git a/server/tests/api/videos/index.ts b/server/tests/api/videos/index.ts
index 5c07f8926..c9c678e9d 100644
--- a/server/tests/api/videos/index.ts
+++ b/server/tests/api/videos/index.ts
@@ -15,7 +15,7 @@ import './video-playlist-thumbnails'
15import './video-privacy' 15import './video-privacy'
16import './video-schedule-update' 16import './video-schedule-update'
17import './video-transcoder' 17import './video-transcoder'
18import './videos-filter' 18import './videos-common-filters'
19import './videos-history' 19import './videos-history'
20import './videos-overview' 20import './videos-overview'
21import './videos-views-cleaner' 21import './videos-views-cleaner'
diff --git a/server/tests/api/videos/multiple-servers.ts b/server/tests/api/videos/multiple-servers.ts
index df9deb1e1..9c255c1c5 100644
--- a/server/tests/api/videos/multiple-servers.ts
+++ b/server/tests/api/videos/multiple-servers.ts
@@ -349,7 +349,7 @@ describe('Test multiple servers', function () {
349 349
350 describe('It should list local videos', function () { 350 describe('It should list local videos', function () {
351 it('Should list only local videos on server 1', async function () { 351 it('Should list only local videos on server 1', async function () {
352 const { data, total } = await servers[0].videos.list({ filter: 'local' }) 352 const { data, total } = await servers[0].videos.list({ isLocal: true })
353 353
354 expect(total).to.equal(1) 354 expect(total).to.equal(1)
355 expect(data).to.be.an('array') 355 expect(data).to.be.an('array')
@@ -358,7 +358,7 @@ describe('Test multiple servers', function () {
358 }) 358 })
359 359
360 it('Should list only local videos on server 2', async function () { 360 it('Should list only local videos on server 2', async function () {
361 const { data, total } = await servers[1].videos.list({ filter: 'local' }) 361 const { data, total } = await servers[1].videos.list({ isLocal: true })
362 362
363 expect(total).to.equal(1) 363 expect(total).to.equal(1)
364 expect(data).to.be.an('array') 364 expect(data).to.be.an('array')
@@ -367,7 +367,7 @@ describe('Test multiple servers', function () {
367 }) 367 })
368 368
369 it('Should list only local videos on server 3', async function () { 369 it('Should list only local videos on server 3', async function () {
370 const { data, total } = await servers[2].videos.list({ filter: 'local' }) 370 const { data, total } = await servers[2].videos.list({ isLocal: true })
371 371
372 expect(total).to.equal(2) 372 expect(total).to.equal(2)
373 expect(data).to.be.an('array') 373 expect(data).to.be.an('array')
diff --git a/server/tests/api/videos/single-server.ts b/server/tests/api/videos/single-server.ts
index 29dac6ec1..a0e4a156c 100644
--- a/server/tests/api/videos/single-server.ts
+++ b/server/tests/api/videos/single-server.ts
@@ -354,19 +354,6 @@ describe('Test a single server', function () {
354 await server.videos.update({ id: videoId, attributes }) 354 await server.videos.update({ id: videoId, attributes })
355 }) 355 })
356 356
357 it('Should filter by tags and category', async function () {
358 {
359 const { data, total } = await server.videos.list({ tagsAllOf: [ 'tagup1', 'tagup2' ], categoryOneOf: [ 4 ] })
360 expect(total).to.equal(1)
361 expect(data[0].name).to.equal('my super video updated')
362 }
363
364 {
365 const { total } = await server.videos.list({ tagsAllOf: [ 'tagup1', 'tagup2' ], categoryOneOf: [ 3 ] })
366 expect(total).to.equal(0)
367 }
368 })
369
370 it('Should have the video updated', async function () { 357 it('Should have the video updated', async function () {
371 this.timeout(60000) 358 this.timeout(60000)
372 359
diff --git a/server/tests/api/videos/videos-common-filters.ts b/server/tests/api/videos/videos-common-filters.ts
new file mode 100644
index 000000000..eb2d2ab50
--- /dev/null
+++ b/server/tests/api/videos/videos-common-filters.ts
@@ -0,0 +1,403 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import 'mocha'
4import { expect } from 'chai'
5import { pick } from '@shared/core-utils'
6import {
7 cleanupTests,
8 createMultipleServers,
9 doubleFollow,
10 makeGetRequest,
11 PeerTubeServer,
12 setAccessTokensToServers,
13 setDefaultVideoChannel,
14 waitJobs
15} from '@shared/extra-utils'
16import { HttpStatusCode, UserRole, Video, VideoInclude, VideoPrivacy } from '@shared/models'
17
18describe('Test videos filter', function () {
19 let servers: PeerTubeServer[]
20 let paths: string[]
21 let remotePaths: string[]
22
23 // ---------------------------------------------------------------
24
25 before(async function () {
26 this.timeout(160000)
27
28 servers = await createMultipleServers(2)
29
30 await setAccessTokensToServers(servers)
31 await setDefaultVideoChannel(servers)
32
33 for (const server of servers) {
34 const moderator = { username: 'moderator', password: 'my super password' }
35 await server.users.create({ username: moderator.username, password: moderator.password, role: UserRole.MODERATOR })
36 server['moderatorAccessToken'] = await server.login.getAccessToken(moderator)
37
38 await server.videos.upload({ attributes: { name: 'public ' + server.serverNumber } })
39
40 {
41 const attributes = { name: 'unlisted ' + server.serverNumber, privacy: VideoPrivacy.UNLISTED }
42 await server.videos.upload({ attributes })
43 }
44
45 {
46 const attributes = { name: 'private ' + server.serverNumber, privacy: VideoPrivacy.PRIVATE }
47 await server.videos.upload({ attributes })
48 }
49 }
50
51 await doubleFollow(servers[0], servers[1])
52
53 paths = [
54 `/api/v1/video-channels/root_channel/videos`,
55 `/api/v1/accounts/root/videos`,
56 '/api/v1/videos',
57 '/api/v1/search/videos'
58 ]
59
60 remotePaths = [
61 `/api/v1/video-channels/root_channel@${servers[1].host}/videos`,
62 `/api/v1/accounts/root@${servers[1].host}/videos`,
63 '/api/v1/videos',
64 '/api/v1/search/videos'
65 ]
66 })
67
68 describe('Check deprecated videos filter', function () {
69
70 async function getVideosNames (server: PeerTubeServer, token: string, filter: string, expectedStatus = HttpStatusCode.OK_200) {
71 const videosResults: Video[][] = []
72
73 for (const path of paths) {
74 const res = await makeGetRequest({
75 url: server.url,
76 path,
77 token,
78 query: {
79 sort: 'createdAt',
80 filter
81 },
82 expectedStatus
83 })
84
85 videosResults.push(res.body.data.map(v => v.name))
86 }
87
88 return videosResults
89 }
90
91 it('Should display local videos', async function () {
92 for (const server of servers) {
93 const namesResults = await getVideosNames(server, server.accessToken, 'local')
94 for (const names of namesResults) {
95 expect(names).to.have.lengthOf(1)
96 expect(names[0]).to.equal('public ' + server.serverNumber)
97 }
98 }
99 })
100
101 it('Should display all local videos by the admin or the moderator', async function () {
102 for (const server of servers) {
103 for (const token of [ server.accessToken, server['moderatorAccessToken'] ]) {
104
105 const namesResults = await getVideosNames(server, token, 'all-local')
106 for (const names of namesResults) {
107 expect(names).to.have.lengthOf(3)
108
109 expect(names[0]).to.equal('public ' + server.serverNumber)
110 expect(names[1]).to.equal('unlisted ' + server.serverNumber)
111 expect(names[2]).to.equal('private ' + server.serverNumber)
112 }
113 }
114 }
115 })
116
117 it('Should display all videos by the admin or the moderator', async function () {
118 for (const server of servers) {
119 for (const token of [ server.accessToken, server['moderatorAccessToken'] ]) {
120
121 const [ channelVideos, accountVideos, videos, searchVideos ] = await getVideosNames(server, token, 'all')
122 expect(channelVideos).to.have.lengthOf(3)
123 expect(accountVideos).to.have.lengthOf(3)
124
125 expect(videos).to.have.lengthOf(5)
126 expect(searchVideos).to.have.lengthOf(5)
127 }
128 }
129 })
130 })
131
132 describe('Check videos filters', function () {
133
134 async function listVideos (options: {
135 server: PeerTubeServer
136 path: string
137 isLocal?: boolean
138 include?: VideoInclude
139 category?: number
140 tagsAllOf?: string[]
141 token?: string
142 expectedStatus?: HttpStatusCode
143 }) {
144 const res = await makeGetRequest({
145 url: options.server.url,
146 path: options.path,
147 token: options.token ?? options.server.accessToken,
148 query: {
149 ...pick(options, [ 'isLocal', 'include', 'category', 'tagsAllOf' ]),
150
151 sort: 'createdAt'
152 },
153 expectedStatus: options.expectedStatus ?? HttpStatusCode.OK_200
154 })
155
156 return res.body.data as Video[]
157 }
158
159 async function getVideosNames (options: {
160 server: PeerTubeServer
161 isLocal?: boolean
162 include?: VideoInclude
163 token?: string
164 expectedStatus?: HttpStatusCode
165 }) {
166 const videosResults: string[][] = []
167
168 for (const path of paths) {
169 const videos = await listVideos({ ...options, path })
170
171 videosResults.push(videos.map(v => v.name))
172 }
173
174 return videosResults
175 }
176
177 it('Should display local videos', async function () {
178 for (const server of servers) {
179 const namesResults = await getVideosNames({ server, isLocal: true })
180
181 for (const names of namesResults) {
182 expect(names).to.have.lengthOf(1)
183 expect(names[0]).to.equal('public ' + server.serverNumber)
184 }
185 }
186 })
187
188 it('Should display local videos with hidden privacy by the admin or the moderator', async function () {
189 for (const server of servers) {
190 for (const token of [ server.accessToken, server['moderatorAccessToken'] ]) {
191
192 const namesResults = await getVideosNames({
193 server,
194 token,
195 isLocal: true,
196 include: VideoInclude.HIDDEN_PRIVACY
197 })
198
199 for (const names of namesResults) {
200 expect(names).to.have.lengthOf(3)
201
202 expect(names[0]).to.equal('public ' + server.serverNumber)
203 expect(names[1]).to.equal('unlisted ' + server.serverNumber)
204 expect(names[2]).to.equal('private ' + server.serverNumber)
205 }
206 }
207 }
208 })
209
210 it('Should display all videos by the admin or the moderator', async function () {
211 for (const server of servers) {
212 for (const token of [ server.accessToken, server['moderatorAccessToken'] ]) {
213
214 const [ channelVideos, accountVideos, videos, searchVideos ] = await getVideosNames({
215 server,
216 token,
217 include: VideoInclude.HIDDEN_PRIVACY
218 })
219
220 expect(channelVideos).to.have.lengthOf(3)
221 expect(accountVideos).to.have.lengthOf(3)
222
223 expect(videos).to.have.lengthOf(5)
224 expect(searchVideos).to.have.lengthOf(5)
225 }
226 }
227 })
228
229 it('Should display only remote videos', async function () {
230 this.timeout(40000)
231
232 await servers[1].videos.upload({ attributes: { name: 'remote video' } })
233
234 await waitJobs(servers)
235
236 const finder = (videos: Video[]) => videos.find(v => v.name === 'remote video')
237
238 for (const path of remotePaths) {
239 {
240 const videos = await listVideos({ server: servers[0], path })
241 const video = finder(videos)
242 expect(video).to.exist
243 }
244
245 {
246 const videos = await listVideos({ server: servers[0], path, isLocal: false })
247 const video = finder(videos)
248 expect(video).to.exist
249 }
250
251 {
252 const videos = await listVideos({ server: servers[0], path, isLocal: true })
253 const video = finder(videos)
254 expect(video).to.not.exist
255 }
256 }
257 })
258
259 it('Should include not published videos', async function () {
260 await servers[0].config.enableLive({ allowReplay: false, transcoding: false })
261 await servers[0].live.create({ fields: { name: 'live video', channelId: servers[0].store.channel.id, privacy: VideoPrivacy.PUBLIC } })
262
263 const finder = (videos: Video[]) => videos.find(v => v.name === 'live video')
264
265 for (const path of paths) {
266 {
267 const videos = await listVideos({ server: servers[0], path })
268 const video = finder(videos)
269 expect(video).to.not.exist
270 expect(videos[0].state).to.not.exist
271 expect(videos[0].waitTranscoding).to.not.exist
272 }
273
274 {
275 const videos = await listVideos({ server: servers[0], path, include: VideoInclude.NOT_PUBLISHED_STATE })
276 const video = finder(videos)
277 expect(video).to.exist
278 expect(video.state).to.exist
279 }
280 }
281 })
282
283 it('Should include blacklisted videos', async function () {
284 const { id } = await servers[0].videos.upload({ attributes: { name: 'blacklisted' } })
285
286 await servers[0].blacklist.add({ videoId: id })
287
288 const finder = (videos: Video[]) => videos.find(v => v.name === 'blacklisted')
289
290 for (const path of paths) {
291 {
292 const videos = await listVideos({ server: servers[0], path })
293 const video = finder(videos)
294 expect(video).to.not.exist
295 expect(videos[0].blacklisted).to.not.exist
296 }
297
298 {
299 const videos = await listVideos({ server: servers[0], path, include: VideoInclude.BLACKLISTED })
300 const video = finder(videos)
301 expect(video).to.exist
302 expect(video.blacklisted).to.be.true
303 }
304 }
305 })
306
307 it('Should include videos from muted account', async function () {
308 const finder = (videos: Video[]) => videos.find(v => v.name === 'remote video')
309
310 await servers[0].blocklist.addToServerBlocklist({ account: 'root@' + servers[1].host })
311
312 for (const path of remotePaths) {
313 {
314 const videos = await listVideos({ server: servers[0], path })
315 const video = finder(videos)
316 expect(video).to.not.exist
317
318 // Some paths won't have videos
319 if (videos[0]) {
320 expect(videos[0].blockedOwner).to.not.exist
321 expect(videos[0].blockedServer).to.not.exist
322 }
323 }
324
325 {
326 const videos = await listVideos({ server: servers[0], path, include: VideoInclude.BLOCKED_OWNER })
327
328 const video = finder(videos)
329 expect(video).to.exist
330 expect(video.blockedServer).to.be.false
331 expect(video.blockedOwner).to.be.true
332 }
333 }
334
335 await servers[0].blocklist.removeFromServerBlocklist({ account: 'root@' + servers[1].host })
336 })
337
338 it('Should include videos from muted server', async function () {
339 const finder = (videos: Video[]) => videos.find(v => v.name === 'remote video')
340
341 await servers[0].blocklist.addToServerBlocklist({ server: servers[1].host })
342
343 for (const path of remotePaths) {
344 {
345 const videos = await listVideos({ server: servers[0], path })
346 const video = finder(videos)
347 expect(video).to.not.exist
348
349 // Some paths won't have videos
350 if (videos[0]) {
351 expect(videos[0].blockedOwner).to.not.exist
352 expect(videos[0].blockedServer).to.not.exist
353 }
354 }
355
356 {
357 const videos = await listVideos({ server: servers[0], path, include: VideoInclude.BLOCKED_OWNER })
358 const video = finder(videos)
359 expect(video).to.exist
360 expect(video.blockedServer).to.be.true
361 expect(video.blockedOwner).to.be.false
362 }
363 }
364
365 await servers[0].blocklist.removeFromServerBlocklist({ server: servers[1].host })
366 })
367
368 it('Should filter by tags and category', async function () {
369 await servers[0].videos.upload({ attributes: { name: 'tag filter', tags: [ 'tag1', 'tag2' ] } })
370 await servers[0].videos.upload({ attributes: { name: 'tag filter with category', tags: [ 'tag3' ], category: 4 } })
371
372 for (const path of paths) {
373 {
374
375 const videos = await listVideos({ server: servers[0], path, tagsAllOf: [ 'tag1', 'tag2' ] })
376 expect(videos).to.have.lengthOf(1)
377 expect(videos[0].name).to.equal('tag filter')
378
379 }
380
381 {
382 const videos = await listVideos({ server: servers[0], path, tagsAllOf: [ 'tag1', 'tag3' ] })
383 expect(videos).to.have.lengthOf(0)
384 }
385
386 {
387 const { data, total } = await servers[0].videos.list({ tagsAllOf: [ 'tag3' ], categoryOneOf: [ 4 ] })
388 expect(total).to.equal(1)
389 expect(data[0].name).to.equal('tag filter with category')
390 }
391
392 {
393 const { total } = await servers[0].videos.list({ tagsAllOf: [ 'tag4' ], categoryOneOf: [ 4 ] })
394 expect(total).to.equal(0)
395 }
396 }
397 })
398 })
399
400 after(async function () {
401 await cleanupTests(servers)
402 })
403})
diff --git a/server/tests/api/videos/videos-filter.ts b/server/tests/api/videos/videos-filter.ts
deleted file mode 100644
index 2306807bf..000000000
--- a/server/tests/api/videos/videos-filter.ts
+++ /dev/null
@@ -1,122 +0,0 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import 'mocha'
4import { expect } from 'chai'
5import {
6 cleanupTests,
7 createMultipleServers,
8 doubleFollow,
9 makeGetRequest,
10 PeerTubeServer,
11 setAccessTokensToServers
12} from '@shared/extra-utils'
13import { HttpStatusCode, UserRole, Video, VideoPrivacy } from '@shared/models'
14
15async function getVideosNames (server: PeerTubeServer, token: string, filter: string, expectedStatus = HttpStatusCode.OK_200) {
16 const paths = [
17 '/api/v1/video-channels/root_channel/videos',
18 '/api/v1/accounts/root/videos',
19 '/api/v1/videos',
20 '/api/v1/search/videos'
21 ]
22
23 const videosResults: Video[][] = []
24
25 for (const path of paths) {
26 const res = await makeGetRequest({
27 url: server.url,
28 path,
29 token,
30 query: {
31 sort: 'createdAt',
32 filter
33 },
34 expectedStatus
35 })
36
37 videosResults.push(res.body.data.map(v => v.name))
38 }
39
40 return videosResults
41}
42
43describe('Test videos filter', function () {
44 let servers: PeerTubeServer[]
45
46 // ---------------------------------------------------------------
47
48 before(async function () {
49 this.timeout(160000)
50
51 servers = await createMultipleServers(2)
52
53 await setAccessTokensToServers(servers)
54
55 for (const server of servers) {
56 const moderator = { username: 'moderator', password: 'my super password' }
57 await server.users.create({ username: moderator.username, password: moderator.password, role: UserRole.MODERATOR })
58 server['moderatorAccessToken'] = await server.login.getAccessToken(moderator)
59
60 await server.videos.upload({ attributes: { name: 'public ' + server.serverNumber } })
61
62 {
63 const attributes = { name: 'unlisted ' + server.serverNumber, privacy: VideoPrivacy.UNLISTED }
64 await server.videos.upload({ attributes })
65 }
66
67 {
68 const attributes = { name: 'private ' + server.serverNumber, privacy: VideoPrivacy.PRIVATE }
69 await server.videos.upload({ attributes })
70 }
71 }
72
73 await doubleFollow(servers[0], servers[1])
74 })
75
76 describe('Check videos filter', function () {
77
78 it('Should display local videos', async function () {
79 for (const server of servers) {
80 const namesResults = await getVideosNames(server, server.accessToken, 'local')
81 for (const names of namesResults) {
82 expect(names).to.have.lengthOf(1)
83 expect(names[0]).to.equal('public ' + server.serverNumber)
84 }
85 }
86 })
87
88 it('Should display all local videos by the admin or the moderator', async function () {
89 for (const server of servers) {
90 for (const token of [ server.accessToken, server['moderatorAccessToken'] ]) {
91
92 const namesResults = await getVideosNames(server, token, 'all-local')
93 for (const names of namesResults) {
94 expect(names).to.have.lengthOf(3)
95
96 expect(names[0]).to.equal('public ' + server.serverNumber)
97 expect(names[1]).to.equal('unlisted ' + server.serverNumber)
98 expect(names[2]).to.equal('private ' + server.serverNumber)
99 }
100 }
101 }
102 })
103
104 it('Should display all videos by the admin or the moderator', async function () {
105 for (const server of servers) {
106 for (const token of [ server.accessToken, server['moderatorAccessToken'] ]) {
107
108 const [ channelVideos, accountVideos, videos, searchVideos ] = await getVideosNames(server, token, 'all')
109 expect(channelVideos).to.have.lengthOf(3)
110 expect(accountVideos).to.have.lengthOf(3)
111
112 expect(videos).to.have.lengthOf(5)
113 expect(searchVideos).to.have.lengthOf(5)
114 }
115 }
116 })
117 })
118
119 after(async function () {
120 await cleanupTests(servers)
121 })
122})
diff --git a/server/types/models/account/account.ts b/server/types/models/account/account.ts
index 984841291..abe0de27b 100644
--- a/server/types/models/account/account.ts
+++ b/server/types/models/account/account.ts
@@ -84,7 +84,7 @@ export type MAccountSummary =
84 84
85export type MAccountSummaryBlocks = 85export type MAccountSummaryBlocks =
86 MAccountSummary & 86 MAccountSummary &
87 Use<'BlockedAccounts', MAccountBlocklistId[]> 87 Use<'BlockedByAccounts', MAccountBlocklistId[]>
88 88
89export type MAccountAPI = 89export type MAccountAPI =
90 MAccount & 90 MAccount &