|
|
|
@ -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 ""; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
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 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::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 std::monostate{}; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
auto TrackTags::set(const Tag& key, const std::pmr::string& val) -> void { |
|
|
|
|
tags_[key] = val; |
|
|
|
|
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::at(const Tag& key) const -> std::optional<std::pmr::string> { |
|
|
|
|
if (tags_.contains(key)) { |
|
|
|
|
return tags_.at(key); |
|
|
|
|
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); |
|
|
|
|
} |
|
|
|
|
return {}; |
|
|
|
|
}; |
|
|
|
|
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::operator[](const Tag& key) const |
|
|
|
|
-> std::optional<std::pmr::string> { |
|
|
|
|
return at(key); |
|
|
|
|
auto TrackTags::artist() const -> const std::optional<std::pmr::string>& { |
|
|
|
|
return artist_; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* 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::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::albumArtist(std::string_view s) -> void { |
|
|
|
|
album_artist_ = s; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
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; |
|
|
|
|
} |
|
|
|
|