]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blobdiff - server/models/video/video.ts
Stronger model typings
[github/Chocobozzz/PeerTube.git] / server / models / video / video.ts
index ec3d5ddb06cb22fb772fd73092ee0e85f50240e2..7b1f0bc316b698cc8403b8725079cbfa46c72a9f 100644 (file)
@@ -63,6 +63,7 @@ import {
   CONSTRAINTS_FIELDS,
   HLS_REDUNDANCY_DIRECTORY,
   HLS_STREAMING_PLAYLIST_DIRECTORY,
+  LAZY_STATIC_PATHS,
   REMOTE_SCHEME,
   STATIC_DOWNLOAD_PATHS,
   STATIC_PATHS,
@@ -91,7 +92,7 @@ import {
 } from '../utils'
 import { TagModel } from './tag'
 import { VideoAbuseModel } from './video-abuse'
-import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel'
+import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel'
 import { VideoCommentModel } from './video-comment'
 import { VideoFileModel } from './video-file'
 import { VideoShareModel } from './video-share'
@@ -110,7 +111,6 @@ import {
   videoModelToFormattedJSON
 } from './video-format-utils'
 import { UserVideoHistoryModel } from '../account/user-video-history'
-import { UserModel } from '../account/user'
 import { VideoImportModel } from './video-import'
 import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
 import { VideoPlaylistElementModel } from './video-playlist-element'
@@ -118,6 +118,25 @@ import { CONFIG } from '../../initializers/config'
 import { ThumbnailModel } from './thumbnail'
 import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
 import { createTorrentPromise } from '../../helpers/webtorrent'
+import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
+import {
+  MChannel,
+  MChannelActorAccountDefault,
+  MChannelId,
+  MUserAccountId,
+  MUserId,
+  MVideoAccountAllFiles,
+  MVideoAccountLight,
+  MVideoDetails,
+  MVideoFullLight,
+  MVideoIdThumbnail,
+  MVideoThumbnail,
+  MVideoWithAllFiles,
+  MVideoWithBlacklistThumbnailScheduled,
+  MVideoWithRights
+} from '../../typings/models'
+import { MVideoFile, MVideoFileRedundanciesOpt } from '../../typings/models/video/video-file'
+import { MThumbnail } from '../../typings/models/video/thumbnail'
 
 // FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
 const indexes: (ModelIndexesOptions & { where?: WhereOptions })[] = [
@@ -190,26 +209,29 @@ export enum ScopeNames {
   WITH_FILES = 'WITH_FILES',
   WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE',
   WITH_BLACKLISTED = 'WITH_BLACKLISTED',
+  WITH_BLOCKLIST = 'WITH_BLOCKLIST',
   WITH_USER_HISTORY = 'WITH_USER_HISTORY',
   WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS',
   WITH_USER_ID = 'WITH_USER_ID',
   WITH_THUMBNAILS = 'WITH_THUMBNAILS'
 }
 
-type ForAPIOptions = {
-  ids: number[]
+export type ForAPIOptions = {
+  ids?: number[]
 
   videoPlaylistId?: number
 
   withFiles?: boolean
+
+  withAccountBlockerIds?: number[]
 }
 
-type AvailableForListIDsOptions = {
+export type AvailableForListIDsOptions = {
   serverAccountId: number
   followerActorId: number
   includeLocalVideos: boolean
 
-  withoutId?: boolean
+  attributesType?: 'none' | 'id' | 'all'
 
   filter?: VideoFilter
   categoryOneOf?: number[]
@@ -227,8 +249,8 @@ type AvailableForListIDsOptions = {
   videoPlaylistId?: number
 
   trendingDays?: number
-  user?: UserModel,
-  historyOfUser?: UserModel
+  user?: MUserAccountId
+  historyOfUser?: MUserId
 
   baseWhere?: WhereOptions[]
 }
@@ -236,14 +258,16 @@ type AvailableForListIDsOptions = {
 @Scopes(() => ({
   [ ScopeNames.FOR_API ]: (options: ForAPIOptions) => {
     const query: FindOptions = {
-      where: {
-        id: {
-          [ Op.in ]: options.ids // FIXME: sequelize ANY seems broken
-        }
-      },
       include: [
         {
-          model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, true ] }),
+          model: VideoChannelModel.scope({
+            method: [
+              VideoChannelScopeNames.SUMMARY, {
+                withAccount: true,
+                withAccountBlockerIds: options.withAccountBlockerIds
+              } as SummaryOptions
+            ]
+          }),
           required: true
         },
         {
@@ -254,6 +278,14 @@ type AvailableForListIDsOptions = {
       ]
     }
 
+    if (options.ids) {
+      query.where = {
+        id: {
+          [ Op.in ]: options.ids // FIXME: sequelize ANY seems broken
+        }
+      }
+    }
+
     if (options.withFiles === true) {
       query.include.push({
         model: VideoFileModel.unscoped(),
@@ -278,10 +310,14 @@ type AvailableForListIDsOptions = {
 
     const query: FindOptions = {
       raw: true,
-      attributes: options.withoutId === true ? [] : [ 'id' ],
       include: []
     }
 
+    const attributesType = options.attributesType || 'id'
+
+    if (attributesType === 'id') query.attributes = [ 'id' ]
+    else if (attributesType === 'none') query.attributes = [ ]
+
     whereAnd.push({
       id: {
         [ Op.notIn ]: Sequelize.literal(
@@ -290,17 +326,19 @@ type AvailableForListIDsOptions = {
       }
     })
 
-    whereAnd.push({
-      channelId: {
-        [ Op.notIn ]: Sequelize.literal(
-          '(' +
-            'SELECT id FROM "videoChannel" WHERE "accountId" IN (' +
-              buildBlockedAccountSQL(options.serverAccountId, options.user ? options.user.Account.id : undefined) +
-            ')' +
-          ')'
-        )
-      }
-    })
+    if (options.serverAccountId) {
+      whereAnd.push({
+        channelId: {
+          [ Op.notIn ]: Sequelize.literal(
+            '(' +
+              'SELECT id FROM "videoChannel" WHERE "accountId" IN (' +
+                buildBlockedAccountSQL(options.serverAccountId, options.user ? options.user.Account.id : undefined) +
+              ')' +
+            ')'
+          )
+        }
+      })
+    }
 
     // Only list public/published videos
     if (!options.filter || options.filter !== 'all-local') {
@@ -527,6 +565,9 @@ type AvailableForListIDsOptions = {
     }
 
     return query
+  },
+  [ScopeNames.WITH_BLOCKLIST]: {
+
   },
   [ ScopeNames.WITH_THUMBNAILS ]: {
     include: [
@@ -610,7 +651,7 @@ type AvailableForListIDsOptions = {
   [ ScopeNames.WITH_BLACKLISTED ]: {
     include: [
       {
-        attributes: [ 'id', 'reason' ],
+        attributes: [ 'id', 'reason', 'unfederated' ],
         model: VideoBlacklistModel,
         required: false
       }
@@ -845,9 +886,9 @@ export class VideoModel extends Model<VideoModel> {
   @HasMany(() => VideoPlaylistElementModel, {
     foreignKey: {
       name: 'videoId',
-      allowNull: false
+      allowNull: true
     },
-    onDelete: 'cascade'
+    onDelete: 'set null'
   })
   VideoPlaylistElements: VideoPlaylistElementModel[]
 
@@ -965,18 +1006,16 @@ export class VideoModel extends Model<VideoModel> {
   VideoCaptions: VideoCaptionModel[]
 
   @BeforeDestroy
-  static async sendDelete (instance: VideoModel, options) {
+  static async sendDelete (instance: MVideoAccountLight, options) {
     if (instance.isOwned()) {
       if (!instance.VideoChannel) {
         instance.VideoChannel = await instance.$get('VideoChannel', {
           include: [
-            {
-              model: AccountModel,
-              include: [ ActorModel ]
-            }
+            ActorModel,
+            AccountModel
           ],
           transaction: options.transaction
-        }) as VideoChannelModel
+        }) as MChannelActorAccountDefault
       }
 
       return sendDeleteVideo(instance, options.transaction)
@@ -1015,7 +1054,7 @@ export class VideoModel extends Model<VideoModel> {
     return undefined
   }
 
-  static listLocal () {
+  static listLocal (): Bluebird<MVideoWithAllFiles[]> {
     const query = {
       where: {
         remote: false
@@ -1135,7 +1174,7 @@ export class VideoModel extends Model<VideoModel> {
     })
   }
 
-  static listUserVideosForApi (accountId: number, start: number, count: number, sort: string, withFiles = false) {
+  static listUserVideosForApi (accountId: number, start: number, count: number, sort: string) {
     function buildBaseQuery (): FindOptions {
       return {
         offset: start,
@@ -1168,19 +1207,12 @@ export class VideoModel extends Model<VideoModel> {
       ScopeNames.WITH_THUMBNAILS
     ]
 
-    if (withFiles === true) {
-      findQuery.include.push({
-        model: VideoFileModel.unscoped(),
-        required: true
-      })
-    }
-
     return Promise.all([
       VideoModel.count(countQuery),
       VideoModel.scope(findScopes).findAll(findQuery)
     ]).then(([ count, rows ]) => {
       return {
-        data: rows,
+        data: rows as MVideoWithBlacklistThumbnailScheduled[],
         total: count
       }
     })
@@ -1204,8 +1236,8 @@ export class VideoModel extends Model<VideoModel> {
     followerActorId?: number
     videoPlaylistId?: number,
     trendingDays?: number,
-    user?: UserModel,
-    historyOfUser?: UserModel
+    user?: MUserAccountId,
+    historyOfUser?: MUserId
   }, countVideos = true) {
     if (options.filter && options.filter === 'all-local' && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) {
       throw new Error('Try to filter all-local but no user has not the see all videos right')
@@ -1270,7 +1302,7 @@ export class VideoModel extends Model<VideoModel> {
     tagsAllOf?: string[]
     durationMin?: number // seconds
     durationMax?: number // seconds
-    user?: UserModel,
+    user?: MUserAccountId,
     filter?: VideoFilter
   }) {
     const whereAnd = []
@@ -1363,7 +1395,7 @@ export class VideoModel extends Model<VideoModel> {
     return VideoModel.getAvailableForApi(query, queryOptions)
   }
 
-  static load (id: number | string, t?: Transaction) {
+  static load (id: number | string, t?: Transaction): Bluebird<MVideoThumbnail> {
     const where = buildWhereIdOrUUID(id)
     const options = {
       where,
@@ -1373,7 +1405,7 @@ export class VideoModel extends Model<VideoModel> {
     return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options)
   }
 
-  static loadWithRights (id: number | string, t?: Transaction) {
+  static loadWithRights (id: number | string, t?: Transaction): Bluebird<MVideoWithRights> {
     const where = buildWhereIdOrUUID(id)
     const options = {
       where,
@@ -1387,7 +1419,7 @@ export class VideoModel extends Model<VideoModel> {
     ]).findOne(options)
   }
 
-  static loadOnlyId (id: number | string, t?: Transaction) {
+  static loadOnlyId (id: number | string, t?: Transaction): Bluebird<MVideoIdThumbnail> {
     const where = buildWhereIdOrUUID(id)
 
     const options = {
@@ -1399,15 +1431,23 @@ export class VideoModel extends Model<VideoModel> {
     return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options)
   }
 
-  static loadWithFiles (id: number, t?: Transaction, logging?: boolean) {
+  static loadWithFiles (id: number | string, t?: Transaction, logging?: boolean): Bluebird<MVideoWithAllFiles> {
+    const where = buildWhereIdOrUUID(id)
+
+    const query = {
+      where,
+      transaction: t,
+      logging
+    }
+
     return VideoModel.scope([
       ScopeNames.WITH_FILES,
       ScopeNames.WITH_STREAMING_PLAYLISTS,
       ScopeNames.WITH_THUMBNAILS
-    ]).findByPk(id, { transaction: t, logging })
+    ]).findOne(query)
   }
 
-  static loadByUUIDWithFile (uuid: string) {
+  static loadByUUID (uuid: string): Bluebird<MVideoThumbnail> {
     const options = {
       where: {
         uuid
@@ -1417,7 +1457,7 @@ export class VideoModel extends Model<VideoModel> {
     return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options)
   }
 
-  static loadByUrl (url: string, transaction?: Transaction) {
+  static loadByUrl (url: string, transaction?: Transaction): Bluebird<MVideoThumbnail> {
     const query: FindOptions = {
       where: {
         url
@@ -1428,7 +1468,7 @@ export class VideoModel extends Model<VideoModel> {
     return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(query)
   }
 
-  static loadByUrlAndPopulateAccount (url: string, transaction?: Transaction) {
+  static loadByUrlAndPopulateAccount (url: string, transaction?: Transaction): Bluebird<MVideoAccountAllFiles> {
     const query: FindOptions = {
       where: {
         url
@@ -1440,11 +1480,12 @@ export class VideoModel extends Model<VideoModel> {
       ScopeNames.WITH_ACCOUNT_DETAILS,
       ScopeNames.WITH_FILES,
       ScopeNames.WITH_STREAMING_PLAYLISTS,
-      ScopeNames.WITH_THUMBNAILS
+      ScopeNames.WITH_THUMBNAILS,
+      ScopeNames.WITH_BLACKLISTED
     ]).findOne(query)
   }
 
-  static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Transaction, userId?: number) {
+  static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Transaction, userId?: number): Bluebird<MVideoFullLight> {
     const where = buildWhereIdOrUUID(id)
 
     const options = {
@@ -1472,7 +1513,12 @@ export class VideoModel extends Model<VideoModel> {
       .findOne(options)
   }
 
-  static loadForGetAPI (id: number | string, t?: Transaction, userId?: number) {
+  static loadForGetAPI (parameters: {
+    id: number | string,
+    t?: Transaction,
+    userId?: number
+  }): Bluebird<MVideoDetails> {
+    const { id, t, userId } = parameters
     const where = buildWhereIdOrUUID(id)
 
     const options = {
@@ -1549,7 +1595,7 @@ export class VideoModel extends Model<VideoModel> {
                      .then(results => results.length === 1)
   }
 
-  static bulkUpdateSupportField (videoChannel: VideoChannelModel, t: Transaction) {
+  static bulkUpdateSupportField (videoChannel: MChannel, t: Transaction) {
     const options = {
       where: {
         channelId: videoChannel.id
@@ -1560,7 +1606,7 @@ export class VideoModel extends Model<VideoModel> {
     return VideoModel.update({ support: videoChannel.support }, options)
   }
 
-  static getAllIdsFromChannel (videoChannel: VideoChannelModel) {
+  static getAllIdsFromChannel (videoChannel: MChannelId): Bluebird<number[]> {
     const query = {
       attributes: [ 'id' ],
       where: {
@@ -1581,7 +1627,7 @@ export class VideoModel extends Model<VideoModel> {
       serverAccountId: serverActor.Account.id,
       followerActorId,
       includeLocalVideos: true,
-      withoutId: true // Don't break aggregation
+      attributesType: 'none' // Don't break aggregation
     }
 
     const query: FindOptions = {
@@ -1710,6 +1756,15 @@ export class VideoModel extends Model<VideoModel> {
     return VIDEO_STATES[ id ] || 'Unknown'
   }
 
+  isBlacklisted () {
+    return !!this.VideoBlacklist
+  }
+
+  isBlocked () {
+    return (this.VideoChannel.Account.Actor.Server && this.VideoChannel.Account.Actor.Server.isBlocked()) ||
+      this.VideoChannel.Account.isBlocked()
+  }
+
   getOriginalFile () {
     if (Array.isArray(this.VideoFiles) === false) return undefined
 
@@ -1717,7 +1772,13 @@ export class VideoModel extends Model<VideoModel> {
     return maxBy(this.VideoFiles, file => file.resolution)
   }
 
-  async addAndSaveThumbnail (thumbnail: ThumbnailModel, transaction: Transaction) {
+  getFile (resolution: number) {
+    if (Array.isArray(this.VideoFiles) === false) return undefined
+
+    return this.VideoFiles.find(f => f.resolution === resolution)
+  }
+
+  async addAndSaveThumbnail (thumbnail: MThumbnail, transaction: Transaction) {
     thumbnail.videoId = this.id
 
     const savedThumbnail = await thumbnail.save({ transaction })
@@ -1730,7 +1791,7 @@ export class VideoModel extends Model<VideoModel> {
     this.Thumbnails.push(savedThumbnail)
   }
 
-  getVideoFilename (videoFile: VideoFileModel) {
+  getVideoFilename (videoFile: MVideoFile) {
     return this.uuid + '-' + videoFile.resolution + videoFile.extname
   }
 
@@ -1754,7 +1815,7 @@ export class VideoModel extends Model<VideoModel> {
     return this.Thumbnails.find(t => t.type === ThumbnailType.PREVIEW)
   }
 
-  getTorrentFileName (videoFile: VideoFileModel) {
+  getTorrentFileName (videoFile: MVideoFile) {
     const extension = '.torrent'
     return this.uuid + '-' + videoFile.resolution + extension
   }
@@ -1763,15 +1824,15 @@ export class VideoModel extends Model<VideoModel> {
     return this.remote === false
   }
 
-  getTorrentFilePath (videoFile: VideoFileModel) {
+  getTorrentFilePath (videoFile: MVideoFile) {
     return join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
   }
 
-  getVideoFilePath (videoFile: VideoFileModel) {
+  getVideoFilePath (videoFile: MVideoFile) {
     return join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile))
   }
 
-  async createTorrentAndSetInfoHash (videoFile: VideoFileModel) {
+  async createTorrentAndSetInfoHash (videoFile: MVideoFile) {
     const options = {
       // Keep the extname, it's used by the client to stream the file inside a web browser
       name: `${this.name} ${videoFile.resolution}p${videoFile.extname}`,
@@ -1814,7 +1875,7 @@ export class VideoModel extends Model<VideoModel> {
     if (!preview) return null
 
     // We use a local cache, so specify our cache endpoint instead of potential remote URL
-    return join(STATIC_PATHS.PREVIEWS, preview.filename)
+    return join(LAZY_STATIC_PATHS.PREVIEWS, preview.filename)
   }
 
   toFormattedJSON (options?: VideoFormattingJSONOptions): Video {
@@ -1850,7 +1911,13 @@ export class VideoModel extends Model<VideoModel> {
     return `/api/${API_VERSION}/videos/${this.uuid}/description`
   }
 
-  removeFile (videoFile: VideoFileModel, isRedundancy = false) {
+  getHLSPlaylist () {
+    if (!this.VideoStreamingPlaylists) return undefined
+
+    return this.VideoStreamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
+  }
+
+  removeFile (videoFile: MVideoFile, isRedundancy = false) {
     const baseDir = isRedundancy ? CONFIG.STORAGE.REDUNDANCY_DIR : CONFIG.STORAGE.VIDEOS_DIR
 
     const filePath = join(baseDir, this.getVideoFilename(videoFile))
@@ -1858,7 +1925,7 @@ export class VideoModel extends Model<VideoModel> {
       .catch(err => logger.warn('Cannot delete file %s.', filePath, { err }))
   }
 
-  removeTorrent (videoFile: VideoFileModel) {
+  removeTorrent (videoFile: MVideoFile) {
     const torrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
     return remove(torrentPath)
       .catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err }))
@@ -1899,7 +1966,7 @@ export class VideoModel extends Model<VideoModel> {
     return { baseUrlHttp, baseUrlWs }
   }
 
-  generateMagnetUri (videoFile: VideoFileModel, baseUrlHttp: string, baseUrlWs: string) {
+  generateMagnetUri (videoFile: MVideoFileRedundanciesOpt, baseUrlHttp: string, baseUrlWs: string) {
     const xs = this.getTorrentUrl(videoFile, baseUrlHttp)
     const announce = this.getTrackerUrls(baseUrlHttp, baseUrlWs)
     let urlList = [ this.getVideoFileUrl(videoFile, baseUrlHttp) ]
@@ -1922,27 +1989,27 @@ export class VideoModel extends Model<VideoModel> {
     return [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ]
   }
 
-  getTorrentUrl (videoFile: VideoFileModel, baseUrlHttp: string) {
+  getTorrentUrl (videoFile: MVideoFile, baseUrlHttp: string) {
     return baseUrlHttp + STATIC_PATHS.TORRENTS + this.getTorrentFileName(videoFile)
   }
 
-  getTorrentDownloadUrl (videoFile: VideoFileModel, baseUrlHttp: string) {
+  getTorrentDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) {
     return baseUrlHttp + STATIC_DOWNLOAD_PATHS.TORRENTS + this.getTorrentFileName(videoFile)
   }
 
-  getVideoFileUrl (videoFile: VideoFileModel, baseUrlHttp: string) {
+  getVideoFileUrl (videoFile: MVideoFile, baseUrlHttp: string) {
     return baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile)
   }
 
-  getVideoRedundancyUrl (videoFile: VideoFileModel, baseUrlHttp: string) {
+  getVideoRedundancyUrl (videoFile: MVideoFile, baseUrlHttp: string) {
     return baseUrlHttp + STATIC_PATHS.REDUNDANCY + this.getVideoFilename(videoFile)
   }
 
-  getVideoFileDownloadUrl (videoFile: VideoFileModel, baseUrlHttp: string) {
+  getVideoFileDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) {
     return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + this.getVideoFilename(videoFile)
   }
 
-  getBandwidthBits (videoFile: VideoFileModel) {
+  getBandwidthBits (videoFile: MVideoFile) {
     return Math.ceil((videoFile.size * 8) / this.duration)
   }
 }