Skip to content

Add MusicMe music provider#3393

Open
JulienDeveaux wants to merge 4 commits intomusic-assistant:devfrom
JulienDeveaux:musicme-provider
Open

Add MusicMe music provider#3393
JulienDeveaux wants to merge 4 commits intomusic-assistant:devfrom
JulienDeveaux:musicme-provider

Conversation

@JulienDeveaux
Copy link

Summary

Add a new music provider for MusicMe (musicme.com), a French legal music streaming service operated by ApachNetwork with a catalogue of 13M+ tracks from major and independent labels.

Features

  • Search: artists, albums, tracks (with web search fallback for niche queries)
  • Browse: new releases (by genre), top artists, radios by theme, homepage highlights
  • Recommendations: powers the Discover page with 4 sections (featured, new releases, top artists, radios)
  • Artist details: albums, top tracks (derived from recent albums)
  • Radios: 26 thematic radios and artist mixes
  • Playlists: user playlist listing
  • Streaming: AAC in MP4 container, 44.1kHz stereo, ~111kbps via ephemeral ticket-based URLs

Supported features

  • LIBRARY_PLAYLISTS, LIBRARY_RADIOS
  • SEARCH
  • ARTIST_ALBUMS, ARTIST_TOPTRACKS
  • BROWSE, RECOMMENDATIONS

Notes

  • Requires a MusicMe account (paid subscription for full streaming)
  • MusicMe is primarily available in France
  • Uses a dedicated HTTP session to isolate auth cookies from other providers
  • Streaming tickets are single-use and ephemeral (generated on-demand before each playback)
  • The dataservice API uses a lightweight XOR cipher for response obfuscation (not encryption)

Copilot AI review requested due to automatic review settings March 14, 2026 15:27
@github-actions
Copy link
Contributor

github-actions bot commented Mar 14, 2026

🔒 Dependency Security Report

✅ No dependency changes detected in this PR.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new MusicMe provider integration that supports search, browse, recommendations, library radios/playlists, and streaming via ticket-based URLs.

Changes:

  • Introduces the MusicMe provider implementation (auth, API client/decryption, model parsing, streaming).
  • Adds provider metadata (manifest) and icons for UI display.
  • Implements browsing/recommendations surfaces backed by MusicMe dataservice endpoints.

Reviewed changes

Copilot reviewed 2 out of 4 changed files in this pull request and generated 6 comments.

File Description
music_assistant/providers/musicme/manifest.json Registers the new MusicMe provider and its metadata.
music_assistant/providers/musicme/icon.svg Adds MusicMe colored icon asset.
music_assistant/providers/musicme/icon_monochrome.svg Adds MusicMe monochrome icon asset.
music_assistant/providers/musicme/init.py Implements the MusicMe provider: login, dataservice calls/decryption, search/browse, library, and streaming.

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings March 14, 2026 15:38
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new MusicMe provider integration to Music Assistant, enabling search/browse/library/recommendations and streaming via MusicMe’s dataservice + ticketed streams.

Changes:

  • Introduces the MusicMe provider implementation (auth, dataservice API access, item parsing, browsing, recommendations, streaming URLs).
  • Adds provider manifest metadata and provider icons (standard + monochrome).
  • Implements a web-search fallback when the dataservice search returns no results.

Reviewed changes

Copilot reviewed 2 out of 4 changed files in this pull request and generated 13 comments.

File Description
music_assistant/providers/musicme/manifest.json Registers the new MusicMe provider and its metadata for discovery/configuration.
music_assistant/providers/musicme/init.py Core provider implementation: login, API calls/decryption, search/browse/library, and streaming ticket URL generation.
music_assistant/providers/musicme/icon.svg Adds the provider icon asset.
music_assistant/providers/musicme/icon_monochrome.svg Adds a monochrome icon variant asset.

Comment on lines +73 to +74
_CLIENT_JSON = json.dumps(
{"type": "desktop-web", "context": "www.musicme.com", "app": "mmplayer"}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the MusicMe dataservice expects literal JSON in the query string. URL-encoding breaks the API.

Comment on lines +952 to +957
def _build_base_params(self) -> str:
"""Build the common query string for dataservice API calls."""
parts = ["format=json", f"partnerid={PARTNER_ID}", f"client={_CLIENT_JSON}"]
if self._user_id:
parts.append(f"userid={self._user_id}")
return "&".join(parts)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the MusicMe dataservice expects literal JSON in the query string. URL-encoding breaks the API.

Comment on lines +397 to +402
folder = RecommendationFolder(
name="A l'affiche",
item_id=f"{self.instance_id}_home",
provider=self.instance_id,
icon="mdi-star",
)
Comment on lines +779 to +790
radio = Radio(
item_id=radio_id,
provider=self.instance_id,
name=radio_obj.get("name", "Radio"),
provider_mappings={
ProviderMapping(
item_id=radio_id,
provider_domain=self.domain,
provider_instance=self.instance_id,
)
},
)
Comment on lines +811 to +822
playlist = Playlist(
item_id=playlist_id,
provider=self.instance_id,
name=playlist_obj.get("name", playlist_obj.get("title", "Playlist")),
provider_mappings={
ProviderMapping(
item_id=playlist_id,
provider_domain=self.domain,
provider_instance=self.instance_id,
)
},
)
@OzGav OzGav added this to the 2.9.0 milestone Mar 15, 2026

SUPPORTED_FEATURES = {
ProviderFeature.LIBRARY_PLAYLISTS,
ProviderFeature.LIBRARY_RADIOS,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should only be declared if the user creates a curated set of radio stations that are pulled from the provider.

"""Handle async initialization of the provider."""
if not self.config.get_value(CONF_USERNAME) or not self.config.get_value(CONF_PASSWORD):
msg = "Invalid login credentials"
raise LoginFailed(msg)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
raise LoginFailed(msg)
raise SetupFailedError(msg)

MIssing credentials is not a login failure

def _session(self) -> aiohttp.ClientSession:
"""Return the provider's dedicated HTTP session."""
if self._http_session is None or self._http_session.closed:
self._http_session = aiohttp.ClientSession()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I'm wrong but this seems a bit of a hack to address an error state by restarting a closed session?


# ---- search ----

@use_cache(3600 * 24)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would mean a user would see the same search results for 24 hours regardless of what they search for


if not result.artists and not result.albums and not result.tracks:
self.logger.debug("API search empty, trying web search fallback")
web_result = await self._web_search_fallback(search_query, media_types, limit)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed? What is wrong with the API?


# ---- library ----

async def get_library_radios(self) -> AsyncGenerator[Radio, None]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to LIBRARY_RADIOS. This method is only for when the provider allows the user to create a curated list of stations (for example TuneIn). If the user selects stations to be added to the library from a large list then that is what browse is for. If MusicMe allows user to save a list of favourite stations then all good.

data = await self._api_get(f"/airplay/{prov_radio_id}?resources=tracks")
if data and "item" in data:
return self._parse_radio(data["item"])
async for radio in self.get_library_radios():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would this be required and why would you not raise MediaNotFoundError immediately?

if not data:
return
items: Any = data.get("results", {})
if isinstance(items, dict) and "items" in items:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is going on here. Why dont you know what the API will return?

if art_obj.get("id"):
track.artists.append(self._parse_artist(art_obj))
if track_obj.get("album"):
track.album = self._parse_album(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think an ItemMapping should be created here as there is no data to populate the album?

url = f"{url}{sep}{self._build_base_params()}"

self.logger.debug("GET %s", endpoint.split("?")[0])
async with self._session.get(url) as response:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no error handling here if there is some sort of connection problem?

@OzGav
Copy link
Contributor

OzGav commented Mar 15, 2026

Also this is very long for one file. We generally like to see the following structure
init.py
constants.py
helpers.py - Put your decrypt function in here
provider.py

and also install and run pre-commit before pushing any changes. Currently this provider has a number of mypy issues.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants