]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blobdiff - server/models/video/video.ts
Add Podcast RSS feeds (#5487)
[github/Chocobozzz/PeerTube.git] / server / models / video / video.ts
index 56cc45cfeda4d8a7b370cb669db023ae9b420230..8e3af62a4504a078249028c91b24dc9d468e16b6 100644 (file)
@@ -1,9 +1,11 @@
 import Bluebird from 'bluebird'
 import { remove } from 'fs-extra'
 import { maxBy, minBy } from 'lodash'
-import { join } from 'path'
 import { FindOptions, Includeable, IncludeOptions, Op, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize'
 import {
+  AfterCreate,
+  AfterDestroy,
+  AfterUpdate,
   AllowNull,
   BeforeDestroy,
   BelongsTo,
@@ -25,16 +27,19 @@ import {
   UpdatedAt
 } from 'sequelize-typescript'
 import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video'
+import { InternalEventEmitter } from '@server/lib/internal-event-emitter'
 import { LiveManager } from '@server/lib/live/live-manager'
 import { removeHLSFileObjectStorageByFilename, removeHLSObjectStorage, removeWebTorrentObjectStorage } from '@server/lib/object-storage'
 import { tracer } from '@server/lib/opentelemetry/tracing'
 import { getHLSDirectory, getHLSRedundancyDirectory, getHlsResolutionPlaylistFilename } from '@server/lib/paths'
+import { Hooks } from '@server/lib/plugins/hooks'
 import { VideoPathManager } from '@server/lib/video-path-manager'
 import { isVideoInPrivateDirectory } from '@server/lib/video-privacy'
 import { getServerActor } from '@server/models/application/application'
-import { ModelCache } from '@server/models/model-cache'
+import { ModelCache } from '@server/models/shared/model-cache'
 import { buildVideoEmbedPath, buildVideoWatchPath, pick } from '@shared/core-utils'
-import { ffprobePromise, getAudioStream, hasAudioStream, uuidToShort } from '@shared/extra-utils'
+import { uuidToShort } from '@shared/extra-utils'
+import { ffprobePromise, getAudioStream, getVideoStreamDimensionsInfo, getVideoStreamFPS, hasAudioStream } from '@shared/ffmpeg'
 import {
   ResultList,
   ThumbnailType,
@@ -62,10 +67,9 @@ import {
   isVideoStateValid,
   isVideoSupportValid
 } from '../../helpers/custom-validators/videos'
-import { getVideoStreamDimensionsInfo } from '../../helpers/ffmpeg'
 import { logger } from '../../helpers/logger'
 import { CONFIG } from '../../initializers/config'
-import { ACTIVITY_PUB, API_VERSION, CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, STATIC_PATHS, WEBSERVER } from '../../initializers/constants'
+import { ACTIVITY_PUB, API_VERSION, CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
 import { sendDeleteVideo } from '../../lib/activitypub/send'
 import {
   MChannel,
@@ -103,10 +107,9 @@ import { VideoRedundancyModel } from '../redundancy/video-redundancy'
 import { ServerModel } from '../server/server'
 import { TrackerModel } from '../server/tracker'
 import { VideoTrackerModel } from '../server/video-tracker'
-import { setAsUpdated } from '../shared'
+import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, setAsUpdated, throwIfNotValid } from '../shared'
 import { UserModel } from '../user/user'
 import { UserVideoHistoryModel } from '../user/user-video-history'
-import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils'
 import { VideoViewModel } from '../view/video-view'
 import {
   videoFilesModelToFormattedJSON,
@@ -706,6 +709,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
       name: 'videoId',
       allowNull: false
     },
+    hooks: true,
     onDelete: 'cascade'
   })
   VideoLive: VideoLiveModel
@@ -739,8 +743,23 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
   })
   VideoJobInfo: VideoJobInfoModel
 
+  @AfterCreate
+  static notifyCreate (video: MVideo) {
+    InternalEventEmitter.Instance.emit('video-created', { video })
+  }
+
+  @AfterUpdate
+  static notifyUpdate (video: MVideo) {
+    InternalEventEmitter.Instance.emit('video-updated', { video })
+  }
+
+  @AfterDestroy
+  static notifyDestroy (video: MVideo) {
+    InternalEventEmitter.Instance.emit('video-deleted', { video })
+  }
+
   @BeforeDestroy
-  static async sendDelete (instance: MVideoAccountLight, options) {
+  static async sendDelete (instance: MVideoAccountLight, options: { transaction: Transaction }) {
     if (!instance.isOwned()) return undefined
 
     // Lazy load channels
@@ -797,7 +816,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
 
     logger.info('Stopping live of video %s after video deletion.', instance.uuid)
 
-    LiveManager.Instance.stopSessionOf(instance.id, null)
+    LiveManager.Instance.stopSessionOf(instance.uuid, null)
   }
 
   @BeforeDestroy
@@ -1085,6 +1104,8 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
     countVideos?: boolean
 
     search?: string
+
+    excludeAlreadyWatched?: boolean
   }) {
     VideoModel.throwIfPrivateIncludeWithoutUser(options.include, options.user)
     VideoModel.throwIfPrivacyOneOfWithoutUser(options.privacyOneOf, options.user)
@@ -1123,7 +1144,8 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
         'historyOfUser',
         'hasHLSFiles',
         'hasWebtorrentFiles',
-        'search'
+        'search',
+        'excludeAlreadyWatched'
       ]),
 
       serverAccountIdForBlock: serverActor.Account.id,
@@ -1169,6 +1191,8 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
     durationMin?: number // seconds
     durationMax?: number // seconds
     uuids?: string[]
+
+    excludeAlreadyWatched?: boolean
   }) {
     VideoModel.throwIfPrivateIncludeWithoutUser(options.include, options.user)
     VideoModel.throwIfPrivacyOneOfWithoutUser(options.privacyOneOf, options.user)
@@ -1202,7 +1226,8 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
         'hasWebtorrentFiles',
         'uuids',
         'search',
-        'displayOnlyForFollower'
+        'displayOnlyForFollower',
+        'excludeAlreadyWatched'
       ]),
       serverAccountIdForBlock: serverActor.Account.id
     }
@@ -1679,15 +1704,14 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
     const thumbnail = this.getMiniature()
     if (!thumbnail) return null
 
-    return join(STATIC_PATHS.THUMBNAILS, thumbnail.filename)
+    return thumbnail.getLocalStaticPath()
   }
 
   getPreviewStaticPath () {
     const preview = this.getPreview()
     if (!preview) return null
 
-    // We use a local cache, so specify our cache endpoint instead of potential remote URL
-    return join(LAZY_STATIC_PATHS.PREVIEWS, preview.filename)
+    return preview.getLocalStaticPath()
   }
 
   toFormattedJSON (this: MVideoFormattable, options?: VideoFormattingJSONOptions): Video {
@@ -1698,24 +1722,40 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
     return videoModelToFormattedDetailsJSON(this)
   }
 
-  getFormattedVideoFilesJSON (includeMagnet = true): VideoFile[] {
+  getFormattedWebVideoFilesJSON (includeMagnet = true): VideoFile[] {
+    return videoFilesModelToFormattedJSON(this, this.VideoFiles, { includeMagnet })
+  }
+
+  getFormattedHLSVideoFilesJSON (includeMagnet = true): VideoFile[] {
+    let acc: VideoFile[] = []
+
+    for (const p of this.VideoStreamingPlaylists) {
+      acc = acc.concat(videoFilesModelToFormattedJSON(this, p.VideoFiles, { includeMagnet }))
+    }
+
+    return acc
+  }
+
+  getFormattedAllVideoFilesJSON (includeMagnet = true): VideoFile[] {
     let files: VideoFile[] = []
 
     if (Array.isArray(this.VideoFiles)) {
-      const result = videoFilesModelToFormattedJSON(this, this.VideoFiles, { includeMagnet })
-      files = files.concat(result)
+      files = files.concat(this.getFormattedWebVideoFilesJSON(includeMagnet))
     }
 
-    for (const p of (this.VideoStreamingPlaylists || [])) {
-      const result = videoFilesModelToFormattedJSON(this, p.VideoFiles, { includeMagnet })
-      files = files.concat(result)
+    if (Array.isArray(this.VideoStreamingPlaylists)) {
+      files = files.concat(this.getFormattedHLSVideoFilesJSON(includeMagnet))
     }
 
     return files
   }
 
-  toActivityPubObject (this: MVideoAP): VideoObject {
-    return videoModelToActivityPubObject(this)
+  toActivityPubObject (this: MVideoAP): Promise<VideoObject> {
+    return Hooks.wrapObject(
+      videoModelToActivityPubObject(this),
+      'filter:activity-pub.video.json-ld.build.result',
+      { video: this }
+    )
   }
 
   getTruncatedDescription () {
@@ -1752,10 +1792,12 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
 
       const { audioStream } = await getAudioStream(originalFilePath, probe)
       const hasAudio = await hasAudioStream(originalFilePath, probe)
+      const fps = await getVideoStreamFPS(originalFilePath, probe)
 
       return {
         audioStream,
         hasAudio,
+        fps,
 
         ...await getVideoStreamDimensionsInfo(originalFilePath, probe)
       }
@@ -1871,7 +1913,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
   }
 
   setAsRefreshed (transaction?: Transaction) {
-    return setAsUpdated('video', this.id, transaction)
+    return setAsUpdated({ sequelize: this.sequelize, table: 'video', id: this.id, transaction })
   }
 
   // ---------------------------------------------------------------------------