lib/membrane_precompiled_dependency_provider.ex

defmodule Membrane.PrecompiledDependencyProvider do
  @moduledoc """
  Module providing URLs for precompiled dependencies used by Membrane plugins.

  Dependencies that are fully located in the repositories of `membraneframework-precompiled` will
  be referred to as generic. Otherwise they will be referred to as non-generic.
  """

  require Logger

  @membrane_precompiled_org_url "https://github.com/membraneframework-precompiled"
  @ffmpeg_builds_url "https://github.com/BtbN/FFmpeg-Builds/releases"

  @typedoc """
  An atom representing a dependency.

  Dependency can be non-generic, which is handled in `get_dependency_url/2`, such as `:ffmpeg`, 
  or generic, which has a corresponding repository in 
  https://github.com/membraneframework-precompiled organization, such as `:opus` or `:libvpx`. 
  """
  @type precompiled_dependency() :: atom()

  @typedoc """
  List of desired versions of the precompiled dependencies.

  If an application requires specific versions of used precompiled dependencies, they can be specified
  by passing such list to a `Config.config/3` function with `:membrane_precompiled_dependency_provider`
  root key and `:versions` key, like so 

      config :membrane_precompiled_dependency_provider, :versions,
        ffmpeg: "6.0.1",
        opus: "1.5.2"
  """
  @type version_config() :: [
          {precompiled_dependency(), String.t()}
        ]

  @doc """
  Get URL of a precompiled build of given dependency for a platform from which this function is being
  called. 

  A specific version of the dependency can be provided with `:version` option or through config 
  (see `t:version_config/0`). If provided in both ways, the config value will be taken.
  For generic dependencies this version needs to be the same as a release name from the
  repository of the precompiled dependency, but without the leading "v". By default the latest
  version is chosen.
  """
  @spec get_dependency_url(precompiled_dependency(), version: String.t()) ::
          String.t() | nil
  def get_dependency_url(dependency, options \\ [])

  def get_dependency_url(:ffmpeg, options) do
    version = resolve_version(:ffmpeg, options)

    generic_url_prefix =
      get_generic_dependency_url_prefix(:ffmpeg, version)

    case Bundlex.get_target() do
      %{abi: "musl"} ->
        nil

      %{architecture: "x86_64", os: "linux"} ->
        get_linux_ffmpeg_release(version, "linux64")

      %{architecture: "aarch64", os: "linux"} ->
        get_linux_ffmpeg_release(version, "linuxarm64")

      %{architecture: "x86_64", os: "darwin" <> _rest_of_os_name} ->
        "#{generic_url_prefix}_macos_intel.tar.gz"

      %{architecture: "aarch64", os: "darwin" <> _rest_of_os_name} ->
        "#{generic_url_prefix}_macos_arm.tar.gz"

      _other ->
        nil
    end
  end

  # ---------------------------------
  # NEW NON-GENERIC DEPENDENCIES HERE
  # defp get_dependency_url(..., target) do 
  #
  # end
  # ---------------------------------

  def get_dependency_url(generic_dependency, options) do
    version = resolve_version(generic_dependency, options)

    generic_url_prefix =
      get_generic_dependency_url_prefix(generic_dependency, version)

    case Bundlex.get_target() do
      %{abi: "musl"} ->
        nil

      %{architecture: "aarch64", os: "linux"} ->
        "#{generic_url_prefix}_linux_arm.tar.gz"

      %{architecture: "x86_64", os: "linux"} ->
        "#{generic_url_prefix}_linux_x86.tar.gz"

      %{architecture: "x86_64", os: "darwin" <> _rest_of_os_name} ->
        "#{generic_url_prefix}_macos_intel.tar.gz"

      %{architecture: "aarch64", os: "darwin" <> _rest_of_os_name} ->
        "#{generic_url_prefix}_macos_arm.tar.gz"

      _other ->
        nil
    end
  end

  @spec resolve_version(precompiled_dependency(), version: String.t()) :: String.t()
  defp resolve_version(dependency, opts) do
    opts_version = Keyword.get(opts, :version, "latest")

    Application.get_env(:membrane_precompiled_dependency_provider, :versions, [])
    |> Keyword.get(dependency, opts_version)
  end

  @spec get_generic_dependency_url_prefix(precompiled_dependency(), String.t()) :: String.t()
  defp get_generic_dependency_url_prefix(dep, version) do
    releases_url = "#{@membrane_precompiled_org_url}/precompiled_#{dep}/releases"

    case version do
      "latest" -> "#{releases_url}/latest/download/#{dep}"
      version -> "#{releases_url}/download/v#{version}/#{dep}"
    end
  end

  @spec get_linux_ffmpeg_release(String.t(), String.t()) :: String.t()
  defp get_linux_ffmpeg_release(version, platform) do
    cond do
      version in ["6.0", "6.0.1"] ->
        "#{@membrane_precompiled_org_url}/precompiled_ffmpeg/releases/download/v6.0.1/ffmpeg_#{platform}.tar.xz"

      version in ["6.1", "6.1.3"] ->
        "#{@ffmpeg_builds_url}/download/autobuild-2025-08-31-13-00/ffmpeg-n6.1.3-#{platform}-gpl-shared-6.1.tar.xz"

      version in ["7.0", "7.0.2"] ->
        "#{@ffmpeg_builds_url}/download/autobuild-2024-08-31-12-50/ffmpeg-n7.0.2-6-g7e69129d2f-#{platform}-gpl-shared-7.0.tar.xz"

      version in ["7.1", "7.1.2"] ->
        "#{@ffmpeg_builds_url}/download/autobuild-2025-09-23-13-17/ffmpeg-n7.1.2-2-gab05459692-#{platform}-gpl-shared-7.1.tar.xz"

      version == "8.0" ->
        "#{@ffmpeg_builds_url}/download/autobuild-2025-09-23-13-17/ffmpeg-n8.0-14-gb9adbf0fcc-#{platform}-gpl-shared-8.0.tar.xz"

      version == "latest" ->
        "#{@ffmpeg_builds_url}/latest/download/ffmpeg-master-latest-#{platform}-gpl-shared.tar.xz"

      true ->
        Logger.warning("Version #{version} not found, using latest")
        "#{@ffmpeg_builds_url}/latest/download/ffmpeg-master-latest-#{platform}-gpl-shared.tar.xz"
    end
  end
end