Introduce a MediaType for each track and index

Initially set based on filepath, or by genre if the filepath doesn't
match one of our presets
custom
jacqueline 10 months ago
parent 9c95c2b422
commit 57af3e64c8
  1. 72
      src/tangara/database/database.cpp
  2. 2
      src/tangara/database/database.hpp
  3. 14
      src/tangara/database/index.cpp
  4. 3
      src/tangara/database/index.hpp
  5. 1
      src/tangara/database/track.cpp
  6. 18
      src/tangara/database/track.hpp

@ -6,7 +6,9 @@
#include "database/database.hpp" #include "database/database.hpp"
#include <bits/ranges_algo.h>
#include <algorithm> #include <algorithm>
#include <cctype>
#include <cstdint> #include <cstdint>
#include <functional> #include <functional>
#include <iomanip> #include <iomanip>
@ -33,6 +35,7 @@
#include "leveldb/write_batch.h" #include "leveldb/write_batch.h"
#include "collation.hpp" #include "collation.hpp"
#include "database.hpp"
#include "database/db_events.hpp" #include "database/db_events.hpp"
#include "database/env_esp.hpp" #include "database/env_esp.hpp"
#include "database/index.hpp" #include "database/index.hpp"
@ -44,7 +47,6 @@
#include "memory_resource.hpp" #include "memory_resource.hpp"
#include "result.hpp" #include "result.hpp"
#include "tasks.hpp" #include "tasks.hpp"
#include "database.hpp"
namespace database { namespace database {
@ -52,7 +54,6 @@ static SingletonEnv<leveldb::EspEnv> sEnv;
[[maybe_unused]] static const char* kTag = "DB"; [[maybe_unused]] static const char* kTag = "DB";
static const char kDbPath[] = "/.tangara-db"; static const char kDbPath[] = "/.tangara-db";
static const char kMusicPath[] = "Music";
static const char kKeyDbVersion[] = "schema_version"; static const char kKeyDbVersion[] = "schema_version";
static const char kKeyCustom[] = "U\0"; static const char kKeyCustom[] = "U\0";
@ -281,7 +282,7 @@ auto Database::getTrackID(std::string path) -> std::optional<TrackId> {
auto Database::setTrackData(TrackId id, const TrackData& data) -> void { auto Database::setTrackData(TrackId id, const TrackData& data) -> void {
std::string key = EncodeDataKey(id); std::string key = EncodeDataKey(id);
std::string raw_val = EncodeDataValue(data); std::string raw_val = EncodeDataValue(data);
auto res = db_->Put(leveldb::WriteOptions(), key, raw_val); auto res = db_->Put(leveldb::WriteOptions(), key, raw_val);
if (!res.ok()) { if (!res.ok()) {
ESP_LOGI(kTag, "Updating track data failed for track ID: %lu", id); ESP_LOGI(kTag, "Updating track data failed for track ID: %lu", id);
@ -292,10 +293,7 @@ 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 { return {
kAllTracks, kAllTracks, kAllAlbums, kAlbumsByArtist, kTracksByGenre, kPodcasts,
kAllAlbums,
kAlbumsByArtist,
kTracksByGenre,
}; };
} }
@ -414,8 +412,9 @@ auto Database::updateIndexes() -> void {
// At this point, we know that the track still exists in its original // 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. // 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(); 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 // This track's tags have changed. Since the filepath is exactly the
// same, we assume this is a legitimate correction. Update the // same, we assume this is a legitimate correction. Update the
// database. // database.
@ -431,6 +430,7 @@ auto Database::updateIndexes() -> void {
track->tags_hash = new_hash; track->tags_hash = new_hash;
dbIngestTagHashes(*tags, track->individual_tag_hashes, batch); dbIngestTagHashes(*tags, track->individual_tag_hashes, batch);
track->type = new_type;
dbCreateIndexesForTrack(*track, *tags, batch); dbCreateIndexesForTrack(*track, *tags, batch);
batch.Put(EncodeDataKey(track->id), EncodeDataValue(*track)); batch.Put(EncodeDataKey(track->id), EncodeDataValue(*track));
batch.Put(EncodeHashKey(new_hash), EncodeHashValue(track->id)); batch.Put(EncodeHashKey(new_hash), EncodeHashValue(track->id));
@ -442,14 +442,8 @@ auto Database::updateIndexes() -> void {
update_tracker_->onVerificationFinished(); update_tracker_->onVerificationFinished();
// Stage 2: search for newly added files. // Stage 2: search for newly added files.
std::string root; ESP_LOGI(kTag, "scanning for new tracks");
FF_DIR dir; track_finder_.launch("");
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);
}; };
auto Database::processCandidateCallback(FILINFO& info, std::string_view path) 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->tags_hash = hash;
data->modified_at = {info.fdate, info.ftime}; data->modified_at = {info.fdate, info.ftime};
data->is_tombstoned = false; data->is_tombstoned = false;
data->type = calculateMediaType(*tags, path);
// Apply all the actual database changes as one atomic batch. This makes // Apply all the actual database changes as one atomic batch. This makes
// the whole 'new track' operation atomic, and also reduces the amount of // the whole 'new track' operation atomic, and also reduces the amount of
@ -531,6 +526,51 @@ auto Database::isUpdating() -> bool {
return is_updating_; 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 { auto Database::dbCalculateNextTrackId() -> void {
std::unique_ptr<leveldb::Iterator> it{ std::unique_ptr<leveldb::Iterator> it{
db_->NewIterator(leveldb::ReadOptions())}; db_->NewIterator(leveldb::ReadOptions())};

@ -14,6 +14,7 @@
#include <optional> #include <optional>
#include <stack> #include <stack>
#include <string> #include <string>
#include <string_view>
#include <utility> #include <utility>
#include <vector> #include <vector>
@ -134,6 +135,7 @@ class Database {
auto processCandidateCallback(FILINFO&, std::string_view) -> void; auto processCandidateCallback(FILINFO&, std::string_view) -> void;
auto indexingCompleteCallback() -> void; auto indexingCompleteCallback() -> void;
auto calculateMediaType(TrackTags&, std::string_view) -> MediaType;
auto dbCalculateNextTrackId() -> void; auto dbCalculateNextTrackId() -> void;
auto dbMintNewTrackId() -> TrackId; auto dbMintNewTrackId() -> TrackId;

@ -30,28 +30,39 @@ namespace database {
const IndexInfo kAlbumsByArtist{ const IndexInfo kAlbumsByArtist{
.id = 1, .id = 1,
.type = MediaType::kMusic,
.name = "Albums by Artist", .name = "Albums by Artist",
.components = {Tag::kAlbumArtist, Tag::kAlbum, Tag::kAlbumOrder}, .components = {Tag::kAlbumArtist, Tag::kAlbum, Tag::kAlbumOrder},
}; };
const IndexInfo kTracksByGenre{ const IndexInfo kTracksByGenre{
.id = 2, .id = 2,
.type = MediaType::kMusic,
.name = "Tracks by Genre", .name = "Tracks by Genre",
.components = {Tag::kGenres, Tag::kTitle}, .components = {Tag::kGenres, Tag::kTitle},
}; };
const IndexInfo kAllTracks{ const IndexInfo kAllTracks{
.id = 3, .id = 3,
.type = MediaType::kMusic,
.name = "All Tracks", .name = "All Tracks",
.components = {Tag::kTitle}, .components = {Tag::kTitle},
}; };
const IndexInfo kAllAlbums{ const IndexInfo kAllAlbums{
.id = 4, .id = 4,
.type = MediaType::kMusic,
.name = "All Albums", .name = "All Albums",
.components = {Tag::kAlbum, Tag::kAlbumOrder}, .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) static auto titleOrFilename(const TrackData& data, const TrackTags& tags)
-> std::pmr::string { -> std::pmr::string {
auto title = tags.title(); auto title = tags.title();
@ -203,6 +214,9 @@ auto Index(locale::ICollator& collator,
const TrackData& data, const TrackData& data,
const TrackTags& tags) const TrackTags& tags)
-> std::vector<std::pair<IndexKey, std::string>> { -> std::vector<std::pair<IndexKey, std::string>> {
if (index.type != data.type) {
return {};
}
Indexer indexer{collator, index, data, tags}; Indexer indexer{collator, index, data, tags};
return indexer.index(); return indexer.index();
} }

@ -28,6 +28,8 @@ typedef uint8_t IndexId;
struct IndexInfo { struct IndexInfo {
// Unique id for this index // Unique id for this index
IndexId id; 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" // Localised, user-friendly description of this index. e.g. "Albums by Artist"
// or "All Tracks". // or "All Tracks".
std::pmr::string name; std::pmr::string name;
@ -76,5 +78,6 @@ 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 kPodcasts;
} // namespace database } // namespace database

@ -302,6 +302,7 @@ auto database::TrackData::clone() const -> std::shared_ptr<TrackData> {
data->is_tombstoned = is_tombstoned; data->is_tombstoned = is_tombstoned;
data->modified_at = modified_at; data->modified_at = modified_at;
data->last_position = last_position; data->last_position = last_position;
data->type = type;
return data; return data;
} }

@ -47,6 +47,23 @@ enum class Container {
kOpus = 5, 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 { enum class Tag {
kTitle = 0, kTitle = 0,
kArtist = 1, kArtist = 1,
@ -169,6 +186,7 @@ struct TrackData {
bool is_tombstoned; bool is_tombstoned;
std::pair<uint16_t, uint16_t> modified_at; std::pair<uint16_t, uint16_t> modified_at;
uint32_t last_position; uint32_t last_position;
MediaType type;
TrackData(const TrackData&& other) = delete; TrackData(const TrackData&& other) = delete;
TrackData& operator=(TrackData& other) = delete; TrackData& operator=(TrackData& other) = delete;

Loading…
Cancel
Save