mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-24 14:47:23 +00:00
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>
117 lines
5.0 KiB
JavaScript
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>
|
|
`;
|
|
}
|