From: Marcel van der Veldt Date: Thu, 15 Aug 2024 18:30:32 +0000 (+0200) Subject: Various optimizations to filesystem metadata retrieval (#1569) X-Git-Url: https://git.kitaultman.com/?a=commitdiff_plain;h=e5d5328fcdb6ca32224237913bb5b0b348ec6d81;p=music-assistant-server.git Various optimizations to filesystem metadata retrieval (#1569) --- diff --git a/music_assistant/common/helpers/util.py b/music_assistant/common/helpers/util.py index 260dd821..0591c5ac 100644 --- a/music_assistant/common/helpers/util.py +++ b/music_assistant/common/helpers/util.py @@ -70,11 +70,11 @@ def try_parse_duration(duration_str: str) -> float: def create_sort_name(input_str: str) -> str: - """Create sort name/title from string.""" + """Create (basic/simple) sort name/title from string.""" input_str = input_str.lower().strip() - for item in ["the ", "de ", "les ", "dj ", ".", "-", "'", "`"]: + for item in ["the ", "de ", "les ", "dj ", "las ", "los ", "le ", "la ", "el ", "a ", "an "]: if input_str.startswith(item): - input_str = input_str.replace(item, "") + input_str = input_str.replace(item, "") + f", {item}" return input_str.strip() diff --git a/music_assistant/common/models/media_items.py b/music_assistant/common/models/media_items.py index e8742bda..85f1cf93 100644 --- a/music_assistant/common/models/media_items.py +++ b/music_assistant/common/models/media_items.py @@ -116,10 +116,22 @@ class ProviderMapping(DataClassDictMixin): def quality(self) -> int: """Return quality score.""" quality = self.audio_format.quality - if "filesystem" in self.provider_domain: - # always prefer local file over online media - quality += 1 - return quality + # append provider score so filebased providers are scored higher + return quality + self.priority + + @property + def priority(self) -> int: + """Return priority score to sort local providers before online.""" + if not (local_provs := get_global_cache_value("non_streaming_providers")): + # this is probably the client + return 0 + if TYPE_CHECKING: + local_provs = cast(set[str], local_provs) + if self.provider_domain in ("filesystem_local", "filesystem_smb"): + return 2 + if self.provider_instance in local_provs: + return 1 + return 0 def __hash__(self) -> int: """Return custom hash.""" @@ -251,8 +263,9 @@ class _MediaItemBase(DataClassDictMixin): provider: str # provider instance id or provider domain name: str version: str = "" - # sort_name and uri are auto generated, do not override unless really needed + # sort_name will be auto generated if omitted sort_name: str | None = None + # uri is auto generated, do not override unless really needed uri: str | None = None external_ids: set[tuple[ExternalID, str]] = field(default_factory=set) media_type: MediaType = MediaType.UNKNOWN diff --git a/music_assistant/constants.py b/music_assistant/constants.py index c949429e..9c2708d1 100644 --- a/music_assistant/constants.py +++ b/music_assistant/constants.py @@ -12,7 +12,7 @@ MASS_LOGGER_NAME: Final[str] = "music_assistant" UNKNOWN_ARTIST: Final[str] = "[unknown]" UNKNOWN_ARTIST_ID_MBID: Final[str] = "125ec42a-7229-4250-afc5-e057484327fe" VARIOUS_ARTISTS_NAME: Final[str] = "Various Artists" -VARIOUS_ARTISTS_ID_MBID: Final[str] = "89ad4ac3-39f7-470e-963a-56509c546377" +VARIOUS_ARTISTS_MBID: Final[str] = "89ad4ac3-39f7-470e-963a-56509c546377" RESOURCES_DIR: Final[pathlib.Path] = ( diff --git a/music_assistant/server/controllers/media/artists.py b/music_assistant/server/controllers/media/artists.py index 32be36f9..706789c0 100644 --- a/music_assistant/server/controllers/media/artists.py +++ b/music_assistant/server/controllers/media/artists.py @@ -27,7 +27,7 @@ from music_assistant.constants import ( DB_TABLE_ALBUM_ARTISTS, DB_TABLE_ARTISTS, DB_TABLE_TRACK_ARTISTS, - VARIOUS_ARTISTS_ID_MBID, + VARIOUS_ARTISTS_MBID, VARIOUS_ARTISTS_NAME, ) from music_assistant.server.controllers.media.base import MediaControllerBase @@ -311,8 +311,8 @@ class ArtistsController(MediaControllerBase[Artist]): item = self._artist_from_item_mapping(item) # enforce various artists name + id if compare_strings(item.name, VARIOUS_ARTISTS_NAME): - item.mbid = VARIOUS_ARTISTS_ID_MBID - if item.mbid == VARIOUS_ARTISTS_ID_MBID: + item.mbid = VARIOUS_ARTISTS_MBID + if item.mbid == VARIOUS_ARTISTS_MBID: item.name = VARIOUS_ARTISTS_NAME # no existing item matched: insert item new_item = await self.mass.music.database.insert( @@ -349,8 +349,8 @@ class ArtistsController(MediaControllerBase[Artist]): mbid = cur_item.mbid if (not mbid or overwrite) and getattr(update, "mbid", None): if compare_strings(update.name, VARIOUS_ARTISTS_NAME): - update.mbid = VARIOUS_ARTISTS_ID_MBID - if update.mbid == VARIOUS_ARTISTS_ID_MBID: + update.mbid = VARIOUS_ARTISTS_MBID + if update.mbid == VARIOUS_ARTISTS_MBID: update.name = VARIOUS_ARTISTS_NAME await self.mass.music.database.update( diff --git a/music_assistant/server/controllers/media/tracks.py b/music_assistant/server/controllers/media/tracks.py index 31a9e3d0..43a5e1f5 100644 --- a/music_assistant/server/controllers/media/tracks.py +++ b/music_assistant/server/controllers/media/tracks.py @@ -113,7 +113,7 @@ class TracksController(MediaControllerBase[Track]): ) except MusicAssistantError as err: # edge case where playlist track has invalid albumdetails - self.logger.warning("Unable to fetch album details %s - %s", track.album.uri, str(err)) + self.logger.warning("Unable to fetch album details for %s - %s", track.uri, str(err)) if not recursive: return track diff --git a/music_assistant/server/controllers/metadata.py b/music_assistant/server/controllers/metadata.py index eb25ad33..aea23337 100644 --- a/music_assistant/server/controllers/metadata.py +++ b/music_assistant/server/controllers/metadata.py @@ -46,7 +46,7 @@ from music_assistant.constants import ( DB_TABLE_ARTISTS, DB_TABLE_PLAYLISTS, DB_TABLE_TRACKS, - VARIOUS_ARTISTS_ID_MBID, + VARIOUS_ARTISTS_MBID, VARIOUS_ARTISTS_NAME, VERBOSE_LOG_LEVEL, ) @@ -417,9 +417,13 @@ class MetaDataController(CoreController): """Get/update rich metadata for an artist.""" # ensure the item is matched to all providers await self.mass.music.artists.match_providers(artist) - # collect metadata from all music providers first unique_keys: set[str] = set() - for prov_mapping in artist.provider_mappings: + # collect metadata from all music providers first + # note that we sort the providers by priority so that we always + # prefer local providers over online providers + for prov_mapping in sorted( + artist.provider_mappings, key=lambda x: x.priority, reverse=True + ): if (prov := self.mass.get_provider(prov_mapping.provider_instance)) is None: continue if prov.lookup_key in unique_keys: @@ -471,9 +475,11 @@ class MetaDataController(CoreController): """Get/update rich metadata for an album.""" # ensure the item is matched to all providers (will also get other quality versions) await self.mass.music.albums.match_providers(album) - # collect metadata from all music providers first unique_keys: set[str] = set() - for prov_mapping in album.provider_mappings: + # collect metadata from all music providers first + # note that we sort the providers by priority so that we always + # prefer local providers over online providers + for prov_mapping in sorted(album.provider_mappings, key=lambda x: x.priority, reverse=True): if (prov := self.mass.get_provider(prov_mapping.provider_instance)) is None: continue if prov.lookup_key in unique_keys: @@ -523,9 +529,11 @@ class MetaDataController(CoreController): """Get/update rich metadata for a track.""" # ensure the item is matched to all providers (will also get other quality versions) await self.mass.music.tracks.match_providers(track) - # collect metadata from all music providers first unique_keys: set[str] = set() - for prov_mapping in track.provider_mappings: + # collect metadata from all music providers first + # note that we sort the providers by priority so that we always + # prefer local providers over online providers + for prov_mapping in sorted(track.provider_mappings, key=lambda x: x.priority, reverse=True): if (prov := self.mass.get_provider(prov_mapping.provider_instance)) is None: continue if prov.lookup_key in unique_keys: @@ -648,7 +656,7 @@ class MetaDataController(CoreController): async def _get_artist_mbid(self, artist: Artist) -> str | None: """Fetch musicbrainz id by performing search using the artist name, albums and tracks.""" if compare_strings(artist.name, VARIOUS_ARTISTS_NAME): - return VARIOUS_ARTISTS_ID_MBID + return VARIOUS_ARTISTS_MBID ref_albums = await self.mass.music.artists.albums( artist.item_id, artist.provider, in_library_only=False ) diff --git a/music_assistant/server/controllers/music.py b/music_assistant/server/controllers/music.py index d50c0aa3..4c8d30f4 100644 --- a/music_assistant/server/controllers/music.py +++ b/music_assistant/server/controllers/music.py @@ -569,15 +569,19 @@ class MusicController(CoreController): available_providers = cast(set[str], available_providers) # fetch the first (available) provider item - for prov_mapping in media_item.provider_mappings: - if self.mass.get_provider(prov_mapping.provider_instance): - with suppress(MediaNotFoundError): - media_item = await ctrl.get_provider_item( - prov_mapping.item_id, prov_mapping.provider_instance, force_refresh=True - ) - provider = media_item.provider - item_id = media_item.item_id - break + for prov_mapping in sorted( + media_item.provider_mappings, key=lambda x: x.priority, reverse=True + ): + if not self.mass.get_provider(prov_mapping.provider_instance): + # ignore unavailable providers + continue + with suppress(MediaNotFoundError): + media_item = await ctrl.get_provider_item( + prov_mapping.item_id, prov_mapping.provider_instance, force_refresh=True + ) + provider = media_item.provider + item_id = media_item.item_id + break else: # try to find a substitute using search searchresult = await self.search(media_item.name, [media_item.media_type], 20) diff --git a/music_assistant/server/providers/filesystem_local/base.py b/music_assistant/server/providers/filesystem_local/base.py index 21b111e1..ba22b5ad 100644 --- a/music_assistant/server/providers/filesystem_local/base.py +++ b/music_assistant/server/providers/filesystem_local/base.py @@ -47,11 +47,13 @@ from music_assistant.constants import ( DB_TABLE_ARTISTS, DB_TABLE_PROVIDER_MAPPINGS, DB_TABLE_TRACK_ARTISTS, + VARIOUS_ARTISTS_MBID, VARIOUS_ARTISTS_NAME, ) +from music_assistant.server.controllers.cache import use_cache from music_assistant.server.helpers.compare import compare_strings, create_safe_string from music_assistant.server.helpers.playlists import parse_m3u, parse_pls -from music_assistant.server.helpers.tags import parse_tags, split_items +from music_assistant.server.helpers.tags import AudioTags, parse_tags, split_items from music_assistant.server.models.music_provider import MusicProvider from .helpers import get_album_dir, get_artist_dir, get_disc_dir @@ -444,20 +446,74 @@ class FileSystemProviderBase(MusicProvider): db_artist = await self.mass.music.artists.get_library_item_by_prov_id( prov_artist_id, self.instance_id ) - if db_artist is None: + if not db_artist: + # this should not be possible, but just in case msg = f"Artist not found: {prov_artist_id}" raise MediaNotFoundError(msg) + # prov_artist_id is either an actual (relative) path or a name (as fallback) + safe_artist_name = create_safe_string(prov_artist_id, lowercase=False, replace_space=False) if await self.exists(prov_artist_id): - # if path exists on disk allow parsing full details to allow refresh of metadata - return await self._parse_artist(db_artist.name, artist_path=prov_artist_id) - return db_artist + artist_path = prov_artist_id + elif await self.exists(safe_artist_name): + artist_path = safe_artist_name + else: + for prov_mapping in db_artist.provider_mappings: + if prov_mapping.provider_instance != self.instance_id: + continue + if prov_mapping.url: + artist_path = prov_mapping.url + break + else: + # this is an artist without an actual path on disk + # return the info we already have in the db + return db_artist + + artist = Artist( + item_id=prov_artist_id, + provider=self.instance_id, + name=db_artist.name, + sort_name=db_artist.sort_name, + provider_mappings={ + ProviderMapping( + item_id=prov_artist_id, + provider_domain=self.domain, + provider_instance=self.instance_id, + url=artist_path, + ) + }, + ) + # grab additional metadata within the Artist's folder + nfo_file = os.path.join(artist_path, "artist.nfo") + if await self.exists(nfo_file): + # found NFO file with metadata + # https://kodi.wiki/view/NFO_files/Artists + data = b"" + async for chunk in self.read_file_content(nfo_file): + data += chunk + info = await asyncio.to_thread(xmltodict.parse, data) + info = info["artist"] + artist.name = info.get("title", info.get("name", db_artist.name)) + if sort_name := info.get("sortname"): + artist.sort_name = sort_name + if mbid := info.get("musicbrainzartistid"): + artist.mbid = mbid + if description := info.get("biography"): + artist.metadata.description = description + if genre := info.get("genre"): + artist.metadata.genres = set(split_items(genre)) + # find local images + if images := await self._get_local_images(artist_path): + artist.metadata.images = UniqueList(images) + + return artist async def get_album(self, prov_album_id: str) -> Album: """Get full album details by id.""" for track in await self.get_album_tracks(prov_album_id): for prov_mapping in track.provider_mappings: if prov_mapping.provider_instance == self.instance_id: - full_track = await self.get_track(prov_mapping.item_id) + file_item = await self.resolve(prov_mapping.item_id) + full_track = await self._parse_track(file_item, full_album_metadata=True) assert isinstance(full_track.album, Album) return full_track.album msg = f"Album not found: {prov_album_id}" @@ -471,7 +527,7 @@ class FileSystemProviderBase(MusicProvider): raise MediaNotFoundError(msg) file_item = await self.resolve(prov_track_id) - return await self._parse_track(file_item) + return await self._parse_track(file_item, full_album_metadata=True) async def get_playlist(self, prov_playlist_id: str) -> Playlist: """Get full playlist details by id.""" @@ -693,7 +749,9 @@ class FileSystemProviderBase(MusicProvider): return file_item.local_path return file_item.absolute_path - async def _parse_track(self, file_item: FileSystemItem) -> Track: + async def _parse_track( + self, file_item: FileSystemItem, full_album_metadata: bool = False + ) -> Track: """Get full track details by id.""" # ruff: noqa: PLR0915, PLR0912 @@ -733,87 +791,28 @@ class FileSystemProviderBase(MusicProvider): if acoustid := tags.get("acoustidid"): track.external_ids.add((ExternalID.ACOUSTID, acoustid)) - album: Album | None = None - album_artists: list[Artist] = [] - # album - if tags.album: - # work out if we have an album and/or disc folder - # disc_dir is the folder level where the tracks are located - # this may be a separate disc folder (Disc 1, Disc 2 etc) underneath the album folder - # or this is an album folder with the disc attached - disc_dir = get_disc_dir(file_item.path, tags.album, tags.disc) - album_dir = get_album_dir(file_item.path, tags.album, disc_dir) - - # album artist(s) - if tags.album_artists: - for index, album_artist_str in enumerate(tags.album_artists): - artist = await self._parse_artist( - album_artist_str, - album_path=album_dir, - sort_name=( - tags.album_artist_sort_names[index] - if index < len(tags.album_artist_sort_names) - else None - ), - ) - if not artist.mbid: - with contextlib.suppress(IndexError): - artist.mbid = tags.musicbrainz_albumartistids[index] - album_artists.append(artist) - else: - # album artist tag is missing, determine fallback - fallback_action = self.config.get_value(CONF_MISSING_ALBUM_ARTIST_ACTION) - if fallback_action == "folder_name" and album_dir: - possible_artist_folder = os.path.dirname(album_dir) - self.logger.warning( - "%s is missing ID3 tag [albumartist], using foldername %s as fallback", - file_item.path, - possible_artist_folder, - ) - album_artist_str = possible_artist_folder.rsplit(os.sep)[-1] - album_artists = [await self._parse_artist(name=album_artist_str)] - # fallback to track artists (if defined by user) - elif fallback_action == "track_artist": - self.logger.warning( - "%s is missing ID3 tag [albumartist], using track artist(s) as fallback", - file_item.path, - ) - album_artists = [ - await self._parse_artist(name=track_artist_str) - for track_artist_str in tags.artists - ] - # all other: fallback to various artists - else: - self.logger.warning( - "%s is missing ID3 tag [albumartist], using %s as fallback", - file_item.path, - VARIOUS_ARTISTS_NAME, - ) - album_artists = [await self._parse_artist(name=VARIOUS_ARTISTS_NAME)] - - album = track.album = await self._parse_album( - tags.album, - track_path=file_item.path, - album_path=album_dir, - disc_path=disc_dir, - artists=album_artists, - barcode=tags.barcode, + album = track.album = ( + await self._parse_album( + track_path=file_item.path, track_tags=tags, full_metadata=full_album_metadata ) + if tags.album + else None + ) # track artist(s) for index, track_artist_str in enumerate(tags.artists): - # reuse album artist details if possible - if album_artist := next((x for x in album_artists if x.name == track_artist_str), None): - artist = album_artist - else: - artist = await self._parse_artist(track_artist_str) - if not artist.mbid: - with contextlib.suppress(IndexError): - artist.mbid = tags.musicbrainz_artistids[index] - # artist sort name - with contextlib.suppress(IndexError): - artist.sort_name = tags.artist_sort_names[index] + artist = await self._create_artist_itemmapping( + track_artist_str, + sort_name=( + tags.artist_sort_names[index] if index < len(tags.artist_sort_names) else None + ), + mbid=( + tags.musicbrainz_artistids[index] + if index < len(tags.musicbrainz_artistids) + else None + ), + ) track.artists.append(artist) # handle embedded cover image @@ -835,8 +834,7 @@ class FileSystemProviderBase(MusicProvider): if album and not album.metadata.images: # set embedded cover on album if it does not have one yet album.metadata.images = track.metadata.images - # copy album image from track (only if the album itself doesn't have an image) - # this deals with embedded images from filesystem providers + # copy (embedded) album image from track (if the album itself doesn't have an image) if album and not album.image and track.image: album.metadata.images = UniqueList([track.image]) @@ -855,150 +853,163 @@ class FileSystemProviderBase(MusicProvider): if tags.musicbrainz_recordingid: track.mbid = tags.musicbrainz_recordingid track.metadata.chapters = UniqueList(tags.chapters) - if album: - if not album.mbid and tags.musicbrainz_albumid: - album.mbid = tags.musicbrainz_albumid - if tags.musicbrainz_releasegroupid: - album.add_external_id(ExternalID.MB_RELEASEGROUP, tags.musicbrainz_releasegroupid) - if not album.year: - album.year = tags.year - album.album_type = tags.album_type - album.metadata.explicit = track.metadata.explicit return track - async def _parse_artist( + @use_cache(300) + async def _create_artist_itemmapping( self, name: str, - artist_path: str | None = None, album_path: str | None = None, sort_name: str | None = None, - ) -> Artist: - """Parse Artist metadata into an Artist object.""" - cache_key = f"{self.instance_id}-artistdata-{name}-{artist_path}" - if cache := await self.mass.cache.get(cache_key): - assert isinstance(cache, Artist) - return cache - if not artist_path and album_path: + mbid: str | None = None, + ) -> ItemMapping: + """Create ItemMapping for a track/album artist.""" + artist_path = None + if album_path: # try to find (album)artist folder based on album path artist_path = get_artist_dir(album_path=album_path, artist_name=name) + if not artist_path: + # check if we have an artist folder for this artist at root level + safe_artist_name = create_safe_string(name, lowercase=False, replace_space=False) + if await self.exists(name): + artist_path = name + elif await self.exists(safe_artist_name): + artist_path = safe_artist_name if not artist_path: # check if we have an existing item to retrieve the artist path async for item in self.mass.music.artists.iter_library_items(search=name): if not compare_strings(name, item.name): continue for prov_mapping in item.provider_mappings: - if prov_mapping.provider_instance == self.instance_id: + if prov_mapping.provider_instance != self.instance_id: + continue + if prov_mapping.url: artist_path = prov_mapping.url break - if artist_path: - break - else: - # check if we have an artist folder for this artist at root level - safe_artist_name = create_safe_string(name, lowercase=False, replace_space=False) - if await self.exists(name): - artist_path = name - elif await self.exists(safe_artist_name): - artist_path = safe_artist_name - - if artist_path: # noqa: SIM108 - # prefer the path as id - item_id = artist_path - else: - # simply use the album name as item id - item_id = name - artist = Artist( - item_id=item_id, + return ItemMapping( + media_type=MediaType.ARTIST, + # simply use the artist name as item id as fallback + item_id=artist_path or name, provider=self.instance_id, name=name, sort_name=sort_name, - provider_mappings={ - ProviderMapping( - item_id=item_id, - provider_domain=self.domain, - provider_instance=self.instance_id, - url=artist_path, - ) - }, + external_ids={(ExternalID.MB_ARTIST, mbid)} if mbid else set(), ) - if artist_path is None or not await self.exists(artist_path): - # return basic object if there is no dedicated artist folder - await self.mass.cache.set(cache_key, artist, expiration=120) - return artist - - nfo_file = os.path.join(artist_path, "artist.nfo") - if await self.exists(nfo_file): - # found NFO file with metadata - # https://kodi.wiki/view/NFO_files/Artists - data = b"" - async for chunk in self.read_file_content(nfo_file): - data += chunk - info = await asyncio.to_thread(xmltodict.parse, data) - info = info["artist"] - artist.name = info.get("title", info.get("name", name)) - if sort_name := info.get("sortname"): - artist.sort_name = sort_name - if mbid := info.get("musicbrainzartistid"): - artist.mbid = mbid - if description := info.get("biography"): - artist.metadata.description = description - if genre := info.get("genre"): - artist.metadata.genres = set(split_items(genre)) - # find local images - if images := await self._get_local_images(artist_path): - artist.metadata.images = UniqueList(images) - - await self.mass.cache.set(cache_key, artist, expiration=120) - return artist - async def _parse_album( - self, - name: str, - track_path: str, - album_path: str | None, - disc_path: str | None, - artists: list[Artist], - barcode: str | None = None, - sort_name: str | None = None, + self, track_path: str, track_tags: AudioTags, full_metadata: bool = False ) -> Album: - """Parse Album metadata into an Album object.""" - cache_key = f"{self.instance_id}-albumdata-{name}-{album_path}" - if cache := await self.mass.cache.get(cache_key): - assert isinstance(cache, Album) - return cache + """Parse Album metadata from Track tags.""" + assert track_tags.album + # work out if we have an album and/or disc folder + # disc_dir is the folder level where the tracks are located + # this may be a separate disc folder (Disc 1, Disc 2 etc) underneath the album folder + # or this is an album folder with the disc attached + disc_dir = get_disc_dir(track_path, track_tags.album, track_tags.disc) + album_dir = get_album_dir(track_path, track_tags.album, disc_dir) + + # album artist(s) + album_artists: UniqueList[Artist | ItemMapping] = UniqueList() + if track_tags.album_artists: + for index, album_artist_str in enumerate(track_tags.album_artists): + artist = await self._create_artist_itemmapping( + album_artist_str, + album_path=album_dir, + sort_name=( + track_tags.album_artist_sort_names[index] + if index < len(track_tags.album_artist_sort_names) + else None + ), + mbid=( + track_tags.musicbrainz_albumartistids[index] + if index < len(track_tags.musicbrainz_albumartistids) + else None + ), + ) + album_artists.append(artist) + else: + # album artist tag is missing, determine fallback + fallback_action = self.config.get_value(CONF_MISSING_ALBUM_ARTIST_ACTION) + if fallback_action == "folder_name" and album_dir: + possible_artist_folder = os.path.dirname(album_dir) + self.logger.warning( + "%s is missing ID3 tag [albumartist], using foldername %s as fallback", + track_path, + possible_artist_folder, + ) + album_artist_str = possible_artist_folder.rsplit(os.sep)[-1] + album_artists = UniqueList( + [await self._create_artist_itemmapping(name=album_artist_str)] + ) + # fallback to track artists (if defined by user) + elif fallback_action == "track_artist": + self.logger.warning( + "%s is missing ID3 tag [albumartist], using track artist(s) as fallback", + track_path, + ) + album_artists = UniqueList( + [ + await self._create_artist_itemmapping(name=track_artist_str) + for track_artist_str in track_tags.artists + ] + ) + # all other: fallback to various artists + else: + self.logger.warning( + "%s is missing ID3 tag [albumartist], using %s as fallback", + track_path, + VARIOUS_ARTISTS_NAME, + ) + album_artists = UniqueList( + [ + await self._create_artist_itemmapping( + name=VARIOUS_ARTISTS_NAME, mbid=VARIOUS_ARTISTS_MBID + ) + ] + ) - if album_path: + if album_dir: # noqa: SIM108 # prefer the path as id - item_id = album_path - elif artists: - # create fake item_id based on artist + album - item_id = artists[0].name + os.sep + name + item_id = album_dir else: - # simply use the album name as item id - album_path = name + # create fake item_id based on artist + album + item_id = album_artists[0].name + os.sep + track_tags.album + name, version = parse_title_and_version(track_tags.album) album = Album( item_id=item_id, provider=self.instance_id, name=name, - sort_name=sort_name, - artists=UniqueList(artists), + version=version, + sort_name=track_tags.album_sort, + artists=album_artists, provider_mappings={ ProviderMapping( item_id=item_id, provider_domain=self.domain, provider_instance=self.instance_id, - url=album_path, + url=album_dir, ) }, ) - if barcode: - album.external_ids.add((ExternalID.BARCODE, barcode)) + if track_tags.barcode: + album.external_ids.add((ExternalID.BARCODE, track_tags.barcode)) + + if track_tags.musicbrainz_albumid: + album.mbid = track_tags.musicbrainz_albumid + if track_tags.musicbrainz_releasegroupid: + album.add_external_id(ExternalID.MB_RELEASEGROUP, track_tags.musicbrainz_releasegroupid) + if track_tags.year: + album.year = track_tags.year + album.album_type = track_tags.album_type # hunt for additional metadata and images in the folder structure - extra_path = os.path.dirname(track_path) if (track_path and not album_path) else None - for folder_path in (disc_path, album_path, extra_path): + if not full_metadata: + return album + + extra_path = os.path.dirname(track_path) if (track_path and not album_dir) else None + for folder_path in (disc_dir, album_dir, extra_path): if not folder_path or not await self.exists(folder_path): continue nfo_file = os.path.join(folder_path, "album.nfo") @@ -1035,7 +1046,6 @@ class FileSystemProviderBase(MusicProvider): else: album.metadata.images += images - await self.mass.cache.set(cache_key, album, expiration=120) return album async def _get_local_images(self, folder: str) -> list[MediaItemImage]: diff --git a/music_assistant/server/providers/qobuz/__init__.py b/music_assistant/server/providers/qobuz/__init__.py index 10f5cfaf..c4d5dcd9 100644 --- a/music_assistant/server/providers/qobuz/__init__.py +++ b/music_assistant/server/providers/qobuz/__init__.py @@ -44,7 +44,7 @@ from music_assistant.common.models.streamdetails import StreamDetails from music_assistant.constants import ( CONF_PASSWORD, CONF_USERNAME, - VARIOUS_ARTISTS_ID_MBID, + VARIOUS_ARTISTS_MBID, VARIOUS_ARTISTS_NAME, ) @@ -501,7 +501,7 @@ class QobuzProvider(MusicProvider): }, ) if artist.item_id == VARIOUS_ARTISTS_ID: - artist.mbid = VARIOUS_ARTISTS_ID_MBID + artist.mbid = VARIOUS_ARTISTS_MBID artist.name = VARIOUS_ARTISTS_NAME if img := self.__get_image(artist_obj): artist.metadata.images = [