aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models/video
diff options
context:
space:
mode:
Diffstat (limited to 'server/models/video')
-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
8 files changed, 225 insertions, 110 deletions
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)