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