From 54314dda65600cb24138c8353a37875cc2c510ae Mon Sep 17 00:00:00 2001 From: dhruvb14 Date: Sat, 28 Jan 2017 00:19:56 -0500 Subject: [PATCH 01/61] Partial fix for broken HR tag's in Email... --- Ombi.Services/Jobs/RecentlyAdded.cs | 29 ++++++++++--------- .../Jobs/Templates/RecentlyAddedTemplate.html | 4 +-- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/Ombi.Services/Jobs/RecentlyAdded.cs b/Ombi.Services/Jobs/RecentlyAdded.cs index 8ac7b6743..563dd35a5 100644 --- a/Ombi.Services/Jobs/RecentlyAdded.cs +++ b/Ombi.Services/Jobs/RecentlyAdded.cs @@ -200,16 +200,17 @@ namespace Ombi.Services.Jobs html = template.LoadTemplate(sb.ToString()); Log.Debug("Loaded the template"); } - - Send(newletterSettings, html, plexSettings, testEmail); + string escapedHtml = new string(html.Where(c => !char.IsControl(c)).ToArray()); + Log.Debug(escapedHtml); + Send(newletterSettings, escapedHtml, plexSettings, testEmail); } private void GenerateMovieHtml(List movies, PlexSettings plexSettings, StringBuilder sb) { var orderedMovies = movies.OrderByDescending(x => x?.addedAt.UnixTimeStampToDateTime()).ToList() ?? new List(); - sb.Append("

New Movies:



"); + sb.Append("

New Movies:



"); sb.Append( ""); foreach (var movie in orderedMovies) @@ -259,13 +260,13 @@ namespace Ombi.Services.Jobs } } - sb.Append("


"); + sb.Append("

"); } private void GenerateMovieHtml(List movies, PlexSettings plexSettings, StringBuilder sb) { var orderedMovies = movies.OrderByDescending(x => x?.addedAt.UnixTimeStampToDateTime()).ToList() ?? new List(); - sb.Append("

New Movies:



"); + sb.Append("

New Movies:



"); sb.Append( ""); foreach (var movie in orderedMovies) @@ -315,14 +316,14 @@ namespace Ombi.Services.Jobs } } - sb.Append("


"); + sb.Append("

"); } private void GenerateTvHtml(List tv, PlexSettings plexSettings, StringBuilder sb) { var orderedTv = tv.OrderByDescending(x => x?.addedAt.UnixTimeStampToDateTime()).ToList(); // TV - sb.Append("

New Episodes:



"); + sb.Append("

New Episodes:



"); sb.Append( ""); foreach (var t in orderedTv) @@ -375,14 +376,14 @@ namespace Ombi.Services.Jobs EndLoopHtml(sb); } } - sb.Append("


"); + sb.Append("

"); } private void GenerateTvHtml(List tv, PlexSettings plexSettings, StringBuilder sb) { var orderedTv = tv.OrderByDescending(x => x?.addedAt.UnixTimeStampToDateTime()).ToList(); // TV - sb.Append("

New Episodes:



"); + sb.Append("

New Episodes:



"); sb.Append( ""); foreach (var t in orderedTv) @@ -435,7 +436,7 @@ namespace Ombi.Services.Jobs EndLoopHtml(sb); } } - sb.Append("


"); + sb.Append("

"); } private void Send(NewletterSettings newletterSettings, string html, PlexSettings plexSettings, bool testEmail = false) @@ -516,10 +517,12 @@ namespace Ombi.Services.Jobs private void EndLoopHtml(StringBuilder sb) { + //NOTE: BR have to be in TD's as per html spec or it will be put outside of the table... + //Source: http://stackoverflow.com/questions/6588638/phantom-br-tag-rendered-by-browsers-prior-to-table-tag + sb.Append("
"); + sb.Append("
"); + sb.Append("
"); sb.Append(""); - sb.Append("
"); - sb.Append("
"); - sb.Append("
"); sb.Append(""); } diff --git a/Ombi.Services/Jobs/Templates/RecentlyAddedTemplate.html b/Ombi.Services/Jobs/Templates/RecentlyAddedTemplate.html index 2229df38c..3bdc22e42 100644 --- a/Ombi.Services/Jobs/Templates/RecentlyAddedTemplate.html +++ b/Ombi.Services/Jobs/Templates/RecentlyAddedTemplate.html @@ -157,14 +157,14 @@ - {@RECENTLYADDED} + - + {@RECENTLYADDED}
- +
- +
@@ -241,4 +241,4 @@ }) - \ No newline at end of file + From 102612742d7dd58f09ad198cde2e323cbc48aca8 Mon Sep 17 00:00:00 2001 From: d2dyno Date: Wed, 1 Feb 2017 03:39:46 -0600 Subject: [PATCH 20/61] Update Radarr placeholder --- Ombi.UI/Views/Integration/Radarr.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Ombi.UI/Views/Integration/Radarr.cshtml b/Ombi.UI/Views/Integration/Radarr.cshtml index 124fdf21c..1ccccc40f 100644 --- a/Ombi.UI/Views/Integration/Radarr.cshtml +++ b/Ombi.UI/Views/Integration/Radarr.cshtml @@ -66,7 +66,7 @@
- +
From 7a2a0573467b695b351953af521ceac0eec13eb5 Mon Sep 17 00:00:00 2001 From: dhruvb14 Date: Wed, 1 Feb 2017 10:06:50 -0500 Subject: [PATCH 21/61] @tidusjar pointed out runtime error!! --- Ombi.UI/NinjectModules/ServicesModule.cs | 1 + Ombi.UI/Views/Admin/NewsletterSettings.cshtml | 21 ++++++++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/Ombi.UI/NinjectModules/ServicesModule.cs b/Ombi.UI/NinjectModules/ServicesModule.cs index 95dfcf433..95fbe4ba5 100644 --- a/Ombi.UI/NinjectModules/ServicesModule.cs +++ b/Ombi.UI/NinjectModules/ServicesModule.cs @@ -49,6 +49,7 @@ namespace Ombi.UI.NinjectModules Bind().To(); Bind().To(); Bind().To(); + Bind().To(); Bind().To(); Bind().To(); Bind().To(); diff --git a/Ombi.UI/Views/Admin/NewsletterSettings.cshtml b/Ombi.UI/Views/Admin/NewsletterSettings.cshtml index 02958cb33..3d02e89d4 100644 --- a/Ombi.UI/Views/Admin/NewsletterSettings.cshtml +++ b/Ombi.UI/Views/Admin/NewsletterSettings.cshtml @@ -64,6 +64,11 @@ Supports HTML +
+
+ +
+
@@ -116,26 +121,26 @@ success: function (response) { if (response) { generateNotify(response.message, "success"); - $('#testEmailSpinner').attr("class", "fa fa-check"); + $('#testSendMassEmailSpinner').attr("class", "fa fa-check"); } else { generateNotify(response.message, "danger"); - $('#testEmailSpinner').attr("class", "fa fa-times"); + $('#testSendMassEmailSpinner').attr("class", "fa fa-times"); } }, error: function (e) { console.log(e); generateNotify("Something went wrong!", "danger"); - $('#testEmailSpinner').attr("class", "fa fa-times"); + $('#testSendMassEmailSpinner').attr("class", "fa fa-times"); } }); }); - $('#sendMassEmailBtn').click(function (e) { + $('#testSendMassEmailBtn').click(function (e) { e.preventDefault(); var base = '@Html.GetBaseUrl()'; var url = createBaseUrl(base, '/admin/testmassadminemail'); - $('#sendMassEmailSpinner').attr("class", "fa fa-spinner fa-spin"); + $('#testSendMassEmailSpinner').attr("class", "fa fa-spinner fa-spin"); $.ajax({ type: "post", url: url, @@ -144,17 +149,17 @@ success: function (response) { if (response) { generateNotify(response.message, "success"); - $('#sendMassEmailSpinner').attr("class", "fa fa-check"); + $('#testSendMassEmailSpinner').attr("class", "fa fa-check"); } else { generateNotify(response.message, "danger"); - $('#sendMassEmailSpinner').attr("class", "fa fa-times"); + $('#testSendMassEmailSpinner').attr("class", "fa fa-times"); } }, error: function (e) { console.log(e); generateNotify("Something went wrong!", "danger"); - $('#sendMassEmailSpinner').attr("class", "fa fa-times"); + $('#testSendMassEmailSpinner').attr("class", "fa fa-times"); } }); }); From 4d3e76856b6ea4fa40ae0159979e17777614446a Mon Sep 17 00:00:00 2001 From: tidusjar Date: Wed, 1 Feb 2017 20:38:04 +0000 Subject: [PATCH 22/61] Fixed #1042 --- Ombi.UI/Modules/Admin/IntegrationModule.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Ombi.UI/Modules/Admin/IntegrationModule.cs b/Ombi.UI/Modules/Admin/IntegrationModule.cs index 36ec5fcc9..4076f9756 100644 --- a/Ombi.UI/Modules/Admin/IntegrationModule.cs +++ b/Ombi.UI/Modules/Admin/IntegrationModule.cs @@ -156,7 +156,7 @@ namespace Ombi.UI.Modules.Admin var cp = await CpSettings.GetSettingsAsync(); if (cp.Enabled) { - return Response.AsJson(new JsonResponseModel { Result = false, Message = "CouchPotato is enabled, we cannot enable Watcher and CouchPotato" }); + return Response.AsJson(new JsonResponseModel { Result = false, Message = "CouchPotato is enabled, we cannot enable Radarr and CouchPotato" }); } var valid = this.Validate(radarrSettings); From ca9469407695bea006e740c9558b6ebcaebf4de7 Mon Sep 17 00:00:00 2001 From: dhruvb14 Date: Wed, 1 Feb 2017 23:49:30 -0500 Subject: [PATCH 23/61] finish implementing mass email feature --- Ombi.Core/Ombi.Core.csproj | 1 + Ombi.Core/SettingModels/MassEmailSettings.cs | 35 +++++ Ombi.Services/Interfaces/IMassEmail.cs | 4 +- Ombi.Services/Jobs/RecentlyAdded.cs | 17 ++- .../Jobs/Templates/MassEmailTemplate.html | 2 +- Ombi.UI/Modules/Admin/AdminModule.cs | 25 +++- Ombi.UI/Views/Admin/NewsletterSettings.cshtml | 132 ++++++++++++------ 7 files changed, 161 insertions(+), 55 deletions(-) create mode 100644 Ombi.Core/SettingModels/MassEmailSettings.cs diff --git a/Ombi.Core/Ombi.Core.csproj b/Ombi.Core/Ombi.Core.csproj index 9866e6fb0..f6562c2fc 100644 --- a/Ombi.Core/Ombi.Core.csproj +++ b/Ombi.Core/Ombi.Core.csproj @@ -124,6 +124,7 @@ + diff --git a/Ombi.Core/SettingModels/MassEmailSettings.cs b/Ombi.Core/SettingModels/MassEmailSettings.cs new file mode 100644 index 000000000..0be28a92d --- /dev/null +++ b/Ombi.Core/SettingModels/MassEmailSettings.cs @@ -0,0 +1,35 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: EmailNotificationSettings.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion +namespace Ombi.Core.SettingModels +{ + public sealed class MassEmailSettings : NotificationSettings + { + public string Users { get; set; } + public string Subject { get; set; } + public string Body { get; set; } + } +} \ No newline at end of file diff --git a/Ombi.Services/Interfaces/IMassEmail.cs b/Ombi.Services/Interfaces/IMassEmail.cs index c7b88cad9..9751cc870 100644 --- a/Ombi.Services/Interfaces/IMassEmail.cs +++ b/Ombi.Services/Interfaces/IMassEmail.cs @@ -5,6 +5,8 @@ namespace Ombi.Services.Jobs public interface IMassEmail { void Execute(IJobExecutionContext context); - void MassEmailAdminTest(string html); + void MassEmailAdminTest(string html, string subject); + void SendMassEmail(string html, string subject); + } } \ No newline at end of file diff --git a/Ombi.Services/Jobs/RecentlyAdded.cs b/Ombi.Services/Jobs/RecentlyAdded.cs index 937eac8ab..9ee52f231 100644 --- a/Ombi.Services/Jobs/RecentlyAdded.cs +++ b/Ombi.Services/Jobs/RecentlyAdded.cs @@ -111,14 +111,23 @@ namespace Ombi.Services.Jobs var settings = NewsletterSettings.GetSettings(); Start(settings, true); } - public void MassEmailAdminTest(string html) + public void MassEmailAdminTest(string html, string subject) { Log.Debug("Starting Mass Email Test"); var settings = NewsletterSettings.GetSettings(); var plexSettings = PlexSettings.GetSettings(); var template = new MassEmailTemplate(); var body = template.LoadTemplate(html); - Send(settings, body, plexSettings, true); + Send(settings, body, plexSettings, true, subject); + } + public void SendMassEmail(string html, string subject) + { + Log.Debug("Starting Mass Email Test"); + var settings = NewsletterSettings.GetSettings(); + var plexSettings = PlexSettings.GetSettings(); + var template = new MassEmailTemplate(); + var body = template.LoadTemplate(html); + Send(settings, body, plexSettings, false, subject); } private void Start(NewletterSettings newletterSettings, bool testEmail = false) @@ -448,7 +457,7 @@ namespace Ombi.Services.Jobs sb.Append("

"); } - private void Send(NewletterSettings newletterSettings, string html, PlexSettings plexSettings, bool testEmail = false) + private void Send(NewletterSettings newletterSettings, string html, PlexSettings plexSettings, bool testEmail = false, string subject = "New Content on Plex!") { Log.Debug("Entering Send"); var settings = EmailSettings.GetSettings(); @@ -463,7 +472,7 @@ namespace Ombi.Services.Jobs var message = new MimeMessage { Body = body.ToMessageBody(), - Subject = "New Content on Plex!", + Subject = subject }; Log.Debug("Created Plain/HTML MIME body"); diff --git a/Ombi.Services/Jobs/Templates/MassEmailTemplate.html b/Ombi.Services/Jobs/Templates/MassEmailTemplate.html index 4fdd04524..02214c6af 100644 --- a/Ombi.Services/Jobs/Templates/MassEmailTemplate.html +++ b/Ombi.Services/Jobs/Templates/MassEmailTemplate.html @@ -148,7 +148,7 @@ - + {@MASSEMAIL} diff --git a/Ombi.UI/Modules/Admin/AdminModule.cs b/Ombi.UI/Modules/Admin/AdminModule.cs index 181cc1d1b..a0b6216f7 100644 --- a/Ombi.UI/Modules/Admin/AdminModule.cs +++ b/Ombi.UI/Modules/Admin/AdminModule.cs @@ -1252,8 +1252,16 @@ namespace Ombi.UI.Modules.Admin { try { + var settings = this.Bind(); Log.Debug("Clicked Admin Mass Email Test"); - MassEmail.MassEmailAdminTest("Dhruv's Test Email"); + if (settings.Subject == null) { + return Response.AsJson(new JsonResponseModel { Result = false, Message = "Please Set a Subject" }); + } + if (settings.Body == null) + { + return Response.AsJson(new JsonResponseModel { Result = false, Message = "Please Set a Body" }); + } + MassEmail.MassEmailAdminTest(settings.Body.Replace("\n", "
"), settings.Subject); return Response.AsJson(new JsonResponseModel { Result = true, Message = "Sent email to administrator" }); } catch (Exception e) @@ -1266,9 +1274,18 @@ namespace Ombi.UI.Modules.Admin { try { - Log.Debug("Clicked Send Mass Email"); - RecentlyAdded.RecentlyAddedAdminTest(); - return Response.AsJson(new JsonResponseModel { Result = true, Message = "Sent email to administrator" }); + var settings = this.Bind(); + Log.Debug("Clicked Admin Mass Email Test"); + if (settings.Subject == null) + { + return Response.AsJson(new JsonResponseModel { Result = false, Message = "Please Set a Subject" }); + } + if (settings.Body == null) + { + return Response.AsJson(new JsonResponseModel { Result = false, Message = "Please Set a Body" }); + } + MassEmail.SendMassEmail(settings.Body.Replace("\n", "
"), settings.Subject); + return Response.AsJson(new JsonResponseModel { Result = true, Message = "Sent email to All users" }); } catch (Exception e) { diff --git a/Ombi.UI/Views/Admin/NewsletterSettings.cshtml b/Ombi.UI/Views/Admin/NewsletterSettings.cshtml index 3d02e89d4..2b9f52838 100644 --- a/Ombi.UI/Views/Admin/NewsletterSettings.cshtml +++ b/Ombi.UI/Views/Admin/NewsletterSettings.cshtml @@ -7,46 +7,48 @@
Newsletter Settings +
- -
-
+ +
+
- Note: This will require you to setup your email notifications -
- @if (Model.SendRecentlyAddedEmail) - { - - } - else - { - - } + Note: This will require you to setup your email notifications +
+ @if (Model.SendRecentlyAddedEmail) + { + + } + else + { + + } +
-
-
- -
- - You can add multiple email addresses by using the ; delimiter -
- +
+ +
+ + You can add multiple email addresses by using the ; delimiter +
+ +
-
-
-
- +
+
+ +
-
-
-
-
-
- +
+
+
+
+ +
@@ -57,21 +59,32 @@
Mass Email -
- +
+
+ Note: This will require you to setup your email notifications +
+
+ +
+ +
+
+
+ - - Supports HTML + + Supports HTML -
-
-
-
-
-
-
- +
+
+ +
+
+
+
+ +
@@ -141,13 +154,14 @@ var base = '@Html.GetBaseUrl()'; var url = createBaseUrl(base, '/admin/testmassadminemail'); $('#testSendMassEmailSpinner').attr("class", "fa fa-spinner fa-spin"); + var data = { "Users": "", "Body": $("#massEmailBody").val(), "Subject": $("#massEmailSubject").val() }; $.ajax({ type: "post", url: url, - data: $("#massEmailBody").val(), + data: data, dataType: "json", success: function (response) { - if (response) { + if (response.result) { generateNotify(response.message, "success"); $('#testSendMassEmailSpinner').attr("class", "fa fa-check"); } else { @@ -163,6 +177,34 @@ } }); }); + $('#sendMassEmailBtn').click(function (e) { + e.preventDefault(); + var base = '@Html.GetBaseUrl()'; + var url = createBaseUrl(base, '/admin/sendmassemail'); + $('#sendMassEmailSpinner').attr("class", "fa fa-spinner fa-spin"); + var data = { "Users": "", "Body": $("#massEmailBody").val(), "Subject": $("#massEmailSubject").val() }; + $.ajax({ + type: "post", + url: url, + data: data, + dataType: "json", + success: function (response) { + if (response.result) { + generateNotify(response.message, "success"); + $('#sendMassEmailSpinner').attr("class", "fa fa-check"); + } else { + + generateNotify(response.message, "danger"); + $('#sendMassEmailSpinner').attr("class", "fa fa-times"); + } + }, + error: function (e) { + console.log(e); + generateNotify("Something went wrong!", "danger"); + $('#sendMassEmailSpinner').attr("class", "fa fa-times"); + } + }); + }); }); From 16b6b6acea740292e8757aeb50da7f8c551b7ac2 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Fri, 3 Feb 2017 14:20:51 +0000 Subject: [PATCH 24/61] Hide the auto update btn #236 Fixed where we were not populating the emby episodes #435 Fixed #1048 issue 1,2,4 --- .../Emby/EmbyEpisodeInformation.cs | 2 +- Ombi.Services/Jobs/EmbyAvailabilityChecker.cs | 4 +- Ombi.Services/Jobs/EmbyEpisodeCacher.cs | 24 +- Ombi.UI/Jobs/Scheduler.cs | 4 +- Ombi.UI/Modules/Admin/AdminModule.cs | 24 +- Ombi.UI/Modules/SearchModule.cs | 3439 +++++++++-------- Ombi.UI/Views/Admin/Emby.cshtml | 2 + Ombi.UI/Views/SystemStatus/Status.cshtml | 4 +- 8 files changed, 1757 insertions(+), 1746 deletions(-) diff --git a/Ombi.Api.Models/Emby/EmbyEpisodeInformation.cs b/Ombi.Api.Models/Emby/EmbyEpisodeInformation.cs index be173faf9..1cdb2985c 100644 --- a/Ombi.Api.Models/Emby/EmbyEpisodeInformation.cs +++ b/Ombi.Api.Models/Emby/EmbyEpisodeInformation.cs @@ -49,7 +49,7 @@ namespace Ombi.Api.Models.Emby public object[] Taglines { get; set; } public object[] Genres { get; set; } public string[] SeriesGenres { get; set; } - public int CommunityRating { get; set; } + public float CommunityRating { get; set; } public int VoteCount { get; set; } public long RunTimeTicks { get; set; } public string PlayAccess { get; set; } diff --git a/Ombi.Services/Jobs/EmbyAvailabilityChecker.cs b/Ombi.Services/Jobs/EmbyAvailabilityChecker.cs index a86a7469d..166ed987a 100644 --- a/Ombi.Services/Jobs/EmbyAvailabilityChecker.cs +++ b/Ombi.Services/Jobs/EmbyAvailabilityChecker.cs @@ -286,7 +286,9 @@ namespace Ombi.Services.Jobs var ep = await EpisodeRepo.CustomAsync(async connection => { connection.Open(); - var result = await connection.QueryAsync("select * from EmbyEpisodes where ProviderId = @ProviderId", new { ProviderId = theTvDbId }); + var result = await connection.QueryAsync(@"select ee.* from EmbyEpisodes ee inner join EmbyContent ec + on ee.ParentId = ec.EmbyId + where ec.ProviderId = @ProviderId", new { ProviderId = theTvDbId }); return result; }); diff --git a/Ombi.Services/Jobs/EmbyEpisodeCacher.cs b/Ombi.Services/Jobs/EmbyEpisodeCacher.cs index 945908149..5679a24b9 100644 --- a/Ombi.Services/Jobs/EmbyEpisodeCacher.cs +++ b/Ombi.Services/Jobs/EmbyEpisodeCacher.cs @@ -74,6 +74,10 @@ namespace Ombi.Services.Jobs { var epInfo = EmbyApi.GetInformation(ep.Id, EmbyMediaType.Episode, settings.ApiKey, settings.AdministratorId, settings.FullUri); + if (epInfo.EpisodeInformation?.ProviderIds?.Tvdb == null) + { + continue; + } model.Add(new EmbyEpisodes { EmbyId = ep.Id, @@ -82,7 +86,7 @@ namespace Ombi.Services.Jobs EpisodeTitle = ep.Name, ParentId = ep.SeriesId, ShowTitle = ep.SeriesName, - ProviderId = epInfo.EpisodeInformation.ProviderIds.Tmdb + ProviderId = epInfo.EpisodeInformation.ProviderIds.Tvdb }); } @@ -108,15 +112,6 @@ namespace Ombi.Services.Jobs return; } - var jobs = Job.GetJobs(); - var job = jobs.FirstOrDefault(x => x.Name.Equals(JobNames.EmbyEpisodeCacher, StringComparison.CurrentCultureIgnoreCase)); - if (job != null) - { - if (job.LastRun > DateTime.Now.AddHours(-11)) // If it's been run in the last 11 hours - { - return; - } - } Job.SetRunning(true, JobNames.EmbyEpisodeCacher); CacheEpisodes(s); } @@ -141,15 +136,6 @@ namespace Ombi.Services.Jobs return; } - var jobs = Job.GetJobs(); - var job = jobs.FirstOrDefault(x => x.Name.Equals(JobNames.EmbyEpisodeCacher, StringComparison.CurrentCultureIgnoreCase)); - if (job != null) - { - if (job.LastRun > DateTime.Now.AddHours(-11)) // If it's been run in the last 11 hours - { - return; - } - } Job.SetRunning(true, JobNames.EmbyEpisodeCacher); CacheEpisodes(s); } diff --git a/Ombi.UI/Jobs/Scheduler.cs b/Ombi.UI/Jobs/Scheduler.cs index 25151b59f..8f43a02b3 100644 --- a/Ombi.UI/Jobs/Scheduler.cs +++ b/Ombi.UI/Jobs/Scheduler.cs @@ -304,8 +304,8 @@ namespace Ombi.UI.Jobs var embyEpisode = TriggerBuilder.Create() .WithIdentity("EmbyEpisodeCacher", "Emby") - //.StartAt(DateBuilder.FutureDate(10, IntervalUnit.Minute)) - .StartAt(DateBuilder.FutureDate(10, IntervalUnit.Minute)) + .StartNow() + //.StartAt(DateBuilder.FutureDate(10, IntervalUnit.Minute)) .WithSimpleSchedule(x => x.WithIntervalInHours(s.EmbyEpisodeCacher).RepeatForever()) .Build(); diff --git a/Ombi.UI/Modules/Admin/AdminModule.cs b/Ombi.UI/Modules/Admin/AdminModule.cs index a83ff8bd1..41864c7b0 100644 --- a/Ombi.UI/Modules/Admin/AdminModule.cs +++ b/Ombi.UI/Modules/Admin/AdminModule.cs @@ -441,11 +441,15 @@ namespace Ombi.UI.Modules.Admin private async Task SavePlex() { var plexSettings = this.Bind(); - var valid = this.Validate(plexSettings); - if (!valid.IsValid) - { - return Response.AsJson(valid.SendJsonError()); - } + + if (plexSettings.Enable) + { + var valid = this.Validate(plexSettings); + if (!valid.IsValid) + { + return Response.AsJson(valid.SendJsonError()); + } + } if (plexSettings.Enable) @@ -462,7 +466,7 @@ namespace Ombi.UI.Modules.Admin } } - if (string.IsNullOrEmpty(plexSettings.MachineIdentifier)) + if (string.IsNullOrEmpty(plexSettings.MachineIdentifier) && plexSettings.Enable) { //Lookup identifier var server = PlexApi.GetServer(plexSettings.PlexAuthToken); @@ -1166,7 +1170,13 @@ namespace Ombi.UI.Modules.Admin FaultQueueHandler = s.FaultQueueHandler, PlexEpisodeCacher = s.PlexEpisodeCacher, PlexUserChecker = s.PlexUserChecker, - UserRequestLimitResetter = s.UserRequestLimitResetter + UserRequestLimitResetter = s.UserRequestLimitResetter, + EmbyAvailabilityChecker = s.EmbyAvailabilityChecker, + EmbyContentCacher = s.EmbyContentCacher, + EmbyEpisodeCacher = s.EmbyEpisodeCacher, + EmbyUserChecker = s.EmbyUserChecker, + RadarrCacher = s.RadarrCacher, + WatcherCacher = s.WatcherCacher }; return View["SchedulerSettings", model]; } diff --git a/Ombi.UI/Modules/SearchModule.cs b/Ombi.UI/Modules/SearchModule.cs index 20b43097d..4a822ce81 100644 --- a/Ombi.UI/Modules/SearchModule.cs +++ b/Ombi.UI/Modules/SearchModule.cs @@ -67,1719 +67,1730 @@ using ISecurityExtensions = Ombi.Core.ISecurityExtensions; namespace Ombi.UI.Modules { - public class SearchModule : BaseAuthModule - { - public SearchModule(ICacheProvider cache, - ISettingsService prSettings, IAvailabilityChecker plexChecker, - IRequestService request, ISonarrApi sonarrApi, ISettingsService sonarrSettings, - ISettingsService sickRageService, ISickRageApi srApi, - INotificationService notify, IMusicBrainzApi mbApi, IHeadphonesApi hpApi, - ISettingsService hpService, - ICouchPotatoCacher cpCacher, IWatcherCacher watcherCacher, ISonarrCacher sonarrCacher, ISickRageCacher sickRageCacher, IPlexApi plexApi, - ISettingsService plexService, ISettingsService auth, - IRepository u, ISettingsService email, - IIssueService issue, IAnalytics a, IRepository rl, ITransientFaultQueue tfQueue, IRepository content, - ISecurityExtensions security, IMovieSender movieSender, IRadarrCacher radarrCacher, ITraktApi traktApi, ISettingsService cus, - IEmbyAvailabilityChecker embyChecker, IRepository embyContent, ISettingsService embySettings) - : base("search", prSettings, security) - { - Auth = auth; - PlexService = plexService; - PlexApi = plexApi; - PrService = prSettings; - MovieApi = new TheMovieDbApi(); - Cache = cache; - PlexChecker = plexChecker; - CpCacher = cpCacher; - SonarrCacher = sonarrCacher; - SickRageCacher = sickRageCacher; - RequestService = request; - SonarrApi = sonarrApi; - SonarrService = sonarrSettings; - SickRageService = sickRageService; - SickrageApi = srApi; - NotificationService = notify; - MusicBrainzApi = mbApi; - HeadphonesApi = hpApi; - HeadphonesService = hpService; - UsersToNotifyRepo = u; - EmailNotificationSettings = email; - IssueService = issue; - Analytics = a; - RequestLimitRepo = rl; - FaultQueue = tfQueue; - TvApi = new TvMazeApi(); - PlexContentRepository = content; - MovieSender = movieSender; - WatcherCacher = watcherCacher; - RadarrCacher = radarrCacher; - TraktApi = traktApi; - CustomizationSettings = cus; - EmbyChecker = embyChecker; - EmbyContentRepository = embyContent; - EmbySettings = embySettings; - - Get["SearchIndex", "/", true] = async (x, ct) => await RequestLoad(); - - Get["movie/{searchTerm}", true] = async (x, ct) => await SearchMovie((string)x.searchTerm); - Get["tv/{searchTerm}", true] = async (x, ct) => await SearchTvShow((string)x.searchTerm); - Get["music/{searchTerm}", true] = async (x, ct) => await SearchAlbum((string)x.searchTerm); - Get["music/coverArt/{id}"] = p => GetMusicBrainzCoverArt((string)p.id); - - Get["movie/upcoming", true] = async (x, ct) => await UpcomingMovies(); - Get["movie/playing", true] = async (x, ct) => await CurrentlyPlayingMovies(); - - Get["tv/popular", true] = async (x, ct) => await ProcessShows(ShowSearchType.Popular); - Get["tv/trending", true] = async (x, ct) => await ProcessShows(ShowSearchType.Trending); - Get["tv/mostwatched", true] = async (x, ct) => await ProcessShows(ShowSearchType.MostWatched); - Get["tv/anticipated", true] = async (x, ct) => await ProcessShows(ShowSearchType.Anticipated); - - Get["tv/poster/{id}"] = p => GetTvPoster((int)p.id); - - Post["request/movie", true] = async (x, ct) => await RequestMovie((int)Request.Form.movieId); - Post["request/tv", true] = - async (x, ct) => await RequestTvShow((int)Request.Form.tvId, (string)Request.Form.seasons); - Post["request/tvEpisodes", true] = async (x, ct) => await RequestTvShow(0, "episode"); - Post["request/album", true] = async (x, ct) => await RequestAlbum((string)Request.Form.albumId); - - Get["/seasons"] = x => GetSeasons(); - Get["/episodes", true] = async (x, ct) => await GetEpisodes(); - } - private ITraktApi TraktApi { get; } - private IWatcherCacher WatcherCacher { get; } - private IMovieSender MovieSender { get; } - private IRepository PlexContentRepository { get; } - private IRepository EmbyContentRepository { get; } - private TvMazeApi TvApi { get; } - private IPlexApi PlexApi { get; } - private TheMovieDbApi MovieApi { get; } - private INotificationService NotificationService { get; } - private ISonarrApi SonarrApi { get; } - private ISickRageApi SickrageApi { get; } - private IRequestService RequestService { get; } - private ICacheProvider Cache { get; } - private ISettingsService Auth { get; } - private ISettingsService EmbySettings { get; } - private ISettingsService PlexService { get; } - private ISettingsService PrService { get; } - private ISettingsService SonarrService { get; } - private ISettingsService SickRageService { get; } - private ISettingsService HeadphonesService { get; } - private ISettingsService EmailNotificationSettings { get; } - private IAvailabilityChecker PlexChecker { get; } - private IEmbyAvailabilityChecker EmbyChecker { get; } - private ICouchPotatoCacher CpCacher { get; } - private ISonarrCacher SonarrCacher { get; } - private ISickRageCacher SickRageCacher { get; } - private IMusicBrainzApi MusicBrainzApi { get; } - private IHeadphonesApi HeadphonesApi { get; } - private IRepository UsersToNotifyRepo { get; } - private IIssueService IssueService { get; } - private IAnalytics Analytics { get; } - private ITransientFaultQueue FaultQueue { get; } - private IRepository RequestLimitRepo { get; } - private IRadarrCacher RadarrCacher { get; } - private ISettingsService CustomizationSettings { get; } - private static Logger Log = LogManager.GetCurrentClassLogger(); - - private async Task RequestLoad() - { - - var settings = await PrService.GetSettingsAsync(); - var custom = await CustomizationSettings.GetSettingsAsync(); - var searchViewModel = new SearchLoadViewModel - { - Settings = settings, - CustomizationSettings = custom - }; - - - return View["Search/Index", searchViewModel]; - } - - private async Task UpcomingMovies() - { - Analytics.TrackEventAsync(Category.Search, Action.Movie, "Upcoming", Username, - CookieHelper.GetAnalyticClientId(Cookies)); - return await ProcessMovies(MovieSearchType.Upcoming, string.Empty); - } - - private async Task CurrentlyPlayingMovies() - { - Analytics.TrackEventAsync(Category.Search, Action.Movie, "CurrentlyPlaying", Username, - CookieHelper.GetAnalyticClientId(Cookies)); - return await ProcessMovies(MovieSearchType.CurrentlyPlaying, string.Empty); - } - - private async Task SearchMovie(string searchTerm) - { - Analytics.TrackEventAsync(Category.Search, Action.Movie, searchTerm, Username, - CookieHelper.GetAnalyticClientId(Cookies)); - return await ProcessMovies(MovieSearchType.Search, searchTerm); - } - - private Response GetTvPoster(int theTvDbId) - { - var result = TvApi.ShowLookupByTheTvDbId(theTvDbId); - - var banner = result.image?.medium; - if (!string.IsNullOrEmpty(banner)) - { - banner = banner.Replace("http", "https"); // Always use the Https banners - } - return banner; - } - private async Task ProcessMovies(MovieSearchType searchType, string searchTerm) - { - List apiMovies; - - switch (searchType) - { - case MovieSearchType.Search: - var movies = await MovieApi.SearchMovie(searchTerm).ConfigureAwait(false); - apiMovies = movies.Select(x => - new MovieResult - { - Adult = x.Adult, - BackdropPath = x.BackdropPath, - GenreIds = x.GenreIds, - Id = x.Id, - OriginalLanguage = x.OriginalLanguage, - OriginalTitle = x.OriginalTitle, - Overview = x.Overview, - Popularity = x.Popularity, - PosterPath = x.PosterPath, - ReleaseDate = x.ReleaseDate, - Title = x.Title, - Video = x.Video, - VoteAverage = x.VoteAverage, - VoteCount = x.VoteCount - }) - .ToList(); - break; - case MovieSearchType.CurrentlyPlaying: - apiMovies = await MovieApi.GetCurrentPlayingMovies(); - break; - case MovieSearchType.Upcoming: - apiMovies = await MovieApi.GetUpcomingMovies(); - break; - default: - apiMovies = new List(); - break; - } - - var allResults = await RequestService.GetAllAsync(); - allResults = allResults.Where(x => x.Type == RequestType.Movie); - - var distinctResults = allResults.DistinctBy(x => x.ProviderId); - var dbMovies = distinctResults.ToDictionary(x => x.ProviderId); - - - var cpCached = CpCacher.QueuedIds(); - var watcherCached = WatcherCacher.QueuedIds(); - var radarrCached = RadarrCacher.QueuedIds(); - - var viewMovies = new List(); - var counter = 0; - foreach (var movie in apiMovies) - { - var viewMovie = new SearchMovieViewModel - { - Adult = movie.Adult, - BackdropPath = movie.BackdropPath, - GenreIds = movie.GenreIds, - Id = movie.Id, - OriginalLanguage = movie.OriginalLanguage, - OriginalTitle = movie.OriginalTitle, - Overview = movie.Overview, - Popularity = movie.Popularity, - PosterPath = movie.PosterPath, - ReleaseDate = movie.ReleaseDate, - Title = movie.Title, - Video = movie.Video, - VoteAverage = movie.VoteAverage, - VoteCount = movie.VoteCount - }; - - if (counter <= 5) // Let's only do it for the first 5 items - { - var movieInfo = MovieApi.GetMovieInformationWithVideos(movie.Id); - - // TODO needs to be careful about this, it's adding extra time to search... - // https://www.themoviedb.org/talk/5807f4cdc3a36812160041f2 - viewMovie.ImdbId = movieInfo?.imdb_id; - viewMovie.Homepage = movieInfo?.homepage; - var videoId = movieInfo?.video ?? false - ? movieInfo?.videos?.results?.FirstOrDefault()?.key - : string.Empty; - - viewMovie.Trailer = string.IsNullOrEmpty(videoId) - ? string.Empty - : $"https://www.youtube.com/watch?v={videoId}"; - - counter++; - } - - var canSee = CanUserSeeThisRequest(viewMovie.Id, Security.HasPermissions(User, Permissions.UsersCanViewOnlyOwnRequests), dbMovies); - - var plexSettings = await PlexService.GetSettingsAsync(); - var embySettings = await EmbySettings.GetSettingsAsync(); - if (plexSettings.Enable) - { - var content = PlexContentRepository.GetAll(); - var plexMovies = PlexChecker.GetPlexMovies(content); - - var plexMovie = PlexChecker.GetMovie(plexMovies.ToArray(), movie.Title, - movie.ReleaseDate?.Year.ToString(), - viewMovie.ImdbId); - if (plexMovie != null) - { - viewMovie.Available = true; - viewMovie.PlexUrl = plexMovie.Url; - } - } - if (embySettings.Enable) - { - var embyContent = EmbyContentRepository.GetAll(); - var embyMovies = EmbyChecker.GetEmbyMovies(embyContent); - - var embyMovie = EmbyChecker.GetMovie(embyMovies.ToArray(), movie.Title, - movie.ReleaseDate?.Year.ToString(), viewMovie.ImdbId); - if (embyMovie != null) - { - viewMovie.Available = true; - } - } - else if (dbMovies.ContainsKey(movie.Id) && canSee) // compare to the requests db - { - var dbm = dbMovies[movie.Id]; - - viewMovie.Requested = true; - viewMovie.Approved = dbm.Approved; - viewMovie.Available = dbm.Available; - } - else if (cpCached.Contains(movie.Id) && canSee) // compare to the couchpotato db - { - viewMovie.Approved = true; - viewMovie.Requested = true; - } - else if (watcherCached.Contains(viewMovie.ImdbId) && canSee) // compare to the watcher db - { - viewMovie.Approved = true; - viewMovie.Requested = true; - } - else if (radarrCached.Contains(movie.Id) && canSee) - { - viewMovie.Approved = true; - viewMovie.Requested = true; - } - viewMovies.Add(viewMovie); - } - - return Response.AsJson(viewMovies); - } - - private bool CanUserSeeThisRequest(int movieId, bool usersCanViewOnlyOwnRequests, - Dictionary moviesInDb) - { - if (usersCanViewOnlyOwnRequests) - { - var result = moviesInDb.FirstOrDefault(x => x.Value.ProviderId == movieId); - return result.Value == null || result.Value.UserHasRequested(Username); - } - - return true; - } - - private async Task ProcessShows(ShowSearchType type) - { - var shows = new List(); - var prSettings = await PrService.GetSettingsAsync(); - switch (type) - { - case ShowSearchType.Popular: - Analytics.TrackEventAsync(Category.Search, Action.TvShow, "Popular", Username, CookieHelper.GetAnalyticClientId(Cookies)); - var popularShows = await TraktApi.GetPopularShows(); - - foreach (var popularShow in popularShows) - { - var theTvDbId = int.Parse(popularShow.Ids.Tvdb.ToString()); - - var model = new SearchTvShowViewModel - { - FirstAired = popularShow.FirstAired?.ToString("yyyy-MM-ddTHH:mm:ss"), - Id = theTvDbId, - ImdbId = popularShow.Ids.Imdb, - Network = popularShow.Network, - Overview = popularShow.Overview.RemoveHtml(), - Rating = popularShow.Rating.ToString(), - Runtime = popularShow.Runtime.ToString(), - SeriesName = popularShow.Title, - Status = popularShow.Status.DisplayName, - DisableTvRequestsByEpisode = prSettings.DisableTvRequestsByEpisode, - DisableTvRequestsBySeason = prSettings.DisableTvRequestsBySeason, - EnableTvRequestsForOnlySeries = (prSettings.DisableTvRequestsByEpisode && prSettings.DisableTvRequestsBySeason), - Trailer = popularShow.Trailer, - Homepage = popularShow.Homepage - }; - shows.Add(model); - } - shows = await MapToTvModel(shows, prSettings); - break; - case ShowSearchType.Anticipated: - Analytics.TrackEventAsync(Category.Search, Action.TvShow, "Anticipated", Username, CookieHelper.GetAnalyticClientId(Cookies)); - var anticipated = await TraktApi.GetAnticipatedShows(); - foreach (var anticipatedShow in anticipated) - { - var show = anticipatedShow.Show; - var theTvDbId = int.Parse(show.Ids.Tvdb.ToString()); - - var model = new SearchTvShowViewModel - { - FirstAired = show.FirstAired?.ToString("yyyy-MM-ddTHH:mm:ss"), - Id = theTvDbId, - ImdbId = show.Ids.Imdb, - Network = show.Network ?? string.Empty, - Overview = show.Overview?.RemoveHtml() ?? string.Empty, - Rating = show.Rating.ToString(), - Runtime = show.Runtime.ToString(), - SeriesName = show.Title, - Status = show.Status?.DisplayName ?? string.Empty, - DisableTvRequestsByEpisode = prSettings.DisableTvRequestsByEpisode, - DisableTvRequestsBySeason = prSettings.DisableTvRequestsBySeason, - EnableTvRequestsForOnlySeries = (prSettings.DisableTvRequestsByEpisode && prSettings.DisableTvRequestsBySeason), - Trailer = show.Trailer, - Homepage = show.Homepage - }; - shows.Add(model); - } - shows = await MapToTvModel(shows, prSettings); - break; - case ShowSearchType.MostWatched: - Analytics.TrackEventAsync(Category.Search, Action.TvShow, "MostWatched", Username, CookieHelper.GetAnalyticClientId(Cookies)); - var mostWatched = await TraktApi.GetMostWatchesShows(); - foreach (var watched in mostWatched) - { - var show = watched.Show; - var theTvDbId = int.Parse(show.Ids.Tvdb.ToString()); - var model = new SearchTvShowViewModel - { - FirstAired = show.FirstAired?.ToString("yyyy-MM-ddTHH:mm:ss"), - Id = theTvDbId, - ImdbId = show.Ids.Imdb, - Network = show.Network, - Overview = show.Overview.RemoveHtml(), - Rating = show.Rating.ToString(), - Runtime = show.Runtime.ToString(), - SeriesName = show.Title, - Status = show.Status.DisplayName, - DisableTvRequestsByEpisode = prSettings.DisableTvRequestsByEpisode, - DisableTvRequestsBySeason = prSettings.DisableTvRequestsBySeason, - EnableTvRequestsForOnlySeries = (prSettings.DisableTvRequestsByEpisode && prSettings.DisableTvRequestsBySeason), - Trailer = show.Trailer, - Homepage = show.Homepage - }; - shows.Add(model); - } - shows = await MapToTvModel(shows, prSettings); - break; - case ShowSearchType.Trending: - Analytics.TrackEventAsync(Category.Search, Action.TvShow, "Trending", Username, CookieHelper.GetAnalyticClientId(Cookies)); - var trending = await TraktApi.GetTrendingShows(); - foreach (var watched in trending) - { - var show = watched.Show; - var theTvDbId = int.Parse(show.Ids.Tvdb.ToString()); - var model = new SearchTvShowViewModel - { - FirstAired = show.FirstAired?.ToString("yyyy-MM-ddTHH:mm:ss"), - Id = theTvDbId, - ImdbId = show.Ids.Imdb, - Network = show.Network, - Overview = show.Overview.RemoveHtml(), - Rating = show.Rating.ToString(), - Runtime = show.Runtime.ToString(), - SeriesName = show.Title, - Status = show.Status.DisplayName, - DisableTvRequestsByEpisode = prSettings.DisableTvRequestsByEpisode, - DisableTvRequestsBySeason = prSettings.DisableTvRequestsBySeason, - EnableTvRequestsForOnlySeries = (prSettings.DisableTvRequestsByEpisode && prSettings.DisableTvRequestsBySeason), - Trailer = show.Trailer, - Homepage = show.Homepage - }; - shows.Add(model); - } - shows = await MapToTvModel(shows, prSettings); - break; - default: - throw new ArgumentOutOfRangeException(nameof(type), type, null); - } - - - return Response.AsJson(shows); - } - - private async Task> MapToTvModel(List shows, PlexRequestSettings prSettings) - { - - var plexSettings = await PlexService.GetSettingsAsync(); - - var providerId = string.Empty; - // Get the requests - var allResults = await RequestService.GetAllAsync(); - allResults = allResults.Where(x => x.Type == RequestType.TvShow); - var distinctResults = allResults.DistinctBy(x => x.ProviderId); - var dbTv = distinctResults.ToDictionary(x => x.ProviderId); - - // Check the external applications - var sonarrCached = SonarrCacher.QueuedIds().ToList(); - var sickRageCache = SickRageCacher.QueuedIds(); // consider just merging sonarr/sickrage arrays - var content = PlexContentRepository.GetAll(); - var plexTvShows = PlexChecker.GetPlexTvShows(content).ToList(); - - foreach (var show in shows) - { - if (plexSettings.AdvancedSearch) - { - providerId = show.Id.ToString(); - } - - var plexShow = PlexChecker.GetTvShow(plexTvShows.ToArray(), show.SeriesName, show.FirstAired?.Substring(0, 4), - providerId); - if (plexShow != null) - { - show.Available = true; - show.PlexUrl = plexShow.Url; - } - else - { - if (dbTv.ContainsKey(show.Id)) - { - var dbt = dbTv[show.Id]; - - show.Requested = true; - show.Episodes = dbt.Episodes.ToList(); - show.Approved = dbt.Approved; - } - if (sonarrCached.Select(x => x.TvdbId).Contains(show.Id) || sickRageCache.Contains(show.Id)) - // compare to the sonarr/sickrage db - { - show.Requested = true; - } - } - } - return shows; - } - - private async Task SearchTvShow(string searchTerm) - { - - Analytics.TrackEventAsync(Category.Search, Action.TvShow, searchTerm, Username, - CookieHelper.GetAnalyticClientId(Cookies)); - var plexSettings = await PlexService.GetSettingsAsync(); - var prSettings = await PrService.GetSettingsAsync(); - var providerId = string.Empty; - - var apiTv = new List(); - await Task.Factory.StartNew(() => new TvMazeApi().Search(searchTerm)).ContinueWith((t) => - { - apiTv = t.Result; - }); - - var allResults = await RequestService.GetAllAsync(); - allResults = allResults.Where(x => x.Type == RequestType.TvShow); - var distinctResults = allResults.DistinctBy(x => x.ProviderId); - var dbTv = distinctResults.ToDictionary(x => x.ProviderId); - - if (!apiTv.Any()) - { - return Response.AsJson(""); - } - - var sonarrCached = SonarrCacher.QueuedIds(); - var sickRageCache = SickRageCacher.QueuedIds(); // consider just merging sonarr/sickrage arrays - var content = PlexContentRepository.GetAll(); - var plexTvShows = PlexChecker.GetPlexTvShows(content); - - var viewTv = new List(); - foreach (var t in apiTv) - { - if (!(t.show.externals?.thetvdb.HasValue) ?? false) - { - continue; - } - var banner = t.show.image?.medium; - if (!string.IsNullOrEmpty(banner)) - { - banner = banner.Replace("http", "https"); // Always use the Https banners - } - - var viewT = new SearchTvShowViewModel - { - Banner = banner, - FirstAired = t.show.premiered, - Id = t.show.externals?.thetvdb ?? 0, - ImdbId = t.show.externals?.imdb, - Network = t.show.network?.name, - NetworkId = t.show.network?.id.ToString(), - Overview = t.show.summary.RemoveHtml(), - Rating = t.score.ToString(CultureInfo.CurrentUICulture), - Runtime = t.show.runtime.ToString(), - SeriesId = t.show.id, - SeriesName = t.show.name, - Status = t.show.status, - DisableTvRequestsByEpisode = prSettings.DisableTvRequestsByEpisode, - DisableTvRequestsBySeason = prSettings.DisableTvRequestsBySeason, - EnableTvRequestsForOnlySeries = (prSettings.DisableTvRequestsByEpisode && prSettings.DisableTvRequestsBySeason) - }; - - - if (plexSettings.AdvancedSearch) - { - providerId = viewT.Id.ToString(); - } - - var plexShow = PlexChecker.GetTvShow(plexTvShows.ToArray(), t.show.name, t.show.premiered?.Substring(0, 4), - providerId); - if (plexShow != null) - { - viewT.Available = true; - viewT.PlexUrl = plexShow.Url; - } - else if (t.show?.externals?.thetvdb != null) - { - var tvdbid = (int)t.show.externals.thetvdb; - if (dbTv.ContainsKey(tvdbid)) - { - var dbt = dbTv[tvdbid]; - - viewT.Requested = true; - viewT.Episodes = dbt.Episodes.ToList(); - viewT.Approved = dbt.Approved; - } - if (sonarrCached.Select(x => x.TvdbId).Contains(tvdbid) || sickRageCache.Contains(tvdbid)) - // compare to the sonarr/sickrage db - { - viewT.Requested = true; - } - } - - viewTv.Add(viewT); - } - - return Response.AsJson(viewTv); - } - - private async Task SearchAlbum(string searchTerm) - { - Analytics.TrackEventAsync(Category.Search, Action.Album, searchTerm, Username, - CookieHelper.GetAnalyticClientId(Cookies)); - var apiAlbums = new List(); - await Task.Run(() => MusicBrainzApi.SearchAlbum(searchTerm)).ContinueWith((t) => - { - apiAlbums = t.Result.releases ?? new List(); - }); - - var allResults = await RequestService.GetAllAsync(); - allResults = allResults.Where(x => x.Type == RequestType.Album); - - var dbAlbum = allResults.ToDictionary(x => x.MusicBrainzId); - - var content = PlexContentRepository.GetAll(); - var plexAlbums = PlexChecker.GetPlexAlbums(content); - - var viewAlbum = new List(); - foreach (var a in apiAlbums) - { - var viewA = new SearchMusicViewModel - { - Title = a.title, - Id = a.id, - Artist = a.ArtistCredit?.Select(x => x.artist?.name).FirstOrDefault(), - Overview = a.disambiguation, - ReleaseDate = a.date, - TrackCount = a.TrackCount, - ReleaseType = a.status, - Country = a.country - }; - - DateTime release; - DateTimeHelper.CustomParse(a.ReleaseEvents?.FirstOrDefault()?.date, out release); - var artist = a.ArtistCredit?.FirstOrDefault()?.artist; - var plexAlbum = PlexChecker.GetAlbum(plexAlbums.ToArray(), a.title, release.ToString("yyyy"), artist?.name); - if (plexAlbum != null) - { - viewA.Available = true; - viewA.PlexUrl = plexAlbum.Url; - } - if (!string.IsNullOrEmpty(a.id) && dbAlbum.ContainsKey(a.id)) - { - var dba = dbAlbum[a.id]; - - viewA.Requested = true; - viewA.Approved = dba.Approved; - viewA.Available = dba.Available; - } - - viewAlbum.Add(viewA); - } - return Response.AsJson(viewAlbum); - } - - private async Task RequestMovie(int movieId) - { - if (Security.HasPermissions(User, Permissions.ReadOnlyUser) || !Security.HasPermissions(User, Permissions.RequestMovie)) - { - return - Response.AsJson(new JsonResponseModel - { - Result = false, - Message = "Sorry, you do not have the correct permissions to request a movie!" - }); - } - var settings = await PrService.GetSettingsAsync(); - if (!await CheckRequestLimit(settings, RequestType.Movie)) - { - return - Response.AsJson(new JsonResponseModel - { - Result = false, - Message = "You have reached your weekly request limit for Movies! Please contact your admin." - }); - } - - Analytics.TrackEventAsync(Category.Search, Action.Request, "Movie", Username, - CookieHelper.GetAnalyticClientId(Cookies)); - var movieInfo = await MovieApi.GetMovieInformation(movieId); - if (movieInfo == null) - { - return - Response.AsJson(new JsonResponseModel - { - Result = false, - Message = "There was an issue adding this movie!" - }); - } - var fullMovieName = - $"{movieInfo.Title}{(movieInfo.ReleaseDate.HasValue ? $" ({movieInfo.ReleaseDate.Value.Year})" : string.Empty)}"; - - var existingRequest = await RequestService.CheckRequestAsync(movieId); - if (existingRequest != null) - { - // check if the current user is already marked as a requester for this movie, if not, add them - if (!existingRequest.UserHasRequested(Username)) - { - existingRequest.RequestedUsers.Add(Username); - await RequestService.UpdateRequestAsync(existingRequest); - } - - return - Response.AsJson(new JsonResponseModel - { - Result = true, - Message = - Security.HasPermissions(User, Permissions.UsersCanViewOnlyOwnRequests) - ? $"{fullMovieName} {Ombi.UI.Resources.UI.Search_SuccessfullyAdded}" - : $"{fullMovieName} {Resources.UI.Search_AlreadyRequested}" - }); - } - - try - { - - var content = PlexContentRepository.GetAll(); - var movies = PlexChecker.GetPlexMovies(content); - if (PlexChecker.IsMovieAvailable(movies.ToArray(), movieInfo.Title, movieInfo.ReleaseDate?.Year.ToString())) - { - return - Response.AsJson(new JsonResponseModel - { - Result = false, - Message = $"{fullMovieName} is already in Plex!" - }); - } - } - catch (Exception e) - { - Log.Error(e); - return - Response.AsJson(new JsonResponseModel - { - Result = false, - Message = string.Format(Resources.UI.Search_CouldNotCheckPlex, fullMovieName) - }); - } - //#endif - - var model = new RequestedModel - { - ProviderId = movieInfo.Id, - Type = RequestType.Movie, - Overview = movieInfo.Overview, - ImdbId = movieInfo.ImdbId, - PosterPath = movieInfo.PosterPath, - Title = movieInfo.Title, - ReleaseDate = movieInfo.ReleaseDate ?? DateTime.MinValue, - Status = movieInfo.Status, - RequestedDate = DateTime.UtcNow, - Approved = false, - RequestedUsers = new List { Username }, - Issues = IssueState.None, - - }; - try - { - if (ShouldAutoApprove(RequestType.Movie)) - { - model.Approved = true; - - var result = await MovieSender.Send(model); - if (result.Result) - { - return await AddRequest(model, settings, - $"{fullMovieName} {Resources.UI.Search_SuccessfullyAdded}"); - } - if (result.Error) - - { - return - Response.AsJson(new JsonResponseModel - { - Message = "Could not add movie, please contract your administrator", - Result = false - }); - } - if (!result.MovieSendingEnabled) - { - - return await AddRequest(model, settings, $"{fullMovieName} {Resources.UI.Search_SuccessfullyAdded}"); - } - - return Response.AsJson(new JsonResponseModel - { - Result = false, - Message = Resources.UI.Search_CouchPotatoError - }); - } - - - return await AddRequest(model, settings, $"{fullMovieName} {Resources.UI.Search_SuccessfullyAdded}"); - } - catch (Exception e) - { - Log.Fatal(e); - await FaultQueue.QueueItemAsync(model, movieInfo.Id.ToString(), RequestType.Movie, FaultType.RequestFault, e.Message); - - await NotificationService.Publish(new NotificationModel - { - DateTime = DateTime.Now, - User = Username, - RequestType = RequestType.Movie, - Title = model.Title, - NotificationType = NotificationType.ItemAddedToFaultQueue - }); - - return Response.AsJson(new JsonResponseModel - { - Result = true, - Message = $"{fullMovieName} {Resources.UI.Search_SuccessfullyAdded}" - }); - } - } - - /// - /// Requests the tv show. - /// - /// The show identifier. - /// The seasons. - /// - private async Task RequestTvShow(int showId, string seasons) - { - if (Security.HasPermissions(User, Permissions.ReadOnlyUser) || !Security.HasPermissions(User, Permissions.RequestTvShow)) - { - return - Response.AsJson(new JsonResponseModel() - { - Result = false, - Message = "Sorry, you do not have the correct permissions to request a TV Show!" - }); - } - // Get the JSON from the request - var req = (Dictionary.ValueCollection)Request.Form.Values; - EpisodeRequestModel episodeModel = null; - if (req.Count == 1) - { - var json = req.FirstOrDefault()?.ToString(); - episodeModel = JsonConvert.DeserializeObject(json); // Convert it into the object - } - var episodeRequest = false; - - var settings = await PrService.GetSettingsAsync(); - if (!await CheckRequestLimit(settings, RequestType.TvShow)) - { - return - Response.AsJson(new JsonResponseModel - { - Result = false, - Message = Resources.UI.Search_WeeklyRequestLimitTVShow - }); - } - Analytics.TrackEventAsync(Category.Search, Action.Request, "TvShow", Username, - CookieHelper.GetAnalyticClientId(Cookies)); - - var sonarrSettings = SonarrService.GetSettingsAsync(); - - // This means we are requesting an episode rather than a whole series or season - if (episodeModel != null) - { - episodeRequest = true; - showId = episodeModel.ShowId; - var s = await sonarrSettings; - if (!s.Enabled) - { - return - Response.AsJson(new JsonResponseModel - { - Message = - "This is currently only supported with Sonarr, Please enable Sonarr for this feature", - Result = false - }); - } - } - - var showInfo = TvApi.ShowLookupByTheTvDbId(showId); - DateTime firstAir; - DateTime.TryParse(showInfo.premiered, out firstAir); - string fullShowName = $"{showInfo.name} ({firstAir.Year})"; - - // For some reason the poster path is always http - var posterPath = showInfo.image?.medium.Replace("http:", "https:"); - var model = new RequestedModel - { - Type = RequestType.TvShow, - Overview = showInfo.summary.RemoveHtml(), - PosterPath = posterPath, - Title = showInfo.name, - ReleaseDate = firstAir, - Status = showInfo.status, - RequestedDate = DateTime.UtcNow, - Approved = false, - RequestedUsers = new List { Username }, - Issues = IssueState.None, - ImdbId = showInfo.externals?.imdb ?? string.Empty, - SeasonCount = showInfo.Season.Count, - TvDbId = showId.ToString() - }; - - var seasonsList = new List(); - switch (seasons) - { - case "first": - seasonsList.Add(1); - model.SeasonsRequested = "First"; - break; - case "latest": - seasonsList.Add(model.SeasonCount); - model.SeasonsRequested = "Latest"; - break; - case "all": - model.SeasonsRequested = "All"; - break; - case "episode": - model.Episodes = new List(); - - foreach (var ep in episodeModel?.Episodes ?? new Models.EpisodesModel[0]) - { - model.Episodes.Add(new EpisodesModel - { - EpisodeNumber = ep.EpisodeNumber, - SeasonNumber = ep.SeasonNumber - }); - } - Analytics.TrackEventAsync(Category.Requests, Action.TvShow, $"Episode request for {model.Title}", - Username, CookieHelper.GetAnalyticClientId(Cookies)); - break; - default: - model.SeasonsRequested = seasons; - var split = seasons.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - var seasonsCount = new int[split.Length]; - for (var i = 0; i < split.Length; i++) - { - int tryInt; - int.TryParse(split[i], out tryInt); - seasonsCount[i] = tryInt; - } - seasonsList.AddRange(seasonsCount); - break; - } - - model.SeasonList = seasonsList.ToArray(); - - // check if the show/episodes have already been requested - var existingRequest = await RequestService.CheckRequestAsync(showId); - var difference = new List(); - if (existingRequest != null) - { - if (episodeRequest) - { - // Make sure we are not somehow adding dupes - difference = GetListDifferences(existingRequest.Episodes, episodeModel.Episodes).ToList(); - if (difference.Any()) - { - // Convert the request into the correct shape - var newEpisodes = episodeModel.Episodes?.Select(x => new EpisodesModel - { - SeasonNumber = x.SeasonNumber, - EpisodeNumber = x.EpisodeNumber - }); - - // Add it to the existing requests - existingRequest.Episodes.AddRange(newEpisodes ?? Enumerable.Empty()); - - // It's technically a new request now, so set the status to not approved. - var autoApprove = ShouldAutoApprove(RequestType.TvShow); - if (autoApprove) - { - return await SendTv(model, sonarrSettings, existingRequest, fullShowName, settings); - } - existingRequest.Approved = false; - - return await AddUserToRequest(existingRequest, settings, fullShowName, true); - } - else - { - // We no episodes to approve - return - Response.AsJson(new JsonResponseModel - { - Result = false, - Message = $"{fullShowName} {Resources.UI.Search_AlreadyInPlex}" - }); - } - } - else if (model.SeasonList.Except(existingRequest.SeasonList).Any()) - { - // This is a season being requested that we do not yet have - // Let's just continue - } - else - { - return await AddUserToRequest(existingRequest, settings, fullShowName); - } - } - - try - { - - var plexSettings = await PlexService.GetSettingsAsync(); - if (plexSettings.Enable) - { - var content = PlexContentRepository.GetAll(); - var shows = PlexChecker.GetPlexTvShows(content); - - var providerId = string.Empty; - if (plexSettings.AdvancedSearch) - { - providerId = showId.ToString(); - } - if (episodeRequest) - { - var cachedEpisodesTask = await PlexChecker.GetEpisodes(); - var cachedEpisodes = cachedEpisodesTask.ToList(); - foreach (var d in difference) // difference is from an existing request - { - if ( - cachedEpisodes.Any( - x => - x.SeasonNumber == d.SeasonNumber && x.EpisodeNumber == d.EpisodeNumber && - x.ProviderId == providerId)) - { - return - Response.AsJson(new JsonResponseModel - { - Result = false, - Message = - $"{fullShowName} {d.SeasonNumber} - {d.EpisodeNumber} {Resources.UI.Search_AlreadyInPlex}" - }); - } - } - - var diff = await GetEpisodeRequestDifference(showId, model); - model.Episodes = diff.ToList(); - } - else - { - if (plexSettings.EnableTvEpisodeSearching) - { - foreach (var s in showInfo.Season) - { - var result = PlexChecker.IsEpisodeAvailable(showId.ToString(), s.SeasonNumber, - s.EpisodeNumber); - if (result) - { - return - Response.AsJson(new JsonResponseModel - { - Result = false, - Message = $"{fullShowName} {Resources.UI.Search_AlreadyInPlex}" - }); - } - } - } - else if (PlexChecker.IsTvShowAvailable(shows.ToArray(), showInfo.name, - showInfo.premiered?.Substring(0, 4), - providerId, model.SeasonList)) - { - return - Response.AsJson(new JsonResponseModel - { - Result = false, - Message = $"{fullShowName} {Resources.UI.Search_AlreadyInPlex}" - }); - } - } - } - var embySettings = await EmbySettings.GetSettingsAsync(); - if (embySettings.Enable) - { - var embyContent = EmbyContentRepository.GetAll(); - var embyMovies = EmbyChecker.GetEmbyTvShows(embyContent); - var providerId = showId.ToString(); - if (episodeRequest) - { - var cachedEpisodesTask = await EmbyChecker.GetEpisodes(); - var cachedEpisodes = cachedEpisodesTask.ToList(); - foreach (var d in difference) // difference is from an existing request - { - if ( - cachedEpisodes.Any( - x => - x.SeasonNumber == d.SeasonNumber && x.EpisodeNumber == d.EpisodeNumber && - x.ProviderId == providerId)) - { - return - Response.AsJson(new JsonResponseModel - { - Result = false, - Message = - $"{fullShowName} {d.SeasonNumber} - {d.EpisodeNumber} {Resources.UI.Search_AlreadyInPlex}" - }); - } - } - - var diff = await GetEpisodeRequestDifference(showId, model); - model.Episodes = diff.ToList(); - } - else - { - if (embySettings.EnableEpisodeSearching) - { - foreach (var s in showInfo.Season) - { - var result = EmbyChecker.IsEpisodeAvailable(showId.ToString(), s.SeasonNumber, - s.EpisodeNumber); - if (result) - { - return - Response.AsJson(new JsonResponseModel - { - Result = false, - Message = $"{fullShowName} is already in Emby!" - }); - } - } - } - else if (EmbyChecker.IsTvShowAvailable(embyMovies.ToArray(), showInfo.name, - showInfo.premiered?.Substring(0, 4), - providerId, model.SeasonList)) - { - return - Response.AsJson(new JsonResponseModel - { - Result = false, - Message = $"{fullShowName} is already in Emby!" - }); - } - } - } - } - catch (Exception) - { - return - Response.AsJson(new JsonResponseModel - { - Result = false, - Message = string.Format(Resources.UI.Search_CouldNotCheckPlex, fullShowName) - }); - } - - if (showInfo.externals?.thetvdb == null) - { - await FaultQueue.QueueItemAsync(model, showInfo.id.ToString(), RequestType.TvShow, FaultType.MissingInformation, "We do not have a TheTVDBId from TVMaze"); - await NotificationService.Publish(new NotificationModel - { - DateTime = DateTime.Now, - User = Username, - RequestType = RequestType.TvShow, - Title = model.Title, - NotificationType = NotificationType.ItemAddedToFaultQueue - }); - return Response.AsJson(new JsonResponseModel - { - Result = true, - Message = $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}" - }); - } - - model.ProviderId = showInfo.externals?.thetvdb ?? 0; - - try - { - if (ShouldAutoApprove(RequestType.TvShow)) - { - return await SendTv(model, sonarrSettings, existingRequest, fullShowName, settings); - } - return await AddRequest(model, settings, $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"); - } - catch (Exception e) - { - await FaultQueue.QueueItemAsync(model, showInfo.id.ToString(), RequestType.TvShow, FaultType.RequestFault, e.Message); - await NotificationService.Publish(new NotificationModel - { - DateTime = DateTime.Now, - User = Username, - RequestType = RequestType.TvShow, - Title = model.Title, - NotificationType = NotificationType.ItemAddedToFaultQueue - }); - Log.Error(e); - return - Response.AsJson(new JsonResponseModel - { - Result = true, - Message = $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}" - }); - } - } - - private async Task AddUserToRequest(RequestedModel existingRequest, PlexRequestSettings settings, - string fullShowName, bool episodeReq = false) - { - // check if the current user is already marked as a requester for this show, if not, add them - if (!existingRequest.UserHasRequested(Username)) - { - existingRequest.RequestedUsers.Add(Username); - } - if (Security.HasPermissions(User, Permissions.UsersCanViewOnlyOwnRequests) || episodeReq) - { - return - await - UpdateRequest(existingRequest, settings, - $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"); - } - - return - await UpdateRequest(existingRequest, settings, $"{fullShowName} {Resources.UI.Search_AlreadyRequested}"); - } - - private bool ShouldSendNotification(RequestType type, PlexRequestSettings prSettings) - { - var sendNotification = ShouldAutoApprove(type) - ? !prSettings.IgnoreNotifyForAutoApprovedRequests - : true; - - if (IsAdmin) - { - sendNotification = false; // Don't bother sending a notification if the user is an admin - - } - return sendNotification; - } - - - private async Task RequestAlbum(string releaseId) - { - if (Security.HasPermissions(User, Permissions.ReadOnlyUser) || !Security.HasPermissions(User, Permissions.RequestMusic)) - { - return - Response.AsJson(new JsonResponseModel - { - Result = false, - Message = "Sorry, you do not have the correct permissions to request music!" - }); - } - - var settings = await PrService.GetSettingsAsync(); - if (!await CheckRequestLimit(settings, RequestType.Album)) - { - return - Response.AsJson(new JsonResponseModel - { - Result = false, - Message = Resources.UI.Search_WeeklyRequestLimitAlbums - }); - } - Analytics.TrackEventAsync(Category.Search, Action.Request, "Album", Username, - CookieHelper.GetAnalyticClientId(Cookies)); - var existingRequest = await RequestService.CheckRequestAsync(releaseId); - - if (existingRequest != null) - { - if (!existingRequest.UserHasRequested(Username)) - { - existingRequest.RequestedUsers.Add(Username); - await RequestService.UpdateRequestAsync(existingRequest); - } - return - Response.AsJson(new JsonResponseModel - { - Result = true, - Message = - Security.HasPermissions(User, Permissions.UsersCanViewOnlyOwnRequests) - ? $"{existingRequest.Title} {Resources.UI.Search_SuccessfullyAdded}" - : $"{existingRequest.Title} {Resources.UI.Search_AlreadyRequested}" - }); - } - - var albumInfo = MusicBrainzApi.GetAlbum(releaseId); - DateTime release; - DateTimeHelper.CustomParse(albumInfo.ReleaseEvents?.FirstOrDefault()?.date, out release); - - var artist = albumInfo.ArtistCredits?.FirstOrDefault()?.artist; - if (artist == null) - { - return - Response.AsJson(new JsonResponseModel - { - Result = false, - Message = Resources.UI.Search_MusicBrainzError - }); - } - - - var content = PlexContentRepository.GetAll(); - var albums = PlexChecker.GetPlexAlbums(content); - var alreadyInPlex = PlexChecker.IsAlbumAvailable(albums.ToArray(), albumInfo.title, release.ToString("yyyy"), - artist.name); - - if (alreadyInPlex) - { - return Response.AsJson(new JsonResponseModel - { - Result = false, - Message = $"{albumInfo.title} {Resources.UI.Search_AlreadyInPlex}" - }); - } - - var img = GetMusicBrainzCoverArt(albumInfo.id); - - var model = new RequestedModel - { - Title = albumInfo.title, - MusicBrainzId = albumInfo.id, - Overview = albumInfo.disambiguation, - PosterPath = img, - Type = RequestType.Album, - ProviderId = 0, - RequestedUsers = new List { Username }, - Status = albumInfo.status, - Issues = IssueState.None, - RequestedDate = DateTime.UtcNow, - ReleaseDate = release, - ArtistName = artist.name, - ArtistId = artist.id - }; - - try - { - if (ShouldAutoApprove(RequestType.Album)) - { - model.Approved = true; - var hpSettings = HeadphonesService.GetSettings(); - - if (!hpSettings.Enabled) - { - await RequestService.AddRequestAsync(model); - return - Response.AsJson(new JsonResponseModel - { - Result = true, - Message = $"{model.Title} {Resources.UI.Search_SuccessfullyAdded}" - }); - } - - var sender = new HeadphonesSender(HeadphonesApi, hpSettings, RequestService); - await sender.AddAlbum(model); - return await AddRequest(model, settings, $"{model.Title} {Resources.UI.Search_SuccessfullyAdded}"); - } - - return await AddRequest(model, settings, $"{model.Title} {Resources.UI.Search_SuccessfullyAdded}"); - } - catch (Exception e) - { - Log.Error(e); - await FaultQueue.QueueItemAsync(model, albumInfo.id, RequestType.Album, FaultType.RequestFault, e.Message); - - await NotificationService.Publish(new NotificationModel - { - DateTime = DateTime.Now, - User = Username, - RequestType = RequestType.Album, - Title = model.Title, - NotificationType = NotificationType.ItemAddedToFaultQueue - }); - throw; - } - } - - private string GetMusicBrainzCoverArt(string id) - { - var coverArt = MusicBrainzApi.GetCoverArt(id); - var firstImage = coverArt?.images?.FirstOrDefault(); - var img = string.Empty; - - if (firstImage != null) - { - img = firstImage.thumbnails?.small ?? firstImage.image; - } - - return img; - } - - private Response GetSeasons() - { - var seriesId = (int)Request.Query.tvId; - var show = TvApi.ShowLookupByTheTvDbId(seriesId); - var seasons = TvApi.GetSeasons(show.id); - var model = seasons.Select(x => x.number); - return Response.AsJson(model); - } - - private async Task GetEpisodes() - { - var seriesId = (int)Request.Query.tvId; - var model = await GetEpisodes(seriesId); - - return Response.AsJson(model); - } - - private async Task> GetEpisodes(int providerId) - { - var s = await SonarrService.GetSettingsAsync(); - var sonarrEnabled = s.Enabled; - var allResults = await RequestService.GetAllAsync(); - - var seriesTask = Task.Run( - () => - { - if (sonarrEnabled) - { - var allSeries = SonarrApi.GetSeries(s.ApiKey, s.FullUri); - var selectedSeries = allSeries.FirstOrDefault(x => x.tvdbId == providerId) ?? new Series(); - return selectedSeries; - } - return new Series(); - }); - - var model = new List(); - - var requests = allResults as RequestedModel[] ?? allResults.ToArray(); - - var existingRequest = requests.FirstOrDefault(x => x.Type == RequestType.TvShow && x.TvDbId == providerId.ToString()); - var show = await Task.Run(() => TvApi.ShowLookupByTheTvDbId(providerId)); - var tvMazeEpisodesTask = await Task.Run(() => TvApi.EpisodeLookup(show.id)); - var tvMazeEpisodes = tvMazeEpisodesTask.ToList(); - - var sonarrEpisodes = new List(); - if (sonarrEnabled) - { - var sonarrSeries = await seriesTask; - var sonarrEp = SonarrApi.GetEpisodes(sonarrSeries.id.ToString(), s.ApiKey, s.FullUri); - sonarrEpisodes = sonarrEp?.ToList() ?? new List(); - } - - var plexSettings = await PlexService.GetSettingsAsync(); - if (plexSettings.Enable) - { - var plexCacheTask = await PlexChecker.GetEpisodes(providerId); - var plexCache = plexCacheTask.ToList(); - foreach (var ep in tvMazeEpisodes) - { - var requested = existingRequest?.Episodes - .Any(episodesModel => - ep.number == episodesModel.EpisodeNumber && - ep.season == episodesModel.SeasonNumber) ?? false; - - var alreadyInPlex = plexCache.Any(x => x.EpisodeNumber == ep.number && x.SeasonNumber == ep.season); - var inSonarr = - sonarrEpisodes.Any(x => x.seasonNumber == ep.season && x.episodeNumber == ep.number && x.hasFile); - - model.Add(new EpisodeListViewModel - { - Id = show.id, - SeasonNumber = ep.season, - EpisodeNumber = ep.number, - Requested = requested || alreadyInPlex || inSonarr, - Name = ep.name, - EpisodeId = ep.id - }); - } - } - var embySettings = await EmbySettings.GetSettingsAsync(); - if (embySettings.Enable) - { - var embyCacheTask = await EmbyChecker.GetEpisodes(providerId); - var cache = embyCacheTask.ToList(); - foreach (var ep in tvMazeEpisodes) - { - var requested = existingRequest?.Episodes - .Any(episodesModel => - ep.number == episodesModel.EpisodeNumber && - ep.season == episodesModel.SeasonNumber) ?? false; - - var alreadyInEmby = cache.Any(x => x.EpisodeNumber == ep.number && x.SeasonNumber == ep.season); - var inSonarr = - sonarrEpisodes.Any(x => x.seasonNumber == ep.season && x.episodeNumber == ep.number && x.hasFile); - - model.Add(new EpisodeListViewModel - { - Id = show.id, - SeasonNumber = ep.season, - EpisodeNumber = ep.number, - Requested = requested || alreadyInEmby || inSonarr, - Name = ep.name, - EpisodeId = ep.id - }); - } - } - return model; - - } - - public async Task CheckRequestLimit(PlexRequestSettings s, RequestType type) - { - if (IsAdmin) - return true; - - if (Security.HasPermissions(User, Permissions.BypassRequestLimit)) - return true; - - var requestLimit = GetRequestLimitForType(type, s); - if (requestLimit == 0) - { - return true; - } - - var limit = await RequestLimitRepo.GetAllAsync(); - var usersLimit = limit.FirstOrDefault(x => x.Username == Username && x.RequestType == type); - if (usersLimit == null) - { - // Have not set a requestLimit yet - return true; - } - - return requestLimit > usersLimit.RequestCount; - } - - private int GetRequestLimitForType(RequestType type, PlexRequestSettings s) - { - int requestLimit; - switch (type) - { - case RequestType.Movie: - requestLimit = s.MovieWeeklyRequestLimit; - break; - case RequestType.TvShow: - requestLimit = s.TvWeeklyRequestLimit; - break; - case RequestType.Album: - requestLimit = s.AlbumWeeklyRequestLimit; - break; - default: - throw new ArgumentOutOfRangeException(nameof(type), type, null); - } - return requestLimit; - } - - private async Task AddRequest(RequestedModel model, PlexRequestSettings settings, string message) - { - await RequestService.AddRequestAsync(model); - - if (ShouldSendNotification(model.Type, settings)) - { - var notificationModel = new NotificationModel - { - Title = model.Title, - User = Username, - DateTime = DateTime.Now, - NotificationType = NotificationType.NewRequest, - RequestType = model.Type, - ImgSrc = model.Type == RequestType.Movie ? $"https://image.tmdb.org/t/p/w300/{model.PosterPath}" : model.PosterPath - }; - await NotificationService.Publish(notificationModel); - } - - var limit = await RequestLimitRepo.GetAllAsync(); - var usersLimit = limit.FirstOrDefault(x => x.Username == Username && x.RequestType == model.Type); - if (usersLimit == null) - { - await RequestLimitRepo.InsertAsync(new RequestLimit - { - Username = Username, - RequestType = model.Type, - FirstRequestDate = DateTime.UtcNow, - RequestCount = 1 - }); - } - else - { - usersLimit.RequestCount++; - await RequestLimitRepo.UpdateAsync(usersLimit); - } - - return Response.AsJson(new JsonResponseModel { Result = true, Message = message }); - } - - private async Task UpdateRequest(RequestedModel model, PlexRequestSettings settings, string message) - { - await RequestService.UpdateRequestAsync(model); - - if (ShouldSendNotification(model.Type, settings)) - { - var notificationModel = new NotificationModel - { - Title = model.Title, - User = Username, - DateTime = DateTime.Now, - NotificationType = NotificationType.NewRequest, - RequestType = model.Type, - ImgSrc = model.Type == RequestType.Movie ? $"https://image.tmdb.org/t/p/w300/{model.PosterPath}" : model.PosterPath - }; - await NotificationService.Publish(notificationModel); - } - - var limit = await RequestLimitRepo.GetAllAsync(); - var usersLimit = limit.FirstOrDefault(x => x.Username == Username && x.RequestType == model.Type); - if (usersLimit == null) - { - await RequestLimitRepo.InsertAsync(new RequestLimit - { - Username = Username, - RequestType = model.Type, - FirstRequestDate = DateTime.UtcNow, - RequestCount = 1 - }); - } - else - { - usersLimit.RequestCount++; - await RequestLimitRepo.UpdateAsync(usersLimit); - } - - return Response.AsJson(new JsonResponseModel { Result = true, Message = message }); - } - - private IEnumerable GetListDifferences(IEnumerable existing, IEnumerable request) - { - var newRequest = request - .Select(r => - new EpisodesModel - { - SeasonNumber = r.SeasonNumber, - EpisodeNumber = r.EpisodeNumber - }).ToList(); - - return newRequest.Except(existing); - } - - private async Task> GetEpisodeRequestDifference(int showId, RequestedModel model) - { - var episodes = await GetEpisodes(showId); - var availableEpisodes = episodes.Where(x => x.Requested).ToList(); - var available = availableEpisodes.Select(a => new EpisodesModel { EpisodeNumber = a.EpisodeNumber, SeasonNumber = a.SeasonNumber }).ToList(); - - var diff = model.Episodes.Except(available); - return diff; - } - - public bool ShouldAutoApprove(RequestType requestType) - { - var admin = Security.HasPermissions(Context.CurrentUser, Permissions.Administrator); - // if the user is an admin, they go ahead and allow auto-approval - if (admin) return true; - - // check by request type if the category requires approval or not - switch (requestType) - { - case RequestType.Movie: - return Security.HasPermissions(User, Permissions.AutoApproveMovie); - case RequestType.TvShow: - return Security.HasPermissions(User, Permissions.AutoApproveTv); - case RequestType.Album: - return Security.HasPermissions(User, Permissions.AutoApproveAlbum); - default: - return false; - } - } - - private enum ShowSearchType - { - Popular, - Anticipated, - MostWatched, - Trending - } - - private async Task SendTv(RequestedModel model, Task sonarrSettings, RequestedModel existingRequest, string fullShowName, PlexRequestSettings settings) - { - model.Approved = true; - var s = await sonarrSettings; - var sender = new TvSenderOld(SonarrApi, SickrageApi, Cache); // TODO put back - if (s.Enabled) - { - var result = await sender.SendToSonarr(s, model); - if (!string.IsNullOrEmpty(result?.title)) - { - if (existingRequest != null) - { - return await UpdateRequest(model, settings, - $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"); - } - return - await - AddRequest(model, settings, - $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"); - } - Log.Debug("Error with sending to sonarr."); - return - Response.AsJson(ValidationHelper.SendSonarrError(result?.ErrorMessages ?? new List())); - } - - var srSettings = SickRageService.GetSettings(); - if (srSettings.Enabled) - { - var result = sender.SendToSickRage(srSettings, model); - if (result?.result == "success") - { - return await AddRequest(model, settings, - $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"); - } - return - Response.AsJson(new JsonResponseModel - { - Result = false, - Message = result?.message ?? Resources.UI.Search_SickrageError - }); - } - - if (!srSettings.Enabled && !s.Enabled) - { - return await AddRequest(model, settings, $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"); - } - - return - Response.AsJson(new JsonResponseModel { Result = false, Message = Resources.UI.Search_TvNotSetUp }); - } - } + public class SearchModule : BaseAuthModule + { + public SearchModule(ICacheProvider cache, + ISettingsService prSettings, IAvailabilityChecker plexChecker, + IRequestService request, ISonarrApi sonarrApi, ISettingsService sonarrSettings, + ISettingsService sickRageService, ISickRageApi srApi, + INotificationService notify, IMusicBrainzApi mbApi, IHeadphonesApi hpApi, + ISettingsService hpService, + ICouchPotatoCacher cpCacher, IWatcherCacher watcherCacher, ISonarrCacher sonarrCacher, ISickRageCacher sickRageCacher, IPlexApi plexApi, + ISettingsService plexService, ISettingsService auth, + IRepository u, ISettingsService email, + IIssueService issue, IAnalytics a, IRepository rl, ITransientFaultQueue tfQueue, IRepository content, + ISecurityExtensions security, IMovieSender movieSender, IRadarrCacher radarrCacher, ITraktApi traktApi, ISettingsService cus, + IEmbyAvailabilityChecker embyChecker, IRepository embyContent, ISettingsService embySettings) + : base("search", prSettings, security) + { + Auth = auth; + PlexService = plexService; + PlexApi = plexApi; + PrService = prSettings; + MovieApi = new TheMovieDbApi(); + Cache = cache; + PlexChecker = plexChecker; + CpCacher = cpCacher; + SonarrCacher = sonarrCacher; + SickRageCacher = sickRageCacher; + RequestService = request; + SonarrApi = sonarrApi; + SonarrService = sonarrSettings; + SickRageService = sickRageService; + SickrageApi = srApi; + NotificationService = notify; + MusicBrainzApi = mbApi; + HeadphonesApi = hpApi; + HeadphonesService = hpService; + UsersToNotifyRepo = u; + EmailNotificationSettings = email; + IssueService = issue; + Analytics = a; + RequestLimitRepo = rl; + FaultQueue = tfQueue; + TvApi = new TvMazeApi(); + PlexContentRepository = content; + MovieSender = movieSender; + WatcherCacher = watcherCacher; + RadarrCacher = radarrCacher; + TraktApi = traktApi; + CustomizationSettings = cus; + EmbyChecker = embyChecker; + EmbyContentRepository = embyContent; + EmbySettings = embySettings; + + Get["SearchIndex", "/", true] = async (x, ct) => await RequestLoad(); + + Get["movie/{searchTerm}", true] = async (x, ct) => await SearchMovie((string)x.searchTerm); + Get["tv/{searchTerm}", true] = async (x, ct) => await SearchTvShow((string)x.searchTerm); + Get["music/{searchTerm}", true] = async (x, ct) => await SearchAlbum((string)x.searchTerm); + Get["music/coverArt/{id}"] = p => GetMusicBrainzCoverArt((string)p.id); + + Get["movie/upcoming", true] = async (x, ct) => await UpcomingMovies(); + Get["movie/playing", true] = async (x, ct) => await CurrentlyPlayingMovies(); + + Get["tv/popular", true] = async (x, ct) => await ProcessShows(ShowSearchType.Popular); + Get["tv/trending", true] = async (x, ct) => await ProcessShows(ShowSearchType.Trending); + Get["tv/mostwatched", true] = async (x, ct) => await ProcessShows(ShowSearchType.MostWatched); + Get["tv/anticipated", true] = async (x, ct) => await ProcessShows(ShowSearchType.Anticipated); + + Get["tv/poster/{id}"] = p => GetTvPoster((int)p.id); + + Post["request/movie", true] = async (x, ct) => await RequestMovie((int)Request.Form.movieId); + Post["request/tv", true] = + async (x, ct) => await RequestTvShow((int)Request.Form.tvId, (string)Request.Form.seasons); + Post["request/tvEpisodes", true] = async (x, ct) => await RequestTvShow(0, "episode"); + Post["request/album", true] = async (x, ct) => await RequestAlbum((string)Request.Form.albumId); + + Get["/seasons"] = x => GetSeasons(); + Get["/episodes", true] = async (x, ct) => await GetEpisodes(); + } + private ITraktApi TraktApi { get; } + private IWatcherCacher WatcherCacher { get; } + private IMovieSender MovieSender { get; } + private IRepository PlexContentRepository { get; } + private IRepository EmbyContentRepository { get; } + private TvMazeApi TvApi { get; } + private IPlexApi PlexApi { get; } + private TheMovieDbApi MovieApi { get; } + private INotificationService NotificationService { get; } + private ISonarrApi SonarrApi { get; } + private ISickRageApi SickrageApi { get; } + private IRequestService RequestService { get; } + private ICacheProvider Cache { get; } + private ISettingsService Auth { get; } + private ISettingsService EmbySettings { get; } + private ISettingsService PlexService { get; } + private ISettingsService PrService { get; } + private ISettingsService SonarrService { get; } + private ISettingsService SickRageService { get; } + private ISettingsService HeadphonesService { get; } + private ISettingsService EmailNotificationSettings { get; } + private IAvailabilityChecker PlexChecker { get; } + private IEmbyAvailabilityChecker EmbyChecker { get; } + private ICouchPotatoCacher CpCacher { get; } + private ISonarrCacher SonarrCacher { get; } + private ISickRageCacher SickRageCacher { get; } + private IMusicBrainzApi MusicBrainzApi { get; } + private IHeadphonesApi HeadphonesApi { get; } + private IRepository UsersToNotifyRepo { get; } + private IIssueService IssueService { get; } + private IAnalytics Analytics { get; } + private ITransientFaultQueue FaultQueue { get; } + private IRepository RequestLimitRepo { get; } + private IRadarrCacher RadarrCacher { get; } + private ISettingsService CustomizationSettings { get; } + private static Logger Log = LogManager.GetCurrentClassLogger(); + + private async Task RequestLoad() + { + + var settings = await PrService.GetSettingsAsync(); + var custom = await CustomizationSettings.GetSettingsAsync(); + var searchViewModel = new SearchLoadViewModel + { + Settings = settings, + CustomizationSettings = custom + }; + + + return View["Search/Index", searchViewModel]; + } + + private async Task UpcomingMovies() + { + Analytics.TrackEventAsync(Category.Search, Action.Movie, "Upcoming", Username, + CookieHelper.GetAnalyticClientId(Cookies)); + return await ProcessMovies(MovieSearchType.Upcoming, string.Empty); + } + + private async Task CurrentlyPlayingMovies() + { + Analytics.TrackEventAsync(Category.Search, Action.Movie, "CurrentlyPlaying", Username, + CookieHelper.GetAnalyticClientId(Cookies)); + return await ProcessMovies(MovieSearchType.CurrentlyPlaying, string.Empty); + } + + private async Task SearchMovie(string searchTerm) + { + Analytics.TrackEventAsync(Category.Search, Action.Movie, searchTerm, Username, + CookieHelper.GetAnalyticClientId(Cookies)); + return await ProcessMovies(MovieSearchType.Search, searchTerm); + } + + private Response GetTvPoster(int theTvDbId) + { + var result = TvApi.ShowLookupByTheTvDbId(theTvDbId); + + var banner = result.image?.medium; + if (!string.IsNullOrEmpty(banner)) + { + banner = banner.Replace("http", "https"); // Always use the Https banners + } + return banner; + } + private async Task ProcessMovies(MovieSearchType searchType, string searchTerm) + { + List apiMovies; + + switch (searchType) + { + case MovieSearchType.Search: + var movies = await MovieApi.SearchMovie(searchTerm).ConfigureAwait(false); + apiMovies = movies.Select(x => + new MovieResult + { + Adult = x.Adult, + BackdropPath = x.BackdropPath, + GenreIds = x.GenreIds, + Id = x.Id, + OriginalLanguage = x.OriginalLanguage, + OriginalTitle = x.OriginalTitle, + Overview = x.Overview, + Popularity = x.Popularity, + PosterPath = x.PosterPath, + ReleaseDate = x.ReleaseDate, + Title = x.Title, + Video = x.Video, + VoteAverage = x.VoteAverage, + VoteCount = x.VoteCount + }) + .ToList(); + break; + case MovieSearchType.CurrentlyPlaying: + apiMovies = await MovieApi.GetCurrentPlayingMovies(); + break; + case MovieSearchType.Upcoming: + apiMovies = await MovieApi.GetUpcomingMovies(); + break; + default: + apiMovies = new List(); + break; + } + + var allResults = await RequestService.GetAllAsync(); + allResults = allResults.Where(x => x.Type == RequestType.Movie); + + var distinctResults = allResults.DistinctBy(x => x.ProviderId); + var dbMovies = distinctResults.ToDictionary(x => x.ProviderId); + + + var cpCached = CpCacher.QueuedIds(); + var watcherCached = WatcherCacher.QueuedIds(); + var radarrCached = RadarrCacher.QueuedIds(); + + var viewMovies = new List(); + var counter = 0; + foreach (var movie in apiMovies) + { + var viewMovie = new SearchMovieViewModel + { + Adult = movie.Adult, + BackdropPath = movie.BackdropPath, + GenreIds = movie.GenreIds, + Id = movie.Id, + OriginalLanguage = movie.OriginalLanguage, + OriginalTitle = movie.OriginalTitle, + Overview = movie.Overview, + Popularity = movie.Popularity, + PosterPath = movie.PosterPath, + ReleaseDate = movie.ReleaseDate, + Title = movie.Title, + Video = movie.Video, + VoteAverage = movie.VoteAverage, + VoteCount = movie.VoteCount + }; + + if (counter <= 5) // Let's only do it for the first 5 items + { + var movieInfo = MovieApi.GetMovieInformationWithVideos(movie.Id); + + // TODO needs to be careful about this, it's adding extra time to search... + // https://www.themoviedb.org/talk/5807f4cdc3a36812160041f2 + viewMovie.ImdbId = movieInfo?.imdb_id; + viewMovie.Homepage = movieInfo?.homepage; + var videoId = movieInfo?.video ?? false + ? movieInfo?.videos?.results?.FirstOrDefault()?.key + : string.Empty; + + viewMovie.Trailer = string.IsNullOrEmpty(videoId) + ? string.Empty + : $"https://www.youtube.com/watch?v={videoId}"; + + counter++; + } + + var canSee = CanUserSeeThisRequest(viewMovie.Id, Security.HasPermissions(User, Permissions.UsersCanViewOnlyOwnRequests), dbMovies); + + var plexSettings = await PlexService.GetSettingsAsync(); + var embySettings = await EmbySettings.GetSettingsAsync(); + if (plexSettings.Enable) + { + var content = PlexContentRepository.GetAll(); + var plexMovies = PlexChecker.GetPlexMovies(content); + + var plexMovie = PlexChecker.GetMovie(plexMovies.ToArray(), movie.Title, + movie.ReleaseDate?.Year.ToString(), + viewMovie.ImdbId); + if (plexMovie != null) + { + viewMovie.Available = true; + viewMovie.PlexUrl = plexMovie.Url; + } + } + if (embySettings.Enable) + { + var embyContent = EmbyContentRepository.GetAll(); + var embyMovies = EmbyChecker.GetEmbyMovies(embyContent); + + var embyMovie = EmbyChecker.GetMovie(embyMovies.ToArray(), movie.Title, + movie.ReleaseDate?.Year.ToString(), viewMovie.ImdbId); + if (embyMovie != null) + { + viewMovie.Available = true; + } + } + else if (dbMovies.ContainsKey(movie.Id) && canSee) // compare to the requests db + { + var dbm = dbMovies[movie.Id]; + + viewMovie.Requested = true; + viewMovie.Approved = dbm.Approved; + viewMovie.Available = dbm.Available; + } + else if (cpCached.Contains(movie.Id) && canSee) // compare to the couchpotato db + { + viewMovie.Approved = true; + viewMovie.Requested = true; + } + else if (watcherCached.Contains(viewMovie.ImdbId) && canSee) // compare to the watcher db + { + viewMovie.Approved = true; + viewMovie.Requested = true; + } + else if (radarrCached.Contains(movie.Id) && canSee) + { + viewMovie.Approved = true; + viewMovie.Requested = true; + } + viewMovies.Add(viewMovie); + } + + return Response.AsJson(viewMovies); + } + + private bool CanUserSeeThisRequest(int movieId, bool usersCanViewOnlyOwnRequests, + Dictionary moviesInDb) + { + if (usersCanViewOnlyOwnRequests) + { + var result = moviesInDb.FirstOrDefault(x => x.Value.ProviderId == movieId); + return result.Value == null || result.Value.UserHasRequested(Username); + } + + return true; + } + + private async Task ProcessShows(ShowSearchType type) + { + var shows = new List(); + var prSettings = await PrService.GetSettingsAsync(); + switch (type) + { + case ShowSearchType.Popular: + Analytics.TrackEventAsync(Category.Search, Action.TvShow, "Popular", Username, CookieHelper.GetAnalyticClientId(Cookies)); + var popularShows = await TraktApi.GetPopularShows(); + + foreach (var popularShow in popularShows) + { + var theTvDbId = int.Parse(popularShow.Ids.Tvdb.ToString()); + + var model = new SearchTvShowViewModel + { + FirstAired = popularShow.FirstAired?.ToString("yyyy-MM-ddTHH:mm:ss"), + Id = theTvDbId, + ImdbId = popularShow.Ids.Imdb, + Network = popularShow.Network, + Overview = popularShow.Overview.RemoveHtml(), + Rating = popularShow.Rating.ToString(), + Runtime = popularShow.Runtime.ToString(), + SeriesName = popularShow.Title, + Status = popularShow.Status.DisplayName, + DisableTvRequestsByEpisode = prSettings.DisableTvRequestsByEpisode, + DisableTvRequestsBySeason = prSettings.DisableTvRequestsBySeason, + EnableTvRequestsForOnlySeries = (prSettings.DisableTvRequestsByEpisode && prSettings.DisableTvRequestsBySeason), + Trailer = popularShow.Trailer, + Homepage = popularShow.Homepage + }; + shows.Add(model); + } + shows = await MapToTvModel(shows, prSettings); + break; + case ShowSearchType.Anticipated: + Analytics.TrackEventAsync(Category.Search, Action.TvShow, "Anticipated", Username, CookieHelper.GetAnalyticClientId(Cookies)); + var anticipated = await TraktApi.GetAnticipatedShows(); + foreach (var anticipatedShow in anticipated) + { + var show = anticipatedShow.Show; + var theTvDbId = int.Parse(show.Ids.Tvdb.ToString()); + + var model = new SearchTvShowViewModel + { + FirstAired = show.FirstAired?.ToString("yyyy-MM-ddTHH:mm:ss"), + Id = theTvDbId, + ImdbId = show.Ids.Imdb, + Network = show.Network ?? string.Empty, + Overview = show.Overview?.RemoveHtml() ?? string.Empty, + Rating = show.Rating.ToString(), + Runtime = show.Runtime.ToString(), + SeriesName = show.Title, + Status = show.Status?.DisplayName ?? string.Empty, + DisableTvRequestsByEpisode = prSettings.DisableTvRequestsByEpisode, + DisableTvRequestsBySeason = prSettings.DisableTvRequestsBySeason, + EnableTvRequestsForOnlySeries = (prSettings.DisableTvRequestsByEpisode && prSettings.DisableTvRequestsBySeason), + Trailer = show.Trailer, + Homepage = show.Homepage + }; + shows.Add(model); + } + shows = await MapToTvModel(shows, prSettings); + break; + case ShowSearchType.MostWatched: + Analytics.TrackEventAsync(Category.Search, Action.TvShow, "MostWatched", Username, CookieHelper.GetAnalyticClientId(Cookies)); + var mostWatched = await TraktApi.GetMostWatchesShows(); + foreach (var watched in mostWatched) + { + var show = watched.Show; + var theTvDbId = int.Parse(show.Ids.Tvdb.ToString()); + var model = new SearchTvShowViewModel + { + FirstAired = show.FirstAired?.ToString("yyyy-MM-ddTHH:mm:ss"), + Id = theTvDbId, + ImdbId = show.Ids.Imdb, + Network = show.Network, + Overview = show.Overview.RemoveHtml(), + Rating = show.Rating.ToString(), + Runtime = show.Runtime.ToString(), + SeriesName = show.Title, + Status = show.Status.DisplayName, + DisableTvRequestsByEpisode = prSettings.DisableTvRequestsByEpisode, + DisableTvRequestsBySeason = prSettings.DisableTvRequestsBySeason, + EnableTvRequestsForOnlySeries = (prSettings.DisableTvRequestsByEpisode && prSettings.DisableTvRequestsBySeason), + Trailer = show.Trailer, + Homepage = show.Homepage + }; + shows.Add(model); + } + shows = await MapToTvModel(shows, prSettings); + break; + case ShowSearchType.Trending: + Analytics.TrackEventAsync(Category.Search, Action.TvShow, "Trending", Username, CookieHelper.GetAnalyticClientId(Cookies)); + var trending = await TraktApi.GetTrendingShows(); + foreach (var watched in trending) + { + var show = watched.Show; + var theTvDbId = int.Parse(show.Ids.Tvdb.ToString()); + var model = new SearchTvShowViewModel + { + FirstAired = show.FirstAired?.ToString("yyyy-MM-ddTHH:mm:ss"), + Id = theTvDbId, + ImdbId = show.Ids.Imdb, + Network = show.Network, + Overview = show.Overview.RemoveHtml(), + Rating = show.Rating.ToString(), + Runtime = show.Runtime.ToString(), + SeriesName = show.Title, + Status = show.Status.DisplayName, + DisableTvRequestsByEpisode = prSettings.DisableTvRequestsByEpisode, + DisableTvRequestsBySeason = prSettings.DisableTvRequestsBySeason, + EnableTvRequestsForOnlySeries = (prSettings.DisableTvRequestsByEpisode && prSettings.DisableTvRequestsBySeason), + Trailer = show.Trailer, + Homepage = show.Homepage + }; + shows.Add(model); + } + shows = await MapToTvModel(shows, prSettings); + break; + default: + throw new ArgumentOutOfRangeException(nameof(type), type, null); + } + + + return Response.AsJson(shows); + } + + private async Task> MapToTvModel(List shows, PlexRequestSettings prSettings) + { + + var plexSettings = await PlexService.GetSettingsAsync(); + + var providerId = string.Empty; + // Get the requests + var allResults = await RequestService.GetAllAsync(); + allResults = allResults.Where(x => x.Type == RequestType.TvShow); + var distinctResults = allResults.DistinctBy(x => x.ProviderId); + var dbTv = distinctResults.ToDictionary(x => x.ProviderId); + + // Check the external applications + var sonarrCached = SonarrCacher.QueuedIds().ToList(); + var sickRageCache = SickRageCacher.QueuedIds(); // consider just merging sonarr/sickrage arrays + var content = PlexContentRepository.GetAll(); + var plexTvShows = PlexChecker.GetPlexTvShows(content).ToList(); + + foreach (var show in shows) + { + if (plexSettings.AdvancedSearch) + { + providerId = show.Id.ToString(); + } + + var plexShow = PlexChecker.GetTvShow(plexTvShows.ToArray(), show.SeriesName, show.FirstAired?.Substring(0, 4), + providerId); + if (plexShow != null) + { + show.Available = true; + show.PlexUrl = plexShow.Url; + } + else + { + if (dbTv.ContainsKey(show.Id)) + { + var dbt = dbTv[show.Id]; + + show.Requested = true; + show.Episodes = dbt.Episodes.ToList(); + show.Approved = dbt.Approved; + } + if (sonarrCached.Select(x => x.TvdbId).Contains(show.Id) || sickRageCache.Contains(show.Id)) + // compare to the sonarr/sickrage db + { + show.Requested = true; + } + } + } + return shows; + } + + private async Task SearchTvShow(string searchTerm) + { + + Analytics.TrackEventAsync(Category.Search, Action.TvShow, searchTerm, Username, + CookieHelper.GetAnalyticClientId(Cookies)); + var plexSettings = await PlexService.GetSettingsAsync(); + var embySettings = await EmbySettings.GetSettingsAsync(); + var prSettings = await PrService.GetSettingsAsync(); + var providerId = string.Empty; + + var apiTv = new List(); + await Task.Factory.StartNew(() => new TvMazeApi().Search(searchTerm)).ContinueWith((t) => + { + apiTv = t.Result; + }); + + var allResults = await RequestService.GetAllAsync(); + allResults = allResults.Where(x => x.Type == RequestType.TvShow); + var distinctResults = allResults.DistinctBy(x => x.ProviderId); + var dbTv = distinctResults.ToDictionary(x => x.ProviderId); + + if (!apiTv.Any()) + { + return Response.AsJson(""); + } + + var sonarrCached = SonarrCacher.QueuedIds(); + var sickRageCache = SickRageCacher.QueuedIds(); // consider just merging sonarr/sickrage arrays + var content = PlexContentRepository.GetAll(); + var plexTvShows = PlexChecker.GetPlexTvShows(content); + var embyContent = EmbyContentRepository.GetAll(); + var embyCached = EmbyChecker.GetEmbyTvShows(embyContent); + + var viewTv = new List(); + foreach (var t in apiTv) + { + if (!(t.show.externals?.thetvdb.HasValue) ?? false) + { + continue; + } + var banner = t.show.image?.medium; + if (!string.IsNullOrEmpty(banner)) + { + banner = banner.Replace("http", "https"); // Always use the Https banners + } + + var viewT = new SearchTvShowViewModel + { + Banner = banner, + FirstAired = t.show.premiered, + Id = t.show.externals?.thetvdb ?? 0, + ImdbId = t.show.externals?.imdb, + Network = t.show.network?.name, + NetworkId = t.show.network?.id.ToString(), + Overview = t.show.summary.RemoveHtml(), + Rating = t.score.ToString(CultureInfo.CurrentUICulture), + Runtime = t.show.runtime.ToString(), + SeriesId = t.show.id, + SeriesName = t.show.name, + Status = t.show.status, + DisableTvRequestsByEpisode = prSettings.DisableTvRequestsByEpisode, + DisableTvRequestsBySeason = prSettings.DisableTvRequestsBySeason, + EnableTvRequestsForOnlySeries = (prSettings.DisableTvRequestsByEpisode && prSettings.DisableTvRequestsBySeason) + }; + + providerId = viewT.Id.ToString(); + + if (embySettings.Enable) + { + var embyShow = EmbyChecker.GetTvShow(embyCached.ToArray(), t.show.name, t.show.premiered?.Substring(0, 4), providerId); + if (embyShow != null) + { + viewT.Available = true; + } + } + if (plexSettings.Enable) + { + var plexShow = PlexChecker.GetTvShow(plexTvShows.ToArray(), t.show.name, t.show.premiered?.Substring(0, 4), + providerId); + if (plexShow != null) + { + viewT.Available = true; + viewT.PlexUrl = plexShow.Url; + } + } + + if (t.show?.externals?.thetvdb != null && !viewT.Available) + { + var tvdbid = (int)t.show.externals.thetvdb; + if (dbTv.ContainsKey(tvdbid)) + { + var dbt = dbTv[tvdbid]; + + viewT.Requested = true; + viewT.Episodes = dbt.Episodes.ToList(); + viewT.Approved = dbt.Approved; + } + if (sonarrCached.Select(x => x.TvdbId).Contains(tvdbid) || sickRageCache.Contains(tvdbid)) + // compare to the sonarr/sickrage db + { + viewT.Requested = true; + } + } + + viewTv.Add(viewT); + } + + return Response.AsJson(viewTv); + } + + private async Task SearchAlbum(string searchTerm) + { + Analytics.TrackEventAsync(Category.Search, Action.Album, searchTerm, Username, + CookieHelper.GetAnalyticClientId(Cookies)); + var apiAlbums = new List(); + await Task.Run(() => MusicBrainzApi.SearchAlbum(searchTerm)).ContinueWith((t) => + { + apiAlbums = t.Result.releases ?? new List(); + }); + + var allResults = await RequestService.GetAllAsync(); + allResults = allResults.Where(x => x.Type == RequestType.Album); + + var dbAlbum = allResults.ToDictionary(x => x.MusicBrainzId); + + var content = PlexContentRepository.GetAll(); + var plexAlbums = PlexChecker.GetPlexAlbums(content); + + var viewAlbum = new List(); + foreach (var a in apiAlbums) + { + var viewA = new SearchMusicViewModel + { + Title = a.title, + Id = a.id, + Artist = a.ArtistCredit?.Select(x => x.artist?.name).FirstOrDefault(), + Overview = a.disambiguation, + ReleaseDate = a.date, + TrackCount = a.TrackCount, + ReleaseType = a.status, + Country = a.country + }; + + DateTime release; + DateTimeHelper.CustomParse(a.ReleaseEvents?.FirstOrDefault()?.date, out release); + var artist = a.ArtistCredit?.FirstOrDefault()?.artist; + var plexAlbum = PlexChecker.GetAlbum(plexAlbums.ToArray(), a.title, release.ToString("yyyy"), artist?.name); + if (plexAlbum != null) + { + viewA.Available = true; + viewA.PlexUrl = plexAlbum.Url; + } + if (!string.IsNullOrEmpty(a.id) && dbAlbum.ContainsKey(a.id)) + { + var dba = dbAlbum[a.id]; + + viewA.Requested = true; + viewA.Approved = dba.Approved; + viewA.Available = dba.Available; + } + + viewAlbum.Add(viewA); + } + return Response.AsJson(viewAlbum); + } + + private async Task RequestMovie(int movieId) + { + if (Security.HasPermissions(User, Permissions.ReadOnlyUser) || !Security.HasPermissions(User, Permissions.RequestMovie)) + { + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = "Sorry, you do not have the correct permissions to request a movie!" + }); + } + var settings = await PrService.GetSettingsAsync(); + if (!await CheckRequestLimit(settings, RequestType.Movie)) + { + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = "You have reached your weekly request limit for Movies! Please contact your admin." + }); + } + + Analytics.TrackEventAsync(Category.Search, Action.Request, "Movie", Username, + CookieHelper.GetAnalyticClientId(Cookies)); + var movieInfo = await MovieApi.GetMovieInformation(movieId); + if (movieInfo == null) + { + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = "There was an issue adding this movie!" + }); + } + var fullMovieName = + $"{movieInfo.Title}{(movieInfo.ReleaseDate.HasValue ? $" ({movieInfo.ReleaseDate.Value.Year})" : string.Empty)}"; + + var existingRequest = await RequestService.CheckRequestAsync(movieId); + if (existingRequest != null) + { + // check if the current user is already marked as a requester for this movie, if not, add them + if (!existingRequest.UserHasRequested(Username)) + { + existingRequest.RequestedUsers.Add(Username); + await RequestService.UpdateRequestAsync(existingRequest); + } + + return + Response.AsJson(new JsonResponseModel + { + Result = true, + Message = + Security.HasPermissions(User, Permissions.UsersCanViewOnlyOwnRequests) + ? $"{fullMovieName} {Ombi.UI.Resources.UI.Search_SuccessfullyAdded}" + : $"{fullMovieName} {Resources.UI.Search_AlreadyRequested}" + }); + } + + try + { + + var content = PlexContentRepository.GetAll(); + var movies = PlexChecker.GetPlexMovies(content); + if (PlexChecker.IsMovieAvailable(movies.ToArray(), movieInfo.Title, movieInfo.ReleaseDate?.Year.ToString())) + { + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = $"{fullMovieName} is already in Plex!" + }); + } + } + catch (Exception e) + { + Log.Error(e); + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = string.Format(Resources.UI.Search_CouldNotCheckPlex, fullMovieName) + }); + } + //#endif + + var model = new RequestedModel + { + ProviderId = movieInfo.Id, + Type = RequestType.Movie, + Overview = movieInfo.Overview, + ImdbId = movieInfo.ImdbId, + PosterPath = movieInfo.PosterPath, + Title = movieInfo.Title, + ReleaseDate = movieInfo.ReleaseDate ?? DateTime.MinValue, + Status = movieInfo.Status, + RequestedDate = DateTime.UtcNow, + Approved = false, + RequestedUsers = new List { Username }, + Issues = IssueState.None, + + }; + try + { + if (ShouldAutoApprove(RequestType.Movie)) + { + model.Approved = true; + + var result = await MovieSender.Send(model); + if (result.Result) + { + return await AddRequest(model, settings, + $"{fullMovieName} {Resources.UI.Search_SuccessfullyAdded}"); + } + if (result.Error) + + { + return + Response.AsJson(new JsonResponseModel + { + Message = "Could not add movie, please contract your administrator", + Result = false + }); + } + if (!result.MovieSendingEnabled) + { + + return await AddRequest(model, settings, $"{fullMovieName} {Resources.UI.Search_SuccessfullyAdded}"); + } + + return Response.AsJson(new JsonResponseModel + { + Result = false, + Message = Resources.UI.Search_CouchPotatoError + }); + } + + + return await AddRequest(model, settings, $"{fullMovieName} {Resources.UI.Search_SuccessfullyAdded}"); + } + catch (Exception e) + { + Log.Fatal(e); + await FaultQueue.QueueItemAsync(model, movieInfo.Id.ToString(), RequestType.Movie, FaultType.RequestFault, e.Message); + + await NotificationService.Publish(new NotificationModel + { + DateTime = DateTime.Now, + User = Username, + RequestType = RequestType.Movie, + Title = model.Title, + NotificationType = NotificationType.ItemAddedToFaultQueue + }); + + return Response.AsJson(new JsonResponseModel + { + Result = true, + Message = $"{fullMovieName} {Resources.UI.Search_SuccessfullyAdded}" + }); + } + } + + /// + /// Requests the tv show. + /// + /// The show identifier. + /// The seasons. + /// + private async Task RequestTvShow(int showId, string seasons) + { + if (Security.HasPermissions(User, Permissions.ReadOnlyUser) || !Security.HasPermissions(User, Permissions.RequestTvShow)) + { + return + Response.AsJson(new JsonResponseModel() + { + Result = false, + Message = "Sorry, you do not have the correct permissions to request a TV Show!" + }); + } + // Get the JSON from the request + var req = (Dictionary.ValueCollection)Request.Form.Values; + EpisodeRequestModel episodeModel = null; + if (req.Count == 1) + { + var json = req.FirstOrDefault()?.ToString(); + episodeModel = JsonConvert.DeserializeObject(json); // Convert it into the object + } + var episodeRequest = false; + + var settings = await PrService.GetSettingsAsync(); + if (!await CheckRequestLimit(settings, RequestType.TvShow)) + { + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = Resources.UI.Search_WeeklyRequestLimitTVShow + }); + } + Analytics.TrackEventAsync(Category.Search, Action.Request, "TvShow", Username, + CookieHelper.GetAnalyticClientId(Cookies)); + + var sonarrSettings = SonarrService.GetSettingsAsync(); + + // This means we are requesting an episode rather than a whole series or season + if (episodeModel != null) + { + episodeRequest = true; + showId = episodeModel.ShowId; + var s = await sonarrSettings; + if (!s.Enabled) + { + return + Response.AsJson(new JsonResponseModel + { + Message = + "This is currently only supported with Sonarr, Please enable Sonarr for this feature", + Result = false + }); + } + } + + var showInfo = TvApi.ShowLookupByTheTvDbId(showId); + DateTime firstAir; + DateTime.TryParse(showInfo.premiered, out firstAir); + string fullShowName = $"{showInfo.name} ({firstAir.Year})"; + + // For some reason the poster path is always http + var posterPath = showInfo.image?.medium.Replace("http:", "https:"); + var model = new RequestedModel + { + Type = RequestType.TvShow, + Overview = showInfo.summary.RemoveHtml(), + PosterPath = posterPath, + Title = showInfo.name, + ReleaseDate = firstAir, + Status = showInfo.status, + RequestedDate = DateTime.UtcNow, + Approved = false, + RequestedUsers = new List { Username }, + Issues = IssueState.None, + ImdbId = showInfo.externals?.imdb ?? string.Empty, + SeasonCount = showInfo.Season.Count, + TvDbId = showId.ToString() + }; + + var seasonsList = new List(); + switch (seasons) + { + case "first": + seasonsList.Add(1); + model.SeasonsRequested = "First"; + break; + case "latest": + seasonsList.Add(model.SeasonCount); + model.SeasonsRequested = "Latest"; + break; + case "all": + model.SeasonsRequested = "All"; + break; + case "episode": + model.Episodes = new List(); + + foreach (var ep in episodeModel?.Episodes ?? new Models.EpisodesModel[0]) + { + model.Episodes.Add(new EpisodesModel + { + EpisodeNumber = ep.EpisodeNumber, + SeasonNumber = ep.SeasonNumber + }); + } + Analytics.TrackEventAsync(Category.Requests, Action.TvShow, $"Episode request for {model.Title}", + Username, CookieHelper.GetAnalyticClientId(Cookies)); + break; + default: + model.SeasonsRequested = seasons; + var split = seasons.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + var seasonsCount = new int[split.Length]; + for (var i = 0; i < split.Length; i++) + { + int tryInt; + int.TryParse(split[i], out tryInt); + seasonsCount[i] = tryInt; + } + seasonsList.AddRange(seasonsCount); + break; + } + + model.SeasonList = seasonsList.ToArray(); + + // check if the show/episodes have already been requested + var existingRequest = await RequestService.CheckRequestAsync(showId); + var difference = new List(); + if (existingRequest != null) + { + if (episodeRequest) + { + // Make sure we are not somehow adding dupes + difference = GetListDifferences(existingRequest.Episodes, episodeModel.Episodes).ToList(); + if (difference.Any()) + { + // Convert the request into the correct shape + var newEpisodes = episodeModel.Episodes?.Select(x => new EpisodesModel + { + SeasonNumber = x.SeasonNumber, + EpisodeNumber = x.EpisodeNumber + }); + + // Add it to the existing requests + existingRequest.Episodes.AddRange(newEpisodes ?? Enumerable.Empty()); + + // It's technically a new request now, so set the status to not approved. + var autoApprove = ShouldAutoApprove(RequestType.TvShow); + if (autoApprove) + { + return await SendTv(model, sonarrSettings, existingRequest, fullShowName, settings); + } + existingRequest.Approved = false; + + return await AddUserToRequest(existingRequest, settings, fullShowName, true); + } + else + { + // We no episodes to approve + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = $"{fullShowName} {Resources.UI.Search_AlreadyInPlex}" + }); + } + } + else if (model.SeasonList.Except(existingRequest.SeasonList).Any()) + { + // This is a season being requested that we do not yet have + // Let's just continue + } + else + { + return await AddUserToRequest(existingRequest, settings, fullShowName); + } + } + + try + { + + var plexSettings = await PlexService.GetSettingsAsync(); + if (plexSettings.Enable) + { + var content = PlexContentRepository.GetAll(); + var shows = PlexChecker.GetPlexTvShows(content); + + var providerId = string.Empty; + if (plexSettings.AdvancedSearch) + { + providerId = showId.ToString(); + } + if (episodeRequest) + { + var cachedEpisodesTask = await PlexChecker.GetEpisodes(); + var cachedEpisodes = cachedEpisodesTask.ToList(); + foreach (var d in difference) // difference is from an existing request + { + if ( + cachedEpisodes.Any( + x => + x.SeasonNumber == d.SeasonNumber && x.EpisodeNumber == d.EpisodeNumber && + x.ProviderId == providerId)) + { + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = + $"{fullShowName} {d.SeasonNumber} - {d.EpisodeNumber} {Resources.UI.Search_AlreadyInPlex}" + }); + } + } + + var diff = await GetEpisodeRequestDifference(showId, model); + model.Episodes = diff.ToList(); + } + else + { + if (plexSettings.EnableTvEpisodeSearching) + { + foreach (var s in showInfo.Season) + { + var result = PlexChecker.IsEpisodeAvailable(showId.ToString(), s.SeasonNumber, + s.EpisodeNumber); + if (result) + { + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = $"{fullShowName} {Resources.UI.Search_AlreadyInPlex}" + }); + } + } + } + else if (PlexChecker.IsTvShowAvailable(shows.ToArray(), showInfo.name, + showInfo.premiered?.Substring(0, 4), + providerId, model.SeasonList)) + { + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = $"{fullShowName} {Resources.UI.Search_AlreadyInPlex}" + }); + } + } + } + var embySettings = await EmbySettings.GetSettingsAsync(); + if (embySettings.Enable) + { + var embyContent = EmbyContentRepository.GetAll(); + var embyMovies = EmbyChecker.GetEmbyTvShows(embyContent); + var providerId = showId.ToString(); + if (episodeRequest) + { + var cachedEpisodesTask = await EmbyChecker.GetEpisodes(); + var cachedEpisodes = cachedEpisodesTask.ToList(); + foreach (var d in difference) // difference is from an existing request + { + if ( + cachedEpisodes.Any( + x => + x.SeasonNumber == d.SeasonNumber && x.EpisodeNumber == d.EpisodeNumber && + x.ProviderId == providerId)) + { + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = + $"{fullShowName} {d.SeasonNumber} - {d.EpisodeNumber} {Resources.UI.Search_AlreadyInPlex}" + }); + } + } + + var diff = await GetEpisodeRequestDifference(showId, model); + model.Episodes = diff.ToList(); + } + else + { + if (embySettings.EnableEpisodeSearching) + { + foreach (var s in showInfo.Season) + { + var result = EmbyChecker.IsEpisodeAvailable(showId.ToString(), s.SeasonNumber, + s.EpisodeNumber); + if (result) + { + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = $"{fullShowName} is already in Emby!" + }); + } + } + } + else if (EmbyChecker.IsTvShowAvailable(embyMovies.ToArray(), showInfo.name, + showInfo.premiered?.Substring(0, 4), + providerId, model.SeasonList)) + { + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = $"{fullShowName} is already in Emby!" + }); + } + } + } + } + catch (Exception) + { + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = string.Format(Resources.UI.Search_CouldNotCheckPlex, fullShowName) + }); + } + + if (showInfo.externals?.thetvdb == null) + { + await FaultQueue.QueueItemAsync(model, showInfo.id.ToString(), RequestType.TvShow, FaultType.MissingInformation, "We do not have a TheTVDBId from TVMaze"); + await NotificationService.Publish(new NotificationModel + { + DateTime = DateTime.Now, + User = Username, + RequestType = RequestType.TvShow, + Title = model.Title, + NotificationType = NotificationType.ItemAddedToFaultQueue + }); + return Response.AsJson(new JsonResponseModel + { + Result = true, + Message = $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}" + }); + } + + model.ProviderId = showInfo.externals?.thetvdb ?? 0; + + try + { + if (ShouldAutoApprove(RequestType.TvShow)) + { + return await SendTv(model, sonarrSettings, existingRequest, fullShowName, settings); + } + return await AddRequest(model, settings, $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"); + } + catch (Exception e) + { + await FaultQueue.QueueItemAsync(model, showInfo.id.ToString(), RequestType.TvShow, FaultType.RequestFault, e.Message); + await NotificationService.Publish(new NotificationModel + { + DateTime = DateTime.Now, + User = Username, + RequestType = RequestType.TvShow, + Title = model.Title, + NotificationType = NotificationType.ItemAddedToFaultQueue + }); + Log.Error(e); + return + Response.AsJson(new JsonResponseModel + { + Result = true, + Message = $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}" + }); + } + } + + private async Task AddUserToRequest(RequestedModel existingRequest, PlexRequestSettings settings, + string fullShowName, bool episodeReq = false) + { + // check if the current user is already marked as a requester for this show, if not, add them + if (!existingRequest.UserHasRequested(Username)) + { + existingRequest.RequestedUsers.Add(Username); + } + if (Security.HasPermissions(User, Permissions.UsersCanViewOnlyOwnRequests) || episodeReq) + { + return + await + UpdateRequest(existingRequest, settings, + $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"); + } + + return + await UpdateRequest(existingRequest, settings, $"{fullShowName} {Resources.UI.Search_AlreadyRequested}"); + } + + private bool ShouldSendNotification(RequestType type, PlexRequestSettings prSettings) + { + var sendNotification = ShouldAutoApprove(type) + ? !prSettings.IgnoreNotifyForAutoApprovedRequests + : true; + + if (IsAdmin) + { + sendNotification = false; // Don't bother sending a notification if the user is an admin + + } + return sendNotification; + } + + + private async Task RequestAlbum(string releaseId) + { + if (Security.HasPermissions(User, Permissions.ReadOnlyUser) || !Security.HasPermissions(User, Permissions.RequestMusic)) + { + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = "Sorry, you do not have the correct permissions to request music!" + }); + } + + var settings = await PrService.GetSettingsAsync(); + if (!await CheckRequestLimit(settings, RequestType.Album)) + { + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = Resources.UI.Search_WeeklyRequestLimitAlbums + }); + } + Analytics.TrackEventAsync(Category.Search, Action.Request, "Album", Username, + CookieHelper.GetAnalyticClientId(Cookies)); + var existingRequest = await RequestService.CheckRequestAsync(releaseId); + + if (existingRequest != null) + { + if (!existingRequest.UserHasRequested(Username)) + { + existingRequest.RequestedUsers.Add(Username); + await RequestService.UpdateRequestAsync(existingRequest); + } + return + Response.AsJson(new JsonResponseModel + { + Result = true, + Message = + Security.HasPermissions(User, Permissions.UsersCanViewOnlyOwnRequests) + ? $"{existingRequest.Title} {Resources.UI.Search_SuccessfullyAdded}" + : $"{existingRequest.Title} {Resources.UI.Search_AlreadyRequested}" + }); + } + + var albumInfo = MusicBrainzApi.GetAlbum(releaseId); + DateTime release; + DateTimeHelper.CustomParse(albumInfo.ReleaseEvents?.FirstOrDefault()?.date, out release); + + var artist = albumInfo.ArtistCredits?.FirstOrDefault()?.artist; + if (artist == null) + { + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = Resources.UI.Search_MusicBrainzError + }); + } + + + var content = PlexContentRepository.GetAll(); + var albums = PlexChecker.GetPlexAlbums(content); + var alreadyInPlex = PlexChecker.IsAlbumAvailable(albums.ToArray(), albumInfo.title, release.ToString("yyyy"), + artist.name); + + if (alreadyInPlex) + { + return Response.AsJson(new JsonResponseModel + { + Result = false, + Message = $"{albumInfo.title} {Resources.UI.Search_AlreadyInPlex}" + }); + } + + var img = GetMusicBrainzCoverArt(albumInfo.id); + + var model = new RequestedModel + { + Title = albumInfo.title, + MusicBrainzId = albumInfo.id, + Overview = albumInfo.disambiguation, + PosterPath = img, + Type = RequestType.Album, + ProviderId = 0, + RequestedUsers = new List { Username }, + Status = albumInfo.status, + Issues = IssueState.None, + RequestedDate = DateTime.UtcNow, + ReleaseDate = release, + ArtistName = artist.name, + ArtistId = artist.id + }; + + try + { + if (ShouldAutoApprove(RequestType.Album)) + { + model.Approved = true; + var hpSettings = HeadphonesService.GetSettings(); + + if (!hpSettings.Enabled) + { + await RequestService.AddRequestAsync(model); + return + Response.AsJson(new JsonResponseModel + { + Result = true, + Message = $"{model.Title} {Resources.UI.Search_SuccessfullyAdded}" + }); + } + + var sender = new HeadphonesSender(HeadphonesApi, hpSettings, RequestService); + await sender.AddAlbum(model); + return await AddRequest(model, settings, $"{model.Title} {Resources.UI.Search_SuccessfullyAdded}"); + } + + return await AddRequest(model, settings, $"{model.Title} {Resources.UI.Search_SuccessfullyAdded}"); + } + catch (Exception e) + { + Log.Error(e); + await FaultQueue.QueueItemAsync(model, albumInfo.id, RequestType.Album, FaultType.RequestFault, e.Message); + + await NotificationService.Publish(new NotificationModel + { + DateTime = DateTime.Now, + User = Username, + RequestType = RequestType.Album, + Title = model.Title, + NotificationType = NotificationType.ItemAddedToFaultQueue + }); + throw; + } + } + + private string GetMusicBrainzCoverArt(string id) + { + var coverArt = MusicBrainzApi.GetCoverArt(id); + var firstImage = coverArt?.images?.FirstOrDefault(); + var img = string.Empty; + + if (firstImage != null) + { + img = firstImage.thumbnails?.small ?? firstImage.image; + } + + return img; + } + + private Response GetSeasons() + { + var seriesId = (int)Request.Query.tvId; + var show = TvApi.ShowLookupByTheTvDbId(seriesId); + var seasons = TvApi.GetSeasons(show.id); + var model = seasons.Select(x => x.number); + return Response.AsJson(model); + } + + private async Task GetEpisodes() + { + var seriesId = (int)Request.Query.tvId; + var model = await GetEpisodes(seriesId); + + return Response.AsJson(model); + } + + private async Task> GetEpisodes(int providerId) + { + var s = await SonarrService.GetSettingsAsync(); + var sonarrEnabled = s.Enabled; + var allResults = await RequestService.GetAllAsync(); + + var seriesTask = Task.Run( + () => + { + if (sonarrEnabled) + { + var allSeries = SonarrApi.GetSeries(s.ApiKey, s.FullUri); + var selectedSeries = allSeries.FirstOrDefault(x => x.tvdbId == providerId) ?? new Series(); + return selectedSeries; + } + return new Series(); + }); + + var model = new List(); + + var requests = allResults as RequestedModel[] ?? allResults.ToArray(); + + var existingRequest = requests.FirstOrDefault(x => x.Type == RequestType.TvShow && x.TvDbId == providerId.ToString()); + var show = await Task.Run(() => TvApi.ShowLookupByTheTvDbId(providerId)); + var tvMazeEpisodesTask = await Task.Run(() => TvApi.EpisodeLookup(show.id)); + var tvMazeEpisodes = tvMazeEpisodesTask.ToList(); + + var sonarrEpisodes = new List(); + if (sonarrEnabled) + { + var sonarrSeries = await seriesTask; + var sonarrEp = SonarrApi.GetEpisodes(sonarrSeries.id.ToString(), s.ApiKey, s.FullUri); + sonarrEpisodes = sonarrEp?.ToList() ?? new List(); + } + + var plexSettings = await PlexService.GetSettingsAsync(); + if (plexSettings.Enable) + { + var plexCacheTask = await PlexChecker.GetEpisodes(providerId); + var plexCache = plexCacheTask.ToList(); + foreach (var ep in tvMazeEpisodes) + { + var requested = existingRequest?.Episodes + .Any(episodesModel => + ep.number == episodesModel.EpisodeNumber && + ep.season == episodesModel.SeasonNumber) ?? false; + + var alreadyInPlex = plexCache.Any(x => x.EpisodeNumber == ep.number && x.SeasonNumber == ep.season); + var inSonarr = + sonarrEpisodes.Any(x => x.seasonNumber == ep.season && x.episodeNumber == ep.number && x.hasFile); + + model.Add(new EpisodeListViewModel + { + Id = show.id, + SeasonNumber = ep.season, + EpisodeNumber = ep.number, + Requested = requested || alreadyInPlex || inSonarr, + Name = ep.name, + EpisodeId = ep.id + }); + } + } + var embySettings = await EmbySettings.GetSettingsAsync(); + if (embySettings.Enable) + { + var embyCacheTask = await EmbyChecker.GetEpisodes(providerId); + var cache = embyCacheTask.ToList(); + foreach (var ep in tvMazeEpisodes) + { + var requested = existingRequest?.Episodes + .Any(episodesModel => + ep.number == episodesModel.EpisodeNumber && + ep.season == episodesModel.SeasonNumber) ?? false; + + var alreadyInEmby = cache.Any(x => x.EpisodeNumber == ep.number && x.SeasonNumber == ep.season); + var inSonarr = + sonarrEpisodes.Any(x => x.seasonNumber == ep.season && x.episodeNumber == ep.number && x.hasFile); + + model.Add(new EpisodeListViewModel + { + Id = show.id, + SeasonNumber = ep.season, + EpisodeNumber = ep.number, + Requested = requested || alreadyInEmby || inSonarr, + Name = ep.name, + EpisodeId = ep.id + }); + } + } + return model; + + } + + public async Task CheckRequestLimit(PlexRequestSettings s, RequestType type) + { + if (IsAdmin) + return true; + + if (Security.HasPermissions(User, Permissions.BypassRequestLimit)) + return true; + + var requestLimit = GetRequestLimitForType(type, s); + if (requestLimit == 0) + { + return true; + } + + var limit = await RequestLimitRepo.GetAllAsync(); + var usersLimit = limit.FirstOrDefault(x => x.Username == Username && x.RequestType == type); + if (usersLimit == null) + { + // Have not set a requestLimit yet + return true; + } + + return requestLimit > usersLimit.RequestCount; + } + + private int GetRequestLimitForType(RequestType type, PlexRequestSettings s) + { + int requestLimit; + switch (type) + { + case RequestType.Movie: + requestLimit = s.MovieWeeklyRequestLimit; + break; + case RequestType.TvShow: + requestLimit = s.TvWeeklyRequestLimit; + break; + case RequestType.Album: + requestLimit = s.AlbumWeeklyRequestLimit; + break; + default: + throw new ArgumentOutOfRangeException(nameof(type), type, null); + } + return requestLimit; + } + + private async Task AddRequest(RequestedModel model, PlexRequestSettings settings, string message) + { + await RequestService.AddRequestAsync(model); + + if (ShouldSendNotification(model.Type, settings)) + { + var notificationModel = new NotificationModel + { + Title = model.Title, + User = Username, + DateTime = DateTime.Now, + NotificationType = NotificationType.NewRequest, + RequestType = model.Type, + ImgSrc = model.Type == RequestType.Movie ? $"https://image.tmdb.org/t/p/w300/{model.PosterPath}" : model.PosterPath + }; + await NotificationService.Publish(notificationModel); + } + + var limit = await RequestLimitRepo.GetAllAsync(); + var usersLimit = limit.FirstOrDefault(x => x.Username == Username && x.RequestType == model.Type); + if (usersLimit == null) + { + await RequestLimitRepo.InsertAsync(new RequestLimit + { + Username = Username, + RequestType = model.Type, + FirstRequestDate = DateTime.UtcNow, + RequestCount = 1 + }); + } + else + { + usersLimit.RequestCount++; + await RequestLimitRepo.UpdateAsync(usersLimit); + } + + return Response.AsJson(new JsonResponseModel { Result = true, Message = message }); + } + + private async Task UpdateRequest(RequestedModel model, PlexRequestSettings settings, string message) + { + await RequestService.UpdateRequestAsync(model); + + if (ShouldSendNotification(model.Type, settings)) + { + var notificationModel = new NotificationModel + { + Title = model.Title, + User = Username, + DateTime = DateTime.Now, + NotificationType = NotificationType.NewRequest, + RequestType = model.Type, + ImgSrc = model.Type == RequestType.Movie ? $"https://image.tmdb.org/t/p/w300/{model.PosterPath}" : model.PosterPath + }; + await NotificationService.Publish(notificationModel); + } + + var limit = await RequestLimitRepo.GetAllAsync(); + var usersLimit = limit.FirstOrDefault(x => x.Username == Username && x.RequestType == model.Type); + if (usersLimit == null) + { + await RequestLimitRepo.InsertAsync(new RequestLimit + { + Username = Username, + RequestType = model.Type, + FirstRequestDate = DateTime.UtcNow, + RequestCount = 1 + }); + } + else + { + usersLimit.RequestCount++; + await RequestLimitRepo.UpdateAsync(usersLimit); + } + + return Response.AsJson(new JsonResponseModel { Result = true, Message = message }); + } + + private IEnumerable GetListDifferences(IEnumerable existing, IEnumerable request) + { + var newRequest = request + .Select(r => + new EpisodesModel + { + SeasonNumber = r.SeasonNumber, + EpisodeNumber = r.EpisodeNumber + }).ToList(); + + return newRequest.Except(existing); + } + + private async Task> GetEpisodeRequestDifference(int showId, RequestedModel model) + { + var episodes = await GetEpisodes(showId); + var availableEpisodes = episodes.Where(x => x.Requested).ToList(); + var available = availableEpisodes.Select(a => new EpisodesModel { EpisodeNumber = a.EpisodeNumber, SeasonNumber = a.SeasonNumber }).ToList(); + + var diff = model.Episodes.Except(available); + return diff; + } + + public bool ShouldAutoApprove(RequestType requestType) + { + var admin = Security.HasPermissions(Context.CurrentUser, Permissions.Administrator); + // if the user is an admin, they go ahead and allow auto-approval + if (admin) return true; + + // check by request type if the category requires approval or not + switch (requestType) + { + case RequestType.Movie: + return Security.HasPermissions(User, Permissions.AutoApproveMovie); + case RequestType.TvShow: + return Security.HasPermissions(User, Permissions.AutoApproveTv); + case RequestType.Album: + return Security.HasPermissions(User, Permissions.AutoApproveAlbum); + default: + return false; + } + } + + private enum ShowSearchType + { + Popular, + Anticipated, + MostWatched, + Trending + } + + private async Task SendTv(RequestedModel model, Task sonarrSettings, RequestedModel existingRequest, string fullShowName, PlexRequestSettings settings) + { + model.Approved = true; + var s = await sonarrSettings; + var sender = new TvSenderOld(SonarrApi, SickrageApi, Cache); // TODO put back + if (s.Enabled) + { + var result = await sender.SendToSonarr(s, model); + if (!string.IsNullOrEmpty(result?.title)) + { + if (existingRequest != null) + { + return await UpdateRequest(model, settings, + $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"); + } + return + await + AddRequest(model, settings, + $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"); + } + Log.Debug("Error with sending to sonarr."); + return + Response.AsJson(ValidationHelper.SendSonarrError(result?.ErrorMessages ?? new List())); + } + + var srSettings = SickRageService.GetSettings(); + if (srSettings.Enabled) + { + var result = sender.SendToSickRage(srSettings, model); + if (result?.result == "success") + { + return await AddRequest(model, settings, + $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"); + } + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = result?.message ?? Resources.UI.Search_SickrageError + }); + } + + if (!srSettings.Enabled && !s.Enabled) + { + return await AddRequest(model, settings, $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"); + } + + return + Response.AsJson(new JsonResponseModel { Result = false, Message = Resources.UI.Search_TvNotSetUp }); + } + } } diff --git a/Ombi.UI/Views/Admin/Emby.cshtml b/Ombi.UI/Views/Admin/Emby.cshtml index f0b05c979..7ebceaf14 100644 --- a/Ombi.UI/Views/Admin/Emby.cshtml +++ b/Ombi.UI/Views/Admin/Emby.cshtml @@ -47,6 +47,8 @@
+ @Html.Checkbox(Model.EnableEpisodeSearching, "EnableEpisodeSearching", "Enable Episode Searching") +
diff --git a/Ombi.UI/Views/SystemStatus/Status.cshtml b/Ombi.UI/Views/SystemStatus/Status.cshtml index 8be76882a..71c960a6a 100644 --- a/Ombi.UI/Views/SystemStatus/Status.cshtml +++ b/Ombi.UI/Views/SystemStatus/Status.cshtml @@ -50,9 +50,9 @@ {
- + @**@
- + @**@ } else { From 29c8b456f413ee6736c843a2891b10aeced8fd9f Mon Sep 17 00:00:00 2001 From: tidusjar Date: Fri, 3 Feb 2017 14:35:41 +0000 Subject: [PATCH 25/61] Removed Plex Request from the notifications --- Ombi.Api/CouchPotatoApi.cs | 12 ++++++------ Ombi.Core.Migration/Migrations/Version2200.cs | 6 +++--- .../Notification/Templates/BasicRequestTemplate.html | 2 +- Ombi.Core/SettingModels/CustomizationSettings.cs | 1 + Ombi.Services/Jobs/Templates/MassEmailTemplate.html | 2 +- .../Jobs/Templates/RecentlyAddedTemplate.html | 2 +- Ombi.UI/Helpers/BaseUrlHelper.cs | 10 +++++++++- Ombi.UI/Views/Customization/Customization.cshtml | 3 ++- 8 files changed, 24 insertions(+), 14 deletions(-) diff --git a/Ombi.Api/CouchPotatoApi.cs b/Ombi.Api/CouchPotatoApi.cs index a6434c30f..8c28faa34 100644 --- a/Ombi.Api/CouchPotatoApi.cs +++ b/Ombi.Api/CouchPotatoApi.cs @@ -100,9 +100,9 @@ namespace Ombi.Api var obj = RetryHandler.Execute(() => Api.Execute(request, url), (exception, timespan) => Log.Error(exception, "Exception when calling GetStatus for CP, Retrying {0}", timespan), new TimeSpan[] { - TimeSpan.FromSeconds (2), - TimeSpan.FromSeconds(5), - TimeSpan.FromSeconds(10)}); + TimeSpan.FromSeconds (1), + TimeSpan.FromSeconds(2), + TimeSpan.FromSeconds(3)}); return obj; } @@ -140,9 +140,9 @@ namespace Ombi.Api { var obj = RetryHandler.Execute(() => Api.Execute(request, baseUrl), (exception, timespan) => Log.Error(exception, "Exception when calling GetMovies for CP, Retrying {0}", timespan), new[] { - TimeSpan.FromSeconds (5), - TimeSpan.FromSeconds(10), - TimeSpan.FromSeconds(30) + TimeSpan.FromSeconds (1), + TimeSpan.FromSeconds(5), + TimeSpan.FromSeconds(5) }); return obj; diff --git a/Ombi.Core.Migration/Migrations/Version2200.cs b/Ombi.Core.Migration/Migrations/Version2200.cs index ac9b5d1a7..f2b3b6fd2 100644 --- a/Ombi.Core.Migration/Migrations/Version2200.cs +++ b/Ombi.Core.Migration/Migrations/Version2200.cs @@ -52,8 +52,8 @@ namespace Ombi.Core.Migration.Migrations public void Start(IDbConnection con) { UpdatePlexSettings(); - //UpdateCustomSettings(); Turned off the migration for now until the search has been improved on. - //UpdateSchema(con, Version); + UpdateCustomSettings(); + UpdateSchema(con, Version); } private void UpdatePlexSettings() @@ -68,7 +68,7 @@ namespace Ombi.Core.Migration.Migrations { var settings = Customization.GetSettings(); - settings.NewSearch = true; // Use the new search + settings.EnableIssues = true; Customization.SaveSettings(settings); diff --git a/Ombi.Core/Notification/Templates/BasicRequestTemplate.html b/Ombi.Core/Notification/Templates/BasicRequestTemplate.html index 3e1109517..3e4d5cf56 100644 --- a/Ombi.Core/Notification/Templates/BasicRequestTemplate.html +++ b/Ombi.Core/Notification/Templates/BasicRequestTemplate.html @@ -144,7 +144,7 @@ diff --git a/Ombi.Core/SettingModels/CustomizationSettings.cs b/Ombi.Core/SettingModels/CustomizationSettings.cs index d7aff1e51..98468c9be 100644 --- a/Ombi.Core/SettingModels/CustomizationSettings.cs +++ b/Ombi.Core/SettingModels/CustomizationSettings.cs @@ -54,6 +54,7 @@ namespace Ombi.Core.SettingModels public int DefaultLang { get; set; } public bool NewSearch { get; set; } + public bool EnableIssues { get; set; } } } \ No newline at end of file diff --git a/Ombi.Services/Jobs/Templates/MassEmailTemplate.html b/Ombi.Services/Jobs/Templates/MassEmailTemplate.html index 02214c6af..18a724b93 100644 --- a/Ombi.Services/Jobs/Templates/MassEmailTemplate.html +++ b/Ombi.Services/Jobs/Templates/MassEmailTemplate.html @@ -144,7 +144,7 @@
- +
diff --git a/Ombi.Services/Jobs/Templates/RecentlyAddedTemplate.html b/Ombi.Services/Jobs/Templates/RecentlyAddedTemplate.html index f5b208138..932aae99f 100644 --- a/Ombi.Services/Jobs/Templates/RecentlyAddedTemplate.html +++ b/Ombi.Services/Jobs/Templates/RecentlyAddedTemplate.html @@ -144,7 +144,7 @@
- +
diff --git a/Ombi.UI/Helpers/BaseUrlHelper.cs b/Ombi.UI/Helpers/BaseUrlHelper.cs index 99c37459e..cb0600ce7 100644 --- a/Ombi.UI/Helpers/BaseUrlHelper.cs +++ b/Ombi.UI/Helpers/BaseUrlHelper.cs @@ -314,6 +314,7 @@ namespace Ombi.UI.Helpers { url = $"/{content}{url}"; } + var returnString = context.Request.Path == url ? $"
  • {title}
  • " : $"
  • {title}
  • "; @@ -328,7 +329,14 @@ namespace Ombi.UI.Helpers { url = $"/{content}{url}"; } - + if (url.Contains("issues")) + { + var custom = GetCustomizationSettings(); + if (!custom.EnableIssues) + { + return helper.Raw(string.Empty); + } + } var returnString = context.Request.Path == url ? $"
  • {title} {extraHtml}
  • " : $"
  • {title} {extraHtml}
  • "; diff --git a/Ombi.UI/Views/Customization/Customization.cshtml b/Ombi.UI/Views/Customization/Customization.cshtml index 583dbabae..a220f950d 100644 --- a/Ombi.UI/Views/Customization/Customization.cshtml +++ b/Ombi.UI/Views/Customization/Customization.cshtml @@ -104,7 +104,8 @@ - @Html.Checkbox(Model.Settings.NewSearch, "NewSearch", "Use New Search") + @*@Html.Checkbox(Model.Settings.NewSearch, "NewSearch", "Use New Search")*@ + @Html.Checkbox(Model.Settings.EnableIssues, "EnableIssues", "Enable Issues")
    From 425522ea06beaf7143c8a9530038aef7356c3870 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Fri, 3 Feb 2017 14:45:39 +0000 Subject: [PATCH 26/61] Fixed the mass email, it was only being set to users with the newsletter feature #358 --- Ombi.Services/Interfaces/IRecentlyAdded.cs | 2 +- Ombi.Services/Jobs/RecentlyAdded.cs | 73 +++++++++++++++---- .../Admin/ScheduledJobsRunnerModule.cs | 2 +- 3 files changed, 59 insertions(+), 18 deletions(-) diff --git a/Ombi.Services/Interfaces/IRecentlyAdded.cs b/Ombi.Services/Interfaces/IRecentlyAdded.cs index c18ca8e27..203d4804b 100644 --- a/Ombi.Services/Interfaces/IRecentlyAdded.cs +++ b/Ombi.Services/Interfaces/IRecentlyAdded.cs @@ -6,6 +6,6 @@ namespace Ombi.Services.Jobs { void Execute(IJobExecutionContext context); void RecentlyAddedAdminTest(); - void Start(); + void StartNewsLetter(); } } \ No newline at end of file diff --git a/Ombi.Services/Jobs/RecentlyAdded.cs b/Ombi.Services/Jobs/RecentlyAdded.cs index 9ee52f231..74474821c 100644 --- a/Ombi.Services/Jobs/RecentlyAdded.cs +++ b/Ombi.Services/Jobs/RecentlyAdded.cs @@ -67,8 +67,6 @@ namespace Ombi.Services.Jobs private IPlexApi Api { get; } private TvMazeApi TvApi = new TvMazeApi(); private readonly TheMovieDbApi _movieApi = new TheMovieDbApi(); - private const int MetadataTypeTv = 4; - private const int MetadataTypeMovie = 1; private ISettingsService PlexSettings { get; } private ISettingsService EmailSettings { get; } private ISettingsService NewsletterSettings { get; } @@ -78,7 +76,7 @@ namespace Ombi.Services.Jobs private static readonly Logger Log = LogManager.GetCurrentClassLogger(); - public void Start() + public void StartNewsLetter() { try { @@ -88,7 +86,7 @@ namespace Ombi.Services.Jobs return; } JobRecord.SetRunning(true, JobNames.RecentlyAddedEmail); - Start(settings); + StartNewsLetter(settings); } catch (Exception e) { @@ -102,35 +100,33 @@ namespace Ombi.Services.Jobs } public void Execute(IJobExecutionContext context) { - Start(); + StartNewsLetter(); } public void RecentlyAddedAdminTest() { Log.Debug("Starting Recently Added Newsletter Test"); var settings = NewsletterSettings.GetSettings(); - Start(settings, true); + StartNewsLetter(settings, true); } + public void MassEmailAdminTest(string html, string subject) { Log.Debug("Starting Mass Email Test"); - var settings = NewsletterSettings.GetSettings(); - var plexSettings = PlexSettings.GetSettings(); var template = new MassEmailTemplate(); var body = template.LoadTemplate(html); - Send(settings, body, plexSettings, true, subject); + SendMassEmail(body, subject, true); } + public void SendMassEmail(string html, string subject) { Log.Debug("Starting Mass Email Test"); - var settings = NewsletterSettings.GetSettings(); - var plexSettings = PlexSettings.GetSettings(); var template = new MassEmailTemplate(); var body = template.LoadTemplate(html); - Send(settings, body, plexSettings, false, subject); + SendMassEmail(body, subject, false); } - private void Start(NewletterSettings newletterSettings, bool testEmail = false) + private void StartNewsLetter(NewletterSettings newletterSettings, bool testEmail = false) { var sb = new StringBuilder(); var plexSettings = PlexSettings.GetSettings(); @@ -222,7 +218,7 @@ namespace Ombi.Services.Jobs string escapedHtml = new string(html.Where(c => !char.IsControl(c)).ToArray()); Log.Debug(escapedHtml); - Send(newletterSettings, escapedHtml, plexSettings, testEmail); + SendNewsletter(newletterSettings, escapedHtml, plexSettings, testEmail); } private void GenerateMovieHtml(List movies, PlexSettings plexSettings, StringBuilder sb) @@ -457,9 +453,49 @@ namespace Ombi.Services.Jobs sb.Append("
    - +


    "); } - private void Send(NewletterSettings newletterSettings, string html, PlexSettings plexSettings, bool testEmail = false, string subject = "New Content on Plex!") + + private void SendMassEmail(string html, string subject, bool testEmail) + { + var settings = EmailSettings.GetSettings(); + + if (!settings.Enabled || string.IsNullOrEmpty(settings.EmailHost)) + { + return; + } + + var body = new BodyBuilder { HtmlBody = html, TextBody = "This email is only available on devices that support HTML." }; + + var message = new MimeMessage + { + Body = body.ToMessageBody(), + Subject = subject + }; + Log.Debug("Created Plain/HTML MIME body"); + + if (!testEmail) + { + var users = UserHelper.GetUsers(); // Get all users + if (users != null) + { + foreach (var user in users) + { + if (!string.IsNullOrEmpty(user.EmailAddress)) + { + message.Bcc.Add(new MailboxAddress(user.Username, user.EmailAddress)); // BCC everyone + } + } + } + } + message.Bcc.Add(new MailboxAddress(settings.EmailUsername, settings.RecipientEmail)); // Include the admin + + message.From.Add(new MailboxAddress(settings.EmailUsername, settings.EmailSender)); + SendMail(settings, message); + } + + // TODO Emby + private void SendNewsletter(NewletterSettings newletterSettings, string html, bool testEmail = false, string subject = "New Content on Plex!") { - Log.Debug("Entering Send"); + Log.Debug("Entering SendNewsletter"); var settings = EmailSettings.GetSettings(); if (!settings.Enabled || string.IsNullOrEmpty(settings.EmailHost)) @@ -506,6 +542,11 @@ namespace Ombi.Services.Jobs message.Bcc.Add(new MailboxAddress(settings.EmailUsername, settings.RecipientEmail)); // Include the admin message.From.Add(new MailboxAddress(settings.EmailUsername, settings.EmailSender)); + SendMail(settings, message); + } + + private void SendMail(EmailNotificationSettings settings, MimeMessage message) + { try { using (var client = new SmtpClient()) diff --git a/Ombi.UI/Modules/Admin/ScheduledJobsRunnerModule.cs b/Ombi.UI/Modules/Admin/ScheduledJobsRunnerModule.cs index a08bd056d..2fbe77eb0 100644 --- a/Ombi.UI/Modules/Admin/ScheduledJobsRunnerModule.cs +++ b/Ombi.UI/Modules/Admin/ScheduledJobsRunnerModule.cs @@ -142,7 +142,7 @@ namespace Ombi.UI.Modules.Admin } if (key.Equals(JobNames.RecentlyAddedEmail, StringComparison.CurrentCultureIgnoreCase)) { - RecentlyAdded.Start(); + RecentlyAdded.StartNewsLetter(); } if (key.Equals(JobNames.FaultQueueHandler, StringComparison.CurrentCultureIgnoreCase)) { From 86144f59bb3f4ec8da530f169740e5b49392cda0 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Fri, 3 Feb 2017 14:46:11 +0000 Subject: [PATCH 27/61] Fixed build --- Ombi.Services/Jobs/RecentlyAdded.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Ombi.Services/Jobs/RecentlyAdded.cs b/Ombi.Services/Jobs/RecentlyAdded.cs index 74474821c..4f9e3b5ca 100644 --- a/Ombi.Services/Jobs/RecentlyAdded.cs +++ b/Ombi.Services/Jobs/RecentlyAdded.cs @@ -218,7 +218,7 @@ namespace Ombi.Services.Jobs string escapedHtml = new string(html.Where(c => !char.IsControl(c)).ToArray()); Log.Debug(escapedHtml); - SendNewsletter(newletterSettings, escapedHtml, plexSettings, testEmail); + SendNewsletter(newletterSettings, escapedHtml, testEmail); } private void GenerateMovieHtml(List movies, PlexSettings plexSettings, StringBuilder sb) From 8393a31a48655a8c3bc0c384b98741cd16f523fc Mon Sep 17 00:00:00 2001 From: tidusjar Date: Fri, 3 Feb 2017 20:26:28 +0000 Subject: [PATCH 28/61] Reworked the newsletter for Emby! Need to rework it for Plex and use the new way to do it. Fixed collections for Emby #435 --- Ombi.Api.Interfaces/IEmbyApi.cs | 1 + Ombi.Api/EmbyApi.cs | 21 ++ Ombi.Core.Migration/Migrations/Version2200.cs | 8 + Ombi.Services/Jobs/EmbyContentCacher.cs | 76 ++-- Ombi.Services/Jobs/EmbyEpisodeCacher.cs | 51 ++- .../EmbyRecentlyAddedNewsletter.cs | 341 ++++++++++++++++++ .../IEmbyAddedNewsletter.cs | 7 + .../RecentlyAddedNewsletter.cs} | 169 +++++---- Ombi.Services/Ombi.Services.csproj | 4 +- Ombi.Store/Models/Emby/EmbyContent.cs | 1 + Ombi.Store/Models/Emby/EmbyEpisodes.cs | 2 + Ombi.Store/Models/RecenetlyAddedLog.cs | 40 ++ Ombi.Store/Ombi.Store.csproj | 1 + Ombi.Store/SqlTables.sql | 17 +- Ombi.UI/Jobs/Scheduler.cs | 3 +- Ombi.UI/Modules/Admin/AdminModule.cs | 5 + Ombi.UI/NinjectModules/ServicesModule.cs | 6 +- 17 files changed, 625 insertions(+), 128 deletions(-) create mode 100644 Ombi.Services/Jobs/RecentlyAddedNewsletter/EmbyRecentlyAddedNewsletter.cs create mode 100644 Ombi.Services/Jobs/RecentlyAddedNewsletter/IEmbyAddedNewsletter.cs rename Ombi.Services/Jobs/{RecentlyAdded.cs => RecentlyAddedNewsletter/RecentlyAddedNewsletter.cs} (79%) create mode 100644 Ombi.Store/Models/RecenetlyAddedLog.cs diff --git a/Ombi.Api.Interfaces/IEmbyApi.cs b/Ombi.Api.Interfaces/IEmbyApi.cs index 7a2e4f6c4..ddc85868c 100644 --- a/Ombi.Api.Interfaces/IEmbyApi.cs +++ b/Ombi.Api.Interfaces/IEmbyApi.cs @@ -9,6 +9,7 @@ namespace Ombi.Api.Interfaces EmbyItemContainer GetAllMovies(string apiKey, string userId, Uri baseUri); EmbyItemContainer GetAllShows(string apiKey, string userId, Uri baseUri); EmbyItemContainer GetAllEpisodes(string apiKey, string userId, Uri baseUri); + EmbyItemContainer GetCollection(string mediaId, string apiKey, string userId, Uri baseUrl); List GetUsers(Uri baseUri, string apiKey); EmbyItemContainer ViewLibrary(string apiKey, string userId, Uri baseUri); EmbyInformation GetInformation(string mediaId, EmbyMediaType type, string apiKey, string userId, Uri baseUri); diff --git a/Ombi.Api/EmbyApi.cs b/Ombi.Api/EmbyApi.cs index bbd25dadd..1cfc0bf0a 100644 --- a/Ombi.Api/EmbyApi.cs +++ b/Ombi.Api/EmbyApi.cs @@ -103,6 +103,27 @@ namespace Ombi.Api return GetAll("Episode", apiKey, userId, baseUri); } + public EmbyItemContainer GetCollection(string mediaId, string apiKey, string userId, Uri baseUrl) + { + var request = new RestRequest + { + Resource = "emby/users/{userId}/items?parentId={mediaId}", + Method = Method.GET + }; + + request.AddUrlSegment("userId", userId); + request.AddUrlSegment("mediaId", mediaId); + + AddHeaders(request, apiKey); + + + var policy = RetryHandler.RetryAndWaitPolicy((exception, timespan) => Log.Error(exception, "Exception when calling GetCollections for Emby, Retrying {0}", timespan), new[] { + TimeSpan.FromSeconds (1), + TimeSpan.FromSeconds(5) + }); + return policy.Execute(() => Api.ExecuteJson>(request, baseUrl)); + } + public EmbyInformation GetInformation(string mediaId, EmbyMediaType type, string apiKey, string userId, Uri baseUri) { var request = new RestRequest diff --git a/Ombi.Core.Migration/Migrations/Version2200.cs b/Ombi.Core.Migration/Migrations/Version2200.cs index f2b3b6fd2..79939c8e3 100644 --- a/Ombi.Core.Migration/Migrations/Version2200.cs +++ b/Ombi.Core.Migration/Migrations/Version2200.cs @@ -30,6 +30,7 @@ using System.Data; using NLog; using Ombi.Core.SettingModels; +using Ombi.Store; namespace Ombi.Core.Migration.Migrations { @@ -53,9 +54,16 @@ namespace Ombi.Core.Migration.Migrations { UpdatePlexSettings(); UpdateCustomSettings(); + AddNewColumns(con); UpdateSchema(con, Version); } + private void AddNewColumns(IDbConnection con) + { + con.AlterTable("EmbyContent", "ADD", "AddedAt", true, "VARCHAR(50)"); + con.AlterTable("EmbyEpisodes", "ADD", "AddedAt", true, "VARCHAR(50)"); + } + private void UpdatePlexSettings() { #if !DEBUG diff --git a/Ombi.Services/Jobs/EmbyContentCacher.cs b/Ombi.Services/Jobs/EmbyContentCacher.cs index 65a47df32..a3eacab65 100644 --- a/Ombi.Services/Jobs/EmbyContentCacher.cs +++ b/Ombi.Services/Jobs/EmbyContentCacher.cs @@ -46,7 +46,7 @@ namespace Ombi.Services.Jobs public class EmbyContentCacher : IJob, IEmbyContentCacher { public EmbyContentCacher(ISettingsService embySettings, IRequestService request, IEmbyApi emby, ICacheProvider cache, - IJobRecord rec, IRepository repo,IRepository content) + IJobRecord rec, IRepository repo, IRepository content) { Emby = embySettings; RequestService = request; @@ -108,35 +108,23 @@ namespace Ombi.Services.Jobs foreach (var m in movies) { - var movieInfo = EmbyApi.GetInformation(m.Id, EmbyMediaType.Movie, embySettings.ApiKey, - embySettings.AdministratorId, embySettings.FullUri).MovieInformation; - - if (string.IsNullOrEmpty(movieInfo.ProviderIds.Imdb)) + if (m.Type.Equals("boxset", StringComparison.CurrentCultureIgnoreCase)) { - Log.Error("Provider Id on movie {0} is null", movieInfo.Name); - continue; + var info = EmbyApi.GetCollection(m.Id, embySettings.ApiKey, + embySettings.AdministratorId, embySettings.FullUri); + foreach (var item in info.Items) + { + var movieInfo = EmbyApi.GetInformation(item.Id, EmbyMediaType.Movie, embySettings.ApiKey, + embySettings.AdministratorId, embySettings.FullUri).MovieInformation; + ProcessMovies(movieInfo); + } } - - // Check if it exists - var item = EmbyContent.Custom(connection => + else { - connection.Open(); - var media = connection.QueryFirstOrDefault("select * from EmbyContent where ProviderId = @ProviderId and type = @type", new { ProviderId = movieInfo.ProviderIds.Imdb, type = 0 }); - connection.Dispose(); - return media; - }); + var movieInfo = EmbyApi.GetInformation(m.Id, EmbyMediaType.Movie, embySettings.ApiKey, + embySettings.AdministratorId, embySettings.FullUri).MovieInformation; - if (item == null) - { - // Doesn't exist, insert it - EmbyContent.Insert(new EmbyContent - { - ProviderId = movieInfo.ProviderIds.Imdb, - PremierDate = movieInfo.PremiereDate, - Title = movieInfo.Name, - Type = Store.Models.Plex.EmbyMediaType.Movie, - EmbyId = m.Id - }); + ProcessMovies(movieInfo); } } @@ -170,7 +158,8 @@ namespace Ombi.Services.Jobs PremierDate = tvInfo.PremiereDate, Title = tvInfo.Name, Type = Store.Models.Plex.EmbyMediaType.Series, - EmbyId = t.Id + EmbyId = t.Id, + AddedAt = DateTime.UtcNow }); } } @@ -216,7 +205,7 @@ namespace Ombi.Services.Jobs } } - + private bool ValidateSettings(EmbySettings emby) { @@ -249,5 +238,36 @@ namespace Ombi.Services.Jobs Job.SetRunning(false, JobNames.EmbyCacher); } } + + private void ProcessMovies(EmbyMovieInformation movieInfo) + { + if (string.IsNullOrEmpty(movieInfo.ProviderIds.Imdb)) + { + Log.Error("Provider Id on movie {0} is null", movieInfo.Name); + return; + } + // Check if it exists + var item = EmbyContent.Custom(connection => + { + connection.Open(); + var media = connection.QueryFirstOrDefault("select * from EmbyContent where ProviderId = @ProviderId and type = @type", new { ProviderId = movieInfo.ProviderIds.Imdb, type = 0 }); + connection.Dispose(); + return media; + }); + + if (item == null) + { + // Doesn't exist, insert it + EmbyContent.Insert(new EmbyContent + { + ProviderId = movieInfo.ProviderIds.Imdb, + PremierDate = movieInfo.PremiereDate, + Title = movieInfo.Name, + Type = Store.Models.Plex.EmbyMediaType.Movie, + EmbyId = movieInfo.Id, + AddedAt = DateTime.UtcNow + }); + } + } } } \ No newline at end of file diff --git a/Ombi.Services/Jobs/EmbyEpisodeCacher.cs b/Ombi.Services/Jobs/EmbyEpisodeCacher.cs index 5679a24b9..0135592cc 100644 --- a/Ombi.Services/Jobs/EmbyEpisodeCacher.cs +++ b/Ombi.Services/Jobs/EmbyEpisodeCacher.cs @@ -28,6 +28,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Dapper; using NLog; using Ombi.Api.Interfaces; using Ombi.Api.Models.Emby; @@ -65,7 +66,8 @@ namespace Ombi.Services.Jobs private const string TableName = "EmbyEpisodes"; - + // Note, once an episode exists, we store it and it always exists. + // We might want to look at checking if something has been removed from the server in the future. public void CacheEpisodes(EmbySettings settings) { var allEpisodes = EmbyApi.GetAllEpisodes(settings.ApiKey, settings.AdministratorId, settings.FullUri); @@ -74,25 +76,40 @@ namespace Ombi.Services.Jobs { var epInfo = EmbyApi.GetInformation(ep.Id, EmbyMediaType.Episode, settings.ApiKey, settings.AdministratorId, settings.FullUri); - if (epInfo.EpisodeInformation?.ProviderIds?.Tvdb == null) - { - continue; - } - model.Add(new EmbyEpisodes + if (epInfo.EpisodeInformation?.ProviderIds?.Tvdb == null) { - EmbyId = ep.Id, - EpisodeNumber = ep.IndexNumber, - SeasonNumber = ep.ParentIndexNumber, - EpisodeTitle = ep.Name, - ParentId = ep.SeriesId, - ShowTitle = ep.SeriesName, - ProviderId = epInfo.EpisodeInformation.ProviderIds.Tvdb - }); - } + continue; + } - // Delete all of the current items - Repo.DeleteAll(TableName); + // Check it this episode exists + var item = Repo.Custom(connection => + { + connection.Open(); + var media = + connection.QueryFirstOrDefault( + "select * from EmbyEpisodes where ProviderId = @ProviderId", + new {ProviderId = epInfo.EpisodeInformation?.ProviderIds?.Tvdb}); + connection.Dispose(); + return media; + }); + if (item == null) + { + // add it + model.Add(new EmbyEpisodes + { + EmbyId = ep.Id, + EpisodeNumber = ep.IndexNumber, + SeasonNumber = ep.ParentIndexNumber, + EpisodeTitle = ep.Name, + ParentId = ep.SeriesId, + ShowTitle = ep.SeriesName, + ProviderId = epInfo.EpisodeInformation.ProviderIds.Tvdb, + AddedAt = DateTime.UtcNow + }); + } + } + // Insert the new items var result = Repo.BatchInsert(model, TableName, typeof(EmbyEpisodes).GetPropertyNames()); diff --git a/Ombi.Services/Jobs/RecentlyAddedNewsletter/EmbyRecentlyAddedNewsletter.cs b/Ombi.Services/Jobs/RecentlyAddedNewsletter/EmbyRecentlyAddedNewsletter.cs new file mode 100644 index 000000000..cfe752c8a --- /dev/null +++ b/Ombi.Services/Jobs/RecentlyAddedNewsletter/EmbyRecentlyAddedNewsletter.cs @@ -0,0 +1,341 @@ +#region Copyright + +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: RecentlyAddedModel.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ + +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NLog; +using Ombi.Api; +using Ombi.Api.Interfaces; +using Ombi.Api.Models.Emby; +using Ombi.Core; +using Ombi.Core.SettingModels; +using Ombi.Services.Jobs.Templates; +using Ombi.Store.Models; +using Ombi.Store.Models.Emby; +using Ombi.Store.Repository; +using EmbyMediaType = Ombi.Store.Models.Plex.EmbyMediaType; + +namespace Ombi.Services.Jobs.RecentlyAddedNewsletter +{ + public class EmbyAddedNewsletter : HtmlTemplateGenerator, IEmbyAddedNewsletter + { + public EmbyAddedNewsletter(IEmbyApi api, ISettingsService embySettings, + ISettingsService email, + ISettingsService newsletter, IRepository log, + IRepository embyContent, IRepository episodes) + { + Api = api; + EmbySettings = embySettings; + EmailSettings = email; + NewsletterSettings = newsletter; + Content = embyContent; + MovieApi = new TheMovieDbApi(); + TvApi = new TvMazeApi(); + Episodes = episodes; + RecentlyAddedLog = log; + } + + private IEmbyApi Api { get; } + private TheMovieDbApi MovieApi { get; } + private TvMazeApi TvApi { get; } + private ISettingsService EmbySettings { get; } + private ISettingsService EmailSettings { get; } + private ISettingsService NewsletterSettings { get; } + private IRepository Content { get; } + private IRepository Episodes { get; } + private IRepository RecentlyAddedLog { get; } + + private static readonly Logger Log = LogManager.GetCurrentClassLogger(); + + public string GetNewsletterHtml(bool test) + { + try + { + return GetHtml(test); + } + catch (Exception e) + { + Log.Error(e); + return string.Empty; + } + } + + private class EmbyRecentlyAddedModel + { + public EmbyInformation EmbyInformation { get; set; } + public EmbyContent EmbyContent { get; set; } + public List EpisodeInformation { get; set; } + } + + private string GetHtml(bool test) + { + var sb = new StringBuilder(); + var embySettings = EmbySettings.GetSettings(); + + var embyContent = Content.GetAll().ToList(); + + var series = embyContent.Where(x => x.Type == EmbyMediaType.Series).ToList(); + var episodes = Episodes.GetAll().ToList(); + var movie = embyContent.Where(x => x.Type == EmbyMediaType.Movie).ToList(); + + var recentlyAdded = RecentlyAddedLog.GetAll(); + + var filteredMovies = movie.Where(m => recentlyAdded.All(x => x.ProviderId != m.ProviderId)).ToList(); + var filteredEp = episodes.Where(m => recentlyAdded.All(x => x.ProviderId != m.ProviderId)).ToList(); + + + var info = new List(); + foreach (var m in filteredMovies) + { + + var i = Api.GetInformation(m.EmbyId, Ombi.Api.Models.Emby.EmbyMediaType.Movie, + embySettings.ApiKey, embySettings.AdministratorId, embySettings.FullUri); + info.Add(new EmbyRecentlyAddedModel + { + EmbyInformation = i, + EmbyContent = m + }); + } + GenerateMovieHtml(info, sb); + + info.Clear(); + foreach (var t in series) + { + var i = Api.GetInformation(t.EmbyId, Ombi.Api.Models.Emby.EmbyMediaType.Series, + embySettings.ApiKey, embySettings.AdministratorId, embySettings.FullUri); + var ep = filteredEp.Where(x => x.ParentId == t.EmbyId); + + if (ep.Any()) + { + var episodeList = new List(); + foreach (var embyEpisodese in ep) + { + var epInfo = Api.GetInformation(embyEpisodese.EmbyId, Ombi.Api.Models.Emby.EmbyMediaType.Episode, + embySettings.ApiKey, embySettings.AdministratorId, embySettings.FullUri); + episodeList.Add(epInfo.EpisodeInformation); + } + info.Add(new EmbyRecentlyAddedModel + { + EmbyContent = t, + EmbyInformation = i, + EpisodeInformation = episodeList + }); + } + } + GenerateTvHtml(info, sb); + + var template = new RecentlyAddedTemplate(); + var html = template.LoadTemplate(sb.ToString()); + Log.Debug("Loaded the template"); + + if (!test) + { + foreach (var a in filteredMovies) + { + RecentlyAddedLog.Insert(new RecentlyAddedLog + { + ProviderId = a.ProviderId, + AddedAt = DateTime.UtcNow + }); + } + foreach (var a in filteredEp) + { + RecentlyAddedLog.Insert(new RecentlyAddedLog + { + ProviderId = a.ProviderId, + AddedAt = DateTime.UtcNow + }); + } + } + + var escapedHtml = new string(html.Where(c => !char.IsControl(c)).ToArray()); + Log.Debug(escapedHtml); + return escapedHtml; + } + + private void GenerateMovieHtml(IEnumerable movies, StringBuilder sb) + { + if (!movies.Any()) + { + return; + } + var orderedMovies = movies.OrderByDescending(x => x.EmbyContent.AddedAt).Select(x => x.EmbyInformation.MovieInformation).ToList(); + sb.Append("

    New Movies:



    "); + sb.Append( + ""); + foreach (var movie in orderedMovies) + { + try + { + + var imdbId = movie.ProviderIds.Imdb; + var info = MovieApi.GetMovieInformation(imdbId).Result; + if (info == null) + { + throw new Exception($"Movie with Imdb id {imdbId} returned null from the MovieApi"); + } + AddImageInsideTable(sb, $"https://image.tmdb.org/t/p/w500{info.BackdropPath}"); + + sb.Append(""); + sb.Append( + "
    "); + + Href(sb, $"https://www.imdb.com/title/{info.ImdbId}/"); + Header(sb, 3, $"{info.Title} {info.ReleaseDate?.ToString("yyyy") ?? 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); + } + catch (Exception e) + { + Log.Error(e); + Log.Error("Error for movie with IMDB Id = {0}", movie.ProviderIds.Imdb); + } + finally + { + EndLoopHtml(sb); + } + + } + sb.Append("


    "); + } + + private class TvModel + { + public EmbySeriesInformation Series { get; set; } + public List Episodes { get; set; } + } + private void GenerateTvHtml(List tv, StringBuilder sb) + { + if (!tv.Any()) + { + return; + } + var orderedTv = tv.OrderByDescending(x => x.EmbyContent.AddedAt).ToList(); + + // TV + sb.Append("

    New Episodes:



    "); + sb.Append( + ""); + foreach (var t in orderedTv) + { + var seriesItem = t.EmbyInformation.SeriesInformation; + var relatedEpisodes = t.EpisodeInformation; + + + try + { + var info = TvApi.ShowLookupByTheTvDbId(int.Parse(seriesItem.ProviderIds.Tvdb)); + + var banner = info.image?.original; + if (!string.IsNullOrEmpty(banner)) + { + banner = banner.Replace("http", "https"); // Always use the Https banners + } + AddImageInsideTable(sb, banner); + + sb.Append(""); + sb.Append( + "
    "); + + var title = $"{seriesItem.Name} {seriesItem.PremiereDate.Year}"; + + Href(sb, $"https://www.imdb.com/title/{info.externals.imdb}/"); + Header(sb, 3, title); + EndTag(sb, "a"); + + var results = relatedEpisodes.GroupBy(p => p.ParentIndexNumber, + (key, g) => new + { + ParentIndexNumber = key, + IndexNumber = g.ToList() + } + ); + // Group the episodes + foreach (var embyEpisodeInformation in results.OrderBy(x => x.ParentIndexNumber)) + { + var epSb = new StringBuilder(); + for (var i = 0; i < embyEpisodeInformation.IndexNumber.Count; i++) + { + var ep = embyEpisodeInformation.IndexNumber[i]; + if (i < embyEpisodeInformation.IndexNumber.Count) + { + epSb.Append($"{ep.IndexNumber},"); + } + else + { + epSb.Append(ep); + } + } + AddParagraph(sb, $"Season: {embyEpisodeInformation.ParentIndexNumber}, Episode: {epSb}"); + } + + if (info.genres.Any()) + { + AddParagraph(sb, $"Genre: {string.Join(", ", info.genres.Select(x => x.ToString()).ToArray())}"); + } + + AddParagraph(sb, string.IsNullOrEmpty(seriesItem.Overview) ? info.summary : seriesItem.Overview); + } + catch (Exception e) + { + Log.Error(e); + } + finally + { + EndLoopHtml(sb); + } + } + sb.Append("


    "); + } + + + + + private void EndLoopHtml(StringBuilder sb) + { + //NOTE: BR have to be in TD's as per html spec or it will be put outside of the table... + //Source: http://stackoverflow.com/questions/6588638/phantom-br-tag-rendered-by-browsers-prior-to-table-tag + sb.Append("
    "); + sb.Append("
    "); + sb.Append("
    "); + sb.Append(""); + sb.Append(""); + } + + } +} \ No newline at end of file diff --git a/Ombi.Services/Jobs/RecentlyAddedNewsletter/IEmbyAddedNewsletter.cs b/Ombi.Services/Jobs/RecentlyAddedNewsletter/IEmbyAddedNewsletter.cs new file mode 100644 index 000000000..bef09ce6e --- /dev/null +++ b/Ombi.Services/Jobs/RecentlyAddedNewsletter/IEmbyAddedNewsletter.cs @@ -0,0 +1,7 @@ +namespace Ombi.Services.Jobs.RecentlyAddedNewsletter +{ + public interface IEmbyAddedNewsletter + { + string GetNewsletterHtml(bool test); + } +} \ No newline at end of file diff --git a/Ombi.Services/Jobs/RecentlyAdded.cs b/Ombi.Services/Jobs/RecentlyAddedNewsletter/RecentlyAddedNewsletter.cs similarity index 79% rename from Ombi.Services/Jobs/RecentlyAdded.cs rename to Ombi.Services/Jobs/RecentlyAddedNewsletter/RecentlyAddedNewsletter.cs index 4f9e3b5ca..0183058d0 100644 --- a/Ombi.Services/Jobs/RecentlyAdded.cs +++ b/Ombi.Services/Jobs/RecentlyAddedNewsletter/RecentlyAddedNewsletter.cs @@ -46,14 +46,15 @@ using Ombi.Services.Interfaces; using Ombi.Services.Jobs.Templates; using Quartz; -namespace Ombi.Services.Jobs +namespace Ombi.Services.Jobs.RecentlyAddedNewsletter { - public class RecentlyAdded : HtmlTemplateGenerator, IJob, IRecentlyAdded, IMassEmail + public class RecentlyAddedNewsletter : HtmlTemplateGenerator, IJob, IRecentlyAdded, IMassEmail { - public RecentlyAdded(IPlexApi api, ISettingsService plexSettings, + public RecentlyAddedNewsletter(IPlexApi api, ISettingsService plexSettings, ISettingsService email, IJobRecord rec, ISettingsService newsletter, - IPlexReadOnlyDatabase db, IUserHelper userHelper) + IPlexReadOnlyDatabase db, IUserHelper userHelper, IEmbyAddedNewsletter embyNews, + ISettingsService embyS) { JobRecord = rec; Api = api; @@ -62,17 +63,21 @@ namespace Ombi.Services.Jobs NewsletterSettings = newsletter; PlexDb = db; UserHelper = userHelper; + EmbyNewsletter = embyNews; + EmbySettings = embyS; } private IPlexApi Api { get; } private TvMazeApi TvApi = new TvMazeApi(); private readonly TheMovieDbApi _movieApi = new TheMovieDbApi(); private ISettingsService PlexSettings { get; } + private ISettingsService EmbySettings { get; } private ISettingsService EmailSettings { get; } private ISettingsService NewsletterSettings { get; } private IJobRecord JobRecord { get; } private IPlexReadOnlyDatabase PlexDb { get; } private IUserHelper UserHelper { get; } + private IEmbyAddedNewsletter EmbyNewsletter { get; } private static readonly Logger Log = LogManager.GetCurrentClassLogger(); @@ -128,97 +133,107 @@ namespace Ombi.Services.Jobs private void StartNewsLetter(NewletterSettings newletterSettings, bool testEmail = false) { - var sb = new StringBuilder(); - var plexSettings = PlexSettings.GetSettings(); - Log.Debug("Got Plex Settings"); + var embySettings = EmbySettings.GetSettings(); + if (embySettings.Enable) + { + var html = EmbyNewsletter.GetNewsletterHtml(testEmail); + + var escapedHtml = new string(html.Where(c => !char.IsControl(c)).ToArray()); + Log.Debug(escapedHtml); + SendNewsletter(newletterSettings, escapedHtml, testEmail); + } + else + { + var sb = new StringBuilder(); + var plexSettings = PlexSettings.GetSettings(); + Log.Debug("Got Plex Settings"); - var libs = Api.GetLibrarySections(plexSettings.PlexAuthToken, plexSettings.FullUri); - Log.Debug("Getting Plex Library Sections"); + var libs = Api.GetLibrarySections(plexSettings.PlexAuthToken, plexSettings.FullUri); + Log.Debug("Getting Plex Library Sections"); - var tvSections = libs.Directories.Where(x => x.type.Equals(PlexMediaType.Show.ToString(), StringComparison.CurrentCultureIgnoreCase)); // We could have more than 1 lib - Log.Debug("Filtered sections for TV"); - var movieSection = libs.Directories.Where(x => x.type.Equals(PlexMediaType.Movie.ToString(), StringComparison.CurrentCultureIgnoreCase)); // We could have more than 1 lib - Log.Debug("Filtered sections for Movies"); + var tvSections = libs.Directories.Where(x => x.type.Equals(PlexMediaType.Show.ToString(), StringComparison.CurrentCultureIgnoreCase)); // We could have more than 1 lib + Log.Debug("Filtered sections for TV"); + var movieSection = libs.Directories.Where(x => x.type.Equals(PlexMediaType.Movie.ToString(), StringComparison.CurrentCultureIgnoreCase)); // We could have more than 1 lib + Log.Debug("Filtered sections for Movies"); - var plexVersion = Api.GetStatus(plexSettings.PlexAuthToken, plexSettings.FullUri).Version; + var plexVersion = Api.GetStatus(plexSettings.PlexAuthToken, plexSettings.FullUri).Version; - var html = string.Empty; - if (plexVersion.StartsWith("1.3")) - { - var tvMetadata = new List(); - var movieMetadata = new List(); - foreach (var tvSection in tvSections) + var html = string.Empty; + if (plexVersion.StartsWith("1.3")) { - var item = Api.RecentlyAdded(plexSettings.PlexAuthToken, plexSettings.FullUri, - tvSection?.Key); - if (item?.MediaContainer?.Metadata != null) + var tvMetadata = new List(); + var movieMetadata = new List(); + foreach (var tvSection in tvSections) { - tvMetadata.AddRange(item?.MediaContainer?.Metadata); + var item = Api.RecentlyAdded(plexSettings.PlexAuthToken, plexSettings.FullUri, + tvSection?.Key); + if (item?.MediaContainer?.Metadata != null) + { + tvMetadata.AddRange(item?.MediaContainer?.Metadata); + } } - } - Log.Debug("Got RecentlyAdded TV Shows"); - foreach (var movie in movieSection) - { - var recentlyAddedMovies = Api.RecentlyAdded(plexSettings.PlexAuthToken, plexSettings.FullUri, movie?.Key); - if (recentlyAddedMovies?.MediaContainer?.Metadata != null) + Log.Debug("Got RecentlyAdded TV Shows"); + foreach (var movie in movieSection) { - movieMetadata.AddRange(recentlyAddedMovies?.MediaContainer?.Metadata); + var recentlyAddedMovies = Api.RecentlyAdded(plexSettings.PlexAuthToken, plexSettings.FullUri, movie?.Key); + if (recentlyAddedMovies?.MediaContainer?.Metadata != null) + { + movieMetadata.AddRange(recentlyAddedMovies?.MediaContainer?.Metadata); + } } + Log.Debug("Got RecentlyAdded Movies"); + + Log.Debug("Started Generating Movie HTML"); + GenerateMovieHtml(movieMetadata, plexSettings, sb); + Log.Debug("Finished Generating Movie HTML"); + Log.Debug("Started Generating TV HTML"); + GenerateTvHtml(tvMetadata, plexSettings, sb); + Log.Debug("Finished Generating TV HTML"); + + var template = new RecentlyAddedTemplate(); + html = template.LoadTemplate(sb.ToString()); + Log.Debug("Loaded the template"); } - Log.Debug("Got RecentlyAdded Movies"); - - Log.Debug("Started Generating Movie HTML"); - GenerateMovieHtml(movieMetadata, plexSettings, sb); - Log.Debug("Finished Generating Movie HTML"); - Log.Debug("Started Generating TV HTML"); - GenerateTvHtml(tvMetadata, plexSettings, sb); - Log.Debug("Finished Generating TV HTML"); - - var template = new RecentlyAddedTemplate(); - html = template.LoadTemplate(sb.ToString()); - Log.Debug("Loaded the template"); - } - else - { - // Old API - var tvChild = new List(); - var movieChild = new List(); - foreach (var tvSection in tvSections) + else { - var recentlyAddedTv = Api.RecentlyAddedOld(plexSettings.PlexAuthToken, plexSettings.FullUri, tvSection?.Key); - if (recentlyAddedTv?._children != null) + // Old API + var tvChild = new List(); + var movieChild = new List(); + foreach (var tvSection in tvSections) { - tvChild.AddRange(recentlyAddedTv?._children); + var recentlyAddedTv = Api.RecentlyAddedOld(plexSettings.PlexAuthToken, plexSettings.FullUri, tvSection?.Key); + if (recentlyAddedTv?._children != null) + { + tvChild.AddRange(recentlyAddedTv?._children); + } } - } - Log.Debug("Got RecentlyAdded TV Shows"); - foreach (var movie in movieSection) - { - var recentlyAddedMovies = Api.RecentlyAddedOld(plexSettings.PlexAuthToken, plexSettings.FullUri, movie?.Key); - if (recentlyAddedMovies?._children != null) + Log.Debug("Got RecentlyAdded TV Shows"); + foreach (var movie in movieSection) { - tvChild.AddRange(recentlyAddedMovies?._children); + var recentlyAddedMovies = Api.RecentlyAddedOld(plexSettings.PlexAuthToken, plexSettings.FullUri, movie?.Key); + if (recentlyAddedMovies?._children != null) + { + tvChild.AddRange(recentlyAddedMovies?._children); + } } + Log.Debug("Got RecentlyAdded Movies"); + + Log.Debug("Started Generating Movie HTML"); + GenerateMovieHtml(movieChild, plexSettings, sb); + Log.Debug("Finished Generating Movie HTML"); + Log.Debug("Started Generating TV HTML"); + GenerateTvHtml(tvChild, plexSettings, sb); + Log.Debug("Finished Generating TV HTML"); + + var template = new RecentlyAddedTemplate(); + html = template.LoadTemplate(sb.ToString()); + Log.Debug("Loaded the template"); } - Log.Debug("Got RecentlyAdded Movies"); - - Log.Debug("Started Generating Movie HTML"); - GenerateMovieHtml(movieChild, plexSettings, sb); - Log.Debug("Finished Generating Movie HTML"); - Log.Debug("Started Generating TV HTML"); - GenerateTvHtml(tvChild, plexSettings, sb); - Log.Debug("Finished Generating TV HTML"); - - var template = new RecentlyAddedTemplate(); - html = template.LoadTemplate(sb.ToString()); - Log.Debug("Loaded the template"); + string escapedHtml = new string(html.Where(c => !char.IsControl(c)).ToArray()); + Log.Debug(escapedHtml); + SendNewsletter(newletterSettings, escapedHtml, testEmail); } - - - string escapedHtml = new string(html.Where(c => !char.IsControl(c)).ToArray()); - Log.Debug(escapedHtml); - SendNewsletter(newletterSettings, escapedHtml, testEmail); } private void GenerateMovieHtml(List movies, PlexSettings plexSettings, StringBuilder sb) diff --git a/Ombi.Services/Ombi.Services.csproj b/Ombi.Services/Ombi.Services.csproj index a37279f7d..17093e031 100644 --- a/Ombi.Services/Ombi.Services.csproj +++ b/Ombi.Services/Ombi.Services.csproj @@ -108,6 +108,8 @@ + + @@ -117,7 +119,7 @@ - + diff --git a/Ombi.Store/Models/Emby/EmbyContent.cs b/Ombi.Store/Models/Emby/EmbyContent.cs index 799487755..07f211cc3 100644 --- a/Ombi.Store/Models/Emby/EmbyContent.cs +++ b/Ombi.Store/Models/Emby/EmbyContent.cs @@ -39,5 +39,6 @@ namespace Ombi.Store.Models.Emby public DateTime PremierDate { get; set; } public string ProviderId { get; set; } public EmbyMediaType Type { get; set; } + public DateTime AddedAt { get; set; } } } \ No newline at end of file diff --git a/Ombi.Store/Models/Emby/EmbyEpisodes.cs b/Ombi.Store/Models/Emby/EmbyEpisodes.cs index a1b900455..24d41f052 100644 --- a/Ombi.Store/Models/Emby/EmbyEpisodes.cs +++ b/Ombi.Store/Models/Emby/EmbyEpisodes.cs @@ -25,6 +25,7 @@ // ************************************************************************/ #endregion +using System; using Dapper.Contrib.Extensions; namespace Ombi.Store.Models.Emby @@ -39,5 +40,6 @@ namespace Ombi.Store.Models.Emby public int SeasonNumber { get; set; } public string ParentId { get; set; } public string ProviderId { get; set; } + public DateTime AddedAt { get; set; } } } \ No newline at end of file diff --git a/Ombi.Store/Models/RecenetlyAddedLog.cs b/Ombi.Store/Models/RecenetlyAddedLog.cs new file mode 100644 index 000000000..4f7a75aba --- /dev/null +++ b/Ombi.Store/Models/RecenetlyAddedLog.cs @@ -0,0 +1,40 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: LogEntity.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion + +using System; +using Dapper.Contrib.Extensions; +using Newtonsoft.Json; + +namespace Ombi.Store.Models +{ + [Table("RecentlyAddedLog")] + public class RecentlyAddedLog : Entity + { + public string ProviderId { get; set; } + public DateTime AddedAt { get; set; } + } +} diff --git a/Ombi.Store/Ombi.Store.csproj b/Ombi.Store/Ombi.Store.csproj index 6bfaa021b..06a0b59fc 100644 --- a/Ombi.Store/Ombi.Store.csproj +++ b/Ombi.Store/Ombi.Store.csproj @@ -68,6 +68,7 @@ + diff --git a/Ombi.Store/SqlTables.sql b/Ombi.Store/SqlTables.sql index f548381a8..cdf5a2f80 100644 --- a/Ombi.Store/SqlTables.sql +++ b/Ombi.Store/SqlTables.sql @@ -187,7 +187,8 @@ CREATE TABLE IF NOT EXISTS EmbyEpisodes SeasonNumber INTEGER NOT NULL, EpisodeNumber INTEGER NOT NULL, ParentId VARCHAR(100) NOT NULL, - ProviderId VARCHAR(100) NOT NULL + ProviderId VARCHAR(100) NOT NULL, + AddedAt VARCHAR(100) NOT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS EmbyEpisodes_Id ON EmbyEpisodes (Id); @@ -198,9 +199,21 @@ CREATE TABLE IF NOT EXISTS EmbyContent PremierDate VARCHAR(100) NOT NULL, EmbyId VARCHAR(100) NOT NULL, ProviderId VARCHAR(100) NOT NULL, - Type INTEGER NOT NULL + Type INTEGER NOT NULL, + AddedAt VARCHAR(100) NOT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS EmbyEpisodes_Id ON EmbyEpisodes (Id); +CREATE TABLE IF NOT EXISTS RecentlyAddedLog +( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + ProviderId VARCHAR(100) NOT NULL, + AddedAt VARCHAR(100) NOT NULL + +); +CREATE UNIQUE INDEX IF NOT EXISTS RecentlyAddedLog_Id ON RecentlyAddedLog (Id); + +CREATE INDEX IF NOT EXISTS RecentlyAddedLog_ProviderId ON RecentlyAddedLog (ProviderId); + COMMIT; \ No newline at end of file diff --git a/Ombi.UI/Jobs/Scheduler.cs b/Ombi.UI/Jobs/Scheduler.cs index 8f43a02b3..d824701c9 100644 --- a/Ombi.UI/Jobs/Scheduler.cs +++ b/Ombi.UI/Jobs/Scheduler.cs @@ -35,6 +35,7 @@ using Ombi.Core; using Ombi.Core.SettingModels; using Ombi.Services.Interfaces; using Ombi.Services.Jobs; +using Ombi.Services.Jobs.RecentlyAddedNewsletter; using Ombi.UI.Helpers; using Quartz; using Quartz.Impl; @@ -70,7 +71,7 @@ namespace Ombi.UI.Jobs JobBuilder.Create().WithIdentity("StoreBackup", "Database").Build(), JobBuilder.Create().WithIdentity("StoreCleanup", "Database").Build(), JobBuilder.Create().WithIdentity("UserRequestLimiter", "Request").Build(), - JobBuilder.Create().WithIdentity("RecentlyAddedModel", "Email").Build(), + JobBuilder.Create().WithIdentity("RecentlyAddedModel", "Email").Build(), JobBuilder.Create().WithIdentity("FaultQueueHandler", "Fault").Build(), JobBuilder.Create().WithIdentity("RadarrCacher", "Cache").Build(), diff --git a/Ombi.UI/Modules/Admin/AdminModule.cs b/Ombi.UI/Modules/Admin/AdminModule.cs index 388a66ef0..8faab45d6 100644 --- a/Ombi.UI/Modules/Admin/AdminModule.cs +++ b/Ombi.UI/Modules/Admin/AdminModule.cs @@ -42,6 +42,7 @@ using Nancy.Validation; using NLog; using Ombi.Api; using Ombi.Api.Interfaces; +using Ombi.Api.Models.Movie; using Ombi.Core; using Ombi.Core.Models; using Ombi.Core.SettingModels; @@ -823,6 +824,10 @@ namespace Ombi.UI.Modules.Admin { return Response.AsJson(valid.SendJsonError()); } + if (!settings.Enabled) + { + return Response.AsJson(new CouchPotatoProfiles{list = new List()}); + } var profiles = CpApi.GetProfiles(settings.FullUri, settings.ApiKey); // set the cache diff --git a/Ombi.UI/NinjectModules/ServicesModule.cs b/Ombi.UI/NinjectModules/ServicesModule.cs index 95fbe4ba5..210cdfd3e 100644 --- a/Ombi.UI/NinjectModules/ServicesModule.cs +++ b/Ombi.UI/NinjectModules/ServicesModule.cs @@ -32,6 +32,7 @@ using Ombi.Helpers.Analytics; using Ombi.Services.Interfaces; using Ombi.Services.Jobs; using Ombi.Services.Jobs.Interfaces; +using Ombi.Services.Jobs.RecentlyAddedNewsletter; using Ombi.UI.Jobs; using Quartz; using Quartz.Impl; @@ -48,8 +49,8 @@ namespace Ombi.UI.NinjectModules Bind().To(); Bind().To(); Bind().To(); - Bind().To(); - Bind().To(); + Bind().To(); + Bind().To(); Bind().To(); Bind().To(); Bind().To(); @@ -65,6 +66,7 @@ namespace Ombi.UI.NinjectModules Bind().To(); Bind().To(); Bind().To(); + Bind().To(); Bind().To(); From 74066d0351580283b323d42f4efb64432f26218f Mon Sep 17 00:00:00 2001 From: tidusjar Date: Fri, 3 Feb 2017 20:34:05 +0000 Subject: [PATCH 29/61] First run of the newsletter set it to a test --- .../RecentlyAddedNewsletter/EmbyRecentlyAddedNewsletter.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Ombi.Services/Jobs/RecentlyAddedNewsletter/EmbyRecentlyAddedNewsletter.cs b/Ombi.Services/Jobs/RecentlyAddedNewsletter/EmbyRecentlyAddedNewsletter.cs index cfe752c8a..07963008d 100644 --- a/Ombi.Services/Jobs/RecentlyAddedNewsletter/EmbyRecentlyAddedNewsletter.cs +++ b/Ombi.Services/Jobs/RecentlyAddedNewsletter/EmbyRecentlyAddedNewsletter.cs @@ -106,7 +106,9 @@ namespace Ombi.Services.Jobs.RecentlyAddedNewsletter var episodes = Episodes.GetAll().ToList(); var movie = embyContent.Where(x => x.Type == EmbyMediaType.Movie).ToList(); - var recentlyAdded = RecentlyAddedLog.GetAll(); + var recentlyAdded = RecentlyAddedLog.GetAll().ToList(); + + var firstRun = !recentlyAdded.Any(); var filteredMovies = movie.Where(m => recentlyAdded.All(x => x.ProviderId != m.ProviderId)).ToList(); var filteredEp = episodes.Where(m => recentlyAdded.All(x => x.ProviderId != m.ProviderId)).ToList(); @@ -156,7 +158,7 @@ namespace Ombi.Services.Jobs.RecentlyAddedNewsletter var html = template.LoadTemplate(sb.ToString()); Log.Debug("Loaded the template"); - if (!test) + if (!test || firstRun) { foreach (var a in filteredMovies) { From 2dfcef980e71338b265ce315a103bb34814c7796 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Fri, 3 Feb 2017 20:45:55 +0000 Subject: [PATCH 30/61] Removed plex from the scheduled jobs ui --- Ombi.UI/Jobs/Scheduler.cs | 4 ++-- Ombi.UI/Modules/Admin/AdminModule.cs | 21 ++++++++++++++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/Ombi.UI/Jobs/Scheduler.cs b/Ombi.UI/Jobs/Scheduler.cs index d824701c9..b0f5266cd 100644 --- a/Ombi.UI/Jobs/Scheduler.cs +++ b/Ombi.UI/Jobs/Scheduler.cs @@ -305,8 +305,8 @@ namespace Ombi.UI.Jobs var embyEpisode = TriggerBuilder.Create() .WithIdentity("EmbyEpisodeCacher", "Emby") - .StartNow() - //.StartAt(DateBuilder.FutureDate(10, IntervalUnit.Minute)) + //.StartNow() + .StartAt(DateBuilder.FutureDate(10, IntervalUnit.Minute)) .WithSimpleSchedule(x => x.WithIntervalInHours(s.EmbyEpisodeCacher).RepeatForever()) .Build(); diff --git a/Ombi.UI/Modules/Admin/AdminModule.cs b/Ombi.UI/Modules/Admin/AdminModule.cs index 8faab45d6..c9bce2faf 100644 --- a/Ombi.UI/Modules/Admin/AdminModule.cs +++ b/Ombi.UI/Modules/Admin/AdminModule.cs @@ -1146,6 +1146,8 @@ namespace Ombi.UI.Modules.Admin var emby = await EmbySettings.GetSettingsAsync(); var plex = await PlexService.GetSettingsAsync(); + + var dict = new Dictionary(); @@ -1158,7 +1160,24 @@ namespace Ombi.UI.Modules.Admin } else { - dict.Add(j.Name,j.LastRun); + if (j.Name.Contains("Plex")) + { + if (plex.Enable) + { + dict.Add(j.Name, j.LastRun); + } + } + else if (j.Name.Contains("Emby")) + { + if (emby.Enable) + { + dict.Add(j.Name, j.LastRun); + } + } + else + { + dict.Add(j.Name, j.LastRun); + } } } From 94bb1ed824cff41e866321777dce646350303988 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Fri, 3 Feb 2017 22:51:28 +0000 Subject: [PATCH 31/61] Removed references to Plex --- Ombi.Core/CacheKeys.cs | 2 + .../Jobs/Templates/RecentlyAddedTemplate.html | 2 +- Ombi.UI/Helpers/BaseUrlHelper.cs | 18 +- Ombi.UI/Models/SearchLoadViewModel.cs | 2 + Ombi.UI/Modules/SearchModule.cs | 3459 +++++++++-------- Ombi.UI/Resources/UI.da.resx | 13 +- Ombi.UI/Resources/UI.de.resx | 13 +- Ombi.UI/Resources/UI.es.resx | 13 +- Ombi.UI/Resources/UI.fr.resx | 13 +- Ombi.UI/Resources/UI.it.resx | 13 +- Ombi.UI/Resources/UI.nl.resx | 13 +- Ombi.UI/Resources/UI.pt.resx | 13 +- Ombi.UI/Resources/UI.resx | 17 +- Ombi.UI/Resources/UI.sv.resx | 13 +- Ombi.UI/Resources/UI1.Designer.cs | 23 +- Ombi.UI/Views/Admin/LandingPage.cshtml | 2 +- Ombi.UI/Views/Admin/NewsletterSettings.cshtml | 2 +- .../Views/Customization/Customization.cshtml | 2 +- Ombi.UI/Views/Landing/Index.cshtml | 2 +- Ombi.UI/Views/Search/Index.cshtml | 5 +- Ombi.UI/Views/UserLogin/Username.cshtml | 2 +- .../UserManagementSettings.cshtml | 2 +- Ombi.UI/Views/UserWizard/Index.cshtml | 2 +- 23 files changed, 1821 insertions(+), 1825 deletions(-) diff --git a/Ombi.Core/CacheKeys.cs b/Ombi.Core/CacheKeys.cs index 39ed7a71b..32466e897 100644 --- a/Ombi.Core/CacheKeys.cs +++ b/Ombi.Core/CacheKeys.cs @@ -45,6 +45,8 @@ namespace Ombi.Core public const string CouchPotatoQualityProfiles = nameof(CouchPotatoQualityProfiles); public const string CouchPotatoQueued = nameof(CouchPotatoQueued); public const string WatcherQueued = nameof(WatcherQueued); + public const string GetCustomizationSettings = nameof(GetCustomizationSettings); + public const string GetEmbySettings = nameof(GetEmbySettings); public const string GetPlexRequestSettings = nameof(GetPlexRequestSettings); public const string LastestProductVersion = nameof(LastestProductVersion); public const string SonarrRootFolders = nameof(SonarrRootFolders); diff --git a/Ombi.Services/Jobs/Templates/RecentlyAddedTemplate.html b/Ombi.Services/Jobs/Templates/RecentlyAddedTemplate.html index 932aae99f..17766147b 100644 --- a/Ombi.Services/Jobs/Templates/RecentlyAddedTemplate.html +++ b/Ombi.Services/Jobs/Templates/RecentlyAddedTemplate.html @@ -151,7 +151,7 @@

    -

    Here is a list of Movies and TV Shows that have recently been added to Plex!

    +

    Here is a list of Movies and TV Shows that have recently been added!

    diff --git a/Ombi.UI/Helpers/BaseUrlHelper.cs b/Ombi.UI/Helpers/BaseUrlHelper.cs index cb0600ce7..d1d3772b3 100644 --- a/Ombi.UI/Helpers/BaseUrlHelper.cs +++ b/Ombi.UI/Helpers/BaseUrlHelper.cs @@ -354,6 +354,12 @@ namespace Ombi.UI.Helpers return helper.Raw(GetCustomizationSettings().ApplicationName); } + public static IHtmlString GetMediaServerName(this HtmlHelpers helper) + { + var s = GetEmbySettings(); + return helper.Raw(s.Enable ? "Emby" : "Plex"); + } + private static string GetBaseUrl() { return GetSettings().BaseUrl; @@ -371,7 +377,7 @@ namespace Ombi.UI.Helpers private static CustomizationSettings GetCustomizationSettings() { - var returnValue = Cache.GetOrSet(CacheKeys.GetPlexRequestSettings, () => + var returnValue = Cache.GetOrSet(CacheKeys.GetCustomizationSettings, () => { var settings = Locator.Resolve>().GetSettings(); return settings; @@ -379,6 +385,16 @@ namespace Ombi.UI.Helpers return returnValue; } + private static EmbySettings GetEmbySettings() + { + var returnValue = Cache.GetOrSet(CacheKeys.GetEmbySettings, () => + { + var settings = Locator.Resolve>().GetSettings(); + return settings; + }); + return returnValue; + } + private static string GetLinkUrl(string assetLocation) { return string.IsNullOrEmpty(assetLocation) ? string.Empty : $"{assetLocation}"; diff --git a/Ombi.UI/Models/SearchLoadViewModel.cs b/Ombi.UI/Models/SearchLoadViewModel.cs index 2bdf5078c..667906431 100644 --- a/Ombi.UI/Models/SearchLoadViewModel.cs +++ b/Ombi.UI/Models/SearchLoadViewModel.cs @@ -32,6 +32,8 @@ namespace Ombi.UI.Models public class SearchLoadViewModel { public PlexRequestSettings Settings { get; set; } + public bool Plex { get; set; } + public bool Emby { get; set; } public CustomizationSettings CustomizationSettings { get; set; } } } \ No newline at end of file diff --git a/Ombi.UI/Modules/SearchModule.cs b/Ombi.UI/Modules/SearchModule.cs index 4a822ce81..59db65270 100644 --- a/Ombi.UI/Modules/SearchModule.cs +++ b/Ombi.UI/Modules/SearchModule.cs @@ -67,1730 +67,1739 @@ using ISecurityExtensions = Ombi.Core.ISecurityExtensions; namespace Ombi.UI.Modules { - public class SearchModule : BaseAuthModule - { - public SearchModule(ICacheProvider cache, - ISettingsService prSettings, IAvailabilityChecker plexChecker, - IRequestService request, ISonarrApi sonarrApi, ISettingsService sonarrSettings, - ISettingsService sickRageService, ISickRageApi srApi, - INotificationService notify, IMusicBrainzApi mbApi, IHeadphonesApi hpApi, - ISettingsService hpService, - ICouchPotatoCacher cpCacher, IWatcherCacher watcherCacher, ISonarrCacher sonarrCacher, ISickRageCacher sickRageCacher, IPlexApi plexApi, - ISettingsService plexService, ISettingsService auth, - IRepository u, ISettingsService email, - IIssueService issue, IAnalytics a, IRepository rl, ITransientFaultQueue tfQueue, IRepository content, - ISecurityExtensions security, IMovieSender movieSender, IRadarrCacher radarrCacher, ITraktApi traktApi, ISettingsService cus, - IEmbyAvailabilityChecker embyChecker, IRepository embyContent, ISettingsService embySettings) - : base("search", prSettings, security) - { - Auth = auth; - PlexService = plexService; - PlexApi = plexApi; - PrService = prSettings; - MovieApi = new TheMovieDbApi(); - Cache = cache; - PlexChecker = plexChecker; - CpCacher = cpCacher; - SonarrCacher = sonarrCacher; - SickRageCacher = sickRageCacher; - RequestService = request; - SonarrApi = sonarrApi; - SonarrService = sonarrSettings; - SickRageService = sickRageService; - SickrageApi = srApi; - NotificationService = notify; - MusicBrainzApi = mbApi; - HeadphonesApi = hpApi; - HeadphonesService = hpService; - UsersToNotifyRepo = u; - EmailNotificationSettings = email; - IssueService = issue; - Analytics = a; - RequestLimitRepo = rl; - FaultQueue = tfQueue; - TvApi = new TvMazeApi(); - PlexContentRepository = content; - MovieSender = movieSender; - WatcherCacher = watcherCacher; - RadarrCacher = radarrCacher; - TraktApi = traktApi; - CustomizationSettings = cus; - EmbyChecker = embyChecker; - EmbyContentRepository = embyContent; - EmbySettings = embySettings; - - Get["SearchIndex", "/", true] = async (x, ct) => await RequestLoad(); - - Get["movie/{searchTerm}", true] = async (x, ct) => await SearchMovie((string)x.searchTerm); - Get["tv/{searchTerm}", true] = async (x, ct) => await SearchTvShow((string)x.searchTerm); - Get["music/{searchTerm}", true] = async (x, ct) => await SearchAlbum((string)x.searchTerm); - Get["music/coverArt/{id}"] = p => GetMusicBrainzCoverArt((string)p.id); - - Get["movie/upcoming", true] = async (x, ct) => await UpcomingMovies(); - Get["movie/playing", true] = async (x, ct) => await CurrentlyPlayingMovies(); - - Get["tv/popular", true] = async (x, ct) => await ProcessShows(ShowSearchType.Popular); - Get["tv/trending", true] = async (x, ct) => await ProcessShows(ShowSearchType.Trending); - Get["tv/mostwatched", true] = async (x, ct) => await ProcessShows(ShowSearchType.MostWatched); - Get["tv/anticipated", true] = async (x, ct) => await ProcessShows(ShowSearchType.Anticipated); - - Get["tv/poster/{id}"] = p => GetTvPoster((int)p.id); - - Post["request/movie", true] = async (x, ct) => await RequestMovie((int)Request.Form.movieId); - Post["request/tv", true] = - async (x, ct) => await RequestTvShow((int)Request.Form.tvId, (string)Request.Form.seasons); - Post["request/tvEpisodes", true] = async (x, ct) => await RequestTvShow(0, "episode"); - Post["request/album", true] = async (x, ct) => await RequestAlbum((string)Request.Form.albumId); - - Get["/seasons"] = x => GetSeasons(); - Get["/episodes", true] = async (x, ct) => await GetEpisodes(); - } - private ITraktApi TraktApi { get; } - private IWatcherCacher WatcherCacher { get; } - private IMovieSender MovieSender { get; } - private IRepository PlexContentRepository { get; } - private IRepository EmbyContentRepository { get; } - private TvMazeApi TvApi { get; } - private IPlexApi PlexApi { get; } - private TheMovieDbApi MovieApi { get; } - private INotificationService NotificationService { get; } - private ISonarrApi SonarrApi { get; } - private ISickRageApi SickrageApi { get; } - private IRequestService RequestService { get; } - private ICacheProvider Cache { get; } - private ISettingsService Auth { get; } - private ISettingsService EmbySettings { get; } - private ISettingsService PlexService { get; } - private ISettingsService PrService { get; } - private ISettingsService SonarrService { get; } - private ISettingsService SickRageService { get; } - private ISettingsService HeadphonesService { get; } - private ISettingsService EmailNotificationSettings { get; } - private IAvailabilityChecker PlexChecker { get; } - private IEmbyAvailabilityChecker EmbyChecker { get; } - private ICouchPotatoCacher CpCacher { get; } - private ISonarrCacher SonarrCacher { get; } - private ISickRageCacher SickRageCacher { get; } - private IMusicBrainzApi MusicBrainzApi { get; } - private IHeadphonesApi HeadphonesApi { get; } - private IRepository UsersToNotifyRepo { get; } - private IIssueService IssueService { get; } - private IAnalytics Analytics { get; } - private ITransientFaultQueue FaultQueue { get; } - private IRepository RequestLimitRepo { get; } - private IRadarrCacher RadarrCacher { get; } - private ISettingsService CustomizationSettings { get; } - private static Logger Log = LogManager.GetCurrentClassLogger(); - - private async Task RequestLoad() - { - - var settings = await PrService.GetSettingsAsync(); - var custom = await CustomizationSettings.GetSettingsAsync(); - var searchViewModel = new SearchLoadViewModel - { - Settings = settings, - CustomizationSettings = custom - }; - - - return View["Search/Index", searchViewModel]; - } - - private async Task UpcomingMovies() - { - Analytics.TrackEventAsync(Category.Search, Action.Movie, "Upcoming", Username, - CookieHelper.GetAnalyticClientId(Cookies)); - return await ProcessMovies(MovieSearchType.Upcoming, string.Empty); - } - - private async Task CurrentlyPlayingMovies() - { - Analytics.TrackEventAsync(Category.Search, Action.Movie, "CurrentlyPlaying", Username, - CookieHelper.GetAnalyticClientId(Cookies)); - return await ProcessMovies(MovieSearchType.CurrentlyPlaying, string.Empty); - } - - private async Task SearchMovie(string searchTerm) - { - Analytics.TrackEventAsync(Category.Search, Action.Movie, searchTerm, Username, - CookieHelper.GetAnalyticClientId(Cookies)); - return await ProcessMovies(MovieSearchType.Search, searchTerm); - } - - private Response GetTvPoster(int theTvDbId) - { - var result = TvApi.ShowLookupByTheTvDbId(theTvDbId); - - var banner = result.image?.medium; - if (!string.IsNullOrEmpty(banner)) - { - banner = banner.Replace("http", "https"); // Always use the Https banners - } - return banner; - } - private async Task ProcessMovies(MovieSearchType searchType, string searchTerm) - { - List apiMovies; - - switch (searchType) - { - case MovieSearchType.Search: - var movies = await MovieApi.SearchMovie(searchTerm).ConfigureAwait(false); - apiMovies = movies.Select(x => - new MovieResult - { - Adult = x.Adult, - BackdropPath = x.BackdropPath, - GenreIds = x.GenreIds, - Id = x.Id, - OriginalLanguage = x.OriginalLanguage, - OriginalTitle = x.OriginalTitle, - Overview = x.Overview, - Popularity = x.Popularity, - PosterPath = x.PosterPath, - ReleaseDate = x.ReleaseDate, - Title = x.Title, - Video = x.Video, - VoteAverage = x.VoteAverage, - VoteCount = x.VoteCount - }) - .ToList(); - break; - case MovieSearchType.CurrentlyPlaying: - apiMovies = await MovieApi.GetCurrentPlayingMovies(); - break; - case MovieSearchType.Upcoming: - apiMovies = await MovieApi.GetUpcomingMovies(); - break; - default: - apiMovies = new List(); - break; - } - - var allResults = await RequestService.GetAllAsync(); - allResults = allResults.Where(x => x.Type == RequestType.Movie); - - var distinctResults = allResults.DistinctBy(x => x.ProviderId); - var dbMovies = distinctResults.ToDictionary(x => x.ProviderId); - - - var cpCached = CpCacher.QueuedIds(); - var watcherCached = WatcherCacher.QueuedIds(); - var radarrCached = RadarrCacher.QueuedIds(); - - var viewMovies = new List(); - var counter = 0; - foreach (var movie in apiMovies) - { - var viewMovie = new SearchMovieViewModel - { - Adult = movie.Adult, - BackdropPath = movie.BackdropPath, - GenreIds = movie.GenreIds, - Id = movie.Id, - OriginalLanguage = movie.OriginalLanguage, - OriginalTitle = movie.OriginalTitle, - Overview = movie.Overview, - Popularity = movie.Popularity, - PosterPath = movie.PosterPath, - ReleaseDate = movie.ReleaseDate, - Title = movie.Title, - Video = movie.Video, - VoteAverage = movie.VoteAverage, - VoteCount = movie.VoteCount - }; - - if (counter <= 5) // Let's only do it for the first 5 items - { - var movieInfo = MovieApi.GetMovieInformationWithVideos(movie.Id); - - // TODO needs to be careful about this, it's adding extra time to search... - // https://www.themoviedb.org/talk/5807f4cdc3a36812160041f2 - viewMovie.ImdbId = movieInfo?.imdb_id; - viewMovie.Homepage = movieInfo?.homepage; - var videoId = movieInfo?.video ?? false - ? movieInfo?.videos?.results?.FirstOrDefault()?.key - : string.Empty; - - viewMovie.Trailer = string.IsNullOrEmpty(videoId) - ? string.Empty - : $"https://www.youtube.com/watch?v={videoId}"; - - counter++; - } - - var canSee = CanUserSeeThisRequest(viewMovie.Id, Security.HasPermissions(User, Permissions.UsersCanViewOnlyOwnRequests), dbMovies); - - var plexSettings = await PlexService.GetSettingsAsync(); - var embySettings = await EmbySettings.GetSettingsAsync(); - if (plexSettings.Enable) - { - var content = PlexContentRepository.GetAll(); - var plexMovies = PlexChecker.GetPlexMovies(content); - - var plexMovie = PlexChecker.GetMovie(plexMovies.ToArray(), movie.Title, - movie.ReleaseDate?.Year.ToString(), - viewMovie.ImdbId); - if (plexMovie != null) - { - viewMovie.Available = true; - viewMovie.PlexUrl = plexMovie.Url; - } - } - if (embySettings.Enable) - { - var embyContent = EmbyContentRepository.GetAll(); - var embyMovies = EmbyChecker.GetEmbyMovies(embyContent); - - var embyMovie = EmbyChecker.GetMovie(embyMovies.ToArray(), movie.Title, - movie.ReleaseDate?.Year.ToString(), viewMovie.ImdbId); - if (embyMovie != null) - { - viewMovie.Available = true; - } - } - else if (dbMovies.ContainsKey(movie.Id) && canSee) // compare to the requests db - { - var dbm = dbMovies[movie.Id]; - - viewMovie.Requested = true; - viewMovie.Approved = dbm.Approved; - viewMovie.Available = dbm.Available; - } - else if (cpCached.Contains(movie.Id) && canSee) // compare to the couchpotato db - { - viewMovie.Approved = true; - viewMovie.Requested = true; - } - else if (watcherCached.Contains(viewMovie.ImdbId) && canSee) // compare to the watcher db - { - viewMovie.Approved = true; - viewMovie.Requested = true; - } - else if (radarrCached.Contains(movie.Id) && canSee) - { - viewMovie.Approved = true; - viewMovie.Requested = true; - } - viewMovies.Add(viewMovie); - } - - return Response.AsJson(viewMovies); - } - - private bool CanUserSeeThisRequest(int movieId, bool usersCanViewOnlyOwnRequests, - Dictionary moviesInDb) - { - if (usersCanViewOnlyOwnRequests) - { - var result = moviesInDb.FirstOrDefault(x => x.Value.ProviderId == movieId); - return result.Value == null || result.Value.UserHasRequested(Username); - } - - return true; - } - - private async Task ProcessShows(ShowSearchType type) - { - var shows = new List(); - var prSettings = await PrService.GetSettingsAsync(); - switch (type) - { - case ShowSearchType.Popular: - Analytics.TrackEventAsync(Category.Search, Action.TvShow, "Popular", Username, CookieHelper.GetAnalyticClientId(Cookies)); - var popularShows = await TraktApi.GetPopularShows(); - - foreach (var popularShow in popularShows) - { - var theTvDbId = int.Parse(popularShow.Ids.Tvdb.ToString()); - - var model = new SearchTvShowViewModel - { - FirstAired = popularShow.FirstAired?.ToString("yyyy-MM-ddTHH:mm:ss"), - Id = theTvDbId, - ImdbId = popularShow.Ids.Imdb, - Network = popularShow.Network, - Overview = popularShow.Overview.RemoveHtml(), - Rating = popularShow.Rating.ToString(), - Runtime = popularShow.Runtime.ToString(), - SeriesName = popularShow.Title, - Status = popularShow.Status.DisplayName, - DisableTvRequestsByEpisode = prSettings.DisableTvRequestsByEpisode, - DisableTvRequestsBySeason = prSettings.DisableTvRequestsBySeason, - EnableTvRequestsForOnlySeries = (prSettings.DisableTvRequestsByEpisode && prSettings.DisableTvRequestsBySeason), - Trailer = popularShow.Trailer, - Homepage = popularShow.Homepage - }; - shows.Add(model); - } - shows = await MapToTvModel(shows, prSettings); - break; - case ShowSearchType.Anticipated: - Analytics.TrackEventAsync(Category.Search, Action.TvShow, "Anticipated", Username, CookieHelper.GetAnalyticClientId(Cookies)); - var anticipated = await TraktApi.GetAnticipatedShows(); - foreach (var anticipatedShow in anticipated) - { - var show = anticipatedShow.Show; - var theTvDbId = int.Parse(show.Ids.Tvdb.ToString()); - - var model = new SearchTvShowViewModel - { - FirstAired = show.FirstAired?.ToString("yyyy-MM-ddTHH:mm:ss"), - Id = theTvDbId, - ImdbId = show.Ids.Imdb, - Network = show.Network ?? string.Empty, - Overview = show.Overview?.RemoveHtml() ?? string.Empty, - Rating = show.Rating.ToString(), - Runtime = show.Runtime.ToString(), - SeriesName = show.Title, - Status = show.Status?.DisplayName ?? string.Empty, - DisableTvRequestsByEpisode = prSettings.DisableTvRequestsByEpisode, - DisableTvRequestsBySeason = prSettings.DisableTvRequestsBySeason, - EnableTvRequestsForOnlySeries = (prSettings.DisableTvRequestsByEpisode && prSettings.DisableTvRequestsBySeason), - Trailer = show.Trailer, - Homepage = show.Homepage - }; - shows.Add(model); - } - shows = await MapToTvModel(shows, prSettings); - break; - case ShowSearchType.MostWatched: - Analytics.TrackEventAsync(Category.Search, Action.TvShow, "MostWatched", Username, CookieHelper.GetAnalyticClientId(Cookies)); - var mostWatched = await TraktApi.GetMostWatchesShows(); - foreach (var watched in mostWatched) - { - var show = watched.Show; - var theTvDbId = int.Parse(show.Ids.Tvdb.ToString()); - var model = new SearchTvShowViewModel - { - FirstAired = show.FirstAired?.ToString("yyyy-MM-ddTHH:mm:ss"), - Id = theTvDbId, - ImdbId = show.Ids.Imdb, - Network = show.Network, - Overview = show.Overview.RemoveHtml(), - Rating = show.Rating.ToString(), - Runtime = show.Runtime.ToString(), - SeriesName = show.Title, - Status = show.Status.DisplayName, - DisableTvRequestsByEpisode = prSettings.DisableTvRequestsByEpisode, - DisableTvRequestsBySeason = prSettings.DisableTvRequestsBySeason, - EnableTvRequestsForOnlySeries = (prSettings.DisableTvRequestsByEpisode && prSettings.DisableTvRequestsBySeason), - Trailer = show.Trailer, - Homepage = show.Homepage - }; - shows.Add(model); - } - shows = await MapToTvModel(shows, prSettings); - break; - case ShowSearchType.Trending: - Analytics.TrackEventAsync(Category.Search, Action.TvShow, "Trending", Username, CookieHelper.GetAnalyticClientId(Cookies)); - var trending = await TraktApi.GetTrendingShows(); - foreach (var watched in trending) - { - var show = watched.Show; - var theTvDbId = int.Parse(show.Ids.Tvdb.ToString()); - var model = new SearchTvShowViewModel - { - FirstAired = show.FirstAired?.ToString("yyyy-MM-ddTHH:mm:ss"), - Id = theTvDbId, - ImdbId = show.Ids.Imdb, - Network = show.Network, - Overview = show.Overview.RemoveHtml(), - Rating = show.Rating.ToString(), - Runtime = show.Runtime.ToString(), - SeriesName = show.Title, - Status = show.Status.DisplayName, - DisableTvRequestsByEpisode = prSettings.DisableTvRequestsByEpisode, - DisableTvRequestsBySeason = prSettings.DisableTvRequestsBySeason, - EnableTvRequestsForOnlySeries = (prSettings.DisableTvRequestsByEpisode && prSettings.DisableTvRequestsBySeason), - Trailer = show.Trailer, - Homepage = show.Homepage - }; - shows.Add(model); - } - shows = await MapToTvModel(shows, prSettings); - break; - default: - throw new ArgumentOutOfRangeException(nameof(type), type, null); - } - - - return Response.AsJson(shows); - } - - private async Task> MapToTvModel(List shows, PlexRequestSettings prSettings) - { - - var plexSettings = await PlexService.GetSettingsAsync(); - - var providerId = string.Empty; - // Get the requests - var allResults = await RequestService.GetAllAsync(); - allResults = allResults.Where(x => x.Type == RequestType.TvShow); - var distinctResults = allResults.DistinctBy(x => x.ProviderId); - var dbTv = distinctResults.ToDictionary(x => x.ProviderId); - - // Check the external applications - var sonarrCached = SonarrCacher.QueuedIds().ToList(); - var sickRageCache = SickRageCacher.QueuedIds(); // consider just merging sonarr/sickrage arrays - var content = PlexContentRepository.GetAll(); - var plexTvShows = PlexChecker.GetPlexTvShows(content).ToList(); - - foreach (var show in shows) - { - if (plexSettings.AdvancedSearch) - { - providerId = show.Id.ToString(); - } - - var plexShow = PlexChecker.GetTvShow(plexTvShows.ToArray(), show.SeriesName, show.FirstAired?.Substring(0, 4), - providerId); - if (plexShow != null) - { - show.Available = true; - show.PlexUrl = plexShow.Url; - } - else - { - if (dbTv.ContainsKey(show.Id)) - { - var dbt = dbTv[show.Id]; - - show.Requested = true; - show.Episodes = dbt.Episodes.ToList(); - show.Approved = dbt.Approved; - } - if (sonarrCached.Select(x => x.TvdbId).Contains(show.Id) || sickRageCache.Contains(show.Id)) - // compare to the sonarr/sickrage db - { - show.Requested = true; - } - } - } - return shows; - } - - private async Task SearchTvShow(string searchTerm) - { - - Analytics.TrackEventAsync(Category.Search, Action.TvShow, searchTerm, Username, - CookieHelper.GetAnalyticClientId(Cookies)); - var plexSettings = await PlexService.GetSettingsAsync(); - var embySettings = await EmbySettings.GetSettingsAsync(); - var prSettings = await PrService.GetSettingsAsync(); - var providerId = string.Empty; - - var apiTv = new List(); - await Task.Factory.StartNew(() => new TvMazeApi().Search(searchTerm)).ContinueWith((t) => - { - apiTv = t.Result; - }); - - var allResults = await RequestService.GetAllAsync(); - allResults = allResults.Where(x => x.Type == RequestType.TvShow); - var distinctResults = allResults.DistinctBy(x => x.ProviderId); - var dbTv = distinctResults.ToDictionary(x => x.ProviderId); - - if (!apiTv.Any()) - { - return Response.AsJson(""); - } - - var sonarrCached = SonarrCacher.QueuedIds(); - var sickRageCache = SickRageCacher.QueuedIds(); // consider just merging sonarr/sickrage arrays - var content = PlexContentRepository.GetAll(); - var plexTvShows = PlexChecker.GetPlexTvShows(content); - var embyContent = EmbyContentRepository.GetAll(); - var embyCached = EmbyChecker.GetEmbyTvShows(embyContent); - - var viewTv = new List(); - foreach (var t in apiTv) - { - if (!(t.show.externals?.thetvdb.HasValue) ?? false) - { - continue; - } - var banner = t.show.image?.medium; - if (!string.IsNullOrEmpty(banner)) - { - banner = banner.Replace("http", "https"); // Always use the Https banners - } - - var viewT = new SearchTvShowViewModel - { - Banner = banner, - FirstAired = t.show.premiered, - Id = t.show.externals?.thetvdb ?? 0, - ImdbId = t.show.externals?.imdb, - Network = t.show.network?.name, - NetworkId = t.show.network?.id.ToString(), - Overview = t.show.summary.RemoveHtml(), - Rating = t.score.ToString(CultureInfo.CurrentUICulture), - Runtime = t.show.runtime.ToString(), - SeriesId = t.show.id, - SeriesName = t.show.name, - Status = t.show.status, - DisableTvRequestsByEpisode = prSettings.DisableTvRequestsByEpisode, - DisableTvRequestsBySeason = prSettings.DisableTvRequestsBySeason, - EnableTvRequestsForOnlySeries = (prSettings.DisableTvRequestsByEpisode && prSettings.DisableTvRequestsBySeason) - }; - - providerId = viewT.Id.ToString(); - - if (embySettings.Enable) - { - var embyShow = EmbyChecker.GetTvShow(embyCached.ToArray(), t.show.name, t.show.premiered?.Substring(0, 4), providerId); - if (embyShow != null) - { - viewT.Available = true; - } - } - if (plexSettings.Enable) - { - var plexShow = PlexChecker.GetTvShow(plexTvShows.ToArray(), t.show.name, t.show.premiered?.Substring(0, 4), - providerId); - if (plexShow != null) - { - viewT.Available = true; - viewT.PlexUrl = plexShow.Url; - } - } - - if (t.show?.externals?.thetvdb != null && !viewT.Available) - { - var tvdbid = (int)t.show.externals.thetvdb; - if (dbTv.ContainsKey(tvdbid)) - { - var dbt = dbTv[tvdbid]; - - viewT.Requested = true; - viewT.Episodes = dbt.Episodes.ToList(); - viewT.Approved = dbt.Approved; - } - if (sonarrCached.Select(x => x.TvdbId).Contains(tvdbid) || sickRageCache.Contains(tvdbid)) - // compare to the sonarr/sickrage db - { - viewT.Requested = true; - } - } - - viewTv.Add(viewT); - } - - return Response.AsJson(viewTv); - } - - private async Task SearchAlbum(string searchTerm) - { - Analytics.TrackEventAsync(Category.Search, Action.Album, searchTerm, Username, - CookieHelper.GetAnalyticClientId(Cookies)); - var apiAlbums = new List(); - await Task.Run(() => MusicBrainzApi.SearchAlbum(searchTerm)).ContinueWith((t) => - { - apiAlbums = t.Result.releases ?? new List(); - }); - - var allResults = await RequestService.GetAllAsync(); - allResults = allResults.Where(x => x.Type == RequestType.Album); - - var dbAlbum = allResults.ToDictionary(x => x.MusicBrainzId); - - var content = PlexContentRepository.GetAll(); - var plexAlbums = PlexChecker.GetPlexAlbums(content); - - var viewAlbum = new List(); - foreach (var a in apiAlbums) - { - var viewA = new SearchMusicViewModel - { - Title = a.title, - Id = a.id, - Artist = a.ArtistCredit?.Select(x => x.artist?.name).FirstOrDefault(), - Overview = a.disambiguation, - ReleaseDate = a.date, - TrackCount = a.TrackCount, - ReleaseType = a.status, - Country = a.country - }; - - DateTime release; - DateTimeHelper.CustomParse(a.ReleaseEvents?.FirstOrDefault()?.date, out release); - var artist = a.ArtistCredit?.FirstOrDefault()?.artist; - var plexAlbum = PlexChecker.GetAlbum(plexAlbums.ToArray(), a.title, release.ToString("yyyy"), artist?.name); - if (plexAlbum != null) - { - viewA.Available = true; - viewA.PlexUrl = plexAlbum.Url; - } - if (!string.IsNullOrEmpty(a.id) && dbAlbum.ContainsKey(a.id)) - { - var dba = dbAlbum[a.id]; - - viewA.Requested = true; - viewA.Approved = dba.Approved; - viewA.Available = dba.Available; - } - - viewAlbum.Add(viewA); - } - return Response.AsJson(viewAlbum); - } - - private async Task RequestMovie(int movieId) - { - if (Security.HasPermissions(User, Permissions.ReadOnlyUser) || !Security.HasPermissions(User, Permissions.RequestMovie)) - { - return - Response.AsJson(new JsonResponseModel - { - Result = false, - Message = "Sorry, you do not have the correct permissions to request a movie!" - }); - } - var settings = await PrService.GetSettingsAsync(); - if (!await CheckRequestLimit(settings, RequestType.Movie)) - { - return - Response.AsJson(new JsonResponseModel - { - Result = false, - Message = "You have reached your weekly request limit for Movies! Please contact your admin." - }); - } - - Analytics.TrackEventAsync(Category.Search, Action.Request, "Movie", Username, - CookieHelper.GetAnalyticClientId(Cookies)); - var movieInfo = await MovieApi.GetMovieInformation(movieId); - if (movieInfo == null) - { - return - Response.AsJson(new JsonResponseModel - { - Result = false, - Message = "There was an issue adding this movie!" - }); - } - var fullMovieName = - $"{movieInfo.Title}{(movieInfo.ReleaseDate.HasValue ? $" ({movieInfo.ReleaseDate.Value.Year})" : string.Empty)}"; - - var existingRequest = await RequestService.CheckRequestAsync(movieId); - if (existingRequest != null) - { - // check if the current user is already marked as a requester for this movie, if not, add them - if (!existingRequest.UserHasRequested(Username)) - { - existingRequest.RequestedUsers.Add(Username); - await RequestService.UpdateRequestAsync(existingRequest); - } - - return - Response.AsJson(new JsonResponseModel - { - Result = true, - Message = - Security.HasPermissions(User, Permissions.UsersCanViewOnlyOwnRequests) - ? $"{fullMovieName} {Ombi.UI.Resources.UI.Search_SuccessfullyAdded}" - : $"{fullMovieName} {Resources.UI.Search_AlreadyRequested}" - }); - } - - try - { - - var content = PlexContentRepository.GetAll(); - var movies = PlexChecker.GetPlexMovies(content); - if (PlexChecker.IsMovieAvailable(movies.ToArray(), movieInfo.Title, movieInfo.ReleaseDate?.Year.ToString())) - { - return - Response.AsJson(new JsonResponseModel - { - Result = false, - Message = $"{fullMovieName} is already in Plex!" - }); - } - } - catch (Exception e) - { - Log.Error(e); - return - Response.AsJson(new JsonResponseModel - { - Result = false, - Message = string.Format(Resources.UI.Search_CouldNotCheckPlex, fullMovieName) - }); - } - //#endif - - var model = new RequestedModel - { - ProviderId = movieInfo.Id, - Type = RequestType.Movie, - Overview = movieInfo.Overview, - ImdbId = movieInfo.ImdbId, - PosterPath = movieInfo.PosterPath, - Title = movieInfo.Title, - ReleaseDate = movieInfo.ReleaseDate ?? DateTime.MinValue, - Status = movieInfo.Status, - RequestedDate = DateTime.UtcNow, - Approved = false, - RequestedUsers = new List { Username }, - Issues = IssueState.None, - - }; - try - { - if (ShouldAutoApprove(RequestType.Movie)) - { - model.Approved = true; - - var result = await MovieSender.Send(model); - if (result.Result) - { - return await AddRequest(model, settings, - $"{fullMovieName} {Resources.UI.Search_SuccessfullyAdded}"); - } - if (result.Error) - - { - return - Response.AsJson(new JsonResponseModel - { - Message = "Could not add movie, please contract your administrator", - Result = false - }); - } - if (!result.MovieSendingEnabled) - { - - return await AddRequest(model, settings, $"{fullMovieName} {Resources.UI.Search_SuccessfullyAdded}"); - } - - return Response.AsJson(new JsonResponseModel - { - Result = false, - Message = Resources.UI.Search_CouchPotatoError - }); - } - - - return await AddRequest(model, settings, $"{fullMovieName} {Resources.UI.Search_SuccessfullyAdded}"); - } - catch (Exception e) - { - Log.Fatal(e); - await FaultQueue.QueueItemAsync(model, movieInfo.Id.ToString(), RequestType.Movie, FaultType.RequestFault, e.Message); - - await NotificationService.Publish(new NotificationModel - { - DateTime = DateTime.Now, - User = Username, - RequestType = RequestType.Movie, - Title = model.Title, - NotificationType = NotificationType.ItemAddedToFaultQueue - }); - - return Response.AsJson(new JsonResponseModel - { - Result = true, - Message = $"{fullMovieName} {Resources.UI.Search_SuccessfullyAdded}" - }); - } - } - - /// - /// Requests the tv show. - /// - /// The show identifier. - /// The seasons. - /// - private async Task RequestTvShow(int showId, string seasons) - { - if (Security.HasPermissions(User, Permissions.ReadOnlyUser) || !Security.HasPermissions(User, Permissions.RequestTvShow)) - { - return - Response.AsJson(new JsonResponseModel() - { - Result = false, - Message = "Sorry, you do not have the correct permissions to request a TV Show!" - }); - } - // Get the JSON from the request - var req = (Dictionary.ValueCollection)Request.Form.Values; - EpisodeRequestModel episodeModel = null; - if (req.Count == 1) - { - var json = req.FirstOrDefault()?.ToString(); - episodeModel = JsonConvert.DeserializeObject(json); // Convert it into the object - } - var episodeRequest = false; - - var settings = await PrService.GetSettingsAsync(); - if (!await CheckRequestLimit(settings, RequestType.TvShow)) - { - return - Response.AsJson(new JsonResponseModel - { - Result = false, - Message = Resources.UI.Search_WeeklyRequestLimitTVShow - }); - } - Analytics.TrackEventAsync(Category.Search, Action.Request, "TvShow", Username, - CookieHelper.GetAnalyticClientId(Cookies)); - - var sonarrSettings = SonarrService.GetSettingsAsync(); - - // This means we are requesting an episode rather than a whole series or season - if (episodeModel != null) - { - episodeRequest = true; - showId = episodeModel.ShowId; - var s = await sonarrSettings; - if (!s.Enabled) - { - return - Response.AsJson(new JsonResponseModel - { - Message = - "This is currently only supported with Sonarr, Please enable Sonarr for this feature", - Result = false - }); - } - } - - var showInfo = TvApi.ShowLookupByTheTvDbId(showId); - DateTime firstAir; - DateTime.TryParse(showInfo.premiered, out firstAir); - string fullShowName = $"{showInfo.name} ({firstAir.Year})"; - - // For some reason the poster path is always http - var posterPath = showInfo.image?.medium.Replace("http:", "https:"); - var model = new RequestedModel - { - Type = RequestType.TvShow, - Overview = showInfo.summary.RemoveHtml(), - PosterPath = posterPath, - Title = showInfo.name, - ReleaseDate = firstAir, - Status = showInfo.status, - RequestedDate = DateTime.UtcNow, - Approved = false, - RequestedUsers = new List { Username }, - Issues = IssueState.None, - ImdbId = showInfo.externals?.imdb ?? string.Empty, - SeasonCount = showInfo.Season.Count, - TvDbId = showId.ToString() - }; - - var seasonsList = new List(); - switch (seasons) - { - case "first": - seasonsList.Add(1); - model.SeasonsRequested = "First"; - break; - case "latest": - seasonsList.Add(model.SeasonCount); - model.SeasonsRequested = "Latest"; - break; - case "all": - model.SeasonsRequested = "All"; - break; - case "episode": - model.Episodes = new List(); - - foreach (var ep in episodeModel?.Episodes ?? new Models.EpisodesModel[0]) - { - model.Episodes.Add(new EpisodesModel - { - EpisodeNumber = ep.EpisodeNumber, - SeasonNumber = ep.SeasonNumber - }); - } - Analytics.TrackEventAsync(Category.Requests, Action.TvShow, $"Episode request for {model.Title}", - Username, CookieHelper.GetAnalyticClientId(Cookies)); - break; - default: - model.SeasonsRequested = seasons; - var split = seasons.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - var seasonsCount = new int[split.Length]; - for (var i = 0; i < split.Length; i++) - { - int tryInt; - int.TryParse(split[i], out tryInt); - seasonsCount[i] = tryInt; - } - seasonsList.AddRange(seasonsCount); - break; - } - - model.SeasonList = seasonsList.ToArray(); - - // check if the show/episodes have already been requested - var existingRequest = await RequestService.CheckRequestAsync(showId); - var difference = new List(); - if (existingRequest != null) - { - if (episodeRequest) - { - // Make sure we are not somehow adding dupes - difference = GetListDifferences(existingRequest.Episodes, episodeModel.Episodes).ToList(); - if (difference.Any()) - { - // Convert the request into the correct shape - var newEpisodes = episodeModel.Episodes?.Select(x => new EpisodesModel - { - SeasonNumber = x.SeasonNumber, - EpisodeNumber = x.EpisodeNumber - }); - - // Add it to the existing requests - existingRequest.Episodes.AddRange(newEpisodes ?? Enumerable.Empty()); - - // It's technically a new request now, so set the status to not approved. - var autoApprove = ShouldAutoApprove(RequestType.TvShow); - if (autoApprove) - { - return await SendTv(model, sonarrSettings, existingRequest, fullShowName, settings); - } - existingRequest.Approved = false; - - return await AddUserToRequest(existingRequest, settings, fullShowName, true); - } - else - { - // We no episodes to approve - return - Response.AsJson(new JsonResponseModel - { - Result = false, - Message = $"{fullShowName} {Resources.UI.Search_AlreadyInPlex}" - }); - } - } - else if (model.SeasonList.Except(existingRequest.SeasonList).Any()) - { - // This is a season being requested that we do not yet have - // Let's just continue - } - else - { - return await AddUserToRequest(existingRequest, settings, fullShowName); - } - } - - try - { - - var plexSettings = await PlexService.GetSettingsAsync(); - if (plexSettings.Enable) - { - var content = PlexContentRepository.GetAll(); - var shows = PlexChecker.GetPlexTvShows(content); - - var providerId = string.Empty; - if (plexSettings.AdvancedSearch) - { - providerId = showId.ToString(); - } - if (episodeRequest) - { - var cachedEpisodesTask = await PlexChecker.GetEpisodes(); - var cachedEpisodes = cachedEpisodesTask.ToList(); - foreach (var d in difference) // difference is from an existing request - { - if ( - cachedEpisodes.Any( - x => - x.SeasonNumber == d.SeasonNumber && x.EpisodeNumber == d.EpisodeNumber && - x.ProviderId == providerId)) - { - return - Response.AsJson(new JsonResponseModel - { - Result = false, - Message = - $"{fullShowName} {d.SeasonNumber} - {d.EpisodeNumber} {Resources.UI.Search_AlreadyInPlex}" - }); - } - } - - var diff = await GetEpisodeRequestDifference(showId, model); - model.Episodes = diff.ToList(); - } - else - { - if (plexSettings.EnableTvEpisodeSearching) - { - foreach (var s in showInfo.Season) - { - var result = PlexChecker.IsEpisodeAvailable(showId.ToString(), s.SeasonNumber, - s.EpisodeNumber); - if (result) - { - return - Response.AsJson(new JsonResponseModel - { - Result = false, - Message = $"{fullShowName} {Resources.UI.Search_AlreadyInPlex}" - }); - } - } - } - else if (PlexChecker.IsTvShowAvailable(shows.ToArray(), showInfo.name, - showInfo.premiered?.Substring(0, 4), - providerId, model.SeasonList)) - { - return - Response.AsJson(new JsonResponseModel - { - Result = false, - Message = $"{fullShowName} {Resources.UI.Search_AlreadyInPlex}" - }); - } - } - } - var embySettings = await EmbySettings.GetSettingsAsync(); - if (embySettings.Enable) - { - var embyContent = EmbyContentRepository.GetAll(); - var embyMovies = EmbyChecker.GetEmbyTvShows(embyContent); - var providerId = showId.ToString(); - if (episodeRequest) - { - var cachedEpisodesTask = await EmbyChecker.GetEpisodes(); - var cachedEpisodes = cachedEpisodesTask.ToList(); - foreach (var d in difference) // difference is from an existing request - { - if ( - cachedEpisodes.Any( - x => - x.SeasonNumber == d.SeasonNumber && x.EpisodeNumber == d.EpisodeNumber && - x.ProviderId == providerId)) - { - return - Response.AsJson(new JsonResponseModel - { - Result = false, - Message = - $"{fullShowName} {d.SeasonNumber} - {d.EpisodeNumber} {Resources.UI.Search_AlreadyInPlex}" - }); - } - } - - var diff = await GetEpisodeRequestDifference(showId, model); - model.Episodes = diff.ToList(); - } - else - { - if (embySettings.EnableEpisodeSearching) - { - foreach (var s in showInfo.Season) - { - var result = EmbyChecker.IsEpisodeAvailable(showId.ToString(), s.SeasonNumber, - s.EpisodeNumber); - if (result) - { - return - Response.AsJson(new JsonResponseModel - { - Result = false, - Message = $"{fullShowName} is already in Emby!" - }); - } - } - } - else if (EmbyChecker.IsTvShowAvailable(embyMovies.ToArray(), showInfo.name, - showInfo.premiered?.Substring(0, 4), - providerId, model.SeasonList)) - { - return - Response.AsJson(new JsonResponseModel - { - Result = false, - Message = $"{fullShowName} is already in Emby!" - }); - } - } - } - } - catch (Exception) - { - return - Response.AsJson(new JsonResponseModel - { - Result = false, - Message = string.Format(Resources.UI.Search_CouldNotCheckPlex, fullShowName) - }); - } - - if (showInfo.externals?.thetvdb == null) - { - await FaultQueue.QueueItemAsync(model, showInfo.id.ToString(), RequestType.TvShow, FaultType.MissingInformation, "We do not have a TheTVDBId from TVMaze"); - await NotificationService.Publish(new NotificationModel - { - DateTime = DateTime.Now, - User = Username, - RequestType = RequestType.TvShow, - Title = model.Title, - NotificationType = NotificationType.ItemAddedToFaultQueue - }); - return Response.AsJson(new JsonResponseModel - { - Result = true, - Message = $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}" - }); - } - - model.ProviderId = showInfo.externals?.thetvdb ?? 0; - - try - { - if (ShouldAutoApprove(RequestType.TvShow)) - { - return await SendTv(model, sonarrSettings, existingRequest, fullShowName, settings); - } - return await AddRequest(model, settings, $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"); - } - catch (Exception e) - { - await FaultQueue.QueueItemAsync(model, showInfo.id.ToString(), RequestType.TvShow, FaultType.RequestFault, e.Message); - await NotificationService.Publish(new NotificationModel - { - DateTime = DateTime.Now, - User = Username, - RequestType = RequestType.TvShow, - Title = model.Title, - NotificationType = NotificationType.ItemAddedToFaultQueue - }); - Log.Error(e); - return - Response.AsJson(new JsonResponseModel - { - Result = true, - Message = $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}" - }); - } - } - - private async Task AddUserToRequest(RequestedModel existingRequest, PlexRequestSettings settings, - string fullShowName, bool episodeReq = false) - { - // check if the current user is already marked as a requester for this show, if not, add them - if (!existingRequest.UserHasRequested(Username)) - { - existingRequest.RequestedUsers.Add(Username); - } - if (Security.HasPermissions(User, Permissions.UsersCanViewOnlyOwnRequests) || episodeReq) - { - return - await - UpdateRequest(existingRequest, settings, - $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"); - } - - return - await UpdateRequest(existingRequest, settings, $"{fullShowName} {Resources.UI.Search_AlreadyRequested}"); - } - - private bool ShouldSendNotification(RequestType type, PlexRequestSettings prSettings) - { - var sendNotification = ShouldAutoApprove(type) - ? !prSettings.IgnoreNotifyForAutoApprovedRequests - : true; - - if (IsAdmin) - { - sendNotification = false; // Don't bother sending a notification if the user is an admin - - } - return sendNotification; - } - - - private async Task RequestAlbum(string releaseId) - { - if (Security.HasPermissions(User, Permissions.ReadOnlyUser) || !Security.HasPermissions(User, Permissions.RequestMusic)) - { - return - Response.AsJson(new JsonResponseModel - { - Result = false, - Message = "Sorry, you do not have the correct permissions to request music!" - }); - } - - var settings = await PrService.GetSettingsAsync(); - if (!await CheckRequestLimit(settings, RequestType.Album)) - { - return - Response.AsJson(new JsonResponseModel - { - Result = false, - Message = Resources.UI.Search_WeeklyRequestLimitAlbums - }); - } - Analytics.TrackEventAsync(Category.Search, Action.Request, "Album", Username, - CookieHelper.GetAnalyticClientId(Cookies)); - var existingRequest = await RequestService.CheckRequestAsync(releaseId); - - if (existingRequest != null) - { - if (!existingRequest.UserHasRequested(Username)) - { - existingRequest.RequestedUsers.Add(Username); - await RequestService.UpdateRequestAsync(existingRequest); - } - return - Response.AsJson(new JsonResponseModel - { - Result = true, - Message = - Security.HasPermissions(User, Permissions.UsersCanViewOnlyOwnRequests) - ? $"{existingRequest.Title} {Resources.UI.Search_SuccessfullyAdded}" - : $"{existingRequest.Title} {Resources.UI.Search_AlreadyRequested}" - }); - } - - var albumInfo = MusicBrainzApi.GetAlbum(releaseId); - DateTime release; - DateTimeHelper.CustomParse(albumInfo.ReleaseEvents?.FirstOrDefault()?.date, out release); - - var artist = albumInfo.ArtistCredits?.FirstOrDefault()?.artist; - if (artist == null) - { - return - Response.AsJson(new JsonResponseModel - { - Result = false, - Message = Resources.UI.Search_MusicBrainzError - }); - } - - - var content = PlexContentRepository.GetAll(); - var albums = PlexChecker.GetPlexAlbums(content); - var alreadyInPlex = PlexChecker.IsAlbumAvailable(albums.ToArray(), albumInfo.title, release.ToString("yyyy"), - artist.name); - - if (alreadyInPlex) - { - return Response.AsJson(new JsonResponseModel - { - Result = false, - Message = $"{albumInfo.title} {Resources.UI.Search_AlreadyInPlex}" - }); - } - - var img = GetMusicBrainzCoverArt(albumInfo.id); - - var model = new RequestedModel - { - Title = albumInfo.title, - MusicBrainzId = albumInfo.id, - Overview = albumInfo.disambiguation, - PosterPath = img, - Type = RequestType.Album, - ProviderId = 0, - RequestedUsers = new List { Username }, - Status = albumInfo.status, - Issues = IssueState.None, - RequestedDate = DateTime.UtcNow, - ReleaseDate = release, - ArtistName = artist.name, - ArtistId = artist.id - }; - - try - { - if (ShouldAutoApprove(RequestType.Album)) - { - model.Approved = true; - var hpSettings = HeadphonesService.GetSettings(); - - if (!hpSettings.Enabled) - { - await RequestService.AddRequestAsync(model); - return - Response.AsJson(new JsonResponseModel - { - Result = true, - Message = $"{model.Title} {Resources.UI.Search_SuccessfullyAdded}" - }); - } - - var sender = new HeadphonesSender(HeadphonesApi, hpSettings, RequestService); - await sender.AddAlbum(model); - return await AddRequest(model, settings, $"{model.Title} {Resources.UI.Search_SuccessfullyAdded}"); - } - - return await AddRequest(model, settings, $"{model.Title} {Resources.UI.Search_SuccessfullyAdded}"); - } - catch (Exception e) - { - Log.Error(e); - await FaultQueue.QueueItemAsync(model, albumInfo.id, RequestType.Album, FaultType.RequestFault, e.Message); - - await NotificationService.Publish(new NotificationModel - { - DateTime = DateTime.Now, - User = Username, - RequestType = RequestType.Album, - Title = model.Title, - NotificationType = NotificationType.ItemAddedToFaultQueue - }); - throw; - } - } - - private string GetMusicBrainzCoverArt(string id) - { - var coverArt = MusicBrainzApi.GetCoverArt(id); - var firstImage = coverArt?.images?.FirstOrDefault(); - var img = string.Empty; - - if (firstImage != null) - { - img = firstImage.thumbnails?.small ?? firstImage.image; - } - - return img; - } - - private Response GetSeasons() - { - var seriesId = (int)Request.Query.tvId; - var show = TvApi.ShowLookupByTheTvDbId(seriesId); - var seasons = TvApi.GetSeasons(show.id); - var model = seasons.Select(x => x.number); - return Response.AsJson(model); - } - - private async Task GetEpisodes() - { - var seriesId = (int)Request.Query.tvId; - var model = await GetEpisodes(seriesId); - - return Response.AsJson(model); - } - - private async Task> GetEpisodes(int providerId) - { - var s = await SonarrService.GetSettingsAsync(); - var sonarrEnabled = s.Enabled; - var allResults = await RequestService.GetAllAsync(); - - var seriesTask = Task.Run( - () => - { - if (sonarrEnabled) - { - var allSeries = SonarrApi.GetSeries(s.ApiKey, s.FullUri); - var selectedSeries = allSeries.FirstOrDefault(x => x.tvdbId == providerId) ?? new Series(); - return selectedSeries; - } - return new Series(); - }); - - var model = new List(); - - var requests = allResults as RequestedModel[] ?? allResults.ToArray(); - - var existingRequest = requests.FirstOrDefault(x => x.Type == RequestType.TvShow && x.TvDbId == providerId.ToString()); - var show = await Task.Run(() => TvApi.ShowLookupByTheTvDbId(providerId)); - var tvMazeEpisodesTask = await Task.Run(() => TvApi.EpisodeLookup(show.id)); - var tvMazeEpisodes = tvMazeEpisodesTask.ToList(); - - var sonarrEpisodes = new List(); - if (sonarrEnabled) - { - var sonarrSeries = await seriesTask; - var sonarrEp = SonarrApi.GetEpisodes(sonarrSeries.id.ToString(), s.ApiKey, s.FullUri); - sonarrEpisodes = sonarrEp?.ToList() ?? new List(); - } - - var plexSettings = await PlexService.GetSettingsAsync(); - if (plexSettings.Enable) - { - var plexCacheTask = await PlexChecker.GetEpisodes(providerId); - var plexCache = plexCacheTask.ToList(); - foreach (var ep in tvMazeEpisodes) - { - var requested = existingRequest?.Episodes - .Any(episodesModel => - ep.number == episodesModel.EpisodeNumber && - ep.season == episodesModel.SeasonNumber) ?? false; - - var alreadyInPlex = plexCache.Any(x => x.EpisodeNumber == ep.number && x.SeasonNumber == ep.season); - var inSonarr = - sonarrEpisodes.Any(x => x.seasonNumber == ep.season && x.episodeNumber == ep.number && x.hasFile); - - model.Add(new EpisodeListViewModel - { - Id = show.id, - SeasonNumber = ep.season, - EpisodeNumber = ep.number, - Requested = requested || alreadyInPlex || inSonarr, - Name = ep.name, - EpisodeId = ep.id - }); - } - } - var embySettings = await EmbySettings.GetSettingsAsync(); - if (embySettings.Enable) - { - var embyCacheTask = await EmbyChecker.GetEpisodes(providerId); - var cache = embyCacheTask.ToList(); - foreach (var ep in tvMazeEpisodes) - { - var requested = existingRequest?.Episodes - .Any(episodesModel => - ep.number == episodesModel.EpisodeNumber && - ep.season == episodesModel.SeasonNumber) ?? false; - - var alreadyInEmby = cache.Any(x => x.EpisodeNumber == ep.number && x.SeasonNumber == ep.season); - var inSonarr = - sonarrEpisodes.Any(x => x.seasonNumber == ep.season && x.episodeNumber == ep.number && x.hasFile); - - model.Add(new EpisodeListViewModel - { - Id = show.id, - SeasonNumber = ep.season, - EpisodeNumber = ep.number, - Requested = requested || alreadyInEmby || inSonarr, - Name = ep.name, - EpisodeId = ep.id - }); - } - } - return model; - - } - - public async Task CheckRequestLimit(PlexRequestSettings s, RequestType type) - { - if (IsAdmin) - return true; - - if (Security.HasPermissions(User, Permissions.BypassRequestLimit)) - return true; - - var requestLimit = GetRequestLimitForType(type, s); - if (requestLimit == 0) - { - return true; - } - - var limit = await RequestLimitRepo.GetAllAsync(); - var usersLimit = limit.FirstOrDefault(x => x.Username == Username && x.RequestType == type); - if (usersLimit == null) - { - // Have not set a requestLimit yet - return true; - } - - return requestLimit > usersLimit.RequestCount; - } - - private int GetRequestLimitForType(RequestType type, PlexRequestSettings s) - { - int requestLimit; - switch (type) - { - case RequestType.Movie: - requestLimit = s.MovieWeeklyRequestLimit; - break; - case RequestType.TvShow: - requestLimit = s.TvWeeklyRequestLimit; - break; - case RequestType.Album: - requestLimit = s.AlbumWeeklyRequestLimit; - break; - default: - throw new ArgumentOutOfRangeException(nameof(type), type, null); - } - return requestLimit; - } - - private async Task AddRequest(RequestedModel model, PlexRequestSettings settings, string message) - { - await RequestService.AddRequestAsync(model); - - if (ShouldSendNotification(model.Type, settings)) - { - var notificationModel = new NotificationModel - { - Title = model.Title, - User = Username, - DateTime = DateTime.Now, - NotificationType = NotificationType.NewRequest, - RequestType = model.Type, - ImgSrc = model.Type == RequestType.Movie ? $"https://image.tmdb.org/t/p/w300/{model.PosterPath}" : model.PosterPath - }; - await NotificationService.Publish(notificationModel); - } - - var limit = await RequestLimitRepo.GetAllAsync(); - var usersLimit = limit.FirstOrDefault(x => x.Username == Username && x.RequestType == model.Type); - if (usersLimit == null) - { - await RequestLimitRepo.InsertAsync(new RequestLimit - { - Username = Username, - RequestType = model.Type, - FirstRequestDate = DateTime.UtcNow, - RequestCount = 1 - }); - } - else - { - usersLimit.RequestCount++; - await RequestLimitRepo.UpdateAsync(usersLimit); - } - - return Response.AsJson(new JsonResponseModel { Result = true, Message = message }); - } - - private async Task UpdateRequest(RequestedModel model, PlexRequestSettings settings, string message) - { - await RequestService.UpdateRequestAsync(model); - - if (ShouldSendNotification(model.Type, settings)) - { - var notificationModel = new NotificationModel - { - Title = model.Title, - User = Username, - DateTime = DateTime.Now, - NotificationType = NotificationType.NewRequest, - RequestType = model.Type, - ImgSrc = model.Type == RequestType.Movie ? $"https://image.tmdb.org/t/p/w300/{model.PosterPath}" : model.PosterPath - }; - await NotificationService.Publish(notificationModel); - } - - var limit = await RequestLimitRepo.GetAllAsync(); - var usersLimit = limit.FirstOrDefault(x => x.Username == Username && x.RequestType == model.Type); - if (usersLimit == null) - { - await RequestLimitRepo.InsertAsync(new RequestLimit - { - Username = Username, - RequestType = model.Type, - FirstRequestDate = DateTime.UtcNow, - RequestCount = 1 - }); - } - else - { - usersLimit.RequestCount++; - await RequestLimitRepo.UpdateAsync(usersLimit); - } - - return Response.AsJson(new JsonResponseModel { Result = true, Message = message }); - } - - private IEnumerable GetListDifferences(IEnumerable existing, IEnumerable request) - { - var newRequest = request - .Select(r => - new EpisodesModel - { - SeasonNumber = r.SeasonNumber, - EpisodeNumber = r.EpisodeNumber - }).ToList(); - - return newRequest.Except(existing); - } - - private async Task> GetEpisodeRequestDifference(int showId, RequestedModel model) - { - var episodes = await GetEpisodes(showId); - var availableEpisodes = episodes.Where(x => x.Requested).ToList(); - var available = availableEpisodes.Select(a => new EpisodesModel { EpisodeNumber = a.EpisodeNumber, SeasonNumber = a.SeasonNumber }).ToList(); - - var diff = model.Episodes.Except(available); - return diff; - } - - public bool ShouldAutoApprove(RequestType requestType) - { - var admin = Security.HasPermissions(Context.CurrentUser, Permissions.Administrator); - // if the user is an admin, they go ahead and allow auto-approval - if (admin) return true; - - // check by request type if the category requires approval or not - switch (requestType) - { - case RequestType.Movie: - return Security.HasPermissions(User, Permissions.AutoApproveMovie); - case RequestType.TvShow: - return Security.HasPermissions(User, Permissions.AutoApproveTv); - case RequestType.Album: - return Security.HasPermissions(User, Permissions.AutoApproveAlbum); - default: - return false; - } - } - - private enum ShowSearchType - { - Popular, - Anticipated, - MostWatched, - Trending - } - - private async Task SendTv(RequestedModel model, Task sonarrSettings, RequestedModel existingRequest, string fullShowName, PlexRequestSettings settings) - { - model.Approved = true; - var s = await sonarrSettings; - var sender = new TvSenderOld(SonarrApi, SickrageApi, Cache); // TODO put back - if (s.Enabled) - { - var result = await sender.SendToSonarr(s, model); - if (!string.IsNullOrEmpty(result?.title)) - { - if (existingRequest != null) - { - return await UpdateRequest(model, settings, - $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"); - } - return - await - AddRequest(model, settings, - $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"); - } - Log.Debug("Error with sending to sonarr."); - return - Response.AsJson(ValidationHelper.SendSonarrError(result?.ErrorMessages ?? new List())); - } - - var srSettings = SickRageService.GetSettings(); - if (srSettings.Enabled) - { - var result = sender.SendToSickRage(srSettings, model); - if (result?.result == "success") - { - return await AddRequest(model, settings, - $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"); - } - return - Response.AsJson(new JsonResponseModel - { - Result = false, - Message = result?.message ?? Resources.UI.Search_SickrageError - }); - } - - if (!srSettings.Enabled && !s.Enabled) - { - return await AddRequest(model, settings, $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"); - } - - return - Response.AsJson(new JsonResponseModel { Result = false, Message = Resources.UI.Search_TvNotSetUp }); - } - } + public class SearchModule : BaseAuthModule + { + public SearchModule(ICacheProvider cache, + ISettingsService prSettings, IAvailabilityChecker plexChecker, + IRequestService request, ISonarrApi sonarrApi, ISettingsService sonarrSettings, + ISettingsService sickRageService, ISickRageApi srApi, + INotificationService notify, IMusicBrainzApi mbApi, IHeadphonesApi hpApi, + ISettingsService hpService, + ICouchPotatoCacher cpCacher, IWatcherCacher watcherCacher, ISonarrCacher sonarrCacher, ISickRageCacher sickRageCacher, IPlexApi plexApi, + ISettingsService plexService, ISettingsService auth, + IRepository u, ISettingsService email, + IIssueService issue, IAnalytics a, IRepository rl, ITransientFaultQueue tfQueue, IRepository content, + ISecurityExtensions security, IMovieSender movieSender, IRadarrCacher radarrCacher, ITraktApi traktApi, ISettingsService cus, + IEmbyAvailabilityChecker embyChecker, IRepository embyContent, ISettingsService embySettings) + : base("search", prSettings, security) + { + Auth = auth; + PlexService = plexService; + PlexApi = plexApi; + PrService = prSettings; + MovieApi = new TheMovieDbApi(); + Cache = cache; + PlexChecker = plexChecker; + CpCacher = cpCacher; + SonarrCacher = sonarrCacher; + SickRageCacher = sickRageCacher; + RequestService = request; + SonarrApi = sonarrApi; + SonarrService = sonarrSettings; + SickRageService = sickRageService; + SickrageApi = srApi; + NotificationService = notify; + MusicBrainzApi = mbApi; + HeadphonesApi = hpApi; + HeadphonesService = hpService; + UsersToNotifyRepo = u; + EmailNotificationSettings = email; + IssueService = issue; + Analytics = a; + RequestLimitRepo = rl; + FaultQueue = tfQueue; + TvApi = new TvMazeApi(); + PlexContentRepository = content; + MovieSender = movieSender; + WatcherCacher = watcherCacher; + RadarrCacher = radarrCacher; + TraktApi = traktApi; + CustomizationSettings = cus; + EmbyChecker = embyChecker; + EmbyContentRepository = embyContent; + EmbySettings = embySettings; + + Get["SearchIndex", "/", true] = async (x, ct) => await RequestLoad(); + + Get["movie/{searchTerm}", true] = async (x, ct) => await SearchMovie((string)x.searchTerm); + Get["tv/{searchTerm}", true] = async (x, ct) => await SearchTvShow((string)x.searchTerm); + Get["music/{searchTerm}", true] = async (x, ct) => await SearchAlbum((string)x.searchTerm); + Get["music/coverArt/{id}"] = p => GetMusicBrainzCoverArt((string)p.id); + + Get["movie/upcoming", true] = async (x, ct) => await UpcomingMovies(); + Get["movie/playing", true] = async (x, ct) => await CurrentlyPlayingMovies(); + + Get["tv/popular", true] = async (x, ct) => await ProcessShows(ShowSearchType.Popular); + Get["tv/trending", true] = async (x, ct) => await ProcessShows(ShowSearchType.Trending); + Get["tv/mostwatched", true] = async (x, ct) => await ProcessShows(ShowSearchType.MostWatched); + Get["tv/anticipated", true] = async (x, ct) => await ProcessShows(ShowSearchType.Anticipated); + + Get["tv/poster/{id}"] = p => GetTvPoster((int)p.id); + + Post["request/movie", true] = async (x, ct) => await RequestMovie((int)Request.Form.movieId); + Post["request/tv", true] = + async (x, ct) => await RequestTvShow((int)Request.Form.tvId, (string)Request.Form.seasons); + Post["request/tvEpisodes", true] = async (x, ct) => await RequestTvShow(0, "episode"); + Post["request/album", true] = async (x, ct) => await RequestAlbum((string)Request.Form.albumId); + + Get["/seasons"] = x => GetSeasons(); + Get["/episodes", true] = async (x, ct) => await GetEpisodes(); + } + private ITraktApi TraktApi { get; } + private IWatcherCacher WatcherCacher { get; } + private IMovieSender MovieSender { get; } + private IRepository PlexContentRepository { get; } + private IRepository EmbyContentRepository { get; } + private TvMazeApi TvApi { get; } + private IPlexApi PlexApi { get; } + private TheMovieDbApi MovieApi { get; } + private INotificationService NotificationService { get; } + private ISonarrApi SonarrApi { get; } + private ISickRageApi SickrageApi { get; } + private IRequestService RequestService { get; } + private ICacheProvider Cache { get; } + private ISettingsService Auth { get; } + private ISettingsService EmbySettings { get; } + private ISettingsService PlexService { get; } + private ISettingsService PrService { get; } + private ISettingsService SonarrService { get; } + private ISettingsService SickRageService { get; } + private ISettingsService HeadphonesService { get; } + private ISettingsService EmailNotificationSettings { get; } + private IAvailabilityChecker PlexChecker { get; } + private IEmbyAvailabilityChecker EmbyChecker { get; } + private ICouchPotatoCacher CpCacher { get; } + private ISonarrCacher SonarrCacher { get; } + private ISickRageCacher SickRageCacher { get; } + private IMusicBrainzApi MusicBrainzApi { get; } + private IHeadphonesApi HeadphonesApi { get; } + private IRepository UsersToNotifyRepo { get; } + private IIssueService IssueService { get; } + private IAnalytics Analytics { get; } + private ITransientFaultQueue FaultQueue { get; } + private IRepository RequestLimitRepo { get; } + private IRadarrCacher RadarrCacher { get; } + private ISettingsService CustomizationSettings { get; } + private static Logger Log = LogManager.GetCurrentClassLogger(); + + private async Task RequestLoad() + { + + var settings = await PrService.GetSettingsAsync(); + var custom = await CustomizationSettings.GetSettingsAsync(); + var emby = await EmbySettings.GetSettingsAsync(); + var plex = await PlexService.GetSettingsAsync(); + var searchViewModel = new SearchLoadViewModel + { + Settings = settings, + CustomizationSettings = custom, + Emby = emby.Enable, + Plex = plex.Enable + }; + + + return View["Search/Index", searchViewModel]; + } + + private async Task UpcomingMovies() + { + Analytics.TrackEventAsync(Category.Search, Action.Movie, "Upcoming", Username, + CookieHelper.GetAnalyticClientId(Cookies)); + return await ProcessMovies(MovieSearchType.Upcoming, string.Empty); + } + + private async Task CurrentlyPlayingMovies() + { + Analytics.TrackEventAsync(Category.Search, Action.Movie, "CurrentlyPlaying", Username, + CookieHelper.GetAnalyticClientId(Cookies)); + return await ProcessMovies(MovieSearchType.CurrentlyPlaying, string.Empty); + } + + private async Task SearchMovie(string searchTerm) + { + Analytics.TrackEventAsync(Category.Search, Action.Movie, searchTerm, Username, + CookieHelper.GetAnalyticClientId(Cookies)); + return await ProcessMovies(MovieSearchType.Search, searchTerm); + } + + private Response GetTvPoster(int theTvDbId) + { + var result = TvApi.ShowLookupByTheTvDbId(theTvDbId); + + var banner = result.image?.medium; + if (!string.IsNullOrEmpty(banner)) + { + banner = banner.Replace("http", "https"); // Always use the Https banners + } + return banner; + } + private async Task ProcessMovies(MovieSearchType searchType, string searchTerm) + { + List apiMovies; + + switch (searchType) + { + case MovieSearchType.Search: + var movies = await MovieApi.SearchMovie(searchTerm).ConfigureAwait(false); + apiMovies = movies.Select(x => + new MovieResult + { + Adult = x.Adult, + BackdropPath = x.BackdropPath, + GenreIds = x.GenreIds, + Id = x.Id, + OriginalLanguage = x.OriginalLanguage, + OriginalTitle = x.OriginalTitle, + Overview = x.Overview, + Popularity = x.Popularity, + PosterPath = x.PosterPath, + ReleaseDate = x.ReleaseDate, + Title = x.Title, + Video = x.Video, + VoteAverage = x.VoteAverage, + VoteCount = x.VoteCount + }) + .ToList(); + break; + case MovieSearchType.CurrentlyPlaying: + apiMovies = await MovieApi.GetCurrentPlayingMovies(); + break; + case MovieSearchType.Upcoming: + apiMovies = await MovieApi.GetUpcomingMovies(); + break; + default: + apiMovies = new List(); + break; + } + + var allResults = await RequestService.GetAllAsync(); + allResults = allResults.Where(x => x.Type == RequestType.Movie); + + var distinctResults = allResults.DistinctBy(x => x.ProviderId); + var dbMovies = distinctResults.ToDictionary(x => x.ProviderId); + + + var cpCached = CpCacher.QueuedIds(); + var watcherCached = WatcherCacher.QueuedIds(); + var radarrCached = RadarrCacher.QueuedIds(); + + var viewMovies = new List(); + var counter = 0; + foreach (var movie in apiMovies) + { + var viewMovie = new SearchMovieViewModel + { + Adult = movie.Adult, + BackdropPath = movie.BackdropPath, + GenreIds = movie.GenreIds, + Id = movie.Id, + OriginalLanguage = movie.OriginalLanguage, + OriginalTitle = movie.OriginalTitle, + Overview = movie.Overview, + Popularity = movie.Popularity, + PosterPath = movie.PosterPath, + ReleaseDate = movie.ReleaseDate, + Title = movie.Title, + Video = movie.Video, + VoteAverage = movie.VoteAverage, + VoteCount = movie.VoteCount + }; + + if (counter <= 5) // Let's only do it for the first 5 items + { + var movieInfo = MovieApi.GetMovieInformationWithVideos(movie.Id); + + // TODO needs to be careful about this, it's adding extra time to search... + // https://www.themoviedb.org/talk/5807f4cdc3a36812160041f2 + viewMovie.ImdbId = movieInfo?.imdb_id; + viewMovie.Homepage = movieInfo?.homepage; + var videoId = movieInfo?.video ?? false + ? movieInfo?.videos?.results?.FirstOrDefault()?.key + : string.Empty; + + viewMovie.Trailer = string.IsNullOrEmpty(videoId) + ? string.Empty + : $"https://www.youtube.com/watch?v={videoId}"; + + counter++; + } + + var canSee = CanUserSeeThisRequest(viewMovie.Id, Security.HasPermissions(User, Permissions.UsersCanViewOnlyOwnRequests), dbMovies); + + var plexSettings = await PlexService.GetSettingsAsync(); + var embySettings = await EmbySettings.GetSettingsAsync(); + if (plexSettings.Enable) + { + var content = PlexContentRepository.GetAll(); + var plexMovies = PlexChecker.GetPlexMovies(content); + + var plexMovie = PlexChecker.GetMovie(plexMovies.ToArray(), movie.Title, + movie.ReleaseDate?.Year.ToString(), + viewMovie.ImdbId); + if (plexMovie != null) + { + viewMovie.Available = true; + viewMovie.PlexUrl = plexMovie.Url; + } + } + if (embySettings.Enable) + { + var embyContent = EmbyContentRepository.GetAll(); + var embyMovies = EmbyChecker.GetEmbyMovies(embyContent); + + var embyMovie = EmbyChecker.GetMovie(embyMovies.ToArray(), movie.Title, + movie.ReleaseDate?.Year.ToString(), viewMovie.ImdbId); + if (embyMovie != null) + { + viewMovie.Available = true; + } + } + else if (dbMovies.ContainsKey(movie.Id) && canSee) // compare to the requests db + { + var dbm = dbMovies[movie.Id]; + + viewMovie.Requested = true; + viewMovie.Approved = dbm.Approved; + viewMovie.Available = dbm.Available; + } + else if (cpCached.Contains(movie.Id) && canSee) // compare to the couchpotato db + { + viewMovie.Approved = true; + viewMovie.Requested = true; + } + else if (watcherCached.Contains(viewMovie.ImdbId) && canSee) // compare to the watcher db + { + viewMovie.Approved = true; + viewMovie.Requested = true; + } + else if (radarrCached.Contains(movie.Id) && canSee) + { + viewMovie.Approved = true; + viewMovie.Requested = true; + } + viewMovies.Add(viewMovie); + } + + return Response.AsJson(viewMovies); + } + + private bool CanUserSeeThisRequest(int movieId, bool usersCanViewOnlyOwnRequests, + Dictionary moviesInDb) + { + if (usersCanViewOnlyOwnRequests) + { + var result = moviesInDb.FirstOrDefault(x => x.Value.ProviderId == movieId); + return result.Value == null || result.Value.UserHasRequested(Username); + } + + return true; + } + + private async Task ProcessShows(ShowSearchType type) + { + var shows = new List(); + var prSettings = await PrService.GetSettingsAsync(); + switch (type) + { + case ShowSearchType.Popular: + Analytics.TrackEventAsync(Category.Search, Action.TvShow, "Popular", Username, CookieHelper.GetAnalyticClientId(Cookies)); + var popularShows = await TraktApi.GetPopularShows(); + + foreach (var popularShow in popularShows) + { + var theTvDbId = int.Parse(popularShow.Ids.Tvdb.ToString()); + + var model = new SearchTvShowViewModel + { + FirstAired = popularShow.FirstAired?.ToString("yyyy-MM-ddTHH:mm:ss"), + Id = theTvDbId, + ImdbId = popularShow.Ids.Imdb, + Network = popularShow.Network, + Overview = popularShow.Overview.RemoveHtml(), + Rating = popularShow.Rating.ToString(), + Runtime = popularShow.Runtime.ToString(), + SeriesName = popularShow.Title, + Status = popularShow.Status.DisplayName, + DisableTvRequestsByEpisode = prSettings.DisableTvRequestsByEpisode, + DisableTvRequestsBySeason = prSettings.DisableTvRequestsBySeason, + EnableTvRequestsForOnlySeries = (prSettings.DisableTvRequestsByEpisode && prSettings.DisableTvRequestsBySeason), + Trailer = popularShow.Trailer, + Homepage = popularShow.Homepage + }; + shows.Add(model); + } + shows = await MapToTvModel(shows, prSettings); + break; + case ShowSearchType.Anticipated: + Analytics.TrackEventAsync(Category.Search, Action.TvShow, "Anticipated", Username, CookieHelper.GetAnalyticClientId(Cookies)); + var anticipated = await TraktApi.GetAnticipatedShows(); + foreach (var anticipatedShow in anticipated) + { + var show = anticipatedShow.Show; + var theTvDbId = int.Parse(show.Ids.Tvdb.ToString()); + + var model = new SearchTvShowViewModel + { + FirstAired = show.FirstAired?.ToString("yyyy-MM-ddTHH:mm:ss"), + Id = theTvDbId, + ImdbId = show.Ids.Imdb, + Network = show.Network ?? string.Empty, + Overview = show.Overview?.RemoveHtml() ?? string.Empty, + Rating = show.Rating.ToString(), + Runtime = show.Runtime.ToString(), + SeriesName = show.Title, + Status = show.Status?.DisplayName ?? string.Empty, + DisableTvRequestsByEpisode = prSettings.DisableTvRequestsByEpisode, + DisableTvRequestsBySeason = prSettings.DisableTvRequestsBySeason, + EnableTvRequestsForOnlySeries = (prSettings.DisableTvRequestsByEpisode && prSettings.DisableTvRequestsBySeason), + Trailer = show.Trailer, + Homepage = show.Homepage + }; + shows.Add(model); + } + shows = await MapToTvModel(shows, prSettings); + break; + case ShowSearchType.MostWatched: + Analytics.TrackEventAsync(Category.Search, Action.TvShow, "MostWatched", Username, CookieHelper.GetAnalyticClientId(Cookies)); + var mostWatched = await TraktApi.GetMostWatchesShows(); + foreach (var watched in mostWatched) + { + var show = watched.Show; + var theTvDbId = int.Parse(show.Ids.Tvdb.ToString()); + var model = new SearchTvShowViewModel + { + FirstAired = show.FirstAired?.ToString("yyyy-MM-ddTHH:mm:ss"), + Id = theTvDbId, + ImdbId = show.Ids.Imdb, + Network = show.Network, + Overview = show.Overview.RemoveHtml(), + Rating = show.Rating.ToString(), + Runtime = show.Runtime.ToString(), + SeriesName = show.Title, + Status = show.Status.DisplayName, + DisableTvRequestsByEpisode = prSettings.DisableTvRequestsByEpisode, + DisableTvRequestsBySeason = prSettings.DisableTvRequestsBySeason, + EnableTvRequestsForOnlySeries = (prSettings.DisableTvRequestsByEpisode && prSettings.DisableTvRequestsBySeason), + Trailer = show.Trailer, + Homepage = show.Homepage + }; + shows.Add(model); + } + shows = await MapToTvModel(shows, prSettings); + break; + case ShowSearchType.Trending: + Analytics.TrackEventAsync(Category.Search, Action.TvShow, "Trending", Username, CookieHelper.GetAnalyticClientId(Cookies)); + var trending = await TraktApi.GetTrendingShows(); + foreach (var watched in trending) + { + var show = watched.Show; + var theTvDbId = int.Parse(show.Ids.Tvdb.ToString()); + var model = new SearchTvShowViewModel + { + FirstAired = show.FirstAired?.ToString("yyyy-MM-ddTHH:mm:ss"), + Id = theTvDbId, + ImdbId = show.Ids.Imdb, + Network = show.Network, + Overview = show.Overview.RemoveHtml(), + Rating = show.Rating.ToString(), + Runtime = show.Runtime.ToString(), + SeriesName = show.Title, + Status = show.Status.DisplayName, + DisableTvRequestsByEpisode = prSettings.DisableTvRequestsByEpisode, + DisableTvRequestsBySeason = prSettings.DisableTvRequestsBySeason, + EnableTvRequestsForOnlySeries = (prSettings.DisableTvRequestsByEpisode && prSettings.DisableTvRequestsBySeason), + Trailer = show.Trailer, + Homepage = show.Homepage + }; + shows.Add(model); + } + shows = await MapToTvModel(shows, prSettings); + break; + default: + throw new ArgumentOutOfRangeException(nameof(type), type, null); + } + + + return Response.AsJson(shows); + } + + private async Task> MapToTvModel(List shows, PlexRequestSettings prSettings) + { + + var plexSettings = await PlexService.GetSettingsAsync(); + + var providerId = string.Empty; + // Get the requests + var allResults = await RequestService.GetAllAsync(); + allResults = allResults.Where(x => x.Type == RequestType.TvShow); + var distinctResults = allResults.DistinctBy(x => x.ProviderId); + var dbTv = distinctResults.ToDictionary(x => x.ProviderId); + + // Check the external applications + var sonarrCached = SonarrCacher.QueuedIds().ToList(); + var sickRageCache = SickRageCacher.QueuedIds(); // consider just merging sonarr/sickrage arrays + var content = PlexContentRepository.GetAll(); + var plexTvShows = PlexChecker.GetPlexTvShows(content).ToList(); + + foreach (var show in shows) + { + if (plexSettings.AdvancedSearch) + { + providerId = show.Id.ToString(); + } + + var plexShow = PlexChecker.GetTvShow(plexTvShows.ToArray(), show.SeriesName, show.FirstAired?.Substring(0, 4), + providerId); + if (plexShow != null) + { + show.Available = true; + show.PlexUrl = plexShow.Url; + } + else + { + if (dbTv.ContainsKey(show.Id)) + { + var dbt = dbTv[show.Id]; + + show.Requested = true; + show.Episodes = dbt.Episodes.ToList(); + show.Approved = dbt.Approved; + } + if (sonarrCached.Select(x => x.TvdbId).Contains(show.Id) || sickRageCache.Contains(show.Id)) + // compare to the sonarr/sickrage db + { + show.Requested = true; + } + } + } + return shows; + } + + private async Task SearchTvShow(string searchTerm) + { + + Analytics.TrackEventAsync(Category.Search, Action.TvShow, searchTerm, Username, + CookieHelper.GetAnalyticClientId(Cookies)); + var plexSettings = await PlexService.GetSettingsAsync(); + var embySettings = await EmbySettings.GetSettingsAsync(); + var prSettings = await PrService.GetSettingsAsync(); + var providerId = string.Empty; + + var apiTv = new List(); + await Task.Factory.StartNew(() => new TvMazeApi().Search(searchTerm)).ContinueWith((t) => + { + apiTv = t.Result; + }); + + var allResults = await RequestService.GetAllAsync(); + allResults = allResults.Where(x => x.Type == RequestType.TvShow); + var distinctResults = allResults.DistinctBy(x => x.ProviderId); + var dbTv = distinctResults.ToDictionary(x => x.ProviderId); + + if (!apiTv.Any()) + { + return Response.AsJson(""); + } + + var sonarrCached = SonarrCacher.QueuedIds(); + var sickRageCache = SickRageCacher.QueuedIds(); // consider just merging sonarr/sickrage arrays + var content = PlexContentRepository.GetAll(); + var plexTvShows = PlexChecker.GetPlexTvShows(content); + var embyContent = EmbyContentRepository.GetAll(); + var embyCached = EmbyChecker.GetEmbyTvShows(embyContent); + + var viewTv = new List(); + foreach (var t in apiTv) + { + if (!(t.show.externals?.thetvdb.HasValue) ?? false) + { + continue; + } + var banner = t.show.image?.medium; + if (!string.IsNullOrEmpty(banner)) + { + banner = banner.Replace("http", "https"); // Always use the Https banners + } + + var viewT = new SearchTvShowViewModel + { + Banner = banner, + FirstAired = t.show.premiered, + Id = t.show.externals?.thetvdb ?? 0, + ImdbId = t.show.externals?.imdb, + Network = t.show.network?.name, + NetworkId = t.show.network?.id.ToString(), + Overview = t.show.summary.RemoveHtml(), + Rating = t.score.ToString(CultureInfo.CurrentUICulture), + Runtime = t.show.runtime.ToString(), + SeriesId = t.show.id, + SeriesName = t.show.name, + Status = t.show.status, + DisableTvRequestsByEpisode = prSettings.DisableTvRequestsByEpisode, + DisableTvRequestsBySeason = prSettings.DisableTvRequestsBySeason, + EnableTvRequestsForOnlySeries = (prSettings.DisableTvRequestsByEpisode && prSettings.DisableTvRequestsBySeason) + }; + + providerId = viewT.Id.ToString(); + + if (embySettings.Enable) + { + var embyShow = EmbyChecker.GetTvShow(embyCached.ToArray(), t.show.name, t.show.premiered?.Substring(0, 4), providerId); + if (embyShow != null) + { + viewT.Available = true; + } + } + if (plexSettings.Enable) + { + var plexShow = PlexChecker.GetTvShow(plexTvShows.ToArray(), t.show.name, t.show.premiered?.Substring(0, 4), + providerId); + if (plexShow != null) + { + viewT.Available = true; + viewT.PlexUrl = plexShow.Url; + } + } + + if (t.show?.externals?.thetvdb != null && !viewT.Available) + { + var tvdbid = (int)t.show.externals.thetvdb; + if (dbTv.ContainsKey(tvdbid)) + { + var dbt = dbTv[tvdbid]; + + viewT.Requested = true; + viewT.Episodes = dbt.Episodes.ToList(); + viewT.Approved = dbt.Approved; + } + if (sonarrCached.Select(x => x.TvdbId).Contains(tvdbid) || sickRageCache.Contains(tvdbid)) + // compare to the sonarr/sickrage db + { + viewT.Requested = true; + } + } + + viewTv.Add(viewT); + } + + return Response.AsJson(viewTv); + } + + private async Task SearchAlbum(string searchTerm) + { + Analytics.TrackEventAsync(Category.Search, Action.Album, searchTerm, Username, + CookieHelper.GetAnalyticClientId(Cookies)); + var apiAlbums = new List(); + await Task.Run(() => MusicBrainzApi.SearchAlbum(searchTerm)).ContinueWith((t) => + { + apiAlbums = t.Result.releases ?? new List(); + }); + + var allResults = await RequestService.GetAllAsync(); + allResults = allResults.Where(x => x.Type == RequestType.Album); + + var dbAlbum = allResults.ToDictionary(x => x.MusicBrainzId); + + var content = PlexContentRepository.GetAll(); + var plexAlbums = PlexChecker.GetPlexAlbums(content); + + var viewAlbum = new List(); + foreach (var a in apiAlbums) + { + var viewA = new SearchMusicViewModel + { + Title = a.title, + Id = a.id, + Artist = a.ArtistCredit?.Select(x => x.artist?.name).FirstOrDefault(), + Overview = a.disambiguation, + ReleaseDate = a.date, + TrackCount = a.TrackCount, + ReleaseType = a.status, + Country = a.country + }; + + DateTime release; + DateTimeHelper.CustomParse(a.ReleaseEvents?.FirstOrDefault()?.date, out release); + var artist = a.ArtistCredit?.FirstOrDefault()?.artist; + var plexAlbum = PlexChecker.GetAlbum(plexAlbums.ToArray(), a.title, release.ToString("yyyy"), artist?.name); + if (plexAlbum != null) + { + viewA.Available = true; + viewA.PlexUrl = plexAlbum.Url; + } + if (!string.IsNullOrEmpty(a.id) && dbAlbum.ContainsKey(a.id)) + { + var dba = dbAlbum[a.id]; + + viewA.Requested = true; + viewA.Approved = dba.Approved; + viewA.Available = dba.Available; + } + + viewAlbum.Add(viewA); + } + return Response.AsJson(viewAlbum); + } + + private async Task RequestMovie(int movieId) + { + if (Security.HasPermissions(User, Permissions.ReadOnlyUser) || !Security.HasPermissions(User, Permissions.RequestMovie)) + { + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = "Sorry, you do not have the correct permissions to request a movie!" + }); + } + var settings = await PrService.GetSettingsAsync(); + if (!await CheckRequestLimit(settings, RequestType.Movie)) + { + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = "You have reached your weekly request limit for Movies! Please contact your admin." + }); + } + var embySettings = await EmbySettings.GetSettingsAsync(); + Analytics.TrackEventAsync(Category.Search, Action.Request, "Movie", Username, + CookieHelper.GetAnalyticClientId(Cookies)); + var movieInfo = await MovieApi.GetMovieInformation(movieId); + if (movieInfo == null) + { + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = "There was an issue adding this movie!" + }); + } + var fullMovieName = + $"{movieInfo.Title}{(movieInfo.ReleaseDate.HasValue ? $" ({movieInfo.ReleaseDate.Value.Year})" : string.Empty)}"; + + var existingRequest = await RequestService.CheckRequestAsync(movieId); + if (existingRequest != null) + { + // check if the current user is already marked as a requester for this movie, if not, add them + if (!existingRequest.UserHasRequested(Username)) + { + existingRequest.RequestedUsers.Add(Username); + await RequestService.UpdateRequestAsync(existingRequest); + } + + return + Response.AsJson(new JsonResponseModel + { + Result = true, + Message = + Security.HasPermissions(User, Permissions.UsersCanViewOnlyOwnRequests) + ? $"{fullMovieName} {Ombi.UI.Resources.UI.Search_SuccessfullyAdded}" + : $"{fullMovieName} {Resources.UI.Search_AlreadyRequested}" + }); + } + + try + { + + var content = PlexContentRepository.GetAll(); + var movies = PlexChecker.GetPlexMovies(content); + if (PlexChecker.IsMovieAvailable(movies.ToArray(), movieInfo.Title, movieInfo.ReleaseDate?.Year.ToString())) + { + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = $"{fullMovieName} is already in Plex!" + }); + } + } + catch (Exception e) + { + Log.Error(e); + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = string.Format(Resources.UI.Search_CouldNotCheckPlex, fullMovieName,GetMediaServerName()) + }); + } + //#endif + + var model = new RequestedModel + { + ProviderId = movieInfo.Id, + Type = RequestType.Movie, + Overview = movieInfo.Overview, + ImdbId = movieInfo.ImdbId, + PosterPath = movieInfo.PosterPath, + Title = movieInfo.Title, + ReleaseDate = movieInfo.ReleaseDate ?? DateTime.MinValue, + Status = movieInfo.Status, + RequestedDate = DateTime.UtcNow, + Approved = false, + RequestedUsers = new List { Username }, + Issues = IssueState.None, + + }; + try + { + if (ShouldAutoApprove(RequestType.Movie)) + { + model.Approved = true; + + var result = await MovieSender.Send(model); + if (result.Result) + { + return await AddRequest(model, settings, + $"{fullMovieName} {Resources.UI.Search_SuccessfullyAdded}"); + } + if (result.Error) + + { + return + Response.AsJson(new JsonResponseModel + { + Message = "Could not add movie, please contract your administrator", + Result = false + }); + } + if (!result.MovieSendingEnabled) + { + + return await AddRequest(model, settings, $"{fullMovieName} {Resources.UI.Search_SuccessfullyAdded}"); + } + + return Response.AsJson(new JsonResponseModel + { + Result = false, + Message = Resources.UI.Search_CouchPotatoError + }); + } + + + return await AddRequest(model, settings, $"{fullMovieName} {Resources.UI.Search_SuccessfullyAdded}"); + } + catch (Exception e) + { + Log.Fatal(e); + await FaultQueue.QueueItemAsync(model, movieInfo.Id.ToString(), RequestType.Movie, FaultType.RequestFault, e.Message); + + await NotificationService.Publish(new NotificationModel + { + DateTime = DateTime.Now, + User = Username, + RequestType = RequestType.Movie, + Title = model.Title, + NotificationType = NotificationType.ItemAddedToFaultQueue + }); + + return Response.AsJson(new JsonResponseModel + { + Result = true, + Message = $"{fullMovieName} {Resources.UI.Search_SuccessfullyAdded}" + }); + } + } + + /// + /// Requests the tv show. + /// + /// The show identifier. + /// The seasons. + /// + private async Task RequestTvShow(int showId, string seasons) + { + if (Security.HasPermissions(User, Permissions.ReadOnlyUser) || !Security.HasPermissions(User, Permissions.RequestTvShow)) + { + return + Response.AsJson(new JsonResponseModel() + { + Result = false, + Message = "Sorry, you do not have the correct permissions to request a TV Show!" + }); + } + // Get the JSON from the request + var req = (Dictionary.ValueCollection)Request.Form.Values; + EpisodeRequestModel episodeModel = null; + if (req.Count == 1) + { + var json = req.FirstOrDefault()?.ToString(); + episodeModel = JsonConvert.DeserializeObject(json); // Convert it into the object + } + var episodeRequest = false; + + var settings = await PrService.GetSettingsAsync(); + if (!await CheckRequestLimit(settings, RequestType.TvShow)) + { + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = Resources.UI.Search_WeeklyRequestLimitTVShow + }); + } + Analytics.TrackEventAsync(Category.Search, Action.Request, "TvShow", Username, + CookieHelper.GetAnalyticClientId(Cookies)); + + var sonarrSettings = SonarrService.GetSettingsAsync(); + + // This means we are requesting an episode rather than a whole series or season + if (episodeModel != null) + { + episodeRequest = true; + showId = episodeModel.ShowId; + var s = await sonarrSettings; + if (!s.Enabled) + { + return + Response.AsJson(new JsonResponseModel + { + Message = + "This is currently only supported with Sonarr, Please enable Sonarr for this feature", + Result = false + }); + } + } + var embySettings = await EmbySettings.GetSettingsAsync(); + var showInfo = TvApi.ShowLookupByTheTvDbId(showId); + DateTime firstAir; + DateTime.TryParse(showInfo.premiered, out firstAir); + string fullShowName = $"{showInfo.name} ({firstAir.Year})"; + + // For some reason the poster path is always http + var posterPath = showInfo.image?.medium.Replace("http:", "https:"); + var model = new RequestedModel + { + Type = RequestType.TvShow, + Overview = showInfo.summary.RemoveHtml(), + PosterPath = posterPath, + Title = showInfo.name, + ReleaseDate = firstAir, + Status = showInfo.status, + RequestedDate = DateTime.UtcNow, + Approved = false, + RequestedUsers = new List { Username }, + Issues = IssueState.None, + ImdbId = showInfo.externals?.imdb ?? string.Empty, + SeasonCount = showInfo.Season.Count, + TvDbId = showId.ToString() + }; + + var seasonsList = new List(); + switch (seasons) + { + case "first": + seasonsList.Add(1); + model.SeasonsRequested = "First"; + break; + case "latest": + seasonsList.Add(model.SeasonCount); + model.SeasonsRequested = "Latest"; + break; + case "all": + model.SeasonsRequested = "All"; + break; + case "episode": + model.Episodes = new List(); + + foreach (var ep in episodeModel?.Episodes ?? new Models.EpisodesModel[0]) + { + model.Episodes.Add(new EpisodesModel + { + EpisodeNumber = ep.EpisodeNumber, + SeasonNumber = ep.SeasonNumber + }); + } + Analytics.TrackEventAsync(Category.Requests, Action.TvShow, $"Episode request for {model.Title}", + Username, CookieHelper.GetAnalyticClientId(Cookies)); + break; + default: + model.SeasonsRequested = seasons; + var split = seasons.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + var seasonsCount = new int[split.Length]; + for (var i = 0; i < split.Length; i++) + { + int tryInt; + int.TryParse(split[i], out tryInt); + seasonsCount[i] = tryInt; + } + seasonsList.AddRange(seasonsCount); + break; + } + + model.SeasonList = seasonsList.ToArray(); + + // check if the show/episodes have already been requested + var existingRequest = await RequestService.CheckRequestAsync(showId); + var difference = new List(); + if (existingRequest != null) + { + if (episodeRequest) + { + // Make sure we are not somehow adding dupes + difference = GetListDifferences(existingRequest.Episodes, episodeModel.Episodes).ToList(); + if (difference.Any()) + { + // Convert the request into the correct shape + var newEpisodes = episodeModel.Episodes?.Select(x => new EpisodesModel + { + SeasonNumber = x.SeasonNumber, + EpisodeNumber = x.EpisodeNumber + }); + + // Add it to the existing requests + existingRequest.Episodes.AddRange(newEpisodes ?? Enumerable.Empty()); + + // It's technically a new request now, so set the status to not approved. + var autoApprove = ShouldAutoApprove(RequestType.TvShow); + if (autoApprove) + { + return await SendTv(model, sonarrSettings, existingRequest, fullShowName, settings); + } + existingRequest.Approved = false; + + return await AddUserToRequest(existingRequest, settings, fullShowName, true); + } + else + { + // We no episodes to approve + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = $"{fullShowName} {string.Format(Resources.UI.Search_AlreadyInPlex,embySettings.Enable ? "Emby" : "Plex")}" + }); + } + } + else if (model.SeasonList.Except(existingRequest.SeasonList).Any()) + { + // This is a season being requested that we do not yet have + // Let's just continue + } + else + { + return await AddUserToRequest(existingRequest, settings, fullShowName); + } + } + + try + { + + var plexSettings = await PlexService.GetSettingsAsync(); + if (plexSettings.Enable) + { + var content = PlexContentRepository.GetAll(); + var shows = PlexChecker.GetPlexTvShows(content); + + var providerId = string.Empty; + if (plexSettings.AdvancedSearch) + { + providerId = showId.ToString(); + } + if (episodeRequest) + { + var cachedEpisodesTask = await PlexChecker.GetEpisodes(); + var cachedEpisodes = cachedEpisodesTask.ToList(); + foreach (var d in difference) // difference is from an existing request + { + if ( + cachedEpisodes.Any( + x => + x.SeasonNumber == d.SeasonNumber && x.EpisodeNumber == d.EpisodeNumber && + x.ProviderId == providerId)) + { + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = + $"{fullShowName} {d.SeasonNumber} - {d.EpisodeNumber} {string.Format(Resources.UI.Search_AlreadyInPlex,GetMediaServerName())}" + }); + } + } + + var diff = await GetEpisodeRequestDifference(showId, model); + model.Episodes = diff.ToList(); + } + else + { + if (plexSettings.EnableTvEpisodeSearching) + { + foreach (var s in showInfo.Season) + { + var result = PlexChecker.IsEpisodeAvailable(showId.ToString(), s.SeasonNumber, + s.EpisodeNumber); + if (result) + { + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = $"{fullShowName} {string.Format(Resources.UI.Search_AlreadyInPlex,GetMediaServerName())}" + }); + } + } + } + else if (PlexChecker.IsTvShowAvailable(shows.ToArray(), showInfo.name, + showInfo.premiered?.Substring(0, 4), + providerId, model.SeasonList)) + { + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = $"{fullShowName} {string.Format(Resources.UI.Search_AlreadyInPlex,GetMediaServerName())}" + }); + } + } + } + if (embySettings.Enable) + { + var embyContent = EmbyContentRepository.GetAll(); + var embyMovies = EmbyChecker.GetEmbyTvShows(embyContent); + var providerId = showId.ToString(); + if (episodeRequest) + { + var cachedEpisodesTask = await EmbyChecker.GetEpisodes(); + var cachedEpisodes = cachedEpisodesTask.ToList(); + foreach (var d in difference) // difference is from an existing request + { + if ( + cachedEpisodes.Any( + x => + x.SeasonNumber == d.SeasonNumber && x.EpisodeNumber == d.EpisodeNumber && + x.ProviderId == providerId)) + { + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = + $"{fullShowName} {d.SeasonNumber} - {d.EpisodeNumber} {string.Format(Resources.UI.Search_AlreadyInPlex,GetMediaServerName())}" + }); + } + } + + var diff = await GetEpisodeRequestDifference(showId, model); + model.Episodes = diff.ToList(); + } + else + { + if (embySettings.EnableEpisodeSearching) + { + foreach (var s in showInfo.Season) + { + var result = EmbyChecker.IsEpisodeAvailable(showId.ToString(), s.SeasonNumber, + s.EpisodeNumber); + if (result) + { + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = $"{fullShowName} is already in Emby!" + }); + } + } + } + else if (EmbyChecker.IsTvShowAvailable(embyMovies.ToArray(), showInfo.name, + showInfo.premiered?.Substring(0, 4), + providerId, model.SeasonList)) + { + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = $"{fullShowName} is already in Emby!" + }); + } + } + } + } + catch (Exception) + { + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = string.Format(Resources.UI.Search_CouldNotCheckPlex, fullShowName,GetMediaServerName()) + }); + } + + if (showInfo.externals?.thetvdb == null) + { + await FaultQueue.QueueItemAsync(model, showInfo.id.ToString(), RequestType.TvShow, FaultType.MissingInformation, "We do not have a TheTVDBId from TVMaze"); + await NotificationService.Publish(new NotificationModel + { + DateTime = DateTime.Now, + User = Username, + RequestType = RequestType.TvShow, + Title = model.Title, + NotificationType = NotificationType.ItemAddedToFaultQueue + }); + return Response.AsJson(new JsonResponseModel + { + Result = true, + Message = $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}" + }); + } + + model.ProviderId = showInfo.externals?.thetvdb ?? 0; + + try + { + if (ShouldAutoApprove(RequestType.TvShow)) + { + return await SendTv(model, sonarrSettings, existingRequest, fullShowName, settings); + } + return await AddRequest(model, settings, $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"); + } + catch (Exception e) + { + await FaultQueue.QueueItemAsync(model, showInfo.id.ToString(), RequestType.TvShow, FaultType.RequestFault, e.Message); + await NotificationService.Publish(new NotificationModel + { + DateTime = DateTime.Now, + User = Username, + RequestType = RequestType.TvShow, + Title = model.Title, + NotificationType = NotificationType.ItemAddedToFaultQueue + }); + Log.Error(e); + return + Response.AsJson(new JsonResponseModel + { + Result = true, + Message = $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}" + }); + } + } + + private async Task AddUserToRequest(RequestedModel existingRequest, PlexRequestSettings settings, + string fullShowName, bool episodeReq = false) + { + // check if the current user is already marked as a requester for this show, if not, add them + if (!existingRequest.UserHasRequested(Username)) + { + existingRequest.RequestedUsers.Add(Username); + } + if (Security.HasPermissions(User, Permissions.UsersCanViewOnlyOwnRequests) || episodeReq) + { + return + await + UpdateRequest(existingRequest, settings, + $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"); + } + + return + await UpdateRequest(existingRequest, settings, $"{fullShowName} {Resources.UI.Search_AlreadyRequested}"); + } + + private bool ShouldSendNotification(RequestType type, PlexRequestSettings prSettings) + { + var sendNotification = ShouldAutoApprove(type) + ? !prSettings.IgnoreNotifyForAutoApprovedRequests + : true; + + if (IsAdmin) + { + sendNotification = false; // Don't bother sending a notification if the user is an admin + + } + return sendNotification; + } + + + private async Task RequestAlbum(string releaseId) + { + if (Security.HasPermissions(User, Permissions.ReadOnlyUser) || !Security.HasPermissions(User, Permissions.RequestMusic)) + { + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = "Sorry, you do not have the correct permissions to request music!" + }); + } + + var settings = await PrService.GetSettingsAsync(); + if (!await CheckRequestLimit(settings, RequestType.Album)) + { + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = Resources.UI.Search_WeeklyRequestLimitAlbums + }); + } + Analytics.TrackEventAsync(Category.Search, Action.Request, "Album", Username, + CookieHelper.GetAnalyticClientId(Cookies)); + var existingRequest = await RequestService.CheckRequestAsync(releaseId); + + if (existingRequest != null) + { + if (!existingRequest.UserHasRequested(Username)) + { + existingRequest.RequestedUsers.Add(Username); + await RequestService.UpdateRequestAsync(existingRequest); + } + return + Response.AsJson(new JsonResponseModel + { + Result = true, + Message = + Security.HasPermissions(User, Permissions.UsersCanViewOnlyOwnRequests) + ? $"{existingRequest.Title} {Resources.UI.Search_SuccessfullyAdded}" + : $"{existingRequest.Title} {Resources.UI.Search_AlreadyRequested}" + }); + } + + var albumInfo = MusicBrainzApi.GetAlbum(releaseId); + DateTime release; + DateTimeHelper.CustomParse(albumInfo.ReleaseEvents?.FirstOrDefault()?.date, out release); + + var artist = albumInfo.ArtistCredits?.FirstOrDefault()?.artist; + if (artist == null) + { + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = Resources.UI.Search_MusicBrainzError + }); + } + + + var content = PlexContentRepository.GetAll(); + var albums = PlexChecker.GetPlexAlbums(content); + var alreadyInPlex = PlexChecker.IsAlbumAvailable(albums.ToArray(), albumInfo.title, release.ToString("yyyy"), + artist.name); + + if (alreadyInPlex) + { + return Response.AsJson(new JsonResponseModel + { + Result = false, + Message = $"{albumInfo.title} {Resources.UI.Search_AlreadyInPlex}" + }); + } + + var img = GetMusicBrainzCoverArt(albumInfo.id); + + var model = new RequestedModel + { + Title = albumInfo.title, + MusicBrainzId = albumInfo.id, + Overview = albumInfo.disambiguation, + PosterPath = img, + Type = RequestType.Album, + ProviderId = 0, + RequestedUsers = new List { Username }, + Status = albumInfo.status, + Issues = IssueState.None, + RequestedDate = DateTime.UtcNow, + ReleaseDate = release, + ArtistName = artist.name, + ArtistId = artist.id + }; + + try + { + if (ShouldAutoApprove(RequestType.Album)) + { + model.Approved = true; + var hpSettings = HeadphonesService.GetSettings(); + + if (!hpSettings.Enabled) + { + await RequestService.AddRequestAsync(model); + return + Response.AsJson(new JsonResponseModel + { + Result = true, + Message = $"{model.Title} {Resources.UI.Search_SuccessfullyAdded}" + }); + } + + var sender = new HeadphonesSender(HeadphonesApi, hpSettings, RequestService); + await sender.AddAlbum(model); + return await AddRequest(model, settings, $"{model.Title} {Resources.UI.Search_SuccessfullyAdded}"); + } + + return await AddRequest(model, settings, $"{model.Title} {Resources.UI.Search_SuccessfullyAdded}"); + } + catch (Exception e) + { + Log.Error(e); + await FaultQueue.QueueItemAsync(model, albumInfo.id, RequestType.Album, FaultType.RequestFault, e.Message); + + await NotificationService.Publish(new NotificationModel + { + DateTime = DateTime.Now, + User = Username, + RequestType = RequestType.Album, + Title = model.Title, + NotificationType = NotificationType.ItemAddedToFaultQueue + }); + throw; + } + } + + private string GetMusicBrainzCoverArt(string id) + { + var coverArt = MusicBrainzApi.GetCoverArt(id); + var firstImage = coverArt?.images?.FirstOrDefault(); + var img = string.Empty; + + if (firstImage != null) + { + img = firstImage.thumbnails?.small ?? firstImage.image; + } + + return img; + } + + private Response GetSeasons() + { + var seriesId = (int)Request.Query.tvId; + var show = TvApi.ShowLookupByTheTvDbId(seriesId); + var seasons = TvApi.GetSeasons(show.id); + var model = seasons.Select(x => x.number); + return Response.AsJson(model); + } + + private async Task GetEpisodes() + { + var seriesId = (int)Request.Query.tvId; + var model = await GetEpisodes(seriesId); + + return Response.AsJson(model); + } + + private async Task> GetEpisodes(int providerId) + { + var s = await SonarrService.GetSettingsAsync(); + var sonarrEnabled = s.Enabled; + var allResults = await RequestService.GetAllAsync(); + + var seriesTask = Task.Run( + () => + { + if (sonarrEnabled) + { + var allSeries = SonarrApi.GetSeries(s.ApiKey, s.FullUri); + var selectedSeries = allSeries.FirstOrDefault(x => x.tvdbId == providerId) ?? new Series(); + return selectedSeries; + } + return new Series(); + }); + + var model = new List(); + + var requests = allResults as RequestedModel[] ?? allResults.ToArray(); + + var existingRequest = requests.FirstOrDefault(x => x.Type == RequestType.TvShow && x.TvDbId == providerId.ToString()); + var show = await Task.Run(() => TvApi.ShowLookupByTheTvDbId(providerId)); + var tvMazeEpisodesTask = await Task.Run(() => TvApi.EpisodeLookup(show.id)); + var tvMazeEpisodes = tvMazeEpisodesTask.ToList(); + + var sonarrEpisodes = new List(); + if (sonarrEnabled) + { + var sonarrSeries = await seriesTask; + var sonarrEp = SonarrApi.GetEpisodes(sonarrSeries.id.ToString(), s.ApiKey, s.FullUri); + sonarrEpisodes = sonarrEp?.ToList() ?? new List(); + } + + var plexSettings = await PlexService.GetSettingsAsync(); + if (plexSettings.Enable) + { + var plexCacheTask = await PlexChecker.GetEpisodes(providerId); + var plexCache = plexCacheTask.ToList(); + foreach (var ep in tvMazeEpisodes) + { + var requested = existingRequest?.Episodes + .Any(episodesModel => + ep.number == episodesModel.EpisodeNumber && + ep.season == episodesModel.SeasonNumber) ?? false; + + var alreadyInPlex = plexCache.Any(x => x.EpisodeNumber == ep.number && x.SeasonNumber == ep.season); + var inSonarr = + sonarrEpisodes.Any(x => x.seasonNumber == ep.season && x.episodeNumber == ep.number && x.hasFile); + + model.Add(new EpisodeListViewModel + { + Id = show.id, + SeasonNumber = ep.season, + EpisodeNumber = ep.number, + Requested = requested || alreadyInPlex || inSonarr, + Name = ep.name, + EpisodeId = ep.id + }); + } + } + var embySettings = await EmbySettings.GetSettingsAsync(); + if (embySettings.Enable) + { + var embyCacheTask = await EmbyChecker.GetEpisodes(providerId); + var cache = embyCacheTask.ToList(); + foreach (var ep in tvMazeEpisodes) + { + var requested = existingRequest?.Episodes + .Any(episodesModel => + ep.number == episodesModel.EpisodeNumber && + ep.season == episodesModel.SeasonNumber) ?? false; + + var alreadyInEmby = cache.Any(x => x.EpisodeNumber == ep.number && x.SeasonNumber == ep.season); + var inSonarr = + sonarrEpisodes.Any(x => x.seasonNumber == ep.season && x.episodeNumber == ep.number && x.hasFile); + + model.Add(new EpisodeListViewModel + { + Id = show.id, + SeasonNumber = ep.season, + EpisodeNumber = ep.number, + Requested = requested || alreadyInEmby || inSonarr, + Name = ep.name, + EpisodeId = ep.id + }); + } + } + return model; + + } + + public async Task CheckRequestLimit(PlexRequestSettings s, RequestType type) + { + if (IsAdmin) + return true; + + if (Security.HasPermissions(User, Permissions.BypassRequestLimit)) + return true; + + var requestLimit = GetRequestLimitForType(type, s); + if (requestLimit == 0) + { + return true; + } + + var limit = await RequestLimitRepo.GetAllAsync(); + var usersLimit = limit.FirstOrDefault(x => x.Username == Username && x.RequestType == type); + if (usersLimit == null) + { + // Have not set a requestLimit yet + return true; + } + + return requestLimit > usersLimit.RequestCount; + } + + private int GetRequestLimitForType(RequestType type, PlexRequestSettings s) + { + int requestLimit; + switch (type) + { + case RequestType.Movie: + requestLimit = s.MovieWeeklyRequestLimit; + break; + case RequestType.TvShow: + requestLimit = s.TvWeeklyRequestLimit; + break; + case RequestType.Album: + requestLimit = s.AlbumWeeklyRequestLimit; + break; + default: + throw new ArgumentOutOfRangeException(nameof(type), type, null); + } + return requestLimit; + } + + private async Task AddRequest(RequestedModel model, PlexRequestSettings settings, string message) + { + await RequestService.AddRequestAsync(model); + + if (ShouldSendNotification(model.Type, settings)) + { + var notificationModel = new NotificationModel + { + Title = model.Title, + User = Username, + DateTime = DateTime.Now, + NotificationType = NotificationType.NewRequest, + RequestType = model.Type, + ImgSrc = model.Type == RequestType.Movie ? $"https://image.tmdb.org/t/p/w300/{model.PosterPath}" : model.PosterPath + }; + await NotificationService.Publish(notificationModel); + } + + var limit = await RequestLimitRepo.GetAllAsync(); + var usersLimit = limit.FirstOrDefault(x => x.Username == Username && x.RequestType == model.Type); + if (usersLimit == null) + { + await RequestLimitRepo.InsertAsync(new RequestLimit + { + Username = Username, + RequestType = model.Type, + FirstRequestDate = DateTime.UtcNow, + RequestCount = 1 + }); + } + else + { + usersLimit.RequestCount++; + await RequestLimitRepo.UpdateAsync(usersLimit); + } + + return Response.AsJson(new JsonResponseModel { Result = true, Message = message }); + } + + private async Task UpdateRequest(RequestedModel model, PlexRequestSettings settings, string message) + { + await RequestService.UpdateRequestAsync(model); + + if (ShouldSendNotification(model.Type, settings)) + { + var notificationModel = new NotificationModel + { + Title = model.Title, + User = Username, + DateTime = DateTime.Now, + NotificationType = NotificationType.NewRequest, + RequestType = model.Type, + ImgSrc = model.Type == RequestType.Movie ? $"https://image.tmdb.org/t/p/w300/{model.PosterPath}" : model.PosterPath + }; + await NotificationService.Publish(notificationModel); + } + + var limit = await RequestLimitRepo.GetAllAsync(); + var usersLimit = limit.FirstOrDefault(x => x.Username == Username && x.RequestType == model.Type); + if (usersLimit == null) + { + await RequestLimitRepo.InsertAsync(new RequestLimit + { + Username = Username, + RequestType = model.Type, + FirstRequestDate = DateTime.UtcNow, + RequestCount = 1 + }); + } + else + { + usersLimit.RequestCount++; + await RequestLimitRepo.UpdateAsync(usersLimit); + } + + return Response.AsJson(new JsonResponseModel { Result = true, Message = message }); + } + + private IEnumerable GetListDifferences(IEnumerable existing, IEnumerable request) + { + var newRequest = request + .Select(r => + new EpisodesModel + { + SeasonNumber = r.SeasonNumber, + EpisodeNumber = r.EpisodeNumber + }).ToList(); + + return newRequest.Except(existing); + } + + private async Task> GetEpisodeRequestDifference(int showId, RequestedModel model) + { + var episodes = await GetEpisodes(showId); + var availableEpisodes = episodes.Where(x => x.Requested).ToList(); + var available = availableEpisodes.Select(a => new EpisodesModel { EpisodeNumber = a.EpisodeNumber, SeasonNumber = a.SeasonNumber }).ToList(); + + var diff = model.Episodes.Except(available); + return diff; + } + + public bool ShouldAutoApprove(RequestType requestType) + { + var admin = Security.HasPermissions(Context.CurrentUser, Permissions.Administrator); + // if the user is an admin, they go ahead and allow auto-approval + if (admin) return true; + + // check by request type if the category requires approval or not + switch (requestType) + { + case RequestType.Movie: + return Security.HasPermissions(User, Permissions.AutoApproveMovie); + case RequestType.TvShow: + return Security.HasPermissions(User, Permissions.AutoApproveTv); + case RequestType.Album: + return Security.HasPermissions(User, Permissions.AutoApproveAlbum); + default: + return false; + } + } + + private enum ShowSearchType + { + Popular, + Anticipated, + MostWatched, + Trending + } + + private async Task SendTv(RequestedModel model, Task sonarrSettings, RequestedModel existingRequest, string fullShowName, PlexRequestSettings settings) + { + model.Approved = true; + var s = await sonarrSettings; + var sender = new TvSenderOld(SonarrApi, SickrageApi, Cache); // TODO put back + if (s.Enabled) + { + var result = await sender.SendToSonarr(s, model); + if (!string.IsNullOrEmpty(result?.title)) + { + if (existingRequest != null) + { + return await UpdateRequest(model, settings, + $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"); + } + return + await + AddRequest(model, settings, + $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"); + } + Log.Debug("Error with sending to sonarr."); + return + Response.AsJson(ValidationHelper.SendSonarrError(result?.ErrorMessages ?? new List())); + } + + var srSettings = SickRageService.GetSettings(); + if (srSettings.Enabled) + { + var result = sender.SendToSickRage(srSettings, model); + if (result?.result == "success") + { + return await AddRequest(model, settings, + $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"); + } + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = result?.message ?? Resources.UI.Search_SickrageError + }); + } + + if (!srSettings.Enabled && !s.Enabled) + { + return await AddRequest(model, settings, $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"); + } + + return + Response.AsJson(new JsonResponseModel { Result = false, Message = Resources.UI.Search_TvNotSetUp }); + } + + private string GetMediaServerName() + { + var e = EmbySettings.GetSettings(); + return e.Enable ? "Emby" : "Plex"; + } + } } diff --git a/Ombi.UI/Resources/UI.da.resx b/Ombi.UI/Resources/UI.da.resx index bb2efd6bf..e4164aeed 100644 --- a/Ombi.UI/Resources/UI.da.resx +++ b/Ombi.UI/Resources/UI.da.resx @@ -121,13 +121,13 @@ Log ind - Ønsker du at se en film eller tv-show, men det er i øjeblikket ikke på Plex? Log nedenfor med dit Plex.tv brugernavn og password !! + Ønsker du at se en film eller tv-show, men det er i øjeblikket ikke på {0}? Log nedenfor med dit brugernavn og password !! Dine login-oplysninger bruges kun til at godkende din Plex konto. - Plex.tv Brugernavn + Brugernavn Brugernavn @@ -211,7 +211,7 @@ Album - Ønsker at se noget, der ikke i øjeblikket på Plex ?! Intet problem! Bare søge efter det nedenfor og anmode den ! + Ønsker at se noget, der ikke i øjeblikket på {0}?! Intet problem! Bare søge efter det nedenfor og anmode den ! Søg @@ -409,7 +409,7 @@ allerede er blevet anmodet !! - Vi kunne ikke kontrollere, om {0} er i Plex, er du sikker på det er korrekt setup ?! + Vi kunne ikke kontrollere, om {0} er i {1}, er du sikker på det er korrekt setup ?! Noget gik galt tilføjer filmen til CouchPotato! Tjek venligst din opsætning.! @@ -418,7 +418,7 @@ Du har nået din ugentlige anmodning grænse for film! Kontakt din administrator.! - er allerede i Plex !! + er allerede i {0}!! Noget gik galt tilføjer filmen til SickRage! Tjek venligst din opsætning.! @@ -435,9 +435,6 @@ Du har nået din ugentlige anmodning grænse for tv-shows! Kontakt din administrator.! - - Beklager, men denne funktionalitet er i øjeblikket kun for brugere med Plex konti! - Beklager, men din administrator har endnu ikke gjort det muligt denne funktionalitet.! diff --git a/Ombi.UI/Resources/UI.de.resx b/Ombi.UI/Resources/UI.de.resx index 1cd1d0192..5b908f1b8 100644 --- a/Ombi.UI/Resources/UI.de.resx +++ b/Ombi.UI/Resources/UI.de.resx @@ -121,13 +121,13 @@ Anmelden - Möchten Sie einen Film oder eine Serie schauen, die momentan noch nicht auf Plex ist? Dann loggen Sie sich unten ein und fordern Sie das Material an! + Möchten Sie einen Film oder eine Serie schauen, die momentan noch nicht auf {0}ist? Dann loggen Sie sich unten ein und fordern Sie das Material an! Ihre Login-Daten werden nur zur Authorisierung Ihres Plex-Konto verwendet. - Plex.tv Benutzername + Benutzername Benutzername @@ -211,7 +211,7 @@ Alben - Möchten Sie etwas schauen, das derzeit nicht auf Plex ist?! Kein Problem! Suchen Sie unten einfach danach und fragen Sie es an! + Möchten Sie etwas schauen, das derzeit nicht auf {0} ist?! Kein Problem! Suchen Sie unten einfach danach und fragen Sie es an! Suche @@ -409,7 +409,7 @@ wurde bereits angefragt! - Wir konnten nicht prüfen ob {0} bereits auf Plex ist. Bist du sicher dass alles richtig installiert ist? + Wir konnten nicht prüfen ob {0} bereits auf {1}ist. Bist du sicher dass alles richtig installiert ist? Etwas ging etwas schief beim hinzufügen des Filmes zu CouchPotato! Bitte überprüfe deine Einstellungen. @@ -418,7 +418,7 @@ Du hast deine wöchentliche Maximalanfragen für neue Filme erreicht. Bitte kontaktiere den Administrator. - ist bereits auf Plex! + ist bereits auf {0}! Etwas ging etwas schief beim hinzufügen des Filmes zu SickRage! Bitte überprüfe deine Einstellungen. @@ -435,9 +435,6 @@ Du hast deine wöchentliche Maximalanfragen für neue Serien erreicht. Bitte kontaktiere den Administrator. - - Entschuldige, aber diese Funktion ist momentan nur für Benutzer mit Plex-Accounts freigeschaltet. - Entschuldige, aber dein Administrator hat diese Funktion noch nicht freigeschaltet. diff --git a/Ombi.UI/Resources/UI.es.resx b/Ombi.UI/Resources/UI.es.resx index dd325f2bb..c00323af5 100644 --- a/Ombi.UI/Resources/UI.es.resx +++ b/Ombi.UI/Resources/UI.es.resx @@ -121,13 +121,13 @@ INICIAR SESIÓN - ¿Quieres ver una película o programa de televisión, pero no es actualmente en Plex? Ingresa abajo con su nombre de usuario y contraseña Plex.tv ! + ¿Quieres ver una película o programa de televisión, pero no es actualmente en {0}? Ingresa abajo con su nombre de usuario y contraseña ! Sus datos de acceso sólo se utilizan para autenticar su cuenta Plex. - Plex.tv nombre de usuario + nombre de usuario Username @@ -211,7 +211,7 @@ Álbumes - ¿Quieres ver algo que no se encuentra actualmente en Plex ?! ¡No hay problema! Sólo la búsqueda de abajo y que solicitarlo ! + ¿Quieres ver algo que no se encuentra actualmente en {0}?! ¡No hay problema! Sólo la búsqueda de abajo y que solicitarlo ! Buscar @@ -409,7 +409,7 @@ ya ha sido solicitada !! - No hemos podido comprobar si {0} está en Plex, ¿estás seguro de que es correcta la configuración ?! + No hemos podido comprobar si {0} está en {1}, ¿estás seguro de que es correcta la configuración ?! Algo salió mal la adición de la película para CouchPotato! Por favor verifica la configuracion.! @@ -418,7 +418,7 @@ Ha llegado a su límite de solicitudes semanales de películas! Por favor, póngase en contacto con su administrador.! - ya está en Plex !! + ya está en {0}!! Algo salió mal la adición de la película para SickRage! Por favor verifica la configuracion.! @@ -435,9 +435,6 @@ Ha llegado a su límite de solicitudes semanales de programas de televisión! Por favor, póngase en contacto con su administrador.! - - Lo sentimos, pero esta funcionalidad es actualmente sólo para los usuarios con cuentas Plex! - Lo sentimos, pero el administrador aún no ha habilitado esta funcionalidad.! diff --git a/Ombi.UI/Resources/UI.fr.resx b/Ombi.UI/Resources/UI.fr.resx index 11f787a36..e9a461b23 100644 --- a/Ombi.UI/Resources/UI.fr.resx +++ b/Ombi.UI/Resources/UI.fr.resx @@ -121,13 +121,13 @@ Connexion - Vous souhaitez avoir accès à un contenu qui n'est pas encore disponible dans Plex ? Demandez-le ici ! + Vous souhaitez avoir accès à un contenu qui n'est pas encore disponible dans {0}? Demandez-le ici ! Vos informations de connexion sont uniquement utilisées pour authentifier votre compte Plex. - Nom d'utilisateur Plex.tv + Nom d'utilisateur Nom d’utilisateur @@ -211,7 +211,7 @@ Albums - Vous souhaitez avoir accès à un contenu qui n'est pas encore disponible dans Plex ?! Aucun problème ! Il suffit d'effectuer une recherche ci-dessous et d'en faire la demande! + Vous souhaitez avoir accès à un contenu qui n'est pas encore disponible dans {0} ?! Aucun problème ! Il suffit d'effectuer une recherche ci-dessous et d'en faire la demande! Rechercher @@ -409,7 +409,7 @@ a déjà été demandé! - Nous ne pouvons pas vérifier que {0} est présent dans Plex, êtes-vous sûr que la configuration est correcte? + Nous ne pouvons pas vérifier que {0} est présent dans {1}, êtes-vous sûr que la configuration est correcte? Une erreur s'est produite lors de l'ajout du film dans CouchPotato! Merci de bien vouloir vérifier vos paramètres. @@ -418,7 +418,7 @@ Vous avez atteint votre quota hebdomadaire de demandes pour les films! Merci de bien vouloir contacter l'administrateur. - est déjà présent dans Plex! + est déjà présent dans {0}! Une erreur s'est produite lors de l'ajout de la série TV dans SickRage! Merci de bien vouloir vérifier vos paramètres. @@ -435,9 +435,6 @@ Vous avez atteint votre quota hebdomadaire de demandes pour les séries TV! Merci de bien vouloir contacter l'administrateur. - - Désolé mais cette fonctionnalité est réservée aux utilisateurs possédant un compte Plex. - Désolé mais l'administrateur n'a pas encore activé cette fonctionnalité. diff --git a/Ombi.UI/Resources/UI.it.resx b/Ombi.UI/Resources/UI.it.resx index 3dd1640c4..3613c3356 100644 --- a/Ombi.UI/Resources/UI.it.resx +++ b/Ombi.UI/Resources/UI.it.resx @@ -121,13 +121,13 @@ Accesso - Vuoi guardare un film o una serie tv ma non è attualmente in Plex? Effettua il login con il tuo username e la password Plex.tv ! + Vuoi guardare un film o una serie tv ma non è attualmente in {0}? Effettua il login con il tuo username e la password ! I dati di accesso vengono utilizzati solo per autenticare l'account Plex. - Plex.tv Nome utente + Nome utente Nome utente @@ -214,7 +214,7 @@ Msuica - Vuoi guardare qualcosa che non è attualmente in Plex?! Non c'è problema! Basta cercarla qui sotto e richiederla! + Vuoi guardare qualcosa che non è attualmente in {0}?! Non c'è problema! Basta cercarla qui sotto e richiederla! Suggerimenti @@ -409,7 +409,7 @@ è già stato richiesto! - Non siamo riusciti a controllare se {0} è in Plex, sei sicuro che sia configurato correttamente? + Non siamo riusciti a controllare se {0} è in {1}, sei sicuro che sia configurato correttamente? Qualcosa è andato storto aggiungendo il film a CouchPotato! Controlla le impostazioni @@ -418,7 +418,7 @@ Hai raggiunto il numero massimo di richieste settimanali per i Film! Contatta l'amministratore - è già disponibile in Plex! + è già disponibile in {0}! Qualcosa è andato storto aggiungendo il film a SickRage! Controlla le impostazioni @@ -435,9 +435,6 @@ Hai raggiunto il numero massimo di richieste settimanali per le Serie TV! Contatta l'amministratore - - Spiacente, ma questa funzione è disponibile solo per gli utenti con un account Plex. - Spiacente, ma l'amministratore non ha ancora abilitato questa funzionalità. diff --git a/Ombi.UI/Resources/UI.nl.resx b/Ombi.UI/Resources/UI.nl.resx index 0b85e2aa9..3761a96cc 100644 --- a/Ombi.UI/Resources/UI.nl.resx +++ b/Ombi.UI/Resources/UI.nl.resx @@ -121,13 +121,13 @@ Inloggen - Wilt u een film of een tv serie kijken, maar staat deze niet op Plex? Log hieronder in met uw gebruikersnaam en wachtwoord van Plex.tv + Wilt u een film of een tv serie kijken, maar staat deze niet op {0}? Log hieronder in met uw gebruikersnaam en wachtwoord van Uw login gegevens worden alleen gebruikt om uw account te verifiëren bij Plex. - Plex.tv Gebruikersnaam + Gebruikersnaam Gebruikersnaam @@ -217,7 +217,7 @@ Albums - Wilt u kijken naar iets dat dat momenteel niet op Plex is?! Geen probleem! zoek hieronder en vraag het aan! + Wilt u kijken naar iets dat dat momenteel niet op {0} is?! Geen probleem! zoek hieronder en vraag het aan! Suggesties @@ -409,10 +409,7 @@ Is al aangevraagd! - Staat al op Plex! - - - Sorry, deze functie is momenteel alleen voor gebruikers met een Plex account. + Staat al op {0}! Sorry, uw administrator heeft deze functie nog niet geactiveerd. @@ -424,7 +421,7 @@ Kon niet opslaan, probeer het later nog eens. - We konden niet controleren of {0} al in plex bestaat, weet je zeker dat het correct is ingesteld? + We konden niet controleren of {0} al in {1} bestaat, weet je zeker dat het correct is ingesteld? Er is iets foutgegaan tijdens het toevoegen van de film aan CouchPotato! Controleer je instellingen diff --git a/Ombi.UI/Resources/UI.pt.resx b/Ombi.UI/Resources/UI.pt.resx index b265ea445..681a11549 100644 --- a/Ombi.UI/Resources/UI.pt.resx +++ b/Ombi.UI/Resources/UI.pt.resx @@ -121,13 +121,13 @@ Entrar - Quer assistir a um filme ou programa de TV, mas não está atualmente em Plex? Entre abaixo com seu nome de usuário e senha Plex.tv !! + Quer assistir a um filme ou programa de TV, mas não está atualmente em {0}? Entre abaixo com seu nome de usuário e senha ! Seus dados de login são apenas usados ​​para autenticar sua conta Plex.! - Plex.tv usuário + usuário Nome de usuário @@ -211,7 +211,7 @@ Álbuns - Quer assistir algo que não está atualmente em Plex ?! Sem problemas! Basta procurá-lo abaixo e solicitá-lo !! + Quer assistir algo que não está atualmente em {0}?! Sem problemas! Basta procurá-lo abaixo e solicitá-lo !! Buscar @@ -409,7 +409,7 @@ já foi solicitado !! - Nós não poderia verificar se {0} está em Plex, você tem certeza que é configurada corretamente ?! + Nós não poderia verificar se {0} está em {1}, você tem certeza que é configurada corretamente ?! Algo deu errado adicionando o filme para CouchPotato! Verifique as suas opções.! @@ -418,7 +418,7 @@ Atingiu seu limite semanal de solicitação para filmes! Entre em contato com seu administrador. - Já está no Plex! + Já está no {0}! Algo deu errado adicionar o filme para SickRage! Por favor, verifique suas configurações. @@ -435,9 +435,6 @@ Atingiu seu limite semanal de solicitação para programas de TV! Entre em contato com seu administrador. - - Desculpe, mas essa funcionalidade é atualmente somente para os usuários com contas de Plex - Desculpe, mas o administrador não permitiu ainda esta funcionalidade. diff --git a/Ombi.UI/Resources/UI.resx b/Ombi.UI/Resources/UI.resx index d24c5ea1c..a9f63e0e8 100644 --- a/Ombi.UI/Resources/UI.resx +++ b/Ombi.UI/Resources/UI.resx @@ -101,14 +101,14 @@ Login - Want to watch a movie or tv show but it's not currently on Plex? - Login below with your Plex.tv username and password! + Want to watch a movie or tv show but it's not currently on {0}? + Login below with your username and password! Your login details are only used to authenticate your Plex account. - Plex.tv Username + Username Username @@ -192,7 +192,7 @@ Albums - Want to watch something that is not currently on Plex?! No problem! Just search for it below and request it! + Want to watch something that is not currently on {0}?! No problem! Just search for it below and request it! Search @@ -390,7 +390,7 @@ has already been requested! - We could not check if {0} is in Plex, are you sure it's correctly setup? + We could not check if {0} is in {1}, are you sure it's correctly setup? Something went wrong adding the movie to CouchPotato! Please check your settings. @@ -399,7 +399,7 @@ You have reached your weekly request limit for Movies! Please contact your admin. - is already in Plex! + is already in {0}! Something went wrong adding the movie to SickRage! Please check your settings. @@ -416,9 +416,6 @@ You have reached your weekly request limit for TV Shows! Please contact your admin. - - Sorry, but this functionality is currently only for users with Plex accounts - Sorry, but your administrator has not yet enabled this functionality. @@ -468,7 +465,7 @@ TV show status - Currently we are indexing all of the available tv shows and movies on the Plex server, so there might be some unexpected behavior. This shouldn't take too long. + Currently we are indexing all of the available tv shows and movies on the media server, so there might be some unexpected behavior. This shouldn't take too long. User Management diff --git a/Ombi.UI/Resources/UI.sv.resx b/Ombi.UI/Resources/UI.sv.resx index 4c70109c7..3d21b5df0 100644 --- a/Ombi.UI/Resources/UI.sv.resx +++ b/Ombi.UI/Resources/UI.sv.resx @@ -121,13 +121,13 @@ Logga in - Vill du titta på en film eller TV-show, men det är inte närvarande på Plex? Logga in nedan med Plex.tv användarnamn och lösenord !! + Vill du titta på en film eller TV-show, men det är inte närvarande på {0}? Logga in nedan med användarnamn och lösenord !! Dina inloggningsuppgifter används endast för att autentisera ditt Plex-konto. - Plex.tv användarnamn + Användarnamn Användarnamn @@ -214,7 +214,7 @@ Album - Vill titta på något som inte är närvarande på Plex ?! Inga problem! Bara söka efter den nedan och begär det ! + Vill titta på något som inte är närvarande på {0}?! Inga problem! Bara söka efter den nedan och begär det ! Sök @@ -409,7 +409,7 @@ har redan begärts - Vi kunde inte kontrollera om {0} är i Plex, är du säker det är korrekt installation? + Vi kunde inte kontrollera om {0} är i {1}, är du säker det är korrekt installation? Något gick fel att lägga till filmen i CouchPotato! Kontrollera inställningarna. @@ -418,7 +418,7 @@ Du har nått din weekly begäran gräns för filmer! Kontakta din admin. - är redan i Plex + är redan i {0} Något gick fel att lägga till filmen i SickRage! Kontrollera inställningarna. @@ -435,9 +435,6 @@ Du har nått din weekly begäran gräns för TV-program! Kontakta din admin. - - Ledsen, men denna funktion är för närvarande endast för användare med Plex konton - Ledsen, men administratören har ännu inte aktiverat denna funktion. diff --git a/Ombi.UI/Resources/UI1.Designer.cs b/Ombi.UI/Resources/UI1.Designer.cs index 56013a95f..4920f2e7f 100644 --- a/Ombi.UI/Resources/UI1.Designer.cs +++ b/Ombi.UI/Resources/UI1.Designer.cs @@ -223,7 +223,7 @@ namespace Ombi.UI.Resources { } /// - /// Looks up a localized string similar to Currently we are indexing all of the available tv shows and movies on the Plex server, so there might be some unexpected behavior. This shouldn't take too long.. + /// Looks up a localized string similar to Currently we are indexing all of the available tv shows and movies on the media server, so there might be some unexpected behavior. This shouldn't take too long.. /// public static string Layout_CacherRunning { get { @@ -736,7 +736,7 @@ namespace Ombi.UI.Resources { } /// - /// Looks up a localized string similar to is already in Plex!. + /// Looks up a localized string similar to is already in {0}!. /// public static string Search_AlreadyInPlex { get { @@ -790,7 +790,7 @@ namespace Ombi.UI.Resources { } /// - /// Looks up a localized string similar to We could not check if {0} is in Plex, are you sure it's correctly setup?. + /// Looks up a localized string similar to We could not check if {0} is in {1}, are you sure it's correctly setup?. /// public static string Search_CouldNotCheckPlex { get { @@ -816,15 +816,6 @@ namespace Ombi.UI.Resources { } } - /// - /// Looks up a localized string similar to Sorry, but this functionality is currently only for users with Plex accounts. - /// - public static string Search_ErrorPlexAccountOnly { - get { - return ResourceManager.GetString("Search_ErrorPlexAccountOnly", resourceCulture); - } - } - /// /// Looks up a localized string similar to First Season. /// @@ -907,7 +898,7 @@ namespace Ombi.UI.Resources { } /// - /// Looks up a localized string similar to Want to watch something that is not currently on Plex?! No problem! Just search for it below and request it!. + /// Looks up a localized string similar to Want to watch something that is not currently on {0}?! No problem! Just search for it below and request it!. /// public static string Search_Paragraph { get { @@ -1132,8 +1123,8 @@ namespace Ombi.UI.Resources { } /// - /// Looks up a localized string similar to Want to watch a movie or tv show but it's not currently on Plex? - /// Login below with your Plex.tv username and password!. + /// Looks up a localized string similar to Want to watch a movie or tv show but it's not currently on {0}? + /// Login below with your username and password!. /// public static string UserLogin_Paragraph { get { @@ -1178,7 +1169,7 @@ namespace Ombi.UI.Resources { } /// - /// Looks up a localized string similar to Plex.tv Username . + /// Looks up a localized string similar to Username . /// public static string UserLogin_Username { get { diff --git a/Ombi.UI/Views/Admin/LandingPage.cshtml b/Ombi.UI/Views/Admin/LandingPage.cshtml index 20ad85c53..e48f5ca92 100644 --- a/Ombi.UI/Views/Admin/LandingPage.cshtml +++ b/Ombi.UI/Views/Admin/LandingPage.cshtml @@ -54,7 +54,7 @@

    Notice Message

    - +
    diff --git a/Ombi.UI/Views/Admin/NewsletterSettings.cshtml b/Ombi.UI/Views/Admin/NewsletterSettings.cshtml index 2b9f52838..bf890eb93 100644 --- a/Ombi.UI/Views/Admin/NewsletterSettings.cshtml +++ b/Ombi.UI/Views/Admin/NewsletterSettings.cshtml @@ -66,7 +66,7 @@
    - +
    diff --git a/Ombi.UI/Views/Customization/Customization.cshtml b/Ombi.UI/Views/Customization/Customization.cshtml index a220f950d..083cc63c7 100644 --- a/Ombi.UI/Views/Customization/Customization.cshtml +++ b/Ombi.UI/Views/Customization/Customization.cshtml @@ -34,7 +34,7 @@
    diff --git a/Ombi.UI/Views/Landing/Index.cshtml b/Ombi.UI/Views/Landing/Index.cshtml index 21feaf7eb..b5710224f 100644 --- a/Ombi.UI/Views/Landing/Index.cshtml +++ b/Ombi.UI/Views/Landing/Index.cshtml @@ -33,7 +33,7 @@

    Checking...

    - The Plex server is Loading... (check this page for continuous status updates) + The Media server is Loading... (check this page for continuous status updates)
    diff --git a/Ombi.UI/Views/Search/Index.cshtml b/Ombi.UI/Views/Search/Index.cshtml index cef2e73e8..5e3d9a30a 100644 --- a/Ombi.UI/Views/Search/Index.cshtml +++ b/Ombi.UI/Views/Search/Index.cshtml @@ -8,12 +8,14 @@ { url = "/" + baseUrl.ToHtmlString(); } + + }

    @UI.Search_Title

    -

    @UI.Search_Paragraph

    +

    @string.Format(UI.Search_Paragraph, Model.Emby ? "Emby" : "Plex")


    @@ -583,3 +585,4 @@ @Html.LoadSearchAssets() + diff --git a/Ombi.UI/Views/UserLogin/Username.cshtml b/Ombi.UI/Views/UserLogin/Username.cshtml index fcf46d8e7..fd06ae1e2 100644 --- a/Ombi.UI/Views/UserLogin/Username.cshtml +++ b/Ombi.UI/Views/UserLogin/Username.cshtml @@ -6,7 +6,7 @@

    @UI.UserLogin_Title

    - @UI.UserLogin_Paragraph + @string.Format(UI.UserLogin_Paragraph, Html.GetMediaServerName())

    diff --git a/Ombi.UI/Views/UserManagementSettings/UserManagementSettings.cshtml b/Ombi.UI/Views/UserManagementSettings/UserManagementSettings.cshtml index db0fae5f2..03164a4ef 100644 --- a/Ombi.UI/Views/UserManagementSettings/UserManagementSettings.cshtml +++ b/Ombi.UI/Views/UserManagementSettings/UserManagementSettings.cshtml @@ -9,7 +9,7 @@ Here you can manage the default permissions and features that your users get - Note: This will not update your users that are currently there, this is to set the default settings to any users added outside of Ombi e.g. You share your Plex Server with a new user, they will be added into Ombi + Note: This will not update your users that are currently there, this is to set the default settings to any users added outside of Ombi e.g. You share your Server with a new user, they will be added into Ombi automatically and will take the permissions and features you have selected below. diff --git a/Ombi.UI/Views/UserWizard/Index.cshtml b/Ombi.UI/Views/UserWizard/Index.cshtml index b8bae77d2..8ecf52d29 100644 --- a/Ombi.UI/Views/UserWizard/Index.cshtml +++ b/Ombi.UI/Views/UserWizard/Index.cshtml @@ -192,7 +192,7 @@ \ No newline at end of file From 4a39bfd2eae51ed370614f7626a4a0d1e6226012 Mon Sep 17 00:00:00 2001 From: dhruvb14 Date: Fri, 10 Feb 2017 16:59:28 -0500 Subject: [PATCH 45/61] Break out Mass Email feature into its own tab, upgrade Font Awesome and clean up some comments --- Ombi.UI/Content/font-awesome.css | 269 +++++++++++++++++- Ombi.UI/Content/font-awesome.min.css | 4 +- Ombi.UI/Modules/Admin/AdminModule.cs | 8 +- Ombi.UI/Ombi.UI.csproj | 3 + Ombi.UI/Views/Admin/MassEmail.cshtml | 105 +++++++ Ombi.UI/Views/Admin/NewsletterSettings.cshtml | 98 +------ Ombi.UI/Views/Shared/Partial/_Sidebar.cshtml | 1 + 7 files changed, 380 insertions(+), 108 deletions(-) create mode 100644 Ombi.UI/Views/Admin/MassEmail.cshtml diff --git a/Ombi.UI/Content/font-awesome.css b/Ombi.UI/Content/font-awesome.css index b2a5fe2f2..ee906a819 100644 --- a/Ombi.UI/Content/font-awesome.css +++ b/Ombi.UI/Content/font-awesome.css @@ -1,13 +1,13 @@ /*! - * Font Awesome 4.5.0 by @davegandy - http://fontawesome.io - @fontawesome + * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) */ /* FONT PATH * -------------------------- */ @font-face { font-family: 'FontAwesome'; - src: url('../fonts/fontawesome-webfont.eot?v=4.5.0'); - src: url('../fonts/fontawesome-webfont.eot?#iefix&v=4.5.0') format('embedded-opentype'), url('../fonts/fontawesome-webfont.woff2?v=4.5.0') format('woff2'), url('../fonts/fontawesome-webfont.woff?v=4.5.0') format('woff'), url('../fonts/fontawesome-webfont.ttf?v=4.5.0') format('truetype'), url('../fonts/fontawesome-webfont.svg?v=4.5.0#fontawesomeregular') format('svg'); + src: url('../fonts/fontawesome-webfont.eot?v=4.7.0'); + src: url('../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'), url('../fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'), url('../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'), url('../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'), url('../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg'); font-weight: normal; font-style: normal; } @@ -118,31 +118,31 @@ } } .fa-rotate-90 { - filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=1); + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=1)"; -webkit-transform: rotate(90deg); -ms-transform: rotate(90deg); transform: rotate(90deg); } .fa-rotate-180 { - filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2); + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2)"; -webkit-transform: rotate(180deg); -ms-transform: rotate(180deg); transform: rotate(180deg); } .fa-rotate-270 { - filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=3); + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)"; -webkit-transform: rotate(270deg); -ms-transform: rotate(270deg); transform: rotate(270deg); } .fa-flip-horizontal { - filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1); + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)"; -webkit-transform: scale(-1, 1); -ms-transform: scale(-1, 1); transform: scale(-1, 1); } .fa-flip-vertical { - filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1); + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"; -webkit-transform: scale(1, -1); -ms-transform: scale(1, -1); transform: scale(1, -1); @@ -1383,7 +1383,7 @@ .fa-digg:before { content: "\f1a6"; } -.fa-pied-piper:before { +.fa-pied-piper-pp:before { content: "\f1a7"; } .fa-pied-piper-alt:before { @@ -1509,6 +1509,7 @@ content: "\f1ce"; } .fa-ra:before, +.fa-resistance:before, .fa-rebel:before { content: "\f1d0"; } @@ -1831,6 +1832,7 @@ content: "\f23e"; } .fa-battery-4:before, +.fa-battery:before, .fa-battery-full:before { content: "\f240"; } @@ -2084,3 +2086,252 @@ .fa-percent:before { content: "\f295"; } +.fa-gitlab:before { + content: "\f296"; +} +.fa-wpbeginner:before { + content: "\f297"; +} +.fa-wpforms:before { + content: "\f298"; +} +.fa-envira:before { + content: "\f299"; +} +.fa-universal-access:before { + content: "\f29a"; +} +.fa-wheelchair-alt:before { + content: "\f29b"; +} +.fa-question-circle-o:before { + content: "\f29c"; +} +.fa-blind:before { + content: "\f29d"; +} +.fa-audio-description:before { + content: "\f29e"; +} +.fa-volume-control-phone:before { + content: "\f2a0"; +} +.fa-braille:before { + content: "\f2a1"; +} +.fa-assistive-listening-systems:before { + content: "\f2a2"; +} +.fa-asl-interpreting:before, +.fa-american-sign-language-interpreting:before { + content: "\f2a3"; +} +.fa-deafness:before, +.fa-hard-of-hearing:before, +.fa-deaf:before { + content: "\f2a4"; +} +.fa-glide:before { + content: "\f2a5"; +} +.fa-glide-g:before { + content: "\f2a6"; +} +.fa-signing:before, +.fa-sign-language:before { + content: "\f2a7"; +} +.fa-low-vision:before { + content: "\f2a8"; +} +.fa-viadeo:before { + content: "\f2a9"; +} +.fa-viadeo-square:before { + content: "\f2aa"; +} +.fa-snapchat:before { + content: "\f2ab"; +} +.fa-snapchat-ghost:before { + content: "\f2ac"; +} +.fa-snapchat-square:before { + content: "\f2ad"; +} +.fa-pied-piper:before { + content: "\f2ae"; +} +.fa-first-order:before { + content: "\f2b0"; +} +.fa-yoast:before { + content: "\f2b1"; +} +.fa-themeisle:before { + content: "\f2b2"; +} +.fa-google-plus-circle:before, +.fa-google-plus-official:before { + content: "\f2b3"; +} +.fa-fa:before, +.fa-font-awesome:before { + content: "\f2b4"; +} +.fa-handshake-o:before { + content: "\f2b5"; +} +.fa-envelope-open:before { + content: "\f2b6"; +} +.fa-envelope-open-o:before { + content: "\f2b7"; +} +.fa-linode:before { + content: "\f2b8"; +} +.fa-address-book:before { + content: "\f2b9"; +} +.fa-address-book-o:before { + content: "\f2ba"; +} +.fa-vcard:before, +.fa-address-card:before { + content: "\f2bb"; +} +.fa-vcard-o:before, +.fa-address-card-o:before { + content: "\f2bc"; +} +.fa-user-circle:before { + content: "\f2bd"; +} +.fa-user-circle-o:before { + content: "\f2be"; +} +.fa-user-o:before { + content: "\f2c0"; +} +.fa-id-badge:before { + content: "\f2c1"; +} +.fa-drivers-license:before, +.fa-id-card:before { + content: "\f2c2"; +} +.fa-drivers-license-o:before, +.fa-id-card-o:before { + content: "\f2c3"; +} +.fa-quora:before { + content: "\f2c4"; +} +.fa-free-code-camp:before { + content: "\f2c5"; +} +.fa-telegram:before { + content: "\f2c6"; +} +.fa-thermometer-4:before, +.fa-thermometer:before, +.fa-thermometer-full:before { + content: "\f2c7"; +} +.fa-thermometer-3:before, +.fa-thermometer-three-quarters:before { + content: "\f2c8"; +} +.fa-thermometer-2:before, +.fa-thermometer-half:before { + content: "\f2c9"; +} +.fa-thermometer-1:before, +.fa-thermometer-quarter:before { + content: "\f2ca"; +} +.fa-thermometer-0:before, +.fa-thermometer-empty:before { + content: "\f2cb"; +} +.fa-shower:before { + content: "\f2cc"; +} +.fa-bathtub:before, +.fa-s15:before, +.fa-bath:before { + content: "\f2cd"; +} +.fa-podcast:before { + content: "\f2ce"; +} +.fa-window-maximize:before { + content: "\f2d0"; +} +.fa-window-minimize:before { + content: "\f2d1"; +} +.fa-window-restore:before { + content: "\f2d2"; +} +.fa-times-rectangle:before, +.fa-window-close:before { + content: "\f2d3"; +} +.fa-times-rectangle-o:before, +.fa-window-close-o:before { + content: "\f2d4"; +} +.fa-bandcamp:before { + content: "\f2d5"; +} +.fa-grav:before { + content: "\f2d6"; +} +.fa-etsy:before { + content: "\f2d7"; +} +.fa-imdb:before { + content: "\f2d8"; +} +.fa-ravelry:before { + content: "\f2d9"; +} +.fa-eercast:before { + content: "\f2da"; +} +.fa-microchip:before { + content: "\f2db"; +} +.fa-snowflake-o:before { + content: "\f2dc"; +} +.fa-superpowers:before { + content: "\f2dd"; +} +.fa-wpexplorer:before { + content: "\f2de"; +} +.fa-meetup:before { + content: "\f2e0"; +} +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} +.sr-only-focusable:active, +.sr-only-focusable:focus { + position: static; + width: auto; + height: auto; + margin: 0; + overflow: visible; + clip: auto; +} diff --git a/Ombi.UI/Content/font-awesome.min.css b/Ombi.UI/Content/font-awesome.min.css index d0603cb4b..540440ce8 100644 --- a/Ombi.UI/Content/font-awesome.min.css +++ b/Ombi.UI/Content/font-awesome.min.css @@ -1,4 +1,4 @@ /*! - * Font Awesome 4.5.0 by @davegandy - http://fontawesome.io - @fontawesome + * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) - */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.5.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.5.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.5.0') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.5.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.5.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.5.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1);-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1);-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"} + */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.7.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.fa-handshake-o:before{content:"\f2b5"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-o:before{content:"\f2b7"}.fa-linode:before{content:"\f2b8"}.fa-address-book:before{content:"\f2b9"}.fa-address-book-o:before{content:"\f2ba"}.fa-vcard:before,.fa-address-card:before{content:"\f2bb"}.fa-vcard-o:before,.fa-address-card-o:before{content:"\f2bc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-circle-o:before{content:"\f2be"}.fa-user-o:before{content:"\f2c0"}.fa-id-badge:before{content:"\f2c1"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:"\f2c3"}.fa-quora:before{content:"\f2c4"}.fa-free-code-camp:before{content:"\f2c5"}.fa-telegram:before{content:"\f2c6"}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-shower:before{content:"\f2cc"}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:"\f2cd"}.fa-podcast:before{content:"\f2ce"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-times-rectangle:before,.fa-window-close:before{content:"\f2d3"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\f2d4"}.fa-bandcamp:before{content:"\f2d5"}.fa-grav:before{content:"\f2d6"}.fa-etsy:before{content:"\f2d7"}.fa-imdb:before{content:"\f2d8"}.fa-ravelry:before{content:"\f2d9"}.fa-eercast:before{content:"\f2da"}.fa-microchip:before{content:"\f2db"}.fa-snowflake-o:before{content:"\f2dc"}.fa-superpowers:before{content:"\f2dd"}.fa-wpexplorer:before{content:"\f2de"}.fa-meetup:before{content:"\f2e0"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto} diff --git a/Ombi.UI/Modules/Admin/AdminModule.cs b/Ombi.UI/Modules/Admin/AdminModule.cs index c9bce2faf..a11c473b7 100644 --- a/Ombi.UI/Modules/Admin/AdminModule.cs +++ b/Ombi.UI/Modules/Admin/AdminModule.cs @@ -225,7 +225,9 @@ namespace Ombi.UI.Modules.Admin Get["/newsletter", true] = async (x, ct) => await Newsletter(); Post["/newsletter", true] = async (x, ct) => await SaveNewsletter(); - Post["/testnewsletteradminemail"] = x => TestNewsletterAdminEmail(); + Post["/testnewsletteradminemail"] = x => TestNewsletterAdminEmail(); + + Get["/massemail"] = _ => MassEmailView(); Post["/testmassadminemail"] = x => TestMassAdminEmail(); Post["/sendmassemail"] = x => SendMassEmail(); @@ -930,6 +932,10 @@ namespace Ombi.UI.Modules.Admin var settings = await NewsLetterService.GetSettingsAsync(); return View["NewsletterSettings", settings]; } + private Negotiator MassEmailView() + { + return View["MassEmail"]; + } private async Task SaveNewsletter() { diff --git a/Ombi.UI/Ombi.UI.csproj b/Ombi.UI/Ombi.UI.csproj index 09f259740..927eef315 100644 --- a/Ombi.UI/Ombi.UI.csproj +++ b/Ombi.UI/Ombi.UI.csproj @@ -876,6 +876,9 @@ Always + + PreserveNewest + PreserveNewest diff --git a/Ombi.UI/Views/Admin/MassEmail.cshtml b/Ombi.UI/Views/Admin/MassEmail.cshtml new file mode 100644 index 000000000..9fe67d137 --- /dev/null +++ b/Ombi.UI/Views/Admin/MassEmail.cshtml @@ -0,0 +1,105 @@ +@using System.Linq +@using Ombi.UI.Helpers +@inherits Nancy.ViewEngines.Razor.NancyRazorViewBase +@Html.Partial("Shared/Partial/_Sidebar") + +
    + +
    + Mass Email + +
    +
    + Note: This will require you to setup your email notifications +
    +
    + +
    + +
    +
    +
    + + + + Supports HTML + +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    + +
    + +
    + + diff --git a/Ombi.UI/Views/Admin/NewsletterSettings.cshtml b/Ombi.UI/Views/Admin/NewsletterSettings.cshtml index bf890eb93..a28c27c30 100644 --- a/Ombi.UI/Views/Admin/NewsletterSettings.cshtml +++ b/Ombi.UI/Views/Admin/NewsletterSettings.cshtml @@ -9,7 +9,7 @@ Newsletter Settings
    - +
    @@ -51,42 +51,7 @@
    - - -
    - - -
    -
    - Mass Email -
    -
    - Note: This will require you to setup your email notifications -
    -
    - -
    - -
    -
    -
    - - - - Supports HTML - -
    -
    -
    - -
    -
    -
    -
    - -
    -
    -
    +
    @@ -94,8 +59,6 @@ diff --git a/Ombi.UI/Views/Shared/Partial/_Sidebar.cshtml b/Ombi.UI/Views/Shared/Partial/_Sidebar.cshtml index d5e8bea2d..f9f16c866 100644 --- a/Ombi.UI/Views/Shared/Partial/_Sidebar.cshtml +++ b/Ombi.UI/Views/Shared/Partial/_Sidebar.cshtml @@ -17,6 +17,7 @@ @Html.GetSidebarUrl(Context, "/admin/sickrage", "SickRage", "fa fa-tv") @Html.GetSidebarUrl(Context, "/admin/headphones", "Headphones (beta)", "glyphicon glyphicon-headphones") @Html.GetSidebarUrl(Context, "/admin/newsletter", "Newsletter Settings", "fa fa-newspaper-o") + @Html.GetSidebarUrl(Context, "/admin/massemail", "Mass Email", "fa fa-reply-all")