123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371 |
- import {
- AllowNull,
- BelongsTo,
- Column,
- CreatedAt,
- DataType,
- Default,
- ForeignKey,
- HasMany,
- Is,
- Model,
- Table,
- UpdatedAt,
- Scopes,
- DefaultScope
- } from 'sequelize-typescript'
- import {
- isVideoFileExtnameValid,
- isVideoFileInfoHashValid,
- isVideoFileResolutionValid,
- isVideoFileSizeValid,
- isVideoFPSResolutionValid
- } from '../../helpers/custom-validators/videos'
- import { parseAggregateResult, throwIfNotValid } from '../utils'
- import { VideoModel } from './video'
- import { VideoRedundancyModel } from '../redundancy/video-redundancy'
- import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
- import { FindOptions, Op, QueryTypes, Transaction } from 'sequelize'
- import { MIMETYPES, MEMOIZE_LENGTH, MEMOIZE_TTL } from '../../initializers/constants'
- import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '../../types/models/video/video-file'
- import { MStreamingPlaylistVideo, MVideo } from '@server/types/models'
- import * as memoizee from 'memoizee'
- import validator from 'validator'
- export enum ScopeNames {
- WITH_VIDEO = 'WITH_VIDEO',
- WITH_METADATA = 'WITH_METADATA'
- }
- @DefaultScope(() => ({
- attributes: {
- exclude: [ 'metadata' ]
- }
- }))
- @Scopes(() => ({
- [ScopeNames.WITH_VIDEO]: {
- include: [
- {
- model: VideoModel.unscoped(),
- required: true
- }
- ]
- },
- [ScopeNames.WITH_METADATA]: {
- attributes: {
- include: [ 'metadata' ]
- }
- }
- }))
- @Table({
- tableName: 'videoFile',
- indexes: [
- {
- fields: [ 'videoId' ],
- where: {
- videoId: {
- [Op.ne]: null
- }
- }
- },
- {
- fields: [ 'videoStreamingPlaylistId' ],
- where: {
- videoStreamingPlaylistId: {
- [Op.ne]: null
- }
- }
- },
- {
- fields: [ 'infoHash' ]
- },
- {
- fields: [ 'videoId', 'resolution', 'fps' ],
- unique: true,
- where: {
- videoId: {
- [Op.ne]: null
- }
- }
- },
- {
- fields: [ 'videoStreamingPlaylistId', 'resolution', 'fps' ],
- unique: true,
- where: {
- videoStreamingPlaylistId: {
- [Op.ne]: null
- }
- }
- }
- ]
- })
- export class VideoFileModel extends Model {
- @CreatedAt
- createdAt: Date
- @UpdatedAt
- updatedAt: Date
- @AllowNull(false)
- @Is('VideoFileResolution', value => throwIfNotValid(value, isVideoFileResolutionValid, 'resolution'))
- @Column
- resolution: number
- @AllowNull(false)
- @Is('VideoFileSize', value => throwIfNotValid(value, isVideoFileSizeValid, 'size'))
- @Column(DataType.BIGINT)
- size: number
- @AllowNull(false)
- @Is('VideoFileExtname', value => throwIfNotValid(value, isVideoFileExtnameValid, 'extname'))
- @Column
- extname: string
- @AllowNull(true)
- @Is('VideoFileInfohash', value => throwIfNotValid(value, isVideoFileInfoHashValid, 'info hash', true))
- @Column
- infoHash: string
- @AllowNull(false)
- @Default(-1)
- @Is('VideoFileFPS', value => throwIfNotValid(value, isVideoFPSResolutionValid, 'fps'))
- @Column
- fps: number
- @AllowNull(true)
- @Column(DataType.JSONB)
- metadata: any
- @AllowNull(true)
- @Column
- metadataUrl: string
- @ForeignKey(() => VideoModel)
- @Column
- videoId: number
- @BelongsTo(() => VideoModel, {
- foreignKey: {
- allowNull: true
- },
- onDelete: 'CASCADE'
- })
- Video: VideoModel
- @ForeignKey(() => VideoStreamingPlaylistModel)
- @Column
- videoStreamingPlaylistId: number
- @BelongsTo(() => VideoStreamingPlaylistModel, {
- foreignKey: {
- allowNull: true
- },
- onDelete: 'CASCADE'
- })
- VideoStreamingPlaylist: VideoStreamingPlaylistModel
- @HasMany(() => VideoRedundancyModel, {
- foreignKey: {
- allowNull: true
- },
- onDelete: 'CASCADE',
- hooks: true
- })
- RedundancyVideos: VideoRedundancyModel[]
- static doesInfohashExistCached = memoizee(VideoFileModel.doesInfohashExist, {
- promise: true,
- max: MEMOIZE_LENGTH.INFO_HASH_EXISTS,
- maxAge: MEMOIZE_TTL.INFO_HASH_EXISTS
- })
- static doesInfohashExist (infoHash: string) {
- const query = 'SELECT 1 FROM "videoFile" WHERE "infoHash" = $infoHash LIMIT 1'
- const options = {
- type: QueryTypes.SELECT as QueryTypes.SELECT,
- bind: { infoHash },
- raw: true
- }
- return VideoModel.sequelize.query(query, options)
- .then(results => results.length === 1)
- }
- static async doesVideoExistForVideoFile (id: number, videoIdOrUUID: number | string) {
- const videoFile = await VideoFileModel.loadWithVideoOrPlaylist(id, videoIdOrUUID)
- return !!videoFile
- }
- static loadWithMetadata (id: number) {
- return VideoFileModel.scope(ScopeNames.WITH_METADATA).findByPk(id)
- }
- static loadWithVideo (id: number) {
- return VideoFileModel.scope(ScopeNames.WITH_VIDEO).findByPk(id)
- }
- static loadWithVideoOrPlaylist (id: number, videoIdOrUUID: number | string) {
- const whereVideo = validator.isUUID(videoIdOrUUID + '')
- ? { uuid: videoIdOrUUID }
- : { id: videoIdOrUUID }
- const options = {
- where: {
- id
- },
- include: [
- {
- model: VideoModel.unscoped(),
- required: false,
- where: whereVideo
- },
- {
- model: VideoStreamingPlaylistModel.unscoped(),
- required: false,
- include: [
- {
- model: VideoModel.unscoped(),
- required: true,
- where: whereVideo
- }
- ]
- }
- ]
- }
- return VideoFileModel.findOne(options)
- .then(file => {
- // We used `required: false` so check we have at least a video or a streaming playlist
- if (!file.Video && !file.VideoStreamingPlaylist) return null
- return file
- })
- }
- static listByStreamingPlaylist (streamingPlaylistId: number, transaction: Transaction) {
- const query = {
- include: [
- {
- model: VideoModel.unscoped(),
- required: true,
- include: [
- {
- model: VideoStreamingPlaylistModel.unscoped(),
- required: true,
- where: {
- id: streamingPlaylistId
- }
- }
- ]
- }
- ],
- transaction
- }
- return VideoFileModel.findAll(query)
- }
- static getStats () {
- const webtorrentFilesQuery: FindOptions = {
- include: [
- {
- attributes: [],
- required: true,
- model: VideoModel.unscoped(),
- where: {
- remote: false
- }
- }
- ]
- }
- const hlsFilesQuery: FindOptions = {
- include: [
- {
- attributes: [],
- required: true,
- model: VideoStreamingPlaylistModel.unscoped(),
- include: [
- {
- attributes: [],
- model: VideoModel.unscoped(),
- required: true,
- where: {
- remote: false
- }
- }
- ]
- }
- ]
- }
- return Promise.all([
- VideoFileModel.aggregate('size', 'SUM', webtorrentFilesQuery),
- VideoFileModel.aggregate('size', 'SUM', hlsFilesQuery)
- ]).then(([ webtorrentResult, hlsResult ]) => ({
- totalLocalVideoFilesSize: parseAggregateResult(webtorrentResult) + parseAggregateResult(hlsResult)
- }))
- }
- // Redefine upsert because sequelize does not use an appropriate where clause in the update query with 2 unique indexes
- static async customUpsert (
- videoFile: MVideoFile,
- mode: 'streaming-playlist' | 'video',
- transaction: Transaction
- ) {
- const baseWhere = {
- fps: videoFile.fps,
- resolution: videoFile.resolution
- }
- if (mode === 'streaming-playlist') Object.assign(baseWhere, { videoStreamingPlaylistId: videoFile.videoStreamingPlaylistId })
- else Object.assign(baseWhere, { videoId: videoFile.videoId })
- const element = await VideoFileModel.findOne({ where: baseWhere, transaction })
- if (!element) return videoFile.save({ transaction })
- for (const k of Object.keys(videoFile.toJSON())) {
- element[k] = videoFile[k]
- }
- return element.save({ transaction })
- }
- static removeHLSFilesOfVideoId (videoStreamingPlaylistId: number) {
- const options = {
- where: { videoStreamingPlaylistId }
- }
- return VideoFileModel.destroy(options)
- }
- getVideoOrStreamingPlaylist (this: MVideoFileVideo | MVideoFileStreamingPlaylistVideo): MVideo | MStreamingPlaylistVideo {
- if (this.videoId) return (this as MVideoFileVideo).Video
- return (this as MVideoFileStreamingPlaylistVideo).VideoStreamingPlaylist
- }
- isAudio () {
- return !!MIMETYPES.AUDIO.EXT_MIMETYPE[this.extname]
- }
- isLive () {
- return this.size === -1
- }
- isHLS () {
- return !!this.videoStreamingPlaylistId
- }
- hasSameUniqueKeysThan (other: MVideoFile) {
- return this.fps === other.fps &&
- this.resolution === other.resolution &&
- (
- (this.videoId !== null && this.videoId === other.videoId) ||
- (this.videoStreamingPlaylistId !== null && this.videoStreamingPlaylistId === other.videoStreamingPlaylistId)
- )
- }
- }
|