defmodule ExshomePlayer.Services.Playlist do
@moduledoc """
Module responsible for a playlist.
"""
alias ExshomePlayer.Events.{PlayerFileEnd, TrackEvent}
alias ExshomePlayer.Schemas.Track
alias ExshomePlayer.Services.Playback
alias ExshomePlayer.Variables.Title
use Exshome.Dependency.GenServerDependency,
dependencies: [{Title, :title}],
events: [PlayerFileEnd, TrackEvent],
name: "playlist"
defstruct [:current_id, tracks: []]
@type t() :: %__MODULE__{
current_id: String.t() | nil,
tracks: list(Track.t())
}
defmodule Data do
@moduledoc """
Inner data format for playback.
"""
defstruct next: [], previous: []
@type t() :: %__MODULE__{
previous: list(Track.t()),
next: list(Track.t())
}
end
@spec play(id :: String.t()) :: :ok
def play(id) when is_binary(id) do
call({:play, id})
end
@spec next() :: :ok
def next, do: call(:next)
@spec previous() :: :ok
def previous, do: call(:previous)
@impl GenServerDependency
def on_init(%DependencyState{} = state) do
Track.refresh_tracklist()
update_playlist(state, fn _ -> %Data{previous: Enum.reverse(Track.list())} end)
end
@impl GenServerDependency
def handle_dependency_change(
%DependencyState{
data: %Data{next: [%Track{type: :file} = track | _]},
deps: %{title: title}
} = state
)
when title != "" do
Track.update!(track, %{title: title})
state
end
def handle_dependency_change(%DependencyState{} = state), do: state
@impl GenServerDependency
def handle_call(
{:play, id},
_from,
%DependencyState{value: %__MODULE__{tracks: tracks}} = state
)
when is_binary(id) do
{previous, next} = Enum.split_while(tracks, &(&1.id != id))
state =
state
|> update_playlist(fn _ ->
%Data{
previous: Enum.reverse(previous),
next: next
}
end)
|> load_track()
{:reply, :ok, state}
end
def handle_call(:next, _from, %DependencyState{} = state) do
state =
state
|> update_playlist(&move_to_next_track/1)
|> load_track()
{:reply, :ok, state}
end
def handle_call(:previous, _from, %DependencyState{} = state) do
state =
state
|> update_playlist(&move_to_previous_track/1)
|> load_track()
{:reply, :ok, state}
end
@impl GenServerDependency
def handle_event(%PlayerFileEnd{reason: reason}, %DependencyState{} = state)
when reason in ["eof", "error"] do
state
|> update_playlist(&move_to_next_track/1)
|> load_track()
end
def handle_event(%PlayerFileEnd{}, %DependencyState{} = state), do: state
def handle_event(%TrackEvent{action: :created, track: track}, %DependencyState{} = state) do
if Enum.any?(state.value.tracks, &(&1.id == track.id)) do
state
else
update_playlist(state, fn %Data{} = data ->
%Data{data | previous: data.previous ++ [track]}
end)
end
end
def handle_event(
%TrackEvent{action: :deleted, track: %Track{id: id}},
%DependencyState{data: %Data{next: [%Track{id: id} | next]}} = state
) do
state
|> update_playlist(fn %Data{} = data ->
%Data{previous: data.previous ++ Enum.reverse(next), next: []}
end)
|> load_track()
end
def handle_event(
%TrackEvent{action: :deleted, track: %Track{id: id}},
%DependencyState{} = state
) do
update_playlist(state, fn %Data{} = data ->
%Data{
previous: Enum.reject(data.previous, &(&1.id == id)),
next: Enum.reject(data.next, &(&1.id == id))
}
end)
end
def handle_event(
%TrackEvent{action: :updated, track: %Track{} = track},
%DependencyState{} = state
) do
update_fn = fn %Track{} = current ->
if current.id == track.id do
track
else
current
end
end
update_playlist(
state,
fn %Data{} = data ->
%Data{
previous: Enum.map(data.previous, update_fn),
next: Enum.map(data.next, update_fn)
}
end
)
end
@spec update_playlist(DependencyState.t(), (Data.t() -> Data.t())) :: DependencyState.t()
defp update_playlist(%DependencyState{} = state, update_fn) do
state
|> update_data(update_fn)
|> refresh_playlist()
end
@spec refresh_playlist(DependencyState.t()) :: DependencyState.t()
defp refresh_playlist(%DependencyState{data: %Data{next: [%Track{id: id} | _]} = data} = state) do
update_value(
state,
%__MODULE__{
current_id: id,
tracks: Enum.reverse(data.previous) ++ data.next
}
)
end
defp refresh_playlist(%DependencyState{data: %Data{} = data} = state) do
update_value(
state,
%__MODULE__{
current_id: nil,
tracks: Enum.reverse(data.previous) ++ data.next
}
)
end
@spec load_track(DependencyState.t()) :: DependencyState.t()
defp load_track(%DependencyState{data: %Data{next: []}} = state) do
Playback.stop()
state
end
defp load_track(%DependencyState{data: %Data{next: [track | _]}} = state) do
track
|> Track.url()
|> Playback.load_url()
state
end
@spec move_to_next_track(Data.t()) :: Data.t()
defp move_to_next_track(%Data{next: []} = data), do: data
defp move_to_next_track(%Data{previous: previous, next: [current | next]}) do
%Data{previous: [current | previous], next: next}
end
@spec move_to_previous_track(Data.t()) :: Data.t()
defp move_to_previous_track(%Data{previous: []} = data), do: data
defp move_to_previous_track(%Data{previous: [current | previous], next: next}) do
%Data{previous: previous, next: [current | next]}
end
end