I have some extra buttons in the house I can link up to programatic effects (python code). I wanted to use these to simply trigger defined amplipi presets that simply “start playing radio-station-x”
However if in two locations I have the same radio-station-x to play I see that loading the preset “steals” the radio-station from the one input down to the other.
I would rather automatically have the zone “join” the source that is already playing the station - conditionally that is: only if it is playing somewhere already (just to avoid the sudden steal effect)
I understand that I could drop the idea of using preset and just use the API to first check up on the sources and then make the correct decision.
However, I want to make sure I am not overlooking a smart preset config that automatically does this?
For completeness the preset-json-configs I am using:
(1) to play stream=1002 on group=100
Being able to add zones and groups to a currently playing stream (or create one if it isn’t) is something that many people would like, myself included.
Presets do not currently support this but if the a source’s id was optional stream “joining” or creation could be implemented in the preset loading functionality.
This is what your first request would look like (almost unchanged):
Another way to add functionality like this to the system would be to add an endpoint similar to the following:
/api/streams/{sid}/play_on
Here is a rough draft of some of the basic changes needed to add support for this from the api:
# in models.py
class PlayRequest(Base):
join: bool = True
zones: List[int] = []
groups: List[int] = []
# in app.py
@api.post('/api/streams/{sid}/play_on', tags=['stream'])
def play_stream_on(play_req: PlayRequest, ctrl: Api = Depends(get_ctrl), sid: int = params.StreamID) -> models.Status:
""" Play stream id=**sid** on speciofied zones potentially joining an existing playback"""
_, stream = utils.find(ctrl.get_state().streams, sid)
if stream is not None:
return code_response(ctrl, ctrl.play_on(sid, play_req))
raise HTTPException(404, f'stream {sid} not found')
# in ctrl.py
def play_on(self, sid: int, pr: PlayRequest):
# TODO: join an existing playback or steal this stream and use it on a source that isn't playing anything
raise UnimplementedException('play_on')
One additional route to add this would be to update set_zones and similar endpoints to accept a stream-id.
Thx for confirming that I do need to take another route than presets to handle this.
Did some quick testing and this approach should work for me already with the current API:
(1) start with a status-get or more narrowly a sources-get
(2) analyze the current state of affairs: i.e. detect if stream=1002 is the input of one of the sources (best to check if state == playing too)
(3) if so join that source with the zone through a zone-update with source_id == the found && playing one
(4) if no such playing source is found – simply resort to the preset approach (since there is no danger of stealing)
Concerning the future API additions you propose:
(a) the preset-with-new-semantics for source_id
feels odd to source_id == join – specially in combo with the source_id == 1 further down in the json?
all of this leaves the negotiation of at what source_id we will eventually end up quite open: if there is nothing to join we might want to indicate in which order the algorithm should further look for non-active/available source_id to use?
in the end this kinda feels like dumping coding instructions into an essential static-state-representation format (encoded in json) – but that is purely personal gut/sensitivity playing up
(b) the new api addition /api/streams/{sid}/play_on
feels better
still leaves the actual source_id to use open – not a bad thing in itself, but we might easily add an additional property to the PlayRequest indicating with allowed_source = List[int] in which order it should look for available sources to make this stream-to-destination connection?
But as you say yourself: best to give all of this some more time and thought?
This is but the first thing I am trying to do with these buttons. And the more I am playing / tinkering / contemplating these possible automation-effects - the more I have the feeling that the current API is taking a (correct and stable) “state oriented” view of the amplipi service – bringing a REST interface approach to essentially the actual internal “state of the system”
An approach we should applaud and even preserve.
The stuff we are adding here now calls for a more “functional oriented” view on things. A higher layer if you like, one that typically wants to be able to ignore the internal state, and approach the service as something that will sort out its own internal state changes to comply to some intentions or functional goals.
Most likely - for access convenience - we will end up mapping those onto webapi(*) calls, but I would actually keep them distinct from the current state-oriented end-points? so keep /api to cover the stateful resources, but introduce a separate space (/api)/calls for this more remote-procedure-call-kind of stuff?
(*) which I would not call REST, but that is just my sensitivity to web design principles playing up