Initial Commit

This commit is contained in:
2026-03-27 09:21:41 +01:00
commit e9b6412d71
40 changed files with 6801 additions and 0 deletions
@@ -0,0 +1,217 @@
//
// 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)
}
}