Add the Issue Reporting functionality (#1811)

* Added issuesreporting and the ability to add categories to the UI
* Added lazy loading!
pull/1817/head
Jamie 7 years ago committed by GitHub
parent 438f56eceb
commit 246f1c07cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -95,6 +95,10 @@ Task("SetVersionInfo")
{
fullVer = buildVersion + "-PR";
}
if(fullVer.Contains("_"))
{
fullVer = fullVer.Replace("_","");
}
buildSettings.ArgumentCustomization = args => args.Append("/p:SemVer=" + versionInfo.AssemblySemVer);
buildSettings.ArgumentCustomization = args => args.Append("/p:FullVer=" + fullVer);
@ -154,7 +158,6 @@ Task("Package")
});
Task("Publish")
.IsDependentOn("Run-Unit-Tests")
.IsDependentOn("PrePublish")
.IsDependentOn("Publish-Windows")
.IsDependentOn("Publish-OSX").IsDependentOn("Publish-Linux")
@ -204,12 +207,6 @@ Task("Publish-Linux")
DotNetCorePublish("./src/Ombi.Updater/Ombi.Updater.csproj", publishSettings);
});
Task("Run-Unit-Tests")
.Does(() =>
{
DotNetCoreBuild(csProj, buildSettings);
});
//////////////////////////////////////////////////////////////////////
// TASK TARGETS
//////////////////////////////////////////////////////////////////////

@ -16,7 +16,7 @@ namespace Ombi.Api
{
public class Api : IApi
{
public Api(ILogger<Api> log, ISettingsService<OmbiSettings> s, IMemoryCache cache)
public Api(ILogger<Api> log, ISettingsService<OmbiSettings> s, ICacheService cache)
{
Logger = log;
_settings = s;
@ -25,15 +25,11 @@ namespace Ombi.Api
private ILogger<Api> Logger { get; }
private readonly ISettingsService<OmbiSettings> _settings;
private readonly IMemoryCache _cache;
private readonly ICacheService _cache;
private async Task<HttpMessageHandler> GetHandler()
{
var settings = await _cache.GetOrCreateAsync(CacheKeys.OmbiSettings, async entry =>
{
entry.AbsoluteExpiration = DateTimeOffset.Now.AddHours(1);
return await _settings.GetSettingsAsync();
});
var settings = await _cache.GetOrAdd(CacheKeys.OmbiSettings, async () => await _settings.GetSettingsAsync(), DateTime.Now.AddHours(1));
if (settings.IgnoreCertificateErrors)
{
return new HttpClientHandler

@ -19,7 +19,7 @@ namespace Ombi.Core.Engine
public class MovieSearchEngine : BaseMediaEngine, IMovieEngine
{
public MovieSearchEngine(IPrincipal identity, IRequestServiceMain service, IMovieDbApi movApi, IMapper mapper,
ILogger<MovieSearchEngine> logger, IRuleEvaluator r, OmbiUserManager um, IMemoryCache mem)
ILogger<MovieSearchEngine> logger, IRuleEvaluator r, OmbiUserManager um, ICacheService mem)
: base(identity, service, r, um)
{
MovieApi = movApi;
@ -31,7 +31,7 @@ namespace Ombi.Core.Engine
private IMovieDbApi MovieApi { get; }
private IMapper Mapper { get; }
private ILogger<MovieSearchEngine> Logger { get; }
private IMemoryCache MemCache { get; }
private ICacheService MemCache { get; }
/// <summary>
/// Lookups the imdb information.
@ -69,11 +69,7 @@ namespace Ombi.Core.Engine
/// <returns></returns>
public async Task<IEnumerable<SearchMovieViewModel>> PopularMovies()
{
var result = await MemCache.GetOrCreateAsync(CacheKeys.PopularMovies, async entry =>
{
entry.AbsoluteExpiration = DateTimeOffset.Now.AddHours(12);
return await MovieApi.PopularMovies();
});
var result = await MemCache.GetOrAdd(CacheKeys.PopularMovies, async () => await MovieApi.PopularMovies(), DateTime.Now.AddHours(12));
if (result != null)
{
Logger.LogDebug("Search Result: {result}", result);
@ -88,11 +84,7 @@ namespace Ombi.Core.Engine
/// <returns></returns>
public async Task<IEnumerable<SearchMovieViewModel>> TopRatedMovies()
{
var result = await MemCache.GetOrCreateAsync(CacheKeys.TopRatedMovies, async entry =>
{
entry.AbsoluteExpiration = DateTimeOffset.Now.AddHours(12);
return await MovieApi.TopRated();
});
var result = await MemCache.GetOrAdd(CacheKeys.TopRatedMovies, async () => await MovieApi.TopRated(), DateTime.Now.AddHours(12));
if (result != null)
{
Logger.LogDebug("Search Result: {result}", result);
@ -107,11 +99,7 @@ namespace Ombi.Core.Engine
/// <returns></returns>
public async Task<IEnumerable<SearchMovieViewModel>> UpcomingMovies()
{
var result = await MemCache.GetOrCreateAsync(CacheKeys.UpcomingMovies, async entry =>
{
entry.AbsoluteExpiration = DateTimeOffset.Now.AddHours(12);
return await MovieApi.Upcoming();
});
var result = await MemCache.GetOrAdd(CacheKeys.UpcomingMovies, async () => await MovieApi.Upcoming(), DateTime.Now.AddHours(12));
if (result != null)
{
Logger.LogDebug("Search Result: {result}", result);
@ -126,11 +114,7 @@ namespace Ombi.Core.Engine
/// <returns></returns>
public async Task<IEnumerable<SearchMovieViewModel>> NowPlayingMovies()
{
var result = await MemCache.GetOrCreateAsync(CacheKeys.NowPlayingMovies, async entry =>
{
entry.AbsoluteExpiration = DateTimeOffset.Now.AddHours(12);
return await MovieApi.NowPlaying();
});
var result = await MemCache.GetOrAdd(CacheKeys.NowPlayingMovies, async () => await MovieApi.NowPlaying(), DateTime.Now.AddHours(12));
if (result != null)
{
Logger.LogDebug("Search Result: {result}", result);

@ -26,7 +26,7 @@ namespace Ombi.Core.Engine
{
public TvSearchEngine(IPrincipal identity, IRequestServiceMain service, ITvMazeApi tvMaze, IMapper mapper, ISettingsService<PlexSettings> plexSettings,
ISettingsService<EmbySettings> embySettings, IPlexContentRepository repo, IEmbyContentRepository embyRepo, ITraktApi trakt, IRuleEvaluator r, OmbiUserManager um,
IMemoryCache memCache)
ICacheService memCache)
: base(identity, service, r, um)
{
TvMazeApi = tvMaze;
@ -46,7 +46,7 @@ namespace Ombi.Core.Engine
private IPlexContentRepository PlexContentRepo { get; }
private IEmbyContentRepository EmbyContentRepo { get; }
private ITraktApi TraktApi { get; }
private IMemoryCache MemCache { get; }
private ICacheService MemCache { get; }
public async Task<IEnumerable<SearchTvShowViewModel>> Search(string searchTerm)
{
@ -124,44 +124,28 @@ namespace Ombi.Core.Engine
public async Task<IEnumerable<TreeNode<SearchTvShowViewModel>>> Popular()
{
var result = await MemCache.GetOrCreateAsync(CacheKeys.PopularTv, async entry =>
{
entry.AbsoluteExpiration = DateTimeOffset.Now.AddHours(12);
return await TraktApi.GetPopularShows();
});
var result = await MemCache.GetOrAdd(CacheKeys.PopularTv, async () => await TraktApi.GetPopularShows(), DateTime.Now.AddHours(12));
var processed = await ProcessResults(result);
return processed.Select(ParseIntoTreeNode).ToList();
}
public async Task<IEnumerable<TreeNode<SearchTvShowViewModel>>> Anticipated()
{
var result = await MemCache.GetOrCreateAsync(CacheKeys.AnticipatedTv, async entry =>
{
entry.AbsoluteExpiration = DateTimeOffset.Now.AddHours(12);
return await TraktApi.GetAnticipatedShows();
});
var result = await MemCache.GetOrAdd(CacheKeys.AnticipatedTv, async () => await TraktApi.GetAnticipatedShows(), DateTime.Now.AddHours(12));
var processed= await ProcessResults(result);
return processed.Select(ParseIntoTreeNode).ToList();
}
public async Task<IEnumerable<TreeNode<SearchTvShowViewModel>>> MostWatches()
{
var result = await MemCache.GetOrCreateAsync(CacheKeys.MostWatchesTv, async entry =>
{
entry.AbsoluteExpiration = DateTimeOffset.Now.AddHours(12);
return await TraktApi.GetMostWatchesShows();
});
var result = await MemCache.GetOrAdd(CacheKeys.MostWatchesTv, async () => await TraktApi.GetMostWatchesShows(), DateTime.Now.AddHours(12));
var processed = await ProcessResults(result);
return processed.Select(ParseIntoTreeNode).ToList();
}
public async Task<IEnumerable<TreeNode<SearchTvShowViewModel>>> Trending()
{
var result = await MemCache.GetOrCreateAsync(CacheKeys.TrendingTv, async entry =>
{
entry.AbsoluteExpiration = DateTimeOffset.Now.AddHours(12);
return await TraktApi.GetTrendingShows();
});
var result = await MemCache.GetOrAdd(CacheKeys.TrendingTv, async () => await TraktApi.GetTrendingShows(), DateTime.Now.AddHours(12));
var processed = await ProcessResults(result);
return processed.Select(ParseIntoTreeNode).ToList();
}

@ -51,6 +51,7 @@ namespace Ombi.Core
RequestType = model.RequestType,
Recipient = model.RequestedUser?.Email ?? string.Empty
};
BackgroundJob.Enqueue(() => NotificationService.Publish(notificationModel));
}
public void Notify(ChildRequests model, NotificationType type)

@ -52,7 +52,8 @@ namespace Ombi.Core.Helpers
RequestedDate = DateTime.UtcNow,
Approved = false,
RequestedUserId = userId,
SeasonRequests = new List<SeasonRequests>()
SeasonRequests = new List<SeasonRequests>(),
Title = model.Title
};
return this;

@ -1,12 +0,0 @@
namespace Ombi.Core.Models.Requests
{
public enum IssueState
{
None = 99,
WrongAudio = 0,
NoSubtitles = 1,
WrongContent = 2,
PlaybackIssues = 3,
Other = 4 // Provide a message
}
}

@ -83,7 +83,10 @@ namespace Ombi.Core.Senders
Success = true
};
}
return new SenderResult();
return new SenderResult
{
Message = "Could not send to SickRage!"
};
}
return new SenderResult
{

@ -128,9 +128,10 @@ namespace Ombi.DependencyInjection
public static void RegisterServices(this IServiceCollection services)
{
services.AddTransient<IRequestServiceMain, RequestService>();
services.AddSingleton<INotificationService, NotificationService>();
services.AddSingleton<IEmailProvider, GenericEmailProvider>();
services.AddTransient<INotificationService, NotificationService>();
services.AddTransient<IEmailProvider, GenericEmailProvider>();
services.AddTransient<INotificationHelper, NotificationHelper>();
services.AddTransient<ICacheService, CacheService>();
services.AddTransient<IDiscordNotification, DiscordNotification>();
services.AddTransient<IEmailNotification, EmailNotification>();
@ -140,7 +141,6 @@ namespace Ombi.DependencyInjection
services.AddTransient<IMattermostNotification, MattermostNotification>();
services.AddTransient<IPushoverNotification, PushoverNotification>();
services.AddTransient<ITelegramNotification, TelegramNotification>();
}
public static void RegisterJobs(this IServiceCollection services)

@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Nito.AsyncEx;
namespace Ombi.Helpers
{
public class CacheService : ICacheService
{
private readonly IMemoryCache _memoryCache;
private readonly AsyncLock _mutex = new AsyncLock();
public CacheService(IMemoryCache memoryCache)
{
_memoryCache = memoryCache ?? throw new ArgumentNullException(nameof(memoryCache));
}
public async Task<T> GetOrAdd<T>(string cacheKey, Func<Task<T>> factory, DateTime absoluteExpiration = default(DateTime))
{
if (absoluteExpiration == default(DateTime))
{
absoluteExpiration = DateTime.Now.AddHours(1);
}
// locks get and set internally
if (_memoryCache.TryGetValue<T>(cacheKey, out var result))
{
return result;
}
using (await _mutex.LockAsync())
{
if (_memoryCache.TryGetValue(cacheKey, out result))
{
return result;
}
result = await factory();
_memoryCache.Set(cacheKey, result, absoluteExpiration);
return result;
}
}
public void Remove(string key)
{
_memoryCache.Remove(key);
}
public T GetOrAdd<T>(string cacheKey, Func<T> factory, DateTime absoluteExpiration)
{
// locks get and set internally
if (_memoryCache.TryGetValue<T>(cacheKey, out var result))
{
return result;
}
lock (TypeLock<T>.Lock)
{
if (_memoryCache.TryGetValue(cacheKey, out result))
{
return result;
}
result = factory();
_memoryCache.Set(cacheKey, result, absoluteExpiration);
return result;
}
}
private static class TypeLock<T>
{
public static object Lock { get; } = new object();
}
}
}

@ -0,0 +1,12 @@
using System;
using System.Threading.Tasks;
namespace Ombi.Helpers
{
public interface ICacheService
{
Task<T> GetOrAdd<T>(string cacheKey, Func<Task<T>> factory, DateTime absoluteExpiration = default(DateTime));
T GetOrAdd<T>(string cacheKey, Func<T> factory, DateTime absoluteExpiration);
void Remove(string key);
}
}

@ -1,54 +0,0 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2017 Jamie Rees
// File: MemoryCacheHelper.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System;
using Microsoft.Extensions.Caching.Memory;
namespace Ombi.Helpers
{
public static class MemoryCacheHelper
{
public static IMemoryCache TryAdd(this IMemoryCache cache, object cacheObject, TimeSpan slidingExpiration)
{
object cachedObject;
if (!cache.TryGetValue(CacheKeys.Update, out cachedObject))
{
// Key not in cache, so get data.
// Set cache options.
var cacheEntryOptions = new MemoryCacheEntryOptions()
.SetSlidingExpiration(slidingExpiration);
// Save data in cache.
cache.Set(CacheKeys.Update, cacheObject, cacheEntryOptions);
}
return cache;
}
}
}

@ -13,6 +13,7 @@
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="10.0.3" />
<PackageReference Include="Nito.AsyncEx" Version="5.0.0-pre-05" />
<PackageReference Include="System.Security.Claims" Version="4.3.0" />
</ItemGroup>

@ -90,6 +90,9 @@ namespace Ombi.Notifications.Agents
return;
}
// Issues should be sent to admin
message.To = settings.AdminEmail;
await Send(message, settings);
}

@ -60,6 +60,7 @@ namespace Ombi.Notifications.Interfaces
// Is this a test?
// The request id for tests is -1
// Also issues are 0 since there might not be a request associated
if (model.RequestId > 0)
{
await LoadRequest(model.RequestId, model.RequestType);
@ -157,11 +158,11 @@ namespace Ombi.Notifications.Interfaces
var curlys = new NotificationMessageCurlys();
if (model.RequestType == RequestType.Movie)
{
curlys.Setup(MovieRequest, Customization);
curlys.Setup(model, MovieRequest, Customization);
}
else
{
curlys.Setup(TvRequest, Customization);
curlys.Setup(model, TvRequest, Customization);
}
var parsed = resolver.ParseMessage(template, curlys);

@ -12,5 +12,7 @@ namespace Ombi.Notifications.Models
public NotificationType NotificationType { get; set; }
public RequestType RequestType { get; set; }
public string Recipient { get; set; }
public string AdditionalInformation { get; set; }
}
}

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using Ombi.Notifications.Models;
using Ombi.Settings.Settings.Models;
using Ombi.Store.Entities;
using Ombi.Store.Entities.Requests;
@ -9,7 +10,7 @@ namespace Ombi.Notifications
public class NotificationMessageCurlys
{
public void Setup(FullBaseRequest req, CustomizationSettings s)
public void Setup(NotificationOptions opts, FullBaseRequest req, CustomizationSettings s)
{
ApplicationUrl = s.ApplicationUrl;
ApplicationName = string.IsNullOrEmpty(s.ApplicationName) ? "Ombi" : s.ApplicationName;
@ -23,9 +24,10 @@ namespace Ombi.Notifications
Year = req.ReleaseDate.Year.ToString();
PosterImage = req.RequestType == RequestType.Movie ?
$"https://image.tmdb.org/t/p/w300{req.PosterPath}" : req.PosterPath;
AdditionalInformation = opts.AdditionalInformation;
}
public void Setup(ChildRequests req, CustomizationSettings s)
public void Setup(NotificationOptions opts, ChildRequests req, CustomizationSettings s)
{
ApplicationUrl = s.ApplicationUrl;
ApplicationName = string.IsNullOrEmpty(s.ApplicationName) ? "Ombi" : s.ApplicationName;
@ -39,6 +41,7 @@ namespace Ombi.Notifications
Year = req.ParentRequest.ReleaseDate.Year.ToString();
PosterImage = req.RequestType == RequestType.Movie ?
$"https://image.tmdb.org/t/p/w300{req.ParentRequest.PosterPath}" : req.ParentRequest.PosterPath;
AdditionalInformation = opts.AdditionalInformation;
// DO Episode and Season Lists
}
@ -54,7 +57,7 @@ namespace Ombi.Notifications
public string Title { get; set; }
public string RequestedDate { get; set; }
public string Type { get; set; }
public string Issue { get; set; }
public string AdditionalInformation { get; set; }
public string Overview { get; set; }
public string Year { get; set; }
public string EpisodesList { get; set; }
@ -75,7 +78,7 @@ namespace Ombi.Notifications
{nameof(Title), Title },
{nameof(RequestedDate), RequestedDate },
{nameof(Type), Type },
{nameof(Issue), Issue },
{nameof(AdditionalInformation), AdditionalInformation },
{nameof(LongDate),LongDate},
{nameof(ShortDate),ShortDate},
{nameof(LongTime),LongTime},

@ -67,7 +67,7 @@ namespace Ombi.Notifications
/// <param name="model">The model.</param>
/// <param name="settings">The settings.</param>
/// <returns></returns>
public async Task Publish(NotificationOptions model, Ombi.Settings.Settings.Models.Settings settings)
public async Task Publish(NotificationOptions model, Settings.Settings.Models.Settings settings)
{
var notificationTasks = NotificationAgents.Select(notification => NotifyAsync(notification, model, settings));

@ -37,7 +37,7 @@ namespace Ombi.Schedule.Jobs.Radarr
await SemaphoreSlim.WaitAsync();
try
{
var settings = RadarrSettings.GetSettings();
var settings = await RadarrSettings.GetSettingsAsync();
if (settings.Enabled)
{
try

@ -8,7 +8,6 @@ using Microsoft.Extensions.Logging;
using Ombi.Api.Sonarr;
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.Context;
@ -60,6 +59,8 @@ namespace Ombi.Schedule.Jobs.Sonarr
_log.LogDebug("Syncing series: {0}", s.title);
var episodes = await _api.GetEpisodes(s.id, settings.ApiKey, settings.FullUri);
var monitoredEpisodes = episodes.Where(x => x.monitored || x.hasFile);
// Add to DB
_log.LogDebug("We have the episodes, adding to db transaction");
await _ctx.SonarrEpisodeCache.AddRangeAsync(monitoredEpisodes.Select(episode => new SonarrEpisodeCache
{
@ -82,58 +83,5 @@ namespace Ombi.Schedule.Jobs.Sonarr
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;
//}
}
}

@ -0,0 +1,8 @@
namespace Ombi.Settings.Settings.Models
{
public class IssueSettings : Settings
{
public bool Enabled { get; set; }
public bool EnableInProgress { get; set; }
}
}

@ -5,7 +5,6 @@ using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
using Microsoft.Extensions.Caching.Memory;
namespace Ombi.Settings.Settings
{
@ -13,7 +12,7 @@ namespace Ombi.Settings.Settings
where T : Models.Settings, new()
{
public SettingsService(ISettingsRepository repo, IMemoryCache cache)
public SettingsService(ISettingsRepository repo, ICacheService cache)
{
Repo = repo;
EntityName = typeof(T).Name;
@ -23,13 +22,12 @@ namespace Ombi.Settings.Settings
private ISettingsRepository Repo { get; }
private string EntityName { get; }
private string CacheName => $"Settings{EntityName}";
private readonly IMemoryCache _cache;
private readonly ICacheService _cache;
public T GetSettings()
{
return _cache.GetOrCreate(CacheName, entry =>
return _cache.GetOrAdd(CacheName, () =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(2);
var result = Repo.Get(EntityName);
if (result == null)
{
@ -43,14 +41,13 @@ namespace Ombi.Settings.Settings
var model = obj;
return model;
});
}, DateTime.Now.AddHours(2));
}
public async Task<T> GetSettingsAsync()
{
return await _cache.GetOrCreateAsync(CacheName, async entry =>
return await _cache.GetOrAdd(CacheName, async () =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(2);
var result = await Repo.GetAsync(EntityName);
if (result == null)
{
@ -64,7 +61,7 @@ namespace Ombi.Settings.Settings
var model = obj;
return model;
});
}, DateTime.Now.AddHours(2));
}
public bool SaveSettings(T model)

@ -30,8 +30,8 @@ namespace Ombi.Store.Context
DbSet<MovieRequests> MovieRequests { get; set; }
DbSet<TvRequests> TvRequests { get; set; }
DbSet<ChildRequests> ChildRequests { get; set; }
DbSet<MovieIssues> MovieIssues { get; set; }
DbSet<TvIssues> TvIssues { get; set; }
DbSet<Issues> Issues { get; set; }
DbSet<IssueCategory> IssueCategories { get; set; }
DbSet<Tokens> Tokens { get; set; }
DbSet<SonarrCache> SonarrCache { get; set; }
DbSet<SonarrEpisodeCache> SonarrEpisodeCache { get; set; }

@ -33,10 +33,13 @@ namespace Ombi.Store.Context
public DbSet<MovieRequests> MovieRequests { get; set; }
public DbSet<TvRequests> TvRequests { get; set; }
public DbSet<ChildRequests> ChildRequests { get; set; }
public DbSet<MovieIssues> MovieIssues { get; set; }
public DbSet<TvIssues> TvIssues { get; set; }
public DbSet<Issues> Issues { get; set; }
public DbSet<IssueCategory> IssueCategories { get; set; }
public DbSet<IssueComments> IssueComments { get; set; }
public DbSet<RequestLog> RequestLogs { get; set; }
public DbSet<Audit> Audit { get; set; }
public DbSet<Tokens> Tokens { get; set; }
public DbSet<SonarrCache> SonarrCache { get; set; }

@ -14,7 +14,7 @@ namespace Ombi.Store.Entities.Requests
[ForeignKey(nameof(IssueId))]
public List<TvIssues> Issues { get; set; }
public List<Issues> Issues { get; set; }
public List<SeasonRequests> SeasonRequests { get; set; }
}

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

@ -0,0 +1,18 @@
using System;
using System.ComponentModel.DataAnnotations.Schema;
namespace Ombi.Store.Entities.Requests
{
public class IssueComments : Entity
{
public string UserId { get; set; }
public string Comment { get; set; }
public int? IssuesId { get; set; }
public DateTime Date { get; set; }
[ForeignKey(nameof(IssuesId))]
public Issues Issues{ get; set; }
[ForeignKey(nameof(UserId))]
public OmbiUser User { get; set; }
}
}

@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
namespace Ombi.Store.Entities.Requests
{
[Table("Issues")]
public class Issues : Entity
{
public string Title { get; set; }
public RequestType RequestType { get; set; }
public string ProviderId { get; set; }
public int? RequestId { get; set; }
public string Subject { get; set; }
public string Description { get; set; }
public int IssueCategoryId { get; set; }
[ForeignKey(nameof(IssueCategoryId))]
public IssueCategory IssueCategory { get; set; }
public IssueStatus Status { get; set; }
public DateTime? ResovledDate { get; set; }
[ForeignKey(nameof(UserReported))]
public string UserReportedId { get; set; }
public OmbiUser UserReported { get; set; }
public List<IssueComments> Comments { get; set; }
}
public enum IssueStatus
{
Pending = 0,
InProgress = 1,
Resolved = 2,
}
}

@ -1,9 +0,0 @@
namespace Ombi.Store.Entities.Requests
{
public class IssuesBase : Entity
{
public string Subect { get; set; }
public string Description { get; set; }
}
}

@ -1,12 +0,0 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace Ombi.Store.Entities.Requests
{
[Table("MovieIssues")]
public class MovieIssues : IssuesBase
{
public int MovieId { get; set; }
[ForeignKey(nameof(MovieId))]
public MovieRequests Movie { get; set; }
}
}

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

@ -1,12 +0,0 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace Ombi.Store.Entities.Requests
{
[Table("TvIssues")]
public class TvIssues : IssuesBase
{
public int TvId { get; set; }
[ForeignKey(nameof(TvId))]
public ChildRequests Child { get; set; }
}
}

@ -447,7 +447,7 @@ namespace Ombi.Store.Migrations
b.Property<int>("MovieId");
b.Property<string>("Subect");
b.Property<string>("Subject");
b.HasKey("Id");
@ -513,7 +513,7 @@ namespace Ombi.Store.Migrations
b.Property<int?>("IssueId");
b.Property<string>("Subect");
b.Property<string>("Subject");
b.Property<int>("TvId");

@ -504,7 +504,7 @@ namespace Ombi.Store.Migrations
Description = table.Column<string>(type: "TEXT", nullable: true),
IssueId = table.Column<int>(type: "INTEGER", nullable: true),
MovieId = table.Column<int>(type: "INTEGER", nullable: false),
Subect = table.Column<string>(type: "TEXT", nullable: true)
Subject = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
@ -551,7 +551,7 @@ namespace Ombi.Store.Migrations
.Annotation("Sqlite:Autoincrement", true),
Description = table.Column<string>(type: "TEXT", nullable: true),
IssueId = table.Column<int>(type: "INTEGER", nullable: true),
Subect = table.Column<string>(type: "TEXT", nullable: true),
Subject = table.Column<string>(type: "TEXT", nullable: true),
TvId = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>

@ -447,7 +447,7 @@ namespace Ombi.Store.Migrations
b.Property<int>("MovieId");
b.Property<string>("Subect");
b.Property<string>("Subject");
b.HasKey("Id");
@ -515,7 +515,7 @@ namespace Ombi.Store.Migrations
b.Property<int?>("IssueId");
b.Property<string>("Subect");
b.Property<string>("Subject");
b.Property<int>("TvId");

@ -449,7 +449,7 @@ namespace Ombi.Store.Migrations
b.Property<int>("MovieId");
b.Property<string>("Subect");
b.Property<string>("Subject");
b.HasKey("Id");
@ -517,7 +517,7 @@ namespace Ombi.Store.Migrations
b.Property<int?>("IssueId");
b.Property<string>("Subect");
b.Property<string>("Subject");
b.Property<int>("TvId");

@ -449,7 +449,7 @@ namespace Ombi.Store.Migrations
b.Property<int>("MovieId");
b.Property<string>("Subect");
b.Property<string>("Subject");
b.HasKey("Id");
@ -517,7 +517,7 @@ namespace Ombi.Store.Migrations
b.Property<int?>("IssueId");
b.Property<string>("Subect");
b.Property<string>("Subject");
b.Property<int>("TvId");

@ -0,0 +1,840 @@
// <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 Ombi.Store.Entities.Requests;
using System;
namespace Ombi.Store.Migrations
{
[DbContext(typeof(OmbiContext))]
[Migration("20171213154624_Issues")]
partial class Issues
{
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.CouchPotatoCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("TheMovieDbId");
b.HasKey("Id");
b.ToTable("CouchPotatoCache");
});
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<string>("EmbyConnectUserId");
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.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?>("PlexServerContentId");
b.Property<int>("SeasonKey");
b.Property<int>("SeasonNumber");
b.HasKey("Id");
b.HasIndex("PlexServerContentId");
b.ToTable("PlexSeasonsContent");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexServerContent", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("AddedAt");
b.Property<string>("ImdbId");
b.Property<int>("Key");
b.Property<string>("Quality");
b.Property<string>("ReleaseYear");
b.Property<string>("TheMovieDbId");
b.Property<string>("Title");
b.Property<string>("TvDbId");
b.Property<int>("Type");
b.Property<string>("Url");
b.HasKey("Id");
b.ToTable("PlexServerContent");
});
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.IssueCategory", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Value");
b.HasKey("Id");
b.ToTable("IssueCategory");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.IssueComments", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Comment");
b.Property<DateTime>("Date");
b.Property<int?>("IssuesId");
b.Property<string>("UserId");
b.HasKey("Id");
b.HasIndex("IssuesId");
b.HasIndex("UserId");
b.ToTable("IssueComments");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.Issues", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Description");
b.Property<int>("IssueCategoryId");
b.Property<int?>("IssueId");
b.Property<string>("ProviderId");
b.Property<int?>("RequestId");
b.Property<int>("RequestType");
b.Property<DateTime?>("ResovledDate");
b.Property<int>("Status");
b.Property<string>("Subject");
b.Property<string>("Title");
b.HasKey("Id");
b.HasIndex("IssueCategoryId");
b.HasIndex("IssueId");
b.ToTable("Issues");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.MovieRequests", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<bool>("Approved");
b.Property<bool>("Available");
b.Property<string>("Background");
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.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.SickRageCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("TvDbId");
b.HasKey("Id");
b.ToTable("SickRageCache");
});
modelBuilder.Entity("Ombi.Store.Entities.SickRageEpisodeCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("EpisodeNumber");
b.Property<int>("SeasonNumber");
b.Property<int>("TvDbId");
b.HasKey("Id");
b.ToTable("SickRageEpisodeCache");
});
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.SonarrEpisodeCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("EpisodeNumber");
b.Property<int>("SeasonNumber");
b.Property<int>("TvDbId");
b.HasKey("Id");
b.ToTable("SonarrEpisodeCache");
});
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.PlexServerContent", "Series")
.WithMany("Episodes")
.HasForeignKey("GrandparentKey")
.HasPrincipalKey("Key")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b =>
{
b.HasOne("Ombi.Store.Entities.PlexServerContent")
.WithMany("Seasons")
.HasForeignKey("PlexServerContentId");
});
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.IssueComments", b =>
{
b.HasOne("Ombi.Store.Entities.Requests.Issues", "Issues")
.WithMany("Comments")
.HasForeignKey("IssuesId");
b.HasOne("Ombi.Store.Entities.OmbiUser", "User")
.WithMany()
.HasForeignKey("UserId");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.Issues", b =>
{
b.HasOne("Ombi.Store.Entities.Requests.IssueCategory", "IssueCategory")
.WithMany()
.HasForeignKey("IssueCategoryId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("Ombi.Store.Entities.Requests.ChildRequests")
.WithMany("Issues")
.HasForeignKey("IssueId");
b.HasOne("Ombi.Store.Entities.Requests.MovieRequests")
.WithMany("Issues")
.HasForeignKey("IssueId");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.MovieRequests", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser")
.WithMany()
.HasForeignKey("RequestedUserId");
});
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,138 @@
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
namespace Ombi.Store.Migrations
{
public partial class Issues : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "IssueCategory",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Value = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_IssueCategory", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Issues",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Description = table.Column<string>(type: "TEXT", nullable: true),
IssueCategoryId = table.Column<int>(type: "INTEGER", nullable: false),
IssueId = table.Column<int>(type: "INTEGER", nullable: true),
ProviderId = table.Column<string>(type: "TEXT", nullable: true),
RequestId = table.Column<int>(type: "INTEGER", nullable: true),
RequestType = table.Column<int>(type: "INTEGER", nullable: false),
ResovledDate = table.Column<DateTime>(type: "TEXT", nullable: true),
Status = table.Column<int>(type: "INTEGER", nullable: false),
Subject = table.Column<string>(type: "TEXT", nullable: true),
Title = table.Column<string>(type: "TEXT", nullable: true),
UserReportedId = table.Column<string>(type: "TEXT", nullable: true),
},
constraints: table =>
{
table.PrimaryKey("PK_Issues", x => x.Id);
table.ForeignKey(
name: "FK_Issues_IssueCategory_IssueCategoryId",
column: x => x.IssueCategoryId,
principalTable: "IssueCategory",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Issues_ChildRequests_IssueId",
column: x => x.IssueId,
principalTable: "ChildRequests",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_Issues_MovieRequests_IssueId",
column: x => x.IssueId,
principalTable: "MovieRequests",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_Issues_AspNetUsers_UserReportedId",
column: x => x.UserReportedId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "IssueComments",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Comment = table.Column<string>(type: "TEXT", nullable: true),
Date = table.Column<DateTime>(type: "TEXT", nullable: false),
IssuesId = table.Column<int>(type: "INTEGER", nullable: true),
UserId = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_IssueComments", x => x.Id);
table.ForeignKey(
name: "FK_IssueComments_Issues_IssuesId",
column: x => x.IssuesId,
principalTable: "Issues",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_IssueComments_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_IssueComments_IssuesId",
table: "IssueComments",
column: "IssuesId");
migrationBuilder.CreateIndex(
name: "IX_IssueComments_UserId",
table: "IssueComments",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_Issues_IssueCategoryId",
table: "Issues",
column: "IssueCategoryId");
migrationBuilder.CreateIndex(
name: "IX_Issues_IssueId",
table: "Issues",
column: "IssueId");
migrationBuilder.CreateIndex(
name: "IX_Issues_UserReportedId",
table: "Issues",
column: "UserReportedId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "IssueComments");
migrationBuilder.DropTable(
name: "Issues");
migrationBuilder.DropTable(
name: "IssueCategory");
}
}
}

@ -8,6 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.Internal;
using Ombi.Helpers;
using Ombi.Store.Context;
using Ombi.Store.Entities;
using Ombi.Store.Entities.Requests;
using System;
namespace Ombi.Store.Migrations
@ -441,26 +442,76 @@ namespace Ombi.Store.Migrations
b.ToTable("ChildRequests");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.MovieIssues", b =>
modelBuilder.Entity("Ombi.Store.Entities.Requests.IssueCategory", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Value");
b.HasKey("Id");
b.ToTable("IssueCategory");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.IssueComments", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Comment");
b.Property<DateTime>("Date");
b.Property<int?>("IssuesId");
b.Property<string>("UserId");
b.HasKey("Id");
b.HasIndex("IssuesId");
b.HasIndex("UserId");
b.ToTable("IssueComments");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.Issues", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Description");
b.Property<int>("IssueCategoryId");
b.Property<int?>("IssueId");
b.Property<int>("MovieId");
b.Property<string>("ProviderId");
b.Property<int?>("RequestId");
b.Property<int>("RequestType");
b.Property<DateTime?>("ResovledDate");
b.Property<int>("Status");
b.Property<string>("Subject");
b.Property<string>("Subect");
b.Property<string>("Title");
b.Property<string>("UserReportedId");
b.HasKey("Id");
b.HasIndex("IssueCategoryId");
b.HasIndex("IssueId");
b.HasIndex("MovieId");
b.HasIndex("UserReportedId");
b.ToTable("MovieIssues");
b.ToTable("Issues");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.MovieRequests", b =>
@ -533,28 +584,6 @@ namespace Ombi.Store.Migrations
b.ToTable("RequestLog");
});
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")
@ -778,23 +807,34 @@ namespace Ombi.Store.Migrations
.HasForeignKey("RequestedUserId");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.MovieIssues", b =>
modelBuilder.Entity("Ombi.Store.Entities.Requests.IssueComments", b =>
{
b.HasOne("Ombi.Store.Entities.Requests.MovieRequests")
.WithMany("Issues")
.HasForeignKey("IssueId");
b.HasOne("Ombi.Store.Entities.Requests.Issues", "Issues")
.WithMany("Comments")
.HasForeignKey("IssuesId");
b.HasOne("Ombi.Store.Entities.Requests.MovieRequests", "Movie")
b.HasOne("Ombi.Store.Entities.OmbiUser", "User")
.WithMany()
.HasForeignKey("MovieId")
.OnDelete(DeleteBehavior.Cascade);
.HasForeignKey("UserId");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.MovieRequests", b =>
modelBuilder.Entity("Ombi.Store.Entities.Requests.Issues", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser")
b.HasOne("Ombi.Store.Entities.Requests.IssueCategory", "IssueCategory")
.WithMany()
.HasForeignKey("RequestedUserId");
.HasForeignKey("IssueCategoryId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("Ombi.Store.Entities.Requests.ChildRequests")
.WithMany("Issues")
.HasForeignKey("IssueId");
b.HasOne("Ombi.Store.Entities.Requests.MovieRequests")
.WithMany("Issues")
.HasForeignKey("IssueId");
b.HasOne("Ombi.Store.Entities.OmbiUser", "UserReported")
.WithMany()
.HasForeignKey("UserReportedId");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.RequestLog", b =>
@ -803,17 +843,13 @@ namespace Ombi.Store.Migrations
.WithMany()
.HasForeignKey("UserId");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.TvIssues", b =>
modelBuilder.Entity("Ombi.Store.Entities.Requests.MovieRequests", b =>
{
b.HasOne("Ombi.Store.Entities.Requests.ChildRequests")
.WithMany("Issues")
.HasForeignKey("IssueId");
b.HasOne("Ombi.Store.Entities.Requests.ChildRequests", "Child")
b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser")
.WithMany()
.HasForeignKey("TvId")
.OnDelete(DeleteBehavior.Cascade);
.HasForeignKey("RequestedUserId");
});
modelBuilder.Entity("Ombi.Store.Entities.Tokens", b =>

@ -7,7 +7,6 @@ namespace Ombi.Store.Repository
{
public interface IPlexContentRepository : IRepository<PlexServerContent>
{
Task<PlexServerContent> Add(PlexServerContent content);
Task<bool> ContentExists(string providerId);
Task<PlexServerContent> Get(string providerId);
Task<PlexServerContent> GetByKey(int key);

@ -14,10 +14,10 @@ namespace Ombi.Store.Repository
IQueryable<T> GetAll();
Task<T> FirstOrDefaultAsync(Expression<Func<T, bool>> predicate);
Task AddRange(IEnumerable<T> content);
Task<T> Add(T content);
Task DeleteRange(IEnumerable<T> req);
Task Delete(T request);
Task<int> SaveChangesAsync();
Task<T> Add(T content);
IIncludableQueryable<TEntity, TProperty> Include<TEntity, TProperty>(
IQueryable<TEntity> source, Expression<Func<TEntity, TProperty>> navigationPropertyPath)

@ -6,7 +6,6 @@ namespace Ombi.Store.Repository.Requests
{
public interface IMovieRequestRepository : IRepository<MovieRequests>
{
Task<MovieRequests> Add(MovieRequests request);
Task<MovieRequests> GetRequestAsync(int theMovieDbId);
MovieRequests GetRequest(int theMovieDbId);
Task Update(MovieRequests request);

@ -4,6 +4,7 @@ using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Ombi.Helpers;
using Ombi.Store.Context;
using Ombi.Store.Entities;
@ -11,14 +12,14 @@ namespace Ombi.Store.Repository
{
public class SettingsJsonRepository : ISettingsRepository
{
public SettingsJsonRepository(IOmbiContext ctx, IMemoryCache mem)
public SettingsJsonRepository(IOmbiContext ctx, ICacheService mem)
{
Db = ctx;
_cache = mem;
}
private IOmbiContext Db { get; }
private readonly IMemoryCache _cache;
private readonly ICacheService _cache;
public GlobalSettings Insert(GlobalSettings entity)
{

@ -34,6 +34,12 @@
<i class="fa fa-plus"></i> {{ 'NavigationBar.Requests' | translate }}</a>
</li>
</ul>
<ul *ngIf="issuesEnabled" class="nav navbar-nav">
<li id="Requests" [routerLinkActive]="['active']">
<a [routerLink]="['/issues']">
<i class="fa fa-exclamation-circle"></i> {{ 'NavigationBar.Issues' | translate }}</a>
</li>
</ul>
<ul *ngIf="hasRole('Admin') || hasRole('PowerUser')" class="nav navbar-nav">
<li id="UserManagement" [routerLinkActive]="['active']">
<a [routerLink]="['/usermanagement']">

@ -16,6 +16,7 @@ import { ICustomizationSettings } from "./interfaces";
export class AppComponent implements OnInit {
public customizationSettings: ICustomizationSettings;
public issuesEnabled = false;
public user: ILocalUser;
public showNav: boolean;
public updateAvailable: boolean;
@ -42,6 +43,7 @@ export class AppComponent implements OnInit {
this.user = this.authService.claims();
this.settingsService.getCustomization().subscribe(x => this.customizationSettings = x);
this.settingsService.issueEnabled().subscribe(x => this.issuesEnabled = x);
this.router.events.subscribe((event: NavigationStart) => {
this.currentUrl = event.url;

@ -10,14 +10,12 @@ import {RouterModule, Routes} from "@angular/router";
import { JwtModule } from "@auth0/angular-jwt";
// Third Party
//import { DragulaModule, DragulaService } from 'ng2-dragula/ng2-dragula';
import { NgbModule } from "@ng-bootstrap/ng-bootstrap";
import { TranslateLoader, TranslateModule } from "@ngx-translate/core";
import { TranslateHttpLoader } from "@ngx-translate/http-loader";
import { CookieService } from "ng2-cookies";
import { GrowlModule } from "primeng/components/growl/growl";
import { ButtonModule, CaptchaModule,ConfirmationService, ConfirmDialogModule, DataTableModule,DialogModule, SharedModule, TooltipModule } from "primeng/primeng";
import { ButtonModule, CaptchaModule, ConfirmationService, ConfirmDialogModule, DataTableModule,DialogModule, SharedModule, SidebarModule, TooltipModule } from "primeng/primeng";
// Components
import { AppComponent } from "./app.component";
@ -36,28 +34,24 @@ import { IdentityService } from "./services";
import { ImageService } from "./services";
import { LandingPageService } from "./services";
import { NotificationService } from "./services";
import { RequestService } from "./services";
import { SettingsService } from "./services";
import { StatusService } from "./services";
// Modules
import { RequestsModule } from "./requests/requests.module";
import { SearchModule } from "./search/search.module";
import { SettingsModule } from "./settings/settings.module";
import { UserManagementModule } from "./usermanagement/usermanagement.module";
import { WizardModule } from "./wizard/wizard.module";
import { IssuesService, JobService, StatusService } from "./services";
const routes: Routes = [
{ path: "*", component: PageNotFoundComponent },
{ path: "", redirectTo: "/search", pathMatch: "full" },
//{ path: 'requests-grid', component: RequestGridComponent },
{ path: "login", component: LoginComponent },
{ path: "login/:landing", component: LoginComponent },
{ path: "reset", component: ResetPasswordComponent },
{ path: "token", component: TokenResetPasswordComponent },
{ path: "landingpage", component: LandingPageComponent },
{ path: "auth/cookie", component: CookieComponent },
{ loadChildren: "./issues/issues.module#IssuesModule", path: "issues" },
{ loadChildren: "./settings/settings.module#SettingsModule", path: "Settings" },
{ loadChildren: "./wizard/wizard.module#WizardModule", path: "Wizard" },
{ loadChildren: "./usermanagement/usermanagement.module#UserManagementModule", path: "usermanagement" },
{ loadChildren: "./requests/requests.module#RequestsModule", path: "requests" },
{ loadChildren: "./search/search.module#SearchModule", path: "search" },
];
// AoT requires an exported function for factories
@ -79,11 +73,8 @@ export function HttpLoaderFactory(http: HttpClient, platformLocation: PlatformLo
GrowlModule,
ButtonModule,
FormsModule,
SettingsModule,
DataTableModule,
SharedModule,
WizardModule,
SearchModule,
DialogModule,
MatButtonModule,
NgbModule.forRoot(),
@ -91,8 +82,6 @@ export function HttpLoaderFactory(http: HttpClient, platformLocation: PlatformLo
MatInputModule,
MatTabsModule,
ReactiveFormsModule,
UserManagementModule,
RequestsModule,
CaptchaModule,
TooltipModule,
ConfirmDialogModule,
@ -115,6 +104,7 @@ export function HttpLoaderFactory(http: HttpClient, platformLocation: PlatformLo
deps: [HttpClient, PlatformLocation],
},
}),
SidebarModule,
],
declarations: [
AppComponent,
@ -124,9 +114,8 @@ export function HttpLoaderFactory(http: HttpClient, platformLocation: PlatformLo
ResetPasswordComponent,
TokenResetPasswordComponent,
CookieComponent,
],
],
providers: [
RequestService,
NotificationService,
AuthService,
AuthGuard,
@ -137,6 +126,8 @@ export function HttpLoaderFactory(http: HttpClient, platformLocation: PlatformLo
ConfirmationService,
ImageService,
CookieService,
JobService,
IssuesService,
],
bootstrap: [AppComponent],
})

@ -0,0 +1,63 @@
import { IIssueCategory, IUser, RequestType } from "./";
export interface IIssues {
id?: number;
title: string;
requestType: RequestType;
providerId: string;
subject: string;
description: string;
issueCategory: IIssueCategory;
issueCategoryId: number;
status: IssueStatus;
resolvedDate?: Date;
comments: IIssueComments[];
requestId: number | undefined;
userReported: IUser | undefined;
}
export enum IssueStatus {
Pending = 0,
InProgress = 1,
Resolved = 2,
}
export interface IIssueCount {
pending: number;
inProgress: number;
resolved: number;
}
export interface IPagenator {
first: number;
rows: number;
page: number;
pageCount: number;
}
export interface IIssueComments {
userId: string;
comment: string;
movieIssueId: number | undefined;
tvIssueId: number | undefined;
date: Date;
user: IUser;
issues: IIssues | undefined;
}
export interface IIssuesChat {
comment: string;
date: Date;
username: string;
adminComment: boolean;
}
export interface INewIssueComments {
comment: string;
issueId: number;
}
export interface IUpdateStatus {
issueId: number;
status: IssueStatus;
}

@ -23,58 +23,11 @@ export interface IMediaBase {
released: boolean;
}
//export interface IMovieRequestModel extends IMediaBase { }
export interface ITvRequestModel extends IMediaBase {
imdbId: string;
tvDbId: string;
childRequests: IChildTvRequest[];
rootFolderSelected: number;
firstAired: string;
}
export interface IRequestCountModel {
pending: number;
approved: number;
available: number;
}
export interface IChildTvRequest extends IMediaBase {
requestAll: boolean;
seasonRequests: ISeasonRequests[];
}
export interface ISeasonRequests {
seasonNumber: number;
episodes: IEpisodesRequested[];
}
export interface IEpisodesRequested {
episodeNumber: number;
title: string;
airDate: Date;
url: string;
requested: boolean;
status: string;
available: boolean;
}
export enum RequestType {
movie = 1,
tvShow = 2,
}
export interface IRequestsPageScroll {
count: number;
position: number;
}
export interface IRequestGrid<T> {
available: T[];
new: T[];
approved: T[];
}
// NEW WORLD
export interface IMovieRequests extends IFullBaseRequest {
@ -117,6 +70,7 @@ export interface IBaseRequest {
requestType: RequestType;
requestedUser: IUser;
canApprove: boolean;
title: string;
}
export interface ITvRequests {

@ -122,6 +122,11 @@ export interface IJobSettings {
sickRageSync: string;
}
export interface IIssueSettings extends ISettings {
enabled: boolean;
enableInProgress: boolean;
}
export interface IAuthenticationSettings extends ISettings {
allowNoPassword: boolean;
// Password
@ -179,3 +184,7 @@ export interface IDogNzbSettings extends ISettings {
movies: boolean;
tvShows: boolean;
}
export interface IIssueCategory extends ISettings {
value: string;
}

@ -12,3 +12,4 @@ export * from "./ISearchTvResult";
export * from "./ISettings";
export * from "./ISonarr";
export * from "./IUser";
export * from "./IIssues";

@ -0,0 +1,76 @@
<div *ngIf="issue">
<h1>{{issue.title}} </h1>
<div class="col-md-6">
<span class="label label-info">{{IssueStatus[issue.status]}}</span>
<span class="label label-success">{{issue.issueCategory.value}}</span>
<h3 *ngIf="issue.userReported?.alias">{{'Issues.ReportedBy' | translate}}: {{issue.userReported.alias}}</h3>
<h3 *ngIf="!issue.userReported?.alias">{{'Issues.ReportedBy' | translate}}: {{issue.userReported.userName}}</h3>
<h3 *ngIf="issue.subject">{{'Issues.Subject' | translate}}: {{issue.subject}}</h3>
<br>
<div class="form-group">
<label for="description" class="control-label" [translate]="'Issues.Description'"></label>
<div>
<textarea class="form-control-custom form-control" disabled="disabled" [(ngModel)]="issue.description" rows="5" type="text"></textarea>
</div>
</div>
</div>
<div class="row chat-window col-xs-7 col-md-5" id="chat_window_1" style="margin-left:10px;">
<div class="col-xs-12 col-md-12">
<div class="panel panel-default">
<div class="panel-heading top-bar">
<div class="col-md-8 col-xs-8">
<h3 class="panel-title">
<span class="glyphicon glyphicon-comment"></span> {{'Issues.Comments' | translate}}</h3>
</div>
</div>
<div *ngIf="comments" class="panel-body msg_container_base">
<div *ngIf="comments.length <= 0" class="row msg_container base_receive">
<div class="col-md-10 col-xs-10">
<div class="messages msg_sent">
<p [translate]="'Issues.NoComments'"></p>
</div>
</div>
</div>
<div *ngFor="let comment of comments" class="row msg_container" [ngClass]="{'base_sent': comment.adminComment, 'base_receive': !comment.adminComment}">
<div class="col-md-10 col-xs-10">
<div class="messages msg_sent">
<p>{{comment.comment}}</p>
<time>{{comment.username}} • {{comment.date | date:'short'}}</time>
</div>
</div>
</div>
</div>
<div class="panel-footer">
<div class="input-group">
<input id="btn-input" type="text" class="form-control input-sm chat_input" [(ngModel)]="newComment.comment" [attr.placeholder]="'Issues.WriteMessagePlaceholder' | translate"
/>
<span class="input-group-btn">
<button class="btn btn-primary btn-sm" id="btn-chat" (click)="addComment()" [translate]="'Issues.SendMessageButton'"></button>
</span>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-12">
<div *ngIf="isAdmin && settings">
<div *ngIf="issue.status === IssueStatus.Pending && settings.enableInProgress">
<button class="btn btn-primary btn-sm" (click)="inProgress()" [translate]="'Issues.MarkInProgress'"></button>
</div>
<div *ngIf="issue.status === IssueStatus.Pending && !settings.enableInProgress || issue.status == IssueStatus.InProgress">
<button class="btn btn-primary btn-sm" (click)="resolve()" [translate]="'Issues.MarkResolved'"></button>
</div>
</div>
</div>
</div>

@ -0,0 +1,137 @@
$color:#424242;
body{
height:400px;
position: fixed;
bottom: 0;
}
.col-md-2, .col-md-10{
padding:0;
}
.panel{
margin-bottom: 0px;
}
.chat-window{
float:right;
margin-left:10px;
}
.chat-window > div > .panel{
border-radius: 5px 5px 0 0;
}
.icon_minim{
padding:2px 10px;
}
.msg_container_base{
background: #e5e5e5;
margin: 0;
padding: 0 10px 10px;
max-height:300px;
overflow-x:hidden;
}
.top-bar {
background: $color;
color: white;
padding: 10px;
position: relative;
overflow: hidden;
}
.msg_receive{
padding-left:0;
margin-left:0;
}
.msg_sent{
padding-bottom:20px !important;
margin-right:0;
}
.messages {
background: white;
padding: 10px;
border-radius: 2px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
max-width:100%;
color:black;
}
.messages > p {
font-size: 13px;
margin: 0 0 0.2rem 0;
}
.messages > time {
font-size: 11px;
color: #ccc;
}
.msg_container {
padding: 10px;
overflow: hidden;
display: flex;
}
img {
display: block;
width: 100%;
}
.avatar {
position: relative;
}
.base_receive > .avatar:after {
content: "";
position: absolute;
top: 0;
right: 0;
width: 0;
height: 0;
border: 5px solid #FFF;
border-left-color: rgba(0, 0, 0, 0);
border-bottom-color: rgba(0, 0, 0, 0);
}
.base_sent {
justify-content: flex-end;
align-items: flex-end;
}
.base_sent > .avatar:after {
content: "";
position: absolute;
bottom: 0;
left: 0;
width: 0;
height: 0;
border: 5px solid white;
border-right-color: transparent;
border-top-color: transparent;
box-shadow: 0px 1px 0px rgba(0, 0, 0, 0.1);
}
.msg_sent > time{
float: right;
}
.msg_container_base::-webkit-scrollbar-track
{
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
background-color: #F5F5F5;
}
.msg_container_base::-webkit-scrollbar
{
width: 12px;
background-color: #F5F5F5;
}
.msg_container_base::-webkit-scrollbar-thumb
{
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3);
background-color: #555;
}
.btn-group.dropup{
position:fixed;
left:0px;
bottom:0;
}
.panel-footer {
padding: 10px 15px;
background-color: $color;
border-top: 1px solid transparent;
border-bottom-right-radius: -1;
border-bottom-left-radius: -1;
}

@ -0,0 +1,88 @@
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { AuthService } from "../auth/auth.service";
import { IssuesService, NotificationService, SettingsService } from "../services";
import { IIssues, IIssuesChat, IIssueSettings, INewIssueComments, IssueStatus } from "../interfaces";
@Component({
templateUrl: "issueDetails.component.html",
styleUrls: ["./issueDetails.component.scss"],
})
export class IssueDetailsComponent implements OnInit {
public issue: IIssues;
public comments: IIssuesChat[];
public newComment: INewIssueComments = {
comment: "",
issueId: 0,
};
public IssueStatus = IssueStatus;
public isAdmin: boolean;
public settings: IIssueSettings;
private issueId: number;
constructor(private issueService: IssuesService,
private route: ActivatedRoute,
private authService: AuthService,
private settingsService: SettingsService,
private notificationService: NotificationService) {
this.route.params
.subscribe((params: any) => {
this.issueId = parseInt(params.id);
});
this.isAdmin = this.authService.hasRole("Admin") || this.authService.hasRole("PowerUser");
this.settingsService.getIssueSettings().subscribe(x => this.settings = x);
}
public ngOnInit() {
this.issueService.getIssue(this.issueId).subscribe(x => {
this.issue = {
comments: x.comments,
id: x.id,
issueCategory: x.issueCategory,
issueCategoryId: x.issueCategoryId,
subject: x.subject,
description: x.description,
status:x.status,
resolvedDate:x.resolvedDate,
title: x.title,
requestType: x.requestType,
requestId: x.requestId,
providerId: x.providerId,
userReported: x.userReported,
};
});
this.loadComments();
}
public addComment() {
this.newComment.issueId = this.issueId;
this.issueService.addComment(this.newComment).subscribe(x => {
this.loadComments();
});
}
public inProgress() {
this.issueService.updateStatus({issueId: this.issueId, status: IssueStatus.InProgress}).subscribe(x => {
this.notificationService.success("Marked issue as In Progress");
this.issue.status = IssueStatus.InProgress;
});
}
public resolve() {
this.issueService.updateStatus({issueId: this.issueId, status: IssueStatus.Resolved}).subscribe(x => {
this.notificationService.success("Marked issue as Resolved");
this.issue.status = IssueStatus.Resolved;
});
}
private loadComments() {
this.issueService.getComments(this.issueId).subscribe(x => this.comments = x);
}
}

@ -0,0 +1,28 @@
<h1 id="issuesTitle" [translate]="'Issues.Title'"></h1>
<ngb-tabset *ngIf="count">
<ngb-tab *ngIf="count.pending > 0">
<ng-template ngbTabTitle>{{'Issues.PendingTitle' | translate}} <span class="badge">{{count.pending}}</span></ng-template>
<ng-template ngbTabContent>
<div *ngIf="pendingIssues">
<issues-table [issues]="pendingIssues" (changePage)="changePagePending($event)" [totalRecords]="count.pending"></issues-table>
</div>
</ng-template>
</ngb-tab>
<ngb-tab *ngIf="count.inProgress > 0">
<ng-template ngbTabTitle>{{'Issues.InProgressTitle' | translate}} <span class="badge">{{count.inProgress}}</span></ng-template>
<ng-template ngbTabContent>
<div *ngIf="inProgressIssues">
<issues-table [issues]="inProgressIssues" (changePage)="changePageInProg($event)" [totalRecords]="count.inProgress"></issues-table>
</div>
</ng-template>
</ngb-tab>
<ngb-tab *ngIf="count.resolved > 0">
<ng-template ngbTabTitle>{{'Issues.ResolvedTitle' | translate}} <span class="badge">{{count.resolved}}</span></ng-template>
<ng-template ngbTabContent>
<div *ngIf="resolvedIssues">
<issues-table [issues]="resolvedIssues" (changePage)="changePageResolved($event)" [totalRecords]="count.resolved"></issues-table>
</div>
</ng-template>
</ngb-tab>
</ngb-tabset>

@ -0,0 +1,65 @@
import { Component, OnInit } from "@angular/core";
import { IssuesService } from "../services";
import { IIssueCount, IIssues, IPagenator, IssueStatus } from "../interfaces";
@Component({
templateUrl: "issues.component.html",
})
export class IssuesComponent implements OnInit {
public pendingIssues: IIssues[];
public inProgressIssues: IIssues[];
public resolvedIssues: IIssues[];
public count: IIssueCount;
private takeAmount = 10;
private pendingSkip = 0;
private inProgressSkip = 0;
private resolvedSkip = 0;
constructor(private issueService: IssuesService) { }
public ngOnInit() {
this.getPending();
this.getInProg();
this.getResolved();
this.issueService.getIssuesCount().subscribe(x => this.count = x);
}
public changePagePending(event: IPagenator) {
this.pendingSkip = event.first;
this.getPending();
}
public changePageInProg(event: IPagenator) {
this.inProgressSkip = event.first;
this.getInProg();
}
public changePageResolved(event: IPagenator) {
this.resolvedSkip = event.first;
this.getResolved();
}
private getPending() {
this.issueService.getIssuesPage(this.takeAmount, this.pendingSkip, IssueStatus.Pending).subscribe(x => {
this.pendingIssues = x;
});
}
private getInProg() {
this.issueService.getIssuesPage(this.takeAmount, this.inProgressSkip, IssueStatus.InProgress).subscribe(x => {
this.inProgressIssues = x;
});
}
private getResolved() {
this.issueService.getIssuesPage(this.takeAmount, this.resolvedSkip, IssueStatus.Resolved).subscribe(x => {
this.resolvedIssues = x;
});
}
}

@ -0,0 +1,49 @@
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { NgbModule } from "@ng-bootstrap/ng-bootstrap";
import { OrderModule } from "ngx-order-pipe";
import { PaginatorModule, SharedModule, TabViewModule } from "primeng/primeng";
import { IdentityService } from "../services";
import { AuthGuard } from "../auth/auth.guard";
import { SharedModule as OmbiShared } from "../shared/shared.module";
import { IssueDetailsComponent } from "./issueDetails.component";
import { IssuesComponent } from "./issues.component";
import { IssuesTableComponent } from "./issuestable.component";
import { PipeModule } from "../pipes/pipe.module";
const routes: Routes = [
{ path: "", component: IssuesComponent, canActivate: [AuthGuard] },
{ path: ":id", component: IssueDetailsComponent, canActivate: [AuthGuard] },
];
@NgModule({
imports: [
RouterModule.forChild(routes),
NgbModule.forRoot(),
SharedModule,
OrderModule,
PipeModule,
OmbiShared,
PaginatorModule,
TabViewModule,
],
declarations: [
IssuesComponent,
IssueDetailsComponent,
IssuesTableComponent,
],
exports: [
RouterModule,
],
providers: [
IdentityService,
],
})
export class IssuesModule { }

@ -0,0 +1,55 @@
<table class="table table-striped table-hover table-responsive table-condensed">
<thead>
<tr>
<th (click)="setOrder('title')">
<a [translate]="'Issues.ColumnTitle'"></a>
<span *ngIf="order === 'title'">
<span [hidden]="reverse"><i class="fa fa-arrow-down" aria-hidden="true"></i></span><span [hidden]="!reverse"><i class="fa fa-arrow-up" aria-hidden="true"></i></span>
</span>
</th>
<th (click)="setOrder('issueCategory.value')">
<a [translate]="'Issues.Category'"></a>
<span *ngIf="order === 'issueCategory.value'">
<span [hidden]="reverse"><i class="fa fa-arrow-down" aria-hidden="true"></i></span><span [hidden]="!reverse"><i class="fa fa-arrow-up" aria-hidden="true"></i></span>
</span>
</th>
<th (click)="setOrder('issue.status')">
<a [translate]="'Issues.Status'"></a>
<span *ngIf="order === 'issue.status'">
<span [hidden]="reverse"><i class="fa fa-arrow-down" aria-hidden="true"></i></span><span [hidden]="!reverse"><i class="fa fa-arrow-up" aria-hidden="true"></i></span>
</span>
</th>
<th (click)="setOrder('issue.reportedUser')">
<a [translate]="'Issues.ReportedBy'"></a>
<span *ngIf="order === 'issue.reportedUser'">
<span [hidden]="reverse"><i class="fa fa-arrow-down" aria-hidden="true"></i></span><span [hidden]="!reverse"><i class="fa fa-arrow-up" aria-hidden="true"></i></span>
</span>
</th>
<th>
</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let issue of issues | orderBy: order : reverse : 'case-insensitive'">
<td>
{{issue.title}}
</td>
<td>
{{issue.issueCategory.value}}
</td>
<td>
{{IssueStatus[issue.status] | humanize}}
</td>
<td *ngIf="issue.userReported?.alias">
{{issue.userReported.alias}}
</td>
<td *ngIf="!issue.userReported?.alias">
{{issue.userReported.userName}}
</td>
<td>
<a [routerLink]="['/issues/' + issue.id]" class="btn btn-sm btn-info-outline" [translate]="'Issues.Details'"></a>
</td>
</tr>
</tbody>
</table>
<p-paginator [rows]="rowCount" [totalRecords]="totalRecords" (onPageChange)="paginate($event)"></p-paginator>

@ -0,0 +1,40 @@
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { IIssues, IPagenator, IssueStatus } from "./../interfaces";
@Component({
selector: "issues-table",
templateUrl: "issuestable.component.html",
})
export class IssuesTableComponent {
@Input() public issues: IIssues[];
@Input() public totalRecords: number;
@Output() public changePage = new EventEmitter<IPagenator>();
public IssueStatus = IssueStatus;
public order: string = "id";
public reverse = false;
public rowCount = 10;
public setOrder(value: string) {
if (this.order === value) {
this.reverse = !this.reverse;
}
this.order = value;
}
public paginate(event: IPagenator) {
//event.first = Index of the first record (current index)
//event.rows = Number of rows to display in new page
//event.page = Index of the new page
//event.pageCount = Total number of pages
this.changePage.emit(event);
}
}

@ -107,27 +107,19 @@
<button *ngIf="!request.available" (click)="changeAvailability(request, true)" style="text-align: right" value="true" class="btn btn-sm btn-success-outline change"><i class="fa fa-plus"></i> {{ 'Requests.MarkAvailable' | translate }}</button>
</form>
<div class="dropdown" *ngIf="issueCategories && issuesEnabled">
<button class="btn btn-sm btn-primary-outline dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<i class="fa fa-plus"></i> Report Issue
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu1">
<li *ngFor="let cat of issueCategories"><a [routerLink]="" (click)="reportIssue(cat, request)">{{cat.value}}</a></li>
</ul>
</div>
</div>
<!--<div class="dropdown">
<button id="{{request.requestId}}" class="btn btn-sm btn-primary-outline dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<i class="fa fa-plus"></i> Report Issue
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu1">
<li><a issue-select="0">@UI.Issues_WrongAudio</a></li>
<li><a issue-select="1">@UI.Issues_NoSubs</a></li>
<li><a issue-select="2">@UI.Issues_WrongContent</a></li>
<li><a issue-select="3">@UI.Issues_Playback</a></li>
<li><a issue-select="4" data-toggle="modal" data-target="#myModal">@UI.Issues_Other</a></li>
</ul>
</div>-->
</div>
</div>
<br/>
@ -138,3 +130,7 @@
</div>
</div>
<issue-report [movie]="true" [visible]="issuesBarVisible" (visibleChange)="issuesBarVisible = $event;" [title]="issueRequest?.title"
[issueCategory]="issueCategorySelected" [id]="issueRequest?.id" [providerId]=""></issue-report>

@ -1,4 +1,4 @@
import { Component, OnInit } from "@angular/core";
import { Component, Input, OnInit } from "@angular/core";
import { DomSanitizer } from "@angular/platform-browser";
import "rxjs/add/operator/debounceTime";
import "rxjs/add/operator/distinctUntilChanged";
@ -8,7 +8,7 @@ import { Subject } from "rxjs/Subject";
import { AuthService } from "../auth/auth.service";
import { NotificationService, RadarrService, RequestService } from "../services";
import { IMovieRequests, IRadarrProfile, IRadarrRootFolder } from "../interfaces";
import { IIssueCategory, IMovieRequests, IRadarrProfile, IRadarrRootFolder } from "../interfaces";
@Component({
selector: "movie-requests",
@ -25,6 +25,13 @@ export class MovieRequestsComponent implements OnInit {
public radarrProfiles: IRadarrProfile[];
public radarrRootFolders: IRadarrRootFolder[];
@Input() public issueCategories: IIssueCategory[];
@Input() public issuesEnabled: boolean;
public issuesBarVisible = false;
public issueRequest: IMovieRequests;
public issueProviderId: string;
public issueCategorySelected: IIssueCategory;
private currentlyLoaded: number;
private amountToLoad: number;
@ -121,6 +128,17 @@ export class MovieRequestsComponent implements OnInit {
this.updateRequest(searchResult);
}
public reportIssue(catId: IIssueCategory, req: IMovieRequests) {
this.issueRequest = req;
this.issueCategorySelected = catId;
this.issuesBarVisible = true;
this.issueProviderId = req.theMovieDbId.toString();
}
public ignore(event: any): void {
event.preventDefault();
}
private loadRequests(amountToLoad: number, currentlyLoaded: number) {
this.requestService.getMovieRequests(amountToLoad, currentlyLoaded + 1)
.subscribe(x => {

@ -1,8 +1,6 @@
<h1 id="searchTitle" [translate]="'Requests.Title'"></h1>
<h4 [translate]="'Requests.Paragraph'"></h4>
<ul id="nav-tabs" class="nav nav-tabs" role="tablist">
<li role="presentation" class="active">
<a id="movieTabButton" aria-controls="home" role="tab" data-toggle="tab" (click)="selectMovieTab()"><i class="fa fa-film"></i> {{ 'Requests.MoviesTab' | translate }}</a>
@ -16,10 +14,10 @@
<!-- Tab panes -->
<div class="tab-content">
<div [hidden]="!showMovie">
<movie-requests></movie-requests>
<movie-requests [issueCategories]="issueCategories" [issuesEnabled]="issuesEnabled"></movie-requests>
</div>
<div [hidden]="!showTv">
<tv-requests></tv-requests>
<tv-requests [issueCategories]="issueCategories" [issuesEnabled]="issuesEnabled"></tv-requests>
</div>
</div>

@ -1,13 +1,30 @@
import { Component } from "@angular/core";

import { Component, OnInit } from "@angular/core";
import { IIssueCategory } from "./../interfaces";
import { IssuesService, SettingsService } from "./../services";
@Component({
templateUrl: "./request.component.html",
})
export class RequestComponent {
export class RequestComponent implements OnInit {
public showMovie = true;
public showTv = false;
public issueCategories: IIssueCategory[];
public issuesEnabled = false;
constructor(private issuesService: IssuesService,
private settingsService: SettingsService) {
}
public ngOnInit(): void {
this.issuesService.getCategories().subscribe(x => this.issueCategories = x);
this.settingsService.getIssueSettings().subscribe(x => this.issuesEnabled = x.enabled);
}
public selectMovieTab() {
this.showMovie = true;
this.showTv = false;

@ -12,20 +12,18 @@ import { RequestComponent } from "./request.component";
import { TvRequestChildrenComponent } from "./tvrequest-children.component";
import { TvRequestsComponent } from "./tvrequests.component";
import { TreeTableModule } from "primeng/primeng";
import { SidebarModule, TreeTableModule } from "primeng/primeng";
import { IdentityService } from "../services";
import { RequestService } from "../services";
import { IdentityService, RadarrService, RequestService } from "../services";
import { AuthGuard } from "../auth/auth.guard";
import { SharedModule } from "../shared/shared.module";
const routes: Routes = [
{ path: "requests", component: RequestComponent, canActivate: [AuthGuard] },
{ path: "requests/:id", component: TvRequestChildrenComponent, canActivate: [AuthGuard] },
{ path: "", component: RequestComponent, canActivate: [AuthGuard] },
{ path: ":id", component: TvRequestChildrenComponent, canActivate: [AuthGuard] },
];
@NgModule({
imports: [
RouterModule.forChild(routes),
@ -35,6 +33,7 @@ const routes: Routes = [
DialogModule,
TreeTableModule,
SharedModule,
SidebarModule,
],
declarations: [
RequestComponent,
@ -48,6 +47,7 @@ const routes: Routes = [
providers: [
IdentityService,
RequestService,
RadarrService,
],
})

@ -12,8 +12,8 @@
</div>
<div class="col-md-1 col-md-push-9" *ngIf="isAdmin">
<div class="col-md-1 col-md-push-9">
<div *ngIf="isAdmin">
<button *ngIf="child.canApprove && !child.approved" (click)="approve(child)" class="btn btn-sm btn-success-outline" type="submit"><i class="fa fa-plus"></i> {{ 'Common.Approve' | translate }}</button>
<button *ngIf="child.available" (click)="changeAvailability(child, false)" style="text-align: right" value="false" class="btn btn-sm btn-info-outline change"><i class="fa fa-minus"></i> {{ 'Requests.MarkUnavailable' | translate }}</button>
<button *ngIf="!child.available" (click)="changeAvailability(child, true)" style="text-align: right" value="true" class="btn btn-sm btn-success-outline change"><i class="fa fa-plus"></i> {{ 'Requests.MarkAvailable' | translate }}</button>
@ -21,6 +21,16 @@
<button *ngIf="!child.denied" type="button" (click)="deny(child)" class="btn btn-sm btn-danger-outline deny"><i class="fa fa-times"></i> {{ 'Requests.Deny' | translate }}</button>
<button type="button" (click)="removeRequest(child)" class="btn btn-sm btn-danger-outline deny"><i class="fa fa-times"></i> {{ 'Requests.Remove' | translate }}</button>
</div>
<div class="dropdown" *ngIf="issueCategories && issuesEnabled">
<button class="btn btn-sm btn-primary-outline dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<i class="fa fa-plus"></i> Report Issue
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu1">
<li *ngFor="let cat of issueCategories"><a [routerLink]="" (click)="reportIssue(cat, child)">{{cat.value}}</a></li>
</ul>
</div>
</div>
</div>
@ -90,4 +100,9 @@
<hr />
</div>
</div>
</div>
<issue-report [movie]="false" [visible]="issuesBarVisible" [title]="issueRequest?.title"
[issueCategory]="issueCategorySelected" [id]="issueRequest?.id" (visibleChange)="issuesBarVisible = $event;"></issue-report>

@ -1,5 +1,6 @@
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { IChildRequests } from "../interfaces";
import { IChildRequests, IIssueCategory } from "../interfaces";
import { NotificationService, RequestService } from "../services";
@Component({
@ -12,6 +13,13 @@ export class TvRequestChildrenComponent {
@Output() public requestDeleted = new EventEmitter<number>();
@Input() public issueCategories: IIssueCategory[];
@Input() public issuesEnabled: boolean;
@Input() public issueProviderId: string;
public issuesBarVisible = false;
public issueRequest: IChildRequests;
public issueCategorySelected: IIssueCategory;
constructor(private requestService: RequestService,
private notificationService: NotificationService) { }
@ -93,6 +101,12 @@ export class TvRequestChildrenComponent {
});
}
public reportIssue(catId: IIssueCategory, req: IChildRequests) {
this.issueRequest = req;
this.issueCategorySelected = catId;
this.issuesBarVisible = true;
}
private removeRequestFromUi(key: IChildRequests) {
const index = this.childRequests.indexOf(key, 0);
if (index > -1) {

@ -58,7 +58,7 @@
</div>
<!--This is the section that holds the child seasons if they want to specify specific episodes-->
<div *ngIf="node.leaf">
<tvrequests-children [childRequests]="node.data" [isAdmin] ="isAdmin" (requestDeleted)="childRequestDeleted($event)" ></tvrequests-children>
<tvrequests-children [childRequests]="node.data" [isAdmin] ="isAdmin" (requestDeleted)="childRequestDeleted($event)" [issueCategories]="issueCategories" [issuesEnabled]="issuesEnabled" [issueProviderId]="node.data.tvDbId" ></tvrequests-children>
</div>
</ng-template>
</p-column>

@ -1,4 +1,4 @@
import { Component, OnInit } from "@angular/core";
import { Component, Input, OnInit } from "@angular/core";
import { DomSanitizer } from "@angular/platform-browser";
import "rxjs/add/operator/debounceTime";
import "rxjs/add/operator/distinctUntilChanged";
@ -14,7 +14,7 @@ import { AuthService } from "../auth/auth.service";
import { RequestService } from "../services";
import { TreeNode } from "primeng/primeng";
import { ITvRequests } from "../interfaces";
import { IIssueCategory, ITvRequests } from "../interfaces";
@Component({
selector: "tv-requests",
@ -30,6 +30,10 @@ export class TvRequestsComponent implements OnInit {
public showChildDialogue = false; // This is for the child modal popup
public selectedSeason: ITvRequests;
@Input() public issueCategories: IIssueCategory[];
@Input() public issuesEnabled: boolean;
public issueProviderId: string;
private currentlyLoaded: number;
private amountToLoad: number;

@ -81,6 +81,15 @@
<div *ngIf="result.available">
<a *ngIf="result.plexUrl" style="text-align: right" class="btn btn-sm btn-success-outline" href="{{result.plexUrl}}" target="_blank"><i class="fa fa-eye"></i> View On Plex</a>
</div>
<div class="dropdown" *ngIf="result.available && issueCategories && issuesEnabled">
<button class="btn btn-sm btn-primary-outline dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<i class="fa fa-plus"></i> Report Issue
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu1">
<li *ngFor="let cat of issueCategories"><a [routerLink]="" (click)="reportIssue(cat, result)">{{cat.value}}</a></li>
</ul>
</div>
</div>
@ -90,4 +99,8 @@
</div>
</div>
</div>
</div>
<issue-report [movie]="true" [visible]="issuesBarVisible" (visibleChange)="issuesBarVisible = $event;" [title]="issueRequestTitle"
[issueCategory]="issueCategorySelected" [id]="issueRequestId" [providerId]="issueProviderId"></issue-report>

@ -1,4 +1,4 @@
import { Component, OnInit } from "@angular/core";
import { Component, Input, OnInit } from "@angular/core";
import { DomSanitizer } from "@angular/platform-browser";
import { TranslateService } from "@ngx-translate/core";
import "rxjs/add/operator/debounceTime";
@ -7,10 +7,9 @@ import "rxjs/add/operator/map";
import { Subject } from "rxjs/Subject";
import { AuthService } from "../auth/auth.service";
import { IIssueCategory, IRequestEngineResult, ISearchMovieResult } from "../interfaces";
import { NotificationService, RequestService, SearchService } from "../services";
import { IRequestEngineResult, ISearchMovieResult } from "../interfaces";
@Component({
selector: "movie-search",
templateUrl: "./moviesearch.component.html",
@ -22,6 +21,14 @@ export class MovieSearchComponent implements OnInit {
public movieResults: ISearchMovieResult[];
public result: IRequestEngineResult;
public searchApplied = false;
@Input() public issueCategories: IIssueCategory[];
@Input() public issuesEnabled: boolean;
public issuesBarVisible = false;
public issueRequestTitle: string;
public issueRequestId: number;
public issueProviderId: string;
public issueCategorySelected: IIssueCategory;
constructor(private searchService: SearchService, private requestService: RequestService,
private notificationService: NotificationService, private authService: AuthService,
@ -131,6 +138,14 @@ export class MovieSearchComponent implements OnInit {
});
}
public reportIssue(catId: IIssueCategory, req: ISearchMovieResult) {
this.issueRequestId = req.id;
this.issueRequestTitle = req.title;
this.issueCategorySelected = catId;
this.issuesBarVisible = true;
this.issueProviderId = req.id.toString();
}
private getExtraInfo() {
this.movieResults.forEach((val, index) => {

@ -19,11 +19,11 @@
<div class="tab-content">
<div [hidden]="!showMovie">
<movie-search></movie-search>
<movie-search [issueCategories]="issueCategories" [issuesEnabled]="issuesEnabled"></movie-search>
</div>
<div [hidden]="!showTv">
<tv-search></tv-search>
<tv-search [issueCategories]="issueCategories" [issuesEnabled]="issuesEnabled"></tv-search>
</div>
</div>

@ -1,14 +1,27 @@
import { Component, OnInit } from "@angular/core";
import { IIssueCategory } from "./../interfaces";
import { IssuesService, SettingsService } from "./../services";
@Component({
templateUrl: "./search.component.html",
})
export class SearchComponent implements OnInit {
public showTv: boolean;
public showMovie: boolean;
public issueCategories: IIssueCategory[];
public issuesEnabled = false;
constructor(private issuesService: IssuesService,
private settingsService: SettingsService) {
}
public ngOnInit() {
this.showMovie = true;
this.showTv = false;
this.issuesService.getCategories().subscribe(x => this.issueCategories = x);
this.settingsService.getIssueSettings().subscribe(x => this.issuesEnabled = x.enabled);
}
public selectMovieTab() {

@ -11,7 +11,7 @@ import { SearchComponent } from "./search.component";
import { SeriesInformationComponent } from "./seriesinformation.component";
import { TvSearchComponent } from "./tvsearch.component";
import { TreeTableModule } from "primeng/primeng";
import { SidebarModule, TreeTableModule } from "primeng/primeng";
import { RequestService } from "../services";
import { SearchService } from "../services";
@ -21,18 +21,18 @@ import { AuthGuard } from "../auth/auth.guard";
import { SharedModule } from "../shared/shared.module";
const routes: Routes = [
{ path: "search", component: SearchComponent, canActivate: [AuthGuard] },
{ path: "search/show/:id", component: SeriesInformationComponent, canActivate: [AuthGuard] },
{ path: "", component: SearchComponent, canActivate: [AuthGuard] },
{ path: "show/:id", component: SeriesInformationComponent, canActivate: [AuthGuard] },
];
@NgModule({
imports: [
imports: [
CommonModule,
FormsModule,
RouterModule.forChild(routes),
NgbModule.forRoot(),
TreeTableModule,
SharedModule,
SidebarModule,
],
declarations: [
SearchComponent,

@ -1,5 +1,4 @@
import { Component, Input, OnDestroy, OnInit} from "@angular/core";
//import { ActivatedRoute } from '@angular/router';
import { Subject } from "rxjs/Subject";
import "rxjs/add/operator/takeUntil";
@ -37,7 +36,6 @@ export class SeriesInformationComponent implements OnInit, OnDestroy {
}
public submitRequests() {
// Make sure something has been selected
const selected = this.series.seasonRequests.some((season) => {
return season.episodes.some((ep) => {

@ -9,12 +9,21 @@
<i class="fa fa-chevron-down"></i>
</a>
<ul class="dropdown-menu">
<li><a (click)="popularShows()">Popular Shows</a></li>
<li><a (click)="trendingShows()">Trending Shows</a></li>
<li><a (click)="mostWatchedShows()">Most Watched Shows</a></li>
<li><a (click)="anticipatedShows()">Most Anticipated Shows</a></li>
<li>
<a (click)="popularShows()">Popular Shows</a>
</li>
<li>
<a (click)="trendingShows()">Trending Shows</a>
</li>
<li>
<a (click)="mostWatchedShows()">Most Watched Shows</a>
</li>
<li>
<a (click)="anticipatedShows()">Most Anticipated Shows</a>
</li>
</ul>
</div><i id="tvSearchButton" class="fa fa-search"></i>
</div>
<i id="tvSearchButton" class="fa fa-search"></i>
</div>
</div>
<br />
@ -24,13 +33,14 @@
</div>
<br />
<br />
<br />
<!-- TV content -->
<div id="tvList">
<div *ngIf="searchApplied && tvResults?.length <= 0" class='no-search-results'>
<i class='fa fa-film no-search-results-icon'></i><div class='no-search-results-text'>Sorry, we didn't find any results!</div>
<i class='fa fa-film no-search-results-icon'></i>
<div class='no-search-results-text'>Sorry, we didn't find any results!</div>
</div>
<p-treeTable [value]="tvResults">
<p-column>
@ -60,16 +70,24 @@
<span *ngIf="node.data.network" class="label label-info" id="networkLabel" target="_blank">{{node.data.network}}</span>
<ng-template [ngIf]="node.data.available"><span class="label label-success" id="availableLabel">Available</span></ng-template>
<ng-template [ngIf]="node.data.partlyAvailable"><span class="label label-warning" id="partiallyAvailableLabel">Partially Available</span></ng-template>
<ng-template [ngIf]="node.data.available">
<span class="label label-success">Available</span>
</ng-template>
<ng-template [ngIf]="node.data.partlyAvailable">
<span class="label label-warning">Partially Available</span>
</ng-template>
<a *ngIf="node.data.homepage" href="{{node.data.homepage}}" target="_blank"><span class="label label-info" id="homepageLabel">HomePage</span></a>
<a *ngIf="node.data.trailer" href="{{node.data.trailer}}" target="_blank"><span class="label label-info" id="trailerLabel">Trailer</span></a>
<br />
<br />
</div>
@ -84,19 +102,42 @@
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu1">
<li><a href="#" (click)="allSeasons(node.data, $event)">All Seasons</a></li>
<li><a href="#" (click)="firstSeason(node.data, $event)">First Season</a></li>
<li><a href="#" (click)="latestSeason(node.data, $event)">Latest Season</a></li>
<li><a href="#" (click)="openClosestTab($event)">Select ...</a></li>
<li>
<a href="#" (click)="allSeasons(node.data, $event)">All Seasons</a>
</li>
<li>
<a href="#" (click)="firstSeason(node.data, $event)">First Season</a>
</li>
<li>
<a href="#" (click)="latestSeason(node.data, $event)">Latest Season</a>
</li>
<li>
<a href="#" (click)="openClosestTab($event)">Select ...</a>
</li>
</ul>
</div>
<div *ngIf="node.data.fullyAvailable">
<button style="text-align: right" class="btn btn-success-outline disabled" disabled><i class="fa fa-check"></i> Available</button>
<button style="text-align: right" class="btn btn-success-outline disabled" disabled>
<i class="fa fa-check"></i> Available</button>
</div>
<br />
<div *ngIf="node.data.available">
<a *ngIf="node.data.plexUrl" style="text-align: right" class="btn btn-sm btn-success-outline" href="{{node.data.plexUrl}}" target="_blank"><i class="fa fa-eye"></i> View On Plex</a>
<a *ngIf="node.data.plexUrl" style="text-align: right" class="btn btn-sm btn-success-outline" href="{{node.data.plexUrl}}"
target="_blank">
<i class="fa fa-eye"></i> View On Plex</a>
</div>
<div class="dropdown" *ngIf="issueCategories && issuesEnabled">
<button class="btn btn-sm btn-primary-outline dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true"
aria-expanded="true">
<i class="fa fa-plus"></i> Report Issue
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu1">
<li *ngFor="let cat of issueCategories">
<a [routerLink]="" (click)="reportIssue(cat, node.data)">{{cat.value}}</a>
</li>
</ul>
</div>
<div *ngIf="!node.data.available">
<br/>
@ -111,11 +152,15 @@
<div *ngIf="node.leaf">
<seriesinformation [seriesId]="node.data.id"></seriesinformation>
</div>
<br/>
<br/>
</ng-template>
</p-column>
</p-treeTable>
</div>
</div>
</div>
<issue-report [movie]="false" [visible]="issuesBarVisible" (visibleChange)="issuesBarVisible = $event;" [title]="issueRequestTitle"
[issueCategory]="issueCategorySelected" [id]="issueRequestId" [providerId]="issueProviderId"></issue-report>

@ -1,6 +1,5 @@
import { Component, OnDestroy, OnInit } from "@angular/core";
import { Component, Input, OnInit } from "@angular/core";
import { DomSanitizer } from "@angular/platform-browser";
import { Router } from "@angular/router";
import { Subject } from "rxjs/Subject";
import { AuthService } from "../auth/auth.service";
@ -8,14 +7,14 @@ import { ImageService, NotificationService, RequestService, SearchService} from
import { TreeNode } from "primeng/primeng";
import { IRequestEngineResult } from "../interfaces";
import { ISearchTvResult } from "../interfaces";
import { IIssueCategory, ISearchTvResult } from "../interfaces";
@Component({
selector: "tv-search",
templateUrl: "./tvsearch.component.html",
styleUrls: ["./../requests/tvrequests.component.scss"],
})
export class TvSearchComponent implements OnInit, OnDestroy {
export class TvSearchComponent implements OnInit {
public searchText: string;
public searchChanged = new Subject<string>();
@ -23,16 +22,21 @@ export class TvSearchComponent implements OnInit, OnDestroy {
public result: IRequestEngineResult;
public searchApplied = false;
private subscriptions = new Subject<void>();
@Input() public issueCategories: IIssueCategory[];
@Input() public issuesEnabled: boolean;
public issuesBarVisible = false;
public issueRequestTitle: string;
public issueRequestId: number;
public issueProviderId: string;
public issueCategorySelected: IIssueCategory;
constructor(private searchService: SearchService, private requestService: RequestService,
private notificationService: NotificationService, private route: Router, private authService: AuthService,
private notificationService: NotificationService, private authService: AuthService,
private imageService: ImageService, private sanitizer: DomSanitizer) {
this.searchChanged
.debounceTime(600) // Wait Xms after the 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 +44,6 @@ export class TvSearchComponent implements OnInit, OnDestroy {
return;
}
this.searchService.searchTvTreeNode(this.searchText)
.takeUntil(this.subscriptions)
.subscribe(x => {
this.tvResults = x;
this.searchApplied = true;
@ -91,7 +94,6 @@ export class TvSearchComponent implements OnInit, OnDestroy {
public popularShows() {
this.clearResults();
this.searchService.popularTv()
.takeUntil(this.subscriptions)
.subscribe(x => {
this.tvResults = x;
this.getExtraInfo();
@ -101,7 +103,6 @@ export class TvSearchComponent implements OnInit, OnDestroy {
public trendingShows() {
this.clearResults();
this.searchService.trendingTv()
.takeUntil(this.subscriptions)
.subscribe(x => {
this.tvResults = x;
this.getExtraInfo();
@ -111,7 +112,6 @@ export class TvSearchComponent implements OnInit, OnDestroy {
public mostWatchedShows() {
this.clearResults();
this.searchService.mostWatchedTv()
.takeUntil(this.subscriptions)
.subscribe(x => {
this.tvResults = x;
this.getExtraInfo();
@ -121,7 +121,6 @@ export class TvSearchComponent implements OnInit, OnDestroy {
public anticipatedShows() {
this.clearResults();
this.searchService.anticipatedTv()
.takeUntil(this.subscriptions)
.subscribe(x => {
this.tvResults = x;
this.getExtraInfo();
@ -138,7 +137,6 @@ export class TvSearchComponent implements OnInit, OnDestroy {
("linear-gradient(to bottom, rgba(0,0,0,0.6) 0%,rgba(0,0,0,0.6) 100%),url(" + x + ")");
});
this.searchService.getShowInformationTreeNode(val.data.id)
.takeUntil(this.subscriptions)
.subscribe(x => {
if (x.data) {
this.updateItem(val, x);
@ -158,7 +156,6 @@ export class TvSearchComponent implements OnInit, OnDestroy {
searchResult.approved = true;
}
this.requestService.requestTv(searchResult)
.takeUntil(this.subscriptions)
.subscribe(x => {
this.result = x;
if (this.result.result) {
@ -192,13 +189,12 @@ export class TvSearchComponent implements OnInit, OnDestroy {
this.request(searchResult);
}
public selectSeason(searchResult: ISearchTvResult) {
this.route.navigate(["/search/show", searchResult.id]);
}
public ngOnDestroy() {
this.subscriptions.next();
this.subscriptions.complete();
public reportIssue(catId: IIssueCategory, req: ISearchTvResult) {
this.issueRequestId = req.id;
this.issueRequestTitle = req.title;
this.issueCategorySelected = catId;
this.issuesBarVisible = true;
this.issueProviderId = req.id.toString();
}
private updateItem(key: TreeNode, updated: TreeNode) {

@ -10,3 +10,4 @@ export * from "./service.helpers";
export * from "./settings.service";
export * from "./status.service";
export * from "./job.service";
export * from "./issues.service";

@ -0,0 +1,59 @@
import { PlatformLocation } from "@angular/common";
import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs/Rx";
import { IIssueCategory, IIssueComments,IIssueCount, IIssues, IIssuesChat, INewIssueComments, IssueStatus, IUpdateStatus } from "../interfaces";
import { ServiceHelpers } from "./service.helpers";
@Injectable()
export class IssuesService extends ServiceHelpers {
constructor(http: HttpClient, public platformLocation: PlatformLocation) {
super(http, "/api/v1/Issues/", platformLocation);
}
public getCategories(): Observable<IIssueCategory[]> {
return this.http.get<IIssueCategory[]>(`${this.url}categories/`, {headers: this.headers});
}
public createCategory(cat: IIssueCategory): Observable<boolean> {
return this.http.post<boolean>(`${this.url}categories/`, JSON.stringify(cat), {headers: this.headers});
}
public deleteCategory(cat: number): Observable<boolean> {
return this.http.delete<boolean>(`${this.url}categories/${cat}`, {headers: this.headers});
}
public getIssues(): Observable<IIssues[]> {
return this.http.get<IIssues[]>(this.url, {headers: this.headers});
}
public getIssuesPage(take: number, skip: number, status: IssueStatus): Observable<IIssues[]> {
return this.http.get<IIssues[]>(`${this.url}${take}/${skip}/${status}`, {headers: this.headers});
}
public getIssuesCount(): Observable<IIssueCount> {
return this.http.get<IIssueCount>(`${this.url}count`, {headers: this.headers});
}
public createIssue(issue: IIssues): Observable<number> {
return this.http.post<number>(this.url, JSON.stringify(issue), {headers: this.headers});
}
public getIssue(id: number): Observable<IIssues> {
return this.http.get<IIssues>(`${this.url}${id}`, {headers: this.headers});
}
public getComments(id: number): Observable<IIssuesChat[]> {
return this.http.get<IIssuesChat[]>(`${this.url}${id}/comments`, {headers: this.headers});
}
public addComment(comment: INewIssueComments): Observable<IIssueComments> {
return this.http.post<IIssueComments>(`${this.url}comments`, JSON.stringify(comment), {headers: this.headers});
}
public updateStatus(model: IUpdateStatus): Observable<boolean> {
return this.http.post<boolean>(`${this.url}status`, JSON.stringify(model), { headers: this.headers });
}
}

@ -12,6 +12,7 @@ import {
IDogNzbSettings,
IEmailNotificationSettings,
IEmbySettings,
IIssueSettings,
IJobSettings,
ILandingPageSettings,
IMattermostNotifcationSettings,
@ -233,4 +234,17 @@ export class SettingsService extends ServiceHelpers {
return this.http
.post<boolean>(`${this.url}/sickrage`, JSON.stringify(settings), {headers: this.headers});
}
public getIssueSettings(): Observable<IIssueSettings> {
return this.http.get<IIssueSettings>(`${this.url}/issues`, {headers: this.headers});
}
public issueEnabled(): Observable<boolean> {
return this.http.get<boolean>(`${this.url}/issuesenabled`, {headers: this.headers});
}
public saveIssueSettings(settings: IIssueSettings): Observable<boolean> {
return this.http
.post<boolean>(`${this.url}/issues`, JSON.stringify(settings), {headers: this.headers});
}
}

@ -0,0 +1,61 @@
<settings-menu></settings-menu>
<wiki [url]="'https://github.com/tidusjar/Ombi/wiki/Issue-Settings'"></wiki>
<fieldset>
<legend>Issues</legend>
<form *ngIf="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">
<div class="checkbox">
<input type="checkbox" id="enableInProgress" formControlName="enableInProgress" ng-checked="form.enableInProgress">
<label for="enableInProgress">Enable In Progress State</label>
</div>
</div>
<div class="form-group">
<div>
<button type="submit" [disabled]="form.invalid" class="btn btn-primary-outline ">Submit</button>
</div>
</div>
</div>
</form>
<div class="col-md-6">
<div *ngIf="categories">
<div class="form-group row">
<div class="col-md-12">
<label for="categoryToAdd" class="control-label">Add Category</label>
</div>
<div class="col-md-9">
<input type="text" [(ngModel)]="categoryToAdd.value" class="form-control form-control-custom " id="categoryToAdd" name="categoryToAdd"
value="{{categoryToAdd.value}}">
</div>
<div class="col-md-3">
<button class="btn btn-primary-outline" (click)="addCategory()">Add</button>
</div>
</div>
<div class="row">
<div *ngFor="let cat of categories">
<div class="col-md-9">
{{cat.value}}
</div>
<div class="col-md-3">
<button class="btn btn-sm btn-danger-outline" (click)="deleteCategory(cat.id)">Delete</button>
</div>
</div>
</div>
</div>
</div>
</fieldset>

@ -0,0 +1,70 @@
import { Component, OnInit } from "@angular/core";
import { FormBuilder, FormGroup } from "@angular/forms";
import { IIssueCategory } from "../../interfaces";
import { IssuesService, NotificationService, SettingsService } from "../../services";
@Component({
templateUrl: "./issues.component.html",
})
export class IssuesComponent implements OnInit {
public categories: IIssueCategory[];
public categoryToAdd: IIssueCategory = {id: 0, value: ""};
public form: FormGroup;
constructor(private issuesService: IssuesService,
private settingsService: SettingsService,
private readonly fb: FormBuilder,
private notificationService: NotificationService) { }
public ngOnInit() {
this.settingsService.getIssueSettings().subscribe(x => {
this.form = this.fb.group({
enabled: [x.enabled],
enableInProgress: [x.enableInProgress],
});
});
this.getCategories();
}
public addCategory(): void {
this.issuesService.createCategory(this.categoryToAdd).subscribe(x => {
if(x) {
this.getCategories();
this.categoryToAdd.value = "";
}
});
}
public deleteCategory(id: number) {
this.issuesService.deleteCategory(id).subscribe(x => {
if(x) {
this.getCategories();
}
});
}
public onSubmit(form: FormGroup) {
if (form.invalid) {
this.notificationService.error("Please check your entered values");
return;
}
const settings = form.value;
this.settingsService.saveIssueSettings(settings).subscribe(x => {
if (x) {
this.notificationService.success("Successfully saved the Issue settings");
} else {
this.notificationService.success("There was an error when saving the Issue settings");
}
});
}
private getCategories() {
this.issuesService.getCategories().subscribe(x => {
this.categories = x;
});
}
}

@ -17,7 +17,7 @@
<p class="form-group">Notice Message</p>
<div class="form-group">
<div>
<textarea rows="4" type="text" class="form-control-custom form-control " id="NoticeMessage" name="NoticeMessage" placeholder="e.g. The server will be down for maintaince (HTML is allowed)" [(ngModel)]="settings.noticeText">{{noticeText}}</textarea>
<textarea rows="4" type="text" class="form-control-custom form-control " id="NoticeMessage" name="NoticeMessage" placeholder="e.g. The server will be down for maintaince (HTML is allowed)" [(ngModel)]="settings.noticeText">{{settings.noticeText}}</textarea>
</div>
</div>

@ -7,7 +7,7 @@ import { ClipboardModule } from "ngx-clipboard/dist";
import { AuthGuard } from "../auth/auth.guard";
import { AuthService } from "../auth/auth.service";
import { CouchPotatoService, JobService, RadarrService, SonarrService, TesterService, ValidationService } from "../services";
import { CouchPotatoService, EmbyService, IssuesService, JobService, PlexService, RadarrService, SonarrService, TesterService, ValidationService } from "../services";
import { PipeModule } from "../pipes/pipe.module";
import { AboutComponent } from "./about/about.component";
@ -16,6 +16,7 @@ import { CouchPotatoComponent } from "./couchpotato/couchpotato.component";
import { CustomizationComponent } from "./customization/customization.component";
import { DogNzbComponent } from "./dognzb/dognzb.component";
import { EmbyComponent } from "./emby/emby.component";
import { IssuesComponent } from "./issues/issues.component";
import { JobsComponent } from "./jobs/jobs.component";
import { LandingPageComponent } from "./landingpage/landingpage.component";
import { DiscordComponent } from "./notifications/discord.component";
@ -40,28 +41,29 @@ import { SettingsMenuComponent } from "./settingsmenu.component";
import { AutoCompleteModule, CalendarModule, InputSwitchModule, InputTextModule, MenuModule, RadioButtonModule, TooltipModule } from "primeng/primeng";
const routes: Routes = [
{ path: "Settings/Ombi", component: OmbiComponent, canActivate: [AuthGuard] },
{ path: "Settings/About", component: AboutComponent, canActivate: [AuthGuard] },
{ path: "Settings/Plex", component: PlexComponent, canActivate: [AuthGuard] },
{ path: "Settings/Emby", component: EmbyComponent, canActivate: [AuthGuard] },
{ path: "Settings/Sonarr", component: SonarrComponent, canActivate: [AuthGuard] },
{ path: "Settings/Radarr", component: RadarrComponent, canActivate: [AuthGuard] },
{ path: "Settings/LandingPage", component: LandingPageComponent, canActivate: [AuthGuard] },
{ path: "Settings/Customization", component: CustomizationComponent, canActivate: [AuthGuard] },
{ path: "Settings/Email", component: EmailNotificationComponent, canActivate: [AuthGuard] },
{ path: "Settings/Discord", component: DiscordComponent, canActivate: [AuthGuard] },
{ path: "Settings/Slack", component: SlackComponent, canActivate: [AuthGuard] },
{ path: "Settings/Pushover", component: PushoverComponent, canActivate: [AuthGuard] },
{ path: "Settings/Pushbullet", component: PushbulletComponent, canActivate: [AuthGuard] },
{ 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] },
{ path: "Settings/DogNzb", component: DogNzbComponent, canActivate: [AuthGuard] },
{ path: "Settings/Telegram", component: TelegramComponent, canActivate: [AuthGuard] },
{ path: "Settings/Jobs", component: JobsComponent, canActivate: [AuthGuard] },
{ path: "Settings/SickRage", component: SickRageComponent, canActivate: [AuthGuard] },
{ path: "Settings/Authentication", component: AuthenticationComponent, canActivate: [AuthGuard] },
{ path: "Ombi", component: OmbiComponent, canActivate: [AuthGuard] },
{ path: "About", component: AboutComponent, canActivate: [AuthGuard] },
{ path: "Plex", component: PlexComponent, canActivate: [AuthGuard] },
{ path: "Emby", component: EmbyComponent, canActivate: [AuthGuard] },
{ path: "Sonarr", component: SonarrComponent, canActivate: [AuthGuard] },
{ path: "Radarr", component: RadarrComponent, canActivate: [AuthGuard] },
{ path: "LandingPage", component: LandingPageComponent, canActivate: [AuthGuard] },
{ path: "Customization", component: CustomizationComponent, canActivate: [AuthGuard] },
{ path: "Email", component: EmailNotificationComponent, canActivate: [AuthGuard] },
{ path: "Discord", component: DiscordComponent, canActivate: [AuthGuard] },
{ path: "Slack", component: SlackComponent, canActivate: [AuthGuard] },
{ path: "Pushover", component: PushoverComponent, canActivate: [AuthGuard] },
{ path: "Pushbullet", component: PushbulletComponent, canActivate: [AuthGuard] },
{ path: "Mattermost", component: MattermostComponent, canActivate: [AuthGuard] },
{ path: "UserManagement", component: UserManagementComponent, canActivate: [AuthGuard] },
{ path: "Update", component: UpdateComponent, canActivate: [AuthGuard] },
{ path: "CouchPotato", component: CouchPotatoComponent, canActivate: [AuthGuard] },
{ path: "DogNzb", component: DogNzbComponent, canActivate: [AuthGuard] },
{ path: "Telegram", component: TelegramComponent, canActivate: [AuthGuard] },
{ path: "Jobs", component: JobsComponent, canActivate: [AuthGuard] },
{ path: "SickRage", component: SickRageComponent, canActivate: [AuthGuard] },
{ path: "Issues", component: IssuesComponent, canActivate: [AuthGuard] },
{ path: "Authentication", component: AuthenticationComponent, canActivate: [AuthGuard] },
];
@NgModule({
@ -107,6 +109,7 @@ const routes: Routes = [
DogNzbComponent,
SickRageComponent,
TelegramComponent,
IssuesComponent,
AuthenticationComponent,
],
exports: [
@ -121,6 +124,9 @@ const routes: Routes = [
TesterService,
JobService,
CouchPotatoService,
IssuesService,
PlexService,
EmbyService,
],
})

@ -9,6 +9,7 @@
<ul class="dropdown-menu">
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Customization']">Customization</a></li>
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/LandingPage']">Landing Page</a></li>
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Issues']">Issues</a></li>
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/UserManagement']">User Importer</a></li>
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Authentication']">Authentication</a></li>
</ul>

@ -0,0 +1,26 @@
<p-sidebar [(visible)]="visible" position="right" styleClass="ui-sidebar-md side-back" (onHide)="hide()">
<div *ngIf="title">
<h3>Reporting an Issue for "{{title}}"</h3>
<h4 *ngIf="issueCategory">Issue type: {{issueCategory.value}}</h4>
<div class="form-group">
<label for="subject" class="control-label">Subject</label>
<div>
<input type="text" [(ngModel)]="issue.subject" class="form-control form-control-custom " id="subject" name="subject"
value="{{issue?.subject}}">
</div>
</div>
<div class="form-group">
<label for="description" class="control-label">Descriptiopn</label>
<div>
<textarea class="form-control-custom form-control" [(ngModel)]="issue.description" rows="5" type="text"></textarea>
</div>
</div>
<button type="button" [disabled]="submitted" class="btn btn-primary-outline" (click)="submit()">Submit</button>
</div>
</p-sidebar>

@ -0,0 +1,74 @@
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { IIssueCategory, IIssues, IssueStatus, RequestType } from "./../interfaces";
import { IssuesService, NotificationService } from "./../services";
@Component({
selector: "issue-report",
templateUrl: "issues-report.component.html",
})
export class IssuesReportComponent {
@Input() public visible: boolean;
@Input() public id: number; // RequestId
@Input() public title: string;
@Input() public issueCategory: IIssueCategory;
@Input() public movie: boolean;
@Input() public providerId: string;
@Output() public visibleChange = new EventEmitter<boolean>();
public submitted: boolean = false;
get getTitle(): string {
return this.title;
}
public issue: IIssues;
constructor(private issueService: IssuesService,
private notification: NotificationService) {
this.issue = {
subject: "",
description: "",
issueCategory: { value: "", id: 0 },
status: IssueStatus.Pending,
resolvedDate: undefined,
id: undefined,
issueCategoryId: 0,
comments: [],
requestId: undefined,
requestType: RequestType.movie,
title: "",
providerId: "",
userReported: undefined,
};
}
public submit() {
this.submitted = true;
const issue = this.issue;
issue.requestId = this.id;
issue.issueCategory = this.issueCategory;
issue.issueCategoryId = this.issueCategory.id;
issue.title = this.title;
issue.providerId = this.providerId;
if (this.movie) {
issue.requestType = RequestType.movie;
} else {
issue.requestType = RequestType.tvShow;
}
this.issueService.createIssue(issue).subscribe(x => {
if (x) {
this.notification.success("Issue Created");
}
});
}
public hide(): void {
this.submitted = false;
this.visible = !this.visible;
this.visibleChange.emit(this.visible);
}
}

@ -3,11 +3,25 @@ import { NgModule } from "@angular/core";
import { FormsModule } from "@angular/forms";
import { TranslateModule } from "@ngx-translate/core";
import { IssuesReportComponent } from "./issues-report.component";
import { SidebarModule } from "primeng/primeng";
@NgModule({
declarations: [
IssuesReportComponent,
],
imports: [
SidebarModule,
FormsModule,
CommonModule,
],
exports: [
TranslateModule,
CommonModule,
FormsModule,
SidebarModule,
IssuesReportComponent,
],
})
export class SharedModule {}

@ -65,7 +65,7 @@
<td>
{{u.lastLoggedIn | date: 'short'}}
</td>
<td ng-hide="hideColumns">
<td>
<span *ngIf="u.userType === 1">Local User</span>
<span *ngIf="u.userType === 2">Plex User</span>
<span *ngIf="u.userType === 3">Emby User</span>

@ -17,10 +17,10 @@ import { IdentityService } from "../services";
import { AuthGuard } from "../auth/auth.guard";
const routes: Routes = [
{ path: "usermanagement", component: UserManagementComponent, canActivate: [AuthGuard] },
{ path: "usermanagement/add", component: UserManagementAddComponent, canActivate: [AuthGuard] },
{ path: "usermanagement/edit/:id", component: UserManagementEditComponent, canActivate: [AuthGuard] },
{ path: "usermanagement/updatedetails", component: UpdateDetailsComponent, canActivate: [AuthGuard] },
{ path: "", component: UserManagementComponent, canActivate: [AuthGuard] },
{ path: "add", component: UserManagementAddComponent, canActivate: [AuthGuard] },
{ path: "edit/:id", component: UserManagementEditComponent, canActivate: [AuthGuard] },
{ path: "updatedetails", component: UpdateDetailsComponent, canActivate: [AuthGuard] },
];
@NgModule({

@ -16,13 +16,12 @@ import { PlexService } from "../services";
import { IdentityService } from "../services";
const routes: Routes = [
{ path: "Wizard", component: WelcomeComponent},
{ path: "Wizard/MediaServer", component: MediaServerComponent},
{ path: "Wizard/Plex", component: PlexComponent},
{ path: "Wizard/Emby", component: EmbyComponent},
{ path: "Wizard/CreateAdmin", component: CreateAdminComponent},
{ path: "", component: WelcomeComponent},
{ path: "MediaServer", component: MediaServerComponent},
{ path: "Plex", component: PlexComponent},
{ path: "Emby", component: EmbyComponent},
{ path: "CreateAdmin", component: CreateAdminComponent},
];
@NgModule({
imports: [
CommonModule,

@ -24,4 +24,3 @@ $bg-colour-disabled: #252424;
background: $primary-colour !important;
color: white;
}*/

@ -325,4 +325,13 @@ button.list-group-item:focus {
.ui-radiobutton, .ui-radiobutton-label{
vertical-align: baseline;
}
}
.side-back {
padding-top:5%;
box-shadow: 0px 0px 3.5em #000000;
}
.ui-widget-overlay .ui-sidebar-mask {
background: black;
}

@ -8,7 +8,7 @@ $info-colour: #5bc0de;
$warning-colour: #f0ad4e;
$danger-colour: #d9534f;
$success-colour: #5cb85c;
$i:!important;
$i: !important;
@media (min-width: 768px ) {
.row {
@ -312,6 +312,10 @@ $border-radius: 10px;
text-align: center;
}
.text-center {
text-align: center;
}
.no-search-results .no-search-results-icon {
font-size: 10em;
color: $form-color;
@ -850,4 +854,8 @@ a > h4:hover {
-webkit-text-stroke-width: 0.3px;
-webkit-text-stroke-color: black;
font-size: 0.9rem !important
}
.card {
padding-top:15px;
}

@ -18,7 +18,7 @@ namespace Ombi.Controllers.External
public class RadarrController : Controller
{
public RadarrController(IRadarrApi radarr, ISettingsService<RadarrSettings> settings,
IMemoryCache mem)
ICacheService mem)
{
RadarrApi = radarr;
RadarrSettings = settings;
@ -27,7 +27,7 @@ namespace Ombi.Controllers.External
private IRadarrApi RadarrApi { get; }
private ISettingsService<RadarrSettings> RadarrSettings { get; }
private IMemoryCache Cache { get; }
private ICacheService Cache { get; }
/// <summary>
/// Gets the Radarr profiles.
/// </summary>
@ -58,16 +58,15 @@ namespace Ombi.Controllers.External
[HttpGet("Profiles")]
public async Task<IEnumerable<RadarrProfile>> GetProfiles()
{
return await Cache.GetOrCreate(CacheKeys.RadarrQualityProfiles, async entry =>
return await Cache.GetOrAdd(CacheKeys.RadarrQualityProfiles, async () =>
{
entry.AbsoluteExpiration = DateTimeOffset.Now.AddHours(1);
var settings = await RadarrSettings.GetSettingsAsync();
if (settings.Enabled)
{
return await RadarrApi.GetProfiles(settings.ApiKey, settings.FullUri);
}
return null;
});
}, DateTime.Now.AddHours(1));
}
/// <summary>
@ -78,16 +77,15 @@ namespace Ombi.Controllers.External
[HttpGet("RootFolders")]
public async Task<IEnumerable<RadarrRootFolder>> GetRootFolders()
{
return await Cache.GetOrCreate(CacheKeys.RadarrRootProfiles, async entry =>
return await Cache.GetOrAdd(CacheKeys.RadarrRootProfiles, async () =>
{
entry.AbsoluteExpiration = DateTimeOffset.Now.AddHours(1);
var settings = await RadarrSettings.GetSettingsAsync();
if (settings.Enabled)
{
return await RadarrApi.GetRootFolders(settings.ApiKey, settings.FullUri);
}
return null;
});
}, DateTime.Now.AddHours(1));
}
}
}

@ -18,7 +18,7 @@ namespace Ombi.Controllers
public class ImagesController : Controller
{
public ImagesController(IFanartTvApi api, IApplicationConfigRepository config,
IOptions<LandingPageBackground> options, IMemoryCache c)
IOptions<LandingPageBackground> options, ICacheService c)
{
Api = api;
Config = config;
@ -29,16 +29,12 @@ namespace Ombi.Controllers
private IFanartTvApi Api { get; }
private IApplicationConfigRepository Config { get; }
private LandingPageBackground Options { get; }
private readonly IMemoryCache _cache;
private readonly ICacheService _cache;
[HttpGet("tv/{tvdbid}")]
public async Task<string> GetTvBanner(int tvdbid)
{
var key = await _cache.GetOrCreateAsync(CacheKeys.FanartTv, async entry =>
{
entry.SlidingExpiration = TimeSpan.FromDays(1);
return await Config.Get(Store.Entities.ConfigurationTypes.FanartTv);
});
var key = await _cache.GetOrAdd(CacheKeys.FanartTv, async () => await Config.Get(Store.Entities.ConfigurationTypes.FanartTv), DateTime.Now.AddDays(1));
var images = await Api.GetTvImages(tvdbid, key.Value);
if (images.tvbanner != null)
@ -62,11 +58,7 @@ namespace Ombi.Controllers
var movieUrl = string.Empty;
var tvUrl = string.Empty;
var key = await _cache.GetOrCreateAsync(CacheKeys.FanartTv, async entry =>
{
entry.SlidingExpiration = TimeSpan.FromDays(1);
return await Config.Get(Store.Entities.ConfigurationTypes.FanartTv);
});
var key = await _cache.GetOrAdd(CacheKeys.FanartTv, async () => await Config.Get(Store.Entities.ConfigurationTypes.FanartTv), DateTime.Now.AddDays(1));
if (moviesArray.Any())
{

@ -0,0 +1,219 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Ombi.Store.Entities.Requests;
using Ombi.Store.Repository;
using System.Collections.Generic;
using System.Linq;
using Hangfire;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Ombi.Attributes;
using Ombi.Core;
using Ombi.Core.Notifications;
using Ombi.Helpers;
using Ombi.Models;
using Ombi.Notifications.Models;
using Ombi.Store.Entities;
namespace Ombi.Controllers
{
[ApiV1]
[Authorize]
[Produces("application/json")]
public class IssuesController : Controller
{
public IssuesController(IRepository<IssueCategory> categories, IRepository<Issues> issues, IRepository<IssueComments> comments,
UserManager<OmbiUser> userManager, INotificationService notify)
{
_categories = categories;
_issues = issues;
_issueComments = comments;
_userManager = userManager;
_notification = notify;
}
private readonly IRepository<IssueCategory> _categories;
private readonly IRepository<Issues> _issues;
private readonly IRepository<IssueComments> _issueComments;
private readonly UserManager<OmbiUser> _userManager;
private readonly INotificationService _notification;
/// <summary>
/// Get's all categories
/// </summary>
/// <returns></returns>
[HttpGet("categories")]
public async Task<IEnumerable<IssueCategory>> Categories()
{
return await _categories.GetAll().ToListAsync();
}
/// <summary>
/// Creates a new category
/// </summary>
/// <param name="cat"></param>
/// <returns></returns>
[PowerUser]
[HttpPost("categories")]
public async Task<bool> CreateCategory([FromBody]IssueCategory cat)
{
var result = await _categories.Add(cat);
if (result.Id > 0)
{
return true;
}
return false;
}
/// <summary>
/// Deletes a Category
/// </summary>
/// <param name="catId"></param>
/// <returns></returns>
[PowerUser]
[HttpDelete("categories/{catId}")]
public async Task<bool> DeleteCategory([FromRoute]int catId)
{
var cat = await _categories.GetAll().FirstOrDefaultAsync(x => x.Id == catId);
await _categories.Delete(cat);
return true;
}
/// <summary>
/// Returns all the issues
/// </summary>
/// <returns></returns>
[HttpGet]
public async Task<IEnumerable<Issues>> GetIssues()
{
return await _issues.GetAll().Include(x => x.IssueCategory).ToListAsync();
}
/// <summary>
/// Returns all the issues
/// </summary>
/// <returns></returns>
[HttpGet("{take}/{skip}/{status}")]
public async Task<IEnumerable<Issues>> GetIssues(int take, int skip, IssueStatus status)
{
return await _issues
.GetAll()
.Where(x => x.Status == status)
.Include(x => x.IssueCategory)
.Include(x => x.UserReported)
.Skip(skip)
.Take(take)
.ToListAsync();
}
/// <summary>
/// Returns all the issues count
/// </summary>
/// <returns></returns>
[HttpGet("count")]
public async Task<IssueCountModel> GetIssueCount()
{
return new IssueCountModel
{
Pending = await _issues.GetAll().Where(x => x.Status == IssueStatus.Pending).CountAsync(),
InProgress = await _issues.GetAll().Where(x => x.Status == IssueStatus.InProgress).CountAsync(),
Resolved = await _issues.GetAll().Where(x => x.Status == IssueStatus.Resolved).CountAsync()
};
}
/// <summary>
/// Create Movie Issue
/// </summary>
[HttpPost]
public async Task<int> CreateIssue([FromBody]Issues i)
{
i.IssueCategory = null;
i.UserReportedId = (await _userManager.Users.FirstOrDefaultAsync(x => x.UserName == User.Identity.Name)).Id;
await _issues.Add(i);
var notificationModel = new NotificationOptions
{
RequestId = 0,
DateTime = DateTime.Now,
NotificationType = NotificationType.Issue,
RequestType = i.RequestType,
Recipient = string.Empty,
AdditionalInformation = $"{i.Subject} | {i.Description}"
};
BackgroundJob.Enqueue(() => _notification.Publish(notificationModel));
return i.Id;
}
/// <summary>
/// Returns the issue by Id
/// </summary>
[HttpGet("{id}")]
public async Task<Issues> GetIssue([FromRoute] int id)
{
return await _issues.GetAll().Where(x => x.Id == id)
.Include(x => x.IssueCategory)
.Include(x => x.UserReported)
.FirstOrDefaultAsync();
}
/// <summary>
/// Get's all the issue comments by id
/// </summary>
[HttpGet("{id}/comments")]
public async Task<IEnumerable<IssueCommentChatViewModel>> GetComments([FromRoute]int id)
{
var comment = await _issueComments.GetAll().Where(x => x.IssuesId == id).Include(x => x.User).ToListAsync();
var vm = new List<IssueCommentChatViewModel>();
foreach (var c in comment)
{
var roles = await _userManager.GetRolesAsync(c.User);
vm.Add(new IssueCommentChatViewModel
{
Comment = c.Comment,
Date = c.Date,
Username = c.User.UserAlias,
AdminComment = roles.Contains(OmbiRoles.PowerUser) || roles.Contains(OmbiRoles.Admin)
});
}
return vm;
}
/// <summary>
/// Adds a comment on an issue
/// </summary>
[HttpPost("comments")]
public async Task<IssueComments> AddComment([FromBody] NewIssueCommentViewModel comment)
{
var userId = await _userManager.Users.Where(x => User.Identity.Name == x.UserName).Select(x => x.Id)
.FirstOrDefaultAsync();
var newComment = new IssueComments
{
Comment = comment.Comment,
Date = DateTime.UtcNow,
UserId = userId,
IssuesId = comment.IssueId,
};
return await _issueComments.Add(newComment);
}
[HttpPost("status")]
public async Task<bool> UpdateStatus([FromBody] IssueStateViewModel model)
{
var issue = await _issues.Find(model.IssueId);
if (issue == null)
{
return false;
}
issue.Status = model.Status;
await _issues.SaveChangesAsync();
return true;
}
}
}

@ -19,7 +19,7 @@ namespace Ombi.Controllers
public class JobController : Controller
{
public JobController(IOmbiAutomaticUpdater updater, IPlexUserImporter userImporter,
IMemoryCache mem, IEmbyUserImporter embyImporter, IPlexContentSync plexContentSync,
ICacheService mem, IEmbyUserImporter embyImporter, IPlexContentSync plexContentSync,
IEmbyContentSync embyContentSync)
{
_updater = updater;
@ -33,7 +33,7 @@ namespace Ombi.Controllers
private readonly IOmbiAutomaticUpdater _updater;
private readonly IPlexUserImporter _plexUserImporter;
private readonly IEmbyUserImporter _embyUserImporter;
private readonly IMemoryCache _memCache;
private readonly ICacheService _memCache;
private readonly IPlexContentSync _plexContentSync;
private readonly IEmbyContentSync _embyContentSync;
@ -74,9 +74,8 @@ namespace Ombi.Controllers
[HttpGet("updateCached")]
public async Task<bool> CheckForUpdateCached()
{
var val = await _memCache.GetOrCreateAsync(CacheKeys.Update, async entry =>
var val = await _memCache.GetOrAdd(CacheKeys.Update, async () =>
{
entry.AbsoluteExpiration = DateTimeOffset.Now.AddHours(1);
var productArray = _updater.GetVersion();
var version = productArray[0];
var branch = productArray[1];

@ -56,7 +56,7 @@ namespace Ombi.Controllers
INotificationTemplatesRepository templateRepo,
IEmbyApi embyApi,
IRadarrSync radarrSync,
IMemoryCache memCache,
ICacheService memCache,
IGithubApi githubApi)
{
SettingsResolver = resolver;
@ -73,7 +73,7 @@ namespace Ombi.Controllers
private INotificationTemplatesRepository TemplateRepository { get; }
private readonly IEmbyApi _embyApi;
private readonly IRadarrSync _radarrSync;
private readonly IMemoryCache _cache;
private readonly ICacheService _cache;
private readonly IGithubApi _githubApi;
/// <summary>
@ -479,6 +479,36 @@ namespace Ombi.Controllers
return await Save(settings);
}
/// <summary>
/// Save the Issues settings.
/// </summary>
/// <param name="settings">The settings.</param>
/// <returns></returns>
[HttpPost("Issues")]
public async Task<bool> IssueSettings([FromBody]IssueSettings settings)
{
return await Save(settings);
}
/// <summary>
/// Gets the Issues Settings.
/// </summary>
/// <returns></returns>
[HttpGet("Issues")]
public async Task<IssueSettings> IssueSettings()
{
return await Get<IssueSettings>();
}
[AllowAnonymous]
[HttpGet("issuesenabled")]
public async Task<bool> IssuesEnabled()
{
var issues = await Get<IssueSettings>();
return issues.Enabled;
}
/// <summary>
/// Saves the email notification settings.
/// </summary>

@ -33,8 +33,15 @@ namespace Ombi
//if (exception is NotFoundException) code = HttpStatusCode.NotFound;
if (exception is UnauthorizedAccessException) code = HttpStatusCode.Unauthorized;
var result = JsonConvert.SerializeObject(new { error = exception.Message });
string result;
if (exception.InnerException != null)
{
result = JsonConvert.SerializeObject(new { error = exception.InnerException.Message });
}
else
{
result = JsonConvert.SerializeObject(new { error = exception.Message });
}
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)code;
return context.Response.WriteAsync(result);

@ -0,0 +1,12 @@
using System;
namespace Ombi.Models
{
public class IssueCommentChatViewModel
{
public string Comment { get; set; }
public DateTime Date { get; set; }
public string Username { get; set; }
public bool AdminComment { get; set; }
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save