text/plain
•
6.93 KB
•
305 lines
#include "podcast_sync.h"
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "moon.h"
#include "network.h"
#include "podcast_xml.h"
#include "storage.h"
#include "ui.h"
#define PODCASTS_ROOT "/Podcasts"
#define PODCASTS_OPML "/Podcasts/podcasts.xml"
#define MAX_DIRNAME 64
typedef enum {
SYNC_INIT,
SYNC_ENABLE_NETWORK,
SYNC_DOWNLOAD_FEED,
SYNC_PARSE_FEED,
SYNC_DOWNLOAD_EPISODE,
SYNC_NEXT_FEED,
SYNC_DONE,
} sync_phase_t;
static podcast_sync_status_t status;
static sync_phase_t phase;
static podcast_feed_t* feeds;
static int feed_count;
static podcast_episode_t* episodes;
static int episode_count;
// Keep feed_dir short enough that appending /<ep_dirname>/<audio_name>
// still fits in STORAGE_MAX_PATH. /Podcasts/ = 10, dirname ≤ 64 → 74 max.
#define FEED_DIR_MAX 80
static char feed_dir[FEED_DIR_MAX];
// Sanitize string for use as directory name
static void sanitize_dirname(const char* src, char* dst, size_t dst_size) {
size_t len = 0;
size_t max = dst_size - 1;
if (max > MAX_DIRNAME) {
max = MAX_DIRNAME;
}
for (const char* p = src; *p && len < max; p++) {
char c = *p;
if (isalnum((unsigned char)c) || c == '.' || c == '_' || c == '-') {
dst[len++] = c;
} else if (len > 0 && dst[len - 1] != '_') {
dst[len++] = '_';
}
}
// Trim trailing underscore
while (len > 0 && dst[len - 1] == '_') {
len--;
}
if (len == 0) {
dst[0] = '_';
len = 1;
}
dst[len] = '\0';
}
void podcast_sync_start(void) {
memset(&status, 0, sizeof(status));
phase = SYNC_INIT;
feeds = NULL;
episodes = NULL;
feed_count = 0;
episode_count = 0;
}
void podcast_sync_start_single(const char* title, const char* url) {
memset(&status, 0, sizeof(status));
phase = SYNC_ENABLE_NETWORK;
episodes = NULL;
episode_count = 0;
feeds = malloc(sizeof(podcast_feed_t));
if (!feeds) {
status.current_action = "Out of memory";
status.done = true;
return;
}
strncpy(feeds[0].title, title, sizeof(feeds[0].title) - 1);
feeds[0].title[sizeof(feeds[0].title) - 1] = '\0';
strncpy(feeds[0].url, url, sizeof(feeds[0].url) - 1);
feeds[0].url[sizeof(feeds[0].url) - 1] = '\0';
feed_count = 1;
status.total_feeds = 1;
status.current_feed = 0;
storage_mkdir("Podcasts");
}
bool podcast_sync_step(void) {
if (status.cancelled || status.done) {
return false;
}
switch (phase) {
case SYNC_INIT: {
status.current_action = "Creating directories";
storage_mkdir("Podcasts");
feeds = malloc(PODCAST_MAX_FEEDS * sizeof(podcast_feed_t));
if (!feeds) {
status.current_action = "Out of memory";
status.done = true;
return false;
}
feed_count =
podcast_parse_opml(PODCASTS_OPML, feeds, PODCAST_MAX_FEEDS);
if (feed_count <= 0) {
status.current_action = "No feeds in podcasts.xml";
status.done = true;
free(feeds);
feeds = NULL;
return false;
}
status.total_feeds = feed_count;
status.current_feed = 0;
phase = SYNC_ENABLE_NETWORK;
return true;
}
case SYNC_ENABLE_NETWORK: {
status.current_action = "Connecting to network";
if (!network_set_enabled(true)) {
status.current_action = "Network connection failed";
snprintf(status.last_error, sizeof(status.last_error),
"Could not connect to WiFi");
status.errors++;
status.done = true;
free(feeds);
feeds = NULL;
return false;
}
phase = SYNC_DOWNLOAD_FEED;
return true;
}
case SYNC_DOWNLOAD_FEED: {
if (status.current_feed >= feed_count) {
phase = SYNC_DONE;
return true;
}
podcast_feed_t* feed = &feeds[status.current_feed];
status.current_action = feed->title;
// Create feed directory
char dirname[MAX_DIRNAME + 1];
sanitize_dirname(feed->title, dirname, sizeof(dirname));
snprintf(feed_dir, sizeof(feed_dir), "%s/%s", PODCASTS_ROOT,
dirname);
storage_mkdir(feed_dir);
// Download feed XML
char feed_path[STORAGE_MAX_PATH];
snprintf(feed_path, sizeof(feed_path), "%s/feed.xml", feed_dir);
if (!network_download_file(feed->url, feed_path)) {
ESP_LOGI("SYNC", "Failed to download feed: %s\n", feed->title);
snprintf(status.last_error, sizeof(status.last_error),
"Failed to download: %.100s", feed->title);
status.errors++;
phase = SYNC_NEXT_FEED;
return true;
}
phase = SYNC_PARSE_FEED;
return true;
}
case SYNC_PARSE_FEED: {
char feed_path[STORAGE_MAX_PATH];
snprintf(feed_path, sizeof(feed_path), "%s/feed.xml", feed_dir);
if (episodes) {
free(episodes);
}
episodes = malloc(PODCAST_MAX_EPISODES * sizeof(podcast_episode_t));
if (!episodes) {
snprintf(status.last_error, sizeof(status.last_error),
"Out of memory parsing feed");
status.errors++;
phase = SYNC_NEXT_FEED;
return true;
}
episode_count =
podcast_parse_rss(feed_path, episodes, PODCAST_MAX_EPISODES);
if (episode_count <= 0) {
ESP_LOGI("SYNC", "No episodes in feed\n");
phase = SYNC_NEXT_FEED;
return true;
}
status.total_episodes = episode_count;
status.current_episode = 0;
phase = SYNC_DOWNLOAD_EPISODE;
return true;
}
case SYNC_DOWNLOAD_EPISODE: {
if (status.current_episode >= episode_count) {
phase = SYNC_NEXT_FEED;
return true;
}
podcast_episode_t* ep = &episodes[status.current_episode];
status.current_action = ep->title;
// Create episode directory
char ep_dirname[MAX_DIRNAME + 1];
sanitize_dirname(ep->guid[0] ? ep->guid : ep->title, ep_dirname,
sizeof(ep_dirname));
// feed_dir(≤80) + "/" + ep_dirname(≤64) + NUL ≤ 146
char ep_dir[FEED_DIR_MAX + 1 + MAX_DIRNAME + 1];
snprintf(ep_dir, sizeof(ep_dir), "%s/%s", feed_dir, ep_dirname);
// Skip if already synced (incremental sync)
char entry_path[STORAGE_MAX_PATH];
snprintf(entry_path, sizeof(entry_path), "%s/entry.xml", ep_dir);
storage_file_t check = storage_open(entry_path, "r");
if (check) {
storage_close(check);
status.current_episode++;
return true;
}
storage_mkdir(ep_dir);
// Save entry.xml (episode metadata as simple XML)
podcast_write_entry(entry_path, ep);
status.current_episode++;
return true;
}
case SYNC_NEXT_FEED: {
if (episodes) {
free(episodes);
episodes = NULL;
}
episode_count = 0;
status.current_feed++;
if (status.current_feed >= feed_count) {
phase = SYNC_DONE;
} else {
phase = SYNC_DOWNLOAD_FEED;
}
return true;
}
case SYNC_DONE: {
network_set_enabled(false);
if (feeds) {
free(feeds);
feeds = NULL;
}
if (episodes) {
free(episodes);
episodes = NULL;
}
status.current_action = "Sync complete";
status.done = true;
return false;
}
}
return false;
}
const podcast_sync_status_t* podcast_sync_get_status(void) {
return &status;
}
void podcast_sync_cancel(void) {
status.cancelled = true;
status.done = true;
network_set_enabled(false);
if (feeds) {
free(feeds);
feeds = NULL;
}
if (episodes) {
free(episodes);
episodes = NULL;
}
}