Add M3U Radio music provider#4180
Conversation
New music provider that imports radio stations from a self-hosted M3U / IPTV playlist and exposes them as a native radio source: library sync, browse by group-title, and search. - Stable item IDs: tvg-id when present, otherwise a 16-char sha1 of name|url, so favourites survive playlist refreshes. - Optional comma-separated group-title include-filter for mixed TV/radio IPTV playlists; multi_instance allows several playlists. - The playlist is re-fetched on every scheduled library sync so playlist edits are picked up automatically. - Direct-URL HTTP streaming, no seeking (live radio), no external pip requirements (stdlib-only parser). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
🔒 Dependency Security Report✅ No dependency changes detected in this PR. |
|
Closing while I review locally — will reopen when ready for review. |
| return radio | ||
|
|
||
|
|
||
| def parse_m3u(content: str) -> list[dict[str, str]]: |
There was a problem hiding this comment.
we have an existing m3u parser in helpers, maybe use that ? If it's missing some fields we could extend it
| raise MediaNotFoundError(f"Station {prov_radio_id} not found") | ||
| return self._parse_radio(st) | ||
|
|
||
| async def search( |
There was a problem hiding this comment.
do you really need search ? If all stations are imported to the library anyways, the library will already provide the search results.
There was a problem hiding this comment.
Did you not yet push that change as I'm still seeing the search
There was a problem hiding this comment.
I've been back up at work. I have not pushed the changes yet.
| ConfigEntry( | ||
| key=CONF_M3U_URL, | ||
| type=ConfigEntryType.STRING, | ||
| label="M3U / IPTV playlist URL", |
There was a problem hiding this comment.
we recently migrated the backend to support localization (which was added right after your pr got opened). Can you adjust these hardcoded strings to translation_key and a strings.json in your provider folder ?
Look at some of the other providers how this is done.
|
Marking this as draft so we know what needs attention. Please press the Ready for Review button when it is ready again. |
| return re.sub(r"[^a-z0-9]+", "-", text.lower()).strip("-") or "ungrouped" | ||
|
|
||
|
|
||
| def _guess_content_type(url: str) -> ContentType: |
There was a problem hiding this comment.
ContentType.try_parse() is in the models already? More importantly, the established convention for live radio is to not guess, somafm sets content_type=ContentType.UNKNOWN and lets the stream be probed. You should drop the helper and its test and use ContentType.UNKNOWN (matching somafm) or ContentType.try_parse(st["url"])
|
You don't override the base class The practical effect is in the base What is your intent? |
| groups_raw = self.config.get_value(CONF_GROUPS) or "" | ||
| group_filter = {g.strip().lower() for g in str(groups_raw).split(",") if g.strip()} | ||
| try: | ||
| async with self.mass.http_session.get(str(url)) as resp: |
There was a problem hiding this comment.
You should set a timeout value here
| try: | ||
| async with self.mass.http_session.get(str(url)) as resp: | ||
| resp.raise_for_status() | ||
| content = await resp.text() |
There was a problem hiding this comment.
This could raise an uncaught error. You could widen the caught errors below, but elsewhere in the codebase detect_charset is used to handle missing/wrong server charsets so read the raw bytes and decode with errors="replace" so it can't throw (see fetch_playlist in helpers/playlists.py).
What does this implement/fix?
Adds a new music provider, M3U Radio (
m3uradio), which imports radio stations from a self-hosted M3U / IPTV playlist URL and exposes them as a native radio source: library sync, browse bygroup-title, and search.Motivation: self-hosters maintain a station list in an M3U editor. Today the options are manually adding stations one by one via the builtin provider, or one-shot import scripts that go stale. This makes the playlist itself the source of truth.
Design notes:
tvg-idwhen present, otherwise a 16-char sha1 ofname|url— favorites and history survive playlist refreshes.sync_libraryoverride on every scheduled library sync, so playlist edits flow in automatically.group-titleinclude-filter, for mixed TV/radio IPTV playlists.multi_instance: trueallows several playlists.StreamType.HTTP, no seeking (live radio). ICY metadata works out of the box.tests/providers/m3uradio/), including the comma-inside-quoted-attributes edge case.Tested locally against dev: library sync (441-station real-world playlist), browse, search, and end-to-end playback with ICY now-playing metadata.
Related issue (if applicable):
Types of changes
bugfixnew-featureenhancementnew-providerbreaking-changerefactordocumentationmaintenancecidependenciesChecklist
pre-commit run --all-filespasses.pytestpasses, and tests have been added/updated undertests/where applicable.music-assistant/modelsis linked. (n/a — no model changes)music-assistant/frontendis linked. (n/a — no UI changes)Generated with help from Claude Code