1
0
Fork 0

Use raw SQL for video get request

This commit is contained in:
Chocobozzz 2021-06-10 14:43:55 +02:00
parent e5dbd5084e
commit d9bf974f5d
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
10 changed files with 877 additions and 389 deletions

View File

@ -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<any>(this.query, { replacements: this.replacements, type: QueryTypes.SELECT, nest })
}
}

View File

@ -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
}
}

View File

@ -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<any>(this.query, options)
}
}

View File

@ -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'
]
}
}

View File

@ -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<number>
private historyDone: Set<number>
private blacklistDone: Set<number>
private liveDone: Set<number>
private redundancyDone: Set<number>
private scheduleVideoUpdateDone: Set<number>
private trackersDone: Set<string>
private tagsDone: Set<string>
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<number>()
this.historyDone = new Set<number>()
this.blacklistDone = new Set<number>()
this.liveDone = new Set<number>()
this.redundancyDone = new Set<number>()
this.scheduleVideoUpdateDone = new Set<number>()
this.trackersDone = new Set<string>()
this.tagsDone = new Set<string>()
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)
}
}

View File

@ -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<number>()
const historyDone = new Set<number>()
const videoFilesDone = new Set<number>()
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
}

View File

@ -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'
}
}

View File

@ -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,12 +57,13 @@ export type BuildVideosListQueryOptions = {
}
export class VideosIdListQueryBuilder extends AbstractVideosQueryBuilder {
private attributes: string[]
protected replacements: any = {}
private readonly and: string[] = []
private attributes: string[]
private joins: string[] = []
private readonly and: string[] = []
private readonly cte: string[] = []
private group = ''

View File

@ -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(', ')
}
}

View File

@ -118,6 +118,7 @@ import {
videoModelToFormattedJSON
} from './formatter/video-format-utils'
import { ScheduleVideoUpdateModel } from './schedule-video-update'
import { VideosModelGetQueryBuilder } from './sql/video-model-get-query-builder'
import { BuildVideosListQueryOptions, VideosIdListQueryBuilder } from './sql/videos-id-list-query-builder'
import { VideosModelListQueryBuilder } from './sql/videos-model-list-query-builder'
import { TagModel } from './tag'
@ -1475,33 +1476,9 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
userId?: number
}): Promise<MVideoDetails> {
const { id, t, userId } = parameters
const where = buildWhereIdOrUUID(id)
const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize)
const options = {
order: [ [ 'Tags', 'name', 'ASC' ] ] as any, // FIXME: sequelize typings
where,
transaction: t
}
const scopes: (string | ScopeOptions)[] = [
ScopeNames.WITH_TAGS,
ScopeNames.WITH_BLACKLISTED,
ScopeNames.WITH_ACCOUNT_DETAILS,
ScopeNames.WITH_SCHEDULED_UPDATE,
ScopeNames.WITH_THUMBNAILS,
ScopeNames.WITH_LIVE,
ScopeNames.WITH_TRACKERS,
{ method: [ ScopeNames.WITH_WEBTORRENT_FILES, true ] },
{ method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] }
]
if (userId) {
scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] })
}
return VideoModel
.scope(scopes)
.findOne(options)
return queryBuilder.queryVideos({ id, transaction: t, forGetAPI: true, userId })
}
static async getStats () {