diff --git a/src/hooks/useAudio.ts b/src/hooks/useAudio.ts index 5320548..090761e 100644 --- a/src/hooks/useAudio.ts +++ b/src/hooks/useAudio.ts @@ -189,6 +189,10 @@ async function play(episode: Episode): Promise { const vol = volume(); const spd = storeSpeed || speed(); + const feedStore = useFeedStore(); + const feed = feedStore.feeds().find((f) => f.podcast.id === episode.podcastId); + const podcastTitle = feed?.customName || feed?.podcast.title || ""; + // Resume from saved progress if available and not completed const savedProgress = progressStore.get(episode.id); let startPos = 0; @@ -200,6 +204,7 @@ async function play(episode: Episode): Promise { volume: vol, speed: spd, startPosition: startPos > 0 ? startPos : undefined, + mediaTitle: podcastTitle ? `${podcastTitle} — ${episode.title}` : episode.title, }); setCurrentEpisode(episode); @@ -212,7 +217,7 @@ async function play(episode: Episode): Promise { const media = useMediaRegistry(); media.setNowPlaying({ title: episode.title, - artist: episode.podcastId, + artist: podcastTitle || episode.podcastId, duration: episode.duration, }); media.setPlaybackState(true); @@ -363,10 +368,18 @@ async function switchBackend(name: BackendName): Promise { // Resume playback if we were playing if (wasPlaying && ep && ep.audioUrl) { try { + const feedStore = useFeedStore(); + const feed = feedStore + .feeds() + .find((f) => f.podcast.id === ep.podcastId); + const podcastTitle = feed?.customName || feed?.podcast.title || ""; await backend.play(ep.audioUrl, { startPosition: pos, volume: vol, speed: spd, + mediaTitle: podcastTitle + ? `${podcastTitle} — ${ep.title}` + : ep.title, }); setIsPlaying(true); startPolling(); diff --git a/src/stores/download.ts b/src/stores/download.ts index 975d5eb..2d64cf8 100644 --- a/src/stores/download.ts +++ b/src/stores/download.ts @@ -12,6 +12,7 @@ import type { DownloadedEpisode } from "../types/episode"; import type { Episode } from "../types/episode"; import { downloadEpisode } from "../utils/episode-downloader"; import { ensureConfigDir, getConfigFilePath } from "../utils/config-dir"; +import { useFeedStore } from "./feed"; const DOWNLOADS_FILE = "downloads.json"; const MAX_CONCURRENT = 2; @@ -201,6 +202,27 @@ function createDownloadStore() { speed: 0, error: null, }); + + // Write the podcast cover beside the audio so mpv's + // --cover-art-auto=exact picks it up for Now Playing art. + const coverUrl = useFeedStore() + .feeds() + .find((f) => f.id === item.feedId)?.podcast.coverUrl; + if (coverUrl && result.filePath) { + const dot = result.filePath.lastIndexOf("."); + if (dot > 0) { + const coverPath = result.filePath.slice(0, dot) + ".jpg"; + fetch(coverUrl) + .then(async (r) => { + if (!r.ok) return; + await Bun.write( + coverPath, + new Uint8Array(await r.arrayBuffer()), + ); + }) + .catch(() => {}); + } + } } else { updateDownload(item.episodeId, { status: DownloadStatus.FAILED, @@ -306,6 +328,11 @@ function createDownloadStore() { try { const { unlink } = await import("fs/promises"); await unlink(dl.filePath); + const dot = dl.filePath.lastIndexOf("."); + if (dot > 0) { + const coverPath = dl.filePath.slice(0, dot) + ".jpg"; + await unlink(coverPath); + } } catch { // File may already be gone } diff --git a/src/utils/audio-player.ts b/src/utils/audio-player.ts index 9c9e843..97dafb1 100644 --- a/src/utils/audio-player.ts +++ b/src/utils/audio-player.ts @@ -47,6 +47,7 @@ export interface PlayOptions { startPosition?: number; volume?: number; speed?: number; + mediaTitle?: string; } // ── Utilities ──────────────────────────────────────────────────────── @@ -109,6 +110,10 @@ export class MpvBackend implements AudioBackend { `--speed=${opts?.speed ?? 1}`, ]; + if (opts?.mediaTitle) { + args.push(`--force-media-title=${opts.mediaTitle}`); + } + if (opts?.startPosition && opts.startPosition > 0) { args.push(`--start=${opts.startPosition}`); }