enable plex for movies (#72)

* re-enable plex, temp force secure connections

* add plex fanart

* synchronize genre from plex

* fix plex library sync

* improve stream error handling

* synchronize plex artwork

* use switch instead of button

* prioritize local connections for insecure plex sources

* sign out of plex

* better plex sign in/out

* code cleanup

* fix plex movie aspect ratio and scan type
This commit is contained in:
Jason Dove
2021-03-13 21:04:54 +00:00
committed by GitHub
parent a13f964200
commit aa938baec8
35 changed files with 602 additions and 132 deletions
@@ -18,5 +18,12 @@ namespace ErsatzTV.Infrastructure.Plex
string key,
[Query] [AliasAs("X-Plex-Token")]
string token);
[Get("/library/metadata/{key}")]
public Task<PlexMediaContainerResponse<PlexMediaContainerMetadataContent<PlexMetadataResponse>>>
GetMetadata(
string key,
[Query] [AliasAs("X-Plex-Token")]
string token);
}
}
@@ -36,6 +36,8 @@ namespace ErsatzTV.Infrastructure.Plex
[Get("/resources")]
Task<List<PlexResource>> GetResources(
[Query] [AliasAs("includeHttps")]
int includeHttps,
[Header("X-Plex-Client-Identifier")]
string clientIdentifier,
[Header("X-Plex-Token")]
@@ -0,0 +1,9 @@
namespace ErsatzTV.Infrastructure.Plex.Models
{
public class PlexGenreResponse
{
public int Id { get; set; }
public string Filter { get; set; }
public string Tag { get; set; }
}
}
@@ -15,5 +15,6 @@ namespace ErsatzTV.Infrastructure.Plex.Models
public int AddedAt { get; set; }
public int UpdatedAt { get; set; }
public List<PlexMediaResponse> Media { get; set; }
public List<PlexGenreResponse> Genre { get; set; }
}
}
@@ -1,4 +1,6 @@
namespace ErsatzTV.Infrastructure.Plex.Models
using System.Collections.Generic;
namespace ErsatzTV.Infrastructure.Plex.Models
{
public class PlexPartResponse
{
@@ -6,5 +8,6 @@
public string Key { get; set; }
public int Duration { get; set; }
public string File { get; set; }
public List<PlexStreamResponse> Stream { get; set; }
}
}
@@ -12,6 +12,7 @@ namespace ErsatzTV.Infrastructure.Plex.Models
public bool Owned { get; set; }
public string Provides { get; set; }
public bool HttpsRequired { get; set; }
public List<PlexResourceConnection> Connections { get; set; }
}
}
@@ -0,0 +1,11 @@
namespace ErsatzTV.Infrastructure.Plex.Models
{
public class PlexStreamResponse
{
public int Id { get; set; }
public int StreamType { get; set; }
public bool Anamorphic { get; set; }
public string PixelAspectRatio { get; set; }
public string ScanType { get; set; }
}
}
@@ -32,12 +32,6 @@ namespace ErsatzTV.Infrastructure.Plex
tokens => tokens.Map(kvp => new PlexUserAuthToken(kvp.Key, kvp.Value)).ToList(),
() => new List<PlexUserAuthToken>()));
public Task<List<PlexServerAuthToken>> GetServerAuthTokens() =>
ReadSecrets().Map(
s => Optional(s.ServerAuthTokens).Match(
tokens => tokens.Map(kvp => new PlexServerAuthToken(kvp.Key, kvp.Value)).ToList(),
() => new List<PlexServerAuthToken>()));
public Task<Option<PlexServerAuthToken>> GetServerAuthToken(string clientIdentifier) =>
ReadSecrets().Map(
s => Optional(s.ServerAuthTokens.SingleOrDefault(kvp => kvp.Key == clientIdentifier))
@@ -61,6 +55,9 @@ namespace ErsatzTV.Infrastructure.Plex
return SaveSecrets(secrets);
});
public Task<Unit> DeleteAll() =>
ReadSecrets().Bind(secrets => SaveSecrets(new PlexSecrets { ClientIdentifier = secrets.ClientIdentifier }));
private static Task<PlexSecrets> ReadSecrets() =>
File.ReadAllTextAsync(FileSystemLayout.PlexSecretsPath)
.Map(JsonConvert.DeserializeObject<PlexSecrets>)
@@ -60,6 +60,27 @@ namespace ErsatzTV.Infrastructure.Plex
}
}
public async Task<Either<BaseError, MediaVersion>> GetStatistics(
PlexMovie movie,
PlexConnection connection,
PlexServerAuthToken token)
{
try
{
IPlexServerApi service = RestService.For<IPlexServerApi>(connection.Uri);
return await service.GetMetadata(movie.Key.Split("/").Last(), token.AuthToken)
.Map(
r => r.MediaContainer.Metadata.Filter(m => m.Media.Count > 0 && m.Media[0].Part.Count > 0)
.HeadOrNone())
.BindT(ProjectToMediaVersion)
.Map(o => o.ToEither<BaseError>("Unable to locate metadata"));
}
catch (Exception ex)
{
return BaseError.New(ex.Message);
}
}
private static Option<PlexLibrary> Project(PlexLibraryResponse response) =>
response.Type switch
{
@@ -99,7 +120,8 @@ namespace ErsatzTV.Infrastructure.Plex
Year = response.Year,
Tagline = response.Tagline,
DateAdded = dateAdded,
DateUpdated = lastWriteTime
DateUpdated = lastWriteTime,
Genres = response.Genre.Map(g => new Genre { Name = g.Tag }).ToList()
};
if (!string.IsNullOrWhiteSpace(response.Thumb))
@@ -117,6 +139,21 @@ namespace ErsatzTV.Infrastructure.Plex
metadata.Artwork.Add(artwork);
}
if (!string.IsNullOrWhiteSpace(response.Art))
{
var path = $"plex/{mediaSourceId}{response.Art}";
var artwork = new Artwork
{
ArtworkKind = ArtworkKind.FanArt,
Path = path,
DateAdded = dateAdded,
DateUpdated = lastWriteTime
};
metadata.Artwork ??= new List<Artwork>();
metadata.Artwork.Add(artwork);
}
var version = new MediaVersion
{
Name = "Main",
@@ -126,7 +163,8 @@ namespace ErsatzTV.Infrastructure.Plex
AudioCodec = media.AudioCodec,
VideoCodec = media.VideoCodec,
VideoProfile = media.VideoProfile,
SampleAspectRatio = ConvertToSAR(media.AspectRatio),
// specifically omit sample aspect ratio
DateUpdated = lastWriteTime,
MediaFiles = new List<MediaFile>
{
new PlexMediaFile
@@ -148,8 +186,21 @@ namespace ErsatzTV.Infrastructure.Plex
return movie;
}
private static string ConvertToSAR(double aspectRatio) => "1:1";
// TODO: fix this with more detailed stats pull from plex for each item
// Math.Abs(aspectRatio - 1) < 0.01 ? "1:1" : $"{(int) (aspectRatio * 100)}:100";
private Option<MediaVersion> ProjectToMediaVersion(PlexMetadataResponse response)
{
Option<PlexStreamResponse> maybeStream =
response.Media.Head().Part.Head().Stream.Find(s => s.StreamType == 1);
return maybeStream.Map(
stream => new MediaVersion
{
SampleAspectRatio = stream.PixelAspectRatio,
VideoScanKind = stream.ScanType switch
{
"interlaced" => VideoScanKind.Interlaced,
"progressive" => VideoScanKind.Progressive,
_ => VideoScanKind.Unknown
}
});
}
}
}
@@ -34,8 +34,22 @@ namespace ErsatzTV.Infrastructure.Plex
string clientIdentifier = await _plexSecretStore.GetClientIdentifier();
foreach (PlexUserAuthToken token in await _plexSecretStore.GetUserAuthTokens())
{
List<PlexResource> resources = await _plexTvApi.GetResources(clientIdentifier, token.AuthToken);
IEnumerable<PlexMediaSource> sources = resources
List<PlexResource> httpResources = await _plexTvApi.GetResources(
0,
clientIdentifier,
token.AuthToken);
List<PlexResource> httpsResources = await _plexTvApi.GetResources(
1,
clientIdentifier,
token.AuthToken);
var allResources = httpResources.Filter(resource => resource.HttpsRequired == false)
.Append(httpsResources.Filter(resource => resource.HttpsRequired))
.ToList();
IEnumerable<PlexMediaSource> sources = allResources
.Filter(r => r.Provides.Split(",").Any(p => p == "server"))
.Filter(r => r.Owned) // TODO: maybe support non-owned servers in the future
.Map(
@@ -46,13 +60,16 @@ namespace ErsatzTV.Infrastructure.Plex
resource.AccessToken);
_plexSecretStore.UpsertServerAuthToken(serverAuthToken);
List<PlexResourceConnection> sortedConnections = resource.HttpsRequired
? resource.Connections
: resource.Connections.OrderBy(c => c.Local ? 0 : 1).ToList();
var source = new PlexMediaSource
{
ServerName = resource.Name,
ProductVersion = resource.ProductVersion,
ClientIdentifier = resource.ClientIdentifier,
Connections = resource.Connections
Connections = sortedConnections
.Map(c => new PlexConnection { Uri = c.Uri }).ToList()
};