Skip to content

Create Services for Playlist Export / Import with options#3387

Open
chrisuthe wants to merge 19 commits intomusic-assistant:devfrom
chrisuthe:task/m3u-playlist-export-import
Open

Create Services for Playlist Export / Import with options#3387
chrisuthe wants to merge 19 commits intomusic-assistant:devfrom
chrisuthe:task/m3u-playlist-export-import

Conversation

@chrisuthe
Copy link
Member

@chrisuthe chrisuthe commented Mar 13, 2026

Allows import/export of MA playlists and radio stations, separately. Export includes an option to export basic data (for URI matching) or full metadata including ISRC, MusicBrainz Recording ID, album, podcast name, and authors for cross-provider "Library Matching" on import. Library matching uses a tiered strategy: exact ID matching (ISRC/MBID) first, then fuzzy metadata fallback. Great for re-adding your library or transferring from one MA user to another. Also added is the ability to replace a playlist track with a single call for an eventual UX of "Manual match" scenario.

I asked AI to create a quick page to test this functionality with since I don't think I'll be the person to implement the UI for it, it's available here: https://gist.github.com/chrisuthe/387180c78b8b12ae12b6659c814fa7d2 just save it locally and pop it open for testing.

New API Commands

music/playlists/export_playlist

  • Parameters: db_playlist_id, include_track_metadata (optional, default false)
  • Exports a builtin playlist as M3U8 string. When include_track_metadata=true, emits #EXTINF (artist, title, duration) and #EXTMA (ISRC, MusicBrainz Recording ID, album, podcast, authors, media type, provider domain) lines.

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, 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/playlists/replace_playlist_track

  • Parameters: db_playlist_id, position (1-based), new_uri
  • Replaces a single track in a builtin playlist by position, preserving playlist order. Enables manual remapping of unresolved tracks after import.

music/radio/export_radios

  • No parameters
  • Exports all builtin radio stations as a single M3U8 string with #EXTINF and #EXTMA metadata.

music/radio/import_radios

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

M3U Format Extension

Uses a custom #EXTMA: comment line (ignored by standard M3U players) to carry structured metadata:

#EXTMA:isrc=USRC17607839,mbid=a1b2c3d4-...,album=OK Computer,media_type=track,provider=spotify
#EXTINF:240,Radiohead - Everything In Its Right Place
spotify://track/abc123

Supported fields: isrc, mbid, album, media_type, provider, podcast, authors, narrators

How Matching Works

When library_matching=true, each track whose URI can't resolve (provider not available) is searched across available providers. 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 and counted as "unmatched" in the import log.

Matching tiers

  1. ISRC — Industry-standard recording code shared across Spotify, Tidal, Qobuz, Apple Music, Deezer. If the exported track has an ISRC and a search result has the same ISRC, it's a definitive match (score 10).
  2. MusicBrainz Recording ID — Open-source equivalent. Same behavior as ISRC (score 10).
  3. Fuzzy fallback — When no ID match is found, scores candidates based on metadata:
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
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"). Useful for:

  • Faster imports when you know which provider has the content
  • Avoiding searches against slow or rate-limited providers
  • Testing matching behavior against specific providers

Test Plan

  • #EXTMA: parsing: metadata dict, without EXTINF, no metadata = None, semicolons in values, podcast episodes
  • generate_m3u() with 4-tuple metadata: emission, ordering, round-trip, backward compat with 3-tuples
  • Score: exact match, no artist, wrong title, wrong artist, close/far/no duration
  • Score: ISRC exact, ISRC mismatch fallback, MBID exact
  • Score: album bonus, album mismatch, media type gate, podcast bonus, authors bonus
  • Full round-trip: tracks + radios + podcast episodes with all metadata fields
  • mypy strict, ruff lint, ruff format all passing

Allow callers to opt-in to resolving track metadata (artist, title,
duration) as EXTINF lines in M3U export. Defaults to False for fast
bare-URI exports; radios always include EXTINF regardless.
When importing an M3U playlist with library_matching=True, tracks whose
provider is unavailable are searched across all providers using EXTINF
metadata (artist, title, duration). Adds parse_extinf_title helper,
_match_track_by_metadata/_score_track_match to builtin provider, and
passes the optional parameter through the controller API.
Add replace_playlist_track to builtin provider and playlist controller
for in-place remapping of unresolvable tracks after import. Add scoring
integration tests for the library matching logic.
@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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

1 participant