Volume: quieter/louder buttons that take the remote's own step
Next to the five volume bars, a − and a + button move the volume by
`volume_increment` device units, the same step as the IR remote's volume keys and
the rotary encoder, via a new `{steps}` form of POST /api/volume. The bars still
jump to a percentage.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -216,7 +216,7 @@ copied to the device.
|
||||
| `WS /api/ws` | Push only. A snapshot on connect, then a frame per change, plus the position at 2 Hz while playing. |
|
||||
| `POST /api/play` | `{album_id, track_index?}` |
|
||||
| `POST /api/resume` `/pause` `/next` `/previous` `/seek` | transport |
|
||||
| `POST /api/volume` | `{percent}` or `{delta_percent}` |
|
||||
| `POST /api/volume` | `{percent}`, `{delta_percent}`, or `{steps}` - the IR remote's step (`volume_increment` device units) per step |
|
||||
| `POST /api/remote/press/{digit}` | what the IR remote's number key does - the keyboard's digits |
|
||||
| `GET` `PUT /api/remote/mapping` | number key -> album (optionally from one `track_index`) or podcast show |
|
||||
| `POST /api/screen/toggle` `/screen/activity` | the kiosk's Del key; its key presses and taps, holding off the idle screen-off |
|
||||
|
||||
@@ -33,6 +33,7 @@ from musicmouse.events import (
|
||||
SetVolumeRequested,
|
||||
ToggleScreenRequested,
|
||||
UserActivity,
|
||||
VolumeChangeRequested,
|
||||
)
|
||||
from musicmouse.services.web.hub import StateHub
|
||||
from musicmouse.services.web.remote_settings import read_mapping, write_mapping
|
||||
@@ -212,12 +213,19 @@ def build_router(
|
||||
@router.post("/volume", status_code=204)
|
||||
def set_volume(body: VolumeIn) -> Response:
|
||||
general = app.config.general
|
||||
if body.steps is not None:
|
||||
# The IR remote's own step, in device units - see LircService._handle.
|
||||
return emit(
|
||||
VolumeChangeRequested(delta=general.volume_increment * body.steps, source="web")
|
||||
)
|
||||
if body.percent is not None:
|
||||
target = body.percent
|
||||
elif body.delta_percent is not None:
|
||||
target = to_percent(app.player.volume, general) + body.delta_percent
|
||||
else:
|
||||
raise HTTPException(status_code=422, detail="percent or delta_percent required")
|
||||
raise HTTPException(
|
||||
status_code=422, detail="percent, delta_percent or steps required"
|
||||
)
|
||||
return emit(
|
||||
SetVolumeRequested(
|
||||
volume=to_device_volume(max(0, min(100, target)), general), source="web"
|
||||
|
||||
@@ -229,10 +229,16 @@ class SeekIn(BaseModel):
|
||||
|
||||
|
||||
class VolumeIn(BaseModel):
|
||||
"""Either an absolute percentage or a relative step; exactly one of the two."""
|
||||
"""An absolute percentage, a relative percentage, or a number of steps; exactly
|
||||
one of the three.
|
||||
|
||||
``steps`` moves by ``volume_increment`` device units per step - the same step the
|
||||
IR remote's volume keys and the rotary encoder take - rather than by a percentage.
|
||||
"""
|
||||
|
||||
percent: int | None = Field(default=None, ge=0, le=100)
|
||||
delta_percent: int | None = Field(default=None, ge=-100, le=100)
|
||||
steps: int | None = Field(default=None, ge=-10, le=10)
|
||||
|
||||
|
||||
class SettingsOut(BaseModel):
|
||||
|
||||
@@ -292,13 +292,29 @@ async def test_volume_steps_clamp_at_the_ends(client: Client, sim: Simulation) -
|
||||
assert (await client.get("/api/state")).json()["volume"] == 100
|
||||
|
||||
|
||||
async def test_remote_steps_move_by_the_volume_increment(client: Client, sim: Simulation) -> None:
|
||||
"""The +/- buttons take the IR remote's step, in device units, not a percentage."""
|
||||
await client.post("/api/volume", json={"percent": 50})
|
||||
await sim.bus.drain()
|
||||
before = sim.player.volume
|
||||
step = sim.app.config.general.volume_increment
|
||||
|
||||
assert (await client.post("/api/volume", json={"steps": 1})).status_code == 204
|
||||
await sim.bus.drain()
|
||||
assert sim.player.volume == before + step
|
||||
|
||||
await client.post("/api/volume", json={"steps": -2})
|
||||
await sim.bus.drain()
|
||||
assert sim.player.volume == before - step
|
||||
|
||||
|
||||
async def test_the_device_volume_range_is_never_exposed(client: Client) -> None:
|
||||
state = (await client.get("/api/state")).json()
|
||||
assert "volume_min" not in state
|
||||
assert "volume_max" not in state
|
||||
|
||||
|
||||
async def test_volume_needs_one_of_the_two_fields(client: Client) -> None:
|
||||
async def test_volume_needs_one_of_the_fields(client: Client) -> None:
|
||||
assert (await client.post("/api/volume", json={})).status_code == 422
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user