Skip to content
gregoryjayukPublic

About

A macOS menu-bar remote for MPD: a record that slides out and turns at 33⅓ rpm with a tonearm tracking the song, live updates, playlists, instant library search, media keys and a CLI. Swift, no dependencies.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

Needle

A menu-bar remote for MPD, built for an always-on home music server that plays a 263,600-song library through an amp. Needle lives in the Mac's menu bar as a small level meter beside the song that's playing; click it and a panel drops down with the record.

Four albums, four colours: Needle takes its colours from the cover of whatever is playing. Shown on Up Next, History, Search and Most played

The panel changing colour by itself as the album changes, the record turning on the platter

What it does

  • The record. The cover sits in its sleeve; when the server plays, the vinyl slides out and turns at a true 33⅓ rpm, and the tonearm swings over, drops onto the lead-in groove and tracks inward as the song plays. Pause and it stops where it was; stop and it slides back in. The panel's backdrop is the cover itself, blurred, and the controls borrow the cover's colour.
  • It takes its colours from the cover. Each cover lends the panel its colour: the blurred backdrop, the play button, scrubber and volume, the record's label (the cover itself, turning) and even the cartridge on the tonearm. A new album, and the whole panel cross-fades to it. Songs without a cover get one drawn for them, seeded by the album, so an album always looks the same.
  • Listening history. History shows what played, day by day and album by album, for the last 30 days ("Dive · 6 songs · 10:02–10:23"), and Most played ranks every song by how often it has been heard; the playing song shows its own count ("played 6×"). The plays are recorded on the server by needle-history (below), so they include everything played from any client, whether or not this Mac was awake.
  • Everything live. Needle holds an idle connection, so anything that happens on the server — from this panel, from mpc on the server, from ncmpcpp, from the phone — shows up at once.
  • The controls you'd expect: play/pause, previous (back to the start first, like a CD player), next, a scrubber, shuffle, repeat (off → all → one song), volume with mute.
  • Up Next, Playlists, Search, History. The queue with the playing song marked; the saved playlists (click to play, or shuffle, or append); a search that answers artists and albums as you type from a local index of the library, then songs a moment later (Return plays the first song); and what has played. A listen in History can be played again exactly as it went.
  • From the menu bar itself: ⌥-click or middle-click to play/pause without opening anything, scroll over the icon to change the volume, right-click for volume presets and playlists.
  • Media keys and Control Center. While the server is playing, the keyboard's play/pause, next and previous keys drive it, and Control Center's media tile shows the song, its cover and a scrubber. Offline or stopped, Needle lets go of the keys.
  • A terminal command, needle: needle status, needle toggle, needle next, needle vol +5, needle load slow-evening, needle search pale blue eyes, needle history 2 (the last two days), needle top 20.

Things it is careful about

  • It never wakes a sleeping drive. Cover art is read from the server only for the song that is playing (that drive is spinning anyway) and kept on this Mac, one image per album folder. Lists show only covers already cached here, so browsing never touches the drives; everything else Needle asks for comes from MPD's in-memory song database.
  • The volume doesn't jump. A PipeWire output reopens at 100 % when playback starts from stopped, and the amp is loud. When Needle starts playback, it puts the previous volume back the moment MPD reports the jump (Settings can turn this off).
  • Away from home it goes quiet. If this Mac is not on the server's network, Needle stops retrying on a timer — it never keeps knocking on whatever owns the server's address on someone else's network — and reconnects when the network changes or the panel is opened.
  • It costs nothing while hidden. The menu-bar meter is animated by Core Animation, outside the app (0.0 % CPU measured while playing); the panel's clocks stop when it closes.

Install

scripts/install.sh

Builds a release, bundles ~/Applications/Needle.app, starts it at login with a LaunchAgent (com.greggaynor.needle), and links the CLI as ~/.local/bin/needle. The first time it runs, macOS asks whether Needle may find devices on the local network: allow it, or it cannot reach the server. scripts/uninstall.sh removes it all (--all also clears its settings and cover cache).

The server defaults to localhost:6600. Change it in the panel's Settings (the gear, or ⌘,), or set NEEDLE_HOST/NEEDLE_PORT (or the classic MPD_HOST, which may be password@host, and MPD_PORT) for the CLI.

Listening history on the server

server/needle-history.py is a small Python script (standard library only) that runs beside MPD as a systemd --user service:

server/install.sh <ssh-host-of-the-mpd-machine>

It watches MPD and counts a song as played once it has been heard for half its length or four minutes, whichever comes first (Last.fm's rule): skips and pauses don't count. Each play is written as three MPD stickers on the song — playcount, lastplayed, and plays (its last 200 play times) — which any MPD client can read and which Needle's History is built from, and as a line in ~/.local/share/needle-history/plays.jsonl, the complete record. It never opens a music file: stickers live in MPD's sticker database, tags come from MPD's song database, so it cannot wake a sleeping drive either. Its first run imports the player: played lines MPD has already written to the systemd journal, leaving out the ones that were skips (judged by the time to the next line). On the server: needle-history --recent 2, --top 20, and --reset --yes to forget everything.

In the panel

key does
space play / pause
← → previous / next
↑ ↓ volume ±5
⌘F search
⌘1 ⌘2 ⌘3 ⌘4 Up Next, Playlists, Search, History
⌘, settings
esc clear the search, then close

pkill -USR1 -x Needle toggles the panel, so a window-manager hotkey can open it.

Building and testing

Swift Package, no dependencies, builds with the Command Line Tools alone (swift build). Note for this toolchain: in the macOS 27 SDK SwiftUI's @State is a macro whose plugin ships only with Xcode, so views use @Local (in Support.swift), a @StateObject-backed stand-in.

  • scripts/mock-mpd.py [port] [timeout] is a small fake MPD — playing, with covers, live idle notifications, MPD's habit of hanging up on silent clients (timeout, 60 s by default) and even the PipeWire volume quirk — so the controls can be exercised without touching the real music.
  • Needle selftest (with NEEDLE_HOST=127.0.0.1 NEEDLE_PORT=6601, against the mock) drives the real player model through every control and checks what the server ends up doing. It refuses to run against anything but 127.0.0.1.
  • Needle probe is a timed, read-only health check of every path the app uses against the real server.
  • Needle --show [--tab queue|playlists|search|history] [--query words] [--most-played] [--settings] opens the panel on launch; with NEEDLE_PIN=1 it stays open when focus moves, for screenshots, and NEEDLE_CACHE_DIR keeps a test run's covers and index away from the real ones. Outside the .app bundle, a bare Needle (or needle) prints the status; --app starts the menu-bar app instead.
  • python3 server/test-needle-history.py 127.0.0.1:6601 tests the recorder's counting rule (no server needed for that part) and its sticker writes and journal import against the mock.
  • scripts/make-screenshots.sh makes this README's pictures from the mock (it seeds a fortnight of listening and draws six covers of its own), capturing Needle's panel window alone.
file what
MPD.swift the wire: one connection, command lists, binary chunks, deadlines
MPDClient.swift self-healing command channel and every command Needle sends
Player.swift the model: three channels (commands, idle, art), reconnects, every control
LibraryIndex.swift albums and artists held locally for instant search
History.swift plays grouped into days and listens, and the most played
Artwork.swift covers, their cache, and the colour taken from each
Hero.swift the record, the tonearm, generated covers
PanelView.swift, Lists.swift the rest of the panel
StatusItem.swift the menu-bar item and its meter
NowPlaying.swift media keys and Control Center
CLI.swift, SelfTest.swift, Icon.swift the terminal command, the self-test, the app icon (drawn by the same code as the record)
server/ needle-history, its systemd unit, installer and tests

Licence

GPL-2.0 — see LICENSE.

About

A macOS menu-bar remote for MPD: a record that slides out and turns at 33⅓ rpm with a tonearm tracking the song, live updates, playlists, instant library search, media keys and a CLI. Swift, no dependencies.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages