diff options
Diffstat (limited to 'src/tangara/database/database.cpp')
| -rw-r--r-- | src/tangara/database/database.cpp | 123 |
1 files changed, 76 insertions, 47 deletions
diff --git a/src/tangara/database/database.cpp b/src/tangara/database/database.cpp index 67893b6e..9854f693 100644 --- a/src/tangara/database/database.cpp +++ b/src/tangara/database/database.cpp @@ -7,6 +7,7 @@ #include "database/database.hpp" #include <bits/ranges_algo.h> +#include <stdint.h> #include <algorithm> #include <cctype> #include <cstdint> @@ -21,6 +22,7 @@ #include "cppbor.h" #include "cppbor_parse.h" +#include "debug.hpp" #include "esp_log.h" #include "esp_timer.h" #include "ff.h" @@ -66,8 +68,8 @@ static std::atomic<bool> sIsDbOpen(false); using std::placeholders::_1; using std::placeholders::_2; -static auto CreateNewDatabase(leveldb::Options& options, locale::ICollator& col) - -> leveldb::DB* { +static auto CreateNewDatabase(leveldb::Options& options, + locale::ICollator& col) -> leveldb::DB* { Database::Destroy(); leveldb::DB* db; options.create_if_missing = true; @@ -348,6 +350,8 @@ auto Database::updateIndexes() -> void { } update_tracker_ = std::make_unique<UpdateTracker>(); + tag_parser_.ClearCaches(); + leveldb::ReadOptions read_options; read_options.fill_cache = false; read_options.verify_checksums = true; @@ -373,21 +377,24 @@ auto Database::updateIndexes() -> void { continue; } + std::shared_ptr<TrackTags> tags; FILINFO info; FRESULT 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) { - continue; - } else { - track->modified_at = modified_at; + std::pair<uint16_t, uint16_t> modified_at{info.fdate, info.ftime}; + if (modified_at == track->modified_at) { + continue; + } else { + // Will be written out later; we make sure we update the track's + // modification time and tags in the same batch so that interrupted + // reindexes don't cause a big mess. + track->modified_at = modified_at; + } + + tags = tag_parser_.ReadAndParseTags( + {track->filepath.data(), track->filepath.size()}); } - 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 @@ -411,30 +418,24 @@ auto Database::updateIndexes() -> void { // 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. - auto new_type = calculateMediaType(*tags, track->filepath); - uint64_t new_hash = tags->Hash(); - if (new_hash != track->tags_hash || new_type != track->type) { - // 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); - - // Again, we remove the old index records first so has to avoid - // dangling references. - dbRemoveIndexes(track); + // Remove the old index records first so has to avoid dangling records. + dbRemoveIndexes(track); - // Atomically correct the hash + create the new index records. - leveldb::WriteBatch batch; - track->tags_hash = new_hash; - dbIngestTagHashes(*tags, track->individual_tag_hashes, batch); + // Atomically correct the hash + create the new index records. + leveldb::WriteBatch batch; - track->type = new_type; - dbCreateIndexesForTrack(*track, *tags, batch); - batch.Put(EncodeDataKey(track->id), EncodeDataValue(*track)); + uint64_t new_hash = tags->Hash(); + if (track->tags_hash != new_hash) { + track->tags_hash = new_hash; batch.Put(EncodeHashKey(new_hash), EncodeHashValue(track->id)); - db_->Write(leveldb::WriteOptions(), &batch); } + + track->type = calculateMediaType(*tags, track->filepath); + batch.Put(EncodeDataKey(track->id), EncodeDataValue(*track)); + + dbIngestTagHashes(*tags, track->individual_tag_hashes, batch); + dbCreateIndexesForTrack(*track, *tags, batch); + db_->Write(leveldb::WriteOptions(), &batch); } } @@ -445,8 +446,8 @@ auto Database::updateIndexes() -> void { track_finder_.launch(""); }; -auto Database::processCandidateCallback(FILINFO& info, std::string_view path) - -> void { +auto Database::processCandidateCallback(FILINFO& info, + std::string_view path) -> void { leveldb::ReadOptions read_options; read_options.fill_cache = true; read_options.verify_checksums = false; @@ -530,9 +531,8 @@ static constexpr char kMusicMediaPath[] = "/Music/"; static constexpr char kPodcastMediaPath[] = "/Podcasts/"; static constexpr char kAudiobookMediaPath[] = "/Audiobooks/"; -auto Database::calculateMediaType(TrackTags& tags, std::string_view path) - -> MediaType { - +auto Database::calculateMediaType(TrackTags& tags, + std::string_view path) -> MediaType { auto equalsIgnoreCase = [&](char lhs, char rhs) { return std::tolower(lhs) == std::tolower(rhs); }; @@ -540,7 +540,8 @@ auto Database::calculateMediaType(TrackTags& tags, std::string_view path) // Use the filepath first, since it's the most explicit way for the user to // tell us what this track is. auto checkPathPrefix = [&](std::string_view path, std::string prefix) { - auto res = std::mismatch(prefix.begin(), prefix.end(), path.begin(), path.end(), equalsIgnoreCase); + auto res = std::mismatch(prefix.begin(), prefix.end(), path.begin(), + path.end(), equalsIgnoreCase); return res.first == prefix.end(); }; @@ -634,8 +635,8 @@ auto Database::dbMintNewTrackId() -> TrackId { return next_track_id_++; } -auto Database::dbGetTrackData(leveldb::ReadOptions options, TrackId id) - -> std::shared_ptr<TrackData> { +auto Database::dbGetTrackData(leveldb::ReadOptions options, + TrackId id) -> std::shared_ptr<TrackData> { std::string key = EncodeDataKey(id); std::string raw_val; if (!db_->Get(options, key, &raw_val).ok()) { @@ -668,26 +669,55 @@ auto Database::dbRemoveIndexes(std::shared_ptr<TrackData> data) -> void { } for (const IndexInfo& index : getIndexes()) { auto entries = Index(collator_, index, *data, *tags); + std::optional<uint8_t> preserve_depth{}; + + // Iterate through the index records backwards, so that we start deleting + // records from the highest depth. This allows us to work out what depth to + // stop at as we go. + // e.g. if the last track of an album is being deleted, we need to delete + // the album index record at n-1 depth. But if there are still other tracks + // in the album, then we should only clear records at depth n. for (auto it = entries.rbegin(); it != entries.rend(); it++) { + if (preserve_depth) { + if (it->first.header.components_hash.size() < *preserve_depth) { + // Some records deeper than this were not removed, so don't try to + // remove this one. + continue; + } + // A record weren't removed, but the current record is either a + // sibling of that record, or a leaf belonging to a sibling of that + // record. Either way, we can start deleting records again. + // preserve_depth will be set to a new value if we start encountering + // siblings again. + preserve_depth.reset(); + } + auto key = EncodeIndexKey(it->first); auto status = db_->Delete(leveldb::WriteOptions{}, key); if (!status.ok()) { return; } + // Are any sibling records left at this depth? If two index records have + // matching headers, they are siblings (same depth, same selections at + // lower depths) std::unique_ptr<leveldb::Iterator> cursor{db_->NewIterator({})}; + + // Check the record before the one we just deleted. cursor->Seek(key); cursor->Prev(); - auto prev_key = ParseIndexKey(cursor->key()); if (prev_key && prev_key->header == it->first.header) { - break; + preserve_depth = it->first.header.components_hash.size(); + continue; } + // Check the record after. cursor->Next(); auto next_key = ParseIndexKey(cursor->key()); if (next_key && next_key->header == it->first.header) { - break; + preserve_depth = it->first.header.components_hash.size(); + continue; } } } @@ -809,8 +839,7 @@ Iterator::Iterator(std::shared_ptr<Database> db, IndexId idx) : Iterator(db, IndexKey::Header{ .id = idx, - .depth = 0, - .components_hash = 0, + .components_hash = {}, }) {} Iterator::Iterator(std::shared_ptr<Database> db, const IndexKey::Header& header) @@ -883,8 +912,8 @@ auto TrackIterator::next() -> void { 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. + // The current top iterator is out of tracks. Pop it, and move the + // parent to the next item. levels_.pop_back(); } else if (std::holds_alternative<IndexKey::Header>(cur->contents())) { // This record is a branch. Push a new iterator. |
