]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blobdiff - server/models/video/video-playlist.ts
Fix live ending banner
[github/Chocobozzz/PeerTube.git] / server / models / video / video-playlist.ts
index 63b4a07153654aeb0f136c62311315f235d9f91a..efe5be36d2824a766436f4c5a4bef7169c253cac 100644 (file)
@@ -1,3 +1,5 @@
+import { join } from 'path'
+import { FindOptions, literal, Op, ScopeOptions, Transaction, WhereOptions } from 'sequelize'
 import {
   AllowNull,
   BelongsTo,
@@ -15,14 +17,20 @@ import {
   Table,
   UpdatedAt
 } from 'sequelize-typescript'
+import { v4 as uuidv4 } from 'uuid'
+import { MAccountId, MChannelId } from '@server/types/models'
+import { ActivityIconObject } from '../../../shared/models/activitypub/objects'
+import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object'
 import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
-import { buildServerIdsFollowedBy, buildWhereIdOrUUID, getSort, isOutdated, throwIfNotValid } from '../utils'
+import { VideoPlaylistType } from '../../../shared/models/videos/playlist/video-playlist-type.model'
+import { VideoPlaylist } from '../../../shared/models/videos/playlist/video-playlist.model'
+import { activityPubCollectionPagination } from '../../helpers/activitypub'
+import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
 import {
   isVideoPlaylistDescriptionValid,
   isVideoPlaylistNameValid,
   isVideoPlaylistPrivacyValid
 } from '../../helpers/custom-validators/video-playlists'
-import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
 import {
   ACTIVITY_PUB,
   CONSTRAINTS_FIELDS,
@@ -32,17 +40,21 @@ import {
   VIDEO_PLAYLIST_TYPES,
   WEBSERVER
 } from '../../initializers/constants'
-import { VideoPlaylist } from '../../../shared/models/videos/playlist/video-playlist.model'
-import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account'
+import { MThumbnail } from '../../types/models/video/thumbnail'
+import {
+  MVideoPlaylistAccountThumbnail,
+  MVideoPlaylistAP,
+  MVideoPlaylistFormattable,
+  MVideoPlaylistFull,
+  MVideoPlaylistFullSummary,
+  MVideoPlaylistIdWithElements
+} from '../../types/models/video/video-playlist'
+import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account'
+import { buildServerIdsFollowedBy, buildWhereIdOrUUID, getPlaylistSort, isOutdated, throwIfNotValid } from '../utils'
+import { ThumbnailModel } from './thumbnail'
 import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel'
-import { join } from 'path'
 import { VideoPlaylistElementModel } from './video-playlist-element'
-import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object'
-import { activityPubCollectionPagination } from '../../helpers/activitypub'
-import { VideoPlaylistType } from '../../../shared/models/videos/playlist/video-playlist-type.model'
-import { ThumbnailModel } from './thumbnail'
-import { ActivityIconObject } from '../../../shared/models/activitypub/objects'
-import { FindOptions, literal, Op, ScopeOptions, Transaction, WhereOptions } from 'sequelize'
+import { ActorModel } from '../activitypub/actor'
 
 enum ScopeNames {
   AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST',
@@ -54,15 +66,16 @@ enum ScopeNames {
 }
 
 type AvailableForListOptions = {
-  followerActorId: number
+  followerActorId?: number
   type?: VideoPlaylistType
   accountId?: number
   videoChannelId?: number
-  privateAndUnlisted?: boolean
+  listMyPlaylists?: boolean
+  search?: string
 }
 
 @Scopes(() => ({
-  [ ScopeNames.WITH_THUMBNAIL ]: {
+  [ScopeNames.WITH_THUMBNAIL]: {
     include: [
       {
         model: ThumbnailModel,
@@ -70,7 +83,7 @@ type AvailableForListOptions = {
       }
     ]
   },
-  [ ScopeNames.WITH_VIDEOS_LENGTH ]: {
+  [ScopeNames.WITH_VIDEOS_LENGTH]: {
     attributes: {
       include: [
         [
@@ -80,7 +93,7 @@ type AvailableForListOptions = {
       ]
     }
   } as FindOptions,
-  [ ScopeNames.WITH_ACCOUNT ]: {
+  [ScopeNames.WITH_ACCOUNT]: {
     include: [
       {
         model: AccountModel,
@@ -88,7 +101,7 @@ type AvailableForListOptions = {
       }
     ]
   },
-  [ ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY ]: {
+  [ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY]: {
     include: [
       {
         model: AccountModel.scope(AccountScopeNames.SUMMARY),
@@ -100,7 +113,7 @@ type AvailableForListOptions = {
       }
     ]
   },
-  [ ScopeNames.WITH_ACCOUNT_AND_CHANNEL ]: {
+  [ScopeNames.WITH_ACCOUNT_AND_CHANNEL]: {
     include: [
       {
         model: AccountModel,
@@ -112,28 +125,37 @@ type AvailableForListOptions = {
       }
     ]
   },
-  [ ScopeNames.AVAILABLE_FOR_LIST ]: (options: AvailableForListOptions) => {
-    // Only list local playlists OR playlists that are on an instance followed by actorId
-    const inQueryInstanceFollow = buildServerIdsFollowedBy(options.followerActorId)
-    const actorWhere = {
-      [ Op.or ]: [
-        {
-          serverId: null
-        },
-        {
-          serverId: {
-            [ Op.in ]: literal(inQueryInstanceFollow)
-          }
-        }
-      ]
-    }
+  [ScopeNames.AVAILABLE_FOR_LIST]: (options: AvailableForListOptions) => {
+    let whereActor: WhereOptions = {}
 
     const whereAnd: WhereOptions[] = []
 
-    if (options.privateAndUnlisted !== true) {
+    if (options.listMyPlaylists !== true) {
       whereAnd.push({
         privacy: VideoPlaylistPrivacy.PUBLIC
       })
+
+      // Only list local playlists
+      const whereActorOr: WhereOptions[] = [
+        {
+          serverId: null
+        }
+      ]
+
+      // … OR playlists that are on an instance followed by actorId
+      if (options.followerActorId) {
+        const inQueryInstanceFollow = buildServerIdsFollowedBy(options.followerActorId)
+
+        whereActorOr.push({
+          serverId: {
+            [Op.in]: literal(inQueryInstanceFollow)
+          }
+        })
+      }
+
+      whereActor = {
+        [Op.or]: whereActorOr
+      }
     }
 
     if (options.accountId) {
@@ -154,19 +176,25 @@ type AvailableForListOptions = {
       })
     }
 
-    const where = {
-      [Op.and]: whereAnd
+    if (options.search) {
+      whereAnd.push({
+        name: {
+          [Op.iLike]: '%' + options.search + '%'
+        }
+      })
     }
 
-    const accountScope = {
-      method: [ AccountScopeNames.SUMMARY, actorWhere ]
+    const where = {
+      [Op.and]: whereAnd
     }
 
     return {
       where,
       include: [
         {
-          model: AccountModel.scope(accountScope),
+          model: AccountModel.scope({
+            method: [ AccountScopeNames.SUMMARY, { whereActor } as SummaryOptions ]
+          }),
           required: true
         },
         {
@@ -193,7 +221,7 @@ type AvailableForListOptions = {
     }
   ]
 })
-export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
+export class VideoPlaylistModel extends Model {
   @CreatedAt
   createdAt: Date
 
@@ -207,7 +235,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
 
   @AllowNull(true)
   @Is('VideoPlaylistDescription', value => throwIfNotValid(value, isVideoPlaylistDescriptionValid, 'description', true))
-  @Column
+  @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_PLAYLISTS.DESCRIPTION.max))
   description: string
 
   @AllowNull(false)
@@ -265,7 +293,6 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
   VideoPlaylistElements: VideoPlaylistElementModel[]
 
   @HasOne(() => ThumbnailModel, {
-
     foreignKey: {
       name: 'videoPlaylistId',
       allowNull: true
@@ -277,18 +304,19 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
 
   static listForApi (options: {
     followerActorId: number
-    start: number,
-    count: number,
-    sort: string,
-    type?: VideoPlaylistType,
-    accountId?: number,
-    videoChannelId?: number,
-    privateAndUnlisted?: boolean
+    start: number
+    count: number
+    sort: string
+    type?: VideoPlaylistType
+    accountId?: number
+    videoChannelId?: number
+    listMyPlaylists?: boolean
+    search?: string
   }) {
     const query = {
       offset: options.start,
       limit: options.count,
-      order: getSort(options.sort)
+      order: getPlaylistSort(options.sort)
     }
 
     const scopes: (string | ScopeOptions)[] = [
@@ -300,7 +328,8 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
             followerActorId: options.followerActorId,
             accountId: options.accountId,
             videoChannelId: options.videoChannelId,
-            privateAndUnlisted: options.privateAndUnlisted
+            listMyPlaylists: options.listMyPlaylists,
+            search: options.search
           } as AvailableForListOptions
         ]
       },
@@ -316,15 +345,24 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
       })
   }
 
-  static listPublicUrlsOfForAP (accountId: number, start: number, count: number) {
+  static listPublicUrlsOfForAP (options: { account?: MAccountId, channel?: MChannelId }, start: number, count: number) {
+    const where = {
+      privacy: VideoPlaylistPrivacy.PUBLIC
+    }
+
+    if (options.account) {
+      Object.assign(where, { ownerAccountId: options.account.id })
+    }
+
+    if (options.channel) {
+      Object.assign(where, { videoChannelId: options.channel.id })
+    }
+
     const query = {
       attributes: [ 'url' ],
       offset: start,
       limit: count,
-      where: {
-        ownerAccountId: accountId,
-        privacy: VideoPlaylistPrivacy.PUBLIC
-      }
+      where
     }
 
     return VideoPlaylistModel.findAndCountAll(query)
@@ -333,7 +371,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
                              })
   }
 
-  static listPlaylistIdsOf (accountId: number, videoIds: number[]) {
+  static listPlaylistIdsOf (accountId: number, videoIds: number[]): Promise<MVideoPlaylistIdWithElements[]> {
     const query = {
       attributes: [ 'id' ],
       where: {
@@ -341,11 +379,11 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
       },
       include: [
         {
-          attributes: [ 'videoId', 'startTimestamp', 'stopTimestamp' ],
+          attributes: [ 'id', 'videoId', 'startTimestamp', 'stopTimestamp' ],
           model: VideoPlaylistElementModel.unscoped(),
           where: {
             videoId: {
-              [Op.in]: videoIds // FIXME: sequelize ANY seems broken
+              [Op.in]: videoIds
             }
           },
           required: true
@@ -358,7 +396,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
 
   static doesPlaylistExist (url: string) {
     const query = {
-      attributes: [],
+      attributes: [ 'id' ],
       where: {
         url
       }
@@ -369,7 +407,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
       .then(e => !!e)
   }
 
-  static loadWithAccountAndChannelSummary (id: number | string, transaction: Transaction) {
+  static loadWithAccountAndChannelSummary (id: number | string, transaction: Transaction): Promise<MVideoPlaylistFullSummary> {
     const where = buildWhereIdOrUUID(id)
 
     const query = {
@@ -382,7 +420,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
       .findOne(query)
   }
 
-  static loadWithAccountAndChannel (id: number | string, transaction: Transaction) {
+  static loadWithAccountAndChannel (id: number | string, transaction: Transaction): Promise<MVideoPlaylistFull> {
     const where = buildWhereIdOrUUID(id)
 
     const query = {
@@ -395,7 +433,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
       .findOne(query)
   }
 
-  static loadByUrlAndPopulateAccount (url: string) {
+  static loadByUrlAndPopulateAccount (url: string): Promise<MVideoPlaylistAccountThumbnail> {
     const query = {
       where: {
         url
@@ -424,7 +462,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
     return VideoPlaylistModel.update({ privacy: VideoPlaylistPrivacy.PRIVATE, videoChannelId: null }, query)
   }
 
-  async setAndSaveThumbnail (thumbnail: ThumbnailModel, t: Transaction) {
+  async setAndSaveThumbnail (thumbnail: MThumbnail, t: Transaction) {
     thumbnail.videoPlaylistId = this.id
 
     this.Thumbnail = await thumbnail.save({ transaction: t })
@@ -434,10 +472,14 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
     return !!this.Thumbnail
   }
 
+  hasGeneratedThumbnail () {
+    return this.hasThumbnail() && this.Thumbnail.automaticallyGenerated === true
+  }
+
   generateThumbnailName () {
     const extension = '.jpg'
 
-    return 'playlist-' + this.uuid + extension
+    return 'playlist-' + uuidv4() + extension
   }
 
   getThumbnailUrl () {
@@ -452,6 +494,41 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
     return join(STATIC_PATHS.THUMBNAILS, this.Thumbnail.filename)
   }
 
+  getWatchUrl () {
+    return WEBSERVER.URL + '/videos/watch/playlist/' + this.uuid
+  }
+
+  getEmbedStaticPath () {
+    return '/video-playlists/embed/' + this.uuid
+  }
+
+  static async getStats () {
+    const totalLocalPlaylists = await VideoPlaylistModel.count({
+      include: [
+        {
+          model: AccountModel,
+          required: true,
+          include: [
+            {
+              model: ActorModel,
+              required: true,
+              where: {
+                serverId: null
+              }
+            }
+          ]
+        }
+      ],
+      where: {
+        privacy: VideoPlaylistPrivacy.PUBLIC
+      }
+    })
+
+    return {
+      totalLocalPlaylists
+    }
+  }
+
   setAsRefreshed () {
     this.changed('updatedAt', true)
 
@@ -468,7 +545,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
     return isOutdated(this, ACTIVITY_PUB.VIDEO_PLAYLIST_REFRESH_INTERVAL)
   }
 
-  toFormattedJSON (): VideoPlaylist {
+  toFormattedJSON (this: MVideoPlaylistFormattable): VideoPlaylist {
     return {
       id: this.id,
       uuid: this.uuid,
@@ -482,6 +559,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
       },
 
       thumbnailPath: this.getThumbnailStaticPath(),
+      embedPath: this.getEmbedStaticPath(),
 
       type: {
         id: this.type,
@@ -494,11 +572,13 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
       updatedAt: this.updatedAt,
 
       ownerAccount: this.OwnerAccount.toFormattedSummaryJSON(),
-      videoChannel: this.VideoChannel ? this.VideoChannel.toFormattedSummaryJSON() : null
+      videoChannel: this.VideoChannel
+        ? this.VideoChannel.toFormattedSummaryJSON()
+        : null
     }
   }
 
-  toActivityPubObject (page: number, t: Transaction): Promise<PlaylistObject> {
+  toActivityPubObject (this: MVideoPlaylistAP, page: number, t: Transaction): Promise<PlaylistObject> {
     const handler = (start: number, count: number) => {
       return VideoPlaylistElementModel.listUrlsOfForAP(this.id, start, count, t)
     }