218 lines
7.9 KiB
Markdown
218 lines
7.9 KiB
Markdown
# 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 `inLibrary` field to `ProviderMapping` model
|
|
- Updated `getProvidersWithPodcasts()` to only count mappings where `inLibrary == true`
|
|
- Updated podcast filtering in library screen to check `inLibrary` field
|
|
|
|
**Files Modified**:
|
|
- `lib/models/media_item.dart` - Added `inLibrary` field
|
|
- `lib/providers/music_assistant_provider.dart` - Updated counting logic
|
|
- `lib/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_podcasts` to 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` - Added `itunes_podcasts` to 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 `_libraryOnly` toggle is ON
|
|
|
|
**Files Modified**:
|
|
- `lib/providers/music_assistant_provider.dart` - Removed filterSearchResults calls
|
|
- `lib/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.dart`
|
|
- `lib/screens/album_details_screen.dart`
|
|
- `lib/screens/artist_details_screen.dart`
|
|
|
|
## Code Changes
|
|
|
|
### ProviderMapping.inLibrary Field
|
|
|
|
```dart
|
|
// 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
|
|
|
|
```dart
|
|
// 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
|
|
|
|
```dart
|
|
// 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
|
|
|
|
```dart
|
|
// 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
|
|
|
|
```dart
|
|
// 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
|
|
|
|
1. **inLibrary field**: Music Assistant's provider mappings include an `in_library` field that indicates whether the user added the item from that specific provider account. This is essential for accurate per-provider filtering.
|
|
|
|
2. **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.
|
|
|
|
3. **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.
|
|
|
|
4. **Search vs Library filtering**: Server-side provider filtering should only apply to library views, not search results (unless the "in library" toggle is enabled).
|