diff --git a/PlexRequests.Api.Interfaces/IApiRequest.cs b/PlexRequests.Api.Interfaces/IApiRequest.cs index a7e274752..f2fa72f44 100644 --- a/PlexRequests.Api.Interfaces/IApiRequest.cs +++ b/PlexRequests.Api.Interfaces/IApiRequest.cs @@ -33,6 +33,7 @@ namespace PlexRequests.Api.Interfaces public interface IApiRequest { T Execute(IRestRequest request, Uri baseUri) where T : new(); + IRestResponse Execute(IRestRequest request, Uri baseUri); T ExecuteXml(IRestRequest request, Uri baseUri) where T : class; T ExecuteJson(IRestRequest request, Uri baseUri) where T : new(); } diff --git a/PlexRequests.Api.Interfaces/IHeadphonesApi.cs b/PlexRequests.Api.Interfaces/IHeadphonesApi.cs index 8fc90f2aa..5af3940e9 100644 --- a/PlexRequests.Api.Interfaces/IHeadphonesApi.cs +++ b/PlexRequests.Api.Interfaces/IHeadphonesApi.cs @@ -25,13 +25,19 @@ // ************************************************************************/ #endregion using System; +using System.Collections.Generic; +using System.Threading.Tasks; + using PlexRequests.Api.Models.Music; namespace PlexRequests.Api.Interfaces { public interface IHeadphonesApi { - bool AddAlbum(string apiKey, Uri baseUrl, string albumId); + Task AddAlbum(string apiKey, Uri baseUrl, string albumId); HeadphonesVersion GetVersion(string apiKey, Uri baseUrl); + Task AddArtist(string apiKey, Uri baseUrl, string artistId); + Task QueueAlbum(string apiKey, Uri baseUrl, string albumId); + Task> GetIndex(string apiKey, Uri baseUrl); } } \ No newline at end of file diff --git a/PlexRequests.Api.Models/Music/HeadphonesGetIndex.cs b/PlexRequests.Api.Models/Music/HeadphonesGetIndex.cs new file mode 100644 index 000000000..7f4d46c9e --- /dev/null +++ b/PlexRequests.Api.Models/Music/HeadphonesGetIndex.cs @@ -0,0 +1,49 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: HeadphonesGetIndex.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion +namespace PlexRequests.Api.Models.Music +{ + public class HeadphonesGetIndex + { + public string Status { get; set; } + public string ThumbURL { get; set; } + public string DateAdded { get; set; } + public string MetaCritic { get; set; } + public int? TotalTracks { get; set; } + public object Type { get; set; } + public int? IncludeExtras { get; set; } + public string ArtistName { get; set; } + public string LastUpdated { get; set; } + public string ReleaseDate { get; set; } + public string AlbumID { get; set; } + public string ArtistID { get; set; } + public string ArtworkURL { get; set; } + public string Extras { get; set; } + public int? HaveTracks { get; set; } + public string LatestAlbum { get; set; } + public string ArtistSortName { get; set; } + } +} \ No newline at end of file diff --git a/PlexRequests.Api.Models/Music/MusicBrainzReleaseInfo.cs b/PlexRequests.Api.Models/Music/MusicBrainzReleaseInfo.cs index 952217c9d..b3ca2ee62 100644 --- a/PlexRequests.Api.Models/Music/MusicBrainzReleaseInfo.cs +++ b/PlexRequests.Api.Models/Music/MusicBrainzReleaseInfo.cs @@ -42,6 +42,8 @@ namespace PlexRequests.Api.Models.Music public class MusicBrainzReleaseInfo { + [JsonProperty(PropertyName = "artist-credit")] + public List ArtistCredits { get; set; } public string date { get; set; } public string status { get; set; } public string asin { get; set; } diff --git a/PlexRequests.Api.Models/Music/MusicBrainzSearchResults.cs b/PlexRequests.Api.Models/Music/MusicBrainzSearchResults.cs index 658b8ca4b..157bd5861 100644 --- a/PlexRequests.Api.Models/Music/MusicBrainzSearchResults.cs +++ b/PlexRequests.Api.Models/Music/MusicBrainzSearchResults.cs @@ -63,6 +63,8 @@ namespace PlexRequests.Api.Models.Music public class ArtistCredit { public Artist artist { get; set; } + public string name { get; set; } + public string joinphrase { get; set; } } public class ReleaseGroup diff --git a/PlexRequests.Api.Models/PlexRequests.Api.Models.csproj b/PlexRequests.Api.Models/PlexRequests.Api.Models.csproj index d9c71d2f5..db7e29259 100644 --- a/PlexRequests.Api.Models/PlexRequests.Api.Models.csproj +++ b/PlexRequests.Api.Models/PlexRequests.Api.Models.csproj @@ -50,6 +50,7 @@ + diff --git a/PlexRequests.Api/ApiRequest.cs b/PlexRequests.Api/ApiRequest.cs index 1b7975462..214676a03 100644 --- a/PlexRequests.Api/ApiRequest.cs +++ b/PlexRequests.Api/ApiRequest.cs @@ -64,6 +64,21 @@ namespace PlexRequests.Api } + public IRestResponse Execute(IRestRequest request, Uri baseUri) + { + var client = new RestClient { BaseUrl = baseUri }; + + var response = client.Execute(request); + + if (response.ErrorException != null) + { + var message = "Error retrieving response. Check inner details for more info."; + throw new ApplicationException(message, response.ErrorException); + } + + return response; + } + public T ExecuteXml(IRestRequest request, Uri baseUri) where T : class { var client = new RestClient { BaseUrl = baseUri }; diff --git a/PlexRequests.Api/HeadphonesApi.cs b/PlexRequests.Api/HeadphonesApi.cs index f675c5f25..398745741 100644 --- a/PlexRequests.Api/HeadphonesApi.cs +++ b/PlexRequests.Api/HeadphonesApi.cs @@ -26,6 +26,7 @@ #endregion using System; using System.Collections.Generic; +using System.Threading.Tasks; using Newtonsoft.Json; @@ -33,6 +34,7 @@ using NLog; using PlexRequests.Api.Interfaces; using PlexRequests.Api.Models.Music; +using PlexRequests.Helpers; using RestSharp; @@ -47,7 +49,7 @@ namespace PlexRequests.Api private ApiRequest Api { get; } private static readonly Logger Log = LogManager.GetCurrentClassLogger(); - public bool AddAlbum(string apiKey, Uri baseUrl, string albumId) + public async Task AddAlbum(string apiKey, Uri baseUrl, string albumId) { Log.Trace("Adding album: {0}", albumId); var request = new RestRequest @@ -61,12 +63,91 @@ namespace PlexRequests.Api try { - //var result = Api.Execute(request, baseUrl); - return false; + var result = await Task.Run(() => Api.Execute(request, baseUrl)).ConfigureAwait(false); + Log.Trace("Add Album Result: {0}", result.DumpJson()); + + var albumResult = result.Content.Equals("OK", StringComparison.CurrentCultureIgnoreCase); + Log.Info("Album add result {0}", albumResult); + + return albumResult; + } + catch (JsonSerializationException jse) + { + Log.Error(jse); + return false; // If there is no matching result we do not get returned a JSON string, it just returns "false". + } + } + public async Task> GetIndex(string apiKey, Uri baseUrl) + { + var request = new RestRequest + { + Resource = "/api", + Method = Method.GET + }; + + request.AddQueryParameter("apikey", apiKey); + request.AddQueryParameter("cmd", "getIndex"); + + try + { + var result = await Task.Run(() => Api.ExecuteJson>(request, baseUrl)).ConfigureAwait(false); + + return result; + } + catch (JsonSerializationException jse) + { + Log.Error(jse); + return new List(); + } + } + public async Task AddArtist(string apiKey, Uri baseUrl, string artistId) + { + Log.Trace("Adding Artist: {0}", artistId); + var request = new RestRequest + { + Resource = "/api", + Method = Method.GET + }; + + request.AddQueryParameter("apikey", apiKey); + request.AddQueryParameter("cmd", "addArtist"); + request.AddQueryParameter("id", artistId); + + try + { + var result = await Task.Run(() => Api.Execute(request, baseUrl)).ConfigureAwait(false); + Log.Info("Add Artist Result: {0}", result.Content); + Log.Trace("Add Artist Result: {0}", result.DumpJson()); + return result.Content.Equals("OK", StringComparison.CurrentCultureIgnoreCase); + } + catch (JsonSerializationException jse) + { + Log.Error(jse); + return false; // If there is no matching result we do not get returned a JSON string, it just returns "false". + } + } + public async Task QueueAlbum(string apiKey, Uri baseUrl, string albumId) + { + Log.Trace("Queing album: {0}", albumId); + var request = new RestRequest + { + Resource = "/api?cmd=queueAlbum&id={albumId}", + Method = Method.GET + }; + + request.AddQueryParameter("apikey", apiKey); + request.AddUrlSegment("albumId", albumId); + + try + { + var result = await Task.Run(() => Api.Execute(request, baseUrl)).ConfigureAwait(false); + Log.Info("Queue Result: {0}", result.Content); + Log.Trace("Queue Result: {0}", result.DumpJson()); + return result.Content.Equals("OK", StringComparison.CurrentCultureIgnoreCase); } catch (JsonSerializationException jse) { - Log.Warn(jse); + Log.Error(jse); return false; // If there is no matching result we do not get returned a JSON string, it just returns "false". } } @@ -88,7 +169,7 @@ namespace PlexRequests.Api } catch (JsonSerializationException jse) { - Log.Warn(jse); + Log.Error(jse); return new HeadphonesVersion(); // If there is no matching result we do not get returned a JSON string, it just returns "false". } } diff --git a/PlexRequests.Api/MusicBrainzApi.cs b/PlexRequests.Api/MusicBrainzApi.cs index 62b0771be..1c9210a6b 100644 --- a/PlexRequests.Api/MusicBrainzApi.cs +++ b/PlexRequests.Api/MusicBrainzApi.cs @@ -73,10 +73,12 @@ namespace PlexRequests.Api Log.Trace("Getting album: {0}", releaseId); var request = new RestRequest { - Resource = "release/{albumId}?fmt=json", + Resource = "release/{albumId}", Method = Method.GET }; request.AddUrlSegment("albumId", releaseId); + request.AddQueryParameter("fmt", "json"); + request.AddQueryParameter("inc", "artists"); try { diff --git a/PlexRequests.Store/RequestedModel.cs b/PlexRequests.Store/RequestedModel.cs index f3ea2cd50..4b6f288cf 100644 --- a/PlexRequests.Store/RequestedModel.cs +++ b/PlexRequests.Store/RequestedModel.cs @@ -38,7 +38,8 @@ namespace PlexRequests.Store public string SeasonsRequested { get; set; } public string MusicBrainzId { get; set; } public List RequestedUsers { get; set; } - public string Artist { get; set; } + public string ArtistName { get; set; } + public string ArtistId { get; set; } [JsonIgnore] public List AllUsers diff --git a/PlexRequests.UI/Helpers/HeadphonesSender.cs b/PlexRequests.UI/Helpers/HeadphonesSender.cs new file mode 100644 index 000000000..008f8078e --- /dev/null +++ b/PlexRequests.UI/Helpers/HeadphonesSender.cs @@ -0,0 +1,151 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: HeadphonesSender.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using NLog; + +using PlexRequests.Api.Interfaces; +using PlexRequests.Core; +using PlexRequests.Core.SettingModels; +using PlexRequests.Store; + +namespace PlexRequests.UI.Helpers +{ + public class HeadphonesSender + { + public HeadphonesSender(IHeadphonesApi api, HeadphonesSettings settings, IRequestService request) + { + Api = api; + Settings = settings; + RequestService = request; + } + + private int WaitTime => 1000; + private int CounterMax => 30; + + private static readonly Logger Log = LogManager.GetCurrentClassLogger(); + private IHeadphonesApi Api { get; } + private IRequestService RequestService { get; } + private HeadphonesSettings Settings { get; } + + public async Task AddAlbum(RequestedModel request) + { + var addArtistResult = await AddArtist(request); + if (!addArtistResult) + { + return false; + } + + // Artist is now active + // Add album + var albumResult = await Api.AddAlbum(Settings.ApiKey, Settings.FullUri, request.MusicBrainzId); + if (!albumResult) + { + Log.Error("Couldn't add the album to headphones"); + } + + // Set the status to wanted and search + var status = await SetAlbumStatus(request); + if (!status) + { + return false; + } + + // Approve it + request.Approved = true; + + // Update the record + var updated = RequestService.UpdateRequest(request); + + return updated; + } + + private async Task AddArtist(RequestedModel request) + { + var index = await Api.GetIndex(Settings.ApiKey, Settings.FullUri); + var artistExists = index.Any(x => x.ArtistID == request.ArtistId); + if (!artistExists) + { + var artistAdd = Api.AddArtist(Settings.ApiKey, Settings.FullUri, request.ArtistId); + Log.Info("Artist add result : {0}", artistAdd); + } + + var counter = 0; + while (index.All(x => x.ArtistID != request.ArtistId)) + { + Thread.Sleep(WaitTime); + counter++; + Log.Trace("Artist is still not present in the index. Counter = {0}", counter); + index = await Api.GetIndex(Settings.ApiKey, Settings.FullUri); + if (counter > CounterMax) + { + Log.Trace("Artist is still not present in the index. Counter = {0}. Returning false", counter); + return false; + } + } + + counter = 0; + var artistStatus = index.Where(x => x.ArtistID == request.ArtistId).Select(x => x.Status).FirstOrDefault(); + while (artistStatus != "Active") + { + Thread.Sleep(WaitTime); + counter++; + Log.Trace("Artist status {1}. Counter = {0}", counter, artistStatus); + index = await Api.GetIndex(Settings.ApiKey, Settings.FullUri); + artistStatus = index.Where(x => x.ArtistID == request.ArtistId).Select(x => x.Status).FirstOrDefault(); + if (counter > CounterMax) + { + Log.Trace("Artist status is still not active. Counter = {0}. Returning false", counter); + return false; + } + } + return true; + } + + private async Task SetAlbumStatus(RequestedModel request) + { + var counter = 0; + var setStatus = await Api.QueueAlbum(Settings.ApiKey, Settings.FullUri, request.MusicBrainzId); + + while (!setStatus) + { + Thread.Sleep(WaitTime); + counter++; + Log.Trace("Setting Album status. Counter = {0}", counter); + setStatus = await Api.QueueAlbum(Settings.ApiKey, Settings.FullUri, request.MusicBrainzId); + if (counter > CounterMax) + { + Log.Trace("Album status is still not active. Counter = {0}. Returning false", counter); + return false; + } + } + return true; + } + } +} \ No newline at end of file diff --git a/PlexRequests.UI/Modules/ApprovalModule.cs b/PlexRequests.UI/Modules/ApprovalModule.cs index cb2567109..d5871481f 100644 --- a/PlexRequests.UI/Modules/ApprovalModule.cs +++ b/PlexRequests.UI/Modules/ApprovalModule.cs @@ -47,7 +47,8 @@ namespace PlexRequests.UI.Modules { public ApprovalModule(IRequestService service, ISettingsService cpService, ICouchPotatoApi cpApi, ISonarrApi sonarrApi, - ISettingsService sonarrSettings, ISickRageApi srApi, ISettingsService srSettings) : base("approval") + ISettingsService sonarrSettings, ISickRageApi srApi, ISettingsService srSettings, + ISettingsService hpSettings, IHeadphonesApi hpApi) : base("approval") { this.RequiresAuthentication(); @@ -58,6 +59,8 @@ namespace PlexRequests.UI.Modules SonarrSettings = sonarrSettings; SickRageApi = srApi; SickRageSettings = srSettings; + HeadphonesSettings = hpSettings; + HeadphoneApi = hpApi; Post["/approve"] = parameters => Approve((int)Request.Form.requestid, (string)Request.Form.qualityId); Post["/approveall"] = x => ApproveAll(); @@ -71,9 +74,11 @@ namespace PlexRequests.UI.Modules private ISettingsService SonarrSettings { get; } private ISettingsService SickRageSettings { get; } private ISettingsService CpService { get; } + private ISettingsService HeadphonesSettings { get; } private ISonarrApi SonarrApi { get; } private ISickRageApi SickRageApi { get; } private ICouchPotatoApi CpApi { get; } + private IHeadphonesApi HeadphoneApi { get; } /// /// Approves the specified request identifier. @@ -102,6 +107,8 @@ namespace PlexRequests.UI.Modules return RequestMovieAndUpdateStatus(request, qualityId); case RequestType.TvShow: return RequestTvAndUpdateStatus(request, qualityId); + case RequestType.Album: + return RequestAlbumAndUpdateStatus(request); default: throw new ArgumentOutOfRangeException(nameof(request)); } @@ -190,7 +197,7 @@ namespace PlexRequests.UI.Modules Message = "We could not approve this request. Please try again or check the logs." }); } - + var result = cp.AddMovie(request.ImdbId, cpSettings.ApiKey, request.Title, cpSettings.FullUri, string.IsNullOrEmpty(qualityId) ? cpSettings.ProfileId : qualityId); Log.Trace("Adding movie to CP result {0}", result); if (result) @@ -219,6 +226,34 @@ namespace PlexRequests.UI.Modules }); } + private Response RequestAlbumAndUpdateStatus(RequestedModel request) + { + var hpSettings = HeadphonesSettings.GetSettings(); + Log.Info("Adding album to Headphones : {0}", request.Title); + if (!hpSettings.Enabled) + { + // Approve it + request.Approved = true; + Log.Warn("We approved Album: {0} but could not add it to Headphones because it has not been setup", request.Title); + + // Update the record + var inserted = Service.UpdateRequest(request); + return Response.AsJson(inserted + ? new JsonResponseModel { Result = true, Message = "This has been approved, but It has not been sent to Headphones because it has not been configured." } + : new JsonResponseModel + { + Result = false, + Message = "We could not approve this request. Please try again or check the logs." + }); + } + + var sender = new HeadphonesSender(HeadphoneApi, hpSettings, Service); + var result = sender.AddAlbum(request); + + + return Response.AsJson( new JsonResponseModel { Result = true, Message = "We have sent the approval to Headphones for processing, This can take a few minutes."} ); + } + private Response ApproveAllMovies() { if (!Context.CurrentUser.IsAuthenticated()) @@ -358,10 +393,10 @@ namespace PlexRequests.UI.Modules { var result = Service.BatchUpdate(updatedRequests); - return Response.AsJson(result - ? new JsonResponseModel { Result = true } - : new JsonResponseModel { Result = false, Message = "We could not approve all of the requests. Please try again or check the logs." }); - } + return Response.AsJson(result + ? new JsonResponseModel { Result = true } + : new JsonResponseModel { Result = false, Message = "We could not approve all of the requests. Please try again or check the logs." }); + } catch (Exception e) { Log.Fatal(e); diff --git a/PlexRequests.UI/Modules/SearchModule.cs b/PlexRequests.UI/Modules/SearchModule.cs index c77fddbbd..9200d69c4 100644 --- a/PlexRequests.UI/Modules/SearchModule.cs +++ b/PlexRequests.UI/Modules/SearchModule.cs @@ -505,6 +505,12 @@ namespace PlexRequests.UI.Modules DateTime release; DateTimeHelper.CustomParse(albumInfo.ReleaseEvents?.FirstOrDefault()?.date, out release); + var artist = albumInfo.ArtistCredits?.FirstOrDefault()?.artist; + if (artist == null) + { + return Response.AsJson("We could not find the artist on MusicBrainz. Please try again later or contact your admin"); + } + var model = new RequestedModel { Title = albumInfo.title, @@ -513,12 +519,13 @@ namespace PlexRequests.UI.Modules PosterPath = img, Type = RequestType.Album, ProviderId = 0, - RequestedUsers = new List() { Username }, + RequestedUsers = new List { Username }, Status = albumInfo.status, Issues = IssueState.None, RequestedDate = DateTime.UtcNow, ReleaseDate = release, - // Artist = albumInfo. + ArtistName = artist.name, + ArtistId = artist.id }; @@ -542,24 +549,14 @@ namespace PlexRequests.UI.Modules }); } - var headphonesResult = HeadphonesApi.AddAlbum(hpSettings.ApiKey, hpSettings.FullUri, model.MusicBrainzId); - Log.Info("Result from adding album to Headphones = {0}", headphonesResult); - RequestService.AddRequest(model); - if (headphonesResult) - { - return - Response.AsJson(new JsonResponseModel - { - Result = true, - Message = $"{model.Title} was successfully added!" - }); - } + var sender = new HeadphonesSender(HeadphonesApi, hpSettings, RequestService); + sender.AddAlbum(model); return Response.AsJson(new JsonResponseModel { - Result = false, - Message = $"There was a problem adding {model.Title}. Please contact your admin!" + Result = true, + Message = $"{model.Title} was successfully added!" }); } diff --git a/PlexRequests.UI/PlexRequests.UI.csproj b/PlexRequests.UI/PlexRequests.UI.csproj index 67e6a2e8a..ea706296a 100644 --- a/PlexRequests.UI/PlexRequests.UI.csproj +++ b/PlexRequests.UI/PlexRequests.UI.csproj @@ -168,6 +168,7 @@ + diff --git a/PlexRequests.UI/Views/Admin/_Sidebar.cshtml b/PlexRequests.UI/Views/Admin/_Sidebar.cshtml index e6d21e3fc..5eb4d757f 100644 --- a/PlexRequests.UI/Views/Admin/_Sidebar.cshtml +++ b/PlexRequests.UI/Views/Admin/_Sidebar.cshtml @@ -54,11 +54,11 @@ } @if (Context.Request.Path == "/admin/headphones") { - Headphones + Headphones (Beta) } else { - Headphones + Headphones (Beta) } @if (Context.Request.Path == "/admin/emailnotification")