mirror of
https://github.com/Ombi-app/Ombi.git
synced 2025-07-16 02:02:55 -07:00
Merge branch 'develop' into imgbot
This commit is contained in:
commit
bbceb56d8b
108 changed files with 3589 additions and 362 deletions
|
@ -3,10 +3,14 @@ configuration: Release
|
||||||
os: Visual Studio 2017
|
os: Visual Studio 2017
|
||||||
environment:
|
environment:
|
||||||
nodejs_version: "9.8.0"
|
nodejs_version: "9.8.0"
|
||||||
|
typescript_version: "3.0.1"
|
||||||
|
|
||||||
install:
|
install:
|
||||||
# Get the latest stable version of Node.js or io.js
|
# Get the latest stable version of Node.js or io.js
|
||||||
- ps: Install-Product node $env:nodejs_version
|
- ps: Install-Product node $env:nodejs_version
|
||||||
|
|
||||||
|
- cmd: set path=%programfiles(x86)%\\Microsoft SDKs\TypeScript\2.2;%path%
|
||||||
|
- cmd: tsc -v
|
||||||
build_script:
|
build_script:
|
||||||
- ps: ./build.ps1 --settings_skipverification=true
|
- ps: ./build.ps1 --settings_skipverification=true
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ namespace Ombi.Api.Plex
|
||||||
Task<PlexAccount> GetAccount(string authToken);
|
Task<PlexAccount> GetAccount(string authToken);
|
||||||
Task<PlexMetadata> GetRecentlyAdded(string authToken, string uri, string sectionId);
|
Task<PlexMetadata> GetRecentlyAdded(string authToken, string uri, string sectionId);
|
||||||
Task<OAuthPin> GetPin(int pinId);
|
Task<OAuthPin> GetPin(int pinId);
|
||||||
Task<Uri> GetOAuthUrl(int pinId, string code, string applicationUrl);
|
Task<Uri> GetOAuthUrl(string code, string applicationUrl);
|
||||||
Task<PlexAddWrapper> AddUser(string emailAddress, string serverId, string authToken, int[] libs);
|
Task<PlexAddWrapper> AddUser(string emailAddress, string serverId, string authToken, int[] libs);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -217,12 +217,11 @@ namespace Ombi.Api.Plex
|
||||||
return await Api.Request<OAuthPin>(request);
|
return await Api.Request<OAuthPin>(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Uri> GetOAuthUrl(int pinId, string code, string applicationUrl)
|
public async Task<Uri> GetOAuthUrl(string code, string applicationUrl)
|
||||||
{
|
{
|
||||||
var request = new Request("auth#", "https://app.plex.tv", HttpMethod.Get);
|
var request = new Request("auth#", "https://app.plex.tv", HttpMethod.Get);
|
||||||
await AddHeaders(request);
|
await AddHeaders(request);
|
||||||
|
|
||||||
request.AddQueryString("pinID", pinId.ToString());
|
|
||||||
request.AddQueryString("code", code);
|
request.AddQueryString("code", code);
|
||||||
request.AddQueryString("context[device][product]", ApplicationName);
|
request.AddQueryString("context[device][product]", ApplicationName);
|
||||||
request.AddQueryString("context[device][environment]", "bundled");
|
request.AddQueryString("context[device][environment]", "bundled");
|
||||||
|
|
73
src/Ombi.Core.Tests/Engine/VoteEngineTests.cs
Normal file
73
src/Ombi.Core.Tests/Engine/VoteEngineTests.cs
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Principal;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using AutoFixture;
|
||||||
|
using Moq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using Ombi.Core.Authentication;
|
||||||
|
using Ombi.Core.Engine;
|
||||||
|
using Ombi.Core.Engine.Interfaces;
|
||||||
|
using Ombi.Core.Rule.Interfaces;
|
||||||
|
using Ombi.Core.Settings;
|
||||||
|
using Ombi.Settings.Settings.Models;
|
||||||
|
using Ombi.Store.Entities;
|
||||||
|
using Ombi.Store.Repository;
|
||||||
|
|
||||||
|
namespace Ombi.Core.Tests.Engine
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class VoteEngineTests
|
||||||
|
{
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
F = new Fixture();
|
||||||
|
VoteRepository = new Mock<IRepository<Votes>>();
|
||||||
|
VoteSettings = new Mock<ISettingsService<VoteSettings>>();
|
||||||
|
MusicRequestEngine = new Mock<IMusicRequestEngine>();
|
||||||
|
TvRequestEngine = new Mock<ITvRequestEngine>();
|
||||||
|
MovieRequestEngine = new Mock<IMovieRequestEngine>();
|
||||||
|
MovieRequestEngine = new Mock<IMovieRequestEngine>();
|
||||||
|
User = new Mock<IPrincipal>();
|
||||||
|
UserManager = new Mock<OmbiUserManager>();
|
||||||
|
UserManager.Setup(x => x.Users)
|
||||||
|
.Returns(new EnumerableQuery<OmbiUser>(new List<OmbiUser> {new OmbiUser {Id = "abc"}}));
|
||||||
|
Rule = new Mock<IRuleEvaluator>();
|
||||||
|
Engine = new VoteEngine(VoteRepository.Object, User.Object, UserManager.Object, Rule.Object, VoteSettings.Object, MusicRequestEngine.Object,
|
||||||
|
TvRequestEngine.Object, MovieRequestEngine.Object);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Fixture F { get; set; }
|
||||||
|
public VoteEngine Engine { get; set; }
|
||||||
|
public Mock<IPrincipal> User { get; set; }
|
||||||
|
public Mock<OmbiUserManager> UserManager { get; set; }
|
||||||
|
public Mock<IRuleEvaluator> Rule { get; set; }
|
||||||
|
public Mock<IRepository<Votes>> VoteRepository { get; set; }
|
||||||
|
public Mock<ISettingsService<VoteSettings>> VoteSettings { get; set; }
|
||||||
|
public Mock<IMusicRequestEngine> MusicRequestEngine { get; set; }
|
||||||
|
public Mock<ITvRequestEngine> TvRequestEngine { get; set; }
|
||||||
|
public Mock<IMovieRequestEngine> MovieRequestEngine { get; set; }
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[Ignore("Need to mock the user manager")]
|
||||||
|
public async Task New_Upvote()
|
||||||
|
{
|
||||||
|
VoteSettings.Setup(x => x.GetSettingsAsync()).ReturnsAsync(new VoteSettings());
|
||||||
|
var votes = F.CreateMany<Votes>().ToList();
|
||||||
|
votes.Add(new Votes
|
||||||
|
{
|
||||||
|
RequestId = 1,
|
||||||
|
RequestType = RequestType.Movie,
|
||||||
|
UserId = "abc"
|
||||||
|
});
|
||||||
|
VoteRepository.Setup(x => x.GetAll()).Returns(new EnumerableQuery<Votes>(votes));
|
||||||
|
var result = await Engine.UpVote(1, RequestType.Movie);
|
||||||
|
|
||||||
|
Assert.That(result.Result, Is.True);
|
||||||
|
VoteRepository.Verify(x => x.Add(It.Is<Votes>(c => c.UserId == "abc" && c.VoteType == VoteType.Upvote)), Times.Once);
|
||||||
|
VoteRepository.Verify(x => x.Delete(It.IsAny<Votes>()), Times.Once);
|
||||||
|
MovieRequestEngine.Verify(x => x.ApproveMovieById(1), Times.Never);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,9 +5,10 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Moq" Version="4.9.0" />
|
<PackageReference Include="AutoFixture" Version="4.5.0" />
|
||||||
|
<PackageReference Include="Moq" Version="4.10.0" />
|
||||||
<PackageReference Include="Nunit" Version="3.10.1" />
|
<PackageReference Include="Nunit" Version="3.10.1" />
|
||||||
<PackageReference Include="NUnit.ConsoleRunner" Version="3.8.0" />
|
<PackageReference Include="NUnit.ConsoleRunner" Version="3.9.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="3.10.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="3.10.0" />
|
||||||
<packagereference Include="Microsoft.NET.Test.Sdk" Version="15.8.0"></packagereference>
|
<packagereference Include="Microsoft.NET.Test.Sdk" Version="15.8.0"></packagereference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
@ -36,17 +36,17 @@ namespace Ombi.Core.Authentication
|
||||||
return await _api.GetAccount(accessToken);
|
return await _api.GetAccount(accessToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Uri> GetOAuthUrl(int pinId, string code, string websiteAddress = null)
|
public async Task<Uri> GetOAuthUrl(string code, string websiteAddress = null)
|
||||||
{
|
{
|
||||||
var settings = await _customizationSettingsService.GetSettingsAsync();
|
var settings = await _customizationSettingsService.GetSettingsAsync();
|
||||||
var url = await _api.GetOAuthUrl(pinId, code, settings.ApplicationUrl.IsNullOrEmpty() ? websiteAddress : settings.ApplicationUrl);
|
var url = await _api.GetOAuthUrl(code, settings.ApplicationUrl.IsNullOrEmpty() ? websiteAddress : settings.ApplicationUrl);
|
||||||
|
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Uri> GetWizardOAuthUrl(int pinId, string code, string websiteAddress)
|
public async Task<Uri> GetWizardOAuthUrl(string code, string websiteAddress)
|
||||||
{
|
{
|
||||||
var url = await _api.GetOAuthUrl(pinId, code, websiteAddress);
|
var url = await _api.GetOAuthUrl(code, websiteAddress);
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
19
src/Ombi.Core/Engine/IVoteEngine.cs
Normal file
19
src/Ombi.Core/Engine/IVoteEngine.cs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Ombi.Core.Models;
|
||||||
|
using Ombi.Core.Models.UI;
|
||||||
|
using Ombi.Store.Entities;
|
||||||
|
|
||||||
|
namespace Ombi.Core.Engine
|
||||||
|
{
|
||||||
|
public interface IVoteEngine
|
||||||
|
{
|
||||||
|
Task<VoteEngineResult> DownVote(int requestId, RequestType requestType);
|
||||||
|
Task<Votes> GetVoteForUser(int requestId, string userId);
|
||||||
|
IQueryable<Votes> GetVotes(int requestId, RequestType requestType);
|
||||||
|
Task RemoveCurrentVote(Votes currentVote);
|
||||||
|
Task<VoteEngineResult> UpVote(int requestId, RequestType requestType);
|
||||||
|
Task<List<VoteViewModel>> GetMovieViewModel();
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,11 +7,8 @@ using Ombi.Core.Models.Search;
|
||||||
using Ombi.Core.Rule.Interfaces;
|
using Ombi.Core.Rule.Interfaces;
|
||||||
using Ombi.Store.Entities.Requests;
|
using Ombi.Store.Entities.Requests;
|
||||||
using Ombi.Store.Entities;
|
using Ombi.Store.Entities;
|
||||||
using Microsoft.AspNetCore.Identity;
|
|
||||||
using System.Linq;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Ombi.Core.Authentication;
|
using Ombi.Core.Authentication;
|
||||||
using Ombi.Helpers;
|
|
||||||
|
|
||||||
namespace Ombi.Core.Engine.Interfaces
|
namespace Ombi.Core.Engine.Interfaces
|
||||||
{
|
{
|
||||||
|
|
|
@ -61,7 +61,7 @@ namespace Ombi.Core.Engine
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
retVal.Add(await ProcessResult(tvMazeSearch));
|
retVal.Add(ProcessResult(tvMazeSearch));
|
||||||
}
|
}
|
||||||
return retVal;
|
return retVal;
|
||||||
}
|
}
|
||||||
|
@ -123,7 +123,7 @@ namespace Ombi.Core.Engine
|
||||||
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));
|
||||||
var processed = await ProcessResults(result);
|
var processed = ProcessResults(result);
|
||||||
return processed;
|
return processed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,35 +131,35 @@ namespace Ombi.Core.Engine
|
||||||
{
|
{
|
||||||
|
|
||||||
var result = await Cache.GetOrAdd(CacheKeys.AnticipatedTv, async () => await TraktApi.GetAnticipatedShows(), DateTime.Now.AddHours(12));
|
var result = await Cache.GetOrAdd(CacheKeys.AnticipatedTv, async () => await TraktApi.GetAnticipatedShows(), DateTime.Now.AddHours(12));
|
||||||
var processed = await ProcessResults(result);
|
var processed = ProcessResults(result);
|
||||||
return processed;
|
return processed;
|
||||||
}
|
}
|
||||||
|
|
||||||
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));
|
||||||
var processed = await ProcessResults(result);
|
var processed = ProcessResults(result);
|
||||||
return processed;
|
return processed;
|
||||||
}
|
}
|
||||||
|
|
||||||
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));
|
||||||
var processed = await ProcessResults(result);
|
var processed = ProcessResults(result);
|
||||||
return processed;
|
return processed;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<IEnumerable<SearchTvShowViewModel>> ProcessResults<T>(IEnumerable<T> items)
|
private IEnumerable<SearchTvShowViewModel> ProcessResults<T>(IEnumerable<T> items)
|
||||||
{
|
{
|
||||||
var retVal = new List<SearchTvShowViewModel>();
|
var retVal = new List<SearchTvShowViewModel>();
|
||||||
foreach (var tvMazeSearch in items)
|
foreach (var tvMazeSearch in items)
|
||||||
{
|
{
|
||||||
retVal.Add(await ProcessResult(tvMazeSearch));
|
retVal.Add(ProcessResult(tvMazeSearch));
|
||||||
}
|
}
|
||||||
return retVal;
|
return retVal;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<SearchTvShowViewModel> ProcessResult<T>(T tvMazeSearch)
|
private SearchTvShowViewModel ProcessResult<T>(T tvMazeSearch)
|
||||||
{
|
{
|
||||||
return Mapper.Map<SearchTvShowViewModel>(tvMazeSearch);
|
return Mapper.Map<SearchTvShowViewModel>(tvMazeSearch);
|
||||||
}
|
}
|
||||||
|
|
254
src/Ombi.Core/Engine/VoteEngine.cs
Normal file
254
src/Ombi.Core/Engine/VoteEngine.cs
Normal file
|
@ -0,0 +1,254 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Principal;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Ombi.Core.Authentication;
|
||||||
|
using Ombi.Core.Engine.Interfaces;
|
||||||
|
using Ombi.Core.Models;
|
||||||
|
using Ombi.Core.Models.UI;
|
||||||
|
using Ombi.Core.Rule.Interfaces;
|
||||||
|
using Ombi.Core.Settings;
|
||||||
|
using Ombi.Schedule.Jobs.Ombi;
|
||||||
|
using Ombi.Settings.Settings.Models;
|
||||||
|
using Ombi.Store.Entities;
|
||||||
|
using Ombi.Store.Repository;
|
||||||
|
|
||||||
|
namespace Ombi.Core.Engine
|
||||||
|
{
|
||||||
|
public class VoteEngine : BaseEngine, IVoteEngine
|
||||||
|
{
|
||||||
|
public VoteEngine(IRepository<Votes> votes, IPrincipal user, OmbiUserManager um, IRuleEvaluator r, ISettingsService<VoteSettings> voteSettings,
|
||||||
|
IMusicRequestEngine musicRequestEngine, ITvRequestEngine tvRequestEngine, IMovieRequestEngine movieRequestEngine) : base(user, um, r)
|
||||||
|
{
|
||||||
|
_voteRepository = votes;
|
||||||
|
_voteSettings = voteSettings;
|
||||||
|
_movieRequestEngine = movieRequestEngine;
|
||||||
|
_musicRequestEngine = musicRequestEngine;
|
||||||
|
_tvRequestEngine = tvRequestEngine;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly IRepository<Votes> _voteRepository;
|
||||||
|
private readonly ISettingsService<VoteSettings> _voteSettings;
|
||||||
|
private readonly IMusicRequestEngine _musicRequestEngine;
|
||||||
|
private readonly ITvRequestEngine _tvRequestEngine;
|
||||||
|
private readonly IMovieRequestEngine _movieRequestEngine;
|
||||||
|
|
||||||
|
public async Task<List<VoteViewModel>> GetMovieViewModel()
|
||||||
|
{
|
||||||
|
var vm = new List<VoteViewModel>();
|
||||||
|
var movieRequests = await _movieRequestEngine.GetRequests();
|
||||||
|
var tvRequestsTask = _tvRequestEngine.GetRequests();
|
||||||
|
var musicRequestsTask = _musicRequestEngine.GetRequests();
|
||||||
|
var user = await GetUser();
|
||||||
|
foreach (var r in movieRequests)
|
||||||
|
{
|
||||||
|
if (r.Available || r.Approved || (r.Denied ?? false))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Make model
|
||||||
|
var votes = GetVotes(r.Id, RequestType.Movie);
|
||||||
|
var upVotes = await votes.Where(x => x.VoteType == VoteType.Upvote).CountAsync();
|
||||||
|
var downVotes = await votes.Where(x => x.VoteType == VoteType.Downvote).CountAsync();
|
||||||
|
var myVote = await votes.FirstOrDefaultAsync(x => x.UserId == user.Id && !x.Deleted);
|
||||||
|
|
||||||
|
vm.Add(new VoteViewModel
|
||||||
|
{
|
||||||
|
Upvotes = upVotes,
|
||||||
|
Downvotes = downVotes,
|
||||||
|
RequestId = r.Id,
|
||||||
|
RequestType = RequestType.Movie,
|
||||||
|
Title = r.Title,
|
||||||
|
Image = $"https://image.tmdb.org/t/p/w500/{r.PosterPath}",
|
||||||
|
Background = $"https://image.tmdb.org/t/p/w1280{r.Background}",
|
||||||
|
Description = r.Overview,
|
||||||
|
AlreadyVoted = myVote != null,
|
||||||
|
MyVote = myVote?.VoteType ?? VoteType.Downvote
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var r in await musicRequestsTask)
|
||||||
|
{
|
||||||
|
if (r.Available || r.Approved || (r.Denied ?? false))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Make model
|
||||||
|
var votes = GetVotes(r.Id, RequestType.Album);
|
||||||
|
var upVotes = await votes.Where(x => x.VoteType == VoteType.Upvote).CountAsync();
|
||||||
|
var downVotes = await votes.Where(x => x.VoteType == VoteType.Downvote).CountAsync();
|
||||||
|
var myVote = await votes.FirstOrDefaultAsync(x => x.UserId == user.Id && !x.Deleted);
|
||||||
|
vm.Add(new VoteViewModel
|
||||||
|
{
|
||||||
|
Upvotes = upVotes,
|
||||||
|
Downvotes = downVotes,
|
||||||
|
RequestId = r.Id,
|
||||||
|
RequestType = RequestType.Album,
|
||||||
|
Title = r.Title,
|
||||||
|
Image = r.Cover,
|
||||||
|
Background = r.Cover,
|
||||||
|
Description = r.ArtistName,
|
||||||
|
AlreadyVoted = myVote != null,
|
||||||
|
MyVote = myVote?.VoteType ?? VoteType.Downvote
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var r in await tvRequestsTask)
|
||||||
|
{
|
||||||
|
|
||||||
|
foreach (var childRequests in r.ChildRequests)
|
||||||
|
{
|
||||||
|
var finalsb = new StringBuilder();
|
||||||
|
if (childRequests.Available || childRequests.Approved || (childRequests.Denied ?? false))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var votes = GetVotes(childRequests.Id, RequestType.TvShow);
|
||||||
|
// Make model
|
||||||
|
var upVotes = await votes.Where(x => x.VoteType == VoteType.Upvote).CountAsync();
|
||||||
|
var downVotes = await votes.Where(x => x.VoteType == VoteType.Downvote).CountAsync();
|
||||||
|
var myVote = await votes.FirstOrDefaultAsync(x => x.UserId == user.Id && !x.Deleted);
|
||||||
|
foreach (var epInformation in childRequests.SeasonRequests.OrderBy(x => x.SeasonNumber))
|
||||||
|
{
|
||||||
|
var orderedEpisodes = epInformation.Episodes.OrderBy(x => x.EpisodeNumber).ToList();
|
||||||
|
var episodeString = NewsletterJob.BuildEpisodeList(orderedEpisodes.Select(x => x.EpisodeNumber));
|
||||||
|
finalsb.Append($"Season: {epInformation.SeasonNumber} - Episodes: {episodeString}");
|
||||||
|
finalsb.Append("<br />");
|
||||||
|
}
|
||||||
|
vm.Add(new VoteViewModel
|
||||||
|
{
|
||||||
|
Upvotes = upVotes,
|
||||||
|
Downvotes = downVotes,
|
||||||
|
RequestId = childRequests.Id,
|
||||||
|
RequestType = RequestType.TvShow,
|
||||||
|
Title = r.Title,
|
||||||
|
Image = r.PosterPath,
|
||||||
|
Background = r.Background,
|
||||||
|
Description = finalsb.ToString(),
|
||||||
|
AlreadyVoted = myVote != null,
|
||||||
|
MyVote = myVote?.VoteType ?? VoteType.Downvote
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return vm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IQueryable<Votes> GetVotes(int requestId, RequestType requestType)
|
||||||
|
{
|
||||||
|
return _voteRepository.GetAll().Where(x => x.RequestType == requestType && requestId == x.RequestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<Votes> GetVoteForUser(int requestId, string userId)
|
||||||
|
{
|
||||||
|
return _voteRepository.GetAll().FirstOrDefaultAsync(x => x.RequestId == requestId && x.UserId == userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<VoteEngineResult> UpVote(int requestId, RequestType requestType)
|
||||||
|
{
|
||||||
|
// How many votes does this have?!
|
||||||
|
var currentVotes = GetVotes(requestId, requestType);
|
||||||
|
var voteSettings = await _voteSettings.GetSettingsAsync();
|
||||||
|
|
||||||
|
// Does this user have a downvote? If so we should revert it and make it an upvote
|
||||||
|
var user = await GetUser();
|
||||||
|
|
||||||
|
var currentVote = await GetVoteForUser(requestId, user.Id);
|
||||||
|
if (currentVote != null && currentVote.VoteType == VoteType.Upvote)
|
||||||
|
{
|
||||||
|
return new VoteEngineResult { ErrorMessage = "You have already voted!" };
|
||||||
|
}
|
||||||
|
await RemoveCurrentVote(currentVote);
|
||||||
|
await _movieRequestEngine.SubscribeToRequest(requestId, requestType);
|
||||||
|
|
||||||
|
await _voteRepository.Add(new Votes
|
||||||
|
{
|
||||||
|
Date = DateTime.UtcNow,
|
||||||
|
RequestId = requestId,
|
||||||
|
RequestType = requestType,
|
||||||
|
UserId = user.Id,
|
||||||
|
VoteType = VoteType.Upvote
|
||||||
|
});
|
||||||
|
|
||||||
|
var upVotes = await currentVotes.Where(x => x.VoteType == VoteType.Upvote).CountAsync();
|
||||||
|
var downVotes = -(await currentVotes.Where(x => x.VoteType == VoteType.Downvote).CountAsync());
|
||||||
|
|
||||||
|
var totalVotes = upVotes + downVotes;
|
||||||
|
RequestEngineResult result = null;
|
||||||
|
switch (requestType)
|
||||||
|
{
|
||||||
|
case RequestType.TvShow:
|
||||||
|
if (totalVotes >= voteSettings.TvShowVoteMax)
|
||||||
|
{
|
||||||
|
result = await _tvRequestEngine.ApproveChildRequest(requestId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case RequestType.Movie:
|
||||||
|
if (totalVotes >= voteSettings.MovieVoteMax)
|
||||||
|
{
|
||||||
|
result = await _movieRequestEngine.ApproveMovieById(requestId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case RequestType.Album:
|
||||||
|
if (totalVotes >= voteSettings.MusicVoteMax)
|
||||||
|
{
|
||||||
|
result = await _musicRequestEngine.ApproveAlbumById(requestId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(requestType), requestType, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result != null && !result.Result)
|
||||||
|
{
|
||||||
|
return new VoteEngineResult
|
||||||
|
{
|
||||||
|
ErrorMessage = "Voted succesfully but could not approve movie!"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return new VoteEngineResult
|
||||||
|
{
|
||||||
|
Result = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<VoteEngineResult> DownVote(int requestId, RequestType requestType)
|
||||||
|
{
|
||||||
|
var user = await GetUser();
|
||||||
|
var currentVote = await GetVoteForUser(requestId, user.Id);
|
||||||
|
if (currentVote != null && currentVote.VoteType == VoteType.Downvote)
|
||||||
|
{
|
||||||
|
return new VoteEngineResult { ErrorMessage = "You have already voted!" };
|
||||||
|
}
|
||||||
|
await RemoveCurrentVote(currentVote);
|
||||||
|
|
||||||
|
await _movieRequestEngine.UnSubscribeRequest(requestId, requestType);
|
||||||
|
|
||||||
|
await _voteRepository.Add(new Votes
|
||||||
|
{
|
||||||
|
Date = DateTime.UtcNow,
|
||||||
|
RequestId = requestId,
|
||||||
|
RequestType = requestType,
|
||||||
|
UserId = user.Id,
|
||||||
|
VoteType = VoteType.Downvote
|
||||||
|
});
|
||||||
|
|
||||||
|
return new VoteEngineResult
|
||||||
|
{
|
||||||
|
Result = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RemoveCurrentVote(Votes currentVote)
|
||||||
|
{
|
||||||
|
if (currentVote != null)
|
||||||
|
{
|
||||||
|
await _voteRepository.Delete(currentVote);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,8 +7,8 @@ namespace Ombi.Core.Authentication
|
||||||
public interface IPlexOAuthManager
|
public interface IPlexOAuthManager
|
||||||
{
|
{
|
||||||
Task<string> GetAccessTokenFromPin(int pinId);
|
Task<string> GetAccessTokenFromPin(int pinId);
|
||||||
Task<Uri> GetOAuthUrl(int pinId, string code, string websiteAddress = null);
|
Task<Uri> GetOAuthUrl(string code, string websiteAddress = null);
|
||||||
Task<Uri> GetWizardOAuthUrl(int pinId, string code, string websiteAddress);
|
Task<Uri> GetWizardOAuthUrl(string code, string websiteAddress);
|
||||||
Task<PlexAccount> GetAccount(string accessToken);
|
Task<PlexAccount> GetAccount(string accessToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
18
src/Ombi.Core/Models/UI/VoteViewModel.cs
Normal file
18
src/Ombi.Core/Models/UI/VoteViewModel.cs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
using Ombi.Store.Entities;
|
||||||
|
|
||||||
|
namespace Ombi.Core.Models.UI
|
||||||
|
{
|
||||||
|
public class VoteViewModel
|
||||||
|
{
|
||||||
|
public int RequestId { get; set; }
|
||||||
|
public RequestType RequestType { get; set; }
|
||||||
|
public string Image { get; set; }
|
||||||
|
public string Background { get; set; }
|
||||||
|
public int Upvotes { get; set; }
|
||||||
|
public int Downvotes { get; set; }
|
||||||
|
public string Title { get; set; }
|
||||||
|
public string Description { get; set; }
|
||||||
|
public bool AlreadyVoted { get; set; }
|
||||||
|
public VoteType MyVote { get; set; }
|
||||||
|
}
|
||||||
|
}
|
10
src/Ombi.Core/Models/VoteEngineResult.cs
Normal file
10
src/Ombi.Core/Models/VoteEngineResult.cs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
namespace Ombi.Core.Models
|
||||||
|
{
|
||||||
|
public class VoteEngineResult
|
||||||
|
{
|
||||||
|
public bool Result { get; set; }
|
||||||
|
public string Message { get; set; }
|
||||||
|
public bool IsError => !string.IsNullOrEmpty(ErrorMessage);
|
||||||
|
public string ErrorMessage { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -76,7 +76,35 @@ namespace Ombi.Core.Senders
|
||||||
var result = await _lidarrApi.AddArtist(newArtist, settings.ApiKey, settings.FullUri);
|
var result = await _lidarrApi.AddArtist(newArtist, settings.ApiKey, settings.FullUri);
|
||||||
if (result != null && result.id > 0)
|
if (result != null && result.id > 0)
|
||||||
{
|
{
|
||||||
// Setup the albums
|
// Search for it
|
||||||
|
if (!settings.AddOnly)
|
||||||
|
{
|
||||||
|
// get the album
|
||||||
|
var album = await _lidarrApi.GetAllAlbumsByArtistId(result.id, settings.ApiKey, settings.FullUri);
|
||||||
|
|
||||||
|
var albumToSearch = album.FirstOrDefault(x =>
|
||||||
|
x.foreignAlbumId.Equals(model.ForeignAlbumId, StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
var maxRetryCount = 10; // 5 seconds
|
||||||
|
var currentRetry = 0;
|
||||||
|
while (albumToSearch != null)
|
||||||
|
{
|
||||||
|
if (currentRetry >= maxRetryCount)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
currentRetry++;
|
||||||
|
await Task.Delay(500);
|
||||||
|
album = await _lidarrApi.GetAllAlbumsByArtistId(result.id, settings.ApiKey, settings.FullUri);
|
||||||
|
albumToSearch = album.FirstOrDefault(x =>
|
||||||
|
x.foreignAlbumId.Equals(model.ForeignAlbumId, StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (albumToSearch != null)
|
||||||
|
{
|
||||||
|
await _lidarrApi.AlbumSearch(new[] {albumToSearch.id}, settings.ApiKey, settings.FullUri);
|
||||||
|
}
|
||||||
|
}
|
||||||
return new SenderResult { Message = "Album has been requested!", Sent = true, Success = true };
|
return new SenderResult { Message = "Album has been requested!", Sent = true, Success = true };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -91,6 +91,7 @@ namespace Ombi.DependencyInjection
|
||||||
services.AddTransient<IMusicSender, MusicSender>();
|
services.AddTransient<IMusicSender, MusicSender>();
|
||||||
services.AddTransient<IMassEmailSender, MassEmailSender>();
|
services.AddTransient<IMassEmailSender, MassEmailSender>();
|
||||||
services.AddTransient<IPlexOAuthManager, PlexOAuthManager>();
|
services.AddTransient<IPlexOAuthManager, PlexOAuthManager>();
|
||||||
|
services.AddTransient<IVoteEngine, VoteEngine>();
|
||||||
}
|
}
|
||||||
public static void RegisterHttp(this IServiceCollection services)
|
public static void RegisterHttp(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
|
|
|
@ -14,5 +14,6 @@
|
||||||
public const string RequestMusic = nameof(RequestMusic);
|
public const string RequestMusic = nameof(RequestMusic);
|
||||||
public const string Disabled = nameof(Disabled);
|
public const string Disabled = nameof(Disabled);
|
||||||
public const string ReceivesNewsletter = nameof(ReceivesNewsletter);
|
public const string ReceivesNewsletter = nameof(ReceivesNewsletter);
|
||||||
|
public const string ManageOwnRequests = nameof(ManageOwnRequests);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -5,11 +5,11 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Nunit" Version="3.8.1" />
|
<PackageReference Include="Nunit" Version="3.10.1" />
|
||||||
<PackageReference Include="NUnit.ConsoleRunner" Version="3.7.0" />
|
<PackageReference Include="NUnit.ConsoleRunner" Version="3.9.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="3.8.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="3.10.0" />
|
||||||
<packagereference Include="Microsoft.NET.Test.Sdk" Version="15.8.0"></packagereference>
|
<packagereference Include="Microsoft.NET.Test.Sdk" Version="15.8.0"></packagereference>
|
||||||
<PackageReference Include="Moq" Version="4.7.99" />
|
<PackageReference Include="Moq" Version="4.10.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
@ -170,6 +170,24 @@ namespace Ombi.Notifications.Interfaces
|
||||||
{
|
{
|
||||||
return new NotificationMessageContent { Disabled = true };
|
return new NotificationMessageContent { Disabled = true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (model.UserId.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
if (model.RequestType == RequestType.Movie)
|
||||||
|
{
|
||||||
|
model.UserId = MovieRequest.RequestedUserId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model.RequestType == RequestType.Album)
|
||||||
|
{
|
||||||
|
model.UserId = AlbumRequest.RequestedUserId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model.RequestType == RequestType.TvShow)
|
||||||
|
{
|
||||||
|
model.UserId = TvRequest.RequestedUserId;
|
||||||
|
}
|
||||||
|
}
|
||||||
var parsed = Parse(model, template, agent);
|
var parsed = Parse(model, template, agent);
|
||||||
|
|
||||||
return parsed;
|
return parsed;
|
||||||
|
@ -184,7 +202,7 @@ namespace Ombi.Notifications.Interfaces
|
||||||
protected UserNotificationPreferences GetUserPreference(string userId, NotificationAgent agent)
|
protected UserNotificationPreferences GetUserPreference(string userId, NotificationAgent agent)
|
||||||
{
|
{
|
||||||
return UserNotificationPreferences.GetAll()
|
return UserNotificationPreferences.GetAll()
|
||||||
.FirstOrDefault(x => x.Enabled && x.Agent == agent && x.UserId == userId);
|
.FirstOrDefault(x => x.Agent == agent && x.UserId == userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private NotificationMessageContent Parse(NotificationOptions model, NotificationTemplates template, NotificationAgent agent)
|
private NotificationMessageContent Parse(NotificationOptions model, NotificationTemplates template, NotificationAgent agent)
|
||||||
|
|
|
@ -19,7 +19,7 @@ namespace Ombi.Notifications
|
||||||
LoadIssues(opts);
|
LoadIssues(opts);
|
||||||
if (pref != null)
|
if (pref != null)
|
||||||
{
|
{
|
||||||
UserPreference = pref.Enabled ? pref.Value : string.Empty;
|
UserPreference = pref.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
string title;
|
string title;
|
||||||
|
@ -268,6 +268,7 @@ namespace Ombi.Notifications
|
||||||
{nameof(IssueUser),IssueUser},
|
{nameof(IssueUser),IssueUser},
|
||||||
{nameof(UserName),UserName},
|
{nameof(UserName),UserName},
|
||||||
{nameof(Alias),Alias},
|
{nameof(Alias),Alias},
|
||||||
|
{nameof(UserPreference),UserPreference},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,11 +1,6 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Moq;
|
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using Ombi.Core.Settings;
|
using static Ombi.Schedule.Jobs.Ombi.NewsletterJob;
|
||||||
using Ombi.Schedule.Jobs.Ombi;
|
|
||||||
using Ombi.Settings.Settings.Models;
|
|
||||||
using Ombi.Settings.Settings.Models.Notifications;
|
|
||||||
using Ombi.Store.Entities;
|
|
||||||
|
|
||||||
namespace Ombi.Schedule.Tests
|
namespace Ombi.Schedule.Tests
|
||||||
{
|
{
|
||||||
|
@ -15,17 +10,12 @@ namespace Ombi.Schedule.Tests
|
||||||
[TestCaseSource(nameof(EpisodeListData))]
|
[TestCaseSource(nameof(EpisodeListData))]
|
||||||
public string BuildEpisodeListTest(List<int> episodes)
|
public string BuildEpisodeListTest(List<int> episodes)
|
||||||
{
|
{
|
||||||
var emailSettings = new Mock<ISettingsService<EmailNotificationSettings>>();
|
|
||||||
var customziation = new Mock<ISettingsService<CustomizationSettings>>();
|
|
||||||
var newsletterSettings = new Mock<ISettingsService<NewsletterSettings>>();
|
|
||||||
var newsletter = new NewsletterJob(null, null, null, null, null, null, customziation.Object, emailSettings.Object, null, null, newsletterSettings.Object, null, null, null, null);
|
|
||||||
|
|
||||||
var ep = new List<int>();
|
var ep = new List<int>();
|
||||||
foreach (var i in episodes)
|
foreach (var i in episodes)
|
||||||
{
|
{
|
||||||
ep.Add(i);
|
ep.Add(i);
|
||||||
}
|
}
|
||||||
var result = newsletter.BuildEpisodeList(ep);
|
var result = BuildEpisodeList(ep);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,9 +6,9 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore" Version="2.1.3" />
|
<PackageReference Include="Microsoft.AspNetCore" Version="2.1.3" />
|
||||||
<PackageReference Include="Moq" Version="4.7.99" />
|
<PackageReference Include="Moq" Version="4.10.0" />
|
||||||
<PackageReference Include="Nunit" Version="3.10.1" />
|
<PackageReference Include="Nunit" Version="3.10.1" />
|
||||||
<PackageReference Include="NUnit.ConsoleRunner" Version="3.8.0" />
|
<PackageReference Include="NUnit.ConsoleRunner" Version="3.9.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="3.10.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="3.10.0" />
|
||||||
<packagereference Include="Microsoft.NET.Test.Sdk" Version="15.8.0"></packagereference>
|
<packagereference Include="Microsoft.NET.Test.Sdk" Version="15.8.0"></packagereference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
@ -20,7 +20,8 @@ namespace Ombi.Schedule
|
||||||
IOmbiAutomaticUpdater updater, IEmbyContentSync embySync, IPlexUserImporter userImporter,
|
IOmbiAutomaticUpdater updater, IEmbyContentSync embySync, IPlexUserImporter userImporter,
|
||||||
IEmbyUserImporter embyUserImporter, ISonarrSync cache, ICouchPotatoSync cpCache,
|
IEmbyUserImporter embyUserImporter, ISonarrSync cache, ICouchPotatoSync cpCache,
|
||||||
ISettingsService<JobSettings> jobsettings, ISickRageSync srSync, IRefreshMetadata refresh,
|
ISettingsService<JobSettings> jobsettings, ISickRageSync srSync, IRefreshMetadata refresh,
|
||||||
INewsletterJob newsletter, IPlexRecentlyAddedSync recentlyAddedPlex, ILidarrArtistSync artist)
|
INewsletterJob newsletter, IPlexRecentlyAddedSync recentlyAddedPlex, ILidarrArtistSync artist,
|
||||||
|
IIssuesPurge purge)
|
||||||
{
|
{
|
||||||
_plexContentSync = plexContentSync;
|
_plexContentSync = plexContentSync;
|
||||||
_radarrSync = radarrSync;
|
_radarrSync = radarrSync;
|
||||||
|
@ -36,6 +37,7 @@ namespace Ombi.Schedule
|
||||||
_newsletter = newsletter;
|
_newsletter = newsletter;
|
||||||
_plexRecentlyAddedSync = recentlyAddedPlex;
|
_plexRecentlyAddedSync = recentlyAddedPlex;
|
||||||
_lidarrArtistSync = artist;
|
_lidarrArtistSync = artist;
|
||||||
|
_issuesPurge = purge;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly IPlexContentSync _plexContentSync;
|
private readonly IPlexContentSync _plexContentSync;
|
||||||
|
@ -52,6 +54,7 @@ namespace Ombi.Schedule
|
||||||
private readonly IRefreshMetadata _refreshMetadata;
|
private readonly IRefreshMetadata _refreshMetadata;
|
||||||
private readonly INewsletterJob _newsletter;
|
private readonly INewsletterJob _newsletter;
|
||||||
private readonly ILidarrArtistSync _lidarrArtistSync;
|
private readonly ILidarrArtistSync _lidarrArtistSync;
|
||||||
|
private readonly IIssuesPurge _issuesPurge;
|
||||||
|
|
||||||
public void Setup()
|
public void Setup()
|
||||||
{
|
{
|
||||||
|
@ -66,6 +69,7 @@ namespace Ombi.Schedule
|
||||||
RecurringJob.AddOrUpdate(() => _srSync.Start(), JobSettingsHelper.SickRageSync(s));
|
RecurringJob.AddOrUpdate(() => _srSync.Start(), JobSettingsHelper.SickRageSync(s));
|
||||||
RecurringJob.AddOrUpdate(() => _refreshMetadata.Start(), JobSettingsHelper.RefreshMetadata(s));
|
RecurringJob.AddOrUpdate(() => _refreshMetadata.Start(), JobSettingsHelper.RefreshMetadata(s));
|
||||||
RecurringJob.AddOrUpdate(() => _lidarrArtistSync.CacheContent(), JobSettingsHelper.LidarrArtistSync(s));
|
RecurringJob.AddOrUpdate(() => _lidarrArtistSync.CacheContent(), JobSettingsHelper.LidarrArtistSync(s));
|
||||||
|
RecurringJob.AddOrUpdate(() => _issuesPurge.Start(), JobSettingsHelper.IssuePurge(s));
|
||||||
|
|
||||||
RecurringJob.AddOrUpdate(() => _updater.Update(null), JobSettingsHelper.Updater(s));
|
RecurringJob.AddOrUpdate(() => _updater.Update(null), JobSettingsHelper.Updater(s));
|
||||||
|
|
||||||
|
|
|
@ -98,7 +98,7 @@ namespace Ombi.Schedule.Jobs.Ombi
|
||||||
// Get the Content
|
// Get the Content
|
||||||
var plexContent = _plex.GetAll().Include(x => x.Episodes).AsNoTracking();
|
var plexContent = _plex.GetAll().Include(x => x.Episodes).AsNoTracking();
|
||||||
var embyContent = _emby.GetAll().Include(x => x.Episodes).AsNoTracking();
|
var embyContent = _emby.GetAll().Include(x => x.Episodes).AsNoTracking();
|
||||||
var lidarrContent = _lidarrAlbumRepository.GetAll().AsNoTracking();
|
var lidarrContent = _lidarrAlbumRepository.GetAll().Where(x => x.FullyAvailable).AsNoTracking();
|
||||||
|
|
||||||
var addedLog = _recentlyAddedLog.GetAll();
|
var addedLog = _recentlyAddedLog.GetAll();
|
||||||
var addedPlexMovieLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Plex && x.ContentType == ContentType.Parent).Select(x => x.ContentId);
|
var addedPlexMovieLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Plex && x.ContentType == ContentType.Parent).Select(x => x.ContentId);
|
||||||
|
@ -654,7 +654,7 @@ namespace Ombi.Schedule.Jobs.Ombi
|
||||||
AddInfoTable(sb);
|
AddInfoTable(sb);
|
||||||
|
|
||||||
var title = "";
|
var title = "";
|
||||||
if (!String.IsNullOrEmpty(info.premiered) && info.premiered.Length > 4)
|
if (!string.IsNullOrEmpty(info.premiered) && info.premiered.Length > 4)
|
||||||
{
|
{
|
||||||
title = $"{t.Title} ({info.premiered.Remove(4)})";
|
title = $"{t.Title} ({info.premiered.Remove(4)})";
|
||||||
} else
|
} else
|
||||||
|
@ -715,7 +715,7 @@ namespace Ombi.Schedule.Jobs.Ombi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public string BuildEpisodeList(IEnumerable<int> orderedEpisodes)
|
public static string BuildEpisodeList(IEnumerable<int> orderedEpisodes)
|
||||||
{
|
{
|
||||||
var epSb = new StringBuilder();
|
var epSb = new StringBuilder();
|
||||||
var previousEpisodes = new List<int>();
|
var previousEpisodes = new List<int>();
|
||||||
|
|
10
src/Ombi.Settings/Settings/Models/VoteSettings.cs
Normal file
10
src/Ombi.Settings/Settings/Models/VoteSettings.cs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
namespace Ombi.Settings.Settings.Models
|
||||||
|
{
|
||||||
|
public class VoteSettings : Settings
|
||||||
|
{
|
||||||
|
public bool Enabled { get; set; }
|
||||||
|
public int MovieVoteMax { get; set; }
|
||||||
|
public int MusicVoteMax { get; set; }
|
||||||
|
public int TvShowVoteMax { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,6 +25,7 @@ namespace Ombi.Store.Context
|
||||||
DbSet<TEntity> Set<TEntity>() where TEntity : class;
|
DbSet<TEntity> Set<TEntity>() where TEntity : class;
|
||||||
DbSet<NotificationTemplates> NotificationTemplates { get; set; }
|
DbSet<NotificationTemplates> NotificationTemplates { get; set; }
|
||||||
DbSet<ApplicationConfiguration> ApplicationConfigurations { get; set; }
|
DbSet<ApplicationConfiguration> ApplicationConfigurations { get; set; }
|
||||||
|
DbSet<Votes> Votes { get; set; }
|
||||||
void Seed();
|
void Seed();
|
||||||
DbSet<Audit> Audit { get; set; }
|
DbSet<Audit> Audit { get; set; }
|
||||||
DbSet<MovieRequests> MovieRequests { get; set; }
|
DbSet<MovieRequests> MovieRequests { get; set; }
|
||||||
|
|
|
@ -40,6 +40,7 @@ namespace Ombi.Store.Context
|
||||||
public DbSet<IssueComments> IssueComments { get; set; }
|
public DbSet<IssueComments> IssueComments { get; set; }
|
||||||
public DbSet<RequestLog> RequestLogs { get; set; }
|
public DbSet<RequestLog> RequestLogs { get; set; }
|
||||||
public DbSet<RecentlyAddedLog> RecentlyAddedLogs { get; set; }
|
public DbSet<RecentlyAddedLog> RecentlyAddedLogs { get; set; }
|
||||||
|
public DbSet<Votes> Votes { get; set; }
|
||||||
|
|
||||||
|
|
||||||
public DbSet<Audit> Audit { get; set; }
|
public DbSet<Audit> Audit { get; set; }
|
||||||
|
@ -144,6 +145,16 @@ namespace Ombi.Store.Context
|
||||||
SaveChanges();
|
SaveChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var manageOwnRequestsRole = Roles.Where(x => x.Name == OmbiRoles.ManageOwnRequests);
|
||||||
|
if (!manageOwnRequestsRole.Any())
|
||||||
|
{
|
||||||
|
Roles.Add(new IdentityRole(OmbiRoles.ManageOwnRequests)
|
||||||
|
{
|
||||||
|
NormalizedName = OmbiRoles.ManageOwnRequests.ToUpper()
|
||||||
|
});
|
||||||
|
SaveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
// Make sure we have the API User
|
// Make sure we have the API User
|
||||||
var apiUserExists = Users.Any(x => x.UserName.Equals("Api", StringComparison.CurrentCultureIgnoreCase));
|
var apiUserExists = Users.Any(x => x.UserName.Equals("Api", StringComparison.CurrentCultureIgnoreCase));
|
||||||
if (!apiUserExists)
|
if (!apiUserExists)
|
||||||
|
|
25
src/Ombi.Store/Entities/Votes.cs
Normal file
25
src/Ombi.Store/Entities/Votes.cs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
using System;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace Ombi.Store.Entities
|
||||||
|
{
|
||||||
|
[Table("Votes")]
|
||||||
|
public class Votes : Entity
|
||||||
|
{
|
||||||
|
public int RequestId { get; set; }
|
||||||
|
public VoteType VoteType { get; set; }
|
||||||
|
public RequestType RequestType { get; set; }
|
||||||
|
public string UserId { get; set; }
|
||||||
|
public DateTime Date { get; set; }
|
||||||
|
public bool Deleted { get; set; }
|
||||||
|
|
||||||
|
[ForeignKey(nameof(UserId))]
|
||||||
|
public OmbiUser User { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum VoteType
|
||||||
|
{
|
||||||
|
Upvote = 0,
|
||||||
|
Downvote = 1
|
||||||
|
}
|
||||||
|
}
|
1182
src/Ombi.Store/Migrations/20180928201334_Votes.Designer.cs
generated
Normal file
1182
src/Ombi.Store/Migrations/20180928201334_Votes.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
46
src/Ombi.Store/Migrations/20180928201334_Votes.cs
Normal file
46
src/Ombi.Store/Migrations/20180928201334_Votes.cs
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
namespace Ombi.Store.Migrations
|
||||||
|
{
|
||||||
|
public partial class Votes : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Votes",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
RequestId = table.Column<int>(nullable: false),
|
||||||
|
VoteType = table.Column<int>(nullable: false),
|
||||||
|
RequestType = table.Column<int>(nullable: false),
|
||||||
|
UserId = table.Column<string>(nullable: true),
|
||||||
|
Date = table.Column<DateTime>(nullable: false),
|
||||||
|
Deleted = table.Column<bool>(nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Votes", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Votes_AspNetUsers_UserId",
|
||||||
|
column: x => x.UserId,
|
||||||
|
principalTable: "AspNetUsers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Votes_UserId",
|
||||||
|
table: "Votes",
|
||||||
|
column: "UserId");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Votes");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -916,6 +916,30 @@ namespace Ombi.Store.Migrations
|
||||||
b.ToTable("UserQualityProfiles");
|
b.ToTable("UserQualityProfiles");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Ombi.Store.Entities.Votes", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<DateTime>("Date");
|
||||||
|
|
||||||
|
b.Property<bool>("Deleted");
|
||||||
|
|
||||||
|
b.Property<int>("RequestId");
|
||||||
|
|
||||||
|
b.Property<int>("RequestType");
|
||||||
|
|
||||||
|
b.Property<string>("UserId");
|
||||||
|
|
||||||
|
b.Property<int>("VoteType");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("Votes");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Ombi.Store.Repository.Requests.EpisodeRequests", b =>
|
modelBuilder.Entity("Ombi.Store.Repository.Requests.EpisodeRequests", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
|
@ -1128,6 +1152,13 @@ namespace Ombi.Store.Migrations
|
||||||
.HasForeignKey("UserId");
|
.HasForeignKey("UserId");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Ombi.Store.Entities.Votes", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Ombi.Store.Entities.OmbiUser", "User")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Ombi.Store.Repository.Requests.EpisodeRequests", b =>
|
modelBuilder.Entity("Ombi.Store.Repository.Requests.EpisodeRequests", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Ombi.Store.Repository.Requests.SeasonRequests", "Season")
|
b.HasOne("Ombi.Store.Repository.Requests.SeasonRequests", "Season")
|
||||||
|
|
|
@ -8,10 +8,10 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="2.1.1" />
|
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="2.1.1" />
|
||||||
<PackageReference Include="Moq" Version="4.7.99" />
|
<PackageReference Include="Moq" Version="4.10.0" />
|
||||||
<PackageReference Include="Nunit" Version="3.8.1" />
|
<PackageReference Include="Nunit" Version="3.10.1" />
|
||||||
<PackageReference Include="NUnit.ConsoleRunner" Version="3.7.0" />
|
<PackageReference Include="NUnit.ConsoleRunner" Version="3.9.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="3.8.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="3.10.0" />
|
||||||
<packagereference Include="Microsoft.NET.Test.Sdk" Version="15.8.0"></packagereference>
|
<packagereference Include="Microsoft.NET.Test.Sdk" Version="15.8.0"></packagereference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
14
src/Ombi/Attributes/UserAttribute.cs
Normal file
14
src/Ombi/Attributes/UserAttribute.cs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Ombi.Helpers;
|
||||||
|
|
||||||
|
|
||||||
|
namespace Ombi.Attributes
|
||||||
|
{
|
||||||
|
public class UserAttribute : AuthorizeAttribute
|
||||||
|
{
|
||||||
|
public UserAttribute()
|
||||||
|
{
|
||||||
|
Roles = "ManageOwnRequests";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -54,6 +54,12 @@
|
||||||
<i class="fa fa-user"></i> {{ 'NavigationBar.UserManagement' | translate }}</a>
|
<i class="fa fa-user"></i> {{ 'NavigationBar.UserManagement' | translate }}</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<ul *ngIf="voteEnabled" class="nav navbar-nav">
|
||||||
|
<li id="Vote" [routerLinkActive]="['active']">
|
||||||
|
<a [routerLink]="['/vote']">
|
||||||
|
<i class="fa fa-thumbs-o-up"></i> {{ 'NavigationBar.Vote' | translate }}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
<ul *ngIf="hasRole('Admin') || hasRole('PowerUser')" class="nav navbar-nav donation">
|
<ul *ngIf="hasRole('Admin') || hasRole('PowerUser')" class="nav navbar-nav donation">
|
||||||
<li>
|
<li>
|
||||||
|
@ -88,13 +94,13 @@
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li [routerLinkActive]="['active']" class="dropdown">
|
<li [routerLinkActive]="['active']" class="dropdown">
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">
|
<a href="#" id="userDropdown" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">
|
||||||
<i class="fa fa-user"></i>{{ 'NavigationBar.Welcome' | translate: {username: user.name} }}
|
<i class="fa fa-user"></i>{{ 'NavigationBar.Welcome' | translate: {username: user.name} }}
|
||||||
<span class="caret"></span>
|
<span class="caret"></span>
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu" role="menu">
|
<ul class="dropdown-menu" role="menu">
|
||||||
<li [routerLinkActive]="['active']">
|
<li [routerLinkActive]="['active']">
|
||||||
<a [routerLink]="['/usermanagement/updatedetails']">
|
<a id="updateUserDetails" [routerLink]="['/usermanagement/updatedetails']">
|
||||||
<i class="fa fa-key"></i>{{ 'NavigationBar.UpdateDetails' | translate }}</a>
|
<i class="fa fa-key"></i>{{ 'NavigationBar.UpdateDetails' | translate }}</a>
|
||||||
</li>
|
</li>
|
||||||
<li [routerLinkActive]="['active']">
|
<li [routerLinkActive]="['active']">
|
||||||
|
@ -111,37 +117,37 @@
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu" role="menu">
|
<ul class="dropdown-menu" role="menu">
|
||||||
<li [ngClass]="{'active': 'en' === translate.currentLang}">
|
<li [ngClass]="{'active': 'en' === translate.currentLang}">
|
||||||
<a (click)="translate.use('en')" [translate]="'NavigationBar.Language.English'"></a>
|
<a (click)="translate.use('en')">English</a>
|
||||||
</li>
|
</li>
|
||||||
<li [ngClass]="{'active': 'fr' === translate.currentLang}">
|
<li [ngClass]="{'active': 'fr' === translate.currentLang}">
|
||||||
<a (click)="translate.use('fr')" [translate]="'NavigationBar.Language.French'"></a>
|
<a (click)="translate.use('fr')">Français</a>
|
||||||
</li>
|
</li>
|
||||||
<li [ngClass]="{'active': 'da' === translate.currentLang}">
|
<li [ngClass]="{'active': 'da' === translate.currentLang}">
|
||||||
<a (click)="translate.use('da')" [translate]="'NavigationBar.Language.Danish'"></a>
|
<a (click)="translate.use('da')">Dansk</a>
|
||||||
</li>
|
</li>
|
||||||
<li [ngClass]="{'active': 'de' === translate.currentLang}">
|
<li [ngClass]="{'active': 'de' === translate.currentLang}">
|
||||||
<a (click)="translate.use('de')" [translate]="'NavigationBar.Language.German'"></a>
|
<a (click)="translate.use('de')">Deutsch</a>
|
||||||
</li>
|
</li>
|
||||||
<li [ngClass]="{'active': 'it' === translate.currentLang}">
|
<li [ngClass]="{'active': 'it' === translate.currentLang}">
|
||||||
<a (click)="translate.use('it')" [translate]="'NavigationBar.Language.Italian'"></a>
|
<a (click)="translate.use('it')">Italiano</a>
|
||||||
</li>
|
</li>
|
||||||
<li [ngClass]="{'active': 'es' === translate.currentLang}">
|
<li [ngClass]="{'active': 'es' === translate.currentLang}">
|
||||||
<a (click)="translate.use('es')" [translate]="'NavigationBar.Language.Spanish'"></a>
|
<a (click)="translate.use('es')">Español</a>
|
||||||
</li>
|
</li>
|
||||||
<li [ngClass]="{'active': 'nl' === translate.currentLang}">
|
<li [ngClass]="{'active': 'nl' === translate.currentLang}">
|
||||||
<a (click)="translate.use('nl')" [translate]="'NavigationBar.Language.Dutch'"></a>
|
<a (click)="translate.use('nl')">Nederlands</a>
|
||||||
</li>
|
</li>
|
||||||
<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')">Norsk</a>
|
||||||
</li>
|
</li>
|
||||||
<li [ngClass]="{'active': 'pt' === translate.currentLang}">
|
<li [ngClass]="{'active': 'pt' === translate.currentLang}">
|
||||||
<a (click)="translate.use('pt')" [translate]="'NavigationBar.Language.BrazillianPortuguese'"></a>
|
<a (click)="translate.use('pt')">Português (Brasil)</a>
|
||||||
</li>
|
</li>
|
||||||
<li [ngClass]="{'active': 'pl' === translate.currentLang}">
|
<li [ngClass]="{'active': 'pl' === translate.currentLang}">
|
||||||
<a (click)="translate.use('pl')" [translate]="'NavigationBar.Language.Polish'"></a>
|
<a (click)="translate.use('pl')">Polski</a>
|
||||||
</li>
|
</li>
|
||||||
<li [ngClass]="{'active': 'sv' === translate.currentLang}">
|
<li [ngClass]="{'active': 'sv' === translate.currentLang}">
|
||||||
<a (click)="translate.use('sv')" [translate]="'NavigationBar.Language.Swedish'"></a>
|
<a (click)="translate.use('sv')">Svenska</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -23,6 +23,7 @@ export class AppComponent implements OnInit {
|
||||||
public updateAvailable: boolean;
|
public updateAvailable: boolean;
|
||||||
public currentUrl: string;
|
public currentUrl: string;
|
||||||
public userAccessToken: string;
|
public userAccessToken: string;
|
||||||
|
public voteEnabled = false;
|
||||||
|
|
||||||
private checkedForUpdate: boolean;
|
private checkedForUpdate: boolean;
|
||||||
|
|
||||||
|
@ -54,6 +55,7 @@ export class AppComponent implements OnInit {
|
||||||
|
|
||||||
this.settingsService.getCustomization().subscribe(x => this.customizationSettings = x);
|
this.settingsService.getCustomization().subscribe(x => this.customizationSettings = x);
|
||||||
this.settingsService.issueEnabled().subscribe(x => this.issuesEnabled = x);
|
this.settingsService.issueEnabled().subscribe(x => this.issuesEnabled = x);
|
||||||
|
this.settingsService.voteEnabled().subscribe(x => this.voteEnabled =x);
|
||||||
|
|
||||||
this.router.events.subscribe((event: NavigationStart) => {
|
this.router.events.subscribe((event: NavigationStart) => {
|
||||||
this.currentUrl = event.url;
|
this.currentUrl = event.url;
|
||||||
|
|
|
@ -15,7 +15,7 @@ import { TranslateLoader, TranslateModule } from "@ngx-translate/core";
|
||||||
import { TranslateHttpLoader } from "@ngx-translate/http-loader";
|
import { TranslateHttpLoader } from "@ngx-translate/http-loader";
|
||||||
import { CookieService } from "ng2-cookies";
|
import { CookieService } from "ng2-cookies";
|
||||||
import { GrowlModule } from "primeng/components/growl/growl";
|
import { GrowlModule } from "primeng/components/growl/growl";
|
||||||
import { ButtonModule, CaptchaModule, ConfirmationService, ConfirmDialogModule, DataTableModule, DialogModule, SharedModule, SidebarModule, TooltipModule } from "primeng/primeng";
|
import { ButtonModule, CaptchaModule, ConfirmationService, ConfirmDialogModule, DataTableModule, DialogModule, OverlayPanelModule, SharedModule, SidebarModule, TooltipModule } from "primeng/primeng";
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import { AppComponent } from "./app.component";
|
import { AppComponent } from "./app.component";
|
||||||
|
@ -55,6 +55,7 @@ const routes: Routes = [
|
||||||
{ loadChildren: "./requests/requests.module#RequestsModule", path: "requests" },
|
{ loadChildren: "./requests/requests.module#RequestsModule", path: "requests" },
|
||||||
{ loadChildren: "./search/search.module#SearchModule", path: "search" },
|
{ loadChildren: "./search/search.module#SearchModule", path: "search" },
|
||||||
{ loadChildren: "./recentlyAdded/recentlyAdded.module#RecentlyAddedModule", path: "recentlyadded" },
|
{ loadChildren: "./recentlyAdded/recentlyAdded.module#RecentlyAddedModule", path: "recentlyadded" },
|
||||||
|
{ loadChildren: "./vote/vote.module#VoteModule", path: "vote" },
|
||||||
];
|
];
|
||||||
|
|
||||||
// AoT requires an exported function for factories
|
// AoT requires an exported function for factories
|
||||||
|
@ -97,6 +98,7 @@ export function JwtTokenGetter() {
|
||||||
CaptchaModule,
|
CaptchaModule,
|
||||||
TooltipModule,
|
TooltipModule,
|
||||||
ConfirmDialogModule,
|
ConfirmDialogModule,
|
||||||
|
OverlayPanelModule,
|
||||||
CommonModule,
|
CommonModule,
|
||||||
JwtModule.forRoot({
|
JwtModule.forRoot({
|
||||||
config: {
|
config: {
|
||||||
|
|
|
@ -59,6 +59,7 @@ export interface INewsletterNotificationSettings extends INotificationSettings {
|
||||||
notificationTemplate: INotificationTemplates;
|
notificationTemplate: INotificationTemplates;
|
||||||
disableMovies: boolean;
|
disableMovies: boolean;
|
||||||
disableTv: boolean;
|
disableTv: boolean;
|
||||||
|
disableMusic: boolean;
|
||||||
externalEmails: string[];
|
externalEmails: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
export enum RequestType {
|
export enum RequestType {
|
||||||
movie = 1,
|
movie = 1,
|
||||||
tvShow = 2,
|
tvShow = 2,
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NEW WORLD
|
// NEW WORLD
|
||||||
|
|
|
@ -228,3 +228,10 @@ export interface IJobSettingsViewModel {
|
||||||
result: boolean;
|
result: boolean;
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IVoteSettings extends ISettings {
|
||||||
|
enabled: boolean;
|
||||||
|
movieVoteMax: number;
|
||||||
|
musicVoteMax: number;
|
||||||
|
tvShowVoteMax: number;
|
||||||
|
}
|
||||||
|
|
30
src/Ombi/ClientApp/app/interfaces/IVote.ts
Normal file
30
src/Ombi/ClientApp/app/interfaces/IVote.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
export interface IVoteViewModel {
|
||||||
|
requestId: number;
|
||||||
|
requestType: RequestTypes;
|
||||||
|
image: string;
|
||||||
|
background: string;
|
||||||
|
upvotes: number;
|
||||||
|
downvotes: number;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
alreadyVoted: boolean;
|
||||||
|
myVote: VoteType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IVoteEngineResult {
|
||||||
|
result: boolean;
|
||||||
|
message: string;
|
||||||
|
isError: boolean;
|
||||||
|
errorMessage: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum RequestTypes {
|
||||||
|
TvShow = 0,
|
||||||
|
Movie = 1,
|
||||||
|
Album = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum VoteType {
|
||||||
|
Upvote = 0,
|
||||||
|
Downvote = 1,
|
||||||
|
}
|
|
@ -16,3 +16,4 @@ export * from "./IIssues";
|
||||||
export * from "./IRecentlyAdded";
|
export * from "./IRecentlyAdded";
|
||||||
export * from "./ILidarr";
|
export * from "./ILidarr";
|
||||||
export * from "./ISearchMusicResult";
|
export * from "./ISearchMusicResult";
|
||||||
|
export * from "./IVote";
|
||||||
|
|
|
@ -63,7 +63,7 @@
|
||||||
|
|
||||||
<div class="messages msg_sent"> <i *ngIf="isAdmin" style="float:right;" class="fa fa-times" aria-hidden="true" (click)="deleteComment(comment.id)"></i>
|
<div class="messages msg_sent"> <i *ngIf="isAdmin" style="float:right;" class="fa fa-times" aria-hidden="true" (click)="deleteComment(comment.id)"></i>
|
||||||
<p>{{comment.comment}}</p>
|
<p>{{comment.comment}}</p>
|
||||||
<time>{{comment.username}} • {{comment.date | date:'short'}}</time>
|
<time>{{comment.username}} • {{comment.date | amLocal | amDateFormat: 'l LT'}}</time>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -33,7 +33,7 @@ include the remember me checkbox
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-success" type="submit" [translate]="'Login.SignInButton'"></button>
|
<button class="btn btn-success" type="submit" data-test='signinbtn' [translate]="'Login.SignInButton'"></button>
|
||||||
<a [routerLink]="['/reset']" class="forgot-password col-md-12">
|
<a [routerLink]="['/reset']" class="forgot-password col-md-12">
|
||||||
<b [translate]="'Login.ForgottenPassword'"></b>
|
<b [translate]="'Login.ForgottenPassword'"></b>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -58,7 +58,7 @@
|
||||||
<div class="col-sm-5 small-padding">
|
<div class="col-sm-5 small-padding">
|
||||||
<div>
|
<div>
|
||||||
<a href="http://www.imdb.com/title/{{request.imdbId}}/" target="_blank">
|
<a href="http://www.imdb.com/title/{{request.imdbId}}/" target="_blank">
|
||||||
<h4 class="request-title">{{request.title}} ({{request.releaseDate | date: 'yyyy'}})</h4>
|
<h4 class="request-title">{{request.title}} ({{request.releaseDate | amLocal | amDateFormat: 'YYYY'}})</h4>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
|
@ -93,9 +93,9 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div id="releaseDate">{{ 'Requests.TheatricalRelease' | translate: {date: request.releaseDate | date: 'mediumDate'} }}</div>
|
<div id="releaseDate">{{ 'Requests.TheatricalRelease' | translate: {date: request.releaseDate | amLocal | amDateFormat: 'LL'} }}</div>
|
||||||
<div *ngIf="request.digitalReleaseDate" id="digitalReleaseDate">{{ 'Requests.DigitalRelease' | translate: {date: request.digitalReleaseDate | date: 'mediumDate'} }}</div>
|
<div *ngIf="request.digitalReleaseDate" id="digitalReleaseDate">{{ 'Requests.DigitalRelease' | translate: {date: request.digitalReleaseDate | amLocal | amDateFormat: 'LL'} }}</div>
|
||||||
<div id="requestedDate">{{ 'Requests.RequestDate' | translate }} {{request.requestedDate | date}}</div>
|
<div id="requestedDate">{{ 'Requests.RequestDate' | translate }} {{request.requestedDate | amLocal | amDateFormat: 'LL'}}</div>
|
||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="isAdmin">
|
<div *ngIf="isAdmin">
|
||||||
|
@ -129,7 +129,7 @@
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!--Radarr Root Folder-->
|
<!--Radarr Root Folder-->
|
||||||
<div *ngIf="radarrRootFolders" class="btn-group btn-split" id="rootFolderBtn">
|
<div *ngIf="radarrRootFolders?.length > 1" class="btn-group btn-split" id="rootFolderBtn">
|
||||||
<button type="button" class="btn btn-sm btn-warning-outline">
|
<button type="button" class="btn btn-sm btn-warning-outline">
|
||||||
<i class="fa fa-plus"></i> {{ 'Requests.ChangeRootFolder' | translate }}
|
<i class="fa fa-plus"></i> {{ 'Requests.ChangeRootFolder' | translate }}
|
||||||
</button>
|
</button>
|
||||||
|
@ -145,7 +145,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!--Radarr Quality Profiles -->
|
<!--Radarr Quality Profiles -->
|
||||||
<div *ngIf="radarrProfiles" class="btn-group btn-split" id="changeQualityBtn">
|
<div *ngIf="radarrProfiles?.length > 1" class="btn-group btn-split" id="changeQualityBtn">
|
||||||
<button type="button" class="btn btn-sm btn-warning-outline">
|
<button type="button" class="btn btn-sm btn-warning-outline">
|
||||||
<i class="fa fa-plus"></i> {{ 'Requests.ChangeQualityProfile' | translate }}
|
<i class="fa fa-plus"></i> {{ 'Requests.ChangeQualityProfile' | translate }}
|
||||||
</button>
|
</button>
|
||||||
|
@ -166,11 +166,7 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form id="removeBtn">
|
|
||||||
<button (click)="removeRequest(request)" style="text-align: right" class="btn btn-sm btn-danger-outline delete">
|
|
||||||
<i class="fa fa-minus"></i> {{ 'Requests.Remove' | translate }}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<form id="markBtnGroup">
|
<form id="markBtnGroup">
|
||||||
<button id="unavailableBtn" *ngIf="request.available" (click)="changeAvailability(request, false)" style="text-align: right"
|
<button id="unavailableBtn" *ngIf="request.available" (click)="changeAvailability(request, false)" style="text-align: right"
|
||||||
|
@ -185,6 +181,13 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div *ngIf="isAdmin || isRequestUser(request)">
|
||||||
|
<form id="removeBtn">
|
||||||
|
<button (click)="removeRequest(request)" style="text-align: right" class="btn btn-sm btn-danger-outline delete">
|
||||||
|
<i class="fa fa-minus"></i> {{ 'Requests.Remove' | translate }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="dropdown" *ngIf="issueCategories && issuesEnabled" id="issuesBtn">
|
<div class="dropdown" *ngIf="issueCategories && issuesEnabled" id="issuesBtn">
|
||||||
<button class="btn btn-sm btn-primary-outline dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true"
|
<button class="btn btn-sm btn-primary-outline dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true"
|
||||||
|
|
|
@ -218,6 +218,13 @@ export class MovieRequestsComponent implements OnInit {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isRequestUser(request: IMovieRequests) {
|
||||||
|
if (request.requestedUser.userName === this.auth.claims().name) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private filterActiveStyle(el: any) {
|
private filterActiveStyle(el: any) {
|
||||||
el = el.toElement || el.relatedTarget || el.target || el.srcElement;
|
el = el.toElement || el.relatedTarget || el.target || el.srcElement;
|
||||||
|
|
||||||
|
@ -365,4 +372,5 @@ export class MovieRequestsComponent implements OnInit {
|
||||||
req.backgroundPath = this.sanitizer.bypassSecurityTrustStyle
|
req.backgroundPath = this.sanitizer.bypassSecurityTrustStyle
|
||||||
("url(" + "https://image.tmdb.org/t/p/w1280" + req.background + ")");
|
("url(" + "https://image.tmdb.org/t/p/w1280" + req.background + ")");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -97,8 +97,8 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div id="releaseDate">{{ 'Requests.ReleaseDate' | translate: {date: request.releaseDate | date: 'mediumDate'} }}</div>
|
<div id="releaseDate">{{ 'Requests.ReleaseDate' | translate: {date: request.releaseDate | amLocal | amDateFormat: 'LL'} }}</div>
|
||||||
<div id="requestedDate">{{ 'Requests.RequestDate' | translate }} {{request.requestedDate | date: 'mediumDate'}}</div>
|
<div id="requestedDate">{{ 'Requests.RequestDate' | translate }} {{request.requestedDate | amLocal | amDateFormat: 'LL'}}</div>
|
||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
<!-- <div *ngIf="isAdmin">
|
<!-- <div *ngIf="isAdmin">
|
||||||
|
@ -169,11 +169,7 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form id="removeBtn" class="col-md-6">
|
|
||||||
<button (click)="removeRequest(request)" style="text-align: right" class="btn btn-sm btn-danger-outline delete">
|
|
||||||
<i class="fa fa-minus"></i> {{ 'Requests.Remove' | translate }}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<form id="markBtnGroup">
|
<form id="markBtnGroup">
|
||||||
<button id="unavailableBtn" *ngIf="request.available" (click)="changeAvailability(request, false)" style="text-align: right"
|
<button id="unavailableBtn" *ngIf="request.available" (click)="changeAvailability(request, false)" style="text-align: right"
|
||||||
|
@ -188,6 +184,13 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div *ngIf="isAdmin || isRequestUser(request)">
|
||||||
|
<form id="removeBtn">
|
||||||
|
<button (click)="removeRequest(request)" style="text-align: right" class="btn btn-sm btn-danger-outline delete">
|
||||||
|
<i class="fa fa-minus"></i> {{ 'Requests.Remove' | translate }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="dropdown" *ngIf="issueCategories && issuesEnabled" id="issuesBtn">
|
<div class="dropdown" *ngIf="issueCategories && issuesEnabled" id="issuesBtn">
|
||||||
<button class="btn btn-sm btn-primary-outline dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true"
|
<button class="btn btn-sm btn-primary-outline dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true"
|
||||||
|
|
|
@ -198,6 +198,13 @@ export class MusicRequestsComponent implements OnInit {
|
||||||
this.loadInit();
|
this.loadInit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isRequestUser(request: IAlbumRequest) {
|
||||||
|
if (request.requestedUser.userName === this.auth.claims().name) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// public subscribe(request: IAlbumRequest) {
|
// public subscribe(request: IAlbumRequest) {
|
||||||
// request.subscribed = true;
|
// request.subscribed = true;
|
||||||
// this.requestService.subscribeToMovie(request.id)
|
// this.requestService.subscribeToMovie(request.id)
|
||||||
|
@ -348,4 +355,5 @@ export class MusicRequestsComponent implements OnInit {
|
||||||
req.background = this.sanitizer.bypassSecurityTrustStyle
|
req.background = this.sanitizer.bypassSecurityTrustStyle
|
||||||
("url(" + req.cover + ")");
|
("url(" + req.cover + ")");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,8 +23,10 @@
|
||||||
|
|
||||||
<button id="denyBtn" *ngIf="!child.denied" type="button" (click)="deny(child)" class="btn btn-sm btn-danger-outline deny"><i class="fa fa-times"></i> {{ 'Requests.Deny' | translate }}</button>
|
<button id="denyBtn" *ngIf="!child.denied" type="button" (click)="deny(child)" class="btn btn-sm btn-danger-outline deny"><i class="fa fa-times"></i> {{ 'Requests.Deny' | translate }}</button>
|
||||||
|
|
||||||
<button id="removeBtn" type="button" (click)="removeRequest(child)" class="btn btn-sm btn-danger-outline deny"><i class="fa fa-times"></i> {{ 'Requests.Remove' | translate }}</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div *ngIf="isAdmin || isRequestUser(child)">
|
||||||
|
<button id="removeBtn" type="button" (click)="removeRequest(child)" class="btn btn-sm btn-danger-outline deny"><i class="fa fa-times"></i> {{ 'Requests.Remove' | translate }}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -71,7 +73,7 @@
|
||||||
{{ep.title}}
|
{{ep.title}}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ep.airDate | date: 'dd/MM/yyyy' }}
|
{{ep.airDate | amLocal | amDateFormat: 'L' }}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span *ngIf="child.denied" class="label label-danger" id="deniedLabel" [translate]="'Common.Denied'"></span>
|
<span *ngIf="child.denied" class="label label-danger" id="deniedLabel" [translate]="'Common.Denied'"></span>
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { NotificationService, RequestService } from "../services";
|
||||||
export class TvRequestChildrenComponent {
|
export class TvRequestChildrenComponent {
|
||||||
@Input() public childRequests: IChildRequests[];
|
@Input() public childRequests: IChildRequests[];
|
||||||
@Input() public isAdmin: boolean;
|
@Input() public isAdmin: boolean;
|
||||||
|
@Input() public currentUser: string;
|
||||||
|
|
||||||
@Output() public requestDeleted = new EventEmitter<number>();
|
@Output() public requestDeleted = new EventEmitter<number>();
|
||||||
|
|
||||||
|
@ -110,10 +111,18 @@ export class TvRequestChildrenComponent {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isRequestUser(request: IChildRequests) {
|
||||||
|
if (request.requestedUser.userName === this.currentUser) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private removeRequestFromUi(key: IChildRequests) {
|
private removeRequestFromUi(key: IChildRequests) {
|
||||||
const index = this.childRequests.indexOf(key, 0);
|
const index = this.childRequests.indexOf(key, 0);
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
this.childRequests.splice(index, 1);
|
this.childRequests.splice(index, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
<div class="col-sm-5 small-padding">
|
<div class="col-sm-5 small-padding">
|
||||||
<div>
|
<div>
|
||||||
<a href="http://www.imdb.com/title/{{node.imdbId}}/" target="_blank">
|
<a href="http://www.imdb.com/title/{{node.imdbId}}/" target="_blank">
|
||||||
<h4 class="request-title">{{node.title}} ({{node.releaseDate | date: 'yyyy'}})</h4>
|
<h4 class="request-title">{{node.title}} ({{node.releaseDate | amLocal | amDateFormat: 'YYYY'}})</h4>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
|
@ -32,7 +32,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div>Release Date: {{node.releaseDate | date}}</div>
|
<div>Release Date: {{node.releaseDate | amLocal | amDateFormat: 'LL'}}</div>
|
||||||
<div *ngIf="isAdmin">
|
<div *ngIf="isAdmin">
|
||||||
<div *ngIf="node.qualityOverrideTitle" class="quality-override">{{ 'Requests.QualityOverride' | translate }}
|
<div *ngIf="node.qualityOverrideTitle" class="quality-override">{{ 'Requests.QualityOverride' | translate }}
|
||||||
<span>{{node.qualityOverrideTitle}} </span>
|
<span>{{node.qualityOverrideTitle}} </span>
|
||||||
|
@ -50,7 +50,7 @@
|
||||||
<i class="fa fa-plus"></i> View</button>
|
<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?.length > 1" class="btn-group btn-split" id="rootFolderBtn">
|
||||||
<button type="button" class="btn btn-sm btn-warning-outline">
|
<button type="button" class="btn btn-sm btn-warning-outline">
|
||||||
<i class="fa fa-plus"></i> {{ 'Requests.ChangeRootFolder' | translate }}
|
<i class="fa fa-plus"></i> {{ 'Requests.ChangeRootFolder' | translate }}
|
||||||
</button>
|
</button>
|
||||||
|
@ -66,7 +66,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!--Sonarr Quality Profiles -->
|
<!--Sonarr Quality Profiles -->
|
||||||
<div *ngIf="sonarrProfiles" class="btn-group btn-split" id="changeQualityBtn">
|
<div *ngIf="sonarrProfiles?.length > 1" class="btn-group btn-split" id="changeQualityBtn">
|
||||||
<button type="button" class="btn btn-sm btn-warning-outline">
|
<button type="button" class="btn btn-sm btn-warning-outline">
|
||||||
<i class="fa fa-plus"></i> {{ 'Requests.ChangeQualityProfile' | translate }}
|
<i class="fa fa-plus"></i> {{ 'Requests.ChangeQualityProfile' | translate }}
|
||||||
</button>
|
</button>
|
||||||
|
@ -99,7 +99,7 @@
|
||||||
</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.open">
|
<div *ngIf="node.open">
|
||||||
<tvrequests-children [childRequests]="node.childRequests" [isAdmin]="isAdmin" (requestDeleted)="childRequestDeleted($event)"></tvrequests-children>
|
<tvrequests-children [childRequests]="node.childRequests" [isAdmin]="isAdmin" [currentUser]="currentUser" (requestDeleted)="childRequestDeleted($event)"></tvrequests-children>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
|
|
|
@ -20,6 +20,7 @@ export class TvRequestsComponent implements OnInit {
|
||||||
public searchChanged = new Subject<string>();
|
public searchChanged = new Subject<string>();
|
||||||
public searchText: string;
|
public searchText: string;
|
||||||
public isAdmin: boolean;
|
public isAdmin: boolean;
|
||||||
|
public currentUser: string;
|
||||||
public showChildDialogue = false; // This is for the child modal popup
|
public showChildDialogue = false; // This is for the child modal popup
|
||||||
public selectedSeason: ITvRequests;
|
public selectedSeason: ITvRequests;
|
||||||
public defaultPoster: string;
|
public defaultPoster: string;
|
||||||
|
@ -48,6 +49,7 @@ export class TvRequestsComponent implements OnInit {
|
||||||
private readonly platformLocation: PlatformLocation) {
|
private readonly platformLocation: PlatformLocation) {
|
||||||
|
|
||||||
this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser");
|
this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser");
|
||||||
|
this.currentUser = this.auth.claims().name;
|
||||||
if (this.isAdmin) {
|
if (this.isAdmin) {
|
||||||
this.sonarrService.getQualityProfilesWithoutSettings()
|
this.sonarrService.getQualityProfilesWithoutSettings()
|
||||||
.subscribe(x => this.sonarrProfiles = x);
|
.subscribe(x => this.sonarrProfiles = x);
|
||||||
|
|
|
@ -41,11 +41,11 @@
|
||||||
<div class="col-sm-8 small-padding">
|
<div class="col-sm-8 small-padding">
|
||||||
<div>
|
<div>
|
||||||
<a href="https://www.themoviedb.org/movie/{{result.id}}/" target="_blank">
|
<a href="https://www.themoviedb.org/movie/{{result.id}}/" target="_blank">
|
||||||
<h4>{{result.title}} ({{result.releaseDate | date: 'yyyy'}})</h4>
|
<h4>{{result.title}} ({{result.releaseDate | amLocal | amDateFormat: 'YYYY'}})</h4>
|
||||||
</a>
|
</a>
|
||||||
<span class="tags">
|
<span class="tags">
|
||||||
<span *ngIf="result.releaseDate" class="label label-info" id="releaseDateLabel" target="_blank">{{ 'Search.TheatricalRelease' | translate: {date: result.releaseDate | date: 'mediumDate'} }}</span>
|
<span *ngIf="result.releaseDate" class="label label-info" id="releaseDateLabel" target="_blank">{{ 'Search.TheatricalRelease' | translate: {date: result.releaseDate | amLocal | amDateFormat: 'LL'} }}</span>
|
||||||
<span *ngIf="result.digitalReleaseDate" class="label label-info" id="releaseDateLabel" target="_blank">{{ 'Search.DigitalDate' | translate: {date: result.digitalReleaseDate | date: 'mediumDate'} }}</span>
|
<span *ngIf="result.digitalReleaseDate" class="label label-info" id="releaseDateLabel" target="_blank">{{ 'Search.DigitalDate' | translate: {date: result.digitalReleaseDate | amLocal | amDateFormat: 'LL'} }}</span>
|
||||||
|
|
||||||
<a *ngIf="result.homepage" href="{{result.homepage}}" id="homePageLabel" target="_blank"><span class="label label-info" [translate]="'Search.Movies.HomePage'"></span></a>
|
<a *ngIf="result.homepage" href="{{result.homepage}}" id="homePageLabel" target="_blank"><span class="label label-info" [translate]="'Search.Movies.HomePage'"></span></a>
|
||||||
|
|
||||||
|
|
|
@ -67,10 +67,10 @@
|
||||||
<div class="col-sm-8">
|
<div class="col-sm-8">
|
||||||
<div>
|
<div>
|
||||||
<a href="https://www.themoviedb.org/movie/{{result.id}}/" target="_blank">
|
<a href="https://www.themoviedb.org/movie/{{result.id}}/" target="_blank">
|
||||||
<h4>{{result.title}} ({{result.releaseDate | date: 'yyyy'}})</h4>
|
<h4>{{result.title}} ({{result.releaseDate | amLocal | amDateFormat: 'YYYY'}})</h4>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<span *ngIf="result.releaseDate" class="label label-info" target="_blank">Release Date: {{result.releaseDate | date: 'dd/MM/yyyy'}}</span>
|
<span *ngIf="result.releaseDate" class="label label-info" target="_blank">Release Date: {{result.releaseDate | amLocal | amDateFormat: 'L'}}</span>
|
||||||
|
|
||||||
<a *ngIf="result.homepage" href="{{result.homepage}}" id="homepageLabel" target="_blank">
|
<a *ngIf="result.homepage" href="{{result.homepage}}" id="homepageLabel" target="_blank">
|
||||||
<span class="label label-info">HomePage</span>
|
<span class="label label-info">HomePage</span>
|
||||||
|
|
|
@ -54,7 +54,7 @@
|
||||||
|
|
||||||
|
|
||||||
<ng-template [ngIf]="result.releaseDate">
|
<ng-template [ngIf]="result.releaseDate">
|
||||||
<span class="label label-info" id="availableLabel">Release Date: {{result.releaseDate | date:'yyyy-MM-dd'}}</span>
|
<span class="label label-info" id="availableLabel">Release Date: {{result.releaseDate | amLocal | amDateFormat: 'L'}}</span>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template [ngIf]="result.rating">
|
<ng-template [ngIf]="result.rating">
|
||||||
<span class="label label-info" id="availableLabel">{{result.rating}}/10</span>
|
<span class="label label-info" id="availableLabel">{{result.rating}}/10</span>
|
||||||
|
|
|
@ -43,7 +43,7 @@
|
||||||
{{ep.title}}
|
{{ep.title}}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ep.airDate | date: 'dd/MM/yyyy' }}
|
{{ep.airDate | amLocal | amDateFormat: 'L' }}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<ng-template [ngIf]="ep.available"><span class="label label-success" id="availableLabel">Available</span></ng-template>
|
<ng-template [ngIf]="ep.available"><span class="label label-success" id="availableLabel">Available</span></ng-template>
|
||||||
|
|
|
@ -57,7 +57,7 @@
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
<a *ngIf="node.imdbId" href="{{node.imdbId}}" target="_blank">
|
<a *ngIf="node.imdbId" href="{{node.imdbId}}" target="_blank">
|
||||||
<h4>{{node.title}} ({{node.firstAired | date: 'yyyy'}})</h4>
|
<h4>{{node.title}} ({{node.firstAired | amLocal | amDateFormat: 'YYYY'}})</h4>
|
||||||
|
|
||||||
</a>
|
</a>
|
||||||
<span class="tags">
|
<span class="tags">
|
||||||
|
@ -72,7 +72,7 @@
|
||||||
<span *ngIf="node.status" class="label label-primary" id="statusLabel" target="_blank">{{node.status}}</span>
|
<span *ngIf="node.status" class="label label-primary" id="statusLabel" target="_blank">{{node.status}}</span>
|
||||||
|
|
||||||
|
|
||||||
<span *ngIf="node.firstAired" class="label label-info" target="_blank" id="airDateLabel">{{ 'Search.TvShows.AirDate' | translate }} {{node.firstAired | date: 'dd/MM/yyyy'}}</span>
|
<span *ngIf="node.firstAired" class="label label-info" target="_blank" id="airDateLabel">{{ 'Search.TvShows.AirDate' | translate }} {{node.firstAired | amLocal | amDateFormat: 'L'}}</span>
|
||||||
|
|
||||||
<span *ngIf="node.network" class="label label-info" id="networkLabel" target="_blank">{{node.network}}</span>
|
<span *ngIf="node.network" class="label label-info" id="networkLabel" target="_blank">{{node.network}}</span>
|
||||||
|
|
||||||
|
|
|
@ -14,3 +14,4 @@ export * from "./issues.service";
|
||||||
export * from "./mobile.service";
|
export * from "./mobile.service";
|
||||||
export * from "./notificationMessage.service";
|
export * from "./notificationMessage.service";
|
||||||
export * from "./recentlyAdded.service";
|
export * from "./recentlyAdded.service";
|
||||||
|
export * from "./vote.service";
|
||||||
|
|
|
@ -34,6 +34,7 @@ import {
|
||||||
IThemes,
|
IThemes,
|
||||||
IUpdateSettings,
|
IUpdateSettings,
|
||||||
IUserManagementSettings,
|
IUserManagementSettings,
|
||||||
|
IVoteSettings,
|
||||||
} from "../interfaces";
|
} from "../interfaces";
|
||||||
|
|
||||||
import { ServiceHelpers } from "./service.helpers";
|
import { ServiceHelpers } from "./service.helpers";
|
||||||
|
@ -284,6 +285,18 @@ export class SettingsService extends ServiceHelpers {
|
||||||
.post<boolean>(`${this.url}/issues`, JSON.stringify(settings), {headers: this.headers});
|
.post<boolean>(`${this.url}/issues`, JSON.stringify(settings), {headers: this.headers});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getVoteSettings(): Observable<IVoteSettings> {
|
||||||
|
return this.http.get<IVoteSettings>(`${this.url}/vote`, {headers: this.headers});
|
||||||
|
}
|
||||||
|
|
||||||
|
public voteEnabled(): Observable<boolean> {
|
||||||
|
return this.http.get<boolean>(`${this.url}/voteenabled`, {headers: this.headers});
|
||||||
|
}
|
||||||
|
|
||||||
|
public saveVoteSettings(settings: IVoteSettings): Observable<boolean> {
|
||||||
|
return this.http.post<boolean>(`${this.url}/vote`, JSON.stringify(settings), {headers: this.headers});
|
||||||
|
}
|
||||||
|
|
||||||
public getNewsletterSettings(): Observable<INewsletterNotificationSettings> {
|
public getNewsletterSettings(): Observable<INewsletterNotificationSettings> {
|
||||||
return this.http.get<INewsletterNotificationSettings>(`${this.url}/notifications/newsletter`, {headers: this.headers});
|
return this.http.get<INewsletterNotificationSettings>(`${this.url}/notifications/newsletter`, {headers: this.headers});
|
||||||
}
|
}
|
||||||
|
|
36
src/Ombi/ClientApp/app/services/vote.service.ts
Normal file
36
src/Ombi/ClientApp/app/services/vote.service.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import { PlatformLocation } from "@angular/common";
|
||||||
|
import { HttpClient } from "@angular/common/http";
|
||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
|
||||||
|
import { IVoteEngineResult, IVoteViewModel } from "../interfaces";
|
||||||
|
import { ServiceHelpers } from "./service.helpers";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class VoteService extends ServiceHelpers {
|
||||||
|
constructor(public http: HttpClient, public platformLocation: PlatformLocation) {
|
||||||
|
super(http, "/api/v1/Vote/", platformLocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getModel(): Promise<IVoteViewModel[]> {
|
||||||
|
return await this.http.get<IVoteViewModel[]>(`${this.url}`, {headers: this.headers}).toPromise();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async upvoteMovie(requestId: number): Promise<IVoteEngineResult> {
|
||||||
|
return await this.http.post<IVoteEngineResult>(`${this.url}up/movie/${requestId}`, {headers: this.headers}).toPromise();
|
||||||
|
}
|
||||||
|
public async upvoteTv(requestId: number): Promise<IVoteEngineResult> {
|
||||||
|
return await this.http.post<IVoteEngineResult>(`${this.url}up/tv/${requestId}`, {headers: this.headers}).toPromise();
|
||||||
|
}
|
||||||
|
public async upvoteAlbum(requestId: number): Promise<IVoteEngineResult> {
|
||||||
|
return await this.http.post<IVoteEngineResult>(`${this.url}up/album/${requestId}`, {headers: this.headers}).toPromise();
|
||||||
|
}
|
||||||
|
public async downvoteMovie(requestId: number): Promise<IVoteEngineResult> {
|
||||||
|
return await this.http.post<IVoteEngineResult>(`${this.url}down/movie/${requestId}`, {headers: this.headers}).toPromise();
|
||||||
|
}
|
||||||
|
public async downvoteTv(requestId: number): Promise<IVoteEngineResult> {
|
||||||
|
return await this.http.post<IVoteEngineResult>(`${this.url}down/tv/${requestId}`, {headers: this.headers}).toPromise();
|
||||||
|
}
|
||||||
|
public async downvoteAlbum(requestId: number): Promise<IVoteEngineResult> {
|
||||||
|
return await this.http.post<IVoteEngineResult>(`${this.url}down/album/${requestId}`, {headers: this.headers}).toPromise();
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,6 +18,21 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="checkbox">
|
||||||
|
<input type="checkbox" id="deleteIssues" formControlName="deleteIssues" ng-checked="form.deleteIssues">
|
||||||
|
<label for="deleteIssues">Delete issues after they have been resolved</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" *ngIf="form.controls.deleteIssues.value">
|
||||||
|
<label for="daysAfterResolvedToDelete" class="control-label">Leave the resolved issue for X amount of
|
||||||
|
days before deletion</label>
|
||||||
|
<input type="number" class="form-control form-control-custom form-small" min="1" max="365" id="daysAfterResolvedToDelete"
|
||||||
|
formControlName="daysAfterResolvedToDelete" ng-checked="form.daysAfterResolvedToDelete">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div>
|
<div>
|
||||||
<button type="submit" [disabled]="form.invalid" class="btn btn-primary-outline ">Submit</button>
|
<button type="submit" [disabled]="form.invalid" class="btn btn-primary-outline ">Submit</button>
|
||||||
|
@ -38,8 +53,8 @@
|
||||||
<label for="categoryToAdd" class="control-label">Add Category</label>
|
<label for="categoryToAdd" class="control-label">Add Category</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
<input type="text" [(ngModel)]="categoryToAdd.value" class="form-control form-control-custom " id="categoryToAdd" name="categoryToAdd"
|
<input type="text" [(ngModel)]="categoryToAdd.value" class="form-control form-control-custom " id="categoryToAdd"
|
||||||
value="{{categoryToAdd.value}}">
|
name="categoryToAdd" value="{{categoryToAdd.value}}">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<button class="btn btn-primary-outline" (click)="addCategory()">Add</button>
|
<button class="btn btn-primary-outline" (click)="addCategory()">Add</button>
|
||||||
|
|
|
@ -53,6 +53,11 @@ export class IssuesComponent implements OnInit {
|
||||||
|
|
||||||
const settings = form.value;
|
const settings = form.value;
|
||||||
|
|
||||||
|
if(settings.deleteIssues && settings.daysAfterResolvedToDelete <= 0) {
|
||||||
|
this.notificationService.error("You need to enter days greater than 0");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.settingsService.saveIssueSettings(settings).subscribe(x => {
|
this.settingsService.saveIssueSettings(settings).subscribe(x => {
|
||||||
if (x) {
|
if (x) {
|
||||||
this.notificationService.success("Successfully saved the Issue settings");
|
this.notificationService.success("Successfully saved the Issue settings");
|
||||||
|
|
|
@ -20,6 +20,12 @@
|
||||||
<input type="checkbox" id="disableMovies" [(ngModel)]="settings.disableMovies" ng-checked="settings.disableMovies"><label for="disableMovies">Disable Movies</label>
|
<input type="checkbox" id="disableMovies" [(ngModel)]="settings.disableMovies" ng-checked="settings.disableMovies"><label for="disableMovies">Disable Movies</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="checkbox">
|
||||||
|
<input type="checkbox" id="disableMusic" [(ngModel)]="settings.disableMusic" ng-checked="settings.disableMusic"><label for="disableMusic">Disable Music</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="control-label">Subject</label>
|
<label class="control-label">Subject</label>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -41,6 +41,7 @@ import { SickRageComponent } from "./sickrage/sickrage.component";
|
||||||
import { SonarrComponent } from "./sonarr/sonarr.component";
|
import { SonarrComponent } from "./sonarr/sonarr.component";
|
||||||
import { UpdateComponent } from "./update/update.component";
|
import { UpdateComponent } from "./update/update.component";
|
||||||
import { UserManagementComponent } from "./usermanagement/usermanagement.component";
|
import { UserManagementComponent } from "./usermanagement/usermanagement.component";
|
||||||
|
import { VoteComponent } from "./vote/vote.component";
|
||||||
import { WikiComponent } from "./wiki.component";
|
import { WikiComponent } from "./wiki.component";
|
||||||
|
|
||||||
import { SettingsMenuComponent } from "./settingsmenu.component";
|
import { SettingsMenuComponent } from "./settingsmenu.component";
|
||||||
|
@ -75,6 +76,7 @@ const routes: Routes = [
|
||||||
{ path: "MassEmail", component: MassEmailComponent, canActivate: [AuthGuard] },
|
{ path: "MassEmail", component: MassEmailComponent, canActivate: [AuthGuard] },
|
||||||
{ path: "Newsletter", component: NewsletterComponent, canActivate: [AuthGuard] },
|
{ path: "Newsletter", component: NewsletterComponent, canActivate: [AuthGuard] },
|
||||||
{ path: "Lidarr", component: LidarrComponent, canActivate: [AuthGuard] },
|
{ path: "Lidarr", component: LidarrComponent, canActivate: [AuthGuard] },
|
||||||
|
{ path: "Vote", component: VoteComponent, canActivate: [AuthGuard] },
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@ -127,6 +129,7 @@ const routes: Routes = [
|
||||||
MassEmailComponent,
|
MassEmailComponent,
|
||||||
NewsletterComponent,
|
NewsletterComponent,
|
||||||
LidarrComponent,
|
LidarrComponent,
|
||||||
|
VoteComponent,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
RouterModule,
|
RouterModule,
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Issues']">Issues</a></li>
|
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Issues']">Issues</a></li>
|
||||||
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/UserManagement']">User Importer</a></li>
|
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/UserManagement']">User Importer</a></li>
|
||||||
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Authentication']">Authentication</a></li>
|
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Authentication']">Authentication</a></li>
|
||||||
|
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Vote']">Vote</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
|
50
src/Ombi/ClientApp/app/settings/vote/vote.component.html
Normal file
50
src/Ombi/ClientApp/app/settings/vote/vote.component.html
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
<settings-menu></settings-menu>
|
||||||
|
<wiki [url]="'https://github.com/tidusjar/Ombi/wiki/Vote-Settings'"></wiki>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>Vote</legend>
|
||||||
|
<form *ngIf="form" novalidate [formGroup]="form" (ngSubmit)="onSubmit(form)" style="padding-top:5%;">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="checkbox">
|
||||||
|
<input type="checkbox" id="enable" formControlName="enabled" ng-checked="form.enabled">
|
||||||
|
<label for="enable">Enable</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>Vote limits tell Ombi how many votes the request needs before approval.</p>
|
||||||
|
<p>e.g. If the Movie vote limit is 10, it requires 10 Upvotes from 10 different users before it will be approved.</p>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="movieVoteMax" class="control-label">Movie Vote Limit</label>
|
||||||
|
<input type="number" class="form-control form-control-custom form-small" min="1" id="movieVoteMax" [ngClass]="{'form-error': form.get('movieVoteMax').hasError('min')}"
|
||||||
|
formControlName="movieVoteMax" ng-checked="form.movieVoteMax">
|
||||||
|
|
||||||
|
<small *ngIf="form.get('movieVoteMax').hasError('min')" class="error-text">The limit needs to be greater than or equal to 1</small>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="musicVoteMax" class="control-label">Music Vote Limit</label>
|
||||||
|
<input type="number" class="form-control form-control-custom form-small" min="1" id="musicVoteMax" [ngClass]="{'form-error': form.get('movieVoteMax').hasError('min')}"
|
||||||
|
formControlName="musicVoteMax" ng-checked="form.musicVoteMax">
|
||||||
|
<small *ngIf="form.get('movieVoteMax').hasError('min')" class="error-text">The limit needs to be greater than or equal to 1</small>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="tvShowVoteMax" class="control-label">TV Show Vote Limit</label>
|
||||||
|
<input type="number" class="form-control form-control-custom form-small" min="1" id="tvShowVoteMax" [ngClass]="{'form-error': form.get('movieVoteMax').hasError('min')}"
|
||||||
|
formControlName="tvShowVoteMax" ng-checked="form.tvShowVoteMax">
|
||||||
|
<small *ngIf="form.get('movieVoteMax').hasError('min')" class="error-text">The limit needs to be greater than or equal to 1</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div>
|
||||||
|
<button type="submit" [disabled]="form.invalid" class="btn btn-primary-outline ">Submit</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</fieldset>
|
44
src/Ombi/ClientApp/app/settings/vote/vote.component.ts
Normal file
44
src/Ombi/ClientApp/app/settings/vote/vote.component.ts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import { Component, OnInit } from "@angular/core";
|
||||||
|
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
|
||||||
|
|
||||||
|
import { NotificationService, SettingsService } from "../../services";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
templateUrl: "./vote.component.html",
|
||||||
|
})
|
||||||
|
export class VoteComponent implements OnInit {
|
||||||
|
|
||||||
|
public form: FormGroup;
|
||||||
|
|
||||||
|
constructor(private settingsService: SettingsService,
|
||||||
|
private readonly fb: FormBuilder,
|
||||||
|
private notificationService: NotificationService) { }
|
||||||
|
|
||||||
|
public ngOnInit() {
|
||||||
|
this.settingsService.getVoteSettings().subscribe(x => {
|
||||||
|
this.form = this.fb.group({
|
||||||
|
enabled: [x.enabled],
|
||||||
|
movieVoteMax: [x.movieVoteMax, Validators.min(1)],
|
||||||
|
musicVoteMax: [x.musicVoteMax, Validators.min(1)],
|
||||||
|
tvShowVoteMax: [x.tvShowVoteMax, Validators.min(1)],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public onSubmit(form: FormGroup) {
|
||||||
|
if (form.invalid) {
|
||||||
|
this.notificationService.error("Please check your entered values");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = form.value;
|
||||||
|
|
||||||
|
this.settingsService.saveVoteSettings(settings).subscribe(x => {
|
||||||
|
if (x) {
|
||||||
|
this.notificationService.success("Successfully saved the Vote settings");
|
||||||
|
} else {
|
||||||
|
this.notificationService.success("There was an error when saving the Vote settings");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ import { NgModule } from "@angular/core";
|
||||||
import { FormsModule } from "@angular/forms";
|
import { FormsModule } from "@angular/forms";
|
||||||
import { TranslateModule } from "@ngx-translate/core";
|
import { TranslateModule } from "@ngx-translate/core";
|
||||||
import { TruncateModule } from "@yellowspot/ng-truncate";
|
import { TruncateModule } from "@yellowspot/ng-truncate";
|
||||||
|
import { MomentModule } from "ngx-moment";
|
||||||
|
|
||||||
import { IssuesReportComponent } from "./issues-report.component";
|
import { IssuesReportComponent } from "./issues-report.component";
|
||||||
|
|
||||||
|
@ -18,6 +19,7 @@ import { InputSwitchModule, SidebarModule } from "primeng/primeng";
|
||||||
CommonModule,
|
CommonModule,
|
||||||
InputSwitchModule,
|
InputSwitchModule,
|
||||||
TruncateModule,
|
TruncateModule,
|
||||||
|
MomentModule,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
TranslateModule,
|
TranslateModule,
|
||||||
|
@ -27,6 +29,7 @@ import { InputSwitchModule, SidebarModule } from "primeng/primeng";
|
||||||
IssuesReportComponent,
|
IssuesReportComponent,
|
||||||
TruncateModule,
|
TruncateModule,
|
||||||
InputSwitchModule,
|
InputSwitchModule,
|
||||||
|
MomentModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class SharedModule {}
|
export class SharedModule {}
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button type="submit" class="btn btn-primary-outline" [disabled]="form.invalid">Save</button>
|
<button type="submit" data-test="submitbtn" class="btn btn-primary-outline" [disabled]="form.invalid">Save</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
<h3 *ngIf="!edit">Create User</h3>
|
<h3 *ngIf="!edit">Create User</h3>
|
||||||
<h3 *ngIf="edit && user"> User: {{user.userName}}</h3>
|
<h3 *ngIf="edit && user"> User: {{user.userName}}</h3>
|
||||||
|
|
||||||
|
<p-confirmDialog></p-confirmDialog>
|
||||||
|
|
||||||
<button type="button" class="btn btn-primary-outline" style="float:right;" [routerLink]="['/usermanagement/']">Back</button>
|
<button type="button" class="btn btn-primary-outline" style="float:right;" [routerLink]="['/usermanagement/']">Back</button>
|
||||||
<div *ngIf="!edit || edit && user">
|
<div *ngIf="!edit || edit && user">
|
||||||
|
|
||||||
|
@ -65,7 +68,7 @@
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<input type="checkbox" [(ngModel)]="c.enabled" [value]="c.value" id="create{{c.value}}"
|
<input type="checkbox" [(ngModel)]="c.enabled" [value]="c.value" id="create{{c.value}}"
|
||||||
[attr.name]="'create' + c.value" ng-checked="c.enabled">
|
[attr.name]="'create' + c.value" ng-checked="c.enabled">
|
||||||
<label for="create{{c.value}}">{{c.value | humanize}}</label>
|
<label id="label{{c.value}}" for="create{{c.value}}">{{c.value | humanize}}</label>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -77,7 +80,7 @@
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<input type="checkbox" [(ngModel)]="c.enabled" [value]="c.value" id="create{{c.value}}"
|
<input type="checkbox" [(ngModel)]="c.enabled" [value]="c.value" id="create{{c.value}}"
|
||||||
[attr.name]="'create' + c.value" ng-checked="c.enabled">
|
[attr.name]="'create' + c.value" ng-checked="c.enabled">
|
||||||
<label for="create{{c.value}}">{{c.value | humanize}}</label>
|
<label id="label{{c.value}}" for="create{{c.value}}">{{c.value | humanize}}</label>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -129,7 +132,7 @@
|
||||||
<label for="{{pref.agent}}" class="control-label">{{NotificationAgent[pref.agent]
|
<label for="{{pref.agent}}" class="control-label">{{NotificationAgent[pref.agent]
|
||||||
| humanize}}</label>
|
| humanize}}</label>
|
||||||
<div>
|
<div>
|
||||||
<input type="text" [(ngModel)]="pref.value" class="form-control form-control-custom"
|
<input type="text" [attr.data-test]="NotificationAgent[pref.agent]" [(ngModel)]="pref.value" class="form-control form-control-custom"
|
||||||
name="{{pref.agent}}" value="{{pref?.value}}">
|
name="{{pref.agent}}" value="{{pref?.value}}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -208,10 +211,10 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<button *ngIf="!edit" type="button" class="btn btn-danger-outline" (click)="create()">Create</button>
|
<button *ngIf="!edit" type="button" data-test="createuserbtn" class="btn btn-danger-outline" (click)="create()">Create</button>
|
||||||
<div *ngIf="edit">
|
<div *ngIf="edit">
|
||||||
<button type="button" class="btn btn-primary-outline" (click)="update()">Update</button>
|
<button type="button" data-test="updatebtn" class="btn btn-primary-outline" (click)="update()">Update</button>
|
||||||
<button type="button" class="btn btn-danger-outline" (click)="delete()">Delete</button>
|
<button type="button" data-test="deletebtn" class="btn btn-danger-outline" (click)="delete()">Delete</button>
|
||||||
<button type="button" style="float:right;" class="btn btn-info-outline" (click)="resetPassword()"
|
<button type="button" style="float:right;" class="btn btn-info-outline" (click)="resetPassword()"
|
||||||
pTooltip="You need your SMTP settings setup">Send Reset Password Link</button>
|
pTooltip="You need your SMTP settings setup">Send Reset Password Link</button>
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<button type="button" class="btn btn-success-outline" [routerLink]="['/usermanagement/user']">Add User To Ombi</button>
|
<button type="button" class="btn btn-success-outline" data-test="adduserbtn" [routerLink]="['/usermanagement/user']">Add User To Ombi</button>
|
||||||
|
|
||||||
<button type="button" style="float:right;" class="btn btn-primary-outline"(click)="showBulkEdit = !showBulkEdit" [disabled]="!hasChecked()">Bulk Edit</button>
|
<button type="button" style="float:right;" class="btn btn-primary-outline"(click)="showBulkEdit = !showBulkEdit" [disabled]="!hasChecked()">Bulk Edit</button>
|
||||||
<div *ngIf="plexEnabled">
|
<div *ngIf="plexEnabled">
|
||||||
|
@ -104,17 +104,24 @@
|
||||||
</td>
|
</td>
|
||||||
<td class="td-labelled" data-label="Request Due">
|
<td class="td-labelled" data-label="Request Due">
|
||||||
<div *ngIf="u.movieRequestQuota != null && u.movieRequestQuota.remaining != u.movieRequestLimit">
|
<div *ngIf="u.movieRequestQuota != null && u.movieRequestQuota.remaining != u.movieRequestLimit">
|
||||||
{{'UserManagment.MovieDue' | translate: {date: (u.movieRequestQuota.nextRequest | date: 'short')} }}
|
{{'UserManagment.MovieDue' | translate: {date: (u.movieRequestQuota.nextRequest | amLocal | amDateFormat: 'l LT')} }}
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="u.episodeRequestQuota != null && u.episodeRequestQuota.remaining != u.episodeRequestLimit">
|
<div *ngIf="u.episodeRequestQuota != null && u.episodeRequestQuota.remaining != u.episodeRequestLimit">
|
||||||
{{'UserManagment.TvDue' | translate: {date: (u.episodeRequestQuota.nextRequest | date: 'short')} }}
|
{{'UserManagment.TvDue' | translate: {date: (u.episodeRequestQuota.nextRequest | amLocal | amDateFormat: 'l LT')} }}
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="u.musicRequestQuota != null && u.musicRequestQuota.remaining != u.musicRequestLimit">
|
<div *ngIf="u.musicRequestQuota != null && u.musicRequestQuota.remaining != u.musicRequestLimit">
|
||||||
{{'UserManagment.MusicDue' | translate: {date: (u.musicRequestQuota.nextRequest | date: 'short')} }}
|
{{'UserManagment.MusicDue' | translate: {date: (u.musicRequestQuota.nextRequest | amLocal | amDateFormat: 'l LT')} }}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="td-labelled" data-label="Last Logged In:">
|
<td class="td-labelled" data-label="Last Logged In:">
|
||||||
{{u.lastLoggedIn | date: 'short'}}
|
<!-- {{u.lastLoggedIn | date: 'short'}} -->
|
||||||
|
<span *ngIf="u.lastLoggedIn">
|
||||||
|
{{u.lastLoggedIn | amLocal | amDateFormat: 'l LT'}}
|
||||||
|
</span>
|
||||||
|
<span *ngIf="!u.lastLoggedIn">
|
||||||
|
Not logged in yet!
|
||||||
|
</span>
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
<td class="td-labelled" data-label="User Type:">
|
<td class="td-labelled" data-label="User Type:">
|
||||||
<span *ngIf="u.userType === 1">Local User</span>
|
<span *ngIf="u.userType === 1">Local User</span>
|
||||||
|
@ -122,7 +129,7 @@
|
||||||
<span *ngIf="u.userType === 3">Emby User</span>
|
<span *ngIf="u.userType === 3">Emby User</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a [routerLink]="['/usermanagement/user/' + u.id]" class="btn btn-sm btn-info-outline">Details/Edit</a>
|
<a id="edit{{u.userName}}" [routerLink]="['/usermanagement/user/' + u.id]" class="btn btn-sm btn-info-outline">Details/Edit</a>
|
||||||
</td>
|
</td>
|
||||||
<td *ngIf="customizationSettings">
|
<td *ngIf="customizationSettings">
|
||||||
<button *ngIf="!u.hasLoggedIn" (click)="welcomeEmail(u)" [disabled]="!customizationSettings.applicationUrl" class="btn btn-sm btn-info-outline">Send Welcome Email</button>
|
<button *ngIf="!u.hasLoggedIn" (click)="welcomeEmail(u)" [disabled]="!customizationSettings.applicationUrl" class="btn btn-sm btn-info-outline">Send Welcome Email</button>
|
||||||
|
|
92
src/Ombi/ClientApp/app/vote/vote.component.html
Normal file
92
src/Ombi/ClientApp/app/vote/vote.component.html
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
<h1>Vote</h1>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<ul id="nav-tabs" class="nav nav-tabs" role="tablist">
|
||||||
|
|
||||||
|
<li role="presentation" class="active">
|
||||||
|
<a id="currentVotes" href="#currentVotes" aria-controls="home" role="tab" data-toggle="tab" (click)="selectCurrentTab()"><i
|
||||||
|
class="fa fa-meh-o"></i> {{ 'Votes.VotesTab' | translate }}</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li role="presentation">
|
||||||
|
<a id="completedVotes" href="#completedVotes" aria-controls="profile" role="tab" data-toggle="tab" (click)="selectCompletedVotesTab()"><i
|
||||||
|
class="fa fa-smile-o"></i> {{ 'Votes.CompletedVotesTab' | translate }}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- Tab panes -->
|
||||||
|
<div class="tab-content">
|
||||||
|
|
||||||
|
<div [hidden]="!showCurrent">
|
||||||
|
<div *ngIf="currentVotes">
|
||||||
|
<table class="table table-striped table-hover table-responsive table-condensed">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td style="width: 10%"></td>
|
||||||
|
<td></td>
|
||||||
|
<td style="width: 10%">Title</td>
|
||||||
|
<td>Description</td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr *ngFor="let vm of currentVotes">
|
||||||
|
<td class="vcenter">
|
||||||
|
<button class="btn btn-info-outline col-md-6" (click)="upvote(vm)"><i class="fa fa-thumbs-o-up"
|
||||||
|
aria-hidden="true"></i></button>
|
||||||
|
<button class="btn btn-info-outline col-md-6" (click)="downvote(vm)" ><i class="fa fa-thumbs-o-down"
|
||||||
|
aria-hidden="true"></i></button>
|
||||||
|
</td>
|
||||||
|
<td style="width: 10%"> <img *ngIf="vm.image" class="img-responsive poster" style="max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
width: 100%;"
|
||||||
|
(click)="toggle($event, vm.image)" src="{{vm.image}}" alt="poster"></td>
|
||||||
|
<td class="vcenter">{{vm.title}}</td>
|
||||||
|
<td class="vcenter" [innerHTML]="vm.description"></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div [hidden]="!showCompleted">
|
||||||
|
<div *ngIf="completedVotes">
|
||||||
|
<table class="table table-striped table-hover table-responsive table-condensed">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td style="width: 10%"></td>
|
||||||
|
<td></td>
|
||||||
|
<td style="width: 10%">Title</td>
|
||||||
|
<td>Description</td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr *ngFor="let vm of completedVotes">
|
||||||
|
<td class="vcenter">
|
||||||
|
<button class="btn btn-info-outline col-md-6" [ngClass]="{'btn-success-outline': vm.myVote == VoteType.Upvote, 'btn-info-outline': vm.myVote != VoteType.Upvote}"
|
||||||
|
(click)="upvote(vm)" [disabled]="vm.myVote == VoteType.Upvote"><i class="fa fa-thumbs-o-up"
|
||||||
|
aria-hidden="true"></i></button>
|
||||||
|
<button class="btn btn-info-outline col-md-6" [ngClass]="{'btn-danger-outline': vm.myVote == VoteType.Downvote, 'btn-info-outline': vm.myVote != VoteType.Downvote}"
|
||||||
|
(click)="downvote(vm)" [disabled]="vm.myVote == VoteType.Downvote"><i class="fa fa-thumbs-o-down"
|
||||||
|
aria-hidden="true"></i></button>
|
||||||
|
</td>
|
||||||
|
<td style="width: 10%"> <img *ngIf="vm.image" class="img-responsive poster" style="max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
width: 100%;"
|
||||||
|
(click)="toggle($event, vm.image)" src="{{vm.image}}" alt="poster"></td>
|
||||||
|
<td class="vcenter">{{vm.title}}</td>
|
||||||
|
<td class="vcenter" [innerHTML]="vm.description"></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<p-overlayPanel #op [dismissable]="true" [styleClass]="'hideBackground'">
|
||||||
|
<img class="img-responsive poster" width="70%" src="{{panelImage}}" alt="poster">
|
||||||
|
</p-overlayPanel>
|
12
src/Ombi/ClientApp/app/vote/vote.component.scss
Normal file
12
src/Ombi/ClientApp/app/vote/vote.component.scss
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
.vcenter {
|
||||||
|
vertical-align: middle;
|
||||||
|
float: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hideBackground {
|
||||||
|
border: 0px solid transparent !important;
|
||||||
|
background: transparent !important;
|
||||||
|
-webkit-box-shadow: 0 0px 0px 0 transparent !important;
|
||||||
|
-moz-box-shadow: 0 0px 0px 0 transparent !important;
|
||||||
|
box-shadow: 0 0px 0px 0 transparent !important;
|
||||||
|
}
|
101
src/Ombi/ClientApp/app/vote/vote.component.ts
Normal file
101
src/Ombi/ClientApp/app/vote/vote.component.ts
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
import { Component, OnInit, ViewChild } from "@angular/core";
|
||||||
|
|
||||||
|
import { OverlayPanel } from "primeng/primeng";
|
||||||
|
import { NotificationService, VoteService } from "../services";
|
||||||
|
|
||||||
|
import { IVoteEngineResult, IVoteViewModel, RequestTypes, VoteType } from "../interfaces";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
templateUrl: "vote.component.html",
|
||||||
|
styleUrls: ["vote.component.scss"],
|
||||||
|
})
|
||||||
|
export class VoteComponent implements OnInit {
|
||||||
|
|
||||||
|
public showCurrent: boolean = true;
|
||||||
|
public showCompleted: boolean;
|
||||||
|
public viewModel: IVoteViewModel[];
|
||||||
|
public currentVotes: IVoteViewModel[];
|
||||||
|
public completedVotes: IVoteViewModel[];
|
||||||
|
public VoteType = VoteType;
|
||||||
|
public panelImage: string;
|
||||||
|
@ViewChild("op") public overlayPanel: OverlayPanel;
|
||||||
|
|
||||||
|
constructor(private voteService: VoteService, private notificationSerivce: NotificationService) { }
|
||||||
|
|
||||||
|
public async ngOnInit() {
|
||||||
|
this.viewModel = await this.voteService.getModel();
|
||||||
|
this.filterLists();
|
||||||
|
}
|
||||||
|
|
||||||
|
public selectCurrentTab() {
|
||||||
|
this.showCurrent = true;
|
||||||
|
this.showCompleted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public selectCompletedVotesTab() {
|
||||||
|
this.showCurrent = false;
|
||||||
|
this.showCompleted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public toggle(event: any, image: string) {
|
||||||
|
this.panelImage = image;
|
||||||
|
this.overlayPanel.toggle(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async upvote(vm: IVoteViewModel) {
|
||||||
|
let result: IVoteEngineResult = {errorMessage:"", isError: false, message:"",result:false};
|
||||||
|
switch(vm.requestType) {
|
||||||
|
case RequestTypes.Album:
|
||||||
|
result = await this.voteService.upvoteAlbum(vm.requestId);
|
||||||
|
break;
|
||||||
|
case RequestTypes.Movie:
|
||||||
|
result = await this.voteService.upvoteMovie(vm.requestId);
|
||||||
|
break;
|
||||||
|
case RequestTypes.TvShow:
|
||||||
|
result = await this.voteService.upvoteTv(vm.requestId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(result.isError) {
|
||||||
|
this.notificationSerivce.error(result.errorMessage);
|
||||||
|
} else {
|
||||||
|
this.notificationSerivce.success("Voted!");
|
||||||
|
vm.alreadyVoted = true;
|
||||||
|
vm.myVote = VoteType.Upvote;
|
||||||
|
this.filterLists();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async downvote(vm: IVoteViewModel) {
|
||||||
|
let result: IVoteEngineResult = {errorMessage:"", isError: false, message:"",result:false};
|
||||||
|
switch(vm.requestType) {
|
||||||
|
case RequestTypes.Album:
|
||||||
|
result = await this.voteService.downvoteAlbum(vm.requestId);
|
||||||
|
break;
|
||||||
|
case RequestTypes.Movie:
|
||||||
|
result = await this.voteService.downvoteMovie(vm.requestId);
|
||||||
|
break;
|
||||||
|
case RequestTypes.TvShow:
|
||||||
|
result = await this.voteService.downvoteTv(vm.requestId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(result.isError) {
|
||||||
|
this.notificationSerivce.error(result.errorMessage);
|
||||||
|
} else {
|
||||||
|
this.notificationSerivce.success("Voted!");
|
||||||
|
vm.alreadyVoted = true;
|
||||||
|
vm.myVote = VoteType.Downvote;
|
||||||
|
this.filterLists();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private filterLists() {
|
||||||
|
this.completedVotes = this.viewModel.filter(vm => {
|
||||||
|
return vm.alreadyVoted;
|
||||||
|
});
|
||||||
|
this.currentVotes = this.viewModel.filter(vm => {
|
||||||
|
return !vm.alreadyVoted;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
41
src/Ombi/ClientApp/app/vote/vote.module.ts
Normal file
41
src/Ombi/ClientApp/app/vote/vote.module.ts
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import { NgModule } from "@angular/core";
|
||||||
|
import { RouterModule, Routes } from "@angular/router";
|
||||||
|
|
||||||
|
import { NgbModule } from "@ng-bootstrap/ng-bootstrap";
|
||||||
|
import { OrderModule } from "ngx-order-pipe";
|
||||||
|
import { OverlayPanelModule, SharedModule, TabViewModule } from "primeng/primeng";
|
||||||
|
|
||||||
|
import { VoteService } from "../services";
|
||||||
|
|
||||||
|
import { AuthGuard } from "../auth/auth.guard";
|
||||||
|
|
||||||
|
import { SharedModule as OmbiShared } from "../shared/shared.module";
|
||||||
|
|
||||||
|
import { VoteComponent } from "./vote.component";
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{ path: "", component: VoteComponent, canActivate: [AuthGuard] },
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forChild(routes),
|
||||||
|
NgbModule.forRoot(),
|
||||||
|
SharedModule,
|
||||||
|
OrderModule,
|
||||||
|
OmbiShared,
|
||||||
|
TabViewModule,
|
||||||
|
OverlayPanelModule,
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
VoteComponent,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
RouterModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
VoteService,
|
||||||
|
],
|
||||||
|
|
||||||
|
})
|
||||||
|
export class VoteModule { }
|
|
@ -19,7 +19,7 @@
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div style="text-align: center; margin-top: 20px">
|
<div style="text-align: center; margin-top: 20px">
|
||||||
<button (click)="createUser()" type="submit" class="btn btn-success-outline">Finish</button>
|
<button (click)="createUser()" data-test="createuserbtn" type="submit" class="btn btn-success-outline">Finish</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<button (click)="skip()" class="btn btn-primary-outline wizard-img" id="plexImg">
|
<button (click)="skip()" data-test="skipbtn" class="btn btn-primary-outline wizard-img" id="plexImg">
|
||||||
Skip
|
Skip
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<small>we are just going to run though the initial Ombi setup!</small>
|
<small>we are just going to run though the initial Ombi setup!</small>
|
||||||
|
|
||||||
<div style="text-align: center; margin-top: 20px">
|
<div style="text-align: center; margin-top: 20px">
|
||||||
<a (click)="next()" class="btn btn-primary-outline">Next</a>
|
<a (click)="next()" data-test="nextbtn" class="btn btn-primary-outline">Next</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1007,5 +1007,4 @@ a > h4:hover {
|
||||||
|
|
||||||
.album-cover {
|
.album-cover {
|
||||||
width:300px;
|
width:300px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -283,12 +283,12 @@ namespace Ombi.Controllers.External
|
||||||
Uri url;
|
Uri url;
|
||||||
if (!wizard.Wizard)
|
if (!wizard.Wizard)
|
||||||
{
|
{
|
||||||
url = await _plexOAuthManager.GetOAuthUrl(wizard.Pin.id, wizard.Pin.code);
|
url = await _plexOAuthManager.GetOAuthUrl(wizard.Pin.code);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var websiteAddress =$"{this.Request.Scheme}://{this.Request.Host}{this.Request.PathBase}";
|
var websiteAddress =$"{this.Request.Scheme}://{this.Request.Host}{this.Request.PathBase}";
|
||||||
url = await _plexOAuthManager.GetWizardOAuthUrl(wizard.Pin.id, wizard.Pin.code, websiteAddress);
|
url = await _plexOAuthManager.GetWizardOAuthUrl(wizard.Pin.code, websiteAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (url == null)
|
if (url == null)
|
||||||
|
|
|
@ -3,7 +3,6 @@ using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
|
||||||
using Ombi.Api.Radarr;
|
using Ombi.Api.Radarr;
|
||||||
using Ombi.Api.Radarr.Models;
|
using Ombi.Api.Radarr.Models;
|
||||||
using Ombi.Attributes;
|
using Ombi.Attributes;
|
||||||
|
@ -13,9 +12,9 @@ using Ombi.Settings.Settings.Models.External;
|
||||||
|
|
||||||
namespace Ombi.Controllers.External
|
namespace Ombi.Controllers.External
|
||||||
{
|
{
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[ApiV1]
|
[ApiV1]
|
||||||
[Produces("application/json")]
|
[Produces("application/json")]
|
||||||
public class RadarrController : Controller
|
public class RadarrController : Controller
|
||||||
{
|
{
|
||||||
public RadarrController(IRadarrApi radarr, ISettingsService<RadarrSettings> settings,
|
public RadarrController(IRadarrApi radarr, ISettingsService<RadarrSettings> settings,
|
||||||
|
@ -24,6 +23,7 @@ namespace Ombi.Controllers.External
|
||||||
RadarrApi = radarr;
|
RadarrApi = radarr;
|
||||||
RadarrSettings = settings;
|
RadarrSettings = settings;
|
||||||
Cache = mem;
|
Cache = mem;
|
||||||
|
RadarrSettings.ClearCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
private IRadarrApi RadarrApi { get; }
|
private IRadarrApi RadarrApi { get; }
|
||||||
|
@ -59,17 +59,15 @@ namespace Ombi.Controllers.External
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpGet("Profiles")]
|
[HttpGet("Profiles")]
|
||||||
|
[PowerUser]
|
||||||
public async Task<IEnumerable<RadarrProfile>> GetProfiles()
|
public async Task<IEnumerable<RadarrProfile>> GetProfiles()
|
||||||
{
|
{
|
||||||
return await Cache.GetOrAdd(CacheKeys.RadarrQualityProfiles, async () =>
|
var settings = await RadarrSettings.GetSettingsAsync();
|
||||||
|
if (settings.Enabled)
|
||||||
{
|
{
|
||||||
var settings = await RadarrSettings.GetSettingsAsync();
|
return await RadarrApi.GetProfiles(settings.ApiKey, settings.FullUri);
|
||||||
if (settings.Enabled)
|
}
|
||||||
{
|
return null;
|
||||||
return await RadarrApi.GetProfiles(settings.ApiKey, settings.FullUri);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}, DateTime.Now.AddHours(1));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -78,17 +76,15 @@ namespace Ombi.Controllers.External
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpGet("RootFolders")]
|
[HttpGet("RootFolders")]
|
||||||
|
[PowerUser]
|
||||||
public async Task<IEnumerable<RadarrRootFolder>> GetRootFolders()
|
public async Task<IEnumerable<RadarrRootFolder>> GetRootFolders()
|
||||||
{
|
{
|
||||||
return await Cache.GetOrAdd(CacheKeys.RadarrRootProfiles, async () =>
|
var settings = await RadarrSettings.GetSettingsAsync();
|
||||||
|
if (settings.Enabled)
|
||||||
{
|
{
|
||||||
var settings = await RadarrSettings.GetSettingsAsync();
|
return await RadarrApi.GetRootFolders(settings.ApiKey, settings.FullUri);
|
||||||
if (settings.Enabled)
|
}
|
||||||
{
|
return null;
|
||||||
return await RadarrApi.GetRootFolders(settings.ApiKey, settings.FullUri);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}, DateTime.Now.AddHours(1));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -20,6 +20,7 @@ namespace Ombi.Controllers.External
|
||||||
{
|
{
|
||||||
SonarrApi = sonarr;
|
SonarrApi = sonarr;
|
||||||
SonarrSettings = settings;
|
SonarrSettings = settings;
|
||||||
|
SonarrSettings.ClearCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
private ISonarrApi SonarrApi { get; }
|
private ISonarrApi SonarrApi { get; }
|
||||||
|
@ -54,6 +55,7 @@ namespace Ombi.Controllers.External
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpGet("Profiles")]
|
[HttpGet("Profiles")]
|
||||||
|
[PowerUser]
|
||||||
public async Task<IEnumerable<SonarrProfile>> GetProfiles()
|
public async Task<IEnumerable<SonarrProfile>> GetProfiles()
|
||||||
{
|
{
|
||||||
var settings = await SonarrSettings.GetSettingsAsync();
|
var settings = await SonarrSettings.GetSettingsAsync();
|
||||||
|
@ -69,6 +71,7 @@ namespace Ombi.Controllers.External
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpGet("RootFolders")]
|
[HttpGet("RootFolders")]
|
||||||
|
[PowerUser]
|
||||||
public async Task<IEnumerable<SonarrRootFolder>> GetRootFolders()
|
public async Task<IEnumerable<SonarrRootFolder>> GetRootFolders()
|
||||||
{
|
{
|
||||||
var settings = await SonarrSettings.GetSettingsAsync();
|
var settings = await SonarrSettings.GetSettingsAsync();
|
||||||
|
|
|
@ -240,6 +240,7 @@ namespace Ombi.Controllers
|
||||||
await CreateRole(OmbiRoles.RequestTv);
|
await CreateRole(OmbiRoles.RequestTv);
|
||||||
await CreateRole(OmbiRoles.Disabled);
|
await CreateRole(OmbiRoles.Disabled);
|
||||||
await CreateRole(OmbiRoles.ReceivesNewsletter);
|
await CreateRole(OmbiRoles.ReceivesNewsletter);
|
||||||
|
await CreateRole(OmbiRoles.ManageOwnRequests);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CreateRole(string role)
|
private async Task CreateRole(string role)
|
||||||
|
|
|
@ -88,7 +88,7 @@ namespace Ombi.Controllers
|
||||||
/// <param name="requestId">The request identifier.</param>
|
/// <param name="requestId">The request identifier.</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpDelete("{requestId:int}")]
|
[HttpDelete("{requestId:int}")]
|
||||||
[PowerUser]
|
[Authorize(Roles = "Admin,PowerUser,ManageOwnRequests")]
|
||||||
public async Task DeleteRequest(int requestId)
|
public async Task DeleteRequest(int requestId)
|
||||||
{
|
{
|
||||||
await _engine.RemoveAlbumRequest(requestId);
|
await _engine.RemoveAlbumRequest(requestId);
|
||||||
|
|
|
@ -95,7 +95,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]
|
[Authorize(Roles = "Admin,PowerUser,ManageOwnRequests")]
|
||||||
public async Task DeleteRequest(int requestId)
|
public async Task DeleteRequest(int requestId)
|
||||||
{
|
{
|
||||||
await MovieRequestEngine.RemoveMovieRequest(requestId);
|
await MovieRequestEngine.RemoveMovieRequest(requestId);
|
||||||
|
@ -269,7 +269,7 @@ namespace Ombi.Controllers
|
||||||
/// <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]
|
[Authorize(Roles = "Admin,PowerUser,ManageOwnRequests")]
|
||||||
public async Task DeleteTvRequest(int requestId)
|
public async Task DeleteTvRequest(int requestId)
|
||||||
{
|
{
|
||||||
await TvRequestEngine.RemoveTvRequest(requestId);
|
await TvRequestEngine.RemoveTvRequest(requestId);
|
||||||
|
@ -380,7 +380,7 @@ namespace Ombi.Controllers
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="requestId">The model.</param>
|
/// <param name="requestId">The model.</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[PowerUser]
|
[Authorize(Roles = "Admin,PowerUser,ManageOwnRequests")]
|
||||||
[HttpDelete("tv/child/{requestId:int}")]
|
[HttpDelete("tv/child/{requestId:int}")]
|
||||||
public async Task<bool> DeleteChildRequest(int requestId)
|
public async Task<bool> DeleteChildRequest(int requestId)
|
||||||
{
|
{
|
||||||
|
|
|
@ -598,12 +598,11 @@ namespace Ombi.Controllers
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Save the Issues settings.
|
/// Save the Vote settings.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="settings">The settings.</param>
|
/// <param name="settings">The settings.</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpPost("Issues")]
|
[HttpPost("Issues")]
|
||||||
[AllowAnonymous]
|
|
||||||
public async Task<bool> IssueSettings([FromBody]IssueSettings settings)
|
public async Task<bool> IssueSettings([FromBody]IssueSettings settings)
|
||||||
{
|
{
|
||||||
return await Save(settings);
|
return await Save(settings);
|
||||||
|
@ -628,6 +627,35 @@ namespace Ombi.Controllers
|
||||||
return issues.Enabled;
|
return issues.Enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Save the Vote settings.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="settings">The settings.</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpPost("vote")]
|
||||||
|
public async Task<bool> VoteSettings([FromBody]VoteSettings settings)
|
||||||
|
{
|
||||||
|
return await Save(settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the Vote Settings.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("vote")]
|
||||||
|
public async Task<VoteSettings> VoteSettings()
|
||||||
|
{
|
||||||
|
return await Get<VoteSettings>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[AllowAnonymous]
|
||||||
|
[HttpGet("voteenabled")]
|
||||||
|
public async Task<bool> VoteEnabled()
|
||||||
|
{
|
||||||
|
var vote = await Get<VoteSettings>();
|
||||||
|
return vote.Enabled;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Saves the email notification settings.
|
/// Saves the email notification settings.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -83,7 +83,7 @@ namespace Ombi.Controllers
|
||||||
|
|
||||||
var websiteAddress = $"{this.Request.Scheme}://{this.Request.Host}{this.Request.PathBase}";
|
var websiteAddress = $"{this.Request.Scheme}://{this.Request.Host}{this.Request.PathBase}";
|
||||||
//https://app.plex.tv/auth#?forwardUrl=http://google.com/&clientID=Ombi-Test&context%5Bdevice%5D%5Bproduct%5D=Ombi%20SSO&pinID=798798&code=4lgfd
|
//https://app.plex.tv/auth#?forwardUrl=http://google.com/&clientID=Ombi-Test&context%5Bdevice%5D%5Bproduct%5D=Ombi%20SSO&pinID=798798&code=4lgfd
|
||||||
var url = await _plexOAuthManager.GetOAuthUrl(model.PlexTvPin.id, model.PlexTvPin.code, websiteAddress);
|
var url = await _plexOAuthManager.GetOAuthUrl(model.PlexTvPin.code, websiteAddress);
|
||||||
if (url == null)
|
if (url == null)
|
||||||
{
|
{
|
||||||
return new JsonResult(new
|
return new JsonResult(new
|
||||||
|
|
118
src/Ombi/Controllers/VoteController.cs
Normal file
118
src/Ombi/Controllers/VoteController.cs
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Ombi.Core.Engine;
|
||||||
|
using Ombi.Core.Models;
|
||||||
|
using Ombi.Core.Models.UI;
|
||||||
|
using Ombi.Store.Entities;
|
||||||
|
|
||||||
|
namespace Ombi.Controllers
|
||||||
|
{
|
||||||
|
[ApiV1]
|
||||||
|
[Authorize]
|
||||||
|
[Produces("application/json")]
|
||||||
|
public class VoteController : Controller
|
||||||
|
{
|
||||||
|
public VoteController(IVoteEngine engine)
|
||||||
|
{
|
||||||
|
_engine = engine;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly IVoteEngine _engine;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the viewmodel to render on the UI
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet]
|
||||||
|
public Task<List<VoteViewModel>> GetView()
|
||||||
|
{
|
||||||
|
return _engine.GetMovieViewModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Upvotes a movie
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("up/movie/{requestId:int}")]
|
||||||
|
public Task<VoteEngineResult> UpvoteMovie(int requestId)
|
||||||
|
{
|
||||||
|
return _engine.UpVote(requestId, RequestType.Movie);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Upvotes a tv show
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("up/tv/{requestId:int}")]
|
||||||
|
public Task<VoteEngineResult> UpvoteTv(int requestId)
|
||||||
|
{
|
||||||
|
return _engine.UpVote(requestId, RequestType.TvShow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Upvotes a album
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("up/album/{requestId:int}")]
|
||||||
|
public Task<VoteEngineResult> UpvoteAlbum(int requestId)
|
||||||
|
{
|
||||||
|
return _engine.UpVote(requestId, RequestType.Album);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Downvotes a movie
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("down/movie/{requestId:int}")]
|
||||||
|
public Task<VoteEngineResult> DownvoteMovie(int requestId)
|
||||||
|
{
|
||||||
|
return _engine.DownVote(requestId, RequestType.Movie);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Downvotes a tv show
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("down/tv/{requestId:int}")]
|
||||||
|
public Task<VoteEngineResult> DownvoteTv(int requestId)
|
||||||
|
{
|
||||||
|
return _engine.DownVote(requestId, RequestType.TvShow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Downvotes a album
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("down/album/{requestId:int}")]
|
||||||
|
public Task<VoteEngineResult> DownvoteAlbum(int requestId)
|
||||||
|
{
|
||||||
|
return _engine.DownVote(requestId, RequestType.Album);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get's all the votes for the request id
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("movie/{requestId:int}")]
|
||||||
|
public Task<List<Votes>> MovieVotes(int requestId)
|
||||||
|
{
|
||||||
|
return _engine.GetVotes(requestId, RequestType.Movie).ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get's all the votes for the request id
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("music/{requestId:int}")]
|
||||||
|
public Task<List<Votes>> MusicVotes(int requestId)
|
||||||
|
{
|
||||||
|
return _engine.GetVotes(requestId, RequestType.Album).ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get's all the votes for the request id
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("tv/{requestId:int}")]
|
||||||
|
public Task<List<Votes>> TvVotes(int requestId)
|
||||||
|
{
|
||||||
|
return _engine.GetVotes(requestId, RequestType.TvShow).ToListAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
4
src/Ombi/cypress.json
Normal file
4
src/Ombi/cypress.json
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"projectId": "gms1wj",
|
||||||
|
"baseUrl": "http://localhost:3577"
|
||||||
|
}
|
5
src/Ombi/cypress/fixtures/example.json
Normal file
5
src/Ombi/cypress/fixtures/example.json
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"name": "Using fixtures to represent data",
|
||||||
|
"email": "hello@cypress.io",
|
||||||
|
"body": "Fixtures are a great way to mock data for responses to routes"
|
||||||
|
}
|
36
src/Ombi/cypress/integration/login.spec.js
Normal file
36
src/Ombi/cypress/integration/login.spec.js
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
/// <reference types="Cypress" />
|
||||||
|
|
||||||
|
describe('Login Page', function () {
|
||||||
|
it('Invalid Password', function () {
|
||||||
|
cy.visit('/');
|
||||||
|
cy.contains('Sign in');
|
||||||
|
|
||||||
|
cy.get('#inputEmail').type('automation');
|
||||||
|
cy.get('#inputPassword').type('incorrectpw');
|
||||||
|
|
||||||
|
cy.get('[data-test=signinbtn]').click();
|
||||||
|
cy.verifyNotification('Incorrect username');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Invalid Username', function () {
|
||||||
|
cy.visit('/');
|
||||||
|
cy.contains('Sign in');
|
||||||
|
|
||||||
|
cy.get('#inputEmail').type('bad username');
|
||||||
|
cy.get('#inputPassword').type('incorrectpw');
|
||||||
|
|
||||||
|
cy.get('[data-test=signinbtn]').click();
|
||||||
|
cy.verifyNotification('Incorrect username');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Correct Login', function () {
|
||||||
|
cy.visit('/');
|
||||||
|
cy.contains('Sign in');
|
||||||
|
|
||||||
|
cy.get('#inputEmail').type('automation');
|
||||||
|
cy.get('#inputPassword').type('password');
|
||||||
|
|
||||||
|
cy.get('[data-test=signinbtn]').click();
|
||||||
|
cy.url().should('include', '/search')
|
||||||
|
});
|
||||||
|
})
|
198
src/Ombi/cypress/integration/usermanagement.spec.js
Normal file
198
src/Ombi/cypress/integration/usermanagement.spec.js
Normal file
|
@ -0,0 +1,198 @@
|
||||||
|
/// <reference types="Cypress" />
|
||||||
|
|
||||||
|
describe('User Management Page', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
cy.login('automation', 'password');
|
||||||
|
cy.createUser('userToDelete', 'password', [{
|
||||||
|
value: "requestmovie",
|
||||||
|
Enabled: "true",
|
||||||
|
}]);
|
||||||
|
|
||||||
|
cy.createUser('userToEdit', 'password', [{
|
||||||
|
value: "requestmovie",
|
||||||
|
Enabled: "true",
|
||||||
|
}]);
|
||||||
|
|
||||||
|
cy.visit('/usermanagement');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Loads users table', function () {
|
||||||
|
cy.contains("User Management");
|
||||||
|
cy.contains("Add User To Ombi");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Creates basic user', function () {
|
||||||
|
cy.get('[data-test=adduserbtn').click();
|
||||||
|
cy.url().should('include', '/user');
|
||||||
|
|
||||||
|
// Setup the form
|
||||||
|
cy.get('#username').type("user1");
|
||||||
|
cy.get('#alias').type("alias1");
|
||||||
|
cy.get('#emailAddress').type("user1@emailaddress.com");
|
||||||
|
cy.get('#password').type("password");
|
||||||
|
cy.get('#confirmPass').type("password");
|
||||||
|
|
||||||
|
// setup the roles
|
||||||
|
cy.contains('Roles').click()
|
||||||
|
cy.get('#labelRequestTv').click();
|
||||||
|
cy.get('#labelRequestMovie').click();
|
||||||
|
|
||||||
|
// submit user
|
||||||
|
cy.get('[data-test=createuserbtn]').click();
|
||||||
|
|
||||||
|
cy.verifyNotification('has been created successfully');
|
||||||
|
|
||||||
|
// Also check if the user is in the table
|
||||||
|
cy.contains('alias1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Tries to create user without roles', function () {
|
||||||
|
cy.get('[data-test=adduserbtn').click();
|
||||||
|
cy.url().should('include', '/user');
|
||||||
|
|
||||||
|
// Setup the form
|
||||||
|
cy.get('#username').type("user1");
|
||||||
|
cy.get('#alias').type("alias1");
|
||||||
|
cy.get('#emailAddress').type("user1@emailaddress.com");
|
||||||
|
cy.get('#password').type("password");
|
||||||
|
cy.get('#confirmPass').type("password");
|
||||||
|
|
||||||
|
// submit user
|
||||||
|
cy.get('[data-test=createuserbtn]').click();
|
||||||
|
|
||||||
|
cy.verifyNotification('Please assign a role');
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Tries to create user when passwords do not match', function () {
|
||||||
|
cy.get('[data-test=adduserbtn').click();
|
||||||
|
cy.url().should('include', '/user');
|
||||||
|
|
||||||
|
// Setup the form
|
||||||
|
cy.get('#username').type("user1");
|
||||||
|
cy.get('#alias').type("alias1");
|
||||||
|
cy.get('#emailAddress').type("user1@emailaddress.com");
|
||||||
|
cy.get('#password').type("password");
|
||||||
|
cy.get('#confirmPass').type("pass22word");
|
||||||
|
|
||||||
|
// submit user
|
||||||
|
cy.get('[data-test=createuserbtn]').click();
|
||||||
|
|
||||||
|
cy.verifyNotification('Passwords do not match');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Delete a user', function () {
|
||||||
|
cy.get('#edituserToDelete').click();
|
||||||
|
cy.contains('User: userToDelete');
|
||||||
|
cy.get('[data-test=deletebtn]').click();
|
||||||
|
cy.contains('Are you sure that you want to delete this user?');
|
||||||
|
cy.contains('Yes').click();
|
||||||
|
cy.verifyNotification('was deleted');
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
it('Add request limits to a user', function () {
|
||||||
|
cy.get('#edituserToEdit').click();
|
||||||
|
|
||||||
|
cy.contains('Request Limits').click();
|
||||||
|
cy.get('#movieRequestLimit').clear().type(2);
|
||||||
|
cy.get('#musicRequestLimit').clear().type(3);
|
||||||
|
cy.get('#episodeRequestLimit').clear().type(4);
|
||||||
|
|
||||||
|
// submit user
|
||||||
|
cy.get('[data-test=updatebtn]').click();
|
||||||
|
|
||||||
|
cy.verifyNotification('successfully');
|
||||||
|
|
||||||
|
// Verify that the limits are set
|
||||||
|
cy.get('#edituserToEdit').click();
|
||||||
|
cy.contains('Request Limits').click();
|
||||||
|
cy.get('#movieRequestLimit').should('have.attr', 'ng-reflect-model', '2')
|
||||||
|
cy.get('#musicRequestLimit').should('have.attr', 'ng-reflect-model', '3')
|
||||||
|
cy.get('#episodeRequestLimit').should('have.attr', 'ng-reflect-model', '4')
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Add notification preferences to user', function () {
|
||||||
|
|
||||||
|
cy.get('#edituserToEdit').click();
|
||||||
|
|
||||||
|
cy.contains('Notification Preferences').click();
|
||||||
|
cy.get('[data-test=Discord]').clear().type("Discord");
|
||||||
|
cy.get('[data-test=Pushbullet]').clear().type("Pushbullet");
|
||||||
|
cy.get('[data-test=Pushover]').clear().type("Pushover");
|
||||||
|
cy.get('[data-test=Telegram]').clear().type("Telegram");
|
||||||
|
cy.get('[data-test=Slack]').clear().type("Slack");
|
||||||
|
cy.get('[data-test=Mattermost]').clear().type("Mattermost");
|
||||||
|
|
||||||
|
// submit user
|
||||||
|
cy.get('[data-test=updatebtn]').click();
|
||||||
|
|
||||||
|
cy.verifyNotification('successfully');
|
||||||
|
|
||||||
|
// Verify that the limits are set
|
||||||
|
cy.get('#edituserToEdit').click();
|
||||||
|
cy.contains('Notification Preferences').click();
|
||||||
|
cy.get('[data-test=Discord]').should('have.attr', 'ng-reflect-model', "Discord");
|
||||||
|
cy.get('[data-test=Pushbullet]').should('have.attr', 'ng-reflect-model', "Pushbullet");
|
||||||
|
cy.get('[data-test=Pushover]').should('have.attr', 'ng-reflect-model', "Pushover");
|
||||||
|
cy.get('[data-test=Telegram]').should('have.attr', 'ng-reflect-model', "Telegram");
|
||||||
|
cy.get('[data-test=Slack]').should('have.attr', 'ng-reflect-model', "Slack");
|
||||||
|
cy.get('[data-test=Mattermost]').should('have.attr', 'ng-reflect-model', "Mattermost");
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Modify roles', function () {
|
||||||
|
|
||||||
|
cy.get('#edituserToEdit').click();
|
||||||
|
|
||||||
|
cy.contains('Roles').click();
|
||||||
|
cy.get('#labelRequestMovie').click();
|
||||||
|
cy.get('#labelRequestTv').click();
|
||||||
|
|
||||||
|
// submit user
|
||||||
|
cy.get('[data-test=updatebtn]').click();
|
||||||
|
|
||||||
|
cy.verifyNotification('successfully');
|
||||||
|
|
||||||
|
// Verify that the limits are set
|
||||||
|
cy.get('#edituserToEdit').click();
|
||||||
|
cy.contains('Roles').click();
|
||||||
|
cy.get('#createRequestMovie').should('have.attr', 'ng-reflect-model', 'true');
|
||||||
|
cy.get('#createRequestTv').should('have.attr', 'ng-reflect-model', 'true');
|
||||||
|
cy.get('#createDisabled').should('have.attr', 'ng-reflect-model', 'false');
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Update local users info', function () {
|
||||||
|
|
||||||
|
cy.get('#userDropdown').click();
|
||||||
|
cy.get('#updateUserDetails').click();
|
||||||
|
|
||||||
|
cy.url().should('include','/updatedetails');
|
||||||
|
|
||||||
|
cy.get('#emailAddress').clear().type("user11@emailaddress.com");
|
||||||
|
cy.get('#currentPassword').type("password");
|
||||||
|
|
||||||
|
cy.get('[data-test=submitbtn]').click();
|
||||||
|
|
||||||
|
cy.verifyNotification('All of your details have now been updated');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Update local users info with bad password', function () {
|
||||||
|
|
||||||
|
cy.get('#userDropdown').click();
|
||||||
|
cy.get('#updateUserDetails').click();
|
||||||
|
|
||||||
|
cy.url().should('include','/updatedetails');
|
||||||
|
|
||||||
|
cy.get('#emailAddress').clear().type("user11@emailaddress.com");
|
||||||
|
cy.get('#currentPassword').type("password32113123123");
|
||||||
|
|
||||||
|
cy.get('[data-test=submitbtn]').click();
|
||||||
|
|
||||||
|
cy.verifyNotification('Your password is incorrect');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
21
src/Ombi/cypress/integration/wizard.spec.js
Normal file
21
src/Ombi/cypress/integration/wizard.spec.js
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
describe('Wizard Setup Tests', function() {
|
||||||
|
it('Setup Wizard User', function() {
|
||||||
|
cy.visit('http://localhost:3577/');
|
||||||
|
cy.url().should('include', '/Wizard')
|
||||||
|
|
||||||
|
cy.get('[data-test=nextbtn]').click();
|
||||||
|
|
||||||
|
// Media server page
|
||||||
|
cy.contains('Please choose your media server');
|
||||||
|
cy.get('[data-test=skipbtn]').click();
|
||||||
|
|
||||||
|
// Create user
|
||||||
|
cy.contains('Create the Admin account');
|
||||||
|
cy.get('#adminUsername').type('automation');
|
||||||
|
cy.get('#adminPassword').type('password');
|
||||||
|
|
||||||
|
// Submit user
|
||||||
|
cy.get('[data-test=createuserbtn]').click();
|
||||||
|
cy.contains('Sign in');
|
||||||
|
})
|
||||||
|
})
|
17
src/Ombi/cypress/plugins/index.js
Normal file
17
src/Ombi/cypress/plugins/index.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
// ***********************************************************
|
||||||
|
// This example plugins/index.js can be used to load plugins
|
||||||
|
//
|
||||||
|
// You can change the location of this file or turn off loading
|
||||||
|
// the plugins file with the 'pluginsFile' configuration option.
|
||||||
|
//
|
||||||
|
// You can read more here:
|
||||||
|
// https://on.cypress.io/plugins-guide
|
||||||
|
// ***********************************************************
|
||||||
|
|
||||||
|
// This function is called when a project is opened or re-opened (e.g. due to
|
||||||
|
// the project's config changing)
|
||||||
|
|
||||||
|
module.exports = (on, config) => {
|
||||||
|
// `on` is used to hook into various events Cypress emits
|
||||||
|
// `config` is the resolved Cypress config
|
||||||
|
}
|
59
src/Ombi/cypress/support/commands.js
Normal file
59
src/Ombi/cypress/support/commands.js
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
// ***********************************************
|
||||||
|
// This example commands.js shows you how to
|
||||||
|
// create various custom commands and overwrite
|
||||||
|
// existing commands.
|
||||||
|
//
|
||||||
|
// For more comprehensive examples of custom
|
||||||
|
// commands please read more here:
|
||||||
|
// https://on.cypress.io/custom-commands
|
||||||
|
// ***********************************************
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This is a parent command --
|
||||||
|
// Cypress.Commands.add("login", (email, password) => { ... })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This is a child command --
|
||||||
|
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This is a dual command --
|
||||||
|
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This is will overwrite an existing command --
|
||||||
|
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
||||||
|
|
||||||
|
Cypress.Commands.add('login', (username, password) => {
|
||||||
|
cy.request({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/v1/token',
|
||||||
|
body: {
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((resp) => {
|
||||||
|
window.localStorage.setItem('id_token', resp.body.access_token)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add('createUser', (username, password, claims) => {
|
||||||
|
cy.request({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/v1/identity',
|
||||||
|
body: {
|
||||||
|
UserName: username,
|
||||||
|
Password: password,
|
||||||
|
Claims: claims,
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ' + window.localStorage.getItem('id_token'),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add('verifyNotification', (text) => {
|
||||||
|
cy.get('.ui-growl-title').should('be.visible');
|
||||||
|
cy.get('.ui-growl-title').next().contains(text)
|
||||||
|
});
|
20
src/Ombi/cypress/support/index.js
Normal file
20
src/Ombi/cypress/support/index.js
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
// ***********************************************************
|
||||||
|
// This example support/index.js is processed and
|
||||||
|
// loaded automatically before your test files.
|
||||||
|
//
|
||||||
|
// This is a great place to put global configuration and
|
||||||
|
// behavior that modifies Cypress.
|
||||||
|
//
|
||||||
|
// You can change the location of this file or turn off
|
||||||
|
// automatically serving support files with the
|
||||||
|
// 'supportFile' configuration option.
|
||||||
|
//
|
||||||
|
// You can read more here:
|
||||||
|
// https://on.cypress.io/configuration
|
||||||
|
// ***********************************************************
|
||||||
|
|
||||||
|
// Import commands.js using ES2015 syntax:
|
||||||
|
import './commands'
|
||||||
|
|
||||||
|
// Alternatively you can use CommonJS syntax:
|
||||||
|
// require('./commands')
|
|
@ -4,10 +4,12 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"vendor": "gulp vendor",
|
"vendor": "gulp vendor",
|
||||||
|
"main": "gulp main",
|
||||||
"lint": "tslint -p .",
|
"lint": "tslint -p .",
|
||||||
"publish": "gulp publish",
|
"publish": "gulp publish",
|
||||||
"restore": "dotnet restore && yarn install",
|
"restore": "dotnet restore && yarn install",
|
||||||
"clean": "gulp clean"
|
"clean": "gulp clean",
|
||||||
|
"cypress": "cypress open"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^6.0.7",
|
"@angular/animations": "^6.0.7",
|
||||||
|
@ -90,5 +92,8 @@
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"@types/tapable": "1.0.0"
|
"@types/tapable": "1.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"cypress": "^3.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
"lib": [
|
"lib": [
|
||||||
|
"es2015",
|
||||||
"es2017",
|
"es2017",
|
||||||
"dom"
|
"dom"
|
||||||
],
|
],
|
||||||
|
@ -27,6 +28,9 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"types": [
|
||||||
|
"cypress"
|
||||||
|
],
|
||||||
"include": [
|
"include": [
|
||||||
"ClientApp/**/*",
|
"ClientApp/**/*",
|
||||||
"typings/**/*",
|
"typings/**/*",
|
||||||
|
|
|
@ -12,8 +12,8 @@
|
||||||
"Common": {
|
"Common": {
|
||||||
"ContinueButton": "Fortsæt",
|
"ContinueButton": "Fortsæt",
|
||||||
"Available": "Tilgængelig",
|
"Available": "Tilgængelig",
|
||||||
"PartiallyAvailable": "Partially Available",
|
"PartiallyAvailable": "Delvist tilgængelig",
|
||||||
"Monitored": "Monitored",
|
"Monitored": "Overvåget",
|
||||||
"NotAvailable": "Ikke tilgængelig",
|
"NotAvailable": "Ikke tilgængelig",
|
||||||
"ProcessingRequest": "Behandler anmodning",
|
"ProcessingRequest": "Behandler anmodning",
|
||||||
"PendingApproval": "Afventer godkendelse",
|
"PendingApproval": "Afventer godkendelse",
|
||||||
|
@ -48,6 +48,7 @@
|
||||||
"Requests": "Anmodninger",
|
"Requests": "Anmodninger",
|
||||||
"UserManagement": "Brugeradministration",
|
"UserManagement": "Brugeradministration",
|
||||||
"Issues": "Problemer",
|
"Issues": "Problemer",
|
||||||
|
"Vote": "Stem",
|
||||||
"Donate": "Donér!",
|
"Donate": "Donér!",
|
||||||
"DonateLibraryMaintainer": "Donér til vedligeholder af bibliotek",
|
"DonateLibraryMaintainer": "Donér til vedligeholder af bibliotek",
|
||||||
"DonateTooltip": "Sådan overbeviser jeg min kone om, at jeg skal bruge min fritid på at udvikle Ombi :)",
|
"DonateTooltip": "Sådan overbeviser jeg min kone om, at jeg skal bruge min fritid på at udvikle Ombi :)",
|
||||||
|
@ -56,36 +57,23 @@
|
||||||
"Welcome": "Velkommen til {{username}}",
|
"Welcome": "Velkommen til {{username}}",
|
||||||
"UpdateDetails": "Opdater loginoplysninger",
|
"UpdateDetails": "Opdater loginoplysninger",
|
||||||
"Logout": "Log af",
|
"Logout": "Log af",
|
||||||
"Language": {
|
|
||||||
"English": "Engelsk",
|
|
||||||
"French": "Fransk",
|
|
||||||
"Spanish": "Spansk",
|
|
||||||
"German": "Tysk",
|
|
||||||
"Italian": "Italiensk",
|
|
||||||
"Danish": "Dansk",
|
|
||||||
"Dutch": "Hollandsk",
|
|
||||||
"Norwegian": "Norsk",
|
|
||||||
"BrazillianPortuguese": "Brazillian Portuguese",
|
|
||||||
"Polish": "Polish",
|
|
||||||
"Swedish": "Swedish"
|
|
||||||
},
|
|
||||||
"OpenMobileApp": "Åbn mobilapp",
|
"OpenMobileApp": "Åbn mobilapp",
|
||||||
"RecentlyAdded": "Recently Added"
|
"RecentlyAdded": "Senest tilføjet"
|
||||||
},
|
},
|
||||||
"Search": {
|
"Search": {
|
||||||
"Title": "Søg",
|
"Title": "Søg",
|
||||||
"Paragraph": "Ønsker du at se noget, som er utilgængeligt? intet problem, bare søg efter det nedenfor og anmod om det!",
|
"Paragraph": "Ønsker du at se noget, som er utilgængeligt? intet problem, bare søg efter det nedenfor og anmod om det!",
|
||||||
"MoviesTab": "Film",
|
"MoviesTab": "Film",
|
||||||
"TvTab": "Tv-serier",
|
"TvTab": "Tv-serier",
|
||||||
"MusicTab": "Music",
|
"MusicTab": "Musik",
|
||||||
"Suggestions": "Forslag",
|
"Suggestions": "Forslag",
|
||||||
"NoResults": "Beklager, vi fandt ingen resultater!",
|
"NoResults": "Beklager, vi fandt ingen resultater!",
|
||||||
"DigitalDate": "Digital Release: {{date}}",
|
"DigitalDate": "Digital udgivelse: {{date}}",
|
||||||
"TheatricalRelease": "Theatrical Release: {{date}}",
|
"TheatricalRelease": "Biografudgivelse: {{date}}",
|
||||||
"ViewOnPlex": "Se på Plex",
|
"ViewOnPlex": "Se på Plex",
|
||||||
"ViewOnEmby": "Se på Emby",
|
"ViewOnEmby": "Se på Emby",
|
||||||
"RequestAdded": "{{title}} er anmodet med succes",
|
"RequestAdded": "{{title}} er anmodet med succes",
|
||||||
"Similar": "Similar",
|
"Similar": "Lignende",
|
||||||
"Movies": {
|
"Movies": {
|
||||||
"PopularMovies": "Populære film",
|
"PopularMovies": "Populære film",
|
||||||
"UpcomingMovies": "Kommende film",
|
"UpcomingMovies": "Kommende film",
|
||||||
|
@ -115,15 +103,15 @@
|
||||||
"Paragraph": "Herunder kan du se dine og alle andre anmodninger, samt status for download og godkendelse.",
|
"Paragraph": "Herunder kan du se dine og alle andre anmodninger, samt status for download og godkendelse.",
|
||||||
"MoviesTab": "Film",
|
"MoviesTab": "Film",
|
||||||
"TvTab": "Tv-serier",
|
"TvTab": "Tv-serier",
|
||||||
"MusicTab": "Music",
|
"MusicTab": "Musik",
|
||||||
"RequestedBy": "Anmodet af:",
|
"RequestedBy": "Anmodet af:",
|
||||||
"Status": "Status:",
|
"Status": "Status:",
|
||||||
"RequestStatus": "Status for anmodning:",
|
"RequestStatus": "Status for anmodning:",
|
||||||
"Denied": " Afvist:",
|
"Denied": " Afvist:",
|
||||||
"TheatricalRelease": "Theatrical Release: {{date}}",
|
"TheatricalRelease": "Biografudgivelse: {{date}}",
|
||||||
"ReleaseDate": "Released: {{date}}",
|
"ReleaseDate": "Udgivet: {{date}}",
|
||||||
"TheatricalReleaseSort": "Theatrical Release",
|
"TheatricalReleaseSort": "Biografudgivelse",
|
||||||
"DigitalRelease": "Digital Release: {{date}}",
|
"DigitalRelease": "Digital udgivelse: {{date}}",
|
||||||
"RequestDate": "Dato for anmodning:",
|
"RequestDate": "Dato for anmodning:",
|
||||||
"QualityOverride": "Tilsidesæt kvalitet:",
|
"QualityOverride": "Tilsidesæt kvalitet:",
|
||||||
"RootFolderOverride": "Tilsidesæt rodmappe:",
|
"RootFolderOverride": "Tilsidesæt rodmappe:",
|
||||||
|
@ -139,20 +127,20 @@
|
||||||
"GridStatus": "Status",
|
"GridStatus": "Status",
|
||||||
"ReportIssue": "Rapportér problem",
|
"ReportIssue": "Rapportér problem",
|
||||||
"Filter": "Filter",
|
"Filter": "Filter",
|
||||||
"Sort": "Sort",
|
"Sort": "Sorter",
|
||||||
"SeasonNumberHeading": "Sæson: {seasonNumber}",
|
"SeasonNumberHeading": "Sæson: {seasonNumber}",
|
||||||
"SortTitleAsc": "Title ▲",
|
"SortTitleAsc": "Titel ▲",
|
||||||
"SortTitleDesc": "Title ▼",
|
"SortTitleDesc": "Titel ▼",
|
||||||
"SortRequestDateAsc": "Request Date ▲",
|
"SortRequestDateAsc": "Dato for anmodning ▲",
|
||||||
"SortRequestDateDesc": "Request Date ▼",
|
"SortRequestDateDesc": "Dato for anmodning ▼",
|
||||||
"SortStatusAsc": "Status ▲",
|
"SortStatusAsc": "Status ▲",
|
||||||
"SortStatusDesc": "Status ▼",
|
"SortStatusDesc": "Status ▼",
|
||||||
"Remaining": {
|
"Remaining": {
|
||||||
"Quota": "{{remaining}}/{{total}} requests remaining",
|
"Quota": "{{remaining}}/{{total}} anmodninger, der er tilbage",
|
||||||
"NextDays": "Another request will be added in {{time}} days",
|
"NextDays": "En anden anmodning vil blive tilføjet i {{time}} Dage",
|
||||||
"NextHours": "Another request will be added in {{time}} hours",
|
"NextHours": "En anden anmodning vil blive tilføjet i {{time}} Timer",
|
||||||
"NextMinutes": "Another request will be added in {{time}} minutes",
|
"NextMinutes": "En anden anmodning vil blive tilføjet i {{time}} Minutter",
|
||||||
"NextMinute": "Another request will be added in {{time}} minute"
|
"NextMinute": "En anden anmodning vil blive tilføjet i {{time}} Minut"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Issues": {
|
"Issues": {
|
||||||
|
@ -179,14 +167,18 @@
|
||||||
"FilterHeaderAvailability": "Tilgængelighed",
|
"FilterHeaderAvailability": "Tilgængelighed",
|
||||||
"FilterHeaderRequestStatus": "Status",
|
"FilterHeaderRequestStatus": "Status",
|
||||||
"Approved": "Godkendt",
|
"Approved": "Godkendt",
|
||||||
"PendingApproval": "Pending Approval"
|
"PendingApproval": "Afventer godkendelse"
|
||||||
},
|
},
|
||||||
"UserManagment": {
|
"UserManagment": {
|
||||||
"TvRemaining": "TV: {{remaining}}/{{total}} remaining",
|
"TvRemaining": "Tv: {{remaining}}/{{total}} Resterende",
|
||||||
"MovieRemaining": "Movies: {{remaining}}/{{total}} remaining",
|
"MovieRemaining": "Film: {{remaining}}/{{total}} Resterende",
|
||||||
"MusicRemaining": "Music: {{remaining}}/{{total}} remaining",
|
"MusicRemaining": "Musik: {{remaining}}/{{total}} Resterende",
|
||||||
"TvDue": "TV: {{date}}",
|
"TvDue": "Tv: {{date}}",
|
||||||
"MovieDue": "Movie: {{date}}",
|
"MovieDue": "Film: {{date}}",
|
||||||
"MusicDue": "Music: {{date}}"
|
"MusicDue": "Musik: {{date}}"
|
||||||
|
},
|
||||||
|
"Votes": {
|
||||||
|
"CompletedVotesTab": "Stemt",
|
||||||
|
"VotesTab": "Nødvendige stemmer"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -48,6 +48,7 @@
|
||||||
"Requests": "Anfragen",
|
"Requests": "Anfragen",
|
||||||
"UserManagement": "Benutzerverwaltung",
|
"UserManagement": "Benutzerverwaltung",
|
||||||
"Issues": "Probleme",
|
"Issues": "Probleme",
|
||||||
|
"Vote": "Bewerten",
|
||||||
"Donate": "Spenden!",
|
"Donate": "Spenden!",
|
||||||
"DonateLibraryMaintainer": "Spende sie an den Bibliotheks Betreuer",
|
"DonateLibraryMaintainer": "Spende sie an den Bibliotheks Betreuer",
|
||||||
"DonateTooltip": "So überzeuge ich meine Frau, meine Freizeit mit der Entwicklung von Ombi zu verbringen ;)",
|
"DonateTooltip": "So überzeuge ich meine Frau, meine Freizeit mit der Entwicklung von Ombi zu verbringen ;)",
|
||||||
|
@ -56,36 +57,23 @@
|
||||||
"Welcome": "Willkommen {{username}}",
|
"Welcome": "Willkommen {{username}}",
|
||||||
"UpdateDetails": "Update-Details",
|
"UpdateDetails": "Update-Details",
|
||||||
"Logout": "Ausloggen",
|
"Logout": "Ausloggen",
|
||||||
"Language": {
|
|
||||||
"English": "Englisch",
|
|
||||||
"French": "Französisch",
|
|
||||||
"Spanish": "Spanisch",
|
|
||||||
"German": "Deutsch",
|
|
||||||
"Italian": "Italienisch",
|
|
||||||
"Danish": "Dänisch",
|
|
||||||
"Dutch": "Niederländisch",
|
|
||||||
"Norwegian": "Norwegisch",
|
|
||||||
"BrazillianPortuguese": "Brazillian Portuguese",
|
|
||||||
"Polish": "Polish",
|
|
||||||
"Swedish": "Swedish"
|
|
||||||
},
|
|
||||||
"OpenMobileApp": "Mobile App",
|
"OpenMobileApp": "Mobile App",
|
||||||
"RecentlyAdded": "Recently Added"
|
"RecentlyAdded": "Kürzlich hinzugefügt"
|
||||||
},
|
},
|
||||||
"Search": {
|
"Search": {
|
||||||
"Title": "Suche",
|
"Title": "Suche",
|
||||||
"Paragraph": "Möchtest du etwas sehen, das nicht verfügbar ist? Kein Problem, benutze einfach die Suchbox und fordere es an!",
|
"Paragraph": "Möchtest du etwas sehen, das nicht verfügbar ist? Kein Problem, benutze einfach die Suchbox und fordere es an!",
|
||||||
"MoviesTab": "Filme",
|
"MoviesTab": "Filme",
|
||||||
"TvTab": "Serien",
|
"TvTab": "Serien",
|
||||||
"MusicTab": "Music",
|
"MusicTab": "Musik",
|
||||||
"Suggestions": "Vorschläge",
|
"Suggestions": "Vorschläge",
|
||||||
"NoResults": "Es tut uns leid, wir haben keine Ergebnisse gefunden!",
|
"NoResults": "Es tut uns leid, wir haben keine Ergebnisse gefunden!",
|
||||||
"DigitalDate": "Digital Release: {{date}}",
|
"DigitalDate": "Digital Release: {{date}}",
|
||||||
"TheatricalRelease": "Theatrical Release: {{date}}",
|
"TheatricalRelease": "Kinostart: {{date}}",
|
||||||
"ViewOnPlex": "In Plex anschauen",
|
"ViewOnPlex": "In Plex anschauen",
|
||||||
"ViewOnEmby": "In Emby anschauen",
|
"ViewOnEmby": "In Emby anschauen",
|
||||||
"RequestAdded": "Anfrage für {{title}} wurde erfolgreich hinzugefügt",
|
"RequestAdded": "Anfrage für {{title}} wurde erfolgreich hinzugefügt",
|
||||||
"Similar": "Similar",
|
"Similar": "Ähnliche",
|
||||||
"Movies": {
|
"Movies": {
|
||||||
"PopularMovies": "Beliebte Filme",
|
"PopularMovies": "Beliebte Filme",
|
||||||
"UpcomingMovies": "Kommende Filme",
|
"UpcomingMovies": "Kommende Filme",
|
||||||
|
@ -115,14 +103,14 @@
|
||||||
"Paragraph": "Unten sehen Sie Ihre und alle anderen Anfragen, sowie deren Download und Genehmigungsstatus.",
|
"Paragraph": "Unten sehen Sie Ihre und alle anderen Anfragen, sowie deren Download und Genehmigungsstatus.",
|
||||||
"MoviesTab": "Filme",
|
"MoviesTab": "Filme",
|
||||||
"TvTab": "Serien",
|
"TvTab": "Serien",
|
||||||
"MusicTab": "Music",
|
"MusicTab": "Musik",
|
||||||
"RequestedBy": "Angefordert von:",
|
"RequestedBy": "Angefordert von:",
|
||||||
"Status": "Status:",
|
"Status": "Status:",
|
||||||
"RequestStatus": "Anfrage Status:",
|
"RequestStatus": "Anfrage Status:",
|
||||||
"Denied": " Abgelehnt:",
|
"Denied": " Abgelehnt:",
|
||||||
"TheatricalRelease": "Theatrical Release: {{date}}",
|
"TheatricalRelease": "Kinostart: {{date}}",
|
||||||
"ReleaseDate": "Veröffentlicht: {{date}}",
|
"ReleaseDate": "Veröffentlicht: {{date}}",
|
||||||
"TheatricalReleaseSort": "Theatrical Release",
|
"TheatricalReleaseSort": "Kinostart",
|
||||||
"DigitalRelease": "Digital Release: {{date}}",
|
"DigitalRelease": "Digital Release: {{date}}",
|
||||||
"RequestDate": "Datum der Anfrage:",
|
"RequestDate": "Datum der Anfrage:",
|
||||||
"QualityOverride": "Qualitäts Überschreiben:",
|
"QualityOverride": "Qualitäts Überschreiben:",
|
||||||
|
@ -139,10 +127,10 @@
|
||||||
"GridStatus": "Status",
|
"GridStatus": "Status",
|
||||||
"ReportIssue": "Problem melden",
|
"ReportIssue": "Problem melden",
|
||||||
"Filter": "Filter",
|
"Filter": "Filter",
|
||||||
"Sort": "Sort",
|
"Sort": "Sortieren",
|
||||||
"SeasonNumberHeading": "Staffel: {seasonNumber}",
|
"SeasonNumberHeading": "Staffel: {seasonNumber}",
|
||||||
"SortTitleAsc": "Title ▲",
|
"SortTitleAsc": "Titel ▲",
|
||||||
"SortTitleDesc": "Title ▼",
|
"SortTitleDesc": "Titel ▼",
|
||||||
"SortRequestDateAsc": "Request Date ▲",
|
"SortRequestDateAsc": "Request Date ▲",
|
||||||
"SortRequestDateDesc": "Request Date ▼",
|
"SortRequestDateDesc": "Request Date ▼",
|
||||||
"SortStatusAsc": "Status ▲",
|
"SortStatusAsc": "Status ▲",
|
||||||
|
@ -179,14 +167,18 @@
|
||||||
"FilterHeaderAvailability": "Verfügbarkeit",
|
"FilterHeaderAvailability": "Verfügbarkeit",
|
||||||
"FilterHeaderRequestStatus": "Status",
|
"FilterHeaderRequestStatus": "Status",
|
||||||
"Approved": "Bestätigt",
|
"Approved": "Bestätigt",
|
||||||
"PendingApproval": "Pending Approval"
|
"PendingApproval": "Genehmigung ausstehend"
|
||||||
},
|
},
|
||||||
"UserManagment": {
|
"UserManagment": {
|
||||||
"TvRemaining": "TV: {{remaining}}/{{total}} remaining",
|
"TvRemaining": "TV: {{remaining}}/{{total}} remaining",
|
||||||
"MovieRemaining": "Movies: {{remaining}}/{{total}} remaining",
|
"MovieRemaining": "Movies: {{remaining}}/{{total}} remaining",
|
||||||
"MusicRemaining": "Music: {{remaining}}/{{total}} remaining",
|
"MusicRemaining": "Music: {{remaining}}/{{total}} remaining",
|
||||||
"TvDue": "TV: {{date}}",
|
"TvDue": "TV: {{date}}",
|
||||||
"MovieDue": "Movie: {{date}}",
|
"MovieDue": "Film: {{date}}",
|
||||||
"MusicDue": "Music: {{date}}"
|
"MusicDue": "Musik: {{date}}"
|
||||||
|
},
|
||||||
|
"Votes": {
|
||||||
|
"CompletedVotesTab": "Bewertet",
|
||||||
|
"VotesTab": "Erforderliche Bewertungen"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -50,6 +50,7 @@
|
||||||
"Requests": "Requests",
|
"Requests": "Requests",
|
||||||
"UserManagement": "User Management",
|
"UserManagement": "User Management",
|
||||||
"Issues":"Issues",
|
"Issues":"Issues",
|
||||||
|
"Vote":"Vote",
|
||||||
"Donate": "Donate!",
|
"Donate": "Donate!",
|
||||||
"DonateLibraryMaintainer": "Donate to Library Maintainer",
|
"DonateLibraryMaintainer": "Donate to Library Maintainer",
|
||||||
"DonateTooltip":
|
"DonateTooltip":
|
||||||
|
@ -59,19 +60,6 @@
|
||||||
"Welcome": "Welcome {{username}}",
|
"Welcome": "Welcome {{username}}",
|
||||||
"UpdateDetails": "Update Details",
|
"UpdateDetails": "Update Details",
|
||||||
"Logout": "Logout",
|
"Logout": "Logout",
|
||||||
"Language": {
|
|
||||||
"English": "English",
|
|
||||||
"French": "French",
|
|
||||||
"Spanish": "Spanish",
|
|
||||||
"German": "German",
|
|
||||||
"Italian": "Italian",
|
|
||||||
"Danish": "Danish",
|
|
||||||
"Dutch": "Dutch",
|
|
||||||
"Norwegian":"Norwegian",
|
|
||||||
"BrazillianPortuguese": "Brazillian Portuguese",
|
|
||||||
"Polish":"Polish",
|
|
||||||
"Swedish":"Swedish"
|
|
||||||
},
|
|
||||||
"OpenMobileApp":"Open Mobile App",
|
"OpenMobileApp":"Open Mobile App",
|
||||||
"RecentlyAdded":"Recently Added"
|
"RecentlyAdded":"Recently Added"
|
||||||
},
|
},
|
||||||
|
@ -193,5 +181,9 @@
|
||||||
"TvDue": "TV: {{date}}",
|
"TvDue": "TV: {{date}}",
|
||||||
"MovieDue": "Movie: {{date}}",
|
"MovieDue": "Movie: {{date}}",
|
||||||
"MusicDue": "Music: {{date}}"
|
"MusicDue": "Music: {{date}}"
|
||||||
|
},
|
||||||
|
"Votes": {
|
||||||
|
"CompletedVotesTab": "Voted",
|
||||||
|
"VotesTab": "Votes Needed"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,6 +48,7 @@
|
||||||
"Requests": "Solicitudes",
|
"Requests": "Solicitudes",
|
||||||
"UserManagement": "Gestión de usuarios",
|
"UserManagement": "Gestión de usuarios",
|
||||||
"Issues": "Incidencias",
|
"Issues": "Incidencias",
|
||||||
|
"Vote": "Vote",
|
||||||
"Donate": "¡Donar!",
|
"Donate": "¡Donar!",
|
||||||
"DonateLibraryMaintainer": "Donate to Library Maintainer",
|
"DonateLibraryMaintainer": "Donate to Library Maintainer",
|
||||||
"DonateTooltip": "Para que mi esposa me deje desarrollar Ombi ;)",
|
"DonateTooltip": "Para que mi esposa me deje desarrollar Ombi ;)",
|
||||||
|
@ -56,19 +57,6 @@
|
||||||
"Welcome": "Bienvenido {{username}}",
|
"Welcome": "Bienvenido {{username}}",
|
||||||
"UpdateDetails": "Detalles de la actualización",
|
"UpdateDetails": "Detalles de la actualización",
|
||||||
"Logout": "Cerrar sesión",
|
"Logout": "Cerrar sesión",
|
||||||
"Language": {
|
|
||||||
"English": "Inglés",
|
|
||||||
"French": "Francés",
|
|
||||||
"Spanish": "Español",
|
|
||||||
"German": "Alemán",
|
|
||||||
"Italian": "Italiano",
|
|
||||||
"Danish": "Danés",
|
|
||||||
"Dutch": "Holandés",
|
|
||||||
"Norwegian": "Norwegian",
|
|
||||||
"BrazillianPortuguese": "Brazillian Portuguese",
|
|
||||||
"Polish": "Polish",
|
|
||||||
"Swedish": "Swedish"
|
|
||||||
},
|
|
||||||
"OpenMobileApp": "Open Mobile App",
|
"OpenMobileApp": "Open Mobile App",
|
||||||
"RecentlyAdded": "Recently Added"
|
"RecentlyAdded": "Recently Added"
|
||||||
},
|
},
|
||||||
|
@ -188,5 +176,9 @@
|
||||||
"TvDue": "TV: {{date}}",
|
"TvDue": "TV: {{date}}",
|
||||||
"MovieDue": "Movie: {{date}}",
|
"MovieDue": "Movie: {{date}}",
|
||||||
"MusicDue": "Music: {{date}}"
|
"MusicDue": "Music: {{date}}"
|
||||||
|
},
|
||||||
|
"Votes": {
|
||||||
|
"CompletedVotesTab": "Voted",
|
||||||
|
"VotesTab": "Votes Needed"
|
||||||
}
|
}
|
||||||
}
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue