prettify json responses
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Tue, 15 Sep 2020 19:27:41 +0000 (21:27 +0200)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Tue, 15 Sep 2020 19:27:41 +0000 (21:27 +0200)
return human readable responses for all enum values

14 files changed:
music_assistant/database.py
music_assistant/http_streamer.py
music_assistant/models/config_entry.py
music_assistant/models/media_types.py
music_assistant/models/player.py
music_assistant/models/player_queue.py
music_assistant/models/provider.py
music_assistant/models/streamdetails.py
music_assistant/music_manager.py
music_assistant/providers/qobuz/__init__.py
music_assistant/utils.py
music_assistant/web.py
requirements.txt
wget-log [new file with mode: 0644]

index 2ce688754ebdf34811f7a17549d438c9f6128c64..c5f3609668223774984c2575ecb412712ef6ece2 100755 (executable)
@@ -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]
index 83983f445864b306f15ece2aeb4910f17a287ba7..db3c4e8b2c183c715e5c6b3f189654f19c3724ce 100755 (executable)
@@ -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)
index ef074e3bf516e08ce541e8f0c4eacd28a299fc65..0b000318064688cc1456175b8f4799d608151d78 100644 (file)
@@ -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
index 10f95de50aa638647933f473eda848c44e5c3fd1..7c8a3318c01089dc35b346e9760f9e5c4ac5e9c5 100755 (executable)
@@ -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)
index 8a90ef91bcbf04182101c09547b6cca6c862ab0b..25617769b10e3d698fe75250e5fef7b03048f38b 100755 (executable)
@@ -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
index b6aaef9d2479ed9e2deac40adc89bb7dfb6df398..348d766b28fc6e28359845323e0a9dcb388c9cbf 100755 (executable)
@@ -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 {
index 35530c191462ef10bc33e0ef5dc5461f22abadba..cd1f010129eca3f7337642eaed669e139b089edf 100644 (file)
@@ -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"
index 8f42bd31c1d494c0ce4f40e05b221d31e4dfc584..fa34426ef1fd2e0efd327e081d8dcb82ab15cb1a 100644 (file)
@@ -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
index 898b530d69b65f3eed98bb349ca5e8e07b39b45d..2d487e478d1a4b9b0a9c6758373fb2e97e977337 100755 (executable)
@@ -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)
index 23cffe93372fc9e3fe4e29b61750c7d9ae8c409c..0c6037a03a8e98e9c176c2989148517e840ec862 100644 (file)
@@ -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):
index 5b330a9250d3339fef15f7d1c66436d2918289bd..3a16feab329347b9fe636341992f46ac6fef4277 100755 (executable)
@@ -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
index 220dc8aa45fe8e380688aac9e06013d22f1f285a..765942d4e86148675f7080dfd1210474c50e950b 100755 (executable)
@@ -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
index c886d1780225a4c3923422cac3b0ccbf8aae97ae..1a38037ee6971a8bd0277516f67d2faf2240d5a8 100755 (executable)
@@ -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 (file)
index 0000000..e69de29