This commit is contained in:
TidusJar 2018-08-02 12:47:42 +01:00
commit c4f22031d3
134 changed files with 9282 additions and 14930 deletions

View file

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

View file

@ -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")

View file

@ -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>

View file

@ -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; }
} }

View file

@ -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; }
} }

View file

@ -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);

View file

@ -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);
} }
} }

View file

@ -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();
} }
} }

View file

@ -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);

View file

@ -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; }
}
}

View file

@ -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.FirstOrDefaultAsync(x => x.Id == requestId);
var results = await allRequests.Where(x => x.Title.Contains(search, CompareOptions.IgnoreCase)).ToListAsync(); results.QualityOverride = profileId;
results.ForEach(async r => { await CheckForSubscription(shouldHide, r); });
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,28 +565,6 @@ 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)
{
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) private static List<ChildRequests> SortEpisodes(List<ChildRequests> items)
{ {
foreach (var value in items) foreach (var value in items)

View file

@ -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>();

View file

@ -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; }
}
}

View file

@ -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;
if (sonarrEpCount == ourRequestCount)
{
// We have the same amount of requests as all of the episodes in the season.
var existingSeason = var existingSeason =
result.seasons.FirstOrDefault(x => x.seasonNumber == season.SeasonNumber); result.seasons.FirstOrDefault(x => x.seasonNumber == season.SeasonNumber);
if (existingSeason == null) 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); Logger.LogError("There was no season numer {0} in Sonarr for title {1}", season.SeasonNumber, model.ParentRequest.Title);
continue; continue;
} }
if (sonarrEpCount == ourRequestCount)
{
// We have the same amount of requests as all of the episodes in the season.
if (!existingSeason.monitored)
{
existingSeason.monitored = true; existingSeason.monitored = true;
seriesChanges = 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;

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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; }

View file

@ -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
}
}
}

View file

@ -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");
}
}
}

View file

@ -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");

23
src/Ombi/.gitignore vendored
View file

@ -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

View file

@ -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": [

View file

@ -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 })),

View file

@ -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>

View file

@ -40,13 +40,13 @@ export class AppComponent implements OnInit {
__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() {

View file

@ -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),
@ -92,13 +100,7 @@ export function HttpLoaderFactory(http: HttpClient, platformLocation: PlatformLo
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({

View file

@ -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";

View file

@ -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;

View file

@ -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 {

View file

@ -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,

View file

@ -61,5 +61,4 @@ export class IssuesComponent implements OnInit {
this.resolvedIssues = x; this.resolvedIssues = x;
}); });
} }
} }

View file

@ -125,7 +125,7 @@ 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) {

View file

@ -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;
}); });
} }
@ -40,7 +39,6 @@ export class LoginOAuthComponent implements OnInit {
}, err => { }, err => {
this.notify.error(err.statusText); this.notify.error(err.statusText);
this.router.navigate(["login"]); this.router.navigate(["login"]);
}); });
} }

View file

@ -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";
@ -43,7 +43,7 @@ export class RecentlyAddedComponent implements OnInit {
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) {}

View file

@ -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>

View file

@ -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",
@ -45,16 +42,17 @@ export class MovieRequestsComponent implements OnInit {
private currentlyLoaded: number; private currentlyLoaded: number;
private amountToLoad: number; private amountToLoad: number;
constructor(private requestService: RequestService, constructor(
private requestService: RequestService,
private auth: AuthService, private auth: AuthService,
private notificationService: NotificationService, private notificationService: NotificationService,
private radarrService: RadarrService, private radarrService: RadarrService,
private sanitizer: DomSanitizer, private sanitizer: DomSanitizer,
private readonly platformLocation: PlatformLocation) { private readonly platformLocation: PlatformLocation) {
this.searchChanged this.searchChanged.pipe(
.debounceTime(600) // Wait Xms after the last event before emitting last event debounceTime(600), // Wait Xms after the last event before emitting last event
.distinctUntilChanged() // only emit if value is different from previous value distinctUntilChanged(), // only emit if value is different from previous value
.subscribe(x => { ).subscribe(x => {
this.searchText = x as string; this.searchText = x as string;
if (this.searchText === "") { if (this.searchText === "") {
this.resetSearch(); this.resetSearch();

View file

@ -4,59 +4,41 @@
</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 infinite-scroll
[infiniteScrollDistance]="1"
[infiniteScrollThrottle]="100"
(scrolled)="loadMore()">-->
<div> <div>
<p-treeTable [value]="tvRequests"> <div *ngFor="let node of tvRequests.collection">
<p-column>
<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--> <!--This is the section that holds the parent level results set-->
<div *ngIf="!node.leaf"> <div>
<div class="row"> <div class="row">
<div class="myBg backdrop" [style.background-image]="node?.data?.background"></div> <div class="myBg backdrop" [style.background-image]="node?.background"></div>
<div class="tint" style="background-image: linear-gradient(to bottom, rgba(0,0,0,0.6) 0%,rgba(0,0,0,0.6) 100%);"></div> <div class="tint" style="background-image: linear-gradient(to bottom, rgba(0,0,0,0.6) 0%,rgba(0,0,0,0.6) 100%);"></div>
<div class="col-sm-2 small-padding"> <div class="col-sm-2 small-padding">
<img class="img-responsive poster" src="{{node.data.posterPath || null}}" alt="poster"> <img class="img-responsive poster" src="{{node.posterPath || null}}" alt="poster">
</div> </div>
<div class="col-sm-5 small-padding"> <div class="col-sm-5 small-padding">
<div> <div>
<a href="http://www.imdb.com/title/{{node.data.imdbId}}/" target="_blank"> <a href="http://www.imdb.com/title/{{node.imdbId}}/" target="_blank">
<h4 class="request-title">{{node.data.title}} ({{node.data.releaseDate | date: 'yyyy'}})</h4> <h4 class="request-title">{{node.title}} ({{node.releaseDate | date: 'yyyy'}})</h4>
</a> </a>
</div> </div>
<br /> <br />
<div> <div>
<span>Status: </span> <span>Status: </span>
<span class="label label-success">{{node.data.status}}</span> <span class="label label-success">{{node.status}}</span>
</div> </div>
<div>Release Date: {{node.data.releaseDate | date}}</div> <div>Release Date: {{node.releaseDate | date}}</div>
<div *ngIf="isAdmin"> <div *ngIf="isAdmin">
<div *ngIf="node.data.qualityOverrideTitle" class="quality-override">{{ 'Requests.QualityOverride' | translate }} <div *ngIf="node.qualityOverrideTitle" class="quality-override">{{ 'Requests.QualityOverride' | translate }}
<span>{{node.data.qualityOverrideTitle}} </span> <span>{{node.qualityOverrideTitle}} </span>
</div> </div>
<div *ngIf="node.data.rootPathOverrideTitle" class="root-override">{{ 'Requests.RootFolderOverride' | translate }} <div *ngIf="node.rootPathOverrideTitle" class="root-override">{{ 'Requests.RootFolderOverride' | translate }}
<span>{{node.data.rootPathOverrideTitle}} </span> <span>{{node.rootPathOverrideTitle}} </span>
</div> </div>
</div> </div>
@ -64,7 +46,8 @@
</div> </div>
<div class="col-sm-3 col-sm-push-3 small-padding"> <div class="col-sm-3 col-sm-push-3 small-padding">
<button style="text-align: right" class="btn btn-sm btn-success-outline" (click)="openClosestTab($event)"><i class="fa fa-plus"></i> View</button> <button style="text-align: right" class="btn btn-sm btn-success-outline" (click)="openClosestTab(node,$event)">
<i class="fa fa-plus"></i> View</button>
<div *ngIf="isAdmin"> <div *ngIf="isAdmin">
<!--Sonarr Root Folder--> <!--Sonarr Root Folder-->
<div *ngIf="sonarrRootFolders" class="btn-group btn-split" id="rootFolderBtn"> <div *ngIf="sonarrRootFolders" class="btn-group btn-split" id="rootFolderBtn">
@ -77,7 +60,7 @@
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li *ngFor="let folder of sonarrRootFolders"> <li *ngFor="let folder of sonarrRootFolders">
<a href="#" (click)="selectRootFolder(node.data, folder, $event)">{{folder.path}}</a> <a href="#" (click)="selectRootFolder(node, folder, $event)">{{folder.path}}</a>
</li> </li>
</ul> </ul>
</div> </div>
@ -93,35 +76,39 @@
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li *ngFor="let profile of sonarrProfiles"> <li *ngFor="let profile of sonarrProfiles">
<a href="#" (click)="selectQualityProfile(node.data, profile, $event)">{{profile.name}}</a> <a href="#" (click)="selectQualityProfile(node, profile, $event)">{{profile.name}}</a>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
<div class="dropdown" *ngIf="issueCategories && issuesEnabled" id="issueBtn"> <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"> <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 }} <i class="fa fa-plus"></i> {{ 'Requests.ReportIssue' | 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 *ngFor="let cat of issueCategories"><a [routerLink]="" (click)="reportIssue(cat, node.data)">{{cat.value}}</a></li> <li *ngFor="let cat of issueCategories">
<a [routerLink]="" (click)="reportIssue(cat, node)">{{cat.value}}</a>
</li>
</ul> </ul>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!--This is the section that holds the child seasons if they want to specify specific episodes--> <!--This is the section that holds the child seasons if they want to specify specific episodes-->
<div *ngIf="node.leaf"> <div *ngIf="node.open">
<tvrequests-children [childRequests]="node.data" [isAdmin] ="isAdmin" <tvrequests-children [childRequests]="node.childRequests" [isAdmin]="isAdmin" (requestDeleted)="childRequestDeleted($event)"></tvrequests-children>
(requestDeleted)="childRequestDeleted($event)"></tvrequests-children> </div>
<br/>
<br/>
</div> </div>
</ng-template>
</p-column>
</p-treeTable>
<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>

View file

@ -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,27 +38,49 @@ export class TvRequestsComponent implements OnInit {
private currentlyLoaded: number; private currentlyLoaded: number;
private amountToLoad: number; private amountToLoad: number;
constructor(private requestService: RequestService, constructor(
private requestService: RequestService,
private auth: AuthService, private auth: AuthService,
private sanitizer: DomSanitizer, private sanitizer: DomSanitizer,
private imageService: ImageService, private imageService: ImageService,
private sonarrService: SonarrService, private sonarrService: SonarrService,
private notificationService: NotificationService, private notificationService: NotificationService,
private readonly platformLocation: PlatformLocation) { private readonly platformLocation: PlatformLocation) {
this.searchChanged
.debounceTime(600) // Wait Xms after the last event before emitting last event this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser");
.distinctUntilChanged() // only emit if value is different from previous value if (this.isAdmin) {
.subscribe(x => { this.sonarrService.getQualityProfilesWithoutSettings()
.subscribe(x => this.sonarrProfiles = x);
this.sonarrService.getRootFoldersWithoutSettings()
.subscribe(x => this.sonarrRootFolders = x);
}
}
public openClosestTab(node: ITvRequests,el: any) {
el.preventDefault();
node.open = !node.open;
}
public ngOnInit() {
this.amountToLoad = 10;
this.currentlyLoaded = 10;
this.tvRequests = {collection:[], total:0};
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; this.searchText = x as string;
if (this.searchText === "") { if (this.searchText === "") {
this.resetSearch(); this.resetSearch();
return; return;
} }
this.requestService.searchTvRequestsTree(this.searchText) this.requestService.searchTvRequests(this.searchText)
.subscribe(m => { .subscribe(m => {
this.tvRequests = m; this.tvRequests.collection = m;
this.tvRequests.forEach((val) => this.loadBackdrop(val)); this.tvRequests.collection.forEach((val) => this.loadBackdrop(val));
this.tvRequests.forEach((val) => this.setOverride(val.data)); this.tvRequests.collection.forEach((val) => this.setOverride(val));
}); });
}); });
this.defaultPoster = "../../../images/default_tv_poster.png"; this.defaultPoster = "../../../images/default_tv_poster.png";
@ -74,50 +88,6 @@ export class TvRequestsComponent implements OnInit {
if (base) { if (base) {
this.defaultPoster = "../../.." + base + "/images/default_tv_poster.png"; 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) {
// 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() {
this.amountToLoad = 10;
this.currentlyLoaded = 10;
this.tvRequests = [];
this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser");
this.loadInit(); this.loadInit();
} }
@ -125,7 +95,7 @@ export class TvRequestsComponent implements OnInit {
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;
@ -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,12 +142,23 @@ 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");
}
}); });
} }
@ -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,20 +201,20 @@ 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 + ")");
} }
}); });

View file

@ -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";
@ -32,15 +30,16 @@ export class MovieSearchComponent implements OnInit {
public issueCategorySelected: IIssueCategory; public issueCategorySelected: IIssueCategory;
public defaultPoster: string; public defaultPoster: string;
constructor(private searchService: SearchService, private requestService: RequestService, constructor(
private searchService: SearchService, private requestService: RequestService,
private notificationService: NotificationService, private authService: AuthService, private notificationService: NotificationService, private authService: AuthService,
private readonly translate: TranslateService, private sanitizer: DomSanitizer, private readonly translate: TranslateService, private sanitizer: DomSanitizer,
private readonly platformLocation: PlatformLocation) { private readonly platformLocation: PlatformLocation) {
this.searchChanged this.searchChanged.pipe(
.debounceTime(600) // Wait Xms after the last event before emitting last event debounceTime(600), // Wait Xms after the last event before emitting last event
.distinctUntilChanged() // only emit if value is different from previous value distinctUntilChanged(), // only emit if value is different from previous value
.subscribe(x => { ).subscribe(x => {
this.searchText = x as string; this.searchText = x as string;
if (this.searchText === "") { if (this.searchText === "") {
this.clearResults(); this.clearResults();

View file

@ -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",
@ -22,13 +19,14 @@ export class MovieSearchGridComponent implements OnInit {
public result: IRequestEngineResult; public result: IRequestEngineResult;
public searchApplied = false; public searchApplied = false;
constructor(private searchService: SearchService, private requestService: RequestService, constructor(
private searchService: SearchService, private requestService: RequestService,
private notificationService: NotificationService, private authService: AuthService) { private notificationService: NotificationService, private authService: AuthService) {
this.searchChanged this.searchChanged.pipe(
.debounceTime(600) // Wait Xms afterthe last event before emitting last event debounceTime(600), // Wait Xms afterthe last event before emitting last event
.distinctUntilChanged() // only emit if value is different from previous value distinctUntilChanged(), // only emit if value is different from previous value
.subscribe(x => { ).subscribe(x => {
this.searchText = x as string; this.searchText = x as string;
if (this.searchText === "") { if (this.searchText === "") {
this.clearResults(); this.clearResults();
@ -157,7 +155,6 @@ export class MovieSearchGridComponent implements OnInit {
this.movieResultGrid.push(container); this.movieResultGrid.push(container);
container = <ISearchMovieResultContainer> { movies: [] }; container = <ISearchMovieResultContainer> { movies: [] };
} else { } else {
container.movies.push(movie); container.movies.push(movie);
} }
}); });

View file

@ -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";

View file

@ -42,46 +42,46 @@
<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">
{{ 'Search.TvShows.Results' | translate }}
</ng-template>
<ng-template let-col let-node="rowData" pTemplate="body">
<!--This is the section that holds the parent level search results set--> <!--This is the section that holds the parent level search results set-->
<div *ngIf="!node.leaf"> <div *ngIf="node">
<div class="row"> <div class="row">
<div class="myBg backdrop" [style.background-image]="node?.data?.background"></div> <div class="myBg backdrop" [style.background-image]="node?.background"></div>
<div class="tint" style="background-image: linear-gradient(to bottom, rgba(0,0,0,0.6) 0%,rgba(0,0,0,0.6) 100%);"></div> <div class="tint" style="background-image: linear-gradient(to bottom, rgba(0,0,0,0.6) 0%,rgba(0,0,0,0.6) 100%);"></div>
<div class="col-sm-2 small-padding"> <div class="col-sm-2 small-padding">
<img *ngIf="node?.data?.banner" class="img-responsive poster" width="150" [src]="node.data.banner" alt="poster"> <img *ngIf="node.banner" class="img-responsive poster" width="150" [src]="node.banner" alt="poster">
</div> </div>
<div class="col-sm-8 small-padding"> <div class="col-sm-8 small-padding">
<div> <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> </a>
<span class="tags"> <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.homepage" id="homepageLabel" href="{{node.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> <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.data.status" class="label label-primary" id="statusLabel" target="_blank">{{node.data.status}}</span> <span *ngIf="node.status" class="label label-primary" id="statusLabel" target="_blank">{{node.status}}</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.firstAired" class="label label-info" target="_blank" id="airDateLabel">{{ 'Search.TvShows.AirDate' | translate }} {{node.firstAired | date: 'dd/MM/yyyy'}}</span>
<span *ngIf="node.data.network" class="label label-info" id="networkLabel" target="_blank">{{node.data.network}}</span> <span *ngIf="node.network" class="label label-info" id="networkLabel" target="_blank">{{node.network}}</span>
<span *ngIf="node.data.available" class="label label-success" id="availableLabel">{{ 'Common.Available' | translate }}</span> <span *ngIf="node.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 *ngIf="node.partlyAvailable" class="label label-warning" id="partiallyAvailableLabel">{{ 'Common.PartlyAvailable' | translate }}</span>
@ -90,47 +90,45 @@
<br /> <br />
<br /> <br />
</div> </div>
<p class="tv-overview">{{node.data.overview}}</p> <p class="tv-overview">{{node.overview}}</p>
</div> </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"> <div *ngIf="node.fullyAvailable">
<button style="text-align: right" class="btn btn-success-outline disabled" disabled> <button style="text-align: right" class="btn btn-success-outline disabled" disabled>
<i class="fa fa-check"></i> {{ 'Common.Available' | translate }} <i class="fa fa-check"></i> {{ 'Common.Available' | translate }}
</button> </button>
</div> </div>
<br /> <br />
<div *ngIf="node.data.plexUrl && node.data.available"> <div *ngIf="node.plexUrl && node.available">
<a style="text-align: right" class="btn btn-sm btn-success-outline" href="{{node.data.plexUrl}}" <a style="text-align: right" class="btn btn-sm btn-success-outline" href="{{node.plexUrl}}" target="_blank">
target="_blank">
<i class="fa fa-eye"></i> {{ 'Search.ViewOnPlex' | translate }} <i class="fa fa-eye"></i> {{ 'Search.ViewOnPlex' | translate }}
</a> </a>
</div> </div>
<div *ngIf="node.data.embyUrl && node.data.available"> <div *ngIf="node.embyUrl && node.available">
<a style="text-align: right" id="embybtn" class="btn btn-sm btn-success-outline" href="{{node.data.embyUrl}}" <a style="text-align: right" id="embybtn" class="btn btn-sm btn-success-outline" href="{{node.embyUrl}}" target="_blank">
target="_blank">
<i class="fa fa-eye"></i> {{ 'Search.ViewOnEmby' | translate }} <i class="fa fa-eye"></i> {{ 'Search.ViewOnEmby' | translate }}
</a> </a>
</div> </div>
@ -142,11 +140,11 @@
</button> </button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu1"> <ul class="dropdown-menu" aria-labelledby="dropdownMenu1">
<li *ngFor="let cat of issueCategories"> <li *ngFor="let cat of issueCategories">
<a [routerLink]="" (click)="reportIssue(cat, node.data)">{{cat.value}}</a> <a [routerLink]="" (click)="reportIssue(cat, node)">{{cat.value}}</a>
</li> </li>
</ul> </ul>
</div> </div>
<div *ngIf="!node.data.available"> <div *ngIf="!node.available">
<br /> <br />
<br /> <br />
</div> </div>
@ -156,15 +154,13 @@
</div> </div>
</div> </div>
<!--This is the section that holds the child seasons if they want to specify specific episodes--> <!--This is the section that holds the child seasons if they want to specify specific episodes-->
<div *ngIf="node.leaf"> <div *ngIf="node.open">
<seriesinformation [seriesId]="node.data.id"></seriesinformation> <seriesinformation [seriesId]="node.id"></seriesinformation>
</div> </div>
<br/> <br/>
<br/> <br/>
</ng-template> </div>
</p-column>
</p-treeTable>
</div> </div>
</div> </div>

View file

@ -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,21 +30,22 @@ 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 searchService: SearchService, private requestService: RequestService,
private notificationService: NotificationService, private authService: AuthService, private notificationService: NotificationService, private authService: AuthService,
private imageService: ImageService, private sanitizer: DomSanitizer, private imageService: ImageService, private sanitizer: DomSanitizer,
private readonly platformLocation: PlatformLocation) { private readonly platformLocation: PlatformLocation) {
this.searchChanged this.searchChanged.pipe(
.debounceTime(600) // Wait Xms after the last event before emitting last event debounceTime(600), // Wait Xms after the last event before emitting last event
.distinctUntilChanged() // only emit if value is different from previous value distinctUntilChanged(), // only emit if value is different from previous value
.subscribe(x => { ).subscribe(x => {
this.searchText = x as string; this.searchText = x as string;
if (this.searchText === "") { if (this.searchText === "") {
this.clearResults(); this.clearResults();
return; return;
} }
this.searchService.searchTvTreeNode(this.searchText) this.searchService.searchTv(this.searchText)
.subscribe(x => { .subscribe(x => {
this.tvResults = x; this.tvResults = x;
this.searchApplied = true; this.searchApplied = true;
@ -59,30 +58,9 @@ export class TvSearchComponent implements OnInit {
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() {
@ -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 {
@ -223,29 +201,29 @@ 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 + "/";
} }
} }

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -2,7 +2,7 @@
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";
@ -10,7 +10,6 @@ import { IPlexPin } from "../../interfaces";
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> {

View file

@ -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";

View file

@ -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";

View file

@ -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});
} }
@ -80,9 +81,11 @@ 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});
} }

View file

@ -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,8 +12,8 @@ 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> {

View file

@ -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";

View file

@ -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 { 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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";
@ -19,6 +19,7 @@ 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});
} }

View file

@ -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";
@ -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[]> {
@ -127,4 +126,10 @@ export class RequestService extends ServiceHelpers {
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});
}
} }

View file

@ -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});
} }
} }

View file

@ -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 {

View file

@ -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,

View file

@ -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";

View file

@ -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,7 +19,8 @@ export class PlexComponent implements OnInit, OnDestroy {
private subscriptions = new Subject<void>(); private subscriptions = new Subject<void>();
constructor(private settingsService: SettingsService, constructor(
private settingsService: SettingsService,
private notificationService: NotificationService, private notificationService: NotificationService,
private plexService: PlexService, private plexService: PlexService,
private testerService: TesterService, private testerService: TesterService,
@ -34,9 +33,9 @@ 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;

View file

@ -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";

View file

@ -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 = [];

View file

@ -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]);
}
} }
}); });
} }

View file

@ -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,

View file

@ -35,10 +35,13 @@ export class PlexComponent implements OnInit {
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,7 +50,7 @@ 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;

View file

@ -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) {
@ -35,33 +34,20 @@ export class PlexOAuthComponent implements OnInit {
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);
// 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"]); 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;
} }
}); });
}); });
} }
} }

View file

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

View file

@ -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);
} }
if (modulePromise) {
modulePromise.then(appModule => appModule.destroy()); modulePromise.then(appModule => appModule.destroy());
}
}); });
} else { } else {
enableProdMode(); enableProdMode();

View file

@ -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");
} }

View file

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

View file

@ -455,7 +455,7 @@ $border-radius: 10px;
} }
.checkbox input[type=checkbox] { .checkbox input[type=checkbox] {
display: none; opacity: 0;
} }
.checkbox input[type=checkbox]:checked + label:before { .checkbox input[type=checkbox]:checked + label:before {
@ -883,11 +883,6 @@ textarea {
display: none; display: none;
} }
.ui-treetable-toggler.fa.fa-fw.ui-clickable.fa-caret-right,
.ui-treetable-toggler.fa.fa-fw.ui-clickable.fa-caret-down {
display: none;
}
.ui-state-highlight { .ui-state-highlight {
background: $primary-colour; background: $primary-colour;
} }
@ -900,15 +895,6 @@ textarea {
border: 1px solid $form-color-lighter; border: 1px solid $form-color-lighter;
} }
.ui-treetable tfoot td, .ui-treetable th {
text-align: left;
}
.ui-treetable tbody td {
white-space: inherit;
overflow: visible;
}
table a:not(.btn) { table a:not(.btn) {
text-decoration: none; text-decoration: none;
} }

View file

@ -11,7 +11,7 @@ using Ombi.Settings.Settings.Models.External;
namespace Ombi.Controllers.External namespace Ombi.Controllers.External
{ {
[Admin] [PowerUser]
[ApiV1] [ApiV1]
[Produces("application/json")] [Produces("application/json")]
public class SonarrController : Controller public class SonarrController : Controller
@ -55,8 +55,12 @@ namespace Ombi.Controllers.External
public async Task<IEnumerable<SonarrProfile>> GetProfiles() public async Task<IEnumerable<SonarrProfile>> GetProfiles()
{ {
var settings = await SonarrSettings.GetSettingsAsync(); var settings = await SonarrSettings.GetSettingsAsync();
if (settings.Enabled)
{
return await SonarrApi.GetProfiles(settings.ApiKey, settings.FullUri); return await SonarrApi.GetProfiles(settings.ApiKey, settings.FullUri);
} }
return null;
}
/// <summary> /// <summary>
/// Gets the Sonarr root folders. /// Gets the Sonarr root folders.
@ -66,7 +70,12 @@ namespace Ombi.Controllers.External
public async Task<IEnumerable<SonarrRootFolder>> GetRootFolders() public async Task<IEnumerable<SonarrRootFolder>> GetRootFolders()
{ {
var settings = await SonarrSettings.GetSettingsAsync(); var settings = await SonarrSettings.GetSettingsAsync();
if (settings.Enabled)
{
return await SonarrApi.GetRootFolders(settings.ApiKey, settings.FullUri); return await SonarrApi.GetRootFolders(settings.ApiKey, settings.FullUri);
} }
return null;
}
} }
} }

View file

@ -59,7 +59,8 @@ namespace Ombi.Controllers
IRepository<Issues> issues, IRepository<Issues> issues,
IRepository<IssueComments> issueComments, IRepository<IssueComments> issueComments,
IRepository<NotificationUserId> notificationRepository, IRepository<NotificationUserId> notificationRepository,
IRepository<RequestSubscription> subscriptionRepository) IRepository<RequestSubscription> subscriptionRepository,
ISettingsService<UserManagementSettings> umSettings)
{ {
UserManager = user; UserManager = user;
Mapper = mapper; Mapper = mapper;
@ -79,6 +80,7 @@ namespace Ombi.Controllers
OmbiSettings = ombiSettings; OmbiSettings = ombiSettings;
_requestSubscriptionRepository = subscriptionRepository; _requestSubscriptionRepository = subscriptionRepository;
_notificationRepository = notificationRepository; _notificationRepository = notificationRepository;
_userManagementSettings = umSettings;
} }
private OmbiUserManager UserManager { get; } private OmbiUserManager UserManager { get; }
@ -87,6 +89,7 @@ namespace Ombi.Controllers
private IEmailProvider EmailProvider { get; } private IEmailProvider EmailProvider { get; }
private ISettingsService<EmailNotificationSettings> EmailSettings { get; } private ISettingsService<EmailNotificationSettings> EmailSettings { get; }
private ISettingsService<CustomizationSettings> CustomizationSettings { get; } private ISettingsService<CustomizationSettings> CustomizationSettings { get; }
private readonly ISettingsService<UserManagementSettings> _userManagementSettings;
private ISettingsService<OmbiSettings> OmbiSettings { get; } private ISettingsService<OmbiSettings> OmbiSettings { get; }
private IWelcomeEmail WelcomeEmail { get; } private IWelcomeEmail WelcomeEmail { get; }
private IMovieRequestRepository MovieRepo { get; } private IMovieRequestRepository MovieRepo { get; }
@ -113,13 +116,13 @@ namespace Ombi.Controllers
[HttpPost("Wizard")] [HttpPost("Wizard")]
[ApiExplorerSettings(IgnoreApi = true)] [ApiExplorerSettings(IgnoreApi = true)]
[AllowAnonymous] [AllowAnonymous]
public async Task<bool> CreateWizardUser([FromBody] CreateUserWizardModel user) public async Task<SaveWizardResult> CreateWizardUser([FromBody] CreateUserWizardModel user)
{ {
var users = UserManager.Users; var users = UserManager.Users;
if (users.Any(x => !x.UserName.Equals("api", StringComparison.CurrentCultureIgnoreCase))) if (users.Any(x => !x.UserName.Equals("api", StringComparison.InvariantCultureIgnoreCase)))
{ {
// No one should be calling this. Only the wizard // No one should be calling this. Only the wizard
return false; return new SaveWizardResult{ Result = false, Errors = new List<string> {"Looks like there is an existing user!"} };
} }
if (user.UsePlexAdminAccount) if (user.UsePlexAdminAccount)
@ -129,7 +132,7 @@ namespace Ombi.Controllers
if (authToken.IsNullOrEmpty()) if (authToken.IsNullOrEmpty())
{ {
_log.LogWarning("Could not find an auth token to create the plex user with"); _log.LogWarning("Could not find an auth token to create the plex user with");
return false; return new SaveWizardResult { Result = false };
} }
var plexUser = await _plexApi.GetAccount(authToken); var plexUser = await _plexApi.GetAccount(authToken);
var adminUser = new OmbiUser var adminUser = new OmbiUser
@ -140,6 +143,11 @@ namespace Ombi.Controllers
ProviderUserId = plexUser.user.id ProviderUserId = plexUser.user.id
}; };
await _userManagementSettings.SaveSettingsAsync(new UserManagementSettings
{
ImportPlexAdmin = true
});
return await SaveWizardUser(user, adminUser); return await SaveWizardUser(user, adminUser);
} }
@ -152,9 +160,10 @@ namespace Ombi.Controllers
return await SaveWizardUser(user, userToCreate); return await SaveWizardUser(user, userToCreate);
} }
private async Task<bool> SaveWizardUser(CreateUserWizardModel user, OmbiUser userToCreate) private async Task<SaveWizardResult> SaveWizardUser(CreateUserWizardModel user, OmbiUser userToCreate)
{ {
IdentityResult result; IdentityResult result;
var retVal = new SaveWizardResult();
// When creating the admin as the plex user, we do not pass in the password. // When creating the admin as the plex user, we do not pass in the password.
if (user.Password.HasValue()) if (user.Password.HasValue())
{ {
@ -182,6 +191,7 @@ namespace Ombi.Controllers
if (!result.Succeeded) if (!result.Succeeded)
{ {
LogErrors(result); LogErrors(result);
retVal.Errors.AddRange(result.Errors.Select(x => x.Description));
} }
// Update the wizard flag // Update the wizard flag
@ -189,7 +199,8 @@ namespace Ombi.Controllers
settings.Wizard = true; settings.Wizard = true;
await OmbiSettings.SaveSettingsAsync(settings); await OmbiSettings.SaveSettingsAsync(settings);
return result.Succeeded; retVal.Result = result.Succeeded;
return retVal;
} }
private void LogErrors(IdentityResult result) private void LogErrors(IdentityResult result)

View file

@ -240,6 +240,18 @@ namespace Ombi.Controllers
return await _issueComments.Add(newComment); return await _issueComments.Add(newComment);
} }
/// <summary>
/// Deletes a comment on a issue
/// </summary>
[HttpDelete("comments/{id:int}")]
[PowerUser]
public async Task<bool> DeleteComment(int commentId)
{
var comment = await _issueComments.GetAll().FirstOrDefaultAsync(x => x.Id == commentId);
await _issueComments.Delete(comment);
return true;
}
[HttpPost("status")] [HttpPost("status")]
public async Task<bool> UpdateStatus([FromBody] IssueStateViewModel model) public async Task<bool> UpdateStatus([FromBody] IssueStateViewModel model)

View file

@ -8,6 +8,7 @@ using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Ombi.Store.Entities.Requests; using Ombi.Store.Entities.Requests;
using System.Diagnostics; using System.Diagnostics;
using Ombi.Attributes;
using Ombi.Core.Models.UI; using Ombi.Core.Models.UI;
using Ombi.Models; using Ombi.Models;
using Ombi.Store.Entities; using Ombi.Store.Entities;
@ -93,6 +94,7 @@ namespace Ombi.Controllers
/// <param name="requestId">The request identifier.</param> /// <param name="requestId">The request identifier.</param>
/// <returns></returns> /// <returns></returns>
[HttpDelete("movie/{requestId:int}")] [HttpDelete("movie/{requestId:int}")]
[PowerUser]
public async Task DeleteRequest(int requestId) public async Task DeleteRequest(int requestId)
{ {
await MovieRequestEngine.RemoveMovieRequest(requestId); await MovieRequestEngine.RemoveMovieRequest(requestId);
@ -104,6 +106,7 @@ namespace Ombi.Controllers
/// <param name="model">The Movie's ID</param> /// <param name="model">The Movie's ID</param>
/// <returns></returns> /// <returns></returns>
[HttpPut("movie")] [HttpPut("movie")]
[PowerUser]
public async Task<MovieRequests> UpdateRequest([FromBody] MovieRequests model) public async Task<MovieRequests> UpdateRequest([FromBody] MovieRequests model)
{ {
return await MovieRequestEngine.UpdateMovieRequest(model); return await MovieRequestEngine.UpdateMovieRequest(model);
@ -115,6 +118,7 @@ namespace Ombi.Controllers
/// <param name="model">The Movie's ID</param> /// <param name="model">The Movie's ID</param>
/// <returns></returns> /// <returns></returns>
[HttpPost("movie/approve")] [HttpPost("movie/approve")]
[PowerUser]
public async Task<RequestEngineResult> ApproveMovie([FromBody] MovieUpdateModel model) public async Task<RequestEngineResult> ApproveMovie([FromBody] MovieUpdateModel model)
{ {
return await MovieRequestEngine.ApproveMovieById(model.Id); return await MovieRequestEngine.ApproveMovieById(model.Id);
@ -126,6 +130,7 @@ namespace Ombi.Controllers
/// <param name="model">The Movie's ID</param> /// <param name="model">The Movie's ID</param>
/// <returns></returns> /// <returns></returns>
[HttpPost("movie/available")] [HttpPost("movie/available")]
[PowerUser]
public async Task<RequestEngineResult> MarkMovieAvailable([FromBody] MovieUpdateModel model) public async Task<RequestEngineResult> MarkMovieAvailable([FromBody] MovieUpdateModel model)
{ {
return await MovieRequestEngine.MarkAvailable(model.Id); return await MovieRequestEngine.MarkAvailable(model.Id);
@ -137,6 +142,7 @@ namespace Ombi.Controllers
/// <param name="model">The Movie's ID</param> /// <param name="model">The Movie's ID</param>
/// <returns></returns> /// <returns></returns>
[HttpPost("movie/unavailable")] [HttpPost("movie/unavailable")]
[PowerUser]
public async Task<RequestEngineResult> MarkMovieUnAvailable([FromBody] MovieUpdateModel model) public async Task<RequestEngineResult> MarkMovieUnAvailable([FromBody] MovieUpdateModel model)
{ {
return await MovieRequestEngine.MarkUnavailable(model.Id); return await MovieRequestEngine.MarkUnavailable(model.Id);
@ -148,23 +154,12 @@ namespace Ombi.Controllers
/// <param name="model">The Movie's ID</param> /// <param name="model">The Movie's ID</param>
/// <returns></returns> /// <returns></returns>
[HttpPut("movie/deny")] [HttpPut("movie/deny")]
[PowerUser]
public async Task<RequestEngineResult> DenyMovie([FromBody] MovieUpdateModel model) public async Task<RequestEngineResult> DenyMovie([FromBody] MovieUpdateModel model)
{ {
return await MovieRequestEngine.DenyMovieById(model.Id); return await MovieRequestEngine.DenyMovieById(model.Id);
} }
/// <summary>
/// Gets the tv requests.
/// </summary>
/// <param name="count">The count of items you want to return.</param>
/// <param name="position">The position.</param>
/// <returns></returns>
[HttpGet("tv/{count:int}/{position:int}/tree")]
public async Task<IEnumerable<TreeNode<TvRequests, List<ChildRequests>>>> GetTvRequestsTree(int count, int position)
{
return await TvRequestEngine.GetRequestsTreeNode(count, position);
}
/// <summary> /// <summary>
/// Gets the total amount of TV requests. /// Gets the total amount of TV requests.
/// </summary> /// </summary>
@ -267,23 +262,13 @@ namespace Ombi.Controllers
return await TvRequestEngine.SearchTvRequest(searchTerm); return await TvRequestEngine.SearchTvRequest(searchTerm);
} }
/// <summary>
/// Searches for a specific tv request
/// </summary>
/// <param name="searchTerm">The search term.</param>
/// <returns></returns>
[HttpGet("tv/search/{searchTerm}/tree")]
public async Task<IEnumerable<TreeNode<TvRequests, List<ChildRequests>>>> SearchTvTree(string searchTerm)
{
return await TvRequestEngine.SearchTvRequestTree(searchTerm);
}
/// <summary> /// <summary>
/// Deletes the a specific tv request /// Deletes the a specific tv request
/// </summary> /// </summary>
/// <param name="requestId">The request identifier.</param> /// <param name="requestId">The request identifier.</param>
/// <returns></returns> /// <returns></returns>
[HttpDelete("tv/{requestId:int}")] [HttpDelete("tv/{requestId:int}")]
[PowerUser]
public async Task DeleteTvRequest(int requestId) public async Task DeleteTvRequest(int requestId)
{ {
await TvRequestEngine.RemoveTvRequest(requestId); await TvRequestEngine.RemoveTvRequest(requestId);
@ -295,17 +280,47 @@ namespace Ombi.Controllers
/// <param name="model">The model.</param> /// <param name="model">The model.</param>
/// <returns></returns> /// <returns></returns>
[HttpPut("tv")] [HttpPut("tv")]
[PowerUser]
public async Task<TvRequests> UpdateRequest([FromBody] TvRequests model) public async Task<TvRequests> UpdateRequest([FromBody] TvRequests model)
{ {
return await TvRequestEngine.UpdateTvRequest(model); return await TvRequestEngine.UpdateTvRequest(model);
} }
/// <summary>
/// Updates the root path for this tv show
/// </summary>
/// <param name="requestId"></param>
/// <param name="rootFolderId"></param>
/// <returns></returns>
[HttpPut("tv/root/{requestId:int}/{rootFolderId:int}")]
[PowerUser]
public async Task<bool> UpdateRootFolder(int requestId, int rootFolderId)
{
await TvRequestEngine.UpdateRootPath(requestId, rootFolderId);
return true;
}
/// <summary>
/// Updates the quality profile for this tv show
/// </summary>
/// <param name="requestId"></param>
/// <param name="qualityId"></param>
/// <returns></returns>
[HttpPut("tv/quality/{requestId:int}/{qualityId:int}")]
[PowerUser]
public async Task<bool> UpdateQuality(int requestId, int qualityId)
{
await TvRequestEngine.UpdateQualityProfile(requestId, qualityId);
return true;
}
/// <summary> /// <summary>
/// Updates the a specific child request /// Updates the a specific child request
/// </summary> /// </summary>
/// <param name="child">The model.</param> /// <param name="child">The model.</param>
/// <returns></returns> /// <returns></returns>
[HttpPut("tv/child")] [HttpPut("tv/child")]
[PowerUser]
public async Task<ChildRequests> UpdateChild([FromBody] ChildRequests child) public async Task<ChildRequests> UpdateChild([FromBody] ChildRequests child)
{ {
return await TvRequestEngine.UpdateChildRequest(child); return await TvRequestEngine.UpdateChildRequest(child);
@ -317,6 +332,7 @@ namespace Ombi.Controllers
/// <param name="model">This is the child request's ID</param> /// <param name="model">This is the child request's ID</param>
/// <returns></returns> /// <returns></returns>
[HttpPut("tv/deny")] [HttpPut("tv/deny")]
[PowerUser]
public async Task<RequestEngineResult> DenyChild([FromBody] TvUpdateModel model) public async Task<RequestEngineResult> DenyChild([FromBody] TvUpdateModel model)
{ {
return await TvRequestEngine.DenyChildRequest(model.Id); return await TvRequestEngine.DenyChildRequest(model.Id);
@ -328,6 +344,7 @@ namespace Ombi.Controllers
/// <param name="model">The Movie's ID</param> /// <param name="model">The Movie's ID</param>
/// <returns></returns> /// <returns></returns>
[HttpPost("tv/available")] [HttpPost("tv/available")]
[PowerUser]
public async Task<RequestEngineResult> MarkTvAvailable([FromBody] TvUpdateModel model) public async Task<RequestEngineResult> MarkTvAvailable([FromBody] TvUpdateModel model)
{ {
return await TvRequestEngine.MarkAvailable(model.Id); return await TvRequestEngine.MarkAvailable(model.Id);
@ -339,6 +356,7 @@ namespace Ombi.Controllers
/// <param name="model">The Movie's ID</param> /// <param name="model">The Movie's ID</param>
/// <returns></returns> /// <returns></returns>
[HttpPost("tv/unavailable")] [HttpPost("tv/unavailable")]
[PowerUser]
public async Task<RequestEngineResult> MarkTvUnAvailable([FromBody] TvUpdateModel model) public async Task<RequestEngineResult> MarkTvUnAvailable([FromBody] TvUpdateModel model)
{ {
return await TvRequestEngine.MarkUnavailable(model.Id); return await TvRequestEngine.MarkUnavailable(model.Id);
@ -350,6 +368,7 @@ namespace Ombi.Controllers
/// <param name="model">This is the child request's ID</param> /// <param name="model">This is the child request's ID</param>
/// <returns></returns> /// <returns></returns>
[HttpPost("tv/approve")] [HttpPost("tv/approve")]
[PowerUser]
public async Task<RequestEngineResult> ApproveChild([FromBody] TvUpdateModel model) public async Task<RequestEngineResult> ApproveChild([FromBody] TvUpdateModel model)
{ {
return await TvRequestEngine.ApproveChildRequest(model.Id); return await TvRequestEngine.ApproveChildRequest(model.Id);
@ -360,6 +379,7 @@ namespace Ombi.Controllers
/// </summary> /// </summary>
/// <param name="requestId">The model.</param> /// <param name="requestId">The model.</param>
/// <returns></returns> /// <returns></returns>
[PowerUser]
[HttpDelete("tv/child/{requestId:int}")] [HttpDelete("tv/child/{requestId:int}")]
public async Task<bool> DeleteChildRequest(int requestId) public async Task<bool> DeleteChildRequest(int requestId)
{ {

View file

@ -1,5 +1,4 @@
using System; using System.Collections.Generic;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -127,31 +126,6 @@ namespace Ombi.Controllers
return await TvEngine.Search(searchTerm); return await TvEngine.Search(searchTerm);
} }
/// <summary>
/// Searches for a Tv Show.
/// </summary>
/// <param name="searchTerm">The search term.</param>
/// <remarks>We use TvMaze as the Provider</remarks>
/// <returns></returns>
[HttpGet("tv/{searchTerm}/tree")]
public async Task<IEnumerable<TreeNode<SearchTvShowViewModel>>> SearchTvTreeNode(string searchTerm)
{
return await TvEngine.SearchTreeNode(searchTerm);
}
/// <summary>
/// Gets extra show information.
/// </summary>
/// <param name="tvdbId">The TVDB identifier.</param>
/// <remarks>We use TvMaze as the Provider</remarks>
/// <returns></returns>
[HttpGet("tv/info/{tvdbId}/tree")]
public async Task<TreeNode<SearchTvShowViewModel>> GetShowInfoTreeNode(int tvdbId)
{
if (tvdbId == 0) return new TreeNode<SearchTvShowViewModel>();
return await TvEngine.GetShowInformationTreeNode(tvdbId);
}
/// <summary> /// <summary>
/// Gets extra show information. /// Gets extra show information.
/// </summary> /// </summary>
@ -164,17 +138,6 @@ namespace Ombi.Controllers
return await TvEngine.GetShowInformation(tvdbId); return await TvEngine.GetShowInformation(tvdbId);
} }
/// <summary>
/// Returns Popular Tv Shows
/// </summary>
/// <remarks>We use Trakt.tv as the Provider</remarks>
/// <returns></returns>
[HttpGet("tv/popular/tree")]
public async Task<IEnumerable<TreeNode<SearchTvShowViewModel>>> PopularTvTree()
{
return await TvEngine.PopularTree();
}
/// <summary> /// <summary>
/// Returns Popular Tv Shows /// Returns Popular Tv Shows
/// </summary> /// </summary>
@ -186,18 +149,6 @@ namespace Ombi.Controllers
return await TvEngine.Popular(); return await TvEngine.Popular();
} }
/// <summary>
/// Returns most Anticiplateds tv shows.
/// </summary>
/// <remarks>We use Trakt.tv as the Provider</remarks>
/// <returns></returns>
[HttpGet("tv/anticipated/tree")]
public async Task<IEnumerable<TreeNode<SearchTvShowViewModel>>> AnticipatedTvTree()
{
return await TvEngine.AnticipatedTree();
}
/// <summary> /// <summary>
/// Returns most Anticiplateds tv shows. /// Returns most Anticiplateds tv shows.
/// </summary> /// </summary>
@ -209,16 +160,6 @@ namespace Ombi.Controllers
return await TvEngine.Anticipated(); return await TvEngine.Anticipated();
} }
/// <summary>
/// Returns Most watched shows.
/// </summary>
/// <remarks>We use Trakt.tv as the Provider</remarks>
/// <returns></returns>
[HttpGet("tv/mostwatched/tree")]
public async Task<IEnumerable<TreeNode<SearchTvShowViewModel>>> MostWatchedTree()
{
return await TvEngine.MostWatchesTree();
}
/// <summary> /// <summary>
/// Returns Most watched shows. /// Returns Most watched shows.
@ -231,17 +172,6 @@ namespace Ombi.Controllers
return await TvEngine.MostWatches(); return await TvEngine.MostWatches();
} }
/// <summary>
/// Returns trending shows
/// </summary>
/// <remarks>We use Trakt.tv as the Provider</remarks>
/// <returns></returns>
[HttpGet("tv/trending/tree")]
public async Task<IEnumerable<TreeNode<SearchTvShowViewModel>>> TrendingTree()
{
return await TvEngine.TrendingTree();
}
/// <summary> /// <summary>
/// Returns trending shows /// Returns trending shows
/// </summary> /// </summary>

View file

@ -0,0 +1,10 @@
using System.Collections.Generic;
namespace Ombi.Models.Identity
{
public class SaveWizardResult
{
public bool Result { get; set; }
public List<string> Errors { get; set; } = new List<string>();
}
}

View file

@ -1,5 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project ToolsVersion="15.0" Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework> <TargetFramework>netcoreapp2.1</TargetFramework>
<RuntimeIdentifiers>win10-x64;win10-x86;osx-x64;ubuntu-x64;debian.8-x64;centos.7-x64;linux-x64;linux-arm;linux-arm64;</RuntimeIdentifiers> <RuntimeIdentifiers>win10-x64;win10-x86;osx-x64;ubuntu-x64;debian.8-x64;centos.7-x64;linux-x64;linux-arm;linux-arm64;</RuntimeIdentifiers>
@ -9,6 +8,7 @@
<FileVersion>$(SemVer)</FileVersion> <FileVersion>$(SemVer)</FileVersion>
<Version>$(FullVer)</Version> <Version>$(FullVer)</Version>
<PackageVersion></PackageVersion> <PackageVersion></PackageVersion>
<TypeScriptToolsVersion>2.8</TypeScriptToolsVersion>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<ServerGarbageCollection>false</ServerGarbageCollection> <ServerGarbageCollection>false</ServerGarbageCollection>
@ -24,10 +24,6 @@
<DefineConstants>TRACE;RELEASE;NETCOREAPP2_0</DefineConstants> <DefineConstants>TRACE;RELEASE;NETCOREAPP2_0</DefineConstants>
</PropertyGroup> </PropertyGroup>
<Target Name="NpmCommandsDebug" Condition="'$(Configuration)'=='Debug'" AfterTargets="Build">
<Exec Command="npm run-script vendor" />
</Target>
<ItemGroup> <ItemGroup>
<!-- Files not to show in IDE --> <!-- Files not to show in IDE -->
<Compile Remove="Logs\**" /> <Compile Remove="Logs\**" />
@ -46,7 +42,17 @@
<None Remove="Styles\**" /> <None Remove="Styles\**" />
<None Remove="wwwroot\dist\**" /> <None Remove="wwwroot\dist\**" />
</ItemGroup> </ItemGroup>
<Target Name="NpmCommandsDebug" Condition="'$(Configuration)'=='Debug'" AfterTargets="Build">
<Exec Command="npm run vendor" />
</Target>
<!-- Disabled as run by CI once for many different runtime builds
<Target Name="NpmCommandsRelease" Condition="'$(Configuration)'=='Release'" AfterTargets="Build">
<Exec Command="npm run publish" />
</Target>
-->
<Target Name="NpmCommandsClean" AfterTargets="Clean">
<Exec Command="npm run clean" />
</Target>
<Target Name="RunWebpack" AfterTargets="ComputeFilesToPublish"> <Target Name="RunWebpack" AfterTargets="ComputeFilesToPublish">
<ItemGroup> <ItemGroup>
<DistFiles Include="wwwroot\dist\**" /> <DistFiles Include="wwwroot\dist\**" />
@ -56,8 +62,6 @@
</ResolvedFileToPublish> </ResolvedFileToPublish>
</ItemGroup> </ItemGroup>
</Target> </Target>
<ItemGroup> <ItemGroup>
<PackageReference Include="AutoMapper" Version="6.1.1" /> <PackageReference Include="AutoMapper" Version="6.1.1" />
<PackageReference Include="CommandLineParser" Version="2.1.1-beta" /> <PackageReference Include="CommandLineParser" Version="2.1.1-beta" />
@ -94,18 +98,4 @@
<ProjectReference Include="..\Ombi.TheMovieDbApi\Ombi.Api.TheMovieDb.csproj" /> <ProjectReference Include="..\Ombi.TheMovieDbApi\Ombi.Api.TheMovieDb.csproj" />
<ProjectReference Include="..\Ombi.Updater\Ombi.Updater.csproj" /> <ProjectReference Include="..\Ombi.Updater\Ombi.Updater.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Include="wwwroot\loading.css" />
<None Include="wwwroot\translations\*.json" />
</ItemGroup>
<ItemGroup>
<None Remove="ClientApp\app\animations\fadeinout.ts" />
</ItemGroup>
<ItemGroup>
<TypeScriptCompile Include="ClientApp\app\animations\fadeinout.ts" />
</ItemGroup>
</Project> </Project>

View file

@ -156,7 +156,12 @@ namespace Ombi
app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions
{ {
HotModuleReplacement = true, HotModuleReplacement = true,
ConfigFile = "webpack.dev.js" ConfigFile = "webpack.config.ts",
//EnvParam = new
//{
// aot = true // can't use AOT with HMR currently https://github.com/angular/angular-cli/issues/6347
//}
}); });
} }

View file

@ -98,8 +98,10 @@
<link rel="stylesheet" href="~/loading.css" asp-append-version="true" /> <link rel="stylesheet" href="~/loading.css" asp-append-version="true" />
<link rel="stylesheet" href="~/dist/vendor.css" asp-append-version="true" /> <link rel="stylesheet" href="~/dist/vendor.css" asp-append-version="true" />
<environment include="Development">
<script src="~/dist/vendor.js" asp-append-version="true" defer></script> <script src="~/dist/vendor.js" asp-append-version="true" defer></script>
<script src="~/dist/main.js" asp-append-version="true" defer></script> </environment>
<script src="~/dist/app.js" asp-append-version="true" defer></script>
</head> </head>
<body> <body>
@{ @{

View file

@ -1,21 +1,25 @@
'use strict'; "use strict";
const gulp = require('gulp'); const gulp = require("gulp");
const run = require('gulp-run'); const run = require("gulp-run");
const runSequence = require('run-sequence'); const runSequence = require("run-sequence");
const del = require('del'); const del = require("del");
const path = require('path'); const path = require("path");
const fs = require('fs'); const fs = require("fs");
const outputDir = './wwwroot/dist'; const outputDir = "./wwwroot/dist";
global.aot = true;
function getEnvOptions() { function getEnvOptions() {
var options = []; const options = [];
if (global.prod) { if (global.prod) {
options.push('--env.prod'); options.push("--env.prod");
} }
if (global.analyse) { if (global.analyse) {
options.push('--env.analyse'); options.push("--env.analyse");
}
if (global.aot) {
options.push("--env.aot");
} }
if (options.length > 0) { if (options.length > 0) {
return " " + options.join(" "); return " " + options.join(" ");
@ -24,12 +28,12 @@ function getEnvOptions() {
} }
} }
function webpack(type) {
function webpack(vendor) { // 'webpack' instead of direct path can cause https://github.com/angular/angular-cli/issues/6417
return run(`webpack --config webpack.config${vendor ? '.vendor' : ''}.ts${getEnvOptions()}`).exec(); return run(`node node_modules\\webpack\\bin\\webpack.js --config webpack.config${type ? `.${type}` : ""}.ts${getEnvOptions()}`).exec();
} }
gulp.task('vendor', () => { gulp.task("vendor", () => {
let build = false; let build = false;
const vendorPath = path.join(outputDir, "vendor.js"); const vendorPath = path.join(outputDir, "vendor.js");
const vendorExists = fs.existsSync(vendorPath); const vendorExists = fs.existsSync(vendorPath);
@ -37,44 +41,47 @@ gulp.task('vendor', () => {
const vendorStat = fs.statSync(vendorPath); const vendorStat = fs.statSync(vendorPath);
const packageStat = fs.statSync("package.json"); const packageStat = fs.statSync("package.json");
const vendorConfigStat = fs.statSync("webpack.config.vendor.ts"); const vendorConfigStat = fs.statSync("webpack.config.vendor.ts");
const commonConfigStat = fs.statSync("webpack.config.common.ts");
if (packageStat.mtime > vendorStat.mtime) { if (packageStat.mtime > vendorStat.mtime) {
build = true; build = true;
} }
if (vendorConfigStat.mtime > vendorStat.mtime) { if (vendorConfigStat.mtime > vendorStat.mtime) {
build = true; build = true;
} }
if (commonConfigStat.mtime > vendorStat.mtime) {
build = true;
}
} else { } else {
build = true; build = true;
} }
if (build) { if (build) {
return webpack(true); return webpack("vendor");
} }
}); });
gulp.task("vendor_force", () => {
gulp.task('vendor_force', () => { return webpack("vendor");
return webpack(true);
}) })
gulp.task('main', () => { gulp.task("main", () => {
return webpack() return webpack()
}); });
gulp.task('prod_var', () => { gulp.task("prod_var", () => {
global.prod = true; global.prod = true;
}) })
gulp.task('analyse_var', () => { gulp.task("analyse_var", () => {
global.analyse = true; global.analyse = true;
}) })
gulp.task('clean', () => { gulp.task("clean", () => {
del.sync(outputDir, { force: true }); del.sync(outputDir, { force: true });
}); });
gulp.task("lint", () => run("npm run lint").exec());
gulp.task('lint', () => run("npm run lint").exec()); gulp.task("lint_fix", () => run("npm run lint -- --fix").exec());
gulp.task('build', callback => runSequence('vendor', 'main', callback)); gulp.task("build", callback => runSequence("vendor", "main", callback));
gulp.task('analyse', callback => runSequence('analyse_var', 'build')); gulp.task("analyse", callback => runSequence("analyse_var", "clean", "build", callback));
gulp.task('full', callback => runSequence('clean', 'build')); gulp.task("full", callback => runSequence("clean", "build", callback));
gulp.task('publish', callback => runSequence('prod_var', 'build')); gulp.task("publish", callback => runSequence("prod_var", "full", callback));

13399
src/Ombi/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,94 +1,93 @@
{ {
"name": "ombi", "name": "ombi",
"version": "2.6.0",
"private": true, "private": true,
"version": "1.0.0",
"scripts": { "scripts": {
"vendor": "gulp vendor", "vendor": "gulp vendor",
"main": "gulp main", "lint": "tslint -p .",
"publish": "gulp publish", "publish": "gulp publish",
"lint": "tslint ClientApp/**/*.ts", "restore": "dotnet restore && yarn install",
"restore": "dotnet restore && npm install" "clean": "gulp clean"
}, },
"dependencies": { "dependencies": {
"@angular/animations": "^5.2.5", "@angular/animations": "^6.0.7",
"@angular/cdk": "^5.2.1", "@angular/cdk": "^6.3.1",
"@angular/common": "^5.2.5", "@angular/common": "^6.0.7",
"@angular/compiler": "^5.2.5", "@angular/compiler": "^6.0.7",
"@angular/core": "^5.2.5", "@angular/compiler-cli": "^6.0.7",
"@angular/forms": "^5.2.5", "@angular/core": "^6.0.7",
"@angular/http": "^5.2.5", "@angular/forms": "^6.0.7",
"@angular/material": "^5.2.1", "@angular/http": "^6.0.7",
"@angular/platform-browser": "^5.2.5", "@angular/material": "^6.3.1",
"@angular/platform-browser-dynamic": "^5.2.5", "@angular/platform-browser": "^6.0.7",
"@angular/platform-server": "^5.2.5", "@angular/platform-browser-dynamic": "^6.0.7",
"@angular/router": "^5.2.5", "@angular/platform-server": "^6.0.7",
"@auth0/angular-jwt": "1.0.0-beta.9", "@angular/router": "^6.0.7",
"@ng-bootstrap/ng-bootstrap": "^1.0.0", "@auth0/angular-jwt": "^2.0.0",
"@ngu/carousel": "^1.4.8", "@ng-bootstrap/ng-bootstrap": "^2.2.0",
"@ngx-translate/core": "^8.0.0", "@ngtools/webpack": "^6.1.0-beta.2",
"@ngx-translate/http-loader": "^2.0.1", "@ngu/carousel": "^1.4.9-beta-2",
"@types/core-js": "^0.9.46", "@ngx-translate/core": "^10.0.2",
"@types/extract-text-webpack-plugin": "^3.0.1", "@ngx-translate/http-loader": "^3.0.1",
"@types/intro.js": "^2.4.3", "@types/core-js": "^2.5.0",
"@types/node": "^8.9.4", "@types/mini-css-extract-plugin": "^0.2.0",
"@types/webpack": "^3.8.7", "@types/node": "^10.5.1",
"angular-router-loader": "^0.8.2", "@types/webpack": "^4.4.4",
"angular2-moment": "^1.8.0", "@types/webpack-bundle-analyzer": "^2.9.2",
"@types/webpack-merge": "^4.1.3",
"angular-router-loader": "^0.8.5",
"angular2-template-loader": "^0.6.2", "angular2-template-loader": "^0.6.2",
"aspnet-webpack": "^2.0.3", "aspnet-webpack": "^3.0.0",
"awesome-typescript-loader": "^3.4.1", "awesome-typescript-loader": "^5.2.0",
"bootstrap": "3.3.7", "bootstrap": "3.3.7",
"bootswatch": "3.3.7", "bootswatch": "3.3.7",
"core-js": "^2.5.3", "copy-webpack-plugin": "^4.5.2",
"css": "^2.2.1", "core-js": "^2.5.7",
"css-loader": "^0.28.9", "css": "^2.2.3",
"css-loader": "^0.28.11",
"del": "^3.0.0", "del": "^3.0.0",
"event-source-polyfill": "^0.0.11", "event-source-polyfill": "^0.0.12",
"expose-loader": "^0.7.4", "expose-loader": "^0.7.5",
"extract-text-webpack-plugin": "^3.0.2", "file-loader": "^1.1.11",
"file-loader": "^1.1.6",
"font-awesome": "^4.7.0", "font-awesome": "^4.7.0",
"gulp": "^3.9.1", "gulp": "^3.9.1",
"gulp-run": "^1.7.1", "gulp-run": "^1.7.1",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
"html-loader": "0.5.1", "html-loader": "^0.5.5",
"jquery": "^3.3.1", "jquery": "^3.3.1",
"mini-css-extract-plugin": "^0.4.1",
"moment": "^2.22.2",
"ng2-cookies": "^1.0.12", "ng2-cookies": "^1.0.12",
"ngx-clipboard": "8.1.1", "ngx-clipboard": "^11.1.1",
"ngx-infinite-scroll": "^0.6.1", "ngx-infinite-scroll": "^6.0.1",
"ngx-moment": "^3.0.1",
"ngx-order-pipe": "^2.0.1", "ngx-order-pipe": "^2.0.1",
"node-sass": "^4.7.2", "node-sass": "^4.9.0",
"npm": "^5.6.0",
"pace-progress": "^1.0.2", "pace-progress": "^1.0.2",
"primeng": "5.0.2", "primeng": "^6.0.0",
"reflect-metadata": "0.1.10", "raw-loader": "^0.5.1",
"reflect-metadata": "^0.1.12",
"run-sequence": "^2.2.1", "run-sequence": "^2.2.1",
"rxjs": "5.5.2", "rxjs": "^6.2.1",
"sass-loader": "^6.0.6", "sass-loader": "^7.0.3",
"style-loader": "^0.19.1", "style-loader": "^0.21.0",
"to-string-loader": "^1.1.5", "to-string-loader": "^1.1.5",
"ts-node": "^3.3.0", "ts-node": "^7.0.0",
"tslint": "^5.9.1", "tslint": "^5.10.0",
"tslint-language-service": "^0.9.8", "tslint-language-service": "^0.9.9",
"typescript": "^2.7.1", "typescript": "2.7.2",
"uglify-es": "^3.3.10", "uglify-es": "^3.3.9",
"uglifyjs-webpack-plugin": "^1.1.8", "uglifyjs-webpack-plugin": "^1.2.7",
"url-loader": "^0.6.2", "url-loader": "^1.0.1",
"webpack": "^3.11.0", "webpack": "^4.14.0",
"webpack-bundle-analyzer": "^2.10.0", "webpack-bundle-analyzer": "^2.13.1",
"webpack-hot-middleware": "^2.21.0", "webpack-cli": "^3.0.8",
"zone.js": "^0.8.20" "webpack-dev-middleware": "^3.1.3",
"webpack-hot-middleware": "^2.22.2",
"webpack-merge": "^4.1.3",
"zone.js": "^0.8.26"
}, },
"devDependencies": { "resolutions": {
"@types/chai": "4.0.4", "@types/tapable": "1.0.0"
"@types/jasmine": "2.6.2",
"chai": "4.1.2",
"jasmine-core": "2.8.0",
"karma": "1.7.1",
"karma-chai": "0.1.0",
"karma-chrome-launcher": "2.2.0",
"karma-cli": "1.0.1",
"karma-jasmine": "1.1.0",
"karma-webpack": "2.0.5"
} }
} }

View file

@ -2,7 +2,7 @@
"compilerOptions": { "compilerOptions": {
"target": "es5", "target": "es5",
"lib": [ "lib": [
"es6", "es2017",
"dom" "dom"
], ],
"moduleResolution": "node", "moduleResolution": "node",
@ -27,12 +27,15 @@
} }
] ]
}, },
"exclude": [ "include": [
"bin", "ClientApp/**/*",
"node_modules", "typings/**/*",
"TempUpdate" "webpack.config.*"
], ],
"angularCompilerOptions": { "angularCompilerOptions": {
"preserveWhitespaces": false "preserveWhitespaces": false,
} "genDir": "./ClientApp/app/ngfactory",
"entryModule": "ClientApp/app/app.module#AppModule"
},
"compileOnSave": false
} }

View file

@ -8,7 +8,7 @@
], ],
"max-line-length": [ "max-line-length": [
true, true,
200 250
], ],
"arrow-parens": false, "arrow-parens": false,
"radix": false, "radix": false,
@ -16,22 +16,33 @@
"indent": [ "indent": [
false false
], ],
"whitespace": [
false
],
"no-unused-expression": [ "no-unused-expression": [
true, true,
"allow-new" "allow-new"
], ],
"no-trailing-whitespace": [
false
],
"max-classes-per-file": [ "max-classes-per-file": [
false false
], ],
"no-shadowed-variable": false, "no-shadowed-variable": false,
"comment-format": [ "comment-format": [
false false
],
"no-namespace": [
false
],
"no-internal-module": [
false
],
"whitespace": [
false,
"check-branch",
"check-decl",
"check-operator",
"check-separator",
"check-type"
],
"no-trailing-whitespace":[
false
] ]
} }
} }

4
src/Ombi/typings/globals.d.ts vendored Normal file
View file

@ -0,0 +1,4 @@
// Globals
declare module "pace-progress";
declare var __webpack_public_path__: any;

View file

@ -1,12 +0,0 @@
// Globals
declare var module: any;
declare var require: any;
declare var localStorage: any;
declare var introJs: any;
declare var __webpack_public_path__: any;
declare module "pace-progress";
declare module "webpack-bundle-analyzer";
declare module "uglifyjs-webpack-plugin";

View file

@ -1 +1 @@
/// <reference path="globals/globals.d.ts" /> /// <reference path="globals.d.ts" />

View file

@ -0,0 +1,83 @@
import { AngularCompilerPlugin } from "@ngtools/webpack";
import * as MiniCssExtractPlugin from "mini-css-extract-plugin";
import * as path from "path";
import { Configuration, ContextReplacementPlugin, ProvidePlugin } from "webpack";
import { BundleAnalyzerPlugin } from "webpack-bundle-analyzer";
export const outputDir = "./wwwroot/dist";
export function isProd(env: any) {
return env && env.prod as boolean;
}
export function isAOT(env: any) {
return env && env.aot as boolean;
}
export const WebpackCommonConfig = (env: any, type: string) => {
const prod = isProd(env);
const aot = isAOT(env);
const vendor = type === "vendor";
console.log(`${prod ? "Production" : "Dev"} ${type} build`);
console.log(`Output directory: ${outputDir}`);
console.log(`${aot ? "Using" : "Not using"} AOT compiler`);
const analyse = env && env.analyse as boolean;
if (analyse) { console.log("Analysing build"); }
const cssLoader = prod ? "css-loader?minimize" : "css-loader";
const bundleConfig: Configuration = {
mode: prod ? "production" : "development",
resolve: {
extensions: [".ts", ".js"],
alias: {
pace: "pace-progress",
},
},
output: {
path: path.resolve(outputDir),
filename: "[name].js",
chunkFilename: "[id].chunk.js",
publicPath: "/dist/",
},
module: {
rules: [
{ test: /\.ts$/, loader: aot ? "@ngtools/webpack" : ["awesome-typescript-loader?silent=true", "angular2-template-loader", "angular-router-loader"] },
{ test: /\.html$/, use: "html-loader?minimize=false" },
{ test: /\.css$/, use: [MiniCssExtractPlugin.loader, cssLoader] },
{ test: /\.scss$/, exclude: /ClientApp/, use: [MiniCssExtractPlugin.loader, cssLoader, "sass-loader"] },
{ test: /\.scss$/, include: /ClientApp(\\|\/)app/, use: ["to-string-loader", cssLoader, "sass-loader"] },
{ test: /\.scss$/, include: /ClientApp(\\|\/)styles/, use: ["style-loader", cssLoader, "sass-loader"] },
{ test: /\.(png|jpg|jpeg|gif|woff|woff2|eot|ttf|svg)(\?|$)/, use: "url-loader?limit=8192" },
{ test: /[\/\\]@angular[\/\\].+\.js$/, parser: { system: true } }, // ignore System.import warnings https://github.com/angular/angular/issues/21560
],
},
plugins: [
new MiniCssExtractPlugin({
filename: "[name].css",
}),
new ProvidePlugin({ $: "jquery", jQuery: "jquery", Hammer: "hammerjs/hammer" }), // Global identifiers
].concat(aot && !vendor ? [
new AngularCompilerPlugin({
mainPath: "./ClientApp/main.ts",
tsConfigPath: "./tsconfig.json",
skipCodeGeneration: false,
compilerOptions: {
noEmit: false,
},
}),
] : [
// AOT chunk splitting does not work while this is active but doesn't seem to be needed under AOT anyway https://github.com/angular/angular-cli/issues/4431
new ContextReplacementPlugin(/angular(\\|\/)core(\\|\/)/, path.join(__dirname, "./ClientApp")), // Workaround for https://github.com/angular/angular/issues/14898
]).concat(analyse ? [
new BundleAnalyzerPlugin({
analyzerMode: "static",
reportFilename: `${type}.html`,
openAnalyzer: false,
}),
] : []),
node: {
fs: "empty",
},
};
return bundleConfig;
};

View file

@ -1,59 +1,26 @@
import { CheckerPlugin } from "awesome-typescript-loader";
import * as path from "path"; import * as path from "path";
import * as UglifyJSPlugin from "uglifyjs-webpack-plugin"; import { Configuration, DllReferencePlugin } from "webpack";
import { BundleAnalyzerPlugin } from "webpack-bundle-analyzer"; import * as webpackMerge from "webpack-merge";
import * as webpack from "webpack"; import { isAOT, isProd, outputDir, WebpackCommonConfig } from "./webpack.config.common";
module.exports = (env: any) => { module.exports = (env: any) => {
// const prod = env && env.prod as boolean; const prod = isProd(env);
const prod = true; const aot = isAOT(env);
console.log(prod ? "Production" : "Dev" + " main build"); if (!prod && aot) { console.warn("Vendor dll bundle will not be used as AOT is enabled"); }
const analyse = env && env.analyse as boolean; const bundleConfig: Configuration = webpackMerge(WebpackCommonConfig(env, "main"), {
if (analyse) { console.log("Analysing build"); } entry: {
const cssLoader = prod ? "css-loader?-url&minimize" : "css-loader?-url"; app: "./ClientApp/main.ts",
const outputDir = "./wwwroot/dist"; },
const bundleConfig: webpack.Configuration = {
entry: { main: "./ClientApp/main.ts" },
stats: { modules: false },
context: __dirname,
resolve: { extensions: [".ts", ".js"] },
devtool: prod ? "source-map" : "eval-source-map", devtool: prod ? "source-map" : "eval-source-map",
output: { plugins: prod || aot ? [] : [
filename: "[name].js", // AOT chunk splitting does not work while this is active https://github.com/angular/angular-cli/issues/4565
chunkFilename: "[id].[chunkhash].js", new DllReferencePlugin({
publicPath: "/dist/",
path: path.join(__dirname, outputDir),
},
module: {
rules: [
{ test: /\.ts$/, include: /ClientApp/, use: ["awesome-typescript-loader?silent=true", "angular2-template-loader", "angular-router-loader"] },
{ test: /\.html$/, use: "html-loader?minimize=false" },
{ test: /\.css$/, use: ["to-string-loader", cssLoader] },
{ test: /\.scss$/, include: /ClientApp(\\|\/)app/, use: ["to-string-loader", cssLoader, "sass-loader"] },
{ test: /\.scss$/, include: /ClientApp(\\|\/)styles/, use: ["style-loader", cssLoader, "sass-loader"] },
{ test: /\.(png|jpg|jpeg|gif|svg)$/, use: "url-loader?limit=25000" },
],
},
plugins: [
new CheckerPlugin(),
new webpack.DllReferencePlugin({
context: __dirname, context: __dirname,
manifest: require(path.join(__dirname, outputDir, "vendor-manifest.json")), manifest: require(path.join(__dirname, outputDir, "vendor-manifest.json")),
}), }),
].concat(prod ? [ ],
// Plugins that apply in production builds only });
new UglifyJSPlugin({ sourceMap: true }),
] : [
// Plugins that apply in development builds only
]).concat(analyse ? [
new BundleAnalyzerPlugin({
analyzerMode: "static",
reportFilename: "main.html",
openAnalyzer: false,
}),
] : []),
};
return bundleConfig; return bundleConfig;
}; };

View file

@ -1,41 +1,25 @@
import * as ExtractTextPlugin from "extract-text-webpack-plugin";
import * as path from "path"; import * as path from "path";
import * as UglifyJSPlugin from "uglifyjs-webpack-plugin";
import * as webpack from "webpack"; import * as webpack from "webpack";
import { BundleAnalyzerPlugin } from "webpack-bundle-analyzer"; import * as webpackMerge from "webpack-merge";
import { isProd, outputDir, WebpackCommonConfig } from "./webpack.config.common";
module.exports = (env: any) => { module.exports = (env: any) => {
const extractCSS = new ExtractTextPlugin("vendor.css"); const prod = isProd(env);
const prod = env && env.prod as boolean; const bundleConfig = webpackMerge(WebpackCommonConfig(env, "vendor"), {
console.log(prod ? "Production" : "Dev" + " vendor build"); output: {
const analyse = env && env.analyse as boolean; library: "[name]_[hash]",
if (analyse) { console.log("Analysing build"); }
const outputDir = "./wwwroot/dist";
const bundleConfig = {
stats: { modules: false },
resolve: {
extensions: [".js"],
alias: {
pace: "pace-progress",
},
},
module: {
rules: [
{ test: /\.(png|woff|woff2|eot|ttf|svg|gif)(\?|$)/, use: "url-loader?limit=100000" },
{ test: /\.css(\?|$)/, use: extractCSS.extract({ use: prod ? "css-loader?minimize" : "css-loader" }) },
{ test: /\.scss(\?|$)/, use: extractCSS.extract({ use: [prod ? "css-loader?minimize" : "css-loader", "sass-loader"] }) },
],
}, },
entry: { entry: {
vendor: [ vendor: (<string[]>[ // add any vendor styles here e.g. bootstrap/dist/css/bootstrap.min.css
"pace-progress/themes/orange/pace-theme-flash.css", "pace-progress/themes/orange/pace-theme-flash.css",
"primeng/resources/primeng.min.css", "primeng/resources/primeng.min.css",
"@angular/material/prebuilt-themes/deeppurple-amber.css", "@angular/material/prebuilt-themes/deeppurple-amber.css",
"font-awesome/scss/font-awesome.scss", "font-awesome/scss/font-awesome.scss",
"bootswatch/superhero/bootstrap.min.css", "bootswatch/superhero/bootstrap.min.css",
]).concat(prod ? [] : [ // used to speed up dev launch time
"@angular/animations", "@angular/animations",
"@angular/common", "@angular/common",
"@angular/common/http",
"@angular/compiler", "@angular/compiler",
"@angular/core", "@angular/core",
"@angular/forms", "@angular/forms",
@ -67,41 +51,14 @@ module.exports = (env: any) => {
"@ngx-translate/core", "@ngx-translate/core",
"@ngx-translate/http-loader", "@ngx-translate/http-loader",
"ngx-order-pipe", "ngx-order-pipe",
//"smartbanner.js/dist/smartbanner.js", ]),
//"smartbanner.js/dist/smartbanner.css",
],
}, },
output: { plugins: prod ? [] : [
publicPath: "/dist/",
filename: "[name].js",
library: "[name]_[hash]",
path: path.join(__dirname, outputDir),
},
node: {
fs: "empty",
},
plugins: [
new webpack.ProvidePlugin({ $: "jquery", jQuery: "jquery", Hammer: "hammerjs/hammer" }), // Global identifiers
new webpack.ContextReplacementPlugin(/\@angular(\\|\/)core(\\|\/)esm5/, path.join(__dirname, "./client")), // Workaround for https://github.com/angular/angular/issues/20357
new webpack.ContextReplacementPlugin(/\@angular\b.*\b(bundles|linker)/, path.join(__dirname, "./ClientApp")), // Workaround for https://github.com/angular/angular/issues/11580
new webpack.ContextReplacementPlugin(/angular(\\|\/)core(\\|\/)@angular/, path.join(__dirname, "./ClientApp")), // Workaround for https://github.com/angular/angular/issues/14898
extractCSS,
new webpack.DllPlugin({ new webpack.DllPlugin({
path: path.join(__dirname, outputDir, "[name]-manifest.json"), path: path.join(__dirname, outputDir, "[name]-manifest.json"),
name: "[name]_[hash]", name: "[name]_[hash]",
}), }),
].concat(prod ? [ ],
// Plugins that apply in production builds only });
new UglifyJSPlugin(),
] : [
// Plugins that apply in development builds only
]).concat(analyse ? [
new BundleAnalyzerPlugin({
analyzerMode: "static",
reportFilename: "vendor.html",
openAnalyzer: false,
}),
] : []),
};
return bundleConfig; return bundleConfig;
}; };

View file

@ -1,4 +0,0 @@
// https://github.com/aspnet/JavaScriptServices/issues/1046
require('ts-node/register')
module.exports = require("./webpack.config.ts");

View file

@ -62,9 +62,12 @@
"Italian": "Italiensk", "Italian": "Italiensk",
"Danish": "Dansk", "Danish": "Dansk",
"Dutch": "Hollandsk", "Dutch": "Hollandsk",
"Norwegian": "Norsk" "Norwegian": "Norsk",
"BrazillianPortuguese": "Brazillian Portuguese",
"Polish": "Polish"
}, },
"OpenMobileApp": "Åbn mobilapp" "OpenMobileApp": "Åbn mobilapp",
"RecentlyAdded": "Recently Added"
}, },
"Search": { "Search": {
"Title": "Søg", "Title": "Søg",
@ -113,6 +116,7 @@
"RequestStatus": "Status for anmodning:", "RequestStatus": "Status for anmodning:",
"Denied": " Afvist:", "Denied": " Afvist:",
"TheatricalRelease": "Theatrical Release: {{date}}", "TheatricalRelease": "Theatrical Release: {{date}}",
"TheatricalReleaseSort": "Theatrical Release",
"DigitalRelease": "Digital Release: {{date}}", "DigitalRelease": "Digital Release: {{date}}",
"RequestDate": "Dato for anmodning:", "RequestDate": "Dato for anmodning:",
"QualityOverride": "Tilsidesæt kvalitet:", "QualityOverride": "Tilsidesæt kvalitet:",
@ -129,7 +133,14 @@
"GridStatus": "Status", "GridStatus": "Status",
"ReportIssue": "Rapportér problem", "ReportIssue": "Rapportér problem",
"Filter": "Filter", "Filter": "Filter",
"SeasonNumberHeading": "Sæson: {seasonNumber}" "Sort": "Sort",
"SeasonNumberHeading": "Sæson: {seasonNumber}",
"SortTitleAsc": "Title ▲",
"SortTitleDesc": "Title ▼",
"SortRequestDateAsc": "Request Date ▲",
"SortRequestDateDesc": "Request Date ▼",
"SortStatusAsc": "Status ▲",
"SortStatusDesc": "Status ▼"
}, },
"Issues": { "Issues": {
"Title": "Problemer", "Title": "Problemer",
@ -154,6 +165,7 @@
"ClearFilter": "Nulstil filter", "ClearFilter": "Nulstil filter",
"FilterHeaderAvailability": "Tilgængelighed", "FilterHeaderAvailability": "Tilgængelighed",
"FilterHeaderRequestStatus": "Status", "FilterHeaderRequestStatus": "Status",
"Approved": "Godkendt" "Approved": "Godkendt",
"PendingApproval": "Pending Approval"
} }
} }

View file

@ -62,9 +62,12 @@
"Italian": "Italienisch", "Italian": "Italienisch",
"Danish": "Dänisch", "Danish": "Dänisch",
"Dutch": "Niederländisch", "Dutch": "Niederländisch",
"Norwegian": "Norwegisch" "Norwegian": "Norwegisch",
"BrazillianPortuguese": "Brazillian Portuguese",
"Polish": "Polish"
}, },
"OpenMobileApp": "Mobile App" "OpenMobileApp": "Mobile App",
"RecentlyAdded": "Recently Added"
}, },
"Search": { "Search": {
"Title": "Suche", "Title": "Suche",
@ -113,6 +116,7 @@
"RequestStatus": "Anfrage Status:", "RequestStatus": "Anfrage Status:",
"Denied": " Abgelehnt:", "Denied": " Abgelehnt:",
"TheatricalRelease": "Theatrical Release: {{date}}", "TheatricalRelease": "Theatrical Release: {{date}}",
"TheatricalReleaseSort": "Theatrical Release",
"DigitalRelease": "Digital Release: {{date}}", "DigitalRelease": "Digital Release: {{date}}",
"RequestDate": "Datum der Anfrage:", "RequestDate": "Datum der Anfrage:",
"QualityOverride": "Qualitäts Überschreiben:", "QualityOverride": "Qualitäts Überschreiben:",
@ -129,7 +133,14 @@
"GridStatus": "Status", "GridStatus": "Status",
"ReportIssue": "Problem melden", "ReportIssue": "Problem melden",
"Filter": "Filter", "Filter": "Filter",
"SeasonNumberHeading": "Staffel: {seasonNumber}" "Sort": "Sort",
"SeasonNumberHeading": "Staffel: {seasonNumber}",
"SortTitleAsc": "Title ▲",
"SortTitleDesc": "Title ▼",
"SortRequestDateAsc": "Request Date ▲",
"SortRequestDateDesc": "Request Date ▼",
"SortStatusAsc": "Status ▲",
"SortStatusDesc": "Status ▼"
}, },
"Issues": { "Issues": {
"Title": "Probleme", "Title": "Probleme",
@ -154,6 +165,7 @@
"ClearFilter": "Filter zurücksetzen", "ClearFilter": "Filter zurücksetzen",
"FilterHeaderAvailability": "Verfügbarkeit", "FilterHeaderAvailability": "Verfügbarkeit",
"FilterHeaderRequestStatus": "Status", "FilterHeaderRequestStatus": "Status",
"Approved": "Bestätigt" "Approved": "Bestätigt",
"PendingApproval": "Pending Approval"
} }
} }

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