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

@ -1,5 +1,83 @@
# 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)
### **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 |
|----------|:---------------------------:|:----------------------------:|
| 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);
return await Api.Request<MovieResult>(request);

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

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

@ -14,14 +14,10 @@ namespace Ombi.Api.Mattermost
private readonly IApi _api;
public async Task<string> PushAsync(string webhook, MattermostBody message)
public async Task PushAsync(string webhook, MattermostMessage message)
{
var request = new Request(string.Empty, webhook, HttpMethod.Post);
request.AddJsonBody(message);
var result = await _api.RequestContent(request);
return result;
var client = new MatterhookClient(webhook);
await client.PostAsync(_api, message);
}
}
}

@ -0,0 +1,168 @@
///
///
///
/// Code taken from https://github.com/PromoFaux/Matterhook.NET.MatterhookClient
///
///
///
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace Ombi.Api.Mattermost.Models
{
public class MatterhookClient
{
private readonly Uri _webhookUrl;
private readonly HttpClient _httpClient = new HttpClient();
/// <summary>
/// Create a new Mattermost Client
/// </summary>
/// <param name="webhookUrl">The URL of your Mattermost Webhook</param>
/// <param name="timeoutSeconds">Timeout Value (Default 100)</param>
public MatterhookClient(string webhookUrl, int timeoutSeconds = 100)
{
if (!Uri.TryCreate(webhookUrl, UriKind.Absolute, out _webhookUrl))
throw new ArgumentException("Mattermost URL invalid");
_httpClient.Timeout = new TimeSpan(0, 0, 0, timeoutSeconds);
}
public MattermostMessage CloneMessage(MattermostMessage inMsg)
{
var outMsg = new MattermostMessage
{
Text = "",
Channel = inMsg.Channel,
Username = inMsg.Username,
IconUrl = inMsg.IconUrl
};
return outMsg;
}
private static MattermostAttachment CloneAttachment(MattermostAttachment inAtt)
{
var outAtt = new MattermostAttachment
{
AuthorIcon = inAtt.AuthorIcon,
AuthorLink = inAtt.AuthorLink,
AuthorName = inAtt.AuthorName,
Color = inAtt.Color,
Fallback = inAtt.Fallback,
Fields = inAtt.Fields,
ImageUrl = inAtt.ImageUrl,
Pretext = inAtt.Pretext,
ThumbUrl = inAtt.ThumbUrl,
Title = inAtt.Title,
TitleLink = inAtt.TitleLink,
Text = ""
};
return outAtt;
}
/// <summary>
/// Post Message to Mattermost server. Messages will be automatically split if total text length > 4000
/// </summary>
/// <param name="api"></param>
/// <param name="inMessage">The messsage you wish to send</param>
/// <returns></returns>
public async Task PostAsync(IApi api, MattermostMessage inMessage)
{
try
{
var outMessages = new List<MattermostMessage>();
var msgCount = 0;
var lines = new string[] { };
if (inMessage.Text != null)
{
lines = inMessage.Text.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None);
}
//start with one cloned inMessage in the list
outMessages.Add(CloneMessage(inMessage));
//add text from original. If we go over 3800, we'll split it to a new inMessage.
foreach (var line in lines)
{
if (line.Length + outMessages[msgCount].Text.Length > 3800)
{
msgCount += 1;
outMessages.Add(CloneMessage(inMessage));
}
outMessages[msgCount].Text += $"{line}\r\n";
}
//Length of text on the last (or first if only one) inMessage.
var lenMessageText = outMessages[msgCount].Text.Length;
//does our original have attachments?
if (inMessage.Attachments?.Any() ?? false)
{
outMessages[msgCount].Attachments = new List<MattermostAttachment>();
//loop through them in a similar fashion to the inMessage text above.
foreach (var att in inMessage.Attachments)
{
//add this attachment to the outgoing message
outMessages[msgCount].Attachments.Add(CloneAttachment(att));
//get a count of attachments on this message, and subtract one so we know the index of the current new attachment
var attIndex = outMessages[msgCount].Attachments.Count - 1;
//Get the text lines
lines = att.Text.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None);
foreach (var line in lines)
{
//Get the total length of all attachments on the current outgoing message
var lenAllAttsText = outMessages[msgCount].Attachments.Sum(a => a.Text.Length);
if (lenMessageText + lenAllAttsText + line.Length > 3800)
{
msgCount += 1;
attIndex = 0;
outMessages.Add(CloneMessage(inMessage));
outMessages[msgCount].Attachments = new List<MattermostAttachment> { CloneAttachment(att) };
}
outMessages[msgCount].Attachments[attIndex].Text += $"{line}\r\n";
}
}
}
if (outMessages.Count > 1)
{
var num = 1;
foreach (var msg in outMessages)
{
msg.Text = $"`({num}/{msgCount + 1}): ` " + msg.Text;
num++;
}
}
foreach (var msg in outMessages)
{
var request = new Request("", _webhookUrl.ToString(), HttpMethod.Post);
request.AddJsonBody(msg);
await api.Request(request);
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
throw;
}
}
}
}

@ -0,0 +1,181 @@
///
///
///
/// Code taken from https://github.com/PromoFaux/Matterhook.NET.MatterhookClient
///
///
///
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace Ombi.Api.Mattermost.Models
{
public class MattermostMessage
{
//https://docs.mattermost.com/developer/webhooks-incoming.html
/// <summary>
/// Channel to post to
/// </summary>
[JsonProperty(PropertyName = "channel")]
public string Channel { get; set; }
/// <summary>
/// Username for bot
/// </summary>
[JsonProperty(PropertyName = "username")]
public string Username { get; set; }
/// <summary>
/// Bot/User Icon
/// </summary>
[JsonProperty(PropertyName = "icon_url")]
public Uri IconUrl { get; set; }
/// <summary>
/// Message body. Supports Markdown
/// </summary>
[JsonProperty(PropertyName = "text")]
public string Text { get; set; }
/// <summary>
/// Richtext attachments
/// </summary>
[JsonProperty(PropertyName = "attachments")]
public List<MattermostAttachment> Attachments { get; set; }
}
/// <summary>
/// https://docs.mattermost.com/developer/message-attachments.html#message-attachments
/// </summary>
public class MattermostAttachment
{
//https://docs.mattermost.com/developer/message-attachments.html#attachment-options
#region AttachmentOptions
/// <summary>
/// A required plain-text summary of the post. This is used in notifications, and in clients that dont support formatted text (eg IRC).
/// </summary>
[JsonProperty(PropertyName = "fallback")]
public string Fallback { get; set; }
/// <summary>
/// A hex color code that will be used as the left border color for the attachment. If not specified, it will default to match the left hand sidebar header background color.
/// </summary>
[JsonProperty(PropertyName = "color")]
public string Color { get; set; }
/// <summary>
/// Optional text that should appear above the formatted data
/// </summary>
[JsonProperty(PropertyName = "pretext")]
public string Pretext { get; set; }
/// <summary>
/// The text to be included in the attachment. It can be formatted using Markdown. If it includes more than 300 characters or more than 5 line breaks, the message will be collapsed and a “Show More” link will be added to expand the message.
/// </summary>
[JsonProperty(PropertyName = "text")]
public string Text { get; set; }
#endregion
//https://docs.mattermost.com/developer/message-attachments.html#author-details
#region AuthorDetails
/// <summary>
/// An optional name used to identify the author. It will be included in a small section at the top of the attachment.
/// </summary>
[JsonProperty(PropertyName = "author_name")]
public string AuthorName { get; set; }
/// <summary>
/// An optional URL used to hyperlink the author_name. If no author_name is specified, this field does nothing.
/// </summary>
[JsonProperty(PropertyName = "author_link")]
public Uri AuthorLink { get; set; }
/// <summary>
/// An optional URL used to display a 16x16 pixel icon beside the author_name.
/// </summary>
[JsonProperty(PropertyName = "author_icon")]
public Uri AuthorIcon { get; set; }
#endregion
//https://docs.mattermost.com/developer/message-attachments.html#titles
#region Titles
/// <summary>
/// An optional title displayed below the author information in the attachment.
/// </summary>
[JsonProperty(PropertyName = "title")]
public string Title { get; set; }
/// <summary>
/// An optional URL used to hyperlink the title. If no title is specified, this field does nothing.
/// </summary>
[JsonProperty(PropertyName = "title_link")]
public Uri TitleLink { get; set; }
#endregion
#region Fields
/// <summary>
/// Fields can be included as an optional array within attachments, and are used to display information in a table format inside the attachment.
/// </summary>
[JsonProperty(PropertyName = "fields")]
public List<MattermostField> Fields { get; set; }
#endregion
//https://docs.mattermost.com/developer/message-attachments.html#images
#region Images
/// <summary>
/// An optional URL to an image file (GIF, JPEG, PNG, or BMP) that is displayed inside a message attachment.
/// Large images are resized to a maximum width of 400px or a maximum height of 300px, while still maintaining the original aspect ratio.
/// </summary>
[JsonProperty(PropertyName = "image_url")]
public Uri ImageUrl { get; set; }
/// <summary>
/// An optional URL to an image file(GIF, JPEG, PNG, or BMP) that is displayed as a 75x75 pixel thumbnail on the right side of an attachment.
/// We recommend using an image that is already 75x75 pixels, but larger images will be scaled down with the aspect ratio maintained.
/// </summary>
[JsonProperty(PropertyName = "thumb_url")]
public Uri ThumbUrl { get; set; }
#endregion
}
/// <summary>
/// https://docs.mattermost.com/developer/message-attachments.html#fieldshttps://docs.mattermost.com/developer/message-attachments.html#fields
/// </summary>
public class MattermostField
{
/// <summary>
/// A title shown in the table above the value.
/// </summary>
[JsonProperty(PropertyName = "title")]
public string Title { get; set; }
/// <summary>
/// The text value of the field. It can be formatted using Markdown.
/// </summary>
[JsonProperty(PropertyName = "value")]
public string Value { get; set; }
/// <summary>
/// Optionally set to “True” or “False” to indicate whether the value is short enough to be displayed beside other values.
/// </summary>
[JsonProperty(PropertyName = "short")]
public bool Short { get; set; }
}
}

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

@ -11,7 +11,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Options" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="2.0.1" />
</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.Headers;
using System.Threading.Tasks;
@ -6,6 +9,7 @@ using System.Xml.Serialization;
using Newtonsoft.Json;
using Microsoft.Extensions.Logging;
using Ombi.Helpers;
using Polly;
namespace Ombi.Api
{
@ -36,6 +40,30 @@ namespace Ombi.Api
if (!httpResponseMessage.IsSuccessStatusCode)
{
LogError(request, httpResponseMessage);
if (request.Retry)
{
var result = Policy
.Handle<HttpRequestException>()
.OrResult<HttpResponseMessage>(r => request.StatusCodeToRetry.Contains(r.StatusCode))
.WaitAndRetryAsync(new[]
{
TimeSpan.FromSeconds(10),
}, (exception, timeSpan, context) =>
{
Logger.LogError(LoggingEvents.Api,
$"Retrying RequestUri: {request.FullUri} Because we got Status Code: {exception?.Result?.StatusCode}");
});
httpResponseMessage = await result.ExecuteAsync(async () =>
{
using (var req = await httpRequestMessage.Clone())
{
return await _client.SendAsync(req);
}
});
}
}
// do something with the response

@ -0,0 +1,45 @@
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
namespace Ombi.Api
{
public static class HttpRequestExtnesions
{
public static async Task<HttpRequestMessage> Clone(this HttpRequestMessage request)
{
var clone = new HttpRequestMessage(request.Method, request.RequestUri)
{
Content = await request.Content.Clone(),
Version = request.Version
};
foreach (KeyValuePair<string, object> prop in request.Properties)
{
clone.Properties.Add(prop);
}
foreach (KeyValuePair<string, IEnumerable<string>> header in request.Headers)
{
clone.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
return clone;
}
public static async Task<HttpContent> Clone(this HttpContent content)
{
if (content == null) return null;
var ms = new MemoryStream();
await content.CopyToAsync(ms);
ms.Position = 0;
var clone = new StreamContent(ms);
foreach (KeyValuePair<string, IEnumerable<string>> header in content.Headers)
{
clone.Headers.Add(header.Key, header.Value);
}
return clone;
}
}
}

@ -9,8 +9,9 @@
</PropertyGroup>
<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="Polly" Version="5.8.0" />
<PackageReference Include="System.Xml.XmlSerializer" Version="4.3.0" />
</ItemGroup>

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Text;
@ -25,6 +26,9 @@ namespace Ombi.Api
public string BaseUrl { get; }
public HttpMethod HttpMethod { get; }
public bool Retry { get; set; }
public List<HttpStatusCode> StatusCodeToRetry { get; set; } = new List<HttpStatusCode>();
public Action<string> OnBeforeDeserialization { get; set; }
private string FullUrl

@ -9,7 +9,7 @@
<PackageReference Include="Nunit" Version="3.8.1" />
<PackageReference Include="NUnit.ConsoleRunner" Version="3.7.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>

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Ombi.Core.Models;
namespace Ombi.Core.Engine
{
public interface IRecentlyAddedEngine
{
IEnumerable<RecentlyAddedMovieModel> GetRecentlyAddedMovies();
IEnumerable<RecentlyAddedMovieModel> GetRecentlyAddedMovies(DateTime from, DateTime to);
IEnumerable<RecentlyAddedTvModel> GetRecentlyAddedTv(DateTime from, DateTime to, bool groupBySeason);
IEnumerable<RecentlyAddedTvModel> GetRecentlyAddedTv(bool groupBySeason);
Task<bool> UpdateRecentlyAddedDatabase();
}
}

@ -54,7 +54,7 @@ namespace Ombi.Core.Engine
{
Result = false,
Message = "There was an issue adding this movie!",
ErrorMessage = $"TheMovieDb didn't have any information for ID {model.TheMovieDbId}"
ErrorMessage = $"Please try again later"
};
}
var fullMovieName =

@ -0,0 +1,224 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Ombi.Core.Models;
using Ombi.Helpers;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
using RecentlyAddedType = Ombi.Store.Entities.RecentlyAddedType;
namespace Ombi.Core.Engine
{
public class RecentlyAddedEngine : IRecentlyAddedEngine
{
public RecentlyAddedEngine(IPlexContentRepository plex, IEmbyContentRepository emby, IRepository<RecentlyAddedLog> recentlyAdded)
{
_plex = plex;
_emby = emby;
_recentlyAddedLog = recentlyAdded;
}
private readonly IPlexContentRepository _plex;
private readonly IEmbyContentRepository _emby;
private readonly IRepository<RecentlyAddedLog> _recentlyAddedLog;
public IEnumerable<RecentlyAddedMovieModel> GetRecentlyAddedMovies(DateTime from, DateTime to)
{
var plexMovies = _plex.GetAll().Where(x => x.Type == PlexMediaTypeEntity.Movie && x.AddedAt > from && x.AddedAt < to);
var embyMovies = _emby.GetAll().Where(x => x.Type == EmbyMediaType.Movie && x.AddedAt > from && x.AddedAt < to);
return GetRecentlyAddedMovies(plexMovies, embyMovies).Take(30);
}
public IEnumerable<RecentlyAddedMovieModel> GetRecentlyAddedMovies()
{
var plexMovies = _plex.GetAll().Where(x => x.Type == PlexMediaTypeEntity.Movie);
var embyMovies = _emby.GetAll().Where(x => x.Type == EmbyMediaType.Movie);
return GetRecentlyAddedMovies(plexMovies, embyMovies);
}
public IEnumerable<RecentlyAddedTvModel> GetRecentlyAddedTv(DateTime from, DateTime to, bool groupBySeason)
{
var plexTv = _plex.GetAll().Include(x => x.Seasons).Include(x => x.Episodes).Where(x => x.Type == PlexMediaTypeEntity.Show && x.AddedAt > from && x.AddedAt < to);
var embyTv = _emby.GetAll().Include(x => x.Episodes).Where(x => x.Type == EmbyMediaType.Series && x.AddedAt > from && x.AddedAt < to);
return GetRecentlyAddedTv(plexTv, embyTv, groupBySeason).Take(30);
}
public IEnumerable<RecentlyAddedTvModel> GetRecentlyAddedTv(bool groupBySeason)
{
var plexTv = _plex.GetAll().Include(x => x.Seasons).Include(x => x.Episodes).Where(x => x.Type == PlexMediaTypeEntity.Show);
var embyTv = _emby.GetAll().Include(x => x.Episodes).Where(x => x.Type == EmbyMediaType.Series);
return GetRecentlyAddedTv(plexTv, embyTv, groupBySeason);
}
public async Task<bool> UpdateRecentlyAddedDatabase()
{
var plexContent = _plex.GetAll().Include(x => x.Episodes);
var embyContent = _emby.GetAll().Include(x => x.Episodes);
var recentlyAddedLog = new HashSet<RecentlyAddedLog>();
foreach (var p in plexContent)
{
if (p.Type == PlexMediaTypeEntity.Movie)
{
recentlyAddedLog.Add(new RecentlyAddedLog
{
AddedAt = DateTime.Now,
Type = RecentlyAddedType.Plex,
ContentId = p.Id,
ContentType = ContentType.Parent
});
}
else
{
// Add the episodes
foreach (var ep in p.Episodes)
{
recentlyAddedLog.Add(new RecentlyAddedLog
{
AddedAt = DateTime.Now,
Type = RecentlyAddedType.Plex,
ContentId = ep.Id,
ContentType = ContentType.Episode
});
}
}
}
foreach (var e in embyContent)
{
if (e.Type == EmbyMediaType.Movie)
{
recentlyAddedLog.Add(new RecentlyAddedLog
{
AddedAt = DateTime.Now,
Type = RecentlyAddedType.Emby,
ContentId = e.Id,
ContentType = ContentType.Parent
});
}
else
{
// Add the episodes
foreach (var ep in e.Episodes)
{
recentlyAddedLog.Add(new RecentlyAddedLog
{
AddedAt = DateTime.Now,
Type = RecentlyAddedType.Emby,
ContentId = ep.Id,
ContentType = ContentType.Episode
});
}
}
}
await _recentlyAddedLog.AddRange(recentlyAddedLog);
return true;
}
private IEnumerable<RecentlyAddedTvModel> GetRecentlyAddedTv(IQueryable<PlexServerContent> plexTv, IQueryable<EmbyContent> embyTv,
bool groupBySeason)
{
var model = new HashSet<RecentlyAddedTvModel>();
TransformPlexShows(plexTv, model);
TransformEmbyShows(embyTv, model);
if (groupBySeason)
{
return model.DistinctBy(x => x.SeasonNumber);
}
return model;
}
private IEnumerable<RecentlyAddedMovieModel> GetRecentlyAddedMovies(IQueryable<PlexServerContent> plexMovies, IQueryable<EmbyContent> embyMovies)
{
var model = new HashSet<RecentlyAddedMovieModel>();
TransformPlexMovies(plexMovies, model);
TransformEmbyMovies(embyMovies, model);
return model;
}
private static void TransformEmbyMovies(IQueryable<EmbyContent> embyMovies, HashSet<RecentlyAddedMovieModel> model)
{
foreach (var emby in embyMovies)
{
model.Add(new RecentlyAddedMovieModel
{
Id = emby.Id,
ImdbId = emby.ProviderId,
AddedAt = emby.AddedAt,
Title = emby.Title,
});
}
}
private static void TransformPlexMovies(IQueryable<PlexServerContent> plexMovies, HashSet<RecentlyAddedMovieModel> model)
{
foreach (var plex in plexMovies)
{
model.Add(new RecentlyAddedMovieModel
{
Id = plex.Id,
ImdbId = plex.ImdbId,
TheMovieDbId = plex.TheMovieDbId,
AddedAt = plex.AddedAt,
Title = plex.Title,
Quality = plex.Quality,
ReleaseYear = plex.ReleaseYear
});
}
}
private static void TransformPlexShows(IQueryable<PlexServerContent> plexShows, HashSet<RecentlyAddedTvModel> model)
{
foreach (var plex in plexShows)
{
foreach (var season in plex.Seasons)
{
foreach (var episode in plex.Episodes)
{
model.Add(new RecentlyAddedTvModel
{
Id = plex.Id,
ImdbId = plex.ImdbId,
TheMovieDbId = plex.TheMovieDbId,
AddedAt = plex.AddedAt,
Title = plex.Title,
Quality = plex.Quality,
ReleaseYear = plex.ReleaseYear,
TvDbId = plex.TvDbId,
EpisodeNumber = episode.EpisodeNumber,
SeasonNumber = season.SeasonNumber
});
}
}
}
}
private static void TransformEmbyShows(IQueryable<EmbyContent> embyShows, HashSet<RecentlyAddedTvModel> model)
{
foreach (var emby in embyShows)
{
foreach (var episode in emby.Episodes)
{
model.Add(new RecentlyAddedTvModel
{
Id = emby.Id,
ImdbId = emby.ProviderId,
AddedAt = emby.AddedAt,
Title = emby.Title,
EpisodeNumber = episode.EpisodeNumber,
SeasonNumber = episode.SeasonNumber
});
}
}
}
}
}

@ -0,0 +1,23 @@
using System;
namespace Ombi.Core.Models
{
public class RecentlyAddedMovieModel
{
public int Id { get; set; }
public string Title { get; set; }
public string Overview { get; set; }
public string ImdbId { get; set; }
public string TvDbId { get; set; }
public string TheMovieDbId { get; set; }
public string ReleaseYear { get; set; }
public DateTime AddedAt { get; set; }
public string Quality { get; set; }
}
public enum RecentlyAddedType
{
Plex,
Emby
}
}

@ -0,0 +1,19 @@
using System;
namespace Ombi.Core.Models
{
public class RecentlyAddedTvModel
{
public int Id { get; set; }
public string Title { get; set; } // Series Title
public string Overview { get; set; }
public string ImdbId { get; set; }
public string TvDbId { get; set; }
public string TheMovieDbId { get; set; }
public string ReleaseYear { get; set; }
public DateTime AddedAt { get; set; }
public string Quality { get; set; }
public int SeasonNumber { get; set; }
public int EpisodeNumber { get; set; }
}
}

@ -0,0 +1,23 @@

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

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

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

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

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

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

@ -9,5 +9,6 @@
public const string RequestTv = nameof(RequestTv);
public const string RequestMovie = nameof(RequestMovie);
public const string Disabled = nameof(Disabled);
public const string RecievesNewsletter = nameof(RecievesNewsletter);
}
}

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

@ -4,19 +4,26 @@ using System.Text;
namespace Ombi.Notifications.Templates
{
public class EmailBasicTemplate : IEmailBasicTemplate
public class EmailBasicTemplate : TemplateBase, IEmailBasicTemplate
{
public string TemplateLocation
public override string TemplateLocation
{
get
{
if (string.IsNullOrEmpty(_templateLocation))
{
#if DEBUG
return Path.Combine(Directory.GetCurrentDirectory(), "bin", "Debug", "netcoreapp2.0", "Templates", "BasicTemplate.html");
_templateLocation = Path.Combine(Directory.GetCurrentDirectory(), "bin", "Debug", "netcoreapp2.0", "Templates",
"BasicTemplate.html");
#else
return Path.Combine(Directory.GetCurrentDirectory(), "Templates","BasicTemplate.html");
_templateLocation = Path.Combine(Directory.GetCurrentDirectory(), "Templates","BasicTemplate.html");
#endif
}
return _templateLocation;
}
}
private string _templateLocation;
private const string SubjectKey = "{@SUBJECT}";
private const string BodyKey = "{@BODY}";
@ -31,7 +38,7 @@ namespace Ombi.Notifications.Templates
sb.Replace(BodyKey, body);
sb.Replace(DateKey, DateTime.Now.ToString("f"));
sb.Replace(Poster, string.IsNullOrEmpty(imgsrc) ? string.Empty : $"<tr><td align=\"center\"><img src=\"{imgsrc}\" alt=\"Poster\" width=\"400px\" text-align=\"center\"/></td></tr>");
sb.Replace(Logo, string.IsNullOrEmpty(logo) ? "http://i.imgur.com/qQsN78U.png" : logo);
sb.Replace(Logo, string.IsNullOrEmpty(logo) ? OmbiLogo : logo);
return sb.ToString();
}

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

@ -0,0 +1,46 @@
using System;
using System.IO;
using System.Text;
namespace Ombi.Notifications.Templates
{
public class NewsletterTemplate : TemplateBase, INewsletterTemplate
{
public override string TemplateLocation
{
get
{
if (string.IsNullOrEmpty(_templateLocation))
{
#if DEBUG
_templateLocation = Path.Combine(Directory.GetCurrentDirectory(), "bin", "Debug", "netcoreapp2.0", "Templates", "NewsletterTemplate.html");
#else
_templateLocation = Path.Combine(Directory.GetCurrentDirectory(), "Templates", "NewsletterTemplate.html");
#endif
}
return _templateLocation;
}
}
private string _templateLocation;
private const string SubjectKey = "{@SUBJECT}";
private const string DateKey = "{@DATENOW}";
private const string Logo = "{@LOGO}";
private const string TableLocation = "{@RECENTLYADDED}";
private const string IntroText = "{@INTRO}";
public string LoadTemplate(string subject, string intro, string tableHtml, string logo)
{
var sb = new StringBuilder(File.ReadAllText(TemplateLocation));
sb.Replace(SubjectKey, subject);
sb.Replace(TableLocation, tableHtml);
sb.Replace(IntroText, intro);
sb.Replace(DateKey, DateTime.Now.ToString("f"));
sb.Replace(Logo, string.IsNullOrEmpty(logo) ? OmbiLogo : logo);
return sb.ToString();
}
}
}

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

@ -0,0 +1,8 @@
namespace Ombi.Notifications.Templates
{
public abstract class TemplateBase
{
public abstract string TemplateLocation { get; }
public virtual string OmbiLogo => "http://i.imgur.com/qQsN78U.png";
}
}

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

@ -8,7 +8,7 @@
<PackageReference Include="Nunit" Version="3.8.1" />
<PackageReference Include="NUnit.ConsoleRunner" Version="3.7.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" />
</ItemGroup>

@ -59,10 +59,18 @@ namespace Ombi.Notifications.Agents
Message = parsed.Message,
};
notification.Other.Add("image", parsed.Image);
AddOtherInformation(model, notification, parsed);
//notification.Other.Add("overview", model.RequestType == RequestType.Movie ? base.MovieRequest.Overview : TvRequest.);
await Send(notification, settings);
}
private void AddOtherInformation(NotificationOptions model, NotificationMessage notification,
NotificationMessageContent parsed)
{
notification.Other.Add("image", parsed.Image);
notification.Other.Add("title", model.RequestType == RequestType.Movie ? MovieRequest.Title : TvRequest.Title);
}
protected override async Task NewIssue(NotificationOptions model, MattermostNotificationSettings settings)
{
var parsed = await LoadTemplate(NotificationAgent.Mattermost, NotificationType.Issue, model);
@ -75,7 +83,7 @@ namespace Ombi.Notifications.Agents
{
Message = parsed.Message,
};
notification.Other.Add("image", parsed.Image);
AddOtherInformation(model, notification, parsed);
await Send(notification, settings);
}
@ -91,7 +99,7 @@ namespace Ombi.Notifications.Agents
{
Message = parsed.Message,
};
notification.Other.Add("image", parsed.Image);
AddOtherInformation(model, notification, parsed);
await Send(notification, settings);
}
@ -107,7 +115,7 @@ namespace Ombi.Notifications.Agents
{
Message = parsed.Message,
};
notification.Other.Add("image", parsed.Image);
AddOtherInformation(model, notification, parsed);
await Send(notification, settings);
}
@ -149,7 +157,7 @@ namespace Ombi.Notifications.Agents
{
Message = parsed.Message,
};
notification.Other.Add("image", parsed.Image);
AddOtherInformation(model, notification, parsed);
await Send(notification, settings);
}
@ -166,7 +174,7 @@ namespace Ombi.Notifications.Agents
Message = parsed.Message,
};
notification.Other.Add("image", parsed.Image);
AddOtherInformation(model, notification, parsed);
await Send(notification, settings);
}
@ -182,7 +190,7 @@ namespace Ombi.Notifications.Agents
{
Message = parsed.Message,
};
notification.Other.Add("image", parsed.Image);
AddOtherInformation(model, notification, parsed);
await Send(notification, settings);
}
@ -190,12 +198,20 @@ namespace Ombi.Notifications.Agents
{
try
{
var body = new MattermostBody
var body = new MattermostMessage
{
username = string.IsNullOrEmpty(settings.Username) ? "Ombi" : settings.Username,
channel = settings.Channel,
text = model.Message,
icon_url = settings.IconUrl
Username = string.IsNullOrEmpty(settings.Username) ? "Ombi" : settings.Username,
Channel = settings.Channel,
Text = model.Message,
IconUrl = new Uri(settings.IconUrl),
Attachments = new List<MattermostAttachment>
{
new MattermostAttachment
{
Title = model.Other.ContainsKey("title") ? model.Other["title"] : string.Empty,
ImageUrl = model.Other.ContainsKey("image") ? new Uri(model.Other["image"]) : null,
}
}
};
await Api.PushAsync(settings.WebhookUrl, body);
}

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

@ -1,10 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Ombi.Helpers;
using Ombi.Notifications.Models;
using Ombi.Settings.Settings.Models;
using Ombi.Store.Entities;
using Ombi.Store.Entities.Requests;
using Ombi.Store.Repository.Requests;
namespace Ombi.Notifications
{
@ -25,9 +28,9 @@ namespace Ombi.Notifications
}
ApplicationUrl = (s?.ApplicationUrl.HasValue() ?? false) ? s.ApplicationUrl : string.Empty;
ApplicationName = string.IsNullOrEmpty(s?.ApplicationName) ? "Ombi" : s?.ApplicationName;
RequestedUser = string.IsNullOrEmpty(req?.RequestedUser?.Alias)
? req?.RequestedUser?.UserName
: req?.RequestedUser?.Alias;
RequestedUser = req?.RequestedUser?.UserName;
UserName = req?.RequestedUser?.UserName;
Alias = (req?.RequestedUser?.Alias.HasValue() ?? false) ? req?.RequestedUser?.Alias : req?.RequestedUser?.UserName;
Title = title;
RequestedDate = req?.RequestedDate.ToString("D");
Type = req?.RequestType.ToString();
@ -38,6 +41,15 @@ namespace Ombi.Notifications
AdditionalInformation = opts?.AdditionalInformation ?? string.Empty;
}
public void SetupNewsletter(CustomizationSettings s, OmbiUser username)
{
ApplicationUrl = (s?.ApplicationUrl.HasValue() ?? false) ? s.ApplicationUrl : string.Empty;
ApplicationName = string.IsNullOrEmpty(s?.ApplicationName) ? "Ombi" : s?.ApplicationName;
RequestedUser = username.UserName;
UserName = username.UserName;
Alias = username.Alias.HasValue() ? username.Alias : username.UserName;
}
public void Setup(NotificationOptions opts, ChildRequests req, CustomizationSettings s)
{
LoadIssues(opts);
@ -52,9 +64,9 @@ namespace Ombi.Notifications
}
ApplicationUrl = (s?.ApplicationUrl.HasValue() ?? false) ? s.ApplicationUrl : string.Empty;
ApplicationName = string.IsNullOrEmpty(s?.ApplicationName) ? "Ombi" : s?.ApplicationName;
RequestedUser = string.IsNullOrEmpty(req?.RequestedUser.Alias)
? req?.RequestedUser.UserName
: req?.RequestedUser.Alias;
RequestedUser = req?.RequestedUser?.UserName;
UserName = req?.RequestedUser?.UserName;
Alias = (req?.RequestedUser?.Alias.HasValue() ?? false) ? req?.RequestedUser?.Alias : req?.RequestedUser?.UserName;
Title = title;
RequestedDate = req?.RequestedDate.ToString("D");
Type = req?.RequestType.ToString();
@ -64,6 +76,40 @@ namespace Ombi.Notifications
$"https://image.tmdb.org/t/p/w300{req?.ParentRequest.PosterPath}" : req?.ParentRequest.PosterPath;
AdditionalInformation = opts.AdditionalInformation;
// DO Episode and Season Lists
var episodes = req?.SeasonRequests?.SelectMany(x => x.Episodes) ?? new List<EpisodeRequests>();
var seasons = req?.SeasonRequests?.OrderBy(x => x.SeasonNumber).ToList() ?? new List<SeasonRequests>();
var orderedEpisodes = episodes.OrderBy(x => x.EpisodeNumber).ToList();
var epSb = new StringBuilder();
var seasonSb = new StringBuilder();
for (var i = 0; i < orderedEpisodes.Count; i++)
{
var ep = orderedEpisodes[i];
if (i < orderedEpisodes.Count - 1)
{
epSb.Append($"{ep.EpisodeNumber},");
}
else
{
epSb.Append($"{ep.EpisodeNumber}");
}
}
for (var i = 0; i < seasons.Count; i++)
{
var ep = seasons[i];
if (i < seasons.Count - 1)
{
seasonSb.Append($"{ep.SeasonNumber},");
}
else
{
seasonSb.Append($"{ep.SeasonNumber}");
}
}
EpisodesList = epSb.ToString();
SeasonsList = seasonSb.ToString();
}
public void Setup(OmbiUser user, CustomizationSettings s)
@ -81,13 +127,14 @@ namespace Ombi.Notifications
IssueStatus = opts.Substitutes.TryGetValue("IssueStatus", out val) ? val : string.Empty;
IssueSubject = opts.Substitutes.TryGetValue("IssueSubject", out val) ? val : string.Empty;
NewIssueComment = opts.Substitutes.TryGetValue("NewIssueComment", out val) ? val : string.Empty;
RequestedUser = opts.Substitutes.TryGetValue("IssueUser", out val) ? val : string.Empty;
UserName = opts.Substitutes.TryGetValue("IssueUser", out val) ? val : string.Empty;
}
// User Defined
public string RequestedUser { get; set; }
public string UserName => RequestedUser;
public string IssueUser => RequestedUser;
public string UserName { get; set; }
public string IssueUser => UserName;
public string Alias { get; set; }
public string Title { get; set; }
public string RequestedDate { get; set; }
@ -137,6 +184,7 @@ namespace Ombi.Notifications
{nameof(NewIssueComment),NewIssueComment},
{nameof(IssueUser),IssueUser},
{nameof(UserName),UserName},
{nameof(Alias),Alias},
};
}
}

@ -6,12 +6,12 @@
<ItemGroup>
<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="Nunit" Version="3.8.1" />
<PackageReference Include="NUnit.ConsoleRunner" Version="3.7.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>

@ -17,46 +17,53 @@ namespace Ombi.Schedule
public JobSetup(IPlexContentSync plexContentSync, IRadarrSync radarrSync,
IOmbiAutomaticUpdater updater, IEmbyContentSync embySync, IPlexUserImporter userImporter,
IEmbyUserImporter embyUserImporter, ISonarrSync cache, ICouchPotatoSync cpCache,
ISettingsService<JobSettings> jobsettings, ISickRageSync srSync)
ISettingsService<JobSettings> jobsettings, ISickRageSync srSync, IRefreshMetadata refresh,
INewsletterJob newsletter)
{
PlexContentSync = plexContentSync;
RadarrSync = radarrSync;
Updater = updater;
EmbyContentSync = embySync;
PlexUserImporter = userImporter;
EmbyUserImporter = embyUserImporter;
SonarrSync = cache;
CpCache = cpCache;
JobSettings = jobsettings;
SrSync = srSync;
_plexContentSync = plexContentSync;
_radarrSync = radarrSync;
_updater = updater;
_embyContentSync = embySync;
_plexUserImporter = userImporter;
_embyUserImporter = embyUserImporter;
_sonarrSync = cache;
_cpCache = cpCache;
_jobSettings = jobsettings;
_srSync = srSync;
_refreshMetadata = refresh;
_newsletter = newsletter;
}
private IPlexContentSync PlexContentSync { get; }
private IRadarrSync RadarrSync { get; }
private IOmbiAutomaticUpdater Updater { get; }
private IPlexUserImporter PlexUserImporter { get; }
private IEmbyContentSync EmbyContentSync { get; }
private IEmbyUserImporter EmbyUserImporter { get; }
private ISonarrSync SonarrSync { get; }
private ICouchPotatoSync CpCache { get; }
private ISickRageSync SrSync { get; }
private ISettingsService<JobSettings> JobSettings { get; set; }
private readonly IPlexContentSync _plexContentSync;
private readonly IRadarrSync _radarrSync;
private readonly IOmbiAutomaticUpdater _updater;
private readonly IPlexUserImporter _plexUserImporter;
private readonly IEmbyContentSync _embyContentSync;
private readonly IEmbyUserImporter _embyUserImporter;
private readonly ISonarrSync _sonarrSync;
private readonly ICouchPotatoSync _cpCache;
private readonly ISickRageSync _srSync;
private readonly ISettingsService<JobSettings> _jobSettings;
private readonly IRefreshMetadata _refreshMetadata;
private readonly INewsletterJob _newsletter;
public void Setup()
{
var s = JobSettings.GetSettings();
var s = _jobSettings.GetSettings();
RecurringJob.AddOrUpdate(() => EmbyContentSync.Start(), JobSettingsHelper.EmbyContent(s));
RecurringJob.AddOrUpdate(() => SonarrSync.Start(), JobSettingsHelper.Sonarr(s));
RecurringJob.AddOrUpdate(() => RadarrSync.CacheContent(), JobSettingsHelper.Radarr(s));
RecurringJob.AddOrUpdate(() => PlexContentSync.CacheContent(), JobSettingsHelper.PlexContent(s));
RecurringJob.AddOrUpdate(() => CpCache.Start(), JobSettingsHelper.CouchPotato(s));
RecurringJob.AddOrUpdate(() => SrSync.Start(), JobSettingsHelper.SickRageSync(s));
RecurringJob.AddOrUpdate(() => _embyContentSync.Start(), JobSettingsHelper.EmbyContent(s));
RecurringJob.AddOrUpdate(() => _sonarrSync.Start(), JobSettingsHelper.Sonarr(s));
RecurringJob.AddOrUpdate(() => _radarrSync.CacheContent(), JobSettingsHelper.Radarr(s));
RecurringJob.AddOrUpdate(() => _plexContentSync.CacheContent(), JobSettingsHelper.PlexContent(s));
RecurringJob.AddOrUpdate(() => _cpCache.Start(), JobSettingsHelper.CouchPotato(s));
RecurringJob.AddOrUpdate(() => _srSync.Start(), JobSettingsHelper.SickRageSync(s));
RecurringJob.AddOrUpdate(() => _refreshMetadata.Start(), JobSettingsHelper.RefreshMetadata(s));
RecurringJob.AddOrUpdate(() => Updater.Update(null), JobSettingsHelper.Updater(s));
RecurringJob.AddOrUpdate(() => _updater.Update(null), JobSettingsHelper.Updater(s));
RecurringJob.AddOrUpdate(() => EmbyUserImporter.Start(), JobSettingsHelper.UserImporter(s));
RecurringJob.AddOrUpdate(() => PlexUserImporter.Start(), JobSettingsHelper.UserImporter(s));
RecurringJob.AddOrUpdate(() => _embyUserImporter.Start(), JobSettingsHelper.UserImporter(s));
RecurringJob.AddOrUpdate(() => _plexUserImporter.Start(), JobSettingsHelper.UserImporter(s));
RecurringJob.AddOrUpdate(() => _newsletter.Start(), JobSettingsHelper.Newsletter(s));
}
}
}

@ -0,0 +1,46 @@
using System.Text;
namespace Ombi.Schedule.Jobs.Ombi
{
public abstract class HtmlTemplateGenerator
{
protected virtual void AddParagraph(StringBuilder stringBuilder, string text, int fontSize = 14, string fontWeight = "normal")
{
stringBuilder.AppendFormat("<p style=\"font-family: sans-serif; font-size: {1}px; font-weight: {2}; margin: 0; Margin-bottom: 15px;\">{0}</p>", text, fontSize, fontWeight);
}
protected virtual void AddImageInsideTable(StringBuilder sb, string url, int size = 400)
{
sb.Append("<tr>");
sb.Append("<td align=\"center\">");
sb.Append($"<img src=\"{url}\" width=\"{size}px\" text-align=\"center\" />");
sb.Append("</td>");
sb.Append("</tr>");
}
protected virtual void Href(StringBuilder sb, string url)
{
sb.AppendFormat("<a href=\"{0}\">", url);
}
protected virtual void TableData(StringBuilder sb)
{
sb.Append(
"<td align=\"center\" style=\"font-family: sans-serif; font-size: 14px; vertical-align: top;\" valign=\"top\">");
}
protected virtual void EndTag(StringBuilder sb, string tag)
{
sb.AppendFormat("</{0}>", tag);
}
protected virtual void Header(StringBuilder sb, int size, string text, string fontWeight = "normal")
{
sb.AppendFormat(
"<h{0} style=\"font-family: sans-serif; font-weight: {2}; margin: 0; Margin-bottom: 15px;\">{1}</h{0}>",
size, text, fontWeight);
}
}
}

@ -0,0 +1,11 @@
using System.Threading.Tasks;
using Ombi.Settings.Settings.Models.Notifications;
namespace Ombi.Schedule.Jobs.Ombi
{
public interface INewsletterJob : IBaseJob
{
Task Start();
Task Start(NewsletterSettings settings, bool test);
}
}

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

@ -0,0 +1,636 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Ombi.Api.TheMovieDb;
using Ombi.Api.TheMovieDb.Models;
using Ombi.Api.TvMaze;
using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Notifications;
using Ombi.Notifications.Models;
using Ombi.Notifications.Templates;
using Ombi.Settings.Settings.Models;
using Ombi.Settings.Settings.Models.Notifications;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
namespace Ombi.Schedule.Jobs.Ombi
{
public class NewsletterJob : HtmlTemplateGenerator, INewsletterJob
{
public NewsletterJob(IPlexContentRepository plex, IEmbyContentRepository emby, IRepository<RecentlyAddedLog> addedLog,
IMovieDbApi movieApi, ITvMazeApi tvApi, IEmailProvider email, ISettingsService<CustomizationSettings> custom,
ISettingsService<EmailNotificationSettings> emailSettings, INotificationTemplatesRepository templateRepo,
UserManager<OmbiUser> um, ISettingsService<NewsletterSettings> newsletter)
{
_plex = plex;
_emby = emby;
_recentlyAddedLog = addedLog;
_movieApi = movieApi;
_tvApi = tvApi;
_email = email;
_customizationSettings = custom;
_templateRepo = templateRepo;
_emailSettings = emailSettings;
_newsletterSettings = newsletter;
_userManager = um;
_emailSettings.ClearCache();
_customizationSettings.ClearCache();
_newsletterSettings.ClearCache();
}
private readonly IPlexContentRepository _plex;
private readonly IEmbyContentRepository _emby;
private readonly IRepository<RecentlyAddedLog> _recentlyAddedLog;
private readonly IMovieDbApi _movieApi;
private readonly ITvMazeApi _tvApi;
private readonly IEmailProvider _email;
private readonly ISettingsService<CustomizationSettings> _customizationSettings;
private readonly INotificationTemplatesRepository _templateRepo;
private readonly ISettingsService<EmailNotificationSettings> _emailSettings;
private readonly ISettingsService<NewsletterSettings> _newsletterSettings;
private readonly UserManager<OmbiUser> _userManager;
public async Task Start(NewsletterSettings settings, bool test)
{
if (!settings.Enabled)
{
return;
}
var template = await _templateRepo.GetTemplate(NotificationAgent.Email, NotificationType.Newsletter);
if (!template.Enabled)
{
return;
}
var emailSettings = await _emailSettings.GetSettingsAsync();
if (!ValidateConfiguration(emailSettings))
{
return;
}
var customization = await _customizationSettings.GetSettingsAsync();
// Get the Content
var plexContent = _plex.GetAll().Include(x => x.Episodes).AsNoTracking();
var embyContent = _emby.GetAll().Include(x => x.Episodes).AsNoTracking();
var addedLog = _recentlyAddedLog.GetAll();
var addedPlexMovieLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Plex && x.ContentType == ContentType.Parent).Select(x => x.ContentId);
var addedEmbyMoviesLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Emby && x.ContentType == ContentType.Parent).Select(x => x.ContentId);
var addedPlexEpisodesLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Plex && x.ContentType == ContentType.Episode).Select(x => x.ContentId);
var addedEmbyEpisodesLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Emby && x.ContentType == ContentType.Episode).Select(x => x.ContentId);
// Filter out the ones that we haven't sent yet
var plexContentMoviesToSend = plexContent.Where(x => x.Type == PlexMediaTypeEntity.Movie && !addedPlexMovieLogIds.Contains(x.Id));
var embyContentMoviesToSend = embyContent.Where(x => x.Type == EmbyMediaType.Movie && !addedEmbyMoviesLogIds.Contains(x.Id));
var plexEpisodesToSend = _plex.GetAllEpisodes().Include(x => x.Series).Where(x => !addedPlexEpisodesLogIds.Contains(x.Id)).AsNoTracking();
var embyEpisodesToSend = _emby.GetAllEpisodes().Include(x => x.Series).Where(x => !addedEmbyEpisodesLogIds.Contains(x.Id)).AsNoTracking();
var body = string.Empty;
if (test)
{
var plexm = plexContent.Where(x => x.Type == PlexMediaTypeEntity.Movie).OrderByDescending(x => x.AddedAt).Take(10);
var embym = embyContent.Where(x => x.Type == EmbyMediaType.Movie).OrderByDescending(x => x.AddedAt).Take(10);
var plext = _plex.GetAllEpisodes().Include(x => x.Series).OrderByDescending(x => x.Series.AddedAt).Take(10);
var embyt = _emby.GetAllEpisodes().Include(x => x.Series).OrderByDescending(x => x.AddedAt).Take(10);
body = await BuildHtml(plexm, embym, plext, embyt);
}
else
{
body = await BuildHtml(plexContentMoviesToSend, embyContentMoviesToSend, plexEpisodesToSend, embyEpisodesToSend);
if (body.IsNullOrEmpty())
{
return;
}
}
if (!test)
{
// Get the users to send it to
var users = await _userManager.GetUsersInRoleAsync(OmbiRoles.RecievesNewsletter);
if (!users.Any())
{
return;
}
var emailTasks = new List<Task>();
foreach (var user in users)
{
if (user.Email.IsNullOrEmpty())
{
continue;
}
var messageContent = ParseTemplate(template, customization, user);
var email = new NewsletterTemplate();
var html = email.LoadTemplate(messageContent.Subject, messageContent.Message, body, customization.Logo);
emailTasks.Add(_email.Send(
new NotificationMessage { Message = html, Subject = messageContent.Subject, To = user.Email },
emailSettings));
}
// Now add all of this to the Recently Added log
var recentlyAddedLog = new HashSet<RecentlyAddedLog>();
foreach (var p in plexContentMoviesToSend)
{
recentlyAddedLog.Add(new RecentlyAddedLog
{
AddedAt = DateTime.Now,
Type = RecentlyAddedType.Plex,
ContentType = ContentType.Parent,
ContentId = p.Id
});
}
foreach (var p in plexEpisodesToSend)
{
recentlyAddedLog.Add(new RecentlyAddedLog
{
AddedAt = DateTime.Now,
Type = RecentlyAddedType.Plex,
ContentType = ContentType.Episode,
ContentId = p.Id
});
}
foreach (var e in embyContentMoviesToSend)
{
if (e.Type == EmbyMediaType.Movie)
{
recentlyAddedLog.Add(new RecentlyAddedLog
{
AddedAt = DateTime.Now,
Type = RecentlyAddedType.Emby,
ContentType = ContentType.Parent,
ContentId = e.Id
});
}
}
foreach (var p in embyEpisodesToSend)
{
recentlyAddedLog.Add(new RecentlyAddedLog
{
AddedAt = DateTime.Now,
Type = RecentlyAddedType.Emby,
ContentType = ContentType.Episode,
ContentId = p.Id
});
}
await _recentlyAddedLog.AddRange(recentlyAddedLog);
await Task.WhenAll(emailTasks.ToArray());
}
else
{
var admins = await _userManager.GetUsersInRoleAsync(OmbiRoles.Admin);
foreach (var a in admins)
{
if (a.Email.IsNullOrEmpty())
{
continue;
}
var messageContent = ParseTemplate(template, customization, a);
var email = new NewsletterTemplate();
var html = email.LoadTemplate(messageContent.Subject, messageContent.Message, body, customization.Logo);
await _email.Send(
new NotificationMessage { Message = html, Subject = messageContent.Subject, To = a.Email },
emailSettings);
}
}
}
public async Task Start()
{
var newsletterSettings = await _newsletterSettings.GetSettingsAsync();
await Start(newsletterSettings, false);
}
private NotificationMessageContent ParseTemplate(NotificationTemplates template, CustomizationSettings settings, OmbiUser username)
{
var resolver = new NotificationMessageResolver();
var curlys = new NotificationMessageCurlys();
curlys.SetupNewsletter(settings, username);
return resolver.ParseMessage(template, curlys);
}
private async Task<string> BuildHtml(IQueryable<PlexServerContent> plexContentToSend, IQueryable<EmbyContent> embyContentToSend, IQueryable<PlexEpisode> plexEpisodes, IQueryable<EmbyEpisode> embyEp)
{
var sb = new StringBuilder();
var plexMovies = plexContentToSend.Where(x => x.Type == PlexMediaTypeEntity.Movie);
var embyMovies = embyContentToSend.Where(x => x.Type == EmbyMediaType.Movie);
if (plexMovies.Any() || embyMovies.Any())
{
sb.Append("<h1>New Movies:</h1><br /><br />");
await ProcessPlexMovies(plexMovies, sb);
await ProcessEmbyMovies(embyMovies, sb);
}
if (plexEpisodes.Any() || embyEp.Any())
{
sb.Append("<h1>New Episodes:</h1><br /><br />");
await ProcessPlexTv(plexEpisodes, sb);
await ProcessEmbyTv(embyEp, sb);
}
return sb.ToString();
}
private async Task ProcessPlexMovies(IQueryable<PlexServerContent> plexContentToSend, StringBuilder sb)
{
sb.Append(
"<table border=\"0\" cellpadding=\"0\" align=\"center\" cellspacing=\"0\" style=\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;\" width=\"100%\">");
var ordered = plexContentToSend.OrderByDescending(x => x.AddedAt);
foreach (var content in ordered)
{
if (content.TheMovieDbId.IsNullOrEmpty())
{
// Maybe we should try the ImdbId?
if (content.ImdbId.HasValue())
{
var findResult = await _movieApi.Find(content.ImdbId, ExternalSource.imdb_id);
var movieId = findResult.movie_results?[0]?.id ?? 0;
content.TheMovieDbId = movieId.ToString();
}
}
int.TryParse(content.TheMovieDbId, out var movieDbId);
var info = await _movieApi.GetMovieInformationWithExtraInfo(movieDbId);
if (info == null)
{
continue;
}
try
{
CreateMovieHtmlContent(sb, info);
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
finally
{
EndLoopHtml(sb);
}
}
}
private async Task ProcessEmbyMovies(IQueryable<EmbyContent> embyContent, StringBuilder sb)
{
sb.Append(
"<table border=\"0\" cellpadding=\"0\" align=\"center\" cellspacing=\"0\" style=\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;\" width=\"100%\">");
var ordered = embyContent.OrderByDescending(x => x.AddedAt);
foreach (var content in ordered)
{
int.TryParse(content.ProviderId, out var movieDbId);
var info = await _movieApi.GetMovieInformationWithExtraInfo(movieDbId);
if (info == null)
{
continue;
}
try
{
CreateMovieHtmlContent(sb, info);
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
finally
{
EndLoopHtml(sb);
}
}
}
private void CreateMovieHtmlContent(StringBuilder sb, MovieResponseDto info)
{
AddImageInsideTable(sb, $"https://image.tmdb.org/t/p/original{info.PosterPath}");
sb.Append("<tr>");
TableData(sb);
Href(sb, $"https://www.imdb.com/title/{info.ImdbId}/");
var releaseDate = string.Empty;
try
{
releaseDate = $"({DateTime.Parse(info.ReleaseDate).Year})";
}
catch (Exception)
{
// Swallow, couldn't parse the date
}
Header(sb, 3, $"{info.Title} {releaseDate}");
EndTag(sb, "a");
if (info.Genres.Any())
{
AddParagraph(sb,
$"Genre: {string.Join(", ", info.Genres.Select(x => x.Name.ToString()).ToArray())}");
}
AddParagraph(sb, info.Overview);
}
private async Task ProcessPlexTv(IQueryable<PlexEpisode> plexContent, StringBuilder sb)
{
var series = new List<PlexServerContent>();
foreach (var plexEpisode in plexContent)
{
var alreadyAdded = series.FirstOrDefault(x => x.Key == plexEpisode.Series.Key);
if (alreadyAdded != null)
{
var episodeExists = alreadyAdded.Episodes.Any(x => x.Key == plexEpisode.Key);
if (!episodeExists)
{
alreadyAdded.Episodes.Add(plexEpisode);
}
}
else
{
plexEpisode.Series.Episodes = new List<PlexEpisode> { plexEpisode };
series.Add(plexEpisode.Series);
}
}
var orderedTv = series.OrderByDescending(x => x.AddedAt);
sb.Append(
"<table border=\"0\" cellpadding=\"0\" align=\"center\" cellspacing=\"0\" style=\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;\" width=\"100%\">");
foreach (var t in orderedTv)
{
try
{
if (!t.HasTvDb)
{
// We may need to use themoviedb for the imdbid or their own id to get info
if (t.HasTheMovieDb)
{
int.TryParse(t.TheMovieDbId, out var movieId);
var externals = await _movieApi.GetTvExternals(movieId);
if (externals == null || externals.tvdb_id <= 0)
{
continue;
}
t.TvDbId = externals.tvdb_id.ToString();
}
// WE could check the below but we need to get the moviedb and then perform the above, let the metadata job figure this out.
//else if(t.HasImdb)
//{
// // Check the imdbid
// var externals = await _movieApi.Find(t.ImdbId, ExternalSource.imdb_id);
// if (externals?.tv_results == null || externals.tv_results.Length <= 0)
// {
// continue;
// }
// t.TvDbId = externals.tv_results.FirstOrDefault()..ToString();
//}
}
int.TryParse(t.TvDbId, out var tvdbId);
var info = await _tvApi.ShowLookupByTheTvDbId(tvdbId);
if (info == null)
{
continue;
}
var banner = info.image?.original;
if (!string.IsNullOrEmpty(banner))
{
banner = banner.Replace("http", "https"); // Always use the Https banners
}
AddImageInsideTable(sb, banner);
sb.Append("<tr>");
sb.Append(
"<td align=\"center\" style=\"font-family: sans-serif; font-size: 14px; vertical-align: top;\" valign=\"top\">");
var title = $"{t.Title} ({t.ReleaseYear})";
Href(sb, $"https://www.imdb.com/title/{info.externals.imdb}/");
Header(sb, 3, title);
EndTag(sb, "a");
// Group by the season number
var results = t.Episodes.GroupBy(p => p.SeasonNumber,
(key, g) => new
{
SeasonNumber = key,
Episodes = g.ToList()
}
);
// Group the episodes
foreach (var epInformation in results.OrderBy(x => x.SeasonNumber))
{
var orderedEpisodes = epInformation.Episodes.OrderBy(x => x.EpisodeNumber).ToList();
var epSb = new StringBuilder();
for (var i = 0; i < orderedEpisodes.Count; i++)
{
var ep = orderedEpisodes[i];
if (i < orderedEpisodes.Count - 1)
{
epSb.Append($"{ep.EpisodeNumber},");
}
else
{
epSb.Append($"{ep.EpisodeNumber}");
}
}
AddParagraph(sb, $"Season: {epInformation.SeasonNumber}, Episode: {epSb}");
}
if (info.genres.Any())
{
AddParagraph(sb, $"Genre: {string.Join(", ", info.genres.Select(x => x.ToString()).ToArray())}");
}
AddParagraph(sb, info.summary);
}
catch (Exception e)
{
//Log.Error(e);
}
finally
{
EndLoopHtml(sb);
}
}
sb.Append("</table><br /><br />");
}
private async Task ProcessEmbyTv(IQueryable<EmbyEpisode> embyContent, StringBuilder sb)
{
var series = new List<EmbyContent>();
foreach (var episode in embyContent)
{
var alreadyAdded = series.FirstOrDefault(x => x.EmbyId == episode.Series.EmbyId);
if (alreadyAdded != null)
{
alreadyAdded.Episodes.Add(episode);
}
else
{
episode.Series.Episodes = new List<EmbyEpisode>
{
episode
};
series.Add(episode.Series);
}
}
var orderedTv = series.OrderByDescending(x => x.AddedAt);
sb.Append(
"<table border=\"0\" cellpadding=\"0\" align=\"center\" cellspacing=\"0\" style=\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;\" width=\"100%\">");
foreach (var t in orderedTv)
{
try
{
int.TryParse(t.ProviderId, out var tvdbId);
var info = await _tvApi.ShowLookupByTheTvDbId(tvdbId);
if (info == null)
{
continue;
}
var banner = info.image?.original;
if (!string.IsNullOrEmpty(banner))
{
banner = banner.Replace("http", "https"); // Always use the Https banners
}
AddImageInsideTable(sb, banner);
sb.Append("<tr>");
sb.Append(
"<td align=\"center\" style=\"font-family: sans-serif; font-size: 14px; vertical-align: top;\" valign=\"top\">");
Href(sb, $"https://www.imdb.com/title/{info.externals.imdb}/");
Header(sb, 3, t.Title);
EndTag(sb, "a");
// Group by the season number
var results = t.Episodes?.GroupBy(p => p.SeasonNumber,
(key, g) => new
{
SeasonNumber = key,
Episodes = g.ToList()
}
);
// Group the episodes
foreach (var epInformation in results.OrderBy(x => x.SeasonNumber))
{
var orderedEpisodes = epInformation.Episodes.OrderBy(x => x.EpisodeNumber).ToList();
var epSb = new StringBuilder();
for (var i = 0; i < orderedEpisodes.Count; i++)
{
var ep = orderedEpisodes[i];
if (i < orderedEpisodes.Count - 1)
{
epSb.Append($"{ep.EpisodeNumber},");
}
else
{
epSb.Append($"{ep.EpisodeNumber}");
}
}
AddParagraph(sb, $"Season: {epInformation.SeasonNumber}, Episode: {epSb}");
}
if (info.genres.Any())
{
AddParagraph(sb, $"Genre: {string.Join(", ", info.genres.Select(x => x.ToString()).ToArray())}");
}
AddParagraph(sb, info.summary);
}
catch (Exception e)
{
//Log.Error(e);
}
finally
{
EndLoopHtml(sb);
}
}
sb.Append("</table><br /><br />");
}
private void EndLoopHtml(StringBuilder sb)
{
//NOTE: BR have to be in TD's as per html spec or it will be put outside of the table...
//Source: http://stackoverflow.com/questions/6588638/phantom-br-tag-rendered-by-browsers-prior-to-table-tag
sb.Append("<hr />");
sb.Append("<br />");
sb.Append("<br />");
sb.Append("</td>");
sb.Append("</tr>");
}
protected bool ValidateConfiguration(EmailNotificationSettings settings)
{
if (!settings.Enabled)
{
return false;
}
if (settings.Authentication)
{
if (string.IsNullOrEmpty(settings.Username) || string.IsNullOrEmpty(settings.Password))
{
return false;
}
}
if (string.IsNullOrEmpty(settings.Host) || string.IsNullOrEmpty(settings.AdminEmail) || string.IsNullOrEmpty(settings.Port.ToString()))
{
return false;
}
return true;
}
private bool _disposed;
protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;
if (disposing)
{
_plex?.Dispose();
_emby?.Dispose();
_newsletterSettings?.Dispose();
_customizationSettings?.Dispose();
_emailSettings.Dispose();
_recentlyAddedLog.Dispose();
_templateRepo?.Dispose();
_userManager?.Dispose();
}
_disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
}

@ -0,0 +1,244 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Ombi.Api.TheMovieDb;
using Ombi.Api.TheMovieDb.Models;
using Ombi.Api.TvMaze;
using Ombi.Core.Settings;
using Ombi.Core.Settings.Models.External;
using Ombi.Helpers;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
using Ombi.Store.Repository.Requests;
namespace Ombi.Schedule.Jobs.Ombi
{
public class RefreshMetadata : IRefreshMetadata
{
public RefreshMetadata(IPlexContentRepository plexRepo, IEmbyContentRepository embyRepo,
ILogger<RefreshMetadata> log, ITvMazeApi tvApi, ISettingsService<PlexSettings> plexSettings,
IMovieDbApi movieApi)
{
_plexRepo = plexRepo;
_embyRepo = embyRepo;
_log = log;
_movieApi = movieApi;
_tvApi = tvApi;
_plexSettings = plexSettings;
}
private readonly IPlexContentRepository _plexRepo;
private readonly IEmbyContentRepository _embyRepo;
private readonly ILogger _log;
private readonly IMovieDbApi _movieApi;
private readonly ITvMazeApi _tvApi;
private readonly ISettingsService<PlexSettings> _plexSettings;
public async Task Start()
{
_log.LogInformation("Starting the Metadata refresh");
try
{
var settings = await _plexSettings.GetSettingsAsync();
if (settings.Enable)
{
await StartPlex();
}
}
catch (Exception e)
{
_log.LogError(e, "Exception when refreshing the Plex Metadata");
throw;
}
}
private async Task StartPlex()
{
await StartPlexMovies();
// Now Tv
await StartPlexTv();
}
private async Task StartPlexTv()
{
var allTv = _plexRepo.GetAll().Where(x =>
x.Type == PlexMediaTypeEntity.Show && (!x.TheMovieDbId.HasValue() || !x.ImdbId.HasValue() || !x.TvDbId.HasValue()));
var tvCount = 0;
foreach (var show in allTv)
{
var hasImdb = show.ImdbId.HasValue();
var hasTheMovieDb = show.TheMovieDbId.HasValue();
var hasTvDbId = show.TvDbId.HasValue();
if (!hasTheMovieDb)
{
var id = await GetTheMovieDbId(hasTvDbId, hasImdb, show.TvDbId, show.ImdbId, show.Title);
show.TheMovieDbId = id;
}
if (!hasImdb)
{
var id = await GetImdbId(hasTheMovieDb, hasTvDbId, show.Title, show.TheMovieDbId, show.TvDbId);
show.ImdbId = id;
_plexRepo.UpdateWithoutSave(show);
}
if (!hasTvDbId)
{
var id = await GetTvDbId(hasTheMovieDb, hasImdb, show.TheMovieDbId, show.ImdbId, show.Title);
show.TvDbId = id;
_plexRepo.UpdateWithoutSave(show);
}
tvCount++;
if (tvCount >= 20)
{
await _plexRepo.SaveChangesAsync();
tvCount = 0;
}
}
await _plexRepo.SaveChangesAsync();
}
private async Task StartPlexMovies()
{
var allMovies = _plexRepo.GetAll().Where(x =>
x.Type == PlexMediaTypeEntity.Movie && (!x.TheMovieDbId.HasValue() || !x.ImdbId.HasValue()));
int movieCount = 0;
foreach (var movie in allMovies)
{
var hasImdb = movie.ImdbId.HasValue();
var hasTheMovieDb = movie.TheMovieDbId.HasValue();
// Movies don't really use TheTvDb
if (!hasImdb)
{
var imdbId = await GetImdbId(hasTheMovieDb, false, movie.Title, movie.TheMovieDbId, string.Empty);
movie.ImdbId = imdbId;
_plexRepo.UpdateWithoutSave(movie);
}
if (!hasTheMovieDb)
{
var id = await GetTheMovieDbId(false, hasImdb, string.Empty, movie.ImdbId, movie.Title);
movie.TheMovieDbId = id;
_plexRepo.UpdateWithoutSave(movie);
}
movieCount++;
if (movieCount >= 20)
{
await _plexRepo.SaveChangesAsync();
movieCount = 0;
}
}
await _plexRepo.SaveChangesAsync();
}
private async Task<string> GetTheMovieDbId(bool hasTvDbId, bool hasImdb, string tvdbID, string imdbId, string title)
{
_log.LogInformation("The Media item {0} does not have a TheMovieDbId, searching for TheMovieDbId", title);
FindResult result = null;
var hasResult = false;
if (hasTvDbId)
{
result = await _movieApi.Find(tvdbID, ExternalSource.tvdb_id);
hasResult = result?.tv_results?.Length > 0;
_log.LogInformation("Setting Show {0} because we have TvDbId, result: {1}", title, hasResult);
}
if (hasImdb && !hasResult)
{
result = await _movieApi.Find(imdbId, ExternalSource.imdb_id);
hasResult = result?.tv_results?.Length > 0;
_log.LogInformation("Setting Show {0} because we have ImdbId, result: {1}", title, hasResult);
}
if (hasResult)
{
return result.tv_results?[0]?.id.ToString() ?? string.Empty;
}
return string.Empty;
}
private async Task<string> GetImdbId(bool hasTheMovieDb, bool hasTvDbId, string title, string theMovieDbId, string tvDbId)
{
_log.LogInformation("The media item {0} does not have a ImdbId, searching for ImdbId", title);
// Looks like TV Maze does not provide the moviedb id, neither does the TV endpoint on TheMovieDb
if (hasTheMovieDb)
{
_log.LogInformation("The show {0} has TheMovieDbId but not ImdbId, searching for ImdbId", title);
if (int.TryParse(theMovieDbId, out var id))
{
var result = await _movieApi.GetTvExternals(id);
return result.imdb_id;
}
}
if (hasTvDbId)
{
_log.LogInformation("The show {0} has tvdbid but not ImdbId, searching for ImdbId", title);
if (int.TryParse(tvDbId, out var id))
{
var result = await _tvApi.ShowLookupByTheTvDbId(id);
return result?.externals?.imdb;
}
}
return string.Empty;
}
private async Task<string> GetTvDbId(bool hasTheMovieDb, bool hasImdb, string theMovieDbId, string imdbId, string title)
{
_log.LogInformation("The media item {0} does not have a TvDbId, searching for TvDbId", title);
if (hasTheMovieDb)
{
_log.LogInformation("The show {0} has theMovieDBId but not ImdbId, searching for ImdbId", title);
if (int.TryParse(theMovieDbId, out var id))
{
var result = await _movieApi.GetTvExternals(id);
return result.tvdb_id.ToString();
}
}
if (hasImdb)
{
_log.LogInformation("The show {0} has ImdbId but not ImdbId, searching for ImdbId", title);
var result = await _movieApi.Find(imdbId, ExternalSource.imdb_id);
if (result?.tv_results?.Length > 0)
{
var movieId = result.tv_results?[0]?.id ?? 0;
var externalResult = await _movieApi.GetTvExternals(movieId);
return externalResult.imdb_id;
}
}
return string.Empty;
}
private bool _disposed;
protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;
if (disposing)
{
_plexRepo?.Dispose();
_embyRepo?.Dispose();
_plexSettings?.Dispose();
}
_disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
}

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

@ -46,7 +46,8 @@ namespace Ombi.Schedule.Processor
if (masterBranch)
{
latestRelease = doc.DocumentNode.Descendants("h2")
.FirstOrDefault(x => x.InnerText != "(unreleased)");
.FirstOrDefault(x => x.InnerText == "(unreleased)");
// TODO: Change this to InnterText != "(unreleased)" once we go live and it's not a prerelease
}
else
{
@ -78,9 +79,9 @@ namespace Ombi.Schedule.Processor
Downloads = new List<Downloads>()
};
var releaseTag = latestRelease.InnerText.Substring(0, 6);
if (masterBranch)
{
var releaseTag = latestRelease.InnerText.Substring(0, 9);
await GetGitubRelease(release, releaseTag);
}
else
@ -147,7 +148,7 @@ namespace Ombi.Schedule.Processor
var builds = await _api.Request<AppveyorBranchResult>(request);
var jobId = builds.build.jobs.FirstOrDefault()?.jobId ?? string.Empty;
if (builds.build.finished == DateTime.MinValue)
if (builds.build.finished == DateTime.MinValue || builds.build.status.Equals("failed"))
{
return;
}

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

@ -10,5 +10,7 @@
public string AutomaticUpdater { get; set; }
public string UserImporter { get; set; }
public string SickRageSync { get; set; }
public string RefreshMetadata { get; set; }
public string Newsletter { get; set; }
}
}

@ -1,4 +1,5 @@
using Ombi.Helpers;
using System;
using Ombi.Helpers;
namespace Ombi.Settings.Settings.Models
{
@ -35,10 +36,18 @@ namespace Ombi.Settings.Settings.Models
{
return Get(s.UserImporter, Cron.Daily());
}
public static string Newsletter(JobSettings s)
{
return Get(s.Newsletter, Cron.Weekly(DayOfWeek.Friday, 12));
}
public static string SickRageSync(JobSettings s)
{
return Get(s.SickRageSync, Cron.Hourly(35));
}
public static string RefreshMetadata(JobSettings s)
{
return Get(s.RefreshMetadata, Cron.Daily(3));
}
private static string Get(string settings, string defaultCron)

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

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

@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Linq;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Ombi.Helpers;
@ -15,7 +16,7 @@ namespace Ombi.Store.Context
public OmbiContext()
{
if (_created) return;
_created = true;
Database.Migrate();
}
@ -37,6 +38,7 @@ namespace Ombi.Store.Context
public DbSet<IssueCategory> IssueCategories { get; set; }
public DbSet<IssueComments> IssueComments { get; set; }
public DbSet<RequestLog> RequestLogs { get; set; }
public DbSet<RecentlyAddedLog> RecentlyAddedLogs { get; set; }
public DbSet<Audit> Audit { get; set; }
@ -55,7 +57,7 @@ namespace Ombi.Store.Context
{
i.StoragePath = string.Empty;
}
optionsBuilder.UseSqlite($"Data Source={Path.Combine(i.StoragePath,"Ombi.db")}");
optionsBuilder.UseSqlite($"Data Source={Path.Combine(i.StoragePath, "Ombi.db")}");
}
protected override void OnModelCreating(ModelBuilder builder)
@ -70,7 +72,7 @@ namespace Ombi.Store.Context
.WithMany(b => b.Episodes)
.HasPrincipalKey(x => x.EmbyId)
.HasForeignKey(p => p.ParentId);
base.OnModelCreating(builder);
}
@ -113,6 +115,15 @@ namespace Ombi.Store.Context
// VACUUM;
Database.ExecuteSqlCommand("VACUUM;");
// Make sure we have the roles
var roles = Roles.Where(x => x.Name == OmbiRoles.RecievesNewsletter);
if (!roles.Any())
{
Roles.Add(new IdentityRole(OmbiRoles.RecievesNewsletter)
{
NormalizedName = OmbiRoles.RecievesNewsletter.ToUpper()
});
}
//Check if templates exist
var templates = NotificationTemplates.ToList();
@ -135,7 +146,7 @@ namespace Ombi.Store.Context
notificationToAdd = new NotificationTemplates
{
NotificationType = notificationType,
Message = "Hello! The user '{RequestedUser}' has requested the {Type} '{Title}'! Please log in to approve this request. Request Date: {RequestedDate}",
Message = "Hello! The user '{UserName}' has requested the {Type} '{Title}'! Please log in to approve this request. Request Date: {RequestedDate}",
Subject = "{ApplicationName}: New {Type} request for {Title}!",
Agent = agent,
Enabled = true,
@ -145,7 +156,7 @@ namespace Ombi.Store.Context
notificationToAdd = new NotificationTemplates
{
NotificationType = notificationType,
Message = "Hello! The user '{IssueUser}' has reported a new issue for the title {Title}! </br> {IssueCategory} - {IssueSubject} : {IssueDescription}",
Message = "Hello! The user '{UserName}' has reported a new issue for the title {Title}! </br> {IssueCategory} - {IssueSubject} : {IssueDescription}",
Subject = "{ApplicationName}: New issue for {Title}!",
Agent = agent,
Enabled = true,
@ -155,7 +166,7 @@ namespace Ombi.Store.Context
notificationToAdd = new NotificationTemplates
{
NotificationType = notificationType,
Message = "Hello! You requested {Title} on {ApplicationName}! This is now available! :)",
Message = "Hello! You {Title} on {ApplicationName}! This is now available! :)",
Subject = "{ApplicationName}: {Title} is now available!",
Agent = agent,
Enabled = true,
@ -199,7 +210,7 @@ namespace Ombi.Store.Context
notificationToAdd = new NotificationTemplates
{
NotificationType = notificationType,
Message = "Hello {IssueUser} Your issue for {Title} has now been resolved.",
Message = "Hello {UserName} Your issue for {Title} has now been resolved.",
Subject = "{ApplicationName}: Issue has been resolved for {Title}!",
Agent = agent,
Enabled = true,
@ -218,6 +229,16 @@ namespace Ombi.Store.Context
break;
case NotificationType.AdminNote:
continue;
case NotificationType.Newsletter:
notificationToAdd = new NotificationTemplates
{
NotificationType = notificationType,
Message = "Here is a list of Movies and TV Shows that have recently been added!",
Subject = "{ApplicationName}: Recently Added Content!",
Agent = agent,
Enabled = true,
};
break;
default:
throw new ArgumentOutOfRangeException();
}

@ -56,6 +56,15 @@ namespace Ombi.Store.Entities
public int Key { get; set; }
public DateTime AddedAt { get; set; }
public string Quality { get; set; }
[NotMapped]
public bool HasImdb => !string.IsNullOrEmpty(ImdbId);
[NotMapped]
public bool HasTvDb => !string.IsNullOrEmpty(TvDbId);
[NotMapped]
public bool HasTheMovieDb => !string.IsNullOrEmpty(TheMovieDbId);
}
[Table("PlexSeasonsContent")]

@ -0,0 +1,26 @@
using System;
using System.ComponentModel.DataAnnotations.Schema;
namespace Ombi.Store.Entities
{
[Table("RecentlyAddedLog")]
public class RecentlyAddedLog : Entity
{
public RecentlyAddedType Type { get; set; }
public ContentType ContentType { get; set; }
public int ContentId { get; set; } // This is dependant on the type
public DateTime AddedAt { get; set; }
}
public enum RecentlyAddedType
{
Plex = 0,
Emby = 1
}
public enum ContentType
{
Parent = 0,
Episode = 1
}
}

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

@ -0,0 +1,34 @@
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
namespace Ombi.Store.Migrations
{
public partial class RecentlyAddedLog : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "RecentlyAddedLog",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("Sqlite:Autoincrement", true),
AddedAt = table.Column<DateTime>(nullable: false),
ContentId = table.Column<int>(nullable: false),
ContentType = table.Column<int>(nullable: false),
Type = table.Column<int>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_RecentlyAddedLog", x => x.Id);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "RecentlyAddedLog");
}
}
}

@ -20,7 +20,7 @@ namespace Ombi.Store.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.0.0-rtm-26452");
.HasAnnotation("ProductVersion", "2.0.2-rtm-10011");
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
@ -430,6 +430,24 @@ namespace Ombi.Store.Migrations
b.ToTable("RadarrCache");
});
modelBuilder.Entity("Ombi.Store.Entities.RecentlyAddedLog", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("AddedAt");
b.Property<int>("ContentId");
b.Property<int>("ContentType");
b.Property<int>("Type");
b.HasKey("Id");
b.ToTable("RecentlyAddedLog");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.ChildRequests", b =>
{
b.Property<int>("Id")

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

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

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

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

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

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query;
using Ombi.Store.Entities;
@ -24,5 +25,6 @@ namespace Ombi.Store.Repository
where TEntity : class;
Task ExecuteSql(string sql);
DbSet<T> _db { get; }
}
}

@ -23,14 +23,14 @@ namespace Ombi.Store.Repository
return Db.NotificationTemplates.AsQueryable();
}
public async Task<IEnumerable<NotificationTemplates>> GetAllTemplates()
public IQueryable<NotificationTemplates> GetAllTemplates()
{
return await Db.NotificationTemplates.ToListAsync();
return Db.NotificationTemplates;
}
public async Task<IEnumerable<NotificationTemplates>> GetAllTemplates(NotificationAgent agent)
public IQueryable<NotificationTemplates> GetAllTemplates(NotificationAgent agent)
{
return await Db.NotificationTemplates.Where(x => x.Agent == agent).ToListAsync();
return Db.NotificationTemplates.Where(x => x.Agent == agent);
}
public async Task<NotificationTemplates> GetTemplate(NotificationAgent agent, NotificationType type)
@ -40,6 +40,11 @@ namespace Ombi.Store.Repository
public async Task Update(NotificationTemplates template)
{
if (Db.Entry(template).State == EntityState.Detached)
{
Db.Attach(template);
Db.Entry(template).State = EntityState.Modified;
}
await Db.SaveChangesAsync();
}
@ -60,5 +65,26 @@ namespace Ombi.Store.Repository
await Db.SaveChangesAsync().ConfigureAwait(false);
return settings.Entity;
}
private bool _disposed;
// Protected implementation of Dispose pattern.
protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;
if (disposing)
{
Db?.Dispose();
}
_disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
}

@ -97,6 +97,16 @@ namespace Ombi.Store.Repository
Db.PlexServerContent.Update(existingContent);
await Db.SaveChangesAsync();
}
public void UpdateWithoutSave(PlexServerContent existingContent)
{
Db.PlexServerContent.Update(existingContent);
}
public async Task UpdateRange(IEnumerable<PlexServerContent> existingContent)
{
Db.PlexServerContent.UpdateRange(existingContent);
await Db.SaveChangesAsync();
}
public IQueryable<PlexEpisode> GetAllEpisodes()
{

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

@ -7,13 +7,12 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="2.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="2.0.2" />
<PackageReference Include="Moq" Version="4.7.99" />
<PackageReference Include="Nunit" Version="3.8.1" />
<PackageReference Include="NUnit.ConsoleRunner" Version="3.7.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>

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

@ -0,0 +1,52 @@
namespace Ombi.Api.TheMovieDb.Models
{
public class FindResult
{
public Movie_Results[] movie_results { get; set; }
public object[] person_results { get; set; }
public TvResults[] tv_results { get; set; }
public object[] tv_episode_results { get; set; }
public object[] tv_season_results { get; set; }
}
public class Movie_Results
{
public bool adult { get; set; }
public string backdrop_path { get; set; }
public int[] genre_ids { get; set; }
public int id { get; set; }
public string original_language { get; set; }
public string original_title { get; set; }
public string overview { get; set; }
public string poster_path { get; set; }
public string release_date { get; set; }
public string title { get; set; }
public bool video { get; set; }
public float vote_average { get; set; }
public int vote_count { get; set; }
}
public class TvResults
{
public string original_name { get; set; }
public int id { get; set; }
public string name { get; set; }
public int vote_count { get; set; }
public float vote_average { get; set; }
public string first_air_date { get; set; }
public string poster_path { get; set; }
public int[] genre_ids { get; set; }
public string original_language { get; set; }
public string backdrop_path { get; set; }
public string overview { get; set; }
public string[] origin_country { get; set; }
}
public enum ExternalSource
{
imdb_id,
tvdb_id
}
}

@ -0,0 +1,16 @@
namespace Ombi.Api.TheMovieDb.Models
{
public class TvExternals
{
public string imdb_id { get; set; }
public string freebase_mid { get; set; }
public string freebase_id { get; set; }
public int tvdb_id { get; set; }
public int tvrage_id { get; set; }
public string facebook_id { get; set; }
public object instagram_id { get; set; }
public object twitter_id { get; set; }
public int id { get; set; }
}
}

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using AutoMapper;
@ -25,15 +26,37 @@ namespace Ombi.Api.TheMovieDb
{
var request = new Request($"movie/{movieId}", BaseUri, HttpMethod.Get);
request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken);
AddRetry(request);
var result = await Api.Request<MovieResponse>(request);
return Mapper.Map<MovieResponseDto>(result);
}
public async Task<FindResult> Find(string externalId, ExternalSource source)
{
var request = new Request($"find/{externalId}", BaseUri, HttpMethod.Get);
request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken);
AddRetry(request);
request.AddQueryString("external_source", source.ToString());
return await Api.Request<FindResult>(request);
}
public async Task<TvExternals> GetTvExternals(int theMovieDbId)
{
var request = new Request($"/tv/{theMovieDbId}/external_ids", BaseUri, HttpMethod.Get);
request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken);
AddRetry(request);
return await Api.Request<TvExternals>(request);
}
public async Task<List<MovieSearchResult>> SimilarMovies(int movieId)
{
var request = new Request($"movie/{movieId}/similar", BaseUri, HttpMethod.Get);
request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken);
AddRetry(request);
var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request);
return Mapper.Map<List<MovieSearchResult>>(result.results);
@ -44,6 +67,7 @@ namespace Ombi.Api.TheMovieDb
var request = new Request($"movie/{movieId}", BaseUri, HttpMethod.Get);
request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken);
request.FullUri = request.FullUri.AddQueryParameter("append_to_response", "videos,release_dates");
AddRetry(request);
var result = await Api.Request<MovieResponse>(request);
return Mapper.Map<MovieResponseDto>(result);
}
@ -53,6 +77,7 @@ namespace Ombi.Api.TheMovieDb
var request = new Request($"search/movie", BaseUri, HttpMethod.Get);
request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken);
request.FullUri = request.FullUri.AddQueryParameter("query", searchTerm);
AddRetry(request);
var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request);
return Mapper.Map<List<MovieSearchResult>>(result.results);
@ -62,6 +87,7 @@ namespace Ombi.Api.TheMovieDb
{
var request = new Request($"movie/popular", BaseUri, HttpMethod.Get);
request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken);
AddRetry(request);
var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request);
return Mapper.Map<List<MovieSearchResult>>(result.results);
}
@ -70,6 +96,7 @@ namespace Ombi.Api.TheMovieDb
{
var request = new Request($"movie/top_rated", BaseUri, HttpMethod.Get);
request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken);
AddRetry(request);
var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request);
return Mapper.Map<List<MovieSearchResult>>(result.results);
}
@ -78,6 +105,7 @@ namespace Ombi.Api.TheMovieDb
{
var request = new Request($"movie/upcoming", BaseUri, HttpMethod.Get);
request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken);
AddRetry(request);
var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request);
return Mapper.Map<List<MovieSearchResult>>(result.results);
}
@ -86,9 +114,14 @@ namespace Ombi.Api.TheMovieDb
{
var request = new Request($"movie/now_playing", BaseUri, HttpMethod.Get);
request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken);
AddRetry(request);
var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request);
return Mapper.Map<List<MovieSearchResult>>(result.results);
}
private static void AddRetry(Request request)
{
request.Retry = true;
request.StatusCodeToRetry.Add((HttpStatusCode)429);
}
}
}

@ -12,14 +12,14 @@
<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.1.1-beta" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="2.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="2.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="2.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="2.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="2.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="2.0.1" />
<PackageReference Include="Serilog" Version="2.6.0-dev-00892" />
<PackageReference Include="Serilog.Extensions.Logging" Version="2.0.2" />
<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>
</li>
</ul>
<div *ngIf="customizationSettings">
<ul *ngIf="customizationSettings.recentlyAddedPage" class="nav navbar-nav">
<li id="RecentlyAdded" [routerLinkActive]="['active']">
<a [routerLink]="['/recentlyadded']">
<i class="fa fa-check"></i> {{ 'NavigationBar.RecentlyAdded' | translate }}</a>
</li>
</ul>
</div>
<ul *ngIf="issuesEnabled" class="nav navbar-nav">
<li id="Issues" [routerLinkActive]="['active']">
<a [routerLink]="['/issues']">
@ -46,6 +54,7 @@
<i class="fa fa-user"></i> {{ 'NavigationBar.UserManagement' | translate }}</a>
</li>
</ul>
<ul *ngIf="hasRole('Admin') || hasRole('PowerUser')" class="nav navbar-nav">
<li>
<a href="https://www.paypal.me/PlexRequestsNet" target="_blank" pTooltip="{{ 'NavigationBar.DonateTooltip' | translate }}">
@ -84,7 +93,7 @@
<a [routerLink]="['/usermanagement/updatedetails']">
<i class="fa fa-key"></i>{{ 'NavigationBar.UpdateDetails' | translate }}</a>
</li>
<li *ngIf="showMobileLink" [routerLinkActive]="['active']">
<li *ngIf="customizationSettings?.mobile" [routerLinkActive]="['active']">
<a href="#" (click)="openMobileApp($event)">
<i class="fa fa-mobile"></i>{{ 'NavigationBar.OpenMobileApp' | translate }}</a>
</li>

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save