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