Lots and Lots of work

pull/1425/head
Jamie.Rees 8 years ago
parent 539419750b
commit 596008b06c

@ -20,7 +20,8 @@ namespace Ombi.Api.TvMaze
var request = new Request("search/shows", Uri, HttpMethod.Get);
request.AddQueryString("q", searchTerm);
request.AddHeader("Content-Type", "application/json");
//request.ContentHeaders.Add("Content-Type", "application/json");
request.ContentHeaders.Add(new KeyValuePair<string, string>("Content-Type","application/json"));
return await Api.Request<List<TvMazeSearch>>(request);
}

@ -82,7 +82,7 @@ namespace Ombi.Api
{
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(value)) return;
var builder = new UriBuilder(_modified);
var builder = new UriBuilder(FullUri);
var startingTag = string.Empty;
var hasQuery = false;
if (string.IsNullOrEmpty(builder.Query))

@ -31,6 +31,6 @@ namespace Ombi.Core.Claims
public const string Admin = nameof(Admin);
public const string AutoApproveMovie = nameof(AutoApproveMovie);
public const string AutoApproveTv = nameof(AutoApproveTv);
public const string PowerUser = nameof(PowerUser);
}
}

@ -8,7 +8,7 @@ namespace Ombi.Core
{
Task<IEnumerable<SearchMovieViewModel>> NowPlayingMovies();
Task<IEnumerable<SearchMovieViewModel>> PopularMovies();
Task<IEnumerable<SearchMovieViewModel>> ProcessMovieSearch(string search);
Task<IEnumerable<SearchMovieViewModel>> Search(string search);
Task<IEnumerable<SearchMovieViewModel>> TopRatedMovies();
Task<IEnumerable<SearchMovieViewModel>> UpcomingMovies();
Task<IEnumerable<SearchMovieViewModel>> LookupImdbInformation(IEnumerable<SearchMovieViewModel> movies);

@ -0,0 +1,11 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Ombi.Core.Models.Search;
namespace Ombi.Core.Engine
{
public interface ITvSearchEngine
{
Task<IEnumerable<SearchTvShowViewModel>> Search(string searchTerm);
}
}

@ -3,6 +3,7 @@ using System.Linq;
using System.Security.Principal;
using System.Threading.Tasks;
using AutoMapper;
using Microsoft.Extensions.Logging;
using Ombi.Api.TheMovieDb;
using Ombi.Api.TheMovieDb.Models;
using Ombi.Core.IdentityResolver;
@ -18,19 +19,22 @@ namespace Ombi.Core.Engine
public class MovieSearchEngine : BaseMediaEngine, IMovieEngine
{
public MovieSearchEngine(IPrincipal identity, IRequestService service, IMovieDbApi movApi, IMapper mapper, ISettingsService<PlexSettings> plexSettings, ISettingsService<EmbySettings> embySettings)
public MovieSearchEngine(IPrincipal identity, IRequestService service, IMovieDbApi movApi, IMapper mapper, ISettingsService<PlexSettings> plexSettings, ISettingsService<EmbySettings> embySettings,
ILogger<MovieSearchEngine> logger)
: base(identity, service)
{
MovieApi = movApi;
Mapper = mapper;
PlexSettings = plexSettings;
EmbySettings = embySettings;
Logger = logger;
}
private IMovieDbApi MovieApi { get; }
private IMapper Mapper { get; }
private ISettingsService<PlexSettings> PlexSettings { get; }
private ISettingsService<EmbySettings> EmbySettings { get; }
private ILogger<MovieSearchEngine> Logger { get; }
public async Task<IEnumerable<SearchMovieViewModel>> LookupImdbInformation(IEnumerable<SearchMovieViewModel> movies)
{
@ -58,11 +62,12 @@ namespace Ombi.Core.Engine
return retVal;
}
public async Task<IEnumerable<SearchMovieViewModel>> ProcessMovieSearch(string search)
public async Task<IEnumerable<SearchMovieViewModel>> Search(string search)
{
var result = await MovieApi.SearchMovie(search);
if (result != null)
{
Logger.LogDebug("Search Result: {result}", result);
return await TransformMovieResultsToResponse(result);
}
return null;
@ -72,6 +77,7 @@ namespace Ombi.Core.Engine
var result = await MovieApi.PopularMovies();
if (result != null)
{
Logger.LogDebug("Search Result: {result}", result);
return await TransformMovieResultsToResponse(result);
}
return null;
@ -82,6 +88,7 @@ namespace Ombi.Core.Engine
var result = await MovieApi.TopRated();
if (result != null)
{
Logger.LogDebug("Search Result: {result}", result);
return await TransformMovieResultsToResponse(result);
}
return null;
@ -92,6 +99,7 @@ namespace Ombi.Core.Engine
var result = await MovieApi.Upcoming();
if (result != null)
{
Logger.LogDebug("Search Result: {result}", result);
return await TransformMovieResultsToResponse(result);
}
return null;
@ -101,6 +109,8 @@ namespace Ombi.Core.Engine
var result = await MovieApi.NowPlaying();
if (result != null)
{
Logger.LogDebug("Search Result: {result}", result);
return await TransformMovieResultsToResponse(result);
}
return null;
@ -125,7 +135,7 @@ namespace Ombi.Core.Engine
private async Task<SearchMovieViewModel> ProcessSingleMovie(SearchMovieViewModel viewMovie,
Dictionary<int, RequestModel> existingRequests, PlexSettings plexSettings, EmbySettings embySettings)
{
{
if (plexSettings.Enable)
{
// var content = PlexContentRepository.GetAll();

@ -1,13 +1,20 @@
using System.Security.Principal;
using System.Collections.Generic;
using System.Linq;
using System.Security.Principal;
using System.Threading.Tasks;
using AutoMapper;
using Ombi.Api.TvMaze;
using Ombi.Api.TvMaze.Models;
using Ombi.Core.Models.Requests;
using Ombi.Core.Models.Search;
using Ombi.Core.Requests.Models;
using Ombi.Core.Settings;
using Ombi.Core.Settings.Models.External;
using Ombi.Store.Entities;
namespace Ombi.Core.Engine
{
public class TvSearchEngine : BaseMediaEngine
public class TvSearchEngine : BaseMediaEngine, ITvSearchEngine
{
public TvSearchEngine(IPrincipal identity, IRequestService service, ITvMazeApi tvMaze, IMapper mapper, ISettingsService<PlexSettings> plexSettings, ISettingsService<EmbySettings> embySettings)
@ -24,6 +31,74 @@ namespace Ombi.Core.Engine
private ISettingsService<PlexSettings> PlexSettings { get; }
private ISettingsService<EmbySettings> EmbySettings { get; }
public async Task<IEnumerable<SearchTvShowViewModel>> Search(string searchTerm)
{
var searchResult = await TvMazeApi.Search(searchTerm);
if (searchResult != null)
{
return await ProcessResults(searchResult);
}
return null;
}
private async Task<IEnumerable<SearchTvShowViewModel>> ProcessResults(IEnumerable<TvMazeSearch> items)
{
var existingRequests = await GetRequests(RequestType.TvShow);
var plexSettings = await PlexSettings.GetSettingsAsync();
var embySettings = await EmbySettings.GetSettingsAsync();
var retVal = new List<SearchTvShowViewModel>();
foreach (var tvMazeSearch in items)
{
retVal.Add(ProcessResult(tvMazeSearch, existingRequests, plexSettings, embySettings));
}
return retVal;
}
private SearchTvShowViewModel ProcessResult(TvMazeSearch item, Dictionary<int, RequestModel> existingRequests, PlexSettings plexSettings, EmbySettings embySettings)
{
var viewT = Mapper.Map<SearchTvShowViewModel>(item);
if (embySettings.Enable)
{
//var embyShow = EmbyChecker.GetTvShow(embyCached.ToArray(), t.show.name, t.show.premiered?.Substring(0, 4), providerId);
//if (embyShow != null)
//{
// viewT.Available = true;
//}
}
if (plexSettings.Enable)
{
//var plexShow = PlexChecker.GetTvShow(plexTvShows.ToArray(), t.show.name, t.show.premiered?.Substring(0, 4),
// providerId);
//if (plexShow != null)
//{
// viewT.Available = true;
// viewT.PlexUrl = plexShow.Url;
//}
}
if (item.show?.externals?.thetvdb != null && !viewT.Available)
{
var tvdbid = (int)item.show.externals.thetvdb;
if (existingRequests.ContainsKey(tvdbid))
{
var dbt = existingRequests[tvdbid];
viewT.Requested = true;
viewT.Episodes = dbt.Episodes.ToList();
viewT.Approved = dbt.Approved;
}
//if (sonarrCached.Select(x => x.TvdbId).Contains(tvdbid) || sickRageCache.Contains(tvdbid))
// // compare to the sonarr/sickrage db
//{
// viewT.Requested = true;
//}
}
return viewT;
}
}
}

@ -6,9 +6,10 @@ namespace Ombi.Core.IdentityResolver
{
public interface IUserIdentityManager
{
Task CreateUser(UserDto user);
Task<UserDto> CreateUser(UserDto user);
Task<bool> CredentialsValid(string username, string password);
Task<UserDto> GetUser(string username);
Task<IEnumerable<UserDto>> GetUsers();
Task DeleteUser(UserDto user);
}
}

@ -68,13 +68,20 @@ namespace Ombi.Core.IdentityResolver
return Mapper.Map<List<UserDto>>(await UserRepository.GetUsers());
}
public async Task CreateUser(UserDto userDto)
public async Task<UserDto> CreateUser(UserDto userDto)
{
var user = Mapper.Map<User>(userDto);
var result = HashPassword(user.Password);
user.Password = result.HashedPass;
user.Salt = result.Salt;
await UserRepository.CreateUser(user);
return Mapper.Map<UserDto>(user);
}
public async Task DeleteUser(UserDto user)
{
await UserRepository.DeleteUser(Mapper.Map<User>(user));
}
private UserHash HashPassword(string password)

@ -0,0 +1,47 @@
using System.Collections.Generic;
using Ombi.Core.Models.Requests;
namespace Ombi.Core.Models.Search
{
public class SearchTvShowViewModel : SearchViewModel
{
public SearchTvShowViewModel()
{
Episodes = new List<EpisodesModel>();
}
public int Id { get; set; }
public string SeriesName { get; set; }
public List<string> Aliases { get; set; }
public string Banner { get; set; }
public int SeriesId { get; set; }
public string Status { get; set; }
public string FirstAired { get; set; }
public string Network { get; set; }
public string NetworkId { get; set; }
public string Runtime { get; set; }
public List<string> Genre { get; set; }
public string Overview { get; set; }
public int LastUpdated { get; set; }
public string AirsDayOfWeek { get; set; }
public string AirsTime { get; set; }
public string Rating { get; set; }
public string ImdbId { get; set; }
public int SiteRating { get; set; }
public List<EpisodesModel> Episodes { get; set; }
/// <summary>
/// This is used from the Trakt API
/// </summary>
/// <value>
/// The trailer.
/// </value>
public string Trailer { get; set; }
/// <summary>
/// This is used from the Trakt API
/// </summary>
/// <value>
/// The trailer.
/// </value>
public string Homepage { get; set; }
}
}

@ -0,0 +1,15 @@
using System.Collections.Generic;
namespace Ombi.Core.Models.UI
{
public class UserViewModel
{
public string Id { get; set; }
public string Username { get; set; }
public string Alias { get; set; }
public List<string> Claims { get; set; }
public string EmailAddress { get; set; }
public string Password { get; set; }
public UserType UserType { get; set; }
}
}

@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Security.Claims;
using System.Security.Principal;
namespace Ombi.Core.Models
{
@ -13,6 +14,9 @@ namespace Ombi.Core.Models
public string Password { get; set; }
public byte[] Salt { get; set; }
public UserType UserType { get; set; }
}
public enum UserType
{
@ -20,4 +24,6 @@ namespace Ombi.Core.Models
PlexUser = 2,
EmbyUser = 3,
}
}

@ -16,6 +16,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Ombi.Api.Plex\Ombi.Api.Plex.csproj" />
<ProjectReference Include="..\Ombi.Api.TvMaze\Ombi.Api.TvMaze.csproj" />
<ProjectReference Include="..\Ombi.Helpers\Ombi.Helpers.csproj" />
<ProjectReference Include="..\Ombi.Notifications\Ombi.Notifications.csproj" />

@ -46,6 +46,7 @@ namespace Ombi.DependencyInjection
{
services.AddTransient<IMovieEngine, MovieSearchEngine>();
services.AddTransient<IRequestEngine, RequestEngine>();
services.AddTransient<ITvSearchEngine, TvSearchEngine>();
return services;
}

@ -0,0 +1,19 @@
using System.Text.RegularExpressions;
namespace Ombi.Helpers
{
public static class HtmlHelper
{
public static string RemoveHtml(this string value)
{
if (string.IsNullOrEmpty(value))
{
return string.Empty;
}
var step1 = Regex.Replace(value, @"<[^>]+>|&nbsp;", "").Trim();
var step2 = Regex.Replace(step1, @"\s{2,}", " ");
return step2;
}
}
}

@ -1,6 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using AutoMapper;
using Ombi.Core.Models;
using Ombi.Core.Models.UI;
using Ombi.Store.Entities;
namespace Ombi.Mapping.Profiles
@ -11,6 +15,14 @@ namespace Ombi.Mapping.Profiles
{
CreateMap<User, UserDto>().ReverseMap();
CreateMap<UserDto, UserViewModel>()
.ForMember(dest => dest.Claims, opts => opts.MapFrom(src => src.Claims.Select(x => x.Value).ToList())); // Map the claims to a List<string>
CreateMap<string, Claim>()
.ConstructUsing(str => new Claim(ClaimTypes.Role, str)); // This is used for the UserViewModel List<string> claims => UserDto List<claim>
CreateMap<UserViewModel, UserDto>();
CreateMap<string, DateTime>().ConvertUsing<StringToDateTimeConverter>();
}
}

@ -0,0 +1,28 @@
using System.Globalization;
using AutoMapper;
using Ombi.Api.TvMaze.Models;
using Ombi.Core.Models.Search;
using Ombi.Helpers;
namespace Ombi.Mapping.Profiles
{
public class TvProfile : Profile
{
public TvProfile()
{
CreateMap<TvMazeSearch, SearchTvShowViewModel>()
.ForMember(dest => dest.Id, opts => opts.MapFrom(src => src.show.externals.thetvdb))
.ForMember(dest => dest.FirstAired, opts => opts.MapFrom(src => src.show.premiered))
.ForMember(dest => dest.ImdbId, opts => opts.MapFrom(src => src.show.externals.imdb))
.ForMember(dest => dest.Network, opts => opts.MapFrom(src => src.show.network.name))
.ForMember(dest => dest.NetworkId, opts => opts.MapFrom(src => src.show.network.id.ToString()))
.ForMember(dest => dest.Overview, opts => opts.MapFrom(src => src.show.summary.RemoveHtml()))
.ForMember(dest => dest.Rating, opts => opts.MapFrom(src => src.score.ToString(CultureInfo.CurrentUICulture)))
.ForMember(dest => dest.Runtime, opts => opts.MapFrom(src => src.show.runtime.ToString()))
.ForMember(dest => dest.SeriesId, opts => opts.MapFrom(src => src.show.id))
.ForMember(dest => dest.SeriesName, opts => opts.MapFrom(src => src.show.name))
.ForMember(dest => dest.Banner, opts => opts.MapFrom(src => !string.IsNullOrEmpty(src.show.image.medium) ? src.show.image.medium.Replace("http","https") : string.Empty))
.ForMember(dest => dest.Status, opts => opts.MapFrom(src => src.show.status));
}
}
}

@ -1,4 +1,6 @@
using System;
using System.Linq;
using System.Reflection;
using Hangfire;
using Microsoft.Extensions.DependencyInjection;
@ -14,7 +16,8 @@ namespace Ombi.Schedule
public override object ActivateJob(Type type)
{
return _container.GetService(type);
var i = type.GetTypeInfo().ImplementedInterfaces.FirstOrDefault();
return _container.GetService(i);
}
}
}

@ -14,7 +14,7 @@ namespace Ombi.Schedule
private IPlexContentCacher Cacher { get; }
public void Setup()
{
RecurringJob.AddOrUpdate(() => Cacher.CacheContent(), Cron.Hourly, TimeZoneInfo.Utc, "cacher");
RecurringJob.AddOrUpdate(() => Cacher.CacheContent(), Cron.Minutely);
}
}
}

@ -28,6 +28,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Logging;
using Ombi.Api.Plex;
using Ombi.Api.Plex.Models;
using Ombi.Core.Settings;
@ -37,14 +38,16 @@ namespace Ombi.Schedule.Jobs
{
public partial class PlexContentCacher : IPlexContentCacher
{
public PlexContentCacher(ISettingsService<PlexSettings> plex, IPlexApi plexApi)
public PlexContentCacher(ISettingsService<PlexSettings> plex, IPlexApi plexApi, ILogger<PlexContentCacher> logger)
{
Plex = plex;
PlexApi = plexApi;
Logger = logger;
}
private ISettingsService<PlexSettings> Plex { get; }
private IPlexApi PlexApi { get; }
private ILogger<PlexContentCacher> Logger { get; }
public void CacheContent()
{
@ -57,6 +60,8 @@ namespace Ombi.Schedule.Jobs
{
return;
}
Logger.LogInformation("Starting Plex Content Cacher");
//TODO
//var libraries = CachedLibraries(plexSettings);

@ -9,6 +9,7 @@
<PackageReference Include="Hangfire.AspNetCore" Version="1.6.12" />
<PackageReference Include="Hangfire.MemoryStorage.Core" Version="1.4.0" />
<PackageReference Include="Hangfire.RecurringJobExtensions" Version="1.1.6" />
<PackageReference Include="Serilog" Version="2.4.0" />
</ItemGroup>
<ItemGroup>

@ -0,0 +1,8 @@
namespace Ombi.Settings.Settings.Models
{
public class CustomizationSettings : Settings
{
public string ApplicationName { get; set; }
public string Logo { get; set; }
}
}

@ -91,8 +91,8 @@ namespace Ombi.Settings.Settings
modified.Id = entity.Id;
var globalSettings = new GlobalSettings { SettingsName = EntityName, Content = JsonConvert.SerializeObject(modified, SerializerSettings.Settings), Id = entity.Id };
globalSettings.Content = EncryptSettings(globalSettings);
await Repo.UpdateAsync(globalSettings).ConfigureAwait(false);
entity.Content = EncryptSettings(globalSettings);
await Repo.UpdateAsync(entity).ConfigureAwait(false);
return true;
}

@ -15,5 +15,6 @@ namespace Ombi.Store.Context
DbSet<GlobalSettings> Settings { get; set; }
DbSet<User> Users { get; set; }
EntityEntry<GlobalSettings> Entry(GlobalSettings settings);
EntityEntry<TEntity> Attach<TEntity>(TEntity entity) where TEntity : class;
}
}

@ -9,5 +9,6 @@ namespace Ombi.Store.Repository
Task CreateUser(User user);
Task<User> GetUser(string username);
Task<IEnumerable<User>> GetUsers();
Task DeleteUser(User user);
}
}

@ -76,7 +76,6 @@ namespace Ombi.Store.Repository
public void Update(GlobalSettings entity)
{
Db.Entry(entity).State = EntityState.Modified;
Db.SaveChanges();
}
}

@ -58,5 +58,11 @@ namespace Ombi.Store.Repository
{
return await Db.Users.ToListAsync();
}
public async Task DeleteUser(User user)
{
Db.Users.Remove(user);
await Db.SaveChangesAsync();
}
}
}

@ -6,7 +6,7 @@ namespace Ombi.Attributes
{
public AdminAttribute()
{
base.Roles = "Admin";
Roles = "Admin";
}
}

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using Ombi.Core.Claims;
namespace Ombi.Attributes
{
@ -6,7 +7,12 @@ namespace Ombi.Attributes
{
public PowerUserAttribute()
{
Roles = "Admin, PowerUser";
var roles = new []
{
OmbiClaims.Admin,
OmbiClaims.PowerUser
};
Roles = string.Join(",",roles);
}
}
}

@ -30,8 +30,8 @@ namespace Ombi.Auth
/// <summary>
/// The expiration time for the generated tokens.
/// </summary>
/// <remarks>The default is five minutes (300 seconds).</remarks>
public TimeSpan Expiration { get; set; } = TimeSpan.FromMinutes(5);
/// <remarks>The default is 7 Days.</remarks>
public TimeSpan Expiration { get; set; } = TimeSpan.FromDays(7);
/// <summary>
/// The signing key to use when generating tokens.

@ -2,12 +2,14 @@
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using AutoMapper;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Ombi.Attributes;
using Ombi.Core.Claims;
using Ombi.Core.IdentityResolver;
using Ombi.Core.Models;
using Ombi.Core.Models.UI;
using Ombi.Models;
namespace Ombi.Controllers
@ -15,12 +17,14 @@ namespace Ombi.Controllers
[PowerUser]
public class IdentityController : BaseV1ApiController
{
public IdentityController(IUserIdentityManager identity)
public IdentityController(IUserIdentityManager identity, IMapper mapper)
{
IdentityManager = identity;
Mapper = mapper;
}
private IUserIdentityManager IdentityManager { get; }
private IMapper Mapper { get; }
[HttpGet]
public async Task<UserDto> GetUser()
@ -58,6 +62,26 @@ namespace Ombi.Controllers
return true;
}
[HttpGet("Users")]
public async Task<IEnumerable<UserViewModel>> GetAllUsers()
{
return Mapper.Map<IEnumerable<UserViewModel>>(await IdentityManager.GetUsers());
}
[HttpPost]
public async Task<UserViewModel> CreateUser([FromBody] UserViewModel user)
{
var userResult = await IdentityManager.CreateUser(Mapper.Map<UserDto>(user));
return Mapper.Map<UserViewModel>(userResult);
}
[HttpDelete]
public async Task<StatusCodeResult> DeleteUser([FromBody] UserViewModel user)
{
await IdentityManager.DeleteUser(Mapper.Map<UserDto>(user));
return Ok();
}
}
}

@ -1,10 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Ombi.Core;
using Ombi.Core.Engine;
using Ombi.Core.Models.Search;
namespace Ombi.Controllers
@ -12,17 +14,22 @@ namespace Ombi.Controllers
[Authorize]
public class SearchController : BaseV1ApiController
{
public SearchController(IMovieEngine movie)
public SearchController(IMovieEngine movie, ITvSearchEngine tvEngine, ILogger<SearchController> logger)
{
MovieEngine = movie;
TvEngine = tvEngine;
Logger = logger;
}
private ILogger<SearchController> Logger { get; }
private IMovieEngine MovieEngine { get; }
private ITvSearchEngine TvEngine { get; }
[HttpGet("movie/{searchTerm}")]
public async Task<IEnumerable<SearchMovieViewModel>> SearchMovie(string searchTerm)
{
return await MovieEngine.ProcessMovieSearch(searchTerm);
Logger.LogDebug("Searching : {searchTerm}", searchTerm);
return await MovieEngine.Search(searchTerm);
}
[HttpPost("movie/extrainfo")]
@ -52,7 +59,10 @@ namespace Ombi.Controllers
return await MovieEngine.UpcomingMovies();
}
[HttpGet("tv/{searchTerm}")]
public async Task<IEnumerable<SearchTvShowViewModel>> SearchTv(string searchTerm)
{
return await TvEngine.Search(searchTerm);
}
}
}

@ -1,11 +1,11 @@
using System;
using System.Threading.Tasks;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Ombi.Attributes;
using Ombi.Core.Settings;
using Ombi.Core.Settings.Models;
using Ombi.Core.Settings.Models.External;
using Ombi.Settings.Settings.Models;
namespace Ombi.Controllers
{
@ -68,6 +68,19 @@ namespace Ombi.Controllers
return await Save(settings);
}
[HttpGet("customization")]
[AllowAnonymous]
public async Task<CustomizationSettings> CustomizationSettings()
{
return await Get<CustomizationSettings>();
}
[HttpPost("customization")]
public async Task<bool> CustomizationSettings([FromBody]CustomizationSettings settings)
{
return await Save(settings);
}
private async Task<T> Get<T>()
{

File diff suppressed because one or more lines are too long

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>netcoreapp1.1</TargetFramework>
<RuntimeIdentifiers>win10-x64;osx.10.12-x64;ubuntu.16.10-x64;debian.8-x64;</RuntimeIdentifiers>
<RuntimeIdentifiers>win10-x64;osx.10.12-x64;ubuntu.16.10-x64;debian.8-x64;centos.7-x64;</RuntimeIdentifiers>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
</PropertyGroup>
@ -30,6 +30,11 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer.Design" Version="1.1.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="1.1.1" />
<PackageReference Include="Microsoft.VisualStudio.Web.BrowserLink" Version="1.1.0" />
<PackageReference Include="Serilog" Version="2.4.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="1.4.0" />
<PackageReference Include="Serilog.Sinks.File" Version="3.2.0" />
<PackageReference Include="Serilog.Sinks.RollingFile" Version="3.3.0" />
<PackageReference Include="Serilog.Sinks.SQLite" Version="3.8.3" />
<PackageReference Include="System.Security.Cryptography.Csp" Version="4.3.0" />
</ItemGroup>
@ -42,4 +47,34 @@
<ProjectReference Include="..\Ombi.Settings\Ombi.Settings.csproj" />
<ProjectReference Include="..\Ombi.TheMovieDbApi\Ombi.Api.TheMovieDb.csproj" />
</ItemGroup>
<ItemGroup>
<Content Update="wwwroot\app\interfaces\ISearchMovieResult - Copy.js">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Update="wwwroot\app\interfaces\ISearchTvResult.ts">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Update="wwwroot\app\search\moviesearch - Copy.component.html">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Update="wwwroot\app\search\moviesearch - Copy.component.js">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Update="wwwroot\app\search\moviesearch - Copy.component.js.map">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Update="wwwroot\app\search\moviesearch - Copy.component.ts">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Update="wwwroot\app\usermanagement\request.component.js">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Update="wwwroot\app\usermanagement\request.component.js.map">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Update="wwwroot\app\usermanagement\request.component.ts">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
</ItemGroup>
</Project>

@ -45,14 +45,14 @@ namespace Ombi
// Validate the token expiry
ValidateLifetime = true,
// If you want to allow a certain amount of clock drift, set that here:
ClockSkew = TimeSpan.Zero
ClockSkew = TimeSpan.Zero,
};
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
TokenValidationParameters = tokenValidationParameters
TokenValidationParameters = tokenValidationParameters,
});
app.UseMiddleware<TokenProviderMiddleware>(Options.Create(tokenProviderOptions));

@ -1,4 +1,5 @@
using System;
using System.IO;
using System.Security.Principal;
using AutoMapper;
using AutoMapper.EquivalencyExpression;
@ -15,6 +16,8 @@ using Microsoft.Extensions.Logging;
using Ombi.DependencyInjection;
using Ombi.Mapping;
using Ombi.Schedule;
using Serilog;
using Serilog.Events;
namespace Ombi
{
@ -30,6 +33,22 @@ namespace Ombi
.AddEnvironmentVariables();
Configuration = builder.Build();
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();
}
if (env.IsProduction())
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.WriteTo.RollingFile(Path.Combine(env.ContentRootPath, "Logs", "log-{Date}.txt"))
.WriteTo.SQLite("Ombi.db", "Logs", LogEventLevel.Debug)
.CreateLogger();
}
}
public IConfigurationRoot Configuration { get; }
@ -45,15 +64,16 @@ namespace Ombi
expression.AddCollectionMappers();
});
services.RegisterDependencies(); // Ioc and EF
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<IPrincipal>(sp => sp.GetService<IHttpContextAccessor>().HttpContext.User);
services.AddHangfire(x =>
{
x.UseMemoryStorage(new MemoryStorageOptions());
//x.UseActivator(new IoCJobActivator(services.BuildServiceProvider()));
x.UseActivator(new IoCJobActivator(services.BuildServiceProvider()));
});
}
@ -61,9 +81,11 @@ namespace Ombi
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
//loggerFactory.AddConsole(Configuration.GetSection("Logging"));
//loggerFactory.AddDebug();
loggerFactory.AddSerilog();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
@ -79,13 +101,13 @@ namespace Ombi
ConfigureAuth(app);
//var provider = new FileExtensionContentTypeProvider();
//provider.Mappings[".map"] = "application/octet-stream";
var provider = new FileExtensionContentTypeProvider();
provider.Mappings[".map"] = "application/octet-stream";
//app.UseStaticFiles(new StaticFileOptions()
//{
// ContentTypeProvider = provider
//});
app.UseStaticFiles(new StaticFileOptions()
{
ContentTypeProvider = provider
});
app.UseMvc(routes =>
{

@ -239,4 +239,11 @@ button.list-group-item:focus {
text-align: center;
font-size: 15px;
padding: 3px 0;
}
.nav .open > a,
.nav .open > a:hover,
.nav .open > a:focus {
background-color: $bg-colour;
border-color: $bg-colour-disabled;
}

@ -3,17 +3,17 @@
"version": "1.0.0",
"private": true,
"dependencies": {
"@angular/animations": "^4.0.2",
"@angular/common": "^4.0.2",
"@angular/compiler": "^4.0.2",
"@angular/compiler-cli": "^4.0.2",
"@angular/core": "^4.0.2",
"@angular/forms": "^4.0.2",
"@angular/http": "^4.0.2",
"@angular/platform-browser": "^4.0.2",
"@angular/platform-browser-dynamic": "^4.0.2",
"@angular/platform-server": "^4.0.2",
"@angular/router": "^4.0.0",
"@angular/animations": "^4.1.0",
"@angular/common": "^4.1.0",
"@angular/compiler": "^4.1.0",
"@angular/compiler-cli": "^4.1.0",
"@angular/core": "^4.1.0",
"@angular/forms": "^4.1.0",
"@angular/http": "^4.1.0",
"@angular/platform-browser": "^4.1.0",
"@angular/platform-browser-dynamic": "^4.1.0",
"@angular/platform-server": "^4.1.0",
"@angular/router": "^4.1.0",
"@types/jquery": "^2.0.33",
"@types/systemjs": "^0.20.2",
"angular2-jwt": "0.2.0",

@ -9,25 +9,29 @@
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" [routerLink]="['/']">Ombi</a>
<div *ngIf="customizationSettings.applicationName; then aplicationNameBlock; else ombiBlock"></div>
<ng-template #aplicationNameBlock><a class="navbar-brand" [routerLink]="['/']">{{customizationSettings.applicationName}}</a></ng-template>
<ng-template #ombiBlock><a class="navbar-brand" [routerLink]="['/']">Ombi</a></ng-template>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
<li><a [routerLinkActive]="['active']" [routerLink]="['/search']"><i class="fa fa-search"></i> Search</a></li>
<li [routerLinkActive]="['active']"><a [routerLink]="['/search']"><i class="fa fa-search"></i> Search</a></li>
</ul>
<ul class="nav navbar-nav">
<li><a [routerLinkActive]="['active']" [routerLink]="['/requests']"><i class="fa fa-plus"></i> Requests</a></li>
<li [routerLinkActive]="['active']"><a [routerLink]="['/requests']"><i class="fa fa-plus"></i> Requests</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li><a [routerLinkActive]="['active']" [routerLink]="['/Settings/Ombi']"><i class="fa fa-cog"></i> Settings</a></li>
<li class="dropdown">
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Ombi']"><i class="fa fa-cog"></i> Settings</a></li>
<li [routerLinkActive]="['active']" class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"><i class="fa fa-user"></i> Welcome {{username}} <span class="caret"></span></a>
<ul class="dropdown-menu" role="menu">
<li><a [routerLink]="['/user/changepassword']"><i class="fa fa-key"></i> Change Password</a></li>
<li><a (click)="logOut()"><i class="fa fa-sign-out"></i> Logout</a></li>
<li [routerLinkActive]="['active']"><a [routerLink]="['/user/changepassword']"><i class="fa fa-key"></i> Change Password</a></li>
<li [routerLinkActive]="['active']"><a (click)="logOut()"><i class="fa fa-sign-out"></i> Logout</a></li>
</ul>
</li>
</ul>

@ -1,8 +1,11 @@
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { NotificationService } from './services/notification.service';
import { SettingsService } from './services/settings.service';
import { AuthService } from './auth/auth.service';
import { ICustomizationSettings } from './interfaces/ISettings';
@Component({
selector: 'ombi',
moduleId: module.id,
@ -10,10 +13,14 @@ import { AuthService } from './auth/auth.service';
})
export class AppComponent implements OnInit {
constructor(public notificationService: NotificationService, public authService: AuthService, private router: Router) {
constructor(public notificationService: NotificationService, public authService: AuthService, private router: Router, private settingsService : SettingsService) {
}
customizationSettings: ICustomizationSettings;
ngOnInit(): void {
this.settingsService.getCustomization().subscribe(x => this.customizationSettings = x);
this.router.events.subscribe(() => {
this.username = localStorage.getItem('currentUser');
this.showNav = this.authService.loggedIn();

@ -10,11 +10,14 @@ import { HttpModule } from '@angular/http';
import { InfiniteScrollModule } from 'ngx-infinite-scroll'
// Components
import { SearchComponent } from './search/search.component';
import { MovieSearchComponent } from './search/moviesearch.component';
import { TvSearchComponent } from './search/tvsearch.component';
import { RequestComponent } from './requests/request.component';
import { LoginComponent } from './login/login.component';
import { LandingPageComponent } from './landingpage/landingpage.component';
import { UserManagementComponent } from './usermanagement/usermanagement.component';
import { PageNotFoundComponent } from './errors/not-found.component';
// Services
@ -44,6 +47,7 @@ const routes: Routes = [
{ path: 'requests', component: RequestComponent, canActivate: [AuthGuard] },
{ path: 'login', component: LoginComponent },
{ path: 'landingpage', component: LandingPageComponent },
{ path: 'usermanagement', component: UserManagementComponent, canActivate: [AuthGuard] },
];
@NgModule({
@ -69,7 +73,9 @@ const routes: Routes = [
RequestComponent,
LoginComponent,
MovieSearchComponent,
LandingPageComponent
TvSearchComponent,
LandingPageComponent,
UserManagementComponent
],
providers: [
SearchService,

@ -0,0 +1,28 @@
export interface ISearchTvResult {
id: number,
seriesName: string,
aliases: string[],
banner: string,
seriesId: number,
status: string,
firstAired: string,
network: string,
networkId: string,
runtime: string,
genre: string[],
overview: string,
lastUpdated: number,
airsDayOfWeek: string,
airsTime: string,
rating: string,
imdbId: string,
siteRating: number,
trailer: string,
homepage:string,
episodes:IEpisodeModel[],
}
export interface IEpisodeModel {
seasonNumber: number,
episodeNumber:number,
}

@ -48,4 +48,9 @@ export interface ILandingPageSettings extends ISettings {
timeLimit: boolean,
startDateTime: Date,
endDateTime:Date,
}
export interface ICustomizationSettings extends ISettings {
applicationName: string,
logo:string
}

@ -0,0 +1,16 @@
export interface IUser {
id: string,
username: string,
alias: string,
claims: string[],
emailAddress: string,
password: string,
userType : UserType,
}
export enum UserType {
LocalUser = 1,
PlexUser = 2,
EmbyUser = 3
}

@ -3,3 +3,9 @@
background: #333333 !important;
border-radius: 2%
}
.landing-logo {
position: relative;
right: 20%;
width: 300px
}

@ -1,8 +1,12 @@
<div *ngIf="landingPageSettings">
<h1>Hey! Welcome back to {{websiteName}}</h1>
<div *ngIf="landingPageSettings && customizationSettings">
<h3 *ngIf="settings.noticeEnabled" style="background-color: {{settings.noticeBackgroundColor}}">{{settings.noticeText}}</h3>
<div *ngIf="customizationSettings.logo" class="landing-logo">
<img [src]="customizationSettings.logo" />
</div>
<h1>Hey! Welcome back to {{customizationSettings.applicationName}}</h1>
<h3 *ngIf="landingPageSettings.noticeEnabled" [innerHtml]="landingPageSettings.noticeText" style="background-color: {{landingPageSettings.noticeBackgroundColor}}"></h3>
<div class="col-md-3 landing-box">
<div>
Request Something

@ -1,6 +1,6 @@
import { Component, OnInit } from '@angular/core';
import { SettingsService } from '../services/settings.service';
import { ILandingPageSettings } from '../interfaces/ISettings';
import { ILandingPageSettings, ICustomizationSettings } from '../interfaces/ISettings';
@Component({
selector: 'ombi',
@ -12,16 +12,11 @@ export class LandingPageComponent implements OnInit {
constructor(private settingsService: SettingsService) { }
websiteName: string;
customizationSettings : ICustomizationSettings;
landingPageSettings: ILandingPageSettings;
ngOnInit(): void {
this.settingsService.getLandingPage().subscribe(x => {
this.landingPageSettings = x;
console.log(x);
});
this.websiteName = "Ombi";
this.settingsService.getCustomization().subscribe(x => this.customizationSettings = x);
this.settingsService.getLandingPage().subscribe(x => this.landingPageSettings = x);
}
}

@ -16,8 +16,7 @@ import { IRequestModel } from '../interfaces/IRequestModel';
@Component({
selector: 'ombi',
moduleId: module.id,
templateUrl: './request.component.html',
providers: [RequestService]
templateUrl: './request.component.html'
})
export class RequestComponent implements OnInit {
constructor(private requestService: RequestService) {

@ -7,17 +7,16 @@
<ul id="nav-tabs" class="nav nav-tabs" role="tablist">
<li role="presentation" class="active">
<a id="movieTabButton" href="#MoviesTab" aria-controls="home" role="tab" data-toggle="tab"><i class="fa fa-film"></i> Movies</a>
<a id="movieTabButton" href="#MoviesTab" aria-controls="home" role="tab" data-toggle="tab" (click)="selectTab('movies')"><i class="fa fa-film"></i> Movies</a>
</li>
<li role="presentation">
<!--<li role="presentation">
<a id="actorTabButton" href="#ActorsTab" aria-controls="profile" role="tab" data-toggle="tab"><i class="fa fa-users"></i> Actors</a>
</li>
</li>-->
<li role="presentation">
<a id="tvTabButton" href="#TvShowTab" aria-controls="profile" role="tab" data-toggle="tab"><i class="fa fa-television"></i> TV Shows</a>
<a id="tvTabButton" href="#TvShowTab" aria-controls="profile" role="tab" data-toggle="tab" (click)="selectTab('tv')"><i class="fa fa-television"></i> TV Shows</a>
</li>
<!--
@ -30,9 +29,11 @@
<!-- Tab panes -->
<div class="tab-content">
<movie-search></movie-search>
<div *ngIf="showMovie">
<movie-search></movie-search>
</div>
<!-- Actors tab -->
<!--
<div role="tabpanel" class="tab-pane" id="ActorsTab">
<div class="input-group">
<input id="actorSearchContent" type="text" class="form-control form-control-custom form-control-search form-control-withbuttons">
@ -46,35 +47,14 @@
<br />
<br />
<!-- Movie content -->
<div id="actorMovieList">
</div>
</div>
</div>-->
<div *ngIf="showTv">
<tv-search></tv-search>
<!-- TV tab -->
<div role="tabpanel" class="tab-pane" id="TvShowTab">
<div class="input-group">
<input id="tvSearchContent" type="text" class="form-control form-control-custom form-control-search form-control-withbuttons">
<div class="input-group-addon">
<div class="btn-group">
<a href="#" class="btn btn-sm btn-primary-outline dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
@UI.Search_Suggestions
<i class="fa fa-chevron-down"></i>
</a>
<ul class="dropdown-menu">
<li><a id="popularShows">Popular Shows</a></li>
<li><a id="trendingShows" href="#">Trending Shows</a></li>
<li><a id="mostWatchedShows" href="#">Most Watched Shows</a></li>
<li><a id="anticipatedShows" href="#">Most Anticipated Shows</a></li>
</ul>
</div><i id="tvSearchButton" class="fa fa-search"></i>
</div>
</div>
<br />
<br />
<!-- TV content -->
<div id="tvList">
</div>
</div>
</div>

@ -23,6 +23,10 @@ export class SearchComponent implements OnInit {
movieResults: ISearchMovieResult[];
result: IRequestEngineResult;
tabSelected: string;
showTv: boolean;
showMovie:boolean;
constructor(private searchService: SearchService, private requestService: RequestService, private notificationService : NotificationService) {
this.searchChanged
.debounceTime(600) // Wait Xms afterthe last event before emitting last event
@ -44,6 +48,8 @@ export class SearchComponent implements OnInit {
}
ngOnInit(): void {
this.selectTab("movies");
this.searchText = "";
this.movieResults = [];
this.result = {
@ -52,6 +58,8 @@ export class SearchComponent implements OnInit {
}
}
search(text: any) {
this.searchChanged.next(text.target.value);
}
@ -70,6 +78,19 @@ export class SearchComponent implements OnInit {
});
}
selectTab(tabName: string) {
console.log(tabName);
this.tabSelected = tabName;
if (tabName === 'movies') {
this.showMovie = true;
this.showTv = false;
} else {
this.showMovie = false;
this.showTv = true;
}
}
popularMovies() {
this.clearResults();
this.searchService.popularMovies().subscribe(x => {

@ -0,0 +1,161 @@
<!-- Movie tab -->
<div role="tabpanel" class="tab-pane" id="TvShowTab">
<div class="input-group">
<input id="tvSearchContent" type="text" class="form-control form-control-custom form-control-search form-control-withbuttons" (keyup)="search($event)">
<div class="input-group-addon">
<div class="btn-group">
<a href="#" class="btn btn-sm btn-primary-outline dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
Suggestions
<i class="fa fa-chevron-down"></i>
</a>
<ul class="dropdown-menu">
<li><a id="popularShows">Popular Shows</a></li>
<li><a id="trendingShows">Trending Shows</a></li>
<li><a id="mostWatchedShows">Most Watched Shows</a></li>
<li><a id="anticipatedShows">Most Anticipated Shows</a></li>
</ul>
</div><i id="tvSearchButton" class="fa fa-search"></i>
</div>
</div>
<br />
<br />
<!-- Movie content -->
<div id="actorMovieList">
</div>
<br />
<br />
<!-- TV content -->
<div id="tvList">
<div *ngFor="let result of tvResults">
<div class="row">
<div class="col-sm-2">
<img *ngIf="result.banner" class="img-responsive" width="150" [src]="result.banner" alt="poster">
</div>
<div class="col-sm-8">
<div>
<a href="http://www.imdb.com/title/{{result.imdbId}}/" target="_blank">
{{result.seriesName}} ({{result.firstAired}})
</a><span *ngIf="result.status" class="label label-primary" style="font-size:60%" target="_blank">{{result.status}}</span>
<span *ngIf="result.firstAired" class="label label-info" target="_blank">Air Date: {{result.firstAired}}</span>
<span *ngIf="result.releaseDate" class="label label-info" target="_blank">Release Date: {{result.releaseDate | date: 'dd/MM/yyyy'}}</span>
<span *ngIf="result.available" class="label label-success">Available</span>
<span *ngIf="result.approved && !result.available" class="label label-info">Processing Request</span>
<div *ngIf="result.requested && !result.available; then requested else notRequested"></div>
<template #requested>
<span *ngIf="!result.available" class="label label-warning">Pending Approval</span>
</template>
<template #notRequested>
<span *ngIf="!result.available" class="label label-danger">Not Yet Requested</span>
</template>
<span id="{{id}}netflixTab"></span>
<a *ngIf="result.homepage" href="{{result.homepage}}" target="_blank"><span class="label label-info">HomePage</span></a>
<a *ngIf="result.trailer" href="{{result.trailer}}" target="_blank"><span class="label label-info">Trailer</span></a>
<br/>
<br/>
</div>
<p style="font-size:0.9rem !important">{{result.overview}}</p>
</div>
<div class="col-sm-2">
<input name="{{type}}Id" type="text" value="{{result.id}}" hidden="hidden" />
<div *ngIf="result.available">
<button style="text-align: right" class="btn btn-success-outline disabled" disabled><i class="fa fa-check"></i> Available</button>
<div *ngIf="result.url">
<br />
<br />
<a style="text-align: right" class="btn btn-sm btn-primary-outline" href="{{result.url}}" target="_blank"><i class="fa fa-eye"></i> View In Plex</a>
</div>
</div>
<div *ngIf="result.requested; then requestedBtn else notRequestedBtn"></div>
<template #requestedBtn>
<button style="text-align: right" class="btn btn-primary-outline disabled" [disabled]><i class="fa fa-check"></i> Requested</button>
</template>
<template #notRequestedBtn>
<button id="{{result.id}}" style="text-align: right" class="btn btn-primary-outline" (click)="request(result)"><i class="fa fa-plus"></i> Request</button>
</template>
<!--{{#if_eq type "tv"}}
{{#if_eq tvFullyAvailable true}}
@*//TODO Not used yet*@
<button style="text-align: right" class="btn btn-success-outline disabled" disabled><i class="fa fa-check"></i> @UI.Search_Available</button><br />
{{else}}
{{#if_eq enableTvRequestsForOnlySeries true}}
<button id="{{id}}" style="text-align: right" class="btn {{#if available}}btn-success-outline{{else}}btn-primary-outline dropdownTv{{/if}} btn-primary-outline" season-select="0" type="button" {{#if available}} disabled{{/if}}><i class="fa fa-plus"></i> {{#if available}}@UI.Search_Available{{else}}@UI.Search_Request{{/if}}</button>
{{else}}
<div class="dropdown">
<button id="{{id}}" class="btn {{#if available}}btn-success-outline{{else}}btn-primary-outline{{/if}} dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<i class="fa fa-plus"></i> {{#if available}}@UI.Search_Available{{else}}@UI.Search_Request {{/if}}
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu1">
<li><a id="{{id}}" season-select="0" class="dropdownTv " href="#">@UI.Search_AllSeasons</a></li>
{{#if_eq disableTvRequestsBySeason false}}
<li><a id="{{id}}" season-select="1" class="dropdownTv" href="#">@UI.Search_FirstSeason</a></li>
<li><a id="{{id}}" season-select="2" class="dropdownTv" href="#">@UI.Search_LatestSeason</a></li>
<li><a id="SeasonSelect" data-identifier="{{id}}" data-toggle="modal" data-target="#seasonsModal" href="#">@UI.Search_SelectSeason...</a></li>
{{/if_eq}}
{{#if_eq disableTvRequestsByEpisode false}}
<li><a id="EpisodeSelect" data-identifier="{{id}}" data-toggle="modal" data-target="#episodesModal" href="#">@UI.Search_SelectEpisode...</a></li>
{{/if_eq}}
</ul>
</div>
{{/if_eq}}
{{#if available}}
{{#if url}}
<br />
<a style="text-align: right" class="btn btn-sm btn-primary-outline" href="{{url}}" target="_blank"><i class="fa fa-eye"></i> @UI.Search_ViewInPlex</a>
{{/if}}
{{/if}}
{{/if_eq}}
{{/if_eq}}-->
<br />
<div *ngIf="result.available">
<input name="providerId" type="text" value="{{id}}" hidden="hidden" />
<input name="type" type="text" value="{{type}}" hidden="hidden" />
<div class="dropdown">
<button class="btn btn-sm btn-danger-outline dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<i class="fa fa-exclamation"></i> @UI.Search_ReportIssue
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu1">
<li><a issue-select="0" class="dropdownIssue" href="#">WrongAudio</a></li>
<li><a issue-select="1" class="dropdownIssue" href="#">NoSubs</a></li>
<li><a issue-select="2" class="dropdownIssue" href="#">WrongContent</a></li>
<li><a issue-select="3" class="dropdownIssue" href="#">Playback</a></li>
<li><a issue-select="4" class="dropdownIssue" href="#" data-toggle="modal" data-target="#issuesModal">Other</a></li>
</ul>
</div>
</div>
</div>
</div>
<hr />
</div>
</div>
</div>

@ -0,0 +1,61 @@
import { Component, OnInit } from '@angular/core';
import { Subject } from 'rxjs/Subject';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
import 'rxjs/add/operator/map';
import { SearchService } from '../services/search.service';
//import { RequestService } from '../services/request.service';
//import { NotificationService } from '../services/notification.service';
import { ISearchTvResult } from '../interfaces/ISearchTvResult';
import { IRequestEngineResult } from '../interfaces/IRequestEngineResult';
@Component({
selector: 'tv-search',
moduleId: module.id,
templateUrl: './tvsearch.component.html',
})
export class TvSearchComponent implements OnInit {
searchText: string;
searchChanged: Subject<string> = new Subject<string>();
tvResults: ISearchTvResult[];
result: IRequestEngineResult;
constructor(private searchService: SearchService/*, private requestService: RequestService, private notificationService: NotificationService*/) {
this.searchChanged
.debounceTime(600) // Wait Xms afterthe last event before emitting last event
.distinctUntilChanged() // only emit if value is different from previous value
.subscribe(x => {
this.searchText = x as string;
if (this.searchText === "") {
this.clearResults();
return;
}
this.searchService.searchTv(this.searchText).subscribe(x => {
this.tvResults = x;
});
});
}
ngOnInit(): void {
this.searchText = "";
this.tvResults = [];
this.result = {
message: "",
requestAdded: false
}
}
search(text: any) {
this.searchChanged.next(text.target.value);
}
private clearResults() {
this.tvResults = [];
}
}

@ -4,6 +4,7 @@ import { Http } from '@angular/http';
import { Observable } from 'rxjs/Rx';
import { ServiceAuthHelpers } from './service.helpers';
import {IUser} from '../interfaces/IUser';
@Injectable()
@ -11,7 +12,11 @@ export class IdentityService extends ServiceAuthHelpers {
constructor(http: AuthHttp, private regularHttp : Http) {
super(http, '/api/v1/Identity/');
}
createUser(username:string,password:string): Observable<boolean> {
createWizardUser(username:string,password:string): Observable<boolean> {
return this.regularHttp.post(`${this.url}/Wizard/`, JSON.stringify({username:username, password:password}), { headers: this.headers }).map(this.extractData);
}
getUsers(): Observable<IUser[]> {
return this.http.get(`${this.url}/Users`).map(this.extractData);
}
}

@ -4,6 +4,7 @@ import { Observable } from 'rxjs/Rx';
import { ServiceAuthHelpers } from './service.helpers';
import { ISearchMovieResult } from '../interfaces/ISearchMovieResult';
import { ISearchTvResult } from '../interfaces/ISearchTvResult';
@Injectable()
export class SearchService extends ServiceAuthHelpers {
@ -30,4 +31,8 @@ export class SearchService extends ServiceAuthHelpers {
extraInfo(movies: ISearchMovieResult[]): Observable<ISearchMovieResult[]> {
return this.http.post(`${this.url}/Movie/extrainfo`, JSON.stringify(movies), { headers: this.headers }).map(this.extractData);
}
searchTv(searchTerm: string): Observable<ISearchTvResult[]> {
return this.http.get(`${this.url}/Tv/` + searchTerm).map(this.extractData);
}
}

@ -4,7 +4,7 @@ import { Http } from '@angular/http';
import { Observable } from 'rxjs/Rx';
import { ServiceAuthHelpers } from './service.helpers';
import { IOmbiSettings, IEmbySettings, IPlexSettings, ISonarrSettings,ILandingPageSettings } from '../interfaces/ISettings';
import { IOmbiSettings, IEmbySettings, IPlexSettings, ISonarrSettings,ILandingPageSettings, ICustomizationSettings } from '../interfaces/ISettings';
@Injectable()
export class SettingsService extends ServiceAuthHelpers {
@ -53,4 +53,15 @@ export class SettingsService extends ServiceAuthHelpers {
return this.httpAuth.post(`${this.url}/LandingPage`, JSON.stringify(settings), { headers: this.headers }).map(this.extractData);
}
// Using http since we need it not to be authenticated to get the customization settings
getCustomization(): Observable<ICustomizationSettings> {
return this.nonAuthHttp.get(`${this.url}/customization`).map(this.extractData);
}
saveCustomization(settings: ICustomizationSettings): Observable<boolean> {
return this.httpAuth.post(`${this.url}/customization`, JSON.stringify(settings), { headers: this.headers }).map(this.extractData);
}
}

@ -0,0 +1,34 @@
<settings-menu></settings-menu>
<fieldset *ngIf="settings">
<legend>Customization</legend>
<div class="form-group">
<label for="applicationName" class="control-label">Application Name</label>
<div>
<input type="text" [(ngModel)]="settings.applicationName" class="form-control form-control-custom " id="applicationName" name="applicationName" placeholder="Ombi" value="{{settings.applicationName}}">
</div>
</div>
<div class="form-group">
<label for="logo" class="control-label">Custom Logo</label>
<div>
<input type="text" [(ngModel)]="settings.logo" class="form-control form-control-custom " id="logo" name="logo" value="{{settings.logo}}">
</div>
</div>
<small>This will be used on all of the notifications e.g. Newsletter, email notification and also the Landing page</small>
<div class="form-group">
<label for="logo" class="control-label">Logo Preview:</label>
<div>
<img *ngIf="settings.logo" [src]="settings.logo" style="width: 300px"/>
</div>
</div>
<div class="form-group">
<div>
<button (click)="save()" type="submit" id="save" class="btn btn-primary-outline">Submit</button>
</div>
</div>
</fieldset>

@ -0,0 +1,32 @@
import { Component, OnInit } from '@angular/core';
import { ICustomizationSettings } from '../../interfaces/ISettings'
import { SettingsService } from '../../services/settings.service';
import { NotificationService } from "../../services/notification.service";
@Component({
selector: 'ombi',
moduleId: module.id,
templateUrl: './customization.component.html',
})
export class CustomizationComponent implements OnInit {
constructor(private settingsService: SettingsService, private notificationService: NotificationService) { }
settings: ICustomizationSettings;
ngOnInit(): void {
this.settingsService.getCustomization().subscribe(x => this.settings = x);
}
save() {
this.settingsService.saveCustomization(this.settings).subscribe(x => {
if (x) {
this.notificationService.success("Settings Saved", "Successfully saved Ombi settings");
} else {
this.notificationService.success("Settings Saved", "There was an error when saving the Ombi settings");
}
});
}
}

@ -1,4 +1,6 @@
<div class="col-sm-8 col-sm-push-1">

<settings-menu></settings-menu>
<div *ngIf="settings">
<fieldset>
<legend>Emby Configuration</legend>

@ -1,4 +1,6 @@
<div class="col-sm-8 col-sm-push-1" *ngIf="settings">

<settings-menu></settings-menu>
<div *ngIf="settings">
<fieldset>
<legend>Landing Page Configuration</legend>

@ -1,5 +1,6 @@
<div class="col-sm-8 col-sm-push-1">
<fieldset>

<settings-menu></settings-menu>
<fieldset *ngIf="settings">
<legend>Ombi Configuration</legend>
<div class="form-group">
@ -47,5 +48,4 @@
<button (click)="save()" type="submit" id="save" class="btn btn-primary-outline">Submit</button>
</div>
</div>
</fieldset>
</div>
</fieldset>

@ -1,4 +1,6 @@
<div class="col-sm-8 col-sm-push-1">

<settings-menu></settings-menu>
<div *ngIf="settings">
<fieldset>
<legend>Plex Configuration</legend>

@ -12,6 +12,7 @@ import { OmbiComponent } from './ombi/ombi.component'
import { PlexComponent } from './plex/plex.component'
import { EmbyComponent } from './emby/emby.component'
import { LandingPageComponent } from './landingpage/landingpage.component'
import { CustomizationComponent } from './customization/customization.component'
import { SettingsMenuComponent } from './settingsmenu.component';
@ -22,6 +23,7 @@ const routes: Routes = [
{ path: 'Settings/Plex', component: PlexComponent, canActivate: [AuthGuard] },
{ path: 'Settings/Emby', component: EmbyComponent, canActivate: [AuthGuard] },
{ path: 'Settings/LandingPage', component: LandingPageComponent, canActivate: [AuthGuard] },
{ path: 'Settings/Customization', component: CustomizationComponent, canActivate: [AuthGuard] },
];
@NgModule({
@ -40,6 +42,7 @@ const routes: Routes = [
PlexComponent,
EmbyComponent,
LandingPageComponent,
CustomizationComponent,
],
exports: [
RouterModule

@ -1 +1,64 @@
<p-menu [model]="menu"></p-menu>
<ul class="nav nav-tabs">
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Ombi']">Ombi</a></li>
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Customization']">Customization</a></li>
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/LandingPage']">Landing Page</a></li>
<li class="dropdown" [routerLinkActive]="['active']">
<a class="dropdown-toggle" data-toggle="dropdown">
Media Server <span class="caret"></span>
</a>
<ul class="dropdown-menu">
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Plex']">Plex</a></li>
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Emby']">Emby</a></li>
</ul>
</li>
<li class="dropdown" [routerLinkActive]="['active']">
<a class="dropdown-toggle" data-toggle="dropdown">
TV <span class="caret"></span>
</a>
<ul class="dropdown-menu">
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Sonarr']" >Sonarr</a></li>
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/SickRage']" >SickRage</a></li>
</ul>
</li>
<li class="dropdown" [routerLinkActive]="['active']">
<a class="dropdown-toggle" data-toggle="dropdown">
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/Radarr']">Radarr</a></li>
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Watcher']">Watcher</a></li>
</ul>
</li>
<li class="dropdown" [routerLinkActive]="['active']">
<a class="dropdown-toggle" data-toggle="dropdown">
Notifications <span class="caret"></span>
</a>
<ul class="dropdown-menu">
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Email']">Email</a></li>
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Newsletter']">Newsletter</a></li>
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Pushbullet']">Pushbullet</a></li>
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Pushover']">Pushover</a></li>
</ul>
</li>
<li class="dropdown" [routerLinkActive]="['active']">
<a class="dropdown-toggle" data-toggle="dropdown">
System <span class="caret"></span>
</a>
<ul class="dropdown-menu">
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Update']">Update</a></li>
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Logs']">Logs</a></li>
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/ScheduledJobs']">Scheduled Jobs</a></li>
</ul>
</li>
</ul>
<hr/>

@ -1,27 +1,9 @@
import { Component, OnInit } from '@angular/core';
import { MenuItem } from 'primeng/primeng';
import { Component } from '@angular/core';
@Component({
selector: 'settings-menu',
moduleId: module.id,
templateUrl: './settingsmenu.component.html'
})
export class SettingsMenuComponent implements OnInit {
private menu: MenuItem[];
ngOnInit() {
this.menu = [{
label: 'File',
items: [
{ label: 'Ombi', icon: 'fa-plus', routerLink:"/Settings/Ombi" },
{ label: 'Open', icon: 'fa-download' }
]
},
{
label: 'Edit',
items: [
{ label: 'Undo', icon: 'fa-refresh' },
{ label: 'Redo', icon: 'fa-repeat' }
]
}];
}
export class SettingsMenuComponent {
}

@ -1,4 +1,6 @@
<div class="col-sm-8 col-sm-push-1">

<settings-menu></settings-menu>
<div *ngIf="settings">
<form class="form-horizontal" method="POST" id="mainForm">
<fieldset>
<legend>Sonarr Settings</legend>

@ -0,0 +1,74 @@
<h1>User Management</h1>
<!--Search-->
<div class="row">
<div class="form-group">
<div class="input-group">
<div class="input-group-addon">
<i class="fa fa-search"></i>
</div>
<input type="text" class="form-control" placeholder="Search" [(ngModel)]="searchTerm">
</div>
</div>
</div>
<!-- Table -->
<table class="table table-striped table-hover table-responsive table-condensed">
<thead>
<tr>
<th>
<a (click)="changeSort('username')">
Username
<!--<span ng-show="sortType == 'username' && !sortReverse" class="fa fa-caret-down"></span>
<span ng-show="sortType == 'username' && sortReverse" class="fa fa-caret-up"></span>-->
</a>
</th>
<th>
<a>
Alias
</a>
</th>
<th>
<a>
Email
</a>
</th>
<th>
Roles
</th>
<th>
<a>
User Type
</a>
</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let u of users">
<td>
{{u.username}}
</td>
<td>
{{u.alias}}
</td>
<td>
{{u.emailAddress}}
</td>
<td>
<span *ngFor="let claim of u.claims">{{claim}}</span>
</td>
<td ng-hide="hideColumns">
<span ng-if="u.userType === 1">Local User</span>
<span ng-if="u.userType === 2">Plex User</span>
<span ng-if="u.userType === 3">Emby User</span>
</td>
<td>
<a (click)="edit(u)" class="btn btn-sm btn-info-outline">Details/Edit</a>
</td>
</tr>
</tbody>
</table>

@ -0,0 +1,38 @@
import { Component, OnInit } from '@angular/core';
import { IUser } from '../interfaces/IUser';
import { IdentityService } from '../services/identity.service';
@Component({
selector: 'ombi',
moduleId: module.id,
templateUrl: './usermanagement.component.html'
})
export class UserManagementComponent implements OnInit {
constructor(private identityService: IdentityService) { }
ngOnInit(): void {
this.users = [];
this.identityService.getUsers().subscribe(x => {
this.users = x;
});
}
users: IUser[];
selectedUser: IUser;
edit(user: IUser) {
this.selectedUser = user;
}
changeSort(username: string) {
//??????
}
//private removeRequestFromUi(key : IRequestModel) {
// var index = this.requests.indexOf(key, 0);
// if (index > -1) {
// this.requests.splice(index, 1);
// }
//}
}

@ -21,7 +21,7 @@ export class CreateAdminComponent {
password: string;
createUser() {
this.identityService.createUser(this.username, this.password).subscribe(x => {
this.identityService.createWizardUser(this.username, this.password).subscribe(x => {
if (x) {
// Log me in.
this.auth.login({ username: this.username, password: this.password }).subscribe(c => {

@ -24,6 +24,7 @@ after_build:
- dotnet publish -c Release -r osx.10.12-x64
- dotnet publish -c Release -r ubuntu.16.10-x64
- dotnet publish -c Release -r debian.8-x64
- dotnet publish -c Release -r centos.7-x64
- cmd: >-
7z a Ombi_windows.zip %APPVEYOR_BUILD_FOLDER%\Ombi\Ombi\bin\Release\netcoreapp1.1\win10-x64\publish
@ -35,6 +36,9 @@ after_build:
7z a Ombi_debian.zip %APPVEYOR_BUILD_FOLDER%\Ombi\Ombi\bin\Release\netcoreapp1.1\debian.8-x64\publish
7z a Ombi_centos.zip %APPVEYOR_BUILD_FOLDER%\Ombi\Ombi\bin\Release\netcoreapp1.1\centos.7-x64\publish
appveyor PushArtifact Ombi_windows.zip
@ -49,6 +53,9 @@ after_build:
appveyor PushArtifact Ombi_debian.zip
appveyor PushArtifact Ombi_centos.zip
cache:
- '%USERPROFILE%\.nuget\packages'

Loading…
Cancel
Save