song -> track
This commit is contained in:
@@ -121,8 +121,8 @@ void RegisterDbInit() {
|
|||||||
esp_console_cmd_register(&cmd);
|
esp_console_cmd_register(&cmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
int CmdDbSongs(int argc, char** argv) {
|
int CmdDbTracks(int argc, char** argv) {
|
||||||
static const std::string usage = "usage: db_songs";
|
static const std::string usage = "usage: db_tracks";
|
||||||
if (argc != 1) {
|
if (argc != 1) {
|
||||||
std::cout << usage << std::endl;
|
std::cout << usage << std::endl;
|
||||||
return 1;
|
return 1;
|
||||||
@@ -133,9 +133,10 @@ int CmdDbSongs(int argc, char** argv) {
|
|||||||
std::cout << "no database open" << std::endl;
|
std::cout << "no database open" << std::endl;
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
std::unique_ptr<database::Result<database::Song>> res(db->GetSongs(5).get());
|
std::unique_ptr<database::Result<database::Track>> res(
|
||||||
|
db->GetTracks(5).get());
|
||||||
while (true) {
|
while (true) {
|
||||||
for (database::Song s : res->values()) {
|
for (database::Track s : res->values()) {
|
||||||
std::cout << s.tags().title.value_or("[BLANK]") << std::endl;
|
std::cout << s.tags().title.value_or("[BLANK]") << std::endl;
|
||||||
}
|
}
|
||||||
if (res->next_page()) {
|
if (res->next_page()) {
|
||||||
@@ -149,11 +150,11 @@ int CmdDbSongs(int argc, char** argv) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void RegisterDbSongs() {
|
void RegisterDbTracks() {
|
||||||
esp_console_cmd_t cmd{.command = "db_songs",
|
esp_console_cmd_t cmd{.command = "db_tracks",
|
||||||
.help = "lists titles of ALL songs in the database",
|
.help = "lists titles of ALL tracks in the database",
|
||||||
.hint = NULL,
|
.hint = NULL,
|
||||||
.func = &CmdDbSongs,
|
.func = &CmdDbTracks,
|
||||||
.argtable = NULL};
|
.argtable = NULL};
|
||||||
esp_console_cmd_register(&cmd);
|
esp_console_cmd_register(&cmd);
|
||||||
}
|
}
|
||||||
@@ -217,7 +218,7 @@ auto AppConsole::RegisterExtraComponents() -> void {
|
|||||||
RegisterAudioStatus();
|
RegisterAudioStatus();
|
||||||
*/
|
*/
|
||||||
RegisterDbInit();
|
RegisterDbInit();
|
||||||
RegisterDbSongs();
|
RegisterDbTracks();
|
||||||
RegisterDbDump();
|
RegisterDbDump();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ std::unique_ptr<FatfsAudioInput> AudioState::sFileSource;
|
|||||||
std::unique_ptr<I2SAudioOutput> AudioState::sI2SOutput;
|
std::unique_ptr<I2SAudioOutput> AudioState::sI2SOutput;
|
||||||
std::vector<std::unique_ptr<IAudioElement>> AudioState::sPipeline;
|
std::vector<std::unique_ptr<IAudioElement>> AudioState::sPipeline;
|
||||||
|
|
||||||
std::deque<AudioState::EnqueuedItem> AudioState::sSongQueue;
|
std::deque<AudioState::EnqueuedItem> AudioState::sTrackQueue;
|
||||||
|
|
||||||
auto AudioState::Init(drivers::GpioExpander* gpio_expander,
|
auto AudioState::Init(drivers::GpioExpander* gpio_expander,
|
||||||
std::weak_ptr<database::Database> database) -> bool {
|
std::weak_ptr<database::Database> database) -> bool {
|
||||||
@@ -83,11 +83,11 @@ void Playback::exit() {
|
|||||||
|
|
||||||
void Playback::react(const InputFileFinished& ev) {
|
void Playback::react(const InputFileFinished& ev) {
|
||||||
ESP_LOGI(kTag, "finished file");
|
ESP_LOGI(kTag, "finished file");
|
||||||
if (sSongQueue.empty()) {
|
if (sTrackQueue.empty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
EnqueuedItem next_item = sSongQueue.front();
|
EnqueuedItem next_item = sTrackQueue.front();
|
||||||
sSongQueue.pop_front();
|
sTrackQueue.pop_front();
|
||||||
|
|
||||||
if (std::holds_alternative<std::string>(next_item)) {
|
if (std::holds_alternative<std::string>(next_item)) {
|
||||||
sFileSource->OpenFile(std::get<std::string>(next_item));
|
sFileSource->OpenFile(std::get<std::string>(next_item));
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ namespace task {
|
|||||||
static const char* kTag = "task";
|
static const char* kTag = "task";
|
||||||
|
|
||||||
// The default amount of time to wait between pipeline iterations for a single
|
// The default amount of time to wait between pipeline iterations for a single
|
||||||
// song.
|
// track.
|
||||||
static constexpr uint_fast16_t kDefaultDelayTicks = pdMS_TO_TICKS(5);
|
static constexpr uint_fast16_t kDefaultDelayTicks = pdMS_TO_TICKS(5);
|
||||||
static constexpr uint_fast16_t kMaxDelayTicks = pdMS_TO_TICKS(10);
|
static constexpr uint_fast16_t kMaxDelayTicks = pdMS_TO_TICKS(10);
|
||||||
static constexpr uint_fast16_t kMinDelayTicks = pdMS_TO_TICKS(1);
|
static constexpr uint_fast16_t kMinDelayTicks = pdMS_TO_TICKS(1);
|
||||||
@@ -54,7 +54,7 @@ void AudioTaskMain(std::unique_ptr<Pipeline> pipeline, IAudioSink* sink) {
|
|||||||
// The stream format for bytes currently in the sink buffer.
|
// The stream format for bytes currently in the sink buffer.
|
||||||
std::optional<StreamInfo::Format> output_format;
|
std::optional<StreamInfo::Format> output_format;
|
||||||
|
|
||||||
// How long to wait between pipeline iterations. This is reset for each song,
|
// How long to wait between pipeline iterations. This is reset for each track,
|
||||||
// and readjusted on the fly to maintain a reasonable amount playback buffer.
|
// and readjusted on the fly to maintain a reasonable amount playback buffer.
|
||||||
// Buffering too much will mean we process samples inefficiently, wasting CPU
|
// Buffering too much will mean we process samples inefficiently, wasting CPU
|
||||||
// time, whilst buffering too little will affect the quality of the output.
|
// time, whilst buffering too little will affect the quality of the output.
|
||||||
|
|||||||
@@ -24,12 +24,12 @@
|
|||||||
|
|
||||||
#include "audio_element.hpp"
|
#include "audio_element.hpp"
|
||||||
#include "chunk.hpp"
|
#include "chunk.hpp"
|
||||||
#include "song.hpp"
|
|
||||||
#include "stream_buffer.hpp"
|
#include "stream_buffer.hpp"
|
||||||
#include "stream_event.hpp"
|
#include "stream_event.hpp"
|
||||||
#include "stream_info.hpp"
|
#include "stream_info.hpp"
|
||||||
#include "stream_message.hpp"
|
#include "stream_message.hpp"
|
||||||
#include "tag_parser.hpp"
|
#include "tag_parser.hpp"
|
||||||
|
#include "track.hpp"
|
||||||
#include "types.hpp"
|
#include "types.hpp"
|
||||||
|
|
||||||
static const char* kTag = "SRC";
|
static const char* kTag = "SRC";
|
||||||
@@ -53,7 +53,7 @@ auto FatfsAudioInput::OpenFile(const std::string& path) -> bool {
|
|||||||
ESP_LOGI(kTag, "opening file %s", path.c_str());
|
ESP_LOGI(kTag, "opening file %s", path.c_str());
|
||||||
|
|
||||||
database::TagParserImpl tag_parser;
|
database::TagParserImpl tag_parser;
|
||||||
database::SongTags tags;
|
database::TrackTags tags;
|
||||||
if (!tag_parser.ReadAndParseTags(path, &tags)) {
|
if (!tag_parser.ReadAndParseTags(path, &tags)) {
|
||||||
ESP_LOGE(kTag, "failed to read tags");
|
ESP_LOGE(kTag, "failed to read tags");
|
||||||
tags.encoding = database::Encoding::kFlac;
|
tags.encoding = database::Encoding::kFlac;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
#include "tinyfsm.hpp"
|
#include "tinyfsm.hpp"
|
||||||
|
|
||||||
#include "song.hpp"
|
#include "track.hpp"
|
||||||
|
|
||||||
namespace audio {
|
namespace audio {
|
||||||
|
|
||||||
@@ -18,10 +18,10 @@ struct PlayFile : tinyfsm::Event {
|
|||||||
std::string filename;
|
std::string filename;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct PlaySong : tinyfsm::Event {
|
struct PlayTrack : tinyfsm::Event {
|
||||||
database::SongId id;
|
database::TrackId id;
|
||||||
std::optional<database::SongData> data;
|
std::optional<database::TrackData> data;
|
||||||
std::optional<database::SongTags> tags;
|
std::optional<database::TrackTags> tags;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct InputFileFinished : tinyfsm::Event {};
|
struct InputFileFinished : tinyfsm::Event {};
|
||||||
|
|||||||
@@ -17,9 +17,9 @@
|
|||||||
#include "gpio_expander.hpp"
|
#include "gpio_expander.hpp"
|
||||||
#include "i2s_audio_output.hpp"
|
#include "i2s_audio_output.hpp"
|
||||||
#include "i2s_dac.hpp"
|
#include "i2s_dac.hpp"
|
||||||
#include "song.hpp"
|
|
||||||
#include "storage.hpp"
|
#include "storage.hpp"
|
||||||
#include "tinyfsm.hpp"
|
#include "tinyfsm.hpp"
|
||||||
|
#include "track.hpp"
|
||||||
|
|
||||||
#include "system_events.hpp"
|
#include "system_events.hpp"
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ class AudioState : public tinyfsm::Fsm<AudioState> {
|
|||||||
void react(const tinyfsm::Event& ev) {}
|
void react(const tinyfsm::Event& ev) {}
|
||||||
|
|
||||||
virtual void react(const system_fsm::BootComplete&) {}
|
virtual void react(const system_fsm::BootComplete&) {}
|
||||||
virtual void react(const PlaySong&) {}
|
virtual void react(const PlayTrack&) {}
|
||||||
virtual void react(const PlayFile&) {}
|
virtual void react(const PlayFile&) {}
|
||||||
|
|
||||||
virtual void react(const InputFileFinished&) {}
|
virtual void react(const InputFileFinished&) {}
|
||||||
@@ -55,8 +55,8 @@ class AudioState : public tinyfsm::Fsm<AudioState> {
|
|||||||
static std::unique_ptr<I2SAudioOutput> sI2SOutput;
|
static std::unique_ptr<I2SAudioOutput> sI2SOutput;
|
||||||
static std::vector<std::unique_ptr<IAudioElement>> sPipeline;
|
static std::vector<std::unique_ptr<IAudioElement>> sPipeline;
|
||||||
|
|
||||||
typedef std::variant<database::SongId, std::string> EnqueuedItem;
|
typedef std::variant<database::TrackId, std::string> EnqueuedItem;
|
||||||
static std::deque<EnqueuedItem> sSongQueue;
|
static std::deque<EnqueuedItem> sTrackQueue;
|
||||||
};
|
};
|
||||||
|
|
||||||
namespace states {
|
namespace states {
|
||||||
@@ -69,7 +69,7 @@ class Uninitialised : public AudioState {
|
|||||||
|
|
||||||
class Standby : public AudioState {
|
class Standby : public AudioState {
|
||||||
public:
|
public:
|
||||||
void react(const PlaySong&) override {}
|
void react(const PlayTrack&) override {}
|
||||||
void react(const PlayFile&) override;
|
void react(const PlayFile&) override;
|
||||||
using AudioState::react;
|
using AudioState::react;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,8 +18,8 @@
|
|||||||
#include "ff.h"
|
#include "ff.h"
|
||||||
#include "freertos/message_buffer.h"
|
#include "freertos/message_buffer.h"
|
||||||
#include "freertos/queue.h"
|
#include "freertos/queue.h"
|
||||||
#include "song.hpp"
|
|
||||||
#include "span.hpp"
|
#include "span.hpp"
|
||||||
|
#include "track.hpp"
|
||||||
|
|
||||||
#include "audio_element.hpp"
|
#include "audio_element.hpp"
|
||||||
#include "stream_buffer.hpp"
|
#include "stream_buffer.hpp"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-only
|
# SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
idf_component_register(
|
idf_component_register(
|
||||||
SRCS "env_esp.cpp" "database.cpp" "song.cpp" "records.cpp" "file_gatherer.cpp" "tag_parser.cpp"
|
SRCS "env_esp.cpp" "database.cpp" "track.cpp" "records.cpp" "file_gatherer.cpp" "tag_parser.cpp"
|
||||||
INCLUDE_DIRS "include"
|
INCLUDE_DIRS "include"
|
||||||
REQUIRES "result" "span" "esp_psram" "fatfs" "libtags" "komihash" "cbor" "tasks")
|
REQUIRES "result" "span" "esp_psram" "fatfs" "libtags" "komihash" "cbor" "tasks")
|
||||||
|
|
||||||
|
|||||||
+57
-57
@@ -28,16 +28,16 @@
|
|||||||
#include "file_gatherer.hpp"
|
#include "file_gatherer.hpp"
|
||||||
#include "records.hpp"
|
#include "records.hpp"
|
||||||
#include "result.hpp"
|
#include "result.hpp"
|
||||||
#include "song.hpp"
|
|
||||||
#include "tag_parser.hpp"
|
#include "tag_parser.hpp"
|
||||||
#include "tasks.hpp"
|
#include "tasks.hpp"
|
||||||
|
#include "track.hpp"
|
||||||
|
|
||||||
namespace database {
|
namespace database {
|
||||||
|
|
||||||
static SingletonEnv<leveldb::EspEnv> sEnv;
|
static SingletonEnv<leveldb::EspEnv> sEnv;
|
||||||
static const char* kTag = "DB";
|
static const char* kTag = "DB";
|
||||||
|
|
||||||
static const char kSongIdKey[] = "next_song_id";
|
static const char kTrackIdKey[] = "next_track_id";
|
||||||
|
|
||||||
static std::atomic<bool> sIsDbOpen(false);
|
static std::atomic<bool> sIsDbOpen(false);
|
||||||
|
|
||||||
@@ -128,8 +128,8 @@ Database::~Database() {
|
|||||||
|
|
||||||
auto Database::Update() -> std::future<void> {
|
auto Database::Update() -> std::future<void> {
|
||||||
return worker_task_->Dispatch<void>([&]() -> void {
|
return worker_task_->Dispatch<void>([&]() -> void {
|
||||||
// Stage 1: verify all existing songs are still valid.
|
// Stage 1: verify all existing tracks are still valid.
|
||||||
ESP_LOGI(kTag, "verifying existing songs");
|
ESP_LOGI(kTag, "verifying existing tracks");
|
||||||
const leveldb::Snapshot* snapshot = db_->GetSnapshot();
|
const leveldb::Snapshot* snapshot = db_->GetSnapshot();
|
||||||
leveldb::ReadOptions read_options;
|
leveldb::ReadOptions read_options;
|
||||||
read_options.fill_cache = false;
|
read_options.fill_cache = false;
|
||||||
@@ -138,8 +138,8 @@ auto Database::Update() -> std::future<void> {
|
|||||||
OwningSlice prefix = CreateDataPrefix();
|
OwningSlice prefix = CreateDataPrefix();
|
||||||
it->Seek(prefix.slice);
|
it->Seek(prefix.slice);
|
||||||
while (it->Valid() && it->key().starts_with(prefix.slice)) {
|
while (it->Valid() && it->key().starts_with(prefix.slice)) {
|
||||||
std::optional<SongData> song = ParseDataValue(it->value());
|
std::optional<TrackData> track = ParseDataValue(it->value());
|
||||||
if (!song) {
|
if (!track) {
|
||||||
// The value was malformed. Drop this record.
|
// The value was malformed. Drop this record.
|
||||||
ESP_LOGW(kTag, "dropping malformed metadata");
|
ESP_LOGW(kTag, "dropping malformed metadata");
|
||||||
db_->Delete(leveldb::WriteOptions(), it->key());
|
db_->Delete(leveldb::WriteOptions(), it->key());
|
||||||
@@ -147,33 +147,33 @@ auto Database::Update() -> std::future<void> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (song->is_tombstoned()) {
|
if (track->is_tombstoned()) {
|
||||||
ESP_LOGW(kTag, "skipping tombstoned %lx", song->id());
|
ESP_LOGW(kTag, "skipping tombstoned %lx", track->id());
|
||||||
it->Next();
|
it->Next();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
SongTags tags;
|
TrackTags tags;
|
||||||
if (!tag_parser_->ReadAndParseTags(song->filepath(), &tags) ||
|
if (!tag_parser_->ReadAndParseTags(track->filepath(), &tags) ||
|
||||||
tags.encoding == Encoding::kUnsupported) {
|
tags.encoding == Encoding::kUnsupported) {
|
||||||
// We couldn't read the tags for this song. Either they were
|
// We couldn't read the tags for this track. Either they were
|
||||||
// malformed, or perhaps the file is missing. Either way, tombstone
|
// malformed, or perhaps the file is missing. Either way, tombstone
|
||||||
// this record.
|
// this record.
|
||||||
ESP_LOGW(kTag, "entombing missing #%lx", song->id());
|
ESP_LOGW(kTag, "entombing missing #%lx", track->id());
|
||||||
dbPutSongData(song->Entomb());
|
dbPutTrackData(track->Entomb());
|
||||||
it->Next();
|
it->Next();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint64_t new_hash = tags.Hash();
|
uint64_t new_hash = tags.Hash();
|
||||||
if (new_hash != song->tags_hash()) {
|
if (new_hash != track->tags_hash()) {
|
||||||
// This song'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.
|
||||||
ESP_LOGI(kTag, "updating hash (%llx -> %llx)", song->tags_hash(),
|
ESP_LOGI(kTag, "updating hash (%llx -> %llx)", track->tags_hash(),
|
||||||
new_hash);
|
new_hash);
|
||||||
dbPutSongData(song->UpdateHash(new_hash));
|
dbPutTrackData(track->UpdateHash(new_hash));
|
||||||
dbPutHash(new_hash, song->id());
|
dbPutHash(new_hash, track->id());
|
||||||
}
|
}
|
||||||
|
|
||||||
it->Next();
|
it->Next();
|
||||||
@@ -182,9 +182,9 @@ auto Database::Update() -> std::future<void> {
|
|||||||
db_->ReleaseSnapshot(snapshot);
|
db_->ReleaseSnapshot(snapshot);
|
||||||
|
|
||||||
// Stage 2: search for newly added files.
|
// Stage 2: search for newly added files.
|
||||||
ESP_LOGI(kTag, "scanning for new songs");
|
ESP_LOGI(kTag, "scanning for new tracks");
|
||||||
file_gatherer_->FindFiles("", [&](const std::string& path) {
|
file_gatherer_->FindFiles("", [&](const std::string& path) {
|
||||||
SongTags tags;
|
TrackTags tags;
|
||||||
if (!tag_parser_->ReadAndParseTags(path, &tags) ||
|
if (!tag_parser_->ReadAndParseTags(path, &tags) ||
|
||||||
tags.encoding == Encoding::kUnsupported) {
|
tags.encoding == Encoding::kUnsupported) {
|
||||||
// No parseable tags; skip this fiile.
|
// No parseable tags; skip this fiile.
|
||||||
@@ -194,32 +194,32 @@ auto Database::Update() -> std::future<void> {
|
|||||||
// Check for any existing record with the same hash.
|
// Check for any existing record with the same hash.
|
||||||
uint64_t hash = tags.Hash();
|
uint64_t hash = tags.Hash();
|
||||||
OwningSlice key = CreateHashKey(hash);
|
OwningSlice key = CreateHashKey(hash);
|
||||||
std::optional<SongId> existing_hash;
|
std::optional<TrackId> existing_hash;
|
||||||
std::string raw_entry;
|
std::string raw_entry;
|
||||||
if (db_->Get(leveldb::ReadOptions(), key.slice, &raw_entry).ok()) {
|
if (db_->Get(leveldb::ReadOptions(), key.slice, &raw_entry).ok()) {
|
||||||
existing_hash = ParseHashValue(raw_entry);
|
existing_hash = ParseHashValue(raw_entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!existing_hash) {
|
if (!existing_hash) {
|
||||||
// We've never met this song before! Or we have, but the entry is
|
// We've never met this track before! Or we have, but the entry is
|
||||||
// malformed. Either way, record this as a new song.
|
// malformed. Either way, record this as a new track.
|
||||||
SongId id = dbMintNewSongId();
|
TrackId id = dbMintNewTrackId();
|
||||||
ESP_LOGI(kTag, "recording new 0x%lx", id);
|
ESP_LOGI(kTag, "recording new 0x%lx", id);
|
||||||
dbPutSong(id, path, hash);
|
dbPutTrack(id, path, hash);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<SongData> existing_data = dbGetSongData(*existing_hash);
|
std::optional<TrackData> existing_data = dbGetTrackData(*existing_hash);
|
||||||
if (!existing_data) {
|
if (!existing_data) {
|
||||||
// We found a hash that matches, but there's no data record? Weird.
|
// We found a hash that matches, but there's no data record? Weird.
|
||||||
SongData new_data(*existing_hash, path, hash);
|
TrackData new_data(*existing_hash, path, hash);
|
||||||
dbPutSongData(new_data);
|
dbPutTrackData(new_data);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existing_data->is_tombstoned()) {
|
if (existing_data->is_tombstoned()) {
|
||||||
ESP_LOGI(kTag, "exhuming song %lu", existing_data->id());
|
ESP_LOGI(kTag, "exhuming track %lu", existing_data->id());
|
||||||
dbPutSongData(existing_data->Exhume(path));
|
dbPutTrackData(existing_data->Exhume(path));
|
||||||
} else if (existing_data->filepath() != path) {
|
} else if (existing_data->filepath() != path) {
|
||||||
ESP_LOGW(kTag, "tag hash collision");
|
ESP_LOGW(kTag, "tag hash collision");
|
||||||
}
|
}
|
||||||
@@ -227,9 +227,9 @@ auto Database::Update() -> std::future<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
auto Database::GetSongs(std::size_t page_size) -> std::future<Result<Song>*> {
|
auto Database::GetTracks(std::size_t page_size) -> std::future<Result<Track>*> {
|
||||||
return worker_task_->Dispatch<Result<Song>*>([=, this]() -> Result<Song>* {
|
return worker_task_->Dispatch<Result<Track>*>([=, this]() -> Result<Track>* {
|
||||||
Continuation<Song> c{.iterator = nullptr,
|
Continuation<Track> c{.iterator = nullptr,
|
||||||
.prefix = CreateDataPrefix().data,
|
.prefix = CreateDataPrefix().data,
|
||||||
.start_key = CreateDataPrefix().data,
|
.start_key = CreateDataPrefix().data,
|
||||||
.forward = true,
|
.forward = true,
|
||||||
@@ -260,32 +260,32 @@ auto Database::GetPage(Continuation<T>* c) -> std::future<Result<T>*> {
|
|||||||
[=, this]() -> Result<T>* { return dbGetPage(copy); });
|
[=, this]() -> Result<T>* { return dbGetPage(copy); });
|
||||||
}
|
}
|
||||||
|
|
||||||
template auto Database::GetPage<Song>(Continuation<Song>* c)
|
template auto Database::GetPage<Track>(Continuation<Track>* c)
|
||||||
-> std::future<Result<Song>*>;
|
-> std::future<Result<Track>*>;
|
||||||
template auto Database::GetPage<std::string>(Continuation<std::string>* c)
|
template auto Database::GetPage<std::string>(Continuation<std::string>* c)
|
||||||
-> std::future<Result<std::string>*>;
|
-> std::future<Result<std::string>*>;
|
||||||
|
|
||||||
auto Database::dbMintNewSongId() -> SongId {
|
auto Database::dbMintNewTrackId() -> TrackId {
|
||||||
SongId next_id = 1;
|
TrackId next_id = 1;
|
||||||
std::string val;
|
std::string val;
|
||||||
auto status = db_->Get(leveldb::ReadOptions(), kSongIdKey, &val);
|
auto status = db_->Get(leveldb::ReadOptions(), kTrackIdKey, &val);
|
||||||
if (status.ok()) {
|
if (status.ok()) {
|
||||||
next_id = BytesToSongId(val).value_or(next_id);
|
next_id = BytesToTrackId(val).value_or(next_id);
|
||||||
} else if (!status.IsNotFound()) {
|
} else if (!status.IsNotFound()) {
|
||||||
// TODO(jacqueline): Handle this more.
|
// TODO(jacqueline): Handle this more.
|
||||||
ESP_LOGE(kTag, "failed to get next song id");
|
ESP_LOGE(kTag, "failed to get next track id");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!db_->Put(leveldb::WriteOptions(), kSongIdKey,
|
if (!db_->Put(leveldb::WriteOptions(), kTrackIdKey,
|
||||||
SongIdToBytes(next_id + 1).slice)
|
TrackIdToBytes(next_id + 1).slice)
|
||||||
.ok()) {
|
.ok()) {
|
||||||
ESP_LOGE(kTag, "failed to write next song id");
|
ESP_LOGE(kTag, "failed to write next track id");
|
||||||
}
|
}
|
||||||
|
|
||||||
return next_id;
|
return next_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto Database::dbEntomb(SongId id, uint64_t hash) -> void {
|
auto Database::dbEntomb(TrackId id, uint64_t hash) -> void {
|
||||||
OwningSlice key = CreateHashKey(hash);
|
OwningSlice key = CreateHashKey(hash);
|
||||||
OwningSlice val = CreateHashValue(id);
|
OwningSlice val = CreateHashValue(id);
|
||||||
if (!db_->Put(leveldb::WriteOptions(), key.slice, val.slice).ok()) {
|
if (!db_->Put(leveldb::WriteOptions(), key.slice, val.slice).ok()) {
|
||||||
@@ -293,7 +293,7 @@ auto Database::dbEntomb(SongId id, uint64_t hash) -> void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto Database::dbPutSongData(const SongData& s) -> void {
|
auto Database::dbPutTrackData(const TrackData& s) -> void {
|
||||||
OwningSlice key = CreateDataKey(s.id());
|
OwningSlice key = CreateDataKey(s.id());
|
||||||
OwningSlice val = CreateDataValue(s);
|
OwningSlice val = CreateDataValue(s);
|
||||||
if (!db_->Put(leveldb::WriteOptions(), key.slice, val.slice).ok()) {
|
if (!db_->Put(leveldb::WriteOptions(), key.slice, val.slice).ok()) {
|
||||||
@@ -301,7 +301,7 @@ auto Database::dbPutSongData(const SongData& s) -> void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto Database::dbGetSongData(SongId id) -> std::optional<SongData> {
|
auto Database::dbGetTrackData(TrackId id) -> std::optional<TrackData> {
|
||||||
OwningSlice key = CreateDataKey(id);
|
OwningSlice key = CreateDataKey(id);
|
||||||
std::string raw_val;
|
std::string raw_val;
|
||||||
if (!db_->Get(leveldb::ReadOptions(), key.slice, &raw_val).ok()) {
|
if (!db_->Get(leveldb::ReadOptions(), key.slice, &raw_val).ok()) {
|
||||||
@@ -311,7 +311,7 @@ auto Database::dbGetSongData(SongId id) -> std::optional<SongData> {
|
|||||||
return ParseDataValue(raw_val);
|
return ParseDataValue(raw_val);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto Database::dbPutHash(const uint64_t& hash, SongId i) -> void {
|
auto Database::dbPutHash(const uint64_t& hash, TrackId i) -> void {
|
||||||
OwningSlice key = CreateHashKey(hash);
|
OwningSlice key = CreateHashKey(hash);
|
||||||
OwningSlice val = CreateHashValue(i);
|
OwningSlice val = CreateHashValue(i);
|
||||||
if (!db_->Put(leveldb::WriteOptions(), key.slice, val.slice).ok()) {
|
if (!db_->Put(leveldb::WriteOptions(), key.slice, val.slice).ok()) {
|
||||||
@@ -319,7 +319,7 @@ auto Database::dbPutHash(const uint64_t& hash, SongId i) -> void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto Database::dbGetHash(const uint64_t& hash) -> std::optional<SongId> {
|
auto Database::dbGetHash(const uint64_t& hash) -> std::optional<TrackId> {
|
||||||
OwningSlice key = CreateHashKey(hash);
|
OwningSlice key = CreateHashKey(hash);
|
||||||
std::string raw_val;
|
std::string raw_val;
|
||||||
if (!db_->Get(leveldb::ReadOptions(), key.slice, &raw_val).ok()) {
|
if (!db_->Get(leveldb::ReadOptions(), key.slice, &raw_val).ok()) {
|
||||||
@@ -329,10 +329,10 @@ auto Database::dbGetHash(const uint64_t& hash) -> std::optional<SongId> {
|
|||||||
return ParseHashValue(raw_val);
|
return ParseHashValue(raw_val);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto Database::dbPutSong(SongId id,
|
auto Database::dbPutTrack(TrackId id,
|
||||||
const std::string& path,
|
const std::string& path,
|
||||||
const uint64_t& hash) -> void {
|
const uint64_t& hash) -> void {
|
||||||
dbPutSongData(SongData(id, path, hash));
|
dbPutTrackData(TrackData(id, path, hash));
|
||||||
dbPutHash(hash, id);
|
dbPutHash(hash, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -455,24 +455,24 @@ auto Database::dbGetPage(const Continuation<T>& c) -> Result<T>* {
|
|||||||
return new Result<T>(std::move(records), next_page, prev_page);
|
return new Result<T>(std::move(records), next_page, prev_page);
|
||||||
}
|
}
|
||||||
|
|
||||||
template auto Database::dbGetPage<Song>(const Continuation<Song>& c)
|
template auto Database::dbGetPage<Track>(const Continuation<Track>& c)
|
||||||
-> Result<Song>*;
|
-> Result<Track>*;
|
||||||
template auto Database::dbGetPage<std::string>(
|
template auto Database::dbGetPage<std::string>(
|
||||||
const Continuation<std::string>& c) -> Result<std::string>*;
|
const Continuation<std::string>& c) -> Result<std::string>*;
|
||||||
|
|
||||||
template <>
|
template <>
|
||||||
auto Database::ParseRecord<Song>(const leveldb::Slice& key,
|
auto Database::ParseRecord<Track>(const leveldb::Slice& key,
|
||||||
const leveldb::Slice& val)
|
const leveldb::Slice& val)
|
||||||
-> std::optional<Song> {
|
-> std::optional<Track> {
|
||||||
std::optional<SongData> data = ParseDataValue(val);
|
std::optional<TrackData> data = ParseDataValue(val);
|
||||||
if (!data || data->is_tombstoned()) {
|
if (!data || data->is_tombstoned()) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
SongTags tags;
|
TrackTags tags;
|
||||||
if (!tag_parser_->ReadAndParseTags(data->filepath(), &tags)) {
|
if (!tag_parser_->ReadAndParseTags(data->filepath(), &tags)) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
return Song(*data, tags);
|
return Track(*data, tags);
|
||||||
}
|
}
|
||||||
|
|
||||||
template <>
|
template <>
|
||||||
|
|||||||
@@ -23,9 +23,9 @@
|
|||||||
#include "leveldb/slice.h"
|
#include "leveldb/slice.h"
|
||||||
#include "records.hpp"
|
#include "records.hpp"
|
||||||
#include "result.hpp"
|
#include "result.hpp"
|
||||||
#include "song.hpp"
|
|
||||||
#include "tag_parser.hpp"
|
#include "tag_parser.hpp"
|
||||||
#include "tasks.hpp"
|
#include "tasks.hpp"
|
||||||
|
#include "track.hpp"
|
||||||
|
|
||||||
namespace database {
|
namespace database {
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ class Database {
|
|||||||
|
|
||||||
auto Update() -> std::future<void>;
|
auto Update() -> std::future<void>;
|
||||||
|
|
||||||
auto GetSongs(std::size_t page_size) -> std::future<Result<Song>*>;
|
auto GetTracks(std::size_t page_size) -> std::future<Result<Track>*>;
|
||||||
auto GetDump(std::size_t page_size) -> std::future<Result<std::string>*>;
|
auto GetDump(std::size_t page_size) -> std::future<Result<std::string>*>;
|
||||||
|
|
||||||
template <typename T>
|
template <typename T>
|
||||||
@@ -109,14 +109,14 @@ class Database {
|
|||||||
ITagParser* tag_parser,
|
ITagParser* tag_parser,
|
||||||
std::shared_ptr<tasks::Worker> worker);
|
std::shared_ptr<tasks::Worker> worker);
|
||||||
|
|
||||||
auto dbMintNewSongId() -> SongId;
|
auto dbMintNewTrackId() -> TrackId;
|
||||||
auto dbEntomb(SongId song, uint64_t hash) -> void;
|
auto dbEntomb(TrackId track, uint64_t hash) -> void;
|
||||||
|
|
||||||
auto dbPutSongData(const SongData& s) -> void;
|
auto dbPutTrackData(const TrackData& s) -> void;
|
||||||
auto dbGetSongData(SongId id) -> std::optional<SongData>;
|
auto dbGetTrackData(TrackId id) -> std::optional<TrackData>;
|
||||||
auto dbPutHash(const uint64_t& hash, SongId i) -> void;
|
auto dbPutHash(const uint64_t& hash, TrackId i) -> void;
|
||||||
auto dbGetHash(const uint64_t& hash) -> std::optional<SongId>;
|
auto dbGetHash(const uint64_t& hash) -> std::optional<TrackId>;
|
||||||
auto dbPutSong(SongId id, const std::string& path, const uint64_t& hash)
|
auto dbPutTrack(TrackId id, const std::string& path, const uint64_t& hash)
|
||||||
-> void;
|
-> void;
|
||||||
|
|
||||||
template <typename T>
|
template <typename T>
|
||||||
@@ -128,9 +128,9 @@ class Database {
|
|||||||
};
|
};
|
||||||
|
|
||||||
template <>
|
template <>
|
||||||
auto Database::ParseRecord<Song>(const leveldb::Slice& key,
|
auto Database::ParseRecord<Track>(const leveldb::Slice& key,
|
||||||
const leveldb::Slice& val)
|
const leveldb::Slice& val)
|
||||||
-> std::optional<Song>;
|
-> std::optional<Track>;
|
||||||
template <>
|
template <>
|
||||||
auto Database::ParseRecord<std::string>(const leveldb::Slice& key,
|
auto Database::ParseRecord<std::string>(const leveldb::Slice& key,
|
||||||
const leveldb::Slice& val)
|
const leveldb::Slice& val)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
#include "leveldb/db.h"
|
#include "leveldb/db.h"
|
||||||
#include "leveldb/slice.h"
|
#include "leveldb/slice.h"
|
||||||
|
|
||||||
#include "song.hpp"
|
#include "track.hpp"
|
||||||
|
|
||||||
namespace database {
|
namespace database {
|
||||||
|
|
||||||
@@ -31,49 +31,49 @@ class OwningSlice {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Returns the prefix added to every SongData key. This can be used to iterate
|
* Returns the prefix added to every TrackData key. This can be used to iterate
|
||||||
* over every data record in the database.
|
* over every data record in the database.
|
||||||
*/
|
*/
|
||||||
auto CreateDataPrefix() -> OwningSlice;
|
auto CreateDataPrefix() -> OwningSlice;
|
||||||
|
|
||||||
/* Creates a data key for a song with the specified id. */
|
/* Creates a data key for a track with the specified id. */
|
||||||
auto CreateDataKey(const SongId& id) -> OwningSlice;
|
auto CreateDataKey(const TrackId& id) -> OwningSlice;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Encodes a SongData instance into bytes, in preparation for storing it within
|
* Encodes a TrackData instance into bytes, in preparation for storing it within
|
||||||
* the database. This encoding is consistent, and will remain stable over time.
|
* the database. This encoding is consistent, and will remain stable over time.
|
||||||
*/
|
*/
|
||||||
auto CreateDataValue(const SongData& song) -> OwningSlice;
|
auto CreateDataValue(const TrackData& track) -> OwningSlice;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Parses bytes previously encoded via CreateDataValue back into a SongData. May
|
* Parses bytes previously encoded via CreateDataValue back into a TrackData.
|
||||||
* return nullopt if parsing fails.
|
* May return nullopt if parsing fails.
|
||||||
*/
|
*/
|
||||||
auto ParseDataValue(const leveldb::Slice& slice) -> std::optional<SongData>;
|
auto ParseDataValue(const leveldb::Slice& slice) -> std::optional<TrackData>;
|
||||||
|
|
||||||
/* Creates a hash key for the specified hash. */
|
/* Creates a hash key for the specified hash. */
|
||||||
auto CreateHashKey(const uint64_t& hash) -> OwningSlice;
|
auto CreateHashKey(const uint64_t& hash) -> OwningSlice;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Encodes a hash value (at this point just a song id) into bytes, in
|
* Encodes a hash value (at this point just a track id) into bytes, in
|
||||||
* preparation for storing within the database. This encoding is consistent, and
|
* preparation for storing within the database. This encoding is consistent, and
|
||||||
* will remain stable over time.
|
* will remain stable over time.
|
||||||
*/
|
*/
|
||||||
auto CreateHashValue(SongId id) -> OwningSlice;
|
auto CreateHashValue(TrackId id) -> OwningSlice;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Parses bytes previously encoded via CreateHashValue back into a song id. May
|
* Parses bytes previously encoded via CreateHashValue back into a track id. May
|
||||||
* return nullopt if parsing fails.
|
* return nullopt if parsing fails.
|
||||||
*/
|
*/
|
||||||
auto ParseHashValue(const leveldb::Slice&) -> std::optional<SongId>;
|
auto ParseHashValue(const leveldb::Slice&) -> std::optional<TrackId>;
|
||||||
|
|
||||||
/* Encodes a SongId as bytes. */
|
/* Encodes a TrackId as bytes. */
|
||||||
auto SongIdToBytes(SongId id) -> OwningSlice;
|
auto TrackIdToBytes(TrackId id) -> OwningSlice;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Converts a song id encoded via SongIdToBytes back into a SongId. May return
|
* Converts a track id encoded via TrackIdToBytes back into a TrackId. May
|
||||||
* nullopt if parsing fails.
|
* return nullopt if parsing fails.
|
||||||
*/
|
*/
|
||||||
auto BytesToSongId(const std::string& bytes) -> std::optional<SongId>;
|
auto BytesToTrackId(const std::string& bytes) -> std::optional<TrackId>;
|
||||||
|
|
||||||
} // namespace database
|
} // namespace database
|
||||||
|
|||||||
@@ -1,166 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2023 jacqueline <me@jacqueline.id.au>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
#include <optional>
|
|
||||||
#include <string>
|
|
||||||
#include <utility>
|
|
||||||
|
|
||||||
#include "leveldb/db.h"
|
|
||||||
#include "span.hpp"
|
|
||||||
|
|
||||||
namespace database {
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Uniquely describes a single song within the database. This value will be
|
|
||||||
* consistent across database updates, and should ideally (but is not guaranteed
|
|
||||||
* to) endure even across a song being removed and re-added.
|
|
||||||
*
|
|
||||||
* Four billion songs should be enough for anybody.
|
|
||||||
*/
|
|
||||||
typedef uint32_t SongId;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Audio file encodings that we are aware of. Used to select an appropriate
|
|
||||||
* decoder at play time.
|
|
||||||
*
|
|
||||||
* Values of this enum are persisted in this database, so it is probably never a
|
|
||||||
* good idea to change the int representation of an existing value.
|
|
||||||
*/
|
|
||||||
enum class Encoding {
|
|
||||||
kUnsupported = 0,
|
|
||||||
kMp3 = 1,
|
|
||||||
kWav = 2,
|
|
||||||
kOgg = 3,
|
|
||||||
kFlac = 4,
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Owning container for tag-related song metadata that was extracted from a
|
|
||||||
* file.
|
|
||||||
*/
|
|
||||||
struct SongTags {
|
|
||||||
Encoding encoding;
|
|
||||||
std::optional<std::string> title;
|
|
||||||
|
|
||||||
// TODO(jacqueline): It would be nice to use shared_ptr's for the artist and
|
|
||||||
// album, since there's likely a fair number of duplicates for each
|
|
||||||
// (especially the former).
|
|
||||||
|
|
||||||
std::optional<std::string> artist;
|
|
||||||
std::optional<std::string> album;
|
|
||||||
|
|
||||||
std::optional<int> channels;
|
|
||||||
std::optional<int> sample_rate;
|
|
||||||
std::optional<int> bits_per_sample;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Returns a hash of the 'identifying' tags of this song. That is, a hash that
|
|
||||||
* can be used to determine if one song is likely the same as another, across
|
|
||||||
* things like re-encoding, re-mastering, or moving the underlying file.
|
|
||||||
*/
|
|
||||||
auto Hash() const -> uint64_t;
|
|
||||||
|
|
||||||
bool operator==(const SongTags&) const = default;
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Immutable owning container for all of the metadata we store for a particular
|
|
||||||
* song. This includes two main kinds of metadata:
|
|
||||||
* 1. static(ish) attributes, such as the id, path on disk, hash of the tags
|
|
||||||
* 2. dynamic attributes, such as the number of times this song has been
|
|
||||||
* played.
|
|
||||||
*
|
|
||||||
* Because a SongData is immutable, it is thread safe but will not reflect any
|
|
||||||
* changes to the dynamic attributes that may happen after it was obtained.
|
|
||||||
*
|
|
||||||
* Songs may be 'tombstoned'; this indicates that the song is no longer present
|
|
||||||
* at its previous location on disk, and we do not have any existing files with
|
|
||||||
* a matching tags_hash. When this is the case, we ignore this SongData for most
|
|
||||||
* purposes. We keep the entry in our database so that we can properly restore
|
|
||||||
* dynamic attributes (such as play count) if the song later re-appears on disk.
|
|
||||||
*/
|
|
||||||
class SongData {
|
|
||||||
private:
|
|
||||||
const SongId id_;
|
|
||||||
const std::string filepath_;
|
|
||||||
const uint64_t tags_hash_;
|
|
||||||
const uint32_t play_count_;
|
|
||||||
const bool is_tombstoned_;
|
|
||||||
|
|
||||||
public:
|
|
||||||
/* Constructor used when adding new songs to the database. */
|
|
||||||
SongData(SongId id, const std::string& path, uint64_t hash)
|
|
||||||
: id_(id),
|
|
||||||
filepath_(path),
|
|
||||||
tags_hash_(hash),
|
|
||||||
play_count_(0),
|
|
||||||
is_tombstoned_(false) {}
|
|
||||||
|
|
||||||
SongData(SongId id,
|
|
||||||
const std::string& path,
|
|
||||||
uint64_t hash,
|
|
||||||
uint32_t play_count,
|
|
||||||
bool is_tombstoned)
|
|
||||||
: id_(id),
|
|
||||||
filepath_(path),
|
|
||||||
tags_hash_(hash),
|
|
||||||
play_count_(play_count),
|
|
||||||
is_tombstoned_(is_tombstoned) {}
|
|
||||||
|
|
||||||
auto id() const -> SongId { return id_; }
|
|
||||||
auto filepath() const -> std::string { return filepath_; }
|
|
||||||
auto play_count() const -> uint32_t { return play_count_; }
|
|
||||||
auto tags_hash() const -> uint64_t { return tags_hash_; }
|
|
||||||
auto is_tombstoned() const -> bool { return is_tombstoned_; }
|
|
||||||
|
|
||||||
auto UpdateHash(uint64_t new_hash) const -> SongData;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Marks this song data as a 'tombstone'. Tombstoned songs are not playable,
|
|
||||||
* and should not generally be shown to users.
|
|
||||||
*/
|
|
||||||
auto Entomb() const -> SongData;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Clears the tombstone bit of this song, and updates the path to reflect its
|
|
||||||
* new location.
|
|
||||||
*/
|
|
||||||
auto Exhume(const std::string& new_path) const -> SongData;
|
|
||||||
|
|
||||||
bool operator==(const SongData&) const = default;
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Immutable and owning combination of a song's tags and metadata.
|
|
||||||
*
|
|
||||||
* Note that instances of this class may have a fairly large memory impact, due
|
|
||||||
* to the large number of strings they own. Prefer to query the database again
|
|
||||||
* (which has its own caching layer), rather than retaining Song instances for a
|
|
||||||
* long time.
|
|
||||||
*/
|
|
||||||
class Song {
|
|
||||||
public:
|
|
||||||
Song(const SongData& data, const SongTags& tags) : data_(data), tags_(tags) {}
|
|
||||||
Song(const Song& other) = default;
|
|
||||||
|
|
||||||
auto data() const -> const SongData& { return data_; }
|
|
||||||
auto tags() const -> const SongTags& { return tags_; }
|
|
||||||
|
|
||||||
bool operator==(const Song&) const = default;
|
|
||||||
Song operator=(const Song& other) const { return Song(other); }
|
|
||||||
|
|
||||||
private:
|
|
||||||
const SongData data_;
|
|
||||||
const SongTags tags_;
|
|
||||||
};
|
|
||||||
|
|
||||||
void swap(Song& first, Song& second);
|
|
||||||
|
|
||||||
} // namespace database
|
|
||||||
@@ -8,20 +8,20 @@
|
|||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "song.hpp"
|
#include "track.hpp"
|
||||||
|
|
||||||
namespace database {
|
namespace database {
|
||||||
|
|
||||||
class ITagParser {
|
class ITagParser {
|
||||||
public:
|
public:
|
||||||
virtual ~ITagParser() {}
|
virtual ~ITagParser() {}
|
||||||
virtual auto ReadAndParseTags(const std::string& path, SongTags* out)
|
virtual auto ReadAndParseTags(const std::string& path, TrackTags* out)
|
||||||
-> bool = 0;
|
-> bool = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
class TagParserImpl : public ITagParser {
|
class TagParserImpl : public ITagParser {
|
||||||
public:
|
public:
|
||||||
virtual auto ReadAndParseTags(const std::string& path, SongTags* out)
|
virtual auto ReadAndParseTags(const std::string& path, TrackTags* out)
|
||||||
-> bool override;
|
-> bool override;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 jacqueline <me@jacqueline.id.au>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "leveldb/db.h"
|
||||||
|
#include "span.hpp"
|
||||||
|
|
||||||
|
namespace database {
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Uniquely describes a single track within the database. This value will be
|
||||||
|
* consistent across database updates, and should ideally (but is not guaranteed
|
||||||
|
* to) endure even across a track being removed and re-added.
|
||||||
|
*
|
||||||
|
* Four billion tracks should be enough for anybody.
|
||||||
|
*/
|
||||||
|
typedef uint32_t TrackId;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Audio file encodings that we are aware of. Used to select an appropriate
|
||||||
|
* decoder at play time.
|
||||||
|
*
|
||||||
|
* Values of this enum are persisted in this database, so it is probably never a
|
||||||
|
* good idea to change the int representation of an existing value.
|
||||||
|
*/
|
||||||
|
enum class Encoding {
|
||||||
|
kUnsupported = 0,
|
||||||
|
kMp3 = 1,
|
||||||
|
kWav = 2,
|
||||||
|
kOgg = 3,
|
||||||
|
kFlac = 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Owning container for tag-related track metadata that was extracted from a
|
||||||
|
* file.
|
||||||
|
*/
|
||||||
|
struct TrackTags {
|
||||||
|
Encoding encoding;
|
||||||
|
std::optional<std::string> title;
|
||||||
|
|
||||||
|
// TODO(jacqueline): It would be nice to use shared_ptr's for the artist and
|
||||||
|
// album, since there's likely a fair number of duplicates for each
|
||||||
|
// (especially the former).
|
||||||
|
|
||||||
|
std::optional<std::string> artist;
|
||||||
|
std::optional<std::string> album;
|
||||||
|
|
||||||
|
std::optional<int> channels;
|
||||||
|
std::optional<int> sample_rate;
|
||||||
|
std::optional<int> bits_per_sample;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Returns a hash of the 'identifying' tags of this track. That is, a hash
|
||||||
|
* that can be used to determine if one track is likely the same as another,
|
||||||
|
* across things like re-encoding, re-mastering, or moving the underlying
|
||||||
|
* file.
|
||||||
|
*/
|
||||||
|
auto Hash() const -> uint64_t;
|
||||||
|
|
||||||
|
bool operator==(const TrackTags&) const = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Immutable owning container for all of the metadata we store for a particular
|
||||||
|
* track. This includes two main kinds of metadata:
|
||||||
|
* 1. static(ish) attributes, such as the id, path on disk, hash of the tags
|
||||||
|
* 2. dynamic attributes, such as the number of times this track has been
|
||||||
|
* played.
|
||||||
|
*
|
||||||
|
* Because a TrackData is immutable, it is thread safe but will not reflect any
|
||||||
|
* changes to the dynamic attributes that may happen after it was obtained.
|
||||||
|
*
|
||||||
|
* Tracks may be 'tombstoned'; this indicates that the track is no longer
|
||||||
|
* present at its previous location on disk, and we do not have any existing
|
||||||
|
* files with a matching tags_hash. When this is the case, we ignore this
|
||||||
|
* TrackData for most purposes. We keep the entry in our database so that we can
|
||||||
|
* properly restore dynamic attributes (such as play count) if the track later
|
||||||
|
* re-appears on disk.
|
||||||
|
*/
|
||||||
|
class TrackData {
|
||||||
|
private:
|
||||||
|
const TrackId id_;
|
||||||
|
const std::string filepath_;
|
||||||
|
const uint64_t tags_hash_;
|
||||||
|
const uint32_t play_count_;
|
||||||
|
const bool is_tombstoned_;
|
||||||
|
|
||||||
|
public:
|
||||||
|
/* Constructor used when adding new tracks to the database. */
|
||||||
|
TrackData(TrackId id, const std::string& path, uint64_t hash)
|
||||||
|
: id_(id),
|
||||||
|
filepath_(path),
|
||||||
|
tags_hash_(hash),
|
||||||
|
play_count_(0),
|
||||||
|
is_tombstoned_(false) {}
|
||||||
|
|
||||||
|
TrackData(TrackId id,
|
||||||
|
const std::string& path,
|
||||||
|
uint64_t hash,
|
||||||
|
uint32_t play_count,
|
||||||
|
bool is_tombstoned)
|
||||||
|
: id_(id),
|
||||||
|
filepath_(path),
|
||||||
|
tags_hash_(hash),
|
||||||
|
play_count_(play_count),
|
||||||
|
is_tombstoned_(is_tombstoned) {}
|
||||||
|
|
||||||
|
auto id() const -> TrackId { return id_; }
|
||||||
|
auto filepath() const -> std::string { return filepath_; }
|
||||||
|
auto play_count() const -> uint32_t { return play_count_; }
|
||||||
|
auto tags_hash() const -> uint64_t { return tags_hash_; }
|
||||||
|
auto is_tombstoned() const -> bool { return is_tombstoned_; }
|
||||||
|
|
||||||
|
auto UpdateHash(uint64_t new_hash) const -> TrackData;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Marks this track data as a 'tombstone'. Tombstoned tracks are not playable,
|
||||||
|
* and should not generally be shown to users.
|
||||||
|
*/
|
||||||
|
auto Entomb() const -> TrackData;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Clears the tombstone bit of this track, and updates the path to reflect its
|
||||||
|
* new location.
|
||||||
|
*/
|
||||||
|
auto Exhume(const std::string& new_path) const -> TrackData;
|
||||||
|
|
||||||
|
bool operator==(const TrackData&) const = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Immutable and owning combination of a track's tags and metadata.
|
||||||
|
*
|
||||||
|
* Note that instances of this class may have a fairly large memory impact, due
|
||||||
|
* to the large number of strings they own. Prefer to query the database again
|
||||||
|
* (which has its own caching layer), rather than retaining Track instances for
|
||||||
|
* a long time.
|
||||||
|
*/
|
||||||
|
class Track {
|
||||||
|
public:
|
||||||
|
Track(const TrackData& data, const TrackTags& tags)
|
||||||
|
: data_(data), tags_(tags) {}
|
||||||
|
Track(const Track& other) = default;
|
||||||
|
|
||||||
|
auto data() const -> const TrackData& { return data_; }
|
||||||
|
auto tags() const -> const TrackTags& { return tags_; }
|
||||||
|
|
||||||
|
bool operator==(const Track&) const = default;
|
||||||
|
Track operator=(const Track& other) const { return Track(other); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
const TrackData data_;
|
||||||
|
const TrackTags tags_;
|
||||||
|
};
|
||||||
|
|
||||||
|
void swap(Track& first, Track& second);
|
||||||
|
|
||||||
|
} // namespace database
|
||||||
+19
-19
@@ -14,7 +14,7 @@
|
|||||||
#include "cbor.h"
|
#include "cbor.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
|
|
||||||
#include "song.hpp"
|
#include "track.hpp"
|
||||||
|
|
||||||
namespace database {
|
namespace database {
|
||||||
|
|
||||||
@@ -60,14 +60,14 @@ auto CreateDataPrefix() -> OwningSlice {
|
|||||||
return OwningSlice({data, 2});
|
return OwningSlice({data, 2});
|
||||||
}
|
}
|
||||||
|
|
||||||
auto CreateDataKey(const SongId& id) -> OwningSlice {
|
auto CreateDataKey(const TrackId& id) -> OwningSlice {
|
||||||
std::ostringstream output;
|
std::ostringstream output;
|
||||||
output.put(kDataPrefix).put(kFieldSeparator);
|
output.put(kDataPrefix).put(kFieldSeparator);
|
||||||
output << SongIdToBytes(id).data;
|
output << TrackIdToBytes(id).data;
|
||||||
return OwningSlice(output.str());
|
return OwningSlice(output.str());
|
||||||
}
|
}
|
||||||
|
|
||||||
auto CreateDataValue(const SongData& song) -> OwningSlice {
|
auto CreateDataValue(const TrackData& track) -> OwningSlice {
|
||||||
uint8_t* buf;
|
uint8_t* buf;
|
||||||
std::size_t buf_len = cbor_encode(&buf, [&](CborEncoder* enc) {
|
std::size_t buf_len = cbor_encode(&buf, [&](CborEncoder* enc) {
|
||||||
CborEncoder array_encoder;
|
CborEncoder array_encoder;
|
||||||
@@ -77,28 +77,28 @@ auto CreateDataValue(const SongData& song) -> OwningSlice {
|
|||||||
ESP_LOGE(kTag, "encoding err %u", err);
|
ESP_LOGE(kTag, "encoding err %u", err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
err = cbor_encode_int(&array_encoder, song.id());
|
err = cbor_encode_int(&array_encoder, track.id());
|
||||||
if (err != CborNoError && err != CborErrorOutOfMemory) {
|
if (err != CborNoError && err != CborErrorOutOfMemory) {
|
||||||
ESP_LOGE(kTag, "encoding err %u", err);
|
ESP_LOGE(kTag, "encoding err %u", err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
err = cbor_encode_text_string(&array_encoder, song.filepath().c_str(),
|
err = cbor_encode_text_string(&array_encoder, track.filepath().c_str(),
|
||||||
song.filepath().size());
|
track.filepath().size());
|
||||||
if (err != CborNoError && err != CborErrorOutOfMemory) {
|
if (err != CborNoError && err != CborErrorOutOfMemory) {
|
||||||
ESP_LOGE(kTag, "encoding err %u", err);
|
ESP_LOGE(kTag, "encoding err %u", err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
err = cbor_encode_uint(&array_encoder, song.tags_hash());
|
err = cbor_encode_uint(&array_encoder, track.tags_hash());
|
||||||
if (err != CborNoError && err != CborErrorOutOfMemory) {
|
if (err != CborNoError && err != CborErrorOutOfMemory) {
|
||||||
ESP_LOGE(kTag, "encoding err %u", err);
|
ESP_LOGE(kTag, "encoding err %u", err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
err = cbor_encode_int(&array_encoder, song.play_count());
|
err = cbor_encode_int(&array_encoder, track.play_count());
|
||||||
if (err != CborNoError && err != CborErrorOutOfMemory) {
|
if (err != CborNoError && err != CborErrorOutOfMemory) {
|
||||||
ESP_LOGE(kTag, "encoding err %u", err);
|
ESP_LOGE(kTag, "encoding err %u", err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
err = cbor_encode_boolean(&array_encoder, song.is_tombstoned());
|
err = cbor_encode_boolean(&array_encoder, track.is_tombstoned());
|
||||||
if (err != CborNoError && err != CborErrorOutOfMemory) {
|
if (err != CborNoError && err != CborErrorOutOfMemory) {
|
||||||
ESP_LOGE(kTag, "encoding err %u", err);
|
ESP_LOGE(kTag, "encoding err %u", err);
|
||||||
return;
|
return;
|
||||||
@@ -114,7 +114,7 @@ auto CreateDataValue(const SongData& song) -> OwningSlice {
|
|||||||
return OwningSlice(as_str);
|
return OwningSlice(as_str);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto ParseDataValue(const leveldb::Slice& slice) -> std::optional<SongData> {
|
auto ParseDataValue(const leveldb::Slice& slice) -> std::optional<TrackData> {
|
||||||
CborParser parser;
|
CborParser parser;
|
||||||
CborValue container;
|
CborValue container;
|
||||||
CborError err;
|
CborError err;
|
||||||
@@ -135,7 +135,7 @@ auto ParseDataValue(const leveldb::Slice& slice) -> std::optional<SongData> {
|
|||||||
if (err != CborNoError) {
|
if (err != CborNoError) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
SongId id = raw_int;
|
TrackId id = raw_int;
|
||||||
err = cbor_value_advance(&val);
|
err = cbor_value_advance(&val);
|
||||||
if (err != CborNoError || !cbor_value_is_text_string(&val)) {
|
if (err != CborNoError || !cbor_value_is_text_string(&val)) {
|
||||||
return {};
|
return {};
|
||||||
@@ -176,7 +176,7 @@ auto ParseDataValue(const leveldb::Slice& slice) -> std::optional<SongData> {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
return SongData(id, path, hash, play_count, is_tombstoned);
|
return TrackData(id, path, hash, play_count, is_tombstoned);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto CreateHashKey(const uint64_t& hash) -> OwningSlice {
|
auto CreateHashKey(const uint64_t& hash) -> OwningSlice {
|
||||||
@@ -193,15 +193,15 @@ auto CreateHashKey(const uint64_t& hash) -> OwningSlice {
|
|||||||
return OwningSlice(output.str());
|
return OwningSlice(output.str());
|
||||||
}
|
}
|
||||||
|
|
||||||
auto ParseHashValue(const leveldb::Slice& slice) -> std::optional<SongId> {
|
auto ParseHashValue(const leveldb::Slice& slice) -> std::optional<TrackId> {
|
||||||
return BytesToSongId(slice.ToString());
|
return BytesToTrackId(slice.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
auto CreateHashValue(SongId id) -> OwningSlice {
|
auto CreateHashValue(TrackId id) -> OwningSlice {
|
||||||
return SongIdToBytes(id);
|
return TrackIdToBytes(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto SongIdToBytes(SongId id) -> OwningSlice {
|
auto TrackIdToBytes(TrackId id) -> OwningSlice {
|
||||||
uint8_t buf[8];
|
uint8_t buf[8];
|
||||||
CborEncoder enc;
|
CborEncoder enc;
|
||||||
cbor_encoder_init(&enc, buf, sizeof(buf), 0);
|
cbor_encoder_init(&enc, buf, sizeof(buf), 0);
|
||||||
@@ -211,7 +211,7 @@ auto SongIdToBytes(SongId id) -> OwningSlice {
|
|||||||
return OwningSlice(as_str);
|
return OwningSlice(as_str);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto BytesToSongId(const std::string& bytes) -> std::optional<SongId> {
|
auto BytesToTrackId(const std::string& bytes) -> std::optional<TrackId> {
|
||||||
CborParser parser;
|
CborParser parser;
|
||||||
CborValue val;
|
CborValue val;
|
||||||
cbor_parser_init(reinterpret_cast<const uint8_t*>(bytes.data()), bytes.size(),
|
cbor_parser_init(reinterpret_cast<const uint8_t*>(bytes.data()), bytes.size(),
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ namespace libtags {
|
|||||||
struct Aux {
|
struct Aux {
|
||||||
FIL file;
|
FIL file;
|
||||||
FILINFO info;
|
FILINFO info;
|
||||||
SongTags* tags;
|
TrackTags* tags;
|
||||||
};
|
};
|
||||||
|
|
||||||
static int read(Tagctx* ctx, void* buf, int cnt) {
|
static int read(Tagctx* ctx, void* buf, int cnt) {
|
||||||
@@ -71,7 +71,7 @@ static void toc(Tagctx* ctx, int ms, int offset) {}
|
|||||||
static const std::size_t kBufSize = 1024;
|
static const std::size_t kBufSize = 1024;
|
||||||
static const char* kTag = "TAGS";
|
static const char* kTag = "TAGS";
|
||||||
|
|
||||||
auto TagParserImpl::ReadAndParseTags(const std::string& path, SongTags* out)
|
auto TagParserImpl::ReadAndParseTags(const std::string& path, TrackTags* out)
|
||||||
-> bool {
|
-> bool {
|
||||||
libtags::Aux aux;
|
libtags::Aux aux;
|
||||||
aux.tags = out;
|
aux.tags = out;
|
||||||
|
|||||||
@@ -18,41 +18,41 @@
|
|||||||
#include "file_gatherer.hpp"
|
#include "file_gatherer.hpp"
|
||||||
#include "i2c_fixture.hpp"
|
#include "i2c_fixture.hpp"
|
||||||
#include "leveldb/db.h"
|
#include "leveldb/db.h"
|
||||||
#include "song.hpp"
|
|
||||||
#include "spi_fixture.hpp"
|
#include "spi_fixture.hpp"
|
||||||
#include "tag_parser.hpp"
|
#include "tag_parser.hpp"
|
||||||
|
#include "track.hpp"
|
||||||
|
|
||||||
namespace database {
|
namespace database {
|
||||||
|
|
||||||
class TestBackends : public IFileGatherer, public ITagParser {
|
class TestBackends : public IFileGatherer, public ITagParser {
|
||||||
public:
|
public:
|
||||||
std::map<std::string, SongTags> songs;
|
std::map<std::string, TrackTags> tracks;
|
||||||
|
|
||||||
auto MakeSong(const std::string& path, const std::string& title) -> void {
|
auto MakeTrack(const std::string& path, const std::string& title) -> void {
|
||||||
SongTags tags;
|
TrackTags tags;
|
||||||
tags.encoding = Encoding::kMp3;
|
tags.encoding = Encoding::kMp3;
|
||||||
tags.title = title;
|
tags.title = title;
|
||||||
songs[path] = tags;
|
tracks[path] = tags;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto FindFiles(const std::string& root,
|
auto FindFiles(const std::string& root,
|
||||||
std::function<void(const std::string&)> cb) -> void override {
|
std::function<void(const std::string&)> cb) -> void override {
|
||||||
for (auto keyval : songs) {
|
for (auto keyval : tracks) {
|
||||||
std::invoke(cb, keyval.first);
|
std::invoke(cb, keyval.first);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto ReadAndParseTags(const std::string& path, SongTags* out)
|
auto ReadAndParseTags(const std::string& path, TrackTags* out)
|
||||||
-> bool override {
|
-> bool override {
|
||||||
if (songs.contains(path)) {
|
if (tracks.contains(path)) {
|
||||||
*out = songs.at(path);
|
*out = tracks.at(path);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
TEST_CASE("song database", "[integration]") {
|
TEST_CASE("track database", "[integration]") {
|
||||||
I2CFixture i2c;
|
I2CFixture i2c;
|
||||||
SpiFixture spi;
|
SpiFixture spi;
|
||||||
drivers::DriverCache drivers;
|
drivers::DriverCache drivers;
|
||||||
@@ -60,104 +60,104 @@ TEST_CASE("song database", "[integration]") {
|
|||||||
|
|
||||||
Database::Destroy();
|
Database::Destroy();
|
||||||
|
|
||||||
TestBackends songs;
|
TestBackends tracks;
|
||||||
auto open_res = Database::Open(&songs, &songs);
|
auto open_res = Database::Open(&tracks, &tracks);
|
||||||
REQUIRE(open_res.has_value());
|
REQUIRE(open_res.has_value());
|
||||||
std::unique_ptr<Database> db(open_res.value());
|
std::unique_ptr<Database> db(open_res.value());
|
||||||
|
|
||||||
SECTION("empty database") {
|
SECTION("empty database") {
|
||||||
std::unique_ptr<Result<Song>> res(db->GetSongs(10).get());
|
std::unique_ptr<Result<Track>> res(db->GetTracks(10).get());
|
||||||
REQUIRE(res->values().size() == 0);
|
REQUIRE(res->values().size() == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
SECTION("add new songs") {
|
SECTION("add new tracks") {
|
||||||
songs.MakeSong("song1.mp3", "Song 1");
|
tracks.MakeTrack("track1.mp3", "Track 1");
|
||||||
songs.MakeSong("song2.wav", "Song 2");
|
tracks.MakeTrack("track2.wav", "Track 2");
|
||||||
songs.MakeSong("song3.exe", "Song 3");
|
tracks.MakeTrack("track3.exe", "Track 3");
|
||||||
|
|
||||||
db->Update();
|
db->Update();
|
||||||
|
|
||||||
std::unique_ptr<Result<Song>> res(db->GetSongs(10).get());
|
std::unique_ptr<Result<Track>> res(db->GetTracks(10).get());
|
||||||
REQUIRE(res->values().size() == 3);
|
REQUIRE(res->values().size() == 3);
|
||||||
CHECK(*res->values().at(0).tags().title == "Song 1");
|
CHECK(*res->values().at(0).tags().title == "Track 1");
|
||||||
CHECK(res->values().at(0).data().id() == 1);
|
CHECK(res->values().at(0).data().id() == 1);
|
||||||
CHECK(*res->values().at(1).tags().title == "Song 2");
|
CHECK(*res->values().at(1).tags().title == "Track 2");
|
||||||
CHECK(res->values().at(1).data().id() == 2);
|
CHECK(res->values().at(1).data().id() == 2);
|
||||||
CHECK(*res->values().at(2).tags().title == "Song 3");
|
CHECK(*res->values().at(2).tags().title == "Track 3");
|
||||||
CHECK(res->values().at(2).data().id() == 3);
|
CHECK(res->values().at(2).data().id() == 3);
|
||||||
|
|
||||||
SECTION("update with no filesystem changes") {
|
SECTION("update with no filesystem changes") {
|
||||||
db->Update();
|
db->Update();
|
||||||
|
|
||||||
std::unique_ptr<Result<Song>> new_res(db->GetSongs(10).get());
|
std::unique_ptr<Result<Track>> new_res(db->GetTracks(10).get());
|
||||||
REQUIRE(new_res->values().size() == 3);
|
REQUIRE(new_res->values().size() == 3);
|
||||||
CHECK(res->values().at(0) == new_res->values().at(0));
|
CHECK(res->values().at(0) == new_res->values().at(0));
|
||||||
CHECK(res->values().at(1) == new_res->values().at(1));
|
CHECK(res->values().at(1) == new_res->values().at(1));
|
||||||
CHECK(res->values().at(2) == new_res->values().at(2));
|
CHECK(res->values().at(2) == new_res->values().at(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
SECTION("update with all songs gone") {
|
SECTION("update with all tracks gone") {
|
||||||
songs.songs.clear();
|
tracks.tracks.clear();
|
||||||
|
|
||||||
db->Update();
|
db->Update();
|
||||||
|
|
||||||
std::unique_ptr<Result<Song>> new_res(db->GetSongs(10).get());
|
std::unique_ptr<Result<Track>> new_res(db->GetTracks(10).get());
|
||||||
CHECK(new_res->values().size() == 0);
|
CHECK(new_res->values().size() == 0);
|
||||||
|
|
||||||
SECTION("update with one song returned") {
|
SECTION("update with one track returned") {
|
||||||
songs.MakeSong("song2.wav", "Song 2");
|
tracks.MakeTrack("track2.wav", "Track 2");
|
||||||
|
|
||||||
db->Update();
|
db->Update();
|
||||||
|
|
||||||
std::unique_ptr<Result<Song>> new_res(db->GetSongs(10).get());
|
std::unique_ptr<Result<Track>> new_res(db->GetTracks(10).get());
|
||||||
REQUIRE(new_res->values().size() == 1);
|
REQUIRE(new_res->values().size() == 1);
|
||||||
CHECK(res->values().at(1) == new_res->values().at(0));
|
CHECK(res->values().at(1) == new_res->values().at(0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SECTION("update with one song gone") {
|
SECTION("update with one track gone") {
|
||||||
songs.songs.erase("song2.wav");
|
tracks.tracks.erase("track2.wav");
|
||||||
|
|
||||||
db->Update();
|
db->Update();
|
||||||
|
|
||||||
std::unique_ptr<Result<Song>> new_res(db->GetSongs(10).get());
|
std::unique_ptr<Result<Track>> new_res(db->GetTracks(10).get());
|
||||||
REQUIRE(new_res->values().size() == 2);
|
REQUIRE(new_res->values().size() == 2);
|
||||||
CHECK(res->values().at(0) == new_res->values().at(0));
|
CHECK(res->values().at(0) == new_res->values().at(0));
|
||||||
CHECK(res->values().at(2) == new_res->values().at(1));
|
CHECK(res->values().at(2) == new_res->values().at(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
SECTION("update with tags changed") {
|
SECTION("update with tags changed") {
|
||||||
songs.MakeSong("song3.exe", "The Song 3");
|
tracks.MakeTrack("track3.exe", "The Track 3");
|
||||||
|
|
||||||
db->Update();
|
db->Update();
|
||||||
|
|
||||||
std::unique_ptr<Result<Song>> new_res(db->GetSongs(10).get());
|
std::unique_ptr<Result<Track>> new_res(db->GetTracks(10).get());
|
||||||
REQUIRE(new_res->values().size() == 3);
|
REQUIRE(new_res->values().size() == 3);
|
||||||
CHECK(res->values().at(0) == new_res->values().at(0));
|
CHECK(res->values().at(0) == new_res->values().at(0));
|
||||||
CHECK(res->values().at(1) == new_res->values().at(1));
|
CHECK(res->values().at(1) == new_res->values().at(1));
|
||||||
CHECK(*new_res->values().at(2).tags().title == "The Song 3");
|
CHECK(*new_res->values().at(2).tags().title == "The Track 3");
|
||||||
// The id should not have changed, since this was just a tag update.
|
// The id should not have changed, since this was just a tag update.
|
||||||
CHECK(res->values().at(2).data().id() ==
|
CHECK(res->values().at(2).data().id() ==
|
||||||
new_res->values().at(2).data().id());
|
new_res->values().at(2).data().id());
|
||||||
}
|
}
|
||||||
|
|
||||||
SECTION("update with one new song") {
|
SECTION("update with one new track") {
|
||||||
songs.MakeSong("my song.midi", "Song 1 (nightcore remix)");
|
tracks.MakeTrack("my track.midi", "Track 1 (nightcore remix)");
|
||||||
|
|
||||||
db->Update();
|
db->Update();
|
||||||
|
|
||||||
std::unique_ptr<Result<Song>> new_res(db->GetSongs(10).get());
|
std::unique_ptr<Result<Track>> new_res(db->GetTracks(10).get());
|
||||||
REQUIRE(new_res->values().size() == 4);
|
REQUIRE(new_res->values().size() == 4);
|
||||||
CHECK(res->values().at(0) == new_res->values().at(0));
|
CHECK(res->values().at(0) == new_res->values().at(0));
|
||||||
CHECK(res->values().at(1) == new_res->values().at(1));
|
CHECK(res->values().at(1) == new_res->values().at(1));
|
||||||
CHECK(res->values().at(2) == new_res->values().at(2));
|
CHECK(res->values().at(2) == new_res->values().at(2));
|
||||||
CHECK(*new_res->values().at(3).tags().title ==
|
CHECK(*new_res->values().at(3).tags().title ==
|
||||||
"Song 1 (nightcore remix)");
|
"Track 1 (nightcore remix)");
|
||||||
CHECK(new_res->values().at(3).data().id() == 4);
|
CHECK(new_res->values().at(3).data().id() == 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
SECTION("get songs with pagination") {
|
SECTION("get tracks with pagination") {
|
||||||
std::unique_ptr<Result<Song>> res(db->GetSongs(1).get());
|
std::unique_ptr<Result<Track>> res(db->GetTracks(1).get());
|
||||||
|
|
||||||
REQUIRE(res->values().size() == 1);
|
REQUIRE(res->values().size() == 1);
|
||||||
CHECK(res->values().at(0).data().id() == 1);
|
CHECK(res->values().at(0).data().id() == 1);
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ std::string ToHex(const std::string& s) {
|
|||||||
namespace database {
|
namespace database {
|
||||||
|
|
||||||
TEST_CASE("database record encoding", "[unit]") {
|
TEST_CASE("database record encoding", "[unit]") {
|
||||||
SECTION("song id to bytes") {
|
SECTION("track id to bytes") {
|
||||||
SongId id = 1234678;
|
TrackId id = 1234678;
|
||||||
OwningSlice as_bytes = SongIdToBytes(id);
|
OwningSlice as_bytes = TrackIdToBytes(id);
|
||||||
|
|
||||||
SECTION("encodes correctly") {
|
SECTION("encodes correctly") {
|
||||||
// Purposefully a brittle test, since we need to be very careful about
|
// Purposefully a brittle test, since we need to be very careful about
|
||||||
@@ -44,18 +44,18 @@ TEST_CASE("database record encoding", "[unit]") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SECTION("round-trips") {
|
SECTION("round-trips") {
|
||||||
CHECK(*BytesToSongId(as_bytes.data) == id);
|
CHECK(*BytesToTrackId(as_bytes.data) == id);
|
||||||
}
|
}
|
||||||
|
|
||||||
SECTION("encodes compactly") {
|
SECTION("encodes compactly") {
|
||||||
OwningSlice small_id = SongIdToBytes(1);
|
OwningSlice small_id = TrackIdToBytes(1);
|
||||||
OwningSlice large_id = SongIdToBytes(999999);
|
OwningSlice large_id = TrackIdToBytes(999999);
|
||||||
|
|
||||||
CHECK(small_id.data.size() < large_id.data.size());
|
CHECK(small_id.data.size() < large_id.data.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
SECTION("decoding rejects garbage") {
|
SECTION("decoding rejects garbage") {
|
||||||
std::optional<SongId> res = BytesToSongId("i'm gay");
|
std::optional<TrackId> res = BytesToTrackId("i'm gay");
|
||||||
|
|
||||||
CHECK(res.has_value() == false);
|
CHECK(res.has_value() == false);
|
||||||
}
|
}
|
||||||
@@ -73,7 +73,7 @@ TEST_CASE("database record encoding", "[unit]") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SECTION("data values") {
|
SECTION("data values") {
|
||||||
SongData data(123, "/some/path.mp3", 0xACAB, 69, true);
|
TrackData data(123, "/some/path.mp3", 0xACAB, 69, true);
|
||||||
|
|
||||||
OwningSlice enc = CreateDataValue(data);
|
OwningSlice enc = CreateDataValue(data);
|
||||||
|
|
||||||
@@ -109,7 +109,7 @@ TEST_CASE("database record encoding", "[unit]") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SECTION("decoding rejects garbage") {
|
SECTION("decoding rejects garbage") {
|
||||||
std::optional<SongData> res = ParseDataValue("hi!");
|
std::optional<TrackData> res = ParseDataValue("hi!");
|
||||||
|
|
||||||
CHECK(res.has_value() == false);
|
CHECK(res.has_value() == false);
|
||||||
}
|
}
|
||||||
@@ -129,14 +129,14 @@ TEST_CASE("database record encoding", "[unit]") {
|
|||||||
SECTION("hash values") {
|
SECTION("hash values") {
|
||||||
OwningSlice val = CreateHashValue(123456);
|
OwningSlice val = CreateHashValue(123456);
|
||||||
|
|
||||||
CHECK(val.data == SongIdToBytes(123456).data);
|
CHECK(val.data == TrackIdToBytes(123456).data);
|
||||||
|
|
||||||
SECTION("round-trips") {
|
SECTION("round-trips") {
|
||||||
CHECK(ParseHashValue(val.slice) == 123456);
|
CHECK(ParseHashValue(val.slice) == 123456);
|
||||||
}
|
}
|
||||||
|
|
||||||
SECTION("decoding rejects garbage") {
|
SECTION("decoding rejects garbage") {
|
||||||
std::optional<SongId> res = ParseHashValue("the first song :)");
|
std::optional<TrackId> res = ParseHashValue("the first track :)");
|
||||||
|
|
||||||
CHECK(res.has_value() == false);
|
CHECK(res.has_value() == false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-only
|
* SPDX-License-Identifier: GPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "song.hpp"
|
#include "track.hpp"
|
||||||
|
|
||||||
#include <komihash.h>
|
#include <komihash.h>
|
||||||
|
|
||||||
@@ -19,8 +19,8 @@ auto HashString(komihash_stream_t* stream, std::string str) -> void {
|
|||||||
* Uses a komihash stream to incrementally hash tags. This lowers the function's
|
* 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.
|
* memory footprint a little so that it's safe to call from any stack.
|
||||||
*/
|
*/
|
||||||
auto SongTags::Hash() const -> uint64_t {
|
auto TrackTags::Hash() const -> uint64_t {
|
||||||
// TODO(jacqueline): this function doesn't work very well for songs with no
|
// TODO(jacqueline): this function doesn't work very well for tracks with no
|
||||||
// tags at all.
|
// tags at all.
|
||||||
komihash_stream_t stream;
|
komihash_stream_t stream;
|
||||||
komihash_stream_init(&stream, 0);
|
komihash_stream_init(&stream, 0);
|
||||||
@@ -30,20 +30,20 @@ auto SongTags::Hash() const -> uint64_t {
|
|||||||
return komihash_stream_final(&stream);
|
return komihash_stream_final(&stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto SongData::UpdateHash(uint64_t new_hash) const -> SongData {
|
auto TrackData::UpdateHash(uint64_t new_hash) const -> TrackData {
|
||||||
return SongData(id_, filepath_, new_hash, play_count_, is_tombstoned_);
|
return TrackData(id_, filepath_, new_hash, play_count_, is_tombstoned_);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto SongData::Entomb() const -> SongData {
|
auto TrackData::Entomb() const -> TrackData {
|
||||||
return SongData(id_, filepath_, tags_hash_, play_count_, true);
|
return TrackData(id_, filepath_, tags_hash_, play_count_, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto SongData::Exhume(const std::string& new_path) const -> SongData {
|
auto TrackData::Exhume(const std::string& new_path) const -> TrackData {
|
||||||
return SongData(id_, new_path, tags_hash_, play_count_, false);
|
return TrackData(id_, new_path, tags_hash_, play_count_, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void swap(Song& first, Song& second) {
|
void swap(Track& first, Track& second) {
|
||||||
Song temp = first;
|
Track temp = first;
|
||||||
first = second;
|
first = second;
|
||||||
second = temp;
|
second = temp;
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user