import * as Bluebird from 'bluebird'
-import { maxBy, minBy } from 'lodash'
+import { maxBy, minBy, pick } from 'lodash'
import { join } from 'path'
import { FindOptions, IncludeOptions, Op, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize'
import {
import { VideoFile } from '@shared/models/videos/video-file.model'
import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
import { ModelCache } from '@server/models/model-cache'
-import { buildListQuery, BuildVideosQueryOptions } from './video-query-builder'
+import { buildListQuery, BuildVideosQueryOptions, wrapForAPIResults } from './video-query-builder'
+import { buildNSFWFilter } from '@server/helpers/express-utils'
export enum ScopeNames {
AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS',
if (options.withFiles === true) {
query.include.push({
- model: VideoFileModel.unscoped(),
+ model: VideoFileModel,
required: true
})
}
return {
include: [
{
- model: VideoFileModel.unscoped(),
+ model: VideoFileModel,
separate: true, // We may have multiple files, having multiple redundancies so let's separate this join
required: false,
include: subInclude
[ScopeNames.WITH_STREAMING_PLAYLISTS]: (withRedundancies = false) => {
const subInclude: IncludeOptions[] = [
{
- model: VideoFileModel.unscoped(),
+ model: VideoFileModel,
required: false
}
]
@HasMany(() => VideoAbuseModel, {
foreignKey: {
name: 'videoId',
- allowNull: false
+ allowNull: true
},
- onDelete: 'cascade'
+ onDelete: 'set null'
})
VideoAbuses: VideoAbuseModel[]
ModelCache.Instance.invalidateCache('video', instance.id)
}
+ @BeforeDestroy
+ static async saveEssentialDataToAbuses (instance: VideoModel, options) {
+ const tasks: Promise<any>[] = []
+
+ logger.info('Saving video abuses details of video %s.', instance.url)
+
+ if (!Array.isArray(instance.VideoAbuses)) {
+ instance.VideoAbuses = await instance.$get('VideoAbuses')
+
+ if (instance.VideoAbuses.length === 0) return undefined
+ }
+
+ const details = instance.toFormattedDetailsJSON()
+
+ for (const abuse of instance.VideoAbuses) {
+ abuse.deletedVideo = details
+ tasks.push(abuse.save({ transaction: options.transaction }))
+ }
+
+ Promise.all(tasks)
+ .catch(err => {
+ logger.error('Some errors when saving details of video %s in its abuses before destroy hook.', instance.uuid, { err })
+ })
+
+ return undefined
+ }
+
static listLocal (): Bluebird<MVideoWithAllFiles[]> {
const query = {
where: {
remote: false
}
})
- const totalVideos = await VideoModel.count()
let totalLocalVideoViews = await VideoModel.sum('views', {
where: {
remote: false
}
})
+
// Sequelize could return null...
if (!totalLocalVideoViews) totalLocalVideoViews = 0
+ const { total: totalVideos } = await VideoModel.listForApi({
+ start: 0,
+ count: 0,
+ sort: '-publishedAt',
+ nsfw: buildNSFWFilter(),
+ includeLocalVideos: true,
+ withFiles: false
+ })
+
return {
totalLocalVideos,
totalLocalVideoViews,
options: BuildVideosQueryOptions,
countVideos = true
) {
- const { query, replacements } = buildListQuery(VideoModel, options)
- const { query: queryCount, replacements: replacementsCount } = buildListQuery(VideoModel, Object.assign({}, options, { isCount: true }))
+ function getCount () {
+ if (countVideos !== true) return Promise.resolve(undefined)
+
+ const countOptions = Object.assign({}, options, { isCount: true })
+ const { query: queryCount, replacements: replacementsCount } = buildListQuery(VideoModel, countOptions)
+
+ return VideoModel.sequelize.query<any>(queryCount, { replacements: replacementsCount, type: QueryTypes.SELECT })
+ .then(rows => rows.length !== 0 ? rows[0].total : 0)
+ }
+
+ function getModels () {
+ if (options.count === 0) return Promise.resolve([])
- const [ count, rows ] = await Promise.all([
- countVideos
- ? this.sequelize.query<any>(queryCount, { replacements: replacementsCount, type: QueryTypes.SELECT })
- .then(rows => rows.length !== 0 ? rows[0].total : 0)
- : Promise.resolve<number>(undefined),
+ const { query, replacements, order } = buildListQuery(VideoModel, options)
+ const queryModels = wrapForAPIResults(query, replacements, options, order)
- this.sequelize.query<any>(query, { replacements, type: QueryTypes.SELECT })
- .then(rows => rows.map(r => r.id))
- .then(ids => VideoModel.loadCompleteVideosForApi(ids, options))
- ])
+ return VideoModel.sequelize.query<any>(queryModels, { replacements, type: QueryTypes.SELECT, nest: true })
+ .then(rows => VideoModel.buildAPIResult(rows))
+ }
+
+ const [ count, rows ] = await Promise.all([ getCount(), getModels() ])
return {
data: rows,
}
}
- private static loadCompleteVideosForApi (ids: number[], options: BuildVideosQueryOptions) {
- if (ids.length === 0) return []
+ private static buildAPIResult (rows: any[]) {
+ const memo: { [ id: number ]: VideoModel } = {}
+
+ 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', 'infoHash', 'fps', 'videoId' ]
+ const videoKeys = [
+ 'id',
+ 'uuid',
+ 'name',
+ 'category',
+ 'licence',
+ 'language',
+ 'privacy',
+ 'nsfw',
+ 'description',
+ 'support',
+ 'duration',
+ 'views',
+ 'likes',
+ 'dislikes',
+ 'remote',
+ 'url',
+ 'commentsEnabled',
+ 'downloadEnabled',
+ 'waitTranscoding',
+ 'state',
+ 'publishedAt',
+ 'originallyPublishedAt',
+ 'channelId',
+ 'createdAt',
+ 'updatedAt'
+ ]
- const secondQuery: FindOptions = {
- offset: 0,
- limit: options.count,
- order: [ // Keep original order
- Sequelize.literal(
- ids.map(id => `"VideoModel".id = ${id} DESC`).join(', ')
- )
- ]
- }
+ function buildActor (rowActor: any) {
+ const avatarModel = rowActor.Avatar.id !== null
+ ? new AvatarModel(pick(rowActor.Avatar, avatarKeys))
+ : null
+
+ const serverModel = rowActor.Server.id !== null
+ ? new ServerModel(pick(rowActor.Server, serverKeys))
+ : null
- const apiScope: (string | ScopeOptions)[] = []
+ const actorModel = new ActorModel(pick(rowActor, actorKeys))
+ actorModel.Avatar = avatarModel
+ actorModel.Server = serverModel
- if (options.user) {
- apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.user.id ] })
+ return actorModel
}
- apiScope.push({
- method: [
- ScopeNames.FOR_API, {
- ids,
- withFiles: options.withFiles,
- videoPlaylistId: options.videoPlaylistId
- } as ForAPIOptions
- ]
- })
+ for (const row of rows) {
+ if (!memo[row.id]) {
+ // Build Channel
+ const channel = row.VideoChannel
+ const channelModel = new VideoChannelModel(pick(channel, [ 'id', 'name', 'description', 'actorId' ]))
+ channelModel.Actor = buildActor(channel.Actor)
+
+ const account = row.VideoChannel.Account
+ const accountModel = new AccountModel(pick(account, [ 'id', 'name' ]))
+ accountModel.Actor = buildActor(account.Actor)
+
+ channelModel.Account = accountModel
+
+ const videoModel = new VideoModel(pick(row, videoKeys))
+ videoModel.VideoChannel = channelModel
+
+ videoModel.UserVideoHistories = []
+ videoModel.Thumbnails = []
+ videoModel.VideoFiles = []
+
+ memo[row.id] = videoModel
+ // Don't take object value to have a sorted array
+ videos.push(videoModel)
+ }
- return VideoModel.scope(apiScope).findAll(secondQuery)
+ const videoModel = memo[row.id]
+
+ if (row.userVideoHistory?.id && !historyDone.has(row.userVideoHistory.id)) {
+ const historyModel = new UserVideoHistoryModel(pick(row.userVideoHistory, [ 'id', 'currentTime' ]))
+ 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' ]))
+ 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))
+ videoModel.VideoFiles.push(videoFileModel)
+
+ videoFilesDone.add(row.VideoFiles.id)
+ }
+ }
+
+ return videos
}
private static isPrivacyForFederation (privacy: VideoPrivacy) {
return baseUrlHttp + STATIC_PATHS.WEBSEED + getVideoFilename(this, videoFile)
}
+ getVideoFileMetadataUrl (videoFile: MVideoFile, baseUrlHttp: string) {
+ const path = '/api/v1/videos/'
+
+ return this.isOwned()
+ ? baseUrlHttp + path + this.uuid + '/metadata/' + videoFile.id
+ : videoFile.metadataUrl
+ }
+
getVideoRedundancyUrl (videoFile: MVideoFile, baseUrlHttp: string) {
return baseUrlHttp + STATIC_PATHS.REDUNDANCY + getVideoFilename(this, videoFile)
}