Merge pull request #2107 from tidusjar/develop

Newsletter merge
pull/2121/head
Jamie 7 years ago committed by GitHub
commit 39344d20b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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)

@ -32,9 +32,9 @@ namespace Ombi.Api.FanartTv
}
}
public async Task<MovieResult> GetMovieImages(int theMovieDbId, string token)
public async Task<MovieResult> 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<MovieResult>(request);

@ -5,7 +5,7 @@ namespace Ombi.Api.FanartTv
{
public interface IFanartTvApi
{
Task<MovieResult> GetMovieImages(int theMovieDbId, string token);
Task<MovieResult> GetMovieImages(string movieOrImdbId, string token);
Task<TvResult> GetTvImages(int tvdbId, string token);
}
}

@ -5,6 +5,6 @@ namespace Ombi.Api.Mattermost
{
public interface IMattermostApi
{
Task<string> PushAsync(string webhook, MattermostBody message);
Task PushAsync(string webhook, MattermostMessage message);
}
}

@ -14,14 +14,10 @@ namespace Ombi.Api.Mattermost
private readonly IApi _api;
public async Task<string> 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);
}
}
}

@ -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();
/// <summary>
/// Create a new Mattermost Client
/// </summary>
/// <param name="webhookUrl">The URL of your Mattermost Webhook</param>
/// <param name="timeoutSeconds">Timeout Value (Default 100)</param>
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;
}
/// <summary>
/// Post Message to Mattermost server. Messages will be automatically split if total text length > 4000
/// </summary>
/// <param name="api"></param>
/// <param name="inMessage">The messsage you wish to send</param>
/// <returns></returns>
public async Task PostAsync(IApi api, MattermostMessage inMessage)
{
try
{
var outMessages = new List<MattermostMessage>();
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<MattermostAttachment>();
//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<MattermostAttachment> { 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;
}
}
}
}

@ -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
/// <summary>
/// Channel to post to
/// </summary>
[JsonProperty(PropertyName = "channel")]
public string Channel { get; set; }
/// <summary>
/// Username for bot
/// </summary>
[JsonProperty(PropertyName = "username")]
public string Username { get; set; }
/// <summary>
/// Bot/User Icon
/// </summary>
[JsonProperty(PropertyName = "icon_url")]
public Uri IconUrl { get; set; }
/// <summary>
/// Message body. Supports Markdown
/// </summary>
[JsonProperty(PropertyName = "text")]
public string Text { get; set; }
/// <summary>
/// Richtext attachments
/// </summary>
[JsonProperty(PropertyName = "attachments")]
public List<MattermostAttachment> Attachments { get; set; }
}
/// <summary>
/// https://docs.mattermost.com/developer/message-attachments.html#message-attachments
/// </summary>
public class MattermostAttachment
{
//https://docs.mattermost.com/developer/message-attachments.html#attachment-options
#region AttachmentOptions
/// <summary>
/// A required plain-text summary of the post. This is used in notifications, and in clients that dont support formatted text (eg IRC).
/// </summary>
[JsonProperty(PropertyName = "fallback")]
public string Fallback { get; set; }
/// <summary>
/// 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.
/// </summary>
[JsonProperty(PropertyName = "color")]
public string Color { get; set; }
/// <summary>
/// Optional text that should appear above the formatted data
/// </summary>
[JsonProperty(PropertyName = "pretext")]
public string Pretext { get; set; }
/// <summary>
/// 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.
/// </summary>
[JsonProperty(PropertyName = "text")]
public string Text { get; set; }
#endregion
//https://docs.mattermost.com/developer/message-attachments.html#author-details
#region AuthorDetails
/// <summary>
/// An optional name used to identify the author. It will be included in a small section at the top of the attachment.
/// </summary>
[JsonProperty(PropertyName = "author_name")]
public string AuthorName { get; set; }
/// <summary>
/// An optional URL used to hyperlink the author_name. If no author_name is specified, this field does nothing.
/// </summary>
[JsonProperty(PropertyName = "author_link")]
public Uri AuthorLink { get; set; }
/// <summary>
/// An optional URL used to display a 16x16 pixel icon beside the author_name.
/// </summary>
[JsonProperty(PropertyName = "author_icon")]
public Uri AuthorIcon { get; set; }
#endregion
//https://docs.mattermost.com/developer/message-attachments.html#titles
#region Titles
/// <summary>
/// An optional title displayed below the author information in the attachment.
/// </summary>
[JsonProperty(PropertyName = "title")]
public string Title { get; set; }
/// <summary>
/// An optional URL used to hyperlink the title. If no title is specified, this field does nothing.
/// </summary>
[JsonProperty(PropertyName = "title_link")]
public Uri TitleLink { get; set; }
#endregion
#region Fields
/// <summary>
/// Fields can be included as an optional array within attachments, and are used to display information in a table format inside the attachment.
/// </summary>
[JsonProperty(PropertyName = "fields")]
public List<MattermostField> Fields { get; set; }
#endregion
//https://docs.mattermost.com/developer/message-attachments.html#images
#region Images
/// <summary>
/// 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.
/// </summary>
[JsonProperty(PropertyName = "image_url")]
public Uri ImageUrl { get; set; }
/// <summary>
/// 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.
/// </summary>
[JsonProperty(PropertyName = "thumb_url")]
public Uri ThumbUrl { get; set; }
#endregion
}
/// <summary>
/// https://docs.mattermost.com/developer/message-attachments.html#fieldshttps://docs.mattermost.com/developer/message-attachments.html#fields
/// </summary>
public class MattermostField
{
/// <summary>
/// A title shown in the table above the value.
/// </summary>
[JsonProperty(PropertyName = "title")]
public string Title { get; set; }
/// <summary>
/// The text value of the field. It can be formatted using Markdown.
/// </summary>
[JsonProperty(PropertyName = "value")]
public string Value { get; set; }
/// <summary>
/// Optionally set to “True” or “False” to indicate whether the value is short enough to be displayed beside other values.
/// </summary>
[JsonProperty(PropertyName = "short")]
public bool Short { get; set; }
}
}

@ -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<HttpRequestException>()
.OrResult<HttpResponseMessage>(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

@ -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<HttpRequestMessage> Clone(this HttpRequestMessage request)
{
var clone = new HttpRequestMessage(request.Method, request.RequestUri)
{
Content = await request.Content.Clone(),
Version = request.Version
};
foreach (KeyValuePair<string, object> prop in request.Properties)
{
clone.Properties.Add(prop);
}
foreach (KeyValuePair<string, IEnumerable<string>> header in request.Headers)
{
clone.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
return clone;
}
public static async Task<HttpContent> 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<string, IEnumerable<string>> header in content.Headers)
{
clone.Headers.Add(header.Key, header.Value);
}
return clone;
}
}
}

@ -11,6 +11,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="10.0.3" />
<PackageReference Include="Polly" Version="5.8.0" />
<PackageReference Include="System.Xml.XmlSerializer" Version="4.3.0" />
</ItemGroup>

@ -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<HttpStatusCode> StatusCodeToRetry { get; set; } = new List<HttpStatusCode>();
public Action<string> OnBeforeDeserialization { get; set; }
private string FullUrl

@ -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<RecentlyAddedMovieModel> GetRecentlyAddedMovies();
IEnumerable<RecentlyAddedMovieModel> GetRecentlyAddedMovies(DateTime from, DateTime to);
IEnumerable<RecentlyAddedTvModel> GetRecentlyAddedTv(DateTime from, DateTime to, bool groupBySeason);
IEnumerable<RecentlyAddedTvModel> GetRecentlyAddedTv(bool groupBySeason);
Task<bool> UpdateRecentlyAddedDatabase();
}
}

@ -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 =

@ -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<RecentlyAddedLog> recentlyAdded)
{
_plex = plex;
_emby = emby;
_recentlyAddedLog = recentlyAdded;
}
private readonly IPlexContentRepository _plex;
private readonly IEmbyContentRepository _emby;
private readonly IRepository<RecentlyAddedLog> _recentlyAddedLog;
public IEnumerable<RecentlyAddedMovieModel> 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<RecentlyAddedMovieModel> 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<RecentlyAddedTvModel> 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<RecentlyAddedTvModel> 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<bool> UpdateRecentlyAddedDatabase()
{
var plexContent = _plex.GetAll().Include(x => x.Episodes);
var embyContent = _emby.GetAll().Include(x => x.Episodes);
var recentlyAddedLog = new HashSet<RecentlyAddedLog>();
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<RecentlyAddedTvModel> GetRecentlyAddedTv(IQueryable<PlexServerContent> plexTv, IQueryable<EmbyContent> embyTv,
bool groupBySeason)
{
var model = new HashSet<RecentlyAddedTvModel>();
TransformPlexShows(plexTv, model);
TransformEmbyShows(embyTv, model);
if (groupBySeason)
{
return model.DistinctBy(x => x.SeasonNumber);
}
return model;
}
private IEnumerable<RecentlyAddedMovieModel> GetRecentlyAddedMovies(IQueryable<PlexServerContent> plexMovies, IQueryable<EmbyContent> embyMovies)
{
var model = new HashSet<RecentlyAddedMovieModel>();
TransformPlexMovies(plexMovies, model);
TransformEmbyMovies(embyMovies, model);
return model;
}
private static void TransformEmbyMovies(IQueryable<EmbyContent> embyMovies, HashSet<RecentlyAddedMovieModel> 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<PlexServerContent> plexMovies, HashSet<RecentlyAddedMovieModel> 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<PlexServerContent> plexShows, HashSet<RecentlyAddedTvModel> 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<EmbyContent> embyShows, HashSet<RecentlyAddedTvModel> 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
});
}
}
}
}
}

@ -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
}
}

@ -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; }
}
}

@ -0,0 +1,23 @@

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

@ -79,6 +79,7 @@ namespace Ombi.DependencyInjection
services.AddTransient<ITvSearchEngine, TvSearchEngine>();
services.AddTransient<IRuleEvaluator, RuleEvaluator>();
services.AddTransient<IMovieSender, MovieSender>();
services.AddTransient<IRecentlyAddedEngine, RecentlyAddedEngine>();
services.AddTransient<ITvSender, TvSender>();
services.AddTransient<IMassEmailSender, MassEmailSender>();
}
@ -172,6 +173,8 @@ namespace Ombi.DependencyInjection
services.AddTransient<ICouchPotatoSync, CouchPotatoSync>();
services.AddTransient<IProcessProvider, ProcessProvider>();
services.AddTransient<ISickRageSync, SickRageSync>();
services.AddTransient<IRefreshMetadata, RefreshMetadata>();
services.AddTransient<INewsletterJob, NewsletterJob>();
}
}
}

@ -13,5 +13,6 @@
WelcomeEmail = 8,
IssueResolved = 9,
IssueComment = 10,
Newsletter = 11,
}
}

@ -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);
}
}

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

@ -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 : $"<tr><td align=\"center\"><img src=\"{imgsrc}\" alt=\"Poster\" width=\"400px\" text-align=\"center\"/></td></tr>");
sb.Replace(Logo, string.IsNullOrEmpty(logo) ? "http://i.imgur.com/qQsN78U.png" : logo);
sb.Replace(Logo, string.IsNullOrEmpty(logo) ? OmbiLogo : logo);
return sb.ToString();
}

@ -0,0 +1,7 @@
namespace Ombi.Notifications.Templates
{
public interface INewsletterTemplate
{
string LoadTemplate(string subject, string intro, string tableHtml, string logo);
}
}

@ -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();
}
}
}

@ -9,6 +9,9 @@
</PropertyGroup>
<ItemGroup>
<None Update="Templates\NewsletterTemplate.html">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Templates\BasicTemplate.html">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>

@ -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";
}
}

@ -0,0 +1,187 @@
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Ombi</title>
<style media="all" type="text/css">
@media all {
.btn-primary table td:hover {
background-color: #34495e !important;
}
.btn-primary a:hover {
background-color: #34495e !important;
border-color: #34495e !important;
}
}
@media all {
.btn-secondary a:hover {
border-color: #34495e !important;
color: #34495e !important;
}
}
@media only screen and (max-width: 620px) {
table[class=body] h1 {
font-size: 28px !important;
margin-bottom: 10px !important;
}
table[class=body] h2 {
font-size: 22px !important;
margin-bottom: 10px !important;
}
table[class=body] h3 {
font-size: 16px !important;
margin-bottom: 10px !important;
}
table[class=body] p,
table[class=body] ul,
table[class=body] ol,
table[class=body] td,
table[class=body] span,
table[class=body] a {
font-size: 16px !important;
}
table[class=body] .wrapper,
table[class=body] .article {
padding: 10px !important;
}
table[class=body] .content {
padding: 0 !important;
}
table[class=body] .container {
padding: 0 !important;
width: 100% !important;
}
table[class=body] .header {
margin-bottom: 10px !important;
}
table[class=body] .main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important;
}
table[class=body] .btn table {
width: 100% !important;
}
table[class=body] .btn a {
width: 100% !important;
}
table[class=body] .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !important;
}
table[class=body] .alert td {
border-radius: 0 !important;
padding: 10px !important;
}
table[class=body] .span-2,
table[class=body] .span-3 {
max-width: none !important;
width: 100% !important;
}
table[class=body] .receipt {
width: 100% !important;
}
}
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%;
}
.apple-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important;
}
}
</style>
</head>
<body class="" style="font-family: sans-serif; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.4; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; background-color: #f6f6f6; margin: 0; padding: 0;">
<table border="0" cellpadding="0" cellspacing="0" class="body" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background-color: #f6f6f6;" width="100%" bgcolor="#f6f6f6">
<tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top">&nbsp;</td>
<td class="container" style="font-family: sans-serif; font-size: 14px; vertical-align: top; display: block; Margin: 0 auto !important; max-width: 580px; padding: 10px; width: 580px;" width="580" valign="top">
<div class="content" style="box-sizing: border-box; display: block; Margin: 0 auto; max-width: 580px; padding: 10px;">
<!-- START CENTERED WHITE CONTAINER -->
<span class="preheader" style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">Ombi Recently Added</span>
<table class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #fff; border-radius: 3px;" width="100%">
<!-- START MAIN CONTENT AREA -->
<tr>
<td class="wrapper" style="font-family: sans-serif; font-size: 14px; vertical-align: top; box-sizing: border-box; padding: 20px;" valign="top">
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;" width="100%">
<tr>
<td align="center">
<img src="{@LOGO}" width="400px" text-align="center" />
</td>
</tr>
<tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top">
<br />
<br />
<p style="font-family: sans-serif; font-size: 20px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{@INTRO}</p>
</td>
</tr>
</table>
{@RECENTLYADDED}
</td>
</tr>
<!-- END MAIN CONTENT AREA -->
</table>
<!-- START FOOTER -->
<div class="footer" style="clear: both; padding-top: 10px; text-align: center; width: 100%;">
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;" width="100%">
<tr>
<td class="content-block powered-by" style="font-family: sans-serif; vertical-align: top; padding-top: 10px; padding-bottom: 10px; font-size: 12px; color: #999999; text-align: center;" valign="top" align="center">
Powered by <a href="https://github.com/tidusjar/Ombi" style="color: #999999; font-size: 12px; text-align: center; text-decoration: underline;">Ombi</a>
</td>
</tr>
</table>
</div>
<!-- END FOOTER -->
<!-- END CENTERED WHITE CONTAINER -->
</div>
</td>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top">&nbsp;</td>
</tr>
</table>
</body>
</html>

@ -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<MattermostAttachment>
{
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);
}

@ -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(),

@ -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<EpisodeRequests>();
var seasons = req?.SeasonRequests?.OrderBy(x => x.SeasonNumber).ToList() ?? new List<SeasonRequests>();
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},
};
}
}

@ -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> jobsettings, ISickRageSync srSync)
ISettingsService<JobSettings> 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> 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> _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));
}
}
}

@ -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("<p style=\"font-family: sans-serif; font-size: {1}px; font-weight: {2}; margin: 0; Margin-bottom: 15px;\">{0}</p>", text, fontSize, fontWeight);
}
protected virtual void AddImageInsideTable(StringBuilder sb, string url, int size = 400)
{
sb.Append("<tr>");
sb.Append("<td align=\"center\">");
sb.Append($"<img src=\"{url}\" width=\"{size}px\" text-align=\"center\" />");
sb.Append("</td>");
sb.Append("</tr>");
}
protected virtual void Href(StringBuilder sb, string url)
{
sb.AppendFormat("<a href=\"{0}\">", url);
}
protected virtual void TableData(StringBuilder sb)
{
sb.Append(
"<td align=\"center\" style=\"font-family: sans-serif; font-size: 14px; vertical-align: top;\" valign=\"top\">");
}
protected virtual void EndTag(StringBuilder sb, string tag)
{
sb.AppendFormat("</{0}>", tag);
}
protected virtual void Header(StringBuilder sb, int size, string text, string fontWeight = "normal")
{
sb.AppendFormat(
"<h{0} style=\"font-family: sans-serif; font-weight: {2}; margin: 0; Margin-bottom: 15px;\">{1}</h{0}>",
size, text, fontWeight);
}
}
}

@ -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);
}
}

@ -0,0 +1,9 @@
using System.Threading.Tasks;
namespace Ombi.Schedule.Jobs.Ombi
{
public interface IRefreshMetadata : IBaseJob
{
Task Start();
}
}

@ -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<RecentlyAddedLog> addedLog,
IMovieDbApi movieApi, ITvMazeApi tvApi, IEmailProvider email, ISettingsService<CustomizationSettings> custom,
ISettingsService<EmailNotificationSettings> emailSettings, INotificationTemplatesRepository templateRepo,
UserManager<OmbiUser> um, ISettingsService<NewsletterSettings> 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> _recentlyAddedLog;
private readonly IMovieDbApi _movieApi;
private readonly ITvMazeApi _tvApi;
private readonly IEmailProvider _email;
private readonly ISettingsService<CustomizationSettings> _customizationSettings;
private readonly INotificationTemplatesRepository _templateRepo;
private readonly ISettingsService<EmailNotificationSettings> _emailSettings;
private readonly ISettingsService<NewsletterSettings> _newsletterSettings;
private readonly UserManager<OmbiUser> _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<Task>();
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<RecentlyAddedLog>();
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<string> BuildHtml(IQueryable<PlexServerContent> plexContentToSend, IQueryable<EmbyContent> embyContentToSend, IQueryable<PlexEpisode> plexEpisodes, IQueryable<EmbyEpisode> 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("<h1>New Movies:</h1><br /><br />");
await ProcessPlexMovies(plexMovies, sb);
await ProcessEmbyMovies(embyMovies, sb);
}
if (plexEpisodes.Any() || embyEp.Any())
{
sb.Append("<h1>New Episodes:</h1><br /><br />");
await ProcessPlexTv(plexEpisodes, sb);
await ProcessEmbyTv(embyEp, sb);
}
return sb.ToString();
}
private async Task ProcessPlexMovies(IQueryable<PlexServerContent> plexContentToSend, StringBuilder sb)
{
sb.Append(
"<table border=\"0\" cellpadding=\"0\" align=\"center\" cellspacing=\"0\" style=\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;\" width=\"100%\">");
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> embyContent, StringBuilder sb)
{
sb.Append(
"<table border=\"0\" cellpadding=\"0\" align=\"center\" cellspacing=\"0\" style=\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;\" width=\"100%\">");
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("<tr>");
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<PlexEpisode> plexContent, StringBuilder sb)
{
var series = new List<PlexServerContent>();
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> { plexEpisode };
series.Add(plexEpisode.Series);
}
}
var orderedTv = series.OrderByDescending(x => x.AddedAt);
sb.Append(
"<table border=\"0\" cellpadding=\"0\" align=\"center\" cellspacing=\"0\" style=\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;\" width=\"100%\">");
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("<tr>");
sb.Append(
"<td align=\"center\" style=\"font-family: sans-serif; font-size: 14px; vertical-align: top;\" valign=\"top\">");
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("</table><br /><br />");
}
private async Task ProcessEmbyTv(IQueryable<EmbyEpisode> embyContent, StringBuilder sb)
{
var series = new List<EmbyContent>();
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<EmbyEpisode>
{
episode
};
series.Add(episode.Series);
}
}
var orderedTv = series.OrderByDescending(x => x.AddedAt);
sb.Append(
"<table border=\"0\" cellpadding=\"0\" align=\"center\" cellspacing=\"0\" style=\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;\" width=\"100%\">");
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("<tr>");
sb.Append(
"<td align=\"center\" style=\"font-family: sans-serif; font-size: 14px; vertical-align: top;\" valign=\"top\">");
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("</table><br /><br />");
}
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("<hr />");
sb.Append("<br />");
sb.Append("<br />");
sb.Append("</td>");
sb.Append("</tr>");
}
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);
}
}
}

@ -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<RefreshMetadata> log, ITvMazeApi tvApi, ISettingsService<PlexSettings> 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> _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<string> 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<string> 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<string> 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);
}
}
}

@ -32,8 +32,10 @@
<ProjectReference Include="..\Ombi.Api.Service\Ombi.Api.Service.csproj" />
<ProjectReference Include="..\Ombi.Api.SickRage\Ombi.Api.SickRage.csproj" />
<ProjectReference Include="..\Ombi.Api.Sonarr\Ombi.Api.Sonarr.csproj" />
<ProjectReference Include="..\Ombi.Api.TvMaze\Ombi.Api.TvMaze.csproj" />
<ProjectReference Include="..\Ombi.Notifications\Ombi.Notifications.csproj" />
<ProjectReference Include="..\Ombi.Settings\Ombi.Settings.csproj" />
<ProjectReference Include="..\Ombi.TheMovieDbApi\Ombi.Api.TheMovieDb.csproj" />
</ItemGroup>
</Project>

@ -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<Downloads>()
};
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<AppveyorBranchResult>(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;
}

@ -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

@ -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; }
}
}

@ -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)

@ -0,0 +1,7 @@
namespace Ombi.Settings.Settings.Models.Notifications
{
public class NewsletterSettings : Settings
{
public bool Enabled { get; set; }
}
}

@ -41,5 +41,6 @@ namespace Ombi.Store.Context
DbSet<SickRageCache> SickRageCache { get; set; }
DbSet<SickRageEpisodeCache> SickRageEpisodeCache { get; set; }
DbSet<RequestLog> RequestLogs { get; set; }
DbSet<RecentlyAddedLog> RecentlyAddedLogs { get; set; }
}
}

@ -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<IssueCategory> IssueCategories { get; set; }
public DbSet<IssueComments> IssueComments { get; set; }
public DbSet<RequestLog> RequestLogs { get; set; }
public DbSet<RecentlyAddedLog> RecentlyAddedLogs { get; set; }
public DbSet<Audit> 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}! </br> {IssueCategory} - {IssueSubject} : {IssueDescription}",
Message = "Hello! The user '{UserName}' has reported a new issue for the title {Title}! </br> {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();
}

@ -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")]

@ -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
}
}

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

@ -0,0 +1,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<int>(nullable: false)
.Annotation("Sqlite:Autoincrement", true),
AddedAt = table.Column<DateTime>(nullable: false),
ContentId = table.Column<int>(nullable: false),
ContentType = table.Column<int>(nullable: false),
Type = table.Column<int>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_RecentlyAddedLog", x => x.Id);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "RecentlyAddedLog");
}
}
}

@ -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<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("AddedAt");
b.Property<int>("ContentId");
b.Property<int>("ContentType");
b.Property<int>("Type");
b.HasKey("Id");
b.ToTable("RecentlyAddedLog");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.ChildRequests", b =>
{
b.Property<int>("Id")

@ -45,9 +45,9 @@ namespace Ombi.Store.Repository
private IOmbiContext Db { get; }
public async Task<IEnumerable<EmbyContent>> GetAll()
public IQueryable<EmbyContent> GetAll()
{
return await Db.EmbyContent.ToListAsync();
return Db.EmbyContent.AsQueryable();
}
public async Task AddRange(IEnumerable<EmbyContent> content)

@ -13,7 +13,7 @@ namespace Ombi.Store.Repository
Task<bool> ContentExists(string providerId);
IQueryable<EmbyContent> Get();
Task<EmbyContent> Get(string providerId);
Task<IEnumerable<EmbyContent>> GetAll();
IQueryable<EmbyContent> GetAll();
Task<EmbyContent> GetByEmbyId(string embyId);
Task Update(EmbyContent existingContent);
IQueryable<EmbyEpisode> GetAllEpisodes();

@ -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<NotificationTemplates> All();
Task<IEnumerable<NotificationTemplates>> GetAllTemplates();
Task<IEnumerable<NotificationTemplates>> GetAllTemplates(NotificationAgent agent);
IQueryable<NotificationTemplates> GetAllTemplates();
IQueryable<NotificationTemplates> GetAllTemplates(NotificationAgent agent);
Task<NotificationTemplates> Insert(NotificationTemplates entity);
Task Update(NotificationTemplates template);
Task UpdateRange(IEnumerable<NotificationTemplates> template);

@ -22,5 +22,7 @@ namespace Ombi.Store.Repository
Task DeleteEpisode(PlexEpisode content);
void DeleteWithoutSave(PlexServerContent content);
void DeleteWithoutSave(PlexEpisode content);
Task UpdateRange(IEnumerable<PlexServerContent> existingContent);
void UpdateWithoutSave(PlexServerContent existingContent);
}
}

@ -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<T> _db { get; }
}
}

@ -23,14 +23,14 @@ namespace Ombi.Store.Repository
return Db.NotificationTemplates.AsQueryable();
}
public async Task<IEnumerable<NotificationTemplates>> GetAllTemplates()
public IQueryable<NotificationTemplates> GetAllTemplates()
{
return await Db.NotificationTemplates.ToListAsync();
return Db.NotificationTemplates;
}
public async Task<IEnumerable<NotificationTemplates>> GetAllTemplates(NotificationAgent agent)
public IQueryable<NotificationTemplates> GetAllTemplates(NotificationAgent agent)
{
return await Db.NotificationTemplates.Where(x => x.Agent == agent).ToListAsync();
return Db.NotificationTemplates.Where(x => x.Agent == agent);
}
public async Task<NotificationTemplates> 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);
}
}
}

@ -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<PlexServerContent> existingContent)
{
Db.PlexServerContent.UpdateRange(existingContent);
await Db.SaveChangesAsync();
}
public IQueryable<PlexEpisode> GetAllEpisodes()
{

@ -17,7 +17,7 @@ namespace Ombi.Store.Repository
_ctx = ctx;
_db = _ctx.Set<T>();
}
private readonly DbSet<T> _db;
public DbSet<T> _db { get; }
private readonly IOmbiContext _ctx;
public async Task<T> Find(object key)

@ -15,5 +15,7 @@ namespace Ombi.Api.TheMovieDb
Task<List<MovieSearchResult>> TopRated();
Task<List<MovieSearchResult>> Upcoming();
Task<List<MovieSearchResult>> SimilarMovies(int movieId);
Task<FindResult> Find(string externalId, ExternalSource source);
Task<TvExternals> GetTvExternals(int theMovieDbId);
}
}

@ -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
}
}

@ -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; }
}
}

@ -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<MovieResponse>(request);
return Mapper.Map<MovieResponseDto>(result);
}
public async Task<FindResult> 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<FindResult>(request);
}
public async Task<TvExternals> 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<TvExternals>(request);
}
public async Task<List<MovieSearchResult>> 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<TheMovieDbContainer<SearchResult>>(request);
return Mapper.Map<List<MovieSearchResult>>(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<MovieResponse>(request);
return Mapper.Map<MovieResponseDto>(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<TheMovieDbContainer<SearchResult>>(request);
return Mapper.Map<List<MovieSearchResult>>(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<TheMovieDbContainer<SearchResult>>(request);
return Mapper.Map<List<MovieSearchResult>>(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<TheMovieDbContainer<SearchResult>>(request);
return Mapper.Map<List<MovieSearchResult>>(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<TheMovieDbContainer<SearchResult>>(request);
return Mapper.Map<List<MovieSearchResult>>(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<TheMovieDbContainer<SearchResult>>(request);
return Mapper.Map<List<MovieSearchResult>>(result.results);
}
private static void AddRetry(Request request)
{
request.Retry = true;
request.StatusCodeToRetry.Add((HttpStatusCode)429);
}
}
}

@ -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 })),
]),
]);

@ -34,6 +34,14 @@
<i class="fa fa-th-list"></i> {{ 'NavigationBar.Requests' | translate }}</a>
</li>
</ul>
<div *ngIf="customizationSettings">
<ul *ngIf="customizationSettings.recentlyAddedPage" class="nav navbar-nav">
<li id="RecentlyAdded" [routerLinkActive]="['active']">
<a [routerLink]="['/recentlyadded']">
<i class="fa fa-check"></i> {{ 'NavigationBar.RecentlyAdded' | translate }}</a>
</li>
</ul>
</div>
<ul *ngIf="issuesEnabled" class="nav navbar-nav">
<li id="Issues" [routerLinkActive]="['active']">
<a [routerLink]="['/issues']">
@ -46,6 +54,7 @@
<i class="fa fa-user"></i> {{ 'NavigationBar.UserManagement' | translate }}</a>
</li>
</ul>
<ul *ngIf="hasRole('Admin') || hasRole('PowerUser')" class="nav navbar-nav">
<li>
<a href="https://www.paypal.me/PlexRequestsNet" target="_blank" pTooltip="{{ 'NavigationBar.DonateTooltip' | translate }}">
@ -84,7 +93,7 @@
<a [routerLink]="['/usermanagement/updatedetails']">
<i class="fa fa-key"></i>{{ 'NavigationBar.UpdateDetails' | translate }}</a>
</li>
<li *ngIf="showMobileLink" [routerLinkActive]="['active']">
<li *ngIf="customizationSettings?.mobile" [routerLinkActive]="['active']">
<a href="#" (click)="openMobileApp($event)">
<i class="fa fa-mobile"></i>{{ 'NavigationBar.OpenMobileApp' | translate }}</a>
</li>

@ -23,7 +23,6 @@ export class AppComponent implements OnInit {
public updateAvailable: boolean;
public currentUrl: string;
public userAccessToken: string;
public showMobileLink = false;
private checkedForUpdate: boolean;

@ -52,6 +52,7 @@ const routes: Routes = [
{ loadChildren: "./usermanagement/usermanagement.module#UserManagementModule", path: "usermanagement" },
{ loadChildren: "./requests/requests.module#RequestsModule", path: "requests" },
{ loadChildren: "./search/search.module#SearchModule", path: "search" },
{ loadChildren: "./recentlyAdded/recentlyAdded.module#RecentlyAddedModule", path: "recentlyadded" },
];
// AoT requires an exported function for factories

@ -46,6 +46,7 @@ export enum NotificationType {
WelcomeEmail = 8,
IssueResolved = 9,
IssueComment = 10,
Newsletter = 11,
}
export interface IDiscordNotifcationSettings extends INotificationSettings {
@ -54,6 +55,10 @@ export interface IDiscordNotifcationSettings extends INotificationSettings {
notificationTemplates: INotificationTemplates[];
}
export interface INewsletterNotificationSettings extends INotificationSettings {
notificationTemplate: INotificationTemplates;
}
export interface ITelegramNotifcationSettings extends INotificationSettings {
botApi: string;
chatId: string;

@ -0,0 +1,29 @@
export interface IRecentlyAddedMovies {
id: number;
title: string;
overview: string;
imdbId: string;
theMovieDbId: string;
releaseYear: string;
addedAt: Date;
quality: string;
// For UI only
posterPath: string;
}
export interface IRecentlyAddedTvShows extends IRecentlyAddedMovies {
seasonNumber: number;
episodeNumber: number;
tvDbId: number;
}
export interface IRecentlyAddedRangeModel {
from: Date;
to: Date;
}
export enum RecentlyAddedType {
Plex,
Emby,
}

@ -97,6 +97,7 @@ export interface ICustomizationSettings extends ISettings {
applicationName: string;
applicationUrl: string;
logo: string;
mobile: boolean;
customCssLink: string;
enableCustomDonations: boolean;
customDonationUrl: string;
@ -106,6 +107,7 @@ export interface ICustomizationSettings extends ISettings {
presetThemeContent: string;
presetThemeDisplayName: string;
presetThemeVersion: string;
recentlyAddedPage: boolean;
}
export interface IThemes {
@ -124,6 +126,8 @@ export interface IJobSettings {
automaticUpdater: string;
userImporter: string;
sickRageSync: string;
refreshMetadata: string;
newsletter: string;
}
export interface IIssueSettings extends ISettings {
@ -192,3 +196,18 @@ export interface IDogNzbSettings extends ISettings {
export interface IIssueCategory extends ISettings {
value: string;
}
export interface ICronTestModel {
success: boolean;
message: string;
schedule: Date[];
}
export interface ICronViewModelBody {
expression: string;
}
export interface IJobSettingsViewModel {
result: boolean;
message: string;
}

@ -13,3 +13,4 @@ export * from "./ISettings";
export * from "./ISonarr";
export * from "./IUser";
export * from "./IIssues";
export * from "./IRecentlyAdded";

@ -1,13 +1,16 @@
<div *ngIf="issue">
<div class="myBg backdrop" [style.background-image]="backgroundPath"></div>
<div class="tint" style="background-image: linear-gradient(to bottom, rgba(0,0,0,0.6) 0%,rgba(0,0,0,0.6) 100%);"></div>
<h1>{{issue.title}} </h1>
<div class="col-md-6">
<span class="label label-info">{{IssueStatus[issue.status]}}</span>
<span class="label label-success">{{issue.issueCategory.value}}</span>
<h3 *ngIf="issue.userReported?.alias">{{'Issues.ReportedBy' | translate}}: {{issue.userReported.alias}}</h3>
<h3 *ngIf="!issue.userReported?.alias">{{'Issues.ReportedBy' | translate}}: {{issue.userReported.userName}}</h3>
<img class="img-responsive poster" src="{{posterPath}}" alt="poster">
<span class="label label-info">{{IssueStatus[issue.status]}}</span>
<span class="label label-success">{{issue.issueCategory.value}}</span>
<h3 *ngIf="issue.userReported?.alias">{{'Issues.ReportedBy' | translate}}: {{issue.userReported.alias}}</h3>
<h3 *ngIf="!issue.userReported?.alias">{{'Issues.ReportedBy' | translate}}: {{issue.userReported.userName}}</h3>
<h3 *ngIf="issue.subject">{{'Issues.Subject' | translate}}: {{issue.subject}}</h3>
<br>
<br>
<div class="form-group">
<label for="description" class="control-label" [translate]="'Issues.Description'"></label>
<div>
@ -26,7 +29,8 @@
<div class="panel-heading top-bar">
<div class="col-md-8 col-xs-8">
<h3 class="panel-title">
<span class="glyphicon glyphicon-comment"></span> {{'Issues.Comments' | translate}}</h3>
<span class="glyphicon glyphicon-comment"></span> {{'Issues.Comments' | translate}}
</h3>
</div>
</div>
@ -51,8 +55,7 @@
</div>
<div class="panel-footer">
<div class="input-group">
<input id="btn-input" type="text" class="form-control input-sm chat_input" [(ngModel)]="newComment.comment" [attr.placeholder]="'Issues.WriteMessagePlaceholder' | translate"
/>
<input id="btn-input" type="text" class="form-control input-sm chat_input" [(ngModel)]="newComment.comment" [attr.placeholder]="'Issues.WriteMessagePlaceholder' | translate" />
<span class="input-group-btn">
<button class="btn btn-primary btn-sm" id="btn-chat" (click)="addComment()" [translate]="'Issues.SendMessageButton'"></button>
</span>

@ -64,6 +64,15 @@ body{
overflow: hidden;
display: flex;
}
.myBg {
z-index: -1;
}
.tint {
z-index: -1;
}
img-responsive poster {
display:block;
}
img {
display: block;
width: 100%;

@ -2,8 +2,9 @@ import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { AuthService } from "../auth/auth.service";
import { IssuesService, NotificationService, SettingsService } from "../services";
import { ImageService, IssuesService, NotificationService, SettingsService } from "../services";
import { DomSanitizer } from "@angular/platform-browser";
import { IIssues, IIssuesChat, IIssueSettings, INewIssueComments, IssueStatus } from "../interfaces";
@Component({
@ -22,6 +23,8 @@ export class IssueDetailsComponent implements OnInit {
public IssueStatus = IssueStatus;
public isAdmin: boolean;
public settings: IIssueSettings;
public backgroundPath: any;
public posterPath: any;
private issueId: number;
@ -29,7 +32,9 @@ export class IssueDetailsComponent implements OnInit {
private route: ActivatedRoute,
private authService: AuthService,
private settingsService: SettingsService,
private notificationService: NotificationService) {
private notificationService: NotificationService,
private imageService: ImageService,
private sanitizer: DomSanitizer) {
this.route.params
.subscribe((params: any) => {
this.issueId = parseInt(params.id);
@ -56,8 +61,8 @@ export class IssueDetailsComponent implements OnInit {
providerId: x.providerId,
userReported: x.userReported,
};
this.setBackground(x);
});
this.loadComments();
}
@ -85,4 +90,26 @@ export class IssueDetailsComponent implements OnInit {
private loadComments() {
this.issueService.getComments(this.issueId).subscribe(x => this.comments = x);
}
private setBackground(issue: any) {
if (issue.requestType === 1) {
this.imageService.getMovieBackground(issue.providerId).subscribe(x => {
this.backgroundPath = this.sanitizer.bypassSecurityTrustStyle
("url(" + x + ")");
});
this.imageService.getMoviePoster(issue.providerId).subscribe(x => {
this.posterPath = x.toString();
});
} else {
this.imageService.getTvBackground(Number(issue.providerId)).subscribe(x => {
this.backgroundPath = this.sanitizer.bypassSecurityTrustStyle
("url(" + x + ")");
});
this.imageService.getTvPoster(Number(issue.providerId)).subscribe(x => {
this.posterPath = x.toString();
});
}
}
}

@ -5,7 +5,7 @@ import { NgbModule } from "@ng-bootstrap/ng-bootstrap";
import { OrderModule } from "ngx-order-pipe";
import { PaginatorModule, SharedModule, TabViewModule } from "primeng/primeng";
import { IdentityService } from "../services";
import { IdentityService, SearchService } from "../services";
import { AuthGuard } from "../auth/auth.guard";
@ -43,6 +43,7 @@ const routes: Routes = [
],
providers: [
IdentityService,
SearchService,
],
})

@ -1,5 +1,5 @@
<div *ngIf="landingPageSettings && customizationSettings">
<div *ngIf="background" class="bg" [style.background-image]="background"></div>
<div *ngIf="background" @fadeInOut class="bg" [style.background-image]="background"></div>
<div class="centered col-md-12">
<div class="row">

@ -1,5 +1,5 @@
import { PlatformLocation } from "@angular/common";
import { Component, OnInit } from "@angular/core";
import { Component, OnDestroy, OnInit } from "@angular/core";
import { IMediaServerStatus } from "../interfaces";
import { ICustomizationSettings, ILandingPageSettings } from "../interfaces";
@ -9,17 +9,21 @@ import { SettingsService } from "../services";
import { DomSanitizer } from "@angular/platform-browser";
import { ImageService } from "../services";
import { fadeInOutAnimation } from "../animations/fadeinout";
@Component({
templateUrl: "./landingpage.component.html",
animations: [fadeInOutAnimation],
styleUrls: ["./landingpage.component.scss"],
})
export class LandingPageComponent implements OnInit {
export class LandingPageComponent implements OnDestroy, OnInit {
public customizationSettings: ICustomizationSettings;
public landingPageSettings: ILandingPageSettings;
public background: any;
public mediaServerStatus: IMediaServerStatus;
public baseUrl: string;
private timer: any;
constructor(private settingsService: SettingsService,
private images: ImageService, private sanitizer: DomSanitizer, private landingPageService: LandingPageService,
@ -31,6 +35,9 @@ export class LandingPageComponent implements OnInit {
this.images.getRandomBackground().subscribe(x => {
this.background = this.sanitizer.bypassSecurityTrustStyle("linear-gradient(-10deg, transparent 20%, rgba(0,0,0,0.7) 20.0%, rgba(0,0,0,0.7) 80.0%, transparent 80%), url(" + x.url + ")");
});
this.timer = setInterval(() => {
this.cycleBackground();
}, 10000);
const base = this.location.getBaseHrefFromDOM();
if (base.length > 1) {
@ -41,4 +48,18 @@ export class LandingPageComponent implements OnInit {
this.mediaServerStatus = x;
});
}
public ngOnDestroy() {
clearInterval(this.timer);
}
public cycleBackground() {
this.images.getRandomBackground().subscribe(x => {
this.background = "";
});
this.images.getRandomBackground().subscribe(x => {
this.background = this.sanitizer
.bypassSecurityTrustStyle("linear-gradient(-10deg, transparent 20%, rgba(0,0,0,0.7) 20.0%, rgba(0,0,0,0.7) 80.0%, transparent 80%), url(" + x.url + ")");
});
}
}

@ -4,7 +4,7 @@ include the remember me checkbox
-->
<div *ngIf="form && customizationSettings">
<div *ngIf="background" class="bg" [style.background-image]="background"></div>
<div *ngIf="background" @fadeInOut class="bg" [style.background-image]="background"></div>
<div class="container" id="login">
<div class="card card-container">
<!-- <img class="profile-img-card" src="//lh3.googleusercontent.com/-6V8xOA6M7BA/AAAAAAAAAAI/AAAAAAAAAAA/rzlHcD0KYwo/photo.jpg?sz=120" alt="" /> -->

@ -1,4 +1,4 @@
import { Component, OnInit } from "@angular/core";
import { Component, OnDestroy, OnInit } from "@angular/core";
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { TranslateService } from "@ngx-translate/core";
@ -13,11 +13,14 @@ import { StatusService } from "../services";
import { DomSanitizer } from "@angular/platform-browser";
import { ImageService } from "../services";
import { fadeInOutAnimation } from "../animations/fadeinout";
@Component({
templateUrl: "./login.component.html",
animations: [fadeInOutAnimation],
styleUrls: ["./login.component.scss"],
})
export class LoginComponent implements OnInit {
export class LoginComponent implements OnDestroy, OnInit {
public form: FormGroup;
public customizationSettings: ICustomizationSettings;
@ -25,6 +28,7 @@ export class LoginComponent implements OnInit {
public background: any;
public landingFlag: boolean;
public baseUrl: string;
private timer: any;
private errorBody: string;
private errorValidation: string;
@ -67,6 +71,10 @@ export class LoginComponent implements OnInit {
this.images.getRandomBackground().subscribe(x => {
this.background = this.sanitizer.bypassSecurityTrustStyle("linear-gradient(-10deg, transparent 20%, rgba(0,0,0,0.7) 20.0%, rgba(0,0,0,0.7) 80.0%, transparent 80%),url(" + x.url + ")");
});
this.timer = setInterval(() => {
this.cycleBackground();
}, 10000);
const base = this.location.getBaseHrefFromDOM();
if (base.length > 1) {
this.baseUrl = base;
@ -102,4 +110,19 @@ export class LoginComponent implements OnInit {
}, err => this.notify.error(this.errorBody));
});
}
public ngOnDestroy() {
clearInterval(this.timer);
}
private cycleBackground() {
this.images.getRandomBackground().subscribe(x => {
this.background = "";
});
this.images.getRandomBackground().subscribe(x => {
this.background = this.sanitizer
.bypassSecurityTrustStyle("linear-gradient(-10deg, transparent 20%, rgba(0,0,0,0.7) 20.0%, rgba(0,0,0,0.7) 80.0%, transparent 80%), url(" + x.url + ")");
});
}
}

@ -0,0 +1,51 @@
<h1>Recently Added</h1>
<input type="checkbox" [(ngModel)]="groupTv" (click)="change()" />
<hr />
<p-calendar [(ngModel)]="range" showButtonBar="true" selectionMode="range" (onClose)="close()"></p-calendar>
<hr />
<style>
.img-conatiner {
position: relative;
text-align: center;
color: white;
}
/* Bottom left text */
.bottom-left {
position: absolute;
bottom: 8px;
left: 16px;
}
</style>
<ngu-carousel [inputs]="carouselTile">
<ngu-tile NguCarouselItem *ngFor="let movie of movies">
<div class="img-container">
<img class="img-responsive poster" src="{{movie.posterPath}}" style="width: 300px" alt="poster">
<div class="bottom-left"> {{movie.title}}</div>
</div>
</ngu-tile>
<button NguCarouselPrev class='leftRs'><i class="fa fa-arrow-left"></i></button>
<button NguCarouselNext class='rightRs'><i class="fa fa-arrow-right"></i></button>
</ngu-carousel>
<hr/>
<ngu-carousel [inputs]="carouselTile">
<ngu-tile NguCarouselItem *ngFor="let t of tv">
<img class="img-responsive poster" src="{{t.posterPath}}" style="width: 300px" alt="poster">
<b>{{t.title}}</b>
<br>
<b>Season: {{t.seasonNumber}}</b>
<br>
<b>Episode: {{t.episodeNumber}}</b>
</ngu-tile>
<button NguCarouselPrev class='leftRs'><i class="fa fa-arrow-left"></i></button>
<button NguCarouselNext class='rightRs'><i class="fa fa-arrow-right"></i></button>
</ngu-carousel>

@ -0,0 +1,127 @@
import { Component, OnInit } from "@angular/core";
import { NguCarousel } from "@ngu/carousel";
import { ImageService, RecentlyAddedService } from "../services";
import { IRecentlyAddedMovies, IRecentlyAddedTvShows } from "./../interfaces";
@Component({
templateUrl: "recentlyAdded.component.html",
styles: [`
.leftRs {
position: absolute;
margin: auto;
top: 0;
bottom: 0;
width: 50px;
height: 50px;
box-shadow: 1px 2px 10px -1px rgba(0, 0, 0, .3);
border-radius: 100%;
left: 0;
background: #df691a;
}
.rightRs {
position: absolute;
margin: auto;
top: 0;
bottom: 0;
width: 50px;
height: 50px;
box-shadow: 1px 2px 10px -1px rgba(0, 0, 0, .3);
border-radius: 100%;
right: 0;
background: #df691a;
}
`],
})
export class RecentlyAddedComponent implements OnInit {
public movies: IRecentlyAddedMovies[];
public tv: IRecentlyAddedTvShows[];
public range: Date[];
public groupTv: boolean = false;
// https://github.com/sheikalthaf/ngu-carousel
public carouselTile: NguCarousel;
constructor(private recentlyAddedService: RecentlyAddedService,
private imageService: ImageService) {}
public ngOnInit() {
this.getMovies();
this.getShows();
this.carouselTile = {
grid: {xs: 2, sm: 3, md: 3, lg: 5, all: 0},
slide: 2,
speed: 400,
animation: "lazy",
point: {
visible: true,
},
load: 2,
touch: true,
easing: "ease",
};
}
public close() {
if(this.range.length < 2) {
return;
}
if(!this.range[1]) {
// If we do not have a second date then just set it to now
this.range[1] = new Date();
}
this.getMovies();
}
public change() {
this.getShows();
}
private getShows() {
if(this.groupTv) {
this.recentlyAddedService.getRecentlyAddedTvGrouped().subscribe(x => {
this.tv = x;
this.tv.forEach((t) => {
this.imageService.getTvPoster(t.tvDbId).subscribe(p => {
t.posterPath = p;
});
});
});
} else {
this.recentlyAddedService.getRecentlyAddedTv().subscribe(x => {
this.tv = x;
this.tv.forEach((t) => {
this.imageService.getTvPoster(t.tvDbId).subscribe(p => {
t.posterPath = p;
});
});
});
}
}
private getMovies() {
this.recentlyAddedService.getRecentlyAddedMovies().subscribe(x => {
this.movies = x;
this.movies.forEach((movie) => {
if(movie.theMovieDbId) {
this.imageService.getMoviePoster(movie.theMovieDbId).subscribe(p => {
movie.posterPath = p;
});
} else if(movie.imdbId) {
this.imageService.getMoviePoster(movie.imdbId).subscribe(p => {
movie.posterPath = p;
});
} else {
movie.posterPath = "";
}
});
});
}
}

@ -0,0 +1,47 @@
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { NgbModule } from "@ng-bootstrap/ng-bootstrap";
import { OrderModule } from "ngx-order-pipe";
import { CalendarModule, PaginatorModule, SharedModule, TabViewModule } from "primeng/primeng";
import { IdentityService, ImageService, RecentlyAddedService } from "../services";
import { AuthGuard } from "../auth/auth.guard";
import { SharedModule as OmbiShared } from "../shared/shared.module";
import { RecentlyAddedComponent } from "./recentlyAdded.component";
import { NguCarouselModule } from "@ngu/carousel";
const routes: Routes = [
{ path: "", component: RecentlyAddedComponent, canActivate: [AuthGuard] },
];
@NgModule({
imports: [
RouterModule.forChild(routes),
NgbModule.forRoot(),
SharedModule,
OrderModule,
OmbiShared,
PaginatorModule,
TabViewModule,
CalendarModule,
NguCarouselModule,
],
declarations: [
RecentlyAddedComponent,
],
exports: [
RouterModule,
],
providers: [
IdentityService,
RecentlyAddedService,
ImageService,
],
})
export class RecentlyAddedModule { }

@ -207,7 +207,7 @@
<issue-report [movie]="true" [visible]="issuesBarVisible" (visibleChange)="issuesBarVisible = $event;" [title]="issueRequest?.title"
[issueCategory]="issueCategorySelected" [id]="issueRequest?.id" [providerId]=""></issue-report>
[issueCategory]="issueCategorySelected" [id]="issueRequest?.id" [providerId]="issueProviderId"></issue-report>
<p-sidebar [(visible)]="filterDisplay" styleClass="ui-sidebar-md side-back side-small">

@ -105,4 +105,4 @@
<issue-report [movie]="false" [visible]="issuesBarVisible" [title]="issueRequest?.title"
[issueCategory]="issueCategorySelected" [id]="issueRequest?.id" (visibleChange)="issuesBarVisible = $event;"></issue-report>
[issueCategory]="issueCategorySelected" [id]="issueRequest?.id" [providerId]="issueProviderId" (visibleChange)="issuesBarVisible = $event;"></issue-report>

@ -105,6 +105,7 @@ export class TvRequestChildrenComponent {
this.issueRequest = req;
this.issueCategorySelected = catId;
this.issuesBarVisible = true;
this.issueProviderId = req.id.toString();
}
private removeRequestFromUi(key: IChildRequests) {

@ -129,7 +129,6 @@ export class TvSearchComponent implements OnInit {
public getExtraInfo() {
this.tvResults.forEach((val, index) => {
this.imageService.getTvBanner(val.data.id).subscribe(x => {
val.data.background = this.sanitizer.

@ -12,6 +12,7 @@ import {
IEmailNotificationSettings,
IEmbyServer,
IMattermostNotifcationSettings,
INewsletterNotificationSettings,
IPlexServer,
IPushbulletNotificationSettings,
IPushoverNotificationSettings,
@ -77,5 +78,8 @@ export class TesterService extends ServiceHelpers {
public sickrageTest(settings: ISickRageSettings): Observable<boolean> {
return this.http.post<boolean>(`${this.url}sickrage`, JSON.stringify(settings), {headers: this.headers});
}
public newsletterTest(settings: INewsletterNotificationSettings): Observable<boolean> {
return this.http.post<boolean>(`${this.url}newsletter`, JSON.stringify(settings), {headers: this.headers});
}
}

@ -20,4 +20,21 @@ export class ImageService extends ServiceHelpers {
public getTvBanner(tvdbid: number): Observable<string> {
return this.http.get<string>(`${this.url}tv/${tvdbid}`, {headers: this.headers});
}
public getMoviePoster(movieDbId: string): Observable<string> {
return this.http.get<string>(`${this.url}poster/movie/${movieDbId}`, { headers: this.headers });
}
public getTvPoster(tvdbid: number): Observable<string> {
return this.http.get<string>(`${this.url}poster/tv/${tvdbid}`, { headers: this.headers });
}
public getMovieBackground(movieDbId: string): Observable<string> {
return this.http.get<string>(`${this.url}background/movie/${movieDbId}`, { headers: this.headers });
}
public getTvBackground(tvdbid: number): Observable<string> {
return this.http.get<string>(`${this.url}background/tv/${tvdbid}`, { headers: this.headers });
}
}

@ -1,4 +1,4 @@
export * from "./applications";
export * from "./applications";
export * from "./helpers";
export * from "./identity.service";
export * from "./image.service";
@ -13,3 +13,4 @@ export * from "./job.service";
export * from "./issues.service";
export * from "./mobile.service";
export * from "./notificationMessage.service";
export * from "./recentlyAdded.service";

@ -38,4 +38,8 @@ export class JobService extends ServiceHelpers {
public runEmbyCacher(): Observable<boolean> {
return this.http.post<boolean>(`${this.url}embycontentcacher/`, {headers: this.headers});
}
public runNewsletter(): Observable<boolean> {
return this.http.post<boolean>(`${this.url}newsletter/`, {headers: this.headers});
}
}

@ -0,0 +1,25 @@
import { PlatformLocation } from "@angular/common";
import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs/Rx";
import { IRecentlyAddedMovies, IRecentlyAddedTvShows } from "./../interfaces";
import { ServiceHelpers } from "./service.helpers";
@Injectable()
export class RecentlyAddedService extends ServiceHelpers {
constructor(http: HttpClient, public platformLocation: PlatformLocation) {
super(http, "/api/v1/recentlyadded/", platformLocation);
}
public getRecentlyAddedMovies(): Observable<IRecentlyAddedMovies[]> {
return this.http.get<IRecentlyAddedMovies[]>(`${this.url}movies/`, {headers: this.headers});
}
public getRecentlyAddedTv(): Observable<IRecentlyAddedTvShows[]> {
return this.http.get<IRecentlyAddedTvShows[]>(`${this. url}tv/`, {headers: this.headers});
}
public getRecentlyAddedTvGrouped(): Observable<IRecentlyAddedTvShows[]> {
return this.http.get<IRecentlyAddedTvShows[]>(`${this.url}tv/grouped`, {headers: this.headers});
}
}

@ -7,6 +7,8 @@ import {
IAbout,
IAuthenticationSettings,
ICouchPotatoSettings,
ICronTestModel,
ICronViewModelBody,
ICustomizationSettings,
IDiscordNotifcationSettings,
IDogNzbSettings,
@ -14,9 +16,11 @@ import {
IEmbySettings,
IIssueSettings,
IJobSettings,
IJobSettingsViewModel,
ILandingPageSettings,
IMattermostNotifcationSettings,
IMobileNotifcationSettings,
INewsletterNotificationSettings,
IOmbiSettings,
IPlexSettings,
IPushbulletNotificationSettings,
@ -231,10 +235,15 @@ export class SettingsService extends ServiceHelpers {
return this.http.get<IJobSettings>(`${this.url}/jobs`, {headers: this.headers});
}
public saveJobSettings(settings: IJobSettings): Observable<boolean> {
public saveJobSettings(settings: IJobSettings): Observable<IJobSettingsViewModel> {
return this.http
.post<boolean>(`${this.url}/jobs`, JSON.stringify(settings), {headers: this.headers});
}
.post<IJobSettingsViewModel>(`${this.url}/jobs`, JSON.stringify(settings), {headers: this.headers});
}
public testCron(body: ICronViewModelBody): Observable<ICronTestModel> {
return this.http
.post<ICronTestModel>(`${this.url}/testcron`, JSON.stringify(body), {headers: this.headers});
}
public getSickRageSettings(): Observable<ISickRageSettings> {
return this.http.get<ISickRageSettings>(`${this.url}/sickrage`, {headers: this.headers});
@ -257,4 +266,17 @@ export class SettingsService extends ServiceHelpers {
return this.http
.post<boolean>(`${this.url}/issues`, JSON.stringify(settings), {headers: this.headers});
}
public getNewsletterSettings(): Observable<INewsletterNotificationSettings> {
return this.http.get<INewsletterNotificationSettings>(`${this.url}/notifications/newsletter`, {headers: this.headers});
}
public updateNewsletterDatabase(): Observable<boolean> {
return this.http.post<boolean>(`${this.url}/notifications/newsletterdatabase`, {headers: this.headers});
}
public saveNewsletterSettings(settings: INewsletterNotificationSettings): Observable<boolean> {
return this.http
.post<boolean>(`${this.url}/notifications/newsletter`, JSON.stringify(settings), {headers: this.headers});
}
}

@ -3,6 +3,12 @@
<fieldset *ngIf="settings">
<legend>Customization</legend>
<div class="row">
<div class="col-md-2 col-md-push-10">
<span style="vertical-align: top;">Advanced</span>
<p-inputSwitch id="customInputSwitch" [(ngModel)]="advanced"></p-inputSwitch>
</div>
</div>
<div class="col-md-5">
<div class="form-group">
<label for="applicationName" class="control-label">Application Name</label>
@ -20,6 +26,20 @@
</div>
</div>
<div class="form-group" *ngIf="advanced">
<div class="checkbox">
<input type="checkbox" id="enable" [(ngModel)]="settings.mobile" [checked]="settings.mobile">
<label for="enable">Enable Mobile</label>
</div>
</div>
<!-- <div class="form-group">
<div class="checkbox">
<input type="checkbox" id="enable" [(ngModel)]="settings.recentlyAddedPage" [checked]="settings.recentlyAddedPage">
<label for="enable">Enable Recently Added Page</label>
</div>
</div> -->
<div class="form-group">
<label for="logo" class="control-label">Custom Logo</label>
<div>

@ -11,6 +11,7 @@ export class CustomizationComponent implements OnInit {
public settings: ICustomizationSettings;
public themes: IThemes[];
public advanced: boolean;
constructor(private settingsService: SettingsService, private notificationService: NotificationService) { }

@ -12,29 +12,34 @@
<label for="sonarrSync" class="control-label">Sonarr Sync</label>
<input type="text" class="form-control form-control-custom" [ngClass]="{'form-error': form.get('sonarrSync').hasError('required')}" id="sonarrSync" name="sonarrSync" formControlName="sonarrSync">
<small *ngIf="form.get('sonarrSync').hasError('required')" class="error-text">The Sonarr Sync is required</small>
<button type="button" class="btn btn-sm btn-primary-outline" (click)="testCron(form.get('sonarrSync')?.value)">Test</button>
</div>
<div class="form-group">
<label for="sickRageSync" class="control-label">SickRage Sync</label>
<input type="text" class="form-control form-control-custom" [ngClass]="{'form-error': form.get('sonarrSync').hasError('required')}" id="sickRageSync" name="sickRageSync" formControlName="sickRageSync">
<small *ngIf="form.get('sickRageSync').hasError('required')" class="error-text">The SickRage Sync is required</small>
<button type="button" class="btn btn-sm btn-primary-outline" (click)="testCron(form.get('sickRageSync')?.value)">Test</button>
</div>
<div class="form-group">
<label for="radarrSync" class="control-label">Radarr Sync</label>
<input type="text" class="form-control form-control-custom" [ngClass]="{'form-error': form.get('radarrSync').hasError('required')}" id="radarrSync" name="radarrSync" formControlName="radarrSync">
<small *ngIf="form.get('radarrSync').hasError('required')" class="error-text">The Radarr Sync is required</small>
<button type="button" class="btn btn-sm btn-primary-outline" (click)="testCron(form.get('radarrSync')?.value)">Test</button>
</div>
<div class="form-group">
<label for="couchPotatoSync" class="control-label">CouchPotato Sync</label>
<input type="text" class="form-control form-control-custom" [ngClass]="{'form-error': form.get('radarrSync').hasError('required')}" id="couchPotatoSync" name="couchPotatoSync" formControlName="couchPotatoSync">
<small *ngIf="form.get('couchPotatoSync').hasError('required')" class="error-text">The CouchPotato Sync is required</small>
<button type="button" class="btn btn-sm btn-primary-outline" (click)="testCron(form.get('couchPotatoSync')?.value)">Test</button>
</div>
<div class="form-group">
<label for="automaticUpdater" class="control-label">Automatic Update</label>
<input type="text" class="form-control form-control-custom" [ngClass]="{'form-error': form.get('automaticUpdater').hasError('required')}" id="automaticUpdater" name="automaticUpdater" formControlName="automaticUpdater">
<small *ngIf="form.get('automaticUpdater').hasError('required')" class="error-text">The Automatic Update is required</small>
<button type="button" class="btn btn-sm btn-primary-outline" (click)="testCron(form.get('automaticUpdater')?.value)">Test</button>
</div>
@ -50,21 +55,45 @@
<label for="plexContentSync" class="control-label">Plex Sync</label>
<input type="text" class="form-control form-control-custom" [ngClass]="{'form-error': form.get('plexContentSync').hasError('required')}" id="plexContentSync" name="plexContentSync" formControlName="plexContentSync">
<small *ngIf="form.get('plexContentSync').hasError('required')" class="error-text">The Plex Sync is required</small>
<button type="button" class="btn btn-sm btn-primary-outline" (click)="testCron(form.get('plexContentSync')?.value)">Test</button>
</div>
<div class="form-group">
<label for="embyContentSync" class="control-label">Emby Sync</label>
<input type="text" class="form-control form-control-custom" [ngClass]="{'form-error': form.get('embyContentSync').hasError('required')}" id="embyContentSync" name="embyContentSync" formControlName="embyContentSync">
<small *ngIf="form.get('embyContentSync').hasError('required')" class="error-text">The Emby Sync is required</small>
<button type="button" class="btn btn-sm btn-primary-outline" (click)="testCron(form.get('embyContentSync')?.value)">Test</button>
</div>
<div class="form-group">
<label for="userImporter" class="control-label">User Importer</label>
<input type="text" class="form-control form-control-custom" [ngClass]="{'form-error': form.get('userImporter').hasError('required')}" id="userImporter" name="userImporter" formControlName="userImporter">
<small *ngIf="form.get('userImporter').hasError('required')" class="error-text">The User Importer is required</small>
<button type="button" class="btn btn-sm btn-primary-outline" (click)="testCron(form.get('userImporter')?.value)">Test</button>
</div>
<div class="form-group">
<label for="userImporter" class="control-label">Refresh Metadata</label>
<input type="text" class="form-control form-control-custom" [ngClass]="{'form-error': form.get('refreshMetadata').hasError('required')}" id="refreshMetadata" name="refreshMetadata" formControlName="refreshMetadata">
<small *ngIf="form.get('refreshMetadata').hasError('required')" class="error-text">The Refresh Metadata is required</small>
<button type="button" class="btn btn-sm btn-primary-outline" (click)="testCron(form.get('refreshMetadata')?.value)">Test</button>
</div>
<div class="form-group">
<label for="userImporter" class="control-label">Newsletter</label>
<input type="text" class="form-control form-control-custom" [ngClass]="{'form-error': form.get('newsletter').hasError('required')}" id="newsletter" name="newsletter" formControlName="newsletter">
<small *ngIf="form.get('newsletter').hasError('required')" class="error-text">The Newsletter is required</small>
<button type="button" class="btn btn-sm btn-primary-outline" (click)="testCron(form.get('newsletter')?.value)">Test</button>
</div>
</div>
</form>
</fieldset>
</div>
</div>
<p-dialog header="CRON Schedule" [(visible)]="displayTest">
<ul *ngIf="testModel">
<li *ngFor="let item of testModel.schedule">{{item | date:'short'}}</li>
</ul>
</p-dialog>

@ -1,7 +1,10 @@
import { Component, OnInit } from "@angular/core";
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
import { NotificationService, SettingsService } from "../../services";
import { ICronTestModel } from "./../../interfaces";
@Component({
templateUrl: "./jobs.component.html",
})
@ -10,6 +13,8 @@ export class JobsComponent implements OnInit {
public form: FormGroup;
public profilesRunning: boolean;
public testModel: ICronTestModel;
public displayTest: boolean;
constructor(private readonly settingsService: SettingsService,
private readonly fb: FormBuilder,
@ -26,7 +31,20 @@ export class JobsComponent implements OnInit {
sonarrSync: [x.radarrSync, Validators.required],
radarrSync: [x.sonarrSync, Validators.required],
sickRageSync: [x.sickRageSync, Validators.required],
});
refreshMetadata: [x.refreshMetadata, Validators.required],
newsletter: [x.newsletter, Validators.required],
});
});
}
public testCron(expression: string) {
this.settingsService.testCron({ expression }).subscribe(x => {
if(x.success) {
this.testModel = x;
this.displayTest = true;
} else {
this.notificationService.error(x.message);
}
});
}
@ -37,10 +55,10 @@ export class JobsComponent implements OnInit {
}
const settings = form.value;
this.settingsService.saveJobSettings(settings).subscribe(x => {
if (x) {
if (x.result) {
this.notificationService.success("Successfully saved the job settings");
} else {
this.notificationService.success("There was an error when saving the job settings");
this.notificationService.error("There was an error when saving the job settings. " + x.message);
}
});
}

@ -0,0 +1,48 @@
<settings-menu></settings-menu>
<wiki [url]="'https://github.com/tidusjar/Ombi/wiki/Newsletter-Settings'"></wiki>
<div *ngIf="settings">
<fieldset>
<legend>Newsletter</legend>
<div class="col-md-6">
<div class="form-group">
<div class="checkbox">
<input type="checkbox" id="enabled" [(ngModel)]="settings.enabled" ng-checked="settings.enabled"><label for="enabled">Enable</label>
</div>
</div>
<div class="form-group">
<label class="control-label">Subject</label>
<div>
<input type="text" class="form-control form-control-custom" [(ngModel)]="settings.notificationTemplate.subject" value="{{settings.notificationTemplate.subject}}">
</div>
</div>
<div class="form-group">
<label class="control-label">Message</label>
<div>
<textarea type="text" class="form-control form-control-custom" [(ngModel)]="settings.notificationTemplate.message" value="{{settings.notificationTemplate.message}}"></textarea>
</div>
</div>
<div class="form-group">
<div>
<button type="submit" id="save" (click)="onSubmit()" class="btn btn-primary-outline">Submit</button>
<button type="button" (click)="test()" class="btn btn-danger-outline">Test</button>
<button type="button" (click)="updateDatabase()" class="btn btn-info-outline" tooltipPosition="top" pTooltip="I recommend running this with a fresh Ombi install, this will set all the current *found* content to have been sent via Newsletter,
if you do not do this then everything that Ombi has found in your libraries will go out on the first email!">Update Database</button>
<button type="button" (click)="trigger()" class="btn btn-danger-outline">Trigger now</button>
</div>
</div>
</div>
<div class="col-md-6">
<small>NOTE: Please see the tooltip on the Update Database button - Please see the wiki for more information</small>
<br/>
<br/>
<small>When testing, the test newsletter will go to all users that have the Admin role, please ensure that there are valid email addresses for this. The test will also only grab the latest 10 movies and 10 shows just to give you an example.</small>
</div>
</fieldset>
</div>

@ -0,0 +1,51 @@
import { Component, OnInit } from "@angular/core";
import { INewsletterNotificationSettings, NotificationType } from "../../interfaces";
import { JobService, NotificationService, SettingsService } from "../../services";
import { TesterService } from "./../../services/applications/tester.service";
@Component({
templateUrl: "./newsletter.component.html",
})
export class NewsletterComponent implements OnInit {
public NotificationType = NotificationType;
public settings: INewsletterNotificationSettings;
constructor(private settingsService: SettingsService,
private notificationService: NotificationService,
private testService: TesterService,
private jobService: JobService) { }
public ngOnInit() {
this.settingsService.getNewsletterSettings().subscribe(x => {
this.settings = x;
});
}
public updateDatabase() {
this.settingsService.updateNewsletterDatabase().subscribe();
this.notificationService.success("Database updated");
}
public test() {
this.testService.newsletterTest(this.settings).subscribe();
this.notificationService.success("Test message queued");
}
public trigger() {
this.jobService.runNewsletter().subscribe();
this.notificationService.success("Triggered newsletter job");
}
public onSubmit() {
this.settingsService.saveNewsletterSettings(this.settings).subscribe(x => {
if (x) {
this.notificationService.success("Successfully saved the Newsletter settings");
} else {
this.notificationService.error("There was an error when saving the Newsletter settings");
}
});
}
}

@ -25,6 +25,7 @@ import { DiscordComponent } from "./notifications/discord.component";
import { EmailNotificationComponent } from "./notifications/emailnotification.component";
import { MattermostComponent } from "./notifications/mattermost.component";
import { MobileComponent } from "./notifications/mobile.component";
import { NewsletterComponent } from "./notifications/newsletter.component";
import { NotificationTemplate } from "./notifications/notificationtemplate.component";
import { PushbulletComponent } from "./notifications/pushbullet.component";
import { PushoverComponent } from "./notifications/pushover.component";
@ -41,7 +42,7 @@ import { WikiComponent } from "./wiki.component";
import { SettingsMenuComponent } from "./settingsmenu.component";
import { AutoCompleteModule, CalendarModule, InputSwitchModule, InputTextModule, MenuModule, RadioButtonModule, TooltipModule } from "primeng/primeng";
import { AutoCompleteModule, CalendarModule, DialogModule, InputSwitchModule, InputTextModule, MenuModule, RadioButtonModule, TooltipModule } from "primeng/primeng";
const routes: Routes = [
{ path: "Ombi", component: OmbiComponent, canActivate: [AuthGuard] },
@ -69,6 +70,7 @@ const routes: Routes = [
{ path: "Authentication", component: AuthenticationComponent, canActivate: [AuthGuard] },
{ path: "Mobile", component: MobileComponent, canActivate: [AuthGuard] },
{ path: "MassEmail", component: MassEmailComponent, canActivate: [AuthGuard] },
{ path: "Newsletter", component: NewsletterComponent, canActivate: [AuthGuard] },
];
@NgModule({
@ -88,6 +90,7 @@ const routes: Routes = [
ClipboardModule,
PipeModule,
RadioButtonModule,
DialogModule,
],
declarations: [
SettingsMenuComponent,
@ -118,6 +121,7 @@ const routes: Routes = [
AuthenticationComponent,
MobileComponent,
MassEmailComponent,
NewsletterComponent,
],
exports: [
RouterModule,

@ -56,7 +56,7 @@
<ul class="dropdown-menu">
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Email']">Email</a></li>
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/MassEmail']">Mass Email</a></li>
<!--<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Newsletter']">Newsletter</a></li>-->
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Newsletter']">Newsletter</a></li>
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Discord']">Discord</a></li>
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Slack']">Slack</a></li>
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Pushbullet']">Pushbullet</a></li>

@ -15,6 +15,8 @@ export class IssuesReportComponent {
@Input() public issueCategory: IIssueCategory;
@Input() public movie: boolean;
@Input() public providerId: string;
@Input() public background: string;
@Input() public posterPath: string;
@Output() public visibleChange = new EventEmitter<boolean>();

@ -1 +1,2 @@
@import './styles.scss';
@import './styles.scss';
@import './scrollbar.scss';

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

Loading…
Cancel
Save