16 } from 'sequelize-typescript'
18 isVideoFileExtnameValid,
19 isVideoFileInfoHashValid,
20 isVideoFileResolutionValid,
22 isVideoFPSResolutionValid
23 } from '../../helpers/custom-validators/videos'
24 import { parseAggregateResult, throwIfNotValid } from '../utils'
25 import { VideoModel } from './video'
26 import { VideoRedundancyModel } from '../redundancy/video-redundancy'
27 import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
28 import { FindOptions, Op, QueryTypes, Transaction } from 'sequelize'
29 import { MIMETYPES, MEMOIZE_LENGTH, MEMOIZE_TTL } from '../../initializers/constants'
30 import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '../../types/models/video/video-file'
31 import { MStreamingPlaylistVideo, MVideo } from '@server/types/models'
32 import * as memoizee from 'memoizee'
33 import validator from 'validator'
35 export enum ScopeNames {
36 WITH_VIDEO = 'WITH_VIDEO',
37 WITH_METADATA = 'WITH_METADATA'
40 @DefaultScope(() => ({
42 exclude: [ 'metadata' ]
46 [ScopeNames.WITH_VIDEO]: {
49 model: VideoModel.unscoped(),
54 [ScopeNames.WITH_METADATA]: {
56 include: [ 'metadata' ]
61 tableName: 'videoFile',
64 fields: [ 'videoId' ],
72 fields: [ 'videoStreamingPlaylistId' ],
74 videoStreamingPlaylistId: {
81 fields: [ 'infoHash' ]
85 fields: [ 'videoId', 'resolution', 'fps' ],
94 fields: [ 'videoStreamingPlaylistId', 'resolution', 'fps' ],
97 videoStreamingPlaylistId: {
104 export class VideoFileModel extends Model<VideoFileModel> {
112 @Is('VideoFileResolution', value => throwIfNotValid(value, isVideoFileResolutionValid, 'resolution'))
117 @Is('VideoFileSize', value => throwIfNotValid(value, isVideoFileSizeValid, 'size'))
118 @Column(DataType.BIGINT)
122 @Is('VideoFileExtname', value => throwIfNotValid(value, isVideoFileExtnameValid, 'extname'))
127 @Is('VideoFileInfohash', value => throwIfNotValid(value, isVideoFileInfoHashValid, 'info hash'))
133 @Is('VideoFileFPS', value => throwIfNotValid(value, isVideoFPSResolutionValid, 'fps'))
138 @Column(DataType.JSONB)
145 @ForeignKey(() => VideoModel)
149 @BelongsTo(() => VideoModel, {
157 @ForeignKey(() => VideoStreamingPlaylistModel)
159 videoStreamingPlaylistId: number
161 @BelongsTo(() => VideoStreamingPlaylistModel, {
167 VideoStreamingPlaylist: VideoStreamingPlaylistModel
169 @HasMany(() => VideoRedundancyModel, {
176 RedundancyVideos: VideoRedundancyModel[]
178 static doesInfohashExistCached = memoizee(VideoFileModel.doesInfohashExist, {
180 max: MEMOIZE_LENGTH.INFO_HASH_EXISTS,
181 maxAge: MEMOIZE_TTL.INFO_HASH_EXISTS
184 static doesInfohashExist (infoHash: string) {
185 const query = 'SELECT 1 FROM "videoFile" WHERE "infoHash" = $infoHash LIMIT 1'
187 type: QueryTypes.SELECT as QueryTypes.SELECT,
192 return VideoModel.sequelize.query(query, options)
193 .then(results => results.length === 1)
196 static async doesVideoExistForVideoFile (id: number, videoIdOrUUID: number | string) {
197 const videoFile = await VideoFileModel.loadWithVideoOrPlaylist(id, videoIdOrUUID)
202 static loadWithMetadata (id: number) {
203 return VideoFileModel.scope(ScopeNames.WITH_METADATA).findByPk(id)
206 static loadWithVideo (id: number) {
207 return VideoFileModel.scope(ScopeNames.WITH_VIDEO).findByPk(id)
210 static loadWithVideoOrPlaylist (id: number, videoIdOrUUID: number | string) {
211 const whereVideo = validator.isUUID(videoIdOrUUID + '')
212 ? { uuid: videoIdOrUUID }
213 : { id: videoIdOrUUID }
221 model: VideoModel.unscoped(),
226 model: VideoStreamingPlaylistModel.unscoped(),
230 model: VideoModel.unscoped(),
239 return VideoFileModel.findOne(options)
241 // We used `required: false` so check we have at least a video or a streaming playlist
242 if (!file.Video && !file.VideoStreamingPlaylist) return null
248 static listByStreamingPlaylist (streamingPlaylistId: number, transaction: Transaction) {
252 model: VideoModel.unscoped(),
256 model: VideoStreamingPlaylistModel.unscoped(),
259 id: streamingPlaylistId
268 return VideoFileModel.findAll(query)
272 const query: FindOptions = {
276 model: VideoModel.unscoped(),
284 return VideoFileModel.aggregate('size', 'SUM', query)
286 totalLocalVideoFilesSize: parseAggregateResult(result)
290 // Redefine upsert because sequelize does not use an appropriate where clause in the update query with 2 unique indexes
291 static async customUpsert (
292 videoFile: MVideoFile,
293 mode: 'streaming-playlist' | 'video',
294 transaction: Transaction
298 resolution: videoFile.resolution
301 if (mode === 'streaming-playlist') Object.assign(baseWhere, { videoStreamingPlaylistId: videoFile.videoStreamingPlaylistId })
302 else Object.assign(baseWhere, { videoId: videoFile.videoId })
304 const element = await VideoFileModel.findOne({ where: baseWhere, transaction })
305 if (!element) return videoFile.save({ transaction })
307 for (const k of Object.keys(videoFile.toJSON())) {
308 element[k] = videoFile[k]
311 return element.save({ transaction })
314 getVideoOrStreamingPlaylist (this: MVideoFileVideo | MVideoFileStreamingPlaylistVideo): MVideo | MStreamingPlaylistVideo {
315 if (this.videoId) return (this as MVideoFileVideo).Video
317 return (this as MVideoFileStreamingPlaylistVideo).VideoStreamingPlaylist
321 return !!MIMETYPES.AUDIO.EXT_MIMETYPE[this.extname]
324 hasSameUniqueKeysThan (other: MVideoFile) {
325 return this.fps === other.fps &&
326 this.resolution === other.resolution &&
328 (this.videoId !== null && this.videoId === other.videoId) ||
329 (this.videoStreamingPlaylistId !== null && this.videoStreamingPlaylistId === other.videoStreamingPlaylistId)