Video file metadata PR cleanup
authorChocobozzz <me@florianbigard.com>
Tue, 10 Mar 2020 13:49:02 +0000 (14:49 +0100)
committerChocobozzz <me@florianbigard.com>
Tue, 10 Mar 2020 15:18:29 +0000 (16:18 +0100)
server/helpers/custom-validators/activitypub/videos.ts
server/helpers/ffmpeg-utils.ts
server/lib/activitypub/videos.ts
server/lib/video-transcoding.ts
server/models/video/video-file.ts
server/models/video/video-format-utils.ts
server/models/video/video.ts
server/tests/api/videos/video-transcoder.ts

index af8c8a0c85dfde07eac907caa0dd3493f2c49cf6..876cc7f50493b90ac24f8a8d839407df1aa38999 100644 (file)
@@ -13,6 +13,7 @@ import {
 import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc'
 import { VideoState } from '../../../../shared/models/videos'
 import { logger } from '@server/helpers/logger'
+import { ActivityVideoFileMetadataObject } from '@shared/models'
 
 function sanitizeAndCheckVideoTorrentUpdateActivity (activity: any) {
   return isBaseActivityValid(activity, 'Update') &&
@@ -104,7 +105,15 @@ function isRemoteVideoUrlValid (url: any) {
       (url.mediaType || url.mimeType) === 'application/x-mpegURL' &&
       isActivityPubUrlValid(url.href) &&
       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,
   isRemoteStringIdentifierValid,
   sanitizeAndCheckVideoTorrentObject,
-  isRemoteVideoUrlValid
+  isRemoteVideoUrlValid,
+  isAPVideoFileMetadataObject
 }
 
 // ---------------------------------------------------------------------------
index 5ee295635eec6d1faaa1ca0430666e483b1fb73e..e0e408ea025436f2deab558977a8d2354b452954 100644 (file)
@@ -170,7 +170,7 @@ async function getVideoFileFPS (path: string) {
   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) => {
     ffmpeg.ffprobe(path, (err, metadata) => {
       if (err) return rej(err)
index 30de4714c2b5c8b93bf8ebd61191d75136128a83..452e43c8cca9022ad22d333e141bd3c68cddda37 100644 (file)
@@ -9,13 +9,13 @@ import {
   ActivityPlaylistUrlObject,
   ActivityTagObject,
   ActivityUrlObject,
+  ActivityVideoFileMetadataObject,
   ActivityVideoUrlObject,
-  VideoState,
-  ActivityVideoFileMetadataObject
+  VideoState
 } from '../../../shared/index'
 import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
 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 { deleteNonExistingModels, resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils'
 import { logger } from '../../helpers/logger'
@@ -26,7 +26,8 @@ import {
   P2P_MEDIA_LOADER_PEER_VERSION,
   PREVIEWS_SIZE,
   REMOTE_SCHEME,
-  STATIC_PATHS, THUMBNAILS_SIZE
+  STATIC_PATHS,
+  THUMBNAILS_SIZE
 } from '../../initializers/constants'
 import { TagModel } from '../../models/video/tag'
 import { VideoModel } from '../../models/video/video'
@@ -69,7 +70,8 @@ import {
   MVideoAPWithoutCaption,
   MVideoFile,
   MVideoFullLight,
-  MVideoId, MVideoImmutable,
+  MVideoId,
+  MVideoImmutable,
   MVideoThumbnail
 } from '../../typings/models'
 import { MThumbnail } from '../../typings/models/video/thumbnail'
@@ -527,10 +529,6 @@ function isAPHashTagObject (url: any): url is ActivityHashTagObject {
   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) {
   logger.debug('Adding remote video %s.', videoObject.id)
 
@@ -701,11 +699,11 @@ function videoFileActivityUrlToDBAttributes (
 
     // Fetch associated metadata url, if any
     const metadata = urls.filter(isAPVideoFileMetadataObject)
-                          .find(u =>
-                            u.height === fileUrl.height &&
-                            u.fps === fileUrl.fps &&
-                            u.rel.includes(fileUrl.mediaType)
-                          )
+                         .find(u => {
+                           return u.height === fileUrl.height &&
+                             u.fps === fileUrl.fps &&
+                             u.rel.includes(fileUrl.mediaType)
+                         })
 
     const mediaType = fileUrl.mediaType
     const attribute = {
index 444b0d954be1dc674d3d198798b98a09ec735249..bde4c2633ff1de483593afbe905d3e4ed953743f 100644 (file)
@@ -237,12 +237,9 @@ async function onVideoFileTranscoding (video: MVideoWithFile, videoFile: MVideoF
 
   await move(transcodingPath, outputPath)
 
-  const extractedVideo = extractVideo(video)
-
   videoFile.size = stats.size
   videoFile.fps = fps
   videoFile.metadata = metadata
-  videoFile.metadataUrl = extractedVideo.getVideoFileMetadataUrl(videoFile, extractedVideo.getBaseUrls().baseUrlHttp)
 
   await createTorrentAndSetInfoHash(video, videoFile)
 
index 0294680046036274b437c96cb3eb94319374e82c..201f0c0f1410d25201a632f05f47e58a0a048c04 100644 (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 { MStreamingPlaylistVideo, MVideo } from '@server/typings/models'
 import * as memoizee from 'memoizee'
+import validator from 'validator'
 
 export enum ScopeNames {
   WITH_VIDEO = 'WITH_VIDEO',
-  WITH_VIDEO_OR_PLAYLIST = 'WITH_VIDEO_OR_PLAYLIST',
   WITH_METADATA = 'WITH_METADATA'
 }
 
-const METADATA_FIELDS = [ 'metadata', 'metadataUrl' ]
-
 @DefaultScope(() => ({
   attributes: {
-    exclude: [ METADATA_FIELDS[0] ]
+    exclude: [ 'metadata' ]
   }
 }))
 @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]: {
     attributes: {
-      include: METADATA_FIELDS
+      include: [ 'metadata' ]
     }
   }
 }))
@@ -223,10 +195,8 @@ export class VideoFileModel extends Model<VideoFileModel> {
 
   static async doesVideoExistForVideoFile (id: number, videoIdOrUUID: number | string) {
     const videoFile = await VideoFileModel.loadWithVideoOrPlaylist(id, videoIdOrUUID)
-    return (videoFile?.Video.id === videoIdOrUUID) ||
-           (videoFile?.Video.uuid === videoIdOrUUID) ||
-           (videoFile?.VideoStreamingPlaylist?.Video?.id === videoIdOrUUID) ||
-           (videoFile?.VideoStreamingPlaylist?.Video?.uuid === videoIdOrUUID)
+
+    return !!videoFile
   }
 
   static loadWithMetadata (id: number) {
@@ -238,12 +208,41 @@ export class VideoFileModel extends Model<VideoFileModel> {
   }
 
   static loadWithVideoOrPlaylist (id: number, videoIdOrUUID: number | string) {
-    return VideoFileModel.scope({
-      method: [
-        ScopeNames.WITH_VIDEO_OR_PLAYLIST,
-        videoIdOrUUID
+    const whereVideo = validator.isUUID(videoIdOrUUID + '')
+      ? { uuid: videoIdOrUUID }
+      : { id: 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) {
index 21f0e0a68b7767f8544a4c0351d5959c149dcd4b..365c9581e60ff25aa687a7d11d97e1d92db3cd1f 100644 (file)
@@ -181,6 +181,8 @@ function videoFilesModelToFormattedJSON (
   baseUrlWs: string,
   videoFiles: MVideoFileRedundanciesOpt[]
 ): VideoFile[] {
+  const video = extractVideo(model)
+
   return videoFiles
     .map(videoFile => {
       return {
@@ -195,7 +197,7 @@ function videoFilesModelToFormattedJSON (
         torrentDownloadUrl: model.getTorrentDownloadUrl(videoFile, baseUrlHttp),
         fileUrl: model.getVideoFileUrl(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
     })
     .sort((a, b) => {
index 5e4b7d44c0b86f0162bbd02912a801258216276c..958a49e6586ccdbf6af1b77e3da7e74d3f95a006 100644 (file)
@@ -1849,7 +1849,8 @@ export class VideoModel extends Model<VideoModel> {
 
   getVideoFileMetadataUrl (videoFile: MVideoFile, baseUrlHttp: string) {
     const path = '/api/v1/videos/'
-    return videoFile.metadata
+
+    return this.isOwned()
       ? baseUrlHttp + path + this.uuid + '/metadata/' + videoFile.id
       : videoFile.metadataUrl
   }
index ce0dd14d50a7665af5145afbafac7c7461ffe254..13b3530b1870a71621141a80250372aa902d0708 100644 (file)
@@ -27,13 +27,14 @@ import {
   root,
   ServerInfo,
   setAccessTokensToServers,
-  uploadVideo,
+  uploadVideo, uploadVideoAndGetId,
   waitJobs,
   webtorrentAdd
 } from '../../../../shared/extra-utils'
 import { join } from 'path'
 import { VIDEO_TRANSCODING_FPS } from '../../../../server/initializers/constants'
 import { FfprobeData } from 'fluent-ffmpeg'
+import { VideoFileMetadata } from '@shared/models/videos/video-file-metadata'
 
 const expect = chai.expect
 
@@ -470,61 +471,56 @@ describe('Test video transcoding', function () {
   it('Should provide valid ffprobe data', async function () {
     this.timeout(160000)
 
-    const videoAttributes = {
-      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)
-
+    const videoUUID = (await uploadVideoAndGetId({ server: servers[1], videoName: 'ffprobe data' })).uuid
     await waitJobs(servers)
 
-    const res = await getVideosList(servers[1].url)
+    {
+      const path = join(root(), 'test' + servers[1].internalServerNumber, 'videos', videoUUID + '-240.mp4')
+      const metadata = await getMetadataFromFile<VideoFileMetadata>(path)
 
-    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
+      // expected format properties
+      for (const p of [
+        'tags.encoder',
+        'format_long_name',
+        'size',
+        'bit_rate'
+      ]) {
+        expect(metadata.format).to.have.nested.property(p)
+      }
 
-    {
-      const path = join(root(), 'test' + servers[1].internalServerNumber, 'videos', videoOnOrigin.uuid + '-240.mp4')
-      const metadata = await getMetadataFromFile(path)
+      // expected stream properties
       for (const p of [
-        // expected format properties
-        'format.encoder',
-        'format.format_long_name',
-        'format.size',
-        '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'
+        'codec_long_name',
+        'profile',
+        'width',
+        'height',
+        'display_aspect_ratio',
+        'avg_frame_rate',
+        'pix_fmt'
       ]) {
-        expect(metadata).to.have.nested.property(p)
+        expect(metadata.streams[0]).to.have.nested.property(p)
       }
+
       expect(metadata).to.not.have.nested.property('format.filename')
     }
 
     for (const server of servers) {
-      const res = await getVideosList(server.url)
-
-      const video = res.body.data.find(v => v.name === videoAttributes.name)
-      const res2 = await getVideo(server.url, video.id)
-      const videoDetails = res2.body
+      const res2 = await getVideo(server.url, videoUUID)
+      const videoDetails: VideoDetails = res2.body
 
       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.metadataUrl).to.exist
         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 metadata: FfprobeData = res3.body
         expect(metadata).to.have.nested.property('format.size')
-        expect(metadata.format.size).to.equal(videoOnOriginDetails.files[index].metadata.format.size)
       }
     }
   })