import * as Bluebird from 'bluebird'
+import { remove } from 'fs-extra'
import { maxBy, minBy, pick } from 'lodash'
import { join } from 'path'
import { FindOptions, IncludeOptions, Op, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize'
Table,
UpdatedAt
} from 'sequelize-typescript'
-import { UserRight, VideoPrivacy, VideoState, ResultList } from '../../../shared'
-import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
+import { buildNSFWFilter } from '@server/helpers/express-utils'
+import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video'
+import { LiveManager } from '@server/lib/live-manager'
+import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
+import { getServerActor } from '@server/models/application/application'
+import { ModelCache } from '@server/models/model-cache'
+import { VideoFile } from '@shared/models/videos/video-file.model'
+import { ResultList, UserRight, VideoPrivacy, VideoState } from '../../../shared'
+import { VideoObject } from '../../../shared/models/activitypub/objects'
import { Video, VideoDetails } from '../../../shared/models/videos'
+import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
import { VideoFilter } from '../../../shared/models/videos/video-query.type'
+import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
import { peertubeTruncate } from '../../helpers/core-utils'
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
import { isBooleanValid } from '../../helpers/custom-validators/misc'
} from '../../helpers/custom-validators/videos'
import { getVideoFileResolution } from '../../helpers/ffmpeg-utils'
import { logger } from '../../helpers/logger'
+import { CONFIG } from '../../initializers/config'
import {
ACTIVITY_PUB,
API_VERSION,
WEBSERVER
} from '../../initializers/constants'
import { sendDeleteVideo } from '../../lib/activitypub/send'
-import { AccountModel } from '../account/account'
-import { AccountVideoRateModel } from '../account/account-video-rate'
-import { ActorModel } from '../activitypub/actor'
-import { AvatarModel } from '../avatar/avatar'
-import { ServerModel } from '../server/server'
-import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils'
-import { TagModel } from './tag'
-import { VideoAbuseModel } from './video-abuse'
-import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel'
-import { VideoCommentModel } from './video-comment'
-import { VideoFileModel } from './video-file'
-import { VideoShareModel } from './video-share'
-import { VideoTagModel } from './video-tag'
-import { ScheduleVideoUpdateModel } from './schedule-video-update'
-import { VideoCaptionModel } from './video-caption'
-import { VideoBlacklistModel } from './video-blacklist'
-import { remove } from 'fs-extra'
-import { VideoViewModel } from './video-views'
-import { VideoRedundancyModel } from '../redundancy/video-redundancy'
-import {
- videoFilesModelToFormattedJSON,
- VideoFormattingJSONOptions,
- videoModelToActivityPubObject,
- videoModelToFormattedDetailsJSON,
- videoModelToFormattedJSON
-} from './video-format-utils'
-import { UserVideoHistoryModel } from '../account/user-video-history'
-import { VideoImportModel } from './video-import'
-import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
-import { VideoPlaylistElementModel } from './video-playlist-element'
-import { CONFIG } from '../../initializers/config'
-import { ThumbnailModel } from './thumbnail'
-import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
-import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
import {
MChannel,
MChannelAccountDefault,
MVideoWithAllFiles,
MVideoWithFile,
MVideoWithRights
-} from '../../typings/models'
-import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../typings/models/video/video-file'
-import { MThumbnail } from '../../typings/models/video/thumbnail'
-import { VideoFile } from '@shared/models/videos/video-file.model'
-import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
-import { ModelCache } from '@server/models/model-cache'
+} from '../../types/models'
+import { MThumbnail } from '../../types/models/video/thumbnail'
+import { MVideoFile, MVideoFileRedundanciesOpt, MVideoFileStreamingPlaylistVideo } from '../../types/models/video/video-file'
+import { VideoAbuseModel } from '../abuse/video-abuse'
+import { AccountModel } from '../account/account'
+import { AccountVideoRateModel } from '../account/account-video-rate'
+import { UserVideoHistoryModel } from '../account/user-video-history'
+import { ActorModel } from '../activitypub/actor'
+import { AvatarModel } from '../avatar/avatar'
+import { VideoRedundancyModel } from '../redundancy/video-redundancy'
+import { ServerModel } from '../server/server'
+import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils'
+import { ScheduleVideoUpdateModel } from './schedule-video-update'
+import { TagModel } from './tag'
+import { ThumbnailModel } from './thumbnail'
+import { VideoBlacklistModel } from './video-blacklist'
+import { VideoCaptionModel } from './video-caption'
+import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel'
+import { VideoCommentModel } from './video-comment'
+import { VideoFileModel } from './video-file'
+import {
+ videoFilesModelToFormattedJSON,
+ VideoFormattingJSONOptions,
+ videoModelToActivityPubObject,
+ videoModelToFormattedDetailsJSON,
+ videoModelToFormattedJSON
+} from './video-format-utils'
+import { VideoImportModel } from './video-import'
+import { VideoLiveModel } from './video-live'
+import { VideoPlaylistElementModel } from './video-playlist-element'
import { buildListQuery, BuildVideosQueryOptions, wrapForAPIResults } from './video-query-builder'
-import { buildNSFWFilter } from '@server/helpers/express-utils'
-import { getServerActor } from '@server/models/application/application'
-import { getPrivaciesForFederation, isPrivacyForFederation } from "@server/helpers/video"
+import { VideoShareModel } from './video-share'
+import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
+import { VideoTagModel } from './video-tag'
+import { VideoViewModel } from './video-view'
export enum ScopeNames {
AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS',
WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS',
WITH_USER_ID = 'WITH_USER_ID',
WITH_IMMUTABLE_ATTRIBUTES = 'WITH_IMMUTABLE_ATTRIBUTES',
- WITH_THUMBNAILS = 'WITH_THUMBNAILS'
+ WITH_THUMBNAILS = 'WITH_THUMBNAILS',
+ WITH_LIVE = 'WITH_LIVE'
}
export type ForAPIOptions = {
}
]
},
+ [ScopeNames.WITH_LIVE]: {
+ include: [
+ {
+ model: VideoLiveModel.unscoped(),
+ required: false
+ }
+ ]
+ },
[ScopeNames.WITH_USER_ID]: {
include: [
{
@AllowNull(false)
@Is('VideoPrivacy', value => throwIfNotValid(value, isVideoPrivacyValid, 'privacy'))
@Column
- privacy: number
+ privacy: VideoPrivacy
@AllowNull(false)
@Is('VideoNSFW', value => throwIfNotValid(value, isBooleanValid, 'NSFW boolean'))
@Column
remote: boolean
+ @AllowNull(false)
+ @Default(false)
+ @Column
+ isLive: boolean
+
@AllowNull(false)
@Is('VideoUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
@Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max))
})
VideoBlacklist: VideoBlacklistModel
+ @HasOne(() => VideoLiveModel, {
+ foreignKey: {
+ name: 'videoId',
+ allowNull: false
+ },
+ onDelete: 'cascade'
+ })
+ VideoLive: VideoLiveModel
+
@HasOne(() => VideoImportModel, {
foreignKey: {
name: 'videoId',
return undefined
}
+ @BeforeDestroy
+ static stopLiveIfNeeded (instance: VideoModel) {
+ if (!instance.isLive) return
+
+ logger.info('Stopping live of video %s after video deletion.', instance.uuid)
+
+ return LiveManager.Instance.stopSessionOf(instance.id)
+ }
+
@BeforeDestroy
static invalidateCache (instance: VideoModel) {
ModelCache.Instance.invalidateCache('video', instance.id)
static async saveEssentialDataToAbuses (instance: VideoModel, options) {
const tasks: Promise<any>[] = []
- logger.info('Saving video abuses details of video %s.', instance.url)
-
if (!Array.isArray(instance.VideoAbuses)) {
instance.VideoAbuses = await instance.$get('VideoAbuses')
if (instance.VideoAbuses.length === 0) return undefined
}
+ logger.info('Saving video abuses details of video %s.', instance.url)
+
const details = instance.toFormattedDetailsJSON()
for (const abuse of instance.VideoAbuses) {
}
]
},
+ {
+ model: VideoStreamingPlaylistModel.unscoped(),
+ required: false,
+ include: [
+ {
+ model: VideoFileModel,
+ required: false
+ }
+ ]
+ },
+ VideoLiveModel.unscoped(),
VideoFileModel,
TagModel
]
})
}
+ static listPublishedLiveIds () {
+ const options = {
+ attributes: [ 'id' ],
+ where: {
+ isLive: true,
+ state: VideoState.PUBLISHED
+ }
+ }
+
+ return VideoModel.findAll(options)
+ .map(v => v.id)
+ }
+
static listUserVideosForApi (
accountId: number,
start: number,
return VideoModel.getAvailableForApi(queryOptions)
}
+ static countLocalLives () {
+ const options = {
+ where: {
+ remote: false,
+ isLive: true
+ }
+ }
+
+ return VideoModel.count(options)
+ }
+
+ static countLivesOfAccount (accountId: number) {
+ const options = {
+ where: {
+ remote: false,
+ isLive: true
+ },
+ include: [
+ {
+ required: true,
+ model: VideoChannelModel.unscoped(),
+ where: {
+ accountId
+ }
+ }
+ ]
+ }
+
+ return VideoModel.count(options)
+ }
+
static load (id: number | string, t?: Transaction): Bluebird<MVideoThumbnail> {
const where = buildWhereIdOrUUID(id)
const options = {
ScopeNames.WITH_SCHEDULED_UPDATE,
ScopeNames.WITH_WEBTORRENT_FILES,
ScopeNames.WITH_STREAMING_PLAYLISTS,
- ScopeNames.WITH_THUMBNAILS
+ ScopeNames.WITH_THUMBNAILS,
+ ScopeNames.WITH_LIVE
]
if (userId) {
ScopeNames.WITH_ACCOUNT_DETAILS,
ScopeNames.WITH_SCHEDULED_UPDATE,
ScopeNames.WITH_THUMBNAILS,
+ ScopeNames.WITH_LIVE,
{ method: [ ScopeNames.WITH_WEBTORRENT_FILES, true ] },
{ method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] }
]
}
private static buildAPIResult (rows: any[]) {
- const memo: { [ id: number ]: VideoModel } = {}
+ const videosMemo: { [ id: number ]: VideoModel } = {}
+ const videoStreamingPlaylistMemo: { [ id: number ]: VideoStreamingPlaylistModel } = {}
const thumbnailsDone = new Set<number>()
const historyDone = new Set<number>()
const actorKeys = [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ]
const serverKeys = [ 'id', 'host' ]
const videoFileKeys = [ 'id', 'createdAt', 'updatedAt', 'resolution', 'size', 'extname', 'infoHash', 'fps', 'videoId' ]
+ const videoStreamingPlaylistKeys = [ 'id' ]
const videoKeys = [
'id',
'uuid',
}
for (const row of rows) {
- if (!memo[row.id]) {
+ if (!videosMemo[row.id]) {
// Build Channel
const channel = row.VideoChannel
const channelModel = new VideoChannelModel(pick(channel, [ 'id', 'name', 'description', 'actorId' ]))
videoModel.UserVideoHistories = []
videoModel.Thumbnails = []
videoModel.VideoFiles = []
+ videoModel.VideoStreamingPlaylists = []
- memo[row.id] = videoModel
+ videosMemo[row.id] = videoModel
// Don't take object value to have a sorted array
videos.push(videoModel)
}
- const videoModel = memo[row.id]
+ const videoModel = videosMemo[row.id]
if (row.userVideoHistory?.id && !historyDone.has(row.userVideoHistory.id)) {
const historyModel = new UserVideoHistoryModel(pick(row.userVideoHistory, [ 'id', 'currentTime' ]))
videoFilesDone.add(row.VideoFiles.id)
}
+
+ if (row.VideoStreamingPlaylists?.id && !videoStreamingPlaylistMemo[row.VideoStreamingPlaylists.id]) {
+ const streamingPlaylist = new VideoStreamingPlaylistModel(pick(row.VideoStreamingPlaylists, videoStreamingPlaylistKeys))
+ streamingPlaylist.VideoFiles = []
+
+ videoModel.VideoStreamingPlaylists.push(streamingPlaylist)
+
+ videoStreamingPlaylistMemo[streamingPlaylist.id] = streamingPlaylist
+ }
+
+ if (row.VideoStreamingPlaylists?.VideoFiles?.id && !videoFilesDone.has(row.VideoStreamingPlaylists.VideoFiles.id)) {
+ const streamingPlaylist = videoStreamingPlaylistMemo[row.VideoStreamingPlaylists.id]
+
+ const videoFileModel = new VideoFileModel(pick(row.VideoStreamingPlaylists.VideoFiles, videoFileKeys))
+ streamingPlaylist.VideoFiles.push(videoFileModel)
+
+ videoFilesDone.add(row.VideoStreamingPlaylists.VideoFiles.id)
+ }
}
return videos
}
isBlocked () {
- return (this.VideoChannel.Account.Actor.Server && this.VideoChannel.Account.Actor.Server.isBlocked()) ||
- this.VideoChannel.Account.isBlocked()
+ return this.VideoChannel.Account.Actor.Server?.isBlocked() || this.VideoChannel.Account.isBlocked()
}
getQualityFileBy<T extends MVideoWithFile> (this: T, fun: (files: MVideoFile[], it: (file: MVideoFile) => number) => MVideoFile) {
getFormattedVideoFilesJSON (): VideoFile[] {
const { baseUrlHttp, baseUrlWs } = this.getBaseUrls()
- return videoFilesModelToFormattedJSON(this, baseUrlHttp, baseUrlWs, this.VideoFiles)
+ let files: MVideoFileRedundanciesOpt[] = []
+
+ if (Array.isArray(this.VideoFiles)) {
+ files = files.concat(this.VideoFiles)
+ }
+
+ for (const p of (this.VideoStreamingPlaylists || [])) {
+ files = files.concat(p.VideoFiles || [])
+ }
+
+ return videoFilesModelToFormattedJSON(this, baseUrlHttp, baseUrlWs, files)
}
- toActivityPubObject (this: MVideoAP): VideoTorrentObject {
+ toActivityPubObject (this: MVideoAP): VideoObject {
return videoModelToActivityPubObject(this)
}
return isPrivacyForFederation(this.privacy)
}
+ hasStateForFederation () {
+ return isStateForFederation(this.state)
+ }
+
isNewVideo (newPrivacy: VideoPrivacy) {
return this.hasPrivacyForFederation() === false && isPrivacyForFederation(newPrivacy) === true
}