Skip to content

Create Services for Playlist Export / Import#3387

Merged
marcelveldt merged 9 commits intomusic-assistant:devfrom
chrisuthe:task/m3u-playlist-export-import
Mar 30, 2026
Merged

Create Services for Playlist Export / Import#3387
marcelveldt merged 9 commits intomusic-assistant:devfrom
chrisuthe:task/m3u-playlist-export-import

Conversation

@chrisuthe
Copy link
Copy Markdown
Member

@chrisuthe chrisuthe commented Mar 13, 2026

Allows import/export of MA playlists and radio stations via M3U8 format. Export writes full metadata using a rich set of extended M3U tags, and import recreates playlists by parsing those tags and optionally running background "Library Matching" to resolve tracks across providers. Matching uses a tiered strategy: exact ID matching (ISRC/MusicBrainz) first, then fuzzy metadata fallback. Great for backing up your library, transferring between MA instances, or migrating across providers.

A quick test page for exercising the API is available here: https://gist.github.com/chrisuthe/387180c78b8b12ae12b6659c814fa7d2 — save it locally and open in a browser.

New API Commands

music/playlists/export_playlist

  • Parameters: db_playlist_id
  • Exports a builtin playlist as an M3U8 string with full metadata. If the playlist has multiple provider mappings (e.g. builtin + filesystem), the builtin mapping is selected explicitly.

music/playlists/import_playlist

  • Parameters: m3u_data, library_matching (optional, default false), match_providers (optional, default null)
  • Imports an M3U/M3U8 string as a new builtin playlist. When library_matching=true, a background task searches providers to resolve unmatched URIs using a tiered matching strategy. When match_providers is set (e.g. ["spotify", "tidal"]), only those providers are searched — accepts instance IDs or domain names. When null, all available providers are searched.

music/radio/export_radios

  • No parameters
  • Exports all builtin radio stations as a single M3U8 string.

music/radio/import_radios

  • Parameters: m3u_data
  • Imports radio stations from an M3U/M3U8 string, skipping duplicates by stream URL.

M3U Format

Uses standard #EXTINF for compatibility with other players, plus Music Assistant extended tags (ignored by standard players) for rich metadata. All extended tags use || (double-pipe) as the field separator to avoid conflicts with commas in metadata values.

Extended Tags

Tag Purpose Format
#EXTMA Key-value metadata key=value||key=value
#EXTPROV Provider mapping domain||item_id||instance_id||content_type||sample_rate||bit_depth||bit_rate
#EXTARTIST Artist info name||provider_domain||item_id||provider_instance
#EXTALBUM Album info name||provider_domain||item_id||provider_instance||version
#EXTPODCAST Podcast info name||provider_domain||item_id||provider_instance
#EXTIMG Image metadata type||path||provider||remotely_accessible

EXTMA fields

name, media_type, isrc, mbid, album, version, podcast, authors, narrators

Example

#EXTM3U
#PLAYLIST:My Playlist
#EXTMA:name=Everything In Its Right Place||media_type=track||isrc=USRC17607839||mbid=a1b2c3d4-...
#EXTPROV:spotify||track/abc123||spotify_1||ogg_vorbis||44100||0||320
#EXTARTIST:Radiohead||spotify||artist/xyz||spotify_1
#EXTALBUM:Kid A||spotify||album/def||spotify_1||
#EXTIMG:thumb||https://example.com/art.jpg||spotify||true
#EXTINF:240,Radiohead - Everything In Its Right Place
spotify://track/abc123

How Matching Works

When library_matching=true, each track whose URI can't be resolved (provider not available) is searched across available providers. Matching runs as a background task with progress tracking. Each search result is scored, and the highest-scoring result across all providers wins. If an ISRC or MusicBrainz ID matches (score 10), it returns immediately without searching further providers.

A score of 0 means "not a match" — the candidate is rejected. Any score > 0 is a valid match candidate. The highest score wins. If no candidates score > 0, the original (unresolvable) URI is kept in the playlist.

Scoring table

Factor Score Behavior
ISRC match 10 Instant win, stops searching all providers
MusicBrainz Recording ID 10 Instant win, stops searching all providers
Title match +1 Required — 0 if title doesn't match
Artist match +2 Required if present — 0 if artist doesn't match
Album match +1 Bonus, helps pick the right version of a song
Version match +2 Bonus for matching version strings
Version mismatch -1 Penalty when versions conflict
Duration within 2s +2 Near-exact bonus
Duration within 5s +1 Close enough bonus (M3U truncates to int)
Podcast name match +2 Like artist, but for podcast episodes
Authors match +2 Like artist, but for audiobooks
Media type mismatch 0 Hard reject — won't match a podcast against a track

Provider filtering

By default all providers are searched. Use match_providers to limit the search:

{"m3u_data": "...", "library_matching": true, "match_providers": ["spotify", "tidal"]}

Accepts provider instance IDs (e.g. "spotify_1") or domain names (e.g. "spotify").

Test Plan

  • M3U parsing: EXTINF durations, negative duration, single digit
  • EXTINF title parsing: with artist, without artist, none, multiple separators
  • EXTMA parsing: metadata dict, without metadata, double-pipe separator
  • EXTPROV parsing: full fields, minimal (2 fields), invalid entries skipped
  • EXTIMG parsing: remotely accessible, not remotely accessible, multiple images
  • Playlist name parsing: present, missing
  • M3U generation: basic, with metadata, with providers, with images, empty, no EXTINF without title
  • Round-trip: full metadata, multiple entries, bare URIs
  • Score: ISRC exact match (case-insensitive), MBID exact match
  • Score: title match/mismatch, artist match/mismatch, album bonus
  • Score: version match bonus, version mismatch penalty
  • Score: duration near/close/far, media type gate, podcast bonus, authors bonus
  • Score: none title returns zero, full metadata high score
  • Radio export/import round-trip
  • mypy strict, ruff lint, ruff format all passing

@chrisuthe chrisuthe added this to the 2.9.0 milestone Mar 13, 2026
@chrisuthe chrisuthe self-assigned this Mar 13, 2026
@chrisuthe chrisuthe marked this pull request as draft March 13, 2026 16:31
@chrisuthe chrisuthe marked this pull request as ready for review March 15, 2026 22:08
@marcelveldt-traveling
Copy link
Copy Markdown
Contributor

Why make "include_track_metadata" optional ? I think it is good to have at all times

@marcelveldt-traveling
Copy link
Copy Markdown
Contributor

Overall I think this is a great feature, nice work!
Some of my thoughts:

  1. Did you also account for the version field ? Only doing name matching can be dangerous when there are different versions of tracks. So always take into account the version field (and add it to the EXTMA).

  2. An idea that has been in my head for a while now is that we should have a proper task management for all long running tasks. So basically an import playlist action would be scheduled on the task manager and user can keep track of it, read status, logs etc. I think it would make sense to add the task manager first and then add this import action to it. I will start working on it in concept.

@chrisuthe
Copy link
Copy Markdown
Member Author

Why make "include_track_metadata" optional ? I think it is good to have at all times

I was really worried about performance, specifically in the case of library delete/reload or switching from beta to prod and back again, the ability to match quickly without additional metadata seemed like a savings during export, but honestly after doing some benchmarking I agree. I'll make it the default and get rid of the option.

@chrisuthe
Copy link
Copy Markdown
Member Author

@marcelveldt-traveling @marcelveldt I've updated to include version field, I also removed the "option" on export, it just always includes the data needed.

I'll hang tight until you have some time to think about the long-running process options, going to flip this back to draft until then.

@chrisuthe chrisuthe marked this pull request as draft March 19, 2026 00:39
@marcelveldt-traveling
Copy link
Copy Markdown
Contributor

marcelveldt-traveling commented Mar 19, 2026

PR for the background tasks controller is now up:
#3426

@chrisuthe chrisuthe force-pushed the task/m3u-playlist-export-import branch from 9a728dd to 1bce226 Compare March 19, 2026 15:56
@chrisuthe chrisuthe changed the title Create Services for Playlist Export / Import with options Create Services for Playlist Export / Import Mar 19, 2026
@chrisuthe chrisuthe force-pushed the task/m3u-playlist-export-import branch from 178b787 to 1bce226 Compare March 19, 2026 16:15
@chrisuthe chrisuthe marked this pull request as ready for review March 19, 2026 16:19
@chrisuthe
Copy link
Copy Markdown
Member Author

PR for the background tasks controller is now up: #3426

Updated to use background task controller :)

@marcelveldt
Copy link
Copy Markdown
Member

OK, inspired by your EXTMA info, I have updated the builtin provider's playlist handling (mainly to fix its inefficiency).
So we now have some helpers to deal with the M3U playlist with extended metadata which can be used for this feature.
See #3451

@chrisuthe chrisuthe force-pushed the task/m3u-playlist-export-import branch from 8b80ef2 to 6ac0c69 Compare March 30, 2026 00:16
Rewrite export/import to leverage the M3U infrastructure from PR music-assistant#3451:
- Export reads stored M3U files directly (already enriched with metadata)
- Import writes parsed PlaylistItem objects preserving all extended tags
- Background matching searches providers for unavailable URIs using
  ISRC/MusicBrainz IDs first, then fuzzy title/artist/duration matching
- Radio export/import via M3U format
- Controller-level API endpoints for playlist and radio export/import
- 17 new tests covering scoring logic and radio round-trip
…ists

Explicitly find the builtin provider mapping instead of using
_select_provider_id, which could pick a non-builtin mapping first
(e.g. filesystem) and then reject it.
@chrisuthe
Copy link
Copy Markdown
Member Author

@marcelveldt If you'd like to have a peak here, I think this now in a decent place with your update, once this is in I will add a basic front end that someone can make better :)

@chrisuthe
Copy link
Copy Markdown
Member Author

Upon re-look @marcelveldt should I break this out to a seperate import_export file in order to try to set up for avoiding init sprawl like has been done for chromecast and spotify?

@marcelveldt
Copy link
Copy Markdown
Member

Upon re-look @marcelveldt should I break this out to a seperate import_export file in order to try to set up for avoiding init sprawl like has been done for chromecast and spotify?

Yeah that would be better as the builtin provider is getting big now. We could split it up into dedicated init.py, provider.py, helpers.py, constants.py etc.

But we can also do that in a follow-up PR and add this new function first, the refactor next

Co-authored-by: Marcel van der Veldt <m.vanderveldt@outlook.com>
@chrisuthe chrisuthe force-pushed the task/m3u-playlist-export-import branch from 9824f16 to 47193ef Compare March 30, 2026 16:25
Extract MediaItem-to-PlaylistItem conversion into a shared helper
(media_item_to_playlist_item) in helpers/playlists.py. Rewrite the
controller's export_playlist to fetch tracks from any provider and
generate M3U on the fly, removing the builtin-only restriction. Slim
the builtin provider's _build_m3u_entry_from_uri to use the shared
helper instead of duplicating the conversion logic.
Trim export_playlist docstring per reviewer feedback. Rewrite
export_radios to export all library radio stations (any provider)
instead of only builtin ones, using the shared helper.
Copy link
Copy Markdown
Member

@marcelveldt marcelveldt left a comment

Choose a reason for hiding this comment

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

Nice work @chrisuthe !
Cool QoL feature that people will love

@marcelveldt marcelveldt merged commit 800536a into music-assistant:dev Mar 30, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Later

Development

Successfully merging this pull request may close these issues.

4 participants