7.9 KiB
Session 2026-01-17: Optimistic Updates & Podcast Provider Filtering
Overview
This session focused on fixing provider filtering for podcasts, resolving iTunes podcast provider issues, and implementing optimistic updates for library buttons across detail screens.
Issues Addressed
1. Podcast Provider Counts Showing Incorrectly
Problem: All Spotify accounts showed the same podcast count instead of only counting podcasts owned by each account.
Root Cause: The podcast counting logic wasn't checking the inLibrary field on provider mappings.
Solution:
- Added
inLibraryfield toProviderMappingmodel - Updated
getProvidersWithPodcasts()to only count mappings whereinLibrary == true - Updated podcast filtering in library screen to check
inLibraryfield
Files Modified:
lib/models/media_item.dart- AddedinLibraryfieldlib/providers/music_assistant_provider.dart- Updated counting logiclib/screens/new_library_screen.dart- Updated filtering logic
2. iTunes Podcasts Not Appearing in Library
Problem: Podcasts added from iTunes search weren't showing in library and iTunes provider filter showed 0 count.
Investigation:
- Added
itunes_podcaststo provider capabilities and music provider domains - Added synthetic provider instance creation for search-only providers
- Added debug logging to understand podcast loading
Resolution: The issue was with Music Assistant's iTunes podcast provider itself - podcasts added via iTunes weren't being properly stored in the MA library. The user removed the iTunes provider from MA and re-added podcasts via Spotify search as a workaround.
Files Modified:
lib/models/provider_instance.dart- Addeditunes_podcaststo capabilities
3. Episode Loading Failing for Non-Library Podcasts
Problem: When a podcast was removed from library, the old library ID became invalid and episodes wouldn't load.
Solution: Added fallback logic to try loading episodes via provider mappings when the primary ID fails.
Files Modified:
lib/screens/podcast_detail_screen.dart- Added fallback episode loading
4. Search Filtering Rework
Problem: Server-side provider filtering was being applied to search results when it should only apply when the "in library" toggle is enabled.
Solution:
- Removed
filterSearchResults()calls from search methods - Added client-side
_filterByEnabledProviders()helper - Applied filtering only when
_libraryOnlytoggle is ON
Files Modified:
lib/providers/music_assistant_provider.dart- Removed filterSearchResults callslib/screens/search_screen.dart- Added client-side filtering
5. Library Buttons Not Updating Immediately
Problem: Add/remove library buttons on detail screens waited for API response before updating, causing perceived lag.
Solution: Implemented optimistic updates - update UI immediately, revert on failure.
Files Modified:
lib/screens/podcast_detail_screen.dartlib/screens/album_details_screen.dartlib/screens/artist_details_screen.dart
Code Changes
ProviderMapping.inLibrary Field
// lib/models/media_item.dart
class ProviderMapping {
final String itemId;
final String providerDomain;
final String providerInstance;
final bool available;
final Map<String, dynamic>? audioFormat;
final bool inLibrary; // NEW: indicates ownership
factory ProviderMapping.fromJson(Map<String, dynamic> json) {
return ProviderMapping(
// ... other fields ...
inLibrary: json['in_library'] == true || json['in_library'] == 1,
);
}
}
Podcast Provider Counting
// lib/providers/music_assistant_provider.dart
Map<String, int> getProvidersWithPodcasts() {
final counts = <String, int>{};
for (final item in _podcasts) {
final mappings = item.providerMappings;
if (mappings != null) {
for (final mapping in mappings) {
// Only count if this provider "owns" the item
if (mapping.inLibrary) {
final instanceId = mapping.providerInstance;
if (instanceId.isNotEmpty) {
counts[instanceId] = (counts[instanceId] ?? 0) + 1;
}
}
}
}
}
return counts;
}
Optimistic Update Pattern
// Example from podcast_detail_screen.dart
Future<void> _toggleLibrary() async {
final newState = !_isInLibrary;
// OPTIMISTIC UPDATE: Update UI immediately
setState(() {
_isInLibrary = newState;
});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(S.of(context)!.addedToLibrary)),
);
}
// Fire and forget - API call happens in background
maProvider.addToLibrary(...).catchError((e) {
// Revert on failure
if (mounted) {
setState(() {
_isInLibrary = !newState;
});
}
});
}
Client-Side Search Filtering
// lib/screens/search_screen.dart
Map<String, List<MediaItem>> _filterByEnabledProviders(
Map<String, List<MediaItem>> results,
Set<String> enabledProviders,
) {
if (enabledProviders.isEmpty) return results;
bool isItemAllowed(MediaItem item) {
final mappings = item.providerMappings;
if (mappings == null || mappings.isEmpty) return true;
return mappings.any((m) => m.inLibrary && enabledProviders.contains(m.providerInstance));
}
return {
for (final entry in results.entries)
entry.key: entry.value.where(isItemAllowed).toList(),
};
}
// Applied when setting results:
if (_libraryOnly) {
final enabledProviders = provider.enabledProviderIds.toSet();
combinedResults = _filterByEnabledProviders(combinedResults, enabledProviders);
}
Provider Capabilities Update
// lib/models/provider_instance.dart
static const Map<String, Set<String>> providerCapabilities = {
'spotify': {'artists', 'albums', 'tracks', 'playlists', 'audiobooks', 'podcasts'},
'ytmusic': {'artists', 'albums', 'tracks', 'playlists', 'podcasts'},
'apple_music': {'artists', 'albums', 'tracks', 'playlists', 'podcasts'},
'plex': {'artists', 'albums', 'tracks', 'playlists', 'audiobooks', 'podcasts'},
'jellyfin': {'artists', 'albums', 'tracks', 'playlists', 'audiobooks', 'podcasts'},
'emby': {'artists', 'albums', 'tracks', 'playlists', 'audiobooks', 'podcasts'},
'audiobookshelf': {'audiobooks', 'podcasts'},
'itunes_podcasts': {'podcasts'}, // NEW
'tunein': {'radio', 'podcasts'},
// ... other providers
};
Summary of All Modified Files
| File | Changes |
|---|---|
lib/models/media_item.dart |
Added inLibrary field to ProviderMapping |
lib/models/provider_instance.dart |
Added itunes_podcasts, podcasts to capabilities |
lib/providers/music_assistant_provider.dart |
Updated podcast counting, removed filterSearchResults |
lib/screens/new_library_screen.dart |
Updated podcast filtering with inLibrary check |
lib/screens/search_screen.dart |
Added client-side filtering for in-library mode |
lib/screens/podcast_detail_screen.dart |
Optimistic updates, fallback episode loading |
lib/screens/album_details_screen.dart |
Optimistic updates for library button |
lib/screens/artist_details_screen.dart |
Optimistic updates for library button |
Key Learnings
-
inLibrary field: Music Assistant's provider mappings include an
in_libraryfield that indicates whether the user added the item from that specific provider account. This is essential for accurate per-provider filtering. -
iTunes podcast provider: The MA iTunes podcast provider is for search only - it doesn't properly store items in the library. Users should add podcasts from streaming providers like Spotify instead.
-
Optimistic updates: For better UX, library toggle buttons should update immediately and only revert if the API call fails, rather than waiting for API response.
-
Search vs Library filtering: Server-side provider filtering should only apply to library views, not search results (unless the "in library" toggle is enabled).