refactor: simplify metadata server tagging
This commit is contained in:
+97
-30
@@ -223,11 +223,15 @@ class PlexClient {
|
||||
}
|
||||
|
||||
/// Extract list of PlexMetadata from response
|
||||
/// Automatically tags all items with this client's serverId and serverName
|
||||
List<PlexMetadata> _extractMetadataList(Response response) {
|
||||
final container = _getMediaContainer(response);
|
||||
if (container != null && container['Metadata'] != null) {
|
||||
return (container['Metadata'] as List)
|
||||
.map((json) => PlexMetadata.fromJson(json))
|
||||
.map((json) => PlexMetadata.fromJson(json).copyWith(
|
||||
serverId: serverId,
|
||||
serverName: serverName,
|
||||
))
|
||||
.toList();
|
||||
}
|
||||
return [];
|
||||
@@ -245,9 +249,15 @@ class PlexClient {
|
||||
}
|
||||
|
||||
/// Extract single PlexMetadata from response (returns first item or null)
|
||||
/// Automatically tags the item with this client's serverId and serverName
|
||||
PlexMetadata? _extractSingleMetadata(Response response) {
|
||||
final metadataJson = _getFirstMetadataJson(response);
|
||||
return metadataJson != null ? PlexMetadata.fromJson(metadataJson) : null;
|
||||
return metadataJson != null
|
||||
? PlexMetadata.fromJson(metadataJson).copyWith(
|
||||
serverId: serverId,
|
||||
serverName: serverName,
|
||||
)
|
||||
: null;
|
||||
}
|
||||
|
||||
/// Generic helper to extract and map Directory list from response
|
||||
@@ -264,6 +274,30 @@ class PlexClient {
|
||||
return [];
|
||||
}
|
||||
|
||||
/// Extract PlexLibrary list from response with auto-tagging
|
||||
List<PlexLibrary> _extractLibraryList(Response response) {
|
||||
final container = _getMediaContainer(response);
|
||||
if (container != null && container['Directory'] != null) {
|
||||
return (container['Directory'] as List)
|
||||
.map((json) => PlexLibrary.fromJson(json as Map<String, dynamic>)
|
||||
.copyWith(serverId: serverId, serverName: serverName))
|
||||
.toList();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/// Extract PlexPlaylist list from response with auto-tagging
|
||||
List<PlexPlaylist> _extractPlaylistList(Response response) {
|
||||
final container = _getMediaContainer(response);
|
||||
if (container != null && container['Metadata'] != null) {
|
||||
return (container['Metadata'] as List)
|
||||
.map((json) => PlexPlaylist.fromJson(json as Map<String, dynamic>)
|
||||
.copyWith(serverId: serverId, serverName: serverName))
|
||||
.toList();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// API Methods
|
||||
// ============================================================================
|
||||
@@ -275,9 +309,10 @@ class PlexClient {
|
||||
}
|
||||
|
||||
/// Get library sections
|
||||
/// Returns libraries automatically tagged with this client's serverId and serverName
|
||||
Future<List<PlexLibrary>> getLibraries() async {
|
||||
final response = await _dio.get('/library/sections');
|
||||
return _extractDirectoryList(response, PlexLibrary.fromJson);
|
||||
return _extractLibraryList(response);
|
||||
}
|
||||
|
||||
/// Get library content by section ID
|
||||
@@ -352,7 +387,10 @@ class PlexClient {
|
||||
|
||||
// Log the parsed metadata JSON
|
||||
if (metadataJson != null) {
|
||||
metadata = PlexMetadata.fromJsonWithImages(metadataJson);
|
||||
metadata = PlexMetadata.fromJsonWithImages(metadataJson).copyWith(
|
||||
serverId: serverId,
|
||||
serverName: serverName,
|
||||
);
|
||||
|
||||
// Check if OnDeck is nested inside Metadata
|
||||
if (metadataJson.containsKey('OnDeck') &&
|
||||
@@ -363,7 +401,10 @@ class PlexClient {
|
||||
if (onDeckData is Map && onDeckData.containsKey('Metadata')) {
|
||||
final onDeckMetadata = onDeckData['Metadata'];
|
||||
if (onDeckMetadata != null) {
|
||||
onDeckEpisode = PlexMetadata.fromJson(onDeckMetadata);
|
||||
onDeckEpisode = PlexMetadata.fromJson(onDeckMetadata).copyWith(
|
||||
serverId: serverId,
|
||||
serverName: serverName,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -377,7 +418,10 @@ class PlexClient {
|
||||
final response = await _dio.get('/library/metadata/$ratingKey');
|
||||
final metadataJson = _getFirstMetadataJson(response);
|
||||
return metadataJson != null
|
||||
? PlexMetadata.fromJsonWithImages(metadataJson)
|
||||
? PlexMetadata.fromJsonWithImages(metadataJson).copyWith(
|
||||
serverId: serverId,
|
||||
serverName: serverName,
|
||||
)
|
||||
: null;
|
||||
}
|
||||
|
||||
@@ -445,7 +489,10 @@ class PlexClient {
|
||||
if (hub['Metadata'] != null) {
|
||||
for (final json in hub['Metadata'] as List) {
|
||||
try {
|
||||
results.add(PlexMetadata.fromJson(json));
|
||||
results.add(PlexMetadata.fromJson(json).copyWith(
|
||||
serverId: serverId,
|
||||
serverName: serverName,
|
||||
));
|
||||
} catch (e) {
|
||||
// Skip items that fail to parse
|
||||
appLogger.w('Failed to parse search result', error: e);
|
||||
@@ -456,7 +503,10 @@ class PlexClient {
|
||||
if (hub['Directory'] != null) {
|
||||
for (final json in hub['Directory'] as List) {
|
||||
try {
|
||||
results.add(PlexMetadata.fromJson(json));
|
||||
results.add(PlexMetadata.fromJson(json).copyWith(
|
||||
serverId: serverId,
|
||||
serverName: serverName,
|
||||
));
|
||||
} catch (e) {
|
||||
// Skip items that fail to parse
|
||||
appLogger.w('Failed to parse search result', error: e);
|
||||
@@ -492,7 +542,10 @@ class PlexClient {
|
||||
final container = _getMediaContainer(response);
|
||||
if (container != null && container['Metadata'] != null) {
|
||||
final allItems = (container['Metadata'] as List)
|
||||
.map((json) => PlexMetadata.fromJsonWithImages(json))
|
||||
.map((json) => PlexMetadata.fromJsonWithImages(json).copyWith(
|
||||
serverId: serverId,
|
||||
serverName: serverName,
|
||||
))
|
||||
.toList();
|
||||
|
||||
// Filter out music content (artists, albums, tracks)
|
||||
@@ -1130,11 +1183,17 @@ class PlexClient {
|
||||
final hub = PlexHub.fromJson(hubJson);
|
||||
// Only include hubs that have items and are movie/show content
|
||||
if (hub.items.isNotEmpty) {
|
||||
// Filter out non-video content types
|
||||
final videoItems = hub.items.where((item) {
|
||||
// Filter out non-video content types and tag with server info
|
||||
final videoItems = hub.items
|
||||
.where((item) {
|
||||
final type = item.type.toLowerCase();
|
||||
return type == 'movie' || type == 'show';
|
||||
}).toList();
|
||||
})
|
||||
.map((item) => item.copyWith(
|
||||
serverId: serverId,
|
||||
serverName: serverName,
|
||||
))
|
||||
.toList();
|
||||
|
||||
if (videoItems.isNotEmpty) {
|
||||
hubs.add(
|
||||
@@ -1146,6 +1205,8 @@ class PlexClient {
|
||||
size: hub.size,
|
||||
more: hub.more,
|
||||
items: videoItems,
|
||||
serverId: serverId,
|
||||
serverName: serverName,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1209,21 +1270,8 @@ class PlexClient {
|
||||
'/playlists',
|
||||
queryParameters: queryParams,
|
||||
);
|
||||
final container = _getMediaContainer(response);
|
||||
|
||||
if (container == null || container['Metadata'] == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final List<dynamic> metadata = container['Metadata'] as List;
|
||||
|
||||
if (metadata.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return metadata
|
||||
.map((item) => PlexPlaylist.fromJson(item as Map<String, dynamic>))
|
||||
.toList();
|
||||
return _extractPlaylistList(response);
|
||||
} catch (e) {
|
||||
appLogger.e('Failed to get playlists: $e');
|
||||
return [];
|
||||
@@ -1596,7 +1644,11 @@ class PlexClient {
|
||||
queryParameters: queryParams,
|
||||
);
|
||||
|
||||
return PlayQueueResponse.fromJson(response.data);
|
||||
return PlayQueueResponse.fromJson(
|
||||
response.data,
|
||||
serverId: serverId,
|
||||
serverName: serverName,
|
||||
);
|
||||
} catch (e) {
|
||||
appLogger.e('Failed to create play queue', error: e);
|
||||
return null;
|
||||
@@ -1628,7 +1680,11 @@ class PlexClient {
|
||||
queryParameters: queryParams,
|
||||
);
|
||||
|
||||
return PlayQueueResponse.fromJson(response.data);
|
||||
return PlayQueueResponse.fromJson(
|
||||
response.data,
|
||||
serverId: serverId,
|
||||
serverName: serverName,
|
||||
);
|
||||
} catch (e) {
|
||||
appLogger.e('Failed to get play queue: $e');
|
||||
return null;
|
||||
@@ -1703,6 +1759,7 @@ class PlexClient {
|
||||
|
||||
/// Extract both Metadata and Directory entries from response
|
||||
/// Folders can come back as either type
|
||||
/// Automatically tags all items with this client's serverId and serverName
|
||||
List<PlexMetadata> _extractMetadataAndDirectories(Response response) {
|
||||
final List<PlexMetadata> items = [];
|
||||
final container = _getMediaContainer(response);
|
||||
@@ -1713,7 +1770,10 @@ class PlexClient {
|
||||
for (final json in container['Metadata'] as List) {
|
||||
try {
|
||||
// Try to parse with full PlexMetadata.fromJson first
|
||||
items.add(PlexMetadata.fromJson(json));
|
||||
items.add(PlexMetadata.fromJson(json).copyWith(
|
||||
serverId: serverId,
|
||||
serverName: serverName,
|
||||
));
|
||||
} catch (e) {
|
||||
// If full parsing fails, use minimal safe parsing
|
||||
appLogger.d('Using minimal parsing for metadata item: $e');
|
||||
@@ -1727,6 +1787,8 @@ class PlexClient {
|
||||
thumb: json['thumb'],
|
||||
art: json['art'],
|
||||
year: json['year'],
|
||||
serverId: serverId,
|
||||
serverName: serverName,
|
||||
),
|
||||
);
|
||||
} catch (e2) {
|
||||
@@ -1741,7 +1803,10 @@ class PlexClient {
|
||||
for (final json in container['Directory'] as List) {
|
||||
try {
|
||||
// Try to parse as PlexMetadata first
|
||||
items.add(PlexMetadata.fromJson(json));
|
||||
items.add(PlexMetadata.fromJson(json).copyWith(
|
||||
serverId: serverId,
|
||||
serverName: serverName,
|
||||
));
|
||||
} catch (e) {
|
||||
// If that fails, use minimal folder representation
|
||||
try {
|
||||
@@ -1753,6 +1818,8 @@ class PlexClient {
|
||||
title: json['title'] ?? 'Untitled',
|
||||
thumb: json['thumb'],
|
||||
art: json['art'],
|
||||
serverId: serverId,
|
||||
serverName: serverName,
|
||||
),
|
||||
);
|
||||
} catch (e2) {
|
||||
|
||||
@@ -47,4 +47,35 @@ class PlexLibrary with MultiServerFields {
|
||||
_$PlexLibraryFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$PlexLibraryToJson(this);
|
||||
|
||||
/// Create a copy of this library with optional field overrides
|
||||
PlexLibrary copyWith({
|
||||
String? key,
|
||||
String? title,
|
||||
String? type,
|
||||
String? agent,
|
||||
String? scanner,
|
||||
String? language,
|
||||
String? uuid,
|
||||
int? updatedAt,
|
||||
int? createdAt,
|
||||
int? hidden,
|
||||
String? serverId,
|
||||
String? serverName,
|
||||
}) {
|
||||
return PlexLibrary(
|
||||
key: key ?? this.key,
|
||||
title: title ?? this.title,
|
||||
type: type ?? this.type,
|
||||
agent: agent ?? this.agent,
|
||||
scanner: scanner ?? this.scanner,
|
||||
language: language ?? this.language,
|
||||
uuid: uuid ?? this.uuid,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
hidden: hidden ?? this.hidden,
|
||||
serverId: serverId ?? this.serverId,
|
||||
serverName: serverName ?? this.serverName,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,10 +105,8 @@ class PlaybackStateProvider with ChangeNotifier {
|
||||
_playQueueShuffled = playQueue.playQueueShuffled;
|
||||
_currentPlayQueueItemID = playQueue.playQueueSelectedItemID;
|
||||
|
||||
// Preserve serverId/serverName on all items
|
||||
_loadedItems = (playQueue.items ?? []).map((item) {
|
||||
return item.copyWith(serverId: serverId, serverName: serverName);
|
||||
}).toList();
|
||||
// Items are already tagged with server info by PlexClient
|
||||
_loadedItems = playQueue.items ?? [];
|
||||
|
||||
_contextKey = contextKey;
|
||||
_playbackMode = PlaybackMode.playQueue;
|
||||
@@ -136,17 +134,8 @@ class PlaybackStateProvider with ChangeNotifier {
|
||||
);
|
||||
|
||||
if (response != null && response.items != null) {
|
||||
// Preserve serverId from existing items
|
||||
final serverId = _loadedItems.isNotEmpty
|
||||
? _loadedItems.first.serverId
|
||||
: null;
|
||||
final serverName = _loadedItems.isNotEmpty
|
||||
? _loadedItems.first.serverName
|
||||
: null;
|
||||
|
||||
_loadedItems = response.items!.map((item) {
|
||||
return item.copyWith(serverId: serverId, serverName: serverName);
|
||||
}).toList();
|
||||
// Items are already tagged with server info by PlexClient
|
||||
_loadedItems = response.items!;
|
||||
// Use size or items length as fallback if totalCount is null
|
||||
_playQueueTotalCount =
|
||||
response.playQueueTotalCount ??
|
||||
@@ -238,25 +227,15 @@ class PlaybackStateProvider with ChangeNotifier {
|
||||
if (response != null &&
|
||||
response.items != null &&
|
||||
response.items!.isNotEmpty) {
|
||||
// Preserve serverId when looping
|
||||
final serverId = _loadedItems.isNotEmpty
|
||||
? _loadedItems.first.serverId
|
||||
: null;
|
||||
final serverName = _loadedItems.isNotEmpty
|
||||
? _loadedItems.first.serverName
|
||||
: null;
|
||||
|
||||
_loadedItems = response.items!.map((item) {
|
||||
return item.copyWith(serverId: serverId, serverName: serverName);
|
||||
}).toList();
|
||||
// Items are already tagged with server info by PlexClient
|
||||
_loadedItems = response.items!;
|
||||
final firstItem = _loadedItems.first;
|
||||
// Don't update _currentPlayQueueItemID here - let setCurrentItem do it when playback starts
|
||||
return firstItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Queue has ended - clear it and let sequential playback take over
|
||||
clearShuffle();
|
||||
// At end of queue - return null but keep queue active so user can still go back
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../client/plex_client.dart';
|
||||
import '../models/plex_metadata.dart';
|
||||
import '../providers/multi_server_provider.dart';
|
||||
import '../utils/provider_extensions.dart';
|
||||
import '../utils/collection_playlist_play_helper.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
@@ -51,6 +53,18 @@ abstract class BaseMediaListDetailScreen<T extends StatefulWidget>
|
||||
}
|
||||
}
|
||||
|
||||
// If serverId is null, fall back to first available server
|
||||
if (serverId == null) {
|
||||
final multiServerProvider = Provider.of<MultiServerProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
if (!multiServerProvider.hasConnectedServers) {
|
||||
throw Exception(t.errors.noClientAvailable);
|
||||
}
|
||||
serverId = multiServerProvider.onlineServerIds.first;
|
||||
}
|
||||
|
||||
return context.getClientForServer(serverId);
|
||||
}
|
||||
|
||||
@@ -231,21 +245,12 @@ mixin StandardItemLoader<T extends StatefulWidget>
|
||||
}
|
||||
|
||||
try {
|
||||
// Items are automatically tagged with server info by PlexClient
|
||||
final newItems = await fetchItems();
|
||||
|
||||
// Tag items with server info for correct client resolution
|
||||
final serverId = (mediaItem as dynamic).serverId as String?;
|
||||
final serverName = (mediaItem as dynamic).serverName as String?;
|
||||
|
||||
final taggedItems = newItems
|
||||
.map(
|
||||
(item) => item.copyWith(serverId: serverId, serverName: serverName),
|
||||
)
|
||||
.toList();
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
items = taggedItems;
|
||||
items = newItems;
|
||||
isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -37,7 +37,18 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
static const Duration _heroAutoScrollDuration = Duration(seconds: 8);
|
||||
|
||||
@override
|
||||
PlexClient get client => _getClientForItem(null);
|
||||
PlexClient get client {
|
||||
final multiServerProvider = Provider.of<MultiServerProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
if (!multiServerProvider.hasConnectedServers) {
|
||||
throw Exception('No servers available');
|
||||
}
|
||||
return context.getClientForServer(
|
||||
multiServerProvider.onlineServerIds.first,
|
||||
);
|
||||
}
|
||||
|
||||
List<PlexMetadata> _onDeck = [];
|
||||
List<PlexHub> _hubs = [];
|
||||
@@ -52,7 +63,21 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
|
||||
/// Get the correct PlexClient for an item's server
|
||||
PlexClient _getClientForItem(PlexMetadata? item) {
|
||||
return context.getClientForServer(item?.serverId);
|
||||
// Items should always have a serverId, but if not, fall back to first available server
|
||||
final serverId = item?.serverId;
|
||||
if (serverId == null) {
|
||||
final multiServerProvider = Provider.of<MultiServerProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
if (!multiServerProvider.hasConnectedServers) {
|
||||
throw Exception('No servers available');
|
||||
}
|
||||
return context.getClientForServer(
|
||||
multiServerProvider.onlineServerIds.first,
|
||||
);
|
||||
}
|
||||
return context.getClientForServer(serverId);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -37,7 +37,7 @@ class _HubDetailScreenState extends State<HubDetailScreen> with Refreshable {
|
||||
|
||||
/// Get the correct PlexClient for this hub's server
|
||||
PlexClient _getClientForHub() {
|
||||
return context.getClientForServer(widget.hub.serverId);
|
||||
return context.getClientForServer(widget.hub.serverId!);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -33,7 +33,18 @@ class LibrariesScreen extends StatefulWidget {
|
||||
class _LibrariesScreenState extends State<LibrariesScreen>
|
||||
with Refreshable, ItemUpdatable, SingleTickerProviderStateMixin {
|
||||
@override
|
||||
PlexClient get client => context.getClientForServer(null);
|
||||
PlexClient get client {
|
||||
final multiServerProvider = Provider.of<MultiServerProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
if (!multiServerProvider.hasConnectedServers) {
|
||||
throw Exception(t.errors.noClientAvailable);
|
||||
}
|
||||
return context.getClientForServer(
|
||||
multiServerProvider.onlineServerIds.first,
|
||||
);
|
||||
}
|
||||
|
||||
late TabController _tabController;
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import '../../models/plex_sort.dart';
|
||||
import '../../providers/settings_provider.dart';
|
||||
import '../../utils/error_message_utils.dart';
|
||||
import '../../utils/grid_size_calculator.dart';
|
||||
import '../../utils/server_tagging_extensions.dart';
|
||||
import '../../widgets/media_card.dart';
|
||||
import '../../widgets/folder_tree_view.dart';
|
||||
import '../../widgets/filters_bottom_sheet.dart';
|
||||
@@ -219,6 +218,7 @@ class _LibraryBrowseTabState extends State<LibraryBrowseTab>
|
||||
);
|
||||
}
|
||||
|
||||
// Items are automatically tagged with server info by PlexClient
|
||||
final items = await client.getLibraryContent(
|
||||
widget.library.key,
|
||||
start: _currentPage * _pageSize,
|
||||
@@ -227,18 +227,15 @@ class _LibraryBrowseTabState extends State<LibraryBrowseTab>
|
||||
cancelToken: _cancelToken,
|
||||
);
|
||||
|
||||
// Tag items with server info for multi-server support
|
||||
final taggedItems = items.tagWithLibrary(widget.library);
|
||||
|
||||
if (currentRequestId != _requestId) return;
|
||||
|
||||
setState(() {
|
||||
if (loadMore) {
|
||||
_items.addAll(taggedItems);
|
||||
_items.addAll(items);
|
||||
} else {
|
||||
_items = taggedItems;
|
||||
_items = items;
|
||||
}
|
||||
_hasMoreItems = taggedItems.length >= _pageSize;
|
||||
_hasMoreItems = items.length >= _pageSize;
|
||||
_currentPage++;
|
||||
_isLoading = false;
|
||||
});
|
||||
@@ -357,6 +354,7 @@ class _LibraryBrowseTabState extends State<LibraryBrowseTab>
|
||||
builder: (context) => FiltersBottomSheet(
|
||||
filters: _filters,
|
||||
selectedFilters: _selectedFilters,
|
||||
serverId: widget.library.serverId!,
|
||||
onFiltersChanged: (filters) async {
|
||||
setState(() {
|
||||
_selectedFilters.clear();
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../models/plex_metadata.dart';
|
||||
import '../../utils/library_refresh_notifier.dart';
|
||||
import '../../utils/server_tagging_extensions.dart';
|
||||
import '../../i18n/strings.g.dart';
|
||||
import '../../widgets/adaptive_media_grid.dart';
|
||||
import 'base_library_tab.dart';
|
||||
@@ -40,10 +39,8 @@ class _LibraryCollectionsTabState
|
||||
// Use server-specific client for this library
|
||||
final client = getClientForLibrary();
|
||||
|
||||
final collections = await client.getLibraryCollections(widget.library.key);
|
||||
|
||||
// Tag collections with server info
|
||||
return collections.tagWithLibrary(widget.library);
|
||||
// Collections are automatically tagged with server info by PlexClient
|
||||
return await client.getLibraryCollections(widget.library.key);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -5,7 +5,6 @@ import '../../providers/settings_provider.dart';
|
||||
import '../../utils/library_refresh_notifier.dart';
|
||||
import '../../services/settings_service.dart' show ViewMode;
|
||||
import '../../utils/grid_size_calculator.dart';
|
||||
import '../../utils/server_tagging_extensions.dart';
|
||||
import '../../widgets/media_card.dart';
|
||||
import '../../i18n/strings.g.dart';
|
||||
import 'base_library_tab.dart';
|
||||
@@ -43,14 +42,11 @@ class _LibraryPlaylistsTabState
|
||||
// Use server-specific client for this library
|
||||
final client = getClientForLibrary();
|
||||
|
||||
// Get playlists for this library
|
||||
final playlists = await client.getLibraryPlaylists(
|
||||
// Playlists are automatically tagged with server info by PlexClient
|
||||
return await client.getLibraryPlaylists(
|
||||
sectionId: widget.library.key,
|
||||
playlistType: 'video',
|
||||
);
|
||||
|
||||
// Tag playlists with server info
|
||||
return playlists.tagWithLibrary(widget.library);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -51,7 +51,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> {
|
||||
|
||||
/// Get the correct PlexClient for this metadata's server
|
||||
PlexClient _getClientForMetadata(BuildContext context) {
|
||||
return context.getClientForServer(widget.metadata.serverId);
|
||||
return context.getClientForServer(widget.metadata.serverId!);
|
||||
}
|
||||
|
||||
Future<void> _loadFullMetadata() async {
|
||||
|
||||
@@ -53,7 +53,7 @@ class _PlaylistDetailScreenState
|
||||
|
||||
/// Get the correct PlexClient for this playlist's server
|
||||
PlexClient _getClientForPlaylist() {
|
||||
return context.getClientForServer(widget.playlist.serverId);
|
||||
return context.getClientForServer(widget.playlist.serverId!);
|
||||
}
|
||||
|
||||
Future<void> _deletePlaylist() async {
|
||||
|
||||
@@ -33,7 +33,7 @@ class _SeasonDetailScreenState extends State<SeasonDetailScreen>
|
||||
|
||||
/// Get the correct PlexClient for this season's server
|
||||
PlexClient _getClientForSeason(BuildContext context) {
|
||||
return context.getClientForServer(widget.season.serverId);
|
||||
return context.getClientForServer(widget.season.serverId!);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -52,8 +52,9 @@ class _SeasonDetailScreenState extends State<SeasonDetailScreen>
|
||||
});
|
||||
|
||||
try {
|
||||
// Episodes are now tagged with server info at the source
|
||||
// Episodes are automatically tagged with server info by PlexClient
|
||||
final episodes = await _client.getChildren(widget.season.ratingKey);
|
||||
|
||||
setState(() {
|
||||
_episodes = episodes;
|
||||
_isLoadingEpisodes = false;
|
||||
|
||||
@@ -79,7 +79,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen>
|
||||
|
||||
/// Get the correct PlexClient for this metadata's server
|
||||
PlexClient _getClientForMetadata(BuildContext context) {
|
||||
return context.getClientForServer(widget.metadata.serverId);
|
||||
return context.getClientForServer(widget.metadata.serverId!);
|
||||
}
|
||||
|
||||
final ValueNotifier<bool> _isBuffering = ValueNotifier<bool>(
|
||||
@@ -462,14 +462,15 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen>
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if there's already an active queue for this show
|
||||
// Check if there's already an active queue
|
||||
final existingContextKey = playbackState.shuffleContextKey;
|
||||
final isQueueActive = playbackState.isQueueActive;
|
||||
|
||||
if (isQueueActive && existingContextKey == showRatingKey) {
|
||||
// Queue already exists for this show, just update the current item
|
||||
if (isQueueActive) {
|
||||
// A queue already exists (could be shuffle, playlist, or sequential)
|
||||
// Just update the current item, don't create a new queue
|
||||
playbackState.setCurrentItem(widget.metadata);
|
||||
appLogger.d('Using existing play queue for show $showRatingKey');
|
||||
appLogger.d('Using existing play queue (context: $existingContextKey)');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,43 +14,24 @@ class DataAggregationService {
|
||||
|
||||
DataAggregationService(this._serverManager);
|
||||
|
||||
/// Fetch libraries from all online servers and tag them with server info
|
||||
/// Fetch libraries from all online servers
|
||||
/// Libraries are automatically tagged with server info by PlexClient
|
||||
Future<List<PlexLibrary>> getLibrariesFromAllServers() async {
|
||||
return _perServer<PlexLibrary>(
|
||||
operationName: 'fetching libraries',
|
||||
operation: (serverId, client, server) async {
|
||||
final libraries = await client.getLibraries();
|
||||
|
||||
// Tag each library with server info
|
||||
return libraries.map((lib) {
|
||||
return PlexLibrary(
|
||||
key: lib.key,
|
||||
title: lib.title,
|
||||
type: lib.type,
|
||||
agent: lib.agent,
|
||||
scanner: lib.scanner,
|
||||
language: lib.language,
|
||||
uuid: lib.uuid,
|
||||
updatedAt: lib.updatedAt,
|
||||
createdAt: lib.createdAt,
|
||||
hidden: lib.hidden,
|
||||
);
|
||||
}).toList();
|
||||
return await client.getLibraries();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Fetch "On Deck" (Continue Watching) from all servers and merge by recency
|
||||
/// Items are automatically tagged with server info by PlexClient
|
||||
Future<List<PlexMetadata>> getOnDeckFromAllServers({int? limit}) async {
|
||||
final allOnDeck = await _perServer<PlexMetadata>(
|
||||
operationName: 'fetching on deck',
|
||||
operation: (serverId, client, server) async {
|
||||
final items = await client.getOnDeck();
|
||||
|
||||
// Tag each item with server info
|
||||
return items.map((item) {
|
||||
return item.copyWith(serverId: serverId, serverName: server?.name);
|
||||
}).toList();
|
||||
return await client.getOnDeck();
|
||||
},
|
||||
);
|
||||
|
||||
@@ -156,6 +137,7 @@ class DataAggregationService {
|
||||
}
|
||||
|
||||
/// Search across all online servers
|
||||
/// Results are automatically tagged with server info by PlexClient
|
||||
Future<List<PlexMetadata>> searchAcrossServers(
|
||||
String query, {
|
||||
int? limit,
|
||||
@@ -167,12 +149,7 @@ class DataAggregationService {
|
||||
final allResults = await _perServer<PlexMetadata>(
|
||||
operationName: 'searching for "$query"',
|
||||
operation: (serverId, client, server) async {
|
||||
final results = await client.search(query);
|
||||
|
||||
// Tag each result with server info
|
||||
return results.map((item) {
|
||||
return item.copyWith(serverId: serverId, serverName: server?.name);
|
||||
}).toList();
|
||||
return await client.search(query);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -196,23 +173,8 @@ class DataAggregationService {
|
||||
}
|
||||
|
||||
try {
|
||||
final libraries = await client.getLibraries();
|
||||
|
||||
// Tag with server info
|
||||
return libraries.map((lib) {
|
||||
return PlexLibrary(
|
||||
key: lib.key,
|
||||
title: lib.title,
|
||||
type: lib.type,
|
||||
agent: lib.agent,
|
||||
scanner: lib.scanner,
|
||||
language: lib.language,
|
||||
uuid: lib.uuid,
|
||||
updatedAt: lib.updatedAt,
|
||||
createdAt: lib.createdAt,
|
||||
hidden: lib.hidden,
|
||||
);
|
||||
}).toList();
|
||||
// Libraries are automatically tagged with server info by PlexClient
|
||||
return await client.getLibraries();
|
||||
} catch (e, stackTrace) {
|
||||
appLogger.e(
|
||||
'Failed to fetch libraries for server $serverId',
|
||||
|
||||
@@ -30,13 +30,17 @@ Future<void> playCollectionOrPlaylist({
|
||||
|
||||
final PlayQueueResponse? playQueue;
|
||||
if (isCollection) {
|
||||
// Validate that machine identifier is available
|
||||
if (client.config.machineIdentifier == null) {
|
||||
throw Exception('Machine identifier is required to play collections');
|
||||
// Get machine identifier (fetch if not cached in config)
|
||||
final machineId =
|
||||
client.config.machineIdentifier ??
|
||||
await client.getMachineIdentifier();
|
||||
|
||||
if (machineId == null) {
|
||||
throw Exception('Could not get server machine identifier');
|
||||
}
|
||||
|
||||
final collectionUri =
|
||||
'server://${client.config.machineIdentifier}/com.plexapp.plugins.library/library/collections/${item.ratingKey}';
|
||||
'server://$machineId/com.plexapp.plugins.library/library/collections/${item.ratingKey}';
|
||||
playQueue = await client.createPlayQueue(
|
||||
uri: collectionUri,
|
||||
type: 'video',
|
||||
@@ -61,30 +65,12 @@ Future<void> playCollectionOrPlaylist({
|
||||
fetchedQueue.items!.isNotEmpty) {
|
||||
if (!context.mounted) return;
|
||||
|
||||
// Preserve serverId for all items in the queue
|
||||
final itemsWithServerId = fetchedQueue.items!.map((queueItem) {
|
||||
return queueItem.copyWith(serverId: serverId, serverName: serverName);
|
||||
}).toList();
|
||||
|
||||
final queueWithServerId = PlayQueueResponse(
|
||||
playQueueID: fetchedQueue.playQueueID,
|
||||
playQueueSelectedItemID: fetchedQueue.playQueueSelectedItemID,
|
||||
playQueueSelectedItemOffset: fetchedQueue.playQueueSelectedItemOffset,
|
||||
playQueueSelectedMetadataItemID:
|
||||
fetchedQueue.playQueueSelectedMetadataItemID,
|
||||
playQueueShuffled: fetchedQueue.playQueueShuffled,
|
||||
playQueueSourceURI: fetchedQueue.playQueueSourceURI,
|
||||
playQueueTotalCount: fetchedQueue.playQueueTotalCount,
|
||||
playQueueVersion: fetchedQueue.playQueueVersion,
|
||||
size: fetchedQueue.size,
|
||||
items: itemsWithServerId,
|
||||
);
|
||||
|
||||
// Items are automatically tagged with server info by PlexClient
|
||||
// Set play queue in provider
|
||||
final playbackState = context.read<PlaybackStateProvider>();
|
||||
playbackState.setClient(client);
|
||||
await playbackState.setPlaybackFromPlayQueue(
|
||||
queueWithServerId,
|
||||
fetchedQueue,
|
||||
ratingKey,
|
||||
serverId: serverId,
|
||||
serverName: serverName,
|
||||
@@ -93,7 +79,7 @@ Future<void> playCollectionOrPlaylist({
|
||||
if (!context.mounted) return;
|
||||
|
||||
// Navigate to first item
|
||||
await navigateToVideoPlayer(context, metadata: itemsWithServerId.first);
|
||||
await navigateToVideoPlayer(context, metadata: fetchedQueue.items!.first);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -111,30 +97,12 @@ Future<void> playCollectionOrPlaylist({
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
// Preserve serverId for all items in the queue
|
||||
final itemsWithServerId = playQueue.items!.map((queueItem) {
|
||||
return queueItem.copyWith(serverId: serverId, serverName: serverName);
|
||||
}).toList();
|
||||
|
||||
final queueWithServerId = PlayQueueResponse(
|
||||
playQueueID: playQueue.playQueueID,
|
||||
playQueueSelectedItemID: playQueue.playQueueSelectedItemID,
|
||||
playQueueSelectedItemOffset: playQueue.playQueueSelectedItemOffset,
|
||||
playQueueSelectedMetadataItemID:
|
||||
playQueue.playQueueSelectedMetadataItemID,
|
||||
playQueueShuffled: playQueue.playQueueShuffled,
|
||||
playQueueSourceURI: playQueue.playQueueSourceURI,
|
||||
playQueueTotalCount: playQueue.playQueueTotalCount,
|
||||
playQueueVersion: playQueue.playQueueVersion,
|
||||
size: playQueue.size,
|
||||
items: itemsWithServerId,
|
||||
);
|
||||
|
||||
// Items are automatically tagged with server info by PlexClient
|
||||
// Set play queue in provider
|
||||
final playbackState = context.read<PlaybackStateProvider>();
|
||||
playbackState.setClient(client);
|
||||
await playbackState.setPlaybackFromPlayQueue(
|
||||
queueWithServerId,
|
||||
playQueue,
|
||||
ratingKey,
|
||||
serverId: serverId,
|
||||
serverName: serverName,
|
||||
@@ -143,7 +111,7 @@ Future<void> playCollectionOrPlaylist({
|
||||
if (!context.mounted) return;
|
||||
|
||||
// Navigate to first item
|
||||
await navigateToVideoPlayer(context, metadata: itemsWithServerId.first);
|
||||
await navigateToVideoPlayer(context, metadata: playQueue.items!.first);
|
||||
} catch (e) {
|
||||
appLogger.e('Failed to ${shuffle ? "shuffle play" : "play"}', error: e);
|
||||
if (context.mounted) {
|
||||
|
||||
@@ -33,31 +33,13 @@ extension ProviderExtensions on BuildContext {
|
||||
PlexUserProfile? get profileSettings => userProfile.profileSettings;
|
||||
|
||||
/// Get PlexClient for a specific server ID
|
||||
/// If serverId is null, returns the first available online client
|
||||
/// Throws an exception if no client is available
|
||||
PlexClient getClientForServer(String? serverId) {
|
||||
/// Throws an exception if no client is available for the given serverId
|
||||
PlexClient getClientForServer(String serverId) {
|
||||
final multiServerProvider = Provider.of<MultiServerProvider>(
|
||||
this,
|
||||
listen: false,
|
||||
);
|
||||
|
||||
if (serverId == null) {
|
||||
// No serverId specified - try to get first online server
|
||||
appLogger.w('No serverId provided, using first available online server');
|
||||
|
||||
if (!multiServerProvider.hasConnectedServers) {
|
||||
throw Exception(t.errors.noClientAvailable);
|
||||
}
|
||||
|
||||
final firstServerId = multiServerProvider.onlineServerIds.first;
|
||||
final client = multiServerProvider.getClientForServer(firstServerId);
|
||||
if (client == null) {
|
||||
throw Exception(t.errors.noClientAvailable);
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
final serverClient = multiServerProvider.getClientForServer(serverId);
|
||||
|
||||
if (serverClient == null) {
|
||||
@@ -71,6 +53,17 @@ extension ProviderExtensions on BuildContext {
|
||||
/// Get PlexClient for a library
|
||||
/// Throws an exception if no client is available
|
||||
PlexClient getClientForLibrary(PlexLibrary library) {
|
||||
return getClientForServer(library.serverId);
|
||||
// If library doesn't have a serverId, fall back to first available server
|
||||
if (library.serverId == null) {
|
||||
final multiServerProvider = Provider.of<MultiServerProvider>(
|
||||
this,
|
||||
listen: false,
|
||||
);
|
||||
if (!multiServerProvider.hasConnectedServers) {
|
||||
throw Exception(t.errors.noClientAvailable);
|
||||
}
|
||||
return getClientForServer(multiServerProvider.onlineServerIds.first);
|
||||
}
|
||||
return getClientForServer(library.serverId!);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import '../models/plex_metadata.dart';
|
||||
import '../models/plex_playlist.dart';
|
||||
import '../models/plex_library.dart';
|
||||
|
||||
/// Extension methods for tagging items with server information
|
||||
/// Used for multi-server support to track which server each item belongs to
|
||||
|
||||
extension PlexMetadataServerTagging on Iterable<PlexMetadata> {
|
||||
/// Tags all items in the collection with the given server ID and name
|
||||
/// Returns a new list with all items updated
|
||||
List<PlexMetadata> tagWithServer(String? serverId, String? serverName) {
|
||||
return map(
|
||||
(item) => item.copyWith(serverId: serverId, serverName: serverName),
|
||||
).toList();
|
||||
}
|
||||
|
||||
/// Tags all items in the collection with server info from a library
|
||||
/// Returns a new list with all items updated
|
||||
List<PlexMetadata> tagWithLibrary(PlexLibrary library) {
|
||||
return tagWithServer(library.serverId, library.serverName);
|
||||
}
|
||||
}
|
||||
|
||||
extension PlexPlaylistServerTagging on Iterable<PlexPlaylist> {
|
||||
/// Tags all playlists in the collection with the given server ID and name
|
||||
/// Returns a new list with all playlists updated
|
||||
List<PlexPlaylist> tagWithServer(String? serverId, String? serverName) {
|
||||
return map(
|
||||
(playlist) =>
|
||||
playlist.copyWith(serverId: serverId, serverName: serverName),
|
||||
).toList();
|
||||
}
|
||||
|
||||
/// Tags all playlists in the collection with server info from a library
|
||||
/// Returns a new list with all playlists updated
|
||||
List<PlexPlaylist> tagWithLibrary(PlexLibrary library) {
|
||||
return tagWithServer(library.serverId, library.serverName);
|
||||
}
|
||||
}
|
||||
@@ -9,12 +9,14 @@ class FiltersBottomSheet extends StatefulWidget {
|
||||
final List<PlexFilter> filters;
|
||||
final Map<String, String> selectedFilters;
|
||||
final Function(Map<String, String>) onFiltersChanged;
|
||||
final String serverId;
|
||||
|
||||
const FiltersBottomSheet({
|
||||
super.key,
|
||||
required this.filters,
|
||||
required this.selectedFilters,
|
||||
required this.onFiltersChanged,
|
||||
required this.serverId,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -60,7 +62,7 @@ class _FiltersBottomSheetState extends State<FiltersBottomSheet> {
|
||||
});
|
||||
|
||||
try {
|
||||
final client = context.getClientForServer(null);
|
||||
final client = context.getClientForServer(widget.serverId);
|
||||
|
||||
final values = await client.getFilterValues(filter.key);
|
||||
setState(() {
|
||||
|
||||
@@ -49,7 +49,7 @@ class _FolderTreeViewState extends State<FolderTreeView> {
|
||||
});
|
||||
|
||||
try {
|
||||
final client = context.getClientForServer(widget.serverId);
|
||||
final client = context.getClientForServer(widget.serverId!);
|
||||
|
||||
final folders = await client.getLibraryFolders(widget.libraryKey);
|
||||
|
||||
@@ -58,7 +58,7 @@ class _FolderTreeViewState extends State<FolderTreeView> {
|
||||
final taggedFolders = folders
|
||||
.map(
|
||||
(folder) => folder.copyWith(
|
||||
serverId: widget.serverId,
|
||||
serverId: widget.serverId!,
|
||||
serverName: null, // server name not required for folders listing
|
||||
),
|
||||
)
|
||||
@@ -101,21 +101,15 @@ class _FolderTreeViewState extends State<FolderTreeView> {
|
||||
});
|
||||
|
||||
try {
|
||||
final client = context.getClientForServer(widget.serverId);
|
||||
final client = context.getClientForServer(widget.serverId!);
|
||||
|
||||
// Items are automatically tagged with server info by PlexClient
|
||||
final children = await client.getFolderChildren(folder.key);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
final taggedChildren = children
|
||||
.map(
|
||||
(child) =>
|
||||
child.copyWith(serverId: widget.serverId, serverName: null),
|
||||
)
|
||||
.toList();
|
||||
|
||||
setState(() {
|
||||
_childrenCache[folder.key] = taggedChildren;
|
||||
_childrenCache[folder.key] = children;
|
||||
_expandedFolders.add(folder.key);
|
||||
_loadingFolders.remove(folder.key);
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
|
||||
import '../client/plex_client.dart';
|
||||
import '../models/plex_metadata.dart';
|
||||
import '../models/plex_playlist.dart';
|
||||
import '../providers/multi_server_provider.dart';
|
||||
import '../providers/settings_provider.dart';
|
||||
import '../services/settings_service.dart';
|
||||
import '../utils/provider_extensions.dart';
|
||||
@@ -659,6 +660,18 @@ PlexClient _getClientForItem(BuildContext context, dynamic item) {
|
||||
serverId = item.serverId;
|
||||
}
|
||||
|
||||
// If serverId is null, fall back to first available server
|
||||
if (serverId == null) {
|
||||
final multiServerProvider = Provider.of<MultiServerProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
if (!multiServerProvider.hasConnectedServers) {
|
||||
throw Exception('No servers available');
|
||||
}
|
||||
serverId = multiServerProvider.onlineServerIds.first;
|
||||
}
|
||||
|
||||
return context.getClientForServer(serverId);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
|
||||
import '../client/plex_client.dart';
|
||||
import '../models/plex_metadata.dart';
|
||||
import '../models/plex_playlist.dart';
|
||||
import '../providers/multi_server_provider.dart';
|
||||
import '../providers/playback_state_provider.dart';
|
||||
import '../utils/provider_extensions.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
@@ -71,6 +72,18 @@ class _MediaContextMenuState extends State<MediaContextMenu> {
|
||||
serverId = (widget.item as PlexPlaylist).serverId;
|
||||
}
|
||||
|
||||
// If serverId is null, fall back to first available server
|
||||
if (serverId == null) {
|
||||
final multiServerProvider = Provider.of<MultiServerProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
if (!multiServerProvider.hasConnectedServers) {
|
||||
throw Exception('No servers available');
|
||||
}
|
||||
serverId = multiServerProvider.onlineServerIds.first;
|
||||
}
|
||||
|
||||
return context.getClientForServer(serverId);
|
||||
}
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ class PlaylistItemCard extends StatelessWidget {
|
||||
|
||||
/// Get the correct PlexClient for this item's server
|
||||
PlexClient _getClientForItem(BuildContext context) {
|
||||
return context.getClientForServer(item.serverId);
|
||||
return context.getClientForServer(item.serverId!);
|
||||
}
|
||||
|
||||
Widget _buildPosterImage(BuildContext context) {
|
||||
|
||||
@@ -41,7 +41,7 @@ class ChapterSheet extends StatelessWidget {
|
||||
|
||||
/// Get the correct PlexClient for the metadata's server
|
||||
PlexClient _getClientForChapters(BuildContext context) {
|
||||
return context.getClientForServer(serverId);
|
||||
return context.getClientForServer(serverId!);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -97,7 +97,7 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
|
||||
|
||||
/// Get the correct PlexClient for this metadata's server
|
||||
PlexClient _getClientForMetadata() {
|
||||
return context.getClientForServer(widget.metadata.serverId);
|
||||
return context.getClientForServer(widget.metadata.serverId!);
|
||||
}
|
||||
|
||||
// Double-tap feedback state
|
||||
|
||||
Reference in New Issue
Block a user