{"id":16424095,"url":"https://github.com/funkatron/now-playing","last_synced_at":"2026-05-08T06:13:54.466Z","repository":{"id":223413701,"uuid":"760211951","full_name":"funkatron/now-playing","owner":"funkatron","description":null,"archived":false,"fork":false,"pushed_at":"2024-12-04T22:53:35.000Z","size":14,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-02-24T15:57:13.692Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/funkatron.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2024-02-20T01:49:23.000Z","updated_at":"2024-12-04T22:53:39.000Z","dependencies_parsed_at":"2024-02-20T05:30:16.823Z","dependency_job_id":"f95ae1ed-02c8-4239-8776-0e9ba310873b","html_url":"https://github.com/funkatron/now-playing","commit_stats":null,"previous_names":["funkatron/now-playing"],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/funkatron%2Fnow-playing","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/funkatron%2Fnow-playing/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/funkatron%2Fnow-playing/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/funkatron%2Fnow-playing/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/funkatron","download_url":"https://codeload.github.com/funkatron/now-playing/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":240587544,"owners_count":19825004,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-11T07:43:06.108Z","updated_at":"2026-05-08T06:13:54.458Z","avatar_url":"https://github.com/funkatron.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Now Playing\n\nThis repo runs a small macOS **now-playing** service. It polls Apple Music or Spotify, keeps local outputs up to date, and exposes current state over HTTP on `127.0.0.1`.\n\n**What you get:**\n\n- **Flat-file output** for OBS and other local consumers\n- Optional **OBS WebSocket** push for text and artwork\n- A **local HTTP API** and a simple **browser viewer**\n- **Apple Music** and **Spotify**, with **auto-detection** of the active provider\n\n## Contents\n\n- [Requirements](#requirements)\n- [Start here](#start-here)\n- [Outputs and HTTP endpoints](#outputs-and-http-endpoints)\n- [Foreground and background](#foreground-and-background)\n- [Smoke test](#smoke-test)\n- [API examples (curl)](#api-examples-curl)\n- [OBS](#obs)\n- [Configuration](#configuration)\n- [Commands](#commands)\n- [Docs maintenance](#docs-maintenance)\n\n## Requirements\n\n- macOS\n- Python 3.9+\n- [uv](https://github.com/astral-sh/uv) (`brew install uv`)\n- Apple Music and/or Spotify installed\n- OBS only if you want WebSocket-driven updates (flat files work without it)\n\n## Start here\n\nInstall dependencies and create a local config file:\n\n```bash\nbrew install uv\nuv sync\nuv run np init-config\n```\n\nRun the service in the foreground and open the viewer:\n\n```bash\nuv run np serve\nopen http://127.0.0.1:8976/\n```\n\nOr install the per-user background service:\n\n```bash\nuv run np install-service\nuv run np status\nopen http://127.0.0.1:8976/\n```\n\n**Behavior notes**\n\n- A bare `uv run np` runs `current --format json`.\n- Global flags such as `--source` and `--idle-text` go **before** the subcommand, e.g. `uv run np --source apple_music current --format text`.\n- `install-service` leaves the LaunchAgent installed and running until you stop or remove it.\n- `uninstall-service` stops the service and removes the plist from `~/Library/LaunchAgents/`.\n- **Apple Music** fits the background LaunchAgent path best.\n- **Spotify** is easier to rely on from an interactive terminal session than from the background agent (see [Foreground and background](#foreground-and-background)).\n\nThe service loads [`config.env`](config.env) when present. Copy from [`config.env.example`](config.env.example) and change only what you need.\n\n## Outputs and HTTP endpoints\n\n### Files (`_data/`)\n\n| File | Role |\n| --- | --- |\n| `_data/current_song.txt` | Rendered text for OBS or editors |\n| `_data/current_track.json` | Structured track payload |\n| `_data/current_artwork.png` | Current artwork image |\n| `_data/now_playing_artworks.txt` | Newline-separated list of artwork paths (when the service tracks multiple) |\n\n### HTTP routes\n\n**Core**\n\n| Method | Path | Description |\n| --- | --- | --- |\n| `GET` | `/current` | Current state as JSON |\n| `GET` | `/current.txt` | Current state as plain text (four-line view) |\n| `GET` | `/artwork` | Artwork path as JSON, or `null` if none |\n| `GET` | `/current_artwork.png` | Current artwork binary |\n| `GET` | `/events` | Server-Sent Events stream for live UI updates |\n| `GET` | `/health` | Health check |\n| `GET` | `/` | Browser viewer |\n\n**Spotify interactive worker** (requires `start-spotify-session`; see below)\n\n| Method | Path | Description |\n| --- | --- | --- |\n| `GET` | `/spotify/current` | Spotify-only state as JSON |\n| `GET` | `/spotify/current.txt` | Spotify-only text view |\n| `GET` | `/spotify/artwork` | Spotify-only artwork path as JSON |\n| `GET` | `/spotify/current_artwork.png` | Spotify-only artwork file |\n| `GET` | `/spotify/` | Spotify-only viewer |\n\n## Foreground and background\n\n**One-shot sync** (writes `_data/` once):\n\n```bash\nuv run np sync\n```\n\n**Foreground server** (polling + HTTP in your terminal):\n\n```bash\nuv run np serve\n```\n\n**Background service** (LaunchAgent):\n\n```bash\nuv run np install-service\nuv run np start-service\nuv run np stop-service\nuv run np restart-service\nuv run np status\nuv run np tail\nuv run np tail --follow\nuv run np uninstall-service\n```\n\n### Spotify interactive session\n\nIf you need Spotify outside the normal LaunchAgent flow:\n\n```bash\nuv run np start-spotify-session\nuv run np stop-spotify-session\n```\n\nThis starts a separate Spotify polling worker in Terminal or iTerm (not via `launchd`). The main process still serves `/` and the usual API; the extra Spotify routes and viewer live under `/spotify/`.\n\n| Action | What to run |\n| --- | --- |\n| Check that it works | `open http://127.0.0.1:8976/spotify/` or `curl http://127.0.0.1:8976/spotify/current` |\n| Stop the worker | `uv run np stop-spotify-session` (does not stop `np serve` in another terminal) |\n\n### Viewer\n\nThe browser UI is minimal: it uses **Server-Sent Events** on `/events`, so updates push from the server instead of polling `/current` on a timer.\n\n### Debugging\n\n- **Port in use:** If `uv run np serve` prints `Address already in use`, stop the installed agent (`uv run np stop-service` or `uninstall-service`) or bind a different port.\n- **Wrong provider:** Run in the foreground so errors print to the terminal:\n\n```bash\nuv run np stop-service\nuv run np serve\nopen http://127.0.0.1:8976/\n```\n\n## Smoke test\n\n```bash\npython3 scripts/smoke_install.py\n```\n\nWith LaunchAgent + HTTP checks:\n\n```bash\npython3 scripts/smoke_install.py --with-service\n```\n\n## API examples (curl)\n\nUse the same paths as in [HTTP routes](#http-routes). Examples:\n\n```bash\ncurl http://127.0.0.1:8976/current\ncurl http://127.0.0.1:8976/current.txt\ncurl http://127.0.0.1:8976/artwork\ncurl http://127.0.0.1:8976/health\ncurl -I http://127.0.0.1:8976/current_artwork.png\n```\n\nViewers:\n\n```bash\nopen http://127.0.0.1:8976/\nopen http://127.0.0.1:8976/spotify/\n```\n\n- `/current` — machine-facing JSON.\n- `/` — human-facing viewer.\n- `/spotify/current` and `/spotify/` — Spotify worker (when that session is running).\n\n## OBS\n\n**Recommended:** file-based integration.\n\n1. Point a text source at `_data/current_song.txt`.\n2. Point an image source at `_data/current_artwork.png`.\n\nThat avoids WebSocket reconnect churn.\n\n**Optional WebSocket pushes:** set in `config.env`:\n\n```bash\nOBSWS_ENABLED=1\nOBSWS_HOST=localhost\nOBSWS_PORT=4455\nOBSWS_PASSWORD=your-password\nOBSWS_IMAGE_INPUT_NAME=NPImage\nOBSWS_TEXT_INPUT_NAME=NPText\nOBSWS_TEXT_FIELD=text\n```\n\nWhen enabled, the service pushes image updates and optional text updates on state changes.\n\n## Configuration\n\nSet variables in [`config.env`](config.env) (loaded automatically by the CLI). Summary:\n\n| Variable | Purpose |\n| --- | --- |\n| `NOW_PLAYING_SOURCE` | `auto`, `apple_music`, or `spotify`. `auto` prefers Apple Music when it is playing, otherwise Spotify. |\n| `NOW_PLAYING_IDLE_TEXT` | Text for `_data/current_song.txt` when idle; leave empty for an empty file. |\n| `NOW_PLAYING_HOST` | Bind address for the HTTP API (keep `127.0.0.1` unless you need remote access). |\n| `NOW_PLAYING_PORT` | HTTP port (default `8976`). |\n| `INTERVAL_SECONDS` | Poll interval for `serve` and the LaunchAgent service. |\n| `PYTHON_LOG_LEVEL` | `DEBUG`, `INFO`, `WARNING`, or `ERROR`. |\n| `OBSWS_ENABLED` | `1` to enable OBS WebSocket; `0` for files only. |\n| `OBSWS_HOST` / `OBSWS_PORT` / `OBSWS_PASSWORD` | OBS WebSocket connection. |\n| `OBSWS_IMAGE_INPUT_NAME` | OBS image input updated with artwork. |\n| `OBSWS_TEXT_INPUT_NAME` | Optional text input; leave blank if OBS reads `_data/current_song.txt` directly. |\n| `OBSWS_TEXT_FIELD` | Field name on the selected text input. |\n| `NOW_PLAYING_LAUNCHD_LABEL` | Label for the plist under `~/Library/LaunchAgents/`. |\n\n## Commands\n\nGlobal pattern:\n\n```bash\nuv run np [--source auto|apple_music|spotify] [--idle-text \"Idle text\"] \u003ccommand\u003e\n```\n\n**Reference**\n\n| Command | What it does |\n| --- | --- |\n| `current --format json` | Current state JSON; best for debugging provider detection. |\n| `current --format text` | Four-line text (matches `_data/current_song.txt`). |\n| `artwork` | Prints artwork path if present. |\n| `sync` | One poll; updates `_data/` and OBS if enabled. |\n| `serve` | Foreground poll loop + HTTP API. |\n| `init-config` | Creates `config.env` from `config.env.example` if missing. |\n| `install-service` | Writes LaunchAgent plist and starts the background service. |\n| `start-service` / `stop-service` / `restart-service` | Control the installed agent without removing the plist. |\n| `status` | JSON: installed, loaded, running, plist path, log path, viewer URL. |\n| `tail` / `tail --follow` | Recent `launchd` logs; `--follow` streams until Ctrl-C. |\n| `start-spotify-session` | Spotify worker in Terminal/iTerm when background path is unreliable. |\n| `stop-spotify-session` | Stops that worker session. |\n| `uninstall-service` | Stops service and removes the plist. |\n\n**Examples**\n\n```bash\nuv run np\nuv run np --source apple_music current --format text\nuv run np --source spotify sync\nuv run np serve --host 127.0.0.1 --port 8976 --interval-seconds 2\nuv run np install-service\nuv run np status\nuv run np tail --follow\nuv run np restart-service\nuv run np start-spotify-session\ncurl http://127.0.0.1:8976/current\ncurl http://127.0.0.1:8976/spotify/current\n```\n\n## Docs maintenance\n\nKeep the happy path before deep reference. When you add commands, routes, or side effects, update this file and the [Contents](#contents) list if you add or rename sections.\n\n**Layout:** This README is the operator-facing guide in one place. Splitting into multiple files under `docs/` is only worth it if the README stops being easy to scroll and search—until then, prefer one file plus [TOOL_DOCS_NOTES.md](TOOL_DOCS_NOTES.md) and [AGENTS.md](AGENTS.md) for process and agent context.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffunkatron%2Fnow-playing","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffunkatron%2Fnow-playing","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffunkatron%2Fnow-playing/lists"}