Skip to content

Add mammamiradio music provider#3836

Draft
florianhorner wants to merge 6 commits into
music-assistant:devfrom
florianhorner:florianhorner/mammamiradio-provider
Draft

Add mammamiradio music provider#3836
florianhorner wants to merge 6 commits into
music-assistant:devfrom
florianhorner:florianhorner/mammamiradio-provider

Conversation

@florianhorner
Copy link
Copy Markdown

Add mammamiradio music provider

mammamiradio is a self-hosted, AI-generated continuous Italian radio station with banter, music, and ads. This provider lets MA users add their mammamiradio HA addon as a music source alongside SomaFM, RadioBrowser, etc.

What it does

  • One configurable field: mammamiradio_url (default http://localhost:8100)
  • Single Radio entry titled "mammamiradio" (one provider, one station)
  • SUPPORTED_FEATURES = {BROWSE, SEARCH} (matches SomaFM)
  • Stream URL hard-coded as ${mammamiradio_url}/stream (Icecast, MP3 128k)
  • Reachability is checked at provider init via ${url}/api/capabilities; a failure logs a warning but does NOT raise — the user can fix the URL in the UI
  • get_stream_details() raises ProviderUnavailableError if the addon is unreachable at stream time, so MA shows "source unavailable" instead of crashing inside ffmpeg

Self-hosted requirement

mammamiradio is fully self-hosted — there is no public stream. Users install the mammamiradio HA addon (or run the standalone Docker image) on their own network. This provider does not register or proxy any external service; it just points MA at the user's own running addon.

Tests

tests/providers/test_mammamiradio.py covers the unit-test paths from the design doc (17 tests):

  • handle_async_init: passes when reachable; logs but does not raise when unreachable
  • browse(): returns the single Radio entry
  • search(): exact match, substring (case-insensitive), no-match, wrong media type
  • get_radio(): valid id returns Radio; invalid id raises MediaNotFoundError
  • get_stream_details(): returns ContentType.MP3 + bitrate 128, ${url}/stream path, raises ProviderUnavailableError on offline/5xx, raises MediaNotFoundError on unknown id
  • URL trailing-slash normalization
  • get_config_entries() returns one required STRING entry with the documented default

E2E paths (discovery flow, configuration error, mid-stream addon stop) are covered by manual smoke testing during dev — MA's reconnect logic handles the mid-stream case at the player level, not the provider level.

Manual smoke checklist

  • Provider appears in Settings → Providers → Add modal
  • Configuring with default URL succeeds, provider loads
  • Wrong URL: provider still loads, warning logged, playback surfaces a clear error
  • pre-commit run --all-files green
  • Provider unit tests green locally (pytest tests/providers/test_mammamiradio.py)

mammamiradio is a self-hosted, AI-generated continuous Italian radio
station with banter, music, and ads. This provider lets MA users add
their mammamiradio HA addon as a music source alongside SomaFM,
RadioBrowser, etc.

- One configurable field: mammamiradio_url (default http://localhost:8100)
- Single Radio entry titled "mammamiradio"
- BROWSE + SEARCH supported
- Stream URL: ${mammamiradio_url}/stream (Icecast, MP3 128k)
- Reachability checked at provider init; clear error if addon offline
- ProviderUnavailableError raised at stream time if addon goes offline

Requires the mammamiradio HA addon installed and running:
https://github.com/florianhorner/mammamiradio
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 4, 2026

🔒 Dependency Security Report

✅ No dependency changes detected in this PR.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dbc466176f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +188 to +192
async with self.mass.http_session.head(
stream_path, timeout=timeout, allow_redirects=True
) as response:
if response.status >= 400:
msg = (
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid rejecting streams that disallow HEAD requests

Playback is blocked behind a HEAD probe, and any >=400 response is converted into ProviderUnavailableError. This breaks valid GET-only stream endpoints (a common setup for Icecast-style mounts): a stream can be playable via GET /stream but still return 405 Method Not Allowed to HEAD, causing MA to always report the source as unavailable and never attempt playback. Treat 405 as non-fatal or use a lightweight GET probe instead.

Useful? React with 👍 / 👎.

@florianhorner florianhorner marked this pull request as draft May 4, 2026 19:50
@florianhorner
Copy link
Copy Markdown
Author

Marking this as draft while I finalize a few things internally before review:

  • swap placeholder icon for the real mammamiradio logo
  • add a live-integration smoke test against a running mammamiradio addon (current tests mock the HTTP layer)
  • run an internal code-review pass on the diff
  • add a sample-audio link in the description so reviewers can hear what mammamiradio actually sounds like before reviewing

Will mark ready-for-review once those land. Sorry for the noise — should not have hit ready on the first push. Appreciate your patience.

@OzGav
Copy link
Copy Markdown
Contributor

OzGav commented May 4, 2026

Instead of a new provider why not just add this one station manually?

Embeds the rendered mammamiradio logo as a base64 PNG inside the SVG
wrapper; the prior icon was a placeholder shipped with the v1 PR.
Three coupled fixes addressing PR music-assistant#3836 review:

- DEFAULT_URL now points to http://localhost:8000 to match the
  mammamiradio HA addon's actual listen port (the prior 8100 default
  meant the primary one-click setup path was broken).
- /healthz replaces /api/capabilities as the reachability probe target;
  /healthz is the addon's canonical liveness endpoint and returns a
  lighter, structured payload.
- get_stream_details now uses GET (not HEAD) for the per-play
  reachability check. Icecast and FastAPI/uvicorn stream endpoints
  commonly 405 on HEAD, which previously false-positived as
  ProviderUnavailableError on valid streams (codex bot P1 finding).

Also strips query/fragment from the configured URL so a paste with
trailing junk no longer corrupts the stream path.
Why: locks in the PR music-assistant#3836 cleanup fixes so they cannot regress.

- HEAD-mock assertions swapped to GET to match the production probe.
- New test_get_stream_details_tolerates_head_405_regression: simulates
  an Icecast-style server that 405s on HEAD and 200s on GET, asserts
  no ProviderUnavailableError and that head() is never called.
- URL strings updated from localhost:8100 to localhost:8000 to follow
  the new DEFAULT_URL; the constant is also imported and asserted via
  the config-entries default.
- Reachability probe assertion updated to /healthz.
- New URL hygiene tests cover trailing slash, query string, and
  fragment stripping in the configured URL.
- New empty-search test asserts an empty query returns no results.
- Opt-in live smoke test (skipped unless MAMMAMIRADIO_LIVE_URL is set)
  exercises handle_async_init, browse, and get_stream_details against
  a running addon for end-to-end verification.
Why: independent code review surfaced that the prior >=400 check still
raised ProviderUnavailableError on a 405 GET response from Icecast mounts
that require Icy headers, contradicting the cleanup commit's stated 405-
tolerance goal. ffmpeg connects with the correct headers and plays the
stream regardless of the bare-GET 405, so the probe should treat 405 as
reachable. Updates the regression test to exercise the production code
path (GET 405 returns reachable) instead of mocking head() that the code
no longer calls. Drops the redundant head() fixture stub.
handle_async_init now raises ProviderUnavailableError on failure (was
logging warning). get_stream_details no longer probes the stream URL —
passes through to ffmpeg, matching NTS, RadioBrowser, and ORF Radiothek.

Stream-URL probing is structurally unreliable for Icecast: GET pushes
audio bytes the probe never consumes, HEAD returns 200 even when the
source is offline (Xiph mailing list, March 2015). A dedicated /healthz
endpoint at init is the only reliable liveness signal, matching MA's
canonical pattern.

Tests: 4 obsolete tests removed (offline-raises, http-error-raises,
get-not-head, 405-tolerance). 1 new test asserts no probing happens at
stream-time. 2 init-time tests now assert ProviderUnavailableError is
raised on ClientError and HTTP error.
@OzGav
Copy link
Copy Markdown
Contributor

OzGav commented May 8, 2026

Again why this provider instead of adding the station manually?

@florianhorner
Copy link
Copy Markdown
Author

Thank you for the check-in, sorry for he lag - was finalizing the cleanup before reply.

Manual URL works for basic playback, fair point.

The reason for a provider is segment-aware metadata:
mammamiradio exposes typed segments (banter / song / ad as distinct typed segments with timing, host attribution, upcoming queue, and HA context) on /public-status that media_player.play_media(url) can't surface. v1 is intentionally minimal; the metadata hook is what I'd see as a separate RFC. Happy to close in favor of a Discussion if that's the cleaner path.

@OzGav
Copy link
Copy Markdown
Contributor

OzGav commented May 10, 2026

OK sounds fair. Currently though this isn't doing much so I would keep going and propose your final version with all the metadata included.

@florianhorner
Copy link
Copy Markdown
Author

Helpful. Two ways I could go:

(a) Close this and refile as one bundled PR with the metadata extension RFC + reference impl + provider together.
(b) Keep this in draft and add the metadata extension as additional commits here before flipping ready.

Which feels cleaner from your side?

@OzGav
Copy link
Copy Markdown
Contributor

OzGav commented May 10, 2026

Personally I think B is fine.

@OzGav OzGav added this to the 2.10.0 milestone May 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants