diff --git a/music_assistant/server/controllers/media/albums.py b/music_assistant/server/controllers/media/albums.py
index 2ed149eff..dd4b86481 100644
--- a/music_assistant/server/controllers/media/albums.py
+++ b/music_assistant/server/controllers/media/albums.py
@@ -4,7 +4,7 @@
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 @@ async def get(
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 @@ async def get(
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 @@ async def get(
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 @@ async def add_item_to_library(self, item: Album, skip_metadata_lookup: bool = Fa
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(
diff --git a/music_assistant/server/controllers/media/artists.py b/music_assistant/server/controllers/media/artists.py
index 02ecc8b85..7890f01aa 100644
--- a/music_assistant/server/controllers/media/artists.py
+++ b/music_assistant/server/controllers/media/artists.py
@@ -56,20 +56,23 @@ def __init__(self, *args, **kwargs):
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(
diff --git a/music_assistant/server/controllers/media/base.py b/music_assistant/server/controllers/media/base.py
index d9ba03a34..4f1e66a80 100644
--- a/music_assistant/server/controllers/media/base.py
+++ b/music_assistant/server/controllers/media/base.py
@@ -46,9 +46,7 @@ def __init__(self, mass: MusicAssistant):
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 @@ async def get(
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 @@ async def get(
# 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 @@ async def _get_artist_mapping(self, artist: Artist | ItemMapping) -> ItemMapping
# 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
diff --git a/music_assistant/server/controllers/media/playlists.py b/music_assistant/server/controllers/media/playlists.py
index 5a202b629..aa4d3e6c6 100644
--- a/music_assistant/server/controllers/media/playlists.py
+++ b/music_assistant/server/controllers/media/playlists.py
@@ -53,9 +53,7 @@ def __init__(self, *args, **kwargs):
"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 @@ async def add_item_to_library(
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(
diff --git a/music_assistant/server/controllers/media/radio.py b/music_assistant/server/controllers/media/radio.py
index af0da213c..e3531844a 100644
--- a/music_assistant/server/controllers/media/radio.py
+++ b/music_assistant/server/controllers/media/radio.py
@@ -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 @@ async def versions(
# 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
diff --git a/music_assistant/server/controllers/media/tracks.py b/music_assistant/server/controllers/media/tracks.py
index ec251730e..e3d7174c8 100644
--- a/music_assistant/server/controllers/media/tracks.py
+++ b/music_assistant/server/controllers/media/tracks.py
@@ -3,6 +3,7 @@
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 @@ async def get(
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 @@ async def get(
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 @@ async def get(
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 @@ async def get(
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 @@ async def add_item_to_library(self, item: Track, skip_metadata_lookup: bool = Fa
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 @@ async def add_item_to_library(self, item: Track, skip_metadata_lookup: bool = Fa
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 @@ async def _set_track_album(self, db_id: int, album: Album, disc_number: int, tra
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):
diff --git a/music_assistant/server/controllers/metadata.py b/music_assistant/server/controllers/metadata.py
index d6b8c0524..66c2312bf 100755
--- a/music_assistant/server/controllers/metadata.py
+++ b/music_assistant/server/controllers/metadata.py
@@ -135,7 +135,7 @@ async def get_artist_metadata(self, artist: Artist) -> None:
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
diff --git a/music_assistant/server/controllers/streams.py b/music_assistant/server/controllers/streams.py
index 989bb4aa3..6b0ef846f 100644
--- a/music_assistant/server/controllers/streams.py
+++ b/music_assistant/server/controllers/streams.py
@@ -9,6 +9,7 @@
import asyncio
import logging
+import time
import urllib.parse
from collections.abc import AsyncGenerator
from contextlib import suppress
@@ -414,8 +415,11 @@ async def resolve_stream_url(
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(
diff --git a/music_assistant/server/helpers/didl_lite.py b/music_assistant/server/helpers/didl_lite.py
index f7cd3cdab..3c91f2c8c 100644
--- a/music_assistant/server/helpers/didl_lite.py
+++ b/music_assistant/server/helpers/didl_lite.py
@@ -65,7 +65,6 @@ def create_didl_metadata(
f"{album}"
f"{artist}"
f"{int(queue_item.duration)}"
- "Music Assistant"
f"{queue_item.queue_item_id}"
f"{escape_string(image_url)}"
"object.item.audioItem.audioBroadcast"
diff --git a/music_assistant/server/models/music_provider.py b/music_assistant/server/models/music_provider.py
index b68a8a9c5..c7f494972 100644
--- a/music_assistant/server/models/music_provider.py
+++ b/music_assistant/server/models/music_provider.py
@@ -410,13 +410,19 @@ async def sync_library(self, media_types: tuple[MediaType, ...]) -> None:
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
diff --git a/music_assistant/server/providers/filesystem_local/base.py b/music_assistant/server/providers/filesystem_local/base.py
index b7865c840..cc2bf1a96 100644
--- a/music_assistant/server/providers/filesystem_local/base.py
+++ b/music_assistant/server/providers/filesystem_local/base.py
@@ -271,7 +271,7 @@ async def browse(self, path: str) -> BrowseFolder:
# 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 @@ async def browse(self, path: str) -> BrowseFolder:
# 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 @@ async def sync_library(self, media_types: tuple[MediaType, ...]) -> None: # noq
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 @@ async def sync_library(self, media_types: tuple[MediaType, ...]) -> None: # noq
# 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 @@ async def _parse_track(
# 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:
diff --git a/music_assistant/server/providers/musicbrainz/__init__.py b/music_assistant/server/providers/musicbrainz/__init__.py
index cf72824ff..4afb0ced4 100644
--- a/music_assistant/server/providers/musicbrainz/__init__.py
+++ b/music_assistant/server/providers/musicbrainz/__init__.py
@@ -122,7 +122,8 @@ async def _search_artist_by_album(
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 @@ async def _search_artist_by_track(
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")
diff --git a/music_assistant/server/providers/plex/__init__.py b/music_assistant/server/providers/plex/__init__.py
index 585858b90..eedc7cda5 100644
--- a/music_assistant/server/providers/plex/__init__.py
+++ b/music_assistant/server/providers/plex/__init__.py
@@ -319,14 +319,16 @@ async def _parse_album(self, plex_album: PlexAlbum) -> Album:
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 @@ async def _parse_artist(self, plex_artist: PlexArtist) -> Artist:
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 @@ async def _parse_playlist(self, plex_playlist: PlexPlaylist) -> Playlist:
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 @@ async def _parse_track(
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(
diff --git a/music_assistant/server/providers/url/__init__.py b/music_assistant/server/providers/url/__init__.py
index abe85c53b..5f626b196 100644
--- a/music_assistant/server/providers/url/__init__.py
+++ b/music_assistant/server/providers/url/__init__.py
@@ -143,7 +143,7 @@ async def parse_item(
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
diff --git a/pyproject.toml b/pyproject.toml
index 15bec75d3..6dfc472be 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -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",
diff --git a/requirements_all.txt b/requirements_all.txt
index 1ffcea052..67a9cfc40 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -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