From c9789458b95029db5cd98da02e6192e2f74039b4 Mon Sep 17 00:00:00 2001 From: Jason Dove Date: Sat, 28 May 2022 20:41:22 -0500 Subject: [PATCH] page media server movie libraries --- CHANGELOG.md | 4 ++ ErsatzTV.Core/Emby/EmbyItemType.cs | 6 ++ ErsatzTV.Core/Emby/EmbyMovieLibraryScanner.cs | 11 +++- .../Interfaces/Emby/IEmbyApiClient.cs | 8 ++- .../Interfaces/Jellyfin/IJellyfinApiClient.cs | 8 ++- .../Interfaces/Plex/IPlexServerApiClient.cs | 7 +- ErsatzTV.Core/Jellyfin/JellyfinItemType.cs | 6 ++ .../Jellyfin/JellyfinMovieLibraryScanner.cs | 11 +++- .../MediaServerMovieLibraryScanner.cs | 30 +++++---- ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs | 10 ++- ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs | 56 +++++++++++----- ErsatzTV.Infrastructure/Emby/IEmbyApi.cs | 21 +++++- .../Emby/Models/EmbyLibraryItemsResponse.cs | 1 + .../Jellyfin/IJellyfinApi.cs | 25 ++++++- .../Jellyfin/JellyfinApiClient.cs | 66 ++++++++++++++----- .../Models/JellyfinLibraryItemsResponse.cs | 1 + .../Plex/IPlexServerApi.cs | 19 ++++++ .../Plex/Models/PlexMediaContainerResponse.cs | 7 ++ .../Plex/PlexServerApiClient.cs | 50 ++++++++++---- 19 files changed, 284 insertions(+), 63 deletions(-) create mode 100644 ErsatzTV.Core/Emby/EmbyItemType.cs create mode 100644 ErsatzTV.Core/Jellyfin/JellyfinItemType.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 0011c846..871e0d9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Added - Enable QSV hardware acceleration for vaapi docker images +### Changed +- Use paging to synchronize movies from Plex, Jellyfin and Emby + - This will reduce memory use and improve reliability of synchronizing large libraries + ## [0.5.8-beta] - 2022-05-20 ### Fixed - Fix error display with `HLS Segmenter` and `MPEG-TS` streaming modes diff --git a/ErsatzTV.Core/Emby/EmbyItemType.cs b/ErsatzTV.Core/Emby/EmbyItemType.cs new file mode 100644 index 00000000..585726f8 --- /dev/null +++ b/ErsatzTV.Core/Emby/EmbyItemType.cs @@ -0,0 +1,6 @@ +namespace ErsatzTV.Core.Emby; + +public static class EmbyItemType +{ + public static readonly string Movie = "Movie"; +} diff --git a/ErsatzTV.Core/Emby/EmbyMovieLibraryScanner.cs b/ErsatzTV.Core/Emby/EmbyMovieLibraryScanner.cs index 76954e40..22101e44 100644 --- a/ErsatzTV.Core/Emby/EmbyMovieLibraryScanner.cs +++ b/ErsatzTV.Core/Emby/EmbyMovieLibraryScanner.cs @@ -79,7 +79,16 @@ public class EmbyMovieLibraryScanner : protected override string MediaServerItemId(EmbyMovie movie) => movie.ItemId; protected override string MediaServerEtag(EmbyMovie movie) => movie.Etag; - protected override Task>> GetMovieLibraryItems( + protected override Task> CountMovieLibraryItems( + EmbyConnectionParameters connectionParameters, + EmbyLibrary library) => + _embyApiClient.GetLibraryItemCount( + connectionParameters.Address, + connectionParameters.ApiKey, + library, + EmbyItemType.Movie); + + protected override IAsyncEnumerable GetMovieLibraryItems( EmbyConnectionParameters connectionParameters, EmbyLibrary library) => _embyApiClient.GetMovieLibraryItems( diff --git a/ErsatzTV.Core/Interfaces/Emby/IEmbyApiClient.cs b/ErsatzTV.Core/Interfaces/Emby/IEmbyApiClient.cs index 7b62aa17..cee715b6 100644 --- a/ErsatzTV.Core/Interfaces/Emby/IEmbyApiClient.cs +++ b/ErsatzTV.Core/Interfaces/Emby/IEmbyApiClient.cs @@ -8,7 +8,7 @@ public interface IEmbyApiClient Task> GetServerInformation(string address, string apiKey); Task>> GetLibraries(string address, string apiKey); - Task>> GetMovieLibraryItems( + IAsyncEnumerable GetMovieLibraryItems( string address, string apiKey, EmbyLibrary library); @@ -37,4 +37,10 @@ public interface IEmbyApiClient string address, string apiKey, string collectionId); + + Task> GetLibraryItemCount( + string address, + string apiKey, + EmbyLibrary library, + string includeItemTypes); } diff --git a/ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinApiClient.cs b/ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinApiClient.cs index 4d938b49..66e8002e 100644 --- a/ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinApiClient.cs +++ b/ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinApiClient.cs @@ -9,7 +9,7 @@ public interface IJellyfinApiClient Task>> GetLibraries(string address, string apiKey); Task> GetAdminUserId(string address, string apiKey); - Task>> GetMovieLibraryItems( + IAsyncEnumerable GetMovieLibraryItems( string address, string apiKey, JellyfinLibrary library); @@ -42,4 +42,10 @@ public interface IJellyfinApiClient string apiKey, int mediaSourceId, string collectionId); + + Task> GetLibraryItemCount( + string address, + string apiKey, + JellyfinLibrary library, + string includeItemTypes); } diff --git a/ErsatzTV.Core/Interfaces/Plex/IPlexServerApiClient.cs b/ErsatzTV.Core/Interfaces/Plex/IPlexServerApiClient.cs index 898c9b1a..a96327d7 100644 --- a/ErsatzTV.Core/Interfaces/Plex/IPlexServerApiClient.cs +++ b/ErsatzTV.Core/Interfaces/Plex/IPlexServerApiClient.cs @@ -13,7 +13,7 @@ public interface IPlexServerApiClient PlexConnection connection, PlexServerAuthToken token); - Task>> GetMovieLibraryContents( + IAsyncEnumerable GetMovieLibraryContents( PlexLibrary library, PlexConnection connection, PlexServerAuthToken token); @@ -58,4 +58,9 @@ public interface IPlexServerApiClient string key, PlexConnection connection, PlexServerAuthToken token); + + Task> GetLibraryItemCount( + PlexLibrary library, + PlexConnection connection, + PlexServerAuthToken token); } diff --git a/ErsatzTV.Core/Jellyfin/JellyfinItemType.cs b/ErsatzTV.Core/Jellyfin/JellyfinItemType.cs new file mode 100644 index 00000000..443a56e3 --- /dev/null +++ b/ErsatzTV.Core/Jellyfin/JellyfinItemType.cs @@ -0,0 +1,6 @@ +namespace ErsatzTV.Core.Jellyfin; + +public static class JellyfinItemType +{ + public static readonly string Movie = "Movie"; +} diff --git a/ErsatzTV.Core/Jellyfin/JellyfinMovieLibraryScanner.cs b/ErsatzTV.Core/Jellyfin/JellyfinMovieLibraryScanner.cs index e3dc9113..09d412bc 100644 --- a/ErsatzTV.Core/Jellyfin/JellyfinMovieLibraryScanner.cs +++ b/ErsatzTV.Core/Jellyfin/JellyfinMovieLibraryScanner.cs @@ -80,7 +80,16 @@ public class JellyfinMovieLibraryScanner : protected override string MediaServerEtag(JellyfinMovie movie) => movie.Etag; - protected override Task>> GetMovieLibraryItems( + protected override Task> CountMovieLibraryItems( + JellyfinConnectionParameters connectionParameters, + JellyfinLibrary library) => + _jellyfinApiClient.GetLibraryItemCount( + connectionParameters.Address, + connectionParameters.ApiKey, + library, + JellyfinItemType.Movie); + + protected override IAsyncEnumerable GetMovieLibraryItems( JellyfinConnectionParameters connectionParameters, JellyfinLibrary library) => _jellyfinApiClient.GetMovieLibraryItems( diff --git a/ErsatzTV.Core/Metadata/MediaServerMovieLibraryScanner.cs b/ErsatzTV.Core/Metadata/MediaServerMovieLibraryScanner.cs index 7447e11d..9bb2f605 100644 --- a/ErsatzTV.Core/Metadata/MediaServerMovieLibraryScanner.cs +++ b/ErsatzTV.Core/Metadata/MediaServerMovieLibraryScanner.cs @@ -53,13 +53,14 @@ public abstract class MediaServerMovieLibraryScanner> entries = await GetMovieLibraryItems(connectionParameters, library); - - foreach (BaseError error in entries.LeftToSeq()) + Either maybeCount = await CountMovieLibraryItems(connectionParameters, library); + foreach (BaseError error in maybeCount.LeftToSeq()) { return error; } + int count = await maybeCount.RightToSeq().HeadOrNone().IfNoneAsync(1); + return await ScanLibrary( movieRepository, connectionParameters, @@ -67,7 +68,8 @@ public abstract class MediaServerMovieLibraryScanner getLocalPath, string ffmpegPath, string ffprobePath, - List movieEntries, + IAsyncEnumerable movieEntries, + int totalMovieCount, bool deepScan, CancellationToken cancellationToken) { + var incomingItemIds = new List(); List existingMovies = await movieRepository.GetExistingMovies(library); - var sortedMovies = movieEntries.OrderBy(m => m.MovieMetadata.Head().SortTitle).ToList(); - foreach (TMovie incoming in sortedMovies) + await foreach (TMovie incoming in movieEntries.WithCancellation(cancellationToken)) { if (cancellationToken.IsCancellationRequested) { return new ScanCanceled(); } - decimal percentCompletion = (decimal)sortedMovies.IndexOf(incoming) / sortedMovies.Count; + incomingItemIds.Add(MediaServerItemId(incoming)); + + decimal percentCompletion = Math.Clamp((decimal)incomingItemIds.Count / totalMovieCount, 0, 1); await _mediator.Publish(new LibraryScanProgress(library.Id, percentCompletion), cancellationToken); string localPath = getLocalPath(incoming); @@ -165,8 +170,7 @@ public abstract class MediaServerMovieLibraryScanner m.MediaServerItemId) - .Except(movieEntries.Map(MediaServerItemId)).ToList(); + var fileNotFoundItemIds = existingMovies.Map(m => m.MediaServerItemId).Except(incomingItemIds).ToList(); List ids = await movieRepository.FlagFileNotFound(library, fileNotFoundItemIds); await _searchIndex.RebuildItems(_searchRepository, ids); @@ -178,7 +182,11 @@ public abstract class MediaServerMovieLibraryScanner>> GetMovieLibraryItems( + protected abstract Task> CountMovieLibraryItems( + TConnectionParameters connectionParameters, + TLibrary library); + + protected abstract IAsyncEnumerable GetMovieLibraryItems( TConnectionParameters connectionParameters, TLibrary library); diff --git a/ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs b/ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs index cc96e372..c1dfde11 100644 --- a/ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs +++ b/ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs @@ -89,7 +89,15 @@ public class PlexMovieLibraryScanner : protected override string MediaServerEtag(PlexMovie movie) => movie.Etag; - protected override Task>> GetMovieLibraryItems( + protected override Task> CountMovieLibraryItems( + PlexConnectionParameters connectionParameters, + PlexLibrary library) + => _plexServerApiClient.GetLibraryItemCount( + library, + connectionParameters.Connection, + connectionParameters.Token); + + protected override IAsyncEnumerable GetMovieLibraryItems( PlexConnectionParameters connectionParameters, PlexLibrary library) => _plexServerApiClient.GetMovieLibraryContents( diff --git a/ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs b/ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs index 78b0d991..03e0f340 100644 --- a/ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs +++ b/ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs @@ -71,24 +71,29 @@ public class EmbyApiClient : IEmbyApiClient } } - public async Task>> GetMovieLibraryItems( - string address, - string apiKey, - EmbyLibrary library) + public async IAsyncEnumerable GetMovieLibraryItems(string address, string apiKey, EmbyLibrary library) { - try - { - IEmbyApi service = RestService.For(address); - EmbyLibraryItemsResponse items = await service.GetMovieLibraryItems(apiKey, library.ItemId); - return items.Items - .Map(i => ProjectToMovie(library, i)) - .Somes() - .ToList(); - } - catch (Exception ex) + IEmbyApi service = RestService.For(address); + int size = await service + .GetLibraryStats(apiKey, library.ItemId, EmbyItemType.Movie) + .Map(r => r.TotalRecordCount); + + const int PAGE_SIZE = 10; + + int pages = (size - 1) / PAGE_SIZE + 1; + + for (var i = 0; i < pages; i++) { - _logger.LogError(ex, "Error getting emby movie library items"); - return BaseError.New(ex.Message); + int skip = i * PAGE_SIZE; + + Task> result = service + .GetMovieLibraryItems(apiKey, library.ItemId, startIndex: skip, limit: PAGE_SIZE) + .Map(items => items.Items.Map(item => ProjectToMovie(library, item)).Somes()); + + foreach (EmbyMovie movie in await result) + { + yield return movie; + } } } @@ -202,6 +207,25 @@ public class EmbyApiClient : IEmbyApiClient } } + public async Task> GetLibraryItemCount( + string address, + string apiKey, + EmbyLibrary library, + string includeItemTypes) + { + try + { + IEmbyApi service = RestService.For(address); + EmbyLibraryItemsResponse items = await service.GetLibraryStats(apiKey, library.ItemId, includeItemTypes); + return items.TotalRecordCount; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting Emby library item count"); + return BaseError.New(ex.Message); + } + } + private Option ProjectToCollection(EmbyLibraryItemResponse item) { try diff --git a/ErsatzTV.Infrastructure/Emby/IEmbyApi.cs b/ErsatzTV.Infrastructure/Emby/IEmbyApi.cs index 4c094ac7..740109a0 100644 --- a/ErsatzTV.Infrastructure/Emby/IEmbyApi.cs +++ b/ErsatzTV.Infrastructure/Emby/IEmbyApi.cs @@ -17,6 +17,21 @@ public interface IEmbyApi [Header("X-Emby-Token")] string apiKey); + [Get("/Items")] + public Task GetLibraryStats( + [Header("X-Emby-Token")] + string apiKey, + [Query] + string parentId, + [Query] + string includeItemTypes, + [Query] + bool recursive = true, + [Query] + int startIndex = 0, + [Query] + int limit = 0); + [Get("/Items")] public Task GetMovieLibraryItems( [Header("X-Emby-Token")] @@ -29,7 +44,11 @@ public interface IEmbyApi [Query] string includeItemTypes = "Movie", [Query] - bool recursive = true); + bool recursive = true, + [Query] + int startIndex = 0, + [Query] + int limit = 0); [Get("/Items")] public Task GetShowLibraryItems( diff --git a/ErsatzTV.Infrastructure/Emby/Models/EmbyLibraryItemsResponse.cs b/ErsatzTV.Infrastructure/Emby/Models/EmbyLibraryItemsResponse.cs index e99621bb..cdf90f90 100644 --- a/ErsatzTV.Infrastructure/Emby/Models/EmbyLibraryItemsResponse.cs +++ b/ErsatzTV.Infrastructure/Emby/Models/EmbyLibraryItemsResponse.cs @@ -3,4 +3,5 @@ public class EmbyLibraryItemsResponse { public List Items { get; set; } + public int TotalRecordCount { get; set; } } diff --git a/ErsatzTV.Infrastructure/Jellyfin/IJellyfinApi.cs b/ErsatzTV.Infrastructure/Jellyfin/IJellyfinApi.cs index 0825e19c..7ecf6e94 100644 --- a/ErsatzTV.Infrastructure/Jellyfin/IJellyfinApi.cs +++ b/ErsatzTV.Infrastructure/Jellyfin/IJellyfinApi.cs @@ -23,6 +23,25 @@ public interface IJellyfinApi string apiKey); [Get("/Items")] + public Task GetLibraryStats( + [Header("X-Emby-Token")] + string apiKey, + [Query] + string userId, + [Query] + string parentId, + [Query] + string includeItemTypes, + [Query] + bool recursive = true, + [Query] + string filters = "IsNotFolder", + [Query] + int startIndex = 0, + [Query] + int limit = 0); + + [Get("/Items?sortOrder=Ascending&sortBy=SortName")] public Task GetMovieLibraryItems( [Header("X-Emby-Token")] string apiKey, @@ -38,7 +57,11 @@ public interface IJellyfinApi [Query] bool recursive = true, [Query] - string filters = "IsNotFolder"); + string filters = "IsNotFolder", + [Query] + int startIndex = 0, + [Query] + int limit = 0); [Get("/Items")] public Task GetShowLibraryItems( diff --git a/ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs b/ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs index 9f37a9a8..52e6562d 100644 --- a/ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs +++ b/ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs @@ -91,29 +91,35 @@ public class JellyfinApiClient : IJellyfinApiClient } } - public async Task>> GetMovieLibraryItems( + public async IAsyncEnumerable GetMovieLibraryItems( string address, string apiKey, JellyfinLibrary library) { - try + if (_memoryCache.TryGetValue($"jellyfin_admin_user_id.{library.MediaSourceId}", out string userId)) { - if (_memoryCache.TryGetValue($"jellyfin_admin_user_id.{library.MediaSourceId}", out string userId)) + IJellyfinApi service = RestService.For(address); + int size = await service + .GetLibraryStats(apiKey, userId, library.ItemId, JellyfinItemType.Movie) + .Map(r => r.TotalRecordCount); + + const int PAGE_SIZE = 10; + + int pages = (size - 1) / PAGE_SIZE + 1; + + for (var i = 0; i < pages; i++) { - IJellyfinApi service = RestService.For(address); - JellyfinLibraryItemsResponse items = await service.GetMovieLibraryItems(apiKey, userId, library.ItemId); - return items.Items - .Map(i => ProjectToMovie(library, i)) - .Somes() - .ToList(); - } + int skip = i * PAGE_SIZE; - return BaseError.New("Jellyfin admin user id is not available"); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting jellyfin movie library items"); - return BaseError.New(ex.Message); + Task> result = service + .GetMovieLibraryItems(apiKey, userId, library.ItemId, startIndex: skip, limit: PAGE_SIZE) + .Map(items => items.Items.Map(item => ProjectToMovie(library, item)).Somes()); + + foreach (JellyfinMovie movie in await result) + { + yield return movie; + } + } } } @@ -262,6 +268,34 @@ public class JellyfinApiClient : IJellyfinApiClient } } + public async Task> GetLibraryItemCount( + string address, + string apiKey, + JellyfinLibrary library, + string includeItemTypes) + { + try + { + if (_memoryCache.TryGetValue($"jellyfin_admin_user_id.{library.MediaSourceId}", out string userId)) + { + IJellyfinApi service = RestService.For(address); + JellyfinLibraryItemsResponse items = await service.GetLibraryStats( + apiKey, + userId, + library.ItemId, + includeItemTypes); + return items.TotalRecordCount; + } + + return BaseError.New("Jellyfin admin user id is not available"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting jellyfin library item count"); + return BaseError.New(ex.Message); + } + } + private Option ProjectToCollectionMediaItem(JellyfinLibraryItemResponse item) { try diff --git a/ErsatzTV.Infrastructure/Jellyfin/Models/JellyfinLibraryItemsResponse.cs b/ErsatzTV.Infrastructure/Jellyfin/Models/JellyfinLibraryItemsResponse.cs index 53f625af..b4b6f8ab 100644 --- a/ErsatzTV.Infrastructure/Jellyfin/Models/JellyfinLibraryItemsResponse.cs +++ b/ErsatzTV.Infrastructure/Jellyfin/Models/JellyfinLibraryItemsResponse.cs @@ -3,4 +3,5 @@ public class JellyfinLibraryItemsResponse { public List Items { get; set; } + public int TotalRecordCount { get; set; } } diff --git a/ErsatzTV.Infrastructure/Plex/IPlexServerApi.cs b/ErsatzTV.Infrastructure/Plex/IPlexServerApi.cs index 72a92f97..154c22c6 100644 --- a/ErsatzTV.Infrastructure/Plex/IPlexServerApi.cs +++ b/ErsatzTV.Infrastructure/Plex/IPlexServerApi.cs @@ -17,11 +17,30 @@ public interface IPlexServerApi [Query] [AliasAs("X-Plex-Token")] string token); + [Get("/library/sections/{key}/all?X-Plex-Container-Start=0&X-Plex-Container-Size=0")] + [Headers("Accept: text/xml")] + public Task GetLibrarySection( + string key, + [Query] [AliasAs("X-Plex-Token")] + string token); + + [Get("/library/sections/{key}/all")] + [Headers("Accept: application/json")] + public Task>> + GetLibrarySectionContents( + string key, + [Query] [AliasAs("X-Plex-Token")] + string token); + [Get("/library/sections/{key}/all")] [Headers("Accept: application/json")] public Task>> GetLibrarySectionContents( string key, + [Query] [AliasAs("X-Plex-Container-Start")] + int skip, + [Query] [AliasAs("X-Plex-Container-Size")] + int take, [Query] [AliasAs("X-Plex-Token")] string token); diff --git a/ErsatzTV.Infrastructure/Plex/Models/PlexMediaContainerResponse.cs b/ErsatzTV.Infrastructure/Plex/Models/PlexMediaContainerResponse.cs index 66277521..f3c662dd 100644 --- a/ErsatzTV.Infrastructure/Plex/Models/PlexMediaContainerResponse.cs +++ b/ErsatzTV.Infrastructure/Plex/Models/PlexMediaContainerResponse.cs @@ -17,6 +17,13 @@ public class PlexMediaContainerMetadataContent public List Metadata { get; set; } } +[XmlRoot("MediaContainer", Namespace = null)] +public class PlexXmlMediaContainerStatsResponse +{ + [XmlAttribute("totalSize")] + public int TotalSize { get; set; } +} + [XmlRoot("MediaContainer", Namespace = null)] public class PlexXmlVideoMetadataResponseContainer { diff --git a/ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs b/ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs index 24d0c860..3da850e4 100644 --- a/ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs +++ b/ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs @@ -26,9 +26,7 @@ public class PlexServerApiClient : IPlexServerApiClient _logger = logger; } - public async Task Ping( - PlexConnection connection, - PlexServerAuthToken token) + public async Task Ping(PlexConnection connection, PlexServerAuthToken token) { try { @@ -76,21 +74,32 @@ public class PlexServerApiClient : IPlexServerApiClient } } - public async Task>> GetMovieLibraryContents( + public async IAsyncEnumerable GetMovieLibraryContents( PlexLibrary library, PlexConnection connection, PlexServerAuthToken token) { - try + IPlexServerApi xmlService = XmlServiceFor(connection.Uri); + int size = await xmlService.GetLibrarySection(library.Key, token.AuthToken).Map(r => r.TotalSize); + + const int PAGE_SIZE = 10; + + IPlexServerApi service = RestService.For(connection.Uri); + int pages = (size - 1) / PAGE_SIZE + 1; + + for (var i = 0; i < pages; i++) { - IPlexServerApi service = RestService.For(connection.Uri); - return await service.GetLibrarySectionContents(library.Key, token.AuthToken) + int skip = i * PAGE_SIZE; + + Task> result = service + .GetLibrarySectionContents(library.Key, skip, PAGE_SIZE, token.AuthToken) .Map(r => r.MediaContainer.Metadata.Filter(m => m.Media.Count > 0 && m.Media[0].Part.Count > 0)) - .Map(list => list.Map(metadata => ProjectToMovie(metadata, library.MediaSourceId)).ToList()); - } - catch (Exception ex) - { - return BaseError.New(ex.ToString()); + .Map(list => list.Map(metadata => ProjectToMovie(metadata, library.MediaSourceId))); + + foreach (PlexMovie movie in await result) + { + yield return movie; + } } } @@ -254,6 +263,23 @@ public class PlexServerApiClient : IPlexServerApiClient } } + public async Task> GetLibraryItemCount( + PlexLibrary library, + PlexConnection connection, + PlexServerAuthToken token) + { + try + { + IPlexServerApi service = XmlServiceFor(connection.Uri); + return await service.GetLibrarySection(library.Key, token.AuthToken).Map(r => r.TotalSize); + } + catch (Exception ex) + { + return BaseError.New(ex.ToString()); + } + } + + private List ProcessMultiEpisodeFiles(IEnumerable episodes) { // add all metadata from duplicate paths to first entry with given path