Add lazy description on server

This commit is contained in:
Chocobozzz 2017-10-30 10:16:27 +01:00
parent 757f0da370
commit 9567011bf0
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
16 changed files with 217 additions and 66 deletions

View File

@ -258,7 +258,7 @@ async function addRemoteVideo (videoToCreateData: RemoteVideoCreateData, fromPod
licence: videoToCreateData.licence, licence: videoToCreateData.licence,
language: videoToCreateData.language, language: videoToCreateData.language,
nsfw: videoToCreateData.nsfw, nsfw: videoToCreateData.nsfw,
description: videoToCreateData.description, description: videoToCreateData.truncatedDescription,
channelId: videoChannel.id, channelId: videoChannel.id,
duration: videoToCreateData.duration, duration: videoToCreateData.duration,
createdAt: videoToCreateData.createdAt, createdAt: videoToCreateData.createdAt,
@ -327,7 +327,7 @@ async function updateRemoteVideo (videoAttributesToUpdate: RemoteVideoUpdateData
videoInstance.set('licence', videoAttributesToUpdate.licence) videoInstance.set('licence', videoAttributesToUpdate.licence)
videoInstance.set('language', videoAttributesToUpdate.language) videoInstance.set('language', videoAttributesToUpdate.language)
videoInstance.set('nsfw', videoAttributesToUpdate.nsfw) videoInstance.set('nsfw', videoAttributesToUpdate.nsfw)
videoInstance.set('description', videoAttributesToUpdate.description) videoInstance.set('description', videoAttributesToUpdate.truncatedDescription)
videoInstance.set('duration', videoAttributesToUpdate.duration) videoInstance.set('duration', videoAttributesToUpdate.duration)
videoInstance.set('createdAt', videoAttributesToUpdate.createdAt) videoInstance.set('createdAt', videoAttributesToUpdate.createdAt)
videoInstance.set('updatedAt', videoAttributesToUpdate.updatedAt) videoInstance.set('updatedAt', videoAttributesToUpdate.updatedAt)

View File

@ -16,7 +16,8 @@ import {
quickAndDirtyUpdateVideoToFriends, quickAndDirtyUpdateVideoToFriends,
addVideoToFriends, addVideoToFriends,
updateVideoToFriends, updateVideoToFriends,
JobScheduler JobScheduler,
fetchRemoteDescription
} from '../../../lib' } from '../../../lib'
import { import {
authenticate, authenticate,
@ -102,6 +103,11 @@ videosRouter.post('/upload',
videosAddValidator, videosAddValidator,
asyncMiddleware(addVideoRetryWrapper) asyncMiddleware(addVideoRetryWrapper)
) )
videosRouter.get('/:id/description',
videosGetValidator,
asyncMiddleware(getVideoDescription)
)
videosRouter.get('/:id', videosRouter.get('/:id',
videosGetValidator, videosGetValidator,
getVideo getVideo
@ -328,6 +334,19 @@ function getVideo (req: express.Request, res: express.Response) {
return res.json(videoInstance.toFormattedDetailsJSON()) return res.json(videoInstance.toFormattedDetailsJSON())
} }
async function getVideoDescription (req: express.Request, res: express.Response) {
const videoInstance = res.locals.video
let description = ''
if (videoInstance.isOwned()) {
description = videoInstance.description
} else {
description = await fetchRemoteDescription(videoInstance)
}
return res.json({ description })
}
async function listVideos (req: express.Request, res: express.Response, next: express.NextFunction) { async function listVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
const resultList = await db.Video.listForApi(req.query.start, req.query.count, req.query.sort) const resultList = await db.Video.listForApi(req.query.start, req.query.count, req.query.sort)

View File

@ -19,7 +19,7 @@ import {
isRemoteVideoLicenceValid, isRemoteVideoLicenceValid,
isRemoteVideoLanguageValid, isRemoteVideoLanguageValid,
isVideoNSFWValid, isVideoNSFWValid,
isVideoDescriptionValid, isVideoTruncatedDescriptionValid,
isVideoDurationValid, isVideoDurationValid,
isVideoFileInfoHashValid, isVideoFileInfoHashValid,
isVideoNameValid, isVideoNameValid,
@ -112,7 +112,7 @@ function isCommonVideoAttributesValid (video: any) {
isRemoteVideoLicenceValid(video.licence) && isRemoteVideoLicenceValid(video.licence) &&
isRemoteVideoLanguageValid(video.language) && isRemoteVideoLanguageValid(video.language) &&
isVideoNSFWValid(video.nsfw) && isVideoNSFWValid(video.nsfw) &&
isVideoDescriptionValid(video.description) && isVideoTruncatedDescriptionValid(video.truncatedDescription) &&
isVideoDurationValid(video.duration) && isVideoDurationValid(video.duration) &&
isVideoNameValid(video.name) && isVideoNameValid(video.name) &&
isVideoTagsValid(video.tags) && isVideoTagsValid(video.tags) &&

View File

@ -54,6 +54,10 @@ function isVideoNSFWValid (value: any) {
return typeof value === 'boolean' || (typeof value === 'string' && validator.isBoolean(value)) return typeof value === 'boolean' || (typeof value === 'string' && validator.isBoolean(value))
} }
function isVideoTruncatedDescriptionValid (value: string) {
return exists(value) && validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.TRUNCATED_DESCRIPTION)
}
function isVideoDescriptionValid (value: string) { function isVideoDescriptionValid (value: string) {
return exists(value) && validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.DESCRIPTION) return exists(value) && validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.DESCRIPTION)
} }
@ -173,6 +177,7 @@ export {
isVideoLicenceValid, isVideoLicenceValid,
isVideoLanguageValid, isVideoLanguageValid,
isVideoNSFWValid, isVideoNSFWValid,
isVideoTruncatedDescriptionValid,
isVideoDescriptionValid, isVideoDescriptionValid,
isVideoDurationValid, isVideoDurationValid,
isVideoFileInfoHashValid, isVideoFileInfoHashValid,

View File

@ -15,7 +15,7 @@ import {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const LAST_MIGRATION_VERSION = 85 const LAST_MIGRATION_VERSION = 90
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -122,7 +122,8 @@ const CONSTRAINTS_FIELDS = {
}, },
VIDEOS: { VIDEOS: {
NAME: { min: 3, max: 120 }, // Length NAME: { min: 3, max: 120 }, // Length
DESCRIPTION: { min: 3, max: 250 }, // Length TRUNCATED_DESCRIPTION: { min: 3, max: 250 }, // Length
DESCRIPTION: { min: 3, max: 3000 }, // Length
EXTNAME: [ '.mp4', '.ogv', '.webm' ], EXTNAME: [ '.mp4', '.ogv', '.webm' ],
INFO_HASH: { min: 40, max: 40 }, // Length, info hash is 20 bytes length but we represent it in hexadecimal so 20 * 2 INFO_HASH: { min: 40, max: 40 }, // Length, info hash is 20 bytes length but we represent it in hexadecimal so 20 * 2
DURATION: { min: 1, max: 7200 }, // Number DURATION: { min: 1, max: 7200 }, // Number

View File

@ -84,9 +84,14 @@ database.init = async (silent: boolean) => {
const filePaths = await getModelFiles(modelDirectory) const filePaths = await getModelFiles(modelDirectory)
for (const filePath of filePaths) { for (const filePath of filePaths) {
const model = sequelize.import(filePath) try {
const model = sequelize.import(filePath)
database[model['name']] = model database[model['name']] = model
} catch (err) {
logger.error('Cannot import database model %s.', filePath, err)
process.exit(0)
}
} }
for (const modelName of Object.keys(database)) { for (const modelName of Object.keys(database)) {

View File

@ -0,0 +1,25 @@
import * as Sequelize from 'sequelize'
async function up (utils: {
transaction: Sequelize.Transaction,
queryInterface: Sequelize.QueryInterface,
sequelize: Sequelize.Sequelize,
db: any
}): Promise<void> {
const q = utils.queryInterface
const data = {
type: Sequelize.STRING(3000),
allowNull: false
}
await q.changeColumn('Videos', 'description', data)
}
function down (options) {
throw new Error('Not implemented.')
}
export {
up,
down
}

View File

@ -349,6 +349,24 @@ function fetchRemotePreview (video: VideoInstance) {
return request.get(REMOTE_SCHEME.HTTP + '://' + host + path) return request.get(REMOTE_SCHEME.HTTP + '://' + host + path)
} }
function fetchRemoteDescription (video: VideoInstance) {
const host = video.VideoChannel.Author.Pod.host
const path = video.getDescriptionPath()
const requestOptions = {
url: REMOTE_SCHEME.HTTP + '://' + host + path,
json: true
}
return new Promise<string>((res, rej) => {
request.get(requestOptions, (err, response, body) => {
if (err) return rej(err)
return res(body.description ? body.description : '')
})
})
}
async function removeFriend (pod: PodInstance) { async function removeFriend (pod: PodInstance) {
const requestParams = { const requestParams = {
method: 'POST' as 'POST', method: 'POST' as 'POST',
@ -407,6 +425,7 @@ export {
getRequestVideoEventScheduler, getRequestVideoEventScheduler,
fetchRemotePreview, fetchRemotePreview,
addVideoChannelToFriends, addVideoChannelToFriends,
fetchRemoteDescription,
updateVideoChannelToFriends, updateVideoChannelToFriends,
removeVideoChannelToFriends removeVideoChannelToFriends
} }

View File

@ -38,6 +38,8 @@ export namespace VideoMethods {
export type GetEmbedPath = (this: VideoInstance) => string export type GetEmbedPath = (this: VideoInstance) => string
export type GetThumbnailPath = (this: VideoInstance) => string export type GetThumbnailPath = (this: VideoInstance) => string
export type GetPreviewPath = (this: VideoInstance) => string export type GetPreviewPath = (this: VideoInstance) => string
export type GetDescriptionPath = (this: VideoInstance) => string
export type GetTruncatedDescription = (this: VideoInstance) => string
// Return thumbnail name // Return thumbnail name
export type GenerateThumbnailFromData = (video: VideoInstance, thumbnailData: string) => Promise<string> export type GenerateThumbnailFromData = (video: VideoInstance, thumbnailData: string) => Promise<string>
@ -135,6 +137,8 @@ export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.In
transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile
getOriginalFileHeight: VideoMethods.GetOriginalFileHeight getOriginalFileHeight: VideoMethods.GetOriginalFileHeight
getEmbedPath: VideoMethods.GetEmbedPath getEmbedPath: VideoMethods.GetEmbedPath
getDescriptionPath: VideoMethods.GetDescriptionPath
getTruncatedDescription : VideoMethods.GetTruncatedDescription
setTags: Sequelize.HasManySetAssociationsMixin<TagAttributes, string> setTags: Sequelize.HasManySetAssociationsMixin<TagAttributes, string>
addVideoFile: Sequelize.HasManyAddAssociationMixin<VideoFileAttributes, string> addVideoFile: Sequelize.HasManyAddAssociationMixin<VideoFileAttributes, string>

View File

@ -6,7 +6,7 @@ import * as parseTorrent from 'parse-torrent'
import { join } from 'path' import { join } from 'path'
import * as Sequelize from 'sequelize' import * as Sequelize from 'sequelize'
import * as Promise from 'bluebird' import * as Promise from 'bluebird'
import { maxBy } from 'lodash' import { maxBy, truncate } from 'lodash'
import { TagInstance } from './tag-interface' import { TagInstance } from './tag-interface'
import { import {
@ -35,7 +35,10 @@ import {
VIDEO_CATEGORIES, VIDEO_CATEGORIES,
VIDEO_LICENCES, VIDEO_LICENCES,
VIDEO_LANGUAGES, VIDEO_LANGUAGES,
THUMBNAILS_SIZE THUMBNAILS_SIZE,
PREVIEWS_SIZE,
CONSTRAINTS_FIELDS,
API_VERSION
} from '../../initializers' } from '../../initializers'
import { removeVideoToFriends } from '../../lib' import { removeVideoToFriends } from '../../lib'
import { VideoResolution } from '../../../shared' import { VideoResolution } from '../../../shared'
@ -48,7 +51,6 @@ import {
VideoMethods VideoMethods
} from './video-interface' } from './video-interface'
import { PREVIEWS_SIZE } from '../../initializers/constants'
let Video: Sequelize.Model<VideoInstance, VideoAttributes> let Video: Sequelize.Model<VideoInstance, VideoAttributes>
let getOriginalFile: VideoMethods.GetOriginalFile let getOriginalFile: VideoMethods.GetOriginalFile
@ -71,6 +73,8 @@ let getVideoFilePath: VideoMethods.GetVideoFilePath
let createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash let createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash
let getOriginalFileHeight: VideoMethods.GetOriginalFileHeight let getOriginalFileHeight: VideoMethods.GetOriginalFileHeight
let getEmbedPath: VideoMethods.GetEmbedPath let getEmbedPath: VideoMethods.GetEmbedPath
let getDescriptionPath: VideoMethods.GetDescriptionPath
let getTruncatedDescription: VideoMethods.GetTruncatedDescription
let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData
let list: VideoMethods.List let list: VideoMethods.List
@ -153,7 +157,7 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
} }
}, },
description: { description: {
type: DataTypes.STRING, type: DataTypes.STRING(CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max),
allowNull: false, allowNull: false,
validate: { validate: {
descriptionValid: value => { descriptionValid: value => {
@ -276,7 +280,9 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
optimizeOriginalVideofile, optimizeOriginalVideofile,
transcodeOriginalVideofile, transcodeOriginalVideofile,
getOriginalFileHeight, getOriginalFileHeight,
getEmbedPath getEmbedPath,
getTruncatedDescription,
getDescriptionPath
] ]
addMethodsToModel(Video, classMethods, instanceMethods) addMethodsToModel(Video, classMethods, instanceMethods)
@ -473,7 +479,7 @@ toFormattedJSON = function (this: VideoInstance) {
language: this.language, language: this.language,
languageLabel, languageLabel,
nsfw: this.nsfw, nsfw: this.nsfw,
description: this.description, description: this.getTruncatedDescription(),
podHost, podHost,
isLocal: this.isOwned(), isLocal: this.isOwned(),
author: this.VideoChannel.Author.name, author: this.VideoChannel.Author.name,
@ -493,59 +499,17 @@ toFormattedJSON = function (this: VideoInstance) {
} }
toFormattedDetailsJSON = function (this: VideoInstance) { toFormattedDetailsJSON = function (this: VideoInstance) {
let podHost const formattedJson = this.toFormattedJSON()
if (this.VideoChannel.Author.Pod) { const detailsJson = {
podHost = this.VideoChannel.Author.Pod.host descriptionPath: this.getDescriptionPath(),
} else {
// It means it's our video
podHost = CONFIG.WEBSERVER.HOST
}
// Maybe our pod is not up to date and there are new categories since our version
let categoryLabel = VIDEO_CATEGORIES[this.category]
if (!categoryLabel) categoryLabel = 'Misc'
// Maybe our pod is not up to date and there are new licences since our version
let licenceLabel = VIDEO_LICENCES[this.licence]
if (!licenceLabel) licenceLabel = 'Unknown'
// Language is an optional attribute
let languageLabel = VIDEO_LANGUAGES[this.language]
if (!languageLabel) languageLabel = 'Unknown'
const json = {
id: this.id,
uuid: this.uuid,
name: this.name,
category: this.category,
categoryLabel,
licence: this.licence,
licenceLabel,
language: this.language,
languageLabel,
nsfw: this.nsfw,
description: this.description,
podHost,
isLocal: this.isOwned(),
author: this.VideoChannel.Author.name,
duration: this.duration,
views: this.views,
likes: this.likes,
dislikes: this.dislikes,
tags: map<TagInstance, string>(this.Tags, 'name'),
thumbnailPath: this.getThumbnailPath(),
previewPath: this.getPreviewPath(),
embedPath: this.getEmbedPath(),
createdAt: this.createdAt,
updatedAt: this.updatedAt,
channel: this.VideoChannel.toFormattedJSON(), channel: this.VideoChannel.toFormattedJSON(),
files: [] files: []
} }
// Format and sort video files // Format and sort video files
const { baseUrlHttp, baseUrlWs } = getBaseUrls(this) const { baseUrlHttp, baseUrlWs } = getBaseUrls(this)
json.files = this.VideoFiles detailsJson.files = this.VideoFiles
.map(videoFile => { .map(videoFile => {
let resolutionLabel = videoFile.resolution + 'p' let resolutionLabel = videoFile.resolution + 'p'
@ -566,7 +530,7 @@ toFormattedDetailsJSON = function (this: VideoInstance) {
return -1 return -1
}) })
return json return Object.assign(formattedJson, detailsJson)
} }
toAddRemoteJSON = function (this: VideoInstance) { toAddRemoteJSON = function (this: VideoInstance) {
@ -581,7 +545,7 @@ toAddRemoteJSON = function (this: VideoInstance) {
licence: this.licence, licence: this.licence,
language: this.language, language: this.language,
nsfw: this.nsfw, nsfw: this.nsfw,
description: this.description, truncatedDescription: this.getTruncatedDescription(),
channelUUID: this.VideoChannel.uuid, channelUUID: this.VideoChannel.uuid,
duration: this.duration, duration: this.duration,
thumbnailData: thumbnailData.toString('binary'), thumbnailData: thumbnailData.toString('binary'),
@ -615,7 +579,7 @@ toUpdateRemoteJSON = function (this: VideoInstance) {
licence: this.licence, licence: this.licence,
language: this.language, language: this.language,
nsfw: this.nsfw, nsfw: this.nsfw,
description: this.description, truncatedDescription: this.getTruncatedDescription(),
duration: this.duration, duration: this.duration,
tags: map<TagInstance, string>(this.Tags, 'name'), tags: map<TagInstance, string>(this.Tags, 'name'),
createdAt: this.createdAt, createdAt: this.createdAt,
@ -638,6 +602,14 @@ toUpdateRemoteJSON = function (this: VideoInstance) {
return json return json
} }
getTruncatedDescription = function (this: VideoInstance) {
const options = {
length: CONSTRAINTS_FIELDS.VIDEOS.TRUNCATED_DESCRIPTION.max
}
return truncate(this.description, options)
}
optimizeOriginalVideofile = function (this: VideoInstance) { optimizeOriginalVideofile = function (this: VideoInstance) {
const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
const newExtname = '.mp4' const newExtname = '.mp4'
@ -730,6 +702,10 @@ getOriginalFileHeight = function (this: VideoInstance) {
return getVideoFileHeight(originalFilePath) return getVideoFileHeight(originalFilePath)
} }
getDescriptionPath = function (this: VideoInstance) {
return `/api/${API_VERSION}/videos/${this.uuid}/description`
}
removeThumbnail = function (this: VideoInstance) { removeThumbnail = function (this: VideoInstance) {
const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName()) const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
return unlinkPromise(thumbnailPath) return unlinkPromise(thumbnailPath)

View File

@ -7,6 +7,7 @@ import './single-pod'
import './video-abuse' import './video-abuse'
import './video-blacklist' import './video-blacklist'
import './video-blacklist-management' import './video-blacklist-management'
import './video-description'
import './multiple-pods' import './multiple-pods'
import './services' import './services'
import './request-schedulers' import './request-schedulers'

View File

@ -0,0 +1,86 @@
/* tslint:disable:no-unused-expression */
import 'mocha'
import * as chai from 'chai'
import {
flushAndRunMultipleServers,
flushTests,
getVideo,
getVideosList,
killallServers,
makeFriends,
ServerInfo,
setAccessTokensToServers,
uploadVideo,
wait,
getVideoDescription
} from '../utils'
const expect = chai.expect
describe('Test video description', function () {
let servers: ServerInfo[] = []
let videoUUID = ''
let longDescription = 'my super description for pod 1'.repeat(50)
before(async function () {
this.timeout(10000)
// Run servers
servers = await flushAndRunMultipleServers(2)
// Get the access tokens
await setAccessTokensToServers(servers)
// Pod 1 makes friend with pod 2
await makeFriends(servers[0].url, servers[0].accessToken)
})
it('Should upload video with long description', async function () {
this.timeout(15000)
const attributes = {
description: longDescription
}
await uploadVideo(servers[0].url, servers[0].accessToken, attributes)
await wait(11000)
const res = await getVideosList(servers[0].url)
videoUUID = res.body.data[0].uuid
})
it('Should have a truncated description on each pod', async function () {
for (const server of servers) {
const res = await getVideo(server.url, videoUUID)
const video = res.body
// 30 characters * 6 -> 240 characters
const truncatedDescription = 'my super description for pod 1'.repeat(8) +
'my supe...'
expect(video.description).to.equal(truncatedDescription)
}
})
it('Should fetch long description on each pod', async function () {
for (const server of servers) {
const res = await getVideo(server.url, videoUUID)
const video = res.body
const res2 = await getVideoDescription(server.url, video.descriptionPath)
expect(res2.body.description).to.equal(longDescription)
}
})
after(async function () {
killallServers(servers)
// Keep the logs if the test failed
if (this['ok']) {
await flushTests()
}
})
})

View File

@ -61,6 +61,14 @@ function getVideo (url: string, id: number | string) {
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
} }
function getVideoDescription (url: string, descriptionPath: string) {
return request(url)
.get(descriptionPath)
.set('Accept', 'application/json')
.expect(200)
.expect('Content-Type', /json/)
}
function getVideosList (url: string) { function getVideosList (url: string) {
const path = '/api/v1/videos' const path = '/api/v1/videos'
@ -263,6 +271,7 @@ function parseTorrentVideo (server: ServerInfo, videoUUID: string, resolution: n
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export { export {
getVideoDescription,
getVideoCategories, getVideoCategories,
getVideoLicences, getVideoLicences,
getVideoLanguages, getVideoLanguages,

View File

@ -9,7 +9,7 @@ export interface RemoteVideoCreateData {
licence: number licence: number
language: number language: number
nsfw: boolean nsfw: boolean
description: string truncatedDescription: string
duration: number duration: number
createdAt: Date createdAt: Date
updatedAt: Date updatedAt: Date

View File

@ -8,7 +8,7 @@ export interface RemoteVideoUpdateData {
licence: number licence: number
language: number language: number
nsfw: boolean nsfw: boolean
description: string truncatedDescription: string
duration: number duration: number
createdAt: Date createdAt: Date
updatedAt: Date updatedAt: Date

View File

@ -37,6 +37,7 @@ export interface Video {
} }
export interface VideoDetails extends Video { export interface VideoDetails extends Video {
descriptionPath: string,
channel: VideoChannel channel: VideoChannel
files: VideoFile[] files: VideoFile[]
} }