53 Commits
Author SHA1 Message Date
Tobias GesellchenandClaude Opus 4.8 2744708a9d chore(release): drop the transitional soundtouch-web alias
soundtouch-web was a transitional alias of soundtouch-player. Stop
building and publishing it everywhere, and refresh the release notes
while at it:

- release.yml: remove the soundtouch-web binary, its individual and
  combined checksums, and its release assets (EXPECTED_COUNT 35 -> 28);
  drop the ghcr.io/...-web Docker image steps. Also slim the
  workflow_dispatch release notes to an accurate AfterTouch header plus
  GitHub's auto-generated changelog, with the bare tag as the title.
- Dockerfile: drop the soundtouch-web image stage.
- Makefile: remove WEB_NAME and the build-web target (and its use in
  build/install).
- Delete scripts/raspberry-pi/install-web.sh (it fetched a release asset
  that is no longer published) and point the docs at install-player.sh.
- Correct README, CLAUDE.md, and main.go wording that claimed the alias
  was still published.

The runtime notice for a binary still run under the soundtouch-web name
is kept, so anyone who renamed the binary is nudged to soundtouch-player.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 22:29:23 +02:00
Tobias GesellchenandClaude Opus 4.8 bd62fd6658 refactor: rename soundtouch-web to soundtouch-player (transitional alias) (refs #451)
The web player is intrinsically LAN-resident: it reaches speakers directly
and only delegates cloud-only features (e.g. TTS) to a possibly-remote
AfterTouch service via --service-url. That is exactly what a cloud-hosted
soundtouch-service cannot do, so the standalone player binary stays useful
and is not being deprecated. Rename it to state its purpose, with a
transition window so existing downloads keep working.

- cmd/soundtouch-web -> cmd/soundtouch-player; CLI name is now
  soundtouch-player. When the binary is invoked under its old name it prints
  a one-line rename notice (filepath.Base(os.Args[0])).
- Build/release both names from the same source: Makefile (build-player +
  build-web alias, dev-player* targets), Dockerfile (soundtouch-player image
  + transitional soundtouch-web image), release.yml and ci.yml (player +
  web artifacts, checksums, Docker images; release notes announce the
  rename). The soundtouch-web binary, image, and install script remain a
  transitional alias to be dropped in a future release (which will break
  stale fetch scripts and nudge users to the release notes).
- scripts/raspberry-pi/install-player.sh is canonical; install-web.sh keeps
  working but warns.
- Sweep docs, code comments, user-facing strings, and assets
  (soundtouch-web-ui.png, soundtouch-web-tunein.png, soundtouch-web-roadmap.md)
  to soundtouch-player; README documents the rename and why the player
  remains separate from the embedded /app.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 16:33:39 +02:00
Tobias GesellchenandClaude Opus 4.8 2dd0143e10 feat(service): add 3 speaker-contract routes for parity (refs #451)
Close the speaker/service-contract gaps found comparing against a reference
implementation — three real Bose routes we did not serve:

- DELETE /streaming/account/{account}/source/{sourceID} — removes a configured
  source from every device of the account (HandleMargeDeleteSource +
  marge.RemoveSourceFromAccount), mirroring the account-level POST add-source.
  Bare 200, empty body. Previously source removal was only reachable via the
  admin /setup surface.
- GET /bmx/tunein — bare TuneIn service descriptor (the registry's `self` link),
  HandleTuneInService. chi routes both /bmx/tunein and /bmx/tunein/.
- GET /core02/svc-bmx-adapter-orion/prod/orion — bare Orion (LOCAL_INTERNET_RADIO)
  adapter descriptor, HandleOrionService.

The two descriptors reuse the existing extractBMXService + applyBMXTemplate
helpers (same {BMX_SERVER}/{MEDIA_SERVER} substitution the registry applies).
Contract tests added (delete_source.http, get_bmx_service_descriptors.http);
router + frozen-coverage goldens updated.

make test-http-client: 95 requests, 0 failed. go test + golangci-lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 19:11:24 +02:00
Tobias GesellchenandClaude Opus 4.8 ea6ee3e097 refactor(service): stub the unused /accounts/* mirror with a 501 "report it" handler (refs #451)
Shrink the route surface the #451 refactor must preserve by retiring the
/accounts/{account}/* compatibility mirror. Across the full recording corpus
(all _/backup/*, _/mitm, _/i195, _/issue-94, captures + data/ + tests/, 139k+
.http files) no speaker or app uses the /accounts prefix, and every operation it
offered is served by the /streaming/account/* paths real clients actually use.

- New HandleUnsupported: returns 501 and logs the full request + client IP + a
  "please report this" message, so any real-world use surfaces instead of being
  silently dropped, and the prefix becomes a clean removal candidate.
- Re-point every /accounts/* route to it. The frozen /streaming/* contract is
  left entirely on its real handlers (those stay even where our corpus didn't
  exercise them — absence of capture is not proof of disuse).
- Migrate the integration tests off the /accounts mirror onto their recorded
  /streaming/account/* equivalents (register/unregister/spotify_full_flow), then
  pin the mirror's 501 contract in unsupported_routes.http.
- Router + frozen-route-coverage golden files updated accordingly.

make test-http-client: 91 requests, 0 failed. go test + golangci-lint clean.

Note for release time: call out the intentional /accounts/* 501 breakage in the
release notes' Noteworthy section (use /streaming/account/* instead).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 19:11:24 +02:00
Tobias GesellchenandClaude Opus 4.8 9bfe2a1a08 fix(ci): gate http-client tests on mock readiness; address semgrep findings (refs #451)
The integration suite flaked in CI: with three `go run` mocks now compiling
concurrently, the spotify/amazon mocks weren't listening within the fixed
`sleep 10`, so the registration requests at the start of the suite hit a
connection-refused and the "Account exists" assertions (and the cascading amazon
oauth token test) failed. Locally it passed because the mock builds were warm.

Replace the fixed sleep with real readiness gating:
- Add a /healthz endpoint to the spotify, amazon and tunein mocks.
- Give all four CI services (the three mocks + soundtouch-service) a compose
  healthcheck (busybox wget; all images are alpine-based), and make the service
  depend_on the mocks being service_healthy.
- `docker compose up -d --build --wait` blocks until everything is healthy, so
  the JetBrains client only runs against a fully-ready stack.

Also clear the two semgrep advisories on the new TuneIn mock:
- cmd/mock-*: annotate the intentional plaintext ListenAndServe with nosemgrep
  (throwaway loopback/CI test servers, never production).
- pkg/testutils/tunein: sanitize the query-supplied guide id to a safe charset
  before interpolating it into the JSON/XML response (raw-html-format).

make test-http-client: 73 requests, 0 failed (clean testdata, healthcheck-gated).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 19:11:24 +02:00
Tobias GesellchenandClaude Opus 4.8 d5b298fc43 test(http-client): pin ignored edges + app/provisioning surface (refs #451)
Two deliberately-unimplemented routes, pinned as "currently ignored" so a future
change to them is conscious:
- GET  /v1/blacklist/{deviceId}  -> 405 (inline stub)
- POST /alexa/certificate        -> 501 (no AWS IoT integration)

App / provisioning surface (app-called, not the speaker data-plane). Shapes come
from the _/mitm capture where one exists, otherwise from the handler (canned /
stub responses):
- GET  /streaming/account/{a}/emailaddress  -> 200 (<emailAddress>, _/mitm)
- GET  /customer/account/{a}                -> 200 (<customer> profile, canned)
- POST /customer/account/{a}                -> 200 (profile update, stub)
- POST /customer/account/{a}/password       -> 200 (password change, stub)

COVERAGE.md gains an app/provisioning section and records the source (mitm vs
handler) for each. make test-http-client: 73 requests, 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 19:11:24 +02:00
Tobias GesellchenandClaude Opus 4.8 e91e7d8ad4 test(http-client): add remaining simple GET->200 cases + fix coverage rows (refs #451)
Two more frozen GET routes that return a static 200:
- GET /bmx/registry/v1/servicesAvailability (embedded availability registry JSON)
- GET /ced/soundtouch/mr4_22097fe2/index.xml (CED firmware-update config; a
  present static file is 200, absent paths 404)

COVERAGE.md: correct the rows that were already covered by the first batch but
left marked as gaps (/v1/auth, /v1/scmudc, orion station, custom playback,
ding, bmx-icons), and record the two new routes. Remaining gaps are the ones
that need an upstream fixture (tunein episode), prior TTS state (media/tts), or
are quirky-status edges.

make test-http-client: 67 requests, 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 19:11:24 +02:00
Tobias GesellchenandClaude Opus 4.8 a974862b07 test(http-client): pin the ETag conditional-GET (304) contract (refs #451)
The speaker re-polls /full and the device presets with the ETag it last saw and
expects 304 Not Modified when nothing changed. Two self-contained flows capture
the current ETag and replay it via If-None-Match, asserting 304. This pins the
conditional-GET behaviour and the case-sensitive ETag header path (CLAUDE.md).

make test-http-client: 65 requests, 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 19:11:24 +02:00
Tobias GesellchenandClaude Opus 4.8 6efad165f6 test(http-client): mock TuneIn upstream so playback tests run offline (refs #451)
Make the BMX TuneIn integration tests independent of the live TuneIn
(radiotime.com) service, the same way Spotify/Amazon are already mocked.

- pkg/service/bmx: the TuneIn upstream base URLs become configurable vars with
  a SetTuneInEndpoints(opmlBase, apiBase) setter that also registers the host in
  the outbound allowlist. Defaults are unchanged (real radiotime hosts), so
  production behaviour is identical; tests can redirect to a mock.
- cmd/soundtouch-service: new --tunein-opml-url / --tunein-api-url flags
  (TUNEIN_OPML_URL / TUNEIN_API_URL) wired through to SetTuneInEndpoints.
- cmd/mock-tunein + pkg/testutils/tunein: a mock TuneIn server serving Tune.ashx
  (stream URLs) and describe.ashx (name/logo) with RFC-5737 values; unmocked
  endpoints 404 so a test needing them fails loudly.
- docker-compose.ci.yml: add the tunein-mock service and point the service at it.
- tunein_playback_station.http now asserts the mock-served stream URL + name,
  proving the path is offline. tunein_favorite.http covers the local-only
  favorite add/remove (202).
- TUNEIN-MOCK-MISSING.md lists the upstream captures still needed (episode /
  navigate / search) before those routes can be mocked + tested.

make test-http-client: 61 requests, 0 failed. golangci-lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 19:11:24 +02:00
Tobias GesellchenandClaude Opus 4.8 41fccd6f1c test(http-client): cover the group delete lifecycle (refs #451)
create_group.http now captures the new group id from the Location header into
{{groupId}}; delete_group.http then completes the lifecycle by removing that
group (DELETE /group/{groupId} -> 200 with <status>) and exercises the no-id,
account-level teardown form a speaker sends on factory reset
(DELETE /group/ -> 200). Inserted after get_group.http, before device teardown.

make test-http-client: 59 requests, 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 19:11:24 +02:00
Tobias GesellchenandClaude Opus 4.8 603765b644 test(http-client): broaden speaker-contract coverage from recordings (refs #451)
Build the regression net the API route layout note calls for, before any
route refactoring: mine real recorded speaker traffic (Bose_Lisa UA) into a
coverage checklist and fill the high-priority, dependency-free gaps.

- COVERAGE.md: inventory of frozen speaker routes (method + status) mapped to
  covering .http files, with the remaining gaps classified by priority.
- New flows, all asserting status/content-type/structure with the firmware UA:
  - GET  /v1/auth                              (app-key probe)
  - POST /v1/scmudc/{deviceId}                 (telemetry upload)
  - GET  /core02/.../orion/station             (Orion custom-stream adapter)
  - GET  /custom/v1/playback/{encodedURL}      (LOCAL_INTERNET_RADIO / ding)
  - POST /bmx/tunein/v1/report                 (STOP -> {}, START -> nextReportIn)
  - GET  /media/aftertouch-ding.wav            (binary: status + content-type)
  - GET  /media/bmx-icons/{provider}/{file}    (binary: status + content-type)

All request/response values use placeholder / RFC-5737 data; no recorded
bodies are committed. make test-http-client: 57 requests, 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 19:11:24 +02:00
Tobias GesellchenandClaude Sonnet 4.6 d71a5c3bed fix(docs): fix sponsor link baseURL and add git commit hash to footer
- Use relURL (no leading slash) for the sponsor link so it respects
  the /Bose-SoundTouch/ base path on GitHub Pages; absURL and relURL
  both ignore the base path when the input starts with /
- Inject HUGO_PARAMS_GITHASH (github.sha) via the docs workflow and
  forward it into the Hugo container via docker-compose.docs.yml +
  make dev-docs, so the deployed footer shows a clickable short hash
  linking to the exact commit
- Use site.Params.githash (global) instead of .Site.Params.githash
  because Hextra calls custom/footer.html with a dict context, not a
  page; .Site is nil in that scope
- Use substr not slice to trim the hash to 7 chars

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 00:37:30 +02:00
Tobias GesellchenandClaude Sonnet 4.6 34f0fec4ad docs: migrate Jekyll site to Hugo + Hextra
Replace docs/_config.yml + docs/SUMMARY.md with Hugo + Hextra theme.
Move all content into docs/content/, images into docs/static/images/.
Update docs_consistency_test.go to check Hugo front matter instead of
SUMMARY.md inclusion. Update CI workflow and screenshot script paths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 13:30:36 +02:00
Tobias Gesellchen 7f2e6abfc3 build: Add automated dependency management for JavaScript libraries
- Sets up Dependabot for JS dependency updates
- Adds GitHub workflow for automated static dependency updates
- Creates update script for Preact and other static JS libraries
- Updates Preact to latest version via new automation
2026-05-18 22:34:26 +02:00
Tobias GesellchenandClaude Opus 4.7 a76a112d92 chore(integration): add testdata rotation target + document workflow
Surfaced via the rfc-5737-cleanup sweep: after the anonymisation pass
updated test-suite assertions to RFC-5737 IPs, the next
`make test-http-client` run failed against the stale local
tests/integration/testdata/ left over from a previous build (which
still carried the old 192.168.1.x state via the compose volume).

Two changes, in one commit so the doc references the target it
documents:

1. Makefile: new `test-http-client-rotate` target that renames any
   existing tests/integration/testdata/ to
   tests/integration/testdata_<timestamp>/. Non-destructive (mv, not
   rm), opt-in (no other target invokes it). Archives stay around
   for retrospective debugging — that directory is debug evidence,
   not disposable scratch.

2. CLAUDE.md: new "Integration tests" section under Build/test/run.
   Explains the docker-compose stack, the testdata mount, the
   per-machine-only nature (via tests/.gitignore), and the
   rotate-then-run pattern when fixtures or schemas have changed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 22:05:13 +02:00
Tobias GesellchenandClaude Opus 4.7 249c2586e9 chore(make): use RFC-5737 documentation IPs in help-text examples
Five `192.168.1.x` references in Makefile usage-error messages and
the `make help` example block. Same hygiene argument as the docs
sweep in 136d24a — replaced with `192.0.2.x` so the example output
clearly reads as a placeholder, not a real LAN.

Behaviour unchanged: these are echo-only strings printed when the
user forgets to set HOST=… or asks for `make help`. The
HOST=<your-IP> contract is unaffected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 22:05:13 +02:00
Tobias GesellchenandClaude Opus 4.7 0c4a12670b fix(stockholm): patch browser_http_proxy.js so the proxy URL respects basePath
Two patching gaps caused every Stockholm HTTP-proxy call from a
/stockholm/* page to hit /api/http-proxy (404) instead of the
basePath-prefixed /stockholm/api/http-proxy:

1. The proxy URL constant in browser_http_proxy.js is declared as
   `var PROXY_PATH` (uppercase). Our patch script only knew about the
   lowercase `var proxyPath` form used in app_comm.js, so it never
   matched the upstream file.

2. Even if the constant had matched, browser_http_proxy.js's IIFE
   evaluates the URL at script-load time — but the injected bootstrap
   that defines window.__stockholmBase is placed just before </head>,
   i.e. after the <script src=…> tags. The captured value would
   always fall back to the unprefixed "/api/http-proxy".

3. The Makefile never passed browser_http_proxy.js to the patch script
   at all.

Fix:

  - Add an uppercase `PROXY_PATH` replacement entry in
    patch-stockholm-bridge.py (keeps the lowercase one for
    app_comm.js).
  - Add a second replacement that rewrites the **use site** in
    browser_http_proxy.js to inline `(window.__stockholmBase||"") +
    "/api/http-proxy?url=" + ...`. Reading __stockholmBase at
    call-time bypasses the load-order trap; the patched
    `var PROXY_PATH = …` declaration above becomes dead code but
    stays harmless.
  - Pass `$(STOCKHOLM_DIR)/js/browser_http_proxy.js` to the patch
    script in the prepare-stockholm target so it actually gets
    rewritten.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 15:05:39 +02:00
Tobias GesellchenandClaude Opus 4.7 548c815c4d chore(stockholm): add dev-service-stockholm make target
Compresses the local roundtrip to a single command:

  make build-stockholm-image    # one-time
  make prepare-stockholm        # once per zip update
  make dev-service-stockholm    # iterative loop

The target only checks that prepare-stockholm has produced
stockholm/index.html (a fast file stat) — it deliberately does NOT
re-run the Docker preparation step on every launch, since that takes
tens of seconds and produces identical output most of the time. Fails
loudly with a hint if Stockholm isn't prepared.

Listed in `make help` under the existing dev-* group. Not added to
.PHONY because the surrounding dev-service / dev-service-proxy targets
aren't either — matching local convention rather than gold-plating.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 15:05:39 +02:00
Tobias GesellchenandClaude Sonnet 4.6 6fb999435a feat(stockholm): add Go backend integration for Stockholm frontend
Implements pkg/service/stockholm with bridge (appSend/runQueue), HTTP
proxy, static serving, config URL rewriting, native state persistence,
and device discovery. Mounts under a configurable base path (/stockholm
by default) with correct http.StripPrefix routing and apiBase-prefixed
bridge API routes matching the patched JS window.__stockholmBase calls.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 15:05:39 +02:00
Tobias GesellchenandClaude Sonnet 4.6 c64e601df6 feat(stockholm): add Dockerfile.stockholm and Makefile targets for frontend prep
Dockerfile.stockholm clones github.com/krahl/soundcork-stockholm-app at build
time and installs the required tools (prettier, patch, unzip, jq). No pre-built
image is published upstream, so users must run `make build-stockholm-image` once
before `make prepare-stockholm`.

`make prepare-stockholm` runs the upstream entrypoint logic (extract zip,
run prettier, apply patches) via a volume-mounted docker run, stopping before
`exec java` so we only collect the processed stockholm/ output. The Go service
then serves that directory directly with no patching required at runtime.

Prerequisites: Docker with internet access, and stockholm_zip/stockholm.zip
(Stockholm source zip placed manually — tracked directory, zip gitignored).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 15:05:39 +02:00
Tobias GesellchenandClaude Opus 4.7 979374b501 test(integration): pin #285 rename PUT behaviour at the HTTP layer
Adds rename_device.http between get_group.http and unregister_device.http
in the make test-http-client sequence. The new test fires the PUT
the speaker emits after a rename and asserts:

  - 200 OK, content type vnd.bose.streaming-v1.2+xml
  - the response carries the renamed value
  - createdOn matches the value captured during register_device.http
    (cross-request global), locking in the "first-paired" semantics
  - ipaddress is preserved from the prior power_on, not reset by the
    rename body's empty IP field
  - a mismatched body deviceid is rejected with 400

register_device.http captures the initial createdOn into a global so
the rename test can assert equality rather than a flakier
updatedOn != createdOn heuristic. The variant POST's stale
updatedOn === createdOn assertion is replaced with an upsert-aware
equality against the same captured global.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 19:04:41 +02:00
Tobias GesellchenandClaude Opus 4.7 ef2b775ce0 test(integration): add http-client test for stereo-pair Marge POST
Add an end-to-end IntelliJ HTTP Client test that replays the exact
request shape a SoundTouch 10 master sends to its configured Marge
server during stereo-pair formation (captured live in issue #252):

  POST /streaming/account/{accountId}/group/
  Authorization: Bearer <token>
  Content-Type:  application/vnd.bose.streaming-v1.2+xml

  <group>
    <masterDeviceId>...</masterDeviceId>
    <name>TEST</name>
    <roles>
      <groupRole><deviceId>...</deviceId><role>LEFT</role></groupRole>
      <groupRole><deviceId>...</deviceId><role>RIGHT</role></groupRole>
    </roles>
  </group>

Assertions cover the wire contract that fails loudly if regressed:
trailing-slash URL is matched, response is 201 Created with the vendor
media type, Location header references the new group under the
account, and the body echoes masterDeviceId, name, and both groupRole
entries.

Wired into the make test-http-client target, sequenced before
get_group.http so the GET runs against the post-create state.
get_group.http's assertion only checks for the presence of a <group>
element, so adding a populated group beforehand is compatible.

Refs #252

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 09:05:14 +02:00
Tobias GesellchenandClaude Opus 4.7 bb71253690 feat(screenshots): add headless-Chrome capture pipeline with fake speaker
Refreshes docs/images/ui-{settings,devices,sync,migration}.png by
driving the web UI in chromedp against a synthetic speaker, so
documentation can be regenerated without real hardware and without
leaking personal data from the local network.

Three independent pieces:

- pkg/service/testing/fakespeaker — embeddable library serving the
  HTTP and telnet surface the migration wizard probes (/info,
  /presets, /recents and a getpdo CurrentSystemConfiguration reply
  that places the device on the unmigrated happy path).
- cmd/dummy-speaker — thin CLI wrapping the library; self-registers
  with a running service via POST /setup/devices.
- scripts/screenshots — chromedp runner driven by a JSON manifest;
  decoupled from speaker/service setup so it can target any backend
  URL. run.sh orchestrates a one-shot end-to-end capture and seeds
  settings.json with a generic hostname plus discovery disabled to
  keep real-network state out of the captures.

Captures are at DPR=2 for retina-sharp text.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 20:37:23 +02:00
Tobias GesellchenandClaude Sonnet 4.6 06042f8728 chore(build): add ARMv7 target and apply trimpath/-s/-w flags globally
Adds build-linux-armv7 target (GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=0)
for deployment to old embedded Linux devices (kernel 3.14+). Introduces
BUILDFLAGS=-trimpath -ldflags="-s -w" applied to all build targets for
smaller, reproducible binaries without local path leakage.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 21:11:41 +02:00
86825c44af feat(backup): add soundtouch-backup tool for cloud and local speaker backup (#197)
Introduces a standalone `soundtouch-backup` CLI with three subcommands:
- `all`: authenticates with the Bose cloud, backs up account data, then
reads device IPs from devices.xml and backs up each reachable speaker
- `cloud`: fetches account profile, devices, sources, presets, and full
endpoint from streaming.bose.com
- `local`: backs up each speaker via HTTP API (12 endpoints) and
optionally via SSH (individual files + /opt/Bose/etc/ and
/mnt/nv/BoseApp-Persistence/1/ directories)

Also centralises pkg/service/ssh → pkg/ssh so both the service and the
backup tool share the same SSH client; adds ReadFile and ReadDir
methods, and handles the firmware quirk where cat exits 1 on empty
files.

Output is a single dated .tar.gz or .zip archive.

Example flow:

```shell
gesellix@Mac Bose-SoundTouch % go run ./cmd/soundtouch-backup all --output _/cloud-backup --email user@example.com
Password: 
Authenticating as user@example.com...
  ✓ Authenticated (account ID: 1234567)
  ✓ email address (107 bytes)
  ✓ devices (1492 bytes)
  ✓ sources (1111 bytes)
  ✓ presets (2585 bytes)
  ✓ full account (55037 bytes)
Found 2 device(s) in cloud account, attempting local backup...
  ✓ ST20: 12 files via HTTP
  ⚠ ST20: SSH skipped /etc/remote_services (Process exited with status 1)
  ⚠ ST20: SSH empty file /mnt/nv/remote_services
  ✓ ST20: 64 files via SSH
  ✓ ST10: 12 files via HTTP
  ⚠ ST10: SSH empty file /etc/remote_services
  ⚠ ST10: SSH skipped /mnt/nv/remote_services (Process exited with status 1)
  ✓ ST10: 48 files via SSH
Archive written: _/cloud-backup/soundtouch-backup-2026-05-02.tar.gz (141 files)
```

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 14:00:23 +02:00
Tobias GesellchenandClaude Sonnet 4.6 ca7f8d5453 feat: wire Amazon mock into http-client integration tests
- Add cmd/mock-amazon/main.go (mirrors mock-spotify, uses testutils/amazon)
- Add amazon-mock service to docker-compose.yml (port 8082)
- Add AMAZON_CLIENT_ID/SECRET/TOKEN_URL/PROFILE_URL to docker-compose.ci.yml
- Add amazon_registration.http: registers account via /mgmt/amazon/callback
  before the token-refresh test runs (mirrors spotify_registration.http)
- Update {{amazonRefreshToken}} in env to match mock response (Atzr|amazon-refresh-token)
- Log amazon-mock output on test failure in Makefile

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 20:30:06 +02:00
Tobias Gesellchen 406180e4ce Prepare http-client test for Amazon 2026-04-29 20:30:06 +02:00
Tobias GesellchenandGitHub cdaf9f0c0a Build and publish a soundtouch-web Docker image (#184)
Relates to https://github.com/gesellix/Bose-SoundTouch/issues/181
2026-04-23 22:23:00 +02:00
Tobias Gesellchen 5b99d7f46b Add a web-based app 2026-04-19 21:59:55 +02:00
Tobias Gesellchen 153d387aaf Fix a complete flow for Spotify registration, preset 2026-04-06 21:15:15 +02:00
Tobias Gesellchen e5b94158e6 Use modern docker compose command syntax 2026-04-06 15:05:44 +02:00
Tobias Gesellchen c4cf078d2a Add Spotify mock server and integration tests 2026-04-06 15:05:44 +02:00
Tobias GesellchenandGitHub 382567d67d Add .../api_versions.xml and .../musicprovider/{providerID}/is_eligible (#150) 2026-04-05 23:25:30 +02:00
Tobias GesellchenandGitHub de96b1f119 Add /streaming/account/{account}/presets/all (#149) 2026-04-05 23:10:53 +02:00
Tobias GesellchenandGitHub d22dc99c9e Add /streaming/account/{account}/devices (#148) 2026-04-05 10:16:22 +02:00
Tobias GesellchenandGitHub bd0e3d64a3 Add /streaming/account/{account}/sources (#147) 2026-04-05 01:09:02 +02:00
Tobias GesellchenandGitHub 50e45ab5f2 Add/improve e2e test cases (#144)
https://github.com/gesellix/Bose-SoundTouch/issues/135
2026-04-04 21:05:34 +02:00
Tobias Gesellchen c1e7d513b4 Add/improve e2e test cases 2026-04-04 18:53:59 +02:00
Tobias GesellchenandGitHub 7c92a785a4 Add/improve e2e tests (#142)
https://github.com/gesellix/Bose-SoundTouch/issues/135
2026-04-04 13:10:13 +02:00
Tobias GesellchenandGitHub b79a168084 Add/improve e2e test cases (#141)
https://github.com/gesellix/Bose-SoundTouch/issues/135
2026-04-04 12:22:58 +02:00
65f1a2565c feat: add spotify source registration and environment config for set_preset_5 integration test (#139)
Co-authored-by: Junie <junie@jetbrains.com>
2026-04-01 22:12:30 +02:00
aa7b2c28ab feat: improve Bose SoundTouch parity, Spotify integration, and data reliability (#138)
feat: improve Bose SoundTouch parity, Spotify integration, and data
reliability

- Update XML marshaling for ServicePreset and ServiceRecent to match
Bose parity requirements.
- Add support for adding music sources via
`/streaming/account/{account}/source`.
- Implement HandleBoseAccountToken for Spotify OAuth code exchange and
token persistence.
- Implement atomic file writes in the datastore to prevent data
corruption.
- Add startup logic to initialize default sources for existing devices.
- Expand test coverage with new parity regression and Spotify
integration tests.

---------

Co-authored-by: Junie <junie@jetbrains.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-04-01 21:55:52 +02:00
Tobias Gesellchen 71e3260823 Extend TuneIn support, add e2e tests 2026-03-30 00:52:00 +02:00
Tobias Gesellchen a06657f3f5 Add more e2e tests 2026-03-29 19:14:48 +02:00
Tobias GesellchenandGitHub e0a84d5904 Split register and unregister device tests (#133) 2026-03-27 22:03:21 +01:00
Tobias GesellchenandGitHub b04b0bcc32 Add account registration/login (#129) 2026-03-27 08:37:50 +01:00
Tobias GesellchenandGitHub a0833c113c Add favicon-gen tool to generate PNG and ICO favicons from SVG sources (#22) 2026-03-21 13:18:16 +01:00
Tobias GesellchenandGitHub c560d399b5 Add Docker support and CI/CD integration for SoundTouch service (#19) 2026-02-08 00:36:49 +01:00
Tobias Gesellchen 1ed562f45e refactor: Replace ldflags version injection with debug.BuildInfo
- Use debug.ReadBuildInfo() for version information (Go 1.18+ best practice)
- Extract version from module info and VCS settings (vcs.revision, vcs.time)
- Remove complex ldflags setup from Makefile and GitHub workflows
- Simplify build process while maintaining all version information
- Cleaner approach recommended by Go community

Thanks to Gopher Slack feedback for this improvement!
2026-01-11 17:27:07 +01:00
Tobias Gesellchen 546634572a feat: implement build-time version injection
- Replace hardcoded version with build-time injected variables
- Add version, commit, and date variables to main.go with default values
- Update Makefile ldflags to use consistent variable names
- Add detailed 'version' subcommand showing build info, Go version, and platform
- Maintain compatibility with existing release workflow
- Support both --version flag (simple) and version subcommand (detailed)
2026-01-10 11:58:11 +01:00