Login
1 branch 0 tags
Ben (Desktop/Arch) Wifi/Podcasts on the ESP32!!!! 90acdde 1 month ago 77 Commits
moon / src / screens / podcast_episode_screen.c
#include "podcast_episode_screen.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "../audio_player.h"
#include "../network.h"
#include "../playlist.h"
#include "../storage.h"
#include "../topbar.h"
#include "files_screen.h"
#include "podcast_feed_screen.h"

// Find first audio file in a directory
static bool find_audio_file(const char* dir, char* out, size_t out_size) {
	storage_entry_t entries[STORAGE_MAX_DIR_ENTRIES];
	int count = storage_list_dir(dir, entries, STORAGE_MAX_DIR_ENTRIES);
	for (int i = 0; i < count; i++) {
		if (entries[i].type == STORAGE_TYPE_FILE &&
		    is_audio_file(entries[i].name)) {
			size_t len = strlen(entries[i].name);
			if (len >= out_size) {
				len = out_size - 1;
			}
			memcpy(out, entries[i].name, len);
			out[len] = '\0';
			return true;
		}
	}
	return false;
}

// Read enclosure URL from url.txt in episode dir
static bool read_url_file(const char* dir, char* out, size_t out_size) {
	char path[STORAGE_MAX_PATH];
	int pn = snprintf(path, sizeof(path), "%s/url.txt", dir);
	if (pn <= 0 || (size_t)pn >= sizeof(path)) {
		return false;
	}

	storage_file_t f = storage_open(path, "r");
	if (!f) {
		return false;
	}
	size_t n = storage_read(f, out, out_size - 1);
	storage_close(f);
	out[n] = '\0';

	// Trim trailing whitespace
	while (n > 0 &&
	       (out[n - 1] == '\n' || out[n - 1] == '\r' || out[n - 1] == ' ')) {
		out[--n] = '\0';
	}
	return n > 0;
}

static void go_back(void) {
	lv_obj_t* scr = navigate_prepare(ANIM_BACK);
	ui_state = setup_podcast_feed_screen(scr, app_state.podcast_dir, "");
	navigate_commit();
}

static void cancel_event_cb(lv_event_t* e) {
	(void)e;
	go_back();
}

static void back_event_cb(lv_event_t* e) {
	if (lv_event_get_code(e) == LV_EVENT_CLICKED) {
		go_back();
	}
}

static void play_now_cb(lv_event_t* e) {
	if (lv_event_get_code(e) != LV_EVENT_CLICKED) {
		return;
	}

	podcast_episode_screen_state_t* state = ui_state.podcast_episode;
	if (!state || !state->has_audio) {
		return;
	}

	audio_player_stop();
	playlist_clear();
	playlist_add(state->audio_path, PLAYLIST_MUSIC);
	playlist_set_index(0);
	playlist_save();
	navigate_to(SCREEN_NOW_PLAYING);
}

static void add_to_queue_cb(lv_event_t* e) {
	if (lv_event_get_code(e) != LV_EVENT_CLICKED) {
		return;
	}

	podcast_episode_screen_state_t* state = ui_state.podcast_episode;
	if (!state || !state->has_audio) {
		return;
	}

	playlist_add(state->audio_path, PLAYLIST_MUSIC);
	playlist_save();
	go_back();
}

static void delete_cb(lv_event_t* e) {
	if (lv_event_get_code(e) != LV_EVENT_CLICKED) {
		return;
	}

	podcast_episode_screen_state_t* state = ui_state.podcast_episode;
	if (!state || !state->has_audio) {
		return;
	}

	storage_remove(state->audio_path);

	// Refresh screen
	lv_obj_t* scr = navigate_prepare(ANIM_NONE);
	ui_state = setup_podcast_episode_screen(scr);
	navigate_commit();
}

static void download_cb(lv_event_t* e) {
	if (lv_event_get_code(e) != LV_EVENT_CLICKED) {
		return;
	}

	podcast_episode_screen_state_t* state = ui_state.podcast_episode;
	if (!state || state->has_audio || state->downloading) {
		return;
	}

	if (state->enclosure_url[0] == '\0') {
		return;
	}

	// Set downloading flag — actual download happens in update
	state->downloading = true;
}

// Extract filename from URL (last path segment, strip query params)
static void url_filename(const char* url, char* out, size_t out_size) {
	const char* last_slash = strrchr(url, '/');
	const char* start = last_slash ? last_slash + 1 : url;

	const char* query = strchr(start, '?');
	size_t len = query ? (size_t)(query - start) : strlen(start);

	if (len >= out_size) {
		len = out_size - 1;
	}
	memcpy(out, start, len);
	out[len] = '\0';
}

ui_state_t setup_podcast_episode_screen(lv_obj_t* parent) {
	podcast_episode_screen_state_t* state =
	    calloc(1, sizeof(podcast_episode_screen_state_t));

	snprintf(state->dir, sizeof(state->dir), "%s",
	         app_state.podcast_episode_dir);

	// Set topbar title from title.txt, fall back to dir basename
	char title_path[STORAGE_MAX_PATH];
	char title[128] = {0};
	int tp =
	    snprintf(title_path, sizeof(title_path), "%s/title.txt", state->dir);
	if (tp > 0 && (size_t)tp < sizeof(title_path)) {
		storage_file_t tf = storage_open(title_path, "r");
		if (tf) {
			size_t n = storage_read(tf, title, sizeof(title) - 1);
			storage_close(tf);
			title[n] = '\0';
		}
	}
	if (title[0]) {
		topbar_set_title(title);
	} else {
		const char* basename = strrchr(state->dir, '/');
		basename = (basename && basename[1]) ? basename + 1 : "Episode";
		topbar_set_title(basename);
	}

	// Check for existing audio
	char audio_name[STORAGE_MAX_NAME];
	if (find_audio_file(state->dir, audio_name, sizeof(audio_name))) {
		int pn = snprintf(state->audio_path, sizeof(state->audio_path), "%s/%s",
		                  state->dir, audio_name);
		if (pn > 0 && (size_t)pn < sizeof(state->audio_path)) {
			state->has_audio = true;
		}
	}

	// Read enclosure URL
	read_url_file(state->dir, state->enclosure_url,
	              sizeof(state->enclosure_url));

	state->list = bvs_list_create(parent);

	if (state->has_audio) {
		lv_obj_t* play_btn = bvs_list_add_button(state->list, NULL, "Play now");
		lv_obj_add_event_cb(play_btn, play_now_cb, LV_EVENT_CLICKED, NULL);
		lv_obj_add_event_cb(play_btn, cancel_event_cb, LV_EVENT_CANCEL, NULL);
		lv_group_add_obj(lv_group_get_default(), play_btn);

		lv_obj_t* queue_btn =
		    bvs_list_add_button(state->list, NULL, "Add to queue");
		lv_obj_add_event_cb(queue_btn, add_to_queue_cb, LV_EVENT_CLICKED, NULL);
		lv_obj_add_event_cb(queue_btn, cancel_event_cb, LV_EVENT_CANCEL, NULL);
		lv_group_add_obj(lv_group_get_default(), queue_btn);

		lv_obj_t* del_btn =
		    bvs_list_add_button(state->list, NULL, "Delete download");
		lv_obj_add_event_cb(del_btn, delete_cb, LV_EVENT_CLICKED, NULL);
		lv_obj_add_event_cb(del_btn, cancel_event_cb, LV_EVENT_CANCEL, NULL);
		lv_group_add_obj(lv_group_get_default(), del_btn);

		lv_group_focus_obj(play_btn);
	} else {
		if (state->enclosure_url[0]) {
			lv_obj_t* dl_btn =
			    bvs_list_add_button(state->list, NULL, "Download");
			lv_obj_add_event_cb(dl_btn, download_cb, LV_EVENT_CLICKED, NULL);
			lv_obj_add_event_cb(dl_btn, cancel_event_cb, LV_EVENT_CANCEL, NULL);
			lv_group_add_obj(lv_group_get_default(), dl_btn);
			lv_group_focus_obj(dl_btn);
		} else {
			lv_obj_t* msg =
			    bvs_list_add_button(state->list, NULL, "No URL found");
			(void)msg;
		}
	}

	lv_obj_t* back_btn = bvs_list_add_button(state->list, NULL, "Back");
	lv_obj_add_event_cb(back_btn, back_event_cb, LV_EVENT_CLICKED, NULL);
	lv_obj_add_event_cb(back_btn, cancel_event_cb, LV_EVENT_CANCEL, NULL);
	lv_group_add_obj(lv_group_get_default(), back_btn);

	return (ui_state_t){.type = SCREEN_PODCAST_EPISODE,
	                    .podcast_episode = state};
}

void free_podcast_episode_screen(podcast_episode_screen_state_t* state) {
	if (!state) {
		return;
	}
	free(state);
}

void update_podcast_episode_screen(podcast_episode_screen_state_t* state) {
	if (!state || !state->downloading) {
		return;
	}

	state->downloading = false;

	// Update UI to show downloading status
	bvs_list_clear(state->list);
	lv_group_remove_all_objs(lv_group_get_default());

	lv_obj_t* msg = bvs_list_add_button(state->list, NULL, "Downloading...");
	lv_group_add_obj(lv_group_get_default(), msg);
	lv_group_focus_obj(msg);

	// Force LVGL to render the "Downloading..." label before blocking
	lv_refr_now(lv_display_get_default());

	// Build audio path from URL
	char audio_name[64];
	url_filename(state->enclosure_url, audio_name, sizeof(audio_name));
	if (audio_name[0] == '\0') {
		snprintf(audio_name, sizeof(audio_name), "audio.mp3");
	}

	char audio_path[STORAGE_MAX_PATH];
	int n = snprintf(audio_path, sizeof(audio_path), "%s/%s", state->dir,
	                 audio_name);
	if (n <= 0 || (size_t)n >= sizeof(audio_path)) {
		return;
	}

	network_set_enabled(true);
	bool ok = network_download_file(state->enclosure_url, audio_path);
	network_set_enabled(false);

	if (ok) {
		state->has_audio = true;
		snprintf(state->audio_path, sizeof(state->audio_path), "%s",
		         audio_path);
	}

	// Refresh screen to show updated buttons
	lv_obj_t* scr = navigate_prepare(ANIM_NONE);
	ui_state = setup_podcast_episode_screen(scr);
	navigate_commit();
}