diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bc02803..5fbd16a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -50,10 +50,7 @@ jobs: - name: Install fftw (cavacore build dependency) run: | if uname -s | grep -qi darwin; then - # mpv is required for the release bundle: build.ts copies it into - # PodTui.app (signed with the podtui bundle identifier) so macOS - # Now Playing shows the PodTui icon instead of a blank placeholder. - brew install fftw mpv + brew install fftw else sudo apt-get update sudo apt-get install -y libfftw3-dev @@ -79,16 +76,6 @@ jobs: printf 'preload = ["./definitely-missing.ts"]\n' > "$SMOKE_DIR/bunfig.toml" cd "$SMOKE_DIR" ./podtui-*/podtui --version - # macOS tarballs must ship PodTui.app with a working bundled mpv - # carrying the podtui bundle identifier — otherwise Now Playing - # attribution silently regresses to a blank icon. - if [ "${{ matrix.plat }}" = "darwin" ]; then - MPV=./podtui-*/PodTui.app/Contents/MacOS/mpv - test -x $MPV || { echo "PodTui.app missing bundled mpv"; exit 1; } - $MPV --version >/dev/null || { echo "bundled mpv does not launch"; exit 1; } - codesign -dvv $MPV 2>&1 | grep -q "Identifier=com.mikefreno.podtui" \ - || { echo "bundled mpv lacks podtui signing identifier"; exit 1; } - fi - name: Upload artifact uses: actions/upload-artifact@v6 diff --git a/README.md b/README.md index eef054e..84738b6 100644 --- a/README.md +++ b/README.md @@ -54,14 +54,6 @@ Linux (arm64/x64). Pick whichever fits your platform. brew install mikefreno/tap/podtui ``` -On macOS the tarball also ships a `PodTui.app` bundle. PodTui plays audio -through a copy of mpv that lives **inside the bundle**, so macOS attributes -the Now Playing session to PodTui — the Control Center / lock-screen entry -shows the PodTui name and icon, and podcast cover art as its artwork — -rather than a blank placeholder for an unbundled binary. Installers can drop -`PodTui.app` into `/Applications`; the `podtui` entry point should point at -`PodTui.app/Contents/MacOS/podtui` so the bundled mpv is used. - ### 2. Standalone tarball (all platforms) Grab `podtui--.tar.gz` from the latest diff --git a/assets/App Icon/AppIcon.icns b/assets/App Icon/AppIcon.icns deleted file mode 100644 index c2326d3..0000000 Binary files a/assets/App Icon/AppIcon.icns and /dev/null differ diff --git a/build.ts b/build.ts index 65f8cd4..b41e091 100644 --- a/build.ts +++ b/build.ts @@ -131,130 +131,6 @@ if (COMPILE) { } } - // macOS app bundle: PodTui.app. We run our audio backend (mpv) from - // INSIDE the bundle (Contents/MacOS/mpv) so macOS attributes its Now - // Playing session to PodTui — the source-app icon + name in Control - // Center / lock screen — instead of a blank placeholder for an - // unbundled binary. AudioPlayer's resolver prefers this sibling. - if (platform === "darwin") { - const appRoot = join(tarRoot, "PodTui.app"); - const macosDir = join(appRoot, "Contents", "MacOS"); - const resDir = join(appRoot, "Contents", "Resources"); - mkdirSync(macosDir, { recursive: true }); - mkdirSync(resDir, { recursive: true }); - - copyFileSync(outfile, join(macosDir, "podtui")); - for (const lib of [`libopentui.${libExt}`, cavacoreLib]) { - const s = join("dist", lib); - if (existsSync(s)) copyFileSync(s, join(macosDir, lib)); - } - - const mpvResolve = Bun.spawnSync(["which", "mpv"]); - const mpvPath = - mpvResolve.exitCode === 0 ? mpvResolve.stdout.toString().trim() : ""; - if (mpvPath) { - copyFileSync(mpvPath, join(macosDir, "mpv")); - } else { - // A darwin release tarball without a bundled mpv silently ships - // without Now Playing attribution (blank icon). Fail loudly so CI - // can't produce it — the runner must have mpv installed. - console.error( - "Error: mpv not found in PATH — PodTui.app requires a bundled mpv for macOS Now Playing attribution (brew install mpv on the build machine)", - ); - process.exit(1); - } - - const icnsSrc = join("assets", "App Icon", "AppIcon.icns"); - if (existsSync(icnsSrc)) { - copyFileSync(icnsSrc, join(resDir, "AppIcon.icns")); - } else { - console.warn( - "Warning: assets/App Icon/AppIcon.icns missing — app bundle has no icon", - ); - } - - // Version for the bundle comes from src/index.tsx (single source of - // truth — release.yml requires bumping it in the tag commit). - const srcIndex = await Bun.file(join("src", "index.tsx")).text(); - const versionMatch = srcIndex.match(/const VERSION = "([^"]+)"/); - const bundleVersion = versionMatch?.[1]; - if (!bundleVersion) { - console.error("Error: could not read VERSION from src/index.tsx"); - process.exit(1); - } - - Bun.write( - join(appRoot, "Contents", "Info.plist"), - ` - - - - CFBundleName - PodTui - CFBundleDisplayName - PodTui - CFBundleIdentifier - com.mikefreno.podtui - CFBundleExecutable - podtui - CFBundlePackageType - APPL - CFBundleIconFile - AppIcon - CFBundleShortVersionString - ${bundleVersion} - CFBundleVersion - ${bundleVersion} - LSMinimumSystemVersion - 12.0 - - -`, - ); - - // Ad-hoc sign so the bundle launches cleanly on fresh machines. - // Identity overridable via PODTUI_CODESIGN_IDENTITY (e.g. a Developer - // ID cert for release builds); default ad-hoc. - const signIdentity = process.env.PODTUI_CODESIGN_IDENTITY || "-"; - const sign = Bun.spawnSync([ - "codesign", - "--force", - "--deep", - "-s", - signIdentity, - appRoot, - ]); - if (sign.exitCode !== 0) { - console.warn( - `Warning: codesign failed (${sign.stderr.toString().trim()}) — app bundle unsigned`, - ); - } - - // Sign the nested mpv LAST with our bundle identifier. mediaremoted - // resolves the Now Playing client from the registering process's - // code-signing identifier — without an explicit --identifier codesign - // stamps "mpv" (its basename) and the audio center shows a blank - // placeholder. Must run after the bundle sign above (a later bundle - // re-seal would re-derive the basename identifier). - const signMpv = Bun.spawnSync([ - "codesign", - "--force", - "-s", - signIdentity, - "--identifier", - "com.mikefreno.podtui", - join(macosDir, "mpv"), - ]); - if (signMpv.exitCode !== 0) { - console.warn( - `Warning: nested mpv signing failed (${signMpv.stderr - .toString() - .trim()}) — Now Playing attribution won't work`, - ); - } - console.log(`App bundle: ${appRoot}`); - } - const tar = Bun.spawnSync([ "tar", "-czf", diff --git a/src/utils/audio-player.ts b/src/utils/audio-player.ts index e1d82ed..6f51c76 100644 --- a/src/utils/audio-player.ts +++ b/src/utils/audio-player.ts @@ -29,7 +29,7 @@ import { platform } from "os"; import { existsSync, unlinkSync } from "fs"; import { tmpdir } from "os"; -import { dirname, join } from "path"; +import { join } from "path"; import type { Socket, Subprocess } from "bun"; // ── Types ──────────────────────────────────────────────────────────── @@ -115,53 +115,13 @@ function mpvSocketPath(): string { // Per-instance, not just per-pid: tests (and backend switching) create // several MpvBackend objects in ONE bun process — a pid-only path makes // every daemon bind the same socket, so later daemons unlink the path - // out from under earlier ones and IPC cross-talks between backends. + // out from under each other. return join( tmpdir(), `podtui-mpv-${process.pid}-${mpvInstance++}.sock`, ); } -/** - * mpv executable to use. Prefers a sibling `mpv` inside the app bundle - * (macOS PodTui.app/Contents/MacOS/mpv): running mpv from inside the bundle - * makes macOS attribute its Now Playing session to PodTui — source-app icon - * and name in Control Center — instead of a blank placeholder for an - * unbundled binary. - * - * The bundled copy is verified to actually launch: it links against brew's - * dylibs by absolute path, and a Homebrew ffmpeg major upgrade can break it - * (dylib gone → immediate non-zero exit). If the bundled binary can't run, - * fall back to PATH mpv so audio keeps working — the icon degrades to blank - * rather than playback dying. Probed once per process. - */ -let resolvedMpv: string | null | undefined; // undefined = not yet probed - -function mpvLaunches(binary: string): boolean { - try { - const proc = Bun.spawnSync([binary, "--version"], { timeout: 3000 }); - return proc.exitCode === 0; - } catch { - return false; - } -} - -function resolveMpvBinary(): string | null { - if (resolvedMpv !== undefined) return resolvedMpv; - let resolved: string | null = null; - try { - const bundled = join(dirname(process.execPath), "mpv"); - if (existsSync(bundled) && mpvLaunches(bundled)) { - resolved = bundled; - } - } catch { - /* process.execPath unusable — fall through to PATH */ - } - if (!resolved) resolved = which("mpv"); - resolvedMpv = resolved; - return resolved; -} - // ── mpv JSON IPC connection ───────────────────────────────────────── // // One persistent Unix-socket connection to the resident mpv daemon. Lines @@ -377,7 +337,7 @@ export class MpvBackend implements AudioBackend { this.proc = Bun.spawn( [ - resolveMpvBinary() ?? "mpv", + "mpv", "--no-video", "--no-terminal", "--really-quiet", @@ -799,7 +759,7 @@ export interface DetectedPlayer { export function detectPlayers(): DetectedPlayer[] { const players: DetectedPlayer[] = []; - const mpvPath = resolveMpvBinary(); + const mpvPath = which("mpv"); if (mpvPath) { players.push({ name: "mpv", @@ -833,13 +793,13 @@ export function createAudioBackend(preferred?: BackendName): AudioBackend { if (backend) return backend; } - return resolveMpvBinary() ? new MpvBackend() : new NoopBackend(); + return which("mpv") ? new MpvBackend() : new NoopBackend(); } function createBackendByName(name: BackendName): AudioBackend | null { switch (name) { case "mpv": - return resolveMpvBinary() ? new MpvBackend() : null; + return which("mpv") ? new MpvBackend() : null; case "none": return new NoopBackend(); }