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.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
new file mode 100644
index 000000000..9751cc870
--- /dev/null
+++ b/Ombi.Services/Interfaces/IMassEmail.cs
@@ -0,0 +1,12 @@
+using Quartz;
+
+namespace Ombi.Services.Jobs
+{
+ public interface IMassEmail
+ {
+ void Execute(IJobExecutionContext context);
+ void MassEmailAdminTest(string html, string subject);
+ void SendMassEmail(string html, string subject);
+
+ }
+}
\ 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/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.Services/Jobs/RecentlyAdded.cs b/Ombi.Services/Jobs/RecentlyAdded.cs
index 563dd35a5..9ee52f231 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,12 +105,30 @@ namespace Ombi.Services.Jobs
Start();
}
- public void Test()
+ public void RecentlyAddedAdminTest()
{
- Log.Debug("Starting Test Newsletter");
+ Log.Debug("Starting Recently Added Newsletter Test");
var settings = NewsletterSettings.GetSettings();
Start(settings, true);
}
+ public void MassEmailAdminTest(string html, string subject)
+ {
+ Log.Debug("Starting Mass Email Test");
+ var settings = NewsletterSettings.GetSettings();
+ var plexSettings = PlexSettings.GetSettings();
+ var template = new MassEmailTemplate();
+ var body = template.LoadTemplate(html);
+ Send(settings, body, plexSettings, true, subject);
+ }
+ 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)
{
@@ -439,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();
@@ -454,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.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..02214c6af
--- /dev/null
+++ b/Ombi.Services/Jobs/Templates/MassEmailTemplate.html
@@ -0,0 +1,181 @@
+
+
+
+
+
+ Ombi
+
+
+
+
+
+
+
+
+
+
+ Ombi Recently Added
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {@MASSEMAIL}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Ombi.Services/Ombi.Services.csproj b/Ombi.Services/Ombi.Services.csproj
index 1735094dd..a37279f7d 100644
--- a/Ombi.Services/Ombi.Services.csproj
+++ b/Ombi.Services/Ombi.Services.csproj
@@ -87,6 +87,7 @@
+
@@ -107,6 +108,7 @@
+
@@ -180,6 +182,9 @@
+
+ PreserveNewest
+ PreserveNewest
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
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..388a66ef0 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; }
@@ -123,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,
@@ -158,6 +159,7 @@ namespace Ombi.UI.Modules.Admin
Analytics = analytics;
NotifySettings = notifyService;
RecentlyAdded = recentlyAdded;
+ MassEmail = massEmail;
WatcherSettings = watcherSettings;
DiscordSettings = discord;
DiscordApi = discordapi;
@@ -222,6 +224,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 +251,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()
@@ -441,11 +445,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 +470,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 +1174,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];
}
@@ -1229,13 +1243,13 @@ namespace Ombi.UI.Modules.Admin
var model = this.Bind();
return View["NotificationSettings", model];
}
-
- private Response RecentlyAddedTest()
+
+ private Response TestNewsletterAdminEmail()
{
try
{
- Log.Debug("Clicked TEST");
- RecentlyAdded.Test();
+ Log.Debug("Clicked Admin Newsletter Email Test");
+ RecentlyAdded.RecentlyAddedAdminTest();
return Response.AsJson(new JsonResponseModel { Result = true, Message = "Sent email to administrator" });
}
catch (Exception e)
@@ -1244,5 +1258,50 @@ namespace Ombi.UI.Modules.Admin
return Response.AsJson(new JsonResponseModel { Result = false, Message = e.Message });
}
}
+ private Response TestMassAdminEmail()
+ {
+ try
+ {
+ 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.MassEmailAdminTest(settings.Body.Replace("\n", " "), settings.Subject);
+ 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
+ {
+ 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)
+ {
+ Log.Error(e);
+ return Response.AsJson(new JsonResponseModel { Result = false, Message = e.Message });
+ }
+ }
}
}
\ No newline at end of file
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);
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/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/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")
+