Exemplo n.º 1
0
def _test_connection(host, port):
    client = MopidyAPI(
        host=host,
        port=port,
        use_websocket=False,
        logger=logging.getLogger(__name__ + ".client"),
    )
    client.rpc_call("core.get_version")
    return True
Exemplo n.º 2
0
def _validate_input(host, port):
    """Validate the user input."""
    client = MopidyAPI(
        host=host,
        port=port,
        use_websocket=False,
        logger=logging.getLogger(__name__ + ".client"),
    )
    client.rpc_call("core.get_version")
    return True
Exemplo n.º 3
0
def _test_connection(host, port):
    client = MopidyAPI(host=host, port=port, use_websocket=False)
    i = client.rpc_call("core.get_version")
    return True
Exemplo n.º 4
0
class MopidyMediaPlayerEntity(MediaPlayerEntity):
    """Representation of the Mopidy server."""
    def __init__(self, hostname, port, name, uuid=None):
        """Initialize the Mopidy device."""
        self.hostname = hostname
        self.port = port
        self.device_name = name
        if uuid is None:
            self.uuid = re.sub("[._-]+", "_", hostname)
        else:
            self.uuid = uuid

        self.server_version = None
        self.player_currenttrack = None
        self.player_streamttile = None
        self.player_currenttrach_source = None

        self._media_position = None
        self._media_position_updated_at = None
        self._state = STATE_UNKNOWN
        self._volume = None
        self._muted = None
        self._media_image_url = None
        self._shuffled = None
        self._repeat_mode = None
        self._playlists = []
        self._currentplaylist = None

        self.client = None
        self._available = None

        self._has_support_volume = None

    def _fetch_status(self):
        """Fetch status from Mopidy."""
        _LOGGER.debug("Fetching Mopidy Server status for %s", self.device_name)
        self.player_currenttrack = self.client.playback.get_current_track()
        self.player_streamttile = self.client.playback.get_stream_title()

        if hasattr(self.player_currenttrack, "uri"):
            self.player_currenttrach_source = self.player_currenttrack.uri.partition(
                ":")[0]
        else:
            self.player_currenttrach_source = None

        media_position = int(self.client.playback.get_time_position() / 1000)
        if media_position != self._media_position:
            self._media_position = media_position
            self._media_position_updated_at = dt_util.utcnow()

        state = self.client.playback.get_state()
        if state is None:
            self._state = STATE_UNAVAILABLE
        elif state == "playing":
            self._state = STATE_PLAYING
        elif state == "paused":
            self._state = STATE_PAUSED
        elif state == "stopped":
            self._state = STATE_OFF
        else:
            self._state = STATE_UNKNOWN

        v = self.client.mixer.get_volume()
        self._volume = None
        self._has_support_volume = False
        if v is not None:
            self._volume = float(v / 100)
            self._has_support_volume = True

        self._muted = self.client.mixer.get_mute()

        if hasattr(self.player_currenttrack, "uri"):
            res = self.client.library.get_images(
                [self.player_currenttrack.uri])
            if (self.player_currenttrack.uri in res
                    and len(res[self.player_currenttrack.uri]) > 0
                    and hasattr(res[self.player_currenttrack.uri][0], "uri")):
                self._media_image_url = res[
                    self.player_currenttrack.uri][0].uri
                if self.player_currenttrach_source == "local":
                    self._media_image_url = (
                        f"http://{self.hostname}:{self.port}{self._media_image_url}"
                    )
        else:
            self._media_image_url = None

        self._shuffled = self.client.tracklist.get_random()
        self._playlists = self.client.playlists.as_list()

        repeat = self.client.tracklist.get_repeat()
        single = self.client.tracklist.get_single()
        if repeat and single:
            self._repeat_mode = REPEAT_MODE_ONE
        elif repeat:
            self._repeat_mode = REPEAT_MODE_ALL
        else:
            self._repeat_mode = REPEAT_MODE_OFF

    @property
    def unique_id(self):
        """Return the unique id for the entity."""
        return self.uuid

    @property
    def name(self) -> str:
        """Return the name of the entity."""
        return self.device_name

    @property
    def icon(self) -> str:
        """Return the icon."""
        return ICON

    @property
    def available(self) -> bool:
        """Return True if entity is available."""
        return self._available

    @property
    def state(self):
        """Return the media state."""
        return self._state

    @property
    def volume_level(self):
        """Volume level of the media player (0..1)."""
        return self._volume

    @property
    def is_volume_muted(self):
        """Boolean if volume is currently muted."""
        return self._muted

    @property
    def media_content_id(self):
        """Return the content ID of current playing media."""
        if hasattr(self.player_currenttrack, "uri"):
            return self.player_currenttrack.uri
        return None

    @property
    def media_content_type(self):
        """Content type of current playing media."""
        return MEDIA_TYPE_MUSIC

    @property
    def media_duration(self):
        """Duration of current playing media in seconds."""
        if hasattr(self.player_currenttrack, "length"):
            return int(self.player_currenttrack.length / 1000)
        return None

    @property
    def media_position(self):
        """Position of current playing media in seconds."""
        return self._media_position

    @property
    def media_position_updated_at(self):
        """Last valid time of media position."""
        return self._media_position_updated_at

    @property
    def media_image_url(self):
        """Image url of current playing media."""
        return self._media_image_url

    @property
    def media_image_remotely_accessible(self):
        """If the image url is remotely accessible."""
        return False

    @property
    def media_title(self):
        """Return the title of current playing media."""
        if self.player_streamttile is not None:
            return self.player_streamttile

        if hasattr(self.player_currenttrack, "name"):
            return self.player_currenttrack.name
        return None

    @property
    def media_artist(self):
        """Artist of current playing media, music track only."""
        if self.player_streamttile is not None:
            if hasattr(self.player_currenttrack, "name"):
                return self.player_currenttrack.name

        if hasattr(self.player_currenttrack, "artists"):
            return ", ".join(
                [a.name for a in self.player_currenttrack.artists])
        return None

    @property
    def media_album_name(self):
        """Album name of current playing media, music track only."""
        if hasattr(self.player_currenttrack, "album") and hasattr(
                self.player_currenttrack.album, "name"):
            return self.player_currenttrack.album.name
        return None

    @property
    def media_album_artist(self):
        """Album artist of current playing media, music track only."""
        if hasattr(self.player_currenttrack, "artists"):
            return ", ".join(
                [a.name for a in self.player_currenttrack.artists])
        return None

    @property
    def media_track(self):
        """Track number of current playing media, music track only."""
        if hasattr(self.player_currenttrack, "track_no"):
            return self.player_currenttrack.track_no
        return None

    @property
    def media_playlist(self):
        """Title of Playlist currently playing."""
        if self._currentplaylist is not None:
            return self._currentplaylist

        if hasattr(self.player_currenttrack, "album") and hasattr(
                self.player_currenttrack.album, "name"):
            return self.player_currenttrack.album.name

        return None

    @property
    def shuffle(self):
        """Boolean if shuffle is enabled."""
        return self._shuffled

    @property
    def repeat(self):
        """Return current repeat mode."""
        return self._repeat_mode

    @property
    def supported_features(self):
        """Flag media player features that are supported."""
        support = SUPPORT_MOPIDY
        if self._has_support_volume:
            support = (support | SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET
                       | SUPPORT_VOLUME_STEP)
        return support

    @property
    def source(self):
        """Name of the current input source."""
        return None

    @property
    def source_list(self):
        """Return the list of available input sources."""
        return [el.name for el in self._playlists]

    def mute_volume(self, mute):
        """Mute the volume."""
        self.client.mixer.set_mute(mute)

    def set_volume_level(self, volume):
        """Set volume level, range 0..1."""
        self.client.mixer.set_volume(int(volume * 100))

    def turn_off(self):
        """Turn the media player off."""
        self.client.playback.stop()

    def turn_on(self):
        """Turn the media player on."""
        self.client.playback.play()

    def media_play(self):
        """Send play command."""
        self.client.playback.play()

    def media_pause(self):
        """Send pause command."""
        self.client.playback.pause()

    def media_stop(self):
        """Send stop command."""
        self.client.playback.stop()

    def media_previous_track(self):
        """Send previous track command."""
        self.client.playback.previous()

    def media_next_track(self):
        """Send next track command."""
        self.client.playback.next()

    def media_seek(self, position):
        """Send seek command."""
        self.client.playback.seek(int(position * 1000))

    def play_media(self, media_type, media_id, **kwargs):
        """Play a piece of media."""
        self._currentplaylist = None

        if media_type == MEDIA_TYPE_PLAYLIST:
            p = self.client.playlists.lookup(media_id)
            self._currentplaylist = p.name
            if media_id.partition(":")[0] == "m3u":
                media_uris = [t.uri for t in p.tracks]
            else:
                media_uris = [media_id]
        else:
            media_uris = [media_id]

        t_uris = []
        schemes = self.client.rpc_call("core.get_uri_schemes")
        for uri in media_uris:
            if "yt" in schemes and (uri.startswith("https://www.youtube.com/")
                                    or uri.startswith("https://youtube.com/")
                                    or uri.startswith("https://youtu.be/")):
                t_uris.append(f"yt:{uri}")
            else:
                t_uris.append(uri)

        media_uris = t_uris

        if len(media_uris) > 0:
            self.client.tracklist.clear()
            self.client.tracklist.add(uris=media_uris)
            self.client.playback.play()

        else:
            _LOGGER.error("No media for %s (%s) could be found.", media_id,
                          media_type)
            raise MissingMediaInformation

    def select_source(self, source):
        """Select input source."""
        for el in self._playlists:
            if el.name == source:
                self.play_media(MEDIA_TYPE_PLAYLIST, el.uri)
                return el.uri
        raise ValueError(f"Could not find {source}")

    def clear_playlist(self):
        """Clear players playlist."""
        self.client.tracklist.clear()

    def set_shuffle(self, shuffle):
        """Enable/disable shuffle mode."""
        self.client.tracklist.set_random(shuffle)

    def set_repeat(self, repeat):
        """Set repeat mode."""
        if repeat == REPEAT_MODE_ALL:
            self.client.tracklist.set_repeat(True)
            self.client.tracklist.set_single(False)
        elif repeat == REPEAT_MODE_ONE:
            self.client.tracklist.set_repeat(True)
            self.client.tracklist.set_single(True)
        else:
            self.client.tracklist.set_repeat(False)
            self.client.tracklist.set_single(False)

    def volume_up(self):
        """Turn volume up for media player."""
        new_volume = self.client.mixer.get_volume() + 5
        if new_volume > 100:
            new_volume = 100

        self.client.mixer.set_volume(new_volume)

    def volume_down(self):
        """Turn volume down for media player."""
        new_volume = self.client.mixer.get_volume() - 5
        if new_volume < 0:
            new_volume = 0

        self.client.mixer.set_volume(new_volume)

    @property
    def device_class(self):
        """Return the device class"""
        return "speaker"

    @property
    def device_info(self):
        """Return device information about this entity."""
        return {
            "indentifiers": {(DOMAIN, self.device_name)},
            "manufacturer": "Mopidy",
            "model": f"Mopidy server {self.server_version}",
            "name": self.device_name,
            "sw_version": self.server_version,
        }

    def _connect(self):
        try:
            self.client = MopidyAPI(host=self.hostname,
                                    port=self.port,
                                    use_websocket=False)
            self.server_version = self.client.rpc_call("core.get_version")
            _LOGGER.debug(
                "Connection to Mopidy server %s (%s:%s) established",
                self.device_name,
                self.hostname,
                self.port,
            )
        except reConnectionError as error:
            _LOGGER.error(
                "Cannot connect to %s @ %s:%s",
                self.device_name,
                self.hostname,
                self.port,
            )
            _LOGGER.error(error)
            self._available = False
            return
        self._available = True

    def update(self):
        """Get the latest data and update the state."""
        if self._available is None:
            self._connect()

        if self._available:
            self._fetch_status()

    async def async_browse_media(self,
                                 media_content_type=None,
                                 media_content_id=None):
        """Implement the websocket media browsing helper."""

        return await self.hass.async_add_executor_job(
            self._media_library_payload,
            dict(
                media_content_type=("library" if media_content_type is None
                                    else media_content_type),
                media_content_id=("library" if media_content_id is None else
                                  media_content_id),
            ),
        )

    def _media_item_image_url(self, source, url):
        """Return the correct url to the item's thumbnail."""

        if source == "local":
            url = f"http://{self.hostname}:{self.port}{url}"

        url = f"{url}?t=x"
        return url

    def _media_library_payload(self, payload):
        """Create response payload to describe contents of a specific library."""
        source = None

        try:
            payload["media_content_type"]
            payload["media_content_id"]
        except KeyError as err:
            _LOGGER.error("Missing type or uri for media item payload: %s",
                          payload)
            raise MissingMediaInformation from err

        library_info = dict(
            media_class=MEDIA_CLASS_DIRECTORY,
            media_content_id="library",
            media_content_type="library",
            title="Media Library",
            can_play=False,
            can_expand=True,
            children=[],
        )
        library_info.update(payload)

        if payload["media_content_id"] == "library":
            library_items = self.client.library.browse(None)
            library_info["title"] = "Media Library"
        else:
            source = payload["media_content_id"].partition(":")[0]
            library_items = self.client.library.browse(
                payload["media_content_id"])
            extra_info = fetch_media_info(payload["media_content_type"],
                                          payload["media_content_id"])
            library_info.update(extra_info)
            image = self.client.library.get_images(
                [payload["media_content_id"]])
            if (payload["media_content_id"] in image
                    and len(image[payload["media_content_id"]]) > 0):
                library_info["thumbnail"] = self._media_item_image_url(
                    source, image[payload["media_content_id"]][0].uri)

        children_uri = []
        for item in library_items:
            library_info["children"].append(
                self._media_item_payload({
                    "name": item.name,
                    "type": item.type,
                    "uri": item.uri
                }))
            children_uri.append(item.uri)

        if source == "spotify":
            # Spotify thumbnail lookup is throttled
            s = 10
        else:
            s = 1000
        uri_set = [
            children_uri[r * s:(r + 1) * s]
            for r in range((len(children_uri) + s - 1) // s)
        ]

        images = dict()
        for s in uri_set:
            images.update(self.client.library.get_images(s))

        if len(images.keys()) > 0:
            for item in library_info["children"]:
                if (item.media_content_id in images
                        and len(images[item.media_content_id]) > 0):
                    item.thumbnail = self._media_item_image_url(
                        source, images[item.media_content_id][0].uri)

        library_info["can_play"] = (library_info["media_content_type"]
                                    in PLAYABLE_MEDIA_TYPES)

        if payload["media_content_id"] in CACHE_ROSETTA:
            library_info["title"] = CACHE_ROSETTA[payload["media_content_id"]]

        r = BrowseMedia(**library_info)
        try:
            r.children_media_class = extra_info["children_media_class"]
        except:
            r.children_media_class = MEDIA_CLASS_DIRECTORY
        return r

    def _media_item_payload(self, item):
        try:
            media_type = item["type"]
            media_id = item["uri"]
        except KeyError as err:
            _LOGGER.error("Missing type or uri for media item: %s", item)
            raise MissingMediaInformation from err

        media_class = MEDIA_CLASS_DIRECTORY
        can_expand = media_type not in [MEDIA_TYPE_TRACK]
        can_play = media_type in PLAYABLE_MEDIA_TYPES

        payload = dict(
            media_class=media_class,
            media_content_id=media_id,
            media_content_type=media_type,
            can_play=can_play,
            can_expand=can_expand,
            title=item.get("name"),
        )
        CACHE_ROSETTA[media_id] = payload["title"]
        extra_info = fetch_media_info(media_type, media_id)
        payload.update(extra_info)
        return BrowseMedia(**payload)
Exemplo n.º 5
0
def _validate_input(host, port):
    """Validate the user input."""
    client = MopidyAPI( host=host, port=port, use_websocket=False )
    t = client.rpc_call("core.get_version")
    return True
Exemplo n.º 6
0
class MopidyMediaPlayerEntity(MediaPlayerEntity):
    """Representation of the Mopidy server."""
    def __init__(self, hostname, port, name, uuid=None):
        """Initialize the Mopidy device."""
        self.hostname = hostname
        self.port = port
        self.device_name = name
        if uuid is None:
            self.uuid = re.sub(r"[._-]+", "_", self.hostname) + "_" + str(
                self.port)
        else:
            self.uuid = uuid

        self.server_version = None
        self.supported_uri_schemes = None
        self.player_currenttrack = None
        self.player_streamttile = None
        self.player_currenttrach_source = None

        self._media_position = None
        self._media_position_updated_at = None
        self._state = STATE_UNKNOWN
        self._volume = None
        self._muted = None
        self._media_image_url = None
        self._shuffled = None
        self._repeat_mode = None
        self._playlists = []
        self._currentplaylist = None
        self._tracklist_tracks = None
        self._tracklist_index = None

        self.client = None
        self._available = None

        self._has_support_volume = None

        self._snapshot = None

        self._reset_variables()

    def _reset_variables(self):
        self.server_version = None
        self.supported_uri_schemes = None
        self.player_currenttrack = None
        self.player_streamttile = None
        self.player_currenttrach_source = None

        self._media_position = None
        self._media_position_updated_at = None
        self._state = STATE_UNKNOWN
        self._volume = None
        self._muted = None
        self._media_image_url = None
        self._shuffled = None
        self._repeat_mode = None
        self._playlists = []
        self._currentplaylist = None
        self._tracklist_tracks = None
        self._tracklist_index = None

        self.client = None
        self._available = None

        self._has_support_volume = None

        self._snapshot = None

    def _fetch_status(self):
        """Fetch status from Mopidy."""
        _LOGGER.debug("Fetching Mopidy Server status for %s", self.device_name)
        try:
            self.player_currenttrack = self.client.playback.get_current_track()
        except reConnectionError:
            self._reset_variables()
            self._state = STATE_UNAVAILABLE
            return

        self.player_streamttile = self.client.playback.get_stream_title()
        self.supported_uri_schemes = self.client.rpc_call(
            "core.get_uri_schemes")

        if hasattr(self.player_currenttrack, "uri"):
            self.player_currenttrach_source = self.player_currenttrack.uri.partition(
                ":")[0]
        else:
            self.player_currenttrach_source = None

        media_position = int(self.client.playback.get_time_position() / 1000)
        if media_position != self._media_position:
            self._media_position = media_position
            self._media_position_updated_at = dt_util.utcnow()

        state = self.client.playback.get_state()
        if state is None:
            self._state = STATE_UNAVAILABLE
        elif state == "playing":
            self._state = STATE_PLAYING
        elif state == "paused":
            self._state = STATE_PAUSED
        elif state == "stopped":
            self._state = STATE_OFF
        else:
            self._state = STATE_UNKNOWN

        volume = self.client.mixer.get_volume()
        self._volume = None
        self._has_support_volume = False
        if volume is not None:
            self._volume = float(volume / 100)
            self._has_support_volume = True

        self._muted = self.client.mixer.get_mute()

        if hasattr(self.player_currenttrack, "uri"):
            res = self.client.library.get_images(
                [self.player_currenttrack.uri])
            if (self.player_currenttrack.uri in res
                    and len(res[self.player_currenttrack.uri]) > 0
                    and hasattr(res[self.player_currenttrack.uri][0], "uri")):
                self._media_image_url = res[
                    self.player_currenttrack.uri][0].uri
                if self.player_currenttrach_source == "local":
                    self._media_image_url = (
                        f"http://{self.hostname}:{self.port}{self._media_image_url}"
                    )
        else:
            self._media_image_url = None

        self._shuffled = self.client.tracklist.get_random()
        self._playlists = self.client.playlists.as_list()

        repeat = self.client.tracklist.get_repeat()
        single = self.client.tracklist.get_single()
        if repeat and single:
            self._repeat_mode = REPEAT_MODE_ONE
        elif repeat:
            self._repeat_mode = REPEAT_MODE_ALL
        else:
            self._repeat_mode = REPEAT_MODE_OFF

        self._tracklist_tracks = [
            t.uri for t in self.client.tracklist.get_tracks()
        ]
        self._tracklist_index = self.client.tracklist.index()

    def search(self, **kwargs):
        """Search the Mopidy Server media library."""
        query = {}
        uris = None
        if isinstance(kwargs.get("keyword"), str):
            query["any"] = [kwargs["keyword"].strip()]

        if isinstance(kwargs.get("keyword_album"), str):
            query["album"] = [kwargs["keyword_album"].strip()]

        if isinstance(kwargs.get("keyword_artist"), str):
            query["artist"] = [kwargs["keyword_artist"].strip()]

        if isinstance(kwargs.get("keyword_genre"), str):
            query["genre"] = [kwargs["keyword_genre"].strip()]

        if isinstance(kwargs.get("keyword_track_name"), str):
            query["track_name"] = [kwargs["keyword_track_name"].strip()]

        if len(query.keys()) == 0:
            return

        if isinstance(kwargs.get("source"), str):
            uris = []
            for source in kwargs["source"].split(","):
                if source.partition(":")[1] == "":
                    source = source + ":"
                if source.partition(":")[0] in self.supported_uri_schemes:
                    uris.append(source)
            if len(uris) == 0:
                uris = None

        search = self.client.library.search(query=query,
                                            uris=uris,
                                            exact=kwargs.get("exact", False))
        track_uris = []
        for result in search:
            for track in getattr(result, "tracks", []):
                track_uris.append(track.uri)

        if len(track_uris) == 0:
            return

        self.client.tracklist.add(uris=track_uris)

    def snapshot(self):
        """Make a snapshot of Mopidy Server."""
        self._snapshot = {
            "mediaposition": self._media_position,
            "muted": self._muted,
            "repeat_mode": self._repeat_mode,
            "shuffled": self._shuffled,
            "state": self._state,
            "tracklist": self._tracklist_tracks,
            "tracklist_index": self._tracklist_index,
            "volume": self._volume,
        }

    def restore(self):
        """Restore Mopidy Server snapshot."""
        if self._snapshot is None:
            return
        self.media_stop()
        self.clear_playlist()
        self.client.tracklist.add(uris=self._snapshot["tracklist"])

        if self._snapshot["state"] == STATE_OFF:
            self.turn_off()
        elif self._snapshot["state"] in [STATE_PLAYING, STATE_PAUSED]:
            self.client.playback.play(tlid=getattr(
                self.client.tracklist.get_tl_tracks()[
                    self._snapshot["tracklist_index"]],
                "tlid",
            ))
            if self._snapshot["mediaposition"] > 0:
                self.media_seek(self._snapshot["mediaposition"])

            if self._snapshot["state"] == STATE_PAUSED:
                self.media_pause()

        self.set_volume_level(self._snapshot["volume"])
        self.mute_volume(self._snapshot["muted"])
        self.set_repeat(self._snapshot["repeat_mode"])
        self.set_shuffle(self._snapshot["shuffled"])

        self._snapshot = None

    @property
    def unique_id(self):
        """Return the unique id for the entity."""
        return self.uuid

    @property
    def name(self) -> str:
        """Return the name of the entity."""
        return self.device_name

    @property
    def icon(self) -> str:
        """Return the icon."""
        return ICON

    @property
    def available(self) -> bool:
        """Return True if entity is available."""
        return self._available

    @property
    def state(self):
        """Return the media state."""
        return self._state

    @property
    def volume_level(self):
        """Volume level of the media player (0..1)."""
        return self._volume

    @property
    def is_volume_muted(self):
        """Boolean if volume is currently muted."""
        return self._muted

    @property
    def media_content_id(self):
        """Return the content ID of current playing media."""
        if hasattr(self.player_currenttrack, "uri"):
            return self.player_currenttrack.uri
        return None

    @property
    def media_content_type(self):
        """Content type of current playing media."""
        return MEDIA_TYPE_MUSIC

    @property
    def media_duration(self):
        """Duration of current playing media in seconds."""
        if hasattr(self.player_currenttrack, "length"):
            return int(self.player_currenttrack.length / 1000)
        return None

    @property
    def media_position(self):
        """Position of current playing media in seconds."""
        return self._media_position

    @property
    def media_position_updated_at(self):
        """Last valid time of media position."""
        return self._media_position_updated_at

    @property
    def media_image_url(self):
        """Image url of current playing media."""
        return self._media_image_url

    @property
    def media_image_remotely_accessible(self):
        """If the image url is remotely accessible."""
        return False

    @property
    def media_title(self):
        """Return the title of current playing media."""
        if self.player_streamttile is not None:
            return self.player_streamttile

        if hasattr(self.player_currenttrack, "name"):
            return self.player_currenttrack.name
        return None

    @property
    def media_artist(self):
        """Artist of current playing media, music track only."""
        if self.player_streamttile is not None:
            if hasattr(self.player_currenttrack, "name"):
                return self.player_currenttrack.name

        if hasattr(self.player_currenttrack, "artists"):
            return ", ".join(
                [a.name for a in self.player_currenttrack.artists])
        return None

    @property
    def media_album_name(self):
        """Album name of current playing media, music track only."""
        if hasattr(self.player_currenttrack, "album") and hasattr(
                self.player_currenttrack.album, "name"):
            return self.player_currenttrack.album.name
        return None

    @property
    def media_album_artist(self):
        """Album artist of current playing media, music track only."""
        if hasattr(self.player_currenttrack, "artists"):
            return ", ".join(
                [a.name for a in self.player_currenttrack.artists])
        return None

    @property
    def media_track(self):
        """Track number of current playing media, music track only."""
        if hasattr(self.player_currenttrack, "track_no"):
            return self.player_currenttrack.track_no
        return None

    @property
    def media_playlist(self):
        """Title of Playlist currently playing."""
        if self._currentplaylist is not None:
            return self._currentplaylist

        if hasattr(self.player_currenttrack, "album") and hasattr(
                self.player_currenttrack.album, "name"):
            return self.player_currenttrack.album.name

        return None

    @property
    def shuffle(self):
        """Boolean if shuffle is enabled."""
        return self._shuffled

    @property
    def repeat(self):
        """Return current repeat mode."""
        return self._repeat_mode

    @property
    def supported_features(self):
        """Flag media player features that are supported."""
        support = SUPPORT_MOPIDY
        if self._has_support_volume:
            support = (support | SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET
                       | SUPPORT_VOLUME_STEP)
        return support

    @property
    def source(self):
        """Name of the current input source."""
        return None

    @property
    def source_list(self):
        """Return the list of available input sources."""
        return sorted([el.name for el in self._playlists])

    def mute_volume(self, mute):
        """Mute the volume."""
        self.client.mixer.set_mute(mute)

    def set_volume_level(self, volume):
        """Set volume level, range 0..1."""
        self.client.mixer.set_volume(int(volume * 100))

    def turn_off(self):
        """Turn the media player off."""
        self.client.playback.stop()

    def turn_on(self):
        """Turn the media player on."""
        self.client.playback.play()

    def media_play(self):
        """Send play command."""
        self.client.playback.play()

    def media_pause(self):
        """Send pause command."""
        self.client.playback.pause()

    def media_stop(self):
        """Send stop command."""
        self.client.playback.stop()

    def media_previous_track(self):
        """Send previous track command."""
        self.client.playback.previous()

    def media_next_track(self):
        """Send next track command."""
        self.client.playback.next()  # pylint: disable=not-callable

    def media_seek(self, position):
        """Send seek command."""
        self.client.playback.seek(int(position * 1000))

    def play_media(self, media_type, media_id, **kwargs):
        """Play a piece of media."""
        self._currentplaylist = None

        if media_type == MEDIA_TYPE_PLAYLIST:
            playlist = self.client.playlists.lookup(media_id)
            self._currentplaylist = playlist.name
            if media_id.partition(":")[0] == "m3u":
                media_uris = [t.uri for t in playlist.tracks]
            else:
                media_uris = [media_id]
        else:
            media_uris = [media_id]

        t_uris = []
        schemes = self.client.rpc_call("core.get_uri_schemes")
        for uri in media_uris:
            if "yt" in schemes and (uri.startswith("https://www.youtube.com/")
                                    or uri.startswith("https://youtube.com/")
                                    or uri.startswith("https://youtu.be/")):
                t_uris.append(f"yt:{uri}")
            else:
                t_uris.append(uri)

        media_uris = t_uris

        if len(media_uris) > 0:
            self.client.tracklist.clear()
            self.client.tracklist.add(uris=media_uris)
            self.client.playback.play()

        else:
            _LOGGER.error("No media for %s (%s) could be found.", media_id,
                          media_type)
            raise MissingMediaInformation

    def select_source(self, source):
        """Select input source."""
        for playlist in self._playlists:
            if playlist.name == source:
                self.play_media(MEDIA_TYPE_PLAYLIST, playlist.uri)
                return playlist.uri
        raise ValueError(f"Could not find {source}")

    def clear_playlist(self):
        """Clear players playlist."""
        self.client.tracklist.clear()

    def set_shuffle(self, shuffle):
        """Enable/disable shuffle mode."""
        self.client.tracklist.set_random(shuffle)

    def set_repeat(self, repeat):
        """Set repeat mode."""
        if repeat == REPEAT_MODE_ALL:
            self.client.tracklist.set_repeat(True)
            self.client.tracklist.set_single(False)
        elif repeat == REPEAT_MODE_ONE:
            self.client.tracklist.set_repeat(True)
            self.client.tracklist.set_single(True)
        else:
            self.client.tracklist.set_repeat(False)
            self.client.tracklist.set_single(False)

    def volume_up(self):
        """Turn volume up for media player."""
        new_volume = self.client.mixer.get_volume() + 5
        if new_volume > 100:
            new_volume = 100

        self.client.mixer.set_volume(new_volume)

    def volume_down(self):
        """Turn volume down for media player."""
        new_volume = self.client.mixer.get_volume() - 5
        if new_volume < 0:
            new_volume = 0

        self.client.mixer.set_volume(new_volume)

    @property
    def device_class(self):
        """Return the device class."""
        return "speaker"

    @property
    def device_info(self):
        """Return device information about this entity."""
        return {
            "indentifiers": {(DOMAIN, self.device_name)},
            "manufacturer": "Mopidy",
            "model": f"Mopidy server {self.server_version}",
            "name": self.device_name,
            "sw_version": self.server_version,
        }

    def _connect(self):
        try:
            self.client = MopidyAPI(
                host=self.hostname,
                port=self.port,
                use_websocket=False,
                logger=logging.getLogger(__name__ + ".client"),
            )
            self.server_version = self.client.rpc_call("core.get_version")
            _LOGGER.debug(
                "Connection to Mopidy server %s (%s:%s) established",
                self.device_name,
                self.hostname,
                self.port,
            )
        except reConnectionError as error:
            _LOGGER.error(
                "Cannot connect to %s @ %s:%s",
                self.device_name,
                self.hostname,
                self.port,
            )
            _LOGGER.error(error)
            self._available = False
            return
        self._available = True

    def update(self):
        """Get the latest data and update the state."""
        if not self._available:
            self._connect()

        if self._available:
            self._fetch_status()

    async def async_browse_media(self,
                                 media_content_type=None,
                                 media_content_id=None):
        """Implement the websocket media browsing helper."""
        return await self.hass.async_add_executor_job(
            self._media_library_payload,
            {
                "media_content_type": ("library" if media_content_type is None
                                       else media_content_type),
                "media_content_id":
                ("library" if media_content_id is None else media_content_id),
            },
        )

    def _media_item_image_url(self, source, url):
        """Return the correct url to the item's thumbnail."""
        if source == "local":
            url = f"http://{self.hostname}:{self.port}{url}"

        url = f"{url}?t=x"
        return url

    def _media_library_payload(self, payload):
        """Create response payload to describe contents of a specific library."""
        _image_uris = []

        if (payload.get("media_content_type") is None
                or payload.get("media_content_id") is None):
            _LOGGER.error("Missing type or uri for media item payload: %s",
                          payload)
            raise MissingMediaInformation

        library_info, mopidy_info = get_media_info(payload)
        if mopidy_info["art_uri"] != "library":
            if mopidy_info["art_uri"] not in CACHE_ART:
                _image_uris.append(mopidy_info["art_uri"])

        library_children = {}
        for path in self.client.library.browse(mopidy_info["browsepath"]):
            library_children[getattr(path, "uri")] = dict(
                zip(
                    ("library_info", "mopidy_info"),
                    get_media_info({
                        "media_content_type":
                        getattr(path, "type", "directory"),
                        "media_content_id":
                        getattr(path, "uri"),
                        "name":
                        getattr(path, "name", "unknown"),
                    }),
                ))
            if (library_children[getattr(path, "uri")]["mopidy_info"]
                    is not None and library_children[getattr(
                        path, "uri")]["mopidy_info"]["art_uri"]
                    not in CACHE_ART):
                _image_uris.append(library_children[getattr(
                    path, "uri")]["mopidy_info"]["art_uri"])

        if mopidy_info["source"] == "spotify":
            # Spotify thumbnail lookup is throttled
            pagesize = 10
        else:
            pagesize = 1000
        uri_sets = [
            _image_uris[r * pagesize:(r + 1) * pagesize]
            for r in range((len(_image_uris) + pagesize - 1) // pagesize)
        ]

        for uri_set in uri_sets:
            if len(uri_set) == 0:
                continue
            i = self.client.library.get_images(uri_set)
            for img_uri in i:
                if len(i[img_uri]) > 0:
                    CACHE_ART[img_uri] = self._media_item_image_url(
                        mopidy_info["source"], i[img_uri][0].uri)
                else:
                    CACHE_ART[img_uri] = None

        if (mopidy_info["art_uri"] in CACHE_ART
                and CACHE_ART[mopidy_info["art_uri"]] is not None):
            library_info["thumbnail"] = CACHE_ART[mopidy_info["art_uri"]]

        for i in library_children:
            if (library_children[i]["mopidy_info"] is not None and
                    library_children[i]["mopidy_info"]["art_uri"] in CACHE_ART
                    and
                    CACHE_ART[library_children[i]["mopidy_info"]["art_uri"]]
                    is not None):
                library_children[i]["library_info"]["thumbnail"] = CACHE_ART[
                    library_children[i]["mopidy_info"]["art_uri"]]

        library_info["children"] = [
            BrowseMedia(**library_children[c]["library_info"])
            for c in library_children
            if library_children[c]["library_info"] is not None
        ]
        return BrowseMedia(**library_info)