Merge pull request #2194 from tidusjar/develop

Develop
This commit is contained in:
Jamie 2018-04-25 20:51:22 +01:00 committed by GitHub
commit a7166e8857
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
122 changed files with 4526 additions and 4159 deletions

File diff suppressed because it is too large Load diff

View file

@ -9,7 +9,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="10.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

@ -1,6 +1,8 @@
using System.Threading.Tasks; using System;
using System.Threading.Tasks;
using Ombi.Api.Plex.Models; using Ombi.Api.Plex.Models;
using Ombi.Api.Plex.Models.Friends; using Ombi.Api.Plex.Models.Friends;
using Ombi.Api.Plex.Models.OAuth;
using Ombi.Api.Plex.Models.Server; using Ombi.Api.Plex.Models.Server;
using Ombi.Api.Plex.Models.Status; using Ombi.Api.Plex.Models.Status;
@ -20,5 +22,8 @@ namespace Ombi.Api.Plex
Task<PlexFriends> GetUsers(string authToken); Task<PlexFriends> GetUsers(string authToken);
Task<PlexAccount> GetAccount(string authToken); Task<PlexAccount> GetAccount(string authToken);
Task<PlexMetadata> GetRecentlyAdded(string authToken, string uri, string sectionId); Task<PlexMetadata> GetRecentlyAdded(string authToken, string uri, string sectionId);
Task<OAuthPin> CreatePin();
Task<OAuthPin> GetPin(int pinId);
Uri GetOAuthUrl(int pinId, string code, string applicationUrl, bool wizard);
} }
} }

View file

@ -0,0 +1,27 @@
using System;
namespace Ombi.Api.Plex.Models.OAuth
{
public class OAuthPin
{
public int id { get; set; }
public string code { get; set; }
public bool trusted { get; set; }
public string clientIdentifier { get; set; }
public Location location { get; set; }
public int expiresIn { get; set; }
public DateTime createdAt { get; set; }
public DateTime expiresAt { get; set; }
public string authToken { get; set; }
}
public class Location
{
public string code { get; set; }
public string country { get; set; }
public string city { get; set; }
public string subdivisions { get; set; }
public string coordinates { get; set; }
}
}

View file

@ -1,20 +1,53 @@
using System.Net.Http; using System;
using System.Net.Http;
using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using Ombi.Api.Plex.Models; using Ombi.Api.Plex.Models;
using Ombi.Api.Plex.Models.Friends; using Ombi.Api.Plex.Models.Friends;
using Ombi.Api.Plex.Models.OAuth;
using Ombi.Api.Plex.Models.Server; using Ombi.Api.Plex.Models.Server;
using Ombi.Api.Plex.Models.Status; using Ombi.Api.Plex.Models.Status;
using Ombi.Core.Settings;
using Ombi.Core.Settings.Models.External;
using Ombi.Helpers;
using Ombi.Settings.Settings.Models;
namespace Ombi.Api.Plex namespace Ombi.Api.Plex
{ {
public class PlexApi : IPlexApi public class PlexApi : IPlexApi
{ {
public PlexApi(IApi api) public PlexApi(IApi api, ISettingsService<CustomizationSettings> settings)
{ {
Api = api; Api = api;
_custom = settings;
} }
private IApi Api { get; } private IApi Api { get; }
private readonly ISettingsService<CustomizationSettings> _custom;
private string _app;
private string ApplicationName
{
get
{
if (string.IsNullOrEmpty(_app))
{
var settings = _custom.GetSettings();
if (settings.ApplicationName.IsNullOrEmpty())
{
_app = "Ombi";
}
else
{
_app = settings.ApplicationName;
}
return _app;
}
return _app;
}
}
private const string SignInUri = "https://plex.tv/users/sign_in.json"; private const string SignInUri = "https://plex.tv/users/sign_in.json";
private const string FriendsUri = "https://plex.tv/pms/friends/all"; private const string FriendsUri = "https://plex.tv/pms/friends/all";
@ -156,6 +189,50 @@ namespace Ombi.Api.Plex
return await Api.Request<PlexMetadata>(request); return await Api.Request<PlexMetadata>(request);
} }
public async Task<OAuthPin> CreatePin()
{
var request = new Request($"api/v2/pins", "https://plex.tv/", HttpMethod.Post);
request.AddQueryString("strong", "true");
AddHeaders(request);
return await Api.Request<OAuthPin>(request);
}
public async Task<OAuthPin> GetPin(int pinId)
{
var request = new Request($"api/v2/pins/{pinId}", "https://plex.tv/", HttpMethod.Get);
AddHeaders(request);
return await Api.Request<OAuthPin>(request);
}
public Uri GetOAuthUrl(int pinId, string code, string applicationUrl, bool wizard)
{
var request = new Request("auth#", "https://app.plex.tv", HttpMethod.Get);
AddHeaders(request);
var forwardUrl = wizard
? new Request($"Wizard/OAuth/{pinId}", applicationUrl, HttpMethod.Get)
: new Request($"Login/OAuth/{pinId}", applicationUrl, HttpMethod.Get);
request.AddQueryString("forwardUrl", forwardUrl.FullUri.ToString());
request.AddQueryString("pinID", pinId.ToString());
request.AddQueryString("code", code);
request.AddQueryString("context[device][product]", "Ombi");
request.AddQueryString("context[device][environment]", "bundled");
request.AddQueryString("clientID", $"OmbiV3");
if (request.FullUri.Fragment.Equals("#"))
{
var uri = request.FullUri.ToString();
var withoutEnd = uri.Remove(uri.Length - 1, 1);
var startOfQueryLocation = withoutEnd.IndexOf('?');
var better = withoutEnd.Insert(startOfQueryLocation, "#");
request.FullUri = new Uri(better);
}
return request.FullUri;
}
/// <summary> /// <summary>
/// Adds the required headers and also the authorization header /// Adds the required headers and also the authorization header
/// </summary> /// </summary>
@ -174,7 +251,7 @@ namespace Ombi.Api.Plex
private void AddHeaders(Request request) private void AddHeaders(Request request)
{ {
request.AddHeader("X-Plex-Client-Identifier", $"OmbiV3"); request.AddHeader("X-Plex-Client-Identifier", $"OmbiV3");
request.AddHeader("X-Plex-Product", "Ombi"); request.AddHeader("X-Plex-Product", ApplicationName);
request.AddHeader("X-Plex-Version", "3"); request.AddHeader("X-Plex-Version", "3");
request.AddContentHeader("Content-Type", request.ContentType == ContentType.Json ? "application/json" : "application/xml"); request.AddContentHeader("Content-Type", request.ContentType == ContentType.Json ? "application/json" : "application/xml");
request.AddHeader("Accept", "application/json"); request.AddHeader("Accept", "application/json");

View file

@ -1,4 +1,5 @@
using System; using System;
using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using Ombi.Api.Pushover.Models; using Ombi.Api.Pushover.Models;
@ -17,7 +18,7 @@ namespace Ombi.Api.Pushover
public async Task<PushoverResponse> PushAsync(string accessToken, string message, string userToken) public async Task<PushoverResponse> PushAsync(string accessToken, string message, string userToken)
{ {
var request = new Request($"messages.json?token={accessToken}&user={userToken}&message={message}", PushoverEndpoint, HttpMethod.Post); var request = new Request($"messages.json?token={accessToken}&user={userToken}&message={WebUtility.HtmlEncode(message)}", PushoverEndpoint, HttpMethod.Post);
var result = await _api.Request<PushoverResponse>(request); var result = await _api.Request<PushoverResponse>(request);
return result; return result;

View file

@ -10,7 +10,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.0.1" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="10.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Include="Polly" Version="5.8.0" /> <PackageReference Include="Polly" Version="5.8.0" />
<PackageReference Include="System.Xml.XmlSerializer" Version="4.3.0" /> <PackageReference Include="System.Xml.XmlSerializer" Version="4.3.0" />
</ItemGroup> </ItemGroup>

View file

@ -10,7 +10,7 @@ namespace Ombi.Api
{ {
public Request() public Request()
{ {
} }
public Request(string endpoint, string baseUrl, HttpMethod http, ContentType contentType = ContentType.Json) public Request(string endpoint, string baseUrl, HttpMethod http, ContentType contentType = ContentType.Json)
@ -105,10 +105,10 @@ namespace Ombi.Api
hasQuery = true; hasQuery = true;
startingTag = builder.Query.Contains("?") ? "&" : "?"; startingTag = builder.Query.Contains("?") ? "&" : "?";
} }
builder.Query = hasQuery builder.Query = hasQuery
? $"{builder.Query}{startingTag}{key}={value}" ? $"{builder.Query}{startingTag}{key}={value}"
: $"{startingTag}{key}={value}"; : $"{startingTag}{key}={value}";
_modified = builder.Uri; _modified = builder.Uri;
} }

View file

@ -0,0 +1,85 @@
using System;
using System.Threading.Tasks;
using Ombi.Api.Plex;
using Ombi.Api.Plex.Models;
using Ombi.Api.Plex.Models.OAuth;
using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Settings.Settings.Models;
namespace Ombi.Core.Authentication
{
public class PlexOAuthManager : IPlexOAuthManager
{
public PlexOAuthManager(IPlexApi api, ISettingsService<CustomizationSettings> settings)
{
_api = api;
_customizationSettingsService = settings;
}
private readonly IPlexApi _api;
private readonly ISettingsService<CustomizationSettings> _customizationSettingsService;
public async Task<OAuthPin> RequestPin()
{
var pin = await _api.CreatePin();
return pin;
}
public async Task<string> GetAccessTokenFromPin(int pinId)
{
var pin = await _api.GetPin(pinId);
if (pin.expiresAt < DateTime.UtcNow)
{
return string.Empty;
}
if (pin.authToken.IsNullOrEmpty())
{
// Looks like we do not have a pin yet, we should retry a few times.
var retryCount = 0;
var retryMax = 5;
var retryWaitMs = 1000;
while (pin.authToken.IsNullOrEmpty() && retryCount < retryMax)
{
retryCount++;
await Task.Delay(retryWaitMs);
pin = await _api.GetPin(pinId);
}
}
return pin.authToken;
}
public async Task<PlexAccount> GetAccount(string accessToken)
{
return await _api.GetAccount(accessToken);
}
public async Task<Uri> GetOAuthUrl(int pinId, string code, string websiteAddress = null)
{
Uri url;
if (websiteAddress.IsNullOrEmpty())
{
var settings = await _customizationSettingsService.GetSettingsAsync();
if (settings.ApplicationUrl.IsNullOrEmpty())
{
return null;
}
url = _api.GetOAuthUrl(pinId, code, settings.ApplicationUrl, false);
}
else
{
url = _api.GetOAuthUrl(pinId, code, websiteAddress, false);
}
return url;
}
public Uri GetWizardOAuthUrl(int pinId, string code, string websiteAddress)
{
var url = _api.GetOAuthUrl(pinId, code, websiteAddress, true);
return url;
}
}
}

View file

@ -32,14 +32,7 @@ namespace Ombi.Core.Engine.Interfaces
private OmbiUser _user; private OmbiUser _user;
protected async Task<OmbiUser> GetUser() protected async Task<OmbiUser> GetUser()
{ {
if (IsApiUser) return _user ?? (_user = await UserManager.Users.FirstOrDefaultAsync(x => x.UserName.Equals(Username, StringComparison.CurrentCultureIgnoreCase)));
{
return new OmbiUser
{
UserName = Username,
};
}
return _user ?? (_user = await UserManager.Users.FirstOrDefaultAsync(x => x.UserName == Username));
} }
protected async Task<string> UserAlias() protected async Task<string> UserAlias()
@ -49,10 +42,6 @@ namespace Ombi.Core.Engine.Interfaces
protected async Task<bool> IsInRole(string roleName) protected async Task<bool> IsInRole(string roleName)
{ {
if (IsApiUser && roleName != OmbiRoles.Disabled)
{
return true;
}
return await UserManager.IsInRoleAsync(await GetUser(), roleName); return await UserManager.IsInRoleAsync(await GetUser(), roleName);
} }
@ -72,7 +61,5 @@ namespace Ombi.Core.Engine.Interfaces
var ruleResults = await Rules.StartSpecificRules(model, rule); var ruleResults = await Rules.StartSpecificRules(model, rule);
return ruleResults; return ruleResults;
} }
private bool IsApiUser => Username.Equals("Api", StringComparison.CurrentCultureIgnoreCase);
} }
} }

View file

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

View file

@ -17,5 +17,6 @@ namespace Ombi.Core.Engine.Interfaces
Task<RequestEngineResult> MarkUnavailable(int modelId); Task<RequestEngineResult> MarkUnavailable(int modelId);
Task<RequestEngineResult> MarkAvailable(int modelId); Task<RequestEngineResult> MarkAvailable(int modelId);
Task<int> GetTotal();
} }
} }

View file

@ -144,6 +144,19 @@ namespace Ombi.Core.Engine
return allRequests; return allRequests;
} }
public async Task<int> GetTotal()
{
var shouldHide = await HideFromOtherUsers();
if (shouldHide.Hide)
{
return await MovieRepository.GetWithUser(shouldHide.UserId).CountAsync();
}
else
{
return await MovieRepository.GetWithUser().CountAsync();
}
}
/// <summary> /// <summary>
/// Gets the requests. /// Gets the requests.
/// </summary> /// </summary>

View file

@ -63,13 +63,17 @@ namespace Ombi.Core.Engine
var recentlyAddedLog = new HashSet<RecentlyAddedLog>(); var recentlyAddedLog = new HashSet<RecentlyAddedLog>();
foreach (var p in plexContent) foreach (var p in plexContent)
{ {
if (!p.HasTheMovieDb)
{
continue;
}
if (p.Type == PlexMediaTypeEntity.Movie) if (p.Type == PlexMediaTypeEntity.Movie)
{ {
recentlyAddedLog.Add(new RecentlyAddedLog recentlyAddedLog.Add(new RecentlyAddedLog
{ {
AddedAt = DateTime.Now, AddedAt = DateTime.Now,
Type = RecentlyAddedType.Plex, Type = RecentlyAddedType.Plex,
ContentId = p.Id, ContentId = int.Parse(p.TheMovieDbId),
ContentType = ContentType.Parent ContentType = ContentType.Parent
}); });
} }
@ -78,12 +82,18 @@ namespace Ombi.Core.Engine
// Add the episodes // Add the episodes
foreach (var ep in p.Episodes) foreach (var ep in p.Episodes)
{ {
if (!ep.Series.HasTvDb)
{
continue;
}
recentlyAddedLog.Add(new RecentlyAddedLog recentlyAddedLog.Add(new RecentlyAddedLog
{ {
AddedAt = DateTime.Now, AddedAt = DateTime.Now,
Type = RecentlyAddedType.Plex, Type = RecentlyAddedType.Plex,
ContentId = ep.Id, ContentId = int.Parse(ep.Series.TvDbId),
ContentType = ContentType.Episode ContentType = ContentType.Episode,
EpisodeNumber = ep.EpisodeNumber,
SeasonNumber = ep.SeasonNumber
}); });
} }
} }
@ -91,13 +101,17 @@ namespace Ombi.Core.Engine
foreach (var e in embyContent) foreach (var e in embyContent)
{ {
if (e.TheMovieDbId.IsNullOrEmpty())
{
continue;
}
if (e.Type == EmbyMediaType.Movie) if (e.Type == EmbyMediaType.Movie)
{ {
recentlyAddedLog.Add(new RecentlyAddedLog recentlyAddedLog.Add(new RecentlyAddedLog
{ {
AddedAt = DateTime.Now, AddedAt = DateTime.Now,
Type = RecentlyAddedType.Emby, Type = RecentlyAddedType.Emby,
ContentId = e.Id, ContentId = int.Parse(e.TheMovieDbId),
ContentType = ContentType.Parent ContentType = ContentType.Parent
}); });
} }
@ -106,12 +120,18 @@ namespace Ombi.Core.Engine
// Add the episodes // Add the episodes
foreach (var ep in e.Episodes) foreach (var ep in e.Episodes)
{ {
if (ep.Series.TvDbId.IsNullOrEmpty())
{
continue;
}
recentlyAddedLog.Add(new RecentlyAddedLog recentlyAddedLog.Add(new RecentlyAddedLog
{ {
AddedAt = DateTime.Now, AddedAt = DateTime.Now,
Type = RecentlyAddedType.Emby, Type = RecentlyAddedType.Emby,
ContentId = ep.Id, ContentId = int.Parse(ep.Series.TvDbId),
ContentType = ContentType.Episode ContentType = ContentType.Episode,
EpisodeNumber = ep.EpisodeNumber,
SeasonNumber = ep.SeasonNumber
}); });
} }
} }

View file

@ -1,6 +1,7 @@
using System; using System;
using AutoMapper; using AutoMapper;
using Ombi.Api.TvMaze; using Ombi.Api.TvMaze;
using Ombi.Api.TheMovieDb;
using Ombi.Core.Models.Requests; using Ombi.Core.Models.Requests;
using Ombi.Core.Models.Search; using Ombi.Core.Models.Search;
using Ombi.Helpers; using Ombi.Helpers;
@ -26,11 +27,12 @@ namespace Ombi.Core.Engine
{ {
public class TvRequestEngine : BaseMediaEngine, ITvRequestEngine public class TvRequestEngine : BaseMediaEngine, ITvRequestEngine
{ {
public TvRequestEngine(ITvMazeApi tvApi, IRequestServiceMain requestService, IPrincipal user, public TvRequestEngine(ITvMazeApi tvApi, IMovieDbApi movApi, IRequestServiceMain requestService, IPrincipal user,
INotificationHelper helper, IRuleEvaluator rule, OmbiUserManager manager, INotificationHelper helper, IRuleEvaluator rule, OmbiUserManager manager,
ITvSender sender, IAuditRepository audit, IRepository<RequestLog> rl, ISettingsService<OmbiSettings> settings, ICacheService cache) : base(user, requestService, rule, manager, cache, settings) ITvSender sender, IAuditRepository audit, IRepository<RequestLog> rl, ISettingsService<OmbiSettings> settings, ICacheService cache) : base(user, requestService, rule, manager, cache, settings)
{ {
TvApi = tvApi; TvApi = tvApi;
MovieDbApi = movApi;
NotificationHelper = helper; NotificationHelper = helper;
TvSender = sender; TvSender = sender;
Audit = audit; Audit = audit;
@ -39,6 +41,7 @@ namespace Ombi.Core.Engine
private INotificationHelper NotificationHelper { get; } private INotificationHelper NotificationHelper { get; }
private ITvMazeApi TvApi { get; } private ITvMazeApi TvApi { get; }
private IMovieDbApi MovieDbApi { get; }
private ITvSender TvSender { get; } private ITvSender TvSender { get; }
private IAuditRepository Audit { get; } private IAuditRepository Audit { get; }
private readonly IRepository<RequestLog> _requestLog; private readonly IRepository<RequestLog> _requestLog;
@ -47,7 +50,7 @@ namespace Ombi.Core.Engine
{ {
var user = await GetUser(); var user = await GetUser();
var tvBuilder = new TvShowRequestBuilder(TvApi); var tvBuilder = new TvShowRequestBuilder(TvApi, MovieDbApi);
(await tvBuilder (await tvBuilder
.GetShowInfo(tv.TvDbId)) .GetShowInfo(tv.TvDbId))
.CreateTvList(tv) .CreateTvList(tv)
@ -127,7 +130,7 @@ namespace Ombi.Core.Engine
var newRequest = tvBuilder.CreateNewRequest(tv); var newRequest = tvBuilder.CreateNewRequest(tv);
return await AddRequest(newRequest.NewRequest); return await AddRequest(newRequest.NewRequest);
} }
public async Task<IEnumerable<TvRequests>> GetRequests(int count, int position) public async Task<IEnumerable<TvRequests>> GetRequests(int count, int position)
{ {
var shouldHide = await HideFromOtherUsers(); var shouldHide = await HideFromOtherUsers();
@ -274,9 +277,10 @@ namespace Ombi.Core.Engine
results.ImdbId = request.ImdbId; results.ImdbId = request.ImdbId;
results.Overview = request.Overview; results.Overview = request.Overview;
results.PosterPath = PosterPathHelper.FixPosterPath(request.PosterPath); results.PosterPath = PosterPathHelper.FixPosterPath(request.PosterPath);
results.Background = PosterPathHelper.FixBackgroundPath(request.Background);
results.QualityOverride = request.QualityOverride; results.QualityOverride = request.QualityOverride;
results.RootFolder = request.RootFolder; results.RootFolder = request.RootFolder;
await TvRepository.Update(results); await TvRepository.Update(results);
return results; return results;
} }
@ -428,6 +432,19 @@ namespace Ombi.Core.Engine
}; };
} }
public async Task<int> GetTotal()
{
var shouldHide = await HideFromOtherUsers();
if (shouldHide.Hide)
{
return await TvRepository.Get(shouldHide.UserId).CountAsync();
}
else
{
return await TvRepository.Get().CountAsync();
}
}
private async Task<RequestEngineResult> AddExistingRequest(ChildRequests newRequest, TvRequests existingRequest) private async Task<RequestEngineResult> AddExistingRequest(ChildRequests newRequest, TvRequests existingRequest)
{ {
// Add the child // Add the child

View file

@ -3,7 +3,9 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Ombi.Api.TvMaze; using Ombi.Api.TvMaze;
using Ombi.Api.TheMovieDb;
using Ombi.Api.TvMaze.Models; using Ombi.Api.TvMaze.Models;
using Ombi.Api.TheMovieDb.Models;
using Ombi.Core.Models.Requests; using Ombi.Core.Models.Requests;
using Ombi.Core.Models.Search; using Ombi.Core.Models.Search;
using Ombi.Helpers; using Ombi.Helpers;
@ -16,23 +18,37 @@ namespace Ombi.Core.Helpers
public class TvShowRequestBuilder public class TvShowRequestBuilder
{ {
public TvShowRequestBuilder(ITvMazeApi tvApi) public TvShowRequestBuilder(ITvMazeApi tvApi, IMovieDbApi movApi)
{ {
TvApi = tvApi; TvApi = tvApi;
MovieDbApi = movApi;
} }
private ITvMazeApi TvApi { get; } private ITvMazeApi TvApi { get; }
private IMovieDbApi MovieDbApi { get; }
public ChildRequests ChildRequest { get; set; } public ChildRequests ChildRequest { get; set; }
public List<SeasonsViewModel> TvRequests { get; protected set; } public List<SeasonsViewModel> TvRequests { get; protected set; }
public string PosterPath { get; protected set; } public string PosterPath { get; protected set; }
public string BackdropPath { get; protected set; }
public DateTime FirstAir { get; protected set; } public DateTime FirstAir { get; protected set; }
public TvRequests NewRequest { get; protected set; } public TvRequests NewRequest { get; protected set; }
protected TvMazeShow ShowInfo { get; set; } protected TvMazeShow ShowInfo { get; set; }
protected List<TvSearchResult> Results { get; set; }
public async Task<TvShowRequestBuilder> GetShowInfo(int id) public async Task<TvShowRequestBuilder> GetShowInfo(int id)
{ {
ShowInfo = await TvApi.ShowLookupByTheTvDbId(id); ShowInfo = await TvApi.ShowLookupByTheTvDbId(id);
Results = await MovieDbApi.SearchTv(ShowInfo.name);
foreach (TvSearchResult result in Results) {
if (result.Name == ShowInfo.name)
{
var showIds = await MovieDbApi.GetTvExternals(result.Id);
ShowInfo.externals.imdb = showIds.imdb_id;
BackdropPath = result.BackdropPath;
break;
}
}
DateTime.TryParse(ShowInfo.premiered, out var dt); DateTime.TryParse(ShowInfo.premiered, out var dt);
@ -226,7 +242,8 @@ namespace Ombi.Core.Helpers
ImdbId = ShowInfo.externals?.imdb ?? string.Empty, ImdbId = ShowInfo.externals?.imdb ?? string.Empty,
TvDbId = tv.TvDbId, TvDbId = tv.TvDbId,
ChildRequests = new List<ChildRequests>(), ChildRequests = new List<ChildRequests>(),
TotalSeasons = tv.Seasons.Count() TotalSeasons = tv.Seasons.Count(),
Background = BackdropPath
}; };
NewRequest.ChildRequests.Add(ChildRequest); NewRequest.ChildRequests.Add(ChildRequest);

View file

@ -0,0 +1,16 @@
using System;
using System.Threading.Tasks;
using Ombi.Api.Plex.Models;
using Ombi.Api.Plex.Models.OAuth;
namespace Ombi.Core.Authentication
{
public interface IPlexOAuthManager
{
Task<string> GetAccessTokenFromPin(int pinId);
Task<OAuthPin> RequestPin();
Task<Uri> GetOAuthUrl(int pinId, string code, string websiteAddress = null);
Uri GetWizardOAuthUrl(int pinId, string code, string websiteAddress);
Task<PlexAccount> GetAccount(string accessToken);
}
}

View file

@ -10,6 +10,7 @@ namespace Ombi.Core.Models.Search
public bool Requested { get; set; } public bool Requested { get; set; }
public bool Available { get; set; } public bool Available { get; set; }
public string PlexUrl { get; set; } public string PlexUrl { get; set; }
public string EmbyUrl { get; set; }
public string Quality { get; set; } public string Quality { get; set; }
public abstract RequestType Type { get; } public abstract RequestType Type { get; }

View file

@ -11,12 +11,12 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="AutoMapper" Version="6.1.1" /> <PackageReference Include="AutoMapper" Version="6.1.1" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="3.0.1" /> <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="3.0.1" />
<PackageReference Include="Hangfire" Version="1.6.17" /> <PackageReference Include="Hangfire" Version="1.6.19" />
<PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="2.0.2" /> <PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="2.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.0.2" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Design" Version="1.1.5" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Design" Version="1.1.5" />
<PackageReference Include="MiniProfiler.AspNetCore" Version="4.0.0-alpha6-79" /> <PackageReference Include="MiniProfiler.AspNetCore" Version="4.0.0-alpha6-79" />
<PackageReference Include="Newtonsoft.Json" Version="10.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Include="System.Diagnostics.Process" Version="4.3.0" /> <PackageReference Include="System.Diagnostics.Process" Version="4.3.0" />
</ItemGroup> </ItemGroup>

View file

@ -44,6 +44,7 @@ namespace Ombi.Core.Rule.Rules.Search
if (item != null) if (item != null)
{ {
obj.Available = true; obj.Available = true;
obj.EmbyUrl = item.Url;
if (obj.Type == RequestType.TvShow) if (obj.Type == RequestType.TvShow)
{ {

View file

@ -51,6 +51,7 @@ using Ombi.Store.Repository.Requests;
using Ombi.Updater; using Ombi.Updater;
using PlexContentCacher = Ombi.Schedule.Jobs.Plex; using PlexContentCacher = Ombi.Schedule.Jobs.Plex;
using Ombi.Api.Telegram; using Ombi.Api.Telegram;
using Ombi.Core.Authentication;
using Ombi.Core.Processor; using Ombi.Core.Processor;
using Ombi.Schedule.Jobs.Plex.Interfaces; using Ombi.Schedule.Jobs.Plex.Interfaces;
using Ombi.Schedule.Jobs.SickRage; using Ombi.Schedule.Jobs.SickRage;
@ -82,6 +83,7 @@ namespace Ombi.DependencyInjection
services.AddTransient<IRecentlyAddedEngine, RecentlyAddedEngine>(); services.AddTransient<IRecentlyAddedEngine, RecentlyAddedEngine>();
services.AddTransient<ITvSender, TvSender>(); services.AddTransient<ITvSender, TvSender>();
services.AddTransient<IMassEmailSender, MassEmailSender>(); services.AddTransient<IMassEmailSender, MassEmailSender>();
services.AddTransient<IPlexOAuthManager, PlexOAuthManager>();
} }
public static void RegisterHttp(this IServiceCollection services) public static void RegisterHttp(this IServiceCollection services)
{ {
@ -175,7 +177,6 @@ namespace Ombi.DependencyInjection
services.AddTransient<ISickRageSync, SickRageSync>(); services.AddTransient<ISickRageSync, SickRageSync>();
services.AddTransient<IRefreshMetadata, RefreshMetadata>(); services.AddTransient<IRefreshMetadata, RefreshMetadata>();
services.AddTransient<INewsletterJob, NewsletterJob>(); services.AddTransient<INewsletterJob, NewsletterJob>();
services.AddTransient<IPlexRecentlyAddedSync, PlexRecentlyAddedSync>();
} }
} }
} }

View file

@ -0,0 +1,17 @@
using System;
using System.Globalization;
using System.Collections.Generic;
using System.Text;
namespace Ombi.Helpers
{
public class EmbyHelper
{
public static string GetEmbyMediaUrl(string mediaId)
{
var url =
$"http://app.emby.media/itemdetails.html?id={mediaId}";
return url;
}
}
}

View file

@ -14,5 +14,12 @@ namespace Ombi.Helpers
yield return source1; yield return source1;
} }
} }
public static HashSet<T> ToHashSet<T>(
this IEnumerable<T> source,
IEqualityComparer<T> comparer = null)
{
return new HashSet<T>(source, comparer);
}
} }
} }

View file

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

View file

@ -18,5 +18,19 @@ namespace Ombi.Helpers
return poster; return poster;
} }
public static string FixBackgroundPath(string background)
{
// https://image.tmdb.org/t/p/w1280/fJAvGOitU8y53ByeHnM4avtKFaG.jpg
if (background.Contains("image.tmdb.org", CompareOptions.IgnoreCase))
{
// Somehow we have a full path here for the poster, we only want the last segment
var backgroundSegments = background.Split('/');
return backgroundSegments.Last();
}
return background;
}
} }
} }

View file

@ -65,5 +65,15 @@ namespace Ombi.Helpers
securePassword.MakeReadOnly(); securePassword.MakeReadOnly();
return securePassword; return securePassword;
} }
public static int IntParseLinq(string stringIn)
{
if (int.TryParse(stringIn, out var result))
{
return result;
}
return -1;
}
} }
} }

View file

@ -112,6 +112,7 @@ namespace Ombi.Helpers
return uriBuilder.Uri; return uriBuilder.Uri;
} }
} }
public class ApplicationSettingsException : Exception public class ApplicationSettingsException : Exception

View file

@ -24,6 +24,19 @@ namespace Ombi.Mapping.Profiles
.ForMember(dest => dest.VoteAverage, opts => opts.MapFrom(src => src.vote_average)) .ForMember(dest => dest.VoteAverage, opts => opts.MapFrom(src => src.vote_average))
.ForMember(dest => dest.VoteCount, opts => opts.MapFrom(src => src.vote_count)); .ForMember(dest => dest.VoteCount, opts => opts.MapFrom(src => src.vote_count));
CreateMap<SearchResult, TvSearchResult>()
.ForMember(dest => dest.BackdropPath, opts => opts.MapFrom(src => src.backdrop_path))
.ForMember(dest => dest.Id, opts => opts.MapFrom(src => src.id))
.ForMember(dest => dest.OriginalLanguage, opts => opts.MapFrom(src => src.original_language))
.ForMember(dest => dest.OriginalName, opts => opts.MapFrom(src => src.original_name))
.ForMember(dest => dest.Overview, opts => opts.MapFrom(src => src.overview))
.ForMember(dest => dest.Popularity, opts => opts.MapFrom(src => src.popularity))
.ForMember(dest => dest.PosterPath, opts => opts.MapFrom(src => src.poster_path))
.ForMember(dest => dest.ReleaseDate, opts => opts.MapFrom(src => src.first_air_date))
.ForMember(dest => dest.Name, opts => opts.MapFrom(src => src.name))
.ForMember(dest => dest.VoteAverage, opts => opts.MapFrom(src => src.vote_average))
.ForMember(dest => dest.VoteCount, opts => opts.MapFrom(src => src.vote_count));
CreateMap<MovieResponse, MovieResponseDto>() CreateMap<MovieResponse, MovieResponseDto>()
.ForMember(dest => dest.Adult, opts => opts.MapFrom(src => src.adult)) .ForMember(dest => dest.Adult, opts => opts.MapFrom(src => src.adult))
.ForMember(dest => dest.BackdropPath, opts => opts.MapFrom(src => src.backdrop_path)) .ForMember(dest => dest.BackdropPath, opts => opts.MapFrom(src => src.backdrop_path))

View file

@ -10,7 +10,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Ensure.That" Version="7.0.0-pre32" /> <PackageReference Include="Ensure.That" Version="7.0.0-pre32" />
<PackageReference Include="MailKit" Version="1.20.0" /> <PackageReference Include="MailKit" Version="2.0.3" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

@ -19,7 +19,7 @@ namespace Ombi.Schedule
IOmbiAutomaticUpdater updater, IEmbyContentSync embySync, IPlexUserImporter userImporter, IOmbiAutomaticUpdater updater, IEmbyContentSync embySync, IPlexUserImporter userImporter,
IEmbyUserImporter embyUserImporter, ISonarrSync cache, ICouchPotatoSync cpCache, IEmbyUserImporter embyUserImporter, ISonarrSync cache, ICouchPotatoSync cpCache,
ISettingsService<JobSettings> jobsettings, ISickRageSync srSync, IRefreshMetadata refresh, ISettingsService<JobSettings> jobsettings, ISickRageSync srSync, IRefreshMetadata refresh,
INewsletterJob newsletter, IPlexRecentlyAddedSync recentlyAddedSync) INewsletterJob newsletter)
{ {
_plexContentSync = plexContentSync; _plexContentSync = plexContentSync;
_radarrSync = radarrSync; _radarrSync = radarrSync;
@ -33,7 +33,6 @@ namespace Ombi.Schedule
_srSync = srSync; _srSync = srSync;
_refreshMetadata = refresh; _refreshMetadata = refresh;
_newsletter = newsletter; _newsletter = newsletter;
_plexRecentlyAddedSync = recentlyAddedSync;
} }
private readonly IPlexContentSync _plexContentSync; private readonly IPlexContentSync _plexContentSync;
@ -48,7 +47,6 @@ namespace Ombi.Schedule
private readonly ISettingsService<JobSettings> _jobSettings; private readonly ISettingsService<JobSettings> _jobSettings;
private readonly IRefreshMetadata _refreshMetadata; private readonly IRefreshMetadata _refreshMetadata;
private readonly INewsletterJob _newsletter; private readonly INewsletterJob _newsletter;
private readonly IPlexRecentlyAddedSync _plexRecentlyAddedSync;
public void Setup() public void Setup()
{ {
@ -68,6 +66,8 @@ namespace Ombi.Schedule
RecurringJob.AddOrUpdate(() => _embyUserImporter.Start(), JobSettingsHelper.UserImporter(s)); RecurringJob.AddOrUpdate(() => _embyUserImporter.Start(), JobSettingsHelper.UserImporter(s));
RecurringJob.AddOrUpdate(() => _plexUserImporter.Start(), JobSettingsHelper.UserImporter(s)); RecurringJob.AddOrUpdate(() => _plexUserImporter.Start(), JobSettingsHelper.UserImporter(s));
RecurringJob.AddOrUpdate(() => _newsletter.Start(), JobSettingsHelper.Newsletter(s)); RecurringJob.AddOrUpdate(() => _newsletter.Start(), JobSettingsHelper.Newsletter(s));
BackgroundJob.Enqueue(() => _refreshMetadata.Start());
} }

View file

@ -114,6 +114,7 @@ namespace Ombi.Schedule.Jobs.Emby
Title = tvInfo.Name, Title = tvInfo.Name,
Type = EmbyMediaType.Series, Type = EmbyMediaType.Series,
EmbyId = tvShow.Id, EmbyId = tvShow.Id,
Url = EmbyHelper.GetEmbyMediaUrl(tvShow.Id),
AddedAt = DateTime.UtcNow AddedAt = DateTime.UtcNow
}); });
} }
@ -135,6 +136,7 @@ namespace Ombi.Schedule.Jobs.Emby
Title = movieInfo.Name, Title = movieInfo.Name,
Type = EmbyMediaType.Movie, Type = EmbyMediaType.Movie,
EmbyId = movieInfo.Id, EmbyId = movieInfo.Id,
Url = EmbyHelper.GetEmbyMediaUrl(movieInfo.Id),
AddedAt = DateTime.UtcNow, AddedAt = DateTime.UtcNow,
}); });
} }

View file

@ -1,11 +1,14 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using MailKit; using MailKit;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Ombi.Api.TheMovieDb; using Ombi.Api.TheMovieDb;
using Ombi.Api.TheMovieDb.Models; using Ombi.Api.TheMovieDb.Models;
using Ombi.Api.TvMaze; using Ombi.Api.TvMaze;
@ -26,7 +29,7 @@ namespace Ombi.Schedule.Jobs.Ombi
public NewsletterJob(IPlexContentRepository plex, IEmbyContentRepository emby, IRepository<RecentlyAddedLog> addedLog, public NewsletterJob(IPlexContentRepository plex, IEmbyContentRepository emby, IRepository<RecentlyAddedLog> addedLog,
IMovieDbApi movieApi, ITvMazeApi tvApi, IEmailProvider email, ISettingsService<CustomizationSettings> custom, IMovieDbApi movieApi, ITvMazeApi tvApi, IEmailProvider email, ISettingsService<CustomizationSettings> custom,
ISettingsService<EmailNotificationSettings> emailSettings, INotificationTemplatesRepository templateRepo, ISettingsService<EmailNotificationSettings> emailSettings, INotificationTemplatesRepository templateRepo,
UserManager<OmbiUser> um, ISettingsService<NewsletterSettings> newsletter) UserManager<OmbiUser> um, ISettingsService<NewsletterSettings> newsletter, ILogger<NewsletterJob> log)
{ {
_plex = plex; _plex = plex;
_emby = emby; _emby = emby;
@ -42,6 +45,7 @@ namespace Ombi.Schedule.Jobs.Ombi
_emailSettings.ClearCache(); _emailSettings.ClearCache();
_customizationSettings.ClearCache(); _customizationSettings.ClearCache();
_newsletterSettings.ClearCache(); _newsletterSettings.ClearCache();
_log = log;
} }
private readonly IPlexContentRepository _plex; private readonly IPlexContentRepository _plex;
@ -55,6 +59,7 @@ namespace Ombi.Schedule.Jobs.Ombi
private readonly ISettingsService<EmailNotificationSettings> _emailSettings; private readonly ISettingsService<EmailNotificationSettings> _emailSettings;
private readonly ISettingsService<NewsletterSettings> _newsletterSettings; private readonly ISettingsService<NewsletterSettings> _newsletterSettings;
private readonly UserManager<OmbiUser> _userManager; private readonly UserManager<OmbiUser> _userManager;
private readonly ILogger _log;
public async Task Start(NewsletterSettings settings, bool test) public async Task Start(NewsletterSettings settings, bool test)
{ {
@ -74,152 +79,172 @@ namespace Ombi.Schedule.Jobs.Ombi
return; return;
} }
var customization = await _customizationSettings.GetSettingsAsync(); try
// Get the Content
var plexContent = _plex.GetAll().Include(x => x.Episodes).AsNoTracking();
var embyContent = _emby.GetAll().Include(x => x.Episodes).AsNoTracking();
var addedLog = _recentlyAddedLog.GetAll();
var addedPlexMovieLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Plex && x.ContentType == ContentType.Parent).Select(x => x.ContentId);
var addedEmbyMoviesLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Emby && x.ContentType == ContentType.Parent).Select(x => x.ContentId);
var addedPlexEpisodesLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Plex && x.ContentType == ContentType.Episode).Select(x => x.ContentId);
var addedEmbyEpisodesLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Emby && x.ContentType == ContentType.Episode).Select(x => x.ContentId);
// Filter out the ones that we haven't sent yet
var plexContentMoviesToSend = plexContent.Where(x => x.Type == PlexMediaTypeEntity.Movie && !addedPlexMovieLogIds.Contains(x.Id));
var embyContentMoviesToSend = embyContent.Where(x => x.Type == EmbyMediaType.Movie && !addedEmbyMoviesLogIds.Contains(x.Id));
var plexEpisodesToSend = _plex.GetAllEpisodes().Include(x => x.Series).Where(x => !addedPlexEpisodesLogIds.Contains(x.Id)).AsNoTracking();
var embyEpisodesToSend = _emby.GetAllEpisodes().Include(x => x.Series).Where(x => !addedEmbyEpisodesLogIds.Contains(x.Id)).AsNoTracking();
var body = string.Empty;
if (test)
{ {
var plexm = plexContent.Where(x => x.Type == PlexMediaTypeEntity.Movie).OrderByDescending(x => x.AddedAt).Take(10);
var embym = embyContent.Where(x => x.Type == EmbyMediaType.Movie).OrderByDescending(x => x.AddedAt).Take(10);
var plext = _plex.GetAllEpisodes().Include(x => x.Series).OrderByDescending(x => x.Series.AddedAt).Take(10); var customization = await _customizationSettings.GetSettingsAsync();
var embyt = _emby.GetAllEpisodes().Include(x => x.Series).OrderByDescending(x => x.AddedAt).Take(10); // Get the Content
body = await BuildHtml(plexm, embym, plext, embyt, settings); var plexContent = _plex.GetAll().Include(x => x.Episodes).AsNoTracking();
} var embyContent = _emby.GetAll().Include(x => x.Episodes).AsNoTracking();
else
{ var addedLog = _recentlyAddedLog.GetAll();
body = await BuildHtml(plexContentMoviesToSend, embyContentMoviesToSend, plexEpisodesToSend, embyEpisodesToSend, settings); var addedPlexMovieLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Plex && x.ContentType == ContentType.Parent).Select(x => x.ContentId);
if (body.IsNullOrEmpty()) var addedEmbyMoviesLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Emby && x.ContentType == ContentType.Parent).Select(x => x.ContentId);
var addedPlexEpisodesLogIds =
addedLog.Where(x => x.Type == RecentlyAddedType.Plex && x.ContentType == ContentType.Episode);
var addedEmbyEpisodesLogIds =
addedLog.Where(x => x.Type == RecentlyAddedType.Emby && x.ContentType == ContentType.Episode);
// Filter out the ones that we haven't sent yet
var plexContentMoviesToSend = plexContent.Where(x => x.Type == PlexMediaTypeEntity.Movie && !addedPlexMovieLogIds.Contains(StringHelper.IntParseLinq(x.TheMovieDbId)));
var embyContentMoviesToSend = embyContent.Where(x => x.Type == EmbyMediaType.Movie && !addedEmbyMoviesLogIds.Contains(StringHelper.IntParseLinq(x.TheMovieDbId)));
_log.LogInformation("Plex Movies to send: {0}", plexContentMoviesToSend.Count());
_log.LogInformation("Emby Movies to send: {0}", embyContentMoviesToSend.Count());
var plexEpisodesToSend =
FilterPlexEpisodes(_plex.GetAllEpisodes().Include(x => x.Series).AsNoTracking(), addedPlexEpisodesLogIds);
var embyEpisodesToSend = FilterEmbyEpisodes(_emby.GetAllEpisodes().Include(x => x.Series).AsNoTracking(),
addedEmbyEpisodesLogIds);
_log.LogInformation("Plex Episodes to send: {0}", plexEpisodesToSend.Count());
_log.LogInformation("Emby Episodes to send: {0}", embyEpisodesToSend.Count());
var body = string.Empty;
if (test)
{ {
return; var plexm = plexContent.Where(x => x.Type == PlexMediaTypeEntity.Movie).OrderByDescending(x => x.AddedAt).Take(10);
var embym = embyContent.Where(x => x.Type == EmbyMediaType.Movie).OrderByDescending(x => x.AddedAt).Take(10);
var plext = _plex.GetAllEpisodes().Include(x => x.Series).OrderByDescending(x => x.Series.AddedAt).Take(10).ToHashSet();
var embyt = _emby.GetAllEpisodes().Include(x => x.Series).OrderByDescending(x => x.AddedAt).Take(10).ToHashSet();
body = await BuildHtml(plexm, embym, plext, embyt, settings);
} }
else
}
if (!test)
{
// Get the users to send it to
var users = await _userManager.GetUsersInRoleAsync(OmbiRoles.RecievesNewsletter);
if (!users.Any())
{ {
return; body = await BuildHtml(plexContentMoviesToSend, embyContentMoviesToSend, plexEpisodesToSend, embyEpisodesToSend, settings);
} if (body.IsNullOrEmpty())
foreach (var emails in settings.ExternalEmails)
{
users.Add(new OmbiUser
{ {
UserName = emails, return;
Email = emails }
});
} }
var emailTasks = new List<Task>();
foreach (var user in users) if (!test)
{ {
if (user.Email.IsNullOrEmpty()) // Get the users to send it to
var users = await _userManager.GetUsersInRoleAsync(OmbiRoles.RecievesNewsletter);
if (!users.Any())
{ {
continue; return;
} }
var messageContent = ParseTemplate(template, customization, user); foreach (var emails in settings.ExternalEmails)
var email = new NewsletterTemplate();
var html = email.LoadTemplate(messageContent.Subject, messageContent.Message, body, customization.Logo);
emailTasks.Add(_email.Send(
new NotificationMessage { Message = html, Subject = messageContent.Subject, To = user.Email },
emailSettings));
}
// Now add all of this to the Recently Added log
var recentlyAddedLog = new HashSet<RecentlyAddedLog>();
foreach (var p in plexContentMoviesToSend)
{
recentlyAddedLog.Add(new RecentlyAddedLog
{ {
AddedAt = DateTime.Now, users.Add(new OmbiUser
Type = RecentlyAddedType.Plex, {
ContentType = ContentType.Parent, UserName = emails,
ContentId = p.Id Email = emails
}); });
}
} var emailTasks = new List<Task>();
foreach (var user in users)
foreach (var p in plexEpisodesToSend)
{
recentlyAddedLog.Add(new RecentlyAddedLog
{ {
AddedAt = DateTime.Now, // Get the users to send it to
Type = RecentlyAddedType.Plex, if (user.Email.IsNullOrEmpty())
ContentType = ContentType.Episode, {
ContentId = p.Id continue;
}); }
}
foreach (var e in embyContentMoviesToSend) var messageContent = ParseTemplate(template, customization, user);
{ var email = new NewsletterTemplate();
if (e.Type == EmbyMediaType.Movie)
var html = email.LoadTemplate(messageContent.Subject, messageContent.Message, body, customization.Logo);
emailTasks.Add(_email.Send(
new NotificationMessage { Message = html, Subject = messageContent.Subject, To = user.Email },
emailSettings));
}
// Now add all of this to the Recently Added log
var recentlyAddedLog = new HashSet<RecentlyAddedLog>();
foreach (var p in plexContentMoviesToSend)
{
recentlyAddedLog.Add(new RecentlyAddedLog
{
AddedAt = DateTime.Now,
Type = RecentlyAddedType.Plex,
ContentType = ContentType.Parent,
ContentId = StringHelper.IntParseLinq(p.TheMovieDbId),
});
}
foreach (var p in plexEpisodesToSend)
{
recentlyAddedLog.Add(new RecentlyAddedLog
{
AddedAt = DateTime.Now,
Type = RecentlyAddedType.Plex,
ContentType = ContentType.Episode,
ContentId = StringHelper.IntParseLinq(p.Series.TvDbId),
EpisodeNumber = p.EpisodeNumber,
SeasonNumber = p.SeasonNumber
});
}
foreach (var e in embyContentMoviesToSend)
{
if (e.Type == EmbyMediaType.Movie)
{
recentlyAddedLog.Add(new RecentlyAddedLog
{
AddedAt = DateTime.Now,
Type = RecentlyAddedType.Emby,
ContentType = ContentType.Parent,
ContentId = StringHelper.IntParseLinq(e.TheMovieDbId),
});
}
}
foreach (var p in embyEpisodesToSend)
{ {
recentlyAddedLog.Add(new RecentlyAddedLog recentlyAddedLog.Add(new RecentlyAddedLog
{ {
AddedAt = DateTime.Now, AddedAt = DateTime.Now,
Type = RecentlyAddedType.Emby, Type = RecentlyAddedType.Emby,
ContentType = ContentType.Parent, ContentType = ContentType.Episode,
ContentId = e.Id ContentId = StringHelper.IntParseLinq(p.Series.TvDbId),
EpisodeNumber = p.EpisodeNumber,
SeasonNumber = p.SeasonNumber
}); });
} }
await _recentlyAddedLog.AddRange(recentlyAddedLog);
await Task.WhenAll(emailTasks.ToArray());
} }
else
foreach (var p in embyEpisodesToSend)
{ {
recentlyAddedLog.Add(new RecentlyAddedLog var admins = await _userManager.GetUsersInRoleAsync(OmbiRoles.Admin);
foreach (var a in admins)
{ {
AddedAt = DateTime.Now, if (a.Email.IsNullOrEmpty())
Type = RecentlyAddedType.Emby, {
ContentType = ContentType.Episode, continue;
ContentId = p.Id }
}); var messageContent = ParseTemplate(template, customization, a);
}
await _recentlyAddedLog.AddRange(recentlyAddedLog);
await Task.WhenAll(emailTasks.ToArray()); var email = new NewsletterTemplate();
}
else var html = email.LoadTemplate(messageContent.Subject, messageContent.Message, body, customization.Logo);
{
var admins = await _userManager.GetUsersInRoleAsync(OmbiRoles.Admin); await _email.Send(
foreach (var a in admins) new NotificationMessage { Message = html, Subject = messageContent.Subject, To = a.Email },
{ emailSettings);
if (a.Email.IsNullOrEmpty())
{
continue;
} }
var messageContent = ParseTemplate(template, customization, a);
var email = new NewsletterTemplate();
var html = email.LoadTemplate(messageContent.Subject, messageContent.Message, body, customization.Logo);
await _email.Send(
new NotificationMessage { Message = html, Subject = messageContent.Subject, To = a.Email },
emailSettings);
} }
}
catch (Exception e)
{
_log.LogError(e, "Error when attempting to create newsletter");
throw;
} }
} }
@ -229,6 +254,40 @@ namespace Ombi.Schedule.Jobs.Ombi
await Start(newsletterSettings, false); await Start(newsletterSettings, false);
} }
private HashSet<PlexEpisode> FilterPlexEpisodes(IEnumerable<PlexEpisode> source, IQueryable<RecentlyAddedLog> recentlyAdded)
{
var itemsToReturn = new HashSet<PlexEpisode>();
foreach (var ep in source)
{
var tvDbId = StringHelper.IntParseLinq(ep.Series.TvDbId);
if (recentlyAdded.Any(x => x.ContentId == tvDbId && x.EpisodeNumber == ep.EpisodeNumber && x.SeasonNumber == ep.SeasonNumber))
{
continue;
}
itemsToReturn.Add(ep);
}
return itemsToReturn;
}
private HashSet<EmbyEpisode> FilterEmbyEpisodes(IEnumerable<EmbyEpisode> source, IQueryable<RecentlyAddedLog> recentlyAdded)
{
var itemsToReturn = new HashSet<EmbyEpisode>();
foreach (var ep in source)
{
var tvDbId = StringHelper.IntParseLinq(ep.Series.TvDbId);
if (recentlyAdded.Any(x => x.ContentId == tvDbId && x.EpisodeNumber == ep.EpisodeNumber && x.SeasonNumber == ep.SeasonNumber))
{
continue;
}
itemsToReturn.Add(ep);
}
return itemsToReturn;
}
private NotificationMessageContent ParseTemplate(NotificationTemplates template, CustomizationSettings settings, OmbiUser username) private NotificationMessageContent ParseTemplate(NotificationTemplates template, CustomizationSettings settings, OmbiUser username)
{ {
var resolver = new NotificationMessageResolver(); var resolver = new NotificationMessageResolver();
@ -239,7 +298,7 @@ namespace Ombi.Schedule.Jobs.Ombi
return resolver.ParseMessage(template, curlys); return resolver.ParseMessage(template, curlys);
} }
private async Task<string> BuildHtml(IQueryable<PlexServerContent> plexContentToSend, IQueryable<EmbyContent> embyContentToSend, IQueryable<PlexEpisode> plexEpisodes, IQueryable<EmbyEpisode> embyEp, NewsletterSettings settings) private async Task<string> BuildHtml(IQueryable<PlexServerContent> plexContentToSend, IQueryable<EmbyContent> embyContentToSend, HashSet<PlexEpisode> plexEpisodes, HashSet<EmbyEpisode> embyEp, NewsletterSettings settings)
{ {
var sb = new StringBuilder(); var sb = new StringBuilder();
@ -285,8 +344,7 @@ namespace Ombi.Schedule.Jobs.Ombi
} }
catch (Exception e) catch (Exception e)
{ {
Console.WriteLine(e); _log.LogError(e, "Error when Processing Plex Movies {0}", info.Title);
throw;
} }
finally finally
{ {
@ -315,8 +373,8 @@ namespace Ombi.Schedule.Jobs.Ombi
theMovieDbId = result.id.ToString(); theMovieDbId = result.id.ToString();
} }
var info = await _movieApi.GetMovieInformationWithExtraInfo(int.Parse(theMovieDbId)); var info = await _movieApi.GetMovieInformationWithExtraInfo(StringHelper.IntParseLinq(theMovieDbId));
if (info == null) if (info == null)
{ {
continue; continue;
@ -327,8 +385,7 @@ namespace Ombi.Schedule.Jobs.Ombi
} }
catch (Exception e) catch (Exception e)
{ {
Console.WriteLine(e); _log.LogError(e, "Error when processing Emby Movies {0}", info.Title);
throw;
} }
finally finally
{ {
@ -366,7 +423,7 @@ namespace Ombi.Schedule.Jobs.Ombi
AddParagraph(sb, info.Overview); AddParagraph(sb, info.Overview);
} }
private async Task ProcessPlexTv(IQueryable<PlexEpisode> plexContent, StringBuilder sb) private async Task ProcessPlexTv(HashSet<PlexEpisode> plexContent, StringBuilder sb)
{ {
var series = new List<PlexServerContent>(); var series = new List<PlexServerContent>();
foreach (var plexEpisode in plexContent) foreach (var plexEpisode in plexContent)
@ -437,7 +494,7 @@ namespace Ombi.Schedule.Jobs.Ombi
sb.Append("<tr>"); sb.Append("<tr>");
sb.Append( sb.Append(
"<td align=\"center\" style=\"font-family: sans-serif; font-size: 14px; vertical-align: top;\" valign=\"top\">"); "<td align=\"center\" style=\"font-family: sans-serif; font-size: 14px; vertical-align: top;\" valign=\"top\">");
var title = $"{t.Title} ({t.ReleaseYear})"; var title = $"{t.Title} ({t.ReleaseYear})";
Href(sb, $"https://www.imdb.com/title/{info.externals.imdb}/"); Href(sb, $"https://www.imdb.com/title/{info.externals.imdb}/");
@ -483,7 +540,7 @@ namespace Ombi.Schedule.Jobs.Ombi
} }
catch (Exception e) catch (Exception e)
{ {
//Log.Error(e); _log.LogError(e, "Error when processing Plex TV {0}", t.Title);
} }
finally finally
{ {
@ -494,7 +551,7 @@ namespace Ombi.Schedule.Jobs.Ombi
} }
private async Task ProcessEmbyTv(IQueryable<EmbyEpisode> embyContent, StringBuilder sb) private async Task ProcessEmbyTv(HashSet<EmbyEpisode> embyContent, StringBuilder sb)
{ {
var series = new List<EmbyContent>(); var series = new List<EmbyContent>();
foreach (var episode in embyContent) foreach (var episode in embyContent)
@ -584,7 +641,7 @@ namespace Ombi.Schedule.Jobs.Ombi
} }
catch (Exception e) catch (Exception e)
{ {
//Log.Error(e); _log.LogError(e, "Error when processing Emby TV {0}", t.Title);
} }
finally finally
{ {

View file

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

View file

@ -1,40 +0,0 @@
using System;
using System.Threading.Tasks;
using Ombi.Api.Plex;
namespace Ombi.Schedule.Jobs.Plex
{
public class PlexRecentlyAddedSync : IPlexRecentlyAddedSync
{
public PlexRecentlyAddedSync(IPlexContentSync contentSync)
{
_sync = contentSync;
}
private readonly IPlexContentSync _sync;
public async Task Start()
{
await _sync.CacheContent(true);
}
private bool _disposed;
protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;
if (disposing)
{
_sync?.Dispose();
}
_disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
}

View file

@ -10,9 +10,9 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Dapper" Version="1.50.2" /> <PackageReference Include="Dapper" Version="1.50.2" />
<PackageReference Include="Hangfire" Version="1.6.17" /> <PackageReference Include="Hangfire" Version="1.6.19" />
<PackageReference Include="Hangfire.AspNetCore" Version="1.6.17" /> <PackageReference Include="Hangfire.AspNetCore" Version="1.6.19" />
<PackageReference Include="Hangfire.Console" Version="1.3.7" /> <PackageReference Include="Hangfire.Console" Version="1.3.10" />
<PackageReference Include="Hangfire.MemoryStorage.Core" Version="1.4.0" /> <PackageReference Include="Hangfire.MemoryStorage.Core" Version="1.4.0" />
<PackageReference Include="Hangfire.RecurringJobExtensions" Version="1.1.6" /> <PackageReference Include="Hangfire.RecurringJobExtensions" Version="1.1.6" />
<PackageReference Include="Hangfire.SQLite" Version="1.4.2" /> <PackageReference Include="Hangfire.SQLite" Version="1.4.2" />

View file

@ -10,7 +10,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="10.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

@ -12,5 +12,6 @@ namespace Ombi.Settings.Settings.Models
public bool RequireLowercase { get; set; } public bool RequireLowercase { get; set; }
public bool RequireNonAlphanumeric { get; set; } public bool RequireNonAlphanumeric { get; set; }
public bool RequireUppercase { get; set; } public bool RequireUppercase { get; set; }
public bool EnableOAuth { get; set; } // Plex OAuth
} }
} }

View file

@ -7,7 +7,6 @@ namespace Ombi.Core.Settings.Models.External
{ {
public bool Enable { get; set; } public bool Enable { get; set; }
public List<PlexServers> Servers { get; set; } public List<PlexServers> Servers { get; set; }
} }
public class PlexServers : ExternalSettings public class PlexServers : ExternalSettings

View file

@ -7,6 +7,6 @@ namespace Ombi.Settings.Settings.Models.Notifications
public bool DisableTv { get; set; } public bool DisableTv { get; set; }
public bool DisableMovies { get; set; } public bool DisableMovies { get; set; }
public bool Enabled { get; set; } public bool Enabled { get; set; }
public List<string> ExternalEmails { get; set; } public List<string> ExternalEmails { get; set; } = new List<string>();
} }
} }

View file

@ -123,7 +123,23 @@ namespace Ombi.Store.Context
{ {
NormalizedName = OmbiRoles.RecievesNewsletter.ToUpper() NormalizedName = OmbiRoles.RecievesNewsletter.ToUpper()
}); });
SaveChanges();
} }
// Make sure we have the API User
var apiUserExists = Users.Any(x => x.UserName.Equals("Api", StringComparison.CurrentCultureIgnoreCase));
if (!apiUserExists)
{
Users.Add(new OmbiUser
{
UserName = "Api",
UserType = UserType.SystemUser,
NormalizedUserName = "API",
});
SaveChanges();
}
//Check if templates exist //Check if templates exist
var templates = NotificationTemplates.ToList(); var templates = NotificationTemplates.ToList();

View file

@ -48,6 +48,7 @@ namespace Ombi.Store.Entities
public string TheMovieDbId { get; set; } public string TheMovieDbId { get; set; }
public string TvDbId { get; set; } public string TvDbId { get; set; }
public string Url { get; set; }
public ICollection<EmbyEpisode> Episodes { get; set; } public ICollection<EmbyEpisode> Episodes { get; set; }
} }

View file

@ -8,7 +8,9 @@ namespace Ombi.Store.Entities
{ {
public RecentlyAddedType Type { get; set; } public RecentlyAddedType Type { get; set; }
public ContentType ContentType { get; set; } public ContentType ContentType { get; set; }
public int ContentId { get; set; } // This is dependant on the type public int ContentId { get; set; } // This is dependant on the type, it's either TMDBID or TVDBID
public int? EpisodeNumber { get; set; }
public int? SeasonNumber { get; set; }
public DateTime AddedAt { get; set; } public DateTime AddedAt { get; set; }
} }

View file

@ -13,6 +13,7 @@ namespace Ombi.Store.Entities.Requests
public string Overview { get; set; } public string Overview { get; set; }
public string Title { get; set; } public string Title { get; set; }
public string PosterPath { get; set; } public string PosterPath { get; set; }
public string Background { get; set; }
public DateTime ReleaseDate { get; set; } public DateTime ReleaseDate { get; set; }
public string Status { get; set; } public string Status { get; set; }
/// <summary> /// <summary>

View file

@ -29,6 +29,7 @@ namespace Ombi.Store.Entities
{ {
public enum UserType public enum UserType
{ {
SystemUser = 0,
LocalUser = 1, LocalUser = 1,
PlexUser = 2, PlexUser = 2,
EmbyUser = 3, EmbyUser = 3,

View file

@ -0,0 +1,950 @@
// <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("20180413021646_tvrequestsbackground")]
partial class tvrequestsbackground
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.0.2-rtm-10011");
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.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>("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.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.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
}
}
}

View file

@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
namespace Ombi.Store.Migrations
{
public partial class tvrequestsbackground : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Background",
table: "TvRequests",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Background",
table: "TvRequests");
}
}
}

View file

@ -0,0 +1,950 @@
// <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("20180419054711_EmbyButton")]
partial class EmbyButton
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.0.2-rtm-10011");
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>("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>("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.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.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
}
}
}

View file

@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
namespace Ombi.Store.Migrations
{
public partial class EmbyButton : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Url",
table: "EmbyContent",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Url",
table: "EmbyContent");
}
}
}

View file

@ -0,0 +1,956 @@
// <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("20180420225638_NewsletterChanges")]
partial class NewsletterChanges
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.0.2-rtm-10011");
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.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.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
}
}
}

View file

@ -0,0 +1,35 @@
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
namespace Ombi.Store.Migrations
{
public partial class NewsletterChanges : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "EpisodeNumber",
table: "RecentlyAddedLog",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "SeasonNumber",
table: "RecentlyAddedLog",
nullable: true);
migrationBuilder.Sql("DELETE FROM RecentlyAddedLog");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "EpisodeNumber",
table: "RecentlyAddedLog");
migrationBuilder.DropColumn(
name: "SeasonNumber",
table: "RecentlyAddedLog");
}
}
}

View file

@ -197,6 +197,8 @@ namespace Ombi.Store.Migrations
b.Property<int>("Type"); b.Property<int>("Type");
b.Property<string>("Url");
b.HasKey("Id"); b.HasKey("Id");
b.ToTable("EmbyContent"); b.ToTable("EmbyContent");
@ -453,6 +455,10 @@ namespace Ombi.Store.Migrations
b.Property<int>("ContentType"); b.Property<int>("ContentType");
b.Property<int?>("EpisodeNumber");
b.Property<int?>("SeasonNumber");
b.Property<int>("Type"); b.Property<int>("Type");
b.HasKey("Id"); b.HasKey("Id");
@ -645,6 +651,8 @@ namespace Ombi.Store.Migrations
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd(); .ValueGeneratedOnAdd();
b.Property<string>("Background");
b.Property<string>("ImdbId"); b.Property<string>("ImdbId");
b.Property<string>("Overview"); b.Property<string>("Overview");

View file

@ -14,7 +14,7 @@
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="2.0.2" /> <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="2.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.0.2" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.0.2" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="10.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="1.1.9" /> <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="1.1.9" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

@ -81,6 +81,7 @@ namespace Ombi.Store.Repository
public void Update(GlobalSettings entity) public void Update(GlobalSettings entity)
{ {
Db.Update(entity);
//_cache.Remove(GetName(entity.SettingsName)); //_cache.Remove(GetName(entity.SettingsName));
Db.SaveChanges(); Db.SaveChanges();
} }

View file

@ -12,6 +12,7 @@ namespace Ombi.Api.TheMovieDb
Task<List<MovieSearchResult>> NowPlaying(); Task<List<MovieSearchResult>> NowPlaying();
Task<List<MovieSearchResult>> PopularMovies(); Task<List<MovieSearchResult>> PopularMovies();
Task<List<MovieSearchResult>> SearchMovie(string searchTerm); Task<List<MovieSearchResult>> SearchMovie(string searchTerm);
Task<List<TvSearchResult>> SearchTv(string searchTerm);
Task<List<MovieSearchResult>> TopRated(); Task<List<MovieSearchResult>> TopRated();
Task<List<MovieSearchResult>> Upcoming(); Task<List<MovieSearchResult>> Upcoming();
Task<List<MovieSearchResult>> SimilarMovies(int movieId); Task<List<MovieSearchResult>> SimilarMovies(int movieId);

View file

@ -32,9 +32,12 @@ namespace Ombi.TheMovieDbApi.Models
public bool adult { get; set; } public bool adult { get; set; }
public string overview { get; set; } public string overview { get; set; }
public string release_date { get; set; } public string release_date { get; set; }
public string first_air_date { get; set; }
public int?[] genre_ids { get; set; } public int?[] genre_ids { get; set; }
public int id { get; set; } public int id { get; set; }
public string original_title { get; set; } public string original_title { get; set; }
public string original_name { get; set; }
public string name { get; set; }
public string original_language { get; set; } public string original_language { get; set; }
public string title { get; set; } public string title { get; set; }
public string backdrop_path { get; set; } public string backdrop_path { get; set; }

View file

@ -0,0 +1,18 @@
namespace Ombi.Api.TheMovieDb.Models
{
public class TvSearchResult
{
public string PosterPath { get; set; }
public string Overview { get; set; }
public string ReleaseDate { get; set; }
public int?[] GenreIds { get; set; }
public int Id { get; set; }
public string OriginalName { get; set; }
public string OriginalLanguage { get; set; }
public string Name { get; set; }
public string BackdropPath { get; set; }
public float Popularity { get; set; }
public int VoteCount { get; set; }
public float VoteAverage { get; set; }
}
}

View file

@ -42,7 +42,18 @@ namespace Ombi.Api.TheMovieDb
return await Api.Request<FindResult>(request); return await Api.Request<FindResult>(request);
} }
public async Task<List<TvSearchResult>> SearchTv(string searchTerm)
{
var request = new Request($"search/tv", BaseUri, HttpMethod.Get);
request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken);
request.FullUri = request.FullUri.AddQueryParameter("query", searchTerm);
AddRetry(request);
var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request);
return Mapper.Map<List<TvSearchResult>>(result.results);
}
public async Task<TvExternals> GetTvExternals(int theMovieDbId) public async Task<TvExternals> GetTvExternals(int theMovieDbId)
{ {
var request = new Request($"/tv/{theMovieDbId}/external_ids", BaseUri, HttpMethod.Get); var request = new Request($"/tv/{theMovieDbId}/external_ids", BaseUri, HttpMethod.Get);

View file

@ -24,6 +24,7 @@ import { CookieComponent } from "./auth/cookie.component";
import { PageNotFoundComponent } from "./errors/not-found.component"; import { PageNotFoundComponent } from "./errors/not-found.component";
import { LandingPageComponent } from "./landingpage/landingpage.component"; import { LandingPageComponent } from "./landingpage/landingpage.component";
import { LoginComponent } from "./login/login.component"; import { LoginComponent } from "./login/login.component";
import { LoginOAuthComponent } from "./login/loginoauth.component";
import { ResetPasswordComponent } from "./login/resetpassword.component"; import { ResetPasswordComponent } from "./login/resetpassword.component";
import { TokenResetPasswordComponent } from "./login/tokenresetpassword.component"; import { TokenResetPasswordComponent } from "./login/tokenresetpassword.component";
@ -41,6 +42,7 @@ const routes: Routes = [
{ path: "*", component: PageNotFoundComponent }, { path: "*", component: PageNotFoundComponent },
{ path: "", redirectTo: "/search", pathMatch: "full" }, { path: "", redirectTo: "/search", pathMatch: "full" },
{ path: "login", component: LoginComponent }, { path: "login", component: LoginComponent },
{ path: "Login/OAuth/:pin", component: LoginOAuthComponent },
{ path: "login/:landing", component: LoginComponent }, { path: "login/:landing", component: LoginComponent },
{ path: "reset", component: ResetPasswordComponent }, { path: "reset", component: ResetPasswordComponent },
{ path: "token", component: TokenResetPasswordComponent }, { path: "token", component: TokenResetPasswordComponent },
@ -116,6 +118,7 @@ export function HttpLoaderFactory(http: HttpClient, platformLocation: PlatformLo
ResetPasswordComponent, ResetPasswordComponent,
TokenResetPasswordComponent, TokenResetPasswordComponent,
CookieComponent, CookieComponent,
LoginOAuthComponent,
], ],
providers: [ providers: [
NotificationService, NotificationService,

View file

@ -2,6 +2,7 @@
username: string; username: string;
password: string; password: string;
rememberMe: boolean; rememberMe: boolean;
usePlexOAuth: boolean;
} }
export interface ILocalUser { export interface ILocalUser {

View file

@ -18,6 +18,10 @@ export class AuthService extends ServiceHelpers {
return this.http.post(`${this.url}/`, JSON.stringify(login), {headers: this.headers}); return this.http.post(`${this.url}/`, JSON.stringify(login), {headers: this.headers});
} }
public oAuth(pin: number): Observable<any> {
return this.http.get<any>(`${this.url}/${pin}`, {headers: this.headers});
}
public requiresPassword(login: IUserLogin): Observable<boolean> { public requiresPassword(login: IUserLogin): Observable<boolean> {
return this.http.post<boolean>(`${this.url}/requirePassword`, JSON.stringify(login), {headers: this.headers}); return this.http.post<boolean>(`${this.url}/requirePassword`, JSON.stringify(login), {headers: this.headers});
} }

View file

@ -2,6 +2,10 @@
user: IPlexUser; user: IPlexUser;
} }
export interface IPlexOAuthAccessToken {
accessToken: string;
}
export interface IPlexUser { export interface IPlexUser {
email: string; email: string;
uuid: string; uuid: string;

View file

@ -21,6 +21,7 @@
requested: boolean; requested: boolean;
available: boolean; available: boolean;
plexUrl: string; plexUrl: string;
embyUrl: string;
quality: string; quality: string;
digitalReleaseDate: Date; digitalReleaseDate: Date;

View file

@ -27,6 +27,7 @@ export interface ISearchTvResult {
requested: boolean; requested: boolean;
available: boolean; available: boolean;
plexUrl: string; plexUrl: string;
embyUrl: string;
firstSeason: boolean; firstSeason: boolean;
latestSeason: boolean; latestSeason: boolean;
} }

View file

@ -145,6 +145,7 @@ export interface IAuthenticationSettings extends ISettings {
requiredLowercase: boolean; requiredLowercase: boolean;
requireNonAlphanumeric: boolean; requireNonAlphanumeric: boolean;
requireUppercase: boolean; requireUppercase: boolean;
enableOAuth: boolean;
} }
export interface IUserManagementSettings extends ISettings { export interface IUserManagementSettings extends ISettings {

View file

@ -1,5 +1,5 @@
<div *ngIf="issue"> <div *ngIf="issue">
<div class="row"> <div class="row issue-details">
<div class="myBg backdrop" [style.background-image]="backgroundPath"></div> <div class="myBg backdrop" [style.background-image]="backgroundPath"></div>
<div class="tint" style="background-image: linear-gradient(to bottom, rgba(0,0,0,0.6) 0%,rgba(0,0,0,0.6) 100%);"></div> <div class="tint" style="background-image: linear-gradient(to bottom, rgba(0,0,0,0.6) 0%,rgba(0,0,0,0.6) 100%);"></div>
<h1>{{issue.title}} </h1> <h1>{{issue.title}} </h1>

View file

@ -98,7 +98,11 @@ export class IssueDetailsComponent implements OnInit {
("url(" + x + ")"); ("url(" + x + ")");
}); });
this.imageService.getMoviePoster(issue.providerId).subscribe(x => { this.imageService.getMoviePoster(issue.providerId).subscribe(x => {
this.posterPath = x.toString(); if (x.length === 0) {
this.posterPath = "../../../images/default_movie_poster.png";
} else {
this.posterPath = x.toString();
}
}); });
} else { } else {
@ -107,7 +111,11 @@ export class IssueDetailsComponent implements OnInit {
("url(" + x + ")"); ("url(" + x + ")");
}); });
this.imageService.getTvPoster(Number(issue.providerId)).subscribe(x => { this.imageService.getTvPoster(Number(issue.providerId)).subscribe(x => {
this.posterPath = x.toString(); if (x.length === 0) {
this.posterPath = "../../../images/default_tv_poster.png";
} else {
this.posterPath = x.toString();
}
}); });
} }

View file

@ -1,25 +1,25 @@
<table class="table table-striped table-hover table-responsive table-condensed"> <table class="table table-striped table-hover table-responsive table-condensed">
<thead> <thead>
<tr> <tr>
<th (click)="setOrder('title')"> <th (click)="setOrder('title', $event)">
<a [translate]="'Issues.ColumnTitle'"></a> <a [translate]="'Issues.ColumnTitle'"></a>
<span *ngIf="order === 'title'"> <span *ngIf="order === 'title'">
<span [hidden]="reverse"><i class="fa fa-arrow-down" aria-hidden="true"></i></span><span [hidden]="!reverse"><i class="fa fa-arrow-up" aria-hidden="true"></i></span> <span [hidden]="reverse"><i class="fa fa-arrow-down" aria-hidden="true"></i></span><span [hidden]="!reverse"><i class="fa fa-arrow-up" aria-hidden="true"></i></span>
</span> </span>
</th> </th>
<th (click)="setOrder('issueCategory.value')"> <th (click)="setOrder('issueCategory.value', $event)">
<a [translate]="'Issues.Category'"></a> <a [translate]="'Issues.Category'"></a>
<span *ngIf="order === 'issueCategory.value'"> <span *ngIf="order === 'issueCategory.value'">
<span [hidden]="reverse"><i class="fa fa-arrow-down" aria-hidden="true"></i></span><span [hidden]="!reverse"><i class="fa fa-arrow-up" aria-hidden="true"></i></span> <span [hidden]="reverse"><i class="fa fa-arrow-down" aria-hidden="true"></i></span><span [hidden]="!reverse"><i class="fa fa-arrow-up" aria-hidden="true"></i></span>
</span> </span>
</th> </th>
<th (click)="setOrder('status')"> <th (click)="setOrder('status', $event)">
<a [translate]="'Issues.Status'"></a> <a [translate]="'Issues.Status'"></a>
<span *ngIf="order === 'status'"> <span *ngIf="order === 'status'">
<span [hidden]="reverse"><i class="fa fa-arrow-down" aria-hidden="true"></i></span><span [hidden]="!reverse"><i class="fa fa-arrow-up" aria-hidden="true"></i></span> <span [hidden]="reverse"><i class="fa fa-arrow-down" aria-hidden="true"></i></span><span [hidden]="!reverse"><i class="fa fa-arrow-up" aria-hidden="true"></i></span>
</span> </span>
</th> </th>
<th (click)="setOrder('reportedUser')"> <th (click)="setOrder('reportedUser', $event)">
<a [translate]="'Issues.ReportedBy'"></a> <a [translate]="'Issues.ReportedBy'"></a>
<span *ngIf="order === 'reportedUser'"> <span *ngIf="order === 'reportedUser'">
<span [hidden]="reverse"><i class="fa fa-arrow-down" aria-hidden="true"></i></span><span [hidden]="!reverse"><i class="fa fa-arrow-up" aria-hidden="true"></i></span> <span [hidden]="reverse"><i class="fa fa-arrow-down" aria-hidden="true"></i></span><span [hidden]="!reverse"><i class="fa fa-arrow-up" aria-hidden="true"></i></span>

View file

@ -20,11 +20,25 @@ export class IssuesTableComponent {
public rowCount = 10; public rowCount = 10;
public setOrder(value: string) { public setOrder(value: string, el: any) {
if (this.order === value) { el = el.toElement || el.relatedTarget || el.target || el.srcElement;
this.reverse = !this.reverse;
if (el.nodeName === "A") {
el = el.parentElement;
} }
const parent = el.parentElement;
const previousFilter = parent.querySelector(".active");
if (this.order === value) {
this.reverse = !this.reverse;
} else {
if (previousFilter) {
previousFilter.className = "";
}
el.className = "active";
}
this.order = value; this.order = value;
} }

View file

@ -7,29 +7,53 @@ include the remember me checkbox
<div *ngIf="background" @fadeInOut class="bg" [style.background-image]="background"></div> <div *ngIf="background" @fadeInOut class="bg" [style.background-image]="background"></div>
<div class="container" id="login"> <div class="container" id="login">
<div class="card card-container"> <div class="card card-container">
<!-- <img class="profile-img-card" src="//lh3.googleusercontent.com/-6V8xOA6M7BA/AAAAAAAAAAI/AAAAAAAAAAA/rzlHcD0KYwo/photo.jpg?sz=120" alt="" /> --> <!-- <img class="profile-img-card" src="//lh3.googleusercontent.com/-6V8xOA6M7BA/AAAAAAAAAAI/AAAAAAAAAAA/rzlHcD0KYwo/photo.jpg?sz=120" alt="" /> -->
<div *ngIf="!customizationSettings.logo"><img id="profile-img" class="profile-img-card" src="{{baseUrl}}/images/logo.png"/></div> <div *ngIf="!customizationSettings.logo">
<div *ngIf="customizationSettings.logo"><img id="profile-img" class="center" [src]="customizationSettings.logo" /></div> <img id="profile-img" class="profile-img-card" src="{{baseUrl}}/images/logo.png" />
</div>
<div *ngIf="customizationSettings.logo">
<img id="profile-img" class="center" [src]="customizationSettings.logo" />
</div>
<p id="profile-name" class="profile-name-card"></p> <p id="profile-name" class="profile-name-card"></p>
<form *ngIf="authenticationSettings" class="form-signin" novalidate [formGroup]="form" (ngSubmit)="onSubmit(form)"> <div *ngIf="!authenticationSettings.enableOAuth || loginWithOmbi">
<form *ngIf="authenticationSettings" class="form-signin" novalidate [formGroup]="form" (ngSubmit)="onSubmit(form)">
<input type="email" id="inputEmail" class="form-control" formControlName="username" [attr.placeholder]="'Login.UsernamePlaceholder' | translate" autofocus>
<input *ngIf="!authenticationSettings.allowNoPassword" type="password" id="inputPassword" class="form-control" formControlName="password" [attr.placeholder]="'Login.PasswordPlaceholder' | translate"> <input type="email" id="inputEmail" class="form-control" formControlName="username" [attr.placeholder]="'Login.UsernamePlaceholder' | translate"
<div class="form-group"> autofocus>
<div class="checkbox"> <input *ngIf="!authenticationSettings.allowNoPassword" type="password" id="inputPassword" class="form-control" formControlName="password"
<input type="checkbox" id="RememberMe" formControlName="rememberMe" > [attr.placeholder]="'Login.PasswordPlaceholder' | translate">
<div class="form-group">
<label for="RememberMe" [translate]="'Login.RememberMe'"></label> <div class="checkbox">
<input type="checkbox" id="RememberMe" formControlName="rememberMe">
<label for="RememberMe" [translate]="'Login.RememberMe'"></label>
</div>
</div> </div>
</div> <button class="btn btn-success" type="submit" [translate]="'Login.SignInButton'"></button>
<button class="btn btn-success" type="submit" [translate]="'Login.SignInButton'"></button> <a [routerLink]="['/reset']" class="forgot-password col-md-12">
</form><!-- /form --> <b [translate]="'Login.ForgottenPassword'"></b>
<a [routerLink]="['/reset']" class="forgot-password col-md-12"> </a>
<b [translate]="'Login.ForgottenPassword'"></b> </form>
</a> <!-- /form -->
</div><!-- /card-container --> </div>
</div><!-- /container --> <!-- Main OAuth Flow -->
<div *ngIf="authenticationSettings.enableOAuth && !loginWithOmbi">
<div class="form-signin">
<button class="btn btn-success" type="button" (click)="loginWithOmbi = true">
Sign In With {{appName}}</button>
</div>
<div class="form-signin">
<button class="btn btn-primary" type="button" (click)="oauth()">
Sign In With Plex</button>
</div>
</div>
</div>
<!-- /card-container -->
</div>
<!-- /container -->
</div> </div>

View file

@ -25,9 +25,20 @@ export class LoginComponent implements OnDestroy, OnInit {
public form: FormGroup; public form: FormGroup;
public customizationSettings: ICustomizationSettings; public customizationSettings: ICustomizationSettings;
public authenticationSettings: IAuthenticationSettings; public authenticationSettings: IAuthenticationSettings;
public plexEnabled: boolean;
public background: any; public background: any;
public landingFlag: boolean; public landingFlag: boolean;
public baseUrl: string; public baseUrl: string;
public loginWithOmbi: boolean;
public get appName(): string {
if(this.customizationSettings.applicationName) {
return this.customizationSettings.applicationName;
} else {
return "Ombi";
}
}
private timer: any; private timer: any;
private errorBody: string; private errorBody: string;
@ -90,7 +101,7 @@ export class LoginComponent implements OnDestroy, OnInit {
return; return;
} }
const value = form.value; const value = form.value;
const user = { password: value.password, username: value.username, rememberMe:value.rememberMe }; const user = { password: value.password, username: value.username, rememberMe: value.rememberMe, usePlexOAuth: false };
this.authService.requiresPassword(user).subscribe(x => { this.authService.requiresPassword(user).subscribe(x => {
if(x && this.authenticationSettings.allowNoPassword) { if(x && this.authenticationSettings.allowNoPassword) {
// Looks like this user requires a password // Looks like this user requires a password
@ -111,6 +122,18 @@ export class LoginComponent implements OnDestroy, OnInit {
}); });
} }
public oauth() {
this.authService.login({usePlexOAuth: true, password:"",rememberMe:true,username:""}).subscribe(x => {
if (window.frameElement) {
// in frame
window.open(x.url, "_blank");
} else {
// not in frame
window.location.href = x.url;
}
});
}
public ngOnDestroy() { public ngOnDestroy() {
clearInterval(this.timer); clearInterval(this.timer);
} }
@ -124,5 +147,4 @@ export class LoginComponent implements OnDestroy, OnInit {
.bypassSecurityTrustStyle("linear-gradient(-10deg, transparent 20%, rgba(0,0,0,0.7) 20.0%, rgba(0,0,0,0.7) 80.0%, transparent 80%), url(" + x.url + ")"); .bypassSecurityTrustStyle("linear-gradient(-10deg, transparent 20%, rgba(0,0,0,0.7) 20.0%, rgba(0,0,0,0.7) 80.0%, transparent 80%), url(" + x.url + ")");
}); });
} }
} }

View file

@ -0,0 +1,16 @@

<div >
<div class="container" id="login">
<div class="card card-container">
<label>Please Wait...</label>
<div *ngIf="error">
<label>{{error}}</label>
<a [routerLink]="['/login']">
Back </a>
</div>
</div><!-- /card-container -->
</div><!-- /container -->
</div>

View file

@ -0,0 +1,47 @@
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { AuthService } from "../auth/auth.service";
import { NotificationService } from "../services";
@Component({
templateUrl: "./loginoauth.component.html",
})
export class LoginOAuthComponent implements OnInit {
public pin: number;
public error: string;
constructor(private authService: AuthService, private router: Router,
private route: ActivatedRoute, private notify: NotificationService) {
this.route.params
.subscribe((params: any) => {
this.pin = params.pin;
});
}
public ngOnInit(): void {
this.auth();
}
public auth() {
this.authService.oAuth(this.pin).subscribe(x => {
if(x.access_token) {
localStorage.setItem("id_token", x.access_token);
if (this.authService.loggedIn()) {
this.router.navigate(["search"]);
return;
}
}
if(x.errorMessage) {
this.error = x.errorMessage;
}
}, err => {
this.notify.error(err.statusText);
this.router.navigate(["login"]);
});
}
}

View file

@ -56,7 +56,7 @@
<br /> <br />
<div infinite-scroll [infiniteScrollDistance]="1" [infiniteScrollThrottle]="100" (scrolled)="loadMore()"> <div>
<div *ngFor="let request of movieRequests | orderBy: order : reverse : 'case-insensitive'"> <div *ngFor="let request of movieRequests | orderBy: order : reverse : 'case-insensitive'">
@ -67,7 +67,7 @@
<div class="tint" style="background-image: linear-gradient(to bottom, rgba(0,0,0,0.6) 0%,rgba(0,0,0,0.6) 100%);"></div> <div class="tint" style="background-image: linear-gradient(to bottom, rgba(0,0,0,0.6) 0%,rgba(0,0,0,0.6) 100%);"></div>
<div class="col-sm-2 small-padding"> <div class="col-sm-2 small-padding">
<img class="img-responsive poster" src="https://image.tmdb.org/t/p/w300/{{request.posterPath}}" alt="poster"> <img class="img-responsive poster" src="{{request.posterPath}}" alt="poster">
</div> </div>
@ -212,6 +212,8 @@
</div> </div>
<p-paginator [rows]="10" [totalRecords]="totalMovies" (onPageChange)="paginate($event)"></p-paginator>
</div> </div>
@ -222,42 +224,43 @@
<p-sidebar [(visible)]="filterDisplay" styleClass="ui-sidebar-md side-back side-small"> <p-sidebar [(visible)]="filterDisplay" styleClass="ui-sidebar-md side-back side-small">
<h3>{{ 'Requests.Filter' | translate }}</h3> <h3>{{ 'Requests.Filter' | translate }}</h3>
<hr> <hr>
<div>
<h4>{{ 'Filter.FilterHeaderAvailability' | translate }}</h4> <h4>{{ 'Filter.FilterHeaderAvailability' | translate }}</h4>
<div class="form-group"> <div class="form-group">
<div class="radio"> <div class="radio">
<input type="radio" id="Available" name="Availability" (click)="filterAvailability(filterType.Available)"> <input type="radio" id="Available" name="Availability" (click)="filterAvailability(filterType.Available, $event)">
<label for="Available">{{ 'Common.Available' | translate }}</label> <label for="Available">{{ 'Common.Available' | translate }}</label>
</div>
</div>
<div class="form-group">
<div class="radio">
<input type="radio" id="notAvailable" name="Availability" (click)="filterAvailability(filterType.NotAvailable, $event)">
<label for="notAvailable">{{ 'Common.NotAvailable' | translate }}</label>
</div>
</div> </div>
</div> </div>
<div class="form-group"> <div>
<div class="radio"> <h4>{{ 'Filter.FilterHeaderRequestStatus' | translate }}</h4>
<input type="radio" id="notAvailable" name="Availability" (click)="filterAvailability(filterType.NotAvailable)"> <div class="form-group">
<label for="notAvailable">{{ 'Common.NotAvailable' | translate }}</label> <div class="radio">
<input type="radio" id="approved" name="Status" (click)="filterStatus(filterType.Approved, $event)">
<label for="approved">{{ 'Filter.Approved' | translate }}</label>
</div>
</div>
<div class="form-group">
<div class="radio">
<input type="radio" id="Processing" name="Status" (click)="filterStatus(filterType.Processing, $event)">
<label for="Processing">{{ 'Common.ProcessingRequest' | translate }}</label>
</div>
</div>
<div class="form-group">
<div class="radio">
<input type="radio" id="pendingApproval" name="Status" (click)="filterStatus(filterType.PendingApproval, $event)">
<label for="pendingApproval">{{ 'Filter.PendingApproval' | translate }}</label>
</div>
</div> </div>
</div> </div>
<h4>{{ 'Filter.FilterHeaderRequestStatus' | translate }}</h4> <button class="btn btn-sm btn-primary-outline" (click)="clearFilter($event)">
<div class="form-group">
<div class="radio">
<input type="radio" id="approved" name="Status" (click)="filterStatus(filterType.Approved)">
<label for="approved">{{ 'Filter.Approved' | translate }}</label>
</div>
</div>
<div class="form-group">
<div class="radio">
<input type="radio" id="Processing" name="Status" (click)="filterStatus(filterType.Processing)">
<label for="Processing">{{ 'Common.ProcessingRequest' | translate }}</label>
</div>
</div>
<div class="form-group">
<div class="radio">
<input type="radio" id="pendingApproval" name="Status" (click)="filterStatus(filterType.PendingApproval, $event)">
<label for="pendingApproval">{{ 'Filter.PendingApproval' | translate }}</label>
</div>
</div>
<button class="btn btn-sm btn-primary-outline" (click)="clearFilter()">
<i class="fa fa-filter"></i> {{ 'Filter.ClearFilter' | translate }}</button> <i class="fa fa-filter"></i> {{ 'Filter.ClearFilter' | translate }}</button>
</p-sidebar> </p-sidebar>

View file

@ -8,7 +8,7 @@ import { Subject } from "rxjs/Subject";
import { AuthService } from "../auth/auth.service"; import { AuthService } from "../auth/auth.service";
import { NotificationService, RadarrService, RequestService } from "../services"; import { NotificationService, RadarrService, RequestService } from "../services";
import { FilterType, IFilter, IIssueCategory, IMovieRequests, IRadarrProfile, IRadarrRootFolder } from "../interfaces"; import { FilterType, IFilter, IIssueCategory, IMovieRequests, IPagenator, IRadarrProfile, IRadarrRootFolder } from "../interfaces";
@Component({ @Component({
selector: "movie-requests", selector: "movie-requests",
@ -39,6 +39,7 @@ export class MovieRequestsComponent implements OnInit {
public order: string = "requestedDate"; public order: string = "requestedDate";
public reverse = false; public reverse = false;
public totalMovies: number = 100;
private currentlyLoaded: number; private currentlyLoaded: number;
private amountToLoad: number; private amountToLoad: number;
@ -65,8 +66,8 @@ export class MovieRequestsComponent implements OnInit {
} }
public ngOnInit() { public ngOnInit() {
this.amountToLoad = 100; this.amountToLoad = 10;
this.currentlyLoaded = 100; this.currentlyLoaded = 10;
this.loadInit(); this.loadInit();
this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser"); this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser");
this.filter = { this.filter = {
@ -74,8 +75,10 @@ export class MovieRequestsComponent implements OnInit {
statusFilter: FilterType.None}; statusFilter: FilterType.None};
} }
public loadMore() { public paginate(event: IPagenator) {
this.loadRequests(this.amountToLoad, this.currentlyLoaded); const skipAmount = event.first;
this.loadRequests(this.amountToLoad, skipAmount);
} }
public search(text: any) { public search(text: any) {
@ -149,7 +152,16 @@ export class MovieRequestsComponent implements OnInit {
event.preventDefault(); event.preventDefault();
} }
public clearFilter() { public clearFilter(el: any) {
el = el.toElement || el.relatedTarget || el.target || el.srcElement;
el = el.parentElement;
el = el.querySelectorAll("INPUT");
for (el of el) {
el.checked = false;
el.parentElement.classList.remove("active");
}
this.filterDisplay = false; this.filterDisplay = false;
this.filter.availabilityFilter = FilterType.None; this.filter.availabilityFilter = FilterType.None;
this.filter.statusFilter = FilterType.None; this.filter.statusFilter = FilterType.None;
@ -157,7 +169,8 @@ export class MovieRequestsComponent implements OnInit {
this.resetSearch(); this.resetSearch();
} }
public filterAvailability(filter: FilterType) { public filterAvailability(filter: FilterType, el: any) {
this.filterActiveStyle(el);
this.filter.availabilityFilter = filter; this.filter.availabilityFilter = filter;
this.requestService.filterMovies(this.filter) this.requestService.filterMovies(this.filter)
.subscribe(x => { .subscribe(x => {
@ -166,7 +179,8 @@ export class MovieRequestsComponent implements OnInit {
}); });
} }
public filterStatus(filter: FilterType) { public filterStatus(filter: FilterType, el: any) {
this.filterActiveStyle(el);
this.filter.statusFilter = filter; this.filter.statusFilter = filter;
this.requestService.filterMovies(this.filter) this.requestService.filterMovies(this.filter)
.subscribe(x => { .subscribe(x => {
@ -190,6 +204,24 @@ export class MovieRequestsComponent implements OnInit {
this.order = value; this.order = value;
} }
private filterActiveStyle(el: any) {
el = el.toElement || el.relatedTarget || el.target || el.srcElement;
el = el.parentElement; //gets radio div
el = el.parentElement; //gets form group div
el = el.parentElement; //gets status filter div
el = el.querySelectorAll("INPUT");
for (el of el) {
if (el.checked) {
if (!el.parentElement.classList.contains("active")) {
el.parentElement.className += " active";
}
} else {
el.parentElement.classList.remove("active");
}
}
}
private loadRequests(amountToLoad: number, currentlyLoaded: number) { private loadRequests(amountToLoad: number, currentlyLoaded: number) {
this.requestService.getMovieRequests(amountToLoad, currentlyLoaded + 1) this.requestService.getMovieRequests(amountToLoad, currentlyLoaded + 1)
.subscribe(x => { .subscribe(x => {
@ -197,7 +229,7 @@ export class MovieRequestsComponent implements OnInit {
if(!this.movieRequests) { if(!this.movieRequests) {
this.movieRequests = []; this.movieRequests = [];
} }
this.movieRequests.push.apply(this.movieRequests, x); this.movieRequests = x;
this.currentlyLoaded = currentlyLoaded + amountToLoad; this.currentlyLoaded = currentlyLoaded + amountToLoad;
}); });
} }
@ -238,12 +270,14 @@ export class MovieRequestsComponent implements OnInit {
} }
private loadInit() { private loadInit() {
this.requestService.getTotalMovies().subscribe(x => this.totalMovies = x);
this.requestService.getMovieRequests(this.amountToLoad, 0) this.requestService.getMovieRequests(this.amountToLoad, 0)
.subscribe(x => { .subscribe(x => {
this.movieRequests = x; this.movieRequests = x;
this.movieRequests.forEach((req) => { this.movieRequests.forEach((req) => {
this.movieRequests.forEach((req) => this.setBackground(req)); this.setBackground(req);
this.setPoster(req);
}); });
this.radarrService.getQualityProfilesFromSettings().subscribe(c => { this.radarrService.getQualityProfilesFromSettings().subscribe(c => {
this.radarrProfiles = c; this.radarrProfiles = c;
@ -296,11 +330,20 @@ export class MovieRequestsComponent implements OnInit {
} }
private setOverride(req: IMovieRequests): void { private setOverride(req: IMovieRequests): void {
this.setPoster(req);
this.setBackground(req); this.setBackground(req);
this.setQualityOverrides(req); this.setQualityOverrides(req);
this.setRootFolderOverrides(req); this.setRootFolderOverrides(req);
} }
private setPoster(req: IMovieRequests): void {
if (req.posterPath === null) {
req.posterPath = "../../../images/default_movie_poster.png";
} else {
req.posterPath = "https://image.tmdb.org/t/p/w300/" + req.posterPath;
}
}
private setBackground(req: IMovieRequests): void { private setBackground(req: IMovieRequests): void {
req.backgroundPath = this.sanitizer.bypassSecurityTrustStyle req.backgroundPath = this.sanitizer.bypassSecurityTrustStyle
("url(" + "https://image.tmdb.org/t/p/w1280" + req.background + ")"); ("url(" + "https://image.tmdb.org/t/p/w1280" + req.background + ")");

View file

@ -6,7 +6,7 @@ import { OrderModule } from "ngx-order-pipe";
import { InfiniteScrollModule } from "ngx-infinite-scroll"; import { InfiniteScrollModule } from "ngx-infinite-scroll";
import { ButtonModule, DialogModule } from "primeng/primeng"; import { ButtonModule, DialogModule, PaginatorModule } from "primeng/primeng";
import { MovieRequestsComponent } from "./movierequests.component"; import { MovieRequestsComponent } from "./movierequests.component";
// Request // Request
import { RequestComponent } from "./request.component"; import { RequestComponent } from "./request.component";
@ -36,6 +36,7 @@ const routes: Routes = [
SharedModule, SharedModule,
SidebarModule, SidebarModule,
OrderModule, OrderModule,
PaginatorModule,
], ],
declarations: [ declarations: [
RequestComponent, RequestComponent,

View file

@ -22,15 +22,7 @@
<button id="removeBtn" type="button" (click)="removeRequest(child)" class="btn btn-sm btn-danger-outline deny"><i class="fa fa-times"></i> {{ 'Requests.Remove' | translate }}</button> <button id="removeBtn" type="button" (click)="removeRequest(child)" class="btn btn-sm btn-danger-outline deny"><i class="fa fa-times"></i> {{ 'Requests.Remove' | translate }}</button>
</div> </div>
<div class="dropdown" *ngIf="issueCategories && issuesEnabled" id="issueBtn">
<button class="btn btn-sm btn-primary-outline dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<i class="fa fa-plus"></i> {{ 'Requests.ReportIssue' | translate }}
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu1">
<li *ngFor="let cat of issueCategories"><a [routerLink]="" (click)="reportIssue(cat, child)">{{cat.value}}</a></li>
</ul>
</div>
</div> </div>
</div> </div>
@ -101,8 +93,3 @@
</div> </div>
</div> </div>
<issue-report [movie]="false" [visible]="issuesBarVisible" [title]="issueRequest?.title"
[issueCategory]="issueCategorySelected" [id]="issueRequest?.id" [providerId]="issueProviderId" (visibleChange)="issuesBarVisible = $event;"></issue-report>

View file

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

View file

@ -64,51 +64,64 @@
</div> </div>
<div class="col-sm-3 col-sm-push-3 small-padding"> <div class="col-sm-3 col-sm-push-3 small-padding">
<button style="text-align: right" class="btn btn-sm btn-success-outline" (click)="openClosestTab($event)"><i class="fa fa-plus"></i> View</button> <button style="text-align: right" class="btn btn-sm btn-success-outline" (click)="openClosestTab($event)"><i class="fa fa-plus"></i> View</button>
<div *ngIf="isAdmin"> <div *ngIf="isAdmin">
<!--Sonarr Root Folder--> <!--Sonarr Root Folder-->
<div *ngIf="sonarrRootFolders" class="btn-group btn-split" id="rootFolderBtn"> <div *ngIf="sonarrRootFolders" class="btn-group btn-split" id="rootFolderBtn">
<button type="button" class="btn btn-sm btn-warning-outline"> <button type="button" class="btn btn-sm btn-warning-outline">
<i class="fa fa-plus"></i> {{ 'Requests.ChangeRootFolder' | translate }}</button> <i class="fa fa-plus"></i> {{ 'Requests.ChangeRootFolder' | translate }}
<button type="button" class="btn btn-warning-outline dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> </button>
<span class="caret"></span> <button type="button" class="btn btn-warning-outline dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="sr-only">Toggle Dropdown</span> <span class="caret"></span>
</button> <span class="sr-only">Toggle Dropdown</span>
<ul class="dropdown-menu"> </button>
<li *ngFor="let folder of sonarrRootFolders"> <ul class="dropdown-menu">
<a href="#" (click)="selectRootFolder(node.data, folder, $event)">{{folder.path}}</a> <li *ngFor="let folder of sonarrRootFolders">
</li> <a href="#" (click)="selectRootFolder(node.data, folder, $event)">{{folder.path}}</a>
</ul> </li>
</ul>
</div>
<!--Sonarr Quality Profiles -->
<div *ngIf="sonarrProfiles" class="btn-group btn-split" id="changeQualityBtn">
<button type="button" class="btn btn-sm btn-warning-outline">
<i class="fa fa-plus"></i> {{ 'Requests.ChangeQualityProfile' | translate }}
</button>
<button type="button" class="btn btn-warning-outline dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu">
<li *ngFor="let profile of sonarrProfiles">
<a href="#" (click)="selectQualityProfile(node.data, profile, $event)">{{profile.name}}</a>
</li>
</ul>
</div>
</div> </div>
<div class="dropdown" *ngIf="issueCategories && issuesEnabled" id="issueBtn">
<!--Sonarr Quality Profiles --> <button class="btn btn-sm btn-primary-outline dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<div *ngIf="sonarrProfiles" class="btn-group btn-split" id="changeQualityBtn"> <i class="fa fa-plus"></i> {{ 'Requests.ReportIssue' | translate }}
<button type="button" class="btn btn-sm btn-warning-outline">
<i class="fa fa-plus"></i> {{ 'Requests.ChangeQualityProfile' | translate }}</button>
<button type="button" class="btn btn-warning-outline dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="caret"></span> <span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu" aria-labelledby="dropdownMenu1">
<li *ngFor="let profile of sonarrProfiles"> <li *ngFor="let cat of issueCategories"><a [routerLink]="" (click)="reportIssue(cat, node.data)">{{cat.value}}</a></li>
<a href="#" (click)="selectQualityProfile(node.data, profile, $event)">{{profile.name}}</a>
</li>
</ul> </ul>
</div> </div>
</div>
</div> </div>
</div> </div>
</div> </div>
<!--This is the section that holds the child seasons if they want to specify specific episodes--> <!--This is the section that holds the child seasons if they want to specify specific episodes-->
<div *ngIf="node.leaf"> <div *ngIf="node.leaf">
<tvrequests-children [childRequests]="node.data" [isAdmin] ="isAdmin" <tvrequests-children [childRequests]="node.data" [isAdmin] ="isAdmin"
(requestDeleted)="childRequestDeleted($event)" (requestDeleted)="childRequestDeleted($event)"></tvrequests-children>
[issueCategories]="issueCategories" [issuesEnabled]="issuesEnabled"
[issueProviderId]="node.data.tvDbId"></tvrequests-children>
</div> </div>
</ng-template> </ng-template>
</p-column> </p-column>
</p-treeTable> </p-treeTable>
<p-paginator [rows]="10" [totalRecords]="totalTv" (onPageChange)="paginate($event)"></p-paginator>
</div> </div>
<issue-report [movie]="false" [visible]="issuesBarVisible" [title]="issueRequest?.title"
[issueCategory]="issueCategorySelected" [id]="issueRequest?.id" [providerId]="issueProviderId" (visibleChange)="issuesBarVisible = $event;"></issue-report>

View file

@ -14,7 +14,7 @@ import { AuthService } from "../auth/auth.service";
import { NotificationService, RequestService, SonarrService } from "../services"; import { NotificationService, RequestService, SonarrService } from "../services";
import { TreeNode } from "primeng/primeng"; import { TreeNode } from "primeng/primeng";
import { IIssueCategory, ISonarrProfile, ISonarrRootFolder, ITvRequests } from "../interfaces"; import { IIssueCategory, IPagenator, ISonarrProfile, ISonarrRootFolder, ITvRequests } from "../interfaces";
@Component({ @Component({
selector: "tv-requests", selector: "tv-requests",
@ -33,10 +33,14 @@ export class TvRequestsComponent implements OnInit {
@Input() public issueCategories: IIssueCategory[]; @Input() public issueCategories: IIssueCategory[];
@Input() public issuesEnabled: boolean; @Input() public issuesEnabled: boolean;
public issueProviderId: string; public issueProviderId: string;
public issuesBarVisible = false;
public issueRequest: ITvRequests;
public issueCategorySelected: IIssueCategory;
public sonarrProfiles: ISonarrProfile[] = []; public sonarrProfiles: ISonarrProfile[] = [];
public sonarrRootFolders: ISonarrRootFolder[] = []; public sonarrRootFolders: ISonarrRootFolder[] = [];
public totalTv: number = 100;
private currentlyLoaded: number; private currentlyLoaded: number;
private amountToLoad: number; private amountToLoad: number;
@ -102,25 +106,22 @@ export class TvRequestsComponent implements OnInit {
} }
public ngOnInit() { public ngOnInit() {
this.amountToLoad = 1000; this.amountToLoad = 10;
this.currentlyLoaded = 1000; this.currentlyLoaded = 10;
this.tvRequests = []; this.tvRequests = [];
this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser"); this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser");
this.loadInit(); this.loadInit();
} }
public loadMore() { public paginate(event: IPagenator) {
//TODO: I believe this +1 is causing off by one error skipping loading of tv shows const skipAmount = event.first;
//When removed and scrolling very slowly everything works as expected, however
//if you scroll really quickly then you start getting duplicates of movies this.requestService.getTvRequestsTree(this.amountToLoad, skipAmount)
//since it's async and some subsequent results return first and then incrementer .subscribe(x => {
//is increased so you see movies which had already been gotten show up... this.tvRequests = x;
this.requestService.getTvRequestsTree(this.amountToLoad, this.currentlyLoaded + 1) this.currentlyLoaded = this.currentlyLoaded + this.amountToLoad;
.subscribe(x => { });
this.tvRequests = x;
this.currentlyLoaded = this.currentlyLoaded + this.amountToLoad;
});
} }
public search(text: any) { public search(text: any) {
@ -151,6 +152,13 @@ export class TvRequestsComponent implements OnInit {
this.updateRequest(searchResult); this.updateRequest(searchResult);
} }
public reportIssue(catId: IIssueCategory, req: ITvRequests) {
this.issueRequest = req;
this.issueCategorySelected = catId;
this.issuesBarVisible = true;
this.issueProviderId = req.id.toString();
}
private setOverride(req: ITvRequests): void { private setOverride(req: ITvRequests): void {
this.setQualityOverrides(req); this.setQualityOverrides(req);
this.setRootFolderOverrides(req); this.setRootFolderOverrides(req);
@ -187,10 +195,12 @@ export class TvRequestsComponent implements OnInit {
} }
private loadInit() { private loadInit() {
this.requestService.getTotalTv().subscribe(x => this.totalTv = x);
this.requestService.getTvRequestsTree(this.amountToLoad, 0) this.requestService.getTvRequestsTree(this.amountToLoad, 0)
.subscribe(x => { .subscribe(x => {
this.tvRequests = x; this.tvRequests = x;
this.tvRequests.forEach((val, index) => { this.tvRequests.forEach((val, index) => {
this.setDefaults(val);
this.loadBackdrop(val); this.loadBackdrop(val);
this.setOverride(val.data); this.setOverride(val.data);
}); });
@ -209,10 +219,22 @@ export class TvRequestsComponent implements OnInit {
this.currentlyLoaded = 5; this.currentlyLoaded = 5;
this.loadInit(); this.loadInit();
} }
private setDefaults(val: any) {
if (val.data.posterPath === null) {
val.data.posterPath = "../../../images/default_tv_poster.png";
}
}
private loadBackdrop(val: TreeNode): void { private loadBackdrop(val: TreeNode): void {
this.imageService.getTvBanner(val.data.tvDbId).subscribe(x => { if (val.data.background != null) {
val.data.background = this.sanitizer.bypassSecurityTrustStyle val.data.background = this.sanitizer.bypassSecurityTrustStyle
("url(" + x + ")"); ("url(https://image.tmdb.org/t/p/w1280" + val.data.background + ")");
} else {
this.imageService.getTvBanner(val.data.tvDbId).subscribe(x => {
val.data.background = this.sanitizer.bypassSecurityTrustStyle
("url(" + x + ")");
}); });
}
} }
} }

View file

@ -33,7 +33,7 @@
<div class="myBg backdrop" [style.background-image]="result.background"></div> <div class="myBg backdrop" [style.background-image]="result.background"></div>
<div class="tint" style="background-image: linear-gradient(to bottom, rgba(0,0,0,0.6) 0%,rgba(0,0,0,0.6) 100%);"></div> <div class="tint" style="background-image: linear-gradient(to bottom, rgba(0,0,0,0.6) 0%,rgba(0,0,0,0.6) 100%);"></div>
<div class="col-sm-2 small-padding"> <div class="col-sm-2 small-padding">
<img *ngIf="result.posterPath" class="img-responsive poster" src="https://image.tmdb.org/t/p/w300/{{result.posterPath}}" alt="poster"> <img *ngIf="result.posterPath" class="img-responsive poster" src="{{result.posterPath}}" alt="poster">
</div> </div>
<div class="col-sm-8 small-padding"> <div class="col-sm-8 small-padding">
@ -85,6 +85,7 @@
<br/> <br/>
<div *ngIf="result.available"> <div *ngIf="result.available">
<a *ngIf="result.plexUrl" style="text-align: right" class="btn btn-sm btn-success-outline" href="{{result.plexUrl}}" target="_blank"><i class="fa fa-eye"></i> View On Plex</a> <a *ngIf="result.plexUrl" style="text-align: right" class="btn btn-sm btn-success-outline" href="{{result.plexUrl}}" target="_blank"><i class="fa fa-eye"></i> View On Plex</a>
<a *ngIf="result.embyUrl" style="text-align: right" class="btn btn-sm btn-success-outline" href="{{result.embyUrl}}" target="_blank"><i class="fa fa-eye"></i> View On Emby</a>
</div> </div>
<div class="dropdown" *ngIf="result.available && issueCategories && issuesEnabled"> <div class="dropdown" *ngIf="result.available && issueCategories && issuesEnabled">
<button class="btn btn-sm btn-primary-outline dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true"> <button class="btn btn-sm btn-primary-outline dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">

View file

@ -157,12 +157,15 @@ export class MovieSearchComponent implements OnInit {
private getExtraInfo() { private getExtraInfo() {
this.movieResults.forEach((val, index) => { this.movieResults.forEach((val, index) => {
if (val.posterPath === null) {
val.background = this.sanitizer. val.posterPath = "../../../images/default_movie_poster.png";
bypassSecurityTrustStyle } else {
("url(" + "https://image.tmdb.org/t/p/w1280" + val.backdropPath + ")"); val.posterPath = "https://image.tmdb.org/t/p/w300/" + val.posterPath;
this.searchService.getMovieInformation(val.id) }
val.background = this.sanitizer.bypassSecurityTrustStyle
("url(" + "https://image.tmdb.org/t/p/w1280" + val.backdropPath + ")");
this.searchService.getMovieInformation(val.id)
.subscribe(m => { .subscribe(m => {
this.updateItem(val, m); this.updateItem(val, m);
}); });
@ -174,7 +177,8 @@ export class MovieSearchComponent implements OnInit {
if (index > -1) { if (index > -1) {
const copy = { ...this.movieResults[index] }; const copy = { ...this.movieResults[index] };
this.movieResults[index] = updated; this.movieResults[index] = updated;
this.movieResults[index].background = copy.background; this.movieResults[index].background = copy.background;
this.movieResults[index].posterPath = copy.posterPath;
} }
} }
private clearResults() { private clearResults() {

View file

@ -62,7 +62,7 @@
<div class="col-sm-8 small-padding"> <div class="col-sm-8 small-padding">
<div> <div>
<a *ngIf="node.data.imdbId" href="http://www.imdb.com/title/{{node.data.imdbId}}/" target="_blank"> <a *ngIf="node.data.imdbId" href="{{node.data.imdbId}}" target="_blank">
<h4>{{node.data.title}} ({{node.data.firstAired | date: 'yyyy'}})</h4> <h4>{{node.data.title}} ({{node.data.firstAired | date: 'yyyy'}})</h4>
</a> </a>
@ -118,17 +118,25 @@
<div *ngIf="node.data.fullyAvailable"> <div *ngIf="node.data.fullyAvailable">
<button style="text-align: right" class="btn btn-success-outline disabled" disabled> <button style="text-align: right" class="btn btn-success-outline disabled" disabled>
<i class="fa fa-check"></i> {{ 'Common.Available' | translate }}</button> <i class="fa fa-check"></i> {{ 'Common.Available' | translate }}
</button>
</div> </div>
<br /> <br />
<div *ngIf="node.data.plexUrl && node.data.available"> <div *ngIf="node.data.plexUrl && node.data.available">
<a style="text-align: right" class="btn btn-sm btn-success-outline" href="{{node.data.plexUrl}}" <a style="text-align: right" class="btn btn-sm btn-success-outline" href="{{node.data.plexUrl}}"
target="_blank"> target="_blank">
<i class="fa fa-eye"></i> {{ 'Search.ViewOnPlex' | translate }}</a> <i class="fa fa-eye"></i> {{ 'Search.ViewOnPlex' | translate }}
</div> </a>
</div>
<div *ngIf="node.data.embyUrl && node.data.available">
<a style="text-align: right" class="btn btn-sm btn-success-outline" href="{{node.data.embyUrl}}"
target="_blank">
<i class="fa fa-eye"></i> {{ 'Search.ViewOnEmby' | translate }}
</a>
</div>
<div class="dropdown" *ngIf="issueCategories && issuesEnabled"> <div class="dropdown" *ngIf="issueCategories && issuesEnabled">
<button class="btn btn-sm btn-primary-outline dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" <button class="btn btn-sm btn-primary-outline dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true"
aria-expanded="true"> aria-expanded="true">
<i class="fa fa-plus"></i> {{ 'Requests.ReportIssue' | translate }} <i class="fa fa-plus"></i> {{ 'Requests.ReportIssue' | translate }}
<span class="caret"></span> <span class="caret"></span>
</button> </button>
@ -139,8 +147,8 @@
</ul> </ul>
</div> </div>
<div *ngIf="!node.data.available"> <div *ngIf="!node.data.available">
<br/> <br />
<br/> <br />
</div> </div>
</div> </div>

View file

@ -130,7 +130,6 @@ export class TvSearchComponent implements OnInit {
public getExtraInfo() { public getExtraInfo() {
this.tvResults.forEach((val, index) => { this.tvResults.forEach((val, index) => {
this.imageService.getTvBanner(val.data.id).subscribe(x => { this.imageService.getTvBanner(val.data.id).subscribe(x => {
val.data.background = this.sanitizer. val.data.background = this.sanitizer.
bypassSecurityTrustStyle bypassSecurityTrustStyle
("url(" + x + ")"); ("url(" + x + ")");
@ -138,6 +137,7 @@ export class TvSearchComponent implements OnInit {
this.searchService.getShowInformationTreeNode(val.data.id) this.searchService.getShowInformationTreeNode(val.data.id)
.subscribe(x => { .subscribe(x => {
if (x.data) { if (x.data) {
this.setDefaults(x);
this.updateItem(val, x); this.updateItem(val, x);
} else { } else {
const index = this.tvResults.indexOf(val, 0); const index = this.tvResults.indexOf(val, 0);
@ -216,6 +216,7 @@ export class TvSearchComponent implements OnInit {
const index = this.tvResults.indexOf(key, 0); const index = this.tvResults.indexOf(key, 0);
if (index > -1) { if (index > -1) {
// Update certain properties, otherwise we will loose some data // Update certain properties, otherwise we will loose some data
this.tvResults[index].data.title = updated.data.title;
this.tvResults[index].data.banner = updated.data.banner; this.tvResults[index].data.banner = updated.data.banner;
this.tvResults[index].data.imdbId = updated.data.imdbId; this.tvResults[index].data.imdbId = updated.data.imdbId;
this.tvResults[index].data.seasonRequests = updated.data.seasonRequests; this.tvResults[index].data.seasonRequests = updated.data.seasonRequests;
@ -225,6 +226,18 @@ export class TvSearchComponent implements OnInit {
} }
} }
private setDefaults(x: any) {
if (x.data.banner === null) {
x.data.banner = "../../../images/default_tv_poster.png";
}
if (x.data.imdbId === null) {
x.data.imdbId = "https://www.tvmaze.com/shows/" + x.data.seriesId;
} else {
x.data.imdbId = "http://www.imdb.com/title/" + x.data.imdbId + "/";
}
}
private clearResults() { private clearResults() {
this.tvResults = []; this.tvResults = [];
this.searchApplied = false; this.searchApplied = false;

View file

@ -4,3 +4,4 @@ export * from "./plex.service";
export * from "./radarr.service"; export * from "./radarr.service";
export * from "./sonarr.service"; export * from "./sonarr.service";
export * from "./tester.service"; export * from "./tester.service";
export * from "./plexoauth.service";

View file

@ -29,4 +29,8 @@ export class PlexService extends ServiceHelpers {
public getFriends(): Observable<IUsersModel[]> { public getFriends(): Observable<IUsersModel[]> {
return this.http.get<IUsersModel[]>(`${this.url}Friends`, {headers: this.headers}); return this.http.get<IUsersModel[]>(`${this.url}Friends`, {headers: this.headers});
} }
public oAuth(wizard: boolean): Observable<any> {
return this.http.get<any>(`${this.url}oauth/${wizard}`, {headers: this.headers});
}
} }

View file

@ -0,0 +1,20 @@
import { PlatformLocation } from "@angular/common";
import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Observable } from "rxjs/Rx";
import { ServiceHelpers } from "../service.helpers";
import { IPlexOAuthAccessToken } from "../../interfaces";
@Injectable()
export class PlexOAuthService extends ServiceHelpers {
constructor(http: HttpClient, public platformLocation: PlatformLocation) {
super(http, "/api/v1/PlexOAuth/", platformLocation);
}
public oAuth(pin: number): Observable<IPlexOAuthAccessToken> {
return this.http.get<IPlexOAuthAccessToken>(`${this.url}${pin}`, {headers: this.headers});
}
}

View file

@ -20,6 +20,14 @@ export class RequestService extends ServiceHelpers {
return this.http.post<IRequestEngineResult>(`${this.url}Movie/`, JSON.stringify(movie), {headers: this.headers}); return this.http.post<IRequestEngineResult>(`${this.url}Movie/`, JSON.stringify(movie), {headers: this.headers});
} }
public getTotalMovies(): Observable<number> {
return this.http.get<number>(`${this.url}Movie/total`, {headers: this.headers});
}
public getTotalTv(): Observable<number> {
return this.http.get<number>(`${this.url}tv/total`, {headers: this.headers});
}
public requestTv(tv: ITvRequestViewModel): Observable<IRequestEngineResult> { public requestTv(tv: ITvRequestViewModel): Observable<IRequestEngineResult> {
return this.http.post<IRequestEngineResult>(`${this.url}TV/`, JSON.stringify(tv), {headers: this.headers}); return this.http.post<IRequestEngineResult>(`${this.url}TV/`, JSON.stringify(tv), {headers: this.headers});
} }

View file

@ -14,6 +14,13 @@
</div> </div>
</div> </div>
<div class="form-group">
<div class="checkbox">
<input type="checkbox" id="enableOAuth" name="enableOAuth" formControlName="enableOAuth">
<label for="enableOAuth" >Enable Plex OAuth</label>
</div>
</div>
<!-- <hr/> <!-- <hr/>
<div class="form-group"> <div class="form-group">
<div class="checkbox"> <div class="checkbox">

View file

@ -24,6 +24,7 @@ export class AuthenticationComponent implements OnInit {
requiredLowercase: [x.requiredLowercase], requiredLowercase: [x.requiredLowercase],
requireNonAlphanumeric: [x.requireNonAlphanumeric], requireNonAlphanumeric: [x.requireNonAlphanumeric],
requireUppercase: [x.requireUppercase], requireUppercase: [x.requireUppercase],
enableOAuth: [x.enableOAuth],
}); });
}); });
} }

View file

@ -59,7 +59,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="plexRecentlyAddedSync" class="control-label">Plex Recently Added Sync</label> <label for="plexRecentlyAddedSync" class="control-label">Plex Recently Added Sync</label>
<input type="text" class="form-control form-control-custom" [ngClass]="{'form-error': form.get('plexRecentlyAddedSync').hasError('required')}" id="plexRecentlyAddedSync" name="plexContentSync" formControlName="plexContentSync"> <input type="text" class="form-control form-control-custom" [ngClass]="{'form-error': form.get('plexRecentlyAddedSync').hasError('required')}" id="plexRecentlyAddedSync" name="plexRecentlyAddedSync" formControlName="plexRecentlyAddedSync">
<small *ngIf="form.get('plexRecentlyAddedSync').hasError('required')" class="error-text">The Plex Sync is required</small> <small *ngIf="form.get('plexRecentlyAddedSync').hasError('required')" class="error-text">The Plex Sync is required</small>
<button type="button" class="btn btn-sm btn-primary-outline" (click)="testCron(form.get('plexRecentlyAddedSync')?.value)">Test</button> <button type="button" class="btn btn-sm btn-primary-outline" (click)="testCron(form.get('plexRecentlyAddedSync')?.value)">Test</button>
</div> </div>

View file

@ -21,7 +21,7 @@ export class CreateAdminComponent {
this.identityService.createWizardUser({username: this.username, password: this.password, usePlexAdminAccount: false}).subscribe(x => { this.identityService.createWizardUser({username: this.username, password: this.password, usePlexAdminAccount: false}).subscribe(x => {
if (x) { if (x) {
// Log me in. // Log me in.
this.auth.login({ username: this.username, password: this.password, rememberMe:false }).subscribe(c => { this.auth.login({ username: this.username, password: this.password, rememberMe: false, usePlexOAuth:false }).subscribe(c => {
localStorage.setItem("id_token", c.access_token); localStorage.setItem("id_token", c.access_token);

View file

@ -17,9 +17,16 @@
<small>Please note we do not store this information, we only store your Plex Authorization Token that will allow Ombi to view your media and friends</small> <small>Please note we do not store this information, we only store your Plex Authorization Token that will allow Ombi to view your media and friends</small>
<div class="form-group"> <div class="form-group">
<div style="text-align: center; margin-top: 20px"> <div style="text-align: center; margin-top: 20px">
<button (click)="requestAuthToken()" class="btn btn-primary-outline">Request Token <i class="fa fa-key"></i></button> <button (click)="requestAuthToken()" class="btn btn-success-outline">Request Token <i class="fa fa-key"></i></button>
</div> </div>
</div> </div>
<p class="text-center">OR</p>
<div class="form-group">
<div style="text-align: center; margin-top: 20px">
<button (click)="oauth()" class="btn btn-primary" type="button">Continue With Plex</button>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,8 +1,6 @@
import { Component } from "@angular/core"; import { Component } from "@angular/core";
import { Router } from "@angular/router"; import { Router } from "@angular/router";
import { ConfirmationService } from "primeng/primeng";
import { PlexService } from "../../services"; import { PlexService } from "../../services";
import { IdentityService, NotificationService, SettingsService } from "../../services"; import { IdentityService, NotificationService, SettingsService } from "../../services";
import { AuthService } from "./../../auth/auth.service"; import { AuthService } from "./../../auth/auth.service";
@ -17,7 +15,6 @@ export class PlexComponent {
constructor(private plexService: PlexService, private router: Router, constructor(private plexService: PlexService, private router: Router,
private notificationService: NotificationService, private notificationService: NotificationService,
private confirmationService: ConfirmationService,
private identityService: IdentityService, private identityService: IdentityService,
private settings: SettingsService, private settings: SettingsService,
private auth: AuthService) { } private auth: AuthService) { }
@ -28,25 +25,21 @@ export class PlexComponent {
this.notificationService.error("Username or password was incorrect. Could not authenticate with Plex."); this.notificationService.error("Username or password was incorrect. Could not authenticate with Plex.");
return; return;
} }
this.confirmationService.confirm({
message: "Do you want your Plex user to be the main admin account on Ombi?", this.identityService.createWizardUser({
header: "Use Plex Account",
icon: "fa fa-check",
accept: () => {
this.identityService.createWizardUser({
username: "", username: "",
password: "", password: "",
usePlexAdminAccount: true, usePlexAdminAccount: true,
}).subscribe(x => { }).subscribe(y => {
if (x) { if (y) {
this.auth.login({ username: this.login, password: this.password, rememberMe:false }).subscribe(c => { this.auth.login({ username: this.login, password: this.password, rememberMe: false, usePlexOAuth: false }).subscribe(c => {
localStorage.setItem("id_token", c.access_token); localStorage.setItem("id_token", c.access_token);
// Mark that we have done the settings now // Mark that we have done the settings now
this.settings.getOmbi().subscribe(ombi => { this.settings.getOmbi().subscribe(ombi => {
ombi.wizard = true; ombi.wizard = true;
this.settings.saveOmbi(ombi).subscribe(x => { this.settings.saveOmbi(ombi).subscribe(s => {
this.settings.getUserManagementSettings().subscribe(usr => { this.settings.getUserManagementSettings().subscribe(usr => {
usr.importPlexAdmin = true; usr.importPlexAdmin = true;
@ -64,10 +57,14 @@ export class PlexComponent {
} }
}); });
}, },
reject: () => { );
this.router.navigate(["Wizard/CreateAdmin"]); }
},
}); public oauth() {
this.plexService.oAuth(true).subscribe(x => {
if(x.url) {
window.location.href = x.url;
}
}); });
} }
} }

View file

@ -0,0 +1,14 @@

<img class="landing-header" src="/images/logo.png" width="300" />
<div class="landing-block shadow">
<div class="media">
<div id="contentBody" class="media-body">
<h4 class="media-heading landing-title">Plex Authentication</h4>
<div class="form-group">
<label for="username" class="control-label">Please Wait</label>
</div>
</div>
</div>
</div>
<p-confirmDialog></p-confirmDialog>

View file

@ -0,0 +1,67 @@
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { IdentityService, PlexOAuthService, SettingsService } from "../../services";
import { AuthService } from "./../../auth/auth.service";
@Component({
templateUrl: "./plexoauth.component.html",
})
export class PlexOAuthComponent implements OnInit {
public pinId: number;
constructor(private route: ActivatedRoute,
private plexOauth: PlexOAuthService,
private identityService: IdentityService,
private settings: SettingsService,
private router: Router,
private auth: AuthService) {
this.route.params
.subscribe((params: any) => {
this.pinId = params.pin;
});
}
public ngOnInit(): void {
this.plexOauth.oAuth(this.pinId).subscribe(x => {
if(!x.accessToken) {
return;
// RETURN
}
this.identityService.createWizardUser({
username: "",
password: "",
usePlexAdminAccount: true,
}).subscribe(u => {
if (u) {
this.auth.oAuth(this.pinId).subscribe(c => {
localStorage.setItem("id_token", c.access_token);
// Mark that we have done the settings now
this.settings.getOmbi().subscribe(ombi => {
ombi.wizard = true;
this.settings.saveOmbi(ombi).subscribe(s => {
this.settings.getUserManagementSettings().subscribe(usr => {
usr.importPlexAdmin = true;
this.settings.saveUserManagementSettings(usr).subscribe(saved => {
this.router.navigate(["login"]);
});
});
});
});
});
} else {
//this.notificationService.error("Could not get the Plex Admin Information");
return;
}
});
});
}
}

View file

@ -14,6 +14,8 @@ import { WelcomeComponent } from "./welcome/welcome.component";
import { EmbyService } from "../services"; import { EmbyService } from "../services";
import { PlexService } from "../services"; import { PlexService } from "../services";
import { IdentityService } from "../services"; import { IdentityService } from "../services";
import { PlexOAuthService } from "../services";
import { PlexOAuthComponent } from "./plex/plexoauth.component";
const routes: Routes = [ const routes: Routes = [
{ path: "", component: WelcomeComponent}, { path: "", component: WelcomeComponent},
@ -21,6 +23,7 @@ const routes: Routes = [
{ path: "Plex", component: PlexComponent}, { path: "Plex", component: PlexComponent},
{ path: "Emby", component: EmbyComponent}, { path: "Emby", component: EmbyComponent},
{ path: "CreateAdmin", component: CreateAdminComponent}, { path: "CreateAdmin", component: CreateAdminComponent},
{ path: "OAuth/:pin", component: PlexOAuthComponent},
]; ];
@NgModule({ @NgModule({
imports: [ imports: [
@ -33,6 +36,7 @@ const routes: Routes = [
WelcomeComponent, WelcomeComponent,
MediaServerComponent, MediaServerComponent,
PlexComponent, PlexComponent,
PlexOAuthComponent,
CreateAdminComponent, CreateAdminComponent,
EmbyComponent, EmbyComponent,
], ],
@ -44,6 +48,7 @@ const routes: Routes = [
IdentityService, IdentityService,
EmbyService, EmbyService,
ConfirmationService, ConfirmationService,
PlexOAuthService,
], ],
}) })

View file

@ -351,5 +351,5 @@ button.list-group-item:focus {
position: absolute; position: absolute;
} }
table.table > thead > tr > th.active { table.table > thead > tr > th.active {
background-color: transparent; background-color: $primary-colour;
} }

View file

@ -541,6 +541,10 @@ $border-radius: 10px;
cursor: pointer; cursor: pointer;
} }
.table-usermanagement {
margin-top: 20px;
}
.input-group-sm { .input-group-sm {
padding-top: 2px; padding-top: 2px;
padding-bottom: 2px; padding-bottom: 2px;
@ -955,3 +959,7 @@ a > h4:hover {
width: 94%; width: 94%;
} }
.ui-state-active {
background-color: $primary-colour-outline $i;
color: black $i;
}

View file

@ -8,6 +8,7 @@ using Microsoft.Extensions.Logging;
using Ombi.Api.Plex; using Ombi.Api.Plex;
using Ombi.Api.Plex.Models; using Ombi.Api.Plex.Models;
using Ombi.Attributes; using Ombi.Attributes;
using Ombi.Core.Authentication;
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;
@ -21,16 +22,18 @@ namespace Ombi.Controllers.External
public class PlexController : Controller public class PlexController : Controller
{ {
public PlexController(IPlexApi plexApi, ISettingsService<PlexSettings> plexSettings, public PlexController(IPlexApi plexApi, ISettingsService<PlexSettings> plexSettings,
ILogger<PlexController> logger) ILogger<PlexController> logger, IPlexOAuthManager manager)
{ {
PlexApi = plexApi; PlexApi = plexApi;
PlexSettings = plexSettings; PlexSettings = plexSettings;
_log = logger; _log = logger;
_plexOAuthManager = manager;
} }
private IPlexApi PlexApi { get; } private IPlexApi PlexApi { get; }
private ISettingsService<PlexSettings> PlexSettings { get; } private ISettingsService<PlexSettings> PlexSettings { get; }
private readonly ILogger<PlexController> _log; private readonly ILogger<PlexController> _log;
private readonly IPlexOAuthManager _plexOAuthManager;
/// <summary> /// <summary>
/// Signs into the Plex API. /// Signs into the Plex API.
@ -173,5 +176,37 @@ namespace Ombi.Controllers.External
// Filter out any dupes // Filter out any dupes
return vm.DistinctBy(x => x.Id); return vm.DistinctBy(x => x.Id);
} }
[HttpGet("oauth/{wizard:bool}")]
[AllowAnonymous]
public async Task<IActionResult> OAuth(bool wizard)
{
//https://app.plex.tv/auth#?forwardUrl=http://google.com/&clientID=Ombi-Test&context%5Bdevice%5D%5Bproduct%5D=Ombi%20SSO&pinID=798798&code=4lgfd
// Plex OAuth
// Redirect them to Plex
// We need a PIN first
var pin = await _plexOAuthManager.RequestPin();
Uri url;
if (!wizard)
{
url = await _plexOAuthManager.GetOAuthUrl(pin.id, pin.code);
}
else
{
var websiteAddress =$"{this.Request.Scheme}://{this.Request.Host}{this.Request.PathBase}";
url = _plexOAuthManager.GetWizardOAuthUrl(pin.id, pin.code, websiteAddress);
}
if (url == null)
{
return new JsonResult(new
{
error = "Application URL has not been set"
});
}
return new JsonResult(new {url = url.ToString()});
}
} }
} }

View file

@ -213,7 +213,7 @@ namespace Ombi.Controllers
[PowerUser] [PowerUser]
public async Task<IEnumerable<UserViewModel>> GetAllUsers() public async Task<IEnumerable<UserViewModel>> GetAllUsers()
{ {
var users = await UserManager.Users var users = await UserManager.Users.Where(x => x.UserType != UserType.SystemUser)
.ToListAsync(); .ToListAsync();
var model = new List<UserViewModel>(); var model = new List<UserViewModel>();

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