@@ -92,6 +92,7 @@ class PlexMetadata with MultiServerFields {
|
||||
final int? playlistItemID; // Playlist item ID (for dumb playlists only)
|
||||
final int? playQueueItemID; // Play queue item ID (unique even for duplicates)
|
||||
final int? librarySectionID; // Library section ID this item belongs to
|
||||
final String? librarySectionTitle; // Library section title this item belongs to
|
||||
final String? ratingImage; // Rating source URI (e.g. rottentomatoes://image.rating.ripe)
|
||||
final String? audienceRatingImage; // Audience rating source URI
|
||||
final String? tagline;
|
||||
@@ -174,6 +175,7 @@ class PlexMetadata with MultiServerFields {
|
||||
this.playlistItemID,
|
||||
this.playQueueItemID,
|
||||
this.librarySectionID,
|
||||
this.librarySectionTitle,
|
||||
this.ratingImage,
|
||||
this.audienceRatingImage,
|
||||
this.tagline,
|
||||
@@ -229,6 +231,7 @@ class PlexMetadata with MultiServerFields {
|
||||
int? playlistItemID,
|
||||
int? playQueueItemID,
|
||||
int? librarySectionID,
|
||||
String? librarySectionTitle,
|
||||
String? ratingImage,
|
||||
String? audienceRatingImage,
|
||||
String? tagline,
|
||||
@@ -282,6 +285,7 @@ class PlexMetadata with MultiServerFields {
|
||||
playlistItemID: playlistItemID ?? this.playlistItemID,
|
||||
playQueueItemID: playQueueItemID ?? this.playQueueItemID,
|
||||
librarySectionID: librarySectionID ?? this.librarySectionID,
|
||||
librarySectionTitle: librarySectionTitle ?? this.librarySectionTitle,
|
||||
ratingImage: ratingImage ?? this.ratingImage,
|
||||
audienceRatingImage: audienceRatingImage ?? this.audienceRatingImage,
|
||||
tagline: tagline ?? this.tagline,
|
||||
|
||||
@@ -48,6 +48,7 @@ PlexMetadata _$PlexMetadataFromJson(Map<String, dynamic> json) => PlexMetadata(
|
||||
playlistItemID: (json['playlistItemID'] as num?)?.toInt(),
|
||||
playQueueItemID: (json['playQueueItemID'] as num?)?.toInt(),
|
||||
librarySectionID: (json['librarySectionID'] as num?)?.toInt(),
|
||||
librarySectionTitle: json['librarySectionTitle'] as String?,
|
||||
ratingImage: json['ratingImage'] as String?,
|
||||
audienceRatingImage: json['audienceRatingImage'] as String?,
|
||||
tagline: json['tagline'] as String?,
|
||||
@@ -100,6 +101,7 @@ Map<String, dynamic> _$PlexMetadataToJson(PlexMetadata instance) => <String, dyn
|
||||
'playlistItemID': instance.playlistItemID,
|
||||
'playQueueItemID': instance.playQueueItemID,
|
||||
'librarySectionID': instance.librarySectionID,
|
||||
'librarySectionTitle': instance.librarySectionTitle,
|
||||
'ratingImage': instance.ratingImage,
|
||||
'audienceRatingImage': instance.audienceRatingImage,
|
||||
'tagline': instance.tagline,
|
||||
|
||||
@@ -84,8 +84,13 @@ class DataAggregationService {
|
||||
return [];
|
||||
}
|
||||
|
||||
// For global hubs, fetch libraries to split "Recently Added" hubs by library
|
||||
final libraries = useGlobalHubs
|
||||
? (librariesByServer ?? groupLibrariesByServer(await getLibrariesFromAllServers()))
|
||||
: librariesByServer;
|
||||
|
||||
return useGlobalHubs
|
||||
? _fetchGlobalHubs(clients, limit: limit, hiddenLibraryKeys: hiddenLibraryKeys)
|
||||
? _fetchGlobalHubs(clients, limit: limit, hiddenLibraryKeys: hiddenLibraryKeys, librariesByServer: libraries)
|
||||
: _fetchLibraryHubs(
|
||||
clients,
|
||||
limit: limit,
|
||||
@@ -99,6 +104,7 @@ class DataAggregationService {
|
||||
Map<String, PlexClient> clients, {
|
||||
int? limit,
|
||||
Set<String>? hiddenLibraryKeys,
|
||||
Map<String, List<PlexLibrary>>? librariesByServer,
|
||||
}) async {
|
||||
appLogger.d('Fetching global hubs from ${clients.length} servers');
|
||||
|
||||
@@ -150,7 +156,9 @@ class DataAggregationService {
|
||||
});
|
||||
|
||||
final results = await Future.wait(hubFutures);
|
||||
final result = _collectAndLimitResults(results, limit);
|
||||
// Split "Recently Added" hubs that combine items from multiple libraries
|
||||
final splitResults = results.map((hubs) => _splitRecentlyAddedHubs(hubs, librariesByServer)).toList();
|
||||
final result = _collectAndLimitResults(splitResults, limit);
|
||||
|
||||
appLogger.i('Fetched ${result.length} global hubs from all servers');
|
||||
|
||||
@@ -300,6 +308,103 @@ class DataAggregationService {
|
||||
return limit != null && limit < all.length ? all.sublist(0, limit) : all;
|
||||
}
|
||||
|
||||
/// Split "Recently Added" hubs that contain items from multiple libraries
|
||||
/// into separate per-library hubs, matching the official Plex client behavior.
|
||||
List<PlexHub> _splitRecentlyAddedHubs(
|
||||
List<PlexHub> hubs,
|
||||
Map<String, List<PlexLibrary>>? librariesByServer,
|
||||
) {
|
||||
final result = <PlexHub>[];
|
||||
|
||||
for (final hub in hubs) {
|
||||
final hubId = hub.hubIdentifier?.toLowerCase() ?? '';
|
||||
if (!hubId.contains('.recent')) {
|
||||
result.add(hub);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Group items by librarySectionID
|
||||
final groups = <int, List<PlexMetadata>>{};
|
||||
final ungrouped = <PlexMetadata>[];
|
||||
|
||||
for (final item in hub.items) {
|
||||
final sectionId = item.librarySectionID;
|
||||
if (sectionId == null) {
|
||||
ungrouped.add(item);
|
||||
} else {
|
||||
groups.putIfAbsent(sectionId, () => []).add(item);
|
||||
}
|
||||
}
|
||||
|
||||
// Single library (or no groupable items) — keep hub unchanged
|
||||
if (groups.length <= 1) {
|
||||
result.add(hub);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Multiple libraries — create one hub per library
|
||||
for (final entry in groups.entries) {
|
||||
final items = entry.value;
|
||||
final libraryName = _resolveLibraryName(items.first, librariesByServer);
|
||||
final title = libraryName != null ? 'Recently Added in $libraryName' : hub.title;
|
||||
|
||||
result.add(PlexHub(
|
||||
hubKey: hub.hubKey,
|
||||
title: title,
|
||||
type: hub.type,
|
||||
hubIdentifier: '${hub.hubIdentifier}_${entry.key}',
|
||||
size: items.length,
|
||||
more: hub.more,
|
||||
items: items,
|
||||
serverId: hub.serverId,
|
||||
serverName: hub.serverName,
|
||||
));
|
||||
}
|
||||
|
||||
// Keep ungrouped items in a hub with the original title
|
||||
if (ungrouped.isNotEmpty) {
|
||||
result.add(PlexHub(
|
||||
hubKey: hub.hubKey,
|
||||
title: hub.title,
|
||||
type: hub.type,
|
||||
hubIdentifier: hub.hubIdentifier,
|
||||
size: ungrouped.length,
|
||||
more: hub.more,
|
||||
items: ungrouped,
|
||||
serverId: hub.serverId,
|
||||
serverName: hub.serverName,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Resolve library name from item metadata or library lookup map.
|
||||
String? _resolveLibraryName(
|
||||
PlexMetadata item,
|
||||
Map<String, List<PlexLibrary>>? librariesByServer,
|
||||
) {
|
||||
// Try librarySectionTitle from the item itself (Plex API often includes it)
|
||||
if (item.librarySectionTitle != null && item.librarySectionTitle!.isNotEmpty) {
|
||||
return item.librarySectionTitle;
|
||||
}
|
||||
|
||||
// Fall back to library lookup
|
||||
if (librariesByServer != null && item.serverId != null && item.librarySectionID != null) {
|
||||
final serverLibraries = librariesByServer[item.serverId];
|
||||
if (serverLibraries != null) {
|
||||
for (final lib in serverLibraries) {
|
||||
if (lib.key == item.librarySectionID.toString()) {
|
||||
return lib.title;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Base helper for per-server fan-out operations
|
||||
///
|
||||
/// Returns raw results as (serverId, result) tuples.
|
||||
|
||||
Reference in New Issue
Block a user