playlists and DLNA playback

This commit is contained in:
2026-08-28 12:51:12 +02:00
parent 69433abee1
commit cd16e3ae42
12 changed files with 1028 additions and 415 deletions
+57 -32
View File
@@ -39,11 +39,13 @@ flowchart TB
Server[private/server.rkt<br/>HTTP adapter]
Users[private/users.rkt<br/>users, networks and sessions]
Player[private/player.rkt<br/>application state and commands]
Playlists[private/playlists.rkt<br/>durable playlist tabs]
DLNAAdapter[private/dlna-playback.rkt<br/>playlist transition orchestration]
Library[private/library.rkt<br/>filesystem and metadata]
UI[public/index.html + styles.css + app.js<br/>browser UI]
Audio[racket-audio<br/>local backend]
Discovery[racket-upnp + racket-sonos<br/>device discovery]
DLNA[racket-audio-dlna<br/>network backend and media server]
DLNA[racket-audio-dlna<br/>transport, seeking and media publication]
AgentGUI[private/player-agent-gui.rkt<br/>GUI adapter]
AgentCLI[player-agent-cli.rkt<br/>CLI adapter]
AgentCore[private/player-agent-core.rkt<br/>polling and audio runtime]
@@ -55,9 +57,11 @@ flowchart TB
Server --> Users
Server --> UI
Player --> Library
Player --> Playlists
Player --> Audio
Player --> Discovery
Player --> DLNA
Player --> DLNAAdapter
DLNAAdapter --> DLNA
AgentGUI --> AgentCore
AgentCLI --> AgentCore
AgentCore --> Server
@@ -115,7 +119,7 @@ relative paths originated from directory listings below a configured root.
The mutable `player` structure is the aggregate root for:
- configured libraries and the current browser location;
- in-memory playlist tabs and the selected tab's tracks;
- playlist tabs, the selected tab, and their durably stored tracks;
- discovered renderers and the selected renderer;
- registered HTTP playback agents and their pending command queues;
- the lazily created local or network backend;
@@ -130,9 +134,12 @@ required, and returns a complete JSON-compatible state snapshot. Commands cover
library navigation, playlist/tab editing, output selection, transport, seeking,
volume, and repeat mode.
Playlist tabs exist only in memory. Selecting, deleting, or creating a tab
stops playback. Tracks are de-duplicated by normalized source path when they are
appended.
Selecting, deleting, or creating a tab stops playback. Tracks are de-duplicated
by normalized source path when they are appended. Every playlist mutation is
written in one `keystore` transaction. `playlists-for-<username>` contains the
ordered playlist GUIDs; each GUID key contains that playlist's name and tracks.
Loading validates every stored track independently against all configured
library roots, so one playlist can safely combine multiple libraries.
### 3.4 Playback backends
@@ -145,15 +152,21 @@ update the player and automatically advance the playlist. The UI's linear
0-100 volume is squared before being sent to the local audio library to provide
a more useful perceived volume curve.
Network outputs use `racket-audio-dlna`. The backend controls the chosen media
renderer and publishes local files over HTTP on the configured DLNA port and
path. After starting a track, the server publishes the following track and sets
it as the renderer's `NextAVTransportURI`. Network playback information is
refreshed whenever a state snapshot is requested. A changed current URI
promotes the prepared playlist index and immediately prepares its successor.
If a renderer does not perform the prepared transition, a confirmed natural
stop advances through a server-driven fallback. Explicit stops and failed play
requests are tracked separately and never trigger that fallback.
Network outputs use the small `private/dlna-playback.rkt` adapter. It owns only
the playlist transition state machine; all transport commands, seeking, HTTP
publication, UPnP calls and cached renderer information are delegated to
`racket-audio-dlna`. After starting a track, the adapter asks that package to
publish the following track and set it as the renderer's
`NextAVTransportURI`.
The adapter polls the package's cached renderer information independently of
browser requests. A changed current URI promotes the prepared playlist index
and immediately prepares its successor. If a renderer does not perform the
prepared transition, a confirmed natural stop advances through a
server-driven fallback. Explicit stops and failed play requests are tracked
separately and never trigger that fallback. A seek uses
`dlna-player-seek-percentage!` directly; its synchronously updated cached
position is pushed to the application state immediately.
Changing the selected renderer closes the existing backend and resets playback
state. The replacement backend remains lazy and is created only when it is
@@ -269,16 +282,24 @@ sequenceDiagram
participant B as Browser
participant H as HTTP server
participant P as Player
participant A as DLNA playback adapter
participant L as racket-audio-dlna
participant D as DLNA renderer
loop Every second
loop Browser state polling
B->>H: GET /api/state
H->>P: player-state->jsexpr
P->>D: Query transport, position, track and volume
D-->>P: Current renderer information
P-->>H: Full state snapshot
H-->>B: JSON response
end
loop DLNA adapter polling
L->>D: Poll transport and position
D-->>L: Current renderer information
A->>L: Read cached state
L-->>A: DLNA info
A-->>P: Update track, transport and time state
end
```
## 5. Concurrency and consistency
@@ -311,15 +332,16 @@ file:
- DLNA media publication port, defaulting to `8734`;
- named library root paths under `[libraries]` (the legacy semicolon-separated
setting remains supported);
- allowed 256-bit playback-agent IDs under `[playback-agents]`.
- allowed 256-bit playback-agent IDs under `[playback-agents]`;
- the optional playlist-keystore override under `[player]`;
- Argon2id user hashes, local networks, trusted proxies, and session timeout.
Command-line network settings override INI values. Library paths from both
sources are combined and de-duplicated.
There is no database, migration process, user account, or persistent playlist
store. Restarting the process resets playlists, output discovery, transport
state, and all other mutable state.
Playlist tabs use the SQLite-backed `keystore` module at
`data/playlists.keystore`. Output discovery, transport state, playback position
and sessions still reset when the process restarts.
## 7. Security and operational boundaries
@@ -353,10 +375,13 @@ subsequent polling requests.
## 8. Testing and extension points
Unit tests embedded in `private/library.rkt` cover root creation, filtering, and
directory ordering. Tests in `private/player.rkt` cover initial state, browser
navigation, repeat mode, playlist-tab operations, unknown commands, and clean
shutdown. The current suite does not exercise real audio devices, network
discovery, DLNA renderers, HTTP routing, or browser behavior.
directory ordering. `private/playlists.rkt` tests transactional round trips,
per-user GUID indexes, multiple libraries, deletion and library-boundary
validation. Tests in `private/player.rkt`
cover initial state, browser navigation, repeat mode, persistent playlist-tab
operations, unknown commands, and clean shutdown. The current suite does not
exercise real audio devices, network discovery, DLNA renderers, HTTP routing,
or browser behavior.
The main extension points are:
@@ -365,15 +390,14 @@ The main extension points are:
dispatch, and state refresh in `private/player.rkt`;
- add an API operation by defining its player command first and exposing it
through the generic command endpoint;
- add persistence behind playlist-tab and player initialization without
changing the browser's snapshot-oriented protocol;
- replace polling with server-pushed updates while keeping the current state
snapshot as the synchronization model.
## 9. Architectural constraints and trade-offs
- **Single shared state:** simple coordination and UI synchronization, but no
multi-user isolation or horizontal scaling.
- **Per-user playlists, shared transport:** playlist collections are isolated
by username, while the renderer and transport remain shared. A playlist
command from another user explicitly takes over that shared player.
- **Full-state snapshots:** a small and predictable client protocol, at the cost
of repeatedly transferring all tracks and browser entries.
- **One-second polling:** robust and dependency-free, but introduces periodic
@@ -381,8 +405,9 @@ The main extension points are:
- **Lazy filesystem and backend initialization:** fast startup and low idle
resource usage, while the first recursive selection or playback command can
be comparatively slow.
- **In-memory playlists:** minimal operational complexity, but no recovery after
restart.
- **Keystore playlists:** transactional recovery after restart without a custom
database layer, with the SQLite-backed keystore remaining a single-node
resource.
- **Explicit backend branching:** easy to follow for the current small set of
outputs, but adding renderer types touches several player functions rather
than one formal backend interface.