diff --git a/backlog/tasks/ml-185 - Simplify-Presto-artwork-handling.md b/backlog/tasks/ml-185 - Simplify-Presto-artwork-handling.md index cdcb2c53..5fba65fd 100644 --- a/backlog/tasks/ml-185 - Simplify-Presto-artwork-handling.md +++ b/backlog/tasks/ml-185 - Simplify-Presto-artwork-handling.md @@ -1,10 +1,10 @@ --- id: ML-185 title: Simplify Presto artwork handling -status: To Do +status: Done assignee: [] created_date: "2026-05-15 21:19" -updated_date: "2026-05-15 21:40" +updated_date: "2026-05-15 21:53" labels: - api - presto @@ -43,11 +43,11 @@ Target state: each API record representation exposes four named cover variants: -- [ ] #1 Collection API record JSON exposes four named cover variants: original, large, medium, and small. -- [ ] #2 The small cover is 80 px wide for the Presto record-list cover, the medium cover is 460 px wide for the Presto record-detail cover, the large cover is 1000 px wide, and original is unscaled. -- [ ] #3 The Presto application consumes the API-provided small and medium cover URLs directly and no longer contains client-side cover resizing or JPEG scale-selection logic. -- [ ] #4 Presto documentation and test fixtures describe the new cover contract and no longer refer to the legacy cover URL fields. -- [ ] #5 Relevant Phoenix and Presto tests are updated and pass. +- [x] #1 Collection API record JSON exposes four named cover variants: original, large, medium, and small. +- [x] #2 The small cover is 80 px wide for the Presto record-list cover, the medium cover is 460 px wide for the Presto record-detail cover, the large cover is 1000 px wide, and original is unscaled. +- [x] #3 The Presto application consumes the API-provided small and medium cover URLs directly and no longer contains client-side cover resizing or JPEG scale-selection logic. +- [x] #4 Presto documentation and test fixtures describe the new cover contract and no longer refer to the legacy cover URL fields. +- [x] #5 Relevant Phoenix and Presto tests are updated and pass. ## Implementation Plan @@ -59,3 +59,25 @@ Target state: each API record representation exposes four named cover variants: 3. Update documentation, fixtures, and smoke/contract tests so the API and device assumptions stay aligned. 4. Run the focused controller tests and Presto syntax/smoke tests; note that physical Presto behavior is not verified unless the device deployment task is run on hardware. + +## Implementation Notes + + + +Both implementation subtasks are now Done. `ML-185.1` updated the collection API to emit `covers.original`, `covers.large`, `covers.medium`, and `covers.small`. `ML-185.2` updated the Presto app to consume `covers.small` and `covers.medium`, use an 80 px row cover and centered 460 px detail cover, and remove JPEG resizing/scale-selection logic. Focused Phoenix controller tests and Presto syntax/smoke tests passed. Physical Presto hardware was not tested. + + + +## Final Summary + + + +Completed the artwork handling simplification across the collection API and Presto client. + +The API now exposes named cover variants: `original` unscaled, `large` at 1000 px, `medium` at 460 px, and `small` at 80 px. The legacy flat cover fields were removed from the collection JSON contract. + +The Presto app now consumes `covers.small` for record rows and `covers.medium` for detail pages, centers the 460 px detail cover in the 480 px display, and no longer performs JPEG dimension probing or scale-selection resizing on-device. Documentation, guidance, mock fixtures, and smoke tests were updated for the new contract. + +Tests run across the subtasks: `mix test test/music_library_web/controllers/collection_controller_test.exs`; Presto syntax check; `mise run test` from `presto/`. Physical Presto hardware was not tested. + + diff --git a/backlog/tasks/ml-185.2 - Use-API-sized-covers-in-Presto.md b/backlog/tasks/ml-185.2 - Use-API-sized-covers-in-Presto.md index 23b720c4..2ee802e1 100644 --- a/backlog/tasks/ml-185.2 - Use-API-sized-covers-in-Presto.md +++ b/backlog/tasks/ml-185.2 - Use-API-sized-covers-in-Presto.md @@ -1,10 +1,11 @@ --- id: ML-185.2 title: Use API-sized covers in Presto -status: To Do -assignee: [] +status: Done +assignee: + - Codex created_date: "2026-05-15 21:22" -updated_date: "2026-05-15 21:40" +updated_date: "2026-05-15 21:53" labels: - presto - artwork @@ -39,12 +40,12 @@ Context for implementer: `presto/main.py` currently renders record-list covers a -- [ ] #1 Record-list rows fetch and draw the API-provided 80 px small cover variant. -- [ ] #2 Record-detail pages fetch and draw the API-provided 460 px medium cover variant centered within the 480 px display. -- [ ] #3 Presto image drawing no longer selects JPEG scale flags or resizes covers to fit a bounding box. -- [ ] #4 Missing or failed cover downloads still render placeholders, and drag redraws still avoid network fetches. -- [ ] #5 Presto README, Presto guidance, and smoke-test fixtures match the new named cover contract. -- [ ] #6 The Presto syntax check and smoke tests pass, with physical device verification only claimed if run on the device. +- [x] #1 Record-list rows fetch and draw the API-provided 80 px small cover variant. +- [x] #2 Record-detail pages fetch and draw the API-provided 460 px medium cover variant centered within the 480 px display. +- [x] #3 Presto image drawing no longer selects JPEG scale flags or resizes covers to fit a bounding box. +- [x] #4 Missing or failed cover downloads still render placeholders, and drag redraws still avoid network fetches. +- [x] #5 Presto README, Presto guidance, and smoke-test fixtures match the new named cover contract. +- [x] #6 The Presto syntax check and smoke tests pass, with physical device verification only claimed if run on the device. ## Implementation Plan @@ -58,3 +59,25 @@ Context for implementer: `presto/main.py` currently renders record-list covers a 5. Update `presto/README.md`, `presto/AGENTS.md`, and test mock records to describe and exercise the new contract. 6. Run `python3 -c "import py_compile; py_compile.compile('main.py', cfile='/tmp/main.pyc', doraise=True)"` from `presto/` and `mise run test` for the Presto smoke suite. + +## Implementation Notes + + + +Updated the Presto cover path to consume only the collection API `covers` object: row covers use `covers.small`, detail covers use `covers.medium`, and the detail cover layout is now 460 px wide and centered at x=10 on the 480 px display. Removed JPEG dimension probing and scale-selection logic so `draw_jpeg` decodes API-sized JPEGs at the requested position and uses the provided dimensions only for placeholder fallback. Preserved row/detail byte cache separation and drag-time placeholder behavior. Ran `python3 -c "import py_compile; py_compile.compile('main.py', cfile='/tmp/main.pyc', doraise=True)"` and `mise run test` from `presto/`; both passed. Physical Presto hardware was not tested. + + + +## Final Summary + + + +Updated the Presto app to trust the API-provided cover sizes. Record rows now read `covers.small`, record detail pages read `covers.medium`, and the detail cover is 460 px wide and centered within the 480 px display. + +Removed the JPEG dimension probing and scale-selection code from `draw_jpeg`; it now decodes API-sized JPEGs directly and only uses placeholder dimensions when a fetch or decode fails. Existing row/detail image caches and no-network-during-drag behavior are preserved. + +Updated Presto guidance, README API examples, and smoke-test fixtures for the new cover contract, and added a focused smoke test assertion for the expected 80 px row cover and 460 px centered detail cover. + +Tests run: Presto syntax check and `mise run test` from `presto/` (8 passed). Physical device behavior was not verified on hardware. + + diff --git a/presto/AGENTS.md b/presto/AGENTS.md index f6bff25f..32ee3fa3 100644 --- a/presto/AGENTS.md +++ b/presto/AGENTS.md @@ -72,7 +72,8 @@ _These rules apply to any scrollable view (day list, detail page, or future addi ## Image Handling -- Row thumbnails prefer `micro_cover_url` → `mini_cover_url` → `thumb_url`. Detail/large cover prefers `thumb_url` → `mini_cover_url` → `micro_cover_url`. +- Row thumbnails use `covers.small` from the API. Detail covers use `covers.medium`. +- The API provides display-ready JPEG sizes: `covers.small` is 80px and `covers.medium` is 460px. Do not resize covers on the device. - Cache downloaded image bytes on the record dict. Use separate cache keys for different sizes (e.g., `_thumb_data` for rows, `_detail_thumb_data` for detail view). Never fetch images during drag — show placeholders instead and repaint real covers on release. - JPEG is the practical default. PNG has worse memory characteristics; raw/RGB565 needs a confirmed stable blit API on the target firmware. @@ -99,7 +100,7 @@ GET https://music-library.claudio-ortolina.org/api/v1/collection/on_this_day?dat Authorization: Bearer ``` -Fields used by the app: `title`, `artists`, `format`, `release_date`, `genres`, `record_type`, `purchased_at`, `micro_cover_url`, `mini_cover_url`, `thumb_url`, `selected_release_id`. +Fields used by the app: `title`, `artists`, `format`, `release_date`, `genres`, `record_type`, `purchased_at`, `covers.small`, `covers.medium`, `selected_release_id`. POST /api/v1/collection/:record_id/scrobble Authorization: Bearer diff --git a/presto/README.md b/presto/README.md index 1905cfb3..abb6dd2d 100644 --- a/presto/README.md +++ b/presto/README.md @@ -102,7 +102,7 @@ The Presto runs `main.py` on boot. The app will: ### Record detail view - Tap any **record row** in the day view to open its detail page -- Shows a **larger cover image** (uses `thumb_url`, 480px source), the full title, artists, genres, record type, format, release year, and purchase date +- Shows a **larger cover image** (uses `covers.medium`, 460px source), the full title, artists, genres, record type, format, release year, and purchase date - Drag vertically to scroll if the content is taller than the screen - The header bar stays fixed at the top while the cover and info scroll underneath - Tap **< Back** to return to the day view @@ -207,9 +207,12 @@ Response format: "selected_release_id": "abc-123-uuid", "title": "Album Title", "artists": ["Artist Name"], - "micro_cover_url": "https://.../api/v1/assets/thumb.jpg?width=40", - "mini_cover_url": "https://.../api/v1/assets/thumb.jpg?width=150", - "thumb_url": "https://.../api/v1/assets/thumb.jpg?width=480", + "covers": { + "original": "https://.../api/v1/assets/original", + "large": "https://.../api/v1/assets/large-1000", + "medium": "https://.../api/v1/assets/medium-460", + "small": "https://.../api/v1/assets/small-80" + }, "release_date": "1973-03-01", "genres": ["Rock"], "format": "Vinyl", @@ -220,8 +223,9 @@ Response format: } ``` -The Presto client prefers `mini_cover_url` for the 80×80 full-resolution row thumbnail, then falls back to `micro_cover_url` and `thumb_url`. -The detail view prefers `thumb_url` for the full-size cover, falling back to `mini_cover_url` and `micro_cover_url`. +The Presto client uses `covers.small` for the 80×80 full-resolution row thumbnail. +The detail view uses `covers.medium` for the 460×460 cover centered in the 480×480 display. +The app does not resize covers on the device; it expects the API to provide display-ready JPEGs. ## License diff --git a/presto/main.py b/presto/main.py index 3b4a82f7..741a755d 100644 --- a/presto/main.py +++ b/presto/main.py @@ -117,7 +117,7 @@ DAY_HEADER_TEXT_H = HEADER_TEXT_H DAY_COUNT_TEXT_H = TEXT_H # Detail view -DETAIL_COVER_SIZE = px(240) +DETAIL_COVER_SIZE = px(230) DETAIL_COVER_X = (WIDTH - DETAIL_COVER_SIZE) // 2 DETAIL_COVER_Y = DAY_HEADER_Y + DAY_HEADER_H + px(12) DETAIL_INFO_GAP = px(4) @@ -832,19 +832,6 @@ def show_search_results(app, recs, had_error): draw_search_results(app) -# ============================================================================ -# JPEG RENDERING -# ============================================================================ - -def _jpeg_scale_options(): - """Return JPEG scale flags paired with their output-size divisors.""" - full = getattr(_jpegdec_lib, "JPEG_SCALE_FULL", 0) - half = getattr(_jpegdec_lib, "JPEG_SCALE_HALF", 1) - quarter = getattr(_jpegdec_lib, "JPEG_SCALE_QUARTER", 2) - eighth = getattr(_jpegdec_lib, "JPEG_SCALE_EIGHTH", 3) - return ((full, 1), (half, 2), (quarter, 4), (eighth, 8)) - - def _close_jpeg(jpeg): """Release a jpegdec object if the firmware exposes close().""" if jpeg is None: @@ -856,18 +843,17 @@ def _close_jpeg(jpeg): pass -def draw_jpeg(data, x, y, max_w, max_h): - """Decode and draw a JPEG image, scaled to fit within max_w x max_h - and centered in the bounding box. +def draw_jpeg(data, x, y, placeholder_w, placeholder_h): + """Decode and draw an API-sized JPEG image at x, y. Uses jpegdec module (standard on Pimoroni firmware). Falls back to a placeholder rectangle on failure. """ if data is None: - _draw_placeholder(x, y, max_w, max_h) + _draw_placeholder(x, y, placeholder_w, placeholder_h) return - # Try jpegdec module with smart scaling + # Try jpegdec module. if _HAS_JPEGDEC: jpeg = None try: @@ -877,32 +863,11 @@ def draw_jpeg(data, x, y, max_w, max_h): except Exception: jpeg.open_RAM(data) - # Attempt smart scaling: get dimensions, pick best scale, center try: - img_h = jpeg.get_height() - img_w = jpeg.get_width() - # Try scales from largest to smallest; use the first that fits. - scale = None - divisor = 1 - for scale_flag, scale_divisor in _jpeg_scale_options(): - if img_w // scale_divisor <= max_w and img_h // scale_divisor <= max_h: - scale = scale_flag - divisor = scale_divisor - break - if scale is None: - scale, divisor = _jpeg_scale_options()[-1] - sw = img_w // divisor - sh = img_h // divisor - ox = x + (max_w - sw) // 2 - oy = y + (max_h - sh) // 2 - jpeg.decode(ox, oy, scale) - _close_jpeg(jpeg) - return - except Exception: - pass - - # Fallback: decode at quarter scale (works for most cover art) - jpeg.decode(x, y, getattr(_jpegdec_lib, "JPEG_SCALE_QUARTER", 2)) + jpeg.decode(x, y) + except TypeError: + # Older firmware expects a third full-size decode argument. + jpeg.decode(x, y, 0) _close_jpeg(jpeg) return except Exception: @@ -927,7 +892,7 @@ def draw_jpeg(data, x, y, max_w, max_h): pass # All methods failed — draw placeholder - _draw_placeholder(x, y, max_w, max_h) + _draw_placeholder(x, y, placeholder_w, placeholder_h) def _draw_placeholder(x, y, w, h): @@ -1608,7 +1573,7 @@ def _prepare_record_for_display(rec): def preload_record_thumbnails(app, recs): """Fetch thumbnail bytes before the first draw of a record list.""" for rec in recs: - if rec.get("micro_cover_url", ""): + if _record_thumbnail_url(rec): _record_thumbnail_data(app, rec) @@ -1681,12 +1646,21 @@ def _record_thumbnail_data(app, rec): def _record_thumbnail_url(rec): - """Return the preferred cover URL for a record row.""" - return ( - rec.get("mini_cover_url", "") - or rec.get("micro_cover_url", "") - or rec.get("thumb_url", "") - ) + """Return the API-sized cover URL for a record row.""" + return _record_cover_url(rec, "small") + + +def _record_detail_cover_url(rec): + """Return the API-sized cover URL for a record detail page.""" + return _record_cover_url(rec, "medium") + + +def _record_cover_url(rec, size): + """Return a named cover URL from the API covers object.""" + covers = rec.get("covers", {}) + if not covers: + return "" + return covers.get(size, "") or "" def _draw_record_row(app, rec, y): @@ -2023,13 +1997,9 @@ def _draw_detail_cover(app, rec, y): if y >= HEIGHT: return - thumb_url = ( - rec.get("thumb_url", "") - or rec.get("mini_cover_url", "") - or rec.get("micro_cover_url", "") - ) + cover_url = _record_detail_cover_url(rec) - if not thumb_url: + if not cover_url: _draw_placeholder(DETAIL_COVER_X, y, DETAIL_COVER_SIZE, DETAIL_COVER_SIZE) return @@ -2042,7 +2012,7 @@ def _draw_detail_cover(app, rec, y): return if data is None and not rec.get("_detail_thumb_failed", False): - data = fetch_thumbnail(app, thumb_url) + data = fetch_thumbnail(app, cover_url) if data is None: rec["_detail_thumb_failed"] = True else: diff --git a/presto/tests/conftest.py b/presto/tests/conftest.py index b9e771fc..457b7301 100644 --- a/presto/tests/conftest.py +++ b/presto/tests/conftest.py @@ -105,6 +105,12 @@ def make_mock_record(index=0): "genres": ["Jazz", "Modal"], "record_type": "LP", "purchased_at": "2024-03-15", + "covers": { + "original": f"https://example.com/covers/original-{index}.jpg", + "large": f"https://example.com/covers/large-{index}.jpg", + "medium": f"https://example.com/covers/medium-{index}.jpg", + "small": f"https://example.com/covers/small-{index}.jpg", + }, # Pre-computed display strings (row view) "_display_title": title, "_display_artists": artist, diff --git a/presto/tests/test_screens.py b/presto/tests/test_screens.py index b8c2c785..eda18bf4 100644 --- a/presto/tests/test_screens.py +++ b/presto/tests/test_screens.py @@ -96,6 +96,16 @@ class TestSmokeScreens: main_module.draw_record_detail(app) self._screenshot(main_module, "record-detail") + def test_cover_contract_uses_api_sized_images(self, main_module): + """Cover helpers use the named API sizes expected by the display.""" + rec = make_mock_record(0) + + assert main_module.THUMB_SIZE == 80 + assert main_module.DETAIL_COVER_SIZE == 460 + assert main_module.DETAIL_COVER_X == 10 + assert main_module._record_thumbnail_url(rec).endswith("/small-0.jpg") + assert main_module._record_detail_cover_url(rec).endswith("/medium-0.jpg") + def test_search_input_renders(self, main_module): """Search input screen with on-screen keyboard.""" app = self._make_app(main_module) @@ -113,7 +123,7 @@ class TestSmokeScreens: app.search.results = recs # Pre-compute content height (prepare_record_list won't trigger - # network calls because _thumb_failed=True and _thumb_url=""). + # network calls because _thumb_failed=True). app.search.content_height = main_module.prepare_record_list(app, recs) main_module.draw_search_results(app)