} from 'sequelize-typescript'
import * as Sequelize from 'sequelize'
import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
-import { buildServerIdsFollowedBy, buildWhereIdOrUUID, getSort, throwIfNotValid } from '../utils'
+import { buildServerIdsFollowedBy, buildWhereIdOrUUID, getSort, isOutdated, throwIfNotValid } from '../utils'
import {
isVideoPlaylistDescriptionValid,
isVideoPlaylistNameValid,
isVideoPlaylistPrivacyValid
} from '../../helpers/custom-validators/video-playlists'
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
-import { CONFIG, CONSTRAINTS_FIELDS, STATIC_PATHS, THUMBNAILS_SIZE, VIDEO_PLAYLIST_PRIVACIES } from '../../initializers'
+import {
+ ACTIVITY_PUB,
+ CONFIG,
+ CONSTRAINTS_FIELDS,
+ STATIC_PATHS,
+ THUMBNAILS_SIZE,
+ VIDEO_PLAYLIST_PRIVACIES,
+ VIDEO_PLAYLIST_TYPES
+} from '../../initializers'
import { VideoPlaylist } from '../../../shared/models/videos/playlist/video-playlist.model'
import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account'
import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel'
import { activityPubCollectionPagination } from '../../helpers/activitypub'
import { remove } from 'fs-extra'
import { logger } from '../../helpers/logger'
+import { VideoPlaylistType } from '../../../shared/models/videos/playlist/video-playlist-type.model'
enum ScopeNames {
AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST',
WITH_VIDEOS_LENGTH = 'WITH_VIDEOS_LENGTH',
+ WITH_ACCOUNT_AND_CHANNEL_SUMMARY = 'WITH_ACCOUNT_AND_CHANNEL_SUMMARY',
+ WITH_ACCOUNT = 'WITH_ACCOUNT',
WITH_ACCOUNT_AND_CHANNEL = 'WITH_ACCOUNT_AND_CHANNEL'
}
type AvailableForListOptions = {
followerActorId: number
- accountId?: number,
+ type?: VideoPlaylistType
+ accountId?: number
videoChannelId?: number
privateAndUnlisted?: boolean
}
@Scopes({
- [ScopeNames.WITH_VIDEOS_LENGTH]: {
+ [ ScopeNames.WITH_VIDEOS_LENGTH ]: {
attributes: {
include: [
[
]
}
},
- [ScopeNames.WITH_ACCOUNT_AND_CHANNEL]: {
+ [ ScopeNames.WITH_ACCOUNT ]: {
+ include: [
+ {
+ model: () => AccountModel,
+ required: true
+ }
+ ]
+ },
+ [ ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY ]: {
include: [
{
model: () => AccountModel.scope(AccountScopeNames.SUMMARY),
}
]
},
- [ScopeNames.AVAILABLE_FOR_LIST]: (options: AvailableForListOptions) => {
+ [ ScopeNames.WITH_ACCOUNT_AND_CHANNEL ]: {
+ include: [
+ {
+ model: () => AccountModel,
+ required: true
+ },
+ {
+ model: () => VideoChannelModel,
+ required: false
+ }
+ ]
+ },
+ [ ScopeNames.AVAILABLE_FOR_LIST ]: (options: AvailableForListOptions) => {
// Only list local playlists OR playlists that are on an instance followed by actorId
const inQueryInstanceFollow = buildServerIdsFollowedBy(options.followerActorId)
const actorWhere = {
})
}
+ if (options.type) {
+ whereAnd.push({
+ type: options.type
+ })
+ }
+
const where = {
[Sequelize.Op.and]: whereAnd
}
@Column(DataType.UUID)
uuid: string
+ @AllowNull(false)
+ @Default(VideoPlaylistType.REGULAR)
+ @Column
+ type: VideoPlaylistType
+
@ForeignKey(() => AccountModel)
@Column
ownerAccountId: number
name: 'videoPlaylistId',
allowNull: false
},
- onDelete: 'cascade'
+ onDelete: 'CASCADE'
})
VideoPlaylistElements: VideoPlaylistElementModel[]
- // Calculated field
- videosLength?: number
-
@BeforeDestroy
static async removeFiles (instance: VideoPlaylistModel) {
logger.info('Removing files of video playlist %s.', instance.url)
start: number,
count: number,
sort: string,
+ type?: VideoPlaylistType,
accountId?: number,
videoChannelId?: number,
privateAndUnlisted?: boolean
method: [
ScopeNames.AVAILABLE_FOR_LIST,
{
+ type: options.type,
followerActorId: options.followerActorId,
accountId: options.accountId,
videoChannelId: options.videoChannelId,
})
}
- static listUrlsOfForAP (accountId: number, start: number, count: number) {
+ static listPublicUrlsOfForAP (accountId: number, start: number, count: number) {
const query = {
attributes: [ 'url' ],
offset: start,
limit: count,
where: {
- ownerAccountId: accountId
+ ownerAccountId: accountId,
+ privacy: VideoPlaylistPrivacy.PUBLIC
}
}
})
}
+ static listPlaylistIdsOf (accountId: number, videoIds: number[]) {
+ const query = {
+ attributes: [ 'id' ],
+ where: {
+ ownerAccountId: accountId
+ },
+ include: [
+ {
+ attributes: [ 'videoId', 'startTimestamp', 'stopTimestamp' ],
+ model: VideoPlaylistElementModel.unscoped(),
+ where: {
+ videoId: {
+ [Sequelize.Op.any]: videoIds
+ }
+ },
+ required: true
+ }
+ ]
+ }
+
+ return VideoPlaylistModel.findAll(query)
+ }
+
static doesPlaylistExist (url: string) {
const query = {
attributes: [],
.then(e => !!e)
}
- static load (id: number | string, transaction: Sequelize.Transaction) {
+ static loadWithAccountAndChannelSummary (id: number | string, transaction: Sequelize.Transaction) {
+ const where = buildWhereIdOrUUID(id)
+
+ const query = {
+ where,
+ transaction
+ }
+
+ return VideoPlaylistModel
+ .scope([ ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY, ScopeNames.WITH_VIDEOS_LENGTH ])
+ .findOne(query)
+ }
+
+ static loadWithAccountAndChannel (id: number | string, transaction: Sequelize.Transaction) {
const where = buildWhereIdOrUUID(id)
const query = {
.findOne(query)
}
+ static loadByUrlAndPopulateAccount (url: string) {
+ const query = {
+ where: {
+ url
+ }
+ }
+
+ return VideoPlaylistModel.scope(ScopeNames.WITH_ACCOUNT).findOne(query)
+ }
+
static getPrivacyLabel (privacy: VideoPlaylistPrivacy) {
return VIDEO_PLAYLIST_PRIVACIES[privacy] || 'Unknown'
}
+ static getTypeLabel (type: VideoPlaylistType) {
+ return VIDEO_PLAYLIST_TYPES[type] || 'Unknown'
+ }
+
+ static resetPlaylistsOfChannel (videoChannelId: number, transaction: Sequelize.Transaction) {
+ const query = {
+ where: {
+ videoChannelId
+ },
+ transaction
+ }
+
+ return VideoPlaylistModel.update({ privacy: VideoPlaylistPrivacy.PRIVATE, videoChannelId: null }, query)
+ }
+
getThumbnailName () {
const extension = '.jpg'
.catch(err => logger.warn('Cannot delete thumbnail %s.', thumbnailPath, { err }))
}
+ setAsRefreshed () {
+ this.changed('updatedAt', true)
+
+ return this.save()
+ }
+
isOwned () {
return this.OwnerAccount.isOwned()
}
+ isOutdated () {
+ if (this.isOwned()) return false
+
+ return isOutdated(this, ACTIVITY_PUB.VIDEO_PLAYLIST_REFRESH_INTERVAL)
+ }
+
toFormattedJSON (): VideoPlaylist {
return {
id: this.id,
thumbnailPath: this.getThumbnailStaticPath(),
- videosLength: this.videosLength,
+ type: {
+ id: this.type,
+ label: VideoPlaylistModel.getTypeLabel(this.type)
+ },
+
+ videosLength: this.get('videosLength'),
createdAt: this.createdAt,
updatedAt: this.updatedAt,
}
}
- toActivityPubObject (): Promise<PlaylistObject> {
+ toActivityPubObject (page: number, t: Sequelize.Transaction): Promise<PlaylistObject> {
const handler = (start: number, count: number) => {
- return VideoPlaylistElementModel.listUrlsOfForAP(this.id, start, count)
+ return VideoPlaylistElementModel.listUrlsOfForAP(this.id, start, count, t)
}
- return activityPubCollectionPagination(this.url, handler, null)
+ return activityPubCollectionPagination(this.url, handler, page)
.then(o => {
return Object.assign(o, {
type: 'Playlist' as 'Playlist',
name: this.name,
content: this.description,
uuid: this.uuid,
+ published: this.createdAt.toISOString(),
+ updated: this.updatedAt.toISOString(),
attributedTo: this.VideoChannel ? [ this.VideoChannel.Actor.url ] : [],
icon: {
type: 'Image' as 'Image',