diff --git a/lua/browser.lua b/lua/browser.lua index bd743152..48c3895f 100644 --- a/lua/browser.lua +++ b/lua/browser.lua @@ -8,6 +8,7 @@ local styles = require("styles") local playback = require("playback") local theme = require("theme") local screen = require("screen") +local database = require("database") return screen:new{ create_ui = function(self) @@ -101,7 +102,12 @@ return screen:new{ }) else queue.clear() - queue.add(contents) + local track = database.track_by_id(contents) + if (track) then + queue.play_from(track.filepath, track.saved_position) + else + queue.add(contents) + end playback.playing:set(true) backstack.push(playing:new()) end diff --git a/luals-stubs/database.lua b/luals-stubs/database.lua index 753961fe..92c2fb8d 100644 --- a/luals-stubs/database.lua +++ b/luals-stubs/database.lua @@ -10,6 +10,19 @@ local database = {} --- @return Index[] function database.indexes() end +--- Returns the track in the database with the id given +--- @param id TrackId +--- @return Track +function database.track_by_id(id) end + + +--- @class Track +--- @field id TrackId The track id of this track +--- @field filepath string The filepath of this track +--- @field saved_position integer The last saved position of this track +--- @field tags table A mapping of any available tags to that tag's value +local Track = {} + --- An iterator is a userdata type that behaves like an ordinary Lua iterator. --- @class Iterator local Iterator = {} @@ -19,6 +32,7 @@ local Iterator = {} --- @class TrackId local TrackId = {} + --- Gets the human-readable text representing this record. The `__tostring` --- metatable function is an alias of this function. --- @class Record diff --git a/luals-stubs/queue.lua b/luals-stubs/queue.lua index 97583587..a0473407 100644 --- a/luals-stubs/queue.lua +++ b/luals-stubs/queue.lua @@ -33,4 +33,10 @@ function queue.next() end --- Moves backward in the play queue, looping back around to the end if repeat is on. function queue.previous() end +--- Play a track starting from a number of seconds in +--- This will replace the existing queue +--- @param filepath string +--- @param seconds_offset integer +function queue.play_from(filepath, seconds_offset) end + return queue diff --git a/src/tangara/audio/audio_events.hpp b/src/tangara/audio/audio_events.hpp index 56d150b2..89dc28ff 100644 --- a/src/tangara/audio/audio_events.hpp +++ b/src/tangara/audio/audio_events.hpp @@ -106,6 +106,8 @@ struct QueueUpdate : tinyfsm::Event { kBulkLoadingUpdate, }; Reason reason; + + std::optional seek_to_second; }; struct StepUpVolume : tinyfsm::Event {}; diff --git a/src/tangara/audio/audio_fsm.cpp b/src/tangara/audio/audio_fsm.cpp index 1daf568e..f9823fb3 100644 --- a/src/tangara/audio/audio_fsm.cpp +++ b/src/tangara/audio/audio_fsm.cpp @@ -78,6 +78,8 @@ StreamCues AudioState::sStreamCues; bool AudioState::sIsPaused = true; bool AudioState::sIsTtsPlaying = false; +uint8_t AudioState::sUpdateCounter = 0; + auto AudioState::emitPlaybackUpdate(bool paused) -> void { std::optional position; auto current = sStreamCues.current(); @@ -88,6 +90,16 @@ auto AudioState::emitPlaybackUpdate(bool paused) -> void { current.first->start_offset.value_or(0); } + // If we've got an elapsed duration and it's more than 5 minutes + // increment a counter. Every 60 counts (ie, every minute) save the current elapsed position + if (position && *position > (5 * 60)) { + sUpdateCounter++; + if (sUpdateCounter >= 60) { + sUpdateCounter = 0; + updateSavedPosition(current.first->uri, *position); + } + } + PlaybackUpdate event{ .current_track = current.first, .track_position = position, @@ -101,7 +113,7 @@ auto AudioState::emitPlaybackUpdate(bool paused) -> void { void AudioState::react(const QueueUpdate& ev) { SetTrack cmd{ .new_track = std::monostate{}, - .seek_to_second = {}, + .seek_to_second = ev.seek_to_second, }; auto current = sServices->track_queue().current(); @@ -387,6 +399,27 @@ void AudioState::react(const OutputModeChanged& ev) { } } +auto AudioState::updateSavedPosition(std::string uri, uint32_t position) + -> void { + sServices->bg_worker().Dispatch([=]() { + auto db = sServices->database().lock(); + if (!db) { + return; + } + auto id = db->getTrackID(uri); + if (!id) { + return; + } + auto track = db->getTrack(*id); + if (!track) { + return; + } + auto data = track->data().clone(); + data->last_position = position; + db->setTrackData(*id, *data); + }); +} + auto AudioState::updateOutputMode() -> void { if (is_in_state() || sIsTtsPlaying) { sOutput->mode(IAudioOutput::Modes::kOnPlaying); diff --git a/src/tangara/audio/audio_fsm.hpp b/src/tangara/audio/audio_fsm.hpp index bc3feb55..c765b417 100644 --- a/src/tangara/audio/audio_fsm.hpp +++ b/src/tangara/audio/audio_fsm.hpp @@ -75,6 +75,8 @@ class AudioState : public tinyfsm::Fsm { auto emitPlaybackUpdate(bool paused) -> void; auto commitVolume() -> void; + auto updateSavedPosition(std::string uri, uint32_t position) -> void; + static std::shared_ptr sServices; static std::shared_ptr sStreamFactory; @@ -90,6 +92,7 @@ class AudioState : public tinyfsm::Fsm { static std::optional sDrainFormat; static bool sIsPaused; + static uint8_t sUpdateCounter; static bool sIsTtsPlaying; }; diff --git a/src/tangara/audio/track_queue.cpp b/src/tangara/audio/track_queue.cpp index 2c1faf96..ff24637b 100644 --- a/src/tangara/audio/track_queue.cpp +++ b/src/tangara/audio/track_queue.cpp @@ -85,6 +85,16 @@ auto notifyChanged(bool current_changed, Reason reason) -> void { events::Audio().Dispatch(ev); } +auto notifyPlayFrom(uint32_t start_from_position) -> void { + QueueUpdate ev{ + .current_changed = true, + .reason = Reason::kExplicitUpdate, + .seek_to_second = start_from_position, + }; + events::Ui().Dispatch(ev); + events::Audio().Dispatch(ev); +} + TrackQueue::TrackQueue(tasks::WorkerPool& bg_worker, database::Handle db) : mutex_(), bg_worker_(bg_worker), @@ -109,6 +119,17 @@ auto TrackQueue::current() const -> TrackItem { return val; } +auto TrackQueue::playFromPosition(const std::string& filepath, + uint32_t position) -> void { + clear(); + { + const std::unique_lock lock(mutex_); + playlist_.append(filepath); + updateShuffler(true); + } + notifyPlayFrom(position); +} + auto TrackQueue::currentPosition() const -> size_t { const std::shared_lock lock(mutex_); return position_; @@ -245,7 +266,7 @@ auto TrackQueue::currentPosition(size_t position) -> bool { goTo(position); } - // If we're explicitly setting the position, we want to treat it as though + // If we're explicitly setting the position, we want to treat it as though // the current track has changed, even if the position was the same notifyChanged(true, Reason::kExplicitUpdate); return true; diff --git a/src/tangara/audio/track_queue.hpp b/src/tangara/audio/track_queue.hpp index a8d1dc3a..727b4be0 100644 --- a/src/tangara/audio/track_queue.hpp +++ b/src/tangara/audio/track_queue.hpp @@ -78,6 +78,7 @@ class TrackQueue { auto open() -> bool; auto openPlaylist(const std::string& playlist_file, bool notify = true) -> bool; + auto playFromPosition(const std::string& filepath, uint32_t position) -> void; using Item = std::variant; diff --git a/src/tangara/database/database.cpp b/src/tangara/database/database.cpp index 64451f48..9d0de695 100644 --- a/src/tangara/database/database.cpp +++ b/src/tangara/database/database.cpp @@ -44,6 +44,7 @@ #include "memory_resource.hpp" #include "result.hpp" #include "tasks.hpp" +#include "database.hpp" namespace database { @@ -269,6 +270,24 @@ auto Database::getTrack(TrackId id) -> std::shared_ptr { return std::make_shared(data, tags); } +auto Database::getTrackID(std::string path) -> std::optional { + std::string raw_data; + if (!db_->Get(leveldb::ReadOptions(), EncodePathKey(path), &raw_data).ok()) { + return {}; + } + return BytesToTrackId(raw_data); +} + +auto Database::setTrackData(TrackId id, const TrackData& data) -> void { + std::string key = EncodeDataKey(id); + std::string raw_val = EncodeDataValue(data); + + auto res = db_->Put(leveldb::WriteOptions(), key, raw_val); + if (!res.ok()) { + ESP_LOGI(kTag, "Updating track data failed for track ID: %lu", id); + } +} + auto Database::getIndexes() -> std::vector { // TODO(jacqueline): This probably needs to be async? When we have runtime // configurable indexes, they will need to come from somewhere. diff --git a/src/tangara/database/database.hpp b/src/tangara/database/database.hpp index 18070353..6dd13b0d 100644 --- a/src/tangara/database/database.hpp +++ b/src/tangara/database/database.hpp @@ -37,7 +37,7 @@ namespace database { -const uint8_t kCurrentDbVersion = 7; +const uint8_t kCurrentDbVersion = 8; struct SearchKey; class Record; @@ -78,6 +78,9 @@ class Database { auto getTrackPath(TrackId id) -> std::optional; auto getTrack(TrackId id) -> std::shared_ptr; + auto getTrackID(std::string path) -> std::optional; + + auto setTrackData(TrackId id, const TrackData& data) -> void; auto getIndexes() -> std::vector; auto updateIndexes() -> void; diff --git a/src/tangara/database/records.cpp b/src/tangara/database/records.cpp index 17009cd8..012cda64 100644 --- a/src/tangara/database/records.cpp +++ b/src/tangara/database/records.cpp @@ -93,6 +93,7 @@ auto EncodeDataValue(const TrackData& track) -> std::string { cppbor::Uint{track.modified_at.first}, cppbor::Uint{track.modified_at.second}, tag_hashes, + cppbor::Uint{track.last_position}, }; return val.toString(); } @@ -104,13 +105,14 @@ auto ParseDataValue(const leveldb::Slice& slice) -> std::shared_ptr { return nullptr; } auto vals = item->asArray(); - if (vals->size() != 7 || vals->get(0)->type() != cppbor::UINT || + if (vals->size() != 8 || vals->get(0)->type() != cppbor::UINT || vals->get(1)->type() != cppbor::TSTR || vals->get(2)->type() != cppbor::UINT || vals->get(3)->type() != cppbor::SIMPLE || vals->get(4)->type() != cppbor::UINT || vals->get(5)->type() != cppbor::UINT || - vals->get(6)->type() != cppbor::MAP) { + vals->get(6)->type() != cppbor::MAP || + vals->get(7)->type() != cppbor::UINT) { return {}; } auto res = std::make_shared(); @@ -127,6 +129,9 @@ auto ParseDataValue(const leveldb::Slice& slice) -> std::shared_ptr { auto tag = static_cast(entry.first->asUint()->unsignedValue()); res->individual_tag_hashes[tag] = entry.second->asUint()->unsignedValue(); } + + res->last_position = vals->get(7)->asUint()->unsignedValue(); + return res; } diff --git a/src/tangara/database/track.cpp b/src/tangara/database/track.cpp index cdb7543c..e737dd37 100644 --- a/src/tangara/database/track.cpp +++ b/src/tangara/database/track.cpp @@ -293,4 +293,16 @@ auto TrackTags::Hash() const -> uint64_t { return komihash_stream_final(&stream); } +auto database::TrackData::clone() const -> std::shared_ptr { + auto data = std::make_shared(); + data->id = id; + data->filepath = filepath; + data->tags_hash = tags_hash; + data->individual_tag_hashes = individual_tag_hashes; + data->is_tombstoned = is_tombstoned; + data->modified_at = modified_at; + data->last_position = last_position; + return data; +} + } // namespace database diff --git a/src/tangara/database/track.hpp b/src/tangara/database/track.hpp index 6501e31f..03fc47b9 100644 --- a/src/tangara/database/track.hpp +++ b/src/tangara/database/track.hpp @@ -159,7 +159,8 @@ struct TrackData { tags_hash(0), individual_tag_hashes(&memory::kSpiRamResource), is_tombstoned(false), - modified_at() {} + modified_at(), + last_position(0) {} TrackId id; std::pmr::string filepath; @@ -167,9 +168,11 @@ struct TrackData { std::pmr::unordered_map individual_tag_hashes; bool is_tombstoned; std::pair modified_at; + uint32_t last_position; - TrackData(TrackData&& other) = delete; + TrackData(const TrackData&& other) = delete; TrackData& operator=(TrackData& other) = delete; + auto clone() const -> std::shared_ptr; bool operator==(const TrackData&) const = default; }; diff --git a/src/tangara/lua/lua_database.cpp b/src/tangara/lua/lua_database.cpp index bf84a399..e184265a 100644 --- a/src/tangara/lua/lua_database.cpp +++ b/src/tangara/lua/lua_database.cpp @@ -73,6 +73,56 @@ static auto indexes(lua_State* state) -> int { return 1; } +auto pushTagValue(lua_State* L, const database::TagValue& val) -> void { + std::visit( + [&](auto&& arg) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + lua_pushlstring(L, arg.data(), arg.size()); + } else if constexpr (std::is_same_v< + T, std::span>) { + lua_createtable(L, 0, arg.size()); + for (const auto& i : arg) { + lua_pushlstring(L, i.data(), i.size()); + lua_pushboolean(L, true); + lua_rawset(L, -3); + } + } else if constexpr (std::is_same_v) { + lua_pushinteger(L, arg); + } else { + lua_pushnil(L); + } + }, + val); +} + +static void pushTrack(lua_State* L, const database::Track& track) { + lua_newtable(L); + + lua_pushliteral(L, "tags"); + lua_newtable(L); + for (const auto& tag : track.tags().allPresent()) { + lua_pushstring(L, database::tagName(tag).c_str()); + pushTagValue(L, track.tags().get(tag)); + lua_settable(L, -3); + } + lua_settable(L, -3); + + lua_pushliteral(L, "id"); + lua_pushinteger(L, track.data().id); + lua_settable(L, -3); + + lua_pushliteral(L, "filepath"); + lua_pushstring(L, track.data().filepath.c_str()); + lua_settable(L, -3); + + lua_pushliteral(L, "saved_position"); + lua_pushinteger(L, track.data().last_position); + lua_settable(L, -3); + + +} + static auto version(lua_State* L) -> int { Bridge* instance = Bridge::Get(L); auto db = instance->services().database().lock(); @@ -111,9 +161,29 @@ static auto update(lua_State* L) -> int { return 0; } +static auto track_by_id(lua_State* L) -> int { + auto id = luaL_checkinteger(L, -1); + + Bridge* instance = Bridge::Get(L); + auto db = instance->services().database().lock(); + if (!db) { + return 0; + } + + auto track = db->getTrack(id); + if (!track) { + return 0; + } + + pushTrack(L, *track); + + return 1; +} + static const struct luaL_Reg kDatabaseFuncs[] = { {"indexes", indexes}, {"version", version}, {"size", size}, - {"recreate", recreate}, {"update", update}, {NULL, NULL}}; + {"recreate", recreate}, {"update", update}, {"track_by_id", track_by_id}, + {NULL, NULL}}; /* * Struct to be used as userdata for the Lua representation of database records. diff --git a/src/tangara/lua/lua_database.hpp b/src/tangara/lua/lua_database.hpp index 328004ef..51e71758 100644 --- a/src/tangara/lua/lua_database.hpp +++ b/src/tangara/lua/lua_database.hpp @@ -14,6 +14,8 @@ namespace lua { auto db_check_iterator(lua_State*, int stack_pos) -> database::Iterator*; +auto pushTagValue(lua_State* L, const database::TagValue& val) -> void; + auto RegisterDatabaseModule(lua_State*) -> void; } // namespace lua diff --git a/src/tangara/lua/lua_queue.cpp b/src/tangara/lua/lua_queue.cpp index 7eb32c62..07093390 100644 --- a/src/tangara/lua/lua_queue.cpp +++ b/src/tangara/lua/lua_queue.cpp @@ -79,10 +79,24 @@ static auto queue_open_playlist(lua_State* state) -> int { return 0; } +static auto queue_play_from(lua_State* state) -> int { + Bridge* instance = Bridge::Get(state); + audio::TrackQueue& queue = instance->services().track_queue(); + size_t len = 0; + const char* str = luaL_checklstring(state, 1, &len); + if (!str) { + return 0; + } + auto pos = luaL_checkinteger(state, 2); + queue.playFromPosition(str, pos); + return 0; +} + static const struct luaL_Reg kQueueFuncs[] = { {"add", queue_add}, {"clear", queue_clear}, {"open_playlist", queue_open_playlist}, + {"play_from", queue_play_from}, {NULL, NULL}}; static auto lua_queue(lua_State* state) -> int { diff --git a/src/tangara/lua/property.cpp b/src/tangara/lua/property.cpp index 1be1fd2d..847bbe15 100644 --- a/src/tangara/lua/property.cpp +++ b/src/tangara/lua/property.cpp @@ -19,6 +19,7 @@ #include "lauxlib.h" #include "lua.h" #include "lua.hpp" +#include "lua/lua_database.hpp" #include "lua/lua_thread.hpp" #include "lvgl.h" #include "memory_resource.hpp" @@ -240,29 +241,6 @@ auto Property::set(const LuaValue& val) -> bool { return true; } -static auto pushTagValue(lua_State* L, const database::TagValue& val) -> void { - std::visit( - [&](auto&& arg) { - using T = std::decay_t; - if constexpr (std::is_same_v) { - lua_pushlstring(L, arg.data(), arg.size()); - } else if constexpr (std::is_same_v< - T, std::span>) { - lua_createtable(L, 0, arg.size()); - for (const auto& i : arg) { - lua_pushlstring(L, i.data(), i.size()); - lua_pushboolean(L, true); - lua_rawset(L, -3); - } - } else if constexpr (std::is_same_v) { - lua_pushinteger(L, arg); - } else { - lua_pushnil(L); - } - }, - val); -} - static void pushTrack(lua_State* L, const audio::TrackInfo& track) { lua_newtable(L);