a68ccaead6
Keep the same behaviour as unlisted videos The frontend is in charge to blur the video element if the nsfw setting is "hide" or "blur"
367 lines
9.2 KiB
TypeScript
367 lines
9.2 KiB
TypeScript
import { AggregateOptions, Op, ScopeOptions, Sequelize, Transaction } from 'sequelize'
|
|
import {
|
|
AllowNull,
|
|
BelongsTo,
|
|
Column,
|
|
CreatedAt,
|
|
DataType,
|
|
Default,
|
|
ForeignKey,
|
|
Is,
|
|
IsInt,
|
|
Min,
|
|
Model,
|
|
Table,
|
|
UpdatedAt
|
|
} from 'sequelize-typescript'
|
|
import validator from 'validator'
|
|
import { MUserAccountId } from '@server/types/models'
|
|
import {
|
|
MVideoPlaylistElement,
|
|
MVideoPlaylistElementAP,
|
|
MVideoPlaylistElementFormattable,
|
|
MVideoPlaylistElementVideoUrlPlaylistPrivacy,
|
|
MVideoPlaylistVideoThumbnail
|
|
} from '@server/types/models/video/video-playlist-element'
|
|
import { forceNumber } from '@shared/core-utils'
|
|
import { AttributesOnly } from '@shared/typescript-utils'
|
|
import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object'
|
|
import { VideoPrivacy } from '../../../shared/models/videos'
|
|
import { VideoPlaylistElement, VideoPlaylistElementType } from '../../../shared/models/videos/playlist/video-playlist-element.model'
|
|
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
|
|
import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
|
|
import { AccountModel } from '../account/account'
|
|
import { getSort, throwIfNotValid } from '../shared'
|
|
import { ForAPIOptions, ScopeNames as VideoScopeNames, VideoModel } from './video'
|
|
import { VideoPlaylistModel } from './video-playlist'
|
|
|
|
@Table({
|
|
tableName: 'videoPlaylistElement',
|
|
indexes: [
|
|
{
|
|
fields: [ 'videoPlaylistId' ]
|
|
},
|
|
{
|
|
fields: [ 'videoId' ]
|
|
},
|
|
{
|
|
fields: [ 'url' ],
|
|
unique: true
|
|
}
|
|
]
|
|
})
|
|
export class VideoPlaylistElementModel extends Model<Partial<AttributesOnly<VideoPlaylistElementModel>>> {
|
|
@CreatedAt
|
|
createdAt: Date
|
|
|
|
@UpdatedAt
|
|
updatedAt: Date
|
|
|
|
@AllowNull(true)
|
|
@Is('VideoPlaylistUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url', true))
|
|
@Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_PLAYLISTS.URL.max))
|
|
url: string
|
|
|
|
@AllowNull(false)
|
|
@Default(1)
|
|
@IsInt
|
|
@Min(1)
|
|
@Column
|
|
position: number
|
|
|
|
@AllowNull(true)
|
|
@IsInt
|
|
@Min(0)
|
|
@Column
|
|
startTimestamp: number
|
|
|
|
@AllowNull(true)
|
|
@IsInt
|
|
@Min(0)
|
|
@Column
|
|
stopTimestamp: number
|
|
|
|
@ForeignKey(() => VideoPlaylistModel)
|
|
@Column
|
|
videoPlaylistId: number
|
|
|
|
@BelongsTo(() => VideoPlaylistModel, {
|
|
foreignKey: {
|
|
allowNull: false
|
|
},
|
|
onDelete: 'CASCADE'
|
|
})
|
|
VideoPlaylist: VideoPlaylistModel
|
|
|
|
@ForeignKey(() => VideoModel)
|
|
@Column
|
|
videoId: number
|
|
|
|
@BelongsTo(() => VideoModel, {
|
|
foreignKey: {
|
|
allowNull: true
|
|
},
|
|
onDelete: 'set null'
|
|
})
|
|
Video: VideoModel
|
|
|
|
static deleteAllOf (videoPlaylistId: number, transaction?: Transaction) {
|
|
const query = {
|
|
where: {
|
|
videoPlaylistId
|
|
},
|
|
transaction
|
|
}
|
|
|
|
return VideoPlaylistElementModel.destroy(query)
|
|
}
|
|
|
|
static listForApi (options: {
|
|
start: number
|
|
count: number
|
|
videoPlaylistId: number
|
|
serverAccount: AccountModel
|
|
user?: MUserAccountId
|
|
}) {
|
|
const accountIds = [ options.serverAccount.id ]
|
|
const videoScope: (ScopeOptions | string)[] = [
|
|
VideoScopeNames.WITH_BLACKLISTED
|
|
]
|
|
|
|
if (options.user) {
|
|
accountIds.push(options.user.Account.id)
|
|
videoScope.push({ method: [ VideoScopeNames.WITH_USER_HISTORY, options.user.id ] })
|
|
}
|
|
|
|
const forApiOptions: ForAPIOptions = { withAccountBlockerIds: accountIds }
|
|
videoScope.push({
|
|
method: [
|
|
VideoScopeNames.FOR_API, forApiOptions
|
|
]
|
|
})
|
|
|
|
const findQuery = {
|
|
offset: options.start,
|
|
limit: options.count,
|
|
order: getSort('position'),
|
|
where: {
|
|
videoPlaylistId: options.videoPlaylistId
|
|
},
|
|
include: [
|
|
{
|
|
model: VideoModel.scope(videoScope),
|
|
required: false
|
|
}
|
|
]
|
|
}
|
|
|
|
const countQuery = {
|
|
where: {
|
|
videoPlaylistId: options.videoPlaylistId
|
|
}
|
|
}
|
|
|
|
return Promise.all([
|
|
VideoPlaylistElementModel.count(countQuery),
|
|
VideoPlaylistElementModel.findAll(findQuery)
|
|
]).then(([ total, data ]) => ({ total, data }))
|
|
}
|
|
|
|
static loadByPlaylistAndVideo (videoPlaylistId: number, videoId: number): Promise<MVideoPlaylistElement> {
|
|
const query = {
|
|
where: {
|
|
videoPlaylistId,
|
|
videoId
|
|
}
|
|
}
|
|
|
|
return VideoPlaylistElementModel.findOne(query)
|
|
}
|
|
|
|
static loadById (playlistElementId: number | string): Promise<MVideoPlaylistElement> {
|
|
return VideoPlaylistElementModel.findByPk(playlistElementId)
|
|
}
|
|
|
|
static loadByPlaylistAndElementIdForAP (
|
|
playlistId: number | string,
|
|
playlistElementId: number
|
|
): Promise<MVideoPlaylistElementVideoUrlPlaylistPrivacy> {
|
|
const playlistWhere = validator.isUUID('' + playlistId)
|
|
? { uuid: playlistId }
|
|
: { id: playlistId }
|
|
|
|
const query = {
|
|
include: [
|
|
{
|
|
attributes: [ 'privacy' ],
|
|
model: VideoPlaylistModel.unscoped(),
|
|
where: playlistWhere
|
|
},
|
|
{
|
|
attributes: [ 'url' ],
|
|
model: VideoModel.unscoped()
|
|
}
|
|
],
|
|
where: {
|
|
id: playlistElementId
|
|
}
|
|
}
|
|
|
|
return VideoPlaylistElementModel.findOne(query)
|
|
}
|
|
|
|
static listUrlsOfForAP (videoPlaylistId: number, start: number, count: number, t?: Transaction) {
|
|
const getQuery = (forCount: boolean) => {
|
|
return {
|
|
attributes: forCount
|
|
? []
|
|
: [ 'url' ],
|
|
offset: start,
|
|
limit: count,
|
|
order: getSort('position'),
|
|
where: {
|
|
videoPlaylistId
|
|
},
|
|
transaction: t
|
|
}
|
|
}
|
|
|
|
return Promise.all([
|
|
VideoPlaylistElementModel.count(getQuery(true)),
|
|
VideoPlaylistElementModel.findAll(getQuery(false))
|
|
]).then(([ total, rows ]) => ({
|
|
total,
|
|
data: rows.map(e => e.url)
|
|
}))
|
|
}
|
|
|
|
static loadFirstElementWithVideoThumbnail (videoPlaylistId: number): Promise<MVideoPlaylistVideoThumbnail> {
|
|
const query = {
|
|
order: getSort('position'),
|
|
where: {
|
|
videoPlaylistId
|
|
},
|
|
include: [
|
|
{
|
|
model: VideoModel.scope(VideoScopeNames.WITH_THUMBNAILS),
|
|
required: true
|
|
}
|
|
]
|
|
}
|
|
|
|
return VideoPlaylistElementModel
|
|
.findOne(query)
|
|
}
|
|
|
|
static getNextPositionOf (videoPlaylistId: number, transaction?: Transaction) {
|
|
const query: AggregateOptions<number> = {
|
|
where: {
|
|
videoPlaylistId
|
|
},
|
|
transaction
|
|
}
|
|
|
|
return VideoPlaylistElementModel.max('position', query)
|
|
.then(position => position ? position + 1 : 1)
|
|
}
|
|
|
|
static reassignPositionOf (options: {
|
|
videoPlaylistId: number
|
|
firstPosition: number
|
|
endPosition: number
|
|
newPosition: number
|
|
transaction?: Transaction
|
|
}) {
|
|
const { videoPlaylistId, firstPosition, endPosition, newPosition, transaction } = options
|
|
|
|
const query = {
|
|
where: {
|
|
videoPlaylistId,
|
|
position: {
|
|
[Op.gte]: firstPosition,
|
|
[Op.lte]: endPosition
|
|
}
|
|
},
|
|
transaction,
|
|
validate: false // We use a literal to update the position
|
|
}
|
|
|
|
const positionQuery = Sequelize.literal(`${forceNumber(newPosition)} + "position" - ${forceNumber(firstPosition)}`)
|
|
return VideoPlaylistElementModel.update({ position: positionQuery }, query)
|
|
}
|
|
|
|
static increasePositionOf (
|
|
videoPlaylistId: number,
|
|
fromPosition: number,
|
|
by = 1,
|
|
transaction?: Transaction
|
|
) {
|
|
const query = {
|
|
where: {
|
|
videoPlaylistId,
|
|
position: {
|
|
[Op.gte]: fromPosition
|
|
}
|
|
},
|
|
transaction
|
|
}
|
|
|
|
return VideoPlaylistElementModel.increment({ position: by }, query)
|
|
}
|
|
|
|
toFormattedJSON (
|
|
this: MVideoPlaylistElementFormattable,
|
|
options: { accountId?: number } = {}
|
|
): VideoPlaylistElement {
|
|
return {
|
|
id: this.id,
|
|
position: this.position,
|
|
startTimestamp: this.startTimestamp,
|
|
stopTimestamp: this.stopTimestamp,
|
|
|
|
type: this.getType(options.accountId),
|
|
|
|
video: this.getVideoElement(options.accountId)
|
|
}
|
|
}
|
|
|
|
getType (this: MVideoPlaylistElementFormattable, accountId?: number) {
|
|
const video = this.Video
|
|
|
|
if (!video) return VideoPlaylistElementType.DELETED
|
|
|
|
// Owned video, don't filter it
|
|
if (accountId && video.VideoChannel.Account.id === accountId) return VideoPlaylistElementType.REGULAR
|
|
|
|
// Internal video?
|
|
if (video.privacy === VideoPrivacy.INTERNAL && accountId) return VideoPlaylistElementType.REGULAR
|
|
|
|
if (video.privacy === VideoPrivacy.PRIVATE || video.privacy === VideoPrivacy.INTERNAL) return VideoPlaylistElementType.PRIVATE
|
|
|
|
if (video.isBlacklisted() || video.isBlocked()) return VideoPlaylistElementType.UNAVAILABLE
|
|
|
|
return VideoPlaylistElementType.REGULAR
|
|
}
|
|
|
|
getVideoElement (this: MVideoPlaylistElementFormattable, accountId?: number) {
|
|
if (!this.Video) return null
|
|
if (this.getType(accountId) !== VideoPlaylistElementType.REGULAR) return null
|
|
|
|
return this.Video.toFormattedJSON()
|
|
}
|
|
|
|
toActivityPubObject (this: MVideoPlaylistElementAP): PlaylistElementObject {
|
|
const base: PlaylistElementObject = {
|
|
id: this.url,
|
|
type: 'PlaylistElement',
|
|
|
|
url: this.Video?.url || null,
|
|
position: this.position
|
|
}
|
|
|
|
if (this.startTimestamp) base.startTimestamp = this.startTimestamp
|
|
if (this.stopTimestamp) base.stopTimestamp = this.stopTimestamp
|
|
|
|
return base
|
|
}
|
|
}
|