Thank you for your interest in contributing! This document provides guidelines and information for developers.
- macOS 26.0 or later
- Xcode 16.0 or later
- Swift 6.0
# Clone the repository
git clone https://github.com/sozercan/kaset.git
cd kaset
# Build from command line
swift build
# Run tests
swift test
# Package and run the app
Scripts/compile_and_run.sh
# Lint & Format
swiftlint --strict && swiftformat .Or open Package.swift in Xcode to work in the IDE.
Package.swift → SPM manifest (build configuration)
Sources/
└── Kaset/ → Main app target
├── Models/ → Data models (Song, Playlist, Album, Artist, etc.)
├── Services/
│ ├── API/ → YTMusicClient (YouTube Music API calls)
│ ├── Auth/ → AuthService (login state machine)
│ ├── Player/ → PlayerService, NowPlayingManager (playback, media keys)
│ └── WebKit/ → WebKitManager (cookie store, persistent login)
├── ViewModels/ → HomeViewModel, LibraryViewModel, SearchViewModel
├── Utilities/ → DiagnosticsLogger, extensions
└── Views/ → SwiftUI views (MainWindow, Sidebar, PlayerBar, etc.)
└── APIExplorer/ → API explorer CLI tool
Tests/ → Unit tests (KasetTests/)
Scripts/ → Build scripts
docs/ → Detailed documentation
| File | Purpose |
|---|---|
Sources/Kaset/AppDelegate.swift |
Window lifecycle, background audio support |
Sources/Kaset/Services/WebKit/WebKitManager.swift |
Cookie store & persistence |
Sources/Kaset/Services/Auth/AuthService.swift |
Login state machine |
Sources/Kaset/Services/Player/PlayerService.swift |
Playback state & control |
Sources/Kaset/Views/MiniPlayerWebView.swift |
Singleton WebView, playback UI |
Sources/Kaset/Views/MainWindow.swift |
Main app window |
Sources/Kaset/Utilities/DiagnosticsLogger.swift |
Logging |
For detailed architecture documentation, see the docs/ folder:
- docs/architecture.md — Services, state management, data flow
- docs/playback.md — WebView playback system, background audio
- docs/testing.md — Test commands, patterns, debugging
The app uses a clean architecture with:
- Observable Pattern:
@Observableclasses for reactive state management - MainActor Isolation: All UI and service classes are
@MainActorfor thread safety - WebKit Integration: Persistent
WKWebsiteDataStorefor cookie management - Swift Concurrency:
async/awaitthroughout, noDispatchQueue
User clicks Play
│
▼
PlayerService.play(videoId:)
│
├── Sets pendingPlayVideoId
└── Shows mini player toast (160×90)
│
▼
SingletonPlayerWebView.shared
│
├── One WebView for entire app
├── Loads music.youtube.com/watch?v={id}
└── JS bridge sends state updates
│
▼
PlayerService updates:
- isPlaying
- progress
- duration
App Launch → Check cookies → __Secure-3PAPISID exists?
│ │
│ No │ Yes
▼ ▼
Show LoginSheet AuthService.loggedIn
│
│ User signs in
▼
Observer detects cookie → Dismiss sheet
Close window (⌘W) → Window hides → Audio continues
Click dock icon → Window shows → Same WebView
Quit app (⌘Q) → App terminates → Audio stops
| ❌ Avoid | ✅ Use |
|---|---|
.foregroundColor() |
.foregroundStyle() |
.cornerRadius() |
.clipShape(.rect(cornerRadius:)) |
onChange(of:) { newValue in } |
onChange(of:) { _, newValue in } |
Task.sleep(nanoseconds:) |
Task.sleep(for: .seconds()) |
NavigationView |
NavigationSplitView or NavigationStack |
onTapGesture() |
Button (unless tap location needed) |
tabItem() |
Tab API |
AnyView |
Concrete types or @ViewBuilder |
print() |
DiagnosticsLogger |
DispatchQueue |
Swift concurrency (async/await) |
String(format: "%.2f", n) |
Text(n, format: .number.precision(...)) |
Force unwraps (!) |
Optional handling or guard |
| Image-only buttons without labels | Add .accessibilityLabel() |
- Mark
@Observableclasses with@MainActor - Never use
DispatchQueue— useasync/await,MainActor - For
@MainActortest classes, don't callsuper.setUp()in async context:
@MainActor
final class MyServiceTests: XCTestCase {
override func setUp() async throws {
// Do NOT call: try await super.setUp()
// Set up test fixtures here
}
override func tearDown() async throws {
// Clean up here
// Do NOT call: try await super.tearDown()
}
}Why? XCTestCase is not Sendable. Calling super.setUp() from a @MainActor async context sends self across actor boundaries, causing Swift 6 strict concurrency errors.
- Use
WebKitManager's sharedWKWebsiteDataStorefor cookie persistence - Use
SingletonPlayerWebView.sharedfor playback (never create multiple WebViews) - Compute
SAPISIDHASHfresh per request using current cookies
- Preserve standard macOS app/window shortcuts such as
⌘M,⌘W,⌘Q,⌘H, and⌘,unless there is explicit product direction to change them - Prefer native macOS and Apple Music shortcut conventions for playback actions
- Update
docs/keyboard-shortcuts.mdwhenever shortcut behavior changes
- Throw
YTMusicError.authExpiredon HTTP 401/403 - Use
DiagnosticsLoggerfor all logging (notprint()) - Show user-friendly error messages with retry options
- No Third-Party Frameworks — Do not introduce third-party dependencies without discussion first
- Build Must Pass — Run
swift build - Tests Must Pass — Run
swift test - Linting — Run
swiftlint --strict && swiftformat .before submitting - Small PRs — Keep changes focused and reviewable
- Share AI Prompts — If you used AI assistance, include the prompt in your PR (see below)
We embrace AI-assisted development! Whether you use GitHub Copilot, Claude, Cursor, or other AI tools, we welcome contributions that leverage these capabilities.
A prompt request is a contribution where you share the AI prompt that generates code, rather than (or in addition to) the code itself. This approach:
- Captures intent — The prompt often explains why better than a code diff
- Enables review before implementation — Maintainers can validate the approach
- Supports iteration — Prompts can be refined before code is generated
- Improves reproducibility — Anyone can run the prompt to verify results
Submit code as usual, but include the AI prompt in the PR template's "AI Prompt" section. This helps reviewers understand your approach and intent.
Create an issue using the Prompt Request template if you:
- Have a well-crafted prompt but haven't run it yet
- Want feedback on your approach before implementation
- Prefer maintainers to run and merge the prompt themselves
- Be specific — Include file paths, function names, and concrete requirements
- Reference project conventions — Mention AGENTS.md and relevant patterns
- Define acceptance criteria — How will we know it worked?
- Include context — Link to issues, docs, or examples
- Test locally when possible — Verify the prompt produces working code
Add haptic feedback to the shuffle button in PlayerBar.swift.
Requirements:
- Use HapticService.toggle() on button tap
- Only trigger haptic on state change (not when already shuffled)
- Follow existing haptic patterns used in volume controls
- Add unit test in PlayerServiceTests.swift
Reference: Sources/Kaset/Services/HapticService.swift for existing patterns
# Run all tests
swift test
# Run specific test (use --filter)
swift test --filter PlayerServiceTestsSee docs/testing.md for detailed testing patterns and debugging tips.