mirror of
https://github.com/Readarr/Readarr.git
synced 2026-04-27 22:56:45 -04:00
New: Readarr 0.1
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user