]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/models/video/video-playlist-element.ts
Fix subscriptions
[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/typescript-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 }, query)
280 }
281
282 static increasePositionOf (
283 videoPlaylistId: number,
284 fromPosition: number,
285 by = 1,
286 transaction?: Transaction
287 ) {
288 const query = {
289 where: {
290 videoPlaylistId,
291 position: {
292 [Op.gte]: fromPosition
293 }
294 },
295 transaction
296 }
297
298 return VideoPlaylistElementModel.increment({ position: by }, query)
299 }
300
301 getType (this: MVideoPlaylistElementFormattable, displayNSFW?: boolean, accountId?: number) {
302 const video = this.Video
303
304 if (!video) return VideoPlaylistElementType.DELETED
305
306 // Owned video, don't filter it
307 if (accountId && video.VideoChannel.Account.id === accountId) return VideoPlaylistElementType.REGULAR
308
309 // Internal video?
310 if (video.privacy === VideoPrivacy.INTERNAL && accountId) return VideoPlaylistElementType.REGULAR
311
312 if (video.privacy === VideoPrivacy.PRIVATE || video.privacy === VideoPrivacy.INTERNAL) 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 }