From b7af9557c1573b5e648137b0b757740f5bbb2b5c Mon Sep 17 00:00:00 2001 From: Claudio Ortolina Date: Sat, 11 Apr 2026 21:26:04 +0100 Subject: [PATCH] Add MusicBrainz.get_all_releases/1 --- lib/music_brainz.ex | 29 ++++++++++++ test/music_brainz_test.exs | 97 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+) diff --git a/lib/music_brainz.ex b/lib/music_brainz.ex index b9c83dad..45fe29b4 100644 --- a/lib/music_brainz.ex +++ b/lib/music_brainz.ex @@ -5,6 +5,8 @@ defmodule MusicBrainz do alias MusicBrainz.API + @page_size 100 + @type search_opts :: [limit: non_neg_integer(), offset: non_neg_integer()] @spec search_release_group(String.t(), search_opts()) :: @@ -35,6 +37,33 @@ defmodule MusicBrainz do API.get_releases(musicbrainz_id, opts, music_brainz_config()) end + @doc """ + Fetches all releases belonging to the given release group by paging through + `get_releases/2` until an incomplete page is returned. + + Returns the accumulated list of release maps as returned by + `MusicBrainz.API.get_releases/3`. On a failure partway through the loop + the error is returned and accumulated results are discarded. + """ + @spec get_all_releases(String.t()) :: {:ok, [map()]} | {:error, term()} + def get_all_releases(release_group_id) do + fetch_all_releases(release_group_id, [], 0) + end + + defp fetch_all_releases(release_group_id, acc, offset) do + opts = [limit: @page_size, offset: offset] + + with {:ok, %{"releases" => page}} <- get_releases(release_group_id, opts) do + acc = acc ++ page + + if length(page) < @page_size do + {:ok, acc} + else + fetch_all_releases(release_group_id, acc, offset + @page_size) + end + end + end + @spec get_release(String.t()) :: {:ok, map()} | {:error, term()} def get_release(musicbrainz_id) do API.get_release(musicbrainz_id, music_brainz_config()) diff --git a/test/music_brainz_test.exs b/test/music_brainz_test.exs index b243f36a..14a88696 100644 --- a/test/music_brainz_test.exs +++ b/test/music_brainz_test.exs @@ -45,4 +45,101 @@ defmodule MusicBrainzTest do end) end end + + describe "get_all_releases/1" do + @release_group_id "ae504fd6-8498-463e-8d96-14f9e11d1863" + + defp release_page(count) do + releases = + for i <- 1..count do + %{"id" => "rel-#{:erlang.unique_integer([:positive])}", "title" => "Release #{i}"} + end + + %{"releases" => releases, "release-offset" => 0, "release-count" => count} + end + + test "single-page response returns the whole list" do + page = release_page(25) + + Req.Test.stub(MusicBrainz.API, fn conn -> + conn = Plug.Conn.fetch_query_params(conn) + assert conn.query_params["offset"] == "0" + assert conn.query_params["limit"] == "100" + Req.Test.json(conn, page) + end) + + assert {:ok, releases} = MusicBrainz.get_all_releases(@release_group_id) + assert releases == page["releases"] + end + + test "multi-page response accumulates pages in order until a short page is returned" do + full_page = release_page(100) + tail_page = release_page(42) + + {:ok, agent} = Agent.start_link(fn -> [] end) + + Req.Test.stub(MusicBrainz.API, fn conn -> + conn = Plug.Conn.fetch_query_params(conn) + Agent.update(agent, fn calls -> calls ++ [conn.query_params["offset"]] end) + + case conn.query_params["offset"] do + "0" -> Req.Test.json(conn, full_page) + "100" -> Req.Test.json(conn, tail_page) + end + end) + + assert {:ok, releases} = MusicBrainz.get_all_releases(@release_group_id) + assert length(releases) == 142 + assert releases == full_page["releases"] ++ tail_page["releases"] + assert Agent.get(agent, & &1) == ["0", "100"] + end + + test "exact-boundary page (equal to limit) triggers an extra fetch that returns empty" do + full_page = release_page(100) + empty_page = %{"releases" => []} + + Req.Test.stub(MusicBrainz.API, fn conn -> + conn = Plug.Conn.fetch_query_params(conn) + + case conn.query_params["offset"] do + "0" -> Req.Test.json(conn, full_page) + "100" -> Req.Test.json(conn, empty_page) + end + end) + + assert {:ok, releases} = MusicBrainz.get_all_releases(@release_group_id) + assert length(releases) == 100 + assert releases == full_page["releases"] + end + + test "empty response returns an empty list" do + Req.Test.stub(MusicBrainz.API, fn conn -> + Req.Test.json(conn, %{"releases" => []}) + end) + + assert {:ok, releases} = MusicBrainz.get_all_releases(@release_group_id) + assert releases == [] + end + + test "error on a later page is returned immediately" do + full_page = release_page(100) + + Req.Test.stub(MusicBrainz.API, fn conn -> + conn = Plug.Conn.fetch_query_params(conn) + + case conn.query_params["offset"] do + "0" -> + Req.Test.json(conn, full_page) + + "100" -> + conn + |> Plug.Conn.put_resp_content_type("application/json") + |> Plug.Conn.send_resp(503, ~s({"error":"service unavailable"})) + end + end) + + assert {:error, %{"error" => "service unavailable"}} = + MusicBrainz.get_all_releases(@release_group_id) + end + end end