From 9f9e273371f4bdfaeef844c4adca7fc3a2e867e9 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 16 Sep 2024 20:24:08 +0200 Subject: [PATCH 1/7] Fix version parsing logic --- music_assistant/common/helpers/util.py | 120 ++++++++++--------------- tests/test_helpers.py | 12 ++- 2 files changed, 58 insertions(+), 74 deletions(-) diff --git a/music_assistant/common/helpers/util.py b/music_assistant/common/helpers/util.py index 0591c5acc..142859024 100644 --- a/music_assistant/common/helpers/util.py +++ b/music_assistant/common/helpers/util.py @@ -26,6 +26,33 @@ multi_space_pattern = re.compile(r"\s{2,}") end_junk_pattern = re.compile(r"(.+?)(\s\W+)$") +VERSION_PARTS = ( + # list of common version strings + "version", + "live", + "edit", + "remix", + "mix", + "acoustic", + "instrumental", + "karaoke", + "remaster", + "versie", + "unplugged", + "disco", + "akoestisch", + "deluxe", +) +IGNORE_TITLE_PARTS = ( + # strings that may be stripped off a title part + # (most important the featuring parts) + "feat.", + "featuring", + "ft.", + "with ", + "explicit", +) + def filename_from_string(string: str) -> str: """Create filename from unsafe string.""" @@ -79,81 +106,30 @@ def create_sort_name(input_str: str) -> str: def parse_title_and_version(title: str, track_version: str | None = None) -> tuple[str, str]: - """Try to parse clean track title and version from the title.""" - version = "" - for splitter in [" (", " [", " - ", " (", " [", "-"]: - if splitter in title: - title_parts = title.split(splitter) - for title_part in title_parts: - # look for the end splitter - for end_splitter in [")", "]"]: - if end_splitter in title_part: - title_part = title_part.split(end_splitter)[0] # noqa: PLW2901 - for version_str in [ - "version", - "live", - "edit", - "remix", - "mix", - "acoustic", - "instrumental", - "karaoke", - "remaster", - "versie", - "radio", - "unplugged", - "disco", - "akoestisch", - "deluxe", - ]: - if version_str in title_part.lower(): - version = title_part - title = title.split(splitter + version)[0] - title = clean_title(title) - if not version and track_version: - version = track_version - version = get_version_substitute(version).title() - if version == title: - version = "" + """Try to parse version from the title.""" + version = track_version or "" + for regex in (r"\(.*?\)", r"\[.*?\]", r" - .*"): + for title_part in re.findall(regex, title): + for ignore_str in IGNORE_TITLE_PARTS: + if ignore_str in title_part.lower(): + title = title.replace(title_part, "").strip() + continue + for version_str in VERSION_PARTS: + if version_str not in title_part.lower(): + continue + version = ( + title_part.replace("(", "") + .replace(")", "") + .replace("[", "") + .replace("]", "") + .replace("-", "") + .strip() + ) + title = title.replace(title_part, "").strip() + return (title, version) return title, version -def clean_title(title: str) -> str: - """Strip unwanted additional text from title.""" - for splitter in [" (", " [", " - ", " (", " [", "-"]: - if splitter in title: - title_parts = title.split(splitter) - for title_part in title_parts: - # look for the end splitter - for end_splitter in [")", "]"]: - if end_splitter in title_part: - title_part = title_part.split(end_splitter)[0] # noqa: PLW2901 - for ignore_str in ["feat.", "featuring", "ft.", "with ", "explicit"]: - if ignore_str in title_part.lower(): - return title.split(splitter + title_part)[0].strip() - return title.strip() - - -def get_version_substitute(version_str: str) -> str: - """Transform provider version str to universal version type.""" - version_str = version_str.lower() - # substitute edit and edition with version - if "edition" in version_str or "edit" in version_str: - version_str = version_str.replace(" edition", " version") - version_str = version_str.replace(" edit ", " version") - if version_str.startswith("the "): - version_str = version_str.split("the ")[1] - if "radio mix" in version_str: - version_str = "radio version" - elif "video mix" in version_str: - version_str = "video version" - elif "spanglish" in version_str or "spanish" in version_str: - version_str = "spanish version" - elif "remaster" in version_str: - version_str = "remaster" - return version_str.strip() - - def strip_ads(line: str) -> str: """Strip Ads from line.""" if ad_pattern.search(line): diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 5b87ffa47..a01934e5e 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -18,18 +18,26 @@ def test_version_extract() -> None: title, version = util.parse_title_and_version(test_str) assert title == "Bam Bam" assert version == "Karaoke Version" + test_str = "Bam Bam (feat. Ed Sheeran) [Karaoke Version]" + title, version = util.parse_title_and_version(test_str) + assert title == "Bam Bam" + assert version == "Karaoke Version" test_str = "SuperSong (2011 Remaster)" title, version = util.parse_title_and_version(test_str) assert title == "SuperSong" - assert version == "Remaster" + assert version == "2011 Remaster" test_str = "SuperSong (Live at Wembley)" title, version = util.parse_title_and_version(test_str) assert title == "SuperSong" - assert version == "Live At Wembley" + assert version == "Live at Wembley" test_str = "SuperSong (Instrumental)" title, version = util.parse_title_and_version(test_str) assert title == "SuperSong" assert version == "Instrumental" + test_str = "SuperSong (Explicit)" + title, version = util.parse_title_and_version(test_str) + assert title == "SuperSong" + assert version == "" async def test_uri_parsing() -> None: From f9ffbf094173217b00c6988961bee4386a5d9e88 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 16 Sep 2024 21:21:17 +0200 Subject: [PATCH 2/7] enforce flow mode on HA players that do not support enqueuing --- .../server/providers/hass_players/__init__.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/music_assistant/server/providers/hass_players/__init__.py b/music_assistant/server/providers/hass_players/__init__.py index 15454d200..694ccb5cb 100644 --- a/music_assistant/server/providers/hass_players/__init__.py +++ b/music_assistant/server/providers/hass_players/__init__.py @@ -31,6 +31,7 @@ ) from music_assistant.common.models.errors import SetupFailedError from music_assistant.common.models.player import DeviceInfo, Player, PlayerMedia +from music_assistant.constants import CONF_FLOW_MODE from music_assistant.server.models.player_provider import PlayerProvider from music_assistant.server.providers.hass import DOMAIN as HASS_DOMAIN @@ -410,6 +411,14 @@ async def _setup_player( supported_features=tuple(supported_features), state=StateMap.get(state["state"], PlayerState.IDLE), ) + # bugfix: correct flow-mode setting for players that do not support media_enque + # remove this after MA release 2.5+ + if MediaPlayerEntityFeature.MEDIA_ENQUEUE not in hass_supported_features: + self.mass.config.set_raw_player_config_value( + player.player_id, + CONF_FLOW_MODE, + True, + ) if MediaPlayerEntityFeature.GROUPING in hass_supported_features: player.can_sync_with = platform_players self._update_player_attributes(player, state["attributes"]) From 9cc9d77d3e15909ba839eb10607f763b477fbd8d Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 16 Sep 2024 22:13:18 +0200 Subject: [PATCH 3/7] enable flow mode by default on dlna --- music_assistant/server/providers/dlna/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/music_assistant/server/providers/dlna/__init__.py b/music_assistant/server/providers/dlna/__init__.py index 1484c318e..9281141d4 100644 --- a/music_assistant/server/providers/dlna/__init__.py +++ b/music_assistant/server/providers/dlna/__init__.py @@ -28,6 +28,7 @@ CONF_ENTRY_CROSSFADE_FLOW_MODE_REQUIRED, CONF_ENTRY_ENABLE_ICY_METADATA, CONF_ENTRY_ENFORCE_MP3, + CONF_ENTRY_FLOW_MODE_DEFAULT_ENABLED, CONF_ENTRY_HTTP_PROFILE, ConfigEntry, ConfigValueType, @@ -71,6 +72,9 @@ CONF_ENTRY_ENFORCE_MP3, CONF_ENTRY_HTTP_PROFILE, CONF_ENTRY_ENABLE_ICY_METADATA, + # enable flow mode by default because + # most dlna players do not support enqueueing + CONF_ENTRY_FLOW_MODE_DEFAULT_ENABLED, create_sample_rates_config_entry(192000, 24, 96000, 24), ) From a032a52f765e8d1f5e88d5203c8c32e4824fdd3a Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 16 Sep 2024 22:14:23 +0200 Subject: [PATCH 4/7] enable http profile 2 for slimproto groups --- music_assistant/server/providers/slimproto/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/music_assistant/server/providers/slimproto/__init__.py b/music_assistant/server/providers/slimproto/__init__.py index 37b462944..be477c253 100644 --- a/music_assistant/server/providers/slimproto/__init__.py +++ b/music_assistant/server/providers/slimproto/__init__.py @@ -280,6 +280,7 @@ async def get_player_config_entries(self, player_id: str) -> tuple[ConfigEntry]: *base_entries, CONF_ENTRY_CROSSFADE, CONF_ENTRY_CROSSFADE_DURATION, + CONF_ENTRY_HTTP_PROFILE_FORCED_2, create_sample_rates_config_entry(96000, 24, 48000, 24), ) From a19870063db8fb0f14b81b2b864a69b9568633a1 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 16 Sep 2024 22:37:45 +0200 Subject: [PATCH 5/7] ignore decode errors in stderr log reading --- music_assistant/server/helpers/process.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/music_assistant/server/helpers/process.py b/music_assistant/server/helpers/process.py index 6c243acb7..36e1f0abf 100644 --- a/music_assistant/server/helpers/process.py +++ b/music_assistant/server/helpers/process.py @@ -202,7 +202,7 @@ async def iter_stderr(self) -> AsyncGenerator[str, None]: line = await self.read_stderr() if line == b"": break - line = line.decode().strip() + line = line.decode("utf-8", errors="ignore").strip() if not line: continue yield line From 259ff7b29bac311e5a06f2825ec333a117f6aa28 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 16 Sep 2024 22:41:12 +0200 Subject: [PATCH 6/7] fix index error in queue --- music_assistant/server/controllers/player_queues.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/music_assistant/server/controllers/player_queues.py b/music_assistant/server/controllers/player_queues.py index f723c9937..65e22d8a1 100644 --- a/music_assistant/server/controllers/player_queues.py +++ b/music_assistant/server/controllers/player_queues.py @@ -1075,7 +1075,7 @@ async def preload_next_item( msg = f"PlayerQueue {queue_id} is not available" raise PlayerUnavailableError(msg) if current_item_id_or_index is None: - cur_index = queue.index_in_buffer + cur_index = queue.index_in_buffer or queue.current_index or 0 elif isinstance(current_item_id_or_index, str): cur_index = self.index_by_id(queue_id, current_item_id_or_index) else: From 5f7b3a815175f7752e220928deb1ffa12a984deb Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 16 Sep 2024 23:14:35 +0200 Subject: [PATCH 7/7] fix cast child player media status --- music_assistant/server/providers/chromecast/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/music_assistant/server/providers/chromecast/__init__.py b/music_assistant/server/providers/chromecast/__init__.py index 9c54500ec..23b8507f1 100644 --- a/music_assistant/server/providers/chromecast/__init__.py +++ b/music_assistant/server/providers/chromecast/__init__.py @@ -445,6 +445,7 @@ def on_new_media_status(self, castplayer: CastPlayer, status: MediaStatus) -> No status.player_state, ) # handle castplayer playing from a group + group_player: CastPlayer | None = None if castplayer.active_group is not None: if not (group_player := self.castplayers.get(castplayer.active_group)): return @@ -471,7 +472,9 @@ def on_new_media_status(self, castplayer: CastPlayer, status: MediaStatus) -> No castplayer.player.elapsed_time = status.current_time # active source - if castplayer.cc.app_id == MASS_APP_ID: + if group_player: + castplayer.player.active_source = group_player.player.active_source + elif castplayer.cc.app_id == MASS_APP_ID: castplayer.player.active_source = castplayer.player_id else: castplayer.player.active_source = castplayer.cc.app_display_name