Files
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

97 lines
4.5 KiB
JavaScript

import { h } from 'preact';
import { useState } from 'preact/hooks';
import htm from 'htm';
import { api } from '../api.js';
const html = htm.bind(h);
// TTS is a "source" view (like PlayURL / TuneIn / RadioBrowser): enter text,
// pick a device, and the AfterTouch service synthesizes and plays it. Synthesis,
// credentials, and the service URL all live server-side — soundtouch-player proxies
// to the service it was started with (--service-url). The service URL is shown
// read-only: unlike Play URL (whose URL is handed to the speaker), here
// soundtouch-player makes the request itself, so a browser-supplied URL would be an
// open SSRF proxy.
export function TTS({ devices, serverServiceUrl }) {
const [text, setText] = useState('');
const [pendingSpeak, setPendingSpeak] = useState(null);
const [status, setStatus] = useState(null);
function startSpeak() {
const trimmed = text.trim();
if (!trimmed) return;
setStatus(null);
setPendingSpeak({ text: trimmed });
}
async function speakOn(deviceId) {
const item = pendingSpeak;
setPendingSpeak(null);
setStatus('Speaking…');
try {
const resp = await api.speak(deviceId, item.text);
setStatus(resp.success ? 'Speaking' : 'Error: ' + (resp.error || 'Unknown error'));
} catch (e) {
setStatus('Error: ' + e.message);
}
}
const deviceEntries = Object.entries(devices);
return html`
<div class="tunein-browser">
<div class="tunein-toolbar">
<input
type="text"
class="tunein-search-input"
placeholder="Say something…"
value=${text}
onInput=${(e) => setText(e.target.value)}
onKeyDown=${(e) => e.key === 'Enter' && startSpeak()}
/>
<button class="btn-primary" onClick=${startSpeak} disabled=${!text.trim() || !serverServiceUrl}>🔊 Speak</button>
</div>
<div class="tunein-toolbar" style="margin-top:.4rem">
<input
type="url"
class="tunein-search-input"
value=${serverServiceUrl || ''}
placeholder="(not configured — start soundtouch-player with --service-url)"
readonly
title="AfterTouch service URL — set server-side via --service-url"
/>
</div>
<div class="track-meta" style="margin-top:.4rem; opacity:.85">
${serverServiceUrl
? html`Synthesized by the AfterTouch service (Settings → Integrations) and played on the speaker.`
: html`<strong>TTS is unavailable:</strong> start soundtouch-player with <code>--service-url</code>.`}
<br/>
The service URL is fixed server-side and can't be edited here: soundtouch-player
makes the request itself, so a browser-supplied URL would let anyone use it as
an SSRF proxy.
</div>
${status && html`<div class="track-meta" style="margin-top:.6rem">${status}</div>`}
${pendingSpeak ? html`
<div class="overlay" onClick=${() => setPendingSpeak(null)}>
<div class="device-picker" onClick=${(e) => e.stopPropagation()}>
<h3 class="picker-title">Speak on device</h3>
<p class="picker-item-name">${pendingSpeak.text}</p>
<div class="picker-devices">
${deviceEntries.length === 0 ? html`<p class="picker-no-devices">No devices found. Try discovering first.</p>` : null}
${deviceEntries.map(([id, d]) => html`
<button class="picker-device-btn" key=${id} onClick=${() => speakOn(id)}>
<div class="picker-device-info">
<span class="picker-device-name">${d.info?.name || id}</span>
<span class="picker-device-ip">${d.info?.ip_address || ''}</span>
</div>
</button>
`)}
</div>
<button class="btn-secondary picker-cancel" onClick=${() => setPendingSpeak(null)}>Cancel</button>
</div>
</div>
` : null}
</div>
`;
}