Player: seek and volume on /api/player

seek {ms} runs on the Spotify session task like a seek from the app (buffer
flushed, app notified). volume {value}|{delta} sets the player volume and
pushes it to the session, so the app's slider follows; setRemoteVolume now
also runs on the session task.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 23:04:11 +10:00
parent 8485ecb5b7
commit a55a3ce14a
4 changed files with 94 additions and 18 deletions
+60 -6
View File
@@ -423,7 +423,7 @@ static const char *transport(const char *cmd)
if (src == PLAYER_SRC_SPOTIFY) {
spotify_cmd_t c = pause ? SPOTIFY_CMD_PAUSE : play ? SPOTIFY_CMD_PLAY :
next ? SPOTIFY_CMD_NEXT : SPOTIFY_CMD_PREV;
return spotify_command(c) ? NULL : "no Spotify session";
return spotify_command(c, 0) ? NULL : "no Spotify session";
}
if (src == PLAYER_SRC_HLS) {
if (next || prev) {
@@ -435,6 +435,61 @@ static const char *transport(const char *cmd)
return "no player source active";
}
// Small JSON body of a POST; NULL if missing or invalid.
static cJSON *read_body(httpd_req_t *req)
{
char buf[128];
if (req->content_len == 0 || req->content_len >= sizeof(buf)) {
return NULL;
}
size_t got = 0;
while (got < req->content_len) {
int r = httpd_req_recv(req, buf + got, req->content_len - got);
if (r == HTTPD_SOCK_ERR_TIMEOUT) {
continue;
}
if (r <= 0) {
return NULL;
}
got += r;
}
buf[got] = '\0';
return cJSON_Parse(buf);
}
static esp_err_t send_409(httpd_req_t *req, const char *msg)
{
httpd_resp_set_status(req, "409 Conflict");
return httpd_resp_sendstr(req, msg);
}
// POST /api/player/seek {ms}, /api/player/volume {value} or {delta}
static esp_err_t seek_or_volume(httpd_req_t *req, bool seek)
{
cJSON *body = read_body(req);
const cJSON *v = cJSON_GetObjectItemCaseSensitive(body, seek ? "ms" : "value");
const cJSON *d = seek ? NULL : cJSON_GetObjectItemCaseSensitive(body, "delta");
double val = cJSON_IsNumber(v) ? v->valuedouble : cJSON_IsNumber(d) ? s_volume_pct + d->valuedouble : -1;
cJSON_Delete(body);
if (val < 0 && !(cJSON_IsNumber(d) && !seek)) {
return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, seek ? "expected {\"ms\": n}" :
"expected {\"value\": 0-100} or {\"delta\": n}");
}
if (seek) {
if (s_src != PLAYER_SRC_SPOTIFY) {
return send_409(req, "source can't seek");
}
if (!spotify_command(SPOTIFY_CMD_SEEK, (uint32_t)val)) {
return send_409(req, "no Spotify session");
}
} else {
int pct = val < 0 ? 0 : val > 100 ? 100 : (int)(val + 0.5);
set_volume(pct);
spotify_set_volume(pct); // the app follows (if a session runs)
}
return send_player(req);
}
// POST /api/player/<cmd>
static esp_err_t player_post(httpd_req_t *req)
{
@@ -450,13 +505,12 @@ static esp_err_t player_post(httpd_req_t *req)
for (int i = 0; i < sizeof(TRANSPORT) / sizeof(TRANSPORT[0]); i++) {
if (!strcmp(name, TRANSPORT[i])) {
const char *err = transport(name);
if (err) {
httpd_resp_set_status(req, "409 Conflict");
return httpd_resp_sendstr(req, err);
}
return send_player(req);
return err ? send_409(req, err) : send_player(req);
}
}
if (!strcmp(name, "seek") || !strcmp(name, "volume")) {
return seek_or_volume(req, name[0] == 's');
}
return httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "unknown player command");
}