diff --git a/src/audio/include/track_queue.hpp b/src/audio/include/track_queue.hpp index 9d5ef5b2..bec887ae 100644 --- a/src/audio/include/track_queue.hpp +++ b/src/audio/include/track_queue.hpp @@ -86,6 +86,9 @@ class TrackQueue { */ auto Clear(Editor&) -> void; + auto Save(std::weak_ptr) -> void; + auto Load(std::weak_ptr) -> void; + // Cannot be copied or moved. TrackQueue(const TrackQueue&) = delete; TrackQueue& operator=(const TrackQueue&) = delete; diff --git a/src/audio/track_queue.cpp b/src/audio/track_queue.cpp index b3a128b2..eb761590 100644 --- a/src/audio/track_queue.cpp +++ b/src/audio/track_queue.cpp @@ -5,6 +5,7 @@ */ #include "track_queue.hpp" +#include #include #include @@ -13,6 +14,8 @@ #include "audio_events.hpp" #include "audio_fsm.hpp" +#include "cppbor.h" +#include "cppbor_parse.h" #include "database.hpp" #include "event_queue.hpp" #include "memory_resource.hpp" @@ -24,6 +27,11 @@ namespace audio { [[maybe_unused]] static constexpr char kTag[] = "tracks"; +static const std::string kSerialiseKey = "queue"; +static const std::string kCurrentKey = "cur"; +static const std::string kPlayedKey = "prev"; +static const std::string kEnqueuedKey = "next"; + TrackQueue::Editor::Editor(TrackQueue& queue) : lock_(queue.mutex_), has_current_changed_(false) {} @@ -227,4 +235,177 @@ auto TrackQueue::Clear(Editor& ed) -> void { enqueued_.clear(); } +auto TrackQueue::Save(std::weak_ptr db) -> void { + cppbor::Map root{}; + + if (current_) { + root.add(cppbor::Bstr{kCurrentKey}, cppbor::Uint{*current_}); + } + + cppbor::Array played{}; + for (const auto& id : played_) { + played.add(cppbor::Uint{id}); + } + root.add(cppbor::Bstr{kPlayedKey}, std::move(played)); + + cppbor::Array enqueued{}; + for (const auto& item : enqueued_) { + std::visit( + [&](auto&& arg) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + enqueued.add(cppbor::Uint{arg}); + } else if constexpr (std::is_same_v) { + enqueued.add(arg.cbor()); + } + }, + item); + } + root.add(cppbor::Bstr{kEnqueuedKey}, std::move(enqueued)); + + auto db_lock = db.lock(); + if (!db_lock) { + return; + } + db_lock->Put(kSerialiseKey, root.toString()); +} + +class Parser : public cppbor::ParseClient { + public: + Parser(std::weak_ptr db, + std::optional& current, + std::pmr::vector& played, + std::pmr::vector& enqueued) + : state_(State::kInit), + db_(db), + current_(current), + played_(played), + enqueued_(enqueued) {} + + virtual ParseClient* item(std::unique_ptr& item, + const uint8_t* hdrBegin, + const uint8_t* valueBegin, + const uint8_t* end) override { + switch (state_) { + case State::kInit: + if (item->type() == cppbor::MAP) { + state_ = State::kRoot; + } + break; + case State::kRoot: + if (item->type() != cppbor::TSTR) { + break; + } + if (item->asTstr()->value() == kCurrentKey) { + state_ = State::kCurrent; + } else if (item->asTstr()->value() == kPlayedKey) { + state_ = State::kPlayed; + } else if (item->asTstr()->value() == kEnqueuedKey) { + state_ = State::kEnqueued; + } + break; + case State::kCurrent: + if (item->type() == cppbor::UINT) { + current_ = item->asUint()->value(); + } + state_ = State::kRoot; + break; + case State::kPlayed: + if (item->type() == cppbor::UINT) { + played_.push_back(item->asUint()->value()); + } + break; + case State::kEnqueued: + if (item->type() == cppbor::UINT) { + played_.push_back(item->asUint()->value()); + } else if (item->type() == cppbor::ARRAY) { + queue_depth_ = 1; + state_ = State::kEnqueuedIterator; + } + break; + case State::kEnqueuedIterator: + if (item->type() == cppbor::MAP || item->type() == cppbor::ARRAY) { + queue_depth_++; + } + break; + case State::kFinished: + break; + } + + return this; + } + + ParseClient* itemEnd(std::unique_ptr& item, + const uint8_t* hdrBegin, + const uint8_t* valueBegin, + const uint8_t* end) override { + std::optional parsed_it; + switch (state_) { + case State::kInit: + case State::kRoot: + case State::kCurrent: + state_ = State::kFinished; + break; + case State::kEnqueued: + case State::kPlayed: + state_ = State::kRoot; + break; + case State::kEnqueuedIterator: + if (item->type() == cppbor::MAP || item->type() == cppbor::ARRAY) { + queue_depth_++; + } + if (queue_depth_ == 0) { + parsed_it = database::TrackIterator::Parse(db_, *item->asArray()); + if (parsed_it) { + enqueued_.push_back(std::move(*parsed_it)); + } + } + state_ = State::kEnqueued; + break; + case State::kFinished: + break; + } + return this; + } + + void error(const uint8_t* position, + const std::string& errorMessage) override { + ESP_LOGE(kTag, "restoring saved queue failed: %s", errorMessage.c_str()); + } + + private: + enum class State { + kInit, + kRoot, + kCurrent, + kPlayed, + kEnqueued, + kEnqueuedIterator, + kFinished, + } state_; + + std::weak_ptr db_; + + int queue_depth_; + + std::optional& current_; + std::pmr::vector& played_; + std::pmr::vector& enqueued_; +}; + +auto TrackQueue::Load(std::weak_ptr db) -> void { + auto db_lock = db.lock(); + if (!db_lock) { + return; + } + auto raw = db_lock->Get(kSerialiseKey); + if (!raw) { + return; + } + + Parser p{db, current_, played_, enqueued_}; + const uint8_t* data = reinterpret_cast(raw->data()); + cppbor::parse(data, data + raw->size(), &p); +} + } // namespace audio diff --git a/src/database/database.cpp b/src/database/database.cpp index 03451c05..e646154e 100644 --- a/src/database/database.cpp +++ b/src/database/database.cpp @@ -18,6 +18,7 @@ #include #include "collation.hpp" +#include "cppbor.h" #include "esp_log.h" #include "ff.h" #include "freertos/projdefs.h" @@ -52,6 +53,8 @@ static const char kDbPath[] = "/.tangara-db"; static const char kKeyDbVersion[] = "schema_version"; static const uint8_t kCurrentDbVersion = 3; + +static const char kKeyCustom[] = "U\0"; static const char kKeyCollator[] = "collator"; static const char kKeyTrackId[] = "next_track_id"; @@ -197,6 +200,19 @@ Database::~Database() { sIsDbOpen.store(false); } +auto Database::Put(const std::string& key, const std::string& val) -> void { + db_->Put(leveldb::WriteOptions{}, kKeyCustom + key, val); +} + +auto Database::Get(const std::string& key) -> std::optional { + std::string val; + auto res = db_->Get(leveldb::ReadOptions{}, kKeyCustom + key, &val); + if (!res.ok()) { + return {}; + } + return val; +} + auto Database::Update() -> std::future { events::Ui().Dispatch(event::UpdateStarted{}); return worker_task_->Dispatch([&]() -> void { @@ -883,6 +899,45 @@ Iterator::Iterator(std::weak_ptr db, const IndexInfo& idx) .page_size = 1}; } +auto Iterator::Parse(std::weak_ptr db, const cppbor::Array& encoded) + -> std::optional { + // Ensure the input looks reasonable. + if (encoded.size() != 3) { + return {}; + } + + if (encoded[0]->type() != cppbor::TSTR) { + return {}; + } + const std::string& prefix = encoded[0]->asTstr()->value(); + + std::optional current_pos{}; + if (encoded[1]->type() == cppbor::TSTR) { + const std::string& key = encoded[1]->asTstr()->value(); + current_pos = Continuation{ + .prefix = {prefix.data(), prefix.size()}, + .start_key = {key.data(), key.size()}, + .forward = true, + .was_prev_forward = true, + .page_size = 1, + }; + } + + std::optional prev_pos{}; + if (encoded[2]->type() == cppbor::TSTR) { + const std::string& key = encoded[2]->asTstr()->value(); + current_pos = Continuation{ + .prefix = {prefix.data(), prefix.size()}, + .start_key = {key.data(), key.size()}, + .forward = false, + .was_prev_forward = false, + .page_size = 1, + }; + } + + return Iterator{db, std::move(current_pos), std::move(prev_pos)}; +} + Iterator::Iterator(std::weak_ptr db, const Continuation& c) : db_(db), pos_mutex_(), current_pos_(c), prev_pos_() {} @@ -892,6 +947,11 @@ Iterator::Iterator(const Iterator& other) current_pos_(other.current_pos_), prev_pos_(other.prev_pos_) {} +Iterator::Iterator(std::weak_ptr db, + std::optional&& cur, + std::optional&& prev) + : db_(db), current_pos_(cur), prev_pos_(prev) {} + Iterator& Iterator::operator=(const Iterator& other) { current_pos_ = other.current_pos_; prev_pos_ = other.prev_pos_; @@ -995,6 +1055,53 @@ auto Iterator::InvokeNull(Callback cb) -> void { std::invoke(cb, std::optional{}); } +auto Iterator::cbor() const -> cppbor::Array&& { + cppbor::Array res; + + std::pmr::string prefix; + if (current_pos_) { + prefix = current_pos_->prefix; + } else if (prev_pos_) { + prefix = prev_pos_->prefix; + } else { + ESP_LOGW(kTag, "iterator has no prefix"); + return std::move(res); + } + + if (current_pos_) { + res.add(cppbor::Tstr(current_pos_->start_key)); + } else { + res.add(cppbor::Null()); + } + + if (prev_pos_) { + res.add(cppbor::Tstr(prev_pos_->start_key)); + } else { + res.add(cppbor::Null()); + } + + return std::move(res); +} + +auto TrackIterator::Parse(std::weak_ptr db, + const cppbor::Array& encoded) + -> std::optional { + TrackIterator ret{db}; + + for (const auto& item : encoded) { + if (item->type() == cppbor::ARRAY) { + auto it = Iterator::Parse(db, *item->asArray()); + if (it) { + ret.levels_.push_back(std::move(*it)); + } else { + return {}; + } + } + } + + return ret; +} + TrackIterator::TrackIterator(const Iterator& it) : db_(it.db_), levels_() { if (it.current_pos_) { levels_.push_back(it); @@ -1005,6 +1112,8 @@ TrackIterator::TrackIterator(const Iterator& it) : db_(it.db_), levels_() { TrackIterator::TrackIterator(const TrackIterator& other) : db_(other.db_), levels_(other.levels_) {} +TrackIterator::TrackIterator(std::weak_ptr db) : db_(db), levels_() {} + TrackIterator& TrackIterator::operator=(TrackIterator&& other) { levels_ = std::move(other.levels_); return *this; @@ -1057,4 +1166,12 @@ auto TrackIterator::NextLeaf() -> void { } } +auto TrackIterator::cbor() const -> cppbor::Array&& { + cppbor::Array res; + for (const auto& i : levels_) { + res.add(i.cbor()); + } + return std::move(res); +} + } // namespace database diff --git a/src/database/include/database.hpp b/src/database/include/database.hpp index 263153fb..327db3cb 100644 --- a/src/database/include/database.hpp +++ b/src/database/include/database.hpp @@ -18,6 +18,7 @@ #include #include "collation.hpp" +#include "cppbor.h" #include "file_gatherer.hpp" #include "index.hpp" #include "leveldb/cache.h" @@ -105,6 +106,9 @@ class Database { ~Database(); + auto Put(const std::string& key, const std::string& val) -> void; + auto Get(const std::string& key) -> std::optional; + auto Update() -> std::future; auto GetTrackPath(TrackId id) -> std::future>; @@ -194,6 +198,9 @@ auto Database::ParseRecord(const leveldb::Slice& key, */ class Iterator { public: + static auto Parse(std::weak_ptr, const cppbor::Array&) + -> std::optional; + Iterator(std::weak_ptr, const IndexInfo&); Iterator(std::weak_ptr, const Continuation&); Iterator(const Iterator&); @@ -213,7 +220,13 @@ class Iterator { auto Size() const -> size_t; + auto cbor() const -> cppbor::Array&&; + private: + Iterator(std::weak_ptr, + std::optional&&, + std::optional&&); + friend class TrackIterator; auto InvokeNull(Callback) -> void; @@ -227,6 +240,9 @@ class Iterator { class TrackIterator { public: + static auto Parse(std::weak_ptr, const cppbor::Array&) + -> std::optional; + TrackIterator(const Iterator&); TrackIterator(const TrackIterator&); @@ -235,7 +251,11 @@ class TrackIterator { auto Next() -> std::optional; auto Size() const -> size_t; + auto cbor() const -> cppbor::Array&&; + private: + TrackIterator(std::weak_ptr); + auto NextLeaf() -> void; std::weak_ptr db_;