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