/* * Copyright 2023 jacqueline * * SPDX-License-Identifier: GPL-3.0-only */ #include "database/track.hpp" #include #include #include #include #include #include #include "esp_log.h" #include "komihash.h" #include "memory_resource.hpp" namespace database { static constexpr char kGenreDelimiters[] = ",;"; auto tagName(Tag t) -> std::string { switch (t) { case Tag::kTitle: return "title"; case Tag::kArtist: return "artist"; case Tag::kAlbum: return "album"; 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"; } 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, std::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 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, std::span>) { std::ostringstream builder{}; for (const auto& str : arg) { builder << std::string{str.data(), str.size()} << ","; } return builder.str(); } }, val); return ""; } auto TrackTags::create() -> std::shared_ptr { return std::allocate_shared>( &memory::kSpiRamResource); } template auto valueOrMonostate(std::optional 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(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 derived 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::albumArtist(std::string_view s) -> void { album_artist_ = s; } auto TrackTags::disc() const -> const std::optional& { return disc_; } auto TrackTags::disc(const std::string_view s) -> void { disc_ = std::strtol(s.data(), nullptr, 10); } auto TrackTags::track() const -> const std::optional& { return track_; } auto TrackTags::track(const std::string_view s) -> void { track_ = std::strtol(s.data(), nullptr, 10); } auto TrackTags::albumOrder() const -> uint32_t { return (disc_.value_or(0) << 16) | track_.value_or(0); } auto TrackTags::genres() const -> std::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 = [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); } } } /* * 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 // tags at all. komihash_stream_t stream; komihash_stream_init(&stream, 0); 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); } } // namespace database