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 vol = volume();
|
||||||
const spd = storeSpeed || speed();
|
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
|
// Resume from saved progress if available and not completed
|
||||||
const savedProgress = progressStore.get(episode.id);
|
const savedProgress = progressStore.get(episode.id);
|
||||||
let startPos = 0;
|
let startPos = 0;
|
||||||
@@ -200,6 +204,7 @@ async function play(episode: Episode): Promise<void> {
|
|||||||
volume: vol,
|
volume: vol,
|
||||||
speed: spd,
|
speed: spd,
|
||||||
startPosition: startPos > 0 ? startPos : undefined,
|
startPosition: startPos > 0 ? startPos : undefined,
|
||||||
|
mediaTitle: podcastTitle ? `${podcastTitle} — ${episode.title}` : episode.title,
|
||||||
});
|
});
|
||||||
|
|
||||||
setCurrentEpisode(episode);
|
setCurrentEpisode(episode);
|
||||||
@@ -212,7 +217,7 @@ async function play(episode: Episode): Promise<void> {
|
|||||||
const media = useMediaRegistry();
|
const media = useMediaRegistry();
|
||||||
media.setNowPlaying({
|
media.setNowPlaying({
|
||||||
title: episode.title,
|
title: episode.title,
|
||||||
artist: episode.podcastId,
|
artist: podcastTitle || episode.podcastId,
|
||||||
duration: episode.duration,
|
duration: episode.duration,
|
||||||
});
|
});
|
||||||
media.setPlaybackState(true);
|
media.setPlaybackState(true);
|
||||||
@@ -363,10 +368,18 @@ async function switchBackend(name: BackendName): Promise<void> {
|
|||||||
// Resume playback if we were playing
|
// Resume playback if we were playing
|
||||||
if (wasPlaying && ep && ep.audioUrl) {
|
if (wasPlaying && ep && ep.audioUrl) {
|
||||||
try {
|
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, {
|
await backend.play(ep.audioUrl, {
|
||||||
startPosition: pos,
|
startPosition: pos,
|
||||||
volume: vol,
|
volume: vol,
|
||||||
speed: spd,
|
speed: spd,
|
||||||
|
mediaTitle: podcastTitle
|
||||||
|
? `${podcastTitle} — ${ep.title}`
|
||||||
|
: ep.title,
|
||||||
});
|
});
|
||||||
setIsPlaying(true);
|
setIsPlaying(true);
|
||||||
startPolling();
|
startPolling();
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import type { DownloadedEpisode } from "../types/episode";
|
|||||||
import type { Episode } from "../types/episode";
|
import type { Episode } from "../types/episode";
|
||||||
import { downloadEpisode } from "../utils/episode-downloader";
|
import { downloadEpisode } from "../utils/episode-downloader";
|
||||||
import { ensureConfigDir, getConfigFilePath } from "../utils/config-dir";
|
import { ensureConfigDir, getConfigFilePath } from "../utils/config-dir";
|
||||||
|
import { useFeedStore } from "./feed";
|
||||||
|
|
||||||
const DOWNLOADS_FILE = "downloads.json";
|
const DOWNLOADS_FILE = "downloads.json";
|
||||||
const MAX_CONCURRENT = 2;
|
const MAX_CONCURRENT = 2;
|
||||||
@@ -201,6 +202,27 @@ function createDownloadStore() {
|
|||||||
speed: 0,
|
speed: 0,
|
||||||
error: null,
|
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 {
|
} else {
|
||||||
updateDownload(item.episodeId, {
|
updateDownload(item.episodeId, {
|
||||||
status: DownloadStatus.FAILED,
|
status: DownloadStatus.FAILED,
|
||||||
@@ -306,6 +328,11 @@ function createDownloadStore() {
|
|||||||
try {
|
try {
|
||||||
const { unlink } = await import("fs/promises");
|
const { unlink } = await import("fs/promises");
|
||||||
await unlink(dl.filePath);
|
await unlink(dl.filePath);
|
||||||
|
const dot = dl.filePath.lastIndexOf(".");
|
||||||
|
if (dot > 0) {
|
||||||
|
const coverPath = dl.filePath.slice(0, dot) + ".jpg";
|
||||||
|
await unlink(coverPath);
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// File may already be gone
|
// File may already be gone
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export interface PlayOptions {
|
|||||||
startPosition?: number;
|
startPosition?: number;
|
||||||
volume?: number;
|
volume?: number;
|
||||||
speed?: number;
|
speed?: number;
|
||||||
|
mediaTitle?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Utilities ────────────────────────────────────────────────────────
|
// ── Utilities ────────────────────────────────────────────────────────
|
||||||
@@ -109,6 +110,10 @@ export class MpvBackend implements AudioBackend {
|
|||||||
`--speed=${opts?.speed ?? 1}`,
|
`--speed=${opts?.speed ?? 1}`,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (opts?.mediaTitle) {
|
||||||
|
args.push(`--force-media-title=${opts.mediaTitle}`);
|
||||||
|
}
|
||||||
|
|
||||||
if (opts?.startPosition && opts.startPosition > 0) {
|
if (opts?.startPosition && opts.startPosition > 0) {
|
||||||
args.push(`--start=${opts.startPosition}`);
|
args.push(`--start=${opts.startPosition}`);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user