]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blobdiff - server/models/video/video-playlist.ts
Add user adminFlags
[github/Chocobozzz/PeerTube.git] / server / models / video / video-playlist.ts
index 397887ebf1a6c793d83e75f27e5711d138c8f8a2..0725b752a9432e22ceacfd54e421bda535d36874 100644 (file)
@@ -17,14 +17,22 @@ import {
 } from 'sequelize-typescript'
 import * as Sequelize from 'sequelize'
 import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
-import { buildServerIdsFollowedBy, buildWhereIdOrUUID, getSort, throwIfNotValid } from '../utils'
+import { buildServerIdsFollowedBy, buildWhereIdOrUUID, getSort, isOutdated, throwIfNotValid } from '../utils'
 import {
   isVideoPlaylistDescriptionValid,
   isVideoPlaylistNameValid,
   isVideoPlaylistPrivacyValid
 } from '../../helpers/custom-validators/video-playlists'
 import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
-import { CONFIG, CONSTRAINTS_FIELDS, STATIC_PATHS, THUMBNAILS_SIZE, VIDEO_PLAYLIST_PRIVACIES } from '../../initializers'
+import {
+  ACTIVITY_PUB,
+  CONSTRAINTS_FIELDS,
+  STATIC_PATHS,
+  THUMBNAILS_SIZE,
+  VIDEO_PLAYLIST_PRIVACIES,
+  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 { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel'
@@ -34,22 +42,27 @@ import { PlaylistObject } from '../../../shared/models/activitypub/objects/playl
 import { activityPubCollectionPagination } from '../../helpers/activitypub'
 import { remove } from 'fs-extra'
 import { logger } from '../../helpers/logger'
+import { VideoPlaylistType } from '../../../shared/models/videos/playlist/video-playlist-type.model'
+import { CONFIG } from '../../initializers/config'
 
 enum ScopeNames {
   AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST',
   WITH_VIDEOS_LENGTH = 'WITH_VIDEOS_LENGTH',
+  WITH_ACCOUNT_AND_CHANNEL_SUMMARY = 'WITH_ACCOUNT_AND_CHANNEL_SUMMARY',
+  WITH_ACCOUNT = 'WITH_ACCOUNT',
   WITH_ACCOUNT_AND_CHANNEL = 'WITH_ACCOUNT_AND_CHANNEL'
 }
 
 type AvailableForListOptions = {
   followerActorId: number
-  accountId?: number,
+  type?: VideoPlaylistType
+  accountId?: number
   videoChannelId?: number
   privateAndUnlisted?: boolean
 }
 
 @Scopes({
-  [ScopeNames.WITH_VIDEOS_LENGTH]: {
+  [ ScopeNames.WITH_VIDEOS_LENGTH ]: {
     attributes: {
       include: [
         [
@@ -59,7 +72,15 @@ type AvailableForListOptions = {
       ]
     }
   },
-  [ScopeNames.WITH_ACCOUNT_AND_CHANNEL]: {
+  [ ScopeNames.WITH_ACCOUNT ]: {
+    include: [
+      {
+        model: () => AccountModel,
+        required: true
+      }
+    ]
+  },
+  [ ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY ]: {
     include: [
       {
         model: () => AccountModel.scope(AccountScopeNames.SUMMARY),
@@ -71,7 +92,19 @@ type AvailableForListOptions = {
       }
     ]
   },
-  [ScopeNames.AVAILABLE_FOR_LIST]: (options: AvailableForListOptions) => {
+  [ ScopeNames.WITH_ACCOUNT_AND_CHANNEL ]: {
+    include: [
+      {
+        model: () => AccountModel,
+        required: true
+      },
+      {
+        model: () => VideoChannelModel,
+        required: false
+      }
+    ]
+  },
+  [ 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 = {
@@ -107,6 +140,12 @@ type AvailableForListOptions = {
       })
     }
 
+    if (options.type) {
+      whereAnd.push({
+        type: options.type
+      })
+    }
+
     const where = {
       [Sequelize.Op.and]: whereAnd
     }
@@ -179,6 +218,11 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
   @Column(DataType.UUID)
   uuid: string
 
+  @AllowNull(false)
+  @Default(VideoPlaylistType.REGULAR)
+  @Column
+  type: VideoPlaylistType
+
   @ForeignKey(() => AccountModel)
   @Column
   ownerAccountId: number
@@ -208,13 +252,10 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
       name: 'videoPlaylistId',
       allowNull: false
     },
-    onDelete: 'cascade'
+    onDelete: 'CASCADE'
   })
   VideoPlaylistElements: VideoPlaylistElementModel[]
 
-  // Calculated field
-  videosLength?: number
-
   @BeforeDestroy
   static async removeFiles (instance: VideoPlaylistModel) {
     logger.info('Removing files of video playlist %s.', instance.url)
@@ -227,6 +268,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
     start: number,
     count: number,
     sort: string,
+    type?: VideoPlaylistType,
     accountId?: number,
     videoChannelId?: number,
     privateAndUnlisted?: boolean
@@ -242,6 +284,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
         method: [
           ScopeNames.AVAILABLE_FOR_LIST,
           {
+            type: options.type,
             followerActorId: options.followerActorId,
             accountId: options.accountId,
             videoChannelId: options.videoChannelId,
@@ -260,13 +303,14 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
       })
   }
 
-  static listUrlsOfForAP (accountId: number, start: number, count: number) {
+  static listPublicUrlsOfForAP (accountId: number, start: number, count: number) {
     const query = {
       attributes: [ 'url' ],
       offset: start,
       limit: count,
       where: {
-        ownerAccountId: accountId
+        ownerAccountId: accountId,
+        privacy: VideoPlaylistPrivacy.PUBLIC
       }
     }
 
@@ -276,6 +320,29 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
                              })
   }
 
+  static listPlaylistIdsOf (accountId: number, videoIds: number[]) {
+    const query = {
+      attributes: [ 'id' ],
+      where: {
+        ownerAccountId: accountId
+      },
+      include: [
+        {
+          attributes: [ 'videoId', 'startTimestamp', 'stopTimestamp' ],
+          model: VideoPlaylistElementModel.unscoped(),
+          where: {
+            videoId: {
+              [Sequelize.Op.any]: videoIds
+            }
+          },
+          required: true
+        }
+      ]
+    }
+
+    return VideoPlaylistModel.findAll(query)
+  }
+
   static doesPlaylistExist (url: string) {
     const query = {
       attributes: [],
@@ -289,7 +356,20 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
       .then(e => !!e)
   }
 
-  static load (id: number | string, transaction: Sequelize.Transaction) {
+  static loadWithAccountAndChannelSummary (id: number | string, transaction: Sequelize.Transaction) {
+    const where = buildWhereIdOrUUID(id)
+
+    const query = {
+      where,
+      transaction
+    }
+
+    return VideoPlaylistModel
+      .scope([ ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY, ScopeNames.WITH_VIDEOS_LENGTH ])
+      .findOne(query)
+  }
+
+  static loadWithAccountAndChannel (id: number | string, transaction: Sequelize.Transaction) {
     const where = buildWhereIdOrUUID(id)
 
     const query = {
@@ -302,10 +382,35 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
       .findOne(query)
   }
 
+  static loadByUrlAndPopulateAccount (url: string) {
+    const query = {
+      where: {
+        url
+      }
+    }
+
+    return VideoPlaylistModel.scope(ScopeNames.WITH_ACCOUNT).findOne(query)
+  }
+
   static getPrivacyLabel (privacy: VideoPlaylistPrivacy) {
     return VIDEO_PLAYLIST_PRIVACIES[privacy] || 'Unknown'
   }
 
+  static getTypeLabel (type: VideoPlaylistType) {
+    return VIDEO_PLAYLIST_TYPES[type] || 'Unknown'
+  }
+
+  static resetPlaylistsOfChannel (videoChannelId: number, transaction: Sequelize.Transaction) {
+    const query = {
+      where: {
+        videoChannelId
+      },
+      transaction
+    }
+
+    return VideoPlaylistModel.update({ privacy: VideoPlaylistPrivacy.PRIVATE, videoChannelId: null }, query)
+  }
+
   getThumbnailName () {
     const extension = '.jpg'
 
@@ -313,7 +418,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
   }
 
   getThumbnailUrl () {
-    return CONFIG.WEBSERVER.URL + STATIC_PATHS.THUMBNAILS + this.getThumbnailName()
+    return WEBSERVER.URL + STATIC_PATHS.THUMBNAILS + this.getThumbnailName()
   }
 
   getThumbnailStaticPath () {
@@ -326,10 +431,22 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
       .catch(err => logger.warn('Cannot delete thumbnail %s.', thumbnailPath, { err }))
   }
 
+  setAsRefreshed () {
+    this.changed('updatedAt', true)
+
+    return this.save()
+  }
+
   isOwned () {
     return this.OwnerAccount.isOwned()
   }
 
+  isOutdated () {
+    if (this.isOwned()) return false
+
+    return isOutdated(this, ACTIVITY_PUB.VIDEO_PLAYLIST_REFRESH_INTERVAL)
+  }
+
   toFormattedJSON (): VideoPlaylist {
     return {
       id: this.id,
@@ -345,7 +462,12 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
 
       thumbnailPath: this.getThumbnailStaticPath(),
 
-      videosLength: this.videosLength,
+      type: {
+        id: this.type,
+        label: VideoPlaylistModel.getTypeLabel(this.type)
+      },
+
+      videosLength: this.get('videosLength'),
 
       createdAt: this.createdAt,
       updatedAt: this.updatedAt,
@@ -355,18 +477,20 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
     }
   }
 
-  toActivityPubObject (): Promise<PlaylistObject> {
+  toActivityPubObject (page: number, t: Sequelize.Transaction): Promise<PlaylistObject> {
     const handler = (start: number, count: number) => {
-      return VideoPlaylistElementModel.listUrlsOfForAP(this.id, start, count)
+      return VideoPlaylistElementModel.listUrlsOfForAP(this.id, start, count, t)
     }
 
-    return activityPubCollectionPagination(this.url, handler, null)
+    return activityPubCollectionPagination(this.url, handler, page)
       .then(o => {
         return Object.assign(o, {
           type: 'Playlist' as 'Playlist',
           name: this.name,
           content: this.description,
           uuid: this.uuid,
+          published: this.createdAt.toISOString(),
+          updated: this.updatedAt.toISOString(),
           attributedTo: this.VideoChannel ? [ this.VideoChannel.Actor.url ] : [],
           icon: {
             type: 'Image' as 'Image',