Merge pull request #4194 from Ombi-app/develop

Sync up
This commit is contained in:
Twan Ariens 2021-05-18 19:51:46 +02:00 committed by GitHub
commit dde517cecc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
129 changed files with 4120 additions and 842 deletions

View file

@ -27,4 +27,4 @@ variables:
value: "4.0.$(Build.BuildId)" value: "4.0.$(Build.BuildId)"
- name: isMain - name: isMain
value: $[or(eq(variables['Build.SourceBranch'], 'refs/heads/develop'), eq(variables['Build.SourceBranch'], 'refs/heads/main'))] value: $[or(eq(variables['Build.SourceBranch'], 'refs/heads/develop'), eq(variables['Build.SourceBranch'], 'refs/heads/master'))]

1
.gitignore vendored
View file

@ -247,5 +247,6 @@ _Pvt_Extensions
# Ignore local vscode config # Ignore local vscode config
*.vscode *.vscode
/src/Ombi/database.json /src/Ombi/database.json
/src/Ombi/databases.json
/src/Ombi/healthchecksdb /src/Ombi/healthchecksdb
/src/Ombi/ClientApp/package-lock.json /src/Ombi/ClientApp/package-lock.json

View file

@ -1,39 +1,122 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Net.Mime;
namespace Ombi.Api.Radarr.Models namespace Ombi.Api.Radarr.Models
{ {
public class MovieResponse public class MovieResponse
{ {
public string title { get; set; } public string title { get; set; }
public string originalTitle { get; set; }
public Alternatetitle[] alternateTitles { get; set; }
public int secondaryYearSourceId { get; set; }
public string sortTitle { get; set; } public string sortTitle { get; set; }
public double sizeOnDisk { get; set; } public long sizeOnDisk { get; set; }
public string status { get; set; } public string status { get; set; }
public string overview { get; set; } public string overview { get; set; }
public string inCinemas { get; set; } public DateTime inCinemas { get; set; }
public string physicalRelease { get; set; } public DateTime physicalRelease { get; set; }
public List<Image> images { get; set; } public DateTime digitalRelease { get; set; }
public Image[] images { get; set; }
public string website { get; set; } public string website { get; set; }
public bool downloaded { get; set; }
public int year { get; set; } public int year { get; set; }
public bool hasFile { get; set; } public bool hasFile { get; set; }
public string youTubeTrailerId { get; set; } public string youTubeTrailerId { get; set; }
public string studio { get; set; } public string studio { get; set; }
public string path { get; set; } public string path { get; set; }
public int profileId { get; set; } public int qualityProfileId { get; set; }
public string minimumAvailability { get; set; }
public bool monitored { get; set; } public bool monitored { get; set; }
public string minimumAvailability { get; set; }
public bool isAvailable { get; set; }
public string folderName { get; set; }
public int runtime { get; set; } public int runtime { get; set; }
public string lastInfoSync { get; set; }
public string cleanTitle { get; set; } public string cleanTitle { get; set; }
public string imdbId { get; set; } public string imdbId { get; set; }
public int tmdbId { get; set; } public int tmdbId { get; set; }
public string titleSlug { get; set; } public string titleSlug { get; set; }
public List<string> genres { get; set; } public string certification { get; set; }
public List<object> tags { get; set; } public string[] genres { get; set; }
public string added { get; set; } public object[] tags { get; set; }
public DateTime added { get; set; }
public Ratings ratings { get; set; } public Ratings ratings { get; set; }
//public List<string> alternativeTitles { get; set; } public Moviefile movieFile { get; set; }
public int qualityProfileId { get; set; } public Collection collection { get; set; }
public int id { get; set; } public int id { get; set; }
} }
public class Moviefile
{
public int movieId { get; set; }
public string relativePath { get; set; }
public string path { get; set; }
public long size { get; set; }
public DateTime dateAdded { get; set; }
public string sceneName { get; set; }
public int indexerFlags { get; set; }
public V3.Quality quality { get; set; }
public Mediainfo mediaInfo { get; set; }
public string originalFilePath { get; set; }
public bool qualityCutoffNotMet { get; set; }
public Language[] languages { get; set; }
public string releaseGroup { get; set; }
public string edition { get; set; }
public int id { get; set; }
}
public class Revision
{
public int version { get; set; }
public int real { get; set; }
public bool isRepack { get; set; }
}
public class Mediainfo
{
public string audioAdditionalFeatures { get; set; }
public int audioBitrate { get; set; }
public float audioChannels { get; set; }
public string audioCodec { get; set; }
public string audioLanguages { get; set; }
public int audioStreamCount { get; set; }
public int videoBitDepth { get; set; }
public int videoBitrate { get; set; }
public string videoCodec { get; set; }
public float videoFps { get; set; }
public string resolution { get; set; }
public string runTime { get; set; }
public string scanType { get; set; }
public string subtitles { get; set; }
}
public class Language
{
public int id { get; set; }
public string name { get; set; }
}
public class Collection
{
public string name { get; set; }
public int tmdbId { get; set; }
public object[] images { get; set; }
}
public class Alternatetitle
{
public string sourceType { get; set; }
public int movieId { get; set; }
public string title { get; set; }
public int sourceId { get; set; }
public int votes { get; set; }
public int voteCount { get; set; }
public Language1 language { get; set; }
public int id { get; set; }
}
public class Language1
{
public int id { get; set; }
public string name { get; set; }
}
} }

View file

@ -28,5 +28,6 @@ namespace Ombi.Api.Radarr.Models
public string titleSlug { get; set; } public string titleSlug { get; set; }
public int year { get; set; } public int year { get; set; }
public string minimumAvailability { get; set; } public string minimumAvailability { get; set; }
public long sizeOnDisk { get; set; }
} }
} }

View file

@ -82,7 +82,8 @@ namespace Ombi.Api.Radarr
titleSlug = title + year, titleSlug = title + year,
monitored = true, monitored = true,
year = year, year = year,
minimumAvailability = minimumAvailability minimumAvailability = minimumAvailability,
sizeOnDisk = 0
}; };
if (searchNow) if (searchNow)

View file

@ -65,7 +65,7 @@ namespace Ombi.Api.Radarr
public async Task<MovieResponse> UpdateMovie(MovieResponse movie, string apiKey, string baseUrl) public async Task<MovieResponse> UpdateMovie(MovieResponse movie, string apiKey, string baseUrl)
{ {
var request = new Request($"/api/v3/movie/", baseUrl, HttpMethod.Put); var request = new Request($"/api/v3/movie/{movie.id}", baseUrl, HttpMethod.Put);
AddHeaders(request, apiKey); AddHeaders(request, apiKey);
request.AddJsonBody(movie); request.AddJsonBody(movie);
@ -85,7 +85,8 @@ namespace Ombi.Api.Radarr
titleSlug = title + year, titleSlug = title + year,
monitored = true, monitored = true,
year = year, year = year,
minimumAvailability = minimumAvailability minimumAvailability = minimumAvailability,
sizeOnDisk = 0
}; };
if (searchNow) if (searchNow)

View file

@ -19,7 +19,7 @@ namespace Ombi.Api.Webhook
public async Task PushAsync(string baseUrl, string accessToken, IDictionary<string, string> parameters) public async Task PushAsync(string baseUrl, string accessToken, IDictionary<string, string> parameters)
{ {
var request = new Request("/", baseUrl, HttpMethod.Post); var request = new Request("", baseUrl, HttpMethod.Post);
if (!string.IsNullOrWhiteSpace(accessToken)) if (!string.IsNullOrWhiteSpace(accessToken))
{ {

View file

@ -16,14 +16,14 @@ namespace Ombi.Api
{ {
public class Api : IApi public class Api : IApi
{ {
public Api(ILogger<Api> log, IOmbiHttpClient client) public Api(ILogger<Api> log, HttpClient client)
{ {
Logger = log; Logger = log;
_client = client; _client = client;
} }
private ILogger<Api> Logger { get; } private ILogger<Api> Logger { get; }
private readonly IOmbiHttpClient _client; private readonly HttpClient _client;
public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings
{ {

View file

@ -1,14 +0,0 @@
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace Ombi.Api
{
public interface IOmbiHttpClient
{
Task<HttpResponseMessage> SendAsync(HttpRequestMessage request);
Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken);
Task<string> GetStringAsync(Uri requestUri);
}
}

View file

@ -10,6 +10,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="Polly" Version="7.1.0" /> <PackageReference Include="Polly" Version="7.1.0" />

View file

@ -1,110 +0,0 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2017 Jamie Rees
// File: OmbiHttpClient.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Settings.Settings.Models;
namespace Ombi.Api
{
/// <summary>
/// The purpose of this class is simple, keep one instance of the HttpClient in play.
/// There are many articles related to when using multiple HttpClient's keeping the socket in a WAIT state
/// https://blogs.msdn.microsoft.com/alazarev/2017/12/29/disposable-finalizers-and-httpclient/
/// https://aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/
/// </summary>
public class OmbiHttpClient : IOmbiHttpClient
{
public OmbiHttpClient(ICacheService cache, ISettingsService<OmbiSettings> s)
{
_cache = cache;
_settings = s;
_runtimeVersion = AssemblyHelper.GetRuntimeVersion();
}
private static HttpClient _client;
private static HttpMessageHandler _handler;
private readonly ICacheService _cache;
private readonly ISettingsService<OmbiSettings> _settings;
private readonly string _runtimeVersion;
public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request)
{
await Setup();
return await _client.SendAsync(request);
}
public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
await Setup();
return await _client.SendAsync(request, cancellationToken);
}
public async Task<string> GetStringAsync(Uri requestUri)
{
await Setup();
return await _client.GetStringAsync(requestUri);
}
private async Task Setup()
{
if (_client == null)
{
if (_handler == null)
{
// Get the handler
_handler = await GetHandler();
}
_client = new HttpClient(_handler);
_client.DefaultRequestHeaders.Add("User-Agent", $"Ombi/{_runtimeVersion} (https://ombi.io/)");
}
}
private async Task<HttpMessageHandler> GetHandler()
{
if (_cache == null)
{
return new HttpClientHandler();
}
var settings = await _cache.GetOrAdd(CacheKeys.OmbiSettings, async () => await _settings.GetSettingsAsync(), DateTime.Now.AddHours(1));
if (settings.IgnoreCertificateErrors)
{
return new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (message, certificate2, arg3, arg4) => true,
};
}
return new HttpClientHandler();
}
}
}

View file

@ -30,7 +30,7 @@ namespace Ombi.Core.Tests.Rule.Search
public async Task Should_Not_Be_Monitored_Or_Available() public async Task Should_Not_Be_Monitored_Or_Available()
{ {
var request = new SearchAlbumViewModel { ForeignAlbumId = "abc" }; var request = new SearchAlbumViewModel { ForeignAlbumId = "abc" };
var result = await Rule.Execute(request); var result = await Rule.Execute(request, string.Empty);
Assert.True(result.Success); Assert.True(result.Success);
Assert.False(request.Approved); Assert.False(request.Approved);
@ -49,7 +49,7 @@ namespace Ombi.Core.Tests.Rule.Search
} }
}.AsQueryable()); }.AsQueryable());
var request = new SearchAlbumViewModel { ForeignAlbumId = "abc" }; var request = new SearchAlbumViewModel { ForeignAlbumId = "abc" };
var result = await Rule.Execute(request); var result = await Rule.Execute(request, string.Empty);
Assert.True(result.Success); Assert.True(result.Success);
Assert.False(request.Approved); Assert.False(request.Approved);
@ -71,7 +71,7 @@ namespace Ombi.Core.Tests.Rule.Search
} }
}.AsQueryable()); }.AsQueryable());
var request = new SearchAlbumViewModel { ForeignAlbumId = "abc" }; var request = new SearchAlbumViewModel { ForeignAlbumId = "abc" };
var result = await Rule.Execute(request); var result = await Rule.Execute(request, string.Empty);
Assert.True(result.Success); Assert.True(result.Success);
Assert.False(request.Approved); Assert.False(request.Approved);
@ -93,7 +93,7 @@ namespace Ombi.Core.Tests.Rule.Search
} }
}.AsQueryable()); }.AsQueryable());
var request = new SearchAlbumViewModel { ForeignAlbumId = "abc" }; var request = new SearchAlbumViewModel { ForeignAlbumId = "abc" };
var result = await Rule.Execute(request); var result = await Rule.Execute(request, string.Empty);
Assert.True(result.Success); Assert.True(result.Success);
Assert.False(request.Approved); Assert.False(request.Approved);
@ -114,7 +114,7 @@ namespace Ombi.Core.Tests.Rule.Search
} }
}.AsQueryable()); }.AsQueryable());
var request = new SearchAlbumViewModel { ForeignAlbumId = "abc" }; var request = new SearchAlbumViewModel { ForeignAlbumId = "abc" };
var result = await Rule.Execute(request); var result = await Rule.Execute(request, string.Empty);
Assert.True(result.Success); Assert.True(result.Success);
Assert.False(request.Approved); Assert.False(request.Approved);

View file

@ -29,7 +29,7 @@ namespace Ombi.Core.Tests.Rule.Search
public async Task Should_Not_Be_Monitored() public async Task Should_Not_Be_Monitored()
{ {
var request = new SearchArtistViewModel { ForignArtistId = "abc" }; var request = new SearchArtistViewModel { ForignArtistId = "abc" };
var result = await Rule.Execute(request); var result = await Rule.Execute(request, string.Empty);
Assert.True(result.Success); Assert.True(result.Success);
Assert.False(request.Monitored); Assert.False(request.Monitored);
@ -46,7 +46,7 @@ namespace Ombi.Core.Tests.Rule.Search
} }
}.AsQueryable()); }.AsQueryable());
var request = new SearchArtistViewModel { ForignArtistId = "abc" }; var request = new SearchArtistViewModel { ForignArtistId = "abc" };
var result = await Rule.Execute(request); var result = await Rule.Execute(request, string.Empty);
Assert.True(result.Success); Assert.True(result.Success);
Assert.True(request.Monitored); Assert.True(request.Monitored);
@ -64,7 +64,7 @@ namespace Ombi.Core.Tests.Rule.Search
} }
}.AsQueryable()); }.AsQueryable());
var request = new SearchArtistViewModel { ForignArtistId = "abc" }; var request = new SearchArtistViewModel { ForignArtistId = "abc" };
var result = await Rule.Execute(request); var result = await Rule.Execute(request, string.Empty);
Assert.True(result.Success); Assert.True(result.Success);
Assert.True(request.Monitored); Assert.True(request.Monitored);

View file

@ -9,6 +9,7 @@ using Ombi.Store.Entities.Requests;
using Ombi.Store.Entities; using Ombi.Store.Entities;
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
{ {
@ -29,6 +30,10 @@ namespace Ombi.Core.Engine.Interfaces
private OmbiUser _user; private OmbiUser _user;
protected async Task<OmbiUser> GetUser() protected async Task<OmbiUser> GetUser()
{ {
if(!Username.HasValue())
{
return null;
}
var username = Username.ToUpper(); var username = Username.ToUpper();
return _user ?? (_user = await UserManager.Users.FirstOrDefaultAsync(x => x.NormalizedUserName == username)); return _user ?? (_user = await UserManager.Users.FirstOrDefaultAsync(x => x.NormalizedUserName == username));
} }
@ -54,9 +59,9 @@ namespace Ombi.Core.Engine.Interfaces
var ruleResults = await Rules.StartSearchRules(model); var ruleResults = await Rules.StartSearchRules(model);
return ruleResults; return ruleResults;
} }
public async Task<RuleResult> RunSpecificRule(object model, SpecificRules rule) public async Task<RuleResult> RunSpecificRule(object model, SpecificRules rule, string requestOnBehalf)
{ {
var ruleResults = await Rules.StartSpecificRules(model, rule); var ruleResults = await Rules.StartSpecificRules(model, rule, requestOnBehalf);
return ruleResults; return ruleResults;
} }
} }

View file

@ -19,7 +19,7 @@ namespace Ombi.Core.Engine.Interfaces
Task<IEnumerable<SearchMovieViewModel>> NowPlayingMovies(int currentPosition, int amountToLoad); Task<IEnumerable<SearchMovieViewModel>> NowPlayingMovies(int currentPosition, int amountToLoad);
Task<MovieCollectionsViewModel> GetCollection(int collectionId, CancellationToken cancellationToken, string langCode = null); Task<MovieCollectionsViewModel> GetCollection(int collectionId, CancellationToken cancellationToken, string langCode = null);
Task<int> GetTvDbId(int theMovieDbId); Task<int> GetTvDbId(int theMovieDbId);
Task<IEnumerable<SearchMovieViewModel>> PopularMovies(int currentlyLoaded, int toLoad, CancellationToken cancellationToken); Task<IEnumerable<SearchMovieViewModel>> PopularMovies(int currentlyLoaded, int toLoad, CancellationToken cancellationToken, string langCustomCode = null);
Task<IEnumerable<SearchMovieViewModel>> TopRatedMovies(int currentlyLoaded, int toLoad); Task<IEnumerable<SearchMovieViewModel>> TopRatedMovies(int currentlyLoaded, int toLoad);
Task<IEnumerable<SearchMovieViewModel>> UpcomingMovies(int currentlyLoaded, int toLoad); Task<IEnumerable<SearchMovieViewModel>> UpcomingMovies(int currentlyLoaded, int toLoad);
Task<ActorCredits> GetMoviesByActor(int actorId, string langCode); Task<ActorCredits> GetMoviesByActor(int actorId, string langCode);

View file

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Ombi.Core.Models; using Ombi.Core.Models;
using Ombi.Core.Models.Requests; using Ombi.Core.Models.Requests;
@ -24,5 +25,6 @@ namespace Ombi.Core.Engine.Interfaces
Task UnSubscribeRequest(int requestId, RequestType type); Task UnSubscribeRequest(int requestId, RequestType type);
Task SubscribeToRequest(int requestId, RequestType type); Task SubscribeToRequest(int requestId, RequestType type);
Task<RequestQuotaCountModel> GetRemainingRequests(OmbiUser user = null); Task<RequestQuotaCountModel> GetRemainingRequests(OmbiUser user = null);
Task<RequestEngineResult> ReProcessRequest(int requestId, CancellationToken cancellationToken);
} }
} }

View file

@ -11,7 +11,7 @@ namespace Ombi.Core
Task<SearchFullInfoTvShowViewModel> GetShowInformation(string tvdbid, CancellationToken token); Task<SearchFullInfoTvShowViewModel> GetShowInformation(string tvdbid, CancellationToken token);
Task<SearchFullInfoTvShowViewModel> GetShowByRequest(int requestId, CancellationToken token); Task<SearchFullInfoTvShowViewModel> GetShowByRequest(int requestId, CancellationToken token);
Task<IEnumerable<StreamingData>> GetStreamInformation(int movieDbId, CancellationToken cancellationToken); Task<IEnumerable<StreamingData>> GetStreamInformation(int movieDbId, CancellationToken cancellationToken);
Task<IEnumerable<SearchTvShowViewModel>> Popular(int currentlyLoaded, int amountToLoad); Task<IEnumerable<SearchTvShowViewModel>> Popular(int currentlyLoaded, int amountToLoad, string langCustomCode = null);
Task<IEnumerable<SearchTvShowViewModel>> Anticipated(int currentlyLoaded, int amountToLoad); Task<IEnumerable<SearchTvShowViewModel>> Anticipated(int currentlyLoaded, int amountToLoad);
Task<IEnumerable<SearchTvShowViewModel>> Trending(int currentlyLoaded, int amountToLoad); Task<IEnumerable<SearchTvShowViewModel>> Trending(int currentlyLoaded, int amountToLoad);
} }

View file

@ -21,6 +21,7 @@ using Ombi.Settings.Settings.Models;
using Ombi.Store.Entities.Requests; using Ombi.Store.Entities.Requests;
using Ombi.Store.Repository; using Ombi.Store.Repository;
using Ombi.Core.Models; using Ombi.Core.Models;
using System.Threading;
namespace Ombi.Core.Engine namespace Ombi.Core.Engine
{ {
@ -70,7 +71,7 @@ namespace Ombi.Core.Engine
var canRequestOnBehalf = model.RequestOnBehalf.HasValue(); var canRequestOnBehalf = model.RequestOnBehalf.HasValue();
var isAdmin = await UserManager.IsInRoleAsync(userDetails, OmbiRoles.PowerUser) || await UserManager.IsInRoleAsync(userDetails, OmbiRoles.Admin); var isAdmin = await UserManager.IsInRoleAsync(userDetails, OmbiRoles.PowerUser) || await UserManager.IsInRoleAsync(userDetails, OmbiRoles.Admin);
if (model.RequestOnBehalf.HasValue() && !isAdmin) if (canRequestOnBehalf && !isAdmin)
{ {
return new RequestEngineResult return new RequestEngineResult
{ {
@ -549,12 +550,17 @@ namespace Ombi.Core.Engine
request.Denied = false; request.Denied = false;
await MovieRepository.Update(request); await MovieRepository.Update(request);
var canNotify = await RunSpecificRule(request, SpecificRules.CanSendNotification); var canNotify = await RunSpecificRule(request, SpecificRules.CanSendNotification, string.Empty);
if (canNotify.Success) if (canNotify.Success)
{ {
await NotificationHelper.Notify(request, NotificationType.RequestApproved); await NotificationHelper.Notify(request, NotificationType.RequestApproved);
} }
return await ProcessSendingMovie(request);
}
private async Task<RequestEngineResult> ProcessSendingMovie(MovieRequests request)
{
if (request.Approved) if (request.Approved)
{ {
var result = await Sender.Send(request); var result = await Sender.Send(request);
@ -634,6 +640,21 @@ namespace Ombi.Core.Engine
return await MovieRepository.GetAll().AnyAsync(x => x.RequestedUserId == userId); return await MovieRepository.GetAll().AnyAsync(x => x.RequestedUserId == userId);
} }
public async Task<RequestEngineResult> ReProcessRequest(int requestId, CancellationToken cancellationToken)
{
var request = await MovieRepository.Find(requestId);
if (request == null)
{
return new RequestEngineResult
{
Result = false,
ErrorMessage = "Request does not exist"
};
}
return await ProcessSendingMovie(request);
}
public async Task<RequestEngineResult> MarkUnavailable(int modelId) public async Task<RequestEngineResult> MarkUnavailable(int modelId)
{ {
var request = await MovieRepository.Find(modelId); var request = await MovieRepository.Find(modelId);
@ -682,7 +703,7 @@ namespace Ombi.Core.Engine
{ {
await MovieRepository.Add(model); await MovieRepository.Add(model);
var result = await RunSpecificRule(model, SpecificRules.CanSendNotification); var result = await RunSpecificRule(model, SpecificRules.CanSendNotification, requestOnBehalf);
if (result.Success) if (result.Success)
{ {
await NotificationHelper.NewRequest(model); await NotificationHelper.NewRequest(model);

View file

@ -362,7 +362,7 @@ namespace Ombi.Core.Engine
await MusicRepository.Update(request); await MusicRepository.Update(request);
var canNotify = await RunSpecificRule(request, SpecificRules.CanSendNotification); var canNotify = await RunSpecificRule(request, SpecificRules.CanSendNotification, string.Empty);
if (canNotify.Success) if (canNotify.Success)
{ {
await NotificationHelper.Notify(request, NotificationType.RequestApproved); await NotificationHelper.Notify(request, NotificationType.RequestApproved);
@ -506,7 +506,7 @@ namespace Ombi.Core.Engine
{ {
await MusicRepository.Add(model); await MusicRepository.Add(model);
var result = await RunSpecificRule(model, SpecificRules.CanSendNotification); var result = await RunSpecificRule(model, SpecificRules.CanSendNotification, string.Empty);
if (result.Success) if (result.Success)
{ {
await NotificationHelper.NewRequest(model); await NotificationHelper.NewRequest(model);

View file

@ -151,7 +151,7 @@ namespace Ombi.Core.Engine
} }
await Rules.StartSpecificRules(vm, SpecificRules.LidarrArtist); await Rules.StartSpecificRules(vm, SpecificRules.LidarrArtist, string.Empty);
return vm; return vm;
} }
@ -190,7 +190,7 @@ namespace Ombi.Core.Engine
vm.Cover = a.images?.FirstOrDefault(x => x.coverType.Equals("cover"))?.url?.ToHttpsUrl(); vm.Cover = a.images?.FirstOrDefault(x => x.coverType.Equals("cover"))?.url?.ToHttpsUrl();
await Rules.StartSpecificRules(vm, SpecificRules.LidarrAlbum); await Rules.StartSpecificRules(vm, SpecificRules.LidarrAlbum, string.Empty);
await RunSearchRules(vm); await RunSearchRules(vm);
@ -230,7 +230,7 @@ namespace Ombi.Core.Engine
vm.Cover = a.remoteCover; vm.Cover = a.remoteCover;
} }
await Rules.StartSpecificRules(vm, SpecificRules.LidarrAlbum); await Rules.StartSpecificRules(vm, SpecificRules.LidarrAlbum, string.Empty);
await RunSearchRules(vm); await RunSearchRules(vm);
@ -258,7 +258,7 @@ namespace Ombi.Core.Engine
vm.Cover = fullAlbum.remoteCover; vm.Cover = fullAlbum.remoteCover;
} }
await Rules.StartSpecificRules(vm, SpecificRules.LidarrAlbum); await Rules.StartSpecificRules(vm, SpecificRules.LidarrAlbum, string.Empty);
await RunSearchRules(vm); await RunSearchRules(vm);

View file

@ -25,6 +25,7 @@ using Ombi.Settings.Settings.Models;
using Ombi.Store.Entities.Requests; using Ombi.Store.Entities.Requests;
using Ombi.Store.Repository; using Ombi.Store.Repository;
using Ombi.Core.Models; using Ombi.Core.Models;
using System.Threading;
namespace Ombi.Core.Engine namespace Ombi.Core.Engine
{ {
@ -164,7 +165,7 @@ namespace Ombi.Core.Engine
}; };
} }
if ((tv.RootFolderOverride.HasValue || tv.QualityPathOverride.HasValue) && !isAdmin) if ((tv.RootFolderOverride.HasValue || tv.QualityPathOverride.HasValue || tv.LanguageProfile.HasValue) && !isAdmin)
{ {
return new RequestEngineResult return new RequestEngineResult
{ {
@ -250,7 +251,7 @@ namespace Ombi.Core.Engine
} }
// This is a new request // This is a new request
var newRequest = tvBuilder.CreateNewRequest(tv, tv.RootFolderOverride.GetValueOrDefault(), tv.QualityPathOverride.GetValueOrDefault()); var newRequest = tvBuilder.CreateNewRequest(tv, tv.RootFolderOverride.GetValueOrDefault(), tv.QualityPathOverride.GetValueOrDefault(), tv.LanguageProfile.GetValueOrDefault());
return await AddRequest(newRequest.NewRequest, tv.RequestOnBehalf); return await AddRequest(newRequest.NewRequest, tv.RequestOnBehalf);
} }
@ -896,9 +897,25 @@ namespace Ombi.Core.Engine
} }
public async Task<RequestEngineResult> ReProcessRequest(int requestId, CancellationToken cancellationToken)
{
var request = await TvRepository.GetChild().FirstOrDefaultAsync(x => x.Id == requestId, cancellationToken);
if (request == null)
{
return new RequestEngineResult
{
Result = false,
ErrorMessage = "Request does not exist"
};
}
return await ProcessSendingShow(request);
}
private async Task<RequestEngineResult> AfterRequest(ChildRequests model, string requestOnBehalf) private async Task<RequestEngineResult> AfterRequest(ChildRequests model, string requestOnBehalf)
{ {
var sendRuleResult = await RunSpecificRule(model, SpecificRules.CanSendNotification); var sendRuleResult = await RunSpecificRule(model, SpecificRules.CanSendNotification, requestOnBehalf);
if (sendRuleResult.Success) if (sendRuleResult.Success)
{ {
await NotificationHelper.NewRequest(model); await NotificationHelper.NewRequest(model);
@ -913,6 +930,11 @@ namespace Ombi.Core.Engine
EpisodeCount = model.SeasonRequests.Select(m => m.Episodes.Count).Sum(), EpisodeCount = model.SeasonRequests.Select(m => m.Episodes.Count).Sum(),
}); });
return await ProcessSendingShow(model);
}
private async Task<RequestEngineResult> ProcessSendingShow(ChildRequests model)
{
if (model.Approved) if (model.Approved)
{ {
// Autosend // Autosend
@ -997,6 +1019,10 @@ namespace Ombi.Core.Engine
request.QualityOverride = options.QualityOverride; request.QualityOverride = options.QualityOverride;
request.RootFolder = options.RootPathOverride; request.RootFolder = options.RootPathOverride;
if (options.LanguageProfile > 0)
{
request.LanguageProfile = options.LanguageProfile;
}
await TvRepository.Update(request); await TvRepository.Update(request);

View file

@ -124,9 +124,9 @@ namespace Ombi.Core.Engine.V2
/// Gets popular movies by paging /// Gets popular movies by paging
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
public async Task<IEnumerable<SearchMovieViewModel>> PopularMovies(int currentlyLoaded, int toLoad, CancellationToken cancellationToken) public async Task<IEnumerable<SearchMovieViewModel>> PopularMovies(int currentlyLoaded, int toLoad, CancellationToken cancellationToken, string langCustomCode = null)
{ {
var langCode = await DefaultLanguageCode(null); var langCode = await DefaultLanguageCode(langCustomCode);
var pages = PaginationHelper.GetNextPages(currentlyLoaded, toLoad, _theMovieDbMaxPageItems); var pages = PaginationHelper.GetNextPages(currentlyLoaded, toLoad, _theMovieDbMaxPageItems);
@ -371,6 +371,7 @@ namespace Ombi.Core.Engine.V2
mapped.Requested = movie.Requested; mapped.Requested = movie.Requested;
mapped.PlexUrl = movie.PlexUrl; mapped.PlexUrl = movie.PlexUrl;
mapped.EmbyUrl = movie.EmbyUrl; mapped.EmbyUrl = movie.EmbyUrl;
mapped.JellyfinUrl = movie.JellyfinUrl;
mapped.Subscribed = movie.Subscribed; mapped.Subscribed = movie.Subscribed;
mapped.ShowSubscribe = movie.ShowSubscribe; mapped.ShowSubscribe = movie.ShowSubscribe;
mapped.ReleaseDate = movie.ReleaseDate; mapped.ReleaseDate = movie.ReleaseDate;

View file

@ -22,6 +22,7 @@ using Microsoft.EntityFrameworkCore;
using System.Threading; using System.Threading;
using Ombi.Api.TheMovieDb; using Ombi.Api.TheMovieDb;
using Ombi.Api.TheMovieDb.Models; using Ombi.Api.TheMovieDb.Models;
using System.Diagnostics;
namespace Ombi.Core.Engine.V2 namespace Ombi.Core.Engine.V2
{ {
@ -49,13 +50,14 @@ namespace Ombi.Core.Engine.V2
public async Task<SearchFullInfoTvShowViewModel> GetShowByRequest(int requestId, CancellationToken token) public async Task<SearchFullInfoTvShowViewModel> GetShowByRequest(int requestId, CancellationToken token)
{ {
var request = await RequestService.TvRequestService.Get().FirstOrDefaultAsync(x => x.Id == requestId); var request = await RequestService.TvRequestService.Get().FirstOrDefaultAsync(x => x.Id == requestId);
return await GetShowInformation(request.ExternalProviderId.ToString(), token); // TODO return await GetShowInformation(request.ExternalProviderId.ToString(), token);
} }
public async Task<SearchFullInfoTvShowViewModel> GetShowInformation(string tvdbid, CancellationToken token) public async Task<SearchFullInfoTvShowViewModel> GetShowInformation(string tvdbid, CancellationToken token)
{ {
var show = await Cache.GetOrAdd(nameof(GetShowInformation) + tvdbid, var langCode = await DefaultLanguageCode(null);
async () => await _movieApi.GetTVInfo(tvdbid), DateTime.Now.AddHours(12)); var show = await Cache.GetOrAdd(nameof(GetShowInformation) + langCode + tvdbid,
async () => await _movieApi.GetTVInfo(tvdbid, langCode), DateTime.Now.AddHours(12));
if (show == null || show.name == null) if (show == null || show.name == null)
{ {
// We don't have enough information // We don't have enough information
@ -69,47 +71,15 @@ namespace Ombi.Core.Engine.V2
{ {
var seasonEpisodes = (await _movieApi.GetSeasonEpisodes(show.id, tvSeason.season_number, token)); var seasonEpisodes = (await _movieApi.GetSeasonEpisodes(show.id, tvSeason.season_number, token));
foreach (var episode in seasonEpisodes.episodes) MapSeasons(mapped.SeasonRequests, tvSeason, seasonEpisodes);
{
var season = mapped.SeasonRequests.FirstOrDefault(x => x.SeasonNumber == episode.season_number);
if (season == null)
{
var newSeason = new SeasonRequests
{
SeasonNumber = episode.season_number,
Overview = tvSeason.overview,
Episodes = new List<EpisodeRequests>()
};
newSeason.Episodes.Add(new EpisodeRequests
{
//Url = episode...ToHttpsUrl(),
Title = episode.name,
AirDate = episode.air_date.HasValue() ? DateTime.Parse(episode.air_date) : DateTime.MinValue,
EpisodeNumber = episode.episode_number,
});
mapped.SeasonRequests.Add(newSeason);
}
else
{
// We already have the season, so just add the episode
season.Episodes.Add(new EpisodeRequests
{
//Url = e.url.ToHttpsUrl(),
Title = episode.name,
AirDate = episode.air_date.HasValue() ? DateTime.Parse(episode.air_date) : DateTime.MinValue,
EpisodeNumber = episode.episode_number,
});
}
}
} }
return await ProcessResult(mapped); return await ProcessResult(mapped);
} }
public async Task<IEnumerable<SearchTvShowViewModel>> Popular(int currentlyLoaded, int amountToLoad) public async Task<IEnumerable<SearchTvShowViewModel>> Popular(int currentlyLoaded, int amountToLoad, string langCustomCode = null)
{ {
var langCode = await DefaultLanguageCode(null); var langCode = await DefaultLanguageCode(langCustomCode);
var pages = PaginationHelper.GetNextPages(currentlyLoaded, amountToLoad, ResultLimit); var pages = PaginationHelper.GetNextPages(currentlyLoaded, amountToLoad, ResultLimit);
var results = new List<MovieDbSearchResult>(); var results = new List<MovieDbSearchResult>();
@ -152,6 +122,7 @@ namespace Ombi.Core.Engine.V2
async () => await _movieApi.TopRatedTv(langCode, pagesToLoad.Page), DateTime.Now.AddHours(12)); async () => await _movieApi.TopRatedTv(langCode, pagesToLoad.Page), DateTime.Now.AddHours(12));
results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take)); results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take));
} }
var processed = ProcessResults(results); var processed = ProcessResults(results);
return await processed; return await processed;
} }
@ -177,22 +148,77 @@ namespace Ombi.Core.Engine.V2
return data; return data;
} }
private async Task<IEnumerable<SearchTvShowViewModel>> ProcessResults<T>(IEnumerable<T> items) private async Task<IEnumerable<SearchTvShowViewModel>> ProcessResults(List<MovieDbSearchResult> items)
{ {
var retVal = new List<SearchTvShowViewModel>(); var retVal = new List<SearchTvShowViewModel>();
var settings = await _customization.GetSettingsAsync(); var settings = await _customization.GetSettingsAsync();
foreach (var tvMazeSearch in items) foreach (var tvMazeSearch in items)
{ {
if (settings.HideAvailableFromDiscover)
{
// To hide, we need to know if it's fully available, the only way to do this is to lookup it's episodes to check if we have every episode
var show = await Cache.GetOrAdd(nameof(GetShowInformation) + tvMazeSearch.Id.ToString(),
async () => await _movieApi.GetTVInfo(tvMazeSearch.Id.ToString()), DateTime.Now.AddHours(12));
foreach (var tvSeason in show.seasons.Where(x => x.season_number != 0)) // skip the first season
{
var seasonEpisodes = await Cache.GetOrAdd("SeasonEpisodes" + show.id + tvSeason.season_number, async () =>
{
return await _movieApi.GetSeasonEpisodes(show.id, tvSeason.season_number, CancellationToken.None);
}, DateTime.Now.AddHours(12));
MapSeasons(tvMazeSearch.SeasonRequests, tvSeason, seasonEpisodes);
}
}
var result = await ProcessResult(tvMazeSearch); var result = await ProcessResult(tvMazeSearch);
if (result == null || settings.HideAvailableFromDiscover && result.Available) if (result == null || settings.HideAvailableFromDiscover && result.FullyAvailable)
{ {
continue; continue;
} }
retVal.Add(result); retVal.Add(result);
} }
return retVal; return retVal;
} }
private static void MapSeasons(List<SeasonRequests> seasonRequests, Season tvSeason, SeasonDetails seasonEpisodes)
{
foreach (var episode in seasonEpisodes.episodes)
{
var season = seasonRequests.FirstOrDefault(x => x.SeasonNumber == episode.season_number);
if (season == null)
{
var newSeason = new SeasonRequests
{
SeasonNumber = episode.season_number,
Overview = tvSeason.overview,
Episodes = new List<EpisodeRequests>()
};
newSeason.Episodes.Add(new EpisodeRequests
{
//Url = episode...ToHttpsUrl(),
Title = episode.name,
AirDate = episode.air_date.HasValue() ? DateTime.Parse(episode.air_date) : DateTime.MinValue,
EpisodeNumber = episode.episode_number,
});
seasonRequests.Add(newSeason);
}
else
{
// We already have the season, so just add the episode
season.Episodes.Add(new EpisodeRequests
{
//Url = e.url.ToHttpsUrl(),
Title = episode.name,
AirDate = episode.air_date.HasValue() ? DateTime.Parse(episode.air_date) : DateTime.MinValue,
EpisodeNumber = episode.episode_number,
});
}
}
}
private async Task<SearchTvShowViewModel> ProcessResult<T>(T tvMazeSearch) private async Task<SearchTvShowViewModel> ProcessResult<T>(T tvMazeSearch)
{ {
var item = _mapper.Map<SearchTvShowViewModel>(tvMazeSearch); var item = _mapper.Map<SearchTvShowViewModel>(tvMazeSearch);
@ -216,6 +242,9 @@ namespace Ombi.Core.Engine.V2
item.Approved = oldModel.Approved; item.Approved = oldModel.Approved;
item.SeasonRequests = oldModel.SeasonRequests; item.SeasonRequests = oldModel.SeasonRequests;
item.RequestId = oldModel.RequestId; item.RequestId = oldModel.RequestId;
item.PlexUrl = oldModel.PlexUrl;
item.EmbyUrl = oldModel.EmbyUrl;
item.JellyfinUrl = oldModel.JellyfinUrl;
if (!string.IsNullOrEmpty(item.Images?.Medium)) if (!string.IsNullOrEmpty(item.Images?.Medium))
{ {

View file

@ -217,7 +217,7 @@ namespace Ombi.Core.Helpers
} }
public TvShowRequestBuilderV2 CreateNewRequest(TvRequestViewModelV2 tv, int rootPathOverride, int qualityOverride) public TvShowRequestBuilderV2 CreateNewRequest(TvRequestViewModelV2 tv, int rootPathOverride, int qualityOverride, int langProfile)
{ {
int.TryParse(TheMovieDbRecord.ExternalIds?.TvDbId, out var tvdbId); int.TryParse(TheMovieDbRecord.ExternalIds?.TvDbId, out var tvdbId);
NewRequest = new TvRequests NewRequest = new TvRequests
@ -234,7 +234,8 @@ namespace Ombi.Core.Helpers
TotalSeasons = tv.Seasons.Count(), TotalSeasons = tv.Seasons.Count(),
Background = BackdropPath, Background = BackdropPath,
RootFolder = rootPathOverride, RootFolder = rootPathOverride,
QualityOverride = qualityOverride QualityOverride = qualityOverride,
LanguageProfile = langProfile
}; };
NewRequest.ChildRequests.Add(ChildRequest); NewRequest.ChildRequests.Add(ChildRequest);

View file

@ -5,5 +5,6 @@
public int RequestId { get; set; } public int RequestId { get; set; }
public int RootPathOverride { get; set; } public int RootPathOverride { get; set; }
public int QualityOverride { get; set; } public int QualityOverride { get; set; }
public int LanguageProfile { get; set; }
} }
} }

View file

@ -24,6 +24,7 @@ namespace Ombi.Core.Models.Requests
{ {
public bool RequestAll { get; set; } public bool RequestAll { get; set; }
public bool LatestSeason { get; set; } public bool LatestSeason { get; set; }
public int? LanguageProfile { get; set; }
public bool FirstSeason { get; set; } public bool FirstSeason { get; set; }
public List<SeasonsViewModel> Seasons { get; set; } = new List<SeasonsViewModel>(); public List<SeasonsViewModel> Seasons { get; set; } = new List<SeasonsViewModel>();
[JsonIgnore] [JsonIgnore]

View file

@ -58,9 +58,6 @@ namespace Ombi.Core.Models.Search
public bool PartlyAvailable { get; set; } public bool PartlyAvailable { get; set; }
public override RequestType Type => RequestType.TvShow; public override RequestType Type => RequestType.TvShow;
/// <summary>
/// Only set on the images call
/// </summary>
public string BackdropPath { get; set; } public string BackdropPath { get; set; }
} }
} }

View file

@ -9,6 +9,6 @@ namespace Ombi.Core.Rule.Interfaces
{ {
Task<IEnumerable<RuleResult>> StartRequestRules(BaseRequest obj); Task<IEnumerable<RuleResult>> StartRequestRules(BaseRequest obj);
Task<IEnumerable<RuleResult>> StartSearchRules(SearchViewModel obj); Task<IEnumerable<RuleResult>> StartSearchRules(SearchViewModel obj);
Task<RuleResult> StartSpecificRules(object obj, SpecificRules selectedRule); Task<RuleResult> StartSpecificRules(object obj, SpecificRules selectedRule, string requestOnBehalf);
} }
} }

View file

@ -5,7 +5,7 @@ namespace Ombi.Core.Rule.Interfaces
{ {
public interface ISpecificRule<T> where T : new() public interface ISpecificRule<T> where T : new()
{ {
Task<RuleResult> Execute(T obj); Task<RuleResult> Execute(T obj, string requestOnBehalf);
SpecificRules Rule { get; } SpecificRules Rule { get; }
} }
} }

View file

@ -58,13 +58,13 @@ namespace Ombi.Core.Rule
return results; return results;
} }
public async Task<RuleResult> StartSpecificRules(object obj, SpecificRules selectedRule) public async Task<RuleResult> StartSpecificRules(object obj, SpecificRules selectedRule, string requestOnBehalf)
{ {
foreach (var rule in SpecificRules) foreach (var rule in SpecificRules)
{ {
if (selectedRule == rule.Rule) if (selectedRule == rule.Rule)
{ {
var result = await rule.Execute(obj); var result = await rule.Execute(obj, requestOnBehalf);
return result; return result;
} }
} }

View file

@ -13,7 +13,7 @@ namespace Ombi.Core.Rule.Rules.Search
{ {
public static void CheckForUnairedEpisodes(SearchTvShowViewModel search) public static void CheckForUnairedEpisodes(SearchTvShowViewModel search)
{ {
foreach (var season in search.SeasonRequests) foreach (var season in search.SeasonRequests.ToList())
{ {
// If we have all the episodes for this season, then this season is available // If we have all the episodes for this season, then this season is available
if (season.Episodes.All(x => x.Available)) if (season.Episodes.All(x => x.Available))

View file

@ -67,7 +67,7 @@ namespace Ombi.Core.Rule.Rules.Search
var s = await EmbySettings.GetSettingsAsync(); var s = await EmbySettings.GetSettingsAsync();
if (s.Enable) if (s.Enable)
{ {
var server = s.Servers.FirstOrDefault(x => x.ServerHostname != null); var server = s.Servers.FirstOrDefault();
if ((server?.ServerHostname ?? string.Empty).HasValue()) if ((server?.ServerHostname ?? string.Empty).HasValue())
{ {
obj.EmbyUrl = EmbyHelper.GetEmbyMediaUrl(item.EmbyId, server?.ServerId, server?.ServerHostname); obj.EmbyUrl = EmbyHelper.GetEmbyMediaUrl(item.EmbyId, server?.ServerId, server?.ServerHostname);

View file

@ -9,7 +9,7 @@ using Ombi.Store.Repository;
namespace Ombi.Core.Rule.Rules.Search namespace Ombi.Core.Rule.Rules.Search
{ {
public class LidarrAlbumCacheRule : BaseSearchRule, IRules<SearchViewModel> public class LidarrAlbumCacheRule : SpecificRule, ISpecificRule<object>
{ {
public LidarrAlbumCacheRule(IExternalRepository<LidarrAlbumCache> db) public LidarrAlbumCacheRule(IExternalRepository<LidarrAlbumCache> db)
{ {
@ -18,7 +18,9 @@ namespace Ombi.Core.Rule.Rules.Search
private readonly IExternalRepository<LidarrAlbumCache> _db; private readonly IExternalRepository<LidarrAlbumCache> _db;
public Task<RuleResult> Execute(SearchViewModel objec) public override SpecificRules Rule => SpecificRules.LidarrAlbum;
public Task<RuleResult> Execute(object objec, string requestOnBehalf)
{ {
if (objec is SearchAlbumViewModel obj) if (objec is SearchAlbumViewModel obj)
{ {

View file

@ -17,7 +17,7 @@ namespace Ombi.Core.Rule.Rules.Search
private readonly IExternalRepository<LidarrArtistCache> _db; private readonly IExternalRepository<LidarrArtistCache> _db;
public Task<RuleResult> Execute(object objec) public Task<RuleResult> Execute(object objec, string requestOnBehalf)
{ {
var obj = (SearchArtistViewModel) objec; var obj = (SearchArtistViewModel) objec;
// Check if it's in Lidarr // Check if it's in Lidarr
@ -30,6 +30,7 @@ namespace Ombi.Core.Rule.Rules.Search
return Task.FromResult(Success()); return Task.FromResult(Success());
} }
public override SpecificRules Rule => SpecificRules.LidarrArtist; public override SpecificRules Rule => SpecificRules.LidarrArtist;
} }
} }

View file

@ -25,10 +25,11 @@ namespace Ombi.Core.Rule.Rules.Search
PlexServerContent item = null; PlexServerContent item = null;
var useImdb = false; var useImdb = false;
var useTheMovieDb = false; var useTheMovieDb = false;
var useId = false;
var useTvDb = false; var useTvDb = false;
if (obj.ImdbId.HasValue()) if (obj.ImdbId.HasValue())
{ {
item = await PlexContentRepository.Get(obj.ImdbId); item = await PlexContentRepository.Get(obj.ImdbId, ProviderType.ImdbId);
if (item != null) if (item != null)
{ {
useImdb = true; useImdb = true;
@ -36,9 +37,17 @@ namespace Ombi.Core.Rule.Rules.Search
} }
if (item == null) if (item == null)
{ {
if (obj.Id > 0)
{
item = await PlexContentRepository.Get(obj.Id.ToString(), ProviderType.TheMovieDbId);
if (item != null)
{
useId = true;
}
}
if (obj.TheMovieDbId.HasValue()) if (obj.TheMovieDbId.HasValue())
{ {
item = await PlexContentRepository.Get(obj.TheMovieDbId); item = await PlexContentRepository.Get(obj.TheMovieDbId, ProviderType.TheMovieDbId);
if (item != null) if (item != null)
{ {
useTheMovieDb = true; useTheMovieDb = true;
@ -49,7 +58,7 @@ namespace Ombi.Core.Rule.Rules.Search
{ {
if (obj.TheTvDbId.HasValue()) if (obj.TheTvDbId.HasValue())
{ {
item = await PlexContentRepository.Get(obj.TheTvDbId); item = await PlexContentRepository.Get(obj.TheTvDbId, ProviderType.TvDbId);
if (item != null) if (item != null)
{ {
useTvDb = true; useTvDb = true;
@ -60,6 +69,11 @@ namespace Ombi.Core.Rule.Rules.Search
if (item != null) if (item != null)
{ {
if (useId)
{
obj.TheMovieDbId = obj.Id.ToString();
useTheMovieDb = true;
}
obj.Available = true; obj.Available = true;
obj.PlexUrl = item.Url; obj.PlexUrl = item.Url;
obj.Quality = item.Quality; obj.Quality = item.Quality;
@ -71,9 +85,9 @@ namespace Ombi.Core.Rule.Rules.Search
if (search.SeasonRequests.Any()) if (search.SeasonRequests.Any())
{ {
var allEpisodes = PlexContentRepository.GetAllEpisodes(); var allEpisodes = PlexContentRepository.GetAllEpisodes();
foreach (var season in search.SeasonRequests) foreach (var season in search.SeasonRequests.ToList())
{ {
foreach (var episode in season.Episodes) foreach (var episode in season.Episodes.ToList())
{ {
await AvailabilityRuleHelper.SingleEpisodeCheck(useImdb, allEpisodes, episode, season, item, useTheMovieDb, useTvDb, Log); await AvailabilityRuleHelper.SingleEpisodeCheck(useImdb, allEpisodes, episode, season, item, useTheMovieDb, useTvDb, Log);
} }

View file

@ -22,11 +22,20 @@ namespace Ombi.Core.Rule.Rules.Specific
private OmbiUserManager UserManager { get; } private OmbiUserManager UserManager { get; }
private ISettingsService<OmbiSettings> Settings { get; } private ISettingsService<OmbiSettings> Settings { get; }
public async Task<RuleResult> Execute(object obj) public async Task<RuleResult> Execute(object obj, string requestOnBehalf)
{ {
var req = (BaseRequest)obj; var req = (BaseRequest)obj;
var canRequestonBehalf = requestOnBehalf.HasValue();
var settings = await Settings.GetSettingsAsync(); var settings = await Settings.GetSettingsAsync();
var sendNotification = true; var sendNotification = true;
if (settings.DoNotSendNotificationsForAutoApprove && canRequestonBehalf)
{
return new RuleResult
{
Success = false
};
}
var requestedUser = await UserManager.Users.FirstOrDefaultAsync(x => x.Id == req.RequestedUserId); var requestedUser = await UserManager.Users.FirstOrDefaultAsync(x => x.Id == req.RequestedUserId);
if (req.RequestType == RequestType.Movie) if (req.RequestType == RequestType.Movie)
{ {

View file

@ -158,6 +158,8 @@ namespace Ombi.Core.Senders
} }
int qualityToUse; int qualityToUse;
var sonarrV3 = s.V3;
var languageProfileId = s.LanguageProfile;
string rootFolderPath; string rootFolderPath;
string seriesType; string seriesType;
@ -167,8 +169,17 @@ namespace Ombi.Core.Senders
{ {
// Get the root path from the rootfolder selected. // Get the root path from the rootfolder selected.
// For some reason, if we haven't got one use the first root folder in Sonarr // For some reason, if we haven't got one use the first root folder in Sonarr
rootFolderPath = await GetSonarrRootPath(int.Parse(s.RootPathAnime), s); if (!int.TryParse(s.RootPathAnime, out int animePath))
int.TryParse(s.QualityProfileAnime, out qualityToUse); {
animePath = int.Parse(s.RootPath); // Set it to the main root folder if we have no anime folder.
}
rootFolderPath = await GetSonarrRootPath(animePath, s);
languageProfileId = s.LanguageProfileAnime > 0 ? s.LanguageProfileAnime : s.LanguageProfile;
if (!int.TryParse(s.QualityProfileAnime, out qualityToUse))
{
qualityToUse = int.Parse(s.QualityProfile);
}
if (profiles != null) if (profiles != null)
{ {
if (profiles.SonarrRootPathAnime > 0) if (profiles.SonarrRootPathAnime > 0)
@ -181,7 +192,6 @@ namespace Ombi.Core.Senders
} }
} }
seriesType = "anime"; seriesType = "anime";
} }
else else
{ {
@ -221,9 +231,14 @@ namespace Ombi.Core.Senders
} }
} }
// Are we using v3 sonarr? if (model.ParentRequest.LanguageProfile.HasValue)
var sonarrV3 = s.V3; {
var languageProfileId = s.LanguageProfile; var languageProfile = model.ParentRequest.LanguageProfile.Value;
if (languageProfile > 0)
{
languageProfileId = languageProfile;
}
}
try try
{ {
@ -264,6 +279,10 @@ namespace Ombi.Core.Senders
var seasonsToAdd = GetSeasonsToCreate(model); var seasonsToAdd = GetSeasonsToCreate(model);
newSeries.seasons = seasonsToAdd; newSeries.seasons = seasonsToAdd;
var result = await SonarrApi.AddSeries(newSeries, s.ApiKey, s.FullUri); var result = await SonarrApi.AddSeries(newSeries, s.ApiKey, s.FullUri);
if (result?.ErrorMessages?.Any() ?? false)
{
throw new Exception(string.Join(',', result.ErrorMessages));
}
existingSeries = await SonarrApi.GetSeriesById(result.id, s.ApiKey, s.FullUri); existingSeries = await SonarrApi.GetSeriesById(result.id, s.ApiKey, s.FullUri);
await SendToSonarr(model, existingSeries, s); await SendToSonarr(model, existingSeries, s);
} }
@ -407,7 +426,6 @@ namespace Ombi.Core.Senders
await SonarrApi.SeasonPass(s.ApiKey, s.FullUri, result); await SonarrApi.SeasonPass(s.ApiKey, s.FullUri, result);
} }
if (!s.AddOnly) if (!s.AddOnly)
{ {
await SearchForRequest(model, sonarrEpList, result, s, episodesToUpdate); await SearchForRequest(model, sonarrEpList, result, s, episodesToUpdate);

View file

@ -23,7 +23,6 @@ using Ombi.Notifications;
using Ombi.Schedule; using Ombi.Schedule;
using Ombi.Schedule.Jobs; using Ombi.Schedule.Jobs;
using Ombi.Settings.Settings; using Ombi.Settings.Settings;
using Ombi.Store.Context;
using Ombi.Store.Repository; using Ombi.Store.Repository;
using Ombi.Notifications.Agents; using Ombi.Notifications.Agents;
using Ombi.Schedule.Jobs.Radarr; using Ombi.Schedule.Jobs.Radarr;
@ -68,6 +67,8 @@ using Ombi.Api.MusicBrainz;
using Ombi.Api.Twilio; using Ombi.Api.Twilio;
using Ombi.Api.CloudService; using Ombi.Api.CloudService;
using Ombi.Api.RottenTomatoes; using Ombi.Api.RottenTomatoes;
using System.Net.Http;
using Microsoft.Extensions.Logging;
namespace Ombi.DependencyInjection namespace Ombi.DependencyInjection
{ {
@ -119,14 +120,24 @@ namespace Ombi.DependencyInjection
public static void RegisterHttp(this IServiceCollection services) public static void RegisterHttp(this IServiceCollection services)
{ {
var runtimeVersion = AssemblyHelper.GetRuntimeVersion();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<IPrincipal>(sp => sp.GetService<IHttpContextAccessor>().HttpContext.User); services.AddScoped<IPrincipal>(sp => sp.GetService<IHttpContextAccessor>().HttpContext.User);
services.AddHttpClient("OmbiClient", client =>
{
client.DefaultRequestHeaders.Add("User-Agent", $"Ombi/{runtimeVersion} (https://ombi.io/)");
}).ConfigurePrimaryHttpMessageHandler(() =>
{
var httpClientHandler = new HttpClientHandler();
httpClientHandler.ServerCertificateCustomValidationCallback = (message, certificate2, arg3, arg4) => true;
return httpClientHandler;
});
} }
public static void RegisterApi(this IServiceCollection services) public static void RegisterApi(this IServiceCollection services)
{ {
services.AddScoped<IApi, Api.Api>(); services.AddScoped<IApi, Api.Api>(s => new Api.Api(s.GetRequiredService<ILogger<Api.Api>>(), s.GetRequiredService<IHttpClientFactory>().CreateClient("OmbiClient")));
services.AddScoped<IOmbiHttpClient, OmbiHttpClient>(); // https://blogs.msdn.microsoft.com/alazarev/2017/12/29/disposable-finalizers-and-httpclient/
services.AddTransient<IMovieDbApi, Api.TheMovieDb.TheMovieDbApi>(); services.AddTransient<IMovieDbApi, Api.TheMovieDb.TheMovieDbApi>();
services.AddTransient<IPlexApi, PlexApi>(); services.AddTransient<IPlexApi, PlexApi>();
services.AddTransient<IEmbyApi, EmbyApi>(); services.AddTransient<IEmbyApi, EmbyApi>();

View file

@ -13,6 +13,7 @@
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="5.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="5.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" /> <PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

@ -61,6 +61,7 @@ namespace Ombi.Helpers.Tests
get get
{ {
yield return new TestCaseData("plex://movie/5e1632df2d4d84003e48e54e|imdb://tt9178402|tmdb://610201", new ProviderId { ImdbId = "tt9178402", TheMovieDb = "610201" }).SetName("V2 Regular Plex Id"); yield return new TestCaseData("plex://movie/5e1632df2d4d84003e48e54e|imdb://tt9178402|tmdb://610201", new ProviderId { ImdbId = "tt9178402", TheMovieDb = "610201" }).SetName("V2 Regular Plex Id");
yield return new TestCaseData("plex://movie/5e1632df2d4d84003e48e54e|imdb://tt9178402|tmdb://610201|thetvdb://12345", new ProviderId { ImdbId = "tt9178402", TheMovieDb = "610201", TheTvDb = "12345" }).SetName("V2 Regular Plex Id w/ tvdb");
yield return new TestCaseData("plex://movie/5d7768253c3c2a001fbcab72|imdb://tt0119567|tmdb://330", new ProviderId { ImdbId = "tt0119567", TheMovieDb = "330" }).SetName("V2 Regular Plex Id Another"); yield return new TestCaseData("plex://movie/5d7768253c3c2a001fbcab72|imdb://tt0119567|tmdb://330", new ProviderId { ImdbId = "tt0119567", TheMovieDb = "330" }).SetName("V2 Regular Plex Id Another");
yield return new TestCaseData("plex://movie/5d7768253c3c2a001fbcab72|imdb://tt0119567", new ProviderId { ImdbId = "tt0119567" }).SetName("V2 Regular Plex Id Single Imdb"); yield return new TestCaseData("plex://movie/5d7768253c3c2a001fbcab72|imdb://tt0119567", new ProviderId { ImdbId = "tt0119567" }).SetName("V2 Regular Plex Id Single Imdb");
yield return new TestCaseData("plex://movie/5d7768253c3c2a001fbcab72|tmdb://330", new ProviderId { TheMovieDb = "330" }).SetName("V2 Regular Plex Id Single Tmdb"); yield return new TestCaseData("plex://movie/5d7768253c3c2a001fbcab72|tmdb://330", new ProviderId { TheMovieDb = "330" }).SetName("V2 Regular Plex Id Single Tmdb");

View file

@ -1,4 +1,5 @@
using Microsoft.Extensions.PlatformAbstractions; using Microsoft.Extensions.PlatformAbstractions;
using System.Linq;
using System.Reflection; using System.Reflection;
namespace Ombi.Helpers namespace Ombi.Helpers
@ -8,7 +9,8 @@ namespace Ombi.Helpers
public static string GetRuntimeVersion() public static string GetRuntimeVersion()
{ {
ApplicationEnvironment app = PlatformServices.Default.Application; ApplicationEnvironment app = PlatformServices.Default.Application;
return app.ApplicationVersion; var split = app.ApplicationVersion.Split('.');
return string.Join('.', split.Take(3));
} }
} }
} }

View file

@ -81,7 +81,9 @@ namespace Ombi.Mapping.Profiles
.ForMember(dest => dest.Rating, opts => opts.MapFrom(src => src.VoteAverage.ToString())) .ForMember(dest => dest.Rating, opts => opts.MapFrom(src => src.VoteAverage.ToString()))
.ForMember(dest => dest.BackdropPath, opts => opts.MapFrom(src => src.PosterPath)) .ForMember(dest => dest.BackdropPath, opts => opts.MapFrom(src => src.PosterPath))
//.ForMember(dest => dest.Runtime, opts => opts.MapFrom(src => src.Runtime.ToString())) //.ForMember(dest => dest.Runtime, opts => opts.MapFrom(src => src.Runtime.ToString()))
.ForMember(dest => dest.Title, opts => opts.MapFrom(src => src.Title)); .ForMember(dest => dest.Title, opts => opts.MapFrom(src => src.Title))
.ForMember(dest => dest.SeasonRequests, opts => opts.MapFrom(src => src.SeasonRequests))
;
//.ForMember(dest => dest.Status, opts => opts.MapFrom(src => TraktEnumHelper.GetDescription(src.Status))) //.ForMember(dest => dest.Status, opts => opts.MapFrom(src => TraktEnumHelper.GetDescription(src.Status)))
//.ForMember(dest => dest.Trailer, //.ForMember(dest => dest.Trailer,
// opts => opts.MapFrom(src => src.Trailer != null ? src.Trailer.ToString().ToHttpsUrl() : string.Empty)) // opts => opts.MapFrom(src => src.Trailer != null ? src.Trailer.ToString().ToHttpsUrl() : string.Empty))

View file

@ -0,0 +1,342 @@
using AutoFixture;
using NUnit.Framework;
using Ombi.Notifications.Models;
using Ombi.Settings.Settings.Models;
using Ombi.Store.Entities;
using Ombi.Store.Entities.Requests;
using Ombi.Store.Repository.Requests;
using System.Collections.Generic;
using System.Linq;
namespace Ombi.Notifications.Tests
{
public class NotificationMessageCurlysTests
{
private NotificationMessageCurlys sut { get; set; }
private Fixture F { get; set; }
[SetUp]
public void Setup()
{
F = new Fixture();
F.Behaviors.OfType<ThrowingRecursionBehavior>().ToList()
.ForEach(b => F.Behaviors.Remove(b));
F.Behaviors.Add(new OmitOnRecursionBehavior());
sut = new NotificationMessageCurlys();
}
[Test]
public void MovieNotificationTests()
{
var notificationOptions = new NotificationOptions();
var req = F.Build<MovieRequests>()
.With(x => x.RequestType, RequestType.Movie)
.With(x => x.Available, true)
.Create();
var customization = new CustomizationSettings
{
ApplicationUrl = "url",
ApplicationName = "name"
};
var userPrefs = new UserNotificationPreferences();
sut.Setup(notificationOptions, req, customization, userPrefs);
Assert.That(req.Id.ToString(), Is.EqualTo(sut.RequestId));
Assert.That(req.TheMovieDbId.ToString(), Is.EqualTo(sut.ProviderId));
Assert.That(req.Title.ToString(), Is.EqualTo(sut.Title));
Assert.That(req.RequestedUser.UserName, Is.EqualTo(sut.RequestedUser));
Assert.That(req.RequestedUser.Alias, Is.EqualTo(sut.Alias));
Assert.That(req.RequestedDate.ToString("D"), Is.EqualTo(sut.RequestedDate));
Assert.That("Movie", Is.EqualTo(sut.Type));
Assert.That(req.Overview, Is.EqualTo(sut.Overview));
Assert.That(req.ReleaseDate.Year.ToString(), Is.EqualTo(sut.Year));
Assert.That(req.DeniedReason, Is.EqualTo(sut.DenyReason));
Assert.That(req.MarkedAsAvailable?.ToString("D"), Is.EqualTo(sut.AvailableDate));
Assert.That("https://image.tmdb.org/t/p/w300/" + req.PosterPath, Is.EqualTo(sut.PosterImage));
Assert.That(req.DeniedReason, Is.EqualTo(sut.DenyReason));
Assert.That(req.RequestedUser.Alias, Is.EqualTo(sut.UserPreference));
Assert.That(string.Empty, Is.EqualTo(sut.AdditionalInformation));
Assert.That("Available", Is.EqualTo(sut.RequestStatus));
Assert.That("url", Is.EqualTo(sut.ApplicationUrl));
Assert.That("name", Is.EqualTo(sut.ApplicationName));
}
[Test]
public void MovieIssueNotificationTests()
{
var notificationOptions = new NotificationOptions
{
Substitutes = new Dictionary<string, string>
{
{ "IssueDescription", "Desc" },
{ "IssueCategory", "Cat" },
{ "IssueStatus", "state" },
{ "IssueSubject", "sub" },
{ "NewIssueComment", "a" },
{ "IssueUser", "User" },
{ "IssueUserAlias", "alias" },
{ "RequestType", "Movie" },
}
};
var req = F.Build<MovieRequests>()
.With(x => x.RequestType, RequestType.Movie)
.Create();
var customization = new CustomizationSettings();
var userPrefs = new UserNotificationPreferences();
sut.Setup(notificationOptions, req, customization, userPrefs);
Assert.That("Desc", Is.EqualTo(sut.IssueDescription));
Assert.That("Cat", Is.EqualTo(sut.IssueCategory));
Assert.That("state", Is.EqualTo(sut.IssueStatus));
Assert.That("a", Is.EqualTo(sut.NewIssueComment));
Assert.That("User", Is.EqualTo(sut.UserName));
Assert.That("alias", Is.EqualTo(sut.Alias));
Assert.That("Movie", Is.EqualTo(sut.Type));
}
[Test]
public void MovieNotificationUserPreferences()
{
var notificationOptions = new NotificationOptions
{
AdditionalInformation = "add"
};
var req = F.Build<MovieRequests>()
.With(x => x.RequestType, RequestType.Movie)
.Without(x => x.MarkedAsAvailable)
.Create();
var customization = new CustomizationSettings();
var userPrefs = new UserNotificationPreferences
{
Value = "PrefValue"
};
sut.Setup(notificationOptions, req, customization, userPrefs);
Assert.That("PrefValue", Is.EqualTo(sut.UserPreference));
Assert.That(string.Empty, Is.EqualTo(sut.AvailableDate));
Assert.That("add", Is.EqualTo(sut.AdditionalInformation));
}
[TestCaseSource(nameof(RequestStatusData))]
public string MovieNotificationTests_RequestStatus(bool available, bool denied, bool approved)
{
var notificationOptions = new NotificationOptions();
var req = F.Build<MovieRequests>()
.With(x => x.RequestType, RequestType.Movie)
.With(x => x.Available, available)
.With(x => x.Denied, denied)
.With(x => x.Approved, approved)
.Create();
var customization = new CustomizationSettings();
var userPrefs = new UserNotificationPreferences();
sut.Setup(notificationOptions, req, customization, userPrefs);
return sut.RequestStatus;
}
private static IEnumerable<TestCaseData> RequestStatusData
{
get
{
yield return new TestCaseData(true, false, false).Returns("Available");
yield return new TestCaseData(false, true, false).Returns("Denied");
yield return new TestCaseData(false, false, true).Returns("Processing Request");
yield return new TestCaseData(false, false, false).Returns("Pending Approval");
}
}
[Test]
public void NewsletterTests()
{
var customization = new CustomizationSettings
{
ApplicationUrl = "url",
ApplicationName = "name"
};
sut.SetupNewsletter(customization);
Assert.That("url", Is.EqualTo(sut.ApplicationUrl));
Assert.That("name", Is.EqualTo(sut.ApplicationName));
}
[Test]
public void MusicNotificationTests()
{
var notificationOptions = new NotificationOptions();
var req = F.Build<AlbumRequest>()
.With(x => x.RequestType, RequestType.Album)
.With(x => x.Available, true)
.Create();
var customization = new CustomizationSettings
{
ApplicationUrl = "url",
ApplicationName = "name"
};
var userPrefs = new UserNotificationPreferences();
sut.Setup(notificationOptions, req, customization, userPrefs);
Assert.That(req.Id.ToString(), Is.EqualTo(sut.RequestId));
Assert.That(req.ForeignArtistId.ToString(), Is.EqualTo(sut.ProviderId));
Assert.That(req.Title.ToString(), Is.EqualTo(sut.Title));
Assert.That(req.RequestedUser.UserName, Is.EqualTo(sut.RequestedUser));
Assert.That(req.RequestedUser.Alias, Is.EqualTo(sut.Alias));
Assert.That(req.RequestedDate.ToString("D"), Is.EqualTo(sut.RequestedDate));
Assert.That("Album", Is.EqualTo(sut.Type));
Assert.That(req.ReleaseDate.Year.ToString(), Is.EqualTo(sut.Year));
Assert.That(req.DeniedReason, Is.EqualTo(sut.DenyReason));
Assert.That(req.MarkedAsAvailable?.ToString("D"), Is.EqualTo(sut.AvailableDate));
Assert.That(req.Cover, Is.EqualTo(sut.PosterImage));
Assert.That(req.DeniedReason, Is.EqualTo(sut.DenyReason));
Assert.That(req.RequestedUser.Alias, Is.EqualTo(sut.UserPreference));
Assert.That(string.Empty, Is.EqualTo(sut.AdditionalInformation));
Assert.That("Available", Is.EqualTo(sut.RequestStatus));
Assert.That("url", Is.EqualTo(sut.ApplicationUrl));
Assert.That("name", Is.EqualTo(sut.ApplicationName));
}
[TestCaseSource(nameof(RequestStatusData))]
public string MusicNotificationTests_RequestStatus(bool available, bool denied, bool approved)
{
var notificationOptions = new NotificationOptions();
var req = F.Build<AlbumRequest>()
.With(x => x.RequestType, RequestType.Album)
.With(x => x.Available, available)
.With(x => x.Denied, denied)
.With(x => x.Approved, approved)
.Create();
var customization = new CustomizationSettings();
var userPrefs = new UserNotificationPreferences();
sut.Setup(notificationOptions, req, customization, userPrefs);
return sut.RequestStatus;
}
[Test]
public void TvNotificationTests()
{
var notificationOptions = new NotificationOptions();
var req = F.Build<ChildRequests>()
.With(x => x.RequestType, RequestType.TvShow)
.With(x => x.Available, true)
.Create();
var customization = new CustomizationSettings
{
ApplicationUrl = "url",
ApplicationName = "name"
};
var userPrefs = new UserNotificationPreferences();
sut.Setup(notificationOptions, req, customization, userPrefs);
Assert.That(req.Id.ToString(), Is.EqualTo(sut.RequestId));
Assert.That(req.ParentRequest.ExternalProviderId.ToString(), Is.EqualTo(sut.ProviderId));
Assert.That(req.ParentRequest.Title.ToString(), Is.EqualTo(sut.Title));
Assert.That(req.RequestedUser.UserName, Is.EqualTo(sut.RequestedUser));
Assert.That(req.RequestedUser.Alias, Is.EqualTo(sut.Alias));
Assert.That(req.RequestedDate.ToString("D"), Is.EqualTo(sut.RequestedDate));
Assert.That("TV Show", Is.EqualTo(sut.Type));
Assert.That(req.ParentRequest.Overview, Is.EqualTo(sut.Overview));
Assert.That(req.ParentRequest.ReleaseDate.Year.ToString(), Is.EqualTo(sut.Year));
Assert.That(req.DeniedReason, Is.EqualTo(sut.DenyReason));
Assert.That(req.MarkedAsAvailable?.ToString("D"), Is.EqualTo(sut.AvailableDate));
Assert.That("https://image.tmdb.org/t/p/w300/" + req.ParentRequest.PosterPath, Is.EqualTo(sut.PosterImage));
Assert.That(req.DeniedReason, Is.EqualTo(sut.DenyReason));
Assert.That(req.RequestedUser.Alias, Is.EqualTo(sut.UserPreference));
Assert.That(null, Is.EqualTo(sut.AdditionalInformation));
Assert.That("Available", Is.EqualTo(sut.RequestStatus));
Assert.That("url", Is.EqualTo(sut.ApplicationUrl));
Assert.That("name", Is.EqualTo(sut.ApplicationName));
}
[Test]
public void TvNotification_EpisodeList()
{
var episodeRequests = new List<EpisodeRequests>
{
new EpisodeRequests
{
EpisodeNumber = 1,
},
new EpisodeRequests
{
EpisodeNumber = 2,
},
new EpisodeRequests
{
EpisodeNumber = 3,
}
};
var seasonRequests = new List<SeasonRequests>
{
new SeasonRequests
{
Episodes = episodeRequests,
SeasonNumber = 1
},
new SeasonRequests
{
Episodes = episodeRequests,
SeasonNumber = 2
},
new SeasonRequests
{
Episodes = episodeRequests,
SeasonNumber = 3
}
};
var notificationOptions = new NotificationOptions();
var req = F.Build<ChildRequests>()
.With(x => x.RequestType, RequestType.TvShow)
.With(x => x.Available, true)
.With(x => x.SeasonRequests, seasonRequests)
.Create();
var customization = new CustomizationSettings
{
ApplicationUrl = "url",
ApplicationName = "name"
};
var userPrefs = new UserNotificationPreferences();
sut.Setup(notificationOptions, req, customization, userPrefs);
Assert.That(sut.EpisodesList, Is.EqualTo("1,1,1,2,2,2,3,3,3"));
Assert.That(sut.SeasonsList, Is.EqualTo("1,2,3"));
}
[TestCaseSource(nameof(RequestStatusData))]
public string TvShowNotificationTests_RequestStatus(bool available, bool denied, bool approved)
{
var notificationOptions = new NotificationOptions();
var req = F.Build<ChildRequests>()
.With(x => x.RequestType, RequestType.TvShow)
.With(x => x.Available, available)
.With(x => x.Denied, denied)
.With(x => x.Approved, approved)
.Create();
var customization = new CustomizationSettings();
var userPrefs = new UserNotificationPreferences();
sut.Setup(notificationOptions, req, customization, userPrefs);
return sut.RequestStatus;
}
[Test]
public void EmailSetupTests()
{
var user = F.Create<OmbiUser>();
var customization = new CustomizationSettings
{
ApplicationUrl = "url",
ApplicationName = "name"
};
sut.Setup(user, customization);
Assert.That(user.UserName, Is.EqualTo(sut.RequestedUser));
Assert.That(user.UserName, Is.EqualTo(sut.UserName));
Assert.That(user.UserAlias, Is.EqualTo(sut.Alias));
Assert.That(sut.ApplicationUrl, Is.EqualTo("url"));
Assert.That(sut.ApplicationName, Is.EqualTo("name"));
}
}
}

View file

@ -5,6 +5,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AutoFixture" Version="4.11.0" />
<PackageReference Include="Nunit" Version="3.11.0" /> <PackageReference Include="Nunit" Version="3.11.0" />
<PackageReference Include="NUnit.ConsoleRunner" Version="3.9.0" /> <PackageReference Include="NUnit.ConsoleRunner" Version="3.9.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.13.0" /> <PackageReference Include="NUnit3TestAdapter" Version="3.13.0" />

View file

@ -106,21 +106,23 @@ namespace Ombi.Notifications.Agents
}; };
var fields = new List<DiscordField>(); var fields = new List<DiscordField>();
if (!settings.HideUser)
if (model.Data.TryGetValue("Alias", out var alias))
{ {
if (alias.HasValue()) if (model.Data.TryGetValue("Alias", out var alias))
{ {
fields.Add(new DiscordField { name = "Requested By", value = alias, inline = true }); if (alias.HasValue())
}
}
else
{
if (model.Data.TryGetValue("RequestedUser", out var requestedUser))
{
if (requestedUser.HasValue())
{ {
fields.Add(new DiscordField { name = "Requested By", value = requestedUser, inline = true }); fields.Add(new DiscordField { name = "Requested By", value = alias, inline = true });
}
}
else
{
if (model.Data.TryGetValue("RequestedUser", out var requestedUser))
{
if (requestedUser.HasValue())
{
fields.Add(new DiscordField { name = "Requested By", value = requestedUser, inline = true });
}
} }
} }
} }

View file

@ -240,9 +240,9 @@ namespace Ombi.Notifications.Agents
private async Task SendToSubscribers(EmailNotificationSettings settings, NotificationMessage message) private async Task SendToSubscribers(EmailNotificationSettings settings, NotificationMessage message)
{ {
if (await SubsribedUsers.AnyAsync()) if (await Subscribed.AnyAsync())
{ {
foreach (var user in SubsribedUsers) foreach (var user in Subscribed)
{ {
if (user.Email.IsNullOrEmpty()) if (user.Email.IsNullOrEmpty())
{ {

View file

@ -304,9 +304,9 @@ namespace Ombi.Notifications.Agents
private async Task AddSubscribedUsers(List<string> playerIds) private async Task AddSubscribedUsers(List<string> playerIds)
{ {
if (await SubsribedUsers.AnyAsync()) if (await Subscribed.AnyAsync())
{ {
foreach (var user in SubsribedUsers) foreach (var user in Subscribed)
{ {
var notificationId = user.NotificationUserIds; var notificationId = user.NotificationUserIds;
if (notificationId.Any()) if (notificationId.Any())

View file

@ -57,6 +57,7 @@ namespace Ombi.Notifications.Agents
var notification = new NotificationMessage var notification = new NotificationMessage
{ {
Message = parsed.Message, Message = parsed.Message,
Subject = "New Request",
Data = GetNotificationData(parsed, NotificationType.NewRequest) Data = GetNotificationData(parsed, NotificationType.NewRequest)
}; };
@ -76,6 +77,7 @@ namespace Ombi.Notifications.Agents
var notification = new NotificationMessage var notification = new NotificationMessage
{ {
Message = parsed.Message, Message = parsed.Message,
Subject = "New Issue",
Data = GetNotificationData(parsed, NotificationType.Issue) Data = GetNotificationData(parsed, NotificationType.Issue)
}; };
@ -127,6 +129,7 @@ namespace Ombi.Notifications.Agents
var notification = new NotificationMessage var notification = new NotificationMessage
{ {
Message = parsed.Message, Message = parsed.Message,
Subject = "Issue Resolved",
Data = GetNotificationData(parsed, NotificationType.IssueResolved) Data = GetNotificationData(parsed, NotificationType.IssueResolved)
}; };
@ -149,6 +152,7 @@ namespace Ombi.Notifications.Agents
var notification = new NotificationMessage var notification = new NotificationMessage
{ {
Message = parsed.Message, Message = parsed.Message,
Subject = "Request Error",
Data = GetNotificationData(parsed, NotificationType.ItemAddedToFaultQueue) Data = GetNotificationData(parsed, NotificationType.ItemAddedToFaultQueue)
}; };
@ -168,6 +172,7 @@ namespace Ombi.Notifications.Agents
var notification = new NotificationMessage var notification = new NotificationMessage
{ {
Message = parsed.Message, Message = parsed.Message,
Subject = "Request Declined",
Data = GetNotificationData(parsed, NotificationType.RequestDeclined) Data = GetNotificationData(parsed, NotificationType.RequestDeclined)
}; };
@ -188,6 +193,7 @@ namespace Ombi.Notifications.Agents
var notification = new NotificationMessage var notification = new NotificationMessage
{ {
Message = parsed.Message, Message = parsed.Message,
Subject = "Request Approved",
Data = GetNotificationData(parsed, NotificationType.RequestApproved) Data = GetNotificationData(parsed, NotificationType.RequestApproved)
}; };
@ -212,6 +218,7 @@ namespace Ombi.Notifications.Agents
var notification = new NotificationMessage var notification = new NotificationMessage
{ {
Message = parsed.Message, Message = parsed.Message,
Subject = "Request Available",
Data = data Data = data
}; };
// Send to user // Send to user
@ -259,6 +266,7 @@ namespace Ombi.Notifications.Agents
var notification = new NotificationMessage var notification = new NotificationMessage
{ {
Message = message, Message = message,
Subject = "Test Notification"
}; };
// Send to user // Send to user
var user = await _userManager.Users.Include(x => x.NotificationUserIds).FirstOrDefaultAsync(x => x.Id.Equals(model.UserId)); var user = await _userManager.Users.Include(x => x.NotificationUserIds).FirstOrDefaultAsync(x => x.Id.Equals(model.UserId));
@ -338,9 +346,9 @@ namespace Ombi.Notifications.Agents
private async Task AddSubscribedUsers(List<string> playerIds) private async Task AddSubscribedUsers(List<string> playerIds)
{ {
if (await SubsribedUsers.AnyAsync()) if (await Subscribed.AnyAsync())
{ {
foreach (var user in SubsribedUsers) foreach (var user in Subscribed)
{ {
var notificationIds = await _notifications.GetAll().Where(x => x.UserId == user.Id).ToListAsync(); var notificationIds = await _notifications.GetAll().Where(x => x.UserId == user.Id).ToListAsync();

View file

@ -48,7 +48,7 @@ namespace Ombi.Notifications
protected ChildRequests TvRequest { get; set; } protected ChildRequests TvRequest { get; set; }
protected AlbumRequest AlbumRequest { get; set; } protected AlbumRequest AlbumRequest { get; set; }
protected MovieRequests MovieRequest { get; set; } protected MovieRequests MovieRequest { get; set; }
protected IQueryable<OmbiUser> SubsribedUsers { get; private set; } protected IQueryable<OmbiUser> Subscribed { get; private set; }
public abstract string NotificationName { get; } public abstract string NotificationName { get; }
@ -75,7 +75,7 @@ namespace Ombi.Notifications
if (model.RequestId > 0) if (model.RequestId > 0)
{ {
await LoadRequest(model.RequestId, model.RequestType); await LoadRequest(model.RequestId, model.RequestType);
SubsribedUsers = GetSubscriptions(model.RequestId, model.RequestType); Subscribed = GetSubscriptions(model.RequestId, model.RequestType);
} }
Customization = await CustomizationSettings.GetSettingsAsync(); Customization = await CustomizationSettings.GetSettingsAsync();
@ -209,7 +209,6 @@ namespace Ombi.Notifications
if (model.RequestType == RequestType.Movie) if (model.RequestType == RequestType.Movie)
{ {
_log.LogDebug("Notification options: {@model}, Req: {@MovieRequest}, Settings: {@Customization}", model, MovieRequest, Customization); _log.LogDebug("Notification options: {@model}, Req: {@MovieRequest}, Settings: {@Customization}", model, MovieRequest, Customization);
curlys.Setup(model, MovieRequest, Customization, preference); curlys.Setup(model, MovieRequest, Customization, preference);
} }
else if (model.RequestType == RequestType.TvShow) else if (model.RequestType == RequestType.TvShow)

View file

@ -14,218 +14,156 @@ namespace Ombi.Notifications
{ {
public class NotificationMessageCurlys public class NotificationMessageCurlys
{ {
public void Setup(NotificationOptions opts, MovieRequests req, CustomizationSettings s, UserNotificationPreferences pref)
{
LoadIssues(opts);
RequestId = req?.Id.ToString();
ProviderId = req?.TheMovieDbId.ToString() ?? string.Empty;
string title;
if (req == null)
{
opts.Substitutes.TryGetValue("Title", out title);
}
else
{
title = req?.Title;
}
ApplicationUrl = (s?.ApplicationUrl.HasValue() ?? false) ? s.ApplicationUrl : string.Empty;
ApplicationName = string.IsNullOrEmpty(s?.ApplicationName) ? "Ombi" : s?.ApplicationName;
RequestedUser = req?.RequestedUser?.UserName;
if (UserName.IsNullOrEmpty())
{
// Can be set if it's an issue
UserName = req?.RequestedUser?.UserName;
}
if (Alias.IsNullOrEmpty())
{
// Can be set if it's an issue
Alias = (req?.RequestedUser?.Alias.HasValue() ?? false) ? req?.RequestedUser?.Alias : req?.RequestedUser?.UserName;
}
if (pref != null)
{
UserPreference = pref.Value.HasValue() ? pref.Value : Alias;
}
Title = title;
RequestedDate = req?.RequestedDate.ToString("D");
if (Type.IsNullOrEmpty())
{
Type = req?.RequestType.Humanize();
}
Overview = req?.Overview;
Year = req?.ReleaseDate.Year.ToString();
DenyReason = req?.DeniedReason;
AvailableDate = req?.MarkedAsAvailable?.ToString("D") ?? string.Empty;
PosterImage = string.Format((req?.PosterPath ?? string.Empty).StartsWith("/", StringComparison.InvariantCultureIgnoreCase)
? "https://image.tmdb.org/t/p/w300{0}" : "https://image.tmdb.org/t/p/w300/{0}", req?.PosterPath);
AdditionalInformation = opts?.AdditionalInformation ?? string.Empty;
CalculateRequestStatus(req);
}
public void Setup(NotificationOptions opts, AlbumRequest req, CustomizationSettings s, UserNotificationPreferences pref)
{
LoadIssues(opts);
RequestId = req?.Id.ToString();
ProviderId = req?.ForeignArtistId ?? string.Empty;
string title;
if (req == null)
{
opts.Substitutes.TryGetValue("Title", out title);
}
else
{
title = req?.Title;
}
ApplicationUrl = (s?.ApplicationUrl.HasValue() ?? false) ? s.ApplicationUrl : string.Empty;
ApplicationName = string.IsNullOrEmpty(s?.ApplicationName) ? "Ombi" : s?.ApplicationName;
RequestedUser = req?.RequestedUser?.UserName;
if (UserName.IsNullOrEmpty())
{
// Can be set if it's an issue
UserName = req?.RequestedUser?.UserName;
}
AvailableDate = req?.MarkedAsAvailable?.ToString("D") ?? string.Empty;
DenyReason = req?.DeniedReason;
if (Alias.IsNullOrEmpty())
{
Alias = (req?.RequestedUser?.Alias.HasValue() ?? false) ? req?.RequestedUser?.Alias : req?.RequestedUser?.UserName;
}
if (pref != null)
{
UserPreference = pref.Value.HasValue() ? pref.Value : Alias;
}
Title = title;
RequestedDate = req?.RequestedDate.ToString("D");
if (Type.IsNullOrEmpty())
{
Type = req?.RequestType.Humanize();
}
Year = req?.ReleaseDate.Year.ToString();
PosterImage = (req?.Cover.HasValue() ?? false) ? req.Cover : req?.Disk ?? string.Empty;
AdditionalInformation = opts?.AdditionalInformation ?? string.Empty;
CalculateRequestStatus(req);
}
public void SetupNewsletter(CustomizationSettings s) public void SetupNewsletter(CustomizationSettings s)
{ {
ApplicationUrl = (s?.ApplicationUrl.HasValue() ?? false) ? s.ApplicationUrl : string.Empty; ApplicationName = string.IsNullOrEmpty(s?.ApplicationName) ? "Ombi" : s.ApplicationName;
ApplicationName = string.IsNullOrEmpty(s?.ApplicationName) ? "Ombi" : s?.ApplicationName; ApplicationUrl = s?.ApplicationUrl.HasValue() ?? false ? s.ApplicationUrl : string.Empty;
}
public void Setup(NotificationOptions opts, ChildRequests req, CustomizationSettings s, UserNotificationPreferences pref)
{
LoadIssues(opts);
RequestId = req?.Id.ToString();
ProviderId = req?.ParentRequest?.ExternalProviderId.ToString() ?? string.Empty;
string title;
if (req == null)
{
opts.Substitutes.TryGetValue("Title", out title);
}
else
{
title = req?.ParentRequest.Title;
}
DenyReason = req?.DeniedReason;
ApplicationUrl = (s?.ApplicationUrl.HasValue() ?? false) ? s.ApplicationUrl : string.Empty;
ApplicationName = string.IsNullOrEmpty(s?.ApplicationName) ? "Ombi" : s?.ApplicationName;
RequestedUser = req?.RequestedUser?.UserName;
if (UserName.IsNullOrEmpty())
{
// Can be set if it's an issue
UserName = req?.RequestedUser?.UserName;
}
AvailableDate = req?.MarkedAsAvailable?.ToString("D") ?? string.Empty;
if (Alias.IsNullOrEmpty())
{
Alias = (req?.RequestedUser?.Alias.HasValue() ?? false) ? req?.RequestedUser?.Alias : req?.RequestedUser?.UserName;
}
if (pref != null)
{
UserPreference = pref.Value.HasValue() ? pref.Value : Alias;
}
Title = title;
RequestedDate = req?.RequestedDate.ToString("D");
if (Type.IsNullOrEmpty())
{
Type = req?.RequestType.Humanize();
}
Overview = req?.ParentRequest.Overview;
Year = req?.ParentRequest.ReleaseDate.Year.ToString();
PosterImage = string.Format((req?.ParentRequest.PosterPath ?? string.Empty).StartsWith("/", StringComparison.InvariantCultureIgnoreCase)
? "https://image.tmdb.org/t/p/w300{0}" : "https://image.tmdb.org/t/p/w300/{0}", req?.ParentRequest.PosterPath);
AdditionalInformation = opts.AdditionalInformation;
// DO Episode and Season Lists
var episodes = req?.SeasonRequests?.SelectMany(x => x.Episodes) ?? new List<EpisodeRequests>();
var seasons = req?.SeasonRequests?.OrderBy(x => x.SeasonNumber).ToList() ?? new List<SeasonRequests>();
var orderedEpisodes = episodes.OrderBy(x => x.EpisodeNumber).ToList();
var epSb = new StringBuilder();
var seasonSb = new StringBuilder();
for (var i = 0; i < orderedEpisodes.Count; i++)
{
var ep = orderedEpisodes[i];
if (i < orderedEpisodes.Count - 1)
{
epSb.Append($"{ep.EpisodeNumber},");
}
else
{
epSb.Append($"{ep.EpisodeNumber}");
}
}
for (var i = 0; i < seasons.Count; i++)
{
var ep = seasons[i];
if (i < seasons.Count - 1)
{
seasonSb.Append($"{ep.SeasonNumber},");
}
else
{
seasonSb.Append($"{ep.SeasonNumber}");
}
}
EpisodesList = epSb.ToString();
SeasonsList = seasonSb.ToString();
CalculateRequestStatus(req);
} }
public void Setup(OmbiUser user, CustomizationSettings s) public void Setup(OmbiUser user, CustomizationSettings s)
{ {
ApplicationUrl = (s?.ApplicationUrl.HasValue() ?? false) ? s.ApplicationUrl : string.Empty; ApplicationName = string.IsNullOrEmpty(s?.ApplicationName) ? "Ombi" : s.ApplicationName;
ApplicationName = string.IsNullOrEmpty(s?.ApplicationName) ? "Ombi" : s?.ApplicationName; ApplicationUrl = s?.ApplicationUrl.HasValue() ?? false ? s.ApplicationUrl : string.Empty;
RequestedUser = user.UserName; RequestedUser = user.UserName;
Alias = user.UserAlias; Alias = user.UserAlias;
UserName = user.UserName; UserName = user.UserName;
} }
public void Setup(NotificationOptions opts, MovieRequests req, CustomizationSettings s,
UserNotificationPreferences pref)
{
LoadIssues(opts);
LoadCommon(req, s, pref);
LoadTitle(opts, req);
ProviderId = req?.TheMovieDbId.ToString() ?? string.Empty;
Year = req?.ReleaseDate.Year.ToString();
Overview = req?.Overview;
AdditionalInformation = opts?.AdditionalInformation ?? string.Empty;
PosterImage = $"https://image.tmdb.org/t/p/w300/{req?.PosterPath?.TrimStart('/') ?? string.Empty}";
CalculateRequestStatus(req);
}
public void Setup(NotificationOptions opts, ChildRequests req, CustomizationSettings s,
UserNotificationPreferences pref)
{
LoadIssues(opts);
LoadCommon(req, s, pref);
LoadTitle(opts, req);
ProviderId = req?.ParentRequest?.ExternalProviderId.ToString() ?? string.Empty;
Year = req?.ParentRequest?.ReleaseDate.Year.ToString();
Overview = req?.ParentRequest?.Overview;
AdditionalInformation = opts.AdditionalInformation;
PosterImage =
$"https://image.tmdb.org/t/p/w300/{req?.ParentRequest?.PosterPath?.TrimStart('/') ?? string.Empty}";
// Generate episode list.
StringBuilder epSb = new StringBuilder();
IEnumerable<EpisodeRequests> episodes = req?.SeasonRequests?
.SelectMany(x => x.Episodes) ?? new List<EpisodeRequests>();
episodes
.OrderBy(x => x.EpisodeNumber)
.ToList()
.ForEach(ep => epSb.Append($"{ep.EpisodeNumber},"));
if (epSb.Length > 0) epSb.Remove(epSb.Length - 1, 1);
EpisodesList = epSb.ToString();
// Generate season list.
StringBuilder seasonSb = new StringBuilder();
List<SeasonRequests> seasons = req?.SeasonRequests ?? new List<SeasonRequests>();
seasons
.OrderBy(x => x.SeasonNumber)
.ToList()
.ForEach(ep => seasonSb.Append($"{ep.SeasonNumber},"));
if (seasonSb.Length > 0) seasonSb.Remove(seasonSb.Length - 1, 1);
SeasonsList = seasonSb.ToString();
CalculateRequestStatus(req);
}
public void Setup(NotificationOptions opts, AlbumRequest req, CustomizationSettings s,
UserNotificationPreferences pref)
{
LoadIssues(opts);
LoadCommon(req, s, pref);
LoadTitle(opts, req);
ProviderId = req?.ForeignArtistId ?? string.Empty;
Year = req?.ReleaseDate.Year.ToString();
AdditionalInformation = opts?.AdditionalInformation ?? string.Empty;
PosterImage = req?.Cover.HasValue() ?? false ? req.Cover : req?.Disk ?? string.Empty;
CalculateRequestStatus(req);
}
private void LoadIssues(NotificationOptions opts) private void LoadIssues(NotificationOptions opts)
{ {
var val = string.Empty; IssueDescription = opts.Substitutes.TryGetValue("IssueDescription", out string val) ? val : string.Empty;
IssueDescription = opts.Substitutes.TryGetValue("IssueDescription", out val) ? val : string.Empty;
IssueCategory = opts.Substitutes.TryGetValue("IssueCategory", out val) ? val : string.Empty; IssueCategory = opts.Substitutes.TryGetValue("IssueCategory", out val) ? val : string.Empty;
IssueStatus = opts.Substitutes.TryGetValue("IssueStatus", out val) ? val : string.Empty; IssueStatus = opts.Substitutes.TryGetValue("IssueStatus", out val) ? val : string.Empty;
IssueSubject = opts.Substitutes.TryGetValue("IssueSubject", out val) ? val : string.Empty; IssueSubject = opts.Substitutes.TryGetValue("IssueSubject", out val) ? val : string.Empty;
NewIssueComment = opts.Substitutes.TryGetValue("NewIssueComment", out val) ? val : string.Empty; NewIssueComment = opts.Substitutes.TryGetValue("NewIssueComment", out val) ? val : string.Empty;
UserName = opts.Substitutes.TryGetValue("IssueUser", out val) ? val : string.Empty; UserName = opts.Substitutes.TryGetValue("IssueUser", out val) ? val : string.Empty;
Alias = opts.Substitutes.TryGetValue("IssueUserAlias", out val) ? val : string.Empty; Alias = opts.Substitutes.TryGetValue("IssueUserAlias", out val) ? val : string.Empty;
Type = opts.Substitutes.TryGetValue("RequestType", out val) ? val.Humanize() : string.Empty; Type = opts.Substitutes.TryGetValue("RequestType", out val) && Enum.TryParse(val, out RequestType type)
? HumanizeReturnType(type)
: string.Empty;
}
private void LoadCommon(BaseRequest req, CustomizationSettings s, UserNotificationPreferences pref)
{
ApplicationName = string.IsNullOrEmpty(s?.ApplicationName) ? "Ombi" : s.ApplicationName;
ApplicationUrl = s?.ApplicationUrl.HasValue() ?? false ? s.ApplicationUrl : string.Empty;
AvailableDate = req?.MarkedAsAvailable?.ToString("D") ?? string.Empty;
DenyReason = req?.DeniedReason;
RequestId = req?.Id.ToString();
RequestedUser = req?.RequestedUser?.UserName;
RequestedDate = req?.RequestedDate.ToString("D");
if (Type.IsNullOrEmpty())
{
Type = HumanizeReturnType(req?.RequestType);
}
if (UserName.IsNullOrEmpty())
{
UserName = req?.RequestedUser?.UserName;
}
if (Alias.IsNullOrEmpty())
{
Alias = req?.RequestedUser?.Alias.HasValue() ?? false
? req.RequestedUser?.Alias
: req?.RequestedUser?.UserName;
}
if (pref != null)
{
UserPreference = pref.Value.HasValue() ? pref.Value : Alias;
}
}
private static string HumanizeReturnType(RequestType? requestType)
{
return requestType switch
{
null => string.Empty,
RequestType.TvShow => "TV Show",
_ => requestType.Humanize()
};
}
private void LoadTitle(NotificationOptions opts, BaseRequest req)
{
switch (req)
{
case null:
opts.Substitutes.TryGetValue("Title", out string title);
Title = title;
break;
case ChildRequests tvShowRequest:
Title = tvShowRequest.ParentRequest?.Title;
break;
default:
Title = req.Title;
break;
}
} }
private void CalculateRequestStatus(BaseRequest req) private void CalculateRequestStatus(BaseRequest req)
@ -238,16 +176,19 @@ namespace Ombi.Notifications
RequestStatus = "Available"; RequestStatus = "Available";
return; return;
} }
if (req.Denied ?? false) if (req.Denied ?? false)
{ {
RequestStatus = "Denied"; RequestStatus = "Denied";
return; return;
} }
if (!req.Available && req.Approved) if (!req.Available && req.Approved)
{ {
RequestStatus = "Processing Request"; RequestStatus = "Processing Request";
return; return;
} }
RequestStatus = "Pending Approval"; RequestStatus = "Pending Approval";
} }
} }
@ -288,36 +229,36 @@ namespace Ombi.Notifications
public Dictionary<string, string> Curlys => new Dictionary<string, string> public Dictionary<string, string> Curlys => new Dictionary<string, string>
{ {
{nameof(RequestId), RequestId }, { nameof(RequestId), RequestId },
{nameof(RequestedUser), RequestedUser }, { nameof(RequestedUser), RequestedUser },
{nameof(Title), Title }, { nameof(Title), Title },
{nameof(RequestedDate), RequestedDate }, { nameof(RequestedDate), RequestedDate },
{nameof(Type), Type }, { nameof(Type), Type },
{nameof(AdditionalInformation), AdditionalInformation }, { nameof(AdditionalInformation), AdditionalInformation },
{nameof(LongDate),LongDate}, { nameof(LongDate), LongDate },
{nameof(ShortDate),ShortDate}, { nameof(ShortDate), ShortDate },
{nameof(LongTime),LongTime}, { nameof(LongTime), LongTime },
{nameof(ShortTime),ShortTime}, { nameof(ShortTime), ShortTime },
{nameof(Overview),Overview}, { nameof(Overview), Overview },
{nameof(Year),Year}, { nameof(Year), Year },
{nameof(EpisodesList),EpisodesList}, { nameof(EpisodesList), EpisodesList },
{nameof(SeasonsList),SeasonsList}, { nameof(SeasonsList), SeasonsList },
{nameof(PosterImage),PosterImage}, { nameof(PosterImage), PosterImage },
{nameof(ApplicationName),ApplicationName}, { nameof(ApplicationName), ApplicationName },
{nameof(ApplicationUrl),ApplicationUrl}, { nameof(ApplicationUrl), ApplicationUrl },
{nameof(IssueDescription),IssueDescription}, { nameof(IssueDescription), IssueDescription },
{nameof(IssueCategory),IssueCategory}, { nameof(IssueCategory), IssueCategory },
{nameof(IssueStatus),IssueStatus}, { nameof(IssueStatus), IssueStatus },
{nameof(IssueSubject),IssueSubject}, { nameof(IssueSubject), IssueSubject },
{nameof(NewIssueComment),NewIssueComment}, { nameof(NewIssueComment), NewIssueComment },
{nameof(IssueUser),IssueUser}, { nameof(IssueUser), IssueUser },
{nameof(UserName),UserName}, { nameof(UserName), UserName },
{nameof(Alias),Alias}, { nameof(Alias), Alias },
{nameof(UserPreference),UserPreference}, { nameof(UserPreference), UserPreference },
{nameof(DenyReason),DenyReason}, { nameof(DenyReason), DenyReason },
{nameof(AvailableDate),AvailableDate}, { nameof(AvailableDate), AvailableDate },
{nameof(RequestStatus),RequestStatus}, { nameof(RequestStatus), RequestStatus },
{nameof(ProviderId),ProviderId}, { nameof(ProviderId), ProviderId },
}; };
} }
} }

View file

@ -17,6 +17,7 @@ using Ombi.Store.Entities;
using Ombi.Store.Entities.Requests; using Ombi.Store.Entities.Requests;
using Ombi.Store.Repository; using Ombi.Store.Repository;
using Ombi.Store.Repository.Requests; using Ombi.Store.Repository.Requests;
using Ombi.Helpers;
namespace Ombi.Schedule.Tests namespace Ombi.Schedule.Tests
{ {
@ -53,7 +54,7 @@ namespace Ombi.Schedule.Tests
ImdbId = "test" ImdbId = "test"
}; };
_movie.Setup(x => x.GetAll()).Returns(new List<MovieRequests> { request }.AsQueryable()); _movie.Setup(x => x.GetAll()).Returns(new List<MovieRequests> { request }.AsQueryable());
_repo.Setup(x => x.Get("test")).ReturnsAsync(new PlexServerContent()); _repo.Setup(x => x.Get("test", ProviderType.ImdbId)).ReturnsAsync(new PlexServerContent());
await Checker.Execute(null); await Checker.Execute(null);

View file

@ -5,6 +5,6 @@ namespace Ombi.Schedule.Jobs.Ombi
public interface IOmbiAutomaticUpdater : IBaseJob public interface IOmbiAutomaticUpdater : IBaseJob
{ {
string[] GetVersion(); string[] GetVersion();
Task<bool> UpdateAvailable(string branch, string currentVersion); Task<bool> UpdateAvailable(string currentVersion);
} }
} }

View file

@ -49,10 +49,10 @@ namespace Ombi.Schedule.Jobs.Ombi
var productArray = productVersion.Split('-'); var productArray = productVersion.Split('-');
return productArray; return productArray;
} }
public async Task<bool> UpdateAvailable(string branch, string currentVersion) public async Task<bool> UpdateAvailable(string currentVersion)
{ {
var updates = await Processor.Process(branch); var updates = await Processor.Process();
var serverVersion = updates.UpdateVersionString; var serverVersion = updates.UpdateVersionString;
return !serverVersion.Equals(currentVersion, StringComparison.CurrentCultureIgnoreCase); return !serverVersion.Equals(currentVersion, StringComparison.CurrentCultureIgnoreCase);
@ -88,7 +88,7 @@ namespace Ombi.Schedule.Jobs.Ombi
Logger.LogDebug(LoggingEvents.Updater, "Looking for updates now"); Logger.LogDebug(LoggingEvents.Updater, "Looking for updates now");
//TODO this fails because the branch = featureupdater when it should be feature/updater //TODO this fails because the branch = featureupdater when it should be feature/updater
var updates = await Processor.Process(branch); var updates = await Processor.Process();
Logger.LogDebug(LoggingEvents.Updater, "Updates: {0}", updates); Logger.LogDebug(LoggingEvents.Updater, "Updates: {0}", updates);

View file

@ -183,13 +183,13 @@ namespace Ombi.Schedule.Jobs.Plex
PlexServerContent item = null; PlexServerContent item = null;
if (movie.ImdbId.HasValue()) if (movie.ImdbId.HasValue())
{ {
item = await _repo.Get(movie.ImdbId); item = await _repo.Get(movie.ImdbId, ProviderType.ImdbId);
} }
if (item == null) if (item == null)
{ {
if (movie.TheMovieDbId.ToString().HasValue()) if (movie.TheMovieDbId.ToString().HasValue())
{ {
item = await _repo.Get(movie.TheMovieDbId.ToString()); item = await _repo.Get(movie.TheMovieDbId.ToString(), ProviderType.TheMovieDbId);
} }
} }
if (item == null) if (item == null)

View file

@ -109,8 +109,8 @@ namespace Ombi.Core.Processor
public string UpdateVersionString { get; set; } public string UpdateVersionString { get; set; }
public int UpdateVersion { get; set; } public int UpdateVersion { get; set; }
public DateTime UpdateDate { get; set; } public DateTime UpdateDate { get; set; }
public bool UpdateAvailable { get; set; }
public List<ChangeLog> ChangeLogs { get; set; } public string ChangeLogs { get; set; }
public List<Downloads> Downloads { get; set; } public List<Downloads> Downloads { get; set; }
} }

View file

@ -16,94 +16,41 @@ namespace Ombi.Schedule.Processor
{ {
public class ChangeLogProcessor : IChangeLogProcessor public class ChangeLogProcessor : IChangeLogProcessor
{ {
public ChangeLogProcessor(IApi api, IOmbiHttpClient client) public ChangeLogProcessor(IApi api, IHttpClientFactory client)
{ {
_api = api; _api = api;
_client = client; _client = client.CreateClient("OmbiClient");
} }
private readonly IApi _api; private readonly IApi _api;
private readonly IOmbiHttpClient _client; private readonly HttpClient _client;
private const string _changeLogUrl = "https://raw.githubusercontent.com/tidusjar/Ombi/{0}/CHANGELOG.md"; private const string _changeLogUrl = "https://raw.githubusercontent.com/tidusjar/Ombi/{0}/CHANGELOG.md";
private const string AppveyorApiUrl = "https://ci.appveyor.com/api"; private const string AppveyorApiUrl = "https://ci.appveyor.com/api";
private string ChangeLogUrl(string branch) => string.Format(_changeLogUrl, branch); private string ChangeLogUrl(string branch) => string.Format(_changeLogUrl, branch);
public async Task<UpdateModel> Process(string branch) public async Task<UpdateModel> Process()
{ {
var masterBranch = branch.Equals("master", StringComparison.CurrentCultureIgnoreCase);
string githubChangeLog;
githubChangeLog = await _client.GetStringAsync(new Uri(ChangeLogUrl(branch)));
var html = Markdown.ToHtml(githubChangeLog);
var doc = new HtmlDocument();
doc.LoadHtml(html);
HtmlNode latestRelease;
if (masterBranch)
{
latestRelease = doc.DocumentNode.Descendants("h2")
.FirstOrDefault(x => x.InnerText != "(unreleased)");
}
else
{
latestRelease = doc.DocumentNode.Descendants("h2")
.FirstOrDefault(x => x.InnerText == "(unreleased)");
if (latestRelease == null)
{
latestRelease = doc.DocumentNode.Descendants("h2")
.FirstOrDefault(x => x.InnerText != "(unreleased)");
}
}
var newFeatureList = latestRelease.NextSibling.NextSibling.NextSibling.NextSibling;
var featuresString = newFeatureList.ChildNodes.Where(x => x.Name != "#text").Select(x => x.InnerText.Replace("\\n", "")).ToList();
var fixes = newFeatureList.NextSibling.NextSibling.NextSibling.NextSibling;
var fixesString = fixes.ChildNodes.Where(x => x.Name != "#text").Select(x => x.InnerText.Replace("\\n", "")).ToList();
// Cleanup
var featuresList = featuresString.Distinct().ToList();
var fixesList = fixesString.Distinct().ToList();
// Get release
var release = new Release var release = new Release
{ {
Version = latestRelease.InnerText,
Features = featuresList,
Fixes = fixesList,
Downloads = new List<Downloads>() Downloads = new List<Downloads>()
}; };
if (masterBranch) await GetGitubRelease(release);
{
var releaseTag = latestRelease.InnerText.Substring(0, 9);
await GetGitubRelease(release, releaseTag);
}
else
{
// Get AppVeyor
await GetAppVeyorRelease(release, branch);
}
return TransformUpdate(release,!masterBranch);
return TransformUpdate(release);
} }
private UpdateModel TransformUpdate(Release release, bool develop) private UpdateModel TransformUpdate(Release release)
{ {
var newUpdate = new UpdateModel var newUpdate = new UpdateModel
{ {
UpdateVersionString = develop ? release.Version : release.Version.Substring(1,8), UpdateVersionString = release.Version,
UpdateVersion = release.Version == "(unreleased)" ? 0 : int.Parse(release.Version.Substring(1, 5).Replace(".", "")), UpdateVersion = int.Parse(release.Version.Substring(1, 5).Replace(".", "")),
UpdateDate = DateTime.Now, UpdateDate = DateTime.Now,
ChangeLogs = new List<ChangeLog>(), ChangeLogs = release.Description,
Downloads = new List<Downloads>() Downloads = new List<Downloads>(),
}; UpdateAvailable = release.Version != "v" + AssemblyHelper.GetRuntimeVersion()
};
foreach (var dl in release.Downloads) foreach (var dl in release.Downloads)
{ {
@ -114,75 +61,16 @@ namespace Ombi.Schedule.Processor
}); });
} }
foreach (var f in release.Features)
{
var change = new ChangeLog
{
Descripion = f,
Type = "New",
};
newUpdate.ChangeLogs.Add(change);
}
foreach (var f in release.Fixes)
{
var change = new ChangeLog
{
Descripion = f,
Type = "Fixed",
};
newUpdate.ChangeLogs.Add(change);
}
return newUpdate; return newUpdate;
} }
private async Task GetAppVeyorRelease(Release release, string branch) private async Task GetGitubRelease(Release release)
{ {
var request = new Request($"/projects/tidusjar/requestplex/branch/{branch}", AppVeyorApi.AppveyorApiUrl, HttpMethod.Get); var client = new GitHubClient(Octokit.ProductHeaderValue.Parse("OmbiV4"));
request.ApplicationJsonContentType();
var builds = await _api.Request<AppveyorBranchResult>(request); var releases = await client.Repository.Release.GetAll("ombi-app", "ombi");
var jobId = builds.build.jobs.FirstOrDefault()?.jobId ?? string.Empty; var latest = releases.OrderByDescending(x => x.CreatedAt).FirstOrDefault();
if (builds.build.finished == DateTime.MinValue || builds.build.status.Equals("failed"))
{
return;
}
release.Version = builds.build.version;
// get the artifacts
request = new Request($"/buildjobs/{jobId}/artifacts", AppVeyorApi.AppveyorApiUrl, HttpMethod.Get);
request.ApplicationJsonContentType();
var artifacts = await _api.Request<List<BuildArtifacts>>(request);
foreach (var item in artifacts)
{
var d = new Downloads
{
Name = item.fileName,
Url = $"{AppveyorApiUrl}/buildjobs/{jobId}/artifacts/{item.fileName}"
};
release.Downloads.Add(d);
}
}
private async Task GetGitubRelease(Release release, string releaseTag)
{
var client = new GitHubClient(Octokit.ProductHeaderValue.Parse("OmbiV3"));
var releases = await client.Repository.Release.GetAll("tidusjar", "ombi");
var latest = releases.FirstOrDefault(x => x.TagName.Equals(releaseTag, StringComparison.InvariantCultureIgnoreCase));
if (latest.Name.Contains("V2", CompareOptions.IgnoreCase))
{
latest = null;
}
if (latest == null)
{
latest = releases.OrderByDescending(x => x.CreatedAt).FirstOrDefault();
}
foreach (var item in latest.Assets) foreach (var item in latest.Assets)
{ {
var d = new Downloads var d = new Downloads
@ -192,6 +80,8 @@ namespace Ombi.Schedule.Processor
}; };
release.Downloads.Add(d); release.Downloads.Add(d);
} }
release.Description = Markdown.ToHtml(latest.Body);
release.Version = latest.TagName;
} }
} }
public class Release public class Release
@ -199,8 +89,7 @@ namespace Ombi.Schedule.Processor
public string Version { get; set; } public string Version { get; set; }
public string CheckinVersion { get; set; } public string CheckinVersion { get; set; }
public List<Downloads> Downloads { get; set; } public List<Downloads> Downloads { get; set; }
public List<string> Features { get; set; } public string Description { get; set; }
public List<string> Fixes { get; set; }
} }
public class Downloads public class Downloads

View file

@ -4,6 +4,6 @@ namespace Ombi.Core.Processor
{ {
public interface IChangeLogProcessor public interface IChangeLogProcessor
{ {
Task<UpdateModel> Process(string branch); Task<UpdateModel> Process();
} }
} }

View file

@ -20,6 +20,7 @@
public bool AddOnly { get; set; } public bool AddOnly { get; set; }
public bool V3 { get; set; } public bool V3 { get; set; }
public int LanguageProfile { get; set; } public int LanguageProfile { get; set; }
public int LanguageProfileAnime { get; set; }
public bool ScanForAvailability { get; set; } public bool ScanForAvailability { get; set; }
} }
} }

View file

@ -9,6 +9,7 @@ namespace Ombi.Settings.Settings.Models.Notifications
public string WebhookUrl { get; set; } public string WebhookUrl { get; set; }
public string Username { get; set; } public string Username { get; set; }
public string Icon { get; set; } public string Icon { get; set; }
public bool HideUser { get; set; }
[JsonIgnore] [JsonIgnore]
public string WebHookId => SplitWebUrl(4); public string WebHookId => SplitWebUrl(4);

View file

@ -6,7 +6,6 @@
public bool CollectAnalyticData { get; set; } public bool CollectAnalyticData { get; set; }
public bool Wizard { get; set; } public bool Wizard { get; set; }
public string ApiKey { get; set; } public string ApiKey { get; set; }
public bool IgnoreCertificateErrors { get; set; }
public bool DoNotSendNotificationsForAutoApprove { get; set; } public bool DoNotSendNotificationsForAutoApprove { get; set; }
public bool HideRequestsUsers { get; set; } public bool HideRequestsUsers { get; set; }
public bool DisableHealthChecks { get; set; } public bool DisableHealthChecks { get; set; }

View file

@ -16,6 +16,7 @@ namespace Ombi.Store.Entities.Requests
public string ImdbId { get; set; } public string ImdbId { get; set; }
public int? QualityOverride { get; set; } public int? QualityOverride { get; set; }
public int? RootFolder { get; set; } public int? RootFolder { get; set; }
public int? LanguageProfile { get; set; }
public string Overview { get; set; } public string Overview { get; set; }
public string Title { get; set; } public string Title { get; set; }
public string PosterPath { get; set; } public string PosterPath { get; set; }

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,23 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace Ombi.Store.Migrations.OmbiMySql
{
public partial class SonarrProfileOnRequest : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "LanguageProfile",
table: "TvRequests",
type: "int",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "LanguageProfile",
table: "TvRequests");
}
}
}

View file

@ -771,6 +771,9 @@ namespace Ombi.Store.Migrations.OmbiMySql
b.Property<string>("ImdbId") b.Property<string>("ImdbId")
.HasColumnType("longtext"); .HasColumnType("longtext");
b.Property<int?>("LanguageProfile")
.HasColumnType("int");
b.Property<string>("Overview") b.Property<string>("Overview")
.HasColumnType("longtext"); .HasColumnType("longtext");

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,23 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace Ombi.Store.Migrations.OmbiSqlite
{
public partial class SonarrProfileOnRequest : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "LanguageProfile",
table: "TvRequests",
type: "INTEGER",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "LanguageProfile",
table: "TvRequests");
}
}
}

View file

@ -770,6 +770,9 @@ namespace Ombi.Store.Migrations.OmbiSqlite
b.Property<string>("ImdbId") b.Property<string>("ImdbId")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<int?>("LanguageProfile")
.HasColumnType("INTEGER");
b.Property<string>("Overview") b.Property<string>("Overview")
.HasColumnType("TEXT"); .HasColumnType("TEXT");

View file

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Data.Sqlite; using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -28,6 +29,11 @@ namespace Ombi.Store.Repository
return await _db.FindAsync(key); return await _db.FindAsync(key);
} }
public async Task<T> Find(object key, CancellationToken cancellationToken)
{
return await _db.FindAsync(new[] { key }, cancellationToken: cancellationToken);
}
public IQueryable<T> GetAll() public IQueryable<T> GetAll()
{ {
return _db.AsQueryable(); return _db.AsQueryable();

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Ombi.Helpers;
using Ombi.Store.Entities; using Ombi.Store.Entities;
namespace Ombi.Store.Repository namespace Ombi.Store.Repository
@ -10,7 +11,7 @@ namespace Ombi.Store.Repository
public interface IPlexContentRepository : IExternalRepository<PlexServerContent> public interface IPlexContentRepository : IExternalRepository<PlexServerContent>
{ {
Task<bool> ContentExists(string providerId); Task<bool> ContentExists(string providerId);
Task<PlexServerContent> Get(string providerId); Task<PlexServerContent> Get(string providerId, ProviderType type);
Task<PlexServerContent> GetByKey(int key); Task<PlexServerContent> GetByKey(int key);
Task Update(PlexServerContent existingContent); Task Update(PlexServerContent existingContent);
IQueryable<PlexEpisode> GetAllEpisodes(); IQueryable<PlexEpisode> GetAllEpisodes();

View file

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Query;
@ -12,6 +13,7 @@ namespace Ombi.Store.Repository
public interface IRepository<T> where T : Entity public interface IRepository<T> where T : Entity
{ {
Task<T> Find(object key); Task<T> Find(object key);
Task<T> Find(object key, CancellationToken cancellationToken);
IQueryable<T> GetAll(); IQueryable<T> GetAll();
Task<T> FirstOrDefaultAsync(Expression<Func<T, bool>> predicate); Task<T> FirstOrDefaultAsync(Expression<Func<T, bool>> predicate);
Task AddRange(IEnumerable<T> content, bool save = true); Task AddRange(IEnumerable<T> content, bool save = true);

View file

@ -31,6 +31,7 @@ using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Ombi.Helpers;
using Ombi.Store.Context; using Ombi.Store.Context;
using Ombi.Store.Entities; using Ombi.Store.Entities;
@ -61,18 +62,21 @@ namespace Ombi.Store.Repository
return any; return any;
} }
public async Task<PlexServerContent> Get(string providerId) public async Task<PlexServerContent> Get(string providerId, ProviderType type)
{ {
var item = await Db.PlexServerContent.FirstOrDefaultAsync(x => x.ImdbId == providerId); switch (type)
if (item == null)
{ {
item = await Db.PlexServerContent.FirstOrDefaultAsync(x => x.TheMovieDbId == providerId); case ProviderType.ImdbId:
if (item == null) return await Db.PlexServerContent.FirstOrDefaultAsync(x => x.ImdbId == providerId);
{ case ProviderType.TheMovieDbId:
item = await Db.PlexServerContent.FirstOrDefaultAsync(x => x.TvDbId == providerId); return await Db.PlexServerContent.FirstOrDefaultAsync(x => x.TheMovieDbId == providerId);
} case ProviderType.TvDbId:
return await Db.PlexServerContent.FirstOrDefaultAsync(x => x.TvDbId == providerId);
default:
break;
} }
return item;
return null;
} }
public async Task<PlexServerContent> GetByKey(int key) public async Task<PlexServerContent> GetByKey(int key)

View file

@ -97,6 +97,8 @@ namespace Ombi.Api.TheMovieDb.Models
{ {
[JsonProperty("results")] [JsonProperty("results")]
public List<KeywordsValue> KeywordsValue { get; set; } public List<KeywordsValue> KeywordsValue { get; set; }
[JsonProperty("keywords")]
private List<KeywordsValue> _movieKeywordValue { set { KeywordsValue = value; }}
} }
public class KeywordsValue public class KeywordsValue

View file

@ -1,4 +1,7 @@
namespace Ombi.Api.TheMovieDb.Models using Ombi.Store.Repository.Requests;
using System.Collections.Generic;
namespace Ombi.Api.TheMovieDb.Models
{ {
public class MovieDbSearchResult public class MovieDbSearchResult
{ {
@ -16,5 +19,10 @@
public int VoteCount { get; set; } public int VoteCount { get; set; }
public bool Video { get; set; } public bool Video { get; set; }
public float VoteAverage { get; set; } public float VoteAverage { get; set; }
/// <summary>
/// Mapped Property and not set from the API
/// </summary>
public List<SeasonRequests> SeasonRequests { get; set; } = new List<SeasonRequests>();
} }
} }

View file

@ -59,6 +59,7 @@ export interface IDiscordNotifcationSettings extends INotificationSettings {
webhookUrl: string; webhookUrl: string;
username: string; username: string;
icon: string; icon: string;
hideUser: boolean;
notificationTemplates: INotificationTemplates[]; notificationTemplates: INotificationTemplates[];
} }

View file

@ -1,5 +1,6 @@
import { IChildRequests, IMovieRequests } from "."; import { IChildRequests, IMovieRequests } from ".";
import { ITvRequests } from "./IRequestModel"; import { ITvRequests } from "./IRequestModel";
import { ILanguageProfiles } from "./ISonarr";
export interface IRadarrRootFolder { export interface IRadarrRootFolder {
id: number; id: number;
@ -27,6 +28,9 @@ export interface IAdvancedData {
rootFolder: IRadarrRootFolder; rootFolder: IRadarrRootFolder;
rootFolders: IRadarrRootFolder[]; rootFolders: IRadarrRootFolder[];
rootFolderId: number; rootFolderId: number;
language: ILanguageProfiles;
languages: ILanguageProfiles[];
languageId: number;
movieRequest: IMovieRequests; movieRequest: IMovieRequests;
tvRequest: ITvRequests; tvRequest: ITvRequests;
} }

View file

@ -26,6 +26,7 @@ export interface IMovieAdvancedOptions {
requestId: number; requestId: number;
qualityOverride: number; qualityOverride: number;
rootPathOverride: number; rootPathOverride: number;
languageProfile: number;
} }
export interface IAlbumRequest extends IBaseRequest { export interface IAlbumRequest extends IBaseRequest {
@ -110,6 +111,7 @@ export interface ITvRequests {
status: string; status: string;
childRequests: IChildRequests[]; childRequests: IChildRequests[];
qualityOverride: number; qualityOverride: number;
languageProfile: number;
background: any; background: any;
totalSeasons: number; totalSeasons: number;
tvDbId: number; // NO LONGER USED tvDbId: number; // NO LONGER USED
@ -119,6 +121,7 @@ export interface ITvRequests {
// For UI display // For UI display
qualityOverrideTitle: string; qualityOverrideTitle: string;
rootPathOverrideTitle: string; rootPathOverrideTitle: string;
languageOverrideTitle: string;
} }
export interface IChildRequests extends IBaseRequest { export interface IChildRequests extends IBaseRequest {

View file

@ -51,6 +51,7 @@ export interface ITvRequestViewModelBase extends BaseRequestOptions {
requestAll: boolean; requestAll: boolean;
firstSeason: boolean; firstSeason: boolean;
latestSeason: boolean; latestSeason: boolean;
languageProfile: number | undefined;
seasons: ISeasonsViewModel[]; seasons: ISeasonsViewModel[];
} }

View file

@ -12,7 +12,6 @@ export interface IOmbiSettings extends ISettings {
collectAnalyticData: boolean; collectAnalyticData: boolean;
wizard: boolean; wizard: boolean;
apiKey: string; apiKey: string;
ignoreCertificateErrors: boolean;
doNotSendNotificationsForAutoApprove: boolean; doNotSendNotificationsForAutoApprove: boolean;
hideRequestsUsers: boolean; hideRequestsUsers: boolean;
defaultLanguageCode: string; defaultLanguageCode: string;
@ -104,6 +103,7 @@ export interface ISonarrSettings extends IExternalSettings {
addOnly: boolean; addOnly: boolean;
v3: boolean; v3: boolean;
languageProfile: number; languageProfile: number;
languageProfileAnime: number;
scanForAvailability: boolean; scanForAvailability: boolean;
} }
@ -285,3 +285,19 @@ export interface ITheMovieDbSettings extends ISettings {
showAdultMovies: boolean; showAdultMovies: boolean;
excludedKeywordIds: number[]; excludedKeywordIds: number[];
} }
export interface IUpdateModel
{
updateVersionString: string;
updateVersion: number;
updateDate: Date,
updateAvailable: boolean;
changeLogs: string;
downloads: IUpdateDonloads[];
}
export interface IUpdateDonloads
{
name: string;
url: string
}

View file

@ -146,6 +146,9 @@ export class LoginComponent implements OnDestroy, OnInit {
} }
public oauth() { public oauth() {
if (this.oAuthWindow) {
this.oAuthWindow.close();
}
this.oAuthWindow = window.open(window.location.toString(), "_blank", `toolbar=0, this.oAuthWindow = window.open(window.location.toString(), "_blank", `toolbar=0,
location=0, location=0,
status=0, status=0,
@ -159,16 +162,22 @@ export class LoginComponent implements OnDestroy, OnInit {
this.authService.login({ usePlexOAuth: true, password: "", rememberMe: true, username: "", plexTvPin: pin }).subscribe(x => { this.authService.login({ usePlexOAuth: true, password: "", rememberMe: true, username: "", plexTvPin: pin }).subscribe(x => {
this.oAuthWindow!.location.replace(x.url); this.oAuthWindow!.location.replace(x.url);
this.pinTimer = setInterval(() => { if (this.pinTimer) {
clearInterval(this.pinTimer);
}
this.oauthLoading = true; this.pinTimer = setInterval(() => {
this.getPinResult(x.pinId); if(this.oAuthWindow.closed) {
}, 4000); this.oauthLoading = true;
this.getPinResult(x.pinId);
}
}, 1000);
}); });
}); });
} }
public getPinResult(pinId: number) { public getPinResult(pinId: number) {
clearInterval(this.pinTimer);
this.authService.oAuth(pinId).subscribe(x => { this.authService.oAuth(pinId).subscribe(x => {
if(x.access_token) { if(x.access_token) {
this.store.save("id_token", x.access_token); this.store.save("id_token", x.access_token);
@ -176,7 +185,7 @@ export class LoginComponent implements OnDestroy, OnInit {
if (this.authService.loggedIn()) { if (this.authService.loggedIn()) {
this.ngOnDestroy(); this.ngOnDestroy();
if(this.oAuthWindow) { if (this.oAuthWindow) {
this.oAuthWindow.close(); this.oAuthWindow.close();
} }
this.oauthLoading = false; this.oauthLoading = false;
@ -184,6 +193,10 @@ export class LoginComponent implements OnDestroy, OnInit {
return; return;
} }
} }
this.notify.open("Could not log you in!", "OK", {
duration: 3000
});
this.oauthLoading = false;
}, err => { }, err => {
console.log(err); console.log(err);

View file

@ -24,6 +24,7 @@
[type]="requestType" [type]="requestType"
(openTrailer)="openDialog()" (openTrailer)="openDialog()"
(onAdvancedOptions)="openAdvancedOptions()" (onAdvancedOptions)="openAdvancedOptions()"
(onReProcessRequest)="reProcessRequest()"
> >
</social-icons> </social-icons>

View file

@ -184,12 +184,22 @@ export class MovieDetailsComponent {
if (result) { if (result) {
result.rootFolder = result.rootFolders.filter(f => f.id === +result.rootFolderId)[0]; result.rootFolder = result.rootFolders.filter(f => f.id === +result.rootFolderId)[0];
result.profile = result.profiles.filter(f => f.id === +result.profileId)[0]; result.profile = result.profiles.filter(f => f.id === +result.profileId)[0];
await this.requestService2.updateMovieAdvancedOptions({ qualityOverride: result.profileId, rootPathOverride: result.rootFolderId, requestId: this.movieRequest.id }).toPromise(); await this.requestService2.updateMovieAdvancedOptions({ qualityOverride: result.profileId, rootPathOverride: result.rootFolderId, languageProfile: 0, requestId: this.movieRequest.id }).toPromise();
this.setAdvancedOptions(result); this.setAdvancedOptions(result);
} }
}); });
} }
public reProcessRequest() {
this.requestService2.reprocessRequest(this.movieRequest.id, RequestType.movie).subscribe(result => {
if (result.result) {
this.messageService.send(result.message ? result.message : "Successfully Re-processed the request", "Ok");
} else {
this.messageService.send(result.errorMessage, "Ok");
}
});
}
private loadBanner() { private loadBanner() {
this.imageService.getMovieBanner(this.theMovidDbId.toString()).subscribe(x => { this.imageService.getMovieBanner(this.theMovidDbId.toString()).subscribe(x => {
if (!this.movie.backdropPath) { if (!this.movie.backdropPath) {

View file

@ -2,7 +2,7 @@
<div class="rating medium-font"> <div class="rating medium-font">
<span *ngIf="movie.voteAverage" <span *ngIf="movie.voteAverage"
matTooltip="{{'MediaDetails.Votes' | translate }} {{movie.voteCount | thousandShort: 1}}"> matTooltip="{{'MediaDetails.Votes' | translate }} {{movie.voteCount | thousandShort: 1}}">
<img class="rating-small" src="{{baseUrl}}images/tmdb-logo.svg"> {{movie.voteAverage | number:'1.0-1'}}/10 <img class="rating-small" src="{{baseUrl}}/images/tmdb-logo.svg"> {{movie.voteAverage | number:'1.0-1'}}/10
</span> </span>
<span *ngIf="ratings?.critics_rating && ratings?.critics_score"> <span *ngIf="ratings?.critics_rating && ratings?.critics_score">
<img class="rating-small" <img class="rating-small"

View file

@ -35,5 +35,9 @@
<span *ngIf="type === RequestType.movie"> {{ 'MediaDetails.RadarrConfiguration' | translate}}</span> <span *ngIf="type === RequestType.movie"> {{ 'MediaDetails.RadarrConfiguration' | translate}}</span>
<span *ngIf="type === RequestType.tvShow"> {{ 'MediaDetails.SonarrConfiguration' | translate}}</span> <span *ngIf="type === RequestType.tvShow"> {{ 'MediaDetails.SonarrConfiguration' | translate}}</span>
</button> </button>
<button *ngIf="type === RequestType.movie" mat-menu-item (click)="reProcessRequest()">
<i class="fas fa-sync icon-spacing"></i>
<span> {{ 'MediaDetails.ReProcessRequest' | translate}}</span>
</button>
</mat-menu> </mat-menu>
</div> </div>

View file

@ -26,6 +26,7 @@ export class SocialIconsComponent {
@Output() openTrailer: EventEmitter<any> = new EventEmitter(); @Output() openTrailer: EventEmitter<any> = new EventEmitter();
@Output() onAdvancedOptions: EventEmitter<any> = new EventEmitter(); @Output() onAdvancedOptions: EventEmitter<any> = new EventEmitter();
@Output() onReProcessRequest: EventEmitter<any> = new EventEmitter();
public RequestType = RequestType; public RequestType = RequestType;
@ -37,4 +38,8 @@ export class SocialIconsComponent {
public openAdvancedOptions() { public openAdvancedOptions() {
this.onAdvancedOptions.emit(); this.onAdvancedOptions.emit();
} }
public reProcessRequest() {
this.onReProcessRequest.emit();
}
} }

View file

@ -5,7 +5,7 @@
<i class="fas fa-x7 fa-exclamation-triangle glyphicon"></i> <i class="fas fa-x7 fa-exclamation-triangle glyphicon"></i>
<span>{{'MediaDetails.AutoApproveOptionsTvShort' | translate }}</span> <span>{{'MediaDetails.AutoApproveOptionsTvShort' | translate }}</span>
</div> </div>
<mat-form-field> <mat-form-field *ngIf="sonarrEnabled">
<mat-label>{{'MediaDetails.QualityProfilesSelect' | translate }}</mat-label> <mat-label>{{'MediaDetails.QualityProfilesSelect' | translate }}</mat-label>
<mat-select [(value)]="data.profileId"> <mat-select [(value)]="data.profileId">
<mat-option *ngFor="let profile of sonarrProfiles" value="{{profile.id}}">{{profile.name}}</mat-option> <mat-option *ngFor="let profile of sonarrProfiles" value="{{profile.id}}">{{profile.name}}</mat-option>
@ -13,13 +13,21 @@
</mat-form-field> </mat-form-field>
</div> </div>
<div mat-dialog-content> <div mat-dialog-content>
<mat-form-field> <mat-form-field *ngIf="sonarrEnabled">
<mat-label>{{'MediaDetails.RootFolderSelect' | translate }}</mat-label> <mat-label>{{'MediaDetails.RootFolderSelect' | translate }}</mat-label>
<mat-select [(value)]="data.rootFolderId"> <mat-select [(value)]="data.rootFolderId">
<mat-option *ngFor="let profile of sonarrRootFolders" value="{{profile.id}}">{{profile.path}}</mat-option> <mat-option *ngFor="let profile of sonarrRootFolders" value="{{profile.id}}">{{profile.path}}</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
<div mat-dialog-content>
<mat-form-field *ngIf="sonarrEnabled">
<mat-label>{{'MediaDetails.LanguageProfileSelect' | translate }}</mat-label>
<mat-select [(value)]="data.languageId">
<mat-option *ngFor="let profile of sonarrLanguageProfiles" value="{{profile.id}}">{{profile.name}}</mat-option>
</mat-select>
</mat-form-field>
</div>
<div mat-dialog-actions> <div mat-dialog-actions>
<button mat-raised-button [mat-dialog-close]="" color="warn"><i class="fas fa-times"></i> {{ 'Common.Cancel' | translate }}</button> <button mat-raised-button [mat-dialog-close]="" color="warn"><i class="fas fa-times"></i> {{ 'Common.Cancel' | translate }}</button>
<button mat-raised-button [mat-dialog-close]="data" color="accent" cdkFocusInitial><i class="fas fa-plus"></i> {{ 'Common.Submit' | translate }}</button> <button mat-raised-button [mat-dialog-close]="data" color="accent" cdkFocusInitial><i class="fas fa-plus"></i> {{ 'Common.Submit' | translate }}</button>

View file

@ -1,55 +1,92 @@
import { Component, Inject, OnInit } from "@angular/core"; import { Component, Inject, OnInit } from "@angular/core";
import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material/dialog"; import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material/dialog";
import { IAdvancedData, ISonarrProfile, ISonarrRootFolder } from "../../../../../interfaces"; import {
import { SonarrService } from "../../../../../services"; IAdvancedData,
ILanguageProfiles,
ISonarrProfile,
ISonarrRootFolder,
ISonarrSettings,
} from "../../../../../interfaces";
import { SettingsService, SonarrService } from "../../../../../services";
@Component({ @Component({
templateUrl: "./tv-advanced-options.component.html", templateUrl: "./tv-advanced-options.component.html",
selector: "tv-advanced-options", selector: "tv-advanced-options",
}) })
export class TvAdvancedOptionsComponent implements OnInit { export class TvAdvancedOptionsComponent implements OnInit {
public sonarrProfiles: ISonarrProfile[];
public sonarrRootFolders: ISonarrRootFolder[];
public sonarrLanguageProfiles: ILanguageProfiles[];
public sonarrEnabled: boolean;
public sonarrProfiles: ISonarrProfile[]; constructor(
public sonarrRootFolders: ISonarrRootFolder[]; public dialogRef: MatDialogRef<TvAdvancedOptionsComponent>,
@Inject(MAT_DIALOG_DATA) public data: IAdvancedData,
private sonarrService: SonarrService,
private settingsService: SettingsService
) {}
constructor(public dialogRef: MatDialogRef<TvAdvancedOptionsComponent>, @Inject(MAT_DIALOG_DATA) public data: IAdvancedData, public async ngOnInit() {
private sonarrService: SonarrService this.settingsService.getSonarr().subscribe((settings: ISonarrSettings) => {
) { if (!settings.enabled) {
this.sonarrEnabled = false;
return;
}
this.sonarrEnabled = true;
this.sonarrService.getQualityProfilesWithoutSettings().subscribe((c) => {
this.sonarrProfiles = c;
this.data.profiles = c;
this.setQualityOverrides();
});
this.sonarrService.getRootFoldersWithoutSettings().subscribe((c) => {
this.sonarrRootFolders = c;
this.data.rootFolders = c;
this.setRootFolderOverrides();
});
if (settings.v3) {
this.sonarrService
.getV3LanguageProfiles(settings)
.subscribe((profiles: ILanguageProfiles[]) => {
this.sonarrLanguageProfiles = profiles;
this.data.languages = profiles;
this.setLanguageOverride();
});
}
});
}
private setQualityOverrides(): void {
if (this.sonarrProfiles) {
const profile = this.sonarrProfiles.filter((p) => {
return p.id === this.data.tvRequest.qualityOverride;
});
if (profile.length > 0) {
this.data.tvRequest.qualityOverrideTitle = profile[0].name;
}
} }
}
private setRootFolderOverrides(): void {
public async ngOnInit() { if (this.sonarrRootFolders) {
this.sonarrService.getQualityProfilesWithoutSettings().subscribe(c => { const path = this.sonarrRootFolders.filter((folder) => {
this.sonarrProfiles = c; return folder.id === this.data.tvRequest.rootFolder;
this.data.profiles = c; });
this.setQualityOverrides(); if (path.length > 0) {
}); this.data.tvRequest.rootPathOverrideTitle = path[0].path;
this.sonarrService.getRootFoldersWithoutSettings().subscribe(c => { }
this.sonarrRootFolders = c;
this.data.rootFolders = c;
this.setRootFolderOverrides();
});
} }
}
private setQualityOverrides(): void { private setLanguageOverride(): void {
if (this.sonarrProfiles) { if (this.sonarrLanguageProfiles) {
const profile = this.sonarrProfiles.filter((p) => { const profile = this.sonarrLanguageProfiles.filter((p) => {
return p.id === this.data.tvRequest.qualityOverride; return p.id === this.data.tvRequest.languageProfile;
}); });
if (profile.length > 0) { if (profile.length > 0) {
this.data.movieRequest.qualityOverrideTitle = profile[0].name; this.data.tvRequest.languageOverrideTitle = profile[0].name;
} }
}
}
private setRootFolderOverrides(): void {
if (this.sonarrRootFolders) {
const path = this.sonarrRootFolders.filter((folder) => {
return folder.id === this.data.tvRequest.rootFolder;
});
if (path.length > 0) {
this.data.movieRequest.rootPathOverrideTitle = path[0].path;
}
}
} }
}
} }

View file

@ -67,6 +67,7 @@ export class TvRequestGridComponent {
viewModel.requestOnBehalf = result.username?.id; viewModel.requestOnBehalf = result.username?.id;
viewModel.qualityPathOverride = result?.sonarrPathId; viewModel.qualityPathOverride = result?.sonarrPathId;
viewModel.rootFolderOverride = result?.sonarrFolderId; viewModel.rootFolderOverride = result?.sonarrFolderId;
viewModel.languageProfile = result?.sonarrLanguageId;
const requestResult = await this.requestServiceV2.requestTv(viewModel).toPromise(); const requestResult = await this.requestServiceV2.requestTv(viewModel).toPromise();
this.postRequest(requestResult); this.postRequest(requestResult);

View file

@ -70,6 +70,7 @@
<button *ngIf="request.available" mat-raised-button color="warn" (click)="changeAvailability(request, false);">{{ 'Requests.MarkUnavailable' | translate }}</button> <button *ngIf="request.available" mat-raised-button color="warn" (click)="changeAvailability(request, false);">{{ 'Requests.MarkUnavailable' | translate }}</button>
<button *ngIf="!request.denied" mat-raised-button color="danger" (click)="deny(request);">{{ 'Requests.Deny' | translate }}</button> <button *ngIf="!request.denied" mat-raised-button color="danger" (click)="deny(request);">{{ 'Requests.Deny' | translate }}</button>
<button mat-raised-button color="danger" (click)="delete(request);">{{ 'Requests.RequestPanel.Delete' | translate }}</button> <button mat-raised-button color="danger" (click)="delete(request);">{{ 'Requests.RequestPanel.Delete' | translate }}</button>
<button mat-raised-button color="accent" (click)="reProcessRequest(request);">{{ 'MediaDetails.ReProcessRequest' | translate }}</button>
</div> </div>

View file

@ -4,6 +4,7 @@ import { RequestService } from "../../../../../services/request.service";
import { MessageService } from "../../../../../services"; import { MessageService } from "../../../../../services";
import { MatDialog } from "@angular/material/dialog"; import { MatDialog } from "@angular/material/dialog";
import { DenyDialogComponent } from "../../../shared/deny-dialog/deny-dialog.component"; import { DenyDialogComponent } from "../../../shared/deny-dialog/deny-dialog.component";
import { RequestServiceV2 } from "../../../../../services/requestV2.service";
@Component({ @Component({
templateUrl: "./tv-requests-panel.component.html", templateUrl: "./tv-requests-panel.component.html",
@ -16,7 +17,9 @@ export class TvRequestsPanelComponent {
public displayedColumns: string[] = ['number', 'title', 'airDate', 'status']; public displayedColumns: string[] = ['number', 'title', 'airDate', 'status'];
constructor(private requestService: RequestService, private messageService: MessageService, constructor(private requestService: RequestService,
private requestService2: RequestServiceV2,
private messageService: MessageService,
public dialog: MatDialog) { public dialog: MatDialog) {
} }
@ -93,4 +96,14 @@ export class TvRequestsPanelComponent {
}); });
}); });
} }
public reProcessRequest(request: IChildRequests) {
this.requestService2.reprocessRequest(request.id, RequestType.tvShow).subscribe(result => {
if (result.result) {
this.messageService.send(result.message ? result.message : "Successfully Re-processed the request", "Ok");
} else {
this.messageService.send(result.errorMessage, "Ok");
}
});
}
} }

View file

@ -102,7 +102,8 @@ export class TvDetailsComponent implements OnInit {
// get the name and ids // get the name and ids
result.rootFolder = result.rootFolders.filter(f => f.id === +result.rootFolderId)[0]; result.rootFolder = result.rootFolders.filter(f => f.id === +result.rootFolderId)[0];
result.profile = result.profiles.filter(f => f.id === +result.profileId)[0]; result.profile = result.profiles.filter(f => f.id === +result.profileId)[0];
await this.requestService2.updateTvAdvancedOptions({ qualityOverride: result.profileId, rootPathOverride: result.rootFolderId, requestId: this.showRequest.id }).toPromise(); result.language = result.languages.filter(x => x.id === +result.langaugeId)[0];
await this.requestService2.updateTvAdvancedOptions({ qualityOverride: result.profileId, rootPathOverride: result.rootFolderId, languageProfile: result.languageId, requestId: this.showRequest.id }).toPromise();
this.setAdvancedOptions(result); this.setAdvancedOptions(result);
} }
}); });
@ -117,15 +118,20 @@ export class TvDetailsComponent implements OnInit {
if (data.profileId) { if (data.profileId) {
this.showRequest.rootPathOverrideTitle = data.rootFolders.filter(x => x.id == data.rootFolderId)[0].path; this.showRequest.rootPathOverrideTitle = data.rootFolders.filter(x => x.id == data.rootFolderId)[0].path;
} }
if (data.languageId) {
this.showRequest.languageOverrideTitle = data.languages.filter(x => x.id == data.languageId)[0].name;
}
} }
private loadAdvancedInfo() { private loadAdvancedInfo() {
const profile = this.sonarrService.getQualityProfilesWithoutSettings(); const profile = this.sonarrService.getQualityProfilesWithoutSettings();
const folders = this.sonarrService.getRootFoldersWithoutSettings(); const folders = this.sonarrService.getRootFoldersWithoutSettings();
const languages = this.sonarrService.getV3LanguageProfilesWithoutSettings();
forkJoin([profile, folders]).subscribe(x => { forkJoin([profile, folders, languages]).subscribe(x => {
const sonarrProfiles = x[0]; const sonarrProfiles = x[0];
const sonarrRootFolders = x[1]; const sonarrRootFolders = x[1];
const languageProfiles = x[2];
const profile = sonarrProfiles.filter((p) => { const profile = sonarrProfiles.filter((p) => {
return p.id === this.showRequest.qualityOverride; return p.id === this.showRequest.qualityOverride;
@ -141,6 +147,13 @@ export class TvDetailsComponent implements OnInit {
this.showRequest.rootPathOverrideTitle = path[0].path; this.showRequest.rootPathOverrideTitle = path[0].path;
} }
const lang = languageProfiles.filter((folder) => {
return folder.id === this.showRequest.languageProfile;
});
if (lang.length > 0) {
this.showRequest.languageOverrideTitle = lang[0].name;
}
}); });
} }
} }

View file

@ -6,12 +6,12 @@
<mat-toolbar class="application-name" id="nav-applicationName">{{applicationName}}</mat-toolbar> <mat-toolbar class="application-name" id="nav-applicationName">{{applicationName}}</mat-toolbar>
<mat-nav-list> <mat-nav-list>
<span *ngFor="let nav of navItems"> <span mat-list-item *ngFor="let nav of navItems">
<div class="menu-spacing" *ngIf="(nav.requiresAdmin && isAdmin || !nav.requiresAdmin) && nav.enabled"> <div class="menu-spacing mat-ripple" mat-ripple *ngIf="(nav.requiresAdmin && isAdmin || !nav.requiresAdmin) && nav.enabled">
<a id="{{nav.id}}" *ngIf="nav.externalLink" mat-list-item [href]="nav.link" target="_blank" <a [disableRipple]="true" mat-list-item id="{{nav.id}}" *ngIf="nav.externalLink" [href]="nav.link" target="_blank"
matTooltip="{{nav.toolTipMessage | translate}}" matTooltipPosition="right" matTooltip="{{nav.toolTipMessage | translate}}" matTooltipPosition="right"
[routerLinkActive]="'active-list-item'"> [routerLinkActive]="'active-list-item'">
@ -19,7 +19,7 @@
style="padding-left: 5px; padding-right: 5px;" aria-hidden="true"></i> style="padding-left: 5px; padding-right: 5px;" aria-hidden="true"></i>
&nbsp;{{nav.name | translate}} &nbsp;{{nav.name | translate}}
</a> </a>
<a id="{{nav.id}}" *ngIf="!nav.externalLink" mat-list-item [routerLink]="nav.link" [style]="nav.color" <a [disableRipple]="true" mat-list-item id="{{nav.id}}" *ngIf="!nav.externalLink" [routerLink]="nav.link" [style]="nav.color"
[routerLinkActive]="'active-list-item'"> [routerLinkActive]="'active-list-item'">
<i class="fa-lg {{nav.icon}} icon-spacing"></i> <i class="fa-lg {{nav.icon}} icon-spacing"></i>
@ -28,7 +28,7 @@
</div> </div>
</span> </span>
<a class="menu-spacing" id="nav-logout" mat-list-item [routerLinkActive]="'active-list-item'" <a mat-list-item [disableRipple]="true" class="menu-spacing" id="nav-logout" [routerLinkActive]="'active-list-item'"
aria-label="Toggle sidenav" (click)="logOut();"> aria-label="Toggle sidenav" (click)="logOut();">
<i class="fa-lg fas fa-sign-out-alt icon-spacing"></i> <i class="fa-lg fas fa-sign-out-alt icon-spacing"></i>
&nbsp;{{ 'NavigationBar.Logout' | translate }} &nbsp;{{ 'NavigationBar.Logout' | translate }}

View file

@ -1,5 +1,5 @@
<form [formGroup]='searchForm'> <form [formGroup]='searchForm'>
<mat-form-field floatLabel="never" style="width: 100%;"> <mat-form-field floatLabel="never" style="width: 100%;">
<input id="nav-search" matInput placeholder="{{'NavigationBar.Search' | translate}}" formControlName='input'> <input id="nav-search" autofocus="autofocus" matInput placeholder="{{'NavigationBar.Search' | translate}}" formControlName='input'>
</mat-form-field> </mat-form-field>
</form> </form>

View file

@ -32,6 +32,10 @@ export class SonarrService extends ServiceHelpers {
return this.http.post<ILanguageProfiles[]>(`${this.url}/v3/languageprofiles/`, JSON.stringify(settings), {headers: this.headers}); return this.http.post<ILanguageProfiles[]>(`${this.url}/v3/languageprofiles/`, JSON.stringify(settings), {headers: this.headers});
} }
public getV3LanguageProfilesWithoutSettings(): Observable<ILanguageProfiles[]> {
return this.http.get<ILanguageProfiles[]>(`${this.url}/v3/languageprofiles/`, {headers: this.headers});
}
public isEnabled(): Promise<boolean> { public isEnabled(): Promise<boolean> {
return this.http.get<boolean>(`${this.url}/enabled/`, { headers: this.headers }).toPromise(); return this.http.get<boolean>(`${this.url}/enabled/`, { headers: this.headers }).toPromise();
} }

View file

@ -187,5 +187,4 @@ export class RequestService extends ServiceHelpers {
public removeAlbumRequest(request: number): any { public removeAlbumRequest(request: number): any {
return this.http.delete(`${this.url}music/${request}`, {headers: this.headers}); return this.http.delete(`${this.url}music/${request}`, {headers: this.headers});
} }
} }

View file

@ -4,7 +4,7 @@ import { Injectable, Inject } from "@angular/core";
import { HttpClient } from "@angular/common/http"; import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs"; import { Observable } from "rxjs";
import { ServiceHelpers } from "./service.helpers"; import { ServiceHelpers } from "./service.helpers";
import { IRequestsViewModel, IMovieRequests, IChildRequests, IMovieAdvancedOptions as IMediaAdvancedOptions, IRequestEngineResult, IAlbumRequest, ITvRequestViewModelV2 } from "../interfaces"; import { IRequestsViewModel, IMovieRequests, IChildRequests, IMovieAdvancedOptions as IMediaAdvancedOptions, IRequestEngineResult, IAlbumRequest, ITvRequestViewModelV2, RequestType } from "../interfaces";
@Injectable() @Injectable()
@ -92,4 +92,8 @@ export class RequestServiceV2 extends ServiceHelpers {
public requestTv(tv: ITvRequestViewModelV2): Observable<IRequestEngineResult> { public requestTv(tv: ITvRequestViewModelV2): Observable<IRequestEngineResult> {
return this.http.post<IRequestEngineResult>(`${this.url}TV/`, JSON.stringify(tv), {headers: this.headers}); return this.http.post<IRequestEngineResult>(`${this.url}TV/`, JSON.stringify(tv), {headers: this.headers});
} }
public reprocessRequest(requestId: number, type: RequestType): Observable<IRequestEngineResult> {
return this.http.post<IRequestEngineResult>(`${this.url}reprocess/${type}/${requestId}`, undefined, { headers: this.headers });
}
} }

View file

@ -0,0 +1,18 @@
import { PlatformLocation, APP_BASE_HREF } from "@angular/common";
import { Injectable, Inject } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs";
import { ServiceHelpers } from "./service.helpers";
import { IUpdateModel } from "../interfaces";
@Injectable()
export class UpdateService extends ServiceHelpers {
constructor(http: HttpClient, @Inject(APP_BASE_HREF) href:string) {
super(http, "/api/v1/Update/", href);
}
public checkForUpdate(): Observable<IUpdateModel> {
return this.http.get<IUpdateModel>(`${this.url}`, {headers: this.headers});
}
}

View file

@ -13,8 +13,10 @@
</div> </div>
<div class="mat-row"> <div class="mat-row">
<div class="mat-cell">Version</div> <div class="mat-cell">Version</div>
<div class="mat-cell">{{about.version}} <a [routerLink]="['/Settings/Update']" *ngIf="newUpdate" <div class="mat-cell">{{about.version}} &nbsp; <a (click)="openUpdate()" *ngIf="newUpdate"
style="color:#df691a"><b>(New Update Available)</b></a></div> style="color:#df691a; text-decoration: underline; cursor: pointer;"><b><i class="fas fa-code-branch"></i> (New Update Available)</b></a>
<span *ngIf="!newUpdate"> <i class="far fa-thumbs-up" matTooltip="Nice work bro! Latest version FTW!"></i></span>
</div>
</div> </div>
<!-- <div class="mat-row"> <!-- <div class="mat-row">

View file

@ -1,7 +1,10 @@
import { Component, OnInit } from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { IAbout } from "../../interfaces/ISettings"; import { IAbout, IUpdateModel } from "../../interfaces/ISettings";
import { JobService, SettingsService, HubService, SystemService } from "../../services"; import { SettingsService, HubService, SystemService } from "../../services";
import { IConnectedUser } from "../../interfaces"; import { IConnectedUser } from "../../interfaces";
import { UpdateService } from "../../services/update.service";
import { MatDialog } from "@angular/material/dialog";
import { UpdateDialogComponent } from "./update-dialog.component";
@Component({ @Component({
templateUrl: "./about.component.html", templateUrl: "./about.component.html",
@ -14,22 +17,29 @@ export class AboutComponent implements OnInit {
public connectedUsers: IConnectedUser[]; public connectedUsers: IConnectedUser[];
public newsHtml: string; public newsHtml: string;
private update: IUpdateModel;
constructor(private readonly settingsService: SettingsService, constructor(private readonly settingsService: SettingsService,
private readonly jobService: JobService, private readonly jobService: UpdateService,
private readonly hubService: HubService, private readonly hubService: HubService,
private readonly systemService: SystemService) { } private readonly systemService: SystemService,
private readonly dialog: MatDialog) { }
public async ngOnInit() { public async ngOnInit() {
this.settingsService.about().subscribe(x => this.about = x); this.settingsService.about().subscribe(x => this.about = x);
this.newsHtml = await this.systemService.getNews().toPromise(); this.newsHtml = await this.systemService.getNews().toPromise();
// TODO this.jobService.checkForUpdate().subscribe(x => {
// this.jobService.getCachedUpdate().subscribe(x => { this.update = x;
// if (x === true) { if (x.updateAvailable) {
// // this.newUpdate = true; // TODO this.newUpdate = true;
// } }
// }); });
this.connectedUsers = await this.hubService.getConnectedUsers(); this.connectedUsers = await this.hubService.getConnectedUsers();
} }
public openUpdate() {
this.dialog.open(UpdateDialogComponent, { width: "700px", data: this.update, panelClass: 'modal-panel' });
}
} }

View file

@ -0,0 +1,28 @@
<h1 mat-dialog-title><i class="fas fa-code-branch"></i> Latest Version: {{data.updateVersionString}}</h1>
<mat-dialog-content>
<div [innerHTML]="data.changeLogs">
</div>
<div class="mat-table">
<div class="mat-header-row">
<div class="mat-header-cell">Binary</div>
<div class="mat-header-cell">Download</div></div>
<div *ngFor="let d of data.downloads" class="mat-row" >
<div class="mat-cell">{{d.name}}</div>
<div class="mat-cell"><a href="{{d.url}}">Download</a></div>
</div>
</div>
<small>Updated at {{data.updateDate | date}}</small>
</mat-dialog-content>
<div mat-dialog-actions class="right-buttons">
<button mat-raised-button id="cancelButton" [mat-dialog-close]="" color="warn"><i class="fas fa-times"></i> Close</button>
</div>

View file

@ -0,0 +1,40 @@
.mat-table {
display: block;
}
.mat-row,
.mat-header-row {
display: flex;
border-bottom-width: 1px;
border-bottom-style: solid;
align-items: center;
min-height: 48px;
padding: 0 24px;
}
.mat-cell,
.mat-header-cell {
flex: 1;
overflow: hidden;
word-wrap: break-word;
}
.small-middle-container{
margin: auto;
width: 85%;
margin-top:10px;
}
:host ::ng-deep strong {
color: #fff;
background-color: #007bff;
display: inline-block;
padding: 0.25em 0.4em;
font-size: 75%;
font-weight: 700;
line-height: 1;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: 0.25rem;
}

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