Browse Source

use latest authorization method with jellyfin api (#2821)

* use latest authorization method with jellyfin api

* temp pin dotnet sdk version to 10.0.102

* fix parameter name
pull/2822/head
Jason Dove 6 months ago committed by GitHub
parent
commit
794d209941
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 6
      .github/workflows/artifacts.yml
  2. 8
      .github/workflows/pr.yml
  3. 1
      CHANGELOG.md
  4. 4
      ErsatzTV.Application/Jellyfin/Commands/SaveJellyfinSecretsHandler.cs
  5. 40
      ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinLibrariesHandler.cs
  6. 26
      ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinApiClient.cs
  7. 6
      ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinCollectionScanner.cs
  8. 4
      ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinMovieLibraryScanner.cs
  9. 7
      ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinTelevisionLibraryScanner.cs
  10. 5
      ErsatzTV.Core/Jellyfin/JellyfinConnectionParameters.cs
  11. 52
      ErsatzTV.Infrastructure/Jellyfin/IJellyfinApi.cs
  12. 54
      ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs
  13. 34
      ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinCollectionsHandler.cs
  14. 25
      ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinLibraryByIdHandler.cs
  15. 25
      ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinShowByIdHandler.cs
  16. 12
      ErsatzTV.Scanner/Core/Jellyfin/JellyfinCollectionScanner.cs
  17. 9
      ErsatzTV.Scanner/Core/Jellyfin/JellyfinMovieLibraryScanner.cs
  18. 28
      ErsatzTV.Scanner/Core/Jellyfin/JellyfinTelevisionLibraryScanner.cs

6
.github/workflows/artifacts.yml

@ -57,7 +57,7 @@ jobs:
- name: Setup dotnet - name: Setup dotnet
uses: actions/setup-dotnet@v4 uses: actions/setup-dotnet@v4
with: with:
dotnet-version: '10.0.x' dotnet-version: '10.0.102'
- name: Clean - name: Clean
run: dotnet clean --configuration Release && dotnet nuget locals all --clear run: dotnet clean --configuration Release && dotnet nuget locals all --clear
@ -172,7 +172,7 @@ jobs:
- name: Setup dotnet - name: Setup dotnet
uses: actions/setup-dotnet@v4 uses: actions/setup-dotnet@v4
with: with:
dotnet-version: '10.0.x' dotnet-version: '10.0.102'
- name: Clean - name: Clean
run: dotnet clean --configuration Release && dotnet nuget locals all --clear run: dotnet clean --configuration Release && dotnet nuget locals all --clear
@ -229,7 +229,7 @@ jobs:
- name: Setup dotnet - name: Setup dotnet
uses: actions/setup-dotnet@v4 uses: actions/setup-dotnet@v4
with: with:
dotnet-version: '10.0.x' dotnet-version: '10.0.102'
- name: Clean - name: Clean
run: dotnet clean --configuration Release && dotnet nuget locals all --clear run: dotnet clean --configuration Release && dotnet nuget locals all --clear

8
.github/workflows/pr.yml

@ -14,7 +14,7 @@ jobs:
- name: Setup dotnet - name: Setup dotnet
uses: actions/setup-dotnet@v4 uses: actions/setup-dotnet@v4
with: with:
dotnet-version: '10.0.x' dotnet-version: '10.0.102'
- name: Clean - name: Clean
run: dotnet clean --configuration Release && dotnet nuget locals all --clear run: dotnet clean --configuration Release && dotnet nuget locals all --clear
@ -36,7 +36,7 @@ jobs:
- name: Setup dotnet - name: Setup dotnet
uses: actions/setup-dotnet@v4 uses: actions/setup-dotnet@v4
with: with:
dotnet-version: '10.0.x' dotnet-version: '10.0.102'
- name: Clean - name: Clean
run: dotnet clean --configuration Release && dotnet nuget locals all --clear run: dotnet clean --configuration Release && dotnet nuget locals all --clear
@ -72,7 +72,7 @@ jobs:
- name: Setup dotnet - name: Setup dotnet
uses: actions/setup-dotnet@v4 uses: actions/setup-dotnet@v4
with: with:
dotnet-version: '10.0.x' dotnet-version: '10.0.102'
- name: Clean - name: Clean
run: dotnet clean --configuration Release && dotnet nuget locals all --clear run: dotnet clean --configuration Release && dotnet nuget locals all --clear
@ -100,7 +100,7 @@ jobs:
- name: Setup dotnet - name: Setup dotnet
uses: actions/setup-dotnet@v4 uses: actions/setup-dotnet@v4
with: with:
dotnet-version: '10.0.x' dotnet-version: '10.0.102'
- name: Clean - name: Clean
run: dotnet clean --configuration Release && dotnet nuget locals all --clear run: dotnet clean --configuration Release && dotnet nuget locals all --clear

1
CHANGELOG.md

@ -17,6 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Changed ### Changed
- Move dark/light mode toggle to **Settings** > **UI** - Move dark/light mode toggle to **Settings** > **UI**
- Use latest (non-deprecated) authorization method with Jellyfin API
### Fixed ### Fixed
- Improve stability of playback orders `Shuffle` and `Shuffle in Order` over time - Improve stability of playback orders `Shuffle` and `Shuffle in Order` over time

4
ErsatzTV.Application/Jellyfin/Commands/SaveJellyfinSecretsHandler.cs

@ -36,8 +36,10 @@ public class SaveJellyfinSecretsHandler : IRequestHandler<SaveJellyfinSecrets, E
private async Task<Validation<BaseError, Parameters>> Validate(SaveJellyfinSecrets request) private async Task<Validation<BaseError, Parameters>> Validate(SaveJellyfinSecrets request)
{ {
var connectionParameters = new JellyfinConnectionParameters(request.Secrets.Address, request.Secrets.ApiKey, 0);
Either<BaseError, JellyfinServerInformation> maybeServerInformation = await _jellyfinApiClient Either<BaseError, JellyfinServerInformation> maybeServerInformation = await _jellyfinApiClient
.GetServerInformation(request.Secrets.Address, request.Secrets.ApiKey); .GetServerInformation(connectionParameters.Address, connectionParameters.AuthorizationHeader);
return maybeServerInformation.Match( return maybeServerInformation.Match(
info => Validation<BaseError, Parameters>.Success(new Parameters(request.Secrets, info)), info => Validation<BaseError, Parameters>.Success(new Parameters(request.Secrets, info)),

40
ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinLibrariesHandler.cs

@ -38,7 +38,7 @@ public class
.MapT(p => SynchronizeLibraries(p, cancellationToken)) .MapT(p => SynchronizeLibraries(p, cancellationToken))
.Bind(v => v.ToEitherAsync()); .Bind(v => v.ToEitherAsync());
private Task<Validation<BaseError, ConnectionParameters>> Validate(SynchronizeJellyfinLibraries request) => private Task<Validation<BaseError, ConnectionAndSource>> Validate(SynchronizeJellyfinLibraries request) =>
MediaSourceMustExist(request) MediaSourceMustExist(request)
.BindT(MediaSourceMustHaveActiveConnection) .BindT(MediaSourceMustHaveActiveConnection)
.BindT(MediaSourceMustHaveApiKey); .BindT(MediaSourceMustHaveApiKey);
@ -48,43 +48,48 @@ public class
_mediaSourceRepository.GetJellyfin(request.JellyfinMediaSourceId) _mediaSourceRepository.GetJellyfin(request.JellyfinMediaSourceId)
.Map(o => o.ToValidation<BaseError>("Jellyfin media source does not exist.")); .Map(o => o.ToValidation<BaseError>("Jellyfin media source does not exist."));
private Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection( private Validation<BaseError, ConnectionAndSource> MediaSourceMustHaveActiveConnection(
JellyfinMediaSource jellyfinMediaSource) JellyfinMediaSource jellyfinMediaSource)
{ {
Option<JellyfinConnection> maybeConnection = jellyfinMediaSource.Connections.HeadOrNone(); Option<JellyfinConnection> maybeConnection = jellyfinMediaSource.Connections.HeadOrNone();
return maybeConnection.Map(connection => new ConnectionParameters(jellyfinMediaSource, connection)) return maybeConnection.Map(connection => new ConnectionAndSource(
new JellyfinConnectionParameters(connection.Address, string.Empty, connection.JellyfinMediaSourceId),
jellyfinMediaSource))
.ToValidation<BaseError>("Jellyfin media source requires an active connection"); .ToValidation<BaseError>("Jellyfin media source requires an active connection");
} }
private async Task<Validation<BaseError, ConnectionParameters>> MediaSourceMustHaveApiKey( private async Task<Validation<BaseError, ConnectionAndSource>> MediaSourceMustHaveApiKey(
ConnectionParameters connectionParameters) ConnectionAndSource connectionAndSource)
{ {
JellyfinSecrets secrets = await _jellyfinSecretStore.ReadSecrets(); JellyfinSecrets secrets = await _jellyfinSecretStore.ReadSecrets();
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address) return Optional(secrets.Address == connectionAndSource.ConnectionParameters.Address)
.Where(match => match) .Where(match => match)
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey }) .Map(_ => connectionAndSource with
{
ConnectionParameters = connectionAndSource.ConnectionParameters with { ApiKey = secrets.ApiKey }
})
.ToValidation<BaseError>("Jellyfin media source requires an api key"); .ToValidation<BaseError>("Jellyfin media source requires an api key");
} }
private async Task<Unit> SynchronizeLibraries( private async Task<Unit> SynchronizeLibraries(
ConnectionParameters connectionParameters, ConnectionAndSource connectionAndSource,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
Either<BaseError, List<JellyfinLibrary>> maybeLibraries = await _jellyfinApiClient.GetLibraries( Either<BaseError, List<JellyfinLibrary>> maybeLibraries = await _jellyfinApiClient.GetLibraries(
connectionParameters.ActiveConnection.Address, connectionAndSource.ConnectionParameters.Address,
connectionParameters.ApiKey); connectionAndSource.ConnectionParameters.AuthorizationHeader);
foreach (BaseError error in maybeLibraries.LeftToSeq()) foreach (BaseError error in maybeLibraries.LeftToSeq())
{ {
_logger.LogWarning( _logger.LogWarning(
"Unable to synchronize libraries from jellyfin server {JellyfinServer}: {Error}", "Unable to synchronize libraries from jellyfin server {JellyfinServer}: {Error}",
connectionParameters.JellyfinMediaSource.ServerName, connectionAndSource.MediaSource.ServerName,
error.Value); error.Value);
} }
foreach (List<JellyfinLibrary> libraries in maybeLibraries.RightToSeq()) foreach (List<JellyfinLibrary> libraries in maybeLibraries.RightToSeq())
{ {
var existing = connectionParameters.JellyfinMediaSource.Libraries var existing = connectionAndSource.MediaSource.Libraries
.OfType<JellyfinLibrary>() .OfType<JellyfinLibrary>()
.ToList(); .ToList();
var toAdd = libraries.Filter(library => existing.All(l => l.ItemId != library.ItemId)).ToList(); var toAdd = libraries.Filter(library => existing.All(l => l.ItemId != library.ItemId)).ToList();
@ -92,7 +97,7 @@ public class
var toUpdate = libraries var toUpdate = libraries
.Filter(l => toAdd.All(a => a.ItemId != l.ItemId) && toRemove.All(r => r.ItemId != l.ItemId)).ToList(); .Filter(l => toAdd.All(a => a.ItemId != l.ItemId) && toRemove.All(r => r.ItemId != l.ItemId)).ToList();
List<int> ids = await _mediaSourceRepository.UpdateLibraries( List<int> ids = await _mediaSourceRepository.UpdateLibraries(
connectionParameters.JellyfinMediaSource.Id, connectionAndSource.MediaSource.Id,
toAdd, toAdd,
toRemove, toRemove,
toUpdate, toUpdate,
@ -107,10 +112,7 @@ public class
return Unit.Default; return Unit.Default;
} }
private sealed record ConnectionParameters( private sealed record ConnectionAndSource(
JellyfinMediaSource JellyfinMediaSource, JellyfinConnectionParameters ConnectionParameters,
JellyfinConnection ActiveConnection) JellyfinMediaSource MediaSource);
{
public string ApiKey { get; set; }
}
} }

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

@ -5,69 +5,69 @@ namespace ErsatzTV.Core.Interfaces.Jellyfin;
public interface IJellyfinApiClient public interface IJellyfinApiClient
{ {
Task<Either<BaseError, JellyfinServerInformation>> GetServerInformation(string address, string apiKey); Task<Either<BaseError, JellyfinServerInformation>> GetServerInformation(string address, string authorizationHeader);
Task<Either<BaseError, List<JellyfinLibrary>>> GetLibraries(string address, string apiKey); Task<Either<BaseError, List<JellyfinLibrary>>> GetLibraries(string address, string authorizationHeader);
IAsyncEnumerable<Tuple<JellyfinMovie, int>> GetMovieLibraryItems( IAsyncEnumerable<Tuple<JellyfinMovie, int>> GetMovieLibraryItems(
string address, string address,
string apiKey, string authorizationHeader,
JellyfinLibrary library); JellyfinLibrary library);
IAsyncEnumerable<Tuple<JellyfinShow, int>> GetShowLibraryItemsWithoutPeople( IAsyncEnumerable<Tuple<JellyfinShow, int>> GetShowLibraryItemsWithoutPeople(
string address, string address,
string apiKey, string authorizationHeader,
JellyfinLibrary library); JellyfinLibrary library);
IAsyncEnumerable<Tuple<JellyfinSeason, int>> GetSeasonLibraryItems( IAsyncEnumerable<Tuple<JellyfinSeason, int>> GetSeasonLibraryItems(
string address, string address,
string apiKey, string authorizationHeader,
JellyfinLibrary library, JellyfinLibrary library,
string showId); string showId);
IAsyncEnumerable<Tuple<JellyfinEpisode, int>> GetEpisodeLibraryItems( IAsyncEnumerable<Tuple<JellyfinEpisode, int>> GetEpisodeLibraryItems(
string address, string address,
string apiKey, string authorizationHeader,
JellyfinLibrary library, JellyfinLibrary library,
string seasonId); string seasonId);
IAsyncEnumerable<Tuple<JellyfinEpisode, int>> GetEpisodeLibraryItemsWithoutPeople( IAsyncEnumerable<Tuple<JellyfinEpisode, int>> GetEpisodeLibraryItemsWithoutPeople(
string address, string address,
string apiKey, string authorizationHeader,
JellyfinLibrary library, JellyfinLibrary library,
string seasonId); string seasonId);
IAsyncEnumerable<Tuple<JellyfinCollection, int>> GetCollectionLibraryItems( IAsyncEnumerable<Tuple<JellyfinCollection, int>> GetCollectionLibraryItems(
string address, string address,
string apiKey, string authorizationHeader,
int mediaSourceId); int mediaSourceId);
IAsyncEnumerable<Tuple<MediaItem, int>> GetCollectionItems( IAsyncEnumerable<Tuple<MediaItem, int>> GetCollectionItems(
string address, string address,
string apiKey, string authorizationHeader,
int mediaSourceId, int mediaSourceId,
string collectionId); string collectionId);
Task<Either<BaseError, MediaVersion>> GetPlaybackInfo( Task<Either<BaseError, MediaVersion>> GetPlaybackInfo(
string address, string address,
string apiKey, string authorizationHeader,
JellyfinLibrary library, JellyfinLibrary library,
string itemId); string itemId);
Task<Either<BaseError, Option<JellyfinShow>>> GetSingleShow( Task<Either<BaseError, Option<JellyfinShow>>> GetSingleShow(
string address, string address,
string apiKey, string authorizationHeader,
JellyfinLibrary library, JellyfinLibrary library,
string showId); string showId);
Task<Either<BaseError, List<JellyfinShow>>> SearchShowsByTitle( Task<Either<BaseError, List<JellyfinShow>>> SearchShowsByTitle(
string address, string address,
string apiKey, string authorizationHeader,
JellyfinLibrary library, JellyfinLibrary library,
string showTitle); string showTitle);
Task<Either<BaseError, Option<JellyfinEpisode>>> GetSingleEpisode( Task<Either<BaseError, Option<JellyfinEpisode>>> GetSingleEpisode(
string address, string address,
string apiKey, string authorizationHeader,
JellyfinLibrary library, JellyfinLibrary library,
string seasonId, string seasonId,
string episodeId); string episodeId);

6
ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinCollectionScanner.cs

@ -2,5 +2,9 @@
public interface IJellyfinCollectionScanner public interface IJellyfinCollectionScanner
{ {
Task<Either<BaseError, Unit>> ScanCollections(string address, string apiKey, int mediaSourceId, bool deepScan); Task<Either<BaseError, Unit>> ScanCollections(
string address,
string authorizationHeader,
int mediaSourceId,
bool deepScan);
} }

4
ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinMovieLibraryScanner.cs

@ -1,12 +1,12 @@
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Jellyfin;
namespace ErsatzTV.Core.Interfaces.Jellyfin; namespace ErsatzTV.Core.Interfaces.Jellyfin;
public interface IJellyfinMovieLibraryScanner public interface IJellyfinMovieLibraryScanner
{ {
Task<Either<BaseError, Unit>> ScanLibrary( Task<Either<BaseError, Unit>> ScanLibrary(
string address, JellyfinConnectionParameters connectionParameters,
string apiKey,
JellyfinLibrary library, JellyfinLibrary library,
bool deepScan, bool deepScan,
CancellationToken cancellationToken); CancellationToken cancellationToken);

7
ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinTelevisionLibraryScanner.cs

@ -1,19 +1,18 @@
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Jellyfin;
namespace ErsatzTV.Core.Interfaces.Jellyfin; namespace ErsatzTV.Core.Interfaces.Jellyfin;
public interface IJellyfinTelevisionLibraryScanner public interface IJellyfinTelevisionLibraryScanner
{ {
Task<Either<BaseError, Unit>> ScanLibrary( Task<Either<BaseError, Unit>> ScanLibrary(
string address, JellyfinConnectionParameters connectionParameters,
string apiKey,
JellyfinLibrary library, JellyfinLibrary library,
bool deepScan, bool deepScan,
CancellationToken cancellationToken); CancellationToken cancellationToken);
Task<Either<BaseError, Unit>> ScanSingleShow( Task<Either<BaseError, Unit>> ScanSingleShow(
string address, JellyfinConnectionParameters connectionParameters,
string apiKey,
JellyfinLibrary library, JellyfinLibrary library,
string showId, string showId,
string showTitle, string showTitle,

5
ErsatzTV.Core/Jellyfin/JellyfinConnectionParameters.cs

@ -3,4 +3,7 @@
namespace ErsatzTV.Core.Jellyfin; namespace ErsatzTV.Core.Jellyfin;
public record JellyfinConnectionParameters(string Address, string ApiKey, int MediaSourceId) public record JellyfinConnectionParameters(string Address, string ApiKey, int MediaSourceId)
: MediaServerConnectionParameters; : MediaServerConnectionParameters
{
public string AuthorizationHeader => $"MediaBrowser Token={ApiKey}";
}

52
ErsatzTV.Infrastructure/Jellyfin/IJellyfinApi.cs

@ -8,24 +8,24 @@ public interface IJellyfinApi
{ {
[Get("/System/Info")] [Get("/System/Info")]
Task<JellyfinSystemInformationResponse> GetSystemInformation( Task<JellyfinSystemInformationResponse> GetSystemInformation(
[Header("X-Emby-Token")] [Header("Authorization")]
string apiKey, string authorizationHeader,
CancellationToken cancellationToken); CancellationToken cancellationToken);
[Get("/Users")] [Get("/Users")]
Task<List<JellyfinUserResponse>> GetUsers( Task<List<JellyfinUserResponse>> GetUsers(
[Header("X-Emby-Token")] [Header("Authorization")]
string apiKey); string authorizationHeader);
[Get("/Library/VirtualFolders")] [Get("/Library/VirtualFolders")]
Task<List<JellyfinLibraryResponse>> GetLibraries( Task<List<JellyfinLibraryResponse>> GetLibraries(
[Header("X-Emby-Token")] [Header("Authorization")]
string apiKey); string authorizationHeader);
[Get("/Items?sortOrder=Ascending&sortBy=SortName")] [Get("/Items?sortOrder=Ascending&sortBy=SortName")]
Task<JellyfinLibraryItemsResponse> GetMovieLibraryItems( Task<JellyfinLibraryItemsResponse> GetMovieLibraryItems(
[Header("X-Emby-Token")] [Header("Authorization")]
string apiKey, string authorizationHeader,
[Query] [Query]
string parentId, string parentId,
[Query] [Query]
@ -44,8 +44,8 @@ public interface IJellyfinApi
[Get("/Items?sortOrder=Ascending&sortBy=SortName")] [Get("/Items?sortOrder=Ascending&sortBy=SortName")]
Task<JellyfinLibraryItemsResponse> GetShowLibraryItemsWithoutPeople( Task<JellyfinLibraryItemsResponse> GetShowLibraryItemsWithoutPeople(
[Header("X-Emby-Token")] [Header("Authorization")]
string apiKey, string authorizationHeader,
[Query] [Query]
string parentId, string parentId,
[Query] [Query]
@ -64,8 +64,8 @@ public interface IJellyfinApi
[Get("/Items?sortOrder=Ascending&sortBy=SortName")] [Get("/Items?sortOrder=Ascending&sortBy=SortName")]
Task<JellyfinLibraryItemsResponse> GetShowLibraryItems( Task<JellyfinLibraryItemsResponse> GetShowLibraryItems(
[Header("X-Emby-Token")] [Header("Authorization")]
string apiKey, string authorizationHeader,
[Query] [Query]
string parentId, string parentId,
[Query] [Query]
@ -84,8 +84,8 @@ public interface IJellyfinApi
[Get("/Items?sortOrder=Ascending&sortBy=SortName")] [Get("/Items?sortOrder=Ascending&sortBy=SortName")]
Task<JellyfinLibraryItemsResponse> GetSeasonLibraryItems( Task<JellyfinLibraryItemsResponse> GetSeasonLibraryItems(
[Header("X-Emby-Token")] [Header("Authorization")]
string apiKey, string authorizationHeader,
[Query] [Query]
string parentId, string parentId,
[Query] [Query]
@ -101,8 +101,8 @@ public interface IJellyfinApi
[Get("/Items?sortOrder=Ascending&sortBy=SortName")] [Get("/Items?sortOrder=Ascending&sortBy=SortName")]
Task<JellyfinLibraryItemsResponse> GetEpisodeLibraryItems( Task<JellyfinLibraryItemsResponse> GetEpisodeLibraryItems(
[Header("X-Emby-Token")] [Header("Authorization")]
string apiKey, string authorizationHeader,
[Query] [Query]
string parentId, string parentId,
[Query] [Query]
@ -120,8 +120,8 @@ public interface IJellyfinApi
[Get("/Items?sortOrder=Ascending&sortBy=SortName")] [Get("/Items?sortOrder=Ascending&sortBy=SortName")]
Task<JellyfinLibraryItemsResponse> GetEpisodeLibraryItemsWithoutPeople( Task<JellyfinLibraryItemsResponse> GetEpisodeLibraryItemsWithoutPeople(
[Header("X-Emby-Token")] [Header("Authorization")]
string apiKey, string authorizationHeader,
[Query] [Query]
string parentId, string parentId,
[Query] [Query]
@ -137,8 +137,8 @@ public interface IJellyfinApi
[Get("/Items?sortOrder=Ascending&sortBy=SortName")] [Get("/Items?sortOrder=Ascending&sortBy=SortName")]
Task<JellyfinLibraryItemsResponse> GetCollectionLibraryItems( Task<JellyfinLibraryItemsResponse> GetCollectionLibraryItems(
[Header("X-Emby-Token")] [Header("Authorization")]
string apiKey, string authorizationHeader,
[Query] [Query]
string parentId, string parentId,
[Query] [Query]
@ -154,8 +154,8 @@ public interface IJellyfinApi
[Get("/Items?sortOrder=Ascending&sortBy=SortName")] [Get("/Items?sortOrder=Ascending&sortBy=SortName")]
Task<JellyfinLibraryItemsResponse> GetCollectionItems( Task<JellyfinLibraryItemsResponse> GetCollectionItems(
[Header("X-Emby-Token")] [Header("Authorization")]
string apiKey, string authorizationHeader,
[Query] [Query]
string parentId, string parentId,
[Query] [Query]
@ -171,14 +171,14 @@ public interface IJellyfinApi
[Get("/Items/{itemId}/PlaybackInfo")] [Get("/Items/{itemId}/PlaybackInfo")]
Task<JellyfinPlaybackInfoResponse> GetPlaybackInfo( Task<JellyfinPlaybackInfoResponse> GetPlaybackInfo(
[Header("X-Emby-Token")] [Header("Authorization")]
string apiKey, string authorizationHeader,
string itemId); string itemId);
[Get("/Search/Hints")] [Get("/Search/Hints")]
Task<JellyfinSearchHintsResponse> SearchHints( Task<JellyfinSearchHintsResponse> SearchHints(
[Header("X-Emby-Token")] [Header("Authorization")]
string apiKey, string authorizationHeader,
[Query] [Query]
string searchTerm, string searchTerm,
[Query] [Query]

54
ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs

@ -36,14 +36,14 @@ public class JellyfinApiClient : IJellyfinApiClient
public async Task<Either<BaseError, JellyfinServerInformation>> GetServerInformation( public async Task<Either<BaseError, JellyfinServerInformation>> GetServerInformation(
string address, string address,
string apiKey) string authorizationHeader)
{ {
try try
{ {
IJellyfinApi service = ServiceForAddress(address); IJellyfinApi service = ServiceForAddress(address);
var cts = new CancellationTokenSource(); var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(5)); cts.CancelAfter(TimeSpan.FromSeconds(5));
return await service.GetSystemInformation(apiKey, cts.Token) return await service.GetSystemInformation(authorizationHeader, cts.Token)
.Map(response => new JellyfinServerInformation(response.ServerName, response.OperatingSystem)); .Map(response => new JellyfinServerInformation(response.ServerName, response.OperatingSystem));
} }
catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException) catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException)
@ -58,12 +58,12 @@ public class JellyfinApiClient : IJellyfinApiClient
} }
} }
public async Task<Either<BaseError, List<JellyfinLibrary>>> GetLibraries(string address, string apiKey) public async Task<Either<BaseError, List<JellyfinLibrary>>> GetLibraries(string address, string authorizationHeader)
{ {
try try
{ {
IJellyfinApi service = ServiceForAddress(address); IJellyfinApi service = ServiceForAddress(address);
List<JellyfinLibraryResponse> libraries = await service.GetLibraries(apiKey); List<JellyfinLibraryResponse> libraries = await service.GetLibraries(authorizationHeader);
return libraries return libraries
.Map(Project) .Map(Project)
.Somes() .Somes()
@ -78,7 +78,7 @@ public class JellyfinApiClient : IJellyfinApiClient
public IAsyncEnumerable<Tuple<JellyfinMovie, int>> GetMovieLibraryItems( public IAsyncEnumerable<Tuple<JellyfinMovie, int>> GetMovieLibraryItems(
string address, string address,
string apiKey, string authorizationHeader,
JellyfinLibrary library) => JellyfinLibrary library) =>
GetPagedLibraryItems( GetPagedLibraryItems(
"JF Movies", "JF Movies",
@ -87,7 +87,7 @@ public class JellyfinApiClient : IJellyfinApiClient
library.MediaSourceId, library.MediaSourceId,
library.ItemId, library.ItemId,
(service, itemId, skip, pageSize) => service.GetMovieLibraryItems( (service, itemId, skip, pageSize) => service.GetMovieLibraryItems(
apiKey, authorizationHeader,
itemId, itemId,
startIndex: skip, startIndex: skip,
limit: pageSize), limit: pageSize),
@ -95,7 +95,7 @@ public class JellyfinApiClient : IJellyfinApiClient
public IAsyncEnumerable<Tuple<JellyfinShow, int>> GetShowLibraryItemsWithoutPeople( public IAsyncEnumerable<Tuple<JellyfinShow, int>> GetShowLibraryItemsWithoutPeople(
string address, string address,
string apiKey, string authorizationHeader,
JellyfinLibrary library) => JellyfinLibrary library) =>
GetPagedLibraryItems( GetPagedLibraryItems(
"JF Shows", "JF Shows",
@ -104,7 +104,7 @@ public class JellyfinApiClient : IJellyfinApiClient
library.MediaSourceId, library.MediaSourceId,
library.ItemId, library.ItemId,
(service, itemId, skip, pageSize) => service.GetShowLibraryItemsWithoutPeople( (service, itemId, skip, pageSize) => service.GetShowLibraryItemsWithoutPeople(
apiKey, authorizationHeader,
itemId, itemId,
startIndex: skip, startIndex: skip,
limit: pageSize), limit: pageSize),
@ -112,7 +112,7 @@ public class JellyfinApiClient : IJellyfinApiClient
public IAsyncEnumerable<Tuple<JellyfinSeason, int>> GetSeasonLibraryItems( public IAsyncEnumerable<Tuple<JellyfinSeason, int>> GetSeasonLibraryItems(
string address, string address,
string apiKey, string authorizationHeader,
JellyfinLibrary library, JellyfinLibrary library,
string showId) => string showId) =>
GetPagedLibraryItems( GetPagedLibraryItems(
@ -122,7 +122,7 @@ public class JellyfinApiClient : IJellyfinApiClient
library.MediaSourceId, library.MediaSourceId,
showId, showId,
(service, _, skip, pageSize) => service.GetSeasonLibraryItems( (service, _, skip, pageSize) => service.GetSeasonLibraryItems(
apiKey, authorizationHeader,
showId, showId,
startIndex: skip, startIndex: skip,
limit: pageSize), limit: pageSize),
@ -130,7 +130,7 @@ public class JellyfinApiClient : IJellyfinApiClient
public IAsyncEnumerable<Tuple<JellyfinEpisode, int>> GetEpisodeLibraryItems( public IAsyncEnumerable<Tuple<JellyfinEpisode, int>> GetEpisodeLibraryItems(
string address, string address,
string apiKey, string authorizationHeader,
JellyfinLibrary library, JellyfinLibrary library,
string seasonId) => string seasonId) =>
GetPagedLibraryItems( GetPagedLibraryItems(
@ -140,7 +140,7 @@ public class JellyfinApiClient : IJellyfinApiClient
library.MediaSourceId, library.MediaSourceId,
seasonId, seasonId,
(service, _, skip, pageSize) => service.GetEpisodeLibraryItems( (service, _, skip, pageSize) => service.GetEpisodeLibraryItems(
apiKey, authorizationHeader,
seasonId, seasonId,
startIndex: skip, startIndex: skip,
limit: pageSize), limit: pageSize),
@ -148,7 +148,7 @@ public class JellyfinApiClient : IJellyfinApiClient
public IAsyncEnumerable<Tuple<JellyfinEpisode, int>> GetEpisodeLibraryItemsWithoutPeople( public IAsyncEnumerable<Tuple<JellyfinEpisode, int>> GetEpisodeLibraryItemsWithoutPeople(
string address, string address,
string apiKey, string authorizationHeader,
JellyfinLibrary library, JellyfinLibrary library,
string seasonId) => string seasonId) =>
GetPagedLibraryItems( GetPagedLibraryItems(
@ -158,7 +158,7 @@ public class JellyfinApiClient : IJellyfinApiClient
library.MediaSourceId, library.MediaSourceId,
seasonId, seasonId,
(service, _, skip, pageSize) => service.GetEpisodeLibraryItemsWithoutPeople( (service, _, skip, pageSize) => service.GetEpisodeLibraryItemsWithoutPeople(
apiKey, authorizationHeader,
seasonId, seasonId,
startIndex: skip, startIndex: skip,
limit: pageSize), limit: pageSize),
@ -166,7 +166,7 @@ public class JellyfinApiClient : IJellyfinApiClient
public IAsyncEnumerable<Tuple<JellyfinCollection, int>> GetCollectionLibraryItems( public IAsyncEnumerable<Tuple<JellyfinCollection, int>> GetCollectionLibraryItems(
string address, string address,
string apiKey, string authorizationHeader,
int mediaSourceId) int mediaSourceId)
{ {
// TODO: should we enumerate collection libraries here? // TODO: should we enumerate collection libraries here?
@ -180,7 +180,7 @@ public class JellyfinApiClient : IJellyfinApiClient
mediaSourceId, mediaSourceId,
itemId, itemId,
(service, _, skip, pageSize) => service.GetCollectionLibraryItems( (service, _, skip, pageSize) => service.GetCollectionLibraryItems(
apiKey, authorizationHeader,
itemId, itemId,
startIndex: skip, startIndex: skip,
limit: pageSize), limit: pageSize),
@ -192,7 +192,7 @@ public class JellyfinApiClient : IJellyfinApiClient
public IAsyncEnumerable<Tuple<MediaItem, int>> GetCollectionItems( public IAsyncEnumerable<Tuple<MediaItem, int>> GetCollectionItems(
string address, string address,
string apiKey, string authorizationHeader,
int mediaSourceId, int mediaSourceId,
string collectionId) => string collectionId) =>
GetPagedLibraryItems( GetPagedLibraryItems(
@ -202,7 +202,7 @@ public class JellyfinApiClient : IJellyfinApiClient
mediaSourceId, mediaSourceId,
collectionId, collectionId,
(service, _, skip, pageSize) => service.GetCollectionItems( (service, _, skip, pageSize) => service.GetCollectionItems(
apiKey, authorizationHeader,
collectionId, collectionId,
startIndex: skip, startIndex: skip,
limit: pageSize), limit: pageSize),
@ -210,7 +210,7 @@ public class JellyfinApiClient : IJellyfinApiClient
public async Task<Either<BaseError, MediaVersion>> GetPlaybackInfo( public async Task<Either<BaseError, MediaVersion>> GetPlaybackInfo(
string address, string address,
string apiKey, string authorizationHeader,
JellyfinLibrary library, JellyfinLibrary library,
string itemId) string itemId)
{ {
@ -219,7 +219,7 @@ public class JellyfinApiClient : IJellyfinApiClient
using (ScanProfiler.Measure("JF Playback Info")) using (ScanProfiler.Measure("JF Playback Info"))
{ {
IJellyfinApi service = ServiceForAddress(address); IJellyfinApi service = ServiceForAddress(address);
JellyfinPlaybackInfoResponse playbackInfo = await service.GetPlaybackInfo(apiKey, itemId); JellyfinPlaybackInfoResponse playbackInfo = await service.GetPlaybackInfo(authorizationHeader, itemId);
Option<MediaVersion> maybeVersion = ProjectToMediaVersion(playbackInfo); Option<MediaVersion> maybeVersion = ProjectToMediaVersion(playbackInfo);
return maybeVersion.ToEither(() => BaseError.New("Unable to locate Jellyfin statistics")); return maybeVersion.ToEither(() => BaseError.New("Unable to locate Jellyfin statistics"));
} }
@ -233,7 +233,7 @@ public class JellyfinApiClient : IJellyfinApiClient
public async Task<Either<BaseError, Option<JellyfinShow>>> GetSingleShow( public async Task<Either<BaseError, Option<JellyfinShow>>> GetSingleShow(
string address, string address,
string apiKey, string authorizationHeader,
JellyfinLibrary library, JellyfinLibrary library,
string showId) string showId)
{ {
@ -243,7 +243,7 @@ public class JellyfinApiClient : IJellyfinApiClient
{ {
IJellyfinApi service = ServiceForAddress(address); IJellyfinApi service = ServiceForAddress(address);
JellyfinLibraryItemsResponse itemsResponse = await service.GetShowLibraryItems( JellyfinLibraryItemsResponse itemsResponse = await service.GetShowLibraryItems(
apiKey, authorizationHeader,
parentId: library.ItemId, parentId: library.ItemId,
recursive: false, recursive: false,
startIndex: 0, startIndex: 0,
@ -267,7 +267,7 @@ public class JellyfinApiClient : IJellyfinApiClient
public async Task<Either<BaseError, List<JellyfinShow>>> SearchShowsByTitle( public async Task<Either<BaseError, List<JellyfinShow>>> SearchShowsByTitle(
string address, string address,
string apiKey, string authorizationHeader,
JellyfinLibrary library, JellyfinLibrary library,
string showTitle) string showTitle)
{ {
@ -275,7 +275,7 @@ public class JellyfinApiClient : IJellyfinApiClient
{ {
IJellyfinApi service = ServiceForAddress(address); IJellyfinApi service = ServiceForAddress(address);
JellyfinSearchHintsResponse searchResponse = await service.SearchHints( JellyfinSearchHintsResponse searchResponse = await service.SearchHints(
apiKey, authorizationHeader,
showTitle, showTitle,
"Series", "Series",
library.ItemId); library.ItemId);
@ -288,7 +288,7 @@ public class JellyfinApiClient : IJellyfinApiClient
string.Equals(hint.Name, showTitle, StringComparison.OrdinalIgnoreCase)) string.Equals(hint.Name, showTitle, StringComparison.OrdinalIgnoreCase))
{ {
JellyfinLibraryItemsResponse detailResponse = await service.GetShowLibraryItems( JellyfinLibraryItemsResponse detailResponse = await service.GetShowLibraryItems(
apiKey, authorizationHeader,
hint.Id, hint.Id,
recursive: false, recursive: false,
startIndex: 0, startIndex: 0,
@ -316,7 +316,7 @@ public class JellyfinApiClient : IJellyfinApiClient
public async Task<Either<BaseError, Option<JellyfinEpisode>>> GetSingleEpisode( public async Task<Either<BaseError, Option<JellyfinEpisode>>> GetSingleEpisode(
string address, string address,
string apiKey, string authorizationHeader,
JellyfinLibrary library, JellyfinLibrary library,
string seasonId, string seasonId,
string episodeId) string episodeId)
@ -327,7 +327,7 @@ public class JellyfinApiClient : IJellyfinApiClient
{ {
IJellyfinApi service = ServiceForAddress(address); IJellyfinApi service = ServiceForAddress(address);
JellyfinLibraryItemsResponse itemsResponse = await service.GetEpisodeLibraryItems( JellyfinLibraryItemsResponse itemsResponse = await service.GetEpisodeLibraryItems(
apiKey, authorizationHeader,
parentId: seasonId, parentId: seasonId,
recursive: false, recursive: false,
startIndex: 0, startIndex: 0,

34
ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinCollectionsHandler.cs

@ -45,13 +45,13 @@ public class
SynchronizeJellyfinCollections request, SynchronizeJellyfinCollections request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
Task<Validation<BaseError, ConnectionParameters>> mediaSource = MediaSourceMustExist(request) Task<Validation<BaseError, ConnectionAndSource>> mediaSource = MediaSourceMustExist(request)
.BindT(MediaSourceMustHaveActiveConnection) .BindT(MediaSourceMustHaveActiveConnection)
.BindT(MediaSourceMustHaveApiKey); .BindT(MediaSourceMustHaveApiKey);
return (await mediaSource, await ValidateLibraryRefreshInterval(cancellationToken)) return (await mediaSource, await ValidateLibraryRefreshInterval(cancellationToken))
.Apply((connectionParameters, libraryRefreshInterval) => new RequestParameters( .Apply((connectionParameters, libraryRefreshInterval) => new RequestParameters(
connectionParameters, connectionParameters.ConnectionParameters,
connectionParameters.MediaSource, connectionParameters.MediaSource,
request.ForceScan, request.ForceScan,
request.DeepScan, request.DeepScan,
@ -69,21 +69,26 @@ public class
_mediaSourceRepository.GetJellyfin(request.JellyfinMediaSourceId) _mediaSourceRepository.GetJellyfin(request.JellyfinMediaSourceId)
.Map(o => o.ToValidation<BaseError>("Jellyfin media source does not exist.")); .Map(o => o.ToValidation<BaseError>("Jellyfin media source does not exist."));
private static Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection( private static Validation<BaseError, ConnectionAndSource> MediaSourceMustHaveActiveConnection(
JellyfinMediaSource jellyfinMediaSource) JellyfinMediaSource jellyfinMediaSource)
{ {
Option<JellyfinConnection> maybeConnection = jellyfinMediaSource.Connections.HeadOrNone(); Option<JellyfinConnection> maybeConnection = jellyfinMediaSource.Connections.HeadOrNone();
return maybeConnection.Map(connection => new ConnectionParameters(jellyfinMediaSource, connection)) return maybeConnection.Map(connection => new ConnectionAndSource(
new JellyfinConnectionParameters(connection.Address, string.Empty, connection.JellyfinMediaSourceId),
jellyfinMediaSource))
.ToValidation<BaseError>("Jellyfin media source requires an active connection"); .ToValidation<BaseError>("Jellyfin media source requires an active connection");
} }
private async Task<Validation<BaseError, ConnectionParameters>> MediaSourceMustHaveApiKey( private async Task<Validation<BaseError, ConnectionAndSource>> MediaSourceMustHaveApiKey(
ConnectionParameters connectionParameters) ConnectionAndSource connectionAndSource)
{ {
JellyfinSecrets secrets = await _jellyfinSecretStore.ReadSecrets(); JellyfinSecrets secrets = await _jellyfinSecretStore.ReadSecrets();
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address) return Optional(secrets.Address == connectionAndSource.ConnectionParameters.Address)
.Where(match => match) .Where(match => match)
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey }) .Map(_ => connectionAndSource with
{
ConnectionParameters = connectionAndSource.ConnectionParameters with { ApiKey = secrets.ApiKey }
})
.ToValidation<BaseError>("Jellyfin media source requires an api key"); .ToValidation<BaseError>("Jellyfin media source requires an api key");
} }
@ -98,8 +103,8 @@ public class
if (parameters.ForceScan || parameters.LibraryRefreshInterval > 0 && nextScan < DateTimeOffset.Now) if (parameters.ForceScan || parameters.LibraryRefreshInterval > 0 && nextScan < DateTimeOffset.Now)
{ {
Either<BaseError, Unit> result = await _scanner.ScanCollections( Either<BaseError, Unit> result = await _scanner.ScanCollections(
parameters.ConnectionParameters.ActiveConnection.Address, parameters.ConnectionParameters.Address,
parameters.ConnectionParameters.ApiKey, parameters.ConnectionParameters.AuthorizationHeader,
parameters.MediaSource.Id, parameters.MediaSource.Id,
parameters.DeepScan); parameters.DeepScan);
@ -116,15 +121,14 @@ public class
} }
private record RequestParameters( private record RequestParameters(
ConnectionParameters ConnectionParameters, JellyfinConnectionParameters ConnectionParameters,
JellyfinMediaSource MediaSource, JellyfinMediaSource MediaSource,
bool ForceScan, bool ForceScan,
bool DeepScan, bool DeepScan,
int LibraryRefreshInterval, int LibraryRefreshInterval,
string BaseUrl); string BaseUrl);
private record ConnectionParameters(JellyfinMediaSource MediaSource, JellyfinConnection ActiveConnection) private sealed record ConnectionAndSource(
{ JellyfinConnectionParameters ConnectionParameters,
public string? ApiKey { get; init; } JellyfinMediaSource MediaSource);
}
} }

25
ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinLibraryByIdHandler.cs

@ -65,15 +65,13 @@ public class
{ {
LibraryMediaKind.Movies => LibraryMediaKind.Movies =>
await _jellyfinMovieLibraryScanner.ScanLibrary( await _jellyfinMovieLibraryScanner.ScanLibrary(
parameters.ConnectionParameters.ActiveConnection.Address, parameters.ConnectionParameters,
parameters.ConnectionParameters.ApiKey,
parameters.Library, parameters.Library,
parameters.DeepScan, parameters.DeepScan,
cancellationToken), cancellationToken),
LibraryMediaKind.Shows => LibraryMediaKind.Shows =>
await _jellyfinTelevisionLibraryScanner.ScanLibrary( await _jellyfinTelevisionLibraryScanner.ScanLibrary(
parameters.ConnectionParameters.ActiveConnection.Address, parameters.ConnectionParameters,
parameters.ConnectionParameters.ApiKey,
parameters.Library, parameters.Library,
parameters.DeepScan, parameters.DeepScan,
cancellationToken), cancellationToken),
@ -114,7 +112,7 @@ public class
request.BaseUrl request.BaseUrl
)); ));
private Task<Validation<BaseError, ConnectionParameters>> ValidateConnection( private Task<Validation<BaseError, JellyfinConnectionParameters>> ValidateConnection(
SynchronizeJellyfinLibraryById request) => SynchronizeJellyfinLibraryById request) =>
JellyfinMediaSourceMustExist(request) JellyfinMediaSourceMustExist(request)
.BindT(MediaSourceMustHaveActiveConnection) .BindT(MediaSourceMustHaveActiveConnection)
@ -126,19 +124,19 @@ public class
.Map(v => v.ToValidation<BaseError>( .Map(v => v.ToValidation<BaseError>(
$"Jellyfin media source for library {request.JellyfinLibraryId} does not exist.")); $"Jellyfin media source for library {request.JellyfinLibraryId} does not exist."));
private Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection( private Validation<BaseError, JellyfinConnectionParameters> MediaSourceMustHaveActiveConnection(
JellyfinMediaSource jellyfinMediaSource) JellyfinMediaSource jellyfinMediaSource)
{ {
Option<JellyfinConnection> maybeConnection = jellyfinMediaSource.Connections.HeadOrNone(); Option<JellyfinConnection> maybeConnection = jellyfinMediaSource.Connections.HeadOrNone();
return maybeConnection.Map(connection => new ConnectionParameters(connection)) return maybeConnection.Map(connection => new JellyfinConnectionParameters(connection.Address, string.Empty, connection.JellyfinMediaSourceId))
.ToValidation<BaseError>("Jellyfin media source requires an active connection"); .ToValidation<BaseError>("Jellyfin media source requires an active connection");
} }
private async Task<Validation<BaseError, ConnectionParameters>> MediaSourceMustHaveApiKey( private async Task<Validation<BaseError, JellyfinConnectionParameters>> MediaSourceMustHaveApiKey(
ConnectionParameters connectionParameters) JellyfinConnectionParameters connectionParameters)
{ {
JellyfinSecrets secrets = await _jellyfinSecretStore.ReadSecrets(); JellyfinSecrets secrets = await _jellyfinSecretStore.ReadSecrets();
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address) return Optional(secrets.Address == connectionParameters.Address)
.Where(match => match) .Where(match => match)
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey }) .Map(_ => connectionParameters with { ApiKey = secrets.ApiKey })
.ToValidation<BaseError>("Jellyfin media source requires an api key"); .ToValidation<BaseError>("Jellyfin media source requires an api key");
@ -155,15 +153,10 @@ public class
.Map(lri => lri.ToValidation<BaseError>("Library refresh interval is invalid")); .Map(lri => lri.ToValidation<BaseError>("Library refresh interval is invalid"));
private record RequestParameters( private record RequestParameters(
ConnectionParameters ConnectionParameters, JellyfinConnectionParameters ConnectionParameters,
JellyfinLibrary Library, JellyfinLibrary Library,
bool ForceScan, bool ForceScan,
int LibraryRefreshInterval, int LibraryRefreshInterval,
bool DeepScan, bool DeepScan,
string BaseUrl); string BaseUrl);
private record ConnectionParameters(JellyfinConnection ActiveConnection)
{
public string? ApiKey { get; init; }
}
} }

25
ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinShowByIdHandler.cs

@ -61,8 +61,7 @@ public class
parameters.Library.Name); parameters.Library.Name);
Either<BaseError, Unit> result = await _jellyfinTelevisionLibraryScanner.ScanSingleShow( Either<BaseError, Unit> result = await _jellyfinTelevisionLibraryScanner.ScanSingleShow(
parameters.ConnectionParameters.ActiveConnection.Address, parameters.ConnectionParameters,
parameters.ConnectionParameters.ApiKey,
parameters.Library, parameters.Library,
parameters.ItemId, parameters.ItemId,
parameters.ShowTitle, parameters.ShowTitle,
@ -92,7 +91,7 @@ public class
request.BaseUrl request.BaseUrl
)); ));
private Task<Validation<BaseError, ConnectionParameters>> ValidateConnection( private Task<Validation<BaseError, JellyfinConnectionParameters>> ValidateConnection(
SynchronizeJellyfinShowById request) => SynchronizeJellyfinShowById request) =>
JellyfinMediaSourceMustExist(request) JellyfinMediaSourceMustExist(request)
.BindT(MediaSourceMustHaveActiveConnection) .BindT(MediaSourceMustHaveActiveConnection)
@ -104,19 +103,22 @@ public class
.Map(v => v.ToValidation<BaseError>( .Map(v => v.ToValidation<BaseError>(
$"Jellyfin media source for library {request.JellyfinLibraryId} does not exist.")); $"Jellyfin media source for library {request.JellyfinLibraryId} does not exist."));
private Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection( private Validation<BaseError, JellyfinConnectionParameters> MediaSourceMustHaveActiveConnection(
JellyfinMediaSource jellyfinMediaSource) JellyfinMediaSource jellyfinMediaSource)
{ {
Option<JellyfinConnection> maybeConnection = jellyfinMediaSource.Connections.HeadOrNone(); Option<JellyfinConnection> maybeConnection = jellyfinMediaSource.Connections.HeadOrNone();
return maybeConnection.Map(connection => new ConnectionParameters(connection)) return maybeConnection.Map(connection => new JellyfinConnectionParameters(
connection.Address,
string.Empty,
connection.JellyfinMediaSourceId))
.ToValidation<BaseError>("Jellyfin media source requires an active connection"); .ToValidation<BaseError>("Jellyfin media source requires an active connection");
} }
private async Task<Validation<BaseError, ConnectionParameters>> MediaSourceMustHaveApiKey( private async Task<Validation<BaseError, JellyfinConnectionParameters>> MediaSourceMustHaveApiKey(
ConnectionParameters connectionParameters) JellyfinConnectionParameters connectionParameters)
{ {
JellyfinSecrets secrets = await _jellyfinSecretStore.ReadSecrets(); JellyfinSecrets secrets = await _jellyfinSecretStore.ReadSecrets();
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address) return Optional(secrets.Address == connectionParameters.Address)
.Where(match => match) .Where(match => match)
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey }) .Map(_ => connectionParameters with { ApiKey = secrets.ApiKey })
.ToValidation<BaseError>("Jellyfin media source requires an api key"); .ToValidation<BaseError>("Jellyfin media source requires an api key");
@ -135,15 +137,10 @@ public class
$"Jellyfin show {request.ShowId} does not exist in library {request.JellyfinLibraryId}.")); $"Jellyfin show {request.ShowId} does not exist in library {request.JellyfinLibraryId}."));
private record RequestParameters( private record RequestParameters(
ConnectionParameters ConnectionParameters, JellyfinConnectionParameters ConnectionParameters,
JellyfinLibrary Library, JellyfinLibrary Library,
string ItemId, string ItemId,
string ShowTitle, string ShowTitle,
bool DeepScan, bool DeepScan,
string BaseUrl); string BaseUrl);
private record ConnectionParameters(JellyfinConnection ActiveConnection)
{
public string? ApiKey { get; init; }
}
} }

12
ErsatzTV.Scanner/Core/Jellyfin/JellyfinCollectionScanner.cs

@ -28,14 +28,14 @@ public class JellyfinCollectionScanner : IJellyfinCollectionScanner
public async Task<Either<BaseError, Unit>> ScanCollections( public async Task<Either<BaseError, Unit>> ScanCollections(
string address, string address,
string apiKey, string authorizationHeader,
int mediaSourceId, int mediaSourceId,
bool deepScan) bool deepScan)
{ {
try try
{ {
// need to call get libraries to find library that contains collections (box sets) // need to call get libraries to find library that contains collections (box sets)
await _jellyfinApiClient.GetLibraries(address, apiKey); await _jellyfinApiClient.GetLibraries(address, authorizationHeader);
var incomingItemIds = new List<string>(); var incomingItemIds = new List<string>();
@ -45,7 +45,7 @@ public class JellyfinCollectionScanner : IJellyfinCollectionScanner
// loop over collections // loop over collections
await foreach ((JellyfinCollection collection, int _) in _jellyfinApiClient.GetCollectionLibraryItems( await foreach ((JellyfinCollection collection, int _) in _jellyfinApiClient.GetCollectionLibraryItems(
address, address,
apiKey, authorizationHeader,
mediaSourceId)) mediaSourceId))
{ {
incomingItemIds.Add(collection.ItemId); incomingItemIds.Add(collection.ItemId);
@ -67,7 +67,7 @@ public class JellyfinCollectionScanner : IJellyfinCollectionScanner
await _jellyfinCollectionRepository.AddCollection(collection); await _jellyfinCollectionRepository.AddCollection(collection);
} }
if (await SyncCollectionItems(address, apiKey, mediaSourceId, collection)) if (await SyncCollectionItems(address, authorizationHeader, mediaSourceId, collection))
{ {
// save collection etag // save collection etag
await _jellyfinCollectionRepository.SetEtag(collection); await _jellyfinCollectionRepository.SetEtag(collection);
@ -92,7 +92,7 @@ public class JellyfinCollectionScanner : IJellyfinCollectionScanner
private async Task<bool> SyncCollectionItems( private async Task<bool> SyncCollectionItems(
string address, string address,
string apiKey, string authorizationHeader,
int mediaSourceId, int mediaSourceId,
JellyfinCollection collection) JellyfinCollection collection)
{ {
@ -101,7 +101,7 @@ public class JellyfinCollectionScanner : IJellyfinCollectionScanner
// get collection items from JF // get collection items from JF
IAsyncEnumerable<Tuple<MediaItem, int>> items = _jellyfinApiClient.GetCollectionItems( IAsyncEnumerable<Tuple<MediaItem, int>> items = _jellyfinApiClient.GetCollectionItems(
address, address,
apiKey, authorizationHeader,
mediaSourceId, mediaSourceId,
collection.ItemId); collection.ItemId);

9
ErsatzTV.Scanner/Core/Jellyfin/JellyfinMovieLibraryScanner.cs

@ -50,8 +50,7 @@ public class JellyfinMovieLibraryScanner :
protected override bool ServerSupportsRemoteStreaming => true; protected override bool ServerSupportsRemoteStreaming => true;
public async Task<Either<BaseError, Unit>> ScanLibrary( public async Task<Either<BaseError, Unit>> ScanLibrary(
string address, JellyfinConnectionParameters connectionParameters,
string apiKey,
JellyfinLibrary library, JellyfinLibrary library,
bool deepScan, bool deepScan,
CancellationToken cancellationToken) CancellationToken cancellationToken)
@ -69,7 +68,7 @@ public class JellyfinMovieLibraryScanner :
return await ScanLibrary( return await ScanLibrary(
_jellyfinMovieRepository, _jellyfinMovieRepository,
new JellyfinConnectionParameters(address, apiKey, library.MediaSourceId), connectionParameters,
library, library,
GetLocalPath, GetLocalPath,
deepScan, deepScan,
@ -85,7 +84,7 @@ public class JellyfinMovieLibraryScanner :
JellyfinLibrary library) => JellyfinLibrary library) =>
_jellyfinApiClient.GetMovieLibraryItems( _jellyfinApiClient.GetMovieLibraryItems(
connectionParameters.Address, connectionParameters.Address,
connectionParameters.ApiKey, connectionParameters.AuthorizationHeader,
library); library);
protected override Task<Option<MovieMetadata>> GetFullMetadata( protected override Task<Option<MovieMetadata>> GetFullMetadata(
@ -113,7 +112,7 @@ public class JellyfinMovieLibraryScanner :
Either<BaseError, MediaVersion> maybeVersion = Either<BaseError, MediaVersion> maybeVersion =
await _jellyfinApiClient.GetPlaybackInfo( await _jellyfinApiClient.GetPlaybackInfo(
connectionParameters.Address, connectionParameters.Address,
connectionParameters.ApiKey, connectionParameters.AuthorizationHeader,
library, library,
incoming.ItemId); incoming.ItemId);

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

@ -57,8 +57,7 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
protected override bool ServerSupportsRemoteStreaming => true; protected override bool ServerSupportsRemoteStreaming => true;
public async Task<Either<BaseError, Unit>> ScanLibrary( public async Task<Either<BaseError, Unit>> ScanLibrary(
string address, JellyfinConnectionParameters connectionParameters,
string apiKey,
JellyfinLibrary library, JellyfinLibrary library,
bool deepScan, bool deepScan,
CancellationToken cancellationToken) CancellationToken cancellationToken)
@ -76,7 +75,7 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
return await ScanLibrary( return await ScanLibrary(
_jellyfinTelevisionRepository, _jellyfinTelevisionRepository,
new JellyfinConnectionParameters(address, apiKey, library.MediaSourceId), connectionParameters,
library, library,
GetLocalPath, GetLocalPath,
deepScan, deepScan,
@ -84,8 +83,7 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
} }
public async Task<Either<BaseError, Unit>> ScanSingleShow( public async Task<Either<BaseError, Unit>> ScanSingleShow(
string address, JellyfinConnectionParameters connectionParameters,
string apiKey,
JellyfinLibrary library, JellyfinLibrary library,
string showId, string showId,
string showTitle, string showTitle,
@ -105,8 +103,8 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
// Search for the specific show // Search for the specific show
Either<BaseError, Option<JellyfinShow>> searchResult = await _jellyfinApiClient.GetSingleShow( Either<BaseError, Option<JellyfinShow>> searchResult = await _jellyfinApiClient.GetSingleShow(
address, connectionParameters.Address,
apiKey, connectionParameters.AuthorizationHeader,
library, library,
showId); showId);
@ -122,7 +120,7 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
return await ScanSingleShowInternal( return await ScanSingleShowInternal(
_jellyfinTelevisionRepository, _jellyfinTelevisionRepository,
new JellyfinConnectionParameters(address, apiKey, library.MediaSourceId), connectionParameters,
library, library,
show, show,
GetLocalPath, GetLocalPath,
@ -142,7 +140,7 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
JellyfinLibrary library) => JellyfinLibrary library) =>
_jellyfinApiClient.GetShowLibraryItemsWithoutPeople( _jellyfinApiClient.GetShowLibraryItemsWithoutPeople(
connectionParameters.Address, connectionParameters.Address,
connectionParameters.ApiKey, connectionParameters.AuthorizationHeader,
library); library);
protected override string MediaServerItemId(JellyfinShow show) => show.ItemId; protected override string MediaServerItemId(JellyfinShow show) => show.ItemId;
@ -159,7 +157,7 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
JellyfinShow show) => JellyfinShow show) =>
_jellyfinApiClient.GetSeasonLibraryItems( _jellyfinApiClient.GetSeasonLibraryItems(
connectionParameters.Address, connectionParameters.Address,
connectionParameters.ApiKey, connectionParameters.AuthorizationHeader,
library, library,
show.ItemId); show.ItemId);
@ -174,14 +172,14 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
{ {
return _jellyfinApiClient.GetEpisodeLibraryItems( return _jellyfinApiClient.GetEpisodeLibraryItems(
connectionParameters.Address, connectionParameters.Address,
connectionParameters.ApiKey, connectionParameters.AuthorizationHeader,
library, library,
season.ItemId); season.ItemId);
} }
return _jellyfinApiClient.GetEpisodeLibraryItemsWithoutPeople( return _jellyfinApiClient.GetEpisodeLibraryItemsWithoutPeople(
connectionParameters.Address, connectionParameters.Address,
connectionParameters.ApiKey, connectionParameters.AuthorizationHeader,
library, library,
season.ItemId); season.ItemId);
} }
@ -197,7 +195,7 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
{ {
Either<BaseError, Option<JellyfinShow>> maybeShowResult = await _jellyfinApiClient.GetSingleShow( Either<BaseError, Option<JellyfinShow>> maybeShowResult = await _jellyfinApiClient.GetSingleShow(
connectionParameters.Address, connectionParameters.Address,
connectionParameters.ApiKey, connectionParameters.AuthorizationHeader,
library, library,
incoming.ItemId); incoming.ItemId);
@ -238,7 +236,7 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
{ {
Either<BaseError, Option<JellyfinEpisode>> maybeEpisodeResult = await _jellyfinApiClient.GetSingleEpisode( Either<BaseError, Option<JellyfinEpisode>> maybeEpisodeResult = await _jellyfinApiClient.GetSingleEpisode(
connectionParameters.Address, connectionParameters.Address,
connectionParameters.ApiKey, connectionParameters.AuthorizationHeader,
library, library,
jellyfinSeason.ItemId, jellyfinSeason.ItemId,
incoming.ItemId); incoming.ItemId);
@ -277,7 +275,7 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
Either<BaseError, MediaVersion> maybeVersion = Either<BaseError, MediaVersion> maybeVersion =
await _jellyfinApiClient.GetPlaybackInfo( await _jellyfinApiClient.GetPlaybackInfo(
connectionParameters.Address, connectionParameters.Address,
connectionParameters.ApiKey, connectionParameters.AuthorizationHeader,
library, library,
incoming.ItemId); incoming.ItemId);

Loading…
Cancel
Save