initial commit
This commit is contained in:
@@ -0,0 +1,953 @@
|
||||
#include "database_manager.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <pwd.h>
|
||||
#include <unordered_map>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace radiantwave::db {
|
||||
|
||||
// ─── Constructor & Destructor ──────────────────────────────────────────
|
||||
|
||||
DatabaseManager::DatabaseManager() = default;
|
||||
|
||||
DatabaseManager::~DatabaseManager() {
|
||||
close();
|
||||
}
|
||||
|
||||
// ─── Path Resolution ───────────────────────────────────────────────────
|
||||
|
||||
std::string DatabaseManager::resolvePath(const std::string& raw_path) const {
|
||||
std::string path = raw_path;
|
||||
|
||||
if (path.empty()) {
|
||||
const char* home = std::getenv("HOME");
|
||||
if (!home) {
|
||||
struct passwd* pw = getpwuid(getuid());
|
||||
if (pw) {
|
||||
home = pw->pw_dir;
|
||||
}
|
||||
}
|
||||
|
||||
if (home) {
|
||||
path = std::string(home) + "/.local/radiantwave/radiantwave.db";
|
||||
} else {
|
||||
path = "/tmp/radiantwave/radiantwave.db";
|
||||
}
|
||||
} else if (path.front() == '~') {
|
||||
const char* home = std::getenv("HOME");
|
||||
if (!home) {
|
||||
struct passwd* pw = getpwuid(getuid());
|
||||
if (pw) {
|
||||
home = pw->pw_dir;
|
||||
}
|
||||
}
|
||||
|
||||
if (home) {
|
||||
path = std::string(home) + path.substr(1);
|
||||
}
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
// ─── Lifecycle & Migrations ────────────────────────────────────────────
|
||||
|
||||
bool DatabaseManager::initialize(const std::string& custom_path) {
|
||||
db_path_ = resolvePath(custom_path);
|
||||
|
||||
// Create directories if needed
|
||||
try {
|
||||
std::filesystem::path p(db_path_);
|
||||
if (p.has_parent_path()) {
|
||||
std::filesystem::create_directories(p.parent_path());
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "[DB] Failed to create directories: " << e.what() << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
int rc = sqlite3_open_v2(
|
||||
db_path_.c_str(),
|
||||
&db_,
|
||||
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX,
|
||||
nullptr
|
||||
);
|
||||
|
||||
if (rc != SQLITE_OK) {
|
||||
std::cerr << "[DB] Cannot open database " << db_path_ << ": " << sqlite3_errmsg(db_) << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Enable WAL mode and foreign keys for optimal kiosk performance
|
||||
executeSql("PRAGMA journal_mode=WAL;");
|
||||
executeSql("PRAGMA foreign_keys=ON;");
|
||||
|
||||
return runMigrations();
|
||||
}
|
||||
|
||||
void DatabaseManager::close() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (db_) {
|
||||
sqlite3_close_v2(db_);
|
||||
db_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool DatabaseManager::isOpen() const {
|
||||
return db_ != nullptr;
|
||||
}
|
||||
|
||||
bool DatabaseManager::executeSql(const std::string& sql) {
|
||||
char* err_msg = nullptr;
|
||||
int rc = sqlite3_exec(db_, sql.c_str(), nullptr, nullptr, &err_msg);
|
||||
if (rc != SQLITE_OK) {
|
||||
std::cerr << "[DB] SQL error: " << (err_msg ? err_msg : "unknown") << std::endl;
|
||||
if (err_msg) sqlite3_free(err_msg);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DatabaseManager::runMigrations() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
|
||||
const char* schema = R"(
|
||||
CREATE TABLE IF NOT EXISTS system_kv (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS proclamations (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
solfeggio_freq_hz REAL DEFAULT 528.0,
|
||||
category TEXT DEFAULT 'general',
|
||||
is_active INTEGER DEFAULT 1,
|
||||
selected INTEGER DEFAULT 0,
|
||||
category_id INTEGER DEFAULT 0,
|
||||
solfeggio_frequency_id INTEGER DEFAULT 0,
|
||||
required_level_id INTEGER DEFAULT 1,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS proclamation_categories (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
parent_id INTEGER DEFAULT 0,
|
||||
sort_order INTEGER DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS asset_downloads (
|
||||
id TEXT PRIMARY KEY,
|
||||
filename TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
status TEXT CHECK (status IN ('pending', 'downloading', 'completed', 'failed')) DEFAULT 'pending',
|
||||
bytes_downloaded INTEGER DEFAULT 0,
|
||||
total_bytes INTEGER DEFAULT 0,
|
||||
error_message TEXT DEFAULT '',
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS audio_files (
|
||||
id INTEGER PRIMARY KEY,
|
||||
display_name TEXT NOT NULL,
|
||||
filename TEXT NOT NULL UNIQUE,
|
||||
display_order INTEGER DEFAULT 0,
|
||||
solfeggio_frequency_id INTEGER DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS event_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
level TEXT CHECK (level IN ('INFO', 'WARN', 'ERROR')) DEFAULT 'INFO',
|
||||
component TEXT NOT NULL,
|
||||
message TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS color_palettes (
|
||||
solfeggio_frequency_id INTEGER NOT NULL,
|
||||
color_index INTEGER NOT NULL,
|
||||
hex TEXT NOT NULL,
|
||||
PRIMARY KEY (solfeggio_frequency_id, color_index)
|
||||
);
|
||||
)";
|
||||
|
||||
bool ok = executeSql(schema);
|
||||
if (ok) {
|
||||
// Back-fill columns for databases created before these fields existed.
|
||||
ensureColumn("proclamations", "selected", "selected INTEGER DEFAULT 0");
|
||||
ensureColumn("proclamations", "category_id", "category_id INTEGER DEFAULT 0");
|
||||
ensureColumn("proclamations", "solfeggio_frequency_id", "solfeggio_frequency_id INTEGER DEFAULT 0");
|
||||
ensureColumn("proclamations", "required_level_id", "required_level_id INTEGER DEFAULT 1");
|
||||
ensureColumn("audio_files", "solfeggio_frequency_id", "solfeggio_frequency_id INTEGER DEFAULT 0");
|
||||
}
|
||||
if (ok) {
|
||||
seedColorPalettes();
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
void DatabaseManager::seedColorPalettes() {
|
||||
static const char* kSeedSql =
|
||||
"INSERT OR IGNORE INTO color_palettes (solfeggio_frequency_id, color_index, hex) VALUES "
|
||||
// 396 Hz
|
||||
"(2,0,'#8B0000'),(2,1,'#B22222'),(2,2,'#DC143C'),(2,3,'#FF6347'),(2,4,'#FF8C69'),(2,5,'#FFA500'),(2,6,'#FFDAB9'),"
|
||||
// 417 Hz
|
||||
"(3,0,'#FF4500'),(3,1,'#FF7F50'),(3,2,'#FF8C00'),(3,3,'#FFA500'),(3,4,'#FFC87F'),(3,5,'#FFDFBA'),(3,6,'#FFEBCD'),"
|
||||
// 528 Hz
|
||||
"(5,0,'#32CD32'),(5,1,'#3CB371'),(5,2,'#90EE90'),(5,3,'#98FB98'),(5,4,'#ADFFA9'),(5,5,'#C8FFD2'),(5,6,'#F0FFF0'),"
|
||||
// 639 Hz
|
||||
"(6,0,'#00FF7F'),(6,1,'#00CED1'),(6,2,'#40E0D0'),(6,3,'#5F9EA0'),(6,4,'#7FFFD4'),(6,5,'#AFEEEE'),(6,6,'#E0FFFF'),"
|
||||
// 741 Hz
|
||||
"(7,0,'#00BFFF'),(7,1,'#1E90FF'),(7,2,'#4169E1'),(7,3,'#6495ED'),(7,4,'#87CEEB'),(7,5,'#ADD8E6'),(7,6,'#F0F8FF'),"
|
||||
// 852 Hz
|
||||
"(8,0,'#4B0082'),(8,1,'#9370DB'),(8,2,'#8A2BE2'),(8,3,'#7B68EE'),(8,4,'#DA70D6'),(8,5,'#D8BFD8'),(8,6,'#E6E6FA'),"
|
||||
// 963 Hz
|
||||
"(9,0,'#9400D3'),(9,1,'#BA55D3'),(9,2,'#C71585'),(9,3,'#DDA0DD'),(9,4,'#EE82EE'),(9,5,'#FFF0F5'),(9,6,'#FAF0FA'),"
|
||||
// 174/285 Hz (grounding/relief): warm gold
|
||||
"(1,0,'#8B6914'),(1,1,'#B8860B'),(1,2,'#DAA520'),(1,3,'#E8B64C'),(1,4,'#FFD700'),(1,5,'#FFE9A8'),(1,6,'#FFF4CC'),"
|
||||
// 432 Hz (clarity/wellbeing): soft rose
|
||||
"(4,0,'#8B2252'),(4,1,'#CD3278'),(4,2,'#DB7093'),(4,3,'#FF69B4'),(4,4,'#FFB6C1'),(4,5,'#FFC0CB'),(4,6,'#FFE4E1');";
|
||||
|
||||
executeSql(kSeedSql);
|
||||
}
|
||||
|
||||
bool DatabaseManager::columnExists(const std::string& table, const std::string& column) {
|
||||
if (!db_) return false;
|
||||
|
||||
const char* sql = "SELECT COUNT(*) FROM pragma_table_info(?) WHERE name = ?;";
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
|
||||
return false;
|
||||
}
|
||||
|
||||
sqlite3_bind_text(stmt, 1, table.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 2, column.c_str(), -1, SQLITE_TRANSIENT);
|
||||
|
||||
bool exists = (sqlite3_step(stmt) == SQLITE_ROW && sqlite3_column_int(stmt, 0) > 0);
|
||||
sqlite3_finalize(stmt);
|
||||
return exists;
|
||||
}
|
||||
|
||||
bool DatabaseManager::ensureColumn(const std::string& table, const std::string& column,
|
||||
const std::string& column_ddl) {
|
||||
if (columnExists(table, column)) {
|
||||
return true;
|
||||
}
|
||||
return executeSql("ALTER TABLE " + table + " ADD COLUMN " + column_ddl + ";");
|
||||
}
|
||||
|
||||
// ─── Key-Value Storage (system_kv table) ───────────────────────────────
|
||||
|
||||
bool DatabaseManager::setKV(const std::string& key, const std::string& value) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (!db_) return false;
|
||||
|
||||
const char* sql = "INSERT INTO system_kv (key, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP) "
|
||||
"ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=CURRENT_TIMESTAMP;";
|
||||
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
|
||||
return false;
|
||||
}
|
||||
|
||||
sqlite3_bind_text(stmt, 1, key.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 2, value.c_str(), -1, SQLITE_TRANSIENT);
|
||||
|
||||
int rc = sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
return rc == SQLITE_DONE;
|
||||
}
|
||||
|
||||
std::string DatabaseManager::getKV(const std::string& key, const std::string& defaultValue) const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (!db_) return defaultValue;
|
||||
|
||||
const char* sql = "SELECT value FROM system_kv WHERE key = ?;";
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
sqlite3_bind_text(stmt, 1, key.c_str(), -1, SQLITE_TRANSIENT);
|
||||
|
||||
std::string result = defaultValue;
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
const unsigned char* text = sqlite3_column_text(stmt, 0);
|
||||
if (text) {
|
||||
result = reinterpret_cast<const char*>(text);
|
||||
}
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool DatabaseManager::hasKV(const std::string& key) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (!db_) return false;
|
||||
|
||||
const char* sql = "SELECT 1 FROM system_kv WHERE key = ?;";
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
|
||||
return false;
|
||||
}
|
||||
|
||||
sqlite3_bind_text(stmt, 1, key.c_str(), -1, SQLITE_TRANSIENT);
|
||||
|
||||
bool exists = (sqlite3_step(stmt) == SQLITE_ROW);
|
||||
sqlite3_finalize(stmt);
|
||||
return exists;
|
||||
}
|
||||
|
||||
bool DatabaseManager::deleteKV(const std::string& key) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (!db_) return false;
|
||||
|
||||
const char* sql = "DELETE FROM system_kv WHERE key = ?;";
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
|
||||
return false;
|
||||
}
|
||||
|
||||
sqlite3_bind_text(stmt, 1, key.c_str(), -1, SQLITE_TRANSIENT);
|
||||
|
||||
int rc = sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
return rc == SQLITE_DONE;
|
||||
}
|
||||
|
||||
// ─── Proclamations ─────────────────────────────────────────────────────
|
||||
|
||||
void DatabaseManager::bindProclamationRecord(sqlite3_stmt* stmt, const ProclamationRecord& record) {
|
||||
sqlite3_bind_text(stmt, 1, record.id.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 2, record.title.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 3, record.content.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_double(stmt, 4, record.solfeggio_freq_hz);
|
||||
sqlite3_bind_text(stmt, 5, record.category.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_int(stmt, 6, record.is_active ? 1 : 0);
|
||||
sqlite3_bind_int(stmt, 7, record.selected ? 1 : 0);
|
||||
sqlite3_bind_int(stmt, 8, record.category_id);
|
||||
sqlite3_bind_int(stmt, 9, record.solfeggio_frequency_id);
|
||||
sqlite3_bind_int(stmt, 10, record.required_level_id);
|
||||
}
|
||||
|
||||
bool DatabaseManager::saveProclamation(const ProclamationRecord& record) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (!db_) return false;
|
||||
|
||||
const char* sql = "INSERT INTO proclamations (id, title, content, solfeggio_freq_hz, category, "
|
||||
"is_active, selected, category_id, solfeggio_frequency_id, required_level_id) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) "
|
||||
"ON CONFLICT(id) DO UPDATE SET title=excluded.title, content=excluded.content, "
|
||||
"solfeggio_freq_hz=excluded.solfeggio_freq_hz, category=excluded.category, "
|
||||
"is_active=excluded.is_active, selected=excluded.selected, "
|
||||
"category_id=excluded.category_id, solfeggio_frequency_id=excluded.solfeggio_frequency_id, "
|
||||
"required_level_id=excluded.required_level_id;";
|
||||
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bindProclamationRecord(stmt, record);
|
||||
|
||||
int rc = sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
return rc == SQLITE_DONE;
|
||||
}
|
||||
|
||||
static ProclamationRecord readProclamationRecord(sqlite3_stmt* stmt, int firstCol) {
|
||||
ProclamationRecord rec;
|
||||
rec.id = reinterpret_cast<const char*>(sqlite3_column_text(stmt, firstCol + 0));
|
||||
rec.title = reinterpret_cast<const char*>(sqlite3_column_text(stmt, firstCol + 1));
|
||||
rec.content = reinterpret_cast<const char*>(sqlite3_column_text(stmt, firstCol + 2));
|
||||
rec.solfeggio_freq_hz = sqlite3_column_double(stmt, firstCol + 3);
|
||||
rec.category = reinterpret_cast<const char*>(sqlite3_column_text(stmt, firstCol + 4));
|
||||
rec.is_active = (sqlite3_column_int(stmt, firstCol + 5) != 0);
|
||||
rec.selected = (sqlite3_column_int(stmt, firstCol + 6) != 0);
|
||||
rec.category_id = sqlite3_column_int(stmt, firstCol + 7);
|
||||
rec.solfeggio_frequency_id = sqlite3_column_int(stmt, firstCol + 8);
|
||||
rec.required_level_id = sqlite3_column_int(stmt, firstCol + 9);
|
||||
return rec;
|
||||
}
|
||||
|
||||
std::vector<ProclamationRecord> DatabaseManager::getActiveProclamations() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
std::vector<ProclamationRecord> result;
|
||||
if (!db_) return result;
|
||||
|
||||
const char* sql = "SELECT id, title, content, solfeggio_freq_hz, category, is_active, "
|
||||
"selected, category_id, solfeggio_frequency_id, required_level_id "
|
||||
"FROM proclamations WHERE is_active = 1;";
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
|
||||
return result;
|
||||
}
|
||||
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
result.push_back(readProclamationRecord(stmt, 0));
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool DatabaseManager::replaceCategories(const std::vector<ProclamationCategoryRecord>& 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;
|
||||
const char* delSql = "DELETE FROM proclamation_categories;";
|
||||
if (sqlite3_exec(db_, delSql, nullptr, nullptr, nullptr) != SQLITE_OK) {
|
||||
sqlite3_exec(db_, "ROLLBACK;", nullptr, nullptr, nullptr);
|
||||
return false;
|
||||
}
|
||||
|
||||
const char* insSql = "INSERT INTO proclamation_categories (id, name, parent_id, sort_order) "
|
||||
"VALUES (?, ?, ?, ?);";
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_, insSql, -1, &stmt, nullptr) != SQLITE_OK) {
|
||||
sqlite3_exec(db_, "ROLLBACK;", nullptr, nullptr, nullptr);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const auto& cat : records) {
|
||||
sqlite3_bind_int(stmt, 1, cat.id);
|
||||
sqlite3_bind_text(stmt, 2, cat.name.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_int(stmt, 3, cat.parent_id);
|
||||
sqlite3_bind_int(stmt, 4, cat.sort_order);
|
||||
if (sqlite3_step(stmt) != SQLITE_DONE) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
sqlite3_reset(stmt);
|
||||
sqlite3_clear_bindings(stmt);
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
if (ok) {
|
||||
sqlite3_exec(db_, "COMMIT;", nullptr, nullptr, nullptr);
|
||||
} else {
|
||||
sqlite3_exec(db_, "ROLLBACK;", nullptr, nullptr, nullptr);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool DatabaseManager::syncProclamations(const std::vector<ProclamationRecord>& records,
|
||||
const std::string& defaultProclamationId) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (!db_) return false;
|
||||
|
||||
// Stash current selection so a re-sync doesn't reset what the user picked.
|
||||
std::unordered_map<std::string, bool> stash;
|
||||
{
|
||||
const char* selSql = "SELECT id, selected FROM proclamations;";
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_, selSql, -1, &stmt, nullptr) == SQLITE_OK) {
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
std::string id = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
|
||||
stash[id] = (sqlite3_column_int(stmt, 1) != 0);
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
}
|
||||
|
||||
if (sqlite3_exec(db_, "BEGIN;", nullptr, nullptr, nullptr) != SQLITE_OK) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ok = true;
|
||||
|
||||
// Remove proclamations the API no longer returns, but never the default one.
|
||||
{
|
||||
std::string delSql = "DELETE FROM proclamations WHERE id NOT IN (";
|
||||
if (!records.empty()) {
|
||||
for (size_t i = 0; i < records.size(); ++i) {
|
||||
delSql += i == 0 ? "?" : ",?";
|
||||
}
|
||||
} else {
|
||||
delSql += "''";
|
||||
}
|
||||
delSql += ") AND id != ?;";
|
||||
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_, delSql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
|
||||
ok = false;
|
||||
} else {
|
||||
for (size_t i = 0; i < records.size(); ++i) {
|
||||
sqlite3_bind_text(stmt, static_cast<int>(i + 1), records[i].id.c_str(), -1, SQLITE_TRANSIENT);
|
||||
}
|
||||
sqlite3_bind_text(stmt, static_cast<int>(records.size() + 1), defaultProclamationId.c_str(), -1, SQLITE_TRANSIENT);
|
||||
if (sqlite3_step(stmt) != SQLITE_DONE) {
|
||||
ok = false;
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
}
|
||||
|
||||
// Upsert each proclamation, preserving the stashed selection state.
|
||||
if (ok) {
|
||||
const char* sql = "INSERT INTO proclamations (id, title, content, solfeggio_freq_hz, category, "
|
||||
"is_active, selected, category_id, solfeggio_frequency_id, required_level_id) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) "
|
||||
"ON CONFLICT(id) DO UPDATE SET title=excluded.title, content=excluded.content, "
|
||||
"solfeggio_freq_hz=excluded.solfeggio_freq_hz, category=excluded.category, "
|
||||
"is_active=excluded.is_active, category_id=excluded.category_id, "
|
||||
"solfeggio_frequency_id=excluded.solfeggio_frequency_id, "
|
||||
"required_level_id=excluded.required_level_id;";
|
||||
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
|
||||
ok = false;
|
||||
} else {
|
||||
for (const auto& rec : records) {
|
||||
ProclamationRecord r = rec;
|
||||
auto it = stash.find(r.id);
|
||||
r.selected = (it != stash.end()) ? it->second : false;
|
||||
bindProclamationRecord(stmt, r);
|
||||
if (sqlite3_step(stmt) != SQLITE_DONE) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
sqlite3_reset(stmt);
|
||||
sqlite3_clear_bindings(stmt);
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
}
|
||||
|
||||
// Force the default proclamation to remain selected.
|
||||
if (ok) {
|
||||
const char* defSql = "UPDATE proclamations SET selected = 1 WHERE id = ?;";
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_, defSql, -1, &stmt, nullptr) == SQLITE_OK) {
|
||||
sqlite3_bind_text(stmt, 1, defaultProclamationId.c_str(), -1, SQLITE_TRANSIENT);
|
||||
if (sqlite3_step(stmt) != SQLITE_DONE) {
|
||||
ok = false;
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
} else {
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
sqlite3_exec(db_, "COMMIT;", nullptr, nullptr, nullptr);
|
||||
} else {
|
||||
sqlite3_exec(db_, "ROLLBACK;", nullptr, nullptr, nullptr);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
std::vector<ProclamationRecord> DatabaseManager::getAllProclamations() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
std::vector<ProclamationRecord> result;
|
||||
if (!db_) return result;
|
||||
|
||||
const char* sql = "SELECT id, title, content, solfeggio_freq_hz, category, is_active, "
|
||||
"selected, category_id, solfeggio_frequency_id, required_level_id FROM proclamations;";
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
|
||||
return result;
|
||||
}
|
||||
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
result.push_back(readProclamationRecord(stmt, 0));
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<ProclamationCategoryRecord> DatabaseManager::getAllCategories() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
std::vector<ProclamationCategoryRecord> result;
|
||||
if (!db_) return result;
|
||||
|
||||
const char* sql = "SELECT id, name, parent_id, sort_order FROM proclamation_categories "
|
||||
"ORDER BY sort_order ASC, id ASC;";
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
|
||||
return result;
|
||||
}
|
||||
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
ProclamationCategoryRecord rec;
|
||||
rec.id = sqlite3_column_int(stmt, 0);
|
||||
rec.name = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1));
|
||||
rec.parent_id = sqlite3_column_int(stmt, 2);
|
||||
rec.sort_order = sqlite3_column_int(stmt, 3);
|
||||
result.push_back(rec);
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool DatabaseManager::setProclamationSelected(const std::string& id, bool selected) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (!db_) return false;
|
||||
|
||||
const char* sql = "UPDATE proclamations SET selected = ? WHERE id = ?;";
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
|
||||
return false;
|
||||
}
|
||||
|
||||
sqlite3_bind_int(stmt, 1, selected ? 1 : 0);
|
||||
sqlite3_bind_text(stmt, 2, id.c_str(), -1, SQLITE_TRANSIENT);
|
||||
|
||||
int rc = sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
return rc == SQLITE_DONE;
|
||||
}
|
||||
|
||||
bool DatabaseManager::saveSelectedProclamations(const std::vector<std::string>& selectedIds,
|
||||
const std::string& defaultProclamationId) {
|
||||
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_, "UPDATE proclamations SET selected = 0;", nullptr, nullptr, nullptr) != SQLITE_OK) {
|
||||
sqlite3_exec(db_, "ROLLBACK;", nullptr, nullptr, nullptr);
|
||||
return false;
|
||||
}
|
||||
|
||||
const char* sql = "UPDATE proclamations SET selected = 1 WHERE id = ?;";
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
|
||||
sqlite3_exec(db_, "ROLLBACK;", nullptr, nullptr, nullptr);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const auto& id : selectedIds) {
|
||||
sqlite3_bind_text(stmt, 1, id.c_str(), -1, SQLITE_TRANSIENT);
|
||||
if (sqlite3_step(stmt) != SQLITE_DONE) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
sqlite3_reset(stmt);
|
||||
sqlite3_clear_bindings(stmt);
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
if (ok) {
|
||||
ok = ensureDefaultProclamationSelectedUnlocked(defaultProclamationId);
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
sqlite3_exec(db_, "COMMIT;", nullptr, nullptr, nullptr);
|
||||
} else {
|
||||
sqlite3_exec(db_, "ROLLBACK;", nullptr, nullptr, nullptr);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool DatabaseManager::ensureDefaultProclamationSelected(const std::string& defaultProclamationId) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return ensureDefaultProclamationSelectedUnlocked(defaultProclamationId);
|
||||
}
|
||||
|
||||
bool DatabaseManager::ensureDefaultProclamationSelectedUnlocked(const std::string& defaultProclamationId) {
|
||||
if (!db_) return false;
|
||||
|
||||
const char* sql = "UPDATE proclamations SET selected = 1 WHERE id = ?;";
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
|
||||
return false;
|
||||
}
|
||||
|
||||
sqlite3_bind_text(stmt, 1, defaultProclamationId.c_str(), -1, SQLITE_TRANSIENT);
|
||||
|
||||
int rc = sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
return rc == SQLITE_DONE;
|
||||
}
|
||||
|
||||
// ─── Asset Downloads ───────────────────────────────────────────────────
|
||||
|
||||
bool DatabaseManager::updateAssetDownload(const AssetDownloadRecord& record) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (!db_) return false;
|
||||
|
||||
const char* sql = "INSERT INTO asset_downloads (id, filename, url, status, bytes_downloaded, total_bytes, error_message, updated_at) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) "
|
||||
"ON CONFLICT(id) DO UPDATE SET filename=excluded.filename, url=excluded.url, status=excluded.status, "
|
||||
"bytes_downloaded=excluded.bytes_downloaded, total_bytes=excluded.total_bytes, error_message=excluded.error_message, "
|
||||
"updated_at=CURRENT_TIMESTAMP;";
|
||||
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
|
||||
return false;
|
||||
}
|
||||
|
||||
sqlite3_bind_text(stmt, 1, record.id.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 2, record.filename.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 3, record.url.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 4, record.status.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_int64(stmt, 5, record.bytes_downloaded);
|
||||
sqlite3_bind_int64(stmt, 6, record.total_bytes);
|
||||
sqlite3_bind_text(stmt, 7, record.error_message.c_str(), -1, SQLITE_TRANSIENT);
|
||||
|
||||
int rc = sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
return rc == SQLITE_DONE;
|
||||
}
|
||||
|
||||
std::vector<AssetDownloadRecord> DatabaseManager::getPendingAssetDownloads() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
std::vector<AssetDownloadRecord> result;
|
||||
if (!db_) return result;
|
||||
|
||||
const char* sql = "SELECT id, filename, url, status, bytes_downloaded, total_bytes, error_message FROM asset_downloads WHERE status = 'pending';";
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
|
||||
return result;
|
||||
}
|
||||
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
AssetDownloadRecord rec;
|
||||
rec.id = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
|
||||
rec.filename = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1));
|
||||
rec.url = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 2));
|
||||
rec.status = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 3));
|
||||
rec.bytes_downloaded = sqlite3_column_int64(stmt, 4);
|
||||
rec.total_bytes = sqlite3_column_int64(stmt, 5);
|
||||
rec.error_message = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 6));
|
||||
|
||||
result.push_back(rec);
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
return result;
|
||||
}
|
||||
|
||||
// ─── Event Logging ─────────────────────────────────────────────────────
|
||||
|
||||
bool DatabaseManager::logEvent(const std::string& level, const std::string& component, const std::string& message) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (!db_) return false;
|
||||
|
||||
const char* sql = "INSERT INTO event_logs (level, component, message) VALUES (?, ?, ?);";
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
|
||||
return false;
|
||||
}
|
||||
|
||||
sqlite3_bind_text(stmt, 1, level.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 2, component.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 3, message.c_str(), -1, SQLITE_TRANSIENT);
|
||||
|
||||
int rc = sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
return rc == SQLITE_DONE;
|
||||
}
|
||||
|
||||
// ─── Color Palettes Table ──────────────────────────────────────────────
|
||||
|
||||
static std::vector<PaletteColor> readPaletteUnlocked(sqlite3* db, int solfeggio_frequency_id) {
|
||||
std::vector<PaletteColor> result;
|
||||
if (!db) return result;
|
||||
|
||||
const char* sql = "SELECT hex FROM color_palettes WHERE solfeggio_frequency_id = ? "
|
||||
"ORDER BY color_index ASC;";
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) {
|
||||
return result;
|
||||
}
|
||||
|
||||
sqlite3_bind_int(stmt, 1, solfeggio_frequency_id);
|
||||
|
||||
auto parseHex = [](const std::string& hex) -> PaletteColor {
|
||||
PaletteColor c;
|
||||
auto nibble = [&hex](size_t i) -> int {
|
||||
char ch = hex.size() > i ? hex[i] : '0';
|
||||
if (ch >= '0' && ch <= '9') return ch - '0';
|
||||
if (ch >= 'a' && ch <= 'f') return ch - 'a' + 10;
|
||||
if (ch >= 'A' && ch <= 'F') return ch - 'A' + 10;
|
||||
return 0;
|
||||
};
|
||||
// Accept "#RRGGBB" or "RRGGBB".
|
||||
size_t base = (!hex.empty() && hex[0] == '#') ? 1 : 0;
|
||||
c.r = static_cast<uint8_t>(nibble(base) * 16 + nibble(base + 1));
|
||||
c.g = static_cast<uint8_t>(nibble(base + 2) * 16 + nibble(base + 3));
|
||||
c.b = static_cast<uint8_t>(nibble(base + 4) * 16 + nibble(base + 5));
|
||||
return c;
|
||||
};
|
||||
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
const unsigned char* text = sqlite3_column_text(stmt, 0);
|
||||
if (text) {
|
||||
result.push_back(parseHex(reinterpret_cast<const char*>(text)));
|
||||
}
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<PaletteColor> DatabaseManager::getPaletteForFrequencyId(int solfeggio_frequency_id) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (!db_) return {};
|
||||
|
||||
std::vector<PaletteColor> result = readPaletteUnlocked(db_, solfeggio_frequency_id);
|
||||
if (result.empty()) {
|
||||
result = readPaletteUnlocked(db_, 5); // 528 Hz fallback
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool DatabaseManager::replaceColorPalettes(const std::vector<PaletteRecord>& 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 color_palettes;", nullptr, nullptr, nullptr) != SQLITE_OK) {
|
||||
sqlite3_exec(db_, "ROLLBACK;", nullptr, nullptr, nullptr);
|
||||
return false;
|
||||
}
|
||||
|
||||
const char* insSql = "INSERT INTO color_palettes (solfeggio_frequency_id, color_index, hex) "
|
||||
"VALUES (?, ?, ?);";
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_, insSql, -1, &stmt, nullptr) != SQLITE_OK) {
|
||||
sqlite3_exec(db_, "ROLLBACK;", nullptr, nullptr, nullptr);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const auto& rec : records) {
|
||||
for (size_t i = 0; i < rec.colors.size(); ++i) {
|
||||
const PaletteColor& c = rec.colors[i];
|
||||
char hex[8];
|
||||
std::snprintf(hex, sizeof(hex), "#%02x%02x%02x", c.r, c.g, c.b);
|
||||
|
||||
sqlite3_bind_int(stmt, 1, rec.solfeggio_frequency_id);
|
||||
sqlite3_bind_int(stmt, 2, static_cast<int>(i));
|
||||
sqlite3_bind_text(stmt, 3, hex, -1, SQLITE_TRANSIENT);
|
||||
if (sqlite3_step(stmt) != SQLITE_DONE) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
sqlite3_reset(stmt);
|
||||
sqlite3_clear_bindings(stmt);
|
||||
}
|
||||
if (!ok) break;
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
if (ok) {
|
||||
sqlite3_exec(db_, "COMMIT;", nullptr, nullptr, nullptr);
|
||||
} else {
|
||||
sqlite3_exec(db_, "ROLLBACK;", nullptr, nullptr, nullptr);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
// ─── Audio Files Table ─────────────────────────────────────────────────
|
||||
|
||||
bool DatabaseManager::syncAudioFiles(const std::vector<AudioFileRecord>& records) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (!db_) return false;
|
||||
|
||||
sqlite3_exec(db_, "BEGIN;", nullptr, nullptr, nullptr);
|
||||
bool ok = true;
|
||||
|
||||
// Upsert each record by filename. The manifest is the source of truth for
|
||||
// display_name / display_order / solfeggio_frequency_id, so all are
|
||||
// refreshed on every sync.
|
||||
const char* upsertSql =
|
||||
"INSERT INTO audio_files (display_name, filename, display_order, solfeggio_frequency_id) "
|
||||
"VALUES (?, ?, ?, ?) "
|
||||
"ON CONFLICT(filename) DO UPDATE SET "
|
||||
"display_name=excluded.display_name, display_order=excluded.display_order, "
|
||||
"solfeggio_frequency_id=excluded.solfeggio_frequency_id;";
|
||||
|
||||
for (const auto& rec : records) {
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_, upsertSql, -1, &stmt, nullptr) != SQLITE_OK) {
|
||||
ok = false;
|
||||
continue;
|
||||
}
|
||||
sqlite3_bind_text(stmt, 1, rec.display_name.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 2, rec.filename.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_int(stmt, 3, rec.display_order);
|
||||
sqlite3_bind_int(stmt, 4, rec.solfeggio_frequency_id);
|
||||
if (sqlite3_step(stmt) != SQLITE_DONE) ok = false;
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
// Delete rows whose filename is not part of the provided set.
|
||||
sqlite3_stmt* delStmt = nullptr;
|
||||
if (records.empty()) {
|
||||
ok = ok && executeSql("DELETE FROM audio_files;");
|
||||
} else {
|
||||
std::string deleteSql = "DELETE FROM audio_files WHERE filename NOT IN (";
|
||||
for (size_t i = 0; i < records.size(); ++i) {
|
||||
if (i > 0) deleteSql += ", ";
|
||||
deleteSql += "?";
|
||||
}
|
||||
deleteSql += ");";
|
||||
|
||||
if (sqlite3_prepare_v2(db_, deleteSql.c_str(), -1, &delStmt, nullptr) == SQLITE_OK) {
|
||||
for (size_t i = 0; i < records.size(); ++i) {
|
||||
sqlite3_bind_text(delStmt, static_cast<int>(i + 1), records[i].filename.c_str(), -1, SQLITE_TRANSIENT);
|
||||
}
|
||||
if (sqlite3_step(delStmt) != SQLITE_DONE) ok = false;
|
||||
sqlite3_finalize(delStmt);
|
||||
} else {
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
sqlite3_exec(db_, "COMMIT;", nullptr, nullptr, nullptr);
|
||||
return ok;
|
||||
}
|
||||
|
||||
std::vector<AudioFileRecord> DatabaseManager::getAudioFiles() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
std::vector<AudioFileRecord> result;
|
||||
if (!db_) return result;
|
||||
|
||||
const char* sql = "SELECT id, display_name, filename, display_order, solfeggio_frequency_id "
|
||||
"FROM audio_files ORDER BY display_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) {
|
||||
AudioFileRecord rec;
|
||||
rec.id = sqlite3_column_int(stmt, 0);
|
||||
rec.display_name = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1));
|
||||
rec.filename = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 2));
|
||||
rec.display_order = sqlite3_column_int(stmt, 3);
|
||||
rec.solfeggio_frequency_id = sqlite3_column_int(stmt, 4);
|
||||
result.push_back(rec);
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace radiantwave::db
|
||||
@@ -0,0 +1,161 @@
|
||||
#pragma once
|
||||
|
||||
#include <sqlite3.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <mutex>
|
||||
|
||||
namespace radiantwave::db {
|
||||
|
||||
struct ProclamationRecord {
|
||||
std::string id;
|
||||
std::string title;
|
||||
std::string content;
|
||||
double solfeggio_freq_hz = 528.0;
|
||||
std::string category = "general";
|
||||
bool is_active = true;
|
||||
bool selected = false;
|
||||
int category_id = 0; // maps to ProclamationCategoryRecord::id
|
||||
int solfeggio_frequency_id = 0; // maps to the solfeggio frequency preset
|
||||
int required_level_id = 1; // minimum SubscriptionLevel id to unlock it
|
||||
};
|
||||
|
||||
struct ProclamationCategoryRecord {
|
||||
int id = 0;
|
||||
std::string name;
|
||||
int parent_id = 0; // 0 = root category
|
||||
int sort_order = 0;
|
||||
};
|
||||
|
||||
struct AssetDownloadRecord {
|
||||
std::string id;
|
||||
std::string filename;
|
||||
std::string url;
|
||||
std::string status = "pending"; // pending, downloading, completed, failed
|
||||
int64_t bytes_downloaded = 0;
|
||||
int64_t total_bytes = 0;
|
||||
std::string error_message = "";
|
||||
};
|
||||
|
||||
struct AudioFileRecord {
|
||||
int id = 0;
|
||||
std::string display_name;
|
||||
std::string filename;
|
||||
int display_order = 0;
|
||||
int solfeggio_frequency_id = 0; // 0 = not associated with a solfeggio frequency
|
||||
};
|
||||
|
||||
// A single color in a solfeggio frequency's color palette (RGBA).
|
||||
struct PaletteColor {
|
||||
uint8_t r = 0;
|
||||
uint8_t g = 0;
|
||||
uint8_t b = 0;
|
||||
uint8_t a = 255;
|
||||
};
|
||||
|
||||
// The ordered palette for one solfeggio frequency (as stored in the
|
||||
// color_palettes table, keyed by color_index).
|
||||
struct PaletteRecord {
|
||||
int solfeggio_frequency_id = 0;
|
||||
std::vector<PaletteColor> colors;
|
||||
};
|
||||
|
||||
class DatabaseManager {
|
||||
public:
|
||||
DatabaseManager();
|
||||
~DatabaseManager();
|
||||
|
||||
// Non-copyable
|
||||
DatabaseManager(const DatabaseManager&) = delete;
|
||||
DatabaseManager& operator=(const DatabaseManager&) = delete;
|
||||
|
||||
// Initialize & open database at path (default: ~/.local/radiantwave/radiantwave.db)
|
||||
bool initialize(const std::string& custom_path = "");
|
||||
|
||||
// Close database connection
|
||||
void close();
|
||||
|
||||
// True once initialize() has opened the database.
|
||||
bool isOpen() const;
|
||||
|
||||
// ── Key-Value Storage (system_kv table) ───────────────────────────
|
||||
bool setKV(const std::string& key, const std::string& value);
|
||||
std::string getKV(const std::string& key, const std::string& defaultValue = "") const;
|
||||
bool hasKV(const std::string& key);
|
||||
bool deleteKV(const std::string& key);
|
||||
|
||||
// ── Proclamations Table ───────────────────────────────────────────
|
||||
bool saveProclamation(const ProclamationRecord& record);
|
||||
std::vector<ProclamationRecord> getActiveProclamations();
|
||||
|
||||
// Full sync: replaces all categories with the given set.
|
||||
bool replaceCategories(const std::vector<ProclamationCategoryRecord>& records);
|
||||
|
||||
// Full sync: upserts proclamations, deletes any not in `records`,
|
||||
// preserves the existing `selected` state, and forces the proclamation
|
||||
// identified by `defaultProclamationId` to remain selected.
|
||||
bool syncProclamations(const std::vector<ProclamationRecord>& records,
|
||||
const std::string& defaultProclamationId);
|
||||
|
||||
// Fetch all proclamations (regardless of is_active).
|
||||
std::vector<ProclamationRecord> getAllProclamations();
|
||||
|
||||
// Fetch all categories ordered by sort_order then id.
|
||||
std::vector<ProclamationCategoryRecord> getAllCategories();
|
||||
|
||||
// Toggle a single proclamation's selected flag.
|
||||
bool setProclamationSelected(const std::string& id, bool selected);
|
||||
|
||||
// Atomically replace the selection with `selectedIds`, then force the
|
||||
// default proclamation to remain selected.
|
||||
bool saveSelectedProclamations(const std::vector<std::string>& selectedIds,
|
||||
const std::string& defaultProclamationId);
|
||||
|
||||
// Force the default proclamation to be flagged selected in the database.
|
||||
bool ensureDefaultProclamationSelected(const std::string& defaultProclamationId);
|
||||
|
||||
// ── Asset Downloads Table ─────────────────────────────────────────
|
||||
bool updateAssetDownload(const AssetDownloadRecord& record);
|
||||
std::vector<AssetDownloadRecord> getPendingAssetDownloads();
|
||||
|
||||
// ── Audio Files Table ─────────────────────────────────────────────
|
||||
// Reconciles audio_files to the given set (the on-disk audio files).
|
||||
// Upserts by filename with the manifest as source of truth for
|
||||
// display_name / display_order / solfeggio_frequency_id, and deletes
|
||||
// any row whose filename is not in `records`.
|
||||
bool syncAudioFiles(const std::vector<AudioFileRecord>& records);
|
||||
std::vector<AudioFileRecord> getAudioFiles();
|
||||
|
||||
// ── Color Palettes Table ──────────────────────────────────────────
|
||||
// Returns the 7-color palette for a solfeggio frequency id, ordered by
|
||||
// color_index. Falls back to the 528 Hz palette when the id has no rows.
|
||||
std::vector<PaletteColor> getPaletteForFrequencyId(int solfeggio_frequency_id);
|
||||
|
||||
// Full sync: replaces the entire color_palettes table with `records`
|
||||
// (the portal is the source of truth for palettes). Pass an empty vector
|
||||
// to clear all palettes. Colors are stored as "#RRGGBB" hex strings.
|
||||
//
|
||||
// NOTE: not currently called from the app flow. This is readiness code so
|
||||
// palettes can be pushed from the portal once the API route exists.
|
||||
bool replaceColorPalettes(const std::vector<PaletteRecord>& records);
|
||||
|
||||
// ── Event Logging Table ───────────────────────────────────────────
|
||||
bool logEvent(const std::string& level, const std::string& component, const std::string& message);
|
||||
|
||||
private:
|
||||
std::string resolvePath(const std::string& raw_path) const;
|
||||
bool runMigrations();
|
||||
bool executeSql(const std::string& sql);
|
||||
bool columnExists(const std::string& table, const std::string& column);
|
||||
bool ensureColumn(const std::string& table, const std::string& column,
|
||||
const std::string& column_ddl);
|
||||
void seedColorPalettes();
|
||||
void bindProclamationRecord(sqlite3_stmt* stmt, const ProclamationRecord& record);
|
||||
bool ensureDefaultProclamationSelectedUnlocked(const std::string& defaultProclamationId);
|
||||
|
||||
sqlite3* db_ = nullptr;
|
||||
std::string db_path_;
|
||||
mutable std::mutex mutex_;
|
||||
};
|
||||
|
||||
} // namespace radiantwave::db
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
namespace radiantwave::db {
|
||||
|
||||
// Canonical system_kv keys. These are the single source of truth for where
|
||||
// device configuration lives in the local database.
|
||||
constexpr const char* KV_EMAIL_ADDRESS = "email_address";
|
||||
constexpr const char* KV_LICENSE_KEY = "license_key";
|
||||
constexpr const char* KV_SUBSCRIPTION_LEVEL_ID = "subscription_level_id";
|
||||
constexpr const char* KV_SUBSCRIPTION_LEVEL_NAME = "subscription_level_name";
|
||||
constexpr const char* KV_SUBSCRIPTION_LEVEL_DESCRIPTION = "subscription_level_description";
|
||||
|
||||
// Unix epoch seconds of the last successful license validation against the
|
||||
// portal. Used to grant a 7-day offline grace window so transient network
|
||||
// outages do not revoke a known-good license.
|
||||
constexpr const char* KV_LICENSE_LAST_VERIFIED = "license_last_verified";
|
||||
|
||||
} // namespace radiantwave::db
|
||||
Reference in New Issue
Block a user