summaryrefslogtreecommitdiff
path: root/src/tangara/database/track.cpp
diff options
context:
space:
mode:
authorcooljqln <cooljqln@noreply.codeberg.org>2024-05-03 04:48:17 +0000
committercooljqln <cooljqln@noreply.codeberg.org>2024-05-03 04:48:17 +0000
commit3ceb8025ee4330c177101ed30ec17dfb0002f41e (patch)
tree58350210f15df7d00d967cac6f30eeceeb031a3c /src/tangara/database/track.cpp
parent964da15a0b84f8e5f00e8abac2f7dfda0bf60488 (diff)
parent9fafd797a5504f458b5fcae4a1d28a68da936315 (diff)
downloadtangara-fw-3ceb8025ee4330c177101ed30ec17dfb0002f41e.tar.gz
Merge pull request 'Break dependency cycles with our components by merging co-dependent components together' (#68) from jqln/component-merge into main
Reviewed-on: https://codeberg.org/cool-tech-zone/tangara-fw/pulls/68
Diffstat (limited to 'src/tangara/database/track.cpp')
-rw-r--r--src/tangara/database/track.cpp307
1 files changed, 307 insertions, 0 deletions
diff --git a/src/tangara/database/track.cpp b/src/tangara/database/track.cpp
new file mode 100644
index 00000000..5bf8c3e2
--- /dev/null
+++ b/src/tangara/database/track.cpp
@@ -0,0 +1,307 @@
+/*
+ * Copyright 2023 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#include "database/track.hpp"
+
+#include <iomanip>
+#include <iostream>
+#include <memory_resource>
+#include <span>
+#include <sstream>
+#include <string>
+
+#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<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, std::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, std::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 "";
+}
+
+auto TrackTags::create() -> std::shared_ptr<TrackTags> {
+ return std::allocate_shared<TrackTags,
+ std::pmr::polymorphic_allocator<TrackTags>>(
+ &memory::kSpiRamResource);
+}
+
+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(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::allPresent() const -> std::vector<Tag> {
+ std::vector<Tag> 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<std::pmr::string>& {
+ return title_;
+}
+
+auto TrackTags::title(std::string_view s) -> void {
+ title_ = s;
+}
+
+auto TrackTags::artist() const -> const std::optional<std::pmr::string>& {
+ return artist_;
+}
+
+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::strtol(s.data(), nullptr, 10);
+}
+
+auto TrackTags::track() const -> const std::optional<uint16_t>& {
+ 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<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 = [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);
+}
+
+auto Track::TitleOrFilename() const -> std::pmr::string {
+ auto title = tags().title();
+ if (title) {
+ return *title;
+ }
+ auto start = data().filepath.find_last_of('/');
+ if (start == std::pmr::string::npos) {
+ return data().filepath;
+ }
+ return data().filepath.substr(start + 1);
+}
+} // namespace database