diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 645f203..281e6e4 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -50,6 +50,12 @@ jobs:
/tmp/npd-packaging-venv/bin/now-playing --help
/tmp/npd-packaging-venv/bin/python -m now_playing_desktops --help
+ - name: PyInstaller Linux release binary
+ run: |
+ pip install -e ".[linux]"
+ pyinstaller --noconfirm packaging/now-playing-linux.spec
+ dist/now-playing-linux --help
+
- name: Tests
run: pytest --cov=now_playing_desktops --cov-report=term-missing -q
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 249ec61..4ea25e5 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -7,7 +7,7 @@ on:
workflow_dispatch:
inputs:
dry_run:
- description: "Build Windows .exe as artifact only (no GitHub Release or PyPI)"
+ description: "Build Windows/macOS/Linux binaries as artifacts only (no GitHub Release or PyPI)"
type: boolean
default: true
@@ -51,6 +51,74 @@ jobs:
files: dist/now-playing-windows.exe
generate_release_notes: true
+ macos-app:
+ name: macOS executable
+ runs-on: macos-latest
+ steps:
+ - uses: actions/checkout@v7
+
+ - name: Set up Python
+ uses: actions/setup-python@v7
+ with:
+ python-version: "3.12"
+ cache: pip
+
+ - name: Install package and PyInstaller
+ run: |
+ python -m pip install --upgrade pip
+ pip install -e ".[macos]"
+ pip install pyinstaller
+
+ - name: Build now-playing-macos
+ run: pyinstaller --noconfirm packaging/now-playing-macos.spec
+
+ - name: Upload dry-run artifact
+ if: github.event_name == 'workflow_dispatch'
+ uses: actions/upload-artifact@v4
+ with:
+ name: now-playing-macos
+ path: dist/now-playing-macos
+
+ - name: Attach to GitHub Release
+ if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
+ uses: softprops/action-gh-release@v2
+ with:
+ files: dist/now-playing-macos
+
+ linux-app:
+ name: Linux executable
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v7
+
+ - name: Set up Python
+ uses: actions/setup-python@v7
+ with:
+ python-version: "3.12"
+ cache: pip
+
+ - name: Install package and PyInstaller
+ run: |
+ python -m pip install --upgrade pip
+ pip install -e ".[linux]"
+ pip install pyinstaller
+
+ - name: Build now-playing-linux
+ run: pyinstaller --noconfirm packaging/now-playing-linux.spec
+
+ - name: Upload dry-run artifact
+ if: github.event_name == 'workflow_dispatch'
+ uses: actions/upload-artifact@v4
+ with:
+ name: now-playing-linux
+ path: dist/now-playing-linux
+
+ - name: Attach to GitHub Release
+ if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
+ uses: softprops/action-gh-release@v2
+ with:
+ files: dist/now-playing-linux
+
publish-pypi:
name: Publish to PyPI
runs-on: ubuntu-latest
diff --git a/README.md b/README.md
index eb301f2..281a0d7 100644
--- a/README.md
+++ b/README.md
@@ -14,9 +14,21 @@

+## Download (pre-built)
+
+GitHub [Releases](https://github.com/hippi345/nowPlayingDesktops/releases) attach one-file PyInstaller builds per OS (also available as workflow artifacts from **Actions → Release → Run workflow** with dry run enabled):
+
+| OS | Download | Run |
+|----|----------|-----|
+| **Windows** | `now-playing-windows.exe` | Double-click or run from a terminal |
+| **macOS** | `now-playing-macos` | `chmod +x now-playing-macos && ./now-playing-macos run --source local` |
+| **Linux** | `now-playing-linux` | `chmod +x now-playing-linux && ./now-playing-linux run --source local` |
+
+Or install with [pipx](https://pipx.pypa.io/): `pipx install now-playing-desktops`
+
## Quick start (Windows)
-1. **Get the app** — download `now-playing-windows.exe` from [Releases](https://github.com/hippi345/nowPlayingDesktops/releases), or install with [pipx](https://pipx.pypa.io/): `pipx install now-playing-desktops`
+1. **Get the app** — download `now-playing-windows.exe` from [Releases](https://github.com/hippi345/nowPlayingDesktops/releases), or use pipx (above)
2. **Run it** — double-click the `.exe`, or run `now-playing run --source local` (pipx puts `now-playing` on your PATH)
3. **Done** — play something in the Spotify desktop app; pause or exit to restore your wallpaper
@@ -59,7 +71,10 @@ now-playing run --source local
### Linux (local)
-**Prerequisites**
+1. **Get the app** — download `now-playing-linux` from [Releases](https://github.com/hippi345/nowPlayingDesktops/releases), `chmod +x` it, and run `./now-playing-linux run --source local`, or install from source below.
+2. **Player** — Spotify desktop or another **MPRIS** player; pause or exit restores your wallpaper.
+
+**Prerequisites (source install)**
- **Python 3.12+**
- **Install:** `pip install -e ".[linux]"` (`dbus-next` for MPRIS).
@@ -78,7 +93,10 @@ now-playing run --source local
### macOS (local)
-**Prerequisites**
+1. **Get the app** — download `now-playing-macos` from [Releases](https://github.com/hippi345/nowPlayingDesktops/releases), `chmod +x` it, and run `./now-playing-macos run --source local`, or install from source below.
+2. **Player** — **Spotify.app** on this Mac; pause or exit restores your wallpaper.
+
+**Prerequisites (source install)**
- **Python 3.12+**
- **Install:** `pip install -e ".[macos]"` (`appscript` for Spotify AppleScript control).
diff --git a/packaging/now-playing-linux.spec b/packaging/now-playing-linux.spec
new file mode 100644
index 0000000..a7fbc97
--- /dev/null
+++ b/packaging/now-playing-linux.spec
@@ -0,0 +1,48 @@
+# -*- mode: python ; coding: utf-8 -*-
+"""PyInstaller spec for now-playing-linux (run on ubuntu-latest)."""
+
+from PyInstaller.utils.hooks import collect_all
+
+block_cipher = None
+
+pkg_datas, pkg_binaries, pkg_hidden = collect_all("now_playing_desktops")
+dbus_datas, dbus_binaries, dbus_hidden = collect_all("dbus_next")
+pkg_datas += dbus_datas
+pkg_binaries += dbus_binaries
+hiddenimports = list(pkg_hidden) + list(dbus_hidden)
+
+a = Analysis(
+ ["pyinstaller_linux_entry.py"],
+ pathex=["../src"],
+ binaries=pkg_binaries,
+ datas=pkg_datas,
+ hiddenimports=hiddenimports,
+ hookspath=[],
+ hooksconfig={},
+ runtime_hooks=[],
+ excludes=[],
+ noarchive=False,
+ optimize=0,
+)
+pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
+
+exe = EXE(
+ pyz,
+ a.scripts,
+ a.binaries,
+ a.datas,
+ [],
+ name="now-playing-linux",
+ debug=False,
+ bootloader_ignore_signals=False,
+ strip=False,
+ upx=True,
+ upx_exclude=[],
+ runtime_tmpdir=None,
+ console=True,
+ disable_windowed_traceback=False,
+ argv_emulation=False,
+ target_arch=None,
+ codesign_identity=None,
+ entitlements_file=None,
+)
diff --git a/packaging/now-playing-macos.spec b/packaging/now-playing-macos.spec
new file mode 100644
index 0000000..5ccf4a1
--- /dev/null
+++ b/packaging/now-playing-macos.spec
@@ -0,0 +1,48 @@
+# -*- mode: python ; coding: utf-8 -*-
+"""PyInstaller spec for now-playing-macos (run on macos-latest)."""
+
+from PyInstaller.utils.hooks import collect_all
+
+block_cipher = None
+
+pkg_datas, pkg_binaries, pkg_hidden = collect_all("now_playing_desktops")
+app_datas, app_binaries, app_hidden = collect_all("appscript")
+pkg_datas += app_datas
+pkg_binaries += app_binaries
+hiddenimports = list(pkg_hidden) + list(app_hidden)
+
+a = Analysis(
+ ["pyinstaller_macos_entry.py"],
+ pathex=["../src"],
+ binaries=pkg_binaries,
+ datas=pkg_datas,
+ hiddenimports=hiddenimports,
+ hookspath=[],
+ hooksconfig={},
+ runtime_hooks=[],
+ excludes=[],
+ noarchive=False,
+ optimize=0,
+)
+pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
+
+exe = EXE(
+ pyz,
+ a.scripts,
+ a.binaries,
+ a.datas,
+ [],
+ name="now-playing-macos",
+ debug=False,
+ bootloader_ignore_signals=False,
+ strip=False,
+ upx=True,
+ upx_exclude=[],
+ runtime_tmpdir=None,
+ console=True,
+ disable_windowed_traceback=False,
+ argv_emulation=False,
+ target_arch=None,
+ codesign_identity=None,
+ entitlements_file=None,
+)
diff --git a/packaging/pyinstaller_linux_entry.py b/packaging/pyinstaller_linux_entry.py
new file mode 100644
index 0000000..d3885ff
--- /dev/null
+++ b/packaging/pyinstaller_linux_entry.py
@@ -0,0 +1,6 @@
+"""PyInstaller entry point for the Linux console build."""
+
+from now_playing_desktops.cli import main
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/packaging/pyinstaller_macos_entry.py b/packaging/pyinstaller_macos_entry.py
new file mode 100644
index 0000000..9d31afc
--- /dev/null
+++ b/packaging/pyinstaller_macos_entry.py
@@ -0,0 +1,6 @@
+"""PyInstaller entry point for the macOS console build."""
+
+from now_playing_desktops.cli import main_macos
+
+if __name__ == "__main__":
+ main_macos()