Merge pull request 'Add an 'All Artists' index including tracks under multiple artists' (#144) from jqln/all-artists into main

Reviewed-on: https://codeberg.org/cool-tech-zone/tangara-fw/pulls/144
custom
cooljqln 4 months ago
commit 25ecf038ac
  1. 4
      lib/libtags/id3v2.c
  2. 1
      lib/libtags/tags.h
  3. 1
      lib/libtags/vorbis.c
  4. 2
      src/tangara/database/database.cpp
  5. 2
      src/tangara/database/database.hpp
  6. 16
      src/tangara/database/index.cpp
  7. 1
      src/tangara/database/index.hpp
  8. 64
      src/tangara/database/tag_parser.cpp
  9. 2
      src/tangara/database/tag_parser.hpp
  10. 127
      src/tangara/database/track.cpp
  11. 7
      src/tangara/database/track.hpp

@ -68,6 +68,10 @@ v2cb(Tagctx *ctx, char *k, char *v)
return 0; return 0;
}else if(strcmp(k-1, "COM") == 0 || strcmp(k-1, "COMM") == 0){ }else if(strcmp(k-1, "COM") == 0 || strcmp(k-1, "COMM") == 0){
txtcb(ctx, Tcomment, k-1, v); txtcb(ctx, Tcomment, k-1, v);
}else if(strcmp(k, "XXX") == 0){
k = v;
v += strlen(v) + 1;
txtcb(ctx, Tunknown, k, v);
}else{ }else{
txtcb(ctx, Tunknown, k-1, v); txtcb(ctx, Tunknown, k-1, v);
} }

@ -10,6 +10,7 @@ enum
{ {
Tunknown = -1, Tunknown = -1,
Tartist, Tartist,
Tmultiartists,
Talbumartist, Talbumartist,
Talbum, Talbum,
Ttitle, Ttitle,

@ -11,6 +11,7 @@ static const struct {
{"album", Talbum}, {"album", Talbum},
{"title", Ttitle}, {"title", Ttitle},
{"artist", Tartist}, {"artist", Tartist},
{"artists", Tmultiartists},
{"albumartist", Talbumartist}, {"albumartist", Talbumartist},
{"tracknumber", Ttrack}, {"tracknumber", Ttrack},
{"date", Tdate}, {"date", Tdate},

@ -292,7 +292,7 @@ auto Database::setTrackData(TrackId id, const TrackData& data) -> void {
auto Database::getIndexes() -> std::vector<IndexInfo> { auto Database::getIndexes() -> std::vector<IndexInfo> {
// TODO(jacqueline): This probably needs to be async? When we have runtime // TODO(jacqueline): This probably needs to be async? When we have runtime
// configurable indexes, they will need to come from somewhere. // configurable indexes, they will need to come from somewhere.
return {kAllTracks, kAllAlbums, kAlbumsByArtist, return {kAllTracks, kAllAlbums, kAllArtists, kAlbumsByArtist,
kTracksByGenre, kPodcasts, kAudiobooks}; kTracksByGenre, kPodcasts, kAudiobooks};
} }

@ -38,7 +38,7 @@
namespace database { namespace database {
const uint8_t kCurrentDbVersion = 8; const uint8_t kCurrentDbVersion = 9;
struct SearchKey; struct SearchKey;
class Record; class Record;

@ -56,15 +56,22 @@ const IndexInfo kAllAlbums{
.components = {Tag::kAlbum, Tag::kAlbumOrder}, .components = {Tag::kAlbum, Tag::kAlbumOrder},
}; };
const IndexInfo kPodcasts{ const IndexInfo kAllArtists{
.id = 5, .id = 5,
.type = MediaType::kMusic,
.name = "All Artists",
.components = {Tag::kAllArtists, Tag::kTitle},
};
const IndexInfo kPodcasts{
.id = 6,
.type = MediaType::kPodcast, .type = MediaType::kPodcast,
.name = "Podcasts", .name = "Podcasts",
.components = {Tag::kAlbum, Tag::kTitle}, .components = {Tag::kAlbum, Tag::kTitle},
}; };
const IndexInfo kAudiobooks{ const IndexInfo kAudiobooks{
.id = 6, .id = 7,
.type = MediaType::kAudiobook, .type = MediaType::kAudiobook,
.name = "Audiobooks", .name = "Audiobooks",
.components = {Tag::kAlbum, Tag::kAlbumOrder}, .components = {Tag::kAlbum, Tag::kAlbumOrder},
@ -109,11 +116,12 @@ class Indexer {
case Tag::kTitle: case Tag::kTitle:
return titleOrFilename(track_data_, track_tags_); return titleOrFilename(track_data_, track_tags_);
case Tag::kArtist: case Tag::kArtist:
case Tag::kAlbumArtist:
return "Unknown Artist"; return "Unknown Artist";
case Tag::kAlbum: case Tag::kAlbum:
return "Unknown Album"; return "Unknown Album";
case Tag::kAlbumArtist: case Tag::kAllArtists:
return track_tags_.artist().value_or("Unknown Artist"); return std::pmr::vector<std::pmr::string>{};
case Tag::kGenres: case Tag::kGenres:
return std::pmr::vector<std::pmr::string>{}; return std::pmr::vector<std::pmr::string>{};
case Tag::kDisc: case Tag::kDisc:

@ -78,6 +78,7 @@ extern const IndexInfo kAlbumsByArtist;
extern const IndexInfo kTracksByGenre; extern const IndexInfo kTracksByGenre;
extern const IndexInfo kAllTracks; extern const IndexInfo kAllTracks;
extern const IndexInfo kAllAlbums; extern const IndexInfo kAllAlbums;
extern const IndexInfo kAllArtists;
extern const IndexInfo kPodcasts; extern const IndexInfo kPodcasts;
extern const IndexInfo kAudiobooks; extern const IndexInfo kAudiobooks;

@ -13,6 +13,7 @@
#include <iomanip> #include <iomanip>
#include <memory> #include <memory>
#include <mutex> #include <mutex>
#include <optional>
#include "database/track.hpp" #include "database/track.hpp"
#include "debug.hpp" #include "debug.hpp"
@ -32,6 +33,8 @@ static auto convert_tag(int tag) -> std::optional<Tag> {
return Tag::kTitle; return Tag::kTitle;
case Tartist: case Tartist:
return Tag::kArtist; return Tag::kArtist;
case Tmultiartists:
return Tag::kAllArtists;
case Talbumartist: case Talbumartist:
return Tag::kAlbumArtist; return Tag::kAlbumArtist;
case Talbum: case Talbum:
@ -45,6 +48,29 @@ static auto convert_tag(int tag) -> std::optional<Tag> {
} }
} }
static std::unordered_map<std::string, Tag> sVorbisNameToTag{
{"TITLE", Tag::kTitle},
{"ALBUM", Tag::kAlbum},
{"ARTIST", Tag::kArtist},
{"ARTISTS", Tag::kAllArtists},
{"ALBUMARTIST", Tag::kAlbumArtist},
{"TRACK", Tag::kTrack},
{"TRACKNUMBER", Tag::kTrack},
{"GENRE", Tag::kGenres},
{"DISC", Tag::kDisc},
{"DISCNUMBER", Tag::kDisc},
};
static auto convert_vorbis_tag(const std::string_view name)
-> std::optional<Tag> {
std::string name_upper{name};
std::transform(name.begin(), name.end(), name_upper.begin(), ::toupper);
if (sVorbisNameToTag.contains(name_upper)) {
return sVorbisNameToTag[name_upper];
}
return {};
}
namespace libtags { namespace libtags {
struct Aux { struct Aux {
@ -94,7 +120,14 @@ static void tag(Tagctx* ctx,
int size, int size,
Tagread f) { Tagread f) {
Aux* aux = reinterpret_cast<Aux*>(ctx->aux); Aux* aux = reinterpret_cast<Aux*>(ctx->aux);
auto tag = convert_tag(t); std::optional<Tag> tag;
if (t == Tunknown && k && v) {
// Sometimes 'unknown' tags are vorbis comments shoved into a generic tag
// name in other containers.
tag = convert_vorbis_tag(k);
} else {
tag = convert_tag(t);
}
if (!tag) { if (!tag) {
return; return;
} }
@ -166,17 +199,7 @@ auto TagParserImpl::ReadAndParseTags(std::string_view path)
return tags; return tags;
} }
OggTagParser::OggTagParser() { OggTagParser::OggTagParser() {}
nameToTag_["TITLE"] = Tag::kTitle;
nameToTag_["ALBUM"] = Tag::kAlbum;
nameToTag_["ARTIST"] = Tag::kArtist;
nameToTag_["ALBUMARTIST"] = Tag::kAlbumArtist;
nameToTag_["TRACK"] = Tag::kTrack;
nameToTag_["TRACKNUMBER"] = Tag::kTrack;
nameToTag_["GENRE"] = Tag::kGenres;
nameToTag_["DISC"] = Tag::kDisc;
nameToTag_["DISCNUMBER"] = Tag::kDisc;
}
auto OggTagParser::ReadAndParseTags(std::string_view p) auto OggTagParser::ReadAndParseTags(std::string_view p)
-> std::shared_ptr<TrackTags> { -> std::shared_ptr<TrackTags> {
@ -292,8 +315,9 @@ auto OggTagParser::parseComments(TrackTags& res, std::span<unsigned char> data)
std::string key_upper{key}; std::string key_upper{key};
std::transform(key.begin(), key.end(), key_upper.begin(), ::toupper); std::transform(key.begin(), key.end(), key_upper.begin(), ::toupper);
if (nameToTag_.contains(key_upper) && !val.empty()) { auto tag = convert_vorbis_tag(key);
res.set(nameToTag_[key_upper], val); if (tag && !val.empty()) {
res.set(*tag, val);
} }
} }
@ -313,14 +337,16 @@ auto GenericTagParser::ReadAndParseTags(std::string_view p)
std::string path{p}; std::string path{p};
libtags::Aux aux; libtags::Aux aux;
// Fail fast if trying to parse a file that doesn't appear to be a supported audio format // Fail fast if trying to parse a file that doesn't appear to be a supported
// For context, see: https://codeberg.org/cool-tech-zone/tangara-fw/issues/149 // audio format For context, see:
// https://codeberg.org/cool-tech-zone/tangara-fw/issues/149
bool found = false; bool found = false;
for (const auto& ext : supported_exts) { for (const auto& ext : supported_exts) {
// Case-insensitive file extension check // Case-insensitive file extension check
if (std::equal(ext.rbegin(), ext.rend(), path.rbegin(), if (std::equal(ext.rbegin(), ext.rend(), path.rbegin(), [](char a, char b) {
[](char a, char b) { return std::tolower(a) == std::tolower(b); })) { return std::tolower(a) == std::tolower(b);
found=true; })) {
found = true;
break; break;
} }
} }

@ -47,8 +47,6 @@ class OggTagParser : public ITagParser {
private: private:
auto parseComments(TrackTags&, std::span<unsigned char> data) -> void; auto parseComments(TrackTags&, std::span<unsigned char> data) -> void;
auto parseLength(std::span<unsigned char> data) -> uint64_t; auto parseLength(std::span<unsigned char> data) -> uint64_t;
std::unordered_map<std::string, Tag> nameToTag_;
}; };
class GenericTagParser : public ITagParser { class GenericTagParser : public ITagParser {

@ -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;

@ -73,6 +73,7 @@ enum class Tag {
kTrack = 5, kTrack = 5,
kAlbumOrder = 6, kAlbumOrder = 6,
kGenres = 7, kGenres = 7,
kAllArtists = 8,
}; };
using TagValue = std::variant<std::monostate, using TagValue = std::variant<std::monostate,
@ -114,6 +115,9 @@ class TrackTags {
auto artist() const -> const std::optional<std::pmr::string>&; auto artist() const -> const std::optional<std::pmr::string>&;
auto artist(std::string_view) -> void; auto artist(std::string_view) -> void;
auto allArtists() const -> std::span<const std::pmr::string>;
auto allArtists(const std::string_view) -> void;
auto album() const -> const std::optional<std::pmr::string>&; auto album() const -> const std::optional<std::pmr::string>&;
auto album(std::string_view) -> void; auto album(std::string_view) -> void;
@ -140,10 +144,13 @@ class TrackTags {
auto Hash() const -> uint64_t; auto Hash() const -> uint64_t;
private: private:
auto maybeSynthesizeAllArtists() -> void;
Container encoding_; Container encoding_;
std::optional<std::pmr::string> title_; std::optional<std::pmr::string> title_;
std::optional<std::pmr::string> artist_; std::optional<std::pmr::string> artist_;
std::pmr::vector<std::pmr::string> allArtists_;
std::optional<std::pmr::string> album_; std::optional<std::pmr::string> album_;
std::optional<std::pmr::string> album_artist_; std::optional<std::pmr::string> album_artist_;
std::optional<uint8_t> disc_; std::optional<uint8_t> disc_;

Loading…
Cancel
Save