Spotify follows source mode, name and credentials live
- spotify_init() sets up the logger, zeroconf routes and session task once; spotify_apply() (at boot and on every source save) enables or disables: enabled = mode spotify/auto + credentials. Disable removes the mDNS entry, zeroconf answers 404 and a running session ends (loop exits within 200 ms, SpircHandler::disconnect stops the queue and player tasks). A new device name ends the session and re-advertises. The login blob is swapped under a mutex. - Verified: hls/spotify/rename switch the mDNS entry and /spotify_info (404/200) live; switching to hls during Spotify playback ended the session in < 2 s, HLS played after ~5 s, internal heap 221 -> 365 KB (cspot frees everything). - CLAUDE.md: OTA-with-session findings (intermittent, flash-write stalls seen as TX resyncs during uploads), serial-port reset caveat. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -22,9 +22,12 @@ typedef enum {
|
||||
typedef size_t (*spotify_pcm_cb_t)(const int16_t *pcm, size_t frames);
|
||||
typedef void (*spotify_event_cb_t)(spotify_event_t ev, int value);
|
||||
|
||||
// Registers the zeroconf endpoints (/spotify_info) and, when source.mode is spotify/auto and the
|
||||
// client credentials are set, advertises the device and starts the session task.
|
||||
// Registers the zeroconf endpoints (/spotify_info), starts the session task and applies the config.
|
||||
esp_err_t spotify_init(spotify_pcm_cb_t pcm, spotify_event_cb_t event);
|
||||
// Re-read the "source" config: enabled (advertised, sessions accepted) when mode is spotify/auto and
|
||||
// the client credentials are set; otherwise not advertised and a running session is ended.
|
||||
// A new device name ends the session and re-advertises.
|
||||
void spotify_apply(void);
|
||||
// Pause/resume the running session (the Spotify app follows). No-op without a session.
|
||||
void spotify_pause(bool pause);
|
||||
// Current state for status.spotify_state.
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <exception>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
#include "BellLogger.h"
|
||||
@@ -26,7 +27,11 @@ extern "C" {
|
||||
|
||||
static const char *TAG = "spotify";
|
||||
|
||||
static std::shared_ptr<cspot::LoginBlob> s_blob;
|
||||
static std::shared_ptr<cspot::LoginBlob> s_blob; // guarded by s_mutex
|
||||
static std::mutex s_mutex;
|
||||
static std::string s_name; // advertised device name ("" = not advertised)
|
||||
static std::atomic<bool> s_enabled{false};
|
||||
static std::atomic<bool> s_stop{false}; // end the running session
|
||||
static std::atomic<bool> s_got_blob{false};
|
||||
static std::atomic<const char *> s_state{"disabled"};
|
||||
static std::atomic<uint64_t> s_pcm_bytes{0};
|
||||
@@ -93,9 +98,19 @@ public:
|
||||
|
||||
/* ----- zeroconf (Spotify Connect discovery) on our httpd ----- */
|
||||
|
||||
static std::shared_ptr<cspot::LoginBlob> blob()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(s_mutex);
|
||||
return s_blob;
|
||||
}
|
||||
|
||||
static esp_err_t info_get(httpd_req_t *req)
|
||||
{
|
||||
std::string json = s_blob->buildZeroconfInfo();
|
||||
auto b = blob();
|
||||
if (!s_enabled || !b) {
|
||||
return httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "Spotify Connect is disabled");
|
||||
}
|
||||
std::string json = b->buildZeroconfInfo();
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
return httpd_resp_send(req, json.data(), json.size());
|
||||
}
|
||||
@@ -120,6 +135,10 @@ static std::string url_decode(const char *s, size_t n)
|
||||
// The Spotify app posts its login blob (action=addUser&userName=...&blob=...&clientKey=...).
|
||||
static esp_err_t info_post(httpd_req_t *req)
|
||||
{
|
||||
auto b = blob();
|
||||
if (!s_enabled || !b) {
|
||||
return httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "Spotify Connect is disabled");
|
||||
}
|
||||
if (req->content_len == 0 || req->content_len > BODY_MAX) {
|
||||
return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "bad body");
|
||||
}
|
||||
@@ -143,7 +162,7 @@ static esp_err_t info_post(httpd_req_t *req)
|
||||
pos = end + 1;
|
||||
}
|
||||
ESP_LOGI(TAG, "zeroconf %s from \"%s\"", query["action"].c_str(), query["userName"].c_str());
|
||||
s_blob->loadZeroconfQuery(query);
|
||||
b->loadZeroconfQuery(query);
|
||||
s_got_blob = true;
|
||||
static const char ok[] = "{\"status\":101,\"spotifyError\":0,\"statusString\":\"ERROR-OK\"}";
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
@@ -178,14 +197,15 @@ static void session(void)
|
||||
int bitrate = (int)cJSON_GetObjectItemCaseSensitive(src, "spotify_bitrate")->valuedouble;
|
||||
cJSON_Delete(src);
|
||||
|
||||
auto ctx = cspot::Context::createFromBlob(s_blob);
|
||||
auto login = blob();
|
||||
auto ctx = cspot::Context::createFromBlob(login);
|
||||
ctx->config.clientId = client_id;
|
||||
ctx->config.clientSecret = client_secret;
|
||||
ctx->config.audioFormat = bitrate >= 320 ? AudioFormat_OGG_VORBIS_320
|
||||
: bitrate >= 160 ? AudioFormat_OGG_VORBIS_160 : AudioFormat_OGG_VORBIS_96;
|
||||
s_state = "connecting";
|
||||
ctx->session->connectWithRandomAp();
|
||||
auto token = ctx->session->authenticate(s_blob);
|
||||
auto token = ctx->session->authenticate(login);
|
||||
if (token.empty()) {
|
||||
s_state = "login failed";
|
||||
ESP_LOGE(TAG, "login failed (check the client ID/secret and that the account is Premium)");
|
||||
@@ -238,19 +258,26 @@ static void session(void)
|
||||
s_handler = handler;
|
||||
s_state = "connected";
|
||||
ESP_LOGI(TAG, "connected as %s", ctx->config.username.c_str());
|
||||
while (true) {
|
||||
ctx->session->handlePacket();
|
||||
while (!s_stop) {
|
||||
ctx->session->handlePacket(); // waits at most 200 ms
|
||||
}
|
||||
// Stops the queue and player tasks (waits for them), then closes the connection.
|
||||
s_handler.reset();
|
||||
handler->disconnect();
|
||||
ESP_LOGI(TAG, "session ended");
|
||||
}
|
||||
|
||||
static void spotify_task(void *arg)
|
||||
{
|
||||
while (true) {
|
||||
s_state = "waiting for Spotify app";
|
||||
while (!s_got_blob) {
|
||||
while (!s_enabled || !s_got_blob) {
|
||||
if (s_enabled) {
|
||||
s_state = "waiting for Spotify app";
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(500));
|
||||
}
|
||||
s_got_blob = false;
|
||||
s_stop = false;
|
||||
try {
|
||||
session();
|
||||
} catch (const std::exception &e) {
|
||||
@@ -262,6 +289,47 @@ static void spotify_task(void *arg)
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" void spotify_apply(void)
|
||||
{
|
||||
cJSON *src = cfg_get("source");
|
||||
std::string mode = cJSON_GetObjectItemCaseSensitive(src, "mode")->valuestring;
|
||||
std::string name = cJSON_GetObjectItemCaseSensitive(src, "spotify_name")->valuestring;
|
||||
bool creds = cJSON_GetObjectItemCaseSensitive(src, "spotify_client_id")->valuestring[0] &&
|
||||
cJSON_GetObjectItemCaseSensitive(src, "spotify_client_secret")->valuestring[0];
|
||||
cJSON_Delete(src);
|
||||
bool want = (mode == "spotify" || mode == "auto") && creds;
|
||||
|
||||
if (!want || name != s_name) {
|
||||
// Disable, or re-advertise under a new name: end any session, drop the old entry.
|
||||
s_enabled = false;
|
||||
s_stop = true;
|
||||
s_got_blob = false;
|
||||
if (!s_name.empty()) {
|
||||
mdns_service_remove("_spotify-connect", "_tcp");
|
||||
ESP_LOGI(TAG, "Spotify Connect \"%s\" withdrawn", s_name.c_str());
|
||||
s_name.clear();
|
||||
}
|
||||
}
|
||||
if (!want) {
|
||||
s_state = !creds ? "no client credentials" : "disabled (source mode)";
|
||||
if (!creds && (mode == "spotify" || mode == "auto")) {
|
||||
ESP_LOGW(TAG, "not started: set the Spotify client ID and secret (web UI, Source)");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (s_name.empty()) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(s_mutex);
|
||||
s_blob = std::make_shared<cspot::LoginBlob>(name);
|
||||
}
|
||||
mdns_txt_item_t txt[] = { { "VERSION", "1.0" }, { "CPath", "/spotify_info" }, { "Stack", "SP" } };
|
||||
esp_err_t err = mdns_service_add(name.c_str(), "_spotify-connect", "_tcp", 80, txt, 3);
|
||||
s_name = name;
|
||||
ESP_LOGI(TAG, "Spotify Connect \"%s\" advertised (%s)", name.c_str(), esp_err_to_name(err));
|
||||
}
|
||||
s_enabled = true;
|
||||
}
|
||||
|
||||
extern "C" esp_err_t spotify_init(spotify_pcm_cb_t pcm, spotify_event_cb_t event)
|
||||
{
|
||||
s_pcm_cb = pcm;
|
||||
@@ -269,29 +337,13 @@ extern "C" esp_err_t spotify_init(spotify_pcm_cb_t pcm, spotify_event_cb_t event
|
||||
if (!bell::bellGlobalLogger) {
|
||||
bell::bellGlobalLogger = new EspLogger();
|
||||
}
|
||||
cJSON *src = cfg_get("source");
|
||||
std::string mode = cJSON_GetObjectItemCaseSensitive(src, "mode")->valuestring;
|
||||
std::string name = cJSON_GetObjectItemCaseSensitive(src, "spotify_name")->valuestring;
|
||||
bool creds = cJSON_GetObjectItemCaseSensitive(src, "spotify_client_id")->valuestring[0] &&
|
||||
cJSON_GetObjectItemCaseSensitive(src, "spotify_client_secret")->valuestring[0];
|
||||
cJSON_Delete(src);
|
||||
|
||||
if (mode != "spotify" && mode != "auto") {
|
||||
s_state = "disabled (source mode)";
|
||||
return ESP_OK;
|
||||
}
|
||||
if (!creds) {
|
||||
s_state = "no client credentials";
|
||||
ESP_LOGW(TAG, "not started: set the Spotify client ID and secret (web UI, Source)");
|
||||
return ESP_OK;
|
||||
}
|
||||
s_blob = std::make_shared<cspot::LoginBlob>(name);
|
||||
static const httpd_uri_t get = { .uri = "/spotify_info", .method = HTTP_GET, .handler = info_get, .user_ctx = nullptr };
|
||||
static const httpd_uri_t post = { .uri = "/spotify_info", .method = HTTP_POST, .handler = info_post, .user_ctx = nullptr };
|
||||
web_register_uri(&get);
|
||||
web_register_uri(&post);
|
||||
mdns_txt_item_t txt[] = { { "VERSION", "1.0" }, { "CPath", "/spotify_info" }, { "Stack", "SP" } };
|
||||
esp_err_t err = mdns_service_add(name.c_str(), "_spotify-connect", "_tcp", 80, txt, 3);
|
||||
ESP_LOGI(TAG, "Spotify Connect \"%s\" advertised (%s)", name.c_str(), esp_err_to_name(err));
|
||||
return xTaskCreate(spotify_task, "spotify", 32 * 1024, nullptr, 5, nullptr) == pdPASS ? ESP_OK : ESP_ERR_NO_MEM;
|
||||
if (xTaskCreate(spotify_task, "spotify", 32 * 1024, nullptr, 5, nullptr) != pdPASS) {
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
spotify_apply();
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user