mirror of
https://github.com/home-assistant/core.git
synced 2026-08-24 10:13:52 -05:00
Fix stale WiiM artwork when media image URLs are reused (#179461)
Co-authored-by: Tao Jiang <tao.jiang@linkplay.com>
This commit is contained in:
co-authored by
Tao Jiang
parent
cb9ad3aa6e
commit
1e2984c987
@@ -2,6 +2,8 @@
|
||||
|
||||
from collections.abc import Awaitable, Callable, Coroutine
|
||||
from functools import wraps
|
||||
from hashlib import sha256
|
||||
import json
|
||||
from typing import Any, Concatenate, override
|
||||
|
||||
from async_upnp_client.client import UpnpService, UpnpStateVariable
|
||||
@@ -219,12 +221,52 @@ class WiimMediaPlayerEntity(WiimBaseEntity, MediaPlayerEntity):
|
||||
self._attr_media_artist = None
|
||||
self._attr_media_album_name = None
|
||||
self._attr_media_image_url = None
|
||||
self._attr_media_image_hash = None
|
||||
self._attr_media_content_id = None
|
||||
self._attr_media_content_type = None
|
||||
self._attr_media_duration = None
|
||||
self._attr_media_position = None
|
||||
self._attr_media_position_updated_at = None
|
||||
|
||||
@callback
|
||||
def _set_media_image_hash(
|
||||
self,
|
||||
*,
|
||||
image_url: str | None,
|
||||
media_uri: str | None,
|
||||
title: str | None,
|
||||
artist: str | None,
|
||||
album: str | None,
|
||||
) -> None:
|
||||
"""Set a cache-busting media image hash for Home Assistant.
|
||||
|
||||
Some WiiM sources reuse the same artwork URL across tracks, so the
|
||||
default HA URL-based hash is not sufficient to invalidate the image cache.
|
||||
"""
|
||||
if not image_url:
|
||||
self._attr_media_image_hash = None
|
||||
return
|
||||
|
||||
digest_source = json.dumps(
|
||||
[image_url, media_uri, title, artist, album],
|
||||
ensure_ascii=False,
|
||||
separators=(",", ":"),
|
||||
)
|
||||
self._attr_media_image_hash = sha256(
|
||||
digest_source.encode("utf-8"), usedforsecurity=False
|
||||
).hexdigest()
|
||||
|
||||
@override
|
||||
async def async_get_media_image(self) -> tuple[bytes | None, str | None]:
|
||||
"""Fetch the media image using a track-aware cache key."""
|
||||
if (url := self.media_image_url) is None:
|
||||
return None, None
|
||||
|
||||
if (image_hash := self.media_image_hash) is not None:
|
||||
url = f"{url.partition('#')[0]}#{image_hash}"
|
||||
|
||||
return await self._async_fetch_image_from_cache(url)
|
||||
|
||||
@callback
|
||||
def _get_command_target_device(self, action_name: str) -> WiimDevice:
|
||||
"""Return the device that should receive a grouped playback command."""
|
||||
@@ -342,6 +384,13 @@ class WiimMediaPlayerEntity(WiimBaseEntity, MediaPlayerEntity):
|
||||
self._attr_media_artist = media.artist
|
||||
self._attr_media_album_name = media.album
|
||||
self._attr_media_image_url = media.image_url
|
||||
self._set_media_image_hash(
|
||||
image_url=media.image_url,
|
||||
media_uri=media.uri,
|
||||
title=media.title,
|
||||
artist=media.artist,
|
||||
album=media.album,
|
||||
)
|
||||
self._attr_media_content_id = media.uri
|
||||
self._attr_media_content_type = MediaType.MUSIC
|
||||
self._attr_media_duration = media.duration
|
||||
|
||||
@@ -19,6 +19,7 @@ from wiim.models import (
|
||||
from wiim.wiim_device import WiimDevice
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
ATTR_ENTITY_PICTURE_LOCAL,
|
||||
ATTR_GROUP_MEMBERS,
|
||||
ATTR_INPUT_SOURCE,
|
||||
ATTR_MEDIA_ALBUM_NAME,
|
||||
@@ -57,13 +58,20 @@ from homeassistant.components.media_player import (
|
||||
)
|
||||
import homeassistant.components.wiim as wiim_component
|
||||
from homeassistant.components.wiim.const import DOMAIN
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, STATE_UNAVAILABLE
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_ENTITY_PICTURE,
|
||||
CONF_HOST,
|
||||
STATE_UNAVAILABLE,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
|
||||
from . import fire_general_update, fire_transport_update, setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
from tests.typing import ClientSessionGenerator
|
||||
|
||||
MEDIA_PLAYER_ENTITY_ID = "media_player.test_wiim_device"
|
||||
|
||||
@@ -1313,3 +1321,68 @@ async def test_join_service_invalid_member_uses_translation(
|
||||
assert exc_info.value.translation_key == "invalid_grouping_entity"
|
||||
assert exc_info.value.translation_placeholders == {"entity_id": invalid_entity_id}
|
||||
mock_wiim_controller.async_join_group.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_wiim_controller")
|
||||
async def test_media_image_hash_changes_for_same_local_artwork_url(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_wiim_device: MagicMock,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_client: ClientSessionGenerator,
|
||||
) -> None:
|
||||
"""Test the media proxy returns new artwork when its URL is reused."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
image_url = "https://192.168.1.100/changing-album-art.jpg"
|
||||
|
||||
client = await hass_client()
|
||||
|
||||
mock_wiim_device.current_media = WiimMediaMetadata(
|
||||
title="First Song",
|
||||
artist="Artist",
|
||||
album="Album",
|
||||
uri="http://example.com/first.flac",
|
||||
image_url=image_url,
|
||||
)
|
||||
await fire_general_update(hass, mock_wiim_device)
|
||||
state = hass.states.get(MEDIA_PLAYER_ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.attributes[ATTR_ENTITY_PICTURE] == image_url
|
||||
first_local_image = state.attributes[ATTR_ENTITY_PICTURE_LOCAL]
|
||||
|
||||
aioclient_mock.get(
|
||||
image_url,
|
||||
content=b"first-image",
|
||||
headers={"Content-Type": "image/jpeg"},
|
||||
)
|
||||
media_response = await client.get(first_local_image)
|
||||
assert media_response.status == 200
|
||||
first_image = await media_response.read()
|
||||
assert first_image == b"first-image"
|
||||
|
||||
mock_wiim_device.current_media = WiimMediaMetadata(
|
||||
title="Second Song",
|
||||
artist="Artist",
|
||||
album="Album",
|
||||
uri="http://example.com/second.flac",
|
||||
image_url=image_url,
|
||||
)
|
||||
await fire_general_update(hass, mock_wiim_device)
|
||||
state = hass.states.get(MEDIA_PLAYER_ENTITY_ID)
|
||||
assert state is not None
|
||||
|
||||
assert state.attributes[ATTR_ENTITY_PICTURE] == image_url
|
||||
second_local_image = state.attributes[ATTR_ENTITY_PICTURE_LOCAL]
|
||||
assert second_local_image != first_local_image
|
||||
|
||||
aioclient_mock.clear_requests()
|
||||
aioclient_mock.get(
|
||||
image_url,
|
||||
content=b"second-image",
|
||||
headers={"Content-Type": "image/jpeg"},
|
||||
)
|
||||
media_response = await client.get(second_local_image)
|
||||
assert media_response.status == 200
|
||||
second_image = await media_response.read()
|
||||
assert second_image == b"second-image"
|
||||
assert second_image != first_image
|
||||
|
||||
Reference in New Issue
Block a user