summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjacqueline <me@jacqueline.id.au>2023-12-12 12:59:38 +1100
committerjacqueline <me@jacqueline.id.au>2023-12-12 12:59:38 +1100
commit01eb8683733f39a6de984111f035bb8f71dcf8b8 (patch)
treefb682164eb781a26d1798294180632e63a724f52
parentbd730c82b0423af65f7148bb4abe01e1c3430691 (diff)
downloadtangara-fw-01eb8683733f39a6de984111f035bb8f71dcf8b8.tar.gz
Support more datatypes in track tags
-rw-r--r--src/database/database.cpp35
-rw-r--r--src/database/include/database.hpp5
-rw-r--r--src/database/include/index.hpp2
-rw-r--r--src/database/include/track.hpp63
-rw-r--r--src/database/index.cpp190
-rw-r--r--src/database/records.cpp1
-rw-r--r--src/database/tag_parser.cpp47
-rw-r--r--src/database/track.cpp274
-rw-r--r--src/lua/property.cpp30
9 files changed, 482 insertions, 165 deletions
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 <cstdint>
#include <functional>
#include <iomanip>
+#include <iostream>
#include <memory>
#include <optional>
#include <sstream>
@@ -53,7 +54,7 @@ static SingletonEnv<leveldb::EspEnv> 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<tasks::Type::kDatabase>();
}
- std::shared_ptr<tasks::Worker> worker(
- tasks::Worker::Start<tasks::Type::kDatabase>());
- return worker
- ->Dispatch<cpp::result<Database*, DatabaseError>>(
+ return bg_worker
+ .Dispatch<cpp::result<Database*, DatabaseError>>(
[&]() -> cpp::result<Database*, DatabaseError> {
leveldb::DB* db;
std::unique_ptr<leveldb::Cache> 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<tasks::Worker> 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<Tag, uint64_t>& 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<tasks::Worker> 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<tasks::Worker> 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::pair<IndexKey, std::pmr::string>>;
+ -> std::vector<std::pair<IndexKey, std::string>>;
auto ExpandHeader(const IndexKey::Header&,
const std::optional<std::pmr::string>&) -> 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 <string>
#include <unordered_map>
#include <utility>
+#include <variant>
#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<std::monostate,
+ std::pmr::string,
+ uint32_t,
+ cpp::span<const std::pmr::string>>;
+
+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<Tag>;
+
auto encoding() const -> Container { return encoding_; };
auto encoding(Container e) -> void { encoding_ = e; };
- std::optional<int> channels;
- std::optional<int> sample_rate;
- std::optional<int> bits_per_sample;
+ auto title() const -> const std::optional<std::pmr::string>&;
+ auto title(std::string_view) -> void;
+
+ auto artist() const -> const std::optional<std::pmr::string>&;
+ auto artist(std::string_view) -> void;
- std::optional<int> duration;
+ auto album() const -> const std::optional<std::pmr::string>&;
+ 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<std::pmr::string>;
- auto operator[](const Tag& key) const -> std::optional<std::pmr::string>;
+ auto albumArtist() const -> const std::optional<std::pmr::string>&;
+ auto albumArtist(std::string_view) -> void;
- auto tags() const -> const std::pmr::unordered_map<Tag, std::pmr::string>& {
- return tags_;
- }
+ auto disc() const -> const std::optional<uint8_t>&;
+ auto disc(const std::string_view) -> void;
+
+ auto track() const -> const std::optional<uint16_t>&;
+ auto track(const std::string_view) -> void;
+
+ auto albumOrder() const -> uint32_t;
+
+ auto genres() const -> cpp::span<const std::pmr::string>;
+ 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<Tag, std::pmr::string> tags_;
+
+ std::optional<std::pmr::string> title_;
+ std::optional<std::pmr::string> artist_;
+ std::optional<std::pmr::string> album_;
+ std::optional<std::pmr::string> album_artist_;
+ std::optional<uint8_t> disc_;
+ std::optional<uint16_t> track_;
+ std::pmr::vector<std::pmr::string> 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 <sys/_stdint.h>
#include <cstdint>
+#include <iomanip>
+#include <iostream>
#include <sstream>
+#include <string>
#include <variant>
+#include <vector>
#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<std::pmr::string> {
- 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<std::pair<IndexKey, std::string>>;
+
+ private:
+ auto handleLevel(const IndexKey::Header& header,
+ cpp::span<const Tag> components) -> void;
+
+ auto handleItem(const IndexKey::Header& header,
+ std::variant<std::pmr::string, uint32_t> item,
+ cpp::span<const Tag> 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<std::pmr::string>{};
+ 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<std::pair<IndexKey, std::string>> out_;
+};
+
+auto Indexer::index() -> std::vector<std::pair<IndexKey, std::string>> {
+ 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::pair<IndexKey, std::pmr::string>> {
- std::vector<std::pair<IndexKey, std::pmr::string>> out;
- IndexKey key{
- .header{
- .id = info.id,
- .depth = 0,
- .components_hash = 0,
+auto Indexer::handleLevel(const IndexKey::Header& header,
+ cpp::span<const Tag> components) -> void {
+ Tag component = components.front();
+ TagValue value = track_.tags().get(component);
+ if (std::holds_alternative<std::monostate>(value)) {
+ value = missing_value(component);
+ }
+
+ std::visit(
+ [&](auto&& arg) {
+ using T = std::decay_t<decltype(arg)>;
+ if constexpr (std::is_same_v<T, std::monostate>) {
+ ESP_LOGW(kTag, "dropping component without value: %s",
+ tagName(components.front()).c_str());
+ } else if constexpr (std::is_same_v<T, std::pmr::string>) {
+ handleItem(header, arg, components);
+ } else if constexpr (std::is_same_v<T, uint32_t>) {
+ handleItem(header, arg, components);
+ } else if constexpr (std::is_same_v<
+ T, cpp::span<const std::pmr::string>>) {
+ for (const auto& i : arg) {
+ handleItem(header, i, components);
+ }
+ }
},
+ value);
+}
+
+auto Indexer::handleItem(const IndexKey::Header& header,
+ std::variant<std::pmr::string, uint32_t> item,
+ cpp::span<const Tag> components) -> void {
+ IndexKey key{
+ .header = header,
.item = {},
.track = {},
};
+ std::string value;
+
+ std::string item_text;
+ std::visit(
+ [&](auto&& arg) {
+ using T = std::decay_t<decltype(arg)>;
+ if constexpr (std::is_same_v<T, std::pmr::string>) {
+ value = {arg.data(), arg.size()};
+ auto xfrm = collator_.Transform(value);
+ key.item = {xfrm.data(), xfrm.size()};
+ } else if constexpr (std::is_same_v<T, uint32_t>) {
+ 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<IndexKey::Header> 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<std::pair<IndexKey, std::string>> {
+ 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 <functional>
#include <iomanip>
+#include <iostream>
#include <memory_resource>
#include <sstream>
#include <string>
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<std::pair<const char*, Tag>, 5> kVorbisIdToTag = {{
+const static std::array<std::pair<const char*, Tag>, 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<Tag> {
switch (tag) {
case Ttitle:
@@ -50,9 +40,9 @@ static auto convert_tag(int tag) -> std::optional<Tag> {
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<std::mutex> 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 <komihash.h>
-#include <sys/_stdint.h>
+#include <iomanip>
+#include <iostream>
+#include <sstream>
+#include <string>
+
+#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<decltype(arg)>;
+ if constexpr (std::is_same_v<T, std::monostate>) {
+ return static_cast<uint64_t>(0);
+ } else if constexpr (std::is_same_v<T, std::pmr::string>) {
+ return komihash(arg.data(), arg.size(), 0);
+ } else if constexpr (std::is_same_v<T, uint32_t>) {
+ return komihash(&arg, sizeof(arg), 0);
+ } else if constexpr (std::is_same_v<
+ T, cpp::span<const std::pmr::string>>) {
+ 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<decltype(arg)>;
+ if constexpr (std::is_same_v<T, std::monostate>) {
+ return "";
+ } else if constexpr (std::is_same_v<T, std::pmr::string>) {
+ return {arg.data(), arg.size()};
+ } else if constexpr (std::is_same_v<T, uint32_t>) {
+ return std::to_string(arg);
+ } else if constexpr (std::is_same_v<
+ T, cpp::span<const std::pmr::string>>) {
+ std::ostringstream builder{};
+ for (const auto& str : arg) {
+ builder << std::string{str.data(), str.size()} << ",";
+ }
+ return builder.str();
+ }
+ },
+ val);
+ return "";
+}
+
+template <typename T>
+auto valueOrMonostate(std::optional<T> t) -> TagValue {
+ if (t) {
+ return *t;
+ }
+ return std::monostate{};
}
-auto TrackTags::at(const Tag& key) const -> std::optional<std::pmr::string> {
- 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<Tag> {
+ std::vector<Tag> 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<std::pmr::string>& {
+ return title_;
+}
+
+auto TrackTags::title(std::string_view s) -> void {
+ title_ = s;
+}
+
+auto TrackTags::artist() const -> const std::optional<std::pmr::string>& {
+ return artist_;
+}
+
+auto TrackTags::artist(std::string_view s) -> void {
+ artist_ = s;
+}
+
+auto TrackTags::album() const -> const std::optional<std::pmr::string>& {
+ return album_;
+}
+
+auto TrackTags::album(std::string_view s) -> void {
+ album_ = s;
+}
+
+auto TrackTags::albumArtist() const -> const std::optional<std::pmr::string>& {
+ return album_artist_;
}
-auto TrackTags::operator[](const Tag& key) const
- -> std::optional<std::pmr::string> {
- 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<uint8_t>& {
+ 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<uint16_t>& {
+ 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<const std::pmr::string> {
+ 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 <sys/_stdint.h>
#include <memory>
#include <string>
@@ -160,6 +161,29 @@ Property::Property(const LuaValue& val,
std::function<bool(const LuaValue& val)> 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<decltype(arg)>;
+ if constexpr (std::is_same_v<T, std::pmr::string>) {
+ lua_pushlstring(L, arg.data(), arg.size());
+ } else if constexpr (std::is_same_v<
+ T, cpp::span<const std::pmr::string>>) {
+ 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<T, uint32_t>) {
+ 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<T, audio::Track>) {
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) {