New: Readarr 0.1

This commit is contained in:
ta264
2020-05-06 21:14:11 +01:00
parent 476f2d6047
commit 08496c82af
911 changed files with 14837 additions and 24442 deletions
@@ -7,11 +7,10 @@ namespace NzbDrone.Core.Notifications
public class AlbumDownloadMessage
{
public string Message { get; set; }
public Artist Artist { get; set; }
public Album Album { get; set; }
public AlbumRelease Release { get; set; }
public List<TrackFile> TrackFiles { get; set; }
public List<TrackFile> OldFiles { get; set; }
public Author Artist { get; set; }
public Book Album { get; set; }
public List<BookFile> TrackFiles { get; set; }
public List<BookFile> OldFiles { get; set; }
public string DownloadClient { get; set; }
public string DownloadId { get; set; }
@@ -42,7 +42,7 @@ namespace NzbDrone.Core.Notifications.CustomScript
environmentVariables.Add("Readarr_EventType", "Grab");
environmentVariables.Add("Readarr_Artist_Id", artist.Id.ToString());
environmentVariables.Add("Readarr_Artist_Name", artist.Metadata.Value.Name);
environmentVariables.Add("Readarr_Artist_MBId", artist.Metadata.Value.ForeignArtistId);
environmentVariables.Add("Readarr_Artist_MBId", artist.Metadata.Value.ForeignAuthorId);
environmentVariables.Add("Readarr_Artist_Type", artist.Metadata.Value.Type);
environmentVariables.Add("Readarr_Release_AlbumCount", remoteAlbum.Albums.Count.ToString());
environmentVariables.Add("Readarr_Release_AlbumReleaseDates", string.Join(",", remoteAlbum.Albums.Select(e => e.ReleaseDate)));
@@ -63,19 +63,17 @@ namespace NzbDrone.Core.Notifications.CustomScript
{
var artist = message.Artist;
var album = message.Album;
var release = message.Release;
var environmentVariables = new StringDictionary();
environmentVariables.Add("Readarr_EventType", "AlbumDownload");
environmentVariables.Add("Readarr_Artist_Id", artist.Id.ToString());
environmentVariables.Add("Readarr_Artist_Name", artist.Metadata.Value.Name);
environmentVariables.Add("Readarr_Artist_Path", artist.Path);
environmentVariables.Add("Readarr_Artist_MBId", artist.Metadata.Value.ForeignArtistId);
environmentVariables.Add("Readarr_Artist_MBId", artist.Metadata.Value.ForeignAuthorId);
environmentVariables.Add("Readarr_Artist_Type", artist.Metadata.Value.Type);
environmentVariables.Add("Readarr_Album_Id", album.Id.ToString());
environmentVariables.Add("Readarr_Album_Title", album.Title);
environmentVariables.Add("Readarr_Album_MBId", album.ForeignAlbumId);
environmentVariables.Add("Readarr_AlbumRelease_MBId", release.ForeignReleaseId);
environmentVariables.Add("Readarr_Album_MBId", album.ForeignBookId);
environmentVariables.Add("Readarr_Album_ReleaseDate", album.ReleaseDate.ToString());
environmentVariables.Add("Readarr_Download_Client", message.DownloadClient ?? string.Empty);
environmentVariables.Add("Readarr_Download_Id", message.DownloadId ?? string.Empty);
@@ -93,7 +91,7 @@ namespace NzbDrone.Core.Notifications.CustomScript
ExecuteScript(environmentVariables);
}
public override void OnRename(Artist artist)
public override void OnRename(Author artist)
{
var environmentVariables = new StringDictionary();
@@ -101,7 +99,7 @@ namespace NzbDrone.Core.Notifications.CustomScript
environmentVariables.Add("Readarr_Artist_Id", artist.Id.ToString());
environmentVariables.Add("Readarr_Artist_Name", artist.Metadata.Value.Name);
environmentVariables.Add("Readarr_Artist_Path", artist.Path);
environmentVariables.Add("Readarr_Artist_MBId", artist.Metadata.Value.ForeignArtistId);
environmentVariables.Add("Readarr_Artist_MBId", artist.Metadata.Value.ForeignAuthorId);
environmentVariables.Add("Readarr_Artist_Type", artist.Metadata.Value.Type);
ExecuteScript(environmentVariables);
@@ -111,7 +109,6 @@ namespace NzbDrone.Core.Notifications.CustomScript
{
var artist = message.Artist;
var album = message.Album;
var release = message.Release;
var trackFile = message.TrackFile;
var environmentVariables = new StringDictionary();
@@ -119,18 +116,14 @@ namespace NzbDrone.Core.Notifications.CustomScript
environmentVariables.Add("Readarr_Artist_Id", artist.Id.ToString());
environmentVariables.Add("Readarr_Artist_Name", artist.Metadata.Value.Name);
environmentVariables.Add("Readarr_Artist_Path", artist.Path);
environmentVariables.Add("Readarr_Artist_MBId", artist.Metadata.Value.ForeignArtistId);
environmentVariables.Add("Readarr_Artist_MBId", artist.Metadata.Value.ForeignAuthorId);
environmentVariables.Add("Readarr_Artist_Type", artist.Metadata.Value.Type);
environmentVariables.Add("Readarr_Album_Id", album.Id.ToString());
environmentVariables.Add("Readarr_Album_Title", album.Title);
environmentVariables.Add("Readarr_Album_MBId", album.ForeignAlbumId);
environmentVariables.Add("Readarr_AlbumRelease_MBId", release.ForeignReleaseId);
environmentVariables.Add("Readarr_Album_MBId", album.ForeignBookId);
environmentVariables.Add("Readarr_Album_ReleaseDate", album.ReleaseDate.ToString());
environmentVariables.Add("Readarr_TrackFile_Id", trackFile.Id.ToString());
environmentVariables.Add("Readarr_TrackFile_TrackCount", trackFile.Tracks.Value.Count.ToString());
environmentVariables.Add("Readarr_TrackFile_Path", trackFile.Path);
environmentVariables.Add("Readarr_TrackFile_TrackNumbers", string.Join(",", trackFile.Tracks.Value.Select(e => e.TrackNumber)));
environmentVariables.Add("Readarr_TrackFile_TrackTitles", string.Join("|", trackFile.Tracks.Value.Select(e => e.Title)));
environmentVariables.Add("Readarr_TrackFile_Quality", trackFile.Quality.Quality.Name);
environmentVariables.Add("Readarr_TrackFile_QualityVersion", trackFile.Quality.Revision.Version.ToString());
environmentVariables.Add("Readarr_TrackFile_ReleaseGroup", trackFile.ReleaseGroup ?? string.Empty);
@@ -54,7 +54,7 @@ namespace NzbDrone.Core.Notifications.Discord
_proxy.SendPayload(payload, Settings);
}
public override void OnRename(Artist artist)
public override void OnRename(Author artist)
{
var attachments = new List<Embed>
{
@@ -7,7 +7,7 @@ namespace NzbDrone.Core.Notifications
public class GrabMessage
{
public string Message { get; set; }
public Artist Artist { get; set; }
public Author Artist { get; set; }
public RemoteAlbum Album { get; set; }
public QualityModel Quality { get; set; }
public string DownloadClient { get; set; }
@@ -9,7 +9,7 @@ namespace NzbDrone.Core.Notifications
void OnGrab(GrabMessage grabMessage);
void OnReleaseImport(AlbumDownloadMessage message);
void OnRename(Artist artist);
void OnRename(Author artist);
void OnHealthIssue(HealthCheck.HealthCheck healthCheck);
void OnDownloadFailure(DownloadFailedMessage message);
void OnImportFailure(AlbumDownloadMessage message);
@@ -1,74 +0,0 @@
using System.Collections.Generic;
using FluentValidation.Results;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Music;
namespace NzbDrone.Core.Notifications.Emby
{
public class MediaBrowser : NotificationBase<MediaBrowserSettings>
{
private readonly IMediaBrowserService _mediaBrowserService;
public MediaBrowser(IMediaBrowserService mediaBrowserService)
{
_mediaBrowserService = mediaBrowserService;
}
public override string Link => "https://emby.media/";
public override string Name => "Emby (Media Browser)";
public override void OnGrab(GrabMessage grabMessage)
{
if (Settings.Notify)
{
_mediaBrowserService.Notify(Settings, ALBUM_GRABBED_TITLE_BRANDED, grabMessage.Message);
}
}
public override void OnReleaseImport(AlbumDownloadMessage message)
{
if (Settings.Notify)
{
_mediaBrowserService.Notify(Settings, ALBUM_DOWNLOADED_TITLE_BRANDED, message.Message);
}
if (Settings.UpdateLibrary)
{
_mediaBrowserService.Update(Settings, message.Artist);
}
}
public override void OnRename(Artist artist)
{
if (Settings.UpdateLibrary)
{
_mediaBrowserService.Update(Settings, artist);
}
}
public override void OnHealthIssue(HealthCheck.HealthCheck message)
{
if (Settings.Notify)
{
_mediaBrowserService.Notify(Settings, HEALTH_ISSUE_TITLE_BRANDED, message.Message);
}
}
public override void OnTrackRetag(TrackRetagMessage message)
{
if (Settings.Notify)
{
_mediaBrowserService.Notify(Settings, TRACK_RETAGGED_TITLE_BRANDED, message.Message);
}
}
public override ValidationResult Test()
{
var failures = new List<ValidationFailure>();
failures.AddIfNotNull(_mediaBrowserService.Test(Settings));
return new ValidationResult(failures);
}
}
}
@@ -1,115 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using NLog;
using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Notifications.MediaBrowser.Model;
namespace NzbDrone.Core.Notifications.Emby
{
public class MediaBrowserProxy
{
private readonly IHttpClient _httpClient;
private readonly Logger _logger;
public MediaBrowserProxy(IHttpClient httpClient, Logger logger)
{
_httpClient = httpClient;
_logger = logger;
}
public void Notify(MediaBrowserSettings settings, string title, string message)
{
var path = "/Notifications/Admin";
var request = BuildRequest(path, settings);
request.Headers.ContentType = "application/json";
request.Method = HttpMethod.POST;
request.SetContent(new
{
Name = title,
Description = message,
ImageUrl = "https://raw.github.com/readarr/Readarr/develop/Logo/64.png"
}.ToJson());
ProcessRequest(request, settings);
}
public void Update(MediaBrowserSettings settings, List<string> musicCollectionPaths)
{
string path;
HttpRequest request;
if (musicCollectionPaths.Any())
{
path = "/Library/Media/Updated";
request = BuildRequest(path, settings);
request.Headers.ContentType = "application/json";
var updateInfo = new List<EmbyMediaUpdateInfo>();
foreach (var colPath in musicCollectionPaths)
{
updateInfo.Add(new EmbyMediaUpdateInfo
{
Path = colPath,
UpdateType = "Created"
});
}
request.SetContent(new
{
Updates = updateInfo
}.ToJson());
}
else
{
path = "/Library/Refresh";
request = BuildRequest(path, settings);
}
request.Method = HttpMethod.POST;
ProcessRequest(request, settings);
}
private string ProcessRequest(HttpRequest request, MediaBrowserSettings settings)
{
request.Headers.Add("X-MediaBrowser-Token", settings.ApiKey);
var response = _httpClient.Execute(request);
_logger.Trace("Response: {0}", response.Content);
CheckForError(response);
return response.Content;
}
private HttpRequest BuildRequest(string path, MediaBrowserSettings settings)
{
var scheme = settings.UseSsl ? "https" : "http";
var url = $@"{scheme}://{settings.Address}/mediabrowser";
return new HttpRequestBuilder(url).Resource(path).Build();
}
private void CheckForError(HttpResponse response)
{
_logger.Debug("Looking for error in response: {0}", response);
//TODO: actually check for the error
}
public List<EmbyMediaFolder> GetArtist(MediaBrowserSettings settings)
{
var path = "/Library/MediaFolders";
var request = BuildRequest(path, settings);
request.Method = HttpMethod.GET;
var response = ProcessRequest(request, settings);
return Json.Deserialize<EmbyMediaFoldersResponse>(response).Items;
}
}
}
@@ -1,67 +0,0 @@
using System;
using System.Linq;
using System.Net;
using FluentValidation.Results;
using NLog;
using NzbDrone.Core.Music;
using NzbDrone.Core.Rest;
namespace NzbDrone.Core.Notifications.Emby
{
public interface IMediaBrowserService
{
void Notify(MediaBrowserSettings settings, string title, string message);
void Update(MediaBrowserSettings settings, Artist artist);
ValidationFailure Test(MediaBrowserSettings settings);
}
public class MediaBrowserService : IMediaBrowserService
{
private readonly MediaBrowserProxy _proxy;
private readonly Logger _logger;
public MediaBrowserService(MediaBrowserProxy proxy, Logger logger)
{
_proxy = proxy;
_logger = logger;
}
public void Notify(MediaBrowserSettings settings, string title, string message)
{
_proxy.Notify(settings, title, message);
}
public void Update(MediaBrowserSettings settings, Artist artist)
{
var folders = _proxy.GetArtist(settings);
var musicPaths = folders.Select(e => e.CollectionType = "music").ToList();
_proxy.Update(settings, musicPaths);
}
public ValidationFailure Test(MediaBrowserSettings settings)
{
try
{
_logger.Debug("Testing connection to MediaBrowser: {0}", settings.Address);
Notify(settings, "Test from Readarr", "Success! MediaBrowser has been successfully configured!");
}
catch (RestException ex)
{
if (ex.Response.StatusCode == HttpStatusCode.Unauthorized)
{
return new ValidationFailure("ApiKey", "API Key is incorrect");
}
}
catch (Exception ex)
{
_logger.Error(ex, "Unable to send test message");
return new ValidationFailure("Host", "Unable to send test message: " + ex.Message);
}
return null;
}
}
}
@@ -1,55 +0,0 @@
using FluentValidation;
using Newtonsoft.Json;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Notifications.Emby
{
public class MediaBrowserSettingsValidator : AbstractValidator<MediaBrowserSettings>
{
public MediaBrowserSettingsValidator()
{
RuleFor(c => c.Host).ValidHost();
RuleFor(c => c.ApiKey).NotEmpty();
}
}
public class MediaBrowserSettings : IProviderConfig
{
private static readonly MediaBrowserSettingsValidator Validator = new MediaBrowserSettingsValidator();
public MediaBrowserSettings()
{
Port = 8096;
}
[FieldDefinition(0, Label = "Host")]
public string Host { get; set; }
[FieldDefinition(1, Label = "Port")]
public int Port { get; set; }
[FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Connect to Emby over HTTPS instead of HTTP")]
public bool UseSsl { get; set; }
[FieldDefinition(3, Label = "API Key")]
public string ApiKey { get; set; }
[FieldDefinition(4, Label = "Send Notifications", HelpText = "Have MediaBrowser send notfications to configured providers", Type = FieldType.Checkbox)]
public bool Notify { get; set; }
[FieldDefinition(5, Label = "Update Library", HelpText = "Update Library on Download & Rename?", Type = FieldType.Checkbox)]
public bool UpdateLibrary { get; set; }
[JsonIgnore]
public string Address => $"{Host}:{Port}";
public bool IsValid => !string.IsNullOrWhiteSpace(Host) && Port > 0;
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}
@@ -1,8 +0,0 @@
namespace NzbDrone.Core.Notifications.MediaBrowser.Model
{
public class EmbyMediaFolder
{
public string Path { get; set; }
public string CollectionType { get; set; }
}
}
@@ -1,9 +0,0 @@
using System.Collections.Generic;
namespace NzbDrone.Core.Notifications.MediaBrowser.Model
{
public class EmbyMediaFoldersResponse
{
public List<EmbyMediaFolder> Items { get; set; }
}
}
@@ -1,8 +0,0 @@
namespace NzbDrone.Core.Notifications.MediaBrowser.Model
{
public class EmbyMediaUpdateInfo
{
public string Path { get; set; }
public string UpdateType { get; set; }
}
}
@@ -44,7 +44,7 @@ namespace NzbDrone.Core.Notifications
{
}
public virtual void OnRename(Artist artist)
public virtual void OnRename(Author artist)
{
}
@@ -32,7 +32,7 @@ namespace NzbDrone.Core.Notifications
_logger = logger;
}
private string GetMessage(Artist artist, List<Album> albums, QualityModel quality)
private string GetMessage(Author artist, List<Book> albums, QualityModel quality)
{
var qualityString = quality.Quality.ToString();
@@ -49,7 +49,7 @@ namespace NzbDrone.Core.Notifications
qualityString);
}
private string GetAlbumDownloadMessage(Artist artist, Album album, List<TrackFile> tracks)
private string GetAlbumDownloadMessage(Author artist, Book album, List<BookFile> tracks)
{
return string.Format("{0} - {1} ({2} Tracks Imported)",
artist.Name,
@@ -69,14 +69,14 @@ namespace NzbDrone.Core.Notifications
return text.IsNullOrWhiteSpace() ? "<missing>" : text;
}
private string GetTrackRetagMessage(Artist artist, TrackFile trackFile, Dictionary<string, Tuple<string, string>> diff)
private string GetTrackRetagMessage(Author artist, BookFile trackFile, Dictionary<string, Tuple<string, string>> diff)
{
return string.Format("{0}:\n{1}",
trackFile.Path,
string.Join("\n", diff.Select(x => $"{x.Key}: {FormatMissing(x.Value.Item1)} → {FormatMissing(x.Value.Item2)}")));
}
private bool ShouldHandleArtist(ProviderDefinition definition, Artist artist)
private bool ShouldHandleArtist(ProviderDefinition definition, Author artist)
{
if (definition.Tags.Empty())
{
@@ -152,7 +152,6 @@ namespace NzbDrone.Core.Notifications
Message = GetAlbumDownloadMessage(message.Artist, message.Album, message.ImportedTracks),
Artist = message.Artist,
Album = message.Album,
Release = message.AlbumRelease,
DownloadClient = message.DownloadClient,
DownloadId = message.DownloadId,
TrackFiles = message.ImportedTracks,
@@ -258,7 +257,6 @@ namespace NzbDrone.Core.Notifications
Message = GetTrackRetagMessage(message.Artist, message.TrackFile, message.Diff),
Artist = message.Artist,
Album = message.TrackFile.Album,
Release = message.TrackFile.Tracks.Value.First().AlbumRelease.Value,
TrackFile = message.TrackFile,
Diff = message.Diff,
Scrubbed = message.Scrubbed
@@ -1,15 +0,0 @@
namespace NzbDrone.Core.Notifications.Plex
{
public class PlexAuthenticationException : PlexException
{
public PlexAuthenticationException(string message)
: base(message)
{
}
public PlexAuthenticationException(string message, params object[] args)
: base(message, args)
{
}
}
}
@@ -1,23 +0,0 @@
using System;
using NzbDrone.Common.Exceptions;
namespace NzbDrone.Core.Notifications.Plex
{
public class PlexException : NzbDroneException
{
public PlexException(string message)
: base(message)
{
}
public PlexException(string message, params object[] args)
: base(message, args)
{
}
public PlexException(string message, Exception innerException)
: base(message, innerException)
{
}
}
}
@@ -1,9 +0,0 @@
namespace NzbDrone.Core.Notifications.Plex.PlexTv
{
public class PlexTvPinResponse
{
public int Id { get; set; }
public string Code { get; set; }
public string AuthToken { get; set; }
}
}
@@ -1,11 +0,0 @@
using System.Collections.Generic;
namespace NzbDrone.Core.Notifications.Plex.PlexTv
{
public class PlexTvPinUrlResponse
{
public string Url { get; set; }
public string Method => "POST";
public Dictionary<string, string> Headers { get; set; }
}
}
@@ -1,79 +0,0 @@
using System.Net;
using NLog;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Exceptions;
namespace NzbDrone.Core.Notifications.Plex.PlexTv
{
public interface IPlexTvProxy
{
string GetAuthToken(string clientIdentifier, int pinId);
}
public class PlexTvProxy : IPlexTvProxy
{
private readonly IHttpClient _httpClient;
private readonly Logger _logger;
public PlexTvProxy(IHttpClient httpClient, Logger logger)
{
_httpClient = httpClient;
_logger = logger;
}
public string GetAuthToken(string clientIdentifier, int pinId)
{
var request = BuildRequest(clientIdentifier);
request.ResourceUrl = $"/api/v2/pins/{pinId}";
PlexTvPinResponse response;
if (!Json.TryDeserialize<PlexTvPinResponse>(ProcessRequest(request), out response))
{
response = new PlexTvPinResponse();
}
return response.AuthToken;
}
private HttpRequestBuilder BuildRequest(string clientIdentifier)
{
var requestBuilder = new HttpRequestBuilder("https://plex.tv")
.Accept(HttpAccept.Json)
.AddQueryParam("X-Plex-Client-Identifier", clientIdentifier)
.AddQueryParam("X-Plex-Product", BuildInfo.AppName)
.AddQueryParam("X-Plex-Platform", "Windows")
.AddQueryParam("X-Plex-Platform-Version", "7")
.AddQueryParam("X-Plex-Device-Name", BuildInfo.AppName)
.AddQueryParam("X-Plex-Version", BuildInfo.Version.ToString());
return requestBuilder;
}
private string ProcessRequest(HttpRequestBuilder requestBuilder)
{
var httpRequest = requestBuilder.Build();
HttpResponse response;
_logger.Debug("Url: {0}", httpRequest.Url);
try
{
response = _httpClient.Execute(httpRequest);
}
catch (HttpException ex)
{
throw new NzbDroneClientException(ex.Response.StatusCode, "Unable to connect to plex.tv");
}
catch (WebException)
{
throw new NzbDroneClientException(HttpStatusCode.BadRequest, "Unable to connect to plex.tv");
}
return response.Content;
}
}
}
@@ -1,83 +0,0 @@
using System.Linq;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
namespace NzbDrone.Core.Notifications.Plex.PlexTv
{
public interface IPlexTvService
{
PlexTvPinUrlResponse GetPinUrl();
PlexTvSignInUrlResponse GetSignInUrl(string callbackUrl, int pinId, string pinCode);
string GetAuthToken(int pinId);
}
public class PlexTvService : IPlexTvService
{
private readonly IPlexTvProxy _proxy;
private readonly IConfigService _configService;
public PlexTvService(IPlexTvProxy proxy, IConfigService configService)
{
_proxy = proxy;
_configService = configService;
}
public PlexTvPinUrlResponse GetPinUrl()
{
var clientIdentifier = _configService.PlexClientIdentifier;
var requestBuilder = new HttpRequestBuilder("https://plex.tv/api/v2/pins")
.Accept(HttpAccept.Json)
.AddQueryParam("X-Plex-Client-Identifier", clientIdentifier)
.AddQueryParam("X-Plex-Product", BuildInfo.AppName)
.AddQueryParam("X-Plex-Platform", "Windows")
.AddQueryParam("X-Plex-Platform-Version", "7")
.AddQueryParam("X-Plex-Device-Name", BuildInfo.AppName)
.AddQueryParam("X-Plex-Version", BuildInfo.Version.ToString())
.AddQueryParam("strong", true);
requestBuilder.Method = HttpMethod.POST;
var request = requestBuilder.Build();
return new PlexTvPinUrlResponse
{
Url = request.Url.ToString(),
Headers = request.Headers.ToDictionary(h => h.Key, h => h.Value)
};
}
public PlexTvSignInUrlResponse GetSignInUrl(string callbackUrl, int pinId, string pinCode)
{
var clientIdentifier = _configService.PlexClientIdentifier;
var requestBuilder = new HttpRequestBuilder("https://app.plex.tv/auth/hashBang")
.AddQueryParam("clientID", clientIdentifier)
.AddQueryParam("forwardUrl", callbackUrl)
.AddQueryParam("code", pinCode)
.AddQueryParam("context[device][product]", BuildInfo.AppName)
.AddQueryParam("context[device][platform]", "Windows")
.AddQueryParam("context[device][platformVersion]", "7")
.AddQueryParam("context[device][version]", BuildInfo.Version.ToString());
// #! is stripped out of the URL when building, this works around it.
requestBuilder.Segments.Add("hashBang", "#!");
var request = requestBuilder.Build();
return new PlexTvSignInUrlResponse
{
OauthUrl = request.Url.ToString(),
PinId = pinId
};
}
public string GetAuthToken(int pinId)
{
var authToken = _proxy.GetAuthToken(_configService.PlexClientIdentifier, pinId);
return authToken;
}
}
}
@@ -1,8 +0,0 @@
namespace NzbDrone.Core.Notifications.Plex.PlexTv
{
public class PlexTvSignInUrlResponse
{
public string OauthUrl { get; set; }
public int PinId { get; set; }
}
}
@@ -1,17 +0,0 @@
using NzbDrone.Common.Exceptions;
namespace NzbDrone.Core.Notifications.Plex
{
public class PlexVersionException : NzbDroneException
{
public PlexVersionException(string message)
: base(message)
{
}
public PlexVersionException(string message, params object[] args)
: base(message, args)
{
}
}
}
@@ -1,7 +0,0 @@
namespace NzbDrone.Core.Notifications.Plex.Server
{
public class PlexError
{
public string Error { get; set; }
}
}
@@ -1,8 +0,0 @@
namespace NzbDrone.Core.Notifications.Plex.Server
{
public class PlexIdentity
{
public string MachineIdentifier { get; set; }
public string Version { get; set; }
}
}
@@ -1,24 +0,0 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace NzbDrone.Core.Notifications.Plex.Server
{
public class PlexPreferences
{
[JsonProperty("Setting")]
public List<PlexPreference> Preferences { get; set; }
}
public class PlexPreference
{
public string Id { get; set; }
public string Type { get; set; }
public string Value { get; set; }
}
public class PlexPreferencesLegacy
{
[JsonProperty("_children")]
public List<PlexPreference> Preferences { get; set; }
}
}
@@ -1,7 +0,0 @@
namespace NzbDrone.Core.Notifications.Plex.Server
{
public class PlexResponse<T>
{
public T MediaContainer { get; set; }
}
}
@@ -1,57 +0,0 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace NzbDrone.Core.Notifications.Plex.Server
{
public class PlexSectionLocation
{
public int Id { get; set; }
public string Path { get; set; }
}
public class PlexSection
{
public PlexSection()
{
Locations = new List<PlexSectionLocation>();
}
[JsonProperty("key")]
public int Id { get; set; }
public string Type { get; set; }
public string Language { get; set; }
[JsonProperty("Location")]
public List<PlexSectionLocation> Locations { get; set; }
}
public class PlexSectionsContainer
{
public PlexSectionsContainer()
{
Sections = new List<PlexSection>();
}
[JsonProperty("Directory")]
public List<PlexSection> Sections { get; set; }
}
public class PlexSectionLegacy
{
[JsonProperty("key")]
public int Id { get; set; }
public string Type { get; set; }
public string Language { get; set; }
[JsonProperty("_children")]
public List<PlexSectionLocation> Locations { get; set; }
}
public class PlexMediaContainerLegacy
{
[JsonProperty("_children")]
public List<PlexSectionLegacy> Sections { get; set; }
}
}
@@ -1,25 +0,0 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace NzbDrone.Core.Notifications.Plex.Server
{
public class PlexSectionItem
{
[JsonProperty("ratingKey")]
public int Id { get; set; }
public string Title { get; set; }
}
public class PlexSectionResponse
{
[JsonProperty("Metadata")]
public List<PlexSectionItem> Items { get; set; }
}
public class PlexSectionResponseLegacy
{
[JsonProperty("_children")]
public List<PlexSectionItem> Items { get; set; }
}
}
@@ -1,107 +0,0 @@
using System;
using System.Collections.Generic;
using FluentValidation.Results;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Exceptions;
using NzbDrone.Core.Music;
using NzbDrone.Core.Notifications.Plex.PlexTv;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Notifications.Plex.Server
{
public class PlexServer : NotificationBase<PlexServerSettings>
{
private readonly IPlexServerService _plexServerService;
private readonly IPlexTvService _plexTvService;
public PlexServer(IPlexServerService plexServerService, IPlexTvService plexTvService)
{
_plexServerService = plexServerService;
_plexTvService = plexTvService;
}
public override string Link => "https://www.plex.tv/";
public override string Name => "Plex Media Server";
public override void OnReleaseImport(AlbumDownloadMessage message)
{
UpdateIfEnabled(message.Artist);
}
public override void OnRename(Artist artist)
{
UpdateIfEnabled(artist);
}
public override void OnTrackRetag(TrackRetagMessage message)
{
UpdateIfEnabled(message.Artist);
}
private void UpdateIfEnabled(Artist artist)
{
if (Settings.UpdateLibrary)
{
_plexServerService.UpdateLibrary(artist, Settings);
}
}
public override ValidationResult Test()
{
var failures = new List<ValidationFailure>();
failures.AddIfNotNull(_plexServerService.Test(Settings));
return new ValidationResult(failures);
}
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 authToken = _plexTvService.GetAuthToken(Convert.ToInt32(query["pinId"]));
return new
{
authToken
};
}
return new { };
}
}
}
@@ -1,224 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Net;
using NLog;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Configuration;
namespace NzbDrone.Core.Notifications.Plex.Server
{
public interface IPlexServerProxy
{
List<PlexSection> GetArtistSections(PlexServerSettings settings);
void Update(int sectionId, PlexServerSettings settings);
void UpdateArtist(int metadataId, PlexServerSettings settings);
string Version(PlexServerSettings settings);
List<PlexPreference> Preferences(PlexServerSettings settings);
int? GetMetadataId(int sectionId, string mbId, string language, PlexServerSettings settings);
}
public class PlexServerProxy : IPlexServerProxy
{
private readonly IHttpClient _httpClient;
private readonly IConfigService _configService;
private readonly Logger _logger;
public PlexServerProxy(IHttpClient httpClient, IConfigService configService, Logger logger)
{
_httpClient = httpClient;
_configService = configService;
_logger = logger;
}
public List<PlexSection> GetArtistSections(PlexServerSettings settings)
{
var request = BuildRequest("library/sections", HttpMethod.GET, settings);
var response = ProcessRequest(request);
CheckForError(response);
if (response.Contains("_children"))
{
return Json.Deserialize<PlexMediaContainerLegacy>(response)
.Sections
.Where(d => d.Type == "artist")
.Select(s => new PlexSection
{
Id = s.Id,
Language = s.Language,
Locations = s.Locations,
Type = s.Type
})
.ToList();
}
return Json.Deserialize<PlexResponse<PlexSectionsContainer>>(response)
.MediaContainer
.Sections
.Where(d => d.Type == "artist")
.ToList();
}
public void Update(int sectionId, PlexServerSettings settings)
{
var resource = $"library/sections/{sectionId}/refresh";
var request = BuildRequest(resource, HttpMethod.GET, settings);
var response = ProcessRequest(request);
CheckForError(response);
}
public void UpdateArtist(int metadataId, PlexServerSettings settings)
{
var resource = $"library/metadata/{metadataId}/refresh";
var request = BuildRequest(resource, HttpMethod.PUT, settings);
var response = ProcessRequest(request);
CheckForError(response);
}
public string Version(PlexServerSettings settings)
{
var request = BuildRequest("identity", HttpMethod.GET, settings);
var response = ProcessRequest(request);
CheckForError(response);
if (response.Contains("_children"))
{
return Json.Deserialize<PlexIdentity>(response)
.Version;
}
return Json.Deserialize<PlexResponse<PlexIdentity>>(response)
.MediaContainer
.Version;
}
public List<PlexPreference> Preferences(PlexServerSettings settings)
{
var request = BuildRequest(":/prefs", HttpMethod.GET, settings);
var response = ProcessRequest(request);
CheckForError(response);
if (response.Contains("_children"))
{
return Json.Deserialize<PlexPreferencesLegacy>(response)
.Preferences;
}
return Json.Deserialize<PlexResponse<PlexPreferences>>(response)
.MediaContainer
.Preferences;
}
public int? GetMetadataId(int sectionId, string mbId, string language, PlexServerSettings settings)
{
var guid = string.Format("com.plexapp.agents.lastfm://{0}?lang={1}", mbId, language); // TODO Plex Route for MB? LastFM?
var resource = $"library/sections/{sectionId}/all?guid={System.Web.HttpUtility.UrlEncode(guid)}";
var request = BuildRequest(resource, HttpMethod.GET, settings);
var response = ProcessRequest(request);
CheckForError(response);
List<PlexSectionItem> items;
if (response.Contains("_children"))
{
items = Json.Deserialize<PlexSectionResponseLegacy>(response)
.Items;
}
else
{
items = Json.Deserialize<PlexResponse<PlexSectionResponse>>(response)
.MediaContainer
.Items;
}
if (items == null || items.Empty())
{
return null;
}
return items.First().Id;
}
private HttpRequestBuilder BuildRequest(string resource, HttpMethod method, PlexServerSettings settings)
{
var scheme = settings.UseSsl ? "https" : "http";
var requestBuilder = new HttpRequestBuilder($"{scheme}://{settings.Host}:{settings.Port}")
.Accept(HttpAccept.Json)
.AddQueryParam("X-Plex-Client-Identifier", _configService.PlexClientIdentifier)
.AddQueryParam("X-Plex-Product", BuildInfo.AppName)
.AddQueryParam("X-Plex-Platform", "Windows")
.AddQueryParam("X-Plex-Platform-Version", "7")
.AddQueryParam("X-Plex-Device-Name", BuildInfo.AppName)
.AddQueryParam("X-Plex-Version", BuildInfo.Version.ToString());
if (settings.AuthToken.IsNotNullOrWhiteSpace())
{
requestBuilder.AddQueryParam("X-Plex-Token", settings.AuthToken);
}
requestBuilder.ResourceUrl = resource;
requestBuilder.Method = method;
return requestBuilder;
}
private string ProcessRequest(HttpRequestBuilder requestBuilder)
{
var httpRequest = requestBuilder.Build();
HttpResponse response;
_logger.Debug("Url: {0}", httpRequest.Url);
try
{
response = _httpClient.Execute(httpRequest);
}
catch (HttpException ex)
{
if (ex.Response.StatusCode == HttpStatusCode.Unauthorized)
{
throw new PlexAuthenticationException("Unauthorized - AuthToken is invalid");
}
throw new PlexException("Unable to connect to Plex Media Server. Status Code: {0}", ex.Response.StatusCode);
}
catch (WebException ex)
{
throw new PlexException("Unable to connect to Plex Media Server", ex);
}
return response.Content;
}
private void CheckForError(string response)
{
_logger.Trace("Checking for error");
if (response.IsNullOrWhiteSpace())
{
_logger.Trace("No response body returned, no error detected");
return;
}
var error = response.Contains("_children") ?
Json.Deserialize<PlexError>(response) :
Json.Deserialize<PlexResponse<PlexError>>(response).MediaContainer;
if (error != null && !error.Error.IsNullOrWhiteSpace())
{
throw new PlexException(error.Error);
}
_logger.Trace("No error detected");
}
}
}
@@ -1,183 +0,0 @@
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.Music;
namespace NzbDrone.Core.Notifications.Plex.Server
{
public interface IPlexServerService
{
void UpdateLibrary(Artist artist, PlexServerSettings settings);
ValidationFailure Test(PlexServerSettings settings);
}
public class PlexServerService : IPlexServerService
{
private readonly ICached<Version> _versionCache;
private readonly ICached<bool> _partialUpdateCache;
private readonly IPlexServerProxy _plexServerProxy;
private readonly Logger _logger;
public PlexServerService(ICacheManager cacheManager, IPlexServerProxy plexServerProxy, Logger logger)
{
_versionCache = cacheManager.GetCache<Version>(GetType(), "versionCache");
_partialUpdateCache = cacheManager.GetCache<bool>(GetType(), "partialUpdateCache");
_plexServerProxy = plexServerProxy;
_logger = logger;
}
public void UpdateLibrary(Artist artist, PlexServerSettings settings)
{
try
{
_logger.Debug("Sending Update Request to Plex Server");
var version = _versionCache.Get(settings.Host, () => GetVersion(settings), TimeSpan.FromHours(2));
ValidateVersion(version);
var sections = GetSections(settings);
var partialUpdates = _partialUpdateCache.Get(settings.Host, () => PartialUpdatesAllowed(settings, version), TimeSpan.FromHours(2));
if (partialUpdates)
{
UpdatePartialSection(artist, sections, settings);
}
else
{
sections.ForEach(s => UpdateSection(s.Id, settings));
}
}
catch (Exception ex)
{
_logger.Warn(ex, "Failed to Update Plex host: " + settings.Host);
throw;
}
}
private List<PlexSection> GetSections(PlexServerSettings settings)
{
_logger.Debug("Getting sections from Plex host: {0}", settings.Host);
return _plexServerProxy.GetArtistSections(settings).ToList();
}
private bool PartialUpdatesAllowed(PlexServerSettings settings, Version version)
{
try
{
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.Warn(ex, "Unable to check if partial updates are allowed");
}
return false;
}
private void ValidateVersion(Version version)
{
if (version >= new Version(1, 3, 0) && version < new Version(1, 3, 1))
{
throw new PlexVersionException("Found version {0}, upgrade to PMS 1.3.1 to fix library updating and then restart Readarr", version);
}
}
private Version GetVersion(PlexServerSettings settings)
{
_logger.Debug("Getting version from Plex host: {0}", settings.Host);
var rawVersion = _plexServerProxy.Version(settings);
var version = new Version(Regex.Match(rawVersion, @"^(\d+[.-]){4}").Value.Trim('.', '-'));
return version;
}
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 UpdatePartialSection(Artist artist, List<PlexSection> sections, PlexServerSettings settings)
{
var partiallyUpdated = false;
foreach (var section in sections)
{
var metadataId = GetMetadataId(section.Id, artist, section.Language, settings);
if (metadataId.HasValue)
{
_logger.Debug("Updating Plex host: {0}, Section: {1}, Artist: {2}", settings.Host, section.Id, artist);
_plexServerProxy.UpdateArtist(metadataId.Value, settings);
partiallyUpdated = true;
}
}
// Only update complete sections if all partial updates failed
if (!partiallyUpdated)
{
_logger.Debug("Unable to update partial section, updating all Music sections");
sections.ForEach(s => UpdateSection(s.Id, settings));
}
}
private int? GetMetadataId(int sectionId, Artist artist, string language, PlexServerSettings settings)
{
_logger.Debug("Getting metadata from Plex host: {0} for artist: {1}", settings.Host, artist);
return _plexServerProxy.GetMetadataId(sectionId, artist.Metadata.Value.ForeignArtistId, language, settings);
}
public ValidationFailure Test(PlexServerSettings settings)
{
try
{
var sections = GetSections(settings);
if (sections.Empty())
{
return new ValidationFailure("Host", "At least one Music library is required");
}
}
catch (PlexAuthenticationException ex)
{
_logger.Error(ex, "Unable to connect to Plex Server");
return new ValidationFailure("AuthToken", "Invalid authentication token");
}
catch (Exception ex)
{
_logger.Error(ex, "Unable to connect to Plex Server");
return new ValidationFailure("Host", "Unable to connect to Plex Server");
}
return null;
}
}
}
@@ -1,53 +0,0 @@
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Notifications.Plex.Server
{
public class PlexServerSettingsValidator : AbstractValidator<PlexServerSettings>
{
public PlexServerSettingsValidator()
{
RuleFor(c => c.Host).ValidHost();
RuleFor(c => c.Port).InclusiveBetween(1, 65535);
}
}
public class PlexServerSettings : IProviderConfig
{
private static readonly PlexServerSettingsValidator Validator = new PlexServerSettingsValidator();
public PlexServerSettings()
{
Port = 32400;
UpdateLibrary = true;
SignIn = "startOAuth";
}
[FieldDefinition(0, Label = "Host")]
public string Host { get; set; }
[FieldDefinition(1, Label = "Port")]
public int Port { get; set; }
[FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Connect to Plex over HTTPS instead of HTTP")]
public bool UseSsl { get; set; }
[FieldDefinition(3, Label = "Auth Token", Type = FieldType.Textbox, Advanced = true)]
public string AuthToken { get; set; }
[FieldDefinition(4, Label = "Authenticate with Plex.tv", Type = FieldType.OAuth)]
public string SignIn { get; set; }
[FieldDefinition(5, Label = "Update Library", Type = FieldType.Checkbox)]
public bool UpdateLibrary { get; set; }
public bool IsValid => !string.IsNullOrWhiteSpace(Host);
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}
@@ -54,7 +54,7 @@ namespace NzbDrone.Core.Notifications.Slack
_proxy.SendPayload(payload, Settings);
}
public override void OnRename(Artist artist)
public override void OnRename(Author artist)
{
var attachments = new List<Attachment>
{
@@ -35,7 +35,7 @@ namespace NzbDrone.Core.Notifications.Subsonic
Update();
}
public override void OnRename(Artist artist)
public override void OnRename(Author artist)
{
Update();
}
@@ -38,7 +38,7 @@ namespace NzbDrone.Core.Notifications.Synology
}
}
public override void OnRename(Artist artist)
public override void OnRename(Author artist)
{
if (Settings.UpdateLibrary)
{
@@ -8,10 +8,9 @@ namespace NzbDrone.Core.Notifications
public class TrackRetagMessage
{
public string Message { get; set; }
public Artist Artist { get; set; }
public Album Album { get; set; }
public AlbumRelease Release { get; set; }
public TrackFile TrackFile { get; set; }
public Author Artist { get; set; }
public Book Album { get; set; }
public BookFile TrackFile { get; set; }
public Dictionary<string, Tuple<string, string>> Diff { get; set; }
public bool Scrubbed { get; set; }
@@ -48,13 +48,7 @@ namespace NzbDrone.Core.Notifications.Webhook
{
EventType = "Download",
Artist = new WebhookArtist(message.Artist),
Tracks = trackFiles.SelectMany(x => x.Tracks.Value.Select(y => new WebhookTrack(y)
{
// TODO: Stop passing these parameters inside an episode v3
Quality = x.Quality.Quality.Name,
QualityVersion = x.Quality.Revision.Version,
ReleaseGroup = x.ReleaseGroup
})).ToList(),
Book = new WebhookAlbum(message.Album),
TrackFiles = trackFiles.ConvertAll(x => new WebhookTrackFile(x)),
IsUpgrade = message.OldFiles.Any()
};
@@ -62,7 +56,7 @@ namespace NzbDrone.Core.Notifications.Webhook
_proxy.SendWebhook(payload, Settings);
}
public override void OnRename(Artist artist)
public override void OnRename(Author artist)
{
var payload = new WebhookPayload
{
@@ -9,7 +9,7 @@ namespace NzbDrone.Core.Notifications.Webhook
{
}
public WebhookAlbum(Album album)
public WebhookAlbum(Book album)
{
Id = album.Id;
Title = album.Title;
@@ -13,12 +13,12 @@ namespace NzbDrone.Core.Notifications.Webhook
{
}
public WebhookArtist(Artist artist)
public WebhookArtist(Author artist)
{
Id = artist.Id;
Name = artist.Name;
Path = artist.Path;
MBId = artist.Metadata.Value.ForeignArtistId;
MBId = artist.Metadata.Value.ForeignAuthorId;
}
}
}
@@ -4,7 +4,7 @@ namespace NzbDrone.Core.Notifications.Webhook
{
public class WebhookImportPayload : WebhookPayload
{
public List<WebhookTrack> Tracks { get; set; }
public WebhookAlbum Book { get; set; }
public List<WebhookTrackFile> TrackFiles { get; set; }
public bool IsUpgrade { get; set; }
}
@@ -1,26 +0,0 @@
using NzbDrone.Core.Music;
namespace NzbDrone.Core.Notifications.Webhook
{
public class WebhookTrack
{
public WebhookTrack()
{
}
public WebhookTrack(Track track)
{
Id = track.Id;
Title = track.Title;
TrackNumber = track.TrackNumber;
}
public int Id { get; set; }
public string Title { get; set; }
public string TrackNumber { get; set; }
public string Quality { get; set; }
public int QualityVersion { get; set; }
public string ReleaseGroup { get; set; }
}
}
@@ -8,7 +8,7 @@ namespace NzbDrone.Core.Notifications.Webhook
{
}
public WebhookTrackFile(TrackFile trackFile)
public WebhookTrackFile(BookFile trackFile)
{
Id = trackFile.Id;
Path = trackFile.Path;
@@ -1,14 +0,0 @@
namespace NzbDrone.Core.Notifications.Xbmc.Model
{
public class ActivePlayer
{
public int PlayerId { get; set; }
public string Type { get; set; }
public ActivePlayer(int playerId, string type)
{
PlayerId = playerId;
Type = type;
}
}
}
@@ -1,11 +0,0 @@
using System.Collections.Generic;
namespace NzbDrone.Core.Notifications.Xbmc.Model
{
public class ActivePlayersResult
{
public string Id { get; set; }
public string JsonRpc { get; set; }
public List<ActivePlayer> Result { get; set; }
}
}
@@ -1,9 +0,0 @@
namespace NzbDrone.Core.Notifications.Xbmc.Model
{
public class ArtistResponse
{
public string Id { get; set; }
public string JsonRpc { get; set; }
public ArtistResult Result { get; set; }
}
}
@@ -1,15 +0,0 @@
using System.Collections.Generic;
namespace NzbDrone.Core.Notifications.Xbmc.Model
{
public class ArtistResult
{
public Dictionary<string, int> Limits { get; set; }
public List<KodiArtist> Artists;
public ArtistResult()
{
Artists = new List<KodiArtist>();
}
}
}
@@ -1,11 +0,0 @@
using System.Collections.Generic;
namespace NzbDrone.Core.Notifications.Xbmc.Model
{
public class ErrorResult
{
public string Id { get; set; }
public string JsonRpc { get; set; }
public Dictionary<string, string> Error { get; set; }
}
}
@@ -1,12 +0,0 @@
using System.Collections.Generic;
namespace NzbDrone.Core.Notifications.Xbmc.Model
{
public class KodiArtist
{
public int ArtistId { get; set; }
public string Label { get; set; }
public List<string> MusicbrainzArtistId { get; set; }
public string File { get; set; }
}
}
@@ -1,9 +0,0 @@
namespace NzbDrone.Core.Notifications.Xbmc.Model
{
public class XbmcJsonResult<T>
{
public string Id { get; set; }
public string JsonRpc { get; set; }
public T Result { get; set; }
}
}
@@ -1,102 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Music;
namespace NzbDrone.Core.Notifications.Xbmc
{
public class Xbmc : NotificationBase<XbmcSettings>
{
private readonly IXbmcService _xbmcService;
private readonly Logger _logger;
public Xbmc(IXbmcService xbmcService, Logger logger)
{
_xbmcService = xbmcService;
_logger = logger;
}
public override string Link => "http://xbmc.org/";
public override void OnGrab(GrabMessage grabMessage)
{
const string header = "Readarr - Grabbed";
Notify(Settings, header, grabMessage.Message);
}
public override void OnReleaseImport(AlbumDownloadMessage message)
{
const string header = "Readarr - Downloaded";
Notify(Settings, header, message.Message);
UpdateAndClean(message.Artist, message.OldFiles.Any());
}
public override void OnRename(Artist artist)
{
UpdateAndClean(artist);
}
public override void OnHealthIssue(HealthCheck.HealthCheck healthCheck)
{
Notify(Settings, HEALTH_ISSUE_TITLE_BRANDED, healthCheck.Message);
}
public override void OnTrackRetag(TrackRetagMessage message)
{
UpdateAndClean(message.Artist);
}
public override string Name => "Kodi";
public override ValidationResult Test()
{
var failures = new List<ValidationFailure>();
failures.AddIfNotNull(_xbmcService.Test(Settings, "Success! Kodi has been successfully configured!"));
return new ValidationResult(failures);
}
private void Notify(XbmcSettings settings, string header, string message)
{
try
{
if (Settings.Notify)
{
_xbmcService.Notify(Settings, header, message);
}
}
catch (SocketException ex)
{
var logMessage = string.Format("Unable to connect to Kodi Host: {0}:{1}", Settings.Host, Settings.Port);
_logger.Debug(ex, logMessage);
}
}
private void UpdateAndClean(Artist artist, bool clean = true)
{
try
{
if (Settings.UpdateLibrary)
{
_xbmcService.Update(Settings, artist);
}
if (clean && Settings.CleanLibrary)
{
_xbmcService.Clean(Settings);
}
}
catch (SocketException ex)
{
var logMessage = string.Format("Unable to connect to Kodi Host: {0}:{1}", Settings.Host, Settings.Port);
_logger.Debug(ex, logMessage);
}
}
}
}
@@ -1,143 +0,0 @@
using System.Collections.Generic;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Notifications.Xbmc.Model;
using NzbDrone.Core.Rest;
using RestSharp;
using RestSharp.Authenticators;
namespace NzbDrone.Core.Notifications.Xbmc
{
public interface IXbmcJsonApiProxy
{
string GetJsonVersion(XbmcSettings settings);
void Notify(XbmcSettings settings, string title, string message);
string UpdateLibrary(XbmcSettings settings, string path);
void CleanLibrary(XbmcSettings settings);
List<ActivePlayer> GetActivePlayers(XbmcSettings settings);
List<KodiArtist> GetArtist(XbmcSettings settings);
}
public class XbmcJsonApiProxy : IXbmcJsonApiProxy
{
private readonly Logger _logger;
public XbmcJsonApiProxy(Logger logger)
{
_logger = logger;
}
public string GetJsonVersion(XbmcSettings settings)
{
var request = new RestRequest();
return ProcessRequest(request, settings, "JSONRPC.Version");
}
public void Notify(XbmcSettings settings, string title, string message)
{
var request = new RestRequest();
var parameters = new Dictionary<string, object>();
parameters.Add("title", title);
parameters.Add("message", message);
parameters.Add("image", "https://raw.github.com/Readarr/Readarr/develop/Logo/64.png");
parameters.Add("displaytime", settings.DisplayTime * 1000);
ProcessRequest(request, settings, "GUI.ShowNotification", parameters);
}
public string UpdateLibrary(XbmcSettings settings, string path)
{
var request = new RestRequest();
var parameters = new Dictionary<string, object>();
parameters.Add("directory", path);
if (path.IsNullOrWhiteSpace())
{
parameters = null;
}
var response = ProcessRequest(request, settings, "AudioLibrary.Scan", parameters);
return Json.Deserialize<XbmcJsonResult<string>>(response).Result;
}
public void CleanLibrary(XbmcSettings settings)
{
var request = new RestRequest();
ProcessRequest(request, settings, "AudioLibrary.Clean");
}
public List<ActivePlayer> GetActivePlayers(XbmcSettings settings)
{
var request = new RestRequest();
var response = ProcessRequest(request, settings, "Player.GetActivePlayers");
return Json.Deserialize<ActivePlayersResult>(response).Result;
}
public List<KodiArtist> GetArtist(XbmcSettings settings)
{
var request = new RestRequest();
var parameters = new Dictionary<string, object>();
parameters.Add("properties", new[] { "musicbrainzartistid" }); //TODO: Figure out why AudioLibrary doesnt list file location like videoLibray
var response = ProcessRequest(request, settings, "AudioLibrary.GetArtists", parameters);
return Json.Deserialize<ArtistResponse>(response).Result.Artists;
}
private string ProcessRequest(IRestRequest request, XbmcSettings settings, string method, Dictionary<string, object> parameters = null)
{
var client = BuildClient(settings);
request.Method = Method.POST;
request.RequestFormat = DataFormat.Json;
request.JsonSerializer = new JsonNetSerializer();
request.AddBody(new { jsonrpc = "2.0", method = method, id = 10, @params = parameters });
var response = client.ExecuteAndValidate(request);
_logger.Trace("Response: {0}", response.Content);
CheckForError(response);
return response.Content;
}
private IRestClient BuildClient(XbmcSettings settings)
{
var url = string.Format(@"http://{0}/jsonrpc", settings.Address);
var client = RestClientFactory.BuildClient(url);
if (!settings.Username.IsNullOrWhiteSpace())
{
client.Authenticator = new HttpBasicAuthenticator(settings.Username, settings.Password);
}
return client;
}
private void CheckForError(IRestResponse response)
{
if (string.IsNullOrWhiteSpace(response.Content))
{
throw new XbmcJsonException("Invalid response from XBMC, the response is not valid JSON");
}
_logger.Trace("Looking for error in response, {0}", response.Content);
if (response.Content.StartsWith("{\"error\""))
{
var error = Json.Deserialize<ErrorResult>(response.Content);
var code = error.Error["code"];
var message = error.Error["message"];
var errorMessage = string.Format("XBMC Json Error. Code = {0}, Message: {1}", code, message);
throw new XbmcJsonException(errorMessage);
}
}
}
}
@@ -1,16 +0,0 @@
using System;
namespace NzbDrone.Core.Notifications.Xbmc
{
public class XbmcJsonException : Exception
{
public XbmcJsonException()
{
}
public XbmcJsonException(string message)
: base(message)
{
}
}
}
@@ -1,121 +0,0 @@
using System;
using System.Linq;
using FluentValidation.Results;
using NLog;
using NzbDrone.Core.Music;
namespace NzbDrone.Core.Notifications.Xbmc
{
public interface IXbmcService
{
void Notify(XbmcSettings settings, string title, string message);
void Update(XbmcSettings settings, Artist artist);
void Clean(XbmcSettings settings);
ValidationFailure Test(XbmcSettings settings, string message);
}
public class XbmcService : IXbmcService
{
private readonly IXbmcJsonApiProxy _proxy;
private readonly Logger _logger;
public XbmcService(IXbmcJsonApiProxy proxy,
Logger logger)
{
_proxy = proxy;
_logger = logger;
}
public void Notify(XbmcSettings settings, string title, string message)
{
_proxy.Notify(settings, title, message);
}
public void Update(XbmcSettings settings, Artist artist)
{
if (!settings.AlwaysUpdate)
{
_logger.Debug("Determining if there are any active players on XBMC host: {0}", settings.Address);
var activePlayers = _proxy.GetActivePlayers(settings);
if (activePlayers.Any(a => a.Type.Equals("audio")))
{
_logger.Debug("Audio is currently playing, skipping library update");
return;
}
}
UpdateLibrary(settings, artist);
}
public void Clean(XbmcSettings settings)
{
_proxy.CleanLibrary(settings);
}
public string GetArtistPath(XbmcSettings settings, Artist artist)
{
var allArtists = _proxy.GetArtist(settings);
if (!allArtists.Any())
{
_logger.Debug("No Artists returned from XBMC");
return null;
}
var matchingArtist = allArtists.FirstOrDefault(s =>
{
var musicBrainzId = s.MusicbrainzArtistId.FirstOrDefault();
return musicBrainzId == artist.Metadata.Value.ForeignArtistId || s.Label == artist.Name;
});
return matchingArtist?.File;
}
private void UpdateLibrary(XbmcSettings settings, Artist artist)
{
try
{
var artistPath = GetArtistPath(settings, artist);
if (artistPath != null)
{
_logger.Debug("Updating artist {0} (Path: {1}) on XBMC host: {2}", artist, artistPath, settings.Address);
}
else
{
_logger.Debug("Artist {0} doesn't exist on XBMC host: {1}, Updating Entire Library",
artist,
settings.Address);
}
var response = _proxy.UpdateLibrary(settings, artistPath);
if (!response.Equals("OK", StringComparison.InvariantCultureIgnoreCase))
{
_logger.Debug("Failed to update library for: {0}", settings.Address);
}
}
catch (Exception ex)
{
_logger.Debug(ex, ex.Message);
}
}
public ValidationFailure Test(XbmcSettings settings, string message)
{
try
{
Notify(settings, "Test Notification", message);
}
catch (Exception ex)
{
_logger.Error(ex, "Unable to send test message");
return new ValidationFailure("Host", "Unable to send test message");
}
return null;
}
}
}
@@ -1,65 +0,0 @@
using System.ComponentModel;
using FluentValidation;
using Newtonsoft.Json;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Notifications.Xbmc
{
public class XbmcSettingsValidator : AbstractValidator<XbmcSettings>
{
public XbmcSettingsValidator()
{
RuleFor(c => c.Host).ValidHost();
RuleFor(c => c.DisplayTime).GreaterThanOrEqualTo(2);
}
}
public class XbmcSettings : IProviderConfig
{
private static readonly XbmcSettingsValidator Validator = new XbmcSettingsValidator();
public XbmcSettings()
{
Port = 8080;
DisplayTime = 5;
}
[FieldDefinition(0, Label = "Host")]
public string Host { get; set; }
[FieldDefinition(1, Label = "Port")]
public int Port { get; set; }
[FieldDefinition(2, Label = "Username")]
public string Username { get; set; }
[FieldDefinition(3, Label = "Password", Type = FieldType.Password)]
public string Password { get; set; }
[DefaultValue(5)]
[FieldDefinition(4, Label = "Display Time", HelpText = "How long the notification will be displayed for (In seconds)")]
public int DisplayTime { get; set; }
[FieldDefinition(5, Label = "GUI Notification", Type = FieldType.Checkbox)]
public bool Notify { get; set; }
[FieldDefinition(6, Label = "Update Library", HelpText = "Update Library on Download & Rename?", Type = FieldType.Checkbox)]
public bool UpdateLibrary { get; set; }
[FieldDefinition(7, Label = "Clean Library", HelpText = "Clean Library after update?", Type = FieldType.Checkbox)]
public bool CleanLibrary { get; set; }
[FieldDefinition(8, Label = "Always Update", HelpText = "Update Library even when a file is playing?", Type = FieldType.Checkbox)]
public bool AlwaysUpdate { get; set; }
[JsonIgnore]
public string Address => string.Format("{0}:{1}", Host, Port);
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}