Merge pull request #4194 from Ombi-app/develop

Sync up
pull/4197/head
Twan Ariens 3 years ago committed by GitHub
commit dde517cecc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

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

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

@ -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 });
if (alias.HasValue())
{
fields.Add(new DiscordField { name = "Requested By", value = alias, inline = true });
}
}
}
else
{
if (model.Data.TryGetValue("RequestedUser", out var requestedUser))
else
{
if (requestedUser.HasValue())
if (model.Data.TryGetValue("RequestedUser", out var requestedUser))
{
fields.Add(new DiscordField { name = "Requested By", value = requestedUser, inline = true });
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)
public void SetupNewsletter(CustomizationSettings s)
{
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;
}
ApplicationName = string.IsNullOrEmpty(s?.ApplicationName) ? "Ombi" : s.ApplicationName;
ApplicationUrl = s?.ApplicationUrl.HasValue() ?? false ? s.ApplicationUrl : string.Empty;
}
if (Alias.IsNullOrEmpty())
{
// Can be set if it's an issue
Alias = (req?.RequestedUser?.Alias.HasValue() ?? false) ? req?.RequestedUser?.Alias : req?.RequestedUser?.UserName;
}
public void Setup(OmbiUser user, CustomizationSettings s)
{
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;
}
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;
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();
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);
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, AlbumRequest req, CustomizationSettings s, UserNotificationPreferences pref)
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}";
RequestId = req?.Id.ToString();
ProviderId = req?.ForeignArtistId ?? 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();
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;
}
// 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);
}
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();
}
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();
PosterImage = (req?.Cover.HasValue() ?? false) ? req.Cover : req?.Disk ?? string.Empty;
AdditionalInformation = opts?.AdditionalInformation ?? string.Empty;
PosterImage = req?.Cover.HasValue() ?? false ? req.Cover : req?.Disk ?? string.Empty;
CalculateRequestStatus(req);
}
public void SetupNewsletter(CustomizationSettings s)
private void LoadIssues(NotificationOptions opts)
{
ApplicationUrl = (s?.ApplicationUrl.HasValue() ?? false) ? s.ApplicationUrl : string.Empty;
ApplicationName = string.IsNullOrEmpty(s?.ApplicationName) ? "Ombi" : s?.ApplicationName;
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) && Enum.TryParse(val, out RequestType type)
? HumanizeReturnType(type)
: string.Empty;
}
public void Setup(NotificationOptions opts, ChildRequests req, CustomizationSettings s, UserNotificationPreferences pref)
private void LoadCommon(BaseRequest req, CustomizationSettings s, UserNotificationPreferences pref)
{
LoadIssues(opts);
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();
ProviderId = req?.ParentRequest?.ExternalProviderId.ToString() ?? string.Empty;
string title;
if (req == null)
{
opts.Substitutes.TryGetValue("Title", out title);
}
else
RequestedUser = req?.RequestedUser?.UserName;
RequestedDate = req?.RequestedDate.ToString("D");
if (Type.IsNullOrEmpty())
{
title = req?.ParentRequest.Title;
Type = HumanizeReturnType(req?.RequestType);
}
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;
Alias = req?.RequestedUser?.Alias.HasValue() ?? false
? req.RequestedUser?.Alias
: req?.RequestedUser?.UserName;
}
if (pref != null)
{
UserPreference = pref.Value.HasValue() ? pref.Value : Alias;
}
Title = title;
RequestedDate = req?.RequestedDate.ToString("D");
if (Type.IsNullOrEmpty())
{
Type = req?.RequestType.Humanize();
}
Overview = req?.ParentRequest.Overview;
Year = req?.ParentRequest.ReleaseDate.Year.ToString();
PosterImage = string.Format((req?.ParentRequest.PosterPath ?? string.Empty).StartsWith("/", StringComparison.InvariantCultureIgnoreCase)
? "https://image.tmdb.org/t/p/w300{0}" : "https://image.tmdb.org/t/p/w300/{0}", req?.ParentRequest.PosterPath);
AdditionalInformation = opts.AdditionalInformation;
// DO Episode and Season Lists
var episodes = req?.SeasonRequests?.SelectMany(x => x.Episodes) ?? new List<EpisodeRequests>();
var seasons = req?.SeasonRequests?.OrderBy(x => x.SeasonNumber).ToList() ?? new List<SeasonRequests>();
var orderedEpisodes = episodes.OrderBy(x => x.EpisodeNumber).ToList();
var epSb = new StringBuilder();
var seasonSb = new StringBuilder();
for (var i = 0; i < orderedEpisodes.Count; i++)
{
var ep = orderedEpisodes[i];
if (i < orderedEpisodes.Count - 1)
{
epSb.Append($"{ep.EpisodeNumber},");
}
else
{
epSb.Append($"{ep.EpisodeNumber}");
}
}
for (var i = 0; i < seasons.Count; i++)
{
var ep = seasons[i];
if (i < seasons.Count - 1)
{
seasonSb.Append($"{ep.SeasonNumber},");
}
else
{
seasonSb.Append($"{ep.SeasonNumber}");
}
}
EpisodesList = epSb.ToString();
SeasonsList = seasonSb.ToString();
CalculateRequestStatus(req);
}
public void Setup(OmbiUser user, CustomizationSettings s)
private static string HumanizeReturnType(RequestType? requestType)
{
ApplicationUrl = (s?.ApplicationUrl.HasValue() ?? false) ? s.ApplicationUrl : string.Empty;
ApplicationName = string.IsNullOrEmpty(s?.ApplicationName) ? "Ombi" : s?.ApplicationName;
RequestedUser = user.UserName;
Alias = user.UserAlias;
UserName = user.UserName;
return requestType switch
{
null => string.Empty,
RequestType.TvShow => "TV Show",
_ => requestType.Humanize()
};
}
private void LoadIssues(NotificationOptions opts)
private void LoadTitle(NotificationOptions opts, BaseRequest req)
{
var val = string.Empty;
IssueDescription = opts.Substitutes.TryGetValue("IssueDescription", out val) ? val : string.Empty;
IssueCategory = opts.Substitutes.TryGetValue("IssueCategory", out val) ? val : string.Empty;
IssueStatus = opts.Substitutes.TryGetValue("IssueStatus", out val) ? val : string.Empty;
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;
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; }

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

@ -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();
});
}
});
}
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 setQualityOverrides(): void {
if (this.sonarrProfiles) {
const profile = this.sonarrProfiles.filter((p) => {
return p.id === this.data.tvRequest.qualityOverride;
});
if (profile.length > 0) {
this.data.tvRequest.qualityOverrideTitle = profile[0].name;
}
}
}
private 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.tvRequest.rootPathOverrideTitle = path[0].path;
}
}
}
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>
&nbsp;{{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>
&nbsp;{{ '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 });
}
}

@ -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}} &nbsp; <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…
Cancel
Save