Skip to content

Latest commit

 

History

History
260 lines (197 loc) · 8.86 KB

File metadata and controls

260 lines (197 loc) · 8.86 KB

Contributing to Kaset

Thank you for your interest in contributing! This document provides guidelines and information for developers.

Getting Started

Requirements

  • macOS 26.0 or later
  • Xcode 16.0 or later
  • Swift 6.0

Build & Run

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

Project Structure

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

Key Files

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

Architecture

For detailed architecture documentation, see the docs/ folder:

High-Level Overview

The app uses a clean architecture with:

  • Observable Pattern: @Observable classes for reactive state management
  • MainActor Isolation: All UI and service classes are @MainActor for thread safety
  • WebKit Integration: Persistent WKWebsiteDataStore for cookie management
  • Swift Concurrency: async/await throughout, no DispatchQueue

Playback Architecture

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

Authentication Flow

App Launch → Check cookies → __Secure-3PAPISID exists?
    │                              │
    │ No                           │ Yes
    ▼                              ▼
Show LoginSheet              AuthService.loggedIn
    │
    │ User signs in
    ▼
Observer detects cookie → Dismiss sheet

Background Audio

Close window (⌘W) → Window hides → Audio continues
Click dock icon    → Window shows → Same WebView
Quit app (⌘Q)     → App terminates → Audio stops

Coding Guidelines

Modern SwiftUI APIs

❌ 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()

Swift Concurrency

  • Mark @Observable classes with @MainActor
  • Never use DispatchQueue — use async/await, MainActor
  • For @MainActor test classes, don't call super.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.

WebKit Patterns

  • Use WebKitManager's shared WKWebsiteDataStore for cookie persistence
  • Use SingletonPlayerWebView.shared for playback (never create multiple WebViews)
  • Compute SAPISIDHASH fresh per request using current cookies

Keyboard Shortcuts

  • 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.md whenever shortcut behavior changes

Error Handling

  • Throw YTMusicError.authExpired on HTTP 401/403
  • Use DiagnosticsLogger for all logging (not print())
  • Show user-friendly error messages with retry options

Pull Request Guidelines

  1. No Third-Party Frameworks — Do not introduce third-party dependencies without discussion first
  2. Build Must Pass — Run swift build
  3. Tests Must Pass — Run swift test
  4. Linting — Run swiftlint --strict && swiftformat . before submitting
  5. Small PRs — Keep changes focused and reviewable
  6. Share AI Prompts — If you used AI assistance, include the prompt in your PR (see below)

AI-Assisted Contributions & Prompt Requests

We embrace AI-assisted development! Whether you use GitHub Copilot, Claude, Cursor, or other AI tools, we welcome contributions that leverage these capabilities.

What is a Prompt Request?

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

Contributing with AI Assistance

Option 1: Traditional PR with AI Prompt Disclosure

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.

Option 2: Prompt Request (Prompt-Only)

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

Best Practices for AI Prompts

  1. Be specific — Include file paths, function names, and concrete requirements
  2. Reference project conventions — Mention AGENTS.md and relevant patterns
  3. Define acceptance criteria — How will we know it worked?
  4. Include context — Link to issues, docs, or examples
  5. Test locally when possible — Verify the prompt produces working code

Example Prompt

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

Testing

# Run all tests
swift test

# Run specific test (use --filter)
swift test --filter PlayerServiceTests

See docs/testing.md for detailed testing patterns and debugging tips.