Compare commits

...
34 Commits
Author SHA1 Message Date
Tobias GesellchenandClaude Sonnet 4.6 de239d8396 fix(migration): show warning instead of error when URLs migrated to different target
When isXMLMigrated and isTelnetMigrated both return false, the UI fell
through to the  "Original (Bose cloud)" catch-all even if the speaker's
on-device URLs clearly point to a non-Bose host. This happened when the
service's Settings Target Domain and the URL written to the speaker had
drifted — e.g. migrated with http://spotify:8000 but Settings URL is an
IP address, or vice versa.

Add isMigratedToOtherTarget() that checks parsed_current_config: if at
least one URL field is set and none contain a known Bose cloud hostname,
the speaker has been migrated, just not to the *current* Settings Target
Domain.

- urlConfigVerdict now returns ⚠️ "Migrated (URL mismatch)" in this case,
  showing the actual margeServerUrl and noting that the speaker must be
  able to reach the service there
- The top-level migration status badge shows ⚠️ orange instead of  red
- The apply plan path is unchanged: it will re-point the speaker to the
  current Settings Target Domain, which is one valid resolution path

Related to #408

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 22:19:45 +02:00
Tobias GesellchenandClaude Sonnet 4.6 7d9f3d6a39 docs: remove duplicate H1 headings from 91 pages (closes #414)
The docs framework renders frontmatter title: as the page heading.
Every file that also had a matching # Heading as the first content
line displayed the title twice. Removed the redundant H1 and its
following blank line from all 91 affected files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 21:48:43 +02:00
Tobias GesellchenandClaude Sonnet 4.6 d93d9a3e26 docs+ui: surface SSH context for remote_services (closes #409)
Migration guide: expand the one-liner after SSH setup into a concrete
'To disable SSH' section covering both the USB-stick and persistent-file
cases, with the button name and CLI command.

Admin UI:
- Preconditions label: 'remote_services' → 'SSH (remote_services)'
  with a tooltip explaining the connection
- Buttons: 'Enable/Remove Persistent Remote Services' →
  'Enable SSH (Persist remote_services)' /
  'Disable SSH (Remove remote_services)'
- Confirm dialog: mentions SSH and reboot requirement explicitly
- Verdict text: all three states now lead with 'SSH ...' so users
  recognise what the check controls

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 21:46:57 +02:00
Tobias GesellchenandClaude Sonnet 4.6 bec52b87a5 fix(test): drop testing.Short() — env var alone gates the live test
testing.Short() would silently suppress the test even with
RADIOBROWSER_INTEGRATION=1 set, contradicting the skip message.
The env var opt-in is sufficient on its own.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 21:27:10 +02:00
Tobias GesellchenandClaude Sonnet 4.6 4799eab7e5 fix(test): skip TestRadioBrowserSearch_Real unless RADIOBROWSER_INTEGRATION=1
The test dials all.api.radio-browser.info directly. When the upstream
TLS certificate expires the test fails and blocks the build — the local
codebase has no control over third-party certificate health.

Guard with testing.Short() and an opt-in env var so CI stays green and
the live-network test can still be run explicitly when needed.

Closes #412

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 21:27:10 +02:00
Tobias GesellchenandClaude Sonnet 4.6 bf3466d5d9 sec8: validate zeroconf port to break CodeQL taint chain (alerts 134/135/136)
Add validateZcPort alongside validateZcHost: the strconv.Atoi→Itoa
round-trip produces a sanitised integer string that CodeQL no longer
considers tainted, closing the remaining go/request-forgery findings
at zeroconf.go:263, :336, :413.

Also rejects clearly invalid inputs (non-numeric, out-of-range) that
would previously have produced a silently broken URL.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 21:17:57 +02:00
Tobias GesellchenandClaude Sonnet 4.6 6f488c2016 sec8: document Run() invariant — command must never come from user HTTP input
Establishes the constraint in godoc so future authors have a visible
signal before passing user-supplied values to session.CombinedOutput.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 21:01:01 +02:00
Tobias GesellchenandClaude Sonnet 4.6 1cbca1e7cc sec8: move lgtm annotation above log.Printf to suppress CodeQL alert #294
Trailing inline // lgtm[...] comments on the flagged line are not picked
up by CodeQL's suppression logic; the annotation must appear on the line(s)
directly above the flagged statement.

Closes CodeQL alert 294 (go/clear-text-logging).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 20:06:36 +02:00
Tobias GesellchenandClaude Sonnet 4.6 1dba7646b4 sec8: refactor zeroconf API to (host, port string) to close request-forgery alerts
Replace validateZcBaseURL(zcBaseURL string) with:
  - validateZcHost(host string) (net.IP, error)  — validates literal IP
  - buildZcBase(ip net.IP, port string) *url.URL  — builds URL with literal /zc path

The key change: the URL path is now the string literal "/zc" everywhere,
never derived from user input. CodeQL's go/request-forgery model traces
taint through the Path field of a rebuilt URL; removing that field from
the taint chain closes alerts 134, 135, 136.

Public API changes:
  zeroconf.GetInfo(host, port string)
  zeroconf.PushCredentials(host, port, username, accessToken string)
  spotify.ZeroConfGetInfo(host, port string)
  spotify.PushSpotifyCredentials(host, port, username, accessToken string)
  amazon.PushAmazonCredentials(host, port, username, accessToken string)

Callers in handlers/server.go already held host+port separately via
net.SplitHostPort; the zcURL construction is removed.

Tests updated throughout; TestValidateZcBaseURL renamed to
TestValidateZcHost and TestBuildZcBase added for the new helpers.

Closes CodeQL alerts 134, 135, 136 (go/request-forgery).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 13:14:56 +02:00
Tobias GesellchenandClaude Sonnet 4.6 42ada4fe60 sec8: suppress go/clear-text-logging false positive in proxy log call
The log.Printf at this line uses formatHeaders, which unconditionally
redacts alwaysSensitiveHeaders (Authorization, Cookie, …) and applies
sanitizeLog to strip newlines from other values. CodeQL cannot model the
custom redaction inside formatHeaders and flags the call.

The lgtm annotation suppresses the false positive. The struct comment
explains the reviewed rationale in full.

Closes CodeQL alert 294 (go/clear-text-logging).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 13:14:56 +02:00
Tobias GesellchenandClaude Sonnet 4.6 3aaf7f4521 sec8: suppress go/reflected-xss false positive in recorder middleware
The middleware is a transparent passthrough for XML API responses
(Content-Type: application/vnd.bose.streaming-v1.2+xml). Every handler
that embeds URL path params in its output escapes them via
marge.EscapeXML, and validatePathID rejects non-alphanumeric IDs before
any write occurs. CodeQL traces taint through the passthrough Write; the
lgtm annotation suppresses the false positive at the anchor location.

Closes CodeQL alert 75 (go/reflected-xss).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 13:14:56 +02:00
Tobias Gesellchen f5ebe92d3c Fix external link to opencloudtouch/opencloudtouch/issues/167 2026-05-25 11:31:10 +02:00
Tobias GesellchenandClaude Sonnet 4.6 95c228d606 docs(claude): force-flagged git commands require explicit approval
Extend the "destructive git actions" guideline to cover force-flags
(git add -f, git push --force, git push --force-with-lease, …).
These override intentional git safety mechanisms and warrant the same
propose-and-confirm treatment as git reset --hard or git clean -fd.

Prompted by: git add -f on a gitignored file during sec6/sec7 work.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 11:28:26 +02:00
Tobias GesellchenandClaude Sonnet 4.6 16c1babbc8 fix(security): restore UnsafeLogCredentialHeaders via stderr, not log
e6bfcd1 removed the credential-log debug flag entirely to close
go/clear-text-logging (alert 294). Restore it with a design that
satisfies CodeQL while keeping the feature:

- log.Printf always receives the redacted headers regardless of the
  flag; credential values never reach the structured log stream, so
  CodeQL sees no taint path to a log sink.

- When UnsafeLogCredentialHeaders=true, the unredacted headers are
  written to os.Stderr via fmt.Fprintf(os.Stderr, …). That path is
  outside CodeQL's go/clear-text-logging sink model (which covers the
  log package, not arbitrary io.Writer writes).

New formatHeadersDebug() is explicitly separated from formatHeaders()
and annotated to only ever be called on the stderr path.

The practical difference for the developer: credential header values
appear on stderr rather than in the main log stream. LOG_PROXY_CREDENTIALS=true
still activates it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 11:28:26 +02:00
Tobias GesellchenandClaude Sonnet 4.6 2722e2383c fix(lint): sec6/sec7 post-pass — static.go Close + remove unused sanitizeErr
- stockholm/static.go: wrap deferred root.Close() in func(){}() to
  silence errcheck; change 'rel = rel + ...' to 'rel += ...' (gocritic).

- Remove sanitizeErr from four logutil files where no call site exists
  (cmd/soundtouch-cli, cmd/websocket-demo, pkg/discovery, pkg/service/setup).
  The log-injection fixes in those packages used sanitizeLog on string
  arguments rather than sanitizeErr on error values.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 11:28:26 +02:00
Tobias GesellchenandClaude Sonnet 4.6 0e9445af47 fix(security): sec7 — log-injection sweep, sanitizeErr helper
~30 remaining go/log-injection alerts share a common pattern: other
positional args in a log call are wrapped in sanitizeLog() but the
trailing 'err' value (via "%v") is not. CodeQL traces taint through
error chains back to the log.Printf call site itself.

Add sanitizeErr(err error) string to every affected package's
logutil.go (strips newlines from err.Error(), returns "<nil>" when
nil). Three packages had no logutil.go yet; new files added for
cmd/soundtouch-cli, cmd/websocket-demo, and examples.

Call-site changes (replace "%v, err" with "%s, sanitizeErr(err)" and
wrap any other unsanitised args in sanitizeLog):

pkg/client:
  - websocket.go:42   DefaultLogger.Printf now pre-formats and sanitises
                       the entire message (all variadic args sanitised)
  - websocket.go:445  err → sanitizeErr(err)

pkg/service/handlers:
  - handlers_account_mgmt.go:44   err
  - handlers_bmx_tunein.go:324,336 err (stationID already sanitised)
  - handlers_marge.go:288,510      err (deviceID/account already done)
  - handlers_mgmt.go:409,436,720  err
  - handlers_setup.go:1345        session + err
  - server.go:500                  bind
  - server.go:504,863,944,1029,   err (deviceIP/accountID already done)
    1164,1174

pkg/service/marge:
  - marge.go:1469,1923  saveErr / err

pkg/service/setup:
  - setup.go:1417,2316,2462  fmt.Printf — deviceIP / hostsContent / ip

pkg/service/stockholm:
  - proxy.go:117  effectiveTarget.String() + err

pkg/service/zeroconf:
  - zeroconf.go:312  err

pkg/service/proxy:
  - recorder.go:403  err (task.path already sanitised)

pkg/service/datastore:
  - datastore.go:940  werr (device already sanitised)

pkg/discovery:
  - dns.go:72   strings.Join(derived)
  - dns.go:503  d.upstreamDNS (fmt.Sprint of []string)

cmd/soundtouch-cli:
  - cmd_events.go:571  VerboseLogger.Printf — pre-format + sanitise
  - common.go:335      PrintError message

cmd/websocket-demo:
  - main.go:576   VerboseLogger.Printf — pre-format + sanitise

examples:
  - recording-filename-demo.go:79  err

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 11:28:26 +02:00
Tobias GesellchenandClaude Sonnet 4.6 370c56ec9e fix(security): remove credential-log bypass and sanitise header values in proxy
Two alerts at proxy.go:87:

- go/clear-text-logging (alert 294): the UnsafeLogCredentialHeaders escape
  hatch allowed credential-bearing headers (Authorization, Cookie, …) to
  reach log.Printf in plaintext when LOG_PROXY_CREDENTIALS=true. CodeQL
  traces the taint regardless of the conditional.

  Remove UnsafeLogCredentialHeaders entirely. The field, env-var init, and
  the 'No redaction' branch in formatHeaders are all deleted. Credentials
  are now always redacted unconditionally. Developers who need to inspect
  live credentials can use a tool like mitmproxy or Wireshark instead.

- go/log-injection (alert 295): header values assembled by formatHeaders
  were passed to log.Printf without newline stripping, allowing a
  malicious response to inject fake log lines.

  Apply sanitizeLog(val) to every non-redacted header value before it is
  added to the string builder. Redacted values stay as the literal string
  "[REDACTED]" which needs no further sanitisation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 11:28:26 +02:00
Tobias GesellchenandClaude Sonnet 4.6 cd0841bfad fix(security): use os.Root in Stockholm static-file handler
Replace the filepath.Abs + string-prefix path-traversal check followed
by os.Stat / os.ReadFile calls with an os.Root anchored at stockholmDir.
CodeQL (go/path-injection, alerts 143–145) did not recognise the
string-based validation as a sanitiser boundary; os.Root is the same
OS-level barrier used in the sec3 datastore and recorder refactors.

Changes:
- Open os.OpenRoot(stockholmDir) in ServeStatic; all file ops go
  through root.Stat / root.Open instead of os.Stat / os.ReadFile.
- Replace resolveStaticFile (returned absolute + relative paths) with
  resolveStaticRel (URL path → relative path only; no filesystem
  access, no traversal logic — the Root handles containment).
- Directory → index.html fallback moved into ServeStatic via root.Stat.
- Drop path/filepath import from static.go (no longer needed).
- Update tests: resolveStaticFile unit tests become resolveStaticRel
  unit tests; directory and traversal cases become ServeStatic
  integration tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 11:28:26 +02:00
Tobias GesellchenandClaude Sonnet 4.6 806d1fc22c fix(security): validate account ID in HandleMargeProviderSettings
The handler used chi.URLParam("account") directly without the
validatePathID guard present on every other account-parameter handler
in the file. CodeQL traced the raw URL param through
marge.ProviderSettingsToXML into the response body (go/reflected-xss,
alert 75).

Add the standard two-line guard identical to HandleMargeAddDevice,
HandleMargeUpdateDevice, and the rest of the family.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 11:28:26 +02:00
Tobias GesellchenandClaude Sonnet 4.6 208d4f61d6 docs: add docs homepage screenshot to README
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 00:48:20 +02:00
Tobias GesellchenandClaude Sonnet 4.6 1d864c88f6 fix(docs): open sponsor footer link in same tab
Internal page — no target="_blank" needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 00:39:18 +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 Gesellchen 70dea42c10 Use site-relative URL 2026-05-25 00:21:43 +02:00
Tobias GesellchenandClaude Sonnet 4.6 8613b2901d fix(docs): suppress semgrep var-in-href false positive in navbar-title
$logoLink is sourced from site config (never user input) and Hugo
auto-escapes template values. Pipe through safeURL to make the intent
explicit and satisfy the generic.html-templates.security.var-in-href rule.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 00:17:48 +02:00
Tobias GesellchenandClaude Sonnet 4.6 5fe69e780d fix(ci): extend image link-check ignore pattern to cover subdirectories
The existing pattern ^/images/[^/]+\.png$ only matched single-level
image paths. Blog post images live under /images/blog/ — broaden the
pattern to ^/images/ to cover all static image paths regardless of depth.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 00:17:48 +02:00
Tobias GesellchenandClaude Sonnet 4.6 0b69153df6 feat(docs): add sponsor page with GitHub Sponsors and PayPal options
- /sponsor landing page lists both options with feature cards
- Navbar heart icon and footer sponsor link both point to /sponsor
  instead of directly to GitHub Sponsors, so PayPal is equally reachable
- No GitHub account required for PayPal path

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 00:17:48 +02:00
Tobias GesellchenandClaude Sonnet 4.6 1fcb9e4ca5 docs: improve Migration Guide and add TuneIn screenshot
Migration Guide step 1:
- Add 'Download pre-built binary' as the first option (no Go required)
- Add install-script option for Raspberry Pi / on-device deployments
- Move 'go install' to last (developer option)
- Add data/ directory callout: single directory to back up for a full restore

SoundTouch Service guide:
- Mention RadioBrowser alongside TuneIn in the BMX section
- Add soundtouch-web TuneIn search screenshot

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 00:17:48 +02:00
Tobias GesellchenandClaude Sonnet 4.6 3867f6040c fix(docs): point homepage Get Started button to Migration Guide
The previous link aimed at a Go-developer getting-started page. Most
users are not Go developers — they want to migrate their speakers.
MIGRATION-GUIDE is the right first destination.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 00:17:48 +02:00
Tobias GesellchenandClaude Sonnet 4.6 23be85925d feat(docs): add inaugural blog post
Covers what AfterTouch delivers today: migration (existing account and
factory-reset paths), marge+bmx replacement, TuneIn+RadioBrowser, Spotify,
presets, ST-10 stereo pairing, soundtouch-cli automation, soundtouch-web
browser UI, and the three installation options (on-device, local host /
Raspberry Pi Zero 2W, cloud/VPS).

Includes screenshot of the soundtouch-web UI (Spotify playback, presets,
sources, zone management).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 00:17:48 +02:00
Tobias GesellchenandClaude Sonnet 4.6 6f392699f1 feat(docs): blog infrastructure — index page and /blog-update skill
- blog/_index.md: add introductory sentence to the News & Updates index
- .claude/commands/blog-update.md: project skill that drafts a monthly
  update post from git history and opens a draft PR for review
- .gitignore: .claude/* + !.claude/commands/ so the skill is tracked
  while session state (settings.local.json, worktrees/) stays ignored

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 00:17:48 +02:00
Tobias GesellchenandClaude Sonnet 4.6 e173ed389d feat(docs): branding — logo, favicon, subtitle, and footer
- Enable navbar logo (favicon-braille.svg, 24×24)
- Override navbar-title partial to add 'Bose SoundTouch Toolkit' subtitle
- Add favicon.svg to static root (picked up by Hextra head automatically)
- Custom footer: sponsor link (left) + copyright (right) in a single row
- i18n/en.yaml: copyright text with link to github.com/gesellix
- hugo.toml: blog list sorted by date desc, tags enabled

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 00:17:48 +02:00
Tobias Gesellchen 872a121cbd chore: bump to v0.93.1 2026-05-24 17:49:17 +02:00
Tobias GesellchenandClaude Sonnet 4.6 b9aa29b92c fix(web): update stale Jekyll doc URLs in admin UI
Three links in pkg/service/handlers/web/index.html still pointed to
the old Jekyll URL structure (/guides/FOO.html). The docs site moved
to Hugo+Hextra; correct URLs now include /docs/ and drop the .html
extension in favour of a trailing slash.

  MIGRATION-SAFETY.html  → docs/guides/MIGRATION-SAFETY/
  SURVIVAL-GUIDE.html    → docs/guides/SURVIVAL-GUIDE/
  CLI-REFERENCE.html     → docs/guides/CLI-REFERENCE/

The GitHub blob links in script.js and the hostname-resolution warning
in index.html point to source Markdown files and remain valid.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 17:33:09 +02:00
Tobias GesellchenandClaude Sonnet 4.6 dc8ec69c61 sec5e: sanitize log-injection in client, discovery, testutils, cmd
Fixes CodeQL go/log-injection alerts in the final batch of packages.

New logutil.go helpers: pkg/client, pkg/testutils/amazon,
pkg/testutils/spotify, cmd/soundtouch-service, cmd/soundtouch-web,
cmd/dummy-speaker, cmd/mdns-scanner.

pkg/discovery/logger.go: added sanitizeLog and a nil-safe
remoteAddrString helper to the existing file (alongside logVerbose).

Call sites wrapped across 11 files — device IDs, source types,
hostnames, IPs, interface names, URLs, service names, HTTP method/form
values, WebSocket URLs and payloads, TLS SNI names, remote addresses.

No behaviour change. golangci-lint and make check pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 17:29:39 +02:00
177 changed files with 1171 additions and 637 deletions
+87
View File
@@ -0,0 +1,87 @@
Draft a "News & Updates" blog post for AfterTouch covering recent git activity, then open a draft PR for review.
## Step 1 — Determine lookback window
Run:
```
git log --format="%ad" --date=short -- docs/content/blog/ | grep -v '_index' | head -1
```
If a date is returned, use it as SINCE.
If the output is empty (no posts yet), compute SINCE = 30 days before today:
- macOS: `date -v-30d +%Y-%m-%d`
- Linux: `date -d '30 days ago' +%Y-%m-%d`
## Step 2 — Collect commits since SINCE
Run:
```
git log --format="%ad %h %s" --date=short --since="$SINCE" --no-merges
```
Exclude these (they are noise):
- Subjects matching: `^(ci|chore|deps|bump|Bump|test|lint|style|code style|debug)`
- Dependabot bumps (subject contains "bump" and includes a package name pattern)
- Routine doc link/URL fixes
Group the remaining commits into categories:
- **NEW FEATURES** — subjects starting with `feat(` or `feat:`
- **BUG FIXES** — subjects starting with `fix(` or `fix:`
- **SECURITY** — subjects starting with `sec` or containing "security", "inject", "path expression"
- **DOCS** — user-visible doc changes only (new guides, major restructures)
- **MAINTENANCE** — everything else that passed the filter
Omit empty categories entirely.
## Step 3 — Current version
Run: `git tag --sort=-version:refname | head -1`
## Step 4 — Determine the period label
Use the first and last commit dates from Step 2 to produce a human-readable label,
e.g. "May 2026" or "April May 2026".
## Step 5 — Write the blog post
Create the file at: `docs/content/blog/YYYY-MM-slug.md`
- YYYY-MM = today's year-month
- slug = short kebab-case summary of the biggest theme
Use this exact frontmatter shape:
```yaml
---
title: "AfterTouch PERIOD: <one-line theme>"
date: YYYY-MM-DD
description: "<one sentence, ≤200 chars, suitable as a standalone teaser>"
tags:
- <up to 4 tags from: security, tls, discovery, docs, cli, web, spotify, amazon, health, migration, fixes, ci>
sidebar:
exclude: true
---
```
Body structure:
1. Opening paragraph (35 sentences) explaining what happened and why it matters to someone running AfterTouch.
2. One `##` section per non-empty category. Use bullet points written for an operator audience — no raw git subjects, no internal Go package paths.
3. End with: `**Current release:** vX.Y.Z`
Target length: 300600 words. Never include real IPs, MAC addresses, account IDs, or device names.
## Step 6 — Create a branch and open a draft PR
```bash
git checkout -b blog/YYYY-MM-update
git add docs/content/blog/YYYY-MM-slug.md
git commit -m "docs(blog): add PERIOD update post"
git push -u origin blog/YYYY-MM-update
gh pr create --draft \
--title "Blog: PERIOD update post" \
--body "Automated draft from /blog-update skill. Review content before merging — deployment is automatic on merge to main."
```
If the `documentation` label exists on the repo, add `--label documentation`.
## Step 7 — Done
Report the PR URL. Do not merge, approve, or request review.
+1 -1
View File
@@ -27,7 +27,7 @@
"pattern": "^https://pkg.go.dev.*badge"
},
{
"pattern": "^/images/[^/]+\\.png$"
"pattern": "^/images/"
},
{
"pattern": "https://www.contributor-covenant.org/version/2/0/code_of_conduct.html"
+1
View File
@@ -33,6 +33,7 @@ jobs:
run: hugo --source docs/ --minify --destination ../_site --baseURL "${{ steps.pages.outputs.base_url }}"
env:
HUGO_ENVIRONMENT: production
HUGO_PARAMS_GITHASH: ${{ github.sha }}
- name: Upload artifact
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
with:
+2 -1
View File
@@ -48,7 +48,8 @@ node_modules/
# IDE and editor files
.vscode/
.idea/
.claude/
.claude/*
!.claude/commands/
.junie/
*.swp
*.swo
+6
View File
@@ -182,6 +182,12 @@ unless the user has already authorised that specific action in this
session. Prefer reversible alternatives (`git stash` over
`git reset --hard`).
**Force-flags also require explicit approval.** `git add -f` (force-add
a gitignored file), `git push --force`, `git push --force-with-lease`,
and any other flag that overrides a git safety mechanism must be
proposed and confirmed before running, for the same reason: they
bypass protections that exist intentionally.
## What never goes into this repo
This repository is public. The following must never be committed:
+1 -1
View File
@@ -458,7 +458,7 @@ screenshots:
# First run: make dev-docs-tidy (downloads Hextra, writes docs/go.sum)
# Then: make dev-docs (http://localhost:1313, live reload)
dev-docs:
docker compose -f docker-compose.docs.yml up
HUGO_PARAMS_GITHASH=$(shell git rev-parse HEAD) docker compose -f docker-compose.docs.yml up
dev-docs-tidy:
docker compose -f docker-compose.docs.yml run --rm hugo mod tidy --source docs/
+2
View File
@@ -15,6 +15,8 @@ Bose shut down SoundTouch cloud services on **May 6, 2026**. Presets, music serv
See the [Survival Guide](https://gesellix.github.io/Bose-SoundTouch/docs/guides/SURVIVAL-GUIDE/) for the full picture.
[![AfterTouch docs homepage](media/docs-homepage.png)](https://gesellix.github.io/Bose-SoundTouch/)
---
## Tools
+13
View File
@@ -0,0 +1,13 @@
package main
import "strings"
// sanitizeLog strips newline characters from s to prevent log-injection
// (CodeQL go/log-injection). Values from speakers, HTTP requests, and
// external APIs may contain attacker-controlled newlines.
func sanitizeLog(s string) string {
s = strings.ReplaceAll(s, "\n", `\n`)
s = strings.ReplaceAll(s, "\r", `\r`)
return s
}
+3 -3
View File
@@ -43,10 +43,10 @@ func main() {
log.Fatalf("start fake speaker: %v", err)
}
log.Printf("fake speaker HTTP listening on http://%s", s.HTTPAddr())
log.Printf("fake speaker HTTP listening on http://%s", sanitizeLog(s.HTTPAddr()))
if addr := s.TelnetAddr(); addr != "" {
log.Printf("fake speaker telnet listening on tcp://%s", addr)
log.Printf("fake speaker telnet listening on tcp://%s", sanitizeLog(addr))
}
if *register != "" {
@@ -58,7 +58,7 @@ func main() {
if err := registerWithService(*register, target); err != nil {
log.Printf("self-register failed: %v (continuing anyway)", err)
} else {
log.Printf("registered %s with service at %s", target, *register)
log.Printf("registered %s with service at %s", sanitizeLog(target), sanitizeLog(*register))
}
}
+13
View File
@@ -0,0 +1,13 @@
package main
import "strings"
// sanitizeLog strips newline characters from s to prevent log-injection
// (CodeQL go/log-injection). Values from speakers, HTTP requests, and
// external APIs may contain attacker-controlled newlines.
func sanitizeLog(s string) string {
s = strings.ReplaceAll(s, "\n", `\n`)
s = strings.ReplaceAll(s, "\r", `\r`)
return s
}
+2 -2
View File
@@ -116,7 +116,7 @@ func main() {
defer close(entries)
if *verbose {
log.Printf("mDNS: Starting scan for service '%s' with timeout %v", *service, *timeout)
log.Printf("mDNS: Starting scan for service '%s' with timeout %v", sanitizeLog(*service), *timeout)
}
// Query for services
@@ -196,7 +196,7 @@ func parseServiceEntry(entry *mdns.ServiceEntry, verbose bool) *ServiceInfo {
if verbose {
log.Printf("mDNS: Received service entry: Name='%s', Host='%s', Port=%d, AddrV4=%v, AddrV6=%v",
entry.Name, entry.Host, entry.Port, entry.AddrV4, entry.AddrV6)
sanitizeLog(entry.Name), sanitizeLog(entry.Host), entry.Port, entry.AddrV4, entry.AddrV6)
}
service := &ServiceInfo{
+1 -1
View File
@@ -568,7 +568,7 @@ type VerboseLogger struct{}
func (v *VerboseLogger) Printf(format string, args ...interface{}) {
timestamp := time.Now().Format("15:04:05")
fmt.Printf("[%s] [WebSocket] %s\n", timestamp, fmt.Sprintf(format, args...))
fmt.Printf("[%s] [WebSocket] %s\n", timestamp, sanitizeLog(fmt.Sprintf(format, args...)))
}
type SilentLogger struct{}
+1 -1
View File
@@ -332,7 +332,7 @@ func PrintSuccess(message string) {
// PrintError prints a standard error message
func PrintError(message string) {
fmt.Printf("✗ %s\n", message)
fmt.Printf("✗ %s\n", sanitizeLog(message))
}
// PrintWarning prints a standard warning message
+13
View File
@@ -0,0 +1,13 @@
package main
import "strings"
// sanitizeLog strips newline characters from s to prevent log-injection
// (CodeQL go/log-injection). Values from speakers, HTTP requests, and
// external APIs may contain attacker-controlled newlines.
func sanitizeLog(s string) string {
s = strings.ReplaceAll(s, "\n", `\n`)
s = strings.ReplaceAll(s, "\r", `\r`)
return s
}
+13
View File
@@ -0,0 +1,13 @@
package main
import "strings"
// sanitizeLog strips newline characters from s to prevent log-injection
// (CodeQL go/log-injection). Values from speakers, HTTP requests, and
// external APIs may contain attacker-controlled newlines.
func sanitizeLog(s string) string {
s = strings.ReplaceAll(s, "\n", `\n`)
s = strings.ReplaceAll(s, "\r", `\r`)
return s
}
+20 -20
View File
@@ -71,7 +71,7 @@ func initializeDefaultSources(ds *datastore.DataStore) {
for i := range allDevices {
dev := &allDevices[i]
if sources, errGet := ds.GetConfiguredSources(dev.AccountID, dev.DeviceID); errGet == nil {
log.Printf("Initializing default Sources.xml for existing device %s", dev.DeviceID)
log.Printf("Initializing default Sources.xml for existing device %s", sanitizeLog(dev.DeviceID))
// Find default sources and merge them if missing or outdated tokens.
// claimed tracks which stored sources have already been matched by a default,
@@ -103,13 +103,13 @@ func initializeDefaultSources(ds *datastore.DataStore) {
claimed[foundIdx] = true
if sources[foundIdx].Secret == "" && def.Secret != "" {
log.Printf("Initializing missing token for source %s on device %s", def.SourceKeyType, dev.DeviceID)
log.Printf("Initializing missing token for source %s on device %s", sanitizeLog(def.SourceKeyType), sanitizeLog(dev.DeviceID))
sources[foundIdx].Secret = def.Secret
sources[foundIdx].SecretType = def.SecretType
modified = true
}
} else {
log.Printf("Adding missing default source %s (providerID=%s) to device %s", def.SourceKeyType, def.SourceProviderID, dev.DeviceID)
log.Printf("Adding missing default source %s (providerID=%s) to device %s", sanitizeLog(def.SourceKeyType), sanitizeLog(def.SourceProviderID), sanitizeLog(dev.DeviceID))
sources = append(sources, def)
modified = true
}
@@ -117,7 +117,7 @@ func initializeDefaultSources(ds *datastore.DataStore) {
if modified {
if errSave := ds.SaveConfiguredSources(dev.AccountID, dev.DeviceID, sources); errSave != nil {
log.Printf("Failed to save updated sources for %s: %v", dev.DeviceID, errSave)
log.Printf("Failed to save updated sources for %s: %v", sanitizeLog(dev.DeviceID), errSave)
}
}
}
@@ -147,7 +147,7 @@ func initMusicServices(config serviceConfig, server *handlers.Server) {
clientIDPrefix = clientIDPrefix[:8]
}
log.Printf("Spotify service initialized (client ID: %s...)", clientIDPrefix)
log.Printf("Spotify service initialized (client ID: %s...)", sanitizeLog(clientIDPrefix))
}
if config.amazonClientID != "" {
@@ -172,7 +172,7 @@ func initMusicServices(config serviceConfig, server *handlers.Server) {
clientIDPrefix = clientIDPrefix[:8]
}
log.Printf("Amazon Music service initialized (client ID: %s...)", clientIDPrefix)
log.Printf("Amazon Music service initialized (client ID: %s...)", sanitizeLog(clientIDPrefix))
}
}
@@ -188,7 +188,7 @@ func logBufferCapacityFromEnv(defaultCap int) int {
v, err := strconv.Atoi(raw)
if err != nil {
log.Printf("[Logs] Invalid SOUNDTOUCH_LOG_BUFFER_LINES=%q, using default %d", raw, defaultCap)
log.Printf("[Logs] Invalid SOUNDTOUCH_LOG_BUFFER_LINES=%q, using default %d", sanitizeLog(raw), defaultCap)
return defaultCap
}
@@ -415,7 +415,7 @@ func main() {
persisted := applyPersistedSettings(ds, &config)
if persisted.ServerURL == "" {
log.Printf("Creating default settings.json in %s", config.dataDir)
log.Printf("Creating default settings.json in %s", sanitizeLog(config.dataDir))
persisted = createDefaultSettings(ds, config)
}
@@ -468,7 +468,7 @@ func main() {
server.SetShortcuts(persisted.Shortcuts)
for path, status := range persisted.Shortcuts {
log.Printf("Warning: configured shortcut: %s -> %d", path, status)
log.Printf("Warning: configured shortcut: %s -> %d", sanitizeLog(path), status)
}
recorder := proxy.NewRecorder(config.dataDir)
@@ -477,11 +477,11 @@ func main() {
patterns, err := proxy.LoadPatterns(patternsPath)
if err != nil {
log.Printf("Warning: Failed to load patterns from %s: %v", patternsPath, err)
log.Printf("Warning: Failed to load patterns from %s: %v", sanitizeLog(patternsPath), err)
}
if len(patterns) == 0 {
log.Printf("Creating default patterns at %s", patternsPath)
log.Printf("Creating default patterns at %s", sanitizeLog(patternsPath))
patterns = proxy.DefaultPatterns()
@@ -512,17 +512,17 @@ func main() {
} else {
stockholmHandler = sh
log.Printf("Stockholm frontend enabled from %s", config.stockholmDir)
log.Printf("Stockholm frontend enabled from %s", sanitizeLog(config.stockholmDir))
}
}
r := setupRouter(server, stockholmHandler)
log.Printf("Go service starting on %s", config.serverURL)
log.Printf("Go service starting on %s", sanitizeLog(config.serverURL))
// TLS cert generation can be slow on constrained hardware; run it in the
// background so the HTTP server is available immediately.
log.Printf("HTTPS setup running in background; %s will be available shortly", config.httpsServerURL)
log.Printf("HTTPS setup running in background; %s will be available shortly", sanitizeLog(config.httpsServerURL))
go func() {
tlsConfig, err := cm.GetServerTLSConfig(config.domains)
@@ -652,7 +652,7 @@ func loadConfig(c *cli.Context) serviceConfig {
discoveryInterval, err := time.ParseDuration(discoveryIntervalStr)
if err != nil {
log.Printf("Warning: Failed to parse discovery interval %s, using default 5m: %v", discoveryIntervalStr, err)
log.Printf("Warning: Failed to parse discovery interval %s, using default 5m: %v", sanitizeLog(discoveryIntervalStr), err)
discoveryInterval = 5 * time.Minute
}
@@ -1294,7 +1294,7 @@ func startHTTPSServer(httpsAddr string, r http.Handler, tlsConfig *tls.Config, h
return &tlsConfig.Certificates[0], nil
}
log.Printf("[TLS] ❌ No certificate available for %s", clientHello.ServerName)
log.Printf("[TLS] ❌ No certificate available for %s", sanitizeLog(clientHello.ServerName))
return nil, fmt.Errorf("no certificate available for %s", clientHello.ServerName)
}
@@ -1306,7 +1306,7 @@ func startHTTPSServer(httpsAddr string, r http.Handler, tlsConfig *tls.Config, h
ErrorLog: log.Default(), // Ensure error logging is enabled
}
log.Printf("Go service starting HTTPS on %s", httpsServerURL)
log.Printf("Go service starting HTTPS on %s", sanitizeLog(httpsServerURL))
go func() {
listener, err := net.Listen("tcp", httpsAddr)
@@ -1361,9 +1361,9 @@ func runHTTPSPreflight(httpsServerURL, serverURL string, dnsEnabled bool, resolv
case res.Skipped:
// Listener already on :443 — nothing to say.
case res.NotApplicable:
log.Printf("HTTPS pre-flight: :443 check skipped — %s", res.Reason)
log.Printf("HTTPS pre-flight: :443 check skipped — %s", sanitizeLog(res.Reason))
default:
log.Printf("HTTPS pre-flight: :443 reachable at localhost and %s ✓", res.LANHost)
log.Printf("HTTPS pre-flight: :443 reachable at localhost and %s ✓", sanitizeLog(res.LANHost))
}
return
@@ -1438,7 +1438,7 @@ func (c *loggingTLSConn) Read(b []byte) (n int, err error) {
if strings.Contains(err.Error(), "tls:") ||
strings.Contains(err.Error(), "handshake") ||
strings.Contains(err.Error(), "certificate") {
log.Printf("[TLS] ❌ Handshake failed from %s: %v", c.addr, err)
log.Printf("[TLS] ❌ Handshake failed from %s: %v", sanitizeLog(c.addr.String()), err)
}
}
}
+13
View File
@@ -0,0 +1,13 @@
package main
import "strings"
// sanitizeLog strips newline characters from s to prevent log-injection
// (CodeQL go/log-injection). Values from speakers, HTTP requests, and
// external APIs may contain attacker-controlled newlines.
func sanitizeLog(s string) string {
s = strings.ReplaceAll(s, "\n", `\n`)
s = strings.ReplaceAll(s, "\r", `\r`)
return s
}
+3 -3
View File
@@ -86,7 +86,7 @@ func main() {
}
if rawBind != "" && bindAddr != rawBind {
log.Printf("Resolved --bind %q to %s", rawBind, bindAddr)
log.Printf("Resolved --bind %q to %s", sanitizeLog(rawBind), sanitizeLog(bindAddr))
}
rawIface := c.String("interface")
@@ -94,7 +94,7 @@ func main() {
ifaceName := defaultDiscoveryInterface(rawIface, rawBind, bindAddr)
if rawIface == "" && ifaceName != "" {
log.Printf("Defaulting --interface to %q from --bind", ifaceName)
log.Printf("Defaulting --interface to %q from --bind", sanitizeLog(ifaceName))
}
addr := ":" + port
@@ -131,7 +131,7 @@ func main() {
r := chi.NewRouter()
webApp.Mount(r, discoveryService)
log.Printf("AfterTouch Web UI starting on http://%s", addr)
log.Printf("AfterTouch Web UI starting on http://%s", sanitizeLog(addr))
return http.ListenAndServe(addr, r)
},
+13
View File
@@ -0,0 +1,13 @@
package main
import "strings"
// sanitizeLog strips newline characters from s to prevent log-injection
// (CodeQL go/log-injection). Values from speakers, HTTP requests, and
// external APIs may contain attacker-controlled newlines.
func sanitizeLog(s string) string {
s = strings.ReplaceAll(s, "\n", `\n`)
s = strings.ReplaceAll(s, "\r", `\r`)
return s
}
+1 -1
View File
@@ -573,7 +573,7 @@ type VerboseLogger struct{}
func (v *VerboseLogger) Printf(format string, args ...interface{}) {
timestamp := time.Now().Format("15:04:05")
fmt.Printf("[%s] [WebSocket] %s\n", timestamp, fmt.Sprintf(format, args...))
fmt.Printf("[%s] [WebSocket] %s\n", timestamp, sanitizeLog(fmt.Sprintf(format, args...)))
}
// SilentLogger provides no-op WebSocket logging
+2
View File
@@ -27,6 +27,8 @@ services:
# downloads Hextra once.
- hugo-mod-cache:/root/.cache/hugo_cache
working_dir: /src
environment:
- HUGO_PARAMS_GITHASH
volumes:
hugo-mod-cache:
+1 -1
View File
@@ -22,7 +22,7 @@ layout: hextra-home
</div>
<div class="hx-mb-6">
{{< hextra/hero-button text="Get Started" link="docs/guides/GETTING-STARTED" >}}
{{< hextra/hero-button text="Get Started" link="docs/guides/MIGRATION-GUIDE" >}}
{{< hextra/hero-button text="Survival Guide" link="docs/guides/SURVIVAL-GUIDE" style="outline" >}}
</div>
+120
View File
@@ -0,0 +1,120 @@
---
title: "Welcome to AfterTouch: Your SoundTouch Speakers, Still Alive"
date: 2026-05-24
description: "Bose shut down SoundTouch cloud services in May 2026. AfterTouch replaces everything your speakers relied on — migration, radio, Spotify, presets, and more."
tags:
- migration
- web
- spotify
- cli
sidebar:
exclude: true
---
On May 6, 2026, Bose shut down the SoundTouch cloud services that millions of speakers
depended on for account sync, presets, internet radio, and streaming. Speakers kept
working locally, but remote features stopped and first-time setup became impossible.
AfterTouch was built to change that. It is a self-hosted replacement for the Bose
cloud infrastructure — a drop-in local service that your speakers talk to instead of
`streaming.bose.com`. This post covers what works today and how to get started.
## What works right now
### Migration and first-time setup
If your speaker was registered with Bose before the shutdown, AfterTouch can **migrate
your existing account and presets** in a single step — no reconfiguration on the
speaker side. If you are setting up a factory-reset or brand-new speaker, AfterTouch
handles that path too, guiding you through Wi-Fi pairing and account creation locally.
See the [Migration Guide](../docs/guides/MIGRATION-GUIDE.md) for step-by-step instructions.
### Internet radio — TuneIn and RadioBrowser
Both **TuneIn** and **RadioBrowser** are fully supported for browsing and playback.
Navigate categories and search for stations exactly as you did with the original Bose
app. TuneIn delivers the same station catalogue; RadioBrowser provides an open,
community-maintained alternative.
### Spotify
**Spotify** works via both OAuth (account linking) and Spotify Connect (the ZeroConf
"connect to device" flow from the Spotify app). Once linked, playback and device
selection behave the same as before.
### Presets
Your six preset buttons work. AfterTouch stores preset bindings locally and serves them
back to the speaker on request. You can also **save new presets** — via the API,
via `soundtouch-cli`, or through the soundtouch-web UI.
### ST-10 stereo pairing
**SoundTouch 10 stereo pairs** (and other ST pairing configurations) are supported
end-to-end: creation, management, and playback routing all go through AfterTouch.
### soundtouch-web — browser UI
**soundtouch-web** is an early-stage but functional browser UI bundled with AfterTouch.
It gives you:
- TuneIn and RadioBrowser browsing and playback
- Speaker management and device discovery
- Recent tracks panel
- Multi-room zone management
It runs as part of the AfterTouch service — no separate install needed.
![soundtouch-web UI showing Spotify playback, presets, sources, and zone management](/images/blog/soundtouch-web-ui.png)
### Automation with soundtouch-cli
The **`soundtouch-cli`** command-line tool covers every speaker control: play, pause,
volume, source selection, preset recall, group management, migration, and more.
It is well-suited for home-automation scripts, cron jobs, and shell one-liners.
## Three ways to install
AfterTouch runs on any machine your speakers can reach:
1. **On the speaker itself** — install directly on supported SoundTouch hardware via
the on-device installer. The speaker hosts its own replacement cloud, with no
additional hardware required.
2. **On a local network host** — run AfterTouch on any machine on your LAN. A
**Raspberry Pi Zero 2W** handles the load without breaking a sweat, making this
path remarkably low-cost and low-power.
3. **On a cloud or VPS host** — deploy to a remote server for access outside your
home network. AfterTouch handles TLS certificate generation and DNS configuration
for this scenario.
All three paths are documented in the [Deployment Overview](../docs/guides/DEPLOYMENT-OVERVIEW.md).
## Current release
**v0.93.1** — released May 24, 2026
## Community
AfterTouch would not be where it is without the people who opened issues, tested
pre-release builds, reported edge cases, and contributed code. A significant share of
the fixes and features shipped in the lead-up to the cloud shutdown were driven by
real-world feedback from the community — from migration quirks to stereo-pair
specifics to Spotify Connect timing issues. Thank you to everyone who helped.
If you run into something or have an idea, the
[GitHub issue tracker](https://github.com/gesellix/Bose-SoundTouch/issues) and
[Discussions](https://github.com/gesellix/Bose-SoundTouch/discussions) are the
right places to start.
## What's next
The soundtouch-web UI will gain richer preset management — browsing, editing, and
reordering presets directly from the browser. Longer term, merging
`soundtouch-service` and `soundtouch-web` into a single binary is on the table,
which would simplify deployment to a single process with no extra flags.
This blog will be updated monthly — or whenever something significant ships.
Subscribe to the [GitHub releases](https://github.com/gesellix/Bose-SoundTouch/releases)
for individual version notes.
+2
View File
@@ -1,3 +1,5 @@
---
title: News & Updates
---
Project updates, release notes, and development notes for AfterTouch — the local replacement for the Bose SoundTouch cloud.
@@ -1,9 +1,6 @@
---
title: "Placeholder values for examples"
---
# Placeholder values for examples
This repo is public. Documentation, READMEs, example configs, and test
fixtures must never carry real LAN IPs, real device MACs, real Bose
account IDs, or personal device names from any maintainer or
@@ -1,9 +1,6 @@
---
title: "Bose SoundTouch API Coverage Analysis"
---
# Bose SoundTouch API Coverage Analysis
**Last Updated:** February 2026
**API Version:** Official Bose SoundTouch Web API v1.0
**Implementation Status:** 100% Official Coverage + Extended Features
@@ -1,9 +1,6 @@
---
title: "Bose SoundTouch Traffic Interception Runbook"
---
# Bose SoundTouch Traffic Interception Runbook
Intercept HTTPS/WebSocket traffic from the Bose SoundTouch Android app using an Android emulator, mitmproxy, and Frida. Tested on Apple Silicon (ARM64) Mac.
## Automated Setup
@@ -1,9 +1,6 @@
---
title: "Bose SoundTouch Traffic Analysis Runbook"
---
# Bose SoundTouch Traffic Analysis Runbook
> **Goal:** Set up a Raspberry Pi as a transparent access point to fully observe the traffic of the Bose SoundTouch app specifically the pairing flow with the Bose Cloud. This serves as a basis for later reverse engineering / simulation of the cloud endpoints.
---
@@ -1,9 +1,6 @@
---
title: "Device Redirect Methods & Custom Service Setup"
---
# Device Redirect Methods & Custom Service Setup
To enable offline operation or use custom services like **SoundCork** or **ÜberBöse API**, SoundTouch devices must be redirected from Bose's official cloud endpoints to a local or custom server. This document outlines the three known methods to achieve this, gathered from community reverse-engineering efforts in the **SoundCork** and **ÜberBöse API** projects.
> A fourth, **SSH-free** path — driving the device's diagnostic shell on TCP port 17000 — is being added as a peer to the XML and DNS methods. See **[TELNET-MIGRATION-METHOD.md](TELNET-MIGRATION-METHOD.md)** for the use cases, community findings, and feasibility analysis. The `/etc/hosts` method documented below is now deprecated and will not be exposed in the web UI.
@@ -1,9 +1,6 @@
---
title: "What a SoundTouch speaker does during factory reset"
---
# What a SoundTouch speaker does during factory reset
Observed live on ST10 firmware `27.0.6.46330.5043500` (build `epdbuild.trunk.hepdswbld04.2022-08-04`) on 2026-05-12, by running `soundtouch-cli setup factory-reset` and tailing the speaker's `logread` over SSH. The trace is preserved at `_/logs/factory-reset.txt` for reference.
## Sequence
@@ -1,9 +1,6 @@
---
title: "IoT Configuration Quick Reference"
---
# IoT Configuration Quick Reference
## Key Files and Locations
| File/Location | Purpose | Notes |
@@ -1,9 +1,6 @@
---
title: "IoT Configuration Analysis"
---
# IoT Configuration Analysis
## Overview
This document provides a detailed analysis of the AWS IoT configuration system used by Bose SoundTouch devices, based on firmware backup analysis from ST10 and ST20 models.
@@ -1,9 +1,6 @@
---
title: "Spotify Account Addition Implementation Status"
---
# Spotify Account Addition Implementation Status
To fully replace Bose cloud services for the Spotify account addition flow in the "Stockholm" SoundTouch application, the following routes have been implemented in the `soundtouch-service`:
## 1. OAuth Token Exchange (Bose Cloud)
@@ -1,9 +1,6 @@
---
title: "Experiment: Does bare `setMargeAccount` work outside the SETUP bracket?"
---
# Experiment: Does bare `setMargeAccount` work outside the SETUP bracket?
## Why we are doing this
Our captured pairing flow (`docs/reference/DEVICE-PAIRING-FLOW.md`) shows the official Bose app always sends `setMargeAccount` *inside* a `SETUP_START``SETUP_ENTER``SETUP_LEAVE` state-machine bracket over WebSocket. The question this experiment answers:
@@ -1,9 +1,6 @@
---
title: "SoundTouch supportedURLs Endpoint Analysis"
---
# SoundTouch supportedURLs Endpoint Analysis
This document provides a comprehensive analysis of the `/supportedURLs` endpoint response from real Bose SoundTouch devices and compares it with our current implementation.
## Discovery Summary
@@ -1,9 +1,6 @@
---
title: "Bose SoundTouch Telnet (Port 17000) Command Reference"
---
# Bose SoundTouch Telnet (Port 17000) Command Reference
A consolidated reference for the diagnostic shell that listens on TCP port
17000 across the SoundTouch line. Compiled from multiple community sources
to give a single map of what's been observed in the wild — useful both for
@@ -1,9 +1,6 @@
---
title: "Telnet (Port 17000) Migration Method — Analysis"
---
# Telnet (Port 17000) Migration Method — Analysis
This document captures the use cases, community findings, and feasibility analysis
for adding a **Telnet/port 17000** migration path to `soundtouch-service` as a
peer of the existing XML and DNS-based methods. The `/etc/hosts` method stays
@@ -12,7 +9,7 @@ deprecated and is intentionally kept off the visible UI options.
> **Sources** — community discussion synthesised from
> [gesellix/Bose-SoundTouch#221](https://github.com/gesellix/Bose-SoundTouch/issues/221),
> [gesellix/Bose-SoundTouch#236](https://github.com/gesellix/Bose-SoundTouch/issues/236),
> [scheilch/opencloudtouch#167](https://github.com/scheilch/opencloudtouch/issues/167),
> [scheilch/opencloudtouch#167](https://github.com/opencloudtouch/opencloudtouch/issues/167),
> [deborahgu/soundcork#228](https://github.com/deborahgu/soundcork/issues/228),
> [deborahgu/soundcork#141](https://github.com/deborahgu/soundcork/issues/141),
> the post-EOS walkthrough PDF in `docs/`,
@@ -1,9 +1,6 @@
---
title: "Upstream URLs & Domains Analysis"
---
# Upstream URLs & Domains Analysis
This document provides a comprehensive overview of the upstream Bose cloud services and domains that SoundTouch devices communicate with. These details were gathered from firmware analysis of ST10/ST20 devices, binary string extraction, and community research from the **SoundCork** project (Issue #128).
## Core Service Domains
@@ -1,9 +1,6 @@
---
title: "SoundTouch API Comparison: Community Wiki vs Current Implementation"
---
# SoundTouch API Comparison: Community Wiki vs Current Implementation
**Date:** January 2026
**Source:** [SoundTouch Plus Wiki](https://github.com/thlucas1/homeassistantcomponent_soundtouchplus/wiki/SoundTouch-WebServices-API)
**Our Implementation:** Bose-SoundTouch Go Library v1.0
@@ -1,9 +1,6 @@
---
title: "Bose SoundTouch — Community Tools for Post-EOL Preservation"
---
# Bose SoundTouch — Community Tools for Post-EOL Preservation
> **Context:** Bose announced the shutdown of SoundTouch cloud services, extended to **May 6, 2026**. On that date the official SoundTouch app will update to a local-only version. Bose has released the [SoundTouch Web API documentation](https://assets.bosecreative.com/m/496577402d128874/original/SoundTouch-Web-API.pdf) as open-source to enable community-driven development. This document surveys the active community projects, their feature coverage, and open development opportunities.
---
@@ -3,9 +3,6 @@ title: "Navigation API Reference"
sidebar:
exclude: true
---
# Navigation API Reference
## Overview
This document provides a complete API reference for the Bose SoundTouch navigation and station management functionality. For usage examples and workflows, see [NAVIGATION-GUIDE.md](NAVIGATION-GUIDE.md).
-3
View File
@@ -3,9 +3,6 @@ title: "CLAUDE.md - Development Guidelines for Bose SoundTouch Project"
sidebar:
exclude: true
---
# CLAUDE.md - Development Guidelines for Bose SoundTouch Project
## Documentation Overview
This document contains important development guidelines for working on the Bose SoundTouch project. Please also read the following documentation:
@@ -3,9 +3,6 @@ title: "Content Selection Implementation Summary"
sidebar:
exclude: true
---
# Content Selection Implementation Summary
This document summarizes the implementation of advanced content selection features for the Bose SoundTouch Go client, including full support for the LOCAL_INTERNET_RADIO streamUrl format and LOCAL_MUSIC/STORED_MUSIC content selection.
## ✅ Implementation Status: COMPLETE
@@ -3,9 +3,6 @@ title: "Device Customization Setup Guide"
sidebar:
exclude: true
---
# Device Customization Setup Guide
This guide documents the manual steps required to configure your Bose SoundTouch device for customization using the SoundCork approach.
Based on: https://github.com/deborahgu/soundcork
@@ -3,9 +3,6 @@ title: "Device Logging & Troubleshooting"
sidebar:
exclude: true
---
# Device Logging & Troubleshooting
Accessing logs from SoundTouch devices is critical for debugging custom service integrations and understanding internal device behavior. This document outlines the methods for collecting logs, as discovered by the **SoundCork** and **ÜberBöse API** communities.
## Log Types
@@ -3,9 +3,6 @@ title: "Bose SoundTouch Device Setup Flow"
sidebar:
exclude: true
---
# Bose SoundTouch Device Setup Flow
This document details the multi-step process required to fully set up a Bose SoundTouch device, as derived from the Stockholm firmware (`setup/js/`) analysis.
A complete setup flow involves a sequence of local (WebSocket) and cloud (HTTP) actions that move the device from a factory-reset state to a fully registered, functional system.
@@ -3,9 +3,6 @@ title: "Encrypted Diagnostic Export"
sidebar:
exclude: true
---
# Encrypted Diagnostic Export
AfterTouch can produce an encrypted diagnostic report that users can download and
send to the project maintainer without exposing sensitive data to third parties.
The report is encrypted with an SSH public key using
@@ -3,9 +3,6 @@ title: "Technical Proposal: External Service Provider Abstraction"
sidebar:
exclude: true
---
# Technical Proposal: External Service Provider Abstraction
This document outlines a strategy to refactor the SoundTouch Service's content handling into a modular provider-based system.
## 1. Problem Statement
@@ -3,9 +3,6 @@ title: "Feature Development History"
sidebar:
exclude: true
---
# Feature Development History
This document tracks the detailed evolution of features and capabilities in the Bose SoundTouch API client library.
## Development Timeline
@@ -3,9 +3,6 @@ title: "Host:Port Parsing Feature"
sidebar:
exclude: true
---
# Host:Port Parsing Feature
This document describes the automatic host:port parsing functionality added to the SoundTouch CLI, which allows users to specify both host and port in a single `-host` flag.
## Overview
@@ -3,9 +3,6 @@ title: "Manual Network Discovery on macOS"
sidebar:
exclude: true
---
# Manual Network Discovery on macOS
This document provides comprehensive guidance for manually discovering network services and devices using built-in macOS tools and command-line utilities. This is particularly useful for troubleshooting network discovery issues or understanding what services are available on your local network.
## Overview
@@ -3,9 +3,6 @@ title: "Navigation and Station Management Guide"
sidebar:
exclude: true
---
# Navigation and Station Management Guide
## Overview
The Bose SoundTouch Go client provides comprehensive navigation and station management functionality that allows you to:
@@ -3,9 +3,6 @@ title: "Official SoundTouch Web API Verification"
sidebar:
exclude: true
---
# Official SoundTouch Web API Verification
**Source**: Official Bose SoundTouch Web API v1.0 Documentation (January 7, 2026)
**Verification Date**: January 9, 2026
**Project Status**: Complete API coverage verification
@@ -3,9 +3,6 @@ title: "Parity Analysis: Bose-SoundTouch (Go) vs. OpenCloudTouch (Python)"
sidebar:
exclude: true
---
# Parity Analysis: Bose-SoundTouch (Go) vs. OpenCloudTouch (Python)
This document provides a comparative analysis of the current Go implementation and the `scheilch/opencloudtouch` project, identifying functional gaps and potential improvements.
## 1. Core Architecture and Language
@@ -3,9 +3,6 @@ title: "Parity Analysis: Bose-SoundTouch (Go) vs. SoundCork (Python)"
sidebar:
exclude: true
---
# Parity Analysis: Bose-SoundTouch (Go) vs. SoundCork (Python)
This document provides a comparative analysis of the current Go implementation and the `deborahgu/soundcork` project, identifying functional gaps and potential improvements.
## 1. Core Architecture and Language
@@ -3,9 +3,6 @@ title: "Preset Management Quick Start Guide"
sidebar:
exclude: true
---
# Preset Management Quick Start Guide
**Save your favorite music, radio stations, and playlists as 1-6 presets for instant access.**
## Overview
@@ -3,8 +3,6 @@ title: "Project Structure Patterns: Bose SoundTouch API Client"
sidebar:
exclude: true
---
# Project Structure Patterns: Bose SoundTouch API Client
## Summary for Reuse in API Client Projects
This document describes the most important patterns for the Bose SoundTouch API client, especially for XML-based API clients with Web UI, CLI tool, and WASM support.
@@ -3,9 +3,6 @@ title: "Request Recording Concept"
sidebar:
exclude: true
---
# Request Recording Concept
## Problem Statement
The current request recording system has fundamental issues when dealing with request cloning, body consumption, and multiple response scenarios. Specifically:
@@ -3,9 +3,6 @@ title: "SCMUDC Enrichment Implementation Summary"
sidebar:
exclude: true
---
# SCMUDC Enrichment Implementation Summary
## Overview
This document summarizes the implementation of SCMUDC (Sound Control Management Usage Data Collection) event enrichment in the AfterTouch toolkit. The enhancement provides human-readable analysis of device telemetry data to improve usability and debugging capabilities.
@@ -3,9 +3,6 @@ title: "Service Availability Implementation Summary"
sidebar:
exclude: true
---
# Service Availability Implementation Summary
## Overview
This document summarizes the implementation of the `/serviceAvailability` endpoint support in the Bose SoundTouch Go client library. This feature enables applications to query which music services and input sources are available on a SoundTouch device, providing better user feedback about supported stations and sources.
@@ -3,9 +3,6 @@ title: "🎉 Introducing SoundTouch Service: Local Cloud Service Emulation"
sidebar:
exclude: true
---
# 🎉 Introducing SoundTouch Service: Local Cloud Service Emulation
**Date**: February 2026
**Version**: v2.0.0+
**Status**: Production Ready
@@ -3,8 +3,6 @@ title: "Undocumented Community Features & API Discoveries"
sidebar:
exclude: true
---
# Undocumented Community Features & API Discoveries
This document captures advanced API endpoints and device behaviors discovered by the SoundTouch community through reverse engineering projects like **SoundCork** and **ÜberBöse API**. These features are not documented in the official Bose SoundTouch Web API v1.0 but are crucial for full device emulation and offline operation.
## Cloud Emulation (Marge/BMX) Discoveries
While the local `/8090` API is well-documented, the cloud-side service emulation reveals deeper device integration points.
@@ -3,9 +3,6 @@ title: "Unimplemented SoundTouch API Endpoints"
sidebar:
exclude: true
---
# Unimplemented SoundTouch API Endpoints
**Last Updated:** January 2026
**Source:** [SoundTouch Plus Wiki](https://github.com/thlucas1/homeassistantcomponent_soundtouchplus/wiki/SoundTouch-WebServices-API)
**Current Implementation:** 35 endpoints (including preset & navigation management discovered via SoundTouch Plus Wiki)
@@ -3,9 +3,6 @@ title: "Device Lifecycle and /power_on Enhancement"
sidebar:
exclude: true
---
# Device Lifecycle and /power_on Enhancement
## Overview
This document provides a comprehensive analysis of the current SoundTouch device registration and lifecycle management implementation, and proposes enhancements using the `/power_on` endpoint to reduce dependency on local network connectivity.
@@ -3,9 +3,6 @@ title: "Device Lifecycle Analysis - Executive Summary"
sidebar:
exclude: true
---
# Device Lifecycle Analysis - Executive Summary
## Current State Assessment
The SoundTouch service currently relies heavily on local network connectivity for device discovery and management:
@@ -3,9 +3,6 @@ title: "/power_on Implementation Guide"
sidebar:
exclude: true
---
# /power_on Implementation Guide
## Overview
This guide provides detailed technical specifications for implementing `/power_on` endpoint enhancements to reduce network dependency and improve device lifecycle management in the SoundTouch service.
@@ -3,9 +3,6 @@ title: "SoundTouch `/storePreset` Implementation Guide"
sidebar:
exclude: true
---
# SoundTouch `/storePreset` Implementation Guide
## Overview
This document analyzes the feasibility and implementation approach for adding `/storePreset` functionality to the Bose SoundTouch API client, based on [GitHub Issue #14](https://github.com/gesellix/Bose-SoundTouch/issues/14) and endpoints discovered through the comprehensive [SoundTouch Plus Wiki](https://github.com/thlucas1/homeassistantcomponent_soundtouchplus/wiki/SoundTouch-WebServices-API).
@@ -3,9 +3,6 @@ title: "SCMUDC Events Analysis"
sidebar:
exclude: true
---
# SCMUDC Events Analysis
## Overview
SCMUDC (Sound Control Management Usage Data Collection) events are telemetry data sent from SoundTouch devices to `events.api.bosecm.com` via `/v1/scmudc/{deviceId}` endpoints. These events track user interactions and device behaviors for analytics and monitoring.
@@ -3,9 +3,6 @@ title: "soundtouch-web: remaining features"
sidebar:
exclude: true
---
# soundtouch-web: remaining features
Four features complete the parity gap between soundtouch-web and the Stockholm
app's local-control functionality. Everything else in Stockholm (OAuth flows,
setup wizard, service account linking, onboarding, analytics) is cloud
@@ -3,9 +3,6 @@ title: "Stockholm Backend — Port Guide for Bose-SoundTouch (Go)"
sidebar:
exclude: true
---
# Stockholm Backend — Port Guide for Bose-SoundTouch (Go)
This document describes everything needed to integrate the
[krahl/soundcork-stockholm-app](https://github.com/krahl/soundcork-stockholm-app)
functionality into the Go service. It is written as a reference; nothing here
@@ -1,9 +1,6 @@
---
title: "Device-Local Install: Four User Journeys"
---
# Device-Local Install: Four User Journeys
> **Looking for how to actually install AfterTouch?**
> See the [Deployment Overview](../guides/DEPLOYMENT-OVERVIEW.md) for user-friendly
> step-by-step guides for both deployment scenarios (external host and on-device).
@@ -1,9 +1,6 @@
---
title: "Encrypting Sensitive Data Exports with SSH/age or GPG"
---
# Encrypting Sensitive Data Exports with SSH/age or GPG
## Problem
Allow users of our software to export potentially sensitive data, encrypt it locally, and send it to us. We decrypt on our side. Goal: no key exchange, minimal user friction.
@@ -1,9 +1,6 @@
---
title: "Amazon Music OAuth Integration"
---
# Amazon Music OAuth Integration
This document describes the plan and specification for adding Amazon Music OAuth support to the SoundTouch service, enabling continued Amazon Music playback after the Bose cloud shutdown (May 2026).
The implementation mirrors the [Spotify OAuth integration](spotify-oauth.md) closely. Read that document first — this one calls out only the differences.
@@ -1,9 +1,6 @@
---
title: "Spotify OAuth Integration"
---
# Spotify OAuth Integration
> **New here?** Start with [spotify-overview.md](spotify-overview.md) for the
> mental model (Spotify Connect vs OAuth-intercept, DNS rewrite gotcha,
> end-to-end token lifecycle). This document zooms in on the OAuth flows and
@@ -1,9 +1,6 @@
---
title: "Spotify on SoundTouch — Overview"
---
# Spotify on SoundTouch — Overview
This is the entry point for understanding how Spotify works on a SoundTouch
speaker behind AfterTouch. Read this first; the deeper docs assume you already
have the mental model below.
@@ -1,9 +1,6 @@
---
title: "Spotify Priming Strategy"
---
# Spotify Priming Strategy
> **New here?** Start with [spotify-overview.md](spotify-overview.md) for the
> mental model. This document goes deep on the priming protocol, ZeroConf DH
> exchange, and deployment topologies.
@@ -1,9 +1,6 @@
---
title: "Migration Flow Diagrams"
---
# Migration Flow Diagrams
This document specifies the diagrams needed for the migration guide, with descriptions that can be used to create actual visual diagrams.
## 1. Overall Migration Process Flow
@@ -1,9 +1,6 @@
---
title: "Capture Device Pairing Traffic"
---
# Capture Device Pairing Traffic
Step-by-step runbook for factory-resetting a SoundTouch speaker, pairing it to a Bose cloud account, and capturing every cloud request via mitmproxy. Tested on Apple Silicon Mac.
**Goal:** obtain a full `.mitm` recording of the account-pairing flow (streaming.bose.com) triggered by the official Android app.
@@ -1,9 +1,6 @@
---
title: "Capture Speaker Migration Traffic"
---
# Capture Speaker Migration Traffic
Runbook for migrating a SoundTouch speaker to `soundtouch-service` and capturing
all traffic (App→Service and Speaker→Service) to identify unimplemented endpoints.
@@ -1,9 +1,6 @@
---
title: "SoundTouch CLI Reference"
---
# SoundTouch CLI Reference
**Complete command reference for the soundtouch-cli tool**
This document provides comprehensive documentation for all available commands and options in the `soundtouch-cli` tool.
@@ -1,9 +1,6 @@
---
title: "Cloud Deployment Walkthrough"
---
# Cloud Deployment Walkthrough
A step-by-step guide to running AfterTouch on a VPS or cloud server and
pointing your local Bose SoundTouch speakers at it.
@@ -1,9 +1,6 @@
---
title: "AfterTouch Deployment Overview"
---
# AfterTouch Deployment Overview
AfterTouch replaces the Bose SoundTouch cloud, which shut down on 2026-05-06. There are
three ways to run it — pick the one that fits your situation.
-3
View File
@@ -1,9 +1,6 @@
---
title: "SoundTouch Production Deployment Guide"
---
# SoundTouch Production Deployment Guide
**Best practices for deploying SoundTouch Go applications in production environments**
This guide covers everything you need to know to deploy robust, scalable SoundTouch applications in production, including configuration management, monitoring, security, and operational considerations.
@@ -1,9 +1,6 @@
---
title: "SoundTouch Device Initial Setup Variants"
---
# SoundTouch Device Initial Setup Variants
Based on community research from the **SoundCork** and **ÜberBöse API** projects, as well as analysis of the Stockholm firmware (`firmware/Stockholm/.../setup/`), this document outlines the methods used for the "out-of-the-box" setup of SoundTouch devices.
## Setup Overview
@@ -1,9 +1,6 @@
---
title: "External Host Walkthrough"
---
# External Host Walkthrough
A step-by-step guide to running AfterTouch on a Raspberry Pi (or any always-on
computer) and migrating your Bose SoundTouch speakers to use it.
@@ -43,7 +40,7 @@ starts on boot.
To install a specific version:
```bash
sudo bash install.sh v0.92.0
sudo bash install.sh v0.93.1
```
Check that the service is running:
@@ -219,7 +216,7 @@ curl -s http://192.0.2.1:8090/presets
```bash
sudo bash install.sh # updates to latest release
sudo bash install.sh v0.93.0 # updates to a specific version
sudo bash install.sh v0.93.1 # updates to a specific version
```
The installer stops the service, downloads the new binary, and restarts
@@ -1,9 +1,6 @@
---
title: "Getting Started with SoundTouch Go Client"
---
# Getting Started with SoundTouch Go Client
**A complete guide to controlling your Bose SoundTouch devices with Go**
This guide will get you up and running with the SoundTouch Go client in under 10 minutes. By the end, you'll be able to discover devices, control playback, manage volume, and monitor real-time events.
-3
View File
@@ -1,9 +1,6 @@
---
title: "HTTPS & Custom CA Certificate"
---
# HTTPS & Custom CA Certificate
SoundTouch speakers communicate with cloud services over HTTPS. For the local service to work over HTTPS, speakers must trust the AfterTouch Root CA. The service manages this automatically — it generates a CA on first start and the web UI guides you through installing it on each speaker as part of the migration flow.
> ### ⚠️ Speakers connect to `:443`, AfterTouch defaults to `:8443`
@@ -1,9 +1,6 @@
---
title: "IoT Implementation Guide"
---
# IoT Implementation Guide
## Overview
This guide provides technical implementation details for integrating with the Bose SoundTouch IoT configuration system. It covers the AWS IoT Core integration, certificate management, and device shadow operations.
@@ -1,9 +1,6 @@
---
title: "MAC Address to Serial Number Mapping"
---
# MAC Address to Serial Number Mapping
**Understanding and troubleshooting device identification in SoundTouch service**
This guide explains how the SoundTouch service handles device identification through MAC address to serial number mapping, and how to troubleshoot related issues.
+34 -6
View File
@@ -1,9 +1,6 @@
---
title: "Migration Guide: From Bose Cloud to AfterTouch"
---
# Migration Guide: From Bose Cloud to AfterTouch
This guide walks through the complete process of migrating your SoundTouch speakers from Bose's cloud services to **AfterTouch**, the replacement provided by `soundtouch-service`. By the end, your speakers will work fully independently of Bose's servers.
For a shorter overview, see the [Survival Guide](SURVIVAL-GUIDE.md). For safety considerations and rollback options, see the [Migration & Safety Guide](MIGRATION-SAFETY.md).
@@ -23,15 +20,28 @@ For a shorter overview, see the [Survival Guide](SURVIVAL-GUIDE.md). For safety
Choose the option that fits your setup.
### Binary (go install)
### Download a pre-built binary (no Go required)
Download the latest release for your platform from the
[GitHub releases page](https://github.com/gesellix/Bose-SoundTouch/releases).
Unzip, make executable, and run:
```bash
go install github.com/gesellix/bose-soundtouch/cmd/soundtouch-service@latest
soundtouch-service
# Linux / macOS example
chmod +x soundtouch-service
./soundtouch-service
```
The service starts on port 8000. Open `http://localhost:8000` in your browser.
### Install script (Raspberry Pi or on-device)
A one-line installer handles download, installation as a system service, and
auto-start on boot. See the
[On-Device Install Walkthrough](ON-DEVICE-INSTALL-WALKTHROUGH.md) and
[External Host Walkthrough](EXTERNAL-HOST-WALKTHROUGH.md) for the exact
commands for each platform.
### Docker Compose (recommended for home servers and VMs)
The repository ships a `docker-compose.yml` ready for this use case. Clone or download it, copy the example config, then edit `.env` before starting:
@@ -76,8 +86,17 @@ docker run -d \
On macOS/Windows, device discovery via mDNS won't work inside the container — you'll add devices by IP address in Step 4.
### go install (if you have Go installed)
```bash
go install github.com/gesellix/bose-soundtouch/cmd/soundtouch-service@latest
soundtouch-service
```
See [Raspberry Pi Setup](RASPBERRY-PI.md) and the [SoundTouch Service Guide](SOUNDTOUCH-SERVICE.md) for more deployment options.
> **Data directory**: all methods store device state, presets, and settings in a `data/` directory next to the binary (or mounted at `/app/data` in Docker). This directory is the single thing you need to back up — copying it is enough to restore a complete AfterTouch installation on a new machine.
---
## Step 2: Configure the service URL
@@ -110,6 +129,15 @@ The XML migration writes updated configuration to the speaker's filesystem, whic
You only need to do this once per speaker. SSH can remain enabled for future maintenance or be disabled after migration — your choice.
**To disable SSH after migration:**
- **USB stick only (no persistent file written):** remove the stick and reboot the speaker — SSH will not be available after the next boot.
- **Persistent `remote_services` file** (written automatically during XML migration): remove it via the admin UI → *Migrate* tab → **Disable SSH (Remove remote_services)** button, then reboot. Or via the CLI:
```shell
soundtouch-cli --host <SPEAKER-IP> setup remote-services --remove
```
Then reboot the speaker for the change to take effect.
### Telnet:17000 (fallback when SSH isn't possible)
If the USB-stick unlock doesn't work on your speaker (some firmware revisions refuse it — notably SA-5, ST520, and recent ST Portables), the wizard falls back to the speaker's **built-in diagnostic shell on TCP port 17000**. No setup required — most SoundTouch firmware exposes it automatically. The wizard detects which transports are available and picks the right one; you don't have to choose manually.
@@ -1,9 +1,6 @@
---
title: "Migration & Safety Guide"
---
# Migration & Safety Guide
Starting a migration on real hardware requires a "Safety First" approach. This guide outlines the safety features implemented in the `soundtouch-service` and provides a checklist for a successful migration.
#### 🛠 Technical Safety Enhancements
@@ -1,9 +1,6 @@
---
title: "MQTT Integration Design for SoundTouch Service"
---
# MQTT Integration Design for SoundTouch Service
## Overview
This document outlines the design for integrating MQTT support into the existing SoundTouch service to simulate AWS IoT Core functionality. The integration will provide real-time device communication, shadow state management, and prepare for the AWS IoT service shutdown in May 2026.
@@ -1,9 +1,6 @@
---
title: "Connecting Music Services (Spotify & Amazon Music)"
---
# Connecting Music Services (Spotify & Amazon Music)
This guide explains how to link your Spotify or Amazon Music account to AfterTouch so your speakers can stream music from those services.
> For Spotify, a higher-level mental model of how the integration works —
@@ -1,9 +1,6 @@
---
title: "On-Device Install Walkthrough"
---
# On-Device Install Walkthrough
A complete end-to-end runbook for installing AfterTouch directly on a
Bose SoundTouch speaker — from first SSH connection through verified
radio preset playback.
@@ -103,7 +100,7 @@ Verify the installed version:
wget -qO- http://localhost:8000/health
```
The JSON response should include `"version":"v0.92.0"` (or whichever
The JSON response should include `"version":"v0.93.1"` (or whichever
version you installed).
---
@@ -191,13 +188,13 @@ next reboot — which is fine for a one-time setup run):
cd /tmp
curl -L --fail -o soundtouch-cli \
https://github.com/gesellix/Bose-SoundTouch/releases/download/v0.92.0/soundtouch-cli-v0.92.0-linux-armv7
https://github.com/gesellix/Bose-SoundTouch/releases/download/v0.93.1/soundtouch-cli-v0.93.1-linux-armv7
chmod +x soundtouch-cli
/tmp/soundtouch-cli --version
```
Replace `v0.92.0` with the version you installed.
Replace `v0.93.1` with the version you installed.
---
+2 -5
View File
@@ -1,9 +1,6 @@
---
title: "Raspberry Pi Installation Guide"
---
# Raspberry Pi Installation Guide
This guide explains how to install the `soundtouch-service` as a persistent systemd service on a Raspberry Pi (tested on Raspberry Pi Zero 2W, 3, and 4).
For a complete walkthrough — from install through speaker migration and preset setup — see
@@ -39,7 +36,7 @@ You can customize the installation using environment variables:
```bash
sudo \
VERSION=v0.92.0 \
VERSION=v0.93.1 \
HOSTNAME_FQDN=soundtouch.local \
HTTP_PORT=80 \
HTTPS_PORT=443 \
@@ -51,7 +48,7 @@ sudo \
To update the service to a specific version, run the installer with the version as an argument:
```bash
sudo bash install.sh v0.92.0
sudo bash install.sh v0.93.1
```
The installer will automatically fetch the latest version of itself for that release and then update the service binary and restart it.
-3
View File
@@ -1,9 +1,6 @@
---
title: "Self-Hosting AfterTouch"
---
# Self-Hosting AfterTouch
This guide walks you through running AfterTouch on your own computer or server. No programming knowledge required.
---
@@ -1,9 +1,6 @@
---
title: "SoundTouch Service"
---
# SoundTouch Service
The `soundtouch-service` is a comprehensive local server that emulates Bose's cloud services, enabling offline SoundTouch device operation and advanced debugging capabilities. This service is particularly valuable given Bose's announcement that cloud support will end in May 2026.
## Overview
@@ -28,10 +25,13 @@ The service consists of several key components:
### BMX Services (Bose Media eXchange)
- **TuneIn Integration**: Direct playback of radio stations and podcasts
- **RadioBrowser Integration**: Open community radio directory alongside TuneIn
- **Custom Streams**: Flexible playback of any internet radio URL via dynamic proxy
- **Service Registry**: Media service discovery and configuration
- **Playback Control**: Stream URL resolution and audio metadata
![soundtouch-web TuneIn search — browsing smooth jazz stations](/images/soundtouch-web-tunein.png)
### Marge Services (Account & Device Management)
- **Account Management**: User account simulation and device association
- **Preset Synchronization**: Cross-device preset storage and sync
@@ -1,9 +1,6 @@
---
title: "Keeping Your Speakers Alive After the Bose Cloud Shutdown"
---
# Keeping Your Speakers Alive After the Bose Cloud Shutdown
Bose shut down SoundTouch cloud services on **May 6, 2026**. Per the [official end-of-life page](https://www.bose.com/soundtouch-end-of-life), the following no longer work:
- **Presets** — preset buttons on the product and in the app
@@ -1,9 +1,6 @@
---
title: "SoundTouch Troubleshooting Guide"
---
# SoundTouch Troubleshooting Guide
**Complete guide to diagnosing and fixing common SoundTouch Go client issues**
This guide helps you quickly identify and resolve problems with the SoundTouch Go client library. Issues are organized by category with step-by-step solutions.
@@ -1,9 +1,6 @@
---
title: "SoundTouch API Cookbook"
---
# SoundTouch API Cookbook
**Real-world patterns, recipes, and best practices for the SoundTouch Go client**
This cookbook provides practical solutions to common SoundTouch integration challenges. Each recipe includes working code, error handling, and production considerations.

Some files were not shown because too many files have changed in this diff Show More