592 lines
16 KiB
C++
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 "";
|
|
}
|