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