diff options
Diffstat (limited to 'server/models')
-rw-r--r-- | server/models/account/account-video-rate.ts | 4 | ||||
-rw-r--r-- | server/models/account/account.ts | 71 | ||||
-rw-r--r-- | server/models/server/server-blocklist.ts | 1 | ||||
-rw-r--r-- | server/models/server/server.ts | 14 | ||||
-rw-r--r-- | server/models/video/video-blacklist.ts | 4 | ||||
-rw-r--r-- | server/models/video/video-channel.ts | 15 | ||||
-rw-r--r-- | server/models/video/video-format-utils.ts | 12 | ||||
-rw-r--r-- | server/models/video/video-playlist-element.ts | 103 | ||||
-rw-r--r-- | server/models/video/video-playlist.ts | 8 | ||||
-rw-r--r-- | server/models/video/video.ts | 79 |
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' | |||
9 | import { getSort, throwIfNotValid } from '../utils' | 9 | import { getSort, throwIfNotValid } from '../utils' |
10 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 10 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
11 | import { AccountVideoRate } from '../../../shared' | 11 | import { AccountVideoRate } from '../../../shared' |
12 | import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from '../video/video-channel' | 12 | import { 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' | |||
27 | import { AvatarModel } from '../avatar/avatar' | 27 | import { AvatarModel } from '../avatar/avatar' |
28 | import { VideoPlaylistModel } from '../video/video-playlist' | 28 | import { VideoPlaylistModel } from '../video/video-playlist' |
29 | import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' | 29 | import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' |
30 | import { Op, Transaction, WhereOptions } from 'sequelize' | 30 | import { FindOptions, IncludeOptions, Op, Transaction, WhereOptions } from 'sequelize' |
31 | import { AccountBlocklistModel } from './account-blocklist' | ||
32 | import { ServerBlocklistModel } from '../server/server-blocklist' | ||
31 | 33 | ||
32 | export enum ScopeNames { | 34 | export enum ScopeNames { |
33 | SUMMARY = 'SUMMARY' | 35 | SUMMARY = 'SUMMARY' |
34 | } | 36 | } |
35 | 37 | ||
38 | export 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 | |||
2 | import { isHostValid } from '../../helpers/custom-validators/servers' | 2 | import { isHostValid } from '../../helpers/custom-validators/servers' |
3 | import { ActorModel } from '../activitypub/actor' | 3 | import { ActorModel } from '../activitypub/actor' |
4 | import { throwIfNotValid } from '../utils' | 4 | import { throwIfNotValid } from '../utils' |
5 | import { AccountBlocklistModel } from '../account/account-blocklist' | ||
6 | import { 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 @@ | |||
1 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' | 1 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' |
2 | import { getSortOnModel, SortType, throwIfNotValid } from '../utils' | 2 | import { getSortOnModel, SortType, throwIfNotValid } from '../utils' |
3 | import { ScopeNames as VideoModelScopeNames, VideoModel } from './video' | 3 | import { ScopeNames as VideoModelScopeNames, VideoModel } from './video' |
4 | import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' | 4 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel' |
5 | import { isVideoBlacklistReasonValid, isVideoBlacklistTypeValid } from '../../helpers/custom-validators/video-blacklist' | 5 | import { isVideoBlacklistReasonValid, isVideoBlacklistTypeValid } from '../../helpers/custom-validators/video-blacklist' |
6 | import { VideoBlacklist, VideoBlacklistType } from '../../../shared/models/videos' | 6 | import { VideoBlacklist, VideoBlacklistType } from '../../../shared/models/videos' |
7 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' | 7 | import { 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' |
26 | import { sendDeleteActor } from '../../lib/activitypub/send' | 26 | import { sendDeleteActor } from '../../lib/activitypub/send' |
27 | import { AccountModel, ScopeNames as AccountModelScopeNames } from '../account/account' | 27 | import { AccountModel, ScopeNames as AccountModelScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account' |
28 | import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor' | 28 | import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor' |
29 | import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils' | 29 | import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils' |
30 | import { VideoModel } from './video' | 30 | import { VideoModel } from './video' |
@@ -58,6 +58,11 @@ type AvailableForListOptions = { | |||
58 | actorId: number | 58 | actorId: number |
59 | } | 59 | } |
60 | 60 | ||
61 | export 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 | } |
32 | function videoModelToFormattedJSON (video: VideoModel, options?: VideoFormattingJSONOptions): Video { | 31 | function 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' |
16 | import { VideoModel } from './video' | 16 | import { ForAPIOptions, ScopeNames as VideoScopeNames, VideoModel } from './video' |
17 | import { VideoPlaylistModel } from './video-playlist' | 17 | import { VideoPlaylistModel } from './video-playlist' |
18 | import { getSort, throwIfNotValid } from '../utils' | 18 | import { getSort, throwIfNotValid } from '../utils' |
19 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 19 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
20 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' | 20 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' |
21 | import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object' | 21 | import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object' |
22 | import * as validator from 'validator' | 22 | import * as validator from 'validator' |
23 | import { AggregateOptions, Op, Sequelize, Transaction } from 'sequelize' | 23 | import { AggregateOptions, Op, ScopeOptions, Sequelize, Transaction } from 'sequelize' |
24 | import { UserModel } from '../account/user' | ||
25 | import { VideoPlaylistElement, VideoPlaylistElementType } from '../../../shared/models/videos/playlist/video-playlist-element.model' | ||
26 | import { AccountModel } from '../account/account' | ||
27 | import { 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' |
35 | import { VideoPlaylist } from '../../../shared/models/videos/playlist/video-playlist.model' | 35 | import { VideoPlaylist } from '../../../shared/models/videos/playlist/video-playlist.model' |
36 | import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account' | 36 | import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account' |
37 | import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' | 37 | import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' |
38 | import { join } from 'path' | 38 | import { join } from 'path' |
39 | import { VideoPlaylistElementModel } from './video-playlist-element' | 39 | import { 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' |
92 | import { TagModel } from './tag' | 92 | import { TagModel } from './tag' |
93 | import { VideoAbuseModel } from './video-abuse' | 93 | import { VideoAbuseModel } from './video-abuse' |
94 | import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' | 94 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel' |
95 | import { VideoCommentModel } from './video-comment' | 95 | import { VideoCommentModel } from './video-comment' |
96 | import { VideoFileModel } from './video-file' | 96 | import { VideoFileModel } from './video-file' |
97 | import { VideoShareModel } from './video-share' | 97 | import { 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 | ||
199 | type ForAPIOptions = { | 200 | export 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 | ||
207 | type AvailableForListIDsOptions = { | 210 | export 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 | ||