@@ -670,12 +670,15 @@ which is what a foreground run whose terminal has just closed should do.
670670### Flags, and what they refuse
671671
672672The flags follow squeezelite's: ` -o ` output device, ` -l ` list devices, ` -n ` name,
673- ` -s ` server, ` -z ` daemonize, ` -P ` pidfile, ` -d ` /` -f ` logging. Six are long-only
673+ ` -s ` server, ` -z ` daemonize, ` -P ` pidfile, ` -d ` /` -f ` logging. All but ` -l ` and
674+ ` -z ` also have a long spelling — `--output --name --server --pidfile --logfile
675+ --log-level` — so that every config key is a flag name. Eight more are long-only
674676because they are not squeezelite's: ` --port ` , the port this player serves on,
675- ` --buffer-ms ` , the two mDNS flags ` --no-mdns ` and ` --mdns-name ` , and the two
676- control-socket flags ` --control-socket ` and ` --no-control ` . Run ` --help ` for the
677- current state of each — a few still point at
678- [ ` docs/ROADMAP.md ` ] ( docs/ROADMAP.md ) for behaviour that is not built yet.
677+ ` --buffer-ms ` , the two mDNS flags ` --no-mdns ` and ` --mdns-name ` , the two
678+ control-socket flags ` --control-socket ` and ` --no-control ` , and ` --config ` and
679+ ` --state-dir ` for the two files above. Run ` --help ` for the current state of each
680+ — a few still point at [ ` docs/ROADMAP.md ` ] ( docs/ROADMAP.md ) for behaviour that is
681+ not built yet.
679682
680683A ** subcommand comes first** , before any flag: ` sendspin-cli vol 50 --port 9000 ` ,
681684not ` sendspin-cli --port 9000 vol 50 ` . That is not getopt permutation showing
@@ -713,6 +716,112 @@ or a full `ws://`/`wss://` URL, or `mdns:[<name>]` to discover one. An IPv6 lite
713716must be bracketed (` [::1]:8927 ` ), since an unbracketed one cannot be told from a
714717host with a port.
715718
719+ ### The config file, and what the player remembers
720+
721+ Two files, and the split is deliberate: one you write and the daemon only ever
722+ reads, one the daemon writes and you never need to touch.
723+
724+ ** The config file** holds anything you would otherwise type. Keys are the long
725+ flag names without their dashes, one per line, and a value is exactly the string
726+ the flag would have been given:
727+
728+ ```
729+ # /etc/sendspin-cli.conf, or ~/.config/sendspin-cli/config
730+ name = kitchen
731+ output = hw:1,0
732+ buffer-ms = 250
733+ server = mdns:Living Room
734+ no-mdns = true
735+ ```
736+
737+ That is why ` -o -n -s -P -f -d ` grew the long aliases `--output --name --server
738+ --pidfile --logfile --log-level` : one vocabulary, so ` --help` is the config
739+ reference rather than a second document to keep in step. ` # ` starts a comment
740+ only at the ** start** of a line — a name or a path is free to contain one, and a
741+ reader that ate everything after a ` # ` would silently truncate it. Booleans take
742+ ` true ` /` yes ` /` on ` /` 1 ` or ` false ` /` no ` /` off ` /` 0 ` . Where a key appears twice, the
743+ last one wins.
744+
745+ The first of these that exists is read ** whole** , and nothing below it is merged
746+ over the top:
747+
748+ 1 . ` --config <path> ` — and this one is ** fatal if it cannot be read** , because
749+ you named it. Falling back would start a player on options nobody chose.
750+ 2 . ` $XDG_CONFIG_HOME/sendspin-cli/config `
751+ 3 . ` $HOME/.config/sendspin-cli/config `
752+ 4 . ` /etc/sendspin-cli.conf `
753+
754+ Finding none is silent and normal. There is deliberately no ` --no-config ` : the
755+ asymmetry above already gives you ` --config /dev/null ` . A half-overridden config
756+ assembled from several layers is far harder to reason about than one file you can
757+ read top to bottom, which is also why there is no ` $XDG_CONFIG_DIRS ` traversal.
758+
759+ ** Precedence is command line > config file > built-in default** , per option
760+ rather than per file — ` -n bathroom ` on a line whose config also sets
761+ ` buffer-ms ` overrides only the name. Everything can come from a file except ` -l ` ,
762+ ` -z ` , ` --config ` itself, and ` --help ` /` --version ` , which have no meaning in one.
763+ Run shape stays on the command line: excluding it is
764+ reversible, and debugging a ` daemonize ` that came out of a file under systemd is
765+ not. A config that names one is refused as an unknown key.
766+
767+ A configured value is validated by exactly the code that validates a typed one,
768+ with the same message and the line to go and fix:
769+
770+ ``` console
771+ $ sendspin-cli
772+ error: /etc/sendspin-cli.conf:4: invalid --buffer-ms '5' -- expected 10-2000
773+ ```
774+
775+ An unknown key or a line that is not ` key = value ` is refused the same way. A
776+ silently ignored typo is the failure mode this whole surface exists to avoid — so
777+ is a config that is quietly skipped, which is why a file that exists and does not
778+ parse stops the run rather than falling through to ` /etc ` . ` --help ` , ` --version `
779+ and ` -l ` short-circuit above all of it: a broken config must not stop ` --help `
780+ from telling you how to fix it. Startup logs one line naming the file in use, or
781+ saying there is none.
782+
783+ ` server ` from a config file behaves exactly as ` -s ` does, and that includes
784+ suppressing the mDNS advertisement — the spec forbids advertising
785+ ` _sendspin._tcp ` while this end is the one dialling out.
786+
787+ ** The state file** is the other half: what the daemon remembers for itself,
788+ across restarts.
789+
790+ ```
791+ # Written by sendspin-cli. Edits are overwritten.
792+ last-server = 7f3a…
793+ last-server-hash = 3387423128
794+ static-delay-ms = 375
795+ volume = 42
796+ muted = true
797+ ```
798+
799+ ` static-delay-ms ` is there because the spec requires it — * "Clients must persist
800+ ` static_delay_ms ` locally across reboots and server reconnections"* — and
801+ ` volume ` /` muted ` because the spec marks those RECOMMENDED. ` last-server ` is the
802+ server id mDNS discovery uses to break a tie between candidates;
803+ ` last-server-hash ` is the opaque ` uint32_t ` the library asks us to keep so * it* can
804+ prefer the last-played server among inbound connections. They mean different things
805+ and are deliberately not reconciled with each other.
806+
807+ ** A remembered ` static-delay-ms ` is reported, not yet applied.** The player hands
808+ the figure back to the server so the two agree across a restart, which is the part
809+ the spec asks for; actually shifting playout by it is
810+ [ ` docs/ROADMAP.md ` ] ( docs/ROADMAP.md ) item 13's. Until then a delay you set is
811+ remembered faithfully and does nothing you can hear.
812+
813+ It lives at ` $XDG_STATE_HOME/sendspin-cli/state ` , then
814+ ` $HOME/.local/state/sendspin-cli/state ` , and ` --state-dir <dir> ` overrides both —
815+ a systemd ** system** unit has neither variable and gets ` /var/lib/sendspin-cli `
816+ from ` StateDirectory= ` . With none of the three the player still runs and simply
817+ remembers nothing. Writes go through a temporary, an ` fsync ` and a ` rename ` at
818+ mode ` 0600 ` , so a player that loses power mid-write leaves either the old file or
819+ the new one and never half of either.
820+
821+ Two players on one host share this file unless you give each its own
822+ ` --state-dir ` . They already need different ` --port ` s; give them different state
823+ directories too, or the second one to save its volume overwrites the first's.
824+
716825## Tests
717826
718827``` bash
@@ -740,8 +849,10 @@ and exits `0` on `SIGTERM` — plus the whole of the control socket, which needs
740849processes by definition: that it appears at the default path as ` 0600 ` , that
741850` status ` round-trips, that ` --no-control ` binds nothing, that a stale socket is
742851taken over after a ` SIGKILL ` , that it is gone after ` SIGTERM ` , and that a second
743- instance on the same socket is refused. CI runs it on every platform leg; run it
744- yourself against any build.
852+ instance on the same socket is refused. The config file is in here for the same
853+ reason: a player configured entirely from a file has to be findable by a
854+ subcommand that repeats none of it, and that is two processes agreeing on one
855+ path. CI runs it on every platform leg; run it yourself against any build.
745856
746857## CI
747858
0 commit comments