Merge pull request #2 from tidusjar/develop

Getting Develop up to date
pull/2118/head
Anojh Thayaparan 7 years ago committed by GitHub
commit 8a94f967a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -4,14 +4,68 @@
### **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**
- Fixed #2074 and #2079. [Jamie]
- 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)
@ -19,6 +73,10 @@
- Updated the .Net core dependancies #2072. [Jamie]
### **Fixes**
- Delete Ombi.testdb. [Jamie]
## v3.0.3020 (2018-03-13)

@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Ombi.Core.Models.Requests;
using Ombi.Core.Models.Search;
using Ombi.Store.Entities.Requests;
@ -9,7 +10,7 @@ namespace Ombi.Core.Engine.Interfaces
{
Task RemoveTvRequest(int requestId);
Task<RequestEngineResult> RequestTvShow(SearchTvShowViewModel tv);
Task<RequestEngineResult> RequestTvShow(TvRequestViewModel tv);
Task<RequestEngineResult> DenyChildRequest(int requestId);
Task<IEnumerable<TvRequests>> SearchTvRequest(string search);
Task<IEnumerable<TreeNode<TvRequests, List<ChildRequests>>>> SearchTvRequestTree(string search);

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

@ -43,13 +43,13 @@ namespace Ombi.Core.Engine
private IAuditRepository Audit { get; }
private readonly IRepository<RequestLog> _requestLog;
public async Task<RequestEngineResult> RequestTvShow(SearchTvShowViewModel tv)
public async Task<RequestEngineResult> RequestTvShow(TvRequestViewModel tv)
{
var user = await GetUser();
var tvBuilder = new TvShowRequestBuilder(TvApi);
(await tvBuilder
.GetShowInfo(tv.Id))
.GetShowInfo(tv.TvDbId))
.CreateTvList(tv)
.CreateChild(tv, user.Id);
@ -78,9 +78,9 @@ namespace Ombi.Core.Engine
}
}
await Audit.Record(AuditType.Added, AuditArea.TvRequest, $"Added Request {tv.Title}", Username);
await Audit.Record(AuditType.Added, AuditArea.TvRequest, $"Added Request {tvBuilder.ChildRequest.Title}", Username);
var existingRequest = await TvRepository.Get().FirstOrDefaultAsync(x => x.TvDbId == tv.Id);
var existingRequest = await TvRepository.Get().FirstOrDefaultAsync(x => x.TvDbId == tv.TvDbId);
if (existingRequest != null)
{
// Remove requests we already have, we just want new ones
@ -127,7 +127,7 @@ namespace Ombi.Core.Engine
var newRequest = tvBuilder.CreateNewRequest(tv);
return await AddRequest(newRequest.NewRequest);
}
public async Task<IEnumerable<TvRequests>> GetRequests(int count, int position)
{
var shouldHide = await HideFromOtherUsers();

@ -4,6 +4,7 @@ using System.Linq;
using System.Threading.Tasks;
using Ombi.Api.TvMaze;
using Ombi.Api.TvMaze.Models;
using Ombi.Core.Models.Requests;
using Ombi.Core.Models.Search;
using Ombi.Helpers;
using Ombi.Store.Entities;
@ -23,7 +24,7 @@ namespace Ombi.Core.Helpers
private ITvMazeApi TvApi { get; }
public ChildRequests ChildRequest { get; set; }
public List<SeasonRequests> TvRequests { get; protected set; }
public List<SeasonsViewModel> TvRequests { get; protected set; }
public string PosterPath { get; protected set; }
public DateTime FirstAir { get; protected set; }
public TvRequests NewRequest { get; protected set; }
@ -33,7 +34,7 @@ namespace Ombi.Core.Helpers
{
ShowInfo = await TvApi.ShowLookupByTheTvDbId(id);
DateTime.TryParse(ShowInfo.premiered, out DateTime dt);
DateTime.TryParse(ShowInfo.premiered, out var dt);
FirstAir = dt;
@ -43,37 +44,29 @@ namespace Ombi.Core.Helpers
return this;
}
public TvShowRequestBuilder CreateChild(SearchTvShowViewModel model, string userId)
public TvShowRequestBuilder CreateChild(TvRequestViewModel model, string userId)
{
ChildRequest = new ChildRequests
{
Id = model.Id,
Id = model.TvDbId,
RequestType = RequestType.TvShow,
RequestedDate = DateTime.UtcNow,
Approved = false,
RequestedUserId = userId,
SeasonRequests = new List<SeasonRequests>(),
Title = model.Title,
Title = ShowInfo.name,
SeriesType = ShowInfo.type.Equals("Animation", StringComparison.CurrentCultureIgnoreCase) ? SeriesType.Anime : SeriesType.Standard
};
return this;
}
public TvShowRequestBuilder CreateTvList(SearchTvShowViewModel tv)
public TvShowRequestBuilder CreateTvList(TvRequestViewModel tv)
{
TvRequests = new List<SeasonRequests>();
TvRequests = new List<SeasonsViewModel>();
// Only have the TV requests we actually requested and not everything
foreach (var season in tv.SeasonRequests)
foreach (var season in tv.Seasons)
{
for (int i = season.Episodes.Count - 1; i >= 0; i--)
{
if (!season.Episodes[i].Requested)
{
season.Episodes.RemoveAt(i); // Remove the episode since it's not requested
}
}
if (season.Episodes.Any())
{
TvRequests.Add(season);
@ -81,11 +74,10 @@ namespace Ombi.Core.Helpers
}
return this;
}
public async Task<TvShowRequestBuilder> BuildEpisodes(SearchTvShowViewModel tv)
public async Task<TvShowRequestBuilder> BuildEpisodes(TvRequestViewModel tv)
{
if (tv.RequestAll)
{
@ -173,26 +165,68 @@ namespace Ombi.Core.Helpers
else
{
// It's a custom request
ChildRequest.SeasonRequests = TvRequests;
var seasonRequests = new List<SeasonRequests>();
var episodes = await TvApi.EpisodeLookup(ShowInfo.id);
foreach (var ep in episodes)
{
var existingSeasonRequest = seasonRequests.FirstOrDefault(x => x.SeasonNumber == ep.season);
if (existingSeasonRequest != null)
{
var requestedSeason = tv.Seasons.FirstOrDefault(x => x.SeasonNumber == ep.season);
var requestedEpisode = requestedSeason?.Episodes?.Any(x => x.EpisodeNumber == ep.number) ?? false;
if (requestedSeason != null && requestedEpisode)
{
// We already have this, let's just add the episodes to it
existingSeasonRequest.Episodes.Add(new EpisodeRequests
{
EpisodeNumber = ep.number,
AirDate = FormatDate(ep.airdate),
Title = ep.name,
Url = ep.url,
});
}
}
else
{
var newRequest = new SeasonRequests {SeasonNumber = ep.season};
var requestedSeason = tv.Seasons.FirstOrDefault(x => x.SeasonNumber == ep.season);
var requestedEpisode = requestedSeason?.Episodes?.Any(x => x.EpisodeNumber == ep.number) ?? false;
if (requestedSeason != null && requestedEpisode)
{
newRequest.Episodes.Add(new EpisodeRequests
{
EpisodeNumber = ep.number,
AirDate = FormatDate(ep.airdate),
Title = ep.name,
Url = ep.url,
});
seasonRequests.Add(newRequest);
}
}
}
foreach (var s in seasonRequests)
{
ChildRequest.SeasonRequests.Add(s);
}
}
return this;
}
public TvShowRequestBuilder CreateNewRequest(SearchTvShowViewModel tv)
public TvShowRequestBuilder CreateNewRequest(TvRequestViewModel tv)
{
NewRequest = new TvRequests
{
Id = tv.Id,
Overview = ShowInfo.summary.RemoveHtml(),
PosterPath = PosterPath,
Title = ShowInfo.name,
ReleaseDate = FirstAir,
Status = ShowInfo.status,
ImdbId = ShowInfo.externals?.imdb ?? string.Empty,
TvDbId = tv.Id,
TvDbId = tv.TvDbId,
ChildRequests = new List<ChildRequests>(),
TotalSeasons = tv.SeasonRequests.Count()
TotalSeasons = tv.Seasons.Count()
};
NewRequest.ChildRequests.Add(ChildRequest);

@ -0,0 +1,25 @@
using System.Collections.Generic;
namespace Ombi.Core.Models.Requests
{
public class TvRequestViewModel
{
public bool RequestAll { get; set; }
public bool LatestSeason { get; set; }
public bool FirstSeason { get; set; }
public int TvDbId { get; set; }
public List<SeasonsViewModel> Seasons { get; set; } = new List<SeasonsViewModel>();
}
public class SeasonsViewModel
{
public int SeasonNumber { get; set; }
public List<EpisodesViewModel> Episodes { get; set; } = new List<EpisodesViewModel>();
}
public class EpisodesViewModel
{
public int EpisodeNumber { get; set; }
}
}

@ -151,7 +151,7 @@
<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;">Here is a list of Movies and TV Shows that have recently been added!</p>
<p style="font-family: sans-serif; font-size: 20px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{@INTRO}</p>
</td>
</tr>

@ -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();
@ -43,6 +46,8 @@ namespace Ombi.Notifications
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)
@ -59,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();
@ -71,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)
@ -88,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; }
@ -144,6 +184,7 @@ namespace Ombi.Notifications
{nameof(NewIssueComment),NewIssueComment},
{nameof(IssueUser),IssueUser},
{nameof(UserName),UserName},
{nameof(Alias),Alias},
};
}
}

@ -76,8 +76,8 @@ namespace Ombi.Schedule.Jobs.Ombi
var customization = await _customizationSettings.GetSettingsAsync();
// Get the Content
var plexContent = _plex.GetAll().Include(x => x.Episodes);
var embyContent = _emby.GetAll().Include(x => x.Episodes);
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);
@ -90,24 +90,21 @@ namespace Ombi.Schedule.Jobs.Ombi
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 plexContentTvToSend = plexContent.Where(x => x.Type == PlexMediaTypeEntity.Show && x.Episodes.Any(e => !addedPlexEpisodesLogIds.Contains(e.Id)));
var embyContentTvToSend = embyContent.Where(x => x.Type == EmbyMediaType.Series && x.Episodes.Any(e => !addedEmbyEpisodesLogIds.Contains(e.Id)));
var plexContentToSend = plexContentMoviesToSend.Union(plexContentTvToSend);
var embyContentToSend = embyContentMoviesToSend.Union(embyContentTvToSend);
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 = plexContent.Where(x => x.Type == PlexMediaTypeEntity.Show).OrderByDescending(x => x.AddedAt).Take(10);
var embyt = embyContent.Where(x => x.Type == EmbyMediaType.Series).OrderByDescending(x => x.AddedAt).Take(10);
body = await BuildHtml(plexm.Union(plext), embym.Union(embyt));
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(plexContentToSend, embyContentToSend);
body = await BuildHtml(plexContentMoviesToSend, embyContentMoviesToSend, plexEpisodesToSend, embyEpisodesToSend);
if (body.IsNullOrEmpty())
{
return;
@ -137,7 +134,7 @@ namespace Ombi.Schedule.Jobs.Ombi
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},
new NotificationMessage { Message = html, Subject = messageContent.Subject, To = user.Email },
emailSettings));
}
@ -145,28 +142,25 @@ namespace Ombi.Schedule.Jobs.Ombi
var recentlyAddedLog = new HashSet<RecentlyAddedLog>();
foreach (var p in plexContentMoviesToSend)
{
if (p.Type == PlexMediaTypeEntity.Movie)
recentlyAddedLog.Add(new RecentlyAddedLog
{
recentlyAddedLog.Add(new RecentlyAddedLog
{
AddedAt = DateTime.Now,
Type = RecentlyAddedType.Plex,
ContentId = p.Id
});
}
else
AddedAt = DateTime.Now,
Type = RecentlyAddedType.Plex,
ContentType = ContentType.Parent,
ContentId = p.Id
});
}
foreach (var p in plexEpisodesToSend)
{
recentlyAddedLog.Add(new RecentlyAddedLog
{
// Add the episodes
foreach (var ep in p.Episodes)
{
recentlyAddedLog.Add(new RecentlyAddedLog
{
AddedAt = DateTime.Now,
Type = RecentlyAddedType.Plex,
ContentId = ep.Id
});
}
}
AddedAt = DateTime.Now,
Type = RecentlyAddedType.Plex,
ContentType = ContentType.Episode,
ContentId = p.Id
});
}
foreach (var e in embyContentMoviesToSend)
@ -177,22 +171,21 @@ namespace Ombi.Schedule.Jobs.Ombi
{
AddedAt = DateTime.Now,
Type = RecentlyAddedType.Emby,
ContentType = ContentType.Parent,
ContentId = e.Id
});
}
else
}
foreach (var p in embyEpisodesToSend)
{
recentlyAddedLog.Add(new RecentlyAddedLog
{
// Add the episodes
foreach (var ep in e.Episodes)
{
recentlyAddedLog.Add(new RecentlyAddedLog
{
AddedAt = DateTime.Now,
Type = RecentlyAddedType.Plex,
ContentId = ep.Id
});
}
}
AddedAt = DateTime.Now,
Type = RecentlyAddedType.Emby,
ContentType = ContentType.Episode,
ContentId = p.Id
});
}
await _recentlyAddedLog.AddRange(recentlyAddedLog);
@ -212,7 +205,7 @@ namespace Ombi.Schedule.Jobs.Ombi
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);
@ -236,7 +229,7 @@ namespace Ombi.Schedule.Jobs.Ombi
return resolver.ParseMessage(template, curlys);
}
private async Task<string> BuildHtml(IQueryable<PlexServerContent> plexContentToSend, IQueryable<EmbyContent> embyContentToSend)
private async Task<string> BuildHtml(IQueryable<PlexServerContent> plexContentToSend, IQueryable<EmbyContent> embyContentToSend, IQueryable<PlexEpisode> plexEpisodes, IQueryable<EmbyEpisode> embyEp)
{
var sb = new StringBuilder();
@ -249,13 +242,11 @@ namespace Ombi.Schedule.Jobs.Ombi
await ProcessEmbyMovies(embyMovies, sb);
}
var plexTv = plexContentToSend.Where(x => x.Type == PlexMediaTypeEntity.Show);
var embyTv = embyContentToSend.Where(x => x.Type == EmbyMediaType.Series);
if (plexTv.Any() || embyTv.Any())
if (plexEpisodes.Any() || embyEp.Any())
{
sb.Append("<h1>New Episodes:</h1><br /><br />");
await ProcessPlexTv(plexTv, sb);
await ProcessEmbyMovies(embyTv, sb);
await ProcessPlexTv(plexEpisodes, sb);
await ProcessEmbyTv(embyEp, sb);
}
return sb.ToString();
@ -288,22 +279,7 @@ namespace Ombi.Schedule.Jobs.Ombi
}
try
{
AddImageInsideTable(sb, $"https://image.tmdb.org/t/p/original{info.BackdropPath}");
sb.Append("<tr>");
TableData(sb);
Href(sb, $"https://www.imdb.com/title/{info.ImdbId}/");
Header(sb, 3, $"{info.Title} {info.ReleaseDate ?? string.Empty}");
EndTag(sb, "a");
if (info.Genres.Any())
{
AddParagraph(sb,
$"Genre: {string.Join(", ", info.Genres.Select(x => x.Name.ToString()).ToArray())}");
}
AddParagraph(sb, info.Overview);
CreateMovieHtmlContent(sb, info);
}
catch (Exception e)
{
@ -316,6 +292,7 @@ namespace Ombi.Schedule.Jobs.Ombi
}
}
}
private async Task ProcessEmbyMovies(IQueryable<EmbyContent> embyContent, StringBuilder sb)
{
sb.Append(
@ -323,30 +300,21 @@ namespace Ombi.Schedule.Jobs.Ombi
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);
var imdbId = content.ProviderId;
var findResult = await _movieApi.Find(imdbId, ExternalSource.imdb_id);
var result = findResult.movie_results?.FirstOrDefault();
if(result == null)
{
continue;
}
var info = await _movieApi.GetMovieInformationWithExtraInfo(result.id);
if (info == null)
{
continue;
}
try
{
AddImageInsideTable(sb, $"https://image.tmdb.org/t/p/original{info.BackdropPath}");
sb.Append("<tr>");
TableData(sb);
Href(sb, $"https://www.imdb.com/title/{info.ImdbId}/");
Header(sb, 3, $"{info.Title} {info.ReleaseDate ?? string.Empty}");
EndTag(sb, "a");
if (info.Genres.Any())
{
AddParagraph(sb,
$"Genre: {string.Join(", ", info.Genres.Select(x => x.Name.ToString()).ToArray())}");
}
AddParagraph(sb, info.Overview);
CreateMovieHtmlContent(sb, info);
}
catch (Exception e)
{
@ -360,9 +328,57 @@ namespace Ombi.Schedule.Jobs.Ombi
}
}
private async Task ProcessPlexTv(IQueryable<PlexServerContent> plexContent, StringBuilder 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 orderedTv = plexContent.OrderByDescending(x => x.AddedAt);
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)
@ -412,15 +428,15 @@ namespace Ombi.Schedule.Jobs.Ombi
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}";
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,
var results = t.Episodes.GroupBy(p => p.SeasonNumber,
(key, g) => new
{
SeasonNumber = key,
@ -469,9 +485,26 @@ namespace Ombi.Schedule.Jobs.Ombi
}
private async Task ProcessEmbyTv(IQueryable<EmbyContent> plexContent, StringBuilder sb)
private async Task ProcessEmbyTv(IQueryable<EmbyEpisode> embyContent, StringBuilder sb)
{
var orderedTv = plexContent.OrderByDescending(x => x.AddedAt);
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)

@ -119,7 +119,10 @@ namespace Ombi.Store.Context
var roles = Roles.Where(x => x.Name == OmbiRoles.RecievesNewsletter);
if (!roles.Any())
{
Roles.Add(new IdentityRole(OmbiRoles.RecievesNewsletter));
Roles.Add(new IdentityRole(OmbiRoles.RecievesNewsletter)
{
NormalizedName = OmbiRoles.RecievesNewsletter.ToUpper()
});
}
//Check if templates exist
var templates = NotificationTemplates.ToList();
@ -143,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,
@ -153,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,
@ -163,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,
@ -207,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,

@ -10,7 +10,7 @@ namespace Ombi.Store.Repository.Requests
public class SeasonRequests : Entity
{
public int SeasonNumber { get; set; }
public List<EpisodeRequests> Episodes { get; set; }
public List<EpisodeRequests> Episodes { get; set; } = new List<EpisodeRequests>();
public int ChildRequestId { get; set; }
[ForeignKey(nameof(ChildRequestId))]

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

@ -30,3 +30,20 @@ export interface ISearchTvResult {
firstSeason: boolean;
latestSeason: boolean;
}
export interface ITvRequestViewModel {
requestAll: boolean;
firstSeason: boolean;
latestSeason: boolean;
tvDbId: number;
seasons: ISeasonsViewModel[];
}
export interface ISeasonsViewModel {
seasonNumber: number;
episodes: IEpisodesViewModel[];
}
export interface IEpisodesViewModel {
episodeNumber: number;
}

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

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

@ -90,12 +90,6 @@ export class TvRequestsComponent implements OnInit {
}
public ngOnInit() {
const profile = <ISonarrProfile>{name:"test",id:1 };
const folder = <ISonarrRootFolder>{path:"testpath", id:1};
this.sonarrProfiles.push(profile);
this.sonarrRootFolders.push(folder);
this.amountToLoad = 1000;
this.currentlyLoaded = 1000;
this.tvRequests = [];
@ -204,7 +198,7 @@ export class TvRequestsComponent implements OnInit {
this.loadInit();
}
private loadBackdrop(val: TreeNode): void {
this.imageService.getTvBanner(val.data.id).subscribe(x => {
this.imageService.getTvBanner(val.data.tvDbId).subscribe(x => {
val.data.background = this.sanitizer.bypassSecurityTrustStyle
("url(" + x + ")");
});

@ -5,7 +5,7 @@ import { NotificationService } from "../services";
import { RequestService } from "../services";
import { SearchService } from "../services";
import { INewSeasonRequests, IRequestEngineResult } from "../interfaces";
import { INewSeasonRequests, IRequestEngineResult, ISeasonsViewModel, ITvRequestViewModel } from "../interfaces";
import { IEpisodesRequests } from "../interfaces";
import { ISearchTvResult } from "../interfaces";
@ -46,7 +46,22 @@ export class SeriesInformationComponent implements OnInit {
this.series.requested = true;
this.requestService.requestTv(this.series)
const viewModel = <ITvRequestViewModel>{ firstSeason: this.series.firstSeason, latestSeason: this.series.latestSeason, requestAll: this.series.requestAll, tvDbId: this.series.id};
viewModel.seasons = [];
this.series.seasonRequests.forEach((season) => {
const seasonsViewModel = <ISeasonsViewModel>{seasonNumber: season.seasonNumber, episodes: []};
season.episodes.forEach(ep => {
if(!this.series.latestSeason || !this.series.requestAll || !this.series.firstSeason) {
if(ep.requested) {
seasonsViewModel.episodes.push({episodeNumber: ep.episodeNumber});
}
}
});
viewModel.seasons.push(seasonsViewModel);
});
this.requestService.requestTv(viewModel)
.subscribe(x => {
this.result = x as IRequestEngineResult;
if (this.result.result) {

@ -7,7 +7,7 @@ import { ImageService, NotificationService, RequestService, SearchService} from
import { TreeNode } from "primeng/primeng";
import { IRequestEngineResult } from "../interfaces";
import { IIssueCategory, ISearchTvResult } from "../interfaces";
import { IIssueCategory, ISearchTvResult, ISeasonsViewModel, ITvRequestViewModel } from "../interfaces";
@Component({
selector: "tv-search",
@ -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.
@ -155,7 +154,23 @@ export class TvSearchComponent implements OnInit {
if (this.authService.hasRole("admin") || this.authService.hasRole("AutoApproveMovie")) {
searchResult.approved = true;
}
this.requestService.requestTv(searchResult)
const viewModel = <ITvRequestViewModel>{ firstSeason: searchResult.firstSeason, latestSeason: searchResult.latestSeason, requestAll: searchResult.requestAll, tvDbId: searchResult.id};
viewModel.seasons = [];
searchResult.seasonRequests.forEach((season) => {
const seasonsViewModel = <ISeasonsViewModel>{seasonNumber: season.seasonNumber, episodes: []};
season.episodes.forEach(ep => {
if(!searchResult.latestSeason || !searchResult.requestAll || !searchResult.firstSeason) {
if(ep.requested) {
seasonsViewModel.episodes.push({episodeNumber: ep.episodeNumber});
}
}
});
viewModel.seasons.push(seasonsViewModel);
});
this.requestService.requestTv(viewModel)
.subscribe(x => {
this.result = x;
if (this.result.result) {

@ -19,11 +19,22 @@ 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(themoviedbid: string): Observable<string> {
return this.http.get<string>(`${this.url}poster/movie/${themoviedbid}`, {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});
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 });
}
}

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

@ -7,7 +7,7 @@ import { Observable } from "rxjs/Rx";
import { TreeNode } from "primeng/primeng";
import { IRequestEngineResult } from "../interfaces";
import { IChildRequests, IFilter, IMovieRequestModel, IMovieRequests, IMovieUpdateModel, ITvRequests, ITvUpdateModel } from "../interfaces";
import { ISearchTvResult } from "../interfaces";
import { ITvRequestViewModel } from "../interfaces";
import { ServiceHelpers } from "./service.helpers";
@Injectable()
@ -20,7 +20,7 @@ export class RequestService extends ServiceHelpers {
return this.http.post<IRequestEngineResult>(`${this.url}Movie/`, JSON.stringify(movie), {headers: this.headers});
}
public requestTv(tv: ISearchTvResult): Observable<IRequestEngineResult> {
public requestTv(tv: ITvRequestViewModel): Observable<IRequestEngineResult> {
return this.http.post<IRequestEngineResult>(`${this.url}TV/`, JSON.stringify(tv), {headers: this.headers});
}

@ -33,12 +33,12 @@
</div>
</div>
<div class="form-group">
<!-- <div class="form-group">
<div class="checkbox">
<input type="checkbox" id="enable" [(ngModel)]="settings.recentlyAddedPage" [checked]="settings.recentlyAddedPage">
<label for="enable">Enable Recently Added Page</label>
</div>
</div>
</div> -->
<div class="form-group">
<label for="logo" class="control-label">Custom Logo</label>

@ -1,6 +1,6 @@
<settings-menu></settings-menu>
<wiki [url]="'https://github.com/tidusjar/Ombi/wiki/Newsletter'"></wiki>
<wiki [url]="'https://github.com/tidusjar/Ombi/wiki/Newsletter-Settings'"></wiki>
<div *ngIf="settings">
<fieldset>
<legend>Newsletter</legend>
@ -30,14 +30,18 @@
<button type="button" (click)="test()" class="btn btn-danger-outline">Test</button>
<button type="button" (click)="updateDatabase()" class="btn btn-info-outline" tooltipPosition="top" pTooltip="I recommend running this with a fresh Ombi install, this will set all the current *found* content to have been sent via Newsletter,
if you do not do this then everything that Ombi has found in your libraries will go out on the first email!">Update Database</button>
<button type="button" (click)="trigger()" class="btn btn-danger-outline">Trigger now</button>
</div>
<small>NOTE: Please see the tooltip on the Update Database button.</small>
<small>When testing, the test newsletter will go to all users that have the Admin role, please ensure that there are valid email addresses for this. The test will also only grab the latest 10 movies and 10 shows just to give you an example.</small>
</div>
</div>
</div>
<div class="col-md-6">
<small>NOTE: Please see the tooltip on the Update Database button - Please see the wiki for more information</small>
<br/>
<br/>
<small>When testing, the test newsletter will go to all users that have the Admin role, please ensure that there are valid email addresses for this. The test will also only grab the latest 10 movies and 10 shows just to give you an example.</small>
</div>
</fieldset>

@ -1,9 +1,7 @@

import { Component, OnInit } from "@angular/core";
import { Component, OnInit } from "@angular/core";
import { INewsletterNotificationSettings, NotificationType } from "../../interfaces";
import { NotificationService } from "../../services";
import { SettingsService } from "../../services";
import { JobService, NotificationService, SettingsService } from "../../services";
import { TesterService } from "./../../services/applications/tester.service";
@Component({
@ -16,7 +14,8 @@ export class NewsletterComponent implements OnInit {
constructor(private settingsService: SettingsService,
private notificationService: NotificationService,
private testService: TesterService) { }
private testService: TesterService,
private jobService: JobService) { }
public ngOnInit() {
this.settingsService.getNewsletterSettings().subscribe(x => {
@ -26,10 +25,17 @@ export class NewsletterComponent implements OnInit {
public updateDatabase() {
this.settingsService.updateNewsletterDatabase().subscribe();
this.notificationService.success("Database updated");
}
public test() {
this.testService.newsletterTest(this.settings).subscribe();
this.notificationService.success("Test message queued");
}
public trigger() {
this.jobService.runNewsletter().subscribe();
this.notificationService.success("Triggered newsletter job");
}
public onSubmit() {

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

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

@ -0,0 +1,23 @@
::-webkit-scrollbar {
width: 15px;
}
::-webkit-scrollbar-track {
background-color: rgba(0,0,0,.2);
}
::-webkit-scrollbar-thumb {
min-height: 50px;
background-color: rgba(255,255,255,.15);
border: 3px solid transparent;
border-radius: 8px;
background-clip: padding-box;
}
pre::-webkit-scrollbar-track {
background-color: rgba(255,255,255,.2);
}
pre::-webkit-scrollbar-thumb {
background-color: rgba(0,0,0,.15);
}

@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc;
using Ombi.Api.FanartTv;
using Ombi.Store.Repository;
using System;
@ -31,7 +31,7 @@ namespace Ombi.Controllers
private IApplicationConfigRepository Config { get; }
private LandingPageBackground Options { get; }
private readonly ICacheService _cache;
[HttpGet("tv/{tvdbid}")]
public async Task<string> GetTvBanner(int tvdbid)
{
@ -71,7 +71,7 @@ namespace Ombi.Controllers
if (images.movieposter?.Any() ?? false)
{
var enImage = images.movieposter.Where(x => x.lang == "en").OrderByDescending(x => x.likes).Select(x => x.url).FirstOrDefault();
var enImage = images.movieposter.Where(x => x.lang == "en").OrderByDescending(x => x.likes).Select(x => x.url).FirstOrDefault();
if (enImage == null)
{
return images.movieposter.OrderByDescending(x => x.likes).Select(x => x.url).FirstOrDefault();
@ -117,6 +117,56 @@ namespace Ombi.Controllers
return string.Empty;
}
[HttpGet("background/movie/{movieDbId}")]
public async Task<string> GetMovieBackground(string movieDbId)
{
var key = await _cache.GetOrAdd(CacheKeys.FanartTv, async () => await Config.Get(Store.Entities.ConfigurationTypes.FanartTv), DateTime.Now.AddDays(1));
var images = await FanartTvApi.GetMovieImages(movieDbId, key.Value);
if (images == null)
{
return string.Empty;
}
if (images.moviebackground?.Any() ?? false)
{
var enImage = images.moviebackground.Where(x => x.lang == "en").OrderByDescending(x => x.likes).Select(x => x.url).FirstOrDefault();
if (enImage == null)
{
return images.moviebackground.OrderByDescending(x => x.likes).Select(x => x.url).FirstOrDefault();
}
return enImage;
}
return string.Empty;
}
[HttpGet("background/tv/{tvdbid}")]
public async Task<string> GetTvBackground(int tvdbid)
{
var key = await _cache.GetOrAdd(CacheKeys.FanartTv, async () => await Config.Get(Store.Entities.ConfigurationTypes.FanartTv), DateTime.Now.AddDays(1));
var images = await FanartTvApi.GetTvImages(tvdbid, key.Value);
if (images == null)
{
return string.Empty;
}
if (images.showbackground?.Any() ?? false)
{
var enImage = images.showbackground.Where(x => x.lang == "en").OrderByDescending(x => x.likes).Select(x => x.url).FirstOrDefault();
if (enImage == null)
{
return images.showbackground.OrderByDescending(x => x.likes).Select(x => x.url).FirstOrDefault();
}
return enImage;
}
return string.Empty;
}
[HttpGet("background")]
public async Task<object> GetBackgroundImage()
{
@ -133,7 +183,7 @@ namespace Ombi.Controllers
{
var item = rand.Next(moviesArray.Length);
var result = await FanartTvApi.GetMovieImages(moviesArray[item].ToString(), key.Value);
while (!result.moviebackground.Any())
{
result = await FanartTvApi.GetMovieImages(moviesArray[item].ToString(), key.Value);
@ -141,7 +191,7 @@ namespace Ombi.Controllers
movieUrl = result.moviebackground[0].url;
}
if(tvArray.Any())
if (tvArray.Any())
{
var item = rand.Next(tvArray.Length);
var result = await FanartTvApi.GetTvImages(tvArray[item], key.Value);
@ -157,8 +207,8 @@ namespace Ombi.Controllers
if (!string.IsNullOrEmpty(movieUrl) && !string.IsNullOrEmpty(tvUrl))
{
var result = rand.Next(2);
if (result == 0) return new { url = movieUrl };
if (result == 1) return new { url = tvUrl };
if (result == 0) return new { url = movieUrl };
if (result == 1) return new { url = tvUrl };
}
if (!string.IsNullOrEmpty(movieUrl))

@ -20,7 +20,7 @@ namespace Ombi.Controllers
{
public JobController(IOmbiAutomaticUpdater updater, IPlexUserImporter userImporter,
ICacheService mem, IEmbyUserImporter embyImporter, IPlexContentSync plexContentSync,
IEmbyContentSync embyContentSync)
IEmbyContentSync embyContentSync, INewsletterJob newsletter)
{
_updater = updater;
_plexUserImporter = userImporter;
@ -28,6 +28,7 @@ namespace Ombi.Controllers
_memCache = mem;
_plexContentSync = plexContentSync;
_embyContentSync = embyContentSync;
_newsletterJob = newsletter;
}
private readonly IOmbiAutomaticUpdater _updater;
@ -36,6 +37,7 @@ namespace Ombi.Controllers
private readonly ICacheService _memCache;
private readonly IPlexContentSync _plexContentSync;
private readonly IEmbyContentSync _embyContentSync;
private readonly INewsletterJob _newsletterJob;
/// <summary>
/// Runs the update job
@ -129,5 +131,16 @@ namespace Ombi.Controllers
BackgroundJob.Enqueue(() => _embyContentSync.Start());
return true;
}
/// <summary>
/// Runs the newsletter
/// </summary>
/// <returns></returns>
[HttpPost("newsletter")]
public bool StartNewsletter()
{
BackgroundJob.Enqueue(() => _newsletterJob.Start());
return true;
}
}
}

@ -174,7 +174,7 @@ namespace Ombi.Controllers
/// <param name="tv">The tv.</param>
/// <returns></returns>
[HttpPost("tv")]
public async Task<RequestEngineResult> RequestTv([FromBody] SearchTvShowViewModel tv)
public async Task<RequestEngineResult> RequestTv([FromBody] TvRequestViewModel tv)
{
return await TvRequestEngine.RequestTvShow(tv);
}

@ -97,5 +97,13 @@
<None Include="wwwroot\loading.css" />
<None Include="wwwroot\translations\*.json" />
</ItemGroup>
<ItemGroup>
<None Remove="ClientApp\app\animations\fadeinout.ts" />
</ItemGroup>
<ItemGroup>
<TypeScriptCompile Include="ClientApp\app\animations\fadeinout.ts" />
</ItemGroup>
</Project>

@ -165,6 +165,12 @@ namespace Ombi
var ombiService =
app.ApplicationServices.GetService<ISettingsService<OmbiSettings>>();
var settings = ombiService.GetSettings();
if (settings.ApiKey.IsNullOrEmpty())
{
// Generate a API Key
settings.ApiKey = Guid.NewGuid().ToString("N");
ombiService.SaveSettings(settings);
}
if (settings.BaseUrl.HasValue())
{
app.UsePathBase(settings.BaseUrl);

@ -64,26 +64,16 @@ O:::::::OOO:::::::Om::::m m::::m m::::mb:::::bbbbbb::::::bi::::::i
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>@appName</title>
<base href="/@baseUrl"/>
<link rel="SHORTCUT ICON" href="~/images/favicon/favicon.ico"/>
<link rel="icon" href="~/images/favicon/favicon.ico" type="image/ico"/>
<link rel="apple-touch-icon" sizes="57x57" href="~/images/favicon/apple-icon-57x57.png"/>
<link rel="apple-touch-icon" sizes="60x60" href="~/images/favicon/apple-icon-60x60.png"/>
<link rel="apple-touch-icon" sizes="72x72" href="~/images/favicon/apple-icon-72x72.png"/>
<link rel="apple-touch-icon" sizes="76x76" href="~/images/favicon/apple-icon-76x76.png"/>
<link rel="apple-touch-icon" sizes="114x114" href="~/images/favicon/apple-icon-114x114.png"/>
<link rel="apple-touch-icon" sizes="120x120" href="~/images/favicon/apple-icon-120x120.png"/>
<link rel="apple-touch-icon" sizes="144x144" href="~/images/favicon/apple-icon-144x144.png"/>
<link rel="apple-touch-icon" sizes="152x152" href="~/images/favicon/apple-icon-152x152.png"/>
<link rel="apple-touch-icon" sizes="180x180" href="~/images/favicon/apple-icon-180x180.png"/>
<link rel="icon" type="image/png" sizes="192x192" href="~/images/favicon/android-icon-192x192.png"/>
<link rel="icon" type="image/png" sizes="32x32" href="~/images/favicon/favicon-32x32.png"/>
<link rel="icon" type="image/png" sizes="96x96" href="~/images/favicon/favicon-96x96.png"/>
<link rel="icon" type="image/png" sizes="16x16" href="~/images/favicon/favicon-16x16.png"/>
<link rel="manifest" href="~/images/favicon/manifest.json">
<meta name="msapplication-TileColor" content="#ffffff"/>
<meta name="msapplication-TileImage" content="~/images/favicon/ms-icon-144x144.png"/>
<meta name="theme-color" content="#ffffff"/>
<link rel="apple-touch-icon" sizes="180x180" href="~/images/favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="~/images/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="~/images/favicon/favicon-16x16.png">
<link rel="manifest" href="~/images/favicon/site.webmanifest">
<link rel="mask-icon" href="~/images/favicon/safari-pinned-tab.svg" color="#df691a">
<link rel="shortcut icon" href="~/images/favicon/favicon.ico">
<meta name="msapplication-TileColor" content="#df691a">
<meta name="msapplication-config" content="~/images/favicon/browserconfig.xml">
<meta name="theme-color" content="#df691a">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">

@ -80,7 +80,7 @@ module.exports = (env: any) => {
},
plugins: [
new webpack.ProvidePlugin({ $: "jquery", jQuery: "jquery", Hammer: "hammerjs/hammer" }), // Global identifiers
new webpack.ContextReplacementPlugin(/\@angular(\\|\/)core(\\|\/)esm5/, path.join(__dirname,"./client")), // Workaround for https://github.com/angular/angular/issues/20357
new webpack.ContextReplacementPlugin(/\@angular(\\|\/)core(\\|\/)esm5/, path.join(__dirname, "./client")), // Workaround for https://github.com/angular/angular/issues/20357
new webpack.ContextReplacementPlugin(/\@angular\b.*\b(bundles|linker)/, path.join(__dirname, "./ClientApp")), // Workaround for https://github.com/angular/angular/issues/11580
new webpack.ContextReplacementPlugin(/angular(\\|\/)core(\\|\/)@angular/, path.join(__dirname, "./ClientApp")), // Workaround for https://github.com/angular/angular/issues/14898
extractCSS,

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

@ -1,2 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig><msapplication><tile><square70x70logo src="/ms-icon-70x70.png"/><square150x150logo src="/ms-icon-150x150.png"/><square310x310logo src="/ms-icon-310x310.png"/><TileColor>#ffffff</TileColor></tile></msapplication></browserconfig>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/images/favicon/mstile-150x150.png"/>
<TileColor>#df691a</TileColor>
</tile>
</msapplication>
</browserconfig>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 680 B

After

Width:  |  Height:  |  Size: 505 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 738 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 15 KiB

@ -1,41 +0,0 @@
{
"name": "App",
"icons": [
{
"src": "\/android-icon-36x36.png",
"sizes": "36x36",
"type": "image\/png",
"density": "0.75"
},
{
"src": "\/android-icon-48x48.png",
"sizes": "48x48",
"type": "image\/png",
"density": "1.0"
},
{
"src": "\/android-icon-72x72.png",
"sizes": "72x72",
"type": "image\/png",
"density": "1.5"
},
{
"src": "\/android-icon-96x96.png",
"sizes": "96x96",
"type": "image\/png",
"density": "2.0"
},
{
"src": "\/android-icon-144x144.png",
"sizes": "144x144",
"type": "image\/png",
"density": "3.0"
},
{
"src": "\/android-icon-192x192.png",
"sizes": "192x192",
"type": "image\/png",
"density": "4.0"
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@ -0,0 +1 @@
<svg version="1" xmlns="http://www.w3.org/2000/svg" width="1365.333" height="1365.333" viewBox="0 0 1024.000000 1024.000000"><path d="M487.2 71c-140.4 9.8-263.6 80.9-341 196.7-39.5 59-63.1 124.3-71.8 198.8-2.5 21.1-2.5 71.9 0 93 6.5 56 19.9 101.7 43.7 150 21.9 44.5 48.5 81.2 85.3 118 47.8 47.7 105.8 83.8 168.9 105 160.4 53.9 335.7 13.4 454.3-105 36.8-36.8 63.4-73.5 85.3-118 23.8-48.3 37.2-94 43.7-150 2.5-21.1 2.5-71.9 0-93-5.2-44.4-15.1-83.6-30.9-122.2-32.3-78.8-88.7-148.4-159.2-196.6-61.7-42.1-130.7-67.4-205.5-75.2-17.9-1.9-56.3-2.7-72.8-1.5zM391 283c11.3 3.2 21.2 8.6 31.4 17.1 5 4.2 28.7 27.4 52.6 51.5 46.2 46.6 48 48.8 54.4 64.8 4.3 10.7 5.9 19.7 5.8 33.1 0 13.9-1.9 22.6-7.8 35.4l-3.7 8.2 3.8 3.9 3.8 3.9 4.6-2.3c19-9.6 43.2-12.1 63.1-6.6 9.7 2.7 22.1 8.7 29.6 14.4 3.2 2.4 27.1 25.7 52.9 51.8 41.7 42 47.5 48.2 51.4 55.1 8.8 15.7 12.8 32.1 11.7 48.7-.7 10.7-3.4 23-6.7 30.5-6.2 14.2-20 30.1-34.1 39.2-25.2 16.3-57.3 17.7-85.8 3.7l-11.5-5.7-50.1-50.1c-54.7-54.7-56.7-57.1-62.5-73.9-4-11.5-5.2-21.2-4.6-34.7.7-13.3 2.3-20.2 7.7-31.7l3.8-8.1-3.7-3.6-3.7-3.7-7.9 3.6c-27.5 12.6-61.6 10-86.2-6.5-6.6-4.5-98.4-96-103.9-103.7-10.6-14.6-15.7-31.5-15.7-51.3 0-14.6 1.6-22.4 7.3-34.6 12.5-27.1 34.7-44.6 63.5-50.3 8.4-1.7 31.9-.6 40.5 1.9zm179 66c1.9 1.9 2 3.3 2 35.2 0 32.7 0 33.2-2.2 35.5-3.1 3.3-7.7 3.1-11.1-.5l-2.7-2.8V384c0-31.9 0-32.4 2.2-34.7 2.8-3 8.9-3.2 11.8-.3zm74.1 30.9c5.6 5.6 5.3 6.1-20.3 31.8-25.6 25.6-26.9 26.5-31.9 21.7-2.1-2-2.9-3.7-2.9-6.3 0-3.4 1.5-5.1 22.3-25.7 12.2-12.2 23.1-22.7 24.2-23.3 3.3-1.7 5.5-1.3 8.6 1.8zm29.9 73.6c2.1 1.1 3.1 2.5 3.6 5.2.6 3.3.3 4-2.5 6.5L672 468h-64.4l-2.8-2.7c-3.6-3.4-3.7-7.5-.3-10.8l2.4-2.5H639c26.6 0 32.6.3 35 1.5z"/><path d="M358.4 324.5c-28.1 6.1-42.3 34.4-30.7 61 2.4 5.4 7.3 10.6 48.1 51.7 48.4 48.6 50 50 62.7 53.3 7.1 1.8 14.4 1.9 18 .3 2.5-1.1 2.3-1.4-20.8-24.7-21.3-21.5-23.5-24-24.6-28.3-3.3-13.7 6.7-26.8 20.6-26.8 2.9 0 7 .7 9.1 1.6 2.4.9 11.8 9.5 26.3 24l22.6 22.5 1.3-3.1c2.8-6.6-.7-22.3-6.9-31.6-2.3-3.4-22.5-24.3-47.8-49.4-47-46.8-48.3-47.8-61-50.5-8.1-1.7-9-1.7-16.9 0zM567.8 533.7c-1 .2-1.8.7-1.8 1.1 0 .3 13.2 13.9 29.4 30.2 28.2 28.2 29.5 29.7 31 35.1 2 6.8 1.1 12.1-3 18-2.9 4.2-8.8 8.2-13.8 9.3-1.5.4-5.4.2-8.5-.5l-5.6-1.1-30-29.9c-16.6-16.4-30.3-29.9-30.7-29.9-.3 0-1.1 1.9-1.7 4.3-1.9 7.6 1.2 20.8 6.9 29.7 4.2 6.4 90.5 92.1 96 95.2 16.7 9.5 36.8 7 51.4-6.3 13.7-12.5 17.2-33 8.5-50.8-2.8-6-7.6-11.1-48.8-52.2-48.8-48.7-49.3-49.1-61.9-51.8-5.2-1.1-14-1.3-17.4-.4z"/></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

@ -0,0 +1,19 @@
{
"name": "Ombi",
"short_name": "Ombi",
"icons": [
{
"src": "/images/favicon/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/images/favicon/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#df691a",
"background_color": "#df691a",
"display": "standalone"
}
Loading…
Cancel
Save