Add schedule sync and schedule-aware playback

This commit is contained in:
NeilClack
2026-09-11 19:14:55 -05:00
parent a043ec84d8
commit f37beaf07e
23 changed files with 893 additions and 32 deletions
+128
View File
@@ -181,6 +181,23 @@ bool DatabaseManager::runMigrations() {
hex TEXT NOT NULL,
PRIMARY KEY (solfeggio_frequency_id, color_index)
);
CREATE TABLE IF NOT EXISTS schedule_blocks (
id TEXT PRIMARY KEY,
start_minutes INTEGER NOT NULL DEFAULT 0,
end_minutes INTEGER NOT NULL DEFAULT 0,
overnight INTEGER NOT NULL DEFAULT 0,
duration_minutes INTEGER NOT NULL DEFAULT 0,
dynamic_audio INTEGER NOT NULL DEFAULT 1,
sort_order INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS schedule_block_proclamations (
block_id TEXT NOT NULL,
position INTEGER NOT NULL,
proclamation_id TEXT NOT NULL,
PRIMARY KEY (block_id, position)
);
)";
bool ok = executeSql(schema);
@@ -1083,4 +1100,115 @@ std::vector<AudioFileRecord> DatabaseManager::getAudioFiles() {
return result;
}
// ─── Schedule Blocks Table ─────────────────────────────────────────────
bool DatabaseManager::replaceScheduleBlocks(const std::vector<ScheduleBlockRecord>& records) {
std::lock_guard<std::mutex> lock(mutex_);
if (!db_) return false;
if (sqlite3_exec(db_, "BEGIN;", nullptr, nullptr, nullptr) != SQLITE_OK) {
return false;
}
bool ok = true;
if (sqlite3_exec(db_, "DELETE FROM schedule_block_proclamations;", nullptr, nullptr, nullptr) != SQLITE_OK ||
sqlite3_exec(db_, "DELETE FROM schedule_blocks;", nullptr, nullptr, nullptr) != SQLITE_OK) {
sqlite3_exec(db_, "ROLLBACK;", nullptr, nullptr, nullptr);
return false;
}
const char* blockSql = "INSERT INTO schedule_blocks "
"(id, start_minutes, end_minutes, overnight, duration_minutes, "
" dynamic_audio, sort_order) "
"VALUES (?, ?, ?, ?, ?, ?, ?);";
const char* procSql = "INSERT INTO schedule_block_proclamations "
"(block_id, position, proclamation_id) VALUES (?, ?, ?);";
for (size_t i = 0; i < records.size() && ok; ++i) {
const ScheduleBlockRecord& rec = records[i];
sqlite3_stmt* blockStmt = nullptr;
if (sqlite3_prepare_v2(db_, blockSql, -1, &blockStmt, nullptr) != SQLITE_OK) {
ok = false;
break;
}
sqlite3_bind_text(blockStmt, 1, rec.id.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_int(blockStmt, 2, rec.start_minutes);
sqlite3_bind_int(blockStmt, 3, rec.end_minutes);
sqlite3_bind_int(blockStmt, 4, rec.overnight ? 1 : 0);
sqlite3_bind_int(blockStmt, 5, rec.duration_minutes);
sqlite3_bind_int(blockStmt, 6, rec.dynamic_audio ? 1 : 0);
sqlite3_bind_int(blockStmt, 7, static_cast<int>(i));
if (sqlite3_step(blockStmt) != SQLITE_DONE) ok = false;
sqlite3_finalize(blockStmt);
if (!ok) break;
for (size_t p = 0; p < rec.proclamation_ids.size(); ++p) {
sqlite3_stmt* procStmt = nullptr;
if (sqlite3_prepare_v2(db_, procSql, -1, &procStmt, nullptr) != SQLITE_OK) {
ok = false;
break;
}
sqlite3_bind_text(procStmt, 1, rec.id.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_int(procStmt, 2, static_cast<int>(p));
sqlite3_bind_text(procStmt, 3, rec.proclamation_ids[p].c_str(), -1, SQLITE_TRANSIENT);
if (sqlite3_step(procStmt) != SQLITE_DONE) ok = false;
sqlite3_finalize(procStmt);
if (!ok) break;
}
}
if (ok) {
sqlite3_exec(db_, "COMMIT;", nullptr, nullptr, nullptr);
} else {
sqlite3_exec(db_, "ROLLBACK;", nullptr, nullptr, nullptr);
}
return ok;
}
std::vector<ScheduleBlockRecord> DatabaseManager::getScheduleBlocks() {
std::lock_guard<std::mutex> lock(mutex_);
std::vector<ScheduleBlockRecord> result;
if (!db_) return result;
const char* sql = "SELECT id, start_minutes, end_minutes, overnight, duration_minutes, "
"dynamic_audio FROM schedule_blocks ORDER BY sort_order ASC;";
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
return result;
}
while (sqlite3_step(stmt) == SQLITE_ROW) {
ScheduleBlockRecord rec;
rec.id = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
rec.start_minutes = sqlite3_column_int(stmt, 1);
rec.end_minutes = sqlite3_column_int(stmt, 2);
rec.overnight = (sqlite3_column_int(stmt, 3) != 0);
rec.duration_minutes = sqlite3_column_int(stmt, 4);
rec.dynamic_audio = (sqlite3_column_int(stmt, 5) != 0);
result.push_back(std::move(rec));
}
sqlite3_finalize(stmt);
// Attach each block's proclamation ids in play order.
const char* procSql = "SELECT proclamation_id FROM schedule_block_proclamations "
"WHERE block_id = ? ORDER BY position ASC;";
for (auto& block : result) {
sqlite3_stmt* procStmt = nullptr;
if (sqlite3_prepare_v2(db_, procSql, -1, &procStmt, nullptr) != SQLITE_OK) {
break;
}
sqlite3_bind_text(procStmt, 1, block.id.c_str(), -1, SQLITE_TRANSIENT);
while (sqlite3_step(procStmt) == SQLITE_ROW) {
const unsigned char* text = sqlite3_column_text(procStmt, 0);
if (text) {
block.proclamation_ids.push_back(reinterpret_cast<const char*>(text));
}
}
sqlite3_finalize(procStmt);
}
return result;
}
} // namespace radiantwave::db
+22
View File
@@ -54,6 +54,18 @@ struct AudioFileRecord {
int solfeggio_frequency_id = 0; // 0 = not associated with a solfeggio frequency
};
// One scheduled listening block, mirrored from GET /schedule. `proclamation_ids`
// is the ordered play list for the block.
struct ScheduleBlockRecord {
std::string id;
int start_minutes = 0;
int end_minutes = 0;
bool overnight = false;
int duration_minutes = 0;
bool dynamic_audio = true; // override the device audio for this block
std::vector<std::string> proclamation_ids;
};
// A single color in a solfeggio frequency's color palette (RGBA).
struct PaletteColor {
uint8_t r = 0;
@@ -154,6 +166,16 @@ public:
// palettes can be pushed from the portal once the API route exists.
bool replaceColorPalettes(const std::vector<PaletteRecord>& records);
// ── Schedule Blocks Table ─────────────────────────────────────────
// Full sync: replaces the stored schedule with `records` (the portal is the
// source of truth). Pass an empty vector to clear the schedule. Block order
// and per-block proclamation order are preserved exactly as given.
bool replaceScheduleBlocks(const std::vector<ScheduleBlockRecord>& records);
// Fetch all schedule blocks, ordered as stored, with their proclamation ids
// in play order.
std::vector<ScheduleBlockRecord> getScheduleBlocks();
// ── Event Logging Table ───────────────────────────────────────────
bool logEvent(const std::string& level, const std::string& component, const std::string& message);