X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=server%2Fmodels%2Fvideo%2Fvideo.ts;h=4979cee500b8e1bb2325105f9bbdaf91848cb13d;hb=e5dbd5084e7ae91ce118c0bccd5b84c47b88c55f;hp=e55a21a6b69c0c031ebc195423adf42e03975862;hpb=1fd61899eaea245a5844e33e21f04b2562f16e5e;p=github%2FChocobozzz%2FPeerTube.git diff --git a/server/models/video/video.ts b/server/models/video/video.ts index e55a21a6b..4979cee50 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -1,6 +1,6 @@ import * as Bluebird from 'bluebird' import { remove } from 'fs-extra' -import { maxBy, minBy, pick } from 'lodash' +import { maxBy, minBy } from 'lodash' import { join } from 'path' import { FindOptions, Includeable, IncludeOptions, Op, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize' import { @@ -24,12 +24,14 @@ import { Table, UpdatedAt } from 'sequelize-typescript' +import { setAsUpdated } from '@server/helpers/database-utils' import { buildNSFWFilter } from '@server/helpers/express-utils' import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video' import { LiveManager } from '@server/lib/live-manager' import { getHLSDirectory, getVideoFilePath } from '@server/lib/video-paths' import { getServerActor } from '@server/models/application/application' import { ModelCache } from '@server/models/model-cache' +import { AttributesOnly } from '@shared/core-utils' import { VideoFile } from '@shared/models/videos/video-file.model' import { ResultList, UserRight, VideoPrivacy, VideoState } from '../../../shared' import { VideoObject } from '../../../shared/models/activitypub/objects' @@ -99,16 +101,25 @@ import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../types/models import { VideoAbuseModel } from '../abuse/video-abuse' import { AccountModel } from '../account/account' import { AccountVideoRateModel } from '../account/account-video-rate' -import { ActorImageModel } from '../account/actor-image' -import { UserModel } from '../account/user' -import { UserVideoHistoryModel } from '../account/user-video-history' -import { ActorModel } from '../activitypub/actor' +import { ActorModel } from '../actor/actor' +import { ActorImageModel } from '../actor/actor-image' import { VideoRedundancyModel } from '../redundancy/video-redundancy' import { ServerModel } from '../server/server' import { TrackerModel } from '../server/tracker' import { VideoTrackerModel } from '../server/video-tracker' +import { UserModel } from '../user/user' +import { UserVideoHistoryModel } from '../user/user-video-history' import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils' +import { + videoFilesModelToFormattedJSON, + VideoFormattingJSONOptions, + videoModelToActivityPubObject, + videoModelToFormattedDetailsJSON, + videoModelToFormattedJSON +} from './formatter/video-format-utils' import { ScheduleVideoUpdateModel } from './schedule-video-update' +import { BuildVideosListQueryOptions, VideosIdListQueryBuilder } from './sql/videos-id-list-query-builder' +import { VideosModelListQueryBuilder } from './sql/videos-model-list-query-builder' import { TagModel } from './tag' import { ThumbnailModel } from './thumbnail' import { VideoBlacklistModel } from './video-blacklist' @@ -116,17 +127,9 @@ 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 { VideoShareModel } from './video-share' import { VideoStreamingPlaylistModel } from './video-streaming-playlist' import { VideoTagModel } from './video-tag' @@ -488,7 +491,7 @@ export type AvailableForListIDsOptions = { } ] }) -export class VideoModel extends Model { +export class VideoModel extends Model>> { @AllowNull(false) @Default(DataType.UUIDV4) @@ -1002,18 +1005,19 @@ export class VideoModel extends Model { }) } - static async listPublishedLiveIds () { + static async listPublishedLiveUUIDs () { const options = { - attributes: [ 'id' ], + attributes: [ 'uuid' ], where: { isLive: true, + remote: false, state: VideoState.PUBLISHED } } const result = await VideoModel.findAll(options) - return result.map(v => v.id) + return result.map(v => v.uuid) } static listUserVideosForApi (options: { @@ -1604,7 +1608,7 @@ export class VideoModel extends Model { const serverActor = await getServerActor() const followerActorId = serverActor.id - const queryOptions: BuildVideosQueryOptions = { + const queryOptions: BuildVideosListQueryOptions = { attributes: [ `"${field}"` ], group: `GROUP BY "${field}"`, having: `HAVING COUNT("${field}") >= ${threshold}`, @@ -1616,10 +1620,10 @@ export class VideoModel extends Model { includeLocalVideos: true } - const { query, replacements } = buildListQuery(VideoModel, queryOptions) + const queryBuilder = new VideosIdListQueryBuilder(VideoModel.sequelize) - return this.sequelize.query(query, { replacements, type: QueryTypes.SELECT }) - .then(rows => rows.map(r => r[field])) + return queryBuilder.queryVideoIds(queryOptions) + .then(rows => rows.map(r => r[field])) } static buildTrendingQuery (trendingDays: number) { @@ -1637,27 +1641,24 @@ export class VideoModel extends Model { } private static async getAvailableForApi ( - options: BuildVideosQueryOptions, + options: BuildVideosListQueryOptions, countVideos = true ): Promise> { function getCount () { if (countVideos !== true) return Promise.resolve(undefined) const countOptions = Object.assign({}, options, { isCount: true }) - const { query: queryCount, replacements: replacementsCount } = buildListQuery(VideoModel, countOptions) + const queryBuilder = new VideosIdListQueryBuilder(VideoModel.sequelize) - return VideoModel.sequelize.query(queryCount, { replacements: replacementsCount, type: QueryTypes.SELECT }) - .then(rows => rows.length !== 0 ? rows[0].total : 0) + return queryBuilder.countVideoIds(countOptions) } function getModels () { if (options.count === 0) return Promise.resolve([]) - const { query, replacements, order } = buildListQuery(VideoModel, options) - const queryModels = wrapForAPIResults(query, replacements, options, order) + const queryBuilder = new VideosModelListQueryBuilder(VideoModel.sequelize) - return VideoModel.sequelize.query(queryModels, { replacements, type: QueryTypes.SELECT, nest: true }) - .then(rows => VideoModel.buildAPIResult(rows)) + return queryBuilder.queryVideos(options) } const [ count, rows ] = await Promise.all([ getCount(), getModels() ]) @@ -1668,153 +1669,6 @@ export class VideoModel extends Model { } } - private static buildAPIResult (rows: any[]) { - const videosMemo: { [ id: number ]: VideoModel } = {} - const videoStreamingPlaylistMemo: { [ id: number ]: VideoStreamingPlaylistModel } = {} - - const thumbnailsDone = new Set() - const historyDone = new Set() - const videoFilesDone = 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', - 'filename', - 'fileUrl', - 'torrentFilename', - 'torrentUrl', - 'infoHash', - 'fps', - 'videoId', - 'videoStreamingPlaylistId' - ] - const videoStreamingPlaylistKeys = [ 'id', 'type', 'playlistUrl' ] - const videoKeys = [ - 'id', - 'uuid', - 'name', - 'category', - 'licence', - 'language', - 'privacy', - 'nsfw', - 'description', - 'support', - 'duration', - 'views', - 'likes', - 'dislikes', - 'remote', - 'isLive', - 'url', - 'commentsEnabled', - 'downloadEnabled', - 'waitTranscoding', - 'state', - 'publishedAt', - 'originallyPublishedAt', - 'channelId', - 'createdAt', - 'updatedAt' - ] - const buildOpts = { raw: true } - - function buildActor (rowActor: any) { - const avatarModel = rowActor.Avatar.id !== null - ? new ActorImageModel(pick(rowActor.Avatar, avatarKeys), buildOpts) - : null - - const serverModel = rowActor.Server.id !== null - ? new ServerModel(pick(rowActor.Server, serverKeys), buildOpts) - : null - - const actorModel = new ActorModel(pick(rowActor, actorKeys), buildOpts) - actorModel.Avatar = avatarModel - actorModel.Server = serverModel - - return actorModel - } - - for (const row of rows) { - if (!videosMemo[row.id]) { - // Build Channel - const channel = row.VideoChannel - const channelModel = new VideoChannelModel(pick(channel, [ 'id', 'name', 'description', 'actorId' ]), buildOpts) - channelModel.Actor = buildActor(channel.Actor) - - const account = row.VideoChannel.Account - const accountModel = new AccountModel(pick(account, [ 'id', 'name' ]), buildOpts) - accountModel.Actor = buildActor(account.Actor) - - channelModel.Account = accountModel - - const videoModel = new VideoModel(pick(row, videoKeys), buildOpts) - videoModel.VideoChannel = channelModel - - videoModel.UserVideoHistories = [] - videoModel.Thumbnails = [] - videoModel.VideoFiles = [] - videoModel.VideoStreamingPlaylists = [] - - videosMemo[row.id] = videoModel - // Don't take object value to have a sorted array - videos.push(videoModel) - } - - const videoModel = videosMemo[row.id] - - if (row.userVideoHistory?.id && !historyDone.has(row.userVideoHistory.id)) { - const historyModel = new UserVideoHistoryModel(pick(row.userVideoHistory, [ 'id', 'currentTime' ]), buildOpts) - videoModel.UserVideoHistories.push(historyModel) - - historyDone.add(row.userVideoHistory.id) - } - - if (row.Thumbnails?.id && !thumbnailsDone.has(row.Thumbnails.id)) { - const thumbnailModel = new ThumbnailModel(pick(row.Thumbnails, [ 'id', 'type', 'filename' ]), buildOpts) - videoModel.Thumbnails.push(thumbnailModel) - - thumbnailsDone.add(row.Thumbnails.id) - } - - if (row.VideoFiles?.id && !videoFilesDone.has(row.VideoFiles.id)) { - const videoFileModel = new VideoFileModel(pick(row.VideoFiles, videoFileKeys), buildOpts) - 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), buildOpts) - 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), buildOpts) - streamingPlaylist.VideoFiles.push(videoFileModel) - - videoFilesDone.add(row.VideoStreamingPlaylists.VideoFiles.id) - } - } - - return videos - } - static getCategoryLabel (id: number) { return VIDEO_CATEGORIES[id] || 'Misc' } @@ -1883,7 +1737,7 @@ export class VideoModel extends Model { return Array.isArray(this.VideoFiles) === true && this.VideoFiles.length !== 0 } - async addAndSaveThumbnail (thumbnail: MThumbnail, transaction: Transaction) { + async addAndSaveThumbnail (thumbnail: MThumbnail, transaction?: Transaction) { thumbnail.videoId = this.id const savedThumbnail = await thumbnail.save({ transaction }) @@ -1917,7 +1771,7 @@ export class VideoModel extends Model { } getWatchStaticPath () { - return '/videos/watch/' + this.uuid + return '/w/' + this.uuid } getEmbedStaticPath () { @@ -2053,11 +1907,7 @@ export class VideoModel extends Model { } setAsRefreshed () { - const options = { - where: { id: this.id } - } - - return VideoModel.update({ updatedAt: new Date() }, options) + return setAsUpdated('video', this.id) } requiresAuth () {