Merge pull request 'Add a 'MediaType' enum to TrackData and IndexInfo, plus a new index for podcasts' (#105) from jqln/track-type into main
Reviewed-on: https://codeberg.org/cool-tech-zone/tangara-fw/pulls/105
This commit is contained in:
@@ -6,7 +6,9 @@
|
||||
|
||||
#include "database/database.hpp"
|
||||
|
||||
#include <bits/ranges_algo.h>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <iomanip>
|
||||
@@ -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<leveldb::EspEnv> 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<TrackId> {
|
||||
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<IndexInfo> {
|
||||
// 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<leveldb::Iterator> it{
|
||||
db_->NewIterator(leveldb::ReadOptions())};
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <optional>
|
||||
#include <stack>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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::kTitle},
|
||||
};
|
||||
|
||||
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<std::pair<IndexKey, std::string>> {
|
||||
if (index.type != data.type) {
|
||||
return {};
|
||||
}
|
||||
Indexer indexer{collator, index, data, tags};
|
||||
return indexer.index();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -94,6 +94,7 @@ auto EncodeDataValue(const TrackData& track) -> std::string {
|
||||
cppbor::Uint{track.modified_at.second},
|
||||
tag_hashes,
|
||||
cppbor::Uint{track.last_position},
|
||||
cppbor::Uint{static_cast<unsigned int>(track.type)},
|
||||
};
|
||||
return val.toString();
|
||||
}
|
||||
@@ -105,13 +106,13 @@ auto ParseDataValue(const leveldb::Slice& slice) -> std::shared_ptr<TrackData> {
|
||||
return nullptr;
|
||||
}
|
||||
auto vals = item->asArray();
|
||||
if (vals->size() != 8 || vals->get(0)->type() != cppbor::UINT ||
|
||||
if (vals->size() < 8 || vals->get(0)->type() != cppbor::UINT ||
|
||||
vals->get(1)->type() != cppbor::TSTR ||
|
||||
vals->get(2)->type() != cppbor::UINT ||
|
||||
vals->get(3)->type() != cppbor::SIMPLE ||
|
||||
vals->get(4)->type() != cppbor::UINT ||
|
||||
vals->get(5)->type() != cppbor::UINT ||
|
||||
vals->get(6)->type() != cppbor::MAP ||
|
||||
vals->get(6)->type() != cppbor::MAP ||
|
||||
vals->get(7)->type() != cppbor::UINT) {
|
||||
return {};
|
||||
}
|
||||
@@ -132,6 +133,20 @@ auto ParseDataValue(const leveldb::Slice& slice) -> std::shared_ptr<TrackData> {
|
||||
|
||||
res->last_position = vals->get(7)->asUint()->unsignedValue();
|
||||
|
||||
if (vals->size() >= 9 && vals->get(8)->type() == cppbor::UINT) {
|
||||
auto val = vals->get(8)->asUint()->unsignedValue();
|
||||
switch (val) {
|
||||
case 1:
|
||||
case 2:
|
||||
case 3:
|
||||
res->type = static_cast<MediaType>(val);
|
||||
break;
|
||||
case 0:
|
||||
default:
|
||||
res->type = MediaType::kUnknown;
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
@@ -302,6 +302,7 @@ auto database::TrackData::clone() const -> std::shared_ptr<TrackData> {
|
||||
data->is_tombstoned = is_tombstoned;
|
||||
data->modified_at = modified_at;
|
||||
data->last_position = last_position;
|
||||
data->type = type;
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<uint16_t, uint16_t> modified_at;
|
||||
uint32_t last_position;
|
||||
MediaType type;
|
||||
|
||||
TrackData(const TrackData&& other) = delete;
|
||||
TrackData& operator=(TrackData& other) = delete;
|
||||
|
||||
Reference in New Issue
Block a user