diff options
| author | Ailurux <ailuruxx@gmail.com> | 2023-06-19 11:21:32 +1000 |
|---|---|---|
| committer | Ailurux <ailuruxx@gmail.com> | 2023-06-19 11:21:32 +1000 |
| commit | 039272455acddbe446269ea4b6ef66f44f457f1e (patch) | |
| tree | 1e9a8173aeb4eb027701e89019e9410a9550d3cb /src/database | |
| parent | 8ce751ad56c7efe19f835e3b6bbb1a843cef9119 (diff) | |
| parent | 6ff8b5886ef91ed46dba08686900d519f6c9c62d (diff) | |
| download | tangara-fw-039272455acddbe446269ea4b6ef66f44f457f1e.tar.gz | |
Merge branch 'main' of https://git.sr.ht/~jacqueline/tangara-fw
Diffstat (limited to 'src/database')
| -rw-r--r-- | src/database/CMakeLists.txt | 2 | ||||
| -rw-r--r-- | src/database/database.cpp | 150 | ||||
| -rw-r--r-- | src/database/env_esp.cpp | 5 | ||||
| -rw-r--r-- | src/database/include/database.hpp | 26 | ||||
| -rw-r--r-- | src/database/include/env_esp.hpp | 2 | ||||
| -rw-r--r-- | src/database/include/records.hpp | 36 | ||||
| -rw-r--r-- | src/database/include/song.hpp | 166 | ||||
| -rw-r--r-- | src/database/include/tag_parser.hpp | 6 | ||||
| -rw-r--r-- | src/database/include/track.hpp | 169 | ||||
| -rw-r--r-- | src/database/records.cpp | 38 | ||||
| -rw-r--r-- | src/database/tag_parser.cpp | 20 | ||||
| -rw-r--r-- | src/database/test/test_database.cpp | 80 | ||||
| -rw-r--r-- | src/database/test/test_records.cpp | 22 | ||||
| -rw-r--r-- | src/database/track.cpp (renamed from src/database/song.cpp) | 22 |
14 files changed, 390 insertions, 354 deletions
diff --git a/src/database/CMakeLists.txt b/src/database/CMakeLists.txt index 211a63cd..e7b1f62c 100644 --- a/src/database/CMakeLists.txt +++ b/src/database/CMakeLists.txt @@ -3,7 +3,7 @@ # SPDX-License-Identifier: GPL-3.0-only 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" REQUIRES "result" "span" "esp_psram" "fatfs" "libtags" "komihash" "cbor" "tasks") diff --git a/src/database/database.cpp b/src/database/database.cpp index 71954bbb..ac5e4873 100644 --- a/src/database/database.cpp +++ b/src/database/database.cpp @@ -17,6 +17,7 @@ #include "esp_log.h" #include "ff.h" +#include "freertos/projdefs.h" #include "leveldb/cache.h" #include "leveldb/db.h" #include "leveldb/iterator.h" @@ -28,16 +29,16 @@ #include "file_gatherer.hpp" #include "records.hpp" #include "result.hpp" -#include "song.hpp" #include "tag_parser.hpp" #include "tasks.hpp" +#include "track.hpp" namespace database { static SingletonEnv<leveldb::EspEnv> sEnv; 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); @@ -68,12 +69,13 @@ auto Database::Open(IFileGatherer* gatherer, ITagParser* parser) return cpp::fail(DatabaseError::ALREADY_OPEN); } + leveldb::sBackgroundThread.reset( + tasks::Worker::Start<tasks::Type::kDatabaseBackground>()); std::shared_ptr<tasks::Worker> worker( tasks::Worker::Start<tasks::Type::kDatabase>()); - leveldb::sBackgroundThread = std::weak_ptr<tasks::Worker>(worker); return worker ->Dispatch<cpp::result<Database*, DatabaseError>>( - [&]() -> cpp::result<Database*, DatabaseError> { + [=]() -> cpp::result<Database*, DatabaseError> { leveldb::DB* db; leveldb::Cache* cache = leveldb::NewLRUCache(24 * 1024); leveldb::Options options; @@ -121,15 +123,15 @@ Database::~Database() { delete db_; delete cache_; - leveldb::sBackgroundThread = std::weak_ptr<tasks::Worker>(); + leveldb::sBackgroundThread.reset(); sIsDbOpen.store(false); } auto Database::Update() -> std::future<void> { return worker_task_->Dispatch<void>([&]() -> void { - // Stage 1: verify all existing songs are still valid. - ESP_LOGI(kTag, "verifying existing songs"); + // Stage 1: verify all existing tracks are still valid. + ESP_LOGI(kTag, "verifying existing tracks"); const leveldb::Snapshot* snapshot = db_->GetSnapshot(); leveldb::ReadOptions read_options; read_options.fill_cache = false; @@ -138,8 +140,8 @@ auto Database::Update() -> std::future<void> { OwningSlice prefix = CreateDataPrefix(); it->Seek(prefix.slice); while (it->Valid() && it->key().starts_with(prefix.slice)) { - std::optional<SongData> song = ParseDataValue(it->value()); - if (!song) { + std::optional<TrackData> track = ParseDataValue(it->value()); + if (!track) { // The value was malformed. Drop this record. ESP_LOGW(kTag, "dropping malformed metadata"); db_->Delete(leveldb::WriteOptions(), it->key()); @@ -147,33 +149,33 @@ auto Database::Update() -> std::future<void> { continue; } - if (song->is_tombstoned()) { - ESP_LOGW(kTag, "skipping tombstoned %lx", song->id()); + if (track->is_tombstoned()) { + ESP_LOGW(kTag, "skipping tombstoned %lx", track->id()); it->Next(); continue; } - SongTags tags; - if (!tag_parser_->ReadAndParseTags(song->filepath(), &tags) || + TrackTags tags; + if (!tag_parser_->ReadAndParseTags(track->filepath(), &tags) || 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 // this record. - ESP_LOGW(kTag, "entombing missing #%lx", song->id()); - dbPutSongData(song->Entomb()); + ESP_LOGW(kTag, "entombing missing #%lx", track->id()); + dbPutTrackData(track->Entomb()); it->Next(); continue; } uint64_t new_hash = tags.Hash(); - if (new_hash != song->tags_hash()) { - // This song's tags have changed. Since the filepath is exactly the + if (new_hash != track->tags_hash()) { + // This track's tags have changed. Since the filepath is exactly the // same, we assume this is a legitimate correction. Update the // database. - ESP_LOGI(kTag, "updating hash (%llx -> %llx)", song->tags_hash(), + ESP_LOGI(kTag, "updating hash (%llx -> %llx)", track->tags_hash(), new_hash); - dbPutSongData(song->UpdateHash(new_hash)); - dbPutHash(new_hash, song->id()); + dbPutTrackData(track->UpdateHash(new_hash)); + dbPutHash(new_hash, track->id()); } it->Next(); @@ -182,9 +184,9 @@ auto Database::Update() -> std::future<void> { db_->ReleaseSnapshot(snapshot); // 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) { - SongTags tags; + TrackTags tags; if (!tag_parser_->ReadAndParseTags(path, &tags) || tags.encoding == Encoding::kUnsupported) { // No parseable tags; skip this fiile. @@ -194,32 +196,32 @@ auto Database::Update() -> std::future<void> { // Check for any existing record with the same hash. uint64_t hash = tags.Hash(); OwningSlice key = CreateHashKey(hash); - std::optional<SongId> existing_hash; + std::optional<TrackId> existing_hash; std::string raw_entry; if (db_->Get(leveldb::ReadOptions(), key.slice, &raw_entry).ok()) { existing_hash = ParseHashValue(raw_entry); } if (!existing_hash) { - // We've never met this song before! Or we have, but the entry is - // malformed. Either way, record this as a new song. - SongId id = dbMintNewSongId(); + // We've never met this track before! Or we have, but the entry is + // malformed. Either way, record this as a new track. + TrackId id = dbMintNewTrackId(); ESP_LOGI(kTag, "recording new 0x%lx", id); - dbPutSong(id, path, hash); + dbPutTrack(id, path, hash); return; } - std::optional<SongData> existing_data = dbGetSongData(*existing_hash); + std::optional<TrackData> existing_data = dbGetTrackData(*existing_hash); if (!existing_data) { // We found a hash that matches, but there's no data record? Weird. - SongData new_data(*existing_hash, path, hash); - dbPutSongData(new_data); + TrackData new_data(*existing_hash, path, hash); + dbPutTrackData(new_data); return; } if (existing_data->is_tombstoned()) { - ESP_LOGI(kTag, "exhuming song %lu", existing_data->id()); - dbPutSongData(existing_data->Exhume(path)); + ESP_LOGI(kTag, "exhuming track %lu", existing_data->id()); + dbPutTrackData(existing_data->Exhume(path)); } else if (existing_data->filepath() != path) { ESP_LOGW(kTag, "tag hash collision"); } @@ -227,14 +229,26 @@ auto Database::Update() -> std::future<void> { }); } -auto Database::GetSongs(std::size_t page_size) -> std::future<Result<Song>*> { - return worker_task_->Dispatch<Result<Song>*>([=, this]() -> Result<Song>* { - Continuation<Song> c{.iterator = nullptr, - .prefix = CreateDataPrefix().data, - .start_key = CreateDataPrefix().data, - .forward = true, - .was_prev_forward = true, - .page_size = page_size}; +auto Database::GetTrackPath(TrackId id) + -> std::future<std::optional<std::string>> { + return worker_task_->Dispatch<std::optional<std::string>>( + [=, this]() -> std::optional<std::string> { + auto track_data = dbGetTrackData(id); + if (track_data) { + return track_data->filepath(); + } + return {}; + }); +} + +auto Database::GetTracks(std::size_t page_size) -> std::future<Result<Track>*> { + return worker_task_->Dispatch<Result<Track>*>([=, this]() -> Result<Track>* { + Continuation<Track> c{.iterator = nullptr, + .prefix = CreateDataPrefix().data, + .start_key = CreateDataPrefix().data, + .forward = true, + .was_prev_forward = true, + .page_size = page_size}; return dbGetPage(c); }); } @@ -260,32 +274,32 @@ auto Database::GetPage(Continuation<T>* c) -> std::future<Result<T>*> { [=, this]() -> Result<T>* { return dbGetPage(copy); }); } -template auto Database::GetPage<Song>(Continuation<Song>* c) - -> std::future<Result<Song>*>; +template auto Database::GetPage<Track>(Continuation<Track>* c) + -> std::future<Result<Track>*>; template auto Database::GetPage<std::string>(Continuation<std::string>* c) -> std::future<Result<std::string>*>; -auto Database::dbMintNewSongId() -> SongId { - SongId next_id = 1; +auto Database::dbMintNewTrackId() -> TrackId { + TrackId next_id = 1; std::string val; - auto status = db_->Get(leveldb::ReadOptions(), kSongIdKey, &val); + auto status = db_->Get(leveldb::ReadOptions(), kTrackIdKey, &val); if (status.ok()) { - next_id = BytesToSongId(val).value_or(next_id); + next_id = BytesToTrackId(val).value_or(next_id); } else if (!status.IsNotFound()) { // 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, - SongIdToBytes(next_id + 1).slice) + if (!db_->Put(leveldb::WriteOptions(), kTrackIdKey, + TrackIdToBytes(next_id + 1).slice) .ok()) { - ESP_LOGE(kTag, "failed to write next song id"); + ESP_LOGE(kTag, "failed to write next track 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 val = CreateHashValue(id); if (!db_->Put(leveldb::WriteOptions(), key.slice, val.slice).ok()) { @@ -293,7 +307,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 val = CreateDataValue(s); if (!db_->Put(leveldb::WriteOptions(), key.slice, val.slice).ok()) { @@ -301,7 +315,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); std::string raw_val; if (!db_->Get(leveldb::ReadOptions(), key.slice, &raw_val).ok()) { @@ -311,7 +325,7 @@ auto Database::dbGetSongData(SongId id) -> std::optional<SongData> { 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 val = CreateHashValue(i); if (!db_->Put(leveldb::WriteOptions(), key.slice, val.slice).ok()) { @@ -319,7 +333,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); std::string raw_val; if (!db_->Get(leveldb::ReadOptions(), key.slice, &raw_val).ok()) { @@ -329,10 +343,10 @@ auto Database::dbGetHash(const uint64_t& hash) -> std::optional<SongId> { return ParseHashValue(raw_val); } -auto Database::dbPutSong(SongId id, - const std::string& path, - const uint64_t& hash) -> void { - dbPutSongData(SongData(id, path, hash)); +auto Database::dbPutTrack(TrackId id, + const std::string& path, + const uint64_t& hash) -> void { + dbPutTrackData(TrackData(id, path, hash)); dbPutHash(hash, id); } @@ -455,24 +469,24 @@ auto Database::dbGetPage(const Continuation<T>& c) -> Result<T>* { return new Result<T>(std::move(records), next_page, prev_page); } -template auto Database::dbGetPage<Song>(const Continuation<Song>& c) - -> Result<Song>*; +template auto Database::dbGetPage<Track>(const Continuation<Track>& c) + -> Result<Track>*; template auto Database::dbGetPage<std::string>( const Continuation<std::string>& c) -> Result<std::string>*; template <> -auto Database::ParseRecord<Song>(const leveldb::Slice& key, - const leveldb::Slice& val) - -> std::optional<Song> { - std::optional<SongData> data = ParseDataValue(val); +auto Database::ParseRecord<Track>(const leveldb::Slice& key, + const leveldb::Slice& val) + -> std::optional<Track> { + std::optional<TrackData> data = ParseDataValue(val); if (!data || data->is_tombstoned()) { return {}; } - SongTags tags; + TrackTags tags; if (!tag_parser_->ReadAndParseTags(data->filepath(), &tags)) { return {}; } - return Song(*data, tags); + return Track(*data, tags); } template <> diff --git a/src/database/env_esp.cpp b/src/database/env_esp.cpp index 704e0a54..ad1f2221 100644 --- a/src/database/env_esp.cpp +++ b/src/database/env_esp.cpp @@ -15,6 +15,7 @@ #include <cstring> #include <functional> #include <limits> +#include <memory> #include <mutex> #include <queue> #include <set> @@ -39,7 +40,7 @@ namespace leveldb { -std::weak_ptr<tasks::Worker> sBackgroundThread; +std::shared_ptr<tasks::Worker> sBackgroundThread; std::string ErrToStr(FRESULT err) { switch (err) { @@ -463,7 +464,7 @@ EspEnv::EspEnv() {} void EspEnv::Schedule( void (*background_work_function)(void* background_work_arg), void* background_work_arg) { - auto worker = sBackgroundThread.lock(); + auto worker = sBackgroundThread; if (worker) { worker->Dispatch<void>( [=]() { std::invoke(background_work_function, background_work_arg); }); diff --git a/src/database/include/database.hpp b/src/database/include/database.hpp index 5214b8df..8fecc5f6 100644 --- a/src/database/include/database.hpp +++ b/src/database/include/database.hpp @@ -23,9 +23,9 @@ #include "leveldb/slice.h" #include "records.hpp" #include "result.hpp" -#include "song.hpp" #include "tag_parser.hpp" #include "tasks.hpp" +#include "track.hpp" namespace database { @@ -82,7 +82,9 @@ class Database { auto Update() -> std::future<void>; - auto GetSongs(std::size_t page_size) -> std::future<Result<Song>*>; + auto GetTrackPath(TrackId id) -> std::future<std::optional<std::string>>; + + auto GetTracks(std::size_t page_size) -> std::future<Result<Track>*>; auto GetDump(std::size_t page_size) -> std::future<Result<std::string>*>; template <typename T> @@ -109,14 +111,14 @@ class Database { ITagParser* tag_parser, std::shared_ptr<tasks::Worker> worker); - auto dbMintNewSongId() -> SongId; - auto dbEntomb(SongId song, uint64_t hash) -> void; + auto dbMintNewTrackId() -> TrackId; + auto dbEntomb(TrackId track, uint64_t hash) -> void; - auto dbPutSongData(const SongData& s) -> void; - auto dbGetSongData(SongId id) -> std::optional<SongData>; - auto dbPutHash(const uint64_t& hash, SongId i) -> void; - auto dbGetHash(const uint64_t& hash) -> std::optional<SongId>; - auto dbPutSong(SongId id, const std::string& path, const uint64_t& hash) + auto dbPutTrackData(const TrackData& s) -> void; + auto dbGetTrackData(TrackId id) -> std::optional<TrackData>; + auto dbPutHash(const uint64_t& hash, TrackId i) -> void; + auto dbGetHash(const uint64_t& hash) -> std::optional<TrackId>; + auto dbPutTrack(TrackId id, const std::string& path, const uint64_t& hash) -> void; template <typename T> @@ -128,9 +130,9 @@ class Database { }; template <> -auto Database::ParseRecord<Song>(const leveldb::Slice& key, - const leveldb::Slice& val) - -> std::optional<Song>; +auto Database::ParseRecord<Track>(const leveldb::Slice& key, + const leveldb::Slice& val) + -> std::optional<Track>; template <> auto Database::ParseRecord<std::string>(const leveldb::Slice& key, const leveldb::Slice& val) diff --git a/src/database/include/env_esp.hpp b/src/database/include/env_esp.hpp index c7da6d91..eba6e8a9 100644 --- a/src/database/include/env_esp.hpp +++ b/src/database/include/env_esp.hpp @@ -18,7 +18,7 @@ namespace leveldb { -extern std::weak_ptr<tasks::Worker> sBackgroundThread; +extern std::shared_ptr<tasks::Worker> sBackgroundThread; // Tracks the files locked by EspEnv::LockFile(). // diff --git a/src/database/include/records.hpp b/src/database/include/records.hpp index 1b66ad42..95a1a1e8 100644 --- a/src/database/include/records.hpp +++ b/src/database/include/records.hpp @@ -13,7 +13,7 @@ #include "leveldb/db.h" #include "leveldb/slice.h" -#include "song.hpp" +#include "track.hpp" 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. */ auto CreateDataPrefix() -> OwningSlice; -/* Creates a data key for a song with the specified id. */ -auto CreateDataKey(const SongId& id) -> OwningSlice; +/* Creates a data key for a track with the specified id. */ +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. */ -auto CreateDataValue(const SongData& song) -> OwningSlice; +auto CreateDataValue(const TrackData& track) -> OwningSlice; /* - * Parses bytes previously encoded via CreateDataValue back into a SongData. May - * return nullopt if parsing fails. + * Parses bytes previously encoded via CreateDataValue back into a TrackData. + * 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. */ 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 * 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. */ -auto ParseHashValue(const leveldb::Slice&) -> std::optional<SongId>; +auto ParseHashValue(const leveldb::Slice&) -> std::optional<TrackId>; -/* Encodes a SongId as bytes. */ -auto SongIdToBytes(SongId id) -> OwningSlice; +/* Encodes a TrackId as bytes. */ +auto TrackIdToBytes(TrackId id) -> OwningSlice; /* - * Converts a song id encoded via SongIdToBytes back into a SongId. May return - * nullopt if parsing fails. + * Converts a track id encoded via TrackIdToBytes back into a TrackId. May + * 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 diff --git a/src/database/include/song.hpp b/src/database/include/song.hpp deleted file mode 100644 index d03660dc..00000000 --- a/src/database/include/song.hpp +++ /dev/null @@ -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 diff --git a/src/database/include/tag_parser.hpp b/src/database/include/tag_parser.hpp index 7dab93a1..4be5ad16 100644 --- a/src/database/include/tag_parser.hpp +++ b/src/database/include/tag_parser.hpp @@ -8,20 +8,20 @@ #include <string> -#include "song.hpp" +#include "track.hpp" namespace database { class ITagParser { public: virtual ~ITagParser() {} - virtual auto ReadAndParseTags(const std::string& path, SongTags* out) + virtual auto ReadAndParseTags(const std::string& path, TrackTags* out) -> bool = 0; }; class TagParserImpl : public ITagParser { public: - virtual auto ReadAndParseTags(const std::string& path, SongTags* out) + virtual auto ReadAndParseTags(const std::string& path, TrackTags* out) -> bool override; }; diff --git a/src/database/include/track.hpp b/src/database/include/track.hpp new file mode 100644 index 00000000..5a0c0ca8 --- /dev/null +++ b/src/database/include/track.hpp @@ -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 diff --git a/src/database/records.cpp b/src/database/records.cpp index f04e5da7..49e5db0b 100644 --- a/src/database/records.cpp +++ b/src/database/records.cpp @@ -14,7 +14,7 @@ #include "cbor.h" #include "esp_log.h" -#include "song.hpp" +#include "track.hpp" namespace database { @@ -60,14 +60,14 @@ auto CreateDataPrefix() -> OwningSlice { return OwningSlice({data, 2}); } -auto CreateDataKey(const SongId& id) -> OwningSlice { +auto CreateDataKey(const TrackId& id) -> OwningSlice { std::ostringstream output; output.put(kDataPrefix).put(kFieldSeparator); - output << SongIdToBytes(id).data; + output << TrackIdToBytes(id).data; return OwningSlice(output.str()); } -auto CreateDataValue(const SongData& song) -> OwningSlice { +auto CreateDataValue(const TrackData& track) -> OwningSlice { uint8_t* buf; std::size_t buf_len = cbor_encode(&buf, [&](CborEncoder* enc) { CborEncoder array_encoder; @@ -77,28 +77,28 @@ auto CreateDataValue(const SongData& song) -> OwningSlice { ESP_LOGE(kTag, "encoding err %u", err); return; } - err = cbor_encode_int(&array_encoder, song.id()); + err = cbor_encode_int(&array_encoder, track.id()); if (err != CborNoError && err != CborErrorOutOfMemory) { ESP_LOGE(kTag, "encoding err %u", err); return; } - err = cbor_encode_text_string(&array_encoder, song.filepath().c_str(), - song.filepath().size()); + err = cbor_encode_text_string(&array_encoder, track.filepath().c_str(), + track.filepath().size()); if (err != CborNoError && err != CborErrorOutOfMemory) { ESP_LOGE(kTag, "encoding err %u", err); return; } - err = cbor_encode_uint(&array_encoder, song.tags_hash()); + err = cbor_encode_uint(&array_encoder, track.tags_hash()); if (err != CborNoError && err != CborErrorOutOfMemory) { ESP_LOGE(kTag, "encoding err %u", err); return; } - err = cbor_encode_int(&array_encoder, song.play_count()); + err = cbor_encode_int(&array_encoder, track.play_count()); if (err != CborNoError && err != CborErrorOutOfMemory) { ESP_LOGE(kTag, "encoding err %u", err); return; } - err = cbor_encode_boolean(&array_encoder, song.is_tombstoned()); + err = cbor_encode_boolean(&array_encoder, track.is_tombstoned()); if (err != CborNoError && err != CborErrorOutOfMemory) { ESP_LOGE(kTag, "encoding err %u", err); return; @@ -114,7 +114,7 @@ auto CreateDataValue(const SongData& song) -> OwningSlice { return OwningSlice(as_str); } -auto ParseDataValue(const leveldb::Slice& slice) -> std::optional<SongData> { +auto ParseDataValue(const leveldb::Slice& slice) -> std::optional<TrackData> { CborParser parser; CborValue container; CborError err; @@ -135,7 +135,7 @@ auto ParseDataValue(const leveldb::Slice& slice) -> std::optional<SongData> { if (err != CborNoError) { return {}; } - SongId id = raw_int; + TrackId id = raw_int; err = cbor_value_advance(&val); if (err != CborNoError || !cbor_value_is_text_string(&val)) { return {}; @@ -176,7 +176,7 @@ auto ParseDataValue(const leveldb::Slice& slice) -> std::optional<SongData> { 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 { @@ -193,15 +193,15 @@ auto CreateHashKey(const uint64_t& hash) -> OwningSlice { return OwningSlice(output.str()); } -auto ParseHashValue(const leveldb::Slice& slice) -> std::optional<SongId> { - return BytesToSongId(slice.ToString()); +auto ParseHashValue(const leveldb::Slice& slice) -> std::optional<TrackId> { + return BytesToTrackId(slice.ToString()); } -auto CreateHashValue(SongId id) -> OwningSlice { - return SongIdToBytes(id); +auto CreateHashValue(TrackId id) -> OwningSlice { + return TrackIdToBytes(id); } -auto SongIdToBytes(SongId id) -> OwningSlice { +auto TrackIdToBytes(TrackId id) -> OwningSlice { uint8_t buf[8]; CborEncoder enc; cbor_encoder_init(&enc, buf, sizeof(buf), 0); @@ -211,7 +211,7 @@ auto SongIdToBytes(SongId id) -> OwningSlice { return OwningSlice(as_str); } -auto BytesToSongId(const std::string& bytes) -> std::optional<SongId> { +auto BytesToTrackId(const std::string& bytes) -> std::optional<TrackId> { CborParser parser; CborValue val; cbor_parser_init(reinterpret_cast<const uint8_t*>(bytes.data()), bytes.size(), diff --git a/src/database/tag_parser.cpp b/src/database/tag_parser.cpp index 27d4163f..83b0a796 100644 --- a/src/database/tag_parser.cpp +++ b/src/database/tag_parser.cpp @@ -17,7 +17,7 @@ namespace libtags { struct Aux { FIL file; FILINFO info; - SongTags* tags; + TrackTags* tags; }; static int read(Tagctx* ctx, void* buf, int cnt) { @@ -71,8 +71,14 @@ static void toc(Tagctx* ctx, int ms, int offset) {} static const std::size_t kBufSize = 1024; static const char* kTag = "TAGS"; -auto TagParserImpl::ReadAndParseTags(const std::string& path, SongTags* out) +auto TagParserImpl::ReadAndParseTags(const std::string& path, TrackTags* out) -> bool { + if (path.ends_with(".m4a")) { + // TODO(jacqueline): Re-enabled once libtags is fixed. + ESP_LOGW(kTag, "skipping m4a %s", path.c_str()); + return false; + } + libtags::Aux aux; aux.tags = out; if (f_stat(path.c_str(), &aux.info) != FR_OK || @@ -96,6 +102,7 @@ auto TagParserImpl::ReadAndParseTags(const std::string& path, SongTags* out) if (res != 0) { // Parsing failed. + ESP_LOGE(kTag, "tag parsing failed, reason %d", res); return false; } @@ -103,6 +110,15 @@ auto TagParserImpl::ReadAndParseTags(const std::string& path, SongTags* out) case Fmp3: out->encoding = Encoding::kMp3; break; + case Fogg: + out->encoding = Encoding::kOgg; + break; + case Fflac: + out->encoding = Encoding::kFlac; + break; + case Fwav: + out->encoding = Encoding::kWav; + break; default: out->encoding = Encoding::kUnsupported; } diff --git a/src/database/test/test_database.cpp b/src/database/test/test_database.cpp index ebaa6307..1ce364d9 100644 --- a/src/database/test/test_database.cpp +++ b/src/database/test/test_database.cpp @@ -18,41 +18,41 @@ #include "file_gatherer.hpp" #include "i2c_fixture.hpp" #include "leveldb/db.h" -#include "song.hpp" #include "spi_fixture.hpp" #include "tag_parser.hpp" +#include "track.hpp" namespace database { class TestBackends : public IFileGatherer, public ITagParser { public: - std::map<std::string, SongTags> songs; + std::map<std::string, TrackTags> tracks; - auto MakeSong(const std::string& path, const std::string& title) -> void { - SongTags tags; + auto MakeTrack(const std::string& path, const std::string& title) -> void { + TrackTags tags; tags.encoding = Encoding::kMp3; tags.title = title; - songs[path] = tags; + tracks[path] = tags; } auto FindFiles(const std::string& root, std::function<void(const std::string&)> cb) -> void override { - for (auto keyval : songs) { + for (auto keyval : tracks) { std::invoke(cb, keyval.first); } } - auto ReadAndParseTags(const std::string& path, SongTags* out) + auto ReadAndParseTags(const std::string& path, TrackTags* out) -> bool override { - if (songs.contains(path)) { - *out = songs.at(path); + if (tracks.contains(path)) { + *out = tracks.at(path); return true; } return false; } }; -TEST_CASE("song database", "[integration]") { +TEST_CASE("track database", "[integration]") { I2CFixture i2c; SpiFixture spi; drivers::DriverCache drivers; @@ -60,104 +60,104 @@ TEST_CASE("song database", "[integration]") { Database::Destroy(); - TestBackends songs; - auto open_res = Database::Open(&songs, &songs); + TestBackends tracks; + auto open_res = Database::Open(&tracks, &tracks); REQUIRE(open_res.has_value()); std::unique_ptr<Database> db(open_res.value()); 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); } - SECTION("add new songs") { - songs.MakeSong("song1.mp3", "Song 1"); - songs.MakeSong("song2.wav", "Song 2"); - songs.MakeSong("song3.exe", "Song 3"); + SECTION("add new tracks") { + tracks.MakeTrack("track1.mp3", "Track 1"); + tracks.MakeTrack("track2.wav", "Track 2"); + tracks.MakeTrack("track3.exe", "Track 3"); 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); - 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(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(2).tags().title == "Song 3"); + CHECK(*res->values().at(2).tags().title == "Track 3"); CHECK(res->values().at(2).data().id() == 3); SECTION("update with no filesystem changes") { 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); CHECK(res->values().at(0) == new_res->values().at(0)); CHECK(res->values().at(1) == new_res->values().at(1)); CHECK(res->values().at(2) == new_res->values().at(2)); } - SECTION("update with all songs gone") { - songs.songs.clear(); + SECTION("update with all tracks gone") { + tracks.tracks.clear(); 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); - SECTION("update with one song returned") { - songs.MakeSong("song2.wav", "Song 2"); + SECTION("update with one track returned") { + tracks.MakeTrack("track2.wav", "Track 2"); 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); CHECK(res->values().at(1) == new_res->values().at(0)); } } - SECTION("update with one song gone") { - songs.songs.erase("song2.wav"); + SECTION("update with one track gone") { + tracks.tracks.erase("track2.wav"); 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); CHECK(res->values().at(0) == new_res->values().at(0)); CHECK(res->values().at(2) == new_res->values().at(1)); } SECTION("update with tags changed") { - songs.MakeSong("song3.exe", "The Song 3"); + tracks.MakeTrack("track3.exe", "The Track 3"); 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); CHECK(res->values().at(0) == new_res->values().at(0)); 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. CHECK(res->values().at(2).data().id() == new_res->values().at(2).data().id()); } - SECTION("update with one new song") { - songs.MakeSong("my song.midi", "Song 1 (nightcore remix)"); + SECTION("update with one new track") { + tracks.MakeTrack("my track.midi", "Track 1 (nightcore remix)"); 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); CHECK(res->values().at(0) == new_res->values().at(0)); CHECK(res->values().at(1) == new_res->values().at(1)); CHECK(res->values().at(2) == new_res->values().at(2)); 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); } - SECTION("get songs with pagination") { - std::unique_ptr<Result<Song>> res(db->GetSongs(1).get()); + SECTION("get tracks with pagination") { + std::unique_ptr<Result<Track>> res(db->GetTracks(1).get()); REQUIRE(res->values().size() == 1); CHECK(res->values().at(0).data().id() == 1); diff --git a/src/database/test/test_records.cpp b/src/database/test/test_records.cpp index ca518458..5729003e 100644 --- a/src/database/test/test_records.cpp +++ b/src/database/test/test_records.cpp @@ -25,9 +25,9 @@ std::string ToHex(const std::string& s) { namespace database { TEST_CASE("database record encoding", "[unit]") { - SECTION("song id to bytes") { - SongId id = 1234678; - OwningSlice as_bytes = SongIdToBytes(id); + SECTION("track id to bytes") { + TrackId id = 1234678; + OwningSlice as_bytes = TrackIdToBytes(id); SECTION("encodes correctly") { // 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") { - CHECK(*BytesToSongId(as_bytes.data) == id); + CHECK(*BytesToTrackId(as_bytes.data) == id); } SECTION("encodes compactly") { - OwningSlice small_id = SongIdToBytes(1); - OwningSlice large_id = SongIdToBytes(999999); + OwningSlice small_id = TrackIdToBytes(1); + OwningSlice large_id = TrackIdToBytes(999999); CHECK(small_id.data.size() < large_id.data.size()); } 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); } @@ -73,7 +73,7 @@ TEST_CASE("database record encoding", "[unit]") { } 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); @@ -109,7 +109,7 @@ TEST_CASE("database record encoding", "[unit]") { } SECTION("decoding rejects garbage") { - std::optional<SongData> res = ParseDataValue("hi!"); + std::optional<TrackData> res = ParseDataValue("hi!"); CHECK(res.has_value() == false); } @@ -129,14 +129,14 @@ TEST_CASE("database record encoding", "[unit]") { SECTION("hash values") { OwningSlice val = CreateHashValue(123456); - CHECK(val.data == SongIdToBytes(123456).data); + CHECK(val.data == TrackIdToBytes(123456).data); SECTION("round-trips") { CHECK(ParseHashValue(val.slice) == 123456); } 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); } diff --git a/src/database/song.cpp b/src/database/track.cpp index c717e55e..00acc1f6 100644 --- a/src/database/song.cpp +++ b/src/database/track.cpp @@ -4,7 +4,7 @@ * SPDX-License-Identifier: GPL-3.0-only */ -#include "song.hpp" +#include "track.hpp" #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 * memory footprint a little so that it's safe to call from any stack. */ -auto SongTags::Hash() const -> uint64_t { - // TODO(jacqueline): this function doesn't work very well for songs with no +auto TrackTags::Hash() const -> uint64_t { + // TODO(jacqueline): this function doesn't work very well for tracks with no // tags at all. komihash_stream_t stream; komihash_stream_init(&stream, 0); @@ -30,20 +30,20 @@ auto SongTags::Hash() const -> uint64_t { return komihash_stream_final(&stream); } -auto SongData::UpdateHash(uint64_t new_hash) const -> SongData { - return SongData(id_, filepath_, new_hash, play_count_, is_tombstoned_); +auto TrackData::UpdateHash(uint64_t new_hash) const -> TrackData { + return TrackData(id_, filepath_, new_hash, play_count_, is_tombstoned_); } -auto SongData::Entomb() const -> SongData { - return SongData(id_, filepath_, tags_hash_, play_count_, true); +auto TrackData::Entomb() const -> TrackData { + return TrackData(id_, filepath_, tags_hash_, play_count_, true); } -auto SongData::Exhume(const std::string& new_path) const -> SongData { - return SongData(id_, new_path, tags_hash_, play_count_, false); +auto TrackData::Exhume(const std::string& new_path) const -> TrackData { + return TrackData(id_, new_path, tags_hash_, play_count_, false); } -void swap(Song& first, Song& second) { - Song temp = first; +void swap(Track& first, Track& second) { + Track temp = first; first = second; second = temp; } |
