#include "DirectoryNode.h" #include "globals.h" #include DirectoryNode::DirectoryNode(const String &nodeName) : name(nodeName), currentPlaying(nullptr) { 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; } void DirectoryNode::setCurrentPlaying(const String *mp3File) { currentPlaying = mp3File; for (int i = 0; i < mp3Files.size(); i++) { if (mp3Files[i] == *mp3File && ids.size() > i) { currentPlayingId = ids[i]; } } } 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() { 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(); // First collect entries so we can sort them alphabetically std::vector dirNames; std::vector fileNames; File rootDir = SD.open(currentPath); while (true) { File entry = rootDir.openNextFile(); if (!entry) { break; } if (entry.isDirectory() && entry.name()[0] != '.' && strcmp(entry.name(), sys_dir)) { dirNames.push_back(String(entry.name())); } else { String entryName = entry.name(); if (entryName.endsWith(".mp3") || entryName.endsWith(".MP3")) { fileNames.push_back(entryName); } } entry.close(); } rootDir.close(); // Case-insensitive alphabetical sort auto ciLess = [](const String &a, const String &b) { String al = a; String bl = b; al.toLowerCase(); bl.toLowerCase(); return al.compareTo(bl) < 0; }; 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()); // Create subdirectories in alphabetical order for (const String &dirName : dirNames) { DirectoryNode *newNode = new DirectoryNode(dirName); subdirectories.push_back(newNode); String childPath = String(currentPath); if (!childPath.endsWith("/")) childPath += "/"; childPath += dirName; newNode->buildDirectoryTree(childPath.c_str()); } // Add MP3 files in alphabetical order for (const String &fileName : fileNames) { String fullPath = String(currentPath); if (!fullPath.endsWith("/")) fullPath += "/"; fullPath += fileName; mp3Files.push_back(fullPath); ids.push_back(getNextId()); } } void DirectoryNode::printDirectoryTree(int level) const { for (int i = 0; i < level; i++) { Serial.print(" "); } Serial.println(name); for (const String &mp3File : mp3Files) { for (int i = 0; i <= level; i++) { Serial.print(" "); } 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 currentPlaying = &mp3Files[i]; 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() != nullptr) { return result; } } // If we get here, no song with this ID was found Serial.println("advanceToMP3: No song found for ID: " + String(id)); return nullptr; } DirectoryNode *DirectoryNode::advanceToMP3(const String *songName) { if (songName == nullptr) { Serial.println("advanceToMP3: songName is null"); 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(); String lowNormPath = normalizedPath; lowNormPath.toLowerCase(); // First, search in the current directory's MP3 files for (size_t i = 0; i < mp3Files.size(); i++) { if (isAbsolutePath) { if (mp3Files[i].equalsIgnoreCase(*songName)) { setCurrentPlaying(&mp3Files[i]); return this; } } else { String f = mp3Files[i]; f.toLowerCase(); if (f.endsWith(lowTarget)) { setCurrentPlaying(&mp3Files[i]); return this; } } } // Then search in subdirectories for (auto subdir : subdirectories) { // Absolute folder target: match directory by its full path derived from its files if (isAbsolutePath && subdir->mp3Files.size() > 0) { String anyFile = subdir->mp3Files[0]; int lastSlash = anyFile.lastIndexOf('/'); String subdirPath = (lastSlash >= 0) ? anyFile.substring(0, lastSlash) : String(); if (subdirPath.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->mp3Files[i].equalsIgnoreCase(*songName)) { subdir->setCurrentPlaying(&subdir->mp3Files[i]); return subdir; } } else { String f = subdir->mp3Files[i]; f.toLowerCase(); if (f.endsWith(lowTarget)) { 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.println("advanceToMP3: No song found for: " + *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 == nullptr) { Serial.println("goToPreviousMP3: currentPlaying is null"); return nullptr; } // If we've been playing for more than threshold seconds, restart current song if (secondsPlayed > thresholdSeconds) { Serial.println("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++) { if (*currentPlaying == mp3Files[i]) { currentIndex = i; break; } } // If current song found and not the first song, move to previous if (currentIndex > 0) { Serial.print("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("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 *currentGlobal) { if (currentGlobal == nullptr) { Serial.println("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; if (node->mp3Files[fileIndex] == *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; Serial.print("findPreviousMP3Globally: Moving to previous song globally: "); Serial.println(prevNode->mp3Files[prevFileIndex]); prevNode->setCurrentPlaying(&prevNode->mp3Files[prevFileIndex]); return prevNode; } Serial.println("findPreviousMP3Globally: No previous song found globally"); return nullptr; } void DirectoryNode::buildFlatMP3List(std::vector> &allMP3s) { // Add all MP3 files from this directory for (size_t i = 0; i < mp3Files.size(); i++) { allMP3s.push_back(std::make_pair(this, i)); } // Recursively add MP3 files from subdirectories for (DirectoryNode *subdir : subdirectories) { subdir->buildFlatMP3List(allMP3s); } } DirectoryNode *DirectoryNode::advanceToNextMP3(const String *currentGlobal) { bool useFirst = false; Serial.println(currentGlobal->c_str()); if (currentGlobal != nullptr) { for (size_t i = 0; i < mp3Files.size(); i++) { if (*currentGlobal == mp3Files[i]) { // Found the current playing MP3 file if (i < mp3Files.size() - 1) { // Advance to the next MP3 file in the same directory setCurrentPlaying(&mp3Files[i + 1]); return this; } useFirst = true; // Reached the end of the MP3 files in the directory break; } } } // We are either not playing, or we've exhausted all the MP3 files in this directory. // Therefore, we need to recursively look in our subdirectories. for (auto subdir : subdirectories) { if (useFirst && subdir->mp3Files.size() > 0) { subdir->setCurrentPlaying(&subdir->mp3Files[0]); return subdir; } // Have each subdirectory advance its song for (size_t i = 0; i < subdir->mp3Files.size(); i++) { if (*currentGlobal == subdir->mp3Files[i]) { // Found the current playing MP3 file if (i < subdir->mp3Files.size() - 1) { // Advance to the next MP3 file in the same directory subdir->setCurrentPlaying(&subdir->mp3Files[i + 1]); return subdir; } else { useFirst = true; } // Reached the end of the MP3 files in the directory break; } } } // If we get here, there were no MP3 files or subdirectories left to check currentPlaying = nullptr; Serial.println("no more nodes found"); return this; } void DirectoryNode::streamDirectoryHTML(Print &out) const { if (name == "/") { out.println(F("
    ")); delay(0); // yield to WiFi/other tasks } if (name != "/") { out.print(F("
  • ")); out.print(name); out.println(F("
  • ")); delay(0); // yield periodically while streaming } for (size_t i = 0; i < mp3Files.size(); i++) { out.print(F("
  • ")); out.print(mp3Files[i]); out.println(F("
  • ")); if ((i & 0x0F) == 0) { // yield every ~16 items delay(0); } } for (DirectoryNode* child : subdirectories) { delay(0); // yield before descending child->streamDirectoryHTML(out); delay(0); // and after returning } if (name == "/") { out.println(F("
")); delay(0); } } void DirectoryNode::appendIndentation(String &html, int level) const { for (int i = 0; i < level; i++) { html.concat(" "); } } String DirectoryNode::getCurrentPlayingFilePath() const { if (currentPlaying != nullptr) { return *currentPlaying; } return ""; }