Skip to content
WolfWave

Contribute

Set up Xcode 16, run the test suite, regenerate design tokens, and ship a PR to WolfWave. The open-source Apple Music streamer app.

Contribute

This guide covers everything you need to contribute to WolfWave.

Prerequisites

  • macOS 26.0+ (Tahoe)
  • Apple Silicon (M1 or later)
  • Xcode 16.0+
  • Swift 5.9+
  • Command Line Tools: xcode-select --install

Setup

  1. Fork and clone the repository
  2. Copy apps/native/WolfWave/Config.xcconfig.example to apps/native/WolfWave/Config.xcconfig
  3. Set your Twitch Client ID and Discord Client ID in Config.xcconfig
  4. Open the project: make open-xcode
  5. Resolve dependencies: make update-deps

Branding overrides

DOCS_URL, COMMUNITY_DISCORD_URL, and COPYRIGHT_HOLDER control the docs link, community Discord invite, and legal entity shown in About + Monthly Wrap. You can change them at three levels, in priority order:

  1. Per-machine / fork: set them in Config.xcconfig. Highest priority. Gitignored, so this only affects your local builds.
  2. Project-wide default: edit the fallback strings in Core/AppConstants.swift (AppInfo.copyrightHolder, URLs.docs, URLs.communityDiscord). This is the source of truth shipped to every user. Change it here if you want to move the upstream default.
  3. CI: .github/workflows/build_release.yml and test.yml write these keys into the generated Config.xcconfig so the built Info.plist carries literal values instead of empty placeholders. Keep them in sync with the AppConstants defaults if you change the upstream values.

Each key falls back to the AppConstants default when blank or unset, so leaving them out never breaks a build. To rename the app itself, edit CFBundleDisplayName in Info.plist (or set PRODUCT_NAME in an xcconfig overlay).

Development Commands

CommandDescription
make buildDebug build
make cleanClean build artifacts
make testRun unit tests (XCTest + Swift Testing)
make update-depsResolve SwiftPM dependencies
make open-xcodeOpen the Xcode project
make ciCI-friendly build
make prod-buildRelease build + DMG in builds/
make prod-installRelease build + install to /Applications
make notarizeNotarize the DMG (requires Developer ID)
make verify-notarizeVerify notarization of DMG

Code Quality

This project follows Swift best practices with professional-grade documentation:

  • Swift 5.9+ with modern concurrency (async/await)
  • SwiftUI for user interfaces
  • @Observable macro for ViewModels (migrated from ObservableObject / @Published)
  • Comprehensive Documentation: DocC-style comments throughout with usage examples
  • MARK Sections: All files organized with clear section markers for easy navigation
  • Separation of Concerns: Clean architecture across Core / Services / Views / Monitors
  • Secure Credential Storage: macOS Keychain for all sensitive data
  • ScriptingBridge Integration: Direct Apple Music communication without spawning subprocesses
  • Robust Error Handling: Typed errors with localized descriptions
  • Type Safety: Strongly typed models for all data structures
  • Minimal Dependencies: Native Apple frameworks plus Sparkle for auto-updates

Playback Source

Playback is abstracted behind the PlaybackSource protocol so additional sources can plug in later.

  • AppleMusicSource. ScriptingBridge + distributed notifications, with a 2-second fallback poll.
  • PlaybackSourceManager. Selects + multiplexes sources, exposes PlaybackSourceDelegate for track updates.

Twitch Chat Service

The bot is implemented with TwitchChatService using Twitch Helix + EventSub (no IRC).

Features

  • EventSub WebSocket: Real-time chat message notifications.
  • OAuth Device Code Flow: Secure authentication via TwitchDeviceAuth.
  • Token Validation: Automatic token validation on app launch.
  • Network Path Monitoring: Reconnects automatically when the network comes back.
  • Command System: Extensible bot command architecture via BotCommandDispatcher.

Usage

  • Connect with saved credentials: joinChannel(broadcasterID:botID:token:clientID:) or connectToChannel(channelName:token:clientID:)
  • Send chat messages via Helix: sendMessage(_:) or sendMessage(_:replyTo:)
  • Supply current track info for commands: set getCurrentSongInfo on the service
  • The service respects commandsEnabled so you can disable all commands from Settings

Bot Commands

BotCommandDispatcher routes incoming chat messages to registered commands. Concrete commands live in Services/Twitch/Commands/:

  • TrackInfoCommand. Handles both !song / !currentsong / !nowplaying and !last / !lastsong / !prevsong via shared fixtures.
  • SongRequestCommand , !songrequest / !sr.
  • QueueCommand / MyQueueCommand. Read-only queue inspection.
  • SkipCommand / HoldCommand / ClearQueueCommand. Mod-only controls.
  • CooldownManager enforces global + per-user cooldowns; mods bypass automatically.

Song Request Service

SongRequestService orchestrates request flow end-to-end.

  • SongRequestQueue. Queue with hold mode + Music.app-closed buffering.
  • SongSearchResolver / LinkResolverService. Resolve queries via MusicKit and Apple Music URLs.
  • AppleMusicController. Plays tracks via AppleScript while preserving window focus.
  • SongBlocklist. Block tracks, artists, or albums by ID.

Discord Rich Presence Service

DiscordRPCService communicates with Discord via the local IPC Unix domain socket.

Features

  • IPC Socket Client: Connects to Discord's local Unix domain socket (discord-ipc-{0..9}).
  • Dynamic Artwork: Fetches album art from the iTunes Search API with caching.
  • Auto-reconnect: Automatic reconnection with backoff when Discord restarts.
  • Sandbox Compatible: Works within the macOS App Sandbox via SBPL entitlements.
  • Thread-safe: Serial ipcQueue confinement plus enabledLock.

Usage

  • Start the service: connect(). Discovers and connects to Discord's IPC socket.
  • Update presence: updatePresence(track:artist:album:duration:elapsed:isPaused:). Sets the listening activity. Pass isPaused: true when Music.app reports kPSp so the live ticker is suppressed and the pause badge swaps in.
  • Clear presence: clearPresence(). Removes the activity from the user's profile.

Paused-state handling

Discord has no native "paused" activity flag. WolfWave works around it with two changes inside buildActivity when isPaused == true:

  1. The timestamps block is omitted. Without start/end the Discord client stops the progress ticker instead of marching past the real elapsed value.
  2. assets.small_image is swapped from apple_music to pause and assets.small_text becomes "Paused".

The track text, large image, and buttons stay unchanged so chat can still read what's loaded.

Rich Presence art assets (required upload)

small_image / large_image reference asset names registered against your Discord application. The PNGs live in discord-assets/ and are not bundled with the app. You upload them once via the Discord developer portal.

Asset nameUsed when
apple_musicDefault large_image fallback + small_image "source" badge while playing
pausesmall_image badge that replaces apple_music while paused

Fork maintainers: if you ship WolfWave with your own DISCORD_CLIENT_ID, you must upload both assets to your Discord application's Rich Presence → Art Assets page before Rich Presence will render correctly. See discord-assets/README.md for the upload steps and asset specs.

WebSocket Server Service

WebSocketServerService runs a local WebSocket server using Network.framework to broadcast now-playing data to stream overlay clients.

Features

  • Network.framework NWListener: Native WebSocket server with auto-ping support.
  • Multi-client Support: Multiple browser sources or tools can connect simultaneously.
  • Progress Broadcasting: 1-second interval timer broadcasts elapsed time during playback.
  • Auto-retry: Reconnects the listener after failures with configurable delay.
  • Loopback-only: Binds to 127.0.0.1. Never reachable from the network.

Usage

  • Enable/disable: setEnabled(_:). Starts or stops the NWListener.
  • Update port: updatePort(_:). Restarts the server on a new port if running (default 8765).
  • Track change: updateNowPlaying(track:artist:album:duration:elapsed:artworkURL:). Broadcasts now_playing JSON.
  • Clear: clearNowPlaying(). Broadcasts playback_state with isPlaying: false.

Widget HTTP Service

WidgetHTTPService is a tiny companion HTTP server that serves the bundled widget.html to OBS.

  • Loopback-only: Binds to 127.0.0.1:8766. Never exposed to the network.
  • GET /200 OK with widget.html bytes from the app bundle.
  • All other requests404 Not Found.
  • Lifecycle: Owned by WebSocketServerService; starts/stops with the WS server.

Building the OBS Widget

The bundled widget.html is not hand-written. It's generated from the apps/widget/ Tailwind + TypeScript workspace. Source files:

  • apps/widget/src/widget.html. HTML shell with placeholders
  • apps/widget/src/widget.css. Tailwind directives + custom transition classes
  • apps/widget/src/widget.ts. Runtime (state, transitions, WS, render)
  • apps/widget/build.ts. Bundles JS, runs Tailwind, inlines everything

The build emits a single self-contained file at apps/native/WolfWave/Resources/widget.html (committed to the repo so Xcode-only builds keep working).

When to rebuild

Whenever you touch any of these:

  • Anything under apps/widget/src/**
  • design-system/tokens.json (themes / layouts / colors)
  • apps/widget/tailwind.config.ts or build.ts

How rebuilds happen

TriggerMechanism
Cmd+B in XcodePre-build Run Script phase Build OBS Widget (Tailwind → inline) runs bun run --filter widget build automatically
CI (test + release)Bun setup + widget build runs before every xcodebuild invocation
Manualbun run --filter widget build from the repo root

The Xcode script is tolerant of missing bun. It exits 0 with a warning so a fresh clone without the JS toolchain still builds (just with whatever widget.html is committed).

For the full architecture, message contract, theme system, and transition state machine, see the OBS Widget Architecture page.

Sparkle Updater Service

SparkleUpdaterService wraps the Sparkle framework for auto-updates.

  • EdDSA-signed appcast: Every update is verified against the public key in Info.plist.
  • DEBUG builds: Automatic checks disabled; manual "Check Now" reads the bundled dev-appcast.xml.
  • Release builds: Polls the remote SUFeedURL.
  • Homebrew installs: Fully disabled. Updates handled by Homebrew.

Testing

WolfWave includes a comprehensive unit test suite using XCTest + Swift Testing. Run all tests with:

make test

Or press Cmd+U in Xcode. The test target (WolfWaveTests) is a hosted unit test bundle that runs inside the app process.

Test Coverage

Tests live in apps/native/WolfWaveTests/ and cover the playback parser, command dispatcher, Twitch + Discord services, WebSocket auth handshake, Keychain wrapper, onboarding state machine, listening history pipeline, and the full Sparkle updater.

For the live pass count and per-file coverage, run make test locally or browse the directory on GitHub. Enumerating files here just rots on every PR.

Adding New Tests

Test files are auto-discovered via PBXFileSystemSynchronizedRootGroup. Just add .swift files to apps/native/WolfWaveTests/. Use @testable import WolfWave to access internal types.

CI/CD

The project includes these GitHub Actions workflows:

  • Test (.github/workflows/test.yml): Runs xcodebuild test on every push and pull request to main. Automatically creates a placeholder Config.xcconfig for builds.
  • Build & Release (.github/workflows/build_release.yml): On tag push (v*), builds a signed and notarized DMG in CI, then creates a draft GitHub Release with the DMG attached. The developer reviews the draft and publishes it.
  • Update Homebrew (.github/workflows/update_homebrew.yml): After a published Release, auto-bumps the Homebrew cask.
  • Docs (.github/workflows/docs.yml): Deploys the Fumadocs site to GitHub Pages.

Required GitHub Secrets

SecretDescription
TWITCH_CLIENT_IDTwitch application Client ID
DISCORD_CLIENT_IDDiscord application ID
DEVELOPER_ID_CERT_P12Base64-encoded Developer ID certificate
DEVELOPER_ID_CERT_PASSWORDPassword for the P12 certificate
APPLE_IDApple ID for notarization
APPLE_TEAM_IDApple Developer Team ID
APPLE_APP_PASSWORDApp-specific password for notarization
SPARKLE_PRIVATE_KEYEdDSA private key for appcast signing

Code signing, notarization, and DMG creation all happen in the release workflow. GITHUB_TOKEN is provided automatically by GitHub Actions.

Triggering a Release

git tag v2.0.0
git push origin v2.0.0

This triggers the release workflow, which builds, signs, notarizes, and uploads the DMG to a draft GitHub Release. Once the workflow completes, review the draft and publish it.

For a complete step-by-step release checklist, see PUBLISH.md in the repository root.


Support development

WolfWave is built and maintained by one person. If the project saves you time or earns you viewers, please consider sponsoring. It directly funds development.

Sponsor on GitHub →

On this page