Files
rkt-web-player/scribblings/rkt-web-player.scrbl
T
2026-09-08 15:20:27 +02:00

343 lines
16 KiB
Racket
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#lang scribble/manual
@(require (for-label racket/base
racket/contract
rkt-web-player
rkt-web-player/player-agent
rkt-web-player/set-user
rkt-web-player/users))
@title{RKT Web Player}
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
@defmodule[rkt-web-player]
RKT Web Player is a central music server for a trusted network. It makes one
or more configured music libraries available through a web interface and
plays that music on different kinds of renderers: the server's own audio
device, UPnP devices, Sonos groups, and player agents. The package also
provides those player agents as GUI and headless CLI applications, so a
computer without UPnP support can become a network renderer of its own. The
central server and each agent communicate over HTTP; the agent downloads its
assigned tracks and plays them on its local audio device.
@section{What the player provides}
The central server is the heart of the application. Its main features are:
@itemlist[
@item{Multiple named music libraries, including local and UNC/network paths.}
@item{Lazy directory browsing: startup reads only the library roots, while
recursive contents and audio metadata are read when needed.}
@item{Playback of the central library through the server's local audio
device, discovered UPnP renderers, Sonos groups, or registered agents.}
@item{Playlist tabs with adding, removing, reordering, renaming and
drag-and-drop support. Tracks from different libraries can share a
playlist. The playlist toolbar shows the selected playlist's track count
and total known playing time, rounded to minutes and expressed in hours
and minutes. Tracks without duration metadata contribute zero to the total.}
@item{Gapless network playback where the renderer or playback agent supports
preloading the next track, with a server-side fallback.}
@item{Transport controls, seeking, volume, repeat mode and live playback
status.}
@item{Embedded album art and adjacent @tt{cover}, @tt{folder} and
@tt{front} JPEG/PNG artwork.}
@item{Per-user playlists, language preference and playback pipeline, with
shared physical outputs that can be transferred between users.}
@item{Dutch, English, German, French, Spanish, Italian, Swedish, Norwegian,
Finnish and Icelandic interfaces.}
@item{Optional Argon2id browser authentication and a separate default-deny
allowlist for playback agents.}
]
The browser is a thin remote control, not a second player. It polls player
status once per second and sends commands to the central server;
playlists, preferences and playback state are kept there. The server exposes
the browser application under the root URL and its JSON API under @tt{/api}.
The API includes state, discovery, playback commands, preferences, artwork and
the agent registration, polling and media routes.
@subsection{Playlist synchronization}
State responses include an opaque @tt{playlistVersion} for the selected
playlist. The browser sends its last received version as the
@tt{playlistVersion} query parameter on @tt{/api/state}, @tt{/api/discover}
and @tt{/api/command/}@italic{command}. When that version still matches,
@tt{tracks} is JSON @tt{null}; otherwise the response includes the complete
track array. Requests without a version continue to receive all tracks.
The version and track data are returned together in one consistent snapshot.
The server caches serialized tracks, count and total duration per playlist.
Changing the track list invalidates that snapshot and generates a fresh version
when it is next requested. Renaming a tab or updating playback position does
not invalidate the tracks. Versions are renewed after a server restart and
are distinct between playlists and users. Changes from another browser are
picked up by the next poll.
The browser keeps the active playlist in memory and serializes state-producing
requests through @tt{PlayerStateClient}, so a delayed response cannot replace
newer state. Unchanged playlist rows are reused; changing the interface language
still rebuilds their translated labels.
@subsection{Saved playlists in the library}
The library has @italic{Folders} and @italic{Playlists} tabs. The playlist
toolbar's @italic{Save playlist} button names and saves the current tab in
the user's library. The library row's @tt{} button opens that playlist as
its own tab, or selects its existing tab. @tt{} does the same and starts
playback. These actions never append tracks to or replace another tab.
A saved playlist and its open tab share one UUID and one track list. Track
edits and renaming therefore update the saved playlist automatically. Closing
the tab keeps it in the library. Closing the last saved tab leaves a new empty
default tab. Existing tabs remain open and appear in the library only after
the user explicitly saves them.
The keystore retains @tt{playlists-for-}@italic{username} for the ordered open
tabs and adds @tt{saved-playlists-for-}@italic{username} for library playlists.
Each UUID has one stored value, shared by both indexes. The private storage
procedure @tt{load-user-playlists} reads the open index by default and the saved
index with @tt{#:saved? #t}. @tt{save-user-playlists!} accepts the saved collection
through @tt{#:saved} and updates both indexes atomically. Values absent from
both indexes are removed. Legacy stores without a saved index need no migration.
Browser state includes @tt{savedPlaylists} summaries without track arrays and
a @tt{saved} flag on each tab. @tt{playlist-save} takes an open tab's @tt{id}
and a @tt{name}; @tt{playlist-open} and @tt{playlist-play} take a saved playlist's
@tt{id}. All three commands operate within the requesting user's collection.
@subsection{Player agents}
A player agent is an additional renderer implemented by this package. It runs
on a computer connected to speakers, registers with the central server, polls
for commands, downloads the selected music and plays it locally. Agents do not
open an inbound network port. The package supplies two forms:
@itemlist[
@item{The GUI agent, with a desktop window and system-tray integration.}
@item{The headless CLI agent, suitable for a small always-on computer or a
machine without a graphical desktop.}
]
The server treats each registered agent as an output alongside UPnP and Sonos
renderers. The server can prefetch the next track to an agent for gapless
transitions. A 256-bit application ID identifies the agent; the server's
@tt{[playback-agents]} allowlist decides which agents may connect.
@defproc[(run-web-player
[music-paths (listof (or/c path-string?
(list/c string? path-string?)))]
[#:allowed-agent-ids allowed-agent-ids (listof string?) null]
[#:users users (listof (cons/c string? string?)) null]
[#:trusted-proxies trusted-proxies (listof string?)]
[#:session-seconds session-seconds exact-positive-integer? 604800]
[#:listen-ip listen-ip string? "127.0.0.1"]
[#:port port exact-positive-integer? 8080]
[#:dlna-port dlna-port exact-positive-integer? 8734]
[#:local-output? local-output? boolean? #t]
[#:playlist-keystore playlist-keystore (or/c path-string? #f)]
[#:log-file log-file path-string?]
[#:log-retention-days log-retention-days exact-positive-integer? 7]
[#:log-level log-level symbol? 'debug]
[#:launch-browser? launch-browser? boolean? #t]) any/c]{
Treats every entry in @racket[music-paths] as a separate music library. A
two-element list supplies an explicit display name and path. The
@racket[allowed-agent-ids] list contains the 64-character application IDs that
may register as polling playback agents. All agents are denied when it is empty.
The function
starts the web interface on @racket[listen-ip] and @racket[port]. Directory
contents are browsed one level at a time. Metadata and recursive contents are
only read when the user adds or plays a selected item.
The @racket[dlna-port] is used to publish local audio files to a selected
network renderer. Set @racket[local-output?] to @racket[#f] to omit the
server's own @tt{racket-audio} output. When no other renderer is available, the
player waits for discovery or playback-agent registration. Playlist tabs are
atomically persisted in
@racket[playlist-keystore]; @racket[#f] disables playlist persistence. Player
resources are closed when the web server exits. The default is
@tt{data/playlists.keystore} below the installed rkt-web-player collection.
Each username owns an ordered GUID index and separate playlist values.
It also owns an independent playback pipeline and a durable interface-language
preference. Different outputs can play concurrently. Selecting an output that
another user owns stops the previous backend and transfers that output. The web
interface supports Dutch, English, German, French, Spanish, Italian, Swedish,
Norwegian, Finnish and Icelandic; browser language is the initial default and a
manual choice is stored per username.
When @racket[users] is non-empty, every browser client must authenticate.
Sessions have a sliding idle timeout; an active browser cookie is renewed once
half of @racket[session-seconds] has elapsed. Playback-agent endpoints continue
to use their separate application-ID allowlist.
Logging is written to @racket[log-file]. Files are rotated after
@racket[log-retention-days] days, and @racket[log-level] selects the
@tt{simple-log} level. The default log file is
@tt{data/rkt-web-player.log} below the installed collection. The
@racket[playlist-keystore], @racket[log-file] and @racket[log-level] arguments
are useful when embedding the player; the command-line entry point can also
read these values from an INI file.
The default listen address only exposes the interface to the local computer.
Use a LAN address deliberately if other devices should control the player.
}
@section{Command line and INI configuration}
The package entry point accepts one or more music directories:
@verbatim{
racket main.rkt MUSIC-DIRECTORY [...]
racket main.rkt --config rkt-web-player.ini --no-browser
}
The @tt{--config} option reads defaults from an INI file. The file is normally
named @tt{rkt-web-player.ini} and is read from the current directory when the
server is started with @tt{--config}. Relative paths are interpreted in the
process's working directory.
Command-line values for @tt{--listen-ip}, @tt{--port}, @tt{--dlna-port},
@tt{--playlist-keystore}, @tt{--log-file} and @tt{--no-browser} override the
corresponding defaults. Logging retention, log level, authentication and the
playback-agent allowlist are configured in the INI file. Libraries configured
under @tt{[libraries]} use the INI key as their display name and the value as
the root directory. The legacy @tt{[library] paths} setting accepts a
semicolon-separated list and is also supported. A complete example is:
@verbatim{
[server]
listen-ip=127.0.0.1
port=8080
[player]
dlna-port=8734
local-output=true
playlist-keystore=data/playlists.keystore
[logging]
log-file=data/rkt-web-player.log
log-retention-days=7
log-level=debug
[authentication]
trusted-proxies=127.0.0.0/8;::1/128
session-seconds=604800
[playback-agents]
; 64 hexadecimal characters, copied from an agent
0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef=true
[users]
; Generate hashes with make-password-hash or set-user.
hans=$argon2id$v=19$m=19456,t=2,p=1$...
}
The @tt{[server]} section controls the browser server. The default
@tt{listen-ip} is @tt{127.0.0.1}, so the interface is local-only by default;
@tt{0.0.0.0} or a LAN address makes it reachable from other machines.
@tt{port} defaults to @tt{8080}.
The @tt{[player]} section controls playback. @tt{dlna-port} defaults to
@tt{8734} and must be reachable by network renderers when they play files
published by the server. @tt{local-output=true} (the default) adds the
server's own audio device as an output. @tt{playlist-keystore} selects the
keystore for playlist tabs and language preferences; omission uses
@tt{data/playlists.keystore} below the installed collection. When embedding
the player through @racket[run-web-player], passing @racket[#f] disables
persistence.
The @tt{[logging]} section controls the rotating log file. The default file is
@tt{data/rkt-web-player.log}, retention defaults to seven days, and the default
level is @tt{debug}. The level is passed to @tt{simple-log}; use a level
supported by that package.
The @tt{[playback-agents]} section is a default-deny allowlist. Each key must
be the complete 64-character hexadecimal application ID of an agent and a
value other than @tt{false} permits registration. An empty section rejects all
agents. This ID is a shared bearer credential, not a replacement for HTTPS.
The @tt{[authentication]} section enables browser login when @tt{[users]} has
at least one entry. Each user value is an Argon2id hash, not a plaintext
password. @tt{session-seconds} defaults to seven days and is a sliding idle
timeout. @tt{trusted-proxies} is a semicolon-separated list of IP addresses or
CIDR ranges whose forwarded client address may be trusted; keep it limited to
the actual reverse proxy. The application does not terminate TLS, so use
HTTPS at the reverse proxy before exposing an authenticated instance beyond a
trusted LAN.
Create a password hash with @racket[make-password-hash], or run
@racket[set-user] in the directory containing the INI file. The
@racket[set-user] procedure updates @tt{rkt-web-player.ini} interactively.
@subsection{Playback-agent INI file}
The GUI and CLI agents use a separate private INI file named
@tt{rkt-web-player-agent.ini} in the user's Racket configuration directory.
It contains the following values:
@verbatim{
[server]
url=http://127.0.0.1:8080
[agent]
name=My playback
app-id=64 hexadecimal characters
}
The agent generates @tt{app-id} once and reuses it so the server allowlist
continues to work. The GUI writes the server URL and display name; the CLI can
override them with @tt{--server} and @tt{--name}. Use @tt{--config} with the
CLI to select another agent INI file and therefore another identity. Agents
make only outbound HTTP requests: they register and poll the server, download
assigned tracks, and play them locally. They do not open an inbound port.
@defmodule[rkt-web-player/users]
@defproc[(make-password-hash [password string?]) string?] {
Creates a salted Argon2id password hash suitable for a value in the INI
@tt{[users]} section. Passwords must contain at least twelve characters.
}
@defproc[(password-hash-valid? [password string?]
[encoded string?]) boolean?] {
Checks a password against an encoded Argon2id hash.
}
@defmodule[rkt-web-player/set-user]
@defproc[(set-user) void?] {
Interactively reads a username and password and writes the corresponding
Argon2id hash to the @tt{[users]} section of @filepath{rkt-web-player.ini} in
the current directory. The password must contain at least twelve characters.
}
@defmodule[rkt-web-player/player-agent]
@defproc[(run-player-agent) any/c] {
Starts the graphical polling playback agent. The agent keeps a generated
256-bit application identifier in a user-specific INI file, registers with the
configured RKT Web Player server, downloads assigned tracks over HTTP, and
plays them with @tt{racket-audio}. Its configured display name is authoritative
and is followed by the server. Importing the module does not start the GUI; the
function must be called explicitly.
The GUI and its @tt{racket-tray} system tray support the same ten languages
based on the operating-system language, with English as fallback. Closing or
minimizing the window hides it in the tray. The tray menu restores the window
or shuts down the agent; no SDL3 runtime is required.
}
@defproc[(run-player-agent-cli [#:server-url server-url
(or/c string? #f) #f]
[#:name name (or/c string? #f) #f]
[#:config-file config-file
(or/c path-string? #f) #f]) void?] {
Starts a headless playback agent using the same audio and gapless-prefetch
runtime as the GUI. Missing keyword values are read from the normal agent INI
file. The procedure runs until interrupted.
}