Add url field in caption and use it for thumbnails
authorChocobozzz <me@florianbigard.com>
Thu, 30 Jan 2020 10:53:38 +0000 (11:53 +0100)
committerChocobozzz <me@florianbigard.com>
Thu, 30 Jan 2020 10:53:38 +0000 (11:53 +0100)
20 files changed:
server/controllers/api/videos/captions.ts
server/helpers/activitypub.ts
server/helpers/core-utils.ts
server/helpers/custom-validators/activitypub/videos.ts
server/initializers/constants.ts
server/initializers/migrations/0480-caption-file-url.ts [new file with mode: 0644]
server/lib/activitypub/videos.ts
server/lib/files-cache/videos-caption-cache.ts
server/lib/files-cache/videos-preview-cache.ts
server/lib/job-queue/handlers/video-views.ts
server/lib/job-queue/job-queue.ts
server/models/video/thumbnail.ts
server/models/video/video-caption.ts
server/models/video/video-format-utils.ts
server/models/video/video.ts
server/typings/models/video/video-caption.ts
server/typings/models/video/video.ts
shared/models/activitypub/objects/common-objects.ts
shared/models/activitypub/objects/video-torrent-object.ts
shared/models/users/user-role.ts

index 37481d12f525b4291868bd626934d5a214a41df4..fd7b165fb9ca45f735214f3c8336c2ccb75f89d9 100644 (file)
@@ -66,7 +66,7 @@ async function addVideoCaption (req: express.Request, res: express.Response) {
   await moveAndProcessCaptionFile(videoCaptionPhysicalFile, videoCaption)
 
   await sequelizeTypescript.transaction(async t => {
-    await VideoCaptionModel.insertOrReplaceLanguage(video.id, req.params.captionLanguage, t)
+    await VideoCaptionModel.insertOrReplaceLanguage(video.id, req.params.captionLanguage, null, t)
 
     // Update video update
     await federateVideoIfNeeded(video, false, t)
index 239d8291d8ef9936b4cb183f020eec0c45f44546..9f9e8fba79fe2b31d9e64af34c865e454d6df99b 100644 (file)
@@ -2,11 +2,11 @@ import * as Bluebird from 'bluebird'
 import validator from 'validator'
 import { ResultList } from '../../shared/models'
 import { Activity } from '../../shared/models/activitypub'
-import { ACTIVITY_PUB } from '../initializers/constants'
+import { ACTIVITY_PUB, REMOTE_SCHEME } from '../initializers/constants'
 import { signJsonLDObject } from './peertube-crypto'
 import { pageToStartAndCount } from './core-utils'
 import { parse } from 'url'
-import { MActor } from '../typings/models'
+import { MActor, MVideoAccountLight } from '../typings/models'
 
 function activityPubContextify <T> (data: T) {
   return Object.assign(data, {
@@ -167,6 +167,12 @@ function checkUrlsSameHost (url1: string, url2: string) {
   return idHost && actorHost && idHost.toLowerCase() === actorHost.toLowerCase()
 }
 
+function buildRemoteVideoBaseUrl (video: MVideoAccountLight, path: string) {
+  const host = video.VideoChannel.Account.Actor.Server.host
+
+  return REMOTE_SCHEME.HTTP + '://' + host + path
+}
+
 // ---------------------------------------------------------------------------
 
 export {
@@ -174,5 +180,6 @@ export {
   getAPId,
   activityPubContextify,
   activityPubCollectionPagination,
-  buildSignedActivity
+  buildSignedActivity,
+  buildRemoteVideoBaseUrl
 }
index 7e8252aa40f3db496d367303335ad9ab39b94f63..519dc83d0e4d09beec402f4a6316b9afa4c0ea1b 100644 (file)
@@ -199,6 +199,8 @@ function sha1 (str: string | Buffer, encoding: HexBase64Latin1Encoding = 'hex')
   return createHash('sha1').update(str).digest(encoding)
 }
 
+
+
 function execShell (command: string, options?: ExecOptions) {
   return new Promise<{ err?: Error, stdout: string, stderr: string }>((res, rej) => {
     exec(command, options, (err, stdout, stderr) => {
index 224f03f4ee20c3b39c792a59ae2fbcbf929f5ae5..22b5e14a2bbed65a24c8269db0b40401e472b5c4 100644 (file)
@@ -51,6 +51,10 @@ function sanitizeAndCheckVideoTorrentObject (video: any) {
     logger.debug('Video has invalid captions', { video })
     return false
   }
+  if (!setValidRemoteIcon(video)) {
+    logger.debug('Video has invalid icons', { video })
+    return false
+  }
 
   // Default attributes
   if (!isVideoStateValid(video.state)) video.state = VideoState.PUBLISHED
@@ -73,7 +77,6 @@ function sanitizeAndCheckVideoTorrentObject (video: any) {
     isDateValid(video.updated) &&
     (!video.originallyPublishedAt || isDateValid(video.originallyPublishedAt)) &&
     (!video.content || isRemoteVideoContentValid(video.mediaType, video.content)) &&
-    isRemoteVideoIconValid(video.icon) &&
     video.url.length !== 0 &&
     video.attributedTo.length !== 0
 }
@@ -132,6 +135,8 @@ function setValidRemoteCaptions (video: any) {
   if (Array.isArray(video.subtitleLanguage) === false) return false
 
   video.subtitleLanguage = video.subtitleLanguage.filter(caption => {
+    if (!isActivityPubUrlValid(caption.url)) caption.url = null
+
     return isRemoteStringIdentifierValid(caption)
   })
 
@@ -150,12 +155,19 @@ function isRemoteVideoContentValid (mediaType: string, content: string) {
   return mediaType === 'text/markdown' && isVideoTruncatedDescriptionValid(content)
 }
 
-function isRemoteVideoIconValid (icon: any) {
-  return icon.type === 'Image' &&
-    isActivityPubUrlValid(icon.url) &&
-    icon.mediaType === 'image/jpeg' &&
-    validator.isInt(icon.width + '', { min: 0 }) &&
-    validator.isInt(icon.height + '', { min: 0 })
+function setValidRemoteIcon (video: any) {
+  if (video.icon && !isArray(video.icon)) video.icon = [ video.icon ]
+  if (!video.icon) video.icon = []
+
+  video.icon = video.icon.filter(icon => {
+    return icon.type === 'Image' &&
+      isActivityPubUrlValid(icon.url) &&
+      icon.mediaType === 'image/jpeg' &&
+      validator.isInt(icon.width + '', { min: 0 }) &&
+      validator.isInt(icon.height + '', { min: 0 })
+  })
+
+  return video.icon.length !== 0
 }
 
 function setValidRemoteVideoUrls (video: any) {
index 64803b1dbd9f6255e5367bbc694aeb96b46942b2..3a9946bbad3f8ab23348d326e2fe5eb11758b323 100644 (file)
@@ -14,7 +14,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
 
 // ---------------------------------------------------------------------------
 
-const LAST_MIGRATION_VERSION = 475
+const LAST_MIGRATION_VERSION = 480
 
 // ---------------------------------------------------------------------------
 
@@ -541,11 +541,13 @@ let STATIC_MAX_AGE = {
 // Videos thumbnail size
 const THUMBNAILS_SIZE = {
   width: 223,
-  height: 122
+  height: 122,
+  minWidth: 150
 }
 const PREVIEWS_SIZE = {
   width: 850,
-  height: 480
+  height: 480,
+  minWidth: 400
 }
 const AVATARS_SIZE = {
   width: 120,
diff --git a/server/initializers/migrations/0480-caption-file-url.ts b/server/initializers/migrations/0480-caption-file-url.ts
new file mode 100644 (file)
index 0000000..7d8a3d4
--- /dev/null
@@ -0,0 +1,27 @@
+import * as Sequelize from 'sequelize'
+
+async function up (utils: {
+  transaction: Sequelize.Transaction,
+  queryInterface: Sequelize.QueryInterface,
+  sequelize: Sequelize.Sequelize,
+  db: any
+}): Promise<void> {
+  {
+    const data = {
+      type: Sequelize.STRING,
+      allowNull: true,
+      defaultValue: null
+    }
+
+    await utils.queryInterface.addColumn('videoCaption', 'fileUrl', data)
+  }
+}
+
+function down (options) {
+  throw new Error('Not implemented.')
+}
+
+export {
+  up,
+  down
+}
index 7a9d5168bd994d2c585c26a1c1ae943df2bc9736..6bc2258cc79d5ac1d07fcb40f91a18e94e4597f8 100644 (file)
@@ -6,7 +6,8 @@ import {
   ActivityHashTagObject,
   ActivityMagnetUrlObject,
   ActivityPlaylistSegmentHashesObject,
-  ActivityPlaylistUrlObject, ActivityTagObject,
+  ActivityPlaylistUrlObject,
+  ActivityTagObject,
   ActivityUrlObject,
   ActivityVideoUrlObject,
   VideoState
@@ -17,14 +18,14 @@ import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validat
 import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
 import { deleteNonExistingModels, resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils'
 import { logger } from '../../helpers/logger'
-import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests'
+import { doRequest } from '../../helpers/requests'
 import {
   ACTIVITY_PUB,
   MIMETYPES,
   P2P_MEDIA_LOADER_PEER_VERSION,
   PREVIEWS_SIZE,
   REMOTE_SCHEME,
-  STATIC_PATHS
+  STATIC_PATHS, THUMBNAILS_SIZE
 } from '../../initializers/constants'
 import { TagModel } from '../../models/video/tag'
 import { VideoModel } from '../../models/video/video'
@@ -40,7 +41,7 @@ import { ActivitypubHttpFetcherPayload } from '../job-queue/handlers/activitypub
 import { createRates } from './video-rates'
 import { addVideoShares, shareVideoByServerAndChannel } from './share'
 import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video'
-import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
+import { buildRemoteVideoBaseUrl, checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
 import { Notifier } from '../notifier'
 import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist'
 import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
@@ -71,6 +72,7 @@ import {
   MVideoThumbnail
 } from '../../typings/models'
 import { MThumbnail } from '../../typings/models/video/thumbnail'
+import { maxBy, minBy } from 'lodash'
 
 async function federateVideoIfNeeded (videoArg: MVideoAPWithoutCaption, isNewVideo: boolean, transaction?: sequelize.Transaction) {
   const video = videoArg as MVideoAP
@@ -131,19 +133,6 @@ async function fetchRemoteVideoDescription (video: MVideoAccountLight) {
   return body.description ? body.description : ''
 }
 
-function fetchRemoteVideoStaticFile (video: MVideoAccountLight, path: string, destPath: string) {
-  const url = buildRemoteBaseUrl(video, path)
-
-  // We need to provide a callback, if no we could have an uncaught exception
-  return doRequestAndSaveToFile({ uri: url }, destPath)
-}
-
-function buildRemoteBaseUrl (video: MVideoAccountLight, path: string) {
-  const host = video.VideoChannel.Account.Actor.Server.host
-
-  return REMOTE_SCHEME.HTTP + '://' + host + path
-}
-
 function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) {
   const channel = videoObject.attributedTo.find(a => a.type === 'Group')
   if (!channel) throw new Error('Cannot find associated video channel to video ' + videoObject.url)
@@ -173,7 +162,7 @@ async function syncVideoExternalAttributes (video: MVideo, fetchedVideo: VideoTo
     const cleaner = crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, 'like' as 'like', crawlStartDate)
 
     await crawlCollectionPage<string>(fetchedVideo.likes, handler, cleaner)
-      .catch(err => logger.error('Cannot add likes of video %s.', video.uuid, { err }))
+      .catch(err => logger.error('Cannot add likes of video %s.', video.uuid, { err, rootUrl: fetchedVideo.likes }))
   } else {
     jobPayloads.push({ uri: fetchedVideo.likes, videoId: video.id, type: 'video-likes' as 'video-likes' })
   }
@@ -183,7 +172,7 @@ async function syncVideoExternalAttributes (video: MVideo, fetchedVideo: VideoTo
     const cleaner = crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, 'dislike' as 'dislike', crawlStartDate)
 
     await crawlCollectionPage<string>(fetchedVideo.dislikes, handler, cleaner)
-      .catch(err => logger.error('Cannot add dislikes of video %s.', video.uuid, { err }))
+      .catch(err => logger.error('Cannot add dislikes of video %s.', video.uuid, { err, rootUrl: fetchedVideo.dislikes }))
   } else {
     jobPayloads.push({ uri: fetchedVideo.dislikes, videoId: video.id, type: 'video-dislikes' as 'video-dislikes' })
   }
@@ -193,7 +182,7 @@ async function syncVideoExternalAttributes (video: MVideo, fetchedVideo: VideoTo
     const cleaner = crawlStartDate => VideoShareModel.cleanOldSharesOf(video.id, crawlStartDate)
 
     await crawlCollectionPage<string>(fetchedVideo.shares, handler, cleaner)
-      .catch(err => logger.error('Cannot add shares of video %s.', video.uuid, { err }))
+      .catch(err => logger.error('Cannot add shares of video %s.', video.uuid, { err, rootUrl: fetchedVideo.shares }))
   } else {
     jobPayloads.push({ uri: fetchedVideo.shares, videoId: video.id, type: 'video-shares' as 'video-shares' })
   }
@@ -203,7 +192,7 @@ async function syncVideoExternalAttributes (video: MVideo, fetchedVideo: VideoTo
     const cleaner = crawlStartDate => VideoCommentModel.cleanOldCommentsOf(video.id, crawlStartDate)
 
     await crawlCollectionPage<string>(fetchedVideo.comments, handler, cleaner)
-      .catch(err => logger.error('Cannot add comments of video %s.', video.uuid, { err }))
+      .catch(err => logger.error('Cannot add comments of video %s.', video.uuid, { err, rootUrl: fetchedVideo.comments }))
   } else {
     jobPayloads.push({ uri: fetchedVideo.comments, videoId: video.id, type: 'video-comments' as 'video-comments' })
   }
@@ -284,7 +273,7 @@ async function updateVideoFromAP (options: {
     let thumbnailModel: MThumbnail
 
     try {
-      thumbnailModel = await createVideoMiniatureFromUrl(videoObject.icon.url, video, ThumbnailType.MINIATURE)
+      thumbnailModel = await createVideoMiniatureFromUrl(getThumbnailFromIcons(videoObject).url, video, ThumbnailType.MINIATURE)
     } catch (err) {
       logger.warn('Cannot generate thumbnail of %s.', videoObject.id, { err })
     }
@@ -327,8 +316,7 @@ async function updateVideoFromAP (options: {
 
       if (thumbnailModel) await videoUpdated.addAndSaveThumbnail(thumbnailModel, t)
 
-      // FIXME: use icon URL instead
-      const previewUrl = buildRemoteBaseUrl(videoUpdated, join(STATIC_PATHS.PREVIEWS, videoUpdated.getPreview().filename))
+      const previewUrl = videoUpdated.getPreview().getFileUrl(videoUpdated)
       const previewModel = createPlaceholderThumbnail(previewUrl, video, ThumbnailType.PREVIEW, PREVIEWS_SIZE)
       await videoUpdated.addAndSaveThumbnail(previewModel, t)
 
@@ -391,7 +379,7 @@ async function updateVideoFromAP (options: {
         await VideoCaptionModel.deleteAllCaptionsOfRemoteVideo(videoUpdated.id, t)
 
         const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => {
-          return VideoCaptionModel.insertOrReplaceLanguage(videoUpdated.id, c.identifier, t)
+          return VideoCaptionModel.insertOrReplaceLanguage(videoUpdated.id, c.identifier, c.url, t)
         })
         await Promise.all(videoCaptionsPromises)
       }
@@ -483,7 +471,6 @@ export {
   federateVideoIfNeeded,
   fetchRemoteVideo,
   getOrCreateVideoAndAccountAndChannel,
-  fetchRemoteVideoStaticFile,
   fetchRemoteVideoDescription,
   getOrCreateVideoChannelFromVideoObject
 }
@@ -519,7 +506,7 @@ async function createVideo (videoObject: VideoTorrentObject, channel: MChannelAc
   const videoData = await videoActivityObjectToDBAttributes(channel, videoObject, videoObject.to)
   const video = VideoModel.build(videoData) as MVideoThumbnail
 
-  const promiseThumbnail = createVideoMiniatureFromUrl(videoObject.icon.url, video, ThumbnailType.MINIATURE)
+  const promiseThumbnail = createVideoMiniatureFromUrl(getThumbnailFromIcons(videoObject).url, video, ThumbnailType.MINIATURE)
 
   let thumbnailModel: MThumbnail
   if (waitThumbnail === true) {
@@ -534,9 +521,12 @@ async function createVideo (videoObject: VideoTorrentObject, channel: MChannelAc
 
     if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t)
 
-    // FIXME: use icon URL instead
-    const previewUrl = buildRemoteBaseUrl(videoCreated, join(STATIC_PATHS.PREVIEWS, video.generatePreviewName()))
-    const previewModel = createPlaceholderThumbnail(previewUrl, video, ThumbnailType.PREVIEW, PREVIEWS_SIZE)
+    const previewIcon = getPreviewFromIcons(videoObject)
+    const previewUrl = previewIcon
+      ? previewIcon.url
+      : buildRemoteVideoBaseUrl(videoCreated, join(STATIC_PATHS.PREVIEWS, video.generatePreviewName()))
+    const previewModel = createPlaceholderThumbnail(previewUrl, videoCreated, ThumbnailType.PREVIEW, PREVIEWS_SIZE)
+
     if (thumbnailModel) await videoCreated.addAndSaveThumbnail(previewModel, t)
 
     // Process files
@@ -567,7 +557,7 @@ async function createVideo (videoObject: VideoTorrentObject, channel: MChannelAc
 
     // Process captions
     const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => {
-      return VideoCaptionModel.insertOrReplaceLanguage(videoCreated.id, c.identifier, t)
+      return VideoCaptionModel.insertOrReplaceLanguage(videoCreated.id, c.identifier, c.url, t)
     })
     await Promise.all(videoCaptionsPromises)
 
@@ -721,3 +711,19 @@ function streamingPlaylistActivityUrlToDBAttributes (video: MVideoId, videoObjec
 
   return attributes
 }
+
+function getThumbnailFromIcons (videoObject: VideoTorrentObject) {
+  let validIcons = videoObject.icon.filter(i => i.width > THUMBNAILS_SIZE.minWidth)
+  // Fallback if there are not valid icons
+  if (validIcons.length === 0) validIcons = videoObject.icon
+
+  return minBy(validIcons, 'width')
+}
+
+function getPreviewFromIcons (videoObject: VideoTorrentObject) {
+  const validIcons = videoObject.icon.filter(i => i.width > PREVIEWS_SIZE.minWidth)
+
+  // FIXME: don't put a fallback here for compatibility with PeerTube <2.2
+
+  return maxBy(validIcons, 'width')
+}
index 440c3fde81c2de19c4c763ad0d217e659e4ca4f0..26ab3bd0d0c621b35b84f005fb69ccd059f76361 100644 (file)
@@ -5,7 +5,7 @@ import { VideoCaptionModel } from '../../models/video/video-caption'
 import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache'
 import { CONFIG } from '../../initializers/config'
 import { logger } from '../../helpers/logger'
-import { fetchRemoteVideoStaticFile } from '../activitypub'
+import { doRequestAndSaveToFile } from '@server/helpers/requests'
 
 type GetPathParam = { videoId: string, language: string }
 
@@ -46,11 +46,10 @@ class VideosCaptionCache extends AbstractVideoStaticFileCache <GetPathParam> {
     const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId)
     if (!video) return undefined
 
-    // FIXME: use URL
-    const remoteStaticPath = videoCaption.getCaptionStaticPath()
+    const remoteUrl = videoCaption.getFileUrl(video)
     const destPath = join(FILES_CACHE.VIDEO_CAPTIONS.DIRECTORY, videoCaption.getCaptionName())
 
-    await fetchRemoteVideoStaticFile(video, remoteStaticPath, destPath)
+    await doRequestAndSaveToFile({ uri: remoteUrl }, destPath)
 
     return { isOwned: false, path: destPath }
   }
index 3da6bb1388c5bdf1361c50181357f48316725e9c..7bfeb57833e6304fa96cce89d4b80193c1a1ec92 100644 (file)
@@ -2,8 +2,8 @@ import { join } from 'path'
 import { FILES_CACHE, STATIC_PATHS } from '../../initializers/constants'
 import { VideoModel } from '../../models/video/video'
 import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache'
-import { CONFIG } from '../../initializers/config'
-import { fetchRemoteVideoStaticFile } from '../activitypub'
+import { doRequestAndSaveToFile } from '@server/helpers/requests'
+import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub'
 
 class VideosPreviewCache extends AbstractVideoStaticFileCache <string> {
 
@@ -32,11 +32,11 @@ class VideosPreviewCache extends AbstractVideoStaticFileCache <string> {
 
     if (video.isOwned()) throw new Error('Cannot load remote preview of owned video.')
 
-    // FIXME: use URL
-    const remoteStaticPath = join(STATIC_PATHS.PREVIEWS, video.getPreview().filename)
-    const destPath = join(FILES_CACHE.PREVIEWS.DIRECTORY, video.getPreview().filename)
+    const preview = video.getPreview()
+    const destPath = join(FILES_CACHE.PREVIEWS.DIRECTORY, preview.filename)
 
-    await fetchRemoteVideoStaticFile(video, remoteStaticPath, destPath)
+    const remoteUrl = preview.getFileUrl(video)
+    await doRequestAndSaveToFile({ uri: remoteUrl }, destPath)
 
     return { isOwned: false, path: destPath }
   }
index 73fa5ed041df8f937f67c572f3b97f6f1b0f3883..2258cd02989304c3216d094939e3528ee4622767 100644 (file)
@@ -23,6 +23,8 @@ async function processVideosViews () {
   for (const videoId of videoIds) {
     try {
       const views = await Redis.Instance.getVideoViews(videoId, hour)
+      await Redis.Instance.deleteVideoViews(videoId, hour)
+
       if (views) {
         logger.debug('Adding %d views to video %d in hour %d.', views, videoId, hour)
 
@@ -52,8 +54,6 @@ async function processVideosViews () {
           logger.error('Cannot create video views for video %d in hour %d.', videoId, hour, { err })
         }
       }
-
-      await Redis.Instance.deleteVideoViews(videoId, hour)
     } catch (err) {
       logger.error('Cannot update video views of video %d in hour %d.', videoId, hour, { err })
     }
index a1c623b2575d0997d4987f003f21278c73c05df6..61f07c48718ec8f2c879326f613a4ac39f16b84b 100644 (file)
@@ -136,7 +136,6 @@ class JobQueue {
 
     const filteredJobTypes = this.filterJobTypes(jobType)
 
-    // TODO: optimize
     for (const jobType of filteredJobTypes) {
       const queue = this.queues[ jobType ]
       if (queue === undefined) {
index 3b011b1d285de4fb1caf5e44f4f5a2a020b377ed..b69bc087260d1537dc94dff6e27453bb4919f6d4 100644 (file)
@@ -19,6 +19,8 @@ import { CONFIG } from '../../initializers/config'
 import { VideoModel } from './video'
 import { VideoPlaylistModel } from './video-playlist'
 import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
+import { MVideoAccountLight } from '@server/typings/models'
+import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub'
 
 @Table({
   tableName: 'thumbnail',
@@ -126,11 +128,14 @@ export class ThumbnailModel extends Model<ThumbnailModel> {
     return videoUUID + '.jpg'
   }
 
-  getFileUrl (isLocal: boolean) {
-    if (isLocal === false) return this.fileUrl
+  getFileUrl (video: MVideoAccountLight) {
+    const staticPath = ThumbnailModel.types[this.type].staticPath + this.filename
 
-    const staticPath = ThumbnailModel.types[this.type].staticPath
-    return WEBSERVER.URL + staticPath + this.filename
+    if (video.isOwned()) return WEBSERVER.URL + staticPath
+    if (this.fileUrl) return this.fileUrl
+
+    // Fallback if we don't have a file URL
+    return buildRemoteVideoBaseUrl(video, staticPath)
   }
 
   getPath () {
index 6335d44e4f088be495d231aadd2a33302deb1a38..1307c27f1082aa488f80b70eb07b3fbb83440437 100644 (file)
@@ -4,7 +4,7 @@ import {
   BeforeDestroy,
   BelongsTo,
   Column,
-  CreatedAt,
+  CreatedAt, DataType,
   ForeignKey,
   Is,
   Model,
@@ -16,13 +16,14 @@ import { buildWhereIdOrUUID, throwIfNotValid } from '../utils'
 import { VideoModel } from './video'
 import { isVideoCaptionLanguageValid } from '../../helpers/custom-validators/video-captions'
 import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model'
-import { LAZY_STATIC_PATHS, VIDEO_LANGUAGES } from '../../initializers/constants'
+import { CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, STATIC_PATHS, VIDEO_LANGUAGES, WEBSERVER } from '../../initializers/constants'
 import { join } from 'path'
 import { logger } from '../../helpers/logger'
 import { remove } from 'fs-extra'
 import { CONFIG } from '../../initializers/config'
 import * as Bluebird from 'bluebird'
-import { MVideoCaptionFormattable, MVideoCaptionVideo } from '@server/typings/models'
+import { MVideo, MVideoAccountLight, MVideoCaptionFormattable, MVideoCaptionVideo } from '@server/typings/models'
+import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub'
 
 export enum ScopeNames {
   WITH_VIDEO_UUID_AND_REMOTE = 'WITH_VIDEO_UUID_AND_REMOTE'
@@ -64,6 +65,10 @@ export class VideoCaptionModel extends Model<VideoCaptionModel> {
   @Column
   language: string
 
+  @AllowNull(true)
+  @Column(DataType.STRING(CONSTRAINTS_FIELDS.COMMONS.URL.max))
+  fileUrl: string
+
   @ForeignKey(() => VideoModel)
   @Column
   videoId: number
@@ -114,10 +119,11 @@ export class VideoCaptionModel extends Model<VideoCaptionModel> {
     return VideoCaptionModel.findOne(query)
   }
 
-  static insertOrReplaceLanguage (videoId: number, language: string, transaction: Transaction) {
+  static insertOrReplaceLanguage (videoId: number, language: string, fileUrl: string, transaction: Transaction) {
     const values = {
       videoId,
-      language
+      language,
+      fileUrl
     }
 
     return VideoCaptionModel.upsert(values, { transaction, returning: true })
@@ -175,4 +181,14 @@ export class VideoCaptionModel extends Model<VideoCaptionModel> {
   removeCaptionFile (this: MVideoCaptionFormattable) {
     return remove(CONFIG.STORAGE.CAPTIONS_DIR + this.getCaptionName())
   }
+
+  getFileUrl (video: MVideoAccountLight) {
+    if (!this.Video) this.Video = video as VideoModel
+
+    if (video.isOwned()) return WEBSERVER.URL + this.getCaptionStaticPath()
+    if (this.fileUrl) return this.fileUrl
+
+    // Fallback if we don't have a file URL
+    return buildRemoteVideoBaseUrl(video, this.getCaptionStaticPath())
+  }
 }
index 2aa5b8677cfffc42be08282e5719f47692958f60..bb50edcaab76537c21a8bf4ba6d68579747f9e38 100644 (file)
@@ -307,11 +307,12 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoTorrentObject {
   for (const caption of video.VideoCaptions) {
     subtitleLanguage.push({
       identifier: caption.language,
-      name: VideoCaptionModel.getLanguageLabel(caption.language)
+      name: VideoCaptionModel.getLanguageLabel(caption.language),
+      url: caption.getFileUrl(video)
     })
   }
 
-  const miniature = video.getMiniature()
+  const icons = [ video.getMiniature(), video.getPreview() ]
 
   return {
     type: 'Video' as 'Video',
@@ -336,13 +337,13 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoTorrentObject {
     content: video.getTruncatedDescription(),
     support: video.support,
     subtitleLanguage,
-    icon: {
+    icon: icons.map(i => ({
       type: 'Image',
-      url: miniature.getFileUrl(video.isOwned()),
+      url: i.getFileUrl(video),
       mediaType: 'image/jpeg',
-      width: miniature.width,
-      height: miniature.height
-    },
+      width: i.width,
+      height: i.height
+    })),
     url,
     likes: getVideoLikesActivityPubUrl(video),
     dislikes: getVideoDislikesActivityPubUrl(video),
index 20e1f1c4aa97c043243001a231b75d23df473f17..1a924e6c9f6e1968fa4a2bf0b8198955e9e9db4c 100644 (file)
@@ -1121,7 +1121,7 @@ export class VideoModel extends Model<VideoModel> {
       },
       include: [
         {
-          attributes: [ 'language' ],
+          attributes: [ 'language', 'fileUrl' ],
           model: VideoCaptionModel.unscoped(),
           required: false
         },
index ffa56f54468646ee80a3619d45c3596522993bb7..eeddedb4011add3e24c050f144631a1dd42700cd 100644 (file)
@@ -11,6 +11,7 @@ export type MVideoCaption = Omit<VideoCaptionModel, 'Video'>
 // ############################################################################
 
 export type MVideoCaptionLanguage = Pick<MVideoCaption, 'language'>
+export type MVideoCaptionLanguageUrl = Pick<MVideoCaption, 'language' | 'fileUrl' | 'getFileUrl'>
 
 export type MVideoCaptionVideo = MVideoCaption &
   Use<'Video', Pick<MVideo, 'id' | 'remote' | 'uuid'>>
index bcc5e5028e326a95e23b627e4dace41dac7aac92..82d76f40ca5dc68e50c8d507218c6f03e5c371db 100644 (file)
@@ -9,7 +9,7 @@ import {
   MChannelUserId
 } from './video-channels'
 import { MTag } from './tag'
-import { MVideoCaptionLanguage } from './video-caption'
+import { MVideoCaptionLanguage, MVideoCaptionLanguageUrl } from './video-caption'
 import {
   MStreamingPlaylistFiles,
   MStreamingPlaylistRedundancies,
@@ -140,7 +140,7 @@ export type MVideoAP = MVideo &
   Use<'Tags', MTag[]> &
   Use<'VideoChannel', MChannelAccountLight> &
   Use<'VideoStreamingPlaylists', MStreamingPlaylistFiles[]> &
-  Use<'VideoCaptions', MVideoCaptionLanguage[]> &
+  Use<'VideoCaptions', MVideoCaptionLanguageUrl[]> &
   Use<'VideoBlacklist', MVideoBlacklistUnfederated> &
   Use<'VideoFiles', MVideoFileRedundanciesOpt[]> &
   Use<'Thumbnails', MThumbnail[]>
index de1116ab3fd45790dc3a77ab97036cdde558124e..bab3ce366c6ce6b1b5615ef08948e5f4a9d30ef3 100644 (file)
@@ -1,6 +1,7 @@
 export interface ActivityIdentifierObject {
   identifier: string
   name: string
+  url?: string
 }
 
 export interface ActivityIconObject {
index 239822bc48497f31f9c32722405ea5f7a767d77f..cadd0ea4910fd55fc5688561c35a9c894653c66b 100644 (file)
@@ -30,7 +30,9 @@ export interface VideoTorrentObject {
   mediaType: 'text/markdown'
   content: string
   support: string
-  icon: ActivityIconObject
+
+  icon: ActivityIconObject[]
+
   url: ActivityUrlObject[]
   likes: string
   dislikes: string
index 0b6554e5160cf4d470473d6b8ec3121c95e83ef3..ae3a0d983ea294ce9fe3deb7663b6f82352f9be8 100644 (file)
@@ -7,15 +7,13 @@ export enum UserRole {
   USER = 2
 }
 
-// TODO: use UserRole for key once https://github.com/Microsoft/TypeScript/issues/13042 is fixed
-export const USER_ROLE_LABELS: { [ id: number ]: string } = {
+export const USER_ROLE_LABELS: { [ id in UserRole ]: string } = {
   [UserRole.USER]: 'User',
   [UserRole.MODERATOR]: 'Moderator',
   [UserRole.ADMINISTRATOR]: 'Administrator'
 }
 
-// TODO: use UserRole for key once https://github.com/Microsoft/TypeScript/issues/13042 is fixed
-const userRoleRights: { [ id: number ]: UserRight[] } = {
+const userRoleRights: { [ id in UserRole ]: UserRight[] } = {
   [UserRole.ADMINISTRATOR]: [
     UserRight.ALL
   ],