aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models
diff options
context:
space:
mode:
Diffstat (limited to 'server/models')
-rw-r--r--server/models/account/account-video-rate.ts4
-rw-r--r--server/models/account/account.ts71
-rw-r--r--server/models/server/server-blocklist.ts1
-rw-r--r--server/models/server/server.ts14
-rw-r--r--server/models/video/video-blacklist.ts4
-rw-r--r--server/models/video/video-channel.ts15
-rw-r--r--server/models/video/video-format-utils.ts12
-rw-r--r--server/models/video/video-playlist-element.ts103
-rw-r--r--server/models/video/video-playlist.ts8
-rw-r--r--server/models/video/video.ts79
10 files changed, 248 insertions, 63 deletions
diff --git a/server/models/account/account-video-rate.ts b/server/models/account/account-video-rate.ts
index 59f586b54..85af9e378 100644
--- a/server/models/account/account-video-rate.ts
+++ b/server/models/account/account-video-rate.ts
@@ -9,7 +9,7 @@ import { ActorModel } from '../activitypub/actor'
9import { getSort, throwIfNotValid } from '../utils' 9import { getSort, throwIfNotValid } from '../utils'
10import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 10import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
11import { AccountVideoRate } from '../../../shared' 11import { AccountVideoRate } from '../../../shared'
12import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from '../video/video-channel' 12import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from '../video/video-channel'
13 13
14/* 14/*
15 Account rates per video. 15 Account rates per video.
@@ -109,7 +109,7 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
109 required: true, 109 required: true,
110 include: [ 110 include: [
111 { 111 {
112 model: VideoChannelModel.scope({ method: [VideoChannelScopeNames.SUMMARY, true] }), 112 model: VideoChannelModel.scope({ method: [VideoChannelScopeNames.SUMMARY, { withAccount: true } as SummaryOptions ] }),
113 required: true 113 required: true
114 } 114 }
115 ] 115 ]
diff --git a/server/models/account/account.ts b/server/models/account/account.ts
index 09cada096..28014946f 100644
--- a/server/models/account/account.ts
+++ b/server/models/account/account.ts
@@ -27,12 +27,19 @@ import { UserModel } from './user'
27import { AvatarModel } from '../avatar/avatar' 27import { AvatarModel } from '../avatar/avatar'
28import { VideoPlaylistModel } from '../video/video-playlist' 28import { VideoPlaylistModel } from '../video/video-playlist'
29import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' 29import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
30import { Op, Transaction, WhereOptions } from 'sequelize' 30import { FindOptions, IncludeOptions, Op, Transaction, WhereOptions } from 'sequelize'
31import { AccountBlocklistModel } from './account-blocklist'
32import { ServerBlocklistModel } from '../server/server-blocklist'
31 33
32export enum ScopeNames { 34export enum ScopeNames {
33 SUMMARY = 'SUMMARY' 35 SUMMARY = 'SUMMARY'
34} 36}
35 37
38export type SummaryOptions = {
39 whereActor?: WhereOptions
40 withAccountBlockerIds?: number[]
41}
42
36@DefaultScope(() => ({ 43@DefaultScope(() => ({
37 include: [ 44 include: [
38 { 45 {
@@ -42,8 +49,16 @@ export enum ScopeNames {
42 ] 49 ]
43})) 50}))
44@Scopes(() => ({ 51@Scopes(() => ({
45 [ ScopeNames.SUMMARY ]: (whereActor?: WhereOptions) => { 52 [ ScopeNames.SUMMARY ]: (options: SummaryOptions = {}) => {
46 return { 53 const whereActor = options.whereActor || undefined
54
55 const serverInclude: IncludeOptions = {
56 attributes: [ 'host' ],
57 model: ServerModel.unscoped(),
58 required: false
59 }
60
61 const query: FindOptions = {
47 attributes: [ 'id', 'name' ], 62 attributes: [ 'id', 'name' ],
48 include: [ 63 include: [
49 { 64 {
@@ -52,11 +67,8 @@ export enum ScopeNames {
52 required: true, 67 required: true,
53 where: whereActor, 68 where: whereActor,
54 include: [ 69 include: [
55 { 70 serverInclude,
56 attributes: [ 'host' ], 71
57 model: ServerModel.unscoped(),
58 required: false
59 },
60 { 72 {
61 model: AvatarModel.unscoped(), 73 model: AvatarModel.unscoped(),
62 required: false 74 required: false
@@ -65,6 +77,35 @@ export enum ScopeNames {
65 } 77 }
66 ] 78 ]
67 } 79 }
80
81 if (options.withAccountBlockerIds) {
82 query.include.push({
83 attributes: [ 'id' ],
84 model: AccountBlocklistModel.unscoped(),
85 as: 'BlockedAccounts',
86 required: false,
87 where: {
88 accountId: {
89 [Op.in]: options.withAccountBlockerIds
90 }
91 }
92 })
93
94 serverInclude.include = [
95 {
96 attributes: [ 'id' ],
97 model: ServerBlocklistModel.unscoped(),
98 required: false,
99 where: {
100 accountId: {
101 [Op.in]: options.withAccountBlockerIds
102 }
103 }
104 }
105 ]
106 }
107
108 return query
68 } 109 }
69})) 110}))
70@Table({ 111@Table({
@@ -163,6 +204,16 @@ export class AccountModel extends Model<AccountModel> {
163 }) 204 })
164 VideoComments: VideoCommentModel[] 205 VideoComments: VideoCommentModel[]
165 206
207 @HasMany(() => AccountBlocklistModel, {
208 foreignKey: {
209 name: 'targetAccountId',
210 allowNull: false
211 },
212 as: 'BlockedAccounts',
213 onDelete: 'CASCADE'
214 })
215 BlockedAccounts: AccountBlocklistModel[]
216
166 @BeforeDestroy 217 @BeforeDestroy
167 static async sendDeleteIfOwned (instance: AccountModel, options) { 218 static async sendDeleteIfOwned (instance: AccountModel, options) {
168 if (!instance.Actor) { 219 if (!instance.Actor) {
@@ -343,4 +394,8 @@ export class AccountModel extends Model<AccountModel> {
343 getDisplayName () { 394 getDisplayName () {
344 return this.name 395 return this.name
345 } 396 }
397
398 isBlocked () {
399 return this.BlockedAccounts && this.BlockedAccounts.length !== 0
400 }
346} 401}
diff --git a/server/models/server/server-blocklist.ts b/server/models/server/server-blocklist.ts
index 92c01f642..5138b0f76 100644
--- a/server/models/server/server-blocklist.ts
+++ b/server/models/server/server-blocklist.ts
@@ -67,7 +67,6 @@ export class ServerBlocklistModel extends Model<ServerBlocklistModel> {
67 67
68 @BelongsTo(() => ServerModel, { 68 @BelongsTo(() => ServerModel, {
69 foreignKey: { 69 foreignKey: {
70 name: 'targetServerId',
71 allowNull: false 70 allowNull: false
72 }, 71 },
73 onDelete: 'CASCADE' 72 onDelete: 'CASCADE'
diff --git a/server/models/server/server.ts b/server/models/server/server.ts
index 300d70938..1d211f1e0 100644
--- a/server/models/server/server.ts
+++ b/server/models/server/server.ts
@@ -2,6 +2,8 @@ import { AllowNull, Column, CreatedAt, Default, HasMany, Is, Model, Table, Updat
2import { isHostValid } from '../../helpers/custom-validators/servers' 2import { isHostValid } from '../../helpers/custom-validators/servers'
3import { ActorModel } from '../activitypub/actor' 3import { ActorModel } from '../activitypub/actor'
4import { throwIfNotValid } from '../utils' 4import { throwIfNotValid } from '../utils'
5import { AccountBlocklistModel } from '../account/account-blocklist'
6import { ServerBlocklistModel } from './server-blocklist'
5 7
6@Table({ 8@Table({
7 tableName: 'server', 9 tableName: 'server',
@@ -40,6 +42,14 @@ export class ServerModel extends Model<ServerModel> {
40 }) 42 })
41 Actors: ActorModel[] 43 Actors: ActorModel[]
42 44
45 @HasMany(() => ServerBlocklistModel, {
46 foreignKey: {
47 allowNull: false
48 },
49 onDelete: 'CASCADE'
50 })
51 BlockedByAccounts: ServerBlocklistModel[]
52
43 static loadByHost (host: string) { 53 static loadByHost (host: string) {
44 const query = { 54 const query = {
45 where: { 55 where: {
@@ -50,6 +60,10 @@ export class ServerModel extends Model<ServerModel> {
50 return ServerModel.findOne(query) 60 return ServerModel.findOne(query)
51 } 61 }
52 62
63 isBlocked () {
64 return this.BlockedByAccounts && this.BlockedByAccounts.length !== 0
65 }
66
53 toFormattedJSON () { 67 toFormattedJSON () {
54 return { 68 return {
55 host: this.host 69 host: this.host
diff --git a/server/models/video/video-blacklist.ts b/server/models/video/video-blacklist.ts
index baef1d6ce..22d949da0 100644
--- a/server/models/video/video-blacklist.ts
+++ b/server/models/video/video-blacklist.ts
@@ -1,7 +1,7 @@
1import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' 1import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
2import { getSortOnModel, SortType, throwIfNotValid } from '../utils' 2import { getSortOnModel, SortType, throwIfNotValid } from '../utils'
3import { ScopeNames as VideoModelScopeNames, VideoModel } from './video' 3import { ScopeNames as VideoModelScopeNames, VideoModel } from './video'
4import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' 4import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel'
5import { isVideoBlacklistReasonValid, isVideoBlacklistTypeValid } from '../../helpers/custom-validators/video-blacklist' 5import { isVideoBlacklistReasonValid, isVideoBlacklistTypeValid } from '../../helpers/custom-validators/video-blacklist'
6import { VideoBlacklist, VideoBlacklistType } from '../../../shared/models/videos' 6import { VideoBlacklist, VideoBlacklistType } from '../../../shared/models/videos'
7import { CONSTRAINTS_FIELDS } from '../../initializers/constants' 7import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
@@ -71,7 +71,7 @@ export class VideoBlacklistModel extends Model<VideoBlacklistModel> {
71 required: true, 71 required: true,
72 include: [ 72 include: [
73 { 73 {
74 model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, true ] }), 74 model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: true } as SummaryOptions ] }),
75 required: true 75 required: true
76 }, 76 },
77 { 77 {
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts
index b0b261c88..6241a75a3 100644
--- a/server/models/video/video-channel.ts
+++ b/server/models/video/video-channel.ts
@@ -24,7 +24,7 @@ import {
24 isVideoChannelSupportValid 24 isVideoChannelSupportValid
25} from '../../helpers/custom-validators/video-channels' 25} from '../../helpers/custom-validators/video-channels'
26import { sendDeleteActor } from '../../lib/activitypub/send' 26import { sendDeleteActor } from '../../lib/activitypub/send'
27import { AccountModel, ScopeNames as AccountModelScopeNames } from '../account/account' 27import { AccountModel, ScopeNames as AccountModelScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account'
28import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor' 28import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor'
29import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils' 29import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils'
30import { VideoModel } from './video' 30import { VideoModel } from './video'
@@ -58,6 +58,11 @@ type AvailableForListOptions = {
58 actorId: number 58 actorId: number
59} 59}
60 60
61export type SummaryOptions = {
62 withAccount?: boolean // Default: false
63 withAccountBlockerIds?: number[]
64}
65
61@DefaultScope(() => ({ 66@DefaultScope(() => ({
62 include: [ 67 include: [
63 { 68 {
@@ -67,7 +72,7 @@ type AvailableForListOptions = {
67 ] 72 ]
68})) 73}))
69@Scopes(() => ({ 74@Scopes(() => ({
70 [ScopeNames.SUMMARY]: (withAccount = false) => { 75 [ScopeNames.SUMMARY]: (options: SummaryOptions = {}) => {
71 const base: FindOptions = { 76 const base: FindOptions = {
72 attributes: [ 'name', 'description', 'id', 'actorId' ], 77 attributes: [ 'name', 'description', 'id', 'actorId' ],
73 include: [ 78 include: [
@@ -90,9 +95,11 @@ type AvailableForListOptions = {
90 ] 95 ]
91 } 96 }
92 97
93 if (withAccount === true) { 98 if (options.withAccount === true) {
94 base.include.push({ 99 base.include.push({
95 model: AccountModel.scope(AccountModelScopeNames.SUMMARY), 100 model: AccountModel.scope({
101 method: [ AccountModelScopeNames.SUMMARY, { withAccountBlockerIds: options.withAccountBlockerIds } as AccountSummaryOptions ]
102 }),
96 required: true 103 required: true
97 }) 104 })
98 } 105 }
diff --git a/server/models/video/video-format-utils.ts b/server/models/video/video-format-utils.ts
index b947eb16f..284539def 100644
--- a/server/models/video/video-format-utils.ts
+++ b/server/models/video/video-format-utils.ts
@@ -26,7 +26,6 @@ export type VideoFormattingJSONOptions = {
26 waitTranscoding?: boolean, 26 waitTranscoding?: boolean,
27 scheduledUpdate?: boolean, 27 scheduledUpdate?: boolean,
28 blacklistInfo?: boolean 28 blacklistInfo?: boolean
29 playlistInfo?: boolean
30 } 29 }
31} 30}
32function videoModelToFormattedJSON (video: VideoModel, options?: VideoFormattingJSONOptions): Video { 31function videoModelToFormattedJSON (video: VideoModel, options?: VideoFormattingJSONOptions): Video {
@@ -98,17 +97,6 @@ function videoModelToFormattedJSON (video: VideoModel, options?: VideoFormatting
98 videoObject.blacklisted = !!video.VideoBlacklist 97 videoObject.blacklisted = !!video.VideoBlacklist
99 videoObject.blacklistedReason = video.VideoBlacklist ? video.VideoBlacklist.reason : null 98 videoObject.blacklistedReason = video.VideoBlacklist ? video.VideoBlacklist.reason : null
100 } 99 }
101
102 if (options.additionalAttributes.playlistInfo === true) {
103 // We filtered on a specific videoId/videoPlaylistId, that is unique
104 const playlistElement = video.VideoPlaylistElements[0]
105
106 videoObject.playlistElement = {
107 position: playlistElement.position,
108 startTimestamp: playlistElement.startTimestamp,
109 stopTimestamp: playlistElement.stopTimestamp
110 }
111 }
112 } 100 }
113 101
114 return videoObject 102 return videoObject
diff --git a/server/models/video/video-playlist-element.ts b/server/models/video/video-playlist-element.ts
index eeb3d6bbd..bed6f8eaf 100644
--- a/server/models/video/video-playlist-element.ts
+++ b/server/models/video/video-playlist-element.ts
@@ -13,14 +13,18 @@ import {
13 Table, 13 Table,
14 UpdatedAt 14 UpdatedAt
15} from 'sequelize-typescript' 15} from 'sequelize-typescript'
16import { VideoModel } from './video' 16import { ForAPIOptions, ScopeNames as VideoScopeNames, VideoModel } from './video'
17import { VideoPlaylistModel } from './video-playlist' 17import { VideoPlaylistModel } from './video-playlist'
18import { getSort, throwIfNotValid } from '../utils' 18import { getSort, throwIfNotValid } from '../utils'
19import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 19import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
20import { CONSTRAINTS_FIELDS } from '../../initializers/constants' 20import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
21import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object' 21import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object'
22import * as validator from 'validator' 22import * as validator from 'validator'
23import { AggregateOptions, Op, Sequelize, Transaction } from 'sequelize' 23import { AggregateOptions, Op, ScopeOptions, Sequelize, Transaction } from 'sequelize'
24import { UserModel } from '../account/user'
25import { VideoPlaylistElement, VideoPlaylistElementType } from '../../../shared/models/videos/playlist/video-playlist-element.model'
26import { AccountModel } from '../account/account'
27import { VideoPrivacy } from '../../../shared/models/videos'
24 28
25@Table({ 29@Table({
26 tableName: 'videoPlaylistElement', 30 tableName: 'videoPlaylistElement',
@@ -90,9 +94,9 @@ export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel>
90 94
91 @BelongsTo(() => VideoModel, { 95 @BelongsTo(() => VideoModel, {
92 foreignKey: { 96 foreignKey: {
93 allowNull: false 97 allowNull: true
94 }, 98 },
95 onDelete: 'CASCADE' 99 onDelete: 'set null'
96 }) 100 })
97 Video: VideoModel 101 Video: VideoModel
98 102
@@ -107,6 +111,57 @@ export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel>
107 return VideoPlaylistElementModel.destroy(query) 111 return VideoPlaylistElementModel.destroy(query)
108 } 112 }
109 113
114 static listForApi (options: {
115 start: number,
116 count: number,
117 videoPlaylistId: number,
118 serverAccount: AccountModel,
119 user?: UserModel
120 }) {
121 const accountIds = [ options.serverAccount.id ]
122 const videoScope: (ScopeOptions | string)[] = [
123 VideoScopeNames.WITH_BLACKLISTED
124 ]
125
126 if (options.user) {
127 accountIds.push(options.user.Account.id)
128 videoScope.push({ method: [ VideoScopeNames.WITH_USER_HISTORY, options.user.id ] })
129 }
130
131 const forApiOptions: ForAPIOptions = { withAccountBlockerIds: accountIds }
132 videoScope.push({
133 method: [
134 VideoScopeNames.FOR_API, forApiOptions
135 ]
136 })
137
138 const findQuery = {
139 offset: options.start,
140 limit: options.count,
141 order: getSort('position'),
142 where: {
143 videoPlaylistId: options.videoPlaylistId
144 },
145 include: [
146 {
147 model: VideoModel.scope(videoScope),
148 required: false
149 }
150 ]
151 }
152
153 const countQuery = {
154 where: {
155 videoPlaylistId: options.videoPlaylistId
156 }
157 }
158
159 return Promise.all([
160 VideoPlaylistElementModel.count(countQuery),
161 VideoPlaylistElementModel.findAll(findQuery)
162 ]).then(([ total, data ]) => ({ total, data }))
163 }
164
110 static loadByPlaylistAndVideo (videoPlaylistId: number, videoId: number) { 165 static loadByPlaylistAndVideo (videoPlaylistId: number, videoId: number) {
111 const query = { 166 const query = {
112 where: { 167 where: {
@@ -118,6 +173,10 @@ export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel>
118 return VideoPlaylistElementModel.findOne(query) 173 return VideoPlaylistElementModel.findOne(query)
119 } 174 }
120 175
176 static loadById (playlistElementId: number) {
177 return VideoPlaylistElementModel.findByPk(playlistElementId)
178 }
179
121 static loadByPlaylistAndVideoForAP (playlistId: number | string, videoId: number | string) { 180 static loadByPlaylistAndVideoForAP (playlistId: number | string, videoId: number | string) {
122 const playlistWhere = validator.isUUID('' + playlistId) ? { uuid: playlistId } : { id: playlistId } 181 const playlistWhere = validator.isUUID('' + playlistId) ? { uuid: playlistId } : { id: playlistId }
123 const videoWhere = validator.isUUID('' + videoId) ? { uuid: videoId } : { id: videoId } 182 const videoWhere = validator.isUUID('' + videoId) ? { uuid: videoId } : { id: videoId }
@@ -213,6 +272,42 @@ export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel>
213 return VideoPlaylistElementModel.increment({ position: by }, query) 272 return VideoPlaylistElementModel.increment({ position: by }, query)
214 } 273 }
215 274
275 getType (displayNSFW?: boolean, accountId?: number) {
276 const video = this.Video
277
278 if (!video) return VideoPlaylistElementType.DELETED
279
280 // Owned video, don't filter it
281 if (accountId && video.VideoChannel.Account.id === accountId) return VideoPlaylistElementType.REGULAR
282
283 if (video.privacy === VideoPrivacy.PRIVATE) return VideoPlaylistElementType.PRIVATE
284
285 if (video.isBlacklisted() || video.isBlocked()) return VideoPlaylistElementType.UNAVAILABLE
286 if (video.nsfw === true && displayNSFW === false) return VideoPlaylistElementType.UNAVAILABLE
287
288 return VideoPlaylistElementType.REGULAR
289 }
290
291 getVideoElement (displayNSFW?: boolean, accountId?: number) {
292 if (!this.Video) return null
293 if (this.getType(displayNSFW, accountId) !== VideoPlaylistElementType.REGULAR) return null
294
295 return this.Video.toFormattedJSON()
296 }
297
298 toFormattedJSON (options: { displayNSFW?: boolean, accountId?: number } = {}): VideoPlaylistElement {
299 return {
300 id: this.id,
301 position: this.position,
302 startTimestamp: this.startTimestamp,
303 stopTimestamp: this.stopTimestamp,
304
305 type: this.getType(options.displayNSFW, options.accountId),
306
307 video: this.getVideoElement(options.displayNSFW, options.accountId)
308 }
309 }
310
216 toActivityPubObject (): PlaylistElementObject { 311 toActivityPubObject (): PlaylistElementObject {
217 const base: PlaylistElementObject = { 312 const base: PlaylistElementObject = {
218 id: this.url, 313 id: this.url,
diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts
index 63b4a0715..61ff78bd2 100644
--- a/server/models/video/video-playlist.ts
+++ b/server/models/video/video-playlist.ts
@@ -33,7 +33,7 @@ import {
33 WEBSERVER 33 WEBSERVER
34} from '../../initializers/constants' 34} from '../../initializers/constants'
35import { VideoPlaylist } from '../../../shared/models/videos/playlist/video-playlist.model' 35import { VideoPlaylist } from '../../../shared/models/videos/playlist/video-playlist.model'
36import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account' 36import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account'
37import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' 37import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel'
38import { join } from 'path' 38import { join } from 'path'
39import { VideoPlaylistElementModel } from './video-playlist-element' 39import { VideoPlaylistElementModel } from './video-playlist-element'
@@ -115,7 +115,7 @@ type AvailableForListOptions = {
115 [ ScopeNames.AVAILABLE_FOR_LIST ]: (options: AvailableForListOptions) => { 115 [ ScopeNames.AVAILABLE_FOR_LIST ]: (options: AvailableForListOptions) => {
116 // Only list local playlists OR playlists that are on an instance followed by actorId 116 // Only list local playlists OR playlists that are on an instance followed by actorId
117 const inQueryInstanceFollow = buildServerIdsFollowedBy(options.followerActorId) 117 const inQueryInstanceFollow = buildServerIdsFollowedBy(options.followerActorId)
118 const actorWhere = { 118 const whereActor = {
119 [ Op.or ]: [ 119 [ Op.or ]: [
120 { 120 {
121 serverId: null 121 serverId: null
@@ -159,7 +159,7 @@ type AvailableForListOptions = {
159 } 159 }
160 160
161 const accountScope = { 161 const accountScope = {
162 method: [ AccountScopeNames.SUMMARY, actorWhere ] 162 method: [ AccountScopeNames.SUMMARY, { whereActor } as SummaryOptions ]
163 } 163 }
164 164
165 return { 165 return {
@@ -341,7 +341,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
341 }, 341 },
342 include: [ 342 include: [
343 { 343 {
344 attributes: [ 'videoId', 'startTimestamp', 'stopTimestamp' ], 344 attributes: [ 'id', 'videoId', 'startTimestamp', 'stopTimestamp' ],
345 model: VideoPlaylistElementModel.unscoped(), 345 model: VideoPlaylistElementModel.unscoped(),
346 where: { 346 where: {
347 videoId: { 347 videoId: {
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index c7f2658ed..05d625fc1 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -91,7 +91,7 @@ import {
91} from '../utils' 91} from '../utils'
92import { TagModel } from './tag' 92import { TagModel } from './tag'
93import { VideoAbuseModel } from './video-abuse' 93import { VideoAbuseModel } from './video-abuse'
94import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' 94import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel'
95import { VideoCommentModel } from './video-comment' 95import { VideoCommentModel } from './video-comment'
96import { VideoFileModel } from './video-file' 96import { VideoFileModel } from './video-file'
97import { VideoShareModel } from './video-share' 97import { VideoShareModel } from './video-share'
@@ -190,26 +190,29 @@ export enum ScopeNames {
190 WITH_FILES = 'WITH_FILES', 190 WITH_FILES = 'WITH_FILES',
191 WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE', 191 WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE',
192 WITH_BLACKLISTED = 'WITH_BLACKLISTED', 192 WITH_BLACKLISTED = 'WITH_BLACKLISTED',
193 WITH_BLOCKLIST = 'WITH_BLOCKLIST',
193 WITH_USER_HISTORY = 'WITH_USER_HISTORY', 194 WITH_USER_HISTORY = 'WITH_USER_HISTORY',
194 WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS', 195 WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS',
195 WITH_USER_ID = 'WITH_USER_ID', 196 WITH_USER_ID = 'WITH_USER_ID',
196 WITH_THUMBNAILS = 'WITH_THUMBNAILS' 197 WITH_THUMBNAILS = 'WITH_THUMBNAILS'
197} 198}
198 199
199type ForAPIOptions = { 200export type ForAPIOptions = {
200 ids: number[] 201 ids?: number[]
201 202
202 videoPlaylistId?: number 203 videoPlaylistId?: number
203 204
204 withFiles?: boolean 205 withFiles?: boolean
206
207 withAccountBlockerIds?: number[]
205} 208}
206 209
207type AvailableForListIDsOptions = { 210export type AvailableForListIDsOptions = {
208 serverAccountId: number 211 serverAccountId: number
209 followerActorId: number 212 followerActorId: number
210 includeLocalVideos: boolean 213 includeLocalVideos: boolean
211 214
212 withoutId?: boolean 215 attributesType?: 'none' | 'id' | 'all'
213 216
214 filter?: VideoFilter 217 filter?: VideoFilter
215 categoryOneOf?: number[] 218 categoryOneOf?: number[]
@@ -236,14 +239,16 @@ type AvailableForListIDsOptions = {
236@Scopes(() => ({ 239@Scopes(() => ({
237 [ ScopeNames.FOR_API ]: (options: ForAPIOptions) => { 240 [ ScopeNames.FOR_API ]: (options: ForAPIOptions) => {
238 const query: FindOptions = { 241 const query: FindOptions = {
239 where: {
240 id: {
241 [ Op.in ]: options.ids // FIXME: sequelize ANY seems broken
242 }
243 },
244 include: [ 242 include: [
245 { 243 {
246 model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, true ] }), 244 model: VideoChannelModel.scope({
245 method: [
246 VideoChannelScopeNames.SUMMARY, {
247 withAccount: true,
248 withAccountBlockerIds: options.withAccountBlockerIds
249 } as SummaryOptions
250 ]
251 }),
247 required: true 252 required: true
248 }, 253 },
249 { 254 {
@@ -254,6 +259,14 @@ type AvailableForListIDsOptions = {
254 ] 259 ]
255 } 260 }
256 261
262 if (options.ids) {
263 query.where = {
264 id: {
265 [ Op.in ]: options.ids // FIXME: sequelize ANY seems broken
266 }
267 }
268 }
269
257 if (options.withFiles === true) { 270 if (options.withFiles === true) {
258 query.include.push({ 271 query.include.push({
259 model: VideoFileModel.unscoped(), 272 model: VideoFileModel.unscoped(),
@@ -278,10 +291,14 @@ type AvailableForListIDsOptions = {
278 291
279 const query: FindOptions = { 292 const query: FindOptions = {
280 raw: true, 293 raw: true,
281 attributes: options.withoutId === true ? [] : [ 'id' ],
282 include: [] 294 include: []
283 } 295 }
284 296
297 const attributesType = options.attributesType || 'id'
298
299 if (attributesType === 'id') query.attributes = [ 'id' ]
300 else if (attributesType === 'none') query.attributes = [ ]
301
285 whereAnd.push({ 302 whereAnd.push({
286 id: { 303 id: {
287 [ Op.notIn ]: Sequelize.literal( 304 [ Op.notIn ]: Sequelize.literal(
@@ -290,17 +307,19 @@ type AvailableForListIDsOptions = {
290 } 307 }
291 }) 308 })
292 309
293 whereAnd.push({ 310 if (options.serverAccountId) {
294 channelId: { 311 whereAnd.push({
295 [ Op.notIn ]: Sequelize.literal( 312 channelId: {
296 '(' + 313 [ Op.notIn ]: Sequelize.literal(
297 'SELECT id FROM "videoChannel" WHERE "accountId" IN (' + 314 '(' +
298 buildBlockedAccountSQL(options.serverAccountId, options.user ? options.user.Account.id : undefined) + 315 'SELECT id FROM "videoChannel" WHERE "accountId" IN (' +
299 ')' + 316 buildBlockedAccountSQL(options.serverAccountId, options.user ? options.user.Account.id : undefined) +
300 ')' 317 ')' +
301 ) 318 ')'
302 } 319 )
303 }) 320 }
321 })
322 }
304 323
305 // Only list public/published videos 324 // Only list public/published videos
306 if (!options.filter || options.filter !== 'all-local') { 325 if (!options.filter || options.filter !== 'all-local') {
@@ -528,6 +547,9 @@ type AvailableForListIDsOptions = {
528 547
529 return query 548 return query
530 }, 549 },
550 [ScopeNames.WITH_BLOCKLIST]: {
551
552 },
531 [ ScopeNames.WITH_THUMBNAILS ]: { 553 [ ScopeNames.WITH_THUMBNAILS ]: {
532 include: [ 554 include: [
533 { 555 {
@@ -845,9 +867,9 @@ export class VideoModel extends Model<VideoModel> {
845 @HasMany(() => VideoPlaylistElementModel, { 867 @HasMany(() => VideoPlaylistElementModel, {
846 foreignKey: { 868 foreignKey: {
847 name: 'videoId', 869 name: 'videoId',
848 allowNull: false 870 allowNull: true
849 }, 871 },
850 onDelete: 'cascade' 872 onDelete: 'set null'
851 }) 873 })
852 VideoPlaylistElements: VideoPlaylistElementModel[] 874 VideoPlaylistElements: VideoPlaylistElementModel[]
853 875
@@ -1586,7 +1608,7 @@ export class VideoModel extends Model<VideoModel> {
1586 serverAccountId: serverActor.Account.id, 1608 serverAccountId: serverActor.Account.id,
1587 followerActorId, 1609 followerActorId,
1588 includeLocalVideos: true, 1610 includeLocalVideos: true,
1589 withoutId: true // Don't break aggregation 1611 attributesType: 'none' // Don't break aggregation
1590 } 1612 }
1591 1613
1592 const query: FindOptions = { 1614 const query: FindOptions = {
@@ -1719,6 +1741,11 @@ export class VideoModel extends Model<VideoModel> {
1719 return !!this.VideoBlacklist 1741 return !!this.VideoBlacklist
1720 } 1742 }
1721 1743
1744 isBlocked () {
1745 return (this.VideoChannel.Account.Actor.Server && this.VideoChannel.Account.Actor.Server.isBlocked()) ||
1746 this.VideoChannel.Account.isBlocked()
1747 }
1748
1722 getOriginalFile () { 1749 getOriginalFile () {
1723 if (Array.isArray(this.VideoFiles) === false) return undefined 1750 if (Array.isArray(this.VideoFiles) === false) return undefined
1724 1751