diff --git a/src/database/database.cpp b/src/database/database.cpp index 1adfec87..27b5c24c 100644 --- a/src/database/database.cpp +++ b/src/database/database.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -53,7 +54,7 @@ static SingletonEnv sEnv; static const char kDbPath[] = "/.tangara-db"; static const char kKeyDbVersion[] = "schema_version"; -static const uint8_t kCurrentDbVersion = 3; +static const uint8_t kCurrentDbVersion = 4; static const char kKeyCustom[] = "U\0"; static const char kKeyCollator[] = "collator"; @@ -129,13 +130,11 @@ auto Database::Open(IFileGatherer& gatherer, } if (!leveldb::sBackgroundThread) { - leveldb::sBackgroundThread = &bg_worker; + leveldb::sBackgroundThread = tasks::Worker::Start(); } - std::shared_ptr worker( - tasks::Worker::Start()); - return worker - ->Dispatch>( + return bg_worker + .Dispatch>( [&]() -> cpp::result { leveldb::DB* db; std::unique_ptr cache{ @@ -167,8 +166,8 @@ auto Database::Open(IFileGatherer& gatherer, } ESP_LOGI(kTag, "Database opened successfully"); - return new Database(db, cache.release(), gatherer, parser, collator, - worker); + return new Database(db, cache.release(), gatherer, parser, + collator); }) .get(); } @@ -183,11 +182,9 @@ Database::Database(leveldb::DB* db, leveldb::Cache* cache, IFileGatherer& file_gatherer, ITagParser& tag_parser, - locale::ICollator& collator, - std::shared_ptr worker) + locale::ICollator& collator) : db_(db), cache_(cache), - worker_task_(worker), file_gatherer_(file_gatherer), tag_parser_(tag_parser), collator_(collator) {} @@ -412,9 +409,9 @@ auto Database::updateIndexes() -> void { 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()); + tags->title().value_or("no title").c_str(), + tags->artist().value_or("no artist").c_str(), + tags->album().value_or("no album").c_str()); } }); events::Ui().Dispatch(event::UpdateFinished{}); @@ -533,11 +530,11 @@ auto Database::dbIngestTagHashes(const TrackTags& tags, std::pmr::unordered_map& out) -> void { leveldb::WriteBatch batch{}; - for (auto& entry : tags.tags()) { - auto hash = - komihash_stream_oneshot(entry.second.data(), entry.second.size(), 0); - batch.Put(EncodeTagHashKey(hash), entry.second.c_str()); - out[entry.first] = hash; + for (const auto& tag : tags.allPresent()) { + auto val = tags.get(tag); + auto hash = tagHash(val); + batch.Put(EncodeTagHashKey(hash), tagToString(val)); + out[tag] = hash; } db_->Write(leveldb::WriteOptions{}, &batch); } diff --git a/src/database/include/database.hpp b/src/database/include/database.hpp index c75dbf96..88a18e17 100644 --- a/src/database/include/database.hpp +++ b/src/database/include/database.hpp @@ -87,8 +87,6 @@ class Database { leveldb::DB* db_; leveldb::Cache* cache_; - std::shared_ptr worker_task_; - // Not owned. IFileGatherer& file_gatherer_; ITagParser& tag_parser_; @@ -98,8 +96,7 @@ class Database { leveldb::Cache* cache, IFileGatherer& file_gatherer, ITagParser& tag_parser, - locale::ICollator& collator, - std::shared_ptr worker); + locale::ICollator& collator); auto dbMintNewTrackId() -> TrackId; diff --git a/src/database/include/index.hpp b/src/database/include/index.hpp index 15b21ee8..45dae464 100644 --- a/src/database/include/index.hpp +++ b/src/database/include/index.hpp @@ -62,7 +62,7 @@ struct IndexKey { }; auto Index(locale::ICollator&, const IndexInfo&, const Track&) - -> std::vector>; + -> std::vector>; auto ExpandHeader(const IndexKey::Header&, const std::optional&) -> IndexKey::Header; diff --git a/src/database/include/track.hpp b/src/database/include/track.hpp index 0497c94d..610ab487 100644 --- a/src/database/include/track.hpp +++ b/src/database/include/track.hpp @@ -15,6 +15,7 @@ #include #include #include +#include #include "leveldb/db.h" #include "memory_resource.hpp" @@ -51,12 +52,21 @@ enum class Tag { kTitle = 0, kArtist = 1, kAlbum = 2, - kAlbumTrack = 3, - kGenre = 4, - kDuration = 5, + kAlbumArtist = 3, + kDisc = 4, + kTrack = 5, + kAlbumOrder = 6, + kGenres = 7, }; -auto TagToString(Tag t) -> std::string; +using TagValue = std::variant>; + +auto tagName(Tag) -> std::string; +auto tagHash(const TagValue&) -> uint64_t; +auto tagToString(const TagValue&) -> std::string; /* * Owning container for tag-related track metadata that was extracted from a @@ -65,29 +75,43 @@ auto TagToString(Tag t) -> std::string; class TrackTags { public: TrackTags() - : encoding_(Container::kUnsupported), tags_(&memory::kSpiRamResource) {} + : encoding_(Container::kUnsupported), genres_(&memory::kSpiRamResource) {} TrackTags(const TrackTags& other) = delete; TrackTags& operator=(TrackTags& other) = delete; bool operator==(const TrackTags&) const = default; + auto get(Tag) const -> TagValue; + auto set(Tag, std::string_view) -> void; + + auto allPresent() const -> std::vector; + auto encoding() const -> Container { return encoding_; }; auto encoding(Container e) -> void { encoding_ = e; }; - std::optional channels; - std::optional sample_rate; - std::optional bits_per_sample; + auto title() const -> const std::optional&; + auto title(std::string_view) -> void; + + auto artist() const -> const std::optional&; + auto artist(std::string_view) -> void; - std::optional duration; + auto album() const -> const std::optional&; + auto album(std::string_view) -> void; - auto set(const Tag& key, const std::pmr::string& val) -> void; - auto at(const Tag& key) const -> std::optional; - auto operator[](const Tag& key) const -> std::optional; + auto albumArtist() const -> const std::optional&; + auto albumArtist(std::string_view) -> void; - auto tags() const -> const std::pmr::unordered_map& { - return tags_; - } + auto disc() const -> const std::optional&; + auto disc(const std::string_view) -> void; + + auto track() const -> const std::optional&; + auto track(const std::string_view) -> void; + + auto albumOrder() const -> uint32_t; + + auto genres() const -> cpp::span; + auto genres(const std::string_view) -> void; /* * Returns a hash of the 'identifying' tags of this track. That is, a hash @@ -99,7 +123,14 @@ class TrackTags { private: Container encoding_; - std::pmr::unordered_map tags_; + + std::optional title_; + std::optional artist_; + std::optional album_; + std::optional album_artist_; + std::optional disc_; + std::optional track_; + std::pmr::vector genres_; }; /* diff --git a/src/database/index.cpp b/src/database/index.cpp index 7d556192..857fbcc5 100644 --- a/src/database/index.cpp +++ b/src/database/index.cpp @@ -5,30 +5,39 @@ */ #include "index.hpp" +#include #include +#include +#include #include +#include #include +#include #include "collation.hpp" +#include "cppbor.h" #include "esp_log.h" #include "komihash.h" #include "leveldb/write_batch.h" #include "records.hpp" +#include "track.hpp" namespace database { +[[maybe_unused]] static const char* kTag = "index"; + const IndexInfo kAlbumsByArtist{ .id = 1, .name = "Albums by Artist", - .components = {Tag::kArtist, Tag::kAlbum, Tag::kAlbumTrack}, + .components = {Tag::kAlbumArtist, Tag::kAlbum, Tag::kAlbumOrder}, }; const IndexInfo kTracksByGenre{ .id = 2, .name = "Tracks by Genre", - .components = {Tag::kGenre, Tag::kTitle}, + .components = {Tag::kGenres, Tag::kTitle}, }; const IndexInfo kAllTracks{ @@ -40,71 +49,144 @@ const IndexInfo kAllTracks{ const IndexInfo kAllAlbums{ .id = 4, .name = "All Albums", - .components = {Tag::kAlbum, Tag::kAlbumTrack}, + .components = {Tag::kAlbum, Tag::kAlbumOrder}, }; -static auto missing_component_text(const Track& track, Tag tag) - -> std::optional { - switch (tag) { - case Tag::kArtist: - return "Unknown Artist"; - case Tag::kAlbum: - return "Unknown Album"; - case Tag::kGenre: - return "Unknown Genre"; - case Tag::kTitle: - return track.TitleOrFilename(); - case Tag::kAlbumTrack: - return "0000"; - case Tag::kDuration: - default: - return {}; +class Indexer { + public: + Indexer(locale::ICollator& collator, const Track& t, const IndexInfo& idx) + : collator_(collator), track_(t), index_(idx) {} + + auto index() -> std::vector>; + + private: + auto handleLevel(const IndexKey::Header& header, + cpp::span components) -> void; + + auto handleItem(const IndexKey::Header& header, + std::variant item, + cpp::span components) -> void; + + auto missing_value(Tag tag) -> TagValue { + switch (tag) { + case Tag::kTitle: + return track_.TitleOrFilename(); + case Tag::kArtist: + return "Unknown Artist"; + case Tag::kAlbum: + return "Unknown Album"; + case Tag::kAlbumArtist: + return track_.tags().artist().value_or("Unknown Artist"); + return "Unknown Album"; + case Tag::kGenres: + return std::pmr::vector{}; + case Tag::kDisc: + return 0u; + case Tag::kTrack: + return 0u; + case Tag::kAlbumOrder: + return 0u; + } + return std::monostate{}; } + + locale::ICollator& collator_; + const Track& track_; + const IndexInfo index_; + + std::vector> out_; +}; + +auto Indexer::index() -> std::vector> { + out_.clear(); + + IndexKey::Header root_header{ + .id = index_.id, + .depth = 0, + .components_hash = 0, + }; + handleLevel(root_header, index_.components); + + return out_; } -auto Index(locale::ICollator& collator, const IndexInfo& info, const Track& t) - -> std::vector> { - std::vector> out; - IndexKey key{ - .header{ - .id = info.id, - .depth = 0, - .components_hash = 0, +auto Indexer::handleLevel(const IndexKey::Header& header, + cpp::span components) -> void { + Tag component = components.front(); + TagValue value = track_.tags().get(component); + if (std::holds_alternative(value)) { + value = missing_value(component); + } + + std::visit( + [&](auto&& arg) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + ESP_LOGW(kTag, "dropping component without value: %s", + tagName(components.front()).c_str()); + } else if constexpr (std::is_same_v) { + handleItem(header, arg, components); + } else if constexpr (std::is_same_v) { + handleItem(header, arg, components); + } else if constexpr (std::is_same_v< + T, cpp::span>) { + for (const auto& i : arg) { + handleItem(header, i, components); + } + } }, + value); +} + +auto Indexer::handleItem(const IndexKey::Header& header, + std::variant item, + cpp::span components) -> void { + IndexKey key{ + .header = header, .item = {}, .track = {}, }; + std::string value; + + std::string item_text; + std::visit( + [&](auto&& arg) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + value = {arg.data(), arg.size()}; + auto xfrm = collator_.Transform(value); + key.item = {xfrm.data(), xfrm.size()}; + } else if constexpr (std::is_same_v) { + value = std::to_string(arg); + // FIXME: this sucks lol. we should just write the number directly, + // LSB-first, but then we need to be able to parse it back properly. + std::ostringstream str; + str << std::setw(8) << std::setfill('0') << arg; + std::string encoded = str.str(); + key.item = {encoded.data(), encoded.size()}; + } + }, + item); - for (std::uint8_t i = 0; i < info.components.size(); i++) { - // Fill in the text for this depth. - auto text = t.tags().at(info.components.at(i)); - std::pmr::string value; - if (text) { - std::pmr::string orig = *text; - auto xfrm = collator.Transform({orig.data(), orig.size()}); - key.item = {xfrm.data(), xfrm.size()}; - value = *text; - } else { - key.item = {}; - value = missing_component_text(t, info.components.at(i)).value_or(""); - } - - // If this is the last component, then we should also fill in the track id - // and title. - if (i == info.components.size() - 1) { - key.track = t.data().id; - value = t.TitleOrFilename(); - } + std::optional next_level; + if (components.size() == 1) { + value = track_.TitleOrFilename(); + key.track = track_.data().id; + } else { + next_level = ExpandHeader(key.header, key.item); + } - out.push_back(std::make_pair(key, value)); + out_.emplace_back(key, value); - // If there are more components after this, then we need to finish by - // narrowing the header with the current title. - if (i < info.components.size() - 1) { - key.header = ExpandHeader(key.header, key.item); - } + if (next_level) { + handleLevel(*next_level, components.subspan(1)); } - return out; +} + +auto Index(locale::ICollator& c, const IndexInfo& i, const Track& t) + -> std::vector> { + Indexer indexer{c, t, i}; + return indexer.index(); } auto ExpandHeader(const IndexKey::Header& header, diff --git a/src/database/records.cpp b/src/database/records.cpp index a0aac69a..af81dc5c 100644 --- a/src/database/records.cpp +++ b/src/database/records.cpp @@ -11,6 +11,7 @@ #include #include +#include #include #include #include diff --git a/src/database/tag_parser.cpp b/src/database/tag_parser.cpp index 885c71dd..0efe5804 100644 --- a/src/database/tag_parser.cpp +++ b/src/database/tag_parser.cpp @@ -21,26 +21,16 @@ namespace database { -const static std::array, 5> kVorbisIdToTag = {{ +const static std::array, 7> kVorbisIdToTag = {{ {"TITLE", Tag::kTitle}, {"ARTIST", Tag::kArtist}, {"ALBUM", Tag::kAlbum}, - {"TRACKNUMBER", Tag::kAlbumTrack}, - {"GENRE", Tag::kGenre}, + {"ALBUMARTIST", Tag::kAlbumArtist}, + {"DISCNUMBER", Tag::kDisc}, + {"TRACKNUMBER", Tag::kTrack}, + {"GENRE", Tag::kGenres}, }}; -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(), &memory::kSpiRamResource); -} - -static auto convert_track_number(const std::pmr::string& raw) - -> std::pmr::string { - uint32_t as_int = std::atoi(raw.c_str()); - return convert_track_number(as_int); -} - static auto convert_tag(int tag) -> std::optional { switch (tag) { case Ttitle: @@ -50,9 +40,9 @@ static auto convert_tag(int tag) -> std::optional { case Talbum: return Tag::kAlbum; case Ttrack: - return Tag::kAlbumTrack; + return Tag::kTrack; case Tgenre: - return Tag::kGenre; + return Tag::kGenres; default: return {}; } @@ -115,8 +105,6 @@ static void tag(Tagctx* ctx, if (value.empty()) { return; } - if (*tag == Tag::kAlbumTrack) { - } aux->tags->set(*tag, value); } @@ -161,19 +149,14 @@ auto TagParserImpl::ReadAndParseTags(const std::string& path) // There wasn't a track number found in the track's tags. Try to synthesize // one from the filename, which will sometimes have a track number at the // start. - if (!tags->at(Tag::kAlbumTrack)) { + if (!tags->track()) { auto slash_pos = path.find_last_of("/"); 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()}); + tags->track({trunc.data(), trunc.size()}); } } - // Normalise track numbers; they're usually treated as strings, but we would - // like to sort them lexicographically. - tags->set(Tag::kAlbumTrack, - convert_track_number(tags->at(Tag::kAlbumTrack).value_or("0"))); - { std::lock_guard lock{cache_mutex_}; cache_.Put({path.data(), path.size(), &memory::kSpiRamResource}, tags); @@ -241,18 +224,6 @@ auto GenericTagParser::ReadAndParseTags(const std::string& path) out->encoding(Container::kUnsupported); } - if (ctx.channels > 0) { - out->channels = ctx.channels; - } - if (ctx.samplerate > 0) { - out->sample_rate = ctx.samplerate; - } - if (ctx.bitrate > 0) { - out->bits_per_sample = ctx.bitrate; - } - if (ctx.duration > 0) { - out->duration = ctx.duration; - } return out; } diff --git a/src/database/track.cpp b/src/database/track.cpp index 871e3087..acd479f1 100644 --- a/src/database/track.cpp +++ b/src/database/track.cpp @@ -6,14 +6,22 @@ #include "track.hpp" -#include -#include +#include +#include +#include +#include + +#include "esp_log.h" +#include "komihash.h" #include "memory_resource.hpp" +#include "span.hpp" namespace database { -auto TagToString(Tag t) -> std::string { +static constexpr char kGenreDelimiters[] = ",;"; + +auto tagName(Tag t) -> std::string { switch (t) { case Tag::kTitle: return "title"; @@ -21,42 +29,241 @@ auto TagToString(Tag t) -> std::string { return "artist"; case Tag::kAlbum: return "album"; - case Tag::kAlbumTrack: - return "album_track"; - case Tag::kGenre: + case Tag::kAlbumArtist: + return "album_artist"; + case Tag::kDisc: + return "disc"; + case Tag::kTrack: + return "track"; + case Tag::kAlbumOrder: + return "album_order"; + case Tag::kGenres: return "genre"; - case Tag::kDuration: - return "duration"; - default: - return ""; } + return ""; +} + +auto tagHash(const TagValue& t) -> uint64_t { + return std::visit( + [&](auto&& arg) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + return static_cast(0); + } else if constexpr (std::is_same_v) { + return komihash(arg.data(), arg.size(), 0); + } else if constexpr (std::is_same_v) { + return komihash(&arg, sizeof(arg), 0); + } else if constexpr (std::is_same_v< + T, cpp::span>) { + komihash_stream_t hash; + komihash_stream_init(&hash, 0); + for (const auto& i : arg) { + komihash_stream_update(&hash, i.data(), i.size()); + } + return komihash_stream_final(&hash); + } + }, + t); + return 0; } -auto TrackTags::set(const Tag& key, const std::pmr::string& val) -> void { - tags_[key] = val; +auto tagToString(const TagValue& val) -> std::string { + return std::visit( + [&](auto&& arg) -> std::string { + using T = std::decay_t; + if constexpr (std::is_same_v) { + return ""; + } else if constexpr (std::is_same_v) { + return {arg.data(), arg.size()}; + } else if constexpr (std::is_same_v) { + return std::to_string(arg); + } else if constexpr (std::is_same_v< + T, cpp::span>) { + std::ostringstream builder{}; + for (const auto& str : arg) { + builder << std::string{str.data(), str.size()} << ","; + } + return builder.str(); + } + }, + val); + return ""; +} + +template +auto valueOrMonostate(std::optional t) -> TagValue { + if (t) { + return *t; + } + return std::monostate{}; } -auto TrackTags::at(const Tag& key) const -> std::optional { - if (tags_.contains(key)) { - return tags_.at(key); +auto TrackTags::get(Tag t) const -> TagValue { + switch (t) { + case Tag::kTitle: + return valueOrMonostate(title_); + case Tag::kArtist: + return valueOrMonostate(artist_); + case Tag::kAlbum: + return valueOrMonostate(album_); + case Tag::kAlbumArtist: + return valueOrMonostate(album_artist_); + case Tag::kDisc: + return valueOrMonostate(disc_); + case Tag::kTrack: + return valueOrMonostate(track_); + case Tag::kAlbumOrder: + return albumOrder(); + case Tag::kGenres: + return genres_; } - return {}; + return std::monostate{}; +} + +auto TrackTags::set(Tag t, std::string_view v) -> void { + switch (t) { + case Tag::kTitle: + title(v); + break; + case Tag::kArtist: + artist(v); + break; + case Tag::kAlbum: + album(v); + break; + case Tag::kAlbumArtist: + albumArtist(v); + break; + case Tag::kDisc: + disc(v); + break; + case Tag::kTrack: + track(v); + break; + case Tag::kAlbumOrder: + // This tag is derices from disc and track, and so it can't be set. + break; + case Tag::kGenres: + genres(v); + break; + } +} + +auto TrackTags::allPresent() const -> std::vector { + std::vector out; + auto add_if_present = [&](Tag t, auto opt) { + if (opt) { + out.push_back(t); + } + }; + add_if_present(Tag::kTitle, title_); + add_if_present(Tag::kArtist, artist_); + add_if_present(Tag::kAlbum, album_); + add_if_present(Tag::kAlbumArtist, album_artist_); + add_if_present(Tag::kDisc, disc_); + add_if_present(Tag::kTrack, track_); + add_if_present(Tag::kGenres, !genres_.empty()); + return out; +} + +auto TrackTags::title() const -> const std::optional& { + return title_; +} + +auto TrackTags::title(std::string_view s) -> void { + title_ = s; +} + +auto TrackTags::artist() const -> const std::optional& { + return artist_; +} + +auto TrackTags::artist(std::string_view s) -> void { + artist_ = s; +} + +auto TrackTags::album() const -> const std::optional& { + return album_; +} + +auto TrackTags::album(std::string_view s) -> void { + album_ = s; +} + +auto TrackTags::albumArtist() const -> const std::optional& { + return album_artist_; } -auto TrackTags::operator[](const Tag& key) const - -> std::optional { - return at(key); +auto TrackTags::albumArtist(std::string_view s) -> void { + album_artist_ = s; } -/* Helper function to update a komihash stream with a std::pmr::string. */ -auto HashString(komihash_stream_t* stream, const std::pmr::string& str) - -> void { - komihash_stream_update(stream, str.c_str(), str.length()); +auto TrackTags::disc() const -> const std::optional& { + return disc_; +} + +auto TrackTags::disc(const std::string_view s) -> void { + disc_ = std::stoi({s.data(), s.size()}); +} + +auto TrackTags::track() const -> const std::optional& { + return track_; +} + +auto TrackTags::track(const std::string_view s) -> void { + track_ = std::stoi({s.data(), s.size()}); +} + +auto TrackTags::albumOrder() const -> uint32_t { + return (disc_.value_or(0) << 16) | track_.value_or(0); +} + +auto TrackTags::genres() const -> cpp::span { + return genres_; +} + +auto TrackTags::genres(const std::string_view s) -> void { + genres_.clear(); + std::string src = {s.data(), s.size()}; + char* token = std::strtok(src.data(), kGenreDelimiters); + + auto trim_and_add = [=](std::string_view s) { + std::string copy = {s.data(), s.size()}; + + // Trim the left + copy.erase(copy.begin(), + std::find_if(copy.begin(), copy.end(), [](unsigned char ch) { + return !std::isspace(ch); + })); + + // Trim the right + copy.erase(std::find_if(copy.rbegin(), copy.rend(), + [](unsigned char ch) { return !std::isspace(ch); }) + .base(), + copy.end()); + + // Ignore empty strings. + if (!copy.empty()) { + genres_.push_back({copy.data(), copy.size()}); + } + }; + + if (token == NULL) { + // No delimiters found in the input. Treat this as a single genre. + trim_and_add(s); + } else { + while (token != NULL) { + // Add tokens until no more delimiters found. + trim_and_add(token); + token = std::strtok(NULL, kGenreDelimiters); + } + } } /* - * Uses a komihash stream to incrementally hash tags. This lowers the function's - * memory footprint a little so that it's safe to call from any stack. + * Uses a komihash stream to incrementally hash tags. This lowers the + * function's memory footprint a little so that it's safe to call from any + * stack. */ auto TrackTags::Hash() const -> uint64_t { // TODO(jacqueline): this function doesn't work very well for tracks with no @@ -64,16 +271,23 @@ auto TrackTags::Hash() const -> uint64_t { komihash_stream_t stream; komihash_stream_init(&stream, 0); - HashString(&stream, at(Tag::kTitle).value_or("")); - HashString(&stream, at(Tag::kArtist).value_or("")); - HashString(&stream, at(Tag::kAlbum).value_or("")); - HashString(&stream, at(Tag::kAlbumTrack).value_or("")); + auto add = [&](const uint64_t& h) { + komihash_stream_update(&stream, &h, sizeof(h)); + }; + + add(tagHash(get(Tag::kTitle))); + add(tagHash(get(Tag::kArtist))); + add(tagHash(get(Tag::kAlbum))); + add(tagHash(get(Tag::kAlbumArtist))); + + // TODO: Should we be including this? + add(tagHash(get(Tag::kAlbumOrder))); return komihash_stream_final(&stream); } auto Track::TitleOrFilename() const -> std::pmr::string { - auto title = tags().at(Tag::kTitle); + auto title = tags().title(); if (title) { return *title; } diff --git a/src/lua/property.cpp b/src/lua/property.cpp index b424a866..7a45552b 100644 --- a/src/lua/property.cpp +++ b/src/lua/property.cpp @@ -5,6 +5,7 @@ */ #include "property.hpp" +#include #include #include @@ -160,6 +161,29 @@ Property::Property(const LuaValue& val, std::function cb) : value_(val), cb_(cb) {} +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, cpp::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, -2); + } + } else if constexpr (std::is_same_v) { + lua_pushinteger(L, arg); + } else { + lua_pushnil(L); + } + }, + val); +} + auto Property::PushValue(lua_State& s) -> int { std::visit( [&](auto&& arg) { @@ -177,9 +201,9 @@ auto Property::PushValue(lua_State& s) -> int { } else if constexpr (std::is_same_v) { lua_newtable(&s); int table = lua_gettop(&s); - for (const auto& [key, val] : arg.tags->tags()) { - lua_pushstring(&s, database::TagToString(key).c_str()); - lua_pushstring(&s, val.c_str()); + for (const auto& tag : arg.tags->allPresent()) { + lua_pushstring(&s, database::tagName(tag).c_str()); + pushTagValue(&s, arg.tags->get(tag)); lua_settable(&s, table); } if (arg.db_info) {