fix: populate podcast dropdown from sources, filter via API

The podcast filter dropdown was built from loaded episodes only,
so podcasts with older episodes didn't appear until scrolling.
Now uses the full sources list and queries the API server-side
when filtering by a specific podcast.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Ricardo
2026-02-03 18:05:02 +01:00
parent 8e50a7948d
commit 80e4ec8b2b
+34 -12
View File
@@ -53,8 +53,8 @@ permalink: /podroll/
class="w-full sm:w-auto appearance-none bg-surface-100 dark:bg-surface-800 border border-surface-300 dark:border-surface-600 rounded-lg px-4 py-2 pr-10 text-sm text-surface-700 dark:text-surface-300 focus:outline-none focus:ring-2 focus:ring-primary-500" class="w-full sm:w-auto appearance-none bg-surface-100 dark:bg-surface-800 border border-surface-300 dark:border-surface-600 rounded-lg px-4 py-2 pr-10 text-sm text-surface-700 dark:text-surface-300 focus:outline-none focus:ring-2 focus:ring-primary-500"
> >
<option value="all">All Podcasts</option> <option value="all">All Podcasts</option>
<template x-for="podcast in uniquePodcasts" :key="podcast"> <template x-for="source in sortedSources" :key="source.title">
<option :value="podcast" x-text="podcast"></option> <option :value="source.title" x-text="source.title"></option>
</template> </template>
</select> </select>
<svg class="absolute right-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-surface-500 pointer-events-none" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="absolute right-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-surface-500 pointer-events-none" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -248,6 +248,8 @@ function podrollApp() {
async init() { async init() {
await this.fetchData(); await this.fetchData();
// Re-fetch from API when podcast filter changes
this.$watch('filterPodcast', () => this.fetchEpisodes());
// Auto-refresh every 5 minutes // Auto-refresh every 5 minutes
this.refreshInterval = setInterval(() => this.fetchData(true), 5 * 60 * 1000); this.refreshInterval = setInterval(() => this.fetchData(true), 5 * 60 * 1000);
}, },
@@ -265,7 +267,7 @@ function podrollApp() {
this.episodes = episodesRes.items || []; this.episodes = episodesRes.items || [];
this.hasMore = episodesRes.hasMore || false; this.hasMore = episodesRes.hasMore || false;
this.offset = episodesRes.offset || 0; this.offset = 0;
this.sources = sourcesRes.items || []; this.sources = sourcesRes.items || [];
this.status = statusRes; this.status = statusRes;
} catch (err) { } catch (err) {
@@ -276,12 +278,36 @@ function podrollApp() {
} }
}, },
async fetchEpisodes() {
this.loading = true;
this.error = null;
this.offset = 0;
try {
let url = `/podrollapi/api/episodes?limit=${this.limit}`;
if (this.filterPodcast !== 'all') {
url += `&source=${encodeURIComponent(this.filterPodcast)}`;
}
const res = await fetch(url).then(r => r.json());
this.episodes = res.items || [];
this.hasMore = res.hasMore || false;
} catch (err) {
this.error = 'Failed to load episodes: ' + err.message;
} finally {
this.loading = false;
}
},
async loadMore() { async loadMore() {
this.loadingMore = true; this.loadingMore = true;
const newOffset = this.offset + this.limit; const newOffset = this.offset + this.limit;
try { try {
const res = await fetch(`/podrollapi/api/episodes?limit=${this.limit}&offset=${newOffset}`).then(r => r.json()); let url = `/podrollapi/api/episodes?limit=${this.limit}&offset=${newOffset}`;
if (this.filterPodcast !== 'all') {
url += `&source=${encodeURIComponent(this.filterPodcast)}`;
}
const res = await fetch(url).then(r => r.json());
this.episodes = [...this.episodes, ...(res.items || [])]; this.episodes = [...this.episodes, ...(res.items || [])];
this.hasMore = res.hasMore || false; this.hasMore = res.hasMore || false;
this.offset = newOffset; this.offset = newOffset;
@@ -293,20 +319,16 @@ function podrollApp() {
}, },
async refresh() { async refresh() {
this.filterPodcast = 'all';
await this.fetchData(); await this.fetchData();
}, },
get filteredEpisodes() { get filteredEpisodes() {
if (this.filterPodcast === 'all') return this.episodes; return this.episodes;
return this.episodes.filter(ep => ep.podcast?.title === this.filterPodcast);
}, },
get uniquePodcasts() { get sortedSources() {
const podcasts = new Set(); return [...this.sources].sort((a, b) => a.title.localeCompare(b.title));
this.episodes.forEach(ep => {
if (ep.podcast?.title) podcasts.add(ep.podcast.title);
});
return Array.from(podcasts).sort();
}, },
formatDate(dateStr, format = 'short') { formatDate(dateStr, format = 'short') {