From 01eb8683733f39a6de984111f035bb8f71dcf8b8 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Tue, 12 Dec 2023 12:59:38 +1100 Subject: Support more datatypes in track tags --- src/database/track.cpp | 274 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 244 insertions(+), 30 deletions(-) (limited to 'src/database/track.cpp') 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; } -- cgit v1.2.3