]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blobdiff - server/models/video/video-playlist.ts
Add ability to save replay of permanent lives
[github/Chocobozzz/PeerTube.git] / server / models / video / video-playlist.ts
index 72ba474b40bb80c626b28679983fb532fba715dd..8fb3d5f153539ac910174b3506985edb3db78dec 100644 (file)
@@ -1,5 +1,5 @@
 import { join } from 'path'
-import { FindOptions, literal, Op, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize'
+import { FindOptions, Includeable, literal, Op, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize'
 import {
   AllowNull,
   BelongsTo,
@@ -17,16 +17,16 @@ import {
   Table,
   UpdatedAt
 } from 'sequelize-typescript'
-import { setAsUpdated } from '@server/helpers/database-utils'
-import { buildUUID, uuidToShort } from '@server/helpers/uuid'
+import { activityPubCollectionPagination } from '@server/lib/activitypub/collection'
 import { MAccountId, MChannelId } from '@server/types/models'
-import { AttributesOnly, buildPlaylistEmbedPath, buildPlaylistWatchPath } from '@shared/core-utils'
+import { buildPlaylistEmbedPath, buildPlaylistWatchPath, pick } from '@shared/core-utils'
+import { buildUUID, uuidToShort } from '@shared/extra-utils'
+import { AttributesOnly } from '@shared/typescript-utils'
 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 { 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,
@@ -53,6 +53,7 @@ import {
 } from '../../types/models/video/video-playlist'
 import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account'
 import { ActorModel } from '../actor/actor'
+import { setAsUpdated } from '../shared'
 import {
   buildServerIdsFollowedBy,
   buildTrigramSearchIndex,
@@ -82,7 +83,10 @@ type AvailableForListOptions = {
   videoChannelId?: number
   listMyPlaylists?: boolean
   search?: string
+  host?: string
+  uuids?: string[]
   withVideos?: boolean
+  forCount?: boolean
 }
 
 function getVideoLengthSelect () {
@@ -141,9 +145,19 @@ function getVideoLengthSelect () {
     ]
   },
   [ScopeNames.AVAILABLE_FOR_LIST]: (options: AvailableForListOptions) => {
+    const whereAnd: WhereOptions[] = []
+
+    const whereServer = options.host && options.host !== WEBSERVER.HOST
+      ? { host: options.host }
+      : undefined
+
     let whereActor: WhereOptions = {}
 
-    const whereAnd: WhereOptions[] = []
+    if (options.host === WEBSERVER.HOST) {
+      whereActor = {
+        [Op.and]: [ { serverId: null } ]
+      }
+    }
 
     if (options.listMyPlaylists !== true) {
       whereAnd.push({
@@ -168,9 +182,7 @@ function getVideoLengthSelect () {
         })
       }
 
-      whereActor = {
-        [Op.or]: whereActorOr
-      }
+      Object.assign(whereActor, { [Op.or]: whereActorOr })
     }
 
     if (options.accountId) {
@@ -191,18 +203,26 @@ function getVideoLengthSelect () {
       })
     }
 
+    if (options.uuids) {
+      whereAnd.push({
+        uuid: {
+          [Op.in]: options.uuids
+        }
+      })
+    }
+
     if (options.withVideos === true) {
       whereAnd.push(
         literal(`(${getVideoLengthSelect()}) != 0`)
       )
     }
 
-    const attributesInclude = []
+    let attributesInclude: any[] = [ literal('0 as similarity') ]
 
     if (options.search) {
       const escapedSearch = VideoPlaylistModel.sequelize.escape(options.search)
       const escapedLikeSearch = VideoPlaylistModel.sequelize.escape('%' + options.search + '%')
-      attributesInclude.push(createSimilarityAttribute('VideoPlaylistModel.name', options.search))
+      attributesInclude = [ createSimilarityAttribute('VideoPlaylistModel.name', options.search) ]
 
       whereAnd.push({
         [Op.or]: [
@@ -220,23 +240,28 @@ function getVideoLengthSelect () {
       [Op.and]: whereAnd
     }
 
+    const include: Includeable[] = [
+      {
+        model: AccountModel.scope({
+          method: [ AccountScopeNames.SUMMARY, { whereActor, whereServer, forCount: options.forCount } as SummaryOptions ]
+        }),
+        required: true
+      }
+    ]
+
+    if (options.forCount !== true) {
+      include.push({
+        model: VideoChannelModel.scope(VideoChannelScopeNames.SUMMARY),
+        required: false
+      })
+    }
+
     return {
       attributes: {
         include: attributesInclude
       },
       where,
-      include: [
-        {
-          model: AccountModel.scope({
-            method: [ AccountScopeNames.SUMMARY, { whereActor } as SummaryOptions ]
-          }),
-          required: true
-        },
-        {
-          model: VideoChannelModel.scope(VideoChannelScopeNames.SUMMARY),
-          required: false
-        }
-      ]
+      include
     } as FindOptions
   }
 }))
@@ -339,17 +364,10 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli
   })
   Thumbnail: ThumbnailModel
 
-  static listForApi (options: {
-    followerActorId: number
+  static listForApi (options: AvailableForListOptions & {
     start: number
     count: number
     sort: string
-    type?: VideoPlaylistType
-    accountId?: number
-    videoChannelId?: number
-    listMyPlaylists?: boolean
-    search?: string
-    withVideos?: boolean // false by default
   }) {
     const query = {
       offset: options.start,
@@ -357,17 +375,24 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli
       order: getPlaylistSort(options.sort)
     }
 
-    const scopes: (string | ScopeOptions)[] = [
+    const commonAvailableForListOptions = pick(options, [
+      'type',
+      'followerActorId',
+      'accountId',
+      'videoChannelId',
+      'listMyPlaylists',
+      'search',
+      'host',
+      'uuids'
+    ])
+
+    const scopesFind: (string | ScopeOptions)[] = [
       {
         method: [
           ScopeNames.AVAILABLE_FOR_LIST,
           {
-            type: options.type,
-            followerActorId: options.followerActorId,
-            accountId: options.accountId,
-            videoChannelId: options.videoChannelId,
-            listMyPlaylists: options.listMyPlaylists,
-            search: options.search,
+            ...commonAvailableForListOptions,
+
             withVideos: options.withVideos || false
           } as AvailableForListOptions
         ]
@@ -376,23 +401,36 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli
       ScopeNames.WITH_THUMBNAIL
     ]
 
-    return VideoPlaylistModel
-      .scope(scopes)
-      .findAndCountAll(query)
-      .then(({ rows, count }) => {
-        return { total: count, data: rows }
-      })
+    const scopesCount: (string | ScopeOptions)[] = [
+      {
+        method: [
+          ScopeNames.AVAILABLE_FOR_LIST,
+
+          {
+            ...commonAvailableForListOptions,
+
+            withVideos: options.withVideos || false,
+            forCount: true
+          } as AvailableForListOptions
+        ]
+      },
+      ScopeNames.WITH_VIDEOS_LENGTH
+    ]
+
+    return Promise.all([
+      VideoPlaylistModel.scope(scopesCount).count(),
+      VideoPlaylistModel.scope(scopesFind).findAll(query)
+    ]).then(([ count, rows ]) => ({ total: count, data: rows }))
   }
 
-  static searchForApi (options: {
-    followerActorId: number
+  static searchForApi (options: Pick<AvailableForListOptions, 'followerActorId' | 'search'| 'host'| 'uuids'> & {
     start: number
     count: number
     sort: string
-    search?: string
   }) {
     return VideoPlaylistModel.listForApi({
       ...options,
+
       type: VideoPlaylistType.REGULAR,
       listMyPlaylists: false,
       withVideos: true
@@ -412,17 +450,24 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli
       Object.assign(where, { videoChannelId: options.channel.id })
     }
 
-    const query = {
-      attributes: [ 'url' ],
-      offset: start,
-      limit: count,
-      where
+    const getQuery = (forCount: boolean) => {
+      return {
+        attributes: forCount === true
+          ? []
+          : [ 'url' ],
+        offset: start,
+        limit: count,
+        where
+      }
     }
 
-    return VideoPlaylistModel.findAndCountAll(query)
-                             .then(({ rows, count }) => {
-                               return { total: count, data: rows.map(p => p.url) }
-                             })
+    return Promise.all([
+      VideoPlaylistModel.count(getQuery(true)),
+      VideoPlaylistModel.findAll(getQuery(false))
+    ]).then(([ total, rows ]) => ({
+      total,
+      data: rows.map(p => p.url)
+    }))
   }
 
   static listPlaylistIdsOf (accountId: number, videoIds: number[]): Promise<MVideoPlaylistIdWithElements[]> {
@@ -672,6 +717,7 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli
           type: 'Playlist' as 'Playlist',
           name: this.name,
           content: this.description,
+          mediaType: 'text/markdown' as 'text/markdown',
           uuid: this.uuid,
           published: this.createdAt.toISOString(),
           updated: this.updatedAt.toISOString(),