Move scrobble refresh infra to Oban

This commit is contained in:
Claudio Ortolina
2026-03-28 08:59:51 +00:00
parent d45aff21ec
commit 5c5a523fe6
9 changed files with 52 additions and 118 deletions
-3
View File
@@ -24,9 +24,6 @@ defmodule MusicLibrary.Application do
repos: Application.fetch_env!(:music_library, :ecto_repos), skip: skip_migrations?()},
{Task.Supervisor, name: MusicLibrary.TaskSupervisor},
{Phoenix.PubSub, name: MusicLibrary.PubSub},
{MusicLibrary.ListeningStats.Refresh,
{LastFm.Config.resolve(:music_library),
Application.fetch_env!(:music_library, MusicLibrary.ListeningStats.Refresh)}},
# Start a worker by calling: MusicLibrary.Worker.start_link(arg)
# {MusicLibrary.Worker, arg},
# Start to serve requests, typically the last entry
+3 -3
View File
@@ -15,7 +15,6 @@ defmodule MusicLibrary.ListeningStats do
Artists,
BackgroundRepo,
Collection,
ListeningStats.Refresh,
Records.ArtistRecord,
Records.Record,
Repo,
@@ -76,9 +75,10 @@ defmodule MusicLibrary.ListeningStats do
Phoenix.PubSub.subscribe(MusicLibrary.PubSub, "listening_stats:update")
end
@spec refresh() :: :ok | {:error, term()}
@spec refresh() :: {:ok, Oban.Job.t()} | {:error, Ecto.Changeset.t()}
def refresh do
Refresh.refresh()
Worker.RefreshScrobbles.new(%{})
|> Oban.insert()
end
@spec lowest_scrobbled_at_uts() :: integer() | nil
@@ -1,101 +0,0 @@
defmodule MusicLibrary.ListeningStats.Refresh do
@moduledoc """
A GenServer that manages periodic refreshing of Last.fm scrobbled tracks.
Fetches recent tracks from Last.fm at configurable intervals and persists
them via `MusicLibrary.ListeningStats.update/1`.
## Configuration
Accepts a tuple of `{LastFm.Config.t(), keyword()}` where the keyword list contains:
- `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
"""
use GenServer
alias LastFm.{API, Config}
alias MusicLibrary.ListeningStats
@type state :: %{
api_config: Config.t(),
auto_refresh: boolean(),
refresh_interval: pos_integer()
}
@spec start_link({Config.t(), keyword()}) :: GenServer.on_start()
def start_link({api_config, refresh_opts}) do
state = %{
api_config: api_config,
auto_refresh: Keyword.get(refresh_opts, :auto_refresh, true),
refresh_interval: Keyword.get(refresh_opts, :refresh_interval, 60_000)
}
GenServer.start_link(__MODULE__, state, name: __MODULE__)
end
@spec refresh() :: :ok
def refresh do
GenServer.call(__MODULE__, :refresh, 10_000)
end
@impl true
@spec init(state) :: {:ok, state, {:continue, :refresh}} | :ignore
def init(state) do
if state.auto_refresh do
{:ok, state, {:continue, :refresh}}
else
:ignore
end
end
@impl true
@spec handle_call(:refresh, GenServer.from(), state) ::
{:reply, :ok | {:error, term()}, state, pos_integer()}
def handle_call(:refresh, _from, state) do
case API.get_recent_tracks(state.api_config) do
{:ok, tracks} ->
ListeningStats.update(tracks)
{:reply, :ok, state, state.refresh_interval}
error ->
{:reply, error, state, state.refresh_interval}
end
end
@impl true
@spec handle_continue(atom(), state) :: {:noreply, state, pos_integer()}
def handle_continue(:refresh, state), do: do_refresh(state)
@impl true
@spec handle_info(atom(), state) :: {:noreply, state, pos_integer()}
def handle_info(:refresh, state), do: do_refresh(state)
def handle_info(:timeout, state), do: do_refresh(state)
defp do_refresh(state) do
case API.get_recent_tracks(state.api_config) do
{:ok, tracks} ->
ListeningStats.update(tracks)
{:noreply, state, state.refresh_interval}
{:error, error} ->
if API.ErrorResponse.retryable_error?(error) do
{:noreply, state, API.ErrorResponse.retry_delay(error)}
else
{:stop, error, state}
end
end
end
end
@@ -0,0 +1,35 @@
defmodule MusicLibrary.Worker.RefreshScrobbles do
@moduledoc """
Fetches recent scrobbled tracks from Last.fm and persists them.
Scheduled via Oban Cron and can be triggered manually via
`MusicLibrary.ListeningStats.refresh/0`.
"""
use Oban.Worker,
queue: :last_fm,
max_attempts: 3,
unique: [period: 60, states: [:available, :scheduled, :executing]]
alias LastFm.{API, Config}
alias MusicLibrary.ListeningStats
@impl Oban.Worker
def perform(_job) do
api_config = Config.resolve(:music_library)
case API.get_recent_tracks(api_config) do
{:ok, tracks} ->
ListeningStats.update(tracks)
:ok
{:error, %API.ErrorResponse{error: error} = resp} ->
if API.ErrorResponse.retryable_error?(error) do
seconds = error |> API.ErrorResponse.retry_delay() |> div(1000)
{:snooze, seconds}
else
{:cancel, inspect(resp)}
end
end
end
end