mirror of https://github.com/ErsatzTV/ErsatzTV.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1304 lines
49 KiB
1304 lines
49 KiB
using System.Globalization; |
|
using System.Xml.Serialization; |
|
using ErsatzTV.Core; |
|
using ErsatzTV.Core.Domain; |
|
using ErsatzTV.Core.Interfaces.Plex; |
|
using ErsatzTV.Core.Metadata; |
|
using ErsatzTV.Core.Plex; |
|
using ErsatzTV.Infrastructure.Plex.Models; |
|
using Microsoft.Extensions.Logging; |
|
using Newtonsoft.Json; |
|
using Refit; |
|
|
|
namespace ErsatzTV.Infrastructure.Plex; |
|
|
|
public class PlexServerApiClient : IPlexServerApiClient |
|
{ |
|
private readonly ILogger<PlexServerApiClient> _logger; |
|
private readonly PlexEtag _plexEtag; |
|
|
|
public PlexServerApiClient(PlexEtag plexEtag, ILogger<PlexServerApiClient> logger) |
|
{ |
|
_plexEtag = plexEtag; |
|
_logger = logger; |
|
} |
|
|
|
public async Task<bool> Ping(PlexConnection connection, PlexServerAuthToken token, CancellationToken cancellationToken) |
|
{ |
|
try |
|
{ |
|
IPlexServerApi service = XmlServiceFor(connection.Uri, TimeSpan.FromSeconds(5)); |
|
PlexXmlMediaContainerPingResponse pingResult = await service.Ping(token.AuthToken, cancellationToken); |
|
return token.ClientIdentifier == pingResult.MachineIdentifier; |
|
} |
|
catch (Exception) |
|
{ |
|
return false; |
|
} |
|
} |
|
|
|
public async Task<Either<BaseError, List<PlexLibrary>>> GetLibraries( |
|
PlexConnection connection, |
|
PlexServerAuthToken token) |
|
{ |
|
try |
|
{ |
|
IPlexServerApi service = RestService.For<IPlexServerApi>( |
|
new HttpClient |
|
{ |
|
BaseAddress = new Uri(connection.Uri), |
|
Timeout = TimeSpan.FromSeconds(10) |
|
}); |
|
List<PlexLibraryResponse> directory = |
|
await service.GetLibraries(token.AuthToken).Map(r => r.MediaContainer.Directory); |
|
List<PlexLibrary> response = directory |
|
.Filter(l => l.Type.ToLowerInvariant() is "movie" or "show") |
|
.Map(Project) |
|
.Somes() |
|
.ToList(); |
|
return response; |
|
} |
|
catch (Exception ex) |
|
{ |
|
return BaseError.New(ex.ToString()); |
|
} |
|
} |
|
|
|
public IAsyncEnumerable<Tuple<PlexMovie, int>> GetMovieLibraryContents( |
|
PlexLibrary library, |
|
PlexConnection connection, |
|
PlexServerAuthToken token) |
|
{ |
|
Task<PlexXmlMediaContainerStatsResponse> CountItems(IPlexServerApi service) |
|
{ |
|
return service.GetLibrarySection(library.Key, token.AuthToken); |
|
} |
|
|
|
Task<IEnumerable<PlexMovie>> GetItems(IPlexServerApi _, IPlexServerApi jsonService, int skip, int pageSize) |
|
{ |
|
return jsonService |
|
.GetLibrarySectionContents(library.Key, skip, pageSize, token.AuthToken) |
|
.Map( |
|
r => r.MediaContainer.Metadata.Filter( |
|
m => m.Media.Count > 0 && m.Media.Any(media => media.Part.Count > 0))) |
|
.Map(list => list.Map(metadata => ProjectToMovie(metadata, library.MediaSourceId))); |
|
} |
|
|
|
return GetPagedLibraryContents(connection, CountItems, GetItems); |
|
} |
|
|
|
public IAsyncEnumerable<Tuple<PlexShow, int>> GetShowLibraryContents( |
|
PlexLibrary library, |
|
PlexConnection connection, |
|
PlexServerAuthToken token) |
|
{ |
|
Task<PlexXmlMediaContainerStatsResponse> CountItems(IPlexServerApi service) |
|
{ |
|
return service.GetLibrarySection(library.Key, token.AuthToken); |
|
} |
|
|
|
Task<IEnumerable<PlexShow>> GetItems(IPlexServerApi _, IPlexServerApi jsonService, int skip, int pageSize) |
|
{ |
|
return jsonService |
|
.GetLibrarySectionContents(library.Key, skip, pageSize, token.AuthToken) |
|
.Map(r => r.MediaContainer.Metadata ?? new List<PlexMetadataResponse>()) |
|
.Map(list => list.Map(metadata => ProjectToShow(metadata, library.MediaSourceId))); |
|
} |
|
|
|
return GetPagedLibraryContents(connection, CountItems, GetItems); |
|
} |
|
|
|
public IAsyncEnumerable<Tuple<PlexOtherVideo, int>> GetOtherVideoLibraryContents( |
|
PlexLibrary library, |
|
PlexConnection connection, |
|
PlexServerAuthToken token) |
|
{ |
|
Task<PlexXmlMediaContainerStatsResponse> CountItems(IPlexServerApi service) |
|
{ |
|
return service.GetLibrarySection(library.Key, token.AuthToken); |
|
} |
|
|
|
Task<IEnumerable<PlexOtherVideo>> GetItems(IPlexServerApi _, IPlexServerApi jsonService, int skip, int pageSize) |
|
{ |
|
return jsonService |
|
.GetLibrarySectionContents(library.Key, skip, pageSize, token.AuthToken) |
|
.Map( |
|
r => r.MediaContainer.Metadata.Filter( |
|
m => m.Media.Count > 0 && m.Media.Any(media => media.Part.Count > 0))) |
|
.Map(list => list.Map(metadata => ProjectToOtherVideo(metadata, library.MediaSourceId, library))); |
|
} |
|
|
|
return GetPagedLibraryContents(connection, CountItems, GetItems); |
|
} |
|
|
|
public IAsyncEnumerable<Tuple<PlexSeason, int>> GetShowSeasons( |
|
PlexLibrary library, |
|
PlexShow show, |
|
PlexConnection connection, |
|
PlexServerAuthToken token) |
|
{ |
|
string showMetadataKey = show.Key.Split("/").Reverse().Skip(1).Head(); |
|
|
|
Task<PlexXmlMediaContainerStatsResponse> CountItems(IPlexServerApi service) |
|
{ |
|
return service.CountShowChildren(showMetadataKey, token.AuthToken); |
|
} |
|
|
|
Task<IEnumerable<PlexSeason>> GetItems(IPlexServerApi xmlService, IPlexServerApi _, int skip, int pageSize) |
|
{ |
|
return xmlService.GetShowChildren(showMetadataKey, skip, pageSize, token.AuthToken) |
|
.Map(r => r.Metadata.Filter(m => !m.Key.Contains("allLeaves"))) |
|
.Map(list => list.Map(metadata => ProjectToSeason(metadata, library.MediaSourceId))); |
|
} |
|
|
|
return GetPagedLibraryContents(connection, CountItems, GetItems); |
|
} |
|
|
|
public IAsyncEnumerable<Tuple<PlexEpisode, int>> GetSeasonEpisodes( |
|
PlexLibrary library, |
|
PlexSeason season, |
|
PlexConnection connection, |
|
PlexServerAuthToken token) |
|
{ |
|
string seasonMetadataKey = season.Key.Split("/").Reverse().Skip(1).Head(); |
|
|
|
Task<PlexXmlMediaContainerStatsResponse> CountItems(IPlexServerApi service) |
|
{ |
|
return service.CountSeasonChildren(seasonMetadataKey, token.AuthToken); |
|
} |
|
|
|
Task<IEnumerable<PlexEpisode>> GetItems(IPlexServerApi xmlService, IPlexServerApi _, int skip, int pageSize) |
|
{ |
|
return xmlService.GetSeasonChildren(seasonMetadataKey, skip, pageSize, token.AuthToken) |
|
.Map(r => r.Metadata.Filter(m => m.Media.Count > 0 && m.Media.Any(media => media.Part.Count > 0))) |
|
.Map(list => list.Map(metadata => ProjectToEpisode(metadata, library.MediaSourceId))); |
|
} |
|
|
|
return GetPagedLibraryContents(connection, CountItems, GetItems); |
|
} |
|
|
|
public async Task<Either<BaseError, ShowMetadata>> GetShowMetadata( |
|
PlexLibrary library, |
|
string key, |
|
PlexConnection connection, |
|
PlexServerAuthToken token) |
|
{ |
|
try |
|
{ |
|
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) |
|
{ |
|
return BaseError.New(ex.ToString()); |
|
} |
|
} |
|
|
|
public async Task<Either<BaseError, Tuple<MovieMetadata, MediaVersion>>> GetMovieMetadataAndStatistics( |
|
int plexMediaSourceId, |
|
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.Any(media => media.Part.Count > 0))); |
|
return maybeResponse.Match( |
|
response => |
|
{ |
|
Option<MediaVersion> maybeVersion = ProjectToMediaVersion(response.Metadata); |
|
return maybeVersion.Match<Either<BaseError, Tuple<MovieMetadata, MediaVersion>>>( |
|
version => Tuple( |
|
ProjectToMovieMetadata(version, response.Metadata, plexMediaSourceId), |
|
version), |
|
() => BaseError.New("Unable to locate metadata")); |
|
}, |
|
() => BaseError.New("Unable to locate metadata")); |
|
} |
|
catch (Exception ex) |
|
{ |
|
return BaseError.New(ex.ToString()); |
|
} |
|
} |
|
|
|
public async Task<Either<BaseError, Tuple<OtherVideoMetadata, MediaVersion>>> GetOtherVideoMetadataAndStatistics( |
|
int plexMediaSourceId, |
|
string key, |
|
PlexConnection connection, |
|
PlexServerAuthToken token, |
|
PlexLibrary library) |
|
{ |
|
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.Any(media => media.Part.Count > 0))); |
|
return maybeResponse.Match( |
|
response => |
|
{ |
|
Option<MediaVersion> maybeVersion = ProjectToMediaVersion(response.Metadata); |
|
return maybeVersion.Match<Either<BaseError, Tuple<OtherVideoMetadata, MediaVersion>>>( |
|
version => Tuple( |
|
ProjectToOtherVideoMetadata(version, response.Metadata, plexMediaSourceId, library), |
|
version), |
|
() => BaseError.New("Unable to locate metadata")); |
|
}, |
|
() => BaseError.New("Unable to locate metadata")); |
|
} |
|
catch (Exception ex) |
|
{ |
|
return BaseError.New(ex.ToString()); |
|
} |
|
} |
|
|
|
public async Task<Either<BaseError, Tuple<EpisodeMetadata, MediaVersion>>> GetEpisodeMetadataAndStatistics( |
|
int plexMediaSourceId, |
|
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.Any(media => media.Part.Count > 0))); |
|
return maybeResponse.Match( |
|
response => |
|
{ |
|
Option<MediaVersion> maybeVersion = ProjectToMediaVersion(response.Metadata); |
|
return maybeVersion.Match<Either<BaseError, Tuple<EpisodeMetadata, MediaVersion>>>( |
|
version => Tuple( |
|
ProjectToEpisodeMetadata(version, response.Metadata, plexMediaSourceId), |
|
version), |
|
() => BaseError.New("Unable to locate metadata")); |
|
}, |
|
() => BaseError.New("Unable to locate metadata")); |
|
} |
|
catch (Exception ex) |
|
{ |
|
return BaseError.New(ex.ToString()); |
|
} |
|
} |
|
|
|
public IAsyncEnumerable<Tuple<PlexCollection, int>> GetAllCollections( |
|
PlexConnection connection, |
|
PlexServerAuthToken token, |
|
CancellationToken cancellationToken) |
|
{ |
|
return GetPagedLibraryContents(connection, CountItems, GetItems); |
|
|
|
Task<PlexXmlMediaContainerStatsResponse> CountItems(IPlexServerApi service) |
|
{ |
|
return service.GetCollectionCount(token.AuthToken); |
|
} |
|
|
|
Task<IEnumerable<PlexCollection>> GetItems(IPlexServerApi _, IPlexServerApi jsonService, int skip, int pageSize) |
|
{ |
|
return jsonService |
|
.GetCollections(skip, pageSize, token.AuthToken) |
|
.Map(r => r.MediaContainer.Metadata) |
|
.Map(list => list.Map(m => ProjectToCollection(connection.PlexMediaSource, m)).Somes()); |
|
} |
|
} |
|
|
|
public IAsyncEnumerable<Tuple<MediaItem, int>> GetCollectionItems( |
|
PlexConnection connection, |
|
PlexServerAuthToken token, |
|
string key, |
|
CancellationToken cancellationToken) |
|
{ |
|
return GetPagedLibraryContents(connection, CountItems, GetItems); |
|
|
|
Task<PlexXmlMediaContainerStatsResponse> CountItems(IPlexServerApi service) |
|
{ |
|
return service.GetCollectionItemsCount(key, token.AuthToken); |
|
} |
|
|
|
Task<IEnumerable<MediaItem>> GetItems(IPlexServerApi _, IPlexServerApi jsonService, int skip, int pageSize) |
|
{ |
|
return jsonService |
|
.GetCollectionItems(key, skip, pageSize, token.AuthToken) |
|
.Map(r => Optional(r.MediaContainer.Metadata).Flatten()) |
|
.Map(list => list.Map(ProjectToCollectionMediaItem).Somes()); |
|
} |
|
} |
|
|
|
private static async IAsyncEnumerable<Tuple<TItem, int>> GetPagedLibraryContents<TItem>( |
|
PlexConnection connection, |
|
Func<IPlexServerApi, Task<PlexXmlMediaContainerStatsResponse>> countItems, |
|
Func<IPlexServerApi, IPlexServerApi, int, int, Task<IEnumerable<TItem>>> getItems) |
|
{ |
|
IPlexServerApi xmlService = XmlServiceFor(connection.Uri); |
|
|
|
int size = await countItems(xmlService).Map(r => r.TotalSize); |
|
if (size == 0) |
|
{ |
|
yield break; |
|
} |
|
|
|
const int PAGE_SIZE = 10; |
|
|
|
IPlexServerApi jsonService = RestService.For<IPlexServerApi>(connection.Uri); |
|
int pages = (size - 1) / PAGE_SIZE + 1; |
|
|
|
for (var i = 0; i < pages; i++) |
|
{ |
|
int skip = i * PAGE_SIZE; |
|
|
|
Task<IEnumerable<TItem>> result = getItems(xmlService, jsonService, skip, PAGE_SIZE); |
|
|
|
foreach (TItem item in await result) |
|
{ |
|
yield return new Tuple<TItem, int>(item, size); |
|
} |
|
} |
|
} |
|
|
|
private static IPlexServerApi XmlServiceFor(string uri, TimeSpan? timeout = null) |
|
{ |
|
var overrides = new XmlAttributeOverrides(); |
|
var attrs = new XmlAttributes { XmlIgnore = true }; |
|
overrides.Add(typeof(PlexMetadataResponse), "Media", attrs); |
|
|
|
TimeSpan httpClientTimeout = timeout ?? TimeSpan.FromSeconds(30); |
|
|
|
return RestService.For<IPlexServerApi>( |
|
new HttpClient |
|
{ |
|
BaseAddress = new Uri(uri), |
|
Timeout = httpClientTimeout |
|
}, |
|
new RefitSettings |
|
{ |
|
ContentSerializer = new XmlContentSerializer( |
|
new XmlContentSerializerSettings |
|
{ |
|
XmlAttributeOverrides = overrides |
|
}) |
|
}); |
|
} |
|
|
|
private static Option<PlexLibrary> Project(PlexLibraryResponse response) |
|
{ |
|
List<LibraryPath> paths = |
|
[ |
|
new LibraryPath |
|
{ |
|
Path = JsonConvert.SerializeObject( |
|
new LibraryPaths { Paths = response.Location.Map(l => l.Path).ToList() }) |
|
} |
|
]; |
|
|
|
return response.Type switch |
|
{ |
|
"show" => new PlexLibrary |
|
{ |
|
Key = response.Key, |
|
Name = response.Title, |
|
MediaKind = LibraryMediaKind.Shows, |
|
ShouldSyncItems = false, |
|
Paths = paths |
|
}, |
|
"movie" => new PlexLibrary |
|
{ |
|
Key = response.Key, |
|
Name = response.Title, |
|
MediaKind = response.Agent == "com.plexapp.agents.none" && response.Language == "xn" |
|
? LibraryMediaKind.OtherVideos |
|
: LibraryMediaKind.Movies, |
|
ShouldSyncItems = false, |
|
Paths = paths |
|
}, |
|
// TODO: "artist" for music libraries |
|
_ => None |
|
}; |
|
} |
|
|
|
private Option<PlexCollection> ProjectToCollection( |
|
PlexMediaSource plexMediaSource, |
|
PlexCollectionMetadataResponse item) |
|
{ |
|
try |
|
{ |
|
// skip collections in libraries that are not synchronized |
|
if (plexMediaSource.Libraries.OfType<PlexLibrary>().Any( |
|
l => l.Key == item.LibrarySectionId.ToString(CultureInfo.InvariantCulture) && |
|
l.ShouldSyncItems == false)) |
|
{ |
|
return Option<PlexCollection>.None; |
|
} |
|
|
|
return new PlexCollection |
|
{ |
|
Key = item.RatingKey, |
|
Etag = _plexEtag.ForCollection(item), |
|
Name = item.Title |
|
}; |
|
} |
|
catch (Exception ex) |
|
{ |
|
_logger.LogWarning(ex, "Error projecting Plex collection"); |
|
return None; |
|
} |
|
} |
|
|
|
private Option<MediaItem> ProjectToCollectionMediaItem(PlexCollectionItemMetadataResponse item) |
|
{ |
|
try |
|
{ |
|
return item.Type switch |
|
{ |
|
"movie" => new PlexMovie { Key = item.Key }, |
|
"show" => new PlexShow { Key = item.Key }, |
|
"season" => new PlexSeason { Key = item.Key }, |
|
"episode" => new PlexEpisode { Key = item.Key }, |
|
_ => None |
|
}; |
|
} |
|
catch (Exception ex) |
|
{ |
|
_logger.LogWarning(ex, "Error projecting Plex collection media item"); |
|
return None; |
|
} |
|
} |
|
|
|
private PlexMovie ProjectToMovie(PlexMetadataResponse response, int mediaSourceId) |
|
{ |
|
PlexMediaResponse<PlexPartResponse> media = response.Media |
|
.Filter(media => media.Part.Count != 0) |
|
.MaxBy(media => media.Id); |
|
|
|
PlexPartResponse part = media.Part.Head(); |
|
DateTime dateAdded = DateTimeOffset.FromUnixTimeSeconds(response.AddedAt).DateTime; |
|
DateTime lastWriteTime = DateTimeOffset.FromUnixTimeSeconds(response.UpdatedAt).DateTime; |
|
|
|
var version = new MediaVersion |
|
{ |
|
Name = "Main", |
|
Duration = TimeSpan.FromMilliseconds(media.Duration), |
|
Width = media.Width, |
|
Height = media.Height, |
|
// specifically omit sample aspect ratio |
|
DateAdded = dateAdded, |
|
DateUpdated = lastWriteTime, |
|
MediaFiles = new List<MediaFile> |
|
{ |
|
new PlexMediaFile |
|
{ |
|
PlexId = part.Id, |
|
Key = part.Key, |
|
Path = part.File |
|
} |
|
}, |
|
Streams = new List<MediaStream>() |
|
}; |
|
|
|
MovieMetadata metadata = ProjectToMovieMetadata(version, response, mediaSourceId); |
|
|
|
var movie = new PlexMovie |
|
{ |
|
Etag = _plexEtag.ForMovie(response), |
|
Key = response.Key, |
|
MovieMetadata = new List<MovieMetadata> { metadata }, |
|
MediaVersions = new List<MediaVersion> { version }, |
|
TraktListItems = new List<TraktListItem>() |
|
}; |
|
|
|
return movie; |
|
} |
|
|
|
private MovieMetadata ProjectToMovieMetadata(MediaVersion version, PlexMetadataResponse response, int mediaSourceId) |
|
{ |
|
DateTime dateAdded = DateTimeOffset.FromUnixTimeSeconds(response.AddedAt).DateTime; |
|
DateTime lastWriteTime = DateTimeOffset.FromUnixTimeSeconds(response.UpdatedAt).DateTime; |
|
|
|
var metadata = new MovieMetadata |
|
{ |
|
MetadataKind = MetadataKind.External, |
|
Title = response.Title, |
|
SortTitle = SortTitle.GetSortTitle(response.Title), |
|
Plot = response.Summary, |
|
Year = response.Year, |
|
Tagline = response.Tagline, |
|
ContentRating = response.ContentRating, |
|
DateAdded = dateAdded, |
|
DateUpdated = lastWriteTime, |
|
Genres = Optional(response.Genre).Flatten().Map(g => new Genre { Name = g.Tag }).ToList(), |
|
Tags = new List<Tag>(), |
|
Studios = new List<Studio>(), |
|
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(), |
|
Subtitles = new List<Subtitle>() |
|
}; |
|
|
|
var subtitleStreams = version.Streams |
|
.Filter(s => s.MediaStreamKind is MediaStreamKind.Subtitle or MediaStreamKind.ExternalSubtitle) |
|
.ToList(); |
|
|
|
metadata.Subtitles.AddRange(subtitleStreams.Map(Subtitle.FromMediaStream)); |
|
|
|
if (response is PlexXmlMetadataResponse xml) |
|
{ |
|
metadata.Guids = Optional(xml.Guid).Flatten().Map(g => new MetadataGuid { Guid = g.Id }).ToList(); |
|
if (!string.IsNullOrWhiteSpace(xml.PlexGuid)) |
|
{ |
|
Option<string> normalized = NormalizeGuid(xml.PlexGuid); |
|
foreach (string guid in normalized) |
|
{ |
|
if (metadata.Guids.All(g => g.Guid != guid)) |
|
{ |
|
metadata.Guids.Add(new MetadataGuid { Guid = guid }); |
|
} |
|
} |
|
} |
|
} |
|
else |
|
{ |
|
metadata.Guids = new List<MetadataGuid>(); |
|
} |
|
|
|
foreach (PlexLabelResponse label in Optional(response.Label).Flatten()) |
|
{ |
|
metadata.Tags.Add( |
|
new Tag { Name = label.Tag, ExternalCollectionId = label.Id.ToString(CultureInfo.InvariantCulture) }); |
|
} |
|
|
|
if (!string.IsNullOrWhiteSpace(response.Studio)) |
|
{ |
|
metadata.Studios.Add(new Studio { Name = response.Studio }); |
|
} |
|
|
|
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.Poster, |
|
Path = path, |
|
DateAdded = dateAdded, |
|
DateUpdated = lastWriteTime |
|
}; |
|
|
|
metadata.Artwork ??= new List<Artwork>(); |
|
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); |
|
} |
|
|
|
return metadata; |
|
} |
|
|
|
private static Option<MediaVersion> ProjectToMediaVersion(PlexXmlMetadataResponse response) |
|
{ |
|
PlexMediaResponse<PlexXmlPartResponse> media = response.Media |
|
.Filter(media => media.Part.Count != 0) |
|
.MaxBy(media => media.Id); |
|
|
|
List<PlexStreamResponse> streams = media.Part.Head().Stream; |
|
DateTime dateUpdated = DateTimeOffset.FromUnixTimeSeconds(response.UpdatedAt).DateTime; |
|
Option<PlexStreamResponse> maybeVideoStream = streams.Find(s => s.StreamType == 1); |
|
return maybeVideoStream.Map( |
|
videoStream => |
|
{ |
|
var version = new MediaVersion |
|
{ |
|
Duration = TimeSpan.FromMilliseconds(media.Duration), |
|
SampleAspectRatio = string.IsNullOrWhiteSpace(videoStream.PixelAspectRatio) |
|
? "1:1" |
|
: videoStream.PixelAspectRatio, |
|
VideoScanKind = videoStream.ScanType switch |
|
{ |
|
"interlaced" => VideoScanKind.Interlaced, |
|
"progressive" => VideoScanKind.Progressive, |
|
_ => VideoScanKind.Unknown |
|
}, |
|
Streams = new List<MediaStream>(), |
|
DateUpdated = dateUpdated, |
|
Width = videoStream.Width, |
|
Height = videoStream.Height, |
|
RFrameRate = videoStream.FrameRate, |
|
DisplayAspectRatio = media.AspectRatio == 0 |
|
? string.Empty |
|
: media.AspectRatio.ToString("0.00###", CultureInfo.InvariantCulture), |
|
Chapters = Optional(response.Chapters).Flatten().Map(ProjectToModel).ToList() |
|
}; |
|
|
|
version.Streams.Add( |
|
new MediaStream |
|
{ |
|
MediaVersionId = version.Id, |
|
MediaStreamKind = MediaStreamKind.Video, |
|
Index = videoStream.Index!.Value, |
|
Codec = videoStream.Codec, |
|
Profile = (videoStream.Profile ?? string.Empty).ToLowerInvariant(), |
|
Default = videoStream.Default, |
|
Language = videoStream.LanguageCode, |
|
Forced = videoStream.Forced, |
|
BitsPerRawSample = videoStream.BitDepth, |
|
ColorRange = (videoStream.ColorRange ?? string.Empty).ToLowerInvariant(), |
|
ColorSpace = (videoStream.ColorSpace ?? string.Empty).ToLowerInvariant(), |
|
ColorTransfer = (videoStream.ColorTrc ?? string.Empty).ToLowerInvariant(), |
|
ColorPrimaries = (videoStream.ColorPrimaries ?? string.Empty).ToLowerInvariant() |
|
}); |
|
|
|
foreach (PlexStreamResponse audioStream in streams.Filter(s => s.StreamType == 2 && s.Index.HasValue)) |
|
{ |
|
var stream = new MediaStream |
|
{ |
|
MediaVersionId = version.Id, |
|
MediaStreamKind = MediaStreamKind.Audio, |
|
Index = audioStream.Index.Value, |
|
Codec = audioStream.Codec, |
|
Profile = (audioStream.Profile ?? string.Empty).ToLowerInvariant(), |
|
Channels = audioStream.Channels, |
|
Default = audioStream.Default, |
|
Forced = audioStream.Forced, |
|
Language = audioStream.LanguageCode, |
|
Title = audioStream.Title ?? string.Empty |
|
}; |
|
|
|
version.Streams.Add(stream); |
|
} |
|
|
|
// filter to embedded subtitles, but ignore "embedded in video" closed-caption streams |
|
foreach (PlexStreamResponse subtitleStream in |
|
streams.Filter(s => s.StreamType == 3 && s.Index.HasValue && !s.EmbeddedInVideo)) |
|
{ |
|
var stream = new MediaStream |
|
{ |
|
MediaVersionId = version.Id, |
|
MediaStreamKind = MediaStreamKind.Subtitle, |
|
Index = subtitleStream.Index.Value, |
|
Codec = subtitleStream.Codec, |
|
Default = subtitleStream.Default, |
|
Forced = subtitleStream.Forced, |
|
Language = subtitleStream.LanguageCode |
|
}; |
|
|
|
version.Streams.Add(stream); |
|
} |
|
|
|
// also include external subtitles |
|
foreach (PlexStreamResponse subtitleStream in |
|
streams.Filter( |
|
s => s.StreamType == 3 && !s.Index.HasValue && !string.IsNullOrWhiteSpace(s.Key))) |
|
{ |
|
var stream = new MediaStream |
|
{ |
|
MediaVersionId = version.Id, |
|
MediaStreamKind = MediaStreamKind.ExternalSubtitle, |
|
|
|
// hacky? maybe... |
|
FileName = subtitleStream.Key, |
|
Index = subtitleStream.Id, |
|
|
|
Codec = subtitleStream.Codec, |
|
Default = subtitleStream.Default, |
|
Forced = subtitleStream.Forced, |
|
Language = subtitleStream.LanguageCode |
|
}; |
|
|
|
version.Streams.Add(stream); |
|
} |
|
|
|
return version; |
|
}); |
|
} |
|
|
|
private PlexShow ProjectToShow(PlexMetadataResponse response, int mediaSourceId) |
|
{ |
|
ShowMetadata metadata = ProjectToShowMetadata(response, mediaSourceId); |
|
|
|
var show = new PlexShow |
|
{ |
|
Key = response.Key, |
|
Etag = _plexEtag.ForShow(response), |
|
ShowMetadata = new List<ShowMetadata> { metadata }, |
|
TraktListItems = new List<TraktListItem>() |
|
}; |
|
|
|
return show; |
|
} |
|
|
|
private ShowMetadata ProjectToShowMetadata(PlexMetadataResponse response, int mediaSourceId) |
|
{ |
|
DateTime dateAdded = DateTimeOffset.FromUnixTimeSeconds(response.AddedAt).DateTime; |
|
DateTime lastWriteTime = DateTimeOffset.FromUnixTimeSeconds(response.UpdatedAt).DateTime; |
|
|
|
var metadata = new ShowMetadata |
|
{ |
|
MetadataKind = MetadataKind.External, |
|
Title = response.Title, |
|
SortTitle = SortTitle.GetSortTitle(response.Title), |
|
Plot = response.Summary, |
|
Year = response.Year, |
|
Tagline = response.Tagline, |
|
ContentRating = response.ContentRating, |
|
DateAdded = dateAdded, |
|
DateUpdated = lastWriteTime, |
|
Genres = Optional(response.Genre).Flatten().Map(g => new Genre { Name = g.Tag }).ToList(), |
|
Tags = new List<Tag>(), |
|
Studios = new List<Studio>(), |
|
Actors = Optional(response.Role).Flatten().Map(r => ProjectToModel(r, dateAdded, lastWriteTime)) |
|
.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)) |
|
{ |
|
Option<string> normalized = NormalizeGuid(xml.PlexGuid); |
|
foreach (string guid in normalized) |
|
{ |
|
if (metadata.Guids.All(g => g.Guid != guid)) |
|
{ |
|
metadata.Guids.Add(new MetadataGuid { Guid = guid }); |
|
} |
|
} |
|
} |
|
} |
|
else |
|
{ |
|
metadata.Guids = new List<MetadataGuid>(); |
|
} |
|
|
|
if (!string.IsNullOrWhiteSpace(response.Studio)) |
|
{ |
|
metadata.Studios.Add(new Studio { Name = response.Studio }); |
|
} |
|
|
|
foreach (PlexLabelResponse label in Optional(response.Label).Flatten()) |
|
{ |
|
metadata.Tags.Add( |
|
new Tag { Name = label.Tag, ExternalCollectionId = label.Id.ToString(CultureInfo.InvariantCulture) }); |
|
} |
|
|
|
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.Poster, |
|
Path = path, |
|
DateAdded = dateAdded, |
|
DateUpdated = lastWriteTime |
|
}; |
|
|
|
metadata.Artwork ??= new List<Artwork>(); |
|
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); |
|
} |
|
|
|
return metadata; |
|
} |
|
|
|
private PlexSeason ProjectToSeason(PlexXmlMetadataResponse response, int mediaSourceId) |
|
{ |
|
DateTime dateAdded = DateTimeOffset.FromUnixTimeSeconds(response.AddedAt).DateTime; |
|
DateTime lastWriteTime = DateTimeOffset.FromUnixTimeSeconds(response.UpdatedAt).DateTime; |
|
|
|
var metadata = new SeasonMetadata |
|
{ |
|
MetadataKind = MetadataKind.External, |
|
Title = response.Title, |
|
SortTitle = SortTitle.GetSortTitle(response.Title), |
|
Year = response.Year, |
|
DateAdded = dateAdded, |
|
DateUpdated = lastWriteTime, |
|
Tags = new List<Tag>() |
|
}; |
|
|
|
metadata.Guids = Optional(response.Guid).Flatten().Map(g => new MetadataGuid { Guid = g.Id }).ToList(); |
|
if (!string.IsNullOrWhiteSpace(response.PlexGuid)) |
|
{ |
|
Option<string> normalized = NormalizeGuid(response.PlexGuid); |
|
foreach (string guid in normalized) |
|
{ |
|
if (metadata.Guids.All(g => g.Guid != guid)) |
|
{ |
|
metadata.Guids.Add(new MetadataGuid { Guid = guid }); |
|
} |
|
} |
|
} |
|
|
|
if (!string.IsNullOrWhiteSpace(response.Thumb)) |
|
{ |
|
var path = $"plex/{mediaSourceId}{response.Thumb}"; |
|
var artwork = new Artwork |
|
{ |
|
ArtworkKind = ArtworkKind.Poster, |
|
Path = path, |
|
DateAdded = dateAdded, |
|
DateUpdated = lastWriteTime |
|
}; |
|
|
|
metadata.Artwork ??= new List<Artwork>(); |
|
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 season = new PlexSeason |
|
{ |
|
Key = response.Key, |
|
Etag = _plexEtag.ForSeason(response), |
|
SeasonNumber = response.Index, |
|
SeasonMetadata = new List<SeasonMetadata> { metadata }, |
|
TraktListItems = new List<TraktListItem>() |
|
}; |
|
|
|
return season; |
|
} |
|
|
|
private PlexEpisode ProjectToEpisode(PlexXmlMetadataResponse response, int mediaSourceId) |
|
{ |
|
PlexMediaResponse<PlexXmlPartResponse> media = response.Media |
|
.Filter(media => media.Part.Count != 0) |
|
.MaxBy(media => media.Id); |
|
|
|
PlexXmlPartResponse part = media.Part.Head(); |
|
DateTime dateAdded = DateTimeOffset.FromUnixTimeSeconds(response.AddedAt).DateTime; |
|
DateTime lastWriteTime = DateTimeOffset.FromUnixTimeSeconds(response.UpdatedAt).DateTime; |
|
|
|
var version = new MediaVersion |
|
{ |
|
Name = "Main", |
|
Duration = TimeSpan.FromMilliseconds(media.Duration), |
|
Width = media.Width, |
|
Height = media.Height, |
|
DateAdded = dateAdded, |
|
DateUpdated = lastWriteTime, |
|
MediaFiles = new List<MediaFile> |
|
{ |
|
new PlexMediaFile |
|
{ |
|
PlexId = part.Id, |
|
Key = part.Key, |
|
Path = part.File |
|
} |
|
}, |
|
// specifically omit stream details |
|
Streams = new List<MediaStream>() |
|
}; |
|
|
|
EpisodeMetadata metadata = ProjectToEpisodeMetadata(version, response, mediaSourceId); |
|
|
|
var episode = new PlexEpisode |
|
{ |
|
Key = response.Key, |
|
Etag = _plexEtag.ForEpisode(response), |
|
EpisodeMetadata = new List<EpisodeMetadata> { metadata }, |
|
MediaVersions = new List<MediaVersion> { version }, |
|
TraktListItems = new List<TraktListItem>() |
|
}; |
|
|
|
return episode; |
|
} |
|
|
|
private EpisodeMetadata ProjectToEpisodeMetadata( |
|
MediaVersion version, |
|
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 = SortTitle.GetSortTitle(response.Title), |
|
EpisodeNumber = response.Index, |
|
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(), |
|
Tags = new List<Tag>(), |
|
Subtitles = new List<Subtitle>() |
|
}; |
|
|
|
var subtitleStreams = version.Streams |
|
.Filter(s => s.MediaStreamKind is MediaStreamKind.Subtitle or MediaStreamKind.ExternalSubtitle) |
|
.ToList(); |
|
|
|
metadata.Subtitles.AddRange(subtitleStreams.Map(Subtitle.FromMediaStream)); |
|
|
|
if (response is PlexXmlMetadataResponse xml) |
|
{ |
|
metadata.Guids = Optional(xml.Guid).Flatten().Map(g => new MetadataGuid { Guid = g.Id }).ToList(); |
|
if (!string.IsNullOrWhiteSpace(xml.PlexGuid)) |
|
{ |
|
Option<string> normalized = NormalizeGuid(xml.PlexGuid); |
|
foreach (string guid in normalized) |
|
{ |
|
if (metadata.Guids.All(g => g.Guid != guid)) |
|
{ |
|
metadata.Guids.Add(new MetadataGuid { Guid = guid }); |
|
} |
|
} |
|
} |
|
} |
|
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 static Actor ProjectToModel(PlexRoleResponse role, DateTime dateAdded, DateTime lastWriteTime) |
|
{ |
|
var actor = new Actor { Name = role.Tag, Role = role.Role }; |
|
if (!string.IsNullOrWhiteSpace(role.Thumb)) |
|
{ |
|
actor.Artwork = new Artwork |
|
{ |
|
Path = role.Thumb, |
|
ArtworkKind = ArtworkKind.Thumbnail, |
|
DateAdded = dateAdded, |
|
DateUpdated = lastWriteTime |
|
}; |
|
} |
|
|
|
return actor; |
|
} |
|
|
|
private static MediaChapter ProjectToModel(PlexChapterResponse chapter) => |
|
new() |
|
{ |
|
ChapterId = chapter.Index, |
|
StartTime = TimeSpan.FromMilliseconds(chapter.StartTimeOffset), |
|
EndTime = TimeSpan.FromMilliseconds(chapter.EndTimeOffset) |
|
}; |
|
|
|
private PlexOtherVideo ProjectToOtherVideo(PlexMetadataResponse response, int mediaSourceId, PlexLibrary library) |
|
{ |
|
PlexMediaResponse<PlexPartResponse> media = response.Media |
|
.Filter(media => media.Part.Count != 0) |
|
.MaxBy(media => media.Id); |
|
|
|
PlexPartResponse part = media.Part.Head(); |
|
DateTime dateAdded = DateTimeOffset.FromUnixTimeSeconds(response.AddedAt).DateTime; |
|
DateTime lastWriteTime = DateTimeOffset.FromUnixTimeSeconds(response.UpdatedAt).DateTime; |
|
|
|
var version = new MediaVersion |
|
{ |
|
Name = "Main", |
|
Duration = TimeSpan.FromMilliseconds(media.Duration), |
|
Width = media.Width, |
|
Height = media.Height, |
|
// specifically omit sample aspect ratio |
|
DateAdded = dateAdded, |
|
DateUpdated = lastWriteTime, |
|
MediaFiles = new List<MediaFile> |
|
{ |
|
new PlexMediaFile |
|
{ |
|
PlexId = part.Id, |
|
Key = part.Key, |
|
Path = part.File |
|
} |
|
}, |
|
Streams = new List<MediaStream>() |
|
}; |
|
|
|
OtherVideoMetadata metadata = ProjectToOtherVideoMetadata(version, response, mediaSourceId, library); |
|
|
|
var otherVideo = new PlexOtherVideo |
|
{ |
|
Etag = _plexEtag.ForMovie(response), |
|
Key = response.Key, |
|
OtherVideoMetadata = new List<OtherVideoMetadata> { metadata }, |
|
MediaVersions = new List<MediaVersion> { version }, |
|
TraktListItems = new List<TraktListItem>() |
|
}; |
|
|
|
return otherVideo; |
|
} |
|
|
|
private OtherVideoMetadata ProjectToOtherVideoMetadata(MediaVersion version, PlexMetadataResponse response, int mediaSourceId, PlexLibrary library) |
|
{ |
|
DateTime dateAdded = DateTimeOffset.FromUnixTimeSeconds(response.AddedAt).DateTime; |
|
DateTime lastWriteTime = DateTimeOffset.FromUnixTimeSeconds(response.UpdatedAt).DateTime; |
|
|
|
var metadata = new OtherVideoMetadata |
|
{ |
|
MetadataKind = MetadataKind.External, |
|
Title = response.Title, |
|
SortTitle = SortTitle.GetSortTitle(response.Title), |
|
Plot = response.Summary, |
|
Year = response.Year, |
|
Tagline = response.Tagline, |
|
ContentRating = response.ContentRating, |
|
DateAdded = dateAdded, |
|
DateUpdated = lastWriteTime, |
|
Genres = Optional(response.Genre).Flatten().Map(g => new Genre { Name = g.Tag }).ToList(), |
|
Tags = new List<Tag>(), |
|
Studios = new List<Studio>(), |
|
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(), |
|
Subtitles = new List<Subtitle>() |
|
}; |
|
|
|
var subtitleStreams = version.Streams |
|
.Filter(s => s.MediaStreamKind is MediaStreamKind.Subtitle or MediaStreamKind.ExternalSubtitle) |
|
.ToList(); |
|
|
|
metadata.Subtitles.AddRange(subtitleStreams.Map(Subtitle.FromMediaStream)); |
|
|
|
if (response is PlexXmlMetadataResponse xml) |
|
{ |
|
metadata.Guids = Optional(xml.Guid).Flatten().Map(g => new MetadataGuid { Guid = g.Id }).ToList(); |
|
if (!string.IsNullOrWhiteSpace(xml.PlexGuid)) |
|
{ |
|
Option<string> normalized = NormalizeGuid(xml.PlexGuid); |
|
foreach (string guid in normalized) |
|
{ |
|
if (metadata.Guids.All(g => g.Guid != guid)) |
|
{ |
|
metadata.Guids.Add(new MetadataGuid { Guid = guid }); |
|
} |
|
} |
|
} |
|
|
|
PlexMediaResponse<PlexXmlPartResponse> media = xml.Media |
|
.Filter(media => media.Part.Count != 0) |
|
.MaxBy(media => media.Id); |
|
|
|
PlexXmlPartResponse part = media.Part.Head(); |
|
string folder = Path.GetDirectoryName(part.File); |
|
|
|
if (!string.IsNullOrWhiteSpace(folder)) |
|
{ |
|
IEnumerable<string> libraryPaths = library.Paths |
|
.HeadOrNone() |
|
.Map(p => p.Path) |
|
.Map(JsonConvert.DeserializeObject<LibraryPaths>) |
|
.Map(lp => lp.Paths) |
|
.Flatten(); |
|
|
|
// check each library path from plex |
|
foreach (string libraryPath in libraryPaths) |
|
{ |
|
// if the media file belongs to this library path |
|
if (folder.StartsWith(libraryPath, StringComparison.OrdinalIgnoreCase)) |
|
{ |
|
// try to get a parent directory of the library path |
|
string parent = Optional(Directory.GetParent(libraryPath)).Match( |
|
di => di.FullName, |
|
() => libraryPath); |
|
|
|
// get all folders between parent and media file |
|
string diff = Path.GetRelativePath(parent, folder); |
|
|
|
// each folder becomes a tag |
|
IEnumerable<Tag> tags = diff.Split(Path.DirectorySeparatorChar) |
|
.Map(t => new Tag { Name = t }); |
|
|
|
metadata.Tags.AddRange(tags); |
|
break; |
|
} |
|
} |
|
} |
|
} |
|
else |
|
{ |
|
metadata.Guids = new List<MetadataGuid>(); |
|
} |
|
|
|
foreach (PlexLabelResponse label in Optional(response.Label).Flatten()) |
|
{ |
|
metadata.Tags.Add( |
|
new Tag { Name = label.Tag, ExternalCollectionId = label.Id.ToString(CultureInfo.InvariantCulture) }); |
|
} |
|
|
|
if (!string.IsNullOrWhiteSpace(response.Studio)) |
|
{ |
|
metadata.Studios.Add(new Studio { Name = response.Studio }); |
|
} |
|
|
|
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.Poster, |
|
Path = path, |
|
DateAdded = dateAdded, |
|
DateUpdated = lastWriteTime |
|
}; |
|
|
|
metadata.Artwork ??= new List<Artwork>(); |
|
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); |
|
} |
|
|
|
return metadata; |
|
} |
|
|
|
private Option<string> NormalizeGuid(string guid) |
|
{ |
|
if (guid.StartsWith("plex://show", StringComparison.OrdinalIgnoreCase) || |
|
guid.StartsWith("plex://season", StringComparison.OrdinalIgnoreCase) || |
|
guid.StartsWith("plex://episode", StringComparison.OrdinalIgnoreCase) || |
|
guid.StartsWith("plex://movie", StringComparison.OrdinalIgnoreCase)) |
|
{ |
|
return guid; |
|
} |
|
|
|
if (guid.StartsWith("com.plexapp.agents.imdb", StringComparison.OrdinalIgnoreCase)) |
|
{ |
|
string strip1 = guid.Replace("com.plexapp.agents.imdb://", string.Empty); |
|
string strip2 = strip1.Split("?").Head(); |
|
return $"imdb://{strip2}"; |
|
} |
|
|
|
if (guid.StartsWith("com.plexapp.agents.thetvdb", StringComparison.OrdinalIgnoreCase)) |
|
{ |
|
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", StringComparison.OrdinalIgnoreCase)) |
|
{ |
|
string strip1 = guid.Replace("com.plexapp.agents.themoviedb://", string.Empty); |
|
string strip2 = strip1.Split("?").Head(); |
|
return $"tmdb://{strip2}"; |
|
} |
|
|
|
if (guid.StartsWith("local://", StringComparison.OrdinalIgnoreCase) || |
|
guid.StartsWith("com.plexapp.agents.none://", StringComparison.OrdinalIgnoreCase)) |
|
{ |
|
// _logger.LogDebug("Ignoring local Plex guid: {Guid}", guid); |
|
} |
|
else |
|
{ |
|
_logger.LogWarning("Unsupported guid format from Plex; ignoring: {Guid}", guid); |
|
} |
|
|
|
return None; |
|
} |
|
|
|
private sealed class LibraryPaths |
|
{ |
|
[JsonProperty("paths")] |
|
public List<string> Paths { get; set; } = []; |
|
} |
|
}
|
|
|