Use raw SQL for most of video queries
This commit is contained in:
parent
3c79c2ce86
commit
71d4af1efc
16 changed files with 208 additions and 198 deletions
|
@ -36,7 +36,7 @@ async function run () {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const video = await VideoModel.loadByUUID(options.video)
|
const video = await VideoModel.load(options.video)
|
||||||
if (!video) throw new Error('Video not found.')
|
if (!video) throw new Error('Video not found.')
|
||||||
if (video.isOwned() === false) throw new Error('Cannot import files of a non owned video.')
|
if (video.isOwned() === false) throw new Error('Cannot import files of a non owned video.')
|
||||||
|
|
||||||
|
@ -45,7 +45,7 @@ async function run () {
|
||||||
filePath: resolve(options.import)
|
filePath: resolve(options.import)
|
||||||
}
|
}
|
||||||
|
|
||||||
await JobQueue.Instance.init()
|
JobQueue.Instance.init()
|
||||||
await JobQueue.Instance.createJobWithPromise({ type: 'video-file-import', payload: dataInput })
|
await JobQueue.Instance.createJobWithPromise({ type: 'video-file-import', payload: dataInput })
|
||||||
console.log('Import job for video %s created.', video.uuid)
|
console.log('Import job for video %s created.', video.uuid)
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,7 +89,7 @@ async function pruneDirectory (directory: string, existFun: ExistFun) {
|
||||||
function doesVideoExist (keepOnlyOwned: boolean) {
|
function doesVideoExist (keepOnlyOwned: boolean) {
|
||||||
return async (file: string) => {
|
return async (file: string) => {
|
||||||
const uuid = getUUIDFromFilename(file)
|
const uuid = getUUIDFromFilename(file)
|
||||||
const video = await VideoModel.loadByUUID(uuid)
|
const video = await VideoModel.load(uuid)
|
||||||
|
|
||||||
return video && (keepOnlyOwned === false || video.isOwned())
|
return video && (keepOnlyOwned === false || video.isOwned())
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,12 +78,12 @@ activityPubClientRouter.get('/accounts?/:name/dislikes/:videoId',
|
||||||
activityPubClientRouter.get('/videos/watch/:id',
|
activityPubClientRouter.get('/videos/watch/:id',
|
||||||
executeIfActivityPub,
|
executeIfActivityPub,
|
||||||
asyncMiddleware(cacheRoute()(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS)),
|
asyncMiddleware(cacheRoute()(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS)),
|
||||||
asyncMiddleware(videosCustomGetValidator('only-video-with-rights')),
|
asyncMiddleware(videosCustomGetValidator('all')),
|
||||||
asyncMiddleware(videoController)
|
asyncMiddleware(videoController)
|
||||||
)
|
)
|
||||||
activityPubClientRouter.get('/videos/watch/:id/activity',
|
activityPubClientRouter.get('/videos/watch/:id/activity',
|
||||||
executeIfActivityPub,
|
executeIfActivityPub,
|
||||||
asyncMiddleware(videosCustomGetValidator('only-video-with-rights')),
|
asyncMiddleware(videosCustomGetValidator('all')),
|
||||||
asyncMiddleware(videoController)
|
asyncMiddleware(videoController)
|
||||||
)
|
)
|
||||||
activityPubClientRouter.get('/videos/watch/:id/announces',
|
activityPubClientRouter.get('/videos/watch/:id/announces',
|
||||||
|
@ -222,8 +222,7 @@ function getAccountVideoRateFactory (rateType: VideoRateType) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function videoController (req: express.Request, res: express.Response) {
|
async function videoController (req: express.Request, res: express.Response) {
|
||||||
// We need more attributes
|
const video = res.locals.videoAll
|
||||||
const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(res.locals.onlyVideoWithRights.id)
|
|
||||||
|
|
||||||
if (redirectIfNotOwned(video.url, res)) return
|
if (redirectIfNotOwned(video.url, res)) return
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { isStreamingPlaylist, MStreamingPlaylistVideo, MVideo } from '@server/ty
|
||||||
import { VideoPrivacy, VideoState } from '@shared/models'
|
import { VideoPrivacy, VideoState } from '@shared/models'
|
||||||
|
|
||||||
function getVideoWithAttributes (res: Response) {
|
function getVideoWithAttributes (res: Response) {
|
||||||
return res.locals.videoAPI || res.locals.videoAll || res.locals.onlyVideo || res.locals.onlyVideoWithRights
|
return res.locals.videoAPI || res.locals.videoAll || res.locals.onlyVideo
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractVideo (videoOrPlaylist: MVideo | MStreamingPlaylistVideo) {
|
function extractVideo (videoOrPlaylist: MVideo | MStreamingPlaylistVideo) {
|
||||||
|
|
|
@ -3,31 +3,30 @@ import {
|
||||||
MVideoAccountLightBlacklistAllFiles,
|
MVideoAccountLightBlacklistAllFiles,
|
||||||
MVideoFormattableDetails,
|
MVideoFormattableDetails,
|
||||||
MVideoFullLight,
|
MVideoFullLight,
|
||||||
MVideoIdThumbnail,
|
MVideoId,
|
||||||
MVideoImmutable,
|
MVideoImmutable,
|
||||||
MVideoThumbnail,
|
MVideoThumbnail,
|
||||||
MVideoWithRights
|
MVideoWithRights
|
||||||
} from '@server/types/models'
|
} from '@server/types/models'
|
||||||
import { Hooks } from '../plugins/hooks'
|
import { Hooks } from '../plugins/hooks'
|
||||||
|
|
||||||
type VideoLoadType = 'for-api' | 'all' | 'only-video' | 'only-video-with-rights' | 'id' | 'none' | 'only-immutable-attributes'
|
type VideoLoadType = 'for-api' | 'all' | 'only-video' | 'id' | 'none' | 'only-immutable-attributes'
|
||||||
|
|
||||||
function loadVideo (id: number | string, fetchType: 'for-api', userId?: number): Promise<MVideoFormattableDetails>
|
function loadVideo (id: number | string, fetchType: 'for-api', userId?: number): Promise<MVideoFormattableDetails>
|
||||||
function loadVideo (id: number | string, fetchType: 'all', userId?: number): Promise<MVideoFullLight>
|
function loadVideo (id: number | string, fetchType: 'all', userId?: number): Promise<MVideoFullLight>
|
||||||
function loadVideo (id: number | string, fetchType: 'only-immutable-attributes'): Promise<MVideoImmutable>
|
function loadVideo (id: number | string, fetchType: 'only-immutable-attributes'): Promise<MVideoImmutable>
|
||||||
function loadVideo (id: number | string, fetchType: 'only-video', userId?: number): Promise<MVideoThumbnail>
|
function loadVideo (id: number | string, fetchType: 'only-video', userId?: number): Promise<MVideoThumbnail>
|
||||||
function loadVideo (id: number | string, fetchType: 'only-video-with-rights', userId?: number): Promise<MVideoWithRights>
|
function loadVideo (id: number | string, fetchType: 'id' | 'none', userId?: number): Promise<MVideoId>
|
||||||
function loadVideo (id: number | string, fetchType: 'id' | 'none', userId?: number): Promise<MVideoIdThumbnail>
|
|
||||||
function loadVideo (
|
function loadVideo (
|
||||||
id: number | string,
|
id: number | string,
|
||||||
fetchType: VideoLoadType,
|
fetchType: VideoLoadType,
|
||||||
userId?: number
|
userId?: number
|
||||||
): Promise<MVideoFullLight | MVideoThumbnail | MVideoWithRights | MVideoIdThumbnail | MVideoImmutable>
|
): Promise<MVideoFullLight | MVideoThumbnail | MVideoId | MVideoImmutable>
|
||||||
function loadVideo (
|
function loadVideo (
|
||||||
id: number | string,
|
id: number | string,
|
||||||
fetchType: VideoLoadType,
|
fetchType: VideoLoadType,
|
||||||
userId?: number
|
userId?: number
|
||||||
): Promise<MVideoFullLight | MVideoThumbnail | MVideoWithRights | MVideoIdThumbnail | MVideoImmutable> {
|
): Promise<MVideoFullLight | MVideoThumbnail | MVideoId | MVideoImmutable> {
|
||||||
|
|
||||||
if (fetchType === 'for-api') {
|
if (fetchType === 'for-api') {
|
||||||
return Hooks.wrapPromiseFun(
|
return Hooks.wrapPromiseFun(
|
||||||
|
@ -41,8 +40,6 @@ function loadVideo (
|
||||||
|
|
||||||
if (fetchType === 'only-immutable-attributes') return VideoModel.loadImmutableAttributes(id)
|
if (fetchType === 'only-immutable-attributes') return VideoModel.loadImmutableAttributes(id)
|
||||||
|
|
||||||
if (fetchType === 'only-video-with-rights') return VideoModel.loadWithRights(id)
|
|
||||||
|
|
||||||
if (fetchType === 'only-video') return VideoModel.load(id)
|
if (fetchType === 'only-video') return VideoModel.load(id)
|
||||||
|
|
||||||
if (fetchType === 'id' || fetchType === 'none') return VideoModel.loadOnlyId(id)
|
if (fetchType === 'id' || fetchType === 'none') return VideoModel.loadOnlyId(id)
|
||||||
|
|
|
@ -8,7 +8,7 @@ import {
|
||||||
MVideoAccountLight,
|
MVideoAccountLight,
|
||||||
MVideoFormattableDetails,
|
MVideoFormattableDetails,
|
||||||
MVideoFullLight,
|
MVideoFullLight,
|
||||||
MVideoIdThumbnail,
|
MVideoId,
|
||||||
MVideoImmutable,
|
MVideoImmutable,
|
||||||
MVideoThumbnail,
|
MVideoThumbnail,
|
||||||
MVideoWithRights
|
MVideoWithRights
|
||||||
|
@ -43,16 +43,12 @@ async function doesVideoExist (id: number | string, res: Response, fetchType: Vi
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'id':
|
case 'id':
|
||||||
res.locals.videoId = video as MVideoIdThumbnail
|
res.locals.videoId = video as MVideoId
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'only-video':
|
case 'only-video':
|
||||||
res.locals.onlyVideo = video as MVideoThumbnail
|
res.locals.onlyVideo = video as MVideoThumbnail
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'only-video-with-rights':
|
|
||||||
res.locals.onlyVideoWithRights = video as MVideoWithRights
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { getResumableUploadPath } from '@server/helpers/upload'
|
||||||
import { isAbleToUploadVideo } from '@server/lib/user'
|
import { isAbleToUploadVideo } from '@server/lib/user'
|
||||||
import { getServerActor } from '@server/models/application/application'
|
import { getServerActor } from '@server/models/application/application'
|
||||||
import { ExpressPromiseHandler } from '@server/types/express'
|
import { ExpressPromiseHandler } from '@server/types/express'
|
||||||
import { MUserAccountId, MVideoWithRights } from '@server/types/models'
|
import { MUserAccountId, MVideoFullLight } from '@server/types/models'
|
||||||
import { ServerErrorCode, UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../../shared'
|
import { ServerErrorCode, UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../../shared'
|
||||||
import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
|
import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
|
||||||
import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/change-ownership/video-change-ownership-accept.model'
|
import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/change-ownership/video-change-ownership-accept.model'
|
||||||
|
@ -258,7 +258,7 @@ async function checkVideoFollowConstraints (req: express.Request, res: express.R
|
||||||
}
|
}
|
||||||
|
|
||||||
const videosCustomGetValidator = (
|
const videosCustomGetValidator = (
|
||||||
fetchType: 'for-api' | 'all' | 'only-video' | 'only-video-with-rights' | 'only-immutable-attributes',
|
fetchType: 'for-api' | 'all' | 'only-video' | 'only-immutable-attributes',
|
||||||
authenticateInQuery = false
|
authenticateInQuery = false
|
||||||
) => {
|
) => {
|
||||||
return [
|
return [
|
||||||
|
@ -273,7 +273,7 @@ const videosCustomGetValidator = (
|
||||||
// Controllers does not need to check video rights
|
// Controllers does not need to check video rights
|
||||||
if (fetchType === 'only-immutable-attributes') return next()
|
if (fetchType === 'only-immutable-attributes') return next()
|
||||||
|
|
||||||
const video = getVideoWithAttributes(res) as MVideoWithRights
|
const video = getVideoWithAttributes(res) as MVideoFullLight
|
||||||
|
|
||||||
// Video private or blacklisted
|
// Video private or blacklisted
|
||||||
if (video.requiresAuth()) {
|
if (video.requiresAuth()) {
|
||||||
|
|
|
@ -80,6 +80,18 @@ export class AbstractVideosModelQueryBuilder extends AbstractVideosQueryBuilder
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected includeOwnerUser () {
|
||||||
|
this.addJoin('INNER JOIN "videoChannel" AS "VideoChannel" ON "video"."channelId" = "VideoChannel"."id"')
|
||||||
|
this.addJoin('INNER JOIN "account" AS "VideoChannel->Account" ON "VideoChannel"."accountId" = "VideoChannel->Account"."id"')
|
||||||
|
|
||||||
|
this.attributes = {
|
||||||
|
...this.attributes,
|
||||||
|
|
||||||
|
...this.buildAttributesObject('VideoChannel', this.tables.getChannelAttributes()),
|
||||||
|
...this.buildAttributesObject('VideoChannel->Account', this.tables.getUserAccountAttributes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected includeThumbnails () {
|
protected includeThumbnails () {
|
||||||
this.addJoin('LEFT OUTER JOIN "thumbnail" AS "Thumbnails" ON "video"."id" = "Thumbnails"."videoId"')
|
this.addJoin('LEFT OUTER JOIN "thumbnail" AS "Thumbnails" ON "video"."id" = "Thumbnails"."videoId"')
|
||||||
|
|
||||||
|
@ -269,14 +281,20 @@ export class AbstractVideosModelQueryBuilder extends AbstractVideosQueryBuilder
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
protected whereId (id: string | number) {
|
protected whereId (options: { id?: string | number, url?: string }) {
|
||||||
if (validator.isInt('' + id)) {
|
if (options.url) {
|
||||||
|
this.where = 'WHERE "video"."url" = :videoUrl'
|
||||||
|
this.replacements.videoUrl = options.url
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validator.isInt('' + options.id)) {
|
||||||
this.where = 'WHERE "video".id = :videoId'
|
this.where = 'WHERE "video".id = :videoId'
|
||||||
} else {
|
} else {
|
||||||
this.where = 'WHERE uuid = :videoId'
|
this.where = 'WHERE uuid = :videoId'
|
||||||
}
|
}
|
||||||
|
|
||||||
this.replacements.videoId = id
|
this.replacements.videoId = options.id
|
||||||
}
|
}
|
||||||
|
|
||||||
protected addJoin (join: string) {
|
protected addJoin (join: string) {
|
||||||
|
|
|
@ -13,16 +13,17 @@ export class AbstractVideosQueryBuilder {
|
||||||
protected query: string
|
protected query: string
|
||||||
protected replacements: any = {}
|
protected replacements: any = {}
|
||||||
|
|
||||||
protected runQuery (transaction?: Transaction) {
|
protected runQuery (options: { transaction?: Transaction, logging?: boolean } = {}) {
|
||||||
logger.debug('Running videos query.', { query: this.query, replacements: this.replacements })
|
logger.debug('Running videos query.', { query: this.query, replacements: this.replacements })
|
||||||
|
|
||||||
const options = {
|
const queryOptions = {
|
||||||
transaction,
|
transaction: options.transaction,
|
||||||
|
logging: options.logging,
|
||||||
replacements: this.replacements,
|
replacements: this.replacements,
|
||||||
type: QueryTypes.SELECT as QueryTypes.SELECT,
|
type: QueryTypes.SELECT as QueryTypes.SELECT,
|
||||||
next: false
|
next: false
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.sequelize.query<any>(this.query, options)
|
return this.sequelize.query<any>(this.query, queryOptions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,13 +18,13 @@ export class VideoFileQueryBuilder extends AbstractVideosModelQueryBuilder {
|
||||||
queryWebTorrentVideos (options: BuildVideoGetQueryOptions) {
|
queryWebTorrentVideos (options: BuildVideoGetQueryOptions) {
|
||||||
this.buildWebtorrentFilesQuery(options)
|
this.buildWebtorrentFilesQuery(options)
|
||||||
|
|
||||||
return this.runQuery(options.transaction)
|
return this.runQuery(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
queryStreamingPlaylistVideos (options: BuildVideoGetQueryOptions) {
|
queryStreamingPlaylistVideos (options: BuildVideoGetQueryOptions) {
|
||||||
this.buildVideoStreamingPlaylistFilesQuery(options)
|
this.buildVideoStreamingPlaylistFilesQuery(options)
|
||||||
|
|
||||||
return this.runQuery(options.transaction)
|
return this.runQuery(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildWebtorrentFilesQuery (options: BuildVideoGetQueryOptions) {
|
private buildWebtorrentFilesQuery (options: BuildVideoGetQueryOptions) {
|
||||||
|
@ -34,11 +34,11 @@ export class VideoFileQueryBuilder extends AbstractVideosModelQueryBuilder {
|
||||||
|
|
||||||
this.includeWebtorrentFiles(true)
|
this.includeWebtorrentFiles(true)
|
||||||
|
|
||||||
if (options.forGetAPI === true) {
|
if (this.shouldIncludeRedundancies(options)) {
|
||||||
this.includeWebTorrentRedundancies()
|
this.includeWebTorrentRedundancies()
|
||||||
}
|
}
|
||||||
|
|
||||||
this.whereId(options.id)
|
this.whereId(options)
|
||||||
|
|
||||||
this.query = this.buildQuery()
|
this.query = this.buildQuery()
|
||||||
}
|
}
|
||||||
|
@ -50,11 +50,11 @@ export class VideoFileQueryBuilder extends AbstractVideosModelQueryBuilder {
|
||||||
|
|
||||||
this.includeStreamingPlaylistFiles(true)
|
this.includeStreamingPlaylistFiles(true)
|
||||||
|
|
||||||
if (options.forGetAPI === true) {
|
if (this.shouldIncludeRedundancies(options)) {
|
||||||
this.includeStreamingPlaylistRedundancies()
|
this.includeStreamingPlaylistRedundancies()
|
||||||
}
|
}
|
||||||
|
|
||||||
this.whereId(options.id)
|
this.whereId(options)
|
||||||
|
|
||||||
this.query = this.buildQuery()
|
this.query = this.buildQuery()
|
||||||
}
|
}
|
||||||
|
@ -62,4 +62,8 @@ export class VideoFileQueryBuilder extends AbstractVideosModelQueryBuilder {
|
||||||
private buildQuery () {
|
private buildQuery () {
|
||||||
return `${this.buildSelect()} FROM "video" ${this.joins} ${this.where}`
|
return `${this.buildSelect()} FROM "video" ${this.joins} ${this.where}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private shouldIncludeRedundancies (options: BuildVideoGetQueryOptions) {
|
||||||
|
return options.type === 'api'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
|
|
||||||
import { logger } from '@server/helpers/logger'
|
|
||||||
import { AccountModel } from '@server/models/account/account'
|
import { AccountModel } from '@server/models/account/account'
|
||||||
import { ActorModel } from '@server/models/actor/actor'
|
import { ActorModel } from '@server/models/actor/actor'
|
||||||
import { ActorImageModel } from '@server/models/actor/actor-image'
|
import { ActorImageModel } from '@server/models/actor/actor-image'
|
||||||
|
@ -56,7 +55,7 @@ export class VideoModelBuilder {
|
||||||
this.reinit()
|
this.reinit()
|
||||||
|
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
this.buildVideo(row)
|
this.buildVideoAndAccount(row)
|
||||||
|
|
||||||
const videoModel = this.videosMemo[row.id]
|
const videoModel = this.videosMemo[row.id]
|
||||||
|
|
||||||
|
@ -131,22 +130,10 @@ export class VideoModelBuilder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildVideo (row: SQLRow) {
|
private buildVideoAndAccount (row: SQLRow) {
|
||||||
if (this.videosMemo[row.id]) return
|
if (this.videosMemo[row.id]) return
|
||||||
|
|
||||||
// Build Channel
|
|
||||||
const channelModel = new VideoChannelModel(this.grab(row, this.tables.getChannelAttributes(), 'VideoChannel'), this.buildOpts)
|
|
||||||
channelModel.Actor = this.buildActor(row, 'VideoChannel')
|
|
||||||
|
|
||||||
const accountModel = new AccountModel(this.grab(row, this.tables.getAccountAttributes(), 'VideoChannel.Account'), this.buildOpts)
|
|
||||||
accountModel.Actor = this.buildActor(row, 'VideoChannel.Account')
|
|
||||||
|
|
||||||
channelModel.Account = accountModel
|
|
||||||
|
|
||||||
const videoModel = new VideoModel(this.grab(row, this.tables.getVideoAttributes(), ''), this.buildOpts)
|
const videoModel = new VideoModel(this.grab(row, this.tables.getVideoAttributes(), ''), this.buildOpts)
|
||||||
videoModel.VideoChannel = channelModel
|
|
||||||
|
|
||||||
this.videosMemo[row.id] = videoModel
|
|
||||||
|
|
||||||
videoModel.UserVideoHistories = []
|
videoModel.UserVideoHistories = []
|
||||||
videoModel.Thumbnails = []
|
videoModel.Thumbnails = []
|
||||||
|
@ -155,10 +142,29 @@ export class VideoModelBuilder {
|
||||||
videoModel.Tags = []
|
videoModel.Tags = []
|
||||||
videoModel.Trackers = []
|
videoModel.Trackers = []
|
||||||
|
|
||||||
|
this.buildAccount(row, videoModel)
|
||||||
|
|
||||||
|
this.videosMemo[row.id] = videoModel
|
||||||
|
|
||||||
// Keep rows order
|
// Keep rows order
|
||||||
this.videos.push(videoModel)
|
this.videos.push(videoModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private buildAccount (row: SQLRow, videoModel: VideoModel) {
|
||||||
|
const id = row['VideoChannel.Account.id']
|
||||||
|
if (!id) return
|
||||||
|
|
||||||
|
const channelModel = new VideoChannelModel(this.grab(row, this.tables.getChannelAttributes(), 'VideoChannel'), this.buildOpts)
|
||||||
|
channelModel.Actor = this.buildActor(row, 'VideoChannel')
|
||||||
|
|
||||||
|
const accountModel = new AccountModel(this.grab(row, this.tables.getAccountAttributes(), 'VideoChannel.Account'), this.buildOpts)
|
||||||
|
accountModel.Actor = this.buildActor(row, 'VideoChannel.Account')
|
||||||
|
|
||||||
|
channelModel.Account = accountModel
|
||||||
|
|
||||||
|
videoModel.VideoChannel = channelModel
|
||||||
|
}
|
||||||
|
|
||||||
private buildActor (row: SQLRow, prefix: string) {
|
private buildActor (row: SQLRow, prefix: string) {
|
||||||
const actorPrefix = `${prefix}.Actor`
|
const actorPrefix = `${prefix}.Actor`
|
||||||
const avatarPrefix = `${actorPrefix}.Avatar`
|
const avatarPrefix = `${actorPrefix}.Avatar`
|
||||||
|
|
|
@ -10,6 +10,10 @@ export class VideoTables {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getChannelAttributesForUser () {
|
||||||
|
return [ 'id', 'accountId' ]
|
||||||
|
}
|
||||||
|
|
||||||
getChannelAttributes () {
|
getChannelAttributes () {
|
||||||
let attributeKeys = [
|
let attributeKeys = [
|
||||||
'id',
|
'id',
|
||||||
|
@ -29,6 +33,10 @@ export class VideoTables {
|
||||||
return attributeKeys
|
return attributeKeys
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getUserAccountAttributes () {
|
||||||
|
return [ 'id', 'userId' ]
|
||||||
|
}
|
||||||
|
|
||||||
getAccountAttributes () {
|
getAccountAttributes () {
|
||||||
let attributeKeys = [ 'id', 'name', 'actorId' ]
|
let attributeKeys = [ 'id', 'name', 'actorId' ]
|
||||||
|
|
||||||
|
|
|
@ -11,10 +11,15 @@ import { VideoTables } from './shared/video-tables'
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type BuildVideoGetQueryOptions = {
|
export type BuildVideoGetQueryOptions = {
|
||||||
id: number | string
|
id?: number | string
|
||||||
transaction?: Transaction
|
url?: string
|
||||||
|
|
||||||
|
type: 'api' | 'full-light' | 'account-blacklist-files' | 'all-files' | 'thumbnails' | 'thumbnails-blacklist' | 'id' | 'blacklist-rights'
|
||||||
|
|
||||||
userId?: number
|
userId?: number
|
||||||
forGetAPI?: boolean
|
transaction?: Transaction
|
||||||
|
|
||||||
|
logging?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export class VideosModelGetQueryBuilder {
|
export class VideosModelGetQueryBuilder {
|
||||||
|
@ -32,11 +37,17 @@ export class VideosModelGetQueryBuilder {
|
||||||
this.videoModelBuilder = new VideoModelBuilder('get', new VideoTables('get'))
|
this.videoModelBuilder = new VideoModelBuilder('get', new VideoTables('get'))
|
||||||
}
|
}
|
||||||
|
|
||||||
async queryVideos (options: BuildVideoGetQueryOptions) {
|
async queryVideo (options: BuildVideoGetQueryOptions) {
|
||||||
const [ videoRows, webtorrentFilesRows, streamingPlaylistFilesRows ] = await Promise.all([
|
const [ videoRows, webtorrentFilesRows, streamingPlaylistFilesRows ] = await Promise.all([
|
||||||
this.videoQueryBuilder.queryVideos(options),
|
this.videoQueryBuilder.queryVideos(options),
|
||||||
this.webtorrentFilesQueryBuilder.queryWebTorrentVideos(options),
|
|
||||||
this.streamingPlaylistFilesQueryBuilder.queryStreamingPlaylistVideos(options)
|
this.shouldQueryVideoFiles(options)
|
||||||
|
? this.webtorrentFilesQueryBuilder.queryWebTorrentVideos(options)
|
||||||
|
: Promise.resolve(undefined),
|
||||||
|
|
||||||
|
this.shouldQueryVideoFiles(options)
|
||||||
|
? this.streamingPlaylistFilesQueryBuilder.queryStreamingPlaylistVideos(options)
|
||||||
|
: Promise.resolve(undefined)
|
||||||
])
|
])
|
||||||
|
|
||||||
const videos = this.videoModelBuilder.buildVideosFromRows(videoRows, webtorrentFilesRows, streamingPlaylistFilesRows)
|
const videos = this.videoModelBuilder.buildVideosFromRows(videoRows, webtorrentFilesRows, streamingPlaylistFilesRows)
|
||||||
|
@ -48,6 +59,10 @@ export class VideosModelGetQueryBuilder {
|
||||||
if (videos.length === 0) return null
|
if (videos.length === 0) return null
|
||||||
return videos[0]
|
return videos[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private shouldQueryVideoFiles (options: BuildVideoGetQueryOptions) {
|
||||||
|
return [ 'api', 'full-light', 'account-blacklist-files', 'all-files' ].includes(options.type)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class VideosModelGetQuerySubBuilder extends AbstractVideosModelQueryBuilder {
|
export class VideosModelGetQuerySubBuilder extends AbstractVideosModelQueryBuilder {
|
||||||
|
@ -63,7 +78,7 @@ export class VideosModelGetQuerySubBuilder extends AbstractVideosModelQueryBuild
|
||||||
queryVideos (options: BuildVideoGetQueryOptions) {
|
queryVideos (options: BuildVideoGetQueryOptions) {
|
||||||
this.buildMainGetQuery(options)
|
this.buildMainGetQuery(options)
|
||||||
|
|
||||||
return this.runQuery(options.transaction)
|
return this.runQuery(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildMainGetQuery (options: BuildVideoGetQueryOptions) {
|
private buildMainGetQuery (options: BuildVideoGetQueryOptions) {
|
||||||
|
@ -71,36 +86,91 @@ export class VideosModelGetQuerySubBuilder extends AbstractVideosModelQueryBuild
|
||||||
'"video".*': ''
|
'"video".*': ''
|
||||||
}
|
}
|
||||||
|
|
||||||
this.includeChannels()
|
if (this.shouldIncludeThumbnails(options)) {
|
||||||
this.includeAccounts()
|
this.includeThumbnails()
|
||||||
|
}
|
||||||
|
|
||||||
this.includeTags()
|
if (this.shouldIncludeBlacklisted(options)) {
|
||||||
|
this.includeBlacklisted()
|
||||||
|
}
|
||||||
|
|
||||||
this.includeThumbnails()
|
if (this.shouldIncludeAccount(options)) {
|
||||||
|
this.includeChannels()
|
||||||
|
this.includeAccounts()
|
||||||
|
}
|
||||||
|
|
||||||
this.includeBlacklisted()
|
if (this.shouldIncludeTags(options)) {
|
||||||
|
this.includeTags()
|
||||||
|
}
|
||||||
|
|
||||||
this.includeScheduleUpdate()
|
if (this.shouldIncludeScheduleUpdate(options)) {
|
||||||
|
this.includeScheduleUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
this.includeLive()
|
if (this.shouldIncludeLive(options)) {
|
||||||
|
this.includeLive()
|
||||||
|
}
|
||||||
|
|
||||||
if (options.userId) {
|
if (options.userId && this.shouldIncludeUserHistory(options)) {
|
||||||
this.includeUserHistory(options.userId)
|
this.includeUserHistory(options.userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.forGetAPI === true) {
|
if (this.shouldIncludeOwnerUser(options)) {
|
||||||
|
this.includeOwnerUser()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.shouldIncludeTrackers(options)) {
|
||||||
this.includeTrackers()
|
this.includeTrackers()
|
||||||
}
|
}
|
||||||
|
|
||||||
this.whereId(options.id)
|
this.whereId(options)
|
||||||
|
|
||||||
this.query = this.buildQuery()
|
this.query = this.buildQuery(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildQuery () {
|
private buildQuery (options: BuildVideoGetQueryOptions) {
|
||||||
const order = 'ORDER BY "Tags"."name" ASC'
|
const order = this.shouldIncludeTags(options)
|
||||||
|
? 'ORDER BY "Tags"."name" ASC'
|
||||||
|
: ''
|
||||||
|
|
||||||
const from = `SELECT * FROM "video" ${this.where} LIMIT 1`
|
const from = `SELECT * FROM "video" ${this.where} LIMIT 1`
|
||||||
|
|
||||||
return `${this.buildSelect()} FROM (${from}) AS "video" ${this.joins} ${order}`
|
return `${this.buildSelect()} FROM (${from}) AS "video" ${this.joins} ${order}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private shouldIncludeTrackers (options: BuildVideoGetQueryOptions) {
|
||||||
|
return options.type === 'api'
|
||||||
|
}
|
||||||
|
|
||||||
|
private shouldIncludeLive (options: BuildVideoGetQueryOptions) {
|
||||||
|
return [ 'api', 'full-light' ].includes(options.type)
|
||||||
|
}
|
||||||
|
|
||||||
|
private shouldIncludeScheduleUpdate (options: BuildVideoGetQueryOptions) {
|
||||||
|
return [ 'api', 'full-light' ].includes(options.type)
|
||||||
|
}
|
||||||
|
|
||||||
|
private shouldIncludeTags (options: BuildVideoGetQueryOptions) {
|
||||||
|
return [ 'api', 'full-light' ].includes(options.type)
|
||||||
|
}
|
||||||
|
|
||||||
|
private shouldIncludeUserHistory (options: BuildVideoGetQueryOptions) {
|
||||||
|
return [ 'api', 'full-light' ].includes(options.type)
|
||||||
|
}
|
||||||
|
|
||||||
|
private shouldIncludeAccount (options: BuildVideoGetQueryOptions) {
|
||||||
|
return [ 'api', 'full-light', 'account-blacklist-files' ].includes(options.type)
|
||||||
|
}
|
||||||
|
|
||||||
|
private shouldIncludeBlacklisted (options: BuildVideoGetQueryOptions) {
|
||||||
|
return [ 'api', 'full-light', 'account-blacklist-files', 'thumbnails-blacklist', 'blacklist-rights' ].includes(options.type)
|
||||||
|
}
|
||||||
|
|
||||||
|
private shouldIncludeOwnerUser (options: BuildVideoGetQueryOptions) {
|
||||||
|
return options.type === 'blacklist-rights'
|
||||||
|
}
|
||||||
|
|
||||||
|
private shouldIncludeThumbnails (options: BuildVideoGetQueryOptions) {
|
||||||
|
return [ 'api', 'full-light', 'account-blacklist-files', 'thumbnails', 'thumbnails-blacklist' ].includes(options.type)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,8 @@ export class VideosModelListQueryBuilder extends AbstractVideosModelQueryBuilder
|
||||||
this.buildInnerQuery(options)
|
this.buildInnerQuery(options)
|
||||||
this.buildListQueryFromIdsQuery(options)
|
this.buildListQueryFromIdsQuery(options)
|
||||||
|
|
||||||
return this.runQuery(undefined).then(rows => this.videoModelBuilder.buildVideosFromRows(rows))
|
return this.runQuery()
|
||||||
|
.then(rows => this.videoModelBuilder.buildVideosFromRows(rows))
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildInnerQuery (options: BuildVideosListQueryOptions) {
|
private buildInnerQuery (options: BuildVideosListQueryOptions) {
|
||||||
|
|
|
@ -88,13 +88,12 @@ import {
|
||||||
MVideoFormattableDetails,
|
MVideoFormattableDetails,
|
||||||
MVideoForUser,
|
MVideoForUser,
|
||||||
MVideoFullLight,
|
MVideoFullLight,
|
||||||
MVideoIdThumbnail,
|
MVideoId,
|
||||||
MVideoImmutable,
|
MVideoImmutable,
|
||||||
MVideoThumbnail,
|
MVideoThumbnail,
|
||||||
MVideoThumbnailBlacklist,
|
MVideoThumbnailBlacklist,
|
||||||
MVideoWithAllFiles,
|
MVideoWithAllFiles,
|
||||||
MVideoWithFile,
|
MVideoWithFile
|
||||||
MVideoWithRights
|
|
||||||
} from '../../types/models'
|
} from '../../types/models'
|
||||||
import { MThumbnail } from '../../types/models/video/thumbnail'
|
import { MThumbnail } from '../../types/models/video/thumbnail'
|
||||||
import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../types/models/video/video-file'
|
import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../types/models/video/video-file'
|
||||||
|
@ -1301,27 +1300,16 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
|
||||||
return VideoModel.count(options)
|
return VideoModel.count(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
static load (id: number | string, t?: Transaction): Promise<MVideoThumbnail> {
|
static load (id: number | string, transaction?: Transaction): Promise<MVideoThumbnail> {
|
||||||
const where = buildWhereIdOrUUID(id)
|
const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize)
|
||||||
const options = {
|
|
||||||
where,
|
|
||||||
transaction: t
|
|
||||||
}
|
|
||||||
|
|
||||||
return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options)
|
return queryBuilder.queryVideo({ id, transaction, type: 'thumbnails' })
|
||||||
}
|
}
|
||||||
|
|
||||||
static loadWithBlacklist (id: number | string, t?: Transaction): Promise<MVideoThumbnailBlacklist> {
|
static loadWithBlacklist (id: number | string, transaction?: Transaction): Promise<MVideoThumbnailBlacklist> {
|
||||||
const where = buildWhereIdOrUUID(id)
|
const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize)
|
||||||
const options = {
|
|
||||||
where,
|
|
||||||
transaction: t
|
|
||||||
}
|
|
||||||
|
|
||||||
return VideoModel.scope([
|
return queryBuilder.queryVideo({ id, transaction, type: 'thumbnails-blacklist' })
|
||||||
ScopeNames.WITH_THUMBNAILS,
|
|
||||||
ScopeNames.WITH_BLACKLISTED
|
|
||||||
]).findOne(options)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static loadImmutableAttributes (id: number | string, t?: Transaction): Promise<MVideoImmutable> {
|
static loadImmutableAttributes (id: number | string, t?: Transaction): Promise<MVideoImmutable> {
|
||||||
|
@ -1342,68 +1330,6 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
static loadWithRights (id: number | string, t?: Transaction): Promise<MVideoWithRights> {
|
|
||||||
const where = buildWhereIdOrUUID(id)
|
|
||||||
const options = {
|
|
||||||
where,
|
|
||||||
transaction: t
|
|
||||||
}
|
|
||||||
|
|
||||||
return VideoModel.scope([
|
|
||||||
ScopeNames.WITH_BLACKLISTED,
|
|
||||||
ScopeNames.WITH_USER_ID
|
|
||||||
]).findOne(options)
|
|
||||||
}
|
|
||||||
|
|
||||||
static loadOnlyId (id: number | string, t?: Transaction): Promise<MVideoIdThumbnail> {
|
|
||||||
const where = buildWhereIdOrUUID(id)
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
attributes: [ 'id' ],
|
|
||||||
where,
|
|
||||||
transaction: t
|
|
||||||
}
|
|
||||||
|
|
||||||
return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options)
|
|
||||||
}
|
|
||||||
|
|
||||||
static loadWithFiles (id: number | string, t?: Transaction, logging?: boolean): Promise<MVideoWithAllFiles> {
|
|
||||||
const where = buildWhereIdOrUUID(id)
|
|
||||||
|
|
||||||
const query = {
|
|
||||||
where,
|
|
||||||
transaction: t,
|
|
||||||
logging
|
|
||||||
}
|
|
||||||
|
|
||||||
return VideoModel.scope([
|
|
||||||
ScopeNames.WITH_WEBTORRENT_FILES,
|
|
||||||
ScopeNames.WITH_STREAMING_PLAYLISTS,
|
|
||||||
ScopeNames.WITH_THUMBNAILS
|
|
||||||
]).findOne(query)
|
|
||||||
}
|
|
||||||
|
|
||||||
static loadByUUID (uuid: string): Promise<MVideoThumbnail> {
|
|
||||||
const options = {
|
|
||||||
where: {
|
|
||||||
uuid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options)
|
|
||||||
}
|
|
||||||
|
|
||||||
static loadByUrl (url: string, transaction?: Transaction): Promise<MVideoThumbnail> {
|
|
||||||
const query: FindOptions = {
|
|
||||||
where: {
|
|
||||||
url
|
|
||||||
},
|
|
||||||
transaction
|
|
||||||
}
|
|
||||||
|
|
||||||
return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(query)
|
|
||||||
}
|
|
||||||
|
|
||||||
static loadByUrlImmutableAttributes (url: string, transaction?: Transaction): Promise<MVideoImmutable> {
|
static loadByUrlImmutableAttributes (url: string, transaction?: Transaction): Promise<MVideoImmutable> {
|
||||||
const fun = () => {
|
const fun = () => {
|
||||||
const query: FindOptions = {
|
const query: FindOptions = {
|
||||||
|
@ -1424,50 +1350,34 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
static loadByUrlAndPopulateAccount (url: string, transaction?: Transaction): Promise<MVideoAccountLightBlacklistAllFiles> {
|
static loadOnlyId (id: number | string, transaction?: Transaction): Promise<MVideoId> {
|
||||||
const query: FindOptions = {
|
const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize)
|
||||||
where: {
|
|
||||||
url
|
|
||||||
},
|
|
||||||
transaction
|
|
||||||
}
|
|
||||||
|
|
||||||
return VideoModel.scope([
|
return queryBuilder.queryVideo({ id, transaction, type: 'id' })
|
||||||
ScopeNames.WITH_ACCOUNT_DETAILS,
|
}
|
||||||
ScopeNames.WITH_WEBTORRENT_FILES,
|
|
||||||
ScopeNames.WITH_STREAMING_PLAYLISTS,
|
static loadWithFiles (id: number | string, transaction?: Transaction, logging?: boolean): Promise<MVideoWithAllFiles> {
|
||||||
ScopeNames.WITH_THUMBNAILS,
|
const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize)
|
||||||
ScopeNames.WITH_BLACKLISTED
|
|
||||||
]).findOne(query)
|
return queryBuilder.queryVideo({ id, transaction, type: 'all-files', logging })
|
||||||
|
}
|
||||||
|
|
||||||
|
static loadByUrl (url: string, transaction?: Transaction): Promise<MVideoThumbnail> {
|
||||||
|
const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize)
|
||||||
|
|
||||||
|
return queryBuilder.queryVideo({ url, transaction, type: 'thumbnails' })
|
||||||
|
}
|
||||||
|
|
||||||
|
static loadByUrlAndPopulateAccount (url: string, transaction?: Transaction): Promise<MVideoAccountLightBlacklistAllFiles> {
|
||||||
|
const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize)
|
||||||
|
|
||||||
|
return queryBuilder.queryVideo({ url, transaction, type: 'account-blacklist-files' })
|
||||||
}
|
}
|
||||||
|
|
||||||
static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Transaction, userId?: number): Promise<MVideoFullLight> {
|
static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Transaction, userId?: number): Promise<MVideoFullLight> {
|
||||||
const where = buildWhereIdOrUUID(id)
|
const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize)
|
||||||
|
|
||||||
const options = {
|
return queryBuilder.queryVideo({ id, transaction: t, type: 'full-light', userId })
|
||||||
order: [ [ 'Tags', 'name', 'ASC' ] ] as any,
|
|
||||||
where,
|
|
||||||
transaction: t
|
|
||||||
}
|
|
||||||
|
|
||||||
const scopes: (string | ScopeOptions)[] = [
|
|
||||||
ScopeNames.WITH_TAGS,
|
|
||||||
ScopeNames.WITH_BLACKLISTED,
|
|
||||||
ScopeNames.WITH_ACCOUNT_DETAILS,
|
|
||||||
ScopeNames.WITH_SCHEDULED_UPDATE,
|
|
||||||
ScopeNames.WITH_WEBTORRENT_FILES,
|
|
||||||
ScopeNames.WITH_STREAMING_PLAYLISTS,
|
|
||||||
ScopeNames.WITH_THUMBNAILS,
|
|
||||||
ScopeNames.WITH_LIVE
|
|
||||||
]
|
|
||||||
|
|
||||||
if (userId) {
|
|
||||||
scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] })
|
|
||||||
}
|
|
||||||
|
|
||||||
return VideoModel
|
|
||||||
.scope(scopes)
|
|
||||||
.findOne(options)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static loadForGetAPI (parameters: {
|
static loadForGetAPI (parameters: {
|
||||||
|
@ -1478,7 +1388,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
|
||||||
const { id, transaction, userId } = parameters
|
const { id, transaction, userId } = parameters
|
||||||
const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize)
|
const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize)
|
||||||
|
|
||||||
return queryBuilder.queryVideos({ id, transaction, forGetAPI: true, userId })
|
return queryBuilder.queryVideo({ id, transaction, type: 'api', userId })
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getStats () {
|
static async getStats () {
|
||||||
|
|
4
server/typings/express/index.d.ts
vendored
4
server/typings/express/index.d.ts
vendored
|
@ -11,6 +11,7 @@ import {
|
||||||
MVideoChangeOwnershipFull,
|
MVideoChangeOwnershipFull,
|
||||||
MVideoFile,
|
MVideoFile,
|
||||||
MVideoFormattableDetails,
|
MVideoFormattableDetails,
|
||||||
|
MVideoId,
|
||||||
MVideoImmutable,
|
MVideoImmutable,
|
||||||
MVideoLive,
|
MVideoLive,
|
||||||
MVideoPlaylistFull,
|
MVideoPlaylistFull,
|
||||||
|
@ -106,8 +107,7 @@ declare module 'express' {
|
||||||
videoAll?: MVideoFullLight
|
videoAll?: MVideoFullLight
|
||||||
onlyImmutableVideo?: MVideoImmutable
|
onlyImmutableVideo?: MVideoImmutable
|
||||||
onlyVideo?: MVideoThumbnail
|
onlyVideo?: MVideoThumbnail
|
||||||
onlyVideoWithRights?: MVideoWithRights
|
videoId?: MVideoId
|
||||||
videoId?: MVideoIdThumbnail
|
|
||||||
|
|
||||||
videoLive?: MVideoLive
|
videoLive?: MVideoLive
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue