Pull to reload, Umplatzierung Search-Button, Album-Artist
This commit is contained in:
@@ -19,18 +19,22 @@ final class MALibraryManager {
|
||||
|
||||
// Published library data
|
||||
private(set) var artists: [MAArtist] = []
|
||||
private(set) var albumArtists: [MAArtist] = []
|
||||
private(set) var albums: [MAAlbum] = []
|
||||
private(set) var playlists: [MAPlaylist] = []
|
||||
|
||||
// Pagination
|
||||
private var artistsOffset = 0
|
||||
private var albumArtistsOffset = 0
|
||||
private var albumsOffset = 0
|
||||
private var hasMoreArtists = true
|
||||
private var hasMoreAlbumArtists = true
|
||||
private var hasMoreAlbums = true
|
||||
private let pageSize = 50
|
||||
|
||||
// Loading states
|
||||
private(set) var isLoadingArtists = false
|
||||
private(set) var isLoadingAlbumArtists = false
|
||||
private(set) var isLoadingAlbums = false
|
||||
private(set) var isLoadingPlaylists = false
|
||||
|
||||
@@ -40,6 +44,7 @@ final class MALibraryManager {
|
||||
|
||||
// Last refresh timestamps (persisted in UserDefaults)
|
||||
private(set) var lastArtistsRefresh: Date?
|
||||
private(set) var lastAlbumArtistsRefresh: Date?
|
||||
private(set) var lastAlbumsRefresh: Date?
|
||||
private(set) var lastPlaylistsRefresh: Date?
|
||||
|
||||
@@ -71,7 +76,7 @@ final class MALibraryManager {
|
||||
logger.info("Cache version mismatch (\(storedVersion) → \(Self.cacheVersion)), clearing library cache")
|
||||
try? FileManager.default.removeItem(at: cacheDirectory)
|
||||
try? FileManager.default.createDirectory(at: cacheDirectory, withIntermediateDirectories: true)
|
||||
for key in ["lib.lastArtistsRefresh", "lib.lastAlbumsRefresh", "lib.lastPlaylistsRefresh"] {
|
||||
for key in ["lib.lastArtistsRefresh", "lib.lastAlbumArtistsRefresh", "lib.lastAlbumsRefresh", "lib.lastPlaylistsRefresh"] {
|
||||
UserDefaults.standard.removeObject(forKey: key)
|
||||
}
|
||||
UserDefaults.standard.set(Self.cacheVersion, forKey: "lib.cacheVersion")
|
||||
@@ -90,6 +95,11 @@ final class MALibraryManager {
|
||||
artistsOffset = cached.count
|
||||
logger.info("Loaded \(cached.count) artists from disk cache")
|
||||
}
|
||||
if let cached: [MAArtist] = load("albumartists.json") {
|
||||
albumArtists = cached
|
||||
albumArtistsOffset = cached.count
|
||||
logger.info("Loaded \(cached.count) album artists from disk cache")
|
||||
}
|
||||
if let cached: [MAAlbum] = load("albums.json") {
|
||||
albums = cached
|
||||
albumsOffset = cached.count
|
||||
@@ -102,12 +112,14 @@ final class MALibraryManager {
|
||||
|
||||
// Seed favorite URIs from cached data
|
||||
for artist in artists where artist.favorite { favoriteURIs.insert(artist.uri) }
|
||||
for artist in albumArtists 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
|
||||
lastPlaylistsRefresh = ud.object(forKey: "lib.lastPlaylistsRefresh") as? Date
|
||||
lastArtistsRefresh = ud.object(forKey: "lib.lastArtistsRefresh") as? Date
|
||||
lastAlbumArtistsRefresh = ud.object(forKey: "lib.lastAlbumArtistsRefresh") as? Date
|
||||
lastAlbumsRefresh = ud.object(forKey: "lib.lastAlbumsRefresh") as? Date
|
||||
lastPlaylistsRefresh = ud.object(forKey: "lib.lastPlaylistsRefresh") as? Date
|
||||
}
|
||||
|
||||
private func save<T: Encodable>(_ value: T, _ filename: String) {
|
||||
@@ -136,45 +148,49 @@ final class MALibraryManager {
|
||||
guard !isLoadingArtists else { return }
|
||||
guard let service else { throw MAWebSocketClient.ClientError.notConnected }
|
||||
|
||||
// For refresh, reset pagination counters but keep existing data visible until new data arrives
|
||||
let fetchOffset = refresh ? 0 : artistsOffset
|
||||
if refresh {
|
||||
hasMoreArtists = true
|
||||
}
|
||||
// Full refresh: load ALL pages from scratch so we never lose data
|
||||
isLoadingArtists = true
|
||||
defer { isLoadingArtists = false }
|
||||
|
||||
guard hasMoreArtists else { return }
|
||||
|
||||
isLoadingArtists = true
|
||||
defer { isLoadingArtists = false }
|
||||
|
||||
logger.info("Loading artists (offset: \(fetchOffset), refresh: \(refresh))")
|
||||
|
||||
let newArtists = try await service.getArtists(limit: pageSize, offset: fetchOffset)
|
||||
|
||||
// DEBUG: log first artist's image state so we can trace artwork loading
|
||||
if let a = newArtists.first {
|
||||
logger.debug("DEBUG Artist[0] name=\(a.name) metadata=\(String(describing: a.metadata)) imageUrl=\(a.imageUrl ?? "nil") imageProvider=\(a.imageProvider ?? "nil")")
|
||||
}
|
||||
|
||||
// Replace or append atomically — no intermediate empty state
|
||||
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 {
|
||||
logger.info("Refreshing all artists")
|
||||
var allArtists: [MAArtist] = []
|
||||
var offset = 0
|
||||
var hasMore = true
|
||||
while hasMore {
|
||||
let page = try await service.getArtists(limit: pageSize, offset: offset)
|
||||
allArtists.append(contentsOf: page)
|
||||
offset += page.count
|
||||
hasMore = page.count >= pageSize
|
||||
}
|
||||
artists = allArtists
|
||||
artistsOffset = allArtists.count
|
||||
hasMoreArtists = false
|
||||
for a in allArtists where a.favorite { favoriteURIs.insert(a.uri) }
|
||||
save(artists, "artists.json")
|
||||
lastArtistsRefresh = markRefreshed("lib.lastArtistsRefresh")
|
||||
}
|
||||
logger.info("Refreshed all artists, total: \(allArtists.count)")
|
||||
} else {
|
||||
// Pagination: load next page
|
||||
guard hasMoreArtists else { return }
|
||||
|
||||
logger.info("Loaded \(newArtists.count) artists, total: \(self.artists.count)")
|
||||
isLoadingArtists = true
|
||||
defer { isLoadingArtists = false }
|
||||
|
||||
logger.info("Loading artists page (offset: \(self.artistsOffset))")
|
||||
let newArtists = try await service.getArtists(limit: pageSize, offset: self.artistsOffset)
|
||||
|
||||
artists.append(contentsOf: newArtists)
|
||||
artistsOffset += newArtists.count
|
||||
for a in newArtists where a.favorite { favoriteURIs.insert(a.uri) }
|
||||
hasMoreArtists = newArtists.count >= pageSize
|
||||
|
||||
if !hasMoreArtists || artistsOffset <= pageSize {
|
||||
save(artists, "artists.json")
|
||||
lastArtistsRefresh = markRefreshed("lib.lastArtistsRefresh")
|
||||
}
|
||||
logger.info("Loaded \(newArtists.count) artists, total: \(self.artists.count)")
|
||||
}
|
||||
}
|
||||
|
||||
/// Persist updated artist list after pagination completes.
|
||||
@@ -187,43 +203,111 @@ final class MALibraryManager {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Album Artists
|
||||
|
||||
func loadAlbumArtists(refresh: Bool = false) async throws {
|
||||
guard !isLoadingAlbumArtists else { return }
|
||||
guard let service else { throw MAWebSocketClient.ClientError.notConnected }
|
||||
|
||||
if refresh {
|
||||
isLoadingAlbumArtists = true
|
||||
defer { isLoadingAlbumArtists = false }
|
||||
|
||||
logger.info("Refreshing all album artists")
|
||||
var allArtists: [MAArtist] = []
|
||||
var offset = 0
|
||||
var hasMore = true
|
||||
while hasMore {
|
||||
let page = try await service.getAlbumArtists(limit: pageSize, offset: offset)
|
||||
allArtists.append(contentsOf: page)
|
||||
offset += page.count
|
||||
hasMore = page.count >= pageSize
|
||||
}
|
||||
albumArtists = allArtists
|
||||
albumArtistsOffset = allArtists.count
|
||||
hasMoreAlbumArtists = false
|
||||
for a in allArtists where a.favorite { favoriteURIs.insert(a.uri) }
|
||||
save(albumArtists, "albumartists.json")
|
||||
lastAlbumArtistsRefresh = markRefreshed("lib.lastAlbumArtistsRefresh")
|
||||
logger.info("Refreshed all album artists, total: \(allArtists.count)")
|
||||
} else {
|
||||
guard hasMoreAlbumArtists else { return }
|
||||
|
||||
isLoadingAlbumArtists = true
|
||||
defer { isLoadingAlbumArtists = false }
|
||||
|
||||
logger.info("Loading album artists page (offset: \(self.albumArtistsOffset))")
|
||||
let newArtists = try await service.getAlbumArtists(limit: pageSize, offset: self.albumArtistsOffset)
|
||||
|
||||
albumArtists.append(contentsOf: newArtists)
|
||||
albumArtistsOffset += newArtists.count
|
||||
for a in newArtists where a.favorite { favoriteURIs.insert(a.uri) }
|
||||
hasMoreAlbumArtists = newArtists.count >= pageSize
|
||||
|
||||
if !hasMoreAlbumArtists || albumArtistsOffset <= pageSize {
|
||||
save(albumArtists, "albumartists.json")
|
||||
lastAlbumArtistsRefresh = markRefreshed("lib.lastAlbumArtistsRefresh")
|
||||
}
|
||||
logger.info("Loaded \(newArtists.count) album artists, total: \(self.albumArtists.count)")
|
||||
}
|
||||
}
|
||||
|
||||
func loadMoreAlbumArtistsIfNeeded(currentItem: MAArtist?) async throws {
|
||||
guard let currentItem else { return }
|
||||
let thresholdIndex = albumArtists.index(albumArtists.endIndex, offsetBy: -10)
|
||||
if let idx = albumArtists.firstIndex(where: { $0.id == currentItem.id }), idx >= thresholdIndex {
|
||||
try await loadAlbumArtists(refresh: false)
|
||||
save(albumArtists, "albumartists.json")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Albums
|
||||
|
||||
func loadAlbums(refresh: Bool = false) async throws {
|
||||
guard !isLoadingAlbums else { return }
|
||||
guard let service else { throw MAWebSocketClient.ClientError.notConnected }
|
||||
|
||||
let fetchOffset = refresh ? 0 : albumsOffset
|
||||
if refresh {
|
||||
hasMoreAlbums = true
|
||||
}
|
||||
isLoadingAlbums = true
|
||||
defer { isLoadingAlbums = false }
|
||||
|
||||
guard hasMoreAlbums else { return }
|
||||
|
||||
isLoadingAlbums = true
|
||||
defer { isLoadingAlbums = false }
|
||||
|
||||
logger.info("Loading albums (offset: \(fetchOffset), refresh: \(refresh))")
|
||||
|
||||
let newAlbums = try await service.getAlbums(limit: pageSize, offset: fetchOffset)
|
||||
|
||||
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 {
|
||||
logger.info("Refreshing all albums")
|
||||
var allAlbums: [MAAlbum] = []
|
||||
var offset = 0
|
||||
var hasMore = true
|
||||
while hasMore {
|
||||
let page = try await service.getAlbums(limit: pageSize, offset: offset)
|
||||
allAlbums.append(contentsOf: page)
|
||||
offset += page.count
|
||||
hasMore = page.count >= pageSize
|
||||
}
|
||||
albums = allAlbums
|
||||
albumsOffset = allAlbums.count
|
||||
hasMoreAlbums = false
|
||||
for a in allAlbums where a.favorite { favoriteURIs.insert(a.uri) }
|
||||
save(albums, "albums.json")
|
||||
lastAlbumsRefresh = markRefreshed("lib.lastAlbumsRefresh")
|
||||
}
|
||||
logger.info("Refreshed all albums, total: \(allAlbums.count)")
|
||||
} else {
|
||||
guard hasMoreAlbums else { return }
|
||||
|
||||
logger.info("Loaded \(newAlbums.count) albums, total: \(self.albums.count)")
|
||||
isLoadingAlbums = true
|
||||
defer { isLoadingAlbums = false }
|
||||
|
||||
logger.info("Loading albums page (offset: \(self.albumsOffset))")
|
||||
let newAlbums = try await service.getAlbums(limit: pageSize, offset: self.albumsOffset)
|
||||
|
||||
albums.append(contentsOf: newAlbums)
|
||||
albumsOffset += newAlbums.count
|
||||
for a in newAlbums where a.favorite { favoriteURIs.insert(a.uri) }
|
||||
hasMoreAlbums = newAlbums.count >= pageSize
|
||||
|
||||
if !hasMoreAlbums || albumsOffset <= pageSize {
|
||||
save(albums, "albums.json")
|
||||
lastAlbumsRefresh = markRefreshed("lib.lastAlbumsRefresh")
|
||||
}
|
||||
logger.info("Loaded \(newAlbums.count) albums, total: \(self.albums.count)")
|
||||
}
|
||||
}
|
||||
|
||||
func loadMoreAlbumsIfNeeded(currentItem: MAAlbum?) async throws {
|
||||
|
||||
Reference in New Issue
Block a user