pull/2988/head
tidusjar 5 years ago
commit 0fb6e31bfb

@ -1,17 +1,115 @@
# Changelog
## v3.0.4119 (2019-1-09)
## v3.0.4256 (2019-02-18)
### **New Features**
- Added a new Custom Page, this will allow you to completely change the page via a WYSIWYG editor! [TidusJar]
- Update discord link to follow the scheme of other links. [Tom McClellan]
- Added the ability to search movies via the movie db with a different language! [tidusjar]
- Update issue templates. [Jamie]
- Added the ability to specify a year when searching for movies. [tidusjar]
- Update README.md. [Jamie]
- Made the newsletter use the default lanuage code set in the Ombi settings for movie information. [TidusJar]
- Update CHANGELOG.md. [Jamie]
- Added the functionality to remove a user from getting notifications to their mobile device #2780. [tidusjar]
- Added a demo mode, this will only show movies and shows that are in the public domain. Dam that stupid fruit company. [tidusjar]
- Added Actor Searching for Movies! [TidusJar]
- Added the ability to change where the View on Emby link goes to #2730. [TidusJar]
- Added the request queue to the notifications UI so you can turn it off per notification agent #2747. [TidusJar]
- Added new classes to the posters #2732. [TidusJar]
### **Fixes**
- Fix: src/Ombi/package.json to reduce vulnerabilities. [snyk-bot]
- Fixed #2801 this is when a season is not correctly monitored in sonarr when approved by an admin. [tidusjar]
- Small improvements to try and mitigate #2750. [tidusjar]
- Removed some areas where we clear out the cache. This should help with DB locking #2750. [tidusjar]
- Fixed #2810. [tidusjar]
- Cannot create an issue comment with the API #2811. [tidusjar]
- Set the local domain if the Application URL is set for the HELO or EHLO commands. #2636. [tidusjar]
- New translations en.json (Spanish) [Jamie]
- Delete ISSUE_TEMPLATE.md. [Jamie]
- More minor grammatical edits. [Andrew Metzger]
- Minor grammatical edits. [Andrew Metzger]
- Fixed #2802 the issue where "Issues" were not being deleted correctly. [tidusjar]
- Fixed #2797. [tidusjar]
- New translations en.json (Dutch) [Jamie]
- New translations en.json (Spanish) [Jamie]
- New translations en.json (Portuguese, Brazilian) [Jamie]
- Fixed #2786. [tidusjar]
- Fixed #2756. [tidusjar]
- Ignore the UserName header as part of the Api is the value is an empty string. [tidusjar]
- Fixed #2759. [tidusjar]
- Did #2756. [TidusJar]
- Fixed the exception that sometimes makes ombi fallover. [TidusJar]
- New translations en.json (Swedish) [Jamie]
- New translations en.json (Swedish) [Jamie]
- New translations en.json (Swedish) [Jamie]
- New translations en.json (Swedish) [Jamie]
- New translations en.json (Swedish) [Jamie]
- New translations en.json (Swedish) [Jamie]
- New translations en.json (Swedish) [Jamie]
- New translations en.json (Swedish) [Jamie]
- New translations en.json (Swedish) [Jamie]
- New translations en.json (Swedish) [Jamie]
- Log the error to the ui to figure out what's going on with #2755. [tidusjar]
- Small fix when denying a request with a reason, we wasn't updating the ui. [TidusJar]
- Make sure we can only set the ApiAlias when using the API Key. [tidusjar]
- #2363 Added the ability to pass any username into the API using the ApiAlias header. [tidusjar]
- Removed the Add user to Plex from Ombi. [tidusjar]
## v3.0.4119 (2019-01-09)
### **New Features**
- Update CHANGELOG.md. [Jamie]
- Added a page where the admin can write/style/basically do whatever they want with e.g. FAQ for the users #2715 This needs to be enabled in the Customization Settings and then it's all configured on the page. [TidusJar]
- Updated the AspnetCore.App package to remove the CVE-2019-0564 vulnerability. [TidusJar]
- Added a global language flag that now applies to the search by default. [tidusjar]
@ -21,28 +119,114 @@
- Added {AvailableDate} as a Notification Variable, this is the date the request was marked as available. See here: https://github.com/tidusjar/Ombi/wiki/Notification-Template-Variables. [tidusjar]
- Updated the Newsletter template! Better mail client support [d1slact0r]
- Added the ability to search movies via the movie db with a different language! [tidusjar]
- Updated boostrap #2694. [TidusJar]
- Added the ability to specify a year when searching for movies. [tidusjar]
- Added the ability to deny a request with a reason. [TidusJar]
- Update NewsletterTemplate.html. [d1slact0r]
- Updated to .net core 2.2 and included a linux-arm64 build. [aptalca]
- Update NewsletterTemplate.html. [d1slact0r]
- Make the newsletter BCC the users rather than creating a million newsletters (Hopefully will stop SMTP providers from marking as spam). This does mean that the custom user customization in the newsletter will no longer work. [TidusJar]
- Update NewsletterTemplate.html. [d1slact0r]
- Update HtmlTemplateGenerator.cs. [d1slact0r]
- Update NewsletterTemplate.html. [d1slact0r]
- Update HtmlTemplateGenerator.cs. [d1slact0r]
- Update NewsletterTemplate.html. [d1slact0r]
- Update NewsletterTemplate.html. [d1slact0r]
- Update NewsletterTemplate.html. [d1slact0r]
- Update HtmlTemplateGenerator.cs. [d1slact0r]
- Updated boostrap #2694. [Jamie]
- Added the ability to deny a request with a reason. [TidusJar]
- Update EmbyEpisodeSync.cs. [Jamie]
- New translations [TidusJar]
- Updated to .net core 2.2 and included a linux-arm64 build. [TidusJar]
### **Fixes**
- There is now a new Job in ombi that will clear out the Plex/Emby data and recache. This will prevent the issues going forward that we have when Ombi and the Media server fall out of sync with deletions/updates #2641 #2362 #1566. [TidusJar]
- Potentially fix #2726. [TidusJar]
- New translations en.json (Spanish) [Jamie]
- New translations en.json (Spanish) [Jamie]
- New translations en.json (Dutch) [Jamie]
- Fixed #2725 and #2721. [TidusJar]
- Made the newsletter use the default lanuage code set in the Ombi settings for movie information. [TidusJar]
- Save the language code against the request so we can use it later e.g. Sending to the DVR apps. [tidusjar]
- Fixed #2716. [tidusjar]
- Make the newsletter BCC the users rather than creating a million newsletters (Hopefully will stop SMTP providers from marking as spam). This does mean that the custom user customization in the newsletter will no longer work. [TidusJar]
- If we don't know the Plex agent, then see if it's a ImdbId, if it's not check the string for any episode and season hints #2695. [tidusjar]
- New translations en.json (Swedish) [Jamie]
- New translations en.json (Spanish) [Jamie]
- New translations en.json (Portuguese, Brazilian) [Jamie]
- New translations en.json (Polish) [Jamie]
- New translations en.json (Norwegian) [Jamie]
- New translations en.json (Italian) [Jamie]
- New translations en.json (German) [Jamie]
- New translations en.json (French) [Jamie]
- New translations en.json (Dutch) [Jamie]
- New translations en.json (Danish) [Jamie]
- New translations en.json (Dutch) [Jamie]
- New translations en.json (Dutch) [Jamie]
- New translations en.json (Dutch) [Jamie]
- Made the search results the language specified in the search refinement. [tidusjar]
- Fixed #2704. [tidusjar]
- Now it is fixed :) [d1slact0r]
- Android please be nice now. [d1slact0r]
- Fixed title bit better. [d1slact0r]
- Fixed titles. [d1slact0r]
- This should fix the build for sure (stupid quotes) [d1slact0r]
- Fixes build. [d1slact0r]
- Rewritten the whole newsletter template. [d1slact0r]
- Fixed #2697. [tidusjar]
- Add linux-arm runtime identifier. [aptalca]
- Add back arm packages. [aptalca]
- Add arm32 package. [aptalca]
- Fixed #2691. [tidusjar]
- Fixed linting. [TidusJar]
@ -51,10 +235,13 @@
- Fixed #2678. [TidusJar]
- Deny reason for movie requests. [TidusJar]
- Set the landing and login pages background refresh to 15 seconds rather than 10 and 7. [TidusJar]
- Fixed a bug with us thinking future dated emby episodes are not available, Consoldated the emby and plex search rules (since they have the same logic) [TidusJar]
- Fixed build. [TidusJar]
## v3.0.4036 (2018-12-11)

@ -10,11 +10,19 @@ ____
[![Paypal](https://img.shields.io/badge/paypal-donate-yellow.svg)](https://paypal.me/PlexRequestsNet)
___
<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>
<a href='https://itunes.apple.com/us/app/ombi/id1335260043?ls=1&mt=8'><img width="150" alt='Get it on App Store' src='https://i.imgur.com/cJFa0M4.png'/></a>
[![Twitter](https://img.shields.io/twitter/follow/tidusjar.svg?style=social)](https://twitter.com/intent/follow?screen_name=tidusjar)
Follow me developing Ombi!
[![Twitch](https://img.shields.io/badge/Twitch-Watch-blue.svg?style=flat-square&logo=twitch)](https://twitch.tv/tiusjar)
___
<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._
___
We also now have merch up on Teespring!
@ -34,6 +42,7 @@ Here are some of the features Ombi V3 has:
* Now working without crashes on Linux.
* Lets users request Movies, Music, and TV Shows (whether it being the entire series, an entire season, or even single episodes.)
* Easily manage your requests
* Allows you to set specific users to automatically have requests approved and added to the relevant service (Sonarr/Radarr/Lidarr/Couchpotato etc)
* User management system (supports plex.tv, Emby and local accounts)
* A landing page that will give you the availability of your Plex/Emby server and also add custom notification text to inform your users of downtime.
* Allows your users to get custom notifications!
@ -41,7 +50,7 @@ Here are some of the features Ombi V3 has:
* Will show if the request is already on plex or even if it's already monitored.
* Automatically updates the status of requests when they are available on Plex/Emby
* Slick, responsive and mobile friendly UI
* Ombi will automatically update itself :)
* Ombi will automatically update itself :) (YMMV)
* Very fast!
### Integration

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

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

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

@ -19,5 +19,6 @@ namespace Ombi.Core
Task<SearchMovieViewModel> LookupImdbInformation(int theMovieDbId, string langCode = null);
Task<IEnumerable<SearchMovieViewModel>> SimilarMovies(int theMovieDbId, string langCode);
Task<IEnumerable<SearchMovieViewModel>> SearchActor(string search, string langaugeCode);
}
}

@ -83,7 +83,8 @@ namespace Ombi.Core.Engine
Approved = false,
RequestedUserId = userDetails.Id,
Background = movieInfo.BackdropPath,
LangCode = model.LanguageCode
LangCode = model.LanguageCode,
RequestedByAlias = model.RequestedByAlias
};
var usDates = movieInfo.ReleaseDates?.Results?.FirstOrDefault(x => x.IsoCode == "US");
@ -325,6 +326,7 @@ namespace Ombi.Core.Engine
return new RequestEngineResult
{
Result = true,
Message = "Request successfully deleted",
};
}

@ -31,11 +31,11 @@ namespace Ombi.Core.Engine
Logger = logger;
}
private IMovieDbApi MovieApi { get; }
private IMapper Mapper { get; }
protected IMovieDbApi MovieApi { get; }
protected IMapper Mapper { get; }
private ILogger<MovieSearchEngine> Logger { get; }
private const int MovieLimit = 10;
protected const int MovieLimit = 10;
/// <summary>
/// Lookups the imdb information.
@ -54,8 +54,6 @@ namespace Ombi.Core.Engine
/// <summary>
/// Searches the specified movie.
/// </summary>
/// <param name="search">The search.</param>
/// <returns></returns>
public async Task<IEnumerable<SearchMovieViewModel>> Search(string search, int? year, string langaugeCode)
{
langaugeCode = await DefaultLanguageCode(langaugeCode);
@ -68,6 +66,33 @@ namespace Ombi.Core.Engine
return null;
}
public async Task<IEnumerable<SearchMovieViewModel>> SearchActor(string search, string langaugeCode)
{
langaugeCode = await DefaultLanguageCode(langaugeCode);
var people = await MovieApi.SearchByActor(search, langaugeCode);
var person = people?.results?.Count > 0 ? people.results.FirstOrDefault() : null;
var resultSet = new List<SearchMovieViewModel>();
if (person == null)
{
return resultSet;
}
// Get this person movie credits
var credits = await MovieApi.GetActorMovieCredits(person.id, langaugeCode);
// Grab results from both cast and crew, prefer items in cast. we can handle directors like this.
var movieResults = (from role in credits.cast select new { Id = role.id, Title = role.title, ReleaseDate = role.release_date }).ToList();
movieResults.AddRange((from job in credits.crew select new { Id = job.id, Title = job.title, ReleaseDate = job.release_date }).ToList());
movieResults = movieResults.Take(10).ToList();
foreach (var movieResult in movieResults)
{
resultSet.Add(await LookupImdbInformation(movieResult.Id, langaugeCode));
}
return resultSet;
}
/// <summary>
/// Get similar movies to the id passed in
/// </summary>
@ -159,7 +184,7 @@ namespace Ombi.Core.Engine
return null;
}
private async Task<List<SearchMovieViewModel>> TransformMovieResultsToResponse(
protected async Task<List<SearchMovieViewModel>> TransformMovieResultsToResponse(
IEnumerable<MovieSearchResult> movies)
{
var viewMovies = new List<SearchMovieViewModel>();
@ -170,24 +195,25 @@ namespace Ombi.Core.Engine
return viewMovies;
}
private async Task<SearchMovieViewModel> ProcessSingleMovie(SearchMovieViewModel viewMovie, bool lookupExtraInfo = false)
protected async Task<SearchMovieViewModel> ProcessSingleMovie(SearchMovieViewModel viewMovie, bool lookupExtraInfo = false)
{
if (lookupExtraInfo)
if (lookupExtraInfo && viewMovie.ImdbId.IsNullOrEmpty())
{
var showInfo = await MovieApi.GetMovieInformation(viewMovie.Id);
viewMovie.Id = showInfo.Id; // TheMovieDbId
viewMovie.ImdbId = showInfo.ImdbId;
var usDates = viewMovie.ReleaseDates?.Results?.FirstOrDefault(x => x.IsoCode == "US");
viewMovie.DigitalReleaseDate = usDates?.ReleaseDate?.FirstOrDefault(x => x.Type == ReleaseDateType.Digital)?.ReleaseDate;
}
var usDates = viewMovie.ReleaseDates?.Results?.FirstOrDefault(x => x.IsoCode == "US");
viewMovie.DigitalReleaseDate = usDates?.ReleaseDate?.FirstOrDefault(x => x.Type == ReleaseDateType.Digital)?.ReleaseDate;
viewMovie.TheMovieDbId = viewMovie.Id.ToString();
await RunSearchRules(viewMovie);
// This requires the rules to be run first to populate the RequestId property
await CheckForSubscription(viewMovie);
return viewMovie;
}

@ -83,7 +83,8 @@ namespace Ombi.Core.Engine
Title = album.title,
Disk = album.images?.FirstOrDefault(x => x.coverType.Equals("disc"))?.url,
Cover = album.images?.FirstOrDefault(x => x.coverType.Equals("cover"))?.url,
ForeignArtistId = album?.artist?.foreignArtistId ?? string.Empty
ForeignArtistId = album?.artist?.foreignArtistId ?? string.Empty,
RequestedByAlias = model.RequestedByAlias
};
if (requestModel.Cover.IsNullOrEmpty())
{

@ -385,6 +385,7 @@ namespace Ombi.Core.Engine
foreach (var ep in s.Episodes)
{
ep.Approved = true;
ep.Requested = true;
}
}

@ -40,8 +40,8 @@ namespace Ombi.Core.Engine
EmbyContentRepo = embyRepo;
}
private ITvMazeApi TvMazeApi { get; }
private IMapper Mapper { get; }
protected ITvMazeApi TvMazeApi { get; }
protected IMapper Mapper { get; }
private ISettingsService<PlexSettings> PlexSettings { get; }
private ISettingsService<EmbySettings> EmbySettings { get; }
private IPlexContentRepository PlexContentRepo { get; }
@ -99,7 +99,7 @@ namespace Ombi.Core.Engine
{
Url = e.url,
Title = e.name,
AirDate = DateTime.Parse(e.airstamp ?? DateTime.MinValue.ToString()),
AirDate = e.airstamp.HasValue() ? DateTime.Parse(e.airstamp) : DateTime.MinValue,
EpisodeNumber = e.number,
});
@ -112,7 +112,7 @@ namespace Ombi.Core.Engine
{
Url = e.url,
Title = e.name,
AirDate = DateTime.Parse(e.airstamp ?? DateTime.MinValue.ToString()),
AirDate = e.airstamp.HasValue() ? DateTime.Parse(e.airstamp) : DateTime.MinValue,
EpisodeNumber = e.number,
});
}
@ -149,7 +149,7 @@ namespace Ombi.Core.Engine
return processed;
}
private IEnumerable<SearchTvShowViewModel> ProcessResults<T>(IEnumerable<T> items)
protected IEnumerable<SearchTvShowViewModel> ProcessResults<T>(IEnumerable<T> items)
{
var retVal = new List<SearchTvShowViewModel>();
foreach (var tvMazeSearch in items)
@ -159,7 +159,7 @@ namespace Ombi.Core.Engine
return retVal;
}
private SearchTvShowViewModel ProcessResult<T>(T tvMazeSearch)
protected SearchTvShowViewModel ProcessResult<T>(T tvMazeSearch)
{
return Mapper.Map<SearchTvShowViewModel>(tvMazeSearch);
}

@ -72,6 +72,7 @@ namespace Ombi.Core.Helpers
SeasonRequests = new List<SeasonRequests>(),
Title = ShowInfo.name,
ReleaseYear = FirstAir,
RequestedByAlias = model.RequestedByAlias,
SeriesType = ShowInfo.genres.Any( s => s.Equals("Anime", StringComparison.InvariantCultureIgnoreCase)) ? SeriesType.Anime : SeriesType.Standard
};

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

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

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

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

@ -49,7 +49,6 @@ namespace Ombi.Core.Senders
{
try
{
var cpSettings = await CouchPotatoSettings.GetSettingsAsync();
//var watcherSettings = await WatcherSettings.GetSettingsAsync();
var radarrSettings = await RadarrSettings.GetSettingsAsync();
@ -76,7 +75,7 @@ namespace Ombi.Core.Senders
}
catch (Exception e)
{
Log.LogError(e, "Error when seing movie to DVR app, added to the request queue");
Log.LogError(e, "Error when sending movie to DVR app, added to the request queue");
// Check if already in request quee
var existingQueue = await _requestQueuRepository.FirstOrDefaultAsync(x => x.RequestId == model.Id);

@ -16,6 +16,7 @@ using Ombi.Settings.Settings.Models.External;
using Ombi.Store.Entities;
using Ombi.Store.Entities.Requests;
using Ombi.Store.Repository;
using Remotion.Linq.Parsing.Structure.IntermediateModel;
namespace Ombi.Core.Senders
{
@ -57,7 +58,7 @@ namespace Ombi.Core.Senders
var sonarr = await SonarrSettings.GetSettingsAsync();
if (sonarr.Enabled)
{
var result = await SendToSonarr(model);
var result = await SendToSonarr(model, sonarr);
if (result != null)
{
return new SenderResult
@ -109,7 +110,7 @@ namespace Ombi.Core.Senders
catch (Exception e)
{
Logger.LogError(e, "Exception thrown when sending a movie to DVR app, added to the request queue");
// Check if already in request quee
// Check if already in request queue
var existingQueue = await _requestQueueRepository.FirstOrDefaultAsync(x => x.RequestId == model.Id);
if (existingQueue != null)
{
@ -134,7 +135,7 @@ namespace Ombi.Core.Senders
return new SenderResult
{
Success = false,
Message = "Something wen't wrong!"
Message = "Something went wrong!"
};
}
@ -150,13 +151,8 @@ namespace Ombi.Core.Senders
/// <param name="s"></param>
/// <param name="model"></param>
/// <returns></returns>
public async Task<NewSeries> SendToSonarr(ChildRequests model)
public async Task<NewSeries> SendToSonarr(ChildRequests model, SonarrSettings s)
{
var s = await SonarrSettings.GetSettingsAsync();
if (!s.Enabled)
{
return null;
}
if (string.IsNullOrEmpty(s.ApiKey))
{
return null;
@ -319,10 +315,19 @@ namespace Ombi.Core.Senders
foreach (var season in model.SeasonRequests)
{
var sonarrSeason = sonarrEpList.Where(x => x.seasonNumber == season.SeasonNumber);
var sonarrEpCount = sonarrSeason.Count();
var sonarrEpisodeList = sonarrEpList.Where(x => x.seasonNumber == season.SeasonNumber).ToList();
var sonarrEpCount = sonarrEpisodeList.Count;
var ourRequestCount = season.Episodes.Count;
var ourEpisodes = season.Episodes.Select(x => x.EpisodeNumber).ToList();
var unairedEpisodes = sonarrEpisodeList.Where(x => x.airDateUtc > DateTime.UtcNow).Select(x => x.episodeNumber).ToList();
//// Check if we have requested all the latest episodes, if we have then monitor
//// NOTE, not sure if needed since ombi ui displays future episodes anyway...
//ourEpisodes.AddRange(unairedEpisodes);
//var distinctEpisodes = ourEpisodes.Distinct().ToList();
//var missingEpisodes = Enumerable.Range(distinctEpisodes.Min(), distinctEpisodes.Count).Except(distinctEpisodes);
var existingSeason =
result.seasons.FirstOrDefault(x => x.seasonNumber == season.SeasonNumber);
if (existingSeason == null)
@ -332,7 +337,7 @@ namespace Ombi.Core.Senders
}
if (sonarrEpCount == ourRequestCount)
if (sonarrEpCount == ourRequestCount /*|| !missingEpisodes.Any()*/)
{
// We have the same amount of requests as all of the episodes in the season.

@ -53,6 +53,7 @@ using Ombi.Updater;
using PlexContentCacher = Ombi.Schedule.Jobs.Plex;
using Ombi.Api.Telegram;
using Ombi.Core.Authentication;
using Ombi.Core.Engine.Demo;
using Ombi.Core.Processor;
using Ombi.Schedule.Jobs.Lidarr;
using Ombi.Schedule.Jobs.Plex.Interfaces;
@ -92,6 +93,8 @@ namespace Ombi.DependencyInjection
services.AddTransient<IMassEmailSender, MassEmailSender>();
services.AddTransient<IPlexOAuthManager, PlexOAuthManager>();
services.AddTransient<IVoteEngine, VoteEngine>();
services.AddTransient<IDemoMovieSearchEngine, DemoMovieSearchEngine>();
services.AddTransient<IDemoTvSearchEngine, DemoTvSearchEngine>();
}
public static void RegisterHttp(this IServiceCollection services)
{

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

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

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

@ -15,5 +15,6 @@
public const string Disabled = nameof(Disabled);
public const string ReceivesNewsletter = nameof(ReceivesNewsletter);
public const string ManageOwnRequests = nameof(ManageOwnRequests);
public const string EditCustomPage = nameof(EditCustomPage);
}
}

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

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

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

@ -46,20 +46,7 @@ namespace Ombi.Notifications.Agents
protected override async Task NewRequest(NotificationOptions model, MattermostNotificationSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Mattermost, NotificationType.NewRequest, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {NotificationType.NewRequest} is disabled for {NotificationAgent.Mattermost}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
AddOtherInformation(model, notification, parsed);
//notification.Other.Add("overview", model.RequestType == RequestType.Movie ? base.MovieRequest.Overview : TvRequest.);
await Send(notification, settings);
await Run(model, settings, NotificationType.NewRequest);
}
private void AddOtherInformation(NotificationOptions model, NotificationMessage notification,
@ -71,125 +58,37 @@ namespace Ombi.Notifications.Agents
protected override async Task NewIssue(NotificationOptions model, MattermostNotificationSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Mattermost, NotificationType.Issue, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {NotificationType.Issue} is disabled for {NotificationAgent.Mattermost}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
AddOtherInformation(model, notification, parsed);
await Send(notification, settings);
await Run(model, settings, NotificationType.Issue);
}
protected override async Task IssueComment(NotificationOptions model, MattermostNotificationSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Mattermost, NotificationType.IssueComment, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {NotificationType.IssueComment} is disabled for {NotificationAgent.Mattermost}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
AddOtherInformation(model, notification, parsed);
await Send(notification, settings);
await Run(model, settings, NotificationType.IssueComment);
}
protected override async Task IssueResolved(NotificationOptions model, MattermostNotificationSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Mattermost, NotificationType.IssueResolved, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {NotificationType.IssueResolved} is disabled for {NotificationAgent.Mattermost}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
AddOtherInformation(model, notification, parsed);
await Send(notification, settings);
await Run(model, settings, NotificationType.IssueResolved);
}
protected override async Task AddedToRequestQueue(NotificationOptions model, MattermostNotificationSettings settings)
{
var user = string.Empty;
var title = string.Empty;
var image = string.Empty;
if (model.RequestType == RequestType.Movie)
{
user = MovieRequest.RequestedUser.UserAlias;
title = MovieRequest.Title;
image = MovieRequest.PosterPath;
}
else
{
user = TvRequest.RequestedUser.UserAlias;
title = TvRequest.ParentRequest.Title;
image = TvRequest.ParentRequest.PosterPath;
}
var message = $"Hello! The user '{user}' has requested {title} but it could not be added. This has been added into the requests queue and will keep retrying";
var notification = new NotificationMessage
{
Message = message
};
notification.Other.Add("image", image);
await Send(notification, settings);
await Run(model, settings, NotificationType.ItemAddedToFaultQueue);
}
protected override async Task RequestDeclined(NotificationOptions model, MattermostNotificationSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Mattermost, NotificationType.RequestDeclined, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {NotificationType.RequestDeclined} is disabled for {NotificationAgent.Mattermost}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
AddOtherInformation(model, notification, parsed);
await Send(notification, settings);
await Run(model, settings, NotificationType.RequestDeclined);
}
protected override async Task RequestApproved(NotificationOptions model, MattermostNotificationSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Mattermost, NotificationType.RequestApproved, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {NotificationType.RequestApproved} is disabled for {NotificationAgent.Mattermost}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
AddOtherInformation(model, notification, parsed);
await Send(notification, settings);
await Run(model, settings, NotificationType.RequestApproved);
}
protected override async Task AvailableRequest(NotificationOptions model, MattermostNotificationSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Mattermost, NotificationType.RequestAvailable, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {NotificationType.RequestAvailable} is disabled for {NotificationAgent.Mattermost}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
AddOtherInformation(model, notification, parsed);
await Send(notification, settings);
await Run(model, settings, NotificationType.RequestAvailable);
}
protected override async Task Send(NotificationMessage model, MattermostNotificationSettings settings)
@ -228,5 +127,21 @@ namespace Ombi.Notifications.Agents
};
await Send(notification, settings);
}
private async Task Run(NotificationOptions model, MattermostNotificationSettings settings, NotificationType type)
{
var parsed = await LoadTemplate(NotificationAgent.Mattermost, type, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {type} is disabled for {NotificationAgent.Mattermost}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
AddOtherInformation(model, notification, parsed);
await Send(notification, settings);
}
}
}

@ -130,23 +130,18 @@ namespace Ombi.Notifications.Agents
protected override async Task AddedToRequestQueue(NotificationOptions model, MobileNotificationSettings settings)
{
string user;
string title;
if (model.RequestType == RequestType.Movie)
{
user = MovieRequest.RequestedUser.UserAlias;
title = MovieRequest.Title;
}
else
var parsed = await LoadTemplate(NotificationAgent.Mobile, NotificationType.ItemAddedToFaultQueue, model);
if (parsed.Disabled)
{
user = TvRequest.RequestedUser.UserAlias;
title = TvRequest.ParentRequest.Title;
_logger.LogInformation($"Template {NotificationType.ItemAddedToFaultQueue} is disabled for {NotificationAgent.Mobile}");
return;
}
var message = $"Hello! The user '{user}' has requested {title} but it could not be added. This has been added into the requests queue and will keep retrying";
var notification = new NotificationMessage
{
Message = message
Message = parsed.Message,
};
// Get admin devices
var playerIds = await GetAdmins(NotificationType.Test);
await Send(playerIds, notification, settings, model);
@ -294,6 +289,5 @@ namespace Ombi.Notifications.Agents
}
}
}
}
}

@ -44,131 +44,43 @@ namespace Ombi.Notifications.Agents
protected override async Task NewRequest(NotificationOptions model, PushbulletSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Pushbullet, NotificationType.NewRequest, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {NotificationType.NewRequest} is disabled for {NotificationAgent.Pushbullet}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
await Send(notification, settings);
await Run(model, settings, NotificationType.NewRequest);
}
protected override async Task NewIssue(NotificationOptions model, PushbulletSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Pushbullet, NotificationType.Issue, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {NotificationType.Issue} is disabled for {NotificationAgent.Pushbullet}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
await Send(notification, settings);
await Run(model, settings, NotificationType.Issue);
}
protected override async Task IssueComment(NotificationOptions model, PushbulletSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Pushbullet, NotificationType.IssueComment, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {NotificationType.IssueComment} is disabled for {NotificationAgent.Pushbullet}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
await Send(notification, settings);
await Run(model, settings, NotificationType.IssueComment);
}
protected override async Task IssueResolved(NotificationOptions model, PushbulletSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Pushbullet, NotificationType.IssueResolved, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {NotificationType.IssueResolved} is disabled for {NotificationAgent.Pushbullet}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
await Send(notification, settings);
await Run(model, settings, NotificationType.IssueResolved);
}
protected override async Task AddedToRequestQueue(NotificationOptions model, PushbulletSettings settings)
{
string user;
string title;
if (model.RequestType == RequestType.Movie)
{
user = MovieRequest.RequestedUser.UserAlias;
title = MovieRequest.Title;
}
else
{
user = TvRequest.RequestedUser.UserAlias;
title = TvRequest.ParentRequest.Title;
}
var message = $"Hello! The user '{user}' has requested {title} but it could not be added. This has been added into the requests queue and will keep retrying";
var notification = new NotificationMessage
{
Message = message
};
await Send(notification, settings);
await Run(model, settings, NotificationType.ItemAddedToFaultQueue);
}
protected override async Task RequestDeclined(NotificationOptions model, PushbulletSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Pushbullet, NotificationType.RequestDeclined, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {NotificationType.RequestDeclined} is disabled for {NotificationAgent.Pushbullet}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
await Send(notification, settings);
await Run(model, settings, NotificationType.RequestDeclined);
}
protected override async Task RequestApproved(NotificationOptions model, PushbulletSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Pushbullet, NotificationType.RequestApproved, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {NotificationType.RequestApproved} is disabled for {NotificationAgent.Pushbullet}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
await Send(notification, settings);
await Run(model, settings, NotificationType.RequestApproved);
}
protected override async Task AvailableRequest(NotificationOptions model, PushbulletSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Pushbullet, NotificationType.RequestAvailable, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {NotificationType.RequestAvailable} is disabled for {NotificationAgent.Pushbullet}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
await Send(notification, settings);
await Run(model, settings, NotificationType.RequestAvailable);
}
protected override async Task Send(NotificationMessage model, PushbulletSettings settings)
@ -192,5 +104,22 @@ namespace Ombi.Notifications.Agents
};
await Send(notification, settings);
}
private async Task Run(NotificationOptions model, PushbulletSettings settings, NotificationType type)
{
var parsed = await LoadTemplate(NotificationAgent.Pushbullet, type, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {type} is disabled for {NotificationAgent.Pushbullet}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
await Send(notification, settings);
}
}
}

@ -45,132 +45,42 @@ namespace Ombi.Notifications.Agents
protected override async Task NewRequest(NotificationOptions model, PushoverSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Pushover, NotificationType.NewRequest, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {NotificationType.NewRequest} is disabled for {NotificationAgent.Pushover}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
await Send(notification, settings);
await Run(model, settings, NotificationType.NewRequest);
}
protected override async Task NewIssue(NotificationOptions model, PushoverSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Pushover, NotificationType.Issue, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {NotificationType.Issue} is disabled for {NotificationAgent.Pushover}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
await Send(notification, settings);
await Run(model, settings, NotificationType.Issue);
}
protected override async Task IssueComment(NotificationOptions model, PushoverSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Pushover, NotificationType.IssueComment, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {NotificationType.IssueComment} is disabled for {NotificationAgent.Pushover}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
await Send(notification, settings);
await Run(model, settings, NotificationType.IssueComment);
}
protected override async Task IssueResolved(NotificationOptions model, PushoverSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Pushover, NotificationType.IssueResolved, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {NotificationType.IssueResolved} is disabled for {NotificationAgent.Pushover}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
await Send(notification, settings);
await Run(model, settings, NotificationType.IssueResolved);
}
protected override async Task AddedToRequestQueue(NotificationOptions model, PushoverSettings settings)
{
string user;
string title;
if (model.RequestType == RequestType.Movie)
{
user = MovieRequest.RequestedUser.UserAlias;
title = MovieRequest.Title;
}
else
{
user = TvRequest.RequestedUser.UserAlias;
title = TvRequest.ParentRequest.Title;
}
var message = $"Hello! The user '{user}' has requested {title} but it could not be added. This has been added into the requests queue and will keep retrying";
var notification = new NotificationMessage
{
Message = message
};
await Send(notification, settings);
await Run(model, settings, NotificationType.ItemAddedToFaultQueue);
}
protected override async Task RequestDeclined(NotificationOptions model, PushoverSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Pushover, NotificationType.RequestDeclined, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {NotificationType.RequestDeclined} is disabled for {NotificationAgent.Pushover}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
await Send(notification, settings);
await Run(model, settings, NotificationType.RequestDeclined);
}
protected override async Task RequestApproved(NotificationOptions model, PushoverSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Pushover, NotificationType.RequestApproved, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {NotificationType.RequestApproved} is disabled for {NotificationAgent.Pushover}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
await Send(notification, settings);
await Run(model, settings, NotificationType.RequestApproved);
}
protected override async Task AvailableRequest(NotificationOptions model, PushoverSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Pushover, NotificationType.RequestAvailable, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {NotificationType.RequestAvailable} is disabled for {NotificationAgent.Pushover}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
await Send(notification, settings);
await Run(model, settings, NotificationType.RequestAvailable);
}
protected override async Task Send(NotificationMessage model, PushoverSettings settings)
@ -195,5 +105,21 @@ namespace Ombi.Notifications.Agents
};
await Send(notification, settings);
}
private async Task Run(NotificationOptions model, PushoverSettings settings, NotificationType type)
{
var parsed = await LoadTemplate(NotificationAgent.Pushover, type, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {type} is disabled for {NotificationAgent.Pushover}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
await Send(notification, settings);
}
}
}

@ -54,138 +54,42 @@ namespace Ombi.Notifications.Agents
protected override async Task NewRequest(NotificationOptions model, SlackNotificationSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Slack, NotificationType.NewRequest, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {NotificationType.NewRequest} is disabled for {NotificationAgent.Slack}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
notification.Other.Add("image", parsed.Image);
await Send(notification, settings);
await Run(model, settings, NotificationType.NewRequest);
}
protected override async Task NewIssue(NotificationOptions model, SlackNotificationSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Slack, NotificationType.Issue, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {NotificationType.Issue} is disabled for {NotificationAgent.Slack}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
notification.Other.Add("image", parsed.Image);
await Send(notification, settings);
await Run(model, settings, NotificationType.Issue);
}
protected override async Task IssueComment(NotificationOptions model, SlackNotificationSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Slack, NotificationType.IssueComment, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {NotificationType.IssueComment} is disabled for {NotificationAgent.Slack}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
notification.Other.Add("image", parsed.Image);
await Send(notification, settings);
await Run(model, settings, NotificationType.IssueComment);
}
protected override async Task IssueResolved(NotificationOptions model, SlackNotificationSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Slack, NotificationType.IssueResolved, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {NotificationType.IssueResolved} is disabled for {NotificationAgent.Slack}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
notification.Other.Add("image", parsed.Image);
await Send(notification, settings);
await Run(model, settings, NotificationType.IssueResolved);
}
protected override async Task AddedToRequestQueue(NotificationOptions model, SlackNotificationSettings settings)
{
var user = string.Empty;
var title = string.Empty;
if (model.RequestType == RequestType.Movie)
{
user = MovieRequest.RequestedUser.UserAlias;
title = MovieRequest.Title;
}
else
{
user = TvRequest.RequestedUser.UserAlias;
title = TvRequest.ParentRequest.Title;
}
var message = $"Hello! The user '{user}' has requested {title} but it could not be added. This has been added into the requests queue and will keep retrying";
var notification = new NotificationMessage
{
Message = message
};
await Send(notification, settings);
await Run(model, settings, NotificationType.ItemAddedToFaultQueue);
}
protected override async Task RequestDeclined(NotificationOptions model, SlackNotificationSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Slack, NotificationType.RequestDeclined, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {NotificationType.RequestDeclined} is disabled for {NotificationAgent.Slack}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
notification.Other.Add("image", parsed.Image);
await Send(notification, settings);
await Run(model, settings, NotificationType.RequestAvailable);
}
protected override async Task RequestApproved(NotificationOptions model, SlackNotificationSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Slack, NotificationType.RequestApproved, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {NotificationType.RequestApproved} is disabled for {NotificationAgent.Slack}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
notification.Other.Add("image", parsed.Image);
await Send(notification, settings);
await Run(model, settings, NotificationType.RequestApproved);
}
protected override async Task AvailableRequest(NotificationOptions model, SlackNotificationSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Slack, NotificationType.RequestAvailable, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {NotificationType.RequestAvailable} is disabled for {NotificationAgent.Slack}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
notification.Other.Add("image", parsed.Image);
await Send(notification, settings);
await Run(model, settings, NotificationType.RequestAvailable);
}
protected override async Task Send(NotificationMessage model, SlackNotificationSettings settings)
@ -218,5 +122,21 @@ namespace Ombi.Notifications.Agents
};
await Send(notification, settings);
}
private async Task Run(NotificationOptions model, SlackNotificationSettings settings, NotificationType type)
{
var parsed = await LoadTemplate(NotificationAgent.Slack, type, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {type} is disabled for {NotificationAgent.Slack}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
notification.Other.Add("image", parsed.Image);
await Send(notification, settings);
}
}
}

@ -41,134 +41,42 @@ namespace Ombi.Notifications.Agents
protected override async Task NewRequest(NotificationOptions model, TelegramSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Telegram, NotificationType.NewRequest, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {NotificationType.NewRequest} is disabled for {NotificationAgent.Telegram}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
await Send(notification, settings);
await Run(model, settings, NotificationType.NewRequest);
}
protected override async Task NewIssue(NotificationOptions model, TelegramSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Telegram, NotificationType.Issue, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {NotificationType.Issue} is disabled for {NotificationAgent.Telegram}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
await Send(notification, settings);
await Run(model, settings, NotificationType.Issue);
}
protected override async Task IssueComment(NotificationOptions model, TelegramSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Telegram, NotificationType.IssueComment, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {NotificationType.IssueComment} is disabled for {NotificationAgent.Telegram}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
await Send(notification, settings);
await Run(model, settings, NotificationType.IssueComment);
}
protected override async Task IssueResolved(NotificationOptions model, TelegramSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Telegram, NotificationType.IssueResolved, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {NotificationType.IssueResolved} is disabled for {NotificationAgent.Telegram}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
await Send(notification, settings);
await Run(model, settings, NotificationType.IssueResolved);
}
protected override async Task AddedToRequestQueue(NotificationOptions model, TelegramSettings settings)
{
var user = string.Empty;
var title = string.Empty;
var image = string.Empty;
if (model.RequestType == RequestType.Movie)
{
user = MovieRequest.RequestedUser.UserAlias;
title = MovieRequest.Title;
image = MovieRequest.PosterPath;
}
else
{
user = TvRequest.RequestedUser.UserAlias;
title = TvRequest.ParentRequest.Title;
image = TvRequest.ParentRequest.PosterPath;
}
var message = $"Hello! The user '{user}' has requested {title} but it could not be added. This has been added into the requests queue and will keep retrying";
var notification = new NotificationMessage
{
Message = message
};
await Send(notification, settings);
await Run(model, settings, NotificationType.ItemAddedToFaultQueue);
}
protected override async Task RequestDeclined(NotificationOptions model, TelegramSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Telegram, NotificationType.RequestDeclined, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {NotificationType.RequestDeclined} is disabled for {NotificationAgent.Telegram}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
await Send(notification, settings);
await Run(model, settings, NotificationType.RequestDeclined);
}
protected override async Task RequestApproved(NotificationOptions model, TelegramSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Telegram, NotificationType.RequestApproved, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {NotificationType.RequestApproved} is disabled for {NotificationAgent.Telegram}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message ?? string.Empty,
};
await Send(notification, settings);
await Run(model, settings, NotificationType.RequestApproved);
}
protected override async Task AvailableRequest(NotificationOptions model, TelegramSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Telegram, NotificationType.RequestAvailable, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {NotificationType.RequestAvailable} is disabled for {NotificationAgent.Telegram}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
await Send(notification, settings);
await Run(model, settings, NotificationType.RequestAvailable);
}
protected override async Task Send(NotificationMessage model, TelegramSettings settings)
@ -192,5 +100,20 @@ namespace Ombi.Notifications.Agents
};
await Send(notification, settings);
}
private async Task Run(NotificationOptions model, TelegramSettings settings, NotificationType type)
{
var parsed = await LoadTemplate(NotificationAgent.Telegram, type, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {type} is disabled for {NotificationAgent.Telegram}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
await Send(notification, settings);
}
}
}

@ -26,8 +26,6 @@ namespace Ombi.Notifications
MovieRepository = movie;
TvRepository = tv;
CustomizationSettings = customization;
Settings.ClearCache();
CustomizationSettings.ClearCache();
RequestSubscription = sub;
_log = log;
AlbumRepository = album;
@ -55,14 +53,12 @@ namespace Ombi.Notifications
public async Task NotifyAsync(NotificationOptions model)
{
Settings.ClearCache();
var configuration = await GetConfiguration();
await NotifyAsync(model, configuration);
}
public async Task NotifyAsync(NotificationOptions model, Settings.Settings.Models.Settings settings)
{
Settings.ClearCache();
if (settings == null) await NotifyAsync(model);
var notificationSettings = (T)settings;

@ -5,6 +5,7 @@ using MailKit.Net.Smtp;
using Microsoft.Extensions.Logging;
using MimeKit;
using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Notifications.Models;
using Ombi.Notifications.Templates;
using Ombi.Settings.Settings.Models;

@ -28,7 +28,6 @@ namespace Ombi.Schedule.Jobs.Emby
_repo = repo;
_episodeSync = epSync;
_metadata = metadata;
_settings.ClearCache();
}
private readonly ILogger<EmbyContentSync> _logger;
@ -87,7 +86,7 @@ namespace Ombi.Schedule.Jobs.Emby
await _api.GetCollection(movie.Id, server.ApiKey, server.AdministratorId, server.FullUri);
foreach (var item in movieInfo.Items)
{
await ProcessMovies(item, mediaToAdd);
await ProcessMovies(item, mediaToAdd, server);
}
processed++;
@ -96,7 +95,7 @@ namespace Ombi.Schedule.Jobs.Emby
{
processed++;
// Regular movie
await ProcessMovies(movie, mediaToAdd);
await ProcessMovies(movie, mediaToAdd, server);
}
}
@ -138,7 +137,7 @@ namespace Ombi.Schedule.Jobs.Emby
Title = tvShow.Name,
Type = EmbyMediaType.Series,
EmbyId = tvShow.Id,
Url = EmbyHelper.GetEmbyMediaUrl(tvShow.Id),
Url = EmbyHelper.GetEmbyMediaUrl(tvShow.Id, server.ServerHostname),
AddedAt = DateTime.UtcNow
});
}
@ -164,7 +163,7 @@ namespace Ombi.Schedule.Jobs.Emby
await _repo.AddRange(mediaToAdd);
}
private async Task ProcessMovies(EmbyMovie movieInfo, ICollection<EmbyContent> content)
private async Task ProcessMovies(EmbyMovie movieInfo, ICollection<EmbyContent> content, EmbyServers server)
{
// Check if it exists
var existingMovie = await _repo.GetByEmbyId(movieInfo.Id);
@ -179,7 +178,7 @@ namespace Ombi.Schedule.Jobs.Emby
Title = movieInfo.Name,
Type = EmbyMediaType.Movie,
EmbyId = movieInfo.Id,
Url = EmbyHelper.GetEmbyMediaUrl(movieInfo.Id),
Url = EmbyHelper.GetEmbyMediaUrl(movieInfo.Id, server.ServerHostname),
AddedAt = DateTime.UtcNow,
});
}

@ -49,7 +49,6 @@ namespace Ombi.Schedule.Jobs.Emby
_settings = s;
_repo = repo;
_avaliabilityChecker = checker;
_settings.ClearCache();
}
private readonly ISettingsService<EmbySettings> _settings;

@ -50,8 +50,6 @@ namespace Ombi.Schedule.Jobs.Emby
_log = log;
_embySettings = embySettings;
_userManagementSettings = ums;
_userManagementSettings.ClearCache();
_embySettings.ClearCache();
}
private readonly IEmbyApi _api;

@ -1,19 +1,16 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Hangfire;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.Extensions.Logging;
using Ombi.Api.Lidarr;
using Ombi.Api.Radarr;
using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Settings.Settings.Models.External;
using Ombi.Store.Context;
using Ombi.Store.Entities;
using Serilog;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace Ombi.Schedule.Jobs.Lidarr
@ -29,7 +26,6 @@ namespace Ombi.Schedule.Jobs.Lidarr
_ctx = ctx;
_job = job;
_availability = availability;
_lidarrSettings.ClearCache();
}
private readonly ISettingsService<LidarrSettings> _lidarrSettings;

@ -1,19 +1,16 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Hangfire;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.Extensions.Logging;
using Ombi.Api.Lidarr;
using Ombi.Api.Radarr;
using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Settings.Settings.Models.External;
using Ombi.Store.Context;
using Ombi.Store.Entities;
using Serilog;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace Ombi.Schedule.Jobs.Lidarr
@ -29,7 +26,6 @@ namespace Ombi.Schedule.Jobs.Lidarr
_ctx = ctx;
_job = background;
_albumSync = album;
_lidarrSettings.ClearCache();
}
private readonly ISettingsService<LidarrSettings> _lidarrSettings;

@ -28,9 +28,9 @@ namespace Ombi.Schedule.Jobs.Ombi
return;
}
var now = DateTime.Now.AddDays(-settings.DaysAfterResolvedToDelete).Date;
var deletionDate = DateTime.Now.AddDays(settings.DaysAfterResolvedToDelete).Date;
var resolved = _issuesRepository.GetAll().Where(x => x.Status == IssueStatus.Resolved);
var toDelete = resolved.Where(x => x.ResovledDate.HasValue && x.ResovledDate.Value.Date <= now);
var toDelete = resolved.Where(x => x.ResovledDate.HasValue && x.ResovledDate.Value.Date >= deletionDate);
foreach (var d in toDelete)
{

@ -10,28 +10,23 @@ using Ombi.Schedule.Jobs.Emby;
using Ombi.Schedule.Jobs.Plex.Interfaces;
using Ombi.Store.Repository;
namespace Ombi.Schedule.Jobs.Plex
namespace Ombi.Schedule.Jobs.Ombi
{
public class MediaDatabaseRefresh : IMediaDatabaseRefresh
{
public MediaDatabaseRefresh(ISettingsService<PlexSettings> s, ILogger<MediaDatabaseRefresh> log, IPlexApi plexApi,
IPlexContentRepository plexRepo, IPlexContentSync c, IEmbyContentRepository embyRepo, IEmbyContentSync embySync)
public MediaDatabaseRefresh(ISettingsService<PlexSettings> s, ILogger<MediaDatabaseRefresh> log,
IPlexContentRepository plexRepo, IEmbyContentRepository embyRepo, IEmbyContentSync embySync)
{
_settings = s;
_log = log;
_api = plexApi;
_plexRepo = plexRepo;
_plexContentSync = c;
_embyRepo = embyRepo;
_embyContentSync = embySync;
_settings.ClearCache();
}
private readonly ISettingsService<PlexSettings> _settings;
private readonly ILogger _log;
private readonly IPlexApi _api;
private readonly IPlexContentRepository _plexRepo;
private readonly IPlexContentSync _plexContentSync;
private readonly IEmbyContentRepository _embyRepo;
private readonly IEmbyContentSync _embyContentSync;

@ -16,6 +16,7 @@ using Ombi.Api.TheMovieDb;
using Ombi.Api.TheMovieDb.Models;
using Ombi.Api.TvMaze;
using Ombi.Core.Settings;
using Ombi.Core.Settings.Models.External;
using Ombi.Helpers;
using Ombi.Notifications;
using Ombi.Notifications.Models;
@ -36,7 +37,7 @@ namespace Ombi.Schedule.Jobs.Ombi
ISettingsService<EmailNotificationSettings> emailSettings, INotificationTemplatesRepository templateRepo,
UserManager<OmbiUser> um, ISettingsService<NewsletterSettings> newsletter, ILogger<NewsletterJob> log,
ILidarrApi lidarrApi, IRepository<LidarrAlbumCache> albumCache, ISettingsService<LidarrSettings> lidarrSettings,
ISettingsService<OmbiSettings> ombiSettings)
ISettingsService<OmbiSettings> ombiSettings, ISettingsService<PlexSettings> plexSettings, ISettingsService<EmbySettings> embySettings)
{
_plex = plex;
_emby = emby;
@ -49,16 +50,13 @@ namespace Ombi.Schedule.Jobs.Ombi
_emailSettings = emailSettings;
_newsletterSettings = newsletter;
_userManager = um;
_emailSettings.ClearCache();
_customizationSettings.ClearCache();
_newsletterSettings.ClearCache();
_log = log;
_lidarrApi = lidarrApi;
_lidarrAlbumRepository = albumCache;
_lidarrSettings = lidarrSettings;
_ombiSettings = ombiSettings;
_ombiSettings.ClearCache();
_lidarrSettings.ClearCache();
_plexSettings = plexSettings;
_embySettings = embySettings;
}
private readonly IPlexContentRepository _plex;
@ -77,6 +75,8 @@ namespace Ombi.Schedule.Jobs.Ombi
private readonly ILidarrApi _lidarrApi;
private readonly IRepository<LidarrAlbumCache> _lidarrAlbumRepository;
private readonly ISettingsService<LidarrSettings> _lidarrSettings;
private readonly ISettingsService<PlexSettings> _plexSettings;
private readonly ISettingsService<EmbySettings> _embySettings;
public async Task Start(NewsletterSettings settings, bool test)
{
@ -132,6 +132,8 @@ namespace Ombi.Schedule.Jobs.Ombi
_log.LogInformation("Plex Episodes to send: {0}", plexEpisodesToSend.Count());
_log.LogInformation("Emby Episodes to send: {0}", embyEpisodesToSend.Count());
var plexSettings = await _plexSettings.GetSettingsAsync();
var embySettings = await _embySettings.GetSettingsAsync();
var body = string.Empty;
if (test)
{
@ -140,11 +142,11 @@ namespace Ombi.Schedule.Jobs.Ombi
var plext = _plex.GetAllEpisodes().Include(x => x.Series).OrderByDescending(x => x.Series.AddedAt).Take(10).ToHashSet();
var embyt = _emby.GetAllEpisodes().Include(x => x.Series).OrderByDescending(x => x.AddedAt).Take(10).ToHashSet();
var lidarr = lidarrContent.OrderByDescending(x => x.AddedAt).Take(10).ToHashSet();
body = await BuildHtml(plexm, embym, plext, embyt, lidarr, settings);
body = await BuildHtml(plexm, embym, plext, embyt, lidarr, settings, embySettings, plexSettings);
}
else
{
body = await BuildHtml(plexContentMoviesToSend, embyContentMoviesToSend, plexEpisodesToSend, embyEpisodesToSend, lidarrContentAlbumsToSend, settings);
body = await BuildHtml(plexContentMoviesToSend, embyContentMoviesToSend, plexEpisodesToSend, embyEpisodesToSend, lidarrContentAlbumsToSend, settings, embySettings, plexSettings);
if (body.IsNullOrEmpty())
{
return;
@ -333,7 +335,8 @@ namespace Ombi.Schedule.Jobs.Ombi
}
private async Task<string> BuildHtml(IQueryable<PlexServerContent> plexContentToSend, IQueryable<EmbyContent> embyContentToSend,
HashSet<PlexEpisode> plexEpisodes, HashSet<EmbyEpisode> embyEp, HashSet<LidarrAlbumCache> albums, NewsletterSettings settings)
HashSet<PlexEpisode> plexEpisodes, HashSet<EmbyEpisode> embyEp, HashSet<LidarrAlbumCache> albums, NewsletterSettings settings, EmbySettings embySettings,
PlexSettings plexSettings)
{
var ombiSettings = await _ombiSettings.GetSettingsAsync();
var sb = new StringBuilder();
@ -349,8 +352,16 @@ namespace Ombi.Schedule.Jobs.Ombi
sb.Append("<td style=\"font-family: 'Open Sans', Helvetica, Arial, sans-serif; font-size: 14px; vertical-align: top; \">");
sb.Append("<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\" style=\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; \">");
sb.Append("<tr>");
await ProcessPlexMovies(plexMovies, sb, ombiSettings.DefaultLanguageCode);
await ProcessEmbyMovies(embyMovies, sb, ombiSettings.DefaultLanguageCode);
if (plexSettings.Enable)
{
await ProcessPlexMovies(plexMovies, sb, ombiSettings.DefaultLanguageCode);
}
if (embySettings.Enable)
{
await ProcessEmbyMovies(embyMovies, sb, ombiSettings.DefaultLanguageCode);
}
sb.Append("</tr>");
sb.Append("</table>");
sb.Append("</td>");
@ -367,8 +378,16 @@ namespace Ombi.Schedule.Jobs.Ombi
sb.Append("<td style=\"font-family: 'Open Sans', Helvetica, Arial, sans-serif; font-size: 14px; vertical-align: top; \">");
sb.Append("<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\" style=\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; \">");
sb.Append("<tr>");
await ProcessPlexTv(plexEpisodes, sb);
await ProcessEmbyTv(embyEp, sb);
if (plexSettings.Enable)
{
await ProcessPlexTv(plexEpisodes, sb);
}
if (embySettings.Enable)
{
await ProcessEmbyTv(embyEp, sb);
}
sb.Append("</tr>");
sb.Append("</table>");
sb.Append("</td>");

@ -5,18 +5,13 @@ using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Hangfire;
using Hangfire.Console;
using Hangfire.Server;
using Microsoft.Extensions.Logging;
using Ombi.Api.Service;
using Ombi.Api.Service.Models;
using Ombi.Core.Processor;
using Ombi.Core.Settings;
using Ombi.Helpers;
@ -40,7 +35,6 @@ namespace Ombi.Schedule.Jobs.Ombi
Settings = s;
_processProvider = proc;
_appConfig = appConfig;
Settings.ClearCache();
}
private ILogger<OmbiAutomaticUpdater> Logger { get; }

@ -4,6 +4,7 @@ using System.Linq;
using System.Threading.Tasks;
using Hangfire;
using Microsoft.Extensions.Logging;
using Ombi.Api.Emby;
using Ombi.Api.TheMovieDb;
using Ombi.Api.TheMovieDb.Models;
using Ombi.Api.TvMaze;
@ -21,7 +22,8 @@ namespace Ombi.Schedule.Jobs.Ombi
{
public RefreshMetadata(IPlexContentRepository plexRepo, IEmbyContentRepository embyRepo,
ILogger<RefreshMetadata> log, ITvMazeApi tvApi, ISettingsService<PlexSettings> plexSettings,
IMovieDbApi movieApi, ISettingsService<EmbySettings> embySettings, IPlexAvailabilityChecker plexAvailability, IEmbyAvaliabilityChecker embyAvaliability)
IMovieDbApi movieApi, ISettingsService<EmbySettings> embySettings, IPlexAvailabilityChecker plexAvailability, IEmbyAvaliabilityChecker embyAvaliability,
IEmbyApi embyApi)
{
_plexRepo = plexRepo;
_embyRepo = embyRepo;
@ -32,6 +34,7 @@ namespace Ombi.Schedule.Jobs.Ombi
_embySettings = embySettings;
_plexAvailabilityChecker = plexAvailability;
_embyAvaliabilityChecker = embyAvaliability;
_embyApi = embyApi;
}
private readonly IPlexContentRepository _plexRepo;
@ -43,6 +46,7 @@ namespace Ombi.Schedule.Jobs.Ombi
private readonly ITvMazeApi _tvApi;
private readonly ISettingsService<PlexSettings> _plexSettings;
private readonly ISettingsService<EmbySettings> _embySettings;
private readonly IEmbyApi _embyApi;
public async Task Start()
{
@ -54,11 +58,11 @@ namespace Ombi.Schedule.Jobs.Ombi
{
await StartPlex();
}
var embySettings = await _embySettings.GetSettingsAsync();
if (embySettings.Enable)
{
await StartEmby();
await StartEmby(embySettings);
}
}
catch (Exception e)
@ -123,9 +127,9 @@ namespace Ombi.Schedule.Jobs.Ombi
await StartPlexTv(allTv);
}
private async Task StartEmby()
private async Task StartEmby(EmbySettings s)
{
await StartEmbyMovies();
await StartEmbyMovies(s);
await StartEmbyTv();
}
@ -158,7 +162,7 @@ namespace Ombi.Schedule.Jobs.Ombi
_plexRepo.UpdateWithoutSave(show);
}
tvCount++;
if (tvCount >= 20)
if (tvCount >= 75)
{
await _plexRepo.SaveChangesAsync();
tvCount = 0;
@ -198,7 +202,7 @@ namespace Ombi.Schedule.Jobs.Ombi
_embyRepo.UpdateWithoutSave(show);
}
tvCount++;
if (tvCount >= 20)
if (tvCount >= 75)
{
await _embyRepo.SaveChangesAsync();
tvCount = 0;
@ -229,7 +233,7 @@ namespace Ombi.Schedule.Jobs.Ombi
_plexRepo.UpdateWithoutSave(movie);
}
movieCount++;
if (movieCount >= 20)
if (movieCount >= 75)
{
await _plexRepo.SaveChangesAsync();
movieCount = 0;
@ -239,31 +243,56 @@ namespace Ombi.Schedule.Jobs.Ombi
await _plexRepo.SaveChangesAsync();
}
private async Task StartEmbyMovies()
private async Task StartEmbyMovies(EmbySettings settings)
{
var allMovies = _embyRepo.GetAll().Where(x =>
x.Type == EmbyMediaType.Movie && (!x.TheMovieDbId.HasValue() || !x.ImdbId.HasValue()));
int movieCount = 0;
foreach (var movie in allMovies)
{
var hasImdb = movie.ImdbId.HasValue();
var hasTheMovieDb = movie.TheMovieDbId.HasValue();
movie.ImdbId.HasValue();
movie.TheMovieDbId.HasValue();
// Movies don't really use TheTvDb
if (!hasImdb)
// Check if it even has 1 ID
if (!movie.HasImdb && !movie.HasTheMovieDb)
{
var imdbId = await GetImdbId(hasTheMovieDb, false, movie.Title, movie.TheMovieDbId, string.Empty);
// Ok this sucks,
// The only think I can think that has happened is that we scanned Emby before Emby has got the metadata
// So let's recheck emby to see if they have got the metadata now
_log.LogInformation($"Movie {movie.Title} does not have a ImdbId or TheMovieDbId, so rechecking emby");
foreach (var server in settings.Servers)
{
_log.LogInformation($"Checking server {server.Name} for upto date metadata");
var movieInfo = await _embyApi.GetMovieInformation(movie.EmbyId, server.ApiKey, server.AdministratorId,
server.FullUri);
if (movieInfo.ProviderIds?.Imdb.HasValue() ?? false)
{
movie.ImdbId = movieInfo.ProviderIds.Imdb;
}
if (movieInfo.ProviderIds?.Tmdb.HasValue() ?? false)
{
movie.TheMovieDbId = movieInfo.ProviderIds.Tmdb;
}
}
}
if (!movie.HasImdb)
{
var imdbId = await GetImdbId(movie.HasTheMovieDb, false, movie.Title, movie.TheMovieDbId, string.Empty);
movie.ImdbId = imdbId;
_embyRepo.UpdateWithoutSave(movie);
}
if (!hasTheMovieDb)
if (!movie.HasTheMovieDb)
{
var id = await GetTheMovieDbId(false, hasImdb, string.Empty, movie.ImdbId, movie.Title, true);
var id = await GetTheMovieDbId(false, movie.HasImdb, string.Empty, movie.ImdbId, movie.Title, true);
movie.TheMovieDbId = id;
_embyRepo.UpdateWithoutSave(movie);
}
movieCount++;
if (movieCount >= 20)
if (movieCount >= 75)
{
await _embyRepo.SaveChangesAsync();
movieCount = 0;

@ -20,8 +20,6 @@ namespace Ombi.Schedule.Jobs.Ombi
_email = provider;
_templates = template;
_customizationSettings = c;
email.ClearCache();
_customizationSettings.ClearCache();
}
private readonly ISettingsService<EmailNotificationSettings> _emailSettings;

@ -57,7 +57,6 @@ namespace Ombi.Schedule.Jobs.Plex
EpisodeSync = epsiodeSync;
Metadata = metadataRefresh;
Checker = checker;
plex.ClearCache();
}
private ISettingsService<PlexSettings> Plex { get; }

@ -26,7 +26,6 @@ namespace Ombi.Schedule.Jobs.Plex
_api = plexApi;
_repo = repo;
_availabilityChecker = a;
_settings.ClearCache();
}
private readonly ISettingsService<PlexSettings> _settings;

@ -24,8 +24,6 @@ namespace Ombi.Schedule.Jobs.Plex
_log = log;
_plexSettings = plexSettings;
_userManagementSettings = ums;
_userManagementSettings.ClearCache();
_plexSettings.ClearCache();
}
private readonly IPlexApi _api;

@ -22,7 +22,6 @@ namespace Ombi.Schedule.Jobs.Radarr
RadarrApi = radarrApi;
Logger = log;
_ctx = ctx;
RadarrSettings.ClearCache();
}
private ISettingsService<RadarrSettings> RadarrSettings { get; }

@ -22,7 +22,6 @@ namespace Ombi.Schedule.Jobs.SickRage
_api = api;
_log = l;
_ctx = ctx;
_settings.ClearCache();
}
private readonly ISettingsService<SickRageSettings> _settings;

@ -25,7 +25,6 @@ namespace Ombi.Schedule.Jobs.Sonarr
_api = api;
_log = l;
_ctx = ctx;
_settings.ClearCache();
}
private readonly ISettingsService<SonarrSettings> _settings;

@ -14,6 +14,7 @@ namespace Ombi.Core.Settings.Models.External
public string Name { get; set; }
public string ApiKey { get; set; }
public string AdministratorId { get; set; }
public string ServerHostname { get; set; }
public bool EnableEpisodeSearching { get; set; }
}
}

@ -61,7 +61,7 @@ namespace Ombi.Settings.Settings
var model = obj;
return model;
}, DateTime.Now.AddHours(2));
}, DateTime.Now.AddHours(5));
}
public bool SaveSettings(T model)

@ -87,43 +87,6 @@ namespace Ombi.Store.Context
public void Seed()
{
// VACUUM;
Database.ExecuteSqlCommand("VACUUM;");
// Make sure we have the roles
var newsletterRole = Roles.Where(x => x.Name == OmbiRoles.ReceivesNewsletter);
if (!newsletterRole.Any())
{
Roles.Add(new IdentityRole(OmbiRoles.ReceivesNewsletter)
{
NormalizedName = OmbiRoles.ReceivesNewsletter.ToUpper()
});
SaveChanges();
}
var requestMusicRole = Roles.Where(x => x.Name == OmbiRoles.RequestMusic);
if (!requestMusicRole.Any())
{
Roles.Add(new IdentityRole(OmbiRoles.RequestMusic)
{
NormalizedName = OmbiRoles.RequestMusic.ToUpper()
});
Roles.Add(new IdentityRole(OmbiRoles.AutoApproveMusic)
{
NormalizedName = OmbiRoles.AutoApproveMusic.ToUpper()
});
SaveChanges();
}
var manageOwnRequestsRole = Roles.Where(x => x.Name == OmbiRoles.ManageOwnRequests);
if (!manageOwnRequestsRole.Any())
{
Roles.Add(new IdentityRole(OmbiRoles.ManageOwnRequests)
{
NormalizedName = OmbiRoles.ManageOwnRequests.ToUpper()
});
SaveChanges();
}
// Make sure we have the API User
var apiUserExists = Users.Any(x => x.UserName.Equals("Api", StringComparison.CurrentCultureIgnoreCase));
if (!apiUserExists)
@ -209,7 +172,15 @@ namespace Ombi.Store.Context
};
break;
case NotificationType.ItemAddedToFaultQueue:
continue;
notificationToAdd = new NotificationTemplates
{
NotificationType = notificationType,
Message = "Hello! The user '{UserName}' has requested {Title} but it could not be added. This has been added into the requests queue and will keep retrying",
Subject = "Item Added To Retry Queue",
Agent = agent,
Enabled = true,
};
break;
case NotificationType.WelcomeEmail:
notificationToAdd = new NotificationTemplates
{

@ -66,5 +66,10 @@ namespace Ombi.Store.Context
SaveChanges();
}
~SettingsContext()
{
}
}
}

@ -34,7 +34,7 @@ namespace Ombi.Store.Entities
public bool IsEmbyConnect => UserType == UserType.EmbyUser && EmbyConnectUserId.HasValue();
[NotMapped]
public string UserAlias => string.IsNullOrEmpty(Alias) ? UserName : Alias;
public virtual string UserAlias => string.IsNullOrEmpty(Alias) ? UserName : Alias;
[NotMapped]
public bool EmailLogin { get; set; }

@ -17,6 +17,7 @@ namespace Ombi.Store.Entities.Requests
public DateTime MarkedAsDenied { get; set; }
public string DeniedReason { get; set; }
public RequestType RequestType { get; set; }
public string RequestedByAlias { get; set; }
[ForeignKey(nameof(RequestedUserId))]
public OmbiUser RequestedUser { get; set; }

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Globalization;
using Ombi.Store.Entities;
using Ombi.Store.Entities.Requests;
@ -27,9 +28,10 @@ namespace Ombi.Store.Repository.Requests
public bool Approved { get; set; }
public bool Requested { get; set; }
public int SeasonId { get; set; }
[ForeignKey(nameof(SeasonId))]
public SeasonRequests Season { get; set; }
[NotMapped] public string AirDateDisplay => AirDate == DateTime.MinValue ? "Unknown" : AirDate.ToString(CultureInfo.InvariantCulture);
}
}

@ -17,10 +17,6 @@ namespace Ombi.Store.Entities.Requests
public DateTime ReleaseDate { get; set; }
public string Status { get; set; }
/// <summary>
/// This is so we can correctly send the right amount of seasons to Sonarr
/// </summary>
[NotMapped]
public int TotalSeasons { get; set; }
public List<ChildRequests> ChildRequests { get; set; }

File diff suppressed because it is too large Load Diff

@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace Ombi.Store.Migrations
{
public partial class RequestedByAlias : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "RequestedByAlias",
table: "MovieRequests",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "RequestedByAlias",
table: "ChildRequests",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "RequestedByAlias",
table: "AlbumRequests",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "RequestedByAlias",
table: "MovieRequests");
migrationBuilder.DropColumn(
name: "RequestedByAlias",
table: "ChildRequests");
migrationBuilder.DropColumn(
name: "RequestedByAlias",
table: "AlbumRequests");
}
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,32 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Ombi.Helpers;
namespace Ombi.Store.Migrations
{
public partial class Roles : Migration
{
protected override void Up(MigrationBuilder mb)
{
// Make sure we have the roles
InsertRole(mb, OmbiRoles.ReceivesNewsletter);
InsertRole(mb, OmbiRoles.RequestMusic);
InsertRole(mb, OmbiRoles.AutoApproveMusic);
InsertRole(mb, OmbiRoles.ManageOwnRequests);
InsertRole(mb, OmbiRoles.EditCustomPage);
}
private void InsertRole(MigrationBuilder mb, string role)
{
mb.Sql($@"
INSERT INTO AspnetRoles(Id, ConcurrencyStamp, Name, NormalizedName)
SELECT '{Guid.NewGuid().ToString()}','{Guid.NewGuid().ToString()}','{role}', '{role.ToUpper()}'
WHERE NOT EXISTS(SELECT 1 FROM AspnetRoles WHERE Name = '{role}');");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,23 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace Ombi.Store.Migrations
{
public partial class TvRequestsTotalSeasons : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "TotalSeasons",
table: "TvRequests",
nullable: false,
defaultValue: 0);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "TotalSeasons",
table: "TvRequests");
}
}
}

@ -14,7 +14,7 @@ namespace Ombi.Store.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.2.0-rtm-35687");
.HasAnnotation("ProductVersion", "2.2.1-servicing-10028");
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
@ -583,6 +583,8 @@ namespace Ombi.Store.Migrations
b.Property<int>("RequestType");
b.Property<string>("RequestedByAlias");
b.Property<DateTime>("RequestedDate");
b.Property<string>("RequestedUserId");
@ -621,6 +623,8 @@ namespace Ombi.Store.Migrations
b.Property<int>("RequestType");
b.Property<string>("RequestedByAlias");
b.Property<DateTime>("RequestedDate");
b.Property<string>("RequestedUserId");
@ -749,6 +753,8 @@ namespace Ombi.Store.Migrations
b.Property<int>("RequestType");
b.Property<string>("RequestedByAlias");
b.Property<DateTime>("RequestedDate");
b.Property<string>("RequestedUserId");
@ -813,6 +819,8 @@ namespace Ombi.Store.Migrations
b.Property<string>("Title");
b.Property<int>("TotalSeasons");
b.Property<int>("TvDbId");
b.HasKey("Id");

@ -1,71 +1,71 @@
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Moq;
using Ombi.Api.Emby;
using Ombi.Api.Plex;
using Ombi.Core.Authentication;
using Ombi.Core.Settings;
using Ombi.Core.Settings.Models.External;
using Ombi.Models.Identity;
using Ombi.Store.Context;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
//using System;
//using Microsoft.AspNetCore.Builder;
//using Microsoft.AspNetCore.Hosting;
//using Microsoft.AspNetCore.Http;
//using Microsoft.AspNetCore.Http.Features.Authentication;
//using Microsoft.AspNetCore.Identity;
//using Microsoft.Extensions.DependencyInjection;
//using Microsoft.Extensions.Options;
//using Moq;
//using Ombi.Api.Emby;
//using Ombi.Api.Plex;
//using Ombi.Core.Authentication;
//using Ombi.Core.Settings;
//using Ombi.Core.Settings.Models.External;
//using Ombi.Models.Identity;
//using Ombi.Store.Context;
//using Ombi.Store.Entities;
//using Ombi.Store.Repository;
namespace Ombi.Tests
{
public class TestStartup
{
public IServiceProvider ConfigureServices(IServiceCollection services)
{
var _plexApi = new Mock<IPlexApi>();
var _embyApi = new Mock<IEmbyApi>();
var _tokenSettings = new Mock<IOptions<TokenAuthentication>>();
var _embySettings = new Mock<ISettingsService<EmbySettings>>();
var _plexSettings = new Mock<ISettingsService<PlexSettings>>();
var audit = new Mock<IAuditRepository>();
var tokenRepo = new Mock<ITokenRepository>();
//namespace Ombi.Tests
//{
// public class TestStartup
// {
// public IServiceProvider ConfigureServices(IServiceCollection services)
// {
// var _plexApi = new Mock<IPlexApi>();
// var _embyApi = new Mock<IEmbyApi>();
// var _tokenSettings = new Mock<IOptions<TokenAuthentication>>();
// var _embySettings = new Mock<ISettingsService<EmbySettings>>();
// var _plexSettings = new Mock<ISettingsService<PlexSettings>>();
// var audit = new Mock<IAuditRepository>();
// var tokenRepo = new Mock<ITokenRepository>();
services.AddEntityFrameworkInMemoryDatabase()
.AddDbContext<OmbiContext>();
services.AddIdentity<OmbiUser, IdentityRole>()
.AddEntityFrameworkStores<OmbiContext>().AddUserManager<OmbiUserManager>();
// services.AddEntityFrameworkInMemoryDatabase()
// .AddDbContext<OmbiContext>();
// services.AddIdentity<OmbiUser, IdentityRole>()
// .AddEntityFrameworkStores<OmbiContext>().AddUserManager<OmbiUserManager>();
services.AddTransient(x => _plexApi.Object);
services.AddTransient(x => _embyApi.Object);
services.AddTransient(x => _tokenSettings.Object);
services.AddTransient(x => _embySettings.Object);
services.AddTransient(x => _plexSettings.Object);
services.AddTransient(x => audit.Object);
services.AddTransient(x => tokenRepo.Object);
// Taken from https://github.com/aspnet/MusicStore/blob/dev/test/MusicStore.Test/ManageControllerTest.cs (and modified)
var context = new DefaultHttpContext();
context.Features.Set<IHttpAuthenticationFeature>(new HttpAuthenticationFeature());
services.AddSingleton<IHttpContextAccessor>(h => new HttpContextAccessor { HttpContext = context });
// services.AddTransient(x => _plexApi.Object);
// services.AddTransient(x => _embyApi.Object);
// services.AddTransient(x => _tokenSettings.Object);
// services.AddTransient(x => _embySettings.Object);
// services.AddTransient(x => _plexSettings.Object);
// services.AddTransient(x => audit.Object);
// services.AddTransient(x => tokenRepo.Object);
// // Taken from https://github.com/aspnet/MusicStore/blob/dev/test/MusicStore.Test/ManageControllerTest.cs (and modified)
// var context = new DefaultHttpContext();
// context.Features.Set<IHttpAuthenticationFeature>(new HttpAuthenticationFeature());
// services.AddSingleton<IHttpContextAccessor>(h => new HttpContextAccessor { HttpContext = context });
services.Configure<IdentityOptions>(options =>
{
options.Password.RequireDigit = false;
options.Password.RequiredLength = 1;
options.Password.RequireLowercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.User.AllowedUserNameCharacters = string.Empty;
});
// services.Configure<IdentityOptions>(options =>
// {
// options.Password.RequireDigit = false;
// options.Password.RequiredLength = 1;
// options.Password.RequireLowercase = false;
// options.Password.RequireNonAlphanumeric = false;
// options.Password.RequireUppercase = false;
// options.User.AllowedUserNameCharacters = string.Empty;
// });
return services.BuildServiceProvider();
// return services.BuildServiceProvider();
}
// }
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// public void Configure(IApplicationBuilder app, IHostingEnvironment env)
// {
}
}
}
// }
// }
//}

@ -1,60 +1,60 @@
using System.Net.Http;
using System.Threading.Tasks;
using AutoMapper;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Moq;
using NUnit.Framework;
using Ombi.Api.Emby;
using Ombi.Api.Plex;
using Ombi.Controllers;
using Ombi.Core.Authentication;
using Ombi.Core.Settings;
using Ombi.Core.Settings.Models.External;
using Ombi.Models.Identity;
using Ombi.Notifications;
using Ombi.Schedule.Jobs.Ombi;
using Ombi.Settings.Settings.Models;
using Ombi.Settings.Settings.Models.Notifications;
using Ombi.Store.Context;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.TestHost;
using Newtonsoft.Json;
using Ombi.Models;
//using System.Net.Http;
//using System.Threading.Tasks;
//using AutoMapper;
//using Microsoft.AspNetCore.Hosting;
//using Microsoft.AspNetCore.Http;
//using Microsoft.AspNetCore.Http.Features.Authentication;
//using Microsoft.AspNetCore.Identity;
//using Microsoft.Extensions.DependencyInjection;
//using Microsoft.Extensions.Options;
//using Moq;
//using NUnit.Framework;
//using Ombi.Api.Emby;
//using Ombi.Api.Plex;
//using Ombi.Controllers;
//using Ombi.Core.Authentication;
//using Ombi.Core.Settings;
//using Ombi.Core.Settings.Models.External;
//using Ombi.Models.Identity;
//using Ombi.Notifications;
//using Ombi.Schedule.Jobs.Ombi;
//using Ombi.Settings.Settings.Models;
//using Ombi.Settings.Settings.Models.Notifications;
//using Ombi.Store.Context;
//using Ombi.Store.Entities;
//using Ombi.Store.Repository;
//using Microsoft.AspNetCore.Hosting.Server;
//using Microsoft.AspNetCore.TestHost;
//using Newtonsoft.Json;
//using Ombi.Models;
namespace Ombi.Tests
{
[TestFixture]
[Ignore("TODO")]
public class TokenControllerTests
{
[SetUp]
public void Setup()
{
_testServer = new TestServer(new WebHostBuilder()
.UseStartup<TestStartup>());
_client = _testServer.CreateClient();
}
//namespace Ombi.Tests
//{
// [TestFixture]
// [Ignore("TODO")]
// public class TokenControllerTests
// {
// [SetUp]
// public void Setup()
// {
// _testServer = new TestServer(new WebHostBuilder()
// .UseStartup<TestStartup>());
// _client = _testServer.CreateClient();
// }
private TestServer _testServer;
private HttpClient _client;
// private TestServer _testServer;
// private HttpClient _client;
[Test]
public async Task GetToken_FromValid_LocalUser()
{
var model = new UserAuthModel
{
Password = "a",
Username = "a"
};
HttpResponseMessage response = await _client.PostAsync("/api/v1/token", new StringContent(JsonConvert.SerializeObject(model)) );
}
}
}
// [Test]
// public async Task GetToken_FromValid_LocalUser()
// {
// var model = new UserAuthModel
// {
// Password = "a",
// Username = "a"
// };
// HttpResponseMessage response = await _client.PostAsync("/api/v1/token", new StringContent(JsonConvert.SerializeObject(model)) );
// }
// }
//}

@ -19,5 +19,7 @@ namespace Ombi.Api.TheMovieDb
Task<FindResult> Find(string externalId, ExternalSource source);
Task<TvExternals> GetTvExternals(int theMovieDbId);
Task<TvInfo> GetTVInfo(string themoviedbid);
Task<TheMovieDbContainer<ActorResult>> SearchByActor(string searchTerm, string langCode);
Task<ActorCredits> GetActorMovieCredits(int actorId, string langCode);
}
}

@ -0,0 +1,51 @@
namespace Ombi.Api.TheMovieDb.Models
{
public class ActorCredits
{
public Cast[] cast { get; set; }
public Crew[] crew { get; set; }
public int id { get; set; }
}
public class Cast
{
public string character { get; set; }
public string credit_id { get; set; }
public string poster_path { get; set; }
public int id { get; set; }
public bool video { get; set; }
public int vote_count { get; set; }
public bool adult { get; set; }
public string backdrop_path { get; set; }
public int?[] genre_ids { get; set; }
public string original_language { get; set; }
public string original_title { get; set; }
public float popularity { get; set; }
public string title { get; set; }
public float vote_average { get; set; }
public string overview { get; set; }
public string release_date { get; set; }
}
public class Crew
{
public int id { get; set; }
public string department { get; set; }
public string original_language { get; set; }
public string original_title { get; set; }
public string job { get; set; }
public string overview { get; set; }
public int vote_count { get; set; }
public bool video { get; set; }
public string release_date { get; set; }
public float vote_average { get; set; }
public string title { get; set; }
public float popularity { get; set; }
public int?[] genre_ids { get; set; }
public string backdrop_path { get; set; }
public bool adult { get; set; }
public string poster_path { get; set; }
public string credit_id { get; set; }
}
}

@ -0,0 +1,33 @@
namespace Ombi.Api.TheMovieDb.Models
{
public class ActorResult
{
public float popularity { get; set; }
public int id { get; set; }
public string profile_path { get; set; }
public string name { get; set; }
public Known_For[] known_for { get; set; }
public bool adult { get; set; }
}
public class Known_For
{
public float vote_average { get; set; }
public int vote_count { get; set; }
public int id { get; set; }
public bool video { get; set; }
public string media_type { get; set; }
public string title { get; set; }
public float popularity { get; set; }
public string poster_path { get; set; }
public string original_language { get; set; }
public string original_title { get; set; }
public int[] genre_ids { get; set; }
public string backdrop_path { get; set; }
public bool adult { get; set; }
public string overview { get; set; }
public string release_date { get; set; }
}
}

@ -43,6 +43,27 @@ namespace Ombi.Api.TheMovieDb
return await Api.Request<FindResult>(request);
}
public async Task<TheMovieDbContainer<ActorResult>> SearchByActor(string searchTerm, string langCode)
{
var request = new Request($"search/person", BaseUri, HttpMethod.Get);
request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken);
request.FullUri = request.FullUri.AddQueryParameter("query", searchTerm);
request.FullUri = request.FullUri.AddQueryParameter("language", langCode);
var result = await Api.Request<TheMovieDbContainer<ActorResult>>(request);
return result;
}
public async Task<ActorCredits> GetActorMovieCredits(int actorId, string langCode)
{
var request = new Request($"person/{actorId}/movie_credits", BaseUri, HttpMethod.Get);
request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken);
request.FullUri = request.FullUri.AddQueryParameter("language", langCode);
var result = await Api.Request<ActorCredits>(request);
return result;
}
public async Task<List<TvSearchResult>> SearchTv(string searchTerm)
{
var request = new Request($"search/tv", BaseUri, HttpMethod.Get);

@ -8,6 +8,7 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Ombi.Core.Authentication;
using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Settings.Settings.Models;
namespace Ombi
@ -98,6 +99,10 @@ namespace Ombi
if (context.Request.Headers.Keys.Contains("UserName", StringComparer.InvariantCultureIgnoreCase))
{
var username = context.Request.Headers["UserName"].FirstOrDefault();
if (username.IsNullOrEmpty())
{
UseApiUser(context);
}
var um = context.RequestServices.GetService<OmbiUserManager>();
var user = await um.Users.FirstOrDefaultAsync(x =>
x.UserName.Equals(username, StringComparison.InvariantCultureIgnoreCase));
@ -114,13 +119,18 @@ namespace Ombi
}
else
{
var identity = new GenericIdentity("API");
var principal = new GenericPrincipal(identity, new[] { "Admin", "ApiUser" });
context.User = principal;
UseApiUser(context);
}
await next.Invoke(context);
}
}
private void UseApiUser(HttpContext context)
{
var identity = new GenericIdentity("API");
var principal = new GenericPrincipal(identity, new[] { "Admin", "ApiUser" });
context.User = principal;
}
}
}

@ -4,7 +4,7 @@ import { NavigationStart, Router } from "@angular/router";
import { TranslateService } from "@ngx-translate/core";
import { AuthService } from "./auth/auth.service";
import { ILocalUser } from "./auth/IUserLogin";
import { IdentityService, NotificationService } from "./services";
import { CustomPageService, IdentityService, NotificationService } from "./services";
import { JobService, SettingsService } from "./services";
import { ICustomizationSettings, ICustomPage } from "./interfaces";
@ -35,7 +35,8 @@ export class AppComponent implements OnInit {
private readonly jobService: JobService,
public readonly translate: TranslateService,
private readonly identityService: IdentityService,
private readonly platformLocation: PlatformLocation) {
private readonly platformLocation: PlatformLocation,
private readonly customPageService: CustomPageService) {
const base = this.platformLocation.getBaseHrefFromDOM();
if (base.length > 1) {
@ -57,7 +58,7 @@ export class AppComponent implements OnInit {
this.settingsService.getCustomization().subscribe(x => {
this.customizationSettings = x;
if(this.customizationSettings.useCustomPage) {
this.settingsService.getCustomPage().subscribe(c => {
this.customPageService.getCustomPage().subscribe(c => {
this.customPageSettings = c;
if(!this.customPageSettings.title) {
this.customPageSettings.title = "Custom Page";

@ -39,7 +39,7 @@ import { ImageService } from "./services";
import { LandingPageService } from "./services";
import { NotificationService } from "./services";
import { SettingsService } from "./services";
import { IssuesService, JobService, PlexTvService, StatusService } from "./services";
import { CustomPageService, IssuesService, JobService, PlexTvService, StatusService } from "./services";
const routes: Routes = [
{ path: "*", component: PageNotFoundComponent },
@ -144,6 +144,7 @@ export function JwtTokenGetter() {
JobService,
IssuesService,
PlexTvService,
CustomPageService,
],
bootstrap: [AppComponent],
})

@ -2,7 +2,7 @@
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
import { DomSanitizer } from "@angular/platform-browser";
import { AuthService } from "../auth/auth.service";
import { NotificationService, SettingsService } from "../services";
import { CustomPageService, NotificationService } from "../services";
@Component({
templateUrl: "./custompage.component.html",
@ -14,7 +14,7 @@ export class CustomPageComponent implements OnInit {
public isEditing: boolean;
public isAdmin: boolean;
constructor(private auth: AuthService, private settings: SettingsService, private fb: FormBuilder,
constructor(private auth: AuthService, private settings: CustomPageService, private fb: FormBuilder,
private notificationService: NotificationService,
private sanitizer: DomSanitizer) {
}
@ -29,7 +29,7 @@ export class CustomPageComponent implements OnInit {
fontAwesomeIcon: [x.fontAwesomeIcon, [Validators.required]],
});
});
this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser");
this.isAdmin = this.auth.hasRole("EditCustomPage");
}
public onSubmit() {

@ -87,6 +87,7 @@ export interface IBaseRequest {
requestedUser: IUser;
canApprove: boolean;
title: string;
requestedByAlias: string;
}
export interface ITvRequests {
@ -145,6 +146,7 @@ export interface IEpisodesRequests {
episodeNumber: number;
title: string;
airDate: Date;
airDateDisplay: string;
url: string;
available: boolean;
requested: boolean;

@ -41,6 +41,7 @@ export interface IEmbyServer extends IExternalSettings {
apiKey: string;
administratorId: string;
enableEpisodeSearching: boolean;
serverHostname: string;
}
export interface IPlexSettings extends ISettings {

@ -161,7 +161,8 @@ export class LoginComponent implements OnDestroy, OnInit {
}
}, err => {
this.notify.error(err.statusText);
console.log(err);
this.notify.error(err.body);
this.router.navigate(["login"]);
});

@ -1,36 +1,39 @@
<div class="form-group">
<div class="input-group">
<input type="text" id="search" class="form-control form-control-custom searchwidth" placeholder="Search" (keyup)="search($event)">
<input type="text" id="search" class="form-control form-control-custom searchwidth" placeholder="Search"
(keyup)="search($event)">
<span class="input-group-btn">
<button id="filterBtn" class="btn btn-sm btn-info-outline" (click)="filterDisplay = !filterDisplay">
<i class="fa fa-filter"></i> {{ 'Requests.Filter' | translate }}
</button>
<button class="btn btn-sm btn-primary-outline dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true"
aria-expanded="true">
<button class="btn btn-sm btn-primary-outline dropdown-toggle" type="button" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="true">
<i class="fa fa-sort"></i> {{ 'Requests.Sort' | translate }}
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu2">
<li>
<a (click)="setOrder(OrderType.RequestedDateAsc, $event)">{{ 'Requests.SortRequestDateAsc' | translate }}
<a (click)="setOrder(OrderType.RequestedDateAsc, $event)">{{ 'Requests.SortRequestDateAsc' |
translate }}
</a>
<a class="active" (click)="setOrder(OrderType.RequestedDateDesc, $event)">{{ 'Requests.SortRequestDateDesc' | translate }}
<a class="active" (click)="setOrder(OrderType.RequestedDateDesc, $event)">{{
'Requests.SortRequestDateDesc' | translate }}
</a>
<a (click)="setOrder(OrderType.TitleAsc, $event)">{{ 'Requests.SortTitleAsc' | translate}}
</a>
<a (click)="setOrder(OrderType.TitleDesc, $event)">{{ 'Requests.SortTitleDesc' | translate}}
</a>
<a (click)="setOrder(OrderType.StatusAsc, $event)">{{ 'Requests.SortStatusAsc' | translate}}
</a>
<a (click)="setOrder(OrderType.StatusDesc, $event)">{{ 'Requests.SortStatusDesc' | translate}}
</a>
</li>
@ -58,16 +61,20 @@
<div class="col-sm-5 small-padding">
<div>
<a href="http://www.imdb.com/title/{{request.imdbId}}/" target="_blank">
<h4 class="request-title">{{request.title}} ({{request.releaseDate | amLocal | amDateFormat: 'YYYY'}})</h4>
<h4 class="request-title">{{request.title}} ({{request.releaseDate | amLocal | amDateFormat:
'YYYY'}})</h4>
</a>
</div>
<br />
<div class="request-info">
<div class="request-by">
<span>{{ 'Requests.RequestedBy' | translate }} </span>
<span *ngIf="!isAdmin">{{request.requestedUser.userName}}</span>
<span *ngIf="isAdmin && request.requestedUser.alias">{{request.requestedUser.alias}}</span>
<span *ngIf="isAdmin && !request.requestedUser.alias">{{request.requestedUser.userName}}</span>
<span *ngIf="request.requestedByAlias">{{request.requestedByAlias}}</span>
<span *ngIf="!request.requestedByAlias">
<span *ngIf="!isAdmin">{{request.requestedUser.userName}}</span>
<span *ngIf="isAdmin && request.requestedUser.alias">{{request.requestedUser.alias}}</span>
<span *ngIf="isAdmin && !request.requestedUser.alias">{{request.requestedUser.userName}}</span>
</span>
</div>
<div class="request-status">
<span>{{ 'Requests.Status' | translate }} </span>
@ -77,13 +84,11 @@
<div class="requested-status">
<span>{{ 'Requests.RequestStatus' | translate }} </span>
<span *ngIf="request.available" class="label label-success" id="availableLabel" [translate]="'Common.Available'"></span>
<span *ngIf="request.approved && !request.available" id="processingRequestLabel" class="label label-info" [translate]="'Common.ProcessingRequest'"></span>
<span *ngIf="request.approved && !request.available" id="processingRequestLabel" class="label label-info"
[translate]="'Common.ProcessingRequest'"></span>
<span *ngIf="request.denied" class="label label-danger" id="requestDeclinedLabel" [translate]="'Common.RequestDenied'"></span>
<span *ngIf="request.deniedReason" title="{{request.deniedReason}}">
<i class="fa fa-info-circle"></i>
</span>
<span *ngIf="!request.approved && !request.availble && !request.denied" id="pendingApprovalLabel" class="label label-warning"
[translate]="'Common.PendingApproval'"></span>
<span *ngIf="!request.approved && !request.availble && !request.denied" id="pendingApprovalLabel"
class="label label-warning" [translate]="'Common.PendingApproval'"></span>
</div>
<div *ngIf="request.denied" id="requestDenied">
@ -93,16 +98,21 @@
</div>
<div id="releaseDate">{{ 'Requests.TheatricalRelease' | translate: {date: request.releaseDate | amLocal | amDateFormat: 'LL'} }}</div>
<div *ngIf="request.digitalReleaseDate" id="digitalReleaseDate">{{ 'Requests.DigitalRelease' | translate: {date: request.digitalReleaseDate | amLocal | amDateFormat: 'LL'} }}</div>
<div id="requestedDate">{{ 'Requests.RequestDate' | translate }} {{request.requestedDate | amLocal | amDateFormat: 'LL'}}</div>
<div id="releaseDate">{{ 'Requests.TheatricalRelease' | translate: {date: request.releaseDate |
amLocal | amDateFormat: 'LL'} }}</div>
<div *ngIf="request.digitalReleaseDate" id="digitalReleaseDate">{{ 'Requests.DigitalRelease' |
translate: {date: request.digitalReleaseDate | amLocal | amDateFormat: 'LL'} }}</div>
<div id="requestedDate">{{ 'Requests.RequestDate' | translate }} {{request.requestedDate | amLocal
| amDateFormat: 'LL'}}</div>
<br />
</div>
<div *ngIf="isAdmin">
<div *ngIf="request.qualityOverrideTitle" class="quality-override">{{ 'Requests.QualityOverride' | translate }}
<div *ngIf="request.qualityOverrideTitle" class="quality-override">{{ 'Requests.QualityOverride' |
translate }}
<span>{{request.qualityOverrideTitle}} </span>
</div>
<div *ngIf="request.rootPathOverrideTitle" class="root-override">{{ 'Requests.RootFolderOverride' | translate }}
<div *ngIf="request.rootPathOverrideTitle" class="root-override">{{ 'Requests.RootFolderOverride' |
translate }}
<span>{{request.rootPathOverrideTitle}} </span>
</div>
</div>
@ -112,10 +122,12 @@
<div class="row">
<div class="col-md-2 col-md-push-6">
<a *ngIf="request.showSubscribe && !request.subscribed" style="color:white" (click)="subscribe(request)" pTooltip="Subscribe for notifications">
<a *ngIf="request.showSubscribe && !request.subscribed" style="color:white" (click)="subscribe(request)"
pTooltip="Subscribe for notifications">
<i class="fa fa-rss"></i>
</a>
<a *ngIf="request.showSubscribe && request.subscribed" style="color:red" (click)="unSubscribe(request)" pTooltip="Unsubscribe notification">
<a *ngIf="request.showSubscribe && request.subscribed" style="color:red" (click)="unSubscribe(request)"
pTooltip="Unsubscribe notification">
<i class="fa fa-rss"></i>
</a>
</div>
@ -123,7 +135,8 @@
<div *ngIf="isAdmin">
<div *ngIf="!request.approved" id="approveBtn">
<form>
<button (click)="approve(request)" style="text-align: right" class="btn btn-sm btn-success-outline approve" type="submit">
<button (click)="approve(request)" style="text-align: right" class="btn btn-sm btn-success-outline approve"
type="submit">
<i class="fa fa-plus"></i> {{ 'Common.Approve' | translate }}
</button>
</form>
@ -133,7 +146,8 @@
<button type="button" class="btn btn-sm btn-warning-outline">
<i class="fa fa-plus"></i> {{ 'Requests.ChangeRootFolder' | translate }}
</button>
<button type="button" class="btn btn-warning-outline dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<button type="button" class="btn btn-warning-outline dropdown-toggle" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
@ -149,7 +163,8 @@
<button type="button" class="btn btn-sm btn-warning-outline">
<i class="fa fa-plus"></i> {{ 'Requests.ChangeQualityProfile' | translate }}
</button>
<button type="button" class="btn btn-warning-outline dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<button type="button" class="btn btn-warning-outline dropdown-toggle" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
@ -166,15 +181,15 @@
</button>
</div>
</div>
<form id="markBtnGroup">
<button id="unavailableBtn" *ngIf="request.available" (click)="changeAvailability(request, false)" style="text-align: right"
value="false" class="btn btn-sm btn-info-outline change">
<button id="unavailableBtn" *ngIf="request.available" (click)="changeAvailability(request, false)"
style="text-align: right" value="false" class="btn btn-sm btn-info-outline change">
<i class="fa fa-minus"></i> {{ 'Requests.MarkUnavailable' | translate }}
</button>
<button id="availableBtn" *ngIf="!request.available" (click)="changeAvailability(request, true)" style="text-align: right"
value="true" class="btn btn-sm btn-success-outline change">
<button id="availableBtn" *ngIf="!request.available" (click)="changeAvailability(request, true)"
style="text-align: right" value="true" class="btn btn-sm btn-success-outline change">
<i class="fa fa-plus"></i> {{ 'Requests.MarkAvailable' | translate }}
</button>
</form>
@ -190,8 +205,8 @@
</form>
</div>
<div class="dropdown" *ngIf="issueCategories && issuesEnabled" id="issuesBtn">
<button class="btn btn-sm btn-primary-outline dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true"
aria-expanded="true">
<button class="btn btn-sm btn-primary-outline dropdown-toggle" type="button" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="true">
<i class="fa fa-plus"></i> {{ 'Requests.ReportIssue' | translate }}
<span class="caret"></span>
</button>
@ -204,8 +219,8 @@
</div>
</div>
<br/>
<br/>
<br />
<br />
@ -216,11 +231,11 @@
</div>
<p-dialog *ngIf="requestToDeny" header="Deny Request '{{requestToDeny.title}}''" [(visible)]="denyDisplay" [draggable]="false">
<span>Please enter a rejection reason, the user will be notified of this:</span>
<textarea [(ngModel)]="rejectionReason" class="form-control-custom form-control"></textarea>
<span>Please enter a rejection reason, the user will be notified of this:</span>
<textarea [(ngModel)]="rejectionReason" class="form-control-custom form-control"></textarea>
<p-footer>
<button type="button" (click)="denyRequest();" label="Reject" class="btn btn-success">Deny</button>
<button type="button"(click)="denyDisplay=false" label="Close" class="btn btn-danger">Close</button>
<button type="button" (click)="denyDisplay=false" label="Close" class="btn btn-danger">Close</button>
</p-footer>
</p-dialog>

@ -135,7 +135,7 @@ export class MovieRequestsComponent implements OnInit {
public deny(request: IMovieRequests) {
this.requestToDeny = request;
this.denyDisplay = true;
}
}
public denyRequest() {
this.requestService.denyMovie({ id: this.requestToDeny.id, reason: this.rejectionReason })
@ -144,6 +144,10 @@ export class MovieRequestsComponent implements OnInit {
if (x.result) {
this.notificationService.success(
`Request for ${this.requestToDeny.title} has been denied successfully`);
const index = this.movieRequests.indexOf(this.requestToDeny, 0);
if (index > -1) {
this.movieRequests[index].denied = true;
}
} else {
this.notificationService.warning("Request Denied", x.message ? x.message : x.errorMessage);
this.requestToDeny.denied = false;

@ -1,36 +1,39 @@
<div class="form-group">
<div class="input-group">
<input type="text" id="search" class="form-control form-control-custom searchwidth" placeholder="Search" (keyup)="search($event)">
<input type="text" id="search" class="form-control form-control-custom searchwidth" placeholder="Search"
(keyup)="search($event)">
<span class="input-group-btn">
<button id="filterBtn" class="btn btn-sm btn-info-outline" (click)="filterDisplay = !filterDisplay">
<i class="fa fa-filter"></i> {{ 'Requests.Filter' | translate }}
</button>
<button class="btn btn-sm btn-primary-outline dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true"
aria-expanded="true">
<button class="btn btn-sm btn-primary-outline dropdown-toggle" type="button" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="true">
<i class="fa fa-sort"></i> {{ 'Requests.Sort' | translate }}
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu2">
<li>
<a (click)="setOrder(OrderType.RequestedDateAsc, $event)">{{ 'Requests.SortRequestDateAsc' | translate }}
<a (click)="setOrder(OrderType.RequestedDateAsc, $event)">{{ 'Requests.SortRequestDateAsc' |
translate }}
</a>
<a class="active" (click)="setOrder(OrderType.RequestedDateDesc, $event)">{{ 'Requests.SortRequestDateDesc' | translate }}
<a class="active" (click)="setOrder(OrderType.RequestedDateDesc, $event)">{{
'Requests.SortRequestDateDesc' | translate }}
</a>
<a (click)="setOrder(OrderType.TitleAsc, $event)">{{ 'Requests.SortTitleAsc' | translate}}
</a>
<a (click)="setOrder(OrderType.TitleDesc, $event)">{{ 'Requests.SortTitleDesc' | translate}}
</a>
<a (click)="setOrder(OrderType.StatusAsc, $event)">{{ 'Requests.SortStatusAsc' | translate}}
</a>
<a (click)="setOrder(OrderType.StatusDesc, $event)">{{ 'Requests.SortStatusDesc' | translate}}
</a>
</li>
@ -45,7 +48,7 @@
<div class="col-md-12">
<div *ngFor="let request of albumRequests" class="col-md-4">
<div *ngFor="let request of albumRequests" class="col-md-4">
<div class="row">
<div class="album-bg backdrop" [style.background-image]="request.background"></div>
<div class="album-tint" style="background-image: linear-gradient(to bottom, rgba(0,0,0,0.6) 0%,rgba(0,0,0,0.6) 100%);"></div>
@ -53,15 +56,15 @@
<div class="col-sm-12 small-padding">
<img *ngIf="request.disk" class="img-responsive poster album-cover" src="{{request.disk}}" alt="poster">
</div>
<div class="col-sm-12 small-padding">
<div>
<h4>
<a href="" target="_blank">
{{request.title | truncate: 36}}
{{request.title | truncate: 36}}
</a>
</h4>
<h5>
<a href="">
@ -69,25 +72,29 @@
</a>
</h5>
</div>
<div class="request-info">
<div class="request-by">
<span>{{ 'Requests.RequestedBy' | translate }} </span>
<span *ngIf="!isAdmin">{{request.requestedUser.userName}}</span>
<span *ngIf="isAdmin && request.requestedUser.alias">{{request.requestedUser.alias}}</span>
<span *ngIf="isAdmin && !request.requestedUser.alias">{{request.requestedUser.userName}}</span>
<span *ngIf="request.requestedByAlias">{{request.requestedByAlias}}</span>
<span *ngIf="!request.requestedByAlias">
<span *ngIf="!isAdmin">{{request.requestedUser.userName}}</span>
<span *ngIf="isAdmin && request.requestedUser.alias">{{request.requestedUser.alias}}</span>
<span *ngIf="isAdmin && !request.requestedUser.alias">{{request.requestedUser.userName}}</span>
</span>
</div>
<div class="requested-status">
<span>{{ 'Requests.RequestStatus' | translate }} </span>
<span *ngIf="request.available" class="label label-success" id="availableLabel" [translate]="'Common.Available'"></span>
<span *ngIf="request.approved && !request.available" id="processingRequestLabel" class="label label-info" [translate]="'Common.ProcessingRequest'"></span>
<span *ngIf="request.approved && !request.available" id="processingRequestLabel" class="label label-info"
[translate]="'Common.ProcessingRequest'"></span>
<span *ngIf="request.denied" class="label label-danger" id="requestDeclinedLabel" [translate]="'Common.RequestDenied'"></span>
<span *ngIf="request.deniedReason" title="{{request.deniedReason}}">
<i class="fa fa-info-circle"></i>
</span>
<span *ngIf="!request.approved && !request.availble && !request.denied" id="pendingApprovalLabel" class="label label-warning"
[translate]="'Common.PendingApproval'"></span>
<span *ngIf="!request.approved && !request.availble && !request.denied" id="pendingApprovalLabel"
class="label label-warning" [translate]="'Common.PendingApproval'"></span>
</div>
<div *ngIf="request.denied" id="requestDenied">
@ -97,8 +104,10 @@
</div>
<div id="releaseDate">{{ 'Requests.ReleaseDate' | translate: {date: request.releaseDate | amLocal | amDateFormat: 'LL'} }}</div>
<div id="requestedDate">{{ 'Requests.RequestDate' | translate }} {{request.requestedDate | amLocal | amDateFormat: 'LL'}}</div>
<div id="releaseDate">{{ 'Requests.ReleaseDate' | translate: {date: request.releaseDate | amLocal |
amDateFormat: 'LL'} }}</div>
<div id="requestedDate">{{ 'Requests.RequestDate' | translate }} {{request.requestedDate | amLocal
| amDateFormat: 'LL'}}</div>
<br />
</div>
<!-- <div *ngIf="isAdmin">
@ -125,8 +134,9 @@
</div> -->
<div *ngIf="isAdmin">
<div *ngIf="!request.approved" id="approveBtn">
<form class="col-md-6">
<button (click)="approve(request)" style="text-align: right" class="btn btn-sm btn-success-outline approve" type="submit">
<form class="col-md-6">
<button (click)="approve(request)" style="text-align: right" class="btn btn-sm btn-success-outline approve"
type="submit">
<i class="fa fa-plus"></i> {{ 'Common.Approve' | translate }}
</button>
</form>
@ -169,15 +179,15 @@
</button>
</div>
</div>
<form id="markBtnGroup">
<button id="unavailableBtn" *ngIf="request.available" (click)="changeAvailability(request, false)" style="text-align: right"
value="false" class="btn btn-sm btn-info-outline change">
<button id="unavailableBtn" *ngIf="request.available" (click)="changeAvailability(request, false)"
style="text-align: right" value="false" class="btn btn-sm btn-info-outline change">
<i class="fa fa-minus"></i> {{ 'Requests.MarkUnavailable' | translate }}
</button>
<button id="availableBtn" *ngIf="!request.available" (click)="changeAvailability(request, true)" style="text-align: right"
value="true" class="btn btn-sm btn-success-outline change">
<button id="availableBtn" *ngIf="!request.available" (click)="changeAvailability(request, true)"
style="text-align: right" value="true" class="btn btn-sm btn-success-outline change">
<i class="fa fa-plus"></i> {{ 'Requests.MarkAvailable' | translate }}
</button>
</form>
@ -193,8 +203,8 @@
</form>
</div>
<div class="dropdown" *ngIf="issueCategories && issuesEnabled" id="issuesBtn">
<button class="btn btn-sm btn-primary-outline dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true"
aria-expanded="true">
<button class="btn btn-sm btn-primary-outline dropdown-toggle" type="button" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="true">
<i class="fa fa-plus"></i> {{ 'Requests.ReportIssue' | translate }}
<span class="caret"></span>
</button>
@ -207,8 +217,8 @@
</div>
</div>
<br/>
<br/>
<br />
<br />
@ -272,8 +282,8 @@
<p-dialog *ngIf="requestToDeny" header="Deny Request '{{requestToDeny.title}}''" [(visible)]="denyDisplay" [draggable]="false">
<span>Please enter a rejection reason, the user will be notified of this:</span>
<textarea [(ngModel)]="rejectionReason" class="form-control-custom form-control"></textarea>
<p-footer>
<button type="button" (click)="denyRequest();" label="Reject" class="btn btn-success">Deny</button>
<button type="button"(click)="denyDisplay=false" label="Close" class="btn btn-danger">Close</button>
</p-footer>
</p-dialog>
<p-footer>
<button type="button" (click)="denyRequest();" label="Reject" class="btn btn-success">Deny</button>
<button type="button" (click)="denyDisplay=false" label="Close" class="btn btn-danger">Close</button>
</p-footer>
</p-dialog>

@ -23,17 +23,11 @@ export class RemainingRequestsComponent implements OnInit {
}
public ngOnInit() {
const self = this;
this.update();
this.quotaRefreshEvents.subscribe(() => {
this.quotaRefreshEvents.subscribe(() => {
this.update();
});
setInterval(() => {
self.update();
}, 60000);
}
public update(): void {
@ -43,7 +37,6 @@ export class RemainingRequestsComponent implements OnInit {
this.calculateTime();
}
});
if (this.movie) {
this.requestService.getRemainingMovieRequests().subscribe(callback);
}

@ -5,31 +5,43 @@
<div class="col-md-2">
<span [translate]="'Requests.RequestedBy'"></span>
<span *ngIf="!isAdmin">{{child.requestedUser.userName}}</span>
<span *ngIf="isAdmin && child.requestedUser.alias">{{child.requestedUser.alias}}</span>
<span *ngIf="isAdmin && !child.requestedUser.alias">{{child.requestedUser.userName}}</span>
<span *ngIf="child.requestedByAlias">{{child.requestedByAlias}}</span>
<span *ngIf="!child.requestedByAlias">
<span *ngIf="!isAdmin">{{child.requestedUser.userName}}</span>
<span *ngIf="isAdmin && child.requestedUser.alias">{{child.requestedUser.alias}}</span>
<span *ngIf="isAdmin && !child.requestedUser.alias">{{child.requestedUser.userName}}</span>
</span>
</div>
<div class="col-md-1 col-md-push-9">
<button id="subscribeBtn" *ngIf="child.showSubscribe && !child.subscribed" (click)="subscribe(child)" class="btn btn-sm btn-primary-outline" pTooltip="Subscribe for notifications" type="submit"><i class="fa fa-rss"></i> Subscribe</button>
<button id="subscribeBtn" *ngIf="child.showSubscribe && child.subscribed" (click)="unSubscribe(child)" class="btn btn-sm btn-danger-outline" pTooltip="UnSubscribe for notifications" type="submit"><i class="fa fa-rss"></i> UnSubscribe</button>
<div *ngIf="isAdmin">
<button id="approveBtn" *ngIf="child.canApprove && !child.approved" (click)="approve(child)" class="btn btn-sm btn-success-outline" type="submit"><i class="fa fa-plus"></i> {{ 'Common.Approve' | translate }}</button>
<button id="unavailableBtn" *ngIf="child.available" (click)="changeAvailability(child, false)" style="text-align: right" value="false" class="btn btn-sm btn-info-outline change"><i class="fa fa-minus"></i> {{ 'Requests.MarkUnavailable' | translate }}</button>
<button id="availableBtn" *ngIf="!child.available" (click)="changeAvailability(child, true)" style="text-align: right" value="true" class="btn btn-sm btn-success-outline change"><i class="fa fa-plus"></i> {{ 'Requests.MarkAvailable' | translate }}</button>
<button id="subscribeBtn" *ngIf="child.showSubscribe && !child.subscribed" (click)="subscribe(child)"
class="btn btn-sm btn-primary-outline" pTooltip="Subscribe for notifications" type="submit"><i
class="fa fa-rss"></i> Subscribe</button>
<button id="subscribeBtn" *ngIf="child.showSubscribe && child.subscribed" (click)="unSubscribe(child)"
class="btn btn-sm btn-danger-outline" pTooltip="UnSubscribe for notifications" type="submit"><i
class="fa fa-rss"></i> UnSubscribe</button>
<div *ngIf="isAdmin">
<button id="approveBtn" *ngIf="child.canApprove && !child.approved" (click)="approve(child)" class="btn btn-sm btn-success-outline"
type="submit"><i class="fa fa-plus"></i> {{ 'Common.Approve' | translate }}</button>
<button id="unavailableBtn" *ngIf="child.available" (click)="changeAvailability(child, false)"
style="text-align: right" value="false" class="btn btn-sm btn-info-outline change"><i class="fa fa-minus"></i>
{{ 'Requests.MarkUnavailable' | translate }}</button>
<button id="availableBtn" *ngIf="!child.available" (click)="changeAvailability(child, true)" style="text-align: right"
value="true" class="btn btn-sm btn-success-outline change"><i class="fa fa-plus"></i> {{
'Requests.MarkAvailable' | translate }}</button>
<button id="denyBtn" *ngIf="!child.denied" type="button" (click)="deny(child)" class="btn btn-sm btn-danger-outline deny">
<i class="fa fa-times"></i> {{ 'Requests.Deny' | translate }}</button>
</div>
<div *ngIf="isAdmin || isRequestUser(child)">
<button id="removeBtn" type="button" (click)="removeRequest(child)" class="btn btn-sm btn-danger-outline deny"><i
class="fa fa-times"></i> {{ 'Requests.Remove' | translate }}</button>
</div>
<div *ngIf="isAdmin || isRequestUser(child)">
<button id="removeBtn" type="button" (click)="removeRequest(child)" class="btn btn-sm btn-danger-outline deny"><i class="fa fa-times"></i> {{ 'Requests.Remove' | translate }}</button>
</div>
</div>
</div>
<div class="col-md-12">
@ -77,15 +89,19 @@
{{ep.airDate | amLocal | amDateFormat: 'L' }}
</td>
<td>
<span *ngIf="child.denied" class="label label-danger" id="deniedLabel" [translate]="'Common.Denied'">
<i style="color:red;" class="fa fa-check" pTooltip="{{child.deniedReason}}"></i>
<span *ngIf="child.denied" class="label label-danger" id="deniedLabel"
[translate]="'Common.Denied'">
<i style="color:red;" class="fa fa-check" pTooltip="{{child.deniedReason}}"></i>
</span>
<span *ngIf="!child.denied && ep.available" class="label label-success" id="availableLabel" [translate]="'Common.Available'"></span>
<span *ngIf="!child.denied &&ep.approved && !ep.available" class="label label-info" id="processingRequestLabel" [translate]="'Common.ProcessingRequest'"></span>
<span *ngIf="!child.denied && ep.available" class="label label-success" id="availableLabel"
[translate]="'Common.Available'"></span>
<span *ngIf="!child.denied &&ep.approved && !ep.available" class="label label-info"
id="processingRequestLabel" [translate]="'Common.ProcessingRequest'"></span>
<div *ngIf="!child.denied && !ep.approved">
<div *ngIf="!ep.available"><span class="label label-warning" id="pendingApprovalLabel" [translate]="'Common.PendingApproval'"></span></div>
<div *ngIf="!ep.available"><span class="label label-warning" id="pendingApprovalLabel"
[translate]="'Common.PendingApproval'"></span></div>
</div>
</td>
</tr>
</tbody>
@ -105,8 +121,8 @@
<p-dialog *ngIf="requestToDeny" header="Deny Request '{{requestToDeny.title}}''" [(visible)]="denyDisplay" [draggable]="false">
<span>Please enter a rejection reason, the user will be notified of this:</span>
<textarea [(ngModel)]="rejectionReason" class="form-control-custom form-control"></textarea>
<p-footer>
<button type="button" (click)="denyRequest();" label="Reject" class="btn btn-success">Deny</button>
<button type="button"(click)="denyDisplay=false" label="Close" class="btn btn-danger">Close</button>
</p-footer>
</p-dialog>
<p-footer>
<button type="button" (click)="denyRequest();" label="Reject" class="btn btn-success">Deny</button>
<button type="button" (click)="denyDisplay=false" label="Close" class="btn btn-danger">Close</button>
</p-footer>
</p-dialog>

@ -27,28 +27,35 @@
</div>
<!-- Refine search options -->
<div class="row top-spacing form-group vcenter" *ngIf="refineSearchEnabled">
<div class="col-md-1">
<div class="form-group">
<label class="control-label">Year</label>
<div class="col-md-1">
<div class="form-group">
<label class="control-label">Year</label>
<input [(ngModel)]="searchYear" class="form-control form-control-custom refine-option">
</div>
</div>
</div>
<!-- <label for="name" class="col-xs-2 col-md-1">Language:</label> -->
<div class="col-md-2">
<div class="form-group">
<label for="select" class="control-label">Language</label>
<div id="profiles">
<select [(ngModel)]="selectedLanguage" class="form-control form-control-custom refine-option" id="select">
<option *ngFor="let lang of langauges" value="{{lang.code}}">{{lang.nativeName}}</option>
</select>
</div>
</div>
</div>
<!-- <label for="name" class="col-xs-2 col-md-1">Language:</label> -->
<div class="col-md-2">
<div class="form-group">
<label for="select" class="control-label">Language</label>
<div id="profiles">
<select [(ngModel)]="selectedLanguage" class="form-control form-control-custom refine-option"
id="select">
<option *ngFor="let lang of langauges" value="{{lang.code}}">{{lang.nativeName}}</option>
</select>
</div>
</div>
<div class="col-md-2">
<div class="form-group">
<div class="checkbox">
<input type="checkbox" id="actorSearch" name="actorSearch" [(ngModel)]="actorSearch">
<label for="actorSearch" tooltipPosition="top" pTooltip="Search for movies by actor">Actor Search</label>
</div>
</div>
<div class="col-md-9">
</div>
<div class="col-md-7">
<button class="btn pull-right btn-success-outline" (click)="applyRefinedSearch()">Apply</button>
</div>
</div>
@ -70,7 +77,8 @@
<div class="myBg backdrop" [style.background-image]="result.background"></div>
<div class="tint" style="background-image: linear-gradient(to bottom, rgba(0,0,0,0.6) 0%,rgba(0,0,0,0.6) 100%);"></div>
<div class="col-sm-2 small-padding">
<img *ngIf="result.posterPath" class="img-responsive poster" src="{{result.posterPath}}" alt="poster">
<img *ngIf="result.posterPath" class="img-responsive poster movie-poster" src="{{result.posterPath}}"
alt="poster">
</div>
<div class="col-sm-8 small-padding">

@ -27,6 +27,7 @@ export class MovieSearchComponent implements OnInit {
public searchApplied = false;
public refineSearchEnabled = false;
public searchYear?: number;
public actorSearch: boolean;
public selectedLanguage: string;
public langauges: ILanguageRefine[];
@ -204,7 +205,7 @@ export class MovieSearchComponent implements OnInit {
}
val.background = this.sanitizer.bypassSecurityTrustStyle
("url(" + "https://image.tmdb.org/t/p/w1280" + val.backdropPath + ")");
if (this.applyRefinedSearch) {
this.searchService.getMovieInformationWithRefined(val.id, this.selectedLanguage)
.subscribe(m => {
@ -212,9 +213,9 @@ export class MovieSearchComponent implements OnInit {
});
} else {
this.searchService.getMovieInformation(val.id)
.subscribe(m => {
this.updateItem(val, m);
});
.subscribe(m => {
this.updateItem(val, m);
});
}
});
}
@ -239,14 +240,25 @@ export class MovieSearchComponent implements OnInit {
return;
}
if (this.refineOpen) {
this.searchService.searchMovieWithRefined(this.searchText, this.searchYear, this.selectedLanguage)
.subscribe(x => {
this.movieResults = x;
this.searchApplied = true;
// Now let's load some extra info including IMDB Id
// This way the search is fast at displaying results.
this.getExtraInfo();
});
if (!this.actorSearch) {
this.searchService.searchMovieWithRefined(this.searchText, this.searchYear, this.selectedLanguage)
.subscribe(x => {
this.movieResults = x;
this.searchApplied = true;
// Now let's load some extra info including IMDB Id
// This way the search is fast at displaying results.
this.getExtraInfo();
});
} else {
this.searchService.searchMovieByActor(this.searchText, this.selectedLanguage)
.subscribe(x => {
this.movieResults = x;
this.searchApplied = true;
// Now let's load some extra info including IMDB Id
// This way the search is fast at displaying results.
this.getExtraInfo();
});
}
} else {
this.searchService.searchMovie(this.searchText)
.subscribe(x => {

@ -3,7 +3,7 @@
<div class="myBg backdrop" [style.background-image]="result.background"></div>
<div class="tint" style="background-image: linear-gradient(to bottom, rgba(0,0,0,0.6) 0%,rgba(0,0,0,0.6) 100%);"></div>
<div class="col-sm-3 small-padding">
<img *ngIf="result.poster" class="img-responsive poster" src="{{result.poster}}" alt="poster">
<img *ngIf="result.poster" class="img-responsive poster artist-cover" src="{{result.poster}}" alt="poster">
</div>
<div class="col-sm-7 small-padding">

@ -42,8 +42,11 @@
<td>
{{ep.title}}
</td>
<td>
<td *ngIf="ep.airDateDisplay != 'Unknown'">
{{ep.airDate | amLocal | amDateFormat: 'L' }}
</td>
<td *ngIf="ep.airDateDisplay == 'Unknown'">
{{ep.airDateDisplay }}
</td>
<td>
<ng-template [ngIf]="ep.available"><span class="label label-success" id="availableLabel">Available</span></ng-template>

@ -50,7 +50,7 @@
<div class="tint" style="background-image: linear-gradient(to bottom, rgba(0,0,0,0.6) 0%,rgba(0,0,0,0.6) 100%);"></div>
<div class="col-sm-2 small-padding">
<img *ngIf="node.banner" class="img-responsive poster" width="150" [src]="node.banner" alt="poster">
<img *ngIf="node.banner" class="img-responsive poster tv-poster" width="150" [src]="node.banner" alt="poster">
</div>
<div class="col-sm-8 small-padding">

@ -0,0 +1,25 @@
import { PlatformLocation } from "@angular/common";
import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Observable } from "rxjs";
import {
ICustomPage,
} from "../interfaces";
import { ServiceHelpers } from "./service.helpers";
@Injectable()
export class CustomPageService extends ServiceHelpers {
constructor(public http: HttpClient, public platformLocation: PlatformLocation) {
super(http, "/api/v1/CustomPage", platformLocation);
}
public getCustomPage(): Observable<ICustomPage> {
return this.http.get<ICustomPage>(this.url, {headers: this.headers});
}
public saveCustomPage(model: ICustomPage): Observable<boolean> {
return this.http.post<boolean>(this.url, model, {headers: this.headers});
}
}

@ -16,3 +16,4 @@ export * from "./notificationMessage.service";
export * from "./recentlyAdded.service";
export * from "./vote.service";
export * from "./requestretry.service";
export * from "./custompage.service";

@ -15,4 +15,8 @@ export class MobileService extends ServiceHelpers {
public getUserDeviceList(): Observable<IMobileUsersViewModel[]> {
return this.http.get<IMobileUsersViewModel[]>(`${this.url}notification/`, {headers: this.headers});
}
public deleteUser(userId: string): Observable<boolean> {
return this.http.post<boolean>(`${this.url}remove/`, userId, {headers: this.headers});
}
}

@ -49,6 +49,10 @@ export class SearchService extends ServiceHelpers {
return this.http.post<ISearchMovieResult>(`${this.url}/Movie/info`, { theMovieDbId, languageCode: langCode });
}
public searchMovieByActor(searchTerm: string, langCode: string): Observable<ISearchMovieResult[]> {
return this.http.post<ISearchMovieResult[]>(`${this.url}/Movie/Actor`, { searchTerm, languageCode: langCode });
}
// TV
public searchTv(searchTerm: string): Observable<ISearchTvResult[]> {
return this.http.get<ISearchTvResult[]>(`${this.url}/Tv/${searchTerm}`, { headers: this.headers });

@ -10,7 +10,6 @@ import {
ICronTestModel,
ICronViewModelBody,
ICustomizationSettings,
ICustomPage,
IDiscordNotifcationSettings,
IDogNzbSettings,
IEmailNotificationSettings,
@ -113,14 +112,6 @@ export class SettingsService extends ServiceHelpers {
return this.http.get<IAuthenticationSettings>(`${this.url}/Authentication`, {headers: this.headers});
}
public getCustomPage(): Observable<ICustomPage> {
return this.http.get<ICustomPage>(`${this.url}/CustomPage`, {headers: this.headers});
}
public saveCustomPage(model: ICustomPage): Observable<boolean> {
return this.http.post<boolean>(`${this.url}/CustomPage`, model, {headers: this.headers});
}
public getClientId(): Observable<string> {
return this.http.get<string>(`${this.url}/clientid`, {headers: this.headers});
}

@ -38,7 +38,7 @@
<span>Discord</span>
</td>
<td>
<a href="https://discord.gg/Sa7wNWb" target="_blank">https://discord.gg/</a>
<a href="https://discord.gg/Sa7wNWb" target="_blank">https://discord.gg/Sa7wNWb</a>
</td>
</tr>

@ -75,7 +75,7 @@
<div class="form-group">
<div class="checkbox">
<input type="checkbox" id="useCustomPage" name="useCustomPage" [(ngModel)]="settings.useCustomPage">
<label for="useCustomPage" tooltipPosition="top" pTooltip="Enabled a custom page where you can fully edit">Use
<label for="useCustomPage" tooltipPosition="top" pTooltip="Enabled a custom page where you can fully edit. You will need the Edit Custom Page role.">Use
Custom Page</label>
</div>

@ -63,6 +63,18 @@
<input type="text" class="form-control-custom form-control" id="authToken" [(ngModel)]="server.apiKey" placeholder="Emby Api Key" value="{{server.apiKey}}">
</div>
</div>
<div class="form-group">
<label for="authToken" class="control-label">Externally Facing Hostname
<i class="fa fa-question-circle"
pTooltip="This will be the external address that users will naviagte to when they press the 'View On Emby' button"></i>
</label>
<div>
<input type="text" class="form-control-custom form-control" id="authToken" [(ngModel)]="server.serverHostname" placeholder="e.g. https://jellyfin.server.com/" value="{{server.serverHostname}}">
<small><span *ngIf="server.serverHostname">Current URL: "{{server.serverHostname}}/#!/itemdetails.html?id=1"</span>
<span *ngIf="!server.serverHostname">Current URL: "https://app.emby.media/#!/itemdetails.html?id=1</span></small>
</div>
</div>
<div class="form-group">
<div>
<button id="testEmby" type="button" (click)="test(server)" class="btn btn-primary-outline">Test Connectivity <div id="spinner"></div></button>

@ -35,7 +35,7 @@
<div class="row">
<div class="form-group">
<label for="select" class="control-label">User to send test notification to</label>
<label for="select" class="control-label">Users</label>
<div>
<select class="form-control form-control-custom" id="select" [(ngModel)]="testUserId" [ngModelOptions]="{standalone: true}">
<option value="">Please select</option>
@ -46,7 +46,12 @@
<div class="form-group">
<div>
<button [disabled]="form.invalid" type="button" (click)="test(form)" class="btn btn-danger-outline">Test</button>
<button [disabled]="form.invalid" type="button" (click)="test(form)" class="btn btn-danger-outline">Send Test Notification</button>
</div>
</div>
<div class="form-group">
<div>
<button [disabled]="form.invalid" type="button" (click)="remove(form)" class="btn btn-danger-outline">Remove User</button>
</div>
</div>

@ -79,4 +79,24 @@ export class MobileComponent implements OnInit {
});
}
public remove() {
if (!this.testUserId) {
this.notificationService.warning("Warning", "Please select a user to remove");
return;
}
this.mobileService.deleteUser(this.testUserId).subscribe(x => {
if (x) {
this.notificationService.success("Removed users notification");
const userToRemove = this.userList.filter(u => {
return u.userId === this.testUserId;
})[1];
this.userList.splice(this.userList.indexOf(userToRemove),1);
} else {
this.notificationService.error("There was an error when removing the notification. Please check your logs");
}
});
}
}

@ -1,66 +0,0 @@
<div class="modal-header">
<h3>Add A Friend!</h3>
</div>
<div class="modal-body">
<p>You can invite a user to share your Plex Library here. The invited user will be asked to confirm friendship.</p>
<p>Please note that this user will not appear in your Ombi Users since they have not accepted the Plex Invite, as soon as they accept
the Plex invite then the User Importer job will run (if enabled) and add the user into Ombi.
</p>
<div *ngIf="plexServers">
<form novalidate [formGroup]="form" (ngSubmit)="onSubmit(form)" style="padding-top:5%;">
<div class="form-group">
<label for="username" class="control-label">Username/Email</label>
<input type="text" class="form-control form-control-custom " id="username" name="username" p formControlName="username" [ngClass]="{'form-error': form.get('username').hasError('required')}">
<small *ngIf="form.get('username').hasError('required')" class="error-text">The Username/Email is required</small>
</div>
<div class="form-group">
<label for="select" class="control-label">Select a Server</label>
<div id="profiles">
<select formControlName="selectedServer" (change)="selected()" class="form-control form-control-custom" id="select" [ngClass]="{'form-error': form.get('selectedServer').hasError('required')}">
<option *ngFor="let server of plexServers" value="{{server.machineId}}">{{server.serverName}}</option>
</select>
</div>
<small *ngIf="form.get('selectedServer').hasError('required')" class="error-text">You need to select a server!</small>
</div>
<div *ngIf="plexLibs" class="form-group">
<label for="select" class="control-label">Libraries to share</label>
<div class="form-group">
<div class="checkbox">
<input type="checkbox" id="selectAll" formControlName="allLibsSelected">
<label for="selectAll">All</label>
</div>
</div>
<div *ngIf="!form.value.allLibsSelected">
<div *ngFor="let lib of plexLibs">
<div class="col-md-4">
<div class="checkbox">
<input type="checkbox" id="{{lib.id}}" value={{lib.id}} (change)="checkedLib($event.target.checked, $event.target.value)">
<label for="{{lib.id}}">{{lib.title}}</label>
</div>
</div>
</div>
</div>
</div>
<br>
<br>
<br>
</form>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary-outline" (click)="onSubmit(form)" [disabled]="form.invalid">Add</button>
<button type="button" class="btn btn-danger-outline" (click)="activeModal.close('Close click')">Close</button>
</div>

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

Loading…
Cancel
Save