refactor: discover page performance improvements
This commit is contained in:
+24
-19
@@ -21,7 +21,6 @@ import 'providers/playback_state_provider.dart';
|
||||
import 'services/multi_server_manager.dart';
|
||||
import 'services/data_aggregation_service.dart';
|
||||
import 'services/server_registry.dart';
|
||||
import 'utils/language_codes.dart';
|
||||
import 'utils/app_logger.dart';
|
||||
import 'utils/orientation_helper.dart';
|
||||
import 'i18n/strings.g.dart';
|
||||
@@ -40,20 +39,22 @@ void main() async {
|
||||
PaintingBinding.instance.imageCache.maximumSizeBytes = 500 << 20; // 500MB
|
||||
PaintingBinding.instance.imageCache.maximumSize = 500; // 500 images
|
||||
|
||||
// Initialize services in parallel where possible
|
||||
final futures = <Future<void>>[];
|
||||
|
||||
// Initialize window_manager for desktop platforms
|
||||
if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) {
|
||||
await windowManager.ensureInitialized();
|
||||
futures.add(windowManager.ensureInitialized());
|
||||
}
|
||||
|
||||
// Configure macOS window with custom titlebar
|
||||
await MacOSTitlebarService.setupCustomTitlebar();
|
||||
// Configure macOS window with custom titlebar (depends on window manager)
|
||||
futures.add(MacOSTitlebarService.setupCustomTitlebar());
|
||||
|
||||
// Note: Orientation will be set dynamically based on device type in MainApp
|
||||
// Initialize storage service
|
||||
futures.add(StorageService.getInstance().then((_) {}));
|
||||
|
||||
await StorageService.getInstance();
|
||||
|
||||
// Initialize language codes for track selection
|
||||
await LanguageCodes.initialize();
|
||||
// Wait for all parallel services to complete
|
||||
await Future.wait(futures);
|
||||
|
||||
// Initialize logger level based on debug setting
|
||||
final debugEnabled = settings.getEnableDebugLogging();
|
||||
@@ -90,12 +91,16 @@ class MainApp extends StatelessWidget {
|
||||
),
|
||||
ChangeNotifierProvider(create: (context) => ServerStateProvider()),
|
||||
// Existing providers
|
||||
ChangeNotifierProvider(
|
||||
create: (context) => UserProfileProvider()..initialize(),
|
||||
),
|
||||
ChangeNotifierProvider(create: (context) => UserProfileProvider()),
|
||||
ChangeNotifierProvider(create: (context) => ThemeProvider()),
|
||||
ChangeNotifierProvider(create: (context) => SettingsProvider()),
|
||||
ChangeNotifierProvider(create: (context) => HiddenLibrariesProvider()),
|
||||
ChangeNotifierProvider(
|
||||
create: (context) => SettingsProvider(),
|
||||
lazy: true,
|
||||
),
|
||||
ChangeNotifierProvider(
|
||||
create: (context) => HiddenLibrariesProvider(),
|
||||
lazy: true,
|
||||
),
|
||||
ChangeNotifierProvider(create: (context) => PlaybackStateProvider()),
|
||||
],
|
||||
child: Consumer<ThemeProvider>(
|
||||
@@ -155,7 +160,7 @@ class _SetupScreenState extends State<SetupScreen> {
|
||||
_loadSavedCredentials();
|
||||
}
|
||||
|
||||
void _checkForUpdatesOnStartup() async {
|
||||
Future<void> _checkForUpdatesOnStartup() async {
|
||||
// Delay slightly to allow UI to settle
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
@@ -284,10 +289,7 @@ class _SetupScreenState extends State<SetupScreen> {
|
||||
appLogger.i('Successfully connected to $connectedCount servers');
|
||||
|
||||
if (mounted) {
|
||||
// Check for updates BEFORE navigation
|
||||
_checkForUpdatesOnStartup();
|
||||
|
||||
// Navigate to main screen
|
||||
// Navigate to main screen immediately
|
||||
// Get first connected client for backward compatibility
|
||||
final firstClient =
|
||||
multiServerProvider.serverManager.onlineClients.values.first;
|
||||
@@ -298,6 +300,9 @@ class _SetupScreenState extends State<SetupScreen> {
|
||||
builder: (context) => MainScreen(client: firstClient),
|
||||
),
|
||||
);
|
||||
|
||||
// Check for updates in background after navigation
|
||||
_checkForUpdatesOnStartup();
|
||||
}
|
||||
} else {
|
||||
// All connections failed
|
||||
|
||||
@@ -10,13 +10,17 @@ class HiddenLibrariesProvider extends ChangeNotifier {
|
||||
bool _isInitialized = false;
|
||||
|
||||
/// Get an unmodifiable copy of hidden library keys
|
||||
Set<String> get hiddenLibraryKeys => Set.unmodifiable(_hiddenLibraryKeys);
|
||||
Set<String> get hiddenLibraryKeys {
|
||||
if (!_isInitialized) _initialize();
|
||||
return Set.unmodifiable(_hiddenLibraryKeys);
|
||||
}
|
||||
|
||||
/// Check if the provider has completed initialization
|
||||
bool get isInitialized => _isInitialized;
|
||||
|
||||
HiddenLibrariesProvider() {
|
||||
_initialize();
|
||||
// Don't initialize immediately if lazy-loaded
|
||||
// _initialize() will be called when first accessed
|
||||
}
|
||||
|
||||
/// Initialize the provider by loading hidden libraries from storage
|
||||
@@ -30,6 +34,7 @@ class HiddenLibrariesProvider extends ChangeNotifier {
|
||||
/// Hide a library by its key
|
||||
/// Updates both in-memory state and persistent storage
|
||||
Future<void> hideLibrary(String libraryKey) async {
|
||||
if (!_isInitialized) await _initialize();
|
||||
if (!_hiddenLibraryKeys.contains(libraryKey)) {
|
||||
_hiddenLibraryKeys = Set.from(_hiddenLibraryKeys)..add(libraryKey);
|
||||
await _storageService.saveHiddenLibraries(_hiddenLibraryKeys);
|
||||
@@ -40,6 +45,7 @@ class HiddenLibrariesProvider extends ChangeNotifier {
|
||||
/// Unhide a library by its key
|
||||
/// Updates both in-memory state and persistent storage
|
||||
Future<void> unhideLibrary(String libraryKey) async {
|
||||
if (!_isInitialized) await _initialize();
|
||||
if (_hiddenLibraryKeys.contains(libraryKey)) {
|
||||
_hiddenLibraryKeys = Set.from(_hiddenLibraryKeys)..remove(libraryKey);
|
||||
await _storageService.saveHiddenLibraries(_hiddenLibraryKeys);
|
||||
@@ -49,6 +55,7 @@ class HiddenLibrariesProvider extends ChangeNotifier {
|
||||
|
||||
/// Check if a specific library is hidden
|
||||
bool isLibraryHidden(String libraryKey) {
|
||||
if (!_isInitialized) _initialize();
|
||||
return _hiddenLibraryKeys.contains(libraryKey);
|
||||
}
|
||||
|
||||
|
||||
@@ -67,6 +67,7 @@ class MultiServerProvider extends ChangeNotifier {
|
||||
/// Clear all server connections
|
||||
void clearAllConnections() {
|
||||
_serverManager.disconnectAll();
|
||||
_aggregationService.clearCache(); // Clear cached data when servers change
|
||||
appLogger.d('MultiServerProvider: All connections cleared');
|
||||
notifyListeners();
|
||||
}
|
||||
@@ -79,6 +80,7 @@ class MultiServerProvider extends ChangeNotifier {
|
||||
}) async {
|
||||
// Clear existing connections first
|
||||
_serverManager.disconnectAll();
|
||||
_aggregationService.clearCache(); // Clear cached data when servers change
|
||||
appLogger.d(
|
||||
'MultiServerProvider: Cleared connections, reconnecting to ${servers.length} servers',
|
||||
);
|
||||
|
||||
@@ -2,58 +2,82 @@ import 'package:flutter/material.dart';
|
||||
import '../services/settings_service.dart';
|
||||
|
||||
class SettingsProvider extends ChangeNotifier {
|
||||
late SettingsService _settingsService;
|
||||
SettingsService? _settingsService;
|
||||
LibraryDensity _libraryDensity = LibraryDensity.normal;
|
||||
ViewMode _viewMode = ViewMode.grid;
|
||||
bool _useSeasonPoster = false;
|
||||
bool _showHeroSection = true;
|
||||
bool _isInitialized = false;
|
||||
|
||||
SettingsProvider() {
|
||||
_initializeSettings();
|
||||
// Don't initialize immediately if lazy-loaded
|
||||
// _initializeSettings() will be called when first accessed
|
||||
}
|
||||
|
||||
Future<void> _initializeSettings() async {
|
||||
if (_isInitialized) return;
|
||||
|
||||
_settingsService = await SettingsService.getInstance();
|
||||
_libraryDensity = _settingsService.getLibraryDensity();
|
||||
_viewMode = _settingsService.getViewMode();
|
||||
_useSeasonPoster = _settingsService.getUseSeasonPoster();
|
||||
_showHeroSection = _settingsService.getShowHeroSection();
|
||||
_libraryDensity = _settingsService!.getLibraryDensity();
|
||||
_viewMode = _settingsService!.getViewMode();
|
||||
_useSeasonPoster = _settingsService!.getUseSeasonPoster();
|
||||
_showHeroSection = _settingsService!.getShowHeroSection();
|
||||
_isInitialized = true;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
LibraryDensity get libraryDensity => _libraryDensity;
|
||||
ViewMode get viewMode => _viewMode;
|
||||
bool get useSeasonPoster => _useSeasonPoster;
|
||||
bool get showHeroSection => _showHeroSection;
|
||||
LibraryDensity get libraryDensity {
|
||||
if (!_isInitialized) _initializeSettings();
|
||||
return _libraryDensity;
|
||||
}
|
||||
|
||||
ViewMode get viewMode {
|
||||
if (!_isInitialized) _initializeSettings();
|
||||
return _viewMode;
|
||||
}
|
||||
|
||||
bool get useSeasonPoster {
|
||||
if (!_isInitialized) _initializeSettings();
|
||||
return _useSeasonPoster;
|
||||
}
|
||||
|
||||
bool get showHeroSection {
|
||||
if (!_isInitialized) _initializeSettings();
|
||||
return _showHeroSection;
|
||||
}
|
||||
|
||||
Future<void> setLibraryDensity(LibraryDensity density) async {
|
||||
if (!_isInitialized) await _initializeSettings();
|
||||
if (_libraryDensity != density) {
|
||||
_libraryDensity = density;
|
||||
await _settingsService.setLibraryDensity(density);
|
||||
await _settingsService!.setLibraryDensity(density);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setViewMode(ViewMode mode) async {
|
||||
if (!_isInitialized) await _initializeSettings();
|
||||
if (_viewMode != mode) {
|
||||
_viewMode = mode;
|
||||
await _settingsService.setViewMode(mode);
|
||||
await _settingsService!.setViewMode(mode);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setUseSeasonPoster(bool value) async {
|
||||
if (!_isInitialized) await _initializeSettings();
|
||||
if (_useSeasonPoster != value) {
|
||||
_useSeasonPoster = value;
|
||||
await _settingsService.setUseSeasonPoster(value);
|
||||
await _settingsService!.setUseSeasonPoster(value);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setShowHeroSection(bool value) async {
|
||||
if (!_isInitialized) await _initializeSettings();
|
||||
if (_showHeroSection != value) {
|
||||
_showHeroSection = value;
|
||||
await _settingsService.setShowHeroSection(value);
|
||||
await _settingsService!.setShowHeroSection(value);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ class UserProfileProvider extends ChangeNotifier {
|
||||
PlexUserProfile? _profileSettings;
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
bool _isInitialized = false;
|
||||
|
||||
PlexHome? get home => _home;
|
||||
PlexHomeUser? get currentUser => _currentUser;
|
||||
@@ -56,6 +57,12 @@ class UserProfileProvider extends ChangeNotifier {
|
||||
}
|
||||
|
||||
Future<void> initialize() async {
|
||||
// Prevent duplicate initialization
|
||||
if (_isInitialized) {
|
||||
appLogger.d('UserProfileProvider: Already initialized, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
appLogger.d('UserProfileProvider: Initializing...');
|
||||
try {
|
||||
_authService = await PlexAuthService.create();
|
||||
@@ -90,6 +97,7 @@ class UserProfileProvider extends ChangeNotifier {
|
||||
// Don't set error here, cached profile (if any) was already loaded
|
||||
}
|
||||
|
||||
_isInitialized = true;
|
||||
appLogger.d('UserProfileProvider: Initialization complete');
|
||||
} catch (e) {
|
||||
appLogger.e(
|
||||
@@ -100,6 +108,7 @@ class UserProfileProvider extends ChangeNotifier {
|
||||
// Ensure services are null on failure
|
||||
_authService = null;
|
||||
_storageService = null;
|
||||
_isInitialized = false; // Allow retry on failure
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,8 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
List<PlexHub> _hubs = [];
|
||||
bool _isLoading = true;
|
||||
bool _isInitialLoad = true;
|
||||
bool _isOnDeckLoaded = false;
|
||||
bool _areHubsLoading = true;
|
||||
String? _errorMessage;
|
||||
final PageController _heroController = PageController();
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
@@ -276,6 +278,8 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
appLogger.d('Loading discover content from all servers');
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_isOnDeckLoaded = false;
|
||||
_areHubsLoading = true;
|
||||
_errorMessage = null;
|
||||
});
|
||||
|
||||
@@ -290,16 +294,45 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
throw Exception('No servers available');
|
||||
}
|
||||
|
||||
// Fetch on deck and hubs from all servers in parallel for optimal performance
|
||||
final results = await Future.wait([
|
||||
multiServerProvider.aggregationService.getOnDeckFromAllServers(
|
||||
limit: 20,
|
||||
),
|
||||
multiServerProvider.aggregationService.getHubsFromAllServers(),
|
||||
]);
|
||||
// Start OnDeck and libraries fetch in parallel
|
||||
final onDeckFuture = multiServerProvider.aggregationService
|
||||
.getOnDeckFromAllServers(limit: 20);
|
||||
final librariesFuture = multiServerProvider.aggregationService
|
||||
.getLibrariesFromAllServersGrouped();
|
||||
|
||||
final onDeck = results[0] as List<PlexMetadata>;
|
||||
final allHubs = results[1] as List<PlexHub>;
|
||||
// Wait for OnDeck to complete and show it immediately
|
||||
final onDeck = await onDeckFuture;
|
||||
|
||||
setState(() {
|
||||
_onDeck = onDeck;
|
||||
_isOnDeckLoaded = true;
|
||||
_isLoading = false; // Show content, but hubs still loading
|
||||
|
||||
// Reset hero index to avoid sync issues
|
||||
_currentHeroIndex = 0;
|
||||
});
|
||||
|
||||
// Focus the hero on initial load
|
||||
if (_isInitialLoad && onDeck.isNotEmpty) {
|
||||
_isInitialLoad = false;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
_heroFocusNode.requestFocus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Sync PageController to first page after OnDeck loads
|
||||
if (_heroController.hasClients && onDeck.isNotEmpty) {
|
||||
_heroController.jumpToPage(0);
|
||||
}
|
||||
|
||||
// Wait for libraries and then fetch hubs
|
||||
final librariesByServer = await librariesFuture;
|
||||
|
||||
// Fetch hubs using the pre-fetched libraries
|
||||
final allHubs = await multiServerProvider.aggregationService
|
||||
.getHubsFromAllServers(librariesByServer: librariesByServer);
|
||||
|
||||
// Filter out duplicate hubs that we already fetch separately
|
||||
final filteredHubs = allHubs.where((hub) {
|
||||
@@ -316,35 +349,17 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
'Received ${onDeck.length} on deck items and ${filteredHubs.length} hubs from all servers',
|
||||
);
|
||||
setState(() {
|
||||
_onDeck = onDeck;
|
||||
_hubs = filteredHubs;
|
||||
_isLoading = false;
|
||||
|
||||
// Reset hero index to avoid sync issues
|
||||
_currentHeroIndex = 0;
|
||||
_areHubsLoading = false;
|
||||
});
|
||||
|
||||
// Sync PageController to first page after data loads
|
||||
if (_heroController.hasClients && onDeck.isNotEmpty) {
|
||||
_heroController.jumpToPage(0);
|
||||
}
|
||||
|
||||
// Focus the hero on initial load
|
||||
if (_isInitialLoad && onDeck.isNotEmpty) {
|
||||
_isInitialLoad = false;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
_heroFocusNode.requestFocus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
appLogger.d('Discover content loaded successfully');
|
||||
} catch (e) {
|
||||
appLogger.e('Failed to load discover content', error: e);
|
||||
setState(() {
|
||||
_errorMessage = 'Failed to load content: $e';
|
||||
_isLoading = false;
|
||||
_areHubsLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -721,7 +736,53 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
),
|
||||
),
|
||||
|
||||
if (_onDeck.isEmpty && _hubs.isEmpty)
|
||||
// Show loading skeleton for hubs while they're loading
|
||||
if (_areHubsLoading && _hubs.isEmpty)
|
||||
for (int i = 0; i < 3; i++)
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Hub title skeleton
|
||||
Container(
|
||||
width: 200,
|
||||
height: 24,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Hub items skeleton
|
||||
SizedBox(
|
||||
height: 200,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: 5,
|
||||
itemBuilder: (context, index) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(right: 12),
|
||||
width: 140,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
if (_onDeck.isEmpty && _hubs.isEmpty && !_areHubsLoading)
|
||||
SliverFillRemaining(
|
||||
child: Center(
|
||||
child: Column(
|
||||
|
||||
@@ -12,8 +12,29 @@ import 'plex_auth_service.dart';
|
||||
class DataAggregationService {
|
||||
final MultiServerManager _serverManager;
|
||||
|
||||
// Cache for libraries with TTL
|
||||
Map<String, List<PlexLibrary>>? _cachedLibrariesByServer;
|
||||
DateTime? _librariesCacheTime;
|
||||
static const Duration _librariesCacheTTL = Duration(hours: 1);
|
||||
|
||||
DataAggregationService(this._serverManager);
|
||||
|
||||
/// Clear the libraries cache (useful for server changes or logout)
|
||||
void clearCache() {
|
||||
_cachedLibrariesByServer = null;
|
||||
_librariesCacheTime = null;
|
||||
}
|
||||
|
||||
/// Check if libraries cache is still valid
|
||||
bool get _isLibrariesCacheValid {
|
||||
if (_cachedLibrariesByServer == null || _librariesCacheTime == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final cacheAge = DateTime.now().difference(_librariesCacheTime!);
|
||||
return cacheAge < _librariesCacheTTL;
|
||||
}
|
||||
|
||||
/// Fetch libraries from all online servers
|
||||
/// Libraries are automatically tagged with server info by PlexClient
|
||||
Future<List<PlexLibrary>> getLibrariesFromAllServers() async {
|
||||
@@ -53,8 +74,67 @@ class DataAggregationService {
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Fetch recommendation hubs from all servers
|
||||
Future<List<PlexHub>> getHubsFromAllServers({int? limit}) async {
|
||||
/// Fetch libraries from all servers and cache them for hub fetching
|
||||
/// This allows libraries to be fetched in parallel with other operations
|
||||
Future<Map<String, List<PlexLibrary>>> getLibrariesFromAllServersGrouped({
|
||||
bool forceRefresh = false,
|
||||
}) async {
|
||||
// Return cached libraries if still valid and not forcing refresh
|
||||
if (!forceRefresh && _isLibrariesCacheValid) {
|
||||
appLogger.d('Using cached libraries data');
|
||||
return _cachedLibrariesByServer!;
|
||||
}
|
||||
|
||||
final clients = _serverManager.onlineClients;
|
||||
|
||||
if (clients.isEmpty) {
|
||||
appLogger.w('No online servers available for fetching libraries');
|
||||
return {};
|
||||
}
|
||||
|
||||
appLogger.d('Fetching libraries from ${clients.length} servers');
|
||||
|
||||
final libraryFutures = clients.entries.map((entry) async {
|
||||
final serverId = entry.key;
|
||||
final client = entry.value;
|
||||
try {
|
||||
final libraries = await client.getLibraries();
|
||||
appLogger.d(
|
||||
'Fetched ${libraries.length} libraries for server $serverId',
|
||||
);
|
||||
return MapEntry(serverId, libraries);
|
||||
} catch (e) {
|
||||
appLogger.e(
|
||||
'Failed to fetch libraries from server $serverId',
|
||||
error: e,
|
||||
);
|
||||
return MapEntry(serverId, <PlexLibrary>[]);
|
||||
}
|
||||
});
|
||||
|
||||
final libraryResults = await Future.wait(libraryFutures);
|
||||
|
||||
final librariesByServer = Map.fromEntries(libraryResults);
|
||||
final totalLibraries = libraryResults.fold<int>(
|
||||
0,
|
||||
(sum, entry) => sum + entry.value.length,
|
||||
);
|
||||
|
||||
// Cache the results
|
||||
_cachedLibrariesByServer = librariesByServer;
|
||||
_librariesCacheTime = DateTime.now();
|
||||
|
||||
appLogger.d(
|
||||
'Fetched $totalLibraries libraries from ${clients.length} servers',
|
||||
);
|
||||
return librariesByServer;
|
||||
}
|
||||
|
||||
/// Fetch recommendation hubs from all servers using pre-fetched libraries
|
||||
Future<List<PlexHub>> getHubsFromAllServers({
|
||||
int? limit,
|
||||
Map<String, List<PlexLibrary>>? librariesByServer,
|
||||
}) async {
|
||||
final clients = _serverManager.onlineClients;
|
||||
|
||||
if (clients.isEmpty) {
|
||||
@@ -62,21 +142,29 @@ class DataAggregationService {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Use pre-fetched libraries or fetch them if not provided
|
||||
final libraries =
|
||||
librariesByServer ?? await getLibrariesFromAllServersGrouped();
|
||||
|
||||
appLogger.d('Fetching hubs from ${clients.length} servers');
|
||||
|
||||
final allHubs = <PlexHub>[];
|
||||
|
||||
// Fetch from all servers in parallel
|
||||
// Fetch from all servers in parallel using cached libraries
|
||||
final hubFutures = clients.entries.map((entry) async {
|
||||
final serverId = entry.key;
|
||||
final client = entry.value;
|
||||
|
||||
try {
|
||||
// Get libraries for this server
|
||||
final libraries = await client.getLibraries();
|
||||
// Use pre-fetched libraries for this server
|
||||
final serverLibraries = libraries[serverId] ?? <PlexLibrary>[];
|
||||
if (serverLibraries.isEmpty) {
|
||||
appLogger.w('No libraries available for server $serverId');
|
||||
return <PlexHub>[];
|
||||
}
|
||||
|
||||
// Filter to only visible movie/show libraries
|
||||
final visibleLibraries = libraries.where((library) {
|
||||
final visibleLibraries = serverLibraries.where((library) {
|
||||
if (library.type != 'movie' && library.type != 'show') {
|
||||
return false;
|
||||
}
|
||||
@@ -90,7 +178,11 @@ class DataAggregationService {
|
||||
final libraryHubFutures = visibleLibraries.map((library) async {
|
||||
try {
|
||||
// Hubs are now tagged with server info at the source
|
||||
return await client.getLibraryHubs(library.key);
|
||||
final hubs = await client.getLibraryHubs(library.key);
|
||||
appLogger.d(
|
||||
'Fetched ${hubs.length} hubs for ${library.title} on $serverId',
|
||||
);
|
||||
return hubs;
|
||||
} catch (e) {
|
||||
appLogger.w(
|
||||
'Failed to fetch hubs for library ${library.title}: $e',
|
||||
@@ -239,9 +331,14 @@ class DataAggregationService {
|
||||
final serverId = entry.key;
|
||||
final client = entry.value;
|
||||
final server = _serverManager.getServer(serverId);
|
||||
final sw = Stopwatch()..start();
|
||||
|
||||
try {
|
||||
return await operation(serverId, client, server);
|
||||
final result = await operation(serverId, client, server);
|
||||
appLogger.d(
|
||||
'$operationName for server $serverId completed in ${sw.elapsedMilliseconds}ms with ${result.length} items',
|
||||
);
|
||||
return result;
|
||||
} catch (e, stackTrace) {
|
||||
appLogger.e(
|
||||
'Failed $operationName from server $serverId',
|
||||
@@ -249,6 +346,9 @@ class DataAggregationService {
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
_serverManager.updateServerStatus(serverId, false);
|
||||
appLogger.d(
|
||||
'$operationName for server $serverId failed after ${sw.elapsedMilliseconds}ms',
|
||||
);
|
||||
return <T>[];
|
||||
}
|
||||
});
|
||||
|
||||
@@ -80,7 +80,8 @@ class PlaybackInitializationService {
|
||||
externalSubtitles.add(
|
||||
SubtitleTrack.uri(
|
||||
url,
|
||||
title: plexTrack.displayTitle ??
|
||||
title:
|
||||
plexTrack.displayTitle ??
|
||||
plexTrack.language ??
|
||||
'Track ${plexTrack.id}',
|
||||
language: plexTrack.languageCode,
|
||||
|
||||
Reference in New Issue
Block a user