summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/app_console/app_console.cpp101
-rw-r--r--src/audio/CMakeLists.txt2
-rw-r--r--src/audio/audio_fsm.cpp40
-rw-r--r--src/audio/fatfs_audio_input.cpp32
-rw-r--r--src/audio/include/audio_events.hpp2
-rw-r--r--src/audio/include/audio_fsm.hpp2
-rw-r--r--src/audio/include/fatfs_audio_input.hpp9
-rw-r--r--src/audio/include/track_queue.hpp60
-rw-r--r--src/audio/track_queue.cpp439
-rw-r--r--src/database/database.cpp1028
-rw-r--r--src/database/file_gatherer.cpp10
-rw-r--r--src/database/include/database.hpp229
-rw-r--r--src/database/include/file_gatherer.hpp8
-rw-r--r--src/database/include/tag_parser.hpp14
-rw-r--r--src/database/include/track.hpp2
-rw-r--r--src/database/tag_parser.cpp24
-rw-r--r--src/lua/bridge.cpp15
-rw-r--r--src/lua/lua_database.cpp144
-rw-r--r--src/lua/lua_queue.cpp12
-rw-r--r--src/playlist/CMakeLists.txt10
-rw-r--r--src/playlist/include/shuffler.hpp68
-rw-r--r--src/playlist/include/source.hpp157
-rw-r--r--src/playlist/shuffler.cpp166
-rw-r--r--src/playlist/source.cpp360
-rw-r--r--src/system_fsm/booting.cpp3
-rw-r--r--src/ui/include/ui_events.hpp12
-rw-r--r--src/ui/include/ui_fsm.hpp1
-rw-r--r--src/ui/ui_fsm.cpp22
28 files changed, 682 insertions, 2290 deletions
diff --git a/src/app_console/app_console.cpp b/src/app_console/app_console.cpp
index 7f576a01..b5698792 100644
--- a/src/app_console/app_console.cpp
+++ b/src/app_console/app_console.cpp
@@ -123,8 +123,7 @@ int CmdPlayFile(int argc, char** argv) {
if (is_id) {
database::TrackId id = std::atoi(argv[1]);
- auto editor = AppConsole::sServices->track_queue().Edit();
- AppConsole::sServices->track_queue().Append(editor, id);
+ AppConsole::sServices->track_queue().append(id);
} else {
std::pmr::string path{&memory::kSpiRamResource};
path += '/';
@@ -134,7 +133,8 @@ int CmdPlayFile(int argc, char** argv) {
path += argv[i];
}
- events::Audio().Dispatch(audio::PlayFile{.filename = path});
+ events::Audio().Dispatch(
+ audio::PlayFile{.filename = {path.data(), path.size()}});
}
return 0;
@@ -161,7 +161,7 @@ int CmdDbInit(int argc, char** argv) {
std::cout << "no database open" << std::endl;
return 1;
}
- db->Update();
+ db->updateIndexes();
return 0;
}
@@ -176,87 +176,6 @@ void RegisterDbInit() {
esp_console_cmd_register(&cmd);
}
-int CmdDbTracks(int argc, char** argv) {
- static const std::pmr::string usage = "usage: db_tracks";
- if (argc != 1) {
- std::cout << usage << std::endl;
- return 1;
- }
-
- auto db = AppConsole::sServices->database().lock();
- if (!db) {
- std::cout << "no database open" << std::endl;
- return 1;
- }
- std::unique_ptr<database::Result<database::Track>> res(
- db->GetTracks(20).get());
- while (true) {
- for (const auto& s : res->values()) {
- std::cout << s->tags()[database::Tag::kTitle].value_or("[BLANK]")
- << std::endl;
- }
- if (res->next_page()) {
- auto continuation = res->next_page().value();
- res.reset(db->GetPage<database::Track>(&continuation).get());
- } else {
- break;
- }
- }
-
- return 0;
-}
-
-void RegisterDbTracks() {
- esp_console_cmd_t cmd{.command = "db_tracks",
- .help = "lists titles of ALL tracks in the database",
- .hint = NULL,
- .func = &CmdDbTracks,
- .argtable = NULL};
- esp_console_cmd_register(&cmd);
-}
-
-int CmdDbDump(int argc, char** argv) {
- static const std::pmr::string usage = "usage: db_dump";
- if (argc != 1) {
- std::cout << usage << std::endl;
- return 1;
- }
-
- auto db = AppConsole::sServices->database().lock();
- if (!db) {
- std::cout << "no database open" << std::endl;
- return 1;
- }
-
- std::cout << "=== BEGIN DUMP ===" << std::endl;
-
- std::unique_ptr<database::Result<std::pmr::string>> res(db->GetDump(5).get());
- while (true) {
- for (const auto& s : res->values()) {
- std::cout << *s << std::endl;
- }
- if (res->next_page()) {
- auto continuation = res->next_page().value();
- res.reset(db->GetPage<std::pmr::string>(&continuation).get());
- } else {
- break;
- }
- }
-
- std::cout << "=== END DUMP ===" << std::endl;
-
- return 0;
-}
-
-void RegisterDbDump() {
- esp_console_cmd_t cmd{.command = "db_dump",
- .help = "prints every key/value pair in the db",
- .hint = NULL,
- .func = &CmdDbDump,
- .argtable = NULL};
- esp_console_cmd_register(&cmd);
-}
-
int CmdTasks(int argc, char** argv) {
#if (configUSE_TRACE_FACILITY == 0)
std::cout << "configUSE_TRACE_FACILITY must be enabled" << std::endl;
@@ -270,8 +189,8 @@ int CmdTasks(int argc, char** argv) {
return 1;
}
- // Pad the number of tasks so that uxTaskGetSystemState still returns info if
- // new tasks are started during measurement.
+ // Pad the number of tasks so that uxTaskGetSystemState still returns info
+ // if new tasks are started during measurement.
size_t num_tasks = uxTaskGetNumberOfTasks() + 4;
TaskStatus_t* start_status = new TaskStatus_t[num_tasks];
TaskStatus_t* end_status = new TaskStatus_t[num_tasks];
@@ -576,9 +495,11 @@ int CmdHaptics(int argc, char** argv) {
" haptic_effect from-effect to-effect\n"
" haptic_effect from-effect to-effect library\n"
"eg.\n"
- " haptic_effect (plays from 1 to 123 with the default library)\n"
+ " haptic_effect (plays from 1 to 123 with the default "
+ "library)\n"
" haptic_effect 3 (plays from 1 to 123 with library 3\n"
- " haptic_effect 1 100 (plays from 1 to 100 with the default library)\n"
+ " haptic_effect 1 100 (plays from 1 to 100 with the default "
+ "library)\n"
" haptic_effect 1 10 4 (plays from 1 to 10 with library 4)";
auto& haptics = AppConsole::sServices->haptics();
@@ -643,8 +564,6 @@ auto AppConsole::RegisterExtraComponents() -> void {
RegisterAudioStatus();
*/
RegisterDbInit();
- RegisterDbTracks();
- RegisterDbDump();
RegisterTasks();
RegisterHeaps();
diff --git a/src/audio/CMakeLists.txt b/src/audio/CMakeLists.txt
index 95bab4c2..0f90334b 100644
--- a/src/audio/CMakeLists.txt
+++ b/src/audio/CMakeLists.txt
@@ -9,6 +9,6 @@ idf_component_register(
"audio_source.cpp"
INCLUDE_DIRS "include"
REQUIRES "codecs" "drivers" "cbor" "result" "tasks" "span" "memory" "tinyfsm"
- "database" "system_fsm" "playlist" "speexdsp")
+ "database" "system_fsm" "speexdsp")
target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS})
diff --git a/src/audio/audio_fsm.cpp b/src/audio/audio_fsm.cpp
index ce610abb..3bd4d396 100644
--- a/src/audio/audio_fsm.cpp
+++ b/src/audio/audio_fsm.cpp
@@ -93,6 +93,17 @@ void AudioState::react(const OutputModeChanged& ev) {
sOutput->SetMode(IAudioOutput::Modes::kOnPaused);
}
+auto AudioState::playTrack(database::TrackId id) -> void {
+ sCurrentTrack = id;
+ sServices->bg_worker().Dispatch<void>([=]() {
+ auto db = sServices->database().lock();
+ if (!db) {
+ return;
+ }
+ sFileSource->SetPath(db->getTrackPath(id));
+ });
+}
+
namespace states {
void Uninitialised::react(const system_fsm::BootComplete& ev) {
@@ -143,19 +154,11 @@ void Standby::react(const internal::InputFileOpened& ev) {
}
void Standby::react(const QueueUpdate& ev) {
- auto current_track = sServices->track_queue().Current();
+ auto current_track = sServices->track_queue().current();
if (!current_track || (sCurrentTrack && *sCurrentTrack == *current_track)) {
return;
}
-
- sCurrentTrack = current_track;
-
- auto db = sServices->database().lock();
- if (!db) {
- ESP_LOGW(kTag, "database not open; ignoring play request");
- return;
- }
- sFileSource->SetPath(db->GetTrackPath(*current_track));
+ playTrack(*current_track);
}
void Standby::react(const TogglePlayPause& ev) {
@@ -187,22 +190,14 @@ void Playback::react(const QueueUpdate& ev) {
if (!ev.current_changed) {
return;
}
- auto current_track = sServices->track_queue().Current();
+ auto current_track = sServices->track_queue().current();
if (!current_track) {
sFileSource->SetPath();
sCurrentTrack.reset();
transit<Standby>();
return;
}
-
- sCurrentTrack = current_track;
-
- auto db = sServices->database().lock();
- if (!db) {
- return;
- }
-
- sFileSource->SetPath(db->GetTrackPath(*current_track));
+ playTrack(*current_track);
}
void Playback::react(const TogglePlayPause& ev) {
@@ -220,9 +215,8 @@ void Playback::react(const internal::InputFileClosed& ev) {}
void Playback::react(const internal::InputFileFinished& ev) {
ESP_LOGI(kTag, "finished playing file");
- auto editor = sServices->track_queue().Edit();
- sServices->track_queue().Next(editor);
- if (!sServices->track_queue().Current()) {
+ sServices->track_queue().next();
+ if (!sServices->track_queue().current()) {
transit<Standby>();
}
}
diff --git a/src/audio/fatfs_audio_input.cpp b/src/audio/fatfs_audio_input.cpp
index 5594718f..58d5852f 100644
--- a/src/audio/fatfs_audio_input.cpp
+++ b/src/audio/fatfs_audio_input.cpp
@@ -50,23 +50,19 @@ FatfsAudioInput::FatfsAudioInput(database::ITagParser& tag_parser,
bg_worker_(bg_worker),
new_stream_mutex_(),
new_stream_(),
- has_new_stream_(false),
- pending_path_() {}
+ has_new_stream_(false) {}
FatfsAudioInput::~FatfsAudioInput() {}
-auto FatfsAudioInput::SetPath(std::future<std::optional<std::pmr::string>> fut)
- -> void {
- std::lock_guard<std::mutex> guard{new_stream_mutex_};
- pending_path_.reset(
- new database::FutureFetcher<std::optional<std::pmr::string>>(
- std::move(fut)));
-
- has_new_stream_ = true;
- has_new_stream_.notify_one();
+auto FatfsAudioInput::SetPath(std::optional<std::string> path) -> void {
+ if (path) {
+ SetPath(*path);
+ } else {
+ SetPath();
+ }
}
-auto FatfsAudioInput::SetPath(const std::pmr::string& path) -> void {
+auto FatfsAudioInput::SetPath(const std::string& path) -> void {
std::lock_guard<std::mutex> guard{new_stream_mutex_};
if (OpenFile(path)) {
has_new_stream_ = true;
@@ -96,16 +92,6 @@ auto FatfsAudioInput::NextStream() -> std::shared_ptr<TaggedStream> {
continue;
}
- // If the path is a future, then wait for it to complete.
- if (pending_path_) {
- auto res = pending_path_->Result();
- pending_path_.reset();
-
- if (res && *res) {
- OpenFile(**res);
- }
- }
-
if (new_stream_ == nullptr) {
continue;
}
@@ -117,7 +103,7 @@ auto FatfsAudioInput::NextStream() -> std::shared_ptr<TaggedStream> {
}
}
-auto FatfsAudioInput::OpenFile(const std::pmr::string& path) -> bool {
+auto FatfsAudioInput::OpenFile(const std::string& path) -> bool {
ESP_LOGI(kTag, "opening file %s", path.c_str());
auto tags = tag_parser_.ReadAndParseTags(path);
diff --git a/src/audio/include/audio_events.hpp b/src/audio/include/audio_events.hpp
index 9994a9f6..68efcafb 100644
--- a/src/audio/include/audio_events.hpp
+++ b/src/audio/include/audio_events.hpp
@@ -42,7 +42,7 @@ struct QueueUpdate : tinyfsm::Event {
};
struct PlayFile : tinyfsm::Event {
- std::pmr::string filename;
+ std::string filename;
};
struct StepUpVolume : tinyfsm::Event {};
diff --git a/src/audio/include/audio_fsm.hpp b/src/audio/include/audio_fsm.hpp
index 256e1430..c9fac08b 100644
--- a/src/audio/include/audio_fsm.hpp
+++ b/src/audio/include/audio_fsm.hpp
@@ -60,6 +60,8 @@ class AudioState : public tinyfsm::Fsm<AudioState> {
virtual void react(const internal::AudioPipelineIdle&) {}
protected:
+ auto playTrack(database::TrackId id) -> void;
+
static std::shared_ptr<system_fsm::ServiceLocator> sServices;
static std::shared_ptr<FatfsAudioInput> sFileSource;
diff --git a/src/audio/include/fatfs_audio_input.hpp b/src/audio/include/fatfs_audio_input.hpp
index c7d52ca3..f9635b86 100644
--- a/src/audio/include/fatfs_audio_input.hpp
+++ b/src/audio/include/fatfs_audio_input.hpp
@@ -38,8 +38,8 @@ class FatfsAudioInput : public IAudioSource {
* Immediately cease reading any current source, and begin reading from the
* given file path.
*/
- auto SetPath(std::future<std::optional<std::pmr::string>>) -> void;
- auto SetPath(const std::pmr::string&) -> void;
+ auto SetPath(std::optional<std::string>) -> void;
+ auto SetPath(const std::string&) -> void;
auto SetPath() -> void;
auto HasNewStream() -> bool override;
@@ -49,7 +49,7 @@ class FatfsAudioInput : public IAudioSource {
FatfsAudioInput& operator=(const FatfsAudioInput&) = delete;
private:
- auto OpenFile(const std::pmr::string& path) -> bool;
+ auto OpenFile(const std::string& path) -> bool;
auto ContainerToStreamType(database::Container)
-> std::optional<codecs::StreamType>;
@@ -61,9 +61,6 @@ class FatfsAudioInput : public IAudioSource {
std::shared_ptr<TaggedStream> new_stream_;
std::atomic<bool> has_new_stream_;
-
- std::unique_ptr<database::FutureFetcher<std::optional<std::pmr::string>>>
- pending_path_;
};
} // namespace audio
diff --git a/src/audio/include/track_queue.hpp b/src/audio/include/track_queue.hpp
index bec887ae..4a1984c9 100644
--- a/src/audio/include/track_queue.hpp
+++ b/src/audio/include/track_queue.hpp
@@ -9,10 +9,11 @@
#include <list>
#include <memory>
#include <mutex>
+#include <shared_mutex>
#include <vector>
#include "database.hpp"
-#include "source.hpp"
+#include "tasks.hpp"
#include "track.hpp"
namespace audio {
@@ -32,77 +33,52 @@ namespace audio {
*/
class TrackQueue {
public:
- TrackQueue();
-
- class Editor {
- public:
- ~Editor();
-
- // Cannot be copied or moved.
- Editor(const Editor&) = delete;
- Editor& operator=(const Editor&) = delete;
-
- private:
- friend TrackQueue;
-
- Editor(TrackQueue&);
-
- std::lock_guard<std::recursive_mutex> lock_;
- bool has_current_changed_;
- };
-
- auto Edit() -> Editor;
+ TrackQueue(tasks::Worker& bg_worker);
/* Returns the currently playing track. */
- auto Current() const -> std::optional<database::TrackId>;
+ auto current() const -> std::optional<database::TrackId>;
/* Returns, in order, tracks that have been queued to be played next. */
- auto PeekNext(std::size_t limit) const -> std::vector<database::TrackId>;
+ auto peekNext(std::size_t limit) const -> std::vector<database::TrackId>;
/*
* Returns the tracks in the queue that have already been played, ordered
* most recently played first.
*/
- auto PeekPrevious(std::size_t limit) const -> std::vector<database::TrackId>;
+ auto peekPrevious(std::size_t limit) const -> std::vector<database::TrackId>;
- auto GetCurrentPosition() const -> size_t;
- auto GetTotalSize() const -> size_t;
+ auto currentPosition() const -> size_t;
+ auto totalSize() const -> size_t;
using Item = std::variant<database::TrackId, database::TrackIterator>;
- auto Insert(Editor&, Item, size_t) -> void;
- auto Append(Editor&, Item i) -> void;
+ auto insert(Item) -> void;
+ auto append(Item i) -> void;
/*
* Advances to the next track in the queue, placing the current track at the
* front of the 'played' queue.
*/
- auto Next(Editor&) -> std::optional<database::TrackId>;
- auto Previous(Editor&) -> std::optional<database::TrackId>;
+ auto next() -> void;
+ auto previous() -> void;
- auto SkipTo(Editor&, database::TrackId) -> void;
+ auto skipTo(database::TrackId) -> void;
/*
* Removes all tracks from all queues, and stops any currently playing track.
*/
- auto Clear(Editor&) -> void;
-
- auto Save(std::weak_ptr<database::Database>) -> void;
- auto Load(std::weak_ptr<database::Database>) -> void;
+ auto clear() -> void;
// Cannot be copied or moved.
TrackQueue(const TrackQueue&) = delete;
TrackQueue& operator=(const TrackQueue&) = delete;
private:
- // FIXME: Make this a shared_mutex so that multithread reads don't block.
- mutable std::recursive_mutex mutex_;
+ mutable std::shared_mutex mutex_;
- std::optional<database::TrackId> current_;
+ tasks::Worker& bg_worker_;
- // Note: stored in reverse order, i.e. most recent played it at the *back* of
- // this vector.
- std::pmr::vector<database::TrackId> played_;
- std::pmr::vector<Item> enqueued_;
+ size_t pos_;
+ std::pmr::vector<database::TrackId> tracks_;
};
} // namespace audio
diff --git a/src/audio/track_queue.cpp b/src/audio/track_queue.cpp
index eb761590..c1187107 100644
--- a/src/audio/track_queue.cpp
+++ b/src/audio/track_queue.cpp
@@ -8,6 +8,7 @@
#include <stdint.h>
#include <algorithm>
+#include <memory>
#include <mutex>
#include <optional>
#include <variant>
@@ -19,7 +20,7 @@
#include "database.hpp"
#include "event_queue.hpp"
#include "memory_resource.hpp"
-#include "source.hpp"
+#include "tasks.hpp"
#include "track.hpp"
#include "ui_fsm.hpp"
@@ -27,385 +28,137 @@ 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) {}
-
-TrackQueue::Editor::~Editor() {
- QueueUpdate ev{.current_changed = has_current_changed_};
- events::Audio().Dispatch(ev);
+auto notifyChanged(bool current_changed) -> void {
+ QueueUpdate ev{.current_changed = current_changed};
events::Ui().Dispatch(ev);
+ events::Audio().Dispatch(ev);
}
-TrackQueue::TrackQueue()
+TrackQueue::TrackQueue(tasks::Worker& bg_worker)
: mutex_(),
- current_(),
- played_(&memory::kSpiRamResource),
- enqueued_(&memory::kSpiRamResource) {}
+ bg_worker_(bg_worker),
+ pos_(0),
+ tracks_(&memory::kSpiRamResource) {}
-auto TrackQueue::Edit() -> Editor {
- return Editor(*this);
-}
-
-auto TrackQueue::Current() const -> std::optional<database::TrackId> {
- const std::lock_guard<std::recursive_mutex> lock(mutex_);
- return current_;
-}
-
-auto TrackQueue::PeekNext(std::size_t limit) const
- -> std::vector<database::TrackId> {
- const std::lock_guard<std::recursive_mutex> lock(mutex_);
- std::vector<database::TrackId> ret;
-
- for (auto it = enqueued_.begin(); it != enqueued_.end() && limit > 0; it++) {
- std::visit(
- [&](auto&& arg) {
- using T = std::decay_t<decltype(arg)>;
- if constexpr (std::is_same_v<T, database::TrackId>) {
- ret.push_back(arg);
- limit--;
- } else if constexpr (std::is_same_v<T, database::TrackIterator>) {
- auto copy = arg;
- while (limit > 0) {
- auto next = copy.Next();
- if (!next) {
- break;
- }
- ret.push_back(*next);
- limit--;
- }
- }
- },
- *it);
+auto TrackQueue::current() const -> std::optional<database::TrackId> {
+ const std::shared_lock<std::shared_mutex> lock(mutex_);
+ if (pos_ >= tracks_.size()) {
+ return {};
}
-
- return ret;
+ return tracks_[pos_];
}
-auto TrackQueue::PeekPrevious(std::size_t limit) const
+auto TrackQueue::peekNext(std::size_t limit) const
-> std::vector<database::TrackId> {
- const std::lock_guard<std::recursive_mutex> lock(mutex_);
- std::vector<database::TrackId> ret;
- ret.reserve(limit);
-
- for (auto it = played_.rbegin(); it != played_.rend(); it++, limit--) {
- ret.push_back(*it);
- }
-
- return ret;
-}
-
-auto TrackQueue::GetCurrentPosition() const -> size_t {
- const std::lock_guard<std::recursive_mutex> lock(mutex_);
- size_t played = played_.size();
- if (current_) {
- played += 1;
+ const std::shared_lock<std::shared_mutex> lock(mutex_);
+ std::vector<database::TrackId> out;
+ for (size_t i = pos_ + 1; i < pos_ + limit + 1 && i < tracks_.size(); i++) {
+ out.push_back(i);
}
- return played;
+ return out;
}
-auto TrackQueue::GetTotalSize() const -> size_t {
- const std::lock_guard<std::recursive_mutex> lock(mutex_);
- size_t total = GetCurrentPosition();
-
- for (const auto& item : enqueued_) {
- std::visit(
- [&](auto&& arg) {
- using T = std::decay_t<decltype(arg)>;
- if constexpr (std::is_same_v<T, database::TrackId>) {
- total++;
- } else if constexpr (std::is_same_v<T, database::TrackIterator>) {
- total += arg.Size();
- }
- },
- item);
+auto TrackQueue::peekPrevious(std::size_t limit) const
+ -> std::vector<database::TrackId> {
+ const std::shared_lock<std::shared_mutex> lock(mutex_);
+ std::vector<database::TrackId> out;
+ for (size_t i = pos_ - 1; i < pos_ - limit - 1 && i >= tracks_.size(); i--) {
+ out.push_back(i);
}
-
- return total;
+ return out;
}
-auto TrackQueue::Insert(Editor& ed, Item i, size_t index) -> void {
- if (index == 0) {
- enqueued_.insert(enqueued_.begin(), i);
- }
-
- // We can't insert halfway through an iterator, so we need to ensure that the
- // first `index` items in the queue are reified into track ids.
- size_t current_index = 0;
- while (current_index < index && current_index < enqueued_.size()) {
- std::visit(
- [&](auto&& arg) {
- using T = std::decay_t<decltype(arg)>;
- if constexpr (std::is_same_v<T, database::TrackId>) {
- // This item is already a track id; nothing to do.
- current_index++;
- } else if constexpr (std::is_same_v<T, database::TrackIterator>) {
- // This item is an iterator. Push it back one, replacing its old
- // index with the next value from it.
- auto next = arg.Next();
- auto iterator_index = enqueued_.begin() + current_index;
- if (!next) {
- // Out of values. Remove the iterator completely.
- enqueued_.erase(iterator_index);
- // Don't increment current_index, since the next item in the
- // queue will have been moved down.
- } else {
- enqueued_.insert(iterator_index, *next);
- current_index++;
- }
- }
- },
- enqueued_[current_index]);
- }
-
- // Double check the previous loop didn't run out of items.
- if (index > enqueued_.size()) {
- ESP_LOGE(kTag, "insert index was out of bounds");
- return;
- }
-
- // Finally, we can now do the actual insertion.
- enqueued_.insert(enqueued_.begin() + index, i);
+auto TrackQueue::currentPosition() const -> size_t {
+ const std::shared_lock<std::shared_mutex> lock(mutex_);
+ return pos_;
}
-auto TrackQueue::Append(Editor& ed, Item i) -> void {
- enqueued_.push_back(i);
- if (!current_) {
- Next(ed);
- }
+auto TrackQueue::totalSize() const -> size_t {
+ const std::shared_lock<std::shared_mutex> lock(mutex_);
+ return tracks_.size();
}
-auto TrackQueue::Next(Editor& ed) -> std::optional<database::TrackId> {
- if (current_) {
- ed.has_current_changed_ = true;
- played_.push_back(*current_);
- }
- current_.reset();
-
- while (!current_ && !enqueued_.empty()) {
- ed.has_current_changed_ = true;
- std::visit(
- [&](auto&& arg) {
- using T = std::decay_t<decltype(arg)>;
- if constexpr (std::is_same_v<T, database::TrackId>) {
- current_ = arg;
- enqueued_.erase(enqueued_.begin());
- } else if constexpr (std::is_same_v<T, database::TrackIterator>) {
- auto next = arg.Next();
- if (!next) {
- enqueued_.erase(enqueued_.begin());
- } else {
- current_ = *next;
- }
- }
- },
- enqueued_.front());
+auto TrackQueue::insert(Item i) -> void {
+ bool current_changed = pos_ == tracks_.size();
+ if (std::holds_alternative<database::TrackId>(i)) {
+ const std::unique_lock<std::shared_mutex> lock(mutex_);
+ tracks_.push_back(std::get<database::TrackId>(i));
+ notifyChanged(current_changed);
+ } else if (std::holds_alternative<database::TrackIterator>(i)) {
+ bg_worker_.Dispatch<void>([=, this]() {
+ database::TrackIterator it = std::get<database::TrackIterator>(i);
+ size_t working_pos = pos_;
+ while (true) {
+ auto next = *it;
+ if (!next) {
+ break;
+ }
+ const std::unique_lock<std::shared_mutex> lock(mutex_);
+ tracks_.insert(tracks_.begin() + working_pos, *next);
+ working_pos++;
+ it++;
+ }
+ notifyChanged(current_changed);
+ });
}
-
- return current_;
}
-auto TrackQueue::Previous(Editor& ed) -> std::optional<database::TrackId> {
- if (played_.empty()) {
- return current_;
- }
- ed.has_current_changed_ = true;
- if (current_) {
- enqueued_.insert(enqueued_.begin(), *current_);
+auto TrackQueue::append(Item i) -> void {
+ bool current_changed = pos_ == tracks_.size();
+ if (std::holds_alternative<database::TrackId>(i)) {
+ const std::unique_lock<std::shared_mutex> lock(mutex_);
+ tracks_.push_back(std::get<database::TrackId>(i));
+ notifyChanged(current_changed);
+ } else if (std::holds_alternative<database::TrackIterator>(i)) {
+ bg_worker_.Dispatch<void>([=, this]() {
+ database::TrackIterator it = std::get<database::TrackIterator>(i);
+ while (true) {
+ auto next = *it;
+ if (!next) {
+ break;
+ }
+ const std::unique_lock<std::shared_mutex> lock(mutex_);
+ tracks_.push_back(*next);
+ it++;
+ }
+ notifyChanged(current_changed);
+ });
}
- current_ = played_.back();
- played_.pop_back();
- return current_;
}
-auto TrackQueue::SkipTo(Editor& ed, database::TrackId id) -> void {
- while ((!current_ || *current_ != id) && !enqueued_.empty()) {
- Next(ed);
- }
-}
+auto TrackQueue::next() -> void {
+ const std::unique_lock<std::shared_mutex> lock(mutex_);
+ pos_ = std::min<size_t>(pos_ + 1, tracks_.size());
-auto TrackQueue::Clear(Editor& ed) -> void {
- ed.has_current_changed_ = current_.has_value();
- current_.reset();
- played_.clear();
- enqueued_.clear();
+ notifyChanged(true);
}
-auto TrackQueue::Save(std::weak_ptr<database::Database> 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<decltype(arg)>;
- if constexpr (std::is_same_v<T, database::TrackId>) {
- enqueued.add(cppbor::Uint{arg});
- } else if constexpr (std::is_same_v<T, database::TrackIterator>) {
- enqueued.add(arg.cbor());
- }
- },
- item);
+auto TrackQueue::previous() -> void {
+ const std::unique_lock<std::shared_mutex> lock(mutex_);
+ if (pos_ > 0) {
+ pos_--;
}
- root.add(cppbor::Bstr{kEnqueuedKey}, std::move(enqueued));
- auto db_lock = db.lock();
- if (!db_lock) {
- return;
- }
- db_lock->Put(kSerialiseKey, root.toString());
+ notifyChanged(true);
}
-class Parser : public cppbor::ParseClient {
- public:
- Parser(std::weak_ptr<database::Database> db,
- std::optional<database::TrackId>& current,
- std::pmr::vector<database::TrackId>& played,
- std::pmr::vector<TrackQueue::Item>& enqueued)
- : state_(State::kInit),
- db_(db),
- current_(current),
- played_(played),
- enqueued_(enqueued) {}
-
- virtual ParseClient* item(std::unique_ptr<cppbor::Item>& 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<cppbor::Item>& item,
- const uint8_t* hdrBegin,
- const uint8_t* valueBegin,
- const uint8_t* end) override {
- std::optional<database::TrackIterator> 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;
+auto TrackQueue::skipTo(database::TrackId id) -> void {
+ const std::unique_lock<std::shared_mutex> lock(mutex_);
+ for (size_t i = pos_; i < tracks_.size(); i++) {
+ if (tracks_[i] == id) {
+ pos_ = i;
}
- 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<database::Database> db_;
-
- int queue_depth_;
-
- std::optional<database::TrackId>& current_;
- std::pmr::vector<database::TrackId>& played_;
- std::pmr::vector<TrackQueue::Item>& enqueued_;
-};
+ notifyChanged(true);
+}
-auto TrackQueue::Load(std::weak_ptr<database::Database> db) -> void {
- auto db_lock = db.lock();
- if (!db_lock) {
- return;
- }
- auto raw = db_lock->Get(kSerialiseKey);
- if (!raw) {
- return;
- }
+auto TrackQueue::clear() -> void {
+ const std::unique_lock<std::shared_mutex> lock(mutex_);
+ pos_ = 0;
+ tracks_.clear();
- Parser p{db, current_, played_, enqueued_};
- const uint8_t* data = reinterpret_cast<const uint8_t*>(raw->data());
- cppbor::parse(data, data + raw->size(), &p);
+ notifyChanged(true);
}
} // namespace audio
diff --git a/src/database/database.cpp b/src/database/database.cpp
index e646154e..1adfec87 100644
--- a/src/database/database.cpp
+++ b/src/database/database.cpp
@@ -16,6 +16,7 @@
#include <memory>
#include <optional>
#include <sstream>
+#include <variant>
#include "collation.hpp"
#include "cppbor.h"
@@ -200,11 +201,11 @@ Database::~Database() {
sIsDbOpen.store(false);
}
-auto Database::Put(const std::string& key, const std::string& val) -> void {
+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> {
+auto Database::get(const std::string& key) -> std::optional<std::string> {
std::string val;
auto res = db_->Get(leveldb::ReadOptions{}, kKeyCustom + key, &val);
if (!res.ok()) {
@@ -213,320 +214,212 @@ auto Database::Get(const std::string& key) -> std::optional<std::string> {
return val;
}
-auto Database::Update() -> std::future<void> {
+auto Database::getTrackPath(TrackId id) -> std::optional<std::string> {
+ auto track_data = dbGetTrackData(id);
+ if (!track_data) {
+ return {};
+ }
+ return std::string{track_data->filepath.data(), track_data->filepath.size()};
+}
+
+auto Database::getTrack(TrackId id) -> std::shared_ptr<Track> {
+ std::shared_ptr<TrackData> data = dbGetTrackData(id);
+ if (!data || data->is_tombstoned) {
+ return {};
+ }
+ std::shared_ptr<TrackTags> tags = tag_parser_.ReadAndParseTags(
+ {data->filepath.data(), data->filepath.size()});
+ if (!tags) {
+ return {};
+ }
+ return std::make_shared<Track>(data, tags);
+}
+
+auto Database::getIndexes() -> std::vector<IndexInfo> {
+ // TODO(jacqueline): This probably needs to be async? When we have runtime
+ // configurable indexes, they will need to come from somewhere.
+ return {
+ kAllTracks,
+ kAllAlbums,
+ kAlbumsByArtist,
+ kTracksByGenre,
+ };
+}
+
+auto Database::updateIndexes() -> void {
events::Ui().Dispatch(event::UpdateStarted{});
- return worker_task_->Dispatch<void>([&]() -> void {
- leveldb::ReadOptions read_options;
- read_options.fill_cache = false;
-
- std::pair<uint16_t, uint16_t> newest_track{0, 0};
-
- // Stage 1: verify all existing tracks are still valid.
- ESP_LOGI(kTag, "verifying existing tracks");
- {
- uint64_t num_processed = 0;
- std::unique_ptr<leveldb::Iterator> it{db_->NewIterator(read_options)};
- std::string prefix = EncodeDataPrefix();
- for (it->Seek(prefix); it->Valid() && it->key().starts_with(prefix);
- it->Next()) {
- num_processed++;
- events::Ui().Dispatch(event::UpdateProgress{
- .stage = event::UpdateProgress::Stage::kVerifyingExistingTracks,
- .val = num_processed,
- });
-
- std::shared_ptr<TrackData> track = ParseDataValue(it->value());
- if (!track) {
- // The value was malformed. Drop this record.
- ESP_LOGW(kTag, "dropping malformed metadata");
- db_->Delete(leveldb::WriteOptions(), it->key());
- continue;
- }
-
- if (track->is_tombstoned) {
- ESP_LOGW(kTag, "skipping tombstoned %lx", track->id);
- continue;
- }
-
- FRESULT res;
- FILINFO info;
- {
- auto lock = drivers::acquire_spi();
- res = f_stat(track->filepath.c_str(), &info);
- }
-
- std::pair<uint16_t, uint16_t> modified_at{0, 0};
- if (res == FR_OK) {
- modified_at = {info.fdate, info.ftime};
- }
- if (modified_at == track->modified_at) {
- newest_track = std::max(modified_at, newest_track);
- continue;
- } else {
- track->modified_at = modified_at;
- }
-
- std::shared_ptr<TrackTags> tags =
- tag_parser_.ReadAndParseTags(track->filepath);
- if (!tags || tags->encoding() == Container::kUnsupported) {
- // We couldn't read the tags for this track. Either they were
- // malformed, or perhaps the file is missing. Either way, tombstone
- // this record.
- ESP_LOGW(kTag, "entombing missing #%lx", track->id);
- dbRemoveIndexes(track);
- track->is_tombstoned = true;
- dbPutTrackData(*track);
- continue;
- }
-
- // At this point, we know that the track still exists in its original
- // location. All that's left to do is update any metadata about it.
-
- uint64_t new_hash = tags->Hash();
- if (new_hash != track->tags_hash) {
- // This track's tags have changed. Since the filepath is exactly the
- // same, we assume this is a legitimate correction. Update the
- // database.
- ESP_LOGI(kTag, "updating hash (%llx -> %llx)", track->tags_hash,
- new_hash);
- dbRemoveIndexes(track);
-
- track->tags_hash = new_hash;
- dbIngestTagHashes(*tags, track->individual_tag_hashes);
- dbPutTrackData(*track);
- dbPutHash(new_hash, track->id);
- }
- }
- }
+ leveldb::ReadOptions read_options;
+ read_options.fill_cache = false;
- ESP_LOGI(kTag, "newest unmodified was at %u,%u", newest_track.first,
- newest_track.second);
+ std::pair<uint16_t, uint16_t> newest_track{0, 0};
- // Stage 2: search for newly added files.
- ESP_LOGI(kTag, "scanning for new tracks");
+ // Stage 1: verify all existing tracks are still valid.
+ ESP_LOGI(kTag, "verifying existing tracks");
+ {
uint64_t num_processed = 0;
- file_gatherer_.FindFiles("", [&](const std::pmr::string& path,
- const FILINFO& info) {
+ std::unique_ptr<leveldb::Iterator> it{db_->NewIterator(read_options)};
+ std::string prefix = EncodeDataPrefix();
+ for (it->Seek(prefix); it->Valid() && it->key().starts_with(prefix);
+ it->Next()) {
num_processed++;
events::Ui().Dispatch(event::UpdateProgress{
- .stage = event::UpdateProgress::Stage::kScanningForNewTracks,
+ .stage = event::UpdateProgress::Stage::kVerifyingExistingTracks,
.val = num_processed,
});
- std::pair<uint16_t, uint16_t> modified{info.fdate, info.ftime};
- if (modified < newest_track) {
- return;
+ std::shared_ptr<TrackData> track = ParseDataValue(it->value());
+ if (!track) {
+ // The value was malformed. Drop this record.
+ ESP_LOGW(kTag, "dropping malformed metadata");
+ db_->Delete(leveldb::WriteOptions(), it->key());
+ continue;
}
- std::shared_ptr<TrackTags> tags = tag_parser_.ReadAndParseTags(path);
- if (!tags || tags->encoding() == Container::kUnsupported) {
- // No parseable tags; skip this fiile.
- return;
+ if (track->is_tombstoned) {
+ ESP_LOGW(kTag, "skipping tombstoned %lx", track->id);
+ continue;
}
- // Check for any existing record with the same hash.
- uint64_t hash = tags->Hash();
- std::string key = EncodeHashKey(hash);
- std::optional<TrackId> existing_hash;
- std::string raw_entry;
- if (db_->Get(leveldb::ReadOptions(), key, &raw_entry).ok()) {
- existing_hash = ParseHashValue(raw_entry);
+ FRESULT res;
+ FILINFO info;
+ {
+ auto lock = drivers::acquire_spi();
+ res = f_stat(track->filepath.c_str(), &info);
}
- if (!existing_hash) {
- // We've never met this track before! Or we have, but the entry is
- // malformed. Either way, record this as a new track.
- TrackId id = dbMintNewTrackId();
- ESP_LOGI(kTag, "recording new 0x%lx", id);
-
- auto data = std::make_shared<TrackData>();
- data->id = id;
- data->filepath = path;
- data->tags_hash = hash;
- data->modified_at = modified;
- dbIngestTagHashes(*tags, data->individual_tag_hashes);
-
- dbPutTrackData(*data);
- dbPutHash(hash, id);
- auto t = std::make_shared<Track>(data, tags);
- dbCreateIndexesForTrack(*t);
- return;
+ std::pair<uint16_t, uint16_t> modified_at{0, 0};
+ if (res == FR_OK) {
+ modified_at = {info.fdate, info.ftime};
+ }
+ if (modified_at == track->modified_at) {
+ newest_track = std::max(modified_at, newest_track);
+ continue;
+ } else {
+ track->modified_at = modified_at;
}
- std::shared_ptr<TrackData> existing_data = dbGetTrackData(*existing_hash);
- if (!existing_data) {
- // We found a hash that matches, but there's no data record? Weird.
- auto new_data = std::make_shared<TrackData>();
- new_data->id = dbMintNewTrackId();
- new_data->filepath = path;
- new_data->tags_hash = hash;
- new_data->modified_at = modified;
- dbIngestTagHashes(*tags, new_data->individual_tag_hashes);
- dbPutTrackData(*new_data);
- auto t = std::make_shared<Track>(new_data, tags);
- dbCreateIndexesForTrack(*t);
- return;
+ std::shared_ptr<TrackTags> tags = tag_parser_.ReadAndParseTags(
+ {track->filepath.data(), track->filepath.size()});
+ if (!tags || tags->encoding() == Container::kUnsupported) {
+ // We couldn't read the tags for this track. Either they were
+ // malformed, or perhaps the file is missing. Either way, tombstone
+ // this record.
+ ESP_LOGW(kTag, "entombing missing #%lx", track->id);
+ dbRemoveIndexes(track);
+ track->is_tombstoned = true;
+ dbPutTrackData(*track);
+ continue;
}
- if (existing_data->is_tombstoned) {
- ESP_LOGI(kTag, "exhuming track %lu", existing_data->id);
- existing_data->is_tombstoned = false;
- existing_data->modified_at = modified;
- dbPutTrackData(*existing_data);
- auto t = std::make_shared<Track>(existing_data, tags);
- dbCreateIndexesForTrack(*t);
- } else if (existing_data->filepath != path) {
- ESP_LOGW(kTag, "tag hash collision for %s and %s",
- existing_data->filepath.c_str(), path.c_str());
- ESP_LOGI(kTag, "hash components: %s, %s, %s",
- tags->at(Tag::kTitle).value_or("no title").c_str(),
- tags->at(Tag::kArtist).value_or("no artist").c_str(),
- tags->at(Tag::kAlbum).value_or("no album").c_str());
+ // At this point, we know that the track still exists in its original
+ // location. All that's left to do is update any metadata about it.
+
+ uint64_t new_hash = tags->Hash();
+ if (new_hash != track->tags_hash) {
+ // This track's tags have changed. Since the filepath is exactly the
+ // same, we assume this is a legitimate correction. Update the
+ // database.
+ ESP_LOGI(kTag, "updating hash (%llx -> %llx)", track->tags_hash,
+ new_hash);
+ dbRemoveIndexes(track);
+
+ track->tags_hash = new_hash;
+ dbIngestTagHashes(*tags, track->individual_tag_hashes);
+ dbPutTrackData(*track);
+ dbPutHash(new_hash, track->id);
}
- });
- events::Ui().Dispatch(event::UpdateFinished{});
- });
-}
+ }
+ }
-auto Database::GetTrackPath(TrackId id)
- -> std::future<std::optional<std::pmr::string>> {
- return worker_task_->Dispatch<std::optional<std::pmr::string>>(
- [=, this]() -> std::optional<std::pmr::string> {
- auto track_data = dbGetTrackData(id);
- if (track_data) {
- return track_data->filepath;
- }
- return {};
- });
-}
+ ESP_LOGI(kTag, "newest unmodified was at %u,%u", newest_track.first,
+ newest_track.second);
-auto Database::GetTrack(TrackId id) -> std::future<std::shared_ptr<Track>> {
- return worker_task_->Dispatch<std::shared_ptr<Track>>(
- [=, this]() -> std::shared_ptr<Track> {
- std::shared_ptr<TrackData> data = dbGetTrackData(id);
- if (!data || data->is_tombstoned) {
- return {};
- }
- std::shared_ptr<TrackTags> tags =
- tag_parser_.ReadAndParseTags(data->filepath);
- if (!tags) {
- return {};
- }
- return std::make_shared<Track>(data, tags);
- });
-}
+ // Stage 2: search for newly added files.
+ ESP_LOGI(kTag, "scanning for new tracks");
+ uint64_t num_processed = 0;
+ file_gatherer_.FindFiles("", [&](const std::string& path,
+ const FILINFO& info) {
+ num_processed++;
+ events::Ui().Dispatch(event::UpdateProgress{
+ .stage = event::UpdateProgress::Stage::kScanningForNewTracks,
+ .val = num_processed,
+ });
-auto Database::GetBulkTracks(std::vector<TrackId> ids)
- -> std::future<std::vector<std::shared_ptr<Track>>> {
- return worker_task_->Dispatch<std::vector<std::shared_ptr<Track>>>(
- [=, this]() -> std::vector<std::shared_ptr<Track>> {
- std::map<TrackId, std::shared_ptr<Track>> id_to_track{};
-
- // Sort the list of ids so that we can retrieve them all in a single
- // iteration through the database, without re-seeking.
- std::vector<TrackId> sorted_ids = ids;
- std::sort(sorted_ids.begin(), sorted_ids.end());
-
- std::unique_ptr<leveldb::Iterator> it{
- db_->NewIterator(leveldb::ReadOptions{})};
- for (const TrackId& id : sorted_ids) {
- std::string key = EncodeDataKey(id);
- it->Seek(key);
- if (!it->Valid() || it->key() != key) {
- // This id wasn't found at all. Skip it.
- continue;
- }
- std::shared_ptr<Track> track =
- ParseRecord<Track>(it->key(), it->value());
- if (track) {
- id_to_track.insert({id, track});
- }
- }
-
- // We've fetched all of the ids in the request, so now just put them
- // back into the order they were asked for in.
- std::vector<std::shared_ptr<Track>> results;
- for (const TrackId& id : ids) {
- if (id_to_track.contains(id)) {
- results.push_back(id_to_track.at(id));
- } else {
- // This lookup failed.
- results.push_back({});
- }
- }
- return results;
- });
-}
+ std::pair<uint16_t, uint16_t> modified{info.fdate, info.ftime};
+ if (modified < newest_track) {
+ return;
+ }
-auto Database::GetIndexes() -> std::vector<IndexInfo> {
- // TODO(jacqueline): This probably needs to be async? When we have runtime
- // configurable indexes, they will need to come from somewhere.
- return {
- kAllTracks,
- kAllAlbums,
- kAlbumsByArtist,
- kTracksByGenre,
- };
-}
+ std::shared_ptr<TrackTags> tags = tag_parser_.ReadAndParseTags(path);
+ if (!tags || tags->encoding() == Container::kUnsupported) {
+ // No parseable tags; skip this fiile.
+ return;
+ }
-auto Database::GetTracksByIndex(IndexId index, std::size_t page_size)
- -> std::future<Result<IndexRecord>*> {
- return worker_task_->Dispatch<Result<IndexRecord>*>(
- [=, this]() -> Result<IndexRecord>* {
- IndexKey::Header header{
- .id = index,
- .depth = 0,
- .components_hash = 0,
- };
- std::string prefix = EncodeIndexPrefix(header);
- Continuation c{.prefix = {prefix.data(), prefix.size()},
- .start_key = {prefix.data(), prefix.size()},
- .forward = true,
- .was_prev_forward = true,
- .page_size = page_size};
- return dbGetPage<IndexRecord>(c);
- });
-}
+ // Check for any existing record with the same hash.
+ uint64_t hash = tags->Hash();
+ std::string key = EncodeHashKey(hash);
+ std::optional<TrackId> existing_hash;
+ std::string raw_entry;
+ if (db_->Get(leveldb::ReadOptions(), key, &raw_entry).ok()) {
+ existing_hash = ParseHashValue(raw_entry);
+ }
-auto Database::GetTracks(std::size_t page_size) -> std::future<Result<Track>*> {
- return worker_task_->Dispatch<Result<Track>*>([=, this]() -> Result<Track>* {
- std::string prefix = EncodeDataPrefix();
- Continuation c{.prefix = {prefix.data(), prefix.size()},
- .start_key = {prefix.data(), prefix.size()},
- .forward = true,
- .was_prev_forward = true,
- .page_size = page_size};
- return dbGetPage<Track>(c);
- });
-}
+ if (!existing_hash) {
+ // We've never met this track before! Or we have, but the entry is
+ // malformed. Either way, record this as a new track.
+ TrackId id = dbMintNewTrackId();
+ ESP_LOGI(kTag, "recording new 0x%lx", id);
+
+ auto data = std::make_shared<TrackData>();
+ data->id = id;
+ data->filepath = path;
+ data->tags_hash = hash;
+ data->modified_at = modified;
+ dbIngestTagHashes(*tags, data->individual_tag_hashes);
+
+ dbPutTrackData(*data);
+ dbPutHash(hash, id);
+ auto t = std::make_shared<Track>(data, tags);
+ dbCreateIndexesForTrack(*t);
+ return;
+ }
-auto Database::GetDump(std::size_t page_size)
- -> std::future<Result<std::pmr::string>*> {
- return worker_task_->Dispatch<Result<std::pmr::string>*>(
- [=, this]() -> Result<std::pmr::string>* {
- Continuation c{.prefix = "",
- .start_key = "",
- .forward = true,
- .was_prev_forward = true,
- .page_size = page_size};
- return dbGetPage<std::pmr::string>(c);
- });
-}
+ std::shared_ptr<TrackData> existing_data = dbGetTrackData(*existing_hash);
+ if (!existing_data) {
+ // We found a hash that matches, but there's no data record? Weird.
+ auto new_data = std::make_shared<TrackData>();
+ new_data->id = dbMintNewTrackId();
+ new_data->filepath = path;
+ new_data->tags_hash = hash;
+ new_data->modified_at = modified;
+ dbIngestTagHashes(*tags, new_data->individual_tag_hashes);
+ dbPutTrackData(*new_data);
+ auto t = std::make_shared<Track>(new_data, tags);
+ dbCreateIndexesForTrack(*t);
+ return;
+ }
-template <typename T>
-auto Database::GetPage(Continuation* c) -> std::future<Result<T>*> {
- Continuation copy = *c;
- return worker_task_->Dispatch<Result<T>*>(
- [=, this]() -> Result<T>* { return dbGetPage<T>(copy); });
+ if (existing_data->is_tombstoned) {
+ ESP_LOGI(kTag, "exhuming track %lu", existing_data->id);
+ existing_data->is_tombstoned = false;
+ existing_data->modified_at = modified;
+ dbPutTrackData(*existing_data);
+ auto t = std::make_shared<Track>(existing_data, tags);
+ dbCreateIndexesForTrack(*t);
+ } else if (existing_data->filepath !=
+ std::pmr::string{path.data(), path.size()}) {
+ ESP_LOGW(kTag, "tag hash collision for %s and %s",
+ existing_data->filepath.c_str(), path.c_str());
+ ESP_LOGI(kTag, "hash components: %s, %s, %s",
+ tags->at(Tag::kTitle).value_or("no title").c_str(),
+ tags->at(Tag::kArtist).value_or("no artist").c_str(),
+ tags->at(Tag::kAlbum).value_or("no album").c_str());
+ }
+ });
+ events::Ui().Dispatch(event::UpdateFinished{});
}
-template auto Database::GetPage<Track>(Continuation* c)
- -> std::future<Result<Track>*>;
-template auto Database::GetPage<IndexRecord>(Continuation* c)
- -> std::future<Result<IndexRecord>*>;
-template auto Database::GetPage<std::pmr::string>(Continuation* c)
- -> std::future<Result<std::pmr::string>*>;
-
auto Database::dbMintNewTrackId() -> TrackId {
TrackId next_id = 1;
std::string val;
@@ -592,7 +485,7 @@ auto Database::dbGetHash(const uint64_t& hash) -> std::optional<TrackId> {
}
auto Database::dbCreateIndexesForTrack(const Track& track) -> void {
- for (const IndexInfo& index : GetIndexes()) {
+ for (const IndexInfo& index : getIndexes()) {
leveldb::WriteBatch writes;
auto entries = Index(collator_, index, track);
for (const auto& it : entries) {
@@ -609,7 +502,7 @@ auto Database::dbRemoveIndexes(std::shared_ptr<TrackData> data) -> void {
return;
}
Track track{data, tags};
- for (const IndexInfo& index : GetIndexes()) {
+ for (const IndexInfo& index : getIndexes()) {
auto entries = Index(collator_, index, track);
for (auto it = entries.rbegin(); it != entries.rend(); it++) {
auto key = EncodeIndexKey(it->first);
@@ -666,512 +559,209 @@ auto Database::dbRecoverTagsFromHashes(
return out;
}
-template <typename T>
-auto Database::dbGetPage(const Continuation& c) -> Result<T>* {
- // Work out our starting point. Sometimes this will already done.
- std::unique_ptr<leveldb::Iterator> it{
- db_->NewIterator(leveldb::ReadOptions{})};
- it->Seek({c.start_key.data(), c.start_key.size()});
-
- // Fix off-by-one if we just changed direction.
- if (c.forward != c.was_prev_forward) {
- if (c.forward) {
- it->Next();
- } else {
+auto seekToOffset(leveldb::Iterator* it, int offset) {
+ while (it->Valid() && offset != 0) {
+ if (offset < 0) {
it->Prev();
- }
- }
-
- // Grab results.
- std::optional<std::pmr::string> first_key;
- std::vector<std::shared_ptr<T>> records;
- while (records.size() < c.page_size && it->Valid()) {
- if (!it->key().starts_with({c.prefix.data(), c.prefix.size()})) {
- break;
- }
- if (!first_key) {
- first_key = it->key().ToString();
- }
- std::shared_ptr<T> parsed = ParseRecord<T>(it->key(), it->value());
- if (parsed) {
- records.push_back(parsed);
- }
- if (c.forward) {
- it->Next();
+ offset++;
} else {
- it->Prev();
+ it->Next();
+ offset--;
}
}
+}
- if (!it->Valid() ||
- !it->key().starts_with({c.prefix.data(), c.prefix.size()})) {
- it.reset();
- }
-
- // Put results into canonical order if we were iterating backwards.
- if (!c.forward) {
- std::reverse(records.begin(), records.end());
- }
+auto Database::getRecord(const SearchKey& c)
+ -> std::optional<std::pair<std::pmr::string, Record>> {
+ std::unique_ptr<leveldb::Iterator> it{
+ db_->NewIterator(leveldb::ReadOptions{})};
- // Work out the new continuations.
- std::optional<Continuation> next_page;
- if (c.forward) {
- if (it != nullptr) {
- // We were going forward, and now we want the next page.
- std::pmr::string key{it->key().data(), it->key().size(),
- &memory::kSpiRamResource};
- next_page = Continuation{
- .prefix = c.prefix,
- .start_key = key,
- .forward = true,
- .was_prev_forward = true,
- .page_size = c.page_size,
- };
- }
- // No iterator means we ran out of results in this direction.
- } else {
- // We were going backwards, and now we want the next page. This is a
- // reversal, to set the start key to the first record we saw and mark that
- // it's off by one.
- next_page = Continuation{
- .prefix = c.prefix,
- .start_key = *first_key,
- .forward = true,
- .was_prev_forward = false,
- .page_size = c.page_size,
- };
+ it->Seek(c.startKey());
+ seekToOffset(it.get(), c.offset);
+ if (!it->Valid() || !it->key().starts_with(std::string_view{c.prefix})) {
+ return {};
}
- std::optional<Continuation> prev_page;
- if (c.forward) {
- // We were going forwards, and now we want the previous page. Set the
- // search key to the first result we saw, and mark that it's off by one.
- prev_page = Continuation{
- .prefix = c.prefix,
- .start_key = *first_key,
- .forward = false,
- .was_prev_forward = true,
- .page_size = c.page_size,
- };
- } else {
- if (it != nullptr) {
- // We were going backwards, and we still want to go backwards.
- std::pmr::string key{it->key().data(), it->key().size(),
- &memory::kSpiRamResource};
- prev_page = Continuation{
- .prefix = c.prefix,
- .start_key = key,
- .forward = false,
- .was_prev_forward = false,
- .page_size = c.page_size,
- };
- }
- // No iterator means we ran out of results in this direction.
+ std::optional<IndexKey> key = ParseIndexKey(it->key());
+ if (!key) {
+ ESP_LOGW(kTag, "parsing index key failed");
+ return {};
}
- return new Result<T>(std::move(records), next_page, prev_page);
+ return std::make_pair(std::pmr::string{it->key().data(), it->key().size(),
+ &memory::kSpiRamResource},
+ Record{*key, it->value()});
}
-auto Database::dbCount(const Continuation& c) -> size_t {
+auto Database::countRecords(const SearchKey& c) -> size_t {
std::unique_ptr<leveldb::Iterator> it{
db_->NewIterator(leveldb::ReadOptions{})};
- size_t count = 0;
- for (it->Seek({c.start_key.data(), c.start_key.size()});
- it->Valid() && it->key().starts_with({c.prefix.data(), c.prefix.size()});
- it->Next()) {
- count++;
- }
- return count;
-}
-template auto Database::dbGetPage<Track>(const Continuation& c)
- -> Result<Track>*;
-template auto Database::dbGetPage<std::pmr::string>(const Continuation& c)
- -> Result<std::pmr::string>*;
-
-template <>
-auto Database::ParseRecord<IndexRecord>(const leveldb::Slice& key,
- const leveldb::Slice& val)
- -> std::shared_ptr<IndexRecord> {
- std::optional<IndexKey> data = ParseIndexKey(key);
- if (!data) {
+ it->Seek(c.startKey());
+ seekToOffset(it.get(), c.offset);
+ if (!it->Valid() || !it->key().starts_with(std::string_view{c.prefix})) {
return {};
}
- std::optional<std::pmr::string> title;
- if (!val.empty()) {
- title = val.ToString();
+ size_t count = 0;
+ while (it->Valid() && it->key().starts_with(std::string_view{c.prefix})) {
+ it->Next();
+ count++;
}
- return std::make_shared<IndexRecord>(*data, title, data->track);
+ return count;
}
-template <>
-auto Database::ParseRecord<Track>(const leveldb::Slice& key,
- const leveldb::Slice& val)
- -> std::shared_ptr<Track> {
- std::shared_ptr<TrackData> data = ParseDataValue(val);
- if (!data || data->is_tombstoned) {
- return {};
- }
- std::shared_ptr<TrackTags> tags =
- tag_parser_.ReadAndParseTags(data->filepath);
- if (!tags) {
- return {};
+auto SearchKey::startKey() const -> std::string_view {
+ if (key) {
+ return *key;
}
- return std::make_shared<Track>(data, tags);
+ return prefix;
}
-template <>
-auto Database::ParseRecord<std::pmr::string>(const leveldb::Slice& key,
- const leveldb::Slice& val)
- -> std::shared_ptr<std::pmr::string> {
- std::ostringstream stream;
- stream << "key: ";
- if (key.size() < 3 || key.data()[1] != '\0') {
- stream << key.ToString().c_str();
+Record::Record(const IndexKey& key, const leveldb::Slice& t)
+ : text_(t.data(), t.size(), &memory::kSpiRamResource) {
+ if (key.track) {
+ contents_ = *key.track;
} else {
- for (size_t i = 0; i < key.size(); i++) {
- if (i == 0) {
- stream << key.data()[i];
- } else if (i == 1) {
- stream << " / 0x";
- } else {
- stream << std::hex << std::setfill('0') << std::setw(2)
- << static_cast<int>(key.data()[i]);
- }
- }
- }
- if (!val.empty()) {
- stream << "\tval: 0x";
- for (int i = 0; i < val.size(); i++) {
- stream << std::hex << std::setfill('0') << std::setw(2)
- << static_cast<int>(val.data()[i]);
- }
+ contents_ = ExpandHeader(key.header, key.item);
}
- std::pmr::string res{stream.str(), &memory::kSpiRamResource};
- return std::make_shared<std::pmr::string>(res);
}
-IndexRecord::IndexRecord(const IndexKey& key,
- std::optional<std::pmr::string> title,
- std::optional<TrackId> track)
- : key_(key), override_text_(title), track_(track) {}
-
-auto IndexRecord::text() const -> std::optional<std::pmr::string> {
- if (override_text_) {
- return override_text_;
- }
- return key_.item;
+auto Record::text() const -> std::string_view {
+ return text_;
}
-auto IndexRecord::track() const -> std::optional<TrackId> {
- return track_;
+auto Record::contents() const
+ -> const std::variant<TrackId, IndexKey::Header>& {
+ return contents_;
}
-auto IndexRecord::Expand(std::size_t page_size) const
- -> std::optional<Continuation> {
- if (track_) {
- return {};
- }
- std::string new_prefix = EncodeIndexPrefix(ExpandHeader());
- return Continuation{
- .prefix = {new_prefix.data(), new_prefix.size()},
- .start_key = {new_prefix.data(), new_prefix.size()},
- .forward = true,
- .was_prev_forward = true,
- .page_size = page_size,
- };
-}
+Iterator::Iterator(std::shared_ptr<Database> db, IndexId idx)
+ : Iterator(db,
+ IndexKey::Header{
+ .id = idx,
+ .depth = 0,
+ .components_hash = 0,
+ }) {}
-auto IndexRecord::ExpandHeader() const -> IndexKey::Header {
- return ::database::ExpandHeader(key_.header, key_.item);
+Iterator::Iterator(std::shared_ptr<Database> db, const IndexKey::Header& header)
+ : db_(db), key_{}, current_() {
+ std::string prefix = EncodeIndexPrefix(header);
+ key_ = {
+ .prefix = {prefix.data(), prefix.size(), &memory::kSpiRamResource},
+ .key = {},
+ .offset = 0,
+ };
+ iterate(key_);
}
-Iterator::Iterator(std::weak_ptr<Database> db, const IndexInfo& idx)
- : db_(db), pos_mutex_(), current_pos_(), prev_pos_() {
- std::string prefix = EncodeIndexPrefix(
- IndexKey::Header{.id = idx.id, .depth = 0, .components_hash = 0});
- current_pos_ = Continuation{.prefix = {prefix.data(), prefix.size()},
- .start_key = {prefix.data(), prefix.size()},
- .forward = true,
- .was_prev_forward = true,
- .page_size = 1};
+auto Iterator::value() const -> const std::optional<Record>& {
+ return current_;
}
-auto Iterator::Parse(std::weak_ptr<Database> db, const cppbor::Array& encoded)
- -> std::optional<Iterator> {
- // 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<Continuation> 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<Continuation> 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)};
+auto Iterator::next() -> void {
+ SearchKey new_key = key_;
+ new_key.offset = 1;
+ iterate(new_key);
}
-Iterator::Iterator(std::weak_ptr<Database> db, const Continuation& c)
- : db_(db), pos_mutex_(), current_pos_(c), prev_pos_() {}
-
-Iterator::Iterator(const Iterator& other)
- : db_(other.db_),
- pos_mutex_(),
- current_pos_(other.current_pos_),
- prev_pos_(other.prev_pos_) {}
-
-Iterator::Iterator(std::weak_ptr<Database> db,
- std::optional<Continuation>&& cur,
- std::optional<Continuation>&& 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_;
- return *this;
+auto Iterator::prev() -> void {
+ SearchKey new_key = key_;
+ new_key.offset = -1;
+ iterate(new_key);
}
-auto Iterator::Next(Callback cb) -> void {
+auto Iterator::iterate(const SearchKey& key) -> void {
auto db = db_.lock();
if (!db) {
- InvokeNull(cb);
+ ESP_LOGW(kTag, "iterate with dead db");
return;
}
- db->worker_task_->Dispatch<void>([=]() {
- std::lock_guard lock{pos_mutex_};
- if (!current_pos_) {
- InvokeNull(cb);
- return;
- }
- std::unique_ptr<Result<IndexRecord>> res{
- db->dbGetPage<IndexRecord>(*current_pos_)};
- prev_pos_ = current_pos_;
- current_pos_ = res->next_page();
- if (!res || res->values().empty() || !res->values()[0]) {
- ESP_LOGI(kTag, "dropping empty result");
- InvokeNull(cb);
- return;
- }
- std::invoke(cb, *res->values()[0]);
- });
-}
-
-auto Iterator::NextSync() -> std::optional<IndexRecord> {
- auto db = db_.lock();
- if (!db) {
- return {};
- }
- std::lock_guard lock{pos_mutex_};
- if (!current_pos_) {
- return {};
- }
- std::unique_ptr<Result<IndexRecord>> res{
- db->dbGetPage<IndexRecord>(*current_pos_)};
- prev_pos_ = current_pos_;
- current_pos_ = res->next_page();
- if (!res || res->values().empty() || !res->values()[0]) {
- ESP_LOGI(kTag, "dropping empty result");
- return {};
- }
- return *res->values()[0];
-}
-
-auto Iterator::PeekSync() -> std::optional<IndexRecord> {
- auto db = db_.lock();
- if (!db) {
- return {};
- }
- auto pos = current_pos_;
- if (!pos) {
- return {};
- }
- std::unique_ptr<Result<IndexRecord>> res{db->dbGetPage<IndexRecord>(*pos)};
- if (!res || res->values().empty() || !res->values()[0]) {
- return {};
- }
- return *res->values()[0];
-}
-
-auto Iterator::Prev(Callback cb) -> void {
- auto db = db_.lock();
- if (!db) {
- InvokeNull(cb);
- return;
+ auto res = db->getRecord(key);
+ if (res) {
+ key_ = {
+ .prefix = key_.prefix,
+ .key = res->first,
+ .offset = 0,
+ };
+ current_ = res->second;
+ } else {
+ key_ = key;
+ current_.reset();
}
- db->worker_task_->Dispatch<void>([=]() {
- std::lock_guard lock{pos_mutex_};
- if (!prev_pos_) {
- InvokeNull(cb);
- return;
- }
- std::unique_ptr<Result<IndexRecord>> res{
- db->dbGetPage<IndexRecord>(*current_pos_)};
- current_pos_ = prev_pos_;
- prev_pos_ = res->prev_page();
- std::invoke(cb, *res->values()[0]);
- });
}
-auto Iterator::Size() const -> size_t {
+auto Iterator::count() const -> size_t {
auto db = db_.lock();
if (!db) {
- return {};
- }
- std::optional<Continuation> pos = current_pos_;
- if (!pos) {
+ ESP_LOGW(kTag, "count with dead db");
return 0;
}
- return db->dbCount(*pos);
+ return db->countRecords(key_);
}
-auto Iterator::InvokeNull(Callback cb) -> void {
- std::invoke(cb, std::optional<IndexRecord>{});
+TrackIterator::TrackIterator(const Iterator& it) : db_(it.db_), levels_() {
+ levels_.push_back(it);
+ next(false);
}
-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::next() -> void {
+ next(true);
}
-auto TrackIterator::Parse(std::weak_ptr<Database> db,
- const cppbor::Array& encoded)
- -> std::optional<TrackIterator> {
- TrackIterator ret{db};
+auto TrackIterator::next(bool advance) -> void {
+ while (!levels_.empty()) {
+ if (advance) {
+ levels_.back().next();
+ }
- 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 {};
+ auto& cur = levels_.back().value();
+ if (!cur) {
+ // The current top iterator is out of tracks. Pop it, and move the parent
+ // to the next item.
+ levels_.pop_back();
+ advance = true;
+ } else if (std::holds_alternative<IndexKey::Header>(cur->contents())) {
+ // This record is a branch. Push a new iterator.
+ auto key = std::get<IndexKey::Header>(cur->contents());
+ auto db = db_.lock();
+ if (!db) {
+ return;
}
+ levels_.emplace_back(db, key);
+ // Don't skip the first value of the new level.
+ advance = false;
+ } else if (std::holds_alternative<TrackId>(cur->contents())) {
+ // New record is a leaf.
+ break;
}
}
-
- return ret;
}
-TrackIterator::TrackIterator(const Iterator& it) : db_(it.db_), levels_() {
- if (it.current_pos_) {
- levels_.push_back(it);
+auto TrackIterator::value() const -> std::optional<TrackId> {
+ if (levels_.empty()) {
+ return {};
}
- NextLeaf();
-}
-
-TrackIterator::TrackIterator(const TrackIterator& other)
- : db_(other.db_), levels_(other.levels_) {}
-
-TrackIterator::TrackIterator(std::weak_ptr<Database> db) : db_(db), levels_() {}
-
-TrackIterator& TrackIterator::operator=(TrackIterator&& other) {
- levels_ = std::move(other.levels_);
- return *this;
-}
-
-auto TrackIterator::Next() -> std::optional<TrackId> {
- std::optional<TrackId> next{};
- while (!next && !levels_.empty()) {
- auto next_record = levels_.back().NextSync();
- if (!next_record) {
- levels_.pop_back();
- NextLeaf();
- continue;
- }
- // May still be nullopt_t; hence the loop.
- next = next_record->track();
+ auto cur = levels_.back().value();
+ if (!cur) {
+ return {};
}
- return next;
+ if (std::holds_alternative<TrackId>(cur->contents())) {
+ return std::get<TrackId>(cur->contents());
+ }
+ return {};
}
-auto TrackIterator::Size() const -> size_t {
+auto TrackIterator::count() const -> size_t {
size_t size = 0;
TrackIterator copy{*this};
while (!copy.levels_.empty()) {
- size += copy.levels_.back().Size();
+ size += copy.levels_.back().count();
copy.levels_.pop_back();
- copy.NextLeaf();
+ copy.next();
}
return size;
}
-auto TrackIterator::NextLeaf() -> void {
- while (!levels_.empty()) {
- ESP_LOGI(kTag, "check next candidate");
- Iterator& candidate = levels_.back();
- auto next = candidate.PeekSync();
- if (!next) {
- ESP_LOGI(kTag, "candidate is empty");
- levels_.pop_back();
- continue;
- }
- if (!next->track()) {
- ESP_LOGI(kTag, "candidate is a branch");
- candidate.NextSync();
- levels_.push_back(Iterator{db_, next->Expand(1).value()});
- continue;
- }
- ESP_LOGI(kTag, "candidate is a leaf");
- break;
- }
-}
-
-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/file_gatherer.cpp b/src/database/file_gatherer.cpp
index 0809ee0d..f07a1b4d 100644
--- a/src/database/file_gatherer.cpp
+++ b/src/database/file_gatherer.cpp
@@ -21,13 +21,13 @@ namespace database {
static_assert(sizeof(TCHAR) == sizeof(char), "TCHAR must be CHAR");
auto FileGathererImpl::FindFiles(
- const std::pmr::string& root,
- std::function<void(const std::pmr::string&, const FILINFO&)> cb) -> void {
- std::pmr::deque<std::pmr::string> to_explore(&memory::kSpiRamResource);
+ const std::string& root,
+ std::function<void(const std::string&, const FILINFO&)> cb) -> void {
+ std::deque<std::string> to_explore;
to_explore.push_back(root);
while (!to_explore.empty()) {
- std::pmr::string next_path_str = to_explore.front();
+ std::string next_path_str = to_explore.front();
const TCHAR* next_path = static_cast<const TCHAR*>(next_path_str.c_str());
FF_DIR dir;
@@ -54,7 +54,7 @@ auto FileGathererImpl::FindFiles(
// System or hidden file. Ignore it and move on.
continue;
} else {
- std::pmr::string full_path{&memory::kSpiRamResource};
+ std::string full_path;
full_path += next_path_str;
full_path += "/";
full_path += info.fname;
diff --git a/src/database/include/database.hpp b/src/database/include/database.hpp
index 327db3cb..c75dbf96 100644
--- a/src/database/include/database.hpp
+++ b/src/database/include/database.hpp
@@ -35,61 +35,18 @@
namespace database {
-struct Continuation {
- std::pmr::string prefix;
- std::pmr::string start_key;
- bool forward;
- bool was_prev_forward;
- size_t page_size;
-};
+struct SearchKey;
+class Record;
+class Iterator;
/*
- * Wrapper for a set of results from the database. Owns the list of results, as
- * well as a continuation token that can be used to continue fetching more
- * results if they were paginated.
+ * Handle to an open database. This can be used to store large amounts of
+ * persistent data on the SD card, in a manner that can be retrieved later very
+ * quickly.
+ *
+ * A database includes a number of 'indexes'. Each index is a sorted,
+ * hierarchical view of all the playable tracks on the device.
*/
-template <typename T>
-class Result {
- public:
- auto values() const -> const std::vector<std::shared_ptr<T>>& {
- return values_;
- }
-
- auto next_page() -> std::optional<Continuation>& { return next_page_; }
- auto prev_page() -> std::optional<Continuation>& { return prev_page_; }
-
- Result(const std::vector<std::shared_ptr<T>>&& values,
- std::optional<Continuation> next,
- std::optional<Continuation> prev)
- : values_(values), next_page_(next), prev_page_(prev) {}
-
- Result(const Result&) = delete;
- Result& operator=(const Result&) = delete;
-
- private:
- std::vector<std::shared_ptr<T>> values_;
- std::optional<Continuation> next_page_;
- std::optional<Continuation> prev_page_;
-};
-
-class IndexRecord {
- public:
- explicit IndexRecord(const IndexKey&,
- std::optional<std::pmr::string>,
- std::optional<TrackId>);
-
- auto text() const -> std::optional<std::pmr::string>;
- auto track() const -> std::optional<TrackId>;
-
- auto Expand(std::size_t) const -> std::optional<Continuation>;
- auto ExpandHeader() const -> IndexKey::Header;
-
- private:
- IndexKey key_;
- std::optional<std::pmr::string> override_text_;
- std::optional<TrackId> track_;
-};
-
class Database {
public:
enum DatabaseError {
@@ -106,31 +63,19 @@ class Database {
~Database();
- auto Put(const std::string& key, const std::string& val) -> void;
- auto Get(const std::string& key) -> std::optional<std::string>;
-
- auto Update() -> std::future<void>;
+ /* Adds an arbitrary record to the database. */
+ auto put(const std::string& key, const std::string& val) -> void;
- auto GetTrackPath(TrackId id) -> std::future<std::optional<std::pmr::string>>;
+ /* Retrives a value previously stored with `put`. */
+ auto get(const std::string& key) -> std::optional<std::string>;
- auto GetTrack(TrackId id) -> std::future<std::shared_ptr<Track>>;
+ auto getTrackPath(TrackId id) -> std::optional<std::string>;
+ auto getTrack(TrackId id) -> std::shared_ptr<Track>;
- /*
- * Fetches data for multiple tracks more efficiently than multiple calls to
- * GetTrack.
- */
- auto GetBulkTracks(std::vector<TrackId> id)
- -> std::future<std::vector<std::shared_ptr<Track>>>;
-
- auto GetIndexes() -> std::vector<IndexInfo>;
- auto GetTracksByIndex(IndexId index, std::size_t page_size)
- -> std::future<Result<IndexRecord>*>;
- auto GetTracks(std::size_t page_size) -> std::future<Result<Track>*>;
- auto GetDump(std::size_t page_size) -> std::future<Result<std::pmr::string>*>;
-
- template <typename T>
- auto GetPage(Continuation* c) -> std::future<Result<T>*>;
+ auto getIndexes() -> std::vector<IndexInfo>;
+ auto updateIndexes() -> void;
+ // Cannot be copied or moved.
Database(const Database&) = delete;
Database& operator=(const Database&) = delete;
@@ -157,106 +102,134 @@ class Database {
std::shared_ptr<tasks::Worker> worker);
auto dbMintNewTrackId() -> TrackId;
- auto dbEntomb(TrackId track, uint64_t hash) -> void;
+ auto dbEntomb(TrackId track, uint64_t hash) -> void;
auto dbPutTrackData(const TrackData& s) -> void;
auto dbGetTrackData(TrackId id) -> std::shared_ptr<TrackData>;
auto dbPutHash(const uint64_t& hash, TrackId i) -> void;
auto dbGetHash(const uint64_t& hash) -> std::optional<TrackId>;
+
auto dbCreateIndexesForTrack(const Track& track) -> void;
auto dbRemoveIndexes(std::shared_ptr<TrackData>) -> void;
+
auto dbIngestTagHashes(const TrackTags&,
std::pmr::unordered_map<Tag, uint64_t>&) -> void;
auto dbRecoverTagsFromHashes(const std::pmr::unordered_map<Tag, uint64_t>&)
-> std::shared_ptr<TrackTags>;
- template <typename T>
- auto dbGetPage(const Continuation& c) -> Result<T>*;
+ auto getRecord(const SearchKey& c)
+ -> std::optional<std::pair<std::pmr::string, Record>>;
+ auto countRecords(const SearchKey& c) -> size_t;
+};
- auto dbCount(const Continuation& c) -> size_t;
+/*
+ * Container for the data needed to iterate through database records. This is a
+ * lower-level type that the higher-level iterators are built from; most users
+ * outside this namespace shouldn't need to work with continuations.
+ */
+struct SearchKey {
+ std::pmr::string prefix;
+ /* If not given, then iteration starts from `prefix`. */
+ std::optional<std::pmr::string> key;
+ int offset;
- template <typename T>
- auto ParseRecord(const leveldb::Slice& key, const leveldb::Slice& val)
- -> std::shared_ptr<T>;
+ auto startKey() const -> std::string_view;
};
-template <>
-auto Database::ParseRecord<IndexRecord>(const leveldb::Slice& key,
- const leveldb::Slice& val)
- -> std::shared_ptr<IndexRecord>;
-template <>
-auto Database::ParseRecord<Track>(const leveldb::Slice& key,
- const leveldb::Slice& val)
- -> std::shared_ptr<Track>;
-template <>
-auto Database::ParseRecord<std::pmr::string>(const leveldb::Slice& key,
- const leveldb::Slice& val)
- -> std::shared_ptr<std::pmr::string>;
-
/*
- * Utility for accessing a large set of database records, one record at a time.
+ * A record belonging to one of the database's indexes. This may either be a
+ * leaf record, containing a track id, or a branch record, containing a new
+ * Header to retrieve results at the next level of the index.
*/
-class Iterator {
+class Record {
public:
- static auto Parse(std::weak_ptr<Database>, const cppbor::Array&)
- -> std::optional<Iterator>;
+ Record(const IndexKey&, const leveldb::Slice&);
- Iterator(std::weak_ptr<Database>, const IndexInfo&);
- Iterator(std::weak_ptr<Database>, const Continuation&);
- Iterator(const Iterator&);
+ Record(const Record&) = default;
+ Record& operator=(const Record& other) = default;
- Iterator& operator=(const Iterator& other);
+ auto text() const -> std::string_view;
+ auto contents() const -> const std::variant<TrackId, IndexKey::Header>&;
- auto database() const { return db_; }
+ private:
+ std::pmr::string text_;
+ std::variant<TrackId, IndexKey::Header> contents_;
+};
- using Callback = std::function<void(std::optional<IndexRecord>)>;
+/*
+ * Utility for accessing a large set of database records, one record at a time.
+ */
+class Iterator {
+ public:
+ Iterator(std::shared_ptr<Database>, IndexId);
+ Iterator(std::shared_ptr<Database>, const IndexKey::Header&);
- auto Next(Callback) -> void;
- auto NextSync() -> std::optional<IndexRecord>;
+ Iterator(const Iterator&) = default;
+ Iterator& operator=(const Iterator& other) = default;
- auto Prev(Callback) -> void;
+ auto value() const -> const std::optional<Record>&;
+ std::optional<Record> operator*() const { return value(); }
- auto PeekSync() -> std::optional<IndexRecord>;
+ auto next() -> void;
+ std::optional<Record> operator++() {
+ next();
+ return value();
+ }
+ std::optional<Record> operator++(int) {
+ auto val = value();
+ next();
+ return val;
+ }
- auto Size() const -> size_t;
+ auto prev() -> void;
+ std::optional<Record> operator--() {
+ prev();
+ return value();
+ }
+ std::optional<Record> operator--(int) {
+ auto val = value();
+ prev();
+ return val;
+ }
- auto cbor() const -> cppbor::Array&&;
+ auto count() const -> size_t;
private:
- Iterator(std::weak_ptr<Database>,
- std::optional<Continuation>&&,
- std::optional<Continuation>&&);
+ auto iterate(const SearchKey& key) -> void;
friend class TrackIterator;
- auto InvokeNull(Callback) -> void;
-
std::weak_ptr<Database> db_;
-
- std::mutex pos_mutex_;
- std::optional<Continuation> current_pos_;
- std::optional<Continuation> prev_pos_;
+ SearchKey key_;
+ std::optional<Record> current_;
};
class TrackIterator {
public:
- static auto Parse(std::weak_ptr<Database>, const cppbor::Array&)
- -> std::optional<TrackIterator>;
-
TrackIterator(const Iterator&);
- TrackIterator(const TrackIterator&);
- TrackIterator& operator=(TrackIterator&& other);
+ TrackIterator(const TrackIterator&) = default;
+ TrackIterator& operator=(TrackIterator&& other) = default;
+
+ auto value() const -> std::optional<TrackId>;
+ std::optional<TrackId> operator*() const { return value(); }
- auto Next() -> std::optional<TrackId>;
- auto Size() const -> size_t;
+ auto next() -> void;
+ std::optional<TrackId> operator++() {
+ next();
+ return value();
+ }
+ std::optional<TrackId> operator++(int) {
+ auto val = value();
+ next();
+ return val;
+ }
- auto cbor() const -> cppbor::Array&&;
+ auto count() const -> size_t;
private:
TrackIterator(std::weak_ptr<Database>);
-
- auto NextLeaf() -> void;
+ auto next(bool advance) -> void;
std::weak_ptr<Database> db_;
std::vector<Iterator> levels_;
diff --git a/src/database/include/file_gatherer.hpp b/src/database/include/file_gatherer.hpp
index 378727f7..66127bb7 100644
--- a/src/database/include/file_gatherer.hpp
+++ b/src/database/include/file_gatherer.hpp
@@ -20,16 +20,16 @@ class IFileGatherer {
virtual ~IFileGatherer(){};
virtual auto FindFiles(
- const std::pmr::string& root,
- std::function<void(const std::pmr::string&, const FILINFO&)> cb)
+ const std::string& root,
+ std::function<void(const std::string&, const FILINFO&)> cb)
-> void = 0;
};
class FileGathererImpl : public IFileGatherer {
public:
virtual auto FindFiles(
- const std::pmr::string& root,
- std::function<void(const std::pmr::string&, const FILINFO&)> cb)
+ const std::string& root,
+ std::function<void(const std::string&, const FILINFO&)> cb)
-> void override;
};
diff --git a/src/database/include/tag_parser.hpp b/src/database/include/tag_parser.hpp
index 04817c59..977c9afc 100644
--- a/src/database/include/tag_parser.hpp
+++ b/src/database/include/tag_parser.hpp
@@ -16,24 +16,24 @@ namespace database {
class ITagParser {
public:
virtual ~ITagParser() {}
- virtual auto ReadAndParseTags(const std::pmr::string& path)
+ virtual auto ReadAndParseTags(const std::string& path)
-> std::shared_ptr<TrackTags> = 0;
};
class GenericTagParser : public ITagParser {
public:
- auto ReadAndParseTags(const std::pmr::string& path)
+ auto ReadAndParseTags(const std::string& path)
-> std::shared_ptr<TrackTags> override;
};
class TagParserImpl : public ITagParser {
public:
TagParserImpl();
- auto ReadAndParseTags(const std::pmr::string& path)
+ auto ReadAndParseTags(const std::string& path)
-> std::shared_ptr<TrackTags> override;
private:
- std::map<std::pmr::string, std::unique_ptr<ITagParser>> extension_to_parser_;
+ std::map<std::string, std::unique_ptr<ITagParser>> extension_to_parser_;
GenericTagParser generic_parser_;
/*
@@ -43,14 +43,14 @@ class TagParserImpl : public ITagParser {
std::mutex cache_mutex_;
util::LruCache<16, std::pmr::string, std::shared_ptr<TrackTags>> cache_;
- // We could also consider keeping caches of artist name -> std::pmr::string
- // and similar. This hasn't been done yet, as this isn't a common workload in
+ // We could also consider keeping caches of artist name -> std::string and
+ // similar. This hasn't been done yet, as this isn't a common workload in
// any of our UI.
};
class OpusTagParser : public ITagParser {
public:
- auto ReadAndParseTags(const std::pmr::string& path)
+ auto ReadAndParseTags(const std::string& path)
-> std::shared_ptr<TrackTags> override;
};
diff --git a/src/database/include/track.hpp b/src/database/include/track.hpp
index 8a24024f..0497c94d 100644
--- a/src/database/include/track.hpp
+++ b/src/database/include/track.hpp
@@ -123,7 +123,7 @@ struct TrackData {
public:
TrackData()
: id(0),
- filepath(&memory::kSpiRamResource),
+ filepath(),
tags_hash(0),
individual_tag_hashes(&memory::kSpiRamResource),
is_tombstoned(false),
diff --git a/src/database/tag_parser.cpp b/src/database/tag_parser.cpp
index eb5f3a43..885c71dd 100644
--- a/src/database/tag_parser.cpp
+++ b/src/database/tag_parser.cpp
@@ -32,7 +32,7 @@ const static std::array<std::pair<const char*, Tag>, 5> kVorbisIdToTag = {{
static auto convert_track_number(int number) -> std::pmr::string {
std::ostringstream oss;
oss << std::setw(4) << std::setfill('0') << number;
- return std::pmr::string(oss.str());
+ return std::pmr::string(oss.str(), &memory::kSpiRamResource);
}
static auto convert_track_number(const std::pmr::string& raw)
@@ -131,11 +131,12 @@ TagParserImpl::TagParserImpl() {
extension_to_parser_["opus"] = std::make_unique<OpusTagParser>();
}
-auto TagParserImpl::ReadAndParseTags(const std::pmr::string& path)
+auto TagParserImpl::ReadAndParseTags(const std::string& path)
-> std::shared_ptr<TrackTags> {
{
std::lock_guard<std::mutex> lock{cache_mutex_};
- std::optional<std::shared_ptr<TrackTags>> cached = cache_.Get(path);
+ std::optional<std::shared_ptr<TrackTags>> cached =
+ cache_.Get({path.data(), path.size()});
if (cached) {
return *cached;
}
@@ -143,8 +144,8 @@ auto TagParserImpl::ReadAndParseTags(const std::pmr::string& path)
ITagParser* parser = &generic_parser_;
auto dot_pos = path.find_last_of(".");
- if (dot_pos != std::pmr::string::npos && path.size() - dot_pos > 1) {
- std::pmr::string extension = path.substr(dot_pos + 1);
+ if (dot_pos != std::string::npos && path.size() - dot_pos > 1) {
+ std::string extension = path.substr(dot_pos + 1);
std::transform(extension.begin(), extension.end(), extension.begin(),
[](unsigned char c) { return std::tolower(c); });
if (extension_to_parser_.contains(extension)) {
@@ -162,8 +163,9 @@ auto TagParserImpl::ReadAndParseTags(const std::pmr::string& path)
// start.
if (!tags->at(Tag::kAlbumTrack)) {
auto slash_pos = path.find_last_of("/");
- if (slash_pos != std::pmr::string::npos && path.size() - slash_pos > 1) {
- tags->set(Tag::kAlbumTrack, path.substr(slash_pos + 1));
+ if (slash_pos != std::string::npos && path.size() - slash_pos > 1) {
+ std::string trunc = path.substr(slash_pos + 1);
+ tags->set(Tag::kAlbumTrack, {trunc.data(), trunc.size()});
}
}
@@ -174,13 +176,13 @@ auto TagParserImpl::ReadAndParseTags(const std::pmr::string& path)
{
std::lock_guard<std::mutex> lock{cache_mutex_};
- cache_.Put(path, tags);
+ cache_.Put({path.data(), path.size(), &memory::kSpiRamResource}, tags);
}
return tags;
}
-auto GenericTagParser::ReadAndParseTags(const std::pmr::string& path)
+auto GenericTagParser::ReadAndParseTags(const std::string& path)
-> std::shared_ptr<TrackTags> {
libtags::Aux aux;
auto out = std::make_shared<TrackTags>();
@@ -254,10 +256,10 @@ auto GenericTagParser::ReadAndParseTags(const std::pmr::string& path)
return out;
}
-auto OpusTagParser::ReadAndParseTags(const std::pmr::string& path)
+auto OpusTagParser::ReadAndParseTags(const std::string& path)
-> std::shared_ptr<TrackTags> {
auto lock = drivers::acquire_spi();
- std::pmr::string vfs_path = "/sdcard" + path;
+ std::string vfs_path = "/sdcard" + path;
int err;
OggOpusFile* f = op_test_file(vfs_path.c_str(), &err);
if (f == NULL) {
diff --git a/src/lua/bridge.cpp b/src/lua/bridge.cpp
index 070dee98..8d7b4fd0 100644
--- a/src/lua/bridge.cpp
+++ b/src/lua/bridge.cpp
@@ -37,23 +37,8 @@ static auto open_settings_fn(lua_State* state) -> int {
return 0;
}
-static auto open_now_playing_fn(lua_State* state) -> int {
- events::Ui().Dispatch(ui::internal::ShowNowPlaying{});
- return 0;
-}
-
-static auto open_browse_fn(lua_State* state) -> int {
- int index = luaL_checkinteger(state, 1);
- events::Ui().Dispatch(ui::internal::IndexSelected{
- .id = static_cast<uint8_t>(index),
- });
- return 0;
-}
-
static const struct luaL_Reg kLegacyUiFuncs[] = {
{"open_settings", open_settings_fn},
- {"open_now_playing", open_now_playing_fn},
- {"open_browse", open_browse_fn},
{NULL, NULL}};
static auto lua_legacy_ui(lua_State* state) -> int {
diff --git a/src/lua/lua_database.cpp b/src/lua/lua_database.cpp
index d41f4794..82b22343 100644
--- a/src/lua/lua_database.cpp
+++ b/src/lua/lua_database.cpp
@@ -8,7 +8,10 @@
#include <memory>
#include <string>
+#include <type_traits>
+#include <variant>
+#include "bridge.hpp"
#include "lua.hpp"
#include "esp_log.h"
@@ -34,6 +37,17 @@ static constexpr char kDbIndexMetatable[] = "db_index";
static constexpr char kDbRecordMetatable[] = "db_record";
static constexpr char kDbIteratorMetatable[] = "db_iterator";
+struct LuaIndexInfo {
+ database::IndexId id;
+ size_t name_size;
+ char name_data[];
+
+ auto name() -> std::string_view { return {name_data, name_size}; }
+};
+
+static_assert(std::is_trivially_destructible<LuaIndexInfo>());
+static_assert(std::is_trivially_copy_assignable<LuaIndexInfo>());
+
static auto indexes(lua_State* state) -> int {
Bridge* instance = Bridge::Get(state);
@@ -44,11 +58,15 @@ static auto indexes(lua_State* state) -> int {
return 1;
}
- for (const auto& i : db->GetIndexes()) {
- database::IndexInfo** data = reinterpret_cast<database::IndexInfo**>(
- lua_newuserdata(state, sizeof(uintptr_t)));
+ for (const auto& i : db->getIndexes()) {
+ LuaIndexInfo* data = reinterpret_cast<LuaIndexInfo*>(
+ lua_newuserdata(state, sizeof(LuaIndexInfo) + i.name.size()));
luaL_setmetatable(state, kDbIndexMetatable);
- *data = new database::IndexInfo{i};
+ *data = LuaIndexInfo{
+ .id = i.id,
+ .name_size = i.name.size(),
+ };
+ std::memcpy(data->name_data, i.name.data(), i.name.size());
lua_rawseti(state, -2, i.id);
}
@@ -65,33 +83,28 @@ static const struct luaL_Reg kDatabaseFuncs[] = {{"indexes", indexes},
* trivially copyable.
*/
struct LuaRecord {
- database::TrackId id_or_zero;
- database::IndexKey::Header header_at_next_depth;
+ std::variant<database::TrackId, database::IndexKey::Header> contents;
size_t text_size;
char text[];
};
-static_assert(std::is_trivially_copyable_v<LuaRecord> == true);
-
-static auto push_lua_record(lua_State* L, const database::IndexRecord& r)
- -> void {
- // Bake out the text into something concrete.
- auto text = r.text().value_or("");
+static_assert(std::is_trivially_destructible<LuaRecord>());
+static_assert(std::is_trivially_copy_assignable<LuaRecord>());
+static auto push_lua_record(lua_State* L, const database::Record& r) -> void {
// Create and init the userdata.
LuaRecord* record = reinterpret_cast<LuaRecord*>(
- lua_newuserdata(L, sizeof(LuaRecord) + text.size()));
+ lua_newuserdata(L, sizeof(LuaRecord) + r.text().size()));
luaL_setmetatable(L, kDbRecordMetatable);
// Init all the fields
*record = {
- .id_or_zero = r.track().value_or(0),
- .header_at_next_depth = r.ExpandHeader(),
- .text_size = text.size(),
+ .contents = r.contents(),
+ .text_size = r.text().size(),
};
// Copy the string data across.
- std::memcpy(record->text, text.data(), text.size());
+ std::memcpy(record->text, r.text().data(), r.text().size());
}
auto db_check_iterator(lua_State* L, int stack_pos) -> database::Iterator* {
@@ -100,49 +113,30 @@ auto db_check_iterator(lua_State* L, int stack_pos) -> database::Iterator* {
return it;
}
-static auto push_iterator(lua_State* state,
- std::variant<database::Iterator*,
- database::Continuation,
- database::IndexInfo> val) -> void {
- Bridge* instance = Bridge::Get(state);
+static auto push_iterator(lua_State* state, const database::Iterator& it)
+ -> void {
database::Iterator** data = reinterpret_cast<database::Iterator**>(
lua_newuserdata(state, sizeof(uintptr_t)));
- std::visit(
- [&](auto&& arg) {
- using T = std::decay_t<decltype(arg)>;
- if constexpr (std::is_same_v<T, database::Iterator*>) {
- *data = new database::Iterator(*arg);
- } else {
- *data = new database::Iterator(instance->services().database(), arg);
- }
- },
- val);
+ *data = new database::Iterator(it);
luaL_setmetatable(state, kDbIteratorMetatable);
}
static auto db_iterate(lua_State* state) -> int {
database::Iterator* it = db_check_iterator(state, 1);
- luaL_checktype(state, 2, LUA_TFUNCTION);
- int callback_ref = luaL_ref(state, LUA_REGISTRYINDEX);
-
- it->Next([=](std::optional<database::IndexRecord> res) {
- events::Ui().RunOnTask([=]() {
- lua_rawgeti(state, LUA_REGISTRYINDEX, callback_ref);
- if (res) {
- push_lua_record(state, *res);
- } else {
- lua_pushnil(state);
- }
- CallProtected(state, 1, 0);
- luaL_unref(state, LUA_REGISTRYINDEX, callback_ref);
- });
- });
- return 0;
+ std::optional<database::Record> res = (*it)++;
+
+ if (res) {
+ push_lua_record(state, *res);
+ } else {
+ lua_pushnil(state);
+ }
+
+ return 1;
}
static auto db_iterator_clone(lua_State* state) -> int {
database::Iterator* it = db_check_iterator(state, 1);
- push_iterator(state, it);
+ push_iterator(state, *it);
return 1;
}
@@ -154,6 +148,7 @@ static auto db_iterator_gc(lua_State* state) -> int {
static const struct luaL_Reg kDbIteratorFuncs[] = {{"next", db_iterate},
{"clone", db_iterator_clone},
+ {"__call", db_iterate},
{"__gc", db_iterator_gc},
{NULL, NULL}};
@@ -168,18 +163,22 @@ static auto record_contents(lua_State* state) -> int {
LuaRecord* data = reinterpret_cast<LuaRecord*>(
luaL_checkudata(state, 1, kDbRecordMetatable));
- if (data->id_or_zero) {
- lua_pushinteger(state, data->id_or_zero);
- } else {
- std::string p = database::EncodeIndexPrefix(data->header_at_next_depth);
- push_iterator(state, database::Continuation{
- .prefix = {p.data(), p.size()},
- .start_key = {p.data(), p.size()},
- .forward = true,
- .was_prev_forward = true,
- .page_size = 1,
- });
- }
+ std::visit(
+ [&](auto&& arg) {
+ using T = std::decay_t<decltype(arg)>;
+ if constexpr (std::is_same_v<T, database::TrackId>) {
+ lua_pushinteger(state, arg);
+ } else if constexpr (std::is_same_v<T, database::IndexKey::Header>) {
+ Bridge* bridge = Bridge::Get(state);
+ auto db = bridge->services().database().lock();
+ if (!db) {
+ lua_pushnil(state);
+ } else {
+ push_iterator(state, database::Iterator{db, arg});
+ }
+ }
+ },
+ data->contents);
return 1;
}
@@ -190,38 +189,33 @@ static const struct luaL_Reg kDbRecordFuncs[] = {{"title", record_text},
{NULL, NULL}};
static auto index_name(lua_State* state) -> int {
- database::IndexInfo** data = reinterpret_cast<database::IndexInfo**>(
+ LuaIndexInfo* data = reinterpret_cast<LuaIndexInfo*>(
luaL_checkudata(state, 1, kDbIndexMetatable));
if (data == NULL) {
return 0;
}
- lua_pushstring(state, (*data)->name.c_str());
+ lua_pushlstring(state, data->name_data, data->name_size);
return 1;
}
static auto index_iter(lua_State* state) -> int {
- database::IndexInfo** data = reinterpret_cast<database::IndexInfo**>(
+ LuaIndexInfo* data = reinterpret_cast<LuaIndexInfo*>(
luaL_checkudata(state, 1, kDbIndexMetatable));
if (data == NULL) {
return 0;
}
- push_iterator(state, **data);
- return 1;
-}
-
-static auto index_gc(lua_State* state) -> int {
- database::IndexInfo** data = reinterpret_cast<database::IndexInfo**>(
- luaL_checkudata(state, 1, kDbIndexMetatable));
- if (data != NULL) {
- delete *data;
+ Bridge* bridge = Bridge::Get(state);
+ auto db = bridge->services().database().lock();
+ if (!db) {
+ lua_pushnil(state);
}
- return 0;
+ push_iterator(state, database::Iterator{db, data->id});
+ return 1;
}
static const struct luaL_Reg kDbIndexFuncs[] = {{"name", index_name},
{"iter", index_iter},
{"__tostring", index_name},
- {"__gc", index_gc},
{NULL, NULL}};
static auto lua_database(lua_State* state) -> int {
diff --git a/src/lua/lua_queue.cpp b/src/lua/lua_queue.cpp
index fadcb51c..69d3b03d 100644
--- a/src/lua/lua_queue.cpp
+++ b/src/lua/lua_queue.cpp
@@ -21,7 +21,6 @@
#include "index.hpp"
#include "property.hpp"
#include "service_locator.hpp"
-#include "source.hpp"
#include "track.hpp"
#include "track_queue.hpp"
#include "ui_events.hpp"
@@ -37,15 +36,13 @@ static auto queue_add(lua_State* state) -> int {
database::TrackId id = luaL_checkinteger(state, 1);
instance->services().bg_worker().Dispatch<void>([=]() {
audio::TrackQueue& queue = instance->services().track_queue();
- auto editor = queue.Edit();
- queue.Append(editor, id);
+ queue.append(id);
});
} else {
- database::Iterator it = *db_check_iterator(state, 1);
+ database::Iterator* it = db_check_iterator(state, 1);
instance->services().bg_worker().Dispatch<void>([=]() {
audio::TrackQueue& queue = instance->services().track_queue();
- auto editor = queue.Edit();
- queue.Append(editor, database::TrackIterator{it});
+ queue.append(database::TrackIterator{*it});
});
}
@@ -55,8 +52,7 @@ static auto queue_add(lua_State* state) -> int {
static auto queue_clear(lua_State* state) -> int {
Bridge* instance = Bridge::Get(state);
audio::TrackQueue& queue = instance->services().track_queue();
- auto editor = queue.Edit();
- queue.Clear(editor);
+ queue.clear();
return 0;
}
diff --git a/src/playlist/CMakeLists.txt b/src/playlist/CMakeLists.txt
deleted file mode 100644
index 6c08dd5a..00000000
--- a/src/playlist/CMakeLists.txt
+++ /dev/null
@@ -1,10 +0,0 @@
-# Copyright 2023 jacqueline <me@jacqueline.id.au>
-#
-# SPDX-License-Identifier: GPL-3.0-only
-
-idf_component_register(
- SRCS "source.cpp" "shuffler.cpp"
- INCLUDE_DIRS "include"
- REQUIRES "database" "util")
-
-target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS})
diff --git a/src/playlist/include/shuffler.hpp b/src/playlist/include/shuffler.hpp
deleted file mode 100644
index affc6301..00000000
--- a/src/playlist/include/shuffler.hpp
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright 2023 jacqueline <me@jacqueline.id.au>
- *
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-#pragma once
-
-#include <deque>
-#include <memory>
-#include <mutex>
-#include <variant>
-#include <vector>
-
-#include "bloom_filter.hpp"
-#include "database.hpp"
-#include "future_fetcher.hpp"
-#include "random.hpp"
-#include "source.hpp"
-#include "track.hpp"
-
-namespace playlist {
-
-/*
- * A source composes of other sources and/or specific extra tracks. Supports
- * iteration over its contents in a random order.
- */
-class Shuffler : public ISource {
- public:
- static auto Create() -> Shuffler*;
-
- explicit Shuffler(
- util::IRandom* random,
- std::unique_ptr<util::BloomFilter<database::TrackId>> filter);
-
- auto Current() -> std::optional<database::TrackId> override;
- auto Advance() -> std::optional<database::TrackId> override;
- auto Peek(std::size_t, std::vector<database::TrackId>*)
- -> std::size_t override;
-
- typedef std::variant<database::TrackId, std::shared_ptr<IResetableSource>>
- Item;
- auto Add(Item) -> void;
-
- /*
- * Returns the enqueued items, starting from the current item, in their
- * original insertion order.
- */
- auto Unshuffle() -> std::vector<Item>;
-
- // Not copyable or movable.
-
- Shuffler(const Shuffler&) = delete;
- Shuffler& operator=(const Shuffler&) = delete;
-
- private:
- auto RefillBuffer() -> void;
-
- util::IRandom* random_;
-
- std::unique_ptr<util::BloomFilter<database::TrackId>> already_played_;
- bool out_of_items_;
-
- std::deque<Item> ordered_items_;
- std::deque<database::TrackId> shuffled_items_buffer_;
-};
-
-} // namespace playlist
diff --git a/src/playlist/include/source.hpp b/src/playlist/include/source.hpp
deleted file mode 100644
index ce12faf3..00000000
--- a/src/playlist/include/source.hpp
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * Copyright 2023 jacqueline <me@jacqueline.id.au>
- *
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-#pragma once
-
-#include <deque>
-#include <memory>
-#include <mutex>
-#include <stack>
-#include <variant>
-#include <vector>
-
-#include "bloom_filter.hpp"
-#include "database.hpp"
-#include "future_fetcher.hpp"
-#include "random.hpp"
-#include "track.hpp"
-
-namespace playlist {
-
-/*
- * Stateful interface for iterating over a collection of tracks by id.
- */
-class ISource {
- public:
- virtual ~ISource() {}
-
- virtual auto Current() -> std::optional<database::TrackId> = 0;
-
- /*
- * Discards the current track id and continues to the next in this source.
- * Returns the new current track id.
- */
- virtual auto Advance() -> std::optional<database::TrackId> = 0;
-
- /*
- * Repeatedly advances until a track with the given id is the current track.
- * Returns false if this source ran out of tracks before the requested id
- * was encounted, true otherwise.
- */
- virtual auto AdvanceTo(database::TrackId id) -> bool {
- for (auto t = Current(); t.has_value(); t = Advance()) {
- if (*t == id) {
- return true;
- }
- }
- return false;
- }
-
- /*
- * Places the next n tracks into the given vector, in order. Does not change
- * the value returned by Current().
- */
- virtual auto Peek(std::size_t n, std::vector<database::TrackId>*)
- -> std::size_t = 0;
-};
-
-/*
- * A Source that supports restarting iteration from its original initial
- * value.
- */
-class IResetableSource : public ISource {
- public:
- virtual ~IResetableSource() {}
-
- virtual auto Previous() -> std::optional<database::TrackId> = 0;
-
- /*
- * Restarts iteration from this source's initial value.
- */
- virtual auto Reset() -> void = 0;
-};
-
-class IteratorSource : public IResetableSource {
- public:
- IteratorSource(const database::Iterator&);
-
- auto Current() -> std::optional<database::TrackId> override;
- auto Advance() -> std::optional<database::TrackId> override;
- auto Peek(std::size_t n, std::vector<database::TrackId>*)
- -> std::size_t override;
-
- auto Previous() -> std::optional<database::TrackId> override;
- auto Reset() -> void override;
-
- private:
- const database::Iterator& start_;
- std::optional<database::TrackId> current_;
- std::stack<database::Iterator, std::vector<database::Iterator>> next_;
-};
-
-auto CreateSourceFromResults(
- std::weak_ptr<database::Database>,
- std::shared_ptr<database::Result<database::IndexRecord>>)
- -> std::shared_ptr<IResetableSource>;
-
-class IndexRecordSource : public IResetableSource {
- public:
- IndexRecordSource(std::weak_ptr<database::Database> db,
- std::shared_ptr<database::Result<database::IndexRecord>>);
-
- IndexRecordSource(std::weak_ptr<database::Database> db,
- std::shared_ptr<database::Result<database::IndexRecord>>,
- std::size_t,
- std::shared_ptr<database::Result<database::IndexRecord>>,
- std::size_t);
-
- auto Current() -> std::optional<database::TrackId> override;
- auto Advance() -> std::optional<database::TrackId> override;
- auto Peek(std::size_t n, std::vector<database::TrackId>*)
- -> std::size_t override;
-
- auto Previous() -> std::optional<database::TrackId> override;
- auto Reset() -> void override;
-
- private:
- std::weak_ptr<database::Database> db_;
-
- std::shared_ptr<database::Result<database::IndexRecord>> initial_page_;
- ssize_t initial_item_;
-
- std::shared_ptr<database::Result<database::IndexRecord>> current_page_;
- ssize_t current_item_;
-};
-
-class NestedSource : public IResetableSource {
- public:
- NestedSource(std::weak_ptr<database::Database> db,
- std::shared_ptr<database::Result<database::IndexRecord>>);
-
- auto Current() -> std::optional<database::TrackId> override;
- auto Advance() -> std::optional<database::TrackId> override;
- auto Peek(std::size_t n, std::vector<database::TrackId>*)
- -> std::size_t override;
-
- auto Previous() -> std::optional<database::TrackId> override;
- auto Reset() -> void override;
-
- private:
- auto CreateChild(std::shared_ptr<database::IndexRecord> page)
- -> std::shared_ptr<IResetableSource>;
-
- std::weak_ptr<database::Database> db_;
-
- std::shared_ptr<database::Result<database::IndexRecord>> initial_page_;
- ssize_t initial_item_;
-
- std::shared_ptr<database::Result<database::IndexRecord>> current_page_;
- ssize_t current_item_;
-
- std::shared_ptr<IResetableSource> current_child_;
-};
-
-} // namespace playlist
diff --git a/src/playlist/shuffler.cpp b/src/playlist/shuffler.cpp
deleted file mode 100644
index b1c92335..00000000
--- a/src/playlist/shuffler.cpp
+++ /dev/null
@@ -1,166 +0,0 @@
-/*
- * Copyright 2023 jacqueline <me@jacqueline.id.au>
- *
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-#include "shuffler.hpp"
-
-#include <algorithm>
-#include <functional>
-#include <memory>
-#include <set>
-#include <variant>
-
-#include "bloom_filter.hpp"
-#include "database.hpp"
-#include "komihash.h"
-#include "random.hpp"
-#include "track.hpp"
-
-static constexpr std::size_t kShufflerBufferSize = 32;
-
-namespace playlist {
-
-auto Shuffler::Create() -> Shuffler* {
- return new Shuffler(util::sRandom,
- std::make_unique<util::BloomFilter<database::TrackId>>(
- [](database::TrackId id) {
- return komihash(&id, sizeof(database::TrackId), 0);
- }));
-}
-
-Shuffler::Shuffler(util::IRandom* random,
- std::unique_ptr<util::BloomFilter<database::TrackId>> filter)
- : random_(random), already_played_(std::move(filter)) {}
-
-auto Shuffler::Current() -> std::optional<database::TrackId> {
- if (shuffled_items_buffer_.empty()) {
- return {};
- }
- return shuffled_items_buffer_.front();
-}
-
-auto Shuffler::Advance() -> std::optional<database::TrackId> {
- if (shuffled_items_buffer_.empty() && !out_of_items_) {
- RefillBuffer();
- }
-
- auto res = Current();
- if (res) {
- // Mark tracks off in the bloom filter only *after* they've been advanced
- // past. This gives us the most flexibility for reshuffling when adding new
- // items.
- already_played_->Insert(*res);
- shuffled_items_buffer_.pop_front();
- }
- return res;
-}
-
-auto Shuffler::Peek(std::size_t num, std::vector<database::TrackId>* out)
- -> std::size_t {
- if (shuffled_items_buffer_.size() < num) {
- RefillBuffer();
- }
- for (int i = 0; i < num; i++) {
- if (i >= shuffled_items_buffer_.size()) {
- // We must be out of data, since the buffer didn't fill up.
- return i;
- }
- out->push_back(shuffled_items_buffer_.at(i));
- }
- return num;
-}
-
-auto Shuffler::Add(Item item) -> void {
- ordered_items_.push_back(item);
- out_of_items_ = false;
-
- // Empty out the buffer of already shuffled items, since we will need to
- // shuffle again in order to incorporate the newly added item(s). We keep the
- // current item however because we wouldn't want Add() to change the value of
- // Current() unless we're completely out of items.
- if (shuffled_items_buffer_.size() > 1) {
- shuffled_items_buffer_.erase(shuffled_items_buffer_.begin() + 1,
- shuffled_items_buffer_.end());
- }
- RefillBuffer();
-}
-
-auto Shuffler::Unshuffle() -> std::vector<Item> {
- std::vector<Item> ret;
- database::TrackId current = shuffled_items_buffer_.front();
- bool has_found_current = false;
-
- for (const Item& item : ordered_items_) {
- if (!has_found_current) {
- // TODO(jacqueline): *Should* this include previous items? What is the
- // 'previous' button meant to do after unshuffling?
- if (std::holds_alternative<database::TrackId>(item)) {
- has_found_current = current == std::get<database::TrackId>(item);
- } else {
- auto source = std::get<std::shared_ptr<IResetableSource>>(item);
- source->Reset();
- has_found_current =
- std::get<std::shared_ptr<IResetableSource>>(item)->AdvanceTo(
- current);
- }
- } else {
- ret.push_back(item);
- }
- }
-
- return ret;
-}
-
-auto Shuffler::RefillBuffer() -> void {
- // Don't waste time iterating if we know there's nothing new.
- if (out_of_items_) {
- return;
- }
-
- int num_to_sample = kShufflerBufferSize - shuffled_items_buffer_.size();
- int resovoir_offset = shuffled_items_buffer_.size();
-
- std::set<database::TrackId> in_buffer;
- for (const database::TrackId& id : shuffled_items_buffer_) {
- in_buffer.insert(id);
- }
-
- uint32_t i = 0;
- auto consider_item = [&, this](const database::TrackId& item) {
- if (already_played_->Contains(item) || in_buffer.contains(item)) {
- return;
- }
- if (i < num_to_sample) {
- shuffled_items_buffer_.push_back(item);
- } else {
- uint32_t index_to_replace = random_->RangeInclusive(0, i);
- if (index_to_replace < num_to_sample) {
- shuffled_items_buffer_[resovoir_offset + index_to_replace] = item;
- }
- }
- i++;
- };
-
- for (const Item& item : ordered_items_) {
- if (std::holds_alternative<database::TrackId>(item)) {
- std::invoke(consider_item, std::get<database::TrackId>(item));
- } else {
- auto source = std::get<std::shared_ptr<IResetableSource>>(item);
- source->Reset();
- while (source->Advance()) {
- std::invoke(consider_item, *source->Current());
- }
- }
- }
-
- out_of_items_ = i > num_to_sample;
- // We've now got a random *selection*, but the order might be predictable
- // (e.g. if there were only `num_to_sample` new items). Do a final in-memory
- // shuffle.
- std::random_shuffle(shuffled_items_buffer_.begin() + resovoir_offset,
- shuffled_items_buffer_.end());
-}
-
-} // namespace playlist
diff --git a/src/playlist/source.cpp b/src/playlist/source.cpp
deleted file mode 100644
index 2540c3fb..00000000
--- a/src/playlist/source.cpp
+++ /dev/null
@@ -1,360 +0,0 @@
-/*
- * Copyright 2023 jacqueline <me@jacqueline.id.au>
- *
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-#include "source.hpp"
-
-#include <algorithm>
-#include <functional>
-#include <memory>
-#include <set>
-#include <variant>
-
-#include "esp_log.h"
-
-#include "bloom_filter.hpp"
-#include "database.hpp"
-#include "komihash.h"
-#include "random.hpp"
-#include "track.hpp"
-
-namespace playlist {
-
-[[maybe_unused]] static constexpr char kTag[] = "queue_src";
-
-IteratorSource::IteratorSource(const database::Iterator& it)
- : start_(it), current_(), next_() {
- Reset();
- Advance();
-}
-
-auto IteratorSource::Current() -> std::optional<database::TrackId> {
- return current_;
-}
-
-auto IteratorSource::Advance() -> std::optional<database::TrackId> {
- ESP_LOGI(kTag, "advancing");
- while (!next_.empty()) {
- auto next = next_.top().NextSync();
- if (!next) {
- ESP_LOGI(kTag, "top was empty");
- next_.pop();
- continue;
- }
- if (next->track()) {
- ESP_LOGI(kTag, "top held track %lu", next->track().value_or(0));
- current_ = next->track();
- return current_;
- }
- ESP_LOGI(kTag, "top held records");
- next_.push(database::Iterator(start_.database(), next->Expand(1).value()));
- }
- ESP_LOGI(kTag, "exhausted");
- return {};
-}
-
-auto IteratorSource::Peek(std::size_t n, std::vector<database::TrackId>*)
- -> std::size_t {
- return 0;
-}
-
-auto IteratorSource::Previous() -> std::optional<database::TrackId> {
- return {};
-}
-
-auto IteratorSource::Reset() -> void {
- while (!next_.empty()) {
- next_.pop();
- }
- next_.push(start_);
-}
-
-auto CreateSourceFromResults(
- std::weak_ptr<database::Database> db,
- std::shared_ptr<database::Result<database::IndexRecord>> results)
- -> std::shared_ptr<IResetableSource> {
- if (results->values()[0]->track()) {
- return std::make_shared<IndexRecordSource>(db, results);
- } else {
- return std::make_shared<NestedSource>(db, results);
- }
-}
-
-IndexRecordSource::IndexRecordSource(
- std::weak_ptr<database::Database> db,
- std::shared_ptr<database::Result<database::IndexRecord>> initial)
- : db_(db),
- initial_page_(initial),
- initial_item_(0),
- current_page_(initial_page_),
- current_item_(initial_item_) {}
-
-IndexRecordSource::IndexRecordSource(
- std::weak_ptr<database::Database> db,
- std::shared_ptr<database::Result<database::IndexRecord>> initial,
- std::size_t initial_index,
- std::shared_ptr<database::Result<database::IndexRecord>> current,
- std::size_t current_index)
- : db_(db),
- initial_page_(initial),
- initial_item_(initial_index),
- current_page_(current),
- current_item_(current_index) {}
-
-auto IndexRecordSource::Current() -> std::optional<database::TrackId> {
- if (current_page_->values().size() <= current_item_) {
- return {};
- }
- if (current_page_ == initial_page_ && current_item_ < initial_item_) {
- return {};
- }
-
- return current_page_->values().at(current_item_)->track();
-}
-
-auto IndexRecordSource::Advance() -> std::optional<database::TrackId> {
- current_item_++;
- if (current_item_ >= current_page_->values().size()) {
- auto next_page = current_page_->next_page();
- if (!next_page) {
- current_item_--;
- return {};
- }
-
- auto db = db_.lock();
- if (!db) {
- return {};
- }
-
- current_page_.reset(db->GetPage<database::IndexRecord>(&*next_page).get());
- current_item_ = 0;
- }
-
- return Current();
-}
-
-auto IndexRecordSource::Previous() -> std::optional<database::TrackId> {
- if (current_page_ == initial_page_ && current_item_ <= initial_item_) {
- return {};
- }
-
- current_item_--;
- if (current_item_ < 0) {
- auto prev_page = current_page_->prev_page();
- if (!prev_page) {
- return {};
- }
-
- auto db = db_.lock();
- if (!db) {
- return {};
- }
-
- current_page_.reset(db->GetPage<database::IndexRecord>(&*prev_page).get());
- current_item_ = current_page_->values().size() - 1;
- }
-
- return Current();
-}
-
-auto IndexRecordSource::Peek(std::size_t n, std::vector<database::TrackId>* out)
- -> std::size_t {
- if (current_page_->values().size() <= current_item_) {
- return {};
- }
-
- auto db = db_.lock();
- if (!db) {
- return 0;
- }
-
- std::size_t items_added = 0;
-
- std::shared_ptr<database::Result<database::IndexRecord>> working_page =
- current_page_;
- std::size_t working_item = current_item_ + 1;
-
- while (n > 0) {
- if (working_item >= working_page->values().size()) {
- auto next_page = current_page_->next_page();
- if (!next_page) {
- break;
- }
- // TODO(jacqueline): It would probably be a good idea to hold onto these
- // peeked pages, to avoid needing to look them up again later.
- working_page.reset(db->GetPage<database::IndexRecord>(&*next_page).get());
- working_item = 0;
- }
-
- auto record = working_page->values().at(working_item);
- if (record->track()) {
- out->push_back(record->track().value());
- n--;
- items_added++;
- }
- working_item++;
- }
-
- return items_added;
-}
-
-auto IndexRecordSource::Reset() -> void {
- current_page_ = initial_page_;
- current_item_ = initial_item_;
-}
-
-NestedSource::NestedSource(
- std::weak_ptr<database::Database> db,
- std::shared_ptr<database::Result<database::IndexRecord>> initial)
- : db_(db),
- initial_page_(initial),
- initial_item_(0),
- current_page_(initial_page_),
- current_item_(initial_item_),
- current_child_(CreateChild(initial->values()[0])) {}
-
-auto NestedSource::Current() -> std::optional<database::TrackId> {
- if (current_child_) {
- return current_child_->Current();
- }
- return {};
-}
-
-auto NestedSource::Advance() -> std::optional<database::TrackId> {
- if (!current_child_) {
- return {};
- }
-
- auto child_next = current_child_->Advance();
- if (child_next) {
- return child_next;
- }
- // Our current child has run out of tracks. Move on to the next child.
- current_item_++;
- current_child_.reset();
-
- if (current_item_ >= current_page_->values().size()) {
- // We're even out of items in this page!
- auto next_page = current_page_->next_page();
- if (!next_page) {
- current_item_--;
- return {};
- }
-
- auto db = db_.lock();
- if (!db) {
- return {};
- }
-
- current_page_.reset(db->GetPage<database::IndexRecord>(&*next_page).get());
- current_item_ = 0;
- }
- current_child_ = CreateChild(current_page_->values()[current_item_]);
-
- return Current();
-}
-
-auto NestedSource::Previous() -> std::optional<database::TrackId> {
- if (current_page_ == initial_page_ && current_item_ <= initial_item_) {
- return {};
- }
-
- current_item_--;
- current_child_.reset();
-
- if (current_item_ < 0) {
- auto prev_page = current_page_->prev_page();
- if (!prev_page) {
- return {};
- }
-
- auto db = db_.lock();
- if (!db) {
- return {};
- }
-
- current_page_.reset(db->GetPage<database::IndexRecord>(&*prev_page).get());
- current_item_ = current_page_->values().size() - 1;
- }
- current_child_ = CreateChild(current_page_->values()[current_item_]);
-
- return Current();
-}
-
-auto NestedSource::Peek(std::size_t n, std::vector<database::TrackId>* out)
- -> std::size_t {
- if (current_page_->values().size() <= current_item_) {
- return {};
- }
-
- auto db = db_.lock();
- if (!db) {
- return 0;
- }
-
- std::size_t items_added = 0;
-
- std::shared_ptr<database::Result<database::IndexRecord>> working_page =
- current_page_;
- std::size_t working_item = current_item_;
- std::shared_ptr<IResetableSource> working_child = current_child_;
-
- while (working_child) {
- auto res = working_child->Peek(n, out);
- n -= res;
- items_added += res;
-
- if (n == 0) {
- break;
- } else {
- working_item++;
- if (working_item < working_page->values().size()) {
- working_child = CreateChild(working_page->values()[working_item]);
- } else {
- auto next_page = current_page_->next_page();
- if (!next_page) {
- break;
- }
- working_page.reset(
- db->GetPage<database::IndexRecord>(&*next_page).get());
- working_item = 0;
- working_child = CreateChild(working_page->values()[0]);
- }
- }
- }
-
- return items_added;
-}
-
-auto NestedSource::Reset() -> void {
- current_page_ = initial_page_;
- current_item_ = initial_item_;
- current_child_ = CreateChild(initial_page_->values()[initial_item_]);
-}
-
-auto NestedSource::CreateChild(std::shared_ptr<database::IndexRecord> record)
- -> std::shared_ptr<IResetableSource> {
- auto cont = record->Expand(10);
- if (!cont) {
- return {};
- }
- auto db = db_.lock();
- if (!db) {
- return {};
- }
- std::shared_ptr<database::Result<database::IndexRecord>> next_level{
- db->GetPage<database::IndexRecord>(&*cont).get()};
- if (!next_level) {
- return {};
- }
- auto next_level_record = next_level->values()[0];
- if (next_level_record->track()) {
- return std::make_shared<IndexRecordSource>(db_, next_level);
- } else {
- return std::make_shared<NestedSource>(db_, next_level);
- }
-}
-
-} // namespace playlist
diff --git a/src/system_fsm/booting.cpp b/src/system_fsm/booting.cpp
index c9a0a2d2..9ccc107b 100644
--- a/src/system_fsm/booting.cpp
+++ b/src/system_fsm/booting.cpp
@@ -86,7 +86,8 @@ auto Booting::entry() -> void {
sServices->battery(std::make_unique<battery::Battery>(
sServices->samd(), std::unique_ptr<drivers::AdcBattery>(adc)));
- sServices->track_queue(std::make_unique<audio::TrackQueue>());
+ sServices->track_queue(
+ std::make_unique<audio::TrackQueue>(sServices->bg_worker()));
sServices->tag_parser(std::make_unique<database::TagParserImpl>());
sServices->collator(locale::CreateCollator());
diff --git a/src/ui/include/ui_events.hpp b/src/ui/include/ui_events.hpp
index 6a6be304..111f37a8 100644
--- a/src/ui/include/ui_events.hpp
+++ b/src/ui/include/ui_events.hpp
@@ -30,18 +30,6 @@ struct OnSystemError : tinyfsm::Event {};
namespace internal {
-struct RecordSelected : tinyfsm::Event {
- bool show_menu;
- std::pmr::vector<std::pmr::string> new_crumbs;
- std::shared_ptr<database::Result<database::IndexRecord>> initial_page;
- std::shared_ptr<database::Result<database::IndexRecord>> page;
- std::size_t record;
-};
-
-struct IndexSelected : tinyfsm::Event {
- database::IndexId id;
-};
-
struct ControlSchemeChanged : tinyfsm::Event {};
struct ReindexDatabase : tinyfsm::Event {};
diff --git a/src/ui/include/ui_fsm.hpp b/src/ui/include/ui_fsm.hpp
index a8291a46..f5f8c574 100644
--- a/src/ui/include/ui_fsm.hpp
+++ b/src/ui/include/ui_fsm.hpp
@@ -64,7 +64,6 @@ class UiState : public tinyfsm::Fsm<UiState> {
virtual void react(const system_fsm::KeyLockChanged&);
virtual void react(const OnLuaError&) {}
- virtual void react(const internal::RecordSelected&) {}
virtual void react(const internal::BackPressed&) {}
virtual void react(const internal::ShowSettingsPage&){};
virtual void react(const internal::ModalCancelPressed&) {
diff --git a/src/ui/ui_fsm.cpp b/src/ui/ui_fsm.cpp
index 3c57f573..9fc31481 100644
--- a/src/ui/ui_fsm.cpp
+++ b/src/ui/ui_fsm.cpp
@@ -41,7 +41,6 @@
#include "screen_lua.hpp"
#include "screen_settings.hpp"
#include "screen_splash.hpp"
-#include "source.hpp"
#include "spiffs.hpp"
#include "storage.hpp"
#include "system_events.hpp"
@@ -118,7 +117,7 @@ void UiState::react(const audio::PlaybackUpdate& ev) {}
void UiState::react(const audio::QueueUpdate&) {
auto& queue = sServices->track_queue();
- sPlaybackModel.current_track.set(queue.Current());
+ sPlaybackModel.current_track.set(queue.current());
}
void UiState::react(const internal::ControlSchemeChanged&) {
@@ -281,15 +280,14 @@ void Lua::react(const system_fsm::BatteryStateChanged& ev) {
}
void Lua::react(const audio::QueueUpdate&) {
- sServices->bg_worker().Dispatch<void>([=]() {
- auto& queue = sServices->track_queue();
- size_t total_size = queue.GetTotalSize();
- size_t current_pos = queue.GetCurrentPosition();
- events::Ui().RunOnTask([=]() {
- queue_size_->Update(static_cast<int>(total_size));
- queue_position_->Update(static_cast<int>(current_pos));
- });
- });
+ auto& queue = sServices->track_queue();
+ queue_size_->Update(static_cast<int>(queue.totalSize()));
+
+ int current_pos = queue.currentPosition();
+ if (queue.current()) {
+ current_pos++;
+ }
+ queue_position_->Update(current_pos);
}
void Lua::react(const audio::PlaybackStarted& ev) {
@@ -373,7 +371,7 @@ void Indexing::entry() {
// TODO: Hmm.
return;
}
- db->Update();
+ db->updateIndexes();
}
void Indexing::exit() {