mirror of
https://github.com/Ombi-app/Ombi.git
synced 2025-08-20 13:23:20 -07:00
Merge branch 'develop' into feature/updater
This commit is contained in:
commit
e42abd604d
59 changed files with 1664 additions and 277 deletions
69
CHANGELOG.md
69
CHANGELOG.md
|
@ -1,6 +1,71 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## (unreleased)
|
## v3.0.3421 (2018-06-23)
|
||||||
|
|
||||||
|
### **New Features**
|
||||||
|
|
||||||
|
- Added TVRequestsLite. [Jamie]
|
||||||
|
|
||||||
|
- Added a smaller and simplier way of getting TV Request info. [Jamie Rees]
|
||||||
|
|
||||||
|
### **Fixes**
|
||||||
|
|
||||||
|
- Show the popular movies and tv shows by default. [Jamie]
|
||||||
|
|
||||||
|
- Fixed #2348. [Jamie]
|
||||||
|
|
||||||
|
|
||||||
|
## v3.0.3407 (2018-06-18)
|
||||||
|
|
||||||
|
### **New Features**
|
||||||
|
|
||||||
|
- Update appveyor.yml. [Jamie]
|
||||||
|
|
||||||
|
- Update build.cake. [Jamie]
|
||||||
|
|
||||||
|
### **Fixes**
|
||||||
|
|
||||||
|
- Fixed the issue where when we find an episode for the recently added sync, we don't check if we should run the availbility checker. [Jamie]
|
||||||
|
|
||||||
|
- Fixed the API not working due to a bug in .Net Core 2.1. [Jamie]
|
||||||
|
|
||||||
|
- Fixed #2321. [Jamie]
|
||||||
|
|
||||||
|
- Maybe this will fix #2298. [Jamie]
|
||||||
|
|
||||||
|
- Fixed #2312. [Jamie]
|
||||||
|
|
||||||
|
- Fixed the SickRage/Medusa Issue where it was always being set as Skipped/Ignore #2084. [Jamie]
|
||||||
|
|
||||||
|
- Fixed the sorting and filtering on the Movie Requests page, it all functions correctly now. [Jamie]
|
||||||
|
|
||||||
|
- Fixed #2288. [Jamie]
|
||||||
|
|
||||||
|
- Upgrade packages. [Jamie]
|
||||||
|
|
||||||
|
- Inital Migration. [Jamie]
|
||||||
|
|
||||||
|
- Fixed #2317. [Jamie]
|
||||||
|
|
||||||
|
|
||||||
|
## v3.0.3383 (2018-06-07)
|
||||||
|
|
||||||
|
### **New Features**
|
||||||
|
|
||||||
|
- Update CHANGELOG.md. [Jamie]
|
||||||
|
|
||||||
|
### **Fixes**
|
||||||
|
|
||||||
|
- Minor improvements. [Jamie]
|
||||||
|
|
||||||
|
- Run the availability checker on finish of the recentlty added sync. [Jamie]
|
||||||
|
|
||||||
|
- Fixed the issue with the Recently Added Sync sometimes not working as expected. [Jamie]
|
||||||
|
|
||||||
|
- The UI looks at the local time to see if the JWT token has expired. Use local time to generate the token. [Jamie Rees]
|
||||||
|
|
||||||
|
|
||||||
|
## v3.0.3368 (2018-06-03)
|
||||||
|
|
||||||
### **New Features**
|
### **New Features**
|
||||||
|
|
||||||
|
@ -8,6 +73,8 @@
|
||||||
|
|
||||||
- Added the subscribe button to the search page if we have an existing request. [Jamie Rees]
|
- Added the subscribe button to the search page if we have an existing request. [Jamie Rees]
|
||||||
|
|
||||||
|
- Update CHANGELOG.md. [Jamie]
|
||||||
|
|
||||||
### **Fixes**
|
### **Fixes**
|
||||||
|
|
||||||
- Use selected episodes in submitRequest. [Calvin]
|
- Use selected episodes in submitRequest. [Calvin]
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.EntityFrameworkCore.Internal;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Ombi.Api.Emby.Models;
|
using Ombi.Api.Emby.Models;
|
||||||
using Ombi.Api.Emby.Models.Media.Tv;
|
using Ombi.Api.Emby.Models.Media.Tv;
|
||||||
|
@ -100,7 +101,7 @@ namespace Ombi.Api.Emby
|
||||||
|
|
||||||
public async Task<EmbyItemContainer<EmbyMovie>> GetAllMovies(string apiKey, string userId, string baseUri)
|
public async Task<EmbyItemContainer<EmbyMovie>> GetAllMovies(string apiKey, string userId, string baseUri)
|
||||||
{
|
{
|
||||||
return await GetAll<EmbyMovie>("Movie", apiKey, userId, baseUri);
|
return await GetAll<EmbyMovie>("Movie", apiKey, userId, baseUri, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<EmbyItemContainer<EmbyEpisodes>> GetAllEpisodes(string apiKey, string userId, string baseUri)
|
public async Task<EmbyItemContainer<EmbyEpisodes>> GetAllEpisodes(string apiKey, string userId, string baseUri)
|
||||||
|
@ -129,20 +130,22 @@ namespace Ombi.Api.Emby
|
||||||
private async Task<T> GetInformation<T>(string mediaId, string apiKey, string userId, string baseUrl)
|
private async Task<T> GetInformation<T>(string mediaId, string apiKey, string userId, string baseUrl)
|
||||||
{
|
{
|
||||||
var request = new Request($"emby/users/{userId}/items/{mediaId}", baseUrl, HttpMethod.Get);
|
var request = new Request($"emby/users/{userId}/items/{mediaId}", baseUrl, HttpMethod.Get);
|
||||||
|
|
||||||
AddHeaders(request, apiKey);
|
AddHeaders(request, apiKey);
|
||||||
var response = await Api.RequestContent(request);
|
var response = await Api.RequestContent(request);
|
||||||
|
|
||||||
return JsonConvert.DeserializeObject<T>(response);
|
return JsonConvert.DeserializeObject<T>(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<EmbyItemContainer<T>> GetAll<T>(string type, string apiKey, string userId, string baseUri, bool includeOverview = false)
|
||||||
|
|
||||||
private async Task<EmbyItemContainer<T>> GetAll<T>(string type, string apiKey, string userId, string baseUri)
|
|
||||||
{
|
{
|
||||||
var request = new Request($"emby/users/{userId}/items", baseUri, HttpMethod.Get);
|
var request = new Request($"emby/users/{userId}/items", baseUri, HttpMethod.Get);
|
||||||
|
|
||||||
request.AddQueryString("Recursive", true.ToString());
|
request.AddQueryString("Recursive", true.ToString());
|
||||||
request.AddQueryString("IncludeItemTypes", type);
|
request.AddQueryString("IncludeItemTypes", type);
|
||||||
|
request.AddQueryString("Fields", includeOverview ? "ProviderIds,Overview" : "ProviderIds");
|
||||||
|
|
||||||
|
request.AddQueryString("VirtualItem","False");
|
||||||
|
|
||||||
AddHeaders(request, apiKey);
|
AddHeaders(request, apiKey);
|
||||||
|
|
||||||
|
|
|
@ -28,5 +28,7 @@ namespace Ombi.Api.Emby.Models.Movie
|
||||||
public string MediaType { get; set; }
|
public string MediaType { get; set; }
|
||||||
public bool HasSubtitles { get; set; }
|
public bool HasSubtitles { get; set; }
|
||||||
public int CriticRating { get; set; }
|
public int CriticRating { get; set; }
|
||||||
|
public string Overview { get; set; }
|
||||||
|
public EmbyProviderids ProviderIds { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -39,5 +39,6 @@ namespace Ombi.Api.Emby.Models.Media.Tv
|
||||||
public string LocationType { get; set; }
|
public string LocationType { get; set; }
|
||||||
public string MediaType { get; set; }
|
public string MediaType { get; set; }
|
||||||
public bool HasSubtitles { get; set; }
|
public bool HasSubtitles { get; set; }
|
||||||
|
public EmbyProviderids ProviderIds { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -26,5 +26,7 @@ namespace Ombi.Api.Emby.Models.Media.Tv
|
||||||
public string[] BackdropImageTags { get; set; }
|
public string[] BackdropImageTags { get; set; }
|
||||||
public string LocationType { get; set; }
|
public string LocationType { get; set; }
|
||||||
public DateTime EndDate { get; set; }
|
public DateTime EndDate { get; set; }
|
||||||
|
|
||||||
|
public EmbyProviderids ProviderIds { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -21,6 +21,7 @@ namespace Ombi.Api.FanartTv
|
||||||
{
|
{
|
||||||
var request = new Request($"tv/{tvdbId}", Endpoint, HttpMethod.Get);
|
var request = new Request($"tv/{tvdbId}", Endpoint, HttpMethod.Get);
|
||||||
request.AddHeader("api-key", token);
|
request.AddHeader("api-key", token);
|
||||||
|
request.IgnoreErrors = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return await Api.Request<TvResult>(request);
|
return await Api.Request<TvResult>(request);
|
||||||
|
@ -36,6 +37,7 @@ namespace Ombi.Api.FanartTv
|
||||||
{
|
{
|
||||||
var request = new Request($"movies/{movieOrImdbId}", Endpoint, HttpMethod.Get);
|
var request = new Request($"movies/{movieOrImdbId}", Endpoint, HttpMethod.Get);
|
||||||
request.AddHeader("api-key", token);
|
request.AddHeader("api-key", token);
|
||||||
|
request.IgnoreErrors = true;
|
||||||
|
|
||||||
return await Api.Request<MovieResult>(request);
|
return await Api.Request<MovieResult>(request);
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,11 @@ namespace Ombi.Api
|
||||||
|
|
||||||
if (!httpResponseMessage.IsSuccessStatusCode)
|
if (!httpResponseMessage.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
LogError(request, httpResponseMessage);
|
if (!request.IgnoreErrors)
|
||||||
|
{
|
||||||
|
LogError(request, httpResponseMessage);
|
||||||
|
}
|
||||||
|
|
||||||
if (request.Retry)
|
if (request.Retry)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
@ -94,7 +98,10 @@ namespace Ombi.Api
|
||||||
var httpResponseMessage = await _client.SendAsync(httpRequestMessage);
|
var httpResponseMessage = await _client.SendAsync(httpRequestMessage);
|
||||||
if (!httpResponseMessage.IsSuccessStatusCode)
|
if (!httpResponseMessage.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
LogError(request, httpResponseMessage);
|
if (!request.IgnoreErrors)
|
||||||
|
{
|
||||||
|
LogError(request, httpResponseMessage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// do something with the response
|
// do something with the response
|
||||||
var data = httpResponseMessage.Content;
|
var data = httpResponseMessage.Content;
|
||||||
|
@ -112,7 +119,10 @@ namespace Ombi.Api
|
||||||
var httpResponseMessage = await _client.SendAsync(httpRequestMessage);
|
var httpResponseMessage = await _client.SendAsync(httpRequestMessage);
|
||||||
if (!httpResponseMessage.IsSuccessStatusCode)
|
if (!httpResponseMessage.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
LogError(request, httpResponseMessage);
|
if (!request.IgnoreErrors)
|
||||||
|
{
|
||||||
|
LogError(request, httpResponseMessage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@ namespace Ombi.Api
|
||||||
public string Endpoint { get; }
|
public string Endpoint { get; }
|
||||||
public string BaseUrl { get; }
|
public string BaseUrl { get; }
|
||||||
public HttpMethod HttpMethod { get; }
|
public HttpMethod HttpMethod { get; }
|
||||||
|
public bool IgnoreErrors { get; set; }
|
||||||
public bool Retry { get; set; }
|
public bool Retry { get; set; }
|
||||||
public List<HttpStatusCode> StatusCodeToRetry { get; set; } = new List<HttpStatusCode>();
|
public List<HttpStatusCode> StatusCodeToRetry { get; set; } = new List<HttpStatusCode>();
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<PackageReference Include="Nunit" Version="3.8.1" />
|
<PackageReference Include="Nunit" Version="3.8.1" />
|
||||||
<PackageReference Include="NUnit.ConsoleRunner" Version="3.7.0" />
|
<PackageReference Include="NUnit.ConsoleRunner" Version="3.7.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="3.8.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="3.8.0" />
|
||||||
<packagereference Include="Microsoft.NET.Test.Sdk" Version="15.7.0"></packagereference>
|
<packagereference Include="Microsoft.NET.Test.Sdk" Version="15.7.2"></packagereference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Ombi.Core.Models.Requests;
|
using Ombi.Core.Models.Requests;
|
||||||
using Ombi.Core.Models.Search;
|
using Ombi.Core.Models.UI;
|
||||||
using Ombi.Store.Entities.Requests;
|
using Ombi.Store.Entities.Requests;
|
||||||
|
|
||||||
namespace Ombi.Core.Engine.Interfaces
|
namespace Ombi.Core.Engine.Interfaces
|
||||||
|
@ -10,8 +10,10 @@ namespace Ombi.Core.Engine.Interfaces
|
||||||
{
|
{
|
||||||
|
|
||||||
Task RemoveTvRequest(int requestId);
|
Task RemoveTvRequest(int requestId);
|
||||||
|
Task<TvRequests> GetTvRequest(int requestId);
|
||||||
Task<RequestEngineResult> RequestTvShow(TvRequestViewModel tv);
|
Task<RequestEngineResult> RequestTvShow(TvRequestViewModel tv);
|
||||||
Task<RequestEngineResult> DenyChildRequest(int requestId);
|
Task<RequestEngineResult> DenyChildRequest(int requestId);
|
||||||
|
Task<RequestsViewModel<TvRequests>> GetRequestsLite(int count, int position, OrderFilterModel type);
|
||||||
Task<IEnumerable<TvRequests>> SearchTvRequest(string search);
|
Task<IEnumerable<TvRequests>> SearchTvRequest(string search);
|
||||||
Task<IEnumerable<TreeNode<TvRequests, List<ChildRequests>>>> SearchTvRequestTree(string search);
|
Task<IEnumerable<TreeNode<TvRequests, List<ChildRequests>>>> SearchTvRequestTree(string search);
|
||||||
Task<TvRequests> UpdateTvRequest(TvRequests request);
|
Task<TvRequests> UpdateTvRequest(TvRequests request);
|
||||||
|
@ -20,5 +22,6 @@ namespace Ombi.Core.Engine.Interfaces
|
||||||
Task<ChildRequests> UpdateChildRequest(ChildRequests request);
|
Task<ChildRequests> UpdateChildRequest(ChildRequests request);
|
||||||
Task RemoveTvChild(int requestId);
|
Task RemoveTvChild(int requestId);
|
||||||
Task<RequestEngineResult> ApproveChildRequest(int id);
|
Task<RequestEngineResult> ApproveChildRequest(int id);
|
||||||
|
Task<IEnumerable<TvRequests>> GetRequestsLite();
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -60,7 +60,6 @@ namespace Ombi.Core.Engine
|
||||||
|
|
||||||
if (result != null)
|
if (result != null)
|
||||||
{
|
{
|
||||||
Logger.LogDebug("Search Result: {result}", result);
|
|
||||||
return await TransformMovieResultsToResponse(result.Take(10)); // Take 10 to stop us overloading the API
|
return await TransformMovieResultsToResponse(result.Take(10)); // Take 10 to stop us overloading the API
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -91,7 +90,6 @@ namespace Ombi.Core.Engine
|
||||||
var result = await Cache.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);
|
|
||||||
return await TransformMovieResultsToResponse(result.Take(10)); // Take 10 to stop us overloading the API
|
return await TransformMovieResultsToResponse(result.Take(10)); // Take 10 to stop us overloading the API
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -106,7 +104,6 @@ namespace Ombi.Core.Engine
|
||||||
var result = await Cache.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);
|
|
||||||
return await TransformMovieResultsToResponse(result.Take(10)); // Take 10 to stop us overloading the API
|
return await TransformMovieResultsToResponse(result.Take(10)); // Take 10 to stop us overloading the API
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -136,7 +133,6 @@ namespace Ombi.Core.Engine
|
||||||
var result = await Cache.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);
|
|
||||||
return await TransformMovieResultsToResponse(result.Take(10)); // Take 10 to stop us overloading the API
|
return await TransformMovieResultsToResponse(result.Take(10)); // Take 10 to stop us overloading the API
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -168,6 +168,35 @@ namespace Ombi.Core.Engine
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<RequestsViewModel<TvRequests>> GetRequestsLite(int count, int position, OrderFilterModel type)
|
||||||
|
{
|
||||||
|
var shouldHide = await HideFromOtherUsers();
|
||||||
|
List<TvRequests> allRequests;
|
||||||
|
if (shouldHide.Hide)
|
||||||
|
{
|
||||||
|
allRequests = await TvRepository.GetLite(shouldHide.UserId)
|
||||||
|
.OrderByDescending(x => x.ChildRequests.Max(y => y.RequestedDate))
|
||||||
|
.Skip(position).Take(count).ToListAsync();
|
||||||
|
|
||||||
|
// Filter out children
|
||||||
|
|
||||||
|
FilterChildren(allRequests, shouldHide);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
allRequests = await TvRepository.GetLite()
|
||||||
|
.OrderByDescending(x => x.ChildRequests.Max(y => y.RequestedDate))
|
||||||
|
.Skip(position).Take(count).ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
allRequests.ForEach(async r => { await CheckForSubscription(shouldHide, r); });
|
||||||
|
|
||||||
|
return new RequestsViewModel<TvRequests>
|
||||||
|
{
|
||||||
|
Collection = 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 shouldHide = await HideFromOtherUsers();
|
var shouldHide = await HideFromOtherUsers();
|
||||||
|
@ -178,6 +207,7 @@ namespace Ombi.Core.Engine
|
||||||
.Include(x => x.ChildRequests)
|
.Include(x => x.ChildRequests)
|
||||||
.ThenInclude(x => x.SeasonRequests)
|
.ThenInclude(x => x.SeasonRequests)
|
||||||
.ThenInclude(x => x.Episodes)
|
.ThenInclude(x => x.Episodes)
|
||||||
|
.Where(x => x.ChildRequests.Any())
|
||||||
.OrderByDescending(x => x.ChildRequests.Max(y => y.RequestedDate))
|
.OrderByDescending(x => x.ChildRequests.Max(y => y.RequestedDate))
|
||||||
.Skip(position).Take(count).ToListAsync();
|
.Skip(position).Take(count).ToListAsync();
|
||||||
|
|
||||||
|
@ -189,6 +219,7 @@ namespace Ombi.Core.Engine
|
||||||
.Include(x => x.ChildRequests)
|
.Include(x => x.ChildRequests)
|
||||||
.ThenInclude(x => x.SeasonRequests)
|
.ThenInclude(x => x.SeasonRequests)
|
||||||
.ThenInclude(x => x.Episodes)
|
.ThenInclude(x => x.Episodes)
|
||||||
|
.Where(x => x.ChildRequests.Any())
|
||||||
.OrderByDescending(x => x.ChildRequests.Max(y => y.RequestedDate))
|
.OrderByDescending(x => x.ChildRequests.Max(y => y.RequestedDate))
|
||||||
.Skip(position).Take(count).ToListAsync();
|
.Skip(position).Take(count).ToListAsync();
|
||||||
}
|
}
|
||||||
|
@ -216,6 +247,45 @@ namespace Ombi.Core.Engine
|
||||||
return allRequests;
|
return allRequests;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<IEnumerable<TvRequests>> GetRequestsLite()
|
||||||
|
{
|
||||||
|
var shouldHide = await HideFromOtherUsers();
|
||||||
|
List<TvRequests> allRequests;
|
||||||
|
if (shouldHide.Hide)
|
||||||
|
{
|
||||||
|
allRequests = await TvRepository.GetLite(shouldHide.UserId).ToListAsync();
|
||||||
|
|
||||||
|
FilterChildren(allRequests, shouldHide);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
allRequests = await TvRepository.GetLite().ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
allRequests.ForEach(async r => { await CheckForSubscription(shouldHide, r); });
|
||||||
|
return allRequests;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TvRequests> GetTvRequest(int requestId)
|
||||||
|
{
|
||||||
|
var shouldHide = await HideFromOtherUsers();
|
||||||
|
TvRequests request;
|
||||||
|
if (shouldHide.Hide)
|
||||||
|
{
|
||||||
|
request = await TvRepository.Get(shouldHide.UserId).Where(x => x.Id == requestId).FirstOrDefaultAsync();
|
||||||
|
|
||||||
|
FilterChildren(request, shouldHide);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
request = await TvRepository.Get().Where(x => x.Id == requestId).FirstOrDefaultAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
await CheckForSubscription(shouldHide, request);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
private static void FilterChildren(IEnumerable<TvRequests> allRequests, HideResult shouldHide)
|
private static void FilterChildren(IEnumerable<TvRequests> allRequests, HideResult shouldHide)
|
||||||
{
|
{
|
||||||
// Filter out children
|
// Filter out children
|
||||||
|
@ -223,16 +293,27 @@ namespace Ombi.Core.Engine
|
||||||
{
|
{
|
||||||
for (var j = 0; j < t.ChildRequests.Count; j++)
|
for (var j = 0; j < t.ChildRequests.Count; j++)
|
||||||
{
|
{
|
||||||
var child = t.ChildRequests[j];
|
FilterChildren(t, shouldHide);
|
||||||
if (child.RequestedUserId != shouldHide.UserId)
|
|
||||||
{
|
|
||||||
t.ChildRequests.RemoveAt(j);
|
|
||||||
j--;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void FilterChildren(TvRequests t, HideResult shouldHide)
|
||||||
|
{
|
||||||
|
// Filter out children
|
||||||
|
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
var shouldHide = await HideFromOtherUsers();
|
var shouldHide = await HideFromOtherUsers();
|
||||||
|
@ -468,7 +549,7 @@ namespace Ombi.Core.Engine
|
||||||
{
|
{
|
||||||
foreach (var tv in x.ChildRequests)
|
foreach (var tv in x.ChildRequests)
|
||||||
{
|
{
|
||||||
await CheckForSubscription(shouldHide, tv);
|
await CheckForSubscription(shouldHide, tv);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,6 @@
|
||||||
public const string RequestTv = nameof(RequestTv);
|
public const string RequestTv = nameof(RequestTv);
|
||||||
public const string RequestMovie = nameof(RequestMovie);
|
public const string RequestMovie = nameof(RequestMovie);
|
||||||
public const string Disabled = nameof(Disabled);
|
public const string Disabled = nameof(Disabled);
|
||||||
public const string RecievesNewsletter = nameof(RecievesNewsletter);
|
public const string ReceivesNewsletter = nameof(ReceivesNewsletter);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -182,14 +182,16 @@
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
{@RECENTLYADDED}
|
{@RECENTLYADDED}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
<!-- END MAIN CONTENT AREA -->
|
<!-- END MAIN CONTENT AREA -->
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<!-- START FOOTER -->
|
<!-- START FOOTER -->
|
||||||
<div class="footer" style="clear: both; padding-top: 10px; text-align: center; width: 100%;">
|
<div class="footer" style="clear: both; padding-top: 10px; text-align: center; width: 100%;">
|
||||||
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;" width="100%">
|
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="content-block powered-by" style="font-family: sans-serif; vertical-align: top; padding-top: 10px; padding-bottom: 10px; font-size: 12px; color: #999999; text-align: center;" valign="top" align="center">
|
<td class="content-block powered-by" style="font-family: sans-serif; vertical-align: top; padding-top: 10px; padding-bottom: 10px; font-size: 12px; color: #999999; text-align: center;" valign="top" align="center">
|
||||||
Powered by <a href="https://github.com/tidusjar/Ombi" style="color: #999999; font-size: 12px; text-align: center; text-decoration: underline;">Ombi</a>
|
Powered by <a href="https://github.com/tidusjar/Ombi" style="color: #999999; font-size: 12px; text-align: center; text-decoration: underline;">Ombi</a>
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
<PackageReference Include="Nunit" Version="3.8.1" />
|
<PackageReference Include="Nunit" Version="3.8.1" />
|
||||||
<PackageReference Include="NUnit.ConsoleRunner" Version="3.7.0" />
|
<PackageReference Include="NUnit.ConsoleRunner" Version="3.7.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="3.8.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="3.8.0" />
|
||||||
<packagereference Include="Microsoft.NET.Test.Sdk" Version="15.7.0"></packagereference>
|
<packagereference Include="Microsoft.NET.Test.Sdk" Version="15.7.2"></packagereference>
|
||||||
<PackageReference Include="Moq" Version="4.7.99" />
|
<PackageReference Include="Moq" Version="4.7.99" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
@ -264,13 +264,13 @@ namespace Ombi.Notifications.Agents
|
||||||
? MovieRequest?.RequestedUser?.NotificationUserIds
|
? MovieRequest?.RequestedUser?.NotificationUserIds
|
||||||
: TvRequest?.RequestedUser?.NotificationUserIds;
|
: TvRequest?.RequestedUser?.NotificationUserIds;
|
||||||
}
|
}
|
||||||
if (model.UserId.HasValue() && !notificationIds.Any())
|
if (model.UserId.HasValue() && (!notificationIds?.Any() ?? true))
|
||||||
{
|
{
|
||||||
var user= _userManager.Users.Include(x => x.NotificationUserIds).FirstOrDefault(x => x.Id == model.UserId);
|
var user= _userManager.Users.Include(x => x.NotificationUserIds).FirstOrDefault(x => x.Id == model.UserId);
|
||||||
notificationIds = user.NotificationUserIds;
|
notificationIds = user.NotificationUserIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!notificationIds.Any())
|
if (!notificationIds?.Any() ?? true)
|
||||||
{
|
{
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
$"there are no admins to send a notification for {type}, for agent {NotificationAgent.Mobile}");
|
$"there are no admins to send a notification for {type}, for agent {NotificationAgent.Mobile}");
|
||||||
|
|
|
@ -13,7 +13,6 @@ namespace Ombi.Notifications
|
||||||
{
|
{
|
||||||
public class NotificationMessageCurlys
|
public class NotificationMessageCurlys
|
||||||
{
|
{
|
||||||
|
|
||||||
public void Setup(NotificationOptions opts, FullBaseRequest req, CustomizationSettings s)
|
public void Setup(NotificationOptions opts, FullBaseRequest req, CustomizationSettings s)
|
||||||
{
|
{
|
||||||
LoadIssues(opts);
|
LoadIssues(opts);
|
||||||
|
@ -44,8 +43,17 @@ namespace Ombi.Notifications
|
||||||
}
|
}
|
||||||
Overview = req?.Overview;
|
Overview = req?.Overview;
|
||||||
Year = req?.ReleaseDate.Year.ToString();
|
Year = req?.ReleaseDate.Year.ToString();
|
||||||
PosterImage = req?.RequestType == RequestType.Movie ?
|
|
||||||
string.Format("https://image.tmdb.org/t/p/w300{0}", req?.PosterPath) : req?.PosterPath;
|
if (req?.RequestType == RequestType.Movie)
|
||||||
|
{
|
||||||
|
PosterImage = string.Format((req?.PosterPath ?? string.Empty).StartsWith("/", StringComparison.InvariantCultureIgnoreCase)
|
||||||
|
? "https://image.tmdb.org/t/p/w300{0}" : "https://image.tmdb.org/t/p/w300/{0}", req?.PosterPath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PosterImage = req?.PosterPath;
|
||||||
|
}
|
||||||
|
|
||||||
AdditionalInformation = opts?.AdditionalInformation ?? string.Empty;
|
AdditionalInformation = opts?.AdditionalInformation ?? string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,8 +96,15 @@ namespace Ombi.Notifications
|
||||||
|
|
||||||
Overview = req?.ParentRequest.Overview;
|
Overview = req?.ParentRequest.Overview;
|
||||||
Year = req?.ParentRequest.ReleaseDate.Year.ToString();
|
Year = req?.ParentRequest.ReleaseDate.Year.ToString();
|
||||||
PosterImage = req?.RequestType == RequestType.Movie ?
|
if (req?.RequestType == RequestType.Movie)
|
||||||
$"https://image.tmdb.org/t/p/w300{req?.ParentRequest.PosterPath}" : req?.ParentRequest.PosterPath;
|
{
|
||||||
|
PosterImage = string.Format((req?.ParentRequest.PosterPath ?? string.Empty).StartsWith("/", StringComparison.InvariantCultureIgnoreCase)
|
||||||
|
? "https://image.tmdb.org/t/p/w300{0}" : "https://image.tmdb.org/t/p/w300/{0}", req?.ParentRequest.PosterPath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PosterImage = req?.ParentRequest.PosterPath;
|
||||||
|
}
|
||||||
AdditionalInformation = opts.AdditionalInformation;
|
AdditionalInformation = opts.AdditionalInformation;
|
||||||
// DO Episode and Season Lists
|
// DO Episode and Season Lists
|
||||||
|
|
||||||
|
@ -133,6 +148,8 @@ namespace Ombi.Notifications
|
||||||
ApplicationUrl = (s?.ApplicationUrl.HasValue() ?? false) ? s.ApplicationUrl : string.Empty;
|
ApplicationUrl = (s?.ApplicationUrl.HasValue() ?? false) ? s.ApplicationUrl : string.Empty;
|
||||||
ApplicationName = string.IsNullOrEmpty(s?.ApplicationName) ? "Ombi" : s?.ApplicationName;
|
ApplicationName = string.IsNullOrEmpty(s?.ApplicationName) ? "Ombi" : s?.ApplicationName;
|
||||||
RequestedUser = user.UserName;
|
RequestedUser = user.UserName;
|
||||||
|
Alias = user.UserAlias;
|
||||||
|
UserName = user.UserName;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LoadIssues(NotificationOptions opts)
|
private void LoadIssues(NotificationOptions opts)
|
||||||
|
|
|
@ -26,7 +26,7 @@ namespace Ombi.Schedule.Tests
|
||||||
_tv = new Mock<ITvRequestRepository>();
|
_tv = new Mock<ITvRequestRepository>();
|
||||||
_movie = new Mock<IMovieRequestRepository>();
|
_movie = new Mock<IMovieRequestRepository>();
|
||||||
_notify = new Mock<INotificationService>();
|
_notify = new Mock<INotificationService>();
|
||||||
Checker = new PlexAvailabilityChecker(_repo.Object, _tv.Object, _movie.Object, _notify.Object, new Mock<IBackgroundJobClient>().Object);
|
Checker = new PlexAvailabilityChecker(_repo.Object, _tv.Object, _movie.Object, _notify.Object, new Mock<IBackgroundJobClient>().Object, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -75,34 +75,15 @@ namespace Ombi.Schedule.Jobs.Emby
|
||||||
var mediaToAdd = new HashSet<EmbyContent>();
|
var mediaToAdd = new HashSet<EmbyContent>();
|
||||||
foreach (var movie in movies.Items)
|
foreach (var movie in movies.Items)
|
||||||
{
|
{
|
||||||
if (movie.Type.Equals("boxset", StringComparison.CurrentCultureIgnoreCase))
|
// Regular movie
|
||||||
{
|
await ProcessMovies(movie, mediaToAdd);
|
||||||
var movieInfo =
|
|
||||||
await _api.GetCollection(movie.Id, server.ApiKey, server.AdministratorId, server.FullUri);
|
|
||||||
foreach (var item in movieInfo.Items)
|
|
||||||
{
|
|
||||||
var info = await _api.GetMovieInformation(item.Id, server.ApiKey,
|
|
||||||
server.AdministratorId, server.FullUri);
|
|
||||||
await ProcessMovies(info, mediaToAdd);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Regular movie
|
|
||||||
var movieInfo = await _api.GetMovieInformation(movie.Id, server.ApiKey,
|
|
||||||
server.AdministratorId, server.FullUri);
|
|
||||||
|
|
||||||
await ProcessMovies(movieInfo, mediaToAdd);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// TV Time
|
// TV Time
|
||||||
var tv = await _api.GetAllShows(server.ApiKey, server.AdministratorId, server.FullUri);
|
var tv = await _api.GetAllShows(server.ApiKey, server.AdministratorId, server.FullUri);
|
||||||
|
|
||||||
foreach (var tvShow in tv.Items)
|
foreach (var tvShow in tv.Items)
|
||||||
{
|
{
|
||||||
var tvInfo = await _api.GetSeriesInformation(tvShow.Id, server.ApiKey, server.AdministratorId,
|
if (string.IsNullOrEmpty(tvShow.ProviderIds?.Tvdb))
|
||||||
server.FullUri);
|
|
||||||
if (string.IsNullOrEmpty(tvInfo.ProviderIds?.Tvdb))
|
|
||||||
{
|
{
|
||||||
Log.Error("Provider Id on tv {0} is null", tvShow.Name);
|
Log.Error("Provider Id on tv {0} is null", tvShow.Name);
|
||||||
continue;
|
continue;
|
||||||
|
@ -112,10 +93,10 @@ namespace Ombi.Schedule.Jobs.Emby
|
||||||
if (existingTv == null)
|
if (existingTv == null)
|
||||||
mediaToAdd.Add(new EmbyContent
|
mediaToAdd.Add(new EmbyContent
|
||||||
{
|
{
|
||||||
TvDbId = tvInfo.ProviderIds?.Tvdb,
|
TvDbId = tvShow.ProviderIds?.Tvdb,
|
||||||
ImdbId = tvInfo.ProviderIds?.Imdb,
|
ImdbId = tvShow.ProviderIds?.Imdb,
|
||||||
TheMovieDbId = tvInfo.ProviderIds?.Tmdb,
|
TheMovieDbId = tvShow.ProviderIds?.Tmdb,
|
||||||
Title = tvInfo.Name,
|
Title = tvShow.Name,
|
||||||
Type = EmbyMediaType.Series,
|
Type = EmbyMediaType.Series,
|
||||||
EmbyId = tvShow.Id,
|
EmbyId = tvShow.Id,
|
||||||
Url = EmbyHelper.GetEmbyMediaUrl(tvShow.Id),
|
Url = EmbyHelper.GetEmbyMediaUrl(tvShow.Id),
|
||||||
|
@ -127,7 +108,7 @@ namespace Ombi.Schedule.Jobs.Emby
|
||||||
await _repo.AddRange(mediaToAdd);
|
await _repo.AddRange(mediaToAdd);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ProcessMovies(MovieInformation movieInfo, ICollection<EmbyContent> content)
|
private async Task ProcessMovies(EmbyMovie movieInfo, ICollection<EmbyContent> content)
|
||||||
{
|
{
|
||||||
// Check if it exists
|
// Check if it exists
|
||||||
var existingMovie = await _repo.GetByEmbyId(movieInfo.Id);
|
var existingMovie = await _repo.GetByEmbyId(movieInfo.Id);
|
||||||
|
|
|
@ -78,21 +78,9 @@ namespace Ombi.Schedule.Jobs.Emby
|
||||||
|
|
||||||
foreach (var ep in allEpisodes.Items)
|
foreach (var ep in allEpisodes.Items)
|
||||||
{
|
{
|
||||||
if (ep.LocationType.Equals("Virtual", StringComparison.CurrentCultureIgnoreCase))
|
|
||||||
{
|
|
||||||
// This means that we don't actully have the file, it's just Emby being nice and showing future stuff
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var epInfo = await _api.GetEpisodeInformation(ep.Id, server.ApiKey, server.AdministratorId, server.FullUri);
|
|
||||||
//if (epInfo?.ProviderIds?.Tvdb == null)
|
|
||||||
//{
|
|
||||||
// continue;
|
|
||||||
//}
|
|
||||||
|
|
||||||
// Let's make sure we have the parent request, stop those pesky forign key errors,
|
// Let's make sure we have the parent request, stop those pesky forign key errors,
|
||||||
// Damn me having data integrity
|
// Damn me having data integrity
|
||||||
var parent = await _repo.GetByEmbyId(epInfo.SeriesId);
|
var parent = await _repo.GetByEmbyId(ep.SeriesId);
|
||||||
if (parent == null)
|
if (parent == null)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("The episode {0} does not relate to a series, so we cannot save this", ep.Name);
|
_logger.LogInformation("The episode {0} does not relate to a series, so we cannot save this", ep.Name);
|
||||||
|
@ -109,9 +97,9 @@ namespace Ombi.Schedule.Jobs.Emby
|
||||||
EpisodeNumber = ep.IndexNumber,
|
EpisodeNumber = ep.IndexNumber,
|
||||||
SeasonNumber = ep.ParentIndexNumber,
|
SeasonNumber = ep.ParentIndexNumber,
|
||||||
ParentId = ep.SeriesId,
|
ParentId = ep.SeriesId,
|
||||||
TvDbId = epInfo.ProviderIds.Tvdb,
|
TvDbId = ep.ProviderIds.Tvdb,
|
||||||
TheMovieDbId = epInfo.ProviderIds.Tmdb,
|
TheMovieDbId = ep.ProviderIds.Tmdb,
|
||||||
ImdbId = epInfo.ProviderIds.Imdb,
|
ImdbId = ep.ProviderIds.Imdb,
|
||||||
Title = ep.Name,
|
Title = ep.Name,
|
||||||
AddedAt = DateTime.UtcNow
|
AddedAt = DateTime.UtcNow
|
||||||
});
|
});
|
||||||
|
|
|
@ -131,7 +131,7 @@ namespace Ombi.Schedule.Jobs.Ombi
|
||||||
if (!test)
|
if (!test)
|
||||||
{
|
{
|
||||||
// Get the users to send it to
|
// Get the users to send it to
|
||||||
var users = await _userManager.GetUsersInRoleAsync(OmbiRoles.RecievesNewsletter);
|
var users = await _userManager.GetUsersInRoleAsync(OmbiRoles.ReceivesNewsletter);
|
||||||
if (!users.Any())
|
if (!users.Any())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Hangfire;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Ombi.Api.TheMovieDb;
|
using Ombi.Api.TheMovieDb;
|
||||||
using Ombi.Api.TheMovieDb.Models;
|
using Ombi.Api.TheMovieDb.Models;
|
||||||
|
@ -9,6 +10,8 @@ using Ombi.Api.TvMaze;
|
||||||
using Ombi.Core.Settings;
|
using Ombi.Core.Settings;
|
||||||
using Ombi.Core.Settings.Models.External;
|
using Ombi.Core.Settings.Models.External;
|
||||||
using Ombi.Helpers;
|
using Ombi.Helpers;
|
||||||
|
using Ombi.Schedule.Jobs.Emby;
|
||||||
|
using Ombi.Schedule.Jobs.Plex;
|
||||||
using Ombi.Store.Entities;
|
using Ombi.Store.Entities;
|
||||||
using Ombi.Store.Repository;
|
using Ombi.Store.Repository;
|
||||||
|
|
||||||
|
@ -18,7 +21,7 @@ namespace Ombi.Schedule.Jobs.Ombi
|
||||||
{
|
{
|
||||||
public RefreshMetadata(IPlexContentRepository plexRepo, IEmbyContentRepository embyRepo,
|
public RefreshMetadata(IPlexContentRepository plexRepo, IEmbyContentRepository embyRepo,
|
||||||
ILogger<RefreshMetadata> log, ITvMazeApi tvApi, ISettingsService<PlexSettings> plexSettings,
|
ILogger<RefreshMetadata> log, ITvMazeApi tvApi, ISettingsService<PlexSettings> plexSettings,
|
||||||
IMovieDbApi movieApi, ISettingsService<EmbySettings> embySettings)
|
IMovieDbApi movieApi, ISettingsService<EmbySettings> embySettings, IPlexAvailabilityChecker plexAvailability, IEmbyAvaliabilityChecker embyAvaliability)
|
||||||
{
|
{
|
||||||
_plexRepo = plexRepo;
|
_plexRepo = plexRepo;
|
||||||
_embyRepo = embyRepo;
|
_embyRepo = embyRepo;
|
||||||
|
@ -27,10 +30,14 @@ namespace Ombi.Schedule.Jobs.Ombi
|
||||||
_tvApi = tvApi;
|
_tvApi = tvApi;
|
||||||
_plexSettings = plexSettings;
|
_plexSettings = plexSettings;
|
||||||
_embySettings = embySettings;
|
_embySettings = embySettings;
|
||||||
|
_plexAvailabilityChecker = plexAvailability;
|
||||||
|
_embyAvaliabilityChecker = embyAvaliability;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly IPlexContentRepository _plexRepo;
|
private readonly IPlexContentRepository _plexRepo;
|
||||||
private readonly IEmbyContentRepository _embyRepo;
|
private readonly IEmbyContentRepository _embyRepo;
|
||||||
|
private readonly IPlexAvailabilityChecker _plexAvailabilityChecker;
|
||||||
|
private readonly IEmbyAvaliabilityChecker _embyAvaliabilityChecker;
|
||||||
private readonly ILogger _log;
|
private readonly ILogger _log;
|
||||||
private readonly IMovieDbApi _movieApi;
|
private readonly IMovieDbApi _movieApi;
|
||||||
private readonly ITvMazeApi _tvApi;
|
private readonly ITvMazeApi _tvApi;
|
||||||
|
@ -64,10 +71,11 @@ namespace Ombi.Schedule.Jobs.Ombi
|
||||||
public async Task ProcessPlexServerContent(IEnumerable<int> contentIds)
|
public async Task ProcessPlexServerContent(IEnumerable<int> contentIds)
|
||||||
{
|
{
|
||||||
_log.LogInformation("Starting the Metadata refresh from RecentlyAddedSync");
|
_log.LogInformation("Starting the Metadata refresh from RecentlyAddedSync");
|
||||||
|
var plexSettings = await _plexSettings.GetSettingsAsync();
|
||||||
|
var embySettings = await _embySettings.GetSettingsAsync();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var settings = await _plexSettings.GetSettingsAsync();
|
if (plexSettings.Enable)
|
||||||
if (settings.Enable)
|
|
||||||
{
|
{
|
||||||
await StartPlexWithKnownContent(contentIds);
|
await StartPlexWithKnownContent(contentIds);
|
||||||
}
|
}
|
||||||
|
@ -77,6 +85,19 @@ namespace Ombi.Schedule.Jobs.Ombi
|
||||||
_log.LogError(e, "Exception when refreshing the Plex Metadata");
|
_log.LogError(e, "Exception when refreshing the Plex Metadata");
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (plexSettings.Enable)
|
||||||
|
{
|
||||||
|
BackgroundJob.Enqueue(() => _plexAvailabilityChecker.Start());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (embySettings.Enable)
|
||||||
|
{
|
||||||
|
BackgroundJob.Enqueue(() => _embyAvaliabilityChecker.Start());
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task StartPlexWithKnownContent(IEnumerable<int> contentids)
|
private async Task StartPlexWithKnownContent(IEnumerable<int> contentids)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Threading.Tasks;
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Ombi.Schedule.Jobs.Plex
|
namespace Ombi.Schedule.Jobs.Plex
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Ombi.Api.Plex.Models;
|
using Ombi.Api.Plex.Models;
|
||||||
|
@ -9,6 +10,6 @@ namespace Ombi.Schedule.Jobs.Plex.Interfaces
|
||||||
public interface IPlexEpisodeSync : IBaseJob
|
public interface IPlexEpisodeSync : IBaseJob
|
||||||
{
|
{
|
||||||
Task Start();
|
Task Start();
|
||||||
Task ProcessEpsiodes(Metadata[] episodes, IQueryable<PlexEpisode> currentEpisodes);
|
Task<HashSet<PlexEpisode>> ProcessEpsiodes(Metadata[] episodes, IQueryable<PlexEpisode> currentEpisodes);
|
||||||
}
|
}
|
||||||
}
|
}
|
14
src/Ombi.Schedule/Jobs/Plex/Models/ProcessedContent.cs
Normal file
14
src/Ombi.Schedule/Jobs/Plex/Models/ProcessedContent.cs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Ombi.Schedule.Jobs.Plex.Models
|
||||||
|
{
|
||||||
|
public class ProcessedContent
|
||||||
|
{
|
||||||
|
public IEnumerable<int> Content { get; set; }
|
||||||
|
public IEnumerable<int> Episodes { get; set; }
|
||||||
|
|
||||||
|
public bool HasProcessedContent => Content.Any();
|
||||||
|
public bool HasProcessedEpisodes => Episodes.Any();
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,10 +4,12 @@ using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Hangfire;
|
using Hangfire;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Ombi.Core.Notifications;
|
using Ombi.Core.Notifications;
|
||||||
using Ombi.Helpers;
|
using Ombi.Helpers;
|
||||||
using Ombi.Notifications.Models;
|
using Ombi.Notifications.Models;
|
||||||
using Ombi.Store.Entities;
|
using Ombi.Store.Entities;
|
||||||
|
using Ombi.Store.Entities.Requests;
|
||||||
using Ombi.Store.Repository;
|
using Ombi.Store.Repository;
|
||||||
using Ombi.Store.Repository.Requests;
|
using Ombi.Store.Repository.Requests;
|
||||||
|
|
||||||
|
@ -16,13 +18,14 @@ namespace Ombi.Schedule.Jobs.Plex
|
||||||
public class PlexAvailabilityChecker : IPlexAvailabilityChecker
|
public class PlexAvailabilityChecker : IPlexAvailabilityChecker
|
||||||
{
|
{
|
||||||
public PlexAvailabilityChecker(IPlexContentRepository repo, ITvRequestRepository tvRequest, IMovieRequestRepository movies,
|
public PlexAvailabilityChecker(IPlexContentRepository repo, ITvRequestRepository tvRequest, IMovieRequestRepository movies,
|
||||||
INotificationService notification, IBackgroundJobClient background)
|
INotificationService notification, IBackgroundJobClient background, ILogger<PlexAvailabilityChecker> log)
|
||||||
{
|
{
|
||||||
_tvRepo = tvRequest;
|
_tvRepo = tvRequest;
|
||||||
_repo = repo;
|
_repo = repo;
|
||||||
_movieRepo = movies;
|
_movieRepo = movies;
|
||||||
_notificationService = notification;
|
_notificationService = notification;
|
||||||
_backgroundJobClient = background;
|
_backgroundJobClient = background;
|
||||||
|
_log = log;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly ITvRequestRepository _tvRepo;
|
private readonly ITvRequestRepository _tvRepo;
|
||||||
|
@ -30,16 +33,29 @@ namespace Ombi.Schedule.Jobs.Plex
|
||||||
private readonly IPlexContentRepository _repo;
|
private readonly IPlexContentRepository _repo;
|
||||||
private readonly INotificationService _notificationService;
|
private readonly INotificationService _notificationService;
|
||||||
private readonly IBackgroundJobClient _backgroundJobClient;
|
private readonly IBackgroundJobClient _backgroundJobClient;
|
||||||
|
private readonly ILogger _log;
|
||||||
|
|
||||||
public async Task Start()
|
public async Task Start()
|
||||||
{
|
{
|
||||||
await ProcessMovies();
|
try
|
||||||
await ProcessTv();
|
{
|
||||||
|
await ProcessMovies();
|
||||||
|
await ProcessTv();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_log.LogError(e, "Exception thrown in Plex availbility checker");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ProcessTv()
|
private Task ProcessTv()
|
||||||
{
|
{
|
||||||
var tv = _tvRepo.GetChild().Where(x => !x.Available);
|
var tv = _tvRepo.GetChild().Where(x => !x.Available);
|
||||||
|
return ProcessTv(tv);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProcessTv(IQueryable<ChildRequests> tv)
|
||||||
|
{
|
||||||
var plexEpisodes = _repo.GetAllEpisodes().Include(x => x.Series);
|
var plexEpisodes = _repo.GetAllEpisodes().Include(x => x.Series);
|
||||||
|
|
||||||
foreach (var child in tv)
|
foreach (var child in tv)
|
||||||
|
@ -81,6 +97,10 @@ namespace Ombi.Schedule.Jobs.Plex
|
||||||
{
|
{
|
||||||
foreach (var episode in season.Episodes)
|
foreach (var episode in season.Episodes)
|
||||||
{
|
{
|
||||||
|
if (episode.Available)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
var foundEp = await seriesEpisodes.FirstOrDefaultAsync(
|
var foundEp = await seriesEpisodes.FirstOrDefaultAsync(
|
||||||
x => x.EpisodeNumber == episode.EpisodeNumber &&
|
x => x.EpisodeNumber == episode.EpisodeNumber &&
|
||||||
x.SeasonNumber == episode.Season.SeasonNumber);
|
x.SeasonNumber == episode.Season.SeasonNumber);
|
||||||
|
@ -102,7 +122,7 @@ namespace Ombi.Schedule.Jobs.Plex
|
||||||
{
|
{
|
||||||
DateTime = DateTime.Now,
|
DateTime = DateTime.Now,
|
||||||
NotificationType = NotificationType.RequestAvailable,
|
NotificationType = NotificationType.RequestAvailable,
|
||||||
RequestId = child.ParentRequestId,
|
RequestId = child.Id,
|
||||||
RequestType = RequestType.TvShow,
|
RequestType = RequestType.TvShow,
|
||||||
Recipient = child.RequestedUser.Email
|
Recipient = child.RequestedUser.Email
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -39,6 +39,7 @@ using Ombi.Core.Settings.Models.External;
|
||||||
using Ombi.Helpers;
|
using Ombi.Helpers;
|
||||||
using Ombi.Schedule.Jobs.Ombi;
|
using Ombi.Schedule.Jobs.Ombi;
|
||||||
using Ombi.Schedule.Jobs.Plex.Interfaces;
|
using Ombi.Schedule.Jobs.Plex.Interfaces;
|
||||||
|
using Ombi.Schedule.Jobs.Plex.Models;
|
||||||
using Ombi.Store.Entities;
|
using Ombi.Store.Entities;
|
||||||
using Ombi.Store.Repository;
|
using Ombi.Store.Repository;
|
||||||
|
|
||||||
|
@ -47,7 +48,7 @@ namespace Ombi.Schedule.Jobs.Plex
|
||||||
public class PlexContentSync : IPlexContentSync
|
public class PlexContentSync : IPlexContentSync
|
||||||
{
|
{
|
||||||
public PlexContentSync(ISettingsService<PlexSettings> plex, IPlexApi plexApi, ILogger<PlexContentSync> logger, IPlexContentRepository repo,
|
public PlexContentSync(ISettingsService<PlexSettings> plex, IPlexApi plexApi, ILogger<PlexContentSync> logger, IPlexContentRepository repo,
|
||||||
IPlexEpisodeSync epsiodeSync, IRefreshMetadata metadataRefresh)
|
IPlexEpisodeSync epsiodeSync, IRefreshMetadata metadataRefresh, IPlexAvailabilityChecker checker)
|
||||||
{
|
{
|
||||||
Plex = plex;
|
Plex = plex;
|
||||||
PlexApi = plexApi;
|
PlexApi = plexApi;
|
||||||
|
@ -55,6 +56,7 @@ namespace Ombi.Schedule.Jobs.Plex
|
||||||
Repo = repo;
|
Repo = repo;
|
||||||
EpisodeSync = epsiodeSync;
|
EpisodeSync = epsiodeSync;
|
||||||
Metadata = metadataRefresh;
|
Metadata = metadataRefresh;
|
||||||
|
Checker = checker;
|
||||||
plex.ClearCache();
|
plex.ClearCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,6 +66,7 @@ namespace Ombi.Schedule.Jobs.Plex
|
||||||
private IPlexContentRepository Repo { get; }
|
private IPlexContentRepository Repo { get; }
|
||||||
private IPlexEpisodeSync EpisodeSync { get; }
|
private IPlexEpisodeSync EpisodeSync { get; }
|
||||||
private IRefreshMetadata Metadata { get; }
|
private IRefreshMetadata Metadata { get; }
|
||||||
|
private IPlexAvailabilityChecker Checker { get; }
|
||||||
|
|
||||||
public async Task CacheContent(bool recentlyAddedSearch = false)
|
public async Task CacheContent(bool recentlyAddedSearch = false)
|
||||||
{
|
{
|
||||||
|
@ -77,17 +80,13 @@ namespace Ombi.Schedule.Jobs.Plex
|
||||||
Logger.LogError("Plex Settings are not valid");
|
Logger.LogError("Plex Settings are not valid");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var processedContent = new HashSet<int>();
|
var processedContent = new ProcessedContent();
|
||||||
Logger.LogInformation("Starting Plex Content Cacher");
|
Logger.LogInformation("Starting Plex Content Cacher");
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (recentlyAddedSearch)
|
if (recentlyAddedSearch)
|
||||||
{
|
{
|
||||||
var result = await StartTheCache(plexSettings, true);
|
processedContent = await StartTheCache(plexSettings, true);
|
||||||
foreach (var r in result)
|
|
||||||
{
|
|
||||||
processedContent.Add(r);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -105,31 +104,32 @@ namespace Ombi.Schedule.Jobs.Plex
|
||||||
BackgroundJob.Enqueue(() => EpisodeSync.Start());
|
BackgroundJob.Enqueue(() => EpisodeSync.Start());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (processedContent.Any() && recentlyAddedSearch)
|
if ((processedContent?.HasProcessedContent ?? false) && recentlyAddedSearch)
|
||||||
{
|
{
|
||||||
// Just check what we send it
|
// Just check what we send it
|
||||||
BackgroundJob.Enqueue(() => Metadata.ProcessPlexServerContent(processedContent));
|
BackgroundJob.Enqueue(() => Metadata.ProcessPlexServerContent(processedContent.Content));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((processedContent?.HasProcessedEpisodes ?? false) && recentlyAddedSearch)
|
||||||
|
{
|
||||||
|
BackgroundJob.Enqueue(() => Checker.Start());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<IEnumerable<int>> StartTheCache(PlexSettings plexSettings, bool recentlyAddedSearch)
|
private async Task<ProcessedContent> StartTheCache(PlexSettings plexSettings, bool recentlyAddedSearch)
|
||||||
{
|
{
|
||||||
var processedContent = new HashSet<int>();
|
var processedContent = new ProcessedContent();
|
||||||
foreach (var servers in plexSettings.Servers ?? new List<PlexServers>())
|
foreach (var servers in plexSettings.Servers ?? new List<PlexServers>())
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Logger.LogInformation("Starting to cache the content on server {0}", servers.Name);
|
Logger.LogInformation("Starting to cache the content on server {0}", servers.Name);
|
||||||
|
|
||||||
if (recentlyAddedSearch)
|
if (recentlyAddedSearch)
|
||||||
{
|
{
|
||||||
// If it's recently added search then we want the results to pass to the metadata job
|
// If it's recently added search then we want the results to pass to the metadata job
|
||||||
// This way the metadata job is smaller in size to process, it only need to look at newly added shit
|
// This way the metadata job is smaller in size to process, it only need to look at newly added shit
|
||||||
var result = await ProcessServer(servers, true);
|
return await ProcessServer(servers, true);
|
||||||
foreach (var plexServerContent in result)
|
|
||||||
{
|
|
||||||
processedContent.Add(plexServerContent);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -145,12 +145,14 @@ namespace Ombi.Schedule.Jobs.Plex
|
||||||
return processedContent;
|
return processedContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<IEnumerable<int>> ProcessServer(PlexServers servers, bool recentlyAddedSearch)
|
private async Task<ProcessedContent> ProcessServer(PlexServers servers, bool recentlyAddedSearch)
|
||||||
{
|
{
|
||||||
var processedContent = new HashSet<int>();
|
var retVal = new ProcessedContent();
|
||||||
Logger.LogInformation("Getting all content from server {0}", servers.Name);
|
var contentProcessed = new Dictionary<int, int>();
|
||||||
|
var episodesProcessed = new List<int>();
|
||||||
|
Logger.LogDebug("Getting all content from server {0}", servers.Name);
|
||||||
var allContent = await GetAllContent(servers, recentlyAddedSearch);
|
var allContent = await GetAllContent(servers, recentlyAddedSearch);
|
||||||
Logger.LogInformation("We found {0} items", allContent.Count);
|
Logger.LogDebug("We found {0} items", allContent.Count);
|
||||||
|
|
||||||
// Let's now process this.
|
// Let's now process this.
|
||||||
var contentToAdd = new HashSet<PlexServerContent>();
|
var contentToAdd = new HashSet<PlexServerContent>();
|
||||||
|
@ -161,21 +163,21 @@ namespace Ombi.Schedule.Jobs.Plex
|
||||||
{
|
{
|
||||||
if (content.viewGroup.Equals(PlexMediaType.Episode.ToString(), StringComparison.CurrentCultureIgnoreCase))
|
if (content.viewGroup.Equals(PlexMediaType.Episode.ToString(), StringComparison.CurrentCultureIgnoreCase))
|
||||||
{
|
{
|
||||||
Logger.LogInformation("Found some episodes, this must be a recently added sync");
|
Logger.LogDebug("Found some episodes, this must be a recently added sync");
|
||||||
var count = 0;
|
var count = 0;
|
||||||
foreach (var epInfo in content.Metadata)
|
foreach (var epInfo in content.Metadata ?? new Metadata[]{})
|
||||||
{
|
{
|
||||||
count++;
|
count++;
|
||||||
var grandParentKey = epInfo.grandparentRatingKey;
|
var grandParentKey = epInfo.grandparentRatingKey;
|
||||||
// Lookup the rating key
|
// Lookup the rating key
|
||||||
var showMetadata = await PlexApi.GetMetadata(servers.PlexAuthToken, servers.FullUri, grandParentKey);
|
var showMetadata = await PlexApi.GetMetadata(servers.PlexAuthToken, servers.FullUri, grandParentKey);
|
||||||
var show = showMetadata.MediaContainer.Metadata.FirstOrDefault();
|
var show = showMetadata.MediaContainer.Metadata.FirstOrDefault();
|
||||||
if(show == null)
|
if (show == null)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await ProcessTvShow(servers, show, contentToAdd, recentlyAddedSearch, processedContent);
|
await ProcessTvShow(servers, show, contentToAdd, contentProcessed);
|
||||||
if (contentToAdd.Any())
|
if (contentToAdd.Any())
|
||||||
{
|
{
|
||||||
await Repo.AddRange(contentToAdd, false);
|
await Repo.AddRange(contentToAdd, false);
|
||||||
|
@ -183,7 +185,7 @@ namespace Ombi.Schedule.Jobs.Plex
|
||||||
{
|
{
|
||||||
foreach (var plexServerContent in contentToAdd)
|
foreach (var plexServerContent in contentToAdd)
|
||||||
{
|
{
|
||||||
processedContent.Add(plexServerContent.Id);
|
contentProcessed.Add(plexServerContent.Id, plexServerContent.Key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
contentToAdd.Clear();
|
contentToAdd.Clear();
|
||||||
|
@ -197,18 +199,21 @@ namespace Ombi.Schedule.Jobs.Plex
|
||||||
|
|
||||||
// Save just to make sure we don't leave anything hanging
|
// Save just to make sure we don't leave anything hanging
|
||||||
await Repo.SaveChangesAsync();
|
await Repo.SaveChangesAsync();
|
||||||
|
if (content.Metadata != null)
|
||||||
await EpisodeSync.ProcessEpsiodes(content.Metadata, allEps);
|
{
|
||||||
|
var episodesAdded = await EpisodeSync.ProcessEpsiodes(content.Metadata, allEps);
|
||||||
|
episodesProcessed.AddRange(episodesAdded.Select(x => x.Id));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (content.viewGroup.Equals(PlexMediaType.Show.ToString(), StringComparison.CurrentCultureIgnoreCase))
|
if (content.viewGroup.Equals(PlexMediaType.Show.ToString(), StringComparison.CurrentCultureIgnoreCase))
|
||||||
{
|
{
|
||||||
// Process Shows
|
// Process Shows
|
||||||
Logger.LogInformation("Processing TV Shows");
|
Logger.LogDebug("Processing TV Shows");
|
||||||
var count = 0;
|
var count = 0;
|
||||||
foreach (var show in content.Metadata ?? new Metadata[] { })
|
foreach (var show in content.Metadata ?? new Metadata[] { })
|
||||||
{
|
{
|
||||||
count++;
|
count++;
|
||||||
await ProcessTvShow(servers, show, contentToAdd, recentlyAddedSearch, processedContent);
|
await ProcessTvShow(servers, show, contentToAdd, contentProcessed);
|
||||||
|
|
||||||
if (contentToAdd.Any())
|
if (contentToAdd.Any())
|
||||||
{
|
{
|
||||||
|
@ -217,7 +222,7 @@ namespace Ombi.Schedule.Jobs.Plex
|
||||||
{
|
{
|
||||||
foreach (var plexServerContent in contentToAdd)
|
foreach (var plexServerContent in contentToAdd)
|
||||||
{
|
{
|
||||||
processedContent.Add(plexServerContent.Id);
|
contentProcessed.Add(plexServerContent.Id, plexServerContent.Key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
contentToAdd.Clear();
|
contentToAdd.Clear();
|
||||||
|
@ -232,7 +237,7 @@ namespace Ombi.Schedule.Jobs.Plex
|
||||||
}
|
}
|
||||||
if (content.viewGroup.Equals(PlexMediaType.Movie.ToString(), StringComparison.CurrentCultureIgnoreCase))
|
if (content.viewGroup.Equals(PlexMediaType.Movie.ToString(), StringComparison.CurrentCultureIgnoreCase))
|
||||||
{
|
{
|
||||||
Logger.LogInformation("Processing Movies");
|
Logger.LogDebug("Processing Movies");
|
||||||
foreach (var movie in content?.Metadata ?? new Metadata[] { })
|
foreach (var movie in content?.Metadata ?? new Metadata[] { })
|
||||||
{
|
{
|
||||||
// Let's check if we have this movie
|
// Let's check if we have this movie
|
||||||
|
@ -246,7 +251,7 @@ namespace Ombi.Schedule.Jobs.Plex
|
||||||
//var existing = await Repo.GetByKey(movie.ratingKey);
|
//var existing = await Repo.GetByKey(movie.ratingKey);
|
||||||
if (existing != null)
|
if (existing != null)
|
||||||
{
|
{
|
||||||
Logger.LogInformation("We already have movie {0}", movie.title);
|
Logger.LogDebug("We already have movie {0}", movie.title);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -256,7 +261,7 @@ namespace Ombi.Schedule.Jobs.Plex
|
||||||
await Repo.Delete(hasSameKey);
|
await Repo.Delete(hasSameKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.LogInformation("Adding movie {0}", movie.title);
|
Logger.LogDebug("Adding movie {0}", movie.title);
|
||||||
var metaData = await PlexApi.GetMetadata(servers.PlexAuthToken, servers.FullUri,
|
var metaData = await PlexApi.GetMetadata(servers.PlexAuthToken, servers.FullUri,
|
||||||
movie.ratingKey);
|
movie.ratingKey);
|
||||||
var providerIds = PlexHelper.GetProviderIdFromPlexGuid(metaData.MediaContainer.Metadata
|
var providerIds = PlexHelper.GetProviderIdFromPlexGuid(metaData.MediaContainer.Metadata
|
||||||
|
@ -299,7 +304,7 @@ namespace Ombi.Schedule.Jobs.Plex
|
||||||
await Repo.AddRange(contentToAdd);
|
await Repo.AddRange(contentToAdd);
|
||||||
foreach (var c in contentToAdd)
|
foreach (var c in contentToAdd)
|
||||||
{
|
{
|
||||||
processedContent.Add(c.Id);
|
contentProcessed.Add(c.Id, c.Key);
|
||||||
}
|
}
|
||||||
contentToAdd.Clear();
|
contentToAdd.Clear();
|
||||||
}
|
}
|
||||||
|
@ -310,7 +315,7 @@ namespace Ombi.Schedule.Jobs.Plex
|
||||||
await Repo.AddRange(contentToAdd);
|
await Repo.AddRange(contentToAdd);
|
||||||
foreach (var c in contentToAdd)
|
foreach (var c in contentToAdd)
|
||||||
{
|
{
|
||||||
processedContent.Add(c.Id);
|
contentProcessed.Add(c.Id, c.Key);
|
||||||
}
|
}
|
||||||
contentToAdd.Clear();
|
contentToAdd.Clear();
|
||||||
}
|
}
|
||||||
|
@ -321,14 +326,16 @@ namespace Ombi.Schedule.Jobs.Plex
|
||||||
await Repo.AddRange(contentToAdd);
|
await Repo.AddRange(contentToAdd);
|
||||||
foreach (var c in contentToAdd)
|
foreach (var c in contentToAdd)
|
||||||
{
|
{
|
||||||
processedContent.Add(c.Id);
|
contentProcessed.Add(c.Id, c.Key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return processedContent;
|
retVal.Content = contentProcessed.Values;
|
||||||
|
retVal.Episodes = episodesProcessed;
|
||||||
|
return retVal;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ProcessTvShow(PlexServers servers, Metadata show, HashSet<PlexServerContent> contentToAdd, bool recentlyAdded, HashSet<int> contentProcessed)
|
private async Task ProcessTvShow(PlexServers servers, Metadata show, HashSet<PlexServerContent> contentToAdd, Dictionary<int, int> contentProcessed)
|
||||||
{
|
{
|
||||||
var seasonList = await PlexApi.GetSeasons(servers.PlexAuthToken, servers.FullUri,
|
var seasonList = await PlexApi.GetSeasons(servers.PlexAuthToken, servers.FullUri,
|
||||||
show.ratingKey);
|
show.ratingKey);
|
||||||
|
@ -345,7 +352,7 @@ namespace Ombi.Schedule.Jobs.Plex
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do we already have this item?
|
// Do we already have this item?
|
||||||
// Let's try and match
|
// Let's try and match
|
||||||
var existingContent = await Repo.GetFirstContentByCustom(x => x.Title == show.title
|
var existingContent = await Repo.GetFirstContentByCustom(x => x.Title == show.title
|
||||||
&& x.ReleaseYear == show.year.ToString()
|
&& x.ReleaseYear == show.year.ToString()
|
||||||
&& x.Type == PlexMediaTypeEntity.Show);
|
&& x.Type == PlexMediaTypeEntity.Show);
|
||||||
|
@ -366,6 +373,10 @@ namespace Ombi.Schedule.Jobs.Plex
|
||||||
await Repo.Delete(existingKey);
|
await Repo.Delete(existingKey);
|
||||||
existingKey = null;
|
existingKey = null;
|
||||||
}
|
}
|
||||||
|
else if(existingContent == null)
|
||||||
|
{
|
||||||
|
existingContent = await Repo.GetFirstContentByCustom(x => x.Key == show.ratingKey);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingContent != null)
|
if (existingContent != null)
|
||||||
|
@ -397,13 +408,20 @@ namespace Ombi.Schedule.Jobs.Plex
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also make sure it's not already being processed...
|
||||||
|
var alreadyProcessed = contentProcessed.Select(x => x.Value).Any(x => x == show.ratingKey);
|
||||||
|
if (alreadyProcessed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// The ratingKey keeps changing...
|
// The ratingKey keeps changing...
|
||||||
//var existingContent = await Repo.GetByKey(show.ratingKey);
|
//var existingContent = await Repo.GetByKey(show.ratingKey);
|
||||||
if (existingContent != null)
|
if (existingContent != null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Logger.LogInformation("We already have show {0} checking for new seasons",
|
Logger.LogDebug("We already have show {0} checking for new seasons",
|
||||||
existingContent.Title);
|
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;
|
||||||
|
@ -415,6 +433,26 @@ namespace Ombi.Schedule.Jobs.Plex
|
||||||
if (seasonExists != null)
|
if (seasonExists != null)
|
||||||
{
|
{
|
||||||
// We already have this season
|
// We already have this season
|
||||||
|
// check if we have the episode
|
||||||
|
//if (episode != null)
|
||||||
|
//{
|
||||||
|
// var existing = existingContent.Episodes.Any(x =>
|
||||||
|
// x.SeasonNumber == episode.parentIndex && x.EpisodeNumber == episode.index);
|
||||||
|
// if (!existing)
|
||||||
|
// {
|
||||||
|
// // We don't have this episode, lets add it
|
||||||
|
// existingContent.Episodes.Add(new PlexEpisode
|
||||||
|
// {
|
||||||
|
// EpisodeNumber = episode.index,
|
||||||
|
// SeasonNumber = episode.parentIndex,
|
||||||
|
// GrandparentKey = episode.grandparentRatingKey,
|
||||||
|
// ParentKey = episode.parentRatingKey,
|
||||||
|
// Key = episode.ratingKey,
|
||||||
|
// Title = episode.title
|
||||||
|
// });
|
||||||
|
// itemAdded = true;
|
||||||
|
// }
|
||||||
|
//}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -434,7 +472,7 @@ namespace Ombi.Schedule.Jobs.Plex
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Logger.LogInformation("New show {0}, so add it", show.title);
|
Logger.LogDebug("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...
|
||||||
|
@ -535,7 +573,7 @@ namespace Ombi.Schedule.Jobs.Plex
|
||||||
.Select(x => x.Key.ToString()).ToList();
|
.Select(x => x.Key.ToString()).ToList();
|
||||||
if (!keys.Contains(dir.key))
|
if (!keys.Contains(dir.key))
|
||||||
{
|
{
|
||||||
Logger.LogInformation("Lib {0} is not monitored, so skipping", dir.key);
|
Logger.LogDebug("Lib {0} is not monitored, so skipping", dir.key);
|
||||||
// We are not monitoring this lib
|
// We are not monitoring this lib
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
@ -129,7 +129,7 @@ namespace Ombi.Schedule.Jobs.Plex
|
||||||
await _repo.SaveChangesAsync();
|
await _repo.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ProcessEpsiodes(Metadata[] episodes, IQueryable<PlexEpisode> currentEpisodes)
|
public async Task<HashSet<PlexEpisode>> ProcessEpsiodes(Metadata[] episodes, IQueryable<PlexEpisode> currentEpisodes)
|
||||||
{
|
{
|
||||||
var ep = new HashSet<PlexEpisode>();
|
var ep = new HashSet<PlexEpisode>();
|
||||||
try
|
try
|
||||||
|
@ -179,6 +179,7 @@ namespace Ombi.Schedule.Jobs.Plex
|
||||||
}
|
}
|
||||||
|
|
||||||
await _repo.AddRange(ep);
|
await _repo.AddRange(ep);
|
||||||
|
return ep;
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
|
|
@ -117,12 +117,12 @@ namespace Ombi.Store.Context
|
||||||
Database.ExecuteSqlCommand("VACUUM;");
|
Database.ExecuteSqlCommand("VACUUM;");
|
||||||
|
|
||||||
// Make sure we have the roles
|
// Make sure we have the roles
|
||||||
var roles = Roles.Where(x => x.Name == OmbiRoles.RecievesNewsletter);
|
var roles = Roles.Where(x => x.Name == OmbiRoles.ReceivesNewsletter);
|
||||||
if (!roles.Any())
|
if (!roles.Any())
|
||||||
{
|
{
|
||||||
Roles.Add(new IdentityRole(OmbiRoles.RecievesNewsletter)
|
Roles.Add(new IdentityRole(OmbiRoles.ReceivesNewsletter)
|
||||||
{
|
{
|
||||||
NormalizedName = OmbiRoles.RecievesNewsletter.ToUpper()
|
NormalizedName = OmbiRoles.ReceivesNewsletter.ToUpper()
|
||||||
});
|
});
|
||||||
SaveChanges();
|
SaveChanges();
|
||||||
}
|
}
|
||||||
|
|
981
src/Ombi.Store/Migrations/20180613203443_RoleRename.Designer.cs
generated
Normal file
981
src/Ombi.Store/Migrations/20180613203443_RoleRename.Designer.cs
generated
Normal file
|
@ -0,0 +1,981 @@
|
||||||
|
// <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("20180613203443_RoleRename")]
|
||||||
|
partial class RoleRename
|
||||||
|
{
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "2.0.3-rtm-10026");
|
||||||
|
|
||||||
|
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>("ImdbId");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderId");
|
||||||
|
|
||||||
|
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("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>("ImdbId");
|
||||||
|
|
||||||
|
b.Property<string>("ParentId");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderId");
|
||||||
|
|
||||||
|
b.Property<int>("SeasonNumber");
|
||||||
|
|
||||||
|
b.Property<string>("TheMovieDbId");
|
||||||
|
|
||||||
|
b.Property<string>("Title");
|
||||||
|
|
||||||
|
b.Property<string>("TvDbId");
|
||||||
|
|
||||||
|
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.NotificationUserId", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<DateTime>("AddedAt");
|
||||||
|
|
||||||
|
b.Property<string>("PlayerId");
|
||||||
|
|
||||||
|
b.Property<string>("UserId");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("NotificationUserId");
|
||||||
|
});
|
||||||
|
|
||||||
|
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<int?>("EpisodeRequestLimit");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastLoggedIn");
|
||||||
|
|
||||||
|
b.Property<bool>("LockoutEnabled");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LockoutEnd");
|
||||||
|
|
||||||
|
b.Property<int?>("MovieRequestLimit");
|
||||||
|
|
||||||
|
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>("UserAccessToken");
|
||||||
|
|
||||||
|
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<bool>("HasFile");
|
||||||
|
|
||||||
|
b.Property<int>("TheMovieDbId");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("RadarrCache");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Ombi.Store.Entities.RecentlyAddedLog", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<DateTime>("AddedAt");
|
||||||
|
|
||||||
|
b.Property<int>("ContentId");
|
||||||
|
|
||||||
|
b.Property<int>("ContentType");
|
||||||
|
|
||||||
|
b.Property<int?>("EpisodeNumber");
|
||||||
|
|
||||||
|
b.Property<int?>("SeasonNumber");
|
||||||
|
|
||||||
|
b.Property<int>("Type");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("RecentlyAddedLog");
|
||||||
|
});
|
||||||
|
|
||||||
|
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<int>("SeriesType");
|
||||||
|
|
||||||
|
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.Property<string>("UserReportedId");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("IssueCategoryId");
|
||||||
|
|
||||||
|
b.HasIndex("IssueId");
|
||||||
|
|
||||||
|
b.HasIndex("UserReportedId");
|
||||||
|
|
||||||
|
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<DateTime?>("DigitalReleaseDate");
|
||||||
|
|
||||||
|
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.RequestLog", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<int>("EpisodeCount");
|
||||||
|
|
||||||
|
b.Property<DateTime>("RequestDate");
|
||||||
|
|
||||||
|
b.Property<int>("RequestId");
|
||||||
|
|
||||||
|
b.Property<int>("RequestType");
|
||||||
|
|
||||||
|
b.Property<string>("UserId");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("RequestLog");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Ombi.Store.Entities.Requests.TvRequests", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<string>("Background");
|
||||||
|
|
||||||
|
b.Property<string>("ImdbId");
|
||||||
|
|
||||||
|
b.Property<string>("Overview");
|
||||||
|
|
||||||
|
b.Property<string>("PosterPath");
|
||||||
|
|
||||||
|
b.Property<int?>("QualityOverride");
|
||||||
|
|
||||||
|
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.RequestSubscription", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<int>("RequestId");
|
||||||
|
|
||||||
|
b.Property<int>("RequestType");
|
||||||
|
|
||||||
|
b.Property<string>("UserId");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("RequestSubscription");
|
||||||
|
});
|
||||||
|
|
||||||
|
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<bool>("HasFile");
|
||||||
|
|
||||||
|
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.NotificationUserId", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Ombi.Store.Entities.OmbiUser", "User")
|
||||||
|
.WithMany("NotificationUserIds")
|
||||||
|
.HasForeignKey("UserId");
|
||||||
|
});
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
b.HasOne("Ombi.Store.Entities.OmbiUser", "UserReported")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserReportedId");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Ombi.Store.Entities.Requests.MovieRequests", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RequestedUserId");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Ombi.Store.Entities.Requests.RequestLog", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Ombi.Store.Entities.OmbiUser", "User")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Ombi.Store.Entities.RequestSubscription", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Ombi.Store.Entities.OmbiUser", "User")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId");
|
||||||
|
});
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
24
src/Ombi.Store/Migrations/20180613203443_RoleRename.cs
Normal file
24
src/Ombi.Store/Migrations/20180613203443_RoleRename.cs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Ombi.Store.Migrations
|
||||||
|
{
|
||||||
|
public partial class RoleRename : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.Sql(@"
|
||||||
|
UPDATE AspNetRoles
|
||||||
|
SET Name = 'ReceivesNewsletter',
|
||||||
|
NORMALIZEDNAME = 'RECEIVESNEWSLETTER'
|
||||||
|
where Name = 'RecievesNewsletter'
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -947,7 +947,7 @@ namespace Ombi.Store.Migrations
|
||||||
|
|
||||||
modelBuilder.Entity("Ombi.Store.Entities.RequestSubscription", b =>
|
modelBuilder.Entity("Ombi.Store.Entities.RequestSubscription", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Ombi.Store.Entities.OmbiUser", "user")
|
b.HasOne("Ombi.Store.Entities.OmbiUser", "User")
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey("UserId");
|
.HasForeignKey("UserId");
|
||||||
});
|
});
|
||||||
|
|
|
@ -89,6 +89,7 @@ namespace Ombi.Store.Repository
|
||||||
{
|
{
|
||||||
return await Db.PlexServerContent
|
return await Db.PlexServerContent
|
||||||
.Include(x => x.Seasons)
|
.Include(x => x.Seasons)
|
||||||
|
.Include(x => x.Episodes)
|
||||||
.FirstOrDefaultAsync(predicate);
|
.FirstOrDefaultAsync(predicate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,9 @@ 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> GetLite();
|
||||||
IQueryable<TvRequests> Get(string userId);
|
IQueryable<TvRequests> Get(string userId);
|
||||||
|
IQueryable<TvRequests> GetLite(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);
|
||||||
|
|
|
@ -60,6 +60,24 @@ namespace Ombi.Store.Repository.Requests
|
||||||
.Where(x => x.ChildRequests.Any(a => a.RequestedUserId == userId))
|
.Where(x => x.ChildRequests.Any(a => a.RequestedUserId == userId))
|
||||||
.AsQueryable();
|
.AsQueryable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IQueryable<TvRequests> GetLite(string userId)
|
||||||
|
{
|
||||||
|
return Db.TvRequests
|
||||||
|
.Include(x => x.ChildRequests)
|
||||||
|
.ThenInclude(x => x.RequestedUser)
|
||||||
|
.Where(x => x.ChildRequests.Any(a => a.RequestedUserId == userId))
|
||||||
|
.AsQueryable();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IQueryable<TvRequests> GetLite()
|
||||||
|
{
|
||||||
|
return Db.TvRequests
|
||||||
|
.Include(x => x.ChildRequests)
|
||||||
|
.ThenInclude(x => x.RequestedUser)
|
||||||
|
.AsQueryable();
|
||||||
|
}
|
||||||
|
|
||||||
public IQueryable<ChildRequests> GetChild()
|
public IQueryable<ChildRequests> GetChild()
|
||||||
{
|
{
|
||||||
return Db.ChildRequests
|
return Db.ChildRequests
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||||
|
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
104
src/Ombi/ApiKeyMiddlewear.cs
Normal file
104
src/Ombi/ApiKeyMiddlewear.cs
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Security.Principal;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Ombi.Core.Authentication;
|
||||||
|
using Ombi.Core.Settings;
|
||||||
|
using Ombi.Settings.Settings.Models;
|
||||||
|
|
||||||
|
namespace Ombi
|
||||||
|
{
|
||||||
|
public class ApiKeyMiddlewear
|
||||||
|
{
|
||||||
|
public ApiKeyMiddlewear(RequestDelegate next)
|
||||||
|
{
|
||||||
|
_next = next;
|
||||||
|
}
|
||||||
|
private readonly RequestDelegate _next;
|
||||||
|
|
||||||
|
public async Task Invoke(HttpContext context)
|
||||||
|
{
|
||||||
|
if (context.Request.Path.StartsWithSegments(new PathString("/api")))
|
||||||
|
{
|
||||||
|
//Let's check if this is an API Call
|
||||||
|
if (context.Request.Headers.Keys.Contains("ApiKey", StringComparer.InvariantCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
// validate the supplied API key
|
||||||
|
// Validate it
|
||||||
|
var headerKey = context.Request.Headers["ApiKey"].FirstOrDefault();
|
||||||
|
await ValidateApiKey(context, _next, headerKey);
|
||||||
|
}
|
||||||
|
else if (context.Request.Query.ContainsKey("apikey"))
|
||||||
|
{
|
||||||
|
if (context.Request.Query.TryGetValue("apikey", out var queryKey))
|
||||||
|
{
|
||||||
|
await ValidateApiKey(context, _next, queryKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// User access token used by the mobile app
|
||||||
|
else if (context.Request.Headers["UserAccessToken"].Any())
|
||||||
|
{
|
||||||
|
var headerKey = context.Request.Headers["UserAccessToken"].FirstOrDefault();
|
||||||
|
await ValidateUserAccessToken(context, _next, headerKey);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await _next.Invoke(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await _next.Invoke(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ValidateUserAccessToken(HttpContext context, RequestDelegate next, string key)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(key))
|
||||||
|
{
|
||||||
|
await context.Response.WriteAsync("Invalid User Access Token");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var um = context.RequestServices.GetService<OmbiUserManager>();
|
||||||
|
var user = await um.Users.FirstOrDefaultAsync(x => x.UserAccessToken == key);
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
|
||||||
|
await context.Response.WriteAsync("Invalid User Access Token");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
|
||||||
|
var identity = new GenericIdentity(user.UserName);
|
||||||
|
var roles = await um.GetRolesAsync(user);
|
||||||
|
var principal = new GenericPrincipal(identity, roles.ToArray());
|
||||||
|
context.User = principal;
|
||||||
|
await next.Invoke(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ValidateApiKey(HttpContext context, RequestDelegate next, string key)
|
||||||
|
{
|
||||||
|
var repo = context.RequestServices.GetService<ISettingsService<OmbiSettings>>();
|
||||||
|
var ombiSettings = await repo.GetSettingsAsync();
|
||||||
|
var valid = ombiSettings.ApiKey.Equals(key, StringComparison.CurrentCultureIgnoreCase);
|
||||||
|
if (!valid)
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
|
||||||
|
await context.Response.WriteAsync("Invalid API Key");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var identity = new GenericIdentity("API");
|
||||||
|
var principal = new GenericPrincipal(identity, new[] { "Admin", "ApiUser" });
|
||||||
|
context.User = principal;
|
||||||
|
await next.Invoke(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -84,7 +84,7 @@ export class LoginComponent implements OnDestroy, OnInit {
|
||||||
});
|
});
|
||||||
this.timer = setInterval(() => {
|
this.timer = setInterval(() => {
|
||||||
this.cycleBackground();
|
this.cycleBackground();
|
||||||
}, 10000);
|
}, 7000);
|
||||||
|
|
||||||
const base = this.location.getBaseHrefFromDOM();
|
const base = this.location.getBaseHrefFromDOM();
|
||||||
if (base.length > 1) {
|
if (base.length > 1) {
|
||||||
|
|
|
@ -70,6 +70,7 @@ export class MovieSearchComponent implements OnInit {
|
||||||
result: false,
|
result: false,
|
||||||
errorMessage: "",
|
errorMessage: "",
|
||||||
};
|
};
|
||||||
|
this.popularMovies();
|
||||||
}
|
}
|
||||||
|
|
||||||
public search(text: any) {
|
public search(text: any) {
|
||||||
|
|
|
@ -93,6 +93,7 @@ export class TvSearchComponent implements OnInit {
|
||||||
result: false,
|
result: false,
|
||||||
errorMessage:"",
|
errorMessage:"",
|
||||||
};
|
};
|
||||||
|
this.popularShows();
|
||||||
}
|
}
|
||||||
|
|
||||||
public search(text: any) {
|
public search(text: any) {
|
||||||
|
|
|
@ -279,4 +279,7 @@ export class SettingsService extends ServiceHelpers {
|
||||||
return this.http
|
return this.http
|
||||||
.post<boolean>(`${this.url}/notifications/newsletter`, JSON.stringify(settings), {headers: this.headers});
|
.post<boolean>(`${this.url}/notifications/newsletter`, JSON.stringify(settings), {headers: this.headers});
|
||||||
}
|
}
|
||||||
|
public verifyUrl(url: string): Observable<boolean> {
|
||||||
|
return this.http.post<boolean>(`${this.url}/customization/urlverify`, JSON.stringify({url}), {headers: this.headers});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,13 +43,24 @@ export class CustomizationComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
public save() {
|
public save() {
|
||||||
this.settingsService.saveCustomization(this.settings).subscribe(x => {
|
|
||||||
if (x) {
|
this.settingsService.verifyUrl(this.settings.applicationUrl).subscribe(x => {
|
||||||
this.notificationService.success("Successfully saved Ombi settings");
|
if(this.settings.applicationUrl) {
|
||||||
} else {
|
if(!x) {
|
||||||
this.notificationService.success("There was an error when saving the Ombi settings");
|
this.notificationService.error(`The URL "${this.settings.applicationUrl}" is not valid. Please format it correctly e.g. http://www.google.com/`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.settingsService.saveCustomization(this.settings).subscribe(x => {
|
||||||
|
if (x) {
|
||||||
|
this.notificationService.success("Successfully saved Ombi settings");
|
||||||
|
} else {
|
||||||
|
this.notificationService.success("There was an error when saving the Ombi settings");
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public dropDownChange(event: any): void {
|
public dropDownChange(event: any): void {
|
||||||
|
|
|
@ -39,8 +39,8 @@
|
||||||
|
|
||||||
<div class="form-group" *ngFor="let u of users">
|
<div class="form-group" *ngFor="let u of users">
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<input type="checkbox" id="{{u.user.id}}" [(ngModel)]="u.selected" (click)="selectSingleUser(u)">
|
<input type="checkbox" id="user{{u.user.id}}" [(ngModel)]="u.selected" (click)="selectSingleUser(u)">
|
||||||
<label for="{{u.user.id}}">{{u.user.userName}}</label>
|
<label for="user{{u.user.id}}">{{u.user.userName}}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -45,6 +45,12 @@ export class UserManagementComponent implements OnInit {
|
||||||
this.notificationService.error("Email Notifications are not setup, cannot send welcome email");
|
this.notificationService.error("Email Notifications are not setup, cannot send welcome email");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if(!this.emailSettings.notificationTemplates.some(x => {
|
||||||
|
return x.enabled && x.notificationType === 8;
|
||||||
|
})) {
|
||||||
|
this.notificationService.error("The Welcome Email template is not enabled in the Email Setings");
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.identityService.sendWelcomeEmail(user).subscribe();
|
this.identityService.sendWelcomeEmail(user).subscribe();
|
||||||
this.notificationService.success(`Sent a welcome email to ${user.emailAddress}`);
|
this.notificationService.success(`Sent a welcome email to ${user.emailAddress}`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,6 +95,7 @@ hr {
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
border-radius: .25rem $i;
|
border-radius: .25rem $i;
|
||||||
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-group-separated .btn,
|
.btn-group-separated .btn,
|
||||||
|
@ -141,6 +142,10 @@ p {
|
||||||
font-size: 1.1rem $i;
|
font-size: 1.1rem $i;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tags {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
display: inline-block $i;
|
display: inline-block $i;
|
||||||
margin-bottom: .5rem $i;
|
margin-bottom: .5rem $i;
|
||||||
|
@ -443,6 +448,7 @@ $border-radius: 10px;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
top: 3px;
|
||||||
bottom: 1px;
|
bottom: 1px;
|
||||||
border: 2px solid #eee;
|
border: 2px solid #eee;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
@ -940,7 +946,7 @@ a > h4:hover {
|
||||||
|
|
||||||
.backdrop{
|
.backdrop{
|
||||||
box-shadow: 3px 3px 10px #000000;
|
box-shadow: 3px 3px 10px #000000;
|
||||||
background-position: center;
|
background-position: 50% 33%;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -203,7 +203,7 @@ namespace Ombi.Controllers
|
||||||
await CreateRole(OmbiRoles.RequestMovie);
|
await CreateRole(OmbiRoles.RequestMovie);
|
||||||
await CreateRole(OmbiRoles.RequestTv);
|
await CreateRole(OmbiRoles.RequestTv);
|
||||||
await CreateRole(OmbiRoles.Disabled);
|
await CreateRole(OmbiRoles.Disabled);
|
||||||
await CreateRole(OmbiRoles.RecievesNewsletter);
|
await CreateRole(OmbiRoles.ReceivesNewsletter);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CreateRole(string role)
|
private async Task CreateRole(string role)
|
||||||
|
|
|
@ -201,7 +201,10 @@ namespace Ombi.Controllers
|
||||||
result = await FanartTvApi.GetMovieImages(moviesArray[item].ToString(), key.Value);
|
result = await FanartTvApi.GetMovieImages(moviesArray[item].ToString(), key.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
movieUrl = result.moviebackground[0].url;
|
var otherRand = new Random();
|
||||||
|
var res = otherRand.Next(result.moviebackground.Length);
|
||||||
|
|
||||||
|
movieUrl = result.moviebackground[res].url;
|
||||||
}
|
}
|
||||||
if (tvArray.Any())
|
if (tvArray.Any())
|
||||||
{
|
{
|
||||||
|
@ -212,8 +215,10 @@ namespace Ombi.Controllers
|
||||||
{
|
{
|
||||||
result = await FanartTvApi.GetTvImages(tvArray[item], key.Value);
|
result = await FanartTvApi.GetTvImages(tvArray[item], key.Value);
|
||||||
}
|
}
|
||||||
|
var otherRand = new Random();
|
||||||
|
var res = otherRand.Next(result.showbackground.Length);
|
||||||
|
|
||||||
tvUrl = result.showbackground[0].url;
|
tvUrl = result.showbackground[res].url;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(movieUrl) && !string.IsNullOrEmpty(tvUrl))
|
if (!string.IsNullOrEmpty(movieUrl) && !string.IsNullOrEmpty(tvUrl))
|
||||||
|
|
|
@ -189,8 +189,28 @@ namespace Ombi.Controllers
|
||||||
return await TvRequestEngine.GetRequests(count, position, new OrderFilterModel
|
return await TvRequestEngine.GetRequests(count, position, new OrderFilterModel
|
||||||
{
|
{
|
||||||
OrderType = (OrderType)orderType,
|
OrderType = (OrderType)orderType,
|
||||||
AvailabilityFilter = (FilterType) availabilityType,
|
AvailabilityFilter = (FilterType)availabilityType,
|
||||||
StatusFilter = (FilterType) statusType,
|
StatusFilter = (FilterType)statusType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the tv requests lite.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="count">The count of items you want to return.</param>
|
||||||
|
/// <param name="position">The position.</param>
|
||||||
|
/// <param name="orderType"></param>
|
||||||
|
/// <param name="statusType"></param>
|
||||||
|
/// <param name="availabilityType"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("tvlite/{count:int}/{position:int}/{orderType:int}/{statusFilterType:int}/{availabilityFilterType:int}")]
|
||||||
|
public async Task<RequestsViewModel<TvRequests>> GetTvRequestsLite(int count, int position, int orderType, int statusType, int availabilityType)
|
||||||
|
{
|
||||||
|
return await TvRequestEngine.GetRequestsLite(count, position, new OrderFilterModel
|
||||||
|
{
|
||||||
|
OrderType = (OrderType)orderType,
|
||||||
|
AvailabilityFilter = (FilterType)availabilityType,
|
||||||
|
StatusFilter = (FilterType)statusType,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -204,6 +224,27 @@ namespace Ombi.Controllers
|
||||||
return await TvRequestEngine.GetRequests();
|
return await TvRequestEngine.GetRequests();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the tv requests without the whole object graph (Does not include seasons/episodes).
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("tvlite")]
|
||||||
|
public async Task<IEnumerable<TvRequests>> GetTvRequestsLite()
|
||||||
|
{
|
||||||
|
return await TvRequestEngine.GetRequestsLite();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the full request object for the specified requestId
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="requestId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("tv/{requestId:int}")]
|
||||||
|
public async Task<TvRequests> GetTvRequest(int requestId)
|
||||||
|
{
|
||||||
|
return await TvRequestEngine.GetTvRequest(requestId);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Requests a tv show/episode/season.
|
/// Requests a tv show/episode/season.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -1,21 +1,15 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Net.Http.Headers;
|
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using Hangfire;
|
using Hangfire;
|
||||||
using Hangfire.RecurringJobExtensions;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
|
||||||
using Microsoft.Extensions.PlatformAbstractions;
|
|
||||||
using NCrontab;
|
using NCrontab;
|
||||||
using Ombi.Api.Emby;
|
using Ombi.Api.Emby;
|
||||||
using Ombi.Attributes;
|
using Ombi.Attributes;
|
||||||
|
@ -45,16 +39,6 @@ namespace Ombi.Controllers
|
||||||
[Produces("application/json")]
|
[Produces("application/json")]
|
||||||
public class SettingsController : Controller
|
public class SettingsController : Controller
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="SettingsController" /> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="resolver">The resolver.</param>
|
|
||||||
/// <param name="mapper">The mapper.</param>
|
|
||||||
/// <param name="templateRepo">The templateRepo.</param>
|
|
||||||
/// <param name="embyApi">The embyApi.</param>
|
|
||||||
/// <param name="radarrSync">The radarrCacher.</param>
|
|
||||||
/// <param name="memCache">The memory cache.</param>
|
|
||||||
/// <param name="githubApi">The memory cache.</param>
|
|
||||||
public SettingsController(ISettingsResolver resolver,
|
public SettingsController(ISettingsResolver resolver,
|
||||||
IMapper mapper,
|
IMapper mapper,
|
||||||
INotificationTemplatesRepository templateRepo,
|
INotificationTemplatesRepository templateRepo,
|
||||||
|
@ -115,7 +99,7 @@ namespace Ombi.Controllers
|
||||||
OsArchitecture = RuntimeInformation.OSArchitecture.ToString(),
|
OsArchitecture = RuntimeInformation.OSArchitecture.ToString(),
|
||||||
OsDescription = RuntimeInformation.OSDescription,
|
OsDescription = RuntimeInformation.OSDescription,
|
||||||
ProcessArchitecture = RuntimeInformation.ProcessArchitecture.ToString(),
|
ProcessArchitecture = RuntimeInformation.ProcessArchitecture.ToString(),
|
||||||
ApplicationBasePath =PlatformServices.Default.Application.ApplicationBasePath
|
ApplicationBasePath =Directory.GetCurrentDirectory()
|
||||||
};
|
};
|
||||||
|
|
||||||
var version = AssemblyHelper.GetRuntimeVersion();
|
var version = AssemblyHelper.GetRuntimeVersion();
|
||||||
|
@ -234,6 +218,13 @@ namespace Ombi.Controllers
|
||||||
return await Save(settings);
|
return await Save(settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
|
[HttpPost("customization/urlverify")]
|
||||||
|
public bool VerifyUrl([FromBody]UrlVerifyModel url)
|
||||||
|
{
|
||||||
|
return Uri.TryCreate(url.Url, UriKind.Absolute, out var __);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get's the preset themes available
|
/// Get's the preset themes available
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -30,6 +30,7 @@ using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Ombi.Core.Settings;
|
using Ombi.Core.Settings;
|
||||||
|
using Ombi.Helpers;
|
||||||
using Ombi.Settings.Settings.Models;
|
using Ombi.Settings.Settings.Models;
|
||||||
|
|
||||||
namespace Ombi.Controllers
|
namespace Ombi.Controllers
|
||||||
|
@ -57,6 +58,18 @@ namespace Ombi.Controllers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns information about this ombi instance
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[AllowAnonymous]
|
||||||
|
[HttpGet("info")]
|
||||||
|
public string GetInfo()
|
||||||
|
{
|
||||||
|
return AssemblyHelper.GetRuntimeVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks to see if we have run through the wizard
|
/// Checks to see if we have run through the wizard
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -126,7 +126,7 @@ namespace Ombi.Controllers
|
||||||
|
|
||||||
var token = new JwtSecurityToken(
|
var token = new JwtSecurityToken(
|
||||||
claims: claims,
|
claims: claims,
|
||||||
expires: rememberMe ? DateTime.UtcNow.AddDays(7) : DateTime.UtcNow.AddHours(5),
|
expires: rememberMe ? DateTime.Now.AddDays(7) : DateTime.Now.AddDays(1),
|
||||||
signingCredentials: creds,
|
signingCredentials: creds,
|
||||||
audience: "Ombi", issuer: "Ombi"
|
audience: "Ombi", issuer: "Ombi"
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace Ombi
|
namespace Ombi
|
||||||
|
@ -29,6 +31,9 @@ namespace Ombi
|
||||||
|
|
||||||
private static Task HandleExceptionAsync(HttpContext context, Exception exception)
|
private static Task HandleExceptionAsync(HttpContext context, Exception exception)
|
||||||
{
|
{
|
||||||
|
var loggerFact = context.RequestServices.GetService<ILoggerFactory>();
|
||||||
|
var logger = loggerFact.CreateLogger<ErrorHandlingMiddleware>();
|
||||||
|
logger.LogError(exception, "Something bad happened, ErrorMiddleware caught this");
|
||||||
var code = HttpStatusCode.InternalServerError; // 500 if unexpected
|
var code = HttpStatusCode.InternalServerError; // 500 if unexpected
|
||||||
|
|
||||||
//if (exception is NotFoundException) code = HttpStatusCode.NotFound;
|
//if (exception is NotFoundException) code = HttpStatusCode.NotFound;
|
||||||
|
|
7
src/Ombi/Models/UrlVerifyModel.cs
Normal file
7
src/Ombi/Models/UrlVerifyModel.cs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
namespace Ombi.Models
|
||||||
|
{
|
||||||
|
public class UrlVerifyModel
|
||||||
|
{
|
||||||
|
public string Url { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,14 +3,14 @@
|
||||||
"windowsAuthentication": false,
|
"windowsAuthentication": false,
|
||||||
"anonymousAuthentication": true,
|
"anonymousAuthentication": true,
|
||||||
"iisExpress": {
|
"iisExpress": {
|
||||||
"applicationUrl": "http://localhost:5000/",
|
"applicationUrl": "http://localhost:3579/",
|
||||||
"sslPort": 0
|
"sslPort": 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"profiles": {
|
"profiles": {
|
||||||
"IIS Express": {
|
"IIS Express": {
|
||||||
"commandName": "IISExpress",
|
"commandName": "IISExpress",
|
||||||
"commandLineArgs": "-baseurl /testing",
|
"commandLineArgs": "--host http://*:3579",
|
||||||
"launchBrowser": true,
|
"launchBrowser": true,
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
|
|
@ -1,20 +1,12 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using System.Net;
|
|
||||||
using System.Security.Principal;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using AutoMapper.EquivalencyExpression;
|
using AutoMapper.EquivalencyExpression;
|
||||||
using Hangfire;
|
using Hangfire;
|
||||||
using Hangfire.Console;
|
|
||||||
using Hangfire.Dashboard;
|
using Hangfire.Dashboard;
|
||||||
using Hangfire.SQLite;
|
using Hangfire.SQLite;
|
||||||
using Microsoft.ApplicationInsights.Extensibility;
|
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.AspNetCore.HttpOverrides;
|
using Microsoft.AspNetCore.HttpOverrides;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.SpaServices.Webpack;
|
using Microsoft.AspNetCore.SpaServices.Webpack;
|
||||||
|
@ -34,7 +26,6 @@ using Ombi.Store.Context;
|
||||||
using Ombi.Store.Entities;
|
using Ombi.Store.Entities;
|
||||||
using Ombi.Store.Repository;
|
using Ombi.Store.Repository;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using Serilog.Events;
|
|
||||||
|
|
||||||
namespace Ombi
|
namespace Ombi
|
||||||
{
|
{
|
||||||
|
@ -87,7 +78,6 @@ namespace Ombi
|
||||||
// This method gets called by the runtime. Use this method to add services to the container.
|
// This method gets called by the runtime. Use this method to add services to the container.
|
||||||
public IServiceProvider ConfigureServices(IServiceCollection services)
|
public IServiceProvider ConfigureServices(IServiceCollection services)
|
||||||
{
|
{
|
||||||
TelemetryConfiguration.Active.DisableTelemetry = true;
|
|
||||||
// Add framework services.
|
// Add framework services.
|
||||||
services.AddDbContext<OmbiContext>();
|
services.AddDbContext<OmbiContext>();
|
||||||
|
|
||||||
|
@ -217,8 +207,9 @@ namespace Ombi
|
||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
|
|
||||||
app.UseMiddleware<ErrorHandlingMiddleware>();
|
app.UseMiddleware<ErrorHandlingMiddleware>();
|
||||||
|
app.UseMiddleware<ApiKeyMiddlewear>();
|
||||||
|
|
||||||
app.ApiKeyMiddlewear(serviceProvider);
|
//app.ApiKeyMiddlewear(app.ApplicationServices);
|
||||||
app.UseSwagger();
|
app.UseSwagger();
|
||||||
app.UseSwaggerUI(c =>
|
app.UseSwaggerUI(c =>
|
||||||
{
|
{
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
using System.Reflection;
|
||||||
using System.Security.Principal;
|
using System.Security.Principal;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
@ -12,7 +13,6 @@ using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.PlatformAbstractions;
|
using Microsoft.Extensions.PlatformAbstractions;
|
||||||
using Microsoft.Extensions.Primitives;
|
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using Ombi.Config;
|
using Ombi.Config;
|
||||||
using Ombi.Core.Authentication;
|
using Ombi.Core.Authentication;
|
||||||
|
@ -114,90 +114,5 @@ namespace Ombi
|
||||||
x.TokenValidationParameters = tokenValidationParameters;
|
x.TokenValidationParameters = tokenValidationParameters;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static void ApiKeyMiddlewear(this IApplicationBuilder app, IServiceProvider serviceProvider)
|
|
||||||
{
|
|
||||||
app.Use(async (context, next) =>
|
|
||||||
{
|
|
||||||
if (context.Request.Path.StartsWithSegments(new PathString("/api")))
|
|
||||||
{
|
|
||||||
// Let's check if this is an API Call
|
|
||||||
if (context.Request.Headers["ApiKey"].Any())
|
|
||||||
{
|
|
||||||
// validate the supplied API key
|
|
||||||
// Validate it
|
|
||||||
var headerKey = context.Request.Headers["ApiKey"].FirstOrDefault();
|
|
||||||
await ValidateApiKey(serviceProvider, context, next, headerKey);
|
|
||||||
}
|
|
||||||
else if (context.Request.Query.ContainsKey("apikey"))
|
|
||||||
{
|
|
||||||
if (context.Request.Query.TryGetValue("apikey", out var queryKey))
|
|
||||||
{
|
|
||||||
await ValidateApiKey(serviceProvider, context, next, queryKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// User access token used by the mobile app
|
|
||||||
else if (context.Request.Headers["UserAccessToken"].Any())
|
|
||||||
{
|
|
||||||
var headerKey = context.Request.Headers["UserAccessToken"].FirstOrDefault();
|
|
||||||
await ValidateUserAccessToken(serviceProvider, context, next, headerKey);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await next();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task ValidateUserAccessToken(IServiceProvider serviceProvider, HttpContext context, Func<Task> next, string key)
|
|
||||||
{
|
|
||||||
if (key.IsNullOrEmpty())
|
|
||||||
{
|
|
||||||
await context.Response.WriteAsync("Invalid User Access Token");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var um = serviceProvider.GetService<OmbiUserManager>();
|
|
||||||
var user = await um.Users.FirstOrDefaultAsync(x => x.UserAccessToken == key);
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
|
|
||||||
await context.Response.WriteAsync("Invalid User Access Token");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
|
|
||||||
var identity = new GenericIdentity(user.UserName);
|
|
||||||
var roles = await um.GetRolesAsync(user);
|
|
||||||
var principal = new GenericPrincipal(identity, roles.ToArray());
|
|
||||||
context.User = principal;
|
|
||||||
await next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task ValidateApiKey(IServiceProvider serviceProvider, HttpContext context, Func<Task> next, string key)
|
|
||||||
{
|
|
||||||
var settingsProvider = serviceProvider.GetService<ISettingsService<OmbiSettings>>();
|
|
||||||
var ombiSettings = settingsProvider.GetSettings();
|
|
||||||
var valid = ombiSettings.ApiKey.Equals(key, StringComparison.CurrentCultureIgnoreCase);
|
|
||||||
if (!valid)
|
|
||||||
{
|
|
||||||
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
|
|
||||||
await context.Response.WriteAsync("Invalid API Key");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var identity = new GenericIdentity("API");
|
|
||||||
var principal = new GenericPrincipal(identity, new[] { "Admin", "ApiUser" });
|
|
||||||
context.User = principal;
|
|
||||||
await next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -2,15 +2,14 @@
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"IncludeScopes": false,
|
"IncludeScopes": false,
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Debug",
|
"Default": "Information",
|
||||||
"System": "Debug",
|
"System": "Information",
|
||||||
"Microsoft": "None",
|
"Microsoft": "None",
|
||||||
"Hangfire": "None"
|
"Hangfire": "None"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ApplicationSettings": {
|
"ApplicationSettings": {
|
||||||
"Verison": "{{VERSIONNUMBER}}",
|
"Verison": "{{VERSIONNUMBER}}",
|
||||||
"OmbiService": "https://ombiservice.azurewebsites.net/",
|
|
||||||
"Branch": "{{BRANCH}}",
|
"Branch": "{{BRANCH}}",
|
||||||
"FriendlyVersion": "v3.0.0"
|
"FriendlyVersion": "v3.0.0"
|
||||||
},
|
},
|
||||||
|
@ -29,7 +28,13 @@
|
||||||
155,
|
155,
|
||||||
13,
|
13,
|
||||||
1891,
|
1891,
|
||||||
399106
|
399106,
|
||||||
|
351286,
|
||||||
|
348350,
|
||||||
|
260513,
|
||||||
|
372058,
|
||||||
|
299536,
|
||||||
|
383498
|
||||||
],
|
],
|
||||||
"TvShows": [
|
"TvShows": [
|
||||||
121361,
|
121361,
|
||||||
|
@ -37,7 +42,10 @@
|
||||||
81189,
|
81189,
|
||||||
79126,
|
79126,
|
||||||
79349,
|
79349,
|
||||||
275274
|
275274,
|
||||||
|
305288,
|
||||||
|
296762,
|
||||||
|
280619
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,8 @@ import { BundleAnalyzerPlugin } from "webpack-bundle-analyzer";
|
||||||
import * as webpack from "webpack";
|
import * as webpack from "webpack";
|
||||||
|
|
||||||
module.exports = (env: any) => {
|
module.exports = (env: any) => {
|
||||||
const prod = env && env.prod as boolean;
|
// const prod = env && env.prod as boolean;
|
||||||
|
const prod = true;
|
||||||
console.log(prod ? "Production" : "Dev" + " main build");
|
console.log(prod ? "Production" : "Dev" + " main build");
|
||||||
const analyse = env && env.analyse as boolean;
|
const analyse = env && env.analyse as boolean;
|
||||||
if (analyse) { console.log("Analysing build"); }
|
if (analyse) { console.log("Analysing build"); }
|
||||||
|
@ -20,6 +21,7 @@ module.exports = (env: any) => {
|
||||||
devtool: prod ? "source-map" : "eval-source-map",
|
devtool: prod ? "source-map" : "eval-source-map",
|
||||||
output: {
|
output: {
|
||||||
filename: "[name].js",
|
filename: "[name].js",
|
||||||
|
chunkFilename: "[id].[chunkhash].js",
|
||||||
publicPath: "/dist/",
|
publicPath: "/dist/",
|
||||||
path: path.join(__dirname, outputDir),
|
path: path.join(__dirname, outputDir),
|
||||||
},
|
},
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue