hannabox/src/DirectoryNode.cpp

592 lines
16 KiB
C++

#include "DirectoryNode.h"
#include "globals.h"
#include <algorithm>
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 *> &DirectoryNode::getSubdirectories() const
{
return subdirectories;
}
const std::vector<String> &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<String> dirNames;
std::vector<String> 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<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;
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<std::pair<DirectoryNode *, int>> &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("<ul>"));
delay(0); // yield to WiFi/other tasks
}
if (name != "/") {
out.print(F("<li data-id=\""));
out.print(id);
out.print(F("\"><b>"));
out.print(name);
out.println(F("</b></li>"));
delay(0); // yield periodically while streaming
}
for (size_t i = 0; i < mp3Files.size(); i++) {
out.print(F("<li data-id=\""));
out.print(ids[i]);
out.print(F("\">"));
out.print(mp3Files[i]);
out.println(F("</li>"));
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("</ul>"));
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 "";
}