Merge branch 'DotNetCore' into l10n_DotNetCore

pull/1955/head
Jamie 7 years ago committed by GitHub
commit 0c83329025
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,4 +1,3 @@
<!---
!! Please use the Support / bug report template, otherwise we will close the Github issue !!

@ -4,6 +4,66 @@
### **New Features**
- Update README.md. [Jamie]
- Update README.md. [Jamie]
- Update ISSUE_TEMPLATE.md. [Jamie]
- Update appveyor.yml. [Jamie]
- Update ISSUE_TEMPLATE.md. [PotatoQuality]
- Update ISSUE_TEMPLATE.md. [PotatoQuality]
- Update README.md. [Jamie]
- Update README.md. [Jamie]
- Update README.md. [Jamie]
- Update README.md. [PotatoQuality]
- Change the default templates to use {IssueUser} [Jamie]
- Changed the base url validation. [tidusjar]
- Added bulk editing (#1941) [Jamie]
- Change the poster size to w300 #1932. [Jamie]
- Added a default user agent on all API calls. [tidusjar]
- Update request.service.ts. [Jamie]
- Added a filter onto the movies requests page for some inital feedback. [Jamie]
- Added ordering to the User Management screen. [Jamie]
- Update README.md. [Jamie]
- Added custom donation url (#1902) [m4tta]
- Changed the url scheme to make it easier to parse. [Jamie]
- Added Norwegian to the translation code, forgot to check this in. [Jamie]
- Added Norwegian to the language dropdown. [Jamie]
- Added the stuff needed for omBlur. [tidusjar]
- Update README.md (#1872) [xnaas]
- Update README.md. [Jamie]
- Update plex.component.html. [Jamie]
- Change plus to list in menu (#1855) [Louis Laureys]
- Update README.md. [Jamie]
- Update README.md. [Jamie]
- Added user request limits, We can now set the limit for a user. [tidusjar]
- Updated the UI JWT framework. [Jamie]
@ -284,6 +344,174 @@
### **Fixes**
- Small changes that might fix #1985 but doubt it. [Jamie]
- Should fix #1975. [tidusjar]
- Fixed #1789. [tidusjar]
- Fixed #1968. [tidusjar]
- Fixed #1978. [tidusjar]
- Fixed #1954. [tidusjar]
- Small changes to the auto updater, let's see how this works. [Jamie]
- Fixed build. [Jamie]
- Fixed the update check for the master build. [Jamie]
- Removed accidently merged files. [Jamie]
- Create CODE_OF_CONDUCT.md. [Jamie]
- Windows installation guide link update. [PotatoQuality]
- Fixed the issue comment issue #1914 also added another variable for issues {IssueUser} which is the user that reported the issue. [Jamie]
- Fix #1914. [tidusjar]
- Fixed #1914. [tidusjar]
- Fixed build and added logging. [TidusJar]
- New Crowdin translations (#1934) [Jamie]
- Potential fix for #1942. [Jamie]
- Quick change to the Emby Availability rule to make it in line slightly with the Plex one. #1950. [Jamie]
- Turn off mobile notifications. [tidusjar]
- FIXED PLEX!!!!! [tidusjar]
- Batch the PlexContentSync and increase the plex episode batch size. [tidusjar]
- Fixed the migration issue, it's too difficult to migrate the tables. [tidusjar]
- Fixed #1942. [tidusjar]
- Fixed checkboxes style. [Jamie]
- These are not the droids you are looking for. [Jamie]
- Fixed the wrong translation and see if we can VACUUM the db. [tidusjar]
- More translations and added a check on the baseurl to ensure it starts with a '/' [Jamie]
- More translations. [Jamie]
- Fixed #1878 and added a Request all button when selecting episodes. [Jamie]
- Working on the movie matching. Stop dupes #1869. [tidusjar]
- Delete plex episodes on every run due to a bug, need to spend quite a bit of time on this. [tidusjar]
- Fixed the issue where we were always adding emby episodes. Also fixed #1933. [tidusjar]
- New Crowdin translations (#1906) [Jamie]
- Add plain password for emby login (#1925) [dorian ALKOUM]
- Fixed #1924. [Jamie]
- Fixed the issue where I knocked out the ordering of notifications, oops. [tidusjar]
- #1914 for the issue resolved notification. [Jamie]
- #1916. [Jamie]
- Remove the placeholder. [Jamie]
- Feature arm (#1909) [Jamie]
- New Crowdin translations (#1897) [Jamie]
- Fix logo cut off on login screen (#1896) [Louis Laureys]
- E-Mails: Only add poster table row if img is set (#1899) [Louis Laureys]
- New Crowdin translations (#1884) [Jamie]
- Fix mobile layout (#1888) [Louis Laureys]
- Smal changes to the api. [tidusjar]
- OmBlur. [tidusjar]
- Hide the password field if it's not needed #1815. [Jamie]
- Should fix #1885. [Jamie]
- Make user management table responsive (#1882) [Louis Laureys]
- Fixed some stuff for omBlur. [Jamie]
- Some work... No one take a look at this, it's a suprise. [Jamie]
- New Crowdin translations (#1858) [Jamie]
- When requesting Anime, we now mark it correctly as Anime in Sonarr. [tidusjar]
- Fixed #1879 and added the spans. [tidusjar]
- Some work on the auto updater #1460. [tidusjar]
- Removed the potential locking. [tidusjar]
- Fixed #1863. [tidusjar]
- Moved the update check code from the External azure service into Ombi at /api/v1/update/BRANCH. [Jamie]
- Fixed the UI erroring out, also dont show tv with no externals. [tidusjar]
- More memory management and improvements. [tidusjar]
- These are not needed, added accidentally (#1860) [Louis Laureys]
- Some memory management improvements. [tidusjar]
- Fixed #1857. [tidusjar]
- Delete old v2 ombi from v3 branch. [tidusjar]
- New Crowdin translations (#1840) [Jamie]
- Better login backgrounds! (#1852) [Louis Laureys]
- Fixed #1851. [tidusjar]
- Fixed #1826. [tidusjar]
- Redo change #1848. [tidusjar]
- Fix the issue for welcome emails not sending. [tidusjar]
- Fix typo (#1845) [Kyle Lucy]
- Fix user mentions in Slack notifications (#1846) [Aljosa Asanovic]
- If Radarr/Sonarr has noticed that the media is available, then mark it as available in the UI. [Jamie]
- Fixed #1835. [Jamie]
- Enable Multi MIME and add alt tags to images (#1838) [Louis Laureys]
- New Crowdin translations (#1816) [Jamie]
- Fixed #1832. [tidusjar]
- Switch to use a single HTTPClient rather than a new one every request !dev. [tidusjar]
- Fix non-admin rights (#1820) [Rob Gökemeijer]
- Fix duplicated "Requests" element ID on new Issues link (#1817) [Shoghi Cervantes]
- Add the Issue Reporting functionality (#1811) [Jamie]
- Removed the forum. [tidusjar]
- #1659 Made the option to ignore notifcations for auto approve. [Jamie]
- New Crowdin translations (#1806) [Jamie]

@ -0,0 +1,46 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at tidusjar@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

@ -9,8 +9,12 @@ ____
[![Patreon](https://www.ombi.io/img/patreondonate.svg)](https://patreon.com/tidusjar/Ombi)
[![Paypal](https://www.ombi.io/img/paypaldonate.svg)](https://paypal.me/PlexRequestsNet)
[![Report a bug](http://i.imgur.com/xSpw482.png)](https://github.com/tidusjar/Ombi/issues/new) [![Feature request](http://i.imgur.com/mFO0OuX.png)](http://feathub.com/tidusjar/Ombi)
[![Patreon](https://www.ombi.io/img/patreondonate.svg)](https://patreon.com/tidusjar/Ombi)
[![Paypal](https://www.ombi.io/img/paypaldonate.svg)](https://paypal.me/PlexRequestsNet)
___
[![Report a bug](http://i.imgur.com/xSpw482.png)](https://forums.ombi.io/viewforum.php?f=10) [![Feature request](http://i.imgur.com/mFO0OuX.png)](https://forums.ombi.io/posting.php?mode=post&f=20)
| Service | Master (V2) | Open Beta (V3 - Recommended) |
|----------|:---------------------------:|:----------------------------:|

@ -38,4 +38,3 @@ deploy:
draft: true
on:
branch: master

@ -0,0 +1,11 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Ombi.Api.Notifications.Models;
namespace Ombi.Api.Notifications
{
public interface IOneSignalApi
{
Task<OneSignalNotificationResponse> PushNotification(List<string> playerIds, string message);
}
}

@ -0,0 +1,21 @@
namespace Ombi.Api.Notifications.Models
{
public class OneSignalNotificationBody
{
public string app_id { get; set; }
public string[] include_player_ids { get; set; }
public Data data { get; set; }
public Contents contents { get; set; }
}
public class Data
{
public string foo { get; set; }
}
public class Contents
{
public string en { get; set; }
}
}

@ -0,0 +1,9 @@
namespace Ombi.Api.Notifications.Models
{
public class OneSignalNotificationResponse
{
public string id { get; set; }
public int recipients { get; set; }
}
}

@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Ombi.Api\Ombi.Api.csproj" />
</ItemGroup>
</Project>

@ -0,0 +1,48 @@
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Ombi.Api.Notifications.Models;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
namespace Ombi.Api.Notifications
{
public class OneSignalApi : IOneSignalApi
{
public OneSignalApi(IApi api, IApplicationConfigRepository repo)
{
_api = api;
_appConfig = repo;
}
private readonly IApi _api;
private readonly IApplicationConfigRepository _appConfig;
private const string ApiUrl = "https://onesignal.com/api/v1/notifications";
public async Task<OneSignalNotificationResponse> PushNotification(List<string> playerIds, string message)
{
if (!playerIds.Any())
{
return null;
}
var id = await _appConfig.Get(ConfigurationTypes.Notification);
var request = new Request(string.Empty, ApiUrl, HttpMethod.Post);
var body = new OneSignalNotificationBody
{
app_id = id.Value,
contents = new Contents
{
en = message
},
include_player_ids = playerIds.ToArray()
};
request.AddJsonBody(body);
var result = await _api.Request<OneSignalNotificationResponse>(request);
return result;
}
}
}

@ -0,0 +1,9 @@
using System;
namespace Ombi.Api.Radarr
{
public class CommandResult
{
public string name { get; set; }
}
}

@ -10,6 +10,9 @@ namespace Ombi.Api.Radarr
Task<List<RadarrProfile>> GetProfiles(string apiKey, string baseUrl);
Task<List<RadarrRootFolder>> GetRootFolders(string apiKey, string baseUrl);
Task<SystemStatus> SystemStatus(string apiKey, string baseUrl);
Task<MovieResponse> GetMovie(int id, string apiKey, string baseUrl);
Task<MovieResponse> UpdateMovie(MovieResponse movie, string apiKey, string baseUrl);
Task<bool> MovieSearch(int[] movieIds, string apiKey, string baseUrl);
Task<RadarrAddMovieResponse> AddMovie(int tmdbId, string title, int year, int qualityId, string rootPath,string apiKey, string baseUrl, bool searchNow, string minimumAvailability);
}
}

@ -17,10 +17,7 @@ namespace Ombi.Api.Radarr.Models
public bool monitored { get; set; }
public int tmdbId { get; set; }
public List<string> images { get; set; }
public string cleanTitle { get; set; }
public string imdbId { get; set; }
public string titleSlug { get; set; }
public int id { get; set; }
public int year { get; set; }
public string minimumAvailability { get; set; }
}

@ -3,19 +3,10 @@
public class RadarrError
{
public string message { get; set; }
public string description { get; set; }
}
public class RadarrErrorResponse
{
public string propertyName { get; set; }
public string errorMessage { get; set; }
public object attemptedValue { get; set; }
public FormattedMessagePlaceholderValues formattedMessagePlaceholderValues { get; set; }
}
public class FormattedMessagePlaceholderValues
{
public string propertyName { get; set; }
public object propertyValue { get; set; }
}
}

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
@ -53,6 +52,23 @@ namespace Ombi.Api.Radarr
return await Api.Request<List<MovieResponse>>(request);
}
public async Task<MovieResponse> GetMovie(int id, string apiKey, string baseUrl)
{
var request = new Request($"/api/movie/{id}", baseUrl, HttpMethod.Get);
AddHeaders(request, apiKey);
return await Api.Request<MovieResponse>(request);
}
public async Task<MovieResponse> UpdateMovie(MovieResponse movie, string apiKey, string baseUrl)
{
var request = new Request($"/api/movie/", baseUrl, HttpMethod.Put);
AddHeaders(request, apiKey);
request.AddJsonBody(movie);
return await Api.Request<MovieResponse>(request);
}
public async Task<RadarrAddMovieResponse> AddMovie(int tmdbId, string title, int year, int qualityId, string rootPath, string apiKey, string baseUrl, bool searchNow, string minimumAvailability)
{
var request = new Request("/api/movie", baseUrl, HttpMethod.Post);
@ -66,7 +82,7 @@ namespace Ombi.Api.Radarr
titleSlug = title,
monitored = true,
year = year,
minimumAvailability = minimumAvailability,
minimumAvailability = minimumAvailability
};
if (searchNow)
@ -81,9 +97,9 @@ namespace Ombi.Api.Radarr
request.AddHeader("X-Api-Key", apiKey);
request.AddJsonBody(options);
var response = await Api.RequestContent(request);
try
{
var response = await Api.RequestContent(request);
if (response.Contains("\"message\":"))
{
var error = JsonConvert.DeserializeObject<RadarrError>(response);
@ -98,11 +114,24 @@ namespace Ombi.Api.Radarr
}
catch (JsonSerializationException jse)
{
Logger.LogError(LoggingEvents.RadarrApi, jse, "Error When adding movie to Radarr");
Logger.LogError(LoggingEvents.RadarrApi, jse, "Error When adding movie to Radarr, Reponse: {0}", response);
}
return null;
}
public async Task<bool> MovieSearch(int[] movieIds, string apiKey, string baseUrl)
{
var result = await Command(apiKey, baseUrl, new { name = "MoviesSearch", movieIds });
return result != null;
}
private async Task<CommandResult> Command(string apiKey, string baseUrl, object body)
{
var request = new Request($"/api/Command/", baseUrl, HttpMethod.Post);
request.AddHeader("X-Api-Key", apiKey);
request.AddJsonBody(body);
return await Api.Request<CommandResult>(request);
}
/// <summary>
/// Adds the required headers and also the authorization header

@ -8,11 +8,6 @@ namespace Ombi.Api.Sonarr.Models
public class CommandResult
{
public string name { get; set; }
public DateTime startedOn { get; set; }
public DateTime stateChangeTime { get; set; }
public bool sendUpdatesToClient { get; set; }
public string state { get; set; }
public int id { get; set; }
}
}

@ -66,8 +66,10 @@ namespace Ombi.Api.Sonarr
var request = new Request($"/api/series/{id}", baseUrl, HttpMethod.Get);
request.AddHeader("X-Api-Key", apiKey);
var result = await Api.Request<SonarrSeries>(request);
result.seasons.ToList().RemoveAt(0);
if (result?.seasons?.Length > 0)
{
result?.seasons?.ToList().RemoveAt(0);
}
return result;
}

@ -0,0 +1,17 @@
using System.Collections.Generic;
using Ombi.Settings.Settings.Models.Notifications;
using Ombi.Store.Entities;
namespace Ombi.Core.Models.UI
{
public class MobileNotificationsViewModel : MobileNotificationSettings
{
/// <summary>
/// Gets or sets the notification templates.
/// </summary>
/// <value>
/// The notification templates.
/// </value>
public List<NotificationTemplates> NotificationTemplates { get; set; }
}
}

@ -3,6 +3,7 @@ using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Ombi.Core.Models.Search;
using Ombi.Core.Rule.Interfaces;
using Ombi.Helpers;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
@ -20,35 +21,48 @@ namespace Ombi.Core.Rule.Rules.Search
public async Task<RuleResult> Execute(SearchViewModel obj)
{
EmbyContent item = null;
if (obj.Type == RequestType.Movie)
if (obj.ImdbId.HasValue())
{
item = await EmbyContentRepository.Get(obj.ImdbId);
if (item == null)
}
if (item == null)
{
if (obj.TheMovieDbId.HasValue())
{
item = await EmbyContentRepository.Get(obj.TheMovieDbId);
}
if (item == null)
{
if (obj.TheTvDbId.HasValue())
{
item = await EmbyContentRepository.Get(obj.TheTvDbId);
}
}
}
else
{
item = await EmbyContentRepository.Get(obj.TheTvDbId);
}
if (item != null)
{
obj.Available = true;
if (obj.Type == RequestType.TvShow)
{
var searchResult = (SearchTvShowViewModel)obj;
var search = (SearchTvShowViewModel)obj;
// Let's go through the episodes now
if (searchResult.SeasonRequests.Any())
if (search.SeasonRequests.Any())
{
var allEpisodes = EmbyContentRepository.GetAllEpisodes().Include(x => x.Series);
foreach (var season in searchResult.SeasonRequests)
foreach (var season in search.SeasonRequests)
{
foreach (var episode in season.Episodes)
{
var epExists = await allEpisodes.FirstOrDefaultAsync(x =>
x.EpisodeNumber == episode.EpisodeNumber && x.SeasonNumber == season.SeasonNumber && item.ProviderId.ToString() == searchResult.Id.ToString());
EmbyEpisode epExists = null;
epExists = await allEpisodes.FirstOrDefaultAsync(x =>
x.EpisodeNumber == episode.EpisodeNumber && x.SeasonNumber == season.SeasonNumber &&
x.Series.ProviderId == item.ProviderId.ToString());
if (epExists != null)
{
episode.Available = true;

@ -83,11 +83,11 @@ namespace Ombi.Core.Rule.Rules.Search
}
}
if (request.SeasonRequests.Any() && request.SeasonRequests.All(x => x.Episodes.All(e => e.Approved)))
if (request.SeasonRequests.Any() && request.SeasonRequests.All(x => x.Episodes.All(e => e.Available)))
{
request.FullyAvailable = true;
}
if (request.SeasonRequests.Any() && request.SeasonRequests.All(x => x.Episodes.Any(e => e.Approved)))
if (request.SeasonRequests.Any() && request.SeasonRequests.All(x => x.Episodes.Any(e => e.Available)))
{
request.PartlyAvailable = true;
}

@ -48,7 +48,7 @@ namespace Ombi.Core.Senders
var dogSettings = await DogNzbSettings.GetSettingsAsync();
if (dogSettings.Enabled)
{
await SendToDogNzb(model,dogSettings);
await SendToDogNzb(model, dogSettings);
return new SenderResult
{
Success = true,
@ -95,18 +95,40 @@ namespace Ombi.Core.Senders
}
var rootFolderPath = model.RootPathOverride <= 0 ? settings.DefaultRootPath : await RadarrRootPath(model.RootPathOverride, settings);
var result = await RadarrApi.AddMovie(model.TheMovieDbId, model.Title, model.ReleaseDate.Year, qualityToUse, rootFolderPath, settings.ApiKey, settings.FullUri, !settings.AddOnly, settings.MinimumAvailability);
if (!string.IsNullOrEmpty(result.Error?.message))
// Check if the movie already exists? Since it could be unmonitored
var movies = await RadarrApi.GetMovies(settings.ApiKey, settings.FullUri);
var existingMovie = movies.FirstOrDefault(x => x.tmdbId == model.TheMovieDbId);
if (existingMovie == null)
{
Log.LogError(LoggingEvents.RadarrCacher,result.Error.message);
return new SenderResult { Success = false, Message = result.Error.message, Sent = false };
var result = await RadarrApi.AddMovie(model.TheMovieDbId, model.Title, model.ReleaseDate.Year,
qualityToUse, rootFolderPath, settings.ApiKey, settings.FullUri, !settings.AddOnly,
settings.MinimumAvailability);
if (!string.IsNullOrEmpty(result.Error?.message))
{
Log.LogError(LoggingEvents.RadarrCacher, result.Error.message);
return new SenderResult { Success = false, Message = result.Error.message, Sent = false };
}
if (!string.IsNullOrEmpty(result.title))
{
return new SenderResult { Success = true, Sent = false };
}
return new SenderResult { Success = true, Sent = false };
}
if (!string.IsNullOrEmpty(result.title))
// We have the movie, check if we can request it or change the status
if (!existingMovie.monitored)
{
return new SenderResult { Success = true, Sent = false };
// let's set it to monitored and search for it
existingMovie.monitored = true;
await RadarrApi.UpdateMovie(existingMovie, settings.ApiKey, settings.FullUri);
// Search for it
await RadarrApi.MovieSearch(new[] { existingMovie.id }, settings.ApiKey, settings.FullUri);
return new SenderResult { Success = true, Sent = true };
}
return new SenderResult { Success = true, Sent = false };
return new SenderResult { Success = false, Sent = false, Message = "Movie is already monitored" };
}
private async Task<string> RadarrRootPath(int overrideId, RadarrSettings settings)

@ -1,388 +1,388 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Ombi.Api.DogNzb;
using Ombi.Api.DogNzb.Models;
using Ombi.Api.SickRage;
using Ombi.Api.SickRage.Models;
using Ombi.Api.Sonarr;
using Ombi.Api.Sonarr.Models;
using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Settings.Settings.Models.External;
using Ombi.Store.Entities.Requests;
namespace Ombi.Core.Senders
{
public class TvSender : ITvSender
{
public TvSender(ISonarrApi sonarrApi, ILogger<TvSender> log, ISettingsService<SonarrSettings> sonarrSettings,
ISettingsService<DogNzbSettings> dog, IDogNzbApi dogApi, ISettingsService<SickRageSettings> srSettings,
ISickRageApi srApi)
{
SonarrApi = sonarrApi;
Logger = log;
SonarrSettings = sonarrSettings;
DogNzbSettings = dog;
DogNzbApi = dogApi;
SickRageSettings = srSettings;
SickRageApi = srApi;
}
private ISonarrApi SonarrApi { get; }
private IDogNzbApi DogNzbApi { get; }
private ISickRageApi SickRageApi { get; }
private ILogger<TvSender> Logger { get; }
private ISettingsService<SonarrSettings> SonarrSettings { get; }
private ISettingsService<DogNzbSettings> DogNzbSettings { get; }
private ISettingsService<SickRageSettings> SickRageSettings { get; }
public async Task<SenderResult> Send(ChildRequests model)
{
var sonarr = await SonarrSettings.GetSettingsAsync();
if (sonarr.Enabled)
{
var result = await SendToSonarr(model);
if (result != null)
{
return new SenderResult
{
Sent = true,
Success = true
};
}
}
var dog = await DogNzbSettings.GetSettingsAsync();
if (dog.Enabled)
{
var result = await SendToDogNzb(model, dog);
if (!result.Failure)
{
return new SenderResult
{
Sent = true,
Success = true
};
}
return new SenderResult
{
Message = result.ErrorMessage
};
}
var sr = await SickRageSettings.GetSettingsAsync();
if (sr.Enabled)
{
var result = await SendToSickRage(model, sr);
if (result)
{
return new SenderResult
{
Sent = true,
Success = true
};
}
return new SenderResult
{
Message = "Could not send to SickRage!"
};
}
return new SenderResult
{
Success = true
};
}
private async Task<DogNzbAddResult> SendToDogNzb(ChildRequests model, DogNzbSettings settings)
{
var id = model.ParentRequest.TvDbId;
return await DogNzbApi.AddTvShow(settings.ApiKey, id.ToString());
}
/// <summary>
/// Send the request to Sonarr to process
/// </summary>
/// <param name="s"></param>
/// <param name="model"></param>
/// <param name="qualityId">This is for any qualities overriden from the UI</param>
/// <returns></returns>
public async Task<NewSeries> SendToSonarr(ChildRequests model, string qualityId = null)
{
var s = await SonarrSettings.GetSettingsAsync();
if (!s.Enabled)
{
return null;
}
if (string.IsNullOrEmpty(s.ApiKey))
{
return null;
}
var qualityProfile = 0;
if (!string.IsNullOrEmpty(qualityId)) // try to parse the passed in quality, otherwise use the settings default quality
{
int.TryParse(qualityId, out qualityProfile);
}
if (qualityProfile <= 0)
{
int.TryParse(s.QualityProfile, out qualityProfile);
}
// Get the root path from the rootfolder selected.
// For some reason, if we haven't got one use the first root folder in Sonarr
// TODO make this overrideable via the UI
var rootFolderPath = await GetSonarrRootPath(model.ParentRequest.RootFolder ?? int.Parse(s.RootPath), s);
try
{
// Does the series actually exist?
var allSeries = await SonarrApi.GetSeries(s.ApiKey, s.FullUri);
var existingSeries = allSeries.FirstOrDefault(x => x.tvdbId == model.ParentRequest.TvDbId);
if (existingSeries == null)
{
// Time to add a new one
var newSeries = new NewSeries
{
title = model.ParentRequest.Title,
imdbId = model.ParentRequest.ImdbId,
tvdbId = model.ParentRequest.TvDbId,
cleanTitle = model.ParentRequest.Title,
monitored = true,
seasonFolder = s.SeasonFolders,
rootFolderPath = rootFolderPath,
qualityProfileId = qualityProfile,
titleSlug = model.ParentRequest.Title,
addOptions = new AddOptions
{
ignoreEpisodesWithFiles = true, // There shouldn't be any episodes with files, this is a new season
ignoreEpisodesWithoutFiles = true, // We want all missing
searchForMissingEpisodes = false // we want dont want to search yet. We want to make sure everything is unmonitored/monitored correctly.
}
};
// Montitor the correct seasons,
// If we have that season in the model then it's monitored!
var seasonsToAdd = new List<Season>();
for (var i = 1; i < model.ParentRequest.TotalSeasons + 1; i++)
{
var index = i;
var season = new Season
{
seasonNumber = i,
monitored = model.SeasonRequests.Any(x => x.SeasonNumber == index)
};
seasonsToAdd.Add(season);
}
newSeries.seasons = seasonsToAdd;
var result = await SonarrApi.AddSeries(newSeries, s.ApiKey, s.FullUri);
existingSeries = await SonarrApi.GetSeriesById(result.id, s.ApiKey, s.FullUri);
await SendToSonarr(model, existingSeries, s);
}
else
{
await SendToSonarr(model, existingSeries, s);
}
return new NewSeries
{
id = existingSeries.id,
seasons = existingSeries.seasons.ToList(),
cleanTitle = existingSeries.cleanTitle,
title = existingSeries.title,
tvdbId = existingSeries.tvdbId
};
}
catch (Exception e)
{
Logger.LogError(LoggingEvents.SonarrSender, e, "Exception thrown when attempting to send series over to Sonarr");
throw;
}
}
private async Task SendToSonarr(ChildRequests model, SonarrSeries result, SonarrSettings s)
{
var episodesToUpdate = new List<Episode>();
// Ok, now let's sort out the episodes.
if (model.SeriesType == SeriesType.Anime)
{
result.seriesType = "anime";
await SonarrApi.UpdateSeries(result, s.ApiKey, s.FullUri);
}
var sonarrEpisodes = await SonarrApi.GetEpisodes(result.id, s.ApiKey, s.FullUri);
var sonarrEpList = sonarrEpisodes.ToList() ?? new List<Episode>();
while (!sonarrEpList.Any())
{
// It could be that the series metadata is not ready yet. So wait
sonarrEpList = (await SonarrApi.GetEpisodes(result.id, s.ApiKey, s.FullUri)).ToList();
await Task.Delay(500);
}
foreach (var req in model.SeasonRequests)
{
foreach (var ep in req.Episodes)
{
var sonarrEp = sonarrEpList.FirstOrDefault(x =>
x.episodeNumber == ep.EpisodeNumber && x.seasonNumber == req.SeasonNumber);
if (sonarrEp != null)
{
sonarrEp.monitored = true;
episodesToUpdate.Add(sonarrEp);
}
}
}
var seriesChanges = false;
foreach (var season in model.SeasonRequests)
{
var sonarrSeason = sonarrEpList.Where(x => x.seasonNumber == season.SeasonNumber);
var sonarrEpCount = sonarrSeason.Count();
var ourRequestCount = season.Episodes.Count;
if (sonarrEpCount == ourRequestCount)
{
// We have the same amount of requests as all of the episodes in the season.
var existingSeason =
result.seasons.First(x => x.seasonNumber == season.SeasonNumber);
existingSeason.monitored = true;
seriesChanges = true;
}
else
{
// Now update the episodes that need updating
foreach (var epToUpdate in episodesToUpdate.Where(x => x.seasonNumber == season.SeasonNumber))
{
await SonarrApi.UpdateEpisode(epToUpdate, s.ApiKey, s.FullUri);
}
}
}
if (seriesChanges)
{
await SonarrApi.SeasonPass(s.ApiKey, s.FullUri, result);
}
if (!s.AddOnly)
{
await SearchForRequest(model, sonarrEpList, result, s, episodesToUpdate);
}
}
private async Task<bool> SendToSickRage(ChildRequests model, SickRageSettings settings, string qualityId = null)
{
var tvdbid = model.ParentRequest.TvDbId;
if (qualityId.HasValue())
{
var id = qualityId;
if (settings.Qualities.All(x => x.Value != id))
{
qualityId = settings.QualityProfile;
}
}
else
{
qualityId = settings.QualityProfile;
}
// Check if the show exists
var existingShow = await SickRageApi.GetShow(tvdbid, settings.ApiKey, settings.FullUri);
if (existingShow.message.Equals("Show not found", StringComparison.CurrentCultureIgnoreCase))
{
var addResult = await SickRageApi.AddSeries(model.ParentRequest.TvDbId, qualityId, SickRageStatus.Ignored,
settings.ApiKey, settings.FullUri);
Logger.LogDebug("Added the show (tvdbid) {0}. The result is '{2}' : '{3}'", tvdbid, addResult.result, addResult.message);
if (addResult.result.Equals("failure") || addResult.result.Equals("fatal"))
{
// Do something
return false;
}
}
foreach (var seasonRequests in model.SeasonRequests)
{
var srEpisodes = await SickRageApi.GetEpisodesForSeason(tvdbid, seasonRequests.SeasonNumber, settings.ApiKey, settings.FullUri);
while (srEpisodes.message.Equals("Show not found", StringComparison.CurrentCultureIgnoreCase) && srEpisodes.data.Count <= 0)
{
await Task.Delay(TimeSpan.FromSeconds(1));
srEpisodes = await SickRageApi.GetEpisodesForSeason(tvdbid, seasonRequests.SeasonNumber, settings.ApiKey, settings.FullUri);
}
var totalSrEpisodes = srEpisodes.data.Count;
if (totalSrEpisodes == seasonRequests.Episodes.Count)
{
// This is a request for the whole season
var wholeSeasonResult = await SickRageApi.SetEpisodeStatus(settings.ApiKey, settings.FullUri, tvdbid, SickRageStatus.Wanted,
seasonRequests.SeasonNumber);
Logger.LogDebug("Set the status to Wanted for season {0}. The result is '{1}' : '{2}'", seasonRequests.SeasonNumber, wholeSeasonResult.result, wholeSeasonResult.message);
continue;
}
foreach (var srEp in srEpisodes.data)
{
var epNumber = srEp.Key;
var epData = srEp.Value;
var epRequest = seasonRequests.Episodes.FirstOrDefault(x => x.EpisodeNumber == epNumber);
if (epRequest != null)
{
// We want to monior this episode since we have a request for it
// Let's check to see if it's wanted first, save an api call
if (epData.status.Equals(SickRageStatus.Wanted, StringComparison.CurrentCultureIgnoreCase))
{
continue;
}
var epResult = await SickRageApi.SetEpisodeStatus(settings.ApiKey, settings.FullUri, tvdbid,
SickRageStatus.Wanted, seasonRequests.SeasonNumber, epNumber);
Logger.LogDebug("Set the status to Wanted for Episode {0} in season {1}. The result is '{2}' : '{3}'", seasonRequests.SeasonNumber, epNumber, epResult.result, epResult.message);
}
}
}
return true;
}
private async Task SearchForRequest(ChildRequests model, IEnumerable<Episode> sonarrEpList, SonarrSeries existingSeries, SonarrSettings s,
IReadOnlyCollection<Episode> episodesToUpdate)
{
foreach (var season in model.SeasonRequests)
{
var sonarrSeason = sonarrEpList.Where(x => x.seasonNumber == season.SeasonNumber);
var sonarrEpCount = sonarrSeason.Count();
var ourRequestCount = season.Episodes.Count;
if (sonarrEpCount == ourRequestCount)
{
// We have the same amount of requests as all of the episodes in the season.
// Do a season search
await SonarrApi.SeasonSearch(existingSeries.id, season.SeasonNumber, s.ApiKey, s.FullUri);
}
else
{
// There is a miss-match, let's search the episodes indiviaully
await SonarrApi.EpisodeSearch(episodesToUpdate.Select(x => x.id).ToArray(), s.ApiKey, s.FullUri);
}
}
}
private async Task<string> GetSonarrRootPath(int pathId, SonarrSettings sonarrSettings)
{
var rootFoldersResult = await SonarrApi.GetRootFolders(sonarrSettings.ApiKey, sonarrSettings.FullUri);
if (pathId == 0)
{
return rootFoldersResult.FirstOrDefault().path;
}
foreach (var r in rootFoldersResult.Where(r => r.id == pathId))
{
return r.path;
}
return string.Empty;
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Ombi.Api.DogNzb;
using Ombi.Api.DogNzb.Models;
using Ombi.Api.SickRage;
using Ombi.Api.SickRage.Models;
using Ombi.Api.Sonarr;
using Ombi.Api.Sonarr.Models;
using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Settings.Settings.Models.External;
using Ombi.Store.Entities.Requests;
namespace Ombi.Core.Senders
{
public class TvSender : ITvSender
{
public TvSender(ISonarrApi sonarrApi, ILogger<TvSender> log, ISettingsService<SonarrSettings> sonarrSettings,
ISettingsService<DogNzbSettings> dog, IDogNzbApi dogApi, ISettingsService<SickRageSettings> srSettings,
ISickRageApi srApi)
{
SonarrApi = sonarrApi;
Logger = log;
SonarrSettings = sonarrSettings;
DogNzbSettings = dog;
DogNzbApi = dogApi;
SickRageSettings = srSettings;
SickRageApi = srApi;
}
private ISonarrApi SonarrApi { get; }
private IDogNzbApi DogNzbApi { get; }
private ISickRageApi SickRageApi { get; }
private ILogger<TvSender> Logger { get; }
private ISettingsService<SonarrSettings> SonarrSettings { get; }
private ISettingsService<DogNzbSettings> DogNzbSettings { get; }
private ISettingsService<SickRageSettings> SickRageSettings { get; }
public async Task<SenderResult> Send(ChildRequests model)
{
var sonarr = await SonarrSettings.GetSettingsAsync();
if (sonarr.Enabled)
{
var result = await SendToSonarr(model);
if (result != null)
{
return new SenderResult
{
Sent = true,
Success = true
};
}
}
var dog = await DogNzbSettings.GetSettingsAsync();
if (dog.Enabled)
{
var result = await SendToDogNzb(model, dog);
if (!result.Failure)
{
return new SenderResult
{
Sent = true,
Success = true
};
}
return new SenderResult
{
Message = result.ErrorMessage
};
}
var sr = await SickRageSettings.GetSettingsAsync();
if (sr.Enabled)
{
var result = await SendToSickRage(model, sr);
if (result)
{
return new SenderResult
{
Sent = true,
Success = true
};
}
return new SenderResult
{
Message = "Could not send to SickRage!"
};
}
return new SenderResult
{
Success = true
};
}
private async Task<DogNzbAddResult> SendToDogNzb(ChildRequests model, DogNzbSettings settings)
{
var id = model.ParentRequest.TvDbId;
return await DogNzbApi.AddTvShow(settings.ApiKey, id.ToString());
}
/// <summary>
/// Send the request to Sonarr to process
/// </summary>
/// <param name="s"></param>
/// <param name="model"></param>
/// <param name="qualityId">This is for any qualities overriden from the UI</param>
/// <returns></returns>
public async Task<NewSeries> SendToSonarr(ChildRequests model, string qualityId = null)
{
var s = await SonarrSettings.GetSettingsAsync();
if (!s.Enabled)
{
return null;
}
if (string.IsNullOrEmpty(s.ApiKey))
{
return null;
}
var qualityProfile = 0;
if (!string.IsNullOrEmpty(qualityId)) // try to parse the passed in quality, otherwise use the settings default quality
{
int.TryParse(qualityId, out qualityProfile);
}
if (qualityProfile <= 0)
{
int.TryParse(s.QualityProfile, out qualityProfile);
}
// Get the root path from the rootfolder selected.
// For some reason, if we haven't got one use the first root folder in Sonarr
// TODO make this overrideable via the UI
var rootFolderPath = await GetSonarrRootPath(model.ParentRequest.RootFolder ?? int.Parse(s.RootPath), s);
try
{
// Does the series actually exist?
var allSeries = await SonarrApi.GetSeries(s.ApiKey, s.FullUri);
var existingSeries = allSeries.FirstOrDefault(x => x.tvdbId == model.ParentRequest.TvDbId);
if (existingSeries == null)
{
// Time to add a new one
var newSeries = new NewSeries
{
title = model.ParentRequest.Title,
imdbId = model.ParentRequest.ImdbId,
tvdbId = model.ParentRequest.TvDbId,
cleanTitle = model.ParentRequest.Title,
monitored = true,
seasonFolder = s.SeasonFolders,
rootFolderPath = rootFolderPath,
qualityProfileId = qualityProfile,
titleSlug = model.ParentRequest.Title,
addOptions = new AddOptions
{
ignoreEpisodesWithFiles = true, // There shouldn't be any episodes with files, this is a new season
ignoreEpisodesWithoutFiles = true, // We want all missing
searchForMissingEpisodes = false // we want dont want to search yet. We want to make sure everything is unmonitored/monitored correctly.
}
};
// Montitor the correct seasons,
// If we have that season in the model then it's monitored!
var seasonsToAdd = new List<Season>();
for (var i = 1; i < model.ParentRequest.TotalSeasons + 1; i++)
{
var index = i;
var season = new Season
{
seasonNumber = i,
monitored = model.SeasonRequests.Any(x => x.SeasonNumber == index)
};
seasonsToAdd.Add(season);
}
newSeries.seasons = seasonsToAdd;
var result = await SonarrApi.AddSeries(newSeries, s.ApiKey, s.FullUri);
existingSeries = await SonarrApi.GetSeriesById(result.id, s.ApiKey, s.FullUri);
await SendToSonarr(model, existingSeries, s);
}
else
{
await SendToSonarr(model, existingSeries, s);
}
return new NewSeries
{
id = existingSeries.id,
seasons = existingSeries.seasons.ToList(),
cleanTitle = existingSeries.cleanTitle,
title = existingSeries.title,
tvdbId = existingSeries.tvdbId
};
}
catch (Exception e)
{
Logger.LogError(LoggingEvents.SonarrSender, e, "Exception thrown when attempting to send series over to Sonarr");
throw;
}
}
private async Task SendToSonarr(ChildRequests model, SonarrSeries result, SonarrSettings s)
{
var episodesToUpdate = new List<Episode>();
// Ok, now let's sort out the episodes.
if (model.SeriesType == SeriesType.Anime)
{
result.seriesType = "anime";
await SonarrApi.UpdateSeries(result, s.ApiKey, s.FullUri);
}
var sonarrEpisodes = await SonarrApi.GetEpisodes(result.id, s.ApiKey, s.FullUri);
var sonarrEpList = sonarrEpisodes.ToList() ?? new List<Episode>();
while (!sonarrEpList.Any())
{
// It could be that the series metadata is not ready yet. So wait
sonarrEpList = (await SonarrApi.GetEpisodes(result.id, s.ApiKey, s.FullUri)).ToList();
await Task.Delay(500);
}
foreach (var req in model.SeasonRequests)
{
foreach (var ep in req.Episodes)
{
var sonarrEp = sonarrEpList.FirstOrDefault(x =>
x.episodeNumber == ep.EpisodeNumber && x.seasonNumber == req.SeasonNumber);
if (sonarrEp != null)
{
sonarrEp.monitored = true;
episodesToUpdate.Add(sonarrEp);
}
}
}
var seriesChanges = false;
foreach (var season in model.SeasonRequests)
{
var sonarrSeason = sonarrEpList.Where(x => x.seasonNumber == season.SeasonNumber);
var sonarrEpCount = sonarrSeason.Count();
var ourRequestCount = season.Episodes.Count;
if (sonarrEpCount == ourRequestCount)
{
// We have the same amount of requests as all of the episodes in the season.
var existingSeason =
result.seasons.First(x => x.seasonNumber == season.SeasonNumber);
existingSeason.monitored = true;
seriesChanges = true;
}
else
{
// Now update the episodes that need updating
foreach (var epToUpdate in episodesToUpdate.Where(x => x.seasonNumber == season.SeasonNumber))
{
await SonarrApi.UpdateEpisode(epToUpdate, s.ApiKey, s.FullUri);
}
}
}
if (seriesChanges)
{
await SonarrApi.SeasonPass(s.ApiKey, s.FullUri, result);
}
if (!s.AddOnly)
{
await SearchForRequest(model, sonarrEpList, result, s, episodesToUpdate);
}
}
private async Task<bool> SendToSickRage(ChildRequests model, SickRageSettings settings, string qualityId = null)
{
var tvdbid = model.ParentRequest.TvDbId;
if (qualityId.HasValue())
{
var id = qualityId;
if (settings.Qualities.All(x => x.Value != id))
{
qualityId = settings.QualityProfile;
}
}
else
{
qualityId = settings.QualityProfile;
}
// Check if the show exists
var existingShow = await SickRageApi.GetShow(tvdbid, settings.ApiKey, settings.FullUri);
if (existingShow.message.Equals("Show not found", StringComparison.CurrentCultureIgnoreCase))
{
var addResult = await SickRageApi.AddSeries(model.ParentRequest.TvDbId, qualityId, SickRageStatus.Ignored,
settings.ApiKey, settings.FullUri);
Logger.LogDebug("Added the show (tvdbid) {0}. The result is '{2}' : '{3}'", tvdbid, addResult.result, addResult.message);
if (addResult.result.Equals("failure") || addResult.result.Equals("fatal"))
{
// Do something
return false;
}
}
foreach (var seasonRequests in model.SeasonRequests)
{
var srEpisodes = await SickRageApi.GetEpisodesForSeason(tvdbid, seasonRequests.SeasonNumber, settings.ApiKey, settings.FullUri);
while (srEpisodes.message.Equals("Show not found", StringComparison.CurrentCultureIgnoreCase) && srEpisodes.data.Count <= 0)
{
await Task.Delay(TimeSpan.FromSeconds(1));
srEpisodes = await SickRageApi.GetEpisodesForSeason(tvdbid, seasonRequests.SeasonNumber, settings.ApiKey, settings.FullUri);
}
var totalSrEpisodes = srEpisodes.data.Count;
if (totalSrEpisodes == seasonRequests.Episodes.Count)
{
// This is a request for the whole season
var wholeSeasonResult = await SickRageApi.SetEpisodeStatus(settings.ApiKey, settings.FullUri, tvdbid, SickRageStatus.Wanted,
seasonRequests.SeasonNumber);
Logger.LogDebug("Set the status to Wanted for season {0}. The result is '{1}' : '{2}'", seasonRequests.SeasonNumber, wholeSeasonResult.result, wholeSeasonResult.message);
continue;
}
foreach (var srEp in srEpisodes.data)
{
var epNumber = srEp.Key;
var epData = srEp.Value;
var epRequest = seasonRequests.Episodes.FirstOrDefault(x => x.EpisodeNumber == epNumber);
if (epRequest != null)
{
// We want to monior this episode since we have a request for it
// Let's check to see if it's wanted first, save an api call
if (epData.status.Equals(SickRageStatus.Wanted, StringComparison.CurrentCultureIgnoreCase))
{
continue;
}
var epResult = await SickRageApi.SetEpisodeStatus(settings.ApiKey, settings.FullUri, tvdbid,
SickRageStatus.Wanted, seasonRequests.SeasonNumber, epNumber);
Logger.LogDebug("Set the status to Wanted for Episode {0} in season {1}. The result is '{2}' : '{3}'", seasonRequests.SeasonNumber, epNumber, epResult.result, epResult.message);
}
}
}
return true;
}
private async Task SearchForRequest(ChildRequests model, IEnumerable<Episode> sonarrEpList, SonarrSeries existingSeries, SonarrSettings s,
IReadOnlyCollection<Episode> episodesToUpdate)
{
foreach (var season in model.SeasonRequests)
{
var sonarrSeason = sonarrEpList.Where(x => x.seasonNumber == season.SeasonNumber);
var sonarrEpCount = sonarrSeason.Count();
var ourRequestCount = season.Episodes.Count;
if (sonarrEpCount == ourRequestCount)
{
// We have the same amount of requests as all of the episodes in the season.
// Do a season search
await SonarrApi.SeasonSearch(existingSeries.id, season.SeasonNumber, s.ApiKey, s.FullUri);
}
else
{
// There is a miss-match, let's search the episodes indiviaully
await SonarrApi.EpisodeSearch(episodesToUpdate.Select(x => x.id).ToArray(), s.ApiKey, s.FullUri);
}
}
}
private async Task<string> GetSonarrRootPath(int pathId, SonarrSettings sonarrSettings)
{
var rootFoldersResult = await SonarrApi.GetRootFolders(sonarrSettings.ApiKey, sonarrSettings.FullUri);
if (pathId == 0)
{
return rootFoldersResult.FirstOrDefault().path;
}
foreach (var r in rootFoldersResult.Where(r => r.id == pathId))
{
return r.path;
}
return string.Empty;
}
}
}

@ -33,6 +33,7 @@ using Ombi.Api.DogNzb;
using Ombi.Api.FanartTv;
using Ombi.Api.Github;
using Ombi.Api.Mattermost;
using Ombi.Api.Notifications;
using Ombi.Api.Pushbullet;
using Ombi.Api.Pushover;
using Ombi.Api.Service;
@ -110,6 +111,7 @@ namespace Ombi.DependencyInjection
services.AddTransient<IGithubApi, GithubApi>();
services.AddTransient<ISickRageApi, SickRageApi>();
services.AddTransient<IAppVeyorApi, AppVeyorApi>();
services.AddTransient<IOneSignalApi, OneSignalApi>();
}
public static void RegisterStore(this IServiceCollection services) {

@ -22,6 +22,7 @@
<ProjectReference Include="..\Ombi.Api.FanartTv\Ombi.Api.FanartTv.csproj" />
<ProjectReference Include="..\Ombi.Api.Github\Ombi.Api.Github.csproj" />
<ProjectReference Include="..\Ombi.Api.Mattermost\Ombi.Api.Mattermost.csproj" />
<ProjectReference Include="..\Ombi.Api.Notifications\Ombi.Api.Notifications.csproj" />
<ProjectReference Include="..\Ombi.Api.Plex\Ombi.Api.Plex.csproj" />
<ProjectReference Include="..\Ombi.Api.Pushbullet\Ombi.Api.Pushbullet.csproj" />
<ProjectReference Include="..\Ombi.Api.Pushover\Ombi.Api.Pushover.csproj" />

@ -2,12 +2,13 @@
{
public enum NotificationAgent
{
Email,
Discord,
Pushbullet,
Pushover,
Telegram,
Slack,
Mattermost,
Email = 0,
Discord = 1,
Pushbullet = 2,
Pushover = 3,
Telegram = 4,
Slack = 5,
Mattermost = 6,
Mobile = 7,
}
}

@ -12,5 +12,6 @@
ItemAddedToFaultQueue = 7,
WelcomeEmail = 8,
IssueResolved = 9,
IssueComment = 10,
}
}

@ -17,6 +17,7 @@ namespace Ombi.Mapping.Profiles
CreateMap<MattermostNotificationsViewModel, MattermostNotificationSettings>().ReverseMap();
CreateMap<TelegramNotificationsViewModel, TelegramSettings>().ReverseMap();
CreateMap<UpdateSettingsViewModel, UpdateSettings>().ReverseMap();
CreateMap<MobileNotificationsViewModel, MobileNotificationSettings>().ReverseMap();
}
}
}

@ -18,7 +18,10 @@ namespace Ombi.Notifications.Agents
{
public class DiscordNotification : BaseNotification<DiscordNotificationSettings>, IDiscordNotification
{
public DiscordNotification(IDiscordApi api, ISettingsService<DiscordNotificationSettings> sn, ILogger<DiscordNotification> log, INotificationTemplatesRepository r, IMovieRequestRepository m, ITvRequestRepository t, ISettingsService<CustomizationSettings> s) : base(sn, r, m, t,s)
public DiscordNotification(IDiscordApi api, ISettingsService<DiscordNotificationSettings> sn,
ILogger<DiscordNotification> log, INotificationTemplatesRepository r,
IMovieRequestRepository m, ITvRequestRepository t, ISettingsService<CustomizationSettings> s)
: base(sn, r, m, t,s,log)
{
Api = api;
Logger = log;
@ -84,6 +87,22 @@ namespace Ombi.Notifications.Agents
await Send(notification, settings);
}
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);
}
protected override async Task IssueResolved(NotificationOptions model, DiscordNotificationSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Discord, NotificationType.IssueResolved, model);

@ -1,6 +1,8 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using MailKit.Net.Smtp;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using MimeKit;
using Ombi.Core.Settings;
@ -19,14 +21,16 @@ namespace Ombi.Notifications.Agents
public class EmailNotification : BaseNotification<EmailNotificationSettings>, IEmailNotification
{
public EmailNotification(ISettingsService<EmailNotificationSettings> settings, INotificationTemplatesRepository r, IMovieRequestRepository m, ITvRequestRepository t, IEmailProvider prov, ISettingsService<CustomizationSettings> c,
ILogger<EmailNotification> log) : base(settings, r, m, t, c)
ILogger<EmailNotification> log, UserManager<OmbiUser> um) : base(settings, r, m, t, c, log)
{
EmailProvider = prov;
Logger = log;
_userManager = um;
}
private IEmailProvider EmailProvider { get; }
private ILogger<EmailNotification> Logger { get; }
public override string NotificationName => nameof(EmailNotification);
private readonly UserManager<OmbiUser> _userManager;
protected override bool ValidateConfiguration(EmailNotificationSettings settings)
{
@ -65,9 +69,30 @@ namespace Ombi.Notifications.Agents
{
Message = html,
Subject = parsed.Subject,
To = model.Recipient.HasValue() ? model.Recipient : settings.AdminEmail,
};
if (model.Substitutes.TryGetValue("AdminComment", out var isAdminString))
{
var isAdmin = bool.Parse(isAdminString);
if (isAdmin)
{
var user = _userManager.Users.FirstOrDefault(x => x.Id == model.UserId);
// Send to user
message.To = user.Email;
}
else
{
// Send to admin
message.To = settings.AdminEmail;
}
}
else
{
// Send to admin
message.To = settings.AdminEmail;
}
return message;
}
@ -109,8 +134,37 @@ namespace Ombi.Notifications.Agents
await Send(message, settings);
}
protected override async Task IssueComment(NotificationOptions model, EmailNotificationSettings settings)
{
var message = await LoadTemplate(NotificationType.IssueComment, model, settings);
if (message == null)
{
return;
}
var plaintext = await LoadPlainTextMessage(NotificationType.IssueComment, model, settings);
message.Other.Add("PlainTextBody", plaintext);
if (model.Substitutes.TryGetValue("AdminComment", out var isAdminString))
{
var isAdmin = bool.Parse(isAdminString);
message.To = isAdmin ? model.Recipient : settings.AdminEmail;
}
else
{
message.To = model.Recipient;
}
await Send(message, settings);
}
protected override async Task IssueResolved(NotificationOptions model, EmailNotificationSettings settings)
{
if (!model.Recipient.HasValue())
{
return;
}
var message = await LoadTemplate(NotificationType.IssueResolved, model, settings);
if (message == null)
{
@ -120,9 +174,9 @@ namespace Ombi.Notifications.Agents
var plaintext = await LoadPlainTextMessage(NotificationType.IssueResolved, model, settings);
message.Other.Add("PlainTextBody", plaintext);
// Issues should be sent to admin
message.To = settings.AdminEmail;
// Issues resolved should be sent to the user
message.To = model.Recipient;
await Send(message, settings);
}

@ -21,7 +21,7 @@ namespace Ombi.Notifications.Agents
public class MattermostNotification : BaseNotification<MattermostNotificationSettings>, IMattermostNotification
{
public MattermostNotification(IMattermostApi api, ISettingsService<MattermostNotificationSettings> sn, ILogger<MattermostNotification> log, INotificationTemplatesRepository r, IMovieRequestRepository m, ITvRequestRepository t,
ISettingsService<CustomizationSettings> s) : base(sn, r, m, t,s)
ISettingsService<CustomizationSettings> s) : base(sn, r, m, t,s,log)
{
Api = api;
Logger = log;
@ -79,6 +79,22 @@ namespace Ombi.Notifications.Agents
await Send(notification, settings);
}
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,
};
notification.Other.Add("image", parsed.Image);
await Send(notification, settings);
}
protected override async Task IssueResolved(NotificationOptions model, MattermostNotificationSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Mattermost, NotificationType.IssueResolved, model);

@ -0,0 +1,274 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Ombi.Api.Notifications;
using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Notifications.Interfaces;
using Ombi.Notifications.Models;
using Ombi.Settings.Settings.Models;
using Ombi.Settings.Settings.Models.Notifications;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
using Ombi.Store.Repository.Requests;
namespace Ombi.Notifications.Agents
{
public class MobileNotification : BaseNotification<MobileNotificationSettings>
{
public MobileNotification(IOneSignalApi api, ISettingsService<MobileNotificationSettings> sn, ILogger<MobileNotification> log, INotificationTemplatesRepository r,
IMovieRequestRepository m, ITvRequestRepository t, ISettingsService<CustomizationSettings> s, IRepository<NotificationUserId> notification,
UserManager<OmbiUser> um) : base(sn, r, m, t, s,log)
{
_api = api;
_logger = log;
_notifications = notification;
_userManager = um;
}
public override string NotificationName => "MobileNotification";
private readonly IOneSignalApi _api;
private readonly ILogger<MobileNotification> _logger;
private readonly IRepository<NotificationUserId> _notifications;
private readonly UserManager<OmbiUser> _userManager;
protected override bool ValidateConfiguration(MobileNotificationSettings settings)
{
return false;
}
protected override async Task NewRequest(NotificationOptions model, MobileNotificationSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Mobile, NotificationType.NewRequest, model);
if (parsed.Disabled)
{
_logger.LogInformation($"Template {NotificationType.NewRequest} is disabled for {NotificationAgent.Mobile}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
// Get admin devices
var playerIds = await GetAdmins(NotificationType.NewRequest);
await Send(playerIds, notification, settings);
}
protected override async Task NewIssue(NotificationOptions model, MobileNotificationSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Mobile, NotificationType.Issue, model);
if (parsed.Disabled)
{
_logger.LogInformation($"Template {NotificationType.Issue} is disabled for {NotificationAgent.Mobile}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
// Get admin devices
var playerIds = await GetAdmins(NotificationType.Issue);
await Send(playerIds, notification, settings);
}
protected override async Task IssueComment(NotificationOptions model, MobileNotificationSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Mobile, NotificationType.IssueComment, model);
if (parsed.Disabled)
{
_logger.LogInformation($"Template {NotificationType.IssueComment} is disabled for {NotificationAgent.Mobile}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
if (model.Substitutes.TryGetValue("AdminComment", out var isAdminString))
{
var isAdmin = bool.Parse(isAdminString);
if (isAdmin)
{
// Send to user
var playerIds = GetUsers(model, NotificationType.IssueComment);
await Send(playerIds, notification, settings);
}
else
{
// Send to admin
var playerIds = await GetAdmins(NotificationType.IssueComment);
await Send(playerIds, notification, settings);
}
}
}
protected override async Task IssueResolved(NotificationOptions model, MobileNotificationSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Mobile, NotificationType.IssueResolved, model);
if (parsed.Disabled)
{
_logger.LogInformation($"Template {NotificationType.IssueResolved} is disabled for {NotificationAgent.Mobile}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
// Send to user
var playerIds = GetUsers(model, NotificationType.IssueResolved);
await Send(playerIds, notification, settings);
}
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
{
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
};
// Get admin devices
var playerIds = await GetAdmins(NotificationType.Test);
await Send(playerIds, notification, settings);
}
protected override async Task RequestDeclined(NotificationOptions model, MobileNotificationSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Mobile, NotificationType.RequestDeclined, model);
if (parsed.Disabled)
{
_logger.LogInformation($"Template {NotificationType.RequestDeclined} is disabled for {NotificationAgent.Mobile}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
// Send to user
var playerIds = GetUsers(model, NotificationType.RequestDeclined);
await Send(playerIds, notification, settings);
}
protected override async Task RequestApproved(NotificationOptions model, MobileNotificationSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Mobile, NotificationType.RequestApproved, model);
if (parsed.Disabled)
{
_logger.LogInformation($"Template {NotificationType.RequestApproved} is disabled for {NotificationAgent.Mobile}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
// Send to user
var playerIds = GetUsers(model, NotificationType.RequestApproved);
await Send(playerIds, notification, settings);
}
protected override async Task AvailableRequest(NotificationOptions model, MobileNotificationSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Mobile, NotificationType.RequestAvailable, model);
if (parsed.Disabled)
{
_logger.LogInformation($"Template {NotificationType.RequestAvailable} is disabled for {NotificationAgent.Mobile}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
// Send to user
var playerIds = GetUsers(model, NotificationType.RequestAvailable);
await Send(playerIds, notification, settings);
}
protected override Task Send(NotificationMessage model, MobileNotificationSettings settings)
{
throw new NotImplementedException();
}
protected async Task Send(List<string> playerIds, NotificationMessage model, MobileNotificationSettings settings)
{
if (!playerIds.Any())
{
return;
}
var response = await _api.PushNotification(playerIds, model.Message);
_logger.LogDebug("Sent message to {0} recipients with message id {1}", response.recipients, response.id);
}
protected override async Task Test(NotificationOptions model, MobileNotificationSettings settings)
{
var message = $"This is a test from Ombi, if you can see this then we have successfully pushed a notification!";
var notification = new NotificationMessage
{
Message = message,
};
// Send to user
var playerIds = await GetAdmins(NotificationType.RequestAvailable);
await Send(playerIds, notification, settings);
}
private async Task<List<string>> GetAdmins(NotificationType type)
{
var adminUsers = (await _userManager.GetUsersInRoleAsync(OmbiRoles.Admin)).Select(x => x.Id).ToList();
var notificationUsers = _notifications.GetAll().Include(x => x.User).Where(x => adminUsers.Contains(x.UserId));
var playerIds = await notificationUsers.Select(x => x.PlayerId).ToListAsync();
if (!playerIds.Any())
{
_logger.LogInformation(
$"there are no admins to send a notification for {type}, for agent {NotificationAgent.Mobile}");
return null;
}
return playerIds;
}
private List<string> GetUsers(NotificationOptions model, NotificationType type)
{
var notificationIds = new List<NotificationUserId>();
if (MovieRequest != null || TvRequest != null)
{
notificationIds = model.RequestType == RequestType.Movie
? MovieRequest?.RequestedUser?.NotificationUserIds
: TvRequest?.RequestedUser?.NotificationUserIds;
}
if (model.UserId.HasValue() && !notificationIds.Any())
{
var user= _userManager.Users.Include(x => x.NotificationUserIds).FirstOrDefault(x => x.Id == model.UserId);
notificationIds = user.NotificationUserIds;
}
if (!notificationIds.Any())
{
_logger.LogInformation(
$"there are no admins to send a notification for {type}, for agent {NotificationAgent.Mobile}");
return null;
}
var playerIds = notificationIds.Select(x => x.PlayerId).ToList();
return playerIds;
}
}
}

@ -17,7 +17,7 @@ namespace Ombi.Notifications.Agents
public class PushbulletNotification : BaseNotification<PushbulletSettings>, IPushbulletNotification
{
public PushbulletNotification(IPushbulletApi api, ISettingsService<PushbulletSettings> sn, ILogger<PushbulletNotification> log, INotificationTemplatesRepository r, IMovieRequestRepository m, ITvRequestRepository t,
ISettingsService<CustomizationSettings> s) : base(sn, r, m, t,s)
ISettingsService<CustomizationSettings> s) : base(sn, r, m, t,s,log)
{
Api = api;
Logger = log;
@ -73,6 +73,21 @@ namespace Ombi.Notifications.Agents
await Send(notification, settings);
}
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);
}
protected override async Task IssueResolved(NotificationOptions model, PushbulletSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Pushbullet, NotificationType.IssueResolved, model);

@ -17,8 +17,8 @@ namespace Ombi.Notifications.Agents
{
public class PushoverNotification : BaseNotification<PushoverSettings>, IPushoverNotification
{
public PushoverNotification(IPushoverApi api, ISettingsService<PushoverSettings> sn, ILogger<PushbulletNotification> log, INotificationTemplatesRepository r, IMovieRequestRepository m, ITvRequestRepository t,
ISettingsService<CustomizationSettings> s) : base(sn, r, m, t, s)
public PushoverNotification(IPushoverApi api, ISettingsService<PushoverSettings> sn, ILogger<PushoverNotification> log, INotificationTemplatesRepository r, IMovieRequestRepository m, ITvRequestRepository t,
ISettingsService<CustomizationSettings> s) : base(sn, r, m, t, s, log)
{
Api = api;
Logger = log;
@ -27,7 +27,7 @@ namespace Ombi.Notifications.Agents
public override string NotificationName => "PushoverNotification";
private IPushoverApi Api { get; }
private ILogger<PushbulletNotification> Logger { get; }
private ILogger<PushoverNotification> Logger { get; }
protected override bool ValidateConfiguration(PushoverSettings settings)
{
@ -74,6 +74,21 @@ namespace Ombi.Notifications.Agents
await Send(notification, settings);
}
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);
}
protected override async Task IssueResolved(NotificationOptions model, PushoverSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Pushover, NotificationType.IssueResolved, model);

@ -18,7 +18,7 @@ namespace Ombi.Notifications.Agents
public class SlackNotification : BaseNotification<SlackNotificationSettings>, ISlackNotification
{
public SlackNotification(ISlackApi api, ISettingsService<SlackNotificationSettings> sn, ILogger<SlackNotification> log, INotificationTemplatesRepository r, IMovieRequestRepository m, ITvRequestRepository t,
ISettingsService<CustomizationSettings> s) : base(sn, r, m, t, s)
ISettingsService<CustomizationSettings> s) : base(sn, r, m, t, s, log)
{
Api = api;
Logger = log;
@ -85,6 +85,22 @@ namespace Ombi.Notifications.Agents
await Send(notification, settings);
}
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);
}
protected override async Task IssueResolved(NotificationOptions model, SlackNotificationSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Slack, NotificationType.IssueResolved, model);

@ -16,7 +16,9 @@ namespace Ombi.Notifications.Agents
{
public class TelegramNotification : BaseNotification<TelegramSettings>, ITelegramNotification
{
public TelegramNotification(ITelegramApi api, ISettingsService<TelegramSettings> sn, ILogger<TelegramNotification> log, INotificationTemplatesRepository r, IMovieRequestRepository m, ITvRequestRepository t, ISettingsService<CustomizationSettings> s) : base(sn, r, m, t,s)
public TelegramNotification(ITelegramApi api, ISettingsService<TelegramSettings> sn, ILogger<TelegramNotification> log,
INotificationTemplatesRepository r, IMovieRequestRepository m,
ITvRequestRepository t, ISettingsService<CustomizationSettings> s) : base(sn, r, m, t,s,log)
{
Api = api;
Logger = log;
@ -67,6 +69,21 @@ namespace Ombi.Notifications.Agents
await Send(notification, settings);
}
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);
}
protected override async Task IssueResolved(NotificationOptions model, TelegramSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Telegram, NotificationType.IssueResolved, model);

@ -1,6 +1,7 @@
using System;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Notifications.Exceptions;
@ -16,7 +17,7 @@ namespace Ombi.Notifications.Interfaces
public abstract class BaseNotification<T> : INotification where T : Settings.Settings.Models.Settings, new()
{
protected BaseNotification(ISettingsService<T> settings, INotificationTemplatesRepository templateRepo, IMovieRequestRepository movie, ITvRequestRepository tv,
ISettingsService<CustomizationSettings> customization)
ISettingsService<CustomizationSettings> customization, ILogger<BaseNotification<T>> log)
{
Settings = settings;
TemplateRepository = templateRepo;
@ -25,6 +26,7 @@ namespace Ombi.Notifications.Interfaces
CustomizationSettings = customization;
Settings.ClearCache();
CustomizationSettings.ClearCache();
_log = log;
}
protected ISettingsService<T> Settings { get; }
@ -33,6 +35,7 @@ namespace Ombi.Notifications.Interfaces
protected ITvRequestRepository TvRepository { get; }
protected CustomizationSettings Customization { get; set; }
private ISettingsService<CustomizationSettings> CustomizationSettings { get; }
private readonly ILogger<BaseNotification<T>> _log;
protected ChildRequests TvRequest { get; set; }
@ -96,6 +99,9 @@ namespace Ombi.Notifications.Interfaces
case NotificationType.IssueResolved:
await IssueResolved(model, notificationSettings);
break;
case NotificationType.IssueComment:
await IssueComment(model, notificationSettings);
break;
default:
throw new ArgumentOutOfRangeException();
}
@ -159,10 +165,13 @@ namespace Ombi.Notifications.Interfaces
var curlys = new NotificationMessageCurlys();
if (model.RequestType == RequestType.Movie)
{
_log.LogDebug("Notification options: {@model}, Req: {@MovieRequest}, Settings: {@Customization}", model, MovieRequest, Customization);
curlys.Setup(model, MovieRequest, Customization);
}
else
{
_log.LogDebug("Notification options: {@model}, Req: {@TvRequest}, Settings: {@Customization}", model, TvRequest, Customization);
curlys.Setup(model, TvRequest, Customization);
}
var parsed = resolver.ParseMessage(template, curlys);
@ -174,6 +183,7 @@ namespace Ombi.Notifications.Interfaces
protected abstract bool ValidateConfiguration(T settings);
protected abstract Task NewRequest(NotificationOptions model, T settings);
protected abstract Task NewIssue(NotificationOptions model, T settings);
protected abstract Task IssueComment(NotificationOptions model, T settings);
protected abstract Task IssueResolved(NotificationOptions model, T settings);
protected abstract Task AddedToRequestQueue(NotificationOptions model, T settings);
protected abstract Task RequestDeclined(NotificationOptions model, T settings);

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using Ombi.Helpers;
using Ombi.Store;
using Ombi.Store.Entities;
@ -13,6 +14,9 @@ namespace Ombi.Notifications.Models
public RequestType RequestType { get; set; }
public string Recipient { get; set; }
public string AdditionalInformation { get; set; }
public string UserId { get; set; }
public Dictionary<string,string> Substitutes { get; set; } = new Dictionary<string, string>();
}
}

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using Ombi.Helpers;
using Ombi.Notifications.Models;
using Ombi.Settings.Settings.Models;
using Ombi.Store.Entities;
@ -12,46 +13,77 @@ namespace Ombi.Notifications
public void Setup(NotificationOptions opts, FullBaseRequest req, CustomizationSettings s)
{
ApplicationUrl = s?.ApplicationUrl;
LoadIssues(opts);
string title;
if (req == null)
{
opts.Substitutes.TryGetValue("Title", out title);
}
else
{
title = req?.Title;
}
ApplicationUrl = (s?.ApplicationUrl.HasValue() ?? false) ? s.ApplicationUrl : string.Empty;
ApplicationName = string.IsNullOrEmpty(s?.ApplicationName) ? "Ombi" : s?.ApplicationName;
RequestedUser = string.IsNullOrEmpty(req.RequestedUser.Alias)
? req.RequestedUser.UserName
: req.RequestedUser.Alias;
Title = req.Title;
RequestedDate = req.RequestedDate.ToString("D");
Type = req.RequestType.ToString();
Overview = req.Overview;
Year = req.ReleaseDate.Year.ToString();
PosterImage = req.RequestType == RequestType.Movie ?
$"https://image.tmdb.org/t/p/w300{req.PosterPath}" : req.PosterPath;
AdditionalInformation = opts.AdditionalInformation;
RequestedUser = string.IsNullOrEmpty(req?.RequestedUser?.Alias)
? req?.RequestedUser?.UserName
: req?.RequestedUser?.Alias;
Title = title;
RequestedDate = req?.RequestedDate.ToString("D");
Type = req?.RequestType.ToString();
Overview = req?.Overview;
Year = req?.ReleaseDate.Year.ToString();
PosterImage = req?.RequestType == RequestType.Movie ?
string.Format("https://image.tmdb.org/t/p/w300{0}", req?.PosterPath) : req?.PosterPath;
AdditionalInformation = opts?.AdditionalInformation ?? string.Empty;
}
public void Setup(NotificationOptions opts, ChildRequests req, CustomizationSettings s)
{
ApplicationUrl = s?.ApplicationUrl;
LoadIssues(opts);
string title;
if (req == null)
{
opts.Substitutes.TryGetValue("Title", out title);
}
else
{
title = req?.ParentRequest.Title;
}
ApplicationUrl = (s?.ApplicationUrl.HasValue() ?? false) ? s.ApplicationUrl : string.Empty;
ApplicationName = string.IsNullOrEmpty(s?.ApplicationName) ? "Ombi" : s?.ApplicationName;
RequestedUser = string.IsNullOrEmpty(req.RequestedUser.Alias)
? req.RequestedUser.UserName
: req.RequestedUser.Alias;
Title = req.ParentRequest.Title;
RequestedDate = req.RequestedDate.ToString("D");
Type = req.RequestType.ToString();
Overview = req.ParentRequest.Overview;
Year = req.ParentRequest.ReleaseDate.Year.ToString();
PosterImage = req.RequestType == RequestType.Movie ?
$"https://image.tmdb.org/t/p/w300{req.ParentRequest.PosterPath}" : req.ParentRequest.PosterPath;
RequestedUser = string.IsNullOrEmpty(req?.RequestedUser.Alias)
? req?.RequestedUser.UserName
: req?.RequestedUser.Alias;
Title = title;
RequestedDate = req?.RequestedDate.ToString("D");
Type = req?.RequestType.ToString();
Overview = req?.ParentRequest.Overview;
Year = req?.ParentRequest.ReleaseDate.Year.ToString();
PosterImage = req?.RequestType == RequestType.Movie ?
$"https://image.tmdb.org/t/p/w300{req?.ParentRequest.PosterPath}" : req?.ParentRequest.PosterPath;
AdditionalInformation = opts.AdditionalInformation;
// DO Episode and Season Lists
}
public void Setup(OmbiUser user, CustomizationSettings s)
{
ApplicationUrl = s?.ApplicationUrl;
ApplicationUrl = (s?.ApplicationUrl.HasValue() ?? false) ? s.ApplicationUrl : string.Empty;
ApplicationName = string.IsNullOrEmpty(s?.ApplicationName) ? "Ombi" : s?.ApplicationName;
RequestedUser = user.UserName;
}
private void LoadIssues(NotificationOptions opts)
{
var val = string.Empty;
IssueDescription = opts.Substitutes.TryGetValue("IssueDescription", out val) ? val : string.Empty;
IssueCategory = opts.Substitutes.TryGetValue("IssueCategory", out val) ? val : string.Empty;
IssueStatus = opts.Substitutes.TryGetValue("IssueStatus", out val) ? val : string.Empty;
IssueSubject = opts.Substitutes.TryGetValue("IssueSubject", out val) ? val : string.Empty;
NewIssueComment = opts.Substitutes.TryGetValue("NewIssueComment", out val) ? val : string.Empty;
IssueUser = opts.Substitutes.TryGetValue("IssueUser", out val) ? val : string.Empty;
}
// User Defined
public string RequestedUser { get; set; }
public string Title { get; set; }
@ -65,6 +97,12 @@ namespace Ombi.Notifications
public string PosterImage { get; set; }
public string ApplicationName { get; set; }
public string ApplicationUrl { get; set; }
public string IssueDescription { get; set; }
public string IssueCategory { get; set; }
public string IssueStatus { get; set; }
public string IssueSubject { get; set; }
public string NewIssueComment { get; set; }
public string IssueUser { get; set; }
// System Defined
private string LongDate => DateTime.Now.ToString("D");
@ -90,6 +128,12 @@ namespace Ombi.Notifications
{nameof(PosterImage),PosterImage},
{nameof(ApplicationName),ApplicationName},
{nameof(ApplicationUrl),ApplicationUrl},
{nameof(IssueDescription),IssueDescription},
{nameof(IssueCategory),IssueCategory},
{nameof(IssueStatus),IssueStatus},
{nameof(IssueSubject),IssueSubject},
{nameof(NewIssueComment),NewIssueComment},
{nameof(IssueUser),IssueUser},
};
}
}

@ -16,6 +16,7 @@
<ItemGroup>
<ProjectReference Include="..\Ombi.Api.Discord\Ombi.Api.Discord.csproj" />
<ProjectReference Include="..\Ombi.Api.Mattermost\Ombi.Api.Mattermost.csproj" />
<ProjectReference Include="..\Ombi.Api.Notifications\Ombi.Api.Notifications.csproj" />
<ProjectReference Include="..\Ombi.Api.Pushbullet\Ombi.Api.Pushbullet.csproj" />
<ProjectReference Include="..\Ombi.Api.Pushover\Ombi.Api.Pushover.csproj" />
<ProjectReference Include="..\Ombi.Api.Slack\Ombi.Api.Slack.csproj" />

@ -196,11 +196,14 @@ namespace Ombi.Schedule.Jobs.Ombi
var updaterFile = Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location),
"TempUpdate", $"Ombi.Updater{updaterExtension}");
// Make sure the file is an executable
ExecLinuxCommand($"chmod +x {updaterFile}");
// There must be an update
var start = new ProcessStartInfo
{
UseShellExecute = false,
CreateNoWindow = false,
UseShellExecute = true,
CreateNoWindow = false, // Ignored if UseShellExecute is set to true
FileName = updaterFile,
Arguments = GetArgs(settings),
WorkingDirectory = Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), "TempUpdate"),
@ -244,24 +247,16 @@ namespace Ombi.Schedule.Jobs.Ombi
sb.Append($"--windowsServiceName \"{settings.WindowsServiceName}\" ");
}
var sb2 = new StringBuilder();
var hasStartupArgs = false;
if (url?.Value.HasValue() ?? false)
{
hasStartupArgs = true;
sb2.Append(url.Value);
sb2.Append($" --host {url.Value}");
}
if (storage?.Value.HasValue() ?? false)
{
hasStartupArgs = true;
sb2.Append(storage.Value);
}
if (hasStartupArgs)
{
sb.Append($"--startupArgs {sb2.ToString()}");
sb2.Append($" --storage {storage.Value}");
}
return sb.ToString();
//return string.Join(" ", currentLocation, processName, url?.Value ?? string.Empty, storage?.Value ?? string.Empty);
}
private void RunScript(UpdateSettings settings, string downloadUrl)
@ -367,5 +362,30 @@ namespace Ombi.Schedule.Jobs.Ombi
Directory.Delete(path, true);
}
}
public static void ExecLinuxCommand(string cmd)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return;
}
var escapedArgs = cmd.Replace("\"", "\\\"");
var process = new Process
{
StartInfo = new ProcessStartInfo
{
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden,
FileName = "/bin/bash",
Arguments = $"-c \"{escapedArgs}\""
}
};
process.Start();
process.WaitForExit();
}
}
}

@ -1,167 +1,177 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Hangfire;
using Microsoft.EntityFrameworkCore;
using Ombi.Core.Notifications;
using Ombi.Helpers;
using Ombi.Notifications.Models;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
using Ombi.Store.Repository.Requests;
namespace Ombi.Schedule.Jobs.Plex
{
public class PlexAvailabilityChecker : IPlexAvailabilityChecker
{
public PlexAvailabilityChecker(IPlexContentRepository repo, ITvRequestRepository tvRequest, IMovieRequestRepository movies,
INotificationService notification, IBackgroundJobClient background)
{
_tvRepo = tvRequest;
_repo = repo;
_movieRepo = movies;
_notificationService = notification;
_backgroundJobClient = background;
}
private readonly ITvRequestRepository _tvRepo;
private readonly IMovieRequestRepository _movieRepo;
private readonly IPlexContentRepository _repo;
private readonly INotificationService _notificationService;
private readonly IBackgroundJobClient _backgroundJobClient;
public async Task Start()
{
await ProcessMovies();
await ProcessTv();
}
private async Task ProcessTv()
{
var tv = _tvRepo.GetChild().Where(x => !x.Available);
var plexEpisodes = _repo.GetAllEpisodes().Include(x => x.Series);
foreach (var child in tv)
{
var useImdb = false;
var useTvDb = false;
if (child.ParentRequest.ImdbId.HasValue())
{
useImdb = true;
}
if (child.ParentRequest.TvDbId.ToString().HasValue())
{
useTvDb = true;
}
var tvDbId = child.ParentRequest.TvDbId;
var imdbId = child.ParentRequest.ImdbId;
IQueryable<PlexEpisode> seriesEpisodes = null;
if (useImdb)
{
seriesEpisodes = plexEpisodes.Where(x => x.Series.ImdbId == imdbId.ToString());
}
if (useTvDb)
{
seriesEpisodes = plexEpisodes.Where(x => x.Series.TvDbId == tvDbId.ToString());
}
foreach (var season in child.SeasonRequests)
{
foreach (var episode in season.Episodes)
{
var foundEp = await seriesEpisodes.FirstOrDefaultAsync(
x => x.EpisodeNumber == episode.EpisodeNumber &&
x.SeasonNumber == episode.Season.SeasonNumber);
if (foundEp != null)
{
episode.Available = true;
}
}
}
// Check to see if all of the episodes in all seasons are available for this request
var allAvailable = child.SeasonRequests.All(x => x.Episodes.All(c => c.Available));
if (allAvailable)
{
// We have fulfulled this request!
child.Available = true;
_backgroundJobClient.Enqueue(() => _notificationService.Publish(new NotificationOptions
{
DateTime = DateTime.Now,
NotificationType = NotificationType.RequestAvailable,
RequestId = child.ParentRequestId,
RequestType = RequestType.TvShow,
Recipient = child.RequestedUser.Email
}));
}
}
await _tvRepo.Save();
}
private async Task ProcessMovies()
{
// Get all non available
var movies = _movieRepo.GetAll().Include(x => x.RequestedUser).Where(x => !x.Available);
foreach (var movie in movies)
{
PlexServerContent item = null;
if (movie.ImdbId.HasValue())
{
item = await _repo.Get(movie.ImdbId);
}
if (item == null)
{
if (movie.TheMovieDbId.ToString().HasValue())
{
item = await _repo.Get(movie.TheMovieDbId.ToString());
}
}
if (item == null)
{
// We don't yet have this
continue;
}
movie.Available = true;
if (movie.Available)
{
_backgroundJobClient.Enqueue(() => _notificationService.Publish(new NotificationOptions
{
DateTime = DateTime.Now,
NotificationType = NotificationType.RequestAvailable,
RequestId = movie.Id,
RequestType = RequestType.Movie,
Recipient = movie.RequestedUser != null ? movie.RequestedUser.Email : string.Empty
}));
}
}
await _movieRepo.Save();
}
private bool _disposed;
protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;
if (disposing)
{
_movieRepo?.Dispose();
_repo?.Dispose();
}
_disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Hangfire;
using Microsoft.EntityFrameworkCore;
using Ombi.Core.Notifications;
using Ombi.Helpers;
using Ombi.Notifications.Models;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
using Ombi.Store.Repository.Requests;
namespace Ombi.Schedule.Jobs.Plex
{
public class PlexAvailabilityChecker : IPlexAvailabilityChecker
{
public PlexAvailabilityChecker(IPlexContentRepository repo, ITvRequestRepository tvRequest, IMovieRequestRepository movies,
INotificationService notification, IBackgroundJobClient background)
{
_tvRepo = tvRequest;
_repo = repo;
_movieRepo = movies;
_notificationService = notification;
_backgroundJobClient = background;
}
private readonly ITvRequestRepository _tvRepo;
private readonly IMovieRequestRepository _movieRepo;
private readonly IPlexContentRepository _repo;
private readonly INotificationService _notificationService;
private readonly IBackgroundJobClient _backgroundJobClient;
public async Task Start()
{
await ProcessMovies();
await ProcessTv();
}
private async Task ProcessTv()
{
var tv = _tvRepo.GetChild().Where(x => !x.Available);
var plexEpisodes = _repo.GetAllEpisodes().Include(x => x.Series);
foreach (var child in tv)
{
var useImdb = false;
var useTvDb = false;
if (child.ParentRequest.ImdbId.HasValue())
{
useImdb = true;
}
if (child.ParentRequest.TvDbId.ToString().HasValue())
{
useTvDb = true;
}
var tvDbId = child.ParentRequest.TvDbId;
var imdbId = child.ParentRequest.ImdbId;
IQueryable<PlexEpisode> seriesEpisodes = null;
if (useImdb)
{
seriesEpisodes = plexEpisodes.Where(x => x.Series.ImdbId == imdbId.ToString());
}
if (useTvDb)
{
seriesEpisodes = plexEpisodes.Where(x => x.Series.TvDbId == tvDbId.ToString());
}
if (!seriesEpisodes.Any())
{
// Let's try and match the series by name
seriesEpisodes = plexEpisodes.Where(x =>
x.Series.Title.Equals(child.Title, StringComparison.CurrentCultureIgnoreCase) &&
x.Series.ReleaseYear == child.ParentRequest.ReleaseDate.Year.ToString());
}
foreach (var season in child.SeasonRequests)
{
foreach (var episode in season.Episodes)
{
var foundEp = await seriesEpisodes.FirstOrDefaultAsync(
x => x.EpisodeNumber == episode.EpisodeNumber &&
x.SeasonNumber == episode.Season.SeasonNumber);
if (foundEp != null)
{
episode.Available = true;
}
}
}
// Check to see if all of the episodes in all seasons are available for this request
var allAvailable = child.SeasonRequests.All(x => x.Episodes.All(c => c.Available));
if (allAvailable)
{
// We have fulfulled this request!
child.Available = true;
_backgroundJobClient.Enqueue(() => _notificationService.Publish(new NotificationOptions
{
DateTime = DateTime.Now,
NotificationType = NotificationType.RequestAvailable,
RequestId = child.ParentRequestId,
RequestType = RequestType.TvShow,
Recipient = child.RequestedUser.Email
}));
}
}
await _tvRepo.Save();
}
private async Task ProcessMovies()
{
// Get all non available
var movies = _movieRepo.GetAll().Include(x => x.RequestedUser).Where(x => !x.Available);
foreach (var movie in movies)
{
PlexServerContent item = null;
if (movie.ImdbId.HasValue())
{
item = await _repo.Get(movie.ImdbId);
}
if (item == null)
{
if (movie.TheMovieDbId.ToString().HasValue())
{
item = await _repo.Get(movie.TheMovieDbId.ToString());
}
}
if (item == null)
{
// We don't yet have this
continue;
}
movie.Available = true;
if (movie.Available)
{
_backgroundJobClient.Enqueue(() => _notificationService.Publish(new NotificationOptions
{
DateTime = DateTime.Now,
NotificationType = NotificationType.RequestAvailable,
RequestId = movie.Id,
RequestType = RequestType.Movie,
Recipient = movie.RequestedUser != null ? movie.RequestedUser.Email : string.Empty
}));
}
}
await _movieRepo.Save();
}
private bool _disposed;
protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;
if (disposing)
{
_movieRepo?.Dispose();
_repo?.Dispose();
}
_disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
}

@ -30,6 +30,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Hangfire;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Ombi.Api.Plex;
using Ombi.Api.Plex.Models;
@ -81,7 +82,7 @@ namespace Ombi.Schedule.Jobs.Plex
}
catch (Exception e)
{
Logger.LogWarning(LoggingEvents.Cacher, e, "Exception thrown when attempting to cache the Plex Content");
Logger.LogWarning(LoggingEvents.PlexContentCacher, e, "Exception thrown when attempting to cache the Plex Content");
}
Logger.LogInformation("Starting EP Cacher");
@ -123,9 +124,39 @@ namespace Ombi.Schedule.Jobs.Plex
// Do we already have this item?
// Let's try and match
var existingContent = await Repo.GetFirstContentByCustom(x => x.Title == show.title
var existingContent = await Repo.GetFirstContentByCustom(x => x.Title == show.title
&& x.ReleaseYear == show.year.ToString()
&& x.Type == PlexMediaTypeEntity.Show);
if (existingContent != null)
{
// Just check the key
var existingKey = await Repo.GetByKey(show.ratingKey);
if (existingKey != null)
{
// The rating key is all good!
}
else
{
// This means the rating key has changed somehow.
// Should probably delete this and get the new one
var oldKey = existingContent.Key;
Repo.DeleteWithoutSave(existingContent);
// Because we have changed the rating key, we need to change all children too
var episodeToChange = Repo.GetAllEpisodes().Where(x => x.GrandparentKey == oldKey);
if (episodeToChange.Any())
{
foreach (var e in episodeToChange)
{
Repo.DeleteWithoutSave(e);
}
}
await Repo.SaveChangesAsync();
existingContent = null;
}
}
// The ratingKey keeps changing...
//var existingContent = await Repo.GetByKey(show.ratingKey);
if (existingContent != null)
@ -153,46 +184,70 @@ namespace Ombi.Schedule.Jobs.Plex
}
catch (Exception e)
{
Logger.LogError(LoggingEvents.PlexContentCacher, e, "Exception when adding new seasons to title {0}", existingContent.Title);
Logger.LogError(LoggingEvents.PlexContentCacher, e, "Exception when adding new seasons to title {0}", existingContent.Title);
}
}
else
{
try
{
Logger.LogInformation("New show {0}, so add it", show.title);
Logger.LogInformation("New show {0}, so add it", show.title);
// Get the show metadata... This sucks since the `metadata` var contains all information about the show
// But it does not contain the `guid` property that we need to pull out thetvdb id...
var showMetadata = await PlexApi.GetMetadata(servers.PlexAuthToken, servers.FullUri,
show.ratingKey);
var providerIds = PlexHelper.GetProviderIdFromPlexGuid(showMetadata.MediaContainer.Metadata.FirstOrDefault().guid);
// Get the show metadata... This sucks since the `metadata` var contains all information about the show
// But it does not contain the `guid` property that we need to pull out thetvdb id...
var showMetadata = await PlexApi.GetMetadata(servers.PlexAuthToken, servers.FullUri,
show.ratingKey);
var providerIds = PlexHelper.GetProviderIdFromPlexGuid(showMetadata.MediaContainer.Metadata.FirstOrDefault().guid);
var item = new PlexServerContent
{
AddedAt = DateTime.Now,
Key = show.ratingKey,
ReleaseYear = show.year.ToString(),
Type = PlexMediaTypeEntity.Show,
Title = show.title,
Url = PlexHelper.GetPlexMediaUrl(servers.MachineIdentifier, show.ratingKey),
Seasons = new List<PlexSeasonsContent>()
};
if (providerIds.Type == ProviderType.ImdbId)
{
item.ImdbId = providerIds.ImdbId;
}
if (providerIds.Type == ProviderType.TheMovieDbId)
{
item.TheMovieDbId = providerIds.TheMovieDb;
}
if (providerIds.Type == ProviderType.TvDbId)
{
item.TvDbId = providerIds.TheTvDb;
}
// Let's just double check to make sure we do not have it now we have some id's
var existingImdb = false;
var existingMovieDbId = false;
var existingTvDbId = false;
existingImdb = await Repo.GetAll().AnyAsync(x => x.ImdbId == item.ImdbId && x.Type == PlexMediaTypeEntity.Show);
existingMovieDbId = await Repo.GetAll().AnyAsync(x => x.TheMovieDbId == item.TheMovieDbId && x.Type == PlexMediaTypeEntity.Show);
existingTvDbId = await Repo.GetAll().AnyAsync(x => x.TvDbId == item.TvDbId && x.Type == PlexMediaTypeEntity.Show);
if (existingImdb || existingTvDbId || existingMovieDbId)
{
// We already have it!
continue;
}
item.Seasons.ToList().AddRange(seasonsContent);
contentToAdd.Add(item);
var item = new PlexServerContent
{
AddedAt = DateTime.Now,
Key = show.ratingKey,
ReleaseYear = show.year.ToString(),
Type = PlexMediaTypeEntity.Show,
Title = show.title,
Url = PlexHelper.GetPlexMediaUrl(servers.MachineIdentifier, show.ratingKey),
Seasons = new List<PlexSeasonsContent>()
};
if (providerIds.Type == ProviderType.ImdbId)
{
item.ImdbId = providerIds.ImdbId;
}
if (providerIds.Type == ProviderType.TheMovieDbId)
{
item.TheMovieDbId = providerIds.TheMovieDb;
}
if (providerIds.Type == ProviderType.TvDbId)
catch (Exception e)
{
item.TvDbId = providerIds.TheTvDb;
Logger.LogError(LoggingEvents.PlexContentCacher, e, "Exception when adding tv show {0}", show.title);
}
item.Seasons.ToList().AddRange(seasonsContent);
contentToAdd.Add(item);
}
}
}
@ -203,49 +258,68 @@ namespace Ombi.Schedule.Jobs.Plex
{
// Let's check if we have this movie
var existing = await Repo.GetFirstContentByCustom(x => x.Title == movie.title
&& x.ReleaseYear == movie.year.ToString()
&& x.Type == PlexMediaTypeEntity.Movie);
// The rating key keeps changing
//var existing = await Repo.GetByKey(movie.ratingKey);
if (existing != null)
try
{
continue;
}
var existing = await Repo.GetFirstContentByCustom(x => x.Title == movie.title
&& x.ReleaseYear == movie.year.ToString()
&& x.Type == PlexMediaTypeEntity.Movie);
// The rating key keeps changing
//var existing = await Repo.GetByKey(movie.ratingKey);
if (existing != null)
{
Logger.LogInformation("We already have movie {0}", movie.title);
continue;
}
Logger.LogInformation("Adding movie {0}", movie.title);
var metaData = await PlexApi.GetMetadata(servers.PlexAuthToken, servers.FullUri,
movie.ratingKey);
var providerIds = PlexHelper.GetProviderIdFromPlexGuid(metaData.MediaContainer.Metadata
.FirstOrDefault()
.guid);
var hasSameKey = await Repo.GetByKey(movie.ratingKey);
if (hasSameKey != null)
{
await Repo.Delete(hasSameKey);
}
var item = new PlexServerContent
{
AddedAt = DateTime.Now,
Key = movie.ratingKey,
ReleaseYear = movie.year.ToString(),
Type = PlexMediaTypeEntity.Movie,
Title = movie.title,
Url = PlexHelper.GetPlexMediaUrl(servers.MachineIdentifier, movie.ratingKey),
Seasons = new List<PlexSeasonsContent>(),
Quality = movie.Media?.FirstOrDefault()?.videoResolution ?? string.Empty
};
if (providerIds.Type == ProviderType.ImdbId)
{
item.ImdbId = providerIds.ImdbId;
}
if (providerIds.Type == ProviderType.TheMovieDbId)
{
item.TheMovieDbId = providerIds.TheMovieDb;
Logger.LogInformation("Adding movie {0}", movie.title);
var metaData = await PlexApi.GetMetadata(servers.PlexAuthToken, servers.FullUri,
movie.ratingKey);
var providerIds = PlexHelper.GetProviderIdFromPlexGuid(metaData.MediaContainer.Metadata
.FirstOrDefault()
.guid);
var item = new PlexServerContent
{
AddedAt = DateTime.Now,
Key = movie.ratingKey,
ReleaseYear = movie.year.ToString(),
Type = PlexMediaTypeEntity.Movie,
Title = movie.title,
Url = PlexHelper.GetPlexMediaUrl(servers.MachineIdentifier, movie.ratingKey),
Seasons = new List<PlexSeasonsContent>(),
Quality = movie.Media?.FirstOrDefault()?.videoResolution ?? string.Empty
};
if (providerIds.Type == ProviderType.ImdbId)
{
item.ImdbId = providerIds.ImdbId;
}
if (providerIds.Type == ProviderType.TheMovieDbId)
{
item.TheMovieDbId = providerIds.TheMovieDb;
}
if (providerIds.Type == ProviderType.TvDbId)
{
item.TvDbId = providerIds.TheTvDb;
}
contentToAdd.Add(item);
}
if (providerIds.Type == ProviderType.TvDbId)
catch (Exception e)
{
item.TvDbId = providerIds.TheTvDb;
Logger.LogError(LoggingEvents.PlexContentCacher, e, "Exception when adding new Movie {0}", movie.title);
}
contentToAdd.Add(item);
}
}
if (contentToAdd.Count > 500)
{
await Repo.AddRange(contentToAdd);
contentToAdd = new List<PlexServerContent>();
}
}
if (contentToAdd.Any())

@ -48,8 +48,9 @@ namespace Ombi.Schedule.Jobs.Plex
foreach (var server in s.Servers)
{
await Cache(server);
BackgroundJob.Enqueue(() => _availabilityChecker.Start());
}
BackgroundJob.Enqueue(() => _availabilityChecker.Start());
}
catch (Exception e)
{
@ -99,7 +100,7 @@ namespace Ombi.Schedule.Jobs.Plex
private async Task GetEpisodes(PlexServers settings, Directory section)
{
var currentPosition = 0;
var resultCount = settings.EpisodeBatchSize == 0 ? 50 : settings.EpisodeBatchSize;
var resultCount = settings.EpisodeBatchSize == 0 ? 150 : settings.EpisodeBatchSize;
var episodes = await _api.GetAllEpisodes(settings.PlexAuthToken, settings.FullUri, section.key, currentPosition, resultCount);
_log.LogInformation(LoggingEvents.PlexEpisodeCacher, $"Total Epsiodes found for {episodes.MediaContainer.librarySectionTitle} = {episodes.MediaContainer.totalSize}");
@ -127,7 +128,10 @@ namespace Ombi.Schedule.Jobs.Plex
private async Task ProcessEpsiodes(PlexContainer episodes)
{
var ep = new HashSet<PlexEpisode>();
try
{
foreach (var episode in episodes?.MediaContainer?.Metadata ?? new Metadata[]{})
{
// I don't think we need to get the metadata, we only need to get the metadata if we need the provider id (TheTvDbid). Why do we need it for episodes?
@ -142,6 +146,25 @@ namespace Ombi.Schedule.Jobs.Plex
// continue;
//}
// Let's check if we have the parent
var seriesExists = await _repo.GetByKey(episode.grandparentRatingKey);
if (seriesExists == null)
{
// Ok let's try and match it to a title. TODO (This is experimental)
var seriesMatch = await _repo.GetAll().FirstOrDefaultAsync(x =>
x.Title.Equals(episode.grandparentTitle, StringComparison.CurrentCultureIgnoreCase));
if (seriesMatch == null)
{
_log.LogWarning(
"The episode title {0} we cannot find the parent series. The episode grandparentKey = {1}, grandparentTitle = {2}",
episode.title, episode.grandparentRatingKey, episode.grandparentTitle);
continue;
}
// Set the rating key to the correct one
episode.grandparentRatingKey = seriesMatch.Key;
}
ep.Add(new PlexEpisode
{
EpisodeNumber = episode.index,
@ -154,6 +177,12 @@ namespace Ombi.Schedule.Jobs.Plex
}
await _repo.AddRange(ep);
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
private bool Validate(PlexServers settings)

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
@ -9,6 +10,7 @@ using Octokit;
using Ombi.Api;
using Ombi.Api.Service;
using Ombi.Core.Processor;
using Ombi.Helpers;
namespace Ombi.Schedule.Processor
{
@ -44,7 +46,8 @@ namespace Ombi.Schedule.Processor
if (masterBranch)
{
latestRelease = doc.DocumentNode.Descendants("h2")
.FirstOrDefault(x => x.InnerText != "(unreleased)");
.FirstOrDefault(x => x.InnerText == "(unreleased)");
// TODO: Change this to InnterText != "(unreleased)" once we go live and it's not a prerelease
}
else
{
@ -173,10 +176,13 @@ namespace Ombi.Schedule.Processor
var releases = await client.Repository.Release.GetAll("tidusjar", "ombi");
var latest = releases.FirstOrDefault(x => x.TagName == releaseTag);
if (latest.Name.Contains("V2", CompareOptions.IgnoreCase))
{
latest = null;
}
if (latest == null)
{
latest = releases.OrderBy(x => x.CreatedAt).FirstOrDefault();
latest = releases.OrderByDescending(x => x.CreatedAt).FirstOrDefault();
}
foreach (var item in latest.Assets)
{

@ -0,0 +1,7 @@
namespace Ombi.Settings.Settings.Models.Notifications
{
public class MobileNotificationSettings : Settings
{
}
}

@ -99,6 +99,19 @@ namespace Ombi.Store.Context
});
SaveChanges();
}
var notification = ApplicationConfigurations.FirstOrDefault(x => x.Type == ConfigurationTypes.Notification);
if (notification == null)
{
ApplicationConfigurations.Add(new ApplicationConfiguration
{
Type = ConfigurationTypes.Notification,
Value = "4f0260c4-9c3d-41ab-8d68-27cb5a593f0e"
});
SaveChanges();
}
// VACUUM;
Database.ExecuteSqlCommand("VACUUM;");
//Check if templates exist
var templates = NotificationTemplates.ToList();
@ -132,7 +145,7 @@ namespace Ombi.Store.Context
notificationToAdd = new NotificationTemplates
{
NotificationType = notificationType,
Message = "Hello! The user '{RequestedUser}' has reported a new issue for the title {Title}! </br> {Issue}",
Message = "Hello! The user '{IssueUser}' has reported a new issue for the title {Title}! </br> {IssueCategory} - {IssueSubject} : {IssueDescription}",
Subject = "{ApplicationName}: New issue for {Title}!",
Agent = agent,
Enabled = true,
@ -186,12 +199,23 @@ namespace Ombi.Store.Context
notificationToAdd = new NotificationTemplates
{
NotificationType = notificationType,
Message = "Hello {RequestedUser} Your issue for {Title} has now been resolved.",
Message = "Hello {IssueUser} Your issue for {Title} has now been resolved.",
Subject = "{ApplicationName}: Issue has been resolved for {Title}!",
Agent = agent,
Enabled = true,
};
break;
case NotificationType.IssueComment:
notificationToAdd = new NotificationTemplates
{
NotificationType = notificationType,
Message = "Hello, There is a new comment on your issue {IssueSubject}, The comment is: {NewIssueComment}",
Subject = "{ApplicationName}: New comment on issue {IssueSubject}!",
Agent = agent,
Enabled = true,
};
break;
case NotificationType.AdminNote:
continue;
default:

@ -15,6 +15,7 @@ namespace Ombi.Store.Entities
// 2 was used for Port before the beta
FanartTv = 3,
TheMovieDb = 4,
StoragePath = 5
StoragePath = 5,
Notification = 6,
}
}

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
namespace Ombi.Store.Entities
{
public class NotificationUserId : Entity
{
public string PlayerId { get; set; }
public string UserId { get; set; }
public DateTime AddedAt { get; set; }
[ForeignKey(nameof(UserId))]
public OmbiUser User { get; set; }
}
}

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.AspNetCore.Identity;
using Ombi.Helpers;
@ -24,6 +25,8 @@ namespace Ombi.Store.Entities
public string UserAccessToken { get; set; }
public List<NotificationUserId> NotificationUserIds { get; set; }
[NotMapped]
public bool IsEmbyConnect => UserType == UserType.EmbyUser && EmbyConnectUserId.HasValue();

@ -8,8 +8,7 @@ namespace Ombi.Store.Migrations
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(@"
DELETE FROM EmbyEpisode");
}
protected override void Down(MigrationBuilder migrationBuilder)

@ -0,0 +1,914 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal;
using Ombi.Helpers;
using Ombi.Store.Context;
using Ombi.Store.Entities;
using Ombi.Store.Entities.Requests;
using System;
namespace Ombi.Store.Migrations
{
[DbContext(typeof(OmbiContext))]
[Migration("20180201140230_NotificationIds")]
partial class NotificationIds
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.0.0-rtm-26452");
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Name")
.HasMaxLength(256);
b.Property<string>("NormalizedName")
.HasMaxLength(256);
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasName("RoleNameIndex");
b.ToTable("AspNetRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("RoleId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider");
b.Property<string>("ProviderKey");
b.Property<string>("ProviderDisplayName");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("RoleId");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("LoginProvider");
b.Property<string>("Name");
b.Property<string>("Value");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens");
});
modelBuilder.Entity("Ombi.Store.Entities.ApplicationConfiguration", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("Type");
b.Property<string>("Value");
b.HasKey("Id");
b.ToTable("ApplicationConfiguration");
});
modelBuilder.Entity("Ombi.Store.Entities.Audit", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("AuditArea");
b.Property<int>("AuditType");
b.Property<DateTime>("DateTime");
b.Property<string>("Description");
b.Property<string>("User");
b.HasKey("Id");
b.ToTable("Audit");
});
modelBuilder.Entity("Ombi.Store.Entities.CouchPotatoCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("TheMovieDbId");
b.HasKey("Id");
b.ToTable("CouchPotatoCache");
});
modelBuilder.Entity("Ombi.Store.Entities.EmbyContent", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("AddedAt");
b.Property<string>("EmbyId")
.IsRequired();
b.Property<string>("ProviderId");
b.Property<string>("Title");
b.Property<int>("Type");
b.HasKey("Id");
b.ToTable("EmbyContent");
});
modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("AddedAt");
b.Property<string>("EmbyId");
b.Property<int>("EpisodeNumber");
b.Property<string>("ParentId");
b.Property<string>("ProviderId");
b.Property<int>("SeasonNumber");
b.Property<string>("Title");
b.HasKey("Id");
b.HasIndex("ParentId");
b.ToTable("EmbyEpisode");
});
modelBuilder.Entity("Ombi.Store.Entities.GlobalSettings", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Content");
b.Property<string>("SettingsName");
b.HasKey("Id");
b.ToTable("GlobalSettings");
});
modelBuilder.Entity("Ombi.Store.Entities.NotificationTemplates", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("Agent");
b.Property<bool>("Enabled");
b.Property<string>("Message");
b.Property<int>("NotificationType");
b.Property<string>("Subject");
b.HasKey("Id");
b.ToTable("NotificationTemplates");
});
modelBuilder.Entity("Ombi.Store.Entities.NotificationUserId", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("AddedAt");
b.Property<string>("PlayerId");
b.Property<string>("UserId");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("NotificationUserId");
});
modelBuilder.Entity("Ombi.Store.Entities.OmbiUser", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("AccessFailedCount");
b.Property<string>("Alias");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Email")
.HasMaxLength(256);
b.Property<bool>("EmailConfirmed");
b.Property<string>("EmbyConnectUserId");
b.Property<int?>("EpisodeRequestLimit");
b.Property<DateTime?>("LastLoggedIn");
b.Property<bool>("LockoutEnabled");
b.Property<DateTimeOffset?>("LockoutEnd");
b.Property<int?>("MovieRequestLimit");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256);
b.Property<string>("NormalizedUserName")
.HasMaxLength(256);
b.Property<string>("PasswordHash");
b.Property<string>("PhoneNumber");
b.Property<bool>("PhoneNumberConfirmed");
b.Property<string>("ProviderUserId");
b.Property<string>("SecurityStamp");
b.Property<bool>("TwoFactorEnabled");
b.Property<string>("UserAccessToken");
b.Property<string>("UserName")
.HasMaxLength(256);
b.Property<int>("UserType");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasName("UserNameIndex");
b.ToTable("AspNetUsers");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("EpisodeNumber");
b.Property<int>("GrandparentKey");
b.Property<int>("Key");
b.Property<int>("ParentKey");
b.Property<int>("SeasonNumber");
b.Property<string>("Title");
b.HasKey("Id");
b.HasIndex("GrandparentKey");
b.ToTable("PlexEpisode");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("ParentKey");
b.Property<int>("PlexContentId");
b.Property<int?>("PlexServerContentId");
b.Property<int>("SeasonKey");
b.Property<int>("SeasonNumber");
b.HasKey("Id");
b.HasIndex("PlexServerContentId");
b.ToTable("PlexSeasonsContent");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexServerContent", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("AddedAt");
b.Property<string>("ImdbId");
b.Property<int>("Key");
b.Property<string>("Quality");
b.Property<string>("ReleaseYear");
b.Property<string>("TheMovieDbId");
b.Property<string>("Title");
b.Property<string>("TvDbId");
b.Property<int>("Type");
b.Property<string>("Url");
b.HasKey("Id");
b.ToTable("PlexServerContent");
});
modelBuilder.Entity("Ombi.Store.Entities.RadarrCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<bool>("HasFile");
b.Property<int>("TheMovieDbId");
b.HasKey("Id");
b.ToTable("RadarrCache");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.ChildRequests", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<bool>("Approved");
b.Property<bool>("Available");
b.Property<bool?>("Denied");
b.Property<string>("DeniedReason");
b.Property<int?>("IssueId");
b.Property<int>("ParentRequestId");
b.Property<int>("RequestType");
b.Property<DateTime>("RequestedDate");
b.Property<string>("RequestedUserId");
b.Property<int>("SeriesType");
b.Property<string>("Title");
b.HasKey("Id");
b.HasIndex("ParentRequestId");
b.HasIndex("RequestedUserId");
b.ToTable("ChildRequests");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.IssueCategory", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Value");
b.HasKey("Id");
b.ToTable("IssueCategory");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.IssueComments", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Comment");
b.Property<DateTime>("Date");
b.Property<int?>("IssuesId");
b.Property<string>("UserId");
b.HasKey("Id");
b.HasIndex("IssuesId");
b.HasIndex("UserId");
b.ToTable("IssueComments");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.Issues", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Description");
b.Property<int>("IssueCategoryId");
b.Property<int?>("IssueId");
b.Property<string>("ProviderId");
b.Property<int?>("RequestId");
b.Property<int>("RequestType");
b.Property<DateTime?>("ResovledDate");
b.Property<int>("Status");
b.Property<string>("Subject");
b.Property<string>("Title");
b.Property<string>("UserReportedId");
b.HasKey("Id");
b.HasIndex("IssueCategoryId");
b.HasIndex("IssueId");
b.HasIndex("UserReportedId");
b.ToTable("Issues");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.MovieRequests", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<bool>("Approved");
b.Property<bool>("Available");
b.Property<string>("Background");
b.Property<bool?>("Denied");
b.Property<string>("DeniedReason");
b.Property<string>("ImdbId");
b.Property<int?>("IssueId");
b.Property<string>("Overview");
b.Property<string>("PosterPath");
b.Property<int>("QualityOverride");
b.Property<DateTime>("ReleaseDate");
b.Property<int>("RequestType");
b.Property<DateTime>("RequestedDate");
b.Property<string>("RequestedUserId");
b.Property<int>("RootPathOverride");
b.Property<string>("Status");
b.Property<int>("TheMovieDbId");
b.Property<string>("Title");
b.HasKey("Id");
b.HasIndex("RequestedUserId");
b.ToTable("MovieRequests");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.RequestLog", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("EpisodeCount");
b.Property<DateTime>("RequestDate");
b.Property<int>("RequestId");
b.Property<int>("RequestType");
b.Property<string>("UserId");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("RequestLog");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.TvRequests", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ImdbId");
b.Property<string>("Overview");
b.Property<string>("PosterPath");
b.Property<DateTime>("ReleaseDate");
b.Property<int?>("RootFolder");
b.Property<string>("Status");
b.Property<string>("Title");
b.Property<int>("TvDbId");
b.HasKey("Id");
b.ToTable("TvRequests");
});
modelBuilder.Entity("Ombi.Store.Entities.SickRageCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("TvDbId");
b.HasKey("Id");
b.ToTable("SickRageCache");
});
modelBuilder.Entity("Ombi.Store.Entities.SickRageEpisodeCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("EpisodeNumber");
b.Property<int>("SeasonNumber");
b.Property<int>("TvDbId");
b.HasKey("Id");
b.ToTable("SickRageEpisodeCache");
});
modelBuilder.Entity("Ombi.Store.Entities.SonarrCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("TvDbId");
b.HasKey("Id");
b.ToTable("SonarrCache");
});
modelBuilder.Entity("Ombi.Store.Entities.SonarrEpisodeCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("EpisodeNumber");
b.Property<bool>("HasFile");
b.Property<int>("SeasonNumber");
b.Property<int>("TvDbId");
b.HasKey("Id");
b.ToTable("SonarrEpisodeCache");
});
modelBuilder.Entity("Ombi.Store.Entities.Tokens", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Token");
b.Property<string>("UserId");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Tokens");
});
modelBuilder.Entity("Ombi.Store.Repository.Requests.EpisodeRequests", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("AirDate");
b.Property<bool>("Approved");
b.Property<bool>("Available");
b.Property<int>("EpisodeNumber");
b.Property<bool>("Requested");
b.Property<int>("SeasonId");
b.Property<string>("Title");
b.Property<string>("Url");
b.HasKey("Id");
b.HasIndex("SeasonId");
b.ToTable("EpisodeRequests");
});
modelBuilder.Entity("Ombi.Store.Repository.Requests.SeasonRequests", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("ChildRequestId");
b.Property<int>("SeasonNumber");
b.HasKey("Id");
b.HasIndex("ChildRequestId");
b.ToTable("SeasonRequests");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("Ombi.Store.Entities.OmbiUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b =>
{
b.HasOne("Ombi.Store.Entities.EmbyContent", "Series")
.WithMany("Episodes")
.HasForeignKey("ParentId")
.HasPrincipalKey("EmbyId");
});
modelBuilder.Entity("Ombi.Store.Entities.NotificationUserId", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser", "User")
.WithMany("NotificationUserIds")
.HasForeignKey("UserId");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b =>
{
b.HasOne("Ombi.Store.Entities.PlexServerContent", "Series")
.WithMany("Episodes")
.HasForeignKey("GrandparentKey")
.HasPrincipalKey("Key")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b =>
{
b.HasOne("Ombi.Store.Entities.PlexServerContent")
.WithMany("Seasons")
.HasForeignKey("PlexServerContentId");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.ChildRequests", b =>
{
b.HasOne("Ombi.Store.Entities.Requests.TvRequests", "ParentRequest")
.WithMany("ChildRequests")
.HasForeignKey("ParentRequestId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser")
.WithMany()
.HasForeignKey("RequestedUserId");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.IssueComments", b =>
{
b.HasOne("Ombi.Store.Entities.Requests.Issues", "Issues")
.WithMany("Comments")
.HasForeignKey("IssuesId");
b.HasOne("Ombi.Store.Entities.OmbiUser", "User")
.WithMany()
.HasForeignKey("UserId");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.Issues", b =>
{
b.HasOne("Ombi.Store.Entities.Requests.IssueCategory", "IssueCategory")
.WithMany()
.HasForeignKey("IssueCategoryId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("Ombi.Store.Entities.Requests.ChildRequests")
.WithMany("Issues")
.HasForeignKey("IssueId");
b.HasOne("Ombi.Store.Entities.Requests.MovieRequests")
.WithMany("Issues")
.HasForeignKey("IssueId");
b.HasOne("Ombi.Store.Entities.OmbiUser", "UserReported")
.WithMany()
.HasForeignKey("UserReportedId");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.MovieRequests", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser")
.WithMany()
.HasForeignKey("RequestedUserId");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.RequestLog", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser", "User")
.WithMany()
.HasForeignKey("UserId");
});
modelBuilder.Entity("Ombi.Store.Entities.Tokens", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser", "User")
.WithMany()
.HasForeignKey("UserId");
});
modelBuilder.Entity("Ombi.Store.Repository.Requests.EpisodeRequests", b =>
{
b.HasOne("Ombi.Store.Repository.Requests.SeasonRequests", "Season")
.WithMany("Episodes")
.HasForeignKey("SeasonId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Ombi.Store.Repository.Requests.SeasonRequests", b =>
{
b.HasOne("Ombi.Store.Entities.Requests.ChildRequests", "ChildRequest")
.WithMany("SeasonRequests")
.HasForeignKey("ChildRequestId")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
}

@ -0,0 +1,44 @@
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
namespace Ombi.Store.Migrations
{
public partial class NotificationIds : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "NotificationUserId",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
AddedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
PlayerId = table.Column<string>(type: "TEXT", nullable: true),
UserId = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_NotificationUserId", x => x.Id);
table.ForeignKey(
name: "FK_NotificationUserId_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_NotificationUserId_UserId",
table: "NotificationUserId",
column: "UserId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "NotificationUserId");
}
}
}

@ -256,6 +256,24 @@ namespace Ombi.Store.Migrations
b.ToTable("NotificationTemplates");
});
modelBuilder.Entity("Ombi.Store.Entities.NotificationUserId", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("AddedAt");
b.Property<string>("PlayerId");
b.Property<string>("UserId");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("NotificationUserId");
});
modelBuilder.Entity("Ombi.Store.Entities.OmbiUser", b =>
{
b.Property<string>("Id")
@ -787,6 +805,13 @@ namespace Ombi.Store.Migrations
.HasPrincipalKey("EmbyId");
});
modelBuilder.Entity("Ombi.Store.Entities.NotificationUserId", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser", "User")
.WithMany("NotificationUserIds")
.HasForeignKey("UserId");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b =>
{
b.HasOne("Ombi.Store.Entities.PlexServerContent", "Series")

@ -19,5 +19,8 @@ namespace Ombi.Store.Repository
Task AddRange(IEnumerable<PlexEpisode> content);
IEnumerable<PlexServerContent> GetWhereContentByCustom(Expression<Func<PlexServerContent, bool>> predicate);
Task<PlexServerContent> GetFirstContentByCustom(Expression<Func<PlexServerContent, bool>> predicate);
Task DeleteEpisode(PlexEpisode content);
void DeleteWithoutSave(PlexServerContent content);
void DeleteWithoutSave(PlexEpisode content);
}
}

@ -66,7 +66,11 @@ namespace Ombi.Store.Repository
var item = await Db.PlexServerContent.FirstOrDefaultAsync(x => x.ImdbId == providerId);
if (item == null)
{
item = await Db.PlexServerContent.FirstOrDefaultAsync(x => x.TheMovieDbId == providerId) ?? await Db.PlexServerContent.FirstOrDefaultAsync(x => x.TvDbId == providerId);
item = await Db.PlexServerContent.FirstOrDefaultAsync(x => x.TheMovieDbId == providerId);
if (item == null)
{
item = await Db.PlexServerContent.FirstOrDefaultAsync(x => x.TvDbId == providerId);
}
}
return item;
}
@ -83,7 +87,9 @@ namespace Ombi.Store.Repository
public async Task<PlexServerContent> GetFirstContentByCustom(Expression<Func<PlexServerContent, bool>> predicate)
{
return await Db.PlexServerContent.FirstOrDefaultAsync(predicate);
return await Db.PlexServerContent
.Include(x => x.Seasons)
.FirstOrDefaultAsync(predicate);
}
public async Task Update(PlexServerContent existingContent)
@ -94,7 +100,17 @@ namespace Ombi.Store.Repository
public IQueryable<PlexEpisode> GetAllEpisodes()
{
return Db.PlexEpisode.AsQueryable();
return Db.PlexEpisode.Include(x => x.Series).AsQueryable();
}
public void DeleteWithoutSave(PlexServerContent content)
{
Db.PlexServerContent.Remove(content);
}
public void DeleteWithoutSave(PlexEpisode content)
{
Db.PlexEpisode.Remove(content);
}
public async Task<PlexEpisode> Add(PlexEpisode content)
@ -103,6 +119,13 @@ namespace Ombi.Store.Repository
await Db.SaveChangesAsync();
return content;
}
public async Task DeleteEpisode(PlexEpisode content)
{
Db.PlexEpisode.Remove(content);
await Db.SaveChangesAsync();
}
public async Task<PlexEpisode> GetEpisodeByKey(int key)
{
return await Db.PlexEpisode.FirstOrDefaultAsync(x => x.Key == key);

@ -44,6 +44,7 @@ namespace Ombi.Store.Repository.Requests
{
return Db.MovieRequests
.Include(x => x.RequestedUser)
.ThenInclude(x => x.NotificationUserIds)
.AsQueryable();
}

@ -3,6 +3,7 @@ using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using Microsoft.Extensions.Logging;
@ -33,27 +34,27 @@ namespace Ombi.Updater
}
// Make sure the process has been killed
while (p.FindProcessByName(opt.ProcessName).Any())
while (p.FindProcessByName(opt.ProcessName).Any())
{
Thread.Sleep(500);
_log.LogDebug("Found another process called {0}, KILLING!", opt.ProcessName);
var proc = p.FindProcessByName(opt.ProcessName).FirstOrDefault();
if (proc != null)
{
Thread.Sleep(500);
_log.LogDebug("Found another process called {0}, KILLING!", opt.ProcessName);
var proc = p.FindProcessByName(opt.ProcessName).FirstOrDefault();
if (proc != null)
{
_log.LogDebug($"[{proc.Id}] - {proc.Name} - Path: {proc.StartPath}");
opt.OmbiProcessId = proc.Id;
p.Kill(opt);
}
_log.LogDebug($"[{proc.Id}] - {proc.Name} - Path: {proc.StartPath}");
opt.OmbiProcessId = proc.Id;
p.Kill(opt);
}
_log.LogDebug("Starting to move the files");
MoveFiles(opt);
_log.LogDebug("Files replaced");
// Start Ombi
StartOmbi(opt);
}
private void StartOmbi(StartupOptions options)
_log.LogDebug("Starting to move the files");
MoveFiles(opt);
_log.LogDebug("Files replaced");
// Start Ombi
StartOmbi(opt);
}
private void StartOmbi(StartupOptions options)
{
_log.LogDebug("Starting ombi");
var fileName = "Ombi.exe";
@ -71,19 +72,29 @@ namespace Ombi.Updater
Arguments = $"/C net start \"{options.WindowsServiceName}\""
};
using (var process = new Process{StartInfo = startInfo})
using (var process = new Process { StartInfo = startInfo })
{
process.Start();
}
}
else
{
var startupArgsBuilder = new StringBuilder();
if (!string.IsNullOrEmpty(options.Host))
{
startupArgsBuilder.Append($"--host {options.Host} ");
}
if (!string.IsNullOrEmpty(options.Storage))
{
startupArgsBuilder.Append($"--storage {options.Storage}");
}
var start = new ProcessStartInfo
{
UseShellExecute = false,
FileName = Path.Combine(options.ApplicationPath, fileName),
WorkingDirectory = options.ApplicationPath,
Arguments = options.StartupArgs
Arguments = startupArgsBuilder.ToString()
};
using (var proc = new Process { StartInfo = start })
{

@ -1,14 +1,10 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using CommandLine;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Serilog;
using Serilog.Events;
using ILogger = Serilog.ILogger;
namespace Ombi.Updater
{
@ -78,8 +74,10 @@ namespace Ombi.Updater
public string ApplicationPath { get; set; }
[Option("processId", Required = false)]
public int OmbiProcessId { get; set; }
[Option("startupArgs", Required = false)]
public string StartupArgs { get; set; }
[Option("host", Required = false)]
public string Host { get; set; }
[Option("storage", Required = false)]
public string Storage { get; set; }
[Option("windowsServiceName", Required = false)]
public string WindowsServiceName { get; set; }

@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27004.2009
VisualStudioVersion = 15.0.27130.2027
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi", "Ombi\Ombi.csproj", "{C987AA67-AFE1-468F-ACD3-EAD5A48E1F6A}"
EndProject
@ -90,7 +90,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Api.Telegram", "Ombi.A
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Api.Github", "Ombi.Api.Github\Ombi.Api.Github.csproj", "{55866DEE-46D1-4AF7-B1A2-62F6190C8EC7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ombi.Api.SickRage", "Ombi.Api.SickRage\Ombi.Api.SickRage.csproj", "{94C9A366-2595-45EA-AABB-8E4A2E90EC5B}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Api.SickRage", "Ombi.Api.SickRage\Ombi.Api.SickRage.csproj", "{94C9A366-2595-45EA-AABB-8E4A2E90EC5B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ombi.Api.Notifications", "Ombi.Api.Notifications\Ombi.Api.Notifications.csproj", "{10D1FE9D-9124-42B7-B1E1-CEB99B832618}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -238,6 +240,10 @@ Global
{94C9A366-2595-45EA-AABB-8E4A2E90EC5B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{94C9A366-2595-45EA-AABB-8E4A2E90EC5B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{94C9A366-2595-45EA-AABB-8E4A2E90EC5B}.Release|Any CPU.Build.0 = Release|Any CPU
{10D1FE9D-9124-42B7-B1E1-CEB99B832618}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{10D1FE9D-9124-42B7-B1E1-CEB99B832618}.Debug|Any CPU.Build.0 = Debug|Any CPU
{10D1FE9D-9124-42B7-B1E1-CEB99B832618}.Release|Any CPU.ActiveCfg = Release|Any CPU
{10D1FE9D-9124-42B7-B1E1-CEB99B832618}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -272,6 +278,7 @@ Global
{CB9DD209-8E09-4E01-983E-C77C89592D36} = {9293CA11-360A-4C20-A674-B9E794431BF5}
{55866DEE-46D1-4AF7-B1A2-62F6190C8EC7} = {9293CA11-360A-4C20-A674-B9E794431BF5}
{94C9A366-2595-45EA-AABB-8E4A2E90EC5B} = {9293CA11-360A-4C20-A674-B9E794431BF5}
{10D1FE9D-9124-42B7-B1E1-CEB99B832618} = {9293CA11-360A-4C20-A674-B9E794431BF5}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {192E9BF8-00B4-45E4-BCCC-4C215725C869}

@ -35,15 +35,17 @@ export enum NotificationAgent {
}
export enum NotificationType {
NewRequest,
Issue,
RequestAvailable,
RequestApproved,
AdminNote,
Test,
RequestDeclined,
ItemAddedToFaultQueue,
WelcomeEmail,
NewRequest = 0,
Issue = 1,
RequestAvailable = 2,
RequestApproved = 3,
AdminNote = 4,
Test = 5,
RequestDeclined = 6,
ItemAddedToFaultQueue = 7,
WelcomeEmail = 8,
IssueResolved = 9,
IssueComment = 10,
}
export interface IDiscordNotifcationSettings extends INotificationSettings {
@ -87,3 +89,7 @@ export interface IMattermostNotifcationSettings extends INotificationSettings {
iconUrl: string;
notificationTemplates: INotificationTemplates[];
}
export interface IMobileNotifcationSettings extends INotificationSettings {
notificationTemplates: INotificationTemplates[];
}

@ -44,3 +44,8 @@ export interface IResetPasswordToken {
token: string;
password: string;
}
export interface IMobileUsersViewModel {
username: string;
devices: number;
}

@ -2,7 +2,8 @@
<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)">
<span class="input-group-btn"> <button class="btn btn-sm btn-info-outline" (click)="filterDisplay = true" >
<span class="input-group-btn">
<button id="filterBtn" class="btn btn-sm btn-info-outline" (click)="filterDisplay = true" >
<i class="fa fa-filter"></i> {{ 'Requests.Filter' | translate }}</button>
</span>
@ -24,7 +25,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 class="img-responsive poster" src="https://image.tmdb.org/t/p/w150/{{request.posterPath}}" alt="poster">
<img class="img-responsive poster" src="https://image.tmdb.org/t/p/w300/{{request.posterPath}}" alt="poster">
</div>
@ -191,12 +192,6 @@
<label for="approved">{{ 'Filter.Approved' | translate }}</label>
</div>
</div>
<div class="form-group">
<div class="radio">
<input type="radio" id="notApproved" name="Status" (click)="filterStatus(filterType.NotApproved)">
<label for="notApproved">{{ 'Filter.NotApproved' | translate }}</label>
</div>
</div>
<div class="form-group">
<div class="radio">
<input type="radio" id="Processing" name="Status" (click)="filterStatus(filterType.Processing)">

@ -33,7 +33,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-2 small-padding">
<img *ngIf="result.posterPath" class="img-responsive poster" src="https://image.tmdb.org/t/p/w150/{{result.posterPath}}" alt="poster">
<img *ngIf="result.posterPath" class="img-responsive poster" src="https://image.tmdb.org/t/p/w300/{{result.posterPath}}" alt="poster">
</div>
<div class="col-sm-8 small-padding">

@ -8,15 +8,16 @@
}
</style>
<div *ngIf="series">
<button class="btn btn-sm btn-success pull-right" (click)="submitRequests()" title="Go to top">Submit Request</button>
<button class="btn btn-sm btn-success pull-right" (click)="submitRequests()" title="Go to top">{{ 'Search.TvShows.SubmitRequest' | translate }}</button>
<ngb-tabset>
<div *ngFor="let season of series.seasonRequests">
<ngb-tab [id]="season.seasonNumber" [title]="season.seasonNumber">
<ng-template ngbTabContent>
<h2>Season: {{season.seasonNumber}}</h2>
<h2 [translate]="'Requests.SeasonNumberHeading'" [translateParams]="{seasonNumber: season.seasonNumber}">Season: {{season.seasonNumber}}</h2>
<button (click)="addAllEpisodes(season)" class="btn btn-sm btn-primary-outline" [translate]="'Search.TvShows.SelectAllInSeason'" [translateParams]="{seasonNumber: season.seasonNumber}">Select All in Season {{season.seasonNumber}}</button>
<table class="table table-striped table-hover table-responsive table-condensed">
<thead>
<tr>

@ -1,13 +1,11 @@
import { Component, Input, OnDestroy, OnInit} from "@angular/core";
import { Subject } from "rxjs/Subject";
import { Component, Input, OnInit} from "@angular/core";
import "rxjs/add/operator/takeUntil";
import { NotificationService } from "../services";
import { RequestService } from "../services";
import { SearchService } from "../services";
import { IRequestEngineResult } from "../interfaces";
import { INewSeasonRequests, IRequestEngineResult } from "../interfaces";
import { IEpisodesRequests } from "../interfaces";
import { ISearchTvResult } from "../interfaces";
@ -16,20 +14,18 @@ import { ISearchTvResult } from "../interfaces";
templateUrl: "./seriesinformation.component.html",
styleUrls: ["./seriesinformation.component.scss"],
})
export class SeriesInformationComponent implements OnInit, OnDestroy {
export class SeriesInformationComponent implements OnInit {
public result: IRequestEngineResult;
public series: ISearchTvResult;
public requestedEpisodes: IEpisodesRequests[] = [];
@Input() private seriesId: number;
private subscriptions = new Subject<void>();
constructor(private searchService: SearchService, private requestService: RequestService, private notificationService: NotificationService) { }
public ngOnInit() {
this.searchService.getShowInformation(this.seriesId)
.takeUntil(this.subscriptions)
.subscribe(x => {
this.series = x;
});
@ -51,7 +47,6 @@ export class SeriesInformationComponent implements OnInit, OnDestroy {
this.series.requested = true;
this.requestService.requestTv(this.series)
.takeUntil(this.subscriptions)
.subscribe(x => {
this.result = x as IRequestEngineResult;
if (this.result.result) {
@ -80,8 +75,7 @@ export class SeriesInformationComponent implements OnInit, OnDestroy {
episode.selected = false;
}
public ngOnDestroy() {
this.subscriptions.next();
this.subscriptions.complete();
public addAllEpisodes(season: INewSeasonRequests) {
season.episodes.forEach((ep) => this.addRequest(ep));
}
}

@ -5,21 +5,21 @@
<div class="input-group-addon right-radius">
<div class="btn-group">
<a href="#" class="btn btn-sm btn-primary-outline dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
Suggestions
{{ 'Search.Suggestions' | translate }}
<i class="fa fa-chevron-down"></i>
</a>
<ul class="dropdown-menu">
<li>
<a (click)="popularShows()">Popular Shows</a>
<a (click)="popularShows()">{{ 'Search.TvShows.Popular' | translate }} </a>
</li>
<li>
<a (click)="trendingShows()">Trending Shows</a>
<a (click)="trendingShows()">{{ 'Search.TvShows.Trending' | translate }}</a>
</li>
<li>
<a (click)="mostWatchedShows()">Most Watched Shows</a>
<a (click)="mostWatchedShows()">{{ 'Search.TvShows.MostWatched' | translate }}</a>
</li>
<li>
<a (click)="anticipatedShows()">Most Anticipated Shows</a>
<a (click)="anticipatedShows()">{{ 'Search.TvShows.MostAnticipated' | translate }}</a>
</li>
</ul>
</div>
@ -40,12 +40,12 @@
<div *ngIf="searchApplied && tvResults?.length <= 0" class='no-search-results'>
<i class='fa fa-film no-search-results-icon'></i>
<div class='no-search-results-text'>Sorry, we didn't find any results!</div>
<div class='no-search-results-text'>{{ 'Search.NoResults' | translate }}</div>
</div>
<p-treeTable [value]="tvResults">
<p-column>
<ng-template let-col let-node="rowData" pTemplate="header">
Results
{{ 'Search.TvShows.Results' | translate }}
</ng-template>
<ng-template let-col let-node="rowData" pTemplate="body">
<!--This is the section that holds the parent level search results set-->
@ -67,24 +67,22 @@
</a>
<span class="tags">
<a *ngIf="node.data.homepage" id="homepageLabel" href="{{node.data.homepage}}" target="_blank"><span class="label label-info" >HomePage</span></a>
<a *ngIf="node.data.homepage" id="homepageLabel" href="{{node.data.homepage}}" target="_blank"><span class="label label-info" >{{ 'Search.Movies.HomePage' | translate }}</span></a>
<a *ngIf="node.data.trailer" id="trailerLabel" href="{{node.data.trailer}}" target="_blank"><span class="label label-info">Trailer</span></a>
<a *ngIf="node.data.trailer" id="trailerLabel" href="{{node.data.trailer}}" target="_blank"><span class="label label-info">{{ 'Search.Movies.Trailer' | translate }}</span></a>
<span *ngIf="node.data.status" class="label label-primary" id="statusLabel" target="_blank">{{node.data.status}}</span>
<span *ngIf="node.data.firstAired" class="label label-info" target="_blank" id="airDateLabel">Air Date: {{node.data.firstAired | date: 'dd/MM/yyyy'}}</span>
<span *ngIf="node.data.firstAired" class="label label-info" target="_blank" id="airDateLabel">{{ 'Search.TvShows.AirDate' | translate }} {{node.data.firstAired | date: 'dd/MM/yyyy'}}</span>
<span *ngIf="node.data.network" class="label label-info" id="networkLabel" target="_blank">{{node.data.network}}</span>
<ng-template [ngIf]="node.data.available">
<span class="label label-success" id="availableLabel">Available</span>
</ng-template>
<ng-template [ngIf]="node.data.partlyAvailable">
<span class="label label-warning" id="partiallyAvailableLabel">Partially Available</span>
</ng-template>
<span *ngIf="node.data.available" class="label label-success" id="availableLabel">{{ 'Common.Available' | translate }}</span>
<span *ngIf="node.data.partlyAvailable" class="label label-warning" id="partiallyAvailableLabel">{{ 'Common.PartlyAvailable' | translate }}</span>
</span>
@ -99,39 +97,39 @@
<div class="col-sm-2 small-padding">
<div *ngIf="!node.data.fullyAvailable" class="dropdown">
<button class="btn btn-primary-outline dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<i class="fa fa-plus"></i> Request
<i class="fa fa-plus"></i> {{ 'Common.Request' | translate }}
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu1">
<li>
<a href="#" (click)="allSeasons(node.data, $event)">All Seasons</a>
<a href="#" (click)="allSeasons(node.data, $event)">{{ 'Search.TvShows.AllSeasons' | translate }}</a>
</li>
<li>
<a href="#" (click)="firstSeason(node.data, $event)">First Season</a>
<a href="#" (click)="firstSeason(node.data, $event)">{{ 'Search.TvShows.FirstSeason' | translate }}</a>
</li>
<li>
<a href="#" (click)="latestSeason(node.data, $event)">Latest Season</a>
<a href="#" (click)="latestSeason(node.data, $event)">{{ 'Search.TvShows.LatestSeason' | translate }}</a>
</li>
<li>
<a href="#" (click)="openClosestTab($event)">Select ...</a>
<a href="#" (click)="openClosestTab($event)">{{ 'Search.TvShows.Select' | translate }}</a>
</li>
</ul>
</div>
<div *ngIf="node.data.fullyAvailable">
<button style="text-align: right" class="btn btn-success-outline disabled" disabled>
<i class="fa fa-check"></i> Available</button>
<i class="fa fa-check"></i> {{ 'Common.Available' | translate }}</button>
</div>
<br />
<div *ngIf="node.data.plexUrl && node.data.available">
<a style="text-align: right" class="btn btn-sm btn-success-outline" href="{{node.data.plexUrl}}"
target="_blank">
<i class="fa fa-eye"></i> View On Plex</a>
<i class="fa fa-eye"></i> {{ 'Search.ViewOnPlex' | translate }}</a>
</div>
<div class="dropdown" *ngIf="issueCategories && issuesEnabled">
<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> Report Issue
<i class="fa fa-plus"></i> {{ 'Requests.ReportIssue' | translate }}
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu1">

@ -11,3 +11,4 @@ export * from "./settings.service";
export * from "./status.service";
export * from "./job.service";
export * from "./issues.service";
export * from "./mobile.service";

@ -0,0 +1,18 @@
import { PlatformLocation } from "@angular/common";
import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs/Rx";
import { IMobileUsersViewModel } from "./../interfaces";
import { ServiceHelpers } from "./service.helpers";
@Injectable()
export class MobileService extends ServiceHelpers {
constructor(http: HttpClient, public platformLocation: PlatformLocation) {
super(http, "/api/v1/mobile/", platformLocation);
}
public getUserDeviceList(): Observable<IMobileUsersViewModel[]> {
return this.http.get<IMobileUsersViewModel[]>(`${this.url}notification/`, {headers: this.headers});
}
}

@ -16,6 +16,7 @@ import {
IJobSettings,
ILandingPageSettings,
IMattermostNotifcationSettings,
IMobileNotifcationSettings,
IOmbiSettings,
IPlexSettings,
IPushbulletNotificationSettings,
@ -139,14 +140,15 @@ export class SettingsService extends ServiceHelpers {
return this.http.get<IMattermostNotifcationSettings>(`${this.url}/notifications/mattermost`, {headers: this.headers});
}
public saveMattermostNotificationSettings(settings: IMattermostNotifcationSettings): Observable<boolean> {
return this.http.post<boolean>(`${this.url}/notifications/mattermost`, JSON.stringify(settings), {headers: this.headers});
}
public saveDiscordNotificationSettings(settings: IDiscordNotifcationSettings): Observable<boolean> {
return this.http
.post<boolean>(`${this.url}/notifications/discord`, JSON.stringify(settings), {headers: this.headers});
}
public saveMattermostNotificationSettings(settings: IMattermostNotifcationSettings): Observable<boolean> {
return this.http.post<boolean>(`${this.url}/notifications/mattermost`, JSON.stringify(settings), {headers: this.headers});
}
public getPushbulletNotificationSettings(): Observable<IPushbulletNotificationSettings> {
return this.http.get<IPushbulletNotificationSettings>(`${this.url}/notifications/pushbullet`, {headers: this.headers});
}
@ -172,6 +174,14 @@ export class SettingsService extends ServiceHelpers {
.post<boolean>(`${this.url}/notifications/slack`, JSON.stringify(settings), {headers: this.headers});
}
public getMobileNotificationSettings(): Observable<IMobileNotifcationSettings> {
return this.http.get<IMobileNotifcationSettings>(`${this.url}/notifications/mobile`, {headers: this.headers});
}
public saveMobileNotificationSettings(settings: IMobileNotifcationSettings): Observable<boolean> {
return this.http.post<boolean>(`${this.url}/notifications/mobile`, JSON.stringify(settings), {headers: this.headers});
}
public getUpdateSettings(): Observable<IUpdateSettings> {
return this.http.get<IUpdateSettings>(`${this.url}/update`, {headers: this.headers});
}

@ -0,0 +1,62 @@
<settings-menu>
</settings-menu>
<div *ngIf="form">
<fieldset>
<legend>Mobile Notifications</legend>
<div class="col-md-6">
<form novalidate [formGroup]="form" (ngSubmit)="onSubmit(form)">
<div class="row">
<div *ngIf="userList" class="col-md-8">
<table class="table table-striped table-hover table-responsive table-condensed">
<thead>
<tr>
<th>
<a>Username/Alias</a>
</th>
<th>
<a>Mobile Devices Registered</a>
</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let user of userList">
<td>
{{user.username}}
</td>
<td>
{{user.devices}}
</td>
</tbody>
</table>
</div>
</div>
<div class="row">
<div class="form-group">
<div>
<button [disabled]="form.invalid" type="button" (click)="test(form)" class="btn btn-primary-outline">
Test
<div id="spinner"></div>
</button>
</div>
</div>
<div class="form-group">
<div>
<button [disabled]="form.invalid" type="submit" id="save" class="btn btn-primary-outline">Submit</button>
</div>
</div>
</div>
</form>
</div>
<div class="col-md-6">
<notification-templates [templates]="templates" [showSubject]="false"></notification-templates>
</div>
</fieldset>
</div>

@ -0,0 +1,70 @@
import { Component, OnInit } from "@angular/core";
import { FormBuilder, FormGroup } from "@angular/forms";
import { IMobileNotifcationSettings, IMobileUsersViewModel, INotificationTemplates, NotificationType } from "../../interfaces";
import { TesterService } from "../../services";
import { NotificationService } from "../../services";
import { MobileService, SettingsService } from "../../services";
@Component({
templateUrl: "./mobile.component.html",
})
export class MobileComponent implements OnInit {
public NotificationType = NotificationType;
public templates: INotificationTemplates[];
public form: FormGroup;
public userList: IMobileUsersViewModel[];
constructor(private settingsService: SettingsService,
private notificationService: NotificationService,
private fb: FormBuilder,
private testerService: TesterService,
private mobileService: MobileService) { }
public ngOnInit() {
this.settingsService.getMobileNotificationSettings().subscribe(x => {
this.templates = x.notificationTemplates;
this.form = this.fb.group({
});
});
this.mobileService.getUserDeviceList().subscribe(x => this.userList = x);
}
public onSubmit(form: FormGroup) {
if (form.invalid) {
this.notificationService.error("Please check your entered values");
return;
}
const settings = <IMobileNotifcationSettings>form.value;
settings.notificationTemplates = this.templates;
this.settingsService.saveMobileNotificationSettings(settings).subscribe(x => {
if (x) {
this.notificationService.success("Successfully saved the Mobile settings");
} else {
this.notificationService.success("There was an error when saving the Mobile settings");
}
});
}
public test(form: FormGroup) {
if (form.invalid) {
this.notificationService.error("Please check your entered values");
return;
}
this.testerService.discordTest(form.value).subscribe(x => {
if (x) {
this.notificationService.success("Successfully sent a Mobile message, please check the admin mobile device");
} else {
this.notificationService.error("There was an error when sending the Mobile message. Please check your settings");
}
});
}
}

@ -1,6 +1,7 @@
import { Component, OnInit } from "@angular/core";
import { FormBuilder, FormGroup } from "@angular/forms";
import { IOmbiSettings } from "../../interfaces";
import { NotificationService } from "../../services";
import { SettingsService } from "../../services";
@ -39,6 +40,14 @@ export class OmbiComponent implements OnInit {
return;
}
const result = <IOmbiSettings>form.value;
if(result.baseUrl && result.baseUrl.length > 0) {
if(!result.baseUrl.startsWith("/")) {
this.notificationService.error("Please ensure your base url starts with a '/'");
return;
}
}
this.settingsService.saveOmbi(form.value).subscribe(x => {
if (x) {
this.notificationService.success("Successfully saved Ombi settings");

@ -7,7 +7,7 @@ import { ClipboardModule } from "ngx-clipboard/dist";
import { AuthGuard } from "../auth/auth.guard";
import { AuthService } from "../auth/auth.service";
import { CouchPotatoService, EmbyService, IssuesService, JobService, PlexService, RadarrService, SonarrService, TesterService, ValidationService } from "../services";
import { CouchPotatoService, EmbyService, IssuesService, JobService, MobileService, PlexService, RadarrService, SonarrService, TesterService, ValidationService } from "../services";
import { PipeModule } from "../pipes/pipe.module";
import { AboutComponent } from "./about/about.component";
@ -22,6 +22,7 @@ import { LandingPageComponent } from "./landingpage/landingpage.component";
import { DiscordComponent } from "./notifications/discord.component";
import { EmailNotificationComponent } from "./notifications/emailnotification.component";
import { MattermostComponent } from "./notifications/mattermost.component";
import { MobileComponent } from "./notifications/mobile.component";
import { NotificationTemplate } from "./notifications/notificationtemplate.component";
import { PushbulletComponent } from "./notifications/pushbullet.component";
import { PushoverComponent } from "./notifications/pushover.component";
@ -64,6 +65,7 @@ const routes: Routes = [
{ path: "SickRage", component: SickRageComponent, canActivate: [AuthGuard] },
{ path: "Issues", component: IssuesComponent, canActivate: [AuthGuard] },
{ path: "Authentication", component: AuthenticationComponent, canActivate: [AuthGuard] },
{ path: "Mobile", component: MobileComponent, canActivate: [AuthGuard] },
];
@NgModule({
@ -111,6 +113,7 @@ const routes: Routes = [
TelegramComponent,
IssuesComponent,
AuthenticationComponent,
MobileComponent,
],
exports: [
RouterModule,
@ -127,6 +130,7 @@ const routes: Routes = [
IssuesService,
PlexService,
EmbyService,
MobileService,
],
})

@ -3,13 +3,17 @@
<button type="button" class="btn btn-success-outline" [routerLink]="['/usermanagement/add']">Add User</button>
<button type="button" style="float:right;" class="btn btn-primary-outline"(click)="showBulkEdit = !showBulkEdit" [disabled]="!hasChecked()">Bulk Edit</button>
<!-- Table -->
<table class="table table-striped table-hover table-responsive table-condensed table-usermanagement">
<thead>
<tr>
<th >
<th style="width:1%">
<a>
<input type="checkbox" ng-checked="checkAll" (change)="checkAllBoxes()">
<td class="checkbox" data-label="Select:">
<input id="all" type="checkbox" ng-checked="checkAll" (change)="checkAllBoxes()">
<label for="all"></label>
</td>
</a>
</th>
<th (click)="setOrder('u.userName')">
@ -57,8 +61,9 @@
</thead>
<tbody>
<tr *ngFor="let u of users | orderBy: order : reverse : 'case-insensitive'">
<td class="td-labelled" data-label="Select:">
<input type="checkbox" [(ngModel)]="u.checked">
<td class="checkbox" data-label="Select:">
<input id="{{u.id}}" type="checkbox" [(ngModel)]="u.checked">
<label for="{{u.id}}"></label>
</td>
<td class="td-labelled" data-label="Username:">
{{u.userName}}
@ -94,5 +99,31 @@
</table>
<p-sidebar [(visible)]="showBulkEdit" position="right" styleClass="ui-sidebar-md side-back">
<h3>Bulk Edit</h3>
<hr/>
<div *ngFor="let c of availableClaims">
<div class="form-group">
<div class="checkbox">
<input type="checkbox" [(ngModel)]="c.enabled" [value]="c.value" id="create{{c.value}}" [attr.name]="'create' + c.value" ng-checked="c.enabled">
<label for="create{{c.value}}">{{c.value | humanize}}</label>
</div>
</div>
</div>
<div class="form-group">
<label for="movieRequestLimit" class="control-label">Movie Request Limit</label>
<div>
<input type="text" [(ngModel)]="bulkMovieLimit" class="form-control form-small form-control-custom " id="movieRequestLimit" name="movieRequestLimit" value="{{bulkMovieLimit}}">
</div>
</div>
<div class="form-group">
<label for="episodeRequestLimit" class="control-label">Episode Request Limit</label>
<div>
<input type="text" [(ngModel)]="bulkEpisodeLimit" class="form-control form-small form-control-custom " id="episodeRequestLimit" name="episodeRequestLimit" value="{{bulkEpisodeLimit}}">
</div>
</div>
<button type="button" class="btn btn-success-outline" (click)="bulkUpdate()">Update Users</button>
</p-sidebar>

@ -1,6 +1,6 @@
import { Component, OnInit } from "@angular/core";
import { ICustomizationSettings, IEmailNotificationSettings, IUser } from "../interfaces";
import { ICheckbox, ICustomizationSettings, IEmailNotificationSettings,IUser } from "../interfaces";
import { IdentityService, NotificationService, SettingsService } from "../services";
@Component({
@ -16,6 +16,11 @@ export class UserManagementComponent implements OnInit {
public order: string = "u.userName";
public reverse = false;
public showBulkEdit = false;
public availableClaims: ICheckbox[];
public bulkMovieLimit?: number;
public bulkEpisodeLimit?: number;
constructor(private readonly identityService: IdentityService,
private readonly settingsService: SettingsService,
private readonly notificationService: NotificationService) { }
@ -26,6 +31,7 @@ export class UserManagementComponent implements OnInit {
this.users = x;
});
this.identityService.getAllAvailableClaims().subscribe(x => this.availableClaims = x);
this.settingsService.getCustomization().subscribe(x => this.customizationSettings = x);
this.settingsService.getEmailNotificationSettings().subscribe(x => this.emailSettings = x);
}
@ -49,6 +55,43 @@ export class UserManagementComponent implements OnInit {
user.checked = this.checkAll;
});
}
public hasChecked(): boolean {
return this.users.some(x => {
return x.checked;
});
}
public bulkUpdate() {
const anyRoles = this.availableClaims.some(x => {
return x.enabled;
});
this.users.forEach(x => {
if(!x.checked) {
return;
}
if(anyRoles) {
x.claims = this.availableClaims;
}
if(this.bulkEpisodeLimit && this.bulkEpisodeLimit > 0) {
x.episodeRequestLimit = this.bulkEpisodeLimit;
}
if(this.bulkMovieLimit && this.bulkMovieLimit > 0) {
x.movieRequestLimit = this.bulkMovieLimit;
}
this.identityService.updateUser(x).subscribe(y => {
if(!y.successful) {
this.notificationService.error(`Could not update user ${x.userName}. Reason ${y.errors[0]}`);
}
});
});
this.notificationService.success(`Updated users`);
this.showBulkEdit = false;
this.bulkMovieLimit = undefined;
this.bulkEpisodeLimit = undefined;
}
public setOrder(value: string) {
if (this.order === value) {

@ -2,7 +2,7 @@
import { NgModule } from "@angular/core";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { RouterModule, Routes } from "@angular/router";
import { ConfirmationService, ConfirmDialogModule, MultiSelectModule, TooltipModule } from "primeng/primeng";
import { ConfirmationService, ConfirmDialogModule, MultiSelectModule, SidebarModule, TooltipModule } from "primeng/primeng";
import { NgbModule } from "@ng-bootstrap/ng-bootstrap";
@ -37,6 +37,7 @@ const routes: Routes = [
ConfirmDialogModule,
TooltipModule,
OrderModule,
SidebarModule,
],
declarations: [
UserManagementComponent,

@ -411,15 +411,6 @@ $border-radius: 10px;
line-height: 13px;
}
.small-checkbox label {
display: inline-block;
cursor: pointer;
position: relative;
padding-left: 25px;
margin-right: 15px;
font-size: 13px;
margin-bottom: 10px;
}
.radio label {
display: inline-block;
@ -456,6 +447,15 @@ $border-radius: 10px;
line-height: 13px;
}
.small-checkbox label {
display: inline-block;
cursor: pointer;
position: relative;
padding-left: 25px;
margin-right: 15px;
font-size: 13px;
margin-bottom: 10px;
}
.small-checkbox label:before {

@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using System.Web;
using AutoMapper;
using Hangfire;
using Microsoft.AspNetCore.Authorization;
@ -472,8 +472,16 @@ namespace Ombi.Controllers
// Get the roles
var userRoles = await UserManager.GetRolesAsync(user);
// Am I modifying myself?
var modifyingSelf = user.UserName.Equals(User.Identity.Name, StringComparison.CurrentCultureIgnoreCase);
foreach (var role in userRoles)
{
if (modifyingSelf && role.Equals(OmbiRoles.Admin))
{
// We do not want to remove the admin role from yourself, this must be an accident
continue;
}
await UserManager.RemoveFromRoleAsync(user, role);
}
@ -613,25 +621,54 @@ namespace Ombi.Controllers
return defaultMessage;
}
// We have the user
var token = await UserManager.GeneratePasswordResetTokenAsync(user);
// We now need to email the user with this token
var emailSettings = await EmailSettings.GetSettingsAsync();
var customizationSettings = await CustomizationSettings.GetSettingsAsync();
var appName = (string.IsNullOrEmpty(customizationSettings.ApplicationName)
? "Ombi"
: customizationSettings.ApplicationName);
var emailSettings = await EmailSettings.GetSettingsAsync();
customizationSettings.AddToUrl("/token?token=");
var url = customizationSettings.ApplicationUrl;
await EmailProvider.SendAdHoc(new NotificationMessage
if (user.UserType == UserType.PlexUser)
{
await EmailProvider.SendAdHoc(new NotificationMessage
{
To = user.Email,
Subject = $"{appName} Password Reset",
Message =
$"You recently made a request to reset your {appName} account. Please click the link below to complete the process.<br/><br/>" +
$"<a href=\"https://www.plex.tv/sign-in/password-reset/\"> Reset </a>"
}, emailSettings);
}
else if (user.UserType == UserType.EmbyUser && user.IsEmbyConnect)
{
To = user.Email,
Subject = $"{appName} Password Reset",
Message = $"You recently made a request to reset your {appName} account. Please click the link below to complete the process.<br/><br/>" +
$"<a href=\"{url}{token}\"> Reset </a>"
}, emailSettings);
await EmailProvider.SendAdHoc(new NotificationMessage
{
To = user.Email,
Subject = $"{appName} Password Reset",
Message =
$"You recently made a request to reset your {appName} account.<br/><br/>" +
$"To reset your password you need to go to <a href=\"https://emby.media/community/index.php\">Emby.Media</a> and then click on your Username > Edit Profile > Email and Password"
}, emailSettings);
}
else
{
// We have the user
var token = await UserManager.GeneratePasswordResetTokenAsync(user);
var encodedToken = WebUtility.UrlEncode(token);
await EmailProvider.SendAdHoc(new NotificationMessage
{
To = user.Email,
Subject = $"{appName} Password Reset",
Message =
$"You recently made a request to reset your {appName} account. Please click the link below to complete the process.<br/><br/>" +
$"<a href=\"{url}{encodedToken}\"> Reset </a>"
}, emailSettings);
}
return defaultMessage;
}

@ -136,14 +136,17 @@ namespace Ombi.Controllers
var notificationModel = new NotificationOptions
{
RequestId = 0,
RequestId = i.RequestId ?? 0,
DateTime = DateTime.Now,
NotificationType = NotificationType.Issue,
RequestType = i.RequestType,
Recipient = string.Empty,
AdditionalInformation = $"{i.Subject} | {i.Description}"
AdditionalInformation = $"{i.Subject} | {i.Description}",
UserId = i.UserReportedId
};
AddIssueNotificationSubstitutes(notificationModel, i, User.Identity.Name);
BackgroundJob.Enqueue(() => _notification.Publish(notificationModel));
return i.Id;
@ -190,22 +193,56 @@ namespace Ombi.Controllers
[HttpPost("comments")]
public async Task<IssueComments> AddComment([FromBody] NewIssueCommentViewModel comment)
{
var userId = await _userManager.Users.Where(x => User.Identity.Name == x.UserName).Select(x => x.Id)
var user = await _userManager.Users.Where(x => User.Identity.Name == x.UserName)
.FirstOrDefaultAsync();
var issue = await _issues.GetAll().Include(x => x.UserReported).FirstOrDefaultAsync(x => x.Id == comment.IssueId);
if (issue == null)
{
return null;
}
var newComment = new IssueComments
{
Comment = comment.Comment,
Date = DateTime.UtcNow,
UserId = userId,
UserId = user.Id,
IssuesId = comment.IssueId,
};
var notificationModel = new NotificationOptions
{
RequestId = issue.RequestId ?? 0,
DateTime = DateTime.Now,
NotificationType = NotificationType.IssueComment,
RequestType = issue.RequestType,
UserId = user.Id
};
var isAdmin = await _userManager.IsInRoleAsync(user, OmbiRoles.Admin);
AddIssueNotificationSubstitutes(notificationModel, issue, issue.UserReported.UserAlias);
notificationModel.Substitutes.Add("NewIssueComment", comment.Comment);
notificationModel.Substitutes.Add("AdminComment", isAdmin.ToString());
if (isAdmin)
{
// Send to user
notificationModel.Recipient = issue.UserReported.Email;
}
else
{
notificationModel.Recipient = user.Email;
}
BackgroundJob.Enqueue(() => _notification.Publish(notificationModel));
return await _issueComments.Add(newComment);
}
[HttpPost("status")]
public async Task<bool> UpdateStatus([FromBody] IssueStateViewModel model)
{
var issue = await _issues.Find(model.IssueId);
var user = await _userManager.Users.Where(x => User.Identity.Name == x.UserName)
.FirstOrDefaultAsync();
var issue = await _issues.GetAll().Include(x => x.UserReported).FirstOrDefaultAsync(x => x.Id == model.IssueId);
if (issue == null)
{
return false;
@ -214,20 +251,38 @@ namespace Ombi.Controllers
issue.Status = model.Status;
await _issues.SaveChangesAsync();
var notificationModel = new NotificationOptions
if (issue.Status == IssueStatus.Resolved)
{
RequestId = 0,
DateTime = DateTime.Now,
NotificationType = NotificationType.Issue,
RequestType = issue.RequestType,
Recipient = !string.IsNullOrEmpty(issue.UserReported?.Email) ? issue.UserReported.Email : string.Empty,
AdditionalInformation = $"{issue.Subject} | {issue.Description}"
};
var notificationModel = new NotificationOptions
{
RequestId = 0,
DateTime = DateTime.Now,
NotificationType = NotificationType.IssueResolved,
RequestType = issue.RequestType,
Recipient = !string.IsNullOrEmpty(issue.UserReported?.Email)
? issue.UserReported.Email
: string.Empty,
AdditionalInformation = $"{issue.Subject} | {issue.Description}",
UserId = user.Id,
};
AddIssueNotificationSubstitutes(notificationModel, issue, issue.UserReported?.UserAlias ?? string.Empty);
BackgroundJob.Enqueue(() => _notification.Publish(notificationModel));
BackgroundJob.Enqueue(() => _notification.Publish(notificationModel));
}
return true;
}
private static void AddIssueNotificationSubstitutes(NotificationOptions notificationModel, Issues issue, string issueReportedUsername)
{
notificationModel.Substitutes.Add("Title", issue.Title);
notificationModel.Substitutes.Add("IssueDescription", issue.Description);
notificationModel.Substitutes.Add("IssueCategory", issue.IssueCategory?.Value);
notificationModel.Substitutes.Add("IssueStatus", issue.Status.ToString());
notificationModel.Substitutes.Add("IssueSubject", issue.Subject);
notificationModel.Substitutes.Add("IssueUser", issueReportedUsername);
}
}
}

@ -17,7 +17,7 @@ namespace Ombi.Controllers
}
private ILogger Logger { get; }
private const string Message = "Exception: {0} at {1}. Stacktrade {2}";
private const string Message = "Exception: {0} at {1}. Stacktrace {2}";
[HttpPost]
public IActionResult Log([FromBody]UiLoggingModel l)

@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Ombi.Attributes;
using Ombi.Core.Authentication;
using Ombi.Helpers;
using Ombi.Models;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
namespace Ombi.Controllers
{
[ApiV1]
[Authorize]
[Produces("application/json")]
public class MobileController : Controller
{
public MobileController(IRepository<NotificationUserId> notification, OmbiUserManager user)
{
_notification = notification;
_userManager = user;
}
private readonly IRepository<NotificationUserId> _notification;
private readonly OmbiUserManager _userManager;
[HttpPost("Notification")]
[ApiExplorerSettings(IgnoreApi = true)]
public async Task<IActionResult> AddNotitficationId([FromBody] NotificationIdBody body)
{
if (body?.PlayerId.HasValue() ?? false)
{
var user = await _userManager.Users.FirstOrDefaultAsync(x => x.UserName == User.Identity.Name);
// Check if we already have this notification id
var alreadyExists = await _notification.GetAll().AnyAsync(x => x.PlayerId == body.PlayerId && x.UserId == user.Id);
if (alreadyExists)
{
return Ok();
}
// let's add it
await _notification.Add(new NotificationUserId
{
PlayerId = body.PlayerId,
UserId = user.Id,
AddedAt = DateTime.Now,
});
return Ok();
}
return BadRequest();
}
[HttpGet("Notification")]
[ApiExplorerSettings(IgnoreApi = true)]
[Admin]
public IEnumerable<MobileUsersViewModel> GetRegisteredMobileUsers()
{
var users = _userManager.Users.Include(x => x.NotificationUserIds).Where(x => x.NotificationUserIds.Any());
var vm = new List<MobileUsersViewModel>();
foreach (var u in users)
{
vm.Add(new MobileUsersViewModel
{
Username = u.UserAlias,
Devices = u.NotificationUserIds.Count
});
}
return vm;
}
}
}

@ -765,6 +765,40 @@ namespace Ombi.Controllers
return model;
}
/// <summary>
/// Saves the Mobile notification settings.
/// </summary>
/// <param name="model">The model.</param>
/// <returns></returns>
[HttpPost("notifications/mobile")]
public async Task<bool> MobileNotificationSettings([FromBody] MobileNotificationsViewModel model)
{
// Save the email settings
var settings = Mapper.Map<MobileNotificationSettings>(model);
var result = await Save(settings);
// Save the templates
await TemplateRepository.UpdateRange(model.NotificationTemplates);
return result;
}
/// <summary>
/// Gets the Mobile Notification Settings.
/// </summary>
/// <returns></returns>
[HttpGet("notifications/mobile")]
public async Task<MobileNotificationsViewModel> MobileNotificationSettings()
{
var settings = await Get<MobileNotificationSettings>();
var model = Mapper.Map<MobileNotificationsViewModel>(settings);
// Lookup to see if we have any templates saved
model.NotificationTemplates = await BuildTemplates(NotificationAgent.Mobile);
return model;
}
private async Task<List<NotificationTemplates>> BuildTemplates(NotificationAgent agent)
{
var templates = await TemplateRepository.GetAllTemplates(agent);

@ -0,0 +1,8 @@
namespace Ombi.Models
{
public class MobileUsersViewModel
{
public string Username { get; set; }
public int Devices { get; set; }
}
}

@ -0,0 +1,7 @@
namespace Ombi.Models
{
public class NotificationIdBody
{
public string PlayerId { get; set; }
}
}

@ -28,6 +28,12 @@ namespace Ombi
{
host = o.Host;
storagePath = o.StoragePath;
}).WithNotParsed(err =>
{
foreach (var e in err)
{
Console.WriteLine(e);
}
});
Console.WriteLine(HelpOutput(result));
@ -46,7 +52,7 @@ namespace Ombi
url = new ApplicationConfiguration
{
Type = ConfigurationTypes.Url,
Value = "http://*"
Value = "http://*:5000"
};
ctx.ApplicationConfigurations.Add(url);
@ -87,13 +93,13 @@ namespace Ombi
public class Options
{
[Option('h', "host", Required = false, HelpText =
[Option("host", Required = false, HelpText =
"Set to a semicolon-separated (;) list of URL prefixes to which the server should respond. For example, http://localhost:123." +
" Use \"*\" to indicate that the server should listen for requests on any IP address or hostname using the specified port and protocol (for example, http://*:5000). " +
"The protocol (http:// or https://) must be included with each URL. Supported formats vary between servers.", Default = "http://*:5000")]
public string Host { get; set; }
[Option('s', "storage", Required = false, HelpText = "Storage path, where we save the logs and database")]
[Option("storage", Required = false, HelpText = "Storage path, where we save the logs and database")]
public string StoragePath { get; set; }
}

@ -57,7 +57,6 @@ namespace Ombi
config = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.RollingFile(Path.Combine(env.ContentRootPath, "Logs", "log-{Date}.txt"))
.WriteTo.SQLite("Ombi.db", "Logs", LogEventLevel.Debug)
.CreateLogger();
}
else
@ -65,7 +64,6 @@ namespace Ombi
config = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.RollingFile(Path.Combine(StoragePath.StoragePath, "Logs", "log-{Date}.txt"))
.WriteTo.SQLite(Path.Combine(StoragePath.StoragePath, "Ombi.db"), "Logs", LogEventLevel.Debug)
.CreateLogger();
}
Log.Logger = config;

@ -2,9 +2,9 @@
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
"Default": "Trace",
"System": "Trace",
"Microsoft": "Warning"
}
}
}

@ -4,7 +4,8 @@
"LogLevel": {
"Default": "Debug",
"System": "Debug",
"Microsoft": "None"
"Microsoft": "None",
"Hangfire": "None"
}
},
"ApplicationSettings": {

@ -5,6 +5,7 @@ const run = require('gulp-run');
const runSequence = require('run-sequence');
const del = require('del');
const path = require('path');
const fs = require('fs');
const outputDir = './wwwroot/dist';
@ -23,26 +24,56 @@ function getEnvOptions() {
}
}
gulp.task('vendor', function () {
return run('webpack --config webpack.config.vendor.ts' + getEnvOptions()).exec();
function webpack(vendor) {
return run(`webpack --config webpack.config${vendor ? '.vendor' : ''}.ts${getEnvOptions()}`).exec();
}
gulp.task('vendor', () => {
let build = false;
const vendorPath = path.join(outputDir, "vendor.js");
const vendorExists = fs.existsSync(vendorPath);
if (vendorExists) {
const vendorStat = fs.statSync(vendorPath);
const packageStat = fs.statSync("package.json");
const vendorConfigStat = fs.statSync("webpack.config.vendor.ts");
if (packageStat.mtime > vendorStat.mtime) {
build = true;
}
if (vendorConfigStat.mtime > vendorStat.mtime) {
build = true;
}
} else {
build = true;
}
if (build) {
return webpack(true);
}
});
gulp.task('main', function () {
return run('webpack --config webpack.config.ts' + getEnvOptions()).exec();
gulp.task('vendor_force', () => {
return webpack(true);
})
gulp.task('main', () => {
return webpack()
});
gulp.task('prod_var', function () {
gulp.task('prod_var', () => {
global.prod = true;
})
gulp.task('analyse_var', function () {
gulp.task('analyse_var', () => {
global.analyse = true;
})
gulp.task('clean', function() {
del.sync(outputDir, { force: true });
gulp.task('clean', () => {
del.sync(outputDir, { force: true });
});
gulp.task('lint', () => run("npm run lint").exec());
gulp.task('build', callback => runSequence('vendor', 'main', callback));
gulp.task('analyse', callback => runSequence('analyse_var', 'build'));
gulp.task('full', callback => runSequence('clean', 'build'));

@ -21,6 +21,7 @@
"Request": "Request",
"Denied":"Denied",
"Approve":"Approve",
"PartlyAvailable": "Partly Available",
"Errors": {
"Validation": "Please check your entered values"
}
@ -86,6 +87,21 @@
"NowPlayingMovies": "Now Playing Movies",
"HomePage": "Home Page",
"Trailer": "Trailer"
},
"TvShows": {
"Popular":"Popular",
"Trending":"Trending",
"MostWatched":"Most Watched",
"MostAnticipated":"Most Anticipated",
"Results":"Results",
"AirDate":"Air Date:",
"AllSeasons":"All Seasons",
"FirstSeason":"First Season",
"LatestSeason":"Latest Season",
"Select":"Select ...",
"SubmitRequest":"Submit Request",
"Season":"Season: {{seasonNumber}}",
"SelectAllInSeason":"Select All in Season {{seasonNumber}}"
}
},
"Requests": {
@ -113,7 +129,8 @@
"AirDate":"AirDate",
"GridStatus":"Status",
"ReportIssue":"Report Issue",
"Filter":"Filter"
"Filter":"Filter",
"SeasonNumberHeading":"Season: {seasonNumber}"
},
"Issues":{
"Title":"Issues",

Loading…
Cancel
Save