An interactive media controller for the macOS Lock Screen. Controls music playing locally on your Mac in Spotify or Apple Music.
The Lock Screen card is the entire interface — there is no player window and no menu-bar player. While the Mac is unlocked, Nocta is invisible apart from a menu-bar item that opens Settings and quits.
Lock your Mac and the current track stays on screen — artwork, title, artist, live progress — with working play/pause, skip and seek controls you can use without unlocking.
The controller is one wide Liquid Glass card with the artwork inset inside it. Click the artwork and it comes apart: the artwork lifts out and stands on its own above the player, which keeps the card's width, its control row and its type, and gains the album line and volume it had no room for. Click again to put it back together.
Verified working on macOS 26.6.2 (build 25G83). Built with Xcode 26.6 / Swift 6.3.3.
| Feature | State |
|---|---|
| Lock Screen controller, interactive | Working — verified with clicks registered while locked |
| Expandable Lock Screen player | Working — click the artwork to detach it, again to collapse |
| Spotify (Apple Events) | Working — play/pause, skip, seek, volume, shuffle, repeat on/off |
| Spotify repeat-one | Working after the optional Web API sign-in |
| Apple Music (Apple Events) | Working — as above plus native mute and favourite |
| Automatic service detection | Working |
| Global keyboard shortcuts | Working while unlocked (see limitations) |
| Spotify favourite / "like" | Advanced opt-in — needs a Spotify developer app and Premium |
| Timed lyrics on the card | Working — opt-in, off by default (LRCLIB) |
| Multi-display Lock Screen | Best-effort, see docs/LOCK_SCREEN.md |
- macOS 14.0 or later (Lock Screen feature verified on 26.6; see fragility notes)
- Spotify.app and/or Music.app installed
- No Spotify Premium and no account setup — playback control is local, over Apple Events
- Liking a Spotify track is the one exception: it is an opt-in extra for advanced users, and Spotify's own platform rules now require a Premium account to set it up. See Spotify sign-in.
brew install xcodegen
./Scripts/build.shProduces dist/Nocta.app. To make a disk image:
./Scripts/package-dmg.shNocta has no Developer ID certificate behind it, so Scripts/build.sh produces an
ad-hoc signed, unnotarised app. macOS refuses to open it on a double-click.
On macOS 15 (Sequoia) and later, the old right-click → Open bypass no longer works. Double-click the app so the block is recorded, then open System Settings → Privacy & Security, scroll to Security, and click Open Anyway next to the "Nocta.app was blocked" line. The entry only appears after a blocked launch and expires after about an hour.
Note the build is arm64-only — it will not launch on an Intel Mac. For a universal
binary, build with ARCHS="arm64 x86_64" ONLY_ACTIVE_ARCH=NO.
When sending the app to someone else, send the zip Scripts/build.sh writes to
dist/Nocta.zip (ditto -c -k --keepParent), not the bare .app — mail and chat clients
mangle bundles. dist/ is build output and is not tracked in this repository.
If you have a Developer ID certificate and want a notarised build:
xcrun notarytool store-credentials "nocta" \
--apple-id "you@example.com" --team-id "TEAMID" --password "app-specific-password"
DEVELOPER_ID="Developer ID Application: Your Name (TEAMID)" ./Scripts/notarize.shNocta requests exactly one permission: Automation (Apple Events), so it can read what's playing and send playback commands to Spotify and Music. macOS prompts the first time.
It does not request Accessibility, Screen Recording, or Full Disk Access. Global shortcuts
use Carbon's RegisterEventHotKey, which needs no permission — deliberately chosen over a
CGEventTap, which would have required Accessibility for no added benefit.
If you deny the prompt, the player shows "No permission to control Spotify" with a link to the right pane of System Settings. It never fails silently.
SwiftUI (LockScreenView · SettingsView)
│ observes
MediaController polling, provider switching, position interpolation
│ uses
MediaProvider (protocol)
├── SpotifyProvider ┐
├── AppleMusicProvider ├── ScriptingBridge (Apple Events)
└── MockProvider ┘ in-memory, drives all UI work and tests
LockScreenPresenting (protocol)
├── SkyLightLockScreenPresenter ← the only files touching private APIs
└── UnsupportedLockScreenPresenter ← graceful degradation
The UI never knows which provider is active. Adding YouTube Music, Tidal or Amazon Music means
one new file in Sources/Providers/ and one entry in ProviderRegistry.standard().
Providers declare a ProviderCapabilities set, and the UI hides controls a service genuinely
lacks — so there are never dead buttons.
Three things were verified against the shipping apps and API rather than trusted from documentation, and all three contradicted it:
Spotify reports duration in milliseconds. Its scripting dictionary says seconds. A 3m21s
track returns 201946 while player position returns 201.94 seconds. SpotifyProvider
normalises this, and a test pins the behaviour.
Spotify's starred property is broken. It is present in the dictionary but reading it
fails with errAEEventFailed (-10000), even from osascript. So Spotify favourites are not
merely read-only over Apple Events — they are unavailable. Implementing "like" for Spotify
requires the Web API. That layer is built — OAuth PKCE with tokens in the Keychain — and is
entirely optional: the heart button only appears once you've signed in, so the UI never shows a
control that cannot work.
Apple Music is unaffected: its favorited property is writable and works.
Spotify's scripting interface has no repeat-one, though the app does. Its dictionary
declares repeating as a plain boolean, not the three-state enumeration Music.app exposes as
song repeat — and the limitation runs both ways: set repeat-one inside Spotify and Apple
Events still answers true, indistinguishable from repeat-all. The Web API does expose all
three (off / context / track), so repeat-one works once signed in and the control falls
back to off/all when signed out, gated by a .repeatOne capability.
Spotify's /me/tracks endpoints are deprecated and fail as a bare 403 Forbidden. With a
valid token, an allowlisted account and both user-library-read and user-library-modify
granted, GET /me/tracks/contains and PUT /me/tracks still return {"error": {"status": 403, "message": "Forbidden"}} — identical to a permissions failure, with nothing to indicate
deprecation. The replacements are the unified GET /me/library/contains and PUT/DELETE /me/library, which take whole spotify:track:… URIs rather than bare ids. Nocta uses those.
Diagnosing this from the 403 alone is impossible, which is why a failed request now logs a
/me probe: /me needs no scope, so a 200 there proves the token and account are fine and
narrows the fault to the endpoint.
Most people should skip this section. Everything Nocta does day to day — play/pause, skip,
seek, volume, shuffle, repeat, on both Spotify and Apple Music — works with no account, no
sign-in and no Premium subscription, because it goes over Apple Events to the app running on
your Mac. Apple Music's heart works out of the box too; favorited is native AppleScript.
The exceptions are liking a Spotify track and Spotify's repeat-one, both of which Spotify permits only through its Web API. That API cannot be reached on your behalf: an app in Spotify's Development Mode is capped at five authorised users, and Extended Quota Mode requires a registered business with 250,000 monthly active users — individual developers may not apply at all. So there is no version of Nocta that can ship a working Spotify heart to everyone. Each person who wants one has to register their own Spotify developer app, and since 9 March 2026 must have Spotify Premium for that app to function.
If that is you, here is the whole procedure. If it is not, the heart simply won't appear for Spotify and its repeat button cycles off/all instead of off/all/one — Nocta hides or narrows controls a service can't support rather than showing dead buttons.
To enable liking and repeat-one:
- Confirm the Spotify account you will use has Premium. Development Mode apps do not
function for free accounts, and every request fails as an unexplained
403. - Create an app at developer.spotify.com/dashboard.
- In the app's Settings, add
nocta://spotify-callbackas a redirect URI and tick Web API under Which API/SDKs are you planning to use?. - In the app's User Management, add the Spotify account you will sign in with — its full name and the email address on the account. A new app is in Development mode, and Spotify refuses Web API calls for any account not listed there.
- Paste the client ID into Settings › Music Service › Spotify account and click Connect.
Steps 0, 2 and 3 all fail the same silent way if missed: sign-in succeeds, and every request
afterwards is refused with a bare 403 Forbidden. Nocta shows that as an actionable message
rather than Spotify's empty one, and logs a /me probe that distinguishes an account problem
from an endpoint problem.
Nocta uses Authorization Code with PKCE, so there is no client secret to embed or leak —
you register your own app rather than sharing a credential baked into the binary. Access and
refresh tokens live in the Keychain (kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
not iCloud-synced) and are never written to UserDefaults or logged.
Off by default, and the only feature that contacts anyone other than your own music app. Turn it on in Settings › Lock Screen › Lyrics and both arrangements gain the line currently being sung: under the title in the collapsed bar, and under the album line just above the scrubber when expanded. Timing rides the position the scrubber already interpolates, so the line changes accurately between polls without any extra work.
One line, not a sheet, and that is a measured decision. The expanded column has roughly 440pt
to spend on a 14" display, and the artwork is the only part of it that can give ground — a
five-line scrolling sheet cost the cover 84 of its 245 points and read as a different, worse
card. A single line costs 22. LyricsLayoutTests pins both halves of that: the card's
footprint never changes when lyrics are enabled, and the artwork gives up exactly the line's
height and no more.
Lyrics come from LRCLIB, a free, key-less, community-contributed database of LRC files. A request carries the track's title, artist, album and length and nothing else. Tracks with no entry simply show no lyric line, and the card keeps its usual proportions.
Two things to know, recorded here rather than buried:
- Coverage is good but not complete. LRCLIB holds roughly three million entries; obscure and very new releases are the gaps.
- The lyrics themselves are copyrighted works, and LRCLIB does not license them. It is a community database whose stated purpose is free software players. Displaying its content in paid software is a risk borne knowingly; the licensed alternatives are Musixmatch and LyricFind, both of which require a commercial agreement. This is why the feature ships off by default and names its source in Settings rather than presenting lyrics as Nocta's own.
- Global shortcuts do not fire while the Mac is locked. macOS reserves keyboard input for the login window. Use the on-screen Lock Screen controls instead.
- This is the Lock Screen, not the Login Window. After a restart or full log-out, Nocta isn't running, so nothing appears until you log in once.
- Not sandboxed, so it cannot ship on the Mac App Store. Apple Events to arbitrary apps and the Lock Screen technique both require it.
- The Lock Screen feature depends on private APIs and may break in a future macOS. Nocta detects this at launch and reports it in Settings rather than crashing — but since the Lock Screen card is the only player, there is nothing left to fall back to. This is a deliberate trade: the product is the Lock Screen controller, not a music player that also does one.
Nocta never touches authentication. The Lock Screen panel is a non-activating NSPanel with
canBecomeKey and canBecomeMain overridden to false, so it can receive mouse events but
keyboard events can never reach it — password capture is impossible by construction, not by
policy. Placement is clamped away from the clock, avatar, password field and Touch ID prompt.
Full detail, including the API-tier classification and verification logs, is in docs/LOCK_SCREEN.md.
xcodebuild -project Nocta.xcodeproj -scheme Nocta test68 tests across 15 suites, covering the Lock Screen exclusion-zone maths (the safety property, asserted across four screen geometries), Spotify's duration quirk, position interpolation, capability gating, lyrics layout, and the mock provider's playback semantics.
Contributions are welcome — issues, bug reports and pull requests all help. See CONTRIBUTING.md for how to build the project, what the architecture expects of a change, and the areas that most need help.
The highest-value contribution is a new provider: adding YouTube Music, Tidal or Amazon
Music is one new file in Sources/Providers/ and one entry in ProviderRegistry.standard().
The second is testing on other macOS versions and hardware — the Lock Screen technique is
verified on exactly one machine, and reports from Intel Macs, external displays and other
macOS builds are genuinely useful.
Nocta is released under the MIT Licence — see LICENSE.
The third-party components Nocta bundles are also MIT-licensed and are licensed to you under their own terms; those terms, and a note on which projects were deliberately not copied from for licensing reasons, are in NOTICE.