From b4710e10c881747d37b10eca49b15771bb577539 Mon Sep 17 00:00:00 2001 From: Claudio Ortolina Date: Fri, 15 May 2026 22:49:06 +0100 Subject: [PATCH] ML-185.1: expose named cover sizes in collection API --- ...ose-named-cover-sizes-in-collection-API.md | 37 +++++++++++++++---- .../controllers/collection_json.ex | 24 +++++++++--- .../collection_controller_test.exs | 21 +++++++---- test/prod.hurl | 2 +- 4 files changed, 61 insertions(+), 23 deletions(-) diff --git a/backlog/tasks/ml-185.1 - Expose-named-cover-sizes-in-collection-API.md b/backlog/tasks/ml-185.1 - Expose-named-cover-sizes-in-collection-API.md index f7e6f5a9..510bbf48 100644 --- a/backlog/tasks/ml-185.1 - Expose-named-cover-sizes-in-collection-API.md +++ b/backlog/tasks/ml-185.1 - Expose-named-cover-sizes-in-collection-API.md @@ -1,10 +1,11 @@ --- id: ML-185.1 title: Expose named cover sizes in collection API -status: To Do -assignee: [] +status: Done +assignee: + - Codex created_date: "2026-05-15 21:20" -updated_date: "2026-05-15 21:40" +updated_date: "2026-05-15 21:44" labels: - api - artwork @@ -35,11 +36,11 @@ Context for implementer: `MusicLibraryWeb.CollectionJSON` currently builds `cove -- [ ] #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. -- [ ] #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. -- [ ] #5 Production Hurl smoke coverage captures and requests a cover URL from the new contract. +- [x] #1 Collection API responses that render records include a named cover object with original, large, medium, and small URLs. +- [x] #2 The generated URLs encode no width for original, 1000 px for large, 460 px for medium, and 80 px for small. +- [x] #3 Legacy flat cover URL fields are removed from the collection JSON contract unless a compatibility decision is explicitly documented during implementation. +- [x] #4 Controller tests assert the new cover contract for collection record responses. +- [x] #5 Production Hurl smoke coverage captures and requests a cover URL from the new contract. ## 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. 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. + +## Implementation Notes + + + +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. + + + +## Final Summary + + + +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). + + diff --git a/lib/music_library_web/controllers/collection_json.ex b/lib/music_library_web/controllers/collection_json.ex index 19971b83..2ecad48d 100644 --- a/lib/music_library_web/controllers/collection_json.ex +++ b/lib/music_library_web/controllers/collection_json.ex @@ -3,6 +3,10 @@ defmodule MusicLibraryWeb.CollectionJSON do alias MusicLibrary.Assets.Transform + @large_cover_width 1000 + @medium_cover_width 460 + @small_cover_width 80 + def show(%{record: record}) do record(record) end @@ -34,12 +38,20 @@ defmodule MusicLibraryWeb.CollectionJSON do selected_release_id: record.selected_release_id, artists: Enum.map(record.artists, & &1.name), title: record.title, - cover_url: url(~p"/api/v1/assets/#{Transform.new(hash: 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)}") + covers: cover_urls(record.cover_hash) } 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 diff --git a/test/music_library_web/controllers/collection_controller_test.exs b/test/music_library_web/controllers/collection_controller_test.exs index 87dc7b8a..3e93a7c8 100644 --- a/test/music_library_web/controllers/collection_controller_test.exs +++ b/test/music_library_web/controllers/collection_controller_test.exs @@ -4,6 +4,7 @@ defmodule MusicLibraryWeb.CollectionControllerTest do import MusicLibrary.Fixtures.Records alias MusicBrainz.Fixtures.Release, as: ReleaseFixtures + alias MusicLibrary.Assets.Transform alias MusicLibrary.Secrets alias Req.Test @@ -321,14 +322,18 @@ defmodule MusicLibraryWeb.CollectionControllerTest do "purchased_at" => record.purchased_at |> DateTime.to_iso8601(), "artists" => Enum.map(record.artists, & &1.name), "title" => record.title, - "cover_url" => - "http://localhost:4002/api/v1/assets/eyJoYXNoIjoiNTk5NDA3RERGNjk5MDdENEE2MEZFMTNDQ0FBODI0RDI1Q0YwOERDMTI0RkQ2QUEzRThFN0VDRDk4Qzg4NUZGRSIsIndpZHRoIjpudWxsfQ", - "thumb_url" => - "http://localhost:4002/api/v1/assets/eyJoYXNoIjoiNTk5NDA3RERGNjk5MDdENEE2MEZFMTNDQ0FBODI0RDI1Q0YwOERDMTI0RkQ2QUEzRThFN0VDRDk4Qzg4NUZGRSIsIndpZHRoIjo0ODB9", - "mini_cover_url" => - "http://localhost:4002/api/v1/assets/eyJoYXNoIjoiNTk5NDA3RERGNjk5MDdENEE2MEZFMTNDQ0FBODI0RDI1Q0YwOERDMTI0RkQ2QUEzRThFN0VDRDk4Qzg4NUZGRSIsIndpZHRoIjoxNTB9", - "micro_cover_url" => - "http://localhost:4002/api/v1/assets/eyJoYXNoIjoiNTk5NDA3RERGNjk5MDdENEE2MEZFMTNDQ0FBODI0RDI1Q0YwOERDMTI0RkQ2QUEzRThFN0VDRDk4Qzg4NUZGRSIsIndpZHRoIjo0MH0" + "covers" => %{ + "original" => asset_url(record.cover_hash, nil), + "large" => asset_url(record.cover_hash, 1000), + "medium" => asset_url(record.cover_hash, 460), + "small" => asset_url(record.cover_hash, 80) + } } 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 diff --git a/test/prod.hurl b/test/prod.hurl index 2bb2b975..f7692206 100644 --- a/test/prod.hurl +++ b/test/prod.hurl @@ -21,7 +21,7 @@ HTTP 200 jsonpath "$.title" != null [Captures] -cover-url: jsonpath "$.cover_url" +cover-url: jsonpath "$.covers.original" # GET cover image GET {{cover-url}}