1 """Support for Apple TV media player."""
3 from __future__
import annotations
5 from datetime
import datetime
9 from pyatv
import exceptions
10 from pyatv.const
import (
14 MediaType
as AppleMediaType,
19 from pyatv.helpers
import is_streamable
20 from pyatv.interface
import (
34 MediaPlayerEntityFeature,
38 async_process_play_media_url,
45 from .
import AppleTvConfigEntry, AppleTVManager
46 from .browse_media
import build_app_list
47 from .entity
import AppleTVEntity
49 _LOGGER = logging.getLogger(__name__)
54 SUPPORT_BASE = MediaPlayerEntityFeature.TURN_ON | MediaPlayerEntityFeature.TURN_OFF
61 | MediaPlayerEntityFeature.BROWSE_MEDIA
62 | MediaPlayerEntityFeature.PLAY_MEDIA
63 | MediaPlayerEntityFeature.PAUSE
64 | MediaPlayerEntityFeature.PLAY
65 | MediaPlayerEntityFeature.SEEK
66 | MediaPlayerEntityFeature.STOP
67 | MediaPlayerEntityFeature.NEXT_TRACK
68 | MediaPlayerEntityFeature.PREVIOUS_TRACK
69 | MediaPlayerEntityFeature.VOLUME_SET
70 | MediaPlayerEntityFeature.VOLUME_STEP
71 | MediaPlayerEntityFeature.REPEAT_SET
72 | MediaPlayerEntityFeature.SHUFFLE_SET
77 SUPPORT_FEATURE_MAPPING = {
78 FeatureName.PlayUrl: MediaPlayerEntityFeature.BROWSE_MEDIA
79 | MediaPlayerEntityFeature.PLAY_MEDIA,
80 FeatureName.StreamFile: MediaPlayerEntityFeature.BROWSE_MEDIA
81 | MediaPlayerEntityFeature.PLAY_MEDIA,
82 FeatureName.Pause: MediaPlayerEntityFeature.PAUSE,
83 FeatureName.Play: MediaPlayerEntityFeature.PLAY,
84 FeatureName.SetPosition: MediaPlayerEntityFeature.SEEK,
85 FeatureName.Stop: MediaPlayerEntityFeature.STOP,
86 FeatureName.Next: MediaPlayerEntityFeature.NEXT_TRACK,
87 FeatureName.Previous: MediaPlayerEntityFeature.PREVIOUS_TRACK,
88 FeatureName.VolumeUp: MediaPlayerEntityFeature.VOLUME_STEP,
89 FeatureName.VolumeDown: MediaPlayerEntityFeature.VOLUME_STEP,
90 FeatureName.SetRepeat: MediaPlayerEntityFeature.REPEAT_SET,
91 FeatureName.SetShuffle: MediaPlayerEntityFeature.SHUFFLE_SET,
92 FeatureName.SetVolume: MediaPlayerEntityFeature.VOLUME_SET,
93 FeatureName.AppList: MediaPlayerEntityFeature.BROWSE_MEDIA
94 | MediaPlayerEntityFeature.SELECT_SOURCE,
95 FeatureName.LaunchApp: MediaPlayerEntityFeature.BROWSE_MEDIA
96 | MediaPlayerEntityFeature.SELECT_SOURCE,
102 config_entry: AppleTvConfigEntry,
103 async_add_entities: AddEntitiesCallback,
105 """Load Apple TV media player based on a config entry."""
106 name: str = config_entry.data[CONF_NAME]
107 assert config_entry.unique_id
is not None
108 manager = config_entry.runtime_data
113 AppleTVEntity, MediaPlayerEntity, PowerListener, AudioListener, PushListener
115 """Representation of an Apple TV media player."""
117 _attr_supported_features = SUPPORT_APPLE_TV
119 def __init__(self, name: str, identifier: str, manager: AppleTVManager) ->
None:
120 """Initialize the Apple TV media player."""
121 super().
__init__(name, identifier, manager)
122 self.
_playing_playing: Playing |
None =
None
123 self.
_app_list_app_list: dict[str, str] = {}
127 """Handle when connection is made to device."""
129 if atv.features.in_state(FeatureState.Available, FeatureName.PushUpdates):
130 atv.push_updater.listener = self
131 atv.push_updater.start()
139 all_features = atv.features.all_features()
140 for feature_name, support_flag
in SUPPORT_FEATURE_MAPPING.items():
141 feature_info = all_features.get(feature_name)
142 if feature_info
and feature_info.state != FeatureState.Unsupported:
149 atv.power.listener = self
152 atv.audio.listener = self
154 if atv.features.in_state(FeatureState.Available, FeatureName.AppList):
155 self.
managermanager.config_entry.async_create_task(
160 _LOGGER.debug(
"Updating app list")
164 apps = await self.
atvatv.apps.app_list()
165 except exceptions.NotSupportedError:
166 _LOGGER.error(
"Listing apps is not supported")
167 except exceptions.ProtocolError:
168 _LOGGER.exception(
"Failed to update app list")
171 app_name: app.identifier
172 for app
in sorted(apps, key=
lambda app: (app.name
or "").lower())
173 if (app_name := app.name)
is not None
179 """Handle when connection was lost to device."""
183 def state(self) -> MediaPlayerState | None:
184 """Return the state of the device."""
185 if self.
managermanager.is_connecting:
187 if self.
atvatv
is None:
188 return MediaPlayerState.OFF
191 and self.
atvatv.power.power_state == PowerState.Off
193 return MediaPlayerState.STANDBY
195 state = self.
_playing_playing.device_state
196 if state
in (DeviceState.Idle, DeviceState.Loading):
197 return MediaPlayerState.IDLE
198 if state == DeviceState.Playing:
199 return MediaPlayerState.PLAYING
200 if state
in (DeviceState.Paused, DeviceState.Seeking, DeviceState.Stopped):
201 return MediaPlayerState.PAUSED
202 return MediaPlayerState.STANDBY
207 """Print what is currently playing when it changes.
209 This is a callback function from pyatv.interface.PushListener.
216 """Inform about an error and restart push updates.
218 This is a callback function from pyatv.interface.PushListener.
220 _LOGGER.warning(
"A %s error occurred: %s", exception.__class__, exception)
226 """Update power state when it changes.
228 This is a callback function from pyatv.interface.PowerListener.
234 """Update volume when it changes.
236 This is a callback function from pyatv.interface.AudioListener.
242 self, old_devices: list[OutputDevice], new_devices: list[OutputDevice]
244 """Output devices were updated.
246 This is a callback function from pyatv.interface.AudioListener.
251 """ID of the current running app."""
255 and (app := self.
atvatv.metadata.app)
is not None
257 return app.identifier
262 """Name of the current running app."""
266 and (app := self.
atvatv.metadata.app)
is not None
273 """List of available input sources."""
278 """Content type of current playing media."""
281 AppleMediaType.Video: MediaType.VIDEO,
282 AppleMediaType.Music: MediaType.MUSIC,
283 AppleMediaType.TV: MediaType.TVSHOW,
289 """Content ID of current playing media."""
291 return self.
_playing_playing.content_identifier
296 """Volume level of the media player (0..1)."""
298 return self.
atvatv.audio.volume / 100.0
303 """Duration of current playing media in seconds."""
305 return self.
_playing_playing.total_time
310 """Position of current playing media in seconds."""
312 return self.
_playing_playing.position
317 """Last valid time of media position."""
319 return dt_util.utcnow()
323 self, media_type: MediaType | str, media_id: str, **kwargs: Any
325 """Send the play_media command to the media player."""
330 if media_type
in {MediaType.APP, MediaType.URL}:
331 await self.
atvatv.apps.launch_app(media_id)
334 if media_source.is_media_source_id(media_id):
335 play_item = await media_source.async_resolve_media(
339 media_type = MediaType.MUSIC
342 media_type == MediaType.MUSIC
or await is_streamable(media_id)
344 _LOGGER.debug(
"Streaming %s via RAOP", media_id)
345 await self.
atvatv.stream.stream_file(media_id)
347 _LOGGER.debug(
"Playing %s via AirPlay", media_id)
348 await self.
atvatv.stream.play_url(media_id)
350 _LOGGER.error(
"Media streaming is not possible with current configuration")
354 """Hash value for media image."""
360 and state
not in {
None, MediaPlayerState.OFF, MediaPlayerState.IDLE}
362 return self.
atvatv.metadata.artwork_id
366 """Fetch media image of current playing image."""
371 and state
not in {MediaPlayerState.OFF, MediaPlayerState.IDLE}
373 artwork = await self.
atvatv.metadata.artwork()
375 return artwork.bytes, artwork.mimetype
381 """Title of current playing media."""
388 """Artist of current playing media, music track only."""
395 """Album name of current playing media, music track only."""
402 """Title of series of current playing media, TV show only."""
404 return self.
_playing_playing.series_name
409 """Season of current playing media, TV show only."""
416 """Episode of current playing media, TV show only."""
423 """Return current repeat mode."""
427 and (repeat := self.
_playing_playing.repeat)
430 RepeatState.Track: RepeatMode.ONE,
431 RepeatState.All: RepeatMode.ALL,
432 }.
get(repeat, RepeatMode.OFF)
437 """Boolean if shuffle is enabled."""
439 return self.
_playing_playing.shuffle != ShuffleState.Off
443 """Return if a feature is available."""
445 return self.
atvatv.features.in_state(FeatureState.Available, feature)
450 media_content_type: MediaType | str |
None =
None,
451 media_content_id: str |
None =
None,
453 """Implement the websocket media browsing helper."""
454 if media_content_id ==
"apps" or (
467 "content_filter":
lambda item: item.media_content_type.startswith(
472 cur_item = await media_source.async_browse_media(
473 self.
hasshass, media_content_id, **kwargs
477 if media_content_id
is not None:
481 if self.
_app_list_app_list
and cur_item.children
and isinstance(cur_item.children, list):
487 """Turn the media player on."""
489 await self.
atvatv.power.turn_on()
492 """Turn the media player off."""
498 or self.
atvatv.power.power_state == PowerState.On
501 await self.
atvatv.power.turn_off()
504 """Pause media on media player."""
506 await self.
atvatv.remote_control.play_pause()
511 await self.
atvatv.remote_control.play()
514 """Stop the media player."""
516 await self.
atvatv.remote_control.stop()
519 """Pause the media player."""
521 await self.
atvatv.remote_control.pause()
524 """Send next track command."""
526 await self.
atvatv.remote_control.next()
529 """Send previous track command."""
531 await self.
atvatv.remote_control.previous()
534 """Send seek command."""
536 await self.
atvatv.remote_control.set_position(round(position))
539 """Turn volume up for media player."""
541 await self.
atvatv.audio.volume_up()
544 """Turn volume down for media player."""
546 await self.
atvatv.audio.volume_down()
549 """Set volume level, range 0..1."""
552 await self.
atvatv.audio.set_volume(volume * 100.0)
555 """Set repeat mode."""
558 RepeatMode.ONE: RepeatState.Track,
559 RepeatMode.ALL: RepeatState.All,
560 }.
get(repeat, RepeatState.Off)
561 await self.
atvatv.remote_control.set_repeat(mode)
564 """Enable/disable shuffle mode."""
566 await self.
atvatv.remote_control.set_shuffle(
567 ShuffleState.Songs
if shuffle
else ShuffleState.Off
571 """Select input source."""
574 await self.
atvatv.apps.launch_app(app_id)
None async_write_ha_state(self)
web.Response get(self, web.Request request, str config_key)