]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blobdiff - server/models/video/video-file.ts
Add permanent live support
[github/Chocobozzz/PeerTube.git] / server / models / video / video-file.ts
index cacef0106e71e144a536d80bb79208972491afd5..d48c9f5d4d9a0fccf5b0e2a8a16589ade1dc6cb8 100644 (file)
@@ -10,7 +10,9 @@ import {
   Is,
   Model,
   Table,
-  UpdatedAt
+  UpdatedAt,
+  Scopes,
+  DefaultScope
 } from 'sequelize-typescript'
 import {
   isVideoFileExtnameValid,
@@ -24,10 +26,37 @@ 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 } from '../../initializers/constants'
-import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '../../typings/models/video/video-file'
-import { MStreamingPlaylist, MStreamingPlaylistVideo, MVideo } from '@server/typings/models'
+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: [
@@ -94,8 +123,8 @@ export class VideoFileModel extends Model<VideoFileModel> {
   @Column
   extname: string
 
-  @AllowNull(false)
-  @Is('VideoFileInfohash', value => throwIfNotValid(value, isVideoFileInfoHashValid, 'info hash'))
+  @AllowNull(true)
+  @Is('VideoFileInfohash', value => throwIfNotValid(value, isVideoFileInfoHashValid, 'info hash', true))
   @Column
   infoHash: string
 
@@ -105,6 +134,14 @@ export class VideoFileModel extends Model<VideoFileModel> {
   @Column
   fps: number
 
+  @AllowNull(true)
+  @Column(DataType.JSONB)
+  metadata: any
+
+  @AllowNull(true)
+  @Column
+  metadataUrl: string
+
   @ForeignKey(() => VideoModel)
   @Column
   videoId: number
@@ -138,6 +175,12 @@ export class VideoFileModel extends Model<VideoFileModel> {
   })
   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 = {
@@ -150,17 +193,56 @@ export class VideoFileModel extends Model<VideoFileModel> {
               .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: true
+          required: false,
+          where: whereVideo
+        },
+        {
+          model: VideoStreamingPlaylistModel.unscoped(),
+          required: false,
+          include: [
+            {
+              model: VideoModel.unscoped(),
+              required: true,
+              where: whereVideo
+            }
+          ]
         }
       ]
     }
 
-    return VideoFileModel.findByPk(id, options)
+    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) {
@@ -187,10 +269,11 @@ export class VideoFileModel extends Model<VideoFileModel> {
   }
 
   static getStats () {
-    const query: FindOptions = {
+    const webtorrentFilesQuery: FindOptions = {
       include: [
         {
           attributes: [],
+          required: true,
           model: VideoModel.unscoped(),
           where: {
             remote: false
@@ -199,10 +282,32 @@ export class VideoFileModel extends Model<VideoFileModel> {
       ]
     }
 
-    return VideoFileModel.aggregate('size', 'SUM', query)
-      .then(result => ({
-        totalLocalVideoFilesSize: parseAggregateResult(result)
-      }))
+    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
@@ -229,6 +334,14 @@ export class VideoFileModel extends Model<VideoFileModel> {
     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
 
@@ -239,6 +352,14 @@ export class VideoFileModel extends Model<VideoFileModel> {
     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 &&