Merge pull request #2067 from tidusjar/develop

Develop into dynamic-webpack-base
This commit is contained in:
Jamie 2018-03-13 08:41:48 +00:00 committed by GitHub
commit 1b8956e471
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 672 additions and 328 deletions

View file

@ -1,6 +1,6 @@
# Changelog # Changelog
## (unreleased) ## v3.0.3000 (2018-03-09)
### **New Features** ### **New Features**
@ -8,6 +8,12 @@
- Added Pending Approval into the filters list. [tidusjar] - Added Pending Approval into the filters list. [tidusjar]
- Added the ability to hide requests that have not been made by that user (#2052) [Jamie]
- Update README.md. [Jamie]
- Update README.md. [Louis Laureys]
### **Fixes** ### **Fixes**
- Fixed #2042. [Jamie] - Fixed #2042. [Jamie]

View file

@ -14,10 +14,11 @@ ___
[![Report a bug](http://i.imgur.com/xSpw482.png)](https://forums.ombi.io/viewforum.php?f=10) [![Feature request](http://i.imgur.com/mFO0OuX.png)](https://forums.ombi.io/posting.php?mode=post&f=20) [![Report a bug](http://i.imgur.com/xSpw482.png)](https://forums.ombi.io/viewforum.php?f=10) [![Feature request](http://i.imgur.com/mFO0OuX.png)](https://forums.ombi.io/posting.php?mode=post&f=20)
| Service | V3 | Beta |
| Service | Stable | Develop |
|----------|:---------------------------:|:----------------------------:| |----------|:---------------------------:|:----------------------------:|
| AppVeyor | [![Build status](https://ci.appveyor.com/api/projects/status/hgj8j6lcea7j0yhn/branch/master?svg=true)](https://ci.appveyor.com/project/tidusjar/requestplex/branch/master) | [![Build status](https://ci.appveyor.com/api/projects/status/hgj8j6lcea7j0yhn/branch/DotNetCore?svg=true)](https://ci.appveyor.com/project/tidusjar/requestplex/branch/DotNetCore) | | AppVeyor | [![Build status](https://ci.appveyor.com/api/projects/status/hgj8j6lcea7j0yhn/branch/master?svg=true)](https://ci.appveyor.com/project/tidusjar/requestplex/branch/master) | [![Build status](https://ci.appveyor.com/api/projects/status/hgj8j6lcea7j0yhn/branch/develop?svg=true)](https://ci.appveyor.com/project/tidusjar/requestplex/branch/develop) |
| Download |[![Download](http://i.imgur.com/odToka3.png)](https://github.com/tidusjar/Ombi/releases) | [![Download](http://i.imgur.com/odToka3.png)](https://ci.appveyor.com/project/tidusjar/requestplex/branch/DotNetCore/artifacts) | | Download |[![Download](http://i.imgur.com/odToka3.png)](https://github.com/tidusjar/Ombi/releases) | [![Download](http://i.imgur.com/odToka3.png)](https://ci.appveyor.com/project/tidusjar/requestplex/branch/develop/artifacts) |
# Features # Features
Here are some of the features Ombi V3 has: Here are some of the features Ombi V3 has:
* Now working without crashes on Linux. * Now working without crashes on Linux.
@ -91,7 +92,7 @@ Search the existing requests to see if your suggestion has already been submitte
# Installation # Installation
[Click Here](https://github.com/tidusjar/Ombi/wiki/Installation) [Installation Guide](https://github.com/tidusjar/Ombi/wiki/Installation)
[Here for Reverse Proxy Config Examples](https://github.com/tidusjar/Ombi/wiki/Reverse-Proxy-Examples) [Here for Reverse Proxy Config Examples](https://github.com/tidusjar/Ombi/wiki/Reverse-Proxy-Examples)
[PlexGuide.com - Ombi Deployment & 101 Demonstration!](https://www.youtube.com/watch?v=QPNlqqkjNJw&feature=youtu.be) [PlexGuide.com - Ombi Deployment & 101 Demonstration!](https://www.youtube.com/watch?v=QPNlqqkjNJw&feature=youtu.be)

View file

@ -23,7 +23,7 @@ namespace Ombi.Api.Plex.Models
public int leafCount { get; set; } public int leafCount { get; set; }
public int viewedLeafCount { get; set; } public int viewedLeafCount { get; set; }
public int childCount { get; set; } public int childCount { get; set; }
public int addedAt { get; set; } public long addedAt { get; set; }
public int updatedAt { get; set; } public int updatedAt { get; set; }
public Genre[] Genre { get; set; } public Genre[] Genre { get; set; }
//public Role[] Role { get; set; } //public Role[] Role { get; set; }

View file

@ -14,6 +14,8 @@ using Ombi.Store.Repository.Requests;
using Ombi.Store.Entities; using Ombi.Store.Entities;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Ombi.Core.Authentication; using Ombi.Core.Authentication;
using Ombi.Core.Settings;
using Ombi.Settings.Settings.Models;
namespace Ombi.Core.Engine namespace Ombi.Core.Engine
{ {
@ -24,14 +26,18 @@ namespace Ombi.Core.Engine
private Dictionary<int, TvRequests> _dbTv; private Dictionary<int, TvRequests> _dbTv;
protected BaseMediaEngine(IPrincipal identity, IRequestServiceMain requestService, protected BaseMediaEngine(IPrincipal identity, IRequestServiceMain requestService,
IRuleEvaluator rules, OmbiUserManager um) : base(identity, um, rules) IRuleEvaluator rules, OmbiUserManager um, ICacheService cache, ISettingsService<OmbiSettings> ombiSettings) : base(identity, um, rules)
{ {
RequestService = requestService; RequestService = requestService;
Cache = cache;
OmbiSettings = ombiSettings;
} }
protected IRequestServiceMain RequestService { get; } protected IRequestServiceMain RequestService { get; }
protected IMovieRequestRepository MovieRepository => RequestService.MovieRequestService; protected IMovieRequestRepository MovieRepository => RequestService.MovieRequestService;
protected ITvRequestRepository TvRepository => RequestService.TvRequestService; protected ITvRequestRepository TvRepository => RequestService.TvRequestService;
protected readonly ICacheService Cache;
protected readonly ISettingsService<OmbiSettings> OmbiSettings;
protected async Task<Dictionary<int, MovieRequests>> GetMovieRequests() protected async Task<Dictionary<int, MovieRequests>> GetMovieRequests()
{ {
@ -99,5 +105,30 @@ namespace Ombi.Core.Engine
Pending = pendingMovies + pendingTv Pending = pendingMovies + pendingTv
}; };
} }
protected async Task<HideResult> HideFromOtherUsers()
{
if (await IsInRole(OmbiRoles.Admin) || await IsInRole(OmbiRoles.PowerUser))
{
return new HideResult();
}
var settings = await Cache.GetOrAdd(CacheKeys.OmbiSettings, async () => await OmbiSettings.GetSettingsAsync());
var result = new HideResult
{
Hide = settings.HideRequestsUsers
};
if (settings.HideRequestsUsers)
{
var user = await GetUser();
result.UserId = user.Id;
}
return result;
}
public class HideResult
{
public bool Hide { get; set; }
public string UserId { get; set; }
}
} }
} }

View file

@ -1,4 +1,5 @@
using Ombi.Core.Rule; using System;
using Ombi.Core.Rule;
using System.Collections.Generic; using System.Collections.Generic;
using System.Security.Principal; using System.Security.Principal;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -10,6 +11,7 @@ using Microsoft.AspNetCore.Identity;
using System.Linq; using System.Linq;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Ombi.Core.Authentication; using Ombi.Core.Authentication;
using Ombi.Helpers;
namespace Ombi.Core.Engine.Interfaces namespace Ombi.Core.Engine.Interfaces
{ {
@ -30,6 +32,13 @@ namespace Ombi.Core.Engine.Interfaces
private OmbiUser _user; private OmbiUser _user;
protected async Task<OmbiUser> GetUser() protected async Task<OmbiUser> GetUser()
{ {
if (IsApiUser)
{
return new OmbiUser
{
UserName = Username,
};
}
return _user ?? (_user = await UserManager.Users.FirstOrDefaultAsync(x => x.UserName == Username)); return _user ?? (_user = await UserManager.Users.FirstOrDefaultAsync(x => x.UserName == Username));
} }
@ -40,6 +49,10 @@ namespace Ombi.Core.Engine.Interfaces
protected async Task<bool> IsInRole(string roleName) protected async Task<bool> IsInRole(string roleName)
{ {
if (IsApiUser && roleName != OmbiRoles.Disabled)
{
return true;
}
return await UserManager.IsInRoleAsync(await GetUser(), roleName); return await UserManager.IsInRoleAsync(await GetUser(), roleName);
} }
@ -59,5 +72,7 @@ namespace Ombi.Core.Engine.Interfaces
var ruleResults = await Rules.StartSpecificRules(model, rule); var ruleResults = await Rules.StartSpecificRules(model, rule);
return ruleResults; return ruleResults;
} }
private bool IsApiUser => Username.Equals("Api", StringComparison.CurrentCultureIgnoreCase);
} }
} }

View file

@ -17,6 +17,6 @@ namespace Ombi.Core.Engine.Interfaces
Task<RequestEngineResult> ApproveMovie(MovieRequests request); Task<RequestEngineResult> ApproveMovie(MovieRequests request);
Task<RequestEngineResult> ApproveMovieById(int requestId); Task<RequestEngineResult> ApproveMovieById(int requestId);
Task<RequestEngineResult> DenyMovieById(int modelId); Task<RequestEngineResult> DenyMovieById(int modelId);
IEnumerable<MovieRequests> Filter(FilterViewModel vm); Task<IEnumerable<MovieRequests>> Filter(FilterViewModel vm);
} }
} }

View file

@ -1,6 +1,5 @@
using Ombi.Api.TheMovieDb; using Ombi.Api.TheMovieDb;
using Ombi.Core.Models.Requests; using Ombi.Core.Models.Requests;
using Ombi.Core.Models.Search;
using Ombi.Helpers; using Ombi.Helpers;
using Ombi.Store.Entities; using Ombi.Store.Entities;
using System; using System;
@ -15,6 +14,8 @@ using Ombi.Api.TheMovieDb.Models;
using Ombi.Core.Authentication; using Ombi.Core.Authentication;
using Ombi.Core.Engine.Interfaces; using Ombi.Core.Engine.Interfaces;
using Ombi.Core.Rule.Interfaces; using Ombi.Core.Rule.Interfaces;
using Ombi.Core.Settings;
using Ombi.Settings.Settings.Models;
using Ombi.Store.Entities.Requests; using Ombi.Store.Entities.Requests;
using Ombi.Store.Repository; using Ombi.Store.Repository;
@ -24,7 +25,7 @@ namespace Ombi.Core.Engine
{ {
public MovieRequestEngine(IMovieDbApi movieApi, IRequestServiceMain requestService, IPrincipal user, public MovieRequestEngine(IMovieDbApi movieApi, IRequestServiceMain requestService, IPrincipal user,
INotificationHelper helper, IRuleEvaluator r, IMovieSender sender, ILogger<MovieRequestEngine> log, INotificationHelper helper, IRuleEvaluator r, IMovieSender sender, ILogger<MovieRequestEngine> log,
OmbiUserManager manager, IRepository<RequestLog> rl) : base(user, requestService, r, manager) OmbiUserManager manager, IRepository<RequestLog> rl, ICacheService cache, ISettingsService<OmbiSettings> ombiSettings) : base(user, requestService, r, manager, cache, ombiSettings)
{ {
MovieApi = movieApi; MovieApi = movieApi;
NotificationHelper = helper; NotificationHelper = helper;
@ -126,7 +127,16 @@ namespace Ombi.Core.Engine
/// <returns></returns> /// <returns></returns>
public async Task<IEnumerable<MovieRequests>> GetRequests(int count, int position) public async Task<IEnumerable<MovieRequests>> GetRequests(int count, int position)
{ {
var allRequests = await MovieRepository.GetWithUser().Skip(position).Take(count).ToListAsync(); var shouldHide = await HideFromOtherUsers();
List<MovieRequests> allRequests;
if (shouldHide.Hide)
{
allRequests = await MovieRepository.GetWithUser(shouldHide.UserId).Skip(position).Take(count).ToListAsync();
}
else
{
allRequests = await MovieRepository.GetWithUser().Skip(position).Take(count).ToListAsync();
}
allRequests.ForEach(x => allRequests.ForEach(x =>
{ {
x.PosterPath = PosterPathHelper.FixPosterPath(x.PosterPath); x.PosterPath = PosterPathHelper.FixPosterPath(x.PosterPath);
@ -140,7 +150,16 @@ namespace Ombi.Core.Engine
/// <returns></returns> /// <returns></returns>
public async Task<IEnumerable<MovieRequests>> GetRequests() public async Task<IEnumerable<MovieRequests>> GetRequests()
{ {
var allRequests = await MovieRepository.GetWithUser().ToListAsync(); var shouldHide = await HideFromOtherUsers();
List<MovieRequests> allRequests;
if (shouldHide.Hide)
{
allRequests = await MovieRepository.GetWithUser(shouldHide.UserId).ToListAsync();
}
else
{
allRequests = await MovieRepository.GetWithUser().ToListAsync();
}
return allRequests; return allRequests;
} }
@ -151,7 +170,16 @@ namespace Ombi.Core.Engine
/// <returns></returns> /// <returns></returns>
public async Task<IEnumerable<MovieRequests>> SearchMovieRequest(string search) public async Task<IEnumerable<MovieRequests>> SearchMovieRequest(string search)
{ {
var allRequests = await MovieRepository.GetWithUser().ToListAsync(); var shouldHide = await HideFromOtherUsers();
List<MovieRequests> allRequests;
if (shouldHide.Hide)
{
allRequests = await MovieRepository.GetWithUser(shouldHide.UserId).ToListAsync();
}
else
{
allRequests = await MovieRepository.GetWithUser().ToListAsync();
}
var results = allRequests.Where(x => x.Title.Contains(search, CompareOptions.IgnoreCase)).ToList(); var results = allRequests.Where(x => x.Title.Contains(search, CompareOptions.IgnoreCase)).ToList();
results.ForEach(x => results.ForEach(x =>
{ {
@ -339,9 +367,10 @@ namespace Ombi.Core.Engine
return new RequestEngineResult { Result = true, Message = $"{movieName} has been successfully added!" }; return new RequestEngineResult { Result = true, Message = $"{movieName} has been successfully added!" };
} }
public IEnumerable<MovieRequests> Filter(FilterViewModel vm) public async Task<IEnumerable<MovieRequests>> Filter(FilterViewModel vm)
{ {
var requests = MovieRepository.GetWithUser(); var shouldHide = await HideFromOtherUsers();
var requests = shouldHide.Hide ? MovieRepository.GetWithUser(shouldHide.UserId) : MovieRepository.GetWithUser();
switch (vm.AvailabilityFilter) switch (vm.AvailabilityFilter)
{ {
case FilterType.None: case FilterType.None:

View file

@ -12,26 +12,26 @@ using System.Threading.Tasks;
using Ombi.Core.Rule.Interfaces; using Ombi.Core.Rule.Interfaces;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Ombi.Core.Authentication; using Ombi.Core.Authentication;
using Ombi.Core.Settings;
using Ombi.Helpers; using Ombi.Helpers;
using Ombi.Settings.Settings.Models;
namespace Ombi.Core.Engine namespace Ombi.Core.Engine
{ {
public class MovieSearchEngine : BaseMediaEngine, IMovieEngine public class MovieSearchEngine : BaseMediaEngine, IMovieEngine
{ {
public MovieSearchEngine(IPrincipal identity, IRequestServiceMain service, IMovieDbApi movApi, IMapper mapper, public MovieSearchEngine(IPrincipal identity, IRequestServiceMain service, IMovieDbApi movApi, IMapper mapper,
ILogger<MovieSearchEngine> logger, IRuleEvaluator r, OmbiUserManager um, ICacheService mem) ILogger<MovieSearchEngine> logger, IRuleEvaluator r, OmbiUserManager um, ICacheService mem, ISettingsService<OmbiSettings> s)
: base(identity, service, r, um) : base(identity, service, r, um, mem, s)
{ {
MovieApi = movApi; MovieApi = movApi;
Mapper = mapper; Mapper = mapper;
Logger = logger; Logger = logger;
MemCache = mem;
} }
private IMovieDbApi MovieApi { get; } private IMovieDbApi MovieApi { get; }
private IMapper Mapper { get; } private IMapper Mapper { get; }
private ILogger<MovieSearchEngine> Logger { get; } private ILogger<MovieSearchEngine> Logger { get; }
private ICacheService MemCache { get; }
/// <summary> /// <summary>
/// Lookups the imdb information. /// Lookups the imdb information.
@ -85,7 +85,7 @@ namespace Ombi.Core.Engine
/// <returns></returns> /// <returns></returns>
public async Task<IEnumerable<SearchMovieViewModel>> PopularMovies() public async Task<IEnumerable<SearchMovieViewModel>> PopularMovies()
{ {
var result = await MemCache.GetOrAdd(CacheKeys.PopularMovies, async () => await MovieApi.PopularMovies(), DateTime.Now.AddHours(12)); var result = await Cache.GetOrAdd(CacheKeys.PopularMovies, async () => await MovieApi.PopularMovies(), DateTime.Now.AddHours(12));
if (result != null) if (result != null)
{ {
Logger.LogDebug("Search Result: {result}", result); Logger.LogDebug("Search Result: {result}", result);
@ -100,7 +100,7 @@ namespace Ombi.Core.Engine
/// <returns></returns> /// <returns></returns>
public async Task<IEnumerable<SearchMovieViewModel>> TopRatedMovies() public async Task<IEnumerable<SearchMovieViewModel>> TopRatedMovies()
{ {
var result = await MemCache.GetOrAdd(CacheKeys.TopRatedMovies, async () => await MovieApi.TopRated(), DateTime.Now.AddHours(12)); var result = await Cache.GetOrAdd(CacheKeys.TopRatedMovies, async () => await MovieApi.TopRated(), DateTime.Now.AddHours(12));
if (result != null) if (result != null)
{ {
Logger.LogDebug("Search Result: {result}", result); Logger.LogDebug("Search Result: {result}", result);
@ -115,7 +115,7 @@ namespace Ombi.Core.Engine
/// <returns></returns> /// <returns></returns>
public async Task<IEnumerable<SearchMovieViewModel>> UpcomingMovies() public async Task<IEnumerable<SearchMovieViewModel>> UpcomingMovies()
{ {
var result = await MemCache.GetOrAdd(CacheKeys.UpcomingMovies, async () => await MovieApi.Upcoming(), DateTime.Now.AddHours(12)); var result = await Cache.GetOrAdd(CacheKeys.UpcomingMovies, async () => await MovieApi.Upcoming(), DateTime.Now.AddHours(12));
if (result != null) if (result != null)
{ {
Logger.LogDebug("Search Result: {result}", result); Logger.LogDebug("Search Result: {result}", result);
@ -130,7 +130,7 @@ namespace Ombi.Core.Engine
/// <returns></returns> /// <returns></returns>
public async Task<IEnumerable<SearchMovieViewModel>> NowPlayingMovies() public async Task<IEnumerable<SearchMovieViewModel>> NowPlayingMovies()
{ {
var result = await MemCache.GetOrAdd(CacheKeys.NowPlayingMovies, async () => await MovieApi.NowPlaying(), DateTime.Now.AddHours(12)); var result = await Cache.GetOrAdd(CacheKeys.NowPlayingMovies, async () => await MovieApi.NowPlaying(), DateTime.Now.AddHours(12));
if (result != null) if (result != null)
{ {
Logger.LogDebug("Search Result: {result}", result); Logger.LogDebug("Search Result: {result}", result);

View file

@ -17,6 +17,8 @@ using Ombi.Core.Helpers;
using Ombi.Core.Rule; using Ombi.Core.Rule;
using Ombi.Core.Rule.Interfaces; using Ombi.Core.Rule.Interfaces;
using Ombi.Core.Senders; using Ombi.Core.Senders;
using Ombi.Core.Settings;
using Ombi.Settings.Settings.Models;
using Ombi.Store.Entities.Requests; using Ombi.Store.Entities.Requests;
using Ombi.Store.Repository; using Ombi.Store.Repository;
@ -26,7 +28,7 @@ namespace Ombi.Core.Engine
{ {
public TvRequestEngine(ITvMazeApi tvApi, IRequestServiceMain requestService, IPrincipal user, public TvRequestEngine(ITvMazeApi tvApi, IRequestServiceMain requestService, IPrincipal user,
INotificationHelper helper, IRuleEvaluator rule, OmbiUserManager manager, INotificationHelper helper, IRuleEvaluator rule, OmbiUserManager manager,
ITvSender sender, IAuditRepository audit, IRepository<RequestLog> rl) : base(user, requestService, rule, manager) ITvSender sender, IAuditRepository audit, IRepository<RequestLog> rl, ISettingsService<OmbiSettings> settings, ICacheService cache) : base(user, requestService, rule, manager, cache, settings)
{ {
TvApi = tvApi; TvApi = tvApi;
NotificationHelper = helper; NotificationHelper = helper;
@ -128,45 +130,136 @@ namespace Ombi.Core.Engine
public async Task<IEnumerable<TvRequests>> GetRequests(int count, int position) public async Task<IEnumerable<TvRequests>> GetRequests(int count, int position)
{ {
var allRequests = await TvRepository.Get() var shouldHide = await HideFromOtherUsers();
List<TvRequests> allRequests;
if (shouldHide.Hide)
{
allRequests = await TvRepository.Get(shouldHide.UserId)
.Include(x => x.ChildRequests) .Include(x => x.ChildRequests)
.ThenInclude(x => x.SeasonRequests) .ThenInclude(x => x.SeasonRequests)
.ThenInclude(x => x.Episodes) .ThenInclude(x => x.Episodes)
.Skip(position).Take(count).ToListAsync(); .Skip(position).Take(count).ToListAsync();
// Filter out children
FilterChildren(allRequests, shouldHide);
}
else
{
allRequests = await TvRepository.Get()
.Include(x => x.ChildRequests)
.ThenInclude(x => x.SeasonRequests)
.ThenInclude(x => x.Episodes)
.Skip(position).Take(count).ToListAsync();
}
return allRequests; return allRequests;
} }
public async Task<IEnumerable<TreeNode<TvRequests, List<ChildRequests>>>> GetRequestsTreeNode(int count, int position) public async Task<IEnumerable<TreeNode<TvRequests, List<ChildRequests>>>> GetRequestsTreeNode(int count, int position)
{ {
var allRequests = await TvRepository.Get() var shouldHide = await HideFromOtherUsers();
List<TvRequests> allRequests;
if (shouldHide.Hide)
{
allRequests = await TvRepository.Get(shouldHide.UserId)
.Include(x => x.ChildRequests) .Include(x => x.ChildRequests)
.ThenInclude(x => x.SeasonRequests) .ThenInclude(x => x.SeasonRequests)
.ThenInclude(x=>x.Episodes) .ThenInclude(x => x.Episodes)
.Skip(position).Take(count).ToListAsync(); .Skip(position).Take(count).ToListAsync();
FilterChildren(allRequests, shouldHide);
}
else
{
allRequests = await TvRepository.Get()
.Include(x => x.ChildRequests)
.ThenInclude(x => x.SeasonRequests)
.ThenInclude(x => x.Episodes)
.Skip(position).Take(count).ToListAsync();
}
return ParseIntoTreeNode(allRequests); return ParseIntoTreeNode(allRequests);
} }
public async Task<IEnumerable<TvRequests>> GetRequests() public async Task<IEnumerable<TvRequests>> GetRequests()
{ {
var allRequests = TvRepository.Get(); var shouldHide = await HideFromOtherUsers();
IQueryable<TvRequests> allRequests;
if (shouldHide.Hide)
{
allRequests = TvRepository.Get(shouldHide.UserId);
FilterChildren(allRequests, shouldHide);
}
else
{
allRequests = TvRepository.Get();
}
return await allRequests.ToListAsync(); return await allRequests.ToListAsync();
} }
private static void FilterChildren(IEnumerable<TvRequests> allRequests, HideResult shouldHide)
{
// Filter out children
foreach (var t in allRequests)
{
for (var j = 0; j < t.ChildRequests.Count; j++)
{
var child = t.ChildRequests[j];
if (child.RequestedUserId != shouldHide.UserId)
{
t.ChildRequests.RemoveAt(j);
j--;
}
}
}
}
public async Task<IEnumerable<ChildRequests>> GetAllChldren(int tvId) public async Task<IEnumerable<ChildRequests>> GetAllChldren(int tvId)
{ {
return await TvRepository.GetChild().Include(x => x.SeasonRequests).Where(x => x.ParentRequestId == tvId).ToListAsync(); var shouldHide = await HideFromOtherUsers();
List<ChildRequests> allRequests;
if (shouldHide.Hide)
{
allRequests = await TvRepository.GetChild(shouldHide.UserId).Include(x => x.SeasonRequests).Where(x => x.ParentRequestId == tvId).ToListAsync();
}
else
{
allRequests = await TvRepository.GetChild().Include(x => x.SeasonRequests).Where(x => x.ParentRequestId == tvId).ToListAsync();
}
return allRequests;
} }
public async Task<IEnumerable<TvRequests>> SearchTvRequest(string search) public async Task<IEnumerable<TvRequests>> SearchTvRequest(string search)
{ {
var allRequests = TvRepository.Get(); var shouldHide = await HideFromOtherUsers();
IQueryable<TvRequests> allRequests;
if (shouldHide.Hide)
{
allRequests = TvRepository.Get(shouldHide.UserId);
}
else
{
allRequests = TvRepository.Get();
}
var results = await allRequests.Where(x => x.Title.Contains(search, CompareOptions.IgnoreCase)).ToListAsync(); var results = await allRequests.Where(x => x.Title.Contains(search, CompareOptions.IgnoreCase)).ToListAsync();
return results; return results;
} }
public async Task<IEnumerable<TreeNode<TvRequests, List<ChildRequests>>>> SearchTvRequestTree(string search) public async Task<IEnumerable<TreeNode<TvRequests, List<ChildRequests>>>> SearchTvRequestTree(string search)
{ {
var allRequests = TvRepository.Get(); var shouldHide = await HideFromOtherUsers();
IQueryable<TvRequests> allRequests;
if (shouldHide.Hide)
{
allRequests = TvRepository.Get(shouldHide.UserId);
}
else
{
allRequests = TvRepository.Get();
}
var results = await allRequests.Where(x => x.Title.Contains(search, CompareOptions.IgnoreCase)).ToListAsync(); var results = await allRequests.Where(x => x.Title.Contains(search, CompareOptions.IgnoreCase)).ToListAsync();
return ParseIntoTreeNode(results); return ParseIntoTreeNode(results);
} }
@ -402,7 +495,7 @@ namespace Ombi.Core.Engine
var result = await TvSender.Send(model); var result = await TvSender.Send(model);
if (result.Success) if (result.Success)
{ {
return new RequestEngineResult {Result = true}; return new RequestEngineResult { Result = true };
} }
return new RequestEngineResult return new RequestEngineResult
{ {
@ -418,7 +511,7 @@ namespace Ombi.Core.Engine
RequestType = RequestType.TvShow, RequestType = RequestType.TvShow,
}); });
return new RequestEngineResult {Result = true}; return new RequestEngineResult { Result = true };
} }
} }
} }

View file

@ -19,6 +19,7 @@ using Ombi.Store.Repository.Requests;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Ombi.Core.Authentication; using Ombi.Core.Authentication;
using Ombi.Helpers; using Ombi.Helpers;
using Ombi.Settings.Settings.Models;
namespace Ombi.Core.Engine namespace Ombi.Core.Engine
{ {
@ -26,8 +27,8 @@ namespace Ombi.Core.Engine
{ {
public TvSearchEngine(IPrincipal identity, IRequestServiceMain service, ITvMazeApi tvMaze, IMapper mapper, ISettingsService<PlexSettings> plexSettings, 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, ISettingsService<EmbySettings> embySettings, IPlexContentRepository repo, IEmbyContentRepository embyRepo, ITraktApi trakt, IRuleEvaluator r, OmbiUserManager um,
ICacheService memCache) ICacheService memCache, ISettingsService<OmbiSettings> s)
: base(identity, service, r, um) : base(identity, service, r, um, memCache, s)
{ {
TvMazeApi = tvMaze; TvMazeApi = tvMaze;
Mapper = mapper; Mapper = mapper;
@ -36,7 +37,6 @@ namespace Ombi.Core.Engine
PlexContentRepo = repo; PlexContentRepo = repo;
TraktApi = trakt; TraktApi = trakt;
EmbyContentRepo = embyRepo; EmbyContentRepo = embyRepo;
MemCache = memCache;
} }
private ITvMazeApi TvMazeApi { get; } private ITvMazeApi TvMazeApi { get; }
@ -46,7 +46,6 @@ namespace Ombi.Core.Engine
private IPlexContentRepository PlexContentRepo { get; } private IPlexContentRepository PlexContentRepo { get; }
private IEmbyContentRepository EmbyContentRepo { get; } private IEmbyContentRepository EmbyContentRepo { get; }
private ITraktApi TraktApi { get; } private ITraktApi TraktApi { get; }
private ICacheService MemCache { get; }
public async Task<IEnumerable<SearchTvShowViewModel>> Search(string searchTerm) public async Task<IEnumerable<SearchTvShowViewModel>> Search(string searchTerm)
{ {
@ -124,54 +123,55 @@ namespace Ombi.Core.Engine
public async Task<IEnumerable<TreeNode<SearchTvShowViewModel>>> PopularTree() public async Task<IEnumerable<TreeNode<SearchTvShowViewModel>>> PopularTree()
{ {
var result = await MemCache.GetOrAdd(CacheKeys.PopularTv, async () => await TraktApi.GetPopularShows(), DateTime.Now.AddHours(12)); var result = await Cache.GetOrAdd(CacheKeys.PopularTv, async () => await TraktApi.GetPopularShows(), DateTime.Now.AddHours(12));
var processed = await ProcessResults(result); var processed = await ProcessResults(result);
return processed.Select(ParseIntoTreeNode).ToList(); return processed.Select(ParseIntoTreeNode).ToList();
} }
public async Task<IEnumerable<SearchTvShowViewModel>> Popular() public async Task<IEnumerable<SearchTvShowViewModel>> Popular()
{ {
var result = await MemCache.GetOrAdd(CacheKeys.PopularTv, async () => await TraktApi.GetPopularShows(), DateTime.Now.AddHours(12)); var result = await Cache.GetOrAdd(CacheKeys.PopularTv, async () => await TraktApi.GetPopularShows(), DateTime.Now.AddHours(12));
var processed = await ProcessResults(result); var processed = await ProcessResults(result);
return processed; return processed;
} }
public async Task<IEnumerable<TreeNode<SearchTvShowViewModel>>> AnticipatedTree() public async Task<IEnumerable<TreeNode<SearchTvShowViewModel>>> AnticipatedTree()
{ {
var result = await MemCache.GetOrAdd(CacheKeys.AnticipatedTv, async () => await TraktApi.GetAnticipatedShows(), DateTime.Now.AddHours(12)); var result = await Cache.GetOrAdd(CacheKeys.AnticipatedTv, async () => await TraktApi.GetAnticipatedShows(), DateTime.Now.AddHours(12));
var processed = await ProcessResults(result); var processed = await ProcessResults(result);
return processed.Select(ParseIntoTreeNode).ToList(); return processed.Select(ParseIntoTreeNode).ToList();
} }
public async Task<IEnumerable<SearchTvShowViewModel>> Anticipated() public async Task<IEnumerable<SearchTvShowViewModel>> Anticipated()
{ {
var result = await MemCache.GetOrAdd(CacheKeys.AnticipatedTv, async () => await TraktApi.GetAnticipatedShows(), DateTime.Now.AddHours(12));
var result = await Cache.GetOrAdd(CacheKeys.AnticipatedTv, async () => await TraktApi.GetAnticipatedShows(), DateTime.Now.AddHours(12));
var processed = await ProcessResults(result); var processed = await ProcessResults(result);
return processed; return processed;
} }
public async Task<IEnumerable<TreeNode<SearchTvShowViewModel>>> MostWatchesTree() public async Task<IEnumerable<TreeNode<SearchTvShowViewModel>>> MostWatchesTree()
{ {
var result = await MemCache.GetOrAdd(CacheKeys.MostWatchesTv, async () => await TraktApi.GetMostWatchesShows(), DateTime.Now.AddHours(12)); var result = await Cache.GetOrAdd(CacheKeys.MostWatchesTv, async () => await TraktApi.GetMostWatchesShows(), DateTime.Now.AddHours(12));
var processed = await ProcessResults(result); var processed = await ProcessResults(result);
return processed.Select(ParseIntoTreeNode).ToList(); return processed.Select(ParseIntoTreeNode).ToList();
} }
public async Task<IEnumerable<SearchTvShowViewModel>> MostWatches() public async Task<IEnumerable<SearchTvShowViewModel>> MostWatches()
{ {
var result = await MemCache.GetOrAdd(CacheKeys.MostWatchesTv, async () => await TraktApi.GetMostWatchesShows(), DateTime.Now.AddHours(12)); var result = await Cache.GetOrAdd(CacheKeys.MostWatchesTv, async () => await TraktApi.GetMostWatchesShows(), DateTime.Now.AddHours(12));
var processed = await ProcessResults(result); var processed = await ProcessResults(result);
return processed; return processed;
} }
public async Task<IEnumerable<TreeNode<SearchTvShowViewModel>>> TrendingTree() public async Task<IEnumerable<TreeNode<SearchTvShowViewModel>>> TrendingTree()
{ {
var result = await MemCache.GetOrAdd(CacheKeys.TrendingTv, async () => await TraktApi.GetTrendingShows(), DateTime.Now.AddHours(12)); var result = await Cache.GetOrAdd(CacheKeys.TrendingTv, async () => await TraktApi.GetTrendingShows(), DateTime.Now.AddHours(12));
var processed = await ProcessResults(result); var processed = await ProcessResults(result);
return processed.Select(ParseIntoTreeNode).ToList(); return processed.Select(ParseIntoTreeNode).ToList();
} }
public async Task<IEnumerable<SearchTvShowViewModel>> Trending() public async Task<IEnumerable<SearchTvShowViewModel>> Trending()
{ {
var result = await MemCache.GetOrAdd(CacheKeys.TrendingTv, async () => await TraktApi.GetTrendingShows(), DateTime.Now.AddHours(12)); var result = await Cache.GetOrAdd(CacheKeys.TrendingTv, async () => await TraktApi.GetTrendingShows(), DateTime.Now.AddHours(12));
var processed = await ProcessResults(result); var processed = await ProcessResults(result);
return processed; return processed;
} }

View file

@ -9,7 +9,7 @@ namespace Ombi.Helpers
var version = Assembly.GetEntryAssembly() var version = Assembly.GetEntryAssembly()
.GetCustomAttribute<AssemblyInformationalVersionAttribute>() .GetCustomAttribute<AssemblyInformationalVersionAttribute>()
.InformationalVersion; .InformationalVersion;
return version.Equals("1.0.0") ? "3.0.0-DotNetCore" : version; return version.Equals("1.0.0") ? "3.0.0-develop" : version;
} }
} }
} }

View file

@ -93,13 +93,26 @@ namespace Ombi.Schedule.Jobs.Plex
{ {
foreach (var servers in plexSettings.Servers ?? new List<PlexServers>()) foreach (var servers in plexSettings.Servers ?? new List<PlexServers>())
{ {
try
{
Logger.LogInformation("Starting to cache the content on server {0}", servers.Name);
await ProcessServer(servers);
}
catch (Exception e)
{
Logger.LogWarning(LoggingEvents.PlexContentCacher, e, "Exception thrown when attempting to cache the Plex Content in server {0}", servers.Name);
}
}
}
private async Task ProcessServer(PlexServers servers)
{
Logger.LogInformation("Getting all content from server {0}", servers.Name); Logger.LogInformation("Getting all content from server {0}", servers.Name);
var allContent = await GetAllContent(servers); var allContent = await GetAllContent(servers);
Logger.LogInformation("We found {0} items", allContent.Count); Logger.LogInformation("We found {0} items", allContent.Count);
// Let's now process this. // Let's now process this.
var contentToAdd = new List<PlexServerContent>(); var contentToAdd = new HashSet<PlexServerContent>();
foreach (var content in allContent) foreach (var content in allContent)
{ {
if (content.viewGroup.Equals(PlexMediaType.Show.ToString(), StringComparison.CurrentCultureIgnoreCase)) if (content.viewGroup.Equals(PlexMediaType.Show.ToString(), StringComparison.CurrentCultureIgnoreCase))
@ -128,10 +141,27 @@ namespace Ombi.Schedule.Jobs.Plex
&& x.ReleaseYear == show.year.ToString() && x.ReleaseYear == show.year.ToString()
&& x.Type == PlexMediaTypeEntity.Show); && x.Type == PlexMediaTypeEntity.Show);
// Just double check the rating key, since this is our unique constraint
var existingKey = await Repo.GetByKey(show.ratingKey);
if (existingKey != null)
{
// Damn son.
// Let's check if they match up
var doesMatch = show.title.Equals(existingKey.Title,
StringComparison.CurrentCulture);
if (!doesMatch)
{
// Something fucked up on Plex at somepoint... Damn, rebuild of lib maybe?
// Lets delete the matching key
await Repo.Delete(existingKey);
existingKey = null;
}
}
if (existingContent != null) if (existingContent != null)
{ {
// Just check the key // Just check the key
var existingKey = await Repo.GetByKey(show.ratingKey);
if (existingKey != null) if (existingKey != null)
{ {
// The rating key is all good! // The rating key is all good!
@ -155,7 +185,6 @@ namespace Ombi.Schedule.Jobs.Plex
await Repo.SaveChangesAsync(); await Repo.SaveChangesAsync();
existingContent = null; existingContent = null;
} }
} }
// The ratingKey keeps changing... // The ratingKey keeps changing...
//var existingContent = await Repo.GetByKey(show.ratingKey); //var existingContent = await Repo.GetByKey(show.ratingKey);
@ -163,12 +192,14 @@ namespace Ombi.Schedule.Jobs.Plex
{ {
try try
{ {
Logger.LogInformation("We already have show {0} checking for new seasons", existingContent.Title); Logger.LogInformation("We already have show {0} checking for new seasons",
existingContent.Title);
// Ok so we have it, let's check if there are any new seasons // Ok so we have it, let's check if there are any new seasons
var itemAdded = false; var itemAdded = false;
foreach (var season in seasonsContent) foreach (var season in seasonsContent)
{ {
var seasonExists = existingContent.Seasons.FirstOrDefault(x => x.SeasonKey == season.SeasonKey); var seasonExists =
existingContent.Seasons.FirstOrDefault(x => x.SeasonKey == season.SeasonKey);
if (seasonExists != null) if (seasonExists != null)
{ {
@ -184,21 +215,23 @@ namespace Ombi.Schedule.Jobs.Plex
} }
catch (Exception e) catch (Exception e)
{ {
Logger.LogError(LoggingEvents.PlexContentCacher, e, "Exception when adding new seasons to title {0}", existingContent.Title); Logger.LogError(LoggingEvents.PlexContentCacher, e,
"Exception when adding new seasons to title {0}", existingContent.Title);
} }
} }
else else
{ {
try try
{ {
Logger.LogInformation("New show {0}, so add it", show.title); Logger.LogInformation("New show {0}, so add it", show.title);
// Get the show metadata... This sucks since the `metadata` var contains all information about the show // Get the show metadata... This sucks since the `metadata` var contains all information about the show
// But it does not contain the `guid` property that we need to pull out thetvdb id... // But it does not contain the `guid` property that we need to pull out thetvdb id...
var showMetadata = await PlexApi.GetMetadata(servers.PlexAuthToken, servers.FullUri, var showMetadata = await PlexApi.GetMetadata(servers.PlexAuthToken, servers.FullUri,
show.ratingKey); show.ratingKey);
var providerIds = PlexHelper.GetProviderIdFromPlexGuid(showMetadata.MediaContainer.Metadata.FirstOrDefault().guid); var providerIds =
PlexHelper.GetProviderIdFromPlexGuid(showMetadata.MediaContainer.Metadata.FirstOrDefault()
.guid);
var item = new PlexServerContent var item = new PlexServerContent
{ {
@ -251,13 +284,17 @@ namespace Ombi.Schedule.Jobs.Plex
item.Seasons.ToList().AddRange(seasonsContent); item.Seasons.ToList().AddRange(seasonsContent);
contentToAdd.Add(item); contentToAdd.Add(item);
} }
catch (Exception e) catch (Exception e)
{ {
Logger.LogError(LoggingEvents.PlexContentCacher, e, "Exception when adding tv show {0}", show.title); Logger.LogError(LoggingEvents.PlexContentCacher, e, "Exception when adding tv show {0}",
show.title);
} }
}
if (contentToAdd.Count > 500)
{
await Repo.AddRange(contentToAdd);
contentToAdd.Clear();
} }
} }
} }
@ -321,14 +358,21 @@ namespace Ombi.Schedule.Jobs.Plex
} }
catch (Exception e) catch (Exception e)
{ {
Logger.LogError(LoggingEvents.PlexContentCacher, e, "Exception when adding new Movie {0}", movie.title); Logger.LogError(LoggingEvents.PlexContentCacher, e, "Exception when adding new Movie {0}",
movie.title);
}
if (contentToAdd.Count > 500)
{
await Repo.AddRange(contentToAdd);
contentToAdd.Clear();
} }
} }
} }
if (contentToAdd.Count > 500) if (contentToAdd.Count > 500)
{ {
await Repo.AddRange(contentToAdd); await Repo.AddRange(contentToAdd);
contentToAdd = new List<PlexServerContent>(); contentToAdd.Clear();
} }
} }
@ -336,8 +380,6 @@ namespace Ombi.Schedule.Jobs.Plex
{ {
await Repo.AddRange(contentToAdd); await Repo.AddRange(contentToAdd);
} }
}
} }
/// <summary> /// <summary>

View file

@ -62,7 +62,6 @@ namespace Ombi.Schedule.Jobs.Plex
{ {
if (!Validate(settings)) if (!Validate(settings))
{ {
_log.LogWarning("Validation failed"); _log.LogWarning("Validation failed");
return; return;
} }
@ -101,21 +100,25 @@ namespace Ombi.Schedule.Jobs.Plex
{ {
var currentPosition = 0; var currentPosition = 0;
var resultCount = settings.EpisodeBatchSize == 0 ? 150 : settings.EpisodeBatchSize; var resultCount = settings.EpisodeBatchSize == 0 ? 150 : settings.EpisodeBatchSize;
var currentEpisodes = _repo.GetAllEpisodes();
var episodes = await _api.GetAllEpisodes(settings.PlexAuthToken, settings.FullUri, section.key, currentPosition, resultCount); var episodes = await _api.GetAllEpisodes(settings.PlexAuthToken, settings.FullUri, section.key, currentPosition, resultCount);
_log.LogInformation(LoggingEvents.PlexEpisodeCacher, $"Total Epsiodes found for {episodes.MediaContainer.librarySectionTitle} = {episodes.MediaContainer.totalSize}"); _log.LogInformation(LoggingEvents.PlexEpisodeCacher, $"Total Epsiodes found for {episodes.MediaContainer.librarySectionTitle} = {episodes.MediaContainer.totalSize}");
// Delete all the episodes because we cannot uniquly match an episode to series every time, // Delete all the episodes because we cannot uniquly match an episode to series every time,
// see comment below. // see comment below.
await _repo.ExecuteSql("DELETE FROM PlexEpisode");
await ProcessEpsiodes(episodes); // 12.03.2017 - I think we should be able to match them now
//await _repo.ExecuteSql("DELETE FROM PlexEpisode");
await ProcessEpsiodes(episodes, currentEpisodes);
currentPosition += resultCount; currentPosition += resultCount;
while (currentPosition < episodes.MediaContainer.totalSize) while (currentPosition < episodes.MediaContainer.totalSize)
{ {
var ep = await _api.GetAllEpisodes(settings.PlexAuthToken, settings.FullUri, section.key, currentPosition, var ep = await _api.GetAllEpisodes(settings.PlexAuthToken, settings.FullUri, section.key, currentPosition,
resultCount); resultCount);
await ProcessEpsiodes(ep);
await ProcessEpsiodes(ep, currentEpisodes);
_log.LogInformation(LoggingEvents.PlexEpisodeCacher, $"Processed {resultCount} more episodes. Total Remaining {episodes.MediaContainer.totalSize - currentPosition}"); _log.LogInformation(LoggingEvents.PlexEpisodeCacher, $"Processed {resultCount} more episodes. Total Remaining {episodes.MediaContainer.totalSize - currentPosition}");
currentPosition += resultCount; currentPosition += resultCount;
} }
@ -125,35 +128,33 @@ namespace Ombi.Schedule.Jobs.Plex
await _repo.SaveChangesAsync(); await _repo.SaveChangesAsync();
} }
private async Task ProcessEpsiodes(PlexContainer episodes) private async Task ProcessEpsiodes(PlexContainer episodes, IQueryable<PlexEpisode> currentEpisodes)
{ {
var ep = new HashSet<PlexEpisode>(); var ep = new HashSet<PlexEpisode>();
try try
{ {
foreach (var episode in episodes?.MediaContainer?.Metadata ?? new Metadata[] { })
foreach (var episode in episodes?.MediaContainer?.Metadata ?? new Metadata[]{})
{ {
// I don't think we need to get the metadata, we only need to get the metadata if we need the provider id (TheTvDbid). Why do we need it for episodes? // I don't think we need to get the metadata, we only need to get the metadata if we need the provider id (TheTvDbid). Why do we need it for episodes?
// We have the parent and grandparent rating keys to link up to the season and series // We have the parent and grandparent rating keys to link up to the season and series
//var metadata = _api.GetEpisodeMetaData(server.PlexAuthToken, server.FullUri, episode.ratingKey); //var metadata = _api.GetEpisodeMetaData(server.PlexAuthToken, server.FullUri, episode.ratingKey);
// This does seem to work, it looks like we can somehow get different rating, grandparent and parent keys with episodes. Not sure how. // This does seem to work, it looks like we can somehow get different rating, grandparent and parent keys with episodes. Not sure how.
//var epExists = currentEpisodes.Any(x => episode.ratingKey == x.Key && var epExists = currentEpisodes.Any(x => episode.ratingKey == x.Key &&
// episode.grandparentRatingKey == x.GrandparentKey); episode.grandparentRatingKey == x.GrandparentKey);
//if (epExists) if (epExists)
//{ {
// continue; continue;
//} }
// Let's check if we have the parent // Let's check if we have the parent
var seriesExists = await _repo.GetByKey(episode.grandparentRatingKey); var seriesExists = await _repo.GetByKey(episode.grandparentRatingKey);
if (seriesExists == null) if (seriesExists == null)
{ {
// Ok let's try and match it to a title. TODO (This is experimental) // Ok let's try and match it to a title. TODO (This is experimental)
var seriesMatch = await _repo.GetAll().FirstOrDefaultAsync(x => seriesExists = await _repo.GetAll().FirstOrDefaultAsync(x =>
x.Title.Equals(episode.grandparentTitle, StringComparison.CurrentCultureIgnoreCase)); x.Title.Equals(episode.grandparentTitle, StringComparison.CurrentCultureIgnoreCase));
if (seriesMatch == null) if (seriesExists == null)
{ {
_log.LogWarning( _log.LogWarning(
"The episode title {0} we cannot find the parent series. The episode grandparentKey = {1}, grandparentTitle = {2}", "The episode title {0} we cannot find the parent series. The episode grandparentKey = {1}, grandparentTitle = {2}",
@ -162,7 +163,7 @@ namespace Ombi.Schedule.Jobs.Plex
} }
// Set the rating key to the correct one // Set the rating key to the correct one
episode.grandparentRatingKey = seriesMatch.Key; episode.grandparentRatingKey = seriesExists.Key;
} }
ep.Add(new PlexEpisode ep.Add(new PlexEpisode
@ -189,7 +190,7 @@ namespace Ombi.Schedule.Jobs.Plex
{ {
if (string.IsNullOrEmpty(settings.PlexAuthToken)) if (string.IsNullOrEmpty(settings.PlexAuthToken))
{ {
return false ; return false;
} }
return true; return true;

View file

@ -46,8 +46,7 @@ namespace Ombi.Schedule.Processor
if (masterBranch) if (masterBranch)
{ {
latestRelease = doc.DocumentNode.Descendants("h2") latestRelease = doc.DocumentNode.Descendants("h2")
.FirstOrDefault(x => x.InnerText == "(unreleased)"); .FirstOrDefault(x => x.InnerText != "(unreleased)");
// TODO: Change this to InnterText != "(unreleased)" once we go live and it's not a prerelease
} }
else else
{ {

View file

@ -7,7 +7,8 @@
public bool Wizard { get; set; } public bool Wizard { get; set; }
public string ApiKey { get; set; } public string ApiKey { get; set; }
public bool IgnoreCertificateErrors { get; set; } public bool IgnoreCertificateErrors { get; set; }
public bool DoNotSendNotificationsForAutoApprove {get;set;} public bool DoNotSendNotificationsForAutoApprove { get; set; }
public bool HideRequestsUsers { get; set; }
} }
} }

View file

@ -11,5 +11,7 @@ namespace Ombi.Store.Repository.Requests
Task Update(MovieRequests request); Task Update(MovieRequests request);
Task Save(); Task Save();
IQueryable<MovieRequests> GetWithUser(); IQueryable<MovieRequests> GetWithUser();
IQueryable<MovieRequests> GetWithUser(string userId);
IQueryable<MovieRequests> GetAll(string userId);
} }
} }

View file

@ -14,11 +14,13 @@ namespace Ombi.Store.Repository.Requests
Task Delete(TvRequests request); Task Delete(TvRequests request);
Task DeleteChild(ChildRequests request); Task DeleteChild(ChildRequests request);
IQueryable<TvRequests> Get(); IQueryable<TvRequests> Get();
IQueryable<TvRequests> Get(string userId);
Task<TvRequests> GetRequestAsync(int tvDbId); Task<TvRequests> GetRequestAsync(int tvDbId);
TvRequests GetRequest(int tvDbId); TvRequests GetRequest(int tvDbId);
Task Update(TvRequests request); Task Update(TvRequests request);
Task UpdateChild(ChildRequests request); Task UpdateChild(ChildRequests request);
IQueryable<ChildRequests> GetChild(); IQueryable<ChildRequests> GetChild();
IQueryable<ChildRequests> GetChild(string userId);
Task Save(); Task Save();
Task DeleteChildRange(IEnumerable<ChildRequests> request); Task DeleteChildRange(IEnumerable<ChildRequests> request);
} }

View file

@ -33,6 +33,11 @@ namespace Ombi.Store.Repository.Requests
} }
public IQueryable<MovieRequests> GetAll(string userId)
{
return GetWithUser().Where(x => x.RequestedUserId == userId);
}
public MovieRequests GetRequest(int theMovieDbId) public MovieRequests GetRequest(int theMovieDbId)
{ {
return Db.MovieRequests.Where(x => x.TheMovieDbId == theMovieDbId) return Db.MovieRequests.Where(x => x.TheMovieDbId == theMovieDbId)
@ -48,6 +53,16 @@ namespace Ombi.Store.Repository.Requests
.AsQueryable(); .AsQueryable();
} }
public IQueryable<MovieRequests> GetWithUser(string userId)
{
return Db.MovieRequests
.Where(x => x.RequestedUserId == userId)
.Include(x => x.RequestedUser)
.ThenInclude(x => x.NotificationUserIds)
.AsQueryable();
}
public async Task Update(MovieRequests request) public async Task Update(MovieRequests request)
{ {
if (Db.Entry(request).State == EntityState.Detached) if (Db.Entry(request).State == EntityState.Detached)

View file

@ -48,6 +48,18 @@ namespace Ombi.Store.Repository.Requests
.ThenInclude(x => x.Episodes) .ThenInclude(x => x.Episodes)
.AsQueryable(); .AsQueryable();
} }
public IQueryable<TvRequests> Get(string userId)
{
return Db.TvRequests
.Include(x => x.ChildRequests)
.ThenInclude(x => x.RequestedUser)
.Include(x => x.ChildRequests)
.ThenInclude(x => x.SeasonRequests)
.ThenInclude(x => x.Episodes)
.Where(x => x.ChildRequests.Any(a => a.RequestedUserId == userId))
.AsQueryable();
}
public IQueryable<ChildRequests> GetChild() public IQueryable<ChildRequests> GetChild()
{ {
return Db.ChildRequests return Db.ChildRequests
@ -58,6 +70,17 @@ namespace Ombi.Store.Repository.Requests
.AsQueryable(); .AsQueryable();
} }
public IQueryable<ChildRequests> GetChild(string userId)
{
return Db.ChildRequests
.Where(x => x.RequestedUserId == userId)
.Include(x => x.RequestedUser)
.Include(x => x.ParentRequest)
.Include(x => x.SeasonRequests)
.ThenInclude(x => x.Episodes)
.AsQueryable();
}
public async Task Save() public async Task Save()
{ {
await Db.SaveChangesAsync(); await Db.SaveChangesAsync();

View file

@ -14,6 +14,7 @@ export interface IOmbiSettings extends ISettings {
apiKey: string; apiKey: string;
ignoreCertificateErrors: boolean; ignoreCertificateErrors: boolean;
doNotSendNotificationsForAutoApprove: boolean; doNotSendNotificationsForAutoApprove: boolean;
hideRequestsUsers: boolean;
} }
export interface IUpdateSettings extends ISettings { export interface IUpdateSettings extends ISettings {

View file

@ -13,15 +13,15 @@
<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 [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> </span>
</th> </th>
<th (click)="setOrder('issue.status')"> <th (click)="setOrder('status')">
<a [translate]="'Issues.Status'"></a> <a [translate]="'Issues.Status'"></a>
<span *ngIf="order === 'issue.status'"> <span *ngIf="order === '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 [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> </span>
</th> </th>
<th (click)="setOrder('issue.reportedUser')"> <th (click)="setOrder('reportedUser')">
<a [translate]="'Issues.ReportedBy'"></a> <a [translate]="'Issues.ReportedBy'"></a>
<span *ngIf="order === 'issue.reportedUser'"> <span *ngIf="order === '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 [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> </span>
</th> </th>

View file

@ -4,9 +4,49 @@
<input type="text" id="search" class="form-control form-control-custom searchwidth" placeholder="Search" (keyup)="search($event)"> <input type="text" id="search" class="form-control form-control-custom searchwidth" placeholder="Search" (keyup)="search($event)">
<span class="input-group-btn"> <span class="input-group-btn">
<button id="filterBtn" class="btn btn-sm btn-info-outline" (click)="filterDisplay = !filterDisplay" > <button id="filterBtn" class="btn btn-sm btn-info-outline" (click)="filterDisplay = !filterDisplay" >
<i class="fa fa-filter"></i> {{ 'Requests.Filter' | translate }}</button> <i class="fa fa-filter"></i> {{ 'Requests.Filter' | translate }}
<!-- <button id="filterBtn" class="btn btn-sm btn-warning-outline" (click)="sortDisplay = !sortDisplay" > </button>
<i class="fa fa-sort"></i> {{ 'Requests.Sort' | translate }}</button> -->
<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-sort"></i> {{ 'Requests.Sort' | translate }}
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu2">
<li>
<a (click)="setOrder('requestedDate')">{{ 'Requests.SortRequestDate' | translate }}
<span *ngIf="order === 'requestedDate'">
<span [hidden]="reverse"><small><i class="fa fa-arrow-down" aria-hidden="true"></i></small></span>
<span [hidden]="!reverse"><small><i class="fa fa-arrow-up" aria-hidden="true"></i></small></span>
</span>
</a>
<a (click)="setOrder('title')">{{ 'Requests.SortTitle' | translate}}
<span *ngIf="order === 'title'">
<span [hidden]="reverse"><small><i class="fa fa-arrow-down" aria-hidden="true"></i></small></span>
<span [hidden]="!reverse"><small><i class="fa fa-arrow-up" aria-hidden="true"></i></small></span>
</span>
</a>
<a (click)="setOrder('releaseDate')">{{ 'Requests.TheatricalReleaseSort' | translate }}
<span *ngIf="order === 'releaseDate'">
<span [hidden]="reverse"><small><i class="fa fa-arrow-down" aria-hidden="true"></i></small></span>
<span [hidden]="!reverse"><small><i class="fa fa-arrow-up" aria-hidden="true"></i></small></span>
</span>
</a>
<a (click)="setOrder('requestedUser.userAlias')">{{ 'Requests.SortRequestedBy' | translate }}
<span *ngIf="order === 'requestedUser.userAlias'">
<span [hidden]="reverse"><small><i class="fa fa-arrow-down" aria-hidden="true"></i></small></span>
<span [hidden]="!reverse"><small><i class="fa fa-arrow-up" aria-hidden="true"></i></small></span>
</span>
</a>
<a (click)="setOrder('status')">{{ 'Requests.SortStatus' | translate }}
<span *ngIf="order === 'status'">
<span [hidden]="reverse"><small><i class="fa fa-arrow-down" aria-hidden="true"></i></small></span>
<span [hidden]="!reverse"><small><i class="fa fa-arrow-up" aria-hidden="true"></i></small></span>
</span>
</a>
</li>
</ul>
</span> </span>
</div> </div>
@ -19,7 +59,7 @@
<div infinite-scroll [infiniteScrollDistance]="1" [infiniteScrollThrottle]="100" (scrolled)="loadMore()"> <div infinite-scroll [infiniteScrollDistance]="1" [infiniteScrollThrottle]="100" (scrolled)="loadMore()">
<div *ngFor="let request of movieRequests"> <div *ngFor="let request of movieRequests | orderBy: order : reverse : 'case-insensitive'">
<div class="row"> <div class="row">
@ -203,8 +243,8 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="radio"> <div class="radio">
<input type="radio" id="approved" name="Status" (click)="filterStatus(filterType.PendingApproval)"> <input type="radio" id="pendingApproval" name="Status" (click)="filterStatus(filterType.PendingApproval, $event)">
<label for="approved">{{ 'Filter.PendingApproval' | translate }}</label> <label for="pendingApproval">{{ 'Filter.PendingApproval' | translate }}</label>
</div> </div>
</div> </div>

View file

@ -36,7 +36,8 @@ export class MovieRequestsComponent implements OnInit {
public filter: IFilter; public filter: IFilter;
public filterType = FilterType; public filterType = FilterType;
public sortDisplay: boolean; public order: string = "requestedDate";
public reverse = false;
private currentlyLoaded: number; private currentlyLoaded: number;
private amountToLoad: number; private amountToLoad: number;
@ -174,6 +175,14 @@ export class MovieRequestsComponent implements OnInit {
}); });
} }
public setOrder(value: string) {
if (this.order === value) {
this.reverse = !this.reverse;
}
this.order = value;
}
private loadRequests(amountToLoad: number, currentlyLoaded: number) { private loadRequests(amountToLoad: number, currentlyLoaded: number) {
this.requestService.getMovieRequests(amountToLoad, currentlyLoaded + 1) this.requestService.getMovieRequests(amountToLoad, currentlyLoaded + 1)
.subscribe(x => { .subscribe(x => {

View file

@ -2,6 +2,7 @@
import { RouterModule, Routes } from "@angular/router"; import { RouterModule, Routes } from "@angular/router";
import { NgbModule } from "@ng-bootstrap/ng-bootstrap"; import { NgbModule } from "@ng-bootstrap/ng-bootstrap";
import { OrderModule } from "ngx-order-pipe";
import { InfiniteScrollModule } from "ngx-infinite-scroll"; import { InfiniteScrollModule } from "ngx-infinite-scroll";
@ -34,6 +35,7 @@ const routes: Routes = [
TreeTableModule, TreeTableModule,
SharedModule, SharedModule,
SidebarModule, SidebarModule,
OrderModule,
], ],
declarations: [ declarations: [
RequestComponent, RequestComponent,

View file

@ -62,7 +62,7 @@
<div class="col-sm-8 small-padding"> <div class="col-sm-8 small-padding">
<div> <div>
<a href="http://www.imdb.com/title/{{node.data.imdbId}}/" target="_blank"> <a *ngIf="node.data.imdbId" href="http://www.imdb.com/title/{{node.data.imdbId}}/" target="_blank">
<h4>{{node.data.title}} ({{node.data.firstAired | date: 'yyyy'}})</h4> <h4>{{node.data.title}} ({{node.data.firstAired | date: 'yyyy'}})</h4>
</a> </a>

View file

@ -53,6 +53,14 @@
</div> </div>
</div> </div>
<div class="form-group">
<div class="checkbox">
<input type="checkbox" id="hideRequestsUsers" name="hideRequestsUsers" formControlName="hideRequestsUsers">
<label for="hideRequestsUsers">Hide requests from other users</label>
</div>
</div>
<div class="form-group"> <div class="form-group">
<div class="checkbox"> <div class="checkbox">
<input type="checkbox" id="ignoreCertificateErrors" name="ignoreCertificateErrors" formControlName="ignoreCertificateErrors"> <input type="checkbox" id="ignoreCertificateErrors" name="ignoreCertificateErrors" formControlName="ignoreCertificateErrors">

View file

@ -24,6 +24,7 @@ export class OmbiComponent implements OnInit {
ignoreCertificateErrors: [x.ignoreCertificateErrors], ignoreCertificateErrors: [x.ignoreCertificateErrors],
baseUrl: [x.baseUrl], baseUrl: [x.baseUrl],
doNotSendNotificationsForAutoApprove: [x.doNotSendNotificationsForAutoApprove], doNotSendNotificationsForAutoApprove: [x.doNotSendNotificationsForAutoApprove],
hideRequestsUsers: [x.hideRequestsUsers],
}); });
}); });
} }

View file

@ -16,44 +16,45 @@
</td> </td>
</a> </a>
</th> </th>
<th (click)="setOrder('u.userName')"> <th (click)="setOrder('userName')">
<a> <a>
Username Username
</a> </a>
<span *ngIf="order === 'u.userName'"> <span *ngIf="order === 'userName'">
<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 [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> </span>
</th> </th>
<th (click)="setOrder('u.alias')"> <th (click)="setOrder('alias')">
<a> <a>
Alias Alias
</a> </a>
<span *ngIf="order === 'u.alias'"> <span *ngIf="order === 'alias'">
<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 [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> </span>
</th> </th>
<th (click)="setOrder('u.emailAddress')"> <th (click)="setOrder('emailAddress')">
<a> <a>
Email Email
</a> </a>
<span *ngIf="order === 'u.emailAddress'"> <span *ngIf="order === 'emailAddress'">
<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 [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> </span>
</th> </th>
<th> <th>
Roles Roles
</th> </th>
<th (click)="setOrder('u.lastLoggedIn')"> <th (click)="setOrder('lastLoggedIn')">
<a> Last Logged In</a> <a> Last Logged In</a>
<span *ngIf="order === 'u.lastLoggedIn'"> <span *ngIf="order === 'lastLoggedIn'">
<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 [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> </span>
</th> </th>
<th (click)="setOrder('u.userType')"> <th (click)="setOrder('userType')">
<a> <a>
User Type User Type
</a> </a>
<span *ngIf="order === 'u.userType'"> <span *ngIf="order === 'userType'">
<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 [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> </span>
</th> </th>

View file

@ -13,7 +13,7 @@ export class UserManagementComponent implements OnInit {
public emailSettings: IEmailNotificationSettings; public emailSettings: IEmailNotificationSettings;
public customizationSettings: ICustomizationSettings; public customizationSettings: ICustomizationSettings;
public order: string = "u.userName"; public order: string = "userName";
public reverse = false; public reverse = false;
public showBulkEdit = false; public showBulkEdit = false;

View file

@ -334,9 +334,9 @@ namespace Ombi.Controllers
/// <param name="vm"></param> /// <param name="vm"></param>
/// <returns></returns> /// <returns></returns>
[HttpPost("movie/filter")] [HttpPost("movie/filter")]
public IEnumerable<MovieRequests> Filter([FromBody] FilterViewModel vm) public async Task<IEnumerable<MovieRequests>> Filter([FromBody] FilterViewModel vm)
{ {
return MovieRequestEngine.Filter(vm); return await MovieRequestEngine.Filter(vm);
} }
} }
} }

BIN
src/Ombi/Ombi.testdb Normal file

Binary file not shown.

View file

@ -4972,9 +4972,9 @@
"integrity": "sha512-7lASze8zHSDdAAFO3VNop1TY60rs8A7sm8DzQfU33VNcJI27F6mtxwjILIH339s7m6HVC08AS7I64HBjBMw/QQ==" "integrity": "sha512-7lASze8zHSDdAAFO3VNop1TY60rs8A7sm8DzQfU33VNcJI27F6mtxwjILIH339s7m6HVC08AS7I64HBjBMw/QQ=="
}, },
"ngx-order-pipe": { "ngx-order-pipe": {
"version": "1.1.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/ngx-order-pipe/-/ngx-order-pipe-1.1.1.tgz", "resolved": "https://registry.npmjs.org/ngx-order-pipe/-/ngx-order-pipe-2.0.1.tgz",
"integrity": "sha512-hIfdUONbKG14/S5zEyGjr1ukAd2XdUUnUsvA80ct3pyoBCh5aQ7XhBz7N9jCsVzMGTGUPK6R59KYkEPB3n5hbQ==" "integrity": "sha512-t0IUqoNs3705yZQeohmhUQvpiRTj5RX7AhFXkx3PMfq7G6h7GNNrR3x27XbXEsjKgBo5hPgfEfW5OljRYa1VVw=="
}, },
"ngx-window-token": { "ngx-window-token": {
"version": "0.0.4", "version": "0.0.4",

View file

@ -22,7 +22,7 @@
"@angular/platform-browser-dynamic": "^5.1.2", "@angular/platform-browser-dynamic": "^5.1.2",
"@angular/platform-server": "5.0.0", "@angular/platform-server": "5.0.0",
"@angular/router": "^5.1.2", "@angular/router": "^5.1.2",
"@auth0/angular-jwt": "^1.0.0-beta.9", "@auth0/angular-jwt": "1.0.0-beta.9",
"@ng-bootstrap/ng-bootstrap": "^1.0.0-beta.8", "@ng-bootstrap/ng-bootstrap": "^1.0.0-beta.8",
"@ngx-translate/core": "^8.0.0", "@ngx-translate/core": "^8.0.0",
"@ngx-translate/http-loader": "^2.0.1", "@ngx-translate/http-loader": "^2.0.1",
@ -55,7 +55,7 @@
"ng2-cookies": "^1.0.12", "ng2-cookies": "^1.0.12",
"ngx-clipboard": "8.1.1", "ngx-clipboard": "8.1.1",
"ngx-infinite-scroll": "^0.6.1", "ngx-infinite-scroll": "^0.6.1",
"ngx-order-pipe": "^1.1.1", "ngx-order-pipe": "^2.0.1",
"node-sass": "^4.7.2", "node-sass": "^4.7.2",
"npm": "^5.6.0", "npm": "^5.6.0",
"pace-progress": "^1.0.2", "pace-progress": "^1.0.2",

View file

@ -21,7 +21,11 @@
"Request": "Anmod", "Request": "Anmod",
"Denied": "Afvist", "Denied": "Afvist",
"Approve": "Godkendt", "Approve": "Godkendt",
<<<<<<< HEAD
"PartlyAvailable": "Delvist tilgængelig", "PartlyAvailable": "Delvist tilgængelig",
=======
"PartlyAvailable": "Partly Available",
>>>>>>> New translations en.json (Danish)
"Errors": { "Errors": {
"Validation": "Tjek venligst dine indtastede værdier" "Validation": "Tjek venligst dine indtastede værdier"
} }
@ -87,6 +91,7 @@
"Trailer": "Trailer" "Trailer": "Trailer"
}, },
"TvShows": { "TvShows": {
<<<<<<< HEAD
"Popular": "Populære", "Popular": "Populære",
"Trending": "Aktuelle", "Trending": "Aktuelle",
"MostWatched": "Mest sete", "MostWatched": "Mest sete",
@ -100,6 +105,18 @@
"SubmitRequest": "Send anmodning", "SubmitRequest": "Send anmodning",
"Season": "Sæson: {{seasonNumber}}", "Season": "Sæson: {{seasonNumber}}",
"SelectAllInSeason": "Vælg alle i sæson {{seasonNumber}}" "SelectAllInSeason": "Vælg alle i sæson {{seasonNumber}}"
=======
"Popular": "Popular",
"Trending": "Trending",
"MostWatched": "Most Watched",
"MostAnticipated": "Most Anticipated",
"Results": "Results",
"AirDate": "Air Date:",
"AllSeasons": "All Seasons",
"FirstSeason": "First Season",
"LatestSeason": "Latest Season",
"Select": "Select ..."
>>>>>>> New translations en.json (Danish)
} }
}, },
"Requests": { "Requests": {

View file

@ -117,6 +117,7 @@
"RequestStatus": "Request status:", "RequestStatus": "Request status:",
"Denied": " Denied:", "Denied": " Denied:",
"TheatricalRelease": "Theatrical Release: {{date}}", "TheatricalRelease": "Theatrical Release: {{date}}",
"TheatricalReleaseSort": "Theatrical Release",
"DigitalRelease": "Digital Release: {{date}}", "DigitalRelease": "Digital Release: {{date}}",
"RequestDate": "Request Date:", "RequestDate": "Request Date:",
"QualityOverride": "Quality Override:", "QualityOverride": "Quality Override:",
@ -134,7 +135,11 @@
"ReportIssue":"Report Issue", "ReportIssue":"Report Issue",
"Filter":"Filter", "Filter":"Filter",
"Sort":"Sort", "Sort":"Sort",
"SeasonNumberHeading":"Season: {seasonNumber}" "SeasonNumberHeading":"Season: {seasonNumber}",
"SortTitle":"Title",
"SortRequestDate": "Request Date",
"SortRequestedBy":"Requested By",
"SortStatus":"Status"
}, },
"Issues":{ "Issues":{
"Title":"Issues", "Title":"Issues",