using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Ombi.Api.DogNzb; using Ombi.Api.DogNzb.Models; using Ombi.Api.SickRage; using Ombi.Api.SickRage.Models; using Ombi.Api.Sonarr; using Ombi.Api.Sonarr.Models; using Ombi.Core.Settings; using Ombi.Helpers; using Ombi.Settings.Settings.Models.External; using Ombi.Store.Entities; using Ombi.Store.Entities.Requests; using Ombi.Store.Repository; namespace Ombi.Core.Senders { public class TvSender : ITvSender { public TvSender(ISonarrApi sonarrApi, ISonarrV3Api sonarrV3Api, ILogger log, ISettingsService sonarrSettings, ISettingsService dog, IDogNzbApi dogApi, ISettingsService srSettings, ISickRageApi srApi, IRepository userProfiles, IRepository requestQueue, INotificationHelper notify) { SonarrApi = sonarrApi; SonarrV3Api = sonarrV3Api; Logger = log; SonarrSettings = sonarrSettings; DogNzbSettings = dog; DogNzbApi = dogApi; SickRageSettings = srSettings; SickRageApi = srApi; UserQualityProfiles = userProfiles; _requestQueueRepository = requestQueue; _notificationHelper = notify; } private ISonarrApi SonarrApi { get; } private ISonarrV3Api SonarrV3Api { get; } private IDogNzbApi DogNzbApi { get; } private ISickRageApi SickRageApi { get; } private ILogger Logger { get; } private ISettingsService SonarrSettings { get; } private ISettingsService DogNzbSettings { get; } private ISettingsService SickRageSettings { get; } private IRepository UserQualityProfiles { get; } private readonly IRepository _requestQueueRepository; private readonly INotificationHelper _notificationHelper; public async Task Send(ChildRequests model) { try { var sonarr = await SonarrSettings.GetSettingsAsync(); if (sonarr.Enabled) { var result = await SendToSonarr(model, sonarr); if (result != null) { return new SenderResult { Sent = true, Success = true }; } } var dog = await DogNzbSettings.GetSettingsAsync(); if (dog.Enabled) { var result = await SendToDogNzb(model, dog); if (!result.Failure) { return new SenderResult { Sent = true, Success = true }; } return new SenderResult { Message = result.ErrorMessage }; } var sr = await SickRageSettings.GetSettingsAsync(); if (sr.Enabled) { var result = await SendToSickRage(model, sr); if (result) { return new SenderResult { Sent = true, Success = true }; } return new SenderResult { Message = "Could not send to SickRage!" }; } return new SenderResult { Success = true }; } catch (Exception e) { Logger.LogError(e, "Exception thrown when sending a movie to DVR app, added to the request queue"); // Check if already in request queue var existingQueue = await _requestQueueRepository.FirstOrDefaultAsync(x => x.RequestId == model.Id); if (existingQueue != null) { existingQueue.RetryCount++; existingQueue.Error = e.Message; await _requestQueueRepository.SaveChangesAsync(); } else { await _requestQueueRepository.Add(new RequestQueue { Dts = DateTime.UtcNow, Error = e.Message, RequestId = model.Id, Type = RequestType.TvShow, RetryCount = 0 }); await _notificationHelper.Notify(model, NotificationType.ItemAddedToFaultQueue); } } return new SenderResult { Success = false }; } private async Task SendToDogNzb(ChildRequests model, DogNzbSettings settings) { var id = model.ParentRequest.ExternalProviderId; return await DogNzbApi.AddTvShow(settings.ApiKey, id.ToString()); } /// /// Send the request to Sonarr to process /// /// /// /// public async Task SendToSonarr(ChildRequests model, SonarrSettings s) { if (string.IsNullOrEmpty(s.ApiKey)) { return null; } int qualityToUse; var sonarrV3 = s.V3; var languageProfileId = s.LanguageProfile; string rootFolderPath; string seriesType; var profiles = await UserQualityProfiles.GetAll().FirstOrDefaultAsync(x => x.UserId == model.RequestedUserId); if (model.SeriesType == SeriesType.Anime) { // Get the root path from the rootfolder selected. // For some reason, if we haven't got one use the first root folder in Sonarr if (!int.TryParse(s.RootPathAnime, out int animePath)) { animePath = int.Parse(s.RootPath); // Set it to the main root folder if we have no anime folder. } rootFolderPath = await GetSonarrRootPath(animePath, s); languageProfileId = s.LanguageProfileAnime > 0 ? s.LanguageProfileAnime : s.LanguageProfile; if (!int.TryParse(s.QualityProfileAnime, out qualityToUse)) { qualityToUse = int.Parse(s.QualityProfile); } if (profiles != null) { if (profiles.SonarrRootPathAnime > 0) { rootFolderPath = await GetSonarrRootPath(profiles.SonarrRootPathAnime, s); } if (profiles.SonarrQualityProfileAnime > 0) { qualityToUse = profiles.SonarrQualityProfileAnime; } } seriesType = "anime"; } else { int.TryParse(s.QualityProfile, out qualityToUse); // Get the root path from the rootfolder selected. // For some reason, if we haven't got one use the first root folder in Sonarr rootFolderPath = await GetSonarrRootPath(int.Parse(s.RootPath), s); if (profiles != null) { if (profiles.SonarrRootPath > 0) { rootFolderPath = await GetSonarrRootPath(profiles.SonarrRootPath, s); } if (profiles.SonarrQualityProfile > 0) { qualityToUse = profiles.SonarrQualityProfile; } } seriesType = "standard"; } // Overrides on the request take priority if (model.ParentRequest.QualityOverride.HasValue) { var qualityOverride = model.ParentRequest.QualityOverride.Value; if (qualityOverride > 0) { qualityToUse = qualityOverride; } } if (model.ParentRequest.RootFolder.HasValue) { var rootfolderOverride = model.ParentRequest.RootFolder.Value; if (rootfolderOverride > 0) { rootFolderPath = await GetSonarrRootPath(rootfolderOverride, s); } } if (model.ParentRequest.LanguageProfile.HasValue) { var languageProfile = model.ParentRequest.LanguageProfile.Value; if (languageProfile > 0) { languageProfileId = languageProfile; } } 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); 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 = qualityToUse, titleSlug = model.ParentRequest.Title, seriesType = seriesType, addOptions = new AddOptions { ignoreEpisodesWithFiles = false, // There shouldn't be any episodes with files, this is a new season ignoreEpisodesWithoutFiles = false, // We want all missing searchForMissingEpisodes = false // we want dont want to search yet. We want to make sure everything is unmonitored/monitored correctly. } }; if (sonarrV3) { newSeries.languageProfileId = languageProfileId; } // Montitor the correct seasons, // If we have that season in the model then it's monitored! var seasonsToAdd = GetSeasonsToCreate(model); newSeries.seasons = seasonsToAdd; var result = await SonarrApi.AddSeries(newSeries, s.ApiKey, s.FullUri); if (result?.ErrorMessages?.Any() ?? false) { throw new Exception(string.Join(',', result.ErrorMessages)); } existingSeries = await SonarrApi.GetSeriesById(result.id, s.ApiKey, s.FullUri); await SendToSonarr(model, existingSeries, s); } else { await SendToSonarr(model, existingSeries, s); } return new NewSeries { id = existingSeries.id, seasons = existingSeries.seasons.ToList(), cleanTitle = existingSeries.cleanTitle, title = existingSeries.title, tvdbId = existingSeries.tvdbId }; } catch (Exception e) { Logger.LogError(LoggingEvents.SonarrSender, e, "Exception thrown when attempting to send series over to Sonarr"); throw; } } private async Task SendToSonarr(ChildRequests model, SonarrSeries result, SonarrSettings s) { // Check to ensure we have the all the seasons, ensure the Sonarr metadata has grabbed all the data Season existingSeason = null; foreach (var season in model.SeasonRequests) { var attempt = 0; existingSeason = result.seasons.FirstOrDefault(x => x.seasonNumber == season.SeasonNumber); while (existingSeason == null && attempt < 5) { attempt++; Logger.LogInformation("There was no season numer {0} in Sonarr for title {1}. Will try again as the metadata did not get created", season.SeasonNumber, model.ParentRequest.Title); result = await SonarrApi.GetSeriesById(result.id, s.ApiKey, s.FullUri); existingSeason = result.seasons.FirstOrDefault(x => x.seasonNumber == season.SeasonNumber); await Task.Delay(500); } } var episodesToUpdate = new List(); // Ok, now let's sort out the episodes. if (model.SeriesType == SeriesType.Anime) { result.seriesType = "anime"; await SonarrApi.UpdateSeries(result, s.ApiKey, s.FullUri); } var sonarrEpisodes = await SonarrApi.GetEpisodes(result.id, s.ApiKey, s.FullUri); var sonarrEpList = sonarrEpisodes.ToList() ?? new List(); while (!sonarrEpList.Any()) { // It could be that the series metadata is not ready yet. So wait sonarrEpList = (await SonarrApi.GetEpisodes(result.id, s.ApiKey, s.FullUri)).ToList(); await Task.Delay(500); } foreach (var season in model.SeasonRequests) { foreach (var ep in season.Episodes) { var sonarrEp = sonarrEpList.FirstOrDefault(x => x.episodeNumber == ep.EpisodeNumber && x.seasonNumber == season.SeasonNumber); if (sonarrEp != null && !sonarrEp.monitored) { sonarrEp.monitored = true; episodesToUpdate.Add(sonarrEp); } } existingSeason = result.seasons.FirstOrDefault(x => x.seasonNumber == season.SeasonNumber); // Make sure this season is set to monitored if (!existingSeason.monitored) { // We need to monitor it, problem being is all episodes will now be monitored // So we need to monitor the series but unmonitor every episode existingSeason.monitored = true; var sea = result.seasons.FirstOrDefault(x => x.seasonNumber == existingSeason.seasonNumber); sea.monitored = true; result = await SonarrApi.UpdateSeries(result, s.ApiKey, s.FullUri); var epToUnmonitored = new List(); var newEpList = sonarrEpList.ConvertAll(ep => new Episode(ep)); // Clone it so we don't modify the original member foreach (var ep in newEpList.Where(x => x.seasonNumber == existingSeason.seasonNumber).ToList()) { ep.monitored = false; epToUnmonitored.Add(ep); } foreach (var epToUpdate in epToUnmonitored) { await SonarrApi.UpdateEpisode(epToUpdate, s.ApiKey, s.FullUri); } } // Now update the episodes that need updating foreach (var epToUpdate in episodesToUpdate.Where(x => x.seasonNumber == season.SeasonNumber)) { await SonarrApi.UpdateEpisode(epToUpdate, s.ApiKey, s.FullUri); } } if (!s.AddOnly) { await SearchForRequest(model, sonarrEpList, result, s, episodesToUpdate); } } private static List GetSeasonsToCreate(ChildRequests model) { // Let's get a list of seasons just incase we need to change it var seasonsToUpdate = new List(); for (var i = 0; i < model.ParentRequest.TotalSeasons + 1; i++) { var sea = new Season { seasonNumber = i, monitored = false }; seasonsToUpdate.Add(sea); } return seasonsToUpdate; } private async Task SendToSickRage(ChildRequests model, SickRageSettings settings, string qualityId = null) { var tvdbid = model.ParentRequest.TvDbId; if (qualityId.HasValue()) { var id = qualityId; if (settings.Qualities.All(x => x.Value != id)) { qualityId = settings.QualityProfile; } } else { qualityId = settings.QualityProfile; } // Check if the show exists var existingShow = await SickRageApi.GetShow(tvdbid, settings.ApiKey, settings.FullUri); if (existingShow.message.Equals("Show not found", StringComparison.CurrentCultureIgnoreCase)) { var addResult = await SickRageApi.AddSeries(model.ParentRequest.TvDbId, qualityId, SickRageStatus.Ignored, settings.ApiKey, settings.FullUri); Logger.LogDebug("Added the show (tvdbid) {0}. The result is '{2}' : '{3}'", tvdbid, addResult.result, addResult.message); if (addResult.result.Equals("failure") || addResult.result.Equals("fatal")) { // Do something return false; } } foreach (var seasonRequests in model.SeasonRequests) { var srEpisodes = await SickRageApi.GetEpisodesForSeason(tvdbid, seasonRequests.SeasonNumber, settings.ApiKey, settings.FullUri); int retryTimes = 10; var currentRetry = 0; while (srEpisodes.message.Equals("Show not found", StringComparison.CurrentCultureIgnoreCase) || srEpisodes.message.Equals("Season not found", StringComparison.CurrentCultureIgnoreCase) && srEpisodes.data.Count <= 0) { if (currentRetry > retryTimes) { Logger.LogWarning("Couldnt find the SR Season or Show, message: {0}", srEpisodes.message); break; } await Task.Delay(TimeSpan.FromSeconds(1)); currentRetry++; srEpisodes = await SickRageApi.GetEpisodesForSeason(tvdbid, seasonRequests.SeasonNumber, settings.ApiKey, settings.FullUri); } var totalSrEpisodes = srEpisodes.data.Count; if (totalSrEpisodes == seasonRequests.Episodes.Count) { // This is a request for the whole season var wholeSeasonResult = await SickRageApi.SetEpisodeStatus(settings.ApiKey, settings.FullUri, tvdbid, SickRageStatus.Wanted, seasonRequests.SeasonNumber); Logger.LogDebug("Set the status to Wanted for season {0}. The result is '{1}' : '{2}'", seasonRequests.SeasonNumber, wholeSeasonResult.result, wholeSeasonResult.message); continue; } foreach (var srEp in srEpisodes.data) { var epNumber = srEp.Key; var epData = srEp.Value; var epRequest = seasonRequests.Episodes.FirstOrDefault(x => x.EpisodeNumber == epNumber); if (epRequest != null) { // We want to monior this episode since we have a request for it // Let's check to see if it's wanted first, save an api call if (epData.status.Equals(SickRageStatus.Wanted, StringComparison.CurrentCultureIgnoreCase)) { continue; } var epResult = await SickRageApi.SetEpisodeStatus(settings.ApiKey, settings.FullUri, tvdbid, SickRageStatus.Wanted, seasonRequests.SeasonNumber, epNumber); Logger.LogDebug("Set the status to Wanted for Episode {0} in season {1}. The result is '{2}' : '{3}'", seasonRequests.SeasonNumber, epNumber, epResult.result, epResult.message); } } } return true; } private async Task SearchForRequest(ChildRequests model, IEnumerable sonarrEpList, SonarrSeries existingSeries, SonarrSettings s, IReadOnlyCollection episodesToUpdate) { foreach (var season in model.SeasonRequests) { var sonarrSeason = sonarrEpList.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(existingSeries.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); } } } 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; } } }