From d70ec9bf447f7a46e347c3bc5ad58fd88aff46a2 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Tue, 21 Nov 2023 13:49:47 +1100 Subject: [PATCH] Add lua functions to get database content --- lua/main_menu.lua | 6 +- src/database/database.cpp | 44 +++++++ src/database/include/database.hpp | 17 +++ src/lua/CMakeLists.txt | 2 +- src/lua/bridge.cpp | 37 ++---- src/lua/include/lua_database.hpp | 15 +++ src/lua/include/lua_queue.hpp | 15 +++ src/lua/lua_database.cpp | 199 ++++++++++++++++++++++++++++++ src/lua/lua_queue.cpp | 46 +++++++ src/lua/stubs/database.lua | 59 +++++++++ 10 files changed, 408 insertions(+), 32 deletions(-) create mode 100644 src/lua/include/lua_database.hpp create mode 100644 src/lua/include/lua_queue.hpp create mode 100644 src/lua/lua_database.cpp create mode 100644 src/lua/lua_queue.cpp create mode 100644 src/lua/stubs/database.lua diff --git a/lua/main_menu.lua b/lua/main_menu.lua index 3b88111d..e38ed2c1 100644 --- a/lua/main_menu.lua +++ b/lua/main_menu.lua @@ -30,9 +30,9 @@ return function() legacy_ui.open_now_playing(); end) - local indexes = database.get_indexes() - for id, name in ipairs(indexes) do - local btn = menu.list:add_btn(nil, name) + local indexes = database.indexes() + for id, idx in ipairs(indexes) do + local btn = menu.list:add_btn(nil, tostring(idx)) btn:onClicked(function() legacy_ui.open_browse(id); end) diff --git a/src/database/database.cpp b/src/database/database.cpp index 142735d8..0967eb95 100644 --- a/src/database/database.cpp +++ b/src/database/database.cpp @@ -857,4 +857,48 @@ auto IndexRecord::Expand(std::size_t page_size) const }; } +Iterator::Iterator(std::weak_ptr db, const IndexInfo& idx) + : db_(db), prev_pos_(), current_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}; +} + +Iterator::Iterator(std::weak_ptr db, const Continuation& c) + : db_(db), prev_pos_(), current_pos_(c) {} + +auto Iterator::Prev() -> std::optional { + if (!prev_pos_) { + return {}; + } + auto db = db_.lock(); + if (!db) { + return {}; + } + std::unique_ptr> res{ + db->GetPage(&*prev_pos_).get()}; + prev_pos_ = res->prev_page(); + current_pos_ = prev_pos_; + return *res->values()[0]; +} + +auto Iterator::Next() -> std::optional { + if (!current_pos_) { + return {}; + } + auto db = db_.lock(); + if (!db) { + return {}; + } + std::unique_ptr> res{ + db->GetPage(&*current_pos_).get()}; + prev_pos_ = current_pos_; + current_pos_ = res->next_page(); + return *res->values()[0]; +} + } // namespace database diff --git a/src/database/include/database.hpp b/src/database/include/database.hpp index 544e4a62..972871db 100644 --- a/src/database/include/database.hpp +++ b/src/database/include/database.hpp @@ -183,4 +183,21 @@ auto Database::ParseRecord(const leveldb::Slice& key, const leveldb::Slice& val) -> std::shared_ptr; +/* + * Utility for accessing a large set of database records, one record at a time. + */ +class Iterator { + public: + Iterator(std::weak_ptr, const IndexInfo&); + Iterator(std::weak_ptr, const Continuation&); + + auto Prev() -> std::optional; + auto Next() -> std::optional; + + private: + std::weak_ptr db_; + std::optional prev_pos_; + std::optional current_pos_; +}; + } // namespace database diff --git a/src/lua/CMakeLists.txt b/src/lua/CMakeLists.txt index f179a881..654e2763 100644 --- a/src/lua/CMakeLists.txt +++ b/src/lua/CMakeLists.txt @@ -3,7 +3,7 @@ # SPDX-License-Identifier: GPL-3.0-only idf_component_register( - SRCS "lua_thread.cpp" "bridge.cpp" "property.cpp" + SRCS "lua_thread.cpp" "bridge.cpp" "property.cpp" "lua_database.cpp" "lua_queue.cpp" INCLUDE_DIRS "include" REQUIRES "drivers" "lvgl" "tinyfsm" "events" "system_fsm" "database" "esp_timer" "battery" "esp-idf-lua" "luavgl") target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS}) diff --git a/src/lua/bridge.cpp b/src/lua/bridge.cpp index a45f2b27..070dee98 100644 --- a/src/lua/bridge.cpp +++ b/src/lua/bridge.cpp @@ -5,13 +5,19 @@ */ #include "bridge.hpp" +#include #include #include +#include "database.hpp" #include "esp_log.h" +#include "index.hpp" #include "lauxlib.h" +#include "lua.h" #include "lua.hpp" +#include "lua_database.hpp" +#include "lua_queue.hpp" #include "lvgl.h" #include "event_queue.hpp" @@ -22,6 +28,7 @@ namespace lua { [[maybe_unused]] static constexpr char kTag[] = "lua_bridge"; + static constexpr char kBridgeKey[] = "bridge"; static auto open_settings_fn(lua_State* state) -> int { @@ -54,32 +61,6 @@ static auto lua_legacy_ui(lua_State* state) -> int { return 1; } -static auto get_indexes(lua_State* state) -> int { - Bridge* instance = Bridge::Get(state); - - lua_newtable(state); - - auto db = instance->services().database().lock(); - if (!db) { - return 1; - } - - for (const auto& i : db->GetIndexes()) { - lua_pushstring(state, i.name.c_str()); - lua_rawseti(state, -2, i.id); - } - - return 1; -} - -static const struct luaL_Reg kDatabaseFuncs[] = {{"get_indexes", get_indexes}, - {NULL, NULL}}; - -static auto lua_database(lua_State* state) -> int { - luaL_newlib(state, kDatabaseFuncs); - return 1; -} - auto Bridge::Get(lua_State* state) -> Bridge* { lua_pushstring(state, kBridgeKey); lua_gettable(state, LUA_REGISTRYINDEX); @@ -95,8 +76,8 @@ Bridge::Bridge(system_fsm::ServiceLocator& services, lua_State& s) luaL_requiref(&s, "legacy_ui", lua_legacy_ui, true); lua_pop(&s, 1); - luaL_requiref(&s, "database", lua_database, true); - lua_pop(&s, 1); + RegisterDatabaseModule(&s); + RegisterQueueModule(&s); } static auto new_property_module(lua_State* state) -> int { diff --git a/src/lua/include/lua_database.hpp b/src/lua/include/lua_database.hpp new file mode 100644 index 00000000..e47ace08 --- /dev/null +++ b/src/lua/include/lua_database.hpp @@ -0,0 +1,15 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include "lua.hpp" + +namespace lua { + +auto RegisterDatabaseModule(lua_State*) -> void; + +} // namespace lua diff --git a/src/lua/include/lua_queue.hpp b/src/lua/include/lua_queue.hpp new file mode 100644 index 00000000..c5cfe04d --- /dev/null +++ b/src/lua/include/lua_queue.hpp @@ -0,0 +1,15 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include "lua.hpp" + +namespace lua { + +auto RegisterQueueModule(lua_State*) -> void; + +} // namespace lua diff --git a/src/lua/lua_database.cpp b/src/lua/lua_database.cpp new file mode 100644 index 00000000..545dcd31 --- /dev/null +++ b/src/lua/lua_database.cpp @@ -0,0 +1,199 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "lua_database.hpp" + +#include +#include + +#include "lua.hpp" + +#include "esp_log.h" +#include "lauxlib.h" +#include "lua.h" +#include "lvgl.h" + +#include "database.hpp" +#include "event_queue.hpp" +#include "index.hpp" +#include "property.hpp" +#include "service_locator.hpp" +#include "ui_events.hpp" + +namespace lua { + +[[maybe_unused]] static constexpr char kTag[] = "lua_db"; + +static constexpr char kDbIndexMetatable[] = "db_index"; +static constexpr char kDbRecordMetatable[] = "db_record"; +static constexpr char kDbIteratorMetatable[] = "db_record"; + +static auto indexes(lua_State* state) -> int { + Bridge* instance = Bridge::Get(state); + + lua_newtable(state); + + auto db = instance->services().database().lock(); + if (!db) { + return 1; + } + + for (const auto& i : db->GetIndexes()) { + database::IndexInfo** data = reinterpret_cast( + lua_newuserdata(state, sizeof(uintptr_t))); + luaL_setmetatable(state, kDbIndexMetatable); + *data = new database::IndexInfo{i}; + lua_rawseti(state, -2, i.id); + } + + return 1; +} + +static const struct luaL_Reg kDatabaseFuncs[] = {{"indexes", indexes}, + {NULL, NULL}}; + +static auto db_iterate(lua_State* state) -> int { + database::Iterator* it = *reinterpret_cast( + lua_touserdata(state, lua_upvalueindex(1))); + + auto res = it->Next(); + if (res) { + database::IndexRecord** record = reinterpret_cast( + lua_newuserdata(state, sizeof(uintptr_t))); + *record = new database::IndexRecord(*res); + luaL_setmetatable(state, kDbRecordMetatable); + return 1; + } + return 0; +} + +static auto db_iterator_gc(lua_State* state) -> int { + database::Iterator** it = reinterpret_cast( + luaL_checkudata(state, 1, kDbIteratorMetatable)); + if (it != NULL) { + delete *it; + } + return 0; +} + +static auto push_iterator( + lua_State* state, + std::variant val) -> void { + Bridge* instance = Bridge::Get(state); + database::Iterator** data = reinterpret_cast( + lua_newuserdata(state, sizeof(uintptr_t))); + std::visit( + [&](auto&& arg) { + *data = new database::Iterator(instance->services().database(), arg); + }, + val); + luaL_setmetatable(state, kDbIteratorMetatable); + lua_pushcclosure(state, db_iterate, 1); +} + +static auto record_text(lua_State* state) -> int { + database::IndexRecord* data = *reinterpret_cast( + luaL_checkudata(state, 1, kDbRecordMetatable)); + lua_pushstring(state, + data->text().value_or("[tell jacqueline u saw this]").c_str()); + return 1; +} + +static auto record_contents(lua_State* state) -> int { + database::IndexRecord* data = *reinterpret_cast( + luaL_checkudata(state, 1, kDbRecordMetatable)); + + if (data->track()) { + lua_pushinteger(state, *data->track()); + } else { + push_iterator(state, data->Expand(1).value()); + } + + return 1; +} + +static auto record_gc(lua_State* state) -> int { + database::IndexRecord** data = reinterpret_cast( + luaL_checkudata(state, 1, kDbRecordMetatable)); + if (data != NULL) { + delete *data; + } + return 0; +} + +static const struct luaL_Reg kDbRecordFuncs[] = {{"title", record_text}, + {"contents", record_contents}, + {"__tostring", record_text}, + {"__gc", record_gc}, + {NULL, NULL}}; + +static auto index_name(lua_State* state) -> int { + database::IndexInfo** data = reinterpret_cast( + luaL_checkudata(state, 1, kDbIndexMetatable)); + if (data == NULL) { + return 0; + } + lua_pushstring(state, (*data)->name.c_str()); + return 1; +} + +static auto index_iter(lua_State* state) -> int { + database::IndexInfo** data = reinterpret_cast( + 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( + luaL_checkudata(state, 1, kDbIndexMetatable)); + if (data != NULL) { + delete *data; + } + return 0; +} + +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 { + // Metatable for indexes + luaL_newmetatable(state, kDbIndexMetatable); + + lua_pushliteral(state, "__index"); + lua_pushvalue(state, -2); + lua_settable(state, -3); // metatable.__index = metatable + + // Add member funcs to the metatable. + luaL_setfuncs(state, kDbIndexFuncs, 0); + + luaL_newmetatable(state, kDbIteratorMetatable); + lua_pushliteral(state, "__gc"); + lua_pushcfunction(state, db_iterator_gc); + lua_settable(state, -3); + + luaL_newmetatable(state, kDbRecordMetatable); + lua_pushliteral(state, "__index"); + lua_pushvalue(state, -2); + lua_settable(state, -3); // metatable.__index = metatable + luaL_setfuncs(state, kDbRecordFuncs, 0); + + luaL_newlib(state, kDatabaseFuncs); + return 1; +} + +auto RegisterDatabaseModule(lua_State* s) -> void { + luaL_requiref(s, "database", lua_database, true); + lua_pop(s, 1); +} + +} // namespace lua \ No newline at end of file diff --git a/src/lua/lua_queue.cpp b/src/lua/lua_queue.cpp new file mode 100644 index 00000000..500940a2 --- /dev/null +++ b/src/lua/lua_queue.cpp @@ -0,0 +1,46 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "lua_database.hpp" + +#include +#include + +#include "lua.hpp" + +#include "esp_log.h" +#include "lauxlib.h" +#include "lua.h" +#include "lvgl.h" + +#include "database.hpp" +#include "event_queue.hpp" +#include "index.hpp" +#include "property.hpp" +#include "service_locator.hpp" +#include "ui_events.hpp" + +namespace lua { + +[[maybe_unused]] static constexpr char kTag[] = "lua_queue"; + +static auto queue_add(lua_State* state) -> int { + return 0; +} + +static const struct luaL_Reg kQueueFuncs[] = {{"add", queue_add}, {NULL, NULL}}; + +static auto lua_queue(lua_State* state) -> int { + luaL_newlib(state, kQueueFuncs); + return 1; +} + +auto RegisterQueueModule(lua_State* s) -> void { + luaL_requiref(s, "queue", lua_queue, true); + lua_pop(s, 1); +} + +} // namespace lua \ No newline at end of file diff --git a/src/lua/stubs/database.lua b/src/lua/stubs/database.lua new file mode 100644 index 00000000..97359ab1 --- /dev/null +++ b/src/lua/stubs/database.lua @@ -0,0 +1,59 @@ +--- Module for accessing and updating data about the user's library of tracks. +-- @module database + +local database = {} + +--- Returns a list of all indexes in the database. +-- @treturn Array(Index) +function database.indexes() end + +--- An iterator is a userdata type that behaves like an ordinary Lua iterator. +-- @type Iterator +local Iterator = {} + +--- A TrackId is a unique identifier, representing a playable track in the +--- user's library. +-- @type TrackId +local TrackId = {} + +--- A record is an item within an Index, representing some value at a specific +--- depth. +-- @type Record +local Record = {} + +--- Gets the human-readable text representing this record. The `__tostring` +--- metatable function is an alias of this function. +-- @treturn string +function Record:title() end + +--- Returns the value that this record represents. This may be either a track +--- id, for records which uniquely identify a track, or it may be a new +--- Iterator representing the next level of depth for the current index. +--- +--- For example, each Record in the "All Albums" index corresponds to an entire +--- album of tracks; the 'contents' of such a Record is an iterator returning +--- each track in the album represented by the Record. The contents of each of +--- the returned 'track' Records would be a full Track, as there is no further +--- disambiguation needed. +-- @treturn TrackId|Iterator(Record) +function Record:contents() end + +--- An index is heirarchical, sorted, view of the tracks within the database. +--- For example, the 'All Albums' index contains, first, a sorted list of every +--- album name in the library. Then, at the second level of the index, a sorted +--- list of every track within each album. +-- @type Index +local Index = {} + +--- Gets the human-readable name of this index. This is typically something +--- like "All Albums", or "Albums by Artist". The `__tostring` metatable +--- function is an alias of this function. +-- @treturn string +function Index:name() end + +--- Returns a new iterator that can be used to access every record within the +--- first level of this index. +-- @treturn Iterator(Record) +function Index:iter() end + +return database