Merge branch 'DotNetCore' of https://github.com/tidusjar/Ombi into DotNetCore

pull/1529/head
tidusjar 7 years ago
commit b69a5a2e14

@ -9,7 +9,6 @@ ____
[![Patreon](https://www.ombi.io/img/patreondonate.svg)](https://patreon.com/tidusjar/Ombi)
[![Paypal](https://www.ombi.io/img/paypaldonate.svg)](https://paypal.me/PlexRequestsNet)
[![Gratipay User](https://img.shields.io/gratipay/user/tidusjar.svg)](https://gratipay.com/Ombi/)
[![Report a bug](http://i.imgur.com/xSpw482.png)](https://github.com/tidusjar/Ombi/issues/new) [![Feature request](http://i.imgur.com/mFO0OuX.png)](http://feathub.com/tidusjar/Ombi)
@ -100,10 +99,6 @@ You may need to install libwind8.
# FAQ
Do you have an issue or a question? if so check out our [FAQ](https://github.com/tidusjar/Ombi/wiki/FAQ)!
# Docker
Looking for a Docker Image? Well [rogueosb](https://github.com/rogueosb/) has created a docker image for us, You can find it [here](https://github.com/rogueosb/docker-plexrequestsnet) :smile:
# Contributors
We are looking for any contributions to the project! Just pick up a task, if you have any questions ask and i'll get straight on it!

@ -0,0 +1,84 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
using Ombi.Api.CouchPotato.Models;
using Ombi.Helpers;
namespace Ombi.Api.CouchPotato
{
public class CouchPotatoApi : ICouchPotatoApi
{
public CouchPotatoApi(IApi api, ILogger<CouchPotatoApi> log)
{
_api = api;
_log = log;
}
private readonly IApi _api;
private readonly ILogger<CouchPotatoApi> _log;
public async Task<bool> AddMovie(string imdbid, string apiKey, string title, string baseUrl, string profileId = default(string))
{
var request = new Request($"/api/{apiKey}/movie.add", baseUrl, HttpMethod.Get);
request.AddQueryString("title", title);
request.AddQueryString("identifier", imdbid);
if (!string.IsNullOrEmpty(profileId))
{
request.AddQueryString("profile_id", profileId);
}
var obj = await _api.Request<JObject>(request);
if (obj.Count > 0)
{
try
{
var result = (bool)obj["success"];
return result;
}
catch (Exception e)
{
_log.LogError(LoggingEvents.CouchPotatoApi, e, "Error calling AddMovie");
return false;
}
}
return false;
}
public async Task<CouchPotatoStatus> Status(string url, string apiKey)
{
var request = new Request($"api/{apiKey}/app.available/", url, HttpMethod.Get);
return await _api.Request<CouchPotatoStatus>(request);
}
public async Task<CouchPotatoProfiles> GetProfiles(string url, string apiKey)
{
var request = new Request($"api/{apiKey}/profile.list/", url, HttpMethod.Get);
return await _api.Request<CouchPotatoProfiles>(request);
}
public async Task<CouchPotatoMovies> GetMovies(string baseUrl, string apiKey, string[] status)
{
var request = new Request($"/api/{apiKey}/movie.list", baseUrl, HttpMethod.Get);
request.AddQueryString("status",string.Join(",", status));
request.OnBeforeDeserialization = json =>
{
json.Replace("[]", "{}");
};
return await _api.Request<CouchPotatoMovies>(request);
}
public async Task<CouchPotatoApiKey> GetApiKey(string baseUrl, string username, string password)
{
var request = new Request("getkey/",baseUrl, HttpMethod.Get);
request.AddQueryString("u",username.CalcuateMd5Hash());
request.AddQueryString("p",password.CalcuateMd5Hash());
return await _api.Request<CouchPotatoApiKey>(request);
}
}
}

@ -0,0 +1,14 @@
using System.Threading.Tasks;
using Ombi.Api.CouchPotato.Models;
namespace Ombi.Api.CouchPotato
{
public interface ICouchPotatoApi
{
Task<bool> AddMovie(string imdbid, string apiKey, string title, string baseUrl, string profileId = null);
Task<CouchPotatoApiKey> GetApiKey(string baseUrl, string username, string password);
Task<CouchPotatoMovies> GetMovies(string baseUrl, string apiKey, string[] status);
Task<CouchPotatoProfiles> GetProfiles(string url, string apiKey);
Task<CouchPotatoStatus> Status(string url, string apiKey);
}
}

@ -0,0 +1,12 @@
using Newtonsoft.Json;
namespace Ombi.Api.CouchPotato.Models
{
public class CouchPotatoApiKey
{
[JsonProperty("success")]
public bool success { get; set; }
[JsonProperty("api_key")]
public string ApiKey { get; set; }
}
}

@ -0,0 +1,99 @@
using System.Collections.Generic;
namespace Ombi.Api.CouchPotato.Models
{
public class CouchPotatoMovies
{
public List<Movie> movies { get; set; }
public int total { get; set; }
public bool success { get; set; }
public bool empty { get; set; }
}
public class Movie
{
public string _id { get; set; }
public string _rev { get; set; }
public string _t { get; set; }
public object category_id { get; set; }
public Files files { get; set; }
public Identifiers identifiers { get; set; }
public Info info { get; set; }
public int last_edit { get; set; }
public string profile_id { get; set; }
public List<object> releases { get; set; }
public string status { get; set; }
public List<object> tags { get; set; }
public string title { get; set; }
public string type { get; set; }
}
public class CouchPotatoAdd
{
public Movie movie { get; set; }
public bool success { get; set; }
}
public class Rating
{
public List<string> imdb { get; set; }
}
public class Images
{
public List<string> actors { get; set; }
public List<string> backdrop { get; set; }
public List<string> backdrop_original { get; set; }
public List<object> banner { get; set; }
public List<object> clear_art { get; set; }
public List<object> disc_art { get; set; }
public List<object> extra_fanart { get; set; }
public List<object> extra_thumbs { get; set; }
public List<object> landscape { get; set; }
public List<object> logo { get; set; }
public List<string> poster { get; set; }
public List<string> poster_original { get; set; }
}
public class Info
{
public List<string> actor_roles { get; set; }
public List<string> actors { get; set; }
public List<string> directors { get; set; }
public List<string> genres { get; set; }
public Images images { get; set; }
public string imdb { get; set; }
public string mpaa { get; set; }
public string original_title { get; set; }
public string plot { get; set; }
public Rating rating { get; set; }
public Release_Date release_date { get; set; }
public string released { get; set; }
public int runtime { get; set; }
public string tagline { get; set; }
public List<string> titles { get; set; }
public int tmdb_id { get; set; }
public string type { get; set; }
public bool via_imdb { get; set; }
public bool via_tmdb { get; set; }
public List<string> writers { get; set; }
public int year { get; set; }
}
public class Release_Date
{
public bool bluray { get; set; }
public int dvd { get; set; }
public int expires { get; set; }
public int theater { get; set; }
}
public class Files
{
public List<string> image_poster { get; set; }
}
public class Identifiers
{
public string imdb { get; set; }
}
}

@ -0,0 +1,29 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace Ombi.Api.CouchPotato.Models
{
public class ProfileList
{
public bool core { get; set; }
public bool hide { get; set; }
public string _rev { get; set; }
public List<bool> finish { get; set; }
public List<string> qualities { get; set; }
public string _id { get; set; }
public string _t { get; set; }
public string label { get; set; }
public int minimum_score { get; set; }
public List<int> stop_after { get; set; }
public List<object> wait_for { get; set; }
public int order { get; set; }
[JsonProperty(PropertyName = "3d")]
public List<object> threeD { get; set; }
}
public class CouchPotatoProfiles
{
public List<ProfileList> list { get; set; }
public bool success { get; set; }
}
}

@ -0,0 +1,7 @@
namespace Ombi.Api.CouchPotato.Models
{
public class CouchPotatoStatus
{
public bool success { get; set; }
}
}

@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Ombi.Api\Ombi.Api.csproj" />
</ItemGroup>
</Project>

@ -52,6 +52,7 @@ namespace Ombi.Api
var receivedString = await data.ReadAsStringAsync();
if (request.ContentType == ContentType.Json)
{
request.OnBeforeDeserialization?.Invoke(receivedString);
return JsonConvert.DeserializeObject<T>(receivedString, Settings);
}
else

@ -25,6 +25,8 @@ namespace Ombi.Api
public string BaseUrl { get; }
public HttpMethod HttpMethod { get; }
public Action<string> OnBeforeDeserialization { get; set; }
private string FullUrl
{
get

@ -64,7 +64,7 @@ namespace Ombi.Core.Engine
RequestType = RequestType.Movie,
Overview = movieInfo.Overview,
ImdbId = movieInfo.ImdbId,
PosterPath = movieInfo.PosterPath,
PosterPath = movieInfo.PosterPath.TrimStart('/'),
Title = movieInfo.Title,
ReleaseDate = !string.IsNullOrEmpty(movieInfo.ReleaseDate)
? DateTime.Parse(movieInfo.ReleaseDate)

@ -10,9 +10,7 @@ using System.Linq;
using System.Security.Principal;
using System.Threading.Tasks;
using Ombi.Core.Rule.Interfaces;
using StackExchange.Profiling;
using Microsoft.Extensions.Caching.Memory;
using Ombi.Api.Trakt;
using Ombi.Core.Authentication;
using Ombi.Helpers;
@ -55,22 +53,14 @@ namespace Ombi.Core.Engine
/// <returns></returns>
public async Task<IEnumerable<SearchMovieViewModel>> Search(string search)
{
using (MiniProfiler.Current.Step("Starting Movie Search Engine"))
using (MiniProfiler.Current.Step("Searching Movie"))
var result = await MovieApi.SearchMovie(search);
if (result != null)
{
var result = await MovieApi.SearchMovie(search);
using (MiniProfiler.Current.Step("Fin API, Transforming"))
{
if (result != null)
{
Logger.LogDebug("Search Result: {result}", result);
return await TransformMovieResultsToResponse(result.Take(10)); // Take 10 to stop us overloading the API
}
}
return null;
Logger.LogDebug("Search Result: {result}", result);
return await TransformMovieResultsToResponse(result.Take(10)); // Take 10 to stop us overloading the API
}
return null;
}
/// <summary>
@ -174,7 +164,7 @@ namespace Ombi.Core.Engine
// So set the ImdbId to viewMovie.Id and then set it back afterwards
var oldId = viewMovie.Id;
viewMovie.CustomId = viewMovie.ImdbId ?? string.Empty;
await RunSearchRules(viewMovie);
viewMovie.Id = oldId;

@ -15,11 +15,7 @@ using System.Linq;
using System.Security.Principal;
using System.Threading.Tasks;
using Ombi.Core.Rule.Interfaces;
using Ombi.Store.Entities.Requests;
using Ombi.Store.Repository.Requests;
using Ombi.Store.Entities;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Ombi.Core.Authentication;
using Ombi.Helpers;
@ -117,11 +113,7 @@ namespace Ombi.Core.Engine
});
}
}
var existingRequests = await GetTvRequests();
var plexSettings = await PlexSettings.GetSettingsAsync();
var embySettings = await EmbySettings.GetSettingsAsync();
return await ProcessResult(mapped, existingRequests, plexSettings, embySettings);
return await ProcessResult(mapped);
}
public async Task<TreeNode<SearchTvShowViewModel>> GetShowInformationTreeNode(int tvdbid)
@ -189,127 +181,21 @@ namespace Ombi.Core.Engine
};
}
private async Task<IEnumerable<SearchTvShowViewModel>> ProcessResults<T>(IEnumerable<T> items)
{
var existingRequests = await GetTvRequests();
var plexSettings = await PlexSettings.GetSettingsAsync();
var embySettings = await EmbySettings.GetSettingsAsync();
var retVal = new List<SearchTvShowViewModel>();
foreach (var tvMazeSearch in items)
{
var viewT = Mapper.Map<SearchTvShowViewModel>(tvMazeSearch);
retVal.Add(await ProcessResult(viewT, existingRequests, plexSettings, embySettings));
retVal.Add(await ProcessResult(viewT));
}
return retVal;
}
private async Task<SearchTvShowViewModel> ProcessResult(SearchTvShowViewModel item, Dictionary<int, TvRequests> existingRequests, PlexSettings plexSettings, EmbySettings embySettings)
private async Task<SearchTvShowViewModel> ProcessResult(SearchTvShowViewModel item)
{
if (embySettings.Enable)
{
var content = await EmbyContentRepo.Get(item.Id.ToString());
if (content != null)
{
item.Available = true;
}
// Let's go through the episodes now
if (item.SeasonRequests.Any())
{
var allEpisodes = EmbyContentRepo.GetAllEpisodes().Include(x => x.Series);
foreach (var season in item.SeasonRequests)
{
foreach (var episode in season.Episodes)
{
var epExists = await allEpisodes.FirstOrDefaultAsync(x =>
x.EpisodeNumber == episode.EpisodeNumber && x.SeasonNumber == season.SeasonNumber && item.Id.ToString() == x.Series.ProviderId);
if (epExists != null)
{
episode.Available = true;
}
}
}
}
}
if (plexSettings.Enable)
{
var content = await PlexContentRepo.Get(item.Id.ToString());
if (content != null)
{
item.Available = true;
item.PlexUrl = content.Url;
}
// Let's go through the episodes now
if (item.SeasonRequests.Any())
{
var allEpisodes = PlexContentRepo.GetAllEpisodes();
foreach (var season in item.SeasonRequests)
{
foreach (var episode in season.Episodes)
{
var epExists = await allEpisodes.FirstOrDefaultAsync(x =>
x.EpisodeNumber == episode.EpisodeNumber && x.SeasonNumber == season.SeasonNumber && x.Series.ProviderId == item.Id.ToString());
if (epExists != null)
{
episode.Available = true;
}
}
}
}
}
if (item.SeasonRequests.Any() && item.SeasonRequests.All(x => x.Episodes.All(e => e.Approved)))
{
item.FullyAvailable = true;
}
if (item.Id > 0)
{
var tvdbid = item.Id;
if (existingRequests.ContainsKey(tvdbid))
{
var existingRequest = existingRequests[tvdbid];
item.Requested = true;
item.Approved = existingRequest.ChildRequests.Any(x => x.Approved);
// Let's modify the seasonsrequested to reflect what we have requested...
foreach (var season in item.SeasonRequests)
{
foreach (var existingRequestChildRequest in existingRequest.ChildRequests)
{
// Find the existing request season
var existingSeason =
existingRequestChildRequest.SeasonRequests.FirstOrDefault(x => x.SeasonNumber == season.SeasonNumber);
if (existingSeason == null) continue;
foreach (var ep in existingSeason.Episodes)
{
// Find the episode from what we are searching
var episodeSearching = season.Episodes.FirstOrDefault(x => x.EpisodeNumber == ep.EpisodeNumber);
if (episodeSearching == null)
{
continue;
}
episodeSearching.Requested = true;
episodeSearching.Available = ep.Available;
episodeSearching.Approved = ep.Season.ChildRequest.Approved;
}
}
}
}
// TODO CHECK SONARR/RADARR
//if (sonarrCached.Select(x => x.TvdbId).Contains(tvdbid) || sickRageCache.Contains(tvdbid))
// // compare to the sonarr/sickrage db
//{
// item.Requested = true;
//}
}
item.CustomId = item.Id.ToString();
await RunSearchRules(item);
return item;
}

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using Ombi.Store.Entities;
namespace Ombi.Core.Models.Search
{
@ -22,5 +23,8 @@ namespace Ombi.Core.Models.Search
public string Trailer { get; set; }
public string Homepage { get; set; }
public string ImdbId { get; set; }
public int RootPathOverride { get; set; }
public int QualityOverride { get; set; }
public override RequestType Type => RequestType.Movie;
}
}

@ -1,6 +1,7 @@
using Ombi.Core.Models.Requests;
using Ombi.Store.Repository.Requests;
using System.Collections.Generic;
using Ombi.Store.Entities;
namespace Ombi.Core.Models.Search
{
@ -54,5 +55,7 @@ namespace Ombi.Core.Models.Search
/// This is where we have EVERY Episode for that series
/// </summary>
public bool FullyAvailable { get; set; }
public override RequestType Type => RequestType.TvShow;
}
}

@ -1,8 +1,9 @@
using System.ComponentModel.DataAnnotations.Schema;
using Ombi.Store.Entities;
namespace Ombi.Core.Models.Search
{
public class SearchViewModel
public abstract class SearchViewModel
{
public int Id { get; set; }
public bool Approved { get; set; }
@ -10,6 +11,7 @@ namespace Ombi.Core.Models.Search
public bool Available { get; set; }
public string PlexUrl { get; set; }
public string Quality { get; set; }
public abstract RequestType Type { get; }
/// <summary>

@ -1,9 +1,8 @@
using System.Threading.Tasks;
using Ombi.Core.Models.Requests;
namespace Ombi.Core.Rule.Interfaces
{
public interface IRules<T> where T : new()
public interface IRules<T>
{
Task<RuleResult> Execute(T obj);
}

@ -73,7 +73,7 @@ namespace Ombi.Core.Rule
}
private void GetTypes<T>(IServiceProvider provider, Assembly ass, string baseSearchType, List<IRules<T>> ruleList) where T : new()
private void GetTypes<T>(IServiceProvider provider, Assembly ass, string baseSearchType, List<IRules<T>> ruleList)
{
foreach (var ti in ass.DefinedTypes)
{

@ -1,6 +1,9 @@
using System.Threading.Tasks;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Ombi.Core.Models.Search;
using Ombi.Core.Rule.Interfaces;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
namespace Ombi.Core.Rule.Rules.Search
@ -20,6 +23,28 @@ namespace Ombi.Core.Rule.Rules.Search
if (item != null)
{
obj.Available = true;
if (obj.Type == RequestType.TvShow)
{
var searchResult = (SearchTvShowViewModel)obj;
// Let's go through the episodes now
if (searchResult.SeasonRequests.Any())
{
var allEpisodes = EmbyContentRepository.GetAllEpisodes().Include(x => x.Series);
foreach (var season in searchResult.SeasonRequests)
{
foreach (var episode in season.Episodes)
{
var epExists = await allEpisodes.FirstOrDefaultAsync(x =>
x.EpisodeNumber == episode.EpisodeNumber && x.SeasonNumber == season.SeasonNumber && item.ProviderId.ToString() == x.Series.ProviderId);
if (epExists != null)
{
episode.Available = true;
}
}
}
}
}
}
return Success();
}

@ -3,6 +3,7 @@ using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Ombi.Core.Models.Search;
using Ombi.Core.Rule.Interfaces;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
using Ombi.Store.Repository.Requests;
@ -21,28 +22,75 @@ namespace Ombi.Core.Rule.Rules.Search
public Task<RuleResult> Execute(SearchViewModel obj)
{
var movieRequests = Movie.GetRequest(obj.Id);
if (movieRequests != null) // Do we already have a request for this?
if (obj.Type == RequestType.Movie)
{
var movieRequests = Movie.GetRequest(obj.Id);
if (movieRequests != null) // Do we already have a request for this?
{
obj.Requested = true;
obj.Approved = movieRequests.Approved;
obj.Available = movieRequests.Available;
obj.Requested = true;
obj.Approved = movieRequests.Approved;
obj.Available = movieRequests.Available;
return Task.FromResult(Success());
}
return Task.FromResult(Success());
}
var tvRequests = Tv.GetRequest(obj.Id);
if (tvRequests != null) // Do we already have a request for this?
else
{
//var tvRequests = Tv.GetRequest(obj.Id);
//if (tvRequests != null) // Do we already have a request for this?
//{
// obj.Requested = true;
// obj.Approved = tvRequests.ChildRequests.Any(x => x.Approved);
// obj.Available = tvRequests.ChildRequests.Any(x => x.Available);
// return Task.FromResult(Success());
//}
var request = (SearchTvShowViewModel) obj;
var tvRequests = Tv.GetRequest(obj.Id);
if (tvRequests != null) // Do we already have a request for this?
{
request.Requested = true;
request.Approved = tvRequests.ChildRequests.Any(x => x.Approved);
// Let's modify the seasonsrequested to reflect what we have requested...
foreach (var season in request.SeasonRequests)
{
foreach (var existingRequestChildRequest in tvRequests.ChildRequests)
{
// Find the existing request season
var existingSeason =
existingRequestChildRequest.SeasonRequests.FirstOrDefault(x => x.SeasonNumber == season.SeasonNumber);
if (existingSeason == null) continue;
foreach (var ep in existingSeason.Episodes)
{
// Find the episode from what we are searching
var episodeSearching = season.Episodes.FirstOrDefault(x => x.EpisodeNumber == ep.EpisodeNumber);
if (episodeSearching == null)
{
continue;
}
episodeSearching.Requested = true;
episodeSearching.Available = ep.Available;
episodeSearching.Approved = ep.Season.ChildRequest.Approved;
}
}
}
}
if (request.SeasonRequests.Any() && request.SeasonRequests.All(x => x.Episodes.All(e => e.Approved)))
{
request.FullyAvailable = true;
}
obj.Requested = true;
obj.Approved = tvRequests.ChildRequests.Any(x => x.Approved);
obj.Available = tvRequests.ChildRequests.Any(x => x.Available);
return Task.FromResult(Success());
}
return Task.FromResult(Success());
}
}
}

@ -1,6 +1,9 @@
using System.Threading.Tasks;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Ombi.Core.Models.Search;
using Ombi.Core.Rule.Interfaces;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
namespace Ombi.Core.Rule.Rules.Search
@ -22,6 +25,29 @@ namespace Ombi.Core.Rule.Rules.Search
obj.Available = true;
obj.PlexUrl = item.Url;
obj.Quality = item.Quality;
if (obj.Type == RequestType.TvShow)
{
var search = (SearchTvShowViewModel)obj;
// Let's go through the episodes now
if (search.SeasonRequests.Any())
{
var allEpisodes = PlexContentRepository.GetAllEpisodes();
foreach (var season in search.SeasonRequests)
{
foreach (var episode in season.Episodes)
{
var epExists = await allEpisodes.FirstOrDefaultAsync(x =>
x.EpisodeNumber == episode.EpisodeNumber && x.SeasonNumber == season.SeasonNumber &&
x.Series.ProviderId == item.ProviderId.ToString());
if (epExists != null)
{
episode.Available = true;
}
}
}
}
}
}
return Success();
}

@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore;
using Ombi.Core.Models.Search;
using Ombi.Core.Rule.Interfaces;
using Ombi.Store.Context;
using Ombi.Store.Entities;
namespace Ombi.Core.Rule.Rules.Search
{
@ -18,13 +19,16 @@ namespace Ombi.Core.Rule.Rules.Search
public async Task<RuleResult> Execute(SearchViewModel obj)
{
// Check if it's in Radarr
var result = await _ctx.RadarrCache.FirstOrDefaultAsync(x => x.TheMovieDbId == obj.Id);
if (result != null)
if (obj.Type == RequestType.Movie)
{
obj.Approved = true; // It's in radarr so it's approved... Maybe have a new property called "Processing" or something?
// Check if it's in Radarr
var result = await _ctx.RadarrCache.FirstOrDefaultAsync(x => x.TheMovieDbId == obj.Id);
if (result != null)
{
obj.Approved =
true; // It's in radarr so it's approved... Maybe have a new property called "Processing" or something?
}
}
return Success();
}
}

@ -0,0 +1,34 @@
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Ombi.Core.Models.Search;
using Ombi.Core.Rule.Interfaces;
using Ombi.Store.Context;
using Ombi.Store.Entities;
namespace Ombi.Core.Rule.Rules.Search
{
public class SonarrCacheRule : BaseSearchRule, IRules<SearchViewModel>
{
public SonarrCacheRule(IOmbiContext ctx)
{
_ctx = ctx;
}
private readonly IOmbiContext _ctx;
public async Task<RuleResult> Execute(SearchViewModel obj)
{
if (obj.Type == RequestType.TvShow)
{
// Check if it's in Radarr
var result = await _ctx.SonarrCache.FirstOrDefaultAsync(x => x.TvDbId == obj.Id);
if (result != null)
{
obj.Approved =
true; // It's in radarr so it's approved... Maybe have a new property called "Processing" or something?
}
}
return Success();
}
}
}

@ -5,6 +5,6 @@ namespace Ombi.Core
{
public interface IMovieSender
{
Task<MovieSenderResult> Send(MovieRequests model, string qualityId = "");
Task<MovieSenderResult> Send(MovieRequests model);
}
}

@ -1,4 +1,5 @@
using Ombi.Core.Settings;
using System.Linq;
using Ombi.Core.Settings;
using Ombi.Settings.Settings.Models.External;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
@ -21,7 +22,7 @@ namespace Ombi.Core
private IRadarrApi RadarrApi { get; }
private ILogger<MovieSender> Log { get; }
public async Task<MovieSenderResult> Send(MovieRequests model, string qualityId = "")
public async Task<MovieSenderResult> Send(MovieRequests model)
{
//var cpSettings = await CouchPotatoSettings.GetSettingsAsync();
//var watcherSettings = await WatcherSettings.GetSettingsAsync();
@ -39,7 +40,7 @@ namespace Ombi.Core
if (radarrSettings.Enabled)
{
return await SendToRadarr(model, radarrSettings, qualityId);
return await SendToRadarr(model, radarrSettings);
}
return new MovieSenderResult
@ -49,22 +50,16 @@ namespace Ombi.Core
};
}
private async Task<MovieSenderResult> SendToRadarr(MovieRequests model, RadarrSettings settings, string qualityId)
private async Task<MovieSenderResult> SendToRadarr(MovieRequests model, RadarrSettings settings)
{
var qualityProfile = 0;
if (!string.IsNullOrEmpty(qualityId)) // try to parse the passed in quality, otherwise use the settings default quality
var qualityToUse = int.Parse(settings.DefaultQualityProfile);
if (model.QualityOverride > 0)
{
int.TryParse(qualityId, out qualityProfile);
qualityToUse = model.QualityOverride;
}
if (qualityProfile <= 0)
{
int.TryParse(settings.DefaultQualityProfile, out qualityProfile);
}
//var rootFolderPath = model.RootFolderSelected <= 0 ? settings.FullRootPath : GetRootPath(model.RootFolderSelected, settings);
var rootFolderPath = settings.DefaultRootPath; // TODO Allow changing in the UI
var result = await RadarrApi.AddMovie(model.TheMovieDbId, model.Title, model.ReleaseDate.Year, qualityProfile, rootFolderPath, settings.ApiKey, settings.FullUri, !settings.AddOnly, settings.MinimumAvailability);
var rootFolderPath = model.RootPathOverride <= 0 ? settings.DefaultRootPath : await RadarrRootPath(model.RootPathOverride, settings);
var result = await RadarrApi.AddMovie(model.TheMovieDbId, model.Title, model.ReleaseDate.Year, qualityToUse, rootFolderPath, settings.ApiKey, settings.FullUri, !settings.AddOnly, settings.MinimumAvailability);
if (!string.IsNullOrEmpty(result.Error?.message))
{
@ -77,5 +72,12 @@ namespace Ombi.Core
}
return new MovieSenderResult { Success = true, MovieSent = false };
}
private async Task<string> RadarrRootPath(int overrideId, RadarrSettings settings)
{
var paths = await RadarrApi.GetRootFolders(settings.ApiKey, settings.FullUri);
var selectedPath = paths.FirstOrDefault(x => x.id == overrideId);
return selectedPath.path;
}
}
}

@ -8,6 +8,7 @@ using Ombi.Api.Sonarr.Models;
using Ombi.Core.Settings;
using Ombi.Core.Settings.Models.External;
using Ombi.Helpers;
using Ombi.Settings.Settings.Models.External;
using Ombi.Store.Entities.Requests;
namespace Ombi.Core.Senders

@ -1,9 +1,11 @@
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Security.Principal;
using Hangfire;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
@ -31,6 +33,7 @@ using Ombi.Store.Repository;
using Ombi.Notifications.Agents;
using Ombi.Schedule.Jobs.Radarr;
using Ombi.Api;
using Ombi.Api.CouchPotato;
using Ombi.Api.FanartTv;
using Ombi.Api.Mattermost;
using Ombi.Api.Pushbullet;
@ -42,6 +45,7 @@ using Ombi.Core.Senders;
using Ombi.Schedule.Jobs.Emby;
using Ombi.Schedule.Jobs.Ombi;
using Ombi.Schedule.Jobs.Plex;
using Ombi.Schedule.Jobs.Sonarr;
using Ombi.Schedule.Ombi;
using Ombi.Store.Repository.Requests;
using PlexContentCacher = Ombi.Schedule.Jobs.Plex.PlexContentCacher;
@ -94,12 +98,12 @@ namespace Ombi.DependencyInjection
services.AddTransient<IFanartTvApi, FanartTvApi>();
services.AddTransient<IPushoverApi, PushoverApi>();
services.AddTransient<IMattermostApi, MattermostApi>();
services.AddTransient<ICouchPotatoApi, CouchPotatoApi>();
}
public static void RegisterStore(this IServiceCollection services)
{
public static void RegisterStore(this IServiceCollection services) {
services.AddEntityFrameworkSqlite().AddDbContext<OmbiContext>();
services.AddScoped<IOmbiContext, OmbiContext>(); // https://docs.microsoft.com/en-us/aspnet/core/data/entity-framework-6
services.AddTransient<ISettingsRepository, SettingsJsonRepository>();
services.AddTransient<ISettingsResolver, SettingsResolver>();
@ -142,6 +146,7 @@ namespace Ombi.DependencyInjection
services.AddTransient<IPlexAvailabilityChecker, PlexAvailabilityChecker>();
services.AddTransient<IJobSetup, JobSetup>();
services.AddTransient<IRadarrCacher, RadarrCacher>();
services.AddTransient<ISonarrCacher, SonarrCacher>();
services.AddTransient<IOmbiAutomaticUpdater, OmbiAutomaticUpdater>();
services.AddTransient<IPlexUserImporter, PlexUserImporter>();
services.AddTransient<IEmbyUserImporter, EmbyUserImporter>();

@ -15,6 +15,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Ombi.Api.CouchPotato\Ombi.Api.CouchPotato.csproj" />
<ProjectReference Include="..\Ombi.Api.Discord\Ombi.Api.Discord.csproj" />
<ProjectReference Include="..\Ombi.Api.Emby\Ombi.Api.Emby.csproj" />
<ProjectReference Include="..\Ombi.Api.FanartTv\Ombi.Api.FanartTv.csproj" />

@ -16,5 +16,7 @@ namespace Ombi.Helpers
public const string TopRatedMovies = nameof(TopRatedMovies);
public const string UpcomingMovies = nameof(UpcomingMovies);
public const string NowPlayingMovies = nameof(NowPlayingMovies);
public const string RadarrRootProfiles = nameof(RadarrRootProfiles);
public const string RadarrQualityProfiles = nameof(RadarrQualityProfiles);
}
}

@ -8,13 +8,15 @@ namespace Ombi.Helpers
public static EventId Api => new EventId(1000);
public static EventId RadarrApi => new EventId(1001);
public static EventId CouchPotatoApi => new EventId(1002);
public static EventId Cacher => new EventId(2000);
public static EventId RadarrCacher => new EventId(2001);
public static EventId PlexEpisodeCacher => new EventId(2001);
public static EventId EmbyContentCacher => new EventId(2002);
public static EventId PlexUserImporter => new EventId(2003);
public static EventId EmbyUserImporter => new EventId(2004);
public static EventId PlexEpisodeCacher => new EventId(2002);
public static EventId EmbyContentCacher => new EventId(2003);
public static EventId PlexUserImporter => new EventId(2004);
public static EventId EmbyUserImporter => new EventId(2005);
public static EventId SonarrCacher => new EventId(2006);
public static EventId MovieSender => new EventId(3000);

@ -0,0 +1,13 @@
namespace Ombi.Helpers
{
public class StoragePathSingleton
{
private static StoragePathSingleton instance;
private StoragePathSingleton() { }
public static StoragePathSingleton Instance => instance ?? (instance = new StoragePathSingleton());
public string StoragePath { get; set; }
}
}

@ -42,7 +42,7 @@ namespace Ombi.Notifications.Agents
try
{
var a = settings.Token;
var b = settings.WebookId;
var b = settings.WebHookId;
}
catch (IndexOutOfRangeException)
{
@ -164,7 +164,7 @@ namespace Ombi.Notifications.Agents
};
}
await Api.SendMessage(discordBody, settings.WebookId, settings.Token);
await Api.SendMessage(discordBody, settings.WebHookId, settings.Token);
}
catch (Exception e)
{

@ -3,6 +3,7 @@ using Ombi.Schedule.Jobs;
using Ombi.Schedule.Jobs.Emby;
using Ombi.Schedule.Jobs.Plex;
using Ombi.Schedule.Jobs.Radarr;
using Ombi.Schedule.Jobs.Sonarr;
using Ombi.Schedule.Ombi;
namespace Ombi.Schedule
@ -11,7 +12,7 @@ namespace Ombi.Schedule
{
public JobSetup(IPlexContentCacher plexContentCacher, IRadarrCacher radarrCacher,
IOmbiAutomaticUpdater updater, IEmbyContentCacher embyCacher, IPlexUserImporter userImporter,
IEmbyUserImporter embyUserImporter)
IEmbyUserImporter embyUserImporter, ISonarrCacher cache)
{
PlexContentCacher = plexContentCacher;
RadarrCacher = radarrCacher;
@ -19,6 +20,7 @@ namespace Ombi.Schedule
EmbyContentCacher = embyCacher;
PlexUserImporter = userImporter;
EmbyUserImporter = embyUserImporter;
SonarrCacher = cache;
}
private IPlexContentCacher PlexContentCacher { get; }
@ -27,17 +29,17 @@ namespace Ombi.Schedule
private IPlexUserImporter PlexUserImporter { get; }
private IEmbyContentCacher EmbyContentCacher { get; }
private IEmbyUserImporter EmbyUserImporter { get; }
private ISonarrCacher SonarrCacher { get; }
public void Setup()
{
RecurringJob.AddOrUpdate(() => PlexContentCacher.CacheContent(), Cron.Hourly(20));
RecurringJob.AddOrUpdate(() => EmbyContentCacher.Start(), Cron.Hourly(5));
RecurringJob.AddOrUpdate(() => RadarrCacher.CacheContent(), Cron.Hourly(10));
RecurringJob.AddOrUpdate(() => PlexUserImporter.Start(), Cron.Daily(1));
RecurringJob.AddOrUpdate(() => RadarrCacher.CacheContent(), Cron.Hourly(15));
RecurringJob.AddOrUpdate(() => PlexUserImporter.Start(), Cron.Daily(5));
RecurringJob.AddOrUpdate(() => EmbyUserImporter.Start(), Cron.Daily);
RecurringJob.AddOrUpdate(() => Updater.Update(null), Cron.Daily(3));
//BackgroundJob.Enqueue(() => PlexUserImporter.Start());
RecurringJob.AddOrUpdate(() => Updater.Update(null), Cron.HourInterval(6));
}
}
}

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
@ -28,8 +29,11 @@ namespace Ombi.Schedule.Jobs.Radarr
private ILogger<RadarrCacher> Logger { get; }
private readonly IOmbiContext _ctx;
private static readonly SemaphoreSlim SemaphoreSlim = new SemaphoreSlim(1, 1);
public async Task CacheContent()
{
await SemaphoreSlim.WaitAsync();
try
{
var settings = RadarrSettings.GetSettings();
@ -48,7 +52,7 @@ namespace Ombi.Schedule.Jobs.Radarr
{
if (m.tmdbId > 0)
{
movieIds.Add(new RadarrCache { TheMovieDbId = m.tmdbId });
movieIds.Add(new RadarrCache {TheMovieDbId = m.tmdbId});
}
else
{
@ -70,6 +74,10 @@ namespace Ombi.Schedule.Jobs.Radarr
{
Logger.LogInformation(LoggingEvents.RadarrCacher, "Radarr is not setup, cannot cache episodes");
}
finally
{
SemaphoreSlim.Release();
}
}
public async Task<IEnumerable<RadarrCache>> GetCachedContent()

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

@ -0,0 +1,117 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Ombi.Api.Sonarr;
using Ombi.Core.Settings;
using Ombi.Core.Settings.Models.External;
using Ombi.Helpers;
using Ombi.Settings.Settings.Models.External;
using Ombi.Store.Context;
using Ombi.Store.Entities;
namespace Ombi.Schedule.Jobs.Sonarr
{
public class SonarrCacher : ISonarrCacher
{
public SonarrCacher(ISettingsService<SonarrSettings> s, ISonarrApi api, ILogger<SonarrCacher> l, IOmbiContext ctx)
{
_settings = s;
_api = api;
_log = l;
}
private readonly ISettingsService<SonarrSettings> _settings;
private readonly ISonarrApi _api;
private readonly ILogger<SonarrCacher> _log;
private readonly IOmbiContext _ctx;
private static readonly SemaphoreSlim SemaphoreSlim = new SemaphoreSlim(1, 1);
public async Task Start()
{
await SemaphoreSlim.WaitAsync();
try
{
var settings = await _settings.GetSettingsAsync();
if (!settings.Enabled)
{
return;
}
var series = await _api.GetSeries(settings.ApiKey, settings.FullUri);
if (series != null)
{
var ids = series.Select(x => x.tvdbId);
await _ctx.Database.ExecuteSqlCommandAsync("DELETE FROM SonarrCache");
var entites = ids.Select(id => new SonarrCache {TvDbId = id}).ToList();
await _ctx.SonarrCache.AddRangeAsync(entites);
await _ctx.SaveChangesAsync();
}
}
catch (Exception e)
{
_log.LogError(LoggingEvents.SonarrCacher, e, "Exception when trying to cache Sonarr");
}
finally
{
SemaphoreSlim.Release();
}
}
//public void Queued()
//{
// var settings = SonarrSettings.GetSettings();
// if (settings.Enabled)
// {
// Job.SetRunning(true, JobNames.SonarrCacher);
// try
// {
// var series = SonarrApi.GetSeries(settings.ApiKey, settings.FullUri);
// if (series != null)
// {
// Cache.Set(CacheKeys.SonarrQueued, series, CacheKeys.TimeFrameMinutes.SchedulerCaching);
// }
// }
// catch (System.Exception ex)
// {
// Log.Error(ex, "Failed caching queued items from Sonarr");
// }
// finally
// {
// Job.Record(JobNames.SonarrCacher);
// Job.SetRunning(false, JobNames.SonarrCacher);
// }
// }
//}
//// we do not want to set here...
//public IEnumerable<SonarrCachedResult> QueuedIds()
//{
// var result = new List<SonarrCachedResult>();
// var series = Cache.Get<List<Series>>(CacheKeys.SonarrQueued);
// if (series != null)
// {
// foreach (var s in series)
// {
// var cached = new SonarrCachedResult { TvdbId = s.tvdbId };
// foreach (var season in s.seasons)
// {
// cached.Seasons.Add(new SonarrSeasons
// {
// SeasonNumber = season.seasonNumber,
// Monitored = season.monitored
// });
// }
// result.Add(cached);
// }
// }
// return result;
//}
}
}

@ -23,6 +23,7 @@
<ProjectReference Include="..\Ombi.Api.Plex\Ombi.Api.Plex.csproj" />
<ProjectReference Include="..\Ombi.Api.Radarr\Ombi.Api.Radarr.csproj" />
<ProjectReference Include="..\Ombi.Api.Service\Ombi.Api.Service.csproj" />
<ProjectReference Include="..\Ombi.Api.Sonarr\Ombi.Api.Sonarr.csproj" />
<ProjectReference Include="..\Ombi.Notifications\Ombi.Notifications.csproj" />
<ProjectReference Include="..\Ombi.Settings\Ombi.Settings.csproj" />
</ItemGroup>

@ -0,0 +1,13 @@
using Ombi.Core.Settings.Models.External;
namespace Ombi.Settings.Settings.Models.External
{
public class CouchPotatoSettings : ExternalSettings
{
public bool Enabled { get; set; }
public string ApiKey { get; set; }
public string DefaultProfileId { get; set; }
public string Username { get; set; }
public string Password { get; set; }
}
}

@ -1,4 +1,5 @@
using System.Collections.Generic;
using Ombi.Settings.Settings.Models.External;
namespace Ombi.Core.Settings.Models.External
{

@ -1,10 +1,9 @@
using System;
using Newtonsoft.Json;
using Newtonsoft.Json;
using Ombi.Helpers;
namespace Ombi.Core.Settings.Models.External
namespace Ombi.Settings.Settings.Models.External
{
public abstract class ExternalSettings : Ombi.Settings.Settings.Models.Settings
public abstract class ExternalSettings : Models.Settings
{
public bool Ssl { get; set; }
public string SubDir { get; set; }

@ -1,4 +1,5 @@
using System.Collections.Generic;
using Ombi.Settings.Settings.Models.External;
namespace Ombi.Core.Settings.Models.External
{

@ -1,4 +1,4 @@
namespace Ombi.Core.Settings.Models.External
namespace Ombi.Settings.Settings.Models.External
{
public class SonarrSettings : ExternalSettings
{

@ -10,7 +10,7 @@ namespace Ombi.Settings.Settings.Models.Notifications
public string Username { get; set; }
[JsonIgnore]
public string WebookId => SplitWebUrl(4);
public string WebHookId => SplitWebUrl(4);
[JsonIgnore]
public string Token => SplitWebUrl(5);

@ -33,6 +33,7 @@ namespace Ombi.Store.Context
DbSet<MovieIssues> MovieIssues { get; set; }
DbSet<TvIssues> TvIssues { get; set; }
DbSet<Tokens> Tokens { get; set; }
DbSet<SonarrCache> SonarrCache { get; set; }
EntityEntry Update(object entity);
EntityEntry<TEntity> Update<TEntity>(TEntity entity) where TEntity : class;
}

@ -1,4 +1,5 @@
using System;
using System.IO;
using System.Linq;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
@ -14,12 +15,10 @@ namespace Ombi.Store.Context
public OmbiContext()
{
if (_created) return;
_created = true;
Database.Migrate();
// Add the notifcation templates
}
public DbSet<NotificationTemplates> NotificationTemplates { get; set; }
@ -38,12 +37,14 @@ namespace Ombi.Store.Context
public DbSet<Audit> Audit { get; set; }
public DbSet<Tokens> Tokens { get; set; }
public DbSet<SonarrCache> SonarrCache { get; set; }
public DbSet<ApplicationConfiguration> ApplicationConfigurations { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlite("Data Source=Ombi.db");
var i = StoragePathSingleton.Instance;
optionsBuilder.UseSqlite($"Data Source={Path.Combine(i.StoragePath,"Ombi.db")}");
}
protected override void OnModelCreating(ModelBuilder builder)

@ -11,5 +11,8 @@ namespace Ombi.Store.Entities.Requests
public int? IssueId { get; set; }
[ForeignKey(nameof(IssueId))]
public List<MovieIssues> Issues { get; set; }
public int RootPathOverride { get; set; }
public int QualityOverride { get; set; }
}
}

@ -0,0 +1,10 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace Ombi.Store.Entities
{
[Table("SonarrCache")]
public class SonarrCache : Entity
{
public int TvDbId { get; set; }
}
}

@ -0,0 +1,747 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal;
using Ombi.Helpers;
using Ombi.Store.Context;
using Ombi.Store.Entities;
using System;
namespace Ombi.Store.Migrations
{
[DbContext(typeof(OmbiContext))]
[Migration("20171002113357_SonarrCacher")]
partial class SonarrCacher
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.0.0-rtm-26452");
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Name")
.HasMaxLength(256);
b.Property<string>("NormalizedName")
.HasMaxLength(256);
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasName("RoleNameIndex");
b.ToTable("AspNetRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("RoleId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider");
b.Property<string>("ProviderKey");
b.Property<string>("ProviderDisplayName");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("RoleId");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("LoginProvider");
b.Property<string>("Name");
b.Property<string>("Value");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens");
});
modelBuilder.Entity("Ombi.Store.Entities.ApplicationConfiguration", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("Type");
b.Property<string>("Value");
b.HasKey("Id");
b.ToTable("ApplicationConfiguration");
});
modelBuilder.Entity("Ombi.Store.Entities.Audit", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("AuditArea");
b.Property<int>("AuditType");
b.Property<DateTime>("DateTime");
b.Property<string>("Description");
b.Property<string>("User");
b.HasKey("Id");
b.ToTable("Audit");
});
modelBuilder.Entity("Ombi.Store.Entities.EmbyContent", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("AddedAt");
b.Property<string>("EmbyId")
.IsRequired();
b.Property<string>("ProviderId");
b.Property<string>("Title");
b.Property<int>("Type");
b.HasKey("Id");
b.ToTable("EmbyContent");
});
modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("AddedAt");
b.Property<string>("EmbyId");
b.Property<int>("EpisodeNumber");
b.Property<string>("ParentId");
b.Property<string>("ProviderId");
b.Property<int>("SeasonNumber");
b.Property<string>("Title");
b.HasKey("Id");
b.HasIndex("ParentId");
b.ToTable("EmbyEpisode");
});
modelBuilder.Entity("Ombi.Store.Entities.GlobalSettings", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Content");
b.Property<string>("SettingsName");
b.HasKey("Id");
b.ToTable("GlobalSettings");
});
modelBuilder.Entity("Ombi.Store.Entities.NotificationTemplates", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("Agent");
b.Property<bool>("Enabled");
b.Property<string>("Message");
b.Property<int>("NotificationType");
b.Property<string>("Subject");
b.HasKey("Id");
b.ToTable("NotificationTemplates");
});
modelBuilder.Entity("Ombi.Store.Entities.OmbiUser", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("AccessFailedCount");
b.Property<string>("Alias");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Email")
.HasMaxLength(256);
b.Property<bool>("EmailConfirmed");
b.Property<DateTime?>("LastLoggedIn");
b.Property<bool>("LockoutEnabled");
b.Property<DateTimeOffset?>("LockoutEnd");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256);
b.Property<string>("NormalizedUserName")
.HasMaxLength(256);
b.Property<string>("PasswordHash");
b.Property<string>("PhoneNumber");
b.Property<bool>("PhoneNumberConfirmed");
b.Property<string>("ProviderUserId");
b.Property<string>("SecurityStamp");
b.Property<bool>("TwoFactorEnabled");
b.Property<string>("UserName")
.HasMaxLength(256);
b.Property<int>("UserType");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasName("UserNameIndex");
b.ToTable("AspNetUsers");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexContent", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("AddedAt");
b.Property<int>("Key");
b.Property<string>("ProviderId");
b.Property<string>("Quality");
b.Property<string>("ReleaseYear");
b.Property<string>("Title");
b.Property<int>("Type");
b.Property<string>("Url");
b.HasKey("Id");
b.ToTable("PlexContent");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("EpisodeNumber");
b.Property<int>("GrandparentKey");
b.Property<int>("Key");
b.Property<int>("ParentKey");
b.Property<int>("SeasonNumber");
b.Property<string>("Title");
b.HasKey("Id");
b.HasIndex("GrandparentKey");
b.ToTable("PlexEpisode");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("ParentKey");
b.Property<int>("PlexContentId");
b.Property<int>("SeasonKey");
b.Property<int>("SeasonNumber");
b.HasKey("Id");
b.HasIndex("PlexContentId");
b.ToTable("PlexSeasonsContent");
});
modelBuilder.Entity("Ombi.Store.Entities.RadarrCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("TheMovieDbId");
b.HasKey("Id");
b.ToTable("RadarrCache");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.ChildRequests", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<bool>("Approved");
b.Property<bool>("Available");
b.Property<bool?>("Denied");
b.Property<string>("DeniedReason");
b.Property<int?>("IssueId");
b.Property<int>("ParentRequestId");
b.Property<int>("RequestType");
b.Property<DateTime>("RequestedDate");
b.Property<string>("RequestedUserId");
b.Property<string>("Title");
b.HasKey("Id");
b.HasIndex("ParentRequestId");
b.HasIndex("RequestedUserId");
b.ToTable("ChildRequests");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.MovieIssues", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Description");
b.Property<int?>("IssueId");
b.Property<int>("MovieId");
b.Property<string>("Subect");
b.HasKey("Id");
b.HasIndex("IssueId");
b.HasIndex("MovieId");
b.ToTable("MovieIssues");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.MovieRequests", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<bool>("Approved");
b.Property<bool>("Available");
b.Property<bool?>("Denied");
b.Property<string>("DeniedReason");
b.Property<string>("ImdbId");
b.Property<int?>("IssueId");
b.Property<string>("Overview");
b.Property<string>("PosterPath");
b.Property<int>("QualityOverride");
b.Property<DateTime>("ReleaseDate");
b.Property<int>("RequestType");
b.Property<DateTime>("RequestedDate");
b.Property<string>("RequestedUserId");
b.Property<int>("RootPathOverride");
b.Property<string>("Status");
b.Property<int>("TheMovieDbId");
b.Property<string>("Title");
b.HasKey("Id");
b.HasIndex("RequestedUserId");
b.ToTable("MovieRequests");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.TvIssues", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Description");
b.Property<int?>("IssueId");
b.Property<string>("Subect");
b.Property<int>("TvId");
b.HasKey("Id");
b.HasIndex("IssueId");
b.HasIndex("TvId");
b.ToTable("TvIssues");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.TvRequests", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ImdbId");
b.Property<string>("Overview");
b.Property<string>("PosterPath");
b.Property<DateTime>("ReleaseDate");
b.Property<int?>("RootFolder");
b.Property<string>("Status");
b.Property<string>("Title");
b.Property<int>("TvDbId");
b.HasKey("Id");
b.ToTable("TvRequests");
});
modelBuilder.Entity("Ombi.Store.Entities.SonarrCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("TvDbId");
b.HasKey("Id");
b.ToTable("SonarrCache");
});
modelBuilder.Entity("Ombi.Store.Entities.Tokens", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Token");
b.Property<string>("UserId");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Tokens");
});
modelBuilder.Entity("Ombi.Store.Repository.Requests.EpisodeRequests", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("AirDate");
b.Property<bool>("Approved");
b.Property<bool>("Available");
b.Property<int>("EpisodeNumber");
b.Property<bool>("Requested");
b.Property<int>("SeasonId");
b.Property<string>("Title");
b.Property<string>("Url");
b.HasKey("Id");
b.HasIndex("SeasonId");
b.ToTable("EpisodeRequests");
});
modelBuilder.Entity("Ombi.Store.Repository.Requests.SeasonRequests", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("ChildRequestId");
b.Property<int>("SeasonNumber");
b.HasKey("Id");
b.HasIndex("ChildRequestId");
b.ToTable("SeasonRequests");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("Ombi.Store.Entities.OmbiUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b =>
{
b.HasOne("Ombi.Store.Entities.EmbyContent", "Series")
.WithMany("Episodes")
.HasForeignKey("ParentId")
.HasPrincipalKey("EmbyId");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b =>
{
b.HasOne("Ombi.Store.Entities.PlexContent", "Series")
.WithMany("Episodes")
.HasForeignKey("GrandparentKey")
.HasPrincipalKey("Key")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b =>
{
b.HasOne("Ombi.Store.Entities.PlexContent")
.WithMany("Seasons")
.HasForeignKey("PlexContentId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.ChildRequests", b =>
{
b.HasOne("Ombi.Store.Entities.Requests.TvRequests", "ParentRequest")
.WithMany("ChildRequests")
.HasForeignKey("ParentRequestId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser")
.WithMany()
.HasForeignKey("RequestedUserId");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.MovieIssues", b =>
{
b.HasOne("Ombi.Store.Entities.Requests.MovieRequests")
.WithMany("Issues")
.HasForeignKey("IssueId");
b.HasOne("Ombi.Store.Entities.Requests.MovieRequests", "Movie")
.WithMany()
.HasForeignKey("MovieId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.MovieRequests", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser")
.WithMany()
.HasForeignKey("RequestedUserId");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.TvIssues", b =>
{
b.HasOne("Ombi.Store.Entities.Requests.ChildRequests")
.WithMany("Issues")
.HasForeignKey("IssueId");
b.HasOne("Ombi.Store.Entities.Requests.ChildRequests", "Child")
.WithMany()
.HasForeignKey("TvId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Ombi.Store.Entities.Tokens", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser", "User")
.WithMany()
.HasForeignKey("UserId");
});
modelBuilder.Entity("Ombi.Store.Repository.Requests.EpisodeRequests", b =>
{
b.HasOne("Ombi.Store.Repository.Requests.SeasonRequests", "Season")
.WithMany("Episodes")
.HasForeignKey("SeasonId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Ombi.Store.Repository.Requests.SeasonRequests", b =>
{
b.HasOne("Ombi.Store.Entities.Requests.ChildRequests", "ChildRequest")
.WithMany("SeasonRequests")
.HasForeignKey("ChildRequestId")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
}

@ -0,0 +1,53 @@
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
namespace Ombi.Store.Migrations
{
public partial class SonarrCacher : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "QualityOverride",
table: "MovieRequests",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "RootPathOverride",
table: "MovieRequests",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.CreateTable(
name: "SonarrCache",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
TvDbId = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_SonarrCache", x => x.Id);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "SonarrCache");
migrationBuilder.DropColumn(
name: "QualityOverride",
table: "MovieRequests");
migrationBuilder.DropColumn(
name: "RootPathOverride",
table: "MovieRequests");
}
}
}

@ -460,6 +460,8 @@ namespace Ombi.Store.Migrations
b.Property<string>("PosterPath");
b.Property<int>("QualityOverride");
b.Property<DateTime>("ReleaseDate");
b.Property<int>("RequestType");
@ -468,6 +470,8 @@ namespace Ombi.Store.Migrations
b.Property<string>("RequestedUserId");
b.Property<int>("RootPathOverride");
b.Property<string>("Status");
b.Property<int>("TheMovieDbId");
@ -529,6 +533,18 @@ namespace Ombi.Store.Migrations
b.ToTable("TvRequests");
});
modelBuilder.Entity("Ombi.Store.Entities.SonarrCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("TvDbId");
b.HasKey("Id");
b.ToTable("SonarrCache");
});
modelBuilder.Entity("Ombi.Store.Entities.Tokens", b =>
{
b.Property<int>("Id")

@ -45,8 +45,8 @@ namespace Ombi.TheMovieDbApi.Models
public ProductionCompanies[] production_companies { get; set; }
public ProductionCountries[] production_countries { get; set; }
public string release_date { get; set; }
public int revenue { get; set; }
public int runtime { get; set; }
public float revenue { get; set; }
public float runtime { get; set; }
public SpokenLanguages[] spoken_languages { get; set; }
public string status { get; set; }
public string tagline { get; set; }

@ -15,8 +15,8 @@
public float Popularity { get; set; }
public string PosterPath { get; set; }
public string ReleaseDate { get; set; }
public int Revenue { get; set; }
public int Runtime { get; set; }
public float Revenue { get; set; }
public float Runtime { get; set; }
public string Status { get; set; }
public string Tagline { get; set; }
public string Title { get; set; }

@ -80,6 +80,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Api.Pushover", "Ombi.A
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Schedule.Tests", "Ombi.Schedule.Tests\Ombi.Schedule.Tests.csproj", "{BDD8B924-016E-4CDA-9FFA-50B0A34BCD3C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ombi.Api.CouchPotato", "Ombi.Api.CouchPotato\Ombi.Api.CouchPotato.csproj", "{87D7897D-7C73-4856-A0AA-FF5948F4EA86}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -202,6 +204,10 @@ Global
{BDD8B924-016E-4CDA-9FFA-50B0A34BCD3C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BDD8B924-016E-4CDA-9FFA-50B0A34BCD3C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BDD8B924-016E-4CDA-9FFA-50B0A34BCD3C}.Release|Any CPU.Build.0 = Release|Any CPU
{87D7897D-7C73-4856-A0AA-FF5948F4EA86}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{87D7897D-7C73-4856-A0AA-FF5948F4EA86}.Debug|Any CPU.Build.0 = Debug|Any CPU
{87D7897D-7C73-4856-A0AA-FF5948F4EA86}.Release|Any CPU.ActiveCfg = Release|Any CPU
{87D7897D-7C73-4856-A0AA-FF5948F4EA86}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -230,6 +236,7 @@ Global
{737B2620-FE5A-4135-A017-79C269A7D36C} = {9293CA11-360A-4C20-A674-B9E794431BF5}
{CA55DD4F-4EFF-4906-A848-35FCC7BD5654} = {9293CA11-360A-4C20-A674-B9E794431BF5}
{BDD8B924-016E-4CDA-9FFA-50B0A34BCD3C} = {6F42AB98-9196-44C4-B888-D5E409F415A1}
{87D7897D-7C73-4856-A0AA-FF5948F4EA86} = {9293CA11-360A-4C20-A674-B9E794431BF5}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {192E9BF8-00B4-45E4-BCCC-4C215725C869}

@ -0,0 +1,25 @@
export interface ICouchPotatoProfiles {
success: boolean;
list: IProfileList[];
}
export interface IProfileList {
core: boolean;
hide: boolean;
_rev: string;
finish: boolean[];
qualities: string[];
_id: string;
_t: string;
label: string;
minimum_score: number;
stop_after: number[];
wait_for: object[];
order: number;
threeD: object[];
}
export interface ICouchPotatoApiKey {
success: boolean;
apiKey: string;
}

@ -79,6 +79,8 @@ export interface IRequestGrid<T> {
export interface IMovieRequests extends IFullBaseRequest {
theMovieDbId: number;
rootPathOverride: number;
qualityOverride: number;
}
export interface IFullBaseRequest extends IBaseRequest {

@ -119,3 +119,11 @@ export interface IAbout {
processArchitecture: string;
applicationBasePath: string;
}
export interface ICouchPotatoSettings extends IExternalSettings {
enabled: boolean;
apiKey: string;
defaultProfileId: string;
username: string;
password: string;
}

@ -1,4 +1,5 @@
export * from "./ICommon";
export * from "./ICouchPotato";
export * from "./IImages";
export * from "./IMediaServerStatus";
export * from "./INotificationSettings";

@ -41,7 +41,7 @@
</div>
</div>
<div class="col-md-3 col-md-push-4 vcenter">
<button [routerLink]="['/login', 'true']" class="btn btn-lg btn-success-outline">Contine</button>
<button [routerLink]="['/login', 'true']" class="btn btn-lg btn-success-outline">Continue</button>
</div>
</div>
</div>

@ -7,7 +7,7 @@ include the remember me checkbox
<div class="card card-container">
<!-- <img class="profile-img-card" src="//lh3.googleusercontent.com/-6V8xOA6M7BA/AAAAAAAAAAI/AAAAAAAAAAA/rzlHcD0KYwo/photo.jpg?sz=120" alt="" /> -->
<div *ngIf="!customizationSettings.logo"><img id="profile-img" class="profile-img-card" src="/images/ms-icon-150x150.png" /></div>
<div *ngIf="customizationSettings.logo"><img id="profile-img" class="profile-img-card" [src]="customizationSettings.logo" /></div>
<div *ngIf="customizationSettings.logo"><img id="profile-img" class="center" [src]="customizationSettings.logo" /></div>
<p id="profile-name" class="profile-name-card"></p>
<form class="form-signin" novalidate [formGroup]="form" (ngSubmit)="onSubmit(form)">

@ -3,7 +3,7 @@
<div class="card card-container">
<!-- <img class="profile-img-card" src="//lh3.googleusercontent.com/-6V8xOA6M7BA/AAAAAAAAAAI/AAAAAAAAAAA/rzlHcD0KYwo/photo.jpg?sz=120" alt="" /> -->
<div *ngIf="!customizationSettings.logo"><img id="profile-img" class="profile-img-card" src="/images/ms-icon-150x150.png" /></div>
<div *ngIf="customizationSettings.logo"><img id="profile-img" class="profile-img-card" [src]="customizationSettings.logo" /></div>
<div *ngIf="customizationSettings.logo"><img id="profile-img" class="center" [src]="customizationSettings.logo" /></div>
<p id="profile-name" class="profile-name-card"></p>

@ -78,45 +78,32 @@
<div *ngIf="isAdmin">
<div *ngIf="!request.approved">
<form>
<input name="requestId" type="text" value="{{request.requestId}}" hidden="hidden" />
<div *ngIf="request.hasQualities" class="btn-group btn-split">
<button type="button" (click)="approve(request)" class="btn btn-sm btn-success-outline approve"><i class="fa fa-plus"></i> Approve</button>
<button type="button" class="btn btn-success-outline dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
<!--<ul class="dropdown-menu">
{{#each qualities}}
<li><a href="#" class="approve-with-quality" id="{{id}}">{{name}}</a></li>
{{/each}}
</ul>-->
</div>
<button *ngIf="!request.hasQualities" (click)="approve(request)" style="text-align: right" class="btn btn-sm btn-success-outline approve" type="submit"><i class="fa fa-plus"></i> Approve</button>
<button (click)="approve(request)" style="text-align: right" class="btn btn-sm btn-success-outline approve" type="submit"><i class="fa fa-plus"></i> Approve</button>
</form>
<!--<form method="POST" action="@formAction/requests/changeRootFolder{{#if_eq type "tv"}}tv{{else}}movie{{/if_eq}}" id="changeFolder{{requestId}}">
<input name="requestId" type="text" value="{{requestId}}" hidden="hidden"/>
{{#if_eq hasRootFolders true}}
<div class="btn-group btn-split">
<button type="button" class="btn btn-sm btn-success-outline" id="changeRootFolderBtn{{requestId}}" custom-button="{{requestId}}">@*<i class="fa fa-plus"></i>*@ Change Root Folder</button>
<button type="button" class="btn btn-success-outline dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="caret"></span>
<span class="sr-only">@UI.Requests_ToggleDropdown</span>
</button>
<ul class="dropdown-menu">
{{#each rootFolders}}
<li><a href="#" class="change-root-folder" id="{{id}}" requestId="{{requestId}}">{{path}}</a></li>
{{/each}}
</ul>
</div>
{{/if_eq}}
</form>-->
<!--Radarr Root Folder-->
<div *ngIf="radarrRootFolders" class="btn-group btn-split">
<button type="button" class="btn btn-sm btn-success-outline"><i class="fa fa-plus"></i> Change Root Folder</button>
<button type="button" class="btn btn-success-outline dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu">
<li *ngFor="let folder of radarrRootFolders"><a href="#" (click)="selectRootFolder(request, folder)">{{folder.path}}</a></li>
</ul>
</div>
<!--Radarr Quality Profiles -->
<div *ngIf="radarrProfiles" class="btn-group btn-split">
<button type="button" class="btn btn-sm btn-success-outline"><i class="fa fa-plus"></i> Change Quality Profile</button>
<button type="button" class="btn btn-success-outline dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu">
<li *ngFor="let profile of radarrProfiles"><a href="#" (click)="selectQualityProfile(request, profile)">{{profile.name}}</a></li>
</ul>
</div>
<div *ngIf="!request.denied">
<button type="button" (click)="deny(request)" class="btn btn-sm btn-danger-outline deny"><i class="fa fa-times"></i> Deny</button>

@ -5,9 +5,9 @@ import "rxjs/add/operator/map";
import { Subject } from "rxjs/Subject";
import { AuthService } from "../auth/auth.service";
import { NotificationService, RequestService } from "../services";
import { NotificationService, RadarrService, RequestService } from "../services";
import { IMovieRequests } from "../interfaces";
import { IMovieRequests, IRadarrProfile, IRadarrRootFolder } from "../interfaces";
@Component({
selector: "movie-requests",
@ -21,12 +21,16 @@ export class MovieRequestsComponent implements OnInit {
public isAdmin: boolean;
public radarrProfiles: IRadarrProfile[];
public radarrRootFolders: IRadarrRootFolder[];
private currentlyLoaded: number;
private amountToLoad: number;
constructor(private requestService: RequestService,
private auth: AuthService,
private notificationService: NotificationService) {
private notificationService: NotificationService,
private radarrService: RadarrService) {
this.searchChanged
.debounceTime(600) // Wait Xms after the last event before emitting last event
.distinctUntilChanged() // only emit if value is different from previous value
@ -42,10 +46,13 @@ export class MovieRequestsComponent implements OnInit {
}
public ngOnInit() {
this.radarrService.getQualityProfilesFromSettings().subscribe(x => this.radarrProfiles = x);
this.radarrService.getRootFoldersFromSettings().subscribe(x => this.radarrRootFolders = x);
this.amountToLoad = 5;
this.currentlyLoaded = 5;
this.loadInit();
this.isAdmin = this.auth.hasRole("admin");
this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser");
}
public loadMore() {
@ -80,6 +87,14 @@ export class MovieRequestsComponent implements OnInit {
this.updateRequest(request);
}
public selectRootFolder(searchResult: IMovieRequests, rootFolderSelected: IRadarrRootFolder) {
searchResult.rootPathOverride = rootFolderSelected.id;
}
public selectQualityProfile(searchResult: IMovieRequests, profileSelected: IRadarrProfile) {
searchResult.qualityOverride = profileSelected.id;
}
private loadRequests(amountToLoad: number, currentlyLoaded: number) {
this.requestService.getMovieRequests(amountToLoad, currentlyLoaded + 1)
.subscribe(x => {

@ -1,38 +1,32 @@
import { Component, OnDestroy, OnInit } from "@angular/core";
import { Component, OnInit } from "@angular/core";
import "rxjs/add/operator/debounceTime";
import "rxjs/add/operator/distinctUntilChanged";
import "rxjs/add/operator/map";
import "rxjs/add/operator/takeUntil";
import { Subject } from "rxjs/Subject";
import { AuthService } from "../auth/auth.service";
import { NotificationService } from "../services";
import { RequestService } from "../services";
import { SearchService } from "../services";
import { NotificationService, RequestService, SearchService } from "../services";
import { IRequestEngineResult } from "../interfaces";
import { ISearchMovieResult } from "../interfaces";
import { IRequestEngineResult, ISearchMovieResult } from "../interfaces";
@Component({
selector: "movie-search",
templateUrl: "./moviesearch.component.html",
})
export class MovieSearchComponent implements OnInit, OnDestroy {
export class MovieSearchComponent implements OnInit {
public searchText: string;
public searchChanged: Subject<string> = new Subject<string>();
public movieResults: ISearchMovieResult[];
public result: IRequestEngineResult;
public searchApplied = false;
private subscriptions = new Subject<void>();
constructor(private searchService: SearchService, private requestService: RequestService,
private notificationService: NotificationService, private authService: AuthService) {
this.searchChanged
.debounceTime(600) // Wait Xms afterthe last event before emitting last event
.distinctUntilChanged() // only emit if value is different from previous value
.takeUntil(this.subscriptions)
.subscribe(x => {
this.searchText = x as string;
if (this.searchText === "") {
@ -40,7 +34,6 @@ export class MovieSearchComponent implements OnInit, OnDestroy {
return;
}
this.searchService.searchMovie(this.searchText)
.takeUntil(this.subscriptions)
.subscribe(x => {
this.movieResults = x;
this.searchApplied = true;
@ -72,7 +65,6 @@ export class MovieSearchComponent implements OnInit, OnDestroy {
}
this.requestService.requestMovie(searchResult)
.takeUntil(this.subscriptions)
.subscribe(x => {
this.result = x;
@ -90,7 +82,6 @@ export class MovieSearchComponent implements OnInit, OnDestroy {
public popularMovies() {
this.clearResults();
this.searchService.popularMovies()
.takeUntil(this.subscriptions)
.subscribe(x => {
this.movieResults = x;
this.getExtaInfo();
@ -99,7 +90,6 @@ export class MovieSearchComponent implements OnInit, OnDestroy {
public nowPlayingMovies() {
this.clearResults();
this.searchService.nowPlayingMovies()
.takeUntil(this.subscriptions)
.subscribe(x => {
this.movieResults = x;
this.getExtaInfo();
@ -108,7 +98,6 @@ export class MovieSearchComponent implements OnInit, OnDestroy {
public topRatedMovies() {
this.clearResults();
this.searchService.topRatedMovies()
.takeUntil(this.subscriptions)
.subscribe(x => {
this.movieResults = x;
this.getExtaInfo();
@ -117,23 +106,16 @@ export class MovieSearchComponent implements OnInit, OnDestroy {
public upcomingMovies() {
this.clearResults();
this.searchService.upcomignMovies()
.takeUntil(this.subscriptions)
.subscribe(x => {
this.movieResults = x;
this.getExtaInfo();
});
}
public ngOnDestroy() {
this.subscriptions.next();
this.subscriptions.complete();
}
private getExtaInfo() {
private getExtaInfo() {
this.movieResults.forEach((val, index) => {
this.searchService.getMovieInformation(val.id)
.takeUntil(this.subscriptions)
.subscribe(m => this.updateItem(val, m));
});
}

@ -0,0 +1,23 @@
import { PlatformLocation } from "@angular/common";
import { Injectable } from "@angular/core";
import { AuthHttp } from "angular2-jwt";
import { Observable } from "rxjs/Rx";
import { ServiceAuthHelpers } from "../service.helpers";
import { ICouchPotatoApiKey, ICouchPotatoProfiles, ICouchPotatoSettings } from "../../interfaces";
@Injectable()
export class CouchPotatoService extends ServiceAuthHelpers {
constructor(http: AuthHttp, public platformLocation: PlatformLocation) {
super(http, "/api/v1/CouchPotato/", platformLocation);
}
public getProfiles(settings: ICouchPotatoSettings): Observable<ICouchPotatoProfiles> {
return this.http.post(`${this.url}profile`, JSON.stringify(settings), { headers: this.headers }).map(this.extractData);
}
public getApiKey(settings: ICouchPotatoSettings): Observable<ICouchPotatoApiKey> {
return this.http.post(`${this.url}apikey`, JSON.stringify(settings), { headers: this.headers }).map(this.extractData);
}
}

@ -1,4 +1,5 @@
export * from "./emby.service";
export * from "./couchpotato.service";
export * from "./emby.service";
export * from "./plex.service";
export * from "./radarr.service";
export * from "./sonarr.service";

@ -19,4 +19,11 @@ export class RadarrService extends ServiceAuthHelpers {
public getQualityProfiles(settings: IRadarrSettings): Observable<IRadarrProfile[]> {
return this.http.post(`${this.url}/Profiles/`, JSON.stringify(settings), { headers: this.headers }).map(this.extractData);
}
public getRootFoldersFromSettings(): Observable<IRadarrRootFolder[]> {
return this.http.get(`${this.url}/RootFolders/`, { headers: this.headers }).map(this.extractData);
}
public getQualityProfilesFromSettings(): Observable<IRadarrProfile[]> {
return this.http.get(`${this.url}/Profiles/`, { headers: this.headers }).map(this.extractData);
}
}

@ -7,6 +7,7 @@ import { Observable } from "rxjs/Rx";
import {
IAbout,
IAuthenticationSettings,
ICouchPotatoSettings,
ICustomizationSettings,
IDiscordNotifcationSettings,
IEmailNotificationSettings,
@ -195,4 +196,14 @@ export class SettingsService extends ServiceAuthHelpers {
.post(`${this.url}/UserManagement`, JSON.stringify(settings), { headers: this.headers })
.map(this.extractData).catch(this.handleError);
}
public getCouchPotatoSettings(): Observable<ICouchPotatoSettings> {
return this.httpAuth.get(`${this.url}/UserManagement`).map(this.extractData).catch(this.handleError);
}
public saveCouchPotatoSettings(settings: ICouchPotatoSettings): Observable<boolean> {
return this.httpAuth
.post(`${this.url}/UserManagement`, JSON.stringify(settings), { headers: this.headers })
.map(this.extractData).catch(this.handleError);
}
}

@ -0,0 +1,100 @@
<settings-menu>
</settings-menu>
<div *ngIf="form">
<fieldset>
<legend>CouchPotato Settings</legend>
<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" ng-checked="form.enabled">
<label for="enable">Enable</label>
</div>
</div>
<div class="form-group">
<label for="Ip" class="control-label">Hostname or IP</label>
<div class="">
<input type="text" class="form-control form-control-custom" [ngClass]="{'form-error': form.get('ip').hasError('required')}"
id="Ip" name="Ip" placeholder="localhost" formControlName="ip">
<small *ngIf="form.get('ip').hasError('required')" class="error-text">The IP/Hostname is required</small>
</div>
</div>
<div class="form-group">
<label for="portNumber" class="control-label">Port</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>
</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>
</div>
<div class="form-group">
<div class="checkbox">
<input type="checkbox" id="Ssl" name="Ssl" formControlName="ssl"><label for="Ssl">SSL</label>
</div>
</div>
<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">
</div>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="username" class="control-label">Username</label>
<input type="text" class="form-control form-control-custom " formControlName="username" name="username">
</div>
<div class="form-group">
<label for="password" class="control-label">Password</label>
<input type="text" class="form-control form-control-custom " formControlName="password" name="password">
</div>
<div class="form-group">
<button class="btn btn-primary-outline" (click)="requestToken(form)">Request Api Key <i class="fa fa-key"></i></button>
</div>
<div class="form-group">
<div>
<button type="submit" (click)="getProfiles(form)" 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>
<div id="profiles">
<select formControlName="defaultProfileId" class="form-control form-control-custom" id="select">
<option *ngFor="let profile of profiles?.list" value="{{profile._id}}">{{profile.label}}</option>
</select>
</div>
</div>
<div class="form-group">
<div>
<button [disabled]="form.invalid" (click)="test(form)" class="btn btn-primary-outline">Test Connectivity <span id="spinner"></span></button>
</div>
</div>
<div class="form-group">
<div>
<button type="submit" [disabled]="form.invalid" class="btn btn-primary-outline ">Submit</button>
</div>
</div>
</div>
</form>
</fieldset>
</div>

@ -0,0 +1,90 @@
import { Component, OnInit } from "@angular/core";
import { FormBuilder, FormControl, FormGroup, Validators } from "@angular/forms";
import { CouchPotatoService, NotificationService, SettingsService, TesterService } from "../../services";
import { ICouchPotatoProfiles } from "../../interfaces";
@Component({
templateUrl: "./couchpotato.component.html",
})
export class CouchPotatoComponent implements OnInit {
public form: FormGroup;
public profiles: ICouchPotatoProfiles;
public profilesRunning: boolean;
constructor(private readonly settingsService: SettingsService,
private readonly fb: FormBuilder,
private readonly notificationService: NotificationService,
private readonly couchPotatoService: CouchPotatoService,
private readonly testerService: TesterService) { }
public ngOnInit() {
this.settingsService.getCouchPotatoSettings().subscribe(x => {
this.form = this.fb.group({
enabled: [x.enabled],
username: [x.username],
password: [x.password],
apiKey: [x.apiKey, Validators.required],
ip: [x.ip, Validators.required],
port: [x.port, Validators.required],
ssl: [x.ssl],
subDir: [x.subDir],
defaultProfileId: [x.defaultProfileId],
});
});
}
public getProfiles(form: FormGroup) {
this.profilesRunning = true;
this.couchPotatoService.getProfiles(form.value).subscribe(x => {
this.profiles = x;
this.profilesRunning = false;
});
}
public onSubmit(form: FormGroup) {
if (form.invalid) {
this.notificationService.error("Validation", "Please check your entered values");
return;
}
const settings = form.value;
this.settingsService.saveCouchPotatoSettings(settings).subscribe(x => {
if (x) {
this.notificationService.success("Settings Saved", "Successfully saved the CouchPotato settings");
} else {
this.notificationService.success("Settings Saved", "There was an error when saving the CouchPotato settings");
}
});
}
public test(form: FormGroup) {
if (form.invalid) {
this.notificationService.error("Validation", "Please check your entered values");
return;
}
const settings = form.value;
this.testerService.radarrTest(settings).subscribe(x => {
if (x === true) {
this.notificationService.success("Connected", "Successfully connected to Radarr!");
} else {
this.notificationService.error("Connected", "We could not connect to Radarr!");
}
});
}
public requestToken(form: FormGroup) {
this.couchPotatoService.getApiKey(form.value).subscribe(x => {
if (x.success === true) {
(<FormControl>this.form.controls.apiKey).setValue(x.apiKey);
this.notificationService.success("Api Key", "Successfully got the Api Key");
} else {
this.notificationService.error("Api Key", "Could not get the Api Key");
}
});
}
}

@ -13,15 +13,11 @@
</div>
</div>
<div *ngIf="form.invalid && form.dirty" class="alert alert-danger">
<div *ngIf="form.get('webhookUrl').hasError('required')">The Webhook Url is required</div>
</div>
<div class="form-group">
<label for="webhookUrl" class="control-label">Webhook Url</label>
<div>
<input type="text" class="form-control form-control-custom " id="webhookUrl" name="webhookUrl" formControlName="webhookUrl">
</div>
<input type="text" class="form-control form-control-custom " id="webhookUrl" name="webhookUrl" formControlName="webhookUrl" [ngClass]="{'form-error': form.get('webhookUrl').hasError('required')}">
<small *ngIf="form.get('webhookUrl').hasError('required')" class="error-text">The Webhook Url is required</small>
</div>
<div class="form-group">

@ -18,35 +18,27 @@
<input type="checkbox" id="Authentication" formControlName="authentication"><label for="Authentication">Enable SMTP Authentication</label>
</div>
</div>
<div *ngIf="emailForm.invalid && emailForm.dirty" class="alert alert-danger">
<div *ngIf="emailForm.get('host').hasError('required')">Host is required</div>
<div *ngIf="emailForm.get('port').hasError('required')">The Port is required</div>
<div *ngIf="emailForm.get('senderAddress').hasError('required')">The Email Sender Address is required</div>
<div *ngIf="emailForm.get('senderAddress').hasError('incorrectMailFormat')">The Email Sender Address needs to be a valid email address</div>
<div *ngIf="emailForm.get('adminEmail').hasError('required')">The Email Sender is required</div>
<div *ngIf="emailForm.get('adminEmail').hasError('email')">The Admin Email needs to be a valid email address</div>
<div *ngIf="emailForm.get('username').hasError('required')">The Username is required</div>
<div *ngIf="emailForm.get('password').hasError('required')">The Password is required</div>
</div>
<div class="form-group">
<label for="host" class="control-label">SMTP Host</label>
<div>
<input type="text" class="form-control form-control-custom " id="host" name="host" placeholder="localhost" formControlName="host">
</div>
<input type="text" class="form-control form-control-custom " id="host" name="host" placeholder="localhost" formControlName="host" [ngClass]="{'form-error': emailForm.get('host').hasError('required')}">
<small *ngIf="emailForm.get('host').hasError('required')" class="error-text">The Host is required</small>
</div>
<div class="form-group">
<label for="portNumber" class="control-label">SMTP Port</label>
<div>
<input type="text" class="form-control form-control-custom " id="portNumber" name="Port" placeholder="Port Number" formControlName="port">
<input type="text" class="form-control form-control-custom " [ngClass]="{'form-error': emailForm.get('port').hasError('required')}" id="portNumber" name="Port" placeholder="Port Number" formControlName="port">
<small *ngIf="emailForm.get('port').hasError('required')" class="error-text">The Port is required</small>
</div>
</div>
<div class="form-group">
<label style="padding-left: 0" for="senderAddress" class="control-label col-md-12">Email Sender</label>
<div style="padding-left: 0" class="col-md-6">
<input type="text" class="form-control form-control-custom " id="senderAddress" name="senderAddress" formControlName="senderAddress" tooltipPosition="top" placeholder="Sender Address" pTooltip="The email address that the emails will be sent from">
<input type="text" class="form-control form-control-custom " id="senderAddress" [ngClass]="{'form-error': emailForm.get('senderAddress').hasError('required'), 'form-error': emailForm.get('senderAddress').hasError('incorrectMailFormat')}" name="senderAddress" formControlName="senderAddress" tooltipPosition="top" placeholder="Sender Address" pTooltip="The email address that the emails will be sent from">
<small *ngIf="emailForm.get('senderAddress').hasError('required')" class="error-text">The Email Sender Address is required</small>
<small *ngIf="emailForm.get('senderAddress').hasError('email') && !emailForm.get('senderAddress').hasError('required')" class="error-text">The Email Sender Address needs to be a valid email address</small>
</div>
<div style="padding-left: 0" class="col-md-6">
<input type="text" class="form-control form-control-custom " id="senderName" name="senderName" formControlName="senderName" tooltipPosition="top" placeholder="Sender Name" pTooltip="The 'Friendly' name that will appear in the 'FROM:' part of the email">
@ -58,24 +50,26 @@
<div class="form-group">
<label for="adminEmail" class="control-label">Admin Email</label>
<div>
<input type="text" class="form-control form-control-custom " id="adminEmail" name="adminEmail" formControlName="adminEmail" tooltipPosition="top" pTooltip="The administrator email will be used to send emails for admin only notifications (e.g. New Requests that require approvals)">
</div>
<input type="text" class="form-control form-control-custom" [ngClass]="{'form-error': emailForm.get('adminEmail').hasError('required'), 'form-error': emailForm.get('adminEmail').hasError('email')}" id="adminEmail" name="adminEmail" formControlName="adminEmail" tooltipPosition="top" pTooltip="The administrator email will be used to send emails for admin only notifications (e.g. New Requests that require approvals)">
<small *ngIf="emailForm.get('adminEmail').hasError('required')" class="error-text">The Admin Email Address is required</small>
<small *ngIf="emailForm.get('adminEmail').hasError('email') && !emailForm.get('adminEmail').hasError('required')" class="error-text">The Admin Email needs to be a valid email address</small>
</div>
<div class="form-group" *ngIf="emailForm.controls['username'].validator">
<label for="username" class="control-label">Username</label>
<div>
<input type="text" class="form-control form-control-custom " id="username" name="username" formControlName="username" pTooltip="The username if authentication is enabled" tooltipPosition="top">
</div>
<input type="text" class="form-control form-control-custom" [ngClass]="{'form-error': emailForm.get('username').hasError('required')}" id="username" name="username" formControlName="username" pTooltip="The username if authentication is enabled" tooltipPosition="top">
<small *ngIf="emailForm.get('username').hasError('required')" class="error-text">The Username is required</small>
</div>
<div class="form-group" *ngIf="emailForm.get('password').validator">
<label for="password" class="control-label">Password</label>
<div>
<input type="password" class="form-control form-control-custom " id="password" name="password" formControlName="password" pTooltip="The password if authentication is enabled" tooltipPosition="top">
</div>
<input type="password" class="form-control form-control-custom" [ngClass]="{'form-error': emailForm.get('password').hasError('required')}" id="password" name="password" formControlName="password" pTooltip="The password if authentication is enabled" tooltipPosition="top">
<small *ngIf="emailForm.get('password').hasError('required')" class="error-text">The Password is required</small>
</div>
<div class="form-group">

@ -13,16 +13,12 @@
</div>
</div>
<div *ngIf="form.invalid && form.dirty" class="alert alert-danger">
<div *ngIf="form.get('webhookUrl').hasError('required')">The Incoming Webhook Url is required</div>
</div>
<div class="form-group">
<small class="control-label"> Mattermost > Integrations > Incoming Webhook > Add Incoming Webhook. You will then have a Webhook</small>
<label for="webhookUrl" class="control-label">Incoming Webhook Url</label>
<div>
<input type="text" class="form-control form-control-custom " id="webhookUrl" name="webhookUrl" formControlName="webhookUrl">
</div>
<input type="text" class="form-control form-control-custom " id="webhookUrl" name="webhookUrl" formControlName="webhookUrl" [ngClass]="{'form-error': form.get('webhookUrl').hasError('required')}">
<small *ngIf="form.get('webhookUrl').hasError('required')" class="error-text">The Webhook Url is required</small>
</div>

@ -13,15 +13,12 @@
</div>
</div>
<div *ngIf="form.invalid && form.dirty" class="alert alert-danger">
<div *ngIf="form.get('accessToken').hasError('required')">The Access Token is required</div>
</div>
<small>You can find this here: <a href="https://www.pushbullet.com/#settings/account">https://www.pushbullet.com/#settings/account </a></small>
<div class="form-group">
<label for="accessToken" class="control-label">Access Token</label>
<div>
<input type="text" class="form-control form-control-custom " id="accessToken" name="accessToken" formControlName="accessToken">
</div>
<input type="text" class="form-control form-control-custom " id="accessToken" name="accessToken" formControlName="accessToken" [ngClass]="{'form-error': form.get('accessToken').hasError('required')}">
<small *ngIf="form.get('accessToken').hasError('required')" class="error-text">The Access Token is required</small>
</div>
<div class="form-group">

@ -13,14 +13,12 @@
</div>
</div>
<div *ngIf="form.invalid && form.dirty" class="alert alert-danger">
<div *ngIf="form.get('accessToken').hasError('required')">The Access Token is required</div>
</div>
<div class="form-group">
<label for="accessToken" class="control-label">Access Token</label>
<div>
<input type="text" class="form-control form-control-custom " id="accessToken" name="accessToken" formControlName="accessToken" pTooltip="Enter your API Key from Pushover.">
</div>
<input type="text" class="form-control form-control-custom " id="accessToken" name="accessToken" [ngClass]="{'form-error': form.get('accessToken').hasError('required')}" formControlName="accessToken" pTooltip="Enter your API Key from Pushover.">
<small *ngIf="form.get('accessToken').hasError('required')" class="error-text">The Access Token is required</small>
</div>
<div class="form-group">

@ -14,17 +14,15 @@
</div>
</div>
<div *ngIf="form.invalid && form.dirty" class="alert alert-danger">
<div *ngIf="form.get('webhookUrl').hasError('required')">The Webhook Url is required</div>
</div>
<div class="form-group">
<label for="webhookUrl" class="control-label">Webhook Url</label>
<div>
<small class="control-label"> Click <a target="_blank" href="https://my.slack.com/services/new/incoming-webhook/">Here</a> and follow the guide. You will then have a Webhook Url</small>
<input type="text" class="form-control form-control-custom " id="webhookUrl" name="webhookUrl" formControlName="webhookUrl">
<input type="text" class="form-control form-control-custom " id="webhookUrl" name="webhookUrl" formControlName="webhookUrl" [ngClass]="{'form-error': form.get('webhookUrl').hasError('required')}">
<small *ngIf="form.get('webhookUrl').hasError('required')" class="error-text">The Webhook Url is required</small>
</div>
</div>

@ -8,19 +8,6 @@
<p-inputSwitch id="customInputSwitch" [(ngModel)]="advanced"></p-inputSwitch>
</div>
<form novalidate [formGroup]="form" (ngSubmit)="onSubmit(form)" style="padding-top:5%;">
<div *ngIf="form.invalid" class="alert alert-danger">
<div *ngIf="form.dirty">
<div *ngIf="form.get('ip').hasError('required')">The IP/Hostname is required</div>
<div *ngIf="form.get('port').hasError('required')">The Port is required</div>
<div *ngIf="form.get('apiKey').hasError('required')">The Api Key is required</div>
</div>
<div>
<div *ngIf="form.get('defaultQualityProfile').hasError('required')">A Default Quality Profile is required</div>
<div *ngIf="form.get('defaultRootPath').hasError('required')">A Default Root Path is required</div>
<div *ngIf="form.get('minimumAvailability').hasError('required')">A Default Minimum Availability is required</div>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<div class="checkbox">
@ -33,25 +20,24 @@
<div class="form-group">
<label for="Ip" class="control-label">Hostname or IP</label>
<div class="">
<input type="text" class="form-control form-control-custom " id="Ip" name="Ip" placeholder="localhost" formControlName="ip">
</div>
<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>
<div class="">
<input type="text" class="form-control form-control-custom " formControlName="port" id="portNumber" name="Port" placeholder="Port Number">
</div>
<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>
<div>
<input type="text" class="form-control form-control-custom " id="ApiKey" name="ApiKey" formControlName="apiKey">
</div>
<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">
@ -76,10 +62,11 @@
<div class="form-group">
<label for="select" class="control-label">Quality Profiles</label>
<div id="profiles">
<select formControlName="defaultQualityProfile" class="form-control form-control-custom" id="select">
<select formControlName="defaultQualityProfile" class="form-control form-control-custom" id="select" [ngClass]="{'form-error': form.get('defaultQualityProfile').hasError('required')}">
<option *ngFor="let quality of qualities" value="{{quality.id}}">{{quality.name}}</option>
</select>
</div>
<small *ngIf="form.get('defaultQualityProfile').hasError('required')" class="error-text">A Default Quality Profile is required</small>
</div>
<div class="form-group">
@ -92,19 +79,22 @@
<div class="form-group">
<label for="rootFolders" class="control-label">Default Root Folders</label>
<div id="rootFolders">
<select formControlName="defaultRootPath" class="form-control form-control-custom">
<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>
</div>
</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="rootFolders" class="control-label">Default Minimum Availability</label>
<div id="rootFolders">
<select formControlName="minimumAvailability" class="form-control form-control-custom">
<select formControlName="minimumAvailability" class="form-control form-control-custom" [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')" class="error-text">A Default Minimum Availability is required</small>
</div>
<div class="form-group" *ngIf="advanced" style="color:#ff761b">

@ -8,10 +8,11 @@ import { ClipboardModule } from "ngx-clipboard/dist";
import { AuthGuard } from "../auth/auth.guard";
import { AuthModule } from "../auth/auth.module";
import { AuthService } from "../auth/auth.service";
import { JobService, RadarrService, SonarrService, TesterService, ValidationService } from "../services";
import { CouchPotatoService, JobService, RadarrService, SonarrService, TesterService, ValidationService } from "../services";
import { PipeModule } from "../pipes/pipe.module";
import { AboutComponent } from "./about/about.component";
import { CouchPotatoComponent } from "./couchpotato/couchpotato.component";
import { CustomizationComponent } from "./customization/customization.component";
import { EmbyComponent } from "./emby/emby.component";
import { LandingPageComponent } from "./landingpage/landingpage.component";
@ -51,6 +52,7 @@ const routes: Routes = [
{ path: "Settings/Mattermost", component: MattermostComponent, canActivate: [AuthGuard] },
{ path: "Settings/UserManagement", component: UserManagementComponent, canActivate: [AuthGuard] },
{ path: "Settings/Update", component: UpdateComponent, canActivate: [AuthGuard] },
{ path: "Settings/CouchPotato", component: CouchPotatoComponent, canActivate: [AuthGuard] },
];
@NgModule({
@ -91,6 +93,7 @@ const routes: Routes = [
UpdateComponent,
AboutComponent,
WikiComponent,
CouchPotatoComponent,
],
exports: [
RouterModule,
@ -103,6 +106,7 @@ const routes: Routes = [
ValidationService,
TesterService,
JobService,
CouchPotatoService,
],
})

@ -39,7 +39,7 @@
<i class="fa fa-film" aria-hidden="true"></i> Movies <span class="caret"></span>
</a>
<ul class="dropdown-menu">
<!--<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/CouchPotato']">CouchPotato</a></li>-->
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/CouchPotato']">CouchPotato (NOT YET READY)</a></li>
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Radarr']">Radarr</a></li>
<!--<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Watcher']">Watcher</a></li>-->
<li [routerLinkActive]="['active']"><a>More Coming Soon...</a></li>

@ -8,17 +8,7 @@
<p-inputSwitch id="customInputSwitch" [(ngModel)]="advanced"></p-inputSwitch>
</div>
<form novalidate [formGroup]="form" (ngSubmit)="onSubmit(form)" style="padding-top:5%;">
<div *ngIf="form.invalid" class="alert alert-danger">
<div *ngIf="form.dirty">
<div *ngIf="form.get('ip').hasError('required')">The IP/Hostname is required</div>
<div *ngIf="form.get('port').hasError('required')">The Port is required</div>
<div *ngIf="form.get('apiKey').hasError('required')">The Api Key is required</div>
</div>
<div>
<div *ngIf="form.get('qualityProfile').hasError('required')">A Default Quality Profile is required</div>
<div *ngIf="form.get('rootPath').hasError('required')">A Default Root Path is required</div>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<div class="checkbox">
@ -29,25 +19,24 @@
<div class="form-group">
<label for="Ip" class="control-label">Sonarr Hostname or IP</label>
<div class="">
<input type="text" class="form-control form-control-custom " formControlName="ip" id="Ip" name="Ip" placeholder="localhost">
</div>
<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>
</div>
<div class="form-group">
<label for="portNumber" class="control-label">Port</label>
<div class="">
<input type="text" class="form-control form-control-custom " formControlName="port" id="portNumber" name="Port" placeholder="Port Number">
</div>
<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>
</div>
<div class="form-group">
<label for="ApiKey" class="control-label">Sonarr API Key</label>
<div>
<input type="text" class="form-control form-control-custom " formControlName="apiKey" id="ApiKey" name="ApiKey">
</div>
<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>
</div>
<div class="form-group">
<div class="checkbox">
@ -72,10 +61,12 @@
<div class="form-group">
<label for="select" class="control-label">Quality Profiles</label>
<div id="profiles">
<select class="form-control form-control-custom" id="select" formControlName="qualityProfile">
<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">
@ -88,10 +79,12 @@
<div class="form-group">
<label for="rootFolders" class="control-label">Default Root Folders</label>
<div id="rootFolders">
<select class="form-control form-control-custom" formControlName="rootPath">
<select class="form-control form-control-custom" formControlName="rootPath" [ngClass]="{'form-error': form.get('rootPath').hasError('required')}">
<option *ngFor="let folder of rootFolders" value="{{folder.id}}">{{folder.path}}</option>
</select>
</div>
<small *ngIf="form.get('rootPath').hasError('required')" class="error-text">A Default Root Path is required</small>
</div>

@ -102,7 +102,7 @@ export class UserManagementComponent implements OnInit {
public runImporter(): void {
this.jobService.runPlexImporter().subscribe();
this.jobService.runPlexImporter().subscribe();
this.jobService.runEmbyImporter().subscribe();
}
private filter(query: string, users: IUsersModel[]): IUsersModel[] {

@ -816,3 +816,11 @@ a > h4 {
a > h4:hover {
text-decoration: underline;
}
.form-error {
border: 1px solid #d9534f;
}
.error-text {
color: #d9534f;
}

@ -0,0 +1,37 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Ombi.Api.CouchPotato;
using Ombi.Api.CouchPotato.Models;
using Ombi.Attributes;
using Ombi.Settings.Settings.Models.External;
namespace Ombi.Controllers.External
{
[Admin]
[ApiV1]
[Produces("application/json")]
public class CouchPotatoController
{
public CouchPotatoController(ICouchPotatoApi api)
{
_api = api;
}
private readonly ICouchPotatoApi _api;
[HttpPost("profile")]
public async Task<CouchPotatoProfiles> GetQualityProfiles([FromBody] CouchPotatoSettings settings)
{
var profiles = await _api.GetProfiles(settings.FullUri, settings.ApiKey);
return profiles;
}
[HttpPost("apikey")]
public async Task<CouchPotatoApiKey> GetApiKey([FromBody] CouchPotatoSettings settings)
{
var apiKey = await _api.GetApiKey(settings.FullUri, settings.Username, settings.Password);
return apiKey;
}
}
}

@ -13,9 +13,6 @@ using Ombi.Models.External;
namespace Ombi.Controllers.External
{
/// <summary>
///
/// </summary>
[Admin]
[ApiV1]
[Produces("application/json")]

@ -1,28 +1,33 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using Ombi.Api.Radarr;
using Ombi.Api.Radarr.Models;
using Ombi.Attributes;
using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Settings.Settings.Models.External;
namespace Ombi.Controllers.External
{
[Admin]
[PowerUser]
[ApiV1]
[Produces("application/json")]
public class RadarrController : Controller
{
public RadarrController(IRadarrApi radarr, ISettingsService<RadarrSettings> settings)
public RadarrController(IRadarrApi radarr, ISettingsService<RadarrSettings> settings,
IMemoryCache mem)
{
RadarrApi = radarr;
RadarrSettings = settings;
Cache = mem;
}
private IRadarrApi RadarrApi { get; }
private ISettingsService<RadarrSettings> RadarrSettings { get; }
private IMemoryCache Cache { get; }
/// <summary>
/// Gets the Radarr profiles.
/// </summary>
@ -44,5 +49,37 @@ namespace Ombi.Controllers.External
{
return await RadarrApi.GetRootFolders(settings.ApiKey, settings.FullUri);
}
/// <summary>
/// Gets the Radarr profiles using the saved settings
/// <remarks>The data is cached for an hour</remarks>
/// </summary>
/// <returns></returns>
[HttpGet("Profiles")]
public async Task<IEnumerable<RadarrProfile>> GetProfiles()
{
return await Cache.GetOrCreate(CacheKeys.RadarrQualityProfiles, async entry =>
{
entry.AbsoluteExpiration = DateTimeOffset.Now.AddHours(1);
var settings = await RadarrSettings.GetSettingsAsync();
return await RadarrApi.GetProfiles(settings.ApiKey, settings.FullUri);
});
}
/// <summary>
/// Gets the Radar root folders using the saved settings.
/// <remarks>The data is cached for an hour</remarks>
/// </summary>
/// <returns></returns>
[HttpGet("RootFolders")]
public async Task<IEnumerable<RadarrRootFolder>> GetRootFolders()
{
return await Cache.GetOrCreate(CacheKeys.RadarrRootProfiles, async entry =>
{
entry.AbsoluteExpiration = DateTimeOffset.Now.AddHours(1);
var settings = await RadarrSettings.GetSettingsAsync();
return await RadarrApi.GetRootFolders(settings.ApiKey, settings.FullUri);
});
}
}
}

@ -7,6 +7,7 @@ using Ombi.Api.Sonarr.Models;
using Ombi.Attributes;
using Ombi.Core.Settings;
using Ombi.Core.Settings.Models.External;
using Ombi.Settings.Settings.Models.External;
namespace Ombi.Controllers.External
{

@ -7,6 +7,7 @@ using AutoMapper;
using Hangfire;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.PlatformAbstractions;
using Ombi.Api.Emby;
using Ombi.Attributes;
@ -52,7 +53,8 @@ namespace Ombi.Controllers
IEmbyApi embyApi,
IPlexContentCacher cacher,
IEmbyContentCacher embyCacher,
IRadarrCacher radarrCacher)
IRadarrCacher radarrCacher,
IMemoryCache memCache)
{
SettingsResolver = resolver;
Mapper = mapper;
@ -61,6 +63,7 @@ namespace Ombi.Controllers
_plexContentCacher = cacher;
_embyContentCacher = embyCacher;
_radarrCacher = radarrCacher;
_cache = memCache;
}
private ISettingsResolver SettingsResolver { get; }
@ -70,6 +73,7 @@ namespace Ombi.Controllers
private readonly IPlexContentCacher _plexContentCacher;
private readonly IEmbyContentCacher _embyContentCacher;
private readonly IRadarrCacher _radarrCacher;
private readonly IMemoryCache _cache;
/// <summary>
/// Gets the Ombi settings.
@ -290,6 +294,8 @@ namespace Ombi.Controllers
var result = await Save(settings);
if (result)
{
_cache.Remove(CacheKeys.RadarrRootProfiles);
_cache.Remove(CacheKeys.RadarrQualityProfiles);
BackgroundJob.Enqueue(() => _radarrCacher.CacheContent());
}
return result;
@ -337,6 +343,26 @@ namespace Ombi.Controllers
return await Get<UpdateSettings>();
}
/// <summary>
/// Gets the CouchPotatoSettings Settings.
/// </summary>
/// <returns></returns>
[HttpGet("CouchPotato")]
public async Task<CouchPotatoSettings> CouchPotatoSettings()
{
return await Get<CouchPotatoSettings>();
}
/// <summary>
/// Save the CouchPotatoSettings settings.
/// </summary>
/// <param name="settings">The settings.</param>
/// <returns></returns>
[HttpPost("CouchPotato")]
public async Task<bool> CouchPotatoSettings([FromBody]CouchPotatoSettings settings)
{
return await Save(settings);
}
/// <summary>
/// Saves the email notification settings.

@ -9,6 +9,8 @@ using Ombi.Store.Entities;
using CommandLine;
using CommandLine.Text;
using Microsoft.AspNetCore;
using Microsoft.EntityFrameworkCore;
using Ombi.Helpers;
namespace Ombi
{
@ -33,6 +35,8 @@ namespace Ombi
UrlArgs = host;
var urlValue = string.Empty;
var instance = StoragePathSingleton.Instance;
instance.StoragePath = storagePath ?? string.Empty;
using (var ctx = new OmbiContext())
{
var config = ctx.ApplicationConfigurations.ToList();

@ -16,6 +16,7 @@ using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.SpaServices.Webpack;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@ -36,6 +37,7 @@ namespace Ombi
{
public class Startup
{
public static StoragePathSingleton StoragePath => StoragePathSingleton.Instance;
public Startup(IHostingEnvironment env)
{
Console.WriteLine(env.ContentRootPath);
@ -48,11 +50,24 @@ namespace Ombi
//if (env.IsDevelopment())
//{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.RollingFile(Path.Combine(env.ContentRootPath, "Logs", "log-{Date}.txt"))
.WriteTo.SQLite("Ombi.db", "Logs", LogEventLevel.Debug)
.CreateLogger();
Serilog.ILogger config;
if (string.IsNullOrEmpty(StoragePath.StoragePath))
{
config = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.SQLite("Ombi.db", "Logs", LogEventLevel.Debug)
.CreateLogger();
}
else
{
config = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.SQLite(Path.Combine(StoragePath.StoragePath, "Ombi.db"), "Logs", LogEventLevel.Debug)
.CreateLogger();
}
Log.Logger = config;
//}
//if (env.IsProduction())
//{
@ -69,9 +84,10 @@ namespace Ombi
// This method gets called by the runtime. Use this method to add services to the container.
public IServiceProvider ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddDbContext<OmbiContext>();
services.AddIdentity<OmbiUser, IdentityRole>()
.AddEntityFrameworkStores<OmbiContext>()
.AddDefaultTokenProviders()
@ -113,6 +129,7 @@ namespace Ombi
x.UseConsole();
});
// Build the intermediate service provider
return services.BuildServiceProvider();
}
@ -138,11 +155,20 @@ namespace Ombi
});
}
app.UseHangfireServer();
app.UseHangfireDashboard("/hangfire", new DashboardOptions
var ombiService =
app.ApplicationServices.GetService<ISettingsService<OmbiSettings>>();
var settings = ombiService.GetSettings();
if (settings.BaseUrl.HasValue())
{
Authorization = new[] { new HangfireAuthorizationFilter() }
});
app.UsePathBase(settings.BaseUrl);
}
app.UseHangfireServer();
app.UseHangfireDashboard(settings.BaseUrl.HasValue() ? $"{settings.BaseUrl}/hangfire" : "/hangfire",
new DashboardOptions
{
Authorization = new[] {new HangfireAuthorizationFilter()}
});
// Setup the scheduler
var jobSetup = app.ApplicationServices.GetService<IJobSetup>();
@ -151,14 +177,6 @@ namespace Ombi
var provider = new FileExtensionContentTypeProvider { Mappings = { [".map"] = "application/octet-stream" } };
var ombiService =
app.ApplicationServices.GetService<ISettingsService<OmbiSettings>>();
var settings = ombiService.GetSettings();
if (settings.BaseUrl.HasValue())
{
app.UsePathBase(settings.BaseUrl);
}
app.UseStaticFiles(new StaticFileOptions()
{
ContentTypeProvider = provider,

Loading…
Cancel
Save