pull/2121/head v3.0.3111
Jamie 7 years ago
commit a003095540

@ -1,5 +1,83 @@
# Changelog # Changelog
## v3.0.3020 (2018-03-13)
### **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) ## v3.0.3020 (2018-03-13)
### **Fixes** ### **Fixes**

@ -12,9 +12,6 @@ ____
___ ___
[![Report a bug](http://i.imgur.com/xSpw482.png)](https://forums.ombi.io/viewforum.php?f=10) [![Feature request](http://i.imgur.com/mFO0OuX.png)](https://forums.ombi.io/posting.php?mode=post&f=20)
| Service | Stable | Develop | | Service | Stable | Develop |
|----------|:---------------------------:|:----------------------------:| |----------|:---------------------------:|:----------------------------:|
| AppVeyor | [![Build status](https://ci.appveyor.com/api/projects/status/hgj8j6lcea7j0yhn/branch/master?svg=true)](https://ci.appveyor.com/project/tidusjar/requestplex/branch/master) | [![Build status](https://ci.appveyor.com/api/projects/status/hgj8j6lcea7j0yhn/branch/develop?svg=true)](https://ci.appveyor.com/project/tidusjar/requestplex/branch/develop) | | AppVeyor | [![Build status](https://ci.appveyor.com/api/projects/status/hgj8j6lcea7j0yhn/branch/master?svg=true)](https://ci.appveyor.com/project/tidusjar/requestplex/branch/master) | [![Build status](https://ci.appveyor.com/api/projects/status/hgj8j6lcea7j0yhn/branch/develop?svg=true)](https://ci.appveyor.com/project/tidusjar/requestplex/branch/develop) |

@ -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); request.AddHeader("api-key", token);
return await Api.Request<MovieResult>(request); return await Api.Request<MovieResult>(request);

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

@ -5,6 +5,6 @@ namespace Ombi.Api.Mattermost
{ {
public interface IMattermostApi 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; 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); var client = new MatterhookClient(webhook);
await client.PostAsync(_api, message);
request.AddJsonBody(message);
var result = await _api.RequestContent(request);
return result;
} }
} }
} }

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

@ -9,7 +9,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.0.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

@ -11,7 +11,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Options" Version="2.0.0" /> <PackageReference Include="Microsoft.Extensions.Options" Version="2.0.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

@ -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;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -6,6 +9,7 @@ using System.Xml.Serialization;
using Newtonsoft.Json; using Newtonsoft.Json;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Ombi.Helpers; using Ombi.Helpers;
using Polly;
namespace Ombi.Api namespace Ombi.Api
{ {
@ -36,6 +40,30 @@ namespace Ombi.Api
if (!httpResponseMessage.IsSuccessStatusCode) if (!httpResponseMessage.IsSuccessStatusCode)
{ {
LogError(request, httpResponseMessage); 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 // 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;
}
}
}

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

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Text; using System.Text;
@ -25,6 +26,9 @@ namespace Ombi.Api
public string BaseUrl { get; } public string BaseUrl { get; }
public HttpMethod HttpMethod { 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; } public Action<string> OnBeforeDeserialization { get; set; }
private string FullUrl private string FullUrl

@ -9,7 +9,7 @@
<PackageReference Include="Nunit" Version="3.8.1" /> <PackageReference Include="Nunit" Version="3.8.1" />
<PackageReference Include="NUnit.ConsoleRunner" Version="3.7.0" /> <PackageReference Include="NUnit.ConsoleRunner" Version="3.7.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.8.0" /> <PackageReference Include="NUnit3TestAdapter" Version="3.8.0" />
<packagereference Include="Microsoft.NET.Test.Sdk" Version="15.0.0"></packagereference> <packagereference Include="Microsoft.NET.Test.Sdk" Version="15.6.1"></packagereference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

@ -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, Result = false,
Message = "There was an issue adding this movie!", 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 = 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; }
}
}

@ -12,9 +12,9 @@
<PackageReference Include="AutoMapper" Version="6.1.1" /> <PackageReference Include="AutoMapper" Version="6.1.1" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="3.0.1" /> <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="3.0.1" />
<PackageReference Include="Hangfire" Version="1.6.17" /> <PackageReference Include="Hangfire" Version="1.6.17" />
<PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="2.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="2.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Design" Version="1.1.3" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Design" Version="1.1.5" />
<PackageReference Include="MiniProfiler.AspNetCore" Version="4.0.0-alpha6-79" /> <PackageReference Include="MiniProfiler.AspNetCore" Version="4.0.0-alpha6-79" />
<PackageReference Include="Newtonsoft.Json" Version="10.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="10.0.3" />
<PackageReference Include="System.Diagnostics.Process" Version="4.3.0" /> <PackageReference Include="System.Diagnostics.Process" Version="4.3.0" />

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

@ -9,8 +9,8 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="2.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="2.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="2.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="2.0.3" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="2.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="2.0.0" />
</ItemGroup> </ItemGroup>

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

@ -10,8 +10,8 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="EasyCrypto" Version="3.3.2" /> <PackageReference Include="EasyCrypto" Version="3.3.2" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="2.0.0" /> <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="2.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="10.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="10.0.3" />
<PackageReference Include="Nito.AsyncEx" Version="5.0.0-pre-05" /> <PackageReference Include="Nito.AsyncEx" Version="5.0.0-pre-05" />
<PackageReference Include="System.Security.Claims" Version="4.3.0" /> <PackageReference Include="System.Security.Claims" Version="4.3.0" />

@ -9,5 +9,6 @@
public const string RequestTv = nameof(RequestTv); public const string RequestTv = nameof(RequestTv);
public const string RequestMovie = nameof(RequestMovie); public const string RequestMovie = nameof(RequestMovie);
public const string Disabled = nameof(Disabled); 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<TelegramNotificationsViewModel, TelegramSettings>().ReverseMap();
CreateMap<UpdateSettingsViewModel, UpdateSettings>().ReverseMap(); CreateMap<UpdateSettingsViewModel, UpdateSettings>().ReverseMap();
CreateMap<MobileNotificationsViewModel, MobileNotificationSettings>().ReverseMap(); CreateMap<MobileNotificationsViewModel, MobileNotificationSettings>().ReverseMap();
CreateMap<NewsletterNotificationViewModel, NewsletterSettings>().ReverseMap();
} }
} }
} }

@ -4,19 +4,26 @@ using System.Text;
namespace Ombi.Notifications.Templates namespace Ombi.Notifications.Templates
{ {
public class EmailBasicTemplate : IEmailBasicTemplate public class EmailBasicTemplate : TemplateBase, IEmailBasicTemplate
{ {
public string TemplateLocation public override string TemplateLocation
{ {
get get
{ {
if (string.IsNullOrEmpty(_templateLocation))
{
#if DEBUG #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 #else
return Path.Combine(Directory.GetCurrentDirectory(), "Templates","BasicTemplate.html"); _templateLocation = Path.Combine(Directory.GetCurrentDirectory(), "Templates","BasicTemplate.html");
#endif #endif
}
return _templateLocation;
} }
} }
private string _templateLocation;
private const string SubjectKey = "{@SUBJECT}"; private const string SubjectKey = "{@SUBJECT}";
private const string BodyKey = "{@BODY}"; private const string BodyKey = "{@BODY}";
@ -31,7 +38,7 @@ namespace Ombi.Notifications.Templates
sb.Replace(BodyKey, body); sb.Replace(BodyKey, body);
sb.Replace(DateKey, DateTime.Now.ToString("f")); 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(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(); 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> </PropertyGroup>
<ItemGroup> <ItemGroup>
<None Update="Templates\NewsletterTemplate.html">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Templates\BasicTemplate.html"> <None Update="Templates\BasicTemplate.html">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </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>

@ -8,7 +8,7 @@
<PackageReference Include="Nunit" Version="3.8.1" /> <PackageReference Include="Nunit" Version="3.8.1" />
<PackageReference Include="NUnit.ConsoleRunner" Version="3.7.0" /> <PackageReference Include="NUnit.ConsoleRunner" Version="3.7.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.8.0" /> <PackageReference Include="NUnit3TestAdapter" Version="3.8.0" />
<packagereference Include="Microsoft.NET.Test.Sdk" Version="15.0.0"></packagereference> <packagereference Include="Microsoft.NET.Test.Sdk" Version="15.6.1"></packagereference>
<PackageReference Include="Moq" Version="4.7.99" /> <PackageReference Include="Moq" Version="4.7.99" />
</ItemGroup> </ItemGroup>

@ -59,10 +59,18 @@ namespace Ombi.Notifications.Agents
Message = parsed.Message, 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); 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) protected override async Task NewIssue(NotificationOptions model, MattermostNotificationSettings settings)
{ {
var parsed = await LoadTemplate(NotificationAgent.Mattermost, NotificationType.Issue, model); var parsed = await LoadTemplate(NotificationAgent.Mattermost, NotificationType.Issue, model);
@ -75,7 +83,7 @@ namespace Ombi.Notifications.Agents
{ {
Message = parsed.Message, Message = parsed.Message,
}; };
notification.Other.Add("image", parsed.Image); AddOtherInformation(model, notification, parsed);
await Send(notification, settings); await Send(notification, settings);
} }
@ -91,7 +99,7 @@ namespace Ombi.Notifications.Agents
{ {
Message = parsed.Message, Message = parsed.Message,
}; };
notification.Other.Add("image", parsed.Image); AddOtherInformation(model, notification, parsed);
await Send(notification, settings); await Send(notification, settings);
} }
@ -107,7 +115,7 @@ namespace Ombi.Notifications.Agents
{ {
Message = parsed.Message, Message = parsed.Message,
}; };
notification.Other.Add("image", parsed.Image); AddOtherInformation(model, notification, parsed);
await Send(notification, settings); await Send(notification, settings);
} }
@ -149,7 +157,7 @@ namespace Ombi.Notifications.Agents
{ {
Message = parsed.Message, Message = parsed.Message,
}; };
notification.Other.Add("image", parsed.Image); AddOtherInformation(model, notification, parsed);
await Send(notification, settings); await Send(notification, settings);
} }
@ -166,7 +174,7 @@ namespace Ombi.Notifications.Agents
Message = parsed.Message, Message = parsed.Message,
}; };
notification.Other.Add("image", parsed.Image); AddOtherInformation(model, notification, parsed);
await Send(notification, settings); await Send(notification, settings);
} }
@ -182,7 +190,7 @@ namespace Ombi.Notifications.Agents
{ {
Message = parsed.Message, Message = parsed.Message,
}; };
notification.Other.Add("image", parsed.Image); AddOtherInformation(model, notification, parsed);
await Send(notification, settings); await Send(notification, settings);
} }
@ -190,12 +198,20 @@ namespace Ombi.Notifications.Agents
{ {
try try
{ {
var body = new MattermostBody var body = new MattermostMessage
{ {
username = string.IsNullOrEmpty(settings.Username) ? "Ombi" : settings.Username, Username = string.IsNullOrEmpty(settings.Username) ? "Ombi" : settings.Username,
channel = settings.Channel, Channel = settings.Channel,
text = model.Message, Text = model.Message,
icon_url = settings.IconUrl 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); await Api.PushAsync(settings.WebhookUrl, body);
} }

@ -107,9 +107,13 @@ namespace Ombi.Notifications
var body = new BodyBuilder var body = new BodyBuilder
{ {
HtmlBody = model.Message, HtmlBody = model.Message,
TextBody = model.Other["PlainTextBody"]
}; };
if (model.Other.ContainsKey("PlainTextBody"))
{
body.TextBody = model.Other["PlainTextBody"];
}
var message = new MimeMessage var message = new MimeMessage
{ {
Body = body.ToMessageBody(), Body = body.ToMessageBody(),

@ -1,10 +1,13 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Text;
using Ombi.Helpers; using Ombi.Helpers;
using Ombi.Notifications.Models; using Ombi.Notifications.Models;
using Ombi.Settings.Settings.Models; using Ombi.Settings.Settings.Models;
using Ombi.Store.Entities; using Ombi.Store.Entities;
using Ombi.Store.Entities.Requests; using Ombi.Store.Entities.Requests;
using Ombi.Store.Repository.Requests;
namespace Ombi.Notifications namespace Ombi.Notifications
{ {
@ -25,9 +28,9 @@ namespace Ombi.Notifications
} }
ApplicationUrl = (s?.ApplicationUrl.HasValue() ?? false) ? s.ApplicationUrl : string.Empty; ApplicationUrl = (s?.ApplicationUrl.HasValue() ?? false) ? s.ApplicationUrl : string.Empty;
ApplicationName = string.IsNullOrEmpty(s?.ApplicationName) ? "Ombi" : s?.ApplicationName; ApplicationName = string.IsNullOrEmpty(s?.ApplicationName) ? "Ombi" : s?.ApplicationName;
RequestedUser = string.IsNullOrEmpty(req?.RequestedUser?.Alias) RequestedUser = req?.RequestedUser?.UserName;
? req?.RequestedUser?.UserName UserName = req?.RequestedUser?.UserName;
: req?.RequestedUser?.Alias; Alias = (req?.RequestedUser?.Alias.HasValue() ?? false) ? req?.RequestedUser?.Alias : req?.RequestedUser?.UserName;
Title = title; Title = title;
RequestedDate = req?.RequestedDate.ToString("D"); RequestedDate = req?.RequestedDate.ToString("D");
Type = req?.RequestType.ToString(); Type = req?.RequestType.ToString();
@ -38,6 +41,15 @@ namespace Ombi.Notifications
AdditionalInformation = opts?.AdditionalInformation ?? string.Empty; 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) public void Setup(NotificationOptions opts, ChildRequests req, CustomizationSettings s)
{ {
LoadIssues(opts); LoadIssues(opts);
@ -52,9 +64,9 @@ namespace Ombi.Notifications
} }
ApplicationUrl = (s?.ApplicationUrl.HasValue() ?? false) ? s.ApplicationUrl : string.Empty; ApplicationUrl = (s?.ApplicationUrl.HasValue() ?? false) ? s.ApplicationUrl : string.Empty;
ApplicationName = string.IsNullOrEmpty(s?.ApplicationName) ? "Ombi" : s?.ApplicationName; ApplicationName = string.IsNullOrEmpty(s?.ApplicationName) ? "Ombi" : s?.ApplicationName;
RequestedUser = string.IsNullOrEmpty(req?.RequestedUser.Alias) RequestedUser = req?.RequestedUser?.UserName;
? req?.RequestedUser.UserName UserName = req?.RequestedUser?.UserName;
: req?.RequestedUser.Alias; Alias = (req?.RequestedUser?.Alias.HasValue() ?? false) ? req?.RequestedUser?.Alias : req?.RequestedUser?.UserName;
Title = title; Title = title;
RequestedDate = req?.RequestedDate.ToString("D"); RequestedDate = req?.RequestedDate.ToString("D");
Type = req?.RequestType.ToString(); Type = req?.RequestType.ToString();
@ -64,6 +76,40 @@ namespace Ombi.Notifications
$"https://image.tmdb.org/t/p/w300{req?.ParentRequest.PosterPath}" : req?.ParentRequest.PosterPath; $"https://image.tmdb.org/t/p/w300{req?.ParentRequest.PosterPath}" : req?.ParentRequest.PosterPath;
AdditionalInformation = opts.AdditionalInformation; AdditionalInformation = opts.AdditionalInformation;
// DO Episode and Season Lists // 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) public void Setup(OmbiUser user, CustomizationSettings s)
@ -81,13 +127,14 @@ namespace Ombi.Notifications
IssueStatus = opts.Substitutes.TryGetValue("IssueStatus", out val) ? val : string.Empty; IssueStatus = opts.Substitutes.TryGetValue("IssueStatus", out val) ? val : string.Empty;
IssueSubject = opts.Substitutes.TryGetValue("IssueSubject", out val) ? val : string.Empty; IssueSubject = opts.Substitutes.TryGetValue("IssueSubject", out val) ? val : string.Empty;
NewIssueComment = opts.Substitutes.TryGetValue("NewIssueComment", 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 // User Defined
public string RequestedUser { get; set; } public string RequestedUser { get; set; }
public string UserName => RequestedUser; public string UserName { get; set; }
public string IssueUser => RequestedUser; public string IssueUser => UserName;
public string Alias { get; set; }
public string Title { get; set; } public string Title { get; set; }
public string RequestedDate { get; set; } public string RequestedDate { get; set; }
@ -137,6 +184,7 @@ namespace Ombi.Notifications
{nameof(NewIssueComment),NewIssueComment}, {nameof(NewIssueComment),NewIssueComment},
{nameof(IssueUser),IssueUser}, {nameof(IssueUser),IssueUser},
{nameof(UserName),UserName}, {nameof(UserName),UserName},
{nameof(Alias),Alias},
}; };
} }
} }

@ -6,12 +6,12 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.0.0" /> <PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore" Version="2.0.0" /> <PackageReference Include="Microsoft.AspNetCore" Version="2.0.2" />
<PackageReference Include="Moq" Version="4.7.99" /> <PackageReference Include="Moq" Version="4.7.99" />
<PackageReference Include="Nunit" Version="3.8.1" /> <PackageReference Include="Nunit" Version="3.8.1" />
<PackageReference Include="NUnit.ConsoleRunner" Version="3.7.0" /> <PackageReference Include="NUnit.ConsoleRunner" Version="3.7.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.8.0" /> <PackageReference Include="NUnit3TestAdapter" Version="3.8.0" />
<packagereference Include="Microsoft.NET.Test.Sdk" Version="15.0.0"></packagereference> <packagereference Include="Microsoft.NET.Test.Sdk" Version="15.6.1"></packagereference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

@ -17,46 +17,53 @@ namespace Ombi.Schedule
public JobSetup(IPlexContentSync plexContentSync, IRadarrSync radarrSync, public JobSetup(IPlexContentSync plexContentSync, IRadarrSync radarrSync,
IOmbiAutomaticUpdater updater, IEmbyContentSync embySync, IPlexUserImporter userImporter, IOmbiAutomaticUpdater updater, IEmbyContentSync embySync, IPlexUserImporter userImporter,
IEmbyUserImporter embyUserImporter, ISonarrSync cache, ICouchPotatoSync cpCache, IEmbyUserImporter embyUserImporter, ISonarrSync cache, ICouchPotatoSync cpCache,
ISettingsService<JobSettings> jobsettings, ISickRageSync srSync) ISettingsService<JobSettings> jobsettings, ISickRageSync srSync, IRefreshMetadata refresh,
INewsletterJob newsletter)
{ {
PlexContentSync = plexContentSync; _plexContentSync = plexContentSync;
RadarrSync = radarrSync; _radarrSync = radarrSync;
Updater = updater; _updater = updater;
EmbyContentSync = embySync; _embyContentSync = embySync;
PlexUserImporter = userImporter; _plexUserImporter = userImporter;
EmbyUserImporter = embyUserImporter; _embyUserImporter = embyUserImporter;
SonarrSync = cache; _sonarrSync = cache;
CpCache = cpCache; _cpCache = cpCache;
JobSettings = jobsettings; _jobSettings = jobsettings;
SrSync = srSync; _srSync = srSync;
_refreshMetadata = refresh;
_newsletter = newsletter;
} }
private IPlexContentSync PlexContentSync { get; } private readonly IPlexContentSync _plexContentSync;
private IRadarrSync RadarrSync { get; } private readonly IRadarrSync _radarrSync;
private IOmbiAutomaticUpdater Updater { get; } private readonly IOmbiAutomaticUpdater _updater;
private IPlexUserImporter PlexUserImporter { get; } private readonly IPlexUserImporter _plexUserImporter;
private IEmbyContentSync EmbyContentSync { get; } private readonly IEmbyContentSync _embyContentSync;
private IEmbyUserImporter EmbyUserImporter { get; } private readonly IEmbyUserImporter _embyUserImporter;
private ISonarrSync SonarrSync { get; } private readonly ISonarrSync _sonarrSync;
private ICouchPotatoSync CpCache { get; } private readonly ICouchPotatoSync _cpCache;
private ISickRageSync SrSync { get; } private readonly ISickRageSync _srSync;
private ISettingsService<JobSettings> JobSettings { get; set; } private readonly ISettingsService<JobSettings> _jobSettings;
private readonly IRefreshMetadata _refreshMetadata;
private readonly INewsletterJob _newsletter;
public void Setup() public void Setup()
{ {
var s = JobSettings.GetSettings(); var s = _jobSettings.GetSettings();
RecurringJob.AddOrUpdate(() => EmbyContentSync.Start(), JobSettingsHelper.EmbyContent(s)); RecurringJob.AddOrUpdate(() => _embyContentSync.Start(), JobSettingsHelper.EmbyContent(s));
RecurringJob.AddOrUpdate(() => SonarrSync.Start(), JobSettingsHelper.Sonarr(s)); RecurringJob.AddOrUpdate(() => _sonarrSync.Start(), JobSettingsHelper.Sonarr(s));
RecurringJob.AddOrUpdate(() => RadarrSync.CacheContent(), JobSettingsHelper.Radarr(s)); RecurringJob.AddOrUpdate(() => _radarrSync.CacheContent(), JobSettingsHelper.Radarr(s));
RecurringJob.AddOrUpdate(() => PlexContentSync.CacheContent(), JobSettingsHelper.PlexContent(s)); RecurringJob.AddOrUpdate(() => _plexContentSync.CacheContent(), JobSettingsHelper.PlexContent(s));
RecurringJob.AddOrUpdate(() => CpCache.Start(), JobSettingsHelper.CouchPotato(s)); RecurringJob.AddOrUpdate(() => _cpCache.Start(), JobSettingsHelper.CouchPotato(s));
RecurringJob.AddOrUpdate(() => SrSync.Start(), JobSettingsHelper.SickRageSync(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(() => _embyUserImporter.Start(), JobSettingsHelper.UserImporter(s));
RecurringJob.AddOrUpdate(() => PlexUserImporter.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.Service\Ombi.Api.Service.csproj" />
<ProjectReference Include="..\Ombi.Api.SickRage\Ombi.Api.SickRage.csproj" /> <ProjectReference Include="..\Ombi.Api.SickRage\Ombi.Api.SickRage.csproj" />
<ProjectReference Include="..\Ombi.Api.Sonarr\Ombi.Api.Sonarr.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.Notifications\Ombi.Notifications.csproj" />
<ProjectReference Include="..\Ombi.Settings\Ombi.Settings.csproj" /> <ProjectReference Include="..\Ombi.Settings\Ombi.Settings.csproj" />
<ProjectReference Include="..\Ombi.TheMovieDbApi\Ombi.Api.TheMovieDb.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

@ -46,7 +46,8 @@ namespace Ombi.Schedule.Processor
if (masterBranch) if (masterBranch)
{ {
latestRelease = doc.DocumentNode.Descendants("h2") 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 else
{ {
@ -78,9 +79,9 @@ namespace Ombi.Schedule.Processor
Downloads = new List<Downloads>() Downloads = new List<Downloads>()
}; };
var releaseTag = latestRelease.InnerText.Substring(0, 6);
if (masterBranch) if (masterBranch)
{ {
var releaseTag = latestRelease.InnerText.Substring(0, 9);
await GetGitubRelease(release, releaseTag); await GetGitubRelease(release, releaseTag);
} }
else else
@ -147,7 +148,7 @@ namespace Ombi.Schedule.Processor
var builds = await _api.Request<AppveyorBranchResult>(request); var builds = await _api.Request<AppveyorBranchResult>(request);
var jobId = builds.build.jobs.FirstOrDefault()?.jobId ?? string.Empty; 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; return;
} }

@ -9,6 +9,7 @@ namespace Ombi.Settings.Settings.Models
{ {
public string ApplicationName { get; set; } public string ApplicationName { get; set; }
public string ApplicationUrl { get; set; } public string ApplicationUrl { get; set; }
public bool Mobile { get; set; }
public string CustomCssLink { get; set; } public string CustomCssLink { get; set; }
public bool EnableCustomDonations { get; set; } public bool EnableCustomDonations { get; set; }
public string CustomDonationUrl { get; set; } public string CustomDonationUrl { get; set; }
@ -17,6 +18,7 @@ namespace Ombi.Settings.Settings.Models
public string PresetThemeName { get; set; } public string PresetThemeName { get; set; }
public string PresetThemeContent { get; set; } public string PresetThemeContent { get; set; }
public bool RecentlyAddedPage { get; set; }
[NotMapped] [NotMapped]
public string PresetThemeVersion public string PresetThemeVersion

@ -10,5 +10,7 @@
public string AutomaticUpdater { get; set; } public string AutomaticUpdater { get; set; }
public string UserImporter { get; set; } public string UserImporter { get; set; }
public string SickRageSync { 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 namespace Ombi.Settings.Settings.Models
{ {
@ -35,10 +36,18 @@ namespace Ombi.Settings.Settings.Models
{ {
return Get(s.UserImporter, Cron.Daily()); 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) public static string SickRageSync(JobSettings s)
{ {
return Get(s.SickRageSync, Cron.Hourly(35)); 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) 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<SickRageCache> SickRageCache { get; set; }
DbSet<SickRageEpisodeCache> SickRageEpisodeCache { get; set; } DbSet<SickRageEpisodeCache> SickRageEpisodeCache { get; set; }
DbSet<RequestLog> RequestLogs { get; set; } DbSet<RequestLog> RequestLogs { get; set; }
DbSet<RecentlyAddedLog> RecentlyAddedLogs { get; set; }
} }
} }

@ -1,6 +1,7 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Ombi.Helpers; using Ombi.Helpers;
@ -15,7 +16,7 @@ namespace Ombi.Store.Context
public OmbiContext() public OmbiContext()
{ {
if (_created) return; if (_created) return;
_created = true; _created = true;
Database.Migrate(); Database.Migrate();
} }
@ -37,6 +38,7 @@ namespace Ombi.Store.Context
public DbSet<IssueCategory> IssueCategories { get; set; } public DbSet<IssueCategory> IssueCategories { get; set; }
public DbSet<IssueComments> IssueComments { get; set; } public DbSet<IssueComments> IssueComments { get; set; }
public DbSet<RequestLog> RequestLogs { get; set; } public DbSet<RequestLog> RequestLogs { get; set; }
public DbSet<RecentlyAddedLog> RecentlyAddedLogs { get; set; }
public DbSet<Audit> Audit { get; set; } public DbSet<Audit> Audit { get; set; }
@ -55,7 +57,7 @@ namespace Ombi.Store.Context
{ {
i.StoragePath = string.Empty; 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) protected override void OnModelCreating(ModelBuilder builder)
@ -70,7 +72,7 @@ namespace Ombi.Store.Context
.WithMany(b => b.Episodes) .WithMany(b => b.Episodes)
.HasPrincipalKey(x => x.EmbyId) .HasPrincipalKey(x => x.EmbyId)
.HasForeignKey(p => p.ParentId); .HasForeignKey(p => p.ParentId);
base.OnModelCreating(builder); base.OnModelCreating(builder);
} }
@ -113,6 +115,15 @@ namespace Ombi.Store.Context
// VACUUM; // VACUUM;
Database.ExecuteSqlCommand("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 //Check if templates exist
var templates = NotificationTemplates.ToList(); var templates = NotificationTemplates.ToList();
@ -135,7 +146,7 @@ namespace Ombi.Store.Context
notificationToAdd = new NotificationTemplates notificationToAdd = new NotificationTemplates
{ {
NotificationType = notificationType, 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}!", Subject = "{ApplicationName}: New {Type} request for {Title}!",
Agent = agent, Agent = agent,
Enabled = true, Enabled = true,
@ -145,7 +156,7 @@ namespace Ombi.Store.Context
notificationToAdd = new NotificationTemplates notificationToAdd = new NotificationTemplates
{ {
NotificationType = notificationType, 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}!", Subject = "{ApplicationName}: New issue for {Title}!",
Agent = agent, Agent = agent,
Enabled = true, Enabled = true,
@ -155,7 +166,7 @@ namespace Ombi.Store.Context
notificationToAdd = new NotificationTemplates notificationToAdd = new NotificationTemplates
{ {
NotificationType = notificationType, 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!", Subject = "{ApplicationName}: {Title} is now available!",
Agent = agent, Agent = agent,
Enabled = true, Enabled = true,
@ -199,7 +210,7 @@ namespace Ombi.Store.Context
notificationToAdd = new NotificationTemplates notificationToAdd = new NotificationTemplates
{ {
NotificationType = notificationType, 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}!", Subject = "{ApplicationName}: Issue has been resolved for {Title}!",
Agent = agent, Agent = agent,
Enabled = true, Enabled = true,
@ -218,6 +229,16 @@ namespace Ombi.Store.Context
break; break;
case NotificationType.AdminNote: case NotificationType.AdminNote:
continue; 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: default:
throw new ArgumentOutOfRangeException(); throw new ArgumentOutOfRangeException();
} }

@ -56,6 +56,15 @@ namespace Ombi.Store.Entities
public int Key { get; set; } public int Key { get; set; }
public DateTime AddedAt { get; set; } public DateTime AddedAt { get; set; }
public string Quality { 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")] [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 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "2.0.0-rtm-26452"); .HasAnnotation("ProductVersion", "2.0.2-rtm-10011");
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{ {
@ -430,6 +430,24 @@ namespace Ombi.Store.Migrations
b.ToTable("RadarrCache"); 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 => modelBuilder.Entity("Ombi.Store.Entities.Requests.ChildRequests", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")

@ -10,10 +10,10 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="2.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="2.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="2.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="2.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="10.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="10.0.3" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="1.1.9" /> <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="1.1.9" />
</ItemGroup> </ItemGroup>

@ -45,9 +45,9 @@ namespace Ombi.Store.Repository
private IOmbiContext Db { get; } 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) public async Task AddRange(IEnumerable<EmbyContent> content)

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

@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Ombi.Helpers; using Ombi.Helpers;
@ -6,11 +7,11 @@ using Ombi.Store.Entities;
namespace Ombi.Store.Repository namespace Ombi.Store.Repository
{ {
public interface INotificationTemplatesRepository public interface INotificationTemplatesRepository : IDisposable
{ {
IQueryable<NotificationTemplates> All(); IQueryable<NotificationTemplates> All();
Task<IEnumerable<NotificationTemplates>> GetAllTemplates(); IQueryable<NotificationTemplates> GetAllTemplates();
Task<IEnumerable<NotificationTemplates>> GetAllTemplates(NotificationAgent agent); IQueryable<NotificationTemplates> GetAllTemplates(NotificationAgent agent);
Task<NotificationTemplates> Insert(NotificationTemplates entity); Task<NotificationTemplates> Insert(NotificationTemplates entity);
Task Update(NotificationTemplates template); Task Update(NotificationTemplates template);
Task UpdateRange(IEnumerable<NotificationTemplates> template); Task UpdateRange(IEnumerable<NotificationTemplates> template);

@ -22,5 +22,7 @@ namespace Ombi.Store.Repository
Task DeleteEpisode(PlexEpisode content); Task DeleteEpisode(PlexEpisode content);
void DeleteWithoutSave(PlexServerContent content); void DeleteWithoutSave(PlexServerContent content);
void DeleteWithoutSave(PlexEpisode 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;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Query;
using Ombi.Store.Entities; using Ombi.Store.Entities;
@ -24,5 +25,6 @@ namespace Ombi.Store.Repository
where TEntity : class; where TEntity : class;
Task ExecuteSql(string sql); Task ExecuteSql(string sql);
DbSet<T> _db { get; }
} }
} }

@ -23,14 +23,14 @@ namespace Ombi.Store.Repository
return Db.NotificationTemplates.AsQueryable(); 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) public async Task<NotificationTemplates> GetTemplate(NotificationAgent agent, NotificationType type)
@ -40,6 +40,11 @@ namespace Ombi.Store.Repository
public async Task Update(NotificationTemplates template) 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(); await Db.SaveChangesAsync();
} }
@ -60,5 +65,26 @@ namespace Ombi.Store.Repository
await Db.SaveChangesAsync().ConfigureAwait(false); await Db.SaveChangesAsync().ConfigureAwait(false);
return settings.Entity; 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); Db.PlexServerContent.Update(existingContent);
await Db.SaveChangesAsync(); 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() public IQueryable<PlexEpisode> GetAllEpisodes()
{ {

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

@ -7,13 +7,12 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="2.0.0" /> <PackageReference Include="Microsoft.AspNetCore.TestHost" Version="2.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="2.0.0" />
<PackageReference Include="Moq" Version="4.7.99" /> <PackageReference Include="Moq" Version="4.7.99" />
<PackageReference Include="Nunit" Version="3.8.1" /> <PackageReference Include="Nunit" Version="3.8.1" />
<PackageReference Include="NUnit.ConsoleRunner" Version="3.7.0" /> <PackageReference Include="NUnit.ConsoleRunner" Version="3.7.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.8.0" /> <PackageReference Include="NUnit3TestAdapter" Version="3.8.0" />
<packagereference Include="Microsoft.NET.Test.Sdk" Version="15.0.0"></packagereference> <packagereference Include="Microsoft.NET.Test.Sdk" Version="15.6.1"></packagereference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

@ -15,5 +15,7 @@ namespace Ombi.Api.TheMovieDb
Task<List<MovieSearchResult>> TopRated(); Task<List<MovieSearchResult>> TopRated();
Task<List<MovieSearchResult>> Upcoming(); Task<List<MovieSearchResult>> Upcoming();
Task<List<MovieSearchResult>> SimilarMovies(int movieId); 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.Collections.Generic;
using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using AutoMapper; using AutoMapper;
@ -25,15 +26,37 @@ namespace Ombi.Api.TheMovieDb
{ {
var request = new Request($"movie/{movieId}", BaseUri, HttpMethod.Get); var request = new Request($"movie/{movieId}", BaseUri, HttpMethod.Get);
request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken);
AddRetry(request);
var result = await Api.Request<MovieResponse>(request); var result = await Api.Request<MovieResponse>(request);
return Mapper.Map<MovieResponseDto>(result); 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) public async Task<List<MovieSearchResult>> SimilarMovies(int movieId)
{ {
var request = new Request($"movie/{movieId}/similar", BaseUri, HttpMethod.Get); var request = new Request($"movie/{movieId}/similar", BaseUri, HttpMethod.Get);
request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken);
AddRetry(request);
var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request); var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request);
return Mapper.Map<List<MovieSearchResult>>(result.results); return Mapper.Map<List<MovieSearchResult>>(result.results);
@ -44,6 +67,7 @@ namespace Ombi.Api.TheMovieDb
var request = new Request($"movie/{movieId}", BaseUri, HttpMethod.Get); var request = new Request($"movie/{movieId}", BaseUri, HttpMethod.Get);
request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken);
request.FullUri = request.FullUri.AddQueryParameter("append_to_response", "videos,release_dates"); request.FullUri = request.FullUri.AddQueryParameter("append_to_response", "videos,release_dates");
AddRetry(request);
var result = await Api.Request<MovieResponse>(request); var result = await Api.Request<MovieResponse>(request);
return Mapper.Map<MovieResponseDto>(result); return Mapper.Map<MovieResponseDto>(result);
} }
@ -53,6 +77,7 @@ namespace Ombi.Api.TheMovieDb
var request = new Request($"search/movie", BaseUri, HttpMethod.Get); var request = new Request($"search/movie", BaseUri, HttpMethod.Get);
request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken);
request.FullUri = request.FullUri.AddQueryParameter("query", searchTerm); request.FullUri = request.FullUri.AddQueryParameter("query", searchTerm);
AddRetry(request);
var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request); var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request);
return Mapper.Map<List<MovieSearchResult>>(result.results); return Mapper.Map<List<MovieSearchResult>>(result.results);
@ -62,6 +87,7 @@ namespace Ombi.Api.TheMovieDb
{ {
var request = new Request($"movie/popular", BaseUri, HttpMethod.Get); var request = new Request($"movie/popular", BaseUri, HttpMethod.Get);
request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken);
AddRetry(request);
var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request); var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request);
return Mapper.Map<List<MovieSearchResult>>(result.results); 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); var request = new Request($"movie/top_rated", BaseUri, HttpMethod.Get);
request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken);
AddRetry(request);
var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request); var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request);
return Mapper.Map<List<MovieSearchResult>>(result.results); return Mapper.Map<List<MovieSearchResult>>(result.results);
} }
@ -78,6 +105,7 @@ namespace Ombi.Api.TheMovieDb
{ {
var request = new Request($"movie/upcoming", BaseUri, HttpMethod.Get); var request = new Request($"movie/upcoming", BaseUri, HttpMethod.Get);
request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken);
AddRetry(request);
var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request); var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request);
return Mapper.Map<List<MovieSearchResult>>(result.results); 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); var request = new Request($"movie/now_playing", BaseUri, HttpMethod.Get);
request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken);
AddRetry(request);
var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request); var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request);
return Mapper.Map<List<MovieSearchResult>>(result.results); return Mapper.Map<List<MovieSearchResult>>(result.results);
} }
private static void AddRetry(Request request)
{
request.Retry = true;
request.StatusCodeToRetry.Add((HttpStatusCode)429);
}
} }
} }

@ -12,14 +12,14 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.1.1-beta" /> <PackageReference Include="CommandLineParser" Version="2.1.1-beta" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="2.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration" Version="2.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="2.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="2.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="2.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="2.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="2.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="2.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="2.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="2.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="2.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="2.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="2.0.1" />
<PackageReference Include="Serilog" Version="2.6.0-dev-00892" /> <PackageReference Include="Serilog" Version="2.6.0-dev-00892" />
<PackageReference Include="Serilog.Extensions.Logging" Version="2.0.2" /> <PackageReference Include="Serilog.Extensions.Logging" Version="2.0.2" />
<PackageReference Include="Serilog.Sinks.File" Version="3.2.0" /> <PackageReference Include="Serilog.Sinks.File" Version="3.2.0" />

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

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

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

@ -46,6 +46,7 @@ export enum NotificationType {
WelcomeEmail = 8, WelcomeEmail = 8,
IssueResolved = 9, IssueResolved = 9,
IssueComment = 10, IssueComment = 10,
Newsletter = 11,
} }
export interface IDiscordNotifcationSettings extends INotificationSettings { export interface IDiscordNotifcationSettings extends INotificationSettings {
@ -54,6 +55,10 @@ export interface IDiscordNotifcationSettings extends INotificationSettings {
notificationTemplates: INotificationTemplates[]; notificationTemplates: INotificationTemplates[];
} }
export interface INewsletterNotificationSettings extends INotificationSettings {
notificationTemplate: INotificationTemplates;
}
export interface ITelegramNotifcationSettings extends INotificationSettings { export interface ITelegramNotifcationSettings extends INotificationSettings {
botApi: string; botApi: string;
chatId: 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; applicationName: string;
applicationUrl: string; applicationUrl: string;
logo: string; logo: string;
mobile: boolean;
customCssLink: string; customCssLink: string;
enableCustomDonations: boolean; enableCustomDonations: boolean;
customDonationUrl: string; customDonationUrl: string;
@ -106,6 +107,7 @@ export interface ICustomizationSettings extends ISettings {
presetThemeContent: string; presetThemeContent: string;
presetThemeDisplayName: string; presetThemeDisplayName: string;
presetThemeVersion: string; presetThemeVersion: string;
recentlyAddedPage: boolean;
} }
export interface IThemes { export interface IThemes {
@ -124,6 +126,8 @@ export interface IJobSettings {
automaticUpdater: string; automaticUpdater: string;
userImporter: string; userImporter: string;
sickRageSync: string; sickRageSync: string;
refreshMetadata: string;
newsletter: string;
} }
export interface IIssueSettings extends ISettings { export interface IIssueSettings extends ISettings {
@ -192,3 +196,18 @@ export interface IDogNzbSettings extends ISettings {
export interface IIssueCategory extends ISettings { export interface IIssueCategory extends ISettings {
value: string; 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 "./ISonarr";
export * from "./IUser"; export * from "./IUser";
export * from "./IIssues"; export * from "./IIssues";
export * from "./IRecentlyAdded";

@ -1,13 +1,16 @@
<div *ngIf="issue"> <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> <h1>{{issue.title}} </h1>
<div class="col-md-6"> <div class="col-md-6">
<span class="label label-info">{{IssueStatus[issue.status]}}</span> <img class="img-responsive poster" src="{{posterPath}}" alt="poster">
<span class="label label-success">{{issue.issueCategory.value}}</span> <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.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> <h3 *ngIf="issue.subject">{{'Issues.Subject' | translate}}: {{issue.subject}}</h3>
<br> <br>
<div class="form-group"> <div class="form-group">
<label for="description" class="control-label" [translate]="'Issues.Description'"></label> <label for="description" class="control-label" [translate]="'Issues.Description'"></label>
<div> <div>
@ -26,7 +29,8 @@
<div class="panel-heading top-bar"> <div class="panel-heading top-bar">
<div class="col-md-8 col-xs-8"> <div class="col-md-8 col-xs-8">
<h3 class="panel-title"> <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>
</div> </div>
@ -51,8 +55,7 @@
</div> </div>
<div class="panel-footer"> <div class="panel-footer">
<div class="input-group"> <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"> <span class="input-group-btn">
<button class="btn btn-primary btn-sm" id="btn-chat" (click)="addComment()" [translate]="'Issues.SendMessageButton'"></button> <button class="btn btn-primary btn-sm" id="btn-chat" (click)="addComment()" [translate]="'Issues.SendMessageButton'"></button>
</span> </span>

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

@ -2,8 +2,9 @@ import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router"; import { ActivatedRoute } from "@angular/router";
import { AuthService } from "../auth/auth.service"; 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"; import { IIssues, IIssuesChat, IIssueSettings, INewIssueComments, IssueStatus } from "../interfaces";
@Component({ @Component({
@ -22,6 +23,8 @@ export class IssueDetailsComponent implements OnInit {
public IssueStatus = IssueStatus; public IssueStatus = IssueStatus;
public isAdmin: boolean; public isAdmin: boolean;
public settings: IIssueSettings; public settings: IIssueSettings;
public backgroundPath: any;
public posterPath: any;
private issueId: number; private issueId: number;
@ -29,7 +32,9 @@ export class IssueDetailsComponent implements OnInit {
private route: ActivatedRoute, private route: ActivatedRoute,
private authService: AuthService, private authService: AuthService,
private settingsService: SettingsService, private settingsService: SettingsService,
private notificationService: NotificationService) { private notificationService: NotificationService,
private imageService: ImageService,
private sanitizer: DomSanitizer) {
this.route.params this.route.params
.subscribe((params: any) => { .subscribe((params: any) => {
this.issueId = parseInt(params.id); this.issueId = parseInt(params.id);
@ -56,8 +61,8 @@ export class IssueDetailsComponent implements OnInit {
providerId: x.providerId, providerId: x.providerId,
userReported: x.userReported, userReported: x.userReported,
}; };
this.setBackground(x);
}); });
this.loadComments(); this.loadComments();
} }
@ -85,4 +90,26 @@ export class IssueDetailsComponent implements OnInit {
private loadComments() { private loadComments() {
this.issueService.getComments(this.issueId).subscribe(x => this.comments = x); 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 { OrderModule } from "ngx-order-pipe";
import { PaginatorModule, SharedModule, TabViewModule } from "primeng/primeng"; import { PaginatorModule, SharedModule, TabViewModule } from "primeng/primeng";
import { IdentityService } from "../services"; import { IdentityService, SearchService } from "../services";
import { AuthGuard } from "../auth/auth.guard"; import { AuthGuard } from "../auth/auth.guard";
@ -43,6 +43,7 @@ const routes: Routes = [
], ],
providers: [ providers: [
IdentityService, IdentityService,
SearchService,
], ],
}) })

@ -1,5 +1,5 @@
<div *ngIf="landingPageSettings && customizationSettings"> <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="centered col-md-12">
<div class="row"> <div class="row">

@ -1,5 +1,5 @@
import { PlatformLocation } from "@angular/common"; import { PlatformLocation } from "@angular/common";
import { Component, OnInit } from "@angular/core"; import { Component, OnDestroy, OnInit } from "@angular/core";
import { IMediaServerStatus } from "../interfaces"; import { IMediaServerStatus } from "../interfaces";
import { ICustomizationSettings, ILandingPageSettings } from "../interfaces"; import { ICustomizationSettings, ILandingPageSettings } from "../interfaces";
@ -9,17 +9,21 @@ import { SettingsService } from "../services";
import { DomSanitizer } from "@angular/platform-browser"; import { DomSanitizer } from "@angular/platform-browser";
import { ImageService } from "../services"; import { ImageService } from "../services";
import { fadeInOutAnimation } from "../animations/fadeinout";
@Component({ @Component({
templateUrl: "./landingpage.component.html", templateUrl: "./landingpage.component.html",
animations: [fadeInOutAnimation],
styleUrls: ["./landingpage.component.scss"], styleUrls: ["./landingpage.component.scss"],
}) })
export class LandingPageComponent implements OnInit { export class LandingPageComponent implements OnDestroy, OnInit {
public customizationSettings: ICustomizationSettings; public customizationSettings: ICustomizationSettings;
public landingPageSettings: ILandingPageSettings; public landingPageSettings: ILandingPageSettings;
public background: any; public background: any;
public mediaServerStatus: IMediaServerStatus; public mediaServerStatus: IMediaServerStatus;
public baseUrl: string; public baseUrl: string;
private timer: any;
constructor(private settingsService: SettingsService, constructor(private settingsService: SettingsService,
private images: ImageService, private sanitizer: DomSanitizer, private landingPageService: LandingPageService, private images: ImageService, private sanitizer: DomSanitizer, private landingPageService: LandingPageService,
@ -31,6 +35,9 @@ export class LandingPageComponent implements OnInit {
this.images.getRandomBackground().subscribe(x => { 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.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(); const base = this.location.getBaseHrefFromDOM();
if (base.length > 1) { if (base.length > 1) {
@ -41,4 +48,18 @@ export class LandingPageComponent implements OnInit {
this.mediaServerStatus = x; 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="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="container" id="login">
<div class="card card-container"> <div class="card card-container">
<!-- <img class="profile-img-card" src="//lh3.googleusercontent.com/-6V8xOA6M7BA/AAAAAAAAAAI/AAAAAAAAAAA/rzlHcD0KYwo/photo.jpg?sz=120" alt="" /> --> <!-- <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 { FormBuilder, FormGroup, Validators } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute, Router } from "@angular/router";
import { TranslateService } from "@ngx-translate/core"; import { TranslateService } from "@ngx-translate/core";
@ -13,11 +13,14 @@ import { StatusService } from "../services";
import { DomSanitizer } from "@angular/platform-browser"; import { DomSanitizer } from "@angular/platform-browser";
import { ImageService } from "../services"; import { ImageService } from "../services";
import { fadeInOutAnimation } from "../animations/fadeinout";
@Component({ @Component({
templateUrl: "./login.component.html", templateUrl: "./login.component.html",
animations: [fadeInOutAnimation],
styleUrls: ["./login.component.scss"], styleUrls: ["./login.component.scss"],
}) })
export class LoginComponent implements OnInit { export class LoginComponent implements OnDestroy, OnInit {
public form: FormGroup; public form: FormGroup;
public customizationSettings: ICustomizationSettings; public customizationSettings: ICustomizationSettings;
@ -25,6 +28,7 @@ export class LoginComponent implements OnInit {
public background: any; public background: any;
public landingFlag: boolean; public landingFlag: boolean;
public baseUrl: string; public baseUrl: string;
private timer: any;
private errorBody: string; private errorBody: string;
private errorValidation: string; private errorValidation: string;
@ -67,6 +71,10 @@ export class LoginComponent implements OnInit {
this.images.getRandomBackground().subscribe(x => { 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.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(); const base = this.location.getBaseHrefFromDOM();
if (base.length > 1) { if (base.length > 1) {
this.baseUrl = base; this.baseUrl = base;
@ -102,4 +110,19 @@ export class LoginComponent implements OnInit {
}, err => this.notify.error(this.errorBody)); }, 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" <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"> <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" <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.issueRequest = req;
this.issueCategorySelected = catId; this.issueCategorySelected = catId;
this.issuesBarVisible = true; this.issuesBarVisible = true;
this.issueProviderId = req.id.toString();
} }
private removeRequestFromUi(key: IChildRequests) { private removeRequestFromUi(key: IChildRequests) {

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

@ -12,6 +12,7 @@ import {
IEmailNotificationSettings, IEmailNotificationSettings,
IEmbyServer, IEmbyServer,
IMattermostNotifcationSettings, IMattermostNotifcationSettings,
INewsletterNotificationSettings,
IPlexServer, IPlexServer,
IPushbulletNotificationSettings, IPushbulletNotificationSettings,
IPushoverNotificationSettings, IPushoverNotificationSettings,
@ -77,5 +78,8 @@ export class TesterService extends ServiceHelpers {
public sickrageTest(settings: ISickRageSettings): Observable<boolean> { public sickrageTest(settings: ISickRageSettings): Observable<boolean> {
return this.http.post<boolean>(`${this.url}sickrage`, JSON.stringify(settings), {headers: this.headers}); 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> { public getTvBanner(tvdbid: number): Observable<string> {
return this.http.get<string>(`${this.url}tv/${tvdbid}`, {headers: this.headers}); 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 "./helpers";
export * from "./identity.service"; export * from "./identity.service";
export * from "./image.service"; export * from "./image.service";
@ -13,3 +13,4 @@ export * from "./job.service";
export * from "./issues.service"; export * from "./issues.service";
export * from "./mobile.service"; export * from "./mobile.service";
export * from "./notificationMessage.service"; export * from "./notificationMessage.service";
export * from "./recentlyAdded.service";

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

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

Loading…
Cancel
Save