]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Use separate queries for video files
authorChocobozzz <me@florianbigard.com>
Thu, 10 Jun 2021 14:57:13 +0000 (16:57 +0200)
committerChocobozzz <me@florianbigard.com>
Fri, 11 Jun 2021 07:31:59 +0000 (09:31 +0200)
server/controllers/api/videos/index.ts
server/models/video/sql/shared/abstract-videos-model-query-builder.ts
server/models/video/sql/shared/abstract-videos-query-builder.ts
server/models/video/sql/shared/video-attributes.ts
server/models/video/sql/shared/video-file-query-builder.ts [new file with mode: 0644]
server/models/video/sql/shared/video-model-builder.ts
server/models/video/sql/video-model-get-query-builder.ts
server/models/video/sql/videos-id-list-query-builder.ts
server/models/video/sql/videos-model-list-query-builder.ts

index 8c6c441448561598bacba05be3a722531f0122f8..35992e99364808f3349be93f8d66c45254738a5f 100644 (file)
@@ -147,7 +147,7 @@ async function getVideo (_req: express.Request, res: express.Response) {
 
   const video = await Hooks.wrapPromiseFun(
     VideoModel.loadForGetAPI,
-    { id: res.locals.onlyVideoWithRights.id, userId },
+    { id: _req.params.id, userId },
     'filter:api.video.get.result'
   )
 
index bdf926cbef240dd79b5df4f4b7e0bde3f0a375b7..8ed207eeaaa84fd4190bf5cdfb2605be1c87ca8f 100644 (file)
@@ -1,19 +1,24 @@
+import validator from 'validator'
 import { AbstractVideosQueryBuilder } from './abstract-videos-query-builder'
 import { VideoAttributes } from './video-attributes'
-import { VideoModelBuilder } from './video-model-builder'
+
+/**
+ *
+ * Abstract builder to create SQL query and fetch video models
+ *
+ */
 
 export class AbstractVideosModelQueryBuilder extends AbstractVideosQueryBuilder {
   protected attributes: { [key: string]: string } = {}
   protected joins: string[] = []
+  protected where: string
 
   protected videoAttributes: VideoAttributes
-  protected videoModelBuilder: VideoModelBuilder
 
-  constructor (private readonly mode: 'list' | 'get') {
+  constructor (protected readonly mode: 'list' | 'get') {
     super()
 
     this.videoAttributes = new VideoAttributes(this.mode)
-    this.videoModelBuilder = new VideoModelBuilder(this.mode, this.videoAttributes)
   }
 
   protected buildSelect () {
@@ -78,21 +83,30 @@ export class AbstractVideosModelQueryBuilder extends AbstractVideosQueryBuilder
     }
   }
 
-  protected includeFiles () {
-    this.joins.push(
-      'LEFT JOIN "videoFile" AS "VideoFiles" ON "VideoFiles"."videoId" = "video"."id"',
+  protected includeWebtorrentFiles (required: boolean) {
+    const joinType = required ? 'INNER' : 'LEFT'
+    this.joins.push(joinType + ' JOIN "videoFile" AS "VideoFiles" ON "VideoFiles"."videoId" = "video"."id"')
+
+    this.attributes = {
+      ...this.attributes,
+
+      ...this.buildAttributesObject('VideoFiles', this.videoAttributes.getFileAttributes())
+    }
+  }
+
+  protected includeStreamingPlaylistFiles (required: boolean) {
+    const joinType = required ? 'INNER' : 'LEFT'
 
-      'LEFT JOIN "videoStreamingPlaylist" AS "VideoStreamingPlaylists" ON "VideoStreamingPlaylists"."videoId" = "video"."id"',
+    this.joins.push(
+      joinType + ' JOIN "videoStreamingPlaylist" AS "VideoStreamingPlaylists" ON "VideoStreamingPlaylists"."videoId" = "video"."id"',
 
-      'LEFT JOIN "videoFile" AS "VideoStreamingPlaylists->VideoFiles" ' +
+      joinType + ' JOIN "videoFile" AS "VideoStreamingPlaylists->VideoFiles" ' +
         'ON "VideoStreamingPlaylists->VideoFiles"."videoStreamingPlaylistId" = "VideoStreamingPlaylists"."id"'
     )
 
     this.attributes = {
       ...this.attributes,
 
-      ...this.buildAttributesObject('VideoFiles', this.videoAttributes.getFileAttributes()),
-
       ...this.buildAttributesObject('VideoStreamingPlaylists', this.videoAttributes.getStreamingPlaylistAttributes()),
       ...this.buildAttributesObject('VideoStreamingPlaylists->VideoFiles', this.videoAttributes.getFileAttributes())
     }
@@ -196,11 +210,8 @@ export class AbstractVideosModelQueryBuilder extends AbstractVideosQueryBuilder
     }
   }
 
-  protected includeRedundancies () {
+  protected includeWebTorrentRedundancies () {
     this.joins.push(
-      'LEFT OUTER JOIN "videoRedundancy" AS "VideoStreamingPlaylists->RedundancyVideos" ' +
-        'ON "VideoStreamingPlaylists"."id" = "VideoStreamingPlaylists->RedundancyVideos"."videoStreamingPlaylistId"',
-
       'LEFT OUTER JOIN "videoRedundancy" AS "VideoFiles->RedundancyVideos" ON ' +
         '"VideoFiles"."id" = "VideoFiles->RedundancyVideos"."videoFileId"'
     )
@@ -208,7 +219,19 @@ export class AbstractVideosModelQueryBuilder extends AbstractVideosQueryBuilder
     this.attributes = {
       ...this.attributes,
 
-      ...this.buildAttributesObject('VideoFiles->RedundancyVideos', this.videoAttributes.getRedundancyAttributes()),
+      ...this.buildAttributesObject('VideoFiles->RedundancyVideos', this.videoAttributes.getRedundancyAttributes())
+    }
+  }
+
+  protected includeStreamingPlaylistRedundancies () {
+    this.joins.push(
+      'LEFT OUTER JOIN "videoRedundancy" AS "VideoStreamingPlaylists->RedundancyVideos" ' +
+        'ON "VideoStreamingPlaylists"."id" = "VideoStreamingPlaylists->RedundancyVideos"."videoStreamingPlaylistId"'
+    )
+
+    this.attributes = {
+      ...this.attributes,
+
       ...this.buildAttributesObject('VideoStreamingPlaylists->RedundancyVideos', this.videoAttributes.getRedundancyAttributes())
     }
   }
@@ -236,4 +259,14 @@ export class AbstractVideosModelQueryBuilder extends AbstractVideosQueryBuilder
 
     return result
   }
+
+  protected whereId (id: string | number) {
+    if (validator.isInt('' + id)) {
+      this.where = 'WHERE "video".id = :videoId'
+    } else {
+      this.where = 'WHERE uuid = :videoId'
+    }
+
+    this.replacements.videoId = id
+  }
 }
index 01694e6912e6ba75f922a77c05d0b86b1e986c53..c1bbeb71ead6131a8096cbd5114a61f4315b210b 100644 (file)
@@ -1,6 +1,12 @@
 import { QueryTypes, Sequelize, Transaction } from 'sequelize'
 import { logger } from '@server/helpers/logger'
 
+/**
+ *
+ * Abstact builder to run video SQL queries
+ *
+ */
+
 export class AbstractVideosQueryBuilder {
   protected sequelize: Sequelize
 
index 1a1650dc79edccd6890aac1b10261f552c45ba88..e21b33c73d6190e9c060981fd6f4f71274ed0b55 100644 (file)
@@ -1,3 +1,9 @@
+
+/**
+ *
+ * Class to build video attributes we want to fetch from the database
+ *
+ */
 export class VideoAttributes {
 
   constructor (readonly mode: 'get' | 'list') {
diff --git a/server/models/video/sql/shared/video-file-query-builder.ts b/server/models/video/sql/shared/video-file-query-builder.ts
new file mode 100644 (file)
index 0000000..29b11a2
--- /dev/null
@@ -0,0 +1,66 @@
+import { Sequelize } from 'sequelize'
+import { BuildVideoGetQueryOptions } from '../video-model-get-query-builder'
+import { AbstractVideosModelQueryBuilder } from './abstract-videos-model-query-builder'
+
+/**
+ *
+ * Fetch files (webtorrent and streaming playlist) according to a video
+ *
+ */
+
+export class VideoFileQueryBuilder extends AbstractVideosModelQueryBuilder {
+  protected attributes: { [key: string]: string }
+  protected joins: string[] = []
+
+  constructor (protected readonly sequelize: Sequelize) {
+    super('get')
+  }
+
+  queryWebTorrentVideos (options: BuildVideoGetQueryOptions) {
+    this.buildWebtorrentFilesQuery(options)
+
+    return this.runQuery(options.transaction, true)
+  }
+
+  queryStreamingPlaylistVideos (options: BuildVideoGetQueryOptions) {
+    this.buildVideoStreamingPlaylistFilesQuery(options)
+
+    return this.runQuery(options.transaction, true)
+  }
+
+  private buildWebtorrentFilesQuery (options: BuildVideoGetQueryOptions) {
+    this.attributes = {
+      '"video"."id"': ''
+    }
+
+    this.includeWebtorrentFiles(true)
+
+    if (options.forGetAPI === true) {
+      this.includeWebTorrentRedundancies()
+    }
+
+    this.whereId(options.id)
+
+    this.query = this.buildQuery()
+  }
+
+  private buildVideoStreamingPlaylistFilesQuery (options: BuildVideoGetQueryOptions) {
+    this.attributes = {
+      '"video"."id"': ''
+    }
+
+    this.includeStreamingPlaylistFiles(true)
+
+    if (options.forGetAPI === true) {
+      this.includeStreamingPlaylistRedundancies()
+    }
+
+    this.whereId(options.id)
+
+    this.query = this.buildQuery()
+  }
+
+  private buildQuery () {
+    return `${this.buildSelect()} FROM "video" ${this.joins.join(' ')} ${this.where}`
+  }
+}
index 9719f6d2ec4228d6b63deffa3f1ba5a1debaabf9..627ea644385f18ba4c1a82035edfa2aff94f42bd 100644 (file)
@@ -17,6 +17,12 @@ import { VideoLiveModel } from '../../video-live'
 import { VideoStreamingPlaylistModel } from '../../video-streaming-playlist'
 import { VideoAttributes } from './video-attributes'
 
+/**
+ *
+ * Build video models from SQL rows
+ *
+ */
+
 export class VideoModelBuilder {
   private videosMemo: { [ id: number ]: VideoModel }
   private videoStreamingPlaylistMemo: { [ id: number ]: VideoStreamingPlaylistModel }
@@ -43,7 +49,7 @@ export class VideoModelBuilder {
 
   }
 
-  buildVideosFromRows (rows: any[]) {
+  buildVideosFromRows (rows: any[], rowsWebtorrentFiles?: any[], rowsStreamingPlaylist?: any[]) {
     this.reinit()
 
     for (const row of rows) {
@@ -53,10 +59,15 @@ export class VideoModelBuilder {
 
       this.setUserHistory(row, videoModel)
       this.addThumbnail(row, videoModel)
-      this.addWebTorrentFile(row, videoModel)
 
-      this.addStreamingPlaylist(row, videoModel)
-      this.addStreamingPlaylistFile(row)
+      if (!rowsWebtorrentFiles) {
+        this.addWebTorrentFile(row, videoModel)
+      }
+
+      if (!rowsStreamingPlaylist) {
+        this.addStreamingPlaylist(row, videoModel)
+        this.addStreamingPlaylistFile(row)
+      }
 
       if (this.mode === 'get') {
         this.addTag(row, videoModel)
@@ -65,16 +76,30 @@ export class VideoModelBuilder {
         this.setScheduleVideoUpdate(row, videoModel)
         this.setLive(row, videoModel)
 
-        if (row.VideoFiles.id) {
+        if (!rowsWebtorrentFiles && row.VideoFiles.id) {
           this.addRedundancy(row.VideoFiles.RedundancyVideos, this.videoFileMemo[row.VideoFiles.id])
         }
 
-        if (row.VideoStreamingPlaylists.id) {
+        if (!rowsStreamingPlaylist && row.VideoStreamingPlaylists.id) {
           this.addRedundancy(row.VideoStreamingPlaylists.RedundancyVideos, this.videoStreamingPlaylistMemo[row.VideoStreamingPlaylists.id])
         }
       }
     }
 
+    for (const row of rowsWebtorrentFiles || []) {
+      const videoModel = this.videosMemo[row.id]
+      this.addWebTorrentFile(row, videoModel)
+      this.addRedundancy(row.VideoFiles.RedundancyVideos, this.videoFileMemo[row.VideoFiles.id])
+    }
+
+    for (const row of rowsStreamingPlaylist || []) {
+      const videoModel = this.videosMemo[row.id]
+
+      this.addStreamingPlaylist(row, videoModel)
+      this.addStreamingPlaylistFile(row)
+      this.addRedundancy(row.VideoStreamingPlaylists.RedundancyVideos, this.videoStreamingPlaylistMemo[row.VideoStreamingPlaylists.id])
+    }
+
     return this.videos
   }
 
index 0a3723e632aa4a68eb7f134803e3244634c761c3..1a921d80218244de6c1e8b2ace23fc16c3661369 100644 (file)
@@ -1,6 +1,14 @@
 import { Sequelize, Transaction } from 'sequelize'
-import validator from 'validator'
 import { AbstractVideosModelQueryBuilder } from './shared/abstract-videos-model-query-builder'
+import { VideoAttributes } from './shared/video-attributes'
+import { VideoFileQueryBuilder } from './shared/video-file-query-builder'
+import { VideoModelBuilder } from './shared/video-model-builder'
+
+/**
+ *
+ * Build a GET SQL query, fetch rows and create the video model
+ *
+ */
 
 export type BuildVideoGetQueryOptions = {
   id: number | string
@@ -9,31 +17,57 @@ export type BuildVideoGetQueryOptions = {
   forGetAPI?: boolean
 }
 
-export class VideosModelGetQueryBuilder extends AbstractVideosModelQueryBuilder {
+export class VideosModelGetQueryBuilder {
+  videoQueryBuilder: VideosModelGetQuerySubBuilder
+  webtorrentFilesQueryBuilder: VideoFileQueryBuilder
+  streamingPlaylistFilesQueryBuilder: VideoFileQueryBuilder
+
+  private readonly videoModelBuilder: VideoModelBuilder
+
+  constructor (protected readonly sequelize: Sequelize) {
+    this.videoQueryBuilder = new VideosModelGetQuerySubBuilder(sequelize)
+    this.webtorrentFilesQueryBuilder = new VideoFileQueryBuilder(sequelize)
+    this.streamingPlaylistFilesQueryBuilder = new VideoFileQueryBuilder(sequelize)
+
+    this.videoModelBuilder = new VideoModelBuilder('get', new VideoAttributes('get'))
+  }
+
+  async queryVideos (options: BuildVideoGetQueryOptions) {
+    const [ videoRows, webtorrentFilesRows, streamingPlaylistFilesRows ] = await Promise.all([
+      this.videoQueryBuilder.queryVideos(options),
+      this.webtorrentFilesQueryBuilder.queryWebTorrentVideos(options),
+      this.streamingPlaylistFilesQueryBuilder.queryStreamingPlaylistVideos(options)
+    ])
+
+    const videos = this.videoModelBuilder.buildVideosFromRows(videoRows, webtorrentFilesRows, streamingPlaylistFilesRows)
+
+    if (videos.length > 1) {
+      throw new Error('Video results is more than ')
+    }
+
+    if (videos.length === 0) return null
+    return videos[0]
+  }
+}
+
+export class VideosModelGetQuerySubBuilder extends AbstractVideosModelQueryBuilder {
   protected attributes: { [key: string]: string }
   protected joins: string[] = []
-  protected where: string
+
+  protected webtorrentFilesQuery: string
+  protected streamingPlaylistFilesQuery: string
 
   constructor (protected readonly sequelize: Sequelize) {
     super('get')
   }
 
   queryVideos (options: BuildVideoGetQueryOptions) {
-    this.buildGetQuery(options)
-
-    return this.runQuery(options.transaction, true).then(rows => {
-      const videos = this.videoModelBuilder.buildVideosFromRows(rows)
-
-      if (videos.length > 1) {
-        throw new Error('Video results is more than ')
-      }
+    this.buildMainGetQuery(options)
 
-      if (videos.length === 0) return null
-      return videos[0]
-    })
+    return this.runQuery(options.transaction, true)
   }
 
-  private buildGetQuery (options: BuildVideoGetQueryOptions) {
+  private buildMainGetQuery (options: BuildVideoGetQueryOptions) {
     this.attributes = {
       '"video".*': ''
     }
@@ -45,8 +79,6 @@ export class VideosModelGetQueryBuilder extends AbstractVideosModelQueryBuilder
 
     this.includeThumbnails()
 
-    this.includeFiles()
-
     this.includeBlacklisted()
 
     this.includeScheduleUpdate()
@@ -59,28 +91,17 @@ export class VideosModelGetQueryBuilder extends AbstractVideosModelQueryBuilder
 
     if (options.forGetAPI === true) {
       this.includeTrackers()
-      this.includeRedundancies()
     }
 
     this.whereId(options.id)
 
-    const select = this.buildSelect()
-    const order = this.buildOrder()
-
-    this.query = `${select} FROM "video" ${this.joins.join(' ')} ${this.where} ${order}`
+    this.query = this.buildQuery()
   }
 
-  private whereId (id: string | number) {
-    if (validator.isInt('' + id)) {
-      this.where = 'WHERE "video".id = :videoId'
-    } else {
-      this.where = 'WHERE uuid = :videoId'
-    }
-
-    this.replacements.videoId = id
-  }
+  private buildQuery () {
+    const order = 'ORDER BY "Tags"."name" ASC'
+    const from = `SELECT * FROM "video" ${this.where} LIMIT 1`
 
-  private buildOrder () {
-    return 'ORDER BY "Tags"."name" ASC'
+    return `${this.buildSelect()} FROM (${from}) AS "video" ${this.joins.join(' ')} ${this.where} ${order}`
   }
 }
index 6e0d97d9ed487908cf1fa387799ce500ed41a351..30b251f0f33ce3123922ca1f6fb143a8dd0ff44b 100644 (file)
@@ -6,6 +6,12 @@ import { MUserAccountId, MUserId } from '@server/types/models'
 import { VideoFilter, VideoPrivacy, VideoState } from '@shared/models'
 import { AbstractVideosQueryBuilder } from './shared/abstract-videos-query-builder'
 
+/**
+ *
+ * Build videos list SQL query to fetch rows
+ *
+ */
+
 export type BuildVideosListQueryOptions = {
   attributes?: string[]
 
index 38b9c91d09f0d9a571bd537a6ec68e329c4246c2..acb76d80a9ef60081e02d640d87303d2dad7f3f0 100644 (file)
@@ -1,7 +1,14 @@
 import { Sequelize } from 'sequelize'
 import { AbstractVideosModelQueryBuilder } from './shared/abstract-videos-model-query-builder'
+import { VideoModelBuilder } from './shared/video-model-builder'
 import { BuildVideosListQueryOptions, VideosIdListQueryBuilder } from './videos-id-list-query-builder'
 
+/**
+ *
+ * Build videos list SQL query and create video models
+ *
+ */
+
 export class VideosModelListQueryBuilder extends AbstractVideosModelQueryBuilder {
   protected attributes: { [key: string]: string }
   protected joins: string[] = []
@@ -9,8 +16,12 @@ export class VideosModelListQueryBuilder extends AbstractVideosModelQueryBuilder
   private innerQuery: string
   private innerSort: string
 
+  private readonly videoModelBuilder: VideoModelBuilder
+
   constructor (protected readonly sequelize: Sequelize) {
     super('list')
+
+    this.videoModelBuilder = new VideoModelBuilder(this.mode, this.videoAttributes)
   }
 
   queryVideos (options: BuildVideosListQueryOptions) {
@@ -41,7 +52,8 @@ export class VideosModelListQueryBuilder extends AbstractVideosModelQueryBuilder
     this.includeThumbnails()
 
     if (options.withFiles) {
-      this.includeFiles()
+      this.includeWebtorrentFiles(false)
+      this.includeStreamingPlaylistFiles(false)
     }
 
     if (options.user) {