Shared Catalog

This commit is contained in:
khodowany
2026-09-17 00:44:19 -05:00
parent aa1273b07d
commit 5941a4cd49
44 changed files with 3613 additions and 733 deletions
+120 -220
View File
@@ -6,6 +6,7 @@
#include <string>
#include <string_view>
#include <system_error>
#include <tuple>
#include <utility>
#include <vector>
@@ -22,9 +23,6 @@ auto invalid(std::string message) -> modulus::error {
}
auto human_label(std::string value) -> std::string {
if (value.empty()) {
return "Score";
}
auto capitalize_next = std::bool_t{true};
for (auto &ch : value) {
if (ch == '-' || ch == '_') {
@@ -37,31 +35,57 @@ auto human_label(std::string value) -> std::string {
}
capitalize_next = false;
}
return value;
return value.empty() ? "Score" : value;
}
auto practice_type_label(std::string const &value) -> std::string {
return human_label(value) + " Practice";
auto key_rank(std::string const &key) -> std::size_t {
auto const &keys = vamp_zero::practice_key_names();
auto tonic = key.substr(0, key.find(' '));
return static_cast<std::size_t>(std::distance(keys.begin(), std::find(keys.begin(), keys.end(), tonic)));
}
auto load_score_documents(std::filesystem::path const &directory, std::bool_t required) -> std::expected<std::vector<vamp_zero::score_document>, modulus::error> {
}
auto vamp_zero::parse_score_document(std::string_view json, std::string source_path) -> std::expected<vamp_zero::score_document, modulus::error> {
return vamp_zero::parse_notation_document(json, std::move(source_path));
}
auto vamp_zero::read_score_document(std::filesystem::path const &path) -> std::expected<vamp_zero::score_document, modulus::error> {
auto input = std::ifstream{path, std::ios::binary};
if (!input) {
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::NOT_FOUND, "failed to open score JSON: " + path.string()));
}
auto text = std::string{std::istreambuf_iterator<std::char_t>{input}, std::istreambuf_iterator<std::char_t>{}};
if (input.bad()) {
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::UNAVAILABLE, "failed to read score JSON: " + path.string()));
}
return vamp_zero::parse_score_document(text, path.string());
}
auto vamp_zero::score_paths(std::string const &assets_path, vamp_zero::score_category category) -> std::expected<std::vector<std::filesystem::path>, modulus::error> {
auto practice = category == vamp_zero::score_category::PRACTICE;
auto root = std::filesystem::path{assets_path} / (practice ? "practice" : "songs");
auto error = std::error_code{};
auto exists = std::filesystem::exists(directory, error);
auto exists = std::filesystem::exists(root, error);
if (error) {
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::UNAVAILABLE, "failed to inspect score assets: " + error.message()));
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::UNAVAILABLE, root.string() + ": " + error.message()));
}
if (!exists) {
if (required) {
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::NOT_FOUND, "score asset directory not found: " + directory.string()));
if (practice) {
return std::vector<std::filesystem::path>{};
}
return std::vector<vamp_zero::score_document>{};
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::NOT_FOUND, "score asset directory not found: " + root.string()));
}
auto paths = std::vector<std::filesystem::path>{};
auto entries = std::filesystem::directory_iterator{directory, error};
auto end = std::filesystem::directory_iterator{};
auto entries = std::filesystem::recursive_directory_iterator{root, error};
auto end = std::filesystem::recursive_directory_iterator{};
while (!error && entries != end) {
auto const &entry = *entries;
if (entry.is_regular_file(error) && entry.path().extension() == ".json") {
auto relative = entry.path().lexically_relative(root);
if (std::distance(relative.begin(), relative.end()) != (practice ? 4 : 1)) {
return std::unexpected(invalid("unexpected score asset path: " + entry.path().string()));
}
paths.push_back(entry.path());
}
if (!error) {
@@ -72,255 +96,131 @@ auto load_score_documents(std::filesystem::path const &directory, std::bool_t re
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::UNAVAILABLE, "failed to scan score assets: " + error.message()));
}
std::sort(paths.begin(), paths.end());
auto documents = std::vector<vamp_zero::score_document>{};
for (auto const &path : paths) {
auto input = std::ifstream{path, std::ios::binary};
if (!input) {
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::NOT_FOUND, "failed to open score JSON: " + path.string()));
}
auto text = std::string{std::istreambuf_iterator<std::char_t>{input}, std::istreambuf_iterator<std::char_t>{}};
auto document = vamp_zero::parse_score_document(text, path.string());
if (!document) {
return std::unexpected(document.error());
}
documents.push_back(std::move(*document));
}
return documents;
return paths;
}
auto path_parts(std::filesystem::path const &path) -> std::vector<std::string> {
auto vamp_zero::performance_metadata(vamp_zero::score_document const &document) -> vamp_zero::library_part {
return {
.id = document.id,
.title = document.title,
.composer = document.composer,
.key_signature = document.key_signature,
.part = document.part,
.source_path = document.source_path,
.measure_count = document.measure_count,
.total_pages = vamp_zero::score_page_count(document),
};
}
auto vamp_zero::practice_metadata(vamp_zero::score_document const &document, std::string const &assets_path) -> std::expected<vamp_zero::practice_exercise, modulus::error> {
auto relative = std::filesystem::path{document.source_path}.lexically_relative(std::filesystem::path{assets_path} / "practice");
auto parts = std::vector<std::string>{};
for (auto const &part : path) {
for (auto const &part : relative) {
parts.push_back(part.string());
}
return parts;
}
auto key_rank(std::string const &key) -> std::size_t {
static auto const keys = std::vector<std::string>{
"C",
"Db",
"D",
"Eb",
"E",
"F",
"Gb",
"G",
"Ab",
"A",
"Bb",
"B",
};
auto tonic = key.substr(0, key.find(' '));
auto found = std::find(keys.begin(), keys.end(), tonic);
return found == keys.end() ? keys.size() : static_cast<std::size_t>(std::distance(keys.begin(), found));
}
auto sync_primary_variant(vamp_zero::practice_exercise &exercise) -> void {
std::sort(exercise.variants.begin(), exercise.variants.end(), [](auto const &left, auto const &right) {
auto left_rank = key_rank(left.key_signature);
auto right_rank = key_rank(right.key_signature);
if (left_rank != right_rank) {
return left_rank < right_rank;
}
return left.id < right.id;
});
if (exercise.variants.empty()) {
return;
if (parts.size() != 4U) {
return std::unexpected(invalid("practice JSON must be stored as practice/<type>/<lesson_group>/<exercise>/<key>.json: " + document.source_path));
}
auto const &primary = exercise.variants.front();
exercise.key_signature = primary.key_signature;
exercise.source_path = primary.source_path;
exercise.source_json = primary.source_json;
exercise.measure_count = primary.measure_count;
exercise.total_pages = primary.total_pages;
}
auto same_practice_exercise(vamp_zero::practice_exercise const &left, vamp_zero::practice_exercise const &right) -> std::bool_t {
return left.id == right.id && left.practice_type_id == right.practice_type_id &&
left.lesson_group_id == right.lesson_group_id;
}
}
auto vamp_zero::parse_score_document(std::string_view json, std::string source_path) -> std::expected<vamp_zero::score_document, modulus::error> {
return vamp_zero::parse_notation_document(json, std::move(source_path));
auto id = parts[0] + "-" + parts[1] + "-" + parts[2];
return vamp_zero::practice_exercise{
.id = id,
.practice_type_id = parts[0],
.practice_type_title = human_label(parts[0]) + " Practice",
.lesson_group_id = parts[1],
.lesson_group_title = human_label(parts[1]),
.title = document.title,
.composer = document.composer,
.authored_key_signature = document.authored_key_signature,
.variants = {{
.id = id + "-" + std::filesystem::path{parts[3]}.stem().string(),
.key_signature = document.key_signature,
.source_path = document.source_path,
.measure_count = document.measure_count,
.total_pages = vamp_zero::score_page_count(document),
}},
};
}
auto vamp_zero::group_library_parts(std::vector<vamp_zero::library_part> parts) -> vamp_zero::library_state {
std::sort(parts.begin(), parts.end(), [](auto const &left, auto const &right) {
if (left.title != right.title) {
return left.title < right.title;
}
return left.part < right.part;
return std::tie(left.title, left.composer, left.part, left.id) < std::tie(right.title, right.composer, right.part, right.id);
});
auto groups = std::vector<vamp_zero::song_group>{};
for (auto const &part : parts) {
for (auto index = std::size_t{}; index < parts.size(); ++index) {
auto const &part = parts[index];
if (groups.empty() || groups.back().title != part.title || groups.back().composer != part.composer) {
groups.push_back({
.title = part.title,
.composer = part.composer,
});
groups.push_back({.title = part.title, .composer = part.composer});
}
groups.back().parts.push_back(part);
groups.back().parts.push_back(index);
}
return vamp_zero::library_state{
.parts = std::move(parts),
.groups = std::move(groups),
};
return {.parts = std::move(parts), .groups = std::move(groups)};
}
auto vamp_zero::group_practice_exercises(std::vector<vamp_zero::practice_exercise> exercises) -> vamp_zero::practice_library {
auto vamp_zero::group_practice_exercises(std::vector<vamp_zero::practice_exercise> exercises) -> std::expected<vamp_zero::practice_library, modulus::error> {
std::sort(exercises.begin(), exercises.end(), [](auto const &left, auto const &right) {
if (left.practice_type_id != right.practice_type_id) {
return left.practice_type_id < right.practice_type_id;
}
if (left.lesson_group_id != right.lesson_group_id) {
return left.lesson_group_id < right.lesson_group_id;
}
if (left.id != right.id) {
return left.id < right.id;
}
if (key_rank(left.key_signature) != key_rank(right.key_signature)) {
return key_rank(left.key_signature) < key_rank(right.key_signature);
}
return left.source_path < right.source_path;
return std::tie(left.practice_type_id, left.lesson_group_id, left.id) < std::tie(right.practice_type_id, right.lesson_group_id, right.id);
});
auto merged = std::vector<vamp_zero::practice_exercise>{};
for (auto &exercise : exercises) {
if (!merged.empty() && same_practice_exercise(merged.back(), exercise)) {
if (!merged.empty() && std::tie(merged.back().practice_type_id, merged.back().lesson_group_id, merged.back().id) == std::tie(exercise.practice_type_id, exercise.lesson_group_id, exercise.id)) {
if (merged.back().authored_key_signature != exercise.authored_key_signature) {
return std::unexpected(invalid("practice variants disagree on authored key: " + exercise.id));
}
merged.back().variants.insert(merged.back().variants.end(), std::make_move_iterator(exercise.variants.begin()), std::make_move_iterator(exercise.variants.end()));
sync_primary_variant(merged.back());
continue;
} else {
merged.push_back(std::move(exercise));
}
sync_primary_variant(exercise);
merged.push_back(std::move(exercise));
}
auto types = std::vector<vamp_zero::practice_type_group>{};
for (auto const &exercise : merged) {
auto type =
std::find_if(types.begin(), types.end(), [&](auto const &item) { return item.id == exercise.practice_type_id; });
if (type == types.end()) {
types.push_back({
.id = exercise.practice_type_id,
.title = exercise.practice_type_title,
});
type = std::prev(types.end());
}
auto lesson = std::find_if(type->lesson_groups.begin(), type->lesson_groups.end(), [&](auto const &item) {
return item.id == exercise.lesson_group_id;
for (auto index = std::size_t{}; index < merged.size(); ++index) {
auto &exercise = merged[index];
std::sort(exercise.variants.begin(), exercise.variants.end(), [](auto const &left, auto const &right) {
return std::tuple{key_rank(left.key_signature), left.id} < std::tuple{key_rank(right.key_signature), right.id};
});
if (lesson == type->lesson_groups.end()) {
type->lesson_groups.push_back({
.id = exercise.lesson_group_id,
.title = exercise.lesson_group_title,
});
lesson = std::prev(type->lesson_groups.end());
if (types.empty() || types.back().id != exercise.practice_type_id) {
types.push_back({.id = exercise.practice_type_id, .title = exercise.practice_type_title});
}
lesson->exercises.push_back(exercise);
auto &lessons = types.back().lesson_groups;
if (lessons.empty() || lessons.back().id != exercise.lesson_group_id) {
lessons.push_back({.id = exercise.lesson_group_id, .title = exercise.lesson_group_title});
}
lessons.back().exercises.push_back(index);
}
return vamp_zero::practice_library{
.exercises = std::move(merged),
.types = std::move(types),
};
return vamp_zero::practice_library{.exercises = std::move(merged), .types = std::move(types)};
}
auto vamp_zero::load_performance_library(std::string const &assets_path) -> std::expected<vamp_zero::library_state, modulus::error> {
auto documents = load_score_documents(std::filesystem::path{assets_path} / "songs", true);
if (!documents) {
return std::unexpected(documents.error());
auto paths = vamp_zero::score_paths(assets_path, vamp_zero::score_category::PERFORMANCE);
if (!paths) {
return std::unexpected(paths.error());
}
auto parts = std::vector<vamp_zero::library_part>{};
for (auto &document : *documents) {
parts.push_back({
.id = document.id,
.title = document.title,
.composer = document.composer,
.key_signature = document.key_signature,
.part = document.part,
.source_path = document.source_path,
.source_json = std::move(document.source_json),
.measure_count = document.measure_count,
.total_pages = vamp_zero::score_page_count(document),
});
for (auto const &path : *paths) {
auto document = vamp_zero::read_score_document(path);
if (!document) {
return std::unexpected(document.error());
}
parts.push_back(vamp_zero::performance_metadata(*document));
}
return vamp_zero::group_library_parts(std::move(parts));
}
auto vamp_zero::load_practice_library(std::string const &assets_path) -> std::expected<vamp_zero::practice_library, modulus::error> {
auto root = std::filesystem::path{assets_path} / "practice";
auto error = std::error_code{};
auto exists = std::filesystem::exists(root, error);
if (error) {
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::UNAVAILABLE, "failed to inspect practice assets: " + error.message()));
auto paths = vamp_zero::score_paths(assets_path, vamp_zero::score_category::PRACTICE);
if (!paths) {
return std::unexpected(paths.error());
}
if (!exists) {
return vamp_zero::practice_library{};
}
auto paths = std::vector<std::filesystem::path>{};
auto entries = std::filesystem::recursive_directory_iterator{root, error};
auto end = std::filesystem::recursive_directory_iterator{};
while (!error && entries != end) {
auto const &entry = *entries;
if (entry.is_regular_file(error) && entry.path().extension() == ".json") {
paths.push_back(entry.path());
}
if (!error) {
entries.increment(error);
}
}
if (error) {
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::UNAVAILABLE, "failed to scan practice assets: " + error.message()));
}
std::sort(paths.begin(), paths.end());
auto exercises = std::vector<vamp_zero::practice_exercise>{};
for (auto const &path : paths) {
auto relative = std::filesystem::relative(path, root, error);
if (error) {
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::UNAVAILABLE, "failed to inspect practice asset path: " + error.message()));
}
auto parts = path_parts(relative);
if (parts.size() != 4U) {
return std::unexpected(invalid("practice JSON must be stored as practice/<type>/<lesson_group>/<exercise>/<key>.json: " + path.string()));
}
auto input = std::ifstream{path, std::ios::binary};
if (!input) {
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::NOT_FOUND, "failed to open practice JSON: " + path.string()));
}
auto text = std::string{std::istreambuf_iterator<std::char_t>{input}, std::istreambuf_iterator<std::char_t>{}};
auto document = vamp_zero::parse_score_document(text, path.string());
for (auto const &path : *paths) {
auto document = vamp_zero::read_score_document(path);
if (!document) {
return std::unexpected(document.error());
}
auto type_id = std::move(parts[0]);
auto lesson_id = std::move(parts[1]);
auto exercise_slug = std::move(parts[2]);
auto key = document->key_signature.empty() ? std::string{"C"} : document->key_signature;
auto exercise_id = type_id + "-" + lesson_id + "-" + exercise_slug;
auto variant_id = exercise_id + "-" + std::filesystem::path{parts[3]}.stem().string();
auto variant = vamp_zero::practice_exercise::score_variant{
.id = std::move(variant_id),
.key_signature = key,
.source_path = document->source_path,
.source_json = document->source_json,
.measure_count = document->measure_count,
.total_pages = vamp_zero::score_page_count(*document),
};
exercises.push_back({
.id = std::move(exercise_id),
.practice_type_id = type_id,
.practice_type_title = practice_type_label(type_id),
.lesson_group_id = lesson_id,
.lesson_group_title = human_label(lesson_id),
.title = document->title,
.composer = document->composer,
.key_signature = variant.key_signature,
.source_path = variant.source_path,
.source_json = variant.source_json,
.measure_count = variant.measure_count,
.total_pages = variant.total_pages,
.variants = {std::move(variant)},
});
auto exercise = vamp_zero::practice_metadata(*document, assets_path);
if (!exercise) {
return std::unexpected(exercise.error());
}
exercises.push_back(std::move(*exercise));
}
return vamp_zero::group_practice_exercises(std::move(exercises));
}