update poc quartz with develop

This commit is contained in:
tidusjar 2019-04-09 21:59:33 +01:00
commit e9ead2eddb
402 changed files with 30984 additions and 4844 deletions

View file

@ -53,8 +53,6 @@ namespace Ombi.Api.Emby
{
username,
pw = password,
password = password.GetSha1Hash().ToLower(),
passwordMd5 = password.CalcuateMd5Hash()
};
request.AddJsonBody(body);
@ -98,7 +96,7 @@ namespace Ombi.Api.Emby
request.AddQueryString("Fields", "ProviderIds,Overview");
request.AddQueryString("VirtualItem", "False");
request.AddQueryString("IsVirtualItem", "False");
return await Api.Request<EmbyItemContainer<EmbyMovie>>(request);
}
@ -149,7 +147,7 @@ namespace Ombi.Api.Emby
request.AddQueryString("IncludeItemTypes", type);
request.AddQueryString("Fields", includeOverview ? "ProviderIds,Overview" : "ProviderIds");
request.AddQueryString("VirtualItem", "False");
request.AddQueryString("IsVirtualItem", "False");
AddHeaders(request, apiKey);
@ -167,7 +165,7 @@ namespace Ombi.Api.Emby
request.AddQueryString("startIndex", startIndex.ToString());
request.AddQueryString("limit", count.ToString());
request.AddQueryString("VirtualItem", "False");
request.AddQueryString("IsVirtualItem", "False");
AddHeaders(request, apiKey);

View file

@ -24,16 +24,5 @@ namespace Ombi.Api.Github
request.AddHeader("User-Agent", "Ombi");
return await _api.Request<List<CakeThemes>>(request);
}
public async Task<string> GetThemesRawContent(string url)
{
var sections = url.Split('/');
var lastPart = sections.Last();
url = url.Replace(lastPart, string.Empty);
var request = new Request(lastPart, url, HttpMethod.Get);
request.AddHeader("Accept", "application/vnd.github.v3+json");
request.AddHeader("User-Agent", "Ombi");
return await _api.RequestContent(request);
}
}
}

View file

@ -7,6 +7,5 @@ namespace Ombi.Api.Github
public interface IGithubApi
{
Task<List<CakeThemes>> GetCakeThemes();
Task<string> GetThemesRawContent(string url);
}
}

View file

@ -0,0 +1,36 @@
using System.Net.Http;
using System.Threading.Tasks;
namespace Ombi.Api.Gotify
{
public class GotifyApi : IGotifyApi
{
public GotifyApi(IApi api)
{
_api = api;
}
private readonly IApi _api;
public async Task PushAsync(string baseUrl, string accessToken, string subject, string body, sbyte priority)
{
var request = new Request("/message", baseUrl, HttpMethod.Post);
request.AddQueryString("token", accessToken);
request.AddHeader("Access-Token", accessToken);
request.ApplicationJsonContentType();
var jsonBody = new
{
message = body,
title = subject,
priority = priority
};
request.AddJsonBody(jsonBody);
await _api.Request(request);
}
}
}

View file

@ -0,0 +1,9 @@
using System.Threading.Tasks;
namespace Ombi.Api.Gotify
{
public interface IGotifyApi
{
Task PushAsync(string endpoint, string accessToken, string subject, string body, sbyte priority);
}
}

View file

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<AssemblyVersion>3.0.0.0</AssemblyVersion>
<FileVersion>3.0.0.0</FileVersion>
<Version></Version>
<PackageVersion></PackageVersion>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Ombi.Api\Ombi.Api.csproj" />
</ItemGroup>
</Project>

View file

@ -23,5 +23,6 @@ namespace Ombi.Api.Lidarr
Task<List<LanguageProfiles>> GetLanguageProfile(string apiKey, string baseUrl);
Task<LidarrStatus> Status(string apiKey, string baseUrl);
Task<CommandResult> AlbumSearch(int[] albumIds, string apiKey, string baseUrl);
Task<AlbumResponse> AlbumInformation(string albumId, string apiKey, string baseUrl);
}
}

View file

@ -105,6 +105,32 @@ namespace Ombi.Api.Lidarr
return Api.Request<List<AlbumResponse>>(request);
}
public async Task<AlbumResponse> AlbumInformation(string albumId, string apiKey, string baseUrl)
{
var request = new Request($"{ApiVersion}/album", baseUrl, HttpMethod.Get);
request.AddQueryString("foreignAlbumId", albumId);
AddHeaders(request, apiKey);
var albums = await Api.Request<List<AlbumResponse>>(request);
return albums.Where(x => x.foreignAlbumId.Equals(albumId, StringComparison.InvariantCultureIgnoreCase))
.FirstOrDefault();
}
/// <summary>
/// THIS ONLY SUPPORTS ALBUMS THAT THE ARTIST IS IN LIDARR
/// </summary>
/// <param name="albumId"></param>
/// <param name="apiKey"></param>
/// <param name="baseUrl"></param>
/// <returns></returns>
public Task<List<LidarrTrack>> GetTracksForAlbum(int albumId, string apiKey, string baseUrl)
{
var request = new Request($"{ApiVersion}/album", baseUrl, HttpMethod.Get);
request.AddQueryString("albumId", albumId.ToString());
AddHeaders(request, apiKey);
return Api.Request<List<LidarrTrack>>(request);
}
public Task<ArtistResult> AddArtist(ArtistAdd artist, string apiKey, string baseUrl)
{
var request = new Request($"{ApiVersion}/artist", baseUrl, HttpMethod.Post);

View file

@ -1,10 +1,15 @@
using System;
using System.Collections.Generic;
namespace Ombi.Api.Lidarr.Models
{
public class AlbumLookup
{
public string title { get; set; }
public string status { get; set; }
public string artistType { get; set; }
public string disambiguation { get; set; }
public List<LidarrLinks> links { get; set; }
public int artistId { get; set; }
public string foreignAlbumId { get; set; }
public bool monitored { get; set; }

View file

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Ombi.Api.Lidarr.Models
{
public class LidarrLinks
{
public string url { get; set; }
public string name { get; set; }
}
}

View file

@ -0,0 +1,8 @@
namespace Ombi.Api.Lidarr.Models
{
public class LidarrRatings
{
public int votes { get; set; }
public decimal value { get; set; }
}
}

View file

@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Ombi.Api.Lidarr.Models
{
public class LidarrTrack
{
public int artistId { get; set; }
public int trackFileId { get; set; }
public int albumId { get; set; }
public bool _explicit { get; set; }
public int absoluteTrackNumber { get; set; }
public string trackNumber { get; set; }
public string title { get; set; }
public int duration { get; set; }
public int mediumNumber { get; set; }
public bool hasFile { get; set; }
public bool monitored { get; set; }
public int id { get; set; }
}
}

View file

@ -9,7 +9,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
</ItemGroup>
<ItemGroup>

View file

@ -26,7 +26,7 @@ namespace Ombi.Api.Notifications
{
return null;
}
var id = await _appConfig.Get(ConfigurationTypes.Notification);
var id = await _appConfig.GetAsync(ConfigurationTypes.Notification);
var request = new Request(string.Empty, ApiUrl, HttpMethod.Post);
var body = new OneSignalNotificationBody

View file

@ -9,7 +9,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.2.0" />
</ItemGroup>
<ItemGroup>

View file

@ -11,7 +11,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Options" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.Options" Version="2.2.0" />
</ItemGroup>
<ItemGroup>

View file

@ -0,0 +1,13 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Ombi.Api.Sonarr.Models;
using System.Net.Http;
using Ombi.Api.Sonarr.Models.V3;
namespace Ombi.Api.Sonarr
{
public interface ISonarrV3Api : ISonarrApi
{
Task<IEnumerable<LanguageProfiles>> LanguageProfiles(string apiKey, string baseUrl);
}
}

View file

@ -27,6 +27,9 @@ namespace Ombi.Api.Sonarr.Models
public int id { get; set; }
public List<SonarrImage> images { get; set; }
// V3 Property
public int languageProfileId { get; set; }
/// <summary>
/// This is for us
/// </summary>

View file

@ -44,6 +44,7 @@ namespace Ombi.Api.Sonarr.Models
public DateTime added { get; set; }
public Ratings ratings { get; set; }
public int qualityProfileId { get; set; }
public int languageProfileId { get; set; }
public int id { get; set; }
public DateTime nextAiring { get; set; }
}

View file

@ -0,0 +1,30 @@
namespace Ombi.Api.Sonarr.Models.V3
{
public class LanguageProfiles
{
public string name { get; set; }
public bool upgradeAllowed { get; set; }
public Cutoff cutoff { get; set; }
public Languages[] languages { get; set; }
public int id { get; set; }
}
public class Cutoff
{
public int id { get; set; }
public string name { get; set; }
}
public class Languages
{
public Language languages { get; set; }
public bool allowed { get; set; }
}
public class Language
{
public int id { get; set; }
public string name { get; set; }
}
}

View file

@ -16,18 +16,19 @@ namespace Ombi.Api.Sonarr
Api = api;
}
private IApi Api { get; }
protected IApi Api { get; }
protected virtual string ApiBaseUrl => "/api/";
public async Task<IEnumerable<SonarrProfile>> GetProfiles(string apiKey, string baseUrl)
{
var request = new Request("/api/profile", baseUrl, HttpMethod.Get);
var request = new Request($"{ApiBaseUrl}profile", baseUrl, HttpMethod.Get);
request.AddHeader("X-Api-Key", apiKey);
return await Api.Request<List<SonarrProfile>>(request);
}
public async Task<IEnumerable<SonarrRootFolder>> GetRootFolders(string apiKey, string baseUrl)
{
var request = new Request("/api/rootfolder", baseUrl, HttpMethod.Get);
var request = new Request($"{ApiBaseUrl}rootfolder", baseUrl, HttpMethod.Get);
request.AddHeader("X-Api-Key", apiKey);
return await Api.Request<List<SonarrRootFolder>>(request);
}
@ -40,7 +41,7 @@ namespace Ombi.Api.Sonarr
/// <returns></returns>
public async Task<IEnumerable<SonarrSeries>> GetSeries(string apiKey, string baseUrl)
{
var request = new Request("/api/series", baseUrl, HttpMethod.Get);
var request = new Request($"{ApiBaseUrl}series", baseUrl, HttpMethod.Get);
request.AddHeader("X-Api-Key", apiKey);
var results = await Api.Request<List<SonarrSeries>>(request);
@ -63,7 +64,7 @@ namespace Ombi.Api.Sonarr
/// <returns></returns>
public async Task<SonarrSeries> GetSeriesById(int id, string apiKey, string baseUrl)
{
var request = new Request($"/api/series/{id}", baseUrl, HttpMethod.Get);
var request = new Request($"{ApiBaseUrl}series/{id}", baseUrl, HttpMethod.Get);
request.AddHeader("X-Api-Key", apiKey);
var result = await Api.Request<SonarrSeries>(request);
if (result?.seasons?.Length > 0)
@ -82,7 +83,7 @@ namespace Ombi.Api.Sonarr
/// <returns></returns>
public async Task<SonarrSeries> UpdateSeries(SonarrSeries updated, string apiKey, string baseUrl)
{
var request = new Request("/api/series/", baseUrl, HttpMethod.Put);
var request = new Request($"{ApiBaseUrl}series/", baseUrl, HttpMethod.Put);
request.AddHeader("X-Api-Key", apiKey);
request.AddJsonBody(updated);
return await Api.Request<SonarrSeries>(request);
@ -94,7 +95,7 @@ namespace Ombi.Api.Sonarr
{
return new NewSeries { ErrorMessages = new List<string> { seriesToAdd.Validate() } };
}
var request = new Request("/api/series/", baseUrl, HttpMethod.Post);
var request = new Request($"{ApiBaseUrl}series/", baseUrl, HttpMethod.Post);
request.AddHeader("X-Api-Key", apiKey);
request.AddJsonBody(seriesToAdd);
@ -120,7 +121,7 @@ namespace Ombi.Api.Sonarr
/// <returns></returns>
public async Task<IEnumerable<Episode>> GetEpisodes(int seriesId, string apiKey, string baseUrl)
{
var request = new Request($"/api/Episode?seriesId={seriesId}", baseUrl, HttpMethod.Get);
var request = new Request($"{ApiBaseUrl}Episode?seriesId={seriesId}", baseUrl, HttpMethod.Get);
request.AddHeader("X-Api-Key", apiKey);
return await Api.Request<List<Episode>>(request);
}
@ -134,14 +135,14 @@ namespace Ombi.Api.Sonarr
/// <returns></returns>
public async Task<Episode> GetEpisodeById(int episodeId, string apiKey, string baseUrl)
{
var request = new Request($"/api/Episode/{episodeId}", baseUrl, HttpMethod.Get);
var request = new Request($"{ApiBaseUrl}Episode/{episodeId}", baseUrl, HttpMethod.Get);
request.AddHeader("X-Api-Key", apiKey);
return await Api.Request<Episode>(request);
}
public async Task<EpisodeUpdateResult> UpdateEpisode(Episode episodeToUpdate, string apiKey, string baseUrl)
{
var request = new Request($"/api/Episode/", baseUrl, HttpMethod.Put);
var request = new Request($"{ApiBaseUrl}Episode/", baseUrl, HttpMethod.Put);
request.AddHeader("X-Api-Key", apiKey);
request.AddJsonBody(episodeToUpdate);
return await Api.Request<EpisodeUpdateResult>(request);
@ -189,7 +190,7 @@ namespace Ombi.Api.Sonarr
private async Task<CommandResult> Command(string apiKey, string baseUrl, object body)
{
var request = new Request($"/api/Command/", baseUrl, HttpMethod.Post);
var request = new Request($"{ApiBaseUrl}Command/", baseUrl, HttpMethod.Post);
request.AddHeader("X-Api-Key", apiKey);
request.AddJsonBody(body);
return await Api.Request<CommandResult>(request);
@ -197,7 +198,7 @@ namespace Ombi.Api.Sonarr
public async Task<SystemStatus> SystemStatus(string apiKey, string baseUrl)
{
var request = new Request("/api/system/status", baseUrl, HttpMethod.Get);
var request = new Request($"{ApiBaseUrl}system/status", baseUrl, HttpMethod.Get);
request.AddHeader("X-Api-Key", apiKey);
return await Api.Request<SystemStatus>(request);
@ -217,7 +218,7 @@ namespace Ombi.Api.Sonarr
ignoreEpisodesWithoutFiles = false,
}
};
var request = new Request("/api/seasonpass", baseUrl, HttpMethod.Post);
var request = new Request($"{ApiBaseUrl}seasonpass", baseUrl, HttpMethod.Post);
request.AddHeader("X-Api-Key", apiKey);
request.AddJsonBody(seasonPass);

View file

@ -0,0 +1,25 @@
using System.Net.Http;
using System.Collections.Generic;
using System.Threading.Tasks;
using Ombi.Api.Sonarr.Models.V3;
namespace Ombi.Api.Sonarr
{
public class SonarrV3Api : SonarrApi, ISonarrV3Api
{
public SonarrV3Api(IApi api) : base(api)
{
}
protected override string ApiBaseUrl => "/api/v3/";
public async Task<IEnumerable<LanguageProfiles>> LanguageProfiles(string apiKey, string baseUrl)
{
var request = new Request($"{ApiBaseUrl}languageprofile", baseUrl, HttpMethod.Get);
request.AddHeader("X-Api-Key", apiKey);
return await Api.Request<List<LanguageProfiles>>(request);
}
}
}

View file

@ -41,7 +41,7 @@ namespace Ombi.Api
{
if (!request.IgnoreErrors)
{
LogError(request, httpResponseMessage);
await LogError(request, httpResponseMessage);
}
if (request.Retry)
@ -105,7 +105,7 @@ namespace Ombi.Api
{
if (!request.IgnoreErrors)
{
LogError(request, httpResponseMessage);
await LogError(request, httpResponseMessage);
}
}
// do something with the response
@ -126,7 +126,7 @@ namespace Ombi.Api
{
if (!request.IgnoreErrors)
{
LogError(request, httpResponseMessage);
await LogError(request, httpResponseMessage);
}
}
}
@ -149,10 +149,15 @@ namespace Ombi.Api
}
}
private void LogError(Request request, HttpResponseMessage httpResponseMessage)
private async Task LogError(Request request, HttpResponseMessage httpResponseMessage)
{
Logger.LogError(LoggingEvents.Api,
$"StatusCode: {httpResponseMessage.StatusCode}, Reason: {httpResponseMessage.ReasonPhrase}, RequestUri: {request.FullUri}");
if (Logger.IsEnabled(LogLevel.Debug))
{
var content = await httpResponseMessage.Content.ReadAsStringAsync();
Logger.LogDebug(content);
}
}
}
}

View file

@ -9,8 +9,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.1.1" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.2.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
<PackageReference Include="Polly" Version="6.1.0" />
<PackageReference Include="System.Xml.XmlSerializer" Version="4.3.0" />
</ItemGroup>

View file

@ -0,0 +1,73 @@
using System.Collections.Generic;
using System.Linq;
using System.Security.Principal;
using System.Threading.Tasks;
using AutoFixture;
using Moq;
using NUnit.Framework;
using Ombi.Core.Authentication;
using Ombi.Core.Engine;
using Ombi.Core.Engine.Interfaces;
using Ombi.Core.Rule.Interfaces;
using Ombi.Core.Settings;
using Ombi.Settings.Settings.Models;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
namespace Ombi.Core.Tests.Engine
{
[TestFixture]
public class VoteEngineTests
{
[SetUp]
public void Setup()
{
F = new Fixture();
VoteRepository = new Mock<IRepository<Votes>>();
VoteSettings = new Mock<ISettingsService<VoteSettings>>();
MusicRequestEngine = new Mock<IMusicRequestEngine>();
TvRequestEngine = new Mock<ITvRequestEngine>();
MovieRequestEngine = new Mock<IMovieRequestEngine>();
MovieRequestEngine = new Mock<IMovieRequestEngine>();
User = new Mock<IPrincipal>();
UserManager = new Mock<OmbiUserManager>();
UserManager.Setup(x => x.Users)
.Returns(new EnumerableQuery<OmbiUser>(new List<OmbiUser> {new OmbiUser {Id = "abc"}}));
Rule = new Mock<IRuleEvaluator>();
Engine = new VoteEngine(VoteRepository.Object, User.Object, UserManager.Object, Rule.Object, VoteSettings.Object, MusicRequestEngine.Object,
TvRequestEngine.Object, MovieRequestEngine.Object);
}
public Fixture F { get; set; }
public VoteEngine Engine { get; set; }
public Mock<IPrincipal> User { get; set; }
public Mock<OmbiUserManager> UserManager { get; set; }
public Mock<IRuleEvaluator> Rule { get; set; }
public Mock<IRepository<Votes>> VoteRepository { get; set; }
public Mock<ISettingsService<VoteSettings>> VoteSettings { get; set; }
public Mock<IMusicRequestEngine> MusicRequestEngine { get; set; }
public Mock<ITvRequestEngine> TvRequestEngine { get; set; }
public Mock<IMovieRequestEngine> MovieRequestEngine { get; set; }
[Test]
[Ignore("Need to mock the user manager")]
public async Task New_Upvote()
{
VoteSettings.Setup(x => x.GetSettingsAsync()).ReturnsAsync(new VoteSettings());
var votes = F.CreateMany<Votes>().ToList();
votes.Add(new Votes
{
RequestId = 1,
RequestType = RequestType.Movie,
UserId = "abc"
});
VoteRepository.Setup(x => x.GetAll()).Returns(new EnumerableQuery<Votes>(votes));
var result = await Engine.UpVote(1, RequestType.Movie);
Assert.That(result.Result, Is.True);
VoteRepository.Verify(x => x.Add(It.Is<Votes>(c => c.UserId == "abc" && c.VoteType == VoteType.Upvote)), Times.Once);
VoteRepository.Verify(x => x.Delete(It.IsAny<Votes>()), Times.Once);
MovieRequestEngine.Verify(x => x.ApproveMovieById(1), Times.Never);
}
}
}

View file

@ -1,15 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<TargetFramework>netcoreapp2.2</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Moq" Version="4.9.0" />
<PackageReference Include="AutoFixture" Version="4.5.0" />
<PackageReference Include="Moq" Version="4.10.0" />
<PackageReference Include="Nunit" Version="3.10.1" />
<PackageReference Include="NUnit.ConsoleRunner" Version="3.8.0" />
<PackageReference Include="NUnit.ConsoleRunner" Version="3.9.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.10.0" />
<packagereference Include="Microsoft.NET.Test.Sdk" Version="15.8.0"></packagereference>
<packagereference Include="Microsoft.NET.Test.Sdk" Version="15.9.0"></packagereference>
</ItemGroup>
<ItemGroup>

View file

@ -4,6 +4,7 @@ using Moq;
using Ombi.Core.Rule.Rules.Request;
using Ombi.Store.Entities.Requests;
using NUnit.Framework;
using Ombi.Core.Authentication;
using Ombi.Helpers;
namespace Ombi.Core.Tests.Rule.Request
@ -16,7 +17,7 @@ namespace Ombi.Core.Tests.Rule.Request
{
PrincipalMock = new Mock<IPrincipal>();
Rule = new AutoApproveRule(PrincipalMock.Object);
Rule = new AutoApproveRule(PrincipalMock.Object, null);
}

View file

@ -3,6 +3,7 @@ using System.Threading.Tasks;
using Moq;
using NUnit.Framework;
using Ombi.Core.Rule.Rules;
using Ombi.Core.Rule.Rules.Request;
using Ombi.Helpers;
using Ombi.Store.Entities.Requests;
@ -15,7 +16,7 @@ namespace Ombi.Core.Tests.Rule.Request
{
PrincipalMock = new Mock<IPrincipal>();
Rule = new CanRequestRule(PrincipalMock.Object);
Rule = new CanRequestRule(PrincipalMock.Object, null);
}

View file

@ -18,13 +18,13 @@ namespace Ombi.Core.Tests.Rule.Search
[SetUp]
public void Setup()
{
ContextMock = new Mock<IRepository<CouchPotatoCache>>();
ContextMock = new Mock<IExternalRepository<CouchPotatoCache>>();
Rule = new CouchPotatoCacheRule(ContextMock.Object);
}
private CouchPotatoCacheRule Rule { get; set; }
private Mock<IRepository<CouchPotatoCache>> ContextMock { get; set; }
private Mock<IExternalRepository<CouchPotatoCache>> ContextMock { get; set; }
[Test]
public async Task Should_ReturnApproved_WhenMovieIsInCouchPotato()

View file

@ -16,7 +16,7 @@ namespace Ombi.Core.Tests.Rule.Search
public void Setup()
{
ContextMock = new Mock<IEmbyContentRepository>();
Rule = new EmbyAvailabilityRule(ContextMock.Object);
Rule = new EmbyAvailabilityRule(ContextMock.Object, null);
}
private EmbyAvailabilityRule Rule { get; set; }

View file

@ -1,4 +1,5 @@
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Moq;
using NUnit.Framework;
using Ombi.Core.Models.Search;
@ -14,7 +15,7 @@ namespace Ombi.Core.Tests.Rule.Search
public void Setup()
{
ContextMock = new Mock<IPlexContentRepository>();
Rule = new PlexAvailabilityRule(ContextMock.Object);
Rule = new PlexAvailabilityRule(ContextMock.Object, new Mock<ILogger<PlexAvailabilityRule>>().Object);
}
private PlexAvailabilityRule Rule { get; set; }

View file

@ -15,13 +15,13 @@ namespace Ombi.Core.Tests.Rule.Search
[SetUp]
public void Setup()
{
ContextMock = new Mock<IRepository<RadarrCache>>();
ContextMock = new Mock<IExternalRepository<RadarrCache>>();
Rule = new RadarrCacheRule(ContextMock.Object);
}
private RadarrCacheRule Rule { get; set; }
private Mock<IRepository<RadarrCache>> ContextMock { get; set; }
private Mock<IExternalRepository<RadarrCache>> ContextMock { get; set; }
[Test]
public async Task Should_ReturnApproved_WhenMovieIsInRadarr()

View file

@ -29,6 +29,7 @@ using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Ombi.Api.Emby;
@ -101,6 +102,22 @@ namespace Ombi.Core.Authentication
return true;
}
public async Task<OmbiUser> GetOmbiUserFromPlexToken(string plexToken)
{
var plexAccount = await _plexApi.GetAccount(plexToken);
// Check for a ombi user
if (plexAccount?.user != null)
{
var potentialOmbiUser = await Users.FirstOrDefaultAsync(x =>
x.ProviderUserId == plexAccount.user.id);
return potentialOmbiUser;
}
return null;
}
/// <summary>
/// Sign the user into plex and make sure we can get the authentication token.
/// <remarks>We do not check if the user is in the owners "friends" since they must have a local user account to get this far</remarks>

View file

@ -157,6 +157,24 @@ namespace Ombi.Core.Engine
}
}
private string defaultLangCode;
protected async Task<string> DefaultLanguageCode(string currentCode)
{
if (currentCode.HasValue())
{
return currentCode;
}
var s = await GetOmbiSettings();
return s.DefaultLanguageCode;
}
private OmbiSettings ombiSettings;
protected async Task<OmbiSettings> GetOmbiSettings()
{
return ombiSettings ?? (ombiSettings = await OmbiSettings.GetSettingsAsync());
}
public class HideResult
{
public bool Hide { get; set; }

View file

@ -0,0 +1,106 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Principal;
using System.Text;
using System.Threading.Tasks;
using AutoMapper;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Ombi.Api.TheMovieDb;
using Ombi.Api.TheMovieDb.Models;
using Ombi.Config;
using Ombi.Core.Authentication;
using Ombi.Core.Models.Requests;
using Ombi.Core.Models.Search;
using Ombi.Core.Rule.Interfaces;
using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Settings.Settings.Models;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
namespace Ombi.Core.Engine.Demo
{
public class DemoMovieSearchEngine : MovieSearchEngine, IDemoMovieSearchEngine
{
public DemoMovieSearchEngine(IPrincipal identity, IRequestServiceMain service, IMovieDbApi movApi, IMapper mapper,
ILogger<MovieSearchEngine> logger, IRuleEvaluator r, OmbiUserManager um, ICacheService mem, ISettingsService<OmbiSettings> s,
IRepository<RequestSubscription> sub, IOptions<DemoLists> lists)
: base(identity, service, movApi, mapper, logger, r, um, mem, s, sub)
{
_demoLists = lists.Value;
}
private readonly DemoLists _demoLists;
public async Task<IEnumerable<SearchMovieViewModel>> Search(string search)
{
var result = await MovieApi.SearchMovie(search, null, "en");
for (var i = 0; i < result.Count; i++)
{
if (!_demoLists.Movies.Contains(result[i].Id))
{
result.RemoveAt(i);
}
}
if(result.Count > 0)
return await TransformMovieResultsToResponse(result.Take(MovieLimit)); // Take x to stop us overloading the API
return null;
}
public async Task<IEnumerable<SearchMovieViewModel>> NowPlayingMovies()
{
var rand = new Random();
var responses = new List<SearchMovieViewModel>();
for (int i = 0; i < 10; i++)
{
var item = rand.Next(_demoLists.Movies.Length);
var movie = _demoLists.Movies[item];
if (responses.Any(x => x.Id == movie))
{
i--;
continue;
}
var movieResult = await MovieApi.GetMovieInformationWithExtraInfo(movie);
var viewMovie = Mapper.Map<SearchMovieViewModel>(movieResult);
responses.Add(await ProcessSingleMovie(viewMovie));
}
return responses;
}
public async Task<IEnumerable<SearchMovieViewModel>> PopularMovies()
{
return await NowPlayingMovies();
}
public async Task<IEnumerable<SearchMovieViewModel>> TopRatedMovies()
{
return await NowPlayingMovies();
}
public async Task<IEnumerable<SearchMovieViewModel>> UpcomingMovies()
{
return await NowPlayingMovies();
}
}
public interface IDemoMovieSearchEngine
{
Task<IEnumerable<SearchMovieViewModel>> NowPlayingMovies();
Task<IEnumerable<SearchMovieViewModel>> PopularMovies();
Task<IEnumerable<SearchMovieViewModel>> Search(string search);
Task<IEnumerable<SearchMovieViewModel>> TopRatedMovies();
Task<IEnumerable<SearchMovieViewModel>> UpcomingMovies();
}
}

View file

@ -0,0 +1,96 @@
using AutoMapper;
using Microsoft.Extensions.Options;
using Ombi.Api.Trakt;
using Ombi.Api.TvMaze;
using Ombi.Config;
using Ombi.Core.Authentication;
using Ombi.Core.Models.Requests;
using Ombi.Core.Models.Search;
using Ombi.Core.Rule.Interfaces;
using Ombi.Core.Settings;
using Ombi.Core.Settings.Models.External;
using Ombi.Helpers;
using Ombi.Settings.Settings.Models;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Principal;
using System.Threading.Tasks;
namespace Ombi.Core.Engine.Demo
{
public class DemoTvSearchEngine : TvSearchEngine, IDemoTvSearchEngine
{
public DemoTvSearchEngine(IPrincipal identity, IRequestServiceMain service, ITvMazeApi tvMaze, IMapper mapper,
ISettingsService<PlexSettings> plexSettings, ISettingsService<EmbySettings> embySettings, IPlexContentRepository repo,
IEmbyContentRepository embyRepo, ITraktApi trakt, IRuleEvaluator r, OmbiUserManager um, ICacheService memCache,
ISettingsService<OmbiSettings> s, IRepository<RequestSubscription> sub, IOptions<DemoLists> lists)
: base(identity, service, tvMaze, mapper, plexSettings, embySettings, repo, embyRepo, trakt, r, um, memCache, s, sub)
{
_demoLists = lists.Value;
}
private readonly DemoLists _demoLists;
public async Task<IEnumerable<SearchTvShowViewModel>> Search(string search)
{
var searchResult = await TvMazeApi.Search(search);
for (var i = 0; i < searchResult.Count; i++)
{
if (!_demoLists.TvShows.Contains(searchResult[i].show?.externals?.thetvdb ?? 0))
{
searchResult.RemoveAt(i);
}
}
if (searchResult != null)
{
var retVal = new List<SearchTvShowViewModel>();
foreach (var tvMazeSearch in searchResult)
{
if (tvMazeSearch.show.externals == null || !(tvMazeSearch.show.externals?.thetvdb.HasValue ?? false))
{
continue;
}
retVal.Add(ProcessResult(tvMazeSearch));
}
return retVal;
}
return null;
}
public async Task<IEnumerable<SearchTvShowViewModel>> NowPlayingMovies()
{
var rand = new Random();
var responses = new List<SearchTvShowViewModel>();
for (int i = 0; i < 10; i++)
{
var item = rand.Next(_demoLists.TvShows.Length);
var tv = _demoLists.TvShows[item];
if (responses.Any(x => x.Id == tv))
{
i--;
continue;
}
var movieResult = await TvMazeApi.ShowLookup(tv);
responses.Add(ProcessResult(movieResult));
}
return responses;
}
}
public interface IDemoTvSearchEngine
{
Task<IEnumerable<SearchTvShowViewModel>> Search(string search);
Task<IEnumerable<SearchTvShowViewModel>> NowPlayingMovies();
}
}

View file

@ -12,7 +12,7 @@ namespace Ombi.Core.Engine
{
Task<RequestEngineResult>ApproveAlbum(AlbumRequest request);
Task<RequestEngineResult> ApproveAlbumById(int requestId);
Task<RequestEngineResult> DenyAlbumById(int modelId);
Task<RequestEngineResult> DenyAlbumById(int modelId, string reason);
Task<IEnumerable<AlbumRequest>> GetRequests();
Task<RequestsViewModel<AlbumRequest>> GetRequests(int count, int position, OrderFilterModel orderFilter);
Task<int> GetTotal();

View file

@ -0,0 +1,19 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Ombi.Core.Models;
using Ombi.Core.Models.UI;
using Ombi.Store.Entities;
namespace Ombi.Core.Engine
{
public interface IVoteEngine
{
Task<VoteEngineResult> DownVote(int requestId, RequestType requestType);
Task<Votes> GetVoteForUser(int requestId, string userId);
IQueryable<Votes> GetVotes(int requestId, RequestType requestType);
Task RemoveCurrentVote(Votes currentVote);
Task<VoteEngineResult> UpVote(int requestId, RequestType requestType);
Task<List<VoteViewModel>> GetMovieViewModel();
}
}

View file

@ -7,11 +7,8 @@ using Ombi.Core.Models.Search;
using Ombi.Core.Rule.Interfaces;
using Ombi.Store.Entities.Requests;
using Ombi.Store.Entities;
using Microsoft.AspNetCore.Identity;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Ombi.Core.Authentication;
using Ombi.Helpers;
namespace Ombi.Core.Engine.Interfaces
{

View file

@ -10,14 +10,15 @@ namespace Ombi.Core
Task<IEnumerable<SearchMovieViewModel>> PopularMovies();
Task<IEnumerable<SearchMovieViewModel>> Search(string search);
Task<IEnumerable<SearchMovieViewModel>> Search(string search, int? year, string languageCode);
Task<IEnumerable<SearchMovieViewModel>> TopRatedMovies();
Task<IEnumerable<SearchMovieViewModel>> UpcomingMovies();
Task<SearchMovieViewModel> LookupImdbInformation(int theMovieDbId);
Task<SearchMovieViewModel> LookupImdbInformation(int theMovieDbId, string langCode = null);
Task<IEnumerable<SearchMovieViewModel>> SimilarMovies(int theMovieDbId);
Task<IEnumerable<SearchMovieViewModel>> SimilarMovies(int theMovieDbId, string langCode);
Task<IEnumerable<SearchMovieViewModel>> SearchActor(string search, string langaugeCode);
}
}

View file

@ -12,10 +12,11 @@ namespace Ombi.Core.Engine.Interfaces
Task<IEnumerable<MovieRequests>> SearchMovieRequest(string search);
Task RemoveMovieRequest(int requestId);
Task RemoveAllMovieRequests();
Task<MovieRequests> UpdateMovieRequest(MovieRequests request);
Task<RequestEngineResult> ApproveMovie(MovieRequests request);
Task<RequestEngineResult> ApproveMovieById(int requestId);
Task<RequestEngineResult> DenyMovieById(int modelId);
Task<RequestEngineResult> DenyMovieById(int modelId, string denyReason);
}
}

View file

@ -12,5 +12,6 @@ namespace Ombi.Core.Engine
Task<IEnumerable<SearchAlbumViewModel>> GetArtistAlbums(string foreignArtistId);
Task<IEnumerable<SearchAlbumViewModel>> SearchAlbum(string search);
Task<IEnumerable<SearchArtistViewModel>> SearchArtist(string search);
Task<SearchAlbumViewModel> GetAlbumInformation(string foreignAlbumId);
}
}

View file

@ -12,7 +12,7 @@ namespace Ombi.Core.Engine.Interfaces
Task RemoveTvRequest(int requestId);
Task<TvRequests> GetTvRequest(int requestId);
Task<RequestEngineResult> RequestTvShow(TvRequestViewModel tv);
Task<RequestEngineResult> DenyChildRequest(int requestId);
Task<RequestEngineResult> DenyChildRequest(int requestId, string reason);
Task<RequestsViewModel<TvRequests>> GetRequestsLite(int count, int position, OrderFilterModel type);
Task<IEnumerable<TvRequests>> SearchTvRequest(string search);
Task<TvRequests> UpdateTvRequest(TvRequests request);

View file

@ -51,7 +51,7 @@ namespace Ombi.Core.Engine
/// <returns></returns>
public async Task<RequestEngineResult> RequestMovie(MovieRequestViewModel model)
{
var movieInfo = await MovieApi.GetMovieInformationWithExtraInfo(model.TheMovieDbId);
var movieInfo = await MovieApi.GetMovieInformationWithExtraInfo(model.TheMovieDbId, model.LanguageCode);
if (movieInfo == null || movieInfo.Id == 0)
{
return new RequestEngineResult
@ -82,7 +82,9 @@ namespace Ombi.Core.Engine
RequestedDate = DateTime.UtcNow,
Approved = false,
RequestedUserId = userDetails.Id,
Background = movieInfo.BackdropPath
Background = movieInfo.BackdropPath,
LangCode = model.LanguageCode,
RequestedByAlias = model.RequestedByAlias
};
var usDates = movieInfo.ReleaseDates?.Results?.FirstOrDefault(x => x.IsoCode == "US");
@ -305,7 +307,7 @@ namespace Ombi.Core.Engine
return await ApproveMovie(request);
}
public async Task<RequestEngineResult> DenyMovieById(int modelId)
public async Task<RequestEngineResult> DenyMovieById(int modelId, string denyReason)
{
var request = await MovieRepository.Find(modelId);
if (request == null)
@ -317,12 +319,14 @@ namespace Ombi.Core.Engine
}
request.Denied = true;
request.DeniedReason = denyReason;
// We are denying a request
NotificationHelper.Notify(request, NotificationType.RequestDeclined);
await MovieRepository.Update(request);
return new RequestEngineResult
{
Result = true,
Message = "Request successfully deleted",
};
}
@ -416,6 +420,12 @@ namespace Ombi.Core.Engine
await MovieRepository.Delete(request);
}
public async Task RemoveAllMovieRequests()
{
var request = MovieRepository.GetAll();
await MovieRepository.DeleteRange(request);
}
public async Task<bool> UserHasRequest(string userId)
{
return await MovieRepository.GetAll().AnyAsync(x => x.RequestedUserId == userId);
@ -483,7 +493,7 @@ namespace Ombi.Core.Engine
RequestType = RequestType.Movie,
});
return new RequestEngineResult {Result = true, Message = $"{movieName} has been successfully added!"};
return new RequestEngineResult {Result = true, Message = $"{movieName} has been successfully added!", RequestId = model.Id};
}
public async Task<RequestQuotaCountModel> GetRemainingRequests(OmbiUser user)

View file

@ -1,23 +1,22 @@
using System;
using AutoMapper;
using AutoMapper;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Ombi.Api.TheMovieDb;
using Ombi.Api.TheMovieDb.Models;
using Ombi.Core.Authentication;
using Ombi.Core.Models.Requests;
using Ombi.Core.Models.Search;
using System.Collections.Generic;
using System.Linq;
using System.Security.Principal;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Ombi.Core.Rule.Interfaces;
using Microsoft.Extensions.Caching.Memory;
using Ombi.Core.Authentication;
using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Settings.Settings.Models;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Principal;
using System.Threading.Tasks;
namespace Ombi.Core.Engine
{
@ -32,18 +31,21 @@ namespace Ombi.Core.Engine
Logger = logger;
}
private IMovieDbApi MovieApi { get; }
private IMapper Mapper { get; }
protected IMovieDbApi MovieApi { get; }
protected IMapper Mapper { get; }
private ILogger<MovieSearchEngine> Logger { get; }
protected const int MovieLimit = 10;
/// <summary>
/// Lookups the imdb information.
/// </summary>
/// <param name="theMovieDbId">The movie database identifier.</param>
/// <returns></returns>
public async Task<SearchMovieViewModel> LookupImdbInformation(int theMovieDbId)
public async Task<SearchMovieViewModel> LookupImdbInformation(int theMovieDbId, string langCode = null)
{
var movieInfo = await MovieApi.GetMovieInformationWithExtraInfo(theMovieDbId);
langCode = await DefaultLanguageCode(langCode);
var movieInfo = await MovieApi.GetMovieInformationWithExtraInfo(theMovieDbId, langCode);
var viewMovie = Mapper.Map<SearchMovieViewModel>(movieInfo);
return await ProcessSingleMovie(viewMovie, true);
@ -52,31 +54,58 @@ namespace Ombi.Core.Engine
/// <summary>
/// Searches the specified movie.
/// </summary>
/// <param name="search">The search.</param>
/// <returns></returns>
public async Task<IEnumerable<SearchMovieViewModel>> Search(string search)
public async Task<IEnumerable<SearchMovieViewModel>> Search(string search, int? year, string langaugeCode)
{
var result = await MovieApi.SearchMovie(search);
langaugeCode = await DefaultLanguageCode(langaugeCode);
var result = await MovieApi.SearchMovie(search, year, langaugeCode);
if (result != null)
{
return await TransformMovieResultsToResponse(result.Take(10)); // Take 10 to stop us overloading the API
return await TransformMovieResultsToResponse(result.Take(MovieLimit)); // Take x to stop us overloading the API
}
return null;
}
public async Task<IEnumerable<SearchMovieViewModel>> SearchActor(string search, string langaugeCode)
{
langaugeCode = await DefaultLanguageCode(langaugeCode);
var people = await MovieApi.SearchByActor(search, langaugeCode);
var person = people?.results?.Count > 0 ? people.results.FirstOrDefault() : null;
var resultSet = new List<SearchMovieViewModel>();
if (person == null)
{
return resultSet;
}
// Get this person movie credits
var credits = await MovieApi.GetActorMovieCredits(person.id, langaugeCode);
// Grab results from both cast and crew, prefer items in cast. we can handle directors like this.
var movieResults = (from role in credits.cast select new { Id = role.id, Title = role.title, ReleaseDate = role.release_date }).ToList();
movieResults.AddRange((from job in credits.crew select new { Id = job.id, Title = job.title, ReleaseDate = job.release_date }).ToList());
movieResults = movieResults.Take(10).ToList();
foreach (var movieResult in movieResults)
{
resultSet.Add(await LookupImdbInformation(movieResult.Id, langaugeCode));
}
return resultSet;
}
/// <summary>
/// Get similar movies to the id passed in
/// </summary>
/// <param name="theMovieDbId"></param>
/// <returns></returns>
public async Task<IEnumerable<SearchMovieViewModel>> SimilarMovies(int theMovieDbId)
public async Task<IEnumerable<SearchMovieViewModel>> SimilarMovies(int theMovieDbId, string langCode)
{
var result = await MovieApi.SimilarMovies(theMovieDbId);
langCode = await DefaultLanguageCode(langCode);
var result = await MovieApi.SimilarMovies(theMovieDbId, langCode);
if (result != null)
{
Logger.LogDebug("Search Result: {result}", result);
return await TransformMovieResultsToResponse(result.Take(10)); // Take 10 to stop us overloading the API
return await TransformMovieResultsToResponse(result.Take(MovieLimit)); // Take x to stop us overloading the API
}
return null;
}
@ -87,10 +116,15 @@ namespace Ombi.Core.Engine
/// <returns></returns>
public async Task<IEnumerable<SearchMovieViewModel>> PopularMovies()
{
var result = await Cache.GetOrAdd(CacheKeys.PopularMovies, async () => await MovieApi.PopularMovies(), DateTime.Now.AddHours(12));
var result = await Cache.GetOrAdd(CacheKeys.PopularMovies, async () =>
{
var langCode = await DefaultLanguageCode(null);
return await MovieApi.PopularMovies(langCode);
}, DateTime.Now.AddHours(12));
if (result != null)
{
return await TransformMovieResultsToResponse(result.Take(10)); // Take 10 to stop us overloading the API
return await TransformMovieResultsToResponse(result.Take(MovieLimit)); // Take x to stop us overloading the API
}
return null;
}
@ -101,10 +135,14 @@ namespace Ombi.Core.Engine
/// <returns></returns>
public async Task<IEnumerable<SearchMovieViewModel>> TopRatedMovies()
{
var result = await Cache.GetOrAdd(CacheKeys.TopRatedMovies, async () => await MovieApi.TopRated(), DateTime.Now.AddHours(12));
var result = await Cache.GetOrAdd(CacheKeys.TopRatedMovies, async () =>
{
var langCode = await DefaultLanguageCode(null);
return await MovieApi.TopRated(langCode);
}, DateTime.Now.AddHours(12));
if (result != null)
{
return await TransformMovieResultsToResponse(result.Take(10)); // Take 10 to stop us overloading the API
return await TransformMovieResultsToResponse(result.Take(MovieLimit)); // Take x to stop us overloading the API
}
return null;
}
@ -115,11 +153,15 @@ namespace Ombi.Core.Engine
/// <returns></returns>
public async Task<IEnumerable<SearchMovieViewModel>> UpcomingMovies()
{
var result = await Cache.GetOrAdd(CacheKeys.UpcomingMovies, async () => await MovieApi.Upcoming(), DateTime.Now.AddHours(12));
var result = await Cache.GetOrAdd(CacheKeys.UpcomingMovies, async () =>
{
var langCode = await DefaultLanguageCode(null);
return await MovieApi.Upcoming(langCode);
}, DateTime.Now.AddHours(12));
if (result != null)
{
Logger.LogDebug("Search Result: {result}", result);
return await TransformMovieResultsToResponse(result.Take(10)); // Take 10 to stop us overloading the API
return await TransformMovieResultsToResponse(result.Take(MovieLimit)); // Take x to stop us overloading the API
}
return null;
}
@ -130,15 +172,19 @@ namespace Ombi.Core.Engine
/// <returns></returns>
public async Task<IEnumerable<SearchMovieViewModel>> NowPlayingMovies()
{
var result = await Cache.GetOrAdd(CacheKeys.NowPlayingMovies, async () => await MovieApi.NowPlaying(), DateTime.Now.AddHours(12));
var result = await Cache.GetOrAdd(CacheKeys.NowPlayingMovies, async () =>
{
var langCode = await DefaultLanguageCode(null);
return await MovieApi.NowPlaying(langCode);
}, DateTime.Now.AddHours(12));
if (result != null)
{
return await TransformMovieResultsToResponse(result.Take(10)); // Take 10 to stop us overloading the API
return await TransformMovieResultsToResponse(result.Take(MovieLimit)); // Take x to stop us overloading the API
}
return null;
}
private async Task<List<SearchMovieViewModel>> TransformMovieResultsToResponse(
protected async Task<List<SearchMovieViewModel>> TransformMovieResultsToResponse(
IEnumerable<MovieSearchResult> movies)
{
var viewMovies = new List<SearchMovieViewModel>();
@ -149,24 +195,25 @@ namespace Ombi.Core.Engine
return viewMovies;
}
private async Task<SearchMovieViewModel> ProcessSingleMovie(SearchMovieViewModel viewMovie, bool lookupExtraInfo = false)
protected async Task<SearchMovieViewModel> ProcessSingleMovie(SearchMovieViewModel viewMovie, bool lookupExtraInfo = false)
{
if (lookupExtraInfo)
if (lookupExtraInfo && viewMovie.ImdbId.IsNullOrEmpty())
{
var showInfo = await MovieApi.GetMovieInformation(viewMovie.Id);
viewMovie.Id = showInfo.Id; // TheMovieDbId
viewMovie.ImdbId = showInfo.ImdbId;
var usDates = viewMovie.ReleaseDates?.Results?.FirstOrDefault(x => x.IsoCode == "US");
viewMovie.DigitalReleaseDate = usDates?.ReleaseDate?.FirstOrDefault(x => x.Type == ReleaseDateType.Digital)?.ReleaseDate;
}
var usDates = viewMovie.ReleaseDates?.Results?.FirstOrDefault(x => x.IsoCode == "US");
viewMovie.DigitalReleaseDate = usDates?.ReleaseDate?.FirstOrDefault(x => x.Type == ReleaseDateType.Digital)?.ReleaseDate;
viewMovie.TheMovieDbId = viewMovie.Id.ToString();
await RunSearchRules(viewMovie);
// This requires the rules to be run first to populate the RequestId property
await CheckForSubscription(viewMovie);
return viewMovie;
}
@ -174,9 +221,13 @@ namespace Ombi.Core.Engine
{
// Check if this user requested it
var user = await GetUser();
if (user == null)
{
return;
}
var request = await RequestService.MovieRequestService.GetAll()
.AnyAsync(x => x.RequestedUserId.Equals(user.Id) && x.TheMovieDbId == viewModel.Id);
if (request)
if (request || viewModel.Available)
{
viewModel.ShowSubscribe = false;
}

View file

@ -83,7 +83,8 @@ namespace Ombi.Core.Engine
Title = album.title,
Disk = album.images?.FirstOrDefault(x => x.coverType.Equals("disc"))?.url,
Cover = album.images?.FirstOrDefault(x => x.coverType.Equals("cover"))?.url,
ForeignArtistId = album?.artist?.foreignArtistId ?? string.Empty
ForeignArtistId = album?.artist?.foreignArtistId ?? string.Empty,
RequestedByAlias = model.RequestedByAlias
};
if (requestModel.Cover.IsNullOrEmpty())
{
@ -299,7 +300,7 @@ namespace Ombi.Core.Engine
return await ApproveAlbum(request);
}
public async Task<RequestEngineResult> DenyAlbumById(int modelId)
public async Task<RequestEngineResult> DenyAlbumById(int modelId, string reason)
{
var request = await MusicRepository.Find(modelId);
if (request == null)
@ -311,6 +312,7 @@ namespace Ombi.Core.Engine
}
request.Denied = true;
request.DeniedReason = reason;
// We are denying a request
NotificationHelper.Notify(request, NotificationType.RequestDeclined);
await MusicRepository.Update(request);
@ -495,7 +497,7 @@ namespace Ombi.Core.Engine
RequestType = RequestType.Album,
});
return new RequestEngineResult { Result = true, Message = $"{model.Title} has been successfully added!" };
return new RequestEngineResult { Result = true, Message = $"{model.Title} has been successfully added!", RequestId = model.Id };
}

View file

@ -60,6 +60,18 @@ namespace Ombi.Core.Engine
return vm;
}
public async Task<SearchAlbumViewModel> GetAlbumInformation(string foreignAlbumId)
{
var settings = await GetSettings();
var result = await _lidarrApi.AlbumInformation(foreignAlbumId, settings.ApiKey, settings.FullUri);
var vm = await MapIntoAlbumVm(result, settings);
return vm;
}
/// <summary>
/// Searches the specified artist
/// </summary>
@ -143,6 +155,48 @@ namespace Ombi.Core.Engine
return vm;
}
// TODO
private async Task<SearchAlbumViewModel> MapIntoAlbumVm(AlbumResponse a, LidarrSettings settings)
{
var vm = new SearchAlbumViewModel
{
ForeignAlbumId = a.foreignAlbumId,
Monitored = a.monitored,
Rating = a.ratings?.value ?? 0m,
ReleaseDate = a.releaseDate,
Title = a.title,
Disk = a.images?.FirstOrDefault(x => x.coverType.Equals("disc"))?.url?.Replace("http", "https"),
Genres = a.genres
};
if (a.artistId > 0)
{
//TODO THEY HAVE FIXED THIS IN DEV
// The JSON is different for some stupid reason
// Need to lookup the artist now and all the images -.-"
var artist = await _lidarrApi.GetArtist(a.artistId, settings.ApiKey, settings.FullUri);
vm.ArtistName = artist.artistName;
vm.ForeignArtistId = artist.foreignArtistId;
}
else
{
//vm.ForeignArtistId = a.artistId?.foreignArtistId;
//vm.ArtistName = a.artist?.artistName;
}
vm.Cover = a.images?.FirstOrDefault(x => x.coverType.Equals("cover"))?.url?.Replace("http", "https");
if (vm.Cover.IsNullOrEmpty())
{
//vm.Cover = a.remoteCover;
}
await Rules.StartSpecificRules(vm, SpecificRules.LidarrAlbum);
await RunSearchRules(vm);
return vm;
}
private async Task<SearchAlbumViewModel> MapIntoAlbumVm(AlbumLookup a, LidarrSettings settings)
{
var vm = new SearchAlbumViewModel
@ -152,7 +206,8 @@ namespace Ombi.Core.Engine
Rating = a.ratings?.value ?? 0m,
ReleaseDate = a.releaseDate,
Title = a.title,
Disk = a.images?.FirstOrDefault(x => x.coverType.Equals("disc"))?.url
Disk = a.images?.FirstOrDefault(x => x.coverType.Equals("disc"))?.url?.Replace("http", "https"),
Genres = a.genres
};
if (a.artistId > 0)
{
@ -169,7 +224,7 @@ namespace Ombi.Core.Engine
vm.ArtistName = a.artist?.artistName;
}
vm.Cover = a.images?.FirstOrDefault(x => x.coverType.Equals("cover"))?.url;
vm.Cover = a.images?.FirstOrDefault(x => x.coverType.Equals("cover"))?.url?.Replace("http", "https");
if (vm.Cover.IsNullOrEmpty())
{
vm.Cover = a.remoteCover;

View file

@ -6,5 +6,6 @@
public string Message { get; set; }
public bool IsError => !string.IsNullOrEmpty(ErrorMessage);
public string ErrorMessage { get; set; }
public int RequestId { get; set; }
}
}

View file

@ -31,14 +31,13 @@ namespace Ombi.Core.Engine
{
public TvRequestEngine(ITvMazeApi tvApi, IMovieDbApi movApi, IRequestServiceMain requestService, IPrincipal user,
INotificationHelper helper, IRuleEvaluator rule, OmbiUserManager manager,
ITvSender sender, IAuditRepository audit, IRepository<RequestLog> rl, ISettingsService<OmbiSettings> settings, ICacheService cache,
ITvSender sender, IRepository<RequestLog> rl, ISettingsService<OmbiSettings> settings, ICacheService cache,
IRepository<RequestSubscription> sub) : base(user, requestService, rule, manager, cache, settings, sub)
{
TvApi = tvApi;
MovieDbApi = movApi;
NotificationHelper = helper;
TvSender = sender;
Audit = audit;
_requestLog = rl;
}
@ -46,7 +45,6 @@ namespace Ombi.Core.Engine
private ITvMazeApi TvApi { get; }
private IMovieDbApi MovieDbApi { get; }
private ITvSender TvSender { get; }
private IAuditRepository Audit { get; }
private readonly IRepository<RequestLog> _requestLog;
public async Task<RequestEngineResult> RequestTvShow(TvRequestViewModel tv)
@ -84,8 +82,6 @@ namespace Ombi.Core.Engine
}
}
await Audit.Record(AuditType.Added, AuditArea.TvRequest, $"Added Request {tvBuilder.ChildRequest.Title}", Username);
var existingRequest = await TvRepository.Get().FirstOrDefaultAsync(x => x.TvDbId == tv.TvDbId);
if (existingRequest != null)
{
@ -116,6 +112,7 @@ namespace Ombi.Core.Engine
}
// Remove the ID since this is a new child
// This was a TVDBID for the request rules to run
tvBuilder.ChildRequest.Id = 0;
if (!tvBuilder.ChildRequest.SeasonRequests.Any())
{
@ -350,7 +347,6 @@ namespace Ombi.Core.Engine
public async Task<TvRequests> UpdateTvRequest(TvRequests request)
{
await Audit.Record(AuditType.Updated, AuditArea.TvRequest, $"Updated Request {request.Title}", Username);
var allRequests = TvRepository.Get();
var results = await allRequests.FirstOrDefaultAsync(x => x.Id == request.Id);
@ -384,6 +380,7 @@ namespace Ombi.Core.Engine
foreach (var ep in s.Episodes)
{
ep.Approved = true;
ep.Requested = true;
}
}
@ -392,7 +389,6 @@ namespace Ombi.Core.Engine
if (request.Approved)
{
NotificationHelper.Notify(request, NotificationType.RequestApproved);
await Audit.Record(AuditType.Approved, AuditArea.TvRequest, $"Approved Request {request.Title}", Username);
// Autosend
await TvSender.Send(request);
}
@ -402,7 +398,7 @@ namespace Ombi.Core.Engine
};
}
public async Task<RequestEngineResult> DenyChildRequest(int requestId)
public async Task<RequestEngineResult> DenyChildRequest(int requestId, string reason)
{
var request = await TvRepository.GetChild().FirstOrDefaultAsync(x => x.Id == requestId);
if (request == null)
@ -413,6 +409,7 @@ namespace Ombi.Core.Engine
};
}
request.Denied = true;
request.DeniedReason = reason;
await TvRepository.UpdateChild(request);
NotificationHelper.Notify(request, NotificationType.RequestDeclined);
return new RequestEngineResult
@ -423,9 +420,7 @@ namespace Ombi.Core.Engine
public async Task<ChildRequests> UpdateChildRequest(ChildRequests request)
{
await Audit.Record(AuditType.Updated, AuditArea.TvRequest, $"Updated Request {request.Title}", Username);
await TvRepository.UpdateChild(request);
await TvRepository.UpdateChild(request);
return request;
}
@ -443,16 +438,14 @@ namespace Ombi.Core.Engine
// Delete the parent
TvRepository.Db.TvRequests.Remove(parent);
}
await Audit.Record(AuditType.Deleted, AuditArea.TvRequest, $"Deleting Request {request.Title}", Username);
await TvRepository.Db.SaveChangesAsync();
}
public async Task RemoveTvRequest(int requestId)
{
var request = await TvRepository.Get().FirstOrDefaultAsync(x => x.Id == requestId);
await Audit.Record(AuditType.Deleted, AuditArea.TvRequest, $"Deleting Request {request.Title}", Username);
await TvRepository.Delete(request);
await TvRepository.Delete(request);
}
public async Task<bool> UserHasRequest(string userId)
@ -604,15 +597,16 @@ namespace Ombi.Core.Engine
var result = await TvSender.Send(model);
if (result.Success)
{
return new RequestEngineResult { Result = true };
return new RequestEngineResult { Result = true, RequestId = model.Id};
}
return new RequestEngineResult
{
ErrorMessage = result.Message
ErrorMessage = result.Message,
RequestId = model.Id
};
}
return new RequestEngineResult { Result = true };
return new RequestEngineResult { Result = true, RequestId = model.Id };
}
public async Task<RequestQuotaCountModel> GetRemainingRequests(OmbiUser user)

View file

@ -40,8 +40,8 @@ namespace Ombi.Core.Engine
EmbyContentRepo = embyRepo;
}
private ITvMazeApi TvMazeApi { get; }
private IMapper Mapper { get; }
protected ITvMazeApi TvMazeApi { get; }
protected IMapper Mapper { get; }
private ISettingsService<PlexSettings> PlexSettings { get; }
private ISettingsService<EmbySettings> EmbySettings { get; }
private IPlexContentRepository PlexContentRepo { get; }
@ -61,7 +61,7 @@ namespace Ombi.Core.Engine
{
continue;
}
retVal.Add(await ProcessResult(tvMazeSearch));
retVal.Add(ProcessResult(tvMazeSearch));
}
return retVal;
}
@ -99,7 +99,7 @@ namespace Ombi.Core.Engine
{
Url = e.url,
Title = e.name,
AirDate = DateTime.Parse(e.airstamp ?? DateTime.MinValue.ToString()),
AirDate = e.airstamp.HasValue() ? DateTime.Parse(e.airstamp) : DateTime.MinValue,
EpisodeNumber = e.number,
});
@ -112,7 +112,7 @@ namespace Ombi.Core.Engine
{
Url = e.url,
Title = e.name,
AirDate = DateTime.Parse(e.airstamp ?? DateTime.MinValue.ToString()),
AirDate = e.airstamp.HasValue() ? DateTime.Parse(e.airstamp) : DateTime.MinValue,
EpisodeNumber = e.number,
});
}
@ -123,7 +123,7 @@ namespace Ombi.Core.Engine
public async Task<IEnumerable<SearchTvShowViewModel>> Popular()
{
var result = await Cache.GetOrAdd(CacheKeys.PopularTv, async () => await TraktApi.GetPopularShows(), DateTime.Now.AddHours(12));
var processed = await ProcessResults(result);
var processed = ProcessResults(result);
return processed;
}
@ -131,35 +131,35 @@ namespace Ombi.Core.Engine
{
var result = await Cache.GetOrAdd(CacheKeys.AnticipatedTv, async () => await TraktApi.GetAnticipatedShows(), DateTime.Now.AddHours(12));
var processed = await ProcessResults(result);
var processed = ProcessResults(result);
return processed;
}
public async Task<IEnumerable<SearchTvShowViewModel>> MostWatches()
{
var result = await Cache.GetOrAdd(CacheKeys.MostWatchesTv, async () => await TraktApi.GetMostWatchesShows(), DateTime.Now.AddHours(12));
var processed = await ProcessResults(result);
var processed = ProcessResults(result);
return processed;
}
public async Task<IEnumerable<SearchTvShowViewModel>> Trending()
{
var result = await Cache.GetOrAdd(CacheKeys.TrendingTv, async () => await TraktApi.GetTrendingShows(), DateTime.Now.AddHours(12));
var processed = await ProcessResults(result);
var processed = ProcessResults(result);
return processed;
}
private async Task<IEnumerable<SearchTvShowViewModel>> ProcessResults<T>(IEnumerable<T> items)
protected IEnumerable<SearchTvShowViewModel> ProcessResults<T>(IEnumerable<T> items)
{
var retVal = new List<SearchTvShowViewModel>();
foreach (var tvMazeSearch in items)
{
retVal.Add(await ProcessResult(tvMazeSearch));
retVal.Add(ProcessResult(tvMazeSearch));
}
return retVal;
}
private async Task<SearchTvShowViewModel> ProcessResult<T>(T tvMazeSearch)
protected SearchTvShowViewModel ProcessResult<T>(T tvMazeSearch)
{
return Mapper.Map<SearchTvShowViewModel>(tvMazeSearch);
}

View file

@ -0,0 +1,263 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Principal;
using System.Text;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Ombi.Core.Authentication;
using Ombi.Core.Engine.Interfaces;
using Ombi.Core.Models;
using Ombi.Core.Models.UI;
using Ombi.Core.Rule.Interfaces;
using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Settings.Settings.Models;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
namespace Ombi.Core.Engine
{
public class VoteEngine : BaseEngine, IVoteEngine
{
public VoteEngine(IRepository<Votes> votes, IPrincipal user, OmbiUserManager um, IRuleEvaluator r, ISettingsService<VoteSettings> voteSettings,
IMusicRequestEngine musicRequestEngine, ITvRequestEngine tvRequestEngine, IMovieRequestEngine movieRequestEngine) : base(user, um, r)
{
_voteRepository = votes;
_voteSettings = voteSettings;
_movieRequestEngine = movieRequestEngine;
_musicRequestEngine = musicRequestEngine;
_tvRequestEngine = tvRequestEngine;
}
private readonly IRepository<Votes> _voteRepository;
private readonly ISettingsService<VoteSettings> _voteSettings;
private readonly IMusicRequestEngine _musicRequestEngine;
private readonly ITvRequestEngine _tvRequestEngine;
private readonly IMovieRequestEngine _movieRequestEngine;
public async Task<List<VoteViewModel>> GetMovieViewModel()
{
var vm = new List<VoteViewModel>();
var movieRequests = await _movieRequestEngine.GetRequests();
var tvRequestsTask = _tvRequestEngine.GetRequests();
var musicRequestsTask = _musicRequestEngine.GetRequests();
var user = await GetUser();
foreach (var r in movieRequests)
{
if (r.Available || r.Approved || (r.Denied ?? false))
{
continue;
}
// Make model
var votes = GetVotes(r.Id, RequestType.Movie);
var upVotes = await votes.Where(x => x.VoteType == VoteType.Upvote).CountAsync();
var downVotes = await votes.Where(x => x.VoteType == VoteType.Downvote).CountAsync();
var myVote = await votes.FirstOrDefaultAsync(x => x.UserId == user.Id && !x.Deleted);
vm.Add(new VoteViewModel
{
Upvotes = upVotes,
Downvotes = downVotes,
RequestId = r.Id,
RequestType = RequestType.Movie,
Title = r.Title,
Image = $"https://image.tmdb.org/t/p/w500/{r.PosterPath}",
Background = $"https://image.tmdb.org/t/p/w1280{r.Background}",
Description = r.Overview,
AlreadyVoted = myVote != null,
MyVote = myVote?.VoteType ?? VoteType.Downvote
});
}
foreach (var r in await musicRequestsTask)
{
if (r.Available || r.Approved || (r.Denied ?? false))
{
continue;
}
// Make model
var votes = GetVotes(r.Id, RequestType.Album);
var upVotes = await votes.Where(x => x.VoteType == VoteType.Upvote).CountAsync();
var downVotes = await votes.Where(x => x.VoteType == VoteType.Downvote).CountAsync();
var myVote = await votes.FirstOrDefaultAsync(x => x.UserId == user.Id && !x.Deleted);
vm.Add(new VoteViewModel
{
Upvotes = upVotes,
Downvotes = downVotes,
RequestId = r.Id,
RequestType = RequestType.Album,
Title = r.Title,
Image = r.Cover,
Background = r.Cover,
Description = r.ArtistName,
AlreadyVoted = myVote != null,
MyVote = myVote?.VoteType ?? VoteType.Downvote
});
}
foreach (var r in await tvRequestsTask)
{
foreach (var childRequests in r.ChildRequests)
{
var finalsb = new StringBuilder();
if (childRequests.Available || childRequests.Approved || (childRequests.Denied ?? false))
{
continue;
}
var votes = GetVotes(childRequests.Id, RequestType.TvShow);
// Make model
var upVotes = await votes.Where(x => x.VoteType == VoteType.Upvote).CountAsync();
var downVotes = await votes.Where(x => x.VoteType == VoteType.Downvote).CountAsync();
var myVote = await votes.FirstOrDefaultAsync(x => x.UserId == user.Id && !x.Deleted);
foreach (var epInformation in childRequests.SeasonRequests.OrderBy(x => x.SeasonNumber))
{
var orderedEpisodes = epInformation.Episodes.OrderBy(x => x.EpisodeNumber).ToList();
var episodeString = StringHelper.BuildEpisodeList(orderedEpisodes.Select(x => x.EpisodeNumber));
finalsb.Append($"Season: {epInformation.SeasonNumber} - Episodes: {episodeString}");
finalsb.Append("<br />");
}
vm.Add(new VoteViewModel
{
Upvotes = upVotes,
Downvotes = downVotes,
RequestId = childRequests.Id,
RequestType = RequestType.TvShow,
Title = r.Title,
Image = r.PosterPath,
Background = r.Background,
Description = finalsb.ToString(),
AlreadyVoted = myVote != null,
MyVote = myVote?.VoteType ?? VoteType.Downvote
});
}
}
return vm;
}
public IQueryable<Votes> GetVotes(int requestId, RequestType requestType)
{
return _voteRepository.GetAll().Where(x => x.RequestType == requestType && requestId == x.RequestId);
}
public Task<Votes> GetVoteForUser(int requestId, string userId)
{
return _voteRepository.GetAll().FirstOrDefaultAsync(x => x.RequestId == requestId && x.UserId == userId);
}
public async Task<VoteEngineResult> UpVote(int requestId, RequestType requestType)
{
var voteSettings = await _voteSettings.GetSettingsAsync();
if (!voteSettings.Enabled)
{
return new VoteEngineResult {Result = true};
}
// How many votes does this have?!
var currentVotes = GetVotes(requestId, requestType);
var user = await GetUser();
// Does this user have a downvote? If so we should revert it and make it an upvote
var currentVote = await GetVoteForUser(requestId, user.Id);
if (currentVote != null && currentVote.VoteType == VoteType.Upvote)
{
return new VoteEngineResult { ErrorMessage = "You have already voted!" };
}
await RemoveCurrentVote(currentVote);
await _movieRequestEngine.SubscribeToRequest(requestId, requestType);
await _voteRepository.Add(new Votes
{
Date = DateTime.UtcNow,
RequestId = requestId,
RequestType = requestType,
UserId = user.Id,
VoteType = VoteType.Upvote
});
var upVotes = await currentVotes.Where(x => x.VoteType == VoteType.Upvote).CountAsync();
var downVotes = -(await currentVotes.Where(x => x.VoteType == VoteType.Downvote).CountAsync());
var totalVotes = upVotes + downVotes;
RequestEngineResult result = null;
switch (requestType)
{
case RequestType.TvShow:
if (totalVotes >= voteSettings.TvShowVoteMax)
{
result = await _tvRequestEngine.ApproveChildRequest(requestId);
}
break;
case RequestType.Movie:
if (totalVotes >= voteSettings.MovieVoteMax)
{
result = await _movieRequestEngine.ApproveMovieById(requestId);
}
break;
case RequestType.Album:
if (totalVotes >= voteSettings.MusicVoteMax)
{
result = await _musicRequestEngine.ApproveAlbumById(requestId);
}
break;
default:
throw new ArgumentOutOfRangeException(nameof(requestType), requestType, null);
}
if (result != null && !result.Result)
{
return new VoteEngineResult
{
ErrorMessage = "Voted succesfully but could not approve!"
};
}
return new VoteEngineResult
{
Result = true
};
}
public async Task<VoteEngineResult> DownVote(int requestId, RequestType requestType)
{
var voteSettings = await _voteSettings.GetSettingsAsync();
if (!voteSettings.Enabled)
{
return new VoteEngineResult { Result = true };
}
var user = await GetUser();
var currentVote = await GetVoteForUser(requestId, user.Id);
if (currentVote != null && currentVote.VoteType == VoteType.Downvote)
{
return new VoteEngineResult { ErrorMessage = "You have already voted!" };
}
await RemoveCurrentVote(currentVote);
await _movieRequestEngine.UnSubscribeRequest(requestId, requestType);
await _voteRepository.Add(new Votes
{
Date = DateTime.UtcNow,
RequestId = requestId,
RequestType = requestType,
UserId = user.Id,
VoteType = VoteType.Downvote
});
return new VoteEngineResult
{
Result = true
};
}
public async Task RemoveCurrentVote(Votes currentVote)
{
if (currentVote != null)
{
await _voteRepository.Delete(currentVote);
}
}
}
}

View file

@ -41,7 +41,7 @@ namespace Ombi.Core.Helpers
ShowInfo = await TvApi.ShowLookupByTheTvDbId(id);
Results = await MovieDbApi.SearchTv(ShowInfo.name);
foreach (TvSearchResult result in Results) {
if (result.Name == ShowInfo.name)
if (result.Name.Equals(ShowInfo.name, StringComparison.InvariantCultureIgnoreCase))
{
var showIds = await MovieDbApi.GetTvExternals(result.Id);
ShowInfo.externals.imdb = showIds.imdb_id;
@ -64,14 +64,16 @@ namespace Ombi.Core.Helpers
{
ChildRequest = new ChildRequests
{
Id = model.TvDbId,
Id = model.TvDbId, // This is set to 0 after the request rules have run, the request rules needs it to identify the request
RequestType = RequestType.TvShow,
RequestedDate = DateTime.UtcNow,
Approved = false,
RequestedUserId = userId,
SeasonRequests = new List<SeasonRequests>(),
Title = ShowInfo.name,
SeriesType = ShowInfo.genres.Any( s => s.Equals("Anime", StringComparison.OrdinalIgnoreCase)) ? SeriesType.Anime : SeriesType.Standard
ReleaseYear = FirstAir,
RequestedByAlias = model.RequestedByAlias,
SeriesType = ShowInfo.genres.Any( s => s.Equals("Anime", StringComparison.InvariantCultureIgnoreCase)) ? SeriesType.Anime : SeriesType.Standard
};
return this;

View file

@ -24,10 +24,20 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using Newtonsoft.Json;
namespace Ombi.Core.Models.Requests
{
public class MovieRequestViewModel
{
public int TheMovieDbId { get; set; }
public string LanguageCode { get; set; } = "en";
/// <summary>
/// This is only set from a HTTP Header
/// </summary>
[JsonIgnore]
public string RequestedByAlias { get; set; }
}
}

View file

@ -3,5 +3,6 @@
public class MusicAlbumRequestViewModel
{
public string ForeignAlbumId { get; set; }
public string RequestedByAlias { get; set; }
}
}

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace Ombi.Core.Models.Requests
{
@ -9,6 +10,8 @@ namespace Ombi.Core.Models.Requests
public bool FirstSeason { get; set; }
public int TvDbId { get; set; }
public List<SeasonsViewModel> Seasons { get; set; } = new List<SeasonsViewModel>();
[JsonIgnore]
public string RequestedByAlias { get; set; }
}
public class SeasonsViewModel

View file

@ -16,8 +16,13 @@ namespace Ombi.Core.Models.Search
public string Cover { get; set; }
public string Disk { get; set; }
public decimal PercentOfTracks { get; set; }
public object[] Genres { get; set; }
public override RequestType Type => RequestType.Album;
public bool PartiallyAvailable => PercentOfTracks != 100 && PercentOfTracks > 0;
public bool FullyAvailable => PercentOfTracks == 100;
// Below is from the INFO call NEED A SEPERATE VM FOR THIS IN V4 TODO
// TODO ADD TRACK COUNT
}
}

View file

@ -0,0 +1,23 @@

using System.Collections.Generic;
using Ombi.Settings.Settings.Models.Notifications;
using Ombi.Store.Entities;
namespace Ombi.Core.Models.UI
{
/// <summary>
/// The view model for the notification settings page
/// </summary>
/// <seealso cref="GotifyNotificationSettings" />
public class GotifyNotificationViewModel : GotifySettings
{
/// <summary>
/// Gets or sets the notification templates.
/// </summary>
/// <value>
/// The notification templates.
/// </value>
public List<NotificationTemplates> NotificationTemplates { get; set; }
}
}

View file

@ -0,0 +1,18 @@
using Ombi.Store.Entities;
namespace Ombi.Core.Models.UI
{
public class VoteViewModel
{
public int RequestId { get; set; }
public RequestType RequestType { get; set; }
public string Image { get; set; }
public string Background { get; set; }
public int Upvotes { get; set; }
public int Downvotes { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public bool AlreadyVoted { get; set; }
public VoteType MyVote { get; set; }
}
}

View file

@ -0,0 +1,10 @@
namespace Ombi.Core.Models
{
public class VoteEngineResult
{
public bool Result { get; set; }
public string Message { get; set; }
public bool IsError => !string.IsNullOrEmpty(ErrorMessage);
public string ErrorMessage { get; set; }
}
}

View file

@ -11,25 +11,27 @@
<ItemGroup>
<PackageReference Include="AutoMapper" Version="6.1.1" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="3.2.0" />
<PackageReference Include="Hangfire" Version="1.6.19" />
<PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="2.1.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.3" />
<PackageReference Include="Hangfire" Version="1.6.22" />
<PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="2.2.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.2" />
<PackageReference Include="MiniProfiler.AspNetCore" Version="4.0.0-alpha6-79" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
<PackageReference Include="System.Diagnostics.Process" Version="4.3.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Ombi.Api.CouchPotato\Ombi.Api.CouchPotato.csproj" />
<ProjectReference Include="..\Ombi.Api.DogNzb\Ombi.Api.DogNzb.csproj" />
<ProjectReference Include="..\Ombi.Api.Emby\Ombi.Api.Emby.csproj" />
<ProjectReference Include="..\Ombi.Api.Lidarr\Ombi.Api.Lidarr.csproj" />
<ProjectReference Include="..\Ombi.Api.Plex\Ombi.Api.Plex.csproj" />
<ProjectReference Include="..\Ombi.Api.Radarr\Ombi.Api.Radarr.csproj" />
<ProjectReference Include="..\Ombi.Api.SickRage\Ombi.Api.SickRage.csproj" />
<ProjectReference Include="..\Ombi.Api.Sonarr\Ombi.Api.Sonarr.csproj" />
<ProjectReference Include="..\Ombi.Api.Trakt\Ombi.Api.Trakt.csproj" />
<ProjectReference Include="..\Ombi.Api.TvMaze\Ombi.Api.TvMaze.csproj" />
<ProjectReference Include="..\Ombi.Helpers\Ombi.Helpers.csproj" />
<ProjectReference Include="..\Ombi.Notifications\Ombi.Notifications.csproj" />
<ProjectReference Include="..\Ombi.Schedule\Ombi.Schedule.csproj" />
<ProjectReference Include="..\Ombi.Settings\Ombi.Settings.csproj" />
<ProjectReference Include="..\Ombi.Store\Ombi.Store.csproj" />
<ProjectReference Include="..\Ombi.TheMovieDbApi\Ombi.Api.TheMovieDb.csproj" />

View file

@ -1,5 +1,7 @@
using System.Security.Principal;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Ombi.Core.Authentication;
using Ombi.Core.Models.Requests;
using Ombi.Core.Rule.Interfaces;
using Ombi.Helpers;
@ -10,28 +12,31 @@ namespace Ombi.Core.Rule.Rules.Request
{
public class AutoApproveRule : BaseRequestRule, IRules<BaseRequest>
{
public AutoApproveRule(IPrincipal principal)
public AutoApproveRule(IPrincipal principal, OmbiUserManager um)
{
User = principal;
_manager = um;
}
private IPrincipal User { get; }
private readonly OmbiUserManager _manager;
public Task<RuleResult> Execute(BaseRequest obj)
public async Task<RuleResult> Execute(BaseRequest obj)
{
if (User.IsInRole(OmbiRoles.Admin))
var user = await _manager.Users.FirstOrDefaultAsync(x => x.UserName == User.Identity.Name);
if (await _manager.IsInRoleAsync(user, OmbiRoles.Admin))
{
obj.Approved = true;
return Task.FromResult(Success());
return Success();
}
if (obj.RequestType == RequestType.Movie && User.IsInRole(OmbiRoles.AutoApproveMovie))
if (obj.RequestType == RequestType.Movie && await _manager.IsInRoleAsync(user, OmbiRoles.AutoApproveMovie))
obj.Approved = true;
if (obj.RequestType == RequestType.TvShow && User.IsInRole(OmbiRoles.AutoApproveTv))
if (obj.RequestType == RequestType.TvShow && await _manager.IsInRoleAsync(user, OmbiRoles.AutoApproveTv))
obj.Approved = true;
if (obj.RequestType == RequestType.Album && User.IsInRole(OmbiRoles.AutoApproveMusic))
if (obj.RequestType == RequestType.Album && await _manager.IsInRoleAsync(user, OmbiRoles.AutoApproveMusic))
obj.Approved = true;
return Task.FromResult(Success()); // We don't really care, we just don't set the obj to approve
return Success(); // We don't really care, we just don't set the obj to approve
}
}
}

View file

@ -1,46 +1,52 @@
using Ombi.Store.Entities;
using System.Security.Claims;
using System.Security.Principal;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Ombi.Core.Authentication;
using Ombi.Core.Rule.Interfaces;
using Ombi.Helpers;
using Ombi.Store.Entities;
using Ombi.Store.Entities.Requests;
namespace Ombi.Core.Rule.Rules
namespace Ombi.Core.Rule.Rules.Request
{
public class CanRequestRule : BaseRequestRule, IRules<BaseRequest>
{
public CanRequestRule(IPrincipal principal)
public CanRequestRule(IPrincipal principal, OmbiUserManager manager)
{
User = principal;
_manager = manager;
}
private IPrincipal User { get; }
private readonly OmbiUserManager _manager;
public Task<RuleResult> Execute(BaseRequest obj)
public async Task<RuleResult> Execute(BaseRequest obj)
{
if (User.IsInRole(OmbiRoles.Admin))
return Task.FromResult(Success());
var user = await _manager.Users.FirstOrDefaultAsync(x => x.UserName == User.Identity.Name);
if (await _manager.IsInRoleAsync(user, OmbiRoles.Admin))
return Success();
if (obj.RequestType == RequestType.Movie)
{
if (User.IsInRole(OmbiRoles.RequestMovie) || User.IsInRole(OmbiRoles.AutoApproveMovie))
return Task.FromResult(Success());
return Task.FromResult(Fail("You do not have permissions to Request a Movie"));
if (await _manager.IsInRoleAsync(user, OmbiRoles.RequestMovie) || await _manager.IsInRoleAsync(user, OmbiRoles.AutoApproveMovie))
return Success();
return Fail("You do not have permissions to Request a Movie");
}
if (obj.RequestType == RequestType.TvShow)
{
if (User.IsInRole(OmbiRoles.RequestTv) || User.IsInRole(OmbiRoles.AutoApproveTv))
return Task.FromResult(Success());
if (await _manager.IsInRoleAsync(user, OmbiRoles.RequestTv) || await _manager.IsInRoleAsync(user, OmbiRoles.AutoApproveTv))
return Success();
}
if (obj.RequestType == RequestType.Album)
{
if (User.IsInRole(OmbiRoles.RequestMusic) || User.IsInRole(OmbiRoles.AutoApproveMusic))
return Task.FromResult(Success());
if (await _manager.IsInRoleAsync(user, OmbiRoles.RequestMusic) || await _manager.IsInRoleAsync(user, OmbiRoles.AutoApproveMusic))
return Success();
}
return Task.FromResult(Fail("You do not have permissions to Request a TV Show"));
return Fail("You do not have permissions to Request a TV Show");
}
}
}

View file

@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Ombi.Core.Rule.Interfaces;
using Ombi.Store.Entities;
using Ombi.Store.Entities.Requests;
using Ombi.Store.Repository;
namespace Ombi.Core.Rule.Rules.Request
{
public class ExistingPlexRequestRule : BaseRequestRule, IRules<BaseRequest>
{
public ExistingPlexRequestRule(IPlexContentRepository rv)
{
_plexContent = rv;
}
private readonly IPlexContentRepository _plexContent;
/// <summary>
/// We check if the request exists, if it does then we don't want to re-request it.
/// </summary>
/// <param name="obj">The object.</param>
/// <returns></returns>
public async Task<RuleResult> Execute(BaseRequest obj)
{
if (obj.RequestType == RequestType.TvShow)
{
var tvRequest = (ChildRequests) obj;
var tvContent = _plexContent.GetAll().Where(x => x.Type == PlexMediaTypeEntity.Show);
// We need to do a check on the TVDBId
var anyTvDbMatches = await tvContent.Include(x => x.Episodes).FirstOrDefaultAsync(x => x.HasTvDb && x.TvDbId.Equals(tvRequest.Id.ToString())); // the Id on the child is the tvdbid at this point
if (anyTvDbMatches == null)
{
// So we do not have a TVDB Id, that really sucks.
// Let's try and match on the title and year of the show
var titleAndYearMatch = await tvContent.Include(x=> x.Episodes).FirstOrDefaultAsync(x =>
x.Title.Equals(tvRequest.Title, StringComparison.InvariantCultureIgnoreCase)
&& x.ReleaseYear == tvRequest.ReleaseYear.Year.ToString());
if (titleAndYearMatch != null)
{
// We have a match! Suprise Motherfucker
return CheckExistingContent(tvRequest, titleAndYearMatch);
}
// We do not have this
return Success();
}
// looks like we have a match on the TVDbID
return CheckExistingContent(tvRequest, anyTvDbMatches);
}
return Success();
}
private RuleResult CheckExistingContent(ChildRequests child, PlexServerContent content)
{
foreach (var season in child.SeasonRequests)
{
var currentSeasonRequest =
content.Episodes.Where(x => x.SeasonNumber == season.SeasonNumber).ToList();
if (!currentSeasonRequest.Any())
{
continue;
}
foreach (var e in season.Episodes)
{
var hasEpisode = currentSeasonRequest.Any(x => x.EpisodeNumber == e.EpisodeNumber);
if (hasEpisode)
{
return Fail($"We already have episodes requested from series {child.Title}");
}
}
}
return Success();
}
}
}

View file

@ -0,0 +1,57 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Ombi.Core.Rule.Interfaces;
using Ombi.Store.Entities;
using Ombi.Store.Entities.Requests;
using Ombi.Store.Repository.Requests;
namespace Ombi.Core.Rule.Rules.Request
{
public class ExistingTvRequestRule : BaseRequestRule, IRules<BaseRequest>
{
public ExistingTvRequestRule(ITvRequestRepository rv)
{
Tv = rv;
}
private ITvRequestRepository Tv { get; }
/// <summary>
/// We check if the request exists, if it does then we don't want to re-request it.
/// </summary>
/// <param name="obj">The object.</param>
/// <returns></returns>
public async Task<RuleResult> Execute(BaseRequest obj)
{
if (obj.RequestType == RequestType.TvShow)
{
var tv = (ChildRequests) obj;
var tvRequests = Tv.GetChild();
var currentRequest = await tvRequests.FirstOrDefaultAsync(x => x.ParentRequest.TvDbId == tv.Id); // the Id on the child is the tvdbid at this point
if (currentRequest == null)
{
return Success();
}
foreach (var season in tv.SeasonRequests)
{
var currentSeasonRequest =
currentRequest.SeasonRequests.FirstOrDefault(x => x.SeasonNumber == season.SeasonNumber);
if (currentSeasonRequest == null)
{
continue;
}
foreach (var e in season.Episodes)
{
var hasEpisode = currentSeasonRequest.Episodes.Any(x => x.EpisodeNumber == e.EpisodeNumber);
if (hasEpisode)
{
return Fail($"We already have episodes requested from series {tv.Title}");
}
}
}
}
return Success();
}
}
}

View file

@ -7,12 +7,12 @@ namespace Ombi.Core.Rule.Rules.Request
{
public class SonarrCacheRequestRule : BaseRequestRule, IRules<BaseRequest>
{
public SonarrCacheRequestRule(IOmbiContext ctx)
public SonarrCacheRequestRule(IExternalContext ctx)
{
_ctx = ctx;
}
private readonly IOmbiContext _ctx;
private readonly IExternalContext _ctx;
public Task<RuleResult> Execute(BaseRequest obj)
{

View file

@ -0,0 +1,105 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Ombi.Core.Models.Search;
using Ombi.Store.Entities;
using Ombi.Store.Repository.Requests;
namespace Ombi.Core.Rule.Rules.Search
{
public static class AvailabilityRuleHelper
{
public static void CheckForUnairedEpisodes(SearchTvShowViewModel search)
{
if (search.SeasonRequests.All(x => x.Episodes.All(e => e.Available)))
{
search.FullyAvailable = true;
}
else
{
var airedButNotAvailable = search.SeasonRequests.Any(x =>
x.Episodes.Any(c => !c.Available && c.AirDate <= DateTime.Now.Date && c.AirDate != DateTime.MinValue));
if (!airedButNotAvailable)
{
var unairedEpisodes = search.SeasonRequests.Any(x =>
x.Episodes.Any(c => !c.Available && c.AirDate > DateTime.Now.Date || c.AirDate != DateTime.MinValue));
if (unairedEpisodes)
{
search.FullyAvailable = true;
}
}
}
}
public static async Task SingleEpisodeCheck(bool useImdb, IQueryable<PlexEpisode> allEpisodes, EpisodeRequests episode,
SeasonRequests season, PlexServerContent item, bool useTheMovieDb, bool useTvDb, ILogger log)
{
PlexEpisode epExists = null;
try
{
if (useImdb)
{
epExists = await allEpisodes.FirstOrDefaultAsync(x =>
x.EpisodeNumber == episode.EpisodeNumber && x.SeasonNumber == season.SeasonNumber &&
x.Series.ImdbId == item.ImdbId);
}
if (useTheMovieDb)
{
epExists = await allEpisodes.FirstOrDefaultAsync(x =>
x.EpisodeNumber == episode.EpisodeNumber && x.SeasonNumber == season.SeasonNumber &&
x.Series.TheMovieDbId == item.TheMovieDbId);
}
if (useTvDb)
{
epExists = await allEpisodes.FirstOrDefaultAsync(x =>
x.EpisodeNumber == episode.EpisodeNumber && x.SeasonNumber == season.SeasonNumber &&
x.Series.TvDbId == item.TvDbId);
}
}
catch (Exception e)
{
log.LogError(e, "Exception thrown when attempting to check if something is available");
}
if (epExists != null)
{
episode.Available = true;
}
}
public static async Task SingleEpisodeCheck(bool useImdb, IQueryable<EmbyEpisode> allEpisodes, EpisodeRequests episode,
SeasonRequests season, EmbyContent item, bool useTheMovieDb, bool useTvDb)
{
EmbyEpisode epExists = null;
if (useImdb)
{
epExists = await allEpisodes.FirstOrDefaultAsync(x =>
x.EpisodeNumber == episode.EpisodeNumber && x.SeasonNumber == season.SeasonNumber &&
x.Series.ImdbId == item.ImdbId);
}
if (useTheMovieDb)
{
epExists = await allEpisodes.FirstOrDefaultAsync(x =>
x.EpisodeNumber == episode.EpisodeNumber && x.SeasonNumber == season.SeasonNumber &&
x.Series.TheMovieDbId == item.TheMovieDbId);
}
if (useTvDb)
{
epExists = await allEpisodes.FirstOrDefaultAsync(x =>
x.EpisodeNumber == episode.EpisodeNumber && x.SeasonNumber == season.SeasonNumber &&
x.Series.TvDbId == item.TvDbId);
}
if (epExists != null)
{
episode.Available = true;
}
}
}
}

View file

@ -11,12 +11,12 @@ namespace Ombi.Core.Rule.Rules.Search
{
public class CouchPotatoCacheRule : BaseSearchRule, IRules<SearchViewModel>
{
public CouchPotatoCacheRule(IRepository<CouchPotatoCache> ctx)
public CouchPotatoCacheRule(IExternalRepository<CouchPotatoCache> ctx)
{
_ctx = ctx;
}
private readonly IRepository<CouchPotatoCache> _ctx;
private readonly IExternalRepository<CouchPotatoCache> _ctx;
public async Task<RuleResult> Execute(SearchViewModel obj)
{

View file

@ -1,10 +1,10 @@
using System;
using System.Linq;
using System.Linq.Expressions;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Ombi.Core.Models.Search;
using Ombi.Core.Rule.Interfaces;
using Ombi.Core.Settings;
using Ombi.Core.Settings.Models.External;
using Ombi.Helpers;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
@ -13,25 +13,39 @@ namespace Ombi.Core.Rule.Rules.Search
{
public class EmbyAvailabilityRule : BaseSearchRule, IRules<SearchViewModel>
{
public EmbyAvailabilityRule(IEmbyContentRepository repo)
public EmbyAvailabilityRule(IEmbyContentRepository repo, ISettingsService<EmbySettings> s)
{
EmbyContentRepository = repo;
EmbySettings = s;
}
private IEmbyContentRepository EmbyContentRepository { get; }
private ISettingsService<EmbySettings> EmbySettings { get; }
public async Task<RuleResult> Execute(SearchViewModel obj)
{
EmbyContent item = null;
var useImdb = false;
var useTheMovieDb = false;
var useTvDb = false;
if (obj.ImdbId.HasValue())
{
item = await EmbyContentRepository.GetByImdbId(obj.ImdbId);
if (item != null)
{
useImdb = true;
}
}
if (item == null)
{
if (obj.TheMovieDbId.HasValue())
{
item = await EmbyContentRepository.GetByTheMovieDbId(obj.TheMovieDbId);
if (item != null)
{
useTheMovieDb = true;
}
}
if (item == null)
@ -39,14 +53,27 @@ namespace Ombi.Core.Rule.Rules.Search
if (obj.TheTvDbId.HasValue())
{
item = await EmbyContentRepository.GetByTvDbId(obj.TheTvDbId);
if (item != null)
{
useTvDb = true;
}
}
}
}
if (item != null)
{
obj.Available = true;
obj.EmbyUrl = item.Url;
var s = await EmbySettings.GetSettingsAsync();
var server = s.Servers.FirstOrDefault(x => x.ServerHostname != null);
if ((server?.ServerHostname ?? string.Empty).HasValue())
{
obj.EmbyUrl = $"{server.ServerHostname}#!/itemdetails.html?id={item.EmbyId}";
}
else
{
obj.EmbyUrl = $"https://app.emby.media/#!/itemdetails.html?id={item.EmbyId}";
}
if (obj.Type == RequestType.TvShow)
{
@ -59,29 +86,12 @@ namespace Ombi.Core.Rule.Rules.Search
{
foreach (var episode in season.Episodes)
{
EmbyEpisode epExists = null;
if (item.HasImdb)
{
epExists = await allEpisodes.FirstOrDefaultAsync(e => e.EpisodeNumber == episode.EpisodeNumber && e.SeasonNumber == season.SeasonNumber
&& e.ImdbId == item.ImdbId);
} if (item.HasTvDb && epExists == null)
{
epExists = await allEpisodes.FirstOrDefaultAsync(e => e.EpisodeNumber == episode.EpisodeNumber && e.SeasonNumber == season.SeasonNumber
&& e.Series.TvDbId == item.TvDbId);
} if (item.HasTheMovieDb && epExists == null)
{
epExists = await allEpisodes.FirstOrDefaultAsync(e => e.EpisodeNumber == episode.EpisodeNumber && e.SeasonNumber == season.SeasonNumber
&& e.TheMovieDbId == item.TheMovieDbId);
}
if (epExists != null)
{
episode.Available = true;
}
await AvailabilityRuleHelper.SingleEpisodeCheck(useImdb, allEpisodes, episode, season, item, useTheMovieDb, useTvDb);
}
}
}
AvailabilityRuleHelper.CheckForUnairedEpisodes(search);
}
}
return Success();

View file

@ -1,4 +1,5 @@
using System.Linq;
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Ombi.Core.Models.Search;
@ -87,11 +88,11 @@ namespace Ombi.Core.Rule.Rules.Search
}
}
if (request.SeasonRequests.Any() && request.SeasonRequests.All(x => x.Episodes.All(e => e.Available)))
if (request.SeasonRequests.Any() && request.SeasonRequests.All(x => x.Episodes.All(e => e.Available && e.AirDate > DateTime.MinValue)))
{
request.FullyAvailable = true;
}
if (request.SeasonRequests.Any() && request.SeasonRequests.All(x => x.Episodes.Any(e => e.Available)))
if (request.SeasonRequests.Any() && request.SeasonRequests.All(x => x.Episodes.Any(e => e.Available && e.AirDate > DateTime.MinValue)))
{
request.PartlyAvailable = true;
}

View file

@ -10,12 +10,12 @@ namespace Ombi.Core.Rule.Rules.Search
{
public class LidarrAlbumCacheRule : SpecificRule, ISpecificRule<object>
{
public LidarrAlbumCacheRule(IRepository<LidarrAlbumCache> db)
public LidarrAlbumCacheRule(IExternalRepository<LidarrAlbumCache> db)
{
_db = db;
}
private readonly IRepository<LidarrAlbumCache> _db;
private readonly IExternalRepository<LidarrAlbumCache> _db;
public Task<RuleResult> Execute(object objec)
{

View file

@ -10,12 +10,12 @@ namespace Ombi.Core.Rule.Rules.Search
{
public class LidarrArtistCacheRule : SpecificRule, ISpecificRule<object>
{
public LidarrArtistCacheRule(IRepository<LidarrArtistCache> db)
public LidarrArtistCacheRule(IExternalRepository<LidarrArtistCache> db)
{
_db = db;
}
private readonly IRepository<LidarrArtistCache> _db;
private readonly IExternalRepository<LidarrArtistCache> _db;
public Task<RuleResult> Execute(object objec)
{

View file

@ -1,6 +1,6 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Ombi.Core.Models.Search;
using Ombi.Core.Rule.Interfaces;
using Ombi.Helpers;
@ -11,12 +11,14 @@ namespace Ombi.Core.Rule.Rules.Search
{
public class PlexAvailabilityRule : BaseSearchRule, IRules<SearchViewModel>
{
public PlexAvailabilityRule(IPlexContentRepository repo)
public PlexAvailabilityRule(IPlexContentRepository repo, ILogger<PlexAvailabilityRule> log)
{
PlexContentRepository = repo;
Log = log;
}
private IPlexContentRepository PlexContentRepository { get; }
private ILogger Log { get; }
public async Task<RuleResult> Execute(SearchViewModel obj)
{
@ -73,41 +75,17 @@ namespace Ombi.Core.Rule.Rules.Search
{
foreach (var episode in season.Episodes)
{
PlexEpisode epExists = null;
if (useImdb)
{
epExists = await allEpisodes.FirstOrDefaultAsync(x =>
x.EpisodeNumber == episode.EpisodeNumber && x.SeasonNumber == season.SeasonNumber &&
x.Series.ImdbId == item.ImdbId.ToString());
}
if (useTheMovieDb)
{
epExists = await allEpisodes.FirstOrDefaultAsync(x =>
x.EpisodeNumber == episode.EpisodeNumber && x.SeasonNumber == season.SeasonNumber &&
x.Series.TheMovieDbId == item.TheMovieDbId.ToString());
}
if (useTvDb)
{
epExists = await allEpisodes.FirstOrDefaultAsync(x =>
x.EpisodeNumber == episode.EpisodeNumber && x.SeasonNumber == season.SeasonNumber &&
x.Series.TvDbId == item.TvDbId.ToString());
}
if (epExists != null)
{
episode.Available = true;
}
await AvailabilityRuleHelper.SingleEpisodeCheck(useImdb, allEpisodes, episode, season, item, useTheMovieDb, useTvDb, Log);
}
}
if (search.SeasonRequests.All(x => x.Episodes.All(e => e.Available)))
{
search.FullyAvailable = true;
}
AvailabilityRuleHelper.CheckForUnairedEpisodes(search);
}
}
}
return Success();
}
}
}

View file

@ -9,12 +9,12 @@ namespace Ombi.Core.Rule.Rules.Search
{
public class RadarrCacheRule : BaseSearchRule, IRules<SearchViewModel>
{
public RadarrCacheRule(IRepository<RadarrCache> db)
public RadarrCacheRule(IExternalRepository<RadarrCache> db)
{
_db = db;
}
private readonly IRepository<RadarrCache> _db;
private readonly IExternalRepository<RadarrCache> _db;
public Task<RuleResult> Execute(SearchViewModel obj)
{

View file

@ -34,12 +34,12 @@ namespace Ombi.Core.Rule.Rules.Search
{
public class SonarrCacheSearchRule : BaseSearchRule, IRules<SearchViewModel>
{
public SonarrCacheSearchRule(IOmbiContext ctx)
public SonarrCacheSearchRule(IExternalContext ctx)
{
_ctx = ctx;
}
private readonly IOmbiContext _ctx;
private readonly IExternalContext _ctx;
public Task<RuleResult> Execute(SearchViewModel obj)
{

View file

@ -10,12 +10,12 @@ namespace Ombi.Core.Rule.Rules
{
public class SonarrCacheRule
{
public SonarrCacheRule(IOmbiContext ctx)
public SonarrCacheRule(IExternalContext ctx)
{
_ctx = ctx;
}
private readonly IOmbiContext _ctx;
private readonly IExternalContext _ctx;
public async Task<RuleResult> Execute(BaseRequest obj)
{

View file

@ -1,4 +1,5 @@
using System.Linq;
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
@ -19,7 +20,7 @@ namespace Ombi.Core.Senders
{
public MovieSender(ISettingsService<RadarrSettings> radarrSettings, IRadarrApi api, ILogger<MovieSender> log,
ISettingsService<DogNzbSettings> dogSettings, IDogNzbApi dogApi, ISettingsService<CouchPotatoSettings> cpSettings,
ICouchPotatoApi cpApi, IRepository<UserQualityProfiles> userProfiles)
ICouchPotatoApi cpApi, IRepository<UserQualityProfiles> userProfiles, IRepository<RequestQueue> requestQueue, INotificationHelper notify)
{
RadarrSettings = radarrSettings;
RadarrApi = api;
@ -29,6 +30,8 @@ namespace Ombi.Core.Senders
CouchPotatoSettings = cpSettings;
CouchPotatoApi = cpApi;
_userProfiles = userProfiles;
_requestQueuRepository = requestQueue;
_notificationHelper = notify;
}
private ISettingsService<RadarrSettings> RadarrSettings { get; }
@ -39,39 +42,63 @@ namespace Ombi.Core.Senders
private ISettingsService<CouchPotatoSettings> CouchPotatoSettings { get; }
private ICouchPotatoApi CouchPotatoApi { get; }
private readonly IRepository<UserQualityProfiles> _userProfiles;
private readonly IRepository<RequestQueue> _requestQueuRepository;
private readonly INotificationHelper _notificationHelper;
public async Task<SenderResult> Send(MovieRequests model)
{
var cpSettings = await CouchPotatoSettings.GetSettingsAsync();
//var watcherSettings = await WatcherSettings.GetSettingsAsync();
var radarrSettings = await RadarrSettings.GetSettingsAsync();
if (radarrSettings.Enabled)
try
{
return await SendToRadarr(model, radarrSettings);
}
var dogSettings = await DogNzbSettings.GetSettingsAsync();
if (dogSettings.Enabled)
{
await SendToDogNzb(model, dogSettings);
return new SenderResult
var cpSettings = await CouchPotatoSettings.GetSettingsAsync();
//var watcherSettings = await WatcherSettings.GetSettingsAsync();
var radarrSettings = await RadarrSettings.GetSettingsAsync();
if (radarrSettings.Enabled)
{
Success = true,
Sent = true,
};
}
return await SendToRadarr(model, radarrSettings);
}
if (cpSettings.Enabled)
var dogSettings = await DogNzbSettings.GetSettingsAsync();
if (dogSettings.Enabled)
{
await SendToDogNzb(model, dogSettings);
return new SenderResult
{
Success = true,
Sent = true,
};
}
if (cpSettings.Enabled)
{
return await SendToCp(model, cpSettings, cpSettings.DefaultProfileId);
}
}
catch (Exception e)
{
return await SendToCp(model, cpSettings, cpSettings.DefaultProfileId);
Log.LogError(e, "Error when sending movie to DVR app, added to the request queue");
// Check if already in request quee
var existingQueue = await _requestQueuRepository.FirstOrDefaultAsync(x => x.RequestId == model.Id);
if (existingQueue != null)
{
existingQueue.RetryCount++;
existingQueue.Error = e.Message;
await _requestQueuRepository.SaveChangesAsync();
}
else
{
await _requestQueuRepository.Add(new RequestQueue
{
Dts = DateTime.UtcNow,
Error = e.Message,
RequestId = model.Id,
Type = RequestType.Movie,
RetryCount = 0
});
_notificationHelper.Notify(model, NotificationType.ItemAddedToFaultQueue);
}
}
//if (watcherSettings.Enabled)
//{
// return SendToWatcher(model, watcherSettings);
//}
return new SenderResult
{
Success = true,
@ -93,21 +120,25 @@ namespace Ombi.Core.Senders
private async Task<SenderResult> SendToRadarr(MovieRequests model, RadarrSettings settings)
{
var qualityToUse = int.Parse(settings.DefaultQualityProfile);
var rootFolderPath = settings.DefaultRootPath;
var profiles = await _userProfiles.GetAll().FirstOrDefaultAsync(x => x.UserId == model.RequestedUserId);
if (profiles != null)
{
if (profiles.SonarrRootPathAnime > 0)
{
rootFolderPath = await RadarrRootPath(profiles.SonarrRootPathAnime, settings);
if (profiles.RadarrRootPath > 0)
{
var tempPath = await RadarrRootPath(profiles.RadarrRootPath, settings);
if (tempPath.HasValue())
{
rootFolderPath = tempPath;
}
}
if (profiles.SonarrQualityProfileAnime > 0)
if (profiles.RadarrQualityProfile > 0)
{
qualityToUse = profiles.SonarrQualityProfileAnime;
qualityToUse = profiles.RadarrQualityProfile;
}
}
@ -150,7 +181,7 @@ namespace Ombi.Core.Senders
// Search for it
if (!settings.AddOnly)
{
await RadarrApi.MovieSearch(new[] {existingMovie.id}, settings.ApiKey, settings.FullUri);
await RadarrApi.MovieSearch(new[] { existingMovie.id }, settings.ApiKey, settings.FullUri);
}
return new SenderResult { Success = true, Sent = true };
@ -163,7 +194,7 @@ namespace Ombi.Core.Senders
{
var paths = await RadarrApi.GetRootFolders(settings.ApiKey, settings.FullUri);
var selectedPath = paths.FirstOrDefault(x => x.id == overrideId);
return selectedPath.path;
return selectedPath?.path ?? String.Empty;
}
}
}

View file

@ -1,37 +1,76 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using EnsureThat;
using Microsoft.Extensions.Logging;
using Ombi.Api.Lidarr;
using Ombi.Api.Lidarr.Models;
using Ombi.Api.Radarr;
using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Settings.Settings.Models.External;
using Ombi.Store.Entities;
using Ombi.Store.Entities.Requests;
using Serilog;
using Ombi.Store.Repository;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace Ombi.Core.Senders
{
public class MusicSender : IMusicSender
{
public MusicSender(ISettingsService<LidarrSettings> lidarr, ILidarrApi lidarrApi)
public MusicSender(ISettingsService<LidarrSettings> lidarr, ILidarrApi lidarrApi, ILogger<MusicSender> log,
IRepository<RequestQueue> requestQueue, INotificationHelper notify)
{
_lidarrSettings = lidarr;
_lidarrApi = lidarrApi;
_log = log;
_requestQueueRepository = requestQueue;
_notificationHelper = notify;
}
private readonly ISettingsService<LidarrSettings> _lidarrSettings;
private readonly ILidarrApi _lidarrApi;
private readonly ILogger _log;
private readonly IRepository<RequestQueue> _requestQueueRepository;
private readonly INotificationHelper _notificationHelper;
public async Task<SenderResult> Send(AlbumRequest model)
{
var settings = await _lidarrSettings.GetSettingsAsync();
if (settings.Enabled)
try
{
return await SendToLidarr(model, settings);
var settings = await _lidarrSettings.GetSettingsAsync();
if (settings.Enabled)
{
return await SendToLidarr(model, settings);
}
return new SenderResult { Success = false, Sent = false, Message = "Lidarr is not enabled" };
}
catch (Exception e)
{
_log.LogError(e, "Exception thrown when sending a music to DVR app, added to the request queue");
var existingQueue = await _requestQueueRepository.FirstOrDefaultAsync(x => x.RequestId == model.Id);
if (existingQueue != null)
{
existingQueue.RetryCount++;
existingQueue.Error = e.Message;
await _requestQueueRepository.SaveChangesAsync();
}
else
{
await _requestQueueRepository.Add(new RequestQueue
{
Dts = DateTime.UtcNow,
Error = e.Message,
RequestId = model.Id,
Type = RequestType.Album,
RetryCount = 0
});
_notificationHelper.Notify(model, NotificationType.ItemAddedToFaultQueue);
}
}
return new SenderResult { Success = false, Sent = false, Message = "Lidarr is not enabled" };
return new SenderResult { Success = false, Sent = false, Message = "Something went wrong!" };
}
private async Task<SenderResult> SendToLidarr(AlbumRequest model, LidarrSettings settings)
@ -49,6 +88,11 @@ namespace Ombi.Core.Senders
if (artist == null || artist.id <= 0)
{
EnsureArg.IsNotNullOrEmpty(model.ForeignArtistId, nameof(model.ForeignArtistId));
EnsureArg.IsNotNullOrEmpty(model.ForeignAlbumId, nameof(model.ForeignAlbumId));
EnsureArg.IsNotNullOrEmpty(model.ArtistName, nameof(model.ArtistName));
EnsureArg.IsNotNullOrEmpty(rootFolderPath, nameof(rootFolderPath));
// Create artist
var newArtist = new ArtistAdd
{

View file

@ -16,16 +16,18 @@ using Ombi.Settings.Settings.Models.External;
using Ombi.Store.Entities;
using Ombi.Store.Entities.Requests;
using Ombi.Store.Repository;
using Remotion.Linq.Parsing.Structure.IntermediateModel;
namespace Ombi.Core.Senders
{
public class TvSender : ITvSender
{
public TvSender(ISonarrApi sonarrApi, ILogger<TvSender> log, ISettingsService<SonarrSettings> sonarrSettings,
public TvSender(ISonarrApi sonarrApi, ISonarrV3Api sonarrV3Api, ILogger<TvSender> log, ISettingsService<SonarrSettings> sonarrSettings,
ISettingsService<DogNzbSettings> dog, IDogNzbApi dogApi, ISettingsService<SickRageSettings> srSettings,
ISickRageApi srApi, IRepository<UserQualityProfiles> userProfiles)
ISickRageApi srApi, IRepository<UserQualityProfiles> userProfiles, IRepository<RequestQueue> requestQueue, INotificationHelper notify)
{
SonarrApi = sonarrApi;
SonarrV3Api = sonarrV3Api;
Logger = log;
SonarrSettings = sonarrSettings;
DogNzbSettings = dog;
@ -33,9 +35,12 @@ namespace Ombi.Core.Senders
SickRageSettings = srSettings;
SickRageApi = srApi;
UserQualityProfiles = userProfiles;
_requestQueueRepository = requestQueue;
_notificationHelper = notify;
}
private ISonarrApi SonarrApi { get; }
private ISonarrV3Api SonarrV3Api { get; }
private IDogNzbApi DogNzbApi { get; }
private ISickRageApi SickRageApi { get; }
private ILogger<TvSender> Logger { get; }
@ -43,59 +48,94 @@ namespace Ombi.Core.Senders
private ISettingsService<DogNzbSettings> DogNzbSettings { get; }
private ISettingsService<SickRageSettings> SickRageSettings { get; }
private IRepository<UserQualityProfiles> UserQualityProfiles { get; }
private readonly IRepository<RequestQueue> _requestQueueRepository;
private readonly INotificationHelper _notificationHelper;
public async Task<SenderResult> Send(ChildRequests model)
{
var sonarr = await SonarrSettings.GetSettingsAsync();
if (sonarr.Enabled)
try
{
var result = await SendToSonarr(model);
if (result != null)
var sonarr = await SonarrSettings.GetSettingsAsync();
if (sonarr.Enabled)
{
var result = await SendToSonarr(model, sonarr);
if (result != null)
{
return new SenderResult
{
Sent = true,
Success = true
};
}
}
var dog = await DogNzbSettings.GetSettingsAsync();
if (dog.Enabled)
{
var result = await SendToDogNzb(model, dog);
if (!result.Failure)
{
return new SenderResult
{
Sent = true,
Success = true
};
}
return new SenderResult
{
Sent = true,
Success = true
Message = result.ErrorMessage
};
}
}
var dog = await DogNzbSettings.GetSettingsAsync();
if (dog.Enabled)
{
var result = await SendToDogNzb(model, dog);
if (!result.Failure)
var sr = await SickRageSettings.GetSettingsAsync();
if (sr.Enabled)
{
var result = await SendToSickRage(model, sr);
if (result)
{
return new SenderResult
{
Sent = true,
Success = true
};
}
return new SenderResult
{
Sent = true,
Success = true
Message = "Could not send to SickRage!"
};
}
return new SenderResult
{
Message = result.ErrorMessage
Success = true
};
}
var sr = await SickRageSettings.GetSettingsAsync();
if (sr.Enabled)
catch (Exception e)
{
var result = await SendToSickRage(model, sr);
if (result)
Logger.LogError(e, "Exception thrown when sending a movie to DVR app, added to the request queue");
// Check if already in request queue
var existingQueue = await _requestQueueRepository.FirstOrDefaultAsync(x => x.RequestId == model.Id);
if (existingQueue != null)
{
return new SenderResult
{
Sent = true,
Success = true
};
existingQueue.RetryCount++;
existingQueue.Error = e.Message;
await _requestQueueRepository.SaveChangesAsync();
}
return new SenderResult
else
{
Message = "Could not send to SickRage!"
};
await _requestQueueRepository.Add(new RequestQueue
{
Dts = DateTime.UtcNow,
Error = e.Message,
RequestId = model.Id,
Type = RequestType.TvShow,
RetryCount = 0
});
_notificationHelper.Notify(model, NotificationType.ItemAddedToFaultQueue);
}
}
return new SenderResult
{
Success = true
Success = false,
Message = "Something went wrong!"
};
}
@ -111,13 +151,8 @@ namespace Ombi.Core.Senders
/// <param name="s"></param>
/// <param name="model"></param>
/// <returns></returns>
public async Task<NewSeries> SendToSonarr(ChildRequests model)
public async Task<NewSeries> SendToSonarr(ChildRequests model, SonarrSettings s)
{
var s = await SonarrSettings.GetSettingsAsync();
if (!s.Enabled)
{
return null;
}
if (string.IsNullOrEmpty(s.ApiKey))
{
return null;
@ -143,7 +178,7 @@ namespace Ombi.Core.Senders
}
if (profiles.SonarrQualityProfileAnime > 0)
{
qualityToUse = profiles.SonarrQualityProfileAnime;
qualityToUse = profiles.SonarrQualityProfileAnime;
}
}
seriesType = "anime";
@ -163,7 +198,7 @@ namespace Ombi.Core.Senders
}
if (profiles.SonarrQualityProfile > 0)
{
qualityToUse = profiles.SonarrQualityProfile;
qualityToUse = profiles.SonarrQualityProfile;
}
}
seriesType = "standard";
@ -174,7 +209,11 @@ namespace Ombi.Core.Senders
{
qualityToUse = model.ParentRequest.QualityOverride.Value;
}
// Are we using v3 sonarr?
var sonarrV3 = s.V3;
var languageProfileId = s.LanguageProfile;
try
{
// Does the series actually exist?
@ -204,6 +243,11 @@ namespace Ombi.Core.Senders
}
};
if (sonarrV3)
{
newSeries.languageProfileId = languageProfileId;
}
// Montitor the correct seasons,
// If we have that season in the model then it's monitored!
var seasonsToAdd = GetSeasonsToCreate(model);
@ -268,13 +312,22 @@ namespace Ombi.Core.Senders
}
}
var seriesChanges = false;
foreach (var season in model.SeasonRequests)
{
var sonarrSeason = sonarrEpList.Where(x => x.seasonNumber == season.SeasonNumber);
var sonarrEpCount = sonarrSeason.Count();
var sonarrEpisodeList = sonarrEpList.Where(x => x.seasonNumber == season.SeasonNumber).ToList();
var sonarrEpCount = sonarrEpisodeList.Count;
var ourRequestCount = season.Episodes.Count;
var ourEpisodes = season.Episodes.Select(x => x.EpisodeNumber).ToList();
var unairedEpisodes = sonarrEpisodeList.Where(x => x.airDateUtc > DateTime.UtcNow).Select(x => x.episodeNumber).ToList();
//// Check if we have requested all the latest episodes, if we have then monitor
//// NOTE, not sure if needed since ombi ui displays future episodes anyway...
//ourEpisodes.AddRange(unairedEpisodes);
//var distinctEpisodes = ourEpisodes.Distinct().ToList();
//var missingEpisodes = Enumerable.Range(distinctEpisodes.Min(), distinctEpisodes.Count).Except(distinctEpisodes);
var existingSeason =
result.seasons.FirstOrDefault(x => x.seasonNumber == season.SeasonNumber);
if (existingSeason == null)
@ -284,7 +337,7 @@ namespace Ombi.Core.Senders
}
if (sonarrEpCount == ourRequestCount)
if (sonarrEpCount == ourRequestCount /*|| !missingEpisodes.Any()*/)
{
// We have the same amount of requests as all of the episodes in the season.
@ -300,16 +353,16 @@ namespace Ombi.Core.Senders
if (!existingSeason.monitored)
{
// We need to monitor it, problem being is all episodes will now be monitored
// So we need to monior the series but unmonitor every episode
// Except the episodes that are already monitored before we update the series (we do not want to unmonitor episodes that are monitored beforehand)
// So we need to monitor the series but unmonitor every episode
// Except the episodes that are already monitored before we update the series (we do not want to unmonitored episodes that are monitored beforehand)
existingSeason.monitored = true;
var sea = result.seasons.FirstOrDefault(x => x.seasonNumber == existingSeason.seasonNumber);
sea.monitored = true;
//var previouslyMonitoredEpisodes = sonarrEpList.Where(x =>
// x.seasonNumber == existingSeason.seasonNumber && x.monitored).Select(x => x.episodeNumber).ToList(); // We probably don't actually care about this
result = await SonarrApi.UpdateSeries(result, s.ApiKey, s.FullUri);
var epToUnmonitor = new List<Episode>();
var newEpList = sonarrEpList.ConvertAll(ep => new Episode(ep)); // Clone it so we don't modify the orignal member
var epToUnmonitored = new List<Episode>();
var newEpList = sonarrEpList.ConvertAll(ep => new Episode(ep)); // Clone it so we don't modify the original member
foreach (var ep in newEpList.Where(x => x.seasonNumber == existingSeason.seasonNumber).ToList())
{
//if (previouslyMonitoredEpisodes.Contains(ep.episodeNumber))
@ -318,10 +371,10 @@ namespace Ombi.Core.Senders
// continue;
//}
ep.monitored = false;
epToUnmonitor.Add(ep);
epToUnmonitored.Add(ep);
}
foreach (var epToUpdate in epToUnmonitor)
foreach (var epToUpdate in epToUnmonitored)
{
await SonarrApi.UpdateEpisode(epToUpdate, s.ApiKey, s.FullUri);
}
@ -355,7 +408,7 @@ namespace Ombi.Core.Senders
var sea = new Season
{
seasonNumber = i,
monitored = model.SeasonRequests.Any(x => x.SeasonNumber == index && x.SeasonNumber != 0)
monitored = false
};
seasonsToUpdate.Add(sea);
}

View file

@ -32,6 +32,7 @@ using Ombi.Api.CouchPotato;
using Ombi.Api.DogNzb;
using Ombi.Api.FanartTv;
using Ombi.Api.Github;
using Ombi.Api.Gotify;
using Ombi.Api.Lidarr;
using Ombi.Api.Mattermost;
using Ombi.Api.Notifications;
@ -53,6 +54,7 @@ using Ombi.Updater;
using PlexContentCacher = Ombi.Schedule.Jobs.Plex;
using Ombi.Api.Telegram;
using Ombi.Core.Authentication;
using Ombi.Core.Engine.Demo;
using Ombi.Core.Processor;
using Ombi.Schedule.Jobs.Lidarr;
using Ombi.Schedule.Jobs.Plex.Interfaces;
@ -92,6 +94,9 @@ namespace Ombi.DependencyInjection
services.AddTransient<IMusicSender, MusicSender>();
services.AddTransient<IMassEmailSender, MassEmailSender>();
services.AddTransient<IPlexOAuthManager, PlexOAuthManager>();
services.AddTransient<IVoteEngine, VoteEngine>();
services.AddTransient<IDemoMovieSearchEngine, DemoMovieSearchEngine>();
services.AddTransient<IDemoTvSearchEngine, DemoTvSearchEngine>();
}
public static void RegisterHttp(this IServiceCollection services)
{
@ -107,6 +112,7 @@ namespace Ombi.DependencyInjection
services.AddTransient<IPlexApi, PlexApi>();
services.AddTransient<IEmbyApi, EmbyApi>();
services.AddTransient<ISonarrApi, SonarrApi>();
services.AddTransient<ISonarrV3Api, SonarrV3Api>();
services.AddTransient<ISlackApi, SlackApi>();
services.AddTransient<ITvMazeApi, TvMazeApi>();
services.AddTransient<ITraktApi, TraktApi>();
@ -116,6 +122,7 @@ namespace Ombi.DependencyInjection
services.AddTransient<IOmbiService, OmbiService>();
services.AddTransient<IFanartTvApi, FanartTvApi>();
services.AddTransient<IPushoverApi, PushoverApi>();
services.AddTransient<IGotifyApi, GotifyApi>();
services.AddTransient<IMattermostApi, MattermostApi>();
services.AddTransient<ICouchPotatoApi, CouchPotatoApi>();
services.AddTransient<IDogNzbApi, DogNzbApi>();
@ -128,23 +135,28 @@ namespace Ombi.DependencyInjection
}
public static void RegisterStore(this IServiceCollection services) {
services.AddEntityFrameworkSqlite().AddDbContext<OmbiContext>();
services.AddDbContext<OmbiContext>();
services.AddDbContext<SettingsContext>();
services.AddDbContext<ExternalContext>();
services.AddScoped<IOmbiContext, OmbiContext>(); // https://docs.microsoft.com/en-us/aspnet/core/data/entity-framework-6
services.AddTransient<ISettingsRepository, SettingsJsonRepository>();
services.AddTransient<ISettingsResolver, SettingsResolver>();
services.AddTransient<IPlexContentRepository, PlexServerContentRepository>();
services.AddTransient<IEmbyContentRepository, EmbyContentRepository>();
services.AddTransient<INotificationTemplatesRepository, NotificationTemplatesRepository>();
services.AddScoped<ISettingsContext, SettingsContext>(); // https://docs.microsoft.com/en-us/aspnet/core/data/entity-framework-6
services.AddScoped<IExternalContext, ExternalContext>(); // https://docs.microsoft.com/en-us/aspnet/core/data/entity-framework-6
services.AddScoped<ISettingsRepository, SettingsJsonRepository>();
services.AddScoped<ISettingsResolver, SettingsResolver>();
services.AddScoped<IPlexContentRepository, PlexServerContentRepository>();
services.AddScoped<IEmbyContentRepository, EmbyContentRepository>();
services.AddScoped<INotificationTemplatesRepository, NotificationTemplatesRepository>();
services.AddTransient<ITvRequestRepository, TvRequestRepository>();
services.AddTransient<IMovieRequestRepository, MovieRequestRepository>();
services.AddTransient<IMusicRequestRepository, MusicRequestRepository>();
services.AddTransient<IAuditRepository, AuditRepository>();
services.AddTransient<IApplicationConfigRepository, ApplicationConfigRepository>();
services.AddTransient<ITokenRepository, TokenRepository>();
services.AddTransient(typeof(ISettingsService<>), typeof(SettingsService<>));
services.AddTransient(typeof(IRepository<>), typeof(Repository<>));
services.AddScoped<ITvRequestRepository, TvRequestRepository>();
services.AddScoped<IMovieRequestRepository, MovieRequestRepository>();
services.AddScoped<IMusicRequestRepository, MusicRequestRepository>();
services.AddScoped<IAuditRepository, AuditRepository>();
services.AddScoped<IApplicationConfigRepository, ApplicationConfigRepository>();
services.AddScoped<ITokenRepository, TokenRepository>();
services.AddScoped(typeof(ISettingsService<>), typeof(SettingsService<>));
services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
services.AddScoped(typeof(IExternalRepository<>), typeof(ExternalRepository<>));
}
public static void RegisterServices(this IServiceCollection services)
{
@ -152,7 +164,7 @@ namespace Ombi.DependencyInjection
services.AddTransient<INotificationService, NotificationService>();
services.AddTransient<IEmailProvider, GenericEmailProvider>();
services.AddTransient<INotificationHelper, NotificationHelper>();
services.AddTransient<ICacheService, CacheService>();
services.AddSingleton<ICacheService, CacheService>();
services.AddTransient<IDiscordNotification, DiscordNotification>();
services.AddTransient<IEmailNotification, EmailNotification>();
@ -161,6 +173,7 @@ namespace Ombi.DependencyInjection
services.AddTransient<ISlackNotification, SlackNotification>();
services.AddTransient<IMattermostNotification, MattermostNotification>();
services.AddTransient<IPushoverNotification, PushoverNotification>();
services.AddTransient<IGotifyNotification, GotifyNotification>();
services.AddTransient<ITelegramNotification, TelegramNotification>();
services.AddTransient<IMobileNotification, MobileNotification>();
services.AddTransient<IChangeLogProcessor, ChangeLogProcessor>();
@ -194,6 +207,8 @@ namespace Ombi.DependencyInjection
services.AddTransient<ILidarrArtistSync, LidarrArtistSync>();
services.AddTransient<ILidarrAvailabilityChecker, LidarrAvailabilityChecker>();
services.AddTransient<IIssuesPurge, IssuesPurge>();
services.AddTransient<IResendFailedRequests, ResendFailedRequests>();
services.AddTransient<IMediaDatabaseRefresh, MediaDatabaseRefresh>();
}
}
}

View file

@ -9,9 +9,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="2.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="2.1.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="2.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="2.2.0" />
</ItemGroup>
<ItemGroup>

View file

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="nunit" Version="3.11.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.11.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Ombi.Helpers\Ombi.Helpers.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,52 @@
using System;
using NUnit.Framework;
using System.Collections.Generic;
namespace Ombi.Helpers.Tests
{
[TestFixture]
public class PlexHelperTests
{
[TestCaseSource(nameof(ProviderIdGuidData))]
public string GetProviderIdFromPlexGuidTests(string guidInput, ProviderIdType type)
{
var result = PlexHelper.GetProviderIdFromPlexGuid(guidInput);
switch (type)
{
case ProviderIdType.Imdb:
Assert.That(result.ImdbId, Is.Not.Null);
return result.ImdbId;
case ProviderIdType.TvDb:
Assert.That(result.TheTvDb, Is.Not.Null);
return result.TheTvDb;
case ProviderIdType.MovieDb:
Assert.That(result.TheMovieDb, Is.Not.Null);
return result.TheMovieDb;
default:
throw new ArgumentOutOfRangeException(nameof(type), type, null);
}
}
public static IEnumerable<TestCaseData> ProviderIdGuidData
{
get
{
yield return new TestCaseData("com.plexapp.agents.thetvdb://269586/2/8?lang=en", ProviderIdType.TvDb).Returns("269586").SetName("Regular TvDb Id");
yield return new TestCaseData("com.plexapp.agents.themoviedb://390043?lang=en", ProviderIdType.MovieDb).Returns("390043").SetName("Regular MovieDb Id");
yield return new TestCaseData("com.plexapp.agents.imdb://tt2543164?lang=en", ProviderIdType.Imdb).Returns("tt2543164").SetName("Regular Imdb Id");
yield return new TestCaseData("com.plexapp.agents.agent47://tt2543456?lang=en", ProviderIdType.Imdb).Returns("tt2543456").SetName("Unknown IMDB agent");
yield return new TestCaseData("com.plexapp.agents.agent47://456822/1/1?lang=en", ProviderIdType.TvDb).Returns("456822").SetName("Unknown TvDb agent");
yield return new TestCaseData("com.plexapp.agents.agent47://456822/999/999?lang=en", ProviderIdType.TvDb).Returns("456822").SetName("Unknown TvDb agent, large episode and season");
}
}
public enum ProviderIdType
{
Imdb,
TvDb,
MovieDb
}
}
}

View file

@ -28,18 +28,15 @@ namespace Ombi.Helpers
return result;
}
using (await _mutex.LockAsync())
if (_memoryCache.TryGetValue(cacheKey, out result))
{
if (_memoryCache.TryGetValue(cacheKey, out result))
{
return result;
}
result = await factory();
_memoryCache.Set(cacheKey, result, absoluteExpiration);
return result;
}
result = await factory();
_memoryCache.Set(cacheKey, result, absoluteExpiration);
return result;
}
public void Remove(string key)
@ -47,34 +44,34 @@ 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, DateTime absoluteExpiration)
{
// locks get and set internally
if (_memoryCache.TryGetValue<T>(cacheKey, out var result))
{
// 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;
}
lock (TypeLock<T>.Lock)
{
if (_memoryCache.TryGetValue(cacheKey, out result))
{
return result;
}
result = factory();
_memoryCache.Set(cacheKey, result, absoluteExpiration);
result = factory();
_memoryCache.Set(cacheKey, result, absoluteExpiration);
return result;
}
return result;
}
}
private static class TypeLock<T>
{
public static object Lock { get; } = new object();
}
private static class TypeLock<T>
{
public static object Lock { get; } = new object();
}
}
}

View file

@ -0,0 +1,11 @@
namespace Ombi.Config
{
public class DemoLists
{
public bool Enabled { get; set; }
public int[] Movies { get; set; }
public int[] TvShows { get; set; }
}
}

View file

@ -0,0 +1,13 @@
namespace Ombi.Helpers
{
public class DemoSingleton
{
private static DemoSingleton instance;
private DemoSingleton() { }
public static DemoSingleton Instance => instance ?? (instance = new DemoSingleton());
public bool Demo { get; set; }
}
}

View file

@ -7,11 +7,16 @@ namespace Ombi.Helpers
{
public class EmbyHelper
{
public static string GetEmbyMediaUrl(string mediaId)
public static string GetEmbyMediaUrl(string mediaId, string customerServerUrl = null)
{
var url =
$"http://app.emby.media/#!/itemdetails.html?id={mediaId}";
return url;
if (customerServerUrl.HasValue())
{
return $"{customerServerUrl}#!/itemdetails.html?id={mediaId}";
}
else
{
return $"https://app.emby.media/#!/itemdetails.html?id={mediaId}";
}
}
}
}

View file

@ -21,6 +21,7 @@ namespace Ombi.Helpers
public static EventId PlexContentCacher => new EventId(2008);
public static EventId SickRageCacher => new EventId(2009);
public static EventId LidarrArtistCache => new EventId(2010);
public static EventId MediaReferesh => new EventId(2011);
public static EventId MovieSender => new EventId(3000);
@ -31,6 +32,7 @@ namespace Ombi.Helpers
public static EventId MattermostNotification => new EventId(4004);
public static EventId PushoverNotification => new EventId(4005);
public static EventId TelegramNotifcation => new EventId(4006);
public static EventId GotifyNotification => new EventId(4007);
public static EventId TvSender => new EventId(5000);
public static EventId SonarrSender => new EventId(5001);

View file

@ -10,5 +10,6 @@
Slack = 5,
Mattermost = 6,
Mobile = 7,
Gotify = 8,
}
}

View file

@ -10,9 +10,9 @@
<ItemGroup>
<PackageReference Include="EasyCrypto" Version="3.3.2" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="2.1.2" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.1.1" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.2.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
<PackageReference Include="Nito.AsyncEx" Version="5.0.0-pre-05" />
<PackageReference Include="System.Security.Claims" Version="4.3.0" />
</ItemGroup>

View file

@ -14,5 +14,7 @@
public const string RequestMusic = nameof(RequestMusic);
public const string Disabled = nameof(Disabled);
public const string ReceivesNewsletter = nameof(ReceivesNewsletter);
public const string ManageOwnRequests = nameof(ManageOwnRequests);
public const string EditCustomPage = nameof(EditCustomPage);
}
}

View file

@ -27,12 +27,15 @@
using System;
using System.Globalization;
using System.Text.RegularExpressions;
namespace Ombi.Helpers
{
public class PlexHelper
{
private const string ImdbMatchExpression = "tt([0-9]{1,10})";
private const string TvDbIdMatchExpression = "//[0-9]+/([0-9]{1,3})/([0-9]{1,3})";
public static ProviderId GetProviderIdFromPlexGuid(string guid)
{
//com.plexapp.agents.thetvdb://269586/2/8?lang=en
@ -52,7 +55,7 @@ namespace Ombi.Helpers
{
TheTvDb = guidSplit[1]
};
}
} else
if (guid.Contains("themoviedb", CompareOptions.IgnoreCase))
{
return new ProviderId
@ -60,6 +63,7 @@ namespace Ombi.Helpers
TheMovieDb = guidSplit[1]
};
}
else
if (guid.Contains("imdb", CompareOptions.IgnoreCase))
{
return new ProviderId
@ -67,6 +71,31 @@ namespace Ombi.Helpers
ImdbId = guidSplit[1]
};
}
else
{
var imdbRegex = new Regex(ImdbMatchExpression, RegexOptions.Compiled);
var tvdbRegex = new Regex(TvDbIdMatchExpression, RegexOptions.Compiled);
var imdbMatch = imdbRegex.IsMatch(guid);
if (imdbMatch)
{
return new ProviderId
{
ImdbId = guidSplit[1]
};
}
else
{
// Check if it matches the TvDb pattern
var tvdbMatch = tvdbRegex.IsMatch(guid);
if (tvdbMatch)
{
return new ProviderId
{
TheTvDb = guidSplit[1]
};
}
}
}
}
return new ProviderId();
}

View file

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Security;
@ -76,6 +77,49 @@ namespace Ombi.Helpers
return -1;
}
public static string BuildEpisodeList(IEnumerable<int> orderedEpisodes)
{
var epSb = new StringBuilder();
var previousEpisodes = new List<int>();
var previousEpisode = -1;
foreach (var ep in orderedEpisodes)
{
if (ep - 1 == previousEpisode)
{
// This is the next one
previousEpisodes.Add(ep);
}
else
{
if (previousEpisodes.Count > 1)
{
// End it
epSb.Append($"{previousEpisodes.First()}-{previousEpisodes.Last()}, ");
}
else if (previousEpisodes.Count == 1)
{
epSb.Append($"{previousEpisodes.FirstOrDefault()}, ");
}
// New one
previousEpisodes.Clear();
previousEpisodes.Add(ep);
}
previousEpisode = ep;
}
if (previousEpisodes.Count > 1)
{
// Got some left over
epSb.Append($"{previousEpisodes.First()}-{previousEpisodes.Last()}");
}
else if (previousEpisodes.Count == 1)
{
epSb.Append(previousEpisodes.FirstOrDefault());
}
return epSb.ToString();
}
public static string RemoveSpaces(this string str)
{
return str.Replace(" ", "");

View file

@ -19,6 +19,7 @@ namespace Ombi.Mapping.Profiles
CreateMap<UpdateSettingsViewModel, UpdateSettings>().ReverseMap();
CreateMap<MobileNotificationsViewModel, MobileNotificationSettings>().ReverseMap();
CreateMap<NewsletterNotificationViewModel, NewsletterSettings>().ReverseMap();
CreateMap<GotifyNotificationViewModel, GotifySettings>().ReverseMap();
}
}
}

View file

@ -13,7 +13,7 @@ namespace Ombi.Notifications.Templates
if (string.IsNullOrEmpty(_templateLocation))
{
#if DEBUG
_templateLocation = Path.Combine(Directory.GetCurrentDirectory(), "bin", "Debug", "netcoreapp2.0", "Templates",
_templateLocation = Path.Combine(Directory.GetCurrentDirectory(), "bin", "Debug", "netcoreapp2.2", "Templates",
"BasicTemplate.html");
#else
_templateLocation = Path.Combine(Directory.GetCurrentDirectory(), "Templates","BasicTemplate.html");

View file

@ -1,10 +1,17 @@
<!doctype html>
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Ombi</title>
<style type="text/css">
<style>
/* -------------------------------------
GLOBAL RESETS
------------------------------------- */
/*All the styling goes here*/
img {
border: none;
-ms-interpolation-mode: bicubic;
@ -12,6 +19,7 @@
}
body {
background-color: #1f1f1f;
font-family: 'Open Sans', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 14px;
@ -20,6 +28,8 @@
padding: 0;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
color: #FFF;
text-align: center;
}
table {
@ -31,39 +41,54 @@
table td {
font-family: 'Open Sans', Helvetica, Arial, sans-serif;
font-size: 14px;
vertical-align: top;
}
/* -------------------------------------
BODY & CONTAINER
------------------------------------- */
.body {
color: #FFF;
background-color: #1f1f1f;
width: 100%;
text-align: center;
}
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
.container {
display: block;
margin: 0 auto !important;
max-width: 1042px;
Margin: 0 auto !important;
/* makes it centered */
max-width: 1036px;
padding: 10px;
width: 1042px;
width: 1036px;
}
/* This should also be a block element, so that it will fill 100% of the .container */
.content {
box-sizing: border-box;
display: block;
margin: 0 auto;
Margin: 0 auto;
max-width: 1037px;
padding: 10px;
}
/* -------------------------------------
HEADER, FOOTER, MAIN
------------------------------------- */
.main {
color: #FFF;
background: #1f1f1f;
border-radius: 3px;
width: 100%;
text-align: center;
}
.wrapper {
box-sizing: border-box;
padding: 5px;
overflow: auto;
padding: 20px;
}
.content-block {
@ -71,58 +96,241 @@
padding-top: 10px;
}
.media-card {
font-family: 'Open Sans', Helvetica, Arial, sans-serif;
vertical-align: top;
padding: 3px;
width: 500px;
min-width: 500px;
max-width: 500px;
height: 252px;
max-height: 252px;
min-height: 252px;
}
.card-bg {
background-image: url({0});
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
width: 500px;
background-color: #1f1f1f;
background-position: center;
background-size: cover;
background-repeat: no-repeat;
background-clip: padding-box;
border: 2px solid rgba(255, 118, 27, .4);
height: 252px;
max-height: 252px;
}
.bg-tint {
background-color: rgba(0, 0, 0, .6);
}
.poster-container {
vertical-align: top;
width: 150px;
min-width: 150px;
height: 225px;
max-height: 225px;
min-height: 225px;
}
.poster-img {}
.poster-overlay {}
.movie-info {
font-family: 'Open Sans', Helvetica, Arial, sans-serif;
vertical-align: top;
padding-left: 4px;
text-align: left;
height: 227px;
}
.title h1 {
font-family: 'Open Sans', Helvetica, Arial, sans-serif;
font-size: 22px;
line-height: 24px;
vertical-align: top;
max-width: 320px;
white-space: normal;
display: block;
height: 50px;
min-height: 50px;
max-height: 50px;
}
.description {
font-family: 'Open Sans', Helvetica, Arial, sans-serif;
height: 130px;
max-height: 130px;
max-width: 320px;
overflow: hidden;
text-overflow: ellipsis;
display: block;
font-size: 14px !important;
text-align: justify;
}
.meta {
font-family: 'Open Sans', Helvetica, Arial, sans-serif;
max-width: 300px;
min-width: 300px;
padding: 3px 7px;
margin-top: 10px;
font-size: 14px !important;
line-height: 1;
text-align: left;
white-space: nowrap;
vertical-align: middle;
background-color: rgba(255, 118, 27, 0.5);
color: #fff;
border-radius: 2px;
overflow: hidden;
display: block;
}
.footer {
clear: both;
margin-top: 10px;
Margin-top: 10px;
text-align: center;
width: 100%;
font-size: 12px;
}
.footer td,
.footer p,
.footer span,
.footer a {
color: #fff;
color: #999999;
font-size: 12px;
text-align: center;
}
h1 {
color: #ffffff;
/* -------------------------------------
TYPOGRAPHY
------------------------------------- */
h1,
h2,
h3,
h4 {
color: #ff761b;
font-family: 'Open Sans', Helvetica, Arial, sans-serif;
font-weight: 400;
margin: 0;
color: #ff761b;
font-size: 22px;
line-height: 24px;
margin: 0 auto;
text-transform: capitalize;
}
p {
p,
ul,
ol {
font-family: 'Open Sans', Helvetica, Arial, sans-serif;
font-weight: 400;
font-weight: normal;
margin: 0;
}
p li,
ul li {
ul li,
ol li {
list-style-position: inside;
margin-left: 5px;
}
a {
color: #ff761b !important;
text-decoration: none;
font-weight: 400;
}
/* -------------------------------------
OTHER STYLES THAT MIGHT BE USEFUL
------------------------------------- */
.last {
margin-bottom: 0;
}
.first {
margin-top: 0;
}
.align-center {
text-align: center;
}
.align-right {
text-align: right;
}
.align-left {
text-align: left;
}
.clear {
clear: both;
}
.mt0 {
margin-top: 0;
}
.mb0 {
margin-bottom: 0;
}
.preheader {
color: transparent;
display: none;
height: 0;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
mso-hide: all;
visibility: hidden;
width: 0;
}
.powered-by a {
text-decoration: none;
}
@media only screen and (max-width: 1040px) {
.media-card {
display: block !important;
margin-top: 0 !important;
margin-right: auto !important;
margin-bottom: 10px !important;
margin-left: auto !important;
hr {
border: 0;
border-bottom: 1px solid #f6f6f6;
Margin: 20px 0;
}
/* -------------------------------------
RESPONSIVE AND MOBILE FRIENDLY STYLES
------------------------------------- */
@media only screen and (max-width: 1059px) {
table[class=body] h1 {
font-size: 22px;
}
table[class=body] h1 {
font-size: 28px !important;
table[class=body] p,
table[class=body] ul,
table[class=body] ol,
table[class=body] td,
table[class=body] span,
table[class=body] a {
font-size: 12px !important;
}
table[class=body] .wrapper,
table[class=body] .article {
padding: 10px !important;
}
table[class=body] .content {
padding: 0 !important;
}
table[class=body] .container {
@ -135,9 +343,52 @@
border-radius: 0 !important;
border-right-width: 0 !important;
}
table[class=body] .btn table {
width: 100% !important;
}
table[class=body] .btn a {
width: 100% !important;
}
table[class=body] .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !important;
}
.media-card {
display: flex !important;
margin-top: 0 !important;
margin-right: auto !important;
margin-bottom: 10px !important;
margin-left: auto !important;
height: 100% !important;
width: 100% !important;
width: 500px !important;
max-width: 500px !important;
}
}
/* -------------------------------------
PRESERVE THESE STYLES IN THE HEAD
------------------------------------- */
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%;
}
.apple-link a {
color: inherit !important;
font-family: inherit !important;
@ -146,66 +397,78 @@
line-height: inherit !important;
text-decoration: none !important;
}
.btn-primary table td:hover {
background-color: #34495e !important;
}
.btn-primary a:hover {
background-color: #34495e !important;
border-color: #34495e !important;
}
}
</style>
</head>
<body class="" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;-webkit-font-smoothing: antialiased;font-size: 14px;line-height: 1.4;margin: 0;padding: 0;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;">
<table border="0" cellpadding="0" cellspacing="0" class="body" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%; background-color: #1f1f1f; color: #fff;">
<tr>
<td class="container" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;display: block;max-width: 1042px;padding: 10px;width: 1042px;margin: 0 auto !important;">
<div class="content" style="box-sizing: border-box; display: block; margin: 0 auto; max-width: 1037px; padding: 10px;">
<!-- START CENTERED WHITE CONTAINER -->
<span class="preheader" style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">Ombi Recently Added</span>
<table class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; border-radius: 3px;">
<!-- START MAIN CONTENT AREA -->
<tr>
<td class="wrapper" style="font-family: sans-serif; font-size: 14px; vertical-align: top; box-sizing: border-box; padding: 20px;" valign="top">
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; max-width: 1042px; width: 100%;">
<tr>
<td align="center">
<img src="{@LOGO}" width="400px" text-align="center"/>
</td>
</tr>
<tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top">
<br />
<br />
<p style="color: #fff; font-family: sans-serif; font-size: 20px; font-weight: normal; margin: 0; Margin-bottom: 15px; text-align: center;">{@INTRO}</p>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td>
{@RECENTLYADDED}
</td>
</tr>
<!-- END MAIN CONTENT AREA -->
</table>
<!-- START FOOTER -->
<div class="footer" style="clear: both; padding-top: 10px; text-align: center; width: 100%;">
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
<tr>
<td class="content-block powered-by" style="font-family: sans-serif; vertical-align: top; padding-top: 10px; padding-bottom: 10px; font-size: 12px; color: #999999; text-align: center;" valign="top" align="center">
Powered by <a href="https://github.com/tidusjar/Ombi" style="color: #999999; font-size: 12px; text-align: center; text-decoration: underline;">Ombi</a>
</td>
</tr>
<body class="" style="background-color: #1f1f1f; font-family: 'Open Sans', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.4; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; color: #FFF; text-align: center;">
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body" width="100%" bgcolor="#1f1f1f" align="center" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; color: #FFF; background-color: #1f1f1f; width: 100%; text-align: center;">
<tbody>
<tr>
<td class="container" width="1036" valign="top" style="font-family: sans-serif; font-size: 14px; vertical-align: top; max-width: 1036px; padding: 10px; width: 1036px; Margin: 0 auto;">
<div class="content" style="box-sizing: border-box; Margin: 0 auto; max-width: 1037px; padding: 10px;">
<!-- START CENTERED WHITE CONTAINER -->
<span class="preheader" style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">Ombi recently added</span>
<table role="presentation" class="main" width="100%" align="center" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; color: #FFF; background: #1f1f1f; border-radius: 3px; width: 100%; text-align: center;">
<!-- START MAIN CONTENT AREA -->
<tbody>
<tr>
<td class="wrapper" valign="top" style="font-family: sans-serif; font-size: 14px; vertical-align: top; box-sizing: border-box; padding: 20px;">
<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 valign="top" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif; vertical-align: top;">
<img src="{@LOGO}" style="border: none; -ms-interpolation-mode: bicubic; max-width: 100%;">
</td>
</tr>
<tr>
<td valign="top" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif; vertical-align: top;">
<p style="font-family: 'Open Sans', Helvetica, Arial, sans-serif; font-weight: normal; margin: 0; margin-bottom: 15px;">{@INTRO}</p>
</td>
</tr>
<tr>
<td valign="top" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif; vertical-align: top;">
{@RECENTLYADDED}
</td>
</tr>
</table>
</td>
</tr>
<!-- END MAIN CONTENT AREA -->
</tbody>
</table>
<!-- START FOOTER -->
<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;">
Powered by <a href="https://github.com/tidusjar/Ombi" style="font-weight: 400; font-size: 12px; text-align: center; text-decoration: none; color: #ff761b;">Ombi</a>
</td>
</tr>
</tbody>
</table>
</div>
<!-- END FOOTER -->
<!-- END CENTERED WHITE CONTAINER -->
</div>
<!-- END FOOTER -->
<!-- END CENTERED WHITE CONTAINER -->
</div>
</td>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top">&nbsp;</td>
</tr>
</td>
</tr>
</tbody>
</table>
</body>
</html>
</html>
</html>

View file

@ -1,15 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<TargetFramework>netcoreapp2.2</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Nunit" Version="3.8.1" />
<PackageReference Include="NUnit.ConsoleRunner" Version="3.7.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.8.0" />
<packagereference Include="Microsoft.NET.Test.Sdk" Version="15.8.0"></packagereference>
<PackageReference Include="Moq" Version="4.7.99" />
<PackageReference Include="Nunit" Version="3.10.1" />
<PackageReference Include="NUnit.ConsoleRunner" Version="3.9.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.10.0" />
<packagereference Include="Microsoft.NET.Test.Sdk" Version="15.9.0"></packagereference>
<PackageReference Include="Moq" Version="4.10.0" />
</ItemGroup>
<ItemGroup>

View file

@ -6,7 +6,6 @@ using Ombi.Api.Discord;
using Ombi.Api.Discord.Models;
using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Notifications.Interfaces;
using Ombi.Notifications.Models;
using Ombi.Settings.Settings.Models;
using Ombi.Settings.Settings.Models.Notifications;
@ -57,148 +56,42 @@ namespace Ombi.Notifications.Agents
protected override async Task NewRequest(NotificationOptions model, DiscordNotificationSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Discord, NotificationType.NewRequest, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {NotificationType.NewRequest} is disabled for {NotificationAgent.Discord}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
notification.Other.Add("image", parsed.Image);
await Send(notification, settings);
await Run(model, settings, NotificationType.NewRequest);
}
protected override async Task NewIssue(NotificationOptions model, DiscordNotificationSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Discord, NotificationType.Issue, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {NotificationType.Issue} is disabled for {NotificationAgent.Discord}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
notification.Other.Add("image", parsed.Image);
await Send(notification, settings);
await Run(model, settings, NotificationType.Issue);
}
protected override async Task IssueComment(NotificationOptions model, DiscordNotificationSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Discord, NotificationType.IssueComment, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {NotificationType.IssueComment} is disabled for {NotificationAgent.Discord}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
notification.Other.Add("image", parsed.Image);
await Send(notification, settings);
await Run(model, settings, NotificationType.IssueComment);
}
protected override async Task IssueResolved(NotificationOptions model, DiscordNotificationSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Discord, NotificationType.IssueResolved, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {NotificationType.IssueResolved} is disabled for {NotificationAgent.Discord}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
notification.Other.Add("image", parsed.Image);
await Send(notification, settings);
await Run(model, settings, NotificationType.IssueResolved);
}
protected override async Task AddedToRequestQueue(NotificationOptions model, DiscordNotificationSettings settings)
{
var user = string.Empty;
var title = string.Empty;
var image = string.Empty;
if (model.RequestType == RequestType.Movie)
{
user = MovieRequest.RequestedUser.UserAlias;
title = MovieRequest.Title;
image = MovieRequest.PosterPath;
}
else if (model.RequestType == RequestType.TvShow)
{
user = TvRequest.RequestedUser.UserAlias;
title = TvRequest.ParentRequest.Title;
image = TvRequest.ParentRequest.PosterPath;
}
else if (model.RequestType == RequestType.Album)
{
user = AlbumRequest.RequestedUser.UserAlias;
title = AlbumRequest.Title;
image = AlbumRequest.Cover;
}
var message = $"Hello! The user '{user}' has requested {title} but it could not be added. This has been added into the requests queue and will keep retrying";
var notification = new NotificationMessage
{
Message = message
};
notification.Other.Add("image", image);
await Send(notification, settings);
await Run(model, settings, NotificationType.ItemAddedToFaultQueue);
}
protected override async Task RequestDeclined(NotificationOptions model, DiscordNotificationSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Discord, NotificationType.RequestDeclined, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {NotificationType.RequestDeclined} is disabled for {NotificationAgent.Discord}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
notification.Other.Add("image", parsed.Image);
await Send(notification, settings);
await Run(model, settings, NotificationType.RequestDeclined);
}
protected override async Task RequestApproved(NotificationOptions model, DiscordNotificationSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Discord, NotificationType.RequestApproved, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {NotificationType.RequestApproved} is disabled for {NotificationAgent.Discord}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
notification.Other.Add("image", parsed.Image);
await Send(notification, settings);
await Run(model, settings, NotificationType.RequestApproved);
}
protected override async Task AvailableRequest(NotificationOptions model, DiscordNotificationSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Discord, NotificationType.RequestAvailable, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {NotificationType.RequestAvailable} is disabled for {NotificationAgent.Discord}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
notification.Other.Add("image", parsed.Image);
await Send(notification, settings);
await Run(model, settings, NotificationType.RequestAvailable);
}
protected override async Task Send(NotificationMessage model, DiscordNotificationSettings settings)
@ -243,5 +136,21 @@ namespace Ombi.Notifications.Agents
};
await Send(notification, settings);
}
private async Task Run(NotificationOptions model, DiscordNotificationSettings settings, NotificationType type)
{
var parsed = await LoadTemplate(NotificationAgent.Discord, type, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {type} is disabled for {NotificationAgent.Discord}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
notification.Other.Add("image", parsed.Image);
await Send(notification, settings);
}
}
}

View file

@ -8,7 +8,6 @@ using Microsoft.Extensions.Logging;
using MimeKit;
using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Notifications.Interfaces;
using Ombi.Notifications.Models;
using Ombi.Notifications.Templates;
using Ombi.Settings.Settings.Models;
@ -90,7 +89,6 @@ namespace Ombi.Notifications.Agents
}
else
{
// Send to admin
message.To = settings.AdminEmail;
}
@ -184,37 +182,21 @@ namespace Ombi.Notifications.Agents
protected override async Task AddedToRequestQueue(NotificationOptions model, EmailNotificationSettings settings)
{
var email = new EmailBasicTemplate();
var user = string.Empty;
var title = string.Empty;
var img = string.Empty;
if (model.RequestType == RequestType.Movie)
if (!model.Recipient.HasValue())
{
user = MovieRequest.RequestedUser.UserAlias;
title = MovieRequest.Title;
img = $"https://image.tmdb.org/t/p/w300/{MovieRequest.PosterPath}";
return;
}
else
var message = await LoadTemplate(NotificationType.ItemAddedToFaultQueue, model, settings);
if (message == null)
{
user = TvRequest.RequestedUser.UserAlias;
title = TvRequest.ParentRequest.Title;
img = TvRequest.ParentRequest.PosterPath;
return;
}
var html = email.LoadTemplate(
$"{Customization.ApplicationName}: A request could not be added.",
$"Hello! The user '{user}' has requested {title} but it could not be added. This has been added into the requests queue and will keep retrying", img, Customization.Logo);
var message = new NotificationMessage
{
Message = html,
Subject = $"{Customization.ApplicationName}: A request could not be added",
To = settings.AdminEmail,
};
var plaintext = $"Hello! The user '{user}' has requested {title} but it could not be added. This has been added into the requests queue and will keep retrying";
var plaintext = await LoadPlainTextMessage(NotificationType.ItemAddedToFaultQueue, model, settings);
message.Other.Add("PlainTextBody", plaintext);
// Issues resolved should be sent to the user
message.To = settings.AdminEmail;
await Send(message, settings);
}

View file

@ -0,0 +1,116 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Ombi.Api.Gotify;
using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Notifications.Models;
using Ombi.Settings.Settings.Models;
using Ombi.Settings.Settings.Models.Notifications;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
using Ombi.Store.Repository.Requests;
namespace Ombi.Notifications.Agents
{
public class GotifyNotification : BaseNotification<GotifySettings>, IGotifyNotification
{
public GotifyNotification(IGotifyApi api, ISettingsService<GotifySettings> sn, ILogger<GotifyNotification> log, INotificationTemplatesRepository r, IMovieRequestRepository m, ITvRequestRepository t,
ISettingsService<CustomizationSettings> s, IRepository<RequestSubscription> sub, IMusicRequestRepository music,
IRepository<UserNotificationPreferences> userPref) : base(sn, r, m, t, s, log, sub, music, userPref)
{
Api = api;
Logger = log;
}
public override string NotificationName => "GotifyNotification";
private IGotifyApi Api { get; }
private ILogger<GotifyNotification> Logger { get; }
protected override bool ValidateConfiguration(GotifySettings settings)
{
return settings.Enabled && !string.IsNullOrEmpty(settings.BaseUrl) && !string.IsNullOrEmpty(settings.ApplicationToken);
}
protected override async Task NewRequest(NotificationOptions model, GotifySettings settings)
{
await Run(model, settings, NotificationType.NewRequest);
}
protected override async Task NewIssue(NotificationOptions model, GotifySettings settings)
{
await Run(model, settings, NotificationType.Issue);
}
protected override async Task IssueComment(NotificationOptions model, GotifySettings settings)
{
await Run(model, settings, NotificationType.IssueComment);
}
protected override async Task IssueResolved(NotificationOptions model, GotifySettings settings)
{
await Run(model, settings, NotificationType.IssueResolved);
}
protected override async Task AddedToRequestQueue(NotificationOptions model, GotifySettings settings)
{
await Run(model, settings, NotificationType.ItemAddedToFaultQueue);
}
protected override async Task RequestDeclined(NotificationOptions model, GotifySettings settings)
{
await Run(model, settings, NotificationType.RequestDeclined);
}
protected override async Task RequestApproved(NotificationOptions model, GotifySettings settings)
{
await Run(model, settings, NotificationType.RequestApproved);
}
protected override async Task AvailableRequest(NotificationOptions model, GotifySettings settings)
{
await Run(model, settings, NotificationType.RequestAvailable);
}
protected override async Task Send(NotificationMessage model, GotifySettings settings)
{
try
{
await Api.PushAsync(settings.BaseUrl, settings.ApplicationToken, model.Subject, model.Message, settings.Priority);
}
catch (Exception e)
{
Logger.LogError(LoggingEvents.GotifyNotification, e, "Failed to send Gotify notification");
}
}
protected override async Task Test(NotificationOptions model, GotifySettings settings)
{
var message = $"This is a test from Ombi, if you can see this then we have successfully pushed a notification!";
var notification = new NotificationMessage
{
Message = message,
};
await Send(notification, settings);
}
private async Task Run(NotificationOptions model, GotifySettings settings, NotificationType type)
{
var parsed = await LoadTemplate(NotificationAgent.Gotify, type, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {type} is disabled for {NotificationAgent.Gotify}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
await Send(notification, settings);
}
}
}

View file

@ -0,0 +1,6 @@
namespace Ombi.Notifications.Agents
{
public interface IGotifyNotification : INotification
{
}
}

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