From e7ed89056e77d651a6074b6090a1ce7397c11ae8 Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Wed, 12 Aug 2026 11:10:07 -0400 Subject: [PATCH] fix(now-playing): episode as title, podcast as artist on both paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The card put the podcast name in the title slot ('Podcast — Episode' prefix truncating on long names) with an empty artist slot for local playback — the downloaded files carry no artist tag. Now: - mediaTitle is the episode title only (UI + CLI); streams keep their artist from stream tags, matching Apple Podcasts' title/artist layout. - downloads are tagged at completion (ffmpeg -c copy, atomic rename): title=episode, artist=podcast — verified on a real 72MB file in 1.5s. --- src/hooks/useAudio.ts | 10 +++------- src/index.tsx | 2 +- src/stores/download.ts | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/hooks/useAudio.ts b/src/hooks/useAudio.ts index d8441ad..a51f4f0 100644 --- a/src/hooks/useAudio.ts +++ b/src/hooks/useAudio.ts @@ -348,7 +348,7 @@ async function play(episode: Episode): Promise { volume: vol, speed: spd, startPosition: startPos > 0 ? startPos : undefined, - mediaTitle: podcastTitle ? `${podcastTitle} — ${episode.title}` : episode.title, + mediaTitle: episode.title, coverArtPath: coverArtPath ?? undefined, }); @@ -444,9 +444,7 @@ async function load(episode: Episode): Promise { volume: volume(), speed: storeSpeed || speed(), startPosition: pos > 0 ? pos : undefined, - mediaTitle: podcastTitle - ? `${podcastTitle} — ${episode.title}` - : episode.title, +mediaTitle: episode.title, coverArtPath: coverArtPath ?? undefined, }) .catch(() => {}); @@ -620,9 +618,7 @@ async function switchBackend(name: BackendName): Promise { startPosition: pos, volume: vol, speed: spd, - mediaTitle: podcastTitle - ? `${podcastTitle} — ${ep.title}` - : ep.title, +mediaTitle: ep.title, coverArtPath: coverArtPath ?? undefined, }); setIsPlaying(true); diff --git a/src/index.tsx b/src/index.tsx index 9235826..5df82ac 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -195,7 +195,7 @@ async function handlePlay(feeds: Feed[], arg: string): Promise { ? await fetchCoverArt(coverUrl) : null await backend.play(episodeResult.audioUrl, { - mediaTitle: `${feedResult.podcast.title} — ${episodeResult.title}`, + mediaTitle: episodeResult.title, coverArtPath: coverArtPath ?? undefined, }) console.log("Playback started (use the UI to control)") diff --git a/src/stores/download.ts b/src/stores/download.ts index b997dcc..3989463 100644 --- a/src/stores/download.ts +++ b/src/stores/download.ts @@ -269,6 +269,39 @@ function createDownloadStore() { .exited.catch(() => {}); } } + + // Tag the local file (codec-copy, no re-encode) so mpv's Now + // Playing metadata for local playback is title=episode, + // artist=podcast — the source streams carry no usable tags and + // macOS composes "title - artist" from exactly these fields. + // Atomic: ffmpeg writes a temp file, then renames into place. + if (result.filePath && episode) { + const podcastTitle = + feedStore.feeds().find((f) => f.id === item.feedId)?.podcast.title ?? + downloads().get(item.episodeId)?.podcastTitle; + if (podcastTitle) { + const tmp = `${result.filePath}.tag`; + Bun.spawn([ + "ffmpeg", + "-y", + "-i", + result.filePath, + "-c", + "copy", + "-metadata", + `title=${episode.title}`, + "-metadata", + `artist=${podcastTitle}`, + tmp, + ]) + .exited.then(async (code) => { + if (code !== 0) return; + const { renameSync } = await import("node:fs"); + renameSync(tmp, result.filePath); + }) + .catch(() => {}); + } + } } else { updateDownload(item.episodeId, { status: DownloadStatus.FAILED,