Home Assistant Music Plex Synchoniser
Find a file
2026-08-03 18:17:22 +00:00
custom_components/music_own Fix "Fill missing album tracks" button never pushing to the Plex Wish List 2026-08-03 20:16:41 +02:00
.gitattributes Removed git lfs tracking. 2026-06-14 11:17:17 +02:00
.gitignore Add "Fill missing album tracks" — find album gaps via MusicBrainz, add to buy-list 2026-08-03 14:46:24 +02:00
hacs.json Fixed the icon/logo. 2026-06-13 16:53:07 +02:00
README.md Alert when an Apple Music account's session has gone stale 2026-07-28 09:59:46 +02:00

Music Own

A Home Assistant custom integration that migrates your Music Assistant playlists (Spotify, Apple Music — including multiple Apple Music accounts — or anything else MA connects to) into native Plex playlists, and maintains a Qobuz buy-list for the tracks you don't own yet — so you own your music instead of renting it.

The only manual step in the whole system is clicking a buy link and checking out. Nothing here downloads, rips, or circumvents anything: it reads catalogues, writes Plex playlists you own, and shows you what to purchase.

Model

  • Owned track (found in your Plex library) → added to a native Plex playlist of the same name, in the same order as the source playlist.
  • Unowned track → recorded in a durable SQLite wanted store with a Qobuz album URL, surfaced on sensor.music_to_buy (state = count, attributes.links = items).
  • Each sync promotes any wanted track that has since appeared in Plex and drops it from the buy-list. You never hand-edit the list.
  • Plex playlist content is authoritative to the source: every sync makes a Plex playlist's track set and order match its Music Assistant source exactly — tracks removed from the source playlist are removed from the Plex playlist too, and tracks are reordered to match. The Plex API calls to reorder are skipped entirely when a playlist is already in the right order, so a normal sync where nothing changed does no extra work.
  • The playlist itself is never deleted. If a source playlist disappears from Music Assistant entirely (provider disconnected, playlist deleted upstream), its Plex playlist is simply left untouched — not emptied, not removed. Only playlists that are still present in this run's Music Assistant fetch get their contents reconciled.

Multiple Apple Music accounts

Music Assistant already distinguishes separate logins of the same provider (e.g. two Apple Music accounts) by provider instance, not just provider type — this integration reads and writes through that distinction, so nothing extra needs to be configured to mirror playlists from several Apple Music accounts at once. Each playlist keeps its originating account attached (visible in provider on the per-playlist sensor attributes), and playlist names only need to be unique per account, not globally.

Each playlist's tracks are fetched using the specific account's own mapping, not Music Assistant's generic "pick whichever mapping comes first" fallback — that fallback has no account awareness at all and can, in a multi-account setup, try to fetch one account's playlist using a different account's session, which Apple correctly rejects with a 403. Resolving the mapping ourselves avoids that entirely.

Account sessions expire — sometimes much sooner than the documented 180 days

Apple Music's Music-User-Token is meant to last ~180 days per Apple's own docs, but it's created by the browser/device, not the app, and Apple's own developer relations guidance is that it's meant to be re-derived each session — not persisted long-term by a backend. In practice this means a token stored server-side (exactly what Music Assistant does) can start failing with 403s after just a few days, and this is a widely-reported discrepancy independent of Music Assistant or this integration. There's no fix for this beyond re-authenticating that account's provider in Music Assistant's settings when it happens.

To make that obvious instead of something you have to notice in the logs: when every playlist attempted for one account fails in a sync (≥2 attempted, all failed — a strong signal of a dead session rather than one odd playlist), Music Own raises a persistent Home Assistant notification naming that account. It clears automatically once a following sync succeeds for that account again.

If you hit the "wrong account got re-authenticated" symptom (completing the re-auth flow for one account ends up granting a token for a different account you also have signed in), it's very likely because Music Assistant's browser-based sign-in reuses whatever Apple ID web session is already active in that browser — try a private/ incognito window, or sign out of Apple ID in that browser first. Music Assistant's "Manual token" option (extracting the media-user-token cookie directly from music.apple.com's DevTools) sidesteps this ambiguity because you can visually confirm which account is signed in on that page before copying the token — but per Music Assistant's own docs it's the same kind of token with the same nominal 180-day lifetime, so it likely won't change how often you need to re-authenticate.

Missing-track destinations: Qobuz buy-list and "Plex Wish List"

There are two ways to see what you're missing:

  1. Qobuz buy-list (sensor.music_to_buy and friends) — a durable list with buy links, purchase price comparisons, and hi-res availability. Push it to your Qobuz favorites on demand with the Push buy-list to Qobuz wishlist button.

  2. "Plex Wish List" — an Apple Music playlist, on whichever one Apple Music account you designate, that this integration keeps stocked automatically on every run: every track currently missing from Plex is added to it. This gives you a normal, playable Apple Music playlist of everything you're missing, no clicking required.

    Create a playlist named "Plex Wish List" (configurable via wishlist_playlist_name) on the Apple Music account of your choice inside Music Assistant, and add at least one track to it before your first sync — Music Assistant does not appear to index empty playlists into its library at all, so a playlist with zero tracks is invisible to get_library_playlists() and this integration will never find it (it'll just look like it isn't there; nothing will be logged about it either, since it never shows up in the first place). Once it has a track, it stays indexed even after this integration clears it back to your actual gaps. That playlist is automatically excluded from the normal MA → Plex mirror — it is a write-only destination for gaps and is never itself turned into a Plex playlist. If the name isn't found on any Apple Music account, this is logged as a warning each sync (not silent) and the rest of the sync (Plex mirror, Qobuz buy-list) proceeds unaffected; if it's found on more than one account, the first match is used and a warning asks you to rename the others.

    One-way sync, by design of Apple Music's API: this integration only ever adds to "Plex Wish List". Music Assistant's Apple Music provider unconditionally rejects removing tracks from a playlist ("Removing tracks from playlists is not supported by the Apple Music API. Make sure to delete them using the Apple Music app." — from MA's own source). So once a track you were missing gets bought and lands in Plex, it is not auto-removed from "Plex Wish List" — sensor.plex_wish_list's stale_needs_manual_removal attribute tells you how many tracks are sitting there that you can now delete by hand in the Apple Music app.

    Known soft spot: a missing track's identity comes from whatever provider originally hosted its source playlist (Spotify, Apple Music, ...), not necessarily the Apple Music account that owns "Plex Wish List". Whether Music Assistant can resolve a Spotify-sourced track into an Apple-Music-native playlist entry hasn't been verified against a live server yet — check sensor.plex_wish_list's failed_last_sync attribute and the logs after your first sync.

Why a snapshot store

Music Assistant playlists from a provider are a live view: disconnect the provider and they vanish. So a sync snapshots the desired tracks into SQLite before you disconnect a provider; the Plex playlists and the buy-list then persist.

Install

  1. Add this repo to HACS as a custom repository (category: Integration), install, restart HA.
  2. Settings → Devices & Services → Add Integration → Music Own. Enter the values below. Credentials are stored by HA; they are never placed in code or env.
  3. Add a dashboard card (see below).

Getting the values

  • MA URL: the WebSocket URL of your Music Assistant server, e.g. http://homeassistant.local:8095.
  • MA token: Music Assistant requires an access token (introduced in server schema version 31).
    1. Open the Music Assistant web UI (same host, port 8095).
    2. Go to Settings → Users → click your user.
    3. Scroll to Access TokensCreate token, give it a name (e.g. "Music Own").
    4. Copy the token immediately — it is only shown once.
  • Plex URL: base URL of your Plex server, e.g. http://homeassistant.local:32400.
  • Plex token: X-Plex-Token from any authenticated Plex request (sign in to plex.tv, open DevTools → Network, filter for plex.tv requests and look for the X-Plex-Token header or query param).
  • Qobuz app_id and Qobuz token: the Qobuz API requires both your app ID and a user session token. Get them in one step:
    1. Open the Qobuz web player and log in.
    2. Open DevTools (F12) → Network tab → filter for api.json.
    3. Click any request to api.json/0.2/....
    4. In Request Headers copy X-App-Id → paste as Qobuz app_id.
    5. In Request Headers copy X-User-Auth-Token → paste as Qobuz token. Nothing is downloaded; the integration only reads the Qobuz catalogue.
  • Wishlist playlist name (optional, default Plex Wish List): the exact name of the Apple Music playlist that should receive missing tracks. Leave it as the default unless you have a naming conflict.

Runbook (one-time migration)

  1. Connect Spotify and Apple Music (any number of accounts) in Music Assistant.
  2. Call service music_own.migrate. Owned tracks land in Plex playlists in source order; gaps go to the buy-list and to "Plex Wish List" if configured.
  3. Work through sensor.music_to_buy: click each Qobuz link, buy, let the files land in your Plex music folder. The next scheduled sync (or a manual migrate/ reconcile call) moves each newly-owned track into its Plex playlist automatically. It stays in "Plex Wish List" though (Apple Music's API doesn't support removing playlist tracks) — clear it out by hand periodically using sensor.plex_wish_list's stale_needs_manual_removal count as your guide.
  4. When a playlist shows 0 gaps it's fully in Plex. Disconnect Spotify (and Apple Music if you like) — Plex playlists and buy-list persist.

Steady state

Create new playlists in MA (Apple Music is fine for instant play). Every sync mirrors them into Plex and buys the gaps, same as above. If you reorder or trim a source playlist, the next sync brings the matching Plex playlist back in line automatically — no manual playlist editing needed on the Plex side.

Cleanup (careful)

music_own.cleanup deletes MA-native playlists that are fully present in Plex. It refuses to delete provider-backed playlists (Spotify/Apple Music/etc.), because deleting those through MA can propagate the delete to the streaming service. For provider playlists, "cleanup" = remove the provider from MA. This service only ever touches Music Assistant's own library entries — it never deletes a Plex playlist.

Sensors and buttons

All entities are grouped under the Music Own device.

Sensor State Key attributes
sensor.music_to_buy items to buy links, albums_count, tracks_count, tracks_wanted_raw, last_scan
sensor.albums_to_buy albums to buy downloadable/not-downloadable/unknown counts
sensor.tracks_to_buy individual tracks to buy downloadable/not-downloadable/unknown counts
sensor.hi_res_available_to_buy buy-list items with a hi-res Qobuz version downloadable/not-downloadable/unknown counts
sensor.ownership % of tracks owned tracks_owned, tracks_total, tracks_missing
sensor.last_sync timestamp of last completed sync
sensor.playlists_to_remove streaming playlists fully replicated in Plex playlists (name + provider + track count), last_scan
sensor.playlists_pending playlists with ≥1 track still to buy playlists (per-playlist breakdown), last_scan
sensor.tracks_owned tracks owned in Plex tracks_total, percent_owned, playlists (per-playlist breakdown), accounts (per-account health, see below), last_scan
sensor.plex_wish_list tracks currently in the "Plex Wish List" Apple Music playlist found, account, added_last_sync, failed_last_sync, stale_needs_manual_removal, last_scan

Buttons: Sync now (manual full sync), Reset buy-list database (wipes the wanted table and re-syncs from scratch), Push buy-list to Qobuz wishlist (adds the current buy-list to your Qobuz favorites).

Dashboard card

type: markdown
content: >
  ## 🛒 {{ states('sensor.music_to_buy') }} items to buy
  _{{ state_attr('sensor.music_to_buy', 'albums_count') }} albums ·
  {{ state_attr('sensor.music_to_buy', 'tracks_count') }} individual tracks_

  {% for it in state_attr('sensor.music_to_buy', 'links') or [] %}
  - [{{ it.artist }}  {{ it.title }}]({{ it.qobuz_url }}){% if it.is_album %} _(album · {{ it.track_count }} tracks)_{% elif it.album %} _({{ it.album }})_{% endif %}
  {% endfor %}

  ---
  **{{ states('sensor.tracks_owned') }} / {{ state_attr('sensor.tracks_owned', 'tracks_total') }} tracks owned
  ({{ state_attr('sensor.tracks_owned', 'percent_owned') }}%)**

  {% for pl in state_attr('sensor.tracks_owned', 'playlists') or [] %}
  - {{ pl.name }}: {{ pl.tracks_owned }}/{{ pl.tracks_total }}{% if pl.complete %} ✓{% endif %}
  {% endfor %}

  {% if states('sensor.playlists_to_remove') | int > 0 %}
  ---
  **{{ states('sensor.playlists_to_remove') }} playlist(s) ready to remove from streaming:**
  {% for pl in state_attr('sensor.playlists_to_remove', 'playlists') or [] %}
  - {{ pl.name }} _({{ pl.provider }}, {{ pl.tracks }} tracks)_
  {% endfor %}
  {% endif %}

  ---
  **Plex Wish List:** {{ states('sensor.plex_wish_list') }} track(s)
  {% if not state_attr('sensor.plex_wish_list', 'found') %}_(playlist not found this run)_{% endif %}

Clicking a link opens Qobuz in a new tab in the browser you're already logged into. (HA can't auto-open tabs — a click is required — which is exactly the manual buy step.)

Known soft spots

  • The Music Assistant client method names in engine.py (music.get_library_playlists, music.get_playlist_tracks, music.remove_playlist) and the ISRC field are marked TODO/VERIFY — confirm them against your MA :8095/api-docs. Without ISRC the matcher falls back to artist+title.
  • MATCH_THRESHOLD (default 85) trades false matches against missed ones. If the first sync reports far more gaps than expected, lower it or improve Plex tags.
  • "Plex Wish List" cross-provider track resolution is unverified — see above.