Video file metadata PR cleanup

pull/2538/head
Chocobozzz 2020-03-10 14:49:02 +01:00
parent 8319d6ae72
commit 7b81edc854
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
8 changed files with 103 additions and 100 deletions

View File

@ -13,6 +13,7 @@ import {
import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc' import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc'
import { VideoState } from '../../../../shared/models/videos' import { VideoState } from '../../../../shared/models/videos'
import { logger } from '@server/helpers/logger' import { logger } from '@server/helpers/logger'
import { ActivityVideoFileMetadataObject } from '@shared/models'
function sanitizeAndCheckVideoTorrentUpdateActivity (activity: any) { function sanitizeAndCheckVideoTorrentUpdateActivity (activity: any) {
return isBaseActivityValid(activity, 'Update') && return isBaseActivityValid(activity, 'Update') &&
@ -104,7 +105,15 @@ function isRemoteVideoUrlValid (url: any) {
(url.mediaType || url.mimeType) === 'application/x-mpegURL' && (url.mediaType || url.mimeType) === 'application/x-mpegURL' &&
isActivityPubUrlValid(url.href) && isActivityPubUrlValid(url.href) &&
isArray(url.tag) isArray(url.tag)
) ) ||
isAPVideoFileMetadataObject(url)
}
function isAPVideoFileMetadataObject (url: any): url is ActivityVideoFileMetadataObject {
return url &&
url.type === 'Link' &&
url.mediaType === 'application/json' &&
isArray(url.rel) && url.rel.includes('metadata')
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -113,7 +122,8 @@ export {
sanitizeAndCheckVideoTorrentUpdateActivity, sanitizeAndCheckVideoTorrentUpdateActivity,
isRemoteStringIdentifierValid, isRemoteStringIdentifierValid,
sanitizeAndCheckVideoTorrentObject, sanitizeAndCheckVideoTorrentObject,
isRemoteVideoUrlValid isRemoteVideoUrlValid,
isAPVideoFileMetadataObject
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -170,7 +170,7 @@ async function getVideoFileFPS (path: string) {
return 0 return 0
} }
async function getMetadataFromFile<T> (path: string, cb = metadata => metadata) { async function getMetadataFromFile <T> (path: string, cb = metadata => metadata) {
return new Promise<T>((res, rej) => { return new Promise<T>((res, rej) => {
ffmpeg.ffprobe(path, (err, metadata) => { ffmpeg.ffprobe(path, (err, metadata) => {
if (err) return rej(err) if (err) return rej(err)

View File

@ -9,13 +9,13 @@ import {
ActivityPlaylistUrlObject, ActivityPlaylistUrlObject,
ActivityTagObject, ActivityTagObject,
ActivityUrlObject, ActivityUrlObject,
ActivityVideoFileMetadataObject,
ActivityVideoUrlObject, ActivityVideoUrlObject,
VideoState, VideoState
ActivityVideoFileMetadataObject
} from '../../../shared/index' } from '../../../shared/index'
import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
import { VideoPrivacy } from '../../../shared/models/videos' import { VideoPrivacy } from '../../../shared/models/videos'
import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos' import { sanitizeAndCheckVideoTorrentObject, isAPVideoFileMetadataObject } from '../../helpers/custom-validators/activitypub/videos'
import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
import { deleteNonExistingModels, resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' import { deleteNonExistingModels, resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils'
import { logger } from '../../helpers/logger' import { logger } from '../../helpers/logger'
@ -26,7 +26,8 @@ import {
P2P_MEDIA_LOADER_PEER_VERSION, P2P_MEDIA_LOADER_PEER_VERSION,
PREVIEWS_SIZE, PREVIEWS_SIZE,
REMOTE_SCHEME, REMOTE_SCHEME,
STATIC_PATHS, THUMBNAILS_SIZE STATIC_PATHS,
THUMBNAILS_SIZE
} from '../../initializers/constants' } from '../../initializers/constants'
import { TagModel } from '../../models/video/tag' import { TagModel } from '../../models/video/tag'
import { VideoModel } from '../../models/video/video' import { VideoModel } from '../../models/video/video'
@ -69,7 +70,8 @@ import {
MVideoAPWithoutCaption, MVideoAPWithoutCaption,
MVideoFile, MVideoFile,
MVideoFullLight, MVideoFullLight,
MVideoId, MVideoImmutable, MVideoId,
MVideoImmutable,
MVideoThumbnail MVideoThumbnail
} from '../../typings/models' } from '../../typings/models'
import { MThumbnail } from '../../typings/models/video/thumbnail' import { MThumbnail } from '../../typings/models/video/thumbnail'
@ -527,10 +529,6 @@ function isAPHashTagObject (url: any): url is ActivityHashTagObject {
return url && url.type === 'Hashtag' return url && url.type === 'Hashtag'
} }
function isAPVideoFileMetadataObject (url: any): url is ActivityVideoFileMetadataObject {
return url && url.type === 'Link' && url.mediaType === 'application/json' && url.hasAttribute('rel') && url.rel.includes('metadata')
}
async function createVideo (videoObject: VideoTorrentObject, channel: MChannelAccountLight, waitThumbnail = false) { async function createVideo (videoObject: VideoTorrentObject, channel: MChannelAccountLight, waitThumbnail = false) {
logger.debug('Adding remote video %s.', videoObject.id) logger.debug('Adding remote video %s.', videoObject.id)
@ -701,11 +699,11 @@ function videoFileActivityUrlToDBAttributes (
// Fetch associated metadata url, if any // Fetch associated metadata url, if any
const metadata = urls.filter(isAPVideoFileMetadataObject) const metadata = urls.filter(isAPVideoFileMetadataObject)
.find(u => .find(u => {
u.height === fileUrl.height && return u.height === fileUrl.height &&
u.fps === fileUrl.fps && u.fps === fileUrl.fps &&
u.rel.includes(fileUrl.mediaType) u.rel.includes(fileUrl.mediaType)
) })
const mediaType = fileUrl.mediaType const mediaType = fileUrl.mediaType
const attribute = { const attribute = {

View File

@ -237,12 +237,9 @@ async function onVideoFileTranscoding (video: MVideoWithFile, videoFile: MVideoF
await move(transcodingPath, outputPath) await move(transcodingPath, outputPath)
const extractedVideo = extractVideo(video)
videoFile.size = stats.size videoFile.size = stats.size
videoFile.fps = fps videoFile.fps = fps
videoFile.metadata = metadata videoFile.metadata = metadata
videoFile.metadataUrl = extractedVideo.getVideoFileMetadataUrl(videoFile, extractedVideo.getBaseUrls().baseUrlHttp)
await createTorrentAndSetInfoHash(video, videoFile) await createTorrentAndSetInfoHash(video, videoFile)

View File

@ -30,18 +30,16 @@ import { MIMETYPES, MEMOIZE_LENGTH, MEMOIZE_TTL } from '../../initializers/const
import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '../../typings/models/video/video-file' import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '../../typings/models/video/video-file'
import { MStreamingPlaylistVideo, MVideo } from '@server/typings/models' import { MStreamingPlaylistVideo, MVideo } from '@server/typings/models'
import * as memoizee from 'memoizee' import * as memoizee from 'memoizee'
import validator from 'validator'
export enum ScopeNames { export enum ScopeNames {
WITH_VIDEO = 'WITH_VIDEO', WITH_VIDEO = 'WITH_VIDEO',
WITH_VIDEO_OR_PLAYLIST = 'WITH_VIDEO_OR_PLAYLIST',
WITH_METADATA = 'WITH_METADATA' WITH_METADATA = 'WITH_METADATA'
} }
const METADATA_FIELDS = [ 'metadata', 'metadataUrl' ]
@DefaultScope(() => ({ @DefaultScope(() => ({
attributes: { attributes: {
exclude: [ METADATA_FIELDS[0] ] exclude: [ 'metadata' ]
} }
})) }))
@Scopes(() => ({ @Scopes(() => ({
@ -53,35 +51,9 @@ const METADATA_FIELDS = [ 'metadata', 'metadataUrl' ]
} }
] ]
}, },
[ScopeNames.WITH_VIDEO_OR_PLAYLIST]: (videoIdOrUUID: string | number) => {
const where = (typeof videoIdOrUUID === 'number')
? { id: videoIdOrUUID }
: { uuid: videoIdOrUUID }
return {
include: [
{
model: VideoModel.unscoped(),
required: false,
where
},
{
model: VideoStreamingPlaylistModel.unscoped(),
required: false,
include: [
{
model: VideoModel.unscoped(),
required: true,
where
}
]
}
]
}
},
[ScopeNames.WITH_METADATA]: { [ScopeNames.WITH_METADATA]: {
attributes: { attributes: {
include: METADATA_FIELDS include: [ 'metadata' ]
} }
} }
})) }))
@ -223,10 +195,8 @@ export class VideoFileModel extends Model<VideoFileModel> {
static async doesVideoExistForVideoFile (id: number, videoIdOrUUID: number | string) { static async doesVideoExistForVideoFile (id: number, videoIdOrUUID: number | string) {
const videoFile = await VideoFileModel.loadWithVideoOrPlaylist(id, videoIdOrUUID) const videoFile = await VideoFileModel.loadWithVideoOrPlaylist(id, videoIdOrUUID)
return (videoFile?.Video.id === videoIdOrUUID) ||
(videoFile?.Video.uuid === videoIdOrUUID) || return !!videoFile
(videoFile?.VideoStreamingPlaylist?.Video?.id === videoIdOrUUID) ||
(videoFile?.VideoStreamingPlaylist?.Video?.uuid === videoIdOrUUID)
} }
static loadWithMetadata (id: number) { static loadWithMetadata (id: number) {
@ -238,12 +208,41 @@ export class VideoFileModel extends Model<VideoFileModel> {
} }
static loadWithVideoOrPlaylist (id: number, videoIdOrUUID: number | string) { static loadWithVideoOrPlaylist (id: number, videoIdOrUUID: number | string) {
return VideoFileModel.scope({ const whereVideo = validator.isUUID(videoIdOrUUID + '')
method: [ ? { uuid: videoIdOrUUID }
ScopeNames.WITH_VIDEO_OR_PLAYLIST, : { id: videoIdOrUUID }
videoIdOrUUID
const options = {
where: {
id
},
include: [
{
model: VideoModel.unscoped(),
required: false,
where: whereVideo
},
{
model: VideoStreamingPlaylistModel.unscoped(),
required: false,
include: [
{
model: VideoModel.unscoped(),
required: true,
where: whereVideo
}
]
}
] ]
}).findByPk(id) }
return VideoFileModel.findOne(options)
.then(file => {
// We used `required: false` so check we have at least a video or a streaming playlist
if (!file.Video && !file.VideoStreamingPlaylist) return null
return file
})
} }
static listByStreamingPlaylist (streamingPlaylistId: number, transaction: Transaction) { static listByStreamingPlaylist (streamingPlaylistId: number, transaction: Transaction) {

View File

@ -181,6 +181,8 @@ function videoFilesModelToFormattedJSON (
baseUrlWs: string, baseUrlWs: string,
videoFiles: MVideoFileRedundanciesOpt[] videoFiles: MVideoFileRedundanciesOpt[]
): VideoFile[] { ): VideoFile[] {
const video = extractVideo(model)
return videoFiles return videoFiles
.map(videoFile => { .map(videoFile => {
return { return {
@ -195,7 +197,7 @@ function videoFilesModelToFormattedJSON (
torrentDownloadUrl: model.getTorrentDownloadUrl(videoFile, baseUrlHttp), torrentDownloadUrl: model.getTorrentDownloadUrl(videoFile, baseUrlHttp),
fileUrl: model.getVideoFileUrl(videoFile, baseUrlHttp), fileUrl: model.getVideoFileUrl(videoFile, baseUrlHttp),
fileDownloadUrl: model.getVideoFileDownloadUrl(videoFile, baseUrlHttp), fileDownloadUrl: model.getVideoFileDownloadUrl(videoFile, baseUrlHttp),
metadataUrl: videoFile.metadataUrl // only send the metadataUrl and not the metadata over the wire metadataUrl: video.getVideoFileMetadataUrl(videoFile, baseUrlHttp)
} as VideoFile } as VideoFile
}) })
.sort((a, b) => { .sort((a, b) => {

View File

@ -1849,7 +1849,8 @@ export class VideoModel extends Model<VideoModel> {
getVideoFileMetadataUrl (videoFile: MVideoFile, baseUrlHttp: string) { getVideoFileMetadataUrl (videoFile: MVideoFile, baseUrlHttp: string) {
const path = '/api/v1/videos/' const path = '/api/v1/videos/'
return videoFile.metadata
return this.isOwned()
? baseUrlHttp + path + this.uuid + '/metadata/' + videoFile.id ? baseUrlHttp + path + this.uuid + '/metadata/' + videoFile.id
: videoFile.metadataUrl : videoFile.metadataUrl
} }

View File

@ -27,13 +27,14 @@ import {
root, root,
ServerInfo, ServerInfo,
setAccessTokensToServers, setAccessTokensToServers,
uploadVideo, uploadVideo, uploadVideoAndGetId,
waitJobs, waitJobs,
webtorrentAdd webtorrentAdd
} from '../../../../shared/extra-utils' } from '../../../../shared/extra-utils'
import { join } from 'path' import { join } from 'path'
import { VIDEO_TRANSCODING_FPS } from '../../../../server/initializers/constants' import { VIDEO_TRANSCODING_FPS } from '../../../../server/initializers/constants'
import { FfprobeData } from 'fluent-ffmpeg' import { FfprobeData } from 'fluent-ffmpeg'
import { VideoFileMetadata } from '@shared/models/videos/video-file-metadata'
const expect = chai.expect const expect = chai.expect
@ -470,61 +471,56 @@ describe('Test video transcoding', function () {
it('Should provide valid ffprobe data', async function () { it('Should provide valid ffprobe data', async function () {
this.timeout(160000) this.timeout(160000)
const videoAttributes = { const videoUUID = (await uploadVideoAndGetId({ server: servers[1], videoName: 'ffprobe data' })).uuid
name: 'my super name for server 1',
description: 'my super description for server 1',
fixture: 'video_short.webm'
}
await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
await waitJobs(servers) await waitJobs(servers)
const res = await getVideosList(servers[1].url)
const videoOnOrigin = res.body.data.find(v => v.name === videoAttributes.name)
const res2 = await getVideo(servers[1].url, videoOnOrigin.id)
const videoOnOriginDetails: VideoDetails = res2.body
{ {
const path = join(root(), 'test' + servers[1].internalServerNumber, 'videos', videoOnOrigin.uuid + '-240.mp4') const path = join(root(), 'test' + servers[1].internalServerNumber, 'videos', videoUUID + '-240.mp4')
const metadata = await getMetadataFromFile(path) const metadata = await getMetadataFromFile<VideoFileMetadata>(path)
// expected format properties
for (const p of [ for (const p of [
// expected format properties 'tags.encoder',
'format.encoder', 'format_long_name',
'format.format_long_name', 'size',
'format.size', 'bit_rate'
'format.bit_rate',
// expected stream properties
'stream[0].codec_long_name',
'stream[0].profile',
'stream[0].width',
'stream[0].height',
'stream[0].display_aspect_ratio',
'stream[0].avg_frame_rate',
'stream[0].pix_fmt'
]) { ]) {
expect(metadata).to.have.nested.property(p) expect(metadata.format).to.have.nested.property(p)
} }
// expected stream properties
for (const p of [
'codec_long_name',
'profile',
'width',
'height',
'display_aspect_ratio',
'avg_frame_rate',
'pix_fmt'
]) {
expect(metadata.streams[0]).to.have.nested.property(p)
}
expect(metadata).to.not.have.nested.property('format.filename') expect(metadata).to.not.have.nested.property('format.filename')
} }
for (const server of servers) { for (const server of servers) {
const res = await getVideosList(server.url) const res2 = await getVideo(server.url, videoUUID)
const videoDetails: VideoDetails = res2.body
const video = res.body.data.find(v => v.name === videoAttributes.name)
const res2 = await getVideo(server.url, video.id)
const videoDetails = res2.body
const videoFiles = videoDetails.files const videoFiles = videoDetails.files
for (const [ index, file ] of videoFiles.entries()) { .concat(videoDetails.streamingPlaylists[0].files)
expect(videoFiles).to.have.lengthOf(8)
for (const file of videoFiles) {
expect(file.metadata).to.be.undefined expect(file.metadata).to.be.undefined
expect(file.metadataUrl).to.exist
expect(file.metadataUrl).to.contain(servers[1].url) expect(file.metadataUrl).to.contain(servers[1].url)
expect(file.metadataUrl).to.contain(videoOnOrigin.uuid) expect(file.metadataUrl).to.contain(videoUUID)
const res3 = await getVideoFileMetadataUrl(file.metadataUrl) const res3 = await getVideoFileMetadataUrl(file.metadataUrl)
const metadata: FfprobeData = res3.body const metadata: FfprobeData = res3.body
expect(metadata).to.have.nested.property('format.size') expect(metadata).to.have.nested.property('format.size')
expect(metadata.format.size).to.equal(videoOnOriginDetails.files[index].metadata.format.size)
} }
} }
}) })