Files
music_library/lib/music_library/records.ex
T
Claudio Ortolina a59dd22a18 ML-168: broadcast index_changed event after background import
Import workers now broadcast :records_index_changed on
"records:index_changed" after successful import via
Records.broadcast_index_changed/0.

CollectionLive.Index and WishlistLive.Index subscribe to the
topic in mount/3 and reload their record streams on receipt,
with a live_action guard to skip reloads when the grid is
hidden behind a modal (:import, :barcode_scan).

IndexActions.handle_index_changed/1 refreshes total_entries
before reloading to keep the pagination bar accurate.
2026-05-14 17:15:06 +01:00

147 lines
4.8 KiB
Elixir

defmodule MusicLibrary.Records do
@moduledoc """
Provides functions to work with records irrespective of their status
as part of the collection or the wishlist.
Search, import, and enrichment functions are delegated to focused sub-contexts:
`Records.Search`, `Records.Import`, and `Records.Enrichment`.
"""
alias MusicLibrary.Artists
alias MusicLibrary.Records.{Enrichment, Record}
alias MusicLibrary.Repo
# ---- Search delegation ----
defdelegate search_records(initial_search, query, opts), to: MusicLibrary.Records.Search
defdelegate search_records_count(initial_search, query), to: MusicLibrary.Records.Search
defdelegate list_genres, to: MusicLibrary.Records.Search
# ---- Import delegation ----
defdelegate get_release_status(release_id, format), to: MusicLibrary.Records.Import
defdelegate get_artist_records(musicbrainz_id), to: MusicLibrary.Records.Import
defdelegate import_from_musicbrainz_release(musicbrainz_id, opts \\ []),
to: MusicLibrary.Records.Import
defdelegate import_from_musicbrainz_release_group(musicbrainz_id, opts \\ []),
to: MusicLibrary.Records.Import
# ---- Enrichment delegation ----
defdelegate populate_genres(record), to: MusicLibrary.Records.Enrichment
defdelegate populate_genres_async(record), to: MusicLibrary.Records.Enrichment
defdelegate refresh_cover(record), to: MusicLibrary.Records.Enrichment
defdelegate refresh_cover_async(record), to: MusicLibrary.Records.Enrichment
defdelegate extract_colors(record), to: MusicLibrary.Records.Enrichment
defdelegate resize_cover(record), to: MusicLibrary.Records.Enrichment
defdelegate refresh_musicbrainz_data(record), to: MusicLibrary.Records.Enrichment
defdelegate refresh_musicbrainz_data_async(record), to: MusicLibrary.Records.Enrichment
# ---- CRUD functions ----
@spec get_record(String.t()) :: Record.t() | nil
def get_record(id), do: Repo.get(Record, id)
@spec get_record!(String.t()) :: Record.t()
def get_record!(id), do: Repo.get!(Record, id)
@spec create_record(map()) :: {:ok, Record.t()} | {:error, Ecto.Changeset.t()}
def create_record(attrs \\ %{}) do
with {:ok, record} <- do_create_record(attrs),
record = Enrichment.best_effort_extract_colors(record),
:ok <- refresh_artist_info_async(record) do
{:ok, record}
end
end
@spec refresh_artist_info_async(Record.t()) :: :ok
def refresh_artist_info_async(record) do
record
|> Record.artist_ids()
|> Enum.each(fn artist_id ->
Artists.refresh_artist_info_async(artist_id)
end)
end
defp do_create_record(attrs) do
%Record{}
|> Record.changeset(attrs)
|> Repo.insert()
end
@spec update_record(Record.t(), map()) :: {:ok, Record.t()} | {:error, Ecto.Changeset.t()}
def update_record(%Record{} = record, attrs) do
with {:ok, updated_record} <- do_update_record(record, attrs),
:ok <- refresh_artist_info_async(updated_record) do
{:ok, updated_record}
end
end
defp do_update_record(record, attrs) do
record
|> Record.changeset(attrs)
|> Repo.update()
end
@spec delete_record(Record.t()) :: {:ok, Record.t()} | {:error, Ecto.Changeset.t()}
def delete_record(%Record{} = record) do
with {:ok, record} <- Repo.delete(record) do
record
|> Record.artist_ids()
|> Enum.each(fn artist_id ->
Artists.prune_artist_info_async(artist_id)
end)
{:ok, record}
end
end
@spec change_record(Record.t(), map()) :: Ecto.Changeset.t()
def change_record(%Record{} = record, attrs \\ %{}) do
Record.changeset(record, attrs)
end
# ---- PubSub functions ----
@spec subscribe(String.t()) :: :ok | {:error, term()}
def subscribe(record_id) do
Phoenix.PubSub.subscribe(MusicLibrary.PubSub, "records:#{record_id}")
end
@doc """
Unsubscribes the calling process from updates for a given record.
"""
@spec unsubscribe(String.t()) :: :ok | {:error, term()}
def unsubscribe(record_id) do
Phoenix.PubSub.unsubscribe(MusicLibrary.PubSub, "records:#{record_id}")
end
@spec notify_update(Record.t()) :: :ok | {:error, term()}
def notify_update(record) do
Phoenix.PubSub.broadcast(
MusicLibrary.PubSub,
"records:#{record.id}",
{:update, record}
)
end
@doc """
Broadcasts that the records index has changed (new record imported, deleted, etc.).
Index LiveViews subscribe to this topic to auto-refresh.
"""
@spec broadcast_index_changed() :: :ok
def broadcast_index_changed do
Phoenix.PubSub.broadcast(MusicLibrary.PubSub, "records:index_changed", :records_index_changed)
end
@doc """
Subscribes the calling process to records index change notifications.
"""
@spec subscribe_to_index() :: :ok | {:error, term()}
def subscribe_to_index do
Phoenix.PubSub.subscribe(MusicLibrary.PubSub, "records:index_changed")
end
end