Merge pull request #563 from Sonarr/plex-partial-updates

Support for updating single series in Plex Library
pull/2/head
Mark McDowall 10 years ago
commit 49718fbfbe

@ -9,7 +9,7 @@ namespace NzbDrone.Core.Test.NotificationTests
{
[TestFixture]
public class PlexProviderTest : CoreTest
public class PlexClientServiceTest : CoreTest
{
private PlexClientSettings _clientSettings;
@ -43,7 +43,7 @@ namespace NzbDrone.Core.Test.NotificationTests
.Returns("ok");
Mocker.Resolve<PlexService>().Notify(_clientSettings, header, message);
Mocker.Resolve<PlexClientService>().Notify(_clientSettings, header, message);
fakeHttp.Verify(v => v.DownloadString(expectedUrl), Times.Once());
@ -63,8 +63,8 @@ namespace NzbDrone.Core.Test.NotificationTests
fakeHttp.Setup(s => s.DownloadString(expectedUrl, "plex", "plex"))
.Returns("ok");
Mocker.Resolve<PlexService>().Notify(_clientSettings, header, message);
Mocker.Resolve<PlexClientService>().Notify(_clientSettings, header, message);
fakeHttp.Verify(v => v.DownloadString(expectedUrl, "plex", "plex"), Times.Once());

@ -274,7 +274,7 @@
<Compile Include="Messaging\Events\EventAggregatorFixture.cs" />
<Compile Include="Metadata\Consumers\Roksbox\FindMetadataFileFixture.cs" />
<Compile Include="Metadata\Consumers\Wdtv\FindMetadataFileFixture.cs" />
<Compile Include="NotificationTests\PlexProviderTest.cs" />
<Compile Include="NotificationTests\PlexClientServiceTest.cs" />
<Compile Include="NotificationTests\ProwlProviderTest.cs" />
<Compile Include="NotificationTests\Xbmc\Http\ActivePlayersFixture.cs" />
<Compile Include="NotificationTests\Xbmc\Http\CheckForErrorFixture.cs" />

@ -0,0 +1,8 @@
namespace NzbDrone.Core.Notifications.Plex.Models
{
public class PlexIdentity
{
public string MachineIdentifier { get; set; }
public string Version { get; set; }
}
}

@ -0,0 +1,18 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace NzbDrone.Core.Notifications.Plex.Models
{
public class PlexPreferences
{
[JsonProperty("_children")]
public List<PlexPreference> Preferences { get; set; }
}
public class PlexPreference
{
public string Id { get; set; }
public string Type { get; set; }
public string Value { get; set; }
}
}

@ -0,0 +1,30 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace NzbDrone.Core.Notifications.Plex.Models
{
public class PlexSectionDetails
{
public int Id { get; set; }
public string Path { get; set; }
public string Language { get; set; }
}
public class PlexSection
{
[JsonProperty("key")]
public int Id { get; set; }
public string Type { get; set; }
public string Language { get; set; }
[JsonProperty("_children")]
public List<PlexSectionDetails> Sections { get; set; }
}
public class PlexMediaContainer
{
[JsonProperty("_children")]
public List<PlexSection> Directories { get; set; }
}
}

@ -0,0 +1,19 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace NzbDrone.Core.Notifications.Plex.Models
{
public class PlexSectionItem
{
[JsonProperty("ratingKey")]
public int Id { get; set; }
public string Title { get; set; }
}
public class PlexSectionResponse
{
[JsonProperty("_children")]
public List<PlexSectionItem> Items { get; set; }
}
}

@ -7,11 +7,11 @@ namespace NzbDrone.Core.Notifications.Plex
{
public class PlexClient : NotificationBase<PlexClientSettings>
{
private readonly IPlexService _plexService;
private readonly IPlexClientService _plexClientService;
public PlexClient(IPlexService plexService)
public PlexClient(IPlexClientService plexClientService)
{
_plexService = plexService;
_plexClientService = plexClientService;
}
public override string Link
@ -22,13 +22,13 @@ namespace NzbDrone.Core.Notifications.Plex
public override void OnGrab(string message)
{
const string header = "Sonarr [TV] - Grabbed";
_plexService.Notify(Settings, header, message);
_plexClientService.Notify(Settings, header, message);
}
public override void OnDownload(DownloadMessage message)
{
const string header = "Sonarr [TV] - Downloaded";
_plexService.Notify(Settings, header, message.Message);
_plexClientService.Notify(Settings, header, message.Message);
}
public override void AfterRename(Series series)
@ -47,7 +47,7 @@ namespace NzbDrone.Core.Notifications.Plex
{
var failures = new List<ValidationFailure>();
failures.AddIfNotNull(_plexService.Test(Settings));
failures.AddIfNotNull(_plexClientService.Test(Settings));
return new ValidationResult(failures);
}

@ -5,27 +5,24 @@ using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Notifications.Plex.Models;
namespace NzbDrone.Core.Notifications.Plex
{
public interface IPlexService
public interface IPlexClientService
{
void Notify(PlexClientSettings settings, string header, string message);
void UpdateLibrary(PlexServerSettings settings);
ValidationFailure Test(PlexClientSettings settings);
ValidationFailure Test(PlexServerSettings settings);
}
public class PlexService : IPlexService
public class PlexClientService : IPlexClientService
{
private readonly IHttpProvider _httpProvider;
private readonly IPlexServerProxy _plexServerProxy;
private readonly Logger _logger;
public PlexService(IHttpProvider httpProvider, IPlexServerProxy plexServerProxy, Logger logger)
public PlexClientService(IHttpProvider httpProvider, Logger logger)
{
_httpProvider = httpProvider;
_plexServerProxy = plexServerProxy;
_logger = logger;
}
@ -42,36 +39,6 @@ namespace NzbDrone.Core.Notifications.Plex
}
}
public void UpdateLibrary(PlexServerSettings settings)
{
try
{
_logger.Debug("Sending Update Request to Plex Server");
var sections = GetSectionKeys(settings);
sections.ForEach(s => UpdateSection(s, settings));
}
catch(Exception ex)
{
_logger.WarnException("Failed to Update Plex host: " + settings.Host, ex);
throw;
}
}
private List<int> GetSectionKeys(PlexServerSettings settings)
{
_logger.Debug("Getting sections from Plex host: {0}", settings.Host);
return _plexServerProxy.GetTvSections(settings).Select(s => s.Key).ToList();
}
private void UpdateSection(int key, PlexServerSettings settings)
{
_logger.Debug("Updating Plex host: {0}, Section: {1}", settings.Host, key);
_plexServerProxy.Update(key, settings);
}
private string SendCommand(string host, int port, string command, string username, string password)
{
var url = String.Format("http://{0}:{1}/xbmcCmds/xbmcHttp?command={2}", host, port, command);
@ -106,31 +73,5 @@ namespace NzbDrone.Core.Notifications.Plex
return null;
}
public ValidationFailure Test(PlexServerSettings settings)
{
try
{
var sections = GetSectionKeys(new PlexServerSettings
{
Host = settings.Host,
Port = settings.Port,
Username = settings.Username,
Password = settings.Password
});
if (sections.Empty())
{
return new ValidationFailure("Host", "At least one TV library is required");
}
}
catch (Exception ex)
{
_logger.ErrorException("Unable to connect to Plex Server: " + ex.Message, ex);
return new ValidationFailure("Host", "Unable to connect to Plex Server");
}
return null;
}
}
}

@ -1,28 +0,0 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace NzbDrone.Core.Notifications.Plex
{
public class PlexSection
{
public Int32 Id { get; set; }
public String Path { get; set; }
}
public class PlexDirectory
{
public String Type { get; set; }
[JsonProperty("_children")]
public List<PlexSection> Sections { get; set; }
public Int32 Key { get; set; }
}
public class PlexMediaContainer
{
[JsonProperty("_children")]
public List<PlexDirectory> Directories { get; set; }
}
}

@ -7,11 +7,11 @@ namespace NzbDrone.Core.Notifications.Plex
{
public class PlexServer : NotificationBase<PlexServerSettings>
{
private readonly IPlexService _plexService;
private readonly IPlexServerService _plexServerService;
public PlexServer(IPlexService plexService)
public PlexServer(IPlexServerService plexServerService)
{
_plexService = plexService;
_plexServerService = plexServerService;
}
public override string Link
@ -25,19 +25,19 @@ namespace NzbDrone.Core.Notifications.Plex
public override void OnDownload(DownloadMessage message)
{
UpdateIfEnabled();
UpdateIfEnabled(message.Series);
}
public override void AfterRename(Series series)
{
UpdateIfEnabled();
UpdateIfEnabled(series);
}
private void UpdateIfEnabled()
private void UpdateIfEnabled(Series series)
{
if (Settings.UpdateLibrary)
{
_plexService.UpdateLibrary(Settings);
_plexServerService.UpdateLibrary(series, Settings);
}
}
@ -53,7 +53,7 @@ namespace NzbDrone.Core.Notifications.Plex
{
var failures = new List<ValidationFailure>();
failures.AddIfNotNull(_plexService.Test(Settings));
failures.AddIfNotNull(_plexServerService.Test(Settings));
return new ValidationResult(failures);
}

@ -1,12 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using Newtonsoft.Json.Linq;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Notifications.Plex.Models;
using NzbDrone.Core.Rest;
using RestSharp;
@ -14,8 +16,12 @@ namespace NzbDrone.Core.Notifications.Plex
{
public interface IPlexServerProxy
{
List<PlexDirectory> GetTvSections(PlexServerSettings settings);
List<PlexSection> GetTvSections(PlexServerSettings settings);
void Update(int sectionId, PlexServerSettings settings);
void UpdateSeries(int metadataId, PlexServerSettings settings);
string Version(PlexServerSettings settings);
List<PlexPreference> Preferences(PlexServerSettings settings);
int? GetMetadataId(int sectionId, int tvdbId, string language, PlexServerSettings settings);
}
public class PlexServerProxy : IPlexServerProxy
@ -29,16 +35,14 @@ namespace NzbDrone.Core.Notifications.Plex
_logger = logger;
}
public List<PlexDirectory> GetTvSections(PlexServerSettings settings)
public List<PlexSection> GetTvSections(PlexServerSettings settings)
{
var request = GetPlexServerRequest("library/sections", Method.GET, settings);
var client = GetPlexServerClient(settings);
var response = client.Execute(request);
_logger.Trace("Sections response: {0}", response.Content);
CheckForError(response.Content);
CheckForError(response);
return Json.Deserialize<PlexMediaContainer>(response.Content)
.Directories
@ -51,11 +55,68 @@ namespace NzbDrone.Core.Notifications.Plex
var resource = String.Format("library/sections/{0}/refresh", sectionId);
var request = GetPlexServerRequest(resource, Method.GET, settings);
var client = GetPlexServerClient(settings);
var response = client.Execute(request);
_logger.Trace("Update response: {0}", response.Content);
CheckForError(response);
}
public void UpdateSeries(int metadataId, PlexServerSettings settings)
{
var resource = String.Format("library/metadata/{0}/refresh", metadataId);
var request = GetPlexServerRequest(resource, Method.PUT, settings);
var client = GetPlexServerClient(settings);
var response = client.Execute(request);
_logger.Trace("Update Series response: {0}", response.Content);
CheckForError(response);
}
public string Version(PlexServerSettings settings)
{
var request = GetPlexServerRequest("identity", Method.GET, settings);
var client = GetPlexServerClient(settings);
var response = client.Execute(request);
CheckForError(response.Content);
_logger.Debug("Update response: {0}", response.Content);
_logger.Trace("Version response: {0}", response.Content);
CheckForError(response);
return Json.Deserialize<PlexIdentity>(response.Content).Version;
}
public List<PlexPreference> Preferences(PlexServerSettings settings)
{
var request = GetPlexServerRequest(":/prefs", Method.GET, settings);
var client = GetPlexServerClient(settings);
var response = client.Execute(request);
_logger.Trace("Preferences response: {0}", response.Content);
CheckForError(response);
return Json.Deserialize<PlexPreferences>(response.Content).Preferences;
}
public int? GetMetadataId(int sectionId, int tvdbId, string language, PlexServerSettings settings)
{
var guid = String.Format("com.plexapp.agents.thetvdb://{0}?lang={1}", tvdbId, language);
var resource = String.Format("library/sections/{0}/all?guid={1}", sectionId, System.Web.HttpUtility.UrlEncode(guid));
var request = GetPlexServerRequest(resource, Method.GET, settings);
var client = GetPlexServerClient(settings);
var response = client.Execute(request);
_logger.Trace("Sections response: {0}", response.Content);
CheckForError(response);
var item = Json.Deserialize<PlexSectionResponse>(response.Content)
.Items
.FirstOrDefault();
if (item == null)
{
return null;
}
return item.Id;
}
private String Authenticate(string username, string password)
@ -65,8 +126,8 @@ namespace NzbDrone.Core.Notifications.Plex
var response = client.Execute(request);
CheckForError(response.Content);
_logger.Debug("Authentication Response: {0}", response.Content);
CheckForError(response);
var user = Json.Deserialize<PlexUser>(JObject.Parse(response.Content).SelectToken("user").ToString());
@ -109,7 +170,7 @@ namespace NzbDrone.Core.Notifications.Plex
if (!settings.Username.IsNullOrWhiteSpace())
{
request.AddParameter("X-Plex-Token", GetAuthenticationToken(settings.Username, settings.Password));
request.AddParameter("X-Plex-Token", GetAuthenticationToken(settings.Username, settings.Password), ParameterType.HttpHeader);
}
return request;
@ -120,14 +181,23 @@ namespace NzbDrone.Core.Notifications.Plex
return _authCache.Get(username, () => Authenticate(username, password));
}
private void CheckForError(string response)
private void CheckForError(IRestResponse response)
{
var error = Json.Deserialize<PlexError>(response);
_logger.Trace("Checking for error");
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
throw new PlexException("Unauthorized");
}
var error = Json.Deserialize<PlexError>(response.Content);
if (error != null && !error.Error.IsNullOrWhiteSpace())
{
throw new PlexException(error.Error);
}
_logger.Trace("No error detected");
}
}
}

@ -0,0 +1,169 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Notifications.Plex.Models;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Notifications.Plex
{
public interface IPlexServerService
{
void UpdateLibrary(Series series, PlexServerSettings settings);
ValidationFailure Test(PlexServerSettings settings);
}
public class PlexServerService : IPlexServerService
{
private readonly ICached<bool> _partialUpdateCache;
private readonly IPlexServerProxy _plexServerProxy;
private readonly Logger _logger;
public PlexServerService(ICacheManager cacheManager, IPlexServerProxy plexServerProxy, Logger logger)
{
_partialUpdateCache = cacheManager.GetCache<bool>(GetType(), "partialUpdateCache");
_plexServerProxy = plexServerProxy;
_logger = logger;
}
public void UpdateLibrary(Series series, PlexServerSettings settings)
{
try
{
_logger.Debug("Sending Update Request to Plex Server");
var sections = GetSections(settings);
//TODO: How long should we cache this for?
var partialUpdates = _partialUpdateCache.Get(settings.Host, () => PartialUpdatesAllowed(settings), TimeSpan.FromHours(2));
if (partialUpdates)
{
sections.ForEach(s => UpdateSeries(s.Id, series, s.Language, settings));
}
else
{
sections.ForEach(s => UpdateSection(s.Id, settings));
}
}
catch(Exception ex)
{
_logger.WarnException("Failed to Update Plex host: " + settings.Host, ex);
throw;
}
}
private List<PlexSection> GetSections(PlexServerSettings settings)
{
_logger.Debug("Getting sections from Plex host: {0}", settings.Host);
return _plexServerProxy.GetTvSections(settings).ToList();
}
private bool PartialUpdatesAllowed(PlexServerSettings settings)
{
try
{
var rawVersion = GetVersion(settings);
var version = new Version(Regex.Match(rawVersion, @"^(\d+\.){4}").Value.Trim('.'));
if (version >= new Version(0, 9, 12, 0))
{
var preferences = GetPreferences(settings);
var partialScanPreference = preferences.SingleOrDefault(p => p.Id.Equals("FSEventLibraryPartialScanEnabled"));
if (partialScanPreference == null)
{
return false;
}
return Convert.ToBoolean(partialScanPreference.Value);
}
}
catch (Exception ex)
{
_logger.WarnException("Unable to check if partial updates are allowed", ex);
}
return false;
}
private string GetVersion(PlexServerSettings settings)
{
_logger.Debug("Getting version from Plex host: {0}", settings.Host);
return _plexServerProxy.Version(settings);
}
private List<PlexPreference> GetPreferences(PlexServerSettings settings)
{
_logger.Debug("Getting preferences from Plex host: {0}", settings.Host);
return _plexServerProxy.Preferences(settings);
}
private void UpdateSection(int sectionId, PlexServerSettings settings)
{
_logger.Debug("Updating Plex host: {0}, Section: {1}", settings.Host, sectionId);
_plexServerProxy.Update(sectionId, settings);
}
private void UpdateSeries(int sectionId, Series series, string language, PlexServerSettings settings)
{
_logger.Debug("Updating Plex host: {0}, Section: {1}, Series: {2}", settings.Host, sectionId, series);
var metadataId = GetMetadataId(sectionId, series, language, settings);
if (metadataId.HasValue)
{
_plexServerProxy.UpdateSeries(metadataId.Value, settings);
}
else
{
UpdateSection(sectionId, settings);
}
}
private int? GetMetadataId(int sectionId, Series series, string language, PlexServerSettings settings)
{
_logger.Debug("Getting metadata from Plex host: {0} for series: {1}", settings.Host, series);
return _plexServerProxy.GetMetadataId(sectionId, series.TvdbId, language, settings);
}
public ValidationFailure Test(PlexServerSettings settings)
{
try
{
var sections = GetSections(new PlexServerSettings
{
Host = settings.Host,
Port = settings.Port,
Username = settings.Username,
Password = settings.Password
});
if (sections.Empty())
{
return new ValidationFailure("Host", "At least one TV library is required");
}
}
catch (Exception ex)
{
_logger.ErrorException("Unable to connect to Plex Server: " + ex.Message, ex);
return new ValidationFailure("Host", "Unable to connect to Plex Server");
}
return null;
}
}
}

@ -30,6 +30,7 @@ namespace NzbDrone.Core.Notifications.Plex
[FieldDefinition(1, Label = "Port")]
public Int32 Port { get; set; }
//TODO: Change username and password to token and get a plex.tv OAuth token properly
[FieldDefinition(2, Label = "Username")]
public String Username { get; set; }

@ -701,8 +701,13 @@
<Compile Include="Metadata\MetadataType.cs" />
<Compile Include="MetadataSource\IProvideSeriesInfo.cs" />
<Compile Include="MetadataSource\ISearchForNewSeries.cs" />
<Compile Include="Notifications\Plex\Models\PlexIdentity.cs" />
<Compile Include="Notifications\Plex\Models\PlexPreferences.cs" />
<Compile Include="Notifications\Plex\Models\PlexSectionItem.cs" />
<Compile Include="Notifications\Plex\Models\PlexSection.cs" />
<Compile Include="Notifications\Plex\PlexHomeTheater.cs" />
<Compile Include="Notifications\Plex\PlexHomeTheaterSettings.cs" />
<Compile Include="Notifications\Plex\PlexClientService.cs" />
<Compile Include="Notifications\Synology\SynologyException.cs" />
<Compile Include="Notifications\Synology\SynologyIndexer.cs" />
<Compile Include="Notifications\Synology\SynologyIndexerProxy.cs" />
@ -754,11 +759,10 @@
<Compile Include="Notifications\Plex\PlexClientSettings.cs" />
<Compile Include="Notifications\Plex\PlexError.cs" />
<Compile Include="Notifications\Plex\PlexException.cs" />
<Compile Include="Notifications\Plex\PlexSection.cs" />
<Compile Include="Notifications\Plex\PlexServer.cs" />
<Compile Include="Notifications\Plex\PlexServerProxy.cs" />
<Compile Include="Notifications\Plex\PlexServerSettings.cs" />
<Compile Include="Notifications\Plex\PlexService.cs" />
<Compile Include="Notifications\Plex\PlexServerService.cs" />
<Compile Include="Notifications\Plex\PlexUser.cs" />
<Compile Include="Notifications\Prowl\InvalidApiKeyException.cs" />
<Compile Include="Notifications\Prowl\Prowl.cs">

Loading…
Cancel
Save