Fix podcast player stability, V4V payments, and Lightning address

- Add audio.load() reset to prevent cascading failures between episodes
- Add onPlaying event handler for reliable post-buffer playback in WebKitGTK
- Add 15s loading timeout with user-friendly error for broken audio URLs
- Fix V4V LNURL-pay: use Tauri HTTP plugin instead of CORS-blocked browser fetch
- Fix Podcast Index API: use Tauri HTTP plugin for V4V enrichment
- Fall back to Podcast Index enclosure URL when Fountain.fm CDN is broken
- Update Lightning address to jure@getalby.com (Alby cloud migration)
- Add broad HTTPS scope for LNURL-pay across Lightning providers
This commit is contained in:
Jure
2026-04-03 22:27:34 +02:00
parent c83afeabc4
commit 5444214041
7 changed files with 41 additions and 7 deletions

View File

@@ -178,7 +178,7 @@ Vega is free and open-source. If it's useful to you:
| Method | Details | | Method | Details |
|---|---| |---|---|
| ⚡ Zap (in-app) | Open the **support** view in Vega's sidebar and zap directly | | ⚡ Zap (in-app) | Open the **support** view in Vega's sidebar and zap directly |
| ⚡ Lightning | `harpos@getalby.com` | | ⚡ Lightning | `jure@getalby.com` |
| ₿ Bitcoin | `bc1qcgaupf80j28ca537xjlcs9dm9s03khezjs7crp` | | ₿ Bitcoin | `bc1qcgaupf80j28ca537xjlcs9dm9s03khezjs7crp` |
| ☕ Ko-fi | [ko-fi.com/jure](https://ko-fi.com/jure) | | ☕ Ko-fi | [ko-fi.com/jure](https://ko-fi.com/jure) |
| ♥ GitHub Sponsors | [github.com/sponsors/hoornet](https://github.com/sponsors/hoornet) | | ♥ GitHub Sponsors | [github.com/sponsors/hoornet](https://github.com/sponsors/hoornet) |

View File

@@ -15,7 +15,9 @@
{ "url": "https://void.cat/**" }, { "url": "https://void.cat/**" },
{ "url": "https://nostrimg.com/**" }, { "url": "https://nostrimg.com/**" },
{ "url": "https://relay.vertexlab.io/**" }, { "url": "https://relay.vertexlab.io/**" },
{ "url": "https://fountain.fm/**" } { "url": "https://fountain.fm/**" },
{ "url": "https://api.podcastindex.org/**" },
{ "url": "https://**" }
] ]
}, },
"dialog:default", "dialog:default",

View File

@@ -83,13 +83,18 @@ export function PodcastPlayerBar() {
setAudioError(null); setAudioError(null);
setPlaybackState("loading"); setPlaybackState("loading");
// Set source and let it load // Reset and set source — explicit load() is needed to clear error state
// from a previous failed episode, otherwise WebView won't attempt the new URL
audio.src = episode.enclosureUrl; audio.src = episode.enclosureUrl;
audio.load();
audio.playbackRate = playbackRate; audio.playbackRate = playbackRate;
audio.volume = volume; audio.volume = volume;
let loaded = false;
// Wait for the audio to be ready, then seek + play // Wait for the audio to be ready, then seek + play
const onCanPlay = () => { const onCanPlay = () => {
loaded = true;
audio.removeEventListener("canplaythrough", onCanPlay); audio.removeEventListener("canplaythrough", onCanPlay);
const savedPosition = usePodcastStore.getState().loadProgress(episode.guid); const savedPosition = usePodcastStore.getState().loadProgress(episode.guid);
if (savedPosition > 0) { if (savedPosition > 0) {
@@ -99,7 +104,19 @@ export function PodcastPlayerBar() {
}; };
audio.addEventListener("canplaythrough", onCanPlay); audio.addEventListener("canplaythrough", onCanPlay);
return () => audio.removeEventListener("canplaythrough", onCanPlay); // Timeout: if audio doesn't load within 15s, show helpful error
const timeout = setTimeout(() => {
if (!loaded) {
audio.removeEventListener("canplaythrough", onCanPlay);
setAudioError("Audio file not available — the podcast host may be down or the episode URL is broken.");
setPlaybackState("paused");
}
}, 15000);
return () => {
clearTimeout(timeout);
audio.removeEventListener("canplaythrough", onCanPlay);
};
}, [episode, playCounter]); }, [episode, playCounter]);
// Sync playback rate // Sync playback rate
@@ -214,6 +231,7 @@ export function PodcastPlayerBar() {
onLoadedMetadata={handleLoadedMetadata} onLoadedMetadata={handleLoadedMetadata}
onEnded={handleEnded} onEnded={handleEnded}
onPlay={() => setPlaybackState("playing")} onPlay={() => setPlaybackState("playing")}
onPlaying={() => setPlaybackState("playing")}
onPause={() => { onPause={() => {
const s = usePodcastStore.getState().playbackState; const s = usePodcastStore.getState().playbackState;
// Only mark paused if we were actually playing — ignore pause events from src changes // Only mark paused if we were actually playing — ignore pause events from src changes

View File

@@ -5,7 +5,7 @@ import pkg from "../../../package.json";
const DEV_NPUB = "npub1ezt7xcq87ljj65jkjsuagwll4yp75tacgkuyjdhkw6mza8j3azfq2vrvl6"; const DEV_NPUB = "npub1ezt7xcq87ljj65jkjsuagwll4yp75tacgkuyjdhkw6mza8j3azfq2vrvl6";
const DEV_PUBKEY = "c897e36007f7e52d52569439d43bffa903ea2fb845b84936f676b62e9e51e892"; const DEV_PUBKEY = "c897e36007f7e52d52569439d43bffa903ea2fb845b84936f676b62e9e51e892";
const LIGHTNING_ADDRESS = "harpos@getalby.com"; const LIGHTNING_ADDRESS = "jure@getalby.com";
const BITCOIN_ADDRESS = "bc1qcgaupf80j28ca537xjlcs9dm9s03khezjs7crp"; const BITCOIN_ADDRESS = "bc1qcgaupf80j28ca537xjlcs9dm9s03khezjs7crp";
const KOFI_URL = "https://ko-fi.com/jure"; const KOFI_URL = "https://ko-fi.com/jure";
const GITHUB_URL = "https://github.com/hoornet/vega"; const GITHUB_URL = "https://github.com/hoornet/vega";

View File

@@ -69,7 +69,12 @@ export async function resolveFountainEpisode(url: string): Promise<PodcastEpisod
}; };
// Try to enrich with V4V data from Podcast Index (non-blocking) // Try to enrich with V4V data from Podcast Index (non-blocking)
const enriched = await enrichWithV4V(episode); let enriched = episode;
try {
enriched = await enrichWithV4V(episode);
} catch {
// V4V enrichment failed — use episode as-is
}
cache[url] = enriched; cache[url] = enriched;
saveCache(cache); saveCache(cache);

View File

@@ -1,3 +1,4 @@
import { fetch } from "@tauri-apps/plugin-http";
import type { PodcastEpisode, V4VRecipient } from "../../types/podcast"; import type { PodcastEpisode, V4VRecipient } from "../../types/podcast";
const API_KEY = "VKWWTGY25NVCKYJWHSNY"; const API_KEY = "VKWWTGY25NVCKYJWHSNY";
@@ -89,7 +90,14 @@ export async function enrichWithV4V(episode: PodcastEpisode): Promise<PodcastEpi
const value = extractV4V(valueSource.value as Record<string, unknown> | undefined); const value = extractV4V(valueSource.value as Record<string, unknown> | undefined);
if (value.length === 0) return episode; if (value.length === 0) return episode;
return { ...episode, value }; // If Fountain's audio URL is on their broken CDN, use Podcast Index's real enclosure URL
let { enclosureUrl } = episode;
if (match && enclosureUrl.includes("feeds.fountain.fm")) {
const piUrl = match.enclosureUrl as string | undefined;
if (piUrl) enclosureUrl = piUrl;
}
return { ...episode, value, enclosureUrl };
} catch { } catch {
return episode; return episode;
} }

View File

@@ -1,3 +1,4 @@
import { fetch } from "@tauri-apps/plugin-http";
import type { PodcastEpisode, V4VRecipient } from "../../types/podcast"; import type { PodcastEpisode, V4VRecipient } from "../../types/podcast";
import { payInvoiceViaNWC } from "../lightning/nwc"; import { payInvoiceViaNWC } from "../lightning/nwc";