|
| 1 | +## Project Overview |
| 2 | + |
| 3 | +Sendspin CLI is a synchronized audio player client for the [Sendspin Protocol](https://github.com/Sendspin/website/blob/main/src/spec.md). It connects to Sendspin servers via WebSocket, receives time-synchronized audio streams, and plays them back with precise timing to enable multi-room synchronized audio. |
| 4 | + |
| 5 | +**Note**: If uncertain about how something in Sendspin is supposed to work, fetch and refer to the [protocol specification](https://github.com/Sendspin/website/blob/main/src/spec.md) for authoritative implementation details. |
| 6 | + |
| 7 | +## Architecture |
| 8 | + |
| 9 | +``` |
| 10 | + cli.py (main entry) |
| 11 | + │ |
| 12 | + ┌───────────────┼───────────────┐ |
| 13 | + │ │ |
| 14 | + ▼ ▼ |
| 15 | + ┌──────────┐ ┌──────────┐ |
| 16 | + │ tui/ │ │ daemon/ │ |
| 17 | + └──────────┘ └──────────┘ |
| 18 | + │ │ |
| 19 | + ┌───────────┼───────────┐ │ |
| 20 | + │ │ │ │ |
| 21 | + ▼ ▼ ▼ ▼ |
| 22 | +┌────────────┐ ┌─────────┐ ┌─────────┐ ┌─────────────┐ |
| 23 | +│ keyboard │ │ ui │ │ app │ │ daemon │ |
| 24 | +│ │ │ │ │ │ │ │ |
| 25 | +│ - Keys │ │ - TUI │ │ - State │ │ - Headless │ |
| 26 | +│ - Commands │ │ - Panel │ │ - Tasks │ │ - Signals │ |
| 27 | +└────────────┘ └─────────┘ └─────────┘ └─────────────┘ |
| 28 | + │ │ │ │ |
| 29 | + └───────────┼───────────┴───────────────────┘ |
| 30 | + │ |
| 31 | + ┌───────────┼───────────────────┐ |
| 32 | + │ │ │ |
| 33 | + ▼ ▼ ▼ |
| 34 | +┌─────────────┐ ┌───────────────┐ ┌──────────────┐ |
| 35 | +│ audio.py │ │ discovery.py │ │ aiosendspin │ |
| 36 | +│ │ │ │ │ (external) │ |
| 37 | +│ - Playback │ │ - mDNS │ │ │ |
| 38 | +│ - Time sync │ │ - Servers │ │ - WebSocket │ |
| 39 | +└─────────────┘ └───────────────┘ └──────────────┘ |
| 40 | +``` |
| 41 | + |
| 42 | +## File Responsibilities |
| 43 | + |
| 44 | +### `README.md` |
| 45 | +Project documentation, installation instructions, usage guide (including command line arguments), configuration options. |
| 46 | + |
| 47 | +### `sendspin/cli.py` |
| 48 | +Main entry point and orchestrator. Responsibilities: |
| 49 | +- **Argument parsing**: Commands (`serve`, `daemon`), flags (`--url`, `--name`, `--id`, `--audio-device`, `--static-delay-ms`) |
| 50 | +- **Command routing**: Routes to appropriate mode (TUI app, daemon, or server) |
| 51 | +- **Device enumeration**: Lists audio devices and servers via `--list-audio-devices` and `--list-servers` |
| 52 | +- **Backward compatibility**: Handles deprecated `--headless` flag with warning |
| 53 | + |
| 54 | +### `sendspin/tui/` (TUI Mode Package) |
| 55 | +Interactive terminal UI mode with keyboard control. |
| 56 | + |
| 57 | +#### `sendspin/tui/app.py` |
| 58 | +TUI mode application logic. Responsibilities: |
| 59 | +- **Service discovery**: mDNS discovery of Sendspin servers |
| 60 | +- **Connection management**: `ConnectionManager` handles reconnection with exponential backoff |
| 61 | +- **State management**: `AppState` dataclass mirrors server state (playback, metadata, volume) |
| 62 | +- **Audio stream handling**: `AudioStreamHandler` bridges between client and `AudioPlayer` |
| 63 | +- **Event callbacks**: Routes server messages to appropriate handlers (metadata, group updates, commands) |
| 64 | +- **UI coordination**: Manages `SendspinUI` and keyboard input loop |
| 65 | + |
| 66 | +#### `sendspin/tui/keyboard.py` |
| 67 | +Keyboard input handling for interactive control. Responsibilities: |
| 68 | +- **Key capture**: Uses `readchar` library for single-keypress detection |
| 69 | +- **Command parsing**: `CommandHandler` class parses and executes commands |
| 70 | +- **Media commands**: play, pause, stop, next, previous, shuffle, repeat modes, switch group |
| 71 | +- **Volume control**: Group volume (`vol+`/`vol-`/`mute`) and player volume (`pvol+`/`pvol-`/`pmute`) |
| 72 | +- **Delay adjustment**: Real-time static delay adjustment via `delay` command |
| 73 | +- **Keyboard shortcuts**: Arrow keys (prev/next/volume), space (toggle), m (mute), g (switch group), q (quit) |
| 74 | + |
| 75 | +#### `sendspin/tui/ui.py` |
| 76 | +Rich-based terminal UI for visual feedback. Responsibilities: |
| 77 | +- **Now Playing panel**: Track title, artist, album with playback shortcuts |
| 78 | +- **Volume panel**: Group and player volume with mute indicators |
| 79 | +- **Progress bar**: Real-time track progress with interpolation during playback |
| 80 | +- **Status line**: Connection status and quit shortcut |
| 81 | +- **Shortcut highlighting**: Visual feedback when shortcuts are pressed (0.15s highlight) |
| 82 | +- **State management**: `UIState` dataclass for all display state |
| 83 | + |
| 84 | +### `sendspin/daemon/` (Daemon Mode Package) |
| 85 | +Headless operation mode for background services. |
| 86 | + |
| 87 | +#### `sendspin/daemon/daemon.py` |
| 88 | +Daemon mode for headless operation (via `sendspin daemon` or deprecated `--headless`). Responsibilities: |
| 89 | +- **Headless operation**: Runs without UI or keyboard input |
| 90 | +- **Service discovery**: Same mDNS discovery as TUI mode |
| 91 | +- **Connection management**: Reuses `ConnectionManager` from `tui/app.py` |
| 92 | +- **Audio playback**: Same synchronized audio playback |
| 93 | +- **Event logging**: Prints events to stdout instead of UI |
| 94 | +- **Signal handling**: SIGINT/SIGTERM for graceful shutdown |
| 95 | + |
| 96 | +### `sendspin/audio.py` |
| 97 | +Time-synchronized audio playback engine. Responsibilities: |
| 98 | +- **Audio output**: Uses `sounddevice` (PortAudio) for low-latency playback |
| 99 | +- **Time synchronization**: DAC-to-loop time calibration for precise playback timing |
| 100 | +- **Buffer management**: Queue-based buffering with gap/overlap handling |
| 101 | +- **Sync correction**: Sample drop/insert for drift correction (Kalman-filtered) |
| 102 | +- **Playback state machine**: INITIALIZING → WAITING_FOR_START → PLAYING → REANCHORING |
| 103 | +- **Volume control**: Software volume with power curve for natural control |
| 104 | + |
| 105 | +### `sendspin/__init__.py` |
| 106 | +Package entry point, exports `main` from `cli.py`. |
| 107 | + |
| 108 | +## Key Dependencies |
| 109 | + |
| 110 | +- **aiosendspin**: Async Sendspin protocol client (WebSocket, time sync, messages) |
| 111 | +- **sounddevice**: PortAudio wrapper for audio I/O |
| 112 | +- **rich**: Terminal UI rendering |
| 113 | +- **readchar**: Cross-platform keyboard input |
| 114 | +- **zeroconf**: mDNS service discovery |
| 115 | +- **numpy**: Audio sample processing |
| 116 | + |
| 117 | +## Common Tasks |
| 118 | + |
| 119 | +### Running the player |
| 120 | +```bash |
| 121 | +uv run sendspin # TUI mode with auto-discovery |
| 122 | +uv run sendspin --url ws://host:port/sendspin # TUI mode with direct connection |
| 123 | +uv run sendspin daemon # Daemon mode (headless) with auto-discovery |
| 124 | +uv run sendspin daemon --url ws://host:port/sendspin # Daemon mode with direct connection |
| 125 | +``` |
| 126 | + |
| 127 | +### Development commands |
| 128 | +```bash |
| 129 | +uv run ruff check --fix . # Lint and auto-fix |
| 130 | +uv run ruff format . # Format |
| 131 | +uv run mypy sendspin # Type check |
| 132 | +``` |
| 133 | + |
| 134 | +### Before pushing commits |
| 135 | +Run pre-commit before pushing any commit to ensure it is valid: |
| 136 | +```bash |
| 137 | +uv run pre-commit run --all-files |
| 138 | +``` |
| 139 | + |
| 140 | +### Adding a new keyboard shortcut |
| 141 | +1. Add key handler in `tui/keyboard.py` `keyboard_loop()` function |
| 142 | +2. Add UI highlight call: `ui.highlight_shortcut("shortcut_name")` |
| 143 | +3. Execute command via `handler.execute("command")` |
| 144 | +4. Update shortcut display in `tui/ui.py` `_build_now_playing_panel()` or relevant panel |
| 145 | + |
| 146 | +### Adding a new media command |
| 147 | +1. Check if `MediaCommand` enum in aiosendspin supports it |
| 148 | +2. Add command alias in `CommandHandler.execute()` in `tui/keyboard.py` |
| 149 | +3. Command is sent via `client.send_group_command(MediaCommand.XXX)` |
| 150 | + |
| 151 | +### Adding a new configuration option (CLI + settings file) |
| 152 | +Options should be configurable via both CLI flags and the settings file, with CLI taking precedence. |
| 153 | + |
| 154 | +1. **Add to `Settings` dataclass** in `sendspin/settings.py`: |
| 155 | + - Add field with default value (e.g., `use_mpris: bool = True`) |
| 156 | + - Add to `to_dict()` return value |
| 157 | + - Add to `from_dict()` with `data.get("field_name", default)` |
| 158 | + |
| 159 | +2. **Add property to `SettingsManager`** in `sendspin/settings.py`: |
| 160 | + - Add `@property` that returns `self._settings.field_name` |
| 161 | + - Optionally add to `update()` method if it should be changeable at runtime |
| 162 | + |
| 163 | +3. **Add CLI argument** in `sendspin/cli.py` `parse_args()`: |
| 164 | + - Add to both `daemon_parser` and main `parser` if applicable to both modes |
| 165 | + - Use `action="store_true"` for boolean flags |
| 166 | + |
| 167 | +4. **Apply settings default** in `_apply_settings_defaults()`: |
| 168 | + - Compute final value: `args.option = <cli_value> or settings.option` |
| 169 | + - For disable flags: `args.use_x = not args.disable_x and settings.use_x` |
| 170 | + |
| 171 | +5. **Add to Args dataclasses**: |
| 172 | + - Add field to `DaemonArgs` in `daemon/daemon.py` |
| 173 | + - Add field to `AppArgs` in `tui/app.py` |
| 174 | + |
| 175 | +6. **Pass to apps** in `cli.py`: |
| 176 | + - Add `option=args.option` when creating `DaemonArgs` and `AppArgs` |
| 177 | + |
| 178 | +7. **Use in daemon/TUI**: |
| 179 | + - Access via `self._args.option` |
| 180 | + |
| 181 | +8. **Document** in `README.md` (required for user-facing features): |
| 182 | + - Add to example config JSON |
| 183 | + - Add to settings table |
| 184 | + - Add usage section if the feature warrants explanation |
| 185 | + |
| 186 | +**Important:** All new user-facing features (CLI options, settings, commands) must be documented in `README.md`. |
0 commit comments