No description
  • Python 99.1%
  • Dockerfile 0.9%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Ladislav Brodecký c14704ad85 Remember the recognition playlist by id, not just by name
Renaming it in the Music Assistant UI used to orphan the list and create
a second one on the next share; the id is now cached per configured
name, so a UI rename keeps working while changing the setting still
points somewhere else.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-22 12:33:02 +02:00
.forgejo/workflows Auto-bump workflow for upstream MA patch releases 2026-08-13 00:58:00 +02:00
music_assistant_octave Remember the recognition playlist by id, not just by name 2026-08-22 12:33:02 +02:00
tests Score both readings of a dashed share: Audile sends Title - Artist 2026-08-22 12:30:16 +02:00
tools Import matcher skips http paths: use import://yt/ pseudo URIs 2026-08-18 22:28:03 +02:00
.gitignore Keep the ISRC on memoized tracks; drop a stray debug file 2026-08-18 10:01:16 +02:00
octave-api-reference.md Follow the api update: authenticate the resolver, detect the container 2026-08-22 11:13:05 +02:00
README.md Document the recognition endpoint 2026-08-22 12:31:17 +02:00
repository.json Fill in real Forgejo repo URL + codeowners 2026-08-13 00:26:43 +02:00

Octave Streaming → Music Assistant

Neoficiální read-only music provider pro Music Assistant (search + browse + přehrávání z veřejného katalogu Octave Streaming, Deezer ID). Volitelně i library sync z web účtu. API reference: octave-api-reference.md.

Repo je zároveň Home Assistant add-on repository — release se propíše do HA jako běžný update add-onu.

Struktura

repository.json                  # HA add-on repo marker
music_assistant_octave/          # HA add-on (wrapper nad oficiálním MA image)
├── config.yaml                  # verze add-onu — bump = release
├── build.yaml                   # pin MA base image (ghcr.io/music-assistant/server:X.Y.Z)
├── Dockerfile                   # kopíruje provider do site-packages v image
└── octave/                      # samotný provider
    ├── manifest.json
    ├── __init__.py              # MusicProvider implementace
    └── parsers.py               # Octave/Deezer JSON → MA modely (testovatelné standalone)
tests/test_parsers.py            # self-check parserů

MA (i 2.10-dev) načítá providery jen z package adresáře (music_assistant/providers/), žádný custom-provider dir neexistuje — proto injektáž přes vlastní add-on postavený FROM oficiálního server image.

Instalace v Home Assistantu

  1. Settings → Add-ons → Add-on Store → ⋮ → Repositories → přidej URL tohoto Forgejo repa (musí být veřejně klonovatelné, nebo s embedded credentials v URL).
  2. Nainstaluj Music Assistant (Octave). Build proběhne lokálně na HA hostu.
  3. Zastav oficiální MA add-on (stejné porty, host_network) — nesmí běžet oba.
  4. V MA UI přidej provider Octave Streaming.

Data oficiálního add-onu se nepřenáší automaticky (jiný slug = jiný /data) — detaily v DOCS.md.

Release flow (Forgejo)

  1. Uprav provider v music_assistant_octave/octave/.
  2. Bump version v config.yaml (schéma <MA verze>.<build>, např. 2.9.13.2).
  3. Commit + push (+ volitelně git tag).
  4. V HA: Add-on Store → ⋮ → Check for updates → u add-onu se objeví Update.

Upgrade MA verze: automatickyauto-bump.yml denně porovná upstream stable verzi MA add-onu a patch release (2.9.x) sám bumpne + pushne; HA add-on má auto_update, takže se update nasadí sám. Minor/major skok (2.10+) workflow schválně failne — MusicProvider interface se mezi releasy mění, ověř provider proti novému music_assistant/models/music_provider.py a bumpni ručně.

Provider — konfigurace

Volba Default Poznámka
base_url https://api.octavestreaming.com
quality flac FLAC 16/44.1; bez lossless masteru fallback na MP3 320. 128 = on-the-fly transcode, bez seeku
auth_token Bearer token web účtu (octv_…) → zapne library sync
sync_interval 60 s poll /api/sync?meta=1; full sync jen při změně verze. 0 = vypnuto
archive_provider instance Filesystem providera; zrcadlí, co má v MA srdíčko: tracky, celá alba, diskografie umělců, tracky playlistů (s octave mappingem) — jako otagované soubory (FLAC / MP3 320) do jeho složky + re-index. Bez srdíčka se nestahuje nic (import playlistů sám o sobě stahování nespouští). Vyžaduje token
archive_path advanced alternativa: libovolná absolutní cesta místo Filesystem instance
trash_retention_days 90 advanced: purge .trash/ souborů starších než X dní (od unlike; mtime se při trashnutí přerazí). 0 = navždy
recognize_playlist Recognized jméno playlistu pro rozpoznanou hudbu z mobilu; publikuje sdílecí endpoint /octave/recognize (klíč odvozený ze server_id, URL v logu při startu). Prázdné = vypnuto
storefront us jen editorial (spotlight)

Implementační poznámky

  • Playback: GET /api/track/{id} vrací podepsanou URL. Od API updatu 2026-08-21 vyžaduje resolver Bearer token — bez něj přijde gated: true a URL bez podpisu, na kterou /audio/* odpoví 401. Provider proto volá resolver s _auth=True a bez tokenu hlásí srozumitelnou chybu (přehrávání už není možné v režimu „jen katalog"). Resolvuje se až v get_stream_details, podpisy jsou krátkodobé — nikdy se nepersistují.
  • Formát z bajtů, ne z labelu: tier 320 teče u části katalogu jako MP4/AAC (audio/mp4, magic ftyp), jinde pořád MP3; lossless je FLAC. Provider čichne prvních 12 bajtů a podle nich deklaruje kontejner (FLAC / M4A / MP3, jinak bez hintu). Archiver stejně volí příponu .flac / .m4a / .mp3.
  • Lossless: kvalitu vybírá resolver — GET /api/track/{id}?quality=lossless vrátí podepsanou URL a v poli quality ohlásí, co skutečně dal. Pozor 1: u skladby bez lossless masteru resolver spadne na MP3 128, což je horší než výchozích 320 — provider to detekuje a doptá se znovu bez parametru. Pozor 2 (živě 2026-08-18): u části tracků resolver ohlásí lossless, ale /audio/lossless reálně servíruje MP3 bajty (0xFFFB, audio/mpeg). Tvrdý FLAC codec hint pak shodí ffmpeg („Invalid data") a MA track přeskočí — proto provider u lossless čichne první 4 bajty (range request) a deklaruje formát podle reality; při selhání probe nechá detekci na ffmpeg. Pozor 3 (živě 2026-08-20): ty „lossless" MP3 bajty jsou 128 kbps. U části tracků přitom origin nemá nic lepšího — /audio/320 i /audio/lossless vracejí bajt v bajt stejný 128 kbps soubor (ověřeno: id 3127743, 6907156). Provider i archiver proto po zjištění ne-FLAC bajtů zkusí ještě výchozí URL, a archiver do manifestu píše změřenou kvalitu (quality: „lossless" / „320" / „128" z ffprobe) plus claimed = co na lossless dotaz řekl resolver. Ta lež je přechodná — týž track (id 468232842) vrátil jednou 128 kbps a podruhé skutečný FLAC 939 kbps. Upgrady se přesto nedělají: MA váže položku knihovny na archivovaný soubor, takže jeho výměna při dalším indexu položku zruší i s uživatelovým srdíčkem (živě ověřeno 2026-08-20, 9 skladeb). Archiver proto existující soubor nikdy nepřepisuje a stahuje jen to, co na disku chybí — kvalita v manifestu slouží jen jako poctivá informace.
  • Latence startu (změřeno 2026-08-18 debug logem): provider dodá streamdetails za 6256 ms. Zbytek je mimo něj — audio origin má u nedávno nehrané skladby TTFB 1,55 s (stejně pro lossless i 320), MA přidává pevný 1 s debounce na next/prev a čeká na naplnění bufferu. Mrtvé položky v katalogu dřív stály 1333 s čekáním na timeout; teď se poznají podle prázdného preview a přeskočí okamžitě.
  • Retry-After je cooldown, ne pauza (jejich vlastní changelog 2026-08): backend posílá Retry-After: 900 u tracku, který teď nedokáže naservírovat. MA respektuje serverovou hodnotu až do hodiny (MAX_RETRY_AFTER=3600), takže by na 15 minut uspal přehrávač. Provider proto cokoli nad MAX_SERVER_COOLDOWN (30 s) překlápí na ResourceTemporarilyUnavailable — track se přeskočí, fronta běží dál.
  • available: false: výpisy (typicky alba) označují nehratelné položky — celé album může být mrtvé (např. 507208391 Marigold Soundsystem, všech 9 tracků). Parser to promítá do ProviderMapping.available, ať MA takový track vůbec nezařadí do fronty.
  • Měření API: /api/* odmítá Python-urllib User-Agent, ale /audio/* naopak blokuje podvržené prohlížečové UA (Cloudflare 403). MA posílá aiohttp UA a projde obojím — při benchmarcích audia nikdy neposílat falešný Chrome UA, výsledky jsou pak nesmyslné.
  • Library sync: vyžaduje auth_token; čte /api/syncoctave:library.state (liked playlist, savedAlbums, followedArtists, playlists). Jednosměrné Octave → MA. Near-realtime přes poll ?meta=1 (jen {version, updatedAt}).
  • Metadata tracku: API nemá přímý track-metadata endpoint (/api/track/{id} je resolver, /api/dz/track/{id} už neexistuje) → get_track jde přes credits → ISRC → /api/track/isrc/{isrc}. Provider si proto pamatuje raw payloady z výpisů (_track_memo) a ten řetězec použije jen při promáchnutí; ISRC se doplní z dlouho cachovaných credits, protože výpisy ho neobsahují.
  • Cache: dvě úrovně, obě stale-while-revalidate — 30 dní na neměnná data (track, album, umělec, diskografie) a 1 h na proměnlivá (search, playlisty, editorial). Throttle 25 req/s + retry/backoff na 429/503, timeout 30 s (connect 10 s) — mrtvé tracky chytá prázdný preview, ne timeout, takže pomalý cold origin nesmí shazovat živé skladby.
  • Browse: zatím jen spotlight track — Apple editorial charts používají Apple ID, ne Deezer, chybí mapovací vrstva.
  • Explore/Recommendations (ProviderFeature.RECOMMENDATIONS): 6 řádků — Tvůj mix (radio ze 3 náhodných MA favorites), #1 dnes (spotlight, /api/spotlight/{sf} — region je path segment, query param API ignoruje), Světové hity (/api/lastfm/global-chart-tracks), Dnešní hity: (Apple chart /api/apple/charts?sf= mapovaný per-song přes /api/search/tracks; fallback /api/lastfm/geo), Stanice pro tebe (virtuální playlisty radio-artist-<id>/api/artist/{id}/radio, umělci dohledaní přes /api/search/artists z názvů v MA favorites), Podobné jako <umělec> (/api/similar/artists). Last.fm proxy vrací už namapované Deezer objekty vč. coverů. Texty řádků lokalizované podle mass.metadata.preferred_language (cs/en, REC_STRINGS). Vše přes 1h/30d cache; každý builder selhává samostatně.
  • Archiv knihovny (archive.py): zdroj pravdy je MA knihovna — octave tracky s favorite=1 + tracky všech MA playlistů (octave web se přímo nečte, jeho library teče do MA běžným syncem; pozor, provider sync favorite flag v MA NENASTAVUJE). Triggery: set_favorite callback (lajk v MA UI — vyžaduje FAVORITE_TRACKS_EDIT featuru), event bus (změny playlistů), změna octave sync verze, start add-onu. Vše debounced. Zrcadlí se do složky Filesystem providera (SMB mount /tmp/<instance_id> žije ve stejném kontejneru). Resolver rozhodne kvalitu (lossless → FLAC, jinak 320; 128 fallback se detekuje a bere se místo něj 320), kontejner se pozná podle fLaC magic bytes, taguje ffmpeg (-c:a copy, vč. ISRC — na něm stojí párování s Octave trackem), vedle souborů přistane cover.jpg. Stav drží .octave-archive.json v kořeni archivu; odebrané tracky jdou do .trash/ (nikdy hard delete), ne-lossless soubory se při další změně zkusí upgradnout. Po úspěšném zápisu se spustí sync té Filesystem instance — MA pak při přehrávání sám preferuje lokální lossless kopii.
  • Patch upstream bugu importu (patches/builtin_resolve_path_uri.py, aplikuje se při buildu image): matcher importovaných playlistů zapisuje spárované URI jen do cesty M3U položky, ale resolver staví dostupnost výhradně z #EXTPROV: řádků → spárované tracky zůstávaly šedé a playlist hlásil „No playable items found". Patch přidává fallback: cestu s :// rozresolvovat přes get_item_by_uri. Patch build nikdy neshodí: když upstream blok změní nebo opraví, tiše se přeskočí (auto-bump běží dál; nejhorší dopad = importy zase zšednou).
  • AppArmor: add-on přibaluje apparmor.txt (kopie z oficiálního add-onu). Bez něj Supervisor nasadí docker-default profil, který tiše zařízne mount(2) — SMB provider pak hlásí mount error(13) dřív, než odejde jediný paket (žádný CIFS záznam v dmesg, nic v samba logu).

Jednorázový import ze Spotify / YT Music

Pro libovolného MA uživatele, bez API klíčů:

  1. Spotify: exportify.app → export playlistu / Liked Songs jako CSV. YT Music: Google Takeout → YouTube Music → CSV playlistů.
  2. python tools/ma_import_convert.py export.csv --name "Spotify Liked" → vznikne .m3u8 (sloupce se poznají samy, en i cs hlavičky).
  3. V MA (přihlášen daný uživatel): Playlists → Import playlist → nahrát .m3u8 a zapnout track matching. MA na pozadí dohledá tracky u octave.
  4. Playlist ❤️ → archiver ho celý postupně stáhne na share.
  • Rozpoznávání z mobilu (recognize.py): dynamická route na webserveru MA (mass.webserver.register_dynamic_route, tedy mimo auth — chrání ji klíč odvozený z server_id + instance_id). Identita před jménem: Deezer odkaz → ID katalogu, MusicBrainz odkaz → ISRC přes MA MusicBrainz provider, holý ISRC → track/isrc/{isrc}; teprve pak skórované hledání (normalizace bez diakritiky/závorek, obě pořadí interpret/název, práh MIN_SCORE). Přidává přes mass.music.playlists.add_playlist_tracks — běžíme uvnitř MA, takže žádný MA login není potřeba (WS API vyžaduje auth i z localhostu).

Test

pip install music-assistant-models==1.1.129.post1 aiohttp
python tests/test_parsers.py
python tests/test_archive.py
python tests/test_recognize.py
python tests/test_import_convert.py

Omezení

  • Reverse-engineered neoficiální API — může se kdykoli změnit/rozbít; použití mimo oficiální klient je pravděpodobně proti ToS služby.
  • Library sync je jednosměrný (Octave → MA) a vyžaduje token web účtu; lajk udělaný v MA se do Octave nepropíše.

Repo: https://git.xn--la-mia8p.eu/ladab/music-assistant-octave (punycode pro git.láďa.eu — do HA add-on store zadávej punycode formu, git/curl si IDN převádí špatně).