1 import { AggregateOptions, Op, ScopeOptions, Sequelize, Transaction } from 'sequelize'
16 } from 'sequelize-typescript'
17 import validator from 'validator'
18 import { MUserAccountId } from '@server/types/models'
20 MVideoPlaylistElement,
21 MVideoPlaylistElementAP,
22 MVideoPlaylistElementFormattable,
23 MVideoPlaylistElementVideoUrlPlaylistPrivacy,
24 MVideoPlaylistVideoThumbnail
25 } from '@server/types/models/video/video-playlist-element'
26 import { forceNumber } from '@shared/core-utils'
27 import { AttributesOnly } from '@shared/typescript-utils'
28 import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object'
29 import { VideoPrivacy } from '../../../shared/models/videos'
30 import { VideoPlaylistElement, VideoPlaylistElementType } from '../../../shared/models/videos/playlist/video-playlist-element.model'
31 import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
32 import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
33 import { AccountModel } from '../account/account'
34 import { getSort, throwIfNotValid } from '../shared'
35 import { ForAPIOptions, ScopeNames as VideoScopeNames, VideoModel } from './video'
36 import { VideoPlaylistModel } from './video-playlist'
39 tableName: 'videoPlaylistElement',
42 fields: [ 'videoPlaylistId' ]
53 export class VideoPlaylistElementModel extends Model<Partial<AttributesOnly<VideoPlaylistElementModel>>> {
61 @Is('VideoPlaylistUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url', true))
62 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_PLAYLISTS.URL.max))
76 startTimestamp: number
84 @ForeignKey(() => VideoPlaylistModel)
86 videoPlaylistId: number
88 @BelongsTo(() => VideoPlaylistModel, {
94 VideoPlaylist: VideoPlaylistModel
96 @ForeignKey(() => VideoModel)
100 @BelongsTo(() => VideoModel, {
108 static deleteAllOf (videoPlaylistId: number, transaction?: Transaction) {
116 return VideoPlaylistElementModel.destroy(query)
119 static listForApi (options: {
122 videoPlaylistId: number
123 serverAccount: AccountModel
124 user?: MUserAccountId
126 const accountIds = [ options.serverAccount.id ]
127 const videoScope: (ScopeOptions | string)[] = [
128 VideoScopeNames.WITH_BLACKLISTED
132 accountIds.push(options.user.Account.id)
133 videoScope.push({ method: [ VideoScopeNames.WITH_USER_HISTORY, options.user.id ] })
136 const forApiOptions: ForAPIOptions = { withAccountBlockerIds: accountIds }
139 VideoScopeNames.FOR_API, forApiOptions
144 offset: options.start,
145 limit: options.count,
146 order: getSort('position'),
148 videoPlaylistId: options.videoPlaylistId
152 model: VideoModel.scope(videoScope),
160 videoPlaylistId: options.videoPlaylistId
165 VideoPlaylistElementModel.count(countQuery),
166 VideoPlaylistElementModel.findAll(findQuery)
167 ]).then(([ total, data ]) => ({ total, data }))
170 static loadByPlaylistAndVideo (videoPlaylistId: number, videoId: number): Promise<MVideoPlaylistElement> {
178 return VideoPlaylistElementModel.findOne(query)
181 static loadById (playlistElementId: number | string): Promise<MVideoPlaylistElement> {
182 return VideoPlaylistElementModel.findByPk(playlistElementId)
185 static loadByPlaylistAndElementIdForAP (
186 playlistId: number | string,
187 playlistElementId: number
188 ): Promise<MVideoPlaylistElementVideoUrlPlaylistPrivacy> {
189 const playlistWhere = validator.isUUID('' + playlistId)
190 ? { uuid: playlistId }
196 attributes: [ 'privacy' ],
197 model: VideoPlaylistModel.unscoped(),
201 attributes: [ 'url' ],
202 model: VideoModel.unscoped()
206 id: playlistElementId
210 return VideoPlaylistElementModel.findOne(query)
213 static listUrlsOfForAP (videoPlaylistId: number, start: number, count: number, t?: Transaction) {
214 const getQuery = (forCount: boolean) => {
221 order: getSort('position'),
230 VideoPlaylistElementModel.count(getQuery(true)),
231 VideoPlaylistElementModel.findAll(getQuery(false))
232 ]).then(([ total, rows ]) => ({
234 data: rows.map(e => e.url)
238 static loadFirstElementWithVideoThumbnail (videoPlaylistId: number): Promise<MVideoPlaylistVideoThumbnail> {
240 order: getSort('position'),
246 model: VideoModel.scope(VideoScopeNames.WITH_THUMBNAILS),
252 return VideoPlaylistElementModel
256 static getNextPositionOf (videoPlaylistId: number, transaction?: Transaction) {
257 const query: AggregateOptions<number> = {
264 return VideoPlaylistElementModel.max('position', query)
265 .then(position => position ? position + 1 : 1)
268 static reassignPositionOf (options: {
269 videoPlaylistId: number
270 firstPosition: number
273 transaction?: Transaction
275 const { videoPlaylistId, firstPosition, endPosition, newPosition, transaction } = options
281 [Op.gte]: firstPosition,
282 [Op.lte]: endPosition
286 validate: false // We use a literal to update the position
289 const positionQuery = Sequelize.literal(`${forceNumber(newPosition)} + "position" - ${forceNumber(firstPosition)}`)
290 return VideoPlaylistElementModel.update({ position: positionQuery }, query)
293 static increasePositionOf (
294 videoPlaylistId: number,
295 fromPosition: number,
297 transaction?: Transaction
303 [Op.gte]: fromPosition
309 return VideoPlaylistElementModel.increment({ position: by }, query)
313 this: MVideoPlaylistElementFormattable,
314 options: { accountId?: number } = {}
315 ): VideoPlaylistElement {
318 position: this.position,
319 startTimestamp: this.startTimestamp,
320 stopTimestamp: this.stopTimestamp,
322 type: this.getType(options.accountId),
324 video: this.getVideoElement(options.accountId)
328 getType (this: MVideoPlaylistElementFormattable, accountId?: number) {
329 const video = this.Video
331 if (!video) return VideoPlaylistElementType.DELETED
333 // Owned video, don't filter it
334 if (accountId && video.VideoChannel.Account.id === accountId) return VideoPlaylistElementType.REGULAR
337 if (video.privacy === VideoPrivacy.INTERNAL && accountId) return VideoPlaylistElementType.REGULAR
339 if (video.privacy === VideoPrivacy.PRIVATE || video.privacy === VideoPrivacy.INTERNAL) return VideoPlaylistElementType.PRIVATE
341 if (video.isBlacklisted() || video.isBlocked()) return VideoPlaylistElementType.UNAVAILABLE
343 return VideoPlaylistElementType.REGULAR
346 getVideoElement (this: MVideoPlaylistElementFormattable, accountId?: number) {
347 if (!this.Video) return null
348 if (this.getType(accountId) !== VideoPlaylistElementType.REGULAR) return null
350 return this.Video.toFormattedJSON()
353 toActivityPubObject (this: MVideoPlaylistElementAP): PlaylistElementObject {
354 const base: PlaylistElementObject = {
356 type: 'PlaylistElement',
358 url: this.Video?.url || null,
359 position: this.position
362 if (this.startTimestamp) base.startTimestamp = this.startTimestamp
363 if (this.stopTimestamp) base.stopTimestamp = this.stopTimestamp