// // MALibraryManager.swift // Mobile Music Assistant // // Created by Sven Hanold on 26.03.26. // import Foundation import OSLog private let logger = Logger(subsystem: "com.musicassistant.mobile", category: "Library") /// Manages library data and caching @Observable final class MALibraryManager { // MARK: - Properties private weak var service: MAService? // Cache private(set) var artists: [MAArtist] = [] private(set) var albums: [MAAlbum] = [] private(set) var playlists: [MAPlaylist] = [] // Pagination private var artistsOffset = 0 private var albumsOffset = 0 private var hasMoreArtists = true private var hasMoreAlbums = true private let pageSize = 50 // Loading states private(set) var isLoadingArtists = false private(set) var isLoadingAlbums = false private(set) var isLoadingPlaylists = false // MARK: - Initialization init(service: MAService?) { self.service = service } func setService(_ service: MAService) { self.service = service } // MARK: - Artists /// Load initial artists func loadArtists(refresh: Bool = false) async throws { guard !isLoadingArtists else { return } guard let service else { throw MAWebSocketClient.ClientError.notConnected } if refresh { artistsOffset = 0 hasMoreArtists = true await MainActor.run { self.artists = [] } } guard hasMoreArtists else { return } isLoadingArtists = true defer { isLoadingArtists = false } logger.info("Loading artists (offset: \(self.artistsOffset))") do { let newArtists = try await service.getArtists( limit: pageSize, offset: artistsOffset ) await MainActor.run { if refresh { self.artists = newArtists } else { self.artists.append(contentsOf: newArtists) } self.artistsOffset += newArtists.count self.hasMoreArtists = newArtists.count >= self.pageSize logger.info("Loaded \(newArtists.count) artists, total: \(self.artists.count)") } } catch { logger.error("Failed to load artists: \(error.localizedDescription)") throw error } } /// Load more artists (pagination) func loadMoreArtistsIfNeeded(currentItem: MAArtist?) async throws { guard let currentItem else { return } let thresholdIndex = artists.index(artists.endIndex, offsetBy: -10) if let itemIndex = artists.firstIndex(where: { $0.id == currentItem.id }), itemIndex >= thresholdIndex { try await loadArtists(refresh: false) } } // MARK: - Albums /// Load initial albums func loadAlbums(refresh: Bool = false) async throws { guard !isLoadingAlbums else { return } guard let service else { throw MAWebSocketClient.ClientError.notConnected } if refresh { albumsOffset = 0 hasMoreAlbums = true await MainActor.run { self.albums = [] } } guard hasMoreAlbums else { return } isLoadingAlbums = true defer { isLoadingAlbums = false } logger.info("Loading albums (offset: \(self.albumsOffset))") do { let newAlbums = try await service.getAlbums( limit: pageSize, offset: albumsOffset ) await MainActor.run { if refresh { self.albums = newAlbums } else { self.albums.append(contentsOf: newAlbums) } self.albumsOffset += newAlbums.count self.hasMoreAlbums = newAlbums.count >= self.pageSize logger.info("Loaded \(newAlbums.count) albums, total: \(self.albums.count)") } } catch { logger.error("Failed to load albums: \(error.localizedDescription)") throw error } } /// Load more albums (pagination) func loadMoreAlbumsIfNeeded(currentItem: MAAlbum?) async throws { guard let currentItem else { return } let thresholdIndex = albums.index(albums.endIndex, offsetBy: -10) if let itemIndex = albums.firstIndex(where: { $0.id == currentItem.id }), itemIndex >= thresholdIndex { try await loadAlbums(refresh: false) } } // MARK: - Playlists /// Load playlists func loadPlaylists(refresh: Bool = false) async throws { guard !isLoadingPlaylists else { return } guard let service else { throw MAWebSocketClient.ClientError.notConnected } isLoadingPlaylists = true defer { isLoadingPlaylists = false } logger.info("Loading playlists") do { let loadedPlaylists = try await service.getPlaylists() await MainActor.run { self.playlists = loadedPlaylists logger.info("Loaded \(loadedPlaylists.count) playlists") } } catch { logger.error("Failed to load playlists: \(error.localizedDescription)") throw error } } // MARK: - Album Tracks /// Get tracks for an album func getAlbumTracks(albumUri: String) async throws -> [MAMediaItem] { guard let service else { throw MAWebSocketClient.ClientError.notConnected } logger.info("Loading tracks for album \(albumUri)") return try await service.getAlbumTracks(albumUri: albumUri) } // MARK: - Search /// Search library func search(query: String, mediaTypes: [MediaType]? = nil) async throws -> [MAMediaItem] { guard !query.isEmpty else { return [] } guard let service else { throw MAWebSocketClient.ClientError.notConnected } logger.info("Searching for '\(query)'") return try await service.search(query: query, mediaTypes: mediaTypes) } }