class MrpMetadata(Metadata): """Implementation of API for retrieving metadata.""" def __init__(self, protocol, psm, identifier): """Initialize a new MrpPlaying.""" super().__init__(identifier) self.protocol = protocol self.psm = psm self.artwork_cache = Cache(limit=4) async def artwork(self): """Return artwork for what is currently playing (or None).""" identifier = self.artwork_id if not identifier: _LOGGER.debug("No artwork available") return None if identifier in self.artwork_cache: _LOGGER.debug("Retrieved artwork %s from cache", identifier) return self.artwork_cache.get(identifier) artwork = await self._fetch_artwork() if artwork: self.artwork_cache.put(identifier, artwork) return artwork return None async def _fetch_artwork(self): playing = self.psm.playing resp = await self.psm.protocol.send_and_receive( messages.playback_queue_request(playing.location)) if not resp.HasField("type"): return None item = resp.inner().playbackQueue.contentItems[playing.location] return ArtworkInfo(item.artworkData, playing.metadata.artworkMIMEType) @property def artwork_id(self): """Return a unique identifier for current artwork.""" metadata = self.psm.playing.metadata if metadata and metadata.artworkAvailable: if metadata.HasField("artworkIdentifier"): return metadata.artworkIdentifier if metadata.HasField("contentIdentifier"): return metadata.contentIdentifier return None async def playing(self): """Return what is currently playing.""" return MrpPlaying(self.psm.playing) @property def app(self) -> Optional[App]: """Return information about running app.""" state = self.psm.playing if state.client: name = state.client.displayName or None return App(name, state.client.bundleIdentifier) return None
class DmapMetadata(Metadata): """Implementation of API for retrieving metadata from an Apple TV.""" def __init__(self, identifier, apple_tv): """Initialize metadata instance.""" self.identifier = identifier self.apple_tv = apple_tv self.artwork_cache = Cache(limit=4) @property def device_id(self) -> Optional[str]: """Return a unique identifier for current device.""" return self.identifier async def artwork( self, width: Optional[int] = 512, height: Optional[int] = None ) -> Optional[ArtworkInfo]: """Return artwork for what is currently playing (or None). The parameters "width" and "height" makes it possible to request artwork of a specific size. This is just a request, the device might impose restrictions and return artwork of a different size. Set both parameters to None to request default size. Set one of them and let the other one be None to keep original aspect ratio. """ # Having to fetch "playing" here is not ideal, but an identifier is # needed and we cannot trust any previous identifiers. So we have to do # this until a better solution comes along. playing = await self.playing() identifier = playing.hash if identifier in self.artwork_cache: _LOGGER.debug("Retrieved artwork %s from cache", identifier) return self.artwork_cache.get(identifier) _LOGGER.debug("Fetching artwork") artwork = await self.apple_tv.artwork(width, height) if artwork: info = ArtworkInfo(bytes=artwork, mimetype="image/png", width=-1, height=-1) self.artwork_cache.put(identifier, info) return info return None @property def artwork_id(self): """Return a unique identifier for current artwork.""" return self.apple_tv.latest_hash async def playing(self): """Return current device state.""" return await self.apple_tv.playstatus() @property def app(self) -> Optional[App]: """Return information about running app.""" raise exceptions.NotSupportedError()
class DmapMetadata(Metadata): """Implementation of API for retrieving metadata from an Apple TV.""" def __init__(self, identifier, apple_tv): """Initialize metadata instance.""" super().__init__(identifier) self.apple_tv = apple_tv self.artwork_cache = Cache(limit=4) async def artwork(self): """Return artwork for what is currently playing (or None).""" # Having to fetch "playing" here is not ideal, but an identifier is # needed and we cannot trust any previous identifiers. So we have to do # this until a better solution comes along. playing = await self.playing() identifier = playing.hash if identifier in self.artwork_cache: _LOGGER.debug("Retrieved artwork %s from cache", identifier) return self.artwork_cache.get(identifier) artwork = await self._fetch_artwork() if artwork: self.artwork_cache.put(identifier, artwork) return artwork return None async def _fetch_artwork(self): _LOGGER.debug("Fetching artwork") data = await self.apple_tv.artwork() if data: return ArtworkInfo(data, "image/png") return None @property def artwork_id(self): """Return a unique identifier for current artwork.""" return self.apple_tv.latest_hash async def playing(self): """Return current device state.""" return await self.apple_tv.playstatus() @property def app(self) -> Optional[App]: """Return information about running app.""" raise exceptions.NotSupportedError()
class MrpMetadata(Metadata): """Implementation of API for retrieving metadata.""" def __init__(self, protocol, psm, identifier): """Initialize a new MrpPlaying.""" super().__init__(identifier) self.protocol = protocol self.psm = psm self.artwork_cache = Cache(limit=4) async def artwork(self, width=512, height=None) -> Optional[ArtworkInfo]: """Return artwork for what is currently playing (or None). The parameters "width" and "height" makes it possible to request artwork of a specific size. This is just a request, the device might impose restrictions and return artwork of a different size. Set both parameters to None to request default size. Set one of them and let the other one be None to keep original aspect ratio. """ identifier = self.artwork_id if not identifier: _LOGGER.debug("No artwork available") return None if identifier in self.artwork_cache: _LOGGER.debug("Retrieved artwork %s from cache", identifier) return self.artwork_cache.get(identifier) artwork = await self._fetch_artwork(width or 0, height or -1) if artwork: self.artwork_cache.put(identifier, artwork) return artwork return None async def _fetch_artwork(self, width, height): playing = self.psm.playing resp = await self.psm.protocol.send_and_receive( messages.playback_queue_request(playing.location, width, height)) if not resp.HasField("type"): return None item = resp.inner().playbackQueue.contentItems[playing.location] return ArtworkInfo( bytes=item.artworkData, mimetype=playing.metadata.artworkMIMEType, width=item.artworkDataWidth, height=item.artworkDataHeight, ) @property def artwork_id(self): """Return a unique identifier for current artwork.""" metadata = self.psm.playing.metadata if metadata and metadata.artworkAvailable: if metadata.HasField("artworkIdentifier"): return metadata.artworkIdentifier if metadata.HasField("contentIdentifier"): return metadata.contentIdentifier return self.psm.playing.item_identifier return None async def playing(self): """Return what is currently playing.""" return MrpPlaying(copy(self.psm.playing)) @property def app(self) -> Optional[App]: """Return information about running app.""" player_path = self.psm.playing.player_path if player_path and player_path.client: return App(player_path.client.displayName, player_path.client.bundleIdentifier) return None
class CacheTest(unittest.TestCase): def setUp(self): self.cache = Cache(limit=2) def test_cache_is_empty(self): self.assertTrue(self.cache.empty()) def test_put_get_item(self): self.cache.put(ID1, DATA1) self.assertEqual(self.cache.get(ID1), DATA1) def test_put_get_multiple(self): self.cache.put(ID1, DATA1) self.cache.put(ID2, DATA2) self.assertEqual(self.cache.get(ID1), DATA1) self.assertEqual(self.cache.get(ID2), DATA2) def test_cache_not_empty(self): self.cache.put(ID1, DATA1) self.assertFalse(self.cache.empty()) def test_cache_has_item(self): self.cache.put(ID1, DATA1) self.assertTrue(ID1 in self.cache) self.assertFalse(ID2 in self.cache) def test_cache_size(self): self.assertEqual(len(self.cache), 0) self.cache.put(ID1, DATA1) self.assertEqual(len(self.cache), 1) def test_put_same_identifier_replaces_data(self): self.cache.put(ID1, DATA1) self.cache.put(ID1, DATA2) self.assertEqual(self.cache.get(ID1), DATA2) self.assertEqual(len(self.cache), 1) def test_put_removes_oldest(self): self.cache.put(ID1, DATA1) self.cache.put(ID2, DATA2) self.cache.put(ID3, DATA3) self.assertEqual(len(self.cache), 2) self.assertNotIn(ID1, self.cache) self.assertIn(ID2, self.cache) self.assertIn(ID3, self.cache) def test_get_makes_data_newer(self): self.cache.put(ID1, DATA1) self.cache.put(ID2, DATA2) self.cache.get(ID1) self.cache.put(ID3, DATA3) self.assertEqual(len(self.cache), 2) self.assertIn(ID1, self.cache) self.assertNotIn(ID2, self.cache) self.assertIn(ID3, self.cache) def test_get_latest_identifier(self): self.assertEqual(self.cache.latest(), None) self.cache.put(ID1, DATA1) self.assertEqual(self.cache.latest(), ID1) self.cache.put(ID2, DATA2) self.assertEqual(self.cache.latest(), ID2) self.cache.get(ID1) self.assertEqual(self.cache.latest(), ID1)