Number keys: the keyboard's digits work like the remote's; a key can hold one song
The IR remote's digits and the kiosk keyboard's number keys now both emit
PlayRemoteSlotRequested, and one reaction resolves the slot for either (a podcast
show to its newest episode, an album to the album). POST /api/remote/press/{digit}
is the keyboard's way in. Mid-search a digit is still part of the query.
A remote slot can start one song instead of a whole album: `track_index` on an
album slot plays from that track, then the rest of the album. Shift+A on the play
screen arms "this song" where A arms "this album", and the assign popup offers
both. A series slot cannot name a song, and an index past the album's end is
rejected when saving.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,7 @@ from pathlib import Path
|
||||
import httpx2
|
||||
import pytest
|
||||
|
||||
from musicmouse.config import WebConfig, load_config
|
||||
from musicmouse.config import RemoteSlotConfig, WebConfig, load_config
|
||||
from musicmouse.services.web.service import build_app
|
||||
from musicmouse.simulator.harness import Simulation, build_simulation
|
||||
from tests.conftest import VALID_CONFIG, write_config
|
||||
@@ -93,13 +93,93 @@ async def test_saving_an_album_slot_round_trips(
|
||||
assert response.status_code == 200
|
||||
slots = response.json()["slots"]
|
||||
assert slots == [
|
||||
{"digit": "3", "target_kind": "album", "target": target, "resolved_album_id": target}
|
||||
{
|
||||
"digit": "3",
|
||||
"target_kind": "album",
|
||||
"target": target,
|
||||
"resolved_album_id": target,
|
||||
"track_index": 0,
|
||||
}
|
||||
]
|
||||
|
||||
text = (config_dir / "config.yml").read_text(encoding="utf-8")
|
||||
assert "# The MusicMouse config." in text # comments survive
|
||||
reloaded = load_config(config_dir / "config.yml")
|
||||
assert reloaded.remote["3"].target == target
|
||||
assert "track_index" not in text # a whole-album slot reads as it always did
|
||||
|
||||
|
||||
async def test_saving_a_song_slot_round_trips(
|
||||
client: httpx2.AsyncClient, sim: Simulation, config_dir: Path
|
||||
) -> None:
|
||||
target = await album_id(sim, "Kinderparty Lieder")
|
||||
response = await client.put(
|
||||
"/api/remote/mapping",
|
||||
json={"slots": {"4": {"target_kind": "album", "target": target, "track_index": 1}}},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["slots"][0]["track_index"] == 1
|
||||
assert (await client.get("/api/remote/mapping")).json()["slots"][0]["track_index"] == 1
|
||||
assert sim.app.config.remote["4"].track_index == 1
|
||||
|
||||
reloaded = load_config(config_dir / "config.yml")
|
||||
assert reloaded.remote["4"].track_index == 1
|
||||
|
||||
|
||||
async def test_a_song_past_the_end_of_the_album_is_rejected(
|
||||
client: httpx2.AsyncClient, sim: Simulation
|
||||
) -> None:
|
||||
target = await album_id(sim, "Kinderparty Lieder")
|
||||
response = await client.put(
|
||||
"/api/remote/mapping",
|
||||
json={"slots": {"4": {"target_kind": "album", "target": target, "track_index": 2}}},
|
||||
)
|
||||
assert response.status_code == 422
|
||||
assert "no track 2" in response.json()["detail"]
|
||||
|
||||
|
||||
async def test_a_song_of_a_series_is_rejected(client: httpx2.AsyncClient) -> None:
|
||||
response = await client.put(
|
||||
"/api/remote/mapping",
|
||||
json={
|
||||
"slots": {
|
||||
"7": {"target_kind": "series", "target": "Wissen macht Ah", "track_index": 1}
|
||||
}
|
||||
},
|
||||
)
|
||||
assert response.status_code == 422
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- pressing
|
||||
|
||||
|
||||
async def test_pressing_a_key_plays_its_song_and_then_the_rest(
|
||||
client: httpx2.AsyncClient, sim: Simulation
|
||||
) -> None:
|
||||
target = await album_id(sim, "Kinderparty Lieder")
|
||||
sim.app.config.remote = {
|
||||
"4": RemoteSlotConfig(target_kind="album", target=target, track_index=1)
|
||||
}
|
||||
response = await client.post("/api/remote/press/4")
|
||||
assert response.status_code == 204
|
||||
await sim.bus.drain()
|
||||
|
||||
assert sim.player.playlist is not None
|
||||
assert sim.player.playlist.album_id == target
|
||||
assert len(sim.player.playlist) == 2 # the whole album, not just the one song
|
||||
assert sim.player.track_index == 1
|
||||
|
||||
|
||||
async def test_pressing_an_unassigned_key_does_nothing(
|
||||
client: httpx2.AsyncClient, sim: Simulation
|
||||
) -> None:
|
||||
assert (await client.post("/api/remote/press/5")).status_code == 204
|
||||
await sim.bus.drain()
|
||||
assert sim.player.playlist is None
|
||||
|
||||
|
||||
async def test_pressing_a_non_digit_is_rejected(client: httpx2.AsyncClient) -> None:
|
||||
assert (await client.post("/api/remote/press/x")).status_code == 422
|
||||
|
||||
|
||||
async def test_saving_a_series_slot_resolves_to_the_latest_episode(
|
||||
|
||||
Reference in New Issue
Block a user