Files
Ascently/ios/Ascently/Services/MusicService.swift

198 lines
6.6 KiB
Swift

import AVFoundation
import Combine
import MusicKit
import SwiftUI
@MainActor
class MusicService: ObservableObject {
static let shared = MusicService()
@Published var isAuthorized = false
@Published var playlists: MusicItemCollection<Playlist> = []
@Published var selectedPlaylistId: String? {
didSet {
UserDefaults.standard.set(selectedPlaylistId, forKey: "ascently_selected_playlist_id")
}
}
@Published var isMusicEnabled: Bool {
didSet {
UserDefaults.standard.set(isMusicEnabled, forKey: "ascently_music_enabled")
if !isMusicEnabled {
// Genuinely unsure what I want to do with this but we should account for it at some point
}
}
}
@Published var isAutoPlayEnabled: Bool {
didSet {
UserDefaults.standard.set(isAutoPlayEnabled, forKey: "ascently_music_autoplay_enabled")
}
}
@Published var isAutoStopEnabled: Bool {
didSet {
UserDefaults.standard.set(isAutoStopEnabled, forKey: "ascently_music_autostop_enabled")
}
}
@Published var isPlaying = false
private var cancellables = Set<AnyCancellable>()
private var hasStartedSessionPlayback = false
private var currentPlaylistTrackIds: Set<MusicItemID> = []
private init() {
self.selectedPlaylistId = UserDefaults.standard.string(forKey: "ascently_selected_playlist_id")
self.isMusicEnabled = UserDefaults.standard.bool(forKey: "ascently_music_enabled")
self.isAutoPlayEnabled = UserDefaults.standard.bool(forKey: "ascently_music_autoplay_enabled")
self.isAutoStopEnabled = UserDefaults.standard.bool(forKey: "ascently_music_autostop_enabled")
if isMusicEnabled {
Task {
await checkAuthorizationStatus()
}
}
setupObservers()
}
private func setupObservers() {
SystemMusicPlayer.shared.state.objectWillChange
.sink { [weak self] _ in
self?.updatePlaybackStatus()
}
.store(in: &cancellables)
SystemMusicPlayer.shared.queue.objectWillChange
.sink { [weak self] _ in
self?.checkQueueConsistency()
}
.store(in: &cancellables)
}
private func updatePlaybackStatus() {
Task { @MainActor [weak self] in
self?.isPlaying = SystemMusicPlayer.shared.state.playbackStatus == .playing
}
}
private func checkQueueConsistency() {
guard hasStartedSessionPlayback else { return }
Task { @MainActor [weak self] in
guard let self = self else { return }
if let currentEntry = SystemMusicPlayer.shared.queue.currentEntry,
let item = currentEntry.item {
if !self.currentPlaylistTrackIds.isEmpty && !self.currentPlaylistTrackIds.contains(item.id) {
self.hasStartedSessionPlayback = false
}
}
}
}
func toggleMusicEnabled(_ enabled: Bool) {
isMusicEnabled = enabled
if enabled {
Task {
await checkAuthorizationStatus()
}
}
}
func checkAuthorizationStatus() async {
let status = await MusicAuthorization.request()
self.isAuthorized = status == .authorized
if isAuthorized {
await fetchPlaylists()
}
}
func fetchPlaylists() async {
guard isAuthorized else { return }
do {
var request = MusicLibraryRequest<Playlist>()
request.sort(by: \.name, ascending: true)
let response = try await request.response()
self.playlists = response.items
} catch {
print("Error fetching playlists: \(error)")
}
}
func playSelectedPlaylistIfHeadphonesConnected() {
guard isMusicEnabled, isAutoPlayEnabled, let playlistId = selectedPlaylistId else { return }
if isHeadphonesConnected() {
playPlaylist(id: playlistId)
}
}
func resetSessionPlaybackState() {
hasStartedSessionPlayback = false
currentPlaylistTrackIds.removeAll()
}
func playPlaylist(id: String) {
print("Attempting to play playlist \(id)")
Task {
do {
if playlists.isEmpty {
await fetchPlaylists()
}
var targetPlaylist: Playlist?
if let playlist = playlists.first(where: { $0.id.rawValue == id }) {
targetPlaylist = playlist
} else {
var request = MusicLibraryRequest<Playlist>()
request.filter(matching: \.id, equalTo: MusicItemID(id))
let response = try await request.response()
targetPlaylist = response.items.first
}
if let playlist = targetPlaylist {
let detailedPlaylist = try await playlist.with([.tracks])
if let tracks = detailedPlaylist.tracks {
self.currentPlaylistTrackIds = Set(tracks.map { $0.id })
}
SystemMusicPlayer.shared.queue = [playlist]
try await SystemMusicPlayer.shared.play()
hasStartedSessionPlayback = true
}
} catch {
print("Error playing playlist: \(error)")
}
}
}
func stopPlaybackIfEnabled() {
guard isMusicEnabled, isAutoStopEnabled else { return }
SystemMusicPlayer.shared.stop()
}
func togglePlayback() {
Task {
if isPlaying {
SystemMusicPlayer.shared.pause()
} else {
if let playlistId = selectedPlaylistId, !hasStartedSessionPlayback {
playPlaylist(id: playlistId)
} else {
try? await SystemMusicPlayer.shared.play()
}
}
}
}
private func isHeadphonesConnected() -> Bool {
let route = AVAudioSession.sharedInstance().currentRoute
return route.outputs.contains { port in
port.portType == .headphones ||
port.portType == .bluetoothA2DP ||
port.portType == .bluetoothLE ||
port.portType == .bluetoothHFP ||
port.portType == .usbAudio ||
port.portType == .airPlay
}
}
}