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', true))
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 webtorrentFilesQuery: FindOptions = {
277 model: VideoModel.unscoped(),
285 const hlsFilesQuery: FindOptions = {
290 model: VideoStreamingPlaylistModel.unscoped(),
294 model: VideoModel.unscoped(),
306 VideoFileModel.aggregate('size', 'SUM', webtorrentFilesQuery),
307 VideoFileModel.aggregate('size', 'SUM', hlsFilesQuery)
308 ]).then(([ webtorrentResult, hlsResult ]) => ({
309 totalLocalVideoFilesSize: parseAggregateResult(webtorrentResult) + parseAggregateResult(hlsResult)
313 // Redefine upsert because sequelize does not use an appropriate where clause in the update query with 2 unique indexes
314 static async customUpsert (
315 videoFile: MVideoFile,
316 mode: 'streaming-playlist' | 'video',
317 transaction: Transaction
321 resolution: videoFile.resolution
324 if (mode === 'streaming-playlist') Object.assign(baseWhere, { videoStreamingPlaylistId: videoFile.videoStreamingPlaylistId })
325 else Object.assign(baseWhere, { videoId: videoFile.videoId })
327 const element = await VideoFileModel.findOne({ where: baseWhere, transaction })
328 if (!element) return videoFile.save({ transaction })
330 for (const k of Object.keys(videoFile.toJSON())) {
331 element[k] = videoFile[k]
334 return element.save({ transaction })
337 static removeHLSFilesOfVideoId (videoStreamingPlaylistId: number) {
339 where: { videoStreamingPlaylistId }
342 return VideoFileModel.destroy(options)
345 getVideoOrStreamingPlaylist (this: MVideoFileVideo | MVideoFileStreamingPlaylistVideo): MVideo | MStreamingPlaylistVideo {
346 if (this.videoId) return (this as MVideoFileVideo).Video
348 return (this as MVideoFileStreamingPlaylistVideo).VideoStreamingPlaylist
352 return !!MIMETYPES.AUDIO.EXT_MIMETYPE[this.extname]
356 return this.size === -1
360 return !!this.videoStreamingPlaylistId
363 hasSameUniqueKeysThan (other: MVideoFile) {
364 return this.fps === other.fps &&
365 this.resolution === other.resolution &&
367 (this.videoId !== null && this.videoId === other.videoId) ||
368 (this.videoStreamingPlaylistId !== null && this.videoStreamingPlaylistId === other.videoStreamingPlaylistId)