mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-19 00:56:16 +00:00
The merge of soundtouch-web into soundtouch-service was asymmetric:
manual device *adds* propagated to the player UI (HandleAddManualDevice
notifies, the hook re-seeds + broadcasts), but *removals* did not. The
datastore-removal handler never notified, and the web registry's sync
only ever added entries — its map was append-only, so a removed device
lingered in the player UI until restart.
This adds the missing removal path:
- DELETE /api/control/devices/{id} (HandleDeleteDevice). The registry is
keyed by host/IP; the datastore by device ID (MAC), so the handler
resolves one to the other via the connection's DeviceInfo, cascades to
the datastore through a new RemoveDeviceHook (embedded build only),
prunes the in-memory entry, and broadcasts the updated list.
- WebApp.RemoveDevice prunes the registry and stops the per-device
goroutines (status poller + WebSocket reconnect loop) via a new
done-channel + Close() on DeviceConnection — previously both ran for
the life of the process.
- Server.RemoveDeviceByID extracts the cross-account lookup + remove from
HandleRemoveDevice and now fires notifyDevicesChanged, so the admin
Devices tab removal also propagates to the player UI.
- Player UI: a quiet per-card Remove control (visible on hover), a
confirm dialog, optimistic prune, and a note that a still-online
device may reappear after the next discovery scan (honest v1 — no
ignore-list).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
60 lines
3.2 KiB
JavaScript
60 lines
3.2 KiB
JavaScript
const JSON_HEADERS = { 'Content-Type': 'application/json' };
|
|
|
|
async function req(url, opts = {}) {
|
|
const r = await fetch(url, opts);
|
|
return r.json();
|
|
}
|
|
|
|
export const api = {
|
|
devices: () => req('/api/control/devices'),
|
|
device: (id) => req(`/api/control/devices/${id}`),
|
|
removeDevice: (id) => req(`/api/control/devices/${id}`, { method: 'DELETE' }),
|
|
discover: () => req('/api/control/discover', { method: 'POST' }),
|
|
key: (id, key) => req(`/api/control/devices/${id}/key/${key}`, { method: 'POST' }),
|
|
volume: (id, level) => req(`/api/control/devices/${id}/volume/${level}`, { method: 'POST' }),
|
|
bass: (id, level) => req(`/api/control/devices/${id}/action/bass`, {
|
|
method: 'POST',
|
|
headers: JSON_HEADERS,
|
|
body: JSON.stringify({ level }),
|
|
}),
|
|
power: (id) => req(`/api/control/devices/${id}/power`, { method: 'POST' }),
|
|
recents: (id) => req(`/api/control/devices/${id}/recents`),
|
|
zone: (id) => req(`/api/control/devices/${id}/zone`),
|
|
zoneAdd: (masterId, slaveId) => req(`/api/control/devices/${masterId}/zone/add/${slaveId}`, { method: 'POST' }),
|
|
zoneRemove: (masterId, slaveId) => req(`/api/control/devices/${masterId}/zone/remove/${slaveId}`, { method: 'POST' }),
|
|
zoneDissolve: (id) => req(`/api/control/devices/${id}/zone/dissolve`, { method: 'POST' }),
|
|
zoneLeave: (id) => req(`/api/control/devices/${id}/zone/leave`, { method: 'POST' }),
|
|
play: (id, item) => req(`/api/control/devices/${id}/play`, {
|
|
method: 'POST',
|
|
headers: JSON_HEADERS,
|
|
body: JSON.stringify(item),
|
|
}),
|
|
tuneInBrowse: (path) => req(path ? `/api/control/providers/tunein/navigate/${path}` : '/api/control/providers/tunein/navigate'),
|
|
tuneInSearch: (q) => req(`/api/control/providers/tunein/search?q=${encodeURIComponent(q)}`),
|
|
tuneInSearchNext: (cursor) => req(`/api/control/providers/tunein/search/next?cursor=${encodeURIComponent(cursor)}`),
|
|
control: (id, action, presetId) => req(`/api/control/devices/${id}/action/${action}?id=${presetId}`),
|
|
storePreset: (id, slotId) => req(`/api/control/devices/${id}/action/storepreset?id=${slotId}`),
|
|
selectSource: (id, source, account) => req(`/api/control/devices/${id}/action/source?name=${encodeURIComponent(source)}&account=${encodeURIComponent(account || '')}`),
|
|
tuneInPlay: (deviceId, item) => req(`/api/control/devices/${deviceId}/providers/tunein/play`, {
|
|
method: 'POST',
|
|
headers: JSON_HEADERS,
|
|
body: JSON.stringify(item),
|
|
}),
|
|
radioBrowserSearch: (q) => req(`/api/control/providers/radiobrowser/search?q=${encodeURIComponent(q)}`),
|
|
radioBrowserPlay: (deviceId, item) => req(`/api/control/devices/${deviceId}/providers/radiobrowser/play`, {
|
|
method: 'POST',
|
|
headers: JSON_HEADERS,
|
|
body: JSON.stringify(item),
|
|
}),
|
|
playURL: (deviceId, url, name, imageUrl, serviceUrl) => req(`/api/control/devices/${deviceId}/providers/url/play`, {
|
|
method: 'POST',
|
|
headers: JSON_HEADERS,
|
|
body: JSON.stringify({ url, name, imageUrl, serviceUrl }),
|
|
}),
|
|
speak: (deviceId, text) => req(`/api/control/devices/${deviceId}/providers/tts/play`, {
|
|
method: 'POST',
|
|
headers: JSON_HEADERS,
|
|
body: JSON.stringify({ text }),
|
|
}),
|
|
};
|