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

117 lines
5.0 KiB
JavaScript

import { h } from 'preact';
import { useState, useEffect } from 'preact/hooks';
import htm from 'htm';
import { api } from '../api.js';
const html = htm.bind(h);
const LS_KEY = 'aftertouch_service_url';
export function PlayURL({ devices, serverServiceUrl }) {
const [url, setUrl] = useState('');
const [name, setName] = useState('');
const [serviceUrl, setServiceUrl] = useState(() => localStorage.getItem(LS_KEY) || '');
const [pendingPlay, setPendingPlay] = useState(null);
const [status, setStatus] = useState(null);
useEffect(() => {
if (serverServiceUrl && !localStorage.getItem(LS_KEY)) {
setServiceUrl(serverServiceUrl);
}
}, [serverServiceUrl]);
function onServiceUrlChange(val) {
setServiceUrl(val);
if (val) {
localStorage.setItem(LS_KEY, val);
} else {
localStorage.removeItem(LS_KEY);
}
}
function startPlay() {
const trimmedUrl = url.trim();
if (!trimmedUrl) return;
setStatus(null);
setPendingPlay({ url: trimmedUrl, name: name.trim() || trimmedUrl });
}
async function playOn(deviceId) {
const item = pendingPlay;
setPendingPlay(null);
setStatus('Playing…');
try {
// When configured server-side, that value wins; send it so a stale
// localStorage override never matters.
const effectiveServiceUrl = serverServiceUrl || serviceUrl.trim();
const resp = await api.playURL(deviceId, item.url, item.name, '', effectiveServiceUrl);
setStatus(resp.success ? 'Playing — use ★ on the device page to save as preset' : '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="url"
class="tunein-search-input"
placeholder="Stream URL (http://…)"
value=${url}
onInput=${(e) => setUrl(e.target.value)}
onKeyDown=${(e) => e.key === 'Enter' && startPlay()}
/>
<input
type="text"
class="tunein-search-input"
placeholder="Name (optional)"
value=${name}
style="max-width:160px"
onInput=${(e) => setName(e.target.value)}
onKeyDown=${(e) => e.key === 'Enter' && startPlay()}
/>
<button class="btn-primary" onClick=${startPlay} disabled=${!url.trim()}>▶ Play</button>
</div>
<div class="tunein-toolbar" style="margin-top:.4rem">
<input
type="url"
class="tunein-search-input"
placeholder="AfterTouch URL (https://…)"
value=${serverServiceUrl || serviceUrl}
onInput=${(e) => onServiceUrlChange(e.target.value)}
readonly=${!!serverServiceUrl}
title="AfterTouch service base URL — required for LOCAL_INTERNET_RADIO playback and preset save"
/>
</div>
${serverServiceUrl
? html`<div class="track-meta" style="margin-top:.2rem; opacity:.85">Configured server-side (soundtouch-player --service-url); edits here would be ignored.</div>`
: null}
${status && html`<div class="track-meta" style="margin-top:.6rem">${status}</div>`}
${pendingPlay ? html`
<div class="overlay" onClick=${() => setPendingPlay(null)}>
<div class="device-picker" onClick=${(e) => e.stopPropagation()}>
<h3 class="picker-title">Play on device</h3>
<p class="picker-item-name">${pendingPlay.name}</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=${() => playOn(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=${() => setPendingPlay(null)}>Cancel</button>
</div>
</div>
` : null}
</div>
`;
}