17 } from 'sequelize-typescript'
18 import * as Sequelize from 'sequelize'
19 import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
20 import { buildServerIdsFollowedBy, buildWhereIdOrUUID, getSort, isOutdated, throwIfNotValid } from '../utils'
22 isVideoPlaylistDescriptionValid,
23 isVideoPlaylistNameValid,
24 isVideoPlaylistPrivacyValid
25 } from '../../helpers/custom-validators/video-playlists'
26 import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
32 VIDEO_PLAYLIST_PRIVACIES,
35 } from '../../initializers/constants'
36 import { VideoPlaylist } from '../../../shared/models/videos/playlist/video-playlist.model'
37 import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account'
38 import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel'
39 import { join } from 'path'
40 import { VideoPlaylistElementModel } from './video-playlist-element'
41 import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object'
42 import { activityPubCollectionPagination } from '../../helpers/activitypub'
43 import { remove } from 'fs-extra'
44 import { logger } from '../../helpers/logger'
45 import { VideoPlaylistType } from '../../../shared/models/videos/playlist/video-playlist-type.model'
46 import { CONFIG } from '../../initializers/config'
49 AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST',
50 WITH_VIDEOS_LENGTH = 'WITH_VIDEOS_LENGTH',
51 WITH_ACCOUNT_AND_CHANNEL_SUMMARY = 'WITH_ACCOUNT_AND_CHANNEL_SUMMARY',
52 WITH_ACCOUNT = 'WITH_ACCOUNT',
53 WITH_ACCOUNT_AND_CHANNEL = 'WITH_ACCOUNT_AND_CHANNEL'
56 type AvailableForListOptions = {
57 followerActorId: number
58 type?: VideoPlaylistType
60 videoChannelId?: number
61 privateAndUnlisted?: boolean
65 [ ScopeNames.WITH_VIDEOS_LENGTH ]: {
69 Sequelize.literal('(SELECT COUNT("id") FROM "videoPlaylistElement" WHERE "videoPlaylistId" = "VideoPlaylistModel"."id")'),
75 [ ScopeNames.WITH_ACCOUNT ]: {
78 model: () => AccountModel,
83 [ ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY ]: {
86 model: () => AccountModel.scope(AccountScopeNames.SUMMARY),
90 model: () => VideoChannelModel.scope(VideoChannelScopeNames.SUMMARY),
95 [ ScopeNames.WITH_ACCOUNT_AND_CHANNEL ]: {
98 model: () => AccountModel,
102 model: () => VideoChannelModel,
107 [ ScopeNames.AVAILABLE_FOR_LIST ]: (options: AvailableForListOptions) => {
108 // Only list local playlists OR playlists that are on an instance followed by actorId
109 const inQueryInstanceFollow = buildServerIdsFollowedBy(options.followerActorId)
111 [ Sequelize.Op.or ]: [
117 [ Sequelize.Op.in ]: Sequelize.literal(inQueryInstanceFollow)
123 const whereAnd: any[] = []
125 if (options.privateAndUnlisted !== true) {
127 privacy: VideoPlaylistPrivacy.PUBLIC
131 if (options.accountId) {
133 ownerAccountId: options.accountId
137 if (options.videoChannelId) {
139 videoChannelId: options.videoChannelId
150 [Sequelize.Op.and]: whereAnd
153 const accountScope = {
154 method: [ AccountScopeNames.SUMMARY, actorWhere ]
161 model: AccountModel.scope(accountScope),
165 model: VideoChannelModel.scope(VideoChannelScopeNames.SUMMARY),
174 tableName: 'videoPlaylist',
177 fields: [ 'ownerAccountId' ]
180 fields: [ 'videoChannelId' ]
188 export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
196 @Is('VideoPlaylistName', value => throwIfNotValid(value, isVideoPlaylistNameValid, 'name'))
201 @Is('VideoPlaylistDescription', value => throwIfNotValid(value, isVideoPlaylistDescriptionValid, 'description'))
206 @Is('VideoPlaylistPrivacy', value => throwIfNotValid(value, isVideoPlaylistPrivacyValid, 'privacy'))
208 privacy: VideoPlaylistPrivacy
211 @Is('VideoPlaylistUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
212 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_PLAYLISTS.URL.max))
216 @Default(DataType.UUIDV4)
218 @Column(DataType.UUID)
222 @Default(VideoPlaylistType.REGULAR)
224 type: VideoPlaylistType
226 @ForeignKey(() => AccountModel)
228 ownerAccountId: number
230 @BelongsTo(() => AccountModel, {
236 OwnerAccount: AccountModel
238 @ForeignKey(() => VideoChannelModel)
240 videoChannelId: number
242 @BelongsTo(() => VideoChannelModel, {
248 VideoChannel: VideoChannelModel
250 @HasMany(() => VideoPlaylistElementModel, {
252 name: 'videoPlaylistId',
257 VideoPlaylistElements: VideoPlaylistElementModel[]
260 static async removeFiles (instance: VideoPlaylistModel) {
261 logger.info('Removing files of video playlist %s.', instance.url)
263 return instance.removeThumbnail()
266 static listForApi (options: {
267 followerActorId: number
271 type?: VideoPlaylistType,
273 videoChannelId?: number,
274 privateAndUnlisted?: boolean
277 offset: options.start,
278 limit: options.count,
279 order: getSort(options.sort)
285 ScopeNames.AVAILABLE_FOR_LIST,
288 followerActorId: options.followerActorId,
289 accountId: options.accountId,
290 videoChannelId: options.videoChannelId,
291 privateAndUnlisted: options.privateAndUnlisted
292 } as AvailableForListOptions
294 } as any, // FIXME: typings
295 ScopeNames.WITH_VIDEOS_LENGTH
298 return VideoPlaylistModel
300 .findAndCountAll(query)
301 .then(({ rows, count }) => {
302 return { total: count, data: rows }
306 static listPublicUrlsOfForAP (accountId: number, start: number, count: number) {
308 attributes: [ 'url' ],
312 ownerAccountId: accountId,
313 privacy: VideoPlaylistPrivacy.PUBLIC
317 return VideoPlaylistModel.findAndCountAll(query)
318 .then(({ rows, count }) => {
319 return { total: count, data: rows.map(p => p.url) }
323 static listPlaylistIdsOf (accountId: number, videoIds: number[]) {
325 attributes: [ 'id' ],
327 ownerAccountId: accountId
331 attributes: [ 'videoId', 'startTimestamp', 'stopTimestamp' ],
332 model: VideoPlaylistElementModel.unscoped(),
335 [Sequelize.Op.any]: videoIds
343 return VideoPlaylistModel.findAll(query)
346 static doesPlaylistExist (url: string) {
354 return VideoPlaylistModel
359 static loadWithAccountAndChannelSummary (id: number | string, transaction: Sequelize.Transaction) {
360 const where = buildWhereIdOrUUID(id)
367 return VideoPlaylistModel
368 .scope([ ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY, ScopeNames.WITH_VIDEOS_LENGTH ])
372 static loadWithAccountAndChannel (id: number | string, transaction: Sequelize.Transaction) {
373 const where = buildWhereIdOrUUID(id)
380 return VideoPlaylistModel
381 .scope([ ScopeNames.WITH_ACCOUNT_AND_CHANNEL, ScopeNames.WITH_VIDEOS_LENGTH ])
385 static loadByUrlAndPopulateAccount (url: string) {
392 return VideoPlaylistModel.scope(ScopeNames.WITH_ACCOUNT).findOne(query)
395 static getPrivacyLabel (privacy: VideoPlaylistPrivacy) {
396 return VIDEO_PLAYLIST_PRIVACIES[privacy] || 'Unknown'
399 static getTypeLabel (type: VideoPlaylistType) {
400 return VIDEO_PLAYLIST_TYPES[type] || 'Unknown'
403 static resetPlaylistsOfChannel (videoChannelId: number, transaction: Sequelize.Transaction) {
411 return VideoPlaylistModel.update({ privacy: VideoPlaylistPrivacy.PRIVATE, videoChannelId: null }, query)
414 getThumbnailName () {
415 const extension = '.jpg'
417 return 'playlist-' + this.uuid + extension
421 return WEBSERVER.URL + STATIC_PATHS.THUMBNAILS + this.getThumbnailName()
424 getThumbnailStaticPath () {
425 return join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName())
429 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
430 return remove(thumbnailPath)
431 .catch(err => logger.warn('Cannot delete thumbnail %s.', thumbnailPath, { err }))
435 this.changed('updatedAt', true)
441 return this.OwnerAccount.isOwned()
445 if (this.isOwned()) return false
447 return isOutdated(this, ACTIVITY_PUB.VIDEO_PLAYLIST_REFRESH_INTERVAL)
450 toFormattedJSON (): VideoPlaylist {
454 isLocal: this.isOwned(),
456 displayName: this.name,
457 description: this.description,
460 label: VideoPlaylistModel.getPrivacyLabel(this.privacy)
463 thumbnailPath: this.getThumbnailStaticPath(),
467 label: VideoPlaylistModel.getTypeLabel(this.type)
470 videosLength: this.get('videosLength'),
472 createdAt: this.createdAt,
473 updatedAt: this.updatedAt,
475 ownerAccount: this.OwnerAccount.toFormattedSummaryJSON(),
476 videoChannel: this.VideoChannel ? this.VideoChannel.toFormattedSummaryJSON() : null
480 toActivityPubObject (page: number, t: Sequelize.Transaction): Promise<PlaylistObject> {
481 const handler = (start: number, count: number) => {
482 return VideoPlaylistElementModel.listUrlsOfForAP(this.id, start, count, t)
485 return activityPubCollectionPagination(this.url, handler, page)
487 return Object.assign(o, {
488 type: 'Playlist' as 'Playlist',
490 content: this.description,
492 published: this.createdAt.toISOString(),
493 updated: this.updatedAt.toISOString(),
494 attributedTo: this.VideoChannel ? [ this.VideoChannel.Actor.url ] : [],
496 type: 'Image' as 'Image',
497 url: this.getThumbnailUrl(),
498 mediaType: 'image/jpeg' as 'image/jpeg',
499 width: THUMBNAILS_SIZE.width,
500 height: THUMBNAILS_SIZE.height