]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blobdiff - server/models/video/video.ts
Merge branch 'release/v1.0.0' into develop
[github/Chocobozzz/PeerTube.git] / server / models / video / video.ts
index ce2153f870f0d9e929caafe1192618cbbb8ccb22..6c183933b165452829000d5a65c6dd6dfa5678af 100644 (file)
@@ -27,7 +27,7 @@ import {
   Table,
   UpdatedAt
 } from 'sequelize-typescript'
-import { VideoPrivacy, VideoState } from '../../../shared'
+import { UserRight, VideoPrivacy, VideoState } from '../../../shared'
 import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
 import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos'
 import { VideoFilter } from '../../../shared/models/videos/video-query.type'
@@ -70,7 +70,7 @@ import { AccountVideoRateModel } from '../account/account-video-rate'
 import { ActorModel } from '../activitypub/actor'
 import { AvatarModel } from '../avatar/avatar'
 import { ServerModel } from '../server/server'
-import { buildTrigramSearchIndex, createSimilarityAttribute, getVideoSort, throwIfNotValid } from '../utils'
+import { buildBlockedAccountSQL, buildTrigramSearchIndex, createSimilarityAttribute, getVideoSort, throwIfNotValid } from '../utils'
 import { TagModel } from './tag'
 import { VideoAbuseModel } from './video-abuse'
 import { VideoChannelModel } from './video-channel'
@@ -92,6 +92,8 @@ import {
   videoModelToFormattedJSON
 } from './video-format-utils'
 import * as validator from 'validator'
+import { UserVideoHistoryModel } from '../account/user-video-history'
+import { UserModel } from '../account/user'
 
 // FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
 const indexes: Sequelize.DefineIndexesOptions[] = [
@@ -127,7 +129,8 @@ export enum ScopeNames {
   WITH_TAGS = 'WITH_TAGS',
   WITH_FILES = 'WITH_FILES',
   WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE',
-  WITH_BLACKLISTED = 'WITH_BLACKLISTED'
+  WITH_BLACKLISTED = 'WITH_BLACKLISTED',
+  WITH_USER_HISTORY = 'WITH_USER_HISTORY'
 }
 
 type ForAPIOptions = {
@@ -136,6 +139,7 @@ type ForAPIOptions = {
 }
 
 type AvailableForListIDsOptions = {
+  serverAccountId: number
   actorId: number
   includeLocalVideos: boolean
   filter?: VideoFilter
@@ -149,6 +153,7 @@ type AvailableForListIDsOptions = {
   accountId?: number
   videoChannelId?: number
   trendingDays?: number
+  user?: UserModel
 }
 
 @Scopes({
@@ -234,6 +239,22 @@ type AvailableForListIDsOptions = {
             }
           ]
         },
+        channelId: {
+          [ Sequelize.Op.notIn ]: Sequelize.literal(
+            '(' +
+              'SELECT id FROM "videoChannel" WHERE "accountId" IN (' +
+                buildBlockedAccountSQL(options.serverAccountId, options.user ? options.user.Account.id : undefined) +
+              ')' +
+            ')'
+          )
+        }
+      },
+      include: []
+    }
+
+    // Only list public/published videos
+    if (!options.filter || options.filter !== 'all-local') {
+      const privacyWhere = {
         // Always list public videos
         privacy: VideoPrivacy.PUBLIC,
         // Always list published videos, or videos that are being transcoded but on which we don't want to wait for transcoding
@@ -248,8 +269,9 @@ type AvailableForListIDsOptions = {
             }
           }
         ]
-      },
-      include: []
+      }
+
+      Object.assign(query.where, privacyWhere)
     }
 
     if (options.filter || options.accountId || options.videoChannelId) {
@@ -464,6 +486,8 @@ type AvailableForListIDsOptions = {
     include: [
       {
         model: () => VideoFileModel.unscoped(),
+        // FIXME: typings
+        [ 'separate' as any ]: true, // We may have multiple files, having multiple redundancies so let's separate this join
         required: false,
         include: [
           {
@@ -482,6 +506,20 @@ type AvailableForListIDsOptions = {
         required: false
       }
     ]
+  },
+  [ ScopeNames.WITH_USER_HISTORY ]: (userId: number) => {
+    return {
+      include: [
+        {
+          attributes: [ 'currentTime' ],
+          model: UserVideoHistoryModel.unscoped(),
+          required: false,
+          where: {
+            userId
+          }
+        }
+      ]
+    }
   }
 })
 @Table({
@@ -672,11 +710,19 @@ export class VideoModel extends Model<VideoModel> {
       name: 'videoId',
       allowNull: false
     },
-    onDelete: 'cascade',
-    hooks: true
+    onDelete: 'cascade'
   })
   VideoViews: VideoViewModel[]
 
+  @HasMany(() => UserVideoHistoryModel, {
+    foreignKey: {
+      name: 'videoId',
+      allowNull: false
+    },
+    onDelete: 'cascade'
+  })
+  UserVideoHistories: UserVideoHistoryModel[]
+
   @HasOne(() => ScheduleVideoUpdateModel, {
     foreignKey: {
       name: 'videoId',
@@ -762,6 +808,16 @@ export class VideoModel extends Model<VideoModel> {
     return VideoModel.scope(ScopeNames.WITH_FILES).findAll()
   }
 
+  static listLocal () {
+    const query = {
+      where: {
+        remote: false
+      }
+    }
+
+    return VideoModel.scope(ScopeNames.WITH_FILES).findAll(query)
+  }
+
   static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) {
     function getRawQuery (select: string) {
       const queryVideo = 'SELECT ' + select + ' FROM "video" AS "Video" ' +
@@ -930,8 +986,13 @@ export class VideoModel extends Model<VideoModel> {
     accountId?: number,
     videoChannelId?: number,
     actorId?: number
-    trendingDays?: number
+    trendingDays?: number,
+    user?: UserModel
   }, 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')
+    }
+
     const query: IFindOptions<VideoModel> = {
       offset: options.start,
       limit: options.count,
@@ -945,11 +1006,14 @@ export class VideoModel extends Model<VideoModel> {
       query.group = 'VideoModel.id'
     }
 
+    const serverActor = await getServerActor()
+
     // actorId === null has a meaning, so just check undefined
-    const actorId = options.actorId !== undefined ? options.actorId : (await getServerActor()).id
+    const actorId = options.actorId !== undefined ? options.actorId : serverActor.id
 
     const queryOptions = {
       actorId,
+      serverAccountId: serverActor.Account.id,
       nsfw: options.nsfw,
       categoryOneOf: options.categoryOneOf,
       licenceOneOf: options.licenceOneOf,
@@ -961,6 +1025,7 @@ export class VideoModel extends Model<VideoModel> {
       accountId: options.accountId,
       videoChannelId: options.videoChannelId,
       includeLocalVideos: options.includeLocalVideos,
+      user: options.user,
       trendingDays
     }
 
@@ -983,6 +1048,8 @@ export class VideoModel extends Model<VideoModel> {
     tagsAllOf?: string[]
     durationMin?: number // seconds
     durationMax?: number // seconds
+    user?: UserModel,
+    filter?: VideoFilter
   }) {
     const whereAnd = []
 
@@ -1052,13 +1119,16 @@ export class VideoModel extends Model<VideoModel> {
     const serverActor = await getServerActor()
     const queryOptions = {
       actorId: serverActor.id,
+      serverAccountId: serverActor.Account.id,
       includeLocalVideos: options.includeLocalVideos,
       nsfw: options.nsfw,
       categoryOneOf: options.categoryOneOf,
       licenceOneOf: options.licenceOneOf,
       languageOneOf: options.languageOneOf,
       tagsOneOf: options.tagsOneOf,
-      tagsAllOf: options.tagsAllOf
+      tagsAllOf: options.tagsAllOf,
+      user: options.user,
+      filter: options.filter
     }
 
     return VideoModel.getAvailableForApi(query, queryOptions)
@@ -1103,19 +1173,29 @@ export class VideoModel extends Model<VideoModel> {
       .findOne(options)
   }
 
-  static loadByUrlAndPopulateAccount (url: string, t?: Sequelize.Transaction) {
+  static loadByUrl (url: string, transaction?: Sequelize.Transaction) {
     const query: IFindOptions<VideoModel> = {
       where: {
         url
-      }
+      },
+      transaction
     }
 
-    if (t !== undefined) query.transaction = t
+    return VideoModel.findOne(query)
+  }
+
+  static loadByUrlAndPopulateAccount (url: string, transaction?: Sequelize.Transaction) {
+    const query: IFindOptions<VideoModel> = {
+      where: {
+        url
+      },
+      transaction
+    }
 
     return VideoModel.scope([ ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_FILES ]).findOne(query)
   }
 
-  static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Sequelize.Transaction) {
+  static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Sequelize.Transaction, userId?: number) {
     const where = VideoModel.buildWhereIdOrUUID(id)
 
     const options = {
@@ -1124,14 +1204,20 @@ export class VideoModel extends Model<VideoModel> {
       transaction: t
     }
 
+    const scopes = [
+      ScopeNames.WITH_TAGS,
+      ScopeNames.WITH_BLACKLISTED,
+      ScopeNames.WITH_FILES,
+      ScopeNames.WITH_ACCOUNT_DETAILS,
+      ScopeNames.WITH_SCHEDULED_UPDATE
+    ]
+
+    if (userId) {
+      scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] } as any) // FIXME: typings
+    }
+
     return VideoModel
-      .scope([
-        ScopeNames.WITH_TAGS,
-        ScopeNames.WITH_BLACKLISTED,
-        ScopeNames.WITH_FILES,
-        ScopeNames.WITH_ACCOUNT_DETAILS,
-        ScopeNames.WITH_SCHEDULED_UPDATE
-      ])
+      .scope(scopes)
       .findOne(options)
   }
 
@@ -1169,9 +1255,11 @@ export class VideoModel extends Model<VideoModel> {
 
   // threshold corresponds to how many video the field should have to be returned
   static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) {
-    const actorId = (await getServerActor()).id
+    const serverActor = await getServerActor()
+    const actorId = serverActor.id
 
-    const scopeOptions = {
+    const scopeOptions: AvailableForListIDsOptions = {
+      serverAccountId: serverActor.Account.id,
       actorId,
       includeLocalVideos: true
     }
@@ -1206,7 +1294,7 @@ export class VideoModel extends Model<VideoModel> {
   }
 
   private static buildActorWhereWithFilter (filter?: VideoFilter) {
-    if (filter && filter === 'local') {
+    if (filter && (filter === 'local' || filter === 'all-local')) {
       return {
         serverId: null
       }
@@ -1215,7 +1303,11 @@ export class VideoModel extends Model<VideoModel> {
     return {}
   }
 
-  private static async getAvailableForApi (query: IFindOptions<VideoModel>, options: AvailableForListIDsOptions, countVideos = true) {
+  private static async getAvailableForApi (
+    query: IFindOptions<VideoModel>,
+    options: AvailableForListIDsOptions,
+    countVideos = true
+  ) {
     const idsScope = {
       method: [
         ScopeNames.AVAILABLE_FOR_LIST_IDS, options
@@ -1239,8 +1331,15 @@ export class VideoModel extends Model<VideoModel> {
 
     if (ids.length === 0) return { data: [], total: count }
 
-    const apiScope = {
-      method: [ ScopeNames.FOR_API, { ids, withFiles: options.withFiles } as ForAPIOptions ]
+    // FIXME: typings
+    const apiScope: any[] = [
+      {
+        method: [ ScopeNames.FOR_API, { ids, withFiles: options.withFiles } as ForAPIOptions ]
+      }
+    ]
+
+    if (options.user) {
+      apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.user.id ] })
     }
 
     const secondQuery = {