From 794d209941c78d35676b450ede7148313f3bef4f Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Thu, 12 Feb 2026 08:29:47 -0600 Subject: [PATCH] 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 --- .github/workflows/artifacts.yml | 6 +-- .github/workflows/pr.yml | 8 +-- CHANGELOG.md | 1 + .../Commands/SaveJellyfinSecretsHandler.cs | 4 +- .../SynchronizeJellyfinLibrariesHandler.cs | 40 +++++++------- .../Interfaces/Jellyfin/IJellyfinApiClient.cs | 26 ++++----- .../Jellyfin/IJellyfinCollectionScanner.cs | 6 ++- .../Jellyfin/IJellyfinMovieLibraryScanner.cs | 4 +- .../IJellyfinTelevisionLibraryScanner.cs | 7 ++- .../Jellyfin/JellyfinConnectionParameters.cs | 5 +- .../Jellyfin/IJellyfinApi.cs | 52 +++++++++--------- .../Jellyfin/JellyfinApiClient.cs | 54 +++++++++---------- .../SynchronizeJellyfinCollectionsHandler.cs | 34 ++++++------ .../SynchronizeJellyfinLibraryByIdHandler.cs | 25 ++++----- .../SynchronizeJellyfinShowByIdHandler.cs | 25 ++++----- .../Jellyfin/JellyfinCollectionScanner.cs | 12 ++--- .../Jellyfin/JellyfinMovieLibraryScanner.cs | 9 ++-- .../JellyfinTelevisionLibraryScanner.cs | 28 +++++----- 18 files changed, 174 insertions(+), 172 deletions(-) diff --git a/.github/workflows/artifacts.yml b/.github/workflows/artifacts.yml index 177d62ea5..73db910b2 100644 --- a/.github/workflows/artifacts.yml +++ b/.github/workflows/artifacts.yml @@ -57,7 +57,7 @@ jobs: - name: Setup dotnet uses: actions/setup-dotnet@v4 with: - dotnet-version: '10.0.x' + dotnet-version: '10.0.102' - name: Clean run: dotnet clean --configuration Release && dotnet nuget locals all --clear @@ -172,7 +172,7 @@ jobs: - name: Setup dotnet uses: actions/setup-dotnet@v4 with: - dotnet-version: '10.0.x' + dotnet-version: '10.0.102' - name: Clean run: dotnet clean --configuration Release && dotnet nuget locals all --clear @@ -229,7 +229,7 @@ jobs: - name: Setup dotnet uses: actions/setup-dotnet@v4 with: - dotnet-version: '10.0.x' + dotnet-version: '10.0.102' - name: Clean run: dotnet clean --configuration Release && dotnet nuget locals all --clear diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 259419b7f..0ffbd14c1 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -14,7 +14,7 @@ jobs: - name: Setup dotnet uses: actions/setup-dotnet@v4 with: - dotnet-version: '10.0.x' + dotnet-version: '10.0.102' - name: Clean run: dotnet clean --configuration Release && dotnet nuget locals all --clear @@ -36,7 +36,7 @@ jobs: - name: Setup dotnet uses: actions/setup-dotnet@v4 with: - dotnet-version: '10.0.x' + dotnet-version: '10.0.102' - name: Clean run: dotnet clean --configuration Release && dotnet nuget locals all --clear @@ -72,7 +72,7 @@ jobs: - name: Setup dotnet uses: actions/setup-dotnet@v4 with: - dotnet-version: '10.0.x' + dotnet-version: '10.0.102' - name: Clean run: dotnet clean --configuration Release && dotnet nuget locals all --clear @@ -100,7 +100,7 @@ jobs: - name: Setup dotnet uses: actions/setup-dotnet@v4 with: - dotnet-version: '10.0.x' + dotnet-version: '10.0.102' - name: Clean run: dotnet clean --configuration Release && dotnet nuget locals all --clear diff --git a/CHANGELOG.md b/CHANGELOG.md index 947e3e39a..2c289ee07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Changed - Move dark/light mode toggle to **Settings** > **UI** +- Use latest (non-deprecated) authorization method with Jellyfin API ### Fixed - Improve stability of playback orders `Shuffle` and `Shuffle in Order` over time diff --git a/ErsatzTV.Application/Jellyfin/Commands/SaveJellyfinSecretsHandler.cs b/ErsatzTV.Application/Jellyfin/Commands/SaveJellyfinSecretsHandler.cs index fcf302bfd..8141bba4c 100644 --- a/ErsatzTV.Application/Jellyfin/Commands/SaveJellyfinSecretsHandler.cs +++ b/ErsatzTV.Application/Jellyfin/Commands/SaveJellyfinSecretsHandler.cs @@ -36,8 +36,10 @@ public class SaveJellyfinSecretsHandler : IRequestHandler> Validate(SaveJellyfinSecrets request) { + var connectionParameters = new JellyfinConnectionParameters(request.Secrets.Address, request.Secrets.ApiKey, 0); + Either maybeServerInformation = await _jellyfinApiClient - .GetServerInformation(request.Secrets.Address, request.Secrets.ApiKey); + .GetServerInformation(connectionParameters.Address, connectionParameters.AuthorizationHeader); return maybeServerInformation.Match( info => Validation.Success(new Parameters(request.Secrets, info)), diff --git a/ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinLibrariesHandler.cs b/ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinLibrariesHandler.cs index 508a9d081..2d70a6e04 100644 --- a/ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinLibrariesHandler.cs +++ b/ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinLibrariesHandler.cs @@ -38,7 +38,7 @@ public class .MapT(p => SynchronizeLibraries(p, cancellationToken)) .Bind(v => v.ToEitherAsync()); - private Task> Validate(SynchronizeJellyfinLibraries request) => + private Task> Validate(SynchronizeJellyfinLibraries request) => MediaSourceMustExist(request) .BindT(MediaSourceMustHaveActiveConnection) .BindT(MediaSourceMustHaveApiKey); @@ -48,43 +48,48 @@ public class _mediaSourceRepository.GetJellyfin(request.JellyfinMediaSourceId) .Map(o => o.ToValidation("Jellyfin media source does not exist.")); - private Validation MediaSourceMustHaveActiveConnection( + private Validation MediaSourceMustHaveActiveConnection( JellyfinMediaSource jellyfinMediaSource) { Option 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("Jellyfin media source requires an active connection"); } - private async Task> MediaSourceMustHaveApiKey( - ConnectionParameters connectionParameters) + private async Task> MediaSourceMustHaveApiKey( + ConnectionAndSource connectionAndSource) { JellyfinSecrets secrets = await _jellyfinSecretStore.ReadSecrets(); - return Optional(secrets.Address == connectionParameters.ActiveConnection.Address) + return Optional(secrets.Address == connectionAndSource.ConnectionParameters.Address) .Where(match => match) - .Map(_ => connectionParameters with { ApiKey = secrets.ApiKey }) + .Map(_ => connectionAndSource with + { + ConnectionParameters = connectionAndSource.ConnectionParameters with { ApiKey = secrets.ApiKey } + }) .ToValidation("Jellyfin media source requires an api key"); } private async Task SynchronizeLibraries( - ConnectionParameters connectionParameters, + ConnectionAndSource connectionAndSource, CancellationToken cancellationToken) { Either> maybeLibraries = await _jellyfinApiClient.GetLibraries( - connectionParameters.ActiveConnection.Address, - connectionParameters.ApiKey); + connectionAndSource.ConnectionParameters.Address, + connectionAndSource.ConnectionParameters.AuthorizationHeader); foreach (BaseError error in maybeLibraries.LeftToSeq()) { _logger.LogWarning( "Unable to synchronize libraries from jellyfin server {JellyfinServer}: {Error}", - connectionParameters.JellyfinMediaSource.ServerName, + connectionAndSource.MediaSource.ServerName, error.Value); } foreach (List libraries in maybeLibraries.RightToSeq()) { - var existing = connectionParameters.JellyfinMediaSource.Libraries + var existing = connectionAndSource.MediaSource.Libraries .OfType() .ToList(); var toAdd = libraries.Filter(library => existing.All(l => l.ItemId != library.ItemId)).ToList(); @@ -92,7 +97,7 @@ public class var toUpdate = libraries .Filter(l => toAdd.All(a => a.ItemId != l.ItemId) && toRemove.All(r => r.ItemId != l.ItemId)).ToList(); List ids = await _mediaSourceRepository.UpdateLibraries( - connectionParameters.JellyfinMediaSource.Id, + connectionAndSource.MediaSource.Id, toAdd, toRemove, toUpdate, @@ -107,10 +112,7 @@ public class return Unit.Default; } - private sealed record ConnectionParameters( - JellyfinMediaSource JellyfinMediaSource, - JellyfinConnection ActiveConnection) - { - public string ApiKey { get; set; } - } + private sealed record ConnectionAndSource( + JellyfinConnectionParameters ConnectionParameters, + JellyfinMediaSource MediaSource); } diff --git a/ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinApiClient.cs b/ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinApiClient.cs index b3b64ef5a..1b23a3b3f 100644 --- a/ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinApiClient.cs +++ b/ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinApiClient.cs @@ -5,69 +5,69 @@ namespace ErsatzTV.Core.Interfaces.Jellyfin; public interface IJellyfinApiClient { - Task> GetServerInformation(string address, string apiKey); - Task>> GetLibraries(string address, string apiKey); + Task> GetServerInformation(string address, string authorizationHeader); + Task>> GetLibraries(string address, string authorizationHeader); IAsyncEnumerable> GetMovieLibraryItems( string address, - string apiKey, + string authorizationHeader, JellyfinLibrary library); IAsyncEnumerable> GetShowLibraryItemsWithoutPeople( string address, - string apiKey, + string authorizationHeader, JellyfinLibrary library); IAsyncEnumerable> GetSeasonLibraryItems( string address, - string apiKey, + string authorizationHeader, JellyfinLibrary library, string showId); IAsyncEnumerable> GetEpisodeLibraryItems( string address, - string apiKey, + string authorizationHeader, JellyfinLibrary library, string seasonId); IAsyncEnumerable> GetEpisodeLibraryItemsWithoutPeople( string address, - string apiKey, + string authorizationHeader, JellyfinLibrary library, string seasonId); IAsyncEnumerable> GetCollectionLibraryItems( string address, - string apiKey, + string authorizationHeader, int mediaSourceId); IAsyncEnumerable> GetCollectionItems( string address, - string apiKey, + string authorizationHeader, int mediaSourceId, string collectionId); Task> GetPlaybackInfo( string address, - string apiKey, + string authorizationHeader, JellyfinLibrary library, string itemId); Task>> GetSingleShow( string address, - string apiKey, + string authorizationHeader, JellyfinLibrary library, string showId); Task>> SearchShowsByTitle( string address, - string apiKey, + string authorizationHeader, JellyfinLibrary library, string showTitle); Task>> GetSingleEpisode( string address, - string apiKey, + string authorizationHeader, JellyfinLibrary library, string seasonId, string episodeId); diff --git a/ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinCollectionScanner.cs b/ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinCollectionScanner.cs index f1dade4e7..1f62e2311 100644 --- a/ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinCollectionScanner.cs +++ b/ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinCollectionScanner.cs @@ -2,5 +2,9 @@ public interface IJellyfinCollectionScanner { - Task> ScanCollections(string address, string apiKey, int mediaSourceId, bool deepScan); + Task> ScanCollections( + string address, + string authorizationHeader, + int mediaSourceId, + bool deepScan); } diff --git a/ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinMovieLibraryScanner.cs b/ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinMovieLibraryScanner.cs index d717cc46f..e7ad56ba7 100644 --- a/ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinMovieLibraryScanner.cs +++ b/ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinMovieLibraryScanner.cs @@ -1,12 +1,12 @@ using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Jellyfin; namespace ErsatzTV.Core.Interfaces.Jellyfin; public interface IJellyfinMovieLibraryScanner { Task> ScanLibrary( - string address, - string apiKey, + JellyfinConnectionParameters connectionParameters, JellyfinLibrary library, bool deepScan, CancellationToken cancellationToken); diff --git a/ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinTelevisionLibraryScanner.cs b/ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinTelevisionLibraryScanner.cs index cbe7e3e3a..895780421 100644 --- a/ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinTelevisionLibraryScanner.cs +++ b/ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinTelevisionLibraryScanner.cs @@ -1,19 +1,18 @@ using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Jellyfin; namespace ErsatzTV.Core.Interfaces.Jellyfin; public interface IJellyfinTelevisionLibraryScanner { Task> ScanLibrary( - string address, - string apiKey, + JellyfinConnectionParameters connectionParameters, JellyfinLibrary library, bool deepScan, CancellationToken cancellationToken); Task> ScanSingleShow( - string address, - string apiKey, + JellyfinConnectionParameters connectionParameters, JellyfinLibrary library, string showId, string showTitle, diff --git a/ErsatzTV.Core/Jellyfin/JellyfinConnectionParameters.cs b/ErsatzTV.Core/Jellyfin/JellyfinConnectionParameters.cs index a388b39ce..08c69de59 100644 --- a/ErsatzTV.Core/Jellyfin/JellyfinConnectionParameters.cs +++ b/ErsatzTV.Core/Jellyfin/JellyfinConnectionParameters.cs @@ -3,4 +3,7 @@ namespace ErsatzTV.Core.Jellyfin; public record JellyfinConnectionParameters(string Address, string ApiKey, int MediaSourceId) - : MediaServerConnectionParameters; + : MediaServerConnectionParameters +{ + public string AuthorizationHeader => $"MediaBrowser Token={ApiKey}"; +} diff --git a/ErsatzTV.Infrastructure/Jellyfin/IJellyfinApi.cs b/ErsatzTV.Infrastructure/Jellyfin/IJellyfinApi.cs index af43b2a4d..1a895271f 100644 --- a/ErsatzTV.Infrastructure/Jellyfin/IJellyfinApi.cs +++ b/ErsatzTV.Infrastructure/Jellyfin/IJellyfinApi.cs @@ -8,24 +8,24 @@ public interface IJellyfinApi { [Get("/System/Info")] Task GetSystemInformation( - [Header("X-Emby-Token")] - string apiKey, + [Header("Authorization")] + string authorizationHeader, CancellationToken cancellationToken); [Get("/Users")] Task> GetUsers( - [Header("X-Emby-Token")] - string apiKey); + [Header("Authorization")] + string authorizationHeader); [Get("/Library/VirtualFolders")] Task> GetLibraries( - [Header("X-Emby-Token")] - string apiKey); + [Header("Authorization")] + string authorizationHeader); [Get("/Items?sortOrder=Ascending&sortBy=SortName")] Task GetMovieLibraryItems( - [Header("X-Emby-Token")] - string apiKey, + [Header("Authorization")] + string authorizationHeader, [Query] string parentId, [Query] @@ -44,8 +44,8 @@ public interface IJellyfinApi [Get("/Items?sortOrder=Ascending&sortBy=SortName")] Task GetShowLibraryItemsWithoutPeople( - [Header("X-Emby-Token")] - string apiKey, + [Header("Authorization")] + string authorizationHeader, [Query] string parentId, [Query] @@ -64,8 +64,8 @@ public interface IJellyfinApi [Get("/Items?sortOrder=Ascending&sortBy=SortName")] Task GetShowLibraryItems( - [Header("X-Emby-Token")] - string apiKey, + [Header("Authorization")] + string authorizationHeader, [Query] string parentId, [Query] @@ -84,8 +84,8 @@ public interface IJellyfinApi [Get("/Items?sortOrder=Ascending&sortBy=SortName")] Task GetSeasonLibraryItems( - [Header("X-Emby-Token")] - string apiKey, + [Header("Authorization")] + string authorizationHeader, [Query] string parentId, [Query] @@ -101,8 +101,8 @@ public interface IJellyfinApi [Get("/Items?sortOrder=Ascending&sortBy=SortName")] Task GetEpisodeLibraryItems( - [Header("X-Emby-Token")] - string apiKey, + [Header("Authorization")] + string authorizationHeader, [Query] string parentId, [Query] @@ -120,8 +120,8 @@ public interface IJellyfinApi [Get("/Items?sortOrder=Ascending&sortBy=SortName")] Task GetEpisodeLibraryItemsWithoutPeople( - [Header("X-Emby-Token")] - string apiKey, + [Header("Authorization")] + string authorizationHeader, [Query] string parentId, [Query] @@ -137,8 +137,8 @@ public interface IJellyfinApi [Get("/Items?sortOrder=Ascending&sortBy=SortName")] Task GetCollectionLibraryItems( - [Header("X-Emby-Token")] - string apiKey, + [Header("Authorization")] + string authorizationHeader, [Query] string parentId, [Query] @@ -154,8 +154,8 @@ public interface IJellyfinApi [Get("/Items?sortOrder=Ascending&sortBy=SortName")] Task GetCollectionItems( - [Header("X-Emby-Token")] - string apiKey, + [Header("Authorization")] + string authorizationHeader, [Query] string parentId, [Query] @@ -171,14 +171,14 @@ public interface IJellyfinApi [Get("/Items/{itemId}/PlaybackInfo")] Task GetPlaybackInfo( - [Header("X-Emby-Token")] - string apiKey, + [Header("Authorization")] + string authorizationHeader, string itemId); [Get("/Search/Hints")] Task SearchHints( - [Header("X-Emby-Token")] - string apiKey, + [Header("Authorization")] + string authorizationHeader, [Query] string searchTerm, [Query] diff --git a/ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs b/ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs index 5b1e48ee0..a1a8a6118 100644 --- a/ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs +++ b/ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs @@ -36,14 +36,14 @@ public class JellyfinApiClient : IJellyfinApiClient public async Task> GetServerInformation( string address, - string apiKey) + string authorizationHeader) { try { IJellyfinApi service = ServiceForAddress(address); var cts = new CancellationTokenSource(); 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)); } catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException) @@ -58,12 +58,12 @@ public class JellyfinApiClient : IJellyfinApiClient } } - public async Task>> GetLibraries(string address, string apiKey) + public async Task>> GetLibraries(string address, string authorizationHeader) { try { IJellyfinApi service = ServiceForAddress(address); - List libraries = await service.GetLibraries(apiKey); + List libraries = await service.GetLibraries(authorizationHeader); return libraries .Map(Project) .Somes() @@ -78,7 +78,7 @@ public class JellyfinApiClient : IJellyfinApiClient public IAsyncEnumerable> GetMovieLibraryItems( string address, - string apiKey, + string authorizationHeader, JellyfinLibrary library) => GetPagedLibraryItems( "JF Movies", @@ -87,7 +87,7 @@ public class JellyfinApiClient : IJellyfinApiClient library.MediaSourceId, library.ItemId, (service, itemId, skip, pageSize) => service.GetMovieLibraryItems( - apiKey, + authorizationHeader, itemId, startIndex: skip, limit: pageSize), @@ -95,7 +95,7 @@ public class JellyfinApiClient : IJellyfinApiClient public IAsyncEnumerable> GetShowLibraryItemsWithoutPeople( string address, - string apiKey, + string authorizationHeader, JellyfinLibrary library) => GetPagedLibraryItems( "JF Shows", @@ -104,7 +104,7 @@ public class JellyfinApiClient : IJellyfinApiClient library.MediaSourceId, library.ItemId, (service, itemId, skip, pageSize) => service.GetShowLibraryItemsWithoutPeople( - apiKey, + authorizationHeader, itemId, startIndex: skip, limit: pageSize), @@ -112,7 +112,7 @@ public class JellyfinApiClient : IJellyfinApiClient public IAsyncEnumerable> GetSeasonLibraryItems( string address, - string apiKey, + string authorizationHeader, JellyfinLibrary library, string showId) => GetPagedLibraryItems( @@ -122,7 +122,7 @@ public class JellyfinApiClient : IJellyfinApiClient library.MediaSourceId, showId, (service, _, skip, pageSize) => service.GetSeasonLibraryItems( - apiKey, + authorizationHeader, showId, startIndex: skip, limit: pageSize), @@ -130,7 +130,7 @@ public class JellyfinApiClient : IJellyfinApiClient public IAsyncEnumerable> GetEpisodeLibraryItems( string address, - string apiKey, + string authorizationHeader, JellyfinLibrary library, string seasonId) => GetPagedLibraryItems( @@ -140,7 +140,7 @@ public class JellyfinApiClient : IJellyfinApiClient library.MediaSourceId, seasonId, (service, _, skip, pageSize) => service.GetEpisodeLibraryItems( - apiKey, + authorizationHeader, seasonId, startIndex: skip, limit: pageSize), @@ -148,7 +148,7 @@ public class JellyfinApiClient : IJellyfinApiClient public IAsyncEnumerable> GetEpisodeLibraryItemsWithoutPeople( string address, - string apiKey, + string authorizationHeader, JellyfinLibrary library, string seasonId) => GetPagedLibraryItems( @@ -158,7 +158,7 @@ public class JellyfinApiClient : IJellyfinApiClient library.MediaSourceId, seasonId, (service, _, skip, pageSize) => service.GetEpisodeLibraryItemsWithoutPeople( - apiKey, + authorizationHeader, seasonId, startIndex: skip, limit: pageSize), @@ -166,7 +166,7 @@ public class JellyfinApiClient : IJellyfinApiClient public IAsyncEnumerable> GetCollectionLibraryItems( string address, - string apiKey, + string authorizationHeader, int mediaSourceId) { // TODO: should we enumerate collection libraries here? @@ -180,7 +180,7 @@ public class JellyfinApiClient : IJellyfinApiClient mediaSourceId, itemId, (service, _, skip, pageSize) => service.GetCollectionLibraryItems( - apiKey, + authorizationHeader, itemId, startIndex: skip, limit: pageSize), @@ -192,7 +192,7 @@ public class JellyfinApiClient : IJellyfinApiClient public IAsyncEnumerable> GetCollectionItems( string address, - string apiKey, + string authorizationHeader, int mediaSourceId, string collectionId) => GetPagedLibraryItems( @@ -202,7 +202,7 @@ public class JellyfinApiClient : IJellyfinApiClient mediaSourceId, collectionId, (service, _, skip, pageSize) => service.GetCollectionItems( - apiKey, + authorizationHeader, collectionId, startIndex: skip, limit: pageSize), @@ -210,7 +210,7 @@ public class JellyfinApiClient : IJellyfinApiClient public async Task> GetPlaybackInfo( string address, - string apiKey, + string authorizationHeader, JellyfinLibrary library, string itemId) { @@ -219,7 +219,7 @@ public class JellyfinApiClient : IJellyfinApiClient using (ScanProfiler.Measure("JF Playback Info")) { IJellyfinApi service = ServiceForAddress(address); - JellyfinPlaybackInfoResponse playbackInfo = await service.GetPlaybackInfo(apiKey, itemId); + JellyfinPlaybackInfoResponse playbackInfo = await service.GetPlaybackInfo(authorizationHeader, itemId); Option maybeVersion = ProjectToMediaVersion(playbackInfo); return maybeVersion.ToEither(() => BaseError.New("Unable to locate Jellyfin statistics")); } @@ -233,7 +233,7 @@ public class JellyfinApiClient : IJellyfinApiClient public async Task>> GetSingleShow( string address, - string apiKey, + string authorizationHeader, JellyfinLibrary library, string showId) { @@ -243,7 +243,7 @@ public class JellyfinApiClient : IJellyfinApiClient { IJellyfinApi service = ServiceForAddress(address); JellyfinLibraryItemsResponse itemsResponse = await service.GetShowLibraryItems( - apiKey, + authorizationHeader, parentId: library.ItemId, recursive: false, startIndex: 0, @@ -267,7 +267,7 @@ public class JellyfinApiClient : IJellyfinApiClient public async Task>> SearchShowsByTitle( string address, - string apiKey, + string authorizationHeader, JellyfinLibrary library, string showTitle) { @@ -275,7 +275,7 @@ public class JellyfinApiClient : IJellyfinApiClient { IJellyfinApi service = ServiceForAddress(address); JellyfinSearchHintsResponse searchResponse = await service.SearchHints( - apiKey, + authorizationHeader, showTitle, "Series", library.ItemId); @@ -288,7 +288,7 @@ public class JellyfinApiClient : IJellyfinApiClient string.Equals(hint.Name, showTitle, StringComparison.OrdinalIgnoreCase)) { JellyfinLibraryItemsResponse detailResponse = await service.GetShowLibraryItems( - apiKey, + authorizationHeader, hint.Id, recursive: false, startIndex: 0, @@ -316,7 +316,7 @@ public class JellyfinApiClient : IJellyfinApiClient public async Task>> GetSingleEpisode( string address, - string apiKey, + string authorizationHeader, JellyfinLibrary library, string seasonId, string episodeId) @@ -327,7 +327,7 @@ public class JellyfinApiClient : IJellyfinApiClient { IJellyfinApi service = ServiceForAddress(address); JellyfinLibraryItemsResponse itemsResponse = await service.GetEpisodeLibraryItems( - apiKey, + authorizationHeader, parentId: seasonId, recursive: false, startIndex: 0, diff --git a/ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinCollectionsHandler.cs b/ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinCollectionsHandler.cs index 2a3bf3a90..4e121e7cb 100644 --- a/ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinCollectionsHandler.cs +++ b/ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinCollectionsHandler.cs @@ -45,13 +45,13 @@ public class SynchronizeJellyfinCollections request, CancellationToken cancellationToken) { - Task> mediaSource = MediaSourceMustExist(request) + Task> mediaSource = MediaSourceMustExist(request) .BindT(MediaSourceMustHaveActiveConnection) .BindT(MediaSourceMustHaveApiKey); return (await mediaSource, await ValidateLibraryRefreshInterval(cancellationToken)) .Apply((connectionParameters, libraryRefreshInterval) => new RequestParameters( - connectionParameters, + connectionParameters.ConnectionParameters, connectionParameters.MediaSource, request.ForceScan, request.DeepScan, @@ -69,21 +69,26 @@ public class _mediaSourceRepository.GetJellyfin(request.JellyfinMediaSourceId) .Map(o => o.ToValidation("Jellyfin media source does not exist.")); - private static Validation MediaSourceMustHaveActiveConnection( + private static Validation MediaSourceMustHaveActiveConnection( JellyfinMediaSource jellyfinMediaSource) { Option 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("Jellyfin media source requires an active connection"); } - private async Task> MediaSourceMustHaveApiKey( - ConnectionParameters connectionParameters) + private async Task> MediaSourceMustHaveApiKey( + ConnectionAndSource connectionAndSource) { JellyfinSecrets secrets = await _jellyfinSecretStore.ReadSecrets(); - return Optional(secrets.Address == connectionParameters.ActiveConnection.Address) + return Optional(secrets.Address == connectionAndSource.ConnectionParameters.Address) .Where(match => match) - .Map(_ => connectionParameters with { ApiKey = secrets.ApiKey }) + .Map(_ => connectionAndSource with + { + ConnectionParameters = connectionAndSource.ConnectionParameters with { ApiKey = secrets.ApiKey } + }) .ToValidation("Jellyfin media source requires an api key"); } @@ -98,8 +103,8 @@ public class if (parameters.ForceScan || parameters.LibraryRefreshInterval > 0 && nextScan < DateTimeOffset.Now) { Either result = await _scanner.ScanCollections( - parameters.ConnectionParameters.ActiveConnection.Address, - parameters.ConnectionParameters.ApiKey, + parameters.ConnectionParameters.Address, + parameters.ConnectionParameters.AuthorizationHeader, parameters.MediaSource.Id, parameters.DeepScan); @@ -116,15 +121,14 @@ public class } private record RequestParameters( - ConnectionParameters ConnectionParameters, + JellyfinConnectionParameters ConnectionParameters, JellyfinMediaSource MediaSource, bool ForceScan, bool DeepScan, int LibraryRefreshInterval, string BaseUrl); - private record ConnectionParameters(JellyfinMediaSource MediaSource, JellyfinConnection ActiveConnection) - { - public string? ApiKey { get; init; } - } + private sealed record ConnectionAndSource( + JellyfinConnectionParameters ConnectionParameters, + JellyfinMediaSource MediaSource); } diff --git a/ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinLibraryByIdHandler.cs b/ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinLibraryByIdHandler.cs index a3a79f787..1bcc2abc2 100644 --- a/ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinLibraryByIdHandler.cs +++ b/ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinLibraryByIdHandler.cs @@ -65,15 +65,13 @@ public class { LibraryMediaKind.Movies => await _jellyfinMovieLibraryScanner.ScanLibrary( - parameters.ConnectionParameters.ActiveConnection.Address, - parameters.ConnectionParameters.ApiKey, + parameters.ConnectionParameters, parameters.Library, parameters.DeepScan, cancellationToken), LibraryMediaKind.Shows => await _jellyfinTelevisionLibraryScanner.ScanLibrary( - parameters.ConnectionParameters.ActiveConnection.Address, - parameters.ConnectionParameters.ApiKey, + parameters.ConnectionParameters, parameters.Library, parameters.DeepScan, cancellationToken), @@ -114,7 +112,7 @@ public class request.BaseUrl )); - private Task> ValidateConnection( + private Task> ValidateConnection( SynchronizeJellyfinLibraryById request) => JellyfinMediaSourceMustExist(request) .BindT(MediaSourceMustHaveActiveConnection) @@ -126,19 +124,19 @@ public class .Map(v => v.ToValidation( $"Jellyfin media source for library {request.JellyfinLibraryId} does not exist.")); - private Validation MediaSourceMustHaveActiveConnection( + private Validation MediaSourceMustHaveActiveConnection( JellyfinMediaSource jellyfinMediaSource) { Option maybeConnection = jellyfinMediaSource.Connections.HeadOrNone(); - return maybeConnection.Map(connection => new ConnectionParameters(connection)) + return maybeConnection.Map(connection => new JellyfinConnectionParameters(connection.Address, string.Empty, connection.JellyfinMediaSourceId)) .ToValidation("Jellyfin media source requires an active connection"); } - private async Task> MediaSourceMustHaveApiKey( - ConnectionParameters connectionParameters) + private async Task> MediaSourceMustHaveApiKey( + JellyfinConnectionParameters connectionParameters) { JellyfinSecrets secrets = await _jellyfinSecretStore.ReadSecrets(); - return Optional(secrets.Address == connectionParameters.ActiveConnection.Address) + return Optional(secrets.Address == connectionParameters.Address) .Where(match => match) .Map(_ => connectionParameters with { ApiKey = secrets.ApiKey }) .ToValidation("Jellyfin media source requires an api key"); @@ -155,15 +153,10 @@ public class .Map(lri => lri.ToValidation("Library refresh interval is invalid")); private record RequestParameters( - ConnectionParameters ConnectionParameters, + JellyfinConnectionParameters ConnectionParameters, JellyfinLibrary Library, bool ForceScan, int LibraryRefreshInterval, bool DeepScan, string BaseUrl); - - private record ConnectionParameters(JellyfinConnection ActiveConnection) - { - public string? ApiKey { get; init; } - } } diff --git a/ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinShowByIdHandler.cs b/ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinShowByIdHandler.cs index dd14bcc71..b40467b6f 100644 --- a/ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinShowByIdHandler.cs +++ b/ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinShowByIdHandler.cs @@ -61,8 +61,7 @@ public class parameters.Library.Name); Either result = await _jellyfinTelevisionLibraryScanner.ScanSingleShow( - parameters.ConnectionParameters.ActiveConnection.Address, - parameters.ConnectionParameters.ApiKey, + parameters.ConnectionParameters, parameters.Library, parameters.ItemId, parameters.ShowTitle, @@ -92,7 +91,7 @@ public class request.BaseUrl )); - private Task> ValidateConnection( + private Task> ValidateConnection( SynchronizeJellyfinShowById request) => JellyfinMediaSourceMustExist(request) .BindT(MediaSourceMustHaveActiveConnection) @@ -104,19 +103,22 @@ public class .Map(v => v.ToValidation( $"Jellyfin media source for library {request.JellyfinLibraryId} does not exist.")); - private Validation MediaSourceMustHaveActiveConnection( + private Validation MediaSourceMustHaveActiveConnection( JellyfinMediaSource jellyfinMediaSource) { Option maybeConnection = jellyfinMediaSource.Connections.HeadOrNone(); - return maybeConnection.Map(connection => new ConnectionParameters(connection)) + return maybeConnection.Map(connection => new JellyfinConnectionParameters( + connection.Address, + string.Empty, + connection.JellyfinMediaSourceId)) .ToValidation("Jellyfin media source requires an active connection"); } - private async Task> MediaSourceMustHaveApiKey( - ConnectionParameters connectionParameters) + private async Task> MediaSourceMustHaveApiKey( + JellyfinConnectionParameters connectionParameters) { JellyfinSecrets secrets = await _jellyfinSecretStore.ReadSecrets(); - return Optional(secrets.Address == connectionParameters.ActiveConnection.Address) + return Optional(secrets.Address == connectionParameters.Address) .Where(match => match) .Map(_ => connectionParameters with { ApiKey = secrets.ApiKey }) .ToValidation("Jellyfin media source requires an api key"); @@ -135,15 +137,10 @@ public class $"Jellyfin show {request.ShowId} does not exist in library {request.JellyfinLibraryId}.")); private record RequestParameters( - ConnectionParameters ConnectionParameters, + JellyfinConnectionParameters ConnectionParameters, JellyfinLibrary Library, string ItemId, string ShowTitle, bool DeepScan, string BaseUrl); - - private record ConnectionParameters(JellyfinConnection ActiveConnection) - { - public string? ApiKey { get; init; } - } } diff --git a/ErsatzTV.Scanner/Core/Jellyfin/JellyfinCollectionScanner.cs b/ErsatzTV.Scanner/Core/Jellyfin/JellyfinCollectionScanner.cs index df01c0dae..1b08c4693 100644 --- a/ErsatzTV.Scanner/Core/Jellyfin/JellyfinCollectionScanner.cs +++ b/ErsatzTV.Scanner/Core/Jellyfin/JellyfinCollectionScanner.cs @@ -28,14 +28,14 @@ public class JellyfinCollectionScanner : IJellyfinCollectionScanner public async Task> ScanCollections( string address, - string apiKey, + string authorizationHeader, int mediaSourceId, bool deepScan) { try { // 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(); @@ -45,7 +45,7 @@ public class JellyfinCollectionScanner : IJellyfinCollectionScanner // loop over collections await foreach ((JellyfinCollection collection, int _) in _jellyfinApiClient.GetCollectionLibraryItems( address, - apiKey, + authorizationHeader, mediaSourceId)) { incomingItemIds.Add(collection.ItemId); @@ -67,7 +67,7 @@ public class JellyfinCollectionScanner : IJellyfinCollectionScanner await _jellyfinCollectionRepository.AddCollection(collection); } - if (await SyncCollectionItems(address, apiKey, mediaSourceId, collection)) + if (await SyncCollectionItems(address, authorizationHeader, mediaSourceId, collection)) { // save collection etag await _jellyfinCollectionRepository.SetEtag(collection); @@ -92,7 +92,7 @@ public class JellyfinCollectionScanner : IJellyfinCollectionScanner private async Task SyncCollectionItems( string address, - string apiKey, + string authorizationHeader, int mediaSourceId, JellyfinCollection collection) { @@ -101,7 +101,7 @@ public class JellyfinCollectionScanner : IJellyfinCollectionScanner // get collection items from JF IAsyncEnumerable> items = _jellyfinApiClient.GetCollectionItems( address, - apiKey, + authorizationHeader, mediaSourceId, collection.ItemId); diff --git a/ErsatzTV.Scanner/Core/Jellyfin/JellyfinMovieLibraryScanner.cs b/ErsatzTV.Scanner/Core/Jellyfin/JellyfinMovieLibraryScanner.cs index a5c3c6e9e..bdf764141 100644 --- a/ErsatzTV.Scanner/Core/Jellyfin/JellyfinMovieLibraryScanner.cs +++ b/ErsatzTV.Scanner/Core/Jellyfin/JellyfinMovieLibraryScanner.cs @@ -50,8 +50,7 @@ public class JellyfinMovieLibraryScanner : protected override bool ServerSupportsRemoteStreaming => true; public async Task> ScanLibrary( - string address, - string apiKey, + JellyfinConnectionParameters connectionParameters, JellyfinLibrary library, bool deepScan, CancellationToken cancellationToken) @@ -69,7 +68,7 @@ public class JellyfinMovieLibraryScanner : return await ScanLibrary( _jellyfinMovieRepository, - new JellyfinConnectionParameters(address, apiKey, library.MediaSourceId), + connectionParameters, library, GetLocalPath, deepScan, @@ -85,7 +84,7 @@ public class JellyfinMovieLibraryScanner : JellyfinLibrary library) => _jellyfinApiClient.GetMovieLibraryItems( connectionParameters.Address, - connectionParameters.ApiKey, + connectionParameters.AuthorizationHeader, library); protected override Task> GetFullMetadata( @@ -113,7 +112,7 @@ public class JellyfinMovieLibraryScanner : Either maybeVersion = await _jellyfinApiClient.GetPlaybackInfo( connectionParameters.Address, - connectionParameters.ApiKey, + connectionParameters.AuthorizationHeader, library, incoming.ItemId); diff --git a/ErsatzTV.Scanner/Core/Jellyfin/JellyfinTelevisionLibraryScanner.cs b/ErsatzTV.Scanner/Core/Jellyfin/JellyfinTelevisionLibraryScanner.cs index 4c6f603af..2f2b475fb 100644 --- a/ErsatzTV.Scanner/Core/Jellyfin/JellyfinTelevisionLibraryScanner.cs +++ b/ErsatzTV.Scanner/Core/Jellyfin/JellyfinTelevisionLibraryScanner.cs @@ -57,8 +57,7 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan protected override bool ServerSupportsRemoteStreaming => true; public async Task> ScanLibrary( - string address, - string apiKey, + JellyfinConnectionParameters connectionParameters, JellyfinLibrary library, bool deepScan, CancellationToken cancellationToken) @@ -76,7 +75,7 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan return await ScanLibrary( _jellyfinTelevisionRepository, - new JellyfinConnectionParameters(address, apiKey, library.MediaSourceId), + connectionParameters, library, GetLocalPath, deepScan, @@ -84,8 +83,7 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan } public async Task> ScanSingleShow( - string address, - string apiKey, + JellyfinConnectionParameters connectionParameters, JellyfinLibrary library, string showId, string showTitle, @@ -105,8 +103,8 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan // Search for the specific show Either> searchResult = await _jellyfinApiClient.GetSingleShow( - address, - apiKey, + connectionParameters.Address, + connectionParameters.AuthorizationHeader, library, showId); @@ -122,7 +120,7 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan return await ScanSingleShowInternal( _jellyfinTelevisionRepository, - new JellyfinConnectionParameters(address, apiKey, library.MediaSourceId), + connectionParameters, library, show, GetLocalPath, @@ -142,7 +140,7 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan JellyfinLibrary library) => _jellyfinApiClient.GetShowLibraryItemsWithoutPeople( connectionParameters.Address, - connectionParameters.ApiKey, + connectionParameters.AuthorizationHeader, library); protected override string MediaServerItemId(JellyfinShow show) => show.ItemId; @@ -159,7 +157,7 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan JellyfinShow show) => _jellyfinApiClient.GetSeasonLibraryItems( connectionParameters.Address, - connectionParameters.ApiKey, + connectionParameters.AuthorizationHeader, library, show.ItemId); @@ -174,14 +172,14 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan { return _jellyfinApiClient.GetEpisodeLibraryItems( connectionParameters.Address, - connectionParameters.ApiKey, + connectionParameters.AuthorizationHeader, library, season.ItemId); } return _jellyfinApiClient.GetEpisodeLibraryItemsWithoutPeople( connectionParameters.Address, - connectionParameters.ApiKey, + connectionParameters.AuthorizationHeader, library, season.ItemId); } @@ -197,7 +195,7 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan { Either> maybeShowResult = await _jellyfinApiClient.GetSingleShow( connectionParameters.Address, - connectionParameters.ApiKey, + connectionParameters.AuthorizationHeader, library, incoming.ItemId); @@ -238,7 +236,7 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan { Either> maybeEpisodeResult = await _jellyfinApiClient.GetSingleEpisode( connectionParameters.Address, - connectionParameters.ApiKey, + connectionParameters.AuthorizationHeader, library, jellyfinSeason.ItemId, incoming.ItemId); @@ -277,7 +275,7 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan Either maybeVersion = await _jellyfinApiClient.GetPlaybackInfo( connectionParameters.Address, - connectionParameters.ApiKey, + connectionParameters.AuthorizationHeader, library, incoming.ItemId);