Merge branch 'develop' into feature/jellyfin-libs

pull/4252/head
tidusjar 3 years ago
commit 132c935c28

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

@ -40,8 +40,8 @@ Search the existing requests to see if your suggestion has already been submitte
___
<a href='https://play.google.com/store/apps/details?id=com.tidusjar.Ombi&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img width="150" alt='Get it on Google Play' src='https://play.google.com/intl/en_gb/badges/images/generic/en_badge_web_generic.png'/></a>
<br>
_**Note:** There is no longer an iOS app due to complications outside of our control._
<a href='https://apps.apple.com/us/app/ombi/id1335260043'><img width="130" alt='Get it on the App Store' src='https://developer.apple.com/app-store/marketing/guidelines/images/badge-example-preferred.png'/></a>
<br>
# Features
Here are some of the features Ombi has:

@ -23,7 +23,7 @@ namespace Ombi.Api.Plex
Task<PlexFriends> GetUsers(string authToken);
Task<PlexAccount> GetAccount(string authToken);
Task<PlexMetadata> GetRecentlyAdded(string authToken, string uri, string sectionId);
Task<OAuthPin> GetPin(int pinId);
Task<OAuthContainer> GetPin(int pinId);
Task<Uri> GetOAuthUrl(string code, string applicationUrl);
Task<PlexAddWrapper> AddUser(string emailAddress, string serverId, string authToken, int[] libs);
}

@ -1,7 +1,14 @@
using System;
using System.Collections.Generic;
namespace Ombi.Api.Plex.Models.OAuth
{
public class OAuthContainer
{
public OAuthPin Result { get; set; }
public OAuthErrorsContainer Errors { get; set; }
}
public class OAuthPin
{
public int id { get; set; }
@ -24,4 +31,15 @@ namespace Ombi.Api.Plex.Models.OAuth
public string coordinates { get; set; }
}
public class OAuthErrorsContainer
{
public List<OAuthErrors> errors { get; set; }
}
public class OAuthErrors
{
public int code { get; set; }
public string message { get; set; }
}
}

@ -1,7 +1,7 @@
using System;
using System.Net.Http;
using System.Reflection;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Ombi.Api.Plex.Models;
using Ombi.Api.Plex.Models.Friends;
using Ombi.Api.Plex.Models.OAuth;
@ -208,12 +208,28 @@ namespace Ombi.Api.Plex
return await Api.Request<PlexMetadata>(request);
}
public async Task<OAuthPin> GetPin(int pinId)
public async Task<OAuthContainer> GetPin(int pinId)
{
var request = new Request($"api/v2/pins/{pinId}", "https://plex.tv/", HttpMethod.Get);
await AddHeaders(request);
return await Api.Request<OAuthPin>(request);
var response = await Api.RequestContent(request);
if (response.Contains("errors"))
{
var errors = JsonConvert.DeserializeObject<OAuthErrorsContainer>(response, Ombi.Api.Api.Settings);
return new OAuthContainer
{
Errors = errors
};
}
var pinResult = JsonConvert.DeserializeObject<OAuthPin>(response, Ombi.Api.Api.Settings);
return new OAuthContainer
{
Result = pinResult
};
}
public async Task<Uri> GetOAuthUrl(string code, string applicationUrl)

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

@ -41,7 +41,7 @@ namespace Ombi.Api.Radarr
public async Task<SystemStatus> SystemStatus(string apiKey, string baseUrl)
{
var request = new Request("/api/v3/status", baseUrl, HttpMethod.Get);
var request = new Request("/api/v3/system/status", baseUrl, HttpMethod.Get);
AddHeaders(request, apiKey);
return await Api.Request<SystemStatus>(request);
@ -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
{
@ -73,7 +73,7 @@ namespace Ombi.Api
}
// do something with the response
var receivedString = await httpResponseMessage.Content.ReadAsStringAsync();
var receivedString = await httpResponseMessage.Content.ReadAsStringAsync(cancellationToken);
LogDebugContent(receivedString);
if (request.ContentType == ContentType.Json)
{

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

@ -42,8 +42,9 @@ namespace Ombi.Core.Tests.Engine.V2
var cache = new Mock<ICacheService>();
var ombiSettings = new Mock<ISettingsService<OmbiSettings>>();
var requestSubs = new Mock<IRepository<RequestSubscription>>();
var mediaCache = new Mock<IMediaCacheService>();
_engine = new MovieRequestEngine(movieApi.Object, requestService.Object, user.Object, notificationHelper.Object, rules.Object, movieSender.Object,
logger.Object, userManager.Object, requestLogRepo.Object, cache.Object, ombiSettings.Object, requestSubs.Object);
logger.Object, userManager.Object, requestLogRepo.Object, cache.Object, ombiSettings.Object, requestSubs.Object, mediaCache.Object);
}
[Test]

@ -0,0 +1,209 @@
using MockQueryable.Moq;
using Moq;
using NUnit.Framework;
using Ombi.Core.Rule.Rules.Request;
using Ombi.Store.Entities;
using Ombi.Store.Entities.Requests;
using Ombi.Store.Repository;
using Ombi.Store.Repository.Requests;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Ombi.Core.Tests.Rule.Request
{
[TestFixture]
public class ExistingPlexRequestRuleTests
{
private ExistingPlexRequestRule Rule;
private Mock<IPlexContentRepository> PlexContentRepo;
[SetUp]
public void SetUp()
{
PlexContentRepo = new Mock<IPlexContentRepository>();
Rule = new ExistingPlexRequestRule(PlexContentRepo.Object);
}
[Test]
public async Task RequestShow_DoesNotExistAtAll_IsSuccessful()
{
PlexContentRepo.Setup(x => x.GetAll()).Returns(new List<PlexServerContent>().AsQueryable().BuildMock().Object);
var req = new ChildRequests
{
SeasonRequests = new List<SeasonRequests>
{
new SeasonRequests
{
Episodes = new List<EpisodeRequests>
{
new EpisodeRequests
{
Id = 1,
EpisodeNumber = 1,
}
},
SeasonNumber = 1
}
}
};
var result = await Rule.Execute(req);
Assert.That(result.Success, Is.True);
}
[Test]
public async Task RequestShow_AllEpisodesAreaRequested_IsNotSuccessful()
{
SetupMockData();
var req = new ChildRequests
{
SeasonRequests = new List<SeasonRequests>
{
new SeasonRequests
{
Episodes = new List<EpisodeRequests>
{
new EpisodeRequests
{
Id = 1,
EpisodeNumber = 1,
},
new EpisodeRequests
{
Id = 1,
EpisodeNumber = 2,
},
},
SeasonNumber = 1
}
},
Id = 1,
};
var result = await Rule.Execute(req);
Assert.That(result.Success, Is.False);
}
[Test]
public async Task RequestShow_SomeEpisodesAreaRequested_IsSuccessful()
{
SetupMockData();
var req = new ChildRequests
{
RequestType = RequestType.TvShow,
SeasonRequests = new List<SeasonRequests>
{
new SeasonRequests
{
Episodes = new List<EpisodeRequests>
{
new EpisodeRequests
{
Id = 1,
EpisodeNumber = 1,
},
new EpisodeRequests
{
Id = 2,
EpisodeNumber = 2,
},
new EpisodeRequests
{
Id = 3,
EpisodeNumber = 3,
},
},
SeasonNumber = 1
}
},
Id = 1,
};
var result = await Rule.Execute(req);
Assert.That(result.Success, Is.True);
var episodes = req.SeasonRequests.SelectMany(x => x.Episodes);
Assert.That(episodes.Count() == 1, "We didn't remove the episodes that have already been requested!");
Assert.That(episodes.First().EpisodeNumber == 3, "We removed the wrong episode");
}
[Test]
public async Task RequestShow_NewSeasonRequest_IsSuccessful()
{
SetupMockData();
var req = new ChildRequests
{
RequestType = RequestType.TvShow,
SeasonRequests = new List<SeasonRequests>
{
new SeasonRequests
{
Episodes = new List<EpisodeRequests>
{
new EpisodeRequests
{
Id = 1,
EpisodeNumber = 1,
},
new EpisodeRequests
{
Id = 2,
EpisodeNumber = 2,
},
new EpisodeRequests
{
Id = 3,
EpisodeNumber = 3,
},
},
SeasonNumber = 2
}
},
Id = 1,
};
var result = await Rule.Execute(req);
Assert.That(result.Success, Is.True);
}
private void SetupMockData()
{
var childRequests = new List<PlexServerContent>
{
new PlexServerContent
{
Type = PlexMediaTypeEntity.Show,
TheMovieDbId = "1",
Title = "Test",
ReleaseYear = "2001",
Episodes = new List<PlexEpisode>
{
new PlexEpisode
{
EpisodeNumber = 1,
Id = 1,
SeasonNumber = 1,
},
new PlexEpisode
{
EpisodeNumber = 2,
Id = 2,
SeasonNumber = 1,
},
}
}
};
PlexContentRepo.Setup(x => x.GetAll()).Returns(childRequests.AsQueryable().BuildMock().Object);
}
}
}

@ -0,0 +1,215 @@
using MockQueryable.Moq;
using Moq;
using NUnit.Framework;
using Ombi.Core.Rule.Rules.Request;
using Ombi.Store.Entities;
using Ombi.Store.Entities.Requests;
using Ombi.Store.Repository.Requests;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Ombi.Core.Tests.Rule.Request
{
[TestFixture]
public class ExistingTvRequestRuleTests
{
private ExistingTvRequestRule Rule;
private Mock<ITvRequestRepository> TvRequestRepo;
[SetUp]
public void SetUp()
{
TvRequestRepo = new Mock<ITvRequestRepository>();
Rule = new ExistingTvRequestRule(TvRequestRepo.Object);
}
[Test]
public async Task RequestShow_DoesNotExistAtAll_IsSuccessful()
{
TvRequestRepo.Setup(x => x.GetChild()).Returns(new List<ChildRequests>().AsQueryable().BuildMock().Object);
var req = new ChildRequests
{
SeasonRequests = new List<SeasonRequests>
{
new SeasonRequests
{
Episodes = new List<EpisodeRequests>
{
new EpisodeRequests
{
Id = 1,
EpisodeNumber = 1,
}
},
SeasonNumber = 1
}
}
};
var result = await Rule.Execute(req);
Assert.That(result.Success, Is.True);
}
[Test]
public async Task RequestShow_AllEpisodesAreaRequested_IsNotSuccessful()
{
SetupMockData();
var req = new ChildRequests
{
SeasonRequests = new List<SeasonRequests>
{
new SeasonRequests
{
Episodes = new List<EpisodeRequests>
{
new EpisodeRequests
{
Id = 1,
EpisodeNumber = 1,
},
new EpisodeRequests
{
Id = 1,
EpisodeNumber = 2,
},
},
SeasonNumber = 1
}
},
Id = 1,
};
var result = await Rule.Execute(req);
Assert.That(result.Success, Is.False);
}
[Test]
public async Task RequestShow_SomeEpisodesAreaRequested_IsSuccessful()
{
SetupMockData();
var req = new ChildRequests
{
RequestType = RequestType.TvShow,
SeasonRequests = new List<SeasonRequests>
{
new SeasonRequests
{
Episodes = new List<EpisodeRequests>
{
new EpisodeRequests
{
Id = 1,
EpisodeNumber = 1,
},
new EpisodeRequests
{
Id = 2,
EpisodeNumber = 2,
},
new EpisodeRequests
{
Id = 3,
EpisodeNumber = 3,
},
},
SeasonNumber = 1
}
},
Id = 1,
};
var result = await Rule.Execute(req);
Assert.That(result.Success, Is.True);
var episodes = req.SeasonRequests.SelectMany(x => x.Episodes);
Assert.That(episodes.Count() == 1, "We didn't remove the episodes that have already been requested!");
Assert.That(episodes.First().EpisodeNumber == 3, "We removed the wrong episode");
}
[Test]
public async Task RequestShow_NewSeasonRequest_IsSuccessful()
{
SetupMockData();
var req = new ChildRequests
{
RequestType = RequestType.TvShow,
SeasonRequests = new List<SeasonRequests>
{
new SeasonRequests
{
Episodes = new List<EpisodeRequests>
{
new EpisodeRequests
{
Id = 1,
EpisodeNumber = 1,
},
new EpisodeRequests
{
Id = 2,
EpisodeNumber = 2,
},
new EpisodeRequests
{
Id = 3,
EpisodeNumber = 3,
},
},
SeasonNumber = 2
}
},
Id = 1,
};
var result = await Rule.Execute(req);
Assert.That(result.Success, Is.True);
}
private void SetupMockData()
{
var childRequests = new List<ChildRequests>
{
new ChildRequests
{
ParentRequest = new TvRequests
{
Id = 1,
ExternalProviderId = 1,
},
SeasonRequests = new List<SeasonRequests>
{
new SeasonRequests
{
Id = 1,
SeasonNumber = 1,
Episodes = new List<EpisodeRequests>
{
new EpisodeRequests
{
Id = 1,
EpisodeNumber = 1,
},
new EpisodeRequests
{
Id = 1,
EpisodeNumber = 2,
}
}
}
}
}
};
TvRequestRepo.Setup(x => x.GetChild()).Returns(childRequests.AsQueryable().BuildMock().Object);
}
}
}

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

@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Ombi.Api.Plex;
using Ombi.Api.Plex.Models;
using Ombi.Api.Plex.Models.OAuth;
@ -11,24 +13,37 @@ namespace Ombi.Core.Authentication
{
public class PlexOAuthManager : IPlexOAuthManager
{
public PlexOAuthManager(IPlexApi api, ISettingsService<CustomizationSettings> settings)
public PlexOAuthManager(IPlexApi api, ISettingsService<CustomizationSettings> settings, ILogger<PlexOAuthManager> logger)
{
_api = api;
_customizationSettingsService = settings;
_logger = logger;
}
private readonly IPlexApi _api;
private readonly ISettingsService<CustomizationSettings> _customizationSettingsService;
private readonly ILogger _logger;
public async Task<string> GetAccessTokenFromPin(int pinId)
{
var pin = await _api.GetPin(pinId);
if (pin.expiresAt < DateTime.UtcNow)
if (pin.Errors != null)
{
foreach (var err in pin.Errors?.errors ?? new List<OAuthErrors>())
{
_logger.LogError($"Code: '{err.code}' : '{err.message}'");
}
return string.Empty;
}
if (pin.Result.expiresIn <= 0)
{
_logger.LogError("Pin has expired");
return string.Empty;
}
return pin.authToken;
return pin.Result.authToken;
}
public async Task<PlexAccount> GetAccount(string accessToken)

@ -48,6 +48,8 @@ namespace Ombi.Core.Engine
protected readonly ISettingsService<OmbiSettings> OmbiSettings;
protected readonly IRepository<RequestSubscription> _subscriptionRepository;
private bool _demo = DemoSingleton.Instance.Demo;
protected async Task<Dictionary<int, MovieRequests>> GetMovieRequests()
{
var now = DateTime.Now.Ticks;
@ -125,7 +127,7 @@ namespace Ombi.Core.Engine
UserId = user.Id
};
}
var settings = await Cache.GetOrAdd(CacheKeys.OmbiSettings, async () => await OmbiSettings.GetSettingsAsync());
var settings = await Cache.GetOrAddAsync(CacheKeys.OmbiSettings, () => OmbiSettings.GetSettingsAsync());
var result = new HideResult
{
Hide = settings.HideRequestsUsers,
@ -193,6 +195,23 @@ namespace Ombi.Core.Engine
return ombiSettings ?? (ombiSettings = await OmbiSettings.GetSettingsAsync());
}
protected bool DemoCheck(string title)
{
if (!title.HasValue())
{
return false;
}
if (_demo)
{
if (ExcludedDemo.ExcludedContent.Any(x => title.Contains(x, System.Globalization.CompareOptions.OrdinalIgnoreCase)))
{
return true;
}
return false;
}
return false;
}
public class HideResult
{
public bool Hide { get; set; }

@ -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);
@ -28,5 +28,6 @@ namespace Ombi.Core.Engine.Interfaces
Task<MovieFullInfoViewModel> GetMovieInfoByImdbId(string imdbId, CancellationToken requestAborted);
Task<IEnumerable<StreamingData>> GetStreamInformation(int movieDbId, CancellationToken cancellationToken);
Task<IEnumerable<SearchMovieViewModel>> RecentlyRequestedMovies(int currentlyLoaded, int toLoad, CancellationToken cancellationToken);
Task<IEnumerable<SearchMovieViewModel>> SeasonalList(int currentPosition, int amountToLoad, CancellationToken cancellationToken);
}
}

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Ombi.Core.Models.Requests;
using Ombi.Core.Models.UI;
@ -11,6 +12,7 @@ namespace Ombi.Core.Engine.Interfaces
Task<RequestEngineResult> RequestMovie(MovieRequestViewModel model);
Task<IEnumerable<MovieRequests>> SearchMovieRequest(string search);
Task<RequestEngineResult> RequestCollection(int collectionId, CancellationToken cancellationToken);
Task RemoveMovieRequest(int requestId);
Task RemoveAllMovieRequests();

@ -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,8 +11,9 @@ 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);
Task<IEnumerable<SearchFullInfoTvShowViewModel>> RecentlyRequestedShows(int currentlyLoaded, int toLoad, CancellationToken cancellationToken);
}
}

@ -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
{
@ -29,7 +30,7 @@ namespace Ombi.Core.Engine
public MovieRequestEngine(IMovieDbApi movieApi, IRequestServiceMain requestService, IPrincipal user,
INotificationHelper helper, IRuleEvaluator r, IMovieSender sender, ILogger<MovieRequestEngine> log,
OmbiUserManager manager, IRepository<RequestLog> rl, ICacheService cache,
ISettingsService<OmbiSettings> ombiSettings, IRepository<RequestSubscription> sub)
ISettingsService<OmbiSettings> ombiSettings, IRepository<RequestSubscription> sub, IMediaCacheService mediaCacheService)
: base(user, requestService, r, manager, cache, ombiSettings, sub)
{
MovieApi = movieApi;
@ -37,6 +38,7 @@ namespace Ombi.Core.Engine
Sender = sender;
Logger = log;
_requestLog = rl;
_mediaCacheService = mediaCacheService;
}
private IMovieDbApi MovieApi { get; }
@ -44,6 +46,7 @@ namespace Ombi.Core.Engine
private IMovieSender Sender { get; }
private ILogger<MovieRequestEngine> Logger { get; }
private readonly IRepository<RequestLog> _requestLog;
private readonly IMediaCacheService _mediaCacheService;
/// <summary>
/// Requests the movie.
@ -70,7 +73,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
{
@ -370,7 +373,6 @@ namespace Ombi.Core.Engine
};
}
public async Task<RequestEngineResult> UpdateAdvancedOptions(MediaAdvancedOptions options)
{
var request = await MovieRepository.Find(options.RequestId);
@ -526,6 +528,7 @@ namespace Ombi.Core.Engine
// We are denying a request
await NotificationHelper.Notify(request, NotificationType.RequestDeclined);
await MovieRepository.Update(request);
await _mediaCacheService.Purge();
return new RequestEngineResult
{
@ -549,12 +552,42 @@ 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);
}
await _mediaCacheService.Purge();
return await ProcessSendingMovie(request);
}
public async Task<RequestEngineResult> RequestCollection(int collectionId, CancellationToken cancellationToken)
{
var langCode = await DefaultLanguageCode(null);
var collections = await Cache.GetOrAddAsync($"GetCollection{collectionId}{langCode}",
() => MovieApi.GetCollection(langCode, collectionId, cancellationToken), DateTimeOffset.Now.AddDays(1));
var results = new List<RequestEngineResult>();
foreach (var collection in collections.parts)
{
results.Add(await RequestMovie(new MovieRequestViewModel
{
TheMovieDbId = collection.id
}));
}
if (results.All(x => x.IsError))
{
new RequestEngineResult { Result = false, ErrorMessage = $"The whole collection {collections.name} Is already monitored or requested!" };
}
return new RequestEngineResult { Result = true, Message = $"The collection {collections.name} has been successfully added!", RequestId = results.FirstOrDefault().RequestId};
}
private async Task<RequestEngineResult> ProcessSendingMovie(MovieRequests request)
{
if (request.Approved)
{
var result = await Sender.Send(request);
@ -609,6 +642,7 @@ namespace Ombi.Core.Engine
results.RootPathOverride = request.RootPathOverride;
await MovieRepository.Update(results);
await _mediaCacheService.Purge();
return results;
}
@ -621,12 +655,14 @@ namespace Ombi.Core.Engine
{
var request = await MovieRepository.GetAll().FirstOrDefaultAsync(x => x.Id == requestId);
await MovieRepository.Delete(request);
await _mediaCacheService.Purge();
}
public async Task RemoveAllMovieRequests()
{
var request = MovieRepository.GetAll();
await MovieRepository.DeleteRange(request);
await _mediaCacheService.Purge();
}
public async Task<bool> UserHasRequest(string userId)
@ -634,6 +670,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);
@ -647,6 +698,7 @@ namespace Ombi.Core.Engine
request.Available = false;
await MovieRepository.Update(request);
await _mediaCacheService.Purge();
return new RequestEngineResult
{
@ -670,6 +722,7 @@ namespace Ombi.Core.Engine
request.MarkedAsAvailable = DateTime.Now;
await NotificationHelper.Notify(request, NotificationType.RequestAvailable);
await MovieRepository.Update(request);
await _mediaCacheService.Purge();
return new RequestEngineResult
{
@ -682,12 +735,14 @@ 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);
}
await _mediaCacheService.Purge();
await _requestLog.Add(new RequestLog
{
UserId = requestOnBehalf.HasValue() ? requestOnBehalf : (await GetUser()).Id,

@ -45,9 +45,9 @@ namespace Ombi.Core.Engine
public async Task<SearchMovieViewModel> LookupImdbInformation(int theMovieDbId, string langCode = null)
{
langCode = await DefaultLanguageCode(langCode);
var movieInfo = await Cache.GetOrAdd(nameof(LookupImdbInformation) + langCode + theMovieDbId,
async () => await MovieApi.GetMovieInformationWithExtraInfo(theMovieDbId, langCode),
DateTime.Now.AddHours(12));
var movieInfo = await Cache.GetOrAddAsync(nameof(LookupImdbInformation) + langCode + theMovieDbId,
() => MovieApi.GetMovieInformationWithExtraInfo(theMovieDbId, langCode),
DateTimeOffset.Now.AddHours(12));
var viewMovie = Mapper.Map<SearchMovieViewModel>(movieInfo);
return await ProcessSingleMovie(viewMovie, true);
@ -121,11 +121,11 @@ namespace Ombi.Core.Engine
public async Task<IEnumerable<SearchMovieViewModel>> PopularMovies()
{
var result = await Cache.GetOrAdd(CacheKeys.PopularMovies, async () =>
var result = await Cache.GetOrAddAsync(CacheKeys.PopularMovies, async () =>
{
var langCode = await DefaultLanguageCode(null);
return await MovieApi.PopularMovies(langCode);
}, DateTime.Now.AddHours(12));
}, DateTimeOffset.Now.AddHours(12));
if (result != null)
{
return await TransformMovieResultsToResponse(result.Take(ResultLimit)); // Take x to stop us overloading the API
@ -139,11 +139,11 @@ namespace Ombi.Core.Engine
/// <returns></returns>
public async Task<IEnumerable<SearchMovieViewModel>> TopRatedMovies()
{
var result = await Cache.GetOrAdd(CacheKeys.TopRatedMovies, async () =>
var result = await Cache.GetOrAddAsync(CacheKeys.TopRatedMovies, async () =>
{
var langCode = await DefaultLanguageCode(null);
return await MovieApi.TopRated(langCode);
}, DateTime.Now.AddHours(12));
}, DateTimeOffset.Now.AddHours(12));
if (result != null)
{
return await TransformMovieResultsToResponse(result.Take(ResultLimit)); // Take x to stop us overloading the API
@ -157,11 +157,11 @@ namespace Ombi.Core.Engine
/// <returns></returns>
public async Task<IEnumerable<SearchMovieViewModel>> UpcomingMovies()
{
var result = await Cache.GetOrAdd(CacheKeys.UpcomingMovies, async () =>
var result = await Cache.GetOrAddAsync(CacheKeys.UpcomingMovies, async () =>
{
var langCode = await DefaultLanguageCode(null);
return await MovieApi.Upcoming(langCode);
}, DateTime.Now.AddHours(12));
}, DateTimeOffset.Now.AddHours(12));
if (result != null)
{
Logger.LogDebug("Search Result: {result}", result);
@ -176,11 +176,11 @@ namespace Ombi.Core.Engine
/// <returns></returns>
public async Task<IEnumerable<SearchMovieViewModel>> NowPlayingMovies()
{
var result = await Cache.GetOrAdd(CacheKeys.NowPlayingMovies, async () =>
var result = await Cache.GetOrAddAsync(CacheKeys.NowPlayingMovies, async () =>
{
var langCode = await DefaultLanguageCode(null);
return await MovieApi.NowPlaying(langCode);
}, DateTime.Now.AddHours(12));
}, DateTimeOffset.Now.AddHours(12));
if (result != null)
{
return await TransformMovieResultsToResponse(result.Take(ResultLimit)); // Take x to stop us overloading the API

@ -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,28 +25,35 @@ using Ombi.Settings.Settings.Models;
using Ombi.Store.Entities.Requests;
using Ombi.Store.Repository;
using Ombi.Core.Models;
using System.Threading;
using Microsoft.Extensions.Logging;
namespace Ombi.Core.Engine
{
public class TvRequestEngine : BaseMediaEngine, ITvRequestEngine
{
public TvRequestEngine(ITvMazeApi tvApi, IMovieDbApi movApi, IRequestServiceMain requestService, IPrincipal user,
INotificationHelper helper, IRuleEvaluator rule, OmbiUserManager manager,
INotificationHelper helper, IRuleEvaluator rule, OmbiUserManager manager, ILogger<TvRequestEngine> logger,
ITvSender sender, IRepository<RequestLog> rl, ISettingsService<OmbiSettings> settings, ICacheService cache,
IRepository<RequestSubscription> sub) : base(user, requestService, rule, manager, cache, settings, sub)
IRepository<RequestSubscription> sub, IMediaCacheService mediaCacheService) : base(user, requestService, rule, manager, cache, settings, sub)
{
TvApi = tvApi;
MovieDbApi = movApi;
NotificationHelper = helper;
_logger = logger;
TvSender = sender;
_requestLog = rl;
_mediaCacheService = mediaCacheService;
}
private INotificationHelper NotificationHelper { get; }
private ITvMazeApi TvApi { get; }
private IMovieDbApi MovieDbApi { get; }
private ITvSender TvSender { get; }
private readonly ILogger<TvRequestEngine> _logger;
private readonly IRepository<RequestLog> _requestLog;
private readonly IMediaCacheService _mediaCacheService;
public async Task<RequestEngineResult> RequestTvShow(TvRequestViewModel tv)
{
@ -68,7 +75,7 @@ namespace Ombi.Core.Engine
}
}
var tvBuilder = new TvShowRequestBuilder(TvApi, MovieDbApi);
var tvBuilder = new TvShowRequestBuilder(TvApi, MovieDbApi, _logger);
(await tvBuilder
.GetShowInfo(tv.TvDbId))
.CreateTvList(tv)
@ -164,7 +171,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 +257,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);
}
@ -324,6 +331,7 @@ namespace Ombi.Core.Engine
Collection = allRequests
};
}
public async Task<IEnumerable<TvRequests>> GetRequests()
{
var shouldHide = await HideFromOtherUsers();
@ -343,7 +351,6 @@ namespace Ombi.Core.Engine
return allRequests;
}
public async Task<RequestsViewModel<ChildRequests>> GetRequests(int count, int position, string sortProperty, string sortOrder)
{
var shouldHide = await HideFromOtherUsers();
@ -399,7 +406,7 @@ namespace Ombi.Core.Engine
};
}
public async Task<RequestsViewModel<ChildRequests>> GetRequests(int count, int position, string sortProperty, string sortOrder, RequestStatus status)
public async Task<RequestsViewModel<ChildRequests>> GetRequests(int count, int position, string sortProperty, string sortOrder, RequestStatus status)
{
var shouldHide = await HideFromOtherUsers();
List<ChildRequests> allRequests;
@ -471,6 +478,7 @@ namespace Ombi.Core.Engine
Total = total,
};
}
public async Task<RequestsViewModel<ChildRequests>> GetUnavailableRequests(int count, int position, string sortProperty, string sortOrder)
{
var shouldHide = await HideFromOtherUsers();
@ -524,7 +532,6 @@ namespace Ombi.Core.Engine
};
}
public async Task<IEnumerable<TvRequests>> GetRequestsLite()
{
var shouldHide = await HideFromOtherUsers();
@ -694,6 +701,7 @@ namespace Ombi.Core.Engine
}
await TvRepository.UpdateChild(request);
await _mediaCacheService.Purge();
if (request.Approved)
{
@ -720,6 +728,7 @@ namespace Ombi.Core.Engine
request.Denied = true;
request.DeniedReason = reason;
await TvRepository.UpdateChild(request);
await _mediaCacheService.Purge();
await NotificationHelper.Notify(request, NotificationType.RequestDeclined);
return new RequestEngineResult
{
@ -730,6 +739,7 @@ namespace Ombi.Core.Engine
public async Task<ChildRequests> UpdateChildRequest(ChildRequests request)
{
await TvRepository.UpdateChild(request);
await _mediaCacheService.Purge();
return request;
}
@ -749,12 +759,14 @@ namespace Ombi.Core.Engine
}
await TvRepository.Db.SaveChangesAsync();
await _mediaCacheService.Purge();
}
public async Task RemoveTvRequest(int requestId)
{
var request = await TvRepository.Get().FirstOrDefaultAsync(x => x.Id == requestId);
await TvRepository.Delete(request);
await _mediaCacheService.Purge();
}
public async Task<bool> UserHasRequest(string userId)
@ -781,6 +793,7 @@ namespace Ombi.Core.Engine
}
}
await TvRepository.UpdateChild(request);
await _mediaCacheService.Purge();
return new RequestEngineResult
{
Result = true,
@ -809,6 +822,7 @@ namespace Ombi.Core.Engine
}
await TvRepository.UpdateChild(request);
await NotificationHelper.Notify(request, NotificationType.RequestAvailable);
await _mediaCacheService.Purge();
return new RequestEngineResult
{
Result = true,
@ -883,22 +897,25 @@ namespace Ombi.Core.Engine
return await AfterRequest(model.ChildRequests.FirstOrDefault(), requestOnBehalf);
}
private static List<ChildRequests> SortEpisodes(List<ChildRequests> items)
public async Task<RequestEngineResult> ReProcessRequest(int requestId, CancellationToken cancellationToken)
{
foreach (var value in items)
var request = await TvRepository.GetChild().FirstOrDefaultAsync(x => x.Id == requestId, cancellationToken);
if (request == null)
{
foreach (var requests in value.SeasonRequests)
return new RequestEngineResult
{
requests.Episodes = requests.Episodes.OrderBy(x => x.EpisodeNumber).ToList();
}
Result = false,
ErrorMessage = "Request does not exist"
};
}
return items;
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);
@ -912,7 +929,13 @@ namespace Ombi.Core.Engine
RequestType = RequestType.TvShow,
EpisodeCount = model.SeasonRequests.Select(m => m.Episodes.Count).Sum(),
});
await _mediaCacheService.Purge();
return await ProcessSendingShow(model);
}
private async Task<RequestEngineResult> ProcessSendingShow(ChildRequests model)
{
if (model.Approved)
{
// Autosend
@ -997,6 +1020,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);

@ -77,16 +77,16 @@ namespace Ombi.Core.Engine
public async Task<SearchTvShowViewModel> GetShowInformation(string tvdbid, CancellationToken token)
{
var show = await Cache.GetOrAdd(nameof(GetShowInformation) + tvdbid,
async () => await TvMazeApi.ShowLookupByTheTvDbId(int.Parse(tvdbid)), DateTime.Now.AddHours(12));
var show = await Cache.GetOrAddAsync(nameof(GetShowInformation) + tvdbid,
() => TvMazeApi.ShowLookupByTheTvDbId(int.Parse(tvdbid)), DateTimeOffset.Now.AddHours(12));
if (show == null)
{
// We don't have enough information
return null;
}
var episodes = await Cache.GetOrAdd("TvMazeEpisodeLookup" + show.id,
async () => await TvMazeApi.EpisodeLookup(show.id), DateTime.Now.AddHours(12));
var episodes = await Cache.GetOrAddAsync("TvMazeEpisodeLookup" + show.id,
() => TvMazeApi.EpisodeLookup(show.id), DateTimeOffset.Now.AddHours(12));
if (episodes == null || !episodes.Any())
{
// We don't have enough information
@ -133,7 +133,7 @@ namespace Ombi.Core.Engine
public async Task<IEnumerable<SearchTvShowViewModel>> Popular()
{
var result = await Cache.GetOrAdd(CacheKeys.PopularTv, async () => await TraktApi.GetPopularShows(null, ResultLimit), DateTime.Now.AddHours(12));
var result = await Cache.GetOrAddAsync(CacheKeys.PopularTv, () => TraktApi.GetPopularShows(null, ResultLimit), DateTimeOffset.Now.AddHours(12));
var processed = ProcessResults(result);
return await processed;
}
@ -146,8 +146,8 @@ namespace Ombi.Core.Engine
var results = new List<TraktShow>();
foreach (var pagesToLoad in pages)
{
var apiResult = await Cache.GetOrAdd(nameof(Popular) + langCode + pagesToLoad.Page,
async () => await TraktApi.GetPopularShows(pagesToLoad.Page, ResultLimit), DateTime.Now.AddHours(12));
var apiResult = await Cache.GetOrAddAsync(nameof(Popular) + langCode + pagesToLoad.Page,
() => TraktApi.GetPopularShows(pagesToLoad.Page, ResultLimit), DateTimeOffset.Now.AddHours(12));
results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take));
}
@ -158,7 +158,7 @@ namespace Ombi.Core.Engine
public async Task<IEnumerable<SearchTvShowViewModel>> Anticipated()
{
var result = await Cache.GetOrAdd(CacheKeys.AnticipatedTv, async () => await TraktApi.GetAnticipatedShows(null, ResultLimit), DateTime.Now.AddHours(12));
var result = await Cache.GetOrAddAsync(CacheKeys.AnticipatedTv, () => TraktApi.GetAnticipatedShows(null, ResultLimit), DateTimeOffset.Now.AddHours(12));
var processed = ProcessResults(result);
return await processed;
}
@ -171,8 +171,8 @@ namespace Ombi.Core.Engine
var results = new List<TraktShow>();
foreach (var pagesToLoad in pages)
{
var apiResult = await Cache.GetOrAdd(nameof(Anticipated) + langCode + pagesToLoad.Page,
async () => await TraktApi.GetAnticipatedShows(pagesToLoad.Page, ResultLimit), DateTime.Now.AddHours(12));
var apiResult = await Cache.GetOrAddAsync(nameof(Anticipated) + langCode + pagesToLoad.Page,
() => TraktApi.GetAnticipatedShows(pagesToLoad.Page, ResultLimit), DateTimeOffset.Now.AddHours(12));
results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take));
}
var processed = ProcessResults(results);
@ -181,7 +181,7 @@ namespace Ombi.Core.Engine
public async Task<IEnumerable<SearchTvShowViewModel>> Trending()
{
var result = await Cache.GetOrAdd(CacheKeys.TrendingTv, async () => await TraktApi.GetTrendingShows(null, ResultLimit), DateTime.Now.AddHours(12));
var result = await Cache.GetOrAddAsync(CacheKeys.TrendingTv, () => TraktApi.GetTrendingShows(null, ResultLimit), DateTimeOffset.Now.AddHours(12));
var processed = ProcessResults(result);
return await processed;
}
@ -195,8 +195,8 @@ namespace Ombi.Core.Engine
var results = new List<TraktShow>();
foreach (var pagesToLoad in pages)
{
var apiResult = await Cache.GetOrAdd(nameof(Trending) + langCode + pagesToLoad.Page,
async () => await TraktApi.GetTrendingShows(pagesToLoad.Page, ResultLimit), DateTime.Now.AddHours(12));
var apiResult = await Cache.GetOrAddAsync(nameof(Trending) + langCode + pagesToLoad.Page,
() => TraktApi.GetTrendingShows(pagesToLoad.Page, ResultLimit), DateTimeOffset.Now.AddHours(12));
results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take));
}
var processed = ProcessResults(results);

@ -19,6 +19,7 @@ using Ombi.Store.Repository;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
@ -29,7 +30,7 @@ namespace Ombi.Core.Engine.V2
{
public MovieSearchEngineV2(IPrincipal identity, IRequestServiceMain service, IMovieDbApi movApi, IMapper mapper,
ILogger<MovieSearchEngineV2> logger, IRuleEvaluator r, OmbiUserManager um, ICacheService mem, ISettingsService<OmbiSettings> s, IRepository<RequestSubscription> sub,
ISettingsService<CustomizationSettings> customizationSettings, IMovieRequestEngine movieRequestEngine)
ISettingsService<CustomizationSettings> customizationSettings, IMovieRequestEngine movieRequestEngine, IHttpClientFactory httpClientFactory)
: base(identity, service, r, um, mem, s, sub)
{
MovieApi = movApi;
@ -37,6 +38,7 @@ namespace Ombi.Core.Engine.V2
Logger = logger;
_customizationSettings = customizationSettings;
_movieRequestEngine = movieRequestEngine;
_client = httpClientFactory.CreateClient();
}
private IMovieDbApi MovieApi { get; }
@ -44,12 +46,13 @@ namespace Ombi.Core.Engine.V2
private ILogger Logger { get; }
private readonly ISettingsService<CustomizationSettings> _customizationSettings;
private readonly IMovieRequestEngine _movieRequestEngine;
private readonly HttpClient _client;
public async Task<MovieFullInfoViewModel> GetFullMovieInformation(int theMovieDbId, CancellationToken cancellationToken, string langCode = null)
{
langCode = await DefaultLanguageCode(langCode);
var movieInfo = await Cache.GetOrAdd(nameof(GetFullMovieInformation) + theMovieDbId + langCode,
async () => await MovieApi.GetFullMovieInfo(theMovieDbId, cancellationToken, langCode), DateTime.Now.AddHours(12), cancellationToken);
var movieInfo = await Cache.GetOrAddAsync(nameof(GetFullMovieInformation) + theMovieDbId + langCode,
() => MovieApi.GetFullMovieInfo(theMovieDbId, cancellationToken, langCode), DateTimeOffset.Now.AddHours(12));
return await ProcessSingleMovie(movieInfo);
}
@ -58,8 +61,8 @@ namespace Ombi.Core.Engine.V2
{
langCode = await DefaultLanguageCode(langCode);
var request = await RequestService.MovieRequestService.Find(requestId);
var movieInfo = await Cache.GetOrAdd(nameof(GetFullMovieInformation) + request.TheMovieDbId + langCode,
async () => await MovieApi.GetFullMovieInfo(request.TheMovieDbId, cancellationToken, langCode), DateTime.Now.AddHours(12), cancellationToken);
var movieInfo = await Cache.GetOrAddAsync(nameof(GetFullMovieInformation) + request.TheMovieDbId + langCode,
() => MovieApi.GetFullMovieInfo(request.TheMovieDbId, cancellationToken, langCode), DateTimeOffset.Now.AddHours(12));
return await ProcessSingleMovie(movieInfo);
}
@ -67,8 +70,8 @@ namespace Ombi.Core.Engine.V2
public async Task<MovieCollectionsViewModel> GetCollection(int collectionId, CancellationToken cancellationToken, string langCode = null)
{
langCode = await DefaultLanguageCode(langCode);
var collections = await Cache.GetOrAdd(nameof(GetCollection) + collectionId + langCode,
async () => await MovieApi.GetCollection(langCode, collectionId, cancellationToken), DateTime.Now.AddDays(1), cancellationToken);
var collections = await Cache.GetOrAddAsync(nameof(GetCollection) + collectionId + langCode,
() => MovieApi.GetCollection(langCode, collectionId, cancellationToken), DateTimeOffset.Now.AddDays(1));
var c = await ProcessCollection(collections);
c.Collection = c.Collection.OrderBy(x => x.ReleaseDate).ToList();
@ -105,11 +108,11 @@ namespace Ombi.Core.Engine.V2
public async Task<IEnumerable<SearchMovieViewModel>> PopularMovies()
{
var result = await Cache.GetOrAdd(CacheKeys.PopularMovies, async () =>
var result = await Cache.GetOrAddAsync(CacheKeys.PopularMovies, async () =>
{
var langCode = await DefaultLanguageCode(null);
return await MovieApi.PopularMovies(langCode);
}, DateTime.Now.AddHours(12));
}, DateTimeOffset.Now.AddHours(12));
if (result != null)
{
return await TransformMovieResultsToResponse(result.Shuffle().Take(ResultLimit)); // Take x to stop us overloading the API
@ -124,17 +127,17 @@ 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);
var results = new List<MovieDbSearchResult>();
foreach (var pagesToLoad in pages)
{
var apiResult = await Cache.GetOrAdd(nameof(PopularMovies) + pagesToLoad.Page + langCode,
async () => await MovieApi.PopularMovies(langCode, pagesToLoad.Page, cancellationToken), DateTime.Now.AddHours(12), cancellationToken);
var apiResult = await Cache.GetOrAddAsync(nameof(PopularMovies) + pagesToLoad.Page + langCode,
() => MovieApi.PopularMovies(langCode, pagesToLoad.Page, cancellationToken), DateTimeOffset.Now.AddHours(12));
results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take));
}
return await TransformMovieResultsToResponse(results);
@ -146,11 +149,11 @@ namespace Ombi.Core.Engine.V2
/// <returns></returns>
public async Task<IEnumerable<SearchMovieViewModel>> TopRatedMovies()
{
var result = await Cache.GetOrAdd(CacheKeys.TopRatedMovies, async () =>
var result = await Cache.GetOrAddAsync(CacheKeys.TopRatedMovies, async () =>
{
var langCode = await DefaultLanguageCode(null);
return await MovieApi.TopRated(langCode);
}, DateTime.Now.AddHours(12));
}, DateTimeOffset.Now.AddHours(12));
if (result != null)
{
return await TransformMovieResultsToResponse(result.Shuffle().Take(ResultLimit)); // Take x to stop us overloading the API
@ -167,8 +170,8 @@ namespace Ombi.Core.Engine.V2
var results = new List<MovieDbSearchResult>();
foreach (var pagesToLoad in pages)
{
var apiResult = await Cache.GetOrAdd(nameof(TopRatedMovies) + pagesToLoad.Page + langCode,
async () => await MovieApi.TopRated(langCode, pagesToLoad.Page), DateTime.Now.AddHours(12));
var apiResult = await Cache.GetOrAddAsync(nameof(TopRatedMovies) + pagesToLoad.Page + langCode,
() => MovieApi.TopRated(langCode, pagesToLoad.Page), DateTimeOffset.Now.AddHours(12));
results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take));
}
return await TransformMovieResultsToResponse(results);
@ -183,8 +186,32 @@ namespace Ombi.Core.Engine.V2
var results = new List<MovieDbSearchResult>();
foreach (var pagesToLoad in pages)
{
var apiResult = await Cache.GetOrAdd(nameof(NowPlayingMovies) + pagesToLoad.Page + langCode,
async () => await MovieApi.NowPlaying(langCode, pagesToLoad.Page), DateTime.Now.AddHours(12));
var apiResult = await Cache.GetOrAddAsync(nameof(NowPlayingMovies) + pagesToLoad.Page + langCode,
() => MovieApi.NowPlaying(langCode, pagesToLoad.Page), DateTimeOffset.Now.AddHours(12));
results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take));
}
return await TransformMovieResultsToResponse(results);
}
public async Task<IEnumerable<SearchMovieViewModel>> SeasonalList(int currentPosition, int amountToLoad, CancellationToken cancellationToken)
{
var langCode = await DefaultLanguageCode(null);
var result = await _client.GetAsync("https://raw.githubusercontent.com/Ombi-app/Ombi.News/main/Seasonal.md");
var keyWordIds = await result.Content.ReadAsStringAsync();
if (string.IsNullOrEmpty(keyWordIds) || keyWordIds.Equals("\n"))
{
return new List<SearchMovieViewModel>();
}
var pages = PaginationHelper.GetNextPages(currentPosition, amountToLoad, _theMovieDbMaxPageItems);
var results = new List<MovieDbSearchResult>();
foreach (var pagesToLoad in pages)
{
var apiResult = await Cache.GetOrAddAsync(nameof(SeasonalList) + pagesToLoad.Page + langCode + keyWordIds,
() => MovieApi.GetMoviesViaKeywords(keyWordIds, langCode, cancellationToken, pagesToLoad.Page), DateTimeOffset.Now.AddHours(12));
results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take));
}
return await TransformMovieResultsToResponse(results);
@ -200,16 +227,16 @@ namespace Ombi.Core.Engine.V2
var results = new List<MovieResponseDto>();
var requestResult = await Cache.GetOrAdd(nameof(RecentlyRequestedMovies) + "Requests" + toLoad + langCode,
var requestResult = await Cache.GetOrAddAsync(nameof(RecentlyRequestedMovies) + "Requests" + toLoad + langCode,
async () =>
{
return await _movieRequestEngine.GetRequests(toLoad, currentlyLoaded, new Models.UI.OrderFilterModel
{
OrderType = OrderType.RequestedDateDesc
});
}, DateTime.Now.AddMinutes(15), cancellationToken);
}, DateTimeOffset.Now.AddMinutes(15));
var movieDBResults = await Cache.GetOrAdd(nameof(RecentlyRequestedMovies) + toLoad + langCode,
var movieDBResults = await Cache.GetOrAddAsync(nameof(RecentlyRequestedMovies) + toLoad + langCode,
async () =>
{
var responses = new List<MovieResponseDto>();
@ -218,7 +245,7 @@ namespace Ombi.Core.Engine.V2
responses.Add(await MovieApi.GetMovieInformation(movie.TheMovieDbId));
}
return responses;
}, DateTime.Now.AddHours(12), cancellationToken);
}, DateTimeOffset.Now.AddHours(12));
results.AddRange(movieDBResults);
@ -232,11 +259,11 @@ namespace Ombi.Core.Engine.V2
/// <returns></returns>
public async Task<IEnumerable<SearchMovieViewModel>> UpcomingMovies()
{
var result = await Cache.GetOrAdd(CacheKeys.UpcomingMovies, async () =>
var result = await Cache.GetOrAddAsync(CacheKeys.UpcomingMovies, async () =>
{
var langCode = await DefaultLanguageCode(null);
return await MovieApi.Upcoming(langCode);
}, DateTime.Now.AddHours(12));
}, DateTimeOffset.Now.AddHours(12));
if (result != null)
{
Logger.LogDebug("Search Result: {result}", result);
@ -254,8 +281,8 @@ namespace Ombi.Core.Engine.V2
var results = new List<MovieDbSearchResult>();
foreach (var pagesToLoad in pages)
{
var apiResult = await Cache.GetOrAdd(nameof(UpcomingMovies) + pagesToLoad.Page + langCode,
async () => await MovieApi.Upcoming(langCode, pagesToLoad.Page), DateTime.Now.AddHours(12));
var apiResult = await Cache.GetOrAddAsync(nameof(UpcomingMovies) + pagesToLoad.Page + langCode,
() => MovieApi.Upcoming(langCode, pagesToLoad.Page), DateTimeOffset.Now.AddHours(12));
results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take));
}
return await TransformMovieResultsToResponse(results);
@ -267,11 +294,11 @@ namespace Ombi.Core.Engine.V2
/// <returns></returns>
public async Task<IEnumerable<SearchMovieViewModel>> NowPlayingMovies()
{
var result = await Cache.GetOrAdd(CacheKeys.NowPlayingMovies, async () =>
var result = await Cache.GetOrAddAsync(CacheKeys.NowPlayingMovies, async () =>
{
var langCode = await DefaultLanguageCode(null);
return await MovieApi.NowPlaying(langCode);
}, DateTime.Now.AddHours(12));
}, DateTimeOffset.Now.AddHours(12));
if (result != null)
{
return await TransformMovieResultsToResponse(result.Shuffle().Take(ResultLimit)); // Take x to stop us overloading the API
@ -281,8 +308,8 @@ namespace Ombi.Core.Engine.V2
public async Task<ActorCredits> GetMoviesByActor(int actorId, string langCode)
{
var result = await Cache.GetOrAdd(nameof(GetMoviesByActor) + actorId + langCode,
async () => await MovieApi.GetActorMovieCredits(actorId, langCode));
var result = await Cache.GetOrAddAsync(nameof(GetMoviesByActor) + actorId + langCode,
() => MovieApi.GetActorMovieCredits(actorId, langCode), DateTimeOffset.Now.AddHours(12));
// Later we run this through the rules engine
return result;
}
@ -315,6 +342,12 @@ namespace Ombi.Core.Engine.V2
foreach (var movie in movies)
{
var result = await ProcessSingleMovie(movie);
if (DemoCheck(result.Title))
{
continue;
}
if (settings.HideAvailableFromDiscover && result.Available)
{
continue;
@ -333,6 +366,14 @@ namespace Ombi.Core.Engine.V2
private async Task<MovieFullInfoViewModel> ProcessSingleMovie(FullMovieInfo movie)
{
var viewMovie = Mapper.Map<SearchMovieViewModel>(movie);
var user = await GetUser();
var digitalReleaseDate = viewMovie.ReleaseDates?.Results?.FirstOrDefault(x => x.IsoCode == user.StreamingCountry);
if (digitalReleaseDate == null)
{
digitalReleaseDate = viewMovie.ReleaseDates?.Results?.FirstOrDefault(x => x.IsoCode == "US");
}
viewMovie.DigitalReleaseDate = digitalReleaseDate?.ReleaseDate?.FirstOrDefault(x => x.Type == ReleaseDateType.Digital)?.ReleaseDate;
await RunSearchRules(viewMovie);
// This requires the rules to be run first to populate the RequestId property
@ -348,6 +389,7 @@ namespace Ombi.Core.Engine.V2
mapped.JellyfinUrl = viewMovie.JellyfinUrl;
mapped.Subscribed = viewMovie.Subscribed;
mapped.ShowSubscribe = viewMovie.ShowSubscribe;
mapped.DigitalReleaseDate = viewMovie.DigitalReleaseDate;
return mapped;
}
@ -371,6 +413,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;
@ -382,12 +425,21 @@ namespace Ombi.Core.Engine.V2
{
if (viewMovie.ImdbId.IsNullOrEmpty())
{
var showInfo = await Cache.GetOrAdd("GetMovieInformationWIthImdbId" + viewMovie.Id,
async () => await MovieApi.GetMovieInformation(viewMovie.Id), DateTime.Now.AddHours(12));
var showInfo = await Cache.GetOrAddAsync("GetMovieInformationWIthImdbId" + viewMovie.Id,
() => MovieApi.GetMovieInformation(viewMovie.Id), DateTimeOffset.Now.AddHours(12));
viewMovie.Id = showInfo.Id; // TheMovieDbId
viewMovie.ImdbId = showInfo.ImdbId;
}
var user = await GetUser();
var digitalReleaseDate = viewMovie.ReleaseDates?.Results?.FirstOrDefault(x => x.IsoCode == user.StreamingCountry);
if (digitalReleaseDate == null)
{
digitalReleaseDate = viewMovie.ReleaseDates?.Results?.FirstOrDefault(x => x.IsoCode == "US");
}
viewMovie.DigitalReleaseDate = digitalReleaseDate?.ReleaseDate?.FirstOrDefault(x => x.Type == ReleaseDateType.Digital)?.ReleaseDate;
viewMovie.TheMovieDbId = viewMovie.Id.ToString();
await RunSearchRules(viewMovie);
@ -424,12 +476,12 @@ namespace Ombi.Core.Engine.V2
public async Task<MovieFullInfoViewModel> GetMovieInfoByImdbId(string imdbId, CancellationToken cancellationToken)
{
var langCode = await DefaultLanguageCode(null);
var findResult = await Cache.GetOrAdd(nameof(GetMovieInfoByImdbId) + imdbId + langCode,
async () => await MovieApi.Find(imdbId, ExternalSource.imdb_id), DateTime.Now.AddHours(12), cancellationToken);
var findResult = await Cache.GetOrAddAsync(nameof(GetMovieInfoByImdbId) + imdbId + langCode,
() => MovieApi.Find(imdbId, ExternalSource.imdb_id), DateTimeOffset.Now.AddHours(12));
var movie = findResult.movie_results.FirstOrDefault();
var movieInfo = await Cache.GetOrAdd(nameof(GetMovieInfoByImdbId) + movie.id + langCode,
async () => await MovieApi.GetFullMovieInfo(movie.id, cancellationToken, langCode), DateTime.Now.AddHours(12), cancellationToken);
var movieInfo = await Cache.GetOrAddAsync(nameof(GetMovieInfoByImdbId) + movie.id + langCode,
() => MovieApi.GetFullMovieInfo(movie.id, cancellationToken, langCode), DateTimeOffset.Now.AddHours(12));
return await ProcessSingleMovie(movieInfo);
}

@ -35,6 +35,8 @@ namespace Ombi.Core.Engine.V2
private readonly ISettingsService<LidarrSettings> _lidarrSettings;
private readonly IMusicBrainzApi _musicApi;
private bool _demo = DemoSingleton.Instance.Demo;
public async Task<List<MultiSearchResult>> MultiSearch(string searchTerm, MultiSearchFilter filter, CancellationToken cancellationToken)
{
@ -60,6 +62,12 @@ namespace Ombi.Core.Engine.V2
foreach (var multiSearch in movieDbData)
{
if (DemoCheck(multiSearch.title) || DemoCheck(multiSearch.name))
{
continue;
}
var result = new MultiSearchResult
{
MediaType = multiSearch.media_type,

@ -22,6 +22,9 @@ using Microsoft.EntityFrameworkCore;
using System.Threading;
using Ombi.Api.TheMovieDb;
using Ombi.Api.TheMovieDb.Models;
using System.Diagnostics;
using Ombi.Core.Engine.Interfaces;
using Ombi.Core.Models.UI;
namespace Ombi.Core.Engine.V2
{
@ -32,10 +35,11 @@ namespace Ombi.Core.Engine.V2
private readonly ITraktApi _traktApi;
private readonly IMovieDbApi _movieApi;
private readonly ISettingsService<CustomizationSettings> _customization;
private readonly ITvRequestEngine _requestEngine;
public TvSearchEngineV2(IPrincipal identity, IRequestServiceMain service, ITvMazeApi tvMaze, IMapper mapper,
ITraktApi trakt, IRuleEvaluator r, OmbiUserManager um, ICacheService memCache, ISettingsService<OmbiSettings> s,
IRepository<RequestSubscription> sub, IMovieDbApi movieApi, ISettingsService<CustomizationSettings> customization)
IRepository<RequestSubscription> sub, IMovieDbApi movieApi, ISettingsService<CustomizationSettings> customization, ITvRequestEngine requestEngine)
: base(identity, service, r, um, memCache, s, sub)
{
_tvMaze = tvMaze;
@ -43,25 +47,43 @@ namespace Ombi.Core.Engine.V2
_traktApi = trakt;
_movieApi = movieApi;
_customization = customization;
_requestEngine = requestEngine;
}
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.GetOrAddAsync(nameof(GetShowInformation) + langCode + tvdbid,
async () => await _movieApi.GetTVInfo(tvdbid, langCode), DateTimeOffset.Now.AddHours(12));
if (show == null || show.name == null)
{
// We don't have enough information
return null;
}
if (!show.Images?.Posters?.Any() ?? false && !string.Equals(langCode, "en", StringComparison.OrdinalIgnoreCase))
{
// There's no regional assets for this, so
// lookup the en-us version to get them
var enShow = await Cache.GetOrAddAsync(nameof(GetShowInformation) + "en" + tvdbid,
async () => await _movieApi.GetTVInfo(tvdbid, "en"), DateTimeOffset.Now.AddHours(12));
// For some of the more obsecure cases
if (!show.overview.HasValue())
{
show.overview = enShow.overview;
}
show.Images = enShow.Images;
}
var mapped = _mapper.Map<SearchFullInfoTvShowViewModel>(show);
@ -69,54 +91,22 @@ 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>();
foreach (var pagesToLoad in pages)
{
var apiResult = await Cache.GetOrAdd(nameof(Popular) + langCode + pagesToLoad.Page,
async () => await _movieApi.PopularTv(langCode, pagesToLoad.Page), DateTime.Now.AddHours(12));
var apiResult = await Cache.GetOrAddAsync(nameof(Popular) + langCode + pagesToLoad.Page,
async () => await _movieApi.PopularTv(langCode, pagesToLoad.Page), DateTimeOffset.Now.AddHours(12));
results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take));
}
@ -132,8 +122,8 @@ namespace Ombi.Core.Engine.V2
var results = new List<MovieDbSearchResult>();
foreach (var pagesToLoad in pages)
{
var apiResult = await Cache.GetOrAdd(nameof(Anticipated) + langCode + pagesToLoad.Page,
async () => await _movieApi.UpcomingTv(langCode, pagesToLoad.Page), DateTime.Now.AddHours(12));
var apiResult = await Cache.GetOrAddAsync(nameof(Anticipated) + langCode + pagesToLoad.Page,
async () => await _movieApi.UpcomingTv(langCode, pagesToLoad.Page), DateTimeOffset.Now.AddHours(12));
results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take));
}
var processed = ProcessResults(results);
@ -148,10 +138,11 @@ namespace Ombi.Core.Engine.V2
var results = new List<MovieDbSearchResult>();
foreach (var pagesToLoad in pages)
{
var apiResult = await Cache.GetOrAdd(nameof(Trending) + langCode + pagesToLoad.Page,
async () => await _movieApi.TopRatedTv(langCode, pagesToLoad.Page), DateTime.Now.AddHours(12));
var apiResult = await Cache.GetOrAddAsync(nameof(Trending) + langCode + pagesToLoad.Page,
async () => await _movieApi.TopRatedTv(langCode, pagesToLoad.Page), DateTimeOffset.Now.AddHours(12));
results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take));
}
var processed = ProcessResults(results);
return await processed;
}
@ -177,22 +168,118 @@ namespace Ombi.Core.Engine.V2
return data;
}
private async Task<IEnumerable<SearchTvShowViewModel>> ProcessResults<T>(IEnumerable<T> items)
public async Task<IEnumerable<SearchFullInfoTvShowViewModel>> RecentlyRequestedShows(int currentlyLoaded, int toLoad, CancellationToken cancellationToken)
{
var langCode = await DefaultLanguageCode(null);
var results = new List<SearchFullInfoTvShowViewModel>();
var requestResult = await Cache.GetOrAddAsync(nameof(RecentlyRequestedShows) + "Requests" + toLoad + langCode,
async () =>
{
return await _requestEngine.GetRequests(toLoad, currentlyLoaded, new Models.UI.OrderFilterModel
{
OrderType = OrderType.RequestedDateDesc
});
}, DateTimeOffset.Now.AddMinutes(15));
var movieDBResults = await Cache.GetOrAddAsync(nameof(RecentlyRequestedShows) + toLoad + langCode,
async () =>
{
var responses = new List<TvInfo>();
foreach (var movie in requestResult.Collection)
{
responses.Add(await _movieApi.GetTVInfo(movie.ExternalProviderId.ToString()));
}
return responses;
}, DateTimeOffset.Now.AddHours(12));
var mapped = _mapper.Map<List<SearchFullInfoTvShowViewModel>>(movieDBResults);
foreach(var map in mapped)
{
var processed = await ProcessResult(map);
results.Add(processed);
}
return results;
}
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 (DemoCheck(tvMazeSearch.Title))
{
continue;
}
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.GetOrAddAsync(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.GetOrAddAsync("SeasonEpisodes" + show.id + tvSeason.season_number, async () =>
{
return await _movieApi.GetSeasonEpisodes(show.id, tvSeason.season_number, CancellationToken.None);
}, DateTimeOffset.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 +303,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))
{

@ -12,16 +12,19 @@ using Ombi.Helpers;
using Ombi.Store.Entities;
using Ombi.Store.Entities.Requests;
using Ombi.Store.Repository.Requests;
using Microsoft.Extensions.Logging;
namespace Ombi.Core.Helpers
{
public class TvShowRequestBuilder
{
private readonly ILogger _logger;
public TvShowRequestBuilder(ITvMazeApi tvApi, IMovieDbApi movApi)
public TvShowRequestBuilder(ITvMazeApi tvApi, IMovieDbApi movApi, ILogger logger)
{
TvApi = tvApi;
MovieDbApi = movApi;
_logger = logger;
}
private ITvMazeApi TvApi { get; }
@ -45,6 +48,7 @@ namespace Ombi.Core.Helpers
{
if (result.Name.Equals(ShowInfo.name, StringComparison.InvariantCultureIgnoreCase))
{
_logger.LogInformation($"Found matching MovieDb entry for show name {ShowInfo.name}");
TheMovieDbRecord = result;
var showIds = await MovieDbApi.GetTvExternals(result.Id);
ShowInfo.externals.imdb = showIds.imdb_id;
@ -237,18 +241,19 @@ namespace Ombi.Core.Helpers
public TvShowRequestBuilder CreateNewRequest(TvRequestViewModel tv)
{
_logger.LogInformation($"Building Request for {ShowInfo.name} with Provider ID {TheMovieDbRecord?.Id ?? 0}");
NewRequest = new TvRequests
{
Overview = ShowInfo.summary.RemoveHtml(),
PosterPath = PosterPath,
Title = ShowInfo.name,
ReleaseDate = FirstAir,
ExternalProviderId = TheMovieDbRecord.Id,
ExternalProviderId = TheMovieDbRecord?.Id ?? 0,
Status = ShowInfo.status,
ImdbId = ShowInfo.externals?.imdb ?? string.Empty,
TvDbId = tv.TvDbId,
ChildRequests = new List<ChildRequests>(),
TotalSeasons = tv.Seasons.Count(),
TotalSeasons = tv.Seasons?.Count ?? 0,
Background = BackdropPath
};
NewRequest.ChildRequests.Add(ChildRequest);

@ -33,6 +33,14 @@ namespace Ombi.Core.Helpers
public async Task<TvShowRequestBuilderV2> GetShowInfo(int id)
{
TheMovieDbRecord = await MovieDbApi.GetTVInfo(id.ToString());
// Remove 'Specials Season'
var firstSeason = TheMovieDbRecord.seasons.OrderBy(x => x.season_number).FirstOrDefault();
if (firstSeason?.season_number == 0)
{
TheMovieDbRecord.seasons.Remove(firstSeason);
}
BackdropPath = TheMovieDbRecord.Images?.Backdrops?.OrderBy(x => x.VoteCount).ThenBy(x => x.VoteAverage).FirstOrDefault()?.FilePath; ;
DateTime.TryParse(TheMovieDbRecord.first_air_date, out var dt);
@ -149,6 +157,10 @@ namespace Ombi.Core.Helpers
else if (tv.FirstSeason)
{
var first = allEpisodes.OrderBy(x => x.season_number).FirstOrDefault();
if (first.season_number == 0)
{
first = allEpisodes.OrderBy(x => x.season_number).Skip(1).FirstOrDefault();
}
var episodesRequests = new List<EpisodeRequests>();
foreach (var ep in allEpisodes)
{
@ -217,7 +229,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 +246,8 @@ namespace Ombi.Core.Helpers
TotalSeasons = tv.Seasons.Count(),
Background = BackdropPath,
RootFolder = rootPathOverride,
QualityOverride = qualityOverride
QualityOverride = qualityOverride,
LanguageProfile = langProfile
};
NewRequest.ChildRequests.Add(ChildRequest);

@ -23,8 +23,8 @@ namespace Ombi.Core
public async Task<string> GetTvBackground(string tvdbId)
{
var key = await _cache.GetOrAdd(CacheKeys.FanartTv, async () => await _configRepository.GetAsync(Store.Entities.ConfigurationTypes.FanartTv), DateTime.Now.AddDays(1));
var images = await _cache.GetOrAdd($"{CacheKeys.FanartTv}tv{tvdbId}", async () => await _fanartTvApi.GetTvImages(int.Parse(tvdbId), key.Value), DateTime.Now.AddDays(1));
var key = await _cache.GetOrAddAsync(CacheKeys.FanartTv, () => _configRepository.GetAsync(Store.Entities.ConfigurationTypes.FanartTv), DateTimeOffset.Now.AddDays(1));
var images = await _cache.GetOrAddAsync($"{CacheKeys.FanartTv}tv{tvdbId}", () => _fanartTvApi.GetTvImages(int.Parse(tvdbId), key.Value), DateTimeOffset.Now.AddDays(1));
if (images == null)
{

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

@ -16,7 +16,7 @@ namespace Ombi.Core.Models.Search.V2
public string Overview { get; set; }
public string PosterPath { get; set; }
public string Title { get; set; }
public DateTime ReleaseDate { get; set; }
public DateTime? ReleaseDate { get; set; }
public override RequestType Type => RequestType.Movie;

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

@ -7,6 +7,7 @@ using Ombi.Core.Rule.Interfaces;
using Ombi.Store.Entities;
using Ombi.Store.Entities.Requests;
using Ombi.Store.Repository;
using Ombi.Store.Repository.Requests;
namespace Ombi.Core.Rule.Rules.Request
{
@ -60,6 +61,7 @@ namespace Ombi.Core.Rule.Rules.Request
{
foreach (var season in child.SeasonRequests)
{
var episodesToRemove = new List<EpisodeRequests>();
var currentSeasonRequest =
content.Episodes.Where(x => x.SeasonNumber == season.SeasonNumber).ToList();
if (!currentSeasonRequest.Any())
@ -68,12 +70,24 @@ namespace Ombi.Core.Rule.Rules.Request
}
foreach (var e in season.Episodes)
{
var hasEpisode = currentSeasonRequest.Any(x => x.EpisodeNumber == e.EpisodeNumber);
if (hasEpisode)
var existingEpRequest = currentSeasonRequest.FirstOrDefault(x => x.EpisodeNumber == e.EpisodeNumber);
if (existingEpRequest != null)
{
return Fail($"We already have episodes requested from series {child.Title}");
episodesToRemove.Add(e);
}
}
episodesToRemove.ForEach(x =>
{
season.Episodes.Remove(x);
});
}
var anyEpisodes = child.SeasonRequests.SelectMany(x => x.Episodes).Any();
if (!anyEpisodes)
{
return Fail($"We already have episodes requested from series {child.Title}");
}
return Success();

@ -1,4 +1,5 @@
using System.Linq;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Ombi.Core.Rule.Interfaces;
@ -41,15 +42,30 @@ namespace Ombi.Core.Rule.Rules.Request
{
continue;
}
var episodesToRemove = new List<EpisodeRequests>();
foreach (var e in season.Episodes)
{
var hasEpisode = currentSeasonRequest.Episodes.Any(x => x.EpisodeNumber == e.EpisodeNumber);
if (hasEpisode)
var existingEpRequest = currentSeasonRequest.Episodes.FirstOrDefault(x => x.EpisodeNumber == e.EpisodeNumber);
if (existingEpRequest != null)
{
return Fail($"We already have episodes requested from series {tv.Title}");
episodesToRemove.Add(e);
}
}
episodesToRemove.ForEach(x =>
{
season.Episodes.Remove(x);
});
}
var anyEpisodes = tv.SeasonRequests.SelectMany(x => x.Episodes).Any();
if (!anyEpisodes)
{
return Fail($"We already have episodes requested from series {tv.Title}");
}
}
return Success();
}

@ -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,14 @@ namespace Ombi.Core.Rule.Rules.Search
PlexServerContent item = null;
var useImdb = false;
var useTheMovieDb = false;
var useId = false;
var useTvDb = false;
PlexMediaTypeEntity type = ConvertType(obj.Type);
if (obj.ImdbId.HasValue())
{
item = await PlexContentRepository.Get(obj.ImdbId);
item = await PlexContentRepository.GetByType(obj.ImdbId, ProviderType.ImdbId, type);
if (item != null)
{
useImdb = true;
@ -36,9 +40,17 @@ namespace Ombi.Core.Rule.Rules.Search
}
if (item == null)
{
if (obj.Id > 0)
{
item = await PlexContentRepository.GetByType(obj.Id.ToString(), ProviderType.TheMovieDbId, type);
if (item != null)
{
useId = true;
}
}
if (obj.TheMovieDbId.HasValue())
{
item = await PlexContentRepository.Get(obj.TheMovieDbId);
item = await PlexContentRepository.GetByType(obj.TheMovieDbId, ProviderType.TheMovieDbId, type);
if (item != null)
{
useTheMovieDb = true;
@ -49,7 +61,7 @@ namespace Ombi.Core.Rule.Rules.Search
{
if (obj.TheTvDbId.HasValue())
{
item = await PlexContentRepository.Get(obj.TheTvDbId);
item = await PlexContentRepository.GetByType(obj.TheTvDbId, ProviderType.TvDbId, type);
if (item != null)
{
useTvDb = true;
@ -60,6 +72,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 +88,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);
}
@ -86,6 +103,12 @@ namespace Ombi.Core.Rule.Rules.Search
return Success();
}
private PlexMediaTypeEntity ConvertType(RequestType type) =>
type switch
{
RequestType.Movie => PlexMediaTypeEntity.Movie,
RequestType.TvShow => PlexMediaTypeEntity.Show,
_ => PlexMediaTypeEntity.Movie,
};
}
}

@ -1,4 +1,5 @@
using System.Linq;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Ombi.Core.Models.Search;
@ -6,6 +7,7 @@ using Ombi.Helpers;
using Ombi.Store.Context;
using Ombi.Store.Entities;
using Ombi.Store.Entities.Requests;
using Ombi.Store.Repository.Requests;
namespace Ombi.Core.Rule.Rules
{
@ -23,7 +25,7 @@ namespace Ombi.Core.Rule.Rules
if (obj.RequestType == RequestType.TvShow)
{
var vm = (ChildRequests) obj;
var result = await _ctx.SonarrCache.FirstOrDefaultAsync(x => x.TvDbId == vm.Id); // TODO lookup the external provider in the sonarr sync to use themoviedb
var result = await _ctx.SonarrCache.FirstOrDefaultAsync(x => x.TheMovieDbId == vm.Id);
if (result != null)
{
if (vm.SeasonRequests.Any())
@ -31,17 +33,30 @@ namespace Ombi.Core.Rule.Rules
var sonarrEpisodes = _ctx.SonarrEpisodeCache;
foreach (var season in vm.SeasonRequests)
{
var toRemove = new List<EpisodeRequests>();
foreach (var ep in season.Episodes)
{
// Check if we have it
var monitoredInSonarr = sonarrEpisodes.Any(x =>
var monitoredInSonarr = sonarrEpisodes.FirstOrDefault(x =>
x.EpisodeNumber == ep.EpisodeNumber && x.SeasonNumber == season.SeasonNumber
&& x.TvDbId == vm.Id);
if (monitoredInSonarr)
&& x.MovieDbId == vm.Id);
if (monitoredInSonarr != null)
{
return new RuleResult{Message = "We already have this request, please choose the \"Select...\" option to refine your request"};
}
toRemove.Add(ep);
}
}
toRemove.ForEach(x =>
{
season.Episodes.Remove(x);
});
}
var anyEpisodes = vm.SeasonRequests.SelectMany(x => x.Episodes).Any();
if (!anyEpisodes)
{
return new RuleResult { Message = $"We already have episodes requested from series {vm.Title}" };
}
}
}

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

@ -125,7 +125,6 @@ namespace Ombi.Core.Senders
private async Task<SenderResult> SendToRadarr(MovieRequests model, RadarrSettings settings)
{
var v3 = settings.V3;
var qualityToUse = int.Parse(settings.DefaultQualityProfile);
var rootFolderPath = settings.DefaultRootPath;
@ -159,30 +158,16 @@ namespace Ombi.Core.Senders
List<MovieResponse> movies;
// Check if the movie already exists? Since it could be unmonitored
if (settings.V3)
{
movies = await _radarrV3Api.GetMovies(settings.ApiKey, settings.FullUri);
}
else
{
movies = await _radarrV2Api.GetMovies(settings.ApiKey, settings.FullUri);
}
movies = await _radarrV3Api.GetMovies(settings.ApiKey, settings.FullUri);
var existingMovie = movies.FirstOrDefault(x => x.tmdbId == model.TheMovieDbId);
if (existingMovie == null)
{
RadarrAddMovie result;
if (v3)
{
result = await _radarrV3Api.AddMovie(model.TheMovieDbId, model.Title, model.ReleaseDate.Year,
qualityToUse, rootFolderPath, settings.ApiKey, settings.FullUri, !settings.AddOnly,
settings.MinimumAvailability);
}
else
{
result = await _radarrV2Api.AddMovie(model.TheMovieDbId, model.Title, model.ReleaseDate.Year,
qualityToUse, rootFolderPath, settings.ApiKey, settings.FullUri, !settings.AddOnly,
settings.MinimumAvailability);
}
var result = await _radarrV3Api.AddMovie(model.TheMovieDbId, model.Title, model.ReleaseDate.Year,
qualityToUse, rootFolderPath, settings.ApiKey, settings.FullUri, !settings.AddOnly,
settings.MinimumAvailability);
if (!string.IsNullOrEmpty(result.Error?.message))
{
_log.LogError(LoggingEvents.RadarrCacher, result.Error.message);
@ -199,23 +184,12 @@ namespace Ombi.Core.Senders
{
// let's set it to monitored and search for it
existingMovie.monitored = true;
if (v3)
{
await _radarrV3Api.UpdateMovie(existingMovie, settings.ApiKey, settings.FullUri);
// Search for it
if (!settings.AddOnly)
{
await _radarrV3Api.MovieSearch(new[] { existingMovie.id }, settings.ApiKey, settings.FullUri);
}
}
else
await _radarrV3Api.UpdateMovie(existingMovie, settings.ApiKey, settings.FullUri);
// Search for it
if (!settings.AddOnly)
{
await _radarrV2Api.UpdateMovie(existingMovie, settings.ApiKey, settings.FullUri);
// Search for it
if (!settings.AddOnly)
{
await _radarrV2Api.MovieSearch(new[] { existingMovie.id }, settings.ApiKey, settings.FullUri);
}
await _radarrV3Api.MovieSearch(new[] { existingMovie.id }, settings.ApiKey, settings.FullUri);
}
return new SenderResult { Success = true, Sent = true };
@ -226,18 +200,9 @@ namespace Ombi.Core.Senders
private async Task<string> RadarrRootPath(int overrideId, RadarrSettings settings)
{
if (settings.V3)
{
var paths = await _radarrV3Api.GetRootFolders(settings.ApiKey, settings.FullUri);
var selectedPath = paths.FirstOrDefault(x => x.id == overrideId);
return selectedPath?.path ?? string.Empty;
}
else
{
var paths = await _radarrV2Api.GetRootFolders(settings.ApiKey, settings.FullUri);
var selectedPath = paths.FirstOrDefault(x => x.id == overrideId);
return selectedPath?.path ?? string.Empty;
}
var paths = await _radarrV3Api.GetRootFolders(settings.ApiKey, settings.FullUri);
var selectedPath = paths.FirstOrDefault(x => x.id == overrideId);
return selectedPath?.path ?? string.Empty;
}
}
}

@ -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>();
@ -195,6 +206,7 @@ namespace Ombi.DependencyInjection
services.AddTransient<IEmailProvider, GenericEmailProvider>();
services.AddTransient<INotificationHelper, NotificationHelper>();
services.AddSingleton<ICacheService, CacheService>();
services.AddSingleton<IMediaCacheService, MediaCacheService>();
services.AddScoped<IImageService, ImageService>();
services.AddTransient<IDiscordNotification, DiscordNotification>();

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

@ -21,7 +21,7 @@ namespace Ombi.HealthChecks.Checks
using (var scope = CreateScope())
{
var settingsProvider = scope.ServiceProvider.GetRequiredService<ISettingsService<RadarrSettings>>();
var api = scope.ServiceProvider.GetRequiredService<IRadarrApi>();
var api = scope.ServiceProvider.GetRequiredService<IRadarrV3Api>();
var settings = await settingsProvider.GetSettingsAsync();
if (!settings.Enabled)
{

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

@ -1,48 +1,26 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Nito.AsyncEx;
using LazyCache;
namespace Ombi.Helpers
{
public class CacheService : ICacheService
{
private readonly IMemoryCache _memoryCache;
private readonly AsyncLock _mutex = new AsyncLock();
public CacheService(IMemoryCache memoryCache)
protected readonly IAppCache _memoryCache;
public CacheService(IAppCache memoryCache)
{
_memoryCache = memoryCache ?? throw new ArgumentNullException(nameof(memoryCache));
_memoryCache = memoryCache;
}
public async Task<T> GetOrAdd<T>(string cacheKey, Func<Task<T>> factory, DateTime absoluteExpiration = default(DateTime), CancellationToken cancellationToken = default(CancellationToken))
public virtual async Task<T> GetOrAddAsync<T>(string cacheKey, Func<Task<T>> factory, DateTimeOffset absoluteExpiration = default)
{
if (absoluteExpiration == default(DateTime))
if (absoluteExpiration == default)
{
absoluteExpiration = DateTime.Now.AddHours(1);
absoluteExpiration = DateTimeOffset.Now.AddHours(1);
}
// locks get and set internally
if (_memoryCache.TryGetValue<T>(cacheKey, out var result))
{
return result;
}
if (_memoryCache.TryGetValue(cacheKey, out result))
{
return result;
}
if (cancellationToken.CanBeCanceled)
{
cancellationToken.ThrowIfCancellationRequested();
}
result = await factory();
_memoryCache.Set(cacheKey, result, absoluteExpiration);
return result;
return await _memoryCache.GetOrAddAsync<T>(cacheKey, () => factory(), absoluteExpiration);
}
public void Remove(string key)
@ -50,28 +28,10 @@ namespace Ombi.Helpers
_memoryCache.Remove(key);
}
public T GetOrAdd<T>(string cacheKey, Func<T> factory, DateTime absoluteExpiration)
public T GetOrAdd<T>(string cacheKey, Func<T> factory, DateTimeOffset absoluteExpiration)
{
// locks get and set internally
if (_memoryCache.TryGetValue<T>(cacheKey, out var result))
{
return result;
}
lock (TypeLock<T>.Lock)
{
if (_memoryCache.TryGetValue(cacheKey, out result))
{
return result;
}
result = factory();
_memoryCache.Set(cacheKey, result, absoluteExpiration);
return result;
}
return _memoryCache.GetOrAdd<T>(cacheKey, () => factory(), absoluteExpiration);
}
private static class TypeLock<T>

@ -1,4 +1,6 @@
namespace Ombi.Helpers
using System.Collections.Generic;
namespace Ombi.Helpers
{
public class DemoSingleton
{
@ -10,4 +12,460 @@
public bool Demo { get; set; }
}
public static class ExcludedDemo
{
public static HashSet<string> ExcludedContent => new HashSet<string>
{
"101 Dalmatians",
"102 Dalmatians",
"20,000 Leagues Under the Sea",
"A Bug's Life",
"A Far Off Place",
"A Goofy Movie",
"A Kid in King Arthur's Court",
"A Tale of Two Critters",
"A Tiger Walks",
"A Wrinkle in Time",
"ABCD 2",
"African Cats",
"Air Bud",
"Air Bud: Golden Receiver",
"Aladdin",
"Aladdin",
"Alexander and the Terrible, Horrible, No Good, Very Bad Day",
"Alice Through the Looking Glass",
"Alice in Wonderland",
"Alice in Wonderland",
"Aliens of the Deep",
"Almost Angels",
"America's Heart and Soul",
"Amy",
"Anaganaga O Dheerudu",
"Angels in the Outfield",
"Arjun: The Warrior Prince",
"Around the World in 80 Days",
"Artemis Fowl",
"Atlantis: The Lost Empire",
"Babes in Toyland",
"Bambi",
"Bears",
"Beauty and the Beast",
"Beauty and the Beast",
"Bedknobs and Broomsticks",
"Bedtime Stories",
"Benji the Hunted",
"Beverly Hills Chihuahua",
"Big Hero 6",
"Big Red",
"Blackbeard's Ghost",
"Blank Check",
"Blue",
"Bolt",
"Bon Voyage!",
"Born in China",
"Brave",
"Bridge to Terabithia",
"Brother Bear",
"Candleshoe",
"Cars",
"Cars 2",
"Cars 3",
"Charley and the Angel",
"Charlie, the Lonesome Cougar",
"Cheetah",
"Chicken Little",
"Chimpanzee",
"Christopher Robin",
"Cinderella",
"Cinderella",
"Coco",
"College Road Trip",
"Condorman",
"Confessions of a Teenage Drama Queen",
"Cool Runnings",
"D2: The Mighty Ducks",
"D3: The Mighty Ducks",
"Dangal",
"Darby O'Gill and the Little People",
"Dasavathaaram",
"Davy Crockett and the River Pirates",
"Davy Crockett, King of the Wild Frontier",
"Dinosaur",
"Disney's A Christmas Carol",
"Disney's The Kid",
"Do Dooni Chaar",
"Dolphin Reef",
"Doug's 1st Movie",
"Dragonslayer",
"DuckTales the Movie: Treasure of the Lost Lamp",
"Dumbo",
"Dumbo",
"Earth",
"Eight Below",
"Emil and the Detectives",
"Enchanted",
"Endurance",
"Escape to Witch Mountain",
"Expedition China",
"Fantasia",
"Fantasia 2000",
"Finding Dory",
"Finding Nemo",
"First Kid",
"Flight of the Navigator",
"Flubber",
"Follow Me, Boys!",
"Frank and Ollie",
"Frankenweenie",
"Freaky Friday",
"Freaky Friday",
"Frozen",
"Frozen II",
"Onward",
"Star Wars",
"Raya",
"Mandalorian",
"Fun and Fancy Free",
"G-Force",
"George of the Jungle",
"Ghost in the Shell 2: Innocence GITS2",
"Ghost of the Mountains",
"Ghosts of the Abyss",
"Glory Road",
"Greyfriars Bobby",
"Growing Up Wild",
"Gus",
"Hannah Montana and Miley Cyrus: Best of Both Worlds Concert",
"Hannah Montana: The Movie",
"Heavyweights",
"Herbie Goes Bananas",
"Herbie Goes to Monte Carlo",
"Herbie Rides Again",
"Herbie: Fully Loaded",
"Hercules",
"High School Musical 3: Senior Year",
"Hocus Pocus",
"Holes",
"Home on the Range",
"Homeward Bound II: Lost in San Francisco",
"Homeward Bound: The Incredible Journey",
"Honey, I Blew Up the Kid",
"Honey, I Shrunk the Kids",
"Hot Lead and Cold Feet",
"I'll Be Home for Christmas",
"Ice Princess",
"In Search of the Castaways",
"Incredibles 2",
"Inside Out",
"Inspector Gadget",
"Into the Woods",
"Invincible",
"Iron Will",
"Jagga Jasoos",
"James and the Giant Peach",
"John Carter",
"Johnny Tremain",
"Jonas Brothers: The 3D Concert Experience",
"Jungle 2 Jungle",
"Jungle Cat",
"Khoobsurat",
"Kidnapped",
"King of the Grizzlies",
"L'Empereur - March of the Penguins 2: The Next Step[a]",
"Lady and the Tramp",
"Lady and the Tramp",
"Lilly the Witch: The Dragon and the Magic Book",
"Lilly the Witch: The Journey to Mandolan",
"Lilo & Stitch",
"Lt. Robin Crusoe, U.S.N.",
"Make Mine Music",
"Maleficent",
"Maleficent: Mistress of Evil",
"Man of the House",
"Mars Needs Moms",
"Mary Poppins",
"Mary Poppins Returns",
"Max Keeble's Big Move",
"McFarland, USA",
"Meet the Deedles",
"Meet the Robinsons",
"Melody Time",
"Midnight Madness",
"Mighty Joe Young",
"Million Dollar Arm",
"Miracle",
"Miracle of the White Stallions",
"Moana",
"Monkey Kingdom",
"Monkeys, Go Home!",
"Monsters University",
"Monsters, Inc.",
"Moon Pilot",
"Morning Light",
"Mr. Magoo",
"Mulan",
"Muppet Treasure Island",
"Muppets Most Wanted",
"My Favorite Martian",
"Napoleon and Samantha",
"National Treasure",
"National Treasure: Book of Secrets",
"Never Cry Wolf",
"Never a Dull Moment",
"Newsies",
"Night Crossing",
"Nikki, Wild Dog of the North",
"No Deposit, No Return",
"Now You See Him, Now You Don't",
"Oceans",
"Old Dogs",
"Old Yeller",
"Oliver & Company",
"One Hundred and One Dalmatians",
"One Little Indian",
"One Magic Christmas",
"One of Our Dinosaurs Is Missing",
"Operation Dumbo Drop",
"Oz the Great and Powerful",
"Penguins",
"Perri",
"Pete's Dragon",
"Pete's Dragon",
"Peter Pan",
"Piglet's Big Movie",
"Pinocchio",
"Pirates of the Caribbean: At World's End",
"Pirates of the Caribbean: Dead Man's Chest",
"Pirates of the Caribbean: Dead Men Tell No Tales",
"Pirates of the Caribbean: On Stranger Tides",
"Pirates of the Caribbean: The Curse of the Black Pearl",
"Planes",
"Planes: Fire & Rescue",
"Pocahontas",
"Pollyanna",
"Pooh's Heffalump Movie",
"Popeye",
"Prince of Persia: The Sands of Time",
"Prom",
"Queen of Katwe",
"Race to Witch Mountain",
"Ralph Breaks the Internet",
"Rascal",
"Ratatouille",
"Recess: School's Out",
"Remember the Titans",
"Return from Witch Mountain",
"Return to Never Land",
"Return to Oz",
"Return to Snowy River",
"Ride a Wild Pony",
"Roadside Romeo",
"Rob Roy, the Highland Rogue",
"Robin Hood",
"RocketMan",
"Roving Mars",
"Run, Cougar, Run",
"Sacred Planet",
"Saludos Amigos",
"Savage Sam",
"Saving Mr. Banks",
"Scandalous John",
"Secretariat",
"Secrets of Life",
"Shipwrecked",
"Sky High",
"Sleeping Beauty",
"Smith!",
"Snow Dogs",
"Snow White and the Seven Dwarfs",
"Snowball Express",
"So Dear to My Heart",
"Something Wicked This Way Comes",
"Son of Flubber",
"Song of the South",
"Squanto: A Warrior's Tale",
"Summer Magic",
"Superdad",
"Swiss Family Robinson",
"Tall Tale",
"Tangled",
"Tarzan",
"Teacher's Pet",
"Ten Who Dared",
"Tex",
"That Darn Cat",
"That Darn Cat!",
"The Absent-Minded Professor",
"The Adventures of Bullwhip Griffin",
"The Adventures of Huck Finn",
"The Adventures of Ichabod and Mr. Toad",
"The African Lion",
"The Apple Dumpling Gang",
"The Apple Dumpling Gang Rides Again",
"The Aristocats",
"The BFG",
"The Barefoot Executive",
"The Bears and I",
"The Best of Walt Disney's True-Life Adventures",
"The Big Green",
"The Biscuit Eater",
"The Black Cauldron",
"The Black Hole",
"The Boatniks",
"The Book of Masters",
"The Boys: The Sherman Brothers' Story",
"The Castaway Cowboy",
"The Cat from Outer Space",
"The Chronicles of Narnia: Prince Caspian",
"The Chronicles of Narnia: The Lion, the Witch and the Wardrobe",
"The Computer Wore Tennis Shoes",
"The Country Bears",
"The Crimson Wing: Mystery of the Flamingos",
"The Devil and Max Devlin",
"The Emperor's New Groove",
"The Fighting Prince of Donegal",
"The Finest Hours",
"The Fox and the Hound",
"The Game Plan",
"The Gnome-Mobile",
"The Good Dinosaur",
"The Great Locomotive Chase",
"The Great Mouse Detective",
"The Greatest Game Ever Played",
"The Happiest Millionaire",
"The Haunted Mansion",
"The Horse in the Gray Flannel Suit",
"The Hunchback of Notre Dame",
"The Incredible Journey",
"The Incredibles",
"The Island at the Top of the World",
"The Journey of Natty Gann",
"The Jungle Book",
"The Jungle Book",
"The Jungle Book",
"The Jungle Book 2",
"The Last Flight of Noah's Ark",
"The Legend of Lobo",
"The Light in the Forest",
"The Lion King",
"The Lion King",
"The Little Mermaid",
"The Littlest Horse Thieves",
"The Littlest Outlaw",
"The Living Desert",
"The Lizzie McGuire Movie",
"The London Connection",
"The Lone Ranger",
"The Love Bug",
"The Many Adventures of Winnie the Pooh",
"The Mighty Ducks",
"The Million Dollar Duck",
"The Misadventures of Merlin Jones",
"The Monkey's Uncle",
"The Moon-Spinners",
"The Muppet Christmas Carol",
"The Muppets",
"The Nightmare Before Christmas 3D TNBC",
"The North Avenue Irregulars",
"The Nutcracker and the Four Realms",
"The Odd Life of Timothy Green",
"The One and Only, Genuine, Original Family Band",
"The Pacifier",
"The Parent Trap",
"The Parent Trap",
"The Pixar Story",
"The Princess Diaries",
"The Princess Diaries 2: Royal Engagement",
"The Princess and the Frog",
"The Reluctant Dragon",
"The Rescuers",
"The Rescuers Down Under",
"The Rocketeer TR",
"The Rookie",
"The Santa Clause 2",
"The Santa Clause 3: The Escape Clause",
"The Santa Clause TSC",
"The Shaggy D.A.",
"The Shaggy Dog",
"The Shaggy Dog",
"The Sign of Zorro",
"The Sorcerer's Apprentice",
"The Story of Robin Hood and His Merrie Men",
"The Straight Story",
"The Strongest Man in the World",
"The Sword and the Rose",
"The Sword in the Stone",
"The Three Caballeros",
"The Three Lives of Thomasina",
"The Three Musketeers",
"The Tigger Movie",
"The Ugly Dachshund",
"The Vanishing Prairie",
"The Watcher in the Woods",
"The Wild",
"The Wild Country",
"The World's Greatest Athlete",
"The Young Black Stallion",
"Third Man on the Mountain",
"Those Calloways",
"Toby Tyler",
"Tom and Huck",
"Tomorrowland",
"Tonka",
"Toy Story",
"Toy Story 2",
"Toy Story 3",
"Toy Story 4",
"Trail of the Panda",
"Treasure Island",
"Treasure Planet",
"Treasure of Matecumbe",
"Trenchcoat",
"Tron",
"Tron: Legacy",
"Tuck Everlasting",
"Underdog",
"Unidentified Flying Oddball",
"Up",
"Valiant",
"Victory Through Air Power",
"WALL-E",
"Waking Sleeping Beauty",
"Walt & El Grupo",
"Westward Ho the Wagons!",
"White Fang",
"White Fang 2: Myth of the White Wolf",
"White Wilderness",
"Wild Hearts Can't Be Broken",
"Wings of Life",
"Winnie the Pooh",
"Wreck-It Ralph",
"Zokkomon",
"Zootopia",
"Zorro the Avenger",
"Iron Man",
"Hulk",
"Thor",
"Avengers",
"Guardians of the Galaxy",
"Ant-Man",
"Captain America",
"Doctor Strange",
"Guardians of the Galaxy",
"Spider-Man",
"Black Panther",
"Marvel",
"Spider Man",
"SpiderMan",
"Loki",
"Winter Soldier",
"Wanda",
"Small Fry",
"Rex",
"Lamp life",
"Toy",
"Hawaiian"
};
}
}

@ -6,8 +6,8 @@ namespace Ombi.Helpers
{
public interface ICacheService
{
Task<T> GetOrAdd<T>(string cacheKey, Func<Task<T>> factory, DateTime absoluteExpiration = default(DateTime), CancellationToken cancellationToken = default(CancellationToken));
T GetOrAdd<T>(string cacheKey, Func<T> factory, DateTime absoluteExpiration);
Task<T> GetOrAddAsync<T>(string cacheKey, Func<Task<T>> factory, DateTimeOffset absoluteExpiration = default);
T GetOrAdd<T>(string cacheKey, Func<T> factory, DateTimeOffset absoluteExpiration);
void Remove(string key);
}
}

@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using LazyCache;
namespace Ombi.Helpers
{
public interface IMediaCacheService
{
Task<T> GetOrAddAsync<T>(string cacheKey, System.Func<Task<T>> factory, DateTimeOffset absoluteExpiration = default);
Task Purge();
}
public class MediaCacheService : CacheService, IMediaCacheService
{
private const string CacheKey = "MediaCacheServiceKeys";
public MediaCacheService(IAppCache memoryCache) : base(memoryCache)
{
}
public async override Task<T> GetOrAddAsync<T>(string cacheKey, System.Func<Task<T>> factory, DateTimeOffset absoluteExpiration = default)
{
if (absoluteExpiration == default)
{
absoluteExpiration = DateTimeOffset.Now.AddHours(1);
}
if (_memoryCache.TryGetValue<T>($"MediaCacheService_{cacheKey}", out var result))
{
return (T)result;
}
// Not in the cache, so add this Key into our MediaServiceCache
await UpdateLocalCache(cacheKey);
return await _memoryCache.GetOrAddAsync<T>(cacheKey, () => factory(), absoluteExpiration);
}
private async Task UpdateLocalCache(string cacheKey)
{
var mediaServiceCache = await _memoryCache.GetAsync<List<string>>(CacheKey);
if (mediaServiceCache == null)
{
mediaServiceCache = new List<string>();
}
mediaServiceCache.Add(cacheKey);
_memoryCache.Remove(CacheKey);
_memoryCache.Add(CacheKey, mediaServiceCache);
}
public async Task Purge()
{
var keys = await _memoryCache.GetAsync<List<string>>(CacheKey);
foreach (var key in keys)
{
base.Remove(key);
}
}
}
}

@ -11,6 +11,7 @@
<ItemGroup>
<PackageReference Include="EasyCrypto" Version="3.3.2" />
<PackageReference Include="LazyCache.AspNetCore" Version="2.1.3" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />

@ -107,7 +107,7 @@ namespace Ombi.Helpers
public static string GetPlexMediaUrl(string machineId, int mediaId)
{
var url =
$"https://app.plex.tv/web/app#!/server/{machineId}/details?key=library%2Fmetadata%2F{mediaId}";
$"https://app.plex.tv/web/app#!/server/{machineId}/details?key=%2flibrary%2Fmetadata%2F{mediaId}";
return url;
}

@ -104,7 +104,7 @@ namespace Ombi.Mapping.Profiles
.ForMember(x => x.Id, o => o.MapFrom(s => s.id))
.ForMember(x => x.Overview, o => o.MapFrom(s => s.overview))
.ForMember(x => x.PosterPath, o => o.MapFrom(s => s.poster_path))
.ForMember(x => x.ReleaseDate, o => o.MapFrom(s => DateTime.Parse(s.release_date)))
.ForMember(x => x.ReleaseDate, o => o.MapFrom(s => string.IsNullOrEmpty(s.release_date) ? (DateTime?)null : DateTime.Parse(s.release_date)))
.ForMember(x => x.Title, o => o.MapFrom(s => s.title));
CreateMap<SearchMovieViewModel, MovieCollection>().ReverseMap();

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

@ -32,7 +32,7 @@ namespace Ombi.Mapping.Profiles
.ForMember(dest => dest.Images, opts => opts.MapFrom(src => src.Images))
.ForMember(dest => dest.Cast, opts => opts.MapFrom(src => src.Credits.cast))
.ForMember(dest => dest.Crew, opts => opts.MapFrom(src => src.Credits.crew))
.ForMember(dest => dest.Banner, opts => opts.MapFrom(src => GetBanner(src.Images)))
.ForMember(dest => dest.Banner, opts => opts.MapFrom(src => GetBanner(src.Images, src.backdrop_path)))
.ForMember(dest => dest.Genres, opts => opts.MapFrom(src => src.genres))
.ForMember(dest => dest.Keywords, opts => opts.MapFrom(src => src.Keywords))
.ForMember(dest => dest.Tagline, opts => opts.MapFrom(src => src.tagline))
@ -78,20 +78,20 @@ namespace Ombi.Mapping.Profiles
CreateMap<SearchTvShowViewModel, SearchFullInfoTvShowViewModel>().ReverseMap();
}
private string GetBanner(Api.TheMovieDb.Models.Images images)
private string GetBanner(Api.TheMovieDb.Models.Images images, string backdropPath)
{
var hasBackdrop = images?.Backdrops?.Any();
if (hasBackdrop ?? false)
{
return images.Backdrops?.OrderBy(x => x.VoteCount).ThenBy(x => x.VoteAverage).Select(x => x.FilePath).FirstOrDefault();
}
else if (images != null)
else if (images?.Posters?.Any() ?? false)
{
return images.Posters?.OrderBy(x => x.VoteCount).ThenBy(x => x.VoteAverage).Select(x => x.FilePath).FirstOrDefault();
}
else
{
return string.Empty;
return backdropPath;
}
}

@ -2,6 +2,6 @@
{
public interface INewsletterTemplate
{
string LoadTemplate(string subject, string intro, string tableHtml, string logo);
string LoadTemplate(string subject, string intro, string tableHtml, string logo, string unsubscribeLink);
}
}

@ -13,7 +13,7 @@ namespace Ombi.Notifications.Templates
if (string.IsNullOrEmpty(_templateLocation))
{
#if DEBUG
_templateLocation = Path.Combine(Directory.GetCurrentDirectory(), "bin", "Debug", "netcoreapp3.0", "Templates", "NewsletterTemplate.html");
_templateLocation = Path.Combine(Directory.GetCurrentDirectory(), "bin", "Debug", "net5.0", "Templates", "NewsletterTemplate.html");
#else
_templateLocation = Path.Combine(Directory.GetCurrentDirectory(), "Templates", "NewsletterTemplate.html");
#endif
@ -29,9 +29,10 @@ namespace Ombi.Notifications.Templates
private const string Logo = "{@LOGO}";
private const string TableLocation = "{@RECENTLYADDED}";
private const string IntroText = "{@INTRO}";
private const string Unsubscribe = "{@UNSUBSCRIBE}";
public string LoadTemplate(string subject, string intro, string tableHtml, string logo)
public string LoadTemplate(string subject, string intro, string tableHtml, string logo, string unsubscribeLink)
{
var sb = new StringBuilder(File.ReadAllText(TemplateLocation));
sb.Replace(SubjectKey, subject);
@ -39,6 +40,7 @@ namespace Ombi.Notifications.Templates
sb.Replace(IntroText, intro);
sb.Replace(DateKey, DateTime.Now.ToString("f"));
sb.Replace(Logo, string.IsNullOrEmpty(logo) ? OmbiLogo : logo);
sb.Replace(Unsubscribe, string.IsNullOrEmpty(unsubscribeLink) ? string.Empty : unsubscribeLink);
return sb.ToString();
}

@ -451,6 +451,11 @@
<div class="footer" style="clear: both; Margin-top: 10px; text-align: center; width: 100%;">
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
<tbody>
<tr>
<td class="content-block powered-by" valign="top" align="center" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; color: #999999; font-size: 12px; text-align: center;">
<a href="{@UNSUBSCRIBE}" style="font-weight: 400; font-size: 12px; text-align: center; color: #ff761b;">Unsubscribe</a>
</td>
</tr>
<tr>
<td class="content-block powered-by" valign="top" align="center" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; color: #999999; font-size: 12px; text-align: center;">
Powered by <a href="https://github.com/Ombi-app/Ombi" style="font-weight: 400; font-size: 12px; text-align: center; text-decoration: none; color: #ff761b;">Ombi</a>

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

@ -0,0 +1,35 @@
using NUnit.Framework;
using Ombi.Schedule.Jobs.Ombi;
using System.Collections.Generic;
namespace Ombi.Schedule.Tests
{
[TestFixture]
public class NewsletterUnsubscribeTests
{
[TestCaseSource(nameof(Data))]
public string GenerateUnsubscribeLinkTest(string appUrl, string id)
{
return NewsletterJob.GenerateUnsubscribeLink(appUrl, id);
}
private static IEnumerable<TestCaseData> Data
{
get
{
yield return new TestCaseData("https://google.com/", "1").Returns("https://google.com:443/unsubscribe/1").SetName("Fully Qualified");
yield return new TestCaseData("https://google.com", "1").Returns("https://google.com:443/unsubscribe/1").SetName("Missing Slash");
yield return new TestCaseData("google.com", "1").Returns("http://google.com:80/unsubscribe/1").SetName("Missing scheme");
yield return new TestCaseData("ombi.google.com", "1").Returns("http://ombi.google.com:80/unsubscribe/1").SetName("Sub domain missing scheme");
yield return new TestCaseData("https://ombi.google.com", "1").Returns("https://ombi.google.com:443/unsubscribe/1").SetName("Sub domain");
yield return new TestCaseData("https://ombi.google.com/", "1").Returns("https://ombi.google.com:443/unsubscribe/1").SetName("Sub domain with slash");
yield return new TestCaseData("https://google.com/ombi/", "1").Returns("https://google.com:443/ombi/unsubscribe/1").SetName("RP");
yield return new TestCaseData("https://google.com/ombi", "1").Returns("https://google.com:443/ombi/unsubscribe/1").SetName("RP missing slash");
yield return new TestCaseData("https://google.com:3577", "1").Returns("https://google.com:3577/unsubscribe/1").SetName("Port");
yield return new TestCaseData("https://google.com:3577/", "1").Returns("https://google.com:3577/unsubscribe/1").SetName("Port With Slash");
yield return new TestCaseData("", "1").Returns(string.Empty).SetName("Missing App URL empty");
yield return new TestCaseData(null, "1").Returns(string.Empty).SetName("Missing App URL null");
}
}
}
}

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

@ -130,11 +130,11 @@ namespace Ombi.Schedule.Jobs.Ombi
var jellyfinContent = _jellyfin.GetAll().Include(x => x.Episodes).AsNoTracking();
var lidarrContent = _lidarrAlbumRepository.GetAll().AsNoTracking().ToList().Where(x => x.FullyAvailable);
var addedLog = _recentlyAddedLog.GetAll();
var addedPlexMovieLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Plex && x.ContentType == ContentType.Parent).Select(x => x.ContentId).ToHashSet();
var addedEmbyMoviesLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Emby && x.ContentType == ContentType.Parent).Select(x => x.ContentId).ToHashSet();
var addedJellyfinMoviesLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Jellyfin && x.ContentType == ContentType.Parent).Select(x => x.ContentId).ToHashSet();
var addedAlbumLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Lidarr && x.ContentType == ContentType.Album).Select(x => x.AlbumId).ToHashSet();
var addedLog = _recentlyAddedLog.GetAll().ToList();
HashSet<int> addedPlexMovieLogIds, addedEmbyMoviesLogIds, addedJellyfinMoviesLogIds;
HashSet<string> addedAlbumLogIds;
GetRecentlyAddedMoviesData(addedLog, out addedPlexMovieLogIds, out addedEmbyMoviesLogIds, out addedJellyfinMoviesLogIds, out addedAlbumLogIds);
var addedPlexEpisodesLogIds =
addedLog.Where(x => x.Type == RecentlyAddedType.Plex && x.ContentType == ContentType.Episode);
@ -170,6 +170,7 @@ namespace Ombi.Schedule.Jobs.Ombi
plexContentMoviesToSend = plexContentMoviesToSend.DistinctBy(x => x.Id).ToHashSet();
embyContentMoviesToSend = embyContentMoviesToSend.DistinctBy(x => x.Id).ToHashSet();
jellyfinContentMoviesToSend = jellyfinContentMoviesToSend.DistinctBy(x => x.Id).ToHashSet();
var plexEpisodesToSend =
FilterPlexEpisodes(_plex.GetAllEpisodes().Include(x => x.Series).AsNoTracking(), addedPlexEpisodesLogIds);
@ -226,32 +227,33 @@ namespace Ombi.Schedule.Jobs.Ombi
var messageContent = ParseTemplate(template, customization);
var email = new NewsletterTemplate();
var html = email.LoadTemplate(messageContent.Subject, messageContent.Message, body, customization.Logo);
var bodyBuilder = new BodyBuilder
foreach (var user in users)
{
HtmlBody = html,
};
var url = GenerateUnsubscribeLink(customization.ApplicationUrl, user.Id);
var html = email.LoadTemplate(messageContent.Subject, messageContent.Message, body, customization.Logo, url);
var message = new MimeMessage
{
Body = bodyBuilder.ToMessageBody(),
Subject = messageContent.Subject
};
var bodyBuilder = new BodyBuilder
{
HtmlBody = html,
};
var message = new MimeMessage
{
Body = bodyBuilder.ToMessageBody(),
Subject = messageContent.Subject
};
foreach (var user in users)
{
// Get the users to send it to
if (user.Email.IsNullOrEmpty())
{
continue;
}
// BCC the messages
message.Bcc.Add(new MailboxAddress(user.Email.Trim(), user.Email.Trim()));
}
// Send the message to the user
message.To.Add(new MailboxAddress(user.Email.Trim(), user.Email.Trim()));
// Send the email
await _email.Send(message, emailSettings);
// Send the email
await _email.Send(message, emailSettings);
}
// Now add all of this to the Recently Added log
var recentlyAddedLog = new HashSet<RecentlyAddedLog>();
@ -344,11 +346,14 @@ namespace Ombi.Schedule.Jobs.Ombi
{
continue;
}
var unsubscribeLink = GenerateUnsubscribeLink(customization.ApplicationUrl, a.Id);
var messageContent = ParseTemplate(template, customization);
var email = new NewsletterTemplate();
var html = email.LoadTemplate(messageContent.Subject, messageContent.Message, body, customization.Logo);
var html = email.LoadTemplate(messageContent.Subject, messageContent.Message, body, customization.Logo, unsubscribeLink);
await _email.Send(
new NotificationMessage { Message = html, Subject = messageContent.Subject, To = a.Email },
@ -369,6 +374,36 @@ namespace Ombi.Schedule.Jobs.Ombi
.SendAsync(NotificationHub.NotificationEvent, "Newsletter Finished");
}
private void GetRecentlyAddedMoviesData(List<RecentlyAddedLog> addedLog, out HashSet<int> addedPlexMovieLogIds, out HashSet<int> addedEmbyMoviesLogIds, out HashSet<int> addedJellyfinMoviesLogIds, out HashSet<string> addedAlbumLogIds)
{
var plexParent = addedLog.Where(x => x.Type == RecentlyAddedType.Plex && x.ContentType == ContentType.Parent).ToList();
addedPlexMovieLogIds = plexParent != null && plexParent.Any() ? (plexParent?.Select(x => x.ContentId)?.ToHashSet() ?? new HashSet<int>()) : new HashSet<int>();
var embyParent = addedLog.Where(x => x.Type == RecentlyAddedType.Emby && x.ContentType == ContentType.Parent);
addedEmbyMoviesLogIds = embyParent != null && embyParent.Any() ? (embyParent?.Select(x => x.ContentId)?.ToHashSet() ?? new HashSet<int>()) : new HashSet<int>();
var jellyFinParent = addedLog.Where(x => x.Type == RecentlyAddedType.Jellyfin && x.ContentType == ContentType.Parent);
addedJellyfinMoviesLogIds = jellyFinParent != null && jellyFinParent.Any() ? (jellyFinParent?.Select(x => x.ContentId)?.ToHashSet() ?? new HashSet<int>()) : new HashSet<int>();
var lidarrParent = addedLog.Where(x => x.Type == RecentlyAddedType.Lidarr && x.ContentType == ContentType.Album);
addedAlbumLogIds = lidarrParent != null && lidarrParent.Any() ? (lidarrParent?.Select(x => x.AlbumId)?.ToHashSet() ?? new HashSet<string>()) : new HashSet<string>();
}
public static string GenerateUnsubscribeLink(string applicationUrl, string id)
{
if (!applicationUrl.HasValue())
{
return string.Empty;
}
if (!applicationUrl.EndsWith('/'))
{
applicationUrl += '/';
}
var b = new UriBuilder($"{applicationUrl}unsubscribe/{id}");
return b.ToString();
}
private async Task<HashSet<PlexServerContent>> GetMoviesWithoutId(HashSet<int> addedMovieLogIds, HashSet<PlexServerContent> needsMovieDbPlex)
{
foreach (var movie in needsMovieDbPlex)
@ -466,7 +501,7 @@ namespace Ombi.Schedule.Jobs.Ombi
await Start(newsletterSettings, false);
}
private HashSet<PlexEpisode> FilterPlexEpisodes(IEnumerable<PlexEpisode> source, IQueryable<RecentlyAddedLog> recentlyAdded)
private HashSet<PlexEpisode> FilterPlexEpisodes(IEnumerable<PlexEpisode> source, IEnumerable<RecentlyAddedLog> recentlyAdded)
{
var itemsToReturn = new HashSet<PlexEpisode>();
foreach (var ep in source.Where(x => x.Series.HasTvDb))
@ -483,7 +518,7 @@ namespace Ombi.Schedule.Jobs.Ombi
return itemsToReturn;
}
private HashSet<EmbyEpisode> FilterEmbyEpisodes(IEnumerable<EmbyEpisode> source, IQueryable<RecentlyAddedLog> recentlyAdded)
private HashSet<EmbyEpisode> FilterEmbyEpisodes(IEnumerable<EmbyEpisode> source, IEnumerable<RecentlyAddedLog> recentlyAdded)
{
var itemsToReturn = new HashSet<EmbyEpisode>();
foreach (var ep in source.Where(x => x.Series.HasTvDb))
@ -500,7 +535,7 @@ namespace Ombi.Schedule.Jobs.Ombi
return itemsToReturn;
}
private HashSet<JellyfinEpisode> FilterJellyfinEpisodes(IEnumerable<JellyfinEpisode> source, IQueryable<RecentlyAddedLog> recentlyAdded)
private HashSet<JellyfinEpisode> FilterJellyfinEpisodes(IEnumerable<JellyfinEpisode> source, IEnumerable<RecentlyAddedLog> recentlyAdded)
{
var itemsToReturn = new HashSet<JellyfinEpisode>();
foreach (var ep in source.Where(x => x.Series.HasTvDb))
@ -537,7 +572,7 @@ namespace Ombi.Schedule.Jobs.Ombi
var plexMovies = plexContentToSend.Where(x => x.Type == PlexMediaTypeEntity.Movie);
var embyMovies = embyContentToSend.Where(x => x.Type == EmbyMediaType.Movie);
var jellyfinMovies = jellyfinContentToSend.Where(x => x.Type == JellyfinMediaType.Movie);
if ((plexMovies.Any() || embyMovies.Any()) && !settings.DisableMovies)
if ((plexMovies.Any() || embyMovies.Any() || jellyfinMovies.Any()) && !settings.DisableMovies)
{
sb.Append("<h1 style=\"text-align: center; max-width: 1042px;\">New Movies</h1><br /><br />");
sb.Append(
@ -568,7 +603,7 @@ namespace Ombi.Schedule.Jobs.Ombi
sb.Append("</table>");
}
if ((plexEpisodes.Any() || embyEp.Any()) || jellyfinEp.Any() && !settings.DisableTv)
if ((plexEpisodes.Any() || embyEp.Any() || jellyfinEp.Any()) && !settings.DisableTv)
{
sb.Append("<br /><br /><h1 style=\"text-align: center; max-width: 1042px;\">New TV</h1><br /><br />");
sb.Append(

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

@ -175,7 +175,7 @@ namespace Ombi.Schedule.Jobs.Plex
var allEps = Repo.GetAllEpisodes();
foreach (var content in allContent)
foreach (var content in allContent.OrderByDescending(x => x.viewGroup))
{
Logger.LogDebug($"Got type '{content.viewGroup}' to process");
if (content.viewGroup.Equals(PlexMediaType.Episode.ToString(), StringComparison.InvariantCultureIgnoreCase))

@ -17,7 +17,7 @@ namespace Ombi.Schedule.Jobs.Radarr
{
public class RadarrSync : IRadarrSync
{
public RadarrSync(ISettingsService<RadarrSettings> radarr, IRadarrApi radarrApi, ILogger<RadarrSync> log, ExternalContext ctx)
public RadarrSync(ISettingsService<RadarrSettings> radarr, IRadarrV3Api radarrApi, ILogger<RadarrSync> log, ExternalContext ctx)
{
RadarrSettings = radarr;
RadarrApi = radarrApi;
@ -27,7 +27,7 @@ namespace Ombi.Schedule.Jobs.Radarr
}
private ISettingsService<RadarrSettings> RadarrSettings { get; }
private IRadarrApi RadarrApi { get; }
private IRadarrV3Api RadarrApi { get; }
private ILogger<RadarrSync> Logger { get; }
private readonly ExternalContext _ctx;

@ -9,6 +9,8 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Ombi.Api.Sonarr;
using Ombi.Api.Sonarr.Models;
using Ombi.Api.TheMovieDb;
using Ombi.Api.TheMovieDb.Models;
using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Schedule.Jobs.Radarr;
@ -21,12 +23,14 @@ namespace Ombi.Schedule.Jobs.Sonarr
{
public class SonarrSync : ISonarrSync
{
public SonarrSync(ISettingsService<SonarrSettings> s, ISonarrApi api, ILogger<SonarrSync> l, ExternalContext ctx)
public SonarrSync(ISettingsService<SonarrSettings> s, ISonarrApi api, ILogger<SonarrSync> l, ExternalContext ctx,
IMovieDbApi movieDbApi)
{
_settings = s;
_api = api;
_log = l;
_ctx = ctx;
_movieDbApi = movieDbApi;
_settings.ClearCache();
}
@ -34,6 +38,7 @@ namespace Ombi.Schedule.Jobs.Sonarr
private readonly ISonarrApi _api;
private readonly ILogger<SonarrSync> _log;
private readonly ExternalContext _ctx;
private readonly IMovieDbApi _movieDbApi;
public async Task Execute(IJobExecutionContext job)
{
@ -48,7 +53,17 @@ namespace Ombi.Schedule.Jobs.Sonarr
if (series != null)
{
var sonarrSeries = series as ImmutableHashSet<SonarrSeries> ?? series.ToImmutableHashSet();
var ids = sonarrSeries.Select(x => x.tvdbId);
var ids = sonarrSeries.Select(x => new SonarrDto
{
TvDbId = x.tvdbId,
ImdbId = x.imdbId,
Title = x.title,
MovieDbId = 0,
Id = x.id,
Monitored = x.monitored,
EpisodeFileCount = x.episodeFileCount
}).ToHashSet();
var strat = _ctx.Database.CreateExecutionStrategy();
await strat.ExecuteAsync(async () =>
{
@ -60,12 +75,27 @@ namespace Ombi.Schedule.Jobs.Sonarr
});
var existingSeries = await _ctx.SonarrCache.Select(x => x.TvDbId).ToListAsync();
//var entites = ids.Except(existingSeries).Select(id => new SonarrCache { TvDbId = id }).ToImmutableHashSet();
var entites = ids.Select(id => new SonarrCache { TvDbId = id }).ToImmutableHashSet();
await _ctx.SonarrCache.AddRangeAsync(entites);
entites.Clear();
var sonarrCacheToSave = new HashSet<SonarrCache>();
foreach (var id in ids)
{
var cache = new SonarrCache
{
TvDbId = id.TvDbId
};
var findResult = await _movieDbApi.Find(id.TvDbId.ToString(), ExternalSource.tvdb_id);
if (findResult.tv_results.Any())
{
cache.TheMovieDbId = findResult.tv_results.FirstOrDefault()?.id ?? -1;
id.MovieDbId = cache.TheMovieDbId;
}
sonarrCacheToSave.Add(cache);
}
await _ctx.SonarrCache.AddRangeAsync(sonarrCacheToSave);
await _ctx.SaveChangesAsync();
sonarrCacheToSave.Clear();
strat = _ctx.Database.CreateExecutionStrategy();
await strat.ExecuteAsync(async () =>
{
@ -76,15 +106,15 @@ namespace Ombi.Schedule.Jobs.Sonarr
}
});
foreach (var s in sonarrSeries)
foreach (var s in ids)
{
if (!s.monitored || s.episodeFileCount == 0) // We have files
if (!s.Monitored || s.EpisodeFileCount == 0) // We have files
{
continue;
}
_log.LogDebug("Syncing series: {0}", s.title);
var episodes = await _api.GetEpisodes(s.id, settings.ApiKey, settings.FullUri);
_log.LogDebug("Syncing series: {0}", s.Title);
var episodes = await _api.GetEpisodes(s.Id, settings.ApiKey, settings.FullUri);
var monitoredEpisodes = episodes.Where(x => x.monitored || x.hasFile);
//var allExistingEpisodes = await _ctx.SonarrEpisodeCache.Where(x => x.TvDbId == s.tvdbId).ToListAsync();
@ -95,7 +125,8 @@ namespace Ombi.Schedule.Jobs.Sonarr
{
EpisodeNumber = episode.episodeNumber,
SeasonNumber = episode.seasonNumber,
TvDbId = s.tvdbId,
TvDbId = s.TvDbId,
MovieDbId = s.MovieDbId,
HasFile = episode.hasFile
});
//var episodesToAdd = new List<SonarrEpisodeCache>();
@ -166,5 +197,16 @@ namespace Ombi.Schedule.Jobs.Sonarr
Dispose(true);
GC.SuppressFinalize(this);
}
private class SonarrDto
{
public int TvDbId { get; set; }
public string ImdbId { get; set; }
public string Title { get; set; }
public int MovieDbId { get; set; }
public int Id { get; set; }
public bool Monitored { get; set; }
public int EpisodeFileCount { get; set; }
}
}
}

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

@ -3,7 +3,6 @@
public class RadarrSettings : ExternalSettings
{
public bool Enabled { get; set; }
public bool V3 { get; set; }
public string ApiKey { get; set; }
public string DefaultQualityProfile { get; set; }
public string DefaultRootPath { get; set; }

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

@ -7,5 +7,9 @@ namespace Ombi.Core.Settings.Models.External
public bool ShowAdultMovies { get; set; }
public List<int> ExcludedKeywordIds { get; set; }
public List<int> ExcludedMovieGenreIds { get; set; }
public List<int> ExcludedTvGenreIds { 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; }

@ -41,12 +41,12 @@ namespace Ombi.Settings.Settings
var model = obj;
return model;
}, DateTime.Now.AddHours(2));
}, DateTimeOffset.Now.AddHours(2));
}
public async Task<T> GetSettingsAsync()
{
return await _cache.GetOrAdd(CacheName, async () =>
return await _cache.GetOrAddAsync(CacheName, async () =>
{
var result = await Repo.GetAsync(EntityName);
if (result == null)
@ -61,7 +61,7 @@ namespace Ombi.Settings.Settings
var model = obj;
return model;
}, DateTime.Now.AddHours(5));
}, DateTimeOffset.Now.AddHours(5));
}
public bool SaveSettings(T model)

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

Loading…
Cancel
Save