ML-185.2: use API-sized Presto covers
This commit is contained in:
@@ -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:
|
||||
|
||||
<!-- AC:BEGIN -->
|
||||
|
||||
- [ ] #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.
|
||||
<!-- AC:END -->
|
||||
|
||||
## 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.
|
||||
<!-- SECTION:PLAN:END -->
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
<!-- SECTION:NOTES:BEGIN -->
|
||||
|
||||
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.
|
||||
|
||||
<!-- SECTION:NOTES:END -->
|
||||
|
||||
## Final Summary
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||
|
||||
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.
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||
|
||||
@@ -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
|
||||
|
||||
<!-- AC:BEGIN -->
|
||||
|
||||
- [ ] #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.
|
||||
<!-- AC:END -->
|
||||
|
||||
## 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.
|
||||
<!-- SECTION:PLAN:END -->
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
<!-- SECTION:NOTES:BEGIN -->
|
||||
|
||||
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.
|
||||
|
||||
<!-- SECTION:NOTES:END -->
|
||||
|
||||
## Final Summary
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||
|
||||
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.
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||
|
||||
+3
-2
@@ -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 <API_TOKEN>
|
||||
```
|
||||
|
||||
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 <API_TOKEN>
|
||||
|
||||
+10
-6
@@ -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
|
||||
|
||||
|
||||
+29
-59
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user