mirror of
https://github.com/Ombi-app/Ombi.git
synced 2025-07-16 02:02:55 -07:00
commit
dde517cecc
129 changed files with 4120 additions and 842 deletions
|
@ -27,4 +27,4 @@ variables:
|
|||
value: "4.0.$(Build.BuildId)"
|
||||
|
||||
- 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
1
.gitignore
vendored
|
@ -247,5 +247,6 @@ _Pvt_Extensions
|
|||
# Ignore local vscode config
|
||||
*.vscode
|
||||
/src/Ombi/database.json
|
||||
/src/Ombi/databases.json
|
||||
/src/Ombi/healthchecksdb
|
||||
/src/Ombi/ClientApp/package-lock.json
|
||||
|
|
|
@ -1,39 +1,122 @@
|
|||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Mime;
|
||||
|
||||
namespace Ombi.Api.Radarr.Models
|
||||
{
|
||||
{
|
||||
public class MovieResponse
|
||||
{
|
||||
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 double sizeOnDisk { get; set; }
|
||||
public long sizeOnDisk { get; set; }
|
||||
public string status { get; set; }
|
||||
public string overview { get; set; }
|
||||
public string inCinemas { get; set; }
|
||||
public string physicalRelease { get; set; }
|
||||
public List<Image> images { get; set; }
|
||||
public DateTime inCinemas { get; set; }
|
||||
public DateTime physicalRelease { get; set; }
|
||||
public DateTime digitalRelease { get; set; }
|
||||
public Image[] images { get; set; }
|
||||
public string website { get; set; }
|
||||
public bool downloaded { get; set; }
|
||||
public int year { get; set; }
|
||||
public bool hasFile { get; set; }
|
||||
public string youTubeTrailerId { get; set; }
|
||||
public string studio { get; set; }
|
||||
public string path { get; set; }
|
||||
public int profileId { get; set; }
|
||||
public string minimumAvailability { get; set; }
|
||||
public int qualityProfileId { 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 string lastInfoSync { get; set; }
|
||||
public string cleanTitle { get; set; }
|
||||
public string imdbId { get; set; }
|
||||
public int tmdbId { get; set; }
|
||||
public string titleSlug { get; set; }
|
||||
public List<string> genres { get; set; }
|
||||
public List<object> tags { get; set; }
|
||||
public string added { get; set; }
|
||||
public string certification { get; set; }
|
||||
public string[] genres { get; set; }
|
||||
public object[] tags { get; set; }
|
||||
public DateTime added { get; set; }
|
||||
public Ratings ratings { get; set; }
|
||||
//public List<string> alternativeTitles { get; set; }
|
||||
public int qualityProfileId { get; set; }
|
||||
public Moviefile movieFile { get; set; }
|
||||
public Collection collection { 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; }
|
||||
}
|
||||
}
|
|
@ -28,5 +28,6 @@ namespace Ombi.Api.Radarr.Models
|
|||
public string titleSlug { get; set; }
|
||||
public int year { get; set; }
|
||||
public string minimumAvailability { get; set; }
|
||||
public long sizeOnDisk { get; set; }
|
||||
}
|
||||
}
|
|
@ -82,7 +82,8 @@ namespace Ombi.Api.Radarr
|
|||
titleSlug = title + year,
|
||||
monitored = true,
|
||||
year = year,
|
||||
minimumAvailability = minimumAvailability
|
||||
minimumAvailability = minimumAvailability,
|
||||
sizeOnDisk = 0
|
||||
};
|
||||
|
||||
if (searchNow)
|
||||
|
|
|
@ -65,7 +65,7 @@ namespace Ombi.Api.Radarr
|
|||
|
||||
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);
|
||||
request.AddJsonBody(movie);
|
||||
|
||||
|
@ -85,7 +85,8 @@ namespace Ombi.Api.Radarr
|
|||
titleSlug = title + year,
|
||||
monitored = true,
|
||||
year = year,
|
||||
minimumAvailability = minimumAvailability
|
||||
minimumAvailability = minimumAvailability,
|
||||
sizeOnDisk = 0
|
||||
};
|
||||
|
||||
if (searchNow)
|
||||
|
|
|
@ -19,7 +19,7 @@ namespace Ombi.Api.Webhook
|
|||
|
||||
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))
|
||||
{
|
||||
|
|
|
@ -16,14 +16,14 @@ namespace Ombi.Api
|
|||
{
|
||||
public class Api : IApi
|
||||
{
|
||||
public Api(ILogger<Api> log, IOmbiHttpClient client)
|
||||
public Api(ILogger<Api> log, HttpClient client)
|
||||
{
|
||||
Logger = log;
|
||||
_client = client;
|
||||
}
|
||||
|
||||
private ILogger<Api> Logger { get; }
|
||||
private readonly IOmbiHttpClient _client;
|
||||
private readonly HttpClient _client;
|
||||
|
||||
public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<PackageReference Include="Polly" Version="7.1.0" />
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -30,7 +30,7 @@ namespace Ombi.Core.Tests.Rule.Search
|
|||
public async Task Should_Not_Be_Monitored_Or_Available()
|
||||
{
|
||||
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.False(request.Approved);
|
||||
|
@ -49,7 +49,7 @@ namespace Ombi.Core.Tests.Rule.Search
|
|||
}
|
||||
}.AsQueryable());
|
||||
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.False(request.Approved);
|
||||
|
@ -71,7 +71,7 @@ namespace Ombi.Core.Tests.Rule.Search
|
|||
}
|
||||
}.AsQueryable());
|
||||
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.False(request.Approved);
|
||||
|
@ -93,7 +93,7 @@ namespace Ombi.Core.Tests.Rule.Search
|
|||
}
|
||||
}.AsQueryable());
|
||||
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.False(request.Approved);
|
||||
|
@ -114,7 +114,7 @@ namespace Ombi.Core.Tests.Rule.Search
|
|||
}
|
||||
}.AsQueryable());
|
||||
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.False(request.Approved);
|
||||
|
|
|
@ -29,7 +29,7 @@ namespace Ombi.Core.Tests.Rule.Search
|
|||
public async Task Should_Not_Be_Monitored()
|
||||
{
|
||||
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.False(request.Monitored);
|
||||
|
@ -46,7 +46,7 @@ namespace Ombi.Core.Tests.Rule.Search
|
|||
}
|
||||
}.AsQueryable());
|
||||
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(request.Monitored);
|
||||
|
@ -64,7 +64,7 @@ namespace Ombi.Core.Tests.Rule.Search
|
|||
}
|
||||
}.AsQueryable());
|
||||
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(request.Monitored);
|
||||
|
|
|
@ -9,6 +9,7 @@ using Ombi.Store.Entities.Requests;
|
|||
using Ombi.Store.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Ombi.Core.Authentication;
|
||||
using Ombi.Helpers;
|
||||
|
||||
namespace Ombi.Core.Engine.Interfaces
|
||||
{
|
||||
|
@ -29,6 +30,10 @@ namespace Ombi.Core.Engine.Interfaces
|
|||
private OmbiUser _user;
|
||||
protected async Task<OmbiUser> GetUser()
|
||||
{
|
||||
if(!Username.HasValue())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var username = Username.ToUpper();
|
||||
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);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ namespace Ombi.Core.Engine.Interfaces
|
|||
Task<IEnumerable<SearchMovieViewModel>> NowPlayingMovies(int currentPosition, int amountToLoad);
|
||||
Task<MovieCollectionsViewModel> GetCollection(int collectionId, CancellationToken cancellationToken, string langCode = null);
|
||||
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>> UpcomingMovies(int currentlyLoaded, int toLoad);
|
||||
Task<ActorCredits> GetMoviesByActor(int actorId, string langCode);
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Ombi.Core.Models;
|
||||
using Ombi.Core.Models.Requests;
|
||||
|
@ -24,5 +25,6 @@ namespace Ombi.Core.Engine.Interfaces
|
|||
Task UnSubscribeRequest(int requestId, RequestType type);
|
||||
Task SubscribeToRequest(int requestId, RequestType type);
|
||||
Task<RequestQuotaCountModel> GetRemainingRequests(OmbiUser user = null);
|
||||
Task<RequestEngineResult> ReProcessRequest(int requestId, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
|
@ -11,7 +11,7 @@ namespace Ombi.Core
|
|||
Task<SearchFullInfoTvShowViewModel> GetShowInformation(string tvdbid, CancellationToken token);
|
||||
Task<SearchFullInfoTvShowViewModel> GetShowByRequest(int requestId, CancellationToken token);
|
||||
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>> Trending(int currentlyLoaded, int amountToLoad);
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ using Ombi.Settings.Settings.Models;
|
|||
using Ombi.Store.Entities.Requests;
|
||||
using Ombi.Store.Repository;
|
||||
using Ombi.Core.Models;
|
||||
using System.Threading;
|
||||
|
||||
namespace Ombi.Core.Engine
|
||||
{
|
||||
|
@ -70,7 +71,7 @@ namespace Ombi.Core.Engine
|
|||
var canRequestOnBehalf = model.RequestOnBehalf.HasValue();
|
||||
|
||||
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
|
||||
{
|
||||
|
@ -549,12 +550,17 @@ namespace Ombi.Core.Engine
|
|||
request.Denied = false;
|
||||
await MovieRepository.Update(request);
|
||||
|
||||
var canNotify = await RunSpecificRule(request, SpecificRules.CanSendNotification);
|
||||
var canNotify = await RunSpecificRule(request, SpecificRules.CanSendNotification, string.Empty);
|
||||
if (canNotify.Success)
|
||||
{
|
||||
await NotificationHelper.Notify(request, NotificationType.RequestApproved);
|
||||
}
|
||||
|
||||
return await ProcessSendingMovie(request);
|
||||
}
|
||||
|
||||
private async Task<RequestEngineResult> ProcessSendingMovie(MovieRequests request)
|
||||
{
|
||||
if (request.Approved)
|
||||
{
|
||||
var result = await Sender.Send(request);
|
||||
|
@ -634,6 +640,21 @@ namespace Ombi.Core.Engine
|
|||
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)
|
||||
{
|
||||
var request = await MovieRepository.Find(modelId);
|
||||
|
@ -682,7 +703,7 @@ namespace Ombi.Core.Engine
|
|||
{
|
||||
await MovieRepository.Add(model);
|
||||
|
||||
var result = await RunSpecificRule(model, SpecificRules.CanSendNotification);
|
||||
var result = await RunSpecificRule(model, SpecificRules.CanSendNotification, requestOnBehalf);
|
||||
if (result.Success)
|
||||
{
|
||||
await NotificationHelper.NewRequest(model);
|
||||
|
|
|
@ -362,7 +362,7 @@ namespace Ombi.Core.Engine
|
|||
await MusicRepository.Update(request);
|
||||
|
||||
|
||||
var canNotify = await RunSpecificRule(request, SpecificRules.CanSendNotification);
|
||||
var canNotify = await RunSpecificRule(request, SpecificRules.CanSendNotification, string.Empty);
|
||||
if (canNotify.Success)
|
||||
{
|
||||
await NotificationHelper.Notify(request, NotificationType.RequestApproved);
|
||||
|
@ -506,7 +506,7 @@ namespace Ombi.Core.Engine
|
|||
{
|
||||
await MusicRepository.Add(model);
|
||||
|
||||
var result = await RunSpecificRule(model, SpecificRules.CanSendNotification);
|
||||
var result = await RunSpecificRule(model, SpecificRules.CanSendNotification, string.Empty);
|
||||
if (result.Success)
|
||||
{
|
||||
await NotificationHelper.NewRequest(model);
|
||||
|
|
|
@ -151,7 +151,7 @@ namespace Ombi.Core.Engine
|
|||
}
|
||||
|
||||
|
||||
await Rules.StartSpecificRules(vm, SpecificRules.LidarrArtist);
|
||||
await Rules.StartSpecificRules(vm, SpecificRules.LidarrArtist, string.Empty);
|
||||
|
||||
return vm;
|
||||
}
|
||||
|
@ -190,7 +190,7 @@ namespace Ombi.Core.Engine
|
|||
|
||||
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);
|
||||
|
||||
|
@ -230,7 +230,7 @@ namespace Ombi.Core.Engine
|
|||
vm.Cover = a.remoteCover;
|
||||
}
|
||||
|
||||
await Rules.StartSpecificRules(vm, SpecificRules.LidarrAlbum);
|
||||
await Rules.StartSpecificRules(vm, SpecificRules.LidarrAlbum, string.Empty);
|
||||
|
||||
await RunSearchRules(vm);
|
||||
|
||||
|
@ -258,7 +258,7 @@ namespace Ombi.Core.Engine
|
|||
vm.Cover = fullAlbum.remoteCover;
|
||||
}
|
||||
|
||||
await Rules.StartSpecificRules(vm, SpecificRules.LidarrAlbum);
|
||||
await Rules.StartSpecificRules(vm, SpecificRules.LidarrAlbum, string.Empty);
|
||||
|
||||
await RunSearchRules(vm);
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ using Ombi.Settings.Settings.Models;
|
|||
using Ombi.Store.Entities.Requests;
|
||||
using Ombi.Store.Repository;
|
||||
using Ombi.Core.Models;
|
||||
using System.Threading;
|
||||
|
||||
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
|
||||
{
|
||||
|
@ -250,7 +251,7 @@ namespace Ombi.Core.Engine
|
|||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
{
|
||||
var sendRuleResult = await RunSpecificRule(model, SpecificRules.CanSendNotification);
|
||||
var sendRuleResult = await RunSpecificRule(model, SpecificRules.CanSendNotification, requestOnBehalf);
|
||||
if (sendRuleResult.Success)
|
||||
{
|
||||
await NotificationHelper.NewRequest(model);
|
||||
|
@ -913,6 +930,11 @@ namespace Ombi.Core.Engine
|
|||
EpisodeCount = model.SeasonRequests.Select(m => m.Episodes.Count).Sum(),
|
||||
});
|
||||
|
||||
return await ProcessSendingShow(model);
|
||||
}
|
||||
|
||||
private async Task<RequestEngineResult> ProcessSendingShow(ChildRequests model)
|
||||
{
|
||||
if (model.Approved)
|
||||
{
|
||||
// Autosend
|
||||
|
@ -997,6 +1019,10 @@ namespace Ombi.Core.Engine
|
|||
|
||||
request.QualityOverride = options.QualityOverride;
|
||||
request.RootFolder = options.RootPathOverride;
|
||||
if (options.LanguageProfile > 0)
|
||||
{
|
||||
request.LanguageProfile = options.LanguageProfile;
|
||||
}
|
||||
|
||||
await TvRepository.Update(request);
|
||||
|
||||
|
|
|
@ -124,9 +124,9 @@ namespace Ombi.Core.Engine.V2
|
|||
/// Gets popular movies by paging
|
||||
/// </summary>
|
||||
/// <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);
|
||||
|
||||
|
@ -371,6 +371,7 @@ namespace Ombi.Core.Engine.V2
|
|||
mapped.Requested = movie.Requested;
|
||||
mapped.PlexUrl = movie.PlexUrl;
|
||||
mapped.EmbyUrl = movie.EmbyUrl;
|
||||
mapped.JellyfinUrl = movie.JellyfinUrl;
|
||||
mapped.Subscribed = movie.Subscribed;
|
||||
mapped.ShowSubscribe = movie.ShowSubscribe;
|
||||
mapped.ReleaseDate = movie.ReleaseDate;
|
||||
|
|
|
@ -22,6 +22,7 @@ using Microsoft.EntityFrameworkCore;
|
|||
using System.Threading;
|
||||
using Ombi.Api.TheMovieDb;
|
||||
using Ombi.Api.TheMovieDb.Models;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Ombi.Core.Engine.V2
|
||||
{
|
||||
|
@ -49,13 +50,14 @@ namespace Ombi.Core.Engine.V2
|
|||
public async Task<SearchFullInfoTvShowViewModel> GetShowByRequest(int requestId, CancellationToken token)
|
||||
{
|
||||
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)
|
||||
{
|
||||
var show = await Cache.GetOrAdd(nameof(GetShowInformation) + tvdbid,
|
||||
async () => await _movieApi.GetTVInfo(tvdbid), DateTime.Now.AddHours(12));
|
||||
var langCode = await DefaultLanguageCode(null);
|
||||
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)
|
||||
{
|
||||
// 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));
|
||||
|
||||
foreach (var episode in seasonEpisodes.episodes)
|
||||
{
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
MapSeasons(mapped.SeasonRequests, tvSeason, seasonEpisodes);
|
||||
}
|
||||
|
||||
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 results = new List<MovieDbSearchResult>();
|
||||
|
@ -152,6 +122,7 @@ namespace Ombi.Core.Engine.V2
|
|||
async () => await _movieApi.TopRatedTv(langCode, pagesToLoad.Page), DateTime.Now.AddHours(12));
|
||||
results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take));
|
||||
}
|
||||
|
||||
var processed = ProcessResults(results);
|
||||
return await processed;
|
||||
}
|
||||
|
@ -177,22 +148,77 @@ namespace Ombi.Core.Engine.V2
|
|||
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 settings = await _customization.GetSettingsAsync();
|
||||
|
||||
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);
|
||||
if (result == null || settings.HideAvailableFromDiscover && result.Available)
|
||||
if (result == null || settings.HideAvailableFromDiscover && result.FullyAvailable)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
retVal.Add(result);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
var item = _mapper.Map<SearchTvShowViewModel>(tvMazeSearch);
|
||||
|
@ -216,6 +242,9 @@ namespace Ombi.Core.Engine.V2
|
|||
item.Approved = oldModel.Approved;
|
||||
item.SeasonRequests = oldModel.SeasonRequests;
|
||||
item.RequestId = oldModel.RequestId;
|
||||
item.PlexUrl = oldModel.PlexUrl;
|
||||
item.EmbyUrl = oldModel.EmbyUrl;
|
||||
item.JellyfinUrl = oldModel.JellyfinUrl;
|
||||
|
||||
if (!string.IsNullOrEmpty(item.Images?.Medium))
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
NewRequest = new TvRequests
|
||||
|
@ -234,7 +234,8 @@ namespace Ombi.Core.Helpers
|
|||
TotalSeasons = tv.Seasons.Count(),
|
||||
Background = BackdropPath,
|
||||
RootFolder = rootPathOverride,
|
||||
QualityOverride = qualityOverride
|
||||
QualityOverride = qualityOverride,
|
||||
LanguageProfile = langProfile
|
||||
};
|
||||
NewRequest.ChildRequests.Add(ChildRequest);
|
||||
|
||||
|
|
|
@ -5,5 +5,6 @@
|
|||
public int RequestId { get; set; }
|
||||
public int RootPathOverride { get; set; }
|
||||
public int QualityOverride { get; set; }
|
||||
public int LanguageProfile { get; set; }
|
||||
}
|
||||
}
|
|
@ -24,6 +24,7 @@ namespace Ombi.Core.Models.Requests
|
|||
{
|
||||
public bool RequestAll { get; set; }
|
||||
public bool LatestSeason { get; set; }
|
||||
public int? LanguageProfile { get; set; }
|
||||
public bool FirstSeason { get; set; }
|
||||
public List<SeasonsViewModel> Seasons { get; set; } = new List<SeasonsViewModel>();
|
||||
[JsonIgnore]
|
||||
|
|
|
@ -58,9 +58,6 @@ namespace Ombi.Core.Models.Search
|
|||
public bool PartlyAvailable { get; set; }
|
||||
public override RequestType Type => RequestType.TvShow;
|
||||
|
||||
/// <summary>
|
||||
/// Only set on the images call
|
||||
/// </summary>
|
||||
public string BackdropPath { get; set; }
|
||||
}
|
||||
}
|
|
@ -9,6 +9,6 @@ namespace Ombi.Core.Rule.Interfaces
|
|||
{
|
||||
Task<IEnumerable<RuleResult>> StartRequestRules(BaseRequest obj);
|
||||
Task<IEnumerable<RuleResult>> StartSearchRules(SearchViewModel obj);
|
||||
Task<RuleResult> StartSpecificRules(object obj, SpecificRules selectedRule);
|
||||
Task<RuleResult> StartSpecificRules(object obj, SpecificRules selectedRule, string requestOnBehalf);
|
||||
}
|
||||
}
|
|
@ -5,7 +5,7 @@ namespace Ombi.Core.Rule.Interfaces
|
|||
{
|
||||
public interface ISpecificRule<T> where T : new()
|
||||
{
|
||||
Task<RuleResult> Execute(T obj);
|
||||
Task<RuleResult> Execute(T obj, string requestOnBehalf);
|
||||
SpecificRules Rule { get; }
|
||||
}
|
||||
}
|
|
@ -58,13 +58,13 @@ namespace Ombi.Core.Rule
|
|||
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)
|
||||
{
|
||||
if (selectedRule == rule.Rule)
|
||||
{
|
||||
var result = await rule.Execute(obj);
|
||||
var result = await rule.Execute(obj, requestOnBehalf);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,12 +13,12 @@ namespace Ombi.Core.Rule.Rules.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 (season.Episodes.All(x => x.Available))
|
||||
{
|
||||
season.SeasonAvailable = true;
|
||||
season.SeasonAvailable = true;
|
||||
}
|
||||
}
|
||||
if (search.SeasonRequests.All(x => x.Episodes.All(e => e.Available)))
|
||||
|
|
|
@ -67,7 +67,7 @@ namespace Ombi.Core.Rule.Rules.Search
|
|||
var s = await EmbySettings.GetSettingsAsync();
|
||||
if (s.Enable)
|
||||
{
|
||||
var server = s.Servers.FirstOrDefault(x => x.ServerHostname != null);
|
||||
var server = s.Servers.FirstOrDefault();
|
||||
if ((server?.ServerHostname ?? string.Empty).HasValue())
|
||||
{
|
||||
obj.EmbyUrl = EmbyHelper.GetEmbyMediaUrl(item.EmbyId, server?.ServerId, server?.ServerHostname);
|
||||
|
|
|
@ -9,7 +9,7 @@ using Ombi.Store.Repository;
|
|||
|
||||
namespace Ombi.Core.Rule.Rules.Search
|
||||
{
|
||||
public class LidarrAlbumCacheRule : BaseSearchRule, IRules<SearchViewModel>
|
||||
public class LidarrAlbumCacheRule : SpecificRule, ISpecificRule<object>
|
||||
{
|
||||
public LidarrAlbumCacheRule(IExternalRepository<LidarrAlbumCache> db)
|
||||
{
|
||||
|
@ -18,7 +18,9 @@ namespace Ombi.Core.Rule.Rules.Search
|
|||
|
||||
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)
|
||||
{
|
||||
|
|
|
@ -17,7 +17,7 @@ namespace Ombi.Core.Rule.Rules.Search
|
|||
|
||||
private readonly IExternalRepository<LidarrArtistCache> _db;
|
||||
|
||||
public Task<RuleResult> Execute(object objec)
|
||||
public Task<RuleResult> Execute(object objec, string requestOnBehalf)
|
||||
{
|
||||
var obj = (SearchArtistViewModel) objec;
|
||||
// Check if it's in Lidarr
|
||||
|
@ -30,6 +30,7 @@ namespace Ombi.Core.Rule.Rules.Search
|
|||
return Task.FromResult(Success());
|
||||
}
|
||||
|
||||
|
||||
public override SpecificRules Rule => SpecificRules.LidarrArtist;
|
||||
}
|
||||
}
|
|
@ -25,10 +25,11 @@ namespace Ombi.Core.Rule.Rules.Search
|
|||
PlexServerContent item = null;
|
||||
var useImdb = false;
|
||||
var useTheMovieDb = false;
|
||||
var useId = false;
|
||||
var useTvDb = false;
|
||||
if (obj.ImdbId.HasValue())
|
||||
{
|
||||
item = await PlexContentRepository.Get(obj.ImdbId);
|
||||
item = await PlexContentRepository.Get(obj.ImdbId, ProviderType.ImdbId);
|
||||
if (item != null)
|
||||
{
|
||||
useImdb = true;
|
||||
|
@ -36,9 +37,17 @@ namespace Ombi.Core.Rule.Rules.Search
|
|||
}
|
||||
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())
|
||||
{
|
||||
item = await PlexContentRepository.Get(obj.TheMovieDbId);
|
||||
item = await PlexContentRepository.Get(obj.TheMovieDbId, ProviderType.TheMovieDbId);
|
||||
if (item != null)
|
||||
{
|
||||
useTheMovieDb = true;
|
||||
|
@ -49,7 +58,7 @@ namespace Ombi.Core.Rule.Rules.Search
|
|||
{
|
||||
if (obj.TheTvDbId.HasValue())
|
||||
{
|
||||
item = await PlexContentRepository.Get(obj.TheTvDbId);
|
||||
item = await PlexContentRepository.Get(obj.TheTvDbId, ProviderType.TvDbId);
|
||||
if (item != null)
|
||||
{
|
||||
useTvDb = true;
|
||||
|
@ -60,6 +69,11 @@ namespace Ombi.Core.Rule.Rules.Search
|
|||
|
||||
if (item != null)
|
||||
{
|
||||
if (useId)
|
||||
{
|
||||
obj.TheMovieDbId = obj.Id.ToString();
|
||||
useTheMovieDb = true;
|
||||
}
|
||||
obj.Available = true;
|
||||
obj.PlexUrl = item.Url;
|
||||
obj.Quality = item.Quality;
|
||||
|
@ -71,9 +85,9 @@ namespace Ombi.Core.Rule.Rules.Search
|
|||
if (search.SeasonRequests.Any())
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -22,11 +22,20 @@ namespace Ombi.Core.Rule.Rules.Specific
|
|||
private OmbiUserManager UserManager { 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 canRequestonBehalf = requestOnBehalf.HasValue();
|
||||
var settings = await Settings.GetSettingsAsync();
|
||||
var sendNotification = true;
|
||||
|
||||
if (settings.DoNotSendNotificationsForAutoApprove && canRequestonBehalf)
|
||||
{
|
||||
return new RuleResult
|
||||
{
|
||||
Success = false
|
||||
};
|
||||
}
|
||||
var requestedUser = await UserManager.Users.FirstOrDefaultAsync(x => x.Id == req.RequestedUserId);
|
||||
if (req.RequestType == RequestType.Movie)
|
||||
{
|
||||
|
|
|
@ -158,6 +158,8 @@ namespace Ombi.Core.Senders
|
|||
}
|
||||
|
||||
int qualityToUse;
|
||||
var sonarrV3 = s.V3;
|
||||
var languageProfileId = s.LanguageProfile;
|
||||
string rootFolderPath;
|
||||
string seriesType;
|
||||
|
||||
|
@ -167,8 +169,17 @@ namespace Ombi.Core.Senders
|
|||
{
|
||||
// Get the root path from the rootfolder selected.
|
||||
// For some reason, if we haven't got one use the first root folder in Sonarr
|
||||
rootFolderPath = await GetSonarrRootPath(int.Parse(s.RootPathAnime), s);
|
||||
int.TryParse(s.QualityProfileAnime, out qualityToUse);
|
||||
if (!int.TryParse(s.RootPathAnime, out int animePath))
|
||||
{
|
||||
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.SonarrRootPathAnime > 0)
|
||||
|
@ -181,7 +192,6 @@ namespace Ombi.Core.Senders
|
|||
}
|
||||
}
|
||||
seriesType = "anime";
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -220,11 +230,16 @@ namespace Ombi.Core.Senders
|
|||
rootFolderPath = await GetSonarrRootPath(rootfolderOverride, s);
|
||||
}
|
||||
}
|
||||
|
||||
// Are we using v3 sonarr?
|
||||
var sonarrV3 = s.V3;
|
||||
var languageProfileId = s.LanguageProfile;
|
||||
|
||||
if (model.ParentRequest.LanguageProfile.HasValue)
|
||||
{
|
||||
var languageProfile = model.ParentRequest.LanguageProfile.Value;
|
||||
if (languageProfile > 0)
|
||||
{
|
||||
languageProfileId = languageProfile;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Does the series actually exist?
|
||||
|
@ -264,6 +279,10 @@ namespace Ombi.Core.Senders
|
|||
var seasonsToAdd = GetSeasonsToCreate(model);
|
||||
newSeries.seasons = seasonsToAdd;
|
||||
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);
|
||||
await SendToSonarr(model, existingSeries, s);
|
||||
}
|
||||
|
@ -407,7 +426,6 @@ namespace Ombi.Core.Senders
|
|||
await SonarrApi.SeasonPass(s.ApiKey, s.FullUri, result);
|
||||
}
|
||||
|
||||
|
||||
if (!s.AddOnly)
|
||||
{
|
||||
await SearchForRequest(model, sonarrEpList, result, s, episodesToUpdate);
|
||||
|
|
|
@ -23,7 +23,6 @@ using Ombi.Notifications;
|
|||
using Ombi.Schedule;
|
||||
using Ombi.Schedule.Jobs;
|
||||
using Ombi.Settings.Settings;
|
||||
using Ombi.Store.Context;
|
||||
using Ombi.Store.Repository;
|
||||
using Ombi.Notifications.Agents;
|
||||
using Ombi.Schedule.Jobs.Radarr;
|
||||
|
@ -68,6 +67,8 @@ using Ombi.Api.MusicBrainz;
|
|||
using Ombi.Api.Twilio;
|
||||
using Ombi.Api.CloudService;
|
||||
using Ombi.Api.RottenTomatoes;
|
||||
using System.Net.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Ombi.DependencyInjection
|
||||
{
|
||||
|
@ -119,14 +120,24 @@ namespace Ombi.DependencyInjection
|
|||
|
||||
public static void RegisterHttp(this IServiceCollection services)
|
||||
{
|
||||
var runtimeVersion = AssemblyHelper.GetRuntimeVersion();
|
||||
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
||||
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)
|
||||
{
|
||||
services.AddScoped<IApi, Api.Api>();
|
||||
services.AddScoped<IOmbiHttpClient, OmbiHttpClient>(); // https://blogs.msdn.microsoft.com/alazarev/2017/12/29/disposable-finalizers-and-httpclient/
|
||||
services.AddScoped<IApi, Api.Api>(s => new Api.Api(s.GetRequiredService<ILogger<Api.Api>>(), s.GetRequiredService<IHttpClientFactory>().CreateClient("OmbiClient")));
|
||||
services.AddTransient<IMovieDbApi, Api.TheMovieDb.TheMovieDbApi>();
|
||||
services.AddTransient<IPlexApi, PlexApi>();
|
||||
services.AddTransient<IEmbyApi, EmbyApi>();
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
@ -61,6 +61,7 @@ namespace Ombi.Helpers.Tests
|
|||
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|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", 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");
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using Microsoft.Extensions.PlatformAbstractions;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Ombi.Helpers
|
||||
|
@ -8,7 +9,8 @@ namespace Ombi.Helpers
|
|||
public static string GetRuntimeVersion()
|
||||
{
|
||||
ApplicationEnvironment app = PlatformServices.Default.Application;
|
||||
return app.ApplicationVersion;
|
||||
var split = app.ApplicationVersion.Split('.');
|
||||
return string.Join('.', split.Take(3));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -81,7 +81,9 @@ namespace Ombi.Mapping.Profiles
|
|||
.ForMember(dest => dest.Rating, opts => opts.MapFrom(src => src.VoteAverage.ToString()))
|
||||
.ForMember(dest => dest.BackdropPath, opts => opts.MapFrom(src => src.PosterPath))
|
||||
//.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.Trailer,
|
||||
// opts => opts.MapFrom(src => src.Trailer != null ? src.Trailer.ToString().ToHttpsUrl() : string.Empty))
|
||||
|
|
342
src/Ombi.Notifications.Tests/NotificationMessageCurlysTests.cs
Normal file
342
src/Ombi.Notifications.Tests/NotificationMessageCurlysTests.cs
Normal 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"));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoFixture" Version="4.11.0" />
|
||||
<PackageReference Include="Nunit" Version="3.11.0" />
|
||||
<PackageReference Include="NUnit.ConsoleRunner" Version="3.9.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.13.0" />
|
||||
|
|
|
@ -106,21 +106,23 @@ namespace Ombi.Notifications.Agents
|
|||
};
|
||||
|
||||
var fields = new List<DiscordField>();
|
||||
|
||||
if (model.Data.TryGetValue("Alias", out var alias))
|
||||
if (!settings.HideUser)
|
||||
{
|
||||
if (alias.HasValue())
|
||||
if (model.Data.TryGetValue("Alias", out var alias))
|
||||
{
|
||||
fields.Add(new DiscordField { name = "Requested By", value = alias, inline = true });
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (model.Data.TryGetValue("RequestedUser", out var requestedUser))
|
||||
{
|
||||
if (requestedUser.HasValue())
|
||||
if (alias.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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -240,9 +240,9 @@ namespace Ombi.Notifications.Agents
|
|||
|
||||
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())
|
||||
{
|
||||
|
|
|
@ -304,9 +304,9 @@ namespace Ombi.Notifications.Agents
|
|||
|
||||
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;
|
||||
if (notificationId.Any())
|
||||
|
|
|
@ -57,6 +57,7 @@ namespace Ombi.Notifications.Agents
|
|||
var notification = new NotificationMessage
|
||||
{
|
||||
Message = parsed.Message,
|
||||
Subject = "New Request",
|
||||
Data = GetNotificationData(parsed, NotificationType.NewRequest)
|
||||
};
|
||||
|
||||
|
@ -76,6 +77,7 @@ namespace Ombi.Notifications.Agents
|
|||
var notification = new NotificationMessage
|
||||
{
|
||||
Message = parsed.Message,
|
||||
Subject = "New Issue",
|
||||
Data = GetNotificationData(parsed, NotificationType.Issue)
|
||||
};
|
||||
|
||||
|
@ -127,6 +129,7 @@ namespace Ombi.Notifications.Agents
|
|||
var notification = new NotificationMessage
|
||||
{
|
||||
Message = parsed.Message,
|
||||
Subject = "Issue Resolved",
|
||||
Data = GetNotificationData(parsed, NotificationType.IssueResolved)
|
||||
};
|
||||
|
||||
|
@ -149,6 +152,7 @@ namespace Ombi.Notifications.Agents
|
|||
var notification = new NotificationMessage
|
||||
{
|
||||
Message = parsed.Message,
|
||||
Subject = "Request Error",
|
||||
Data = GetNotificationData(parsed, NotificationType.ItemAddedToFaultQueue)
|
||||
};
|
||||
|
||||
|
@ -168,6 +172,7 @@ namespace Ombi.Notifications.Agents
|
|||
var notification = new NotificationMessage
|
||||
{
|
||||
Message = parsed.Message,
|
||||
Subject = "Request Declined",
|
||||
Data = GetNotificationData(parsed, NotificationType.RequestDeclined)
|
||||
};
|
||||
|
||||
|
@ -188,6 +193,7 @@ namespace Ombi.Notifications.Agents
|
|||
var notification = new NotificationMessage
|
||||
{
|
||||
Message = parsed.Message,
|
||||
Subject = "Request Approved",
|
||||
Data = GetNotificationData(parsed, NotificationType.RequestApproved)
|
||||
};
|
||||
|
||||
|
@ -212,6 +218,7 @@ namespace Ombi.Notifications.Agents
|
|||
var notification = new NotificationMessage
|
||||
{
|
||||
Message = parsed.Message,
|
||||
Subject = "Request Available",
|
||||
Data = data
|
||||
};
|
||||
// Send to user
|
||||
|
@ -259,6 +266,7 @@ namespace Ombi.Notifications.Agents
|
|||
var notification = new NotificationMessage
|
||||
{
|
||||
Message = message,
|
||||
Subject = "Test Notification"
|
||||
};
|
||||
// Send to user
|
||||
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)
|
||||
{
|
||||
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();
|
||||
|
||||
|
|
|
@ -48,7 +48,7 @@ namespace Ombi.Notifications
|
|||
protected ChildRequests TvRequest { get; set; }
|
||||
protected AlbumRequest AlbumRequest { 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; }
|
||||
|
||||
|
@ -75,7 +75,7 @@ namespace Ombi.Notifications
|
|||
if (model.RequestId > 0)
|
||||
{
|
||||
await LoadRequest(model.RequestId, model.RequestType);
|
||||
SubsribedUsers = GetSubscriptions(model.RequestId, model.RequestType);
|
||||
Subscribed = GetSubscriptions(model.RequestId, model.RequestType);
|
||||
}
|
||||
|
||||
Customization = await CustomizationSettings.GetSettingsAsync();
|
||||
|
@ -209,7 +209,6 @@ namespace Ombi.Notifications
|
|||
if (model.RequestType == RequestType.Movie)
|
||||
{
|
||||
_log.LogDebug("Notification options: {@model}, Req: {@MovieRequest}, Settings: {@Customization}", model, MovieRequest, Customization);
|
||||
|
||||
curlys.Setup(model, MovieRequest, Customization, preference);
|
||||
}
|
||||
else if (model.RequestType == RequestType.TvShow)
|
||||
|
|
|
@ -14,218 +14,156 @@ namespace Ombi.Notifications
|
|||
{
|
||||
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)
|
||||
{
|
||||
ApplicationUrl = (s?.ApplicationUrl.HasValue() ?? false) ? s.ApplicationUrl : string.Empty;
|
||||
ApplicationName = string.IsNullOrEmpty(s?.ApplicationName) ? "Ombi" : s?.ApplicationName;
|
||||
}
|
||||
|
||||
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);
|
||||
ApplicationName = string.IsNullOrEmpty(s?.ApplicationName) ? "Ombi" : s.ApplicationName;
|
||||
ApplicationUrl = s?.ApplicationUrl.HasValue() ?? false ? s.ApplicationUrl : string.Empty;
|
||||
}
|
||||
|
||||
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;
|
||||
Alias = user.UserAlias;
|
||||
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)
|
||||
{
|
||||
var val = string.Empty;
|
||||
IssueDescription = opts.Substitutes.TryGetValue("IssueDescription", out val) ? val : string.Empty;
|
||||
IssueDescription = opts.Substitutes.TryGetValue("IssueDescription", out string val) ? val : string.Empty;
|
||||
IssueCategory = opts.Substitutes.TryGetValue("IssueCategory", 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;
|
||||
NewIssueComment = opts.Substitutes.TryGetValue("NewIssueComment", 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;
|
||||
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)
|
||||
|
@ -238,16 +176,19 @@ namespace Ombi.Notifications
|
|||
RequestStatus = "Available";
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.Denied ?? false)
|
||||
{
|
||||
RequestStatus = "Denied";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!req.Available && req.Approved)
|
||||
{
|
||||
RequestStatus = "Processing Request";
|
||||
return;
|
||||
}
|
||||
|
||||
RequestStatus = "Pending Approval";
|
||||
}
|
||||
}
|
||||
|
@ -288,36 +229,36 @@ namespace Ombi.Notifications
|
|||
|
||||
public Dictionary<string, string> Curlys => new Dictionary<string, string>
|
||||
{
|
||||
{nameof(RequestId), RequestId },
|
||||
{nameof(RequestedUser), RequestedUser },
|
||||
{nameof(Title), Title },
|
||||
{nameof(RequestedDate), RequestedDate },
|
||||
{nameof(Type), Type },
|
||||
{nameof(AdditionalInformation), AdditionalInformation },
|
||||
{nameof(LongDate),LongDate},
|
||||
{nameof(ShortDate),ShortDate},
|
||||
{nameof(LongTime),LongTime},
|
||||
{nameof(ShortTime),ShortTime},
|
||||
{nameof(Overview),Overview},
|
||||
{nameof(Year),Year},
|
||||
{nameof(EpisodesList),EpisodesList},
|
||||
{nameof(SeasonsList),SeasonsList},
|
||||
{nameof(PosterImage),PosterImage},
|
||||
{nameof(ApplicationName),ApplicationName},
|
||||
{nameof(ApplicationUrl),ApplicationUrl},
|
||||
{nameof(IssueDescription),IssueDescription},
|
||||
{nameof(IssueCategory),IssueCategory},
|
||||
{nameof(IssueStatus),IssueStatus},
|
||||
{nameof(IssueSubject),IssueSubject},
|
||||
{nameof(NewIssueComment),NewIssueComment},
|
||||
{nameof(IssueUser),IssueUser},
|
||||
{nameof(UserName),UserName},
|
||||
{nameof(Alias),Alias},
|
||||
{nameof(UserPreference),UserPreference},
|
||||
{nameof(DenyReason),DenyReason},
|
||||
{nameof(AvailableDate),AvailableDate},
|
||||
{nameof(RequestStatus),RequestStatus},
|
||||
{nameof(ProviderId),ProviderId},
|
||||
{ nameof(RequestId), RequestId },
|
||||
{ nameof(RequestedUser), RequestedUser },
|
||||
{ nameof(Title), Title },
|
||||
{ nameof(RequestedDate), RequestedDate },
|
||||
{ nameof(Type), Type },
|
||||
{ nameof(AdditionalInformation), AdditionalInformation },
|
||||
{ nameof(LongDate), LongDate },
|
||||
{ nameof(ShortDate), ShortDate },
|
||||
{ nameof(LongTime), LongTime },
|
||||
{ nameof(ShortTime), ShortTime },
|
||||
{ nameof(Overview), Overview },
|
||||
{ nameof(Year), Year },
|
||||
{ nameof(EpisodesList), EpisodesList },
|
||||
{ nameof(SeasonsList), SeasonsList },
|
||||
{ nameof(PosterImage), PosterImage },
|
||||
{ nameof(ApplicationName), ApplicationName },
|
||||
{ nameof(ApplicationUrl), ApplicationUrl },
|
||||
{ nameof(IssueDescription), IssueDescription },
|
||||
{ nameof(IssueCategory), IssueCategory },
|
||||
{ nameof(IssueStatus), IssueStatus },
|
||||
{ nameof(IssueSubject), IssueSubject },
|
||||
{ nameof(NewIssueComment), NewIssueComment },
|
||||
{ nameof(IssueUser), IssueUser },
|
||||
{ nameof(UserName), UserName },
|
||||
{ nameof(Alias), Alias },
|
||||
{ nameof(UserPreference), UserPreference },
|
||||
{ nameof(DenyReason), DenyReason },
|
||||
{ nameof(AvailableDate), AvailableDate },
|
||||
{ nameof(RequestStatus), RequestStatus },
|
||||
{ nameof(ProviderId), ProviderId },
|
||||
};
|
||||
}
|
||||
}
|
|
@ -17,6 +17,7 @@ using Ombi.Store.Entities;
|
|||
using Ombi.Store.Entities.Requests;
|
||||
using Ombi.Store.Repository;
|
||||
using Ombi.Store.Repository.Requests;
|
||||
using Ombi.Helpers;
|
||||
|
||||
namespace Ombi.Schedule.Tests
|
||||
{
|
||||
|
@ -53,7 +54,7 @@ namespace Ombi.Schedule.Tests
|
|||
ImdbId = "test"
|
||||
};
|
||||
_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);
|
||||
|
||||
|
|
|
@ -5,6 +5,6 @@ namespace Ombi.Schedule.Jobs.Ombi
|
|||
public interface IOmbiAutomaticUpdater : IBaseJob
|
||||
{
|
||||
string[] GetVersion();
|
||||
Task<bool> UpdateAvailable(string branch, string currentVersion);
|
||||
Task<bool> UpdateAvailable(string currentVersion);
|
||||
}
|
||||
}
|
|
@ -49,10 +49,10 @@ namespace Ombi.Schedule.Jobs.Ombi
|
|||
var productArray = productVersion.Split('-');
|
||||
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;
|
||||
return !serverVersion.Equals(currentVersion, StringComparison.CurrentCultureIgnoreCase);
|
||||
|
||||
|
@ -88,7 +88,7 @@ namespace Ombi.Schedule.Jobs.Ombi
|
|||
|
||||
Logger.LogDebug(LoggingEvents.Updater, "Looking for updates now");
|
||||
//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);
|
||||
|
||||
|
||||
|
|
|
@ -183,13 +183,13 @@ namespace Ombi.Schedule.Jobs.Plex
|
|||
PlexServerContent item = null;
|
||||
if (movie.ImdbId.HasValue())
|
||||
{
|
||||
item = await _repo.Get(movie.ImdbId);
|
||||
item = await _repo.Get(movie.ImdbId, ProviderType.ImdbId);
|
||||
}
|
||||
if (item == null)
|
||||
{
|
||||
if (movie.TheMovieDbId.ToString().HasValue())
|
||||
{
|
||||
item = await _repo.Get(movie.TheMovieDbId.ToString());
|
||||
item = await _repo.Get(movie.TheMovieDbId.ToString(), ProviderType.TheMovieDbId);
|
||||
}
|
||||
}
|
||||
if (item == null)
|
||||
|
|
|
@ -109,8 +109,8 @@ namespace Ombi.Core.Processor
|
|||
public string UpdateVersionString { get; set; }
|
||||
public int UpdateVersion { get; set; }
|
||||
public DateTime UpdateDate { get; set; }
|
||||
|
||||
public List<ChangeLog> ChangeLogs { get; set; }
|
||||
public bool UpdateAvailable { get; set; }
|
||||
public string ChangeLogs { get; set; }
|
||||
public List<Downloads> Downloads { get; set; }
|
||||
}
|
||||
|
||||
|
|
|
@ -16,94 +16,41 @@ namespace Ombi.Schedule.Processor
|
|||
{
|
||||
public class ChangeLogProcessor : IChangeLogProcessor
|
||||
{
|
||||
public ChangeLogProcessor(IApi api, IOmbiHttpClient client)
|
||||
public ChangeLogProcessor(IApi api, IHttpClientFactory client)
|
||||
{
|
||||
_api = api;
|
||||
_client = client;
|
||||
_client = client.CreateClient("OmbiClient");
|
||||
}
|
||||
|
||||
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 AppveyorApiUrl = "https://ci.appveyor.com/api";
|
||||
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
|
||||
{
|
||||
Version = latestRelease.InnerText,
|
||||
Features = featuresList,
|
||||
Fixes = fixesList,
|
||||
Downloads = new List<Downloads>()
|
||||
};
|
||||
|
||||
if (masterBranch)
|
||||
{
|
||||
var releaseTag = latestRelease.InnerText.Substring(0, 9);
|
||||
await GetGitubRelease(release, releaseTag);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Get AppVeyor
|
||||
await GetAppVeyorRelease(release, branch);
|
||||
}
|
||||
|
||||
|
||||
return TransformUpdate(release,!masterBranch);
|
||||
|
||||
await GetGitubRelease(release);
|
||||
|
||||
return TransformUpdate(release);
|
||||
}
|
||||
|
||||
private UpdateModel TransformUpdate(Release release, bool develop)
|
||||
private UpdateModel TransformUpdate(Release release)
|
||||
{
|
||||
var newUpdate = new UpdateModel
|
||||
{
|
||||
UpdateVersionString = develop ? release.Version : release.Version.Substring(1,8),
|
||||
UpdateVersion = release.Version == "(unreleased)" ? 0 : int.Parse(release.Version.Substring(1, 5).Replace(".", "")),
|
||||
UpdateVersionString = release.Version,
|
||||
UpdateVersion = int.Parse(release.Version.Substring(1, 5).Replace(".", "")),
|
||||
UpdateDate = DateTime.Now,
|
||||
ChangeLogs = new List<ChangeLog>(),
|
||||
Downloads = new List<Downloads>()
|
||||
};
|
||||
ChangeLogs = release.Description,
|
||||
Downloads = new List<Downloads>(),
|
||||
UpdateAvailable = release.Version != "v" + AssemblyHelper.GetRuntimeVersion()
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
request.ApplicationJsonContentType();
|
||||
var client = new GitHubClient(Octokit.ProductHeaderValue.Parse("OmbiV4"));
|
||||
|
||||
var builds = await _api.Request<AppveyorBranchResult>(request);
|
||||
var jobId = builds.build.jobs.FirstOrDefault()?.jobId ?? string.Empty;
|
||||
var releases = await client.Repository.Release.GetAll("ombi-app", "ombi");
|
||||
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)
|
||||
{
|
||||
var d = new Downloads
|
||||
|
@ -192,6 +80,8 @@ namespace Ombi.Schedule.Processor
|
|||
};
|
||||
release.Downloads.Add(d);
|
||||
}
|
||||
release.Description = Markdown.ToHtml(latest.Body);
|
||||
release.Version = latest.TagName;
|
||||
}
|
||||
}
|
||||
public class Release
|
||||
|
@ -199,8 +89,7 @@ namespace Ombi.Schedule.Processor
|
|||
public string Version { get; set; }
|
||||
public string CheckinVersion { get; set; }
|
||||
public List<Downloads> Downloads { get; set; }
|
||||
public List<string> Features { get; set; }
|
||||
public List<string> Fixes { get; set; }
|
||||
public string Description { get; set; }
|
||||
}
|
||||
|
||||
public class Downloads
|
||||
|
|
|
@ -4,6 +4,6 @@ namespace Ombi.Core.Processor
|
|||
{
|
||||
public interface IChangeLogProcessor
|
||||
{
|
||||
Task<UpdateModel> Process(string branch);
|
||||
Task<UpdateModel> Process();
|
||||
}
|
||||
}
|
|
@ -20,6 +20,7 @@
|
|||
public bool AddOnly { get; set; }
|
||||
public bool V3 { get; set; }
|
||||
public int LanguageProfile { get; set; }
|
||||
public int LanguageProfileAnime { get; set; }
|
||||
public bool ScanForAvailability { get; set; }
|
||||
}
|
||||
}
|
|
@ -9,6 +9,7 @@ namespace Ombi.Settings.Settings.Models.Notifications
|
|||
public string WebhookUrl { get; set; }
|
||||
public string Username { get; set; }
|
||||
public string Icon { get; set; }
|
||||
public bool HideUser { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public string WebHookId => SplitWebUrl(4);
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
public bool CollectAnalyticData { get; set; }
|
||||
public bool Wizard { get; set; }
|
||||
public string ApiKey { get; set; }
|
||||
public bool IgnoreCertificateErrors { get; set; }
|
||||
public bool DoNotSendNotificationsForAutoApprove { get; set; }
|
||||
public bool HideRequestsUsers { get; set; }
|
||||
public bool DisableHealthChecks { get; set; }
|
||||
|
|
|
@ -16,6 +16,7 @@ namespace Ombi.Store.Entities.Requests
|
|||
public string ImdbId { get; set; }
|
||||
public int? QualityOverride { get; set; }
|
||||
public int? RootFolder { get; set; }
|
||||
public int? LanguageProfile { get; set; }
|
||||
public string Overview { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string PosterPath { get; set; }
|
||||
|
|
1235
src/Ombi.Store/Migrations/OmbiMySql/20210408073336_SonarrProfileOnRequest.Designer.cs
generated
Normal file
1235
src/Ombi.Store/Migrations/OmbiMySql/20210408073336_SonarrProfileOnRequest.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -771,6 +771,9 @@ namespace Ombi.Store.Migrations.OmbiMySql
|
|||
b.Property<string>("ImdbId")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<int?>("LanguageProfile")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Overview")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
|
|
1234
src/Ombi.Store/Migrations/OmbiSqlite/20210408073232_SonarrProfileOnRequest.Designer.cs
generated
Normal file
1234
src/Ombi.Store/Migrations/OmbiSqlite/20210408073232_SonarrProfileOnRequest.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -770,6 +770,9 @@ namespace Ombi.Store.Migrations.OmbiSqlite
|
|||
b.Property<string>("ImdbId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("LanguageProfile")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Overview")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
@ -28,6 +29,11 @@ namespace Ombi.Store.Repository
|
|||
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()
|
||||
{
|
||||
return _db.AsQueryable();
|
||||
|
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Threading.Tasks;
|
||||
using Ombi.Helpers;
|
||||
using Ombi.Store.Entities;
|
||||
|
||||
namespace Ombi.Store.Repository
|
||||
|
@ -10,7 +11,7 @@ namespace Ombi.Store.Repository
|
|||
public interface IPlexContentRepository : IExternalRepository<PlexServerContent>
|
||||
{
|
||||
Task<bool> ContentExists(string providerId);
|
||||
Task<PlexServerContent> Get(string providerId);
|
||||
Task<PlexServerContent> Get(string providerId, ProviderType type);
|
||||
Task<PlexServerContent> GetByKey(int key);
|
||||
Task Update(PlexServerContent existingContent);
|
||||
IQueryable<PlexEpisode> GetAllEpisodes();
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Query;
|
||||
|
@ -12,6 +13,7 @@ namespace Ombi.Store.Repository
|
|||
public interface IRepository<T> where T : Entity
|
||||
{
|
||||
Task<T> Find(object key);
|
||||
Task<T> Find(object key, CancellationToken cancellationToken);
|
||||
IQueryable<T> GetAll();
|
||||
Task<T> FirstOrDefaultAsync(Expression<Func<T, bool>> predicate);
|
||||
Task AddRange(IEnumerable<T> content, bool save = true);
|
||||
|
|
|
@ -31,6 +31,7 @@ using System.Linq;
|
|||
using System.Linq.Expressions;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Ombi.Helpers;
|
||||
using Ombi.Store.Context;
|
||||
using Ombi.Store.Entities;
|
||||
|
||||
|
@ -61,18 +62,21 @@ namespace Ombi.Store.Repository
|
|||
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);
|
||||
if (item == null)
|
||||
switch (type)
|
||||
{
|
||||
item = await Db.PlexServerContent.FirstOrDefaultAsync(x => x.TheMovieDbId == providerId);
|
||||
if (item == null)
|
||||
{
|
||||
item = await Db.PlexServerContent.FirstOrDefaultAsync(x => x.TvDbId == providerId);
|
||||
}
|
||||
case ProviderType.ImdbId:
|
||||
return await Db.PlexServerContent.FirstOrDefaultAsync(x => x.ImdbId == providerId);
|
||||
case ProviderType.TheMovieDbId:
|
||||
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)
|
||||
|
|
|
@ -97,6 +97,8 @@ namespace Ombi.Api.TheMovieDb.Models
|
|||
{
|
||||
[JsonProperty("results")]
|
||||
public List<KeywordsValue> KeywordsValue { get; set; }
|
||||
[JsonProperty("keywords")]
|
||||
private List<KeywordsValue> _movieKeywordValue { set { KeywordsValue = value; }}
|
||||
}
|
||||
|
||||
public class KeywordsValue
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
@ -16,5 +19,10 @@
|
|||
public int VoteCount { get; set; }
|
||||
public bool Video { 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>();
|
||||
}
|
||||
}
|
|
@ -59,6 +59,7 @@ export interface IDiscordNotifcationSettings extends INotificationSettings {
|
|||
webhookUrl: string;
|
||||
username: string;
|
||||
icon: string;
|
||||
hideUser: boolean;
|
||||
notificationTemplates: INotificationTemplates[];
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { IChildRequests, IMovieRequests } from ".";
|
||||
import { ITvRequests } from "./IRequestModel";
|
||||
import { ILanguageProfiles } from "./ISonarr";
|
||||
|
||||
export interface IRadarrRootFolder {
|
||||
id: number;
|
||||
|
@ -27,6 +28,9 @@ export interface IAdvancedData {
|
|||
rootFolder: IRadarrRootFolder;
|
||||
rootFolders: IRadarrRootFolder[];
|
||||
rootFolderId: number;
|
||||
language: ILanguageProfiles;
|
||||
languages: ILanguageProfiles[];
|
||||
languageId: number;
|
||||
movieRequest: IMovieRequests;
|
||||
tvRequest: ITvRequests;
|
||||
}
|
|
@ -26,6 +26,7 @@ export interface IMovieAdvancedOptions {
|
|||
requestId: number;
|
||||
qualityOverride: number;
|
||||
rootPathOverride: number;
|
||||
languageProfile: number;
|
||||
}
|
||||
|
||||
export interface IAlbumRequest extends IBaseRequest {
|
||||
|
@ -110,6 +111,7 @@ export interface ITvRequests {
|
|||
status: string;
|
||||
childRequests: IChildRequests[];
|
||||
qualityOverride: number;
|
||||
languageProfile: number;
|
||||
background: any;
|
||||
totalSeasons: number;
|
||||
tvDbId: number; // NO LONGER USED
|
||||
|
@ -119,6 +121,7 @@ export interface ITvRequests {
|
|||
// For UI display
|
||||
qualityOverrideTitle: string;
|
||||
rootPathOverrideTitle: string;
|
||||
languageOverrideTitle: string;
|
||||
}
|
||||
|
||||
export interface IChildRequests extends IBaseRequest {
|
||||
|
|
|
@ -51,6 +51,7 @@ export interface ITvRequestViewModelBase extends BaseRequestOptions {
|
|||
requestAll: boolean;
|
||||
firstSeason: boolean;
|
||||
latestSeason: boolean;
|
||||
languageProfile: number | undefined;
|
||||
seasons: ISeasonsViewModel[];
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,6 @@ export interface IOmbiSettings extends ISettings {
|
|||
collectAnalyticData: boolean;
|
||||
wizard: boolean;
|
||||
apiKey: string;
|
||||
ignoreCertificateErrors: boolean;
|
||||
doNotSendNotificationsForAutoApprove: boolean;
|
||||
hideRequestsUsers: boolean;
|
||||
defaultLanguageCode: string;
|
||||
|
@ -104,6 +103,7 @@ export interface ISonarrSettings extends IExternalSettings {
|
|||
addOnly: boolean;
|
||||
v3: boolean;
|
||||
languageProfile: number;
|
||||
languageProfileAnime: number;
|
||||
scanForAvailability: boolean;
|
||||
}
|
||||
|
||||
|
@ -285,3 +285,19 @@ export interface ITheMovieDbSettings extends ISettings {
|
|||
showAdultMovies: boolean;
|
||||
excludedKeywordIds: number[];
|
||||
}
|
||||
|
||||
export interface IUpdateModel
|
||||
{
|
||||
updateVersionString: string;
|
||||
updateVersion: number;
|
||||
updateDate: Date,
|
||||
updateAvailable: boolean;
|
||||
changeLogs: string;
|
||||
downloads: IUpdateDonloads[];
|
||||
}
|
||||
|
||||
export interface IUpdateDonloads
|
||||
{
|
||||
name: string;
|
||||
url: string
|
||||
}
|
||||
|
|
|
@ -146,6 +146,9 @@ export class LoginComponent implements OnDestroy, OnInit {
|
|||
}
|
||||
|
||||
public oauth() {
|
||||
if (this.oAuthWindow) {
|
||||
this.oAuthWindow.close();
|
||||
}
|
||||
this.oAuthWindow = window.open(window.location.toString(), "_blank", `toolbar=0,
|
||||
location=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.oAuthWindow!.location.replace(x.url);
|
||||
|
||||
this.pinTimer = setInterval(() => {
|
||||
if (this.pinTimer) {
|
||||
clearInterval(this.pinTimer);
|
||||
}
|
||||
|
||||
this.oauthLoading = true;
|
||||
this.getPinResult(x.pinId);
|
||||
}, 4000);
|
||||
this.pinTimer = setInterval(() => {
|
||||
if(this.oAuthWindow.closed) {
|
||||
this.oauthLoading = true;
|
||||
this.getPinResult(x.pinId);
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public getPinResult(pinId: number) {
|
||||
clearInterval(this.pinTimer);
|
||||
this.authService.oAuth(pinId).subscribe(x => {
|
||||
if(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()) {
|
||||
this.ngOnDestroy();
|
||||
|
||||
if(this.oAuthWindow) {
|
||||
if (this.oAuthWindow) {
|
||||
this.oAuthWindow.close();
|
||||
}
|
||||
this.oauthLoading = false;
|
||||
|
@ -184,6 +193,10 @@ export class LoginComponent implements OnDestroy, OnInit {
|
|||
return;
|
||||
}
|
||||
}
|
||||
this.notify.open("Could not log you in!", "OK", {
|
||||
duration: 3000
|
||||
});
|
||||
this.oauthLoading = false;
|
||||
|
||||
}, err => {
|
||||
console.log(err);
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
[type]="requestType"
|
||||
(openTrailer)="openDialog()"
|
||||
(onAdvancedOptions)="openAdvancedOptions()"
|
||||
(onReProcessRequest)="reProcessRequest()"
|
||||
>
|
||||
</social-icons>
|
||||
|
||||
|
|
|
@ -184,12 +184,22 @@ export class MovieDetailsComponent {
|
|||
if (result) {
|
||||
result.rootFolder = result.rootFolders.filter(f => f.id === +result.rootFolderId)[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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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() {
|
||||
this.imageService.getMovieBanner(this.theMovidDbId.toString()).subscribe(x => {
|
||||
if (!this.movie.backdropPath) {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div class="rating medium-font">
|
||||
<span *ngIf="movie.voteAverage"
|
||||
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 *ngIf="ratings?.critics_rating && ratings?.critics_score">
|
||||
<img class="rating-small"
|
||||
|
@ -122,4 +122,4 @@
|
|||
{{keyword.name}}
|
||||
</mat-chip>
|
||||
</mat-chip-list>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -35,5 +35,9 @@
|
|||
<span *ngIf="type === RequestType.movie"> {{ 'MediaDetails.RadarrConfiguration' | translate}}</span>
|
||||
<span *ngIf="type === RequestType.tvShow"> {{ 'MediaDetails.SonarrConfiguration' | translate}}</span>
|
||||
</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>
|
||||
</div>
|
||||
|
|
|
@ -26,6 +26,7 @@ export class SocialIconsComponent {
|
|||
|
||||
@Output() openTrailer: EventEmitter<any> = new EventEmitter();
|
||||
@Output() onAdvancedOptions: EventEmitter<any> = new EventEmitter();
|
||||
@Output() onReProcessRequest: EventEmitter<any> = new EventEmitter();
|
||||
|
||||
public RequestType = RequestType;
|
||||
|
||||
|
@ -37,4 +38,8 @@ export class SocialIconsComponent {
|
|||
public openAdvancedOptions() {
|
||||
this.onAdvancedOptions.emit();
|
||||
}
|
||||
|
||||
public reProcessRequest() {
|
||||
this.onReProcessRequest.emit();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<i class="fas fa-x7 fa-exclamation-triangle glyphicon"></i>
|
||||
<span>{{'MediaDetails.AutoApproveOptionsTvShort' | translate }}</span>
|
||||
</div>
|
||||
<mat-form-field>
|
||||
<mat-form-field *ngIf="sonarrEnabled">
|
||||
<mat-label>{{'MediaDetails.QualityProfilesSelect' | translate }}</mat-label>
|
||||
<mat-select [(value)]="data.profileId">
|
||||
<mat-option *ngFor="let profile of sonarrProfiles" value="{{profile.id}}">{{profile.name}}</mat-option>
|
||||
|
@ -13,13 +13,21 @@
|
|||
</mat-form-field>
|
||||
</div>
|
||||
<div mat-dialog-content>
|
||||
<mat-form-field>
|
||||
<mat-form-field *ngIf="sonarrEnabled">
|
||||
<mat-label>{{'MediaDetails.RootFolderSelect' | translate }}</mat-label>
|
||||
<mat-select [(value)]="data.rootFolderId">
|
||||
<mat-option *ngFor="let profile of sonarrRootFolders" value="{{profile.id}}">{{profile.path}}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</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>
|
||||
<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>
|
||||
|
|
|
@ -1,55 +1,92 @@
|
|||
import { Component, Inject, OnInit } from "@angular/core";
|
||||
import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material/dialog";
|
||||
import { IAdvancedData, ISonarrProfile, ISonarrRootFolder } from "../../../../../interfaces";
|
||||
import { SonarrService } from "../../../../../services";
|
||||
import {
|
||||
IAdvancedData,
|
||||
ILanguageProfiles,
|
||||
ISonarrProfile,
|
||||
ISonarrRootFolder,
|
||||
ISonarrSettings,
|
||||
} from "../../../../../interfaces";
|
||||
import { SettingsService, SonarrService } from "../../../../../services";
|
||||
|
||||
@Component({
|
||||
templateUrl: "./tv-advanced-options.component.html",
|
||||
selector: "tv-advanced-options",
|
||||
templateUrl: "./tv-advanced-options.component.html",
|
||||
selector: "tv-advanced-options",
|
||||
})
|
||||
export class TvAdvancedOptionsComponent implements OnInit {
|
||||
public sonarrProfiles: ISonarrProfile[];
|
||||
public sonarrRootFolders: ISonarrRootFolder[];
|
||||
public sonarrLanguageProfiles: ILanguageProfiles[];
|
||||
public sonarrEnabled: boolean;
|
||||
|
||||
public sonarrProfiles: ISonarrProfile[];
|
||||
public sonarrRootFolders: ISonarrRootFolder[];
|
||||
constructor(
|
||||
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,
|
||||
private sonarrService: SonarrService
|
||||
) {
|
||||
public async ngOnInit() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async ngOnInit() {
|
||||
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();
|
||||
});
|
||||
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.tvRequest.rootPathOverrideTitle = path[0].path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.movieRequest.qualityOverrideTitle = 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;
|
||||
}
|
||||
}
|
||||
private setLanguageOverride(): void {
|
||||
if (this.sonarrLanguageProfiles) {
|
||||
const profile = this.sonarrLanguageProfiles.filter((p) => {
|
||||
return p.id === this.data.tvRequest.languageProfile;
|
||||
});
|
||||
if (profile.length > 0) {
|
||||
this.data.tvRequest.languageOverrideTitle = profile[0].name;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -67,6 +67,7 @@ export class TvRequestGridComponent {
|
|||
viewModel.requestOnBehalf = result.username?.id;
|
||||
viewModel.qualityPathOverride = result?.sonarrPathId;
|
||||
viewModel.rootFolderOverride = result?.sonarrFolderId;
|
||||
viewModel.languageProfile = result?.sonarrLanguageId;
|
||||
|
||||
const requestResult = await this.requestServiceV2.requestTv(viewModel).toPromise();
|
||||
this.postRequest(requestResult);
|
||||
|
|
|
@ -70,6 +70,7 @@
|
|||
<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 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>
|
||||
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import { RequestService } from "../../../../../services/request.service";
|
|||
import { MessageService } from "../../../../../services";
|
||||
import { MatDialog } from "@angular/material/dialog";
|
||||
import { DenyDialogComponent } from "../../../shared/deny-dialog/deny-dialog.component";
|
||||
import { RequestServiceV2 } from "../../../../../services/requestV2.service";
|
||||
|
||||
@Component({
|
||||
templateUrl: "./tv-requests-panel.component.html",
|
||||
|
@ -16,7 +17,9 @@ export class TvRequestsPanelComponent {
|
|||
|
||||
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) {
|
||||
|
||||
}
|
||||
|
@ -83,9 +86,9 @@ export class TvRequestsPanelComponent {
|
|||
width: '250px',
|
||||
data: {requestId: request.id, requestType: RequestType.tvShow}
|
||||
});
|
||||
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
request.denied = true;
|
||||
request.denied = true;
|
||||
request.seasonRequests.forEach((season) => {
|
||||
season.episodes.forEach((ep) => {
|
||||
ep.approved = false;
|
||||
|
@ -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");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -102,7 +102,8 @@ export class TvDetailsComponent implements OnInit {
|
|||
// get the name and ids
|
||||
result.rootFolder = result.rootFolders.filter(f => f.id === +result.rootFolderId)[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);
|
||||
}
|
||||
});
|
||||
|
@ -117,15 +118,20 @@ export class TvDetailsComponent implements OnInit {
|
|||
if (data.profileId) {
|
||||
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() {
|
||||
const profile = this.sonarrService.getQualityProfilesWithoutSettings();
|
||||
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 sonarrRootFolders = x[1];
|
||||
const languageProfiles = x[2];
|
||||
|
||||
const profile = sonarrProfiles.filter((p) => {
|
||||
return p.id === this.showRequest.qualityOverride;
|
||||
|
@ -141,6 +147,13 @@ export class TvDetailsComponent implements OnInit {
|
|||
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;
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,12 +6,12 @@
|
|||
<mat-toolbar class="application-name" id="nav-applicationName">{{applicationName}}</mat-toolbar>
|
||||
|
||||
<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"
|
||||
[routerLinkActive]="'active-list-item'">
|
||||
|
||||
|
@ -19,7 +19,7 @@
|
|||
style="padding-left: 5px; padding-right: 5px;" aria-hidden="true"></i>
|
||||
{{nav.name | translate}}
|
||||
</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'">
|
||||
|
||||
<i class="fa-lg {{nav.icon}} icon-spacing"></i>
|
||||
|
@ -28,7 +28,7 @@
|
|||
</div>
|
||||
</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();">
|
||||
<i class="fa-lg fas fa-sign-out-alt icon-spacing"></i>
|
||||
{{ 'NavigationBar.Logout' | translate }}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<form [formGroup]='searchForm'>
|
||||
<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>
|
||||
</form>
|
||||
</form>
|
||||
|
|
|
@ -32,6 +32,10 @@ export class SonarrService extends ServiceHelpers {
|
|||
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> {
|
||||
return this.http.get<boolean>(`${this.url}/enabled/`, { headers: this.headers }).toPromise();
|
||||
}
|
||||
|
|
|
@ -187,5 +187,4 @@ export class RequestService extends ServiceHelpers {
|
|||
public removeAlbumRequest(request: number): any {
|
||||
return this.http.delete(`${this.url}music/${request}`, {headers: this.headers});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import { Injectable, Inject } from "@angular/core";
|
|||
import { HttpClient } from "@angular/common/http";
|
||||
import { Observable } from "rxjs";
|
||||
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()
|
||||
|
@ -92,4 +92,8 @@ export class RequestServiceV2 extends ServiceHelpers {
|
|||
public requestTv(tv: ITvRequestViewModelV2): Observable<IRequestEngineResult> {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
|
18
src/Ombi/ClientApp/src/app/services/update.service.ts
Normal file
18
src/Ombi/ClientApp/src/app/services/update.service.ts
Normal 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});
|
||||
}
|
||||
}
|
|
@ -13,8 +13,10 @@
|
|||
</div>
|
||||
<div class="mat-row">
|
||||
<div class="mat-cell">Version</div>
|
||||
<div class="mat-cell">{{about.version}} <a [routerLink]="['/Settings/Update']" *ngIf="newUpdate"
|
||||
style="color:#df691a"><b>(New Update Available)</b></a></div>
|
||||
<div class="mat-cell">{{about.version}} <a (click)="openUpdate()" *ngIf="newUpdate"
|
||||
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 class="mat-row">
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import { Component, OnInit } from "@angular/core";
|
||||
import { IAbout } from "../../interfaces/ISettings";
|
||||
import { JobService, SettingsService, HubService, SystemService } from "../../services";
|
||||
import { IAbout, IUpdateModel } from "../../interfaces/ISettings";
|
||||
import { SettingsService, HubService, SystemService } from "../../services";
|
||||
import { IConnectedUser } from "../../interfaces";
|
||||
import { UpdateService } from "../../services/update.service";
|
||||
import { MatDialog } from "@angular/material/dialog";
|
||||
import { UpdateDialogComponent } from "./update-dialog.component";
|
||||
|
||||
@Component({
|
||||
templateUrl: "./about.component.html",
|
||||
|
@ -14,22 +17,29 @@ export class AboutComponent implements OnInit {
|
|||
public connectedUsers: IConnectedUser[];
|
||||
public newsHtml: string;
|
||||
|
||||
private update: IUpdateModel;
|
||||
|
||||
constructor(private readonly settingsService: SettingsService,
|
||||
private readonly jobService: JobService,
|
||||
private readonly jobService: UpdateService,
|
||||
private readonly hubService: HubService,
|
||||
private readonly systemService: SystemService) { }
|
||||
private readonly systemService: SystemService,
|
||||
private readonly dialog: MatDialog) { }
|
||||
|
||||
public async ngOnInit() {
|
||||
this.settingsService.about().subscribe(x => this.about = x);
|
||||
this.newsHtml = await this.systemService.getNews().toPromise();
|
||||
|
||||
// TODO
|
||||
// this.jobService.getCachedUpdate().subscribe(x => {
|
||||
// if (x === true) {
|
||||
// // this.newUpdate = true; // TODO
|
||||
// }
|
||||
// });
|
||||
this.jobService.checkForUpdate().subscribe(x => {
|
||||
this.update = x;
|
||||
if (x.updateAvailable) {
|
||||
this.newUpdate = true;
|
||||
}
|
||||
});
|
||||
|
||||
this.connectedUsers = await this.hubService.getConnectedUsers();
|
||||
}
|
||||
|
||||
public openUpdate() {
|
||||
this.dialog.open(UpdateDialogComponent, { width: "700px", data: this.update, panelClass: 'modal-panel' });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue