lib/exshome_player/schemas/track.ex

defmodule ExshomePlayer.Schemas.Track do
  @moduledoc """
  Player track data.
  """

  import Ecto.Query, only: [from: 2]
  import Ecto.Changeset
  use Exshome.Schema
  alias Ecto.Changeset
  alias Exshome.Event
  alias Exshome.Repo
  alias ExshomePlayer.Events.TrackEvent
  alias ExshomePlayer.Services.MpvServer

  @types [:file, :url]

  schema "player_tracks" do
    field(:title, :string, default: "")
    field(:type, Ecto.Enum, values: @types, default: :file)
    field(:path, :string)

    timestamps()
  end

  @type t() :: %__MODULE__{
          title: String.t() | nil,
          type: :url | :file,
          path: String.t() | nil
        }

  @spec changeset(t(), map()) :: Changeset.t(t())
  def changeset(struct, params \\ %{}) do
    struct
    |> cast(params, [:title, :type, :path])
    |> validate_required([:type, :path])
    |> validate_inclusion(:type, @types)
    |> validate_path_format()
  end

  @spec get!(String.t()) :: t()
  def get!(id) when is_binary(id), do: Repo.get!(__MODULE__, id)

  @spec list() :: [t()]
  def list, do: from(__MODULE__, order_by: [desc: :type, asc: :path]) |> Repo.all()

  @spec get_or_create_by_path(String.t()) :: t()
  def get_or_create_by_path(path) when is_binary(path) do
    case Repo.get_by(__MODULE__, path: path) do
      %__MODULE__{} = result -> result
      nil -> create!(%{path: path})
    end
  end

  @spec create!(map()) :: t()
  def create!(data) do
    {:ok, result} = create(data)
    result
  end

  @spec create(map()) :: {:ok, t()} | {:error, Changeset.t(t())}
  def create(data) do
    %__MODULE__{}
    |> changeset(data)
    |> Repo.insert()
    |> case do
      {:ok, result} ->
        Event.broadcast(%TrackEvent{action: :created, track: result})
        {:ok, result}

      result ->
        result
    end
  end

  @spec update!(t(), map()) :: t()
  def update!(%__MODULE__{} = track, %{} = data) do
    {:ok, result} = update(track, data)
    result
  end

  @spec update(t(), map()) :: {:ok, t()} | {:error, Changeset.t(t())}
  def update(%__MODULE__{} = track, %{} = data) do
    track
    |> changeset(data)
    |> Repo.update()
    |> case do
      {:ok, result} ->
        Event.broadcast(%TrackEvent{action: :updated, track: result})
        {:ok, result}

      result ->
        result
    end
  end

  @spec delete!(t()) :: :ok
  def delete!(%__MODULE__{} = track) do
    Repo.delete!(track)
    Event.broadcast(%TrackEvent{track: track, action: :deleted})
    on_delete(track)
  end

  @spec refresh_tracklist() :: :ok
  def refresh_tracklist do
    music_folder = MpvServer.music_folder()

    files =
      music_folder
      |> Path.join("**/*.*")
      |> Path.wildcard()
      |> Enum.map(&Path.relative_to(&1, music_folder))
      |> Enum.into(MapSet.new())

    Enum.each(files, &get_or_create_by_path/1)

    from(t in __MODULE__, where: t.type == :file)
    |> Repo.all()
    |> Enum.reject(&MapSet.member?(files, &1.path))
    |> Enum.each(&delete!/1)

    :ok
  end

  @spec url(t()) :: String.t()
  def url(%__MODULE__{type: :url, path: path}), do: path

  def url(%__MODULE__{type: :file, path: path}) do
    Path.join(MpvServer.music_folder(), path)
  end

  @spec on_delete(t()) :: :ok
  defp on_delete(%__MODULE__{type: :file} = track) do
    file_path = url(track)

    if File.exists?(file_path) do
      File.rm!(file_path)
    end

    :ok
  end

  defp on_delete(%__MODULE__{}), do: :ok

  @spec validate_path_format(Changeset.t(t())) :: Changeset.t()
  defp validate_path_format(%Changeset{} = changeset) do
    type = get_field(changeset, :type)

    case type do
      :url ->
        validate_format(changeset, :path, ~r{^https?://},
          message: "It should start with http:// or https://"
        )

      _ ->
        changeset
    end
  end
end