]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blobdiff - server/lib/thumbnail.ts
Force live stream termination
[github/Chocobozzz/PeerTube.git] / server / lib / thumbnail.ts
index 8dbd417711bb8cfdd1a297f94db31b54c9c7893b..02b867a91e6b9fd833514aa82093595925520aba 100644 (file)
-import { generateImageFromVideoFile } from '../helpers/ffmpeg-utils'
+import { join } from 'path'
+import { ThumbnailType } from '@shared/models'
+import { generateImageFilename, generateImageFromVideoFile } from '../helpers/image-utils'
 import { CONFIG } from '../initializers/config'
 import { ASSETS_PATH, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '../initializers/constants'
 import { ThumbnailModel } from '../models/video/thumbnail'
-import { ThumbnailType } from '../../shared/models/videos/thumbnail.type'
-import { processImage } from '../helpers/image-utils'
-import { join } from 'path'
-import { downloadImage } from '../helpers/requests'
-import { MVideoPlaylistThumbnail } from '../typings/models/video/video-playlist'
-import { MVideoFile, MVideoThumbnail } from '../typings/models'
-import { MThumbnail } from '../typings/models/video/thumbnail'
-import { getVideoFilePath } from './video-paths'
-
-type ImageSize = { height: number, width: number }
-
-function createPlaylistMiniatureFromExisting (
-  inputPath: string,
-  playlist: MVideoPlaylistThumbnail,
-  automaticallyGenerated: boolean,
-  keepOriginal = false,
+import { MVideoFile, MVideoThumbnail, MVideoUUID } from '../types/models'
+import { MThumbnail } from '../types/models/video/thumbnail'
+import { MVideoPlaylistThumbnail } from '../types/models/video/video-playlist'
+import { downloadImageFromWorker } from './local-actor'
+import { VideoPathManager } from './video-path-manager'
+import { processImageFromWorker } from './worker/parent-process'
+
+type ImageSize = { height?: number, width?: number }
+
+function updatePlaylistMiniatureFromExisting (options: {
+  inputPath: string
+  playlist: MVideoPlaylistThumbnail
+  automaticallyGenerated: boolean
+  keepOriginal?: boolean // default to false
   size?: ImageSize
-) {
+}) {
+  const { inputPath, playlist, automaticallyGenerated, keepOriginal = false, size } = options
   const { filename, outputPath, height, width, existingThumbnail } = buildMetadataFromPlaylist(playlist, size)
   const type = ThumbnailType.MINIATURE
 
-  const thumbnailCreator = () => processImage(inputPath, outputPath, { width, height }, keepOriginal)
-  return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, automaticallyGenerated, existingThumbnail })
+  const thumbnailCreator = () => {
+    return processImageFromWorker({ path: inputPath, destination: outputPath, newSize: { width, height }, keepOriginal })
+  }
+
+  return updateThumbnailFromFunction({
+    thumbnailCreator,
+    filename,
+    height,
+    width,
+    type,
+    automaticallyGenerated,
+    existingThumbnail
+  })
 }
 
-function createPlaylistMiniatureFromUrl (fileUrl: string, playlist: MVideoPlaylistThumbnail, size?: ImageSize) {
+function updatePlaylistMiniatureFromUrl (options: {
+  downloadUrl: string
+  playlist: MVideoPlaylistThumbnail
+  size?: ImageSize
+}) {
+  const { downloadUrl, playlist, size } = options
   const { filename, basePath, height, width, existingThumbnail } = buildMetadataFromPlaylist(playlist, size)
   const type = ThumbnailType.MINIATURE
 
-  const thumbnailCreator = () => downloadImage(fileUrl, basePath, filename, { width, height })
-  return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl })
+  // Only save the file URL if it is a remote playlist
+  const fileUrl = playlist.isOwned()
+    ? null
+    : downloadUrl
+
+  const thumbnailCreator = () => {
+    return downloadImageFromWorker({ url: downloadUrl, destDir: basePath, destName: filename, size: { width, height } })
+  }
+
+  return updateThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl })
 }
 
-function createVideoMiniatureFromUrl (fileUrl: string, video: MVideoThumbnail, type: ThumbnailType, size?: ImageSize) {
-  const { filename, basePath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size)
-  const thumbnailCreator = () => downloadImage(fileUrl, basePath, filename, { width, height })
+function updateVideoMiniatureFromUrl (options: {
+  downloadUrl: string
+  video: MVideoThumbnail
+  type: ThumbnailType
+  size?: ImageSize
+}) {
+  const { downloadUrl, video, type, size } = options
+  const { filename: updatedFilename, basePath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size)
+
+  // Only save the file URL if it is a remote video
+  const fileUrl = video.isOwned()
+    ? null
+    : downloadUrl
+
+  const thumbnailUrlChanged = hasThumbnailUrlChanged(existingThumbnail, downloadUrl, video)
+
+  // Do not change the thumbnail filename if the file did not change
+  const filename = thumbnailUrlChanged
+    ? updatedFilename
+    : existingThumbnail.filename
+
+  const thumbnailCreator = () => {
+    if (thumbnailUrlChanged) {
+      return downloadImageFromWorker({ url: downloadUrl, destDir: basePath, destName: filename, size: { width, height } })
+    }
+
+    return Promise.resolve()
+  }
 
-  return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl })
+  return updateThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl })
 }
 
-function createVideoMiniatureFromExisting (
-  inputPath: string,
-  video: MVideoThumbnail,
-  type: ThumbnailType,
-  automaticallyGenerated: boolean,
+function updateVideoMiniatureFromExisting (options: {
+  inputPath: string
+  video: MVideoThumbnail
+  type: ThumbnailType
+  automaticallyGenerated: boolean
   size?: ImageSize
-) {
+  keepOriginal?: boolean // default to false
+}) {
+  const { inputPath, video, type, automaticallyGenerated, size, keepOriginal = false } = options
+
   const { filename, outputPath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size)
-  const thumbnailCreator = () => processImage(inputPath, outputPath, { width, height })
 
-  return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, automaticallyGenerated, existingThumbnail })
+  const thumbnailCreator = () => {
+    return processImageFromWorker({ path: inputPath, destination: outputPath, newSize: { width, height }, keepOriginal })
+  }
+
+  return updateThumbnailFromFunction({
+    thumbnailCreator,
+    filename,
+    height,
+    width,
+    type,
+    automaticallyGenerated,
+    existingThumbnail
+  })
 }
 
-function generateVideoMiniature (video: MVideoThumbnail, videoFile: MVideoFile, type: ThumbnailType) {
-  const input = getVideoFilePath(video, videoFile)
+function generateVideoMiniature (options: {
+  video: MVideoThumbnail
+  videoFile: MVideoFile
+  type: ThumbnailType
+}) {
+  const { video, videoFile, type } = options
+
+  return VideoPathManager.Instance.makeAvailableVideoFile(videoFile.withVideoOrPlaylist(video), input => {
+    const { filename, basePath, height, width, existingThumbnail, outputPath } = buildMetadataFromVideo(video, type)
 
-  const { filename, basePath, height, width, existingThumbnail, outputPath } = buildMetadataFromVideo(video, type)
-  const thumbnailCreator = videoFile.isAudio()
-    ? () => processImage(ASSETS_PATH.DEFAULT_AUDIO_BACKGROUND, outputPath, { width, height }, true)
-    : () => generateImageFromVideoFile(input, basePath, filename, { height, width })
+    const thumbnailCreator = videoFile.isAudio()
+      ? () => processImageFromWorker({
+        path: ASSETS_PATH.DEFAULT_AUDIO_BACKGROUND,
+        destination: outputPath,
+        newSize: { width, height },
+        keepOriginal: true
+      })
+      : () => generateImageFromVideoFile({
+        fromPath: input,
+        folder: basePath,
+        imageName: filename,
+        size: { height, width }
+      })
 
-  return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, automaticallyGenerated: true, existingThumbnail })
+    return updateThumbnailFromFunction({
+      thumbnailCreator,
+      filename,
+      height,
+      width,
+      type,
+      automaticallyGenerated: true,
+      existingThumbnail
+    })
+  })
 }
 
-function createPlaceholderThumbnail (fileUrl: string, video: MVideoThumbnail, type: ThumbnailType, size: ImageSize) {
-  const { filename, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size)
+function updatePlaceholderThumbnail (options: {
+  fileUrl: string
+  video: MVideoThumbnail
+  type: ThumbnailType
+  size: ImageSize
+}) {
+  const { fileUrl, video, type, size } = options
+  const { filename: updatedFilename, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size)
+
+  const thumbnailUrlChanged = hasThumbnailUrlChanged(existingThumbnail, fileUrl, video)
 
   const thumbnail = existingThumbnail || new ThumbnailModel()
 
+  // Do not change the thumbnail filename if the file did not change
+  const filename = thumbnailUrlChanged
+    ? updatedFilename
+    : existingThumbnail.filename
+
   thumbnail.filename = filename
   thumbnail.height = height
   thumbnail.width = width
@@ -84,11 +186,20 @@ function createPlaceholderThumbnail (fileUrl: string, video: MVideoThumbnail, ty
 
 export {
   generateVideoMiniature,
-  createVideoMiniatureFromUrl,
-  createVideoMiniatureFromExisting,
-  createPlaceholderThumbnail,
-  createPlaylistMiniatureFromUrl,
-  createPlaylistMiniatureFromExisting
+  updateVideoMiniatureFromUrl,
+  updateVideoMiniatureFromExisting,
+  updatePlaceholderThumbnail,
+  updatePlaylistMiniatureFromUrl,
+  updatePlaylistMiniatureFromExisting
+}
+
+function hasThumbnailUrlChanged (existingThumbnail: MThumbnail, downloadUrl: string, video: MVideoUUID) {
+  const existingUrl = existingThumbnail
+    ? existingThumbnail.fileUrl
+    : null
+
+  // If the thumbnail URL did not change and has a unique filename (introduced in 3.1), avoid thumbnail processing
+  return !existingUrl || existingUrl !== downloadUrl || downloadUrl.endsWith(`${video.uuid}.jpg`)
 }
 
 function buildMetadataFromPlaylist (playlist: MVideoPlaylistThumbnail, size: ImageSize) {
@@ -111,7 +222,7 @@ function buildMetadataFromVideo (video: MVideoThumbnail, type: ThumbnailType, si
     : undefined
 
   if (type === ThumbnailType.MINIATURE) {
-    const filename = video.generateThumbnailName()
+    const filename = generateImageFilename()
     const basePath = CONFIG.STORAGE.THUMBNAILS_DIR
 
     return {
@@ -125,7 +236,7 @@ function buildMetadataFromVideo (video: MVideoThumbnail, type: ThumbnailType, si
   }
 
   if (type === ThumbnailType.PREVIEW) {
-    const filename = video.generatePreviewName()
+    const filename = generateImageFilename()
     const basePath = CONFIG.STORAGE.PREVIEWS_DIR
 
     return {
@@ -141,7 +252,7 @@ function buildMetadataFromVideo (video: MVideoThumbnail, type: ThumbnailType, si
   return undefined
 }
 
-async function createThumbnailFromFunction (parameters: {
+async function updateThumbnailFromFunction (parameters: {
   thumbnailCreator: () => Promise<any>
   filename: string
   height: number
@@ -151,9 +262,22 @@ async function createThumbnailFromFunction (parameters: {
   fileUrl?: string
   existingThumbnail?: MThumbnail
 }) {
-  const { thumbnailCreator, filename, width, height, type, existingThumbnail, automaticallyGenerated = null, fileUrl = null } = parameters
+  const {
+    thumbnailCreator,
+    filename,
+    width,
+    height,
+    type,
+    existingThumbnail,
+    automaticallyGenerated = null,
+    fileUrl = null
+  } = parameters
 
-  const thumbnail = existingThumbnail || new ThumbnailModel()
+  const oldFilename = existingThumbnail && existingThumbnail.filename !== filename
+    ? existingThumbnail.filename
+    : undefined
+
+  const thumbnail: MThumbnail = existingThumbnail || new ThumbnailModel()
 
   thumbnail.filename = filename
   thumbnail.height = height
@@ -162,6 +286,8 @@ async function createThumbnailFromFunction (parameters: {
   thumbnail.fileUrl = fileUrl
   thumbnail.automaticallyGenerated = automaticallyGenerated
 
+  if (oldFilename) thumbnail.previousThumbnailFilename = oldFilename
+
   await thumbnailCreator()
 
   return thumbnail