198 lines
6.6 KiB
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
|
|
}
|
|
}
|
|
}
|