Co-authored-by: FloatingGhost <hannah@coffee-and-dreams.uk> Reviewed-on: https://akkoma.dev/AkkomaGang/akkoma/pulls/371
		
			
				
	
	
		
			162 lines
		
	
	
	
		
			3.7 KiB
		
	
	
	
		
			Elixir
		
	
	
	
	
	
			
		
		
	
	
			162 lines
		
	
	
	
		
			3.7 KiB
		
	
	
	
		
			Elixir
		
	
	
	
	
	
# Pleroma: A lightweight social networking server
 | 
						|
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
 | 
						|
# SPDX-License-Identifier: AGPL-3.0-only
 | 
						|
 | 
						|
defmodule Pleroma.Helpers.MediaHelper do
 | 
						|
  @moduledoc """
 | 
						|
  Handles common media-related operations.
 | 
						|
  """
 | 
						|
 | 
						|
  alias Pleroma.HTTP
 | 
						|
 | 
						|
  require Logger
 | 
						|
 | 
						|
  def missing_dependencies do
 | 
						|
    Enum.reduce([imagemagick: "convert", ffmpeg: "ffmpeg"], [], fn {sym, executable}, acc ->
 | 
						|
      if Pleroma.Utils.command_available?(executable) do
 | 
						|
        acc
 | 
						|
      else
 | 
						|
        [sym | acc]
 | 
						|
      end
 | 
						|
    end)
 | 
						|
  end
 | 
						|
 | 
						|
  def image_resize(url, options) do
 | 
						|
    with executable when is_binary(executable) <- System.find_executable("convert"),
 | 
						|
         {:ok, args} <- prepare_image_resize_args(options),
 | 
						|
         {:ok, env} <- HTTP.get(url, [], []),
 | 
						|
         {:ok, fifo_path} <- mkfifo() do
 | 
						|
      args = List.flatten([fifo_path, args])
 | 
						|
      run_fifo(fifo_path, env, executable, args)
 | 
						|
    else
 | 
						|
      nil -> {:error, {:convert, :command_not_found}}
 | 
						|
      {:error, _} = error -> error
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  defp prepare_image_resize_args(
 | 
						|
         %{max_width: max_width, max_height: max_height, format: "png"} = options
 | 
						|
       ) do
 | 
						|
    quality = options[:quality] || 85
 | 
						|
    resize = Enum.join([max_width, "x", max_height, ">"])
 | 
						|
 | 
						|
    args = [
 | 
						|
      "-resize",
 | 
						|
      resize,
 | 
						|
      "-quality",
 | 
						|
      to_string(quality),
 | 
						|
      "png:-"
 | 
						|
    ]
 | 
						|
 | 
						|
    {:ok, args}
 | 
						|
  end
 | 
						|
 | 
						|
  defp prepare_image_resize_args(%{max_width: max_width, max_height: max_height} = options) do
 | 
						|
    quality = options[:quality] || 85
 | 
						|
    resize = Enum.join([max_width, "x", max_height, ">"])
 | 
						|
 | 
						|
    args = [
 | 
						|
      "-interlace",
 | 
						|
      "Plane",
 | 
						|
      "-resize",
 | 
						|
      resize,
 | 
						|
      "-quality",
 | 
						|
      to_string(quality),
 | 
						|
      "jpg:-"
 | 
						|
    ]
 | 
						|
 | 
						|
    {:ok, args}
 | 
						|
  end
 | 
						|
 | 
						|
  defp prepare_image_resize_args(_), do: {:error, :missing_options}
 | 
						|
 | 
						|
  # Note: video thumbnail is intentionally not resized (always has original dimensions)
 | 
						|
  def video_framegrab(url) do
 | 
						|
    with executable when is_binary(executable) <- System.find_executable("ffmpeg"),
 | 
						|
         {:ok, env} <- HTTP.get(url, [], []),
 | 
						|
         {:ok, fifo_path} <- mkfifo(),
 | 
						|
         args = [
 | 
						|
           "-y",
 | 
						|
           "-i",
 | 
						|
           fifo_path,
 | 
						|
           "-vframes",
 | 
						|
           "1",
 | 
						|
           "-f",
 | 
						|
           "mjpeg",
 | 
						|
           "-loglevel",
 | 
						|
           "error",
 | 
						|
           "-"
 | 
						|
         ] do
 | 
						|
      run_fifo(fifo_path, env, executable, args)
 | 
						|
    else
 | 
						|
      nil -> {:error, {:ffmpeg, :command_not_found}}
 | 
						|
      {:error, _} = error -> error
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  defp run_fifo(fifo_path, env, executable, args) do
 | 
						|
    pid =
 | 
						|
      Port.open({:spawn_executable, executable}, [
 | 
						|
        :use_stdio,
 | 
						|
        :stream,
 | 
						|
        :exit_status,
 | 
						|
        :binary,
 | 
						|
        args: args
 | 
						|
      ])
 | 
						|
 | 
						|
    fifo = File.open!(fifo_path, [:append, :binary])
 | 
						|
    fix = Pleroma.Helpers.QtFastStart.fix(env.body)
 | 
						|
    IO.binwrite(fifo, fix)
 | 
						|
    File.close(fifo)
 | 
						|
    loop_recv(pid)
 | 
						|
  after
 | 
						|
    File.rm(fifo_path)
 | 
						|
  end
 | 
						|
 | 
						|
  defp mkfifo do
 | 
						|
    path = Path.join(System.tmp_dir!(), "pleroma-media-preview-pipe-#{Ecto.UUID.generate()}")
 | 
						|
 | 
						|
    case System.cmd("mkfifo", [path]) do
 | 
						|
      {_, 0} ->
 | 
						|
        spawn(fifo_guard(path))
 | 
						|
        {:ok, path}
 | 
						|
 | 
						|
      {_, err} ->
 | 
						|
        {:error, {:fifo_failed, err}}
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  defp fifo_guard(path) do
 | 
						|
    pid = self()
 | 
						|
 | 
						|
    fn ->
 | 
						|
      ref = Process.monitor(pid)
 | 
						|
 | 
						|
      receive do
 | 
						|
        {:DOWN, ^ref, :process, ^pid, _} ->
 | 
						|
          File.rm(path)
 | 
						|
      end
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  defp loop_recv(pid) do
 | 
						|
    loop_recv(pid, <<>>)
 | 
						|
  end
 | 
						|
 | 
						|
  defp loop_recv(pid, acc) do
 | 
						|
    receive do
 | 
						|
      {^pid, {:data, data}} ->
 | 
						|
        loop_recv(pid, acc <> data)
 | 
						|
 | 
						|
      {^pid, {:exit_status, 0}} ->
 | 
						|
        {:ok, acc}
 | 
						|
 | 
						|
      {^pid, {:exit_status, status}} ->
 | 
						|
        {:error, status}
 | 
						|
    after
 | 
						|
      5000 ->
 | 
						|
        :erlang.port_close(pid)
 | 
						|
        {:error, :timeout}
 | 
						|
    end
 | 
						|
  end
 | 
						|
end
 |