15 } from 'sequelize-typescript'
16 import { ForAPIOptions, ScopeNames as VideoScopeNames, VideoModel } from './video'
17 import { VideoPlaylistModel } from './video-playlist'
18 import { getSort, throwIfNotValid } from '../utils'
19 import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
20 import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
21 import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object'
22 import * as validator from 'validator'
23 import { AggregateOptions, Op, ScopeOptions, Sequelize, Transaction } from 'sequelize'
24 import { UserModel } from '../account/user'
25 import { VideoPlaylistElement, VideoPlaylistElementType } from '../../../shared/models/videos/playlist/video-playlist-element.model'
26 import { AccountModel } from '../account/account'
27 import { VideoPrivacy } from '../../../shared/models/videos'
30 tableName: 'videoPlaylistElement',
33 fields: [ 'videoPlaylistId' ]
39 fields: [ 'videoPlaylistId', 'videoId' ],
48 export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel> {
56 @Is('VideoPlaylistUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
57 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_PLAYLISTS.URL.max))
71 startTimestamp: number
79 @ForeignKey(() => VideoPlaylistModel)
81 videoPlaylistId: number
83 @BelongsTo(() => VideoPlaylistModel, {
89 VideoPlaylist: VideoPlaylistModel
91 @ForeignKey(() => VideoModel)
95 @BelongsTo(() => VideoModel, {
103 static deleteAllOf (videoPlaylistId: number, transaction?: Transaction) {
111 return VideoPlaylistElementModel.destroy(query)
114 static listForApi (options: {
117 videoPlaylistId: number,
118 serverAccount: AccountModel,
121 const accountIds = [ options.serverAccount.id ]
122 const videoScope: (ScopeOptions | string)[] = [
123 VideoScopeNames.WITH_BLACKLISTED
127 accountIds.push(options.user.Account.id)
128 videoScope.push({ method: [ VideoScopeNames.WITH_USER_HISTORY, options.user.id ] })
131 const forApiOptions: ForAPIOptions = { withAccountBlockerIds: accountIds }
134 VideoScopeNames.FOR_API, forApiOptions
139 offset: options.start,
140 limit: options.count,
141 order: getSort('position'),
143 videoPlaylistId: options.videoPlaylistId
147 model: VideoModel.scope(videoScope),
155 videoPlaylistId: options.videoPlaylistId
160 VideoPlaylistElementModel.count(countQuery),
161 VideoPlaylistElementModel.findAll(findQuery)
162 ]).then(([ total, data ]) => ({ total, data }))
165 static loadByPlaylistAndVideo (videoPlaylistId: number, videoId: number) {
173 return VideoPlaylistElementModel.findOne(query)
176 static loadById (playlistElementId: number) {
177 return VideoPlaylistElementModel.findByPk(playlistElementId)
180 static loadByPlaylistAndVideoForAP (playlistId: number | string, videoId: number | string) {
181 const playlistWhere = validator.isUUID('' + playlistId) ? { uuid: playlistId } : { id: playlistId }
182 const videoWhere = validator.isUUID('' + videoId) ? { uuid: videoId } : { id: videoId }
187 attributes: [ 'privacy' ],
188 model: VideoPlaylistModel.unscoped(),
192 attributes: [ 'url' ],
193 model: VideoModel.unscoped(),
199 return VideoPlaylistElementModel.findOne(query)
202 static listUrlsOfForAP (videoPlaylistId: number, start: number, count: number, t?: Transaction) {
204 attributes: [ 'url' ],
207 order: getSort('position'),
214 return VideoPlaylistElementModel
215 .findAndCountAll(query)
216 .then(({ rows, count }) => {
217 return { total: count, data: rows.map(e => e.url) }
221 static getNextPositionOf (videoPlaylistId: number, transaction?: Transaction) {
222 const query: AggregateOptions<number> = {
229 return VideoPlaylistElementModel.max('position', query)
230 .then(position => position ? position + 1 : 1)
233 static reassignPositionOf (
234 videoPlaylistId: number,
235 firstPosition: number,
238 transaction?: Transaction
244 [Op.gte]: firstPosition,
245 [Op.lte]: endPosition
249 validate: false // We use a literal to update the position
252 return VideoPlaylistElementModel.update({ position: Sequelize.literal(`${newPosition} + "position" - ${firstPosition}`) }, query)
255 static increasePositionOf (
256 videoPlaylistId: number,
257 fromPosition: number,
260 transaction?: Transaction
266 [Op.gte]: fromPosition
272 return VideoPlaylistElementModel.increment({ position: by }, query)
275 getType (displayNSFW?: boolean, accountId?: number) {
276 const video = this.Video
278 if (!video) return VideoPlaylistElementType.DELETED
280 // Owned video, don't filter it
281 if (accountId && video.VideoChannel.Account.id === accountId) return VideoPlaylistElementType.REGULAR
283 if (video.privacy === VideoPrivacy.PRIVATE) return VideoPlaylistElementType.PRIVATE
285 if (video.isBlacklisted() || video.isBlocked()) return VideoPlaylistElementType.UNAVAILABLE
286 if (video.nsfw === true && displayNSFW === false) return VideoPlaylistElementType.UNAVAILABLE
288 return VideoPlaylistElementType.REGULAR
291 getVideoElement (displayNSFW?: boolean, accountId?: number) {
292 if (!this.Video) return null
293 if (this.getType(displayNSFW, accountId) !== VideoPlaylistElementType.REGULAR) return null
295 return this.Video.toFormattedJSON()
298 toFormattedJSON (options: { displayNSFW?: boolean, accountId?: number } = {}): VideoPlaylistElement {
301 position: this.position,
302 startTimestamp: this.startTimestamp,
303 stopTimestamp: this.stopTimestamp,
305 type: this.getType(options.displayNSFW, options.accountId),
307 video: this.getVideoElement(options.displayNSFW, options.accountId)
311 toActivityPubObject (): PlaylistElementObject {
312 const base: PlaylistElementObject = {
314 type: 'PlaylistElement',
317 position: this.position
320 if (this.startTimestamp) base.startTimestamp = this.startTimestamp
321 if (this.stopTimestamp) base.stopTimestamp = this.stopTimestamp