import logging
import math
import os
from datetime import datetime
from time import sleep
from typing import Any, Dict, List, Optional, Union
from urllib.parse import urlencode

import requests
from deprecated import deprecated

from .api_objects import (
    AlbumInfo,
    AlbumList,
    AlbumList2,
    AlbumWithSongsID3,
    ArtistInfo,
    ArtistInfo2,
    ArtistsID3,
    ArtistWithAlbumsID3,
    Bookmarks,
    Child,
    Directory,
    Error,
    Genres,
    Indexes,
    InternetRadioStations,
    License,
    Lyrics,
    MusicFolders,
    NowPlaying,
    Playlists,
    PlaylistWithSongs,
    PlayQueue,
    Response,
    ScanStatus,
    SearchResult,
    SearchResult2,
    SearchResult3,
    Shares,
    Songs,
    Starred,
    Starred2,
    User,
    Users,
    VideoInfo,
)


class Server:
    """
    Defines a \\*Sonic server.

    Notes:

    * The ``hls`` endpoint is not supported.
    * The ``getCaptions`` endpoint is not supported
    * None of the podcast endpoints are supported.
    * The ``jukeboxControl`` endpoint is not supported.
    * None of the chat message endpoints are supported.
    * The ``server`` module is stateless. The only thing that it does is allow
      the module's user to query the \\*sonic server via the API.
    """

    class SubsonicServerError(Exception):
        def __init__(self: "Server.SubsonicServerError", error: Error):
            super().__init__(f"{error.code}: {error.message}")

    def __init__(
        self,
        name: str,
        hostname: str,
        username: str,
        password: str,
        disable_cert_verify: bool,
    ):
        self.name: str = name
        self.hostname: str = hostname
        self.username: str = username
        self.password: str = password
        self.disable_cert_verify: bool = disable_cert_verify

    def _get_params(self) -> Dict[str, str]:
        """See Subsonic API Introduction for details."""
        return {
            "u": self.username,
            "p": self.password,
            "c": "Sublime Music",
            "f": "json",
            "v": "1.15.0",
        }

    def _make_url(self, endpoint: str) -> str:
        return f"{self.hostname}/rest/{endpoint}.view"

    # def _get(self, url, timeout=(3.05, 2), **params):
    def _get(self, url: str, **params) -> Any:
        params = {**self._get_params(), **params}
        logging.info(f"[START] get: {url}")

        if os.environ.get("SUBLIME_MUSIC_DEBUG_DELAY"):
            logging.info(
                "SUBLIME_MUSIC_DEBUG_DELAY enabled. Pausing for "
                f"{os.environ['SUBLIME_MUSIC_DEBUG_DELAY']} seconds."
            )
            sleep(float(os.environ["SUBLIME_MUSIC_DEBUG_DELAY"]))

        # Deal with datetime parameters (convert to milliseconds since 1970)
        for k, v in params.items():
            if type(v) == datetime:
                params[k] = int(v.timestamp() * 1000)

        result = requests.get(
            url,
            params=params,
            verify=not self.disable_cert_verify,
            # timeout=timeout,
        )
        # TODO (#122): make better
        if result.status_code != 200:
            raise Exception(f"[FAIL] get: {url} status={result.status_code}")

        logging.info(f"[FINISH] get: {url}")
        return result

    def _get_json(
        self, url: str, **params: Union[None, str, datetime, int, List[int]],
    ) -> Response:
        """
        Make a get request to a *Sonic REST API. Handle all types of errors
        including *Sonic ``<error>`` responses.

        :returns: a Response containing all of the data of the
            response, deserialized
        :raises Exception: needs some work TODO
        """
        result = self._get(url, **params)
        subsonic_response = result.json().get("subsonic-response")

        # TODO (#122):  make better
        if not subsonic_response:
            raise Exception(f"[FAIL] get: invalid JSON from {url}")

        if subsonic_response["status"] == "failed":
            code, message = (
                subsonic_response["error"].get("code"),
                subsonic_response["error"].get("message"),
            )
            raise Exception(f"Subsonic API Error #{code}: {message}")

        response = Response.from_json(subsonic_response)

        # Check for an error and if it exists, raise it.
        if response.error:
            raise Server.SubsonicServerError(response.error)

        return response

    def do_download(self, url: str, **params) -> bytes:
        download = self._get(url, **params)
        if not download:
            raise Exception("Download failed")
        if "json" in download.headers.get("Content-Type"):
            # TODO (#122): make better
            raise Exception("Didn't expect JSON.")
        return download.content

    def ping(self) -> Response:
        """
        Used to test connectivity with the server.
        """
        return self._get_json(self._make_url("ping"))

    def get_license(self) -> License:
        """Get details about the software license."""
        return self._get_json(self._make_url("getLicense")).license

    def get_music_folders(self) -> MusicFolders:
        """Returns all configured top-level music folders."""
        return self._get_json(self._make_url("getMusicFolders")).musicFolders

    def get_indexes(
        self, music_folder_id: int = None, if_modified_since: int = None,
    ) -> Indexes:
        """
        Returns an indexed structure of all artists.

        :param music_folder_id: If specified, only return artists in the music
            folder with the given ID. See ``getMusicFolders``.
        :param if_modified_since: If specified, only return a result if the
            artist collection has changed since the given time.
        """
        result = self._get_json(
            self._make_url("getIndexes"),
            musicFolderId=music_folder_id,
            ifModifiedSince=if_modified_since,
        )
        return result.indexes

    def get_music_directory(self, dir_id: Union[int, str]) -> Directory:
        """
        Returns a listing of all files in a music directory. Typically used
        to get list of albums for an artist, or list of songs for an album.

        :param dir_id: A string which uniquely identifies the music folder.
            Obtained by calls to ``getIndexes`` or ``getMusicDirectory``.
        """
        result = self._get_json(self._make_url("getMusicDirectory"), id=str(dir_id),)
        return result.directory

    def get_genres(self) -> Genres:
        """Returns all genres."""
        return self._get_json(self._make_url("getGenres")).genres

    def get_artists(self, music_folder_id: int = None) -> ArtistsID3:
        """
        Similar to getIndexes, but organizes music according to ID3 tags.

        :param music_folder_id: If specified, only return artists in the music
            folder with the given ID. See ``getMusicFolders``.
        """
        result = self._get_json(
            self._make_url("getArtists"), musicFolderId=music_folder_id,
        )
        return result.artists

    def get_artist(self, artist_id: int) -> ArtistWithAlbumsID3:
        """
        Returns details for an artist, including a list of albums. This method
        organizes music according to ID3 tags.

        :param artist_id: The artist ID.
        """
        return self._get_json(self._make_url("getArtist"), id=artist_id).artist

    def get_album(self, album_id: int) -> AlbumWithSongsID3:
        """
        Returns details for an album, including a list of songs. This method
        organizes music according to ID3 tags.

        :param album_id: The album ID.
        """
        return self._get_json(self._make_url("getAlbum"), id=album_id).album

    def get_song(self, song_id: int) -> Child:
        """
        Returns details for a song.

        :param song_id: The song ID.
        """
        return self._get_json(self._make_url("getSong"), id=song_id).song

    def get_videos(self) -> Optional[List[Child]]:
        """
        Returns all video files.
        """
        return self._get_json(self._make_url("getVideos")).videos.video

    def get_video_info(self, video_id: int) -> Optional[VideoInfo]:
        """
        Returns details for a video, including information about available
        audio tracks, subtitles (captions) and conversions.

        :param video_id: The video ID.
        """
        result = self._get_json(self._make_url("getVideoInfo"), id=video_id)
        return result.videoInfo

    def get_artist_info(
        self, id: int, count: int = None, include_not_present: bool = None,
    ) -> Optional[ArtistInfo]:
        """
        Returns artist info with biography, image URLs and similar artists,
        using data from last.fm.

        :param id: The artist, album, or song ID.
        :param count: Max number of similar artists to return. Defaults to 20,
            according to API Spec.
        :param include_not_present: Whether to return artists that are not
            present in the media library. Defaults to false according to API
            Spec.
        """
        result = self._get_json(
            self._make_url("getArtistInfo"),
            id=id,
            count=count,
            includeNotPresent=include_not_present,
        )
        return result.artistInfo

    def get_artist_info2(
        self, id: int, count: int = None, include_not_present: bool = None,
    ) -> Optional[ArtistInfo2]:
        """
        Similar to getArtistInfo, but organizes music according to ID3 tags.

        :param id: The artist, album, or song ID.
        :param count: Max number of similar artists to return. Defaults to 20,
            according to API Spec.
        :param include_not_present: Whether to return artists that are not
            present in the media library. Defaults to false according to API
            Spec.
        """
        result = self._get_json(
            self._make_url("getArtistInfo2"),
            id=id,
            count=count,
            includeNotPresent=include_not_present,
        )
        return result.artistInfo2

    def get_album_info(self, id: int) -> Optional[AlbumInfo]:
        """
        Returns album notes, image URLs etc, using data from last.fm.

        :param id: The album or song ID.
        """
        result = self._get_json(self._make_url("getAlbumInfo"), id=id)
        return result.albumInfo

    def get_album_info2(self, id: int) -> Optional[AlbumInfo]:
        """
        Similar to getAlbumInfo, but organizes music according to ID3 tags.

        :param id: The album or song ID.
        """
        result = self._get_json(self._make_url("getAlbumInfo2"), id=id)
        return result.albumInfo

    def get_similar_songs(self, id: int, count: int = None) -> List[Child]:
        """
        Returns a random collection of songs from the given artist and similar
        artists, using data from last.fm. Typically used for artist radio
        features.

        :param id: The artist, album or song ID.
        :param count: Max number of songs to return. Defaults to 50 according
            to API Spec.
        """
        result = self._get_json(self._make_url("getSimilarSongs"), id=id, count=count,)
        return result.similarSongs.song

    def get_similar_songs2(self, id: int, count: int = None) -> List[Child]:
        """
        Similar to getSimilarSongs, but organizes music according to ID3 tags.

        :param id: The artist, album or song ID.
        :param count: Max number of songs to return. Defaults to 50 according
            to API Spec.
        """
        result = self._get_json(self._make_url("getSimilarSongs2"), id=id, count=count,)
        return result.similarSongs2.song

    def get_top_songs(self, artist: str, count: int = None) -> List[Child]:
        """
        Returns top songs for the given artist, using data from last.fm.

        :param id: The artist name.
        :param count: Max number of songs to return. Defaults to 50 according
            to API Spec.
        """
        result = self._get_json(
            self._make_url("getTopSongs"), artist=artist, count=count,
        )
        return result.topSongs.song

    def get_album_list(
        self,
        type: str,
        size: int = None,
        offset: int = None,
        from_year: int = None,
        to_year: int = None,
        genre: str = None,
        music_folder_id: int = None,
    ) -> AlbumList:
        """
        Returns a list of random, newest, highest rated etc. albums. Similar to
        the album lists on the home page of the Subsonic web interface.

        :param type: The list type. Must be one of the following: ``random``,
            ``newest``, ``highest``, ``frequent``, ``recent``. Since 1.8.0 you
            can also use ``alphabeticalByName`` or ``alphabeticalByArtist`` to
            page through all albums alphabetically, and ``starred`` to retrieve
            starred albums.  Since 1.10.1 you can use ``byYear`` and
            ``byGenre`` to list albums in a given year range or genre.
        :param size: The number of albums to return. Max 500. Deafult is 10
            according to API Spec.
        :param offset: The list offset. Useful if you for example want to page
            through the list of newest albums. Default is 0 according to API
            Spec.
        :param from_year: Required if ``type`` is ``byYear``. The first year in
            the range. If ``fromYear > toYear`` a reverse chronological list is
            returned.
        :param to_year: Required if ``type`` is ``byYear``. The last year in
            the range.
        :param genre: Required if ``type`` is ``byGenre``. The name of the
            genre, e.g., "Rock".
        :param music_folder_id: (Since 1.11.0) Only return albums in the music
            folder with the given ID. See ``getMusicFolders``.
        """
        result = self._get_json(
            self._make_url("getAlbumList"),
            type=type,
            size=size,
            offset=offset,
            fromYear=from_year,
            toYear=to_year,
            genre=genre,
            musicFolderId=music_folder_id,
        )
        return result.albumList

    def get_album_list2(
        self,
        type: str,
        size: int = None,
        offset: int = None,
        from_year: int = None,
        to_year: int = None,
        genre: str = None,
        music_folder_id: int = None,
    ) -> AlbumList2:
        """
        Similar to getAlbumList, but organizes music according to ID3 tags.

        :param type: The list type. Must be one of the following: ``random``,
            ``newest``, ``frequent``, ``recent``, ``starred``,
            ``alphabeticalByName`` or ``alphabeticalByArtist``. Since 1.10.1
            you can use ``byYear`` and ``byGenre`` to list albums in a given
            year range or genre.
        :param size: The number of albums to return. Max 500. Deafult is 10
            according to API Spec.
        :param offset: The list offset. Useful if you for example want to page
            through the list of newest albums. Default is 0 according to API
            Spec.
        :param from_year: Required if ``type`` is ``byYear``. The first year in
            the range. If ``fromYear > toYear`` a reverse chronological list is
            returned.
        :param to_year: Required if ``type`` is ``byYear``. The last year in
            the range.
        :param genre: Required if ``type`` is ``byGenre``. The name of the
            genre, e.g., "Rock".
        :param music_folder_id: (Since 1.11.0) Only return albums in the music
            folder with the given ID. See ``getMusicFolders``.
        """
        result = self._get_json(
            self._make_url("getAlbumList2"),
            type=type,
            size=size,
            offset=offset,
            fromYear=from_year,
            toYear=to_year,
            genre=genre,
            musicFolderId=music_folder_id,
        )
        return result.albumList2

    def get_random_songs(
        self,
        size: int = None,
        genre: str = None,
        from_year: str = None,
        to_year: str = None,
        music_folder_id: int = None,
    ) -> Songs:
        """
        Returns random songs matching the given criteria.

        :param size: The maximum number of songs to return. Max 500. Defaults
            to 10 according to API Spec.
        :param genre: Only returns songs belonging to this genre.
        :param from_year: Only return songs published after or in this year.
        :param to_year: Only return songs published before or in this year.
        :param music_folder_id: Only return albums in the music folder with the
            given ID. See ``getMusicFolders``.
        """
        result = self._get_json(
            self._make_url("getRandomSongs"),
            size=size,
            genre=genre,
            fromYear=from_year,
            toYear=to_year,
            musicFolderId=music_folder_id,
        )
        return result.randomSongs

    def get_songs_by_genre(
        self,
        genre: str,
        count: int = None,
        offset: int = None,
        music_folder_id: int = None,
    ) -> Songs:
        """
        Returns songs in a given genre.

        :param genre: Only returns songs belonging to this genre.
        :param count: The maximum number of songs to return. Max 500. Defaults
            to 10 according to API Spec.
        :param offset: The offset. Useful if you want to page through the songs
            in a genre.
        :param music_folder_id: (Since 1.12.0) Only return albums in the music
            folder with the given ID. See ``getMusicFolders``.
        """
        result = self._get_json(
            self._make_url("getSongsByGenre"),
            genre=genre,
            count=count,
            offset=offset,
            musicFolderId=music_folder_id,
        )
        return result.songsByGenre

    def get_now_playing(self) -> NowPlaying:
        """
        Returns what is currently being played by all users. Takes no extra
        parameters.
        """
        return self._get_json(self._make_url("getNowPlaying")).nowPlaying

    def get_starred(self, music_folder_id: int = None) -> Starred:
        """
        Returns starred songs, albums and artists.

        :param music_folder_id: (Since 1.12.0) Only return results from the
            music folder with the given ID. See ``getMusicFolders``.
        """
        result = self._get_json(
            self._make_url("getStarred"), musicFolderId=music_folder_id,
        )
        return result.starred

    def get_starred2(self, music_folder_id: int = None) -> Starred2:
        """
        Similar to getStarred, but organizes music according to ID3 tags.

        :param music_folder_id: (Since 1.12.0) Only return results from the
            music folder with the given ID. See ``getMusicFolders``.
        """
        result = self._get_json(
            self._make_url("getStarred2"), musicFolderId=music_folder_id,
        )
        return result.starred2

    @deprecated(version="1.4.0", reason="You should use search2 instead.")
    def search(
        self,
        artist: str = None,
        album: str = None,
        title: str = None,
        any: str = None,
        count: int = None,
        offset: int = None,
        newer_than: datetime = None,
    ) -> SearchResult:
        """
        Returns a listing of files matching the given search criteria. Supports
        paging through the result.

        :param artist: Artist to search for.
        :param album: Album to searh for.
        :param title: Song title to search for.
        :param any: Searches all fields.
        :param count: Maximum number of results to return.
        :param offset: Search result offset. Used for paging.
        :param newer_than: Only return matches that are newer than this.
        """
        result = self._get_json(
            self._make_url("search"),
            artist=artist,
            album=album,
            title=title,
            any=any,
            count=count,
            offset=offset,
            newerThan=math.floor(newer_than.timestamp() * 1000) if newer_than else None,
        )
        return result.searchResult

    def search2(
        self,
        query: str,
        artist_count: int = None,
        artist_offset: int = None,
        album_count: int = None,
        album_offset: int = None,
        song_count: int = None,
        song_offset: int = None,
        music_folder_id: int = None,
    ) -> SearchResult2:
        """
        Returns albums, artists and songs matching the given search criteria.
        Supports paging through the result.

        :param query: Search query.
        :param artist_count: Maximum number of artists to return. Defaults to
            20 according to API Spec.
        :param artist_offset: Search result offset for artists. Used for
            paging. Defualts to 0 according to API Spec.
        :param album_count: Maximum number of albums to return. Defaults to 20
            according to API Spec.
        :param album_offset: Search result offset for albums. Used for paging.
            Defualts to 0 according to API Spec.
        :param song_count: Maximum number of songs to return. Defaults to 20
            according to API Spec.
        :param song_offset: Search result offset for songs. Used for paging.
            Defualts to 0 according to API Spec.
        :param music_folder_id: (Since 1.12.0) Only return results from the
            music folder with the given ID. See ``getMusicFolders``.
        """
        result = self._get_json(
            self._make_url("search2"),
            query=query,
            artistCount=artist_count,
            artistOffset=artist_offset,
            albumCount=album_count,
            albumOffset=album_offset,
            songCount=song_count,
            songOffset=song_offset,
            musicFolderId=music_folder_id,
        )
        return result.searchResult2

    def search3(
        self,
        query: str,
        artist_count: int = None,
        artist_offset: int = None,
        album_count: int = None,
        album_offset: int = None,
        song_count: int = None,
        song_offset: int = None,
        music_folder_id: int = None,
    ) -> SearchResult3:
        """
        Similar to search2, but organizes music according to ID3 tags.

        :param query: Search query.
        :param artist_count: Maximum number of artists to return. Defaults to
            20 according to API Spec.
        :param artist_offset: Search result offset for artists. Used for
            paging. Defualts to 0 according to API Spec.
        :param album_count: Maximum number of albums to return. Defaults to 20
            according to API Spec.
        :param album_offset: Search result offset for albums. Used for paging.
            Defualts to 0 according to API Spec.
        :param song_count: Maximum number of songs to return. Defaults to 20
            according to API Spec.
        :param song_offset: Search result offset for songs. Used for paging.
            Defualts to 0 according to API Spec.
        :param music_folder_id: (Since 1.12.0) Only return results from the
            music folder with the given ID. See ``getMusicFolders``.
        """
        result = self._get_json(
            self._make_url("search3"),
            query=query,
            artistCount=artist_count,
            artistOffset=artist_offset,
            albumCount=album_count,
            albumOffset=album_offset,
            songCount=song_count,
            songOffset=song_offset,
            musicFolderId=music_folder_id,
        )
        return result.searchResult3

    def get_playlists(self, username: str = None) -> Playlists:
        """
        Returns all playlists a user is allowed to play.

        :param username: (Since 1.8.0) If specified, return playlists for this
            user rather than for the authenticated user. The authenticated user
            must have admin role if this parameter is used.
        """
        result = self._get_json(self._make_url("getPlaylists"), username=username,)
        return result.playlists

    def get_playlist(self, id: int) -> PlaylistWithSongs:
        """
        Returns a listing of files in a saved playlist.

        :param username: ID of the playlist to return, as obtained by
            ``getPlaylists``.
        """
        result = self._get_json(self._make_url("getPlaylist"), id=id)
        return result.playlist

    def create_playlist(
        self,
        playlist_id: int = None,
        name: str = None,
        song_id: Union[int, List[int]] = None,
    ) -> Union[PlaylistWithSongs, Response]:
        """
        Creates (or updates) a playlist.

        :param playlist_id: The playlist ID. Required if updating.
        :param name: The human-readable name of the playlist. Required if
            creating.
        :param song_id: ID(s) of a song in the playlist. Can be a single ID or
            a list of IDs.
        """
        result = self._get_json(
            self._make_url("createPlaylist"),
            playlistId=playlist_id,
            name=name,
            songId=song_id,
        )

        return result.playlist or result

    def update_playlist(
        self,
        playlist_id: int,
        name: str = None,
        comment: str = None,
        public: bool = None,
        song_id_to_add: Union[int, List[int]] = None,
        song_index_to_remove: Union[int, List[int]] = None,
    ) -> Response:
        """
        Updates a playlist. Only the owner of a playlist is allowed to update
        it.

        :param playlist_id: The playlist ID. Required if updating.
        :param name: The human-readable name of the playlist.
        :param comment: The playlist comment.
        :param public: ``true`` if the playlist should be visible to all users,
            ``false`` otherwise.
        :param song_id_to_add: Add this song with this/these ID(s) to the
            playlist. Can be a single ID or a list of IDs.
        :param song_id_to_remove: Remove the song at this/these position(s) in
            the playlist. Can be a single ID or a list of IDs.
        """
        return self._get_json(
            self._make_url("updatePlaylist"),
            playlistId=playlist_id,
            name=name,
            comment=comment,
            public=public,
            songIdToAdd=song_id_to_add,
            songIndexToRemove=song_index_to_remove,
        )

    def delete_playlist(self, id: int) -> Response:
        """Deletes a saved playlist."""
        return self._get_json(self._make_url("deletePlaylist"), id=id)

    def get_stream_url(
        self,
        id: str,
        max_bit_rate: int = None,
        format: str = None,
        time_offset: int = None,
        size: int = None,
        estimate_content_length: bool = False,
        converted: bool = False,
    ) -> str:
        """
        Gets the URL to stream a given file.

        :param id: A string which uniquely identifies the file to stream.
            Obtained by calls to ``getMusicDirectory``.
        :param maxBitRate: (Since 1.2.0) If specified, the server will attempt
            to limit the bitrate to this value, in kilobits per second. If set
            to zero, no limit is imposed.
        :param format: (Since 1.6.0) Specifies the preferred target format
            (e.g., "mp3" or "flv") in case there are multiple applicable
            transcodings. Starting with 1.9.0 you can use the special value
            "raw" to disable transcoding.
        :param timeOffset: Only applicable to video streaming. If specified,
            start streaming at the given offset (in seconds) into the video.
            Typically used to implement video skipping.
        :param size: (Since 1.6.0) Only applicable to video streaming.
            Requested video size specified as WxH, for instance "640x480".
        :param estimateContentLength: (Since 1.8.0). If set to ``True``, the
            *Content-Length* HTTP header will be set to an estimated value for
            transcoded or downsampled media. Defaults to False according to the
            API Spec.
        :param converted: (Since 1.14.0) Only applicable to video streaming.
            Subsonic can optimize videos for streaming by converting them to
            MP4. If a conversion exists for the video in question, then setting
            this parameter to ``True`` will cause the converted video to be
            returned instead of the original. Defaults to False according to
            the API Spec.
        """
        params = dict(
            **self._get_params(),
            id=id,
            maxBitRate=max_bit_rate,
            format=format,
            timeOffset=time_offset,
            size=size,
            estimateContentLength=estimate_content_length,
            converted=converted,
        )
        params = {k: v for k, v in params.items() if v}
        return self._make_url("stream") + "?" + urlencode(params)

    def download(self, id: str) -> bytes:
        """
        Downloads a given media file. Similar to stream, but this method
        returns the original media data without transcoding or downsampling.

        :param id: A string which uniquely identifies the file to stream.
            Obtained by calls to ``getMusicDirectory``.
        """
        return self.do_download(self._make_url("download"), id=id)

    def get_cover_art(self, id: str, size: int = 1000) -> bytes:
        """
        Returns the cover art image in binary form.

        :param id: The ID of a song, album or artist.
        :param size: If specified, scale image to this size.
        """
        return self.do_download(self._make_url("getCoverArt"), id=id, size=size,)

    def get_cover_art_url(self, id: str, size: int = 1000) -> str:
        """
        Returns the URL of the cover art image.

        :param id: The ID of a song, album or artist.
        :param size: If specified, scale image to this size.
        """
        params = dict(**self._get_params(), id=id, size=size)
        params = {k: v for k, v in params.items() if v}
        return self._make_url("getCoverArt") + "?" + urlencode(params)

    def get_lyrics(self, artist: str = None, title: str = None) -> Lyrics:
        """
        Searches for and returns lyrics for a given song.

        :param artist: The artist name.
        :param title: The song title.
        """
        result = self._get_json(
            self._make_url("getLyrics"), artist=artist, title=title,
        )
        return result.lyrics

    def get_avatar(self, username: str) -> bytes:
        """
        Returns the avatar (personal image) for a user.

        :param username: the user in question.
        """
        return self.do_download(self._make_url("getAvatar"), username=username)

    def star(
        self,
        id: Union[int, List[int]] = None,
        album_id: Union[int, List[int]] = None,
        artist_id: Union[int, List[int]] = None,
    ) -> Response:
        """
        Attaches a star to a song, album or artist.

        :param id: The ID(s) of the file(s) (song(s)) or folder(s)
            (album(s)/artist(s)) to star. Can be a single ID or a list of IDs.
        :param album_id: The ID(s) of an album/albums to star. Use this rather
            than ``id`` if the client accesses the media collection according
            to ID3 tags rather than file structure. Can be a single ID or a
            list of IDs.
        :param artist_id: The ID(s) of an artist/artists to star. Use this
            rather than ``id`` if the client accesses the media collection
            according to ID3 tags rather than file structure. Can be a single
            ID or a list of IDs.
        """
        return self._get_json(
            self._make_url("star"), id=id, albumId=album_id, artistId=artist_id,
        )

    def unstar(
        self,
        id: Union[int, List[int]] = None,
        album_id: Union[int, List[int]] = None,
        artist_id: Union[int, List[int]] = None,
    ) -> Response:
        """
        Removes the star from a song, album or artist.

        :param id: The ID(s) of the file(s) (song(s)) or folder(s)
            (album(s)/artist(s)) to star. Can be a single ID or a list of IDs.
        :param album_id: The ID(s) of an album/albums to star. Use this rather
            than ``id`` if the client accesses the media collection according
            to ID3 tags rather than file structure. Can be a single ID or a
            list of IDs.
        :param artist_id: The ID(s) of an artist/artists to star. Use this
            rather than ``id`` if the client accesses the media collection
            according to ID3 tags rather than file structure. Can be a single
            ID or a list of IDs.
        """
        return self._get_json(
            self._make_url("unstar"), id=id, albumId=album_id, artistId=artist_id,
        )

    def set_rating(self, id: int, rating: int) -> Response:
        """
        Sets the rating for a music file.

        :param id: A string which uniquely identifies the file (song) or folder
            (album/artist) to rate.
        :param rating: The rating between 1 and 5 (inclusive), or 0 to remove
            the rating.
        """
        return self._get_json(self._make_url("setRating"), id=id, rating=rating,)

    def scrobble(
        self, id: int, time: datetime = None, submission: bool = True,
    ) -> Response:
        """
        Registers the local playback of one or more media files. Typically used
        when playing media that is cached on the client. This operation
        includes the following:

            * "Scrobbles" the media files on last.fm if the user has configured
              his/her last.fm credentials on the Subsonic server (Settings >
              Personal).
            * Updates the play count and last played timestamp for the media
              files. (Since 1.11.0)
            * Makes the media files appear in the "Now playing" page in the web
              app, and appear in the list of songs returned by
              ``getNowPlaying`` (Since 1.11.0)

        Since 1.8.0 you may specify multiple id (and optionally time)
        parameters to scrobble multiple files.

        :param id: The ID of the file to scrobble.
        :param time: (Since 1.8.0) The time at which the song was listened to.
        :param submission: Whether this is a "submission" or a "now playing"
            notification.
        """
        return self._get_json(
            self._make_url("scrobble"), id=id, time=time, submission=submission,
        )

    def get_shares(self) -> Shares:
        """
        Returns information about shared media this user is allowed to manage.
        Takes no extra parameters.
        """
        return self._get_json(self._make_url("getShares")).shares

    def create_share(
        self,
        id: Union[int, List[int]],
        description: str = None,
        expires: datetime = None,
    ) -> Shares:
        """
        Creates a public URL that can be used by anyone to stream music or
        video from the Subsonic server. The URL is short and suitable for
        posting on Facebook, Twitter etc. Note: The user must be authorized to
        share (see Settings > Users > User is allowed to share files with
        anyone).

        :param id: ID(s) of (a) song(s), album(s) or video(s) to share. Can be
            a single ID or a list of IDs.
        :param description: A user-defined description that will be displayed
            to people visiting the shared media.
        :param expires: The time at which the share expires.
        """
        result = self._get_json(
            self._make_url("createShare"),
            id=id,
            description=description,
            expires=expires,
        )
        return result.shares

    def update_share(
        self, id: int, description: str = None, expires: datetime = None,
    ) -> Response:
        """
        Updates the description and/or expiration date for an existing share.

        :param id: ID of the share to update.
        :param description: A user-defined description that will be displayed
            to people visiting the shared media.
        :param expires: The time at which the share expires.
        """
        return self._get_json(
            self._make_url("updateShare"),
            id=id,
            description=description,
            expires=expires,
        )

    def delete_share(self, id: int) -> Response:
        """
        Deletes an existing share.

        :param id: ID of the share to delete.
        """
        return self._get_json(self._make_url("deleteShare"), id=id)

    def get_internet_radio_stations(self) -> InternetRadioStations:
        """Returns all internet radio stations."""
        result = self._get_json(self._make_url("getInternetRadioStations"))
        return result.internetRadioStations

    def create_internet_radio_station(
        self, stream_url: str, name: str, homepage_url: str = None,
    ) -> Response:
        """
        Adds a new internet radio station. Only users with admin privileges are
        allowed to call this method.

        :param stream_url: The stream URL for the station.
        :param name: The user-defined name for the station.
        :param homepage_url: The home page URL for the station.
        """
        return self._get_json(
            self._make_url("createInternetRadioStation"),
            streamUrl=stream_url,
            name=name,
            homepageUrl=homepage_url,
        )

    def update_internet_radio_station(
        self, id: int, stream_url: str, name: str, homepage_url: str = None,
    ) -> Response:
        """
        Updates an existing internet radio station. Only users with admin
        privileges are allowed to call this method.

        :param id: The ID for the station.
        :param stream_url: The stream URL for the station.
        :param name: The user-defined name for the station.
        :param homepage_url: The home page URL for the station.
        """
        return self._get_json(
            self._make_url("updateInternetRadioStation"),
            id=id,
            streamUrl=stream_url,
            name=name,
            homepageUrl=homepage_url,
        )

    def delete_internet_radio_station(self, id: int) -> Response:
        """
        Deletes an existing internet radio station. Only users with admin
        privileges are allowed to call this method.

        :param id: The ID for the station.
        """
        return self._get_json(self._make_url("deleteInternetRadioStation"), id=id,)

    def get_user(self, username: str) -> User:
        """
        Get details about a given user, including which authorization roles and
        folder access it has. Can be used to enable/disable certain features in
        the client, such as jukebox control.

        :param username: The name of the user to retrieve. You can only
            retrieve your own user unless you have admin privileges.
        """
        result = self._get_json(self._make_url("getUser"), username=username)
        return result.user

    def get_users(self) -> Users:
        """
        Get details about all users, including which authorization roles and
        folder access they have. Only users with admin privileges are allowed
        to call this method.
        """
        return self._get_json(self._make_url("getUsers")).users

    def create_user(
        self,
        username: str,
        password: str,
        email: str,
        ldap_authenticated: bool = False,
        admin_role: bool = False,
        settings_role: bool = True,
        stream_role: bool = True,
        jukebox_role: bool = False,
        download_role: bool = False,
        upload_role: bool = False,
        playlist_role: bool = False,
        covert_art_role: bool = False,
        comment_role: bool = False,
        podcast_role: bool = False,
        share_role: bool = False,
        video_conversion_role: bool = False,
        music_folder_id: Union[int, List[int]] = None,
    ) -> Response:
        """
        Creates a new Subsonic user.

        :param username: The name of the new user.
        :param password: The password of the new user, either in clear text or
            hex-encoded.
        :param email: The email address of the new user.
        :param ldap_authenticated: Whether the user is authenicated in LDAP.
        :param admin_role: Whether the user is administrator.
        :param settings_role: Whether the user is allowed to change personal
            settings and password.
        :param stream_role: Whether the user is allowed to play files.
        :param jukebox_role: Whether the user is allowed to play files in
            jukebox mode.
        :param download_role: Whether the user is allowed to download files.
        :param upload_role: Whether the user is allowed to upload files.
        :param playlist_role: Whether the user is allowed to create and delete
            playlists. Since 1.8.0, changing this role has no effect.
        :param covert_art_role: Whether the user is allowed to change cover art
            and tags.
        :param comment_role: Whether the user is allowed to create and edit
            comments and ratings.
        :param podcast_role: Whether the user is allowed to administrate
            Podcasts.
        :param share_role: (Since 1.8.0) Whether the user is allowed to share
            files with anyone.
        :param video_conversion_role: (Since 1.15.0) Whether the user is
            allowed to start video conversions.
        :param music_folder_id: (Since 1.12.0) IDs of the music folders the
            user is allowed access to. Can be a single ID or a list of IDs.
        """
        return self._get_json(
            self._make_url("createUser"),
            username=username,
            password=password,
            email=email,
            ldapAuthenticated=ldap_authenticated,
            adminRole=admin_role,
            settingsRole=settings_role,
            streamRole=stream_role,
            jukeboxRole=jukebox_role,
            downloadRole=download_role,
            uploadRole=upload_role,
            playlistRole=playlist_role,
            coverArtRole=covert_art_role,
            commentRole=comment_role,
            podcastRole=podcast_role,
            shareRole=share_role,
            videoConversionRole=video_conversion_role,
            musicFolderId=music_folder_id,
        )

    def update_user(
        self,
        username: str,
        password: str = None,
        email: str = None,
        ldap_authenticated: bool = False,
        admin_role: bool = False,
        settings_role: bool = True,
        stream_role: bool = True,
        jukebox_role: bool = False,
        download_role: bool = False,
        upload_role: bool = False,
        playlist_role: bool = False,
        covert_art_role: bool = False,
        comment_role: bool = False,
        podcast_role: bool = False,
        share_role: bool = False,
        video_conversion_role: bool = False,
        music_folder_id: Union[int, List[int]] = None,
    ) -> Response:
        """
        Modifies an existing Subsonic user.

        :param username: The name of the user.
        :param password: The password of the user, either in clear text or
            hex-encoded.
        :param email: The email address of the user.
        :param ldap_authenticated: Whether the user is authenicated in LDAP.
        :param admin_role: Whether the user is administrator.
        :param settings_role: Whether the user is allowed to change personal
            settings and password.
        :param stream_role: Whether the user is allowed to play files.
        :param jukebox_role: Whether the user is allowed to play files in
            jukebox mode.
        :param download_role: Whether the user is allowed to download files.
        :param upload_role: Whether the user is allowed to upload files.
        :param playlist_role: Whether the user is allowed to create and delete
            playlists. Since 1.8.0, changing this role has no effect.
        :param covert_art_role: Whether the user is allowed to change cover art
            and tags.
        :param comment_role: Whether the user is allowed to create and edit
            comments and ratings.
        :param podcast_role: Whether the user is allowed to administrate
            Podcasts.
        :param share_role: (Since 1.8.0) Whether the user is allowed to share
            files with anyone.
        :param video_conversion_role: (Since 1.15.0) Whether the user is
            allowed to start video conversions.
        :param music_folder_id: (Since 1.12.0) IDs of the music folders the
            user is allowed access to. Can be a single ID or a list of IDs.
        """
        return self._get_json(
            self._make_url("updateUser"),
            username=username,
            password=password,
            email=email,
            ldapAuthenticated=ldap_authenticated,
            adminRole=admin_role,
            settingsRole=settings_role,
            streamRole=stream_role,
            jukeboxRole=jukebox_role,
            downloadRole=download_role,
            uploadRole=upload_role,
            playlistRole=playlist_role,
            coverArtRole=covert_art_role,
            commentRole=comment_role,
            podcastRole=podcast_role,
            shareRole=share_role,
            videoConversionRole=video_conversion_role,
            musicFolderId=music_folder_id,
        )

    def delete_user(self, username: str) -> Response:
        """
        Deletes an existing Subsonic user.

        :param username: The name of the new user.
        """
        return self._get_json(self._make_url("deleteUser"), username=username)

    def change_password(self, username: str, password: str) -> Response:
        """
        Changes the password of an existing Subsonic user. You can only change
        your own password unless you have admin privileges.

        :param username: The name of the user which should change its password.
        :param password: The new password of the new user, either in clear text
            of hex-encoded.
        """
        return self._get_json(
            self._make_url("changePassword"), username=username, password=password,
        )

    def get_bookmarks(self) -> Bookmarks:
        """
        Returns all bookmarks for this user. A bookmark is a position within a
        certain media file.
        """
        return self._get_json(self._make_url("getBookmarks")).bookmarks

    def create_bookmarks(
        self, id: int, position: int, comment: str = None,
    ) -> Response:
        """
        Creates or updates a bookmark (a position within a media file).
        Bookmarks are personal and not visible to other users.

        :param id: ID of the media file to bookmark. If a bookmark already
            exists for this file it will be overwritten.
        :param position: The position (in milliseconds) within the media file.
        :param comment: A user-defined comment.
        """
        return self._get_json(
            self._make_url("createBookmark"), id=id, position=position, comment=comment,
        )

    def delete_bookmark(self, id: int) -> Response:
        """
        Deletes the bookmark for a given file.

        :param id: ID of the media file for which to delete the bookmark. Other
            users' bookmarks are not affected.
        """
        return self._get_json(self._make_url("deleteBookmark"), id=id)

    def get_play_queue(self) -> Optional[PlayQueue]:
        """
        Returns the state of the play queue for this user (as set by
        ``savePlayQueue``). This includes the tracks in the play queue, the
        currently playing track, and the position within this track. Typically
        used to allow a user to move between different clients/apps while
        retaining the same play queue (for instance when listening to an audio
        book).
        """
        return self._get_json(self._make_url("getPlayQueue")).playQueue

    def save_play_queue(
        self, id: Union[int, List[int]], current: int = None, position: int = None,
    ) -> Response:
        """
        Saves the state of the play queue for this user. This includes the
        tracks in the play queue, the currently playing track, and the position
        within this track. Typically used to allow a user to move between
        different clients/apps while retaining the same play queue (for
        instance when listening to an audio book).

        :param id: ID(s) of a/the song(s) in the play queue. Can be either a
            single ID or a list of IDs.
        :param current: The ID of the current playing song.
        :param position: The position in milliseconds within the currently
            playing song.
        """
        return self._get_json(
            self._make_url("savePlayQueue"), id=id, current=current, position=position,
        )

    def get_scan_status(self) -> ScanStatus:
        """
        Returns the current status for media library scanning. Takes no extra
        parameters.
        """
        return self._get_json(self._make_url("getScanStatus")).scanStatus

    def start_scan(self) -> ScanStatus:
        """
        Initiates a rescan of the media libraries. Takes no extra parameters.
        """
        return self._get_json(self._make_url("startScan")).scanStatus
