X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=server%2Fmodels%2Fvideo%2Fvideo.ts;h=8e3af62a4504a078249028c91b24dc9d468e16b6;hb=f89189907bbdff6c4bc6d3460ed9ef4c49515f17;hp=9399712b80d88626558de262c1904a7ec1644391;hpb=508c1b1e9f3b26752a961e945b7fa59b72b30827;p=github%2FChocobozzz%2FPeerTube.git diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 9399712b8..8e3af62a4 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -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 { removeHLSFileObjectStorage, removeHLSObjectStorage, removeWebTorrentObjectStorage } from '@server/lib/object-storage' +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, 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>> { name: 'videoId', allowNull: false }, + hooks: true, onDelete: 'cascade' }) VideoLive: VideoLiveModel @@ -739,8 +743,23 @@ export class VideoModel extends Model>> { }) 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>> { 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>> { 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>> { 'historyOfUser', 'hasHLSFiles', 'hasWebtorrentFiles', - 'search' + 'search', + 'excludeAlreadyWatched' ]), serverAccountIdForBlock: serverActor.Account.id, @@ -1169,6 +1191,8 @@ export class VideoModel extends Model>> { 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>> { 'hasWebtorrentFiles', 'uuids', 'search', - 'displayOnlyForFollower' + 'displayOnlyForFollower', + 'excludeAlreadyWatched' ]), serverAccountIdForBlock: serverActor.Account.id } @@ -1458,6 +1483,12 @@ export class VideoModel extends Model>> { const query = 'SELECT 1 FROM "videoShare" ' + 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' + 'WHERE "actorFollow"."actorId" = $followerActorId AND "actorFollow"."state" = \'accepted\' AND "videoShare"."videoId" = $videoId ' + + 'UNION ' + + 'SELECT 1 FROM "video" ' + + 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + + 'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' + + 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "account"."actorId" ' + + 'WHERE "actorFollow"."actorId" = $followerActorId AND "actorFollow"."state" = \'accepted\' AND "video"."id" = $videoId ' + 'LIMIT 1' const options = { @@ -1673,15 +1704,14 @@ export class VideoModel extends Model>> { 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 { @@ -1692,24 +1722,40 @@ export class VideoModel extends Model>> { 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 { + return Hooks.wrapObject( + videoModelToActivityPubObject(this), + 'filter:activity-pub.video.json-ld.build.result', + { video: this } + ) } getTruncatedDescription () { @@ -1745,9 +1791,13 @@ export class VideoModel extends Model>> { const probe = await ffprobePromise(originalFilePath) 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) } @@ -1830,8 +1880,8 @@ export class VideoModel extends Model>> { await remove(VideoPathManager.Instance.getFSHLSOutputPath(this, resolutionFilename)) if (videoFile.storage === VideoStorage.OBJECT_STORAGE) { - await removeHLSFileObjectStorage(streamingPlaylist.withVideo(this), videoFile.filename) - await removeHLSFileObjectStorage(streamingPlaylist.withVideo(this), resolutionFilename) + await removeHLSFileObjectStorageByFilename(streamingPlaylist.withVideo(this), videoFile.filename) + await removeHLSFileObjectStorageByFilename(streamingPlaylist.withVideo(this), resolutionFilename) } } @@ -1840,7 +1890,7 @@ export class VideoModel extends Model>> { await remove(filePath) if (streamingPlaylist.storage === VideoStorage.OBJECT_STORAGE) { - await removeHLSFileObjectStorage(streamingPlaylist.withVideo(this), filename) + await removeHLSFileObjectStorageByFilename(streamingPlaylist.withVideo(this), filename) } } @@ -1863,7 +1913,7 @@ export class VideoModel extends Model>> { } setAsRefreshed (transaction?: Transaction) { - return setAsUpdated('video', this.id, transaction) + return setAsUpdated({ sequelize: this.sequelize, table: 'video', id: this.id, transaction }) } // ---------------------------------------------------------------------------