Login
1 branch 0 tags
Ben (Desktop/Arch) Improved podcast episode list e9e6fb4 29 days ago 81 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 "../podcast_xml.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;
}

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;
}

// Build "episode.ext" from URL, defaulting to episode.mp3
static void episode_filename(const char* url, char* out, size_t out_size) {
	// Find last dot in URL path (before ? or #)
	const char* dot = NULL;
	for (const char* p = url; *p && *p != '?' && *p != '#'; p++) {
		if (*p == '.') {
			dot = p;
		}
	}

	// Extract extension, lowercased
	char ext[8] = {0};
	if (dot) {
		size_t i = 0;
		for (const char* p = dot;
		     *p && *p != '?' && *p != '#' && i < sizeof(ext) - 1; p++) {
			ext[i++] = (*p >= 'A' && *p <= 'Z') ? (*p + 32) : *p;
		}
		ext[i] = '\0';
	}

	snprintf(out, out_size, "episode%s", ext[0] ? ext : ".mp3");
}

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);

	// Parse entry.xml for episode metadata
	char entry_path[STORAGE_MAX_PATH + 16];
	snprintf(entry_path, sizeof(entry_path), "%s/entry.xml", state->dir);
	podcast_episode_t parsed = {0};
	if (podcast_parse_entry(entry_path, &parsed)) {
		if (parsed.title[0]) {
			topbar_set_title(parsed.title);
		}
		snprintf(state->enclosure_url, sizeof(state->enclosure_url), "%s",
		         parsed.enclosure_url);
	}
	if (!parsed.title[0]) {
		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;
		}
	}

	state->list = bvs_list_create(parent);

	if (state->has_audio) {
		lv_obj_t* play_btn = bvs_list_add_nav_button(
		    state->list, NULL, "Play now", play_now_cb, NULL, cancel_event_cb);

		bvs_list_add_nav_button(state->list, NULL, "Add to queue",
		                        add_to_queue_cb, NULL, cancel_event_cb);

		bvs_list_add_nav_button(state->list, NULL, "Delete download", delete_cb,
		                        NULL, cancel_event_cb);

		lv_group_focus_obj(play_btn);
	} else {
		if (state->enclosure_url[0]) {
			lv_obj_t* dl_btn =
			    bvs_list_add_nav_button(state->list, NULL, "Download",
			                            download_cb, NULL, cancel_event_cb);
			lv_group_focus_obj(dl_btn);
		} else {
			bvs_list_add_button(state->list, NULL, "No URL found");
		}
	}

	bvs_list_add_nav_button(state->list, NULL, "Back", back_event_cb, NULL,
	                        cancel_event_cb);

	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());
	state->list = bvs_list_create(lv_screen_active());

	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 extension
	char audio_name[32];
	episode_filename(state->enclosure_url, audio_name, sizeof(audio_name));

	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();
}