aboutsummaryrefslogblamecommitdiffhomepage
path: root/server/models/video/video-playlist-element.ts
blob: b832f9768c04a66c0160ac03c47cafc3f8a59e37 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
                                                                                      














                             
                                 
                                                     



                                   
                                               
                              
                                                          
                                                
                                                         





                                                                                                                                    
                                                    

                                                                                  










                                    




                        
                                                                                                          





                 

                                                                                              







































                                                                      
                     
      
                        


                   
                                                                           









                                                   
                               



                               
                         












































                                                                                         
                                                                                                            









                                                   
                                                                                        


                                                                
                                          
                                
                             
                                                            


                                                           









                                               
                                      
         



                             




                                                   
                                                                                                   












                                             

     






                                                        

   
                                                                                                              
















                                                                   

                                                                                 









                                                           




                                       
                             


                                                                                             



                        

                                  

         

                                                                

     
                                                                                                                       
                                                                               




                             
           
                             




                        
                                







                                                                       
















                                                                        






                                                                                                         



                                                                                                                                  

                                                                                               



                                           
                                                                                
                                
                                                                                 



                                       
                                                                              



                                         
                                   








                                                                      
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
  }
}