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/). @@ -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
- Items can "work ahead" (transcode faster than realtime) when less than 3 minutes in duration
- 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
### Added

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

@ -13,7 +13,7 @@ public interface IJellyfinApiClient @@ -13,7 +13,7 @@ public interface IJellyfinApiClient
string apiKey,
JellyfinLibrary library);
IAsyncEnumerable<Tuple<JellyfinShow, int>> GetShowLibraryItems(
IAsyncEnumerable<Tuple<JellyfinShow, int>> GetShowLibraryItemsWithoutPeople(
string address,
string apiKey,
JellyfinLibrary library);
@ -30,6 +30,12 @@ public interface IJellyfinApiClient @@ -30,6 +30,12 @@ public interface IJellyfinApiClient
JellyfinLibrary library,
string seasonId);
IAsyncEnumerable<Tuple<JellyfinEpisode, int>> GetEpisodeLibraryItemsWithoutPeople(
string address,
string apiKey,
JellyfinLibrary library,
string seasonId);
IAsyncEnumerable<Tuple<JellyfinCollection, int>> GetCollectionLibraryItems(
string address,
string apiKey,
@ -58,4 +64,12 @@ public interface IJellyfinApiClient @@ -58,4 +64,12 @@ public interface IJellyfinApiClient
string apiKey,
JellyfinLibrary library,
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 @@ -42,6 +42,26 @@ public interface IJellyfinApi
[Query]
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")]
Task<JellyfinLibraryItemsResponse> GetShowLibraryItems(
[Header("X-Emby-Token")]
@ -94,6 +114,25 @@ public interface IJellyfinApi @@ -94,6 +114,25 @@ public interface IJellyfinApi
[Query]
int startIndex = 0,
[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);
[Get("/Items?sortOrder=Ascending&sortBy=SortName")]

53
ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs

@ -92,7 +92,7 @@ public class JellyfinApiClient : IJellyfinApiClient @@ -92,7 +92,7 @@ public class JellyfinApiClient : IJellyfinApiClient
limit: pageSize),
(maybeLibrary, item) => maybeLibrary.Map(lib => ProjectToMovie(lib, item)).Flatten());
public IAsyncEnumerable<Tuple<JellyfinShow, int>> GetShowLibraryItems(
public IAsyncEnumerable<Tuple<JellyfinShow, int>> GetShowLibraryItemsWithoutPeople(
string address,
string apiKey,
JellyfinLibrary library) =>
@ -101,7 +101,7 @@ public class JellyfinApiClient : IJellyfinApiClient @@ -101,7 +101,7 @@ public class JellyfinApiClient : IJellyfinApiClient
library,
library.MediaSourceId,
library.ItemId,
(service, itemId, skip, pageSize) => service.GetShowLibraryItems(
(service, itemId, skip, pageSize) => service.GetShowLibraryItemsWithoutPeople(
apiKey,
itemId,
startIndex: skip,
@ -142,6 +142,23 @@ public class JellyfinApiClient : IJellyfinApiClient @@ -142,6 +142,23 @@ public class JellyfinApiClient : IJellyfinApiClient
limit: pageSize),
(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(
string address,
string apiKey,
@ -284,6 +301,38 @@ public class JellyfinApiClient : IJellyfinApiClient @@ -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>(
string address,
Option<JellyfinLibrary> maybeLibrary,

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

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

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

@ -23,13 +23,16 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan @@ -23,13 +23,16 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
private readonly ILogger<JellyfinTelevisionLibraryScanner> _logger;
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IJellyfinPathReplacementService _pathReplacementService;
private readonly IJellyfinTelevisionRepository _televisionRepository;
private readonly IMetadataRepository _metadataRepository;
private readonly IJellyfinTelevisionRepository _jellyfinTelevisionRepository;
private readonly ITelevisionRepository _televisionRepository;
public JellyfinTelevisionLibraryScanner(
IScannerProxy scannerProxy,
IJellyfinApiClient jellyfinApiClient,
IMediaSourceRepository mediaSourceRepository,
IJellyfinTelevisionRepository televisionRepository,
IJellyfinTelevisionRepository jellyfinTelevisionRepository,
ITelevisionRepository televisionRepository,
IJellyfinPathReplacementService pathReplacementService,
IFileSystem fileSystem,
ILocalChaptersProvider localChaptersProvider,
@ -44,8 +47,10 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan @@ -44,8 +47,10 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
{
_jellyfinApiClient = jellyfinApiClient;
_mediaSourceRepository = mediaSourceRepository;
_jellyfinTelevisionRepository = jellyfinTelevisionRepository;
_televisionRepository = televisionRepository;
_pathReplacementService = pathReplacementService;
_metadataRepository = metadataRepository;
_logger = logger;
}
@ -70,7 +75,7 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan @@ -70,7 +75,7 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
}
return await ScanLibrary(
_televisionRepository,
_jellyfinTelevisionRepository,
new JellyfinConnectionParameters(address, apiKey, library.MediaSourceId),
library,
GetLocalPath,
@ -116,7 +121,7 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan @@ -116,7 +121,7 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
show.ItemId);
return await ScanSingleShowInternal(
_televisionRepository,
_jellyfinTelevisionRepository,
new JellyfinConnectionParameters(address, apiKey, library.MediaSourceId),
library,
show,
@ -135,7 +140,10 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan @@ -135,7 +140,10 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
protected override IAsyncEnumerable<Tuple<JellyfinShow, int>> GetShowLibraryItems(
JellyfinConnectionParameters connectionParameters,
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(JellyfinSeason season) => season.ItemId;
@ -159,20 +167,56 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan @@ -159,20 +167,56 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
JellyfinLibrary library,
JellyfinConnectionParameters connectionParameters,
JellyfinShow show,
JellyfinSeason season) =>
_jellyfinApiClient.GetEpisodeLibraryItems(
JellyfinSeason season,
bool isNewSeason)
{
if (isNewSeason)
{
return _jellyfinApiClient.GetEpisodeLibraryItems(
connectionParameters.Address,
connectionParameters.ApiKey,
library,
season.ItemId);
}
protected override Task<Option<ShowMetadata>> GetFullMetadata(
return _jellyfinApiClient.GetEpisodeLibraryItemsWithoutPeople(
connectionParameters.Address,
connectionParameters.ApiKey,
library,
season.ItemId);
}
protected override async Task<Option<ShowMetadata>> GetFullMetadata(
JellyfinConnectionParameters connectionParameters,
JellyfinLibrary library,
MediaItemScanResult<JellyfinShow> result,
JellyfinShow incoming,
bool deepScan) =>
Task.FromResult(Option<ShowMetadata>.None);
bool deepScan)
{
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(
JellyfinConnectionParameters connectionParameters,
@ -182,13 +226,39 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan @@ -182,13 +226,39 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
bool deepScan) =>
Task.FromResult(Option<SeasonMetadata>.None);
protected override Task<Option<EpisodeMetadata>> GetFullMetadata(
protected override async Task<Option<EpisodeMetadata>> GetFullMetadata(
JellyfinConnectionParameters connectionParameters,
JellyfinLibrary library,
MediaItemScanResult<JellyfinEpisode> result,
JellyfinEpisode incoming,
bool deepScan) =>
Task.FromResult(Option<EpisodeMetadata>.None);
bool deepScan)
{
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(
JellyfinConnectionParameters connectionParameters,
@ -225,21 +295,131 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan @@ -225,21 +295,131 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
return maybeVersion.ToOption();
}
protected override Task<Either<BaseError, MediaItemScanResult<JellyfinShow>>> UpdateMetadata(
protected override async Task<Either<BaseError, MediaItemScanResult<JellyfinShow>>> UpdateMetadata(
MediaItemScanResult<JellyfinShow> result,
ShowMetadata fullMetadata) =>
Task.FromResult<Either<BaseError, MediaItemScanResult<JellyfinShow>>>(result);
ShowMetadata fullMetadata)
{
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(
MediaItemScanResult<JellyfinSeason> result,
SeasonMetadata fullMetadata) =>
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,
EpisodeMetadata fullMetadata,
CancellationToken cancellationToken) =>
Task.FromResult<Either<BaseError, MediaItemScanResult<JellyfinEpisode>>>(result);
CancellationToken cancellationToken)
{
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(
IJellyfinTelevisionRepository televisionRepository,

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

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

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

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

Loading…
Cancel
Save