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