Skip to content

Repository files navigation

Nocta

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.


Status

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

Requirements

  • 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.

Build

brew install xcodegen
./Scripts/build.sh

Produces dist/Nocta.app. To make a disk image:

./Scripts/package-dmg.sh

Gatekeeper

Nocta 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.sh

Permissions

Nocta 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.

Architecture

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.

Notable findings

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.

Spotify sign-in (advanced, optional)

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:

  1. 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.
  2. Create an app at developer.spotify.com/dashboard.
  3. In the app's Settings, add nocta://spotify-callback as a redirect URI and tick Web API under Which API/SDKs are you planning to use?.
  4. 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.
  5. 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.

Lyrics (opt-in)

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.

Limitations

  • 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.

Safety

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.

Tests

xcodebuild -project Nocta.xcodeproj -scheme Nocta test

68 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.

Contributing

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.

Licence

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.

About

An interactive media controller for the macOS Lock Screen — control Spotify and Apple Music without unlocking your Mac

Topics

Resources

Contributing

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages