Queue, Favorites, Providers, Now playing

This commit is contained in:
2026-04-06 11:46:04 +02:00
parent e7e9a59e70
commit 56199db301
12 changed files with 462 additions and 58 deletions
@@ -34,6 +34,10 @@ final class MALibraryManager {
private(set) var isLoadingAlbums = false
private(set) var isLoadingPlaylists = false
/// URIs currently marked as favorites source of truth for UI.
/// Populated from decoded model data, then mutated optimistically on toggle.
private(set) var favoriteURIs: Set<String> = []
// Last refresh timestamps (persisted in UserDefaults)
private(set) var lastArtistsRefresh: Date?
private(set) var lastAlbumsRefresh: Date?
@@ -42,7 +46,7 @@ final class MALibraryManager {
// MARK: - Disk Cache
/// Increment this whenever the model format changes to invalidate stale caches.
private static let cacheVersion = 2
private static let cacheVersion = 3
private let cacheDirectory: URL = {
let caches = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
@@ -96,6 +100,10 @@ final class MALibraryManager {
logger.info("Loaded \(cached.count) playlists from disk cache")
}
// Seed favorite URIs from cached data
for artist in artists where artist.favorite { favoriteURIs.insert(artist.uri) }
for album in albums where album.favorite { favoriteURIs.insert(album.uri) }
let ud = UserDefaults.standard
lastArtistsRefresh = ud.object(forKey: "lib.lastArtistsRefresh") as? Date
lastAlbumsRefresh = ud.object(forKey: "lib.lastAlbumsRefresh") as? Date
@@ -152,10 +160,13 @@ final class MALibraryManager {
if refresh {
artists = newArtists
artistsOffset = newArtists.count
// Reset and repopulate artist favorites on refresh
for a in artists where a.favorite { favoriteURIs.insert(a.uri) }
} else {
artists.append(contentsOf: newArtists)
artistsOffset += newArtists.count
}
for a in newArtists where a.favorite { favoriteURIs.insert(a.uri) }
hasMoreArtists = newArtists.count >= pageSize
if refresh || artistsOffset <= pageSize {
@@ -199,10 +210,12 @@ final class MALibraryManager {
if refresh {
albums = newAlbums
albumsOffset = newAlbums.count
for a in albums where a.favorite { favoriteURIs.insert(a.uri) }
} else {
albums.append(contentsOf: newAlbums)
albumsOffset += newAlbums.count
}
for a in newAlbums where a.favorite { favoriteURIs.insert(a.uri) }
hasMoreAlbums = newAlbums.count >= pageSize
if refresh || albumsOffset <= pageSize {
@@ -255,6 +268,53 @@ final class MALibraryManager {
return try await service.getAlbumTracks(albumUri: albumUri)
}
func getPlaylistTracks(playlistUri: String) async throws -> [MAMediaItem] {
guard let service else { throw MAWebSocketClient.ClientError.notConnected }
logger.info("Loading tracks for playlist \(playlistUri)")
return try await service.getPlaylistTracks(playlistUri: playlistUri)
}
// MARK: - Favorites
/// Returns whether the given URI is currently favorited.
func isFavorite(uri: String) -> Bool {
favoriteURIs.contains(uri)
}
/// Toggle favorite for any item. Performs optimistic update, then calls server.
/// Reverts on failure.
func toggleFavorite(uri: String, currentlyFavorite: Bool) async {
// Optimistic update
if currentlyFavorite {
favoriteURIs.remove(uri)
} else {
favoriteURIs.insert(uri)
}
// Call server
guard let service else {
// Revert if no service
if currentlyFavorite { favoriteURIs.insert(uri) } else { favoriteURIs.remove(uri) }
return
}
do {
if currentlyFavorite {
try await service.removeFavorite(uri: uri)
} else {
try await service.addFavorite(uri: uri)
}
} catch {
// Revert on failure
if currentlyFavorite {
favoriteURIs.insert(uri)
} else {
favoriteURIs.remove(uri)
}
logger.error("Failed to toggle favorite for \(uri): \(error)")
}
}
// MARK: - Search
func search(query: String, mediaTypes: [MediaType]? = nil) async throws -> [MAMediaItem] {