|
|
@ -20,6 +20,7 @@ |
|
|
|
|
|
|
|
|
|
|
|
namespace database { |
|
|
|
namespace database { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static constexpr char kAllArtistDelimiters[] = ";"; |
|
|
|
static constexpr char kGenreDelimiters[] = ",;"; |
|
|
|
static constexpr char kGenreDelimiters[] = ",;"; |
|
|
|
|
|
|
|
|
|
|
|
auto tagName(Tag t) -> std::string { |
|
|
|
auto tagName(Tag t) -> std::string { |
|
|
@ -28,6 +29,8 @@ auto tagName(Tag t) -> std::string { |
|
|
|
return "title"; |
|
|
|
return "title"; |
|
|
|
case Tag::kArtist: |
|
|
|
case Tag::kArtist: |
|
|
|
return "artist"; |
|
|
|
return "artist"; |
|
|
|
|
|
|
|
case Tag::kAllArtists: |
|
|
|
|
|
|
|
return "all_artists"; |
|
|
|
case Tag::kAlbum: |
|
|
|
case Tag::kAlbum: |
|
|
|
return "album"; |
|
|
|
return "album"; |
|
|
|
case Tag::kAlbumArtist: |
|
|
|
case Tag::kAlbumArtist: |
|
|
@ -91,6 +94,50 @@ auto tagToString(const TagValue& val) -> std::string { |
|
|
|
return ""; |
|
|
|
return ""; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
|
|
* Utility for taking a string containing delimited tags, and splitting it out |
|
|
|
|
|
|
|
* into a vector of individual tags. |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
auto parseDelimitedTags(const std::string_view s, |
|
|
|
|
|
|
|
const char* delimiters, |
|
|
|
|
|
|
|
std::pmr::vector<std::pmr::string>& out) -> void { |
|
|
|
|
|
|
|
out.clear(); |
|
|
|
|
|
|
|
std::string src = {s.data(), s.size()}; |
|
|
|
|
|
|
|
char* token = std::strtok(src.data(), delimiters); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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()) { |
|
|
|
|
|
|
|
out.push_back({copy.data(), copy.size()}); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (token == NULL) { |
|
|
|
|
|
|
|
// No delimiters found in the input. Treat this as a single result.
|
|
|
|
|
|
|
|
trim_and_add(s); |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
while (token != NULL) { |
|
|
|
|
|
|
|
// Add tokens until no more delimiters found.
|
|
|
|
|
|
|
|
trim_and_add(token); |
|
|
|
|
|
|
|
token = std::strtok(NULL, delimiters); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
auto TrackTags::create() -> std::shared_ptr<TrackTags> { |
|
|
|
auto TrackTags::create() -> std::shared_ptr<TrackTags> { |
|
|
|
return std::allocate_shared<TrackTags, |
|
|
|
return std::allocate_shared<TrackTags, |
|
|
|
std::pmr::polymorphic_allocator<TrackTags>>( |
|
|
|
std::pmr::polymorphic_allocator<TrackTags>>( |
|
|
@ -108,21 +155,23 @@ auto valueOrMonostate(std::optional<T> t) -> TagValue { |
|
|
|
auto TrackTags::get(Tag t) const -> TagValue { |
|
|
|
auto TrackTags::get(Tag t) const -> TagValue { |
|
|
|
switch (t) { |
|
|
|
switch (t) { |
|
|
|
case Tag::kTitle: |
|
|
|
case Tag::kTitle: |
|
|
|
return valueOrMonostate(title_); |
|
|
|
return valueOrMonostate(title()); |
|
|
|
case Tag::kArtist: |
|
|
|
case Tag::kArtist: |
|
|
|
return valueOrMonostate(artist_); |
|
|
|
return valueOrMonostate(artist()); |
|
|
|
|
|
|
|
case Tag::kAllArtists: |
|
|
|
|
|
|
|
return allArtists(); |
|
|
|
case Tag::kAlbum: |
|
|
|
case Tag::kAlbum: |
|
|
|
return valueOrMonostate(album_); |
|
|
|
return valueOrMonostate(album()); |
|
|
|
case Tag::kAlbumArtist: |
|
|
|
case Tag::kAlbumArtist: |
|
|
|
return valueOrMonostate(album_artist_); |
|
|
|
return valueOrMonostate(albumArtist()); |
|
|
|
case Tag::kDisc: |
|
|
|
case Tag::kDisc: |
|
|
|
return valueOrMonostate(disc_); |
|
|
|
return valueOrMonostate(disc()); |
|
|
|
case Tag::kTrack: |
|
|
|
case Tag::kTrack: |
|
|
|
return valueOrMonostate(track_); |
|
|
|
return valueOrMonostate(track()); |
|
|
|
case Tag::kAlbumOrder: |
|
|
|
case Tag::kAlbumOrder: |
|
|
|
return albumOrder(); |
|
|
|
return albumOrder(); |
|
|
|
case Tag::kGenres: |
|
|
|
case Tag::kGenres: |
|
|
|
return genres_; |
|
|
|
return genres(); |
|
|
|
} |
|
|
|
} |
|
|
|
return std::monostate{}; |
|
|
|
return std::monostate{}; |
|
|
|
} |
|
|
|
} |
|
|
@ -135,6 +184,9 @@ auto TrackTags::set(Tag t, std::string_view v) -> void { |
|
|
|
case Tag::kArtist: |
|
|
|
case Tag::kArtist: |
|
|
|
artist(v); |
|
|
|
artist(v); |
|
|
|
break; |
|
|
|
break; |
|
|
|
|
|
|
|
case Tag::kAllArtists: |
|
|
|
|
|
|
|
allArtists(v); |
|
|
|
|
|
|
|
break; |
|
|
|
case Tag::kAlbum: |
|
|
|
case Tag::kAlbum: |
|
|
|
album(v); |
|
|
|
album(v); |
|
|
|
break; |
|
|
|
break; |
|
|
@ -165,6 +217,7 @@ auto TrackTags::allPresent() const -> std::vector<Tag> { |
|
|
|
}; |
|
|
|
}; |
|
|
|
add_if_present(Tag::kTitle, title_); |
|
|
|
add_if_present(Tag::kTitle, title_); |
|
|
|
add_if_present(Tag::kArtist, artist_); |
|
|
|
add_if_present(Tag::kArtist, artist_); |
|
|
|
|
|
|
|
add_if_present(Tag::kAllArtists, !allArtists_.empty()); |
|
|
|
add_if_present(Tag::kAlbum, album_); |
|
|
|
add_if_present(Tag::kAlbum, album_); |
|
|
|
add_if_present(Tag::kAlbumArtist, album_artist_); |
|
|
|
add_if_present(Tag::kAlbumArtist, album_artist_); |
|
|
|
add_if_present(Tag::kDisc, disc_); |
|
|
|
add_if_present(Tag::kDisc, disc_); |
|
|
@ -187,6 +240,16 @@ auto TrackTags::artist() const -> const std::optional<std::pmr::string>& { |
|
|
|
|
|
|
|
|
|
|
|
auto TrackTags::artist(std::string_view s) -> void { |
|
|
|
auto TrackTags::artist(std::string_view s) -> void { |
|
|
|
artist_ = s; |
|
|
|
artist_ = s; |
|
|
|
|
|
|
|
maybeSynthesizeAllArtists(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
auto TrackTags::allArtists() const -> std::span<const std::pmr::string> { |
|
|
|
|
|
|
|
return allArtists_; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
auto TrackTags::allArtists(const std::string_view s) -> void { |
|
|
|
|
|
|
|
parseDelimitedTags(s, kAllArtistDelimiters, allArtists_); |
|
|
|
|
|
|
|
maybeSynthesizeAllArtists(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
auto TrackTags::album() const -> const std::optional<std::pmr::string>& { |
|
|
|
auto TrackTags::album() const -> const std::optional<std::pmr::string>& { |
|
|
@ -198,6 +261,9 @@ auto TrackTags::album(std::string_view s) -> void { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
auto TrackTags::albumArtist() const -> const std::optional<std::pmr::string>& { |
|
|
|
auto TrackTags::albumArtist() const -> const std::optional<std::pmr::string>& { |
|
|
|
|
|
|
|
if (!album_artist_) { |
|
|
|
|
|
|
|
return artist_; |
|
|
|
|
|
|
|
} |
|
|
|
return album_artist_; |
|
|
|
return album_artist_; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
@ -230,41 +296,7 @@ auto TrackTags::genres() const -> std::span<const std::pmr::string> { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
auto TrackTags::genres(const std::string_view s) -> void { |
|
|
|
auto TrackTags::genres(const std::string_view s) -> void { |
|
|
|
genres_.clear(); |
|
|
|
parseDelimitedTags(s, kGenreDelimiters, genres_); |
|
|
|
std::string src = {s.data(), s.size()}; |
|
|
|
|
|
|
|
char* token = std::strtok(src.data(), kGenreDelimiters); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
auto trim_and_add = [this](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); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
/*
|
|
|
@ -293,6 +325,17 @@ auto TrackTags::Hash() const -> uint64_t { |
|
|
|
return komihash_stream_final(&stream); |
|
|
|
return komihash_stream_final(&stream); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
|
|
* Adds the current 'artist' tag to 'allArtists' if needed. Many tracks lack a |
|
|
|
|
|
|
|
* fine-grained 'ARTISTS=' tag (or equivalent), but pushing down this nuance to |
|
|
|
|
|
|
|
* consumers of TrackTags adds a lot of complexity. |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
auto TrackTags::maybeSynthesizeAllArtists() -> void { |
|
|
|
|
|
|
|
if (allArtists_.empty() && artist_) { |
|
|
|
|
|
|
|
allArtists_.push_back(*artist_); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
auto database::TrackData::clone() const -> std::shared_ptr<TrackData> { |
|
|
|
auto database::TrackData::clone() const -> std::shared_ptr<TrackData> { |
|
|
|
auto data = std::make_shared<TrackData>(); |
|
|
|
auto data = std::make_shared<TrackData>(); |
|
|
|
data->id = id; |
|
|
|
data->id = id; |
|
|
|