merge latest develop (#230)

* sync guids/provider ids (#227)

* sync guids from plex

* cleanup

* sync local guids

* sync jellyfin and emby guids

* add episodes to search index (#228)

* sync episode directors and writers

* display episode writers and directors

* remove missing episodes from search index

* show episodes in search results

* fix emby and jellyfin episode updates

* fix updating plex episodes

* don't delete channel logos on startup

* add episodes page; fix adding episodes to collection

* cleanup

* multi-part episode grouping fixes (#229)
This commit is contained in:
Jason Dove
2021-05-30 06:08:33 -05:00
committed by GitHub
parent bbd8bc6c7e
commit e506dd38a8
78 changed files with 8328 additions and 484 deletions
+24 -5
View File
@@ -4,15 +4,16 @@ using Refit;
namespace ErsatzTV.Infrastructure.Plex
{
[Headers("Accept: application/json")]
public interface IPlexServerApi
{
[Get("/library/sections")]
[Headers("Accept: application/json")]
public Task<PlexMediaContainerResponse<PlexMediaContainerDirectoryContent<PlexLibraryResponse>>> GetLibraries(
[Query] [AliasAs("X-Plex-Token")]
string token);
[Get("/library/sections/{key}/all")]
[Headers("Accept: application/json")]
public Task<PlexMediaContainerResponse<PlexMediaContainerMetadataContent<PlexMetadataResponse>>>
GetLibrarySectionContents(
string key,
@@ -20,15 +21,33 @@ namespace ErsatzTV.Infrastructure.Plex
string token);
[Get("/library/metadata/{key}")]
public Task<PlexMediaContainerResponse<PlexMediaContainerMetadataContent<PlexMetadataResponse>>>
GetMetadata(
[Headers("Accept: text/xml")]
public Task<PlexXmlVideoMetadataResponseContainer>
GetVideoMetadata(
string key,
[Query] [AliasAs("X-Plex-Token")]
string token);
[Get("/library/metadata/{key}")]
[Headers("Accept: text/xml")]
public Task<PlexXmlDirectoryMetadataResponseContainer>
GetDirectoryMetadata(
string key,
[Query] [AliasAs("X-Plex-Token")]
string token);
[Get("/library/metadata/{key}/children")]
public Task<PlexMediaContainerResponse<PlexMediaContainerMetadataContent<PlexMetadataResponse>>>
GetChildren(
[Headers("Accept: text/xml")]
public Task<PlexXmlSeasonsMetadataResponseContainer>
GetShowChildren(
string key,
[Query] [AliasAs("X-Plex-Token")]
string token);
[Get("/library/metadata/{key}/children")]
[Headers("Accept: text/xml")]
public Task<PlexXmlEpisodesMetadataResponseContainer>
GetSeasonChildren(
string key,
[Query] [AliasAs("X-Plex-Token")]
string token);
@@ -1,7 +1,10 @@
namespace ErsatzTV.Infrastructure.Plex.Models
using System.Xml.Serialization;
namespace ErsatzTV.Infrastructure.Plex.Models
{
public class PlexDirectorResponse
{
[XmlAttribute("tag")]
public string Tag { get; set; }
}
}
@@ -1,9 +1,16 @@
namespace ErsatzTV.Infrastructure.Plex.Models
using System.Xml.Serialization;
namespace ErsatzTV.Infrastructure.Plex.Models
{
public class PlexGenreResponse
{
[XmlAttribute("id")]
public int Id { get; set; }
[XmlAttribute("filter")]
public string Filter { get; set; }
[XmlAttribute("tag")]
public string Tag { get; set; }
}
}
@@ -0,0 +1,10 @@
using System.Xml.Serialization;
namespace ErsatzTV.Infrastructure.Plex.Models
{
public class PlexGuidResponse
{
[XmlAttribute("id")]
public string Id { get; set; }
}
}
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Xml.Serialization;
namespace ErsatzTV.Infrastructure.Plex.Models
{
@@ -16,4 +17,32 @@ namespace ErsatzTV.Infrastructure.Plex.Models
{
public List<T> Metadata { get; set; }
}
[XmlRoot("MediaContainer", Namespace = null)]
public class PlexXmlVideoMetadataResponseContainer
{
[XmlElement("Video")]
public PlexXmlMetadataResponse Metadata { get; set; }
}
[XmlRoot("MediaContainer", Namespace = null)]
public class PlexXmlDirectoryMetadataResponseContainer
{
[XmlElement("Directory")]
public PlexXmlMetadataResponse Metadata { get; set; }
}
[XmlRoot("MediaContainer", Namespace = null)]
public class PlexXmlSeasonsMetadataResponseContainer
{
[XmlElement("Directory")]
public List<PlexXmlMetadataResponse> Metadata { get; set; }
}
[XmlRoot("MediaContainer", Namespace = null)]
public class PlexXmlEpisodesMetadataResponseContainer
{
[XmlElement("Video")]
public List<PlexXmlMetadataResponse> Metadata { get; set; }
}
}
@@ -1,21 +1,50 @@
using System.Collections.Generic;
using System.Xml.Serialization;
namespace ErsatzTV.Infrastructure.Plex.Models
{
public class PlexMediaResponse
public class PlexMediaResponse<T>
{
[XmlAttribute("id")]
public int Id { get; set; }
[XmlAttribute("duration")]
public int Duration { get; set; }
[XmlAttribute("bitrate")]
public int Bitrate { get; set; }
[XmlAttribute("width")]
public int Width { get; set; }
[XmlAttribute("height")]
public int Height { get; set; }
[XmlAttribute("aspectRatio")]
public double AspectRatio { get; set; }
[XmlAttribute("audioChannels")]
public int AudioChannels { get; set; }
[XmlAttribute("audioCodec")]
public string AudioCodec { get; set; }
[XmlAttribute("videoCodec")]
public string VideoCodec { get; set; }
[XmlAttribute("videoResulution")]
public string VideoResolution { get; set; }
[XmlAttribute("videoProfile")]
public string VideoProfile { get; set; }
[XmlAttribute("container")]
public string Container { get; set; }
[XmlAttribute("videoFrameRate")]
public string VideoFrameRate { get; set; }
public List<PlexPartResponse> Part { get; set; }
[XmlElement("Part")]
public List<T> Part { get; set; }
}
}
@@ -1,26 +1,74 @@
using System.Collections.Generic;
using System.Xml.Serialization;
namespace ErsatzTV.Infrastructure.Plex.Models
{
public class PlexMetadataResponse
{
[XmlAttribute("key")]
public string Key { get; set; }
[XmlAttribute("title")]
public string Title { get; set; }
[XmlAttribute("contentRating")]
public string ContentRating { get; set; }
[XmlAttribute("summary")]
public string Summary { get; set; }
[XmlAttribute("year")]
public int Year { get; set; }
[XmlAttribute("tagline")]
public string Tagline { get; set; }
[XmlAttribute("thumb")]
public string Thumb { get; set; }
[XmlAttribute("art")]
public string Art { get; set; }
[XmlAttribute("originallyAvailableAt")]
public string OriginallyAvailableAt { get; set; }
[XmlAttribute("addedAt")]
public long AddedAt { get; set; }
[XmlAttribute("updatedAt")]
public long UpdatedAt { get; set; }
[XmlAttribute("index")]
public int Index { get; set; }
[XmlAttribute("studio")]
public string Studio { get; set; }
public List<PlexMediaResponse> Media { get; set; }
[XmlAttribute("rating")]
public double Rating { get; set; }
[XmlAttribute("audienceRating")]
public double AudienceRating { get; set; }
[XmlAttribute("audienceRatingImage")]
public string AudienceRatingImage { get; set; }
[XmlAttribute("ratingImage")]
public string RatingImage { get; set; }
[XmlIgnore]
public virtual List<PlexMediaResponse<PlexPartResponse>> Media { get; set; }
[XmlElement("Genre")]
public List<PlexGenreResponse> Genre { get; set; }
[XmlElement("Role")]
public List<PlexRoleResponse> Role { get; set; }
[XmlElement("Director")]
public List<PlexDirectorResponse> Director { get; set; }
[XmlElement("Writer")]
public List<PlexWriterResponse> Writer { get; set; }
}
}
@@ -1,6 +1,4 @@
using System.Collections.Generic;
namespace ErsatzTV.Infrastructure.Plex.Models
namespace ErsatzTV.Infrastructure.Plex.Models
{
public class PlexPartResponse
{
@@ -8,6 +6,5 @@ namespace ErsatzTV.Infrastructure.Plex.Models
public string Key { get; set; }
public int Duration { get; set; }
public string File { get; set; }
public List<PlexStreamResponse> Stream { get; set; }
}
}
@@ -1,9 +1,16 @@
namespace ErsatzTV.Infrastructure.Plex.Models
using System.Xml.Serialization;
namespace ErsatzTV.Infrastructure.Plex.Models
{
public class PlexRoleResponse
{
[XmlAttribute("tag")]
public string Tag { get; set; }
[XmlAttribute("role")]
public string Role { get; set; }
[XmlAttribute("thumb")]
public string Thumb { get; set; }
}
}
@@ -1,18 +1,43 @@
namespace ErsatzTV.Infrastructure.Plex.Models
using System.Xml.Serialization;
namespace ErsatzTV.Infrastructure.Plex.Models
{
public class PlexStreamResponse
{
[XmlAttribute("id")]
public int Id { get; set; }
[XmlAttribute("index")]
public int Index { get; set; }
[XmlAttribute("default")]
public bool Default { get; set; }
[XmlAttribute("forced")]
public bool Forced { get; set; }
[XmlAttribute("languageCode")]
public string LanguageCode { get; set; }
[XmlAttribute("streamType")]
public int StreamType { get; set; }
[XmlAttribute("codec")]
public string Codec { get; set; }
[XmlAttribute("profile")]
public string Profile { get; set; }
[XmlAttribute("channels")]
public int Channels { get; set; }
[XmlAttribute("anamorphic")]
public bool Anamorphic { get; set; }
[XmlAttribute("pixelAspectRatio")]
public string PixelAspectRatio { get; set; }
[XmlAttribute("scanType")]
public string ScanType { get; set; }
}
}
@@ -1,7 +1,10 @@
namespace ErsatzTV.Infrastructure.Plex.Models
using System.Xml.Serialization;
namespace ErsatzTV.Infrastructure.Plex.Models
{
public class PlexWriterResponse
{
[XmlAttribute("tag")]
public string Tag { get; set; }
}
}
@@ -0,0 +1,17 @@
using System.Collections.Generic;
using System.Xml.Serialization;
namespace ErsatzTV.Infrastructure.Plex.Models
{
public class PlexXmlMetadataResponse : PlexMetadataResponse
{
[XmlAttribute("guid")]
public string PlexGuid { get; set; }
[XmlElement("Media")]
public new List<PlexMediaResponse<PlexXmlPartResponse>> Media { get; set; }
[XmlElement("Guid")]
public List<PlexGuidResponse> Guid { get; set; }
}
}
@@ -0,0 +1,23 @@
using System.Collections.Generic;
using System.Xml.Serialization;
namespace ErsatzTV.Infrastructure.Plex.Models
{
public class PlexXmlPartResponse
{
[XmlAttribute("id")]
public int Id { get; set; }
[XmlAttribute("key")]
public string Key { get; set; }
[XmlAttribute("duration")]
public int Duration { get; set; }
[XmlAttribute("file")]
public string File { get; set; }
[XmlElement("Stream")]
public List<PlexStreamResponse> Stream { get; set; }
}
}
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Xml.Serialization;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
@@ -87,9 +88,9 @@ namespace ErsatzTV.Infrastructure.Plex
{
try
{
IPlexServerApi service = RestService.For<IPlexServerApi>(connection.Uri);
return await service.GetChildren(show.Key.Split("/").Reverse().Skip(1).Head(), token.AuthToken)
.Map(r => r.MediaContainer.Metadata)
IPlexServerApi service = XmlServiceFor(connection.Uri);
return await service.GetShowChildren(show.Key.Split("/").Reverse().Skip(1).Head(), token.AuthToken)
.Map(r => r.Metadata)
.Map(list => list.Map(metadata => ProjectToSeason(metadata, library.MediaSourceId)).ToList());
}
catch (Exception ex)
@@ -106,9 +107,9 @@ namespace ErsatzTV.Infrastructure.Plex
{
try
{
IPlexServerApi service = RestService.For<IPlexServerApi>(connection.Uri);
return await service.GetChildren(season.Key.Split("/").Reverse().Skip(1).Head(), token.AuthToken)
.Map(r => r.MediaContainer.Metadata.Filter(m => m.Media.Count > 0 && m.Media[0].Part.Count > 0))
IPlexServerApi service = XmlServiceFor(connection.Uri);
return await service.GetSeasonChildren(season.Key.Split("/").Reverse().Skip(1).Head(), token.AuthToken)
.Map(r => r.Metadata.Filter(m => m.Media.Count > 0 && m.Media[0].Part.Count > 0))
.Map(list => list.Map(metadata => ProjectToEpisode(metadata, library.MediaSourceId)).ToList());
}
catch (Exception ex)
@@ -125,12 +126,13 @@ namespace ErsatzTV.Infrastructure.Plex
{
try
{
IPlexServerApi service = RestService.For<IPlexServerApi>(connection.Uri);
return await service.GetMetadata(key, token.AuthToken)
IPlexServerApi service = XmlServiceFor(connection.Uri);
return await service.GetVideoMetadata(key, token.AuthToken)
.Map(Optional)
.Map(
r => r.MediaContainer.Metadata.Filter(m => m.Media.Count > 0 && m.Media[0].Part.Count > 0)
r => r.Filter(m => m.Metadata.Media.Count > 0 && m.Metadata.Media[0].Part.Count > 0)
.HeadOrNone())
.MapT(response => ProjectToMovieMetadata(response, library.MediaSourceId))
.MapT(response => ProjectToMovieMetadata(response.Metadata, library.MediaSourceId))
.Map(o => o.ToEither<BaseError>("Unable to locate metadata"));
}
catch (Exception ex)
@@ -147,10 +149,10 @@ namespace ErsatzTV.Infrastructure.Plex
{
try
{
IPlexServerApi service = RestService.For<IPlexServerApi>(connection.Uri);
return await service.GetMetadata(key, token.AuthToken)
.Map(r => r.MediaContainer.Metadata.HeadOrNone())
.MapT(response => ProjectToShowMetadata(response, library.MediaSourceId))
IPlexServerApi service = XmlServiceFor(connection.Uri);
return await service.GetDirectoryMetadata(key, token.AuthToken)
.Map(Optional)
.MapT(response => ProjectToShowMetadata(response.Metadata, library.MediaSourceId))
.Map(o => o.ToEither<BaseError>("Unable to locate metadata"));
}
catch (Exception ex)
@@ -159,20 +161,30 @@ namespace ErsatzTV.Infrastructure.Plex
}
}
public async Task<Either<BaseError, MediaVersion>> GetStatistics(
public async Task<Either<BaseError, Tuple<MovieMetadata, MediaVersion>>> GetMovieMetadataAndStatistics(
PlexLibrary library,
string key,
PlexConnection connection,
PlexServerAuthToken token)
{
try
{
IPlexServerApi service = RestService.For<IPlexServerApi>(connection.Uri);
return await service.GetMetadata(key, token.AuthToken)
IPlexServerApi service = XmlServiceFor(connection.Uri);
Option<PlexXmlVideoMetadataResponseContainer> maybeResponse = await service
.GetVideoMetadata(key, token.AuthToken)
.Map(Optional)
.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"));
r => r.Filter(m => m.Metadata.Media.Count > 0 && m.Metadata.Media[0].Part.Count > 0)
.HeadOrNone());
return maybeResponse.Match(
response =>
{
Option<MediaVersion> maybeVersion = ProjectToMediaVersion(response.Metadata);
return maybeVersion.Match<Either<BaseError, Tuple<MovieMetadata, MediaVersion>>>(
version => Tuple(ProjectToMovieMetadata(response.Metadata, library.MediaSourceId), version),
() => BaseError.New("Unable to locate metadata"));
},
() => BaseError.New("Unable to locate metadata"));
}
catch (Exception ex)
{
@@ -180,6 +192,57 @@ namespace ErsatzTV.Infrastructure.Plex
}
}
public async Task<Either<BaseError, Tuple<EpisodeMetadata, MediaVersion>>> GetEpisodeMetadataAndStatistics(
PlexLibrary library,
string key,
PlexConnection connection,
PlexServerAuthToken token)
{
try
{
IPlexServerApi service = XmlServiceFor(connection.Uri);
Option<PlexXmlVideoMetadataResponseContainer> maybeResponse = await service
.GetVideoMetadata(key, token.AuthToken)
.Map(Optional)
.Map(
r => r.Filter(m => m.Metadata.Media.Count > 0 && m.Metadata.Media[0].Part.Count > 0)
.HeadOrNone());
return maybeResponse.Match(
response =>
{
Option<MediaVersion> maybeVersion = ProjectToMediaVersion(response.Metadata);
return maybeVersion.Match<Either<BaseError, Tuple<EpisodeMetadata, MediaVersion>>>(
version => Tuple(
ProjectToEpisodeMetadata(response.Metadata, library.MediaSourceId),
version),
() => BaseError.New("Unable to locate metadata"));
},
() => BaseError.New("Unable to locate metadata"));
}
catch (Exception ex)
{
return BaseError.New(ex.ToString());
}
}
private static IPlexServerApi XmlServiceFor(string uri)
{
var overrides = new XmlAttributeOverrides();
var attrs = new XmlAttributes { XmlIgnore = true };
overrides.Add(typeof(PlexMetadataResponse), "Media", attrs);
return RestService.For<IPlexServerApi>(
uri,
new RefitSettings
{
ContentSerializer = new XmlContentSerializer(
new XmlContentSerializerSettings
{
XmlAttributeOverrides = overrides
})
});
}
private static Option<PlexLibrary> Project(PlexLibraryResponse response) =>
response.Type switch
{
@@ -205,7 +268,7 @@ namespace ErsatzTV.Infrastructure.Plex
private PlexMovie ProjectToMovie(PlexMetadataResponse response, int mediaSourceId)
{
PlexMediaResponse media = response.Media.Head();
PlexMediaResponse<PlexPartResponse> media = response.Media.Head();
PlexPartResponse part = media.Part.Head();
DateTime dateAdded = DateTimeOffset.FromUnixTimeSeconds(response.AddedAt).DateTime;
DateTime lastWriteTime = DateTimeOffset.FromUnixTimeSeconds(response.UpdatedAt).DateTime;
@@ -268,6 +331,23 @@ namespace ErsatzTV.Infrastructure.Plex
Writers = Optional(response.Writer).Flatten().Map(w => new Writer { Name = w.Tag }).ToList()
};
if (response is PlexXmlMetadataResponse xml)
{
metadata.Guids = Optional(xml.Guid).Flatten().Map(g => new MetadataGuid { Guid = g.Id }).ToList();
if (!string.IsNullOrWhiteSpace(xml.PlexGuid))
{
string normalized = NormalizeGuid(xml.PlexGuid);
if (!string.IsNullOrWhiteSpace(normalized) && metadata.Guids.All(g => g.Guid != normalized))
{
metadata.Guids.Add(new MetadataGuid { Guid = normalized });
}
}
}
else
{
metadata.Guids = new List<MetadataGuid>();
}
if (!string.IsNullOrWhiteSpace(response.Studio))
{
metadata.Studios.Add(new Studio { Name = response.Studio });
@@ -311,7 +391,7 @@ namespace ErsatzTV.Infrastructure.Plex
return metadata;
}
private Option<MediaVersion> ProjectToMediaVersion(PlexMetadataResponse response)
private Option<MediaVersion> ProjectToMediaVersion(PlexXmlMetadataResponse response)
{
List<PlexStreamResponse> streams = response.Media.Head().Part.Head().Stream;
DateTime dateUpdated = DateTimeOffset.FromUnixTimeSeconds(response.UpdatedAt).DateTime;
@@ -418,6 +498,23 @@ namespace ErsatzTV.Infrastructure.Plex
.ToList()
};
if (response is PlexXmlMetadataResponse xml)
{
metadata.Guids = Optional(xml.Guid).Flatten().Map(g => new MetadataGuid { Guid = g.Id }).ToList();
if (!string.IsNullOrWhiteSpace(xml.PlexGuid))
{
string normalized = NormalizeGuid(xml.PlexGuid);
if (!string.IsNullOrWhiteSpace(normalized) && metadata.Guids.All(g => g.Guid != normalized))
{
metadata.Guids.Add(new MetadataGuid { Guid = normalized });
}
}
}
else
{
metadata.Guids = new List<MetadataGuid>();
}
if (!string.IsNullOrWhiteSpace(response.Studio))
{
metadata.Studios.Add(new Studio { Name = response.Studio });
@@ -461,7 +558,7 @@ namespace ErsatzTV.Infrastructure.Plex
return metadata;
}
private PlexSeason ProjectToSeason(PlexMetadataResponse response, int mediaSourceId)
private PlexSeason ProjectToSeason(PlexXmlMetadataResponse response, int mediaSourceId)
{
DateTime dateAdded = DateTimeOffset.FromUnixTimeSeconds(response.AddedAt).DateTime;
DateTime lastWriteTime = DateTimeOffset.FromUnixTimeSeconds(response.UpdatedAt).DateTime;
@@ -476,6 +573,16 @@ namespace ErsatzTV.Infrastructure.Plex
DateUpdated = lastWriteTime
};
metadata.Guids = Optional(response.Guid).Flatten().Map(g => new MetadataGuid { Guid = g.Id }).ToList();
if (!string.IsNullOrWhiteSpace(response.PlexGuid))
{
string normalized = NormalizeGuid(response.PlexGuid);
if (!string.IsNullOrWhiteSpace(normalized) && metadata.Guids.All(g => g.Guid != normalized))
{
metadata.Guids.Add(new MetadataGuid { Guid = normalized });
}
}
if (!string.IsNullOrWhiteSpace(response.Thumb))
{
var path = $"plex/{mediaSourceId}{response.Thumb}";
@@ -516,47 +623,14 @@ namespace ErsatzTV.Infrastructure.Plex
return season;
}
private PlexEpisode ProjectToEpisode(PlexMetadataResponse response, int mediaSourceId)
private PlexEpisode ProjectToEpisode(PlexXmlMetadataResponse response, int mediaSourceId)
{
PlexMediaResponse media = response.Media.Head();
PlexPartResponse part = media.Part.Head();
PlexMediaResponse<PlexXmlPartResponse> media = response.Media.Head();
PlexXmlPartResponse part = media.Part.Head();
DateTime dateAdded = DateTimeOffset.FromUnixTimeSeconds(response.AddedAt).DateTime;
DateTime lastWriteTime = DateTimeOffset.FromUnixTimeSeconds(response.UpdatedAt).DateTime;
var metadata = new EpisodeMetadata
{
MetadataKind = MetadataKind.External,
Title = response.Title,
SortTitle = _fallbackMetadataProvider.GetSortTitle(response.Title),
Plot = response.Summary,
Year = response.Year,
Tagline = response.Tagline,
DateAdded = dateAdded,
DateUpdated = lastWriteTime,
Actors = Optional(response.Role).Flatten().Map(r => ProjectToModel(r, dateAdded, lastWriteTime))
.ToList()
};
if (DateTime.TryParse(response.OriginallyAvailableAt, out DateTime releaseDate))
{
metadata.ReleaseDate = releaseDate;
}
if (!string.IsNullOrWhiteSpace(response.Thumb))
{
var path = $"plex/{mediaSourceId}{response.Thumb}";
var artwork = new Artwork
{
ArtworkKind = ArtworkKind.Thumbnail,
Path = path,
DateAdded = dateAdded,
DateUpdated = lastWriteTime
};
metadata.Artwork ??= new List<Artwork>();
metadata.Artwork.Add(artwork);
}
EpisodeMetadata metadata = ProjectToEpisodeMetadata(response, mediaSourceId);
var version = new MediaVersion
{
Name = "Main",
@@ -589,6 +663,67 @@ namespace ErsatzTV.Infrastructure.Plex
return episode;
}
private EpisodeMetadata ProjectToEpisodeMetadata(PlexMetadataResponse response, int mediaSourceId)
{
DateTime dateAdded = DateTimeOffset.FromUnixTimeSeconds(response.AddedAt).DateTime;
DateTime lastWriteTime = DateTimeOffset.FromUnixTimeSeconds(response.UpdatedAt).DateTime;
var metadata = new EpisodeMetadata
{
MetadataKind = MetadataKind.External,
Title = response.Title,
SortTitle = _fallbackMetadataProvider.GetSortTitle(response.Title),
Plot = response.Summary,
Year = response.Year,
Tagline = response.Tagline,
DateAdded = dateAdded,
DateUpdated = lastWriteTime,
Actors = Optional(response.Role).Flatten().Map(r => ProjectToModel(r, dateAdded, lastWriteTime))
.ToList(),
Directors = Optional(response.Director).Flatten().Map(d => new Director { Name = d.Tag }).ToList(),
Writers = Optional(response.Writer).Flatten().Map(w => new Writer { Name = w.Tag }).ToList()
};
if (response is PlexXmlMetadataResponse xml)
{
metadata.Guids = Optional(xml.Guid).Flatten().Map(g => new MetadataGuid { Guid = g.Id }).ToList();
if (!string.IsNullOrWhiteSpace(xml.PlexGuid))
{
string normalized = NormalizeGuid(xml.PlexGuid);
if (!string.IsNullOrWhiteSpace(normalized) && metadata.Guids.All(g => g.Guid != normalized))
{
metadata.Guids.Add(new MetadataGuid { Guid = normalized });
}
}
}
else
{
metadata.Guids = new List<MetadataGuid>();
}
if (DateTime.TryParse(response.OriginallyAvailableAt, out DateTime releaseDate))
{
metadata.ReleaseDate = releaseDate;
}
if (!string.IsNullOrWhiteSpace(response.Thumb))
{
var path = $"plex/{mediaSourceId}{response.Thumb}";
var artwork = new Artwork
{
ArtworkKind = ArtworkKind.Thumbnail,
Path = path,
DateAdded = dateAdded,
DateUpdated = lastWriteTime
};
metadata.Artwork ??= new List<Artwork>();
metadata.Artwork.Add(artwork);
}
return metadata;
}
private Actor ProjectToModel(PlexRoleResponse role, DateTime dateAdded, DateTime lastWriteTime)
{
var actor = new Actor { Name = role.Tag, Role = role.Role };
@@ -605,5 +740,32 @@ namespace ErsatzTV.Infrastructure.Plex
return actor;
}
private string NormalizeGuid(string guid)
{
if (guid.StartsWith("plex://show") ||
guid.StartsWith("plex://season") ||
guid.StartsWith("plex://episode") ||
guid.StartsWith("plex://movie"))
{
return guid;
}
if (guid.StartsWith("com.plexapp.agents.thetvdb"))
{
string strip1 = guid.Replace("com.plexapp.agents.thetvdb://", string.Empty);
string strip2 = strip1.Split("?").Head();
return $"tvdb://{strip2}";
}
if (guid.StartsWith("com.plexapp.agents.themoviedb"))
{
string strip1 = guid.Replace("com.plexapp.agents.themoviedb://", string.Empty);
string strip2 = strip1.Split("?").Head();
return $"tmdb://{strip2}";
}
throw new NotSupportedException(guid);
}
}
}