diff --git a/src/tangara/database/database.cpp b/src/tangara/database/database.cpp index 9d0de695..c45f3862 100644 --- a/src/tangara/database/database.cpp +++ b/src/tangara/database/database.cpp @@ -6,7 +6,9 @@ #include "database/database.hpp" +#include #include +#include #include #include #include @@ -33,6 +35,7 @@ #include "leveldb/write_batch.h" #include "collation.hpp" +#include "database.hpp" #include "database/db_events.hpp" #include "database/env_esp.hpp" #include "database/index.hpp" @@ -44,7 +47,6 @@ #include "memory_resource.hpp" #include "result.hpp" #include "tasks.hpp" -#include "database.hpp" namespace database { @@ -52,7 +54,6 @@ static SingletonEnv sEnv; [[maybe_unused]] static const char* kTag = "DB"; static const char kDbPath[] = "/.tangara-db"; -static const char kMusicPath[] = "Music"; static const char kKeyDbVersion[] = "schema_version"; static const char kKeyCustom[] = "U\0"; @@ -281,7 +282,7 @@ auto Database::getTrackID(std::string path) -> std::optional { auto Database::setTrackData(TrackId id, const TrackData& data) -> void { std::string key = EncodeDataKey(id); std::string raw_val = EncodeDataValue(data); - + auto res = db_->Put(leveldb::WriteOptions(), key, raw_val); if (!res.ok()) { ESP_LOGI(kTag, "Updating track data failed for track ID: %lu", id); @@ -292,10 +293,7 @@ auto Database::getIndexes() -> std::vector { // TODO(jacqueline): This probably needs to be async? When we have runtime // configurable indexes, they will need to come from somewhere. return { - kAllTracks, - kAllAlbums, - kAlbumsByArtist, - kTracksByGenre, + kAllTracks, kAllAlbums, kAlbumsByArtist, kTracksByGenre, kPodcasts, }; } @@ -414,8 +412,9 @@ auto Database::updateIndexes() -> void { // At this point, we know that the track still exists in its original // location. All that's left to do is update any metadata about it. + auto new_type = calculateMediaType(*tags, track->filepath); uint64_t new_hash = tags->Hash(); - if (new_hash != track->tags_hash) { + if (new_hash != track->tags_hash || new_type != track->type) { // This track's tags have changed. Since the filepath is exactly the // same, we assume this is a legitimate correction. Update the // database. @@ -431,6 +430,7 @@ auto Database::updateIndexes() -> void { track->tags_hash = new_hash; dbIngestTagHashes(*tags, track->individual_tag_hashes, batch); + track->type = new_type; dbCreateIndexesForTrack(*track, *tags, batch); batch.Put(EncodeDataKey(track->id), EncodeDataValue(*track)); batch.Put(EncodeHashKey(new_hash), EncodeHashValue(track->id)); @@ -442,14 +442,8 @@ auto Database::updateIndexes() -> void { update_tracker_->onVerificationFinished(); // Stage 2: search for newly added files. - std::string root; - FF_DIR dir; - if (f_opendir(&dir, kMusicPath) == FR_OK) { - f_closedir(&dir); - root = kMusicPath; - } - ESP_LOGI(kTag, "scanning for new tracks in '%s'", root.c_str()); - track_finder_.launch(root); + ESP_LOGI(kTag, "scanning for new tracks"); + track_finder_.launch(""); }; auto Database::processCandidateCallback(FILINFO& info, std::string_view path) @@ -507,6 +501,7 @@ auto Database::processCandidateCallback(FILINFO& info, std::string_view path) data->tags_hash = hash; data->modified_at = {info.fdate, info.ftime}; data->is_tombstoned = false; + data->type = calculateMediaType(*tags, path); // Apply all the actual database changes as one atomic batch. This makes // the whole 'new track' operation atomic, and also reduces the amount of @@ -531,6 +526,51 @@ auto Database::isUpdating() -> bool { return is_updating_; } +// FIXME: Make these media paths configurable. +static constexpr char kMusicMediaPath[] = "Music/"; +static constexpr char kPodcastMediaPath[] = "Podcasts/"; +static constexpr char kAudiobookMediaPath[] = "Audiobooks/"; + +auto Database::calculateMediaType(TrackTags& tags, std::string_view path) + -> MediaType { + // Use the filepath first, since it's the most explicit way for the user to + // tell us what this track is. + if (path.starts_with(kMusicMediaPath)) { + return MediaType::kMusic; + } + if (path.starts_with(kPodcastMediaPath)) { + return MediaType::kPodcast; + } + if (path.starts_with(kAudiobookMediaPath)) { + return MediaType::kAudiobook; + } + + // Podcasts may (rarely!) have a genre tag that tells us what they are. Look + // for it. + auto equalsIgnoreCase = [&](char lhs, char rhs) { + // NB: not really safe across languages, but genre tags tend to be in + // English anyway. + return std::tolower(lhs) == std::tolower(rhs); + }; + + auto genres = tags.genres(); + for (const auto& genre : genres) { + if (std::ranges::equal(genre, "podcast", equalsIgnoreCase)) { + return MediaType::kPodcast; + } + // FIXME: Do audiobooks have a common genre as well? + } + + // No path we recognise, no specific genre we recognise. We basically don't + // know what kind of media this track is at this point. If there's any genres + // at all, then guess that it's probably some kind of music. + if (!genres.empty()) { + return MediaType::kMusic; + } + + return MediaType::kUnknown; +} + auto Database::dbCalculateNextTrackId() -> void { std::unique_ptr it{ db_->NewIterator(leveldb::ReadOptions())}; diff --git a/src/tangara/database/database.hpp b/src/tangara/database/database.hpp index 6dd13b0d..e46a123e 100644 --- a/src/tangara/database/database.hpp +++ b/src/tangara/database/database.hpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -134,6 +135,7 @@ class Database { auto processCandidateCallback(FILINFO&, std::string_view) -> void; auto indexingCompleteCallback() -> void; + auto calculateMediaType(TrackTags&, std::string_view) -> MediaType; auto dbCalculateNextTrackId() -> void; auto dbMintNewTrackId() -> TrackId; diff --git a/src/tangara/database/index.cpp b/src/tangara/database/index.cpp index dec458f4..f1d8f258 100644 --- a/src/tangara/database/index.cpp +++ b/src/tangara/database/index.cpp @@ -30,28 +30,39 @@ namespace database { const IndexInfo kAlbumsByArtist{ .id = 1, + .type = MediaType::kMusic, .name = "Albums by Artist", .components = {Tag::kAlbumArtist, Tag::kAlbum, Tag::kAlbumOrder}, }; const IndexInfo kTracksByGenre{ .id = 2, + .type = MediaType::kMusic, .name = "Tracks by Genre", .components = {Tag::kGenres, Tag::kTitle}, }; const IndexInfo kAllTracks{ .id = 3, + .type = MediaType::kMusic, .name = "All Tracks", .components = {Tag::kTitle}, }; const IndexInfo kAllAlbums{ .id = 4, + .type = MediaType::kMusic, .name = "All Albums", .components = {Tag::kAlbum, Tag::kAlbumOrder}, }; +const IndexInfo kPodcasts{ + .id = 5, + .type = MediaType::kPodcast, + .name = "Podcasts", + .components = {Tag::kAlbum, Tag::kAlbumOrder}, +}; + static auto titleOrFilename(const TrackData& data, const TrackTags& tags) -> std::pmr::string { auto title = tags.title(); @@ -203,6 +214,9 @@ auto Index(locale::ICollator& collator, const TrackData& data, const TrackTags& tags) -> std::vector> { + if (index.type != data.type) { + return {}; + } Indexer indexer{collator, index, data, tags}; return indexer.index(); } diff --git a/src/tangara/database/index.hpp b/src/tangara/database/index.hpp index bc01ec2f..d1c10a36 100644 --- a/src/tangara/database/index.hpp +++ b/src/tangara/database/index.hpp @@ -28,6 +28,8 @@ typedef uint8_t IndexId; struct IndexInfo { // Unique id for this index IndexId id; + // What kind of media this index should be used with + MediaType type; // Localised, user-friendly description of this index. e.g. "Albums by Artist" // or "All Tracks". std::pmr::string name; @@ -76,5 +78,6 @@ extern const IndexInfo kAlbumsByArtist; extern const IndexInfo kTracksByGenre; extern const IndexInfo kAllTracks; extern const IndexInfo kAllAlbums; +extern const IndexInfo kPodcasts; } // namespace database diff --git a/src/tangara/database/track.cpp b/src/tangara/database/track.cpp index e737dd37..51d50a38 100644 --- a/src/tangara/database/track.cpp +++ b/src/tangara/database/track.cpp @@ -302,6 +302,7 @@ auto database::TrackData::clone() const -> std::shared_ptr { data->is_tombstoned = is_tombstoned; data->modified_at = modified_at; data->last_position = last_position; + data->type = type; return data; } diff --git a/src/tangara/database/track.hpp b/src/tangara/database/track.hpp index 03fc47b9..71f40910 100644 --- a/src/tangara/database/track.hpp +++ b/src/tangara/database/track.hpp @@ -47,6 +47,23 @@ enum class Container { kOpus = 5, }; +enum class MediaType { + // We don't know what this is. + kUnknown = 0, + // It's music! You know what music is. Usually short-form, but sometimes + // long-form, since this type includes everything from standalone tracks, to + // tracks within an album, to live set and event recordings. + kMusic = 1, + // Usually long usually primarily spoken content. One file per episode, and + // the tagging norms for each file are all over the place. Prefer looking up + // track tags from accompanying RSS feed files. + kPodcast = 2, + // Usually long usually primarily spoken content. One distinct piece of + // 'audiobook' media may be split across several files, with a playlist or + // cue file to tie the pieces back together. May also just be one big mp3. + kAudiobook = 3, +}; + enum class Tag { kTitle = 0, kArtist = 1, @@ -169,6 +186,7 @@ struct TrackData { bool is_tombstoned; std::pair modified_at; uint32_t last_position; + MediaType type; TrackData(const TrackData&& other) = delete; TrackData& operator=(TrackData& other) = delete;