ML-185.1: expose named cover sizes in collection API

This commit is contained in:
Claudio Ortolina
2026-05-15 22:49:06 +01:00
parent f1b33fdc5f
commit b4710e10c8
4 changed files with 61 additions and 23 deletions
@@ -1,10 +1,11 @@
--- ---
id: ML-185.1 id: ML-185.1
title: Expose named cover sizes in collection API title: Expose named cover sizes in collection API
status: To Do status: Done
assignee: [] assignee:
- Codex
created_date: "2026-05-15 21:20" created_date: "2026-05-15 21:20"
updated_date: "2026-05-15 21:40" updated_date: "2026-05-15 21:44"
labels: labels:
- api - api
- artwork - artwork
@@ -35,11 +36,11 @@ Context for implementer: `MusicLibraryWeb.CollectionJSON` currently builds `cove
<!-- AC:BEGIN --> <!-- AC:BEGIN -->
- [ ] #1 Collection API responses that render records include a named cover object with original, large, medium, and small URLs. - [x] #1 Collection API responses that render records include a named cover object with original, large, medium, and small URLs.
- [ ] #2 The generated URLs encode no width for original, 1000 px for large, 460 px for medium, and 80 px for small. - [x] #2 The generated URLs encode no width for original, 1000 px for large, 460 px for medium, and 80 px for small.
- [ ] #3 Legacy flat cover URL fields are removed from the collection JSON contract unless a compatibility decision is explicitly documented during implementation. - [x] #3 Legacy flat cover URL fields are removed from the collection JSON contract unless a compatibility decision is explicitly documented during implementation.
- [ ] #4 Controller tests assert the new cover contract for collection record responses. - [x] #4 Controller tests assert the new cover contract for collection record responses.
- [ ] #5 Production Hurl smoke coverage captures and requests a cover URL from the new contract. - [x] #5 Production Hurl smoke coverage captures and requests a cover URL from the new contract.
<!-- AC:END --> <!-- AC:END -->
## Implementation Plan ## Implementation Plan
@@ -52,3 +53,23 @@ Context for implementer: `MusicLibraryWeb.CollectionJSON` currently builds `cove
4. Update `test/prod.hurl` to capture a cover URL from the new cover object. 4. Update `test/prod.hurl` to capture a cover URL from the new cover object.
5. Run `mix test test/music_library_web/controllers/collection_controller_test.exs` and any focused asset-controller test if URL generation or transform behavior changes. 5. Run `mix test test/music_library_web/controllers/collection_controller_test.exs` and any focused asset-controller test if URL generation or transform behavior changes.
<!-- SECTION:PLAN:END --> <!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Implemented the collection record cover contract as a `covers` object with `original`, `large`, `medium`, and `small` URLs. The old flat fields were removed from `MusicLibraryWeb.CollectionJSON`; exact-map controller expectations verify the legacy fields are absent. Ran `mix test test/music_library_web/controllers/collection_controller_test.exs` successfully: 16 tests passed. Dependency warnings were emitted during compilation, but no warnings from the changed project files.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Updated the collection API record representation to expose named cover variants through `covers.original`, `covers.large`, `covers.medium`, and `covers.small`. The generated asset transform widths are unscaled for `original`, 1000 px for `large`, 460 px for `medium`, and 80 px for `small`; the legacy flat cover URL fields are no longer emitted.
Updated the controller test expected payloads to assert the new exact response shape and switched the production Hurl smoke capture to `$.covers.original`.
Tests run: `mix test test/music_library_web/controllers/collection_controller_test.exs` (16 passed).
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -3,6 +3,10 @@ defmodule MusicLibraryWeb.CollectionJSON do
alias MusicLibrary.Assets.Transform alias MusicLibrary.Assets.Transform
@large_cover_width 1000
@medium_cover_width 460
@small_cover_width 80
def show(%{record: record}) do def show(%{record: record}) do
record(record) record(record)
end end
@@ -34,12 +38,20 @@ defmodule MusicLibraryWeb.CollectionJSON do
selected_release_id: record.selected_release_id, selected_release_id: record.selected_release_id,
artists: Enum.map(record.artists, & &1.name), artists: Enum.map(record.artists, & &1.name),
title: record.title, title: record.title,
cover_url: url(~p"/api/v1/assets/#{Transform.new(hash: record.cover_hash)}"), covers: cover_urls(record.cover_hash)
thumb_url: url(~p"/api/v1/assets/#{Transform.new(hash: record.cover_hash, width: 480)}"),
mini_cover_url:
url(~p"/api/v1/assets/#{Transform.new(hash: record.cover_hash, width: 150)}"),
micro_cover_url:
url(~p"/api/v1/assets/#{Transform.new(hash: record.cover_hash, width: 40)}")
} }
end end
defp cover_urls(cover_hash) do
%{
original: cover_url(cover_hash, nil),
large: cover_url(cover_hash, @large_cover_width),
medium: cover_url(cover_hash, @medium_cover_width),
small: cover_url(cover_hash, @small_cover_width)
}
end
defp cover_url(cover_hash, width) do
url(~p"/api/v1/assets/#{Transform.new(hash: cover_hash, width: width)}")
end
end end
@@ -4,6 +4,7 @@ defmodule MusicLibraryWeb.CollectionControllerTest do
import MusicLibrary.Fixtures.Records import MusicLibrary.Fixtures.Records
alias MusicBrainz.Fixtures.Release, as: ReleaseFixtures alias MusicBrainz.Fixtures.Release, as: ReleaseFixtures
alias MusicLibrary.Assets.Transform
alias MusicLibrary.Secrets alias MusicLibrary.Secrets
alias Req.Test alias Req.Test
@@ -321,14 +322,18 @@ defmodule MusicLibraryWeb.CollectionControllerTest do
"purchased_at" => record.purchased_at |> DateTime.to_iso8601(), "purchased_at" => record.purchased_at |> DateTime.to_iso8601(),
"artists" => Enum.map(record.artists, & &1.name), "artists" => Enum.map(record.artists, & &1.name),
"title" => record.title, "title" => record.title,
"cover_url" => "covers" => %{
"http://localhost:4002/api/v1/assets/eyJoYXNoIjoiNTk5NDA3RERGNjk5MDdENEE2MEZFMTNDQ0FBODI0RDI1Q0YwOERDMTI0RkQ2QUEzRThFN0VDRDk4Qzg4NUZGRSIsIndpZHRoIjpudWxsfQ", "original" => asset_url(record.cover_hash, nil),
"thumb_url" => "large" => asset_url(record.cover_hash, 1000),
"http://localhost:4002/api/v1/assets/eyJoYXNoIjoiNTk5NDA3RERGNjk5MDdENEE2MEZFMTNDQ0FBODI0RDI1Q0YwOERDMTI0RkQ2QUEzRThFN0VDRDk4Qzg4NUZGRSIsIndpZHRoIjo0ODB9", "medium" => asset_url(record.cover_hash, 460),
"mini_cover_url" => "small" => asset_url(record.cover_hash, 80)
"http://localhost:4002/api/v1/assets/eyJoYXNoIjoiNTk5NDA3RERGNjk5MDdENEE2MEZFMTNDQ0FBODI0RDI1Q0YwOERDMTI0RkQ2QUEzRThFN0VDRDk4Qzg4NUZGRSIsIndpZHRoIjoxNTB9", }
"micro_cover_url" =>
"http://localhost:4002/api/v1/assets/eyJoYXNoIjoiNTk5NDA3RERGNjk5MDdENEE2MEZFMTNDQ0FBODI0RDI1Q0YwOERDMTI0RkQ2QUEzRThFN0VDRDk4Qzg4NUZGRSIsIndpZHRoIjo0MH0"
} }
end end
defp asset_url(cover_hash, width) do
payload = Transform.new(hash: cover_hash, width: width)
"http://localhost:4002#{~p"/api/v1/assets/#{payload}"}"
end
end end
+1 -1
View File
@@ -21,7 +21,7 @@ HTTP 200
jsonpath "$.title" != null jsonpath "$.title" != null
[Captures] [Captures]
cover-url: jsonpath "$.cover_url" cover-url: jsonpath "$.covers.original"
# GET cover image # GET cover image
GET {{cover-url}} GET {{cover-url}}