#include "DirectoryNode.h" #include "globals.h" #include #include // strlen, strlcpy, strlcat #include // strcasecmp char DirectoryNode::buffer[DirectoryNode::buffer_size]; DirectoryNode::DirectoryNode(const String &nodeName) : name(nodeName), currentPlaying("") { id = DirectoryNode::idCounter; DirectoryNode::idCounter++; } DirectoryNode::~DirectoryNode() { for (DirectoryNode *childNode : subdirectories) { delete childNode; } } const uint16_t DirectoryNode::getId() const { return id; } const String &DirectoryNode::getName() const { return name; } const std::vector &DirectoryNode::getSubdirectories() const { return subdirectories; } const std::vector &DirectoryNode::getMP3Files() const { return mp3Files; } const String &DirectoryNode::getDirPath() const { return dirPath; } uint16_t DirectoryNode::getFileIdAt(size_t i) const { return (i < ids.size()) ? ids[i] : 0; } String DirectoryNode::buildFullPath(const String &fileName) const { if (dirPath == "/") { String p = "/"; p += fileName; return p; } String p = dirPath; p += "/"; p += fileName; return p; } bool DirectoryNode::comparePathWithString(const char* path, const String& target) const { // Convert target to char* for comparison const char* targetStr = target.c_str(); // Case-insensitive string comparison return strcasecmp(path, targetStr) == 0; } void DirectoryNode::buildFullPath(const String &fileName, char* out, size_t n) const { if (n == 0) return; out[0] = '\0'; if (dirPath == "/") { strlcat(out, "/", n); } else { strlcpy(out, dirPath.c_str(), n); strlcat(out, "/", n); } strlcat(out, fileName.c_str(), n); } void DirectoryNode::setCurrentPlaying(const String &mp3File) { bool isAbs = (mp3File.length() > 0) && (mp3File.charAt(0) == '/'); const String &fileName = isAbs ? mp3File.substring(mp3File.lastIndexOf('/') + 1) : mp3File; if (isAbs) { currentPlaying = mp3File; // Already absolute path } else { // Use buffer for building relative path buildFullPath(fileName, buffer, buffer_size); currentPlaying = String(buffer); // Convert back to String for assignment } for (size_t i = 0; i < mp3Files.size(); i++) { if (mp3Files[i] == fileName && ids.size() > i) { currentPlayingId = ids[i]; break; } } } const String &DirectoryNode::getCurrentPlaying() const { return currentPlaying; } const uint16_t DirectoryNode::getCurrentPlayingId() const { return currentPlayingId; } uint16_t DirectoryNode::getNextId() { uint16_t next = DirectoryNode::idCounter; DirectoryNode::idCounter++; return next; } void DirectoryNode::addSubdirectory(DirectoryNode *subdirectory) { subdirectories.push_back(subdirectory); } void DirectoryNode::addMP3File(const String &mp3File) { mp3Files.push_back(mp3File); ids.push_back(getNextId()); } void DirectoryNode::setSecondsPlayed(const uint32_t seconds) { secondsPlayed = seconds; } uint32_t DirectoryNode::getSecondsPlayed() const { return secondsPlayed; } void DirectoryNode::buildDirectoryTree(const char *currentPath) { // Clear existing data to prevent duplicates when rebuilding for (DirectoryNode *childNode : subdirectories) { delete childNode; } subdirectories.clear(); mp3Files.clear(); ids.clear(); // Set directory path for this node (normalize: keep "/" or remove trailing slash) String path = String(currentPath); if (path.length() > 1 && path.endsWith("/")) { path.remove(path.length() - 1); } dirPath = path; // First collect entries so we can sort them alphabetically std::vector dirNames; std::vector fileNames; File rootDir = SD.open(currentPath); if (!rootDir) { Serial.print(F("buildDirectoryTree: failed to open path: ")); Serial.println(currentPath); return; } while (true) { File entry = rootDir.openNextFile(); if (!entry) { break; } if (entry.isDirectory() && entry.name()[0] != '.' && strcmp(entry.name(), sys_dir) != 0) { dirNames.emplace_back(entry.name()); } else { String entryName = entry.name(); if (entryName.endsWith(".mp3") || entryName.endsWith(".MP3")) { fileNames.push_back(std::move(entryName)); } } entry.close(); } rootDir.close(); // Case-insensitive alphabetical sort without allocations auto ciLess = [](const String &a, const String &b) { const char* pa = a.c_str(); const char* pb = b.c_str(); while (*pa && *pb) { char ca = *pa++; char cb = *pb++; if (ca >= 'A' && ca <= 'Z') ca += 'a' - 'A'; if (cb >= 'A' && cb <= 'Z') cb += 'a' - 'A'; if (ca < cb) return true; if (ca > cb) return false; } return *pa < *pb; }; std::sort(dirNames.begin(), dirNames.end(), ciLess); std::sort(fileNames.begin(), fileNames.end(), ciLess); // Reserve memory to reduce heap fragmentation subdirectories.reserve(dirNames.size()); mp3Files.reserve(fileNames.size()); ids.reserve(fileNames.size()); // Add MP3 files in alphabetical order first to free fileNames memory before recursing for (const String &fileName : fileNames) { mp3Files.push_back(fileName); ids.push_back(getNextId()); } // Free memory used by fileNames vector { std::vector empty; fileNames.swap(empty); } // Create subdirectories in alphabetical order // Use index loop and std::move to free strings in dirNames as we go, reducing stack memory usage during recursion for (size_t i = 0; i < dirNames.size(); ++i) { String dirName = std::move(dirNames[i]); // Move string content out of vector DirectoryNode *newNode = new DirectoryNode(dirName); if (!newNode) { Serial.println(F("buildDirectoryTree: OOM creating DirectoryNode")); continue; } subdirectories.push_back(newNode); String childPath; childPath.reserve(dirPath.length() + 1 + dirName.length()); if (dirPath == "/") { childPath = "/"; childPath += dirName; } else { childPath = dirPath; childPath += "/"; childPath += dirName; } newNode->buildDirectoryTree(childPath.c_str()); } } void DirectoryNode::printDirectoryTree(int level) const { for (int i = 0; i < level; i++) { Serial.print(F(" ")); } Serial.println(name); for (const String &mp3File : mp3Files) { for (int i = 0; i <= level; i++) { Serial.print(F(" ")); } Serial.println(mp3File); } for (DirectoryNode *childNode : subdirectories) { childNode->printDirectoryTree(level + 1); } } DirectoryNode *DirectoryNode::findFirstDirectoryWithMP3s() { if (!mp3Files.empty()) { // Found a directory with MP3 files return this; } for (DirectoryNode *subdirectory : subdirectories) { DirectoryNode *result = subdirectory->findFirstDirectoryWithMP3s(); if (result != nullptr) { // Found a directory with MP3 files in the subdirectories return result; } } // No directory with MP3 files found return nullptr; } void DirectoryNode::advanceToFirstMP3InThisNode() { if (mp3Files.size() > 0) { setCurrentPlaying(mp3Files[0]); } } DirectoryNode *DirectoryNode::advanceToMP3(const uint16_t id) { // First check MP3 files in this directory for (size_t i = 0; i < ids.size(); i++) { if (id == ids[i]) { // Found the current MP3 file buildFullPath(mp3Files[i], buffer, buffer_size); currentPlaying = String(buffer); // Convert back to String for assignment currentPlayingId = id; return this; } } // Recursively search subdirectories for (auto subdir : subdirectories) { // Check if the ID matches a subdirectory ID if (subdir->getId() == id) { subdir->advanceToFirstMP3InThisNode(); return subdir; } // Recursively search in subdirectory DirectoryNode *result = subdir->advanceToMP3(id); if (result != nullptr && !result->getCurrentPlaying().isEmpty()) { return result; } } // If we get here, no song with this ID was found Serial.print(F("advanceToMP3: No song found for ID: ")); Serial.println(id); return nullptr; } DirectoryNode *DirectoryNode::advanceToMP3(const String &songName) { if (songName.isEmpty()) { Serial.println(F("advanceToMP3: songName is empty")); return nullptr; } // Check if the input is an absolute path (starts with '/') or just a filename bool isAbsolutePath = songName.startsWith("/"); // Normalize trailing slash for absolute folder path targets String normalizedPath = songName; if (isAbsolutePath && normalizedPath.length() > 1 && normalizedPath.endsWith("/")) { normalizedPath.remove(normalizedPath.length() - 1); } // Lowercased copies for case-insensitive comparisons (FAT can uppercase names) String lowTarget = songName; lowTarget.toLowerCase(); // First, search in the current directory's MP3 files for (size_t i = 0; i < mp3Files.size(); i++) { if (isAbsolutePath) { // Use static buffer for path building and comparison buildFullPath(mp3Files[i], buffer, buffer_size); if (comparePathWithString(buffer, songName)) { setCurrentPlaying(mp3Files[i]); return this; } } else { // Use static buffer for comparison without allocation buildFullPath(mp3Files[i], buffer, buffer_size); size_t bufLen = strlen(buffer); size_t targetLen = lowTarget.length(); if (bufLen >= targetLen && strcasecmp(buffer + bufLen - targetLen, lowTarget.c_str()) == 0) { setCurrentPlaying(mp3Files[i]); return this; } } } // Then search in subdirectories for (auto subdir : subdirectories) { // Absolute folder target: match directory by its full path (dirPath) if (isAbsolutePath) { if (subdir->getDirPath().equalsIgnoreCase(normalizedPath)) { subdir->advanceToFirstMP3InThisNode(); return subdir; } } if (!isAbsolutePath && subdir->getName().equalsIgnoreCase(songName)) { subdir->advanceToFirstMP3InThisNode(); return subdir; } // Search all files within subdir: for (size_t i = 0; i < subdir->mp3Files.size(); i++) { if (isAbsolutePath) { if (subdir->buildFullPath(subdir->mp3Files[i]).equalsIgnoreCase(songName)) { subdir->setCurrentPlaying(subdir->mp3Files[i]); return subdir; } } else { // Check suffix case-insensitively without creating new Strings const String& fName = subdir->mp3Files[i]; size_t fLen = fName.length(); size_t targetLen = lowTarget.length(); if (fLen >= targetLen && strcasecmp(fName.c_str() + fLen - targetLen, lowTarget.c_str()) == 0) { subdir->setCurrentPlaying(subdir->mp3Files[i]); return subdir; } } } // Recurse into deeper subdirectories to support nested folders and files DirectoryNode* deeper = subdir->advanceToMP3(songName); if (deeper != nullptr) { return deeper; } } // If we get here, no matching song was found Serial.print(F("advanceToMP3: No song found for: ")); Serial.println(songName); return nullptr; } /** * Moves to the previous MP3 file in the directory. * If the current song has been playing for more than a specific threshold, restarts the current song. * If the current song has just started, or it's the first song, moves to the previous song in the directory. * * @param thresholdSeconds The number of seconds to decide whether to restart the current song or go to the previous song. * @return A pointer to the DirectoryNode where the new current song is located, or nullptr if there's no previous song. */ DirectoryNode *DirectoryNode::goToPreviousMP3(uint32_t thresholdSeconds) { // Safety check for null pointer if (currentPlaying.isEmpty()) { Serial.println(F("goToPreviousMP3: currentPlaying is empty")); return nullptr; } // If we've been playing for more than threshold seconds, restart current song if (secondsPlayed > thresholdSeconds) { Serial.println(F("goToPreviousMP3: Restarting current song (played > threshold)")); return this; } // Find the current song index in this directory int currentIndex = -1; for (size_t i = 0; i < mp3Files.size(); i++) { buildFullPath(mp3Files[i], buffer, buffer_size); if (comparePathWithString(buffer, currentPlaying)) { currentIndex = i; break; } } // If current song found and not the first song, move to previous if (currentIndex > 0) { Serial.print(F("goToPreviousMP3: Moving to previous song in same directory: ")); Serial.println(mp3Files[currentIndex - 1]); setCurrentPlaying(mp3Files[currentIndex - 1]); return this; } // If we're at the first song or song not found in current directory, // we need to find the previous song globally Serial.println(F("goToPreviousMP3: At first song or song not found, looking for previous globally")); return nullptr; // Let the caller handle global previous logic } DirectoryNode *DirectoryNode::findPreviousMP3Globally(const String ¤tGlobal) { if (currentGlobal.isEmpty()) { Serial.println(F("findPreviousMP3Globally: currentGlobal is null")); return nullptr; } // Build a flat list of all MP3 files in order std::vector> allMP3s; buildFlatMP3List(allMP3s); // Find current song in the flat list int currentGlobalIndex = -1; for (size_t i = 0; i < allMP3s.size(); i++) { DirectoryNode *node = allMP3s[i].first; int fileIndex = allMP3s[i].second; node->buildFullPath(node->mp3Files[fileIndex], buffer, buffer_size); if (comparePathWithString(buffer, currentGlobal)) { currentGlobalIndex = i; break; } } // If current song found and not the first globally, move to previous if (currentGlobalIndex > 0) { DirectoryNode *prevNode = allMP3s[currentGlobalIndex - 1].first; int prevFileIndex = allMP3s[currentGlobalIndex - 1].second; prevNode->buildFullPath(prevNode->mp3Files[prevFileIndex], buffer, buffer_size); Serial.print(F("findPreviousMP3Globally: Moving to previous song globally: ")); Serial.println(buffer); prevNode->setCurrentPlaying(prevNode->mp3Files[prevFileIndex]); return prevNode; } Serial.println(F("findPreviousMP3Globally: No previous song found globally")); return nullptr; } void DirectoryNode::buildFlatMP3List(std::vector> &allMP3s) { #ifdef DEBUG Serial.println("Building flat mp3 list for folder"); #endif // Pre-reserve to reduce reallocations allMP3s.reserve(allMP3s.size() + mp3Files.size()); // Add all MP3 files from this directory for (size_t i = 0; i < mp3Files.size(); i++) { allMP3s.emplace_back(this, i); } // Recursively add MP3 files from subdirectories for (DirectoryNode *subdir : subdirectories) { subdir->buildFlatMP3List(allMP3s); } } size_t DirectoryNode::getNumOfFiles() const { return subdirectories.size(); } DirectoryNode *DirectoryNode::advanceToNextMP3(const String ¤tGlobal) { Serial.println(currentGlobal.c_str()); // Build a flat list of all MP3 files in order to correctly find the next one across directories std::vector> allMP3s; buildFlatMP3List(allMP3s); if (allMP3s.empty()) { Serial.println(F("advanceToNextMP3: No MP3s found in tree")); currentPlaying = ""; return this; } int currentIndex = -1; if (!currentGlobal.isEmpty()) { for (size_t i = 0; i < allMP3s.size(); i++) { DirectoryNode *node = allMP3s[i].first; int fileIndex = allMP3s[i].second; node->buildFullPath(node->mp3Files[fileIndex], buffer, buffer_size); if (comparePathWithString(buffer, currentGlobal)) { currentIndex = (int)i; break; } } } // If current song found and not the last one, move to next if (currentIndex >= 0 && currentIndex < (int)allMP3s.size() - 1) { DirectoryNode *nextNode = allMP3s[currentIndex + 1].first; int nextFileIndex = allMP3s[currentIndex + 1].second; nextNode->setCurrentPlaying(nextNode->mp3Files[nextFileIndex]); return nextNode; } // If not playing anything (start), play first if (currentIndex == -1 && currentGlobal.isEmpty()) { DirectoryNode *nextNode = allMP3s[0].first; int nextFileIndex = allMP3s[0].second; nextNode->setCurrentPlaying(nextNode->mp3Files[nextFileIndex]); return nextNode; } // If we get here, either we are at the last song, or the current song was not found currentPlaying = ""; Serial.println(F("no more nodes found")); return this; }