No description
  • Dart 94.8%
  • CMake 3%
  • C++ 1.9%
  • C 0.3%
Find a file
2026-09-11 16:07:14 +02:00
.claude Fix "This speaker" casting, add fake-HA casting tests, CI, and agent guide 2026-09-05 20:35:14 +02:00
.github/workflows Gate formatting in CI so the drift cannot return 2026-09-11 07:16:24 +02:00
android Initial scaffold: Linux+Android Flutter app with HA/Plex/Sonos backend layer 2026-06-13 12:41:14 +02:00
lib Give each room in the volume panel its full name and its level 2026-09-11 15:45:08 +02:00
linux Build rich Plex→Sonos player UI on the backend layer 2026-06-13 14:10:55 +02:00
test Give each room in the volume panel its full name and its level 2026-09-11 15:45:08 +02:00
.gitignore Fix "This speaker" casting, add fake-HA casting tests, CI, and agent guide 2026-09-05 20:35:14 +02:00
.metadata Initial scaffold: Linux+Android Flutter app with HA/Plex/Sonos backend layer 2026-06-13 12:41:14 +02:00
analysis_options.yaml Fix "This speaker" casting, add fake-HA casting tests, CI, and agent guide 2026-09-05 20:35:14 +02:00
CHANGELOG.md Give each room in the volume panel its full name and its level 2026-09-11 15:45:08 +02:00
CLAUDE.md Gate formatting in CI so the drift cannot return 2026-09-11 07:16:24 +02:00
pubspec.lock Drop cupertino_icons, which nothing here uses 2026-09-11 15:51:54 +02:00
pubspec.yaml Drop cupertino_icons, which nothing here uses 2026-09-11 15:51:54 +02:00
README.md Make the group fader reach silence and full volume in every room 2026-09-10 23:21:49 +02:00

Plex Sonos Caster

A rich, animated Flutter music app (Linux + Android) that browses your Plex library and casts it to your Sonos speakers — a single speaker or every speaker at once — orchestrated through Home Assistant.

The app talks to your Plex Media Server directly for metadata and artwork, and uses Home Assistant to group Sonos speakers and drive playback.

Features

  • Browse your music library: albums, artists, and playlists, with a recently-added shelf and disk-cached album art.
  • Search across artists, albums, and songs with live results.
  • Cast any album, artist, playlist, or track to one speaker, to any set of Sonos rooms, or to all Sonos at once (whole-home grouping). Non-Sonos HA media players (Google Cast / DLNA, e.g. an LG soundbar) are supported too, as single targets.
  • Full transport controls: play/pause, stop, next/previous, shuffle, repeat, and seek — each enabled per the target's reported capabilities.
  • Group volume on one slider: every room moves by the same amount, keeping the balance between them, while 0% and 100% still mean silence and full volume in every room. Per-room sliders sit underneath.
  • Now playing: album-art-driven gradient ambience, a live-ticking progress bar, and a persistent mini-player docked above the navigation.
  • Up Next queue: see the tracks you cast and which one is playing.
  • Ratings: Plex star ratings (5 stars, half-star precision) shown across the library and the player.
  • Device picker that detects already-grouped Sonos zones and lets you reshape a group while the music keeps playing.

How it works

┌──────────────┐   browse / search    ┌──────────────────┐
│              │ ───────────────────▶ │  Plex Media      │
│  Flutter app │                      │  Server (direct) │
│  (this repo) │                      └──────────────────┘
│              │   discover · group · play_media
│              │ ───────────────────▶ ┌──────────────────┐
└──────────────┘                      │  Home Assistant  │ ──▶ Sonos speakers
                                       └──────────────────┘
  1. Discover Sonos speakers — media_player entities exposing a group_members attribute.
  2. Group the selected rooms behind one coordinator: rooms that were in the group but weren't picked leave via media_player.unjoin first, then the rest join it with media_player.join.
  3. Cast a Plex item to the coordinator with media_player.play_media, using a plex://<machineId>/<ratingKey> content id that HA's Plex integration resolves and streams to the whole group.
  4. Poll the coordinator's media_player state for the now-playing view.

Project layout

lib/
  main.dart                         App entry; wires controllers + theme
  src/
    backends.dart                   Barrel export for the backend layer
    config/                         url + token value objects
    services/
      home_assistant_backend.dart   REST client (states + service calls)
      plex_backend.dart             REST client (search / browse / artwork)
      sonos_service.dart            Discovery, grouping, casting, playback state
      settings_service.dart         On-device persistence (shared_preferences)
    ui/
      app_scope.dart                Inherited access to the controllers
      theme.dart                    Dark theme, colors, gradients, motion
      navigation.dart               Fade/slide page transitions
      controllers/                  connection · library · playback (ChangeNotifier)
      screens/                      root · settings · library · search · detail ·
                                    now-playing · main shell
      widgets/                      album art · cards · track tile · mini player ·
                                    device picker · queue sheet · stars · …

The backend layer (src/config, src/services) imports no Flutter code: plain Dart REST clients and models (its one plugin dependency is settings_service.dart's shared_preferences), unit-tested against an in-memory Home Assistant (test/support/fake_home_assistant.dart). The UI layer talks to it only through the service classes.

Configuration

Two connections are entered in-app on first run and persisted locally:

Setting Example Notes
Home Assistant URL http://homeassistant.local:8123
Home Assistant token long-lived access token Profile → Security
Plex URL http://192.168.1.10:32400
Plex token X-Plex-Token
Music library chosen from a dropdown pick Music vs. Audiobooks etc.

Tap Test connections to verify reachability and load the list of music libraries. Local HTTP (cleartext) traffic is enabled in the Android manifest so on-network servers work without TLS.

Casting format: the app addresses Plex media as plex://<plexMachineIdentifier>/<ratingKey>, the only form HA's Plex integration resolves for Sonos and Cast players. This and the other hard-won details are in CLAUDE.md under "Gotchas".

Speakers

  • Sonos is first-class: pick any set of rooms (or "Everywhere") and they are grouped behind the one you picked first. Picking a single speaker always makes it leave its current group first, so the cast plays on that speaker alone — and the rooms it leaves keep playing what they were playing.
  • Google Cast / DLNA players (e.g. an LG soundbar) work as single targets. HA's Cast integration resolves the same plex:// id, so casting works — but many Cast endpoints only expose play/pause/stop/seek/volume, not skip/shuffle/repeat. The app reads each player's supported_features and greys out controls a device can't do. Such players also often publish no artwork; the app recovers it from Plex.

Limitations

  • Sonos/HA does not expose its live queue over REST, so "Up Next" reflects the collection you cast. For a shuffled cast the songs are correct but the displayed order is the original collection order, not HA's random order.
  • Skip/shuffle/repeat on non-Sonos devices depend on what the player exposes to HA; many Cast devices don't expose them, and there is no client-side queue engine to work around that.
  • Now-playing ratings are looked up from Plex by title, so they appear a moment after the track changes.

Development

flutter pub get
flutter run -d linux            # desktop
flutter run -d <android-device> # Android
flutter analyze                 # must be clean
flutter test                    # casting tests run against an in-memory HA

CI runs flutter analyze and flutter test on pull requests and on pushes to main (.github/workflows/ci.yml). Contributor conventions, boundaries, and the definition of done are in CLAUDE.md.

Acknowledgements

The Home Assistant and Plex REST clients are adapted from angela-dart.