]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/models/video/video-playlist-element.ts
Merge branch 'release/2.1.0' into develop
[github/Chocobozzz/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 if (video.privacy === VideoPrivacy.PRIVATE) return VideoPlaylistElementType.PRIVATE
313
314 if (video.isBlacklisted() || video.isBlocked()) return VideoPlaylistElementType.UNAVAILABLE
315 if (video.nsfw === true && displayNSFW === false) return VideoPlaylistElementType.UNAVAILABLE
316
317 return VideoPlaylistElementType.REGULAR
318 }
319
320 getVideoElement (this: MVideoPlaylistElementFormattable, displayNSFW?: boolean, accountId?: number) {
321 if (!this.Video) return null
322 if (this.getType(displayNSFW, accountId) !== VideoPlaylistElementType.REGULAR) return null
323
324 return this.Video.toFormattedJSON()
325 }
326
327 toFormattedJSON (
328 this: MVideoPlaylistElementFormattable,
329 options: { displayNSFW?: boolean, accountId?: number } = {}
330 ): VideoPlaylistElement {
331 return {
332 id: this.id,
333 position: this.position,
334 startTimestamp: this.startTimestamp,
335 stopTimestamp: this.stopTimestamp,
336
337 type: this.getType(options.displayNSFW, options.accountId),
338
339 video: this.getVideoElement(options.displayNSFW, options.accountId)
340 }
341 }
342
343 toActivityPubObject (this: MVideoPlaylistElementAP): PlaylistElementObject {
344 const base: PlaylistElementObject = {
345 id: this.url,
346 type: 'PlaylistElement',
347
348 url: this.Video.url,
349 position: this.position
350 }
351
352 if (this.startTimestamp) base.startTimestamp = this.startTimestamp
353 if (this.stopTimestamp) base.stopTimestamp = this.stopTimestamp
354
355 return base
356 }
357 }