ATTR_PREVIOUS_VOLUME: Final[str] = "previous_volume"
ATTR_LAST_POLL: Final[str] = "last_poll"
ATTR_GROUP_MEMBERS: Final[str] = "group_members"
+
+# Album type detection patterns
+LIVE_INDICATORS = [
+ r"\bunplugged\b",
+ r"\bin concert\b",
+ r"\bon stage\b",
+ r"\blive\b",
+]
+
+SOUNDTRACK_INDICATORS = [
+ r"\bsoundtrack\b", # Catches all soundtrack variations
+ r"\bmusic from the motion picture\b",
+ r"\boriginal score\b",
+ r"\bthe score\b",
+ r"\bfilm score\b",
+ r"(^|\b)score:\s*", # e.g., "Score: The Two Towers"
+ r"\bfrom the film\b",
+ r"\boriginal.*cast.*recording\b",
+]
from music_assistant.constants import MASS_LOGGER_NAME, UNKNOWN_ARTIST
from music_assistant.helpers.json import json_loads
from music_assistant.helpers.process import AsyncProcess
-from music_assistant.helpers.util import try_parse_int
+from music_assistant.helpers.util import infer_album_type, try_parse_int
LOGGER = logging.getLogger(f"{MASS_LOGGER_NAME}.tags")
"""Return albumtype tag if present."""
if self.tags.get("compilation", "") == "1":
return AlbumType.COMPILATION
+
tag = (
self.tags.get("musicbrainzalbumtype")
or self.tags.get("albumtype")
or self.tags.get("releasetype")
)
- if tag is None:
- return AlbumType.UNKNOWN
- # the album type tag is messy within id3 and may even contain multiple types
- # try to parse one in order of preference
- for album_type in (
- AlbumType.COMPILATION,
- AlbumType.EP,
- AlbumType.SINGLE,
- AlbumType.ALBUM,
- ):
- if album_type.value in tag.lower():
- return album_type
-
- return AlbumType.UNKNOWN
+
+ if tag is not None:
+ # try to parse one in order of preference
+ for album_type in (
+ AlbumType.LIVE,
+ AlbumType.SOUNDTRACK,
+ AlbumType.COMPILATION,
+ AlbumType.EP,
+ AlbumType.SINGLE,
+ AlbumType.ALBUM,
+ ):
+ if album_type.value in tag.lower():
+ return album_type
+
+ # No valid tag found, try inference from album title
+ album_title = self.tags.get("album", "")
+ return infer_album_type(album_title, "")
@property
def isrc(self) -> tuple[str, ...]:
if stream.get("codec_type") == "video":
continue
for key, value in stream.get("tags", {}).items():
- alt_key = key.lower().replace(" ", "").replace("_", "").replace("-", "")
+ alt_key = key.lower()
+ for char in [" ", "_", "-", "/"]:
+ alt_key = alt_key.replace(char, "")
if alt_key in tags:
continue
tags[alt_key] = value
import shutil
import socket
import urllib.error
-import urllib.parse
import urllib.request
from collections.abc import AsyncGenerator, AsyncIterator, Awaitable, Callable, Coroutine
from contextlib import suppress
import chardet
import ifaddr
+from music_assistant_models.enums import AlbumType
from zeroconf import IPVersion
-from music_assistant.constants import VERBOSE_LOG_LEVEL
+from music_assistant.constants import LIVE_INDICATORS, SOUNDTRACK_INDICATORS, VERBOSE_LOG_LEVEL
from music_assistant.helpers.process import check_output
if TYPE_CHECKING:
return title, version
+def infer_album_type(title: str, version: str) -> AlbumType:
+ """Infer album type by looking for live or soundtrack indicators."""
+ combined = f"{title} {version}".lower()
+ for pat in LIVE_INDICATORS:
+ if re.search(pat, combined):
+ return AlbumType.LIVE
+ for pat in SOUNDTRACK_INDICATORS:
+ if re.search(pat, combined):
+ return AlbumType.SOUNDTRACK
+ return AlbumType.UNKNOWN
+
+
def strip_ads(line: str) -> str:
"""Strip Ads from line."""
if ad_pattern.search(line):
from music_assistant.helpers.app_vars import app_var
from music_assistant.helpers.auth import AuthenticationHelper
from music_assistant.helpers.datetime import utc_timestamp
+from music_assistant.helpers.util import infer_album_type
from music_assistant.models import ProviderInstanceType
from music_assistant.models.music_provider import MusicProvider
def get_album_type(self, album: deezer.Album) -> AlbumType:
"""Read and convert the Deezer album type."""
- if not hasattr(album, "record_type"):
- return AlbumType.UNKNOWN
-
- match album.record_type:
- case "album":
- return AlbumType.ALBUM
- case "single":
- return AlbumType.SINGLE
- case "ep":
- return AlbumType.EP
- case "compile":
- return AlbumType.COMPILATION
- case _:
- return AlbumType.UNKNOWN
+ # Get provider's basic type first
+ provider_type = AlbumType.UNKNOWN
+ if hasattr(album, "record_type"):
+ match album.record_type:
+ case "album":
+ provider_type = AlbumType.ALBUM
+ case "single":
+ provider_type = AlbumType.SINGLE
+ case "ep":
+ provider_type = AlbumType.EP
+ case "compile":
+ provider_type = AlbumType.COMPILATION
+
+ # Try inference - override if it finds something more specific
+ inferred_type = infer_album_type(album.title, "")
+ if inferred_type in (AlbumType.SOUNDTRACK, AlbumType.LIVE):
+ return inferred_type
+
+ # Otherwise use provider type
+ return provider_type
### SEARCH AND PARSE FUNCTIONS ###
async def search_and_parse_tracks(
)
from music_assistant_models.unique_list import UniqueList
-from music_assistant.helpers.util import parse_title_and_version
+from music_assistant.helpers.util import infer_album_type, parse_title_and_version
if TYPE_CHECKING:
from .provider import SpotifyProvider
with contextlib.suppress(ValueError):
album.album_type = AlbumType(album_obj["album_type"])
+ # Override with inferred type if version indicates it
+ inferred_type = infer_album_type(album.name, album.version)
+ if inferred_type in (AlbumType.LIVE, AlbumType.SOUNDTRACK):
+ album.album_type = inferred_type
+
if "genres" in album_obj:
album.metadata.genres = set(album_obj["genres"])