Browse Source

don't request people from jellyfin by default

pull/2747/head
Jason Dove 7 months ago
parent
commit
a40ada86a8
No known key found for this signature in database
  1. 2
      CHANGELOG.md
  2. 16
      ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinApiClient.cs
  3. 39
      ErsatzTV.Infrastructure/Jellyfin/IJellyfinApi.cs
  4. 53
      ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs
  5. 3
      ErsatzTV.Scanner/Core/Emby/EmbyTelevisionLibraryScanner.cs
  6. 218
      ErsatzTV.Scanner/Core/Jellyfin/JellyfinTelevisionLibraryScanner.cs
  7. 5
      ErsatzTV.Scanner/Core/Metadata/MediaServerTelevisionLibraryScanner.cs
  8. 3
      ErsatzTV.Scanner/Core/Plex/PlexTelevisionLibraryScanner.cs

2
CHANGELOG.md

@ -82,6 +82,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Hardware acceleration will now be used - Hardware acceleration will now be used
- Items can "work ahead" (transcode faster than realtime) when less than 3 minutes in duration - Items can "work ahead" (transcode faster than realtime) when less than 3 minutes in duration
- Optimize Jellyfin database fields and indexes - Optimize Jellyfin database fields and indexes
- Optimize Jellyfin show library scans by only requesting `People` (actors, directors, writers) when etags don't match
- This should significantly speed up periodic library scans, particularly against Jellyfin 10.11.x
## [25.9.0] - 2025-11-29 ## [25.9.0] - 2025-11-29
### Added ### Added

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

@ -13,7 +13,7 @@ public interface IJellyfinApiClient
string apiKey, string apiKey,
JellyfinLibrary library); JellyfinLibrary library);
IAsyncEnumerable<Tuple<JellyfinShow, int>> GetShowLibraryItems( IAsyncEnumerable<Tuple<JellyfinShow, int>> GetShowLibraryItemsWithoutPeople(
string address, string address,
string apiKey, string apiKey,
JellyfinLibrary library); JellyfinLibrary library);
@ -30,6 +30,12 @@ public interface IJellyfinApiClient
JellyfinLibrary library, JellyfinLibrary library,
string seasonId); string seasonId);
IAsyncEnumerable<Tuple<JellyfinEpisode, int>> GetEpisodeLibraryItemsWithoutPeople(
string address,
string apiKey,
JellyfinLibrary library,
string seasonId);
IAsyncEnumerable<Tuple<JellyfinCollection, int>> GetCollectionLibraryItems( IAsyncEnumerable<Tuple<JellyfinCollection, int>> GetCollectionLibraryItems(
string address, string address,
string apiKey, string apiKey,
@ -58,4 +64,12 @@ public interface IJellyfinApiClient
string apiKey, string apiKey,
JellyfinLibrary library, JellyfinLibrary library,
string showTitle); string showTitle);
Task<Either<BaseError, Option<JellyfinEpisode>>> GetSingleEpisode(
string address,
string apiKey,
JellyfinLibrary library,
string seasonId,
string episodeId);
} }

39
ErsatzTV.Infrastructure/Jellyfin/IJellyfinApi.cs

@ -42,6 +42,26 @@ public interface IJellyfinApi
[Query] [Query]
int limit = 0); int limit = 0);
[Get("/Items?sortOrder=Ascending&sortBy=SortName")]
Task<JellyfinLibraryItemsResponse> GetShowLibraryItemsWithoutPeople(
[Header("X-Emby-Token")]
string apiKey,
[Query]
string parentId,
[Query]
string fields =
"Path,Genres,Tags,DateCreated,Etag,Overview,Taglines,Studios,OfficialRating,ProviderIds",
[Query]
string includeItemTypes = "Series",
[Query]
bool recursive = true,
[Query]
int startIndex = 0,
[Query]
int limit = 0,
[Query]
string ids = null);
[Get("/Items?sortOrder=Ascending&sortBy=SortName")] [Get("/Items?sortOrder=Ascending&sortBy=SortName")]
Task<JellyfinLibraryItemsResponse> GetShowLibraryItems( Task<JellyfinLibraryItemsResponse> GetShowLibraryItems(
[Header("X-Emby-Token")] [Header("X-Emby-Token")]
@ -94,6 +114,25 @@ public interface IJellyfinApi
[Query] [Query]
int startIndex = 0, int startIndex = 0,
[Query] [Query]
int limit = 0,
[Query]
string ids = null);
[Get("/Items?sortOrder=Ascending&sortBy=SortName")]
Task<JellyfinLibraryItemsResponse> GetEpisodeLibraryItemsWithoutPeople(
[Header("X-Emby-Token")]
string apiKey,
[Query]
string parentId,
[Query]
string fields = "Path,Genres,Tags,DateCreated,Etag,Overview,ProviderIds,Chapters",
[Query]
string includeItemTypes = "Episode",
[Query]
bool recursive = true,
[Query]
int startIndex = 0,
[Query]
int limit = 0); int limit = 0);
[Get("/Items?sortOrder=Ascending&sortBy=SortName")] [Get("/Items?sortOrder=Ascending&sortBy=SortName")]

53
ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs

@ -92,7 +92,7 @@ public class JellyfinApiClient : IJellyfinApiClient
limit: pageSize), limit: pageSize),
(maybeLibrary, item) => maybeLibrary.Map(lib => ProjectToMovie(lib, item)).Flatten()); (maybeLibrary, item) => maybeLibrary.Map(lib => ProjectToMovie(lib, item)).Flatten());
public IAsyncEnumerable<Tuple<JellyfinShow, int>> GetShowLibraryItems( public IAsyncEnumerable<Tuple<JellyfinShow, int>> GetShowLibraryItemsWithoutPeople(
string address, string address,
string apiKey, string apiKey,
JellyfinLibrary library) => JellyfinLibrary library) =>
@ -101,7 +101,7 @@ public class JellyfinApiClient : IJellyfinApiClient
library, library,
library.MediaSourceId, library.MediaSourceId,
library.ItemId, library.ItemId,
(service, itemId, skip, pageSize) => service.GetShowLibraryItems( (service, itemId, skip, pageSize) => service.GetShowLibraryItemsWithoutPeople(
apiKey, apiKey,
itemId, itemId,
startIndex: skip, startIndex: skip,
@ -142,6 +142,23 @@ public class JellyfinApiClient : IJellyfinApiClient
limit: pageSize), limit: pageSize),
(maybeLibrary, item) => maybeLibrary.Map(lib => ProjectToEpisode(lib, item)).Flatten()); (maybeLibrary, item) => maybeLibrary.Map(lib => ProjectToEpisode(lib, item)).Flatten());
public IAsyncEnumerable<Tuple<JellyfinEpisode, int>> GetEpisodeLibraryItemsWithoutPeople(
string address,
string apiKey,
JellyfinLibrary library,
string seasonId) =>
GetPagedLibraryItems(
address,
library,
library.MediaSourceId,
seasonId,
(service, _, skip, pageSize) => service.GetEpisodeLibraryItemsWithoutPeople(
apiKey,
seasonId,
startIndex: skip,
limit: pageSize),
(maybeLibrary, item) => maybeLibrary.Map(lib => ProjectToEpisode(lib, item)).Flatten());
public IAsyncEnumerable<Tuple<JellyfinCollection, int>> GetCollectionLibraryItems( public IAsyncEnumerable<Tuple<JellyfinCollection, int>> GetCollectionLibraryItems(
string address, string address,
string apiKey, string apiKey,
@ -284,6 +301,38 @@ public class JellyfinApiClient : IJellyfinApiClient
} }
} }
public async Task<Either<BaseError, Option<JellyfinEpisode>>> GetSingleEpisode(
string address,
string apiKey,
JellyfinLibrary library,
string seasonId,
string episodeId)
{
try
{
IJellyfinApi service = ServiceForAddress(address);
JellyfinLibraryItemsResponse itemsResponse = await service.GetEpisodeLibraryItems(
apiKey,
parentId: seasonId,
recursive: false,
startIndex: 0,
limit: 1,
ids: episodeId);
foreach (JellyfinLibraryItemResponse item in itemsResponse.Items)
{
return ProjectToEpisode(library, item);
}
return BaseError.New($"Unable to locate episode with id {episodeId}");
}
catch (Exception ex)
{
_logger.LogError(ex, "Error searching Jellyfin episodes by id");
return BaseError.New(ex.Message);
}
}
private async IAsyncEnumerable<Tuple<TItem, int>> GetPagedLibraryItems<TItem>( private async IAsyncEnumerable<Tuple<TItem, int>> GetPagedLibraryItems<TItem>(
string address, string address,
Option<JellyfinLibrary> maybeLibrary, Option<JellyfinLibrary> maybeLibrary,

3
ErsatzTV.Scanner/Core/Emby/EmbyTelevisionLibraryScanner.cs

@ -158,7 +158,8 @@ public class EmbyTelevisionLibraryScanner : MediaServerTelevisionLibraryScanner<
EmbyLibrary library, EmbyLibrary library,
EmbyConnectionParameters connectionParameters, EmbyConnectionParameters connectionParameters,
EmbyShow show, EmbyShow show,
EmbySeason season) => EmbySeason season,
bool isNewSeason) =>
_embyApiClient.GetEpisodeLibraryItems( _embyApiClient.GetEpisodeLibraryItems(
connectionParameters.Address, connectionParameters.Address,
connectionParameters.ApiKey, connectionParameters.ApiKey,

218
ErsatzTV.Scanner/Core/Jellyfin/JellyfinTelevisionLibraryScanner.cs

@ -23,13 +23,16 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
private readonly ILogger<JellyfinTelevisionLibraryScanner> _logger; private readonly ILogger<JellyfinTelevisionLibraryScanner> _logger;
private readonly IMediaSourceRepository _mediaSourceRepository; private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IJellyfinPathReplacementService _pathReplacementService; private readonly IJellyfinPathReplacementService _pathReplacementService;
private readonly IJellyfinTelevisionRepository _televisionRepository; private readonly IMetadataRepository _metadataRepository;
private readonly IJellyfinTelevisionRepository _jellyfinTelevisionRepository;
private readonly ITelevisionRepository _televisionRepository;
public JellyfinTelevisionLibraryScanner( public JellyfinTelevisionLibraryScanner(
IScannerProxy scannerProxy, IScannerProxy scannerProxy,
IJellyfinApiClient jellyfinApiClient, IJellyfinApiClient jellyfinApiClient,
IMediaSourceRepository mediaSourceRepository, IMediaSourceRepository mediaSourceRepository,
IJellyfinTelevisionRepository televisionRepository, IJellyfinTelevisionRepository jellyfinTelevisionRepository,
ITelevisionRepository televisionRepository,
IJellyfinPathReplacementService pathReplacementService, IJellyfinPathReplacementService pathReplacementService,
IFileSystem fileSystem, IFileSystem fileSystem,
ILocalChaptersProvider localChaptersProvider, ILocalChaptersProvider localChaptersProvider,
@ -44,8 +47,10 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
{ {
_jellyfinApiClient = jellyfinApiClient; _jellyfinApiClient = jellyfinApiClient;
_mediaSourceRepository = mediaSourceRepository; _mediaSourceRepository = mediaSourceRepository;
_jellyfinTelevisionRepository = jellyfinTelevisionRepository;
_televisionRepository = televisionRepository; _televisionRepository = televisionRepository;
_pathReplacementService = pathReplacementService; _pathReplacementService = pathReplacementService;
_metadataRepository = metadataRepository;
_logger = logger; _logger = logger;
} }
@ -70,7 +75,7 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
} }
return await ScanLibrary( return await ScanLibrary(
_televisionRepository, _jellyfinTelevisionRepository,
new JellyfinConnectionParameters(address, apiKey, library.MediaSourceId), new JellyfinConnectionParameters(address, apiKey, library.MediaSourceId),
library, library,
GetLocalPath, GetLocalPath,
@ -116,7 +121,7 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
show.ItemId); show.ItemId);
return await ScanSingleShowInternal( return await ScanSingleShowInternal(
_televisionRepository, _jellyfinTelevisionRepository,
new JellyfinConnectionParameters(address, apiKey, library.MediaSourceId), new JellyfinConnectionParameters(address, apiKey, library.MediaSourceId),
library, library,
show, show,
@ -135,7 +140,10 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
protected override IAsyncEnumerable<Tuple<JellyfinShow, int>> GetShowLibraryItems( protected override IAsyncEnumerable<Tuple<JellyfinShow, int>> GetShowLibraryItems(
JellyfinConnectionParameters connectionParameters, JellyfinConnectionParameters connectionParameters,
JellyfinLibrary library) => JellyfinLibrary library) =>
_jellyfinApiClient.GetShowLibraryItems(connectionParameters.Address, connectionParameters.ApiKey, library); _jellyfinApiClient.GetShowLibraryItemsWithoutPeople(
connectionParameters.Address,
connectionParameters.ApiKey,
library);
protected override string MediaServerItemId(JellyfinShow show) => show.ItemId; protected override string MediaServerItemId(JellyfinShow show) => show.ItemId;
protected override string MediaServerItemId(JellyfinSeason season) => season.ItemId; protected override string MediaServerItemId(JellyfinSeason season) => season.ItemId;
@ -159,20 +167,56 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
JellyfinLibrary library, JellyfinLibrary library,
JellyfinConnectionParameters connectionParameters, JellyfinConnectionParameters connectionParameters,
JellyfinShow show, JellyfinShow show,
JellyfinSeason season) => JellyfinSeason season,
_jellyfinApiClient.GetEpisodeLibraryItems( bool isNewSeason)
{
if (isNewSeason)
{
return _jellyfinApiClient.GetEpisodeLibraryItems(
connectionParameters.Address,
connectionParameters.ApiKey,
library,
season.ItemId);
}
return _jellyfinApiClient.GetEpisodeLibraryItemsWithoutPeople(
connectionParameters.Address, connectionParameters.Address,
connectionParameters.ApiKey, connectionParameters.ApiKey,
library, library,
season.ItemId); season.ItemId);
}
protected override Task<Option<ShowMetadata>> GetFullMetadata( protected override async Task<Option<ShowMetadata>> GetFullMetadata(
JellyfinConnectionParameters connectionParameters, JellyfinConnectionParameters connectionParameters,
JellyfinLibrary library, JellyfinLibrary library,
MediaItemScanResult<JellyfinShow> result, MediaItemScanResult<JellyfinShow> result,
JellyfinShow incoming, JellyfinShow incoming,
bool deepScan) => bool deepScan)
Task.FromResult(Option<ShowMetadata>.None); {
if (result.IsAdded || result.Item.Etag != incoming.Etag || deepScan)
{
Either<BaseError, Option<JellyfinShow>> maybeShowResult = await _jellyfinApiClient.GetSingleShow(
connectionParameters.Address,
connectionParameters.ApiKey,
library,
incoming.ItemId);
foreach (BaseError error in maybeShowResult.LeftToSeq())
{
_logger.LogWarning("Failed to get show metadata from Jellyfin: {Error}", error.ToString());
}
foreach (Option<JellyfinShow> maybeShow in maybeShowResult.RightToSeq())
{
foreach (JellyfinShow show in maybeShow)
{
return show.ShowMetadata.HeadOrNone();
}
}
}
return None;
}
protected override Task<Option<SeasonMetadata>> GetFullMetadata( protected override Task<Option<SeasonMetadata>> GetFullMetadata(
JellyfinConnectionParameters connectionParameters, JellyfinConnectionParameters connectionParameters,
@ -182,13 +226,39 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
bool deepScan) => bool deepScan) =>
Task.FromResult(Option<SeasonMetadata>.None); Task.FromResult(Option<SeasonMetadata>.None);
protected override Task<Option<EpisodeMetadata>> GetFullMetadata( protected override async Task<Option<EpisodeMetadata>> GetFullMetadata(
JellyfinConnectionParameters connectionParameters, JellyfinConnectionParameters connectionParameters,
JellyfinLibrary library, JellyfinLibrary library,
MediaItemScanResult<JellyfinEpisode> result, MediaItemScanResult<JellyfinEpisode> result,
JellyfinEpisode incoming, JellyfinEpisode incoming,
bool deepScan) => bool deepScan)
Task.FromResult(Option<EpisodeMetadata>.None); {
if (result.Item.Season is JellyfinSeason jellyfinSeason &&
(result.IsAdded || result.Item.Etag != incoming.Etag || deepScan))
{
Either<BaseError, Option<JellyfinEpisode>> maybeEpisodeResult = await _jellyfinApiClient.GetSingleEpisode(
connectionParameters.Address,
connectionParameters.ApiKey,
library,
jellyfinSeason.ItemId,
incoming.ItemId);
foreach (BaseError error in maybeEpisodeResult.LeftToSeq())
{
_logger.LogWarning("Failed to get episode metadata from Jellyfin: {Error}", error.ToString());
}
foreach (Option<JellyfinEpisode> maybeEpisode in maybeEpisodeResult.RightToSeq())
{
foreach (JellyfinEpisode episode in maybeEpisode)
{
return episode.EpisodeMetadata.HeadOrNone();
}
}
}
return None;
}
protected override Task<Option<Tuple<EpisodeMetadata, MediaVersion>>> GetFullMetadataAndStatistics( protected override Task<Option<Tuple<EpisodeMetadata, MediaVersion>>> GetFullMetadataAndStatistics(
JellyfinConnectionParameters connectionParameters, JellyfinConnectionParameters connectionParameters,
@ -225,21 +295,131 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
return maybeVersion.ToOption(); return maybeVersion.ToOption();
} }
protected override Task<Either<BaseError, MediaItemScanResult<JellyfinShow>>> UpdateMetadata( protected override async Task<Either<BaseError, MediaItemScanResult<JellyfinShow>>> UpdateMetadata(
MediaItemScanResult<JellyfinShow> result, MediaItemScanResult<JellyfinShow> result,
ShowMetadata fullMetadata) => ShowMetadata fullMetadata)
Task.FromResult<Either<BaseError, MediaItemScanResult<JellyfinShow>>>(result); {
JellyfinShow existing = result.Item;
ShowMetadata existingMetadata = existing.ShowMetadata.Head();
foreach (Actor actor in existingMetadata.Actors
.Filter(a =>
fullMetadata.Actors.All(a2 => a2.Name != a.Name || a.Artwork == null && a2.Artwork != null))
.ToList())
{
existingMetadata.Actors.Remove(actor);
if (await _metadataRepository.RemoveActor(actor))
{
result.IsUpdated = true;
}
}
foreach (Actor actor in fullMetadata.Actors
.Filter(a => existingMetadata.Actors.All(a2 => a2.Name != a.Name))
.ToList())
{
existingMetadata.Actors.Add(actor);
if (await _televisionRepository.AddActor(existingMetadata, actor))
{
result.IsUpdated = true;
}
}
if (result.IsUpdated)
{
await _metadataRepository.MarkAsUpdated(existingMetadata, fullMetadata.DateUpdated);
}
return result;
}
protected override Task<Either<BaseError, MediaItemScanResult<JellyfinSeason>>> UpdateMetadata( protected override Task<Either<BaseError, MediaItemScanResult<JellyfinSeason>>> UpdateMetadata(
MediaItemScanResult<JellyfinSeason> result, MediaItemScanResult<JellyfinSeason> result,
SeasonMetadata fullMetadata) => SeasonMetadata fullMetadata) =>
Task.FromResult<Either<BaseError, MediaItemScanResult<JellyfinSeason>>>(result); Task.FromResult<Either<BaseError, MediaItemScanResult<JellyfinSeason>>>(result);
protected override Task<Either<BaseError, MediaItemScanResult<JellyfinEpisode>>> UpdateMetadata( protected override async Task<Either<BaseError, MediaItemScanResult<JellyfinEpisode>>> UpdateMetadata(
MediaItemScanResult<JellyfinEpisode> result, MediaItemScanResult<JellyfinEpisode> result,
EpisodeMetadata fullMetadata, EpisodeMetadata fullMetadata,
CancellationToken cancellationToken) => CancellationToken cancellationToken)
Task.FromResult<Either<BaseError, MediaItemScanResult<JellyfinEpisode>>>(result); {
JellyfinEpisode existing = result.Item;
EpisodeMetadata existingMetadata = existing.EpisodeMetadata.Head();
foreach (Actor actor in existingMetadata.Actors
.Filter(a =>
fullMetadata.Actors.All(a2 => a2.Name != a.Name || a.Artwork == null && a2.Artwork != null))
.ToList())
{
existingMetadata.Actors.Remove(actor);
if (await _metadataRepository.RemoveActor(actor))
{
result.IsUpdated = true;
}
}
foreach (Actor actor in fullMetadata.Actors
.Filter(a => existingMetadata.Actors.All(a2 => a2.Name != a.Name))
.ToList())
{
existingMetadata.Actors.Add(actor);
if (await _televisionRepository.AddActor(existingMetadata, actor))
{
result.IsUpdated = true;
}
}
foreach (Director director in existingMetadata.Directors
.Filter(g => fullMetadata.Directors.All(g2 => g2.Name != g.Name))
.ToList())
{
existingMetadata.Directors.Remove(director);
if (await _metadataRepository.RemoveDirector(director))
{
result.IsUpdated = true;
}
}
foreach (Director director in fullMetadata.Directors
.Filter(g => existingMetadata.Directors.All(g2 => g2.Name != g.Name))
.ToList())
{
existingMetadata.Directors.Add(director);
if (await _televisionRepository.AddDirector(existingMetadata, director))
{
result.IsUpdated = true;
}
}
foreach (Writer writer in existingMetadata.Writers
.Filter(g => fullMetadata.Writers.All(g2 => g2.Name != g.Name))
.ToList())
{
existingMetadata.Writers.Remove(writer);
if (await _metadataRepository.RemoveWriter(writer))
{
result.IsUpdated = true;
}
}
foreach (Writer writer in fullMetadata.Writers
.Filter(g => existingMetadata.Writers.All(g2 => g2.Name != g.Name))
.ToList())
{
existingMetadata.Writers.Add(writer);
if (await _televisionRepository.AddWriter(existingMetadata, writer))
{
result.IsUpdated = true;
}
}
if (result.IsUpdated)
{
await _metadataRepository.MarkAsUpdated(existingMetadata, fullMetadata.DateUpdated);
}
return result;
}
private async Task<Either<BaseError, Unit>> ScanSingleShowInternal( private async Task<Either<BaseError, Unit>> ScanSingleShowInternal(
IJellyfinTelevisionRepository televisionRepository, IJellyfinTelevisionRepository televisionRepository,

5
ErsatzTV.Scanner/Core/Metadata/MediaServerTelevisionLibraryScanner.cs

@ -220,7 +220,8 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
TLibrary library, TLibrary library,
TConnectionParameters connectionParameters, TConnectionParameters connectionParameters,
TShow show, TShow show,
TSeason season); TSeason season,
bool isNewSeason);
protected abstract Task<Option<ShowMetadata>> GetFullMetadata( protected abstract Task<Option<ShowMetadata>> GetFullMetadata(
TConnectionParameters connectionParameters, TConnectionParameters connectionParameters,
@ -321,7 +322,7 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
showIsUpdated, showIsUpdated,
result.Item, result.Item,
connectionParameters, connectionParameters,
GetEpisodeLibraryItems(library, connectionParameters, show, result.Item), GetEpisodeLibraryItems(library, connectionParameters, show, result.Item, result.IsAdded),
deepScan, deepScan,
cancellationToken); cancellationToken);

3
ErsatzTV.Scanner/Core/Plex/PlexTelevisionLibraryScanner.cs

@ -252,7 +252,8 @@ public partial class PlexTelevisionLibraryScanner :
PlexLibrary library, PlexLibrary library,
PlexConnectionParameters connectionParameters, PlexConnectionParameters connectionParameters,
PlexShow show, PlexShow show,
PlexSeason season) => PlexSeason season,
bool isNewSeason) =>
_plexServerApiClient.GetSeasonEpisodes( _plexServerApiClient.GetSeasonEpisodes(
library, library,
season, season,

Loading…
Cancel
Save