|
|
@ -352,11 +352,19 @@ auto Database::updateIndexes() -> void { |
|
|
|
// We couldn't read the tags for this track. Either they were
|
|
|
|
// We couldn't read the tags for this track. Either they were
|
|
|
|
// malformed, or perhaps the file is missing. Either way, tombstone
|
|
|
|
// malformed, or perhaps the file is missing. Either way, tombstone
|
|
|
|
// this record.
|
|
|
|
// this record.
|
|
|
|
ESP_LOGW(kTag, "entombing missing #%lx", track->id); |
|
|
|
ESP_LOGI(kTag, "entombing missing #%lx", track->id); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Remove the indexes first, so that interrupted operations don't leave
|
|
|
|
|
|
|
|
// dangling index records.
|
|
|
|
dbRemoveIndexes(track); |
|
|
|
dbRemoveIndexes(track); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Do the rest of the tombstoning as one atomic write.
|
|
|
|
|
|
|
|
leveldb::WriteBatch batch; |
|
|
|
track->is_tombstoned = true; |
|
|
|
track->is_tombstoned = true; |
|
|
|
dbPutTrackData(*track); |
|
|
|
batch.Put(EncodeDataKey(track->id), EncodeDataValue(*track)); |
|
|
|
db_->Delete(leveldb::WriteOptions{}, EncodePathKey(track->filepath)); |
|
|
|
batch.Delete(EncodePathKey(track->filepath)); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
db_->Write(leveldb::WriteOptions(), &batch); |
|
|
|
continue; |
|
|
|
continue; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
@ -370,12 +378,20 @@ auto Database::updateIndexes() -> void { |
|
|
|
// database.
|
|
|
|
// database.
|
|
|
|
ESP_LOGI(kTag, "updating hash (%llx -> %llx)", track->tags_hash, |
|
|
|
ESP_LOGI(kTag, "updating hash (%llx -> %llx)", track->tags_hash, |
|
|
|
new_hash); |
|
|
|
new_hash); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Again, we remove the old index records first so has to avoid
|
|
|
|
|
|
|
|
// dangling references.
|
|
|
|
dbRemoveIndexes(track); |
|
|
|
dbRemoveIndexes(track); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Atomically correct the hash + create the new index records.
|
|
|
|
|
|
|
|
leveldb::WriteBatch batch; |
|
|
|
track->tags_hash = new_hash; |
|
|
|
track->tags_hash = new_hash; |
|
|
|
dbIngestTagHashes(*tags, track->individual_tag_hashes); |
|
|
|
dbIngestTagHashes(*tags, track->individual_tag_hashes, batch); |
|
|
|
dbPutTrackData(*track); |
|
|
|
|
|
|
|
dbPutHash(new_hash, track->id); |
|
|
|
dbCreateIndexesForTrack(*track, *tags, batch); |
|
|
|
|
|
|
|
batch.Put(EncodeDataKey(track->id), EncodeDataValue(*track)); |
|
|
|
|
|
|
|
batch.Put(EncodeHashKey(new_hash), EncodeHashValue(track->id)); |
|
|
|
|
|
|
|
db_->Write(leveldb::WriteOptions(), &batch); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
@ -404,72 +420,56 @@ auto Database::updateIndexes() -> void { |
|
|
|
return; |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Check for any existing record with the same hash.
|
|
|
|
// Check for any existing track with the same hash.
|
|
|
|
uint64_t hash = tags->Hash(); |
|
|
|
uint64_t hash = tags->Hash(); |
|
|
|
std::string key = EncodeHashKey(hash); |
|
|
|
std::optional<TrackId> existing_id; |
|
|
|
std::optional<TrackId> existing_hash; |
|
|
|
|
|
|
|
std::string raw_entry; |
|
|
|
std::string raw_entry; |
|
|
|
if (db_->Get(leveldb::ReadOptions(), key, &raw_entry).ok()) { |
|
|
|
if (db_->Get(leveldb::ReadOptions(), EncodeHashKey(hash), &raw_entry) |
|
|
|
existing_hash = ParseHashValue(raw_entry); |
|
|
|
.ok()) { |
|
|
|
|
|
|
|
existing_id = ParseHashValue(raw_entry); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
std::shared_ptr<TrackData> data; |
|
|
|
|
|
|
|
if (existing_id) { |
|
|
|
|
|
|
|
// Do we have any existing data for this track? This could be the case if
|
|
|
|
|
|
|
|
// this is a tombstoned entry. In such as case, we want to reuse the
|
|
|
|
|
|
|
|
// previous TrackData so that any extra metadata is preserved.
|
|
|
|
|
|
|
|
data = dbGetTrackData(*existing_id); |
|
|
|
|
|
|
|
if (!data) { |
|
|
|
|
|
|
|
data = std::make_shared<TrackData>(); |
|
|
|
|
|
|
|
data->id = *existing_id; |
|
|
|
|
|
|
|
} else if (data->filepath != path) { |
|
|
|
|
|
|
|
ESP_LOGW(kTag, "hash collision: %s, %s, %s", |
|
|
|
|
|
|
|
tags->title().value_or("no title").c_str(), |
|
|
|
|
|
|
|
tags->artist().value_or("no artist").c_str(), |
|
|
|
|
|
|
|
tags->album().value_or("no album").c_str()); |
|
|
|
|
|
|
|
// Don't commit anything if there's a hash collision, since we're
|
|
|
|
|
|
|
|
// likely to make a big mess.
|
|
|
|
|
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
} else { |
|
|
|
std::pair<uint16_t, uint16_t> modified{info.fdate, info.ftime}; |
|
|
|
|
|
|
|
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_LOGD(kTag, "recording new 0x%lx", id); |
|
|
|
|
|
|
|
num_new_tracks++; |
|
|
|
num_new_tracks++; |
|
|
|
|
|
|
|
data = std::make_shared<TrackData>(); |
|
|
|
|
|
|
|
data->id = dbMintNewTrackId(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
auto data = std::make_shared<TrackData>(); |
|
|
|
// Make sure the file-based metadata on the TrackData is up to date.
|
|
|
|
data->id = id; |
|
|
|
|
|
|
|
data->filepath = path; |
|
|
|
data->filepath = path; |
|
|
|
data->tags_hash = hash; |
|
|
|
data->tags_hash = hash; |
|
|
|
data->modified_at = modified; |
|
|
|
data->modified_at = {info.fdate, info.ftime}; |
|
|
|
dbIngestTagHashes(*tags, data->individual_tag_hashes); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
dbPutTrackData(*data); |
|
|
|
|
|
|
|
dbPutHash(hash, id); |
|
|
|
|
|
|
|
auto t = std::make_shared<Track>(data, tags); |
|
|
|
|
|
|
|
dbCreateIndexesForTrack(*t); |
|
|
|
|
|
|
|
db_->Put(leveldb::WriteOptions{}, EncodePathKey(path), |
|
|
|
|
|
|
|
TrackIdToBytes(id)); |
|
|
|
|
|
|
|
return; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
std::shared_ptr<TrackData> existing_data = dbGetTrackData(*existing_hash); |
|
|
|
// Apply all the actual database changes as one atomic batch. This makes
|
|
|
|
if (!existing_data) { |
|
|
|
// the whole 'new track' operation atomic, and also reduces the amount of
|
|
|
|
// We found a hash that matches, but there's no data record? Weird.
|
|
|
|
// lock contention when adding many tracks at once.
|
|
|
|
auto new_data = std::make_shared<TrackData>(); |
|
|
|
leveldb::WriteBatch batch; |
|
|
|
new_data->id = dbMintNewTrackId(); |
|
|
|
dbIngestTagHashes(*tags, data->individual_tag_hashes, batch); |
|
|
|
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); |
|
|
|
|
|
|
|
db_->Put(leveldb::WriteOptions{}, EncodePathKey(path), |
|
|
|
|
|
|
|
TrackIdToBytes(new_data->id)); |
|
|
|
|
|
|
|
return; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (existing_data->is_tombstoned) { |
|
|
|
dbCreateIndexesForTrack(*data, *tags, batch); |
|
|
|
ESP_LOGI(kTag, "exhuming track %lu", existing_data->id); |
|
|
|
batch.Put(EncodeDataKey(data->id), EncodeDataValue(*data)); |
|
|
|
existing_data->is_tombstoned = false; |
|
|
|
batch.Put(EncodeHashKey(data->tags_hash), EncodeHashValue(data->id)); |
|
|
|
existing_data->modified_at = modified; |
|
|
|
batch.Put(EncodePathKey(path), TrackIdToBytes(data->id)); |
|
|
|
dbPutTrackData(*existing_data); |
|
|
|
|
|
|
|
auto t = std::make_shared<Track>(existing_data, tags); |
|
|
|
db_->Write(leveldb::WriteOptions(), &batch); |
|
|
|
dbCreateIndexesForTrack(*t); |
|
|
|
|
|
|
|
db_->Put(leveldb::WriteOptions{}, EncodePathKey(path), |
|
|
|
|
|
|
|
TrackIdToBytes(existing_data->id)); |
|
|
|
|
|
|
|
} else if (existing_data->filepath != |
|
|
|
|
|
|
|
std::pmr::string{path.data(), path.size()}) { |
|
|
|
|
|
|
|
ESP_LOGW(kTag, "hash collision: %s, %s, %s", |
|
|
|
|
|
|
|
tags->title().value_or("no title").c_str(), |
|
|
|
|
|
|
|
tags->artist().value_or("no artist").c_str(), |
|
|
|
|
|
|
|
tags->album().value_or("no album").c_str()); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
uint64_t end_time = esp_timer_get_time(); |
|
|
|
uint64_t end_time = esp_timer_get_time(); |
|
|
@ -536,22 +536,6 @@ auto Database::dbMintNewTrackId() -> TrackId { |
|
|
|
return next_track_id_++; |
|
|
|
return next_track_id_++; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
auto Database::dbEntomb(TrackId id, uint64_t hash) -> void { |
|
|
|
|
|
|
|
std::string key = EncodeHashKey(hash); |
|
|
|
|
|
|
|
std::string val = EncodeHashValue(id); |
|
|
|
|
|
|
|
if (!db_->Put(leveldb::WriteOptions(), key, val).ok()) { |
|
|
|
|
|
|
|
ESP_LOGE(kTag, "failed to entomb #%llx (id #%lx)", hash, id); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
auto Database::dbPutTrackData(const TrackData& s) -> void { |
|
|
|
|
|
|
|
std::string key = EncodeDataKey(s.id); |
|
|
|
|
|
|
|
std::string val = EncodeDataValue(s); |
|
|
|
|
|
|
|
if (!db_->Put(leveldb::WriteOptions(), key, val).ok()) { |
|
|
|
|
|
|
|
ESP_LOGE(kTag, "failed to write data for #%lx", s.id); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
auto Database::dbGetTrackData(TrackId id) -> std::shared_ptr<TrackData> { |
|
|
|
auto Database::dbGetTrackData(TrackId id) -> std::shared_ptr<TrackData> { |
|
|
|
std::string key = EncodeDataKey(id); |
|
|
|
std::string key = EncodeDataKey(id); |
|
|
|
std::string raw_val; |
|
|
|
std::string raw_val; |
|
|
@ -562,33 +546,19 @@ auto Database::dbGetTrackData(TrackId id) -> std::shared_ptr<TrackData> { |
|
|
|
return ParseDataValue(raw_val); |
|
|
|
return ParseDataValue(raw_val); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
auto Database::dbPutHash(const uint64_t& hash, TrackId i) -> void { |
|
|
|
auto Database::dbCreateIndexesForTrack(const Track& track, |
|
|
|
std::string key = EncodeHashKey(hash); |
|
|
|
leveldb::WriteBatch& batch) -> void { |
|
|
|
std::string val = EncodeHashValue(i); |
|
|
|
dbCreateIndexesForTrack(track.data(), track.tags(), batch); |
|
|
|
if (!db_->Put(leveldb::WriteOptions(), key, val).ok()) { |
|
|
|
|
|
|
|
ESP_LOGE(kTag, "failed to write hash for #%lx", i); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
auto Database::dbGetHash(const uint64_t& hash) -> std::optional<TrackId> { |
|
|
|
|
|
|
|
std::string key = EncodeHashKey(hash); |
|
|
|
|
|
|
|
std::string raw_val; |
|
|
|
|
|
|
|
if (!db_->Get(leveldb::ReadOptions(), key, &raw_val).ok()) { |
|
|
|
|
|
|
|
ESP_LOGW(kTag, "no key found for hash #%llx", hash); |
|
|
|
|
|
|
|
return {}; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
return ParseHashValue(raw_val); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
auto Database::dbCreateIndexesForTrack(const Track& track) -> void { |
|
|
|
auto Database::dbCreateIndexesForTrack(const TrackData& data, |
|
|
|
|
|
|
|
const TrackTags& tags, |
|
|
|
|
|
|
|
leveldb::WriteBatch& batch) -> void { |
|
|
|
for (const IndexInfo& index : getIndexes()) { |
|
|
|
for (const IndexInfo& index : getIndexes()) { |
|
|
|
leveldb::WriteBatch writes; |
|
|
|
auto entries = Index(collator_, index, data, tags); |
|
|
|
auto entries = Index(collator_, index, track); |
|
|
|
|
|
|
|
for (const auto& it : entries) { |
|
|
|
for (const auto& it : entries) { |
|
|
|
writes.Put(EncodeIndexKey(it.first), |
|
|
|
batch.Put(EncodeIndexKey(it.first), {it.second.data(), it.second.size()}); |
|
|
|
{it.second.data(), it.second.size()}); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
db_->Write(leveldb::WriteOptions(), &writes); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
@ -597,9 +567,8 @@ auto Database::dbRemoveIndexes(std::shared_ptr<TrackData> data) -> void { |
|
|
|
if (!tags) { |
|
|
|
if (!tags) { |
|
|
|
return; |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
Track track{data, tags}; |
|
|
|
|
|
|
|
for (const IndexInfo& index : getIndexes()) { |
|
|
|
for (const IndexInfo& index : getIndexes()) { |
|
|
|
auto entries = Index(collator_, index, track); |
|
|
|
auto entries = Index(collator_, index, *data, *tags); |
|
|
|
for (auto it = entries.rbegin(); it != entries.rend(); it++) { |
|
|
|
for (auto it = entries.rbegin(); it != entries.rend(); it++) { |
|
|
|
auto key = EncodeIndexKey(it->first); |
|
|
|
auto key = EncodeIndexKey(it->first); |
|
|
|
auto status = db_->Delete(leveldb::WriteOptions{}, key); |
|
|
|
auto status = db_->Delete(leveldb::WriteOptions{}, key); |
|
|
@ -626,16 +595,14 @@ auto Database::dbRemoveIndexes(std::shared_ptr<TrackData> data) -> void { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
auto Database::dbIngestTagHashes(const TrackTags& tags, |
|
|
|
auto Database::dbIngestTagHashes(const TrackTags& tags, |
|
|
|
std::pmr::unordered_map<Tag, uint64_t>& out) |
|
|
|
std::pmr::unordered_map<Tag, uint64_t>& out, |
|
|
|
-> void { |
|
|
|
leveldb::WriteBatch& batch) -> void { |
|
|
|
leveldb::WriteBatch batch{}; |
|
|
|
|
|
|
|
for (const auto& tag : tags.allPresent()) { |
|
|
|
for (const auto& tag : tags.allPresent()) { |
|
|
|
auto val = tags.get(tag); |
|
|
|
auto val = tags.get(tag); |
|
|
|
auto hash = tagHash(val); |
|
|
|
auto hash = tagHash(val); |
|
|
|
batch.Put(EncodeTagHashKey(hash), tagToString(val)); |
|
|
|
batch.Put(EncodeTagHashKey(hash), tagToString(val)); |
|
|
|
out[tag] = hash; |
|
|
|
out[tag] = hash; |
|
|
|
} |
|
|
|
} |
|
|
|
db_->Write(leveldb::WriteOptions{}, &batch); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
auto Database::dbRecoverTagsFromHashes( |
|
|
|
auto Database::dbRecoverTagsFromHashes( |
|
|
|