Files
kidsbox/device/main/CacheManager.cpp
Claude bf9852c345 Implement full NFC-URL streaming architecture
- rc522: add NDEF URL reading via WUPA+cascade anticoll+SELECT+TLV parse
- AudioPlayer: route http:// URIs to esp_player_http_music_play()
- CacheManager: download URLs async to /sdcard/cache/{hash}.{ext}
- TagPlayerControl: NDEF URL → cache-or-stream; fallback to UID local mapping
- RFIDInterface: wire ndef_callback, expose combined uid+url callback
- WebInterface: add /api/info, /api/storage, /api/playback, /api/bluetooth
- BluetoothInterface: stub for A2DP speaker mode
- Networking: WiFi connect with 30s timeout instead of blocking forever
- main.cpp: re-enable WiFi + web server, fix init order, add CacheManager
- www/: full SPA — playback controls, device info, storage, BT pairing
- .gitmodules: switch rc522 submodule URL to HTTPS

https://claude.ai/code/session_01MhAnh68DPNnTjcKqjNYDmk
2026-06-12 19:44:54 +00:00

206 lines
5.9 KiB
C++

#include "CacheManager.hpp"
#include <esp_log.h>
#include <esp_http_client.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <sys/stat.h>
#include <dirent.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <string>
#include <functional>
static const char* TAG = "CacheManager";
struct DownloadArgs {
std::string url;
std::function<void(bool)> onComplete;
};
CacheManager::CacheManager() {
mkdir(CACHE_DIR, 0777);
}
/* djb2 hash of the URL, returned as 8-char hex string */
std::string CacheManager::urlToKey(const std::string& url) const {
uint32_t hash = 5381;
for(char c : url) {
hash = ((hash << 5) + hash) ^ (uint8_t)c;
}
char buf[9];
snprintf(buf, sizeof(buf), "%08lx", (unsigned long)hash);
return std::string(buf);
}
std::string CacheManager::urlExtension(const std::string& url) const {
/* Strip query/fragment before looking for extension */
size_t qs = url.find('?');
std::string path = (qs != std::string::npos) ? url.substr(0, qs) : url;
size_t dot = path.rfind('.');
size_t slash = path.rfind('/');
if(dot != std::string::npos && (slash == std::string::npos || dot > slash)) {
std::string ext = path.substr(dot); /* includes the dot */
if(ext.size() <= 5) return ext; /* ignore suspiciously long "extensions" */
}
return ".mp3"; /* sensible default */
}
std::string CacheManager::keyToPath(const std::string& key, const std::string& ext) const {
return std::string(CACHE_DIR) + "/" + key + ext;
}
/* Find any file in CACHE_DIR whose name starts with key */
std::string CacheManager::findFileByKey(const std::string& key) const {
DIR* dir = opendir(CACHE_DIR);
if(!dir) return "";
struct dirent* entry;
while((entry = readdir(dir)) != NULL) {
std::string name(entry->d_name);
if(name.rfind(key, 0) == 0) {
closedir(dir);
return std::string(CACHE_DIR) + "/" + name;
}
}
closedir(dir);
return "";
}
std::string CacheManager::getCachedPath(const std::string& url) const {
if(url.empty()) return "";
std::string key = urlToKey(url);
std::string path = findFileByKey(key);
if(!path.empty()) {
ESP_LOGI(TAG, "Cache hit: %s -> %s", url.c_str(), path.c_str());
}
return path;
}
void CacheManager::downloadTask(void* arg) {
auto* dl = static_cast<DownloadArgs*>(arg);
ESP_LOGI(TAG, "Downloading: %s", dl->url.c_str());
CacheManager cm;
std::string key = cm.urlToKey(dl->url);
std::string ext = cm.urlExtension(dl->url);
std::string path = cm.keyToPath(key, ext);
esp_http_client_config_t cfg = {};
cfg.url = dl->url.c_str();
cfg.timeout_ms = 30000;
cfg.buffer_size = 4096;
esp_http_client_handle_t client = esp_http_client_init(&cfg);
esp_err_t err = esp_http_client_open(client, 0);
bool success = false;
if(err == ESP_OK) {
int content_len = esp_http_client_fetch_headers(client);
(void)content_len;
FILE* f = fopen(path.c_str(), "wb");
if(f) {
char buf[4096];
int read_len;
while((read_len = esp_http_client_read(client, buf, sizeof(buf))) > 0) {
fwrite(buf, 1, read_len, f);
}
fclose(f);
success = (read_len == 0); /* 0 = clean EOF */
if(!success) {
remove(path.c_str());
ESP_LOGE(TAG, "Download error, partial file deleted");
} else {
ESP_LOGI(TAG, "Cached to %s", path.c_str());
}
} else {
ESP_LOGE(TAG, "Cannot open cache file: %s (errno %d)", path.c_str(), errno);
}
} else {
ESP_LOGE(TAG, "HTTP open failed: %s", esp_err_to_name(err));
}
esp_http_client_cleanup(client);
if(dl->onComplete) dl->onComplete(success);
delete dl;
vTaskDelete(NULL);
}
void CacheManager::downloadAsync(const std::string& url, std::function<void(bool)> onComplete) {
if(url.empty()) return;
auto* args = new DownloadArgs{ url, onComplete };
xTaskCreate(downloadTask, "cache_dl", 8192, args, 3, NULL);
}
std::vector<CacheEntry> CacheManager::listEntries() const {
std::vector<CacheEntry> entries;
DIR* dir = opendir(CACHE_DIR);
if(!dir) return entries;
struct dirent* entry;
while((entry = readdir(dir)) != NULL) {
if(entry->d_name[0] == '.') continue;
std::string name(entry->d_name);
std::string full = std::string(CACHE_DIR) + "/" + name;
struct stat st;
size_t size = 0;
if(stat(full.c_str(), &st) == 0) size = st.st_size;
/* Key is filename without extension */
size_t dot = name.rfind('.');
std::string key = (dot != std::string::npos) ? name.substr(0, dot) : name;
entries.push_back({ key, /*url unknown*/"", full, size });
}
closedir(dir);
return entries;
}
bool CacheManager::deleteEntry(const std::string& key) {
std::string path = findFileByKey(key);
if(path.empty()) return false;
return remove(path.c_str()) == 0;
}
void CacheManager::deleteAll() {
DIR* dir = opendir(CACHE_DIR);
if(!dir) return;
struct dirent* entry;
while((entry = readdir(dir)) != NULL) {
if(entry->d_name[0] == '.') continue;
std::string full = std::string(CACHE_DIR) + "/" + entry->d_name;
remove(full.c_str());
}
closedir(dir);
ESP_LOGI(TAG, "Cache cleared");
}
size_t CacheManager::totalBytes() const {
size_t total = 0;
DIR* dir = opendir(CACHE_DIR);
if(!dir) return 0;
struct dirent* entry;
while((entry = readdir(dir)) != NULL) {
if(entry->d_name[0] == '.') continue;
std::string full = std::string(CACHE_DIR) + "/" + entry->d_name;
struct stat st;
if(stat(full.c_str(), &st) == 0) total += st.st_size;
}
closedir(dir);
return total;
}