diff --git a/lib/last_fm/feed.ex b/lib/last_fm/feed.ex deleted file mode 100644 index b0db3ba4..00000000 --- a/lib/last_fm/feed.ex +++ /dev/null @@ -1,48 +0,0 @@ -defmodule LastFm.Feed do - @moduledoc """ - Persists scrobbled tracks in `MusicLibrary.Repo` and publishes feed updates. - - Tracks are inserted into the `scrobbled_tracks` table with conflict handling on - `[:scrobbled_at_uts, :title]` to avoid duplicates, then scrobble rules are applied - to newly inserted rows. - """ - - @insertable_fields [ - :musicbrainz_id, - :title, - :artist, - :album, - :cover_url, - :scrobbled_at_uts, - :scrobbled_at_label, - :last_fm_data - ] - - @spec update([LastFm.Track.t()]) :: {:ok, non_neg_integer()} | no_return - def update(tracks) do - track_params = - tracks - |> Enum.map(fn t -> Map.take(t, @insertable_fields) end) - |> Enum.map(&Map.to_list/1) - - {count, tracks} = - MusicLibrary.Repo.insert_all(LastFm.Track, track_params, - on_conflict: :nothing, - conflict_target: [:scrobbled_at_uts, :title], - returning: true - ) - - tracks - |> MusicLibrary.ScrobbleRules.apply_all_rules() - |> MusicLibrary.ScrobbleRules.log_apply_results() - - Phoenix.PubSub.broadcast(LastFm.PubSub, "feed:update", %{track_count: count}) - - {:ok, count} - end - - @spec subscribe() :: :ok - def subscribe do - Phoenix.PubSub.subscribe(LastFm.PubSub, "feed:update") - end -end diff --git a/lib/last_fm/refresh.ex b/lib/last_fm/refresh.ex deleted file mode 100644 index 7eb01003..00000000 --- a/lib/last_fm/refresh.ex +++ /dev/null @@ -1,100 +0,0 @@ -defmodule LastFm.Refresh do - @moduledoc """ - A GenServer that manages periodic refreshing of Last.fm scrobbled tracks. - - This module is responsible for: - - Fetching recent tracks from Last.fm at configurable intervals - - Updating an in-memory feed with the latest tracks - - Supporting both automatic and manual refresh modes - - ## Configuration - - The server accepts a `LastFm.Config` struct with the following options: - - `auto_refresh`: When true, automatically starts refreshing on init - - `refresh_interval`: Time in milliseconds between refresh attempts - - ## Operation Modes - - 1. Automatic Mode (`auto_refresh: true`): - - Starts refreshing immediately on initialization - - Continues to refresh at the configured interval - - Handles failures gracefully by continuing to retry - - 2. Manual Mode (`auto_refresh: false`): - - Server remains dormant on initialization - - Refreshes only occur via explicit `refresh/0` calls - - Useful for testing or controlled refresh scenarios - - ## Usage - - # Manual refresh - LastFm.Refresh.refresh() - - The module uses `LastFm.Feed` to store and broadcast track updates to subscribers. - """ - - use GenServer - - alias LastFm.{API, Config, Feed} - - @type config :: Config.t() - - @spec start_link(config) :: GenServer.on_start() - def start_link(config) do - GenServer.start_link(__MODULE__, config, name: __MODULE__) - end - - @spec refresh() :: :ok - def refresh do - GenServer.call(__MODULE__, :refresh, 10_000) - end - - @impl true - @spec init(config) :: {:ok, config, {:continue, :refresh}} | :ignore - def init(config) do - if config.auto_refresh do - {:ok, config, {:continue, :refresh}} - else - :ignore - end - end - - @impl true - @spec handle_call(:refresh, GenServer.from(), config) :: - {:reply, :ok | {:error, term()}, config, pos_integer()} - def handle_call(:refresh, _from, config) do - case API.get_recent_tracks(config) do - {:ok, tracks} -> - Feed.update(tracks) - {:reply, :ok, config, config.refresh_interval} - - error -> - {:reply, error, config, config.refresh_interval} - end - end - - @impl true - @spec handle_continue(atom(), config) :: {:noreply, config, pos_integer()} - def handle_continue(:refresh, config), do: refresh(config) - - @impl true - @spec handle_info(atom(), config) :: {:noreply, config, pos_integer()} - def handle_info(:refresh, config), do: refresh(config) - - def handle_info(:timeout, config), do: refresh(config) - - defp refresh(config) do - case API.get_recent_tracks(config) do - {:ok, tracks} -> - Feed.update(tracks) - {:noreply, config, config.refresh_interval} - - {:error, error} -> - if API.ErrorResponse.retryable_error?(error) do - {:noreply, config, API.ErrorResponse.retry_delay(error)} - else - {:stop, error, config} - end - end - end -end diff --git a/lib/last_fm/supervisor.ex b/lib/last_fm/supervisor.ex deleted file mode 100644 index cc322001..00000000 --- a/lib/last_fm/supervisor.ex +++ /dev/null @@ -1,19 +0,0 @@ -defmodule LastFm.Supervisor do - @moduledoc false - - use Supervisor - - def start_link(config) do - Supervisor.start_link(__MODULE__, config, name: __MODULE__) - end - - @impl true - def init(config) do - children = [ - {Phoenix.PubSub, name: LastFm.PubSub}, - {LastFm.Refresh, config} - ] - - Supervisor.init(children, strategy: :one_for_one) - end -end diff --git a/test/last_fm/feed_test.exs b/test/last_fm/feed_test.exs deleted file mode 100644 index 85a47e5c..00000000 --- a/test/last_fm/feed_test.exs +++ /dev/null @@ -1,52 +0,0 @@ -defmodule LastFm.FeedTest do - use MusicLibrary.DataCase - - alias LastFm.{Album, Artist, Track} - alias MusicLibrary.ListeningStats - - @track_one %Track{ - musicbrainz_id: "5689211e-9afa-3c3e-8e34-63dc0de45ef1", - title: "The Flow", - artist: %Artist{ - musicbrainz_id: "0cf0af1f-20ca-4863-9b24-5f52772f7715", - name: "Anekdoten" - }, - album: %Album{ - musicbrainz_id: "08237599-8fdf-4e2b-a7c9-eb5336f60346", - title: "Vemod" - }, - cover_url: "https://lastfm.freetls.fastly.net/i/u/64s/9741e297b9884a4294624f0f90e14749.jpg", - scrobbled_at_uts: 1_731_318_211, - scrobbled_at_label: "11 Nov 2024, 09:43", - last_fm_data: %{} - } - @track_two %Track{ - musicbrainz_id: "619cb295-b155-3e35-b65a-396a7cd1fc47", - title: "Wheel", - artist: %Artist{ - musicbrainz_id: "0cf0af1f-20ca-4863-9b24-5f52772f7715", - name: "Anekdoten" - }, - album: %Album{ - musicbrainz_id: "08237599-8fdf-4e2b-a7c9-eb5336f60346", - title: "Vemod" - }, - cover_url: "https://lastfm.freetls.fastly.net/i/u/64s/9741e297b9884a4294624f0f90e14749.jpg", - scrobbled_at_uts: 1_731_318_945, - scrobbled_at_label: "11 Nov 2024, 09:55", - last_fm_data: %{} - } - - describe "update and broadcast" do - test "stores the track and broadcasts the updated track count" do - :ok = ListeningStats.subscribe() - - assert {:ok, 2} == ListeningStats.update([@track_two, @track_one]) - assert_receive %{track_count: 2} - - # Tracks have already been inserted, count of new tracks is 0 - assert {:ok, 0} == ListeningStats.update([@track_two, @track_one]) - assert_receive %{track_count: 0} - end - end -end