Add Spotify integration with toggleable music source
- Add OAuth 2.0 PKCE authentication for Spotify Web API - Create SpotifyNowPlayingMonitor for polling current track - Add Settings tab with music source toggle (Apple Music/Spotify) - Store tokens securely in Keychain - Display current track on Glass as NOW_PLAYING card 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,15 @@ final class ContextOrchestrator: NSObject, ObservableObject {
|
||||
@Published private(set) var lastFetchFailed: Bool = false
|
||||
@Published private(set) var musicAuthorization: MusicAuthorization.Status = .notDetermined
|
||||
@Published private(set) var nowPlaying: NowPlayingSnapshot? = nil
|
||||
@Published private(set) var spotifyNowPlaying: SpotifyNowPlaying? = nil
|
||||
@Published var musicSource: MusicSource = .appleMusic {
|
||||
didSet {
|
||||
if oldValue != musicSource {
|
||||
switchMusicMonitor()
|
||||
UserDefaults.standard.set(musicSource.rawValue, forKey: "music_source")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "iris", category: "ContextOrchestrator")
|
||||
|
||||
@@ -38,6 +47,8 @@ final class ContextOrchestrator: NSObject, ObservableObject {
|
||||
private let server: LocalServer
|
||||
private let ble: BlePeripheralManager
|
||||
private let nowPlayingMonitor = NowPlayingMonitor()
|
||||
private let spotifyMonitor: SpotifyNowPlayingMonitor
|
||||
private let spotifyAuth: SpotifyAuthManager
|
||||
|
||||
private var lastRecomputeLocation: CLLocation? = nil
|
||||
private var lastRecomputeAccuracy: CLLocationAccuracy? = nil
|
||||
@@ -46,11 +57,20 @@ final class ContextOrchestrator: NSObject, ObservableObject {
|
||||
|
||||
init(store: FeedStore = FeedStore(),
|
||||
server: LocalServer = LocalServer(),
|
||||
ble: BlePeripheralManager) {
|
||||
ble: BlePeripheralManager,
|
||||
spotifyAuth: SpotifyAuthManager) {
|
||||
self.store = store
|
||||
self.server = server
|
||||
self.ble = ble
|
||||
self.spotifyAuth = spotifyAuth
|
||||
self.spotifyMonitor = SpotifyNowPlayingMonitor(authManager: spotifyAuth)
|
||||
self.ranker = HeuristicRanker(lastShownAt: { id in store.lastShownAt(feedItemId: id) })
|
||||
|
||||
if let savedSource = UserDefaults.standard.string(forKey: "music_source"),
|
||||
let source = MusicSource(rawValue: savedSource) {
|
||||
self.musicSource = source
|
||||
}
|
||||
|
||||
super.init()
|
||||
|
||||
locationManager.delegate = self
|
||||
@@ -73,13 +93,21 @@ final class ContextOrchestrator: NSObject, ObservableObject {
|
||||
|
||||
nowPlayingMonitor.onUpdate = { [weak self] update in
|
||||
Task { @MainActor in
|
||||
guard let self else { return }
|
||||
guard let self, self.musicSource == .appleMusic else { return }
|
||||
self.musicAuthorization = update.authorization
|
||||
self.nowPlaying = update.snapshot
|
||||
self.pushLatestFeedToBle()
|
||||
}
|
||||
}
|
||||
|
||||
spotifyMonitor.onUpdate = { [weak self] update in
|
||||
Task { @MainActor in
|
||||
guard let self, self.musicSource == .spotify else { return }
|
||||
self.spotifyNowPlaying = update.snapshot
|
||||
self.pushLatestFeedToBle()
|
||||
}
|
||||
}
|
||||
|
||||
let feed = store.getFeed()
|
||||
lastFeed = feed
|
||||
}
|
||||
@@ -88,7 +116,7 @@ final class ContextOrchestrator: NSObject, ObservableObject {
|
||||
authorization = locationManager.authorizationStatus
|
||||
logger.info("start auth=\(String(describing: self.authorization), privacy: .public)")
|
||||
server.start()
|
||||
nowPlayingMonitor.start()
|
||||
startMusicMonitor()
|
||||
requestPermissionsIfNeeded()
|
||||
locationManager.startUpdatingLocation()
|
||||
}
|
||||
@@ -96,6 +124,27 @@ final class ContextOrchestrator: NSObject, ObservableObject {
|
||||
func stop() {
|
||||
locationManager.stopUpdatingLocation()
|
||||
nowPlayingMonitor.stop()
|
||||
spotifyMonitor.stop()
|
||||
}
|
||||
|
||||
private func startMusicMonitor() {
|
||||
switch musicSource {
|
||||
case .appleMusic:
|
||||
nowPlayingMonitor.start()
|
||||
case .spotify:
|
||||
spotifyMonitor.start()
|
||||
}
|
||||
}
|
||||
|
||||
private func switchMusicMonitor() {
|
||||
nowPlayingMonitor.stop()
|
||||
spotifyMonitor.stop()
|
||||
nowPlaying = nil
|
||||
spotifyNowPlaying = nil
|
||||
|
||||
startMusicMonitor()
|
||||
pushLatestFeedToBle()
|
||||
logger.info("Switched music source to \(self.musicSource.rawValue)")
|
||||
}
|
||||
|
||||
func recomputeNow(reason: String = "manual") {
|
||||
@@ -500,7 +549,15 @@ final class ContextOrchestrator: NSObject, ObservableObject {
|
||||
}
|
||||
|
||||
private func feedForGlass(base: FeedEnvelope, now: Int) -> FeedEnvelope {
|
||||
guard let nowPlayingCard = nowPlaying?.asFeedItem(baseGeneratedAt: base.generatedAt, now: now) else {
|
||||
let nowPlayingCard: FeedItem?
|
||||
switch musicSource {
|
||||
case .appleMusic:
|
||||
nowPlayingCard = nowPlaying?.asFeedItem(baseGeneratedAt: base.generatedAt, now: now)
|
||||
case .spotify:
|
||||
nowPlayingCard = spotifyNowPlaying?.asFeedItem(baseGeneratedAt: base.generatedAt, now: now)
|
||||
}
|
||||
|
||||
guard let nowPlayingCard else {
|
||||
return base
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user