Skip to content

Commit 9d00620

Browse files
OzGavmarcelveldt
andauthored
Fix now-playing artwork showing a solid background for transparent logos (#4188)
# What does this implement/fix? <!-- Quick description and explanation of changes. --> The `Player.current_media` state property forced `fmt=jpeg` on its image URL, which flattens transparent station logos onto a solid background (black before #4094, white after). That URL is also rendered by the frontend, so the UI showed the flattened variant when reloading while a radio station is playing. `current_media` now keeps the transparency-preserving format for frontend/ API consumers, while `player_image_url()` which is already used, for example, by AirPlay for artwork passed to cliraop. This forces `fmt=jpeg` at the point of device consumption, so players receive the exact same flattened jpeg as before. **Related issue (if applicable):** - related issue <link to issue> ## Types of changes <!-- Tick exactly one box. CI (.github/workflows/pr-labels.yaml) derives the label from the ticked box and applies it automatically; the release-notes generator uses that same label to slot this change into the next release notes. --> - [X] Bugfix (non-breaking change which fixes an issue) — `bugfix` - [ ] New feature (non-breaking change which adds functionality) — `new-feature` - [ ] Enhancement to an existing feature — `enhancement` - [ ] New music/player/metadata/plugin provider — `new-provider` - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) — `breaking-change` - [ ] Refactor (no behaviour change) — `refactor` - [ ] Documentation only — `documentation` - [ ] Maintenance / chore — `maintenance` - [ ] CI / workflow change — `ci` - [ ] Dependencies bump — `dependencies` ## Checklist - [X] The code change is tested and works locally. - [X] `pre-commit run --all-files` passes. - [X] `pytest` passes, and tests have been added/updated under `tests/` where applicable. - [ ] For changes to shared models, the companion PR in `music-assistant/models` is linked. - [ ] For changes affecting the UI, the companion PR in `music-assistant/frontend` is linked. - [X] I have read and complied with the project's [AI Policy](https://github.com/music-assistant/.github/blob/main/AI_POLICY.md) for any AI-assisted contributions. - [ ] I have raised a PR against the documentation repository targeting the main or beta branch as appropriate. --------- Co-authored-by: Marcel van der Veldt <m.vanderveldt@outlook.com>
1 parent 16b2a4f commit 9d00620

3 files changed

Lines changed: 30 additions & 7 deletions

File tree

music_assistant/helpers/images.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,8 @@ def _extract_imageproxy_id(url: str) -> str | None:
182182

183183

184184
def player_image_url(mass: MusicAssistant, url: str | None) -> str | None:
185-
"""Rewrite a public-webserver imageproxy URL to the internal streams server.
185+
"""
186+
Rewrite an imageproxy URL for consumption by a (physical) player.
186187
187188
:param mass: The MusicAssistant instance.
188189
:param url: Image URL as produced for frontend/API consumers.
@@ -191,7 +192,14 @@ def player_image_url(mass: MusicAssistant, url: str | None) -> str | None:
191192
return url
192193
webserver_base = mass.webserver.base_url
193194
if webserver_base and url.startswith(f"{webserver_base}/imageproxy"):
194-
return mass.streams.base_url + url[len(webserver_base) :]
195+
# players may not be able to reach the webserver, so serve from the streams
196+
# server, and force jpeg (= flatten transparency) as players such as legacy
197+
# AirPlay receivers cannot be assumed to handle PNG alpha
198+
url = mass.streams.base_url + url[len(webserver_base) :]
199+
parsed = urllib.parse.urlparse(url)
200+
query = urllib.parse.parse_qs(parsed.query)
201+
query["fmt"] = ["jpeg"]
202+
return parsed._replace(query=urllib.parse.urlencode(query, doseq=True)).geturl()
195203
return url
196204

197205

music_assistant/models/player.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1807,7 +1807,7 @@ def __final_current_media(self) -> PlayerMedia | None:
18071807
if active_queue and (current_item := active_queue.current_item):
18081808
item_image_url = (
18091809
# the image format needs to be 512x512 jpeg for maximum compatibility with players
1810-
self.mass.metadata.get_image_url(current_item.image, size=512, image_format="jpeg")
1810+
self.mass.metadata.get_image_url(current_item.image, size=512)
18111811
if current_item.image
18121812
else None
18131813
)
@@ -1839,11 +1839,8 @@ def __final_current_media(self) -> PlayerMedia | None:
18391839
podcast = getattr(media_item, "podcast", None)
18401840
metadata = getattr(media_item, "metadata", None)
18411841
description = getattr(metadata, "description", None) if metadata else None
1842-
# the image format needs to be 512x512 jpeg for maximum player compatibility
18431842
image_url = (
1844-
self.mass.metadata.get_image_url(
1845-
current_item.media_item.image, size=512, image_format="jpeg"
1846-
)
1843+
self.mass.metadata.get_image_url(current_item.media_item.image, size=512)
18471844
or item_image_url
18481845
if current_item.media_item.image
18491846
else item_image_url

tests/core/test_image_proxy.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
create_thumb_hash,
2424
detect_image_content_format,
2525
is_svg_data,
26+
player_image_url,
2627
)
2728
from music_assistant.mass import MusicAssistant
2829

@@ -349,3 +350,20 @@ async def _fake_resolve(*_args: object, **_kwargs: object) -> tuple[bytes, str]:
349350
jpg_resp = await metadata_controller._serve_thumbnail("p", "builtin", 256, "jpeg")
350351
assert "Content-Security-Policy" not in jpg_resp.headers
351352
assert "X-Content-Type-Options" not in jpg_resp.headers
353+
354+
355+
def test_player_image_url_forces_jpeg_on_imageproxy_urls() -> None:
356+
"""Player-bound imageproxy URLs move to the streams server and force fmt=jpeg."""
357+
mass = MagicMock()
358+
mass.webserver.base_url = "http://192.168.1.2:8095"
359+
mass.streams.base_url = "http://192.168.1.2:8097"
360+
image_id = "a" * 64
361+
362+
url = f"http://192.168.1.2:8095/imageproxy/{image_id}?size=512&fmt=png"
363+
result = player_image_url(mass, url)
364+
assert result == f"http://192.168.1.2:8097/imageproxy/{image_id}?size=512&fmt=jpeg"
365+
366+
# non-imageproxy urls (e.g. remote radio artwork) pass through unchanged
367+
remote = "https://example.com/art.png"
368+
assert player_image_url(mass, remote) == remote
369+
assert player_image_url(mass, None) is None

0 commit comments

Comments
 (0)