From 2c945ebb9b49a0e10558b967ebeecd34ac97ed15 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Fri, 30 Jun 2017 22:02:22 +0100 Subject: [PATCH] Started on sonarr #865 --- src/Ombi.Api.Sonarr/ISonarrApi.cs | 4 + src/Ombi.Api.Sonarr/Models/NewSeries.cs | 75 +++++++++ src/Ombi.Api.Sonarr/Models/SonarrSeries.cs | 88 +++++++++++ src/Ombi.Api.Sonarr/SonarrApi.cs | 74 +++++++++ src/Ombi.Core/Engine/TvRequestEngine.cs | 14 +- src/Ombi.Core/ITvSender.cs | 12 ++ src/Ombi.Core/TvSender.cs | 144 ++++++++++++------ src/Ombi.DependencyInjection/IocExtensions.cs | 1 + .../Entities/Requests/ChildRequests.cs | 2 + .../Entities/Requests/TvRequests.cs | 4 + 10 files changed, 366 insertions(+), 52 deletions(-) create mode 100644 src/Ombi.Api.Sonarr/Models/NewSeries.cs create mode 100644 src/Ombi.Api.Sonarr/Models/SonarrSeries.cs create mode 100644 src/Ombi.Core/ITvSender.cs diff --git a/src/Ombi.Api.Sonarr/ISonarrApi.cs b/src/Ombi.Api.Sonarr/ISonarrApi.cs index 2547ff750..8a43a6abf 100644 --- a/src/Ombi.Api.Sonarr/ISonarrApi.cs +++ b/src/Ombi.Api.Sonarr/ISonarrApi.cs @@ -8,5 +8,9 @@ namespace Ombi.Api.Sonarr { Task> GetProfiles(string apiKey, string baseUrl); Task> GetRootFolders(string apiKey, string baseUrl); + Task> GetSeries(string apiKey, string baseUrl); + Task GetSeriesById(int id, string apiKey, string baseUrl); + Task UpdateSeries(SonarrSeries updated, string apiKey, string baseUrl); + Task AddSeries(NewSeries seriesToAdd, string apiKey, string baseUrl); } } \ No newline at end of file diff --git a/src/Ombi.Api.Sonarr/Models/NewSeries.cs b/src/Ombi.Api.Sonarr/Models/NewSeries.cs new file mode 100644 index 000000000..1647d2b32 --- /dev/null +++ b/src/Ombi.Api.Sonarr/Models/NewSeries.cs @@ -0,0 +1,75 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Ombi.Api.Sonarr.Models +{ + public class NewSeries + { + public NewSeries() + { + images = new List(); + } + public AddOptions addOptions { get; set; } + public string title { get; set; } + public List seasons { get; set; } + public string rootFolderPath { get; set; } + public int qualityProfileId { get; set; } + public bool seasonFolder { get; set; } + public bool monitored { get; set; } + public int tvdbId { get; set; } + public int tvRageId { get; set; } + public string cleanTitle { get; set; } + public string imdbId { get; set; } + public string titleSlug { get; set; } + public int id { get; set; } + public List images { get; set; } + + /// + /// This is for us + /// + [JsonIgnore] + public List ErrorMessages { get; set; } + + public string Validate() + { + var errors = new List(); + var sb = new StringBuilder(); + if(this.tvdbId == 0) + { + sb.AppendLine("TVDBID is missing"); + } + if(string.IsNullOrEmpty(title)) + { + sb.AppendLine("Title is missing"); + } + if(qualityProfileId == 0) + { + sb.AppendLine("Quality ID is missing"); + } + + return sb.ToString(); + } + + } + public class AddOptions + { + public bool ignoreEpisodesWithFiles { get; set; } + public bool ignoreEpisodesWithoutFiles { get; set; } + public bool searchForMissingEpisodes { get; set; } + } + + public class SonarrImage + { + public string coverType { get; set; } + public string url { get; set; } + } + + public class SonarrError + { + public string propertyName { get; set; } + public string errorMessage { get; set; } + public object attemptedValue { get; set; } + } +} diff --git a/src/Ombi.Api.Sonarr/Models/SonarrSeries.cs b/src/Ombi.Api.Sonarr/Models/SonarrSeries.cs new file mode 100644 index 000000000..d66a27ebf --- /dev/null +++ b/src/Ombi.Api.Sonarr/Models/SonarrSeries.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Ombi.Api.Sonarr.Models +{ + + public class SonarrSeries + { + public string title { get; set; } + public Alternatetitle[] alternateTitles { get; set; } + public string sortTitle { get; set; } + public int seasonCount { get; set; } + public int totalEpisodeCount { get; set; } + public int episodeCount { get; set; } + public int episodeFileCount { get; set; } + public long sizeOnDisk { get; set; } + public string status { get; set; } + public string overview { get; set; } + public DateTime previousAiring { get; set; } + public string network { get; set; } + public string airTime { get; set; } + public Image[] images { get; set; } + public Season[] seasons { get; set; } + public int year { get; set; } + public string path { get; set; } + public int profileId { get; set; } + public bool seasonFolder { get; set; } + public bool monitored { get; set; } + public bool useSceneNumbering { get; set; } + public int runtime { get; set; } + public int tvdbId { get; set; } + public int tvRageId { get; set; } + public int tvMazeId { get; set; } + public DateTime firstAired { get; set; } + public DateTime lastInfoSync { get; set; } + public string seriesType { get; set; } + public string cleanTitle { get; set; } + public string imdbId { get; set; } + public string titleSlug { get; set; } + public string certification { get; set; } + public string[] genres { get; set; } + public object[] tags { get; set; } + public DateTime added { get; set; } + public Ratings ratings { get; set; } + public int qualityProfileId { get; set; } + public int id { get; set; } + public DateTime nextAiring { get; set; } + } + + public class Ratings + { + public int votes { get; set; } + public float value { get; set; } + } + + public class Alternatetitle + { + public string title { get; set; } + public int sceneSeasonNumber { get; set; } + public int seasonNumber { get; set; } + } + + public class Image + { + public string coverType { get; set; } + public string url { get; set; } + } + + public class Season + { + public int seasonNumber { get; set; } + public bool monitored { get; set; } + public Statistics statistics { get; set; } + } + + public class Statistics + { + public int episodeFileCount { get; set; } + public int episodeCount { get; set; } + public int totalEpisodeCount { get; set; } + public long sizeOnDisk { get; set; } + public int percentOfEpisodes { get; set; } + public DateTime previousAiring { get; set; } + public DateTime nextAiring { get; set; } + } + +} diff --git a/src/Ombi.Api.Sonarr/SonarrApi.cs b/src/Ombi.Api.Sonarr/SonarrApi.cs index 5fbef3874..cab0eeec6 100644 --- a/src/Ombi.Api.Sonarr/SonarrApi.cs +++ b/src/Ombi.Api.Sonarr/SonarrApi.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Threading.Tasks; using Ombi.Api.Sonarr.Models; +using Newtonsoft.Json; +using System.Linq; namespace Ombi.Api.Sonarr { @@ -33,5 +35,77 @@ namespace Ombi.Api.Sonarr return await Api.Request>(request); } + + /// + /// Returns all the series in Sonarr + /// + /// + /// + /// + public async Task> GetSeries(string apiKey, string baseUrl) + { + var request = new Request("/api/series", baseUrl, HttpMethod.Get); + + request.AddHeader("X-Api-Key", apiKey); + + return await Api.Request>(request); + } + + /// + /// Returns the series by the Sonarr ID + /// + /// Sonarr ID for the series + /// + /// + /// + public async Task GetSeriesById(int id, string apiKey, string baseUrl) + { + var request = new Request($"/api/series/{id}", baseUrl, HttpMethod.Get); + + request.AddHeader("X-Api-Key", apiKey); + + return await Api.Request(request); + } + + /// + /// Update the following series + /// + /// The series to update + /// + /// + /// + public async Task UpdateSeries(SonarrSeries updated, string apiKey, string baseUrl) + { + var request = new Request("/api/series/", baseUrl, HttpMethod.Put); + + request.AddHeader("X-Api-Key", apiKey); + request.AddJsonBody(updated); + + return await Api.Request(request); + } + + public async Task AddSeries(NewSeries seriesToAdd, string apiKey, string baseUrl) + { + if(!string.IsNullOrEmpty(seriesToAdd.Validate())) + { + return new NewSeries { ErrorMessages = new List { seriesToAdd.Validate() } }; + } + var request = new Request("/api/series/", baseUrl, HttpMethod.Post); + + request.AddHeader("X-Api-Key", apiKey); + try + { + + return await Api.Request(request); + } + catch (JsonSerializationException e) + { + var error = await Api.Request>(request); + var messages = error?.Select(x => x.errorMessage).ToList(); + messages?.ForEach(x => Log.Error(x)); + return new NewSeries { ErrorMessages = messages }; + } + } + } } diff --git a/src/Ombi.Core/Engine/TvRequestEngine.cs b/src/Ombi.Core/Engine/TvRequestEngine.cs index e5caa0f5c..5987d8677 100644 --- a/src/Ombi.Core/Engine/TvRequestEngine.cs +++ b/src/Ombi.Core/Engine/TvRequestEngine.cs @@ -24,18 +24,21 @@ namespace Ombi.Core.Engine { public TvRequestEngine(ITvMazeApi tvApi, IRequestServiceMain requestService, IPrincipal user, INotificationHelper helper, IMapper map, - IRuleEvaluator rule, IUserIdentityManager manager) : base(user, requestService, rule) + IRuleEvaluator rule, IUserIdentityManager manager, + ITvSender sender) : base(user, requestService, rule) { TvApi = tvApi; NotificationHelper = helper; Mapper = map; UserManager = manager; + TvSender = sender; } private INotificationHelper NotificationHelper { get; } private ITvMazeApi TvApi { get; } private IMapper Mapper { get; } private IUserIdentityManager UserManager { get; } + private ITvSender TvSender {get;} public async Task RequestTvShow(SearchTvShowViewModel tv) { @@ -223,7 +226,8 @@ namespace Ombi.Core.Engine Status = showInfo.status, ImdbId = showInfo.externals?.imdb ?? string.Empty, TvDbId = tv.Id, - ChildRequests = new List() + ChildRequests = new List(), + TotalSeasons = tv.SeasonRequests.Count() }; model.ChildRequests.Add(childRequest); return await AddRequest(model); @@ -294,6 +298,12 @@ namespace Ombi.Core.Engine //NotificationHelper.NewRequest(model.ParentRequest); } + if(model.Approved) + { + // Autosend + TvSender.SendToSonarr(model,model.ParentRequest.TotalSeasons); + } + //var limit = await RequestLimitRepo.GetAllAsync(); //var usersLimit = limit.FirstOrDefault(x => x.Username == Username && x.RequestType == model.Type); //if (usersLimit == null) diff --git a/src/Ombi.Core/ITvSender.cs b/src/Ombi.Core/ITvSender.cs new file mode 100644 index 000000000..0a7ffc186 --- /dev/null +++ b/src/Ombi.Core/ITvSender.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; +using Ombi.Api.Sonarr.Models; +using Ombi.Core.Settings.Models.External; +using Ombi.Store.Entities.Requests; + +namespace Ombi.Core +{ + public interface ITvSender + { + Task SendToSonarr(ChildRequests model, int totalSeasons, string qualityId = null); + } +} \ No newline at end of file diff --git a/src/Ombi.Core/TvSender.cs b/src/Ombi.Core/TvSender.cs index 820b89b4b..8120ad4b2 100644 --- a/src/Ombi.Core/TvSender.cs +++ b/src/Ombi.Core/TvSender.cs @@ -1,77 +1,121 @@ using Microsoft.Extensions.Logging; using Ombi.Api.Sonarr; +using Ombi.Api.Sonarr.Models; +using Ombi.Core.Settings; using Ombi.Core.Settings.Models.External; +using Ombi.Store.Entities.Requests; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace Ombi.Core { - public class TvSender + public class TvSender : ITvSender { - public TvSender(ISonarrApi sonarrApi, ILogger log) + public TvSender(ISonarrApi sonarrApi, ILogger log, ISettingsService settings) { SonarrApi = sonarrApi; Logger = log; + Settings = settings; } private ISonarrApi SonarrApi { get; } private ILogger Logger { get; } + private ISettingsService Settings { get; } - //public async Task SendToSonarr(SonarrSettings sonarrSettings, TvRequestModel model, - // string qualityId) - //{ - // var qualityProfile = 0; - // if (!string.IsNullOrEmpty(qualityId)) // try to parse the passed in quality, otherwise use the settings default quality - // { - // int.TryParse(qualityId, out qualityProfile); - // } - - // if (qualityProfile <= 0) - // { - // int.TryParse(sonarrSettings.QualityProfile, out qualityProfile); - // } - // var rootFolderPath = model.RootFolderSelected <= 0 ? sonarrSettings.FullRootPath : await GetSonarrRootPath(model.RootFolderSelected, sonarrSettings); - - // //var episodeRequest = model.Episodes.Any(); - // //var requestAll = model.SeasonsRequested?.Equals("All", StringComparison.CurrentCultureIgnoreCase); - // //var first = model.SeasonsRequested?.Equals("First", StringComparison.CurrentCultureIgnoreCase); - // //var latest = model.SeasonsRequested?.Equals("Latest", StringComparison.CurrentCultureIgnoreCase); - // //var specificSeasonRequest = model.SeasonList?.Any(); - - // //if (episodeRequest) - // //{ - // // return await ProcessSonarrEpisodeRequest(sonarrSettings, model, qualityProfile, rootFolderPath); - // //} - - // //if (requestAll ?? false) - // //{ - // // return await ProcessSonarrRequestSeason(sonarrSettings, model, qualityProfile, rootFolderPath); - // //} - - // //if (first ?? false) - // //{ - // // return await ProcessSonarrRequestSeason(sonarrSettings, model, qualityProfile, rootFolderPath); - // //} - - // //if (latest ?? false) - // //{ - // // return await ProcessSonarrRequestSeason(sonarrSettings, model, qualityProfile, rootFolderPath); - // //} - - // //if (specificSeasonRequest ?? false) - // //{ - // // return await ProcessSonarrRequestSeason(sonarrSettings, model, qualityProfile, rootFolderPath); - // //} - - // return null; - //} + /// + /// Send the request to Sonarr to process + /// + /// + /// + /// This is for any qualities overriden from the UI + /// + public async Task SendToSonarr(ChildRequests model, int totalSeasons, string qualityId = null) + { + var s = await Settings.GetSettingsAsync(); + var qualityProfile = 0; + if (!string.IsNullOrEmpty(qualityId)) // try to parse the passed in quality, otherwise use the settings default quality + { + int.TryParse(qualityId, out qualityProfile); + } + + if (qualityProfile <= 0) + { + int.TryParse(s.QualityProfile, out qualityProfile); + } + + // Get the root path from the rootfolder selected. + // For some reason, if we haven't got one use the first root folder in Sonarr + // TODO make this overrideable via the UI + var rootFolderPath = await GetSonarrRootPath(model.ParentRequest.RootFolder ?? 0, s); + + + // Does the series actually exist? + var allSeries = await SonarrApi.GetSeries(s.ApiKey, s.FullUri); + var existingSeries = allSeries.FirstOrDefault(x => x.tvdbId == model.ParentRequest.TvDbId); + + if(existingSeries == null) + { + // Time to add a new one + var newSeries = new NewSeries + { + title = model.ParentRequest.Title, + imdbId = model.ParentRequest.ImdbId, + tvdbId = model.ParentRequest.TvDbId, + cleanTitle = model.ParentRequest.Title, + monitored = true, + seasonFolder = s.SeasonFolders, + rootFolderPath = rootFolderPath, + qualityProfileId = qualityProfile, + titleSlug = model.ParentRequest.Title, + addOptions = new AddOptions + { + ignoreEpisodesWithFiles = true, // There shouldn't be any episodes with files, this is a new season + ignoreEpisodesWithoutFiles = false, // We want all missing + searchForMissingEpisodes = true // we want to search for missing TODO pass this in + } + }; + + // Montitor the correct seasons, + // If we have that season in the model then it's monitored! + var seasonsToAdd = new List(); + for (int i = 1; i < totalSeasons; i++) + { + var season = new Season + { + seasonNumber = i, + monitored = model.SeasonRequests.Any(x => x.SeasonNumber == i) + }; + seasonsToAdd.Add(season); + } + + var result = SonarrApi.AddSeries(newSeries, s.ApiKey, s.FullUri); + return result; + } + else + { + // Let's update the existing + } + + + + + return null; + } private async Task GetSonarrRootPath(int pathId, SonarrSettings sonarrSettings) { var rootFoldersResult = await SonarrApi.GetRootFolders(sonarrSettings.ApiKey, sonarrSettings.FullUri); + if(pathId == 0) + { + return rootFoldersResult.FirstOrDefault().path; + } + foreach (var r in rootFoldersResult.Where(r => r.id == pathId)) + { return r.path; + } return string.Empty; } } diff --git a/src/Ombi.DependencyInjection/IocExtensions.cs b/src/Ombi.DependencyInjection/IocExtensions.cs index 65f28b719..cec44a8a7 100644 --- a/src/Ombi.DependencyInjection/IocExtensions.cs +++ b/src/Ombi.DependencyInjection/IocExtensions.cs @@ -54,6 +54,7 @@ namespace Ombi.DependencyInjection services.AddTransient(); services.AddSingleton(); services.AddTransient(); + services.AddTransient(); } public static void RegisterApi(this IServiceCollection services) diff --git a/src/Ombi.Store/Entities/Requests/ChildRequests.cs b/src/Ombi.Store/Entities/Requests/ChildRequests.cs index 782c41e98..38b5e884f 100644 --- a/src/Ombi.Store/Entities/Requests/ChildRequests.cs +++ b/src/Ombi.Store/Entities/Requests/ChildRequests.cs @@ -11,6 +11,8 @@ namespace Ombi.Store.Entities.Requests public TvRequests ParentRequest { get; set; } public int ParentRequestId { get; set; } public int? IssueId { get; set; } + + [ForeignKey(nameof(IssueId))] public List Issues { get; set; } diff --git a/src/Ombi.Store/Entities/Requests/TvRequests.cs b/src/Ombi.Store/Entities/Requests/TvRequests.cs index 3d92c9847..f915e7df7 100644 --- a/src/Ombi.Store/Entities/Requests/TvRequests.cs +++ b/src/Ombi.Store/Entities/Requests/TvRequests.cs @@ -13,6 +13,10 @@ namespace Ombi.Store.Entities.Requests public string PosterPath { get; set; } public DateTime ReleaseDate { get; set; } public string Status { get; set; } + /// + /// This is so we can correctly send the right amount of seasons to Sonarr + /// + public int TotalSeasons { get; set; } public List ChildRequests { get; set; } }