From d9bf974f5df787bbeaab5b04949ca91a2b3ca2a3 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 10 Jun 2021 14:43:55 +0200 Subject: Use raw SQL for video get request --- .../video/sql/abstract-videos-query-builder.ts | 15 -- .../shared/abstract-videos-model-query-builder.ts | 239 ++++++++++++++++++ .../sql/shared/abstract-videos-query-builder.ts | 22 ++ server/models/video/sql/shared/video-attributes.ts | 247 +++++++++++++++++++ .../models/video/sql/shared/video-model-builder.ts | 268 +++++++++++++++++++++ server/models/video/sql/video-model-builder.ts | 162 ------------- .../video/sql/video-model-get-query-builder.ts | 86 +++++++ .../video/sql/videos-id-list-query-builder.ts | 7 +- .../video/sql/videos-model-list-query-builder.ts | 189 +-------------- 9 files changed, 873 insertions(+), 362 deletions(-) delete mode 100644 server/models/video/sql/abstract-videos-query-builder.ts create mode 100644 server/models/video/sql/shared/abstract-videos-model-query-builder.ts create mode 100644 server/models/video/sql/shared/abstract-videos-query-builder.ts create mode 100644 server/models/video/sql/shared/video-attributes.ts create mode 100644 server/models/video/sql/shared/video-model-builder.ts delete mode 100644 server/models/video/sql/video-model-builder.ts create mode 100644 server/models/video/sql/video-model-get-query-builder.ts (limited to 'server/models/video/sql') diff --git a/server/models/video/sql/abstract-videos-query-builder.ts b/server/models/video/sql/abstract-videos-query-builder.ts deleted file mode 100644 index 597a02af7..000000000 --- a/server/models/video/sql/abstract-videos-query-builder.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { logger } from '@server/helpers/logger' -import { Sequelize, QueryTypes } from 'sequelize' - -export class AbstractVideosQueryBuilder { - protected sequelize: Sequelize - - protected query: string - protected replacements: any = {} - - protected runQuery (nest?: boolean) { - logger.info('Running video query.', { query: this.query, replacements: this.replacements }) - - return this.sequelize.query(this.query, { replacements: this.replacements, type: QueryTypes.SELECT, nest }) - } -} diff --git a/server/models/video/sql/shared/abstract-videos-model-query-builder.ts b/server/models/video/sql/shared/abstract-videos-model-query-builder.ts new file mode 100644 index 000000000..bdf926cbe --- /dev/null +++ b/server/models/video/sql/shared/abstract-videos-model-query-builder.ts @@ -0,0 +1,239 @@ +import { AbstractVideosQueryBuilder } from './abstract-videos-query-builder' +import { VideoAttributes } from './video-attributes' +import { VideoModelBuilder } from './video-model-builder' + +export class AbstractVideosModelQueryBuilder extends AbstractVideosQueryBuilder { + protected attributes: { [key: string]: string } = {} + protected joins: string[] = [] + + protected videoAttributes: VideoAttributes + protected videoModelBuilder: VideoModelBuilder + + constructor (private readonly mode: 'list' | 'get') { + super() + + this.videoAttributes = new VideoAttributes(this.mode) + this.videoModelBuilder = new VideoModelBuilder(this.mode, this.videoAttributes) + } + + protected buildSelect () { + return 'SELECT ' + Object.keys(this.attributes).map(key => { + const value = this.attributes[key] + if (value) return `${key} AS ${value}` + + return key + }).join(', ') + } + + protected includeChannels () { + this.joins.push( + 'INNER JOIN "videoChannel" AS "VideoChannel" ON "video"."channelId" = "VideoChannel"."id"', + 'INNER JOIN "actor" AS "VideoChannel->Actor" ON "VideoChannel"."actorId" = "VideoChannel->Actor"."id"', + + 'LEFT OUTER JOIN "server" AS "VideoChannel->Actor->Server" ON "VideoChannel->Actor"."serverId" = "VideoChannel->Actor->Server"."id"', + + 'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Actor->Avatar" ' + + 'ON "VideoChannel->Actor"."avatarId" = "VideoChannel->Actor->Avatar"."id"' + ) + + this.attributes = { + ...this.attributes, + + ...this.buildAttributesObject('VideoChannel', this.videoAttributes.getChannelAttributes()), + ...this.buildActorInclude('VideoChannel->Actor'), + ...this.buildAvatarInclude('VideoChannel->Actor->Avatar'), + ...this.buildServerInclude('VideoChannel->Actor->Server') + } + } + + protected includeAccounts () { + this.joins.push( + 'INNER JOIN "account" AS "VideoChannel->Account" ON "VideoChannel"."accountId" = "VideoChannel->Account"."id"', + 'INNER JOIN "actor" AS "VideoChannel->Account->Actor" ON "VideoChannel->Account"."actorId" = "VideoChannel->Account->Actor"."id"', + + 'LEFT OUTER JOIN "server" AS "VideoChannel->Account->Actor->Server" ' + + 'ON "VideoChannel->Account->Actor"."serverId" = "VideoChannel->Account->Actor->Server"."id"', + + 'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Account->Actor->Avatar" ' + + 'ON "VideoChannel->Account->Actor"."avatarId" = "VideoChannel->Account->Actor->Avatar"."id"' + ) + + this.attributes = { + ...this.attributes, + + ...this.buildAttributesObject('VideoChannel->Account', this.videoAttributes.getAccountAttributes()), + ...this.buildActorInclude('VideoChannel->Account->Actor'), + ...this.buildAvatarInclude('VideoChannel->Account->Actor->Avatar'), + ...this.buildServerInclude('VideoChannel->Account->Actor->Server') + } + } + + protected includeThumbnails () { + this.joins.push('LEFT OUTER JOIN "thumbnail" AS "Thumbnails" ON "video"."id" = "Thumbnails"."videoId"') + + this.attributes = { + ...this.attributes, + + ...this.buildAttributesObject('Thumbnails', this.videoAttributes.getThumbnailAttributes()) + } + } + + protected includeFiles () { + this.joins.push( + 'LEFT JOIN "videoFile" AS "VideoFiles" ON "VideoFiles"."videoId" = "video"."id"', + + 'LEFT JOIN "videoStreamingPlaylist" AS "VideoStreamingPlaylists" ON "VideoStreamingPlaylists"."videoId" = "video"."id"', + + 'LEFT JOIN "videoFile" AS "VideoStreamingPlaylists->VideoFiles" ' + + 'ON "VideoStreamingPlaylists->VideoFiles"."videoStreamingPlaylistId" = "VideoStreamingPlaylists"."id"' + ) + + this.attributes = { + ...this.attributes, + + ...this.buildAttributesObject('VideoFiles', this.videoAttributes.getFileAttributes()), + + ...this.buildAttributesObject('VideoStreamingPlaylists', this.videoAttributes.getStreamingPlaylistAttributes()), + ...this.buildAttributesObject('VideoStreamingPlaylists->VideoFiles', this.videoAttributes.getFileAttributes()) + } + } + + protected includeUserHistory (userId: number) { + this.joins.push( + 'LEFT OUTER JOIN "userVideoHistory" ' + + 'ON "video"."id" = "userVideoHistory"."videoId" AND "userVideoHistory"."userId" = :userVideoHistoryId' + ) + + this.replacements.userVideoHistoryId = userId + + this.attributes = { + ...this.attributes, + + ...this.buildAttributesObject('userVideoHistory', this.videoAttributes.getUserHistoryAttributes()) + } + } + + protected includePlaylist (playlistId: number) { + this.joins.push( + 'INNER JOIN "videoPlaylistElement" as "VideoPlaylistElement" ON "videoPlaylistElement"."videoId" = "video"."id" ' + + 'AND "VideoPlaylistElement"."videoPlaylistId" = :videoPlaylistId' + ) + + this.replacements.videoPlaylistId = playlistId + + this.attributes = { + ...this.attributes, + + ...this.buildAttributesObject('VideoPlaylistElement', this.videoAttributes.getPlaylistAttributes()) + } + } + + protected includeTags () { + this.joins.push( + 'LEFT OUTER JOIN (' + + '"videoTag" AS "Tags->VideoTagModel" INNER JOIN "tag" AS "Tags" ON "Tags"."id" = "Tags->VideoTagModel"."tagId"' + + ') ' + + 'ON "video"."id" = "Tags->VideoTagModel"."videoId"' + ) + + this.attributes = { + ...this.attributes, + + ...this.buildAttributesObject('Tags', this.videoAttributes.getTagAttributes()), + ...this.buildAttributesObject('Tags->VideoTagModel', this.videoAttributes.getVideoTagAttributes()) + } + } + + protected includeBlacklisted () { + this.joins.push( + 'LEFT OUTER JOIN "videoBlacklist" AS "VideoBlacklist" ON "video"."id" = "VideoBlacklist"."videoId"' + ) + + this.attributes = { + ...this.attributes, + + ...this.buildAttributesObject('VideoBlacklist', this.videoAttributes.getBlacklistedAttributes()) + } + } + + protected includeScheduleUpdate () { + this.joins.push( + 'LEFT OUTER JOIN "scheduleVideoUpdate" AS "ScheduleVideoUpdate" ON "video"."id" = "ScheduleVideoUpdate"."videoId"' + ) + + this.attributes = { + ...this.attributes, + + ...this.buildAttributesObject('ScheduleVideoUpdate', this.videoAttributes.getScheduleUpdateAttributes()) + } + } + + protected includeLive () { + this.joins.push( + 'LEFT OUTER JOIN "videoLive" AS "VideoLive" ON "video"."id" = "VideoLive"."videoId"' + ) + + this.attributes = { + ...this.attributes, + + ...this.buildAttributesObject('VideoLive', this.videoAttributes.getLiveAttributes()) + } + } + + protected includeTrackers () { + this.joins.push( + 'LEFT OUTER JOIN (' + + '"videoTracker" AS "Trackers->VideoTrackerModel" ' + + 'INNER JOIN "tracker" AS "Trackers" ON "Trackers"."id" = "Trackers->VideoTrackerModel"."trackerId"' + + ') ON "video"."id" = "Trackers->VideoTrackerModel"."videoId"' + ) + + this.attributes = { + ...this.attributes, + + ...this.buildAttributesObject('Trackers', this.videoAttributes.getTrackerAttributes()), + ...this.buildAttributesObject('Trackers->VideoTrackerModel', this.videoAttributes.getVideoTrackerAttributes()) + } + } + + protected includeRedundancies () { + this.joins.push( + 'LEFT OUTER JOIN "videoRedundancy" AS "VideoStreamingPlaylists->RedundancyVideos" ' + + 'ON "VideoStreamingPlaylists"."id" = "VideoStreamingPlaylists->RedundancyVideos"."videoStreamingPlaylistId"', + + 'LEFT OUTER JOIN "videoRedundancy" AS "VideoFiles->RedundancyVideos" ON ' + + '"VideoFiles"."id" = "VideoFiles->RedundancyVideos"."videoFileId"' + ) + + this.attributes = { + ...this.attributes, + + ...this.buildAttributesObject('VideoFiles->RedundancyVideos', this.videoAttributes.getRedundancyAttributes()), + ...this.buildAttributesObject('VideoStreamingPlaylists->RedundancyVideos', this.videoAttributes.getRedundancyAttributes()) + } + } + + protected buildActorInclude (prefixKey: string) { + return this.buildAttributesObject(prefixKey, this.videoAttributes.getActorAttributes()) + } + + protected buildAvatarInclude (prefixKey: string) { + return this.buildAttributesObject(prefixKey, this.videoAttributes.getAvatarAttributes()) + } + + protected buildServerInclude (prefixKey: string) { + return this.buildAttributesObject(prefixKey, this.videoAttributes.getServerAttributes()) + } + + protected buildAttributesObject (prefixKey: string, attributeKeys: string[]) { + const result: { [id: string]: string} = {} + + const prefixValue = prefixKey.replace(/->/g, '.') + + for (const attribute of attributeKeys) { + result[`"${prefixKey}"."${attribute}"`] = `"${prefixValue}.${attribute}"` + } + + return result + } +} diff --git a/server/models/video/sql/shared/abstract-videos-query-builder.ts b/server/models/video/sql/shared/abstract-videos-query-builder.ts new file mode 100644 index 000000000..01694e691 --- /dev/null +++ b/server/models/video/sql/shared/abstract-videos-query-builder.ts @@ -0,0 +1,22 @@ +import { QueryTypes, Sequelize, Transaction } from 'sequelize' +import { logger } from '@server/helpers/logger' + +export class AbstractVideosQueryBuilder { + protected sequelize: Sequelize + + protected query: string + protected replacements: any = {} + + protected runQuery (transaction?: Transaction, nest?: boolean) { + logger.debug('Running videos query.', { query: this.query, replacements: this.replacements }) + + const options = { + transaction, + replacements: this.replacements, + type: QueryTypes.SELECT as QueryTypes.SELECT, + nest + } + + return this.sequelize.query(this.query, options) + } +} diff --git a/server/models/video/sql/shared/video-attributes.ts b/server/models/video/sql/shared/video-attributes.ts new file mode 100644 index 000000000..1a1650dc7 --- /dev/null +++ b/server/models/video/sql/shared/video-attributes.ts @@ -0,0 +1,247 @@ +export class VideoAttributes { + + constructor (readonly mode: 'get' | 'list') { + + } + + getChannelAttributes () { + let attributeKeys = [ + 'id', + 'name', + 'description', + 'actorId' + ] + + if (this.mode === 'get') { + attributeKeys = attributeKeys.concat([ + 'support', + 'createdAt', + 'updatedAt' + ]) + } + + return attributeKeys + } + + getAccountAttributes () { + let attributeKeys = [ 'id', 'name', 'actorId' ] + + if (this.mode === 'get') { + attributeKeys = attributeKeys.concat([ + 'description', + 'createdAt', + 'updatedAt' + ]) + } + + return attributeKeys + } + + getThumbnailAttributes () { + let attributeKeys = [ 'id', 'type', 'filename' ] + + if (this.mode === 'get') { + attributeKeys = attributeKeys.concat([ + 'height', + 'width', + 'fileUrl', + 'automaticallyGenerated', + 'videoId', + 'videoPlaylistId', + 'createdAt', + 'updatedAt' + ]) + } + + return attributeKeys + } + + getFileAttributes () { + return [ + 'id', + 'createdAt', + 'updatedAt', + 'resolution', + 'size', + 'extname', + 'filename', + 'fileUrl', + 'torrentFilename', + 'torrentUrl', + 'infoHash', + 'fps', + 'metadataUrl', + 'videoStreamingPlaylistId', + 'videoId' + ] + } + + getStreamingPlaylistAttributes () { + let playlistKeys = [ 'id', 'playlistUrl', 'type' ] + + if (this.mode === 'get') { + playlistKeys = playlistKeys.concat([ + 'p2pMediaLoaderInfohashes', + 'p2pMediaLoaderPeerVersion', + 'segmentsSha256Url', + 'videoId', + 'createdAt', + 'updatedAt' + ]) + } + + return playlistKeys + } + + getUserHistoryAttributes () { + return [ 'id', 'currentTime' ] + } + + getPlaylistAttributes () { + return [ + 'createdAt', + 'updatedAt', + 'url', + 'position', + 'startTimestamp', + 'stopTimestamp', + 'videoPlaylistId' + ] + } + + getTagAttributes () { + return [ 'id', 'name' ] + } + + getVideoTagAttributes () { + return [ 'videoId', 'tagId', 'createdAt', 'updatedAt' ] + } + + getBlacklistedAttributes () { + return [ 'id', 'reason', 'unfederated' ] + } + + getScheduleUpdateAttributes () { + return [ + 'id', + 'updateAt', + 'privacy', + 'videoId', + 'createdAt', + 'updatedAt' + ] + } + + getLiveAttributes () { + return [ + 'id', + 'streamKey', + 'saveReplay', + 'permanentLive', + 'videoId', + 'createdAt', + 'updatedAt' + ] + } + + getTrackerAttributes () { + return [ 'id', 'url' ] + } + + getVideoTrackerAttributes () { + return [ + 'videoId', + 'trackerId', + 'createdAt', + 'updatedAt' + ] + } + + getRedundancyAttributes () { + return [ 'id', 'fileUrl' ] + } + + getActorAttributes () { + let attributeKeys = [ + 'id', + 'preferredUsername', + 'url', + 'serverId', + 'avatarId' + ] + + if (this.mode === 'get') { + attributeKeys = attributeKeys.concat([ + 'type', + 'followersCount', + 'followingCount', + 'inboxUrl', + 'outboxUrl', + 'sharedInboxUrl', + 'followersUrl', + 'followingUrl', + 'remoteCreatedAt', + 'createdAt', + 'updatedAt' + ]) + } + + return attributeKeys + } + + getAvatarAttributes () { + let attributeKeys = [ + 'id', + 'filename', + 'fileUrl', + 'onDisk', + 'createdAt', + 'updatedAt' + ] + + if (this.mode === 'get') { + attributeKeys = attributeKeys.concat([ + 'height', + 'width', + 'type' + ]) + } + + return attributeKeys + } + + getServerAttributes () { + return [ 'id', 'host' ] + } + + getVideoAttributes () { + return [ + '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' + ] + } +} diff --git a/server/models/video/sql/shared/video-model-builder.ts b/server/models/video/sql/shared/video-model-builder.ts new file mode 100644 index 000000000..9719f6d2e --- /dev/null +++ b/server/models/video/sql/shared/video-model-builder.ts @@ -0,0 +1,268 @@ +import { pick } from 'lodash' +import { AccountModel } from '@server/models/account/account' +import { ActorModel } from '@server/models/actor/actor' +import { ActorImageModel } from '@server/models/actor/actor-image' +import { VideoRedundancyModel } from '@server/models/redundancy/video-redundancy' +import { ServerModel } from '@server/models/server/server' +import { TrackerModel } from '@server/models/server/tracker' +import { UserVideoHistoryModel } from '@server/models/user/user-video-history' +import { ScheduleVideoUpdateModel } from '../../schedule-video-update' +import { TagModel } from '../../tag' +import { ThumbnailModel } from '../../thumbnail' +import { VideoModel } from '../../video' +import { VideoBlacklistModel } from '../../video-blacklist' +import { VideoChannelModel } from '../../video-channel' +import { VideoFileModel } from '../../video-file' +import { VideoLiveModel } from '../../video-live' +import { VideoStreamingPlaylistModel } from '../../video-streaming-playlist' +import { VideoAttributes } from './video-attributes' + +export class VideoModelBuilder { + private videosMemo: { [ id: number ]: VideoModel } + private videoStreamingPlaylistMemo: { [ id: number ]: VideoStreamingPlaylistModel } + private videoFileMemo: { [ id: number ]: VideoFileModel } + + private thumbnailsDone: Set + private historyDone: Set + private blacklistDone: Set + private liveDone: Set + private redundancyDone: Set + private scheduleVideoUpdateDone: Set + + private trackersDone: Set + private tagsDone: Set + + private videos: VideoModel[] + + private readonly buildOpts = { raw: true, isNewRecord: false } + + constructor ( + readonly mode: 'get' | 'list', + readonly videoAttributes: VideoAttributes + ) { + + } + + buildVideosFromRows (rows: any[]) { + this.reinit() + + for (const row of rows) { + this.buildVideo(row) + + const videoModel = this.videosMemo[row.id] + + this.setUserHistory(row, videoModel) + this.addThumbnail(row, videoModel) + this.addWebTorrentFile(row, videoModel) + + this.addStreamingPlaylist(row, videoModel) + this.addStreamingPlaylistFile(row) + + if (this.mode === 'get') { + this.addTag(row, videoModel) + this.addTracker(row, videoModel) + this.setBlacklisted(row, videoModel) + this.setScheduleVideoUpdate(row, videoModel) + this.setLive(row, videoModel) + + if (row.VideoFiles.id) { + this.addRedundancy(row.VideoFiles.RedundancyVideos, this.videoFileMemo[row.VideoFiles.id]) + } + + if (row.VideoStreamingPlaylists.id) { + this.addRedundancy(row.VideoStreamingPlaylists.RedundancyVideos, this.videoStreamingPlaylistMemo[row.VideoStreamingPlaylists.id]) + } + } + } + + return this.videos + } + + private reinit () { + this.videosMemo = {} + this.videoStreamingPlaylistMemo = {} + this.videoFileMemo = {} + + this.thumbnailsDone = new Set() + this.historyDone = new Set() + this.blacklistDone = new Set() + this.liveDone = new Set() + this.redundancyDone = new Set() + this.scheduleVideoUpdateDone = new Set() + + this.trackersDone = new Set() + this.tagsDone = new Set() + + this.videos = [] + } + + private buildVideo (row: any) { + if (this.videosMemo[row.id]) return + + // Build Channel + const channel = row.VideoChannel + const channelModel = new VideoChannelModel(pick(channel, this.videoAttributes.getChannelAttributes()), this.buildOpts) + channelModel.Actor = this.buildActor(channel.Actor) + + const account = row.VideoChannel.Account + const accountModel = new AccountModel(pick(account, this.videoAttributes.getAccountAttributes()), this.buildOpts) + accountModel.Actor = this.buildActor(account.Actor) + + channelModel.Account = accountModel + + const videoModel = new VideoModel(pick(row, this.videoAttributes.getVideoAttributes()), this.buildOpts) + videoModel.VideoChannel = channelModel + + this.videosMemo[row.id] = videoModel + + videoModel.UserVideoHistories = [] + videoModel.Thumbnails = [] + videoModel.VideoFiles = [] + videoModel.VideoStreamingPlaylists = [] + videoModel.Tags = [] + videoModel.Trackers = [] + + // Keep rows order + this.videos.push(videoModel) + } + + private buildActor (rowActor: any) { + const avatarModel = rowActor.Avatar.id !== null + ? new ActorImageModel(pick(rowActor.Avatar, this.videoAttributes.getAvatarAttributes()), this.buildOpts) + : null + + const serverModel = rowActor.Server.id !== null + ? new ServerModel(pick(rowActor.Server, this.videoAttributes.getServerAttributes()), this.buildOpts) + : null + + const actorModel = new ActorModel(pick(rowActor, this.videoAttributes.getActorAttributes()), this.buildOpts) + actorModel.Avatar = avatarModel + actorModel.Server = serverModel + + return actorModel + } + + private setUserHistory (row: any, videoModel: VideoModel) { + if (!row.userVideoHistory?.id || this.historyDone.has(row.userVideoHistory.id)) return + + const attributes = pick(row.userVideoHistory, this.videoAttributes.getUserHistoryAttributes()) + const historyModel = new UserVideoHistoryModel(attributes, this.buildOpts) + videoModel.UserVideoHistories.push(historyModel) + + this.historyDone.add(row.userVideoHistory.id) + } + + private addThumbnail (row: any, videoModel: VideoModel) { + if (!row.Thumbnails?.id || this.thumbnailsDone.has(row.Thumbnails.id)) return + + const attributes = pick(row.Thumbnails, this.videoAttributes.getThumbnailAttributes()) + const thumbnailModel = new ThumbnailModel(attributes, this.buildOpts) + videoModel.Thumbnails.push(thumbnailModel) + + this.thumbnailsDone.add(row.Thumbnails.id) + } + + private addWebTorrentFile (row: any, videoModel: VideoModel) { + if (!row.VideoFiles?.id || this.videoFileMemo[row.VideoFiles.id]) return + + const attributes = pick(row.VideoFiles, this.videoAttributes.getFileAttributes()) + const videoFileModel = new VideoFileModel(attributes, this.buildOpts) + videoModel.VideoFiles.push(videoFileModel) + + this.videoFileMemo[row.VideoFiles.id] = videoFileModel + } + + private addStreamingPlaylist (row: any, videoModel: VideoModel) { + if (!row.VideoStreamingPlaylists?.id || this.videoStreamingPlaylistMemo[row.VideoStreamingPlaylists.id]) return + + const attributes = pick(row.VideoStreamingPlaylists, this.videoAttributes.getStreamingPlaylistAttributes()) + const streamingPlaylist = new VideoStreamingPlaylistModel(attributes, this.buildOpts) + streamingPlaylist.VideoFiles = [] + + videoModel.VideoStreamingPlaylists.push(streamingPlaylist) + + this.videoStreamingPlaylistMemo[streamingPlaylist.id] = streamingPlaylist + } + + private addStreamingPlaylistFile (row: any) { + if (!row.VideoStreamingPlaylists?.VideoFiles?.id || this.videoFileMemo[row.VideoStreamingPlaylists.VideoFiles.id]) return + + const streamingPlaylist = this.videoStreamingPlaylistMemo[row.VideoStreamingPlaylists.id] + + const attributes = pick(row.VideoStreamingPlaylists.VideoFiles, this.videoAttributes.getFileAttributes()) + const videoFileModel = new VideoFileModel(attributes, this.buildOpts) + streamingPlaylist.VideoFiles.push(videoFileModel) + + this.videoFileMemo[row.VideoStreamingPlaylists.VideoFiles.id] = videoFileModel + } + + private addRedundancy (redundancyRow: any, to: VideoFileModel | VideoStreamingPlaylistModel) { + if (!to.RedundancyVideos) to.RedundancyVideos = [] + + if (!redundancyRow?.id || this.redundancyDone.has(redundancyRow.id)) return + + const attributes = pick(redundancyRow, this.videoAttributes.getRedundancyAttributes()) + const redundancyModel = new VideoRedundancyModel(attributes, this.buildOpts) + to.RedundancyVideos.push(redundancyModel) + + this.redundancyDone.add(redundancyRow.id) + } + + private addTag (row: any, videoModel: VideoModel) { + if (!row.Tags?.name) return + const association = row.Tags.VideoTagModel + + const key = `${association.videoId}-${association.tagId}` + if (this.tagsDone.has(key)) return + + const attributes = pick(row.Tags, this.videoAttributes.getTagAttributes()) + const tagModel = new TagModel(attributes, this.buildOpts) + videoModel.Tags.push(tagModel) + + this.tagsDone.add(key) + } + + private addTracker (row: any, videoModel: VideoModel) { + if (!row.Trackers?.id) return + const association = row.Trackers.VideoTrackerModel + + const key = `${association.videoId}-${association.trackerId}` + if (this.trackersDone.has(key)) return + + const attributes = pick(row.Trackers, this.videoAttributes.getTrackerAttributes()) + const trackerModel = new TrackerModel(attributes, this.buildOpts) + videoModel.Trackers.push(trackerModel) + + this.trackersDone.add(key) + } + + private setBlacklisted (row: any, videoModel: VideoModel) { + if (!row.VideoBlacklist?.id) return + if (this.blacklistDone.has(row.VideoBlacklist.id)) return + + const attributes = pick(row.VideoBlacklist, this.videoAttributes.getBlacklistedAttributes()) + videoModel.VideoBlacklist = new VideoBlacklistModel(attributes, this.buildOpts) + + this.blacklistDone.add(row.VideoBlacklist.id) + } + + private setScheduleVideoUpdate (row: any, videoModel: VideoModel) { + if (!row.ScheduleVideoUpdate?.id) return + if (this.scheduleVideoUpdateDone.has(row.ScheduleVideoUpdate.id)) return + + const attributes = pick(row.ScheduleVideoUpdate, this.videoAttributes.getScheduleUpdateAttributes()) + videoModel.ScheduleVideoUpdate = new ScheduleVideoUpdateModel(attributes, this.buildOpts) + + this.scheduleVideoUpdateDone.add(row.ScheduleVideoUpdate.id) + } + + private setLive (row: any, videoModel: VideoModel) { + if (!row.VideoLive?.id) return + if (this.liveDone.has(row.VideoLive.id)) return + + const attributes = pick(row.VideoLive, this.videoAttributes.getLiveAttributes()) + videoModel.VideoLive = new VideoLiveModel(attributes, this.buildOpts) + + this.liveDone.add(row.ScheduleVideoUpdate.id) + } +} diff --git a/server/models/video/sql/video-model-builder.ts b/server/models/video/sql/video-model-builder.ts deleted file mode 100644 index c428312fe..000000000 --- a/server/models/video/sql/video-model-builder.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { pick } from 'lodash' -import { AccountModel } from '@server/models/account/account' -import { ActorModel } from '@server/models/actor/actor' -import { ActorImageModel } from '@server/models/actor/actor-image' -import { ServerModel } from '@server/models/server/server' -import { UserVideoHistoryModel } from '@server/models/user/user-video-history' -import { ThumbnailModel } from '../thumbnail' -import { VideoModel } from '../video' -import { VideoChannelModel } from '../video-channel' -import { VideoFileModel } from '../video-file' -import { VideoStreamingPlaylistModel } from '../video-streaming-playlist' - -function buildVideosFromRows (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 -} - -export { - buildVideosFromRows -} diff --git a/server/models/video/sql/video-model-get-query-builder.ts b/server/models/video/sql/video-model-get-query-builder.ts new file mode 100644 index 000000000..0a3723e63 --- /dev/null +++ b/server/models/video/sql/video-model-get-query-builder.ts @@ -0,0 +1,86 @@ +import { Sequelize, Transaction } from 'sequelize' +import validator from 'validator' +import { AbstractVideosModelQueryBuilder } from './shared/abstract-videos-model-query-builder' + +export type BuildVideoGetQueryOptions = { + id: number | string + transaction?: Transaction + userId?: number + forGetAPI?: boolean +} + +export class VideosModelGetQueryBuilder extends AbstractVideosModelQueryBuilder { + protected attributes: { [key: string]: string } + protected joins: string[] = [] + protected where: string + + constructor (protected readonly sequelize: Sequelize) { + super('get') + } + + queryVideos (options: BuildVideoGetQueryOptions) { + this.buildGetQuery(options) + + return this.runQuery(options.transaction, true).then(rows => { + const videos = this.videoModelBuilder.buildVideosFromRows(rows) + + if (videos.length > 1) { + throw new Error('Video results is more than ') + } + + if (videos.length === 0) return null + return videos[0] + }) + } + + private buildGetQuery (options: BuildVideoGetQueryOptions) { + this.attributes = { + '"video".*': '' + } + + this.includeChannels() + this.includeAccounts() + + this.includeTags() + + this.includeThumbnails() + + this.includeFiles() + + this.includeBlacklisted() + + this.includeScheduleUpdate() + + this.includeLive() + + if (options.userId) { + this.includeUserHistory(options.userId) + } + + if (options.forGetAPI === true) { + this.includeTrackers() + this.includeRedundancies() + } + + this.whereId(options.id) + + const select = this.buildSelect() + const order = this.buildOrder() + + this.query = `${select} FROM "video" ${this.joins.join(' ')} ${this.where} ${order}` + } + + private whereId (id: string | number) { + if (validator.isInt('' + id)) { + this.where = 'WHERE "video".id = :videoId' + } else { + this.where = 'WHERE uuid = :videoId' + } + + this.replacements.videoId = id + } + + private buildOrder () { + return 'ORDER BY "Tags"."name" ASC' + } +} diff --git a/server/models/video/sql/videos-id-list-query-builder.ts b/server/models/video/sql/videos-id-list-query-builder.ts index 7bb942ea4..6e0d97d9e 100644 --- a/server/models/video/sql/videos-id-list-query-builder.ts +++ b/server/models/video/sql/videos-id-list-query-builder.ts @@ -4,7 +4,7 @@ import { exists } from '@server/helpers/custom-validators/misc' import { buildDirectionAndField, createSafeIn } from '@server/models/utils' import { MUserAccountId, MUserId } from '@server/types/models' import { VideoFilter, VideoPrivacy, VideoState } from '@shared/models' -import { AbstractVideosQueryBuilder } from './abstract-videos-query-builder' +import { AbstractVideosQueryBuilder } from './shared/abstract-videos-query-builder' export type BuildVideosListQueryOptions = { attributes?: string[] @@ -57,11 +57,12 @@ export type BuildVideosListQueryOptions = { } export class VideosIdListQueryBuilder extends AbstractVideosQueryBuilder { + protected replacements: any = {} + private attributes: string[] + private joins: string[] = [] - protected replacements: any = {} private readonly and: string[] = [] - private joins: string[] = [] private readonly cte: string[] = [] diff --git a/server/models/video/sql/videos-model-list-query-builder.ts b/server/models/video/sql/videos-model-list-query-builder.ts index 4ba9dd878..38b9c91d0 100644 --- a/server/models/video/sql/videos-model-list-query-builder.ts +++ b/server/models/video/sql/videos-model-list-query-builder.ts @@ -1,27 +1,23 @@ - -import { MUserId } from '@server/types/models' import { Sequelize } from 'sequelize' -import { AbstractVideosQueryBuilder } from './abstract-videos-query-builder' -import { buildVideosFromRows } from './video-model-builder' +import { AbstractVideosModelQueryBuilder } from './shared/abstract-videos-model-query-builder' import { BuildVideosListQueryOptions, VideosIdListQueryBuilder } from './videos-id-list-query-builder' -export class VideosModelListQueryBuilder extends AbstractVideosQueryBuilder { - private attributes: { [key: string]: string } - - private joins: string[] = [] +export class VideosModelListQueryBuilder extends AbstractVideosModelQueryBuilder { + protected attributes: { [key: string]: string } + protected joins: string[] = [] private innerQuery: string private innerSort: string constructor (protected readonly sequelize: Sequelize) { - super() + super('list') } queryVideos (options: BuildVideosListQueryOptions) { this.buildInnerQuery(options) this.buildListQueryFromIdsQuery(options) - return this.runQuery(true).then(rows => buildVideosFromRows(rows)) + return this.runQuery(undefined, true).then(rows => this.videoModelBuilder.buildVideosFromRows(rows)) } private buildInnerQuery (options: BuildVideosListQueryOptions) { @@ -49,7 +45,7 @@ export class VideosModelListQueryBuilder extends AbstractVideosQueryBuilder { } if (options.user) { - this.includeUserHistory(options.user) + this.includeUserHistory(options.user.id) } if (options.videoPlaylistId) { @@ -60,175 +56,4 @@ export class VideosModelListQueryBuilder extends AbstractVideosQueryBuilder { this.query = `${select} FROM (${this.innerQuery}) AS "tmp" ${this.joins.join(' ')} ${this.innerSort}` } - - private includeChannels () { - this.attributes = { - ...this.attributes, - - '"VideoChannel"."id"': '"VideoChannel.id"', - '"VideoChannel"."name"': '"VideoChannel.name"', - '"VideoChannel"."description"': '"VideoChannel.description"', - '"VideoChannel"."actorId"': '"VideoChannel.actorId"', - '"VideoChannel->Actor"."id"': '"VideoChannel.Actor.id"', - '"VideoChannel->Actor"."preferredUsername"': '"VideoChannel.Actor.preferredUsername"', - '"VideoChannel->Actor"."url"': '"VideoChannel.Actor.url"', - '"VideoChannel->Actor"."serverId"': '"VideoChannel.Actor.serverId"', - '"VideoChannel->Actor"."avatarId"': '"VideoChannel.Actor.avatarId"', - '"VideoChannel->Actor->Server"."id"': '"VideoChannel.Actor.Server.id"', - '"VideoChannel->Actor->Server"."host"': '"VideoChannel.Actor.Server.host"', - '"VideoChannel->Actor->Avatar"."id"': '"VideoChannel.Actor.Avatar.id"', - '"VideoChannel->Actor->Avatar"."filename"': '"VideoChannel.Actor.Avatar.filename"', - '"VideoChannel->Actor->Avatar"."fileUrl"': '"VideoChannel.Actor.Avatar.fileUrl"', - '"VideoChannel->Actor->Avatar"."onDisk"': '"VideoChannel.Actor.Avatar.onDisk"', - '"VideoChannel->Actor->Avatar"."createdAt"': '"VideoChannel.Actor.Avatar.createdAt"', - '"VideoChannel->Actor->Avatar"."updatedAt"': '"VideoChannel.Actor.Avatar.updatedAt"' - } - - this.joins = this.joins.concat([ - 'INNER JOIN "videoChannel" AS "VideoChannel" ON "video"."channelId" = "VideoChannel"."id"', - 'INNER JOIN "actor" AS "VideoChannel->Actor" ON "VideoChannel"."actorId" = "VideoChannel->Actor"."id"', - - 'LEFT OUTER JOIN "server" AS "VideoChannel->Actor->Server" ON "VideoChannel->Actor"."serverId" = "VideoChannel->Actor->Server"."id"', - 'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Actor->Avatar" ' + - 'ON "VideoChannel->Actor"."avatarId" = "VideoChannel->Actor->Avatar"."id"' - ]) - } - - private includeAccounts () { - this.attributes = { - ...this.attributes, - - '"VideoChannel->Account"."id"': '"VideoChannel.Account.id"', - '"VideoChannel->Account"."name"': '"VideoChannel.Account.name"', - '"VideoChannel->Account->Actor"."id"': '"VideoChannel.Account.Actor.id"', - '"VideoChannel->Account->Actor"."preferredUsername"': '"VideoChannel.Account.Actor.preferredUsername"', - '"VideoChannel->Account->Actor"."url"': '"VideoChannel.Account.Actor.url"', - '"VideoChannel->Account->Actor"."serverId"': '"VideoChannel.Account.Actor.serverId"', - '"VideoChannel->Account->Actor"."avatarId"': '"VideoChannel.Account.Actor.avatarId"', - '"VideoChannel->Account->Actor->Server"."id"': '"VideoChannel.Account.Actor.Server.id"', - '"VideoChannel->Account->Actor->Server"."host"': '"VideoChannel.Account.Actor.Server.host"', - '"VideoChannel->Account->Actor->Avatar"."id"': '"VideoChannel.Account.Actor.Avatar.id"', - '"VideoChannel->Account->Actor->Avatar"."filename"': '"VideoChannel.Account.Actor.Avatar.filename"', - '"VideoChannel->Account->Actor->Avatar"."fileUrl"': '"VideoChannel.Account.Actor.Avatar.fileUrl"', - '"VideoChannel->Account->Actor->Avatar"."onDisk"': '"VideoChannel.Account.Actor.Avatar.onDisk"', - '"VideoChannel->Account->Actor->Avatar"."createdAt"': '"VideoChannel.Account.Actor.Avatar.createdAt"', - '"VideoChannel->Account->Actor->Avatar"."updatedAt"': '"VideoChannel.Account.Actor.Avatar.updatedAt"' - } - - this.joins = this.joins.concat([ - 'INNER JOIN "account" AS "VideoChannel->Account" ON "VideoChannel"."accountId" = "VideoChannel->Account"."id"', - 'INNER JOIN "actor" AS "VideoChannel->Account->Actor" ON "VideoChannel->Account"."actorId" = "VideoChannel->Account->Actor"."id"', - - 'LEFT OUTER JOIN "server" AS "VideoChannel->Account->Actor->Server" ' + - 'ON "VideoChannel->Account->Actor"."serverId" = "VideoChannel->Account->Actor->Server"."id"', - - 'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Account->Actor->Avatar" ' + - 'ON "VideoChannel->Account->Actor"."avatarId" = "VideoChannel->Account->Actor->Avatar"."id"' - ]) - } - - private includeThumbnails () { - this.attributes = { - ...this.attributes, - - '"Thumbnails"."id"': '"Thumbnails.id"', - '"Thumbnails"."type"': '"Thumbnails.type"', - '"Thumbnails"."filename"': '"Thumbnails.filename"' - } - - this.joins.push('LEFT OUTER JOIN "thumbnail" AS "Thumbnails" ON "video"."id" = "Thumbnails"."videoId"') - } - - private includeFiles () { - this.attributes = { - ...this.attributes, - - '"VideoFiles"."id"': '"VideoFiles.id"', - '"VideoFiles"."createdAt"': '"VideoFiles.createdAt"', - '"VideoFiles"."updatedAt"': '"VideoFiles.updatedAt"', - '"VideoFiles"."resolution"': '"VideoFiles.resolution"', - '"VideoFiles"."size"': '"VideoFiles.size"', - '"VideoFiles"."extname"': '"VideoFiles.extname"', - '"VideoFiles"."filename"': '"VideoFiles.filename"', - '"VideoFiles"."fileUrl"': '"VideoFiles.fileUrl"', - '"VideoFiles"."torrentFilename"': '"VideoFiles.torrentFilename"', - '"VideoFiles"."torrentUrl"': '"VideoFiles.torrentUrl"', - '"VideoFiles"."infoHash"': '"VideoFiles.infoHash"', - '"VideoFiles"."fps"': '"VideoFiles.fps"', - '"VideoFiles"."videoId"': '"VideoFiles.videoId"', - - '"VideoStreamingPlaylists"."id"': '"VideoStreamingPlaylists.id"', - '"VideoStreamingPlaylists"."playlistUrl"': '"VideoStreamingPlaylists.playlistUrl"', - '"VideoStreamingPlaylists"."type"': '"VideoStreamingPlaylists.type"', - '"VideoStreamingPlaylists->VideoFiles"."id"': '"VideoStreamingPlaylists.VideoFiles.id"', - '"VideoStreamingPlaylists->VideoFiles"."createdAt"': '"VideoStreamingPlaylists.VideoFiles.createdAt"', - '"VideoStreamingPlaylists->VideoFiles"."updatedAt"': '"VideoStreamingPlaylists.VideoFiles.updatedAt"', - '"VideoStreamingPlaylists->VideoFiles"."resolution"': '"VideoStreamingPlaylists.VideoFiles.resolution"', - '"VideoStreamingPlaylists->VideoFiles"."size"': '"VideoStreamingPlaylists.VideoFiles.size"', - '"VideoStreamingPlaylists->VideoFiles"."extname"': '"VideoStreamingPlaylists.VideoFiles.extname"', - '"VideoStreamingPlaylists->VideoFiles"."filename"': '"VideoStreamingPlaylists.VideoFiles.filename"', - '"VideoStreamingPlaylists->VideoFiles"."fileUrl"': '"VideoStreamingPlaylists.VideoFiles.fileUrl"', - '"VideoStreamingPlaylists->VideoFiles"."torrentFilename"': '"VideoStreamingPlaylists.VideoFiles.torrentFilename"', - '"VideoStreamingPlaylists->VideoFiles"."torrentUrl"': '"VideoStreamingPlaylists.VideoFiles.torrentUrl"', - '"VideoStreamingPlaylists->VideoFiles"."infoHash"': '"VideoStreamingPlaylists.VideoFiles.infoHash"', - '"VideoStreamingPlaylists->VideoFiles"."fps"': '"VideoStreamingPlaylists.VideoFiles.fps"', - '"VideoStreamingPlaylists->VideoFiles"."videoStreamingPlaylistId"': '"VideoStreamingPlaylists.VideoFiles.videoStreamingPlaylistId"', - '"VideoStreamingPlaylists->VideoFiles"."videoId"': '"VideoStreamingPlaylists.VideoFiles.videoId"' - } - - this.joins = this.joins.concat([ - 'LEFT JOIN "videoFile" AS "VideoFiles" ON "VideoFiles"."videoId" = "video"."id"', - - 'LEFT JOIN "videoStreamingPlaylist" AS "VideoStreamingPlaylists" ON "VideoStreamingPlaylists"."videoId" = "video"."id"', - - 'LEFT JOIN "videoFile" AS "VideoStreamingPlaylists->VideoFiles" ' + - 'ON "VideoStreamingPlaylists->VideoFiles"."videoStreamingPlaylistId" = "VideoStreamingPlaylists"."id"' - ]) - } - - private includeUserHistory (user: MUserId) { - this.attributes = { - ...this.attributes, - - '"userVideoHistory"."id"': '"userVideoHistory.id"', - '"userVideoHistory"."currentTime"': '"userVideoHistory.currentTime"' - } - - this.joins.push( - 'LEFT OUTER JOIN "userVideoHistory" ' + - 'ON "video"."id" = "userVideoHistory"."videoId" AND "userVideoHistory"."userId" = :userVideoHistoryId' - ) - - this.replacements.userVideoHistoryId = user.id - } - - private includePlaylist (playlistId: number) { - this.attributes = { - ...this.attributes, - - '"VideoPlaylistElement"."createdAt"': '"VideoPlaylistElement.createdAt"', - '"VideoPlaylistElement"."updatedAt"': '"VideoPlaylistElement.updatedAt"', - '"VideoPlaylistElement"."url"': '"VideoPlaylistElement.url"', - '"VideoPlaylistElement"."position"': '"VideoPlaylistElement.position"', - '"VideoPlaylistElement"."startTimestamp"': '"VideoPlaylistElement.startTimestamp"', - '"VideoPlaylistElement"."stopTimestamp"': '"VideoPlaylistElement.stopTimestamp"', - '"VideoPlaylistElement"."videoPlaylistId"': '"VideoPlaylistElement.videoPlaylistId"' - } - - this.joins.push( - 'INNER JOIN "videoPlaylistElement" as "VideoPlaylistElement" ON "videoPlaylistElement"."videoId" = "video"."id" ' + - 'AND "VideoPlaylistElement"."videoPlaylistId" = :videoPlaylistId' - ) - - this.replacements.videoPlaylistId = playlistId - } - - private buildSelect () { - return 'SELECT ' + Object.keys(this.attributes).map(key => { - const value = this.attributes[key] - if (value) return `${key} AS ${value}` - - return key - }).join(', ') - } } -- cgit v1.2.3