294 lines
13 KiB
Racket
294 lines
13 KiB
Racket
#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 a complete
|
|
JSON state snapshot 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{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.
|
|
}
|