Several (small) Bugfixes (#817)
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Thu, 3 Aug 2023 00:52:44 +0000 (02:52 +0200)
committerGitHub <noreply@github.com>
Thu, 3 Aug 2023 00:52:44 +0000 (02:52 +0200)
* Do not add unavailable items in sync

* remove unneeded playlisttitle

* Fix single track repeat in non-flow mode

* fix some typos

* add album tracks to library

* bump frontend to 2.0.10

* fix typo

16 files changed:
music_assistant/server/controllers/media/albums.py
music_assistant/server/controllers/media/artists.py
music_assistant/server/controllers/media/base.py
music_assistant/server/controllers/media/playlists.py
music_assistant/server/controllers/media/radio.py
music_assistant/server/controllers/media/tracks.py
music_assistant/server/controllers/metadata.py
music_assistant/server/controllers/streams.py
music_assistant/server/helpers/didl_lite.py
music_assistant/server/models/music_provider.py
music_assistant/server/providers/filesystem_local/base.py
music_assistant/server/providers/musicbrainz/__init__.py
music_assistant/server/providers/plex/__init__.py
music_assistant/server/providers/url/__init__.py
pyproject.toml
requirements_all.txt

index 2ed149eff6a9da8a6b3f0b1f177a40181e9b8ffe..dd4b86481a3d9dedd5ab631797caff8a7b0a6021 100644 (file)
@@ -4,7 +4,7 @@ from __future__ import annotations
 import asyncio
 import contextlib
 from random import choice, random
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, Any
 
 from music_assistant.common.helpers.datetime import utc_timestamp
 from music_assistant.common.helpers.json import serialize_to_json
@@ -65,7 +65,7 @@ class AlbumsController(MediaControllerBase[Album]):
         lazy: bool = True,
         details: Album | ItemMapping = None,
         add_to_library: bool = False,
-        skip_metadata_lookup: bool = False,
+        **kwargs: dict[str, Any],
     ) -> Album:
         """Return (full) details for a single media item."""
         album = await super().get(
@@ -75,7 +75,7 @@ class AlbumsController(MediaControllerBase[Album]):
             lazy=lazy,
             details=details,
             add_to_library=add_to_library,
-            skip_metadata_lookup=skip_metadata_lookup,
+            **kwargs,
         )
         # append full artist details to full album item
         album.artists = [
@@ -85,12 +85,19 @@ class AlbumsController(MediaControllerBase[Album]):
                 lazy=lazy,
                 details=item,
                 add_to_library=add_to_library,
+                **kwargs,
             )
             for item in album.artists
         ]
         return album
 
-    async def add_item_to_library(self, item: Album, skip_metadata_lookup: bool = False) -> Album:
+    async def add_item_to_library(
+        self,
+        item: Album,
+        metadata_lookup: bool = True,
+        add_album_tracks: bool = True,
+        **kwargs: dict[str, Any],  # noqa: ARG002
+    ) -> Album:
         """Add album to library and return the database item."""
         if not isinstance(item, Album):
             raise InvalidDataError("Not a valid Album object (ItemMapping can not be added to db)")
@@ -108,24 +115,24 @@ class AlbumsController(MediaControllerBase[Album]):
         if not item.artists:
             raise InvalidDataError("Album is missing artist(s)")
         # grab additional metadata
-        if not skip_metadata_lookup:
+        if not metadata_lookup:
             await self.mass.metadata.get_album_metadata(item)
         # actually add (or update) the item in the library db
         # use the lock to prevent a race condition of the same item being added twice
         async with self._db_add_lock:
             library_item = await self._add_library_item(item)
         # also fetch the same album on all providers
-        if not skip_metadata_lookup:
+        if not metadata_lookup:
             await self._match(library_item)
             library_item = await self.get_library_item(library_item.item_id)
         # also add album tracks
-        if not skip_metadata_lookup and item.provider != "library":
+        if add_album_tracks and item.provider != "library":
             async with asyncio.TaskGroup() as tg:
                 for track in await self._get_provider_album_tracks(item.item_id, item.provider):
                     track.album = library_item
                     tg.create_task(
                         self.mass.music.tracks.add_item_to_library(
-                            track, skip_metadata_lookup=skip_metadata_lookup
+                            track, metadata_lookup=metadata_lookup
                         )
                     )
         self.mass.signal_event(
index 02ecc8b856bf1afe8773885204b56d10b98e2ca0..7890f01aa4560de2874f9d3a46b15dd579d79ce2 100644 (file)
@@ -56,20 +56,23 @@ class ArtistsController(MediaControllerBase[Artist]):
         self.mass.register_api_command("music/artists/artist_tracks", self.tracks)
 
     async def add_item_to_library(
-        self, item: Artist | ItemMapping, skip_metadata_lookup: bool = False
+        self,
+        item: Artist | ItemMapping,
+        metadata_lookup: bool = True,
+        **kwargs: dict[str, Any],  # noqa: ARG002
     ) -> Artist:
         """Add artist to library and return the database item."""
         if isinstance(item, ItemMapping):
-            skip_metadata_lookup = True
+            metadata_lookup = True
         # grab musicbrainz id and additional metadata
-        if not skip_metadata_lookup:
+        if not metadata_lookup:
             await self.mass.metadata.get_artist_metadata(item)
         # actually add (or update) the item in the library db
         # use the lock to prevent a race condition of the same item being added twice
         async with self._db_add_lock:
             library_item = await self._add_library_item(item)
         # also fetch same artist on all providers
-        if not skip_metadata_lookup:
+        if not metadata_lookup:
             await self.match_artist(library_item)
             library_item = await self.get_library_item(library_item.item_id)
         self.mass.signal_event(
index d9ba03a34ceb87070e06f0eaf50718a5db744936..4f1e66a80dc1da8ad96930308f7034d5b0e6771b 100644 (file)
@@ -46,9 +46,7 @@ class MediaControllerBase(Generic[ItemCls], metaclass=ABCMeta):
         self.logger = logging.getLogger(f"{ROOT_LOGGER_NAME}.music.{self.media_type.value}")
 
     @abstractmethod
-    async def add_item_to_library(
-        self, item: ItemCls, skip_metadata_lookup: bool = False
-    ) -> ItemCls:
+    async def add_item_to_library(self, item: ItemCls, **kwargs: dict[str, Any]) -> ItemCls:
         """Add item to library and return the database item."""
         raise NotImplementedError
 
@@ -155,7 +153,7 @@ class MediaControllerBase(Generic[ItemCls], metaclass=ABCMeta):
         lazy: bool = True,
         details: ItemCls = None,
         add_to_library: bool = False,
-        skip_metadata_lookup: bool = False,
+        **kwargs: dict[str, Any],
     ) -> ItemCls:
         """Return (full) details for a single media item."""
         if provider_instance_id_or_domain == "database":
@@ -206,10 +204,7 @@ class MediaControllerBase(Generic[ItemCls], metaclass=ABCMeta):
         # we can set lazy to false and we await the job to complete.
         task_id = f"add_{self.media_type.value}.{details.provider}.{details.item_id}"
         add_task = self.mass.create_task(
-            self.add_item_to_library,
-            item=details,
-            skip_metadata_lookup=skip_metadata_lookup,
-            task_id=task_id,
+            self.add_item_to_library, item=details, task_id=task_id, **kwargs
         )
         if not lazy:
             await add_task
@@ -691,7 +686,7 @@ class MediaControllerBase(Generic[ItemCls], metaclass=ABCMeta):
         # try to request the full item
         with suppress(MediaNotFoundError, AssertionError, InvalidDataError):
             db_artist = await self.mass.music.artists.add_item_to_library(
-                artist, skip_metadata_lookup=True
+                artist, metadata_lookup=False
             )
             return ItemMapping.from_item(db_artist)
         # fallback to just the provider item
index 5a202b629a9e8b01fc63a632e8d12a6df1ffbc0d..aa4d3e6c661af871afbcec344d8c73ff01d28f66 100644 (file)
@@ -53,9 +53,7 @@ class PlaylistController(MediaControllerBase[Playlist]):
             "music/playlists/remove_playlist_tracks", self.remove_playlist_tracks
         )
 
-    async def add_item_to_library(
-        self, item: Playlist, skip_metadata_lookup: bool = False
-    ) -> Playlist:
+    async def add_item_to_library(self, item: Playlist, metadata_lookup: bool = True) -> Playlist:
         """Add playlist to library and return the new database item."""
         if not isinstance(item, Playlist):
             raise InvalidDataError(
@@ -72,7 +70,7 @@ class PlaylistController(MediaControllerBase[Playlist]):
         async for _ in self.tracks(item.item_id, item.provider):
             pass
         # metadata lookup we need to do after adding it to the db
-        if not skip_metadata_lookup:
+        if not metadata_lookup:
             await self.mass.metadata.get_playlist_metadata(library_item)
             library_item = await self.update_item_in_library(library_item.item_id, library_item)
         self.mass.signal_event(
index af0da213c91c86129c022b5e2aa551fbb95af1a8..e3531844a63a86e2ad240505b5be4b3a78e7c490 100644 (file)
@@ -2,6 +2,7 @@
 from __future__ import annotations
 
 import asyncio
+from typing import Any
 
 from music_assistant.common.helpers.datetime import utc_timestamp
 from music_assistant.common.helpers.json import serialize_to_json
@@ -62,13 +63,15 @@ class RadioController(MediaControllerBase[Radio]):
         # return the aggregated result
         return all_versions.values()
 
-    async def add_item_to_library(self, item: Radio, skip_metadata_lookup: bool = False) -> Radio:
+    async def add_item_to_library(
+        self, item: Radio, metadata_lookup: bool = True, **kwargs: dict[str, Any]  # noqa: ARG002
+    ) -> Radio:
         """Add radio to library and return the new database item."""
         if not isinstance(item, Radio):
             raise InvalidDataError("Not a valid Radio object (ItemMapping can not be added to db)")
         if not item.provider_mappings:
             raise InvalidDataError("Radio is missing provider mapping(s)")
-        if not skip_metadata_lookup:
+        if not metadata_lookup:
             await self.mass.metadata.get_radio_metadata(item)
         # actually add (or update) the item in the library db
         # use the lock to prevent a race condition of the same item being added twice
index ec251730e4518fc7184f1f7ba62eb00bd37e35fe..e3d7174c86be166a5d75da16e31149fdd62e006f 100644 (file)
@@ -3,6 +3,7 @@ from __future__ import annotations
 
 import asyncio
 import urllib.parse
+from typing import Any
 
 from music_assistant.common.helpers.datetime import utc_timestamp
 from music_assistant.common.helpers.json import serialize_to_json
@@ -63,7 +64,7 @@ class TracksController(MediaControllerBase[Track]):
         details: Track = None,
         album_uri: str | None = None,
         add_to_library: bool = False,
-        skip_metadata_lookup: bool = False,
+        **kwargs: dict[str, Any],
     ) -> Track:
         """Return (full) details for a single media item."""
         track = await super().get(
@@ -73,7 +74,7 @@ class TracksController(MediaControllerBase[Track]):
             lazy=lazy,
             details=details,
             add_to_library=add_to_library,
-            skip_metadata_lookup=skip_metadata_lookup,
+            **kwargs,
         )
         # append full album details to full track item
         try:
@@ -86,7 +87,7 @@ class TracksController(MediaControllerBase[Track]):
                     lazy=lazy,
                     details=None if isinstance(track.album, ItemMapping) else track.album,
                     add_to_library=add_to_library,
-                    skip_metadata_lookup=skip_metadata_lookup,
+                    **kwargs,
                 )
             elif provider_instance_id_or_domain == "library":
                 # grab the first album this track is attached to
@@ -116,13 +117,15 @@ class TracksController(MediaControllerBase[Track]):
                     lazy=lazy,
                     details=None if isinstance(artist, ItemMapping) else artist,
                     add_to_library=add_to_library,
-                    skip_metadata_lookup=skip_metadata_lookup,
+                    **kwargs,
                 )
             )
         track.artists = full_artists
         return track
 
-    async def add_item_to_library(self, item: Track, skip_metadata_lookup: bool = False) -> Track:
+    async def add_item_to_library(
+        self, item: Track, metadata_lookup: bool = True, **kwargs: dict[str, Any]  # noqa: ARG002
+    ) -> Track:
         """Add track to library and return the new database item."""
         if not isinstance(item, Track):
             raise InvalidDataError("Not a valid Track object (ItemMapping can not be added to db)")
@@ -160,7 +163,7 @@ class TracksController(MediaControllerBase[Track]):
                 for artist in item.album.artists
             ]
         # grab additional metadata
-        if not skip_metadata_lookup:
+        if not metadata_lookup:
             await self.mass.metadata.get_track_metadata(item)
         # fallback track image from album (only if albumtype = single)
         if (
@@ -177,7 +180,7 @@ class TracksController(MediaControllerBase[Track]):
         async with self._db_add_lock:
             library_item = await self._add_library_item(item)
         # also fetch same track on all providers (will also get other quality versions)
-        if not skip_metadata_lookup:
+        if not metadata_lookup:
             await self._match(library_item)
             library_item = await self.get_library_item(library_item.item_id)
         self.mass.signal_event(
@@ -448,7 +451,8 @@ class TracksController(MediaControllerBase[Track]):
                 lazy=False,
                 details=album,
                 add_to_library=True,
-                skip_metadata_lookup=True,
+                metadata_lookup=False,
+                add_album_tracks=False,
             )
         album_mapping = {"track_id": db_id, "album_id": int(db_album.item_id)}
         if db_row := await self.mass.music.database.get_row(DB_TABLE_ALBUM_TRACKS, album_mapping):
index d6b8c0524b8d6caf0f826ff5b82941321f4a6787..66c2312bf4f7ad923a5ef775b51bf1124183ea97 100755 (executable)
@@ -135,7 +135,7 @@ class MetaDataController(CoreController):
 
     async def get_album_metadata(self, album: Album) -> None:
         """Get/update rich metadata for an album."""
-        # ensure the album has a musicbrainz id or artist
+        # ensure the album has a musicbrainz id or artist(s)
         if not (album.mbid or album.artists):
             return
         # collect metadata from all providers
index 989bb4aa37eb5279c7c1309bd897b612f203d1df..6b0ef846f642609e5f8a582bdf371343540b57a9 100644 (file)
@@ -9,6 +9,7 @@ from __future__ import annotations
 
 import asyncio
 import logging
+import time
 import urllib.parse
 from collections.abc import AsyncGenerator
 from contextlib import suppress
@@ -414,8 +415,11 @@ class StreamsController(CoreController):
             query_params["seek_position"] = str(seek_position)
         if fade_in:
             query_params["fade_in"] = "1"
-        if query_params:
-            url += "?" + urllib.parse.urlencode(query_params)
+        # we add a timestamp as basic checksum
+        # most importantly this is to invalidate any caches
+        # but also to handle edge cases such as single track repeat
+        query_params["ts"] = str(int(time.time()))
+        url += "?" + urllib.parse.urlencode(query_params)
         return url
 
     async def create_multi_client_stream_job(
index f7cd3cdab220caf09ff50710cbb6a5ff3d035d14..3c91f2c8c2069d5fd96a3b0dd099e35c98f7800f 100644 (file)
@@ -65,7 +65,6 @@ def create_didl_metadata(
         f"<upnp:album>{album}</upnp:album>"
         f"<upnp:artist>{artist}</upnp:artist>"
         f"<upnp:duration>{int(queue_item.duration)}</upnp:duration>"
-        "<upnp:playlistTitle>Music Assistant</upnp:playlistTitle>"
         f"<dc:queueItemId>{queue_item.queue_item_id}</dc:queueItemId>"
         f"<upnp:albumArtURI>{escape_string(image_url)}</upnp:albumArtURI>"
         "<upnp:class>object.item.audioItem.audioBroadcast</upnp:class>"
index b68a8a9c5ee693081d61a63541c512a5633166a2..c7f4949724be324a27bfc77ca90fb191dd71739e 100644 (file)
@@ -410,13 +410,19 @@ class MusicProvider(Provider):
                     prov_item.provider_mappings,
                 )
                 try:
+                    if not library_item and not prov_item.available:
+                        # skip unavailable tracks
+                        self.logger.debg(
+                            "Skipping sync of item %s because it is unavailable", prov_item.uri
+                        )
+                        continue
                     if not library_item:
                         # create full db item
                         # note that we skip the metadata lookup purely to speed up the sync
                         # the additional metadata is then lazy retrieved afterwards
                         prov_item.favorite = True
                         library_item = await controller.add_item_to_library(
-                            prov_item, skip_metadata_lookup=True
+                            prov_item, metadata_lookup=False
                         )
                     elif (
                         library_item.metadata.checksum and prov_item.metadata.checksum
index b7865c840448c86ba1ad6070dd2f511a0e033f36..cc2bf1a96cf393b2e909565fd537c11f42cb744a 100644 (file)
@@ -271,7 +271,7 @@ class FileSystemProviderBase(MusicProvider):
                     # make sure that the item exists
                     # https://github.com/music-assistant/hass-music-assistant/issues/707
                     library_item = await self.mass.music.tracks.add_item_to_library(
-                        track, skip_metadata_lookup=True
+                        track, metadata_lookup=False
                     )
                     subitems.append(library_item)
                 continue
@@ -284,7 +284,7 @@ class FileSystemProviderBase(MusicProvider):
                     # make sure that the item exists
                     # https://github.com/music-assistant/hass-music-assistant/issues/707
                     library_item = await self.mass.music.playlists.add_item_to_library(
-                        playlist, skip_metadata_lookup=True
+                        playlist, metadata_lookup=False
                     )
                     subitems.append(library_item)
                 continue
@@ -345,9 +345,7 @@ class FileSystemProviderBase(MusicProvider):
                 if item.ext in TRACK_EXTENSIONS:
                     # add/update track to db
                     track = await self._parse_track(item)
-                    await self.mass.music.tracks.add_item_to_library(
-                        track, skip_metadata_lookup=True
-                    )
+                    await self.mass.music.tracks.add_item_to_library(track, metadata_lookup=False)
                 elif item.ext in PLAYLIST_EXTENSIONS:
                     playlist = await self.get_playlist(item.path)
                     # add/update] playlist to db
@@ -355,7 +353,7 @@ class FileSystemProviderBase(MusicProvider):
                     # playlist is always in-library
                     playlist.favorite = True
                     await self.mass.music.playlists.add_item_to_library(
-                        playlist, skip_metadata_lookup=True
+                        playlist, metadata_lookup=False
                     )
             except Exception as err:  # pylint: disable=broad-except
                 # we don't want the whole sync to crash on one file so we catch all exceptions here
@@ -746,7 +744,7 @@ class FileSystemProviderBase(MusicProvider):
             # much space and bandwidth. Instead we set the filename as value so the image can
             # be retrieved later in realtime.
             track.metadata.images = [
-                MediaItemImage(type=ImageType.THUMB, url=file_item.path, provider=self.instance_id)
+                MediaItemImage(type=ImageType.THUMB, path=file_item.path, provider=self.instance_id)
             ]
 
         if track.album and not track.album.metadata.images:
index cf72824ffa2503875b1470bf4246d7b9bf73cf1e..4afb0ced477f5e01048852ffd72088c476a2e05f 100644 (file)
@@ -122,7 +122,8 @@ class MusicbrainzProvider(MetadataProvider):
         album_barcode: str | None = None,
     ) -> str | None:
         """Retrieve musicbrainz artist id by providing the artist name and albumname or barcode."""
-        assert albumname or album_barcode
+        if not (albumname or album_barcode):
+            return None  # may not happen, but guard just in case
         for searchartist in (
             artistname,
             re.sub(LUCENE_SPECIAL, r"\\\1", artistname),
@@ -159,7 +160,8 @@ class MusicbrainzProvider(MetadataProvider):
         track_isrc: str | None = None,
     ) -> str | None:
         """Retrieve artist id by providing the artist name and trackname or track isrc."""
-        assert trackname or track_isrc
+        if not (trackname or track_isrc):
+            return None  # may not happen, but guard just in case
         searchartist = re.sub(LUCENE_SPECIAL, r"\\\1", artistname)
         if track_isrc:
             result = await self.get_data(f"isrc/{track_isrc}", inc="artist-credits")
index 585858b90392f3e5d9a104cc96a916cac201f2dc..eedc7cda593f5e2af376cdc3551a11db55338a56 100644 (file)
@@ -319,14 +319,16 @@ class PlexProvider(MusicProvider):
             album.year = plex_album.year
         if thumb := plex_album.firstAttr("thumb", "parentThumb", "grandparentThumb"):
             album.metadata.images = [
-                MediaItemImage(type=ImageType.THUMB, url=thumb, provider=self.instance_id)
+                MediaItemImage(type=ImageType.THUMB, path=thumb, provider=self.instance_id)
             ]
         if plex_album.summary:
             album.metadata.description = plex_album.summary
 
         album.artists.append(
             self._get_item_mapping(
-                type=MediaType.ARTIST, url=plex_album.parentKey, provider=plex_album.parentTitle
+                media_type=MediaType.ARTIST,
+                url=plex_album.parentKey,
+                provider=plex_album.parentTitle,
             )
         )
         return album
@@ -353,7 +355,7 @@ class PlexProvider(MusicProvider):
             artist.metadata.description = plex_artist.summary
         if thumb := plex_artist.firstAttr("thumb", "parentThumb", "grandparentThumb"):
             artist.metadata.images = [
-                MediaItemImage(type=ImageType.THUMB, path=thumb, url=self.instance_id)
+                MediaItemImage(type=ImageType.THUMB, path=thumb, provider=self.instance_id)
             ]
         return artist
 
@@ -376,7 +378,7 @@ class PlexProvider(MusicProvider):
             playlist.metadata.description = plex_playlist.summary
         if thumb := plex_playlist.firstAttr("thumb", "parentThumb", "grandparentThumb"):
             playlist.metadata.images = [
-                MediaItemImage(type=ImageType.THUMB, url=thumb, provider=self.instance_id)
+                MediaItemImage(type=ImageType.THUMB, path=thumb, provider=self.instance_id)
             ]
         playlist.is_editable = True
         return playlist
@@ -438,7 +440,7 @@ class PlexProvider(MusicProvider):
 
         if thumb := plex_track.firstAttr("thumb", "parentThumb", "grandparentThumb"):
             track.metadata.images = [
-                MediaItemImage(type=ImageType.THUMB, url=thumb, provider=self.instance_id)
+                MediaItemImage(type=ImageType.THUMB, path=thumb, provider=self.instance_id)
             ]
         if plex_track.parentKey:
             track.album = self._get_item_mapping(
index abe85c53b6e3da5ecbbb4094873e274d144c5637..5f626b1969f3583884613b846d0e46758b5f1add 100644 (file)
@@ -143,7 +143,7 @@ class URLProvider(MusicProvider):
 
         if media_info.has_cover_image:
             media_item.metadata.images = [
-                MediaItemImage(type=ImageType.THUMB, path=url, provider="embedded")
+                MediaItemImage(type=ImageType.THUMB, path=url, provider="file")
             ]
         return media_item
 
index 15bec75d3bda03ec600df3b32cc9237bfc85a1da..6dfc472beec3ccadbf710b5ca0fbbac806bbf181 100644 (file)
@@ -37,7 +37,7 @@ server = [
   "python-slugify==8.0.1",
   "mashumaro==3.8.1",
   "memory-tempfile==2.2.3",
-  "music-assistant-frontend==2.0.9",
+  "music-assistant-frontend==2.0.10",
   "pillow==10.0.0",
   "unidecode==1.3.6",
   "xmltodict==0.13.0",
index 1ffcea052ebd15915a80dd24a5e6e7e07d49d667..67a9cfc40c34d89b972872d8142b8bdb10a49fa5 100644 (file)
@@ -18,7 +18,7 @@ git+https://github.com/gieljnssns/python-radios.git@main
 ifaddr==0.2.0
 mashumaro==3.8.1
 memory-tempfile==2.2.3
-music-assistant-frontend==2.0.9
+music-assistant-frontend==2.0.10
 orjson==3.9.2
 pillow==10.0.0
 plexapi==4.15.0