diff --git a/CHANGELOG.md b/CHANGELOG.md index a23ed6887..c317fbc17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,84 @@ ## (unreleased) +### **New Features** + +- Added the Recently Added Newsletter! You are welcome. [tidusjar] + +- Added a new scrollbar to Ombi. [tidusjar] + +- Added the ability to automatically generate the API Key on startup if it does not exist #2070. [tidusjar] + +- Updated npm dependancies. [Jamie] + +- Update README.md. [Jamie] + +- Update README.md. [Jamie] + +- Update ISSUE_TEMPLATE.md. [Jamie] + +- Update appveyor.yml. [Jamie] + +- Added recently added stuff. [Jamie] + +- Added the recently added engine with some basic methods. [Jamie] + +- Added the ability to refresh out backend metadata (#2078) [Jamie] + +### **Fixes** + +- Specific favicons for different platforms. [louis-lau] + +- MovieDbId was switched to string fron number so accomodated for change. [Anojh] + +- Removing duplicate functions. [Anojh Thayaparan] + +- Conflict resolving and adopting Jamie's new method. [Anojh] + +- Wrote new calls to just get poster and bg. [Anojh] + +- Fix for issue #1907, which is to add content poster and bg to issue details page. [Anojh] + +- Dynamic Background Animation. [Anojh] + +- Improved the message for #2037. [tidusjar] + +- Improved the way we use the notification variables, we have now split out the Username and Alias (Requested User is depricated but not removed) [tidusjar] + +- Removed redundant timers. [Anojh] + +- More optimizations by reducing requests. [Anojh] + +- Improved version. [Anojh] + +- Dynamic Background Animation. [Anojh] + +- Fixed #2055 and #1903. [Jamie] + +- 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] + +- Fixed build. [Jamie] + +- Fixed #2074 and #2079. [Jamie] + + +## v3.0.3030 (2018-03-14) + +### **New Features** + +- Updated the .Net core dependancies #2072. [Jamie] + +### **Fixes** + +- Delete Ombi.testdb. [Jamie] + + +## v3.0.3020 (2018-03-13) + ### **Fixes** - Small memory improvements in the Plex Sync. [Jamie] @@ -22,6 +100,8 @@ - Experimental, set the Webpack base root to the ombi base path if we have it. This should hopefully fix the reverse proxy issues. [Jamie] +- Fixed #2056. [tidusjar] + ## v3.0.3000 (2018-03-09) diff --git a/src/Ombi.Api.FanartTv/FanartTvApi.cs b/src/Ombi.Api.FanartTv/FanartTvApi.cs index 3d84681a5..bc819311c 100644 --- a/src/Ombi.Api.FanartTv/FanartTvApi.cs +++ b/src/Ombi.Api.FanartTv/FanartTvApi.cs @@ -32,9 +32,9 @@ namespace Ombi.Api.FanartTv } } - public async Task GetMovieImages(int theMovieDbId, string token) + public async Task GetMovieImages(string movieOrImdbId, string token) { - var request = new Request($"movies/{theMovieDbId}", Endpoint, HttpMethod.Get); + var request = new Request($"movies/{movieOrImdbId}", Endpoint, HttpMethod.Get); request.AddHeader("api-key", token); return await Api.Request(request); diff --git a/src/Ombi.Api.FanartTv/IFanartTvApi.cs b/src/Ombi.Api.FanartTv/IFanartTvApi.cs index 58213eeaf..3e1a21fe4 100644 --- a/src/Ombi.Api.FanartTv/IFanartTvApi.cs +++ b/src/Ombi.Api.FanartTv/IFanartTvApi.cs @@ -5,7 +5,7 @@ namespace Ombi.Api.FanartTv { public interface IFanartTvApi { - Task GetMovieImages(int theMovieDbId, string token); + Task GetMovieImages(string movieOrImdbId, string token); Task GetTvImages(int tvdbId, string token); } } \ No newline at end of file diff --git a/src/Ombi.Api.Mattermost/IMattermostApi.cs b/src/Ombi.Api.Mattermost/IMattermostApi.cs index b07802b25..b8b77864c 100644 --- a/src/Ombi.Api.Mattermost/IMattermostApi.cs +++ b/src/Ombi.Api.Mattermost/IMattermostApi.cs @@ -5,6 +5,6 @@ namespace Ombi.Api.Mattermost { public interface IMattermostApi { - Task PushAsync(string webhook, MattermostBody message); + Task PushAsync(string webhook, MattermostMessage message); } } \ No newline at end of file diff --git a/src/Ombi.Api.Mattermost/MattermostApi.cs b/src/Ombi.Api.Mattermost/MattermostApi.cs index c20641aca..15a954b47 100644 --- a/src/Ombi.Api.Mattermost/MattermostApi.cs +++ b/src/Ombi.Api.Mattermost/MattermostApi.cs @@ -14,14 +14,10 @@ namespace Ombi.Api.Mattermost private readonly IApi _api; - public async Task PushAsync(string webhook, MattermostBody message) + public async Task PushAsync(string webhook, MattermostMessage message) { - var request = new Request(string.Empty, webhook, HttpMethod.Post); - - request.AddJsonBody(message); - - var result = await _api.RequestContent(request); - return result; + var client = new MatterhookClient(webhook); + await client.PostAsync(_api, message); } } } diff --git a/src/Ombi.Api.Mattermost/Models/MattermostClient.cs b/src/Ombi.Api.Mattermost/Models/MattermostClient.cs new file mode 100644 index 000000000..96d3e33f4 --- /dev/null +++ b/src/Ombi.Api.Mattermost/Models/MattermostClient.cs @@ -0,0 +1,168 @@ +/// +/// +/// +/// Code taken from https://github.com/PromoFaux/Matterhook.NET.MatterhookClient +/// +/// +/// +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +namespace Ombi.Api.Mattermost.Models +{ + public class MatterhookClient + { + private readonly Uri _webhookUrl; + private readonly HttpClient _httpClient = new HttpClient(); + + /// + /// Create a new Mattermost Client + /// + /// The URL of your Mattermost Webhook + /// Timeout Value (Default 100) + public MatterhookClient(string webhookUrl, int timeoutSeconds = 100) + { + if (!Uri.TryCreate(webhookUrl, UriKind.Absolute, out _webhookUrl)) + throw new ArgumentException("Mattermost URL invalid"); + + _httpClient.Timeout = new TimeSpan(0, 0, 0, timeoutSeconds); + } + + public MattermostMessage CloneMessage(MattermostMessage inMsg) + { + var outMsg = new MattermostMessage + { + Text = "", + Channel = inMsg.Channel, + Username = inMsg.Username, + IconUrl = inMsg.IconUrl + }; + + return outMsg; + } + + private static MattermostAttachment CloneAttachment(MattermostAttachment inAtt) + { + var outAtt = new MattermostAttachment + { + AuthorIcon = inAtt.AuthorIcon, + AuthorLink = inAtt.AuthorLink, + AuthorName = inAtt.AuthorName, + Color = inAtt.Color, + Fallback = inAtt.Fallback, + Fields = inAtt.Fields, + ImageUrl = inAtt.ImageUrl, + Pretext = inAtt.Pretext, + ThumbUrl = inAtt.ThumbUrl, + Title = inAtt.Title, + TitleLink = inAtt.TitleLink, + Text = "" + }; + return outAtt; + } + + /// + /// Post Message to Mattermost server. Messages will be automatically split if total text length > 4000 + /// + /// + /// The messsage you wish to send + /// + public async Task PostAsync(IApi api, MattermostMessage inMessage) + { + try + { + var outMessages = new List(); + + var msgCount = 0; + + var lines = new string[] { }; + if (inMessage.Text != null) + { + lines = inMessage.Text.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None); + } + + //start with one cloned inMessage in the list + outMessages.Add(CloneMessage(inMessage)); + + //add text from original. If we go over 3800, we'll split it to a new inMessage. + foreach (var line in lines) + { + + if (line.Length + outMessages[msgCount].Text.Length > 3800) + { + + msgCount += 1; + outMessages.Add(CloneMessage(inMessage)); + } + + outMessages[msgCount].Text += $"{line}\r\n"; + } + + //Length of text on the last (or first if only one) inMessage. + var lenMessageText = outMessages[msgCount].Text.Length; + + //does our original have attachments? + if (inMessage.Attachments?.Any() ?? false) + { + outMessages[msgCount].Attachments = new List(); + + //loop through them in a similar fashion to the inMessage text above. + foreach (var att in inMessage.Attachments) + { + //add this attachment to the outgoing message + outMessages[msgCount].Attachments.Add(CloneAttachment(att)); + //get a count of attachments on this message, and subtract one so we know the index of the current new attachment + var attIndex = outMessages[msgCount].Attachments.Count - 1; + + //Get the text lines + lines = att.Text.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None); + + foreach (var line in lines) + { + //Get the total length of all attachments on the current outgoing message + var lenAllAttsText = outMessages[msgCount].Attachments.Sum(a => a.Text.Length); + + if (lenMessageText + lenAllAttsText + line.Length > 3800) + { + msgCount += 1; + attIndex = 0; + outMessages.Add(CloneMessage(inMessage)); + outMessages[msgCount].Attachments = new List { CloneAttachment(att) }; + } + + outMessages[msgCount].Attachments[attIndex].Text += $"{line}\r\n"; + } + } + } + + + if (outMessages.Count > 1) + { + var num = 1; + foreach (var msg in outMessages) + { + msg.Text = $"`({num}/{msgCount + 1}): ` " + msg.Text; + num++; + } + } + + foreach (var msg in outMessages) + { + var request = new Request("", _webhookUrl.ToString(), HttpMethod.Post); + request.AddJsonBody(msg); + await api.Request(request); + } + } + catch (Exception e) + { + Console.WriteLine(e.Message); + throw; + } + } + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Mattermost/Models/MattermostMessage.cs b/src/Ombi.Api.Mattermost/Models/MattermostMessage.cs new file mode 100644 index 000000000..d8fe52b09 --- /dev/null +++ b/src/Ombi.Api.Mattermost/Models/MattermostMessage.cs @@ -0,0 +1,181 @@ +/// +/// +/// +/// Code taken from https://github.com/PromoFaux/Matterhook.NET.MatterhookClient +/// +/// +/// +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Ombi.Api.Mattermost.Models + +{ + public class MattermostMessage + { + + //https://docs.mattermost.com/developer/webhooks-incoming.html + + /// + /// Channel to post to + /// + [JsonProperty(PropertyName = "channel")] + public string Channel { get; set; } + + /// + /// Username for bot + /// + [JsonProperty(PropertyName = "username")] + public string Username { get; set; } + + /// + /// Bot/User Icon + /// + [JsonProperty(PropertyName = "icon_url")] + public Uri IconUrl { get; set; } + + /// + /// Message body. Supports Markdown + /// + [JsonProperty(PropertyName = "text")] + public string Text { get; set; } + + /// + /// Richtext attachments + /// + [JsonProperty(PropertyName = "attachments")] + public List Attachments { get; set; } + + } + + /// + /// https://docs.mattermost.com/developer/message-attachments.html#message-attachments + /// + public class MattermostAttachment + { + //https://docs.mattermost.com/developer/message-attachments.html#attachment-options + #region AttachmentOptions + + /// + /// A required plain-text summary of the post. This is used in notifications, and in clients that don’t support formatted text (eg IRC). + /// + [JsonProperty(PropertyName = "fallback")] + public string Fallback { get; set; } + + /// + /// A hex color code that will be used as the left border color for the attachment. If not specified, it will default to match the left hand sidebar header background color. + /// + [JsonProperty(PropertyName = "color")] + public string Color { get; set; } + + /// + /// Optional text that should appear above the formatted data + /// + [JsonProperty(PropertyName = "pretext")] + public string Pretext { get; set; } + + /// + /// The text to be included in the attachment. It can be formatted using Markdown. If it includes more than 300 characters or more than 5 line breaks, the message will be collapsed and a “Show More” link will be added to expand the message. + /// + [JsonProperty(PropertyName = "text")] + public string Text { get; set; } + + #endregion + + //https://docs.mattermost.com/developer/message-attachments.html#author-details + #region AuthorDetails + + /// + /// An optional name used to identify the author. It will be included in a small section at the top of the attachment. + /// + [JsonProperty(PropertyName = "author_name")] + public string AuthorName { get; set; } + + /// + /// An optional URL used to hyperlink the author_name. If no author_name is specified, this field does nothing. + /// + [JsonProperty(PropertyName = "author_link")] + public Uri AuthorLink { get; set; } + + /// + /// An optional URL used to display a 16x16 pixel icon beside the author_name. + /// + [JsonProperty(PropertyName = "author_icon")] + public Uri AuthorIcon { get; set; } + + #endregion + + //https://docs.mattermost.com/developer/message-attachments.html#titles + #region Titles + + /// + /// An optional title displayed below the author information in the attachment. + /// + [JsonProperty(PropertyName = "title")] + public string Title { get; set; } + + /// + /// An optional URL used to hyperlink the title. If no title is specified, this field does nothing. + /// + [JsonProperty(PropertyName = "title_link")] + public Uri TitleLink { get; set; } + + #endregion + + + #region Fields + + /// + /// Fields can be included as an optional array within attachments, and are used to display information in a table format inside the attachment. + /// + [JsonProperty(PropertyName = "fields")] + public List Fields { get; set; } + + #endregion + + //https://docs.mattermost.com/developer/message-attachments.html#images + #region Images + + /// + /// An optional URL to an image file (GIF, JPEG, PNG, or BMP) that is displayed inside a message attachment. + /// Large images are resized to a maximum width of 400px or a maximum height of 300px, while still maintaining the original aspect ratio. + /// + [JsonProperty(PropertyName = "image_url")] + public Uri ImageUrl { get; set; } + + /// + /// An optional URL to an image file(GIF, JPEG, PNG, or BMP) that is displayed as a 75x75 pixel thumbnail on the right side of an attachment. + /// We recommend using an image that is already 75x75 pixels, but larger images will be scaled down with the aspect ratio maintained. + /// + [JsonProperty(PropertyName = "thumb_url")] + public Uri ThumbUrl { get; set; } + + + #endregion + } + + /// + /// https://docs.mattermost.com/developer/message-attachments.html#fieldshttps://docs.mattermost.com/developer/message-attachments.html#fields + /// + public class MattermostField + { + /// + /// A title shown in the table above the value. + /// + [JsonProperty(PropertyName = "title")] + public string Title { get; set; } + + /// + /// The text value of the field. It can be formatted using Markdown. + /// + [JsonProperty(PropertyName = "value")] + public string Value { get; set; } + + /// + /// Optionally set to “True” or “False” to indicate whether the value is short enough to be displayed beside other values. + /// + [JsonProperty(PropertyName = "short")] + public bool Short { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Api/Api.cs b/src/Ombi.Api/Api.cs index c12258b8e..98fff5e0c 100644 --- a/src/Ombi.Api/Api.cs +++ b/src/Ombi.Api/Api.cs @@ -1,4 +1,7 @@ -using System.IO; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; @@ -6,6 +9,7 @@ using System.Xml.Serialization; using Newtonsoft.Json; using Microsoft.Extensions.Logging; using Ombi.Helpers; +using Polly; namespace Ombi.Api { @@ -36,6 +40,30 @@ namespace Ombi.Api if (!httpResponseMessage.IsSuccessStatusCode) { LogError(request, httpResponseMessage); + if (request.Retry) + { + + var result = Policy + .Handle() + .OrResult(r => request.StatusCodeToRetry.Contains(r.StatusCode)) + .WaitAndRetryAsync(new[] + { + TimeSpan.FromSeconds(10), + }, (exception, timeSpan, context) => + { + + Logger.LogError(LoggingEvents.Api, + $"Retrying RequestUri: {request.FullUri} Because we got Status Code: {exception?.Result?.StatusCode}"); + }); + + httpResponseMessage = await result.ExecuteAsync(async () => + { + using (var req = await httpRequestMessage.Clone()) + { + return await _client.SendAsync(req); + } + }); + } } // do something with the response diff --git a/src/Ombi.Api/HttpRequestExtnesions.cs b/src/Ombi.Api/HttpRequestExtnesions.cs new file mode 100644 index 000000000..fa2ded97d --- /dev/null +++ b/src/Ombi.Api/HttpRequestExtnesions.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; + +namespace Ombi.Api +{ + public static class HttpRequestExtnesions + { + public static async Task Clone(this HttpRequestMessage request) + { + var clone = new HttpRequestMessage(request.Method, request.RequestUri) + { + Content = await request.Content.Clone(), + Version = request.Version + }; + foreach (KeyValuePair prop in request.Properties) + { + clone.Properties.Add(prop); + } + foreach (KeyValuePair> header in request.Headers) + { + clone.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + + return clone; + } + + public static async Task Clone(this HttpContent content) + { + if (content == null) return null; + + var ms = new MemoryStream(); + await content.CopyToAsync(ms); + ms.Position = 0; + + var clone = new StreamContent(ms); + foreach (KeyValuePair> header in content.Headers) + { + clone.Headers.Add(header.Key, header.Value); + } + return clone; + } + } +} \ No newline at end of file diff --git a/src/Ombi.Api/Ombi.Api.csproj b/src/Ombi.Api/Ombi.Api.csproj index e89eb54b1..325f316b8 100644 --- a/src/Ombi.Api/Ombi.Api.csproj +++ b/src/Ombi.Api/Ombi.Api.csproj @@ -11,6 +11,7 @@ + diff --git a/src/Ombi.Api/Request.cs b/src/Ombi.Api/Request.cs index 16dd38055..e4120ed9c 100644 --- a/src/Ombi.Api/Request.cs +++ b/src/Ombi.Api/Request.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Net; using System.Net.Http; using System.Text; @@ -25,6 +26,9 @@ namespace Ombi.Api public string BaseUrl { get; } public HttpMethod HttpMethod { get; } + public bool Retry { get; set; } + public List StatusCodeToRetry { get; set; } = new List(); + public Action OnBeforeDeserialization { get; set; } private string FullUrl diff --git a/src/Ombi.Core/Engine/IRecentlyAddedEngine.cs b/src/Ombi.Core/Engine/IRecentlyAddedEngine.cs new file mode 100644 index 000000000..3087ec829 --- /dev/null +++ b/src/Ombi.Core/Engine/IRecentlyAddedEngine.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Ombi.Core.Models; + +namespace Ombi.Core.Engine +{ + public interface IRecentlyAddedEngine + { + IEnumerable GetRecentlyAddedMovies(); + IEnumerable GetRecentlyAddedMovies(DateTime from, DateTime to); + IEnumerable GetRecentlyAddedTv(DateTime from, DateTime to, bool groupBySeason); + IEnumerable GetRecentlyAddedTv(bool groupBySeason); + Task UpdateRecentlyAddedDatabase(); + } +} \ No newline at end of file diff --git a/src/Ombi.Core/Engine/MovieRequestEngine.cs b/src/Ombi.Core/Engine/MovieRequestEngine.cs index 0b376386f..f4b0ee48c 100644 --- a/src/Ombi.Core/Engine/MovieRequestEngine.cs +++ b/src/Ombi.Core/Engine/MovieRequestEngine.cs @@ -54,7 +54,7 @@ namespace Ombi.Core.Engine { Result = false, Message = "There was an issue adding this movie!", - ErrorMessage = $"TheMovieDb didn't have any information for ID {model.TheMovieDbId}" + ErrorMessage = $"Please try again later" }; } var fullMovieName = diff --git a/src/Ombi.Core/Engine/RecentlyAddedEngine.cs b/src/Ombi.Core/Engine/RecentlyAddedEngine.cs new file mode 100644 index 000000000..59be359f8 --- /dev/null +++ b/src/Ombi.Core/Engine/RecentlyAddedEngine.cs @@ -0,0 +1,224 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Ombi.Core.Models; +using Ombi.Helpers; +using Ombi.Store.Entities; +using Ombi.Store.Repository; +using RecentlyAddedType = Ombi.Store.Entities.RecentlyAddedType; + +namespace Ombi.Core.Engine +{ + public class RecentlyAddedEngine : IRecentlyAddedEngine + { + public RecentlyAddedEngine(IPlexContentRepository plex, IEmbyContentRepository emby, IRepository recentlyAdded) + { + _plex = plex; + _emby = emby; + _recentlyAddedLog = recentlyAdded; + } + + private readonly IPlexContentRepository _plex; + private readonly IEmbyContentRepository _emby; + private readonly IRepository _recentlyAddedLog; + + public IEnumerable GetRecentlyAddedMovies(DateTime from, DateTime to) + { + var plexMovies = _plex.GetAll().Where(x => x.Type == PlexMediaTypeEntity.Movie && x.AddedAt > from && x.AddedAt < to); + var embyMovies = _emby.GetAll().Where(x => x.Type == EmbyMediaType.Movie && x.AddedAt > from && x.AddedAt < to); + + return GetRecentlyAddedMovies(plexMovies, embyMovies).Take(30); + } + + public IEnumerable GetRecentlyAddedMovies() + { + var plexMovies = _plex.GetAll().Where(x => x.Type == PlexMediaTypeEntity.Movie); + var embyMovies = _emby.GetAll().Where(x => x.Type == EmbyMediaType.Movie); + return GetRecentlyAddedMovies(plexMovies, embyMovies); + } + + public IEnumerable GetRecentlyAddedTv(DateTime from, DateTime to, bool groupBySeason) + { + var plexTv = _plex.GetAll().Include(x => x.Seasons).Include(x => x.Episodes).Where(x => x.Type == PlexMediaTypeEntity.Show && x.AddedAt > from && x.AddedAt < to); + var embyTv = _emby.GetAll().Include(x => x.Episodes).Where(x => x.Type == EmbyMediaType.Series && x.AddedAt > from && x.AddedAt < to); + + return GetRecentlyAddedTv(plexTv, embyTv, groupBySeason).Take(30); + } + + + public IEnumerable GetRecentlyAddedTv(bool groupBySeason) + { + var plexTv = _plex.GetAll().Include(x => x.Seasons).Include(x => x.Episodes).Where(x => x.Type == PlexMediaTypeEntity.Show); + var embyTv = _emby.GetAll().Include(x => x.Episodes).Where(x => x.Type == EmbyMediaType.Series); + + return GetRecentlyAddedTv(plexTv, embyTv, groupBySeason); + } + + public async Task UpdateRecentlyAddedDatabase() + { + var plexContent = _plex.GetAll().Include(x => x.Episodes); + var embyContent = _emby.GetAll().Include(x => x.Episodes); + var recentlyAddedLog = new HashSet(); + foreach (var p in plexContent) + { + if (p.Type == PlexMediaTypeEntity.Movie) + { + recentlyAddedLog.Add(new RecentlyAddedLog + { + AddedAt = DateTime.Now, + Type = RecentlyAddedType.Plex, + ContentId = p.Id, + ContentType = ContentType.Parent + }); + } + else + { + // Add the episodes + foreach (var ep in p.Episodes) + { + recentlyAddedLog.Add(new RecentlyAddedLog + { + AddedAt = DateTime.Now, + Type = RecentlyAddedType.Plex, + ContentId = ep.Id, + ContentType = ContentType.Episode + }); + } + } + } + + foreach (var e in embyContent) + { + if (e.Type == EmbyMediaType.Movie) + { + recentlyAddedLog.Add(new RecentlyAddedLog + { + AddedAt = DateTime.Now, + Type = RecentlyAddedType.Emby, + ContentId = e.Id, + ContentType = ContentType.Parent + }); + } + else + { + // Add the episodes + foreach (var ep in e.Episodes) + { + recentlyAddedLog.Add(new RecentlyAddedLog + { + AddedAt = DateTime.Now, + Type = RecentlyAddedType.Emby, + ContentId = ep.Id, + ContentType = ContentType.Episode + }); + } + } + } + await _recentlyAddedLog.AddRange(recentlyAddedLog); + + return true; + } + + private IEnumerable GetRecentlyAddedTv(IQueryable plexTv, IQueryable embyTv, + bool groupBySeason) + { + var model = new HashSet(); + TransformPlexShows(plexTv, model); + TransformEmbyShows(embyTv, model); + + if (groupBySeason) + { + return model.DistinctBy(x => x.SeasonNumber); + } + + return model; + } + + private IEnumerable GetRecentlyAddedMovies(IQueryable plexMovies, IQueryable embyMovies) + { + var model = new HashSet(); + TransformPlexMovies(plexMovies, model); + TransformEmbyMovies(embyMovies, model); + + return model; + } + + private static void TransformEmbyMovies(IQueryable embyMovies, HashSet model) + { + foreach (var emby in embyMovies) + { + model.Add(new RecentlyAddedMovieModel + { + Id = emby.Id, + ImdbId = emby.ProviderId, + AddedAt = emby.AddedAt, + Title = emby.Title, + }); + } + } + + private static void TransformPlexMovies(IQueryable plexMovies, HashSet model) + { + foreach (var plex in plexMovies) + { + model.Add(new RecentlyAddedMovieModel + { + Id = plex.Id, + ImdbId = plex.ImdbId, + TheMovieDbId = plex.TheMovieDbId, + AddedAt = plex.AddedAt, + Title = plex.Title, + Quality = plex.Quality, + ReleaseYear = plex.ReleaseYear + }); + } + } + + private static void TransformPlexShows(IQueryable plexShows, HashSet model) + { + foreach (var plex in plexShows) + { + foreach (var season in plex.Seasons) + { + foreach (var episode in plex.Episodes) + { + model.Add(new RecentlyAddedTvModel + { + Id = plex.Id, + ImdbId = plex.ImdbId, + TheMovieDbId = plex.TheMovieDbId, + AddedAt = plex.AddedAt, + Title = plex.Title, + Quality = plex.Quality, + ReleaseYear = plex.ReleaseYear, + TvDbId = plex.TvDbId, + EpisodeNumber = episode.EpisodeNumber, + SeasonNumber = season.SeasonNumber + }); + } + } + } + } + + private static void TransformEmbyShows(IQueryable embyShows, HashSet model) + { + foreach (var emby in embyShows) + { + foreach (var episode in emby.Episodes) + { + model.Add(new RecentlyAddedTvModel + { + Id = emby.Id, + ImdbId = emby.ProviderId, + AddedAt = emby.AddedAt, + Title = emby.Title, + EpisodeNumber = episode.EpisodeNumber, + SeasonNumber = episode.SeasonNumber + }); + } + } + } + } +} diff --git a/src/Ombi.Core/Models/RecentlyAddedMovieModel.cs b/src/Ombi.Core/Models/RecentlyAddedMovieModel.cs new file mode 100644 index 000000000..c63ea98d3 --- /dev/null +++ b/src/Ombi.Core/Models/RecentlyAddedMovieModel.cs @@ -0,0 +1,23 @@ +using System; + +namespace Ombi.Core.Models +{ + public class RecentlyAddedMovieModel + { + public int Id { get; set; } + public string Title { get; set; } + public string Overview { get; set; } + public string ImdbId { get; set; } + public string TvDbId { get; set; } + public string TheMovieDbId { get; set; } + public string ReleaseYear { get; set; } + public DateTime AddedAt { get; set; } + public string Quality { get; set; } + } + + public enum RecentlyAddedType + { + Plex, + Emby + } +} \ No newline at end of file diff --git a/src/Ombi.Core/Models/RecentlyAddedTvModel.cs b/src/Ombi.Core/Models/RecentlyAddedTvModel.cs new file mode 100644 index 000000000..dd485604d --- /dev/null +++ b/src/Ombi.Core/Models/RecentlyAddedTvModel.cs @@ -0,0 +1,19 @@ +using System; + +namespace Ombi.Core.Models +{ + public class RecentlyAddedTvModel + { + public int Id { get; set; } + public string Title { get; set; } // Series Title + public string Overview { get; set; } + public string ImdbId { get; set; } + public string TvDbId { get; set; } + public string TheMovieDbId { get; set; } + public string ReleaseYear { get; set; } + public DateTime AddedAt { get; set; } + public string Quality { get; set; } + public int SeasonNumber { get; set; } + public int EpisodeNumber { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Core/Models/UI/NewsletterNotificationViewModel.cs b/src/Ombi.Core/Models/UI/NewsletterNotificationViewModel.cs new file mode 100644 index 000000000..a40044670 --- /dev/null +++ b/src/Ombi.Core/Models/UI/NewsletterNotificationViewModel.cs @@ -0,0 +1,23 @@ + +using System.Collections.Generic; +using Ombi.Settings.Settings.Models.Notifications; +using Ombi.Store.Entities; + +namespace Ombi.Core.Models.UI +{ + /// + /// The view model for the notification settings page + /// + /// + public class NewsletterNotificationViewModel : NewsletterSettings + { + /// + /// Gets or sets the notification templates. + /// + /// + /// The notification templates. + /// + public NotificationTemplates NotificationTemplate { get; set; } + + } +} diff --git a/src/Ombi.DependencyInjection/IocExtensions.cs b/src/Ombi.DependencyInjection/IocExtensions.cs index 817dfd551..92ecf8282 100644 --- a/src/Ombi.DependencyInjection/IocExtensions.cs +++ b/src/Ombi.DependencyInjection/IocExtensions.cs @@ -79,6 +79,7 @@ namespace Ombi.DependencyInjection services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); } @@ -172,6 +173,8 @@ namespace Ombi.DependencyInjection services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); + services.AddTransient(); } } } diff --git a/src/Ombi.Helpers/NotificationType.cs b/src/Ombi.Helpers/NotificationType.cs index d6466096c..8ea542063 100644 --- a/src/Ombi.Helpers/NotificationType.cs +++ b/src/Ombi.Helpers/NotificationType.cs @@ -13,5 +13,6 @@ WelcomeEmail = 8, IssueResolved = 9, IssueComment = 10, + Newsletter = 11, } } diff --git a/src/Ombi.Helpers/OmbiRoles.cs b/src/Ombi.Helpers/OmbiRoles.cs index 1b88b5d67..e7527279d 100644 --- a/src/Ombi.Helpers/OmbiRoles.cs +++ b/src/Ombi.Helpers/OmbiRoles.cs @@ -9,5 +9,6 @@ public const string RequestTv = nameof(RequestTv); public const string RequestMovie = nameof(RequestMovie); public const string Disabled = nameof(Disabled); + public const string RecievesNewsletter = nameof(RecievesNewsletter); } } \ No newline at end of file diff --git a/src/Ombi.Mapping/Profiles/SettingsProfile.cs b/src/Ombi.Mapping/Profiles/SettingsProfile.cs index 62232ee19..139290f2b 100644 --- a/src/Ombi.Mapping/Profiles/SettingsProfile.cs +++ b/src/Ombi.Mapping/Profiles/SettingsProfile.cs @@ -18,6 +18,7 @@ namespace Ombi.Mapping.Profiles CreateMap().ReverseMap(); CreateMap().ReverseMap(); CreateMap().ReverseMap(); + CreateMap().ReverseMap(); } } } \ No newline at end of file diff --git a/src/Ombi.Notifications.Templates/EmailBasicTemplate.cs b/src/Ombi.Notifications.Templates/EmailBasicTemplate.cs index 4814945e7..b29122be0 100644 --- a/src/Ombi.Notifications.Templates/EmailBasicTemplate.cs +++ b/src/Ombi.Notifications.Templates/EmailBasicTemplate.cs @@ -4,19 +4,26 @@ using System.Text; namespace Ombi.Notifications.Templates { - public class EmailBasicTemplate : IEmailBasicTemplate + public class EmailBasicTemplate : TemplateBase, IEmailBasicTemplate { - public string TemplateLocation + public override string TemplateLocation { get { + if (string.IsNullOrEmpty(_templateLocation)) + { #if DEBUG - return Path.Combine(Directory.GetCurrentDirectory(), "bin", "Debug", "netcoreapp2.0", "Templates", "BasicTemplate.html"); + _templateLocation = Path.Combine(Directory.GetCurrentDirectory(), "bin", "Debug", "netcoreapp2.0", "Templates", + "BasicTemplate.html"); #else - return Path.Combine(Directory.GetCurrentDirectory(), "Templates","BasicTemplate.html"); + _templateLocation = Path.Combine(Directory.GetCurrentDirectory(), "Templates","BasicTemplate.html"); #endif + } + return _templateLocation; } } + + private string _templateLocation; private const string SubjectKey = "{@SUBJECT}"; private const string BodyKey = "{@BODY}"; @@ -31,7 +38,7 @@ namespace Ombi.Notifications.Templates sb.Replace(BodyKey, body); sb.Replace(DateKey, DateTime.Now.ToString("f")); sb.Replace(Poster, string.IsNullOrEmpty(imgsrc) ? string.Empty : $"\"Poster\""); - sb.Replace(Logo, string.IsNullOrEmpty(logo) ? "http://i.imgur.com/qQsN78U.png" : logo); + sb.Replace(Logo, string.IsNullOrEmpty(logo) ? OmbiLogo : logo); return sb.ToString(); } diff --git a/src/Ombi.Notifications.Templates/INewsletterTemplate.cs b/src/Ombi.Notifications.Templates/INewsletterTemplate.cs new file mode 100644 index 000000000..e3302710d --- /dev/null +++ b/src/Ombi.Notifications.Templates/INewsletterTemplate.cs @@ -0,0 +1,7 @@ +namespace Ombi.Notifications.Templates +{ + public interface INewsletterTemplate + { + string LoadTemplate(string subject, string intro, string tableHtml, string logo); + } +} \ No newline at end of file diff --git a/src/Ombi.Notifications.Templates/NewsletterTemplate.cs b/src/Ombi.Notifications.Templates/NewsletterTemplate.cs new file mode 100644 index 000000000..389ff5cd6 --- /dev/null +++ b/src/Ombi.Notifications.Templates/NewsletterTemplate.cs @@ -0,0 +1,46 @@ +using System; +using System.IO; +using System.Text; + +namespace Ombi.Notifications.Templates +{ + public class NewsletterTemplate : TemplateBase, INewsletterTemplate + { + public override string TemplateLocation + { + get + { + if (string.IsNullOrEmpty(_templateLocation)) + { +#if DEBUG + _templateLocation = Path.Combine(Directory.GetCurrentDirectory(), "bin", "Debug", "netcoreapp2.0", "Templates", "NewsletterTemplate.html"); +#else + _templateLocation = Path.Combine(Directory.GetCurrentDirectory(), "Templates", "NewsletterTemplate.html"); +#endif + } + return _templateLocation; + } + } + + private string _templateLocation; + + private const string SubjectKey = "{@SUBJECT}"; + private const string DateKey = "{@DATENOW}"; + private const string Logo = "{@LOGO}"; + private const string TableLocation = "{@RECENTLYADDED}"; + private const string IntroText = "{@INTRO}"; + + + public string LoadTemplate(string subject, string intro, string tableHtml, string logo) + { + var sb = new StringBuilder(File.ReadAllText(TemplateLocation)); + sb.Replace(SubjectKey, subject); + sb.Replace(TableLocation, tableHtml); + sb.Replace(IntroText, intro); + sb.Replace(DateKey, DateTime.Now.ToString("f")); + sb.Replace(Logo, string.IsNullOrEmpty(logo) ? OmbiLogo : logo); + + return sb.ToString(); + } + } +} diff --git a/src/Ombi.Notifications.Templates/Ombi.Notifications.Templates.csproj b/src/Ombi.Notifications.Templates/Ombi.Notifications.Templates.csproj index 085c44dc1..cf310acc9 100644 --- a/src/Ombi.Notifications.Templates/Ombi.Notifications.Templates.csproj +++ b/src/Ombi.Notifications.Templates/Ombi.Notifications.Templates.csproj @@ -9,6 +9,9 @@ + + Always + Always diff --git a/src/Ombi.Notifications.Templates/TemplateBase.cs b/src/Ombi.Notifications.Templates/TemplateBase.cs new file mode 100644 index 000000000..9b9207c9a --- /dev/null +++ b/src/Ombi.Notifications.Templates/TemplateBase.cs @@ -0,0 +1,8 @@ +namespace Ombi.Notifications.Templates +{ + public abstract class TemplateBase + { + public abstract string TemplateLocation { get; } + public virtual string OmbiLogo => "http://i.imgur.com/qQsN78U.png"; + } +} \ No newline at end of file diff --git a/src/Ombi.Notifications.Templates/Templates/NewsletterTemplate.html b/src/Ombi.Notifications.Templates/Templates/NewsletterTemplate.html new file mode 100644 index 000000000..464e3463b --- /dev/null +++ b/src/Ombi.Notifications.Templates/Templates/NewsletterTemplate.html @@ -0,0 +1,187 @@ + + + + + + Ombi + + + + + + + + + +
  +
+ + + + + + + + + + + +
+ + + + + + + +
+ +
+
+
+

{@INTRO}

+ +
+ + {@RECENTLYADDED} + +
+ + + + + + +
+
 
+ + \ No newline at end of file diff --git a/src/Ombi.Notifications/Agents/MattermostNotification.cs b/src/Ombi.Notifications/Agents/MattermostNotification.cs index efc1df1a6..f07d62b72 100644 --- a/src/Ombi.Notifications/Agents/MattermostNotification.cs +++ b/src/Ombi.Notifications/Agents/MattermostNotification.cs @@ -59,10 +59,18 @@ namespace Ombi.Notifications.Agents Message = parsed.Message, }; - notification.Other.Add("image", parsed.Image); + AddOtherInformation(model, notification, parsed); + //notification.Other.Add("overview", model.RequestType == RequestType.Movie ? base.MovieRequest.Overview : TvRequest.); await Send(notification, settings); } + private void AddOtherInformation(NotificationOptions model, NotificationMessage notification, + NotificationMessageContent parsed) + { + notification.Other.Add("image", parsed.Image); + notification.Other.Add("title", model.RequestType == RequestType.Movie ? MovieRequest.Title : TvRequest.Title); + } + protected override async Task NewIssue(NotificationOptions model, MattermostNotificationSettings settings) { var parsed = await LoadTemplate(NotificationAgent.Mattermost, NotificationType.Issue, model); @@ -75,7 +83,7 @@ namespace Ombi.Notifications.Agents { Message = parsed.Message, }; - notification.Other.Add("image", parsed.Image); + AddOtherInformation(model, notification, parsed); await Send(notification, settings); } @@ -91,7 +99,7 @@ namespace Ombi.Notifications.Agents { Message = parsed.Message, }; - notification.Other.Add("image", parsed.Image); + AddOtherInformation(model, notification, parsed); await Send(notification, settings); } @@ -107,7 +115,7 @@ namespace Ombi.Notifications.Agents { Message = parsed.Message, }; - notification.Other.Add("image", parsed.Image); + AddOtherInformation(model, notification, parsed); await Send(notification, settings); } @@ -149,7 +157,7 @@ namespace Ombi.Notifications.Agents { Message = parsed.Message, }; - notification.Other.Add("image", parsed.Image); + AddOtherInformation(model, notification, parsed); await Send(notification, settings); } @@ -166,7 +174,7 @@ namespace Ombi.Notifications.Agents Message = parsed.Message, }; - notification.Other.Add("image", parsed.Image); + AddOtherInformation(model, notification, parsed); await Send(notification, settings); } @@ -182,7 +190,7 @@ namespace Ombi.Notifications.Agents { Message = parsed.Message, }; - notification.Other.Add("image", parsed.Image); + AddOtherInformation(model, notification, parsed); await Send(notification, settings); } @@ -190,12 +198,20 @@ namespace Ombi.Notifications.Agents { try { - var body = new MattermostBody + var body = new MattermostMessage { - username = string.IsNullOrEmpty(settings.Username) ? "Ombi" : settings.Username, - channel = settings.Channel, - text = model.Message, - icon_url = settings.IconUrl + Username = string.IsNullOrEmpty(settings.Username) ? "Ombi" : settings.Username, + Channel = settings.Channel, + Text = model.Message, + IconUrl = new Uri(settings.IconUrl), + Attachments = new List + { + new MattermostAttachment + { + Title = model.Other.ContainsKey("title") ? model.Other["title"] : string.Empty, + ImageUrl = model.Other.ContainsKey("image") ? new Uri(model.Other["image"]) : null, + } + } }; await Api.PushAsync(settings.WebhookUrl, body); } diff --git a/src/Ombi.Notifications/GenericEmailProvider.cs b/src/Ombi.Notifications/GenericEmailProvider.cs index e28b48c27..462f8918e 100644 --- a/src/Ombi.Notifications/GenericEmailProvider.cs +++ b/src/Ombi.Notifications/GenericEmailProvider.cs @@ -107,9 +107,13 @@ namespace Ombi.Notifications var body = new BodyBuilder { HtmlBody = model.Message, - TextBody = model.Other["PlainTextBody"] }; + if (model.Other.ContainsKey("PlainTextBody")) + { + body.TextBody = model.Other["PlainTextBody"]; + } + var message = new MimeMessage { Body = body.ToMessageBody(), diff --git a/src/Ombi.Notifications/NotificationMessageCurlys.cs b/src/Ombi.Notifications/NotificationMessageCurlys.cs index d958cfc74..8c18fba4c 100644 --- a/src/Ombi.Notifications/NotificationMessageCurlys.cs +++ b/src/Ombi.Notifications/NotificationMessageCurlys.cs @@ -1,10 +1,13 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Text; using Ombi.Helpers; using Ombi.Notifications.Models; using Ombi.Settings.Settings.Models; using Ombi.Store.Entities; using Ombi.Store.Entities.Requests; +using Ombi.Store.Repository.Requests; namespace Ombi.Notifications { @@ -25,9 +28,9 @@ namespace Ombi.Notifications } 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; + RequestedUser = req?.RequestedUser?.UserName; + UserName = req?.RequestedUser?.UserName; + Alias = (req?.RequestedUser?.Alias.HasValue() ?? false) ? req?.RequestedUser?.Alias : req?.RequestedUser?.UserName; Title = title; RequestedDate = req?.RequestedDate.ToString("D"); Type = req?.RequestType.ToString(); @@ -38,6 +41,15 @@ namespace Ombi.Notifications AdditionalInformation = opts?.AdditionalInformation ?? string.Empty; } + public void SetupNewsletter(CustomizationSettings s, OmbiUser username) + { + ApplicationUrl = (s?.ApplicationUrl.HasValue() ?? false) ? s.ApplicationUrl : string.Empty; + ApplicationName = string.IsNullOrEmpty(s?.ApplicationName) ? "Ombi" : s?.ApplicationName; + RequestedUser = username.UserName; + UserName = username.UserName; + Alias = username.Alias.HasValue() ? username.Alias : username.UserName; + } + public void Setup(NotificationOptions opts, ChildRequests req, CustomizationSettings s) { LoadIssues(opts); @@ -52,9 +64,9 @@ namespace Ombi.Notifications } 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; + RequestedUser = req?.RequestedUser?.UserName; + UserName = req?.RequestedUser?.UserName; + Alias = (req?.RequestedUser?.Alias.HasValue() ?? false) ? req?.RequestedUser?.Alias : req?.RequestedUser?.UserName; Title = title; RequestedDate = req?.RequestedDate.ToString("D"); Type = req?.RequestType.ToString(); @@ -64,6 +76,40 @@ namespace Ombi.Notifications $"https://image.tmdb.org/t/p/w300{req?.ParentRequest.PosterPath}" : req?.ParentRequest.PosterPath; AdditionalInformation = opts.AdditionalInformation; // DO Episode and Season Lists + + var episodes = req?.SeasonRequests?.SelectMany(x => x.Episodes) ?? new List(); + var seasons = req?.SeasonRequests?.OrderBy(x => x.SeasonNumber).ToList() ?? new List(); + var orderedEpisodes = episodes.OrderBy(x => x.EpisodeNumber).ToList(); + var epSb = new StringBuilder(); + var seasonSb = new StringBuilder(); + for (var i = 0; i < orderedEpisodes.Count; i++) + { + var ep = orderedEpisodes[i]; + if (i < orderedEpisodes.Count - 1) + { + epSb.Append($"{ep.EpisodeNumber},"); + } + else + { + epSb.Append($"{ep.EpisodeNumber}"); + } + } + + for (var i = 0; i < seasons.Count; i++) + { + var ep = seasons[i]; + if (i < seasons.Count - 1) + { + seasonSb.Append($"{ep.SeasonNumber},"); + } + else + { + seasonSb.Append($"{ep.SeasonNumber}"); + } + } + + EpisodesList = epSb.ToString(); + SeasonsList = seasonSb.ToString(); } public void Setup(OmbiUser user, CustomizationSettings s) @@ -81,13 +127,14 @@ namespace Ombi.Notifications 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; - RequestedUser = opts.Substitutes.TryGetValue("IssueUser", out val) ? val : string.Empty; + UserName = opts.Substitutes.TryGetValue("IssueUser", out val) ? val : string.Empty; } // User Defined public string RequestedUser { get; set; } - public string UserName => RequestedUser; - public string IssueUser => RequestedUser; + public string UserName { get; set; } + public string IssueUser => UserName; + public string Alias { get; set; } public string Title { get; set; } public string RequestedDate { get; set; } @@ -137,6 +184,7 @@ namespace Ombi.Notifications {nameof(NewIssueComment),NewIssueComment}, {nameof(IssueUser),IssueUser}, {nameof(UserName),UserName}, + {nameof(Alias),Alias}, }; } } \ No newline at end of file diff --git a/src/Ombi.Schedule/JobSetup.cs b/src/Ombi.Schedule/JobSetup.cs index dc9d49269..85b842d27 100644 --- a/src/Ombi.Schedule/JobSetup.cs +++ b/src/Ombi.Schedule/JobSetup.cs @@ -17,46 +17,53 @@ namespace Ombi.Schedule public JobSetup(IPlexContentSync plexContentSync, IRadarrSync radarrSync, IOmbiAutomaticUpdater updater, IEmbyContentSync embySync, IPlexUserImporter userImporter, IEmbyUserImporter embyUserImporter, ISonarrSync cache, ICouchPotatoSync cpCache, - ISettingsService jobsettings, ISickRageSync srSync) + ISettingsService jobsettings, ISickRageSync srSync, IRefreshMetadata refresh, + INewsletterJob newsletter) { - PlexContentSync = plexContentSync; - RadarrSync = radarrSync; - Updater = updater; - EmbyContentSync = embySync; - PlexUserImporter = userImporter; - EmbyUserImporter = embyUserImporter; - SonarrSync = cache; - CpCache = cpCache; - JobSettings = jobsettings; - SrSync = srSync; + _plexContentSync = plexContentSync; + _radarrSync = radarrSync; + _updater = updater; + _embyContentSync = embySync; + _plexUserImporter = userImporter; + _embyUserImporter = embyUserImporter; + _sonarrSync = cache; + _cpCache = cpCache; + _jobSettings = jobsettings; + _srSync = srSync; + _refreshMetadata = refresh; + _newsletter = newsletter; } - private IPlexContentSync PlexContentSync { get; } - private IRadarrSync RadarrSync { get; } - private IOmbiAutomaticUpdater Updater { get; } - private IPlexUserImporter PlexUserImporter { get; } - private IEmbyContentSync EmbyContentSync { get; } - private IEmbyUserImporter EmbyUserImporter { get; } - private ISonarrSync SonarrSync { get; } - private ICouchPotatoSync CpCache { get; } - private ISickRageSync SrSync { get; } - private ISettingsService JobSettings { get; set; } + private readonly IPlexContentSync _plexContentSync; + private readonly IRadarrSync _radarrSync; + private readonly IOmbiAutomaticUpdater _updater; + private readonly IPlexUserImporter _plexUserImporter; + private readonly IEmbyContentSync _embyContentSync; + private readonly IEmbyUserImporter _embyUserImporter; + private readonly ISonarrSync _sonarrSync; + private readonly ICouchPotatoSync _cpCache; + private readonly ISickRageSync _srSync; + private readonly ISettingsService _jobSettings; + private readonly IRefreshMetadata _refreshMetadata; + private readonly INewsletterJob _newsletter; public void Setup() { - var s = JobSettings.GetSettings(); + var s = _jobSettings.GetSettings(); - RecurringJob.AddOrUpdate(() => EmbyContentSync.Start(), JobSettingsHelper.EmbyContent(s)); - RecurringJob.AddOrUpdate(() => SonarrSync.Start(), JobSettingsHelper.Sonarr(s)); - RecurringJob.AddOrUpdate(() => RadarrSync.CacheContent(), JobSettingsHelper.Radarr(s)); - RecurringJob.AddOrUpdate(() => PlexContentSync.CacheContent(), JobSettingsHelper.PlexContent(s)); - RecurringJob.AddOrUpdate(() => CpCache.Start(), JobSettingsHelper.CouchPotato(s)); - RecurringJob.AddOrUpdate(() => SrSync.Start(), JobSettingsHelper.SickRageSync(s)); + RecurringJob.AddOrUpdate(() => _embyContentSync.Start(), JobSettingsHelper.EmbyContent(s)); + RecurringJob.AddOrUpdate(() => _sonarrSync.Start(), JobSettingsHelper.Sonarr(s)); + RecurringJob.AddOrUpdate(() => _radarrSync.CacheContent(), JobSettingsHelper.Radarr(s)); + RecurringJob.AddOrUpdate(() => _plexContentSync.CacheContent(), JobSettingsHelper.PlexContent(s)); + RecurringJob.AddOrUpdate(() => _cpCache.Start(), JobSettingsHelper.CouchPotato(s)); + RecurringJob.AddOrUpdate(() => _srSync.Start(), JobSettingsHelper.SickRageSync(s)); + RecurringJob.AddOrUpdate(() => _refreshMetadata.Start(), JobSettingsHelper.RefreshMetadata(s)); - RecurringJob.AddOrUpdate(() => Updater.Update(null), JobSettingsHelper.Updater(s)); + RecurringJob.AddOrUpdate(() => _updater.Update(null), JobSettingsHelper.Updater(s)); - RecurringJob.AddOrUpdate(() => EmbyUserImporter.Start(), JobSettingsHelper.UserImporter(s)); - RecurringJob.AddOrUpdate(() => PlexUserImporter.Start(), JobSettingsHelper.UserImporter(s)); + RecurringJob.AddOrUpdate(() => _embyUserImporter.Start(), JobSettingsHelper.UserImporter(s)); + RecurringJob.AddOrUpdate(() => _plexUserImporter.Start(), JobSettingsHelper.UserImporter(s)); + RecurringJob.AddOrUpdate(() => _newsletter.Start(), JobSettingsHelper.Newsletter(s)); } } } diff --git a/src/Ombi.Schedule/Jobs/Ombi/HtmlTemplateGenerator.cs b/src/Ombi.Schedule/Jobs/Ombi/HtmlTemplateGenerator.cs new file mode 100644 index 000000000..61f4bd7c8 --- /dev/null +++ b/src/Ombi.Schedule/Jobs/Ombi/HtmlTemplateGenerator.cs @@ -0,0 +1,46 @@ +using System.Text; + +namespace Ombi.Schedule.Jobs.Ombi +{ + public abstract class HtmlTemplateGenerator + { + protected virtual void AddParagraph(StringBuilder stringBuilder, string text, int fontSize = 14, string fontWeight = "normal") + { + stringBuilder.AppendFormat("

{0}

", text, fontSize, fontWeight); + } + + protected virtual void AddImageInsideTable(StringBuilder sb, string url, int size = 400) + { + sb.Append(""); + sb.Append(""); + sb.Append($""); + sb.Append(""); + sb.Append(""); + } + + protected virtual void Href(StringBuilder sb, string url) + { + sb.AppendFormat("", url); + } + + protected virtual void TableData(StringBuilder sb) + { + sb.Append( + ""); + } + + protected virtual void EndTag(StringBuilder sb, string tag) + { + sb.AppendFormat("", tag); + } + + protected virtual void Header(StringBuilder sb, int size, string text, string fontWeight = "normal") + { + sb.AppendFormat( + "{1}", + size, text, fontWeight); + } + + + } +} \ No newline at end of file diff --git a/src/Ombi.Schedule/Jobs/Ombi/INewsletterJob.cs b/src/Ombi.Schedule/Jobs/Ombi/INewsletterJob.cs new file mode 100644 index 000000000..887508d34 --- /dev/null +++ b/src/Ombi.Schedule/Jobs/Ombi/INewsletterJob.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using Ombi.Settings.Settings.Models.Notifications; + +namespace Ombi.Schedule.Jobs.Ombi +{ + public interface INewsletterJob : IBaseJob + { + Task Start(); + Task Start(NewsletterSettings settings, bool test); + } +} \ No newline at end of file diff --git a/src/Ombi.Schedule/Jobs/Ombi/IRefreshMetadata.cs b/src/Ombi.Schedule/Jobs/Ombi/IRefreshMetadata.cs new file mode 100644 index 000000000..a08db74d0 --- /dev/null +++ b/src/Ombi.Schedule/Jobs/Ombi/IRefreshMetadata.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace Ombi.Schedule.Jobs.Ombi +{ + public interface IRefreshMetadata : IBaseJob + { + Task Start(); + } +} \ No newline at end of file diff --git a/src/Ombi.Schedule/Jobs/Ombi/NewsletterJob.cs b/src/Ombi.Schedule/Jobs/Ombi/NewsletterJob.cs new file mode 100644 index 000000000..c780509b8 --- /dev/null +++ b/src/Ombi.Schedule/Jobs/Ombi/NewsletterJob.cs @@ -0,0 +1,636 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Ombi.Api.TheMovieDb; +using Ombi.Api.TheMovieDb.Models; +using Ombi.Api.TvMaze; +using Ombi.Core.Settings; +using Ombi.Helpers; +using Ombi.Notifications; +using Ombi.Notifications.Models; +using Ombi.Notifications.Templates; +using Ombi.Settings.Settings.Models; +using Ombi.Settings.Settings.Models.Notifications; +using Ombi.Store.Entities; +using Ombi.Store.Repository; + +namespace Ombi.Schedule.Jobs.Ombi +{ + public class NewsletterJob : HtmlTemplateGenerator, INewsletterJob + { + public NewsletterJob(IPlexContentRepository plex, IEmbyContentRepository emby, IRepository addedLog, + IMovieDbApi movieApi, ITvMazeApi tvApi, IEmailProvider email, ISettingsService custom, + ISettingsService emailSettings, INotificationTemplatesRepository templateRepo, + UserManager um, ISettingsService newsletter) + { + _plex = plex; + _emby = emby; + _recentlyAddedLog = addedLog; + _movieApi = movieApi; + _tvApi = tvApi; + _email = email; + _customizationSettings = custom; + _templateRepo = templateRepo; + _emailSettings = emailSettings; + _newsletterSettings = newsletter; + _userManager = um; + _emailSettings.ClearCache(); + _customizationSettings.ClearCache(); + _newsletterSettings.ClearCache(); + } + + private readonly IPlexContentRepository _plex; + private readonly IEmbyContentRepository _emby; + private readonly IRepository _recentlyAddedLog; + private readonly IMovieDbApi _movieApi; + private readonly ITvMazeApi _tvApi; + private readonly IEmailProvider _email; + private readonly ISettingsService _customizationSettings; + private readonly INotificationTemplatesRepository _templateRepo; + private readonly ISettingsService _emailSettings; + private readonly ISettingsService _newsletterSettings; + private readonly UserManager _userManager; + + public async Task Start(NewsletterSettings settings, bool test) + { + if (!settings.Enabled) + { + return; + } + var template = await _templateRepo.GetTemplate(NotificationAgent.Email, NotificationType.Newsletter); + if (!template.Enabled) + { + return; + } + + var emailSettings = await _emailSettings.GetSettingsAsync(); + if (!ValidateConfiguration(emailSettings)) + { + return; + } + + var customization = await _customizationSettings.GetSettingsAsync(); + + // Get the Content + var plexContent = _plex.GetAll().Include(x => x.Episodes).AsNoTracking(); + var embyContent = _emby.GetAll().Include(x => x.Episodes).AsNoTracking(); + + var addedLog = _recentlyAddedLog.GetAll(); + var addedPlexMovieLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Plex && x.ContentType == ContentType.Parent).Select(x => x.ContentId); + var addedEmbyMoviesLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Emby && x.ContentType == ContentType.Parent).Select(x => x.ContentId); + + var addedPlexEpisodesLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Plex && x.ContentType == ContentType.Episode).Select(x => x.ContentId); + var addedEmbyEpisodesLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Emby && x.ContentType == ContentType.Episode).Select(x => x.ContentId); + + // Filter out the ones that we haven't sent yet + var plexContentMoviesToSend = plexContent.Where(x => x.Type == PlexMediaTypeEntity.Movie && !addedPlexMovieLogIds.Contains(x.Id)); + var embyContentMoviesToSend = embyContent.Where(x => x.Type == EmbyMediaType.Movie && !addedEmbyMoviesLogIds.Contains(x.Id)); + + var plexEpisodesToSend = _plex.GetAllEpisodes().Include(x => x.Series).Where(x => !addedPlexEpisodesLogIds.Contains(x.Id)).AsNoTracking(); + var embyEpisodesToSend = _emby.GetAllEpisodes().Include(x => x.Series).Where(x => !addedEmbyEpisodesLogIds.Contains(x.Id)).AsNoTracking(); + + var body = string.Empty; + if (test) + { + var plexm = plexContent.Where(x => x.Type == PlexMediaTypeEntity.Movie).OrderByDescending(x => x.AddedAt).Take(10); + var embym = embyContent.Where(x => x.Type == EmbyMediaType.Movie).OrderByDescending(x => x.AddedAt).Take(10); + var plext = _plex.GetAllEpisodes().Include(x => x.Series).OrderByDescending(x => x.Series.AddedAt).Take(10); + var embyt = _emby.GetAllEpisodes().Include(x => x.Series).OrderByDescending(x => x.AddedAt).Take(10); + body = await BuildHtml(plexm, embym, plext, embyt); + } + else + { + body = await BuildHtml(plexContentMoviesToSend, embyContentMoviesToSend, plexEpisodesToSend, embyEpisodesToSend); + if (body.IsNullOrEmpty()) + { + return; + } + + } + + if (!test) + { + // Get the users to send it to + var users = await _userManager.GetUsersInRoleAsync(OmbiRoles.RecievesNewsletter); + if (!users.Any()) + { + return; + } + var emailTasks = new List(); + foreach (var user in users) + { + if (user.Email.IsNullOrEmpty()) + { + continue; + } + + var messageContent = ParseTemplate(template, customization, user); + var email = new NewsletterTemplate(); + + var html = email.LoadTemplate(messageContent.Subject, messageContent.Message, body, customization.Logo); + + emailTasks.Add(_email.Send( + new NotificationMessage { Message = html, Subject = messageContent.Subject, To = user.Email }, + emailSettings)); + } + + // Now add all of this to the Recently Added log + var recentlyAddedLog = new HashSet(); + foreach (var p in plexContentMoviesToSend) + { + recentlyAddedLog.Add(new RecentlyAddedLog + { + AddedAt = DateTime.Now, + Type = RecentlyAddedType.Plex, + ContentType = ContentType.Parent, + ContentId = p.Id + }); + + } + + foreach (var p in plexEpisodesToSend) + { + recentlyAddedLog.Add(new RecentlyAddedLog + { + AddedAt = DateTime.Now, + Type = RecentlyAddedType.Plex, + ContentType = ContentType.Episode, + ContentId = p.Id + }); + } + + foreach (var e in embyContentMoviesToSend) + { + if (e.Type == EmbyMediaType.Movie) + { + recentlyAddedLog.Add(new RecentlyAddedLog + { + AddedAt = DateTime.Now, + Type = RecentlyAddedType.Emby, + ContentType = ContentType.Parent, + ContentId = e.Id + }); + } + } + + foreach (var p in embyEpisodesToSend) + { + recentlyAddedLog.Add(new RecentlyAddedLog + { + AddedAt = DateTime.Now, + Type = RecentlyAddedType.Emby, + ContentType = ContentType.Episode, + ContentId = p.Id + }); + } + await _recentlyAddedLog.AddRange(recentlyAddedLog); + + await Task.WhenAll(emailTasks.ToArray()); + } + else + { + var admins = await _userManager.GetUsersInRoleAsync(OmbiRoles.Admin); + foreach (var a in admins) + { + if (a.Email.IsNullOrEmpty()) + { + continue; + } + var messageContent = ParseTemplate(template, customization, a); + + var email = new NewsletterTemplate(); + + var html = email.LoadTemplate(messageContent.Subject, messageContent.Message, body, customization.Logo); + + await _email.Send( + new NotificationMessage { Message = html, Subject = messageContent.Subject, To = a.Email }, + emailSettings); + } + } + } + + public async Task Start() + { + var newsletterSettings = await _newsletterSettings.GetSettingsAsync(); + await Start(newsletterSettings, false); + } + + private NotificationMessageContent ParseTemplate(NotificationTemplates template, CustomizationSettings settings, OmbiUser username) + { + var resolver = new NotificationMessageResolver(); + var curlys = new NotificationMessageCurlys(); + + curlys.SetupNewsletter(settings, username); + + return resolver.ParseMessage(template, curlys); + } + + private async Task BuildHtml(IQueryable plexContentToSend, IQueryable embyContentToSend, IQueryable plexEpisodes, IQueryable embyEp) + { + var sb = new StringBuilder(); + + var plexMovies = plexContentToSend.Where(x => x.Type == PlexMediaTypeEntity.Movie); + var embyMovies = embyContentToSend.Where(x => x.Type == EmbyMediaType.Movie); + if (plexMovies.Any() || embyMovies.Any()) + { + sb.Append("

New Movies:



"); + await ProcessPlexMovies(plexMovies, sb); + await ProcessEmbyMovies(embyMovies, sb); + } + + if (plexEpisodes.Any() || embyEp.Any()) + { + sb.Append("

New Episodes:



"); + await ProcessPlexTv(plexEpisodes, sb); + await ProcessEmbyTv(embyEp, sb); + } + + return sb.ToString(); + } + + private async Task ProcessPlexMovies(IQueryable plexContentToSend, StringBuilder sb) + { + sb.Append( + ""); + var ordered = plexContentToSend.OrderByDescending(x => x.AddedAt); + foreach (var content in ordered) + { + if (content.TheMovieDbId.IsNullOrEmpty()) + { + // Maybe we should try the ImdbId? + if (content.ImdbId.HasValue()) + { + var findResult = await _movieApi.Find(content.ImdbId, ExternalSource.imdb_id); + + var movieId = findResult.movie_results?[0]?.id ?? 0; + content.TheMovieDbId = movieId.ToString(); + } + } + + int.TryParse(content.TheMovieDbId, out var movieDbId); + var info = await _movieApi.GetMovieInformationWithExtraInfo(movieDbId); + if (info == null) + { + continue; + } + try + { + CreateMovieHtmlContent(sb, info); + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + finally + { + EndLoopHtml(sb); + } + } + } + + private async Task ProcessEmbyMovies(IQueryable embyContent, StringBuilder sb) + { + sb.Append( + "
"); + var ordered = embyContent.OrderByDescending(x => x.AddedAt); + foreach (var content in ordered) + { + int.TryParse(content.ProviderId, out var movieDbId); + var info = await _movieApi.GetMovieInformationWithExtraInfo(movieDbId); + if (info == null) + { + continue; + } + try + { + CreateMovieHtmlContent(sb, info); + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + finally + { + EndLoopHtml(sb); + } + } + } + + private void CreateMovieHtmlContent(StringBuilder sb, MovieResponseDto info) + { + AddImageInsideTable(sb, $"https://image.tmdb.org/t/p/original{info.PosterPath}"); + + sb.Append(""); + TableData(sb); + + Href(sb, $"https://www.imdb.com/title/{info.ImdbId}/"); + var releaseDate = string.Empty; + try + { + releaseDate = $"({DateTime.Parse(info.ReleaseDate).Year})"; + } + catch (Exception) + { + // Swallow, couldn't parse the date + } + Header(sb, 3, $"{info.Title} {releaseDate}"); + EndTag(sb, "a"); + + if (info.Genres.Any()) + { + AddParagraph(sb, + $"Genre: {string.Join(", ", info.Genres.Select(x => x.Name.ToString()).ToArray())}"); + } + + AddParagraph(sb, info.Overview); + } + + private async Task ProcessPlexTv(IQueryable plexContent, StringBuilder sb) + { + var series = new List(); + foreach (var plexEpisode in plexContent) + { + var alreadyAdded = series.FirstOrDefault(x => x.Key == plexEpisode.Series.Key); + if (alreadyAdded != null) + { + var episodeExists = alreadyAdded.Episodes.Any(x => x.Key == plexEpisode.Key); + if (!episodeExists) + { + alreadyAdded.Episodes.Add(plexEpisode); + } + } + else + { + plexEpisode.Series.Episodes = new List { plexEpisode }; + series.Add(plexEpisode.Series); + } + } + + var orderedTv = series.OrderByDescending(x => x.AddedAt); + sb.Append( + "
"); + foreach (var t in orderedTv) + { + try + { + if (!t.HasTvDb) + { + // We may need to use themoviedb for the imdbid or their own id to get info + if (t.HasTheMovieDb) + { + int.TryParse(t.TheMovieDbId, out var movieId); + var externals = await _movieApi.GetTvExternals(movieId); + if (externals == null || externals.tvdb_id <= 0) + { + continue; + } + t.TvDbId = externals.tvdb_id.ToString(); + } + // WE could check the below but we need to get the moviedb and then perform the above, let the metadata job figure this out. + //else if(t.HasImdb) + //{ + // // Check the imdbid + // var externals = await _movieApi.Find(t.ImdbId, ExternalSource.imdb_id); + // if (externals?.tv_results == null || externals.tv_results.Length <= 0) + // { + // continue; + // } + // t.TvDbId = externals.tv_results.FirstOrDefault()..ToString(); + //} + + } + + int.TryParse(t.TvDbId, out var tvdbId); + var info = await _tvApi.ShowLookupByTheTvDbId(tvdbId); + if (info == null) + { + continue; + } + var banner = info.image?.original; + if (!string.IsNullOrEmpty(banner)) + { + banner = banner.Replace("http", "https"); // Always use the Https banners + } + AddImageInsideTable(sb, banner); + + sb.Append(""); + sb.Append( + "
"); + + var title = $"{t.Title} ({t.ReleaseYear})"; + + Href(sb, $"https://www.imdb.com/title/{info.externals.imdb}/"); + Header(sb, 3, title); + EndTag(sb, "a"); + + // Group by the season number + var results = t.Episodes.GroupBy(p => p.SeasonNumber, + (key, g) => new + { + SeasonNumber = key, + Episodes = g.ToList() + } + ); + + // Group the episodes + foreach (var epInformation in results.OrderBy(x => x.SeasonNumber)) + { + var orderedEpisodes = epInformation.Episodes.OrderBy(x => x.EpisodeNumber).ToList(); + var epSb = new StringBuilder(); + for (var i = 0; i < orderedEpisodes.Count; i++) + { + var ep = orderedEpisodes[i]; + if (i < orderedEpisodes.Count - 1) + { + epSb.Append($"{ep.EpisodeNumber},"); + } + else + { + epSb.Append($"{ep.EpisodeNumber}"); + } + + } + AddParagraph(sb, $"Season: {epInformation.SeasonNumber}, Episode: {epSb}"); + } + + if (info.genres.Any()) + { + AddParagraph(sb, $"Genre: {string.Join(", ", info.genres.Select(x => x.ToString()).ToArray())}"); + } + + AddParagraph(sb, info.summary); + } + catch (Exception e) + { + //Log.Error(e); + } + finally + { + EndLoopHtml(sb); + } + } + sb.Append("


"); + + } + + private async Task ProcessEmbyTv(IQueryable embyContent, StringBuilder sb) + { + var series = new List(); + foreach (var episode in embyContent) + { + var alreadyAdded = series.FirstOrDefault(x => x.EmbyId == episode.Series.EmbyId); + if (alreadyAdded != null) + { + alreadyAdded.Episodes.Add(episode); + } + else + { + episode.Series.Episodes = new List + { + episode + }; + series.Add(episode.Series); + } + } + var orderedTv = series.OrderByDescending(x => x.AddedAt); + sb.Append( + ""); + foreach (var t in orderedTv) + { + try + { + int.TryParse(t.ProviderId, out var tvdbId); + var info = await _tvApi.ShowLookupByTheTvDbId(tvdbId); + if (info == null) + { + continue; + } + var banner = info.image?.original; + if (!string.IsNullOrEmpty(banner)) + { + banner = banner.Replace("http", "https"); // Always use the Https banners + } + AddImageInsideTable(sb, banner); + + sb.Append(""); + sb.Append( + "
"); + + Href(sb, $"https://www.imdb.com/title/{info.externals.imdb}/"); + Header(sb, 3, t.Title); + EndTag(sb, "a"); + + // Group by the season number + var results = t.Episodes?.GroupBy(p => p.SeasonNumber, + (key, g) => new + { + SeasonNumber = key, + Episodes = g.ToList() + } + ); + + // Group the episodes + foreach (var epInformation in results.OrderBy(x => x.SeasonNumber)) + { + var orderedEpisodes = epInformation.Episodes.OrderBy(x => x.EpisodeNumber).ToList(); + var epSb = new StringBuilder(); + for (var i = 0; i < orderedEpisodes.Count; i++) + { + var ep = orderedEpisodes[i]; + if (i < orderedEpisodes.Count - 1) + { + epSb.Append($"{ep.EpisodeNumber},"); + } + else + { + epSb.Append($"{ep.EpisodeNumber}"); + } + + } + AddParagraph(sb, $"Season: {epInformation.SeasonNumber}, Episode: {epSb}"); + } + + if (info.genres.Any()) + { + AddParagraph(sb, $"Genre: {string.Join(", ", info.genres.Select(x => x.ToString()).ToArray())}"); + } + + AddParagraph(sb, info.summary); + } + catch (Exception e) + { + //Log.Error(e); + } + finally + { + EndLoopHtml(sb); + } + } + sb.Append("


"); + } + + private void EndLoopHtml(StringBuilder sb) + { + //NOTE: BR have to be in TD's as per html spec or it will be put outside of the table... + //Source: http://stackoverflow.com/questions/6588638/phantom-br-tag-rendered-by-browsers-prior-to-table-tag + sb.Append("
"); + sb.Append("
"); + sb.Append("
"); + sb.Append(""); + sb.Append(""); + } + + protected bool ValidateConfiguration(EmailNotificationSettings settings) + { + if (!settings.Enabled) + { + return false; + } + if (settings.Authentication) + { + if (string.IsNullOrEmpty(settings.Username) || string.IsNullOrEmpty(settings.Password)) + { + return false; + } + } + if (string.IsNullOrEmpty(settings.Host) || string.IsNullOrEmpty(settings.AdminEmail) || string.IsNullOrEmpty(settings.Port.ToString())) + { + return false; + } + + return true; + } + + private bool _disposed; + protected virtual void Dispose(bool disposing) + { + if (_disposed) + return; + + if (disposing) + { + _plex?.Dispose(); + _emby?.Dispose(); + _newsletterSettings?.Dispose(); + _customizationSettings?.Dispose(); + _emailSettings.Dispose(); + _recentlyAddedLog.Dispose(); + _templateRepo?.Dispose(); + _userManager?.Dispose(); + } + _disposed = true; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/src/Ombi.Schedule/Jobs/Ombi/RefreshMetadata.cs b/src/Ombi.Schedule/Jobs/Ombi/RefreshMetadata.cs new file mode 100644 index 000000000..225efb7d3 --- /dev/null +++ b/src/Ombi.Schedule/Jobs/Ombi/RefreshMetadata.cs @@ -0,0 +1,244 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Ombi.Api.TheMovieDb; +using Ombi.Api.TheMovieDb.Models; +using Ombi.Api.TvMaze; +using Ombi.Core.Settings; +using Ombi.Core.Settings.Models.External; +using Ombi.Helpers; +using Ombi.Store.Entities; +using Ombi.Store.Repository; +using Ombi.Store.Repository.Requests; + +namespace Ombi.Schedule.Jobs.Ombi +{ + public class RefreshMetadata : IRefreshMetadata + { + public RefreshMetadata(IPlexContentRepository plexRepo, IEmbyContentRepository embyRepo, + ILogger log, ITvMazeApi tvApi, ISettingsService plexSettings, + IMovieDbApi movieApi) + { + _plexRepo = plexRepo; + _embyRepo = embyRepo; + _log = log; + _movieApi = movieApi; + _tvApi = tvApi; + _plexSettings = plexSettings; + } + + private readonly IPlexContentRepository _plexRepo; + private readonly IEmbyContentRepository _embyRepo; + private readonly ILogger _log; + private readonly IMovieDbApi _movieApi; + private readonly ITvMazeApi _tvApi; + private readonly ISettingsService _plexSettings; + + public async Task Start() + { + _log.LogInformation("Starting the Metadata refresh"); + try + { + var settings = await _plexSettings.GetSettingsAsync(); + if (settings.Enable) + { + await StartPlex(); + } + } + catch (Exception e) + { + _log.LogError(e, "Exception when refreshing the Plex Metadata"); + throw; + } + } + + private async Task StartPlex() + { + await StartPlexMovies(); + + // Now Tv + await StartPlexTv(); + } + + private async Task StartPlexTv() + { + var allTv = _plexRepo.GetAll().Where(x => + x.Type == PlexMediaTypeEntity.Show && (!x.TheMovieDbId.HasValue() || !x.ImdbId.HasValue() || !x.TvDbId.HasValue())); + var tvCount = 0; + foreach (var show in allTv) + { + var hasImdb = show.ImdbId.HasValue(); + var hasTheMovieDb = show.TheMovieDbId.HasValue(); + var hasTvDbId = show.TvDbId.HasValue(); + + if (!hasTheMovieDb) + { + var id = await GetTheMovieDbId(hasTvDbId, hasImdb, show.TvDbId, show.ImdbId, show.Title); + show.TheMovieDbId = id; + } + + if (!hasImdb) + { + var id = await GetImdbId(hasTheMovieDb, hasTvDbId, show.Title, show.TheMovieDbId, show.TvDbId); + show.ImdbId = id; + _plexRepo.UpdateWithoutSave(show); + } + + if (!hasTvDbId) + { + var id = await GetTvDbId(hasTheMovieDb, hasImdb, show.TheMovieDbId, show.ImdbId, show.Title); + show.TvDbId = id; + _plexRepo.UpdateWithoutSave(show); + } + tvCount++; + if (tvCount >= 20) + { + await _plexRepo.SaveChangesAsync(); + tvCount = 0; + } + } + await _plexRepo.SaveChangesAsync(); + } + + private async Task StartPlexMovies() + { + var allMovies = _plexRepo.GetAll().Where(x => + x.Type == PlexMediaTypeEntity.Movie && (!x.TheMovieDbId.HasValue() || !x.ImdbId.HasValue())); + int movieCount = 0; + foreach (var movie in allMovies) + { + var hasImdb = movie.ImdbId.HasValue(); + var hasTheMovieDb = movie.TheMovieDbId.HasValue(); + // Movies don't really use TheTvDb + + if (!hasImdb) + { + var imdbId = await GetImdbId(hasTheMovieDb, false, movie.Title, movie.TheMovieDbId, string.Empty); + movie.ImdbId = imdbId; + _plexRepo.UpdateWithoutSave(movie); + } + if (!hasTheMovieDb) + { + var id = await GetTheMovieDbId(false, hasImdb, string.Empty, movie.ImdbId, movie.Title); + movie.TheMovieDbId = id; + _plexRepo.UpdateWithoutSave(movie); + } + movieCount++; + if (movieCount >= 20) + { + await _plexRepo.SaveChangesAsync(); + movieCount = 0; + } + } + + await _plexRepo.SaveChangesAsync(); + } + + private async Task GetTheMovieDbId(bool hasTvDbId, bool hasImdb, string tvdbID, string imdbId, string title) + { + _log.LogInformation("The Media item {0} does not have a TheMovieDbId, searching for TheMovieDbId", title); + FindResult result = null; + var hasResult = false; + if (hasTvDbId) + { + result = await _movieApi.Find(tvdbID, ExternalSource.tvdb_id); + hasResult = result?.tv_results?.Length > 0; + + _log.LogInformation("Setting Show {0} because we have TvDbId, result: {1}", title, hasResult); + } + if (hasImdb && !hasResult) + { + result = await _movieApi.Find(imdbId, ExternalSource.imdb_id); + hasResult = result?.tv_results?.Length > 0; + + _log.LogInformation("Setting Show {0} because we have ImdbId, result: {1}", title, hasResult); + } + if (hasResult) + { + return result.tv_results?[0]?.id.ToString() ?? string.Empty; + } + return string.Empty; + } + + private async Task GetImdbId(bool hasTheMovieDb, bool hasTvDbId, string title, string theMovieDbId, string tvDbId) + { + _log.LogInformation("The media item {0} does not have a ImdbId, searching for ImdbId", title); + // Looks like TV Maze does not provide the moviedb id, neither does the TV endpoint on TheMovieDb + if (hasTheMovieDb) + { + _log.LogInformation("The show {0} has TheMovieDbId but not ImdbId, searching for ImdbId", title); + if (int.TryParse(theMovieDbId, out var id)) + { + var result = await _movieApi.GetTvExternals(id); + + return result.imdb_id; + } + } + + if (hasTvDbId) + { + _log.LogInformation("The show {0} has tvdbid but not ImdbId, searching for ImdbId", title); + if (int.TryParse(tvDbId, out var id)) + { + var result = await _tvApi.ShowLookupByTheTvDbId(id); + return result?.externals?.imdb; + } + } + return string.Empty; + } + + + private async Task GetTvDbId(bool hasTheMovieDb, bool hasImdb, string theMovieDbId, string imdbId, string title) + { + _log.LogInformation("The media item {0} does not have a TvDbId, searching for TvDbId", title); + if (hasTheMovieDb) + { + _log.LogInformation("The show {0} has theMovieDBId but not ImdbId, searching for ImdbId", title); + if (int.TryParse(theMovieDbId, out var id)) + { + var result = await _movieApi.GetTvExternals(id); + + return result.tvdb_id.ToString(); + } + } + + if (hasImdb) + { + _log.LogInformation("The show {0} has ImdbId but not ImdbId, searching for ImdbId", title); + var result = await _movieApi.Find(imdbId, ExternalSource.imdb_id); + if (result?.tv_results?.Length > 0) + { + var movieId = result.tv_results?[0]?.id ?? 0; + + var externalResult = await _movieApi.GetTvExternals(movieId); + + return externalResult.imdb_id; + } + } + return string.Empty; + } + + + private bool _disposed; + protected virtual void Dispose(bool disposing) + { + if (_disposed) + return; + + if (disposing) + { + _plexRepo?.Dispose(); + _embyRepo?.Dispose(); + _plexSettings?.Dispose(); + } + _disposed = true; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/src/Ombi.Schedule/Ombi.Schedule.csproj b/src/Ombi.Schedule/Ombi.Schedule.csproj index cb8cef8ab..5088bc9f8 100644 --- a/src/Ombi.Schedule/Ombi.Schedule.csproj +++ b/src/Ombi.Schedule/Ombi.Schedule.csproj @@ -32,8 +32,10 @@ + +
\ No newline at end of file diff --git a/src/Ombi.Schedule/Processor/ChangeLogProcessor.cs b/src/Ombi.Schedule/Processor/ChangeLogProcessor.cs index 09ceed5d3..ea60d932c 100644 --- a/src/Ombi.Schedule/Processor/ChangeLogProcessor.cs +++ b/src/Ombi.Schedule/Processor/ChangeLogProcessor.cs @@ -46,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 { @@ -78,9 +79,9 @@ namespace Ombi.Schedule.Processor Downloads = new List() }; - var releaseTag = latestRelease.InnerText.Substring(0, 6); if (masterBranch) { + var releaseTag = latestRelease.InnerText.Substring(0, 9); await GetGitubRelease(release, releaseTag); } else @@ -147,7 +148,7 @@ namespace Ombi.Schedule.Processor var builds = await _api.Request(request); var jobId = builds.build.jobs.FirstOrDefault()?.jobId ?? string.Empty; - if (builds.build.finished == DateTime.MinValue) + if (builds.build.finished == DateTime.MinValue || builds.build.status.Equals("failed")) { return; } diff --git a/src/Ombi.Settings/Settings/Models/CustomizationSettings.cs b/src/Ombi.Settings/Settings/Models/CustomizationSettings.cs index eef20573a..515c2fc85 100644 --- a/src/Ombi.Settings/Settings/Models/CustomizationSettings.cs +++ b/src/Ombi.Settings/Settings/Models/CustomizationSettings.cs @@ -9,6 +9,7 @@ namespace Ombi.Settings.Settings.Models { public string ApplicationName { get; set; } public string ApplicationUrl { get; set; } + public bool Mobile { get; set; } public string CustomCssLink { get; set; } public bool EnableCustomDonations { get; set; } public string CustomDonationUrl { get; set; } @@ -17,6 +18,7 @@ namespace Ombi.Settings.Settings.Models public string PresetThemeName { get; set; } public string PresetThemeContent { get; set; } + public bool RecentlyAddedPage { get; set; } [NotMapped] public string PresetThemeVersion diff --git a/src/Ombi.Settings/Settings/Models/JobSettings.cs b/src/Ombi.Settings/Settings/Models/JobSettings.cs index 7cf6e7104..a68ceb8bb 100644 --- a/src/Ombi.Settings/Settings/Models/JobSettings.cs +++ b/src/Ombi.Settings/Settings/Models/JobSettings.cs @@ -10,5 +10,7 @@ public string AutomaticUpdater { get; set; } public string UserImporter { get; set; } public string SickRageSync { get; set; } + public string RefreshMetadata { get; set; } + public string Newsletter { get; set; } } } \ No newline at end of file diff --git a/src/Ombi.Settings/Settings/Models/JobSettingsHelper.cs b/src/Ombi.Settings/Settings/Models/JobSettingsHelper.cs index 69eaf4b33..db4083fcd 100644 --- a/src/Ombi.Settings/Settings/Models/JobSettingsHelper.cs +++ b/src/Ombi.Settings/Settings/Models/JobSettingsHelper.cs @@ -1,4 +1,5 @@ -using Ombi.Helpers; +using System; +using Ombi.Helpers; namespace Ombi.Settings.Settings.Models { @@ -35,10 +36,18 @@ namespace Ombi.Settings.Settings.Models { return Get(s.UserImporter, Cron.Daily()); } + public static string Newsletter(JobSettings s) + { + return Get(s.Newsletter, Cron.Weekly(DayOfWeek.Friday, 12)); + } public static string SickRageSync(JobSettings s) { return Get(s.SickRageSync, Cron.Hourly(35)); } + public static string RefreshMetadata(JobSettings s) + { + return Get(s.RefreshMetadata, Cron.Daily(3)); + } private static string Get(string settings, string defaultCron) diff --git a/src/Ombi.Settings/Settings/Models/Notifications/NewsletterSettings.cs b/src/Ombi.Settings/Settings/Models/Notifications/NewsletterSettings.cs new file mode 100644 index 000000000..380e2d743 --- /dev/null +++ b/src/Ombi.Settings/Settings/Models/Notifications/NewsletterSettings.cs @@ -0,0 +1,7 @@ +namespace Ombi.Settings.Settings.Models.Notifications +{ + public class NewsletterSettings : Settings + { + public bool Enabled { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Store/Context/IOmbiContext.cs b/src/Ombi.Store/Context/IOmbiContext.cs index 64f23669e..55d7db563 100644 --- a/src/Ombi.Store/Context/IOmbiContext.cs +++ b/src/Ombi.Store/Context/IOmbiContext.cs @@ -41,5 +41,6 @@ namespace Ombi.Store.Context DbSet SickRageCache { get; set; } DbSet SickRageEpisodeCache { get; set; } DbSet RequestLogs { get; set; } + DbSet RecentlyAddedLogs { get; set; } } } \ No newline at end of file diff --git a/src/Ombi.Store/Context/OmbiContext.cs b/src/Ombi.Store/Context/OmbiContext.cs index 0ccf8ebe5..e4c9be516 100644 --- a/src/Ombi.Store/Context/OmbiContext.cs +++ b/src/Ombi.Store/Context/OmbiContext.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Linq; +using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using Ombi.Helpers; @@ -15,7 +16,7 @@ namespace Ombi.Store.Context public OmbiContext() { if (_created) return; - + _created = true; Database.Migrate(); } @@ -37,6 +38,7 @@ namespace Ombi.Store.Context public DbSet IssueCategories { get; set; } public DbSet IssueComments { get; set; } public DbSet RequestLogs { get; set; } + public DbSet RecentlyAddedLogs { get; set; } public DbSet Audit { get; set; } @@ -55,7 +57,7 @@ namespace Ombi.Store.Context { i.StoragePath = string.Empty; } - optionsBuilder.UseSqlite($"Data Source={Path.Combine(i.StoragePath,"Ombi.db")}"); + optionsBuilder.UseSqlite($"Data Source={Path.Combine(i.StoragePath, "Ombi.db")}"); } protected override void OnModelCreating(ModelBuilder builder) @@ -70,7 +72,7 @@ namespace Ombi.Store.Context .WithMany(b => b.Episodes) .HasPrincipalKey(x => x.EmbyId) .HasForeignKey(p => p.ParentId); - + base.OnModelCreating(builder); } @@ -113,6 +115,15 @@ namespace Ombi.Store.Context // VACUUM; Database.ExecuteSqlCommand("VACUUM;"); + // Make sure we have the roles + var roles = Roles.Where(x => x.Name == OmbiRoles.RecievesNewsletter); + if (!roles.Any()) + { + Roles.Add(new IdentityRole(OmbiRoles.RecievesNewsletter) + { + NormalizedName = OmbiRoles.RecievesNewsletter.ToUpper() + }); + } //Check if templates exist var templates = NotificationTemplates.ToList(); @@ -135,7 +146,7 @@ namespace Ombi.Store.Context notificationToAdd = new NotificationTemplates { NotificationType = notificationType, - Message = "Hello! The user '{RequestedUser}' has requested the {Type} '{Title}'! Please log in to approve this request. Request Date: {RequestedDate}", + Message = "Hello! The user '{UserName}' has requested the {Type} '{Title}'! Please log in to approve this request. Request Date: {RequestedDate}", Subject = "{ApplicationName}: New {Type} request for {Title}!", Agent = agent, Enabled = true, @@ -145,7 +156,7 @@ namespace Ombi.Store.Context notificationToAdd = new NotificationTemplates { NotificationType = notificationType, - Message = "Hello! The user '{IssueUser}' has reported a new issue for the title {Title}!
{IssueCategory} - {IssueSubject} : {IssueDescription}", + Message = "Hello! The user '{UserName}' has reported a new issue for the title {Title}!
{IssueCategory} - {IssueSubject} : {IssueDescription}", Subject = "{ApplicationName}: New issue for {Title}!", Agent = agent, Enabled = true, @@ -155,7 +166,7 @@ namespace Ombi.Store.Context notificationToAdd = new NotificationTemplates { NotificationType = notificationType, - Message = "Hello! You requested {Title} on {ApplicationName}! This is now available! :)", + Message = "Hello! You {Title} on {ApplicationName}! This is now available! :)", Subject = "{ApplicationName}: {Title} is now available!", Agent = agent, Enabled = true, @@ -199,7 +210,7 @@ namespace Ombi.Store.Context notificationToAdd = new NotificationTemplates { NotificationType = notificationType, - Message = "Hello {IssueUser} Your issue for {Title} has now been resolved.", + Message = "Hello {UserName} Your issue for {Title} has now been resolved.", Subject = "{ApplicationName}: Issue has been resolved for {Title}!", Agent = agent, Enabled = true, @@ -218,6 +229,16 @@ namespace Ombi.Store.Context break; case NotificationType.AdminNote: continue; + case NotificationType.Newsletter: + notificationToAdd = new NotificationTemplates + { + NotificationType = notificationType, + Message = "Here is a list of Movies and TV Shows that have recently been added!", + Subject = "{ApplicationName}: Recently Added Content!", + Agent = agent, + Enabled = true, + }; + break; default: throw new ArgumentOutOfRangeException(); } diff --git a/src/Ombi.Store/Entities/PlexServerContent.cs b/src/Ombi.Store/Entities/PlexServerContent.cs index f62dc7ff9..14028cb57 100644 --- a/src/Ombi.Store/Entities/PlexServerContent.cs +++ b/src/Ombi.Store/Entities/PlexServerContent.cs @@ -56,6 +56,15 @@ namespace Ombi.Store.Entities public int Key { get; set; } public DateTime AddedAt { get; set; } public string Quality { get; set; } + + [NotMapped] + public bool HasImdb => !string.IsNullOrEmpty(ImdbId); + + [NotMapped] + public bool HasTvDb => !string.IsNullOrEmpty(TvDbId); + + [NotMapped] + public bool HasTheMovieDb => !string.IsNullOrEmpty(TheMovieDbId); } [Table("PlexSeasonsContent")] diff --git a/src/Ombi.Store/Entities/RecentlyAddedLog.cs b/src/Ombi.Store/Entities/RecentlyAddedLog.cs new file mode 100644 index 000000000..ba26eb566 --- /dev/null +++ b/src/Ombi.Store/Entities/RecentlyAddedLog.cs @@ -0,0 +1,26 @@ +using System; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Ombi.Store.Entities +{ + [Table("RecentlyAddedLog")] + public class RecentlyAddedLog : Entity + { + public RecentlyAddedType Type { get; set; } + public ContentType ContentType { get; set; } + public int ContentId { get; set; } // This is dependant on the type + public DateTime AddedAt { get; set; } + } + + public enum RecentlyAddedType + { + Plex = 0, + Emby = 1 + } + + public enum ContentType + { + Parent = 0, + Episode = 1 + } +} \ No newline at end of file diff --git a/src/Ombi.Store/Migrations/20180322204610_RecentlyAddedLog.Designer.cs b/src/Ombi.Store/Migrations/20180322204610_RecentlyAddedLog.Designer.cs new file mode 100644 index 000000000..fb493660a --- /dev/null +++ b/src/Ombi.Store/Migrations/20180322204610_RecentlyAddedLog.Designer.cs @@ -0,0 +1,936 @@ +// +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("20180322204610_RecentlyAddedLog")] + partial class RecentlyAddedLog + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.0.2-rtm-10011"); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Name") + .HasMaxLength(256); + + b.Property("NormalizedName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("RoleId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider"); + + b.Property("ProviderKey"); + + b.Property("ProviderDisplayName"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId"); + + b.Property("RoleId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId"); + + b.Property("LoginProvider"); + + b.Property("Name"); + + b.Property("Value"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.ApplicationConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Type"); + + b.Property("Value"); + + b.HasKey("Id"); + + b.ToTable("ApplicationConfiguration"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Audit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AuditArea"); + + b.Property("AuditType"); + + b.Property("DateTime"); + + b.Property("Description"); + + b.Property("User"); + + b.HasKey("Id"); + + b.ToTable("Audit"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.CouchPotatoCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("TheMovieDbId"); + + b.HasKey("Id"); + + b.ToTable("CouchPotatoCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AddedAt"); + + b.Property("EmbyId") + .IsRequired(); + + b.Property("ProviderId"); + + b.Property("Title"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.ToTable("EmbyContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AddedAt"); + + b.Property("EmbyId"); + + b.Property("EpisodeNumber"); + + b.Property("ParentId"); + + b.Property("ProviderId"); + + b.Property("SeasonNumber"); + + b.Property("Title"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.ToTable("EmbyEpisode"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.GlobalSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Content"); + + b.Property("SettingsName"); + + b.HasKey("Id"); + + b.ToTable("GlobalSettings"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.NotificationTemplates", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Agent"); + + b.Property("Enabled"); + + b.Property("Message"); + + b.Property("NotificationType"); + + b.Property("Subject"); + + b.HasKey("Id"); + + b.ToTable("NotificationTemplates"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.NotificationUserId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AddedAt"); + + b.Property("PlayerId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("NotificationUserId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.OmbiUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AccessFailedCount"); + + b.Property("Alias"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Email") + .HasMaxLength(256); + + b.Property("EmailConfirmed"); + + b.Property("EmbyConnectUserId"); + + b.Property("EpisodeRequestLimit"); + + b.Property("LastLoggedIn"); + + b.Property("LockoutEnabled"); + + b.Property("LockoutEnd"); + + b.Property("MovieRequestLimit"); + + b.Property("NormalizedEmail") + .HasMaxLength(256); + + b.Property("NormalizedUserName") + .HasMaxLength(256); + + b.Property("PasswordHash"); + + b.Property("PhoneNumber"); + + b.Property("PhoneNumberConfirmed"); + + b.Property("ProviderUserId"); + + b.Property("SecurityStamp"); + + b.Property("TwoFactorEnabled"); + + b.Property("UserAccessToken"); + + b.Property("UserName") + .HasMaxLength(256); + + b.Property("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("Id") + .ValueGeneratedOnAdd(); + + b.Property("EpisodeNumber"); + + b.Property("GrandparentKey"); + + b.Property("Key"); + + b.Property("ParentKey"); + + b.Property("SeasonNumber"); + + b.Property("Title"); + + b.HasKey("Id"); + + b.HasIndex("GrandparentKey"); + + b.ToTable("PlexEpisode"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ParentKey"); + + b.Property("PlexContentId"); + + b.Property("PlexServerContentId"); + + b.Property("SeasonKey"); + + b.Property("SeasonNumber"); + + b.HasKey("Id"); + + b.HasIndex("PlexServerContentId"); + + b.ToTable("PlexSeasonsContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexServerContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AddedAt"); + + b.Property("ImdbId"); + + b.Property("Key"); + + b.Property("Quality"); + + b.Property("ReleaseYear"); + + b.Property("TheMovieDbId"); + + b.Property("Title"); + + b.Property("TvDbId"); + + b.Property("Type"); + + b.Property("Url"); + + b.HasKey("Id"); + + b.ToTable("PlexServerContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.RadarrCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("HasFile"); + + b.Property("TheMovieDbId"); + + b.HasKey("Id"); + + b.ToTable("RadarrCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.RecentlyAddedLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AddedAt"); + + b.Property("ContentId"); + + b.Property("ContentType"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.ToTable("RecentlyAddedLog"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.ChildRequests", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Approved"); + + b.Property("Available"); + + b.Property("Denied"); + + b.Property("DeniedReason"); + + b.Property("IssueId"); + + b.Property("ParentRequestId"); + + b.Property("RequestType"); + + b.Property("RequestedDate"); + + b.Property("RequestedUserId"); + + b.Property("SeriesType"); + + b.Property("Title"); + + b.HasKey("Id"); + + b.HasIndex("ParentRequestId"); + + b.HasIndex("RequestedUserId"); + + b.ToTable("ChildRequests"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.IssueCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Value"); + + b.HasKey("Id"); + + b.ToTable("IssueCategory"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.IssueComments", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Comment"); + + b.Property("Date"); + + b.Property("IssuesId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("IssuesId"); + + b.HasIndex("UserId"); + + b.ToTable("IssueComments"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.Issues", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Description"); + + b.Property("IssueCategoryId"); + + b.Property("IssueId"); + + b.Property("ProviderId"); + + b.Property("RequestId"); + + b.Property("RequestType"); + + b.Property("ResovledDate"); + + b.Property("Status"); + + b.Property("Subject"); + + b.Property("Title"); + + b.Property("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("Id") + .ValueGeneratedOnAdd(); + + b.Property("Approved"); + + b.Property("Available"); + + b.Property("Background"); + + b.Property("Denied"); + + b.Property("DeniedReason"); + + b.Property("DigitalReleaseDate"); + + b.Property("ImdbId"); + + b.Property("IssueId"); + + b.Property("Overview"); + + b.Property("PosterPath"); + + b.Property("QualityOverride"); + + b.Property("ReleaseDate"); + + b.Property("RequestType"); + + b.Property("RequestedDate"); + + b.Property("RequestedUserId"); + + b.Property("RootPathOverride"); + + b.Property("Status"); + + b.Property("TheMovieDbId"); + + b.Property("Title"); + + b.HasKey("Id"); + + b.HasIndex("RequestedUserId"); + + b.ToTable("MovieRequests"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.RequestLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("EpisodeCount"); + + b.Property("RequestDate"); + + b.Property("RequestId"); + + b.Property("RequestType"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RequestLog"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.TvRequests", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ImdbId"); + + b.Property("Overview"); + + b.Property("PosterPath"); + + b.Property("QualityOverride"); + + b.Property("ReleaseDate"); + + b.Property("RootFolder"); + + b.Property("Status"); + + b.Property("Title"); + + b.Property("TvDbId"); + + b.HasKey("Id"); + + b.ToTable("TvRequests"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SickRageCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("TvDbId"); + + b.HasKey("Id"); + + b.ToTable("SickRageCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SickRageEpisodeCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("EpisodeNumber"); + + b.Property("SeasonNumber"); + + b.Property("TvDbId"); + + b.HasKey("Id"); + + b.ToTable("SickRageEpisodeCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SonarrCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("TvDbId"); + + b.HasKey("Id"); + + b.ToTable("SonarrCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SonarrEpisodeCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("EpisodeNumber"); + + b.Property("HasFile"); + + b.Property("SeasonNumber"); + + b.Property("TvDbId"); + + b.HasKey("Id"); + + b.ToTable("SonarrEpisodeCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Tokens", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Token"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Tokens"); + }); + + modelBuilder.Entity("Ombi.Store.Repository.Requests.EpisodeRequests", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AirDate"); + + b.Property("Approved"); + + b.Property("Available"); + + b.Property("EpisodeNumber"); + + b.Property("Requested"); + + b.Property("SeasonId"); + + b.Property("Title"); + + b.Property("Url"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("EpisodeRequests"); + }); + + modelBuilder.Entity("Ombi.Store.Repository.Requests.SeasonRequests", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChildRequestId"); + + b.Property("SeasonNumber"); + + b.HasKey("Id"); + + b.HasIndex("ChildRequestId"); + + b.ToTable("SeasonRequests"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", 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", 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 + } + } +} diff --git a/src/Ombi.Store/Migrations/20180322204610_RecentlyAddedLog.cs b/src/Ombi.Store/Migrations/20180322204610_RecentlyAddedLog.cs new file mode 100644 index 000000000..bc51af276 --- /dev/null +++ b/src/Ombi.Store/Migrations/20180322204610_RecentlyAddedLog.cs @@ -0,0 +1,34 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; +using System.Collections.Generic; + +namespace Ombi.Store.Migrations +{ + public partial class RecentlyAddedLog : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "RecentlyAddedLog", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + AddedAt = table.Column(nullable: false), + ContentId = table.Column(nullable: false), + ContentType = table.Column(nullable: false), + Type = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RecentlyAddedLog", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "RecentlyAddedLog"); + } + } +} diff --git a/src/Ombi.Store/Migrations/OmbiContextModelSnapshot.cs b/src/Ombi.Store/Migrations/OmbiContextModelSnapshot.cs index 35c5755da..a24aa583a 100644 --- a/src/Ombi.Store/Migrations/OmbiContextModelSnapshot.cs +++ b/src/Ombi.Store/Migrations/OmbiContextModelSnapshot.cs @@ -20,7 +20,7 @@ namespace Ombi.Store.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "2.0.0-rtm-26452"); + .HasAnnotation("ProductVersion", "2.0.2-rtm-10011"); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => { @@ -430,6 +430,24 @@ namespace Ombi.Store.Migrations b.ToTable("RadarrCache"); }); + modelBuilder.Entity("Ombi.Store.Entities.RecentlyAddedLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AddedAt"); + + b.Property("ContentId"); + + b.Property("ContentType"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.ToTable("RecentlyAddedLog"); + }); + modelBuilder.Entity("Ombi.Store.Entities.Requests.ChildRequests", b => { b.Property("Id") diff --git a/src/Ombi.Store/Repository/EmbyContentRepository.cs b/src/Ombi.Store/Repository/EmbyContentRepository.cs index 519138edc..280243455 100644 --- a/src/Ombi.Store/Repository/EmbyContentRepository.cs +++ b/src/Ombi.Store/Repository/EmbyContentRepository.cs @@ -45,9 +45,9 @@ namespace Ombi.Store.Repository private IOmbiContext Db { get; } - public async Task> GetAll() + public IQueryable GetAll() { - return await Db.EmbyContent.ToListAsync(); + return Db.EmbyContent.AsQueryable(); } public async Task AddRange(IEnumerable content) diff --git a/src/Ombi.Store/Repository/IEmbyContentRepository.cs b/src/Ombi.Store/Repository/IEmbyContentRepository.cs index e6fe18067..3ed8d8abd 100644 --- a/src/Ombi.Store/Repository/IEmbyContentRepository.cs +++ b/src/Ombi.Store/Repository/IEmbyContentRepository.cs @@ -13,7 +13,7 @@ namespace Ombi.Store.Repository Task ContentExists(string providerId); IQueryable Get(); Task Get(string providerId); - Task> GetAll(); + IQueryable GetAll(); Task GetByEmbyId(string embyId); Task Update(EmbyContent existingContent); IQueryable GetAllEpisodes(); diff --git a/src/Ombi.Store/Repository/INotificationTemplatesRepository.cs b/src/Ombi.Store/Repository/INotificationTemplatesRepository.cs index 861c6b9bd..2398158db 100644 --- a/src/Ombi.Store/Repository/INotificationTemplatesRepository.cs +++ b/src/Ombi.Store/Repository/INotificationTemplatesRepository.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Ombi.Helpers; @@ -6,11 +7,11 @@ using Ombi.Store.Entities; namespace Ombi.Store.Repository { - public interface INotificationTemplatesRepository + public interface INotificationTemplatesRepository : IDisposable { IQueryable All(); - Task> GetAllTemplates(); - Task> GetAllTemplates(NotificationAgent agent); + IQueryable GetAllTemplates(); + IQueryable GetAllTemplates(NotificationAgent agent); Task Insert(NotificationTemplates entity); Task Update(NotificationTemplates template); Task UpdateRange(IEnumerable template); diff --git a/src/Ombi.Store/Repository/IPlexContentRepository.cs b/src/Ombi.Store/Repository/IPlexContentRepository.cs index 2fef89be2..381a89fa3 100644 --- a/src/Ombi.Store/Repository/IPlexContentRepository.cs +++ b/src/Ombi.Store/Repository/IPlexContentRepository.cs @@ -22,5 +22,7 @@ namespace Ombi.Store.Repository Task DeleteEpisode(PlexEpisode content); void DeleteWithoutSave(PlexServerContent content); void DeleteWithoutSave(PlexEpisode content); + Task UpdateRange(IEnumerable existingContent); + void UpdateWithoutSave(PlexServerContent existingContent); } } \ No newline at end of file diff --git a/src/Ombi.Store/Repository/IRepository.cs b/src/Ombi.Store/Repository/IRepository.cs index ed5ed28c5..c85b45d8f 100644 --- a/src/Ombi.Store/Repository/IRepository.cs +++ b/src/Ombi.Store/Repository/IRepository.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Query; using Ombi.Store.Entities; @@ -24,5 +25,6 @@ namespace Ombi.Store.Repository where TEntity : class; Task ExecuteSql(string sql); + DbSet _db { get; } } } \ No newline at end of file diff --git a/src/Ombi.Store/Repository/NotificationTemplatesRepository.cs b/src/Ombi.Store/Repository/NotificationTemplatesRepository.cs index e4b484967..175d0e6a9 100644 --- a/src/Ombi.Store/Repository/NotificationTemplatesRepository.cs +++ b/src/Ombi.Store/Repository/NotificationTemplatesRepository.cs @@ -23,14 +23,14 @@ namespace Ombi.Store.Repository return Db.NotificationTemplates.AsQueryable(); } - public async Task> GetAllTemplates() + public IQueryable GetAllTemplates() { - return await Db.NotificationTemplates.ToListAsync(); + return Db.NotificationTemplates; } - public async Task> GetAllTemplates(NotificationAgent agent) + public IQueryable GetAllTemplates(NotificationAgent agent) { - return await Db.NotificationTemplates.Where(x => x.Agent == agent).ToListAsync(); + return Db.NotificationTemplates.Where(x => x.Agent == agent); } public async Task GetTemplate(NotificationAgent agent, NotificationType type) @@ -40,6 +40,11 @@ namespace Ombi.Store.Repository public async Task Update(NotificationTemplates template) { + if (Db.Entry(template).State == EntityState.Detached) + { + Db.Attach(template); + Db.Entry(template).State = EntityState.Modified; + } await Db.SaveChangesAsync(); } @@ -60,5 +65,26 @@ namespace Ombi.Store.Repository await Db.SaveChangesAsync().ConfigureAwait(false); return settings.Entity; } + + private bool _disposed; + // Protected implementation of Dispose pattern. + protected virtual void Dispose(bool disposing) + { + if (_disposed) + return; + + if (disposing) + { + Db?.Dispose(); + } + + _disposed = true; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } } } \ No newline at end of file diff --git a/src/Ombi.Store/Repository/PlexContentRepository.cs b/src/Ombi.Store/Repository/PlexContentRepository.cs index 56fec441a..098466310 100644 --- a/src/Ombi.Store/Repository/PlexContentRepository.cs +++ b/src/Ombi.Store/Repository/PlexContentRepository.cs @@ -97,6 +97,16 @@ namespace Ombi.Store.Repository Db.PlexServerContent.Update(existingContent); await Db.SaveChangesAsync(); } + public void UpdateWithoutSave(PlexServerContent existingContent) + { + Db.PlexServerContent.Update(existingContent); + } + + public async Task UpdateRange(IEnumerable existingContent) + { + Db.PlexServerContent.UpdateRange(existingContent); + await Db.SaveChangesAsync(); + } public IQueryable GetAllEpisodes() { diff --git a/src/Ombi.Store/Repository/Repository.cs b/src/Ombi.Store/Repository/Repository.cs index b4b9f8e93..049da0356 100644 --- a/src/Ombi.Store/Repository/Repository.cs +++ b/src/Ombi.Store/Repository/Repository.cs @@ -17,7 +17,7 @@ namespace Ombi.Store.Repository _ctx = ctx; _db = _ctx.Set(); } - private readonly DbSet _db; + public DbSet _db { get; } private readonly IOmbiContext _ctx; public async Task Find(object key) diff --git a/src/Ombi.TheMovieDbApi/IMovieDbApi.cs b/src/Ombi.TheMovieDbApi/IMovieDbApi.cs index dd0d0e92c..787902a4b 100644 --- a/src/Ombi.TheMovieDbApi/IMovieDbApi.cs +++ b/src/Ombi.TheMovieDbApi/IMovieDbApi.cs @@ -15,5 +15,7 @@ namespace Ombi.Api.TheMovieDb Task> TopRated(); Task> Upcoming(); Task> SimilarMovies(int movieId); + Task Find(string externalId, ExternalSource source); + Task GetTvExternals(int theMovieDbId); } } \ No newline at end of file diff --git a/src/Ombi.TheMovieDbApi/Models/FindResult.cs b/src/Ombi.TheMovieDbApi/Models/FindResult.cs new file mode 100644 index 000000000..f76fca564 --- /dev/null +++ b/src/Ombi.TheMovieDbApi/Models/FindResult.cs @@ -0,0 +1,52 @@ +namespace Ombi.Api.TheMovieDb.Models +{ + public class FindResult + { + public Movie_Results[] movie_results { get; set; } + public object[] person_results { get; set; } + public TvResults[] tv_results { get; set; } + public object[] tv_episode_results { get; set; } + public object[] tv_season_results { get; set; } + } + + public class Movie_Results + { + public bool adult { get; set; } + public string backdrop_path { get; set; } + public int[] genre_ids { get; set; } + public int id { get; set; } + public string original_language { get; set; } + public string original_title { get; set; } + public string overview { get; set; } + public string poster_path { get; set; } + public string release_date { get; set; } + public string title { get; set; } + public bool video { get; set; } + public float vote_average { get; set; } + public int vote_count { get; set; } + } + + + public class TvResults + { + public string original_name { get; set; } + public int id { get; set; } + public string name { get; set; } + public int vote_count { get; set; } + public float vote_average { get; set; } + public string first_air_date { get; set; } + public string poster_path { get; set; } + public int[] genre_ids { get; set; } + public string original_language { get; set; } + public string backdrop_path { get; set; } + public string overview { get; set; } + public string[] origin_country { get; set; } + } + + + public enum ExternalSource + { + imdb_id, + tvdb_id + } +} \ No newline at end of file diff --git a/src/Ombi.TheMovieDbApi/Models/TvExternals.cs b/src/Ombi.TheMovieDbApi/Models/TvExternals.cs new file mode 100644 index 000000000..237ae36a7 --- /dev/null +++ b/src/Ombi.TheMovieDbApi/Models/TvExternals.cs @@ -0,0 +1,16 @@ +namespace Ombi.Api.TheMovieDb.Models +{ + public class TvExternals + { + public string imdb_id { get; set; } + public string freebase_mid { get; set; } + public string freebase_id { get; set; } + public int tvdb_id { get; set; } + public int tvrage_id { get; set; } + public string facebook_id { get; set; } + public object instagram_id { get; set; } + public object twitter_id { get; set; } + public int id { get; set; } + } + +} \ No newline at end of file diff --git a/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs b/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs index 1fbfe9aaf..08925e490 100644 --- a/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs +++ b/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Net; using System.Net.Http; using System.Threading.Tasks; using AutoMapper; @@ -25,15 +26,37 @@ namespace Ombi.Api.TheMovieDb { var request = new Request($"movie/{movieId}", BaseUri, HttpMethod.Get); request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); + AddRetry(request); var result = await Api.Request(request); return Mapper.Map(result); } + public async Task Find(string externalId, ExternalSource source) + { + var request = new Request($"find/{externalId}", BaseUri, HttpMethod.Get); + request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); + AddRetry(request); + + request.AddQueryString("external_source", source.ToString()); + + return await Api.Request(request); + } + + public async Task GetTvExternals(int theMovieDbId) + { + var request = new Request($"/tv/{theMovieDbId}/external_ids", BaseUri, HttpMethod.Get); + request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); + AddRetry(request); + + return await Api.Request(request); + } + public async Task> SimilarMovies(int movieId) { var request = new Request($"movie/{movieId}/similar", BaseUri, HttpMethod.Get); request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); + AddRetry(request); var result = await Api.Request>(request); return Mapper.Map>(result.results); @@ -44,6 +67,7 @@ namespace Ombi.Api.TheMovieDb var request = new Request($"movie/{movieId}", BaseUri, HttpMethod.Get); request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); request.FullUri = request.FullUri.AddQueryParameter("append_to_response", "videos,release_dates"); + AddRetry(request); var result = await Api.Request(request); return Mapper.Map(result); } @@ -53,6 +77,7 @@ namespace Ombi.Api.TheMovieDb var request = new Request($"search/movie", BaseUri, HttpMethod.Get); request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); request.FullUri = request.FullUri.AddQueryParameter("query", searchTerm); + AddRetry(request); var result = await Api.Request>(request); return Mapper.Map>(result.results); @@ -62,6 +87,7 @@ namespace Ombi.Api.TheMovieDb { var request = new Request($"movie/popular", BaseUri, HttpMethod.Get); request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); + AddRetry(request); var result = await Api.Request>(request); return Mapper.Map>(result.results); } @@ -70,6 +96,7 @@ namespace Ombi.Api.TheMovieDb { var request = new Request($"movie/top_rated", BaseUri, HttpMethod.Get); request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); + AddRetry(request); var result = await Api.Request>(request); return Mapper.Map>(result.results); } @@ -78,6 +105,7 @@ namespace Ombi.Api.TheMovieDb { var request = new Request($"movie/upcoming", BaseUri, HttpMethod.Get); request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); + AddRetry(request); var result = await Api.Request>(request); return Mapper.Map>(result.results); } @@ -86,9 +114,14 @@ namespace Ombi.Api.TheMovieDb { var request = new Request($"movie/now_playing", BaseUri, HttpMethod.Get); request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); + AddRetry(request); var result = await Api.Request>(request); return Mapper.Map>(result.results); } - + private static void AddRetry(Request request) + { + request.Retry = true; + request.StatusCodeToRetry.Add((HttpStatusCode)429); + } } } diff --git a/src/Ombi/ClientApp/app/animations/fadeinout.ts b/src/Ombi/ClientApp/app/animations/fadeinout.ts new file mode 100644 index 000000000..8ecf15a15 --- /dev/null +++ b/src/Ombi/ClientApp/app/animations/fadeinout.ts @@ -0,0 +1,12 @@ +import { animate, style, transition, trigger } from "@angular/animations"; +import { AnimationEntryMetadata } from "@angular/core"; + +export const fadeInOutAnimation: AnimationEntryMetadata = trigger("fadeInOut", [ + transition(":enter", [ // :enter is alias to 'void => *' + style({ opacity: 0 }), + animate(1000, style({ opacity: 1 })), + ]), + transition(":leave", [ // :leave is alias to '* => void' + animate(1000, style({ opacity: 0 })), + ]), +]); diff --git a/src/Ombi/ClientApp/app/app.component.html b/src/Ombi/ClientApp/app/app.component.html index 6a5ea4f52..0db13fa9c 100644 --- a/src/Ombi/ClientApp/app/app.component.html +++ b/src/Ombi/ClientApp/app/app.component.html @@ -34,6 +34,14 @@ {{ 'NavigationBar.Requests' | translate }}
+ +