Remove LastFm.Feed, LastFm.Refresh, and LastFm.Supervisor
This commit is contained in:
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
Reference in New Issue
Block a user