+ fields: [ 'createdAt' ]
+ },
+ {
+ fields: [ 'duration' ]
+ },
+ {
+ fields: [ 'views' ]
+ },
+ {
+ fields: [ 'likes' ]
+ },
+ {
+ fields: [ 'uuid' ]
+ },
+ {
+ fields: [ 'channelId' ]
+ },
+ {
+ fields: [ 'id', 'privacy' ]
+ },
+ {
+ fields: [ 'url'],
+ unique: true
+ }
+ ]
+})
+export class VideoModel extends Model<VideoModel> {
+
+ @AllowNull(false)
+ @Default(DataType.UUIDV4)
+ @IsUUID(4)
+ @Column(DataType.UUID)
+ uuid: string
+
+ @AllowNull(false)
+ @Is('VideoName', value => throwIfNotValid(value, isVideoNameValid, 'name'))
+ @Column
+ name: string
+
+ @AllowNull(true)
+ @Default(null)
+ @Is('VideoCategory', value => throwIfNotValid(value, isVideoCategoryValid, 'category'))
+ @Column
+ category: number
+
+ @AllowNull(true)
+ @Default(null)
+ @Is('VideoLicence', value => throwIfNotValid(value, isVideoLicenceValid, 'licence'))
+ @Column
+ licence: number
+
+ @AllowNull(true)
+ @Default(null)
+ @Is('VideoLanguage', value => throwIfNotValid(value, isVideoLanguageValid, 'language'))
+ @Column
+ language: number
+
+ @AllowNull(false)
+ @Is('VideoPrivacy', value => throwIfNotValid(value, isVideoPrivacyValid, 'privacy'))
+ @Column
+ privacy: number
+
+ @AllowNull(false)
+ @Is('VideoNSFW', value => throwIfNotValid(value, isBooleanValid, 'NSFW boolean'))
+ @Column
+ nsfw: boolean
+
+ @AllowNull(true)
+ @Default(null)
+ @Is('VideoDescription', value => throwIfNotValid(value, isVideoDescriptionValid, 'description'))
+ @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max))
+ description: string
+
+ @AllowNull(false)
+ @Is('VideoDuration', value => throwIfNotValid(value, isVideoDurationValid, 'duration'))
+ @Column
+ duration: number
+
+ @AllowNull(false)
+ @Default(0)
+ @IsInt
+ @Min(0)
+ @Column
+ views: number
+
+ @AllowNull(false)
+ @Default(0)
+ @IsInt
+ @Min(0)
+ @Column
+ likes: number
+
+ @AllowNull(false)
+ @Default(0)
+ @IsInt
+ @Min(0)
+ @Column
+ dislikes: number
+
+ @AllowNull(false)
+ @Column
+ remote: boolean
+
+ @AllowNull(false)
+ @Is('VideoUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
+ @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max))
+ url: string
+
+ @AllowNull(false)
+ @Column
+ commentsEnabled: boolean
+
+ @CreatedAt
+ createdAt: Date
+
+ @UpdatedAt
+ updatedAt: Date
+
+ @ForeignKey(() => VideoChannelModel)
+ @Column
+ channelId: number
+
+ @BelongsTo(() => VideoChannelModel, {
+ foreignKey: {
+ allowNull: true
+ },
+ onDelete: 'cascade'
+ })
+ VideoChannel: VideoChannelModel
+
+ @BelongsToMany(() => TagModel, {
+ foreignKey: 'videoId',
+ through: () => VideoTagModel,
+ onDelete: 'CASCADE'
+ })
+ Tags: TagModel[]
+
+ @HasMany(() => VideoAbuseModel, {
+ foreignKey: {
+ name: 'videoId',
+ allowNull: false
+ },
+ onDelete: 'cascade'
+ })
+ VideoAbuses: VideoAbuseModel[]
+
+ @HasMany(() => VideoFileModel, {
+ foreignKey: {
+ name: 'videoId',
+ allowNull: false
+ },
+ onDelete: 'cascade'
+ })
+ VideoFiles: VideoFileModel[]
+
+ @HasMany(() => VideoShareModel, {
+ foreignKey: {
+ name: 'videoId',
+ allowNull: false
+ },
+ onDelete: 'cascade'
+ })
+ VideoShares: VideoShareModel[]
+
+ @HasMany(() => AccountVideoRateModel, {
+ foreignKey: {
+ name: 'videoId',
+ allowNull: false
+ },
+ onDelete: 'cascade'
+ })
+ AccountVideoRates: AccountVideoRateModel[]
+
+ @HasMany(() => VideoCommentModel, {
+ foreignKey: {
+ name: 'videoId',
+ allowNull: false
+ },
+ onDelete: 'cascade',
+ hooks: true
+ })
+ VideoComments: VideoCommentModel[]
+
+ @BeforeDestroy
+ static async sendDelete (instance: VideoModel, options) {
+ if (instance.isOwned()) {
+ if (!instance.VideoChannel) {
+ instance.VideoChannel = await instance.$get('VideoChannel', {
+ include: [
+ {
+ model: AccountModel,
+ include: [ ActorModel ]
+ }
+ ],
+ transaction: options.transaction
+ }) as VideoChannelModel
+ }
+
+ logger.debug('Sending delete of video %s.', instance.url)
+
+ return sendDeleteVideo(instance, options.transaction)
+ }
+
+ return undefined
+ }
+
+ @AfterDestroy
+ static async removeFilesAndSendDelete (instance: VideoModel) {
+ const tasks: Promise<any>[] = []
+
+ tasks.push(instance.removeThumbnail())
+
+ if (instance.isOwned()) {
+ if (!Array.isArray(instance.VideoFiles)) {
+ instance.VideoFiles = await instance.$get('VideoFiles') as VideoFileModel[]
+ }
+
+ tasks.push(instance.removePreview())
+
+ // Remove physical files and torrents
+ instance.VideoFiles.forEach(file => {
+ tasks.push(instance.removeFile(file))
+ tasks.push(instance.removeTorrent(file))
+ })
+ }
+
+ return Promise.all(tasks)
+ .catch(err => {
+ logger.error('Some errors when removing files of video %s in after destroy hook.', instance.uuid, err)
+ })
+ }
+
+ static list () {
+ return VideoModel.scope(ScopeNames.WITH_FILES).findAll()
+ }
+
+ static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) {
+ function getRawQuery (select: string) {
+ const queryVideo = 'SELECT ' + select + ' FROM "video" AS "Video" ' +
+ 'INNER JOIN "videoChannel" AS "VideoChannel" ON "VideoChannel"."id" = "Video"."channelId" ' +
+ 'INNER JOIN "account" AS "Account" ON "Account"."id" = "VideoChannel"."accountId" ' +
+ 'WHERE "Account"."actorId" = ' + actorId
+ const queryVideoShare = 'SELECT ' + select + ' FROM "videoShare" AS "VideoShare" ' +
+ 'INNER JOIN "video" AS "Video" ON "Video"."id" = "VideoShare"."videoId" ' +
+ 'WHERE "VideoShare"."actorId" = ' + actorId
+
+ return `(${queryVideo}) UNION (${queryVideoShare})`
+ }
+
+ const rawQuery = getRawQuery('"Video"."id"')
+ const rawCountQuery = getRawQuery('COUNT("Video"."id") as "total"')
+
+ const query = {
+ distinct: true,
+ offset: start,
+ limit: count,
+ order: [ getSort('createdAt'), [ 'Tags', 'name', 'ASC' ] ],
+ where: {
+ id: {
+ [Sequelize.Op.in]: Sequelize.literal('(' + rawQuery + ')')