Move scrobble queries from Records to ListeningStats

Closes #110
This commit is contained in:
Claudio Ortolina
2026-03-12 16:32:10 +00:00
parent 661e6f7c38
commit ee4d2555ee
4 changed files with 101 additions and 40 deletions
+37 -1
View File
@@ -10,7 +10,7 @@ defmodule MusicLibrary.ListeningStats do
import Ecto.Query import Ecto.Query
alias LastFm.Track alias LastFm.Track
alias MusicLibrary.{Artists, Collection, Records.ArtistRecord, Repo, Wishlist} alias MusicLibrary.{Artists, Collection, Records.ArtistRecord, Records.Record, Repo, Wishlist}
@pagination Application.compile_env!(:music_library, :pagination) @pagination Application.compile_env!(:music_library, :pagination)
@@ -247,6 +247,42 @@ defmodule MusicLibrary.ListeningStats do
} }
end end
@spec get_last_listened_track(Record.t()) :: Track.t() | nil
def get_last_listened_track(record) do
q =
from t in scrobbles_for_record_query(record),
order_by: [desc: t.scrobbled_at_uts],
limit: 1
Repo.one(q)
end
@spec play_count(Record.t()) :: non_neg_integer()
def play_count(record) do
record
|> scrobbles_for_record_query()
|> Repo.aggregate(:count)
end
defp scrobbles_for_record_query(record) do
record_id = record.id
q =
from r in fragment("records, json_each(records.release_ids)"),
where: fragment("records.id = ?", ^record_id),
select: r.value
release_ids = Repo.all(q)
main_artist_name = Record.main_artist(record).name
record_title = record.title
from t in Track,
where: fragment("? ->> '$.musicbrainz_id'", t.album) in ^release_ids,
or_where:
fragment("? ->> '$.title'", t.album) == ^record_title and
fragment("? ->> '$.name'", t.artist) == ^main_artist_name
end
defp polyfill_artist(artist, musicbrainz_id) do defp polyfill_artist(artist, musicbrainz_id) do
if is_nil(artist.musicbrainz_id) or artist.musicbrainz_id == "" do if is_nil(artist.musicbrainz_id) or artist.musicbrainz_id == "" do
%{artist | musicbrainz_id: musicbrainz_id} %{artist | musicbrainz_id: musicbrainz_id}
-36
View File
@@ -278,42 +278,6 @@ defmodule MusicLibrary.Records do
end end
end end
@spec get_last_listened_track(Record.t()) :: LastFm.Track.t() | nil
def get_last_listened_track(record) do
q =
from t in scrobbles_for_record_query(record),
order_by: [desc: t.scrobbled_at_uts],
limit: 1
Repo.one(q)
end
@spec play_count(Record.t()) :: non_neg_integer()
def play_count(record) do
record
|> scrobbles_for_record_query()
|> Repo.aggregate(:count)
end
defp scrobbles_for_record_query(record) do
record_id = record.id
q =
from r in fragment("records, json_each(records.release_ids)"),
where: fragment("records.id = ?", ^record_id),
select: r.value
release_ids = Repo.all(q)
main_artist_name = Record.main_artist(record).name
record_title = record.title
from t in LastFm.Track,
where: fragment("? ->> '$.musicbrainz_id'", t.album) in ^release_ids,
or_where:
fragment("? ->> '$.title'", t.album) == ^record_title and
fragment("? ->> '$.name'", t.artist) == ^main_artist_name
end
@spec refresh_cover(Record.t()) :: {:ok, Record.t()} | {:error, term()} @spec refresh_cover(Record.t()) :: {:ok, Record.t()} | {:error, term()}
def refresh_cover(record) do def refresh_cover(record) do
with {:ok, cover_data} <- MusicBrainz.get_cover_art({:url, record.cover_url}), with {:ok, cover_data} <- MusicBrainz.get_cover_art({:url, record.cover_url}),
@@ -21,7 +21,7 @@ defmodule MusicLibraryWeb.CollectionLive.Show do
similar_records: 1 similar_records: 1
] ]
alias MusicLibrary.{Records, RecordSets, ScrobbleActivity} alias MusicLibrary.{ListeningStats, Records, RecordSets, ScrobbleActivity}
alias MusicLibrary.Records.Similarity alias MusicLibrary.Records.Similarity
alias MusicLibraryWeb.ErrorMessages alias MusicLibraryWeb.ErrorMessages
alias Phoenix.LiveView.JS alias Phoenix.LiveView.JS
@@ -346,8 +346,8 @@ defmodule MusicLibraryWeb.CollectionLive.Show do
@impl true @impl true
def handle_params(%{"id" => id}, _, socket) do def handle_params(%{"id" => id}, _, socket) do
record = Records.get_record!(id) record = Records.get_record!(id)
last_listened_track = Records.get_last_listened_track(record) last_listened_track = ListeningStats.get_last_listened_track(record)
play_count = Records.play_count(record) || 0 play_count = ListeningStats.play_count(record) || 0
socket = socket =
if record.selected_release_id do if record.selected_release_id do
@@ -4,7 +4,9 @@ defmodule MusicLibrary.ListeningStatsTest do
import MusicLibrary.ArtistInfoFixtures import MusicLibrary.ArtistInfoFixtures
import MusicLibrary.ScrobbledTracksFixtures import MusicLibrary.ScrobbledTracksFixtures
alias MusicLibrary.Fixtures.Records, as: RecordsFixtures
alias MusicLibrary.ListeningStats alias MusicLibrary.ListeningStats
alias MusicLibrary.Records.Record
describe "scrobble_count/0" do describe "scrobble_count/0" do
test "returns correct count" do test "returns correct count" do
@@ -94,6 +96,65 @@ defmodule MusicLibrary.ListeningStatsTest do
end end
end end
describe "get_last_listened_track/1" do
test "returns the most recent scrobbled track for a record" do
record = RecordsFixtures.record()
main_artist = Record.main_artist(record)
now = System.system_time(:second)
_older =
track_fixture(%{
artist_name: main_artist.name,
album_title: record.title,
title: "Older Track",
scrobbled_at_uts: now - 200
})
track_fixture(%{
artist_name: main_artist.name,
album_title: record.title,
title: "Newer Track",
scrobbled_at_uts: now - 100
})
result = ListeningStats.get_last_listened_track(record)
assert result.title == "Newer Track"
assert result.scrobbled_at_uts == now - 100
end
test "returns nil when no scrobbles exist for the record" do
record = RecordsFixtures.record()
assert ListeningStats.get_last_listened_track(record) == nil
end
end
describe "play_count/1" do
test "returns the correct scrobble count for a record" do
record = RecordsFixtures.record()
main_artist = Record.main_artist(record)
now = System.system_time(:second)
for i <- 1..3 do
track_fixture(%{
artist_name: main_artist.name,
album_title: record.title,
title: "Track #{i}",
scrobbled_at_uts: now - i * 100
})
end
assert ListeningStats.play_count(record) == 3
end
test "returns 0 when no scrobbles exist for the record" do
record = RecordsFixtures.record()
assert ListeningStats.play_count(record) == 0
end
end
describe "get_top_artists_by_days/2" do describe "get_top_artists_by_days/2" do
test "counts tracks with missing artist_infos records within date range" do test "counts tracks with missing artist_infos records within date range" do
artist_info = artist_info =