X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=server%2Fmodels%2Fvideo%2Fvideo.ts;h=ea6c9d44ba080ddadbe2012bacfaaeb3a6cbb1c6;hb=1896bca09e088b0da9d5e845407ecebae330618c;hp=1eded0d56191f775fd15fa73f6b8afa8cf4745ae;hpb=97816649b793bdd0f3df64631ae0ef7cf3d7461c;p=github%2FChocobozzz%2FPeerTube.git diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 1eded0d56..ea6c9d44b 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -2,7 +2,7 @@ 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' +import { FindOptions, Includeable, IncludeOptions, Op, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize' import { AllowNull, BeforeDestroy, @@ -25,13 +25,14 @@ import { UpdatedAt } from 'sequelize-typescript' import { buildNSFWFilter } from '@server/helpers/express-utils' -import { getPrivaciesForFederation, isPrivacyForFederation } from '@server/helpers/video' +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 { VideoTorrentObject } from '../../../shared/models/activitypub/objects' +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' @@ -50,7 +51,7 @@ import { isVideoStateValid, isVideoSupportValid } from '../../helpers/custom-validators/videos' -import { getVideoFileResolution } from '../../helpers/ffmpeg-utils' +import { getVideoFileResolution } from '../../helpers/ffprobe-utils' import { logger } from '../../helpers/logger' import { CONFIG } from '../../initializers/config' import { @@ -95,10 +96,11 @@ import { MVideoWithRights } from '../../types/models' import { MThumbnail } from '../../types/models/video/thumbnail' -import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileRedundanciesOpt } from '../../types/models/video/video-file' +import { MVideoFile, 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 { UserModel } from '../account/user' import { UserVideoHistoryModel } from '../account/user-video-history' import { ActorModel } from '../activitypub/actor' import { AvatarModel } from '../avatar/avatar' @@ -121,13 +123,13 @@ import { 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 { VideoShareModel } from './video-share' import { VideoStreamingPlaylistModel } from './video-streaming-playlist' import { VideoTagModel } from './video-tag' import { VideoViewModel } from './video-view' -import { stream } from 'winston' export enum ScopeNames { AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS', @@ -141,7 +143,8 @@ export enum ScopeNames { 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 = { @@ -149,8 +152,6 @@ export type ForAPIOptions = { videoPlaylistId?: number - withFiles?: boolean - withAccountBlockerIds?: number[] } @@ -188,26 +189,26 @@ export type AvailableForListIDsOptions = { attributes: [ 'id', 'url', 'uuid', 'remote' ] }, [ScopeNames.FOR_API]: (options: ForAPIOptions) => { - const query: FindOptions = { - include: [ - { - model: VideoChannelModel.scope({ - method: [ - VideoChannelScopeNames.SUMMARY, { - withAccount: true, - withAccountBlockerIds: options.withAccountBlockerIds - } as SummaryOptions - ] - }), - required: true - }, - { - attributes: [ 'type', 'filename' ], - model: ThumbnailModel, - required: false - } - ] - } + const include: Includeable[] = [ + { + model: VideoChannelModel.scope({ + method: [ + VideoChannelScopeNames.SUMMARY, { + withAccount: true, + withAccountBlockerIds: options.withAccountBlockerIds + } as SummaryOptions + ] + }), + required: true + }, + { + attributes: [ 'type', 'filename' ], + model: ThumbnailModel, + required: false + } + ] + + const query: FindOptions = {} if (options.ids) { query.where = { @@ -217,15 +218,8 @@ export type AvailableForListIDsOptions = { } } - if (options.withFiles === true) { - query.include.push({ - model: VideoFileModel, - required: true - }) - } - if (options.videoPlaylistId) { - query.include.push({ + include.push({ model: VideoPlaylistElementModel.unscoped(), required: true, where: { @@ -234,6 +228,8 @@ export type AvailableForListIDsOptions = { }) } + query.include = include + return query }, [ScopeNames.WITH_THUMBNAILS]: { @@ -244,6 +240,14 @@ export type AvailableForListIDsOptions = { } ] }, + [ScopeNames.WITH_LIVE]: { + include: [ + { + model: VideoLiveModel.unscoped(), + required: false + } + ] + }, [ScopeNames.WITH_USER_ID]: { include: [ { @@ -467,7 +471,7 @@ export type AvailableForListIDsOptions = { } ] }) -export class VideoModel extends Model { +export class VideoModel extends Model { @AllowNull(false) @Default(DataType.UUIDV4) @@ -550,6 +554,11 @@ export class VideoModel extends Model { @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)) @@ -720,6 +729,15 @@ export class VideoModel extends Model { }) VideoBlacklist: VideoBlacklistModel + @HasOne(() => VideoLiveModel, { + foreignKey: { + name: 'videoId', + allowNull: false + }, + onDelete: 'cascade' + }) + VideoLive: VideoLiveModel + @HasOne(() => VideoImportModel, { foreignKey: { name: 'videoId', @@ -795,6 +813,15 @@ export class VideoModel extends Model { 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) @@ -827,7 +854,7 @@ export class VideoModel extends Model { return undefined } - static listLocal (): Bluebird { + static listLocal (): Promise { const query = { where: { remote: false @@ -921,6 +948,17 @@ export class VideoModel extends Model { } ] }, + { + model: VideoStreamingPlaylistModel.unscoped(), + required: false, + include: [ + { + model: VideoFileModel, + required: false + } + ] + }, + VideoLiveModel.unscoped(), VideoFileModel, TagModel ] @@ -944,13 +982,29 @@ export class VideoModel extends Model { }) } - static listUserVideosForApi ( - accountId: number, - start: number, - count: number, - sort: string, + static async listPublishedLiveIds () { + const options = { + attributes: [ 'id' ], + where: { + isLive: true, + state: VideoState.PUBLISHED + } + } + + const result = await VideoModel.findAll(options) + + return result.map(v => v.id) + } + + static listUserVideosForApi (options: { + accountId: number + start: number + count: number + sort: string search?: string - ) { + }) { + const { accountId, start, count, sort, search } = options + function buildBaseQuery (): FindOptions { let baseQuery = { offset: start, @@ -1027,14 +1081,16 @@ export class VideoModel extends Model { user?: MUserAccountId historyOfUser?: MUserId countVideos?: boolean + search?: string }) { - if (options.filter && options.filter === 'all-local' && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) { + if ((options.filter === 'all-local' || options.filter === 'all') && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) { throw new Error('Try to filter all-local but no user has not the see all videos right') } const trendingDays = options.sort.endsWith('trending') ? CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS : undefined + const hot = options.sort.endsWith('hot') const serverActor = await getServerActor() @@ -1063,7 +1119,9 @@ export class VideoModel extends Model { includeLocalVideos: options.includeLocalVideos, user: options.user, historyOfUser: options.historyOfUser, - trendingDays + trendingDays, + hot, + search: options.search } return VideoModel.getAvailableForApi(queryOptions, options.countVideos) @@ -1120,7 +1178,77 @@ export class VideoModel extends Model { return VideoModel.getAvailableForApi(queryOptions) } - static load (id: number | string, t?: Transaction): Bluebird { + static countLocalLives () { + const options = { + where: { + remote: false, + isLive: true, + state: { + [Op.ne]: VideoState.LIVE_ENDED + } + } + } + + return VideoModel.count(options) + } + + static countVideosUploadedByUserSince (userId: number, since: Date) { + const options = { + include: [ + { + model: VideoChannelModel.unscoped(), + required: true, + include: [ + { + model: AccountModel.unscoped(), + required: true, + include: [ + { + model: UserModel.unscoped(), + required: true, + where: { + id: userId + } + } + ] + } + ] + } + ], + where: { + createdAt: { + [Op.gte]: since + } + } + } + + return VideoModel.unscoped().count(options) + } + + static countLivesOfAccount (accountId: number) { + const options = { + where: { + remote: false, + isLive: true, + state: { + [Op.ne]: VideoState.LIVE_ENDED + } + }, + include: [ + { + required: true, + model: VideoChannelModel.unscoped(), + where: { + accountId + } + } + ] + } + + return VideoModel.count(options) + } + + static load (id: number | string, t?: Transaction): Promise { const where = buildWhereIdOrUUID(id) const options = { where, @@ -1130,7 +1258,7 @@ export class VideoModel extends Model { return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options) } - static loadWithBlacklist (id: number | string, t?: Transaction): Bluebird { + static loadWithBlacklist (id: number | string, t?: Transaction): Promise { const where = buildWhereIdOrUUID(id) const options = { where, @@ -1143,7 +1271,7 @@ export class VideoModel extends Model { ]).findOne(options) } - static loadImmutableAttributes (id: number | string, t?: Transaction): Bluebird { + static loadImmutableAttributes (id: number | string, t?: Transaction): Promise { const fun = () => { const query = { where: buildWhereIdOrUUID(id), @@ -1161,7 +1289,7 @@ export class VideoModel extends Model { }) } - static loadWithRights (id: number | string, t?: Transaction): Bluebird { + static loadWithRights (id: number | string, t?: Transaction): Promise { const where = buildWhereIdOrUUID(id) const options = { where, @@ -1175,7 +1303,7 @@ export class VideoModel extends Model { ]).findOne(options) } - static loadOnlyId (id: number | string, t?: Transaction): Bluebird { + static loadOnlyId (id: number | string, t?: Transaction): Promise { const where = buildWhereIdOrUUID(id) const options = { @@ -1187,7 +1315,7 @@ export class VideoModel extends Model { return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options) } - static loadWithFiles (id: number | string, t?: Transaction, logging?: boolean): Bluebird { + static loadWithFiles (id: number | string, t?: Transaction, logging?: boolean): Promise { const where = buildWhereIdOrUUID(id) const query = { @@ -1203,7 +1331,7 @@ export class VideoModel extends Model { ]).findOne(query) } - static loadByUUID (uuid: string): Bluebird { + static loadByUUID (uuid: string): Promise { const options = { where: { uuid @@ -1213,7 +1341,7 @@ export class VideoModel extends Model { return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options) } - static loadByUrl (url: string, transaction?: Transaction): Bluebird { + static loadByUrl (url: string, transaction?: Transaction): Promise { const query: FindOptions = { where: { url @@ -1224,7 +1352,7 @@ export class VideoModel extends Model { return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(query) } - static loadByUrlImmutableAttributes (url: string, transaction?: Transaction): Bluebird { + static loadByUrlImmutableAttributes (url: string, transaction?: Transaction): Promise { const fun = () => { const query: FindOptions = { where: { @@ -1244,7 +1372,7 @@ export class VideoModel extends Model { }) } - static loadByUrlAndPopulateAccount (url: string, transaction?: Transaction): Bluebird { + static loadByUrlAndPopulateAccount (url: string, transaction?: Transaction): Promise { const query: FindOptions = { where: { url @@ -1261,7 +1389,7 @@ export class VideoModel extends Model { ]).findOne(query) } - static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Transaction, userId?: number): Bluebird { + static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Transaction, userId?: number): Promise { const where = buildWhereIdOrUUID(id) const options = { @@ -1277,7 +1405,8 @@ export class VideoModel extends Model { ScopeNames.WITH_SCHEDULED_UPDATE, ScopeNames.WITH_WEBTORRENT_FILES, ScopeNames.WITH_STREAMING_PLAYLISTS, - ScopeNames.WITH_THUMBNAILS + ScopeNames.WITH_THUMBNAILS, + ScopeNames.WITH_LIVE ] if (userId) { @@ -1293,7 +1422,7 @@ export class VideoModel extends Model { id: number | string t?: Transaction userId?: number - }): Bluebird { + }): Promise { const { id, t, userId } = parameters const where = buildWhereIdOrUUID(id) @@ -1309,6 +1438,7 @@ export class VideoModel extends Model { 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 ] } ] @@ -1391,7 +1521,7 @@ export class VideoModel extends Model { return VideoModel.update({ support: videoChannel.support }, options) } - static getAllIdsFromChannel (videoChannel: MChannelId): Bluebird { + static getAllIdsFromChannel (videoChannel: MChannelId): Promise { const query = { attributes: [ 'id' ], where: { @@ -1479,15 +1609,25 @@ export class VideoModel extends Model { const thumbnailsDone = new Set() const historyDone = new Set() const videoFilesDone = new Set() - const videoStreamingPlaylistsDone = new Set() const videos: VideoModel[] = [] const avatarKeys = [ 'id', 'filename', 'fileUrl', 'onDisk', 'createdAt', 'updatedAt' ] 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 videoFileKeys = [ + 'id', + 'createdAt', + 'updatedAt', + 'resolution', + 'size', + 'extname', + 'infoHash', + 'fps', + 'videoId', + 'videoStreamingPlaylistId' + ] + const videoStreamingPlaylistKeys = [ 'id', 'type', 'playlistUrl' ] const videoKeys = [ 'id', 'uuid', @@ -1504,6 +1644,7 @@ export class VideoModel extends Model { 'likes', 'dislikes', 'remote', + 'isLive', 'url', 'commentsEnabled', 'downloadEnabled', @@ -1581,13 +1722,6 @@ export class VideoModel extends Model { videoFilesDone.add(row.VideoFiles.id) } - if (row.VideoFiles?.id && !videoFilesDone.has(row.VideoFiles.id)) { - const videoFileModel = new VideoFileModel(pick(row.VideoFiles, videoFileKeys)) - videoModel.VideoFiles.push(videoFileModel) - - videoFilesDone.add(row.VideoFiles.id) - } - if (row.VideoStreamingPlaylists?.id && !videoStreamingPlaylistMemo[row.VideoStreamingPlaylists.id]) { const streamingPlaylist = new VideoStreamingPlaylistModel(pick(row.VideoStreamingPlaylists, videoStreamingPlaylistKeys)) streamingPlaylist.VideoFiles = [] @@ -1639,6 +1773,7 @@ export class VideoModel extends Model { } getQualityFileBy (this: T, fun: (files: MVideoFile[], it: (file: MVideoFile) => number) => MVideoFile) { + // We first transcode to WebTorrent format, so try this array first if (Array.isArray(this.VideoFiles) && this.VideoFiles.length !== 0) { const file = fun(this.VideoFiles, file => file.resolution) @@ -1673,6 +1808,10 @@ export class VideoModel extends Model { return Object.assign(file, { Video: this }) } + hasWebTorrentFiles () { + return Array.isArray(this.VideoFiles) === true && this.VideoFiles.length !== 0 + } + async addAndSaveThumbnail (thumbnail: MThumbnail, transaction: Transaction) { thumbnail.videoId = this.id @@ -1747,24 +1886,24 @@ export class VideoModel extends Model { getFormattedVideoFilesJSON (): VideoFile[] { const { baseUrlHttp, baseUrlWs } = this.getBaseUrls() - let files: MVideoFileRedundanciesOpt[] = [] - - logger.info('coucou', { files }) + let files: VideoFile[] = [] if (Array.isArray(this.VideoFiles)) { - files = files.concat(this.VideoFiles) + const result = videoFilesModelToFormattedJSON(this, baseUrlHttp, baseUrlWs, this.VideoFiles) + files = files.concat(result) } for (const p of (this.VideoStreamingPlaylists || [])) { - files = files.concat(p.VideoFiles || []) - } + p.Video = this - logger.info('coucou', { files, video: this.VideoStreamingPlaylists }) + const result = videoFilesModelToFormattedJSON(p, baseUrlHttp, baseUrlWs, p.VideoFiles) + files = files.concat(result) + } - return videoFilesModelToFormattedJSON(this, baseUrlHttp, baseUrlWs, files) + return files } - toActivityPubObject (this: MVideoAP): VideoTorrentObject { + toActivityPubObject (this: MVideoAP): VideoObject { return videoModelToActivityPubObject(this) } @@ -1851,6 +1990,10 @@ export class VideoModel extends Model { return isPrivacyForFederation(this.privacy) } + hasStateForFederation () { + return isStateForFederation(this.state) + } + isNewVideo (newPrivacy: VideoPrivacy) { return this.hasPrivacyForFederation() === false && isPrivacyForFederation(newPrivacy) === true }