New: Import from Plex Watchlist
This commit is contained in:
parent
5923b4ae0d
commit
f6fbd3cfee
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"MediaContainer": {
|
||||
"librarySectionID": "watchlist",
|
||||
"librarySectionTitle": "Watchlist",
|
||||
"offset": 0,
|
||||
"totalSize": 3,
|
||||
"identifier": "tv.plex.provider.metadata",
|
||||
"size": 3,
|
||||
"Metadata": [
|
||||
{
|
||||
"type": "show",
|
||||
"title": "30 Rock",
|
||||
"year": 2006,
|
||||
"Guid": [
|
||||
{
|
||||
"id": "tvdb://79488"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "show",
|
||||
"title": "Anthony Bourdain: No Reservations",
|
||||
"year": 2005,
|
||||
"Guid": [
|
||||
{
|
||||
"id": "imdb://tt0475900"
|
||||
},
|
||||
{
|
||||
"id": "tmdb://4533"
|
||||
},
|
||||
{
|
||||
"id": "tvdb://79668"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "show",
|
||||
"title": "Series Title",
|
||||
"year": 2021,
|
||||
"Guid": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
using System.Linq;
|
||||
using System.Text;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.ImportLists;
|
||||
using NzbDrone.Core.ImportLists.Plex;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.ImportList.Plex
|
||||
{
|
||||
public class PlexTest : CoreTest<PlexParser>
|
||||
{
|
||||
private ImportListResponse CreateResponse(string url, string content)
|
||||
{
|
||||
var httpRequest = new HttpRequest(url);
|
||||
var httpResponse = new HttpResponse(httpRequest, new HttpHeader(), Encoding.UTF8.GetBytes(content));
|
||||
|
||||
return new ImportListResponse(new ImportListRequest(httpRequest), httpResponse);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_parse_plex_watchlist()
|
||||
{
|
||||
var json = ReadAllText("Files/plex_watchlist.json");
|
||||
|
||||
var result = Subject.ParseResponse(CreateResponse("https://metadata.provider.plex.tv/library/sections/watchlist/all", json));
|
||||
|
||||
result.First().Title.Should().Be("30 Rock");
|
||||
result.First().Year.Should().Be(2006);
|
||||
result.First().TvdbId.Should().Be(79488);
|
||||
|
||||
result[1].TmdbId.Should().Be(4533);
|
||||
result[1].ImdbId.Should().Be("tt0475900");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -121,6 +121,8 @@ namespace NzbDrone.Core.ImportLists
|
|||
seriesToAdd.Add(new Series
|
||||
{
|
||||
TvdbId = report.TvdbId,
|
||||
Title = report.Title,
|
||||
Year = report.Year,
|
||||
Monitored = monitored,
|
||||
RootFolderPath = importList.RootFolderPath,
|
||||
QualityProfileId = importList.QualityProfileId,
|
||||
|
|
|
@ -3,6 +3,7 @@ namespace NzbDrone.Core.ImportLists
|
|||
public enum ImportListType
|
||||
{
|
||||
Program,
|
||||
Plex,
|
||||
Trakt,
|
||||
Other
|
||||
}
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Exceptions;
|
||||
using NzbDrone.Core.Notifications.Plex.PlexTv;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.ImportLists.Plex
|
||||
{
|
||||
public class PlexImport : HttpImportListBase<PlexListSettings>
|
||||
{
|
||||
public readonly IPlexTvService _plexTvService;
|
||||
public override ImportListType ListType => ImportListType.Plex;
|
||||
|
||||
public PlexImport(IPlexTvService plexTvService,
|
||||
IHttpClient httpClient,
|
||||
IImportListStatusService importListStatusService,
|
||||
IConfigService configService,
|
||||
IParsingService parsingService,
|
||||
Logger logger)
|
||||
: base(httpClient, importListStatusService, configService, parsingService, logger)
|
||||
{
|
||||
_plexTvService = plexTvService;
|
||||
}
|
||||
|
||||
public override string Name => "Plex Watchlist";
|
||||
|
||||
public override IList<ImportListItemInfo> Fetch()
|
||||
{
|
||||
Settings.Validate().Filter("AccessToken").ThrowOnError();
|
||||
|
||||
// var generator = GetRequestGenerator();
|
||||
|
||||
return FetchItems(g =>g.GetListItems());
|
||||
}
|
||||
|
||||
public override IParseImportListResponse GetParser()
|
||||
{
|
||||
return new PlexParser();
|
||||
}
|
||||
|
||||
public override IImportListRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
return new PlexListRequestGenerator(_plexTvService)
|
||||
{
|
||||
Settings = Settings
|
||||
};
|
||||
}
|
||||
|
||||
public override object RequestAction(string action, IDictionary<string, string> query)
|
||||
{
|
||||
if (action == "startOAuth")
|
||||
{
|
||||
Settings.Validate().Filter("ConsumerKey", "ConsumerSecret").ThrowOnError();
|
||||
|
||||
return _plexTvService.GetPinUrl();
|
||||
}
|
||||
else if (action == "continueOAuth")
|
||||
{
|
||||
Settings.Validate().Filter("ConsumerKey", "ConsumerSecret").ThrowOnError();
|
||||
|
||||
if (query["callbackUrl"].IsNullOrWhiteSpace())
|
||||
{
|
||||
throw new BadRequestException("QueryParam callbackUrl invalid.");
|
||||
}
|
||||
|
||||
if (query["id"].IsNullOrWhiteSpace())
|
||||
{
|
||||
throw new BadRequestException("QueryParam id invalid.");
|
||||
}
|
||||
|
||||
if (query["code"].IsNullOrWhiteSpace())
|
||||
{
|
||||
throw new BadRequestException("QueryParam code invalid.");
|
||||
}
|
||||
|
||||
return _plexTvService.GetSignInUrl(query["callbackUrl"], Convert.ToInt32(query["id"]), query["code"]);
|
||||
}
|
||||
else if (action == "getOAuthToken")
|
||||
{
|
||||
Settings.Validate().Filter("ConsumerKey", "ConsumerSecret").ThrowOnError();
|
||||
|
||||
if (query["pinId"].IsNullOrWhiteSpace())
|
||||
{
|
||||
throw new BadRequestException("QueryParam pinId invalid.");
|
||||
}
|
||||
|
||||
var accessToken = _plexTvService.GetAuthToken(Convert.ToInt32(query["pinId"]));
|
||||
|
||||
return new
|
||||
{
|
||||
accessToken
|
||||
};
|
||||
}
|
||||
|
||||
return new { };
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.Notifications.Plex.PlexTv;
|
||||
|
||||
namespace NzbDrone.Core.ImportLists.Plex
|
||||
{
|
||||
public class PlexListRequestGenerator : IImportListRequestGenerator
|
||||
{
|
||||
private readonly IPlexTvService _plexTvService;
|
||||
public PlexListSettings Settings { get; set; }
|
||||
|
||||
public PlexListRequestGenerator(IPlexTvService plexTvService)
|
||||
{
|
||||
_plexTvService = plexTvService;
|
||||
}
|
||||
|
||||
public virtual ImportListPageableRequestChain GetListItems()
|
||||
{
|
||||
var pageableRequests = new ImportListPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetMoviesRequest());
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
private IEnumerable<ImportListRequest> GetMoviesRequest()
|
||||
{
|
||||
var request = new ImportListRequest(_plexTvService.GetWatchlist(Settings.AccessToken));
|
||||
|
||||
yield return request;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
using FluentValidation;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.ImportLists.Plex
|
||||
{
|
||||
public class PlexListSettingsValidator : AbstractValidator<PlexListSettings>
|
||||
{
|
||||
public PlexListSettingsValidator()
|
||||
{
|
||||
RuleFor(c => c.AccessToken).NotEmpty()
|
||||
.OverridePropertyName("SignIn")
|
||||
.WithMessage("Must authenticate with Plex");
|
||||
}
|
||||
}
|
||||
|
||||
public class PlexListSettings : IImportListSettings
|
||||
{
|
||||
protected virtual PlexListSettingsValidator Validator => new PlexListSettingsValidator();
|
||||
|
||||
public PlexListSettings()
|
||||
{
|
||||
SignIn = "startOAuth";
|
||||
}
|
||||
|
||||
public virtual string Scope => "";
|
||||
|
||||
public string BaseUrl { get; set; }
|
||||
|
||||
[FieldDefinition(0, Label = "Access Token", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)]
|
||||
public string AccessToken { get; set; }
|
||||
|
||||
[FieldDefinition(99, Label = "Authenticate with Plex.tv", Type = FieldType.OAuth)]
|
||||
public string SignIn { get; set; }
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.ImportLists.Exceptions;
|
||||
using NzbDrone.Core.Notifications.Plex.Server;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.ImportLists.Plex
|
||||
{
|
||||
public class PlexParser : IParseImportListResponse
|
||||
{
|
||||
private ImportListResponse _importResponse;
|
||||
|
||||
public virtual IList<ImportListItemInfo> ParseResponse(ImportListResponse importResponse)
|
||||
{
|
||||
List<PlexSectionItem> items;
|
||||
|
||||
_importResponse = importResponse;
|
||||
|
||||
var series = new List<ImportListItemInfo>();
|
||||
|
||||
if (!PreProcess(_importResponse))
|
||||
{
|
||||
return series;
|
||||
}
|
||||
|
||||
items = Json.Deserialize<PlexResponse<PlexSectionResponse>>(_importResponse.Content)
|
||||
.MediaContainer
|
||||
.Items;
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
var tvdbIdString = FindGuid(item.Guids, "tvdb");
|
||||
var tmdbIdString = FindGuid(item.Guids, "tmdb");
|
||||
var imdbId = FindGuid(item.Guids, "imdb");
|
||||
|
||||
int.TryParse(tvdbIdString, out int tvdbId);
|
||||
int.TryParse(tmdbIdString, out int tmdbId);
|
||||
|
||||
series.Add(new ImportListItemInfo
|
||||
{
|
||||
TvdbId = tvdbId,
|
||||
TmdbId = tmdbId,
|
||||
ImdbId = imdbId,
|
||||
Title = item.Title,
|
||||
Year = item.Year
|
||||
});
|
||||
}
|
||||
|
||||
return series;
|
||||
}
|
||||
|
||||
protected virtual bool PreProcess(ImportListResponse importListResponse)
|
||||
{
|
||||
if (importListResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
|
||||
{
|
||||
throw new ImportListException(importListResponse, "Plex API call resulted in an unexpected StatusCode [{0}]", importListResponse.HttpResponse.StatusCode);
|
||||
}
|
||||
|
||||
if (importListResponse.HttpResponse.Headers.ContentType != null && importListResponse.HttpResponse.Headers.ContentType.Contains("text/json") &&
|
||||
importListResponse.HttpRequest.Headers.Accept != null && !importListResponse.HttpRequest.Headers.Accept.Contains("text/json"))
|
||||
{
|
||||
throw new ImportListException(importListResponse, "Plex API responded with html content. Site is likely blocked or unavailable.");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private string FindGuid(List<PlexSectionItemGuid> guids, string prefix)
|
||||
{
|
||||
var scheme = $"{prefix}://";
|
||||
|
||||
return guids.FirstOrDefault((guid) => guid.Id.StartsWith(scheme))?.Id.Replace(scheme, "");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
namespace NzbDrone.Core.Notifications.Plex
|
||||
{
|
||||
public enum PlexMediaType
|
||||
{
|
||||
None,
|
||||
Movie,
|
||||
Show
|
||||
}
|
||||
}
|
|
@ -11,6 +11,8 @@ namespace NzbDrone.Core.Notifications.Plex.PlexTv
|
|||
PlexTvPinUrlResponse GetPinUrl();
|
||||
PlexTvSignInUrlResponse GetSignInUrl(string callbackUrl, int pinId, string pinCode);
|
||||
string GetAuthToken(int pinId);
|
||||
|
||||
HttpRequest GetWatchlist(string authToken);
|
||||
}
|
||||
|
||||
public class PlexTvService : IPlexTvService
|
||||
|
@ -80,5 +82,31 @@ namespace NzbDrone.Core.Notifications.Plex.PlexTv
|
|||
|
||||
return authToken;
|
||||
}
|
||||
|
||||
public HttpRequest GetWatchlist(string authToken)
|
||||
{
|
||||
var clientIdentifier = _configService.PlexClientIdentifier;
|
||||
|
||||
var requestBuilder = new HttpRequestBuilder("https://metadata.provider.plex.tv/library/sections/watchlist/all")
|
||||
.Accept(HttpAccept.Json)
|
||||
.AddQueryParam("clientID", clientIdentifier)
|
||||
.AddQueryParam("context[device][product]", BuildInfo.AppName)
|
||||
.AddQueryParam("context[device][platform]", "Windows")
|
||||
.AddQueryParam("context[device][platformVersion]", "7")
|
||||
.AddQueryParam("context[device][version]", BuildInfo.Version.ToString())
|
||||
.AddQueryParam("includeFields", "title,type,year,ratingKey")
|
||||
.AddQueryParam("includeElements", "Guid")
|
||||
.AddQueryParam("sort", "watchlistedAt:desc")
|
||||
.AddQueryParam("type", (int)PlexMediaType.Show);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(authToken))
|
||||
{
|
||||
requestBuilder.AddQueryParam("X-Plex-Token", authToken);
|
||||
}
|
||||
|
||||
var request = requestBuilder.Build();
|
||||
|
||||
return request;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,12 +3,27 @@ using Newtonsoft.Json;
|
|||
|
||||
namespace NzbDrone.Core.Notifications.Plex.Server
|
||||
{
|
||||
public class PlexSectionItemGuid
|
||||
{
|
||||
public string Id { get; set; }
|
||||
}
|
||||
|
||||
public class PlexSectionItem
|
||||
{
|
||||
public PlexSectionItem()
|
||||
{
|
||||
Guids = new List<PlexSectionItemGuid>();
|
||||
}
|
||||
|
||||
[JsonProperty("ratingKey")]
|
||||
public int Id { get; set; }
|
||||
public string Id { get; set; }
|
||||
|
||||
public string Title { get; set; }
|
||||
|
||||
public int Year { get; set; }
|
||||
|
||||
[JsonProperty("Guid")]
|
||||
public List<PlexSectionItemGuid> Guids { get; set; }
|
||||
}
|
||||
|
||||
public class PlexSectionResponse
|
||||
|
|
|
@ -14,10 +14,10 @@ namespace NzbDrone.Core.Notifications.Plex.Server
|
|||
{
|
||||
List<PlexSection> GetTvSections(PlexServerSettings settings);
|
||||
void Update(int sectionId, PlexServerSettings settings);
|
||||
void UpdateSeries(int metadataId, PlexServerSettings settings);
|
||||
void UpdateSeries(string metadataId, PlexServerSettings settings);
|
||||
string Version(PlexServerSettings settings);
|
||||
List<PlexPreference> Preferences(PlexServerSettings settings);
|
||||
int? GetMetadataId(int sectionId, int tvdbId, string language, PlexServerSettings settings);
|
||||
string GetMetadataId(int sectionId, int tvdbId, string language, PlexServerSettings settings);
|
||||
}
|
||||
|
||||
public class PlexServerProxy : IPlexServerProxy
|
||||
|
@ -71,7 +71,7 @@ namespace NzbDrone.Core.Notifications.Plex.Server
|
|||
CheckForError(response);
|
||||
}
|
||||
|
||||
public void UpdateSeries(int metadataId, PlexServerSettings settings)
|
||||
public void UpdateSeries(string metadataId, PlexServerSettings settings)
|
||||
{
|
||||
var resource = $"library/metadata/{metadataId}/refresh";
|
||||
var request = BuildRequest(resource, HttpMethod.PUT, settings);
|
||||
|
@ -116,7 +116,7 @@ namespace NzbDrone.Core.Notifications.Plex.Server
|
|||
.Preferences;
|
||||
}
|
||||
|
||||
public int? GetMetadataId(int sectionId, int tvdbId, string language, PlexServerSettings settings)
|
||||
public string GetMetadataId(int sectionId, int tvdbId, string language, PlexServerSettings settings)
|
||||
{
|
||||
var guid = $"com.plexapp.agents.thetvdb://{tvdbId}?lang={language}";
|
||||
var resource = $"library/sections/{sectionId}/all?guid={System.Web.HttpUtility.UrlEncode(guid)}";
|
||||
|
|
|
@ -159,10 +159,10 @@ namespace NzbDrone.Core.Notifications.Plex.Server
|
|||
{
|
||||
var metadataId = GetMetadataId(section.Id, series, section.Language, settings);
|
||||
|
||||
if (metadataId.HasValue)
|
||||
if (metadataId.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
_logger.Debug("Updating Plex host: {0}, Section: {1}, Series: {2}", settings.Host, section.Id, series);
|
||||
_plexServerProxy.UpdateSeries(metadataId.Value, settings);
|
||||
_plexServerProxy.UpdateSeries(metadataId, settings);
|
||||
|
||||
partiallyUpdated = true;
|
||||
}
|
||||
|
@ -171,7 +171,7 @@ namespace NzbDrone.Core.Notifications.Plex.Server
|
|||
return partiallyUpdated;
|
||||
}
|
||||
|
||||
private int? GetMetadataId(int sectionId, Series series, string language, PlexServerSettings settings)
|
||||
private string GetMetadataId(int sectionId, Series series, string language, PlexServerSettings settings)
|
||||
{
|
||||
_logger.Debug("Getting metadata from Plex host: {0} for series: {1}", settings.Host, series);
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace NzbDrone.Core.Parser.Model
|
||||
{
|
||||
|
@ -8,7 +7,10 @@ namespace NzbDrone.Core.Parser.Model
|
|||
public int ImportListId { get; set; }
|
||||
public string ImportList { get; set; }
|
||||
public string Title { get; set; }
|
||||
public int Year { get; set; }
|
||||
public int TvdbId { get; set; }
|
||||
public int TmdbId { get; set; }
|
||||
public string ImdbId { get; set; }
|
||||
public DateTime ReleaseDate { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
|
|
|
@ -6,6 +6,7 @@ using FluentValidation;
|
|||
using FluentValidation.Results;
|
||||
using NLog;
|
||||
using NzbDrone.Common.EnsureThat;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Exceptions;
|
||||
using NzbDrone.Core.MetadataSource;
|
||||
using NzbDrone.Core.Organizer;
|
||||
|
@ -61,8 +62,15 @@ namespace NzbDrone.Core.Tv
|
|||
var existingSeries = _seriesService.GetAllSeries();
|
||||
|
||||
foreach (var s in newSeries)
|
||||
{
|
||||
if (s.Path.IsNullOrWhiteSpace())
|
||||
{
|
||||
_logger.Info("Adding Series {0} Root Folder Path: [{1}]", s, s.RootFolderPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Info("Adding Series {0} Path: [{1}]", s, s.Path);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue