]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blobdiff - server/models/redundancy/video-redundancy.ts
Add ability to search available plugins
[github/Chocobozzz/PeerTube.git] / server / models / redundancy / video-redundancy.ts
index 48ec772069ed37a02811ca3fce7f805021c95e94..eb2222256319a7c15ea939d03490fdae354e84c0 100644 (file)
@@ -1,6 +1,6 @@
 import {
-  AfterDestroy,
   AllowNull,
+  BeforeDestroy,
   BelongsTo,
   Column,
   CreatedAt,
@@ -9,46 +9,58 @@ import {
   Is,
   Model,
   Scopes,
-  Sequelize,
   Table,
   UpdatedAt
 } from 'sequelize-typescript'
 import { ActorModel } from '../activitypub/actor'
-import { throwIfNotValid } from '../utils'
+import { getVideoSort, parseAggregateResult, throwIfNotValid } from '../utils'
 import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc'
-import { CONSTRAINTS_FIELDS, VIDEO_EXT_MIMETYPE } from '../../initializers'
+import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers/constants'
 import { VideoFileModel } from '../video/video-file'
-import { isDateValid } from '../../helpers/custom-validators/misc'
 import { getServerActor } from '../../helpers/utils'
 import { VideoModel } from '../video/video'
 import { VideoRedundancyStrategy } from '../../../shared/models/redundancy'
 import { logger } from '../../helpers/logger'
-import { CacheFileObject } from '../../../shared'
+import { CacheFileObject, VideoPrivacy } from '../../../shared'
 import { VideoChannelModel } from '../video/video-channel'
 import { ServerModel } from '../server/server'
 import { sample } from 'lodash'
 import { isTestInstance } from '../../helpers/core-utils'
+import * as Bluebird from 'bluebird'
+import { col, FindOptions, fn, literal, Op, Transaction } from 'sequelize'
+import { VideoStreamingPlaylistModel } from '../video/video-streaming-playlist'
+import { CONFIG } from '../../initializers/config'
 
 export enum ScopeNames {
   WITH_VIDEO = 'WITH_VIDEO'
 }
 
-@Scopes({
+@Scopes(() => ({
   [ ScopeNames.WITH_VIDEO ]: {
     include: [
       {
-        model: () => VideoFileModel,
-        required: true,
+        model: VideoFileModel,
+        required: false,
         include: [
           {
-            model: () => VideoModel,
+            model: VideoModel,
+            required: true
+          }
+        ]
+      },
+      {
+        model: VideoStreamingPlaylistModel,
+        required: false,
+        include: [
+          {
+            model: VideoModel,
             required: true
           }
         ]
       }
     ]
   }
-})
+}))
 
 @Table({
   tableName: 'videoRedundancy',
@@ -97,12 +109,24 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
 
   @BelongsTo(() => VideoFileModel, {
     foreignKey: {
-      allowNull: false
+      allowNull: true
     },
     onDelete: 'cascade'
   })
   VideoFile: VideoFileModel
 
+  @ForeignKey(() => VideoStreamingPlaylistModel)
+  @Column
+  videoStreamingPlaylistId: number
+
+  @BelongsTo(() => VideoStreamingPlaylistModel, {
+    foreignKey: {
+      allowNull: true
+    },
+    onDelete: 'cascade'
+  })
+  VideoStreamingPlaylist: VideoStreamingPlaylistModel
+
   @ForeignKey(() => ActorModel)
   @Column
   actorId: number
@@ -115,19 +139,39 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
   })
   Actor: ActorModel
 
-  @AfterDestroy
-  static removeFilesAndSendDelete (instance: VideoRedundancyModel) {
-    // Not us
-    if (!instance.strategy) return
+  @BeforeDestroy
+  static async removeFile (instance: VideoRedundancyModel) {
+    if (!instance.isOwned()) return
+
+    if (instance.videoFileId) {
+      const videoFile = await VideoFileModel.loadWithVideo(instance.videoFileId)
+
+      const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}`
+      logger.info('Removing duplicated video file %s.', logIdentifier)
+
+      videoFile.Video.removeFile(videoFile, true)
+               .catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err }))
+    }
+
+    if (instance.videoStreamingPlaylistId) {
+      const videoStreamingPlaylist = await VideoStreamingPlaylistModel.loadWithVideo(instance.videoStreamingPlaylistId)
+
+      const videoUUID = videoStreamingPlaylist.Video.uuid
+      logger.info('Removing duplicated video streaming playlist %s.', videoUUID)
 
-    logger.info('Removing video file %s-.', instance.VideoFile.Video.uuid, instance.VideoFile.resolution)
+      videoStreamingPlaylist.Video.removeStreamingPlaylist(true)
+               .catch(err => logger.error('Cannot delete video streaming playlist files of %s.', videoUUID, { err }))
+    }
 
-    return instance.VideoFile.Video.removeFile(instance.VideoFile)
+    return undefined
   }
 
-  static loadByFileId (videoFileId: number) {
+  static async loadLocalByFileId (videoFileId: number) {
+    const actor = await getServerActor()
+
     const query = {
       where: {
+        actorId: actor.id,
         videoFileId
       }
     }
@@ -135,32 +179,209 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
     return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query)
   }
 
-  static loadByUrl (url: string) {
+  static async loadLocalByStreamingPlaylistId (videoStreamingPlaylistId: number) {
+    const actor = await getServerActor()
+
     const query = {
       where: {
-        url
+        actorId: actor.id,
+        videoStreamingPlaylistId
       }
     }
 
+    return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query)
+  }
+
+  static loadByUrl (url: string, transaction?: Transaction) {
+    const query = {
+      where: {
+        url
+      },
+      transaction
+    }
+
+    return VideoRedundancyModel.findOne(query)
+  }
+
+  static async isLocalByVideoUUIDExists (uuid: string) {
+    const actor = await getServerActor()
+
+    const query = {
+      raw: true,
+      attributes: [ 'id' ],
+      where: {
+        actorId: actor.id
+      },
+      include: [
+        {
+          attributes: [ ],
+          model: VideoFileModel,
+          required: true,
+          include: [
+            {
+              attributes: [ ],
+              model: VideoModel,
+              required: true,
+              where: {
+                uuid
+              }
+            }
+          ]
+        }
+      ]
+    }
+
     return VideoRedundancyModel.findOne(query)
+      .then(r => !!r)
+  }
+
+  static async getVideoSample (p: Bluebird<VideoModel[]>) {
+    const rows = await p
+    const ids = rows.map(r => r.id)
+    const id = sample(ids)
+
+    return VideoModel.loadWithFiles(id, undefined, !isTestInstance())
   }
 
   static async findMostViewToDuplicate (randomizedFactor: number) {
     // On VideoModel!
     const query = {
-      logging: !isTestInstance(),
+      attributes: [ 'id', 'views' ],
       limit: randomizedFactor,
-      order: [ [ 'views', 'DESC' ] ],
+      order: getVideoSort('-views'),
+      where: {
+        privacy: VideoPrivacy.PUBLIC
+      },
+      include: [
+        await VideoRedundancyModel.buildVideoFileForDuplication(),
+        VideoRedundancyModel.buildServerRedundancyInclude()
+      ]
+    }
+
+    return VideoRedundancyModel.getVideoSample(VideoModel.unscoped().findAll(query))
+  }
+
+  static async findTrendingToDuplicate (randomizedFactor: number) {
+    // On VideoModel!
+    const query = {
+      attributes: [ 'id', 'views' ],
+      subQuery: false,
+      group: 'VideoModel.id',
+      limit: randomizedFactor,
+      order: getVideoSort('-trending'),
+      where: {
+        privacy: VideoPrivacy.PUBLIC
+      },
+      include: [
+        await VideoRedundancyModel.buildVideoFileForDuplication(),
+        VideoRedundancyModel.buildServerRedundancyInclude(),
+
+        VideoModel.buildTrendingQuery(CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS)
+      ]
+    }
+
+    return VideoRedundancyModel.getVideoSample(VideoModel.unscoped().findAll(query))
+  }
+
+  static async findRecentlyAddedToDuplicate (randomizedFactor: number, minViews: number) {
+    // On VideoModel!
+    const query = {
+      attributes: [ 'id', 'publishedAt' ],
+      limit: randomizedFactor,
+      order: getVideoSort('-publishedAt'),
+      where: {
+        privacy: VideoPrivacy.PUBLIC,
+        views: {
+          [ Op.gte ]: minViews
+        }
+      },
+      include: [
+        await VideoRedundancyModel.buildVideoFileForDuplication(),
+        VideoRedundancyModel.buildServerRedundancyInclude()
+      ]
+    }
+
+    return VideoRedundancyModel.getVideoSample(VideoModel.unscoped().findAll(query))
+  }
+
+  static async loadOldestLocalThatAlreadyExpired (strategy: VideoRedundancyStrategy, expiresAfterMs: number) {
+    const expiredDate = new Date()
+    expiredDate.setMilliseconds(expiredDate.getMilliseconds() - expiresAfterMs)
+
+    const actor = await getServerActor()
+
+    const query = {
+      where: {
+        actorId: actor.id,
+        strategy,
+        createdAt: {
+          [ Op.lt ]: expiredDate
+        }
+      }
+    }
+
+    return VideoRedundancyModel.scope([ ScopeNames.WITH_VIDEO ]).findOne(query)
+  }
+
+  static async getTotalDuplicated (strategy: VideoRedundancyStrategy) {
+    const actor = await getServerActor()
+
+    const query: FindOptions = {
       include: [
         {
-          model: VideoFileModel.unscoped(),
+          attributes: [],
+          model: VideoRedundancyModel,
           required: true,
           where: {
-            id: {
-              [ Sequelize.Op.notIn ]: await VideoRedundancyModel.buildExcludeIn()
-            }
+            actorId: actor.id,
+            strategy
           }
+        }
+      ]
+    }
+
+    return VideoFileModel.aggregate('size', 'SUM', query)
+      .then(result => parseAggregateResult(result))
+  }
+
+  static async listLocalExpired () {
+    const actor = await getServerActor()
+
+    const query = {
+      where: {
+        actorId: actor.id,
+        expiresOn: {
+          [ Op.lt ]: new Date()
+        }
+      }
+    }
+
+    return VideoRedundancyModel.scope([ ScopeNames.WITH_VIDEO ]).findAll(query)
+  }
+
+  static async listRemoteExpired () {
+    const actor = await getServerActor()
+
+    const query = {
+      where: {
+        actorId: {
+          [Op.ne]: actor.id
         },
+        expiresOn: {
+          [ Op.lt ]: new Date()
+        }
+      }
+    }
+
+    return VideoRedundancyModel.scope([ ScopeNames.WITH_VIDEO ]).findAll(query)
+  }
+
+  static async listLocalOfServer (serverId: number) {
+    const actor = await getServerActor()
+    const buildVideoInclude = () => ({
+      model: VideoModel,
+      required: true,
+      include: [
         {
           attributes: [],
           model: VideoChannelModel.unscoped(),
@@ -170,57 +391,93 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
               attributes: [],
               model: ActorModel.unscoped(),
               required: true,
-              include: [
-                {
-                  attributes: [],
-                  model: ServerModel.unscoped(),
-                  required: true,
-                  where: {
-                    redundancyAllowed: true
-                  }
-                }
-              ]
+              where: {
+                serverId
+              }
             }
           ]
         }
       ]
-    }
+    })
 
-    const rows = await VideoModel.unscoped().findAll(query)
+    const query = {
+      where: {
+        actorId: actor.id
+      },
+      include: [
+        {
+          model: VideoFileModel,
+          required: false,
+          include: [ buildVideoInclude() ]
+        },
+        {
+          model: VideoStreamingPlaylistModel,
+          required: false,
+          include: [ buildVideoInclude() ]
+        }
+      ]
+    }
 
-    return sample(rows)
+    return VideoRedundancyModel.findAll(query)
   }
 
-  static async getVideoFiles (strategy: VideoRedundancyStrategy) {
+  static async getStats (strategy: VideoRedundancyStrategy) {
     const actor = await getServerActor()
 
-    const queryVideoFiles = {
-      logging: !isTestInstance(),
+    const query: FindOptions = {
+      raw: true,
+      attributes: [
+        [ fn('COALESCE', fn('SUM', col('VideoFile.size')), '0'), 'totalUsed' ],
+        [ fn('COUNT', fn('DISTINCT', col('videoId'))), 'totalVideos' ],
+        [ fn('COUNT', col('videoFileId')), 'totalVideoFiles' ]
+      ],
       where: {
-        actorId: actor.id,
-        strategy
-      }
+        strategy,
+        actorId: actor.id
+      },
+      include: [
+        {
+          attributes: [],
+          model: VideoFileModel,
+          required: true
+        }
+      ]
     }
 
-    return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO)
-                               .findAll(queryVideoFiles)
+    return VideoRedundancyModel.findOne(query)
+      .then((r: any) => ({
+        totalUsed: parseAggregateResult(r.totalUsed),
+        totalVideos: r.totalVideos,
+        totalVideoFiles: r.totalVideoFiles
+      }))
   }
 
-  static listAllExpired () {
-    const query = {
-      logging: !isTestInstance(),
-      where: {
-        expiresOn: {
-          [Sequelize.Op.lt]: new Date()
-        }
-      }
-    }
+  getVideo () {
+    if (this.VideoFile) return this.VideoFile.Video
 
-    return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO)
-                               .findAll(query)
+    return this.VideoStreamingPlaylist.Video
+  }
+
+  isOwned () {
+    return !!this.strategy
   }
 
   toActivityPubObject (): CacheFileObject {
+    if (this.VideoStreamingPlaylist) {
+      return {
+        id: this.url,
+        type: 'CacheFile' as 'CacheFile',
+        object: this.VideoStreamingPlaylist.Video.url,
+        expires: this.expiresOn.toISOString(),
+        url: {
+          type: 'Link',
+          mimeType: 'application/x-mpegURL',
+          mediaType: 'application/x-mpegURL',
+          href: this.fileUrl
+        }
+      }
+    }
+
     return {
       id: this.url,
       type: 'CacheFile' as 'CacheFile',
@@ -228,7 +485,8 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
       expires: this.expiresOn.toISOString(),
       url: {
         type: 'Link',
-        mimeType: VIDEO_EXT_MIMETYPE[ this.VideoFile.extname ] as any,
+        mimeType: MIMETYPES.VIDEO.EXT_MIMETYPE[ this.VideoFile.extname ] as any,
+        mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[ this.VideoFile.extname ] as any,
         href: this.fileUrl,
         height: this.VideoFile.resolution,
         size: this.VideoFile.size,
@@ -237,13 +495,50 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
     }
   }
 
-  private static async buildExcludeIn () {
+  // Don't include video files we already duplicated
+  private static async buildVideoFileForDuplication () {
     const actor = await getServerActor()
 
-    return Sequelize.literal(
+    const notIn = literal(
       '(' +
-        `SELECT "videoFileId" FROM "videoRedundancy" WHERE "actorId" = ${actor.id} AND "expiresOn" >= NOW()` +
+        `SELECT "videoFileId" FROM "videoRedundancy" WHERE "actorId" = ${actor.id} AND "videoFileId" IS NOT NULL` +
       ')'
     )
+
+    return {
+      attributes: [],
+      model: VideoFileModel.unscoped(),
+      required: true,
+      where: {
+        id: {
+          [ Op.notIn ]: notIn
+        }
+      }
+    }
+  }
+
+  private static buildServerRedundancyInclude () {
+    return {
+      attributes: [],
+      model: VideoChannelModel.unscoped(),
+      required: true,
+      include: [
+        {
+          attributes: [],
+          model: ActorModel.unscoped(),
+          required: true,
+          include: [
+            {
+              attributes: [],
+              model: ServerModel.unscoped(),
+              required: true,
+              where: {
+                redundancyAllowed: true
+              }
+            }
+          ]
+        }
+      ]
+    }
   }
 }