Browse Source

page media server movie libraries

pull/826/head
Jason Dove 3 years ago
parent
commit
c9789458b9
  1. 4
      CHANGELOG.md
  2. 6
      ErsatzTV.Core/Emby/EmbyItemType.cs
  3. 11
      ErsatzTV.Core/Emby/EmbyMovieLibraryScanner.cs
  4. 8
      ErsatzTV.Core/Interfaces/Emby/IEmbyApiClient.cs
  5. 8
      ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinApiClient.cs
  6. 7
      ErsatzTV.Core/Interfaces/Plex/IPlexServerApiClient.cs
  7. 6
      ErsatzTV.Core/Jellyfin/JellyfinItemType.cs
  8. 11
      ErsatzTV.Core/Jellyfin/JellyfinMovieLibraryScanner.cs
  9. 30
      ErsatzTV.Core/Metadata/MediaServerMovieLibraryScanner.cs
  10. 10
      ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs
  11. 56
      ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs
  12. 21
      ErsatzTV.Infrastructure/Emby/IEmbyApi.cs
  13. 1
      ErsatzTV.Infrastructure/Emby/Models/EmbyLibraryItemsResponse.cs
  14. 25
      ErsatzTV.Infrastructure/Jellyfin/IJellyfinApi.cs
  15. 66
      ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs
  16. 1
      ErsatzTV.Infrastructure/Jellyfin/Models/JellyfinLibraryItemsResponse.cs
  17. 19
      ErsatzTV.Infrastructure/Plex/IPlexServerApi.cs
  18. 7
      ErsatzTV.Infrastructure/Plex/Models/PlexMediaContainerResponse.cs
  19. 50
      ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs

4
CHANGELOG.md

@ -12,6 +12,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Added ### Added
- Enable QSV hardware acceleration for vaapi docker images - 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 ## [0.5.8-beta] - 2022-05-20
### Fixed ### Fixed
- Fix error display with `HLS Segmenter` and `MPEG-TS` streaming modes - Fix error display with `HLS Segmenter` and `MPEG-TS` streaming modes

6
ErsatzTV.Core/Emby/EmbyItemType.cs

@ -0,0 +1,6 @@
namespace ErsatzTV.Core.Emby;
public static class EmbyItemType
{
public static readonly string Movie = "Movie";
}

11
ErsatzTV.Core/Emby/EmbyMovieLibraryScanner.cs

@ -79,7 +79,16 @@ public class EmbyMovieLibraryScanner :
protected override string MediaServerItemId(EmbyMovie movie) => movie.ItemId; protected override string MediaServerItemId(EmbyMovie movie) => movie.ItemId;
protected override string MediaServerEtag(EmbyMovie movie) => movie.Etag; protected override string MediaServerEtag(EmbyMovie movie) => movie.Etag;
protected override Task<Either<BaseError, List<EmbyMovie>>> GetMovieLibraryItems( protected override Task<Either<BaseError, int>> CountMovieLibraryItems(
EmbyConnectionParameters connectionParameters,
EmbyLibrary library) =>
_embyApiClient.GetLibraryItemCount(
connectionParameters.Address,
connectionParameters.ApiKey,
library,
EmbyItemType.Movie);
protected override IAsyncEnumerable<EmbyMovie> GetMovieLibraryItems(
EmbyConnectionParameters connectionParameters, EmbyConnectionParameters connectionParameters,
EmbyLibrary library) => EmbyLibrary library) =>
_embyApiClient.GetMovieLibraryItems( _embyApiClient.GetMovieLibraryItems(

8
ErsatzTV.Core/Interfaces/Emby/IEmbyApiClient.cs

@ -8,7 +8,7 @@ public interface IEmbyApiClient
Task<Either<BaseError, EmbyServerInformation>> GetServerInformation(string address, string apiKey); Task<Either<BaseError, EmbyServerInformation>> GetServerInformation(string address, string apiKey);
Task<Either<BaseError, List<EmbyLibrary>>> GetLibraries(string address, string apiKey); Task<Either<BaseError, List<EmbyLibrary>>> GetLibraries(string address, string apiKey);
Task<Either<BaseError, List<EmbyMovie>>> GetMovieLibraryItems( IAsyncEnumerable<EmbyMovie> GetMovieLibraryItems(
string address, string address,
string apiKey, string apiKey,
EmbyLibrary library); EmbyLibrary library);
@ -37,4 +37,10 @@ public interface IEmbyApiClient
string address, string address,
string apiKey, string apiKey,
string collectionId); string collectionId);
Task<Either<BaseError, int>> GetLibraryItemCount(
string address,
string apiKey,
EmbyLibrary library,
string includeItemTypes);
} }

8
ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinApiClient.cs

@ -9,7 +9,7 @@ public interface IJellyfinApiClient
Task<Either<BaseError, List<JellyfinLibrary>>> GetLibraries(string address, string apiKey); Task<Either<BaseError, List<JellyfinLibrary>>> GetLibraries(string address, string apiKey);
Task<Either<BaseError, string>> GetAdminUserId(string address, string apiKey); Task<Either<BaseError, string>> GetAdminUserId(string address, string apiKey);
Task<Either<BaseError, List<JellyfinMovie>>> GetMovieLibraryItems( IAsyncEnumerable<JellyfinMovie> GetMovieLibraryItems(
string address, string address,
string apiKey, string apiKey,
JellyfinLibrary library); JellyfinLibrary library);
@ -42,4 +42,10 @@ public interface IJellyfinApiClient
string apiKey, string apiKey,
int mediaSourceId, int mediaSourceId,
string collectionId); string collectionId);
Task<Either<BaseError, int>> GetLibraryItemCount(
string address,
string apiKey,
JellyfinLibrary library,
string includeItemTypes);
} }

7
ErsatzTV.Core/Interfaces/Plex/IPlexServerApiClient.cs

@ -13,7 +13,7 @@ public interface IPlexServerApiClient
PlexConnection connection, PlexConnection connection,
PlexServerAuthToken token); PlexServerAuthToken token);
Task<Either<BaseError, List<PlexMovie>>> GetMovieLibraryContents( IAsyncEnumerable<PlexMovie> GetMovieLibraryContents(
PlexLibrary library, PlexLibrary library,
PlexConnection connection, PlexConnection connection,
PlexServerAuthToken token); PlexServerAuthToken token);
@ -58,4 +58,9 @@ public interface IPlexServerApiClient
string key, string key,
PlexConnection connection, PlexConnection connection,
PlexServerAuthToken token); PlexServerAuthToken token);
Task<Either<BaseError, int>> GetLibraryItemCount(
PlexLibrary library,
PlexConnection connection,
PlexServerAuthToken token);
} }

6
ErsatzTV.Core/Jellyfin/JellyfinItemType.cs

@ -0,0 +1,6 @@
namespace ErsatzTV.Core.Jellyfin;
public static class JellyfinItemType
{
public static readonly string Movie = "Movie";
}

11
ErsatzTV.Core/Jellyfin/JellyfinMovieLibraryScanner.cs

@ -80,7 +80,16 @@ public class JellyfinMovieLibraryScanner :
protected override string MediaServerEtag(JellyfinMovie movie) => movie.Etag; protected override string MediaServerEtag(JellyfinMovie movie) => movie.Etag;
protected override Task<Either<BaseError, List<JellyfinMovie>>> GetMovieLibraryItems( protected override Task<Either<BaseError, int>> CountMovieLibraryItems(
JellyfinConnectionParameters connectionParameters,
JellyfinLibrary library) =>
_jellyfinApiClient.GetLibraryItemCount(
connectionParameters.Address,
connectionParameters.ApiKey,
library,
JellyfinItemType.Movie);
protected override IAsyncEnumerable<JellyfinMovie> GetMovieLibraryItems(
JellyfinConnectionParameters connectionParameters, JellyfinConnectionParameters connectionParameters,
JellyfinLibrary library) => JellyfinLibrary library) =>
_jellyfinApiClient.GetMovieLibraryItems( _jellyfinApiClient.GetMovieLibraryItems(

30
ErsatzTV.Core/Metadata/MediaServerMovieLibraryScanner.cs

@ -53,13 +53,14 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
{ {
try try
{ {
Either<BaseError, List<TMovie>> entries = await GetMovieLibraryItems(connectionParameters, library); Either<BaseError, int> maybeCount = await CountMovieLibraryItems(connectionParameters, library);
foreach (BaseError error in maybeCount.LeftToSeq())
foreach (BaseError error in entries.LeftToSeq())
{ {
return error; return error;
} }
int count = await maybeCount.RightToSeq().HeadOrNone().IfNoneAsync(1);
return await ScanLibrary( return await ScanLibrary(
movieRepository, movieRepository,
connectionParameters, connectionParameters,
@ -67,7 +68,8 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
getLocalPath, getLocalPath,
ffmpegPath, ffmpegPath,
ffprobePath, ffprobePath,
entries.RightToSeq().Flatten().ToList(), GetMovieLibraryItems(connectionParameters, library),
count,
deepScan, deepScan,
cancellationToken); cancellationToken);
} }
@ -88,21 +90,24 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
Func<TMovie, string> getLocalPath, Func<TMovie, string> getLocalPath,
string ffmpegPath, string ffmpegPath,
string ffprobePath, string ffprobePath,
List<TMovie> movieEntries, IAsyncEnumerable<TMovie> movieEntries,
int totalMovieCount,
bool deepScan, bool deepScan,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var incomingItemIds = new List<string>();
List<TEtag> existingMovies = await movieRepository.GetExistingMovies(library); List<TEtag> existingMovies = await movieRepository.GetExistingMovies(library);
var sortedMovies = movieEntries.OrderBy(m => m.MovieMetadata.Head().SortTitle).ToList(); await foreach (TMovie incoming in movieEntries.WithCancellation(cancellationToken))
foreach (TMovie incoming in sortedMovies)
{ {
if (cancellationToken.IsCancellationRequested) if (cancellationToken.IsCancellationRequested)
{ {
return new ScanCanceled(); 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); await _mediator.Publish(new LibraryScanProgress(library.Id, percentCompletion), cancellationToken);
string localPath = getLocalPath(incoming); string localPath = getLocalPath(incoming);
@ -165,8 +170,7 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
} }
// trash movies that are no longer present on the media server // trash movies that are no longer present on the media server
var fileNotFoundItemIds = existingMovies.Map(m => m.MediaServerItemId) var fileNotFoundItemIds = existingMovies.Map(m => m.MediaServerItemId).Except(incomingItemIds).ToList();
.Except(movieEntries.Map(MediaServerItemId)).ToList();
List<int> ids = await movieRepository.FlagFileNotFound(library, fileNotFoundItemIds); List<int> ids = await movieRepository.FlagFileNotFound(library, fileNotFoundItemIds);
await _searchIndex.RebuildItems(_searchRepository, ids); await _searchIndex.RebuildItems(_searchRepository, ids);
@ -178,7 +182,11 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
protected abstract string MediaServerItemId(TMovie movie); protected abstract string MediaServerItemId(TMovie movie);
protected abstract string MediaServerEtag(TMovie movie); protected abstract string MediaServerEtag(TMovie movie);
protected abstract Task<Either<BaseError, List<TMovie>>> GetMovieLibraryItems( protected abstract Task<Either<BaseError, int>> CountMovieLibraryItems(
TConnectionParameters connectionParameters,
TLibrary library);
protected abstract IAsyncEnumerable<TMovie> GetMovieLibraryItems(
TConnectionParameters connectionParameters, TConnectionParameters connectionParameters,
TLibrary library); TLibrary library);

10
ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs

@ -89,7 +89,15 @@ public class PlexMovieLibraryScanner :
protected override string MediaServerEtag(PlexMovie movie) => movie.Etag; protected override string MediaServerEtag(PlexMovie movie) => movie.Etag;
protected override Task<Either<BaseError, List<PlexMovie>>> GetMovieLibraryItems( protected override Task<Either<BaseError, int>> CountMovieLibraryItems(
PlexConnectionParameters connectionParameters,
PlexLibrary library)
=> _plexServerApiClient.GetLibraryItemCount(
library,
connectionParameters.Connection,
connectionParameters.Token);
protected override IAsyncEnumerable<PlexMovie> GetMovieLibraryItems(
PlexConnectionParameters connectionParameters, PlexConnectionParameters connectionParameters,
PlexLibrary library) => PlexLibrary library) =>
_plexServerApiClient.GetMovieLibraryContents( _plexServerApiClient.GetMovieLibraryContents(

56
ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs

@ -71,24 +71,29 @@ public class EmbyApiClient : IEmbyApiClient
} }
} }
public async Task<Either<BaseError, List<EmbyMovie>>> GetMovieLibraryItems( public async IAsyncEnumerable<EmbyMovie> GetMovieLibraryItems(string address, string apiKey, EmbyLibrary library)
string address,
string apiKey,
EmbyLibrary library)
{ {
try IEmbyApi service = RestService.For<IEmbyApi>(address);
{ int size = await service
IEmbyApi service = RestService.For<IEmbyApi>(address); .GetLibraryStats(apiKey, library.ItemId, EmbyItemType.Movie)
EmbyLibraryItemsResponse items = await service.GetMovieLibraryItems(apiKey, library.ItemId); .Map(r => r.TotalRecordCount);
return items.Items
.Map(i => ProjectToMovie(library, i)) const int PAGE_SIZE = 10;
.Somes()
.ToList(); int pages = (size - 1) / PAGE_SIZE + 1;
}
catch (Exception ex) for (var i = 0; i < pages; i++)
{ {
_logger.LogError(ex, "Error getting emby movie library items"); int skip = i * PAGE_SIZE;
return BaseError.New(ex.Message);
Task<IEnumerable<EmbyMovie>> 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<Either<BaseError, int>> GetLibraryItemCount(
string address,
string apiKey,
EmbyLibrary library,
string includeItemTypes)
{
try
{
IEmbyApi service = RestService.For<IEmbyApi>(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<EmbyCollection> ProjectToCollection(EmbyLibraryItemResponse item) private Option<EmbyCollection> ProjectToCollection(EmbyLibraryItemResponse item)
{ {
try try

21
ErsatzTV.Infrastructure/Emby/IEmbyApi.cs

@ -17,6 +17,21 @@ public interface IEmbyApi
[Header("X-Emby-Token")] [Header("X-Emby-Token")]
string apiKey); string apiKey);
[Get("/Items")]
public Task<EmbyLibraryItemsResponse> 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")] [Get("/Items")]
public Task<EmbyLibraryItemsResponse> GetMovieLibraryItems( public Task<EmbyLibraryItemsResponse> GetMovieLibraryItems(
[Header("X-Emby-Token")] [Header("X-Emby-Token")]
@ -29,7 +44,11 @@ public interface IEmbyApi
[Query] [Query]
string includeItemTypes = "Movie", string includeItemTypes = "Movie",
[Query] [Query]
bool recursive = true); bool recursive = true,
[Query]
int startIndex = 0,
[Query]
int limit = 0);
[Get("/Items")] [Get("/Items")]
public Task<EmbyLibraryItemsResponse> GetShowLibraryItems( public Task<EmbyLibraryItemsResponse> GetShowLibraryItems(

1
ErsatzTV.Infrastructure/Emby/Models/EmbyLibraryItemsResponse.cs

@ -3,4 +3,5 @@
public class EmbyLibraryItemsResponse public class EmbyLibraryItemsResponse
{ {
public List<EmbyLibraryItemResponse> Items { get; set; } public List<EmbyLibraryItemResponse> Items { get; set; }
public int TotalRecordCount { get; set; }
} }

25
ErsatzTV.Infrastructure/Jellyfin/IJellyfinApi.cs

@ -23,6 +23,25 @@ public interface IJellyfinApi
string apiKey); string apiKey);
[Get("/Items")] [Get("/Items")]
public Task<JellyfinLibraryItemsResponse> 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<JellyfinLibraryItemsResponse> GetMovieLibraryItems( public Task<JellyfinLibraryItemsResponse> GetMovieLibraryItems(
[Header("X-Emby-Token")] [Header("X-Emby-Token")]
string apiKey, string apiKey,
@ -38,7 +57,11 @@ public interface IJellyfinApi
[Query] [Query]
bool recursive = true, bool recursive = true,
[Query] [Query]
string filters = "IsNotFolder"); string filters = "IsNotFolder",
[Query]
int startIndex = 0,
[Query]
int limit = 0);
[Get("/Items")] [Get("/Items")]
public Task<JellyfinLibraryItemsResponse> GetShowLibraryItems( public Task<JellyfinLibraryItemsResponse> GetShowLibraryItems(

66
ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs

@ -91,29 +91,35 @@ public class JellyfinApiClient : IJellyfinApiClient
} }
} }
public async Task<Either<BaseError, List<JellyfinMovie>>> GetMovieLibraryItems( public async IAsyncEnumerable<JellyfinMovie> GetMovieLibraryItems(
string address, string address,
string apiKey, string apiKey,
JellyfinLibrary library) 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<IJellyfinApi>(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<IJellyfinApi>(address); int skip = i * PAGE_SIZE;
JellyfinLibraryItemsResponse items = await service.GetMovieLibraryItems(apiKey, userId, library.ItemId);
return items.Items
.Map(i => ProjectToMovie(library, i))
.Somes()
.ToList();
}
return BaseError.New("Jellyfin admin user id is not available"); Task<IEnumerable<JellyfinMovie>> result = service
} .GetMovieLibraryItems(apiKey, userId, library.ItemId, startIndex: skip, limit: PAGE_SIZE)
catch (Exception ex) .Map(items => items.Items.Map(item => ProjectToMovie(library, item)).Somes());
{
_logger.LogError(ex, "Error getting jellyfin movie library items"); foreach (JellyfinMovie movie in await result)
return BaseError.New(ex.Message); {
yield return movie;
}
}
} }
} }
@ -262,6 +268,34 @@ public class JellyfinApiClient : IJellyfinApiClient
} }
} }
public async Task<Either<BaseError, int>> 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<IJellyfinApi>(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<MediaItem> ProjectToCollectionMediaItem(JellyfinLibraryItemResponse item) private Option<MediaItem> ProjectToCollectionMediaItem(JellyfinLibraryItemResponse item)
{ {
try try

1
ErsatzTV.Infrastructure/Jellyfin/Models/JellyfinLibraryItemsResponse.cs

@ -3,4 +3,5 @@
public class JellyfinLibraryItemsResponse public class JellyfinLibraryItemsResponse
{ {
public List<JellyfinLibraryItemResponse> Items { get; set; } public List<JellyfinLibraryItemResponse> Items { get; set; }
public int TotalRecordCount { get; set; }
} }

19
ErsatzTV.Infrastructure/Plex/IPlexServerApi.cs

@ -17,11 +17,30 @@ public interface IPlexServerApi
[Query] [AliasAs("X-Plex-Token")] [Query] [AliasAs("X-Plex-Token")]
string token); string token);
[Get("/library/sections/{key}/all?X-Plex-Container-Start=0&X-Plex-Container-Size=0")]
[Headers("Accept: text/xml")]
public Task<PlexXmlMediaContainerStatsResponse> GetLibrarySection(
string key,
[Query] [AliasAs("X-Plex-Token")]
string token);
[Get("/library/sections/{key}/all")]
[Headers("Accept: application/json")]
public Task<PlexMediaContainerResponse<PlexMediaContainerMetadataContent<PlexMetadataResponse>>>
GetLibrarySectionContents(
string key,
[Query] [AliasAs("X-Plex-Token")]
string token);
[Get("/library/sections/{key}/all")] [Get("/library/sections/{key}/all")]
[Headers("Accept: application/json")] [Headers("Accept: application/json")]
public Task<PlexMediaContainerResponse<PlexMediaContainerMetadataContent<PlexMetadataResponse>>> public Task<PlexMediaContainerResponse<PlexMediaContainerMetadataContent<PlexMetadataResponse>>>
GetLibrarySectionContents( GetLibrarySectionContents(
string key, string key,
[Query] [AliasAs("X-Plex-Container-Start")]
int skip,
[Query] [AliasAs("X-Plex-Container-Size")]
int take,
[Query] [AliasAs("X-Plex-Token")] [Query] [AliasAs("X-Plex-Token")]
string token); string token);

7
ErsatzTV.Infrastructure/Plex/Models/PlexMediaContainerResponse.cs

@ -17,6 +17,13 @@ public class PlexMediaContainerMetadataContent<T>
public List<T> Metadata { get; set; } public List<T> Metadata { get; set; }
} }
[XmlRoot("MediaContainer", Namespace = null)]
public class PlexXmlMediaContainerStatsResponse
{
[XmlAttribute("totalSize")]
public int TotalSize { get; set; }
}
[XmlRoot("MediaContainer", Namespace = null)] [XmlRoot("MediaContainer", Namespace = null)]
public class PlexXmlVideoMetadataResponseContainer public class PlexXmlVideoMetadataResponseContainer
{ {

50
ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs

@ -26,9 +26,7 @@ public class PlexServerApiClient : IPlexServerApiClient
_logger = logger; _logger = logger;
} }
public async Task<bool> Ping( public async Task<bool> Ping(PlexConnection connection, PlexServerAuthToken token)
PlexConnection connection,
PlexServerAuthToken token)
{ {
try try
{ {
@ -76,21 +74,32 @@ public class PlexServerApiClient : IPlexServerApiClient
} }
} }
public async Task<Either<BaseError, List<PlexMovie>>> GetMovieLibraryContents( public async IAsyncEnumerable<PlexMovie> GetMovieLibraryContents(
PlexLibrary library, PlexLibrary library,
PlexConnection connection, PlexConnection connection,
PlexServerAuthToken token) 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<IPlexServerApi>(connection.Uri);
int pages = (size - 1) / PAGE_SIZE + 1;
for (var i = 0; i < pages; i++)
{ {
IPlexServerApi service = RestService.For<IPlexServerApi>(connection.Uri); int skip = i * PAGE_SIZE;
return await service.GetLibrarySectionContents(library.Key, token.AuthToken)
Task<IEnumerable<PlexMovie>> 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(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()); .Map(list => list.Map(metadata => ProjectToMovie(metadata, library.MediaSourceId)));
}
catch (Exception ex) foreach (PlexMovie movie in await result)
{ {
return BaseError.New(ex.ToString()); yield return movie;
}
} }
} }
@ -254,6 +263,23 @@ public class PlexServerApiClient : IPlexServerApiClient
} }
} }
public async Task<Either<BaseError, int>> 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<PlexEpisode> ProcessMultiEpisodeFiles(IEnumerable<PlexEpisode> episodes) private List<PlexEpisode> ProcessMultiEpisodeFiles(IEnumerable<PlexEpisode> episodes)
{ {
// add all metadata from duplicate paths to first entry with given path // add all metadata from duplicate paths to first entry with given path

Loading…
Cancel
Save