From 16823e27398824401aa81584a14d74178c614ebc Mon Sep 17 00:00:00 2001 From: dhruvb14 Date: Tue, 31 Jan 2017 22:03:57 -0500 Subject: [PATCH 1/9] Begin Implementing Mass Email Section --- Ombi.Services/Interfaces/IMassEmail.cs | 9 +++ Ombi.Services/Interfaces/IRecentlyAdded.cs | 2 +- Ombi.Services/Jobs/RecentlyAdded.cs | 10 ++- Ombi.Services/Ombi.Services.csproj | 1 + Ombi.UI/Modules/Admin/AdminModule.cs | 41 ++++++++++-- Ombi.UI/Views/Admin/NewsletterSettings.cshtml | 65 ++++++++++++++++--- 6 files changed, 110 insertions(+), 18 deletions(-) create mode 100644 Ombi.Services/Interfaces/IMassEmail.cs diff --git a/Ombi.Services/Interfaces/IMassEmail.cs b/Ombi.Services/Interfaces/IMassEmail.cs new file mode 100644 index 000000000..f3433c5f0 --- /dev/null +++ b/Ombi.Services/Interfaces/IMassEmail.cs @@ -0,0 +1,9 @@ +using Quartz; + +namespace Ombi.Services.Jobs +{ + public interface IMassEmail + { + void MassEmailAdminTest(); + } +} \ No newline at end of file diff --git a/Ombi.Services/Interfaces/IRecentlyAdded.cs b/Ombi.Services/Interfaces/IRecentlyAdded.cs index 09a7220f5..c18ca8e27 100644 --- a/Ombi.Services/Interfaces/IRecentlyAdded.cs +++ b/Ombi.Services/Interfaces/IRecentlyAdded.cs @@ -5,7 +5,7 @@ namespace Ombi.Services.Jobs public interface IRecentlyAdded { void Execute(IJobExecutionContext context); - void Test(); + void RecentlyAddedAdminTest(); void Start(); } } \ No newline at end of file diff --git a/Ombi.Services/Jobs/RecentlyAdded.cs b/Ombi.Services/Jobs/RecentlyAdded.cs index 563dd35a5..ece148398 100644 --- a/Ombi.Services/Jobs/RecentlyAdded.cs +++ b/Ombi.Services/Jobs/RecentlyAdded.cs @@ -48,7 +48,7 @@ using Quartz; namespace Ombi.Services.Jobs { - public class RecentlyAdded : HtmlTemplateGenerator, IJob, IRecentlyAdded + public class RecentlyAdded : HtmlTemplateGenerator, IJob, IRecentlyAdded, IMassEmail { public RecentlyAdded(IPlexApi api, ISettingsService plexSettings, ISettingsService email, IJobRecord rec, @@ -105,7 +105,13 @@ namespace Ombi.Services.Jobs Start(); } - public void Test() + public void RecentlyAddedAdminTest() + { + Log.Debug("Starting Recently Added Newsletter Test"); + var settings = NewsletterSettings.GetSettings(); + Start(settings, true); + } + public void MassEmailAdminTest() { Log.Debug("Starting Test Newsletter"); var settings = NewsletterSettings.GetSettings(); diff --git a/Ombi.Services/Ombi.Services.csproj b/Ombi.Services/Ombi.Services.csproj index 1735094dd..0e47b3669 100644 --- a/Ombi.Services/Ombi.Services.csproj +++ b/Ombi.Services/Ombi.Services.csproj @@ -87,6 +87,7 @@ + diff --git a/Ombi.UI/Modules/Admin/AdminModule.cs b/Ombi.UI/Modules/Admin/AdminModule.cs index a83ff8bd1..005646165 100644 --- a/Ombi.UI/Modules/Admin/AdminModule.cs +++ b/Ombi.UI/Modules/Admin/AdminModule.cs @@ -92,6 +92,7 @@ namespace Ombi.UI.Modules.Admin private IJobRecord JobRecorder { get; } private IAnalytics Analytics { get; } private IRecentlyAdded RecentlyAdded { get; } + private IMassEmail MassEmail { get; } private ISettingsService NotifySettings { get; } private ISettingsService DiscordSettings { get; } private IDiscordApi DiscordApi { get; } @@ -222,6 +223,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["/testmassadminemail"] = x => TestMassAdminEmail(); + Post["/sendmassemail"] = x => SendMassEmail(); Post["/createapikey"] = x => CreateApiKey(); @@ -246,7 +250,6 @@ namespace Ombi.UI.Modules.Admin Get["/notificationsettings", true] = async (x, ct) => await NotificationSettings(); Post["/notificationsettings"] = x => SaveNotificationSettings(); - Post["/recentlyAddedTest"] = x => RecentlyAddedTest(); } private async Task Authentication() @@ -1229,13 +1232,41 @@ namespace Ombi.UI.Modules.Admin var model = this.Bind(); return View["NotificationSettings", model]; } - - private Response RecentlyAddedTest() + + private Response TestNewsletterAdminEmail() + { + try + { + Log.Debug("Clicked Admin Newsletter Email Test"); + RecentlyAdded.RecentlyAddedAdminTest(); + return Response.AsJson(new JsonResponseModel { Result = true, Message = "Sent email to administrator" }); + } + catch (Exception e) + { + Log.Error(e); + return Response.AsJson(new JsonResponseModel { Result = false, Message = e.Message }); + } + } + private Response TestMassAdminEmail() + { + try + { + Log.Debug("Clicked Admin Mass Email Test"); + RecentlyAdded.RecentlyAddedAdminTest(); + return Response.AsJson(new JsonResponseModel { Result = true, Message = "Sent email to administrator" }); + } + catch (Exception e) + { + Log.Error(e); + return Response.AsJson(new JsonResponseModel { Result = false, Message = e.Message }); + } + } + private Response SendMassEmail() { try { - Log.Debug("Clicked TEST"); - RecentlyAdded.Test(); + Log.Debug("Clicked Send Mass Email"); + RecentlyAdded.RecentlyAddedAdminTest(); return Response.AsJson(new JsonResponseModel { Result = true, Message = "Sent email to administrator" }); } catch (Exception e) diff --git a/Ombi.UI/Views/Admin/NewsletterSettings.cshtml b/Ombi.UI/Views/Admin/NewsletterSettings.cshtml index 898a7a761..0a1e07852 100644 --- a/Ombi.UI/Views/Admin/NewsletterSettings.cshtml +++ b/Ombi.UI/Views/Admin/NewsletterSettings.cshtml @@ -28,17 +28,17 @@
-
+
- You can add multiple email addresses by using the ; delimiter -
+ You can add multiple email addresses by using the ; delimiter +
- +
@@ -46,7 +46,7 @@
- +
@@ -54,6 +54,23 @@ +
+
+ Mass Email +
+ + + + Supports HTML + +
+
+
+ +
+
+
+
@@ -90,26 +107,54 @@ $('#recentlyAddedBtn').click(function (e) { e.preventDefault(); var base = '@Html.GetBaseUrl()'; - var url = createBaseUrl(base, '/admin/recentlyAddedTest'); - $('#spinner').attr("class", "fa fa-spinner fa-spin"); + var url = createBaseUrl(base, '/admin/testnewsletteradminemail'); + $('#testEmailSpinner').attr("class", "fa fa-spinner fa-spin"); + $.ajax({ + type: "post", + url: url, + dataType: "json", + success: function (response) { + if (response) { + generateNotify(response.message, "success"); + $('#testEmailSpinner').attr("class", "fa fa-check"); + } else { + + generateNotify(response.message, "danger"); + $('#testEmailSpinner').attr("class", "fa fa-times"); + } + }, + error: function (e) { + console.log(e); + generateNotify("Something went wrong!", "danger"); + $('#testEmailSpinner').attr("class", "fa fa-times"); + } + }); + }); + + $('#sendMassEmailBtn').click(function (e) { + e.preventDefault(); + var base = '@Html.GetBaseUrl()'; + var url = createBaseUrl(base, '/admin/testmassadminemail'); + $('#sendMassEmailSpinner').attr("class", "fa fa-spinner fa-spin"); $.ajax({ type: "post", url: url, + data: $("#massEmailBody").val(), dataType: "json", success: function (response) { if (response) { generateNotify(response.message, "success"); - $('#spinner').attr("class", "fa fa-check"); + $('#sendMassEmailSpinner').attr("class", "fa fa-check"); } else { generateNotify(response.message, "danger"); - $('#spinner').attr("class", "fa fa-times"); + $('#sendMassEmailSpinner').attr("class", "fa fa-times"); } }, error: function (e) { console.log(e); generateNotify("Something went wrong!", "danger"); - $('#spinner').attr("class", "fa fa-times"); + $('#sendMassEmailSpinner').attr("class", "fa fa-times"); } }); }); From 8f3c4ca1b2cf3bcf18fb3bdfebc6b67d3ce13adf Mon Sep 17 00:00:00 2001 From: dhruvb14 Date: Tue, 31 Jan 2017 22:54:58 -0500 Subject: [PATCH 2/9] Does not compile, need to get data from UI into nancy somehow and figure out why IMassEmail is not initializing --- Ombi.Services/Interfaces/IMassEmail.cs | 3 +- Ombi.Services/Jobs/RecentlyAdded.cs | 9 +- .../Jobs/Templates/MassEmailTemplate.cs | 58 ++++++ .../Jobs/Templates/MassEmailTemplate.html | 181 ++++++++++++++++++ Ombi.Services/Ombi.Services.csproj | 4 + Ombi.UI/Modules/Admin/AdminModule.cs | 5 +- Ombi.UI/Views/Admin/NewsletterSettings.cshtml | 2 +- 7 files changed, 255 insertions(+), 7 deletions(-) create mode 100644 Ombi.Services/Jobs/Templates/MassEmailTemplate.cs create mode 100644 Ombi.Services/Jobs/Templates/MassEmailTemplate.html diff --git a/Ombi.Services/Interfaces/IMassEmail.cs b/Ombi.Services/Interfaces/IMassEmail.cs index f3433c5f0..c7b88cad9 100644 --- a/Ombi.Services/Interfaces/IMassEmail.cs +++ b/Ombi.Services/Interfaces/IMassEmail.cs @@ -4,6 +4,7 @@ namespace Ombi.Services.Jobs { public interface IMassEmail { - void MassEmailAdminTest(); + void Execute(IJobExecutionContext context); + void MassEmailAdminTest(string html); } } \ No newline at end of file diff --git a/Ombi.Services/Jobs/RecentlyAdded.cs b/Ombi.Services/Jobs/RecentlyAdded.cs index ece148398..937eac8ab 100644 --- a/Ombi.Services/Jobs/RecentlyAdded.cs +++ b/Ombi.Services/Jobs/RecentlyAdded.cs @@ -111,11 +111,14 @@ namespace Ombi.Services.Jobs var settings = NewsletterSettings.GetSettings(); Start(settings, true); } - public void MassEmailAdminTest() + public void MassEmailAdminTest(string html) { - Log.Debug("Starting Test Newsletter"); + Log.Debug("Starting Mass Email Test"); var settings = NewsletterSettings.GetSettings(); - Start(settings, true); + var plexSettings = PlexSettings.GetSettings(); + var template = new MassEmailTemplate(); + var body = template.LoadTemplate(html); + Send(settings, body, plexSettings, true); } private void Start(NewletterSettings newletterSettings, bool testEmail = false) diff --git a/Ombi.Services/Jobs/Templates/MassEmailTemplate.cs b/Ombi.Services/Jobs/Templates/MassEmailTemplate.cs new file mode 100644 index 000000000..ea73c027d --- /dev/null +++ b/Ombi.Services/Jobs/Templates/MassEmailTemplate.cs @@ -0,0 +1,58 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: RecentlyAddedTemplate.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.IO; +using System.Text; +using System.Windows.Forms; +using NLog; + +namespace Ombi.Services.Jobs.Templates +{ + public class MassEmailTemplate + { + public string TemplateLocation => Path.Combine(Path.GetDirectoryName(Application.ExecutablePath) ?? string.Empty, "Jobs", "Templates", "MassEmailTemplate.html"); + private static readonly Logger Log = LogManager.GetCurrentClassLogger(); + + private const string RecentlyAddedKey = "{@MASSEMAIL}"; + + public string LoadTemplate(string html) + { + try + { + var sb = new StringBuilder(File.ReadAllText(TemplateLocation)); + sb.Replace(RecentlyAddedKey, html); + return sb.ToString(); + } + catch (Exception e) + { + Log.Error(e); + return string.Empty; + } + } + } +} \ No newline at end of file diff --git a/Ombi.Services/Jobs/Templates/MassEmailTemplate.html b/Ombi.Services/Jobs/Templates/MassEmailTemplate.html new file mode 100644 index 000000000..4fdd04524 --- /dev/null +++ b/Ombi.Services/Jobs/Templates/MassEmailTemplate.html @@ -0,0 +1,181 @@ + + + + + + Ombi + + + + + + + + + +
  +
+ + + + + + + + + + + +
+ + + + + + + +
+ +
+ {@MASSEMAIL} +
+
+ + + + + + +
+
 
+ + \ No newline at end of file diff --git a/Ombi.Services/Ombi.Services.csproj b/Ombi.Services/Ombi.Services.csproj index 0e47b3669..a37279f7d 100644 --- a/Ombi.Services/Ombi.Services.csproj +++ b/Ombi.Services/Ombi.Services.csproj @@ -108,6 +108,7 @@ + @@ -181,6 +182,9 @@
+ + PreserveNewest + PreserveNewest diff --git a/Ombi.UI/Modules/Admin/AdminModule.cs b/Ombi.UI/Modules/Admin/AdminModule.cs index 005646165..181cc1d1b 100644 --- a/Ombi.UI/Modules/Admin/AdminModule.cs +++ b/Ombi.UI/Modules/Admin/AdminModule.cs @@ -124,7 +124,7 @@ namespace Ombi.UI.Modules.Admin ICacheProvider cache, ISettingsService slackSettings, ISlackApi slackApi, ISettingsService lp, ISettingsService scheduler, IJobRecord rec, IAnalytics analytics, - ISettingsService notifyService, IRecentlyAdded recentlyAdded, + ISettingsService notifyService, IRecentlyAdded recentlyAdded, IMassEmail massEmail, ISettingsService watcherSettings , ISettingsService discord, IDiscordApi discordapi, ISettingsService settings, IRadarrApi radarrApi, @@ -159,6 +159,7 @@ namespace Ombi.UI.Modules.Admin Analytics = analytics; NotifySettings = notifyService; RecentlyAdded = recentlyAdded; + MassEmail = massEmail; WatcherSettings = watcherSettings; DiscordSettings = discord; DiscordApi = discordapi; @@ -1252,7 +1253,7 @@ namespace Ombi.UI.Modules.Admin try { Log.Debug("Clicked Admin Mass Email Test"); - RecentlyAdded.RecentlyAddedAdminTest(); + MassEmail.MassEmailAdminTest("Dhruv's Test Email"); return Response.AsJson(new JsonResponseModel { Result = true, Message = "Sent email to administrator" }); } catch (Exception e) diff --git a/Ombi.UI/Views/Admin/NewsletterSettings.cshtml b/Ombi.UI/Views/Admin/NewsletterSettings.cshtml index 0a1e07852..02958cb33 100644 --- a/Ombi.UI/Views/Admin/NewsletterSettings.cshtml +++ b/Ombi.UI/Views/Admin/NewsletterSettings.cshtml @@ -66,7 +66,7 @@
- +
From 941abac10b3bab4b3b8deede5013f2c57497e583 Mon Sep 17 00:00:00 2001 From: "Jamie.Rees" Date: Wed, 1 Feb 2017 08:46:52 +0000 Subject: [PATCH 3/9] Fixed pace loader --- Ombi.UI/Content/base.css | 19 +++++++++++++++++++ Ombi.UI/Content/base.min.css | 2 +- Ombi.UI/Content/base.scss | 23 +++++++++++++++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/Ombi.UI/Content/base.css b/Ombi.UI/Content/base.css index 2b67cfa3e..4232dbf6e 100644 --- a/Ombi.UI/Content/base.css +++ b/Ombi.UI/Content/base.css @@ -525,3 +525,22 @@ label { display: block !important; margin: 0 auto !important; } +.pace { + -webkit-pointer-events: none; + pointer-events: none; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; } + +.pace-inactive { + display: none; } + +.pace .pace-progress { + background: #df691a; + position: fixed; + z-index: 2000; + top: 0; + right: 100%; + width: 100%; + height: 5px; } + diff --git a/Ombi.UI/Content/base.min.css b/Ombi.UI/Content/base.min.css index 164bdc9a5..f8b174325 100644 --- a/Ombi.UI/Content/base.min.css +++ b/Ombi.UI/Content/base.min.css @@ -1 +1 @@ -@media(min-width:768px){.row{position:relative;}.bottom-align-text{position:absolute;bottom:0;right:0;}.landing-block .media{max-width:450px;}}@media(max-width:48em){.home{padding-top:1rem;}}@media(min-width:48em){.home{padding-top:4rem;}}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#fff;}hr{border:1px dashed #777;}.btn{border-radius:.25rem !important;}.btn-group-separated .btn,.btn-group-separated .btn+.btn{margin-left:3px;}.multiSelect{background-color:#4e5d6c;}.form-control-custom{background-color:#4e5d6c !important;color:#fff !important;border-radius:0;box-shadow:0 0 0 !important;}h1{font-size:3.5rem !important;font-weight:600 !important;}.request-title{margin-top:0 !important;font-size:1.9rem !important;}p{font-size:1.1rem !important;}label{display:inline-block !important;margin-bottom:.5rem !important;font-size:16px !important;}.small-label{display:inline-block !important;margin-bottom:.5rem !important;font-size:11px !important;}.small-checkbox{min-height:0 !important;}.round-checkbox{border-radius:8px;}.nav-tabs>li{font-size:13px;line-height:21px;}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{background:#4e5d6c;}.nav-tabs>li>a>.fa{padding:3px 5px 3px 3px;}.nav-tabs>li.nav-tab-right{float:right;}.nav-tabs>li.nav-tab-right a{margin-right:0;margin-left:2px;}.nav-tabs>li.nav-tab-icononly .fa{padding:3px;}.navbar .nav a .fa,.dropdown-menu a .fa{font-size:130%;top:1px;position:relative;display:inline-block;margin-right:5px;}.dropdown-menu a .fa{top:2px;}.btn-danger-outline{color:#d9534f !important;background-color:transparent;background-image:none;border-color:#d9534f !important;}.btn-danger-outline:focus,.btn-danger-outline.focus,.btn-danger-outline:active,.btn-danger-outline.active,.btn-danger-outline:hover,.open>.btn-danger-outline.dropdown-toggle{color:#fff !important;background-color:#d9534f !important;border-color:#d9534f !important;}.btn-primary-outline{color:#ff761b !important;background-color:transparent;background-image:none;border-color:#ff761b !important;}.btn-primary-outline:focus,.btn-primary-outline.focus,.btn-primary-outline:active,.btn-primary-outline.active,.btn-primary-outline:hover,.open>.btn-primary-outline.dropdown-toggle{color:#fff !important;background-color:#df691a !important;border-color:#df691a !important;}.btn-info-outline{color:#5bc0de !important;background-color:transparent;background-image:none;border-color:#5bc0de !important;}.btn-info-outline:focus,.btn-info-outline.focus,.btn-info-outline:active,.btn-info-outline.active,.btn-info-outline:hover,.open>.btn-info-outline.dropdown-toggle{color:#fff !important;background-color:#5bc0de !important;border-color:#5bc0de !important;}.btn-warning-outline{color:#f0ad4e !important;background-color:transparent;background-image:none;border-color:#f0ad4e !important;}.btn-warning-outline:focus,.btn-warning-outline.focus,.btn-warning-outline:active,.btn-warning-outline.active,.btn-warning-outline:hover,.open>.btn-warning-outline.dropdown-toggle{color:#fff !important;background-color:#f0ad4e !important;border-color:#f0ad4e !important;}.btn-success-outline{color:#5cb85c !important;background-color:transparent;background-image:none;border-color:#5cb85c !important;}.btn-success-outline:focus,.btn-success-outline.focus,.btn-success-outline:active,.btn-success-outline.active,.btn-success-outline:hover,.open>.btn-success-outline.dropdown-toggle{color:#fff !important;background-color:#5cb85c !important;border-color:#5cb85c !important;}#movieList .mix{display:none;}#tvList .mix{display:none;}.scroll-top-wrapper{position:fixed;opacity:0;visibility:hidden;overflow:hidden;text-align:center;z-index:99999999;background-color:#4e5d6c;color:#eee;width:50px;height:48px;line-height:48px;right:30px;bottom:30px;padding-top:2px;border-top-left-radius:10px;border-top-right-radius:10px;border-bottom-right-radius:10px;border-bottom-left-radius:10px;-webkit-transition:all .5s ease-in-out;-moz-transition:all .5s ease-in-out;-ms-transition:all .5s ease-in-out;-o-transition:all .5s ease-in-out;transition:all .5s ease-in-out;}.scroll-top-wrapper:hover{background-color:#637689;}.scroll-top-wrapper.show{visibility:visible;cursor:pointer;opacity:1;}.scroll-top-wrapper i.fa{line-height:inherit;}.no-search-results{text-align:center;}.no-search-results .no-search-results-icon{font-size:10em;color:#4e5d6c;}.no-search-results .no-search-results-text{margin:20px 0;color:#ccc;}.form-control-search{padding:13px 105px 13px 16px;height:100%;}.form-control-withbuttons{padding-right:105px;}.input-group-addon .btn-group{position:absolute;right:45px;z-index:3;top:10px;box-shadow:0 0 0;}.input-group-addon .btn-group .btn{border:1px solid rgba(255,255,255,.7) !important;padding:3px 12px;color:rgba(255,255,255,.7) !important;}.btn-split .btn{border-radius:0 !important;}.btn-split .btn:not(.dropdown-toggle){border-radius:.25rem 0 0 .25rem !important;}.btn-split .btn.dropdown-toggle{border-radius:0 .25rem .25rem 0 !important;padding:12px 8px;}#updateAvailable{background-color:#df691a;text-align:center;font-size:15px;padding:3px 0;}#cacherRunning{background-color:#4e5d6c;text-align:center;font-size:15px;padding:3px 0;}.checkbox label{display:inline-block;cursor:pointer;position:relative;padding-left:25px;margin-right:15px;font-size:13px;margin-bottom:10px;}.checkbox label:before{content:"";display:inline-block;width:18px;height:18px;margin-right:10px;position:absolute;left:0;bottom:1px;border:2px solid #eee;border-radius:3px;}.checkbox input[type=checkbox]{display:none;}.checkbox input[type=checkbox]:checked+label:before{content:"✓";font-size:13px;color:#fafafa;text-align:center;line-height:13px;}.small-checkbox label{display:inline-block;cursor:pointer;position:relative;padding-left:25px;margin-right:15px;font-size:13px;margin-bottom:10px;}.small-checkbox label:before{content:"";display:inline-block;width:18px;height:18px;margin-right:10px;position:absolute;left:0;bottom:1px;border:2px solid #eee;border-radius:8px;min-height:0 !important;}.small-checkbox input[type=checkbox]{display:none;}.small-checkbox input[type=checkbox]:checked+label:before{content:"✓";font-size:13px;color:#fafafa;text-align:center;line-height:13px;}.small-checkbox label{min-height:0 !important;padding-left:20px;margin-bottom:0;font-weight:normal;cursor:pointer;}.input-group-sm{padding-top:2px;padding-bottom:2px;}.tab-pane .form-horizontal .form-group{margin-right:15px;margin-left:15px;}.bootstrap-datetimepicker-widget.dropdown-menu{background-color:#4e5d6c;}.bootstrap-datetimepicker-widget.dropdown-menu.bottom:after{border-bottom:6px solid #4e5d6c !important;}.bootstrap-datetimepicker-widget table td.active,.bootstrap-datetimepicker-widget table td.active:hover{color:#fff !important;}.landing-header{display:block;margin:60px auto;}.landing-block{background:#2f2f2f !important;padding:5px;}.landing-block .media{margin:30px auto;max-width:450px;}.landing-block .media .media-left{display:inline-block;float:left;width:70px;}.landing-block .media .media-left i.fa{font-size:3em;}.landing-title{font-weight:bold;}.checkbox-custom{margin-top:0 !important;margin-bottom:0 !important;}.tooltip_templates{display:none;}.shadow{-moz-box-shadow:3px 3px 5px 6px #191919;-webkit-box-shadow:3px 3px 5px 6px #191919;box-shadow:3px 3px 5px 6px #191919;}.img-circle{border-radius:50%;}#wrapper{padding-left:0;-webkit-transition:all .5s ease;-moz-transition:all .5s ease;-o-transition:all .5s ease;transition:all .5s ease;}#wrapper.toggled{padding-right:250px;}#sidebar-wrapper{z-index:1000;position:fixed;right:250px;width:0;height:100%;margin-right:-250px;overflow-y:auto;background:#4e5d6c;padding-left:0;-webkit-transition:all .5s ease;-moz-transition:all .5s ease;-o-transition:all .5s ease;transition:all .5s ease;}#wrapper.toggled #sidebar-wrapper{width:500px;}#page-content-wrapper{width:100%;position:absolute;padding:15px;}#wrapper.toggled #page-content-wrapper{position:absolute;margin-left:-250px;}.sidebar-nav{position:absolute;top:0;width:500px;margin:0;padding-left:0;list-style:none;}.sidebar-nav li{text-indent:20px;line-height:40px;}.sidebar-nav li a{display:block;text-decoration:none;color:#999;}.sidebar-nav li a:hover{text-decoration:none;color:#fff;background:rgba(255,255,255,.2);}.sidebar-nav li a:active,.sidebar-nav li a:focus{text-decoration:none;}.sidebar-nav>.sidebar-brand{height:65px;font-size:18px;line-height:60px;}.sidebar-nav>.sidebar-brand a{color:#999;}.sidebar-nav>.sidebar-brand a:hover{color:#fff;background:none;}@media(min-width:768px){#wrapper{padding-right:250px;}#wrapper.toggled{padding-right:0;}#sidebar-wrapper{width:500px;}#wrapper.toggled #sidebar-wrapper{width:0;}#page-content-wrapper{padding:20px;position:relative;}#wrapper.toggled #page-content-wrapper{position:relative;margin-right:0;}}#lightbox{background-color:#808080;filter:alpha(opacity=50);opacity:.5;-moz-opacity:.5;top:0;left:0;z-index:20;height:100%;width:100%;background-repeat:no-repeat;background-position:center;position:absolute;}.list-group-item-dropdown{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#3e3e3e;border:1px solid transparent;}.wizard-heading{text-align:center;}.wizard-img{width:300px;display:block !important;margin:0 auto !important;} \ No newline at end of file +@media(min-width:768px){.row{position:relative;}.bottom-align-text{position:absolute;bottom:0;right:0;}.landing-block .media{max-width:450px;}}@media(max-width:48em){.home{padding-top:1rem;}}@media(min-width:48em){.home{padding-top:4rem;}}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#fff;}hr{border:1px dashed #777;}.btn{border-radius:.25rem !important;}.btn-group-separated .btn,.btn-group-separated .btn+.btn{margin-left:3px;}.multiSelect{background-color:#4e5d6c;}.form-control-custom{background-color:#4e5d6c !important;color:#fff !important;border-radius:0;box-shadow:0 0 0 !important;}h1{font-size:3.5rem !important;font-weight:600 !important;}.request-title{margin-top:0 !important;font-size:1.9rem !important;}p{font-size:1.1rem !important;}label{display:inline-block !important;margin-bottom:.5rem !important;font-size:16px !important;}.small-label{display:inline-block !important;margin-bottom:.5rem !important;font-size:11px !important;}.small-checkbox{min-height:0 !important;}.round-checkbox{border-radius:8px;}.nav-tabs>li{font-size:13px;line-height:21px;}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{background:#4e5d6c;}.nav-tabs>li>a>.fa{padding:3px 5px 3px 3px;}.nav-tabs>li.nav-tab-right{float:right;}.nav-tabs>li.nav-tab-right a{margin-right:0;margin-left:2px;}.nav-tabs>li.nav-tab-icononly .fa{padding:3px;}.navbar .nav a .fa,.dropdown-menu a .fa{font-size:130%;top:1px;position:relative;display:inline-block;margin-right:5px;}.dropdown-menu a .fa{top:2px;}.btn-danger-outline{color:#d9534f !important;background-color:transparent;background-image:none;border-color:#d9534f !important;}.btn-danger-outline:focus,.btn-danger-outline.focus,.btn-danger-outline:active,.btn-danger-outline.active,.btn-danger-outline:hover,.open>.btn-danger-outline.dropdown-toggle{color:#fff !important;background-color:#d9534f !important;border-color:#d9534f !important;}.btn-primary-outline{color:#ff761b !important;background-color:transparent;background-image:none;border-color:#ff761b !important;}.btn-primary-outline:focus,.btn-primary-outline.focus,.btn-primary-outline:active,.btn-primary-outline.active,.btn-primary-outline:hover,.open>.btn-primary-outline.dropdown-toggle{color:#fff !important;background-color:#df691a !important;border-color:#df691a !important;}.btn-info-outline{color:#5bc0de !important;background-color:transparent;background-image:none;border-color:#5bc0de !important;}.btn-info-outline:focus,.btn-info-outline.focus,.btn-info-outline:active,.btn-info-outline.active,.btn-info-outline:hover,.open>.btn-info-outline.dropdown-toggle{color:#fff !important;background-color:#5bc0de !important;border-color:#5bc0de !important;}.btn-warning-outline{color:#f0ad4e !important;background-color:transparent;background-image:none;border-color:#f0ad4e !important;}.btn-warning-outline:focus,.btn-warning-outline.focus,.btn-warning-outline:active,.btn-warning-outline.active,.btn-warning-outline:hover,.open>.btn-warning-outline.dropdown-toggle{color:#fff !important;background-color:#f0ad4e !important;border-color:#f0ad4e !important;}.btn-success-outline{color:#5cb85c !important;background-color:transparent;background-image:none;border-color:#5cb85c !important;}.btn-success-outline:focus,.btn-success-outline.focus,.btn-success-outline:active,.btn-success-outline.active,.btn-success-outline:hover,.open>.btn-success-outline.dropdown-toggle{color:#fff !important;background-color:#5cb85c !important;border-color:#5cb85c !important;}#movieList .mix{display:none;}#tvList .mix{display:none;}.scroll-top-wrapper{position:fixed;opacity:0;visibility:hidden;overflow:hidden;text-align:center;z-index:99999999;background-color:#4e5d6c;color:#eee;width:50px;height:48px;line-height:48px;right:30px;bottom:30px;padding-top:2px;border-top-left-radius:10px;border-top-right-radius:10px;border-bottom-right-radius:10px;border-bottom-left-radius:10px;-webkit-transition:all .5s ease-in-out;-moz-transition:all .5s ease-in-out;-ms-transition:all .5s ease-in-out;-o-transition:all .5s ease-in-out;transition:all .5s ease-in-out;}.scroll-top-wrapper:hover{background-color:#637689;}.scroll-top-wrapper.show{visibility:visible;cursor:pointer;opacity:1;}.scroll-top-wrapper i.fa{line-height:inherit;}.no-search-results{text-align:center;}.no-search-results .no-search-results-icon{font-size:10em;color:#4e5d6c;}.no-search-results .no-search-results-text{margin:20px 0;color:#ccc;}.form-control-search{padding:13px 105px 13px 16px;height:100%;}.form-control-withbuttons{padding-right:105px;}.input-group-addon .btn-group{position:absolute;right:45px;z-index:3;top:10px;box-shadow:0 0 0;}.input-group-addon .btn-group .btn{border:1px solid rgba(255,255,255,.7) !important;padding:3px 12px;color:rgba(255,255,255,.7) !important;}.btn-split .btn{border-radius:0 !important;}.btn-split .btn:not(.dropdown-toggle){border-radius:.25rem 0 0 .25rem !important;}.btn-split .btn.dropdown-toggle{border-radius:0 .25rem .25rem 0 !important;padding:12px 8px;}#updateAvailable{background-color:#df691a;text-align:center;font-size:15px;padding:3px 0;}#cacherRunning{background-color:#4e5d6c;text-align:center;font-size:15px;padding:3px 0;}.checkbox label{display:inline-block;cursor:pointer;position:relative;padding-left:25px;margin-right:15px;font-size:13px;margin-bottom:10px;}.checkbox label:before{content:"";display:inline-block;width:18px;height:18px;margin-right:10px;position:absolute;left:0;bottom:1px;border:2px solid #eee;border-radius:3px;}.checkbox input[type=checkbox]{display:none;}.checkbox input[type=checkbox]:checked+label:before{content:"✓";font-size:13px;color:#fafafa;text-align:center;line-height:13px;}.small-checkbox label{display:inline-block;cursor:pointer;position:relative;padding-left:25px;margin-right:15px;font-size:13px;margin-bottom:10px;}.small-checkbox label:before{content:"";display:inline-block;width:18px;height:18px;margin-right:10px;position:absolute;left:0;bottom:1px;border:2px solid #eee;border-radius:8px;min-height:0 !important;}.small-checkbox input[type=checkbox]{display:none;}.small-checkbox input[type=checkbox]:checked+label:before{content:"✓";font-size:13px;color:#fafafa;text-align:center;line-height:13px;}.small-checkbox label{min-height:0 !important;padding-left:20px;margin-bottom:0;font-weight:normal;cursor:pointer;}.input-group-sm{padding-top:2px;padding-bottom:2px;}.tab-pane .form-horizontal .form-group{margin-right:15px;margin-left:15px;}.bootstrap-datetimepicker-widget.dropdown-menu{background-color:#4e5d6c;}.bootstrap-datetimepicker-widget.dropdown-menu.bottom:after{border-bottom:6px solid #4e5d6c !important;}.bootstrap-datetimepicker-widget table td.active,.bootstrap-datetimepicker-widget table td.active:hover{color:#fff !important;}.landing-header{display:block;margin:60px auto;}.landing-block{background:#2f2f2f !important;padding:5px;}.landing-block .media{margin:30px auto;max-width:450px;}.landing-block .media .media-left{display:inline-block;float:left;width:70px;}.landing-block .media .media-left i.fa{font-size:3em;}.landing-title{font-weight:bold;}.checkbox-custom{margin-top:0 !important;margin-bottom:0 !important;}.tooltip_templates{display:none;}.shadow{-moz-box-shadow:3px 3px 5px 6px #191919;-webkit-box-shadow:3px 3px 5px 6px #191919;box-shadow:3px 3px 5px 6px #191919;}.img-circle{border-radius:50%;}#wrapper{padding-left:0;-webkit-transition:all .5s ease;-moz-transition:all .5s ease;-o-transition:all .5s ease;transition:all .5s ease;}#wrapper.toggled{padding-right:250px;}#sidebar-wrapper{z-index:1000;position:fixed;right:250px;width:0;height:100%;margin-right:-250px;overflow-y:auto;background:#4e5d6c;padding-left:0;-webkit-transition:all .5s ease;-moz-transition:all .5s ease;-o-transition:all .5s ease;transition:all .5s ease;}#wrapper.toggled #sidebar-wrapper{width:500px;}#page-content-wrapper{width:100%;position:absolute;padding:15px;}#wrapper.toggled #page-content-wrapper{position:absolute;margin-left:-250px;}.sidebar-nav{position:absolute;top:0;width:500px;margin:0;padding-left:0;list-style:none;}.sidebar-nav li{text-indent:20px;line-height:40px;}.sidebar-nav li a{display:block;text-decoration:none;color:#999;}.sidebar-nav li a:hover{text-decoration:none;color:#fff;background:rgba(255,255,255,.2);}.sidebar-nav li a:active,.sidebar-nav li a:focus{text-decoration:none;}.sidebar-nav>.sidebar-brand{height:65px;font-size:18px;line-height:60px;}.sidebar-nav>.sidebar-brand a{color:#999;}.sidebar-nav>.sidebar-brand a:hover{color:#fff;background:none;}@media(min-width:768px){#wrapper{padding-right:250px;}#wrapper.toggled{padding-right:0;}#sidebar-wrapper{width:500px;}#wrapper.toggled #sidebar-wrapper{width:0;}#page-content-wrapper{padding:20px;position:relative;}#wrapper.toggled #page-content-wrapper{position:relative;margin-right:0;}}#lightbox{background-color:#808080;filter:alpha(opacity=50);opacity:.5;-moz-opacity:.5;top:0;left:0;z-index:20;height:100%;width:100%;background-repeat:no-repeat;background-position:center;position:absolute;}.list-group-item-dropdown{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#3e3e3e;border:1px solid transparent;}.wizard-heading{text-align:center;}.wizard-img{width:300px;display:block !important;margin:0 auto !important;}.pace{-webkit-pointer-events:none;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;}.pace-inactive{display:none;}.pace .pace-progress{background:#df691a;position:fixed;z-index:2000;top:0;right:100%;width:100%;height:5px;} \ No newline at end of file diff --git a/Ombi.UI/Content/base.scss b/Ombi.UI/Content/base.scss index 8c0c6e066..69c9c5aec 100644 --- a/Ombi.UI/Content/base.scss +++ b/Ombi.UI/Content/base.scss @@ -650,4 +650,27 @@ $border-radius: 10px; width: 300px; display: block $i; margin: 0 auto $i; +} + +.pace { + -webkit-pointer-events: none; + pointer-events: none; + + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; +} + +.pace-inactive { + display: none; +} + +.pace .pace-progress { + background: $primary-colour; + position: fixed; + z-index: 2000; + top: 0; + right: 100%; + width: 100%; + height: 5px; } \ No newline at end of file From 6b7f33d725ab7ca1f8af72726d40c6563d8828e9 Mon Sep 17 00:00:00 2001 From: d2dyno Date: Wed, 1 Feb 2017 03:36:21 -0600 Subject: [PATCH 4/9] Fix Radarr labels Fixed references to TV, instead of Movies. --- Ombi.UI/Views/Integration/Radarr.cshtml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Ombi.UI/Views/Integration/Radarr.cshtml b/Ombi.UI/Views/Integration/Radarr.cshtml index 3d4520e68..124fdf21c 100644 --- a/Ombi.UI/Views/Integration/Radarr.cshtml +++ b/Ombi.UI/Views/Integration/Radarr.cshtml @@ -64,10 +64,10 @@
- +
- +
@@ -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 5/9] 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 6/9] @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 7/9] 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 8/9] 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 9/9] 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 {