feat(media): expose podcast name + cover art to system Now Playing
- mpv: --force-media-title '<podcast> — <episode>' so macOS Now Playing shows the podcast name instead of the download-hash filename (mediaTitle PlayOption) - media registry: artist is now the human podcast title (customName || podcast.title), falling back to podcastId - downloads: write the podcast cover as a <base>.jpg sibling so mpv's cover-art-auto=exact picks it up for artwork; delete it with the download
This commit is contained in:
@@ -189,6 +189,10 @@ async function play(episode: Episode): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
// 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();
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user