218 lines
6.4 KiB
Swift
218 lines
6.4 KiB
Swift
//
|
|
// 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)
|
|
}
|
|
}
|