#include "BookListStore.h" #include #include #include #include #include namespace { constexpr const char* LOG_TAG = "BLS"; constexpr uint8_t LIST_FILE_VERSION = 1; // Helper to trim whitespace from a string std::string trim(const std::string& str) { const size_t first = str.find_first_not_of(" \t\r\n"); if (first == std::string::npos) return ""; const size_t last = str.find_last_not_of(" \t\r\n"); return str.substr(first, last - first + 1); } // Helper to parse a single CSV line // Format: order,Title,Author,/path/to/file.epub bool parseCsvLine(const std::string& line, BookListItem& item) { if (line.empty()) return false; // Find first comma (after order) size_t pos1 = line.find(','); if (pos1 == std::string::npos) return false; // Find second comma (after title) size_t pos2 = line.find(',', pos1 + 1); if (pos2 == std::string::npos) return false; // Find third comma (after author) size_t pos3 = line.find(',', pos2 + 1); if (pos3 == std::string::npos) return false; // Parse order std::string orderStr = trim(line.substr(0, pos1)); int order = atoi(orderStr.c_str()); if (order <= 0 || order > 255) return false; item.order = static_cast(order); // Parse title, author, path item.title = trim(line.substr(pos1 + 1, pos2 - pos1 - 1)); item.author = trim(line.substr(pos2 + 1, pos3 - pos2 - 1)); item.path = trim(line.substr(pos3 + 1)); // Validate we have at least a path if (item.path.empty()) return false; return true; } } // namespace bool BookListStore::parseFromText(const std::string& text, BookList& list) { list.books.clear(); std::istringstream stream(text); std::string line; while (std::getline(stream, line)) { line = trim(line); if (line.empty()) continue; BookListItem item; if (parseCsvLine(line, item)) { list.books.push_back(item); } else { Serial.printf("[%lu] [%s] Failed to parse line: %s\n", millis(), LOG_TAG, line.c_str()); } } // Sort by order std::sort(list.books.begin(), list.books.end(), [](const BookListItem& a, const BookListItem& b) { return a.order < b.order; }); Serial.printf("[%lu] [%s] Parsed %d books from text\n", millis(), LOG_TAG, list.books.size()); return !list.books.empty(); } bool BookListStore::saveList(const BookList& list) { if (list.name.empty()) { Serial.printf("[%lu] [%s] Cannot save list with empty name\n", millis(), LOG_TAG); return false; } // Ensure lists directory exists SdMan.mkdir(LISTS_DIR); const std::string path = getListPath(list.name); FsFile outputFile; if (!SdMan.openFileForWrite(LOG_TAG, path, outputFile)) { Serial.printf("[%lu] [%s] Failed to open file for writing: %s\n", millis(), LOG_TAG, path.c_str()); return false; } // Write version serialization::writePod(outputFile, LIST_FILE_VERSION); // Write book count const uint8_t count = static_cast(std::min(list.books.size(), size_t(255))); serialization::writePod(outputFile, count); // Write each book for (size_t i = 0; i < count; i++) { const auto& book = list.books[i]; serialization::writePod(outputFile, book.order); serialization::writeString(outputFile, book.title); serialization::writeString(outputFile, book.author); serialization::writeString(outputFile, book.path); } outputFile.close(); Serial.printf("[%lu] [%s] Saved list '%s' with %d books to %s\n", millis(), LOG_TAG, list.name.c_str(), count, path.c_str()); return true; } bool BookListStore::loadList(const std::string& name, BookList& list) { const std::string path = getListPath(name); FsFile inputFile; if (!SdMan.openFileForRead(LOG_TAG, path, inputFile)) { Serial.printf("[%lu] [%s] Failed to open file for reading: %s\n", millis(), LOG_TAG, path.c_str()); return false; } // Read version uint8_t version; serialization::readPod(inputFile, version); if (version != LIST_FILE_VERSION) { Serial.printf("[%lu] [%s] Unknown file version: %d\n", millis(), LOG_TAG, version); inputFile.close(); return false; } // Read book count uint8_t count; serialization::readPod(inputFile, count); list.name = name; list.books.clear(); list.books.reserve(count); // Read each book for (uint8_t i = 0; i < count; i++) { BookListItem item; serialization::readPod(inputFile, item.order); serialization::readString(inputFile, item.title); serialization::readString(inputFile, item.author); serialization::readString(inputFile, item.path); list.books.push_back(item); } inputFile.close(); // Sort by order (should already be sorted, but ensure it) std::sort(list.books.begin(), list.books.end(), [](const BookListItem& a, const BookListItem& b) { return a.order < b.order; }); Serial.printf("[%lu] [%s] Loaded list '%s' with %d books\n", millis(), LOG_TAG, name.c_str(), count); return true; } bool BookListStore::deleteList(const std::string& name) { const std::string path = getListPath(name); if (!SdMan.exists(path.c_str())) { Serial.printf("[%lu] [%s] List not found: %s\n", millis(), LOG_TAG, path.c_str()); return false; } if (!SdMan.remove(path.c_str())) { Serial.printf("[%lu] [%s] Failed to delete list: %s\n", millis(), LOG_TAG, path.c_str()); return false; } Serial.printf("[%lu] [%s] Deleted list: %s\n", millis(), LOG_TAG, name.c_str()); return true; } std::vector BookListStore::listAllLists() { std::vector lists; FsFile dir = SdMan.open(LISTS_DIR); if (!dir || !dir.isDirectory()) { if (dir) dir.close(); return lists; } char name[128]; FsFile entry; while ((entry = dir.openNextFile())) { if (!entry.isDirectory()) { entry.getName(name, sizeof(name)); std::string filename(name); // Only include .bin files if (filename.length() > 4 && filename.substr(filename.length() - 4) == ".bin") { // Strip .bin extension lists.push_back(filename.substr(0, filename.length() - 4)); } } entry.close(); } dir.close(); // Sort alphabetically std::sort(lists.begin(), lists.end()); return lists; } bool BookListStore::listExists(const std::string& name) { const std::string path = getListPath(name); return SdMan.exists(path.c_str()); } std::string BookListStore::getListPath(const std::string& name) { return std::string(LISTS_DIR) + "/" + name + ".bin"; } int BookListStore::getBookCount(const std::string& name) { const std::string path = getListPath(name); FsFile inputFile; if (!SdMan.openFileForRead(LOG_TAG, path, inputFile)) { return -1; } // Read version uint8_t version; serialization::readPod(inputFile, version); if (version != LIST_FILE_VERSION) { inputFile.close(); return -1; } // Read book count uint8_t count; serialization::readPod(inputFile, count); inputFile.close(); return count; }