Merge pull request #4235 from Ombi-app/develop

Next prod release!
pull/4382/head v4.0.1430
Jamie 4 years ago committed by GitHub
commit 79a8b0e9af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

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

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

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

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

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

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

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

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

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

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

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

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

@ -59,9 +59,9 @@ namespace Ombi.Core.Engine.Interfaces
var ruleResults = await Rules.StartSearchRules(model);
return ruleResults;
}
public async Task<RuleResult> RunSpecificRule(object model, SpecificRules rule)
public async Task<RuleResult> RunSpecificRule(object model, SpecificRules rule, string requestOnBehalf)
{
var ruleResults = await Rules.StartSpecificRules(model, rule);
var ruleResults = await Rules.StartSpecificRules(model, rule, requestOnBehalf);
return ruleResults;
}
}

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

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

@ -21,6 +21,7 @@ using Ombi.Settings.Settings.Models;
using Ombi.Store.Entities.Requests;
using Ombi.Store.Repository;
using Ombi.Core.Models;
using System.Threading;
namespace Ombi.Core.Engine
{
@ -70,7 +71,7 @@ namespace Ombi.Core.Engine
var canRequestOnBehalf = model.RequestOnBehalf.HasValue();
var isAdmin = await UserManager.IsInRoleAsync(userDetails, OmbiRoles.PowerUser) || await UserManager.IsInRoleAsync(userDetails, OmbiRoles.Admin);
if (model.RequestOnBehalf.HasValue() && !isAdmin)
if (canRequestOnBehalf && !isAdmin)
{
return new RequestEngineResult
{
@ -549,12 +550,41 @@ namespace Ombi.Core.Engine
request.Denied = false;
await MovieRepository.Update(request);
var canNotify = await RunSpecificRule(request, SpecificRules.CanSendNotification);
var canNotify = await RunSpecificRule(request, SpecificRules.CanSendNotification, string.Empty);
if (canNotify.Success)
{
await NotificationHelper.Notify(request, NotificationType.RequestApproved);
}
return await ProcessSendingMovie(request);
}
public async Task<RequestEngineResult> RequestCollection(int collectionId, CancellationToken cancellationToken)
{
var langCode = await DefaultLanguageCode(null);
var collections = await Cache.GetOrAdd($"GetCollection{collectionId}{langCode}",
async () => await MovieApi.GetCollection(langCode, collectionId, cancellationToken), DateTime.Now.AddDays(1), cancellationToken);
var results = new List<RequestEngineResult>();
foreach (var collection in collections.parts)
{
results.Add(await RequestMovie(new MovieRequestViewModel
{
TheMovieDbId = collection.id
}));
}
if (results.All(x => x.IsError))
{
new RequestEngineResult { Result = false, ErrorMessage = $"The whole collection {collections.name} Is already monitored or requested!" };
}
return new RequestEngineResult { Result = true, Message = $"The collection {collections.name} has been successfully added!", RequestId = results.FirstOrDefault().RequestId};
}
private async Task<RequestEngineResult> ProcessSendingMovie(MovieRequests request)
{
if (request.Approved)
{
var result = await Sender.Send(request);
@ -634,6 +664,21 @@ namespace Ombi.Core.Engine
return await MovieRepository.GetAll().AnyAsync(x => x.RequestedUserId == userId);
}
public async Task<RequestEngineResult> ReProcessRequest(int requestId, CancellationToken cancellationToken)
{
var request = await MovieRepository.Find(requestId);
if (request == null)
{
return new RequestEngineResult
{
Result = false,
ErrorMessage = "Request does not exist"
};
}
return await ProcessSendingMovie(request);
}
public async Task<RequestEngineResult> MarkUnavailable(int modelId)
{
var request = await MovieRepository.Find(modelId);
@ -682,7 +727,7 @@ namespace Ombi.Core.Engine
{
await MovieRepository.Add(model);
var result = await RunSpecificRule(model, SpecificRules.CanSendNotification);
var result = await RunSpecificRule(model, SpecificRules.CanSendNotification, requestOnBehalf);
if (result.Success)
{
await NotificationHelper.NewRequest(model);

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

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

@ -25,19 +25,22 @@ using Ombi.Settings.Settings.Models;
using Ombi.Store.Entities.Requests;
using Ombi.Store.Repository;
using Ombi.Core.Models;
using System.Threading;
using Microsoft.Extensions.Logging;
namespace Ombi.Core.Engine
{
public class TvRequestEngine : BaseMediaEngine, ITvRequestEngine
{
public TvRequestEngine(ITvMazeApi tvApi, IMovieDbApi movApi, IRequestServiceMain requestService, IPrincipal user,
INotificationHelper helper, IRuleEvaluator rule, OmbiUserManager manager,
INotificationHelper helper, IRuleEvaluator rule, OmbiUserManager manager, ILogger<TvRequestEngine> logger,
ITvSender sender, IRepository<RequestLog> rl, ISettingsService<OmbiSettings> settings, ICacheService cache,
IRepository<RequestSubscription> sub) : base(user, requestService, rule, manager, cache, settings, sub)
{
TvApi = tvApi;
MovieDbApi = movApi;
NotificationHelper = helper;
_logger = logger;
TvSender = sender;
_requestLog = rl;
}
@ -46,6 +49,8 @@ namespace Ombi.Core.Engine
private ITvMazeApi TvApi { get; }
private IMovieDbApi MovieDbApi { get; }
private ITvSender TvSender { get; }
private readonly ILogger<TvRequestEngine> _logger;
private readonly IRepository<RequestLog> _requestLog;
public async Task<RequestEngineResult> RequestTvShow(TvRequestViewModel tv)
@ -68,7 +73,7 @@ namespace Ombi.Core.Engine
}
}
var tvBuilder = new TvShowRequestBuilder(TvApi, MovieDbApi);
var tvBuilder = new TvShowRequestBuilder(TvApi, MovieDbApi, _logger);
(await tvBuilder
.GetShowInfo(tv.TvDbId))
.CreateTvList(tv)
@ -896,9 +901,25 @@ namespace Ombi.Core.Engine
}
public async Task<RequestEngineResult> ReProcessRequest(int requestId, CancellationToken cancellationToken)
{
var request = await TvRepository.GetChild().FirstOrDefaultAsync(x => x.Id == requestId, cancellationToken);
if (request == null)
{
return new RequestEngineResult
{
Result = false,
ErrorMessage = "Request does not exist"
};
}
return await ProcessSendingShow(request);
}
private async Task<RequestEngineResult> AfterRequest(ChildRequests model, string requestOnBehalf)
{
var sendRuleResult = await RunSpecificRule(model, SpecificRules.CanSendNotification);
var sendRuleResult = await RunSpecificRule(model, SpecificRules.CanSendNotification, requestOnBehalf);
if (sendRuleResult.Success)
{
await NotificationHelper.NewRequest(model);
@ -913,6 +934,11 @@ namespace Ombi.Core.Engine
EpisodeCount = model.SeasonRequests.Select(m => m.Episodes.Count).Sum(),
});
return await ProcessSendingShow(model);
}
private async Task<RequestEngineResult> ProcessSendingShow(ChildRequests model)
{
if (model.Approved)
{
// Autosend

@ -315,6 +315,12 @@ namespace Ombi.Core.Engine.V2
foreach (var movie in movies)
{
var result = await ProcessSingleMovie(movie);
if (DemoCheck(result.Title))
{
continue;
}
if (settings.HideAvailableFromDiscover && result.Available)
{
continue;

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

@ -50,19 +50,36 @@ namespace Ombi.Core.Engine.V2
public async Task<SearchFullInfoTvShowViewModel> GetShowByRequest(int requestId, CancellationToken token)
{
var request = await RequestService.TvRequestService.Get().FirstOrDefaultAsync(x => x.Id == requestId);
return await GetShowInformation(request.ExternalProviderId.ToString(), token); // TODO
return await GetShowInformation(request.ExternalProviderId.ToString(), token);
}
public async Task<SearchFullInfoTvShowViewModel> GetShowInformation(string tvdbid, CancellationToken token)
{
var show = await Cache.GetOrAdd(nameof(GetShowInformation) + tvdbid,
async () => await _movieApi.GetTVInfo(tvdbid), DateTime.Now.AddHours(12));
var langCode = await DefaultLanguageCode(null);
var show = await Cache.GetOrAdd(nameof(GetShowInformation) + langCode + tvdbid,
async () => await _movieApi.GetTVInfo(tvdbid, langCode), DateTime.Now.AddHours(12));
if (show == null || show.name == null)
{
// We don't have enough information
return null;
}
if (!show.Images?.Posters?.Any() ?? false && !string.Equals(langCode, "en", StringComparison.OrdinalIgnoreCase))
{
// There's no regional assets for this, so
// lookup the en-us version to get them
var enShow = await Cache.GetOrAdd(nameof(GetShowInformation) + "en" + tvdbid,
async () => await _movieApi.GetTVInfo(tvdbid, "en"), DateTime.Now.AddHours(12));
// For some of the more obsecure cases
if (!show.overview.HasValue())
{
show.overview = enShow.overview;
}
show.Images = enShow.Images;
}
var mapped = _mapper.Map<SearchFullInfoTvShowViewModel>(show);
@ -154,6 +171,10 @@ namespace Ombi.Core.Engine.V2
foreach (var tvMazeSearch in items)
{
if (DemoCheck(tvMazeSearch.Title))
{
continue;
}
if (settings.HideAvailableFromDiscover)
{
// To hide, we need to know if it's fully available, the only way to do this is to lookup it's episodes to check if we have every episode

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

@ -33,6 +33,14 @@ namespace Ombi.Core.Helpers
public async Task<TvShowRequestBuilderV2> GetShowInfo(int id)
{
TheMovieDbRecord = await MovieDbApi.GetTVInfo(id.ToString());
// Remove 'Specials Season'
var firstSeason = TheMovieDbRecord.seasons.OrderBy(x => x.season_number).FirstOrDefault();
if (firstSeason?.season_number == 0)
{
TheMovieDbRecord.seasons.Remove(firstSeason);
}
BackdropPath = TheMovieDbRecord.Images?.Backdrops?.OrderBy(x => x.VoteCount).ThenBy(x => x.VoteAverage).FirstOrDefault()?.FilePath; ;
DateTime.TryParse(TheMovieDbRecord.first_air_date, out var dt);
@ -149,6 +157,10 @@ namespace Ombi.Core.Helpers
else if (tv.FirstSeason)
{
var first = allEpisodes.OrderBy(x => x.season_number).FirstOrDefault();
if (first.season_number == 0)
{
first = allEpisodes.OrderBy(x => x.season_number).Skip(1).FirstOrDefault();
}
var episodesRequests = new List<EpisodeRequests>();
foreach (var ep in allEpisodes)
{

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

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

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

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

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

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

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

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

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

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

@ -27,9 +27,12 @@ namespace Ombi.Core.Rule.Rules.Search
var useTheMovieDb = false;
var useId = false;
var useTvDb = false;
PlexMediaTypeEntity type = ConvertType(obj.Type);
if (obj.ImdbId.HasValue())
{
item = await PlexContentRepository.Get(obj.ImdbId);
item = await PlexContentRepository.GetByType(obj.ImdbId, ProviderType.ImdbId, type);
if (item != null)
{
useImdb = true;
@ -39,7 +42,7 @@ namespace Ombi.Core.Rule.Rules.Search
{
if (obj.Id > 0)
{
item = await PlexContentRepository.Get(obj.Id.ToString());
item = await PlexContentRepository.GetByType(obj.Id.ToString(), ProviderType.TheMovieDbId, type);
if (item != null)
{
useId = true;
@ -47,7 +50,7 @@ namespace Ombi.Core.Rule.Rules.Search
}
if (obj.TheMovieDbId.HasValue())
{
item = await PlexContentRepository.Get(obj.TheMovieDbId);
item = await PlexContentRepository.GetByType(obj.TheMovieDbId, ProviderType.TheMovieDbId, type);
if (item != null)
{
useTheMovieDb = true;
@ -58,7 +61,7 @@ namespace Ombi.Core.Rule.Rules.Search
{
if (obj.TheTvDbId.HasValue())
{
item = await PlexContentRepository.Get(obj.TheTvDbId);
item = await PlexContentRepository.GetByType(obj.TheTvDbId, ProviderType.TvDbId, type);
if (item != null)
{
useTvDb = true;
@ -85,9 +88,9 @@ namespace Ombi.Core.Rule.Rules.Search
if (search.SeasonRequests.Any())
{
var allEpisodes = PlexContentRepository.GetAllEpisodes();
foreach (var season in search.SeasonRequests)
foreach (var season in search.SeasonRequests.ToList())
{
foreach (var episode in season.Episodes)
foreach (var episode in season.Episodes.ToList())
{
await AvailabilityRuleHelper.SingleEpisodeCheck(useImdb, allEpisodes, episode, season, item, useTheMovieDb, useTvDb, Log);
}
@ -100,6 +103,12 @@ namespace Ombi.Core.Rule.Rules.Search
return Success();
}
private PlexMediaTypeEntity ConvertType(RequestType type) =>
type switch
{
RequestType.Movie => PlexMediaTypeEntity.Movie,
RequestType.TvShow => PlexMediaTypeEntity.Show,
_ => PlexMediaTypeEntity.Movie,
};
}
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -346,9 +346,9 @@ namespace Ombi.Notifications.Agents
private async Task AddSubscribedUsers(List<string> playerIds)
{
if (await SubsribedUsers.AnyAsync())
if (await Subscribed.AnyAsync())
{
foreach (var user in SubsribedUsers)
foreach (var user in Subscribed)
{
var notificationIds = await _notifications.GetAll().Where(x => x.UserId == user.Id).ToListAsync();

@ -48,7 +48,7 @@ namespace Ombi.Notifications
protected ChildRequests TvRequest { get; set; }
protected AlbumRequest AlbumRequest { get; set; }
protected MovieRequests MovieRequest { get; set; }
protected IQueryable<OmbiUser> SubsribedUsers { get; private set; }
protected IQueryable<OmbiUser> Subscribed { get; private set; }
public abstract string NotificationName { get; }
@ -75,7 +75,7 @@ namespace Ombi.Notifications
if (model.RequestId > 0)
{
await LoadRequest(model.RequestId, model.RequestType);
SubsribedUsers = GetSubscriptions(model.RequestId, model.RequestType);
Subscribed = GetSubscriptions(model.RequestId, model.RequestType);
}
Customization = await CustomizationSettings.GetSettingsAsync();

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

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

@ -131,7 +131,8 @@ namespace Ombi.Schedule.Jobs.Ombi
var lidarrContent = _lidarrAlbumRepository.GetAll().AsNoTracking().ToList().Where(x => x.FullyAvailable);
var addedLog = _recentlyAddedLog.GetAll();
var addedPlexMovieLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Plex && x.ContentType == ContentType.Parent).Select(x => x.ContentId).ToHashSet();
var addedPlexMovieLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Plex && x.ContentType == ContentType.Parent)?.Select(x => x.ContentId)?.ToHashSet() ?? new HashSet<int>();
var addedEmbyMoviesLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Emby && x.ContentType == ContentType.Parent).Select(x => x.ContentId).ToHashSet();
var addedJellyfinMoviesLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Jellyfin && x.ContentType == ContentType.Parent).Select(x => x.ContentId).ToHashSet();
var addedAlbumLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Lidarr && x.ContentType == ContentType.Album).Select(x => x.AlbumId).ToHashSet();
@ -170,6 +171,7 @@ namespace Ombi.Schedule.Jobs.Ombi
plexContentMoviesToSend = plexContentMoviesToSend.DistinctBy(x => x.Id).ToHashSet();
embyContentMoviesToSend = embyContentMoviesToSend.DistinctBy(x => x.Id).ToHashSet();
jellyfinContentMoviesToSend = jellyfinContentMoviesToSend.DistinctBy(x => x.Id).ToHashSet();
var plexEpisodesToSend =
FilterPlexEpisodes(_plex.GetAllEpisodes().Include(x => x.Series).AsNoTracking(), addedPlexEpisodesLogIds);
@ -226,32 +228,33 @@ namespace Ombi.Schedule.Jobs.Ombi
var messageContent = ParseTemplate(template, customization);
var email = new NewsletterTemplate();
var html = email.LoadTemplate(messageContent.Subject, messageContent.Message, body, customization.Logo);
var bodyBuilder = new BodyBuilder
foreach (var user in users)
{
HtmlBody = html,
};
var url = GenerateUnsubscribeLink(customization.ApplicationUrl, user.Id);
var html = email.LoadTemplate(messageContent.Subject, messageContent.Message, body, customization.Logo, url);
var message = new MimeMessage
{
Body = bodyBuilder.ToMessageBody(),
Subject = messageContent.Subject
};
var bodyBuilder = new BodyBuilder
{
HtmlBody = html,
};
var message = new MimeMessage
{
Body = bodyBuilder.ToMessageBody(),
Subject = messageContent.Subject
};
foreach (var user in users)
{
// Get the users to send it to
if (user.Email.IsNullOrEmpty())
{
continue;
}
// BCC the messages
message.Bcc.Add(new MailboxAddress(user.Email.Trim(), user.Email.Trim()));
}
// Send the message to the user
message.To.Add(new MailboxAddress(user.Email.Trim(), user.Email.Trim()));
// Send the email
await _email.Send(message, emailSettings);
// Send the email
await _email.Send(message, emailSettings);
}
// Now add all of this to the Recently Added log
var recentlyAddedLog = new HashSet<RecentlyAddedLog>();
@ -344,11 +347,14 @@ namespace Ombi.Schedule.Jobs.Ombi
{
continue;
}
var unsubscribeLink = GenerateUnsubscribeLink(customization.ApplicationUrl, a.Id);
var messageContent = ParseTemplate(template, customization);
var email = new NewsletterTemplate();
var html = email.LoadTemplate(messageContent.Subject, messageContent.Message, body, customization.Logo);
var html = email.LoadTemplate(messageContent.Subject, messageContent.Message, body, customization.Logo, unsubscribeLink);
await _email.Send(
new NotificationMessage { Message = html, Subject = messageContent.Subject, To = a.Email },
@ -369,6 +375,21 @@ namespace Ombi.Schedule.Jobs.Ombi
.SendAsync(NotificationHub.NotificationEvent, "Newsletter Finished");
}
public static string GenerateUnsubscribeLink(string applicationUrl, string id)
{
if (!applicationUrl.HasValue())
{
return string.Empty;
}
if (!applicationUrl.EndsWith('/'))
{
applicationUrl += '/';
}
var b = new UriBuilder($"{applicationUrl}unsubscribe/{id}");
return b.ToString();
}
private async Task<HashSet<PlexServerContent>> GetMoviesWithoutId(HashSet<int> addedMovieLogIds, HashSet<PlexServerContent> needsMovieDbPlex)
{
foreach (var movie in needsMovieDbPlex)
@ -537,7 +558,7 @@ namespace Ombi.Schedule.Jobs.Ombi
var plexMovies = plexContentToSend.Where(x => x.Type == PlexMediaTypeEntity.Movie);
var embyMovies = embyContentToSend.Where(x => x.Type == EmbyMediaType.Movie);
var jellyfinMovies = jellyfinContentToSend.Where(x => x.Type == JellyfinMediaType.Movie);
if ((plexMovies.Any() || embyMovies.Any()) && !settings.DisableMovies)
if ((plexMovies.Any() || embyMovies.Any()) || jellyfinMovies.Any() && !settings.DisableMovies)
{
sb.Append("<h1 style=\"text-align: center; max-width: 1042px;\">New Movies</h1><br /><br />");
sb.Append(

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

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

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

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

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

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

@ -6,5 +6,6 @@ namespace Ombi.Store.Entities
public class SonarrCache : Entity
{
public int TvDbId { get; set; }
public int TheMovieDbId { get; set; }
}
}

@ -8,6 +8,7 @@ namespace Ombi.Store.Entities
public int SeasonNumber { get; set; }
public int EpisodeNumber { get; set; }
public int TvDbId { get; set; }
public int MovieDbId { get; set; }
public bool HasFile { get; set; }
}
}

@ -0,0 +1,513 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Ombi.Store.Context.MySql;
namespace Ombi.Store.Migrations.ExternalMySql
{
[DbContext(typeof(ExternalMySqlContext))]
[Migration("20210615152049_SonarrSyncMovieDbData")]
partial class SonarrSyncMovieDbData
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("Relational:MaxIdentifierLength", 64)
.HasAnnotation("ProductVersion", "5.0.1");
modelBuilder.Entity("Ombi.Store.Entities.CouchPotatoCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int>("TheMovieDbId")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("CouchPotatoCache");
});
modelBuilder.Entity("Ombi.Store.Entities.EmbyContent", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<DateTime>("AddedAt")
.HasColumnType("datetime(6)");
b.Property<string>("EmbyId")
.IsRequired()
.HasColumnType("varchar(255)");
b.Property<string>("ImdbId")
.HasColumnType("longtext");
b.Property<string>("ProviderId")
.HasColumnType("longtext");
b.Property<string>("TheMovieDbId")
.HasColumnType("longtext");
b.Property<string>("Title")
.HasColumnType("longtext");
b.Property<string>("TvDbId")
.HasColumnType("longtext");
b.Property<int>("Type")
.HasColumnType("int");
b.Property<string>("Url")
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("EmbyContent");
});
modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<DateTime>("AddedAt")
.HasColumnType("datetime(6)");
b.Property<string>("EmbyId")
.HasColumnType("longtext");
b.Property<int>("EpisodeNumber")
.HasColumnType("int");
b.Property<string>("ImdbId")
.HasColumnType("longtext");
b.Property<string>("ParentId")
.HasColumnType("varchar(255)");
b.Property<string>("ProviderId")
.HasColumnType("longtext");
b.Property<int>("SeasonNumber")
.HasColumnType("int");
b.Property<string>("TheMovieDbId")
.HasColumnType("longtext");
b.Property<string>("Title")
.HasColumnType("longtext");
b.Property<string>("TvDbId")
.HasColumnType("longtext");
b.HasKey("Id");
b.HasIndex("ParentId");
b.ToTable("EmbyEpisode");
});
modelBuilder.Entity("Ombi.Store.Entities.JellyfinContent", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<DateTime>("AddedAt")
.HasColumnType("datetime(6)");
b.Property<string>("ImdbId")
.HasColumnType("longtext");
b.Property<string>("JellyfinId")
.IsRequired()
.HasColumnType("varchar(255)");
b.Property<string>("ProviderId")
.HasColumnType("longtext");
b.Property<string>("TheMovieDbId")
.HasColumnType("longtext");
b.Property<string>("Title")
.HasColumnType("longtext");
b.Property<string>("TvDbId")
.HasColumnType("longtext");
b.Property<int>("Type")
.HasColumnType("int");
b.Property<string>("Url")
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("JellyfinContent");
});
modelBuilder.Entity("Ombi.Store.Entities.JellyfinEpisode", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<DateTime>("AddedAt")
.HasColumnType("datetime(6)");
b.Property<int>("EpisodeNumber")
.HasColumnType("int");
b.Property<string>("ImdbId")
.HasColumnType("longtext");
b.Property<string>("JellyfinId")
.HasColumnType("longtext");
b.Property<string>("ParentId")
.HasColumnType("varchar(255)");
b.Property<string>("ProviderId")
.HasColumnType("longtext");
b.Property<int>("SeasonNumber")
.HasColumnType("int");
b.Property<string>("TheMovieDbId")
.HasColumnType("longtext");
b.Property<string>("Title")
.HasColumnType("longtext");
b.Property<string>("TvDbId")
.HasColumnType("longtext");
b.HasKey("Id");
b.HasIndex("ParentId");
b.ToTable("JellyfinEpisode");
});
modelBuilder.Entity("Ombi.Store.Entities.LidarrAlbumCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<DateTime>("AddedAt")
.HasColumnType("datetime(6)");
b.Property<int>("ArtistId")
.HasColumnType("int");
b.Property<string>("ForeignAlbumId")
.HasColumnType("longtext");
b.Property<bool>("Monitored")
.HasColumnType("tinyint(1)");
b.Property<decimal>("PercentOfTracks")
.HasColumnType("decimal(65,30)");
b.Property<DateTime>("ReleaseDate")
.HasColumnType("datetime(6)");
b.Property<string>("Title")
.HasColumnType("longtext");
b.Property<int>("TrackCount")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("LidarrAlbumCache");
});
modelBuilder.Entity("Ombi.Store.Entities.LidarrArtistCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int>("ArtistId")
.HasColumnType("int");
b.Property<string>("ArtistName")
.HasColumnType("longtext");
b.Property<string>("ForeignArtistId")
.HasColumnType("longtext");
b.Property<bool>("Monitored")
.HasColumnType("tinyint(1)");
b.HasKey("Id");
b.ToTable("LidarrArtistCache");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int>("EpisodeNumber")
.HasColumnType("int");
b.Property<int>("GrandparentKey")
.HasColumnType("int");
b.Property<int>("Key")
.HasColumnType("int");
b.Property<int>("ParentKey")
.HasColumnType("int");
b.Property<int>("SeasonNumber")
.HasColumnType("int");
b.Property<string>("Title")
.HasColumnType("longtext");
b.HasKey("Id");
b.HasIndex("GrandparentKey");
b.ToTable("PlexEpisode");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int>("ParentKey")
.HasColumnType("int");
b.Property<int>("PlexContentId")
.HasColumnType("int");
b.Property<int?>("PlexServerContentId")
.HasColumnType("int");
b.Property<int>("SeasonKey")
.HasColumnType("int");
b.Property<int>("SeasonNumber")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("PlexServerContentId");
b.ToTable("PlexSeasonsContent");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexServerContent", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<DateTime>("AddedAt")
.HasColumnType("datetime(6)");
b.Property<string>("ImdbId")
.HasColumnType("longtext");
b.Property<int>("Key")
.HasColumnType("int");
b.Property<string>("Quality")
.HasColumnType("longtext");
b.Property<string>("ReleaseYear")
.HasColumnType("longtext");
b.Property<int?>("RequestId")
.HasColumnType("int");
b.Property<string>("TheMovieDbId")
.HasColumnType("longtext");
b.Property<string>("Title")
.HasColumnType("longtext");
b.Property<string>("TvDbId")
.HasColumnType("longtext");
b.Property<int>("Type")
.HasColumnType("int");
b.Property<string>("Url")
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("PlexServerContent");
});
modelBuilder.Entity("Ombi.Store.Entities.RadarrCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<bool>("HasFile")
.HasColumnType("tinyint(1)");
b.Property<int>("TheMovieDbId")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("RadarrCache");
});
modelBuilder.Entity("Ombi.Store.Entities.SickRageCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int>("TvDbId")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("SickRageCache");
});
modelBuilder.Entity("Ombi.Store.Entities.SickRageEpisodeCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int>("EpisodeNumber")
.HasColumnType("int");
b.Property<int>("SeasonNumber")
.HasColumnType("int");
b.Property<int>("TvDbId")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("SickRageEpisodeCache");
});
modelBuilder.Entity("Ombi.Store.Entities.SonarrCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int>("TheMovieDbId")
.HasColumnType("int");
b.Property<int>("TvDbId")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("SonarrCache");
});
modelBuilder.Entity("Ombi.Store.Entities.SonarrEpisodeCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int>("EpisodeNumber")
.HasColumnType("int");
b.Property<bool>("HasFile")
.HasColumnType("tinyint(1)");
b.Property<int>("MovieDbId")
.HasColumnType("int");
b.Property<int>("SeasonNumber")
.HasColumnType("int");
b.Property<int>("TvDbId")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("SonarrEpisodeCache");
});
modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b =>
{
b.HasOne("Ombi.Store.Entities.EmbyContent", "Series")
.WithMany("Episodes")
.HasForeignKey("ParentId")
.HasPrincipalKey("EmbyId");
b.Navigation("Series");
});
modelBuilder.Entity("Ombi.Store.Entities.JellyfinEpisode", b =>
{
b.HasOne("Ombi.Store.Entities.JellyfinContent", "Series")
.WithMany("Episodes")
.HasForeignKey("ParentId")
.HasPrincipalKey("JellyfinId");
b.Navigation("Series");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b =>
{
b.HasOne("Ombi.Store.Entities.PlexServerContent", "Series")
.WithMany("Episodes")
.HasForeignKey("GrandparentKey")
.HasPrincipalKey("Key")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Series");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b =>
{
b.HasOne("Ombi.Store.Entities.PlexServerContent", null)
.WithMany("Seasons")
.HasForeignKey("PlexServerContentId");
});
modelBuilder.Entity("Ombi.Store.Entities.EmbyContent", b =>
{
b.Navigation("Episodes");
});
modelBuilder.Entity("Ombi.Store.Entities.JellyfinContent", b =>
{
b.Navigation("Episodes");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexServerContent", b =>
{
b.Navigation("Episodes");
b.Navigation("Seasons");
});
#pragma warning restore 612, 618
}
}
}

@ -0,0 +1,35 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace Ombi.Store.Migrations.ExternalMySql
{
public partial class SonarrSyncMovieDbData : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "MovieDbId",
table: "SonarrEpisodeCache",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "TheMovieDbId",
table: "SonarrCache",
type: "int",
nullable: false,
defaultValue: 0);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "MovieDbId",
table: "SonarrEpisodeCache");
migrationBuilder.DropColumn(
name: "TheMovieDbId",
table: "SonarrCache");
}
}
}

@ -413,6 +413,9 @@ namespace Ombi.Store.Migrations.ExternalMySql
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int>("TheMovieDbId")
.HasColumnType("int");
b.Property<int>("TvDbId")
.HasColumnType("int");
@ -433,6 +436,9 @@ namespace Ombi.Store.Migrations.ExternalMySql
b.Property<bool>("HasFile")
.HasColumnType("tinyint(1)");
b.Property<int>("MovieDbId")
.HasColumnType("int");
b.Property<int>("SeasonNumber")
.HasColumnType("int");

@ -0,0 +1,512 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Ombi.Store.Context.Sqlite;
namespace Ombi.Store.Migrations.ExternalSqlite
{
[DbContext(typeof(ExternalSqliteContext))]
[Migration("20210615145321_SonarrSyncMovieDbData")]
partial class SonarrSyncMovieDbData
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "5.0.1");
modelBuilder.Entity("Ombi.Store.Entities.CouchPotatoCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("TheMovieDbId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("CouchPotatoCache");
});
modelBuilder.Entity("Ombi.Store.Entities.EmbyContent", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("AddedAt")
.HasColumnType("TEXT");
b.Property<string>("EmbyId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("ImdbId")
.HasColumnType("TEXT");
b.Property<string>("ProviderId")
.HasColumnType("TEXT");
b.Property<string>("TheMovieDbId")
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.Property<string>("TvDbId")
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.Property<string>("Url")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("EmbyContent");
});
modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("AddedAt")
.HasColumnType("TEXT");
b.Property<string>("EmbyId")
.HasColumnType("TEXT");
b.Property<int>("EpisodeNumber")
.HasColumnType("INTEGER");
b.Property<string>("ImdbId")
.HasColumnType("TEXT");
b.Property<string>("ParentId")
.HasColumnType("TEXT");
b.Property<string>("ProviderId")
.HasColumnType("TEXT");
b.Property<int>("SeasonNumber")
.HasColumnType("INTEGER");
b.Property<string>("TheMovieDbId")
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.Property<string>("TvDbId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ParentId");
b.ToTable("EmbyEpisode");
});
modelBuilder.Entity("Ombi.Store.Entities.JellyfinContent", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("AddedAt")
.HasColumnType("TEXT");
b.Property<string>("ImdbId")
.HasColumnType("TEXT");
b.Property<string>("JellyfinId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("ProviderId")
.HasColumnType("TEXT");
b.Property<string>("TheMovieDbId")
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.Property<string>("TvDbId")
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.Property<string>("Url")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("JellyfinContent");
});
modelBuilder.Entity("Ombi.Store.Entities.JellyfinEpisode", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("AddedAt")
.HasColumnType("TEXT");
b.Property<int>("EpisodeNumber")
.HasColumnType("INTEGER");
b.Property<string>("ImdbId")
.HasColumnType("TEXT");
b.Property<string>("JellyfinId")
.HasColumnType("TEXT");
b.Property<string>("ParentId")
.HasColumnType("TEXT");
b.Property<string>("ProviderId")
.HasColumnType("TEXT");
b.Property<int>("SeasonNumber")
.HasColumnType("INTEGER");
b.Property<string>("TheMovieDbId")
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.Property<string>("TvDbId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ParentId");
b.ToTable("JellyfinEpisode");
});
modelBuilder.Entity("Ombi.Store.Entities.LidarrAlbumCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("AddedAt")
.HasColumnType("TEXT");
b.Property<int>("ArtistId")
.HasColumnType("INTEGER");
b.Property<string>("ForeignAlbumId")
.HasColumnType("TEXT");
b.Property<bool>("Monitored")
.HasColumnType("INTEGER");
b.Property<decimal>("PercentOfTracks")
.HasColumnType("TEXT");
b.Property<DateTime>("ReleaseDate")
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.Property<int>("TrackCount")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("LidarrAlbumCache");
});
modelBuilder.Entity("Ombi.Store.Entities.LidarrArtistCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("ArtistId")
.HasColumnType("INTEGER");
b.Property<string>("ArtistName")
.HasColumnType("TEXT");
b.Property<string>("ForeignArtistId")
.HasColumnType("TEXT");
b.Property<bool>("Monitored")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("LidarrArtistCache");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("EpisodeNumber")
.HasColumnType("INTEGER");
b.Property<int>("GrandparentKey")
.HasColumnType("INTEGER");
b.Property<int>("Key")
.HasColumnType("INTEGER");
b.Property<int>("ParentKey")
.HasColumnType("INTEGER");
b.Property<int>("SeasonNumber")
.HasColumnType("INTEGER");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("GrandparentKey");
b.ToTable("PlexEpisode");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("ParentKey")
.HasColumnType("INTEGER");
b.Property<int>("PlexContentId")
.HasColumnType("INTEGER");
b.Property<int?>("PlexServerContentId")
.HasColumnType("INTEGER");
b.Property<int>("SeasonKey")
.HasColumnType("INTEGER");
b.Property<int>("SeasonNumber")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("PlexServerContentId");
b.ToTable("PlexSeasonsContent");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexServerContent", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("AddedAt")
.HasColumnType("TEXT");
b.Property<string>("ImdbId")
.HasColumnType("TEXT");
b.Property<int>("Key")
.HasColumnType("INTEGER");
b.Property<string>("Quality")
.HasColumnType("TEXT");
b.Property<string>("ReleaseYear")
.HasColumnType("TEXT");
b.Property<int?>("RequestId")
.HasColumnType("INTEGER");
b.Property<string>("TheMovieDbId")
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.Property<string>("TvDbId")
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.Property<string>("Url")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("PlexServerContent");
});
modelBuilder.Entity("Ombi.Store.Entities.RadarrCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("HasFile")
.HasColumnType("INTEGER");
b.Property<int>("TheMovieDbId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("RadarrCache");
});
modelBuilder.Entity("Ombi.Store.Entities.SickRageCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("TvDbId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("SickRageCache");
});
modelBuilder.Entity("Ombi.Store.Entities.SickRageEpisodeCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("EpisodeNumber")
.HasColumnType("INTEGER");
b.Property<int>("SeasonNumber")
.HasColumnType("INTEGER");
b.Property<int>("TvDbId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("SickRageEpisodeCache");
});
modelBuilder.Entity("Ombi.Store.Entities.SonarrCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("TheMovieDbId")
.HasColumnType("INTEGER");
b.Property<int>("TvDbId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("SonarrCache");
});
modelBuilder.Entity("Ombi.Store.Entities.SonarrEpisodeCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("EpisodeNumber")
.HasColumnType("INTEGER");
b.Property<bool>("HasFile")
.HasColumnType("INTEGER");
b.Property<int>("MovieDbId")
.HasColumnType("INTEGER");
b.Property<int>("SeasonNumber")
.HasColumnType("INTEGER");
b.Property<int>("TvDbId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("SonarrEpisodeCache");
});
modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b =>
{
b.HasOne("Ombi.Store.Entities.EmbyContent", "Series")
.WithMany("Episodes")
.HasForeignKey("ParentId")
.HasPrincipalKey("EmbyId");
b.Navigation("Series");
});
modelBuilder.Entity("Ombi.Store.Entities.JellyfinEpisode", b =>
{
b.HasOne("Ombi.Store.Entities.JellyfinContent", "Series")
.WithMany("Episodes")
.HasForeignKey("ParentId")
.HasPrincipalKey("JellyfinId");
b.Navigation("Series");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b =>
{
b.HasOne("Ombi.Store.Entities.PlexServerContent", "Series")
.WithMany("Episodes")
.HasForeignKey("GrandparentKey")
.HasPrincipalKey("Key")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Series");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b =>
{
b.HasOne("Ombi.Store.Entities.PlexServerContent", null)
.WithMany("Seasons")
.HasForeignKey("PlexServerContentId");
});
modelBuilder.Entity("Ombi.Store.Entities.EmbyContent", b =>
{
b.Navigation("Episodes");
});
modelBuilder.Entity("Ombi.Store.Entities.JellyfinContent", b =>
{
b.Navigation("Episodes");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexServerContent", b =>
{
b.Navigation("Episodes");
b.Navigation("Seasons");
});
#pragma warning restore 612, 618
}
}
}

@ -0,0 +1,35 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace Ombi.Store.Migrations.ExternalSqlite
{
public partial class SonarrSyncMovieDbData : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "MovieDbId",
table: "SonarrEpisodeCache",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "TheMovieDbId",
table: "SonarrCache",
type: "INTEGER",
nullable: false,
defaultValue: 0);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "MovieDbId",
table: "SonarrEpisodeCache");
migrationBuilder.DropColumn(
name: "TheMovieDbId",
table: "SonarrCache");
}
}
}

@ -412,6 +412,9 @@ namespace Ombi.Store.Migrations.ExternalSqlite
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("TheMovieDbId")
.HasColumnType("INTEGER");
b.Property<int>("TvDbId")
.HasColumnType("INTEGER");
@ -432,6 +435,9 @@ namespace Ombi.Store.Migrations.ExternalSqlite
b.Property<bool>("HasFile")
.HasColumnType("INTEGER");
b.Property<int>("MovieDbId")
.HasColumnType("INTEGER");
b.Property<int>("SeasonNumber")
.HasColumnType("INTEGER");

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

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

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

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

@ -4,6 +4,10 @@ using System.Threading.Tasks;
using Ombi.Api.TheMovieDb.Models;
using Ombi.TheMovieDbApi.Models;
// Due to conflicting Genre models in
// Ombi.TheMovieDbApi.Models and Ombi.Api.TheMovieDb.Models
using Genre = Ombi.TheMovieDbApi.Models.Genre;
namespace Ombi.Api.TheMovieDb
{
public interface IMovieDbApi
@ -34,5 +38,6 @@ namespace Ombi.Api.TheMovieDb
Task<Keyword> GetKeyword(int keywordId);
Task<WatchProviders> GetMovieWatchProviders(int theMoviedbId, CancellationToken token);
Task<WatchProviders> GetTvWatchProviders(int theMoviedbId, CancellationToken token);
Task<List<Genre>> GetGenres(string media);
}
}
}

@ -36,4 +36,9 @@ namespace Ombi.TheMovieDbApi.Models
public int total_results { get; set; }
public int total_pages { get; set; }
}
}
public class GenreContainer<T>
{
public List<T> genres { get; set; }
}
}

@ -1,4 +1,5 @@
using Newtonsoft.Json;
using System.Collections.Generic;
namespace Ombi.Api.TheMovieDb.Models
{
@ -26,7 +27,7 @@ namespace Ombi.Api.TheMovieDb.Models
public float popularity { get; set; }
public string poster_path { get; set; }
public Production_Companies[] production_companies { get; set; }
public Season[] seasons { get; set; }
public List<Season> seasons { get; set; }
public string status { get; set; }
public string type { get; set; }
public float vote_average { get; set; }

@ -13,6 +13,10 @@ using Ombi.Core.Settings.Models.External;
using Ombi.Helpers;
using Ombi.TheMovieDbApi.Models;
// Due to conflicting Genre models in
// Ombi.TheMovieDbApi.Models and Ombi.Api.TheMovieDb.Models
using Genre = Ombi.TheMovieDbApi.Models.Genre;
namespace Ombi.Api.TheMovieDb
{
public class TheMovieDbApi : IMovieDbApi
@ -198,6 +202,7 @@ namespace Ombi.Api.TheMovieDb
request.AddQueryString("page", page.ToString());
}
await AddDiscoverSettings(request);
await AddGenreFilter(request, type);
AddRetry(request);
var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request, cancellationToken);
return Mapper.Map<List<MovieDbSearchResult>>(result.results);
@ -233,6 +238,7 @@ namespace Ombi.Api.TheMovieDb
request.AddQueryString("vote_count.gte", "250");
await AddDiscoverSettings(request);
await AddGenreFilter(request, type);
AddRetry(request);
var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request);
return Mapper.Map<List<MovieDbSearchResult>>(result.results);
@ -269,6 +275,7 @@ namespace Ombi.Api.TheMovieDb
request.AddQueryString("page", page.ToString());
}
await AddDiscoverSettings(request);
await AddGenreFilter(request, type);
AddRetry(request);
var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request);
return Mapper.Map<List<MovieDbSearchResult>>(result.results);
@ -297,6 +304,7 @@ namespace Ombi.Api.TheMovieDb
}
await AddDiscoverSettings(request);
await AddGenreFilter(request, "movie");
AddRetry(request);
var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request);
return Mapper.Map<List<MovieDbSearchResult>>(result.results);
@ -344,6 +352,16 @@ namespace Ombi.Api.TheMovieDb
return keyword == null || keyword.Id == 0 ? null : keyword;
}
public async Task<List<Genre>> GetGenres(string media)
{
var request = new Request($"genre/{media}/list", BaseUri, HttpMethod.Get);
request.AddQueryString("api_key", ApiToken);
AddRetry(request);
var result = await Api.Request<GenreContainer<Genre>>(request);
return result.genres ?? new List<Genre>();
}
public Task<TheMovieDbContainer<MultiSearch>> MultiSearch(string searchTerm, string languageCode, CancellationToken cancellationToken)
{
var request = new Request("search/multi", BaseUri, HttpMethod.Get);
@ -380,6 +398,28 @@ namespace Ombi.Api.TheMovieDb
}
}
private async Task AddGenreFilter(Request request, string media_type)
{
var settings = await Settings;
List<int> excludedGenres;
switch (media_type) {
case "tv":
excludedGenres = settings.ExcludedTvGenreIds;
break;
case "movie":
excludedGenres = settings.ExcludedMovieGenreIds;
break;
default:
return;
}
if (excludedGenres?.Any() == true)
{
request.AddQueryString("without_genres", string.Join(",", excludedGenres));
}
}
private static void AddRetry(Request request)
{
request.Retry = true;

@ -170,7 +170,17 @@
<div>
<app-my-nav id="main-container dark" [showNav]="showNav" [isAdmin]="isAdmin" [applicationName]="applicationName" [applicationLogo]="customizationSettings?.logo" [username]="username" [email]="user?.email " (logoutClick)="logOut();">
<app-my-nav id="main-container dark"
[showNav]="showNav"
[isAdmin]="isAdmin"
[applicationName]="applicationName"
[applicationLogo]="customizationSettings?.logo"
[username]="username"
[email]="user?.email"
[accessToken]="accessToken"
[applicationUrl]="customizationSettings?.applicationUrl"
(logoutClick)="logOut();"
>
</app-my-nav>

@ -33,6 +33,7 @@ export class AppComponent implements OnInit {
public applicationName: string = "Ombi"
public isAdmin: boolean;
public username: string;
public accessToken: string;
private hubConnected: boolean;
@ -55,6 +56,7 @@ export class AppComponent implements OnInit {
if (this.authService.loggedIn()) {
this.user = this.authService.claims();
this.username = this.user.name;
this.identity.getAccessToken().subscribe(x => this.accessToken = x);
if (!this.hubConnected) {
this.signalrNotification.initialize();
this.hubConnected = true;

@ -91,6 +91,7 @@ const routes: Routes = [
{ loadChildren: () => import("./vote/vote.module").then(m => m.VoteModule), path: "vote" },
{ loadChildren: () => import("./media-details/media-details.module").then(m => m.MediaDetailsModule), path: "details" },
{ loadChildren: () => import("./user-preferences/user-preferences.module").then(m => m.UserPreferencesModule), path: "user-preferences" },
{ loadChildren: () => import("./unsubscribe/unsubscribe.module").then(m => m.UnsubscribeModule), path: "unsubscribe" },
];

@ -11,7 +11,7 @@
</div>
<img [routerLink]="generateDetailsLink()" id="cardImage" src="{{result.posterPath}}" class="image"
alt="{{result.title}}">
<div class="middle">
<div [ngClass]="result.posterPath.includes('images/') ? 'middle-show' : 'middle'">
<a class="poster-overlay" [routerLink]="generateDetailsLink()">
<div class="summary">
<div class="title" id="title{{result.id}}">{{result.title}}</div>
@ -19,7 +19,7 @@
</div>
</a>
</div>
<div class="row button-request-container" *ngIf="!result.available && !result.approved && !result.requested">
<div [ngClass]="result.posterPath.includes('images/') ? 'button-request-container-show' : 'button-request-container'" class="row" *ngIf="!result.available && !result.approved && !result.requested">
<div class="button-request poster-overlay">
<button id="requestButton{{result.id}}{{result.type}}{{discoverType}}" *ngIf="requestable" mat-raised-button class="btn-ombi full-width poster-request-btn" (click)="request($event)">
<i *ngIf="!loading" class="fa-lg fas fa-cloud-download-alt"></i>

@ -105,6 +105,17 @@ small {
-ms-transform: translate(-50%, -50%);
}
.middle-show {
transition: .5s ease;
opacity: 1;
position: absolute;
top: 67%;
width: 90%;
left: 50%;
transform: translate(-50%, -50%);
-ms-transform: translate(-50%, -50%);
}
.c {
position: relative;
@ -253,12 +264,27 @@ a.poster-overlay:hover{
opacity:0;
transition: .5s ease;
}
.ombi-card .button-request-container-show{
position: relative;
width: 100%;
margin-left: 0;
margin-right: 0;
margin-top: -36px;
margin-bottom: 0px;
opacity:1;
transition: .5s ease;
}
::ng-deep .ombi-card .button-request-container .button-request{
padding-right:0px;
padding-left:0px;
width:100%;
}
::ng-deep .ombi-card .button-request-container-show .button-request{
padding-right:0px;
padding-left:0px;
width:100%;
}
.c:hover .button-request-container {
opacity:1;

@ -103,4 +103,10 @@
.card-skeleton {
padding: 5px;
}
@media (min-width:755px){
::ng-deep .p-carousel-item{
flex: 1 0 200px !important;
}
}

@ -46,80 +46,80 @@ export class CarouselListComponent implements OnInit {
{
breakpoint: '4000px',
numVisible: 17,
numScroll: 17
numScroll: 16
},
{
breakpoint: '3800px',
numVisible: 16,
numScroll: 16
numScroll: 15
},
{
breakpoint: '3600px',
numVisible: 15,
numScroll: 15
numScroll: 14
},
{
breakpoint: '3400px',
numVisible: 14,
numScroll: 14
numScroll: 13
},
{
breakpoint: '3200px',
numVisible: 13,
numScroll: 13
numScroll: 12
},
{
breakpoint: '3000px',
numVisible: 12,
numScroll: 12
numScroll: 11
},
{
breakpoint: '2800px',
numVisible: 11,
numScroll: 11
numScroll: 10
},
{
breakpoint: '2600px',
numVisible: 10,
numScroll: 10
numScroll: 9
},
{
breakpoint: '2400px',
numVisible: 9,
numScroll: 9
numScroll: 8
},
{
breakpoint: '2200px',
numVisible: 8,
numScroll: 8
numScroll: 7
},
{
breakpoint: '2000px',
numVisible: 7,
numScroll: 7
numScroll: 6
},
{
breakpoint: '1800px',
numVisible: 6,
numScroll: 6
numScroll: 5
},
{
breakpoint: '1650px',
numVisible: 5,
numScroll: 5
numScroll: 4
},
{
breakpoint: '1500px',
numVisible: 4,
numScroll: 4
numScroll: 3
},
{
breakpoint: '1250px',
breakpoint: '768px',
numVisible: 3,
numScroll: 3
numScroll: 2
},
{
breakpoint: '768px',
breakpoint: '660px',
numVisible: 2,
numScroll: 2
},

@ -10,9 +10,11 @@
<button class="col-2" mat-raised-button color="accent" (click)="requestCollection();">Request
Collection</button>
</div>
<div *ngIf="loadingFlag" class="row justify-content-md-center top-spacing loading-spinner">
<div *ngIf="loadingFlag" class="lightbox row justify-content-md-center">
<div class="row justify-content-md-center top-spacing loading-spinner">
<mat-spinner [color]="'accent'"></mat-spinner>
</div>
</div>
<div *ngIf="discoverResults" class="row full-height">
<div class="col-xl-2 col-lg-3 col-md-3 col-6 col-sm-4 small-padding" *ngFor="let result of discoverResults">
<discover-card [isAdmin]="isAdmins" [result]="result"></discover-card>

@ -14,6 +14,27 @@
margin-bottom: 28px;
}
.lightbox {
/* Default to hidden */
/* Overlay entire screen */
position: fixed;
z-index: 999;
top: 0;
left: 0;
right: 0;
bottom: 0;
/* A bit of padding around image */
padding: 1em;
/* Translucent background */
background: rgba(0, 0, 0, 0.8);
}
.loading-spinner {
margin: 10%;
position: absolute;
width: 100vh;
z-index: 10;
}

@ -1,10 +1,12 @@
import { Component, OnInit } from "@angular/core";
import { MessageService, SearchV2Service } from "../../../services";
import { ActivatedRoute } from "@angular/router";
import { SearchV2Service, RequestService, MessageService } from "../../../services";
import { IMovieCollectionsViewModel } from "../../../interfaces/ISearchTvResultV2";
import { AuthService } from "../../../auth/auth.service";
import { IDiscoverCardResult } from "../../interfaces";
import { IMovieCollectionsViewModel } from "../../../interfaces/ISearchTvResultV2";
import { RequestServiceV2 } from "../../../services/requestV2.service";
import { RequestType } from "../../../interfaces";
import { AuthService } from "../../../auth/auth.service";
@Component({
templateUrl: "./discover-collections.component.html",
@ -21,7 +23,7 @@ export class DiscoverCollectionsComponent implements OnInit {
constructor(private searchService: SearchV2Service,
private route: ActivatedRoute,
private requestService: RequestService,
private requestServiceV2: RequestServiceV2,
private messageService: MessageService,
private auth: AuthService) {
this.route.params.subscribe((params: any) => {
@ -37,10 +39,15 @@ export class DiscoverCollectionsComponent implements OnInit {
}
public async requestCollection() {
await this.collection.collection.forEach(async (movie) => {
await this.requestService.requestMovie({theMovieDbId: movie.id, languageCode: null, requestOnBehalf: null, qualityPathOverride: null, rootFolderOverride: null}).toPromise();
this.loading();
this.requestServiceV2.requestMovieCollection(this.collectionId).subscribe(result => {
if (result.result) {
this.messageService.send(result.message);
} else {
this.messageService.send(result.errorMessage);
}
this.finishLoading();
});
this.messageService.send("Requested Collection");
}
private createModel() {

@ -9,9 +9,4 @@ h2{
margin-top:40px;
margin-left:40px;
font-size: 24px;
}
::ng-deep .p-carousel-item{
min-height:290px;
max-height:290px;
}

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

@ -116,7 +116,6 @@ export interface IRadarrSettings extends IExternalSettings {
addOnly: boolean;
minimumAvailability: string;
scanForAvailability: boolean;
v3: boolean;
}
export interface ILidarrSettings extends IExternalSettings {
@ -284,6 +283,8 @@ export interface IVoteSettings extends ISettings {
export interface ITheMovieDbSettings extends ISettings {
showAdultMovies: boolean;
excludedKeywordIds: number[];
excludedMovieGenreIds: number[];
excludedTvGenreIds: number[]
}
export interface IUpdateModel

@ -0,0 +1,13 @@
.small-middle-container {
margin: auto;
width: 95%;
}
.table thead th{
border-bottom-width: 1px;
border-bottom-style: solid;
}
.table th{
border-top:none;
}

@ -6,6 +6,7 @@ import { IIssuesSummary, IPagenator, IssueStatus } from "../interfaces";
@Component({
selector: "issues-table",
templateUrl: "issuestable.component.html",
styleUrls: ['issuestable.component.scss']
})
export class IssuesTableComponent {

@ -3,12 +3,15 @@
<div class="small-middle-container">
<div class="row">
<div class="col-md-push-3 col-md-6">
<div class="col-md-push-3 col-md-6 logo-container">
<div *ngIf="customizationSettings.logo">
<img [src]="customizationSettings.logo" style="width:100%"/>
<img class="logo-img" [src]="customizationSettings.logo"/>
</div>
<div *ngIf="!customizationSettings.logo">
<img src="{{baseUrl}}/images/logo.png" style="width:100%" />
<div *ngIf="!customizationSettings.logo && customizationSettings.applicationName" class="logo">
{{customizationSettings.applicationName}}
</div>
<div *ngIf="!customizationSettings.logo && !customizationSettings.applicationName" class="logo">
OMBI
</div>
</div>
<div class="col-md-4 col-md-push-3 vcenter">
@ -39,11 +42,11 @@
<span [translate]="'LandingPage.OfflineParagraph'"></span>
<p [translate]="'LandingPage.CheckPageForUpdates'"></p>
</div>
<div class="button-continue">
<button id="continue" mat-raised-button [routerLink]="['/login', 'true']" color="accent" type="submit" data-cy="continue">{{ 'Common.ContinueButton' | translate }}</button>
</div>
</div>
</div>
<div class="col-md-3 offset-md-6 vcenter">
<button id="continue" mat-raised-button [routerLink]="['/login', 'true']" color="accent" type="submit" data-cy="continue">{{ 'Common.ContinueButton' | translate }}</button>
</div>
</div>
</div>

@ -1,10 +1,14 @@

.small-middle-container{
margin: auto;
width: 75%;
padding-top: 15%;
@import "~styles/variables.scss";
.small-middle-container{
height:100vh;
}
.row{
display:flex;
align-items: center;
justify-content: center;
height:100vh;
}
@media only screen and (max-width: 992px) {
div.centered {
@ -32,12 +36,6 @@ div.bg {
position: fixed;
}
.vcenter {
display: inline-block;
vertical-align: middle;
float: none;
}
.online{
color:lightgreen;
}
@ -61,4 +59,41 @@ span, b, i, p {
::ng-deep body {
background-color:#303030 !important;
}
.logo{
font:700 6em 'Montserrat', sans-serif;
text-align:center;
text-transform: uppercase;
color: $ombi-active;
padding:40px 20px;
white-space: normal;
}
@media (max-width: 1000px) {
.logo{
font:700 5em 'Montserrat', sans-serif;
}
}
@media (max-width: 800px) {
.logo{
font:700 4em 'Montserrat', sans-serif;
}
}
@media (max-width: 767px) {
.vcenter{
text-align:center;
}
}
.logo-img{
max-width:420px;
max-height:420px;
}
.logo-container{
display:flex;
justify-content: center;
}

@ -8,7 +8,7 @@
<mat-card class="mat-elevation-z8 top-margin login-card">
<H1 *ngIf="!customizationSettings.logo && !customizationSettings.applicationName" class="login_logo">OMBI</H1>
<H1 *ngIf="customizationSettings.applicationName && !customizationSettings.logo" [ngClass]="{'bigText': customizationSettings.applicationName.length >= 7 && customizationSettings.applicationName.length < 14, 'hugeText': customizationSettings.applicationName.length >= 14 }" class="login_logo custom">{{customizationSettings.applicationName}}</H1>
<img mat-card-image *ngIf="customizationSettings.logo" [src]="customizationSettings.logo">
<img mat-card-image *ngIf="customizationSettings.logo" [src]="customizationSettings.logo" class="logo-img">
<mat-card-content id="login-box" *ngIf="!authenticationSettings.enableOAuth || loginWithOmbi">
<form *ngIf="authenticationSettings" class="form-signin" novalidate [formGroup]="form" (ngSubmit)="onSubmit(form)">

@ -210,6 +210,11 @@ div.bg {
margin-bottom:70px;
}
.logo-img{
max-width:420px;
max-height:420px;
}
@media (max-width: 700px){
.login-card H1.login_logo{
font-size:20vw;

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

@ -24,6 +24,7 @@
[type]="requestType"
(openTrailer)="openDialog()"
(onAdvancedOptions)="openAdvancedOptions()"
(onReProcessRequest)="reProcessRequest()"
>
</social-icons>
@ -48,7 +49,7 @@
{{'Search.ViewOnEmby' | translate}}
<i class="far fa-play-circle fa-2x"></i>
</a>
<a id="viewOnJellyfinButton" *ngIf="movie.jellyfinUrl" href="{{movie.jellyfinUrl}}" mat-raised-button target="_blank" class="btn-spacing viewon-btn jellyfinUrl">
<a id="viewOnJellyfinButton" *ngIf="movie.jellyfinUrl" href="{{movie.jellyfinUrl}}" mat-raised-button target="_blank" class="btn-spacing viewon-btn jellyfin">
{{'Search.ViewOnJellyfin' | translate}}
<i class="far fa-play-circle fa-2x"></i>
</a>
@ -64,7 +65,7 @@
</button>
</ng-template>
<ng-template #notRequestedBtn>
<button id="requestBtn" mat-raised-button class="btn-spacing" color="primary" (click)="request()">
<button *ngIf="!movie.requested" id="requestBtn" mat-raised-button class="btn-spacing" color="primary" (click)="request()">
<i *ngIf="movie.requestProcessing" class="fas fa-circle-notch fa-spin fa-fw"></i>
<i *ngIf="!movie.requestProcessing && !movie.processed" class="fas fa-plus"></i>
<i *ngIf="movie.processed && !movie.requestProcessing" class="fas fa-check"></i>
@ -81,7 +82,7 @@
<i class="fas fa-plus"></i> {{ 'Requests.MarkAvailable' | translate }}
</button>
<button id="denyBtn" *ngIf="movieRequest && !movieRequest.denied && !movie.available" mat-raised-button class="btn-spacing" color="warn" (click)="deny()">
<button id="denyBtn" *ngIf="movieRequest && !movieRequest.denied" mat-raised-button class="btn-spacing" color="warn" (click)="deny()">
<i class="fas fa-times"></i> {{'Requests.Deny' | translate }}
</button>
<button id="deniedButton" *ngIf="movieRequest && movieRequest.denied" [matTooltip]="movieRequest.deniedReason" mat-raised-button class="btn-spacing" color="warn">

@ -190,6 +190,16 @@ export class MovieDetailsComponent {
});
}
public reProcessRequest() {
this.requestService2.reprocessRequest(this.movieRequest.id, RequestType.movie).subscribe(result => {
if (result.result) {
this.messageService.send(result.message ? result.message : "Successfully Re-processed the request", "Ok");
} else {
this.messageService.send(result.errorMessage, "Ok");
}
});
}
private loadBanner() {
this.imageService.getMovieBanner(this.theMovidDbId.toString()).subscribe(x => {
if (!this.movie.backdropPath) {

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

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

Loading…
Cancel
Save