pull/2420/head
TidusJar 6 years ago
commit c4f22031d3

@ -1,6 +1,6 @@
# Changelog # Changelog
## (unreleased) ## v3.0.3477 (2018-07-18)
### **New Features** ### **New Features**

@ -1,10 +1,10 @@
#tool "nuget:?package=GitVersion.CommandLine" #tool "nuget:?package=GitVersion.CommandLine"
#addin "Cake.Gulp" #addin "Cake.Gulp"
#addin "nuget:?package=Cake.Npm&version=0.13.0"
#addin "SharpZipLib" #addin "SharpZipLib"
#addin nuget:?package=Cake.Compression&version=0.1.4 #addin nuget:?package=Cake.Compression&version=0.1.4
#addin "Cake.Incubator" #addin "Cake.Incubator"
#addin "Cake.Yarn"
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
// ARGUMENTS // ARGUMENTS
@ -122,36 +122,19 @@ Task("SetVersionInfo")
Task("NPM") Task("NPM")
.Does(() => { .Does(() => {
var settings = new NpmInstallSettings { Yarn.FromPath(webProjDir).Install();
LogLevel = NpmLogLevel.Silent,
WorkingDirectory = webProjDir,
Production = true
};
NpmInstall(settings);
}); });
Task("Gulp Publish") Task("Gulp Publish")
.IsDependentOn("NPM") .IsDependentOn("NPM")
.Does(() => { .Does(() => {
Yarn.FromPath(webProjDir).RunScript("publish");
var runScriptSettings = new NpmRunScriptSettings {
ScriptName="publish",
WorkingDirectory = webProjDir,
};
NpmRunScript(runScriptSettings);
}); });
Task("TSLint") Task("TSLint")
.Does(() => .Does(() =>
{ {
var settings = new NpmRunScriptSettings { Yarn.FromPath(webProjDir).RunScript("lint");
WorkingDirectory = webProjDir,
ScriptName = "lint"
};
NpmRunScript(settings);
}); });
Task("PrePublish") Task("PrePublish")

@ -4,6 +4,10 @@
<TargetFramework>netstandard2.0</TargetFramework> <TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<PackageReference Include="Humanizer.Core" Version="2.4.2" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Ombi.Api\Ombi.Api.csproj" /> <ProjectReference Include="..\Ombi.Api\Ombi.Api.csproj" />
</ItemGroup> </ItemGroup>

@ -6,6 +6,30 @@ namespace Ombi.Api.Sonarr.Models
{ {
public class Episode public class Episode
{ {
public Episode()
{
}
public Episode(Episode ep)
{
seriesId = ep.seriesId;
episodeFileId = ep.episodeFileId;
seasonNumber = ep.seasonNumber;
episodeNumber = ep.episodeNumber;
title = ep.title;
airDate = ep.airDate;
airDateUtc = ep.airDateUtc;
overview = ep.overview;
hasFile = ep.hasFile;
monitored = ep.monitored;
unverifiedSceneNumbering = ep.unverifiedSceneNumbering;
id = ep.id;
absoluteEpisodeNumber = ep.absoluteEpisodeNumber;
sceneAbsoluteEpisodeNumber = ep.sceneAbsoluteEpisodeNumber;
sceneEpisodeNumber = ep.sceneEpisodeNumber;
sceneSeasonNumber = ep.sceneSeasonNumber;
}
public int seriesId { get; set; } public int seriesId { get; set; }
public int episodeFileId { get; set; } public int episodeFileId { get; set; }
public int seasonNumber { get; set; } public int seasonNumber { get; set; }
@ -27,6 +51,24 @@ namespace Ombi.Api.Sonarr.Models
public class Episodefile public class Episodefile
{ {
public Episodefile()
{
}
public Episodefile(Episodefile e)
{
seriesId = e.seriesId;
seasonNumber = e.seasonNumber;
relativePath = e.relativePath;
path = e.path;
size = e.size;
dateAdded = e.dateAdded;
sceneName = e.sceneName;
quality = new EpisodeQuality(e.quality);
qualityCutoffNotMet = e.qualityCutoffNotMet;
id = e.id;
}
public int seriesId { get; set; } public int seriesId { get; set; }
public int seasonNumber { get; set; } public int seasonNumber { get; set; }
public string relativePath { get; set; } public string relativePath { get; set; }
@ -41,12 +83,32 @@ namespace Ombi.Api.Sonarr.Models
public class EpisodeQuality public class EpisodeQuality
{ {
public EpisodeQuality()
{
}
public EpisodeQuality(EpisodeQuality e)
{
quality = new Quality(e.quality);
revision = new Revision(e.revision);
}
public Quality quality { get; set; } public Quality quality { get; set; }
public Revision revision { get; set; } public Revision revision { get; set; }
} }
public class Revision public class Revision
{ {
public Revision()
{
}
public Revision(Revision r)
{
version = r.version;
real = r.real;
}
public int version { get; set; } public int version { get; set; }
public int real { get; set; } public int real { get; set; }
} }

@ -2,6 +2,16 @@ namespace Ombi.Api.Sonarr.Models
{ {
public class Quality public class Quality
{ {
public Quality()
{
}
public Quality(Quality q)
{
id = q.id;
name = q.name;
}
public int id { get; set; } public int id { get; set; }
public string name { get; set; } public string name { get; set; }
} }

@ -29,7 +29,10 @@ namespace Ombi.Core.Tests.Rule.Search
{ {
ProviderId = "123" ProviderId = "123"
}); });
var search = new SearchMovieViewModel(); var search = new SearchMovieViewModel()
{
TheMovieDbId = "123",
};
var result = await Rule.Execute(search); var result = await Rule.Execute(search);
Assert.True(result.Success); Assert.True(result.Success);

@ -15,13 +15,13 @@ namespace Ombi.Core.Engine.Interfaces
Task<RequestEngineResult> DenyChildRequest(int requestId); Task<RequestEngineResult> DenyChildRequest(int requestId);
Task<RequestsViewModel<TvRequests>> GetRequestsLite(int count, int position, OrderFilterModel type); Task<RequestsViewModel<TvRequests>> GetRequestsLite(int count, int position, OrderFilterModel type);
Task<IEnumerable<TvRequests>> SearchTvRequest(string search); Task<IEnumerable<TvRequests>> SearchTvRequest(string search);
Task<IEnumerable<TreeNode<TvRequests, List<ChildRequests>>>> SearchTvRequestTree(string search);
Task<TvRequests> UpdateTvRequest(TvRequests request); Task<TvRequests> UpdateTvRequest(TvRequests request);
Task<IEnumerable<TreeNode<TvRequests, List<ChildRequests>>>> GetRequestsTreeNode(int count, int position);
Task<IEnumerable<ChildRequests>> GetAllChldren(int tvId); Task<IEnumerable<ChildRequests>> GetAllChldren(int tvId);
Task<ChildRequests> UpdateChildRequest(ChildRequests request); Task<ChildRequests> UpdateChildRequest(ChildRequests request);
Task RemoveTvChild(int requestId); Task RemoveTvChild(int requestId);
Task<RequestEngineResult> ApproveChildRequest(int id); Task<RequestEngineResult> ApproveChildRequest(int id);
Task<IEnumerable<TvRequests>> GetRequestsLite(); Task<IEnumerable<TvRequests>> GetRequestsLite();
Task UpdateQualityProfile(int requestId, int profileId);
Task UpdateRootPath(int requestId, int rootPath);
} }
} }

@ -7,16 +7,10 @@ namespace Ombi.Core.Engine.Interfaces
public interface ITvSearchEngine public interface ITvSearchEngine
{ {
Task<IEnumerable<SearchTvShowViewModel>> Search(string searchTerm); Task<IEnumerable<SearchTvShowViewModel>> Search(string searchTerm);
Task<IEnumerable<TreeNode<SearchTvShowViewModel>>> SearchTreeNode(string searchTerm);
Task<TreeNode<SearchTvShowViewModel>> GetShowInformationTreeNode(int tvdbid);
Task<SearchTvShowViewModel> GetShowInformation(int tvdbid); Task<SearchTvShowViewModel> GetShowInformation(int tvdbid);
Task<IEnumerable<TreeNode<SearchTvShowViewModel>>> PopularTree();
Task<IEnumerable<SearchTvShowViewModel>> Popular(); Task<IEnumerable<SearchTvShowViewModel>> Popular();
Task<IEnumerable<TreeNode<SearchTvShowViewModel>>> AnticipatedTree();
Task<IEnumerable<SearchTvShowViewModel>> Anticipated(); Task<IEnumerable<SearchTvShowViewModel>> Anticipated();
Task<IEnumerable<TreeNode<SearchTvShowViewModel>>> MostWatchesTree();
Task<IEnumerable<SearchTvShowViewModel>> MostWatches(); Task<IEnumerable<SearchTvShowViewModel>> MostWatches();
Task<IEnumerable<TreeNode<SearchTvShowViewModel>>> TrendingTree();
Task<IEnumerable<SearchTvShowViewModel>> Trending(); Task<IEnumerable<SearchTvShowViewModel>> Trending();
} }
} }

@ -452,6 +452,7 @@ namespace Ombi.Core.Engine
} }
request.Available = true; request.Available = true;
request.MarkedAsAvailable = DateTime.Now;
NotificationHelper.Notify(request, NotificationType.RequestAvailable); NotificationHelper.Notify(request, NotificationType.RequestAvailable);
await MovieRepository.Update(request); await MovieRepository.Update(request);

@ -1,23 +0,0 @@
using System.Collections.Generic;
namespace Ombi.Core.Engine
{
public class TreeNode<T>
{
public string Label { get; set; }
public T Data { get; set; }
public List<TreeNode<T>> Children { get; set; }
public bool Leaf { get; set; }
public bool Expanded { get; set; }
}
public class TreeNode<T,U>
{
public string Label { get; set; }
public T Data { get; set; }
public List<TreeNode<U>> Children { get; set; }
public bool Leaf { get; set; }
public bool Expanded { get; set; }
}
}

@ -171,24 +171,30 @@ namespace Ombi.Core.Engine
public async Task<RequestsViewModel<TvRequests>> GetRequestsLite(int count, int position, OrderFilterModel type) public async Task<RequestsViewModel<TvRequests>> GetRequestsLite(int count, int position, OrderFilterModel type)
{ {
var shouldHide = await HideFromOtherUsers(); var shouldHide = await HideFromOtherUsers();
List<TvRequests> allRequests; List<TvRequests> allRequests = null;
if (shouldHide.Hide) if (shouldHide.Hide)
{ {
allRequests = await TvRepository.GetLite(shouldHide.UserId) var tv = TvRepository.GetLite(shouldHide.UserId);
.OrderByDescending(x => x.ChildRequests.Max(y => y.RequestedDate)) if (tv.Any() && tv.Select(x => x.ChildRequests).Any())
.Skip(position).Take(count).ToListAsync(); {
allRequests = await tv.OrderByDescending(x => x.ChildRequests.Max(y => y.RequestedDate)).Skip(position).Take(count).ToListAsync();
}
// Filter out children // Filter out children
FilterChildren(allRequests, shouldHide); FilterChildren(allRequests, shouldHide);
} }
else else
{ {
allRequests = await TvRepository.GetLite() var tv = TvRepository.GetLite();
.OrderByDescending(x => x.ChildRequests.Max(y => y.RequestedDate)) if (tv.Any() && tv.Select(x => x.ChildRequests).Any())
.Skip(position).Take(count).ToListAsync(); {
allRequests = await tv.OrderByDescending(x => x.ChildRequests.Max(y => y.RequestedDate)).Skip(position).Take(count).ToListAsync();
}
}
if (allRequests == null)
{
return new RequestsViewModel<TvRequests>();
} }
allRequests.ForEach(async r => { await CheckForSubscription(shouldHide, r); }); allRequests.ForEach(async r => { await CheckForSubscription(shouldHide, r); });
return new RequestsViewModel<TvRequests> return new RequestsViewModel<TvRequests>
@ -196,38 +202,6 @@ namespace Ombi.Core.Engine
Collection = allRequests Collection = allRequests
}; };
} }
public async Task<IEnumerable<TreeNode<TvRequests, List<ChildRequests>>>> GetRequestsTreeNode(int count, int position)
{
var shouldHide = await HideFromOtherUsers();
List<TvRequests> allRequests;
if (shouldHide.Hide)
{
allRequests = await TvRepository.Get(shouldHide.UserId)
.Include(x => x.ChildRequests)
.ThenInclude(x => x.SeasonRequests)
.ThenInclude(x => x.Episodes)
.Where(x => x.ChildRequests.Any())
.OrderByDescending(x => x.ChildRequests.Max(y => y.RequestedDate))
.Skip(position).Take(count).ToListAsync();
FilterChildren(allRequests, shouldHide);
}
else
{
allRequests = await TvRepository.Get()
.Include(x => x.ChildRequests)
.ThenInclude(x => x.SeasonRequests)
.ThenInclude(x => x.Episodes)
.Where(x => x.ChildRequests.Any())
.OrderByDescending(x => x.ChildRequests.Max(y => y.RequestedDate))
.Skip(position).Take(count).ToListAsync();
}
allRequests.ForEach(async r => { await CheckForSubscription(shouldHide, r); });
return ParseIntoTreeNode(allRequests);
}
public async Task<IEnumerable<TvRequests>> GetRequests() public async Task<IEnumerable<TvRequests>> GetRequests()
{ {
var shouldHide = await HideFromOtherUsers(); var shouldHide = await HideFromOtherUsers();
@ -288,6 +262,10 @@ namespace Ombi.Core.Engine
private static void FilterChildren(IEnumerable<TvRequests> allRequests, HideResult shouldHide) private static void FilterChildren(IEnumerable<TvRequests> allRequests, HideResult shouldHide)
{ {
if (allRequests == null)
{
return;
}
// Filter out children // Filter out children
foreach (var t in allRequests) foreach (var t in allRequests)
{ {
@ -350,21 +328,22 @@ namespace Ombi.Core.Engine
return results; return results;
} }
public async Task<IEnumerable<TreeNode<TvRequests, List<ChildRequests>>>> SearchTvRequestTree(string search) public async Task UpdateRootPath(int requestId, int rootPath)
{ {
var shouldHide = await HideFromOtherUsers(); var allRequests = TvRepository.Get();
IQueryable<TvRequests> allRequests; var results = await allRequests.FirstOrDefaultAsync(x => x.Id == requestId);
if (shouldHide.Hide) results.RootFolder = rootPath;
{
allRequests = TvRepository.Get(shouldHide.UserId); await TvRepository.Update(results);
} }
else
{ public async Task UpdateQualityProfile(int requestId, int profileId)
allRequests = TvRepository.Get(); {
} var allRequests = TvRepository.Get();
var results = await allRequests.Where(x => x.Title.Contains(search, CompareOptions.IgnoreCase)).ToListAsync(); var results = await allRequests.FirstOrDefaultAsync(x => x.Id == requestId);
results.ForEach(async r => { await CheckForSubscription(shouldHide, r); }); results.QualityOverride = profileId;
return ParseIntoTreeNode(results);
await TvRepository.Update(results);
} }
public async Task<TvRequests> UpdateTvRequest(TvRequests request) public async Task<TvRequests> UpdateTvRequest(TvRequests request)
@ -516,6 +495,7 @@ namespace Ombi.Core.Engine
}; };
} }
request.Available = true; request.Available = true;
request.MarkedAsAvailable = DateTime.Now;
foreach (var season in request.SeasonRequests) foreach (var season in request.SeasonRequests)
{ {
foreach (var e in season.Episodes) foreach (var e in season.Episodes)
@ -585,29 +565,7 @@ namespace Ombi.Core.Engine
return await AfterRequest(model.ChildRequests.FirstOrDefault()); return await AfterRequest(model.ChildRequests.FirstOrDefault());
} }
private static List<TreeNode<TvRequests, List<ChildRequests>>> ParseIntoTreeNode(IEnumerable<TvRequests> result) private static List<ChildRequests> SortEpisodes(List<ChildRequests> items)
{
var node = new List<TreeNode<TvRequests, List<ChildRequests>>>();
foreach (var value in result)
{
node.Add(new TreeNode<TvRequests, List<ChildRequests>>
{
Data = value,
Children = new List<TreeNode<List<ChildRequests>>>
{
new TreeNode<List<ChildRequests>>
{
Data = SortEpisodes(value.ChildRequests),
Leaf = true
}
}
});
}
return node;
}
private static List<ChildRequests> SortEpisodes(List<ChildRequests> items)
{ {
foreach (var value in items) foreach (var value in items)
{ {

@ -59,11 +59,6 @@ namespace Ombi.Core.Engine
return null; return null;
} }
public async Task<IEnumerable<TreeNode<SearchTvShowViewModel>>> SearchTreeNode(string searchTerm)
{
var result = await Search(searchTerm);
return result.Select(ParseIntoTreeNode).ToList();
}
public async Task<SearchTvShowViewModel> GetShowInformation(int tvdbid) public async Task<SearchTvShowViewModel> GetShowInformation(int tvdbid)
{ {
var show = await TvMazeApi.ShowLookupByTheTvDbId(tvdbid); var show = await TvMazeApi.ShowLookupByTheTvDbId(tvdbid);
@ -116,19 +111,6 @@ namespace Ombi.Core.Engine
return await ProcessResult(mapped); return await ProcessResult(mapped);
} }
public async Task<TreeNode<SearchTvShowViewModel>> GetShowInformationTreeNode(int tvdbid)
{
var result = await GetShowInformation(tvdbid);
return ParseIntoTreeNode(result);
}
public async Task<IEnumerable<TreeNode<SearchTvShowViewModel>>> PopularTree()
{
var result = await Cache.GetOrAdd(CacheKeys.PopularTv, async () => await TraktApi.GetPopularShows(), DateTime.Now.AddHours(12));
var processed = await ProcessResults(result);
return processed.Select(ParseIntoTreeNode).ToList();
}
public async Task<IEnumerable<SearchTvShowViewModel>> Popular() public async Task<IEnumerable<SearchTvShowViewModel>> Popular()
{ {
var result = await Cache.GetOrAdd(CacheKeys.PopularTv, async () => await TraktApi.GetPopularShows(), DateTime.Now.AddHours(12)); var result = await Cache.GetOrAdd(CacheKeys.PopularTv, async () => await TraktApi.GetPopularShows(), DateTime.Now.AddHours(12));
@ -136,12 +118,6 @@ namespace Ombi.Core.Engine
return processed; return processed;
} }
public async Task<IEnumerable<TreeNode<SearchTvShowViewModel>>> AnticipatedTree()
{
var result = await Cache.GetOrAdd(CacheKeys.AnticipatedTv, async () => await TraktApi.GetAnticipatedShows(), DateTime.Now.AddHours(12));
var processed = await ProcessResults(result);
return processed.Select(ParseIntoTreeNode).ToList();
}
public async Task<IEnumerable<SearchTvShowViewModel>> Anticipated() public async Task<IEnumerable<SearchTvShowViewModel>> Anticipated()
{ {
@ -150,12 +126,6 @@ namespace Ombi.Core.Engine
return processed; return processed;
} }
public async Task<IEnumerable<TreeNode<SearchTvShowViewModel>>> MostWatchesTree()
{
var result = await Cache.GetOrAdd(CacheKeys.MostWatchesTv, async () => await TraktApi.GetMostWatchesShows(), DateTime.Now.AddHours(12));
var processed = await ProcessResults(result);
return processed.Select(ParseIntoTreeNode).ToList();
}
public async Task<IEnumerable<SearchTvShowViewModel>> MostWatches() public async Task<IEnumerable<SearchTvShowViewModel>> MostWatches()
{ {
var result = await Cache.GetOrAdd(CacheKeys.MostWatchesTv, async () => await TraktApi.GetMostWatchesShows(), DateTime.Now.AddHours(12)); var result = await Cache.GetOrAdd(CacheKeys.MostWatchesTv, async () => await TraktApi.GetMostWatchesShows(), DateTime.Now.AddHours(12));
@ -163,13 +133,6 @@ namespace Ombi.Core.Engine
return processed; return processed;
} }
public async Task<IEnumerable<TreeNode<SearchTvShowViewModel>>> TrendingTree()
{
var result = await Cache.GetOrAdd(CacheKeys.TrendingTv, async () => await TraktApi.GetTrendingShows(), DateTime.Now.AddHours(12));
var processed = await ProcessResults(result);
return processed.Select(ParseIntoTreeNode).ToList();
}
public async Task<IEnumerable<SearchTvShowViewModel>> Trending() public async Task<IEnumerable<SearchTvShowViewModel>> Trending()
{ {
var result = await Cache.GetOrAdd(CacheKeys.TrendingTv, async () => await TraktApi.GetTrendingShows(), DateTime.Now.AddHours(12)); var result = await Cache.GetOrAdd(CacheKeys.TrendingTv, async () => await TraktApi.GetTrendingShows(), DateTime.Now.AddHours(12));
@ -177,22 +140,6 @@ namespace Ombi.Core.Engine
return processed; return processed;
} }
private static TreeNode<SearchTvShowViewModel> ParseIntoTreeNode(SearchTvShowViewModel result)
{
return new TreeNode<SearchTvShowViewModel>
{
Data = result,
Children = new List<TreeNode<SearchTvShowViewModel>>
{
new TreeNode<SearchTvShowViewModel>
{
Data = result, Leaf = true
}
},
Leaf = false
};
}
private async Task<IEnumerable<SearchTvShowViewModel>> ProcessResults<T>(IEnumerable<T> items) private async Task<IEnumerable<SearchTvShowViewModel>> ProcessResults<T>(IEnumerable<T> items)
{ {
var retVal = new List<SearchTvShowViewModel>(); var retVal = new List<SearchTvShowViewModel>();

@ -0,0 +1,98 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Ombi.Core.Authentication;
using Ombi.Store.Entities;
using Ombi.Store.Repository.Requests;
namespace Ombi.Core.Engine
{
public class UserStatsEngine
{
public UserStatsEngine(OmbiUserManager um, IMovieRequestRepository movieRequest, ITvRequestRepository tvRequest)
{
_userManager = um;
_movieRequest = movieRequest;
_tvRequest = tvRequest;
}
private readonly OmbiUserManager _userManager;
private readonly IMovieRequestRepository _movieRequest;
private readonly ITvRequestRepository _tvRequest;
public async Task<UserStatsSummary> GetSummary(SummaryRequest request)
{
/* What do we want?
This is Per week/month/all time (filter by date)
1. Total Requests
2. Total Movie Requests
3. Total Tv Requests
4. Total Issues (If enabled)
5. Total Requests fufilled (now available)
Then
2. Most requested user Movie
3. Most requested user tv
Then
1.
*/
// get all movie requests
var movies = _movieRequest.GetWithUser();
var filteredMovies = movies.Where(x => x.RequestedDate >= request.From && x.RequestedDate <= request.To);
var tv = _tvRequest.GetLite();
var children = tv.SelectMany(x =>
x.ChildRequests.Where(c => c.RequestedDate >= request.From && c.RequestedDate <= request.To));
var moviesCount = filteredMovies.CountAsync();
var childrenCount = children.CountAsync();
var availableMovies =
movies.Select(x => x.MarkedAsAvailable >= request.From && x.MarkedAsAvailable <= request.To).CountAsync();
var availableChildren = tv.SelectMany(x =>
x.ChildRequests.Where(c => c.MarkedAsAvailable >= request.From && c.MarkedAsAvailable <= request.To)).CountAsync();
var userMovie = filteredMovies.GroupBy(x => x.RequestedUserId).OrderBy(x => x.Key).FirstOrDefaultAsync();
var userTv = children.GroupBy(x => x.RequestedUserId).OrderBy(x => x.Key).FirstOrDefaultAsync();
return new UserStatsSummary
{
TotalMovieRequests = await moviesCount,
TotalTvRequests = await childrenCount,
CompletedRequestsTv = await availableChildren,
CompletedRequestsMovies = await availableMovies,
MostRequestedUserMovie = (await userMovie).FirstOrDefault().RequestedUser,
MostRequestedUserTv = (await userTv).FirstOrDefault().RequestedUser,
};
}
}
public class SummaryRequest
{
public DateTime From { get; set; }
public DateTime To { get; set; }
}
public class UserStatsSummary
{
public int TotalRequests => TotalTvRequests + TotalTvRequests;
public int TotalMovieRequests { get; set; }
public int TotalTvRequests { get; set; }
public int TotalIssues { get; set; }
public int CompletedRequestsMovies { get; set; }
public int CompletedRequestsTv { get; set; }
public int CompletedRequests => CompletedRequestsMovies + CompletedRequestsTv;
public OmbiUser MostRequestedUserMovie { get; set; }
public OmbiUser MostRequestedUserTv { get; set; }
}
}

@ -165,25 +165,15 @@ namespace Ombi.Core.Senders
titleSlug = model.ParentRequest.Title, titleSlug = model.ParentRequest.Title,
addOptions = new AddOptions addOptions = new AddOptions
{ {
ignoreEpisodesWithFiles = true, // There shouldn't be any episodes with files, this is a new season ignoreEpisodesWithFiles = false, // There shouldn't be any episodes with files, this is a new season
ignoreEpisodesWithoutFiles = true, // We want all missing ignoreEpisodesWithoutFiles = false, // We want all missing
searchForMissingEpisodes = false // we want dont want to search yet. We want to make sure everything is unmonitored/monitored correctly. searchForMissingEpisodes = false // we want dont want to search yet. We want to make sure everything is unmonitored/monitored correctly.
} }
}; };
// Montitor the correct seasons, // Montitor the correct seasons,
// If we have that season in the model then it's monitored! // If we have that season in the model then it's monitored!
var seasonsToAdd = new List<Season>(); var seasonsToAdd = GetSeasonsToCreate(model);
for (var i = 0; i < model.ParentRequest.TotalSeasons + 1; i++)
{
var index = i;
var season = new Season
{
seasonNumber = i,
monitored = model.SeasonRequests.Any(x => x.SeasonNumber == index && x.SeasonNumber != 0)
};
seasonsToAdd.Add(season);
}
newSeries.seasons = seasonsToAdd; newSeries.seasons = seasonsToAdd;
var result = await SonarrApi.AddSeries(newSeries, s.ApiKey, s.FullUri); var result = await SonarrApi.AddSeries(newSeries, s.ApiKey, s.FullUri);
existingSeries = await SonarrApi.GetSeriesById(result.id, s.ApiKey, s.FullUri); existingSeries = await SonarrApi.GetSeriesById(result.id, s.ApiKey, s.FullUri);
@ -237,7 +227,7 @@ namespace Ombi.Core.Senders
{ {
var sonarrEp = sonarrEpList.FirstOrDefault(x => var sonarrEp = sonarrEpList.FirstOrDefault(x =>
x.episodeNumber == ep.EpisodeNumber && x.seasonNumber == req.SeasonNumber); x.episodeNumber == ep.EpisodeNumber && x.seasonNumber == req.SeasonNumber);
if (sonarrEp != null) if (sonarrEp != null && !sonarrEp.monitored)
{ {
sonarrEp.monitored = true; sonarrEp.monitored = true;
episodesToUpdate.Add(sonarrEp); episodesToUpdate.Add(sonarrEp);
@ -245,27 +235,64 @@ namespace Ombi.Core.Senders
} }
} }
var seriesChanges = false; var seriesChanges = false;
foreach (var season in model.SeasonRequests) foreach (var season in model.SeasonRequests)
{ {
var sonarrSeason = sonarrEpList.Where(x => x.seasonNumber == season.SeasonNumber); var sonarrSeason = sonarrEpList.Where(x => x.seasonNumber == season.SeasonNumber);
var sonarrEpCount = sonarrSeason.Count(); var sonarrEpCount = sonarrSeason.Count();
var ourRequestCount = season.Episodes.Count; var ourRequestCount = season.Episodes.Count;
var existingSeason =
result.seasons.FirstOrDefault(x => x.seasonNumber == season.SeasonNumber);
if (existingSeason == null)
{
Logger.LogError("There was no season numer {0} in Sonarr for title {1}", season.SeasonNumber, model.ParentRequest.Title);
continue;
}
if (sonarrEpCount == ourRequestCount) if (sonarrEpCount == ourRequestCount)
{ {
// We have the same amount of requests as all of the episodes in the season. // We have the same amount of requests as all of the episodes in the season.
var existingSeason =
result.seasons.FirstOrDefault(x => x.seasonNumber == season.SeasonNumber); if (!existingSeason.monitored)
if (existingSeason == null)
{ {
Logger.LogError("The sonarr ep count was the same as out request count, but could not match the season number {0}", season.SeasonNumber); existingSeason.monitored = true;
continue; seriesChanges = true;
} }
existingSeason.monitored = true;
seriesChanges = true;
} }
else else
{ {
// Make sure this season is set to monitored
if (!existingSeason.monitored)
{
// We need to monitor it, problem being is all episodes will now be monitored
// So we need to monior the series but unmonitor every episode
// Except the episodes that are already monitored before we update the series (we do not want to unmonitor episodes that are monitored beforehand)
existingSeason.monitored = true;
var sea = result.seasons.FirstOrDefault(x => x.seasonNumber == existingSeason.seasonNumber);
sea.monitored = true;
//var previouslyMonitoredEpisodes = sonarrEpList.Where(x =>
// x.seasonNumber == existingSeason.seasonNumber && x.monitored).Select(x => x.episodeNumber).ToList(); // We probably don't actually care about this
result = await SonarrApi.UpdateSeries(result, s.ApiKey, s.FullUri);
var epToUnmonitor = new List<Episode>();
var newEpList = sonarrEpList.ConvertAll(ep => new Episode(ep)); // Clone it so we don't modify the orignal member
foreach (var ep in newEpList.Where(x => x.seasonNumber == existingSeason.seasonNumber).ToList())
{
//if (previouslyMonitoredEpisodes.Contains(ep.episodeNumber))
//{
// // This was previously monitored.
// continue;
//}
ep.monitored = false;
epToUnmonitor.Add(ep);
}
foreach (var epToUpdate in epToUnmonitor)
{
await SonarrApi.UpdateEpisode(epToUpdate, s.ApiKey, s.FullUri);
}
}
// Now update the episodes that need updating // Now update the episodes that need updating
foreach (var epToUpdate in episodesToUpdate.Where(x => x.seasonNumber == season.SeasonNumber)) foreach (var epToUpdate in episodesToUpdate.Where(x => x.seasonNumber == season.SeasonNumber))
{ {
@ -285,6 +312,24 @@ namespace Ombi.Core.Senders
} }
} }
private static List<Season> GetSeasonsToCreate(ChildRequests model)
{
// Let's get a list of seasons just incase we need to change it
var seasonsToUpdate = new List<Season>();
for (var i = 0; i < model.ParentRequest.TotalSeasons + 1; i++)
{
var index = i;
var sea = new Season
{
seasonNumber = i,
monitored = model.SeasonRequests.Any(x => x.SeasonNumber == index && x.SeasonNumber != 0)
};
seasonsToUpdate.Add(sea);
}
return seasonsToUpdate;
}
private async Task<bool> SendToSickRage(ChildRequests model, SickRageSettings settings, string qualityId = null) private async Task<bool> SendToSickRage(ChildRequests model, SickRageSettings settings, string qualityId = null)
{ {
var tvdbid = model.ParentRequest.TvDbId; var tvdbid = model.ParentRequest.TvDbId;

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using Humanizer;
using Ombi.Helpers; using Ombi.Helpers;
using Ombi.Notifications.Models; using Ombi.Notifications.Models;
using Ombi.Settings.Settings.Models; using Ombi.Settings.Settings.Models;
@ -39,7 +40,7 @@ namespace Ombi.Notifications
RequestedDate = req?.RequestedDate.ToString("D"); RequestedDate = req?.RequestedDate.ToString("D");
if (Type.IsNullOrEmpty()) if (Type.IsNullOrEmpty())
{ {
Type = req?.RequestType.ToString(); Type = req?.RequestType.Humanize();
} }
Overview = req?.Overview; Overview = req?.Overview;
Year = req?.ReleaseDate.Year.ToString(); Year = req?.ReleaseDate.Year.ToString();
@ -91,7 +92,7 @@ namespace Ombi.Notifications
RequestedDate = req?.RequestedDate.ToString("D"); RequestedDate = req?.RequestedDate.ToString("D");
if (Type.IsNullOrEmpty()) if (Type.IsNullOrEmpty())
{ {
Type = req?.RequestType.ToString(); Type = req?.RequestType.Humanize();
} }
Overview = req?.ParentRequest.Overview; Overview = req?.ParentRequest.Overview;
@ -161,7 +162,7 @@ namespace Ombi.Notifications
IssueSubject = opts.Substitutes.TryGetValue("IssueSubject", out val) ? val : string.Empty; IssueSubject = opts.Substitutes.TryGetValue("IssueSubject", out val) ? val : string.Empty;
NewIssueComment = opts.Substitutes.TryGetValue("NewIssueComment", out val) ? val : string.Empty; NewIssueComment = opts.Substitutes.TryGetValue("NewIssueComment", out val) ? val : string.Empty;
UserName = opts.Substitutes.TryGetValue("IssueUser", out val) ? val : string.Empty; UserName = opts.Substitutes.TryGetValue("IssueUser", out val) ? val : string.Empty;
Type = opts.Substitutes.TryGetValue("RequestType", out val) ? val : string.Empty; Type = opts.Substitutes.TryGetValue("RequestType", out val) ? val.Humanize() : string.Empty;
} }
// User Defined // User Defined

@ -89,6 +89,7 @@ namespace Ombi.Schedule.Jobs.Emby
_log.LogInformation("We have found the request {0} on Emby, sending the notification", movie?.Title ?? string.Empty); _log.LogInformation("We have found the request {0} on Emby, sending the notification", movie?.Title ?? string.Empty);
movie.Available = true; movie.Available = true;
movie.MarkedAsAvailable = DateTime.Now;
if (movie.Available) if (movie.Available)
{ {
var recipient = movie.RequestedUser.Email.HasValue() ? movie.RequestedUser.Email : string.Empty; var recipient = movie.RequestedUser.Email.HasValue() ? movie.RequestedUser.Email : string.Empty;
@ -185,6 +186,7 @@ namespace Ombi.Schedule.Jobs.Emby
{ {
// We have fulfulled this request! // We have fulfulled this request!
child.Available = true; child.Available = true;
child.MarkedAsAvailable = DateTime.Now;
BackgroundJob.Enqueue(() => _notificationService.Publish(new NotificationOptions BackgroundJob.Enqueue(() => _notificationService.Publish(new NotificationOptions
{ {
DateTime = DateTime.Now, DateTime = DateTime.Now,

@ -123,6 +123,7 @@ namespace Ombi.Schedule.Jobs.Plex
{ {
// We have fulfulled this request! // We have fulfulled this request!
child.Available = true; child.Available = true;
child.MarkedAsAvailable = DateTime.Now;
_backgroundJobClient.Enqueue(() => _notificationService.Publish(new NotificationOptions _backgroundJobClient.Enqueue(() => _notificationService.Publish(new NotificationOptions
{ {
DateTime = DateTime.Now, DateTime = DateTime.Now,
@ -163,6 +164,7 @@ namespace Ombi.Schedule.Jobs.Plex
} }
movie.Available = true; movie.Available = true;
movie.MarkedAsAvailable = DateTime.Now;
if (movie.Available) if (movie.Available)
{ {
_backgroundJobClient.Enqueue(() => _notificationService.Publish(new NotificationOptions _backgroundJobClient.Enqueue(() => _notificationService.Publish(new NotificationOptions

@ -8,10 +8,13 @@ namespace Ombi.Store.Entities.Requests
{ {
public string Title { get; set; } public string Title { get; set; }
public bool Approved { get; set; } public bool Approved { get; set; }
public DateTime MarkedAsApproved { get; set; }
public DateTime RequestedDate { get; set; } public DateTime RequestedDate { get; set; }
public bool Available { get; set; } public bool Available { get; set; }
public DateTime? MarkedAsAvailable { get; set; }
public string RequestedUserId { get; set; } public string RequestedUserId { get; set; }
public bool? Denied { get; set; } public bool? Denied { get; set; }
public DateTime MarkedAsDenied { get; set; }
public string DeniedReason { get; set; } public string DeniedReason { get; set; }
public RequestType RequestType { get; set; } public RequestType RequestType { get; set; }

@ -0,0 +1,988 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Ombi.Store.Context;
namespace Ombi.Store.Migrations
{
[DbContext(typeof(OmbiContext))]
[Migration("20180730085903_UserStats")]
partial class UserStats
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.1.1-rtm-30846");
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<DateTime>("MarkedAsApproved");
b.Property<DateTime?>("MarkedAsAvailable");
b.Property<DateTime>("MarkedAsDenied");
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<DateTime>("MarkedAsApproved");
b.Property<DateTime?>("MarkedAsAvailable");
b.Property<DateTime>("MarkedAsDenied");
b.Property<string>("Overview");
b.Property<string>("PosterPath");
b.Property<int>("QualityOverride");
b.Property<DateTime>("ReleaseDate");
b.Property<int>("RequestType");
b.Property<DateTime>("RequestedDate");
b.Property<string>("RequestedUserId");
b.Property<int>("RootPathOverride");
b.Property<string>("Status");
b.Property<int>("TheMovieDbId");
b.Property<string>("Title");
b.HasKey("Id");
b.HasIndex("RequestedUserId");
b.ToTable("MovieRequests");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.RequestLog", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("EpisodeCount");
b.Property<DateTime>("RequestDate");
b.Property<int>("RequestId");
b.Property<int>("RequestType");
b.Property<string>("UserId");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("RequestLog");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.TvRequests", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Background");
b.Property<string>("ImdbId");
b.Property<string>("Overview");
b.Property<string>("PosterPath");
b.Property<int?>("QualityOverride");
b.Property<DateTime>("ReleaseDate");
b.Property<int?>("RootFolder");
b.Property<string>("Status");
b.Property<string>("Title");
b.Property<int>("TvDbId");
b.HasKey("Id");
b.ToTable("TvRequests");
});
modelBuilder.Entity("Ombi.Store.Entities.RequestSubscription", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("RequestId");
b.Property<int>("RequestType");
b.Property<string>("UserId");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("RequestSubscription");
});
modelBuilder.Entity("Ombi.Store.Entities.SickRageCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("TvDbId");
b.HasKey("Id");
b.ToTable("SickRageCache");
});
modelBuilder.Entity("Ombi.Store.Entities.SickRageEpisodeCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("EpisodeNumber");
b.Property<int>("SeasonNumber");
b.Property<int>("TvDbId");
b.HasKey("Id");
b.ToTable("SickRageEpisodeCache");
});
modelBuilder.Entity("Ombi.Store.Entities.SonarrCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("TvDbId");
b.HasKey("Id");
b.ToTable("SonarrCache");
});
modelBuilder.Entity("Ombi.Store.Entities.SonarrEpisodeCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("EpisodeNumber");
b.Property<bool>("HasFile");
b.Property<int>("SeasonNumber");
b.Property<int>("TvDbId");
b.HasKey("Id");
b.ToTable("SonarrEpisodeCache");
});
modelBuilder.Entity("Ombi.Store.Entities.Tokens", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Token");
b.Property<string>("UserId");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Tokens");
});
modelBuilder.Entity("Ombi.Store.Repository.Requests.EpisodeRequests", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("AirDate");
b.Property<bool>("Approved");
b.Property<bool>("Available");
b.Property<int>("EpisodeNumber");
b.Property<bool>("Requested");
b.Property<int>("SeasonId");
b.Property<string>("Title");
b.Property<string>("Url");
b.HasKey("Id");
b.HasIndex("SeasonId");
b.ToTable("EpisodeRequests");
});
modelBuilder.Entity("Ombi.Store.Repository.Requests.SeasonRequests", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("ChildRequestId");
b.Property<int>("SeasonNumber");
b.HasKey("Id");
b.HasIndex("ChildRequestId");
b.ToTable("SeasonRequests");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("Ombi.Store.Entities.OmbiUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b =>
{
b.HasOne("Ombi.Store.Entities.EmbyContent", "Series")
.WithMany("Episodes")
.HasForeignKey("ParentId")
.HasPrincipalKey("EmbyId");
});
modelBuilder.Entity("Ombi.Store.Entities.NotificationUserId", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser", "User")
.WithMany("NotificationUserIds")
.HasForeignKey("UserId");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b =>
{
b.HasOne("Ombi.Store.Entities.PlexServerContent", "Series")
.WithMany("Episodes")
.HasForeignKey("GrandparentKey")
.HasPrincipalKey("Key")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b =>
{
b.HasOne("Ombi.Store.Entities.PlexServerContent")
.WithMany("Seasons")
.HasForeignKey("PlexServerContentId");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.ChildRequests", b =>
{
b.HasOne("Ombi.Store.Entities.Requests.TvRequests", "ParentRequest")
.WithMany("ChildRequests")
.HasForeignKey("ParentRequestId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser")
.WithMany()
.HasForeignKey("RequestedUserId");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.IssueComments", b =>
{
b.HasOne("Ombi.Store.Entities.Requests.Issues", "Issues")
.WithMany("Comments")
.HasForeignKey("IssuesId");
b.HasOne("Ombi.Store.Entities.OmbiUser", "User")
.WithMany()
.HasForeignKey("UserId");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.Issues", b =>
{
b.HasOne("Ombi.Store.Entities.Requests.IssueCategory", "IssueCategory")
.WithMany()
.HasForeignKey("IssueCategoryId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("Ombi.Store.Entities.Requests.ChildRequests")
.WithMany("Issues")
.HasForeignKey("IssueId");
b.HasOne("Ombi.Store.Entities.Requests.MovieRequests")
.WithMany("Issues")
.HasForeignKey("IssueId");
b.HasOne("Ombi.Store.Entities.OmbiUser", "UserReported")
.WithMany()
.HasForeignKey("UserReportedId");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.MovieRequests", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser")
.WithMany()
.HasForeignKey("RequestedUserId");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.RequestLog", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser", "User")
.WithMany()
.HasForeignKey("UserId");
});
modelBuilder.Entity("Ombi.Store.Entities.RequestSubscription", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser", "User")
.WithMany()
.HasForeignKey("UserId");
});
modelBuilder.Entity("Ombi.Store.Entities.Tokens", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser", "User")
.WithMany()
.HasForeignKey("UserId");
});
modelBuilder.Entity("Ombi.Store.Repository.Requests.EpisodeRequests", b =>
{
b.HasOne("Ombi.Store.Repository.Requests.SeasonRequests", "Season")
.WithMany("Episodes")
.HasForeignKey("SeasonId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Ombi.Store.Repository.Requests.SeasonRequests", b =>
{
b.HasOne("Ombi.Store.Entities.Requests.ChildRequests", "ChildRequest")
.WithMany("SeasonRequests")
.HasForeignKey("ChildRequestId")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
}

@ -0,0 +1,72 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace Ombi.Store.Migrations
{
public partial class UserStats : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "MarkedAsApproved",
table: "MovieRequests",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<DateTime>(
name: "MarkedAsAvailable",
table: "MovieRequests",
nullable: true);
migrationBuilder.AddColumn<DateTime>(
name: "MarkedAsDenied",
table: "MovieRequests",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<DateTime>(
name: "MarkedAsApproved",
table: "ChildRequests",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<DateTime>(
name: "MarkedAsAvailable",
table: "ChildRequests",
nullable: true);
migrationBuilder.AddColumn<DateTime>(
name: "MarkedAsDenied",
table: "ChildRequests",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "MarkedAsApproved",
table: "MovieRequests");
migrationBuilder.DropColumn(
name: "MarkedAsAvailable",
table: "MovieRequests");
migrationBuilder.DropColumn(
name: "MarkedAsDenied",
table: "MovieRequests");
migrationBuilder.DropColumn(
name: "MarkedAsApproved",
table: "ChildRequests");
migrationBuilder.DropColumn(
name: "MarkedAsAvailable",
table: "ChildRequests");
migrationBuilder.DropColumn(
name: "MarkedAsDenied",
table: "ChildRequests");
}
}
}

@ -1,15 +1,9 @@
// <auto-generated /> // <auto-generated />
using System;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal;
using Ombi.Helpers;
using Ombi.Store.Context; using Ombi.Store.Context;
using Ombi.Store.Entities;
using Ombi.Store.Entities.Requests;
using System;
namespace Ombi.Store.Migrations namespace Ombi.Store.Migrations
{ {
@ -20,7 +14,7 @@ namespace Ombi.Store.Migrations
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "2.0.3-rtm-10026"); .HasAnnotation("ProductVersion", "2.1.1-rtm-30846");
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{ {
@ -481,6 +475,12 @@ namespace Ombi.Store.Migrations
b.Property<int?>("IssueId"); b.Property<int?>("IssueId");
b.Property<DateTime>("MarkedAsApproved");
b.Property<DateTime?>("MarkedAsAvailable");
b.Property<DateTime>("MarkedAsDenied");
b.Property<int>("ParentRequestId"); b.Property<int>("ParentRequestId");
b.Property<int>("RequestType"); b.Property<int>("RequestType");
@ -595,6 +595,12 @@ namespace Ombi.Store.Migrations
b.Property<int?>("IssueId"); b.Property<int?>("IssueId");
b.Property<DateTime>("MarkedAsApproved");
b.Property<DateTime?>("MarkedAsAvailable");
b.Property<DateTime>("MarkedAsDenied");
b.Property<string>("Overview"); b.Property<string>("Overview");
b.Property<string>("PosterPath"); b.Property<string>("PosterPath");

@ -1,23 +1,10 @@
/wwwroot/css/** node_modules
/wwwroot/fonts/** bin
/wwwroot/lib/** obj
/wwwroot/maps/** wwwroot/dist
/wwwroot/dist/** *.log
/wwwroot/*.js.map
/wwwroot/*.js
# dependencies
/node_modules
/bower_components
# misc
/.sass-cache /.sass-cache
/connect.lock /connect.lock
/coverage/* /coverage/*
/libpeerconnection.log
npm-debug.log
testem.log
#/typings
/systemjs.config.js*
/Logs/** /Logs/**
**.db **.db

@ -4,7 +4,7 @@
"version": "2.0.0", "version": "2.0.0",
"tasks": [ "tasks": [
{ {
"taskName": "restore", "label": "restore",
"command": "npm", "command": "npm",
"type": "shell", "type": "shell",
"args": [ "args": [
@ -14,7 +14,16 @@
"problemMatcher": [] "problemMatcher": []
}, },
{ {
"taskName": "build", "label": "clean",
"command": "dotnet",
"type": "shell",
"args": [
"clean"
],
"problemMatcher": "$msCompile"
},
{
"label": "build",
"command": "dotnet", "command": "dotnet",
"type": "shell", "type": "shell",
"args": [ "args": [
@ -27,7 +36,7 @@
"problemMatcher": "$msCompile" "problemMatcher": "$msCompile"
}, },
{ {
"taskName": "lint", "label": "lint",
"type": "shell", "type": "shell",
"command": "npm", "command": "npm",
"args": [ "args": [

@ -1,7 +1,7 @@
import { animate, style, transition, trigger } from "@angular/animations"; import { animate, style, transition, trigger } from "@angular/animations";
import { AnimationEntryMetadata } from "@angular/core"; import { AnimationTriggerMetadata } from "@angular/animations";
export const fadeInOutAnimation: AnimationEntryMetadata = trigger("fadeInOut", [ export const fadeInOutAnimation: AnimationTriggerMetadata = trigger("fadeInOut", [
transition(":enter", [ // :enter is alias to 'void => *' transition(":enter", [ // :enter is alias to 'void => *'
style({ opacity: 0 }), style({ opacity: 0 }),
animate(1000, style({ opacity: 1 })), animate(1000, style({ opacity: 1 })),

@ -134,6 +134,12 @@
<li [ngClass]="{'active': 'no' === translate.currentLang}"> <li [ngClass]="{'active': 'no' === translate.currentLang}">
<a (click)="translate.use('no')" [translate]="'NavigationBar.Language.Norwegian'"></a> <a (click)="translate.use('no')" [translate]="'NavigationBar.Language.Norwegian'"></a>
</li> </li>
<li [ngClass]="{'active': 'pt' === translate.currentLang}">
<a (click)="translate.use('pt')" [translate]="'NavigationBar.Language.BrazillianPortuguese'"></a>
</li>
<li [ngClass]="{'active': 'pl' === translate.currentLang}">
<a (click)="translate.use('pl')" [translate]="'NavigationBar.Language.Polish'"></a>
</li>
</ul> </ul>
</li> </li>
</ul> </ul>

@ -33,20 +33,20 @@ export class AppComponent implements OnInit {
private readonly jobService: JobService, private readonly jobService: JobService,
public readonly translate: TranslateService, public readonly translate: TranslateService,
private readonly identityService: IdentityService, private readonly identityService: IdentityService,
private readonly platformLocation: PlatformLocation) { private readonly platformLocation: PlatformLocation) {
const base = this.platformLocation.getBaseHrefFromDOM(); const base = this.platformLocation.getBaseHrefFromDOM();
if (base.length > 1) { if (base.length > 1) {
__webpack_public_path__ = base + "/dist/"; __webpack_public_path__ = base + "/dist/";
} }
this.translate.addLangs(["en", "de", "fr","da","es","it","nl","sv","no"]); this.translate.addLangs(["en", "de", "fr", "da", "es", "it", "nl", "sv", "no", "pl", "pt"]);
// this language will be used as a fallback when a translation isn't found in the current language // this language will be used as a fallback when a translation isn't found in the current language
this.translate.setDefaultLang("en"); this.translate.setDefaultLang("en");
// See if we can match the supported langs with the current browser lang // See if we can match the supported langs with the current browser lang
const browserLang: string = translate.getBrowserLang(); const browserLang: string = translate.getBrowserLang();
this.translate.use(browserLang.match(/en|fr|da|de|es|it|nl|sv|no/) ? browserLang : "en"); this.translate.use(browserLang.match(/en|fr|da|de|es|it|nl|sv|no|pl|pt/) ? browserLang : "en");
} }
public ngOnInit() { public ngOnInit() {
@ -88,8 +88,8 @@ export class AppComponent implements OnInit {
public openMobileApp(event: any) { public openMobileApp(event: any) {
event.preventDefault(); event.preventDefault();
if(!this.customizationSettings.applicationUrl) { if (!this.customizationSettings.applicationUrl) {
this.notificationService.warning("Mobile","Please ask your admin to setup the Application URL!"); this.notificationService.warning("Mobile", "Please ask your admin to setup the Application URL!");
return; return;
} }

@ -1,12 +1,12 @@
import {CommonModule, PlatformLocation} from "@angular/common"; import { CommonModule, PlatformLocation } from "@angular/common";
import {HttpClient, HttpClientModule} from "@angular/common/http"; import { HttpClient, HttpClientModule } from "@angular/common/http";
import {NgModule} from "@angular/core"; import { NgModule } from "@angular/core";
import {FormsModule, ReactiveFormsModule} from "@angular/forms"; import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import {HttpModule} from "@angular/http"; import { HttpModule } from "@angular/http";
import {MatButtonModule, MatCardModule, MatInputModule, MatTabsModule} from "@angular/material"; import { MatButtonModule, MatCardModule, MatInputModule, MatTabsModule } from "@angular/material";
import {BrowserModule} from "@angular/platform-browser"; import { BrowserModule } from "@angular/platform-browser";
import {BrowserAnimationsModule} from "@angular/platform-browser/animations"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import {RouterModule, Routes} from "@angular/router"; import { RouterModule, Routes } from "@angular/router";
import { JwtModule } from "@auth0/angular-jwt"; import { JwtModule } from "@auth0/angular-jwt";
@ -15,7 +15,7 @@ import { TranslateLoader, TranslateModule } from "@ngx-translate/core";
import { TranslateHttpLoader } from "@ngx-translate/http-loader"; import { TranslateHttpLoader } from "@ngx-translate/http-loader";
import { CookieService } from "ng2-cookies"; import { CookieService } from "ng2-cookies";
import { GrowlModule } from "primeng/components/growl/growl"; import { GrowlModule } from "primeng/components/growl/growl";
import { ButtonModule, CaptchaModule, ConfirmationService, ConfirmDialogModule, DataTableModule,DialogModule, SharedModule, SidebarModule, TooltipModule } from "primeng/primeng"; import { ButtonModule, CaptchaModule, ConfirmationService, ConfirmDialogModule, DataTableModule, DialogModule, SharedModule, SidebarModule, TooltipModule } from "primeng/primeng";
// Components // Components
import { AppComponent } from "./app.component"; import { AppComponent } from "./app.component";
@ -67,6 +67,14 @@ export function HttpLoaderFactory(http: HttpClient, platformLocation: PlatformLo
return new TranslateHttpLoader(http, "/translations/", `.json?v=${version}`); return new TranslateHttpLoader(http, "/translations/", `.json?v=${version}`);
} }
export function JwtTokenGetter() {
const token = localStorage.getItem("id_token");
if (!token) {
return "";
}
return token;
}
@NgModule({ @NgModule({
imports: [ imports: [
RouterModule.forRoot(routes), RouterModule.forRoot(routes),
@ -89,18 +97,12 @@ export function HttpLoaderFactory(http: HttpClient, platformLocation: PlatformLo
CaptchaModule, CaptchaModule,
TooltipModule, TooltipModule,
ConfirmDialogModule, ConfirmDialogModule,
CommonModule, CommonModule,
JwtModule.forRoot({ JwtModule.forRoot({
config: { config: {
tokenGetter: () => { tokenGetter: JwtTokenGetter,
const token = localStorage.getItem("id_token");
if (!token) {
return "";
}
return token;
},
}, },
}), }),
TranslateModule.forRoot({ TranslateModule.forRoot({
loader: { loader: {
provide: TranslateLoader, provide: TranslateLoader,
@ -119,7 +121,7 @@ export function HttpLoaderFactory(http: HttpClient, platformLocation: PlatformLo
TokenResetPasswordComponent, TokenResetPasswordComponent,
CookieComponent, CookieComponent,
LoginOAuthComponent, LoginOAuthComponent,
], ],
providers: [ providers: [
NotificationService, NotificationService,
AuthService, AuthService,

@ -1,8 +1,8 @@
import { PlatformLocation } from "@angular/common"; import { PlatformLocation } from "@angular/common";
import { HttpClient } from "@angular/common/http"; import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { JwtHelperService } from "@auth0/angular-jwt"; import { JwtHelperService } from "@auth0/angular-jwt";
import { Observable } from "rxjs/Rx"; import { Observable } from "rxjs";
import { ServiceHelpers } from "../services"; import { ServiceHelpers } from "../services";
import { ILocalUser, IUserLogin } from "./IUserLogin"; import { ILocalUser, IUserLogin } from "./IUserLogin";
@ -26,13 +26,13 @@ export class AuthService extends ServiceHelpers {
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});
} }
public loggedIn() { public loggedIn() {
const token: string = this.jwtHelperService.tokenGetter(); const token: string = this.jwtHelperService.tokenGetter();
if (!token) { if (!token) {
return false; return false;
} }
const tokenExpired: boolean = this.jwtHelperService.isTokenExpired(token); const tokenExpired: boolean = this.jwtHelperService.isTokenExpired(token);
return !tokenExpired; return !tokenExpired;
} }
@ -53,9 +53,9 @@ export class AuthService extends ServiceHelpers {
} else { } else {
u.roles.push(roles); u.roles.push(roles);
} }
return <ILocalUser>u; return <ILocalUser> u;
} }
return <ILocalUser>{}; return <ILocalUser> { };
} }
public hasRole(role: string): boolean { public hasRole(role: string): boolean {

@ -11,7 +11,7 @@ export class CookieComponent implements OnInit {
public ngOnInit() { public ngOnInit() {
const cookie = this.cookieService.getAll(); const cookie = this.cookieService.getAll();
if(cookie.Auth) { if (cookie.Auth) {
const jwtVal = cookie.Auth; const jwtVal = cookie.Auth;
localStorage.setItem("id_token", jwtVal); localStorage.setItem("id_token", jwtVal);
this.router.navigate(["search"]); this.router.navigate(["search"]);

@ -20,7 +20,7 @@ export interface IRecentlyAddedTvShows extends IRecentlyAddedMovies {
export interface IRecentlyAddedRangeModel { export interface IRecentlyAddedRangeModel {
from: Date; from: Date;
to: Date; to: Date;
} }
export enum RecentlyAddedType { export enum RecentlyAddedType {

@ -71,6 +71,10 @@ export interface ITvRequests {
status: string; status: string;
childRequests: IChildRequests[]; childRequests: IChildRequests[];
qualityOverride: number; qualityOverride: number;
background: any;
totalSeasons: number;
tvDbId: number;
open: boolean; // THIS IS FOR THE UI
// For UI display // For UI display
qualityOverrideTitle: string; qualityOverrideTitle: string;

@ -28,8 +28,16 @@ export interface ISearchTvResult {
available: boolean; available: boolean;
plexUrl: string; plexUrl: string;
embyUrl: string; embyUrl: string;
quality: string;
firstSeason: boolean; firstSeason: boolean;
latestSeason: boolean; latestSeason: boolean;
theTvDbId: string;
subscribed: boolean;
showSubscribe: boolean;
fullyAvailable: boolean;
partlyAvailable: boolean;
background: any;
open: boolean; // THIS IS FOR THE UI
} }
export interface ITvRequestViewModel { export interface ITvRequestViewModel {

@ -23,6 +23,11 @@ export interface ICreateWizardUser {
usePlexAdminAccount: boolean; usePlexAdminAccount: boolean;
} }
export interface IWizardUserResult {
result: boolean;
errors: string[];
}
export enum UserType { export enum UserType {
LocalUser = 1, LocalUser = 1,
PlexUser = 2, PlexUser = 2,

@ -37,10 +37,10 @@ export class IssueDetailsComponent implements OnInit {
private notificationService: NotificationService, private notificationService: NotificationService,
private imageService: ImageService, private imageService: ImageService,
private sanitizer: DomSanitizer, private sanitizer: DomSanitizer,
private readonly platformLocation: PlatformLocation) { private readonly platformLocation: PlatformLocation) {
this.route.params this.route.params
.subscribe((params: any) => { .subscribe((params: any) => {
this.issueId = parseInt(params.id); this.issueId = parseInt(params.id);
}); });
this.isAdmin = this.authService.hasRole("Admin") || this.authService.hasRole("PowerUser"); this.isAdmin = this.authService.hasRole("Admin") || this.authService.hasRole("PowerUser");
@ -53,8 +53,8 @@ export class IssueDetailsComponent implements OnInit {
this.defaultPoster = "../../../images/"; this.defaultPoster = "../../../images/";
} }
} }
public ngOnInit() { public ngOnInit() {
this.issueService.getIssue(this.issueId).subscribe(x => { this.issueService.getIssue(this.issueId).subscribe(x => {
this.issue = { this.issue = {
comments: x.comments, comments: x.comments,
@ -63,8 +63,8 @@ export class IssueDetailsComponent implements OnInit {
issueCategoryId: x.issueCategoryId, issueCategoryId: x.issueCategoryId,
subject: x.subject, subject: x.subject,
description: x.description, description: x.description,
status:x.status, status: x.status,
resolvedDate:x.resolvedDate, resolvedDate: x.resolvedDate,
title: x.title, title: x.title,
requestType: x.requestType, requestType: x.requestType,
requestId: x.requestId, requestId: x.requestId,
@ -117,7 +117,7 @@ export class IssueDetailsComponent implements OnInit {
} else { } else {
this.imageService.getTvBackground(Number(issue.providerId)).subscribe(x => { this.imageService.getTvBackground(Number(issue.providerId)).subscribe(x => {
if(x) { if (x) {
this.backgroundPath = this.sanitizer.bypassSecurityTrustStyle this.backgroundPath = this.sanitizer.bypassSecurityTrustStyle
("url(" + x + ")"); ("url(" + x + ")");
} }

@ -22,8 +22,8 @@ export class IssuesComponent implements OnInit {
constructor(private issueService: IssuesService) { } constructor(private issueService: IssuesService) { }
public ngOnInit() { public ngOnInit() {
this.getPending(); this.getPending();
this.getInProg(); this.getInProg();
this.getResolved(); this.getResolved();
this.issueService.getIssuesCount().subscribe(x => this.count = x); this.issueService.getIssuesCount().subscribe(x => this.count = x);
@ -61,5 +61,4 @@ export class IssuesComponent implements OnInit {
this.resolvedIssues = x; this.resolvedIssues = x;
}); });
} }
} }

@ -11,7 +11,7 @@ export class IssuesTableComponent {
@Input() public issues: IIssues[]; @Input() public issues: IIssues[];
@Input() public totalRecords: number; @Input() public totalRecords: number;
@Output() public changePage = new EventEmitter<IPagenator>(); @Output() public changePage = new EventEmitter<IPagenator>();
public IssueStatus = IssueStatus; public IssueStatus = IssueStatus;
@ -47,7 +47,7 @@ export class IssuesTableComponent {
//event.rows = Number of rows to display in new page //event.rows = Number of rows to display in new page
//event.page = Index of the new page //event.page = Index of the new page
//event.pageCount = Total number of pages //event.pageCount = Total number of pages
this.changePage.emit(event); this.changePage.emit(event);
} }

@ -30,9 +30,9 @@ export class LoginComponent implements OnDestroy, OnInit {
public landingFlag: boolean; public landingFlag: boolean;
public baseUrl: string; public baseUrl: string;
public loginWithOmbi: boolean; public loginWithOmbi: boolean;
public get appName(): string { public get appName(): string {
if(this.customizationSettings.applicationName) { if (this.customizationSettings.applicationName) {
return this.customizationSettings.applicationName; return this.customizationSettings.applicationName;
} else { } else {
return "Ombi"; return "Ombi";
@ -41,7 +41,7 @@ export class LoginComponent implements OnDestroy, OnInit {
private timer: any; private timer: any;
private clientId: string; private clientId: string;
private errorBody: string; private errorBody: string;
private errorValidation: string; private errorValidation: string;
@ -72,7 +72,7 @@ export class LoginComponent implements OnDestroy, OnInit {
} }
}); });
if(authService.loggedIn()) { if (authService.loggedIn()) {
this.router.navigate(["search"]); this.router.navigate(["search"]);
} }
} }
@ -103,9 +103,9 @@ 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, usePlexOAuth: false, plexTvPin: { id: 0, code: ""} }; const user = { password: value.password, username: value.username, rememberMe: value.rememberMe, usePlexOAuth: false, plexTvPin: { id: 0, code: "" } };
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
this.authenticationSettings.allowNoPassword = false; this.authenticationSettings.allowNoPassword = false;
return; return;
@ -125,9 +125,9 @@ export class LoginComponent implements OnDestroy, OnInit {
} }
public oauth() { public oauth() {
this.plexTv.GetPin(this.clientId, this.appName).subscribe(pin => { this.plexTv.GetPin(this.clientId, this.appName).subscribe((pin: any) => {
this.authService.login({usePlexOAuth: true, password:"",rememberMe:true,username:"", plexTvPin: pin}).subscribe(x => { this.authService.login({ usePlexOAuth: true, password: "", rememberMe: true, username: "", plexTvPin: pin }).subscribe(x => {
if (window.frameElement) { if (window.frameElement) {
// in frame // in frame
window.open(x.url, "_blank"); window.open(x.url, "_blank");
@ -144,12 +144,12 @@ export class LoginComponent implements OnDestroy, OnInit {
} }
private cycleBackground() { private cycleBackground() {
this.images.getRandomBackground().subscribe(x => { this.images.getRandomBackground().subscribe(x => {
this.background = ""; this.background = "";
}); });
this.images.getRandomBackground().subscribe(x => { this.images.getRandomBackground().subscribe(x => {
this.background = this.sanitizer this.background = this.sanitizer
.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 + ")");
}); });
} }
} }

@ -16,7 +16,6 @@ export class LoginOAuthComponent implements OnInit {
this.route.params this.route.params
.subscribe((params: any) => { .subscribe((params: any) => {
this.pin = params.pin; this.pin = params.pin;
}); });
} }
@ -26,21 +25,20 @@ export class LoginOAuthComponent implements OnInit {
public auth() { public auth() {
this.authService.oAuth(this.pin).subscribe(x => { this.authService.oAuth(this.pin).subscribe(x => {
if(x.access_token) { if (x.access_token) {
localStorage.setItem("id_token", x.access_token); localStorage.setItem("id_token", x.access_token);
if (this.authService.loggedIn()) { if (this.authService.loggedIn()) {
this.router.navigate(["search"]); this.router.navigate(["search"]);
return; return;
} }
} }
if(x.errorMessage) { if (x.errorMessage) {
this.error = x.errorMessage; this.error = x.errorMessage;
} }
}, err => { }, err => {
this.notify.error(err.statusText); this.notify.error(err.statusText);
this.router.navigate(["login"]); this.router.navigate(["login"]);
}); });
} }

@ -4,7 +4,7 @@ import { FormBuilder, FormGroup, Validators } from "@angular/forms";
import { DomSanitizer } from "@angular/platform-browser"; import { DomSanitizer } from "@angular/platform-browser";
import { ICustomizationSettings } from "../interfaces"; import { ICustomizationSettings } from "../interfaces";
import { IdentityService, ImageService,NotificationService, SettingsService } from "../services"; import { IdentityService, ImageService, NotificationService, SettingsService } from "../services";
@Component({ @Component({
templateUrl: "./resetpassword.component.html", templateUrl: "./resetpassword.component.html",

@ -1,5 +1,5 @@
import { Component, OnInit } from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { NguCarousel } from "@ngu/carousel"; import { NguCarouselConfig } from "@ngu/carousel";
import { ImageService, RecentlyAddedService } from "../services"; import { ImageService, RecentlyAddedService } from "../services";
import { IRecentlyAddedMovies, IRecentlyAddedTvShows } from "./../interfaces"; import { IRecentlyAddedMovies, IRecentlyAddedTvShows } from "./../interfaces";
@ -41,13 +41,13 @@ export class RecentlyAddedComponent implements OnInit {
public range: Date[]; public range: Date[];
public groupTv: boolean = false; public groupTv: boolean = false;
// https://github.com/sheikalthaf/ngu-carousel // https://github.com/sheikalthaf/ngu-carousel
public carouselTile: NguCarousel; public carouselTile: NguCarouselConfig;
constructor(private recentlyAddedService: RecentlyAddedService, constructor(private recentlyAddedService: RecentlyAddedService,
private imageService: ImageService) {} private imageService: ImageService) {}
public ngOnInit() { public ngOnInit() {
this.getMovies(); this.getMovies();
this.getShows(); this.getShows();
@ -67,10 +67,10 @@ export class RecentlyAddedComponent implements OnInit {
} }
public close() { public close() {
if(this.range.length < 2) { if (this.range.length < 2) {
return; return;
} }
if(!this.range[1]) { if (!this.range[1]) {
// If we do not have a second date then just set it to now // If we do not have a second date then just set it to now
this.range[1] = new Date(); this.range[1] = new Date();
} }
@ -82,13 +82,13 @@ export class RecentlyAddedComponent implements OnInit {
} }
private getShows() { private getShows() {
if(this.groupTv) { if (this.groupTv) {
this.recentlyAddedService.getRecentlyAddedTvGrouped().subscribe(x => { this.recentlyAddedService.getRecentlyAddedTvGrouped().subscribe(x => {
this.tv = x; this.tv = x;
this.tv.forEach((t) => { this.tv.forEach((t) => {
this.imageService.getTvPoster(t.tvDbId).subscribe(p => { this.imageService.getTvPoster(t.tvDbId).subscribe(p => {
if(p) { if (p) {
t.posterPath = p; t.posterPath = p;
} }
}); });
@ -97,10 +97,10 @@ export class RecentlyAddedComponent implements OnInit {
} else { } else {
this.recentlyAddedService.getRecentlyAddedTv().subscribe(x => { this.recentlyAddedService.getRecentlyAddedTv().subscribe(x => {
this.tv = x; this.tv = x;
this.tv.forEach((t) => { this.tv.forEach((t) => {
this.imageService.getTvPoster(t.tvDbId).subscribe(p => { this.imageService.getTvPoster(t.tvDbId).subscribe(p => {
if(p) { if (p) {
t.posterPath = p; t.posterPath = p;
} }
}); });
@ -114,11 +114,11 @@ export class RecentlyAddedComponent implements OnInit {
this.movies = x; this.movies = x;
this.movies.forEach((movie) => { this.movies.forEach((movie) => {
if(movie.theMovieDbId) { if (movie.theMovieDbId) {
this.imageService.getMoviePoster(movie.theMovieDbId).subscribe(p => { this.imageService.getMoviePoster(movie.theMovieDbId).subscribe(p => {
movie.posterPath = p; movie.posterPath = p;
}); });
} else if(movie.imdbId) { } else if (movie.imdbId) {
this.imageService.getMoviePoster(movie.imdbId).subscribe(p => { this.imageService.getMoviePoster(movie.imdbId).subscribe(p => {
movie.posterPath = p; movie.posterPath = p;
}); });

@ -45,8 +45,6 @@
<div> <div>
<div *ngFor="let request of movieRequests"> <div *ngFor="let request of movieRequests">
<div class="row"> <div class="row">
<div class="myBg backdrop" [style.background-image]="request.backgroundPath"></div> <div class="myBg backdrop" [style.background-image]="request.backgroundPath"></div>

@ -1,15 +1,12 @@
import { PlatformLocation } from "@angular/common"; import { PlatformLocation } from "@angular/common";
import { Component, Input, OnInit } from "@angular/core"; import { Component, Input, OnInit } from "@angular/core";
import { DomSanitizer } from "@angular/platform-browser"; import { DomSanitizer } from "@angular/platform-browser";
import "rxjs/add/operator/debounceTime"; import { Subject } from "rxjs";
import "rxjs/add/operator/distinctUntilChanged"; import { debounceTime, distinctUntilChanged } from "rxjs/operators";
import "rxjs/add/operator/map";
import { Subject } from "rxjs/Subject";
import { AuthService } from "../auth/auth.service"; import { AuthService } from "../auth/auth.service";
import { NotificationService, RadarrService, RequestService } from "../services";
import { FilterType, IFilter, IIssueCategory, IMovieRequests, IPagenator, IRadarrProfile, IRadarrRootFolder, OrderType } from "../interfaces"; import { FilterType, IFilter, IIssueCategory, IMovieRequests, IPagenator, IRadarrProfile, IRadarrRootFolder, OrderType } from "../interfaces";
import { NotificationService, RadarrService, RequestService } from "../services";
@Component({ @Component({
selector: "movie-requests", selector: "movie-requests",
@ -40,32 +37,33 @@ export class MovieRequestsComponent implements OnInit {
public orderType: OrderType = OrderType.RequestedDateDesc; public orderType: OrderType = OrderType.RequestedDateDesc;
public OrderType = OrderType; public OrderType = OrderType;
public totalMovies: number = 100; public totalMovies: number = 100;
private currentlyLoaded: number; private currentlyLoaded: number;
private amountToLoad: number; private amountToLoad: number;
constructor(private requestService: RequestService, constructor(
private auth: AuthService, private requestService: RequestService,
private notificationService: NotificationService, private auth: AuthService,
private radarrService: RadarrService, private notificationService: NotificationService,
private sanitizer: DomSanitizer, private radarrService: RadarrService,
private readonly platformLocation: PlatformLocation) { private sanitizer: DomSanitizer,
this.searchChanged private readonly platformLocation: PlatformLocation) {
.debounceTime(600) // Wait Xms after the last event before emitting last event this.searchChanged.pipe(
.distinctUntilChanged() // only emit if value is different from previous value debounceTime(600), // Wait Xms after the last event before emitting last event
.subscribe(x => { distinctUntilChanged(), // only emit if value is different from previous value
this.searchText = x as string; ).subscribe(x => {
if (this.searchText === "") { this.searchText = x as string;
this.resetSearch(); if (this.searchText === "") {
return; this.resetSearch();
} return;
this.requestService.searchMovieRequests(this.searchText) }
.subscribe(m => { this.requestService.searchMovieRequests(this.searchText)
this.setOverrides(m); .subscribe(m => {
this.movieRequests = m; this.setOverrides(m);
}); this.movieRequests = m;
}); });
});
this.defaultPoster = "../../../images/default_movie_poster.png"; this.defaultPoster = "../../../images/default_movie_poster.png";
const base = this.platformLocation.getBaseHrefFromDOM(); const base = this.platformLocation.getBaseHrefFromDOM();
if (base) { if (base) {
@ -75,7 +73,7 @@ export class MovieRequestsComponent implements OnInit {
public ngOnInit() { public ngOnInit() {
this.amountToLoad = 10; this.amountToLoad = 10;
this.currentlyLoaded = 10; this.currentlyLoaded = 10;
this.filter = { this.filter = {
availabilityFilter: FilterType.None, availabilityFilter: FilterType.None,
statusFilter: FilterType.None, statusFilter: FilterType.None,
@ -85,7 +83,7 @@ export class MovieRequestsComponent implements OnInit {
} }
public paginate(event: IPagenator) { public paginate(event: IPagenator) {
const skipAmount = event.first; const skipAmount = event.first;
this.loadRequests(this.amountToLoad, skipAmount); this.loadRequests(this.amountToLoad, skipAmount);
} }
@ -102,7 +100,7 @@ export class MovieRequestsComponent implements OnInit {
public changeAvailability(request: IMovieRequests, available: boolean) { public changeAvailability(request: IMovieRequests, available: boolean) {
request.available = available; request.available = available;
if(available) { if (available) {
this.requestService.markMovieAvailable({ id: request.id }).subscribe(x => { this.requestService.markMovieAvailable({ id: request.id }).subscribe(x => {
if (x.result) { if (x.result) {
this.notificationService.success( this.notificationService.success(
@ -173,7 +171,7 @@ export class MovieRequestsComponent implements OnInit {
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;
this.resetSearch(); this.resetSearch();
} }
@ -199,11 +197,11 @@ export class MovieRequestsComponent implements OnInit {
el.className = "active"; el.className = "active";
this.orderType = value; this.orderType = value;
this.loadInit(); this.loadInit();
} }
public subscribe(request: IMovieRequests) { public subscribe(request: IMovieRequests) {
request.subscribed = true; request.subscribed = true;
this.requestService.subscribeToMovie(request.id) this.requestService.subscribeToMovie(request.id)
.subscribe(x => { .subscribe(x => {
@ -238,10 +236,10 @@ export class MovieRequestsComponent implements OnInit {
} }
private loadRequests(amountToLoad: number, currentlyLoaded: number) { private loadRequests(amountToLoad: number, currentlyLoaded: number) {
this.requestService.getMovieRequests(amountToLoad, currentlyLoaded, this.orderType, this.filter) this.requestService.getMovieRequests(amountToLoad, currentlyLoaded, this.orderType, this.filter)
.subscribe(x => { .subscribe(x => {
this.setOverrides(x.collection); this.setOverrides(x.collection);
if(!this.movieRequests) { if (!this.movieRequests) {
this.movieRequests = []; this.movieRequests = [];
} }
this.movieRequests = x.collection; this.movieRequests = x.collection;
@ -364,6 +362,6 @@ export class MovieRequestsComponent implements OnInit {
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 + ")");
} }
} }

@ -4,7 +4,7 @@ import { IChildRequests } from "../interfaces";
import { NotificationService, RequestService } from "../services"; import { NotificationService, RequestService } from "../services";
@Component({ @Component({
selector:"tvrequests-children", selector: "tvrequests-children",
templateUrl: "./tvrequest-children.component.html", templateUrl: "./tvrequest-children.component.html",
}) })
export class TvRequestChildrenComponent { export class TvRequestChildrenComponent {
@ -21,17 +21,17 @@ export class TvRequestChildrenComponent {
.subscribe(x => { .subscribe(x => {
this.removeRequestFromUi(request); this.removeRequestFromUi(request);
this.requestDeleted.emit(request.id); this.requestDeleted.emit(request.id);
}); });
} }
public changeAvailability(request: IChildRequests, available: boolean) { public changeAvailability(request: IChildRequests, available: boolean) {
request.available = available; request.available = available;
request.seasonRequests.forEach((season)=> { request.seasonRequests.forEach((season) => {
season.episodes.forEach((ep)=> { season.episodes.forEach((ep) => {
ep.available = available; ep.available = available;
}); });
}); });
if(available) { if (available) {
this.requestService.markTvAvailable({ id: request.id }).subscribe(x => { this.requestService.markTvAvailable({ id: request.id }).subscribe(x => {
if (x.result) { if (x.result) {
this.notificationService.success( this.notificationService.success(

@ -4,124 +4,111 @@
</div> </div>
</div> </div>
<br /> <br />
<!--TODO: I believe this +1 is causing off by one error skipping loading of tv shows
When removed and scrolling very slowly everything works as expected, however
if you scroll really quickly then you start getting duplicates of movies
since it's async and some subsequent results return first and then incrementer
is increased so you see movies which had already been gotten show up...
Removing infinte-scroll and setting max to 1000 till we work out some sort of fix
--> <div>
<!--<div infinite-scroll <div *ngFor="let node of tvRequests.collection">
[infiniteScrollDistance]="1" <!--This is the section that holds the parent level results set-->
[infiniteScrollThrottle]="100" <div>
(scrolled)="loadMore()">--> <div class="row">
<div> <div class="myBg backdrop" [style.background-image]="node?.background"></div>
<p-treeTable [value]="tvRequests"> <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>
<p-column> <div class="col-sm-2 small-padding">
<ng-template let-col let-node="rowData" pTemplate="header">
Results
</ng-template>
<ng-template let-col let-node="rowData" pTemplate="body">
<!--This is the section that holds the parent level results set-->
<div *ngIf="!node.leaf">
<div class="row">
<div class="myBg backdrop" [style.background-image]="node?.data?.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="col-sm-2 small-padding" > <img class="img-responsive poster" src="{{node.posterPath || null}}" alt="poster">
<img class="img-responsive poster" src="{{node.data.posterPath || null}}" alt="poster"> </div>
<div class="col-sm-5 small-padding">
<div>
<a href="http://www.imdb.com/title/{{node.imdbId}}/" target="_blank">
<h4 class="request-title">{{node.title}} ({{node.releaseDate | date: 'yyyy'}})</h4>
</a>
</div>
<br />
<div>
<span>Status: </span>
<span class="label label-success">{{node.status}}</span>
</div> </div>
<div class="col-sm-5 small-padding">
<div>
<a href="http://www.imdb.com/title/{{node.data.imdbId}}/" target="_blank">
<h4 class="request-title">{{node.data.title}} ({{node.data.releaseDate | date: 'yyyy'}})</h4>
</a>
</div>
<br />
<div>
<span>Status: </span>
<span class="label label-success">{{node.data.status}}</span>
</div>
<div>Release Date: {{node.releaseDate | date}}</div>
<div>Release Date: {{node.data.releaseDate | date}}</div> <div *ngIf="isAdmin">
<div *ngIf="isAdmin"> <div *ngIf="node.qualityOverrideTitle" class="quality-override">{{ 'Requests.QualityOverride' | translate }}
<div *ngIf="node.data.qualityOverrideTitle" class="quality-override">{{ 'Requests.QualityOverride' | translate }} <span>{{node.qualityOverrideTitle}} </span>
<span>{{node.data.qualityOverrideTitle}} </span> </div>
</div> <div *ngIf="node.rootPathOverrideTitle" class="root-override">{{ 'Requests.RootFolderOverride' | translate }}
<div *ngIf="node.data.rootPathOverrideTitle" class="root-override">{{ 'Requests.RootFolderOverride' | translate }} <span>{{node.rootPathOverrideTitle}} </span>
<span>{{node.data.rootPathOverrideTitle}} </span>
</div>
</div> </div>
<br />
</div> </div>
<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>
<div *ngIf="isAdmin">
<!--Sonarr Root Folder-->
<div *ngIf="sonarrRootFolders" class="btn-group btn-split" id="rootFolderBtn">
<button type="button" class="btn btn-sm btn-warning-outline">
<i class="fa fa-plus"></i> {{ 'Requests.ChangeRootFolder' | 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 folder of sonarrRootFolders">
<a href="#" (click)="selectRootFolder(node.data, folder, $event)">{{folder.path}}</a>
</li>
</ul>
</div>
<!--Sonarr Quality Profiles --> <br />
<div *ngIf="sonarrProfiles" class="btn-group btn-split" id="changeQualityBtn"> </div>
<button type="button" class="btn btn-sm btn-warning-outline"> <div class="col-sm-3 col-sm-push-3 small-padding">
<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>
<button style="text-align: right" class="btn btn-sm btn-success-outline" (click)="openClosestTab(node,$event)">
<i class="fa fa-plus"></i> View</button>
<div *ngIf="isAdmin">
<!--Sonarr Root Folder-->
<div *ngIf="sonarrRootFolders" class="btn-group btn-split" id="rootFolderBtn">
<button type="button" class="btn btn-sm btn-warning-outline">
<i class="fa fa-plus"></i> {{ 'Requests.ChangeRootFolder' | 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 folder of sonarrRootFolders">
<a href="#" (click)="selectRootFolder(node, folder, $event)">{{folder.path}}</a>
</li>
</ul>
</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"> <!--Sonarr Quality Profiles -->
<i class="fa fa-plus"></i> {{ 'Requests.ReportIssue' | translate }} <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="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button> </button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu1"> <ul class="dropdown-menu">
<li *ngFor="let cat of issueCategories"><a [routerLink]="" (click)="reportIssue(cat, node.data)">{{cat.value}}</a></li> <li *ngFor="let profile of sonarrProfiles">
<a href="#" (click)="selectQualityProfile(node, profile, $event)">{{profile.name}}</a>
</li>
</ul> </ul>
</div> </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, node)">{{cat.value}}</a>
</li>
</ul>
</div> </div>
</div> </div>
</div> </div>
<!--This is the section that holds the child seasons if they want to specify specific episodes--> </div>
<div *ngIf="node.leaf"> <!--This is the section that holds the child seasons if they want to specify specific episodes-->
<tvrequests-children [childRequests]="node.data" [isAdmin] ="isAdmin" <div *ngIf="node.open">
(requestDeleted)="childRequestDeleted($event)"></tvrequests-children> <tvrequests-children [childRequests]="node.childRequests" [isAdmin]="isAdmin" (requestDeleted)="childRequestDeleted($event)"></tvrequests-children>
</div> </div>
</ng-template>
</p-column> <br/>
</p-treeTable> <br/>
</div>
<p-paginator [rows]="10" [totalRecords]="totalTv" (onPageChange)="paginate($event)"></p-paginator> <p-paginator [rows]="10" [totalRecords]="totalTv" (onPageChange)="paginate($event)"></p-paginator>
</div> </div>
<issue-report [movie]="false" [visible]="issuesBarVisible" [title]="issueRequest?.title" <issue-report [movie]="false" [visible]="issuesBarVisible" [title]="issueRequest?.title" [issueCategory]="issueCategorySelected"
[issueCategory]="issueCategorySelected" [id]="issueRequest?.id" [providerId]="issueProviderId" (visibleChange)="issuesBarVisible = $event;"></issue-report> [id]="issueRequest?.id" [providerId]="issueProviderId" (visibleChange)="issuesBarVisible = $event;"></issue-report>

@ -1,21 +1,13 @@
import { PlatformLocation } from "@angular/common"; import { PlatformLocation } from "@angular/common";
import { Component, Input, OnInit } from "@angular/core"; import { Component, Input, OnInit } from "@angular/core";
import { DomSanitizer } from "@angular/platform-browser"; import { DomSanitizer } from "@angular/platform-browser";
import "rxjs/add/operator/debounceTime"; import { Subject } from "rxjs";
import "rxjs/add/operator/distinctUntilChanged"; import { debounceTime, distinctUntilChanged } from "rxjs/operators";
import "rxjs/add/operator/map";
import { Subject } from "rxjs/Subject";
import { ImageService } from "./../services/image.service";
import "rxjs/add/operator/debounceTime";
import "rxjs/add/operator/distinctUntilChanged";
import "rxjs/add/operator/map";
import { AuthService } from "../auth/auth.service"; import { AuthService } from "../auth/auth.service";
import { FilterType, IIssueCategory, IPagenator, IRequestsViewModel, ISonarrProfile, ISonarrRootFolder, ITvRequests, OrderType } from "../interfaces";
import { NotificationService, RequestService, SonarrService } from "../services"; import { NotificationService, RequestService, SonarrService } from "../services";
import { ImageService } from "./../services/image.service";
import { TreeNode } from "primeng/primeng";
import { IIssueCategory, IPagenator, ISonarrProfile, ISonarrRootFolder, ITvRequests } from "../interfaces";
@Component({ @Component({
selector: "tv-requests", selector: "tv-requests",
@ -24,7 +16,7 @@ import { IIssueCategory, IPagenator, ISonarrProfile, ISonarrRootFolder, ITvRequ
}) })
export class TvRequestsComponent implements OnInit { export class TvRequestsComponent implements OnInit {
public tvRequests: TreeNode[]; public tvRequests: IRequestsViewModel<ITvRequests>;
public searchChanged = new Subject<string>(); public searchChanged = new Subject<string>();
public searchText: string; public searchText: string;
public isAdmin: boolean; public isAdmin: boolean;
@ -46,90 +38,68 @@ export class TvRequestsComponent implements OnInit {
private currentlyLoaded: number; private currentlyLoaded: number;
private amountToLoad: number; private amountToLoad: number;
constructor(private requestService: RequestService, constructor(
private auth: AuthService, private requestService: RequestService,
private sanitizer: DomSanitizer, private auth: AuthService,
private imageService: ImageService, private sanitizer: DomSanitizer,
private sonarrService: SonarrService, private imageService: ImageService,
private notificationService: NotificationService, private sonarrService: SonarrService,
private readonly platformLocation: PlatformLocation) { private notificationService: NotificationService,
this.searchChanged private readonly platformLocation: PlatformLocation) {
.debounceTime(600) // Wait Xms after the last event before emitting last event
.distinctUntilChanged() // only emit if value is different from previous value this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser");
.subscribe(x => { if (this.isAdmin) {
this.searchText = x as string; this.sonarrService.getQualityProfilesWithoutSettings()
if (this.searchText === "") { .subscribe(x => this.sonarrProfiles = x);
this.resetSearch();
return; this.sonarrService.getRootFoldersWithoutSettings()
} .subscribe(x => this.sonarrRootFolders = x);
this.requestService.searchTvRequestsTree(this.searchText)
.subscribe(m => {
this.tvRequests = m;
this.tvRequests.forEach((val) => this.loadBackdrop(val));
this.tvRequests.forEach((val) => this.setOverride(val.data));
});
});
this.defaultPoster = "../../../images/default_tv_poster.png";
const base = this.platformLocation.getBaseHrefFromDOM();
if (base) {
this.defaultPoster = "../../.." + base + "/images/default_tv_poster.png";
}
}
public openClosestTab(el: any) {
const rowclass = "undefined ng-star-inserted";
el = el.toElement || el.relatedTarget || el.target || el.srcElement;
if (el.nodeName === "BUTTON") {
const isButtonAlreadyActive = el.parentElement.querySelector(".active");
// if a Button already has Class: .active
if (isButtonAlreadyActive) {
isButtonAlreadyActive.classList.remove("active");
} else {
el.className += " active";
} }
} }
while (el.className !== rowclass) { public openClosestTab(node: ITvRequests,el: any) {
// Increment the loop to the parent node until we find the row we need el.preventDefault();
el = el.parentNode; node.open = !node.open;
}
// At this point, the while loop has stopped and `el` represents the element that has
// the class you specified
// Then we loop through the children to find the caret which we want to click
const caretright = "fa-caret-right";
const caretdown = "fa-caret-down";
for (const value of el.children) {
// the caret from the ui has 2 class selectors depending on if expanded or not
// we search for both since we want to still toggle the clicking
if (value.className.includes(caretright) || value.className.includes(caretdown)) {
// Then we tell JS to click the element even though we hid it from the UI
value.click();
//Break from loop since we no longer need to continue looking
break;
}
}
} }
public ngOnInit() { public ngOnInit() {
this.amountToLoad = 10; this.amountToLoad = 10;
this.currentlyLoaded = 10; this.currentlyLoaded = 10;
this.tvRequests = []; this.tvRequests = {collection:[], total:0};
this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser");
this.searchChanged.pipe(
debounceTime(600), // Wait Xms after the last event before emitting last event
distinctUntilChanged(), // only emit if value is different from previous value
).subscribe(x => {
this.searchText = x as string;
if (this.searchText === "") {
this.resetSearch();
return;
}
this.requestService.searchTvRequests(this.searchText)
.subscribe(m => {
this.tvRequests.collection = m;
this.tvRequests.collection.forEach((val) => this.loadBackdrop(val));
this.tvRequests.collection.forEach((val) => this.setOverride(val));
});
});
this.defaultPoster = "../../../images/default_tv_poster.png";
const base = this.platformLocation.getBaseHrefFromDOM();
if (base) {
this.defaultPoster = "../../.." + base + "/images/default_tv_poster.png";
}
this.loadInit(); this.loadInit();
} }
public paginate(event: IPagenator) { public paginate(event: IPagenator) {
const skipAmount = event.first; const skipAmount = event.first;
this.requestService.getTvRequestsTree(this.amountToLoad, skipAmount) this.requestService.getTvRequests(this.amountToLoad, skipAmount, OrderType.RequestedDateDesc, FilterType.None, FilterType.None)
.subscribe(x => { .subscribe(x => {
this.tvRequests = x; this.tvRequests = x;
this.currentlyLoaded = this.currentlyLoaded + this.amountToLoad; this.currentlyLoaded = this.currentlyLoaded + this.amountToLoad;
}); });
} }
public search(text: any) { public search(text: any) {
@ -150,14 +120,14 @@ export class TvRequestsComponent implements OnInit {
event.preventDefault(); event.preventDefault();
searchResult.rootFolder = rootFolderSelected.id; searchResult.rootFolder = rootFolderSelected.id;
this.setOverride(searchResult); this.setOverride(searchResult);
this.updateRequest(searchResult); this.setRootFolder(searchResult);
} }
public selectQualityProfile(searchResult: ITvRequests, profileSelected: ISonarrProfile, event: any) { public selectQualityProfile(searchResult: ITvRequests, profileSelected: ISonarrProfile, event: any) {
event.preventDefault(); event.preventDefault();
searchResult.qualityOverride = profileSelected.id; searchResult.qualityOverride = profileSelected.id;
this.setOverride(searchResult); this.setOverride(searchResult);
this.updateRequest(searchResult); this.setQualityProfile(searchResult);
} }
public reportIssue(catId: IIssueCategory, req: ITvRequests) { public reportIssue(catId: IIssueCategory, req: ITvRequests) {
@ -172,13 +142,24 @@ export class TvRequestsComponent implements OnInit {
this.setRootFolderOverrides(req); this.setRootFolderOverrides(req);
} }
private updateRequest(request: ITvRequests) { private setQualityProfile(req: ITvRequests) {
this.requestService.updateTvRequest(request) this.requestService.setQualityProfile(req.id, req.qualityOverride).subscribe(x => {
.subscribe(x => { if(x) {
this.notificationService.success("Request Updated"); this.notificationService.success("Quality profile updated");
this.setOverride(x); } else {
request = x; this.notificationService.error("Could not update the quality profile");
}); }
});
}
private setRootFolder(req: ITvRequests) {
this.requestService.setRootFolder(req.id, req.rootFolder).subscribe(x => {
if(x) {
this.notificationService.success("Quality profile updated");
} else {
this.notificationService.error("Could not update the quality profile");
}
});
} }
private setQualityOverrides(req: ITvRequests): void { private setQualityOverrides(req: ITvRequests): void {
@ -204,23 +185,15 @@ export class TvRequestsComponent implements OnInit {
private loadInit() { private loadInit() {
this.requestService.getTotalTv().subscribe(x => this.totalTv = x); this.requestService.getTotalTv().subscribe(x => this.totalTv = x);
this.requestService.getTvRequestsTree(this.amountToLoad, 0) this.requestService.getTvRequests(this.amountToLoad, 0, OrderType.RequestedDateDesc, FilterType.None, FilterType.None)
.subscribe(x => { .subscribe(x => {
this.tvRequests = x; this.tvRequests = x;
this.tvRequests.forEach((val, index) => { this.tvRequests.collection.forEach((val, index) => {
this.setDefaults(val); this.setDefaults(val);
this.loadBackdrop(val); this.loadBackdrop(val);
this.setOverride(val.data); this.setOverride(val);
}); });
}); });
if(this.isAdmin) {
this.sonarrService.getQualityProfilesWithoutSettings()
.subscribe(x => this.sonarrProfiles = x);
this.sonarrService.getRootFoldersWithoutSettings()
.subscribe(x => this.sonarrRootFolders = x);
}
} }
private resetSearch() { private resetSearch() {
@ -228,21 +201,21 @@ export class TvRequestsComponent implements OnInit {
this.loadInit(); this.loadInit();
} }
private setDefaults(val: any) { private setDefaults(val: ITvRequests) {
if (val.data.posterPath === null) { if (val.posterPath === null) {
val.data.posterPath = this.defaultPoster; val.posterPath = this.defaultPoster;
} }
} }
private loadBackdrop(val: TreeNode): void { private loadBackdrop(val: ITvRequests): void {
if (val.data.background != null) { if (val.background != null) {
val.data.background = this.sanitizer.bypassSecurityTrustStyle val.background = this.sanitizer.bypassSecurityTrustStyle
("url(https://image.tmdb.org/t/p/w1280" + val.data.background + ")"); ("url(https://image.tmdb.org/t/p/w1280" + val.background + ")");
} else { } else {
this.imageService.getTvBanner(val.data.tvDbId).subscribe(x => { this.imageService.getTvBanner(val.tvDbId).subscribe(x => {
if(x) { if (x) {
val.data.background = this.sanitizer.bypassSecurityTrustStyle val.background = this.sanitizer.bypassSecurityTrustStyle
("url(" + x + ")"); ("url(" + x + ")");
} }
}); });
} }

@ -1,11 +1,9 @@
import { PlatformLocation } from "@angular/common"; import { PlatformLocation } from "@angular/common";
import { Component, Input, OnInit } from "@angular/core"; import { Component, Input, OnInit } from "@angular/core";
import { DomSanitizer } from "@angular/platform-browser"; import { DomSanitizer } from "@angular/platform-browser";
import { TranslateService } from "@ngx-translate/core"; import { TranslateService } from "@ngx-translate/core";
import "rxjs/add/operator/debounceTime"; import { Subject } from "rxjs";
import "rxjs/add/operator/distinctUntilChanged"; import { debounceTime, distinctUntilChanged } from "rxjs/operators";
import "rxjs/add/operator/map";
import { Subject } from "rxjs/Subject";
import { AuthService } from "../auth/auth.service"; import { AuthService } from "../auth/auth.service";
import { IIssueCategory, IRequestEngineResult, ISearchMovieResult } from "../interfaces"; import { IIssueCategory, IRequestEngineResult, ISearchMovieResult } from "../interfaces";
@ -22,7 +20,7 @@ export class MovieSearchComponent implements OnInit {
public movieResults: ISearchMovieResult[]; public movieResults: ISearchMovieResult[];
public result: IRequestEngineResult; public result: IRequestEngineResult;
public searchApplied = false; public searchApplied = false;
@Input() public issueCategories: IIssueCategory[]; @Input() public issueCategories: IIssueCategory[];
@Input() public issuesEnabled: boolean; @Input() public issuesEnabled: boolean;
public issuesBarVisible = false; public issuesBarVisible = false;
@ -31,30 +29,31 @@ export class MovieSearchComponent implements OnInit {
public issueProviderId: string; public issueProviderId: string;
public issueCategorySelected: IIssueCategory; public issueCategorySelected: IIssueCategory;
public defaultPoster: string; public defaultPoster: string;
constructor(private searchService: SearchService, private requestService: RequestService, constructor(
private notificationService: NotificationService, private authService: AuthService, private searchService: SearchService, private requestService: RequestService,
private readonly translate: TranslateService, private sanitizer: DomSanitizer, private notificationService: NotificationService, private authService: AuthService,
private readonly platformLocation: PlatformLocation) { private readonly translate: TranslateService, private sanitizer: DomSanitizer,
private readonly platformLocation: PlatformLocation) {
this.searchChanged
.debounceTime(600) // Wait Xms after the last event before emitting last event this.searchChanged.pipe(
.distinctUntilChanged() // only emit if value is different from previous value debounceTime(600), // Wait Xms after the last event before emitting last event
.subscribe(x => { distinctUntilChanged(), // only emit if value is different from previous value
this.searchText = x as string; ).subscribe(x => {
if (this.searchText === "") { this.searchText = x as string;
this.clearResults(); if (this.searchText === "") {
return; this.clearResults();
} return;
this.searchService.searchMovie(this.searchText) }
.subscribe(x => { this.searchService.searchMovie(this.searchText)
this.movieResults = x; .subscribe(x => {
this.searchApplied = true; this.movieResults = x;
// Now let's load some extra info including IMDB Id this.searchApplied = true;
// This way the search is fast at displaying results. // Now let's load some extra info including IMDB Id
this.getExtraInfo(); // This way the search is fast at displaying results.
}); this.getExtraInfo();
}); });
});
this.defaultPoster = "../../../images/default_movie_poster.png"; this.defaultPoster = "../../../images/default_movie_poster.png";
const base = this.platformLocation.getBaseHrefFromDOM(); const base = this.platformLocation.getBaseHrefFromDOM();
if (base) { if (base) {
@ -69,7 +68,7 @@ export class MovieSearchComponent implements OnInit {
message: "", message: "",
result: false, result: false,
errorMessage: "", errorMessage: "",
}; };
this.popularMovies(); this.popularMovies();
} }
@ -105,7 +104,7 @@ export class MovieSearchComponent implements OnInit {
searchResult.approved = false; searchResult.approved = false;
searchResult.processed = false; searchResult.processed = false;
searchResult.requestProcessing = false; searchResult.requestProcessing = false;
} }
}); });
} catch (e) { } catch (e) {
@ -160,12 +159,12 @@ export class MovieSearchComponent implements OnInit {
public similarMovies(theMovieDbId: number) { public similarMovies(theMovieDbId: number) {
this.clearResults(); this.clearResults();
this.searchService.similarMovies(theMovieDbId) this.searchService.similarMovies(theMovieDbId)
.subscribe(x => { .subscribe(x => {
this.movieResults = x; this.movieResults = x;
this.getExtraInfo(); this.getExtraInfo();
}); });
} }
public subscribe(r: ISearchMovieResult) { public subscribe(r: ISearchMovieResult) {
r.subscribed = true; r.subscribed = true;
this.requestService.subscribeToMovie(r.requestId) this.requestService.subscribeToMovie(r.requestId)
@ -182,17 +181,17 @@ export class MovieSearchComponent implements OnInit {
}); });
} }
private getExtraInfo() { private getExtraInfo() {
this.movieResults.forEach((val, index) => { this.movieResults.forEach((val, index) => {
if (val.posterPath === null) { if (val.posterPath === null) {
val.posterPath = this.defaultPoster; val.posterPath = this.defaultPoster;
} else { } else {
val.posterPath = "https://image.tmdb.org/t/p/w300/" + val.posterPath; val.posterPath = "https://image.tmdb.org/t/p/w300/" + val.posterPath;
} }
val.background = this.sanitizer.bypassSecurityTrustStyle val.background = this.sanitizer.bypassSecurityTrustStyle
("url(" + "https://image.tmdb.org/t/p/w1280" + val.backdropPath + ")"); ("url(" + "https://image.tmdb.org/t/p/w1280" + val.backdropPath + ")");
this.searchService.getMovieInformation(val.id) this.searchService.getMovieInformation(val.id)
.subscribe(m => { .subscribe(m => {
this.updateItem(val, m); this.updateItem(val, m);
}); });
@ -203,7 +202,7 @@ export class MovieSearchComponent implements OnInit {
const index = this.movieResults.indexOf(key, 0); const index = this.movieResults.indexOf(key, 0);
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; this.movieResults[index].posterPath = copy.posterPath;
} }

@ -1,13 +1,10 @@
import { Component, OnInit } from "@angular/core"; import { Component, OnInit } from "@angular/core";
import "rxjs/add/operator/debounceTime"; import { Subject } from "rxjs";
import "rxjs/add/operator/distinctUntilChanged"; import { debounceTime, distinctUntilChanged } from "rxjs/operators";
import "rxjs/add/operator/map";
import { Subject } from "rxjs/Subject";
import { AuthService } from "../auth/auth.service"; import { AuthService } from "../auth/auth.service";
import { NotificationService, RequestService, SearchService } from "../services";
import { IRequestEngineResult, ISearchMovieResult, ISearchMovieResultContainer } from "../interfaces"; import { IRequestEngineResult, ISearchMovieResult, ISearchMovieResultContainer } from "../interfaces";
import { NotificationService, RequestService, SearchService } from "../services";
@Component({ @Component({
selector: "movie-search-grid", selector: "movie-search-grid",
@ -21,28 +18,29 @@ export class MovieSearchGridComponent implements OnInit {
public movieResultGrid: ISearchMovieResultContainer[] = []; public movieResultGrid: ISearchMovieResultContainer[] = [];
public result: IRequestEngineResult; public result: IRequestEngineResult;
public searchApplied = false; public searchApplied = false;
constructor(private searchService: SearchService, private requestService: RequestService,
private notificationService: NotificationService, private authService: AuthService) {
this.searchChanged constructor(
.debounceTime(600) // Wait Xms afterthe last event before emitting last event private searchService: SearchService, private requestService: RequestService,
.distinctUntilChanged() // only emit if value is different from previous value private notificationService: NotificationService, private authService: AuthService) {
.subscribe(x => {
this.searchText = x as string; this.searchChanged.pipe(
if (this.searchText === "") { debounceTime(600), // Wait Xms afterthe last event before emitting last event
this.clearResults(); distinctUntilChanged(), // only emit if value is different from previous value
return; ).subscribe(x => {
} this.searchText = x as string;
this.searchService.searchMovie(this.searchText) if (this.searchText === "") {
.subscribe(x => { this.clearResults();
this.movieResults = x; return;
this.searchApplied = true; }
// Now let's load some exta info including IMDBId this.searchService.searchMovie(this.searchText)
// This way the search is fast at displaying results. .subscribe(x => {
this.getExtaInfo(); this.movieResults = x;
}); this.searchApplied = true;
}); // Now let's load some exta info including IMDBId
// This way the search is fast at displaying results.
this.getExtaInfo();
});
});
} }
public ngOnInit() { public ngOnInit() {
@ -67,7 +65,7 @@ export class MovieSearchGridComponent implements OnInit {
} }
try { try {
this.requestService.requestMovie({ theMovieDbId : searchResult.id}) this.requestService.requestMovie({ theMovieDbId: searchResult.id })
.subscribe(x => { .subscribe(x => {
this.result = x; this.result = x;
@ -129,7 +127,7 @@ export class MovieSearchGridComponent implements OnInit {
}); });
} }
private getExtaInfo() { private getExtaInfo() {
this.movieResults.forEach((val) => { this.movieResults.forEach((val) => {
this.searchService.getMovieInformation(val.id) this.searchService.getMovieInformation(val.id)
.subscribe(m => this.updateItem(val, m)); .subscribe(m => this.updateItem(val, m));
@ -147,18 +145,17 @@ export class MovieSearchGridComponent implements OnInit {
this.movieResults = []; this.movieResults = [];
this.searchApplied = false; this.searchApplied = false;
} }
private processGrid(movies: ISearchMovieResult[]) { private processGrid(movies: ISearchMovieResult[]) {
let container = <ISearchMovieResultContainer>{ movies: [] }; let container = <ISearchMovieResultContainer> { movies: [] };
movies.forEach((movie, i) => { movies.forEach((movie, i) => {
i++; i++;
if((i % 4) === 0) { if ((i % 4) === 0) {
container.movies.push(movie); container.movies.push(movie);
this.movieResultGrid.push(container); this.movieResultGrid.push(container);
container = <ISearchMovieResultContainer>{ movies: [] }; container = <ISearchMovieResultContainer> { movies: [] };
} else { } else {
container.movies.push(movie);
container.movies.push(movie);
} }
}); });
this.movieResultGrid.push(container); this.movieResultGrid.push(container);

@ -25,7 +25,7 @@ const routes: Routes = [
{ path: "show/:id", component: SeriesInformationComponent, canActivate: [AuthGuard] }, { path: "show/:id", component: SeriesInformationComponent, canActivate: [AuthGuard] },
]; ];
@NgModule({ @NgModule({
imports: [ imports: [
CommonModule, CommonModule,
FormsModule, FormsModule,
RouterModule.forChild(routes), RouterModule.forChild(routes),

@ -1,5 +1,4 @@
import { Component, Input, OnInit} from "@angular/core"; import { Component, Input, OnInit} from "@angular/core";
import "rxjs/add/operator/takeUntil";
import { NotificationService } from "../services"; import { NotificationService } from "../services";
import { RequestService } from "../services"; import { RequestService } from "../services";
@ -39,25 +38,25 @@ export class SeriesInformationComponent implements OnInit {
}); });
}); });
if(!selected) { if (!selected) {
this.notificationService.error("You need to select some episodes!"); this.notificationService.error("You need to select some episodes!");
return; return;
} }
this.series.requested = true; this.series.requested = true;
const viewModel = <ITvRequestViewModel>{ firstSeason: this.series.firstSeason, latestSeason: this.series.latestSeason, requestAll: this.series.requestAll, tvDbId: this.series.id}; const viewModel = <ITvRequestViewModel> { firstSeason: this.series.firstSeason, latestSeason: this.series.latestSeason, requestAll: this.series.requestAll, tvDbId: this.series.id};
viewModel.seasons = []; viewModel.seasons = [];
this.series.seasonRequests.forEach((season) => { this.series.seasonRequests.forEach((season) => {
const seasonsViewModel = <ISeasonsViewModel>{seasonNumber: season.seasonNumber, episodes: []}; const seasonsViewModel = <ISeasonsViewModel> {seasonNumber: season.seasonNumber, episodes: []};
season.episodes.forEach(ep => { season.episodes.forEach(ep => {
if(!this.series.latestSeason || !this.series.requestAll || !this.series.firstSeason) { if (!this.series.latestSeason || !this.series.requestAll || !this.series.firstSeason) {
if(ep.selected) { if (ep.selected) {
seasonsViewModel.episodes.push({episodeNumber: ep.episodeNumber}); seasonsViewModel.episodes.push({episodeNumber: ep.episodeNumber});
} }
} }
}); });
viewModel.seasons.push(seasonsViewModel); viewModel.seasons.push(seasonsViewModel);
}); });

@ -5,7 +5,7 @@
<div class="input-group-addon right-radius"> <div class="input-group-addon right-radius">
<div class="btn-group"> <div class="btn-group">
<a href="#" class="btn btn-sm btn-primary-outline dropdown-toggle" data-toggle="dropdown" aria-expanded="false"> <a href="#" class="btn btn-sm btn-primary-outline dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
{{ 'Search.Suggestions' | translate }} {{ 'Search.Suggestions' | translate }}
<i class="fa fa-chevron-down"></i> <i class="fa fa-chevron-down"></i>
</a> </a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
@ -42,129 +42,125 @@
<i class='fa fa-film no-search-results-icon'></i> <i class='fa fa-film no-search-results-icon'></i>
<div class='no-search-results-text'>{{ 'Search.NoResults' | translate }}</div> <div class='no-search-results-text'>{{ 'Search.NoResults' | translate }}</div>
</div> </div>
<p-treeTable [value]="tvResults"> <div *ngIf="tvResults" >
<p-column> <div *ngFor="let node of tvResults">
<ng-template let-col let-node="rowData" pTemplate="header"> <!--This is the section that holds the parent level search results set-->
{{ 'Search.TvShows.Results' | translate }} <div *ngIf="node">
</ng-template> <div class="row">
<ng-template let-col let-node="rowData" pTemplate="body">
<!--This is the section that holds the parent level search results set-->
<div *ngIf="!node.leaf">
<div class="row" >
<div class="myBg backdrop" [style.background-image]="node?.data?.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="col-sm-2 small-padding">
<img *ngIf="node?.data?.banner" class="img-responsive poster" width="150" [src]="node.data.banner" alt="poster">
</div> <div class="myBg backdrop" [style.background-image]="node?.background"></div>
<div class="col-sm-8 small-padding"> <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> <div class="col-sm-2 small-padding">
<img *ngIf="node.banner" class="img-responsive poster" width="150" [src]="node.banner" alt="poster">
</div>
<div class="col-sm-8 small-padding">
<div>
<a *ngIf="node.data.imdbId" href="{{node.data.imdbId}}" target="_blank"> <a *ngIf="node.imdbId" href="{{node.imdbId}}" target="_blank">
<h4>{{node.data.title}} ({{node.data.firstAired | date: 'yyyy'}})</h4> <h4>{{node.title}} ({{node.firstAired | date: 'yyyy'}})</h4>
</a>
<span class="tags">
<a *ngIf="node.homepage" id="homepageLabel" href="{{node.homepage}}" target="_blank">
<span class="label label-info">{{ 'Search.Movies.HomePage' | translate }}</span>
</a> </a>
<span class="tags">
<a *ngIf="node.data.homepage" id="homepageLabel" href="{{node.data.homepage}}" target="_blank"><span class="label label-info" >{{ 'Search.Movies.HomePage' | translate }}</span></a>
<a *ngIf="node.data.trailer" id="trailerLabel" href="{{node.data.trailer}}" target="_blank"><span class="label label-info">{{ 'Search.Movies.Trailer' | translate }}</span></a>
<span *ngIf="node.data.status" class="label label-primary" id="statusLabel" target="_blank">{{node.data.status}}</span> <a *ngIf="node.trailer" id="trailerLabel" href="{{node.trailer}}" target="_blank">
<span class="label label-info">{{ 'Search.Movies.Trailer' | translate }}</span>
</a>
<span *ngIf="node.status" class="label label-primary" id="statusLabel" target="_blank">{{node.status}}</span>
<span *ngIf="node.firstAired" class="label label-info" target="_blank" id="airDateLabel">{{ 'Search.TvShows.AirDate' | translate }} {{node.firstAired | date: 'dd/MM/yyyy'}}</span>
<span *ngIf="node.data.firstAired" class="label label-info" target="_blank" id="airDateLabel">{{ 'Search.TvShows.AirDate' | translate }} {{node.data.firstAired | date: 'dd/MM/yyyy'}}</span> <span *ngIf="node.network" class="label label-info" id="networkLabel" target="_blank">{{node.network}}</span>
<span *ngIf="node.available" class="label label-success" id="availableLabel">{{ 'Common.Available' | translate }}</span>
<span *ngIf="node.partlyAvailable" class="label label-warning" id="partiallyAvailableLabel">{{ 'Common.PartlyAvailable' | translate }}</span>
<span *ngIf="node.data.network" class="label label-info" id="networkLabel" target="_blank">{{node.data.network}}</span>
<span *ngIf="node.data.available" class="label label-success" id="availableLabel">{{ 'Common.Available' | translate }}</span>
<span *ngIf="node.data.partlyAvailable" class="label label-warning" id="partiallyAvailableLabel">{{ 'Common.PartlyAvailable' | translate }}</span>
</span> </span>
<br /> <br />
<br /> <br />
</div>
<p class="tv-overview">{{node.data.overview}}</p>
</div> </div>
<p class="tv-overview">{{node.overview}}</p>
</div>
<div class="col-sm-2 small-padding"> <div class="col-sm-2 small-padding">
<div *ngIf="!node.data.fullyAvailable" class="dropdown"> <div *ngIf="!node.fullyAvailable" class="dropdown">
<button class="btn btn-primary-outline dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true"> <button class="btn btn-primary-outline dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<i class="fa fa-plus"></i> {{ 'Common.Request' | translate }} <i class="fa fa-plus"></i> {{ 'Common.Request' | translate }}
<span class="caret"></span> <span class="caret"></span>
</button> </button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu1"> <ul class="dropdown-menu" aria-labelledby="dropdownMenu1">
<li> <li>
<a href="#" (click)="allSeasons(node.data, $event)">{{ 'Search.TvShows.AllSeasons' | translate }}</a> <a href="#" (click)="allSeasons(node, $event)">{{ 'Search.TvShows.AllSeasons' | translate }}</a>
</li> </li>
<li> <li>
<a href="#" (click)="firstSeason(node.data, $event)">{{ 'Search.TvShows.FirstSeason' | translate }}</a> <a href="#" (click)="firstSeason(node, $event)">{{ 'Search.TvShows.FirstSeason' | translate }}</a>
</li> </li>
<li> <li>
<a href="#" (click)="latestSeason(node.data, $event)">{{ 'Search.TvShows.LatestSeason' | translate }}</a> <a href="#" (click)="latestSeason(node, $event)">{{ 'Search.TvShows.LatestSeason' | translate }}</a>
</li> </li>
<li> <li>
<a href="#" (click)="openClosestTab($event)">{{ 'Search.TvShows.Select' | translate }}</a> <a href="#" (click)="openClosestTab(node, $event)">{{ 'Search.TvShows.Select' | translate }}</a>
</li> </li>
</ul> </ul>
</div> </div>
<div *ngIf="node.data.fullyAvailable">
<button style="text-align: right" class="btn btn-success-outline disabled" disabled>
<i class="fa fa-check"></i> {{ 'Common.Available' | translate }}
</button>
</div>
<br />
<div *ngIf="node.data.plexUrl && node.data.available">
<a style="text-align: right" class="btn btn-sm btn-success-outline" href="{{node.data.plexUrl}}"
target="_blank">
<i class="fa fa-eye"></i> {{ 'Search.ViewOnPlex' | translate }}
</a>
</div>
<div *ngIf="node.data.embyUrl && node.data.available">
<a style="text-align: right" id="embybtn" 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">
<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, node.data)">{{cat.value}}</a>
</li>
</ul>
</div>
<div *ngIf="!node.data.available">
<br />
<br />
</div>
<div *ngIf="node.fullyAvailable">
<button style="text-align: right" class="btn btn-success-outline disabled" disabled>
<i class="fa fa-check"></i> {{ 'Common.Available' | translate }}
</button>
</div>
<br />
<div *ngIf="node.plexUrl && node.available">
<a style="text-align: right" class="btn btn-sm btn-success-outline" href="{{node.plexUrl}}" target="_blank">
<i class="fa fa-eye"></i> {{ 'Search.ViewOnPlex' | translate }}
</a>
</div>
<div *ngIf="node.embyUrl && node.available">
<a style="text-align: right" id="embybtn" class="btn btn-sm btn-success-outline" href="{{node.embyUrl}}" target="_blank">
<i class="fa fa-eye"></i> {{ 'Search.ViewOnEmby' | translate }}
</a>
</div>
<div class="dropdown" *ngIf="issueCategories && issuesEnabled">
<button class="btn btn-sm btn-primary-outline dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true"
aria-expanded="true">
<i class="fa fa-plus"></i> {{ '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, node)">{{cat.value}}</a>
</li>
</ul>
</div>
<div *ngIf="!node.available">
<br />
<br />
</div> </div>
</div> </div>
</div>
<!--This is the section that holds the child seasons if they want to specify specific episodes-->
<div *ngIf="node.leaf">
<seriesinformation [seriesId]="node.data.id"></seriesinformation>
</div>
<br/> </div>
<br/> </div>
</ng-template> <!--This is the section that holds the child seasons if they want to specify specific episodes-->
</p-column> <div *ngIf="node.open">
</p-treeTable> <seriesinformation [seriesId]="node.id"></seriesinformation>
</div>
<br/>
<br/>
</div>
</div> </div>
</div> </div>

@ -1,15 +1,13 @@
import { PlatformLocation } from "@angular/common"; import { PlatformLocation } from "@angular/common";
import { Component, Input, OnInit } from "@angular/core"; import { Component, Input, OnInit } from "@angular/core";
import { DomSanitizer } from "@angular/platform-browser"; import { DomSanitizer } from "@angular/platform-browser";
import { Subject } from "rxjs/Subject"; import { Subject } from "rxjs";
import { debounceTime, distinctUntilChanged } from "rxjs/operators";
import { AuthService } from "../auth/auth.service"; import { AuthService } from "../auth/auth.service";
import { IIssueCategory, IRequestEngineResult, ISearchTvResult, ISeasonsViewModel, ITvRequestViewModel } from "../interfaces";
import { ImageService, NotificationService, RequestService, SearchService } from "../services"; import { ImageService, NotificationService, RequestService, SearchService } from "../services";
import { TreeNode } from "primeng/primeng";
import { IRequestEngineResult } from "../interfaces";
import { IIssueCategory, ISearchTvResult, ISeasonsViewModel, ITvRequestViewModel } from "../interfaces";
@Component({ @Component({
selector: "tv-search", selector: "tv-search",
templateUrl: "./tvsearch.component.html", templateUrl: "./tvsearch.component.html",
@ -19,7 +17,7 @@ export class TvSearchComponent implements OnInit {
public searchText: string; public searchText: string;
public searchChanged = new Subject<string>(); public searchChanged = new Subject<string>();
public tvResults: TreeNode[]; public tvResults: ISearchTvResult[];
public result: IRequestEngineResult; public result: IRequestEngineResult;
public searchApplied = false; public searchApplied = false;
public defaultPoster: string; public defaultPoster: string;
@ -32,57 +30,37 @@ export class TvSearchComponent implements OnInit {
public issueProviderId: string; public issueProviderId: string;
public issueCategorySelected: IIssueCategory; public issueCategorySelected: IIssueCategory;
constructor(private searchService: SearchService, private requestService: RequestService, constructor(
private notificationService: NotificationService, private authService: AuthService, private searchService: SearchService, private requestService: RequestService,
private imageService: ImageService, private sanitizer: DomSanitizer, private notificationService: NotificationService, private authService: AuthService,
private readonly platformLocation: PlatformLocation) { private imageService: ImageService, private sanitizer: DomSanitizer,
private readonly platformLocation: PlatformLocation) {
this.searchChanged
.debounceTime(600) // Wait Xms after the last event before emitting last event this.searchChanged.pipe(
.distinctUntilChanged() // only emit if value is different from previous value debounceTime(600), // Wait Xms after the last event before emitting last event
.subscribe(x => { distinctUntilChanged(), // only emit if value is different from previous value
this.searchText = x as string; ).subscribe(x => {
if (this.searchText === "") { this.searchText = x as string;
this.clearResults(); if (this.searchText === "") {
return; this.clearResults();
} return;
this.searchService.searchTvTreeNode(this.searchText) }
.subscribe(x => { this.searchService.searchTv(this.searchText)
this.tvResults = x; .subscribe(x => {
this.searchApplied = true; this.tvResults = x;
this.getExtraInfo(); this.searchApplied = true;
}); this.getExtraInfo();
}); });
});
this.defaultPoster = "../../../images/default_tv_poster.png"; this.defaultPoster = "../../../images/default_tv_poster.png";
const base = this.platformLocation.getBaseHrefFromDOM(); const base = this.platformLocation.getBaseHrefFromDOM();
if(base) { if (base) {
this.defaultPoster = "../../.." + base + "/images/default_tv_poster.png"; this.defaultPoster = "../../.." + base + "/images/default_tv_poster.png";
} }
} }
public openClosestTab(el: any) { public openClosestTab(node: ISearchTvResult,el: any) {
el.preventDefault(); el.preventDefault();
const rowclass = "undefined ng-star-inserted"; node.open = !node.open;
el = el.toElement || el.relatedTarget || el.target;
while (el.className !== rowclass) {
// Increment the loop to the parent node until we find the row we need
el = el.parentNode;
}
// At this point, the while loop has stopped and `el` represents the element that has
// the class you specified
// Then we loop through the children to find the caret which we want to click
const caretright = "fa-caret-right";
const caretdown = "fa-caret-down";
for (const value of el.children) {
// the caret from the ui has 2 class selectors depending on if expanded or not
// we search for both since we want to still toggle the clicking
if (value.className.includes(caretright) || value.className.includes(caretdown)) {
// Then we tell JS to click the element even though we hid it from the UI
value.click();
//Break from loop since we no longer need to continue looking
break;
}
}
} }
public ngOnInit() { public ngOnInit() {
@ -91,7 +69,7 @@ export class TvSearchComponent implements OnInit {
this.result = { this.result = {
message: "", message: "",
result: false, result: false,
errorMessage:"", errorMessage: "",
}; };
this.popularShows(); this.popularShows();
} }
@ -138,16 +116,16 @@ 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.id).subscribe(x => {
if(x) { if (x) {
val.data.background = this.sanitizer. val.background = this.sanitizer.
bypassSecurityTrustStyle bypassSecurityTrustStyle
("url(" + x + ")"); ("url(" + x + ")");
} }
}); });
this.searchService.getShowInformationTreeNode(val.data.id) this.searchService.getShowInformation(val.id)
.subscribe(x => { .subscribe(x => {
if (x.data) { if (x) {
this.setDefaults(x); this.setDefaults(x);
this.updateItem(val, x); this.updateItem(val, x);
} else { } else {
@ -165,19 +143,19 @@ export class TvSearchComponent implements OnInit {
if (this.authService.hasRole("admin") || this.authService.hasRole("AutoApproveMovie")) { if (this.authService.hasRole("admin") || this.authService.hasRole("AutoApproveMovie")) {
searchResult.approved = true; searchResult.approved = true;
} }
const viewModel = <ITvRequestViewModel>{ firstSeason: searchResult.firstSeason, latestSeason: searchResult.latestSeason, requestAll: searchResult.requestAll, tvDbId: searchResult.id}; const viewModel = <ITvRequestViewModel> { firstSeason: searchResult.firstSeason, latestSeason: searchResult.latestSeason, requestAll: searchResult.requestAll, tvDbId: searchResult.id };
viewModel.seasons = []; viewModel.seasons = [];
searchResult.seasonRequests.forEach((season) => { searchResult.seasonRequests.forEach((season) => {
const seasonsViewModel = <ISeasonsViewModel>{seasonNumber: season.seasonNumber, episodes: []}; const seasonsViewModel = <ISeasonsViewModel> { seasonNumber: season.seasonNumber, episodes: [] };
season.episodes.forEach(ep => { season.episodes.forEach(ep => {
if(!searchResult.latestSeason || !searchResult.requestAll || !searchResult.firstSeason) { if (!searchResult.latestSeason || !searchResult.requestAll || !searchResult.firstSeason) {
if(ep.requested) { if (ep.requested) {
seasonsViewModel.episodes.push({episodeNumber: ep.episodeNumber}); seasonsViewModel.episodes.push({ episodeNumber: ep.episodeNumber });
} }
} }
}); });
viewModel.seasons.push(seasonsViewModel); viewModel.seasons.push(seasonsViewModel);
}); });
@ -223,30 +201,30 @@ export class TvSearchComponent implements OnInit {
this.issueProviderId = req.id.toString(); this.issueProviderId = req.id.toString();
} }
private updateItem(key: TreeNode, updated: TreeNode) { private updateItem(key: ISearchTvResult, updated: ISearchTvResult) {
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].title = updated.title;
this.tvResults[index].data.banner = updated.data.banner; this.tvResults[index].banner = updated.banner;
this.tvResults[index].data.imdbId = updated.data.imdbId; this.tvResults[index].imdbId = updated.imdbId;
this.tvResults[index].data.seasonRequests = updated.data.seasonRequests; this.tvResults[index].seasonRequests = updated.seasonRequests;
this.tvResults[index].data.seriesId = updated.data.seriesId; this.tvResults[index].seriesId = updated.seriesId;
this.tvResults[index].data.fullyAvailable = updated.data.fullyAvailable; this.tvResults[index].fullyAvailable = updated.fullyAvailable;
this.tvResults[index].data.backdrop = updated.data.backdrop; this.tvResults[index].background = updated.banner;
} }
} }
private setDefaults(x: any) { private setDefaults(x: ISearchTvResult) {
if (x.data.banner === null) { if (x.banner === null) {
x.data.banner = this.defaultPoster; x.banner = this.defaultPoster;
} }
if (x.data.imdbId === null) { if (x.imdbId === null) {
x.data.imdbId = "https://www.tvmaze.com/shows/" + x.data.seriesId; x.imdbId = "https://www.tvmaze.com/shows/" + x.seriesId;
} else { } else {
x.data.imdbId = "http://www.imdb.com/title/" + x.data.imdbId + "/"; x.imdbId = "http://www.imdb.com/title/" + x.imdbId + "/";
} }
} }
private clearResults() { private clearResults() {

@ -1,7 +1,7 @@
import { PlatformLocation } from "@angular/common"; import { PlatformLocation } from "@angular/common";
import { HttpClient } from "@angular/common/http"; import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { Observable } from "rxjs/Rx"; import { Observable } from "rxjs";
import { ServiceHelpers } from "../service.helpers"; import { ServiceHelpers } from "../service.helpers";

@ -1,7 +1,7 @@
import { PlatformLocation } from "@angular/common"; import { PlatformLocation } from "@angular/common";
import { HttpClient } from "@angular/common/http"; import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { Observable } from "rxjs/Rx"; import { Observable } from "rxjs";
import { ServiceHelpers } from "../service.helpers"; import { ServiceHelpers } from "../service.helpers";

@ -1,12 +1,12 @@
import { PlatformLocation } from "@angular/common"; import { PlatformLocation } from "@angular/common";
import { HttpClient } from "@angular/common/http"; import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { Observable } from "rxjs/Rx"; import { Observable } from "rxjs";
import { ServiceHelpers } from "../service.helpers"; import { ServiceHelpers } from "../service.helpers";
import { IPlexAuthentication, IPlexLibResponse, IPlexOAuthViewModel,IPlexServer, IPlexServerViewModel, IUsersModel } from "../../interfaces"; import { IPlexAuthentication, IPlexLibResponse, IPlexOAuthViewModel, IPlexServer, IPlexServerViewModel, IUsersModel } from "../../interfaces";
@Injectable() @Injectable()
export class PlexService extends ServiceHelpers { export class PlexService extends ServiceHelpers {

@ -1,8 +1,8 @@
import { PlatformLocation } from "@angular/common"; import { PlatformLocation } from "@angular/common";
import { HttpClient } from "@angular/common/http"; import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { Observable } from "rxjs/Rx"; import { Observable } from "rxjs";
import { ServiceHelpers } from "../service.helpers"; import { ServiceHelpers } from "../service.helpers";

@ -2,19 +2,18 @@
import { HttpClient, HttpHeaders } from "@angular/common/http"; import { HttpClient, HttpHeaders } from "@angular/common/http";
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { Observable } from "rxjs/Rx"; import { Observable } from "rxjs";
import { IPlexPin } from "../../interfaces"; import { IPlexPin } from "../../interfaces";
@Injectable() @Injectable()
export class PlexTvService { export class PlexTvService {
constructor(private http: HttpClient, public platformLocation: PlatformLocation) { constructor(private http: HttpClient, public platformLocation: PlatformLocation) {
} }
public GetPin(clientId: string, applicationName: string): Observable<IPlexPin> { public GetPin(clientId: string, applicationName: string): Observable<IPlexPin> {
const headers = new HttpHeaders({"Content-Type":"application/json", const headers = new HttpHeaders({"Content-Type": "application/json",
"X-Plex-Client-Identifier": clientId, "X-Plex-Client-Identifier": clientId,
"X-Plex-Product": applicationName, "X-Plex-Product": applicationName,
"X-Plex-Version": "3", "X-Plex-Version": "3",

@ -1,7 +1,7 @@
import { PlatformLocation } from "@angular/common"; import { PlatformLocation } from "@angular/common";
import { HttpClient } from "@angular/common/http"; import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { Observable } from "rxjs/Rx"; import { Observable } from "rxjs";
import { IRadarrProfile, IRadarrRootFolder } from "../../interfaces"; import { IRadarrProfile, IRadarrRootFolder } from "../../interfaces";
import { IRadarrSettings } from "../../interfaces"; import { IRadarrSettings } from "../../interfaces";

@ -1,8 +1,8 @@
import { PlatformLocation } from "@angular/common"; import { PlatformLocation } from "@angular/common";
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http"; import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs/Rx"; import { Observable } from "rxjs";
import { ISonarrSettings } from "../../interfaces"; import { ISonarrSettings } from "../../interfaces";
import { ISonarrProfile, ISonarrRootFolder } from "../../interfaces"; import { ISonarrProfile, ISonarrRootFolder } from "../../interfaces";

@ -1,8 +1,8 @@
import { PlatformLocation } from "@angular/common"; import { PlatformLocation } from "@angular/common";
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http"; import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs/Rx"; import { Observable } from "rxjs";
import { ServiceHelpers } from "../service.helpers"; import { ServiceHelpers } from "../service.helpers";
@ -37,6 +37,7 @@ export class TesterService extends ServiceHelpers {
public pushbulletTest(settings: IPushbulletNotificationSettings): Observable<boolean> { public pushbulletTest(settings: IPushbulletNotificationSettings): Observable<boolean> {
return this.http.post<boolean>(`${this.url}pushbullet`, JSON.stringify(settings), {headers: this.headers}); return this.http.post<boolean>(`${this.url}pushbullet`, JSON.stringify(settings), {headers: this.headers});
} }
public pushoverTest(settings: IPushoverNotificationSettings): Observable<boolean> { public pushoverTest(settings: IPushoverNotificationSettings): Observable<boolean> {
return this.http.post<boolean>(`${this.url}pushover`, JSON.stringify(settings), {headers: this.headers}); return this.http.post<boolean>(`${this.url}pushover`, JSON.stringify(settings), {headers: this.headers});
} }
@ -52,7 +53,7 @@ export class TesterService extends ServiceHelpers {
public emailTest(settings: IEmailNotificationSettings): Observable<boolean> { public emailTest(settings: IEmailNotificationSettings): Observable<boolean> {
return this.http.post<boolean>(`${this.url}email`, JSON.stringify(settings), {headers: this.headers}); return this.http.post<boolean>(`${this.url}email`, JSON.stringify(settings), {headers: this.headers});
} }
public plexTest(settings: IPlexServer): Observable<boolean> { public plexTest(settings: IPlexServer): Observable<boolean> {
return this.http.post<boolean>(`${this.url}plex`, JSON.stringify(settings), {headers: this.headers}); return this.http.post<boolean>(`${this.url}plex`, JSON.stringify(settings), {headers: this.headers});
} }
@ -67,11 +68,11 @@ export class TesterService extends ServiceHelpers {
public sonarrTest(settings: ISonarrSettings): Observable<boolean> { public sonarrTest(settings: ISonarrSettings): Observable<boolean> {
return this.http.post<boolean>(`${this.url}sonarr`, JSON.stringify(settings), {headers: this.headers}); return this.http.post<boolean>(`${this.url}sonarr`, JSON.stringify(settings), {headers: this.headers});
} }
public couchPotatoTest(settings: ICouchPotatoSettings): Observable<boolean> { public couchPotatoTest(settings: ICouchPotatoSettings): Observable<boolean> {
return this.http.post<boolean>(`${this.url}couchpotato`, JSON.stringify(settings), {headers: this.headers}); return this.http.post<boolean>(`${this.url}couchpotato`, JSON.stringify(settings), {headers: this.headers});
} }
public telegramTest(settings: ITelegramNotifcationSettings): Observable<boolean> { public telegramTest(settings: ITelegramNotifcationSettings): Observable<boolean> {
return this.http.post<boolean>(`${this.url}telegram`, JSON.stringify(settings), {headers: this.headers}); return this.http.post<boolean>(`${this.url}telegram`, JSON.stringify(settings), {headers: this.headers});
@ -79,10 +80,12 @@ export class TesterService extends ServiceHelpers {
public sickrageTest(settings: ISickRageSettings): Observable<boolean> { public sickrageTest(settings: ISickRageSettings): Observable<boolean> {
return this.http.post<boolean>(`${this.url}sickrage`, JSON.stringify(settings), {headers: this.headers}); return this.http.post<boolean>(`${this.url}sickrage`, JSON.stringify(settings), {headers: this.headers});
} }
public newsletterTest(settings: INewsletterNotificationSettings): Observable<boolean> { public newsletterTest(settings: INewsletterNotificationSettings): Observable<boolean> {
return this.http.post<boolean>(`${this.url}newsletter`, JSON.stringify(settings), {headers: this.headers}); return this.http.post<boolean>(`${this.url}newsletter`, JSON.stringify(settings), {headers: this.headers});
} }
public mobileNotificationTest(settings: IMobileNotificationTestSettings): Observable<boolean> { public mobileNotificationTest(settings: IMobileNotificationTestSettings): Observable<boolean> {
return this.http.post<boolean>(`${this.url}mobile`, JSON.stringify(settings), {headers: this.headers}); return this.http.post<boolean>(`${this.url}mobile`, JSON.stringify(settings), {headers: this.headers});
} }

@ -1,10 +1,10 @@
import { PlatformLocation } from "@angular/common"; import { PlatformLocation } from "@angular/common";
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http"; import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs/Rx"; import { Observable } from "rxjs";
import { ICheckbox, ICreateWizardUser, IIdentityResult, IResetPasswordToken, IUpdateLocalUser, IUser } from "../interfaces"; import { ICheckbox, ICreateWizardUser, IIdentityResult, IResetPasswordToken, IUpdateLocalUser, IUser, IWizardUserResult } from "../interfaces";
import { ServiceHelpers } from "./service.helpers"; import { ServiceHelpers } from "./service.helpers";
@Injectable() @Injectable()
@ -12,14 +12,14 @@ export class IdentityService extends ServiceHelpers {
constructor(http: HttpClient, public platformLocation: PlatformLocation) { constructor(http: HttpClient, public platformLocation: PlatformLocation) {
super(http, "/api/v1/Identity/", platformLocation); super(http, "/api/v1/Identity/", platformLocation);
} }
public createWizardUser(user: ICreateWizardUser): Observable<boolean> { public createWizardUser(user: ICreateWizardUser): Observable<IWizardUserResult> {
return this.http.post<boolean>(`${this.url}Wizard/`, JSON.stringify(user), {headers: this.headers}); return this.http.post<IWizardUserResult>(`${this.url}Wizard/`, JSON.stringify(user), {headers: this.headers});
} }
public getUser(): Observable<IUser> { public getUser(): Observable<IUser> {
return this.http.get<IUser>(this.url, {headers: this.headers}); return this.http.get<IUser>(this.url, {headers: this.headers});
} }
public getAccessToken(): Observable<string> { public getAccessToken(): Observable<string> {
return this.http.get<string>(`${this.url}accesstoken`, {headers: this.headers}); return this.http.get<string>(`${this.url}accesstoken`, {headers: this.headers});
} }

@ -1,6 +1,6 @@
import { PlatformLocation } from "@angular/common"; import { PlatformLocation } from "@angular/common";
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { Observable } from "rxjs/Rx"; import { Observable } from "rxjs";
import { HttpClient } from "@angular/common/http"; import { HttpClient } from "@angular/common/http";
@ -20,21 +20,21 @@ export class ImageService extends ServiceHelpers {
public getTvBanner(tvdbid: number): Observable<string> { public getTvBanner(tvdbid: number): Observable<string> {
return this.http.get<string>(`${this.url}tv/${tvdbid}`, {headers: this.headers}); return this.http.get<string>(`${this.url}tv/${tvdbid}`, {headers: this.headers});
} }
public getMoviePoster(movieDbId: string): Observable<string> { public getMoviePoster(movieDbId: string): Observable<string> {
return this.http.get<string>(`${this.url}poster/movie/${movieDbId}`, { headers: this.headers }); return this.http.get<string>(`${this.url}poster/movie/${movieDbId}`, { headers: this.headers });
} }
public getTvPoster(tvdbid: number): Observable<string> { public getTvPoster(tvdbid: number): Observable<string> {
return this.http.get<string>(`${this.url}poster/tv/${tvdbid}`, { headers: this.headers }); return this.http.get<string>(`${this.url}poster/tv/${tvdbid}`, { headers: this.headers });
} }
public getMovieBackground(movieDbId: string): Observable<string> { public getMovieBackground(movieDbId: string): Observable<string> {
return this.http.get<string>(`${this.url}background/movie/${movieDbId}`, { headers: this.headers }); return this.http.get<string>(`${this.url}background/movie/${movieDbId}`, { headers: this.headers });
} }
public getTvBackground(tvdbid: number): Observable<string> { public getTvBackground(tvdbid: number): Observable<string> {
return this.http.get<string>(`${this.url}background/tv/${tvdbid}`, { headers: this.headers }); return this.http.get<string>(`${this.url}background/tv/${tvdbid}`, { headers: this.headers });
} }
} }

@ -1,10 +1,10 @@
import { PlatformLocation } from "@angular/common"; import { PlatformLocation } from "@angular/common";
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http"; import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs/Rx"; import { Observable } from "rxjs";
import { IIssueCategory, IIssueComments,IIssueCount, IIssues, IIssuesChat, INewIssueComments, IssueStatus, IUpdateStatus } from "../interfaces"; import { IIssueCategory, IIssueComments, IIssueCount, IIssues, IIssuesChat, INewIssueComments, IssueStatus, IUpdateStatus } from "../interfaces";
import { ServiceHelpers } from "./service.helpers"; import { ServiceHelpers } from "./service.helpers";
@Injectable() @Injectable()
@ -40,10 +40,10 @@ export class IssuesService extends ServiceHelpers {
public createIssue(issue: IIssues): Observable<number> { public createIssue(issue: IIssues): Observable<number> {
return this.http.post<number>(this.url, JSON.stringify(issue), {headers: this.headers}); return this.http.post<number>(this.url, JSON.stringify(issue), {headers: this.headers});
} }
public getIssue(id: number): Observable<IIssues> { public getIssue(id: number): Observable<IIssues> {
return this.http.get<IIssues>(`${this.url}${id}`, {headers: this.headers}); return this.http.get<IIssues>(`${this.url}${id}`, {headers: this.headers});
} }
public getComments(id: number): Observable<IIssuesChat[]> { public getComments(id: number): Observable<IIssuesChat[]> {
return this.http.get<IIssuesChat[]>(`${this.url}${id}/comments`, {headers: this.headers}); return this.http.get<IIssuesChat[]>(`${this.url}${id}/comments`, {headers: this.headers});

@ -1,8 +1,8 @@
import { PlatformLocation } from "@angular/common"; import { PlatformLocation } from "@angular/common";
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http"; import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs/Rx"; import { Observable } from "rxjs";
import { ServiceHelpers } from "./service.helpers"; import { ServiceHelpers } from "./service.helpers";

@ -1,6 +1,6 @@
import { PlatformLocation } from "@angular/common"; import { PlatformLocation } from "@angular/common";
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { Observable } from "rxjs/Rx"; import { Observable } from "rxjs";
import { HttpClient } from "@angular/common/http"; import { HttpClient } from "@angular/common/http";

@ -1,8 +1,8 @@
import { PlatformLocation } from "@angular/common"; import { PlatformLocation } from "@angular/common";
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http"; import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs/Rx"; import { Observable } from "rxjs";
import { IMobileUsersViewModel } from "./../interfaces"; import { IMobileUsersViewModel } from "./../interfaces";
import { ServiceHelpers } from "./service.helpers"; import { ServiceHelpers } from "./service.helpers";

@ -1,8 +1,8 @@
import { PlatformLocation } from "@angular/common"; import { PlatformLocation } from "@angular/common";
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http"; import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs/Rx"; import { Observable } from "rxjs";
import { IMassEmailModel } from "./../interfaces"; import { IMassEmailModel } from "./../interfaces";

@ -1,8 +1,8 @@
import { PlatformLocation } from "@angular/common"; import { PlatformLocation } from "@angular/common";
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http"; import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs/Rx"; import { Observable } from "rxjs";
import { IRecentlyAddedMovies, IRecentlyAddedTvShows } from "./../interfaces"; import { IRecentlyAddedMovies, IRecentlyAddedTvShows } from "./../interfaces";
import { ServiceHelpers } from "./service.helpers"; import { ServiceHelpers } from "./service.helpers";
@ -18,7 +18,8 @@ export class RecentlyAddedService extends ServiceHelpers {
public getRecentlyAddedTv(): Observable<IRecentlyAddedTvShows[]> { public getRecentlyAddedTv(): Observable<IRecentlyAddedTvShows[]> {
return this.http.get<IRecentlyAddedTvShows[]>(`${this. url}tv/`, {headers: this.headers}); return this.http.get<IRecentlyAddedTvShows[]>(`${this. url}tv/`, {headers: this.headers});
} }
public getRecentlyAddedTvGrouped(): Observable<IRecentlyAddedTvShows[]> { public getRecentlyAddedTvGrouped(): Observable<IRecentlyAddedTvShows[]> {
return this.http.get<IRecentlyAddedTvShows[]>(`${this.url}tv/grouped`, {headers: this.headers}); return this.http.get<IRecentlyAddedTvShows[]>(`${this.url}tv/grouped`, {headers: this.headers});
} }

@ -1,12 +1,11 @@
import { PlatformLocation } from "@angular/common"; import { PlatformLocation } from "@angular/common";
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http"; import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs/Rx"; import { Observable } from "rxjs";
import { TreeNode } from "primeng/primeng"; import { TreeNode } from "primeng/primeng";
import { IRequestEngineResult } from "../interfaces"; import { FilterType, IChildRequests, IFilter, IMovieRequestModel, IMovieRequests, IMovieUpdateModel, IRequestEngineResult, IRequestsViewModel, ITvRequests, ITvUpdateModel, OrderType } from "../interfaces";
import { IChildRequests, IFilter, IMovieRequestModel, IMovieRequests, IMovieUpdateModel, IRequestsViewModel, ITvRequests,ITvUpdateModel, OrderType } from "../interfaces";
import { ITvRequestViewModel } from "../interfaces"; import { ITvRequestViewModel } from "../interfaces";
import { ServiceHelpers } from "./service.helpers"; import { ServiceHelpers } from "./service.helpers";
@ -22,8 +21,8 @@ export class RequestService extends ServiceHelpers {
public getTotalMovies(): Observable<number> { public getTotalMovies(): Observable<number> {
return this.http.get<number>(`${this.url}Movie/total`, {headers: this.headers}); return this.http.get<number>(`${this.url}Movie/total`, {headers: this.headers});
} }
public getTotalTv(): Observable<number> { public getTotalTv(): Observable<number> {
return this.http.get<number>(`${this.url}tv/total`, {headers: this.headers}); return this.http.get<number>(`${this.url}tv/total`, {headers: this.headers});
} }
@ -64,8 +63,8 @@ export class RequestService extends ServiceHelpers {
return this.http.put<IMovieRequests>(`${this.url}movie/`, JSON.stringify(request), {headers: this.headers}); return this.http.put<IMovieRequests>(`${this.url}movie/`, JSON.stringify(request), {headers: this.headers});
} }
public getTvRequests(count: number, position: number): Observable<ITvRequests[]> { public getTvRequests(count: number, position: number, order: OrderType, status: FilterType, availability: FilterType): Observable<IRequestsViewModel<ITvRequests>> {
return this.http.get<ITvRequests[]>(`${this.url}tv/${count}/${position}`, {headers: this.headers}); return this.http.get<IRequestsViewModel<ITvRequests>>(`${this.url}tv/${count}/${position}/${order}/${status}/${availability}`, {headers: this.headers});
} }
public getTvRequestsTree(count: number, position: number): Observable<TreeNode[]> { public getTvRequestsTree(count: number, position: number): Observable<TreeNode[]> {
@ -87,7 +86,7 @@ export class RequestService extends ServiceHelpers {
public removeTvRequest(request: ITvRequests) { public removeTvRequest(request: ITvRequests) {
this.http.delete(`${this.url}tv/${request.id}`, {headers: this.headers}).subscribe(); this.http.delete(`${this.url}tv/${request.id}`, {headers: this.headers}).subscribe();
} }
public markTvAvailable(movie: ITvUpdateModel): Observable<IRequestEngineResult> { public markTvAvailable(movie: ITvUpdateModel): Observable<IRequestEngineResult> {
return this.http.post<IRequestEngineResult>(`${this.url}tv/available`, JSON.stringify(movie), {headers: this.headers}); return this.http.post<IRequestEngineResult>(`${this.url}tv/available`, JSON.stringify(movie), {headers: this.headers});
} }
@ -102,11 +101,11 @@ export class RequestService extends ServiceHelpers {
public updateChild(child: IChildRequests): Observable<IChildRequests> { public updateChild(child: IChildRequests): Observable<IChildRequests> {
return this.http.put<IChildRequests>(`${this.url}tv/child`, JSON.stringify(child), {headers: this.headers}); return this.http.put<IChildRequests>(`${this.url}tv/child`, JSON.stringify(child), {headers: this.headers});
} }
public denyChild(child: ITvUpdateModel): Observable<IRequestEngineResult> { public denyChild(child: ITvUpdateModel): Observable<IRequestEngineResult> {
return this.http.put<IRequestEngineResult>(`${this.url}tv/deny`, JSON.stringify(child), {headers: this.headers}); return this.http.put<IRequestEngineResult>(`${this.url}tv/deny`, JSON.stringify(child), {headers: this.headers});
} }
public approveChild(child: ITvUpdateModel): Observable<IRequestEngineResult> { public approveChild(child: ITvUpdateModel): Observable<IRequestEngineResult> {
return this.http.post<IRequestEngineResult>(`${this.url}tv/approve`, JSON.stringify(child), {headers: this.headers}); return this.http.post<IRequestEngineResult>(`${this.url}tv/approve`, JSON.stringify(child), {headers: this.headers});
@ -114,17 +113,23 @@ export class RequestService extends ServiceHelpers {
public deleteChild(child: IChildRequests): Observable<boolean> { public deleteChild(child: IChildRequests): Observable<boolean> {
return this.http.delete<boolean>(`${this.url}tv/child/${child.id}`, {headers: this.headers}); return this.http.delete<boolean>(`${this.url}tv/child/${child.id}`, {headers: this.headers});
} }
public subscribeToMovie(requestId: number): Observable<boolean> { public subscribeToMovie(requestId: number): Observable<boolean> {
return this.http.post<boolean>(`${this.url}movie/subscribe/${requestId}`, {headers: this.headers}); return this.http.post<boolean>(`${this.url}movie/subscribe/${requestId}`, {headers: this.headers});
} }
public unSubscribeToMovie(requestId: number): Observable<boolean> { public unSubscribeToMovie(requestId: number): Observable<boolean> {
return this.http.post<boolean>(`${this.url}movie/unsubscribe/${requestId}`, {headers: this.headers}); return this.http.post<boolean>(`${this.url}movie/unsubscribe/${requestId}`, {headers: this.headers});
} }
public subscribeToTv(requestId: number): Observable<boolean> { public subscribeToTv(requestId: number): Observable<boolean> {
return this.http.post<boolean>(`${this.url}tv/subscribe/${requestId}`, {headers: this.headers}); return this.http.post<boolean>(`${this.url}tv/subscribe/${requestId}`, {headers: this.headers});
} }
public unSubscribeToTv(requestId: number): Observable<boolean> { public unSubscribeToTv(requestId: number): Observable<boolean> {
return this.http.post<boolean>(`${this.url}tv/unsubscribe/${requestId}`, {headers: this.headers}); return this.http.post<boolean>(`${this.url}tv/unsubscribe/${requestId}`, {headers: this.headers});
} }
public setQualityProfile(requestId: number, qualityId: number): Observable<boolean> {
return this.http.put<boolean>(`${this.url}tv/quality/${requestId}/${qualityId}`, {headers: this.headers});
}
public setRootFolder(requestId: number, rootFolderId: number): Observable<boolean> {
return this.http.put<boolean>(`${this.url}tv/root/${requestId}/${rootFolderId}`, {headers: this.headers});
}
} }

@ -1,8 +1,8 @@
import { PlatformLocation } from "@angular/common"; import { PlatformLocation } from "@angular/common";
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http"; import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs/Rx"; import { Observable } from "rxjs";
import { TreeNode } from "primeng/primeng"; import { TreeNode } from "primeng/primeng";
import { ISearchMovieResult } from "../interfaces"; import { ISearchMovieResult } from "../interfaces";
@ -56,16 +56,16 @@ export class SearchService extends ServiceHelpers {
return this.http.get<ISearchTvResult>(`${this.url}/Tv/info/${theTvDbId}`, {headers: this.headers}); return this.http.get<ISearchTvResult>(`${this.url}/Tv/info/${theTvDbId}`, {headers: this.headers});
} }
public popularTv(): Observable<TreeNode[]> { public popularTv(): Observable<ISearchTvResult[]> {
return this.http.get<TreeNode[]>(`${this.url}/Tv/popular/tree`, {headers: this.headers}); return this.http.get<ISearchTvResult[]>(`${this.url}/Tv/popular`, {headers: this.headers});
} }
public mostWatchedTv(): Observable<TreeNode[]> { public mostWatchedTv(): Observable<ISearchTvResult[]> {
return this.http.get<TreeNode[]>(`${this.url}/Tv/mostwatched/tree`, {headers: this.headers}); return this.http.get<ISearchTvResult[]>(`${this.url}/Tv/mostwatched`, {headers: this.headers});
} }
public anticipatedTv(): Observable<TreeNode[]> { public anticipatedTv(): Observable<ISearchTvResult[]> {
return this.http.get<TreeNode[]>(`${this.url}/Tv/anticipated/tree`, {headers: this.headers}); return this.http.get<ISearchTvResult[]>(`${this.url}/Tv/anticipated`, {headers: this.headers});
} }
public trendingTv(): Observable<TreeNode[]> { public trendingTv(): Observable<ISearchTvResult[]> {
return this.http.get<TreeNode[]>(`${this.url}/Tv/trending/tree`, {headers: this.headers}); return this.http.get<ISearchTvResult[]>(`${this.url}/Tv/trending`, {headers: this.headers});
} }
} }

@ -1,6 +1,5 @@
import { PlatformLocation } from "@angular/common"; import { PlatformLocation } from "@angular/common";
import { HttpClient, HttpHeaders } from "@angular/common/http"; import { HttpClient, HttpHeaders } from "@angular/common/http";
import "rxjs/add/observable/throw";
export class ServiceHelpers { export class ServiceHelpers {

@ -1,7 +1,7 @@
import { PlatformLocation } from "@angular/common"; import { PlatformLocation } from "@angular/common";
import { HttpClient } from "@angular/common/http"; import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { Observable } from "rxjs/Rx"; import { Observable } from "rxjs";
import { import {
IAbout, IAbout,
@ -185,7 +185,7 @@ export class SettingsService extends ServiceHelpers {
public getMobileNotificationSettings(): Observable<IMobileNotifcationSettings> { public getMobileNotificationSettings(): Observable<IMobileNotifcationSettings> {
return this.http.get<IMobileNotifcationSettings>(`${this.url}/notifications/mobile`, {headers: this.headers}); return this.http.get<IMobileNotifcationSettings>(`${this.url}/notifications/mobile`, {headers: this.headers});
} }
public saveMobileNotificationSettings(settings: IMobileNotifcationSettings): Observable<boolean> { public saveMobileNotificationSettings(settings: IMobileNotifcationSettings): Observable<boolean> {
return this.http.post<boolean>(`${this.url}/notifications/mobile`, JSON.stringify(settings), {headers: this.headers}); return this.http.post<boolean>(`${this.url}/notifications/mobile`, JSON.stringify(settings), {headers: this.headers});
} }
@ -228,7 +228,7 @@ export class SettingsService extends ServiceHelpers {
public getTelegramNotificationSettings(): Observable<ITelegramNotifcationSettings> { public getTelegramNotificationSettings(): Observable<ITelegramNotifcationSettings> {
return this.http.get<ITelegramNotifcationSettings>(`${this.url}/notifications/telegram`, {headers: this.headers}); return this.http.get<ITelegramNotifcationSettings>(`${this.url}/notifications/telegram`, {headers: this.headers});
} }
public saveTelegramNotificationSettings(settings: ITelegramNotifcationSettings): Observable<boolean> { public saveTelegramNotificationSettings(settings: ITelegramNotifcationSettings): Observable<boolean> {
return this.http return this.http
@ -242,13 +242,13 @@ export class SettingsService extends ServiceHelpers {
public saveJobSettings(settings: IJobSettings): Observable<IJobSettingsViewModel> { public saveJobSettings(settings: IJobSettings): Observable<IJobSettingsViewModel> {
return this.http return this.http
.post<IJobSettingsViewModel>(`${this.url}/jobs`, JSON.stringify(settings), {headers: this.headers}); .post<IJobSettingsViewModel>(`${this.url}/jobs`, JSON.stringify(settings), {headers: this.headers});
} }
public testCron(body: ICronViewModelBody): Observable<ICronTestModel> { public testCron(body: ICronViewModelBody): Observable<ICronTestModel> {
return this.http return this.http
.post<ICronTestModel>(`${this.url}/testcron`, JSON.stringify(body), {headers: this.headers}); .post<ICronTestModel>(`${this.url}/testcron`, JSON.stringify(body), {headers: this.headers});
} }
public getSickRageSettings(): Observable<ISickRageSettings> { public getSickRageSettings(): Observable<ISickRageSettings> {
return this.http.get<ISickRageSettings>(`${this.url}/sickrage`, {headers: this.headers}); return this.http.get<ISickRageSettings>(`${this.url}/sickrage`, {headers: this.headers});
} }
@ -273,11 +273,11 @@ export class SettingsService extends ServiceHelpers {
public getNewsletterSettings(): Observable<INewsletterNotificationSettings> { public getNewsletterSettings(): Observable<INewsletterNotificationSettings> {
return this.http.get<INewsletterNotificationSettings>(`${this.url}/notifications/newsletter`, {headers: this.headers}); return this.http.get<INewsletterNotificationSettings>(`${this.url}/notifications/newsletter`, {headers: this.headers});
} }
public updateNewsletterDatabase(): Observable<boolean> { public updateNewsletterDatabase(): Observable<boolean> {
return this.http.post<boolean>(`${this.url}/notifications/newsletterdatabase`, {headers: this.headers}); return this.http.post<boolean>(`${this.url}/notifications/newsletterdatabase`, {headers: this.headers});
} }
public saveNewsletterSettings(settings: INewsletterNotificationSettings): Observable<boolean> { public saveNewsletterSettings(settings: INewsletterNotificationSettings): Observable<boolean> {
return this.http return this.http

@ -1,8 +1,8 @@
import { PlatformLocation } from "@angular/common"; import { PlatformLocation } from "@angular/common";
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http"; import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs/Rx"; import { Observable } from "rxjs";
import { ServiceHelpers } from "./service.helpers"; import { ServiceHelpers } from "./service.helpers";

@ -25,13 +25,13 @@ export class CustomizationComponent implements OnInit {
return item.fullName === this.settings.presetThemeName; return item.fullName === this.settings.presetThemeName;
})[0]; })[0];
if(existingTheme) { if (existingTheme) {
const index = this.themes.indexOf(existingTheme, 0); const index = this.themes.indexOf(existingTheme, 0);
if (index > -1) { if (index > -1) {
this.themes.splice(index, 1); this.themes.splice(index, 1);
} }
} }
if(x.hasPresetTheme) { if (x.hasPresetTheme) {
this.themes.unshift({displayName: x.presetThemeDisplayName, fullName: x.presetThemeName, url: existingTheme.url, version: x.presetThemeVersion}); this.themes.unshift({displayName: x.presetThemeDisplayName, fullName: x.presetThemeName, url: existingTheme.url, version: x.presetThemeVersion});
this.themes.unshift({displayName: "None", fullName: "None", url: "", version: ""}); this.themes.unshift({displayName: "None", fullName: "None", url: "", version: ""});
} else { } else {
@ -45,8 +45,8 @@ export class CustomizationComponent implements OnInit {
public save() { public save() {
this.settingsService.verifyUrl(this.settings.applicationUrl).subscribe(x => { this.settingsService.verifyUrl(this.settings.applicationUrl).subscribe(x => {
if(this.settings.applicationUrl) { if (this.settings.applicationUrl) {
if(!x) { if (!x) {
this.notificationService.error(`The URL "${this.settings.applicationUrl}" is not valid. Please format it correctly e.g. http://www.google.com/`); this.notificationService.error(`The URL "${this.settings.applicationUrl}" is not valid. Please format it correctly e.g. http://www.google.com/`);
return; return;
} }
@ -64,16 +64,16 @@ export class CustomizationComponent implements OnInit {
} }
public dropDownChange(event: any): void { public dropDownChange(event: any): void {
const selectedThemeFullName = <string>event.target.value; const selectedThemeFullName = <string> event.target.value;
const selectedTheme = this.themes.filter((val) => { const selectedTheme = this.themes.filter((val) => {
return val.fullName === selectedThemeFullName; return val.fullName === selectedThemeFullName;
})[0]; })[0];
if(selectedTheme.fullName === this.settings.presetThemeName) { if (selectedTheme.fullName === this.settings.presetThemeName) {
return; return;
} }
if(selectedTheme.fullName === "None" || selectedTheme.fullName === "-1") { if (selectedTheme.fullName === "None" || selectedTheme.fullName === "-1") {
this.settings.presetThemeName = ""; this.settings.presetThemeName = "";
this.settings.presetThemeContent = ""; this.settings.presetThemeContent = "";
return; return;

@ -39,7 +39,7 @@ export class DiscordComponent implements OnInit {
return; return;
} }
const settings = <IDiscordNotifcationSettings>form.value; const settings = <IDiscordNotifcationSettings> form.value;
settings.notificationTemplates = this.templates; settings.notificationTemplates = this.templates;
this.settingsService.saveDiscordNotificationSettings(settings).subscribe(x => { this.settingsService.saveDiscordNotificationSettings(settings).subscribe(x => {

@ -54,7 +54,7 @@ export class EmailNotificationComponent implements OnInit {
return; return;
} }
const settings = <IEmailNotificationSettings>form.value; const settings = <IEmailNotificationSettings> form.value;
settings.notificationTemplates = this.templates; settings.notificationTemplates = this.templates;
this.settingsService.saveEmailNotificationSettings(settings).subscribe(x => { this.settingsService.saveEmailNotificationSettings(settings).subscribe(x => {

@ -29,7 +29,7 @@ export class MattermostComponent implements OnInit {
username: [x.username], username: [x.username],
webhookUrl: [x.webhookUrl, [Validators.required]], webhookUrl: [x.webhookUrl, [Validators.required]],
channel: [x.channel], channel: [x.channel],
iconUrl:[x.iconUrl], iconUrl: [x.iconUrl],
}); });
}); });
@ -41,7 +41,7 @@ export class MattermostComponent implements OnInit {
return; return;
} }
const settings = <IMattermostNotifcationSettings>form.value; const settings = <IMattermostNotifcationSettings> form.value;
settings.notificationTemplates = this.templates; settings.notificationTemplates = this.templates;
this.settingsService.saveMattermostNotificationSettings(settings).subscribe(x => { this.settingsService.saveMattermostNotificationSettings(settings).subscribe(x => {

@ -32,9 +32,9 @@ export class MobileComponent implements OnInit {
}); });
this.mobileService.getUserDeviceList().subscribe(x => { this.mobileService.getUserDeviceList().subscribe(x => {
if(x.length <= 0) { if (x.length <= 0) {
this.userList = []; this.userList = [];
this.userList.push({username:"None",devices:0, userId:""}); this.userList.push({username: "None", devices: 0, userId: ""});
} else { } else {
this.userList = x; this.userList = x;
} }
@ -47,7 +47,7 @@ export class MobileComponent implements OnInit {
return; return;
} }
const settings = <IMobileNotifcationSettings>form.value; const settings = <IMobileNotifcationSettings> form.value;
settings.notificationTemplates = this.templates; settings.notificationTemplates = this.templates;
this.settingsService.saveMobileNotificationSettings(settings).subscribe(x => { this.settingsService.saveMobileNotificationSettings(settings).subscribe(x => {
@ -65,8 +65,8 @@ export class MobileComponent implements OnInit {
this.notificationService.error("Please check your entered values"); this.notificationService.error("Please check your entered values");
return; return;
} }
if(!this.testUserId) { if (!this.testUserId) {
this.notificationService.warning("Warning","Please select a user to send the test notification"); this.notificationService.warning("Warning", "Please select a user to send the test notification");
return; return;
} }

@ -49,12 +49,12 @@ export class NewsletterComponent implements OnInit {
}); });
} }
public addEmail() { public addEmail() {
if(this.emailToAdd) { if (this.emailToAdd) {
const emailRegex = "[a-zA-Z0-9.-_]{1,}@[a-zA-Z.-]{2,}[.]{1}[a-zA-Z]{2,}"; const emailRegex = "[a-zA-Z0-9.-_]{1,}@[a-zA-Z.-]{2,}[.]{1}[a-zA-Z]{2,}";
const match = this.emailToAdd.match(emailRegex)!; const match = this.emailToAdd.match(emailRegex)!;
if(match && match.length > 0) { if (match && match.length > 0) {
this.settings.externalEmails.push(this.emailToAdd); this.settings.externalEmails.push(this.emailToAdd);
this.emailToAdd = ""; this.emailToAdd = "";
} else { } else {

@ -37,7 +37,7 @@ export class PushbulletComponent implements OnInit {
return; return;
} }
const settings = <IPushbulletNotificationSettings>form.value; const settings = <IPushbulletNotificationSettings> form.value;
settings.notificationTemplates = this.templates; settings.notificationTemplates = this.templates;
this.settingsService.savePushbulletNotificationSettings(settings).subscribe(x => { this.settingsService.savePushbulletNotificationSettings(settings).subscribe(x => {

@ -37,7 +37,7 @@ export class PushoverComponent implements OnInit {
return; return;
} }
const settings = <IPushoverNotificationSettings>form.value; const settings = <IPushoverNotificationSettings> form.value;
settings.notificationTemplates = this.templates; settings.notificationTemplates = this.templates;
this.settingsService.savePushoverNotificationSettings(settings).subscribe(x => { this.settingsService.savePushoverNotificationSettings(settings).subscribe(x => {

@ -41,7 +41,7 @@ export class SlackComponent implements OnInit {
return; return;
} }
const settings = <ISlackNotificationSettings>form.value; const settings = <ISlackNotificationSettings> form.value;
if (settings.iconEmoji && settings.iconUrl) { if (settings.iconEmoji && settings.iconUrl) {
this.notificationService.error("You cannot have a Emoji icon and a URL icon"); this.notificationService.error("You cannot have a Emoji icon and a URL icon");
@ -65,7 +65,7 @@ export class SlackComponent implements OnInit {
return; return;
} }
const settings = <ISlackNotificationSettings>form.value; const settings = <ISlackNotificationSettings> form.value;
if (settings.iconEmoji && settings.iconUrl) { if (settings.iconEmoji && settings.iconUrl) {
this.notificationService.error("You cannot have a Emoji icon and a URL icon"); this.notificationService.error("You cannot have a Emoji icon and a URL icon");

@ -40,7 +40,7 @@ export class TelegramComponent implements OnInit {
return; return;
} }
const settings = <ITelegramNotifcationSettings>form.value; const settings = <ITelegramNotifcationSettings> form.value;
settings.notificationTemplates = this.templates; settings.notificationTemplates = this.templates;
this.settingsService.saveTelegramNotificationSettings(settings).subscribe(x => { this.settingsService.saveTelegramNotificationSettings(settings).subscribe(x => {

@ -41,9 +41,9 @@ export class OmbiComponent implements OnInit {
return; return;
} }
const result = <IOmbiSettings>form.value; const result = <IOmbiSettings> form.value;
if(result.baseUrl && result.baseUrl.length > 0) { if (result.baseUrl && result.baseUrl.length > 0) {
if(!result.baseUrl.startsWith("/")) { if (!result.baseUrl.startsWith("/")) {
this.notificationService.error("Please ensure your base url starts with a '/'"); this.notificationService.error("Please ensure your base url starts with a '/'");
return; return;
} }

@ -1,10 +1,8 @@
import { Component, OnDestroy, OnInit } from "@angular/core"; import { Component, OnDestroy, OnInit } from "@angular/core";
import "rxjs/add/operator/takeUntil"; import { Subject } from "rxjs";
import { Subject } from "rxjs/Subject"; import { takeUntil } from "rxjs/operators";
import { IPlexServerResponse, IPlexServerViewModel } from "../../interfaces";
import { IPlexLibrariesSettings, IPlexServer, IPlexSettings } from "../../interfaces";
import { IPlexLibrariesSettings, IPlexServer, IPlexServerResponse, IPlexServerViewModel, IPlexSettings } from "../../interfaces";
import { JobService, NotificationService, PlexService, SettingsService, TesterService } from "../../services"; import { JobService, NotificationService, PlexService, SettingsService, TesterService } from "../../services";
@Component({ @Component({
@ -21,11 +19,12 @@ export class PlexComponent implements OnInit, OnDestroy {
private subscriptions = new Subject<void>(); private subscriptions = new Subject<void>();
constructor(private settingsService: SettingsService, constructor(
private notificationService: NotificationService, private settingsService: SettingsService,
private plexService: PlexService, private notificationService: NotificationService,
private testerService: TesterService, private plexService: PlexService,
private jobService: JobService) { } private testerService: TesterService,
private jobService: JobService) { }
public ngOnInit() { public ngOnInit() {
this.settingsService.getPlex().subscribe(x => { this.settingsService.getPlex().subscribe(x => {
@ -34,17 +33,17 @@ export class PlexComponent implements OnInit, OnDestroy {
} }
public requestServers(server: IPlexServer) { public requestServers(server: IPlexServer) {
this.plexService.getServers(this.username, this.password) this.plexService.getServers(this.username, this.password).pipe(
.takeUntil(this.subscriptions) takeUntil(this.subscriptions),
.subscribe(x => { ).subscribe(x => {
if (x.success) { if (x.success) {
this.loadedServers = x; this.loadedServers = x;
this.serversButton = true; this.serversButton = true;
this.notificationService.success("Found the servers! Please select one!"); this.notificationService.success("Found the servers! Please select one!");
} else { } else {
this.notificationService.warning("Error When Requesting Plex Servers", "Please make sure your username and password are correct"); this.notificationService.warning("Error When Requesting Plex Servers", "Please make sure your username and password are correct");
} }
}); });
} }
public selectServer(selectedServer: IPlexServerResponse, server: IPlexServer) { public selectServer(selectedServer: IPlexServerResponse, server: IPlexServer) {
@ -72,7 +71,7 @@ export class PlexComponent implements OnInit, OnDestroy {
if (this.settings.servers == null) { if (this.settings.servers == null) {
this.settings.servers = []; this.settings.servers = [];
} }
this.settings.servers.push(<IPlexServer>{ name: "New*", id: Math.floor(Math.random() * (99999 - 0 + 1) + 1) }); this.settings.servers.push(<IPlexServer> { name: "New*", id: Math.floor(Math.random() * (99999 - 0 + 1) + 1) });
} }
@ -91,19 +90,19 @@ export class PlexComponent implements OnInit, OnDestroy {
this.plexService.getLibraries(server).subscribe(x => { this.plexService.getLibraries(server).subscribe(x => {
server.plexSelectedLibraries = []; server.plexSelectedLibraries = [];
if (x.successful) { if (x.successful) {
x.data.mediaContainer.directory.forEach((item) => { x.data.mediaContainer.directory.forEach((item) => {
const lib: IPlexLibrariesSettings = { const lib: IPlexLibrariesSettings = {
key: item.key, key: item.key,
title: item.title, title: item.title,
enabled: false, enabled: false,
}; };
server.plexSelectedLibraries.push(lib); server.plexSelectedLibraries.push(lib);
}); });
} else { } else {
this.notificationService.error(x.message); this.notificationService.error(x.message);
} }
}, },
err => { this.notificationService.error(err); }); err => { this.notificationService.error(err); });
} }
public save() { public save() {
@ -120,7 +119,7 @@ export class PlexComponent implements OnInit, OnDestroy {
public runCacher(): void { public runCacher(): void {
this.jobService.runPlexCacher().subscribe(x => { this.jobService.runPlexCacher().subscribe(x => {
if(x) { if (x) {
this.notificationService.success("Triggered the Plex Full Sync"); this.notificationService.success("Triggered the Plex Full Sync");
} }
}); });
@ -128,7 +127,7 @@ export class PlexComponent implements OnInit, OnDestroy {
public runRecentlyAddedCacher(): void { public runRecentlyAddedCacher(): void {
this.jobService.runPlexRecentlyAddedCacher().subscribe(x => { this.jobService.runPlexRecentlyAddedCacher().subscribe(x => {
if(x) { if (x) {
this.notificationService.success("Triggered the Plex Recently Added Sync"); this.notificationService.success("Triggered the Plex Recently Added Sync");
} }
}); });

@ -54,7 +54,7 @@ export class RadarrComponent implements OnInit {
this.qualities = []; this.qualities = [];
this.qualities.push({ name: "Please Select", id: -1 }); this.qualities.push({ name: "Please Select", id: -1 });
this.rootFolders = []; this.rootFolders = [];
this.rootFolders.push({ path: "Please Select", id: -1 }); this.rootFolders.push({ path: "Please Select", id: -1 });
this.minimumAvailabilityOptions = [ this.minimumAvailabilityOptions = [
@ -93,7 +93,7 @@ export class RadarrComponent implements OnInit {
this.notificationService.error("Please check your entered values"); this.notificationService.error("Please check your entered values");
return; return;
} }
const settings = <IRadarrSettings>form.value; const settings = <IRadarrSettings> form.value;
this.testerService.radarrTest(settings).subscribe(x => { this.testerService.radarrTest(settings).subscribe(x => {
if (x === true) { if (x === true) {
this.notificationService.success("Successfully connected to Radarr!"); this.notificationService.success("Successfully connected to Radarr!");
@ -108,12 +108,12 @@ public onSubmit(form: FormGroup) {
this.notificationService.error("Please check your entered values"); this.notificationService.error("Please check your entered values");
return; return;
} }
if(form.controls.defaultQualityProfile.value === "-1" || form.controls.defaultRootPath.value === "Please Select") { if (form.controls.defaultQualityProfile.value === "-1" || form.controls.defaultRootPath.value === "Please Select") {
this.notificationService.error("Please check your entered values"); this.notificationService.error("Please check your entered values");
return; return;
} }
const settings = <IRadarrSettings>form.value; const settings = <IRadarrSettings> form.value;
this.settingsService.saveRadarr(settings).subscribe(x => { this.settingsService.saveRadarr(settings).subscribe(x => {
if (x) { if (x) {
this.notificationService.success("Successfully saved Radarr settings"); this.notificationService.success("Successfully saved Radarr settings");

@ -1,14 +1,16 @@
import { CommonModule } from "@angular/common"; import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core"; import { NgModule } from "@angular/core";
import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { RouterModule, Routes } from "@angular/router"; import { RouterModule, Routes } from "@angular/router";
import { NgbAccordionModule, NgbModule } from "@ng-bootstrap/ng-bootstrap"; import { NgbAccordionModule, NgbModule } from "@ng-bootstrap/ng-bootstrap";
import { ClipboardModule } from "ngx-clipboard/dist"; import { ClipboardModule } from "ngx-clipboard";
import { AuthGuard } from "../auth/auth.guard"; import { AuthGuard } from "../auth/auth.guard";
import { AuthService } from "../auth/auth.service"; import { AuthService } from "../auth/auth.service";
import { CouchPotatoService, EmbyService, IssuesService, JobService, MobileService, NotificationMessageService, PlexService, RadarrService, import {
SonarrService, TesterService, ValidationService } from "../services"; CouchPotatoService, EmbyService, IssuesService, JobService, MobileService, NotificationMessageService, PlexService, RadarrService,
SonarrService, TesterService, ValidationService,
} from "../services";
import { PipeModule } from "../pipes/pipe.module"; import { PipeModule } from "../pipes/pipe.module";
import { AboutComponent } from "./about/about.component"; import { AboutComponent } from "./about/about.component";

@ -40,7 +40,7 @@ export class SickRageComponent implements OnInit {
this.notificationService.error("Please check your entered values"); this.notificationService.error("Please check your entered values");
return; return;
} }
const settings = <ISickRageSettings>form.value; const settings = <ISickRageSettings> form.value;
this.testerService.sickrageTest(settings).subscribe(x => { this.testerService.sickrageTest(settings).subscribe(x => {
if (x) { if (x) {
this.notificationService.success("Successfully connected to SickRage!"); this.notificationService.success("Successfully connected to SickRage!");

@ -93,7 +93,7 @@ export class SonarrComponent implements OnInit {
this.notificationService.error("Please check your entered values"); this.notificationService.error("Please check your entered values");
return; return;
} }
const settings = <ISonarrSettings>form.value; const settings = <ISonarrSettings> form.value;
this.testerService.sonarrTest(settings).subscribe(x => { this.testerService.sonarrTest(settings).subscribe(x => {
if (x) { if (x) {
this.notificationService.success("Successfully connected to Sonarr!"); this.notificationService.success("Successfully connected to Sonarr!");
@ -108,13 +108,13 @@ export class SonarrComponent implements OnInit {
this.notificationService.error("Please check your entered values"); this.notificationService.error("Please check your entered values");
return; return;
} }
if(form.controls.defaultQualityProfile) { if (form.controls.defaultQualityProfile) {
if(form.controls.defaultQualityProfile.value === "-1") { if (form.controls.defaultQualityProfile.value === "-1") {
this.notificationService.error("Please check your entered values"); this.notificationService.error("Please check your entered values");
} }
} }
if(form.controls.defaultRootPath) { if (form.controls.defaultRootPath) {
if(form.controls.defaultRootPath.value === "Please Select") { if (form.controls.defaultRootPath.value === "Please Select") {
this.notificationService.error("Please check your entered values"); this.notificationService.error("Please check your entered values");
} }
} }

@ -17,8 +17,8 @@ import { SidebarModule } from "primeng/primeng";
CommonModule, CommonModule,
], ],
exports: [ exports: [
TranslateModule, TranslateModule,
CommonModule, CommonModule,
FormsModule, FormsModule,
SidebarModule, SidebarModule,
IssuesReportComponent, IssuesReportComponent,

@ -19,7 +19,7 @@ export class UpdateDetailsComponent implements OnInit {
this.identityService.getUser().subscribe(x => { this.identityService.getUser().subscribe(x => {
const localUser = x as IUpdateLocalUser; const localUser = x as IUpdateLocalUser;
this.form = this.fb.group({ this.form = this.fb.group({
id:[localUser.id], id: [localUser.id],
username: [localUser.userName], username: [localUser.userName],
emailAddress: [localUser.emailAddress, [Validators.email]], emailAddress: [localUser.emailAddress, [Validators.email]],
confirmNewPassword: [localUser.confirmNewPassword], confirmNewPassword: [localUser.confirmNewPassword],

@ -13,7 +13,7 @@ import { NotificationService } from "../services";
export class UserManagementEditComponent { export class UserManagementEditComponent {
public user: IUser; public user: IUser;
public userId: string; public userId: string;
constructor(private identityService: IdentityService, constructor(private identityService: IdentityService,
private route: ActivatedRoute, private route: ActivatedRoute,
private notificationService: NotificationService, private notificationService: NotificationService,
@ -45,13 +45,13 @@ export class UserManagementEditComponent {
this.notificationService.error(val); this.notificationService.error(val);
}); });
} }
}); });
}, },
reject: () => { reject: () => {
return; return;
}, },
}); });
} }
public resetPassword() { public resetPassword() {

@ -1,6 +1,6 @@
import { Component, OnInit } from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { ICheckbox, ICustomizationSettings, IEmailNotificationSettings,IUser } from "../interfaces"; import { ICheckbox, ICustomizationSettings, IEmailNotificationSettings, IUser } from "../interfaces";
import { IdentityService, NotificationService, SettingsService } from "../services"; import { IdentityService, NotificationService, SettingsService } from "../services";
@Component({ @Component({
@ -10,7 +10,7 @@ export class UserManagementComponent implements OnInit {
public users: IUser[]; public users: IUser[];
public checkAll = false; public checkAll = false;
public emailSettings: IEmailNotificationSettings; public emailSettings: IEmailNotificationSettings;
public customizationSettings: ICustomizationSettings; public customizationSettings: ICustomizationSettings;
public order: string = "userName"; public order: string = "userName";
@ -21,9 +21,9 @@ export class UserManagementComponent implements OnInit {
public bulkMovieLimit?: number; public bulkMovieLimit?: number;
public bulkEpisodeLimit?: number; public bulkEpisodeLimit?: number;
constructor(private readonly identityService: IdentityService, constructor(private identityService: IdentityService,
private readonly settingsService: SettingsService, private settingsService: SettingsService,
private readonly notificationService: NotificationService) { } private notificationService: NotificationService) { }
public ngOnInit() { public ngOnInit() {
this.users = []; this.users = [];
@ -37,7 +37,7 @@ export class UserManagementComponent implements OnInit {
} }
public welcomeEmail(user: IUser) { public welcomeEmail(user: IUser) {
if(!user.emailAddress) { if (!user.emailAddress) {
this.notificationService.error("The user needs an email address."); this.notificationService.error("The user needs an email address.");
return; return;
} }
@ -45,13 +45,13 @@ export class UserManagementComponent implements OnInit {
this.notificationService.error("Email Notifications are not setup, cannot send welcome email"); this.notificationService.error("Email Notifications are not setup, cannot send welcome email");
return; return;
} }
if(!this.emailSettings.notificationTemplates.some(x => { if (!this.emailSettings.notificationTemplates.some(x => {
return x.enabled && x.notificationType === 8; return x.enabled && x.notificationType === 8;
})) { })) {
this.notificationService.error("The Welcome Email template is not enabled in the Email Setings"); this.notificationService.error("The Welcome Email template is not enabled in the Email Setings");
return; return;
} }
this.identityService.sendWelcomeEmail(user).subscribe(); this.identityService.sendWelcomeEmail(user).subscribe();
this.notificationService.success(`Sent a welcome email to ${user.emailAddress}`); this.notificationService.success(`Sent a welcome email to ${user.emailAddress}`);
} }
@ -74,31 +74,31 @@ export class UserManagementComponent implements OnInit {
}); });
this.users.forEach(x => { this.users.forEach(x => {
if(!x.checked) { if (!x.checked) {
return; return;
} }
if(anyRoles) { if (anyRoles) {
x.claims = this.availableClaims; x.claims = this.availableClaims;
} }
if(this.bulkEpisodeLimit) { if (this.bulkEpisodeLimit) {
x.episodeRequestLimit = this.bulkEpisodeLimit; x.episodeRequestLimit = this.bulkEpisodeLimit;
} }
if(this.bulkMovieLimit) { if (this.bulkMovieLimit) {
x.movieRequestLimit = this.bulkMovieLimit; x.movieRequestLimit = this.bulkMovieLimit;
} }
this.identityService.updateUser(x).subscribe(y => { this.identityService.updateUser(x).subscribe(y => {
if(!y.successful) { if (!y.successful) {
this.notificationService.error(`Could not update user ${x.userName}. Reason ${y.errors[0]}`); this.notificationService.error(`Could not update user ${x.userName}. Reason ${y.errors[0]}`);
} }
}); });
}); });
this.notificationService.success(`Updated users`); this.notificationService.success(`Updated users`);
this.showBulkEdit = false; this.showBulkEdit = false;
this.bulkMovieLimit = undefined; this.bulkMovieLimit = undefined;
this.bulkEpisodeLimit = undefined; this.bulkEpisodeLimit = undefined;
} }
public setOrder(value: string, el: any) { public setOrder(value: string, el: any) {
el = el.toElement || el.relatedTarget || el.target || el.srcElement; el = el.toElement || el.relatedTarget || el.target || el.srcElement;
@ -115,7 +115,7 @@ export class UserManagementComponent implements OnInit {
previousFilter.className = ""; previousFilter.className = "";
el.className = "active"; el.className = "active";
} }
this.order = value; this.order = value;
} }
} }

@ -17,10 +17,12 @@ export class CreateAdminComponent {
public createUser() { public createUser() {
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.result) {
this.router.navigate(["login"]); this.router.navigate(["login"]);
} else { } else {
this.notificationService.error("There was an error... You might want to put this on Github..."); if(x.errors.length > 0) {
this.notificationService.error(x.errors[0]);
}
} }
}); });
} }

@ -11,7 +11,7 @@ import { IEmbySettings } from "../../interfaces";
}) })
export class EmbyComponent implements OnInit { export class EmbyComponent implements OnInit {
private embySettings: IEmbySettings; public embySettings: IEmbySettings;
constructor(private embyService: EmbyService, constructor(private embyService: EmbyService,
private router: Router, private router: Router,
@ -21,7 +21,7 @@ export class EmbyComponent implements OnInit {
public ngOnInit() { public ngOnInit() {
this.embySettings = { this.embySettings = {
servers: [], servers: [],
id:0, id: 0,
enable: true, enable: true,
}; };
this.embySettings.servers.push({ this.embySettings.servers.push({

@ -29,16 +29,19 @@ export class PlexComponent implements OnInit {
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.identityService.createWizardUser({ this.identityService.createWizardUser({
username: "", username: "",
password: "", password: "",
usePlexAdminAccount: true, usePlexAdminAccount: true,
}).subscribe(y => { }).subscribe(y => {
if (y) { if (y.result) {
this.router.navigate(["login"]); this.router.navigate(["login"]);
} else { } else {
this.notificationService.error("Could not get the Plex Admin Information"); this.notificationService.error("Could not get the Plex Admin Information");
if(y.errors.length > 0) {
this.notificationService.error(y.errors[0]);
}
return; return;
} }
}); });
@ -47,9 +50,9 @@ export class PlexComponent implements OnInit {
} }
public oauth() { public oauth() {
this.plexTv.GetPin(this.clientId, "Ombi").subscribe(pin => { this.plexTv.GetPin(this.clientId, "Ombi").subscribe((pin: any) => {
this.plexService.oAuth({wizard: true, pin}).subscribe(x => { this.plexService.oAuth({ wizard: true, pin }).subscribe(x => {
if(x.url) { if (x.url) {
window.location.href = x.url; window.location.href = x.url;
} }
}); });

@ -1,7 +1,7 @@
import { Component, OnInit } from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute, Router } from "@angular/router";
import { IdentityService, PlexOAuthService, SettingsService } from "../../services"; import { IdentityService, PlexOAuthService } from "../../services";
import { AuthService } from "./../../auth/auth.service"; import { AuthService } from "./../../auth/auth.service";
@Component({ @Component({
@ -13,7 +13,6 @@ export class PlexOAuthComponent implements OnInit {
constructor(private route: ActivatedRoute, constructor(private route: ActivatedRoute,
private plexOauth: PlexOAuthService, private plexOauth: PlexOAuthService,
private identityService: IdentityService, private identityService: IdentityService,
private settings: SettingsService,
private router: Router, private router: Router,
private auth: AuthService) { private auth: AuthService) {
@ -22,46 +21,33 @@ export class PlexOAuthComponent implements OnInit {
this.pinId = params.pin; this.pinId = params.pin;
}); });
} }
public ngOnInit(): void { public ngOnInit(): void {
this.plexOauth.oAuth(this.pinId).subscribe(x => { this.plexOauth.oAuth(this.pinId).subscribe(x => {
if(!x.accessToken) { if (!x.accessToken) {
return; return;
// RETURN // RETURN
} }
this.identityService.createWizardUser({ this.identityService.createWizardUser({
username: "", username: "",
password: "", password: "",
usePlexAdminAccount: true, usePlexAdminAccount: true,
}).subscribe(u => { }).subscribe(u => {
if (u) { if (u.result) {
this.auth.oAuth(this.pinId).subscribe(c => { this.auth.oAuth(this.pinId).subscribe(c => {
localStorage.setItem("id_token", c.access_token); localStorage.setItem("id_token", c.access_token);
this.router.navigate(["login"]);
// 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 { } else {
//this.notificationService.error("Could not get the Plex Admin Information");
if(u.errors.length > 0) {
console.log(u.errors[0]);
}
return; return;
} }
}); });
}); });
} }
} }

@ -1,2 +0,0 @@
import "rxjs/add/operator/catch";
import "rxjs/add/operator/map";

@ -1,5 +1,6 @@
import * as Pace from "pace-progress"; // Main
import * as Pace from "pace-progress";
Pace.start(); Pace.start();
import "bootstrap/dist/js/bootstrap"; import "bootstrap/dist/js/bootstrap";
@ -11,8 +12,6 @@ import "./polyfills";
import "hammerjs"; import "hammerjs";
import "./imports";
import { enableProdMode } from "@angular/core"; import { enableProdMode } from "@angular/core";
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic"; import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
import { AppModule } from "./app/app.module"; import { AppModule } from "./app/app.module";
@ -29,7 +28,9 @@ if (module.hot) {
oldRootElem.parentNode.insertBefore(newRootElem, oldRootElem); oldRootElem.parentNode.insertBefore(newRootElem, oldRootElem);
oldRootElem.parentNode.removeChild(oldRootElem); oldRootElem.parentNode.removeChild(oldRootElem);
} }
modulePromise.then(appModule => appModule.destroy()); if (modulePromise) {
modulePromise.then(appModule => appModule.destroy());
}
}); });
} else { } else {
enableProdMode(); enableProdMode();

@ -9,6 +9,7 @@ declare var module: any;
if (module.hot) { if (module.hot) {
Error.stackTraceLimit = Infinity; Error.stackTraceLimit = Infinity;
// tslint:disable:no-var-requires
// tslint:disable-next-line
require("zone.js/dist/long-stack-trace-zone"); require("zone.js/dist/long-stack-trace-zone");
} }

@ -24,3 +24,7 @@ $bg-colour-disabled: #252424;
background: $primary-colour !important; background: $primary-colour !important;
color: white; color: white;
}*/ }*/
.label {
margin: 3px;
}

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

Loading…
Cancel
Save