Merge pull request #2686 from tidusjar/develop

Develop
pull/2729/head^2 v3.0.4036
Jamie 6 years ago committed by GitHub
commit 0c36462652
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -4,14 +4,40 @@
### **New Features**
- Added Sonarr v3 #2359. [TidusJar]
### **Fixes**
- Fixed a potential security vulnerability. [Jamie]
- Sorted out some of the settings pages, trying to make it consistent. [Jamie]
- #2669 Fixed missing translations. [TidusJar]
- Maps alias email variable for welcome emails. [Victor Usoltsev]
- Increased the logo size on the landing page to match the container below it. [Jamie]
- Think the request queue is done! [Jamie]
- Finished off the job. [TidusJar]
## v3.0.3988 (2018-11-23)
### **New Features**
- Updated the emby api since we no longer need the extra parameters to send to emby to log in a local user #2546. [Jamie]
- Added the ability to get the ombi user via a Plex Token #2591. [Jamie]
- Update CHANGELOG.md. [Jamie]
### **Fixes**
- Fixed #2601 [TidusJar]
- Made the subscribe/unsubscribe button more obvious on the UI #2309. [Jamie]

@ -24,16 +24,5 @@ namespace Ombi.Api.Github
request.AddHeader("User-Agent", "Ombi");
return await _api.Request<List<CakeThemes>>(request);
}
public async Task<string> GetThemesRawContent(string url)
{
var sections = url.Split('/');
var lastPart = sections.Last();
url = url.Replace(lastPart, string.Empty);
var request = new Request(lastPart, url, HttpMethod.Get);
request.AddHeader("Accept", "application/vnd.github.v3+json");
request.AddHeader("User-Agent", "Ombi");
return await _api.RequestContent(request);
}
}
}

@ -7,6 +7,5 @@ namespace Ombi.Api.Github
public interface IGithubApi
{
Task<List<CakeThemes>> GetCakeThemes();
Task<string> GetThemesRawContent(string url);
}
}

@ -0,0 +1,13 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Ombi.Api.Sonarr.Models;
using System.Net.Http;
using Ombi.Api.Sonarr.Models.V3;
namespace Ombi.Api.Sonarr
{
public interface ISonarrV3Api : ISonarrApi
{
Task<IEnumerable<LanguageProfiles>> LanguageProfiles(string apiKey, string baseUrl);
}
}

@ -27,6 +27,9 @@ namespace Ombi.Api.Sonarr.Models
public int id { get; set; }
public List<SonarrImage> images { get; set; }
// V3 Property
public int languageProfileId { get; set; }
/// <summary>
/// This is for us
/// </summary>

@ -0,0 +1,30 @@
namespace Ombi.Api.Sonarr.Models.V3
{
public class LanguageProfiles
{
public string name { get; set; }
public bool upgradeAllowed { get; set; }
public Cutoff cutoff { get; set; }
public Languages[] languages { get; set; }
public int id { get; set; }
}
public class Cutoff
{
public int id { get; set; }
public string name { get; set; }
}
public class Languages
{
public Language languages { get; set; }
public bool allowed { get; set; }
}
public class Language
{
public int id { get; set; }
public string name { get; set; }
}
}

@ -16,18 +16,19 @@ namespace Ombi.Api.Sonarr
Api = api;
}
private IApi Api { get; }
protected IApi Api { get; }
protected virtual string ApiBaseUrl => "/api/";
public async Task<IEnumerable<SonarrProfile>> GetProfiles(string apiKey, string baseUrl)
{
var request = new Request("/api/profile", baseUrl, HttpMethod.Get);
var request = new Request($"{ApiBaseUrl}profile", baseUrl, HttpMethod.Get);
request.AddHeader("X-Api-Key", apiKey);
return await Api.Request<List<SonarrProfile>>(request);
}
public async Task<IEnumerable<SonarrRootFolder>> GetRootFolders(string apiKey, string baseUrl)
{
var request = new Request("/api/rootfolder", baseUrl, HttpMethod.Get);
var request = new Request($"{ApiBaseUrl}rootfolder", baseUrl, HttpMethod.Get);
request.AddHeader("X-Api-Key", apiKey);
return await Api.Request<List<SonarrRootFolder>>(request);
}
@ -40,7 +41,7 @@ namespace Ombi.Api.Sonarr
/// <returns></returns>
public async Task<IEnumerable<SonarrSeries>> GetSeries(string apiKey, string baseUrl)
{
var request = new Request("/api/series", baseUrl, HttpMethod.Get);
var request = new Request($"{ApiBaseUrl}series", baseUrl, HttpMethod.Get);
request.AddHeader("X-Api-Key", apiKey);
var results = await Api.Request<List<SonarrSeries>>(request);
@ -63,7 +64,7 @@ namespace Ombi.Api.Sonarr
/// <returns></returns>
public async Task<SonarrSeries> GetSeriesById(int id, string apiKey, string baseUrl)
{
var request = new Request($"/api/series/{id}", baseUrl, HttpMethod.Get);
var request = new Request($"{ApiBaseUrl}series/{id}", baseUrl, HttpMethod.Get);
request.AddHeader("X-Api-Key", apiKey);
var result = await Api.Request<SonarrSeries>(request);
if (result?.seasons?.Length > 0)
@ -82,7 +83,7 @@ namespace Ombi.Api.Sonarr
/// <returns></returns>
public async Task<SonarrSeries> UpdateSeries(SonarrSeries updated, string apiKey, string baseUrl)
{
var request = new Request("/api/series/", baseUrl, HttpMethod.Put);
var request = new Request($"{ApiBaseUrl}series/", baseUrl, HttpMethod.Put);
request.AddHeader("X-Api-Key", apiKey);
request.AddJsonBody(updated);
return await Api.Request<SonarrSeries>(request);
@ -94,7 +95,7 @@ namespace Ombi.Api.Sonarr
{
return new NewSeries { ErrorMessages = new List<string> { seriesToAdd.Validate() } };
}
var request = new Request("/api/series/", baseUrl, HttpMethod.Post);
var request = new Request($"{ApiBaseUrl}series/", baseUrl, HttpMethod.Post);
request.AddHeader("X-Api-Key", apiKey);
request.AddJsonBody(seriesToAdd);
@ -120,7 +121,7 @@ namespace Ombi.Api.Sonarr
/// <returns></returns>
public async Task<IEnumerable<Episode>> GetEpisodes(int seriesId, string apiKey, string baseUrl)
{
var request = new Request($"/api/Episode?seriesId={seriesId}", baseUrl, HttpMethod.Get);
var request = new Request($"{ApiBaseUrl}Episode?seriesId={seriesId}", baseUrl, HttpMethod.Get);
request.AddHeader("X-Api-Key", apiKey);
return await Api.Request<List<Episode>>(request);
}
@ -134,14 +135,14 @@ namespace Ombi.Api.Sonarr
/// <returns></returns>
public async Task<Episode> GetEpisodeById(int episodeId, string apiKey, string baseUrl)
{
var request = new Request($"/api/Episode/{episodeId}", baseUrl, HttpMethod.Get);
var request = new Request($"{ApiBaseUrl}Episode/{episodeId}", baseUrl, HttpMethod.Get);
request.AddHeader("X-Api-Key", apiKey);
return await Api.Request<Episode>(request);
}
public async Task<EpisodeUpdateResult> UpdateEpisode(Episode episodeToUpdate, string apiKey, string baseUrl)
{
var request = new Request($"/api/Episode/", baseUrl, HttpMethod.Put);
var request = new Request($"{ApiBaseUrl}Episode/", baseUrl, HttpMethod.Put);
request.AddHeader("X-Api-Key", apiKey);
request.AddJsonBody(episodeToUpdate);
return await Api.Request<EpisodeUpdateResult>(request);
@ -189,7 +190,7 @@ namespace Ombi.Api.Sonarr
private async Task<CommandResult> Command(string apiKey, string baseUrl, object body)
{
var request = new Request($"/api/Command/", baseUrl, HttpMethod.Post);
var request = new Request($"{ApiBaseUrl}Command/", baseUrl, HttpMethod.Post);
request.AddHeader("X-Api-Key", apiKey);
request.AddJsonBody(body);
return await Api.Request<CommandResult>(request);
@ -197,7 +198,7 @@ namespace Ombi.Api.Sonarr
public async Task<SystemStatus> SystemStatus(string apiKey, string baseUrl)
{
var request = new Request("/api/system/status", baseUrl, HttpMethod.Get);
var request = new Request($"{ApiBaseUrl}system/status", baseUrl, HttpMethod.Get);
request.AddHeader("X-Api-Key", apiKey);
return await Api.Request<SystemStatus>(request);
@ -217,7 +218,7 @@ namespace Ombi.Api.Sonarr
ignoreEpisodesWithoutFiles = false,
}
};
var request = new Request("/api/seasonpass", baseUrl, HttpMethod.Post);
var request = new Request($"{ApiBaseUrl}seasonpass", baseUrl, HttpMethod.Post);
request.AddHeader("X-Api-Key", apiKey);
request.AddJsonBody(seasonPass);

@ -0,0 +1,25 @@
using System.Net.Http;
using System.Collections.Generic;
using System.Threading.Tasks;
using Ombi.Api.Sonarr.Models.V3;
namespace Ombi.Api.Sonarr
{
public class SonarrV3Api : SonarrApi, ISonarrV3Api
{
public SonarrV3Api(IApi api) : base(api)
{
}
protected override string ApiBaseUrl => "/api/v3/";
public async Task<IEnumerable<LanguageProfiles>> LanguageProfiles(string apiKey, string baseUrl)
{
var request = new Request($"{ApiBaseUrl}languageprofile", baseUrl, HttpMethod.Get);
request.AddHeader("X-Api-Key", apiKey);
return await Api.Request<List<LanguageProfiles>>(request);
}
}
}

@ -18,13 +18,13 @@ namespace Ombi.Core.Tests.Rule.Search
[SetUp]
public void Setup()
{
ContextMock = new Mock<IRepository<CouchPotatoCache>>();
ContextMock = new Mock<IExternalRepository<CouchPotatoCache>>();
Rule = new CouchPotatoCacheRule(ContextMock.Object);
}
private CouchPotatoCacheRule Rule { get; set; }
private Mock<IRepository<CouchPotatoCache>> ContextMock { get; set; }
private Mock<IExternalRepository<CouchPotatoCache>> ContextMock { get; set; }
[Test]
public async Task Should_ReturnApproved_WhenMovieIsInCouchPotato()

@ -15,13 +15,13 @@ namespace Ombi.Core.Tests.Rule.Search
[SetUp]
public void Setup()
{
ContextMock = new Mock<IRepository<RadarrCache>>();
ContextMock = new Mock<IExternalRepository<RadarrCache>>();
Rule = new RadarrCacheRule(ContextMock.Object);
}
private RadarrCacheRule Rule { get; set; }
private Mock<IRepository<RadarrCache>> ContextMock { get; set; }
private Mock<IExternalRepository<RadarrCache>> ContextMock { get; set; }
[Test]
public async Task Should_ReturnApproved_WhenMovieIsInRadarr()

@ -11,7 +11,7 @@ using Ombi.Core.Models;
using Ombi.Core.Models.UI;
using Ombi.Core.Rule.Interfaces;
using Ombi.Core.Settings;
using Ombi.Schedule.Jobs.Ombi;
using Ombi.Helpers;
using Ombi.Settings.Settings.Models;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
@ -114,7 +114,7 @@ namespace Ombi.Core.Engine
foreach (var epInformation in childRequests.SeasonRequests.OrderBy(x => x.SeasonNumber))
{
var orderedEpisodes = epInformation.Episodes.OrderBy(x => x.EpisodeNumber).ToList();
var episodeString = NewsletterJob.BuildEpisodeList(orderedEpisodes.Select(x => x.EpisodeNumber));
var episodeString = StringHelper.BuildEpisodeList(orderedEpisodes.Select(x => x.EpisodeNumber));
finalsb.Append($"Season: {epInformation.SeasonNumber} - Episodes: {episodeString}");
finalsb.Append("<br />");
}

@ -20,16 +20,18 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Ombi.Api.CouchPotato\Ombi.Api.CouchPotato.csproj" />
<ProjectReference Include="..\Ombi.Api.DogNzb\Ombi.Api.DogNzb.csproj" />
<ProjectReference Include="..\Ombi.Api.Emby\Ombi.Api.Emby.csproj" />
<ProjectReference Include="..\Ombi.Api.Lidarr\Ombi.Api.Lidarr.csproj" />
<ProjectReference Include="..\Ombi.Api.Plex\Ombi.Api.Plex.csproj" />
<ProjectReference Include="..\Ombi.Api.Radarr\Ombi.Api.Radarr.csproj" />
<ProjectReference Include="..\Ombi.Api.SickRage\Ombi.Api.SickRage.csproj" />
<ProjectReference Include="..\Ombi.Api.Sonarr\Ombi.Api.Sonarr.csproj" />
<ProjectReference Include="..\Ombi.Api.Trakt\Ombi.Api.Trakt.csproj" />
<ProjectReference Include="..\Ombi.Api.TvMaze\Ombi.Api.TvMaze.csproj" />
<ProjectReference Include="..\Ombi.Helpers\Ombi.Helpers.csproj" />
<ProjectReference Include="..\Ombi.Notifications\Ombi.Notifications.csproj" />
<ProjectReference Include="..\Ombi.Schedule\Ombi.Schedule.csproj" />
<ProjectReference Include="..\Ombi.Settings\Ombi.Settings.csproj" />
<ProjectReference Include="..\Ombi.Store\Ombi.Store.csproj" />
<ProjectReference Include="..\Ombi.TheMovieDbApi\Ombi.Api.TheMovieDb.csproj" />

@ -20,7 +20,7 @@ namespace Ombi.Core.Senders
{
public MovieSender(ISettingsService<RadarrSettings> radarrSettings, IRadarrApi api, ILogger<MovieSender> log,
ISettingsService<DogNzbSettings> dogSettings, IDogNzbApi dogApi, ISettingsService<CouchPotatoSettings> cpSettings,
ICouchPotatoApi cpApi, IRepository<UserQualityProfiles> userProfiles)
ICouchPotatoApi cpApi, IRepository<UserQualityProfiles> userProfiles, IRepository<RequestQueue> requestQueue, INotificationHelper notify)
{
RadarrSettings = radarrSettings;
RadarrApi = api;
@ -30,6 +30,8 @@ namespace Ombi.Core.Senders
CouchPotatoSettings = cpSettings;
CouchPotatoApi = cpApi;
_userProfiles = userProfiles;
_requestQueuRepository = requestQueue;
_notificationHelper = notify;
}
private ISettingsService<RadarrSettings> RadarrSettings { get; }
@ -40,38 +42,63 @@ namespace Ombi.Core.Senders
private ISettingsService<CouchPotatoSettings> CouchPotatoSettings { get; }
private ICouchPotatoApi CouchPotatoApi { get; }
private readonly IRepository<UserQualityProfiles> _userProfiles;
private readonly IRepository<RequestQueue> _requestQueuRepository;
private readonly INotificationHelper _notificationHelper;
public async Task<SenderResult> Send(MovieRequests model)
{
var cpSettings = await CouchPotatoSettings.GetSettingsAsync();
//var watcherSettings = await WatcherSettings.GetSettingsAsync();
var radarrSettings = await RadarrSettings.GetSettingsAsync();
if (radarrSettings.Enabled)
try
{
return await SendToRadarr(model, radarrSettings);
}
var dogSettings = await DogNzbSettings.GetSettingsAsync();
if (dogSettings.Enabled)
{
await SendToDogNzb(model, dogSettings);
return new SenderResult
var cpSettings = await CouchPotatoSettings.GetSettingsAsync();
//var watcherSettings = await WatcherSettings.GetSettingsAsync();
var radarrSettings = await RadarrSettings.GetSettingsAsync();
if (radarrSettings.Enabled)
{
Success = true,
Sent = true,
};
}
return await SendToRadarr(model, radarrSettings);
}
if (cpSettings.Enabled)
{
return await SendToCp(model, cpSettings, cpSettings.DefaultProfileId);
}
var dogSettings = await DogNzbSettings.GetSettingsAsync();
if (dogSettings.Enabled)
{
await SendToDogNzb(model, dogSettings);
return new SenderResult
{
Success = true,
Sent = true,
};
}
//if (watcherSettings.Enabled)
//{
// return SendToWatcher(model, watcherSettings);
//}
if (cpSettings.Enabled)
{
return await SendToCp(model, cpSettings, cpSettings.DefaultProfileId);
}
}
catch (Exception e)
{
Log.LogError(e, "Error when seing movie to DVR app, added to the request queue");
// Check if already in request quee
var existingQueue = await _requestQueuRepository.FirstOrDefaultAsync(x => x.RequestId == model.Id);
if (existingQueue != null)
{
existingQueue.RetryCount++;
existingQueue.Error = e.Message;
await _requestQueuRepository.SaveChangesAsync();
}
else
{
await _requestQueuRepository.Add(new RequestQueue
{
Dts = DateTime.UtcNow,
Error = e.Message,
RequestId = model.Id,
Type = RequestType.Movie,
RetryCount = 0
});
_notificationHelper.Notify(model, NotificationType.ItemAddedToFaultQueue);
}
}
return new SenderResult
{
@ -94,9 +121,9 @@ namespace Ombi.Core.Senders
private async Task<SenderResult> SendToRadarr(MovieRequests model, RadarrSettings settings)
{
var qualityToUse = int.Parse(settings.DefaultQualityProfile);
var rootFolderPath = settings.DefaultRootPath;
var profiles = await _userProfiles.GetAll().FirstOrDefaultAsync(x => x.UserId == model.RequestedUserId);
@ -155,7 +182,7 @@ namespace Ombi.Core.Senders
// Search for it
if (!settings.AddOnly)
{
await RadarrApi.MovieSearch(new[] {existingMovie.id}, settings.ApiKey, settings.FullUri);
await RadarrApi.MovieSearch(new[] { existingMovie.id }, settings.ApiKey, settings.FullUri);
}
return new SenderResult { Success = true, Sent = true };

@ -1,37 +1,75 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Ombi.Api.Lidarr;
using Ombi.Api.Lidarr.Models;
using Ombi.Api.Radarr;
using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Settings.Settings.Models.External;
using Ombi.Store.Entities;
using Ombi.Store.Entities.Requests;
using Serilog;
using Ombi.Store.Repository;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace Ombi.Core.Senders
{
public class MusicSender : IMusicSender
{
public MusicSender(ISettingsService<LidarrSettings> lidarr, ILidarrApi lidarrApi)
public MusicSender(ISettingsService<LidarrSettings> lidarr, ILidarrApi lidarrApi, ILogger<MusicSender> log,
IRepository<RequestQueue> requestQueue, INotificationHelper notify)
{
_lidarrSettings = lidarr;
_lidarrApi = lidarrApi;
_log = log;
_requestQueueRepository = requestQueue;
_notificationHelper = notify;
}
private readonly ISettingsService<LidarrSettings> _lidarrSettings;
private readonly ILidarrApi _lidarrApi;
private readonly ILogger _log;
private readonly IRepository<RequestQueue> _requestQueueRepository;
private readonly INotificationHelper _notificationHelper;
public async Task<SenderResult> Send(AlbumRequest model)
{
var settings = await _lidarrSettings.GetSettingsAsync();
if (settings.Enabled)
try
{
return await SendToLidarr(model, settings);
var settings = await _lidarrSettings.GetSettingsAsync();
if (settings.Enabled)
{
return await SendToLidarr(model, settings);
}
return new SenderResult { Success = false, Sent = false, Message = "Lidarr is not enabled" };
}
catch (Exception e)
{
_log.LogError(e, "Exception thrown when sending a music to DVR app, added to the 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.Album,
RetryCount = 0
});
_notificationHelper.Notify(model, NotificationType.ItemAddedToFaultQueue);
}
}
return new SenderResult { Success = false, Sent = false, Message = "Lidarr is not enabled" };
return new SenderResult { Success = false, Sent = false, Message = "Something went wrong!" };
}
private async Task<SenderResult> SendToLidarr(AlbumRequest model, LidarrSettings settings)

@ -21,11 +21,12 @@ namespace Ombi.Core.Senders
{
public class TvSender : ITvSender
{
public TvSender(ISonarrApi sonarrApi, ILogger<TvSender> log, ISettingsService<SonarrSettings> sonarrSettings,
public TvSender(ISonarrApi sonarrApi, ISonarrV3Api sonarrV3Api, ILogger<TvSender> log, ISettingsService<SonarrSettings> sonarrSettings,
ISettingsService<DogNzbSettings> dog, IDogNzbApi dogApi, ISettingsService<SickRageSettings> srSettings,
ISickRageApi srApi, IRepository<UserQualityProfiles> userProfiles)
ISickRageApi srApi, IRepository<UserQualityProfiles> userProfiles, IRepository<RequestQueue> requestQueue, INotificationHelper notify)
{
SonarrApi = sonarrApi;
SonarrV3Api = sonarrV3Api;
Logger = log;
SonarrSettings = sonarrSettings;
DogNzbSettings = dog;
@ -33,9 +34,12 @@ namespace Ombi.Core.Senders
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<TvSender> Logger { get; }
@ -43,59 +47,94 @@ namespace Ombi.Core.Senders
private ISettingsService<DogNzbSettings> DogNzbSettings { get; }
private ISettingsService<SickRageSettings> SickRageSettings { get; }
private IRepository<UserQualityProfiles> UserQualityProfiles { get; }
private readonly IRepository<RequestQueue> _requestQueueRepository;
private readonly INotificationHelper _notificationHelper;
public async Task<SenderResult> Send(ChildRequests model)
{
var sonarr = await SonarrSettings.GetSettingsAsync();
if (sonarr.Enabled)
try
{
var result = await SendToSonarr(model);
if (result != null)
var sonarr = await SonarrSettings.GetSettingsAsync();
if (sonarr.Enabled)
{
var result = await SendToSonarr(model);
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
{
Sent = true,
Success = true
Message = result.ErrorMessage
};
}
}
var dog = await DogNzbSettings.GetSettingsAsync();
if (dog.Enabled)
{
var result = await SendToDogNzb(model, dog);
if (!result.Failure)
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
{
Sent = true,
Success = true
Message = "Could not send to SickRage!"
};
}
return new SenderResult
{
Message = result.ErrorMessage
Success = true
};
}
var sr = await SickRageSettings.GetSettingsAsync();
if (sr.Enabled)
catch (Exception e)
{
var result = await SendToSickRage(model, sr);
if (result)
Logger.LogError(e, "Exception thrown when sending a movie to DVR app, added to the request queue");
// Check if already in request quee
var existingQueue = await _requestQueueRepository.FirstOrDefaultAsync(x => x.RequestId == model.Id);
if (existingQueue != null)
{
return new SenderResult
{
Sent = true,
Success = true
};
existingQueue.RetryCount++;
existingQueue.Error = e.Message;
await _requestQueueRepository.SaveChangesAsync();
}
return new SenderResult
else
{
Message = "Could not send to SickRage!"
};
await _requestQueueRepository.Add(new RequestQueue
{
Dts = DateTime.UtcNow,
Error = e.Message,
RequestId = model.Id,
Type = RequestType.TvShow,
RetryCount = 0
});
_notificationHelper.Notify(model, NotificationType.ItemAddedToFaultQueue);
}
}
return new SenderResult
{
Success = true
Success = false,
Message = "Something wen't wrong!"
};
}
@ -143,7 +182,7 @@ namespace Ombi.Core.Senders
}
if (profiles.SonarrQualityProfileAnime > 0)
{
qualityToUse = profiles.SonarrQualityProfileAnime;
qualityToUse = profiles.SonarrQualityProfileAnime;
}
}
seriesType = "anime";
@ -163,7 +202,7 @@ namespace Ombi.Core.Senders
}
if (profiles.SonarrQualityProfile > 0)
{
qualityToUse = profiles.SonarrQualityProfile;
qualityToUse = profiles.SonarrQualityProfile;
}
}
seriesType = "standard";
@ -174,7 +213,11 @@ namespace Ombi.Core.Senders
{
qualityToUse = model.ParentRequest.QualityOverride.Value;
}
// Are we using v3 sonarr?
var sonarrV3 = s.V3;
var languageProfileId = s.LanguageProfile;
try
{
// Does the series actually exist?
@ -204,6 +247,11 @@ namespace Ombi.Core.Senders
}
};
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);
@ -268,7 +316,7 @@ namespace Ombi.Core.Senders
}
}
var seriesChanges = false;
foreach (var season in model.SeasonRequests)
{
var sonarrSeason = sonarrEpList.Where(x => x.seasonNumber == season.SeasonNumber);

@ -107,6 +107,7 @@ namespace Ombi.DependencyInjection
services.AddTransient<IPlexApi, PlexApi>();
services.AddTransient<IEmbyApi, EmbyApi>();
services.AddTransient<ISonarrApi, SonarrApi>();
services.AddTransient<ISonarrV3Api, SonarrV3Api>();
services.AddTransient<ISlackApi, SlackApi>();
services.AddTransient<ITvMazeApi, TvMazeApi>();
services.AddTransient<ITraktApi, TraktApi>();
@ -198,6 +199,7 @@ namespace Ombi.DependencyInjection
services.AddTransient<ILidarrArtistSync, LidarrArtistSync>();
services.AddTransient<ILidarrAvailabilityChecker, LidarrAvailabilityChecker>();
services.AddTransient<IIssuesPurge, IssuesPurge>();
services.AddTransient<IResendFailedRequests, ResendFailedRequests>();
}
}
}

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Security;
@ -76,6 +77,49 @@ namespace Ombi.Helpers
return -1;
}
public static string BuildEpisodeList(IEnumerable<int> orderedEpisodes)
{
var epSb = new StringBuilder();
var previousEpisodes = new List<int>();
var previousEpisode = -1;
foreach (var ep in orderedEpisodes)
{
if (ep - 1 == previousEpisode)
{
// This is the next one
previousEpisodes.Add(ep);
}
else
{
if (previousEpisodes.Count > 1)
{
// End it
epSb.Append($"{previousEpisodes.First()}-{previousEpisodes.Last()}, ");
}
else if (previousEpisodes.Count == 1)
{
epSb.Append($"{previousEpisodes.FirstOrDefault()}, ");
}
// New one
previousEpisodes.Clear();
previousEpisodes.Add(ep);
}
previousEpisode = ep;
}
if (previousEpisodes.Count > 1)
{
// Got some left over
epSb.Append($"{previousEpisodes.First()}-{previousEpisodes.Last()}");
}
else if (previousEpisodes.Count == 1)
{
epSb.Append(previousEpisodes.FirstOrDefault());
}
return epSb.ToString();
}
public static string RemoveSpaces(this string str)
{
return str.Replace(" ", "");

@ -6,7 +6,6 @@ using Ombi.Api.Discord;
using Ombi.Api.Discord.Models;
using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Notifications.Interfaces;
using Ombi.Notifications.Models;
using Ombi.Settings.Settings.Models;
using Ombi.Settings.Settings.Models.Notifications;

@ -8,7 +8,6 @@ using Microsoft.Extensions.Logging;
using MimeKit;
using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Notifications.Interfaces;
using Ombi.Notifications.Models;
using Ombi.Notifications.Templates;
using Ombi.Settings.Settings.Models;

@ -2,13 +2,10 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Ombi.Api.Discord;
using Ombi.Api.Discord.Models;
using Ombi.Api.Mattermost;
using Ombi.Api.Mattermost.Models;
using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Notifications.Interfaces;
using Ombi.Notifications.Models;
using Ombi.Settings.Settings.Models;
using Ombi.Settings.Settings.Models.Notifications;

@ -8,7 +8,6 @@ using Microsoft.Extensions.Logging;
using Ombi.Api.Notifications;
using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Notifications.Interfaces;
using Ombi.Notifications.Models;
using Ombi.Settings.Settings.Models;
using Ombi.Settings.Settings.Models.Notifications;

@ -4,7 +4,6 @@ using Microsoft.Extensions.Logging;
using Ombi.Api.Pushbullet;
using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Notifications.Interfaces;
using Ombi.Notifications.Models;
using Ombi.Settings.Settings.Models;
using Ombi.Settings.Settings.Models.Notifications;

@ -5,7 +5,6 @@ using Ombi.Api.Pushbullet;
using Ombi.Api.Pushover;
using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Notifications.Interfaces;
using Ombi.Notifications.Models;
using Ombi.Settings.Settings.Models;
using Ombi.Settings.Settings.Models.Notifications;

@ -5,7 +5,6 @@ using Ombi.Api.Slack;
using Ombi.Api.Slack.Models;
using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Notifications.Interfaces;
using Ombi.Notifications.Models;
using Ombi.Settings.Settings.Models;
using Ombi.Settings.Settings.Models.Notifications;

@ -3,7 +3,6 @@ using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Notifications.Interfaces;
using Ombi.Notifications.Models;
using Ombi.Settings.Settings.Models;
using Ombi.Settings.Settings.Models.Notifications;

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
@ -14,7 +13,7 @@ using Ombi.Store.Entities.Requests;
using Ombi.Store.Repository;
using Ombi.Store.Repository.Requests;
namespace Ombi.Notifications.Interfaces
namespace Ombi.Notifications
{
public abstract class BaseNotification<T> : INotification where T : Settings.Settings.Models.Settings, new()
{

@ -6,7 +6,6 @@ using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Ombi.Core.Notifications;
using Ombi.Helpers;
using Ombi.Notifications.Interfaces;
using Ombi.Notifications.Models;
namespace Ombi.Notifications

@ -1,5 +1,7 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using NUnit.Framework;
using Ombi.Helpers;
using static Ombi.Schedule.Jobs.Ombi.NewsletterJob;
namespace Ombi.Schedule.Tests
@ -15,7 +17,7 @@ namespace Ombi.Schedule.Tests
{
ep.Add(i);
}
var result = BuildEpisodeList(ep);
var result = StringHelper.BuildEpisodeList(ep);
return result;
}

@ -21,7 +21,7 @@ namespace Ombi.Schedule
IEmbyUserImporter embyUserImporter, ISonarrSync cache, ICouchPotatoSync cpCache,
ISettingsService<JobSettings> jobsettings, ISickRageSync srSync, IRefreshMetadata refresh,
INewsletterJob newsletter, IPlexRecentlyAddedSync recentlyAddedPlex, ILidarrArtistSync artist,
IIssuesPurge purge)
IIssuesPurge purge, IResendFailedRequests resender)
{
_plexContentSync = plexContentSync;
_radarrSync = radarrSync;
@ -38,6 +38,7 @@ namespace Ombi.Schedule
_plexRecentlyAddedSync = recentlyAddedPlex;
_lidarrArtistSync = artist;
_issuesPurge = purge;
_resender = resender;
}
private readonly IPlexContentSync _plexContentSync;
@ -55,6 +56,7 @@ namespace Ombi.Schedule
private readonly INewsletterJob _newsletter;
private readonly ILidarrArtistSync _lidarrArtistSync;
private readonly IIssuesPurge _issuesPurge;
private readonly IResendFailedRequests _resender;
public void Setup()
{
@ -76,6 +78,8 @@ namespace Ombi.Schedule
RecurringJob.AddOrUpdate(() => _embyUserImporter.Start(), JobSettingsHelper.UserImporter(s));
RecurringJob.AddOrUpdate(() => _plexUserImporter.Start(), JobSettingsHelper.UserImporter(s));
RecurringJob.AddOrUpdate(() => _newsletter.Start(), JobSettingsHelper.Newsletter(s));
RecurringJob.AddOrUpdate(() => _newsletter.Start(), JobSettingsHelper.Newsletter(s));
RecurringJob.AddOrUpdate(() => _resender.Start(), JobSettingsHelper.ResendFailedRequests(s));
}

@ -0,0 +1,9 @@
using System.Threading.Tasks;
namespace Ombi.Schedule.Jobs.Ombi
{
public interface IResendFailedRequests
{
Task Start();
}
}

@ -677,7 +677,7 @@ namespace Ombi.Schedule.Jobs.Ombi
foreach (var epInformation in results.OrderBy(x => x.SeasonNumber))
{
var orderedEpisodes = epInformation.Episodes.OrderBy(x => x.EpisodeNumber).ToList();
var episodeString = BuildEpisodeList(orderedEpisodes.Select(x => x.EpisodeNumber));
var episodeString = StringHelper.BuildEpisodeList(orderedEpisodes.Select(x => x.EpisodeNumber));
finalsb.Append($"Season: {epInformation.SeasonNumber} - Episodes: {episodeString}");
finalsb.Append("<br />");
}
@ -715,48 +715,7 @@ namespace Ombi.Schedule.Jobs.Ombi
}
}
public static string BuildEpisodeList(IEnumerable<int> orderedEpisodes)
{
var epSb = new StringBuilder();
var previousEpisodes = new List<int>();
var previousEpisode = -1;
foreach (var ep in orderedEpisodes)
{
if (ep - 1 == previousEpisode)
{
// This is the next one
previousEpisodes.Add(ep);
}
else
{
if (previousEpisodes.Count > 1)
{
// End it
epSb.Append($"{previousEpisodes.First()}-{previousEpisodes.Last()}, ");
}
else if (previousEpisodes.Count == 1)
{
epSb.Append($"{previousEpisodes.FirstOrDefault()}, ");
}
// New one
previousEpisodes.Clear();
previousEpisodes.Add(ep);
}
previousEpisode = ep;
}
if (previousEpisodes.Count > 1)
{
// Got some left over
epSb.Append($"{previousEpisodes.First()}-{previousEpisodes.Last()}");
}
else if(previousEpisodes.Count == 1)
{
epSb.Append(previousEpisodes.FirstOrDefault());
}
return epSb.ToString();
}
private async Task ProcessEmbyTv(HashSet<EmbyEpisode> embyContent, StringBuilder sb)
{
@ -841,7 +800,7 @@ namespace Ombi.Schedule.Jobs.Ombi
foreach (var epInformation in results.OrderBy(x => x.SeasonNumber))
{
var orderedEpisodes = epInformation.Episodes.OrderBy(x => x.EpisodeNumber).ToList();
var episodeString = BuildEpisodeList(orderedEpisodes.Select(x => x.EpisodeNumber));
var episodeString = StringHelper.BuildEpisodeList(orderedEpisodes.Select(x => x.EpisodeNumber));
finalsb.Append($"Season: {epInformation.SeasonNumber} - Episodes: {episodeString}");
finalsb.Append("<br />");
}

@ -0,0 +1,75 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Ombi.Core;
using Ombi.Core.Senders;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
using Ombi.Store.Repository.Requests;
namespace Ombi.Schedule.Jobs.Ombi
{
public class ResendFailedRequests : IResendFailedRequests
{
public ResendFailedRequests(IRepository<RequestQueue> queue, IMovieSender movieSender, ITvSender tvSender, IMusicSender musicSender,
IMovieRequestRepository movieRepo, ITvRequestRepository tvRepo, IMusicRequestRepository music)
{
_requestQueue = queue;
_movieSender = movieSender;
_tvSender = tvSender;
_musicSender = musicSender;
_movieRequestRepository = movieRepo;
_tvRequestRepository = tvRepo;
_musicRequestRepository = music;
}
private readonly IRepository<RequestQueue> _requestQueue;
private readonly IMovieSender _movieSender;
private readonly ITvSender _tvSender;
private readonly IMusicSender _musicSender;
private readonly IMovieRequestRepository _movieRequestRepository;
private readonly ITvRequestRepository _tvRequestRepository;
private readonly IMusicRequestRepository _musicRequestRepository;
public async Task Start()
{
// Get all the failed ones!
var failedRequests = _requestQueue.GetAll().Where(x => !x.Completed.HasValue);
foreach (var request in failedRequests)
{
if (request.Type == RequestType.Movie)
{
var movieRequest = await _movieRequestRepository.GetAll().FirstOrDefaultAsync(x => x.Id == request.RequestId);
var result = await _movieSender.Send(movieRequest);
if (result.Success)
{
request.Completed = DateTime.UtcNow;
await _requestQueue.SaveChangesAsync();
}
}
if (request.Type == RequestType.TvShow)
{
var tvRequest = await _tvRequestRepository.GetChild().FirstOrDefaultAsync(x => x.Id == request.RequestId);
var result = await _tvSender.Send(tvRequest);
if (result.Success)
{
request.Completed = DateTime.UtcNow;
await _requestQueue.SaveChangesAsync();
}
}
if (request.Type == RequestType.Album)
{
var musicRequest = await _musicRequestRepository.GetAll().FirstOrDefaultAsync(x => x.Id == request.RequestId);
var result = await _musicSender.Send(musicRequest);
if (result.Success)
{
request.Completed = DateTime.UtcNow;
await _requestQueue.SaveChangesAsync();
}
}
}
}
}
}

@ -34,6 +34,7 @@
<ProjectReference Include="..\Ombi.Api.SickRage\Ombi.Api.SickRage.csproj" />
<ProjectReference Include="..\Ombi.Api.Sonarr\Ombi.Api.Sonarr.csproj" />
<ProjectReference Include="..\Ombi.Api.TvMaze\Ombi.Api.TvMaze.csproj" />
<ProjectReference Include="..\Ombi.Core\Ombi.Core.csproj" />
<ProjectReference Include="..\Ombi.Notifications\Ombi.Notifications.csproj" />
<ProjectReference Include="..\Ombi.Settings\Ombi.Settings.csproj" />
<ProjectReference Include="..\Ombi.TheMovieDbApi\Ombi.Api.TheMovieDb.csproj" />

@ -1,55 +1,16 @@
using System;
using System.ComponentModel.DataAnnotations.Schema;
using Newtonsoft.Json;
using Ombi.Helpers;
namespace Ombi.Settings.Settings.Models
namespace Ombi.Settings.Settings.Models
{
public class CustomizationSettings : Settings
{
public string ApplicationName { get; set; }
public string ApplicationUrl { get; set; }
public string CustomCssLink { get; set; }
public string CustomCss { get; set; }
public bool EnableCustomDonations { get; set; }
public string CustomDonationUrl { get; set; }
public string CustomDonationMessage { get; set; }
public string Logo { get; set; }
public string PresetThemeName { get; set; }
public string PresetThemeContent { get; set; }
public bool RecentlyAddedPage { get; set; }
[NotMapped]
public string PresetThemeVersion
{
get
{
if (HasPresetTheme)
{
var parts = PresetThemeName.Split('-');
return parts[3].Replace(".css", string.Empty);
}
return string.Empty;
}
}
[NotMapped]
public string PresetThemeDisplayName
{
get
{
if (HasPresetTheme)
{
var parts = PresetThemeName.Split('-');
return parts[1];
}
return string.Empty;
}
}
[NotMapped]
public bool HasPresetTheme => PresetThemeName.HasValue() || PresetThemeContent.HasValue();
public void AddToUrl(string part)
{
if (string.IsNullOrEmpty(ApplicationUrl))

@ -18,5 +18,7 @@
public string QualityProfileAnime { get; set; }
public string RootPathAnime { get; set; }
public bool AddOnly { get; set; }
public bool V3 { get; set; }
public int LanguageProfile { get; set; }
}
}

@ -15,5 +15,6 @@
public string Newsletter { get; set; }
public string LidarrArtistSync { get; set; }
public string IssuesPurge { get; set; }
public string RetryRequests { get; set; }
}
}

@ -61,6 +61,10 @@ namespace Ombi.Settings.Settings.Models
{
return Get(s.IssuesPurge, Cron.Daily());
}
public static string ResendFailedRequests(JobSettings s)
{
return Get(s.RetryRequests, Cron.Daily(6));
}
private static string Get(string settings, string defaultCron)
{

@ -56,6 +56,7 @@ namespace Ombi.Store.Context
public DbSet<RequestSubscription> RequestSubscription { get; set; }
public DbSet<UserNotificationPreferences> UserNotificationPreferences { get; set; }
public DbSet<UserQualityProfiles> UserQualityProfileses { get; set; }
public DbSet<RequestQueue> RequestQueue { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{

@ -0,0 +1,16 @@
using System;
using System.ComponentModel.DataAnnotations.Schema;
namespace Ombi.Store.Entities
{
[Table("RequestQueue")]
public class RequestQueue : Entity
{
public int RequestId { get; set; }
public RequestType Type { get; set; }
public DateTime Dts { get; set; }
public string Error { get; set; }
public DateTime? Completed { get; set; }
public int RetryCount { get; set; }
}
}

@ -1,8 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Ombi.Store.Entities
namespace Ombi.Store.Entities
{
public enum RequestType
{

File diff suppressed because it is too large Load Diff

@ -0,0 +1,35 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace Ombi.Store.Migrations
{
public partial class RequestQueue : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "RequestQueue",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("Sqlite:Autoincrement", true),
RequestId = table.Column<int>(nullable: false),
Type = table.Column<int>(nullable: false),
Dts = table.Column<DateTime>(nullable: false),
Error = table.Column<string>(nullable: true),
Completed = table.Column<DateTime>(nullable: true),
RetryCount = table.Column<int>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_RequestQueue", x => x.Id);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "RequestQueue");
}
}
}

@ -14,7 +14,7 @@ namespace Ombi.Store.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.1.3-rtm-32065");
.HasAnnotation("ProductVersion", "2.1.4-rtm-31024");
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
@ -508,6 +508,28 @@ namespace Ombi.Store.Migrations
b.ToTable("RecentlyAddedLog");
});
modelBuilder.Entity("Ombi.Store.Entities.RequestQueue", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime?>("Completed");
b.Property<DateTime>("Dts");
b.Property<string>("Error");
b.Property<int>("RequestId");
b.Property<int>("RetryCount");
b.Property<int>("Type");
b.HasKey("Id");
b.ToTable("RequestQueue");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.AlbumRequest", b =>
{
b.Property<int>("Id")

@ -0,0 +1,11 @@
import { RequestType } from ".";
export interface IFailedRequestsViewModel {
failedId: number;
title: string;
releaseYear: Date;
requestId: number;
type: RequestType;
dts: Date;
error: string;
retryCount: number;
}

@ -1,9 +1,9 @@
import { IUser } from "./IUser";
export enum RequestType {
tvShow = 0,
movie = 1,
tvShow = 2,
album = 2,
}
// NEW WORLD

@ -72,6 +72,8 @@ export interface ISonarrSettings extends IExternalSettings {
rootPathAnime: string;
fullRootPath: string;
addOnly: boolean;
v3: boolean;
languageProfile: number;
}
export interface IRadarrSettings extends IExternalSettings {
@ -112,25 +114,13 @@ export interface ICustomizationSettings extends ISettings {
applicationName: string;
applicationUrl: string;
logo: string;
customCssLink: string;
customCss: string;
enableCustomDonations: boolean;
customDonationUrl: string;
customDonationMessage: string;
hasPresetTheme: boolean;
presetThemeName: string;
presetThemeContent: string;
presetThemeDisplayName: string;
presetThemeVersion: string;
recentlyAddedPage: boolean;
}
export interface IThemes {
fullName: string;
displayName: string;
version: string;
url: string;
}
export interface IJobSettings {
embyContentSync: string;
sonarrSync: string;
@ -145,6 +135,7 @@ export interface IJobSettings {
plexRecentlyAddedSync: string;
lidarrArtistSync: string;
issuesPurge: string;
retryRequests: string;
}
export interface IIssueSettings extends ISettings {

@ -7,3 +7,8 @@ export interface ISonarrProfile {
name: string;
id: number;
}
export interface ILanguageProfiles {
name: string;
id: number;
}

@ -17,3 +17,4 @@ export * from "./IRecentlyAdded";
export * from "./ILidarr";
export * from "./ISearchMusicResult";
export * from "./IVote";
export * from "./IFailedRequests";

@ -3,7 +3,7 @@
<div class="centered col-md-12">
<div class="row">
<div class="col-md-push-5 col-md-2">
<div class="col-md-push-3 col-md-6">
<div *ngIf="customizationSettings.logo">
<img [src]="customizationSettings.logo" style="width:100%"/>
</div>

@ -111,15 +111,12 @@
<br />
<div *ngIf="result.available">
<a *ngIf="result.plexUrl" style="text-align: right" class="btn btn-sm btn-success-outline" href="{{result.plexUrl}}"
target="_blank"><i class="fa fa-eye"></i> View On Plex</a>
<a *ngIf="result.embyUrl" style="text-align: right" id="embybtn" class="btn btn-sm btn-success-outline"
href="{{result.embyUrl}}" target="_blank"><i class="fa fa-eye"></i> View On Emby</a>
<a *ngIf="result.plexUrl" style="text-align: right" class="btn btn-sm btn-success-outline" href="{{result.plexUrl}}" target="_blank"><i class="fa fa-eye"></i> {{'Search.ViewOnPlex' | translate}}</a>
<a *ngIf="result.embyUrl" style="text-align: right" id="embybtn" class="btn btn-sm btn-success-outline" href="{{result.embyUrl}}" target="_blank"><i class="fa fa-eye"></i> {{'Search.ViewOnEmby' | translate}}</a>
</div>
<div class="dropdown" *ngIf="result.available && issueCategories && issuesEnabled">
<button class="btn btn-sm btn-primary-outline dropdown-toggle" type="button" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="true">
<i class="fa fa-plus"></i> Report Issue
<button class="btn btn-sm btn-primary-outline dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<i class="fa fa-plus"></i> {{'Requests.ReportIssue' | translate}}
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu1">

@ -5,7 +5,7 @@ import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs";
import { ISonarrSettings } from "../../interfaces";
import { ISonarrProfile, ISonarrRootFolder } from "../../interfaces";
import { ILanguageProfiles, ISonarrProfile, ISonarrRootFolder } from "../../interfaces";
import { ServiceHelpers } from "../service.helpers";
@Injectable()
@ -27,4 +27,8 @@ export class SonarrService extends ServiceHelpers {
public getQualityProfilesWithoutSettings(): Observable<ISonarrProfile[]> {
return this.http.get<ISonarrProfile[]>(`${this.url}/Profiles/`, {headers: this.headers});
}
public getV3LanguageProfiles(settings: ISonarrSettings): Observable<ILanguageProfiles[]> {
return this.http.post<ILanguageProfiles[]>(`${this.url}/v3/languageprofiles/`, JSON.stringify(settings), {headers: this.headers});
}
}

@ -15,3 +15,4 @@ export * from "./mobile.service";
export * from "./notificationMessage.service";
export * from "./recentlyAdded.service";
export * from "./vote.service";
export * from "./requestretry.service";

@ -0,0 +1,21 @@
import { PlatformLocation } from "@angular/common";
import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs";
import { IFailedRequestsViewModel } from "../interfaces";
import { ServiceHelpers } from "./service.helpers";
@Injectable()
export class RequestRetryService extends ServiceHelpers {
constructor(http: HttpClient, public platformLocation: PlatformLocation) {
super(http, "/api/v1/requestretry/", platformLocation);
}
public getFailedRequests(): Observable<IFailedRequestsViewModel[]> {
return this.http.get<IFailedRequestsViewModel[]>(this.url, {headers: this.headers});
}
public deleteFailedRequest(failedId: number): Observable<boolean> {
return this.http.delete<boolean>(`${this.url}/${failedId}`, {headers: this.headers});
}
}

@ -31,7 +31,6 @@ import {
ISlackNotificationSettings,
ISonarrSettings,
ITelegramNotifcationSettings,
IThemes,
IUpdateSettings,
IUserManagementSettings,
IVoteSettings,
@ -135,14 +134,6 @@ export class SettingsService extends ServiceHelpers {
return this.http.post<boolean>(`${this.url}/customization`, JSON.stringify(settings), {headers: this.headers});
}
public getThemes(): Observable<IThemes[]> {
return this.http.get<IThemes[]>(`${this.url}/themes`, {headers: this.headers});
}
public getThemeContent(themeUrl: string): Observable<string> {
return this.http.get(`${this.url}/themecontent?url=${themeUrl}`, {responseType: "text", headers: this.headers});
}
public getEmailNotificationSettings(): Observable<IEmailNotificationSettings> {
return this.http.get<IEmailNotificationSettings>(`${this.url}/notifications/email`, {headers: this.headers});
}

@ -47,15 +47,6 @@
</div>
</div>
<div class="form-group">
<label for="customLink" class="control-label">Custom CSS Link</label>
<div>
<input type="text" [(ngModel)]="settings.customCssLink" class="form-control form-control-custom " name="customLink" value="{{settings.customCssLink}}"
tooltipPosition="top" pTooltip="A link to a CSS file, you can use this to use your own styles for Ombi">
</div>
</div>
<div class="form-group">
<div class="checkbox">
<input type="checkbox" id="enableCustomDonations" name="enableCustomDonations" [(ngModel)]="settings.enableCustomDonations">
@ -63,7 +54,7 @@
</div>
</div>
<div class="form-group">
<div class="form-group" *ngIf="settings.enableCustomDonations">
<label for="customDonation" class="control-label">Custom Donation URL</label>
<div>
<input [disabled]="!settings.enableCustomDonations" type="text" [(ngModel)]="settings.customDonationUrl" class="form-control form-control-custom " name="customDonation" value="{{settings.customDonationUrl}}"
@ -71,7 +62,7 @@
</div>
</div>
<div class="form-group">
<div class="form-group" *ngIf="settings.enableCustomDonations">
<label for="customDonationMessage" class="control-label">Donation Button Message</label>
<div>
<input [disabled]="!settings.enableCustomDonations" type="text" [(ngModel)]="settings.customDonationMessage" class="form-control form-control-custom " name="customDonationMessage" value="{{settings.customDonationMessage}}"
@ -89,21 +80,15 @@
</div>
<div class="col-md-7">
<div *ngIf="themes">
<div>
<div class="form-group">
<label for="presetTheme" class="control-label">Preset Themes</label>
<div id="presetTheme">
<select class="form-control form-control-custom" (change)="dropDownChange($event)">
<option *ngFor="let theme of themes" value="{{theme.fullName}}" [selected]="settings.presetThemeName === theme.fullName">{{theme.displayName}} {{theme.version}}</option>
</select>
</div>
<label for="customCss" class="control-label">Custom CSS</label>
</div>
<div class="form-group" *ngIf="settings.presetThemeContent">
<textarea rows="25" type="text" class="form-control-custom form-control " id="themeContent" name="themeContent" [(ngModel)]="settings.presetThemeContent"> {{settings.presetThemeContent}} </textarea>
<div class="form-group language-css" pCode>
<textarea rows="25" type="text"
pTooltip="Enter your custom styles here" tooltipPosition="top"
class="form-control-custom form-control " id="themeContent" name="themeContent" [(ngModel)]="settings.customCss"> {{settings.customCss}} </textarea>
</div>
<small>Preset themes are powered by
<a href="https://github.com/leram84/layer.Cake" target="_blank">layer#Cake</a>.
</small>
</div>
</div>

@ -1,6 +1,6 @@
import { Component, OnInit } from "@angular/core";
import { ICustomizationSettings, IThemes } from "../../interfaces";
import { ICustomizationSettings } from "../../interfaces";
import { NotificationService } from "../../services";
import { SettingsService } from "../../services";
@ -10,7 +10,6 @@ import { SettingsService } from "../../services";
export class CustomizationComponent implements OnInit {
public settings: ICustomizationSettings;
public themes: IThemes[];
public advanced: boolean;
constructor(private settingsService: SettingsService, private notificationService: NotificationService) { }
@ -18,26 +17,6 @@ export class CustomizationComponent implements OnInit {
public ngOnInit() {
this.settingsService.getCustomization().subscribe(x => {
this.settings = x;
this.settingsService.getThemes().subscribe(t => {
this.themes = t;
const existingTheme = this.themes.filter((item) => {
return item.fullName === this.settings.presetThemeName;
})[0];
if (existingTheme) {
const index = this.themes.indexOf(existingTheme, 0);
if (index > -1) {
this.themes.splice(index, 1);
}
}
if (x.hasPresetTheme) {
this.themes.unshift({displayName: x.presetThemeDisplayName, fullName: x.presetThemeName, url: existingTheme.url, version: x.presetThemeVersion});
this.themes.unshift({displayName: "None", fullName: "None", url: "", version: ""});
} else {
this.themes.unshift({displayName: "Please Select", fullName: "-1", url: "-1", version: ""});
}
});
});
}
@ -62,26 +41,4 @@ export class CustomizationComponent implements OnInit {
});
}
public dropDownChange(event: any): void {
const selectedThemeFullName = <string> event.target.value;
const selectedTheme = this.themes.filter((val) => {
return val.fullName === selectedThemeFullName;
})[0];
if (selectedTheme.fullName === this.settings.presetThemeName) {
return;
}
if (selectedTheme.fullName === "None" || selectedTheme.fullName === "-1") {
this.settings.presetThemeName = "";
this.settings.presetThemeContent = "";
return;
}
this.settings.presetThemeName = selectedThemeFullName;
this.settingsService.getThemeContent(selectedTheme.url).subscribe(x => {
this.settings.presetThemeContent = x;
});
}
}

@ -0,0 +1,26 @@

<settings-menu></settings-menu>
<table class="table table-striped table-hover table-responsive table-condensed">
<thead>
<tr>
<td>Title</td>
<td>Type</td>
<td>Retry Count</td>
<td>Error Description</td>
<td>Delete</td>
</tr>
</thead>
<tbody>
<tr *ngFor="let v of vm">
<td class="vcenter">
{{v.title}}
</td>
<td>{{RequestType[v.type] | humanize}}</td>
<td class="vcenter">{{v.retryCount}}</td>
<td class="vcenter"> <i [pTooltip]="v.error" class="fa fa-info-circle"></i></td>
<td class="vcenter"><button class="btn btn-sm btn-danger-outline" (click)="remove(v)">Remove</button></td>
</tr>
</tbody>
</table>

@ -0,0 +1,27 @@
import { Component, OnInit } from "@angular/core";
import { IFailedRequestsViewModel, RequestType } from "../../interfaces";
import { RequestRetryService } from "../../services";
@Component({
templateUrl: "./failedrequest.component.html",
})
export class FailedRequestsComponent implements OnInit {
public vm: IFailedRequestsViewModel[];
public RequestType = RequestType;
constructor(private retry: RequestRetryService) { }
public ngOnInit() {
this.retry.getFailedRequests().subscribe(x => this.vm = x);
}
public remove(failed: IFailedRequestsViewModel) {
this.retry.deleteFailedRequest(failed.failedId).subscribe(x => {
if(x) {
const index = this.vm.indexOf(failed);
this.vm.splice(index,1);
}
});
}
}

@ -48,6 +48,13 @@
<small *ngIf="form.get('automaticUpdater').hasError('required')" class="error-text">The Automatic Update is required</small>
<button type="button" class="btn btn-sm btn-primary-outline" (click)="testCron(form.get('automaticUpdater')?.value)">Test</button>
</div>
<div class="form-group">
<label for="retryRequests" class="control-label">Retry Failed Requests</label>
<input type="text" class="form-control form-control-custom" [ngClass]="{'form-error': form.get('retryRequests').hasError('required')}" id="retryRequests" name="retryRequests" formControlName="retryRequests">
<small *ngIf="form.get('retryRequests').hasError('required')" class="error-text">The Retry Requests is required</small>
<button type="button" class="btn btn-sm btn-primary-outline" (click)="testCron(form.get('retryRequests')?.value)">Test</button>
</div>
</div>
<div class="col-md-6">
<div class="form-group">

@ -36,6 +36,7 @@ export class JobsComponent implements OnInit {
plexRecentlyAddedSync: [x.plexRecentlyAddedSync, Validators.required],
lidarrArtistSync: [x.lidarrArtistSync, Validators.required],
issuesPurge: [x.issuesPurge, Validators.required],
retryRequests: [x.retryRequests, Validators.required],
});
});
}

@ -19,25 +19,28 @@
<div class="form-group">
<label for="Ip" class="control-label">Hostname or IP</label>
<label for="Ip" class="control-label">Hostname or IP
<i *ngIf="form.get('ip').hasError('required')" class="fa fa-exclamation-circle error-text" pTooltip="IP/Hostname is required"></i>
</label>
<input type="text" class="form-control form-control-custom " id="Ip" name="Ip" placeholder="localhost" formControlName="ip" [ngClass]="{'form-error': form.get('ip').hasError('required')}">
<small *ngIf="form.get('ip').hasError('required')" class="error-text">The IP/Hostname is required</small>
</div>
<div class="form-group">
<label for="portNumber" class="control-label">Port</label>
<label for="portNumber" class="control-label">Port
<i *ngIf="form.get('port').hasError('required')" class="fa fa-exclamation-circle error-text" pTooltip="Port is required"></i></label>
<input type="text" class="form-control form-control-custom " formControlName="port" id="portNumber" name="Port" placeholder="Port Number" [ngClass]="{'form-error': form.get('port').hasError('required')}">
<small *ngIf="form.get('port').hasError('required')" class="error-text">The Port is required</small>
</div>
<div class="form-group">
<label for="ApiKey" class="control-label">API Key</label>
<label for="ApiKey" class="control-label">API Key <i *ngIf="form.get('apiKey').hasError('required')" class="fa fa-exclamation-circle error-text" pTooltip="API Key is required"></i></label>
<input type="text" class="form-control form-control-custom " [ngClass]="{'form-error': form.get('apiKey').hasError('required')}" id="ApiKey" name="ApiKey" formControlName="apiKey">
<small *ngIf="form.get('apiKey').hasError('required')" class="error-text">The API Key is required</small>
</div>
<div class="form-group">
<div class="checkbox">
@ -56,19 +59,22 @@
<div class="col-md-6">
<div class="form-group">
<label for="select" class="control-label">Quality Profiles</label>
<label for="select" class="control-label">Quality Profiles
<i *ngIf="form.get('defaultQualityProfile').hasError('required')" class="fa fa-exclamation-circle error-text" pTooltip="Quality Profile is required"></i>
</label>
<div id="profiles">
<select formControlName="defaultQualityProfile" class="form-control form-control-custom col-md-5 form-half" id="select" [ngClass]="{'form-error': form.get('defaultQualityProfile').hasError('required')}">
<option *ngFor="let quality of qualities" value="{{quality.id}}">{{quality.name}}</option>
</select>
<button (click)="getProfiles(form)" type="button" class="btn btn-primary-outline col-md-4 col-md-push-1">Get Quality Profiles <span *ngIf="profilesRunning" class="fa fa-spinner fa-spin"> </span></button>
<small *ngIf="form.get('defaultQualityProfile').hasError('required')" class="error-text">A Default Quality Profile is required</small>
</div>
</div>
<div class="form-group">
<label for="rootFolders" class="control-label">Default Root Folders</label>
<label for="rootFolders" class="control-label">Default Root Folders
<i *ngIf="form.get('defaultRootPath').hasError('required')" class="fa fa-exclamation-circle error-text" pTooltip="Root Path is required"></i>
</label>
<div id="rootFolders">
<select formControlName="defaultRootPath" class="form-control form-control-custom col-md-5 form-half" [ngClass]="{'form-error': form.get('defaultRootPath').hasError('required')}">
<option *ngFor="let folder of rootFolders" value="{{folder.path}}" >{{folder.path}}</option>
@ -76,12 +82,14 @@
<button (click)="getRootFolders(form)" type="button" class="btn btn-primary-outline col-md-4 col-md-push-1">Get Root Folders <span *ngIf="rootFoldersRunning" class="fa fa-spinner fa-spin"></span></button>
</div>
<small *ngIf="form.get('defaultRootPath').hasError('required')" class="error-text">A Default Root Path is required</small>
</div>
<div class="form-group">
<label for="languageProfileId" class="control-label">Language Profile</label>
<label for="languageProfileId" class="control-label">Language Profile
<i *ngIf="form.get('languageProfileId').hasError('required')" class="fa fa-exclamation-circle error-text" pTooltip="Language Profile is required"></i>
</label>
<div id="languageProfileId">
<select formControlName="languageProfileId" class="form-control form-control-custom col-md-5 form-half" [ngClass]="{'form-error': form.get('languageProfileId').hasError('required')}">
<option *ngFor="let folder of languageProfiles" value="{{folder.id}}" >{{folder.name}}</option>
@ -89,12 +97,14 @@
<button (click)="getLanguageProfiles(form)" type="button" class="btn btn-primary-outline col-md-4 col-md-push-1">Get Languages <span *ngIf="rootFoldersRunning" class="fa fa-spinner fa-spin"></span></button>
</div>
<small *ngIf="form.get('languageProfileId').hasError('required')" class="error-text">A Language profile is required</small>
</div>
<div class="form-group">
<label for="metadataProfileId" class="control-label">Metadata Profile</label>
<label for="metadataProfileId" class="control-label">Metadata Profile
<i *ngIf="form.get('metadataProfileId').hasError('required')" class="fa fa-exclamation-circle error-text" pTooltip="Metadata Profile is required"></i>
</label>
<div id="metadataProfileId">
<select formControlName="metadataProfileId" class="form-control form-control-custom col-md-5 form-half" [ngClass]="{'form-error': form.get('metadataProfileId').hasError('required')}">
@ -102,7 +112,6 @@
</select>
<button (click)="getMetadataProfiles(form)" type="button" class="btn btn-primary-outline col-md-4 col-md-push-1">Get Metadata <span *ngIf="rootFoldersRunning" class="fa fa-spinner fa-spin"></span></button>
<small *ngIf="form.get('metadataProfileId').hasError('required')" class="error-text">A Metadata profile is required</small>
</div>
</div>

@ -1,5 +1,4 @@

<settings-menu></settings-menu>
<settings-menu></settings-menu>
<div *ngIf="form">
<fieldset>
<legend>Radarr Settings</legend>
@ -19,25 +18,34 @@
<div class="form-group">
<label for="Ip" class="control-label">Hostname or IP</label>
<label for="Ip" class="control-label">Hostname or IP
<i *ngIf="form.get('ip').hasError('required')" class="fa fa-exclamation-circle error-text"
pTooltip="IP/Hostname is required"></i>
</label>
<input type="text" class="form-control form-control-custom " id="Ip" name="Ip" placeholder="localhost" formControlName="ip" [ngClass]="{'form-error': form.get('ip').hasError('required')}">
<small *ngIf="form.get('ip').hasError('required')" class="error-text">The IP/Hostname is required</small>
<input type="text" class="form-control form-control-custom " id="Ip" name="Ip" placeholder="localhost"
formControlName="ip" [ngClass]="{'form-error': form.get('ip').hasError('required')}">
</div>
<div class="form-group">
<label for="portNumber" class="control-label">Port</label>
<label for="portNumber" class="control-label">Port
<i *ngIf="form.get('port').hasError('required')" class="fa fa-exclamation-circle error-text"
pTooltip="Port is required"></i>
</label>
<input type="text" class="form-control form-control-custom " formControlName="port" id="portNumber" name="Port" placeholder="Port Number" [ngClass]="{'form-error': form.get('port').hasError('required')}">
<small *ngIf="form.get('port').hasError('required')" class="error-text">The Port is required</small>
<input type="text" class="form-control form-control-custom " formControlName="port" id="portNumber"
name="Port" placeholder="Port Number" [ngClass]="{'form-error': form.get('port').hasError('required')}">
</div>
<div class="form-group">
<label for="ApiKey" class="control-label">API Key</label>
<input type="text" class="form-control form-control-custom " [ngClass]="{'form-error': form.get('apiKey').hasError('required')}" id="ApiKey" name="ApiKey" formControlName="apiKey">
<small *ngIf="form.get('apiKey').hasError('required')" class="error-text">The API Key is required</small>
<label for="ApiKey" class="control-label">API Key
<i *ngIf="form.get('apiKey').hasError('required')" class="fa fa-exclamation-circle error-text"
pTooltip="Api Key is required"></i>
</label>
<input type="text" class="form-control form-control-custom " [ngClass]="{'form-error': form.get('apiKey').hasError('required')}"
id="ApiKey" name="ApiKey" formControlName="apiKey">
</div>
<div class="form-group">
<div class="checkbox">
@ -49,63 +57,73 @@
<div class="form-group">
<label for="SubDir" class="control-label">Base Url</label>
<div>
<input type="text" class="form-control form-control-custom" formControlName="subDir" id="SubDir" name="SubDir">
<input type="text" class="form-control form-control-custom" formControlName="subDir" id="SubDir"
name="SubDir">
</div>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<div>
<button (click)="getProfiles(form)" type="button" class="btn btn-primary-outline">Get Quality Profiles <span *ngIf="profilesRunning" class="fa fa-spinner fa-spin"> </span></button>
</div>
</div>
<div class="form-group">
<label for="select" class="control-label">Quality Profiles</label>
<label for="select" class="control-label">Quality Profiles
<i *ngIf="form.get('defaultQualityProfile').hasError('required')" class="fa fa-exclamation-circle error-text"
pTooltip="Quality Profile is required"></i>
</label>
<div id="profiles">
<select formControlName="defaultQualityProfile" class="form-control form-control-custom" id="select" [ngClass]="{'form-error': form.get('defaultQualityProfile').hasError('required')}">
<select formControlName="defaultQualityProfile" class="form-control form-control-custom col-md-5 form-half"
id="select" [ngClass]="{'form-error': form.get('defaultQualityProfile').hasError('required')}">
<option *ngFor="let quality of qualities" value="{{quality.id}}">{{quality.name}}</option>
</select>
<button (click)="getProfiles(form)" type="button" class="btn btn-primary-outline col-md-4 col-md-push-1">Get
Quality Profiles <span *ngIf="profilesRunning" class="fa fa-spinner fa-spin"> </span></button>
</div>
<small *ngIf="form.get('defaultQualityProfile').hasError('required')" class="error-text">A Default Quality Profile is required</small>
</div>
<div class="form-group">
<div>
<button (click)="getRootFolders(form)" type="button" class="btn btn-primary-outline">Get Root Folders <span *ngIf="rootFoldersRunning" class="fa fa-spinner fa-spin"></span></button>
</div>
</div>
<div class="form-group">
<label for="rootFolders" class="control-label">Default Root Folders</label>
<label for="rootFolders" class="control-label">Default Root Folders
<i *ngIf="form.get('defaultRootPath').hasError('required')" class="fa fa-exclamation-circle error-text"
pTooltip="Root Path is required"></i>
</label>
<div id="rootFolders">
<select formControlName="defaultRootPath" class="form-control form-control-custom" [ngClass]="{'form-error': form.get('defaultRootPath').hasError('required')}">
<option *ngFor="let folder of rootFolders" value="{{folder.path}}" >{{folder.path}}</option>
<select formControlName="defaultRootPath" class="form-control form-control-custom col-md-5 form-half"
[ngClass]="{'form-error': form.get('defaultRootPath').hasError('required')}">
<option *ngFor="let folder of rootFolders" value="{{folder.path}}">{{folder.path}}</option>
</select>
</div>
<small *ngIf="form.get('defaultRootPath').hasError('required')" class="error-text">A Default Root Path is required</small>
<button (click)="getRootFolders(form)" type="button" class="btn btn-primary-outline col-md-4 col-md-push-1">Get
Root Folders <span *ngIf="rootFoldersRunning" class="fa fa-spinner fa-spin"></span></button>
</div>
</div>
<div class="form-group">
<label for="rootFolders" class="control-label">Default Minimum Availability</label>
<label for="rootFolders" class="control-label">Default Minimum Availability
<i *ngIf="form.get('minimumAvailability').hasError('required')" class="fa fa-exclamation-circle error-text"
pTooltip="Minimum Availability is required"></i>
</label>
<div id="rootFolders">
<select formControlName="minimumAvailability" class="form-control form-control-custom" [ngClass]="{'form-error': form.get('minimumAvailability').hasError('required')}">
<select formControlName="minimumAvailability" class="form-control form-control-custom col-md-5 form-half"
[ngClass]="{'form-error': form.get('minimumAvailability').hasError('required')}">
<option *ngFor="let min of minimumAvailabilityOptions" value="{{min.value}}">{{min.name}}</option>
</select>
</div>
<small *ngIf="form.get('minimumAvailability').hasError('required')" type="button" class="error-text">A Default Minimum Availability is required</small>
</div>
</div>
<div class="col-md-6">
<div class="form-group" *ngIf="advanced" style="color:#ff761b">
<div class="checkbox">
<input type="checkbox" id="addOnly" formControlName="addOnly">
<label for="addOnly">Do not search</label>
</div>
</div>
<div class="form-group">
<div>
<button type="button" [disabled]="form.invalid" (click)="test(form)" class="btn btn-primary-outline">Test Connectivity <span id="spinner"></span></button>
<button type="button" [disabled]="form.invalid" (click)="test(form)" class="btn btn-primary-outline">Test
Connectivity <span id="spinner"></span></button>
</div>
</div>
@ -118,4 +136,4 @@
</div>
</form>
</fieldset>
</div>
</div>

@ -9,7 +9,7 @@ import { AuthGuard } from "../auth/auth.guard";
import { AuthService } from "../auth/auth.service";
import {
CouchPotatoService, EmbyService, IssuesService, JobService, LidarrService, MobileService, NotificationMessageService, PlexService, RadarrService,
SonarrService, TesterService, ValidationService,
RequestRetryService, SonarrService, TesterService, ValidationService,
} from "../services";
import { PipeModule } from "../pipes/pipe.module";
@ -19,6 +19,7 @@ import { CouchPotatoComponent } from "./couchpotato/couchpotato.component";
import { CustomizationComponent } from "./customization/customization.component";
import { DogNzbComponent } from "./dognzb/dognzb.component";
import { EmbyComponent } from "./emby/emby.component";
import { FailedRequestsComponent } from "./failedrequests/failedrequests.component";
import { IssuesComponent } from "./issues/issues.component";
import { JobsComponent } from "./jobs/jobs.component";
import { LandingPageComponent } from "./landingpage/landingpage.component";
@ -77,6 +78,7 @@ const routes: Routes = [
{ path: "Newsletter", component: NewsletterComponent, canActivate: [AuthGuard] },
{ path: "Lidarr", component: LidarrComponent, canActivate: [AuthGuard] },
{ path: "Vote", component: VoteComponent, canActivate: [AuthGuard] },
{ path: "FailedRequests", component: FailedRequestsComponent, canActivate: [AuthGuard] },
];
@NgModule({
@ -130,6 +132,7 @@ const routes: Routes = [
NewsletterComponent,
LidarrComponent,
VoteComponent,
FailedRequestsComponent,
],
exports: [
RouterModule,
@ -149,6 +152,7 @@ const routes: Routes = [
MobileService,
NotificationMessageService,
LidarrService,
RequestRetryService,
],
})

@ -54,7 +54,7 @@
<i class="fa fa-music" aria-hidden="true"></i> Music <span class="caret"></span>
</a>
<ul class="dropdown-menu">
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Lidarr']">Lidarr (beta)</a></li>
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Lidarr']">Lidarr</a></li>
</ul>
</li>
@ -84,6 +84,7 @@
</a>
<ul class="dropdown-menu">
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/About']">About</a></li>
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/FailedRequests']">Failed Requests</a></li>
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Update']">Update</a></li>
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Jobs']">Jobs</a></li>
<!-- <li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Logs']">Logs (Not available)</a></li>

@ -1,5 +1,4 @@

<settings-menu></settings-menu>
<settings-menu></settings-menu>
<div *ngIf="form">
<fieldset>
<legend>Sonarr Settings</legend>
@ -10,33 +9,53 @@
<form novalidate [formGroup]="form" (ngSubmit)="onSubmit(form)" style="padding-top:5%;">
<div class="col-md-6">
<div class="form-group">
<div class="checkbox">
<input type="checkbox" id="enable" formControlName="enabled">
<label for="enable">Enable</label>
<div class="col-md-6">
<div class="form-group">
<div class="checkbox">
<input type="checkbox" id="enable" formControlName="enabled">
<label for="enable">Enable</label>
</div>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<div class="checkbox">
<input type="checkbox" id="v3" formControlName="v3">
<label for="v3">V3</label>
</div>
</div>
</div>
<div class="form-group">
<label for="Ip" class="control-label">Sonarr Hostname or IP</label>
<input type="text" class="form-control form-control-custom " formControlName="ip" id="Ip" name="Ip" placeholder="localhost" [ngClass]="{'form-error': form.get('ip').hasError('required')}">
<small *ngIf="form.get('ip').hasError('required')" class="error-text">The IP/Hostname is required</small>
<label for="Ip" class="control-label">Sonarr Hostname or IP
<i *ngIf="form.get('ip').hasError('required')" class="fa fa-exclamation-circle error-text" pTooltip="The IP/Hostname is required"></i>
</label>
<input type="text" class="form-control form-control-custom " formControlName="ip" id="Ip" name="Ip"
placeholder="localhost" [ngClass]="{'form-error': form.get('ip').hasError('required')}">
</div>
<div class="form-group">
<label for="portNumber" class="control-label">Port</label>
<label for="portNumber" class="control-label">Port
<i *ngIf="form.get('port').hasError('required')" class="fa fa-exclamation-circle error-text" pTooltip="The Port is required"></i>
</label>
<input type="text" class="form-control form-control-custom " [ngClass]="{'form-error': form.get('port').hasError('required')}" formControlName="port" id="portNumber" name="Port" placeholder="Port Number">
<small *ngIf="form.get('port').hasError('required')" class="error-text">The Port is required</small>
<input type="text" class="form-control form-control-custom " [ngClass]="{'form-error': form.get('port').hasError('required')}"
formControlName="port" id="portNumber" name="Port" placeholder="Port Number">
</div>
<div class="form-group">
<label for="ApiKey" class="control-label">Sonarr API Key</label>
<input type="text" class="form-control form-control-custom " [ngClass]="{'form-error': form.get('apiKey').hasError('required')}" formControlName="apiKey" id="ApiKey" name="ApiKey">
<small *ngIf="form.get('apiKey').hasError('required')" class="error-text">The API Key is required</small>
<label for="ApiKey" class="control-label">Sonarr API Key
<i *ngIf="form.get('apiKey').hasError('required')" class="fa fa-exclamation-circle error-text" pTooltip="The API Key is required"></i>
</label>
<input type="text" class="form-control form-control-custom " [ngClass]="{'form-error': form.get('apiKey').hasError('required')}"
formControlName="apiKey" id="ApiKey" name="ApiKey">
</div>
<div class="form-group">
<div class="checkbox">
@ -48,63 +67,82 @@
<div class="form-group">
<label for="SubDir" class="control-label">Sonarr Base Url</label>
<div>
<input type="text" class="form-control form-control-custom" formControlName="subDir" id="SubDir" name="SubDir">
<input type="text" class="form-control form-control-custom" formControlName="subDir" id="SubDir"
name="SubDir">
</div>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<div>
<button type="button" (click)="getProfiles(form)" class="btn btn-primary-outline">Get Quality Profiles <span *ngIf="profilesRunning" class="fa fa-spinner fa-spin"></span></button>
<div class="form-group col-md-12">
<label for="profiles" class="control-label">Quality Profiles
<i *ngIf="form.get('qualityProfile').hasError('required')" class="fa fa-exclamation-circle error-text" pTooltip="A Default Quality Profile is required"></i>
</label>
<div id="profiles">
<select class="form-control form-control-custom col-md-5 form-half" [ngClass]="{'form-error': form.get('qualityProfile').hasError('required')}"
id="select" formControlName="qualityProfile">
<option *ngFor="let quality of qualities" value="{{quality.id}}">{{quality.name}}</option>
</select>
<button type="button" (click)="getProfiles(form)" class="btn btn-primary-outline col-md-4 col-md-push-1">
Load Qualities <span *ngIf="profilesRunning" class="fa fa-spinner fa-spin"></span></button>
</div>
</div>
</div>
<div class="form-group">
<label for="select" class="control-label">Quality Profiles</label>
<div id="profiles">
<select class="form-control form-control-custom" [ngClass]="{'form-error': form.get('qualityProfile').hasError('required')}" id="select" formControlName="qualityProfile">
<option *ngFor="let quality of qualities" value="{{quality.id}}" >{{quality.name}}</option>
</select>
</div>
<small *ngIf="form.get('qualityProfile').hasError('required')" class="error-text">A Default Quality Profile is required</small>
</div>
<div class="form-group">
<div class="form-group col-md-12">
<label for="select" class="control-label">Quality Profiles (Anime)</label>
<div id="qualityProfileAnime">
<select class="form-control form-control-custom" id="qualityProfileAnime" formControlName="qualityProfileAnime">
<option *ngFor="let quality of qualities" value="{{quality.id}}" >{{quality.name}}</option>
<select class="form-control form-control-custom col-md-5 form-half" id="qualityProfileAnime" formControlName="qualityProfileAnime">
<option *ngFor="let quality of qualities" value="{{quality.id}}">{{quality.name}}</option>
</select>
</div>
</div>
<div class="form-group">
<div>
<button type="button" (click)="getRootFolders(form)" class="btn btn-primary-outline">Get Root Folders <span *ngIf="rootFoldersRunning" class="fa fa-spinner fa-spin"></span></button>
</div>
</div>
<div class="form-group">
<label for="rootFolders" class="control-label">Default Root Folders</label>
<div class="form-group col-md-12">
<label for="rootFolders" class="control-label">Default Root Folders
<i *ngIf="form.get('rootPath').hasError('required')" class="fa fa-exclamation-circle error-text" pTooltip="A Default Root Path is required"></i>
</label>
<div id="rootFolders">
<select class="form-control form-control-custom" formControlName="rootPath" [ngClass]="{'form-error': form.get('rootPath').hasError('required')}">
<select class="form-control form-control-custom col-md-5 form-half" formControlName="rootPath"
[ngClass]="{'form-error': form.get('rootPath').hasError('required')}">
<option *ngFor="let folder of rootFolders" value="{{folder.id}}">{{folder.path}}</option>
</select>
<button type="button" (click)="getRootFolders(form)" class="btn btn-primary-outline col-md-4 col-md-push-1">
Load Folders <span *ngIf="rootFoldersRunning" class="fa fa-spinner fa-spin"></span></button>
</div>
<small *ngIf="form.get('rootPath').hasError('required')" class="error-text">A Default Root Path is required</small>
</div>
<div class="form-group">
<div class="form-group col-md-12">
<label for="rootFoldersAnime" class="control-label">Default Root Folders (Anime)</label>
<div id="rootFoldersAnime">
<select class="form-control form-control-custom" formControlName="rootPathAnime">
<select class="form-control form-control-custom col-md-5 form-half" formControlName="rootPathAnime">
<option *ngFor="let folder of rootFoldersAnime" value="{{folder.id}}">{{folder.path}}</option>
</select>
</div>
</div>
<div class="form-group col-md-12" *ngIf="form.controls.v3.value">
<label for="select" class="control-label">Language Profiles
<i *ngIf="form.get('languageProfile').hasError('required')" class="fa fa-exclamation-circle error-text" pTooltip="A Language Profile is required"></i>
</label>
<div id="langaugeProfile">
<select formControlName="languageProfile" class="form-control form-control-custom col-md-5 form-half"
id="select" [ngClass]="{'form-error': form.get('languageProfile').hasError('required')}">
<option *ngFor="let lang of languageProfiles" [ngValue]="lang.id">{{lang.name}}</option>
</select>
<button (click)="getLanguageProfiles(form)" type="button" class="btn btn-primary-outline col-md-4 col-md-push-1">Load
Languages <span *ngIf="langRunning" class="fa fa-spinner fa-spin"> </span></button>
<div class="form-group">
</div>
</div>
<div class="form-group col-md-12">
<div class="checkbox">
<input type="checkbox" id="SeasonFolders" name="SeasonFolders" formControlName="seasonFolders">
<label for="SeasonFolders">Enable season folders</label>
@ -112,26 +150,27 @@
<label>Enabled Season Folders to organize seasons into individual folders within a show.</label>
</div>
<div class="form-group" *ngIf="advanced" style="color:#ff761b">
<div class="form-group col-md-12" *ngIf="advanced" style="color:#ff761b">
<div class="checkbox">
<input type="checkbox" id="addOnly" formControlName="addOnly">
<label for="addOnly">Do not search</label>
</div>
</div>
<div class="form-group">
<div>
<button type="button" (click)="test(form)" class="btn btn-primary-outline">Test Connectivity <span id="spinner"> </span></button>
<div class="form-group col-md-6">
<div>
<button type="submit" class="btn btn-primary-outline ">Submit</button>
</div>
</div>
</div>
<div class="form-group">
<div class="form-group col-md-6">
<div>
<button type="submit" class="btn btn-primary-outline ">Submit</button>
<button type="button" (click)="test(form)" class="btn btn-primary-outline">Test Connectivity
<span id="spinner"> </span></button>
</div>
</div>
</div>
</form>
</fieldset>
</div>
</div>

@ -1,7 +1,7 @@
import { Component, OnInit } from "@angular/core";
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
import { ISonarrProfile, ISonarrRootFolder } from "../../interfaces";
import { ILanguageProfiles, ISonarrProfile, ISonarrRootFolder } from "../../interfaces";
import { ISonarrSettings } from "../../interfaces";
import { SonarrService } from "../../services";
@ -18,10 +18,13 @@ export class SonarrComponent implements OnInit {
public qualitiesAnime: ISonarrProfile[];
public rootFolders: ISonarrRootFolder[];
public rootFoldersAnime: ISonarrRootFolder[];
public languageProfiles: ILanguageProfiles[];
public selectedRootFolder: ISonarrRootFolder;
public selectedQuality: ISonarrProfile;
public selectedLanguageProfiles: ILanguageProfiles;
public profilesRunning: boolean;
public rootFoldersRunning: boolean;
public langRunning: boolean;
public form: FormGroup;
public advanced = false;
@ -47,6 +50,8 @@ export class SonarrComponent implements OnInit {
port: [x.port, [Validators.required]],
addOnly: [x.addOnly],
seasonFolders: [x.seasonFolders],
v3: [x.v3],
languageProfile: [x.languageProfile],
});
if (x.qualityProfile) {
@ -55,11 +60,19 @@ export class SonarrComponent implements OnInit {
if (x.rootPath) {
this.getRootFolders(this.form);
}
if(x.languageProfile) {
this.getLanguageProfiles(this.form);
}
if(x.v3) {
this.form.controls.languageProfile.setValidators([Validators.required]);
}
});
this.rootFolders = [];
this.qualities = [];
this.languageProfiles = [];
this.rootFolders.push({ path: "Please Select", id: -1 });
this.qualities.push({ name: "Please Select", id: -1 });
this.languageProfiles.push({ name: "Please Select", id: -1 });
}
public getProfiles(form: FormGroup) {
@ -88,6 +101,18 @@ export class SonarrComponent implements OnInit {
});
}
public getLanguageProfiles(form: FormGroup) {
this.langRunning = true;
this.sonarrService.getV3LanguageProfiles(form.value)
.subscribe(x => {
this.languageProfiles = x;
this.languageProfiles.unshift({ name: "Please Select", id: -1 });
this.langRunning = false;
this.notificationService.success("Successfully retrieved the Languge Profiles");
});
}
public test(form: FormGroup) {
if (form.invalid) {
this.notificationService.error("Please check your entered values");

@ -27,4 +27,4 @@ $bg-colour-disabled: #252424;
.label {
margin: 3px;
}
}

@ -843,6 +843,7 @@ body {
padding: 10px 15px;
border-bottom: 1px solid transparent;
background: $form-color;
height: 40px;
}
.card-header > a {

@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Ombi.Api.Sonarr;
using Ombi.Api.Sonarr.Models;
using Ombi.Api.Sonarr.Models.V3;
using Ombi.Attributes;
using Ombi.Core.Settings;
using Ombi.Core.Settings.Models.External;
@ -16,14 +17,16 @@ namespace Ombi.Controllers.External
[Produces("application/json")]
public class SonarrController : Controller
{
public SonarrController(ISonarrApi sonarr, ISettingsService<SonarrSettings> settings)
public SonarrController(ISonarrApi sonarr, ISonarrV3Api sonarrv3, ISettingsService<SonarrSettings> settings)
{
SonarrApi = sonarr;
SonarrV3Api = sonarrv3;
SonarrSettings = settings;
SonarrSettings.ClearCache();
}
private ISonarrApi SonarrApi { get; }
private ISonarrV3Api SonarrV3Api { get; }
private ISettingsService<SonarrSettings> SonarrSettings { get; }
/// <summary>
@ -82,5 +85,36 @@ namespace Ombi.Controllers.External
return null;
}
/// <summary>
/// Gets the Sonarr V3 language profiles
/// </summary>
/// <returns></returns>
[HttpGet("v3/LanguageProfiles")]
[PowerUser]
public async Task<IEnumerable<LanguageProfiles>> GetLanguageProfiles()
{
var settings = await SonarrSettings.GetSettingsAsync();
if (settings.Enabled)
{
return await SonarrV3Api.LanguageProfiles(settings.ApiKey, settings.FullUri);
}
return null;
}
/// <summary>
/// Gets the Sonarr V3 language profiles
/// </summary>
/// <param name="settings">The settings.</param>
/// <returns></returns>
[HttpPost("v3/LanguageProfiles")]
[PowerUser]
public async Task<IEnumerable<LanguageProfiles>> GetLanguageProfiles([FromBody] SonarrSettings settings)
{
return await SonarrV3Api.LanguageProfiles(settings.ApiKey, settings.FullUri);
}
}
}

@ -863,6 +863,7 @@ namespace Ombi.Controllers
{
var ombiUser = new OmbiUser
{
Alias = user.Alias,
Email = user.EmailAddress,
UserName = user.UserName
};

@ -0,0 +1,92 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Ombi.Store.Entities.Requests;
using Ombi.Store.Repository;
using System.Collections.Generic;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Ombi.Attributes;
using Ombi.Models;
using Ombi.Store.Entities;
using Ombi.Store.Repository.Requests;
namespace Ombi.Controllers
{
[ApiV1]
[Admin]
[Produces("application/json")]
public class RequestRetryController : Controller
{
public RequestRetryController(IRepository<RequestQueue> requestQueue, IMovieRequestRepository movieRepo,
ITvRequestRepository tvRepo, IMusicRequestRepository musicRepo)
{
_requestQueueRepository = requestQueue;
_movieRequestRepository = movieRepo;
_tvRequestRepository = tvRepo;
_musicRequestRepository = musicRepo;
}
private readonly IRepository<RequestQueue> _requestQueueRepository;
private readonly IMovieRequestRepository _movieRequestRepository;
private readonly ITvRequestRepository _tvRequestRepository;
private readonly IMusicRequestRepository _musicRequestRepository;
/// <summary>
/// Get's all the failed requests that are currently in the queue
/// </summary>
/// <returns></returns>
[HttpGet]
public async Task<IEnumerable<FailedRequestViewModel>> GetFailedRequests()
{
var failed = await _requestQueueRepository.GetAll().Where(x => !x.Completed.HasValue).ToListAsync();
var vm = new List<FailedRequestViewModel>();
foreach (var f in failed)
{
var vmModel = new FailedRequestViewModel
{
RequestId = f.RequestId,
RetryCount = f.RetryCount,
Dts = f.Dts,
Error = f.Error,
FailedId = f.Id,
Type = f.Type
};
if (f.Type == RequestType.Movie)
{
var request = await _movieRequestRepository.Find(f.RequestId);
vmModel.Title = request.Title;
vmModel.ReleaseYear = request.ReleaseDate;
}
if (f.Type == RequestType.Album)
{
var request = await _musicRequestRepository.Find(f.RequestId);
vmModel.Title = request.Title;
vmModel.ReleaseYear = request.ReleaseDate;
}
if (f.Type == RequestType.TvShow)
{
var request = await _tvRequestRepository.GetChild().Include(x => x.ParentRequest).FirstOrDefaultAsync(x => x.Id == f.RequestId);
vmModel.Title = request.Title;
vmModel.ReleaseYear = request.ParentRequest.ReleaseDate;
}
vm.Add(vmModel);
}
return vm;
}
[HttpDelete("{queueId:int}")]
public async Task<IActionResult> Delete(int queueId)
{
var queueItem = await _requestQueueRepository.GetAll().FirstOrDefaultAsync(x => x.Id == queueId);
await _requestQueueRepository.Delete(queueItem);
return Json(true);
}
}
}

@ -274,19 +274,6 @@ namespace Ombi.Controllers
return model;
}
/// <summary>
/// Gets the content of the theme available
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
[HttpGet("themecontent")]
[AllowAnonymous]
public async Task<IActionResult> GetThemeContent([FromQuery]string url)
{
var css = await _githubApi.GetThemesRawContent(url);
return Content(css, "text/css");
}
/// <summary>
/// Gets the Sonarr Settings.
/// </summary>
@ -520,6 +507,7 @@ namespace Ombi.Controllers
j.Newsletter = j.Newsletter.HasValue() ? j.Newsletter : JobSettingsHelper.Newsletter(j);
j.LidarrArtistSync = j.LidarrArtistSync.HasValue() ? j.LidarrArtistSync : JobSettingsHelper.LidarrArtistSync(j);
j.IssuesPurge = j.IssuesPurge.HasValue() ? j.IssuesPurge : JobSettingsHelper.IssuePurge(j);
j.RetryRequests = j.RetryRequests.HasValue() ? j.RetryRequests : JobSettingsHelper.ResendFailedRequests(j);
return j;
}

@ -0,0 +1,17 @@
using System;
using Ombi.Store.Entities;
namespace Ombi.Models
{
public class FailedRequestViewModel
{
public int FailedId { get; set; }
public string Title { get; set; }
public DateTime ReleaseYear { get; set; }
public int RequestId { get; set; }
public RequestType Type { get; set; }
public DateTime Dts { get; set; }
public string Error { get; set; }
public int RetryCount { get; set; }
}
}

@ -1,4 +1,5 @@
@using Ombi.Core.Settings
@using Ombi.Helpers
@using Ombi.Settings.Settings.Models
@inject ISettingsService<OmbiSettings> Settings
@inject ISettingsService<CustomizationSettings> CustomizationSettings
@ -105,29 +106,12 @@
</head>
<body>
@{
if (customization.HasPresetTheme)
if (customization.CustomCss.HasValue())
{
if (!string.IsNullOrEmpty(baseUrl))
{
if (!customization.PresetThemeContent.Contains("/" + baseUrl))
{
var index = customization.PresetThemeContent.IndexOf("/api/");
if (index > 0)
{
customization.PresetThemeContent = customization.PresetThemeContent.Insert(index, "/" + baseUrl);
}
}
}
<style>
@Html.Raw(customization.PresetThemeContent)
<style>
@Html.Raw(customization.CustomCss)
</style>
}
if (!string.IsNullOrEmpty(customization.CustomCssLink))
{
<link rel="stylesheet" href="@customization.CustomCssLink" asp-append-version="true" />
}
}
@RenderBody()

Loading…
Cancel
Save