diff --git a/src/Ombi.Api.Sonarr/ISonarrApi.cs b/src/Ombi.Api.Sonarr/ISonarrApi.cs index 8a43a6abf..ebb70618d 100644 --- a/src/Ombi.Api.Sonarr/ISonarrApi.cs +++ b/src/Ombi.Api.Sonarr/ISonarrApi.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Ombi.Api.Sonarr.Models; +using System.Net.Http; namespace Ombi.Api.Sonarr { @@ -12,5 +13,12 @@ namespace Ombi.Api.Sonarr Task GetSeriesById(int id, string apiKey, string baseUrl); Task UpdateSeries(SonarrSeries updated, string apiKey, string baseUrl); Task AddSeries(NewSeries seriesToAdd, string apiKey, string baseUrl); + Task> GetEpisodes(int seriesId, string apiKey, string baseUrl); + Task GetEpisodeById(int episodeId, string apiKey, string baseUrl); + Task UpdateEpisode(Episode episodeToUpdate, string apiKey, string baseUrl); + Task EpisodeSearch(int[] episodeIds, string apiKey, string baseUrl); + Task SeasonSearch(int seriesId, int seasonNumber, string apiKey, string baseUrl); + Task SeriesSearch(int seriesId, string apiKey, string baseUrl); + } } \ No newline at end of file diff --git a/src/Ombi.Api.Sonarr/Models/CommandResult.cs b/src/Ombi.Api.Sonarr/Models/CommandResult.cs new file mode 100644 index 000000000..0e9565eb8 --- /dev/null +++ b/src/Ombi.Api.Sonarr/Models/CommandResult.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Ombi.Api.Sonarr.Models +{ + + public class CommandResult + { + public string name { get; set; } + public DateTime startedOn { get; set; } + public DateTime stateChangeTime { get; set; } + public bool sendUpdatesToClient { get; set; } + public string state { get; set; } + public int id { get; set; } + } + +} diff --git a/src/Ombi.Api.Sonarr/Models/Episode.cs b/src/Ombi.Api.Sonarr/Models/Episode.cs new file mode 100644 index 000000000..0e25242c4 --- /dev/null +++ b/src/Ombi.Api.Sonarr/Models/Episode.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Ombi.Api.Sonarr.Models +{ + public class Episode + { + public int seriesId { get; set; } + public int episodeFileId { get; set; } + public int seasonNumber { get; set; } + public int episodeNumber { get; set; } + public string title { get; set; } + public string airDate { get; set; } + public DateTime airDateUtc { get; set; } + public string overview { get; set; } + public bool hasFile { get; set; } + public bool monitored { get; set; } + public bool unverifiedSceneNumbering { get; set; } + public int id { get; set; } + public int absoluteEpisodeNumber { get; set; } + public int sceneAbsoluteEpisodeNumber { get; set; } + public int sceneEpisodeNumber { get; set; } + public int sceneSeasonNumber { get; set; } + public Episodefile episodeFile { get; set; } + } + + public class Episodefile + { + public int seriesId { get; set; } + public int seasonNumber { get; set; } + public string relativePath { get; set; } + public string path { get; set; } + public int size { get; set; } + public DateTime dateAdded { get; set; } + public string sceneName { get; set; } + public EpisodeQuality quality { get; set; } + public bool qualityCutoffNotMet { get; set; } + public int id { get; set; } + } + + public class EpisodeQuality + { + public Quality quality { get; set; } + public Revision revision { get; set; } + } + + public class Revision + { + public int version { get; set; } + public int real { get; set; } + } + + + public class EpisodeUpdateResult + { + public int seriesId { get; set; } + public int episodeFileId { get; set; } + public int seasonNumber { get; set; } + public int episodeNumber { get; set; } + public string title { get; set; } + public string airDate { get; set; } + public DateTime airDateUtc { get; set; } + public string overview { get; set; } + public bool hasFile { get; set; } + public bool monitored { get; set; } + public int sceneEpisodeNumber { get; set; } + public int sceneSeasonNumber { get; set; } + public int tvDbEpisodeId { get; set; } + public int absoluteEpisodeNumber { get; set; } + public bool downloading { get; set; } + public int id { get; set; } + } + + +} diff --git a/src/Ombi.Api.Sonarr/Models/SonarrSeries.cs b/src/Ombi.Api.Sonarr/Models/SonarrSeries.cs index d66a27ebf..568592734 100644 --- a/src/Ombi.Api.Sonarr/Models/SonarrSeries.cs +++ b/src/Ombi.Api.Sonarr/Models/SonarrSeries.cs @@ -28,7 +28,7 @@ namespace Ombi.Api.Sonarr.Models public bool seasonFolder { get; set; } public bool monitored { get; set; } public bool useSceneNumbering { get; set; } - public int runtime { get; set; } + public long runtime { get; set; } public int tvdbId { get; set; } public int tvRageId { get; set; } public int tvMazeId { get; set; } @@ -80,7 +80,7 @@ namespace Ombi.Api.Sonarr.Models public int episodeCount { get; set; } public int totalEpisodeCount { get; set; } public long sizeOnDisk { get; set; } - public int percentOfEpisodes { get; set; } + public long 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 cab0eeec6..b4856b347 100644 --- a/src/Ombi.Api.Sonarr/SonarrApi.cs +++ b/src/Ombi.Api.Sonarr/SonarrApi.cs @@ -10,7 +10,6 @@ namespace Ombi.Api.Sonarr { public class SonarrApi : ISonarrApi { - public SonarrApi(IApi api) { Api = api; @@ -21,18 +20,14 @@ namespace Ombi.Api.Sonarr public async Task> GetProfiles(string apiKey, string baseUrl) { var request = new Request("/api/profile", baseUrl, HttpMethod.Get); - request.AddHeader("X-Api-Key", apiKey); - return await Api.Request>(request); } public async Task> GetRootFolders(string apiKey, string baseUrl) { var request = new Request("/api/rootfolder", baseUrl, HttpMethod.Get); - request.AddHeader("X-Api-Key", apiKey); - return await Api.Request>(request); } @@ -45,10 +40,14 @@ namespace Ombi.Api.Sonarr public async Task> GetSeries(string apiKey, string baseUrl) { var request = new Request("/api/series", baseUrl, HttpMethod.Get); - request.AddHeader("X-Api-Key", apiKey); + var results = await Api.Request>(request); - return await Api.Request>(request); + foreach (var s in results) + { + s.seasons.ToList().RemoveAt(0); + } + return results; } /// @@ -61,10 +60,11 @@ namespace Ombi.Api.Sonarr 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); + var result = await Api.Request(request); + result.seasons.ToList().RemoveAt(0); - return await Api.Request(request); + return result; } /// @@ -77,22 +77,21 @@ namespace Ombi.Api.Sonarr 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())) + 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); + request.AddJsonBody(seriesToAdd); try { @@ -102,10 +101,92 @@ namespace Ombi.Api.Sonarr { 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 }; } } + /// + /// Returns the episodes for the series + /// + /// The Sonarr SeriesId value + /// + /// + /// + public async Task> GetEpisodes(int seriesId, string apiKey, string baseUrl) + { + var request = new Request($"/api/Episode?seriesId={seriesId}", baseUrl, HttpMethod.Get); + request.AddHeader("X-Api-Key", apiKey); + return await Api.Request>(request); + } + + /// + /// Returns the episode for the series + /// + /// The Sonarr Episode ID + /// + /// + /// + public async Task GetEpisodeById(int episodeId, string apiKey, string baseUrl) + { + var request = new Request($"/api/Episode/{episodeId}", baseUrl, HttpMethod.Get); + request.AddHeader("X-Api-Key", apiKey); + return await Api.Request(request); + } + + public async Task UpdateEpisode(Episode episodeToUpdate, string apiKey, string baseUrl) + { + var request = new Request($"/api/Episode/", baseUrl, HttpMethod.Put); + request.AddHeader("X-Api-Key", apiKey); + request.AddJsonBody(episodeToUpdate); + return await Api.Request(request); + } + + /// + /// Search for a list of episodes + /// + /// The episodes to search for + /// + /// + /// + public async Task EpisodeSearch(int[] episodeIds, string apiKey, string baseUrl) + { + var result = await Command("EpisodeSearch", apiKey, baseUrl, episodeIds); + return result != null; + } + + /// + /// Search for all episodes of a particular season + /// + /// Series to search for + /// Season to get all episodes + /// + /// + /// + public async Task SeasonSearch(int seriesId, int seasonNumber, string apiKey, string baseUrl) + { + var result = await Command("SeasonSearch", apiKey, baseUrl, new { seriesId, seasonNumber }); + return result != null; + } + + /// + /// Search for all episodes in a series + /// + /// Series to search for + /// + /// + /// + public async Task SeriesSearch(int seriesId, string apiKey, string baseUrl) + { + var result = await Command("SeasonSearch", apiKey, baseUrl, seriesId); + return result != null; + } + + private async Task Command(string commandName, string apiKey, string baseUrl, object body = null) + { + var request = new Request($"/api/Command/{commandName}", baseUrl, HttpMethod.Post); + request.AddHeader("X-Api-Key", apiKey); + if(body != null) request.AddJsonBody(body); + return await Api.Request(request); + } } } diff --git a/src/Ombi.Core/Engine/TvRequestEngine.cs b/src/Ombi.Core/Engine/TvRequestEngine.cs index 5987d8677..c6d672628 100644 --- a/src/Ombi.Core/Engine/TvRequestEngine.cs +++ b/src/Ombi.Core/Engine/TvRequestEngine.cs @@ -301,7 +301,7 @@ namespace Ombi.Core.Engine if(model.Approved) { // Autosend - TvSender.SendToSonarr(model,model.ParentRequest.TotalSeasons); + TvSender.SendToSonarr(model); } //var limit = await RequestLimitRepo.GetAllAsync(); diff --git a/src/Ombi.Core/ITvSender.cs b/src/Ombi.Core/ITvSender.cs index 0a7ffc186..bff588fd5 100644 --- a/src/Ombi.Core/ITvSender.cs +++ b/src/Ombi.Core/ITvSender.cs @@ -7,6 +7,6 @@ namespace Ombi.Core { public interface ITvSender { - Task SendToSonarr(ChildRequests model, int totalSeasons, string qualityId = null); + Task SendToSonarr(ChildRequests model, string qualityId = null); } } \ No newline at end of file diff --git a/src/Ombi.Core/TvSender.cs b/src/Ombi.Core/TvSender.cs index 8120ad4b2..95058d0b2 100644 --- a/src/Ombi.Core/TvSender.cs +++ b/src/Ombi.Core/TvSender.cs @@ -30,7 +30,7 @@ namespace Ombi.Core /// /// This is for any qualities overriden from the UI /// - public async Task SendToSonarr(ChildRequests model, int totalSeasons, string qualityId = null) + public async Task SendToSonarr(ChildRequests model, string qualityId = null) { var s = await Settings.GetSettingsAsync(); var qualityProfile = 0; @@ -48,58 +48,119 @@ namespace Ombi.Core // 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); + try + { - // Does the series actually exist? - var allSeries = await SonarrApi.GetSeries(s.ApiKey, s.FullUri); - var existingSeries = allSeries.FirstOrDefault(x => x.tvdbId == model.ParentRequest.TvDbId); + // 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 + + + if (existingSeries == null) { - 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 + // 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 = true, // We want all missing + searchForMissingEpisodes = false // we want dont want to search yet. We want to make sure everything is unmonitored/monitored correctly. + } + }; + + // 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 < model.ParentRequest.TotalSeasons +1 ; i++) { - 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 + var season = new Season + { + seasonNumber = i, + monitored = model.SeasonRequests.Any(x => x.SeasonNumber == i) + }; + seasonsToAdd.Add(season); } - }; + newSeries.seasons = seasonsToAdd; + var result = await SonarrApi.AddSeries(newSeries, s.ApiKey, s.FullUri); - // 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 + // Ok, now let's sort out the episodes. + var sonarrEpisodes = await SonarrApi.GetEpisodes(result.id, s.ApiKey, s.FullUri); + while(sonarrEpisodes.Count() == 0) { - seasonNumber = i, - monitored = model.SeasonRequests.Any(x => x.SeasonNumber == i) - }; - seasonsToAdd.Add(season); + sonarrEpisodes = await SonarrApi.GetEpisodes(result.id, s.ApiKey, s.FullUri); + await Task.Delay(300); + } + + var episodesToUpdate = new List(); + foreach (var req in model.SeasonRequests) + { + foreach (var ep in req.Episodes) + { + var sonarrEp = sonarrEpisodes.FirstOrDefault(x => x.episodeNumber == ep.EpisodeNumber && x.seasonNumber == ep.Season.SeasonNumber); + if(sonarrEp != null) + { + sonarrEp.monitored = true; + episodesToUpdate.Add(sonarrEp); + } + } + } + + // Now update the episodes that need updating + foreach (var epToUpdate in episodesToUpdate) + { + await SonarrApi.UpdateEpisode(epToUpdate, s.ApiKey, s.FullUri); + } + + if(!s.AddOnly) + { + foreach (var season in model.SeasonRequests) + { + var sonarrSeason = sonarrEpisodes.Where(x => x.seasonNumber == season.SeasonNumber); + var sonarrEpCount = sonarrSeason.Count(); + var ourRequestCount = season.Episodes.Count(); + + if(sonarrEpCount == ourRequestCount) + { + // We have the same amount of requests as all of the episodes in the season. + // Do a season search + await SonarrApi.SeasonSearch(result.id, season.SeasonNumber, s.ApiKey, s.FullUri); + } + else + { + // There is a miss-match, let's search the episodes indiviaully + await SonarrApi.EpisodeSearch(episodesToUpdate.Select(x => x.id).ToArray(), s.ApiKey, s.FullUri); + } + } + } + + return result; + } + else + { + // Let's update the existing } - var result = SonarrApi.AddSeries(newSeries, s.ApiKey, s.FullUri); - return result; } - else + catch (System.Exception e) { - // Let's update the existing + + throw; } - return null; } @@ -107,7 +168,7 @@ namespace Ombi.Core { var rootFoldersResult = await SonarrApi.GetRootFolders(sonarrSettings.ApiKey, sonarrSettings.FullUri); - if(pathId == 0) + if (pathId == 0) { return rootFoldersResult.FirstOrDefault().path; } diff --git a/src/Ombi.Settings/Settings/Models/External/SonarrSettings.cs b/src/Ombi.Settings/Settings/Models/External/SonarrSettings.cs index 78f68aa21..6b2b0d3b0 100644 --- a/src/Ombi.Settings/Settings/Models/External/SonarrSettings.cs +++ b/src/Ombi.Settings/Settings/Models/External/SonarrSettings.cs @@ -13,5 +13,6 @@ /// The root path. /// public string RootPath { get; set; } + public bool AddOnly { get; set; } } } \ No newline at end of file diff --git a/src/Ombi.Store/Entities/Requests/TvRequests.cs b/src/Ombi.Store/Entities/Requests/TvRequests.cs index f915e7df7..13d6a17ae 100644 --- a/src/Ombi.Store/Entities/Requests/TvRequests.cs +++ b/src/Ombi.Store/Entities/Requests/TvRequests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; namespace Ombi.Store.Entities.Requests { @@ -16,6 +17,7 @@ namespace Ombi.Store.Entities.Requests /// /// This is so we can correctly send the right amount of seasons to Sonarr /// + [NotMapped] public int TotalSeasons { get; set; } public List ChildRequests { get; set; }