From 78fee32323eeb262b7a24228fca95cd4f47e3572 Mon Sep 17 00:00:00 2001 From: mintgrey <30776914+mintgrey@users.noreply.github.com> Date: Fri, 29 May 2026 22:05:36 +0200 Subject: [PATCH 01/12] Update constants.py for API Signin --- music_assistant/providers/musicme/constants.py | 1 + 1 file changed, 1 insertion(+) diff --git a/music_assistant/providers/musicme/constants.py b/music_assistant/providers/musicme/constants.py index d159554c68..c910d284c8 100644 --- a/music_assistant/providers/musicme/constants.py +++ b/music_assistant/providers/musicme/constants.py @@ -14,6 +14,7 @@ ProviderFeature.RECOMMENDATIONS, } +SIGNIN_BY_API = "signin_by_api" # Custom key for ConfigEntry DATASERVICE_BASE = "https://dataservice.musicme.com/dataservice/v3" STREAM_BASE = "https://stream.hosting-media.net/musicme" WEB_BASE = "https://www.musicme.com" From 1bc62004fbcc49cf2ed101cafd5d02594b0345e0 Mon Sep 17 00:00:00 2001 From: mintgrey <30776914+mintgrey@users.noreply.github.com> Date: Fri, 29 May 2026 22:07:33 +0200 Subject: [PATCH 02/12] Update __init__.py for API signin --- music_assistant/providers/musicme/__init__.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/music_assistant/providers/musicme/__init__.py b/music_assistant/providers/musicme/__init__.py index c9b2d762e7..beee11d2f6 100644 --- a/music_assistant/providers/musicme/__init__.py +++ b/music_assistant/providers/musicme/__init__.py @@ -9,7 +9,7 @@ from music_assistant.constants import CONF_PASSWORD, CONF_USERNAME -from .constants import SUPPORTED_FEATURES +from .constants import SUPPORTED_FEATURES, SIGNIN_BY_API from .provider import MusicMeProvider if TYPE_CHECKING: @@ -42,6 +42,13 @@ async def get_config_entries( """ # ruff: noqa: ARG001 return ( + ConfigEntry( + key=SIGNIN_BY_API, + type=ConfigEntryType.BOOLEAN, + label="Signin by API", + required=False, + description="If checked, the signin will be made by API call, if unchecked, login will be made by HTTP authentification.", + ), ConfigEntry( key=CONF_USERNAME, type=ConfigEntryType.STRING, From 24665ab3cf296b7c8db757a3709ca7b3bcc52101 Mon Sep 17 00:00:00 2001 From: mintgrey <30776914+mintgrey@users.noreply.github.com> Date: Fri, 29 May 2026 22:11:46 +0200 Subject: [PATCH 03/12] Update provider.py for API signin, streamable fix and playlist id fix --- music_assistant/providers/musicme/provider.py | 51 +++++++++++++++---- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/music_assistant/providers/musicme/provider.py b/music_assistant/providers/musicme/provider.py index 268d6f113a..d7983e8667 100644 --- a/music_assistant/providers/musicme/provider.py +++ b/music_assistant/providers/musicme/provider.py @@ -50,6 +50,7 @@ from music_assistant.models.music_provider import MusicProvider from .constants import ( + SIGNIN_BY_API, CLIENT_JSON, DATASERVICE_BASE, LOGIN_URL, @@ -80,7 +81,12 @@ async def handle_async_init(self) -> None: # (each instance has its own MusicMe login cookies) self.http_session = create_clientsession(self.mass, cookie_jar=aiohttp.CookieJar()) self.throttler = ThrottlerManager(rate_limit=1, period=1) - await self._login() + if self.config.get_value(SIGNIN_BY_API): + self.logger.info("Trying to signin by API call") + await self._signin_by_api() + else: + self.logger.info("Trying to login by HTTP authentification") + await self._login() async def unload(self, is_removed: bool = False) -> None: """Handle unload/close of the provider.""" @@ -127,7 +133,7 @@ async def search( result.tracks = [ self._parse_track(t) for t in tracks - if t.get("barcode") and t.get("streamable") == 2 + if t.get("barcode") and t.get("streamable") != 0 ] if not result.artists and not result.albums and not result.tracks: @@ -231,7 +237,7 @@ async def _browse_news( ) for album_obj in data.get("results", {}).get("albums", []): - if album_obj.get("barcode") and album_obj.get("streamable", 0) == 2: + if album_obj.get("barcode") and album_obj.get("streamable", 0) != 0: results.append(self._parse_album(album_obj)) return results @@ -323,7 +329,7 @@ async def recommendations(self) -> list[RecommendationFolder]: icon="mdi-new-box", ) for album_obj in news_data.get("results", {}).get("albums", []): - if album_obj.get("barcode") and album_obj.get("streamable", 0) == 2: + if album_obj.get("barcode") and album_obj.get("streamable", 0) != 0: folder.items.append(self._parse_album(album_obj)) if folder.items: result.append(folder) @@ -444,7 +450,7 @@ async def get_artist_toptracks(self, prov_artist_id: str) -> list[Track]: return [] albums = data.get("results", {}).get("albums", []) streamable_barcodes = [ - a["barcode"] for a in albums if a.get("barcode") and a.get("streamable", 0) == 2 + a["barcode"] for a in albums if a.get("barcode") and a.get("streamable", 0) != 0 ] album_results = await asyncio.gather( *(self._api_get(f"/album/{bc}?resources=tracks") for bc in streamable_barcodes) @@ -454,7 +460,7 @@ async def get_artist_toptracks(self, prov_artist_id: str) -> list[Track]: if not album_data: continue for t in album_data.get("results", {}).get("tracks", []): - if t.get("barcode") and t.get("streamable", 0) == 2: + if t.get("barcode") and t.get("streamable", 0) != 0: top_tracks.append(self._parse_track(t)) if len(top_tracks) >= 20: break @@ -520,7 +526,7 @@ async def _get_radio_track(self, radio_id: str) -> str: raise MediaNotFoundError(msg) tracks = data.get("results", {}).get("tracks", []) for t in tracks: - if t.get("barcode") and t.get("streamable", 0) == 2: + if t.get("barcode") and t.get("streamable", 0) != 0: return str(t["barcode"]) msg = f"No streamable tracks in radio {radio_id}" raise MediaNotFoundError(msg) @@ -567,7 +573,7 @@ def _parse_album(self, album_obj: dict[str, Any]) -> Album: item_id=barcode, provider_domain=self.domain, provider_instance=self.instance_id, - available=album_obj.get("streamable", 0) == 2, + available=album_obj.get("streamable", 0) != 0, audio_format=AudioFormat( content_type=ContentType.MP4, codec_type=ContentType.AAC, @@ -608,7 +614,7 @@ def _parse_track(self, track_obj: dict[str, Any]) -> Track: item_id=barcode, provider_domain=self.domain, provider_instance=self.instance_id, - available=track_obj.get("streamable", 0) == 2, + available=track_obj.get("streamable", 0) != 0, audio_format=AudioFormat( content_type=ContentType.MP4, codec_type=ContentType.AAC, @@ -683,7 +689,7 @@ def _parse_radio(self, radio_obj: dict[str, Any]) -> Radio: def _parse_playlist(self, playlist_obj: dict[str, Any]) -> Playlist: """Parse a MusicMe playlist object to a Music Assistant Playlist.""" - playlist_id = str(playlist_obj.get("id", "")) + playlist_id = (str(playlist_obj.get("id", ""))).removeprefix("pl-") playlist = Playlist( item_id=playlist_id, provider=self.instance_id, @@ -768,11 +774,34 @@ async def _web_search_fallback( result.tracks = [ self._parse_track(t) for t in tracks[:limit] - if t.get("barcode") and t.get("streamable") == 2 + if t.get("barcode") and t.get("streamable") != 0 ] return result if (result.artists or result.albums or result.tracks) else None + + async def _signin_by_api(self) -> None: + login = self.config.get_value(CONF_USERNAME) + password = self.config.get_value(CONF_PASSWORD) + + response = await self._api_get( + f"/medialibrary/signin" + f"?channel=65777&lang=fr&format=json&client=%7B%22type%22%3A%22desktop-web%22%2C%22context%22%3A%22pro.bib.musicme.com%22%7D" + f"&key=sKTBA7ybW3nvCUQ6&nocrypt=0&login={login}&password={password}" + ) + + if response and "results" in response: + results = response.get("results", {}) + if results and "user" in results: + self._user_id = results.get("user").get("id") + if not self._user_id: + msg = "Login failed — no user.id in MusicMe API response" + raise LoginFailed(msg) + + self.logger.info( + "Successfully logged in to MusicMe" + ) + async def _login(self) -> None: """Authenticate with MusicMe via web login and extract the userId.""" email = self.config.get_value(CONF_USERNAME) From 90e13323f0d0e2bd3fe5d9429592c8fe1db35006 Mon Sep 17 00:00:00 2001 From: mintgrey <30776914+mintgrey@users.noreply.github.com> Date: Sat, 30 May 2026 09:35:35 +0200 Subject: [PATCH 04/12] Update __init__.py on authentication mispell --- music_assistant/providers/musicme/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/music_assistant/providers/musicme/__init__.py b/music_assistant/providers/musicme/__init__.py index beee11d2f6..2c486b46c7 100644 --- a/music_assistant/providers/musicme/__init__.py +++ b/music_assistant/providers/musicme/__init__.py @@ -47,7 +47,7 @@ async def get_config_entries( type=ConfigEntryType.BOOLEAN, label="Signin by API", required=False, - description="If checked, the signin will be made by API call, if unchecked, login will be made by HTTP authentification.", + description="If checked, the signin will be made by API call, if unchecked, login will be made by HTTP authentication.", ), ConfigEntry( key=CONF_USERNAME, From 31baaa4febc78df3416bb32aaccf037dcbb11cab Mon Sep 17 00:00:00 2001 From: mintgrey <30776914+mintgrey@users.noreply.github.com> Date: Sat, 30 May 2026 09:36:36 +0200 Subject: [PATCH 05/12] Update provider.py on authentication mispell --- music_assistant/providers/musicme/provider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/music_assistant/providers/musicme/provider.py b/music_assistant/providers/musicme/provider.py index d7983e8667..fd0bcedb6c 100644 --- a/music_assistant/providers/musicme/provider.py +++ b/music_assistant/providers/musicme/provider.py @@ -85,7 +85,7 @@ async def handle_async_init(self) -> None: self.logger.info("Trying to signin by API call") await self._signin_by_api() else: - self.logger.info("Trying to login by HTTP authentification") + self.logger.info("Trying to login by HTTP authentication") await self._login() async def unload(self, is_removed: bool = False) -> None: From 9512363b5c9a901123e76975dcb6384f580f7631 Mon Sep 17 00:00:00 2001 From: mintgrey <30776914+mintgrey@users.noreply.github.com> Date: Sat, 30 May 2026 20:14:05 +0200 Subject: [PATCH 06/12] Update test_streaming.py to work with "streamable" fix --- tests/providers/musicme/test_streaming.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/providers/musicme/test_streaming.py b/tests/providers/musicme/test_streaming.py index d571e58838..9eae8e762b 100644 --- a/tests/providers/musicme/test_streaming.py +++ b/tests/providers/musicme/test_streaming.py @@ -72,7 +72,7 @@ async def test_returns_first_streamable_track(self, provider: MusicMeProvider) - return_value={ "results": { "tracks": [ - {"barcode": "AAA-01_01", "streamable": 1}, + {"barcode": "AAA-01_01", "streamable": 0}, {"barcode": "BBB-01_01", "streamable": 2}, {"barcode": "CCC-01_01", "streamable": 2}, ] From 7c5270127fe9485afb36b0c2c25a6fbd07c070ab Mon Sep 17 00:00:00 2001 From: mintgrey <30776914+mintgrey@users.noreply.github.com> Date: Sun, 31 May 2026 10:06:31 +0200 Subject: [PATCH 07/12] Update __init__.py for import sorting --- music_assistant/providers/musicme/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/music_assistant/providers/musicme/__init__.py b/music_assistant/providers/musicme/__init__.py index 2c486b46c7..81b5d243a4 100644 --- a/music_assistant/providers/musicme/__init__.py +++ b/music_assistant/providers/musicme/__init__.py @@ -9,7 +9,7 @@ from music_assistant.constants import CONF_PASSWORD, CONF_USERNAME -from .constants import SUPPORTED_FEATURES, SIGNIN_BY_API +from .constants import SIGNIN_BY_API, SUPPORTED_FEATURES from .provider import MusicMeProvider if TYPE_CHECKING: From e7b7598fd1f31e685d0597bc99d119eae63e3672 Mon Sep 17 00:00:00 2001 From: mintgrey <30776914+mintgrey@users.noreply.github.com> Date: Sun, 31 May 2026 10:10:08 +0200 Subject: [PATCH 08/12] Update provider.py for import sorting and remove blank lines --- music_assistant/providers/musicme/provider.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/music_assistant/providers/musicme/provider.py b/music_assistant/providers/musicme/provider.py index fd0bcedb6c..42b3747022 100644 --- a/music_assistant/providers/musicme/provider.py +++ b/music_assistant/providers/musicme/provider.py @@ -50,11 +50,11 @@ from music_assistant.models.music_provider import MusicProvider from .constants import ( - SIGNIN_BY_API, CLIENT_JSON, DATASERVICE_BASE, LOGIN_URL, PARTNER_ID, + SIGNIN_BY_API, STREAM_BASE, VALID_ID_RE, WEB_BASE, @@ -778,7 +778,7 @@ async def _web_search_fallback( ] return result if (result.artists or result.albums or result.tracks) else None - + async def _signin_by_api(self) -> None: login = self.config.get_value(CONF_USERNAME) password = self.config.get_value(CONF_PASSWORD) @@ -801,7 +801,7 @@ async def _signin_by_api(self) -> None: self.logger.info( "Successfully logged in to MusicMe" ) - + async def _login(self) -> None: """Authenticate with MusicMe via web login and extract the userId.""" email = self.config.get_value(CONF_USERNAME) From 34c6575e80abcf1e19a130af7a988a914c85fad8 Mon Sep 17 00:00:00 2001 From: mintgrey <30776914+mintgrey@users.noreply.github.com> Date: Sun, 31 May 2026 10:16:08 +0200 Subject: [PATCH 09/12] Update __init__.py for default_value on ConfigEntry --- music_assistant/providers/musicme/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/music_assistant/providers/musicme/__init__.py b/music_assistant/providers/musicme/__init__.py index 81b5d243a4..b911b53da0 100644 --- a/music_assistant/providers/musicme/__init__.py +++ b/music_assistant/providers/musicme/__init__.py @@ -47,6 +47,7 @@ async def get_config_entries( type=ConfigEntryType.BOOLEAN, label="Signin by API", required=False, + default_value=False, description="If checked, the signin will be made by API call, if unchecked, login will be made by HTTP authentication.", ), ConfigEntry( From 139c3c81c13ab3a00d0df6c3687cf9d6508396b4 Mon Sep 17 00:00:00 2001 From: mintgrey <30776914+mintgrey@users.noreply.github.com> Date: Sun, 31 May 2026 10:19:56 +0200 Subject: [PATCH 10/12] Update provider.py for url encoding and reformating --- music_assistant/providers/musicme/provider.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/music_assistant/providers/musicme/provider.py b/music_assistant/providers/musicme/provider.py index 42b3747022..d443d81649 100644 --- a/music_assistant/providers/musicme/provider.py +++ b/music_assistant/providers/musicme/provider.py @@ -780,12 +780,12 @@ async def _web_search_fallback( return result if (result.artists or result.albums or result.tracks) else None async def _signin_by_api(self) -> None: - login = self.config.get_value(CONF_USERNAME) - password = self.config.get_value(CONF_PASSWORD) + login = urllib.parse.quote_plus(self.config.get_value(CONF_USERNAME)) + password = urllib.parse.quote_plus(self.config.get_value(CONF_PASSWORD)) response = await self._api_get( f"/medialibrary/signin" - f"?channel=65777&lang=fr&format=json&client=%7B%22type%22%3A%22desktop-web%22%2C%22context%22%3A%22pro.bib.musicme.com%22%7D" + f"?channel=65777&lang=fr&client=%7B%22type%22%3A%22desktop-web%22%2C%22context%22%3A%22pro.bib.musicme.com%22%7D" f"&key=sKTBA7ybW3nvCUQ6&nocrypt=0&login={login}&password={password}" ) @@ -798,9 +798,7 @@ async def _signin_by_api(self) -> None: msg = "Login failed — no user.id in MusicMe API response" raise LoginFailed(msg) - self.logger.info( - "Successfully logged in to MusicMe" - ) + self.logger.info("Successfully logged in to MusicMe") async def _login(self) -> None: """Authenticate with MusicMe via web login and extract the userId.""" From 296f231994b7332441fc50eed050d681fb15d8e5 Mon Sep 17 00:00:00 2001 From: mintgrey <30776914+mintgrey@users.noreply.github.com> Date: Sat, 6 Jun 2026 10:45:14 +0200 Subject: [PATCH 11/12] Update provider.py for explicit str casting on api signin and some indentation corrections --- music_assistant/providers/musicme/provider.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/music_assistant/providers/musicme/provider.py b/music_assistant/providers/musicme/provider.py index d443d81649..79e9f5e892 100644 --- a/music_assistant/providers/musicme/provider.py +++ b/music_assistant/providers/musicme/provider.py @@ -780,8 +780,8 @@ async def _web_search_fallback( return result if (result.artists or result.albums or result.tracks) else None async def _signin_by_api(self) -> None: - login = urllib.parse.quote_plus(self.config.get_value(CONF_USERNAME)) - password = urllib.parse.quote_plus(self.config.get_value(CONF_PASSWORD)) + login = urllib.parse.quote_plus(str(self.config.get_value(CONF_USERNAME))) + password = urllib.parse.quote_plus(str(self.config.get_value(CONF_PASSWORD))) response = await self._api_get( f"/medialibrary/signin" @@ -792,7 +792,7 @@ async def _signin_by_api(self) -> None: if response and "results" in response: results = response.get("results", {}) if results and "user" in results: - self._user_id = results.get("user").get("id") + self._user_id = results.get("user").get("id") if not self._user_id: msg = "Login failed — no user.id in MusicMe API response" From fcb8945e0e47c297c1414779db84cb02ba987fe3 Mon Sep 17 00:00:00 2001 From: mintgrey <30776914+mintgrey@users.noreply.github.com> Date: Sat, 6 Jun 2026 10:49:34 +0200 Subject: [PATCH 12/12] Update test_parsers.py to work with "streamable" fix in test_album_not_streamable --- tests/providers/musicme/test_parsers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/providers/musicme/test_parsers.py b/tests/providers/musicme/test_parsers.py index 6e5d0ae3dd..9be8f06642 100644 --- a/tests/providers/musicme/test_parsers.py +++ b/tests/providers/musicme/test_parsers.py @@ -152,8 +152,8 @@ def test_album_streamable(self, provider: MusicMeProvider) -> None: assert mapping.available is True def test_album_not_streamable(self, provider: MusicMeProvider) -> None: - """Test album with streamable != 2 is marked unavailable.""" - obj = {**ALBUM_OBJ, "streamable": 1} + """Test album with streamable != 0 is marked unavailable.""" + obj = {**ALBUM_OBJ, "streamable": 0} album = provider._parse_album(obj) mapping = next(iter(album.provider_mappings)) assert mapping.available is False