]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/models/video/video-playlist-element.ts
Stricter models typing
[github/Chocobozzz/PeerTube.git] / server / models / video / video-playlist-element.ts
1 import { AggregateOptions, Op, ScopeOptions, Sequelize, Transaction } from 'sequelize'
2 import {
3 AllowNull,
4 BelongsTo,
5 Column,
6 CreatedAt,
7 DataType,
8 Default,
9 ForeignKey,
10 Is,
11 IsInt,
12 Min,
13 Model,
14 Table,
15 UpdatedAt
16 } from 'sequelize-typescript'
17 import validator from 'validator'
18 import { MUserAccountId } from '@server/types/models'
19 import {
20 MVideoPlaylistElement,
21 MVideoPlaylistElementAP,
22 MVideoPlaylistElementFormattable,
23 MVideoPlaylistElementVideoUrlPlaylistPrivacy,
24 MVideoPlaylistVideoThumbnail
25 } from '@server/types/models/video/video-playlist-element'
26 import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object'
27 import { VideoPrivacy } from '../../../shared/models/videos'
28 import { VideoPlaylistElement, VideoPlaylistElementType } from '../../../shared/models/videos/playlist/video-playlist-element.model'
29 import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
30 import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
31 import { AccountModel } from '../account/account'
32 import { getSort, throwIfNotValid } from '../utils'
33 import { ForAPIOptions, ScopeNames as VideoScopeNames, VideoModel } from './video'
34 import { VideoPlaylistModel } from './video-playlist'
35 import { AttributesOnly } from '@shared/core-utils'
36
37 @Table({
38 tableName: 'videoPlaylistElement',
39 indexes: [
40 {
41 fields: [ 'videoPlaylistId' ]
42 },
43 {
44 fields: [ 'videoId' ]
45 },
46 {
47 fields: [ 'url' ],
48 unique: true
49 }
50 ]
51 })
52 export class VideoPlaylistElementModel extends Model<Partial<AttributesOnly<VideoPlaylistElementModel>>> {
53 @CreatedAt
54 createdAt: Date
55
56 @UpdatedAt
57 updatedAt: Date
58
59 @AllowNull(true)
60 @Is('VideoPlaylistUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url', true))
61 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_PLAYLISTS.URL.max))
62 url: string
63
64 @AllowNull(false)
65 @Default(1)
66 @IsInt
67 @Min(1)
68 @Column
69 position: number
70
71 @AllowNull(true)
72 @IsInt
73 @Min(0)
74 @Column
75 startTimestamp: number
76
77 @AllowNull(true)
78 @IsInt
79 @Min(0)
80 @Column
81 stopTimestamp: number
82
83 @ForeignKey(() => VideoPlaylistModel)
84 @Column
85 videoPlaylistId: number
86
87 @BelongsTo(() => VideoPlaylistModel, {
88 foreignKey: {
89 allowNull: false
90 },
91 onDelete: 'CASCADE'
92 })
93 VideoPlaylist: VideoPlaylistModel
94
95 @ForeignKey(() => VideoModel)
96 @Column
97 videoId: number
98
99 @BelongsTo(() => VideoModel, {
100 foreignKey: {
101 allowNull: true
102 },
103 onDelete: 'set null'
104 })
105 Video: VideoModel
106
107 static deleteAllOf (videoPlaylistId: number, transaction?: Transaction) {
108 const query = {
109 where: {
110 videoPlaylistId
111 },
112 transaction
113 }
114
115 return VideoPlaylistElementModel.destroy(query)
116 }
117
118 static listForApi (options: {
119 start: number
120 count: number
121 videoPlaylistId: number
122 serverAccount: AccountModel
123 user?: MUserAccountId
124 }) {
125 const accountIds = [ options.serverAccount.id ]
126 const videoScope: (ScopeOptions | string)[] = [
127 VideoScopeNames.WITH_BLACKLISTED
128 ]
129
130 if (options.user) {
131 accountIds.push(options.user.Account.id)
132 videoScope.push({ method: [ VideoScopeNames.WITH_USER_HISTORY, options.user.id ] })
133 }
134
135 const forApiOptions: ForAPIOptions = { withAccountBlockerIds: accountIds }
136 videoScope.push({
137 method: [
138 VideoScopeNames.FOR_API, forApiOptions
139 ]
140 })
141
142 const findQuery = {
143 offset: options.start,
144 limit: options.count,
145 order: getSort('position'),
146 where: {
147 videoPlaylistId: options.videoPlaylistId
148 },
149 include: [
150 {
151 model: VideoModel.scope(videoScope),
152 required: false
153 }
154 ]
155 }
156
157 const countQuery = {
158 where: {
159 videoPlaylistId: options.videoPlaylistId
160 }
161 }
162
163 return Promise.all([
164 VideoPlaylistElementModel.count(countQuery),
165 VideoPlaylistElementModel.findAll(findQuery)
166 ]).then(([ total, data ]) => ({ total, data }))
167 }
168
169 static loadByPlaylistAndVideo (videoPlaylistId: number, videoId: number): Promise<MVideoPlaylistElement> {
170 const query = {
171 where: {
172 videoPlaylistId,
173 videoId
174 }
175 }
176
177 return VideoPlaylistElementModel.findOne(query)
178 }
179
180 static loadById (playlistElementId: number | string): Promise<MVideoPlaylistElement> {
181 return VideoPlaylistElementModel.findByPk(playlistElementId)
182 }
183
184 static loadByPlaylistAndElementIdForAP (
185 playlistId: number | string,
186 playlistElementId: number
187 ): Promise<MVideoPlaylistElementVideoUrlPlaylistPrivacy> {
188 const playlistWhere = validator.isUUID('' + playlistId) ? { uuid: playlistId } : { id: playlistId }
189
190 const query = {
191 include: [
192 {
193 attributes: [ 'privacy' ],
194 model: VideoPlaylistModel.unscoped(),
195 where: playlistWhere
196 },
197 {
198 attributes: [ 'url' ],
199 model: VideoModel.unscoped()
200 }
201 ],
202 where: {
203 id: playlistElementId
204 }
205 }
206
207 return VideoPlaylistElementModel.findOne(query)
208 }
209
210 static listUrlsOfForAP (videoPlaylistId: number, start: number, count: number, t?: Transaction) {
211 const query = {
212 attributes: [ 'url' ],
213 offset: start,
214 limit: count,
215 order: getSort('position'),
216 where: {
217 videoPlaylistId
218 },
219 transaction: t
220 }
221
222 return VideoPlaylistElementModel
223 .findAndCountAll(query)
224 .then(({ rows, count }) => {
225 return { total: count, data: rows.map(e => e.url) }
226 })
227 }
228
229 static loadFirstElementWithVideoThumbnail (videoPlaylistId: number): Promise<MVideoPlaylistVideoThumbnail> {
230 const query = {
231 order: getSort('position'),
232 where: {
233 videoPlaylistId
234 },
235 include: [
236 {
237 model: VideoModel.scope(VideoScopeNames.WITH_THUMBNAILS),
238 required: true
239 }
240 ]
241 }
242
243 return VideoPlaylistElementModel
244 .findOne(query)
245 }
246
247 static getNextPositionOf (videoPlaylistId: number, transaction?: Transaction) {
248 const query: AggregateOptions<number> = {
249 where: {
250 videoPlaylistId
251 },
252 transaction
253 }
254
255 return VideoPlaylistElementModel.max('position', query)
256 .then(position => position ? position + 1 : 1)
257 }
258
259 static reassignPositionOf (
260 videoPlaylistId: number,
261 firstPosition: number,
262 endPosition: number,
263 newPosition: number,
264 transaction?: Transaction
265 ) {
266 const query = {
267 where: {
268 videoPlaylistId,
269 position: {
270 [Op.gte]: firstPosition,
271 [Op.lte]: endPosition
272 }
273 },
274 transaction,
275 validate: false // We use a literal to update the position
276 }
277
278 const positionQuery = Sequelize.literal(`${newPosition} + "position" - ${firstPosition}`)
279 return VideoPlaylistElementModel.update({ position: positionQuery as any }, query)
280 }
281
282 static increasePositionOf (
283 videoPlaylistId: number,
284 fromPosition: number,
285 toPosition?: number,
286 by = 1,
287 transaction?: Transaction
288 ) {
289 const query = {
290 where: {
291 videoPlaylistId,
292 position: {
293 [Op.gte]: fromPosition
294 }
295 },
296 transaction
297 }
298
299 return VideoPlaylistElementModel.increment({ position: by }, query)
300 }
301
302 getType (this: MVideoPlaylistElementFormattable, displayNSFW?: boolean, accountId?: number) {
303 const video = this.Video
304
305 if (!video) return VideoPlaylistElementType.DELETED
306
307 // Owned video, don't filter it
308 if (accountId && video.VideoChannel.Account.id === accountId) return VideoPlaylistElementType.REGULAR
309
310 // Internal video?
311 if (video.privacy === VideoPrivacy.INTERNAL && accountId) return VideoPlaylistElementType.REGULAR
312
313 if (video.privacy === VideoPrivacy.PRIVATE || video.privacy === VideoPrivacy.INTERNAL) return VideoPlaylistElementType.PRIVATE
314
315 if (video.isBlacklisted() || video.isBlocked()) return VideoPlaylistElementType.UNAVAILABLE
316 if (video.nsfw === true && displayNSFW === false) return VideoPlaylistElementType.UNAVAILABLE
317
318 return VideoPlaylistElementType.REGULAR
319 }
320
321 getVideoElement (this: MVideoPlaylistElementFormattable, displayNSFW?: boolean, accountId?: number) {
322 if (!this.Video) return null
323 if (this.getType(displayNSFW, accountId) !== VideoPlaylistElementType.REGULAR) return null
324
325 return this.Video.toFormattedJSON()
326 }
327
328 toFormattedJSON (
329 this: MVideoPlaylistElementFormattable,
330 options: { displayNSFW?: boolean, accountId?: number } = {}
331 ): VideoPlaylistElement {
332 return {
333 id: this.id,
334 position: this.position,
335 startTimestamp: this.startTimestamp,
336 stopTimestamp: this.stopTimestamp,
337
338 type: this.getType(options.displayNSFW, options.accountId),
339
340 video: this.getVideoElement(options.displayNSFW, options.accountId)
341 }
342 }
343
344 toActivityPubObject (this: MVideoPlaylistElementAP): PlaylistElementObject {
345 const base: PlaylistElementObject = {
346 id: this.url,
347 type: 'PlaylistElement',
348
349 url: this.Video.url,
350 position: this.position
351 }
352
353 if (this.startTimestamp) base.startTimestamp = this.startTimestamp
354 if (this.stopTimestamp) base.stopTimestamp = this.stopTimestamp
355
356 return base
357 }
358 }