From b495b9ac0b03d28264ad08335da7bef409526236 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 15 Sep 2020 21:27:41 +0200 Subject: [PATCH] prettify json responses return human readable responses for all enum values --- music_assistant/database.py | 42 +++++++++--------- music_assistant/http_streamer.py | 41 +++++++++++++++++- music_assistant/models/config_entry.py | 6 ++- music_assistant/models/media_types.py | 43 +++++++------------ music_assistant/models/player.py | 20 +++++---- music_assistant/models/player_queue.py | 43 +------------------ music_assistant/models/provider.py | 2 +- music_assistant/models/streamdetails.py | 8 ++-- music_assistant/music_manager.py | 43 +++++++++++++++++++ music_assistant/providers/qobuz/__init__.py | 18 ++++---- music_assistant/utils.py | 47 ++++++++++++++++----- music_assistant/web.py | 42 ++++++++++-------- requirements.txt | 1 + wget-log | 0 14 files changed, 213 insertions(+), 143 deletions(-) create mode 100644 wget-log diff --git a/music_assistant/database.py b/music_assistant/database.py index 2ce68875..c5f36096 100755 --- a/music_assistant/database.py +++ b/music_assistant/database.py @@ -173,7 +173,7 @@ class Database: sql_query = """SELECT item_id FROM provider_mappings WHERE prov_item_id = ? AND provider = ? AND media_type = ?;""" async with db_conn.execute( - sql_query, (prov_item_id, provider_id, media_type) + sql_query, (prov_item_id, provider_id, int(media_type)) ) as cursor: item_id = await cursor.fetchone() if item_id: @@ -318,7 +318,7 @@ class Database: db_row["playlist_id"], MediaType.Playlist, db_conn ), tags=await self.__async_get_tags( - db_row["playlist_id"], MediaType.Playlist, db_conn + db_row["playlist_id"], int(MediaType.Playlist), db_conn ), external_ids=await self.__async_get_external_ids( db_row["playlist_id"], MediaType.Playlist, db_conn @@ -485,7 +485,7 @@ class Database: item_id = try_parse_int(item_id) sql_query = """INSERT or REPLACE INTO library_items (item_id, provider, media_type) VALUES(?,?,?);""" - await db_conn.execute(sql_query, (item_id, provider, media_type)) + await db_conn.execute(sql_query, (item_id, provider, int(media_type))) await db_conn.commit() async def async_remove_from_library( @@ -495,13 +495,13 @@ class Database: async with DbConnect(self._dbfile) as db_conn: item_id = try_parse_int(item_id) sql_query = "DELETE FROM library_items WHERE item_id=? AND provider=? AND media_type=?;" - await db_conn.execute(sql_query, (item_id, provider, media_type)) + await db_conn.execute(sql_query, (item_id, provider, int(media_type))) if media_type == MediaType.Playlist: sql_query = "DELETE FROM playlists WHERE playlist_id=?;" await db_conn.execute(sql_query, (item_id,)) sql_query = """DELETE FROM provider_mappings WHERE item_id=? AND media_type=? AND provider=?;""" - await db_conn.execute(sql_query, (item_id, media_type, provider)) + await db_conn.execute(sql_query, (item_id, int(media_type), provider)) await db_conn.commit() async def async_get_artists( @@ -679,8 +679,8 @@ class Database: album.artist.item_id, album.name, album.version, - album.year, - album.album_type, + int(album.year), + int(album.album_type), ), ) as cursor: res = await cursor.fetchone() @@ -707,7 +707,7 @@ class Database: query_params = ( album.artist.item_id, album.name, - album.album_type, + int(album.album_type), album.year, album.version, ) @@ -943,7 +943,7 @@ class Database: if value: sql_query = """INSERT or REPLACE INTO metadata (item_id, media_type, key, value) VALUES(?,?,?,?);""" - await db_conn.execute(sql_query, (item_id, media_type, key, value)) + await db_conn.execute(sql_query, (item_id, int(media_type), key, value)) async def __async_get_metadata( self, @@ -959,7 +959,7 @@ class Database: ) if filter_key: sql_query += ' AND key = "%s"' % filter_key - async with db_conn.execute(sql_query, (item_id, media_type)) as cursor: + async with db_conn.execute(sql_query, (item_id, int(media_type))) as cursor: db_rows = await cursor.fetchall() for db_row in db_rows: key = db_row[0] @@ -981,7 +981,7 @@ class Database: tag_id = cursor.lastrowid sql_query = """INSERT or IGNORE INTO media_tags (item_id, media_type, tag_id) VALUES(?,?,?);""" - await db_conn.execute(sql_query, (item_id, media_type, tag_id)) + await db_conn.execute(sql_query, (item_id, int(media_type), tag_id)) async def __async_get_tags( self, item_id: int, media_type: MediaType, db_conn: sqlite3.Connection @@ -990,7 +990,7 @@ class Database: tags = [] sql_query = """SELECT name FROM tags INNER JOIN media_tags ON tags.tag_id = media_tags.tag_id WHERE item_id = ? AND media_type = ?""" - async with db_conn.execute(sql_query, (item_id, media_type)) as cursor: + async with db_conn.execute(sql_query, (item_id, int(media_type))) as cursor: db_rows = await cursor.fetchall() for db_row in db_rows: tags.append(db_row[0]) @@ -1048,7 +1048,7 @@ class Database: for key, value in external_ids.items(): sql_query = """INSERT or REPLACE INTO external_ids (item_id, media_type, key, value) VALUES(?,?,?,?);""" - await db_conn.execute(sql_query, (item_id, media_type, key, value)) + await db_conn.execute(sql_query, (item_id, int(media_type), key, value)) async def __async_get_external_ids( self, item_id: int, media_type: MediaType, db_conn: sqlite3.Connection @@ -1058,7 +1058,9 @@ class Database: sql_query = ( "SELECT key, value FROM external_ids WHERE item_id = ? AND media_type = ?" ) - for db_row in await db_conn.execute_fetchall(sql_query, (item_id, media_type)): + for db_row in await db_conn.execute_fetchall( + sql_query, (item_id, int(media_type)) + ): external_ids[db_row[0]] = db_row[1] return external_ids @@ -1079,10 +1081,10 @@ class Database: sql_query, ( item_id, - media_type, + int(media_type), prov.item_id, prov.provider, - prov.quality, + int(prov.quality), prov.details, ), ) @@ -1095,7 +1097,9 @@ class Database: sql_query = "SELECT prov_item_id, provider, quality, details \ FROM provider_mappings \ WHERE item_id = ? AND media_type = ?" - for db_row in await db_conn.execute_fetchall(sql_query, (item_id, media_type)): + for db_row in await db_conn.execute_fetchall( + sql_query, (item_id, int(media_type)) + ): prov_mapping = MediaItemProviderId( provider=db_row["provider"], item_id=db_row["prov_item_id"], @@ -1114,7 +1118,7 @@ class Database: "SELECT provider FROM library_items WHERE item_id = ? AND media_type = ?" ) for db_row in await db_conn.execute_fetchall( - sql_query, (db_item_id, media_type) + sql_query, (db_item_id, int(media_type)) ): providers.append(db_row[0]) return providers @@ -1127,7 +1131,7 @@ class Database: sql_query = "SELECT (item_id) FROM external_ids \ WHERE media_type=? AND key=? AND value=?;" for db_row in await db_conn.execute_fetchall( - sql_query, (media_item.media_type, key, value) + sql_query, (int(media_item.media_type), key, value) ): if db_row: return db_row[0] diff --git a/music_assistant/http_streamer.py b/music_assistant/http_streamer.py index 83983f44..db3c4e8b 100755 --- a/music_assistant/http_streamer.py +++ b/music_assistant/http_streamer.py @@ -21,6 +21,7 @@ import soundfile from aiohttp import web from music_assistant.constants import EVENT_STREAM_ENDED, EVENT_STREAM_STARTED from music_assistant.models.media_types import MediaType +from music_assistant.models.player_queue import QueueItem from music_assistant.models.streamdetails import ContentType, StreamDetails, StreamType from music_assistant.utils import create_tempfile, get_ip, try_parse_int from music_assistant.web import require_local_subnet @@ -38,6 +39,43 @@ class HTTPStreamer: self.analyze_jobs = {} self.stream_clients = [] + async def async_stream_media_item(self, http_request): + """Start stream for a single media item, player independent.""" + # make sure we have valid params + media_type = MediaType.from_string(http_request.match_info["media_type"]) + if media_type not in [MediaType.Track, MediaType.Radio]: + return web.Response(status=404, reason="Media item is not playable!") + provider = http_request.match_info["provider"] + item_id = http_request.match_info["item_id"] + player_id = http_request.remote # fake player id + # prepare headers as audio/flac content + resp = web.StreamResponse( + status=200, reason="OK", headers={"Content-Type": "audio/flac"} + ) + await resp.prepare(http_request) + # collect tracks to play + media_item = await self.mass.music_manager.async_get_item( + item_id, provider, media_type + ) + queue_item = QueueItem(media_item) + # run the streamer in executor to prevent the subprocess locking up our eventloop + cancelled = threading.Event() + bg_task = self.mass.loop.run_in_executor( + None, + self.__get_queue_item_stream, + player_id, + queue_item, + resp, + cancelled, + ) + # let the streaming begin! + try: + await asyncio.gather(bg_task) + except (asyncio.CancelledError, asyncio.TimeoutError) as exc: + cancelled.set() + raise exc # re-raise + return resp + @require_local_subnet async def async_stream(self, http_request): """Start stream for a player.""" @@ -331,9 +369,8 @@ class HTTPStreamer: ): """Get audio stream from provider and apply additional effects/processing if needed.""" # pylint: disable=subprocess-popen-preexec-fn - player_queue = self.mass.player_manager.get_player_queue(player_id) streamdetails = self.mass.add_job( - player_queue.async_get_stream_details(player_id, queue_item) + self.mass.music_manager.async_get_stream_details(queue_item, player_id) ).result() if not streamdetails: LOGGER.warning("no stream details for %s", queue_item.name) diff --git a/music_assistant/models/config_entry.py b/music_assistant/models/config_entry.py index ef074e3b..0b000318 100644 --- a/music_assistant/models/config_entry.py +++ b/music_assistant/models/config_entry.py @@ -4,8 +4,10 @@ from dataclasses import dataclass, field from enum import Enum from typing import Any, List, Optional, Tuple +from mashumaro import DataClassDictMixin -class ConfigEntryType(str, Enum): + +class ConfigEntryType(Enum): """Enum for the type of a config entry.""" BOOL = "boolean" @@ -18,7 +20,7 @@ class ConfigEntryType(str, Enum): @dataclass -class ConfigEntry: +class ConfigEntry(DataClassDictMixin): """Model for a Config Entry.""" entry_key: str diff --git a/music_assistant/models/media_types.py b/music_assistant/models/media_types.py index 10f95de5..7c8a3318 100755 --- a/music_assistant/models/media_types.py +++ b/music_assistant/models/media_types.py @@ -2,10 +2,13 @@ from dataclasses import dataclass, field from enum import Enum -from typing import List, Optional +from typing import Any, List, Optional +from mashumaro import DataClassDictMixin +from music_assistant.utils import CustomIntEnum -class MediaType(int, Enum): + +class MediaType(CustomIntEnum): """Enum for MediaType.""" Artist = 1 @@ -15,23 +18,7 @@ class MediaType(int, Enum): Radio = 5 -def media_type_from_string(media_type_str: str) -> MediaType: - """Convert a string to a MediaType.""" - media_type_str = media_type_str.lower() - if "artist" in media_type_str or media_type_str == "1": - return MediaType.Artist - if "album" in media_type_str or media_type_str == "2": - return MediaType.Album - if "track" in media_type_str or media_type_str == "3": - return MediaType.Track - if "playlist" in media_type_str or media_type_str == "4": - return MediaType.Playlist - if "radio" in media_type_str or media_type_str == "5": - return MediaType.Radio - return None - - -class ContributorRole(int, Enum): +class ContributorRole(CustomIntEnum): """Enum for Contributor Role.""" Artist = 1 @@ -39,7 +26,7 @@ class ContributorRole(int, Enum): Producer = 3 -class AlbumType(int, Enum): +class AlbumType(CustomIntEnum): """Enum for Album type.""" Album = 1 @@ -47,7 +34,7 @@ class AlbumType(int, Enum): Compilation = 3 -class TrackQuality(int, Enum): +class TrackQuality(CustomIntEnum): """Enum for Track Quality.""" LOSSY_MP3 = 0 @@ -62,7 +49,7 @@ class TrackQuality(int, Enum): @dataclass -class MediaItemProviderId: +class MediaItemProviderId(DataClassDictMixin): """Model for a MediaItem's provider id.""" provider: str @@ -71,7 +58,7 @@ class MediaItemProviderId: details: Optional[str] = None -class ExternalId(str, Enum): +class ExternalId(Enum): """Enum with external id's.""" MUSICBRAINZ = "musicbrainz" @@ -80,15 +67,15 @@ class ExternalId(str, Enum): @dataclass -class MediaItem: +class MediaItem(DataClassDictMixin): """Representation of a media item.""" item_id: str = "" provider: str = "" name: str = "" - metadata: dict = field(default_factory=dict) + metadata: Any = field(default_factory=dict) tags: List[str] = field(default_factory=list) - external_ids: dict = field(default_factory=dict) + external_ids: Any = field(default_factory=dict) provider_ids: List[MediaItemProviderId] = field(default_factory=list) in_library: List[str] = field(default_factory=list) is_lazy: bool = False @@ -134,7 +121,7 @@ class Playlist(MediaItem): media_type: MediaType = MediaType.Playlist owner: str = "" - checksum: [Optional[str]] = None # some value to detect playlist track changes + checksum: str = "" # some value to detect playlist track changes is_editable: bool = False @@ -147,7 +134,7 @@ class Radio(MediaItem): @dataclass -class SearchResult: +class SearchResult(DataClassDictMixin): """Model for Media Item Search result.""" artists: List[Artist] = field(default_factory=list) diff --git a/music_assistant/models/player.py b/music_assistant/models/player.py index 8a90ef91..25617769 100755 --- a/music_assistant/models/player.py +++ b/music_assistant/models/player.py @@ -3,12 +3,14 @@ from dataclasses import dataclass, field from datetime import datetime from enum import Enum -from typing import Any, Awaitable, Callable, List, Optional, Union +from typing import Any, List, Optional +from mashumaro import DataClassDictMixin from music_assistant.models.config_entry import ConfigEntry +from music_assistant.utils import CustomIntEnum -class PlayerState(str, Enum): +class PlayerState(Enum): """Enum for the playstate of a player.""" Off = "off" @@ -18,7 +20,7 @@ class PlayerState(str, Enum): @dataclass -class DeviceInfo: +class DeviceInfo(DataClassDictMixin): """Model for a player's deviceinfo.""" model: Optional[str] = "" @@ -26,7 +28,7 @@ class DeviceInfo: manufacturer: Optional[str] = "" -class PlayerFeature(int, Enum): +class PlayerFeature(CustomIntEnum): """Enum for player features.""" QUEUE = 0 @@ -35,7 +37,7 @@ class PlayerFeature(int, Enum): @dataclass -class Player: +class Player(DataClassDictMixin): """Model for a MusicPlayer.""" player_id: str @@ -50,7 +52,7 @@ class Player: muted: bool = False is_group_player: bool = False group_childs: List[str] = field(default_factory=list) - device_info: Optional[DeviceInfo] = None + device_info: DeviceInfo = None should_poll: bool = False features: List[PlayerFeature] = field(default_factory=list) config_entries: List[ConfigEntry] = field(default_factory=list) @@ -74,7 +76,7 @@ class Player: self._on_update(self.player_id, name) -class PlayerControlType(int, Enum): +class PlayerControlType(CustomIntEnum): """Enum with different player control types.""" POWER = 0 @@ -83,7 +85,7 @@ class PlayerControlType(int, Enum): @dataclass -class PlayerControl: +class PlayerControl(DataClassDictMixin): """ Model for a player control. @@ -95,4 +97,4 @@ class PlayerControl: id: str = "" name: str = "" state: Optional[Any] = None - set_state: Callable[..., Union[None, Awaitable]] = None + set_state: Any = None diff --git a/music_assistant/models/player_queue.py b/music_assistant/models/player_queue.py index b6aaef9d..348d766b 100755 --- a/music_assistant/models/player_queue.py +++ b/music_assistant/models/player_queue.py @@ -13,7 +13,7 @@ from music_assistant.constants import ( EVENT_QUEUE_ITEMS_UPDATED, EVENT_QUEUE_UPDATED, ) -from music_assistant.models.media_types import MediaType, Track +from music_assistant.models.media_types import Track from music_assistant.models.player import PlayerFeature, PlayerState from music_assistant.models.streamdetails import StreamDetails from music_assistant.utils import callback @@ -25,7 +25,7 @@ from music_assistant.utils import callback LOGGER = logging.getLogger("mass") -class QueueOption(str, Enum): +class QueueOption(Enum): """Enum representation of the queue (play) options.""" Play = "play" @@ -476,45 +476,6 @@ class PlayerQueue: self._last_queue_startindex = self._next_queue_startindex return self.get_item(self._next_queue_startindex) - async def async_get_stream_details( - self, player_id: str, queue_item: QueueItem - ) -> StreamDetails: - """ - Get streamdetails for the given queue_item. - - This is called just-in-time when a player/queue wants a QueueItem to be played. - Do not try to request streamdetails in advance as this is expiring data. - param player_id: The id of the player that will be playing the stream. - param queue_item: The QueueItem for which to request the streamdetails for. - """ - # always request the full db track as there might be other qualities available - # except for radio - if queue_item.media_type == MediaType.Radio: - full_track = queue_item - else: - full_track = await self.mass.music_manager.async_get_track( - queue_item.item_id, queue_item.provider, lazy=True, refresh=True - ) - # sort by quality and check track availability - for prov_media in sorted( - full_track.provider_ids, key=lambda x: x.quality, reverse=True - ): - # get streamdetails from provider - music_prov = self.mass.get_provider(prov_media.provider) - if not music_prov: - continue # provider temporary unavailable ? - - streamdetails: StreamDetails = await music_prov.async_get_stream_details( - prov_media.item_id - ) - - if streamdetails: - streamdetails.player_id = player_id - # set streamdetails as attribute on the queue_item - queue_item.streamdetails = streamdetails - return streamdetails - return None - def to_dict(self): """Instance attributes as dict so it can be serialized to json.""" return { diff --git a/music_assistant/models/provider.py b/music_assistant/models/provider.py index 35530c19..cd1f0101 100644 --- a/music_assistant/models/provider.py +++ b/music_assistant/models/provider.py @@ -11,7 +11,7 @@ if TYPE_CHECKING: from music_assistant.mass import MusicAssistant -class ProviderType(str, Enum): +class ProviderType(Enum): """Enum with plugin types.""" MUSIC_PROVIDER = "music_provider" diff --git a/music_assistant/models/streamdetails.py b/music_assistant/models/streamdetails.py index 8f42bd31..fa34426e 100644 --- a/music_assistant/models/streamdetails.py +++ b/music_assistant/models/streamdetails.py @@ -4,8 +4,10 @@ from dataclasses import dataclass from enum import Enum from typing import Any, Optional +from mashumaro import DataClassDictMixin -class StreamType(str, Enum): + +class StreamType(Enum): """Enum with stream types.""" EXECUTABLE = "executable" @@ -13,7 +15,7 @@ class StreamType(str, Enum): FILE = "file" -class ContentType(str, Enum): +class ContentType(Enum): """Enum with stream content types.""" OGG = "ogg" @@ -24,7 +26,7 @@ class ContentType(str, Enum): @dataclass -class StreamDetails: +class StreamDetails(DataClassDictMixin): """Model for streamdetails.""" type: StreamType diff --git a/music_assistant/music_manager.py b/music_assistant/music_manager.py index 898b530d..2d487e47 100755 --- a/music_assistant/music_manager.py +++ b/music_assistant/music_manager.py @@ -24,6 +24,7 @@ from music_assistant.models.media_types import ( ) from music_assistant.models.musicprovider import MusicProvider from music_assistant.models.provider import ProviderType +from music_assistant.models.streamdetails import StreamDetails from music_assistant.utils import compare_strings, run_periodic from PIL import Image @@ -1065,6 +1066,48 @@ class MusicManager: # return file from cache return cache_file_sized + async def async_get_stream_details( + self, media_item: MediaItem, player_id: str = "" + ) -> StreamDetails: + """ + Get streamdetails for the given media_item. + + This is called just-in-time when a player/queue wants a MediaItem to be played. + Do not try to request streamdetails in advance as this is expiring data. + param media_item: The MediaItem (track/radio) for which to request the streamdetails for. + param player_id: Optionally provide the player_id which will play this stream. + """ + if media_item.streamdetails: + media_item.streamdetails.player_id = player_id + return media_item.streamdetails # already present, no need to fetch again! + # always request the full db track as there might be other qualities available + # except for radio + if media_item.media_type == MediaType.Radio: + full_track = media_item + else: + full_track = await self.async_get_track( + media_item.item_id, media_item.provider, lazy=True, refresh=True + ) + # sort by quality and check track availability + for prov_media in sorted( + full_track.provider_ids, key=lambda x: x.quality, reverse=True + ): + # get streamdetails from provider + music_prov = self.mass.get_provider(prov_media.provider) + if not music_prov: + continue # provider temporary unavailable ? + + streamdetails = await music_prov.async_get_stream_details( + prov_media.item_id + ) + + if streamdetails: + streamdetails.player_id = player_id + # set streamdetails as attribute on the media_item + media_item.streamdetails = streamdetails + return streamdetails + return None + ################ Library synchronization logic ################ @run_periodic(3600 * 3) diff --git a/music_assistant/providers/qobuz/__init__.py b/music_assistant/providers/qobuz/__init__.py index 23cffe93..0c6037a0 100644 --- a/music_assistant/providers/qobuz/__init__.py +++ b/music_assistant/providers/qobuz/__init__.py @@ -346,28 +346,28 @@ class QobuzProvider(MusicProvider): async def async_get_stream_details(self, item_id: str) -> StreamDetails: """Return the content details for the given track when it will be streamed.""" - streamdetails = None + streamdata = None for format_id in [27, 7, 6, 5]: # it seems that simply requesting for highest available quality does not work # from time to time the api response is empty for this request ?! params = {"format_id": format_id, "track_id": item_id, "intent": "stream"} - streamdetails = await self.__async_get_data( + streamdata = await self.__async_get_data( "track/getFileUrl", params, sign_request=True ) - if streamdetails and streamdetails.get("url"): + if streamdata and streamdata.get("url"): break - if not streamdetails or not streamdetails.get("url"): + if not streamdata or not streamdata.get("url"): LOGGER.error("Unable to retrieve stream url for track %s", item_id) return None return StreamDetails( type=StreamType.URL, item_id=str(item_id), provider=PROV_ID, - path=streamdetails["url"], - content_type=ContentType(streamdetails["mime_type"].split("/")[1]), - sample_rate=int(streamdetails["sampling_rate"] * 1000), - bit_depth=streamdetails["bit_depth"], - details=streamdetails, # we need these details for reporting playback + path=streamdata["url"], + content_type=ContentType(streamdata["mime_type"].split("/")[1]), + sample_rate=int(streamdata["sampling_rate"] * 1000), + bit_depth=streamdata["bit_depth"], + details=streamdata, # we need these details for reporting playback ) async def async_mass_event(self, msg, msg_details): diff --git a/music_assistant/utils.py b/music_assistant/utils.py index 5b330a92..3a16feab 100755 --- a/music_assistant/utils.py +++ b/music_assistant/utils.py @@ -1,6 +1,5 @@ """Helper and utility functions.""" import asyncio -import dataclasses import functools import logging import os @@ -247,18 +246,15 @@ class EnhancedJSONEncoder(json.JSONEncoder): def default(self, obj): """Return default handler.""" # pylint: disable=method-hidden - if dataclasses.is_dataclass(obj): - return dataclasses.asdict(obj) - if isinstance(obj, Enum): - return str(obj) - if isinstance(obj, Enum): - return int(obj) + try: + # as most of our objects are dataclass, we just try this first + return obj.to_dict() + except AttributeError: + pass if isinstance(obj, datetime): return obj.isoformat() - if hasattr(obj, "to_dict"): - return obj.to_dict() - if hasattr(obj, "items"): - return obj.items() + if isinstance(obj, Enum): + return str(obj) return super().default(obj) @@ -300,3 +296,32 @@ def create_tempfile(): buffering=0 ) return tempfile.NamedTemporaryFile(buffering=0) + + +class CustomIntEnum(Enum): + """Base for IntEnum with some helpers.""" + + # when serializing we prefer the string (name) representation + # internally (database) we use the int value + + def __int__(self): + """Return integer value.""" + return super().value + + def __str__(self): + """Return string value.""" + # pylint: disable=no-member + return self._name_.lower() + + @property + def value(self): + """Return the (json friendly) string name.""" + return self.__str__() + + @classmethod + def from_string(cls, string): + """Create IntEnum from it's string equivalent.""" + for key, value in cls.__dict__.items(): + if key.lower() == string or value == try_parse_int(string): + return value + return KeyError diff --git a/music_assistant/web.py b/music_assistant/web.py index 220dc8aa..765942d4 100755 --- a/music_assistant/web.py +++ b/music_assistant/web.py @@ -19,14 +19,9 @@ from music_assistant.constants import ( CONF_KEY_PLAYERSETTINGS, CONF_KEY_PROVIDERS, ) -from music_assistant.models.media_types import MediaType, media_type_from_string -from music_assistant.utils import ( - EnhancedJSONEncoder, - get_external_ip, - get_hostname, - get_ip, - json_serializer, -) +from music_assistant.models.media_types import MediaType +from music_assistant.models.player_queue import QueueOption +from music_assistant.utils import get_external_ip, get_hostname, get_ip, json_serializer LOGGER = logging.getLogger("mass") @@ -138,7 +133,13 @@ class Web: self.mass.http_streamer.async_stream, allow_head=False, ), + web.get( + "/stream_media/{media_type}/{provider}/{item_id}", + self.mass.http_streamer.async_stream_media_item, + allow_head=False, + ), web.get("/", self.async_index), + web.post("/login", self.async_login), web.get("/jsonrpc.js", self.async_json_rpc), web.post("/jsonrpc.js", self.async_json_rpc), web.get("/ws", self.async_websocket_handler), @@ -222,7 +223,7 @@ class Web: "version": 1, } - @routes.post("/login") + @routes.post("/api/login") async def async_login(self, request): """Handle the retrieval of a JWT token.""" form = await request.json() @@ -388,7 +389,7 @@ class Web: async def async_get_image(self, request): """Get (resized) thumb image.""" media_type_str = request.match_info.get("media_type") - media_type = media_type_from_string(media_type_str) + media_type = MediaType.from_string(media_type_str) media_id = request.match_info.get("media_id") provider = request.rel_url.query.get("provider") if media_id is None or provider is None: @@ -529,7 +530,7 @@ class Web: @routes.post("/api/players/{player_id}/cmd/{cmd}") async def async_player_command(self, request): """Issue player command.""" - result = False + success = False player_id = request.match_info.get("player_id") cmd = request.match_info.get("cmd") try: @@ -538,11 +539,12 @@ class Web: cmd_args = None player_cmd = getattr(self.mass.player_manager, f"async_cmd_{cmd}", None) if player_cmd and cmd_args is not None: - result = await player_cmd(player_id, cmd_args) + success = await player_cmd(player_id, cmd_args) elif player_cmd: - result = await player_cmd(player_id) + success = await player_cmd(player_id) else: return web.Response(text="invalid command", status=501) + result = {"success": success in [True, None]} return web.json_response(result, dumps=json_serializer) @login_required @@ -553,12 +555,13 @@ class Web: player = self.mass.player_manager.get_player(player_id) if not player: return web.Response(status=404) - queue_opt = request.match_info.get("queue_opt", "play") + queue_opt = QueueOption(request.match_info.get("queue_opt", "play")) body = await request.json() media_items = await self.__async_media_items_from_body(body) - result = await self.mass.player_manager.async_play_media( + success = await self.mass.player_manager.async_play_media( player_id, media_items, queue_opt ) + result = {"success": success in [True, None]} return web.json_response(result, dumps=json_serializer) @login_required @@ -808,7 +811,10 @@ class Web: media_items = [] for item in data: media_item = await self.mass.music_manager.async_get_item( - item["item_id"], item["provider"], item["media_type"], lazy=True + item["item_id"], + item["provider"], + MediaType.from_string(item["media_type"]), + lazy=True, ) media_items.append(media_item) return media_items @@ -826,9 +832,9 @@ class Web: async for item in iterator: # write each item into the items object of the json if count: - json_response = "," + json.dumps(item, cls=EnhancedJSONEncoder) + json_response = "," + json_serializer(item) else: - json_response = json.dumps(item, cls=EnhancedJSONEncoder) + json_response = json_serializer(item) await resp.write(json_response.encode("utf-8")) count += 1 # write json close tag diff --git a/requirements.txt b/requirements.txt index c886d178..1a38037e 100755 --- a/requirements.txt +++ b/requirements.txt @@ -22,3 +22,4 @@ zeroconf==0.28.4 passlib==1.7.2 cryptography==3.1 python-vlc==3.0.11115 +mashumaro==1.12 diff --git a/wget-log b/wget-log new file mode 100644 index 00000000..e69de29b -- 2.34.1