diff --git a/src/database/CMakeLists.txt b/src/database/CMakeLists.txt index 129920cd..008757f8 100644 --- a/src/database/CMakeLists.txt +++ b/src/database/CMakeLists.txt @@ -7,7 +7,7 @@ idf_component_register( "file_gatherer.cpp" "tag_parser.cpp" "index.cpp" INCLUDE_DIRS "include" REQUIRES "result" "span" "esp_psram" "fatfs" "libtags" "komihash" "cbor" - "tasks" "shared_string" "util" "tinyfsm" "events") + "tasks" "shared_string" "util" "tinyfsm" "events" "opusfile") target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS}) diff --git a/src/database/database.cpp b/src/database/database.cpp index 0ed5e389..c8dd49be 100644 --- a/src/database/database.cpp +++ b/src/database/database.cpp @@ -254,6 +254,10 @@ auto Database::Update() -> std::future { } else if (existing_data->filepath() != path) { 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()); } }); events::Ui().Dispatch(event::UpdateFinished{}); diff --git a/src/database/include/tag_parser.hpp b/src/database/include/tag_parser.hpp index dcc8aa21..85721357 100644 --- a/src/database/include/tag_parser.hpp +++ b/src/database/include/tag_parser.hpp @@ -20,18 +20,27 @@ class ITagParser { -> bool = 0; }; +class GenericTagParser : public ITagParser { + public: + auto ReadAndParseTags(const std::string& path, TrackTags* out) + -> bool override; +}; + class TagParserImpl : public ITagParser { public: + TagParserImpl(); auto ReadAndParseTags(const std::string& path, TrackTags* out) -> bool override; private: - std::mutex cache_mutex_; + std::map> extension_to_parser_; + GenericTagParser generic_parser_; /* * Cache of tags that have already been extracted from files. Ideally this * cache should be slightly larger than any page sizes in the UI. */ + std::mutex cache_mutex_; util::LruCache<16, std::string, TrackTags> cache_; // We could also consider keeping caches of artist name -> shared_string and @@ -39,4 +48,10 @@ class TagParserImpl : public ITagParser { // of our UI. }; +class OpusTagParser : public ITagParser { + public: + auto ReadAndParseTags(const std::string& path, TrackTags* out) + -> bool override; +}; + } // namespace database diff --git a/src/database/index.cpp b/src/database/index.cpp index 844b33a3..6ec88622 100644 --- a/src/database/index.cpp +++ b/src/database/index.cpp @@ -37,7 +37,8 @@ const IndexInfo kAllAlbums{ .components = {Tag::kAlbum, Tag::kAlbumTrack}, }; -static auto missing_component_text(Tag tag) -> std::optional { +static auto missing_component_text(const Track& track, Tag tag) + -> std::optional { switch (tag) { case Tag::kArtist: return "Unknown Artist"; @@ -45,9 +46,10 @@ static auto missing_component_text(Tag tag) -> std::optional { return "Unknown Album"; case Tag::kGenre: return "Unknown Genre"; + case Tag::kTitle: + return track.TitleOrFilename(); case Tag::kAlbumTrack: case Tag::kDuration: - case Tag::kTitle: default: return {}; } @@ -77,18 +79,14 @@ auto Index(const IndexInfo& info, const Track& t, leveldb::WriteBatch* batch) value = *text; } else { key.item = {}; - value = missing_component_text(info.components.at(i)).value_or(""); + 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(); - if (info.components.at(i) != Tag::kTitle) { - value = t.TitleOrFilename(); - } - } else { - key.track = {}; + value = t.TitleOrFilename(); } auto encoded = EncodeIndexKey(key); diff --git a/src/database/tag_parser.cpp b/src/database/tag_parser.cpp index fc6c95f2..03e92b95 100644 --- a/src/database/tag_parser.cpp +++ b/src/database/tag_parser.cpp @@ -13,6 +13,7 @@ #include #include #include +#include "opusfile.h" namespace database { @@ -83,16 +84,20 @@ static void tag(Tagctx* ctx, Tagread f) { Aux* aux = reinterpret_cast(ctx->aux); auto tag = convert_tag(t); - if (tag) { - std::string value{v}; - if (*tag == Tag::kAlbumTrack) { - uint32_t as_int = std::atoi(v); - std::ostringstream oss; - oss << std::setw(4) << std::setfill('0') << as_int; - value = oss.str(); - } - aux->tags->set(*tag, value); + if (!tag) { + return; + } + std::string value{v}; + if (value.empty()) { + return; + } + if (*tag == Tag::kAlbumTrack) { + uint32_t as_int = std::atoi(v); + std::ostringstream oss; + oss << std::setw(4) << std::setfill('0') << as_int; + value = oss.str(); } + aux->tags->set(*tag, value); } static void toc(Tagctx* ctx, int ms, int offset) {} @@ -102,6 +107,10 @@ static void toc(Tagctx* ctx, int ms, int offset) {} static const std::size_t kBufSize = 1024; static const char* kTag = "TAGS"; +TagParserImpl::TagParserImpl() { + extension_to_parser_["opus"] = std::make_unique(); +} + auto TagParserImpl::ReadAndParseTags(const std::string& path, TrackTags* out) -> bool { { @@ -113,6 +122,31 @@ auto TagParserImpl::ReadAndParseTags(const std::string& path, TrackTags* out) } } + ITagParser* parser = &generic_parser_; + auto dot_pos = path.find_last_of("."); + if (dot_pos != std::string::npos && path.size() - dot_pos > 1) { + std::string extension = path.substr(dot_pos + 1); + std::transform(extension.begin(), extension.end(), extension.begin(), + [](unsigned char c) { return std::tolower(c); }); + if (extension_to_parser_.contains(extension)) { + parser = extension_to_parser_[extension].get(); + } + } + + if (!parser->ReadAndParseTags(path, out)) { + return false; + } + + { + std::lock_guard lock{cache_mutex_}; + cache_.Put(path, *out); + } + + return true; +} + +auto GenericTagParser::ReadAndParseTags(const std::string& path, TrackTags* out) + -> bool { libtags::Aux aux; aux.tags = out; if (f_stat(path.c_str(), &aux.info) != FR_OK || @@ -136,7 +170,7 @@ auto TagParserImpl::ReadAndParseTags(const std::string& path, TrackTags* out) if (res != 0) { // Parsing failed. - ESP_LOGE(kTag, "tag parsing failed, reason %d", res); + ESP_LOGE(kTag, "tag parsing for %s failed, reason %d", path.c_str(), res); return false; } @@ -172,11 +206,41 @@ auto TagParserImpl::ReadAndParseTags(const std::string& path, TrackTags* out) if (ctx.duration > 0) { out->duration = ctx.duration; } + return true; +} - { - std::lock_guard lock{cache_mutex_}; - cache_.Put(path, *out); +auto OpusTagParser::ReadAndParseTags(const std::string& path, TrackTags* out) + -> bool { + std::string vfs_path = "/sdcard" + path; + int err; + OggOpusFile* f = op_test_file(vfs_path.c_str(), &err); + if (f == NULL) { + ESP_LOGE(kTag, "opusfile tag parsing failed: %d", err); + return false; + } + const OpusTags* tags = op_tags(f, -1); + if (tags == NULL) { + ESP_LOGE(kTag, "no tags in opusfile"); + op_free(f); + return false; + } + + out->encoding(Container::kOpus); + const char* tag = NULL; + tag = opus_tags_query(tags, "TITLE", 0); + if (tag != NULL) { + out->set(Tag::kTitle, tag); + } + tag = opus_tags_query(tags, "ARTIST", 0); + if (tag != NULL) { + out->set(Tag::kArtist, tag); } + tag = opus_tags_query(tags, "ALBUM", 0); + if (tag != NULL) { + out->set(Tag::kAlbum, tag); + } + + op_free(f); return true; }