657 lines
18 KiB
C++
657 lines
18 KiB
C++
#include "DirectoryNode.h"
|
|
#include "globals.h"
|
|
#include <algorithm>
|
|
#include <cstring> // strlen, strlcpy, strlcat
|
|
#include <strings.h> // 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 *> &DirectoryNode::getSubdirectories() const
|
|
{
|
|
return subdirectories;
|
|
}
|
|
|
|
const std::vector<String> &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<String> dirNames;
|
|
std::vector<String> 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<String> 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<std::pair<DirectoryNode *, int>> 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<std::pair<DirectoryNode *, int>> &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<std::pair<DirectoryNode *, int>> 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;
|
|
}
|