LibraryIndex.cpp
1 #include "LibraryIndex.h" 2 3 #include <Arduino.h> 4 #include <SdFat.h> 5 #include <Serialization.h> 6 7 #include <algorithm> 8 #include <cctype> 9 #include <cstring> 10 11 #include "../config.h" 12 #include "../content/ContentHandle.h" 13 #include "../content/ProgressManager.h" 14 #include "../core/Core.h" 15 16 namespace signalos { 17 18 // Library index file path 19 static constexpr const char* LIBRARY_INDEX_PATH = SIGNALOS_DIR "/library.idx"; 20 21 // ============================================================================ 22 // Hash and Key Extraction 23 // ============================================================================ 24 25 uint32_t LibraryIndex::hashPath(const char* path) { 26 if (!path) return 0; 27 28 uint32_t hash = FNV_OFFSET_BASIS; 29 while (*path) { 30 hash ^= static_cast<uint8_t>(*path); 31 hash *= FNV_PRIME; 32 path++; 33 } 34 return hash; 35 } 36 37 void LibraryIndex::extractTitleKey(const char* title, uint8_t* key6) { 38 memset(key6, 0, 6); 39 if (!title) return; 40 41 // Skip leading "The ", "A ", "An " for better sorting 42 if (strncasecmp(title, "the ", 4) == 0) 43 title += 4; 44 else if (strncasecmp(title, "a ", 2) == 0) 45 title += 2; 46 else if (strncasecmp(title, "an ", 3) == 0) 47 title += 3; 48 49 size_t i = 0; 50 while (i < 6 && *title) { 51 key6[i++] = static_cast<uint8_t>(std::tolower(static_cast<unsigned char>(*title))); 52 title++; 53 } 54 } 55 56 void LibraryIndex::extractAuthorKey(const char* author, uint8_t* key4) { 57 memset(key4, 0, 4); 58 if (!author) return; 59 60 // Try to find last name (last space before end) 61 const char* lastName = author; 62 const char* p = author; 63 while (*p) { 64 if (*p == ' ' && *(p + 1)) { 65 lastName = p + 1; 66 } 67 p++; 68 } 69 70 size_t i = 0; 71 while (i < 4 && *lastName) { 72 key4[i++] = static_cast<uint8_t>(std::tolower(static_cast<unsigned char>(*lastName))); 73 lastName++; 74 } 75 } 76 77 // ============================================================================ 78 // Comparison Functions 79 // ============================================================================ 80 81 int LibraryEntry::compareTitleKey(const LibraryEntry& a, const LibraryEntry& b) { 82 return memcmp(a.titleKey, b.titleKey, 6); 83 } 84 85 int LibraryEntry::compareAuthorKey(const LibraryEntry& a, const LibraryEntry& b) { 86 return memcmp(a.authorKey, b.authorKey, 4); 87 } 88 89 // ============================================================================ 90 // Load/Save Index 91 // ============================================================================ 92 93 bool LibraryIndex::load(Core& core) { 94 FsFile file; 95 auto result = core.storage.openRead(LIBRARY_INDEX_PATH, file); 96 if (!result.ok()) { 97 Serial.println("[LIBIDX] No index file, will rebuild"); 98 return false; 99 } 100 101 // Read and validate header 102 LibraryIndexHeader header; 103 if (file.read(reinterpret_cast<uint8_t*>(&header), sizeof(header)) != sizeof(header)) { 104 Serial.println("[LIBIDX] Failed to read header"); 105 file.close(); 106 return false; 107 } 108 109 if (header.magic != LIBRARY_MAGIC || header.version != LIBRARY_VERSION) { 110 Serial.printf("[LIBIDX] Invalid magic/version: 0x%08X v%d\n", header.magic, header.version); 111 file.close(); 112 return false; 113 } 114 115 if (header.entryCount > MAX_LIBRARY_ENTRIES) { 116 Serial.printf("[LIBIDX] Too many entries: %u\n", header.entryCount); 117 file.close(); 118 return false; 119 } 120 121 // Read entries 122 size_t bytesToRead = header.entryCount * sizeof(LibraryEntry); 123 if (file.read(reinterpret_cast<uint8_t*>(entries_), bytesToRead) != static_cast<int>(bytesToRead)) { 124 Serial.println("[LIBIDX] Failed to read entries"); 125 file.close(); 126 return false; 127 } 128 129 entryCount_ = header.entryCount; 130 sortValid_ = false; 131 file.close(); 132 133 Serial.printf("[LIBIDX] Loaded %zu entries\n", entryCount_); 134 return true; 135 } 136 137 bool LibraryIndex::save(Core& core) { 138 FsFile file; 139 auto result = core.storage.openWrite(LIBRARY_INDEX_PATH, file); 140 if (!result.ok()) { 141 Serial.println("[LIBIDX] Failed to open for write"); 142 return false; 143 } 144 145 // Write header 146 LibraryIndexHeader header; 147 header.magic = LIBRARY_MAGIC; 148 header.version = LIBRARY_VERSION; 149 memset(header.reserved, 0, sizeof(header.reserved)); 150 header.entryCount = entryCount_; 151 header.checksum = 0; // TODO: compute if needed 152 153 file.write(reinterpret_cast<const uint8_t*>(&header), sizeof(header)); 154 155 // Write entries 156 if (entryCount_ > 0) { 157 file.write(reinterpret_cast<const uint8_t*>(entries_), entryCount_ * sizeof(LibraryEntry)); 158 } 159 160 file.close(); 161 Serial.printf("[LIBIDX] Saved %zu entries\n", entryCount_); 162 return true; 163 } 164 165 // ============================================================================ 166 // Rebuild Index 167 // ============================================================================ 168 169 size_t LibraryIndex::rebuild(Core& core) { 170 Serial.println("[LIBIDX] Rebuilding index..."); 171 172 entryCount_ = 0; 173 sortValid_ = false; 174 175 // Open cache directory 176 FsFile cacheDir; 177 auto result = core.storage.openDir(SIGNALOS_CACHE_DIR, cacheDir); 178 if (!result.ok()) { 179 Serial.println("[LIBIDX] Failed to open cache dir"); 180 return 0; 181 } 182 183 // Iterate through cache subdirectories 184 FsFile entry; 185 char entryName[64]; 186 187 while (entry.openNext(&cacheDir, O_RDONLY)) { 188 if (!entry.isDir()) { 189 entry.close(); 190 continue; 191 } 192 193 entry.getName(entryName, sizeof(entryName)); 194 entry.close(); 195 196 // Skip . and .. 197 if (entryName[0] == '.') { 198 continue; 199 } 200 201 // Check if there's a progress.bin or stats.bin (indicating a cached book) 202 char checkPath[280]; 203 snprintf(checkPath, sizeof(checkPath), "%s/%s/progress.bin", SIGNALOS_CACHE_DIR, entryName); 204 205 FsFile checkFile; 206 bool hasProgress = core.storage.openRead(checkPath, checkFile).ok(); 207 if (hasProgress) { 208 checkFile.close(); 209 } else { 210 // Try stats.bin 211 snprintf(checkPath, sizeof(checkPath), "%s/%s/stats.bin", SIGNALOS_CACHE_DIR, entryName); 212 hasProgress = core.storage.openRead(checkPath, checkFile).ok(); 213 if (hasProgress) { 214 checkFile.close(); 215 } 216 } 217 218 if (!hasProgress) { 219 // No progress file means we don't have metadata for this book yet 220 continue; 221 } 222 223 if (entryCount_ >= MAX_LIBRARY_ENTRIES) { 224 Serial.println("[LIBIDX] Max entries reached"); 225 break; 226 } 227 228 // Parse the directory name as the path hash 229 uint32_t pathHash = strtoul(entryName, nullptr, 16); 230 if (pathHash == 0) { 231 continue; 232 } 233 234 // Create entry 235 LibraryEntry& libEntry = entries_[entryCount_]; 236 memset(&libEntry, 0, sizeof(LibraryEntry)); 237 libEntry.pathHash = pathHash; 238 239 // Load metadata from cache dir 240 char cacheEntryDir[280]; 241 snprintf(cacheEntryDir, sizeof(cacheEntryDir), "%s/%s", SIGNALOS_CACHE_DIR, entryName); 242 loadEntryMetadata(core, cacheEntryDir, libEntry); 243 244 entryCount_++; 245 } 246 247 cacheDir.close(); 248 249 // Save the rebuilt index 250 save(core); 251 252 Serial.printf("[LIBIDX] Rebuilt with %zu entries\n", entryCount_); 253 return entryCount_; 254 } 255 256 bool LibraryIndex::loadEntryMetadata(Core& core, const char* cacheDir, LibraryEntry& entry) { 257 // Load stats to get flags and timestamps 258 BookStats stats = ProgressManager::loadStats(core, cacheDir, ContentType::Epub); // Type doesn't matter for flags 259 entry.flags = stats.flags; 260 entry.bookmarkCount = stats.bookmarkCount; 261 262 // For title/author, we need to read the book's metadata 263 // This would require opening the book, which is expensive 264 // For now, use the path hash as a fallback and rely on FileListState to provide actual names 265 266 // Try to read a metadata.txt file if it exists (created when book is first opened) 267 char metaPath[280]; 268 snprintf(metaPath, sizeof(metaPath), "%s/meta.txt", cacheDir); 269 270 FsFile metaFile; 271 if (core.storage.openRead(metaPath, metaFile).ok()) { 272 char buffer[200]; 273 int bytesRead = metaFile.read(reinterpret_cast<uint8_t*>(buffer), sizeof(buffer) - 1); 274 metaFile.close(); 275 276 if (bytesRead > 0) { 277 buffer[bytesRead] = '\0'; 278 279 // Parse simple format: "title\nauthor\ntimestamp" 280 char* newline = strchr(buffer, '\n'); 281 if (newline) { 282 *newline = '\0'; 283 extractTitleKey(buffer, entry.titleKey); 284 285 char* author = newline + 1; 286 char* newline2 = strchr(author, '\n'); 287 if (newline2) { 288 *newline2 = '\0'; 289 extractAuthorKey(author, entry.authorKey); 290 291 char* timestamp = newline2 + 1; 292 entry.dateAddedTimestamp = strtoul(timestamp, nullptr, 10); 293 } 294 } 295 } 296 } 297 298 return true; 299 } 300 301 // ============================================================================ 302 // Update Operations 303 // ============================================================================ 304 305 void LibraryIndex::updateLastRead(uint32_t pathHash, uint32_t timestamp) { 306 LibraryEntry* entry = find(pathHash); 307 if (entry) { 308 entry->lastReadTimestamp = timestamp; 309 entry->setHasProgress(true); 310 sortValid_ = false; 311 } 312 } 313 314 void LibraryIndex::updateFlags(uint32_t pathHash, uint8_t flags, uint8_t bookmarkCount) { 315 LibraryEntry* entry = find(pathHash); 316 if (entry) { 317 entry->flags = flags; 318 entry->bookmarkCount = bookmarkCount; 319 } 320 } 321 322 LibraryEntry* LibraryIndex::find(uint32_t pathHash) { 323 for (size_t i = 0; i < entryCount_; i++) { 324 if (entries_[i].pathHash == pathHash) { 325 return &entries_[i]; 326 } 327 } 328 return nullptr; 329 } 330 331 const LibraryEntry* LibraryIndex::find(uint32_t pathHash) const { 332 for (size_t i = 0; i < entryCount_; i++) { 333 if (entries_[i].pathHash == pathHash) { 334 return &entries_[i]; 335 } 336 } 337 return nullptr; 338 } 339 340 LibraryEntry* LibraryIndex::at(size_t index) { 341 if (index >= entryCount_) return nullptr; 342 return &entries_[index]; 343 } 344 345 const LibraryEntry* LibraryIndex::at(size_t index) const { 346 if (index >= entryCount_) return nullptr; 347 return &entries_[index]; 348 } 349 350 // ============================================================================ 351 // Sorting 352 // ============================================================================ 353 354 void LibraryIndex::sort(SortOrder order) { 355 if (sortValid_ && currentSort_ == order) { 356 return; // Already sorted 357 } 358 359 // Initialize sorted indices 360 for (size_t i = 0; i < entryCount_; i++) { 361 sortedIndices_[i] = static_cast<uint16_t>(i); 362 } 363 364 // Sort based on order 365 switch (order) { 366 case SortOrder::RecentlyRead: 367 std::sort(sortedIndices_, sortedIndices_ + entryCount_, [this](uint16_t a, uint16_t b) { 368 // Never-read books go last, then sort by most recent first 369 if (entries_[a].lastReadTimestamp == 0 && entries_[b].lastReadTimestamp == 0) { 370 return entries_[a].dateAddedTimestamp > entries_[b].dateAddedTimestamp; // Newest added first 371 } 372 if (entries_[a].lastReadTimestamp == 0) return false; 373 if (entries_[b].lastReadTimestamp == 0) return true; 374 return entries_[a].lastReadTimestamp > entries_[b].lastReadTimestamp; 375 }); 376 break; 377 378 case SortOrder::DateAdded: 379 std::sort(sortedIndices_, sortedIndices_ + entryCount_, [this](uint16_t a, uint16_t b) { 380 return entries_[a].dateAddedTimestamp > entries_[b].dateAddedTimestamp; // Newest first 381 }); 382 break; 383 384 case SortOrder::TitleAZ: 385 std::sort(sortedIndices_, sortedIndices_ + entryCount_, [this](uint16_t a, uint16_t b) { 386 return LibraryEntry::compareTitleKey(entries_[a], entries_[b]) < 0; 387 }); 388 break; 389 390 case SortOrder::AuthorAZ: 391 std::sort(sortedIndices_, sortedIndices_ + entryCount_, [this](uint16_t a, uint16_t b) { 392 int cmp = LibraryEntry::compareAuthorKey(entries_[a], entries_[b]); 393 if (cmp == 0) { 394 return LibraryEntry::compareTitleKey(entries_[a], entries_[b]) < 0; 395 } 396 return cmp < 0; 397 }); 398 break; 399 } 400 401 currentSort_ = order; 402 sortValid_ = true; 403 } 404 405 size_t LibraryIndex::getSortedEntries(const LibraryEntry** out, size_t maxCount) const { 406 if (!out || maxCount == 0) return 0; 407 408 size_t count = std::min(entryCount_, maxCount); 409 for (size_t i = 0; i < count; i++) { 410 out[i] = &entries_[sortedIndices_[i]]; 411 } 412 return count; 413 } 414 415 } // namespace signalos