Browse Source

use latest authorization method with jellyfin api

pull/2821/head
Jason Dove 6 months ago
parent
commit
c1209d496e
No known key found for this signature in database
  1. 1
      CHANGELOG.md
  2. 4
      ErsatzTV.Application/Jellyfin/Commands/SaveJellyfinSecretsHandler.cs
  3. 40
      ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinLibrariesHandler.cs
  4. 26
      ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinApiClient.cs
  5. 6
      ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinCollectionScanner.cs
  6. 4
      ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinMovieLibraryScanner.cs
  7. 7
      ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinTelevisionLibraryScanner.cs
  8. 5
      ErsatzTV.Core/Jellyfin/JellyfinConnectionParameters.cs
  9. 52
      ErsatzTV.Infrastructure/Jellyfin/IJellyfinApi.cs
  10. 54
      ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs
  11. 34
      ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinCollectionsHandler.cs
  12. 25
      ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinLibraryByIdHandler.cs
  13. 25
      ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinShowByIdHandler.cs
  14. 8
      ErsatzTV.Scanner/Core/Jellyfin/JellyfinCollectionScanner.cs
  15. 9
      ErsatzTV.Scanner/Core/Jellyfin/JellyfinMovieLibraryScanner.cs
  16. 28
      ErsatzTV.Scanner/Core/Jellyfin/JellyfinTelevisionLibraryScanner.cs

1
CHANGELOG.md

@ -17,6 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -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

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

@ -36,8 +36,10 @@ public class SaveJellyfinSecretsHandler : IRequestHandler<SaveJellyfinSecrets, E @@ -36,8 +36,10 @@ public class SaveJellyfinSecretsHandler : IRequestHandler<SaveJellyfinSecrets, E
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
.GetServerInformation(request.Secrets.Address, request.Secrets.ApiKey);
.GetServerInformation(connectionParameters.Address, connectionParameters.AuthorizationHeader);
return maybeServerInformation.Match(
info => Validation<BaseError, Parameters>.Success(new Parameters(request.Secrets, info)),

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

@ -38,7 +38,7 @@ public class @@ -38,7 +38,7 @@ public class
.MapT(p => SynchronizeLibraries(p, cancellationToken))
.Bind(v => v.ToEitherAsync());
private Task<Validation<BaseError, ConnectionParameters>> Validate(SynchronizeJellyfinLibraries request) =>
private Task<Validation<BaseError, ConnectionAndSource>> Validate(SynchronizeJellyfinLibraries request) =>
MediaSourceMustExist(request)
.BindT(MediaSourceMustHaveActiveConnection)
.BindT(MediaSourceMustHaveApiKey);
@ -48,43 +48,48 @@ public class @@ -48,43 +48,48 @@ public class
_mediaSourceRepository.GetJellyfin(request.JellyfinMediaSourceId)
.Map(o => o.ToValidation<BaseError>("Jellyfin media source does not exist."));
private Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection(
private Validation<BaseError, ConnectionAndSource> MediaSourceMustHaveActiveConnection(
JellyfinMediaSource jellyfinMediaSource)
{
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");
}
private async Task<Validation<BaseError, ConnectionParameters>> MediaSourceMustHaveApiKey(
ConnectionParameters connectionParameters)
private async Task<Validation<BaseError, ConnectionAndSource>> 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<BaseError>("Jellyfin media source requires an api key");
}
private async Task<Unit> SynchronizeLibraries(
ConnectionParameters connectionParameters,
ConnectionAndSource connectionAndSource,
CancellationToken cancellationToken)
{
Either<BaseError, List<JellyfinLibrary>> 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<JellyfinLibrary> libraries in maybeLibraries.RightToSeq())
{
var existing = connectionParameters.JellyfinMediaSource.Libraries
var existing = connectionAndSource.MediaSource.Libraries
.OfType<JellyfinLibrary>()
.ToList();
var toAdd = libraries.Filter(library => existing.All(l => l.ItemId != library.ItemId)).ToList();
@ -92,7 +97,7 @@ public class @@ -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<int> ids = await _mediaSourceRepository.UpdateLibraries(
connectionParameters.JellyfinMediaSource.Id,
connectionAndSource.MediaSource.Id,
toAdd,
toRemove,
toUpdate,
@ -107,10 +112,7 @@ public class @@ -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);
}

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

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

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

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

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

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

5
ErsatzTV.Core/Jellyfin/JellyfinConnectionParameters.cs

@ -3,4 +3,7 @@ @@ -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}";
}

52
ErsatzTV.Infrastructure/Jellyfin/IJellyfinApi.cs

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

54
ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs

@ -36,14 +36,14 @@ public class JellyfinApiClient : IJellyfinApiClient @@ -36,14 +36,14 @@ public class JellyfinApiClient : IJellyfinApiClient
public async Task<Either<BaseError, JellyfinServerInformation>> 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 @@ -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
{
IJellyfinApi service = ServiceForAddress(address);
List<JellyfinLibraryResponse> libraries = await service.GetLibraries(apiKey);
List<JellyfinLibraryResponse> libraries = await service.GetLibraries(authorizationHeader);
return libraries
.Map(Project)
.Somes()
@ -78,7 +78,7 @@ public class JellyfinApiClient : IJellyfinApiClient @@ -78,7 +78,7 @@ public class JellyfinApiClient : IJellyfinApiClient
public IAsyncEnumerable<Tuple<JellyfinMovie, int>> GetMovieLibraryItems(
string address,
string apiKey,
string authorizationHeader,
JellyfinLibrary library) =>
GetPagedLibraryItems(
"JF Movies",
@ -87,7 +87,7 @@ public class JellyfinApiClient : IJellyfinApiClient @@ -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 @@ -95,7 +95,7 @@ public class JellyfinApiClient : IJellyfinApiClient
public IAsyncEnumerable<Tuple<JellyfinShow, int>> GetShowLibraryItemsWithoutPeople(
string address,
string apiKey,
string authorizationHeader,
JellyfinLibrary library) =>
GetPagedLibraryItems(
"JF Shows",
@ -104,7 +104,7 @@ public class JellyfinApiClient : IJellyfinApiClient @@ -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 @@ -112,7 +112,7 @@ public class JellyfinApiClient : IJellyfinApiClient
public IAsyncEnumerable<Tuple<JellyfinSeason, int>> GetSeasonLibraryItems(
string address,
string apiKey,
string authorizationHeader,
JellyfinLibrary library,
string showId) =>
GetPagedLibraryItems(
@ -122,7 +122,7 @@ public class JellyfinApiClient : IJellyfinApiClient @@ -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 @@ -130,7 +130,7 @@ public class JellyfinApiClient : IJellyfinApiClient
public IAsyncEnumerable<Tuple<JellyfinEpisode, int>> GetEpisodeLibraryItems(
string address,
string apiKey,
string authorizationHeader,
JellyfinLibrary library,
string seasonId) =>
GetPagedLibraryItems(
@ -140,7 +140,7 @@ public class JellyfinApiClient : IJellyfinApiClient @@ -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 @@ -148,7 +148,7 @@ public class JellyfinApiClient : IJellyfinApiClient
public IAsyncEnumerable<Tuple<JellyfinEpisode, int>> GetEpisodeLibraryItemsWithoutPeople(
string address,
string apiKey,
string authorizationHeader,
JellyfinLibrary library,
string seasonId) =>
GetPagedLibraryItems(
@ -158,7 +158,7 @@ public class JellyfinApiClient : IJellyfinApiClient @@ -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 @@ -166,7 +166,7 @@ public class JellyfinApiClient : IJellyfinApiClient
public IAsyncEnumerable<Tuple<JellyfinCollection, int>> GetCollectionLibraryItems(
string address,
string apiKey,
string authorizationHeader,
int mediaSourceId)
{
// TODO: should we enumerate collection libraries here?
@ -180,7 +180,7 @@ public class JellyfinApiClient : IJellyfinApiClient @@ -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 @@ -192,7 +192,7 @@ public class JellyfinApiClient : IJellyfinApiClient
public IAsyncEnumerable<Tuple<MediaItem, int>> GetCollectionItems(
string address,
string apiKey,
string authorizationHeader,
int mediaSourceId,
string collectionId) =>
GetPagedLibraryItems(
@ -202,7 +202,7 @@ public class JellyfinApiClient : IJellyfinApiClient @@ -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 @@ -210,7 +210,7 @@ public class JellyfinApiClient : IJellyfinApiClient
public async Task<Either<BaseError, MediaVersion>> GetPlaybackInfo(
string address,
string apiKey,
string authorizationHeader,
JellyfinLibrary library,
string itemId)
{
@ -219,7 +219,7 @@ public class JellyfinApiClient : IJellyfinApiClient @@ -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<MediaVersion> maybeVersion = ProjectToMediaVersion(playbackInfo);
return maybeVersion.ToEither(() => BaseError.New("Unable to locate Jellyfin statistics"));
}
@ -233,7 +233,7 @@ public class JellyfinApiClient : IJellyfinApiClient @@ -233,7 +233,7 @@ public class JellyfinApiClient : IJellyfinApiClient
public async Task<Either<BaseError, Option<JellyfinShow>>> GetSingleShow(
string address,
string apiKey,
string authorizationHeader,
JellyfinLibrary library,
string showId)
{
@ -243,7 +243,7 @@ public class JellyfinApiClient : IJellyfinApiClient @@ -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 @@ -267,7 +267,7 @@ public class JellyfinApiClient : IJellyfinApiClient
public async Task<Either<BaseError, List<JellyfinShow>>> SearchShowsByTitle(
string address,
string apiKey,
string authorizationHeader,
JellyfinLibrary library,
string showTitle)
{
@ -275,7 +275,7 @@ public class JellyfinApiClient : IJellyfinApiClient @@ -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 @@ -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 @@ -316,7 +316,7 @@ public class JellyfinApiClient : IJellyfinApiClient
public async Task<Either<BaseError, Option<JellyfinEpisode>>> GetSingleEpisode(
string address,
string apiKey,
string authorizationHeader,
JellyfinLibrary library,
string seasonId,
string episodeId)
@ -327,7 +327,7 @@ public class JellyfinApiClient : IJellyfinApiClient @@ -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,

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

@ -45,13 +45,13 @@ public class @@ -45,13 +45,13 @@ public class
SynchronizeJellyfinCollections request,
CancellationToken cancellationToken)
{
Task<Validation<BaseError, ConnectionParameters>> mediaSource = MediaSourceMustExist(request)
Task<Validation<BaseError, ConnectionAndSource>> 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 @@ -69,21 +69,26 @@ public class
_mediaSourceRepository.GetJellyfin(request.JellyfinMediaSourceId)
.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)
{
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");
}
private async Task<Validation<BaseError, ConnectionParameters>> MediaSourceMustHaveApiKey(
ConnectionParameters connectionParameters)
private async Task<Validation<BaseError, ConnectionAndSource>> 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<BaseError>("Jellyfin media source requires an api key");
}
@ -98,8 +103,8 @@ public class @@ -98,8 +103,8 @@ public class
if (parameters.ForceScan || parameters.LibraryRefreshInterval > 0 && nextScan < DateTimeOffset.Now)
{
Either<BaseError, Unit> 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 @@ -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);
}

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

@ -65,15 +65,13 @@ public class @@ -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 @@ -114,7 +112,7 @@ public class
request.BaseUrl
));
private Task<Validation<BaseError, ConnectionParameters>> ValidateConnection(
private Task<Validation<BaseError, JellyfinConnectionParameters>> ValidateConnection(
SynchronizeJellyfinLibraryById request) =>
JellyfinMediaSourceMustExist(request)
.BindT(MediaSourceMustHaveActiveConnection)
@ -126,19 +124,19 @@ public class @@ -126,19 +124,19 @@ public class
.Map(v => v.ToValidation<BaseError>(
$"Jellyfin media source for library {request.JellyfinLibraryId} does not exist."));
private Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection(
private Validation<BaseError, JellyfinConnectionParameters> MediaSourceMustHaveActiveConnection(
JellyfinMediaSource jellyfinMediaSource)
{
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");
}
private async Task<Validation<BaseError, ConnectionParameters>> MediaSourceMustHaveApiKey(
ConnectionParameters connectionParameters)
private async Task<Validation<BaseError, JellyfinConnectionParameters>> 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<BaseError>("Jellyfin media source requires an api key");
@ -155,15 +153,10 @@ public class @@ -155,15 +153,10 @@ public class
.Map(lri => lri.ToValidation<BaseError>("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; }
}
}

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

@ -61,8 +61,7 @@ public class @@ -61,8 +61,7 @@ public class
parameters.Library.Name);
Either<BaseError, Unit> 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 @@ -92,7 +91,7 @@ public class
request.BaseUrl
));
private Task<Validation<BaseError, ConnectionParameters>> ValidateConnection(
private Task<Validation<BaseError, JellyfinConnectionParameters>> ValidateConnection(
SynchronizeJellyfinShowById request) =>
JellyfinMediaSourceMustExist(request)
.BindT(MediaSourceMustHaveActiveConnection)
@ -104,19 +103,22 @@ public class @@ -104,19 +103,22 @@ public class
.Map(v => v.ToValidation<BaseError>(
$"Jellyfin media source for library {request.JellyfinLibraryId} does not exist."));
private Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection(
private Validation<BaseError, JellyfinConnectionParameters> MediaSourceMustHaveActiveConnection(
JellyfinMediaSource jellyfinMediaSource)
{
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");
}
private async Task<Validation<BaseError, ConnectionParameters>> MediaSourceMustHaveApiKey(
ConnectionParameters connectionParameters)
private async Task<Validation<BaseError, JellyfinConnectionParameters>> 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<BaseError>("Jellyfin media source requires an api key");
@ -135,15 +137,10 @@ public class @@ -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; }
}
}

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

@ -28,14 +28,14 @@ public class JellyfinCollectionScanner : IJellyfinCollectionScanner @@ -28,14 +28,14 @@ public class JellyfinCollectionScanner : IJellyfinCollectionScanner
public async Task<Either<BaseError, Unit>> 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<string>();
@ -45,7 +45,7 @@ public class JellyfinCollectionScanner : IJellyfinCollectionScanner @@ -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 @@ -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);

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

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

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

@ -57,8 +57,7 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan @@ -57,8 +57,7 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
protected override bool ServerSupportsRemoteStreaming => true;
public async Task<Either<BaseError, Unit>> ScanLibrary(
string address,
string apiKey,
JellyfinConnectionParameters connectionParameters,
JellyfinLibrary library,
bool deepScan,
CancellationToken cancellationToken)
@ -76,7 +75,7 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan @@ -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 @@ -84,8 +83,7 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
}
public async Task<Either<BaseError, Unit>> ScanSingleShow(
string address,
string apiKey,
JellyfinConnectionParameters connectionParameters,
JellyfinLibrary library,
string showId,
string showTitle,
@ -105,8 +103,8 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan @@ -105,8 +103,8 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
// Search for the specific show
Either<BaseError, Option<JellyfinShow>> searchResult = await _jellyfinApiClient.GetSingleShow(
address,
apiKey,
connectionParameters.Address,
connectionParameters.AuthorizationHeader,
library,
showId);
@ -122,7 +120,7 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan @@ -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 @@ -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 @@ -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 @@ -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 @@ -197,7 +195,7 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
{
Either<BaseError, Option<JellyfinShow>> maybeShowResult = await _jellyfinApiClient.GetSingleShow(
connectionParameters.Address,
connectionParameters.ApiKey,
connectionParameters.AuthorizationHeader,
library,
incoming.ItemId);
@ -238,7 +236,7 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan @@ -238,7 +236,7 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
{
Either<BaseError, Option<JellyfinEpisode>> maybeEpisodeResult = await _jellyfinApiClient.GetSingleEpisode(
connectionParameters.Address,
connectionParameters.ApiKey,
connectionParameters.AuthorizationHeader,
library,
jellyfinSeason.ItemId,
incoming.ItemId);
@ -277,7 +275,7 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan @@ -277,7 +275,7 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
Either<BaseError, MediaVersion> maybeVersion =
await _jellyfinApiClient.GetPlaybackInfo(
connectionParameters.Address,
connectionParameters.ApiKey,
connectionParameters.AuthorizationHeader,
library,
incoming.ItemId);

Loading…
Cancel
Save