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 { AttributesOnly } from '@shared/typescript-utils'
27 import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object'
28 import { VideoPrivacy } from '../../../shared/models/videos'
29 import { VideoPlaylistElement, VideoPlaylistElementType } from '../../../shared/models/videos/playlist/video-playlist-element.model'
30 import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
31 import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
32 import { AccountModel } from '../account/account'
33 import { getSort, throwIfNotValid } from '../utils'
34 import { ForAPIOptions, ScopeNames as VideoScopeNames, VideoModel } from './video'
35 import { VideoPlaylistModel } from './video-playlist'
38 tableName: 'videoPlaylistElement',
41 fields: [ 'videoPlaylistId' ]
52 export class VideoPlaylistElementModel extends Model<Partial<AttributesOnly<VideoPlaylistElementModel>>> {
60 @Is('VideoPlaylistUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url', true))
61 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_PLAYLISTS.URL.max))
75 startTimestamp: number
83 @ForeignKey(() => VideoPlaylistModel)
85 videoPlaylistId: number
87 @BelongsTo(() => VideoPlaylistModel, {
93 VideoPlaylist: VideoPlaylistModel
95 @ForeignKey(() => VideoModel)
99 @BelongsTo(() => VideoModel, {
107 static deleteAllOf (videoPlaylistId: number, transaction?: Transaction) {
115 return VideoPlaylistElementModel.destroy(query)
118 static listForApi (options: {
121 videoPlaylistId: number
122 serverAccount: AccountModel
123 user?: MUserAccountId
125 const accountIds = [ options.serverAccount.id ]
126 const videoScope: (ScopeOptions | string)[] = [
127 VideoScopeNames.WITH_BLACKLISTED
131 accountIds.push(options.user.Account.id)
132 videoScope.push({ method: [ VideoScopeNames.WITH_USER_HISTORY, options.user.id ] })
135 const forApiOptions: ForAPIOptions = { withAccountBlockerIds: accountIds }
138 VideoScopeNames.FOR_API, forApiOptions
143 offset: options.start,
144 limit: options.count,
145 order: getSort('position'),
147 videoPlaylistId: options.videoPlaylistId
151 model: VideoModel.scope(videoScope),
159 videoPlaylistId: options.videoPlaylistId
164 VideoPlaylistElementModel.count(countQuery),
165 VideoPlaylistElementModel.findAll(findQuery)
166 ]).then(([ total, data ]) => ({ total, data }))
169 static loadByPlaylistAndVideo (videoPlaylistId: number, videoId: number): Promise<MVideoPlaylistElement> {
177 return VideoPlaylistElementModel.findOne(query)
180 static loadById (playlistElementId: number | string): Promise<MVideoPlaylistElement> {
181 return VideoPlaylistElementModel.findByPk(playlistElementId)
184 static loadByPlaylistAndElementIdForAP (
185 playlistId: number | string,
186 playlistElementId: number
187 ): Promise<MVideoPlaylistElementVideoUrlPlaylistPrivacy> {
188 const playlistWhere = validator.isUUID('' + playlistId) ? { uuid: playlistId } : { id: playlistId }
193 attributes: [ 'privacy' ],
194 model: VideoPlaylistModel.unscoped(),
198 attributes: [ 'url' ],
199 model: VideoModel.unscoped()
203 id: playlistElementId
207 return VideoPlaylistElementModel.findOne(query)
210 static listUrlsOfForAP (videoPlaylistId: number, start: number, count: number, t?: Transaction) {
211 const getQuery = (forCount: boolean) => {
218 order: getSort('position'),
227 VideoPlaylistElementModel.count(getQuery(true)),
228 VideoPlaylistElementModel.findAll(getQuery(false))
229 ]).then(([ total, rows ]) => ({
231 data: rows.map(e => e.url)
235 static loadFirstElementWithVideoThumbnail (videoPlaylistId: number): Promise<MVideoPlaylistVideoThumbnail> {
237 order: getSort('position'),
243 model: VideoModel.scope(VideoScopeNames.WITH_THUMBNAILS),
249 return VideoPlaylistElementModel
253 static getNextPositionOf (videoPlaylistId: number, transaction?: Transaction) {
254 const query: AggregateOptions<number> = {
261 return VideoPlaylistElementModel.max('position', query)
262 .then(position => position ? position + 1 : 1)
265 static reassignPositionOf (
266 videoPlaylistId: number,
267 firstPosition: number,
270 transaction?: Transaction
276 [Op.gte]: firstPosition,
277 [Op.lte]: endPosition
281 validate: false // We use a literal to update the position
284 const positionQuery = Sequelize.literal(`${newPosition} + "position" - ${firstPosition}`)
285 return VideoPlaylistElementModel.update({ position: positionQuery }, query)
288 static increasePositionOf (
289 videoPlaylistId: number,
290 fromPosition: number,
292 transaction?: Transaction
298 [Op.gte]: fromPosition
304 return VideoPlaylistElementModel.increment({ position: by }, query)
307 getType (this: MVideoPlaylistElementFormattable, displayNSFW?: boolean, accountId?: number) {
308 const video = this.Video
310 if (!video) return VideoPlaylistElementType.DELETED
312 // Owned video, don't filter it
313 if (accountId && video.VideoChannel.Account.id === accountId) return VideoPlaylistElementType.REGULAR
316 if (video.privacy === VideoPrivacy.INTERNAL && accountId) return VideoPlaylistElementType.REGULAR
318 if (video.privacy === VideoPrivacy.PRIVATE || video.privacy === VideoPrivacy.INTERNAL) return VideoPlaylistElementType.PRIVATE
320 if (video.isBlacklisted() || video.isBlocked()) return VideoPlaylistElementType.UNAVAILABLE
321 if (video.nsfw === true && displayNSFW === false) return VideoPlaylistElementType.UNAVAILABLE
323 return VideoPlaylistElementType.REGULAR
326 getVideoElement (this: MVideoPlaylistElementFormattable, displayNSFW?: boolean, accountId?: number) {
327 if (!this.Video) return null
328 if (this.getType(displayNSFW, accountId) !== VideoPlaylistElementType.REGULAR) return null
330 return this.Video.toFormattedJSON()
334 this: MVideoPlaylistElementFormattable,
335 options: { displayNSFW?: boolean, accountId?: number } = {}
336 ): VideoPlaylistElement {
339 position: this.position,
340 startTimestamp: this.startTimestamp,
341 stopTimestamp: this.stopTimestamp,
343 type: this.getType(options.displayNSFW, options.accountId),
345 video: this.getVideoElement(options.displayNSFW, options.accountId)
349 toActivityPubObject (this: MVideoPlaylistElementAP): PlaylistElementObject {
350 const base: PlaylistElementObject = {
352 type: 'PlaylistElement',
355 position: this.position
358 if (this.startTimestamp) base.startTimestamp = this.startTimestamp
359 if (this.stopTimestamp) base.stopTimestamp = this.stopTimestamp