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 { VideoPlaylistType } from '../../../shared/models/videos/playlist/video-playlist-type.model'
44 import { ThumbnailModel } from './thumbnail'
45 import { ActivityIconObject } from '../../../shared/models/activitypub/objects'
48 AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST',
49 WITH_VIDEOS_LENGTH = 'WITH_VIDEOS_LENGTH',
50 WITH_ACCOUNT_AND_CHANNEL_SUMMARY = 'WITH_ACCOUNT_AND_CHANNEL_SUMMARY',
51 WITH_ACCOUNT = 'WITH_ACCOUNT',
52 WITH_THUMBNAIL = 'WITH_THUMBNAIL',
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_THUMBNAIL ]: {
68 model: () => ThumbnailModel,
73 [ ScopeNames.WITH_VIDEOS_LENGTH ]: {
77 Sequelize.literal('(SELECT COUNT("id") FROM "videoPlaylistElement" WHERE "videoPlaylistId" = "VideoPlaylistModel"."id")'),
83 [ ScopeNames.WITH_ACCOUNT ]: {
86 model: () => AccountModel,
91 [ ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY ]: {
94 model: () => AccountModel.scope(AccountScopeNames.SUMMARY),
98 model: () => VideoChannelModel.scope(VideoChannelScopeNames.SUMMARY),
103 [ ScopeNames.WITH_ACCOUNT_AND_CHANNEL ]: {
106 model: () => AccountModel,
110 model: () => VideoChannelModel,
115 [ ScopeNames.AVAILABLE_FOR_LIST ]: (options: AvailableForListOptions) => {
116 // Only list local playlists OR playlists that are on an instance followed by actorId
117 const inQueryInstanceFollow = buildServerIdsFollowedBy(options.followerActorId)
119 [ Sequelize.Op.or ]: [
125 [ Sequelize.Op.in ]: Sequelize.literal(inQueryInstanceFollow)
131 const whereAnd: any[] = []
133 if (options.privateAndUnlisted !== true) {
135 privacy: VideoPlaylistPrivacy.PUBLIC
139 if (options.accountId) {
141 ownerAccountId: options.accountId
145 if (options.videoChannelId) {
147 videoChannelId: options.videoChannelId
158 [Sequelize.Op.and]: whereAnd
161 const accountScope = {
162 method: [ AccountScopeNames.SUMMARY, actorWhere ]
169 model: AccountModel.scope(accountScope),
173 model: VideoChannelModel.scope(VideoChannelScopeNames.SUMMARY),
182 tableName: 'videoPlaylist',
185 fields: [ 'ownerAccountId' ]
188 fields: [ 'videoChannelId' ]
196 export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
204 @Is('VideoPlaylistName', value => throwIfNotValid(value, isVideoPlaylistNameValid, 'name'))
209 @Is('VideoPlaylistDescription', value => throwIfNotValid(value, isVideoPlaylistDescriptionValid, 'description'))
214 @Is('VideoPlaylistPrivacy', value => throwIfNotValid(value, isVideoPlaylistPrivacyValid, 'privacy'))
216 privacy: VideoPlaylistPrivacy
219 @Is('VideoPlaylistUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
220 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_PLAYLISTS.URL.max))
224 @Default(DataType.UUIDV4)
226 @Column(DataType.UUID)
230 @Default(VideoPlaylistType.REGULAR)
232 type: VideoPlaylistType
234 @ForeignKey(() => AccountModel)
236 ownerAccountId: number
238 @BelongsTo(() => AccountModel, {
244 OwnerAccount: AccountModel
246 @ForeignKey(() => VideoChannelModel)
248 videoChannelId: number
250 @BelongsTo(() => VideoChannelModel, {
256 VideoChannel: VideoChannelModel
258 @HasMany(() => VideoPlaylistElementModel, {
260 name: 'videoPlaylistId',
265 VideoPlaylistElements: VideoPlaylistElementModel[]
267 @HasOne(() => ThumbnailModel, {
269 name: 'videoPlaylistId',
275 Thumbnail: ThumbnailModel
277 static listForApi (options: {
278 followerActorId: number
282 type?: VideoPlaylistType,
284 videoChannelId?: number,
285 privateAndUnlisted?: boolean
288 offset: options.start,
289 limit: options.count,
290 order: getSort(options.sort)
296 ScopeNames.AVAILABLE_FOR_LIST,
299 followerActorId: options.followerActorId,
300 accountId: options.accountId,
301 videoChannelId: options.videoChannelId,
302 privateAndUnlisted: options.privateAndUnlisted
303 } as AvailableForListOptions
305 } as any, // FIXME: typings
306 ScopeNames.WITH_VIDEOS_LENGTH,
307 ScopeNames.WITH_THUMBNAIL
310 return VideoPlaylistModel
312 .findAndCountAll(query)
313 .then(({ rows, count }) => {
314 return { total: count, data: rows }
318 static listPublicUrlsOfForAP (accountId: number, start: number, count: number) {
320 attributes: [ 'url' ],
324 ownerAccountId: accountId,
325 privacy: VideoPlaylistPrivacy.PUBLIC
329 return VideoPlaylistModel.findAndCountAll(query)
330 .then(({ rows, count }) => {
331 return { total: count, data: rows.map(p => p.url) }
335 static listPlaylistIdsOf (accountId: number, videoIds: number[]) {
337 attributes: [ 'id' ],
339 ownerAccountId: accountId
343 attributes: [ 'videoId', 'startTimestamp', 'stopTimestamp' ],
344 model: VideoPlaylistElementModel.unscoped(),
347 [Sequelize.Op.any]: videoIds
355 return VideoPlaylistModel.findAll(query)
358 static doesPlaylistExist (url: string) {
366 return VideoPlaylistModel
371 static loadWithAccountAndChannelSummary (id: number | string, transaction: Sequelize.Transaction) {
372 const where = buildWhereIdOrUUID(id)
379 return VideoPlaylistModel
380 .scope([ ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY, ScopeNames.WITH_VIDEOS_LENGTH, ScopeNames.WITH_THUMBNAIL ])
384 static loadWithAccountAndChannel (id: number | string, transaction: Sequelize.Transaction) {
385 const where = buildWhereIdOrUUID(id)
392 return VideoPlaylistModel
393 .scope([ ScopeNames.WITH_ACCOUNT_AND_CHANNEL, ScopeNames.WITH_VIDEOS_LENGTH, ScopeNames.WITH_THUMBNAIL ])
397 static loadByUrlAndPopulateAccount (url: string) {
404 return VideoPlaylistModel.scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_THUMBNAIL ]).findOne(query)
407 static getPrivacyLabel (privacy: VideoPlaylistPrivacy) {
408 return VIDEO_PLAYLIST_PRIVACIES[privacy] || 'Unknown'
411 static getTypeLabel (type: VideoPlaylistType) {
412 return VIDEO_PLAYLIST_TYPES[type] || 'Unknown'
415 static resetPlaylistsOfChannel (videoChannelId: number, transaction: Sequelize.Transaction) {
423 return VideoPlaylistModel.update({ privacy: VideoPlaylistPrivacy.PRIVATE, videoChannelId: null }, query)
426 setThumbnail (thumbnail: ThumbnailModel) {
427 this.Thumbnail = thumbnail
431 return this.Thumbnail
435 return !!this.Thumbnail
438 generateThumbnailName () {
439 const extension = '.jpg'
441 return 'playlist-' + this.uuid + extension
445 if (!this.hasThumbnail()) return null
447 return WEBSERVER.URL + STATIC_PATHS.THUMBNAILS + this.getThumbnail().filename
450 getThumbnailStaticPath () {
451 if (!this.hasThumbnail()) return null
453 return join(STATIC_PATHS.THUMBNAILS, this.getThumbnail().filename)
457 this.changed('updatedAt', true)
463 return this.OwnerAccount.isOwned()
467 if (this.isOwned()) return false
469 return isOutdated(this, ACTIVITY_PUB.VIDEO_PLAYLIST_REFRESH_INTERVAL)
472 toFormattedJSON (): VideoPlaylist {
476 isLocal: this.isOwned(),
478 displayName: this.name,
479 description: this.description,
482 label: VideoPlaylistModel.getPrivacyLabel(this.privacy)
485 thumbnailPath: this.getThumbnailStaticPath(),
489 label: VideoPlaylistModel.getTypeLabel(this.type)
492 videosLength: this.get('videosLength'),
494 createdAt: this.createdAt,
495 updatedAt: this.updatedAt,
497 ownerAccount: this.OwnerAccount.toFormattedSummaryJSON(),
498 videoChannel: this.VideoChannel ? this.VideoChannel.toFormattedSummaryJSON() : null
502 toActivityPubObject (page: number, t: Sequelize.Transaction): Promise<PlaylistObject> {
503 const handler = (start: number, count: number) => {
504 return VideoPlaylistElementModel.listUrlsOfForAP(this.id, start, count, t)
507 let icon: ActivityIconObject
508 if (this.hasThumbnail()) {
510 type: 'Image' as 'Image',
511 url: this.getThumbnailUrl(),
512 mediaType: 'image/jpeg' as 'image/jpeg',
513 width: THUMBNAILS_SIZE.width,
514 height: THUMBNAILS_SIZE.height
518 return activityPubCollectionPagination(this.url, handler, page)
520 return Object.assign(o, {
521 type: 'Playlist' as 'Playlist',
523 content: this.description,
525 published: this.createdAt.toISOString(),
526 updated: this.updatedAt.toISOString(),
527 attributedTo: this.VideoChannel ? [ this.VideoChannel.Actor.url ] : [],